feat(projects): add interactive AI app scaffold skill

This commit is contained in:
2026-09-04 20:36:28 +08:00
parent 336e0bb0ca
commit 959b6333b5
40 changed files with 2647 additions and 65 deletions

View File

@@ -24,6 +24,7 @@ export interface InspectedDevicePackage {
resolvedVersion: string | null;
skillEntries: DevicePackageSkillEntry[];
extensionEntries: string[];
hasSkillScripts: boolean;
ignoredLifecycleScripts: string[];
}
@@ -143,6 +144,25 @@ async function collectSkillFiles(root: string): Promise<string[]> {
return result;
}
async function directoryContainsFile(directory: string): Promise<boolean> {
for (const entry of await readdir(directory, { withFileTypes: true })) {
if (entry.isFile() || entry.isSymbolicLink()) return true;
if (entry.isDirectory() && await directoryContainsFile(path.join(directory, entry.name))) {
return true;
}
}
return false;
}
async function skillHasScripts(skillPath: string): Promise<boolean> {
const scriptsPath = path.join(path.dirname(skillPath), 'scripts');
const metadata = await stat(scriptsPath).catch((error: NodeJS.ErrnoException) => {
if (error.code === 'ENOENT') return null;
throw error;
});
return metadata?.isDirectory() === true && await directoryContainsFile(scriptsPath);
}
function manifestEntries(value: unknown, field: string): string[] {
if (value === undefined) return [];
const values = typeof value === 'string' ? [value] : value;
@@ -195,7 +215,8 @@ export async function inspectDevicePackage(
if (await existsFile(path.join(root, 'SKILL.md'))) skillFiles.push(path.join(root, 'SKILL.md'));
const uniqueSkills = new Map<string, DevicePackageSkillEntry>();
for (const skillPath of [...new Set(skillFiles.map((entry) => path.resolve(entry)))]) {
const uniqueSkillPaths = [...new Set(skillFiles.map((entry) => path.resolve(entry)))];
for (const skillPath of uniqueSkillPaths) {
const id = await skillId(skillPath);
if (uniqueSkills.has(id)) throw new Error(`Duplicate Skill id: ${id}`);
uniqueSkills.set(id, { id, entryPath: relativeContainedPath(root, skillPath) });
@@ -214,6 +235,7 @@ export async function inspectDevicePackage(
resolvedVersion: nonempty(packageJson?.version, 128) ?? nonempty(codexManifest?.version, 128),
skillEntries: [...uniqueSkills.values()].sort((left, right) => left.id.localeCompare(right.id)),
extensionEntries: extensionEntries.sort(),
hasSkillScripts: (await Promise.all(uniqueSkillPaths.map(skillHasScripts))).some(Boolean),
ignoredLifecycleScripts: LIFECYCLE_SCRIPT_NAMES.filter((name) => typeof scripts[name] === 'string'),
};
}