@antomanc/aginit 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/README.md +108 -28
  3. package/THIRD_PARTY_NOTICES.md +92 -16
  4. package/dist/adapters/agent-docs.d.ts +32 -0
  5. package/dist/adapters/agent-docs.d.ts.map +1 -0
  6. package/dist/adapters/agent-docs.js +146 -0
  7. package/dist/adapters/agent-docs.js.map +1 -0
  8. package/dist/adapters/agents-md.d.ts +13 -1
  9. package/dist/adapters/agents-md.d.ts.map +1 -1
  10. package/dist/adapters/agents-md.js +137 -120
  11. package/dist/adapters/agents-md.js.map +1 -1
  12. package/dist/adapters/e2e.d.ts +15 -0
  13. package/dist/adapters/e2e.d.ts.map +1 -0
  14. package/dist/adapters/e2e.js +55 -0
  15. package/dist/adapters/e2e.js.map +1 -0
  16. package/dist/adapters/skills.d.ts +5 -0
  17. package/dist/adapters/skills.d.ts.map +1 -1
  18. package/dist/adapters/skills.js +17 -0
  19. package/dist/adapters/skills.js.map +1 -1
  20. package/dist/adapters/tool-ignores.d.ts +9 -0
  21. package/dist/adapters/tool-ignores.d.ts.map +1 -0
  22. package/dist/adapters/tool-ignores.js +121 -0
  23. package/dist/adapters/tool-ignores.js.map +1 -0
  24. package/dist/cli/capabilities.d.ts +17 -0
  25. package/dist/cli/capabilities.d.ts.map +1 -0
  26. package/dist/cli/capabilities.js +149 -0
  27. package/dist/cli/capabilities.js.map +1 -0
  28. package/dist/cli/doctor.d.ts +16 -0
  29. package/dist/cli/doctor.d.ts.map +1 -1
  30. package/dist/cli/doctor.js +104 -70
  31. package/dist/cli/doctor.js.map +1 -1
  32. package/dist/cli/init.js +1 -1
  33. package/dist/cli/init.js.map +1 -1
  34. package/dist/cli/main.d.ts.map +1 -1
  35. package/dist/cli/main.js +123 -20
  36. package/dist/cli/main.js.map +1 -1
  37. package/dist/cli/new.d.ts +15 -1
  38. package/dist/cli/new.d.ts.map +1 -1
  39. package/dist/cli/new.js +163 -67
  40. package/dist/cli/new.js.map +1 -1
  41. package/dist/cli/wizard.d.ts +5 -0
  42. package/dist/cli/wizard.d.ts.map +1 -1
  43. package/dist/cli/wizard.js +104 -102
  44. package/dist/cli/wizard.js.map +1 -1
  45. package/dist/config/defaults.d.ts +14 -6
  46. package/dist/config/defaults.d.ts.map +1 -1
  47. package/dist/config/defaults.js +38 -174
  48. package/dist/config/defaults.js.map +1 -1
  49. package/dist/config/schema.d.ts +14 -0
  50. package/dist/config/schema.d.ts.map +1 -1
  51. package/dist/config/schema.js.map +1 -1
  52. package/dist/config/validation.d.ts +1 -0
  53. package/dist/config/validation.d.ts.map +1 -1
  54. package/dist/config/validation.js +63 -0
  55. package/dist/config/validation.js.map +1 -1
  56. package/dist/detect/index.d.ts +18 -0
  57. package/dist/detect/index.d.ts.map +1 -0
  58. package/dist/detect/index.js +120 -0
  59. package/dist/detect/index.js.map +1 -0
  60. package/dist/index.d.ts +5 -0
  61. package/dist/index.d.ts.map +1 -1
  62. package/dist/index.js +5 -0
  63. package/dist/index.js.map +1 -1
  64. package/dist/presets/cli.d.ts.map +1 -1
  65. package/dist/presets/cli.js +7 -0
  66. package/dist/presets/cli.js.map +1 -1
  67. package/dist/presets/generic.d.ts.map +1 -1
  68. package/dist/presets/generic.js +4 -1
  69. package/dist/presets/generic.js.map +1 -1
  70. package/dist/presets/testing.d.ts +1 -1
  71. package/dist/presets/testing.d.ts.map +1 -1
  72. package/dist/presets/testing.js +22 -8
  73. package/dist/presets/testing.js.map +1 -1
  74. package/dist/presets/web.d.ts.map +1 -1
  75. package/dist/presets/web.js +39 -5
  76. package/dist/presets/web.js.map +1 -1
  77. package/dist/registry/catalog.d.ts +13 -0
  78. package/dist/registry/catalog.d.ts.map +1 -0
  79. package/dist/registry/catalog.js +544 -0
  80. package/dist/registry/catalog.js.map +1 -0
  81. package/dist/registry/index.d.ts +84 -0
  82. package/dist/registry/index.d.ts.map +1 -0
  83. package/dist/registry/index.js +309 -0
  84. package/dist/registry/index.js.map +1 -0
  85. package/dist/registry/setup.d.ts +14 -0
  86. package/dist/registry/setup.d.ts.map +1 -0
  87. package/dist/registry/setup.js +25 -0
  88. package/dist/registry/setup.js.map +1 -0
  89. package/dist/registry/types.d.ts +115 -0
  90. package/dist/registry/types.d.ts.map +1 -0
  91. package/dist/registry/types.js +2 -0
  92. package/dist/registry/types.js.map +1 -0
  93. package/package.json +3 -2
  94. package/src/adapters/agent-docs.ts +184 -0
  95. package/src/adapters/agents-md.ts +160 -137
  96. package/src/adapters/e2e.ts +58 -0
  97. package/src/adapters/skills.ts +18 -0
  98. package/src/adapters/tool-ignores.ts +158 -0
  99. package/src/cli/capabilities.ts +176 -0
  100. package/src/cli/doctor.ts +100 -66
  101. package/src/cli/init.ts +1 -1
  102. package/src/cli/main.ts +130 -20
  103. package/src/cli/new.ts +199 -73
  104. package/src/cli/wizard.ts +130 -103
  105. package/src/config/defaults.ts +57 -179
  106. package/src/config/schema.ts +15 -0
  107. package/src/config/validation.ts +56 -0
  108. package/src/detect/index.ts +134 -0
  109. package/src/index.ts +5 -0
  110. package/src/presets/cli.ts +6 -0
  111. package/src/presets/generic.ts +4 -1
  112. package/src/presets/testing.ts +29 -12
  113. package/src/presets/web.ts +39 -5
  114. package/src/registry/catalog.ts +569 -0
  115. package/src/registry/index.ts +395 -0
  116. package/src/registry/setup.ts +40 -0
  117. package/src/registry/types.ts +109 -0
@@ -0,0 +1,158 @@
1
+ import path from 'node:path';
2
+ import { fileExists, readTextFile, safeWriteFile } from '../utils/fs.js';
3
+ import { readPackageManifest } from '../config/validation.js';
4
+
5
+ // Vendored skill sources ship third-party JS and Markdown that the project's own
6
+ // linters and formatters must not check. No ignore convention is shared across tools,
7
+ // so each tool below says where its ignores live and how to add to them.
8
+ const VENDORED_DIRS = ['.agents', '.claude'];
9
+
10
+ interface Project {
11
+ manifest: Record<string, any> | null;
12
+ has(dependency: string): boolean;
13
+ exists(file: string): boolean;
14
+ }
15
+
16
+ interface ToolIgnore {
17
+ /** The file holding this tool's ignores, when the project uses the tool. */
18
+ file(project: Project): string | undefined;
19
+ /** Vendored directories the file does not ignore yet. */
20
+ missing(text: string): string[];
21
+ /** The file with those directories ignored, or null when it cannot be edited safely. */
22
+ patch(text: string, dirs: string[]): string | null;
23
+ advice(file: string, dirs: string[]): string;
24
+ }
25
+
26
+ const configs = (base: string, extensions: string[]) => extensions.map((ext) => `${base}${ext}`);
27
+
28
+ /** Gitignore-syntax files: append the directories. */
29
+ function ignoreFile(name: string, used: (project: Project) => boolean): ToolIgnore {
30
+ return {
31
+ file: (project) => (used(project) ? name : undefined),
32
+ missing: (text) => {
33
+ const lines = text.split(/\r?\n/).map((line) => line.trim().replace(/^\/|\/(\*\*)?$/g, ''));
34
+ return VENDORED_DIRS.filter((dir) => !lines.includes(dir));
35
+ },
36
+ patch: (text, dirs) => {
37
+ const block = `# Vendored agent skills (aginit)\n${dirs.map((dir) => `${dir}/`).join('\n')}\n`;
38
+ return text.trim() ? `${text.trimEnd()}\n\n${block}` : block;
39
+ },
40
+ advice: (file, dirs) => `add ${dirs.map((dir) => `${dir}/`).join(' and ')} to ${file}`
41
+ };
42
+ }
43
+
44
+ const prettier = ignoreFile(
45
+ '.prettierignore',
46
+ (project) =>
47
+ project.has('prettier') ||
48
+ Boolean(project.manifest?.prettier) ||
49
+ [
50
+ ...configs('.prettierrc', ['', '.json', '.json5', '.yaml', '.yml', '.toml']),
51
+ ...configs('.prettierrc', ['.js', '.cjs', '.mjs', '.ts', '.cts', '.mts']),
52
+ ...configs('prettier.config', ['.js', '.cjs', '.mjs', '.ts', '.cts', '.mts'])
53
+ ].some(project.exists)
54
+ );
55
+
56
+ // Legacy ESLint and oxlint read .eslintignore; flat-config ESLint does not.
57
+ const eslintIgnore = ignoreFile(
58
+ '.eslintignore',
59
+ (project) =>
60
+ project.has('oxlint') ||
61
+ Boolean(project.manifest?.eslintConfig) ||
62
+ configs('.eslintrc', ['', '.js', '.cjs', '.json', '.yaml', '.yml']).some(project.exists)
63
+ );
64
+
65
+ /** Flat ESLint config: extend a global ignores array if there is an unambiguous one,
66
+ * `globalIgnores([...])` or an object holding only `ignores`. */
67
+ const eslintFlat: ToolIgnore = {
68
+ file: (project) =>
69
+ configs('eslint.config', ['.js', '.mjs', '.cjs', '.ts', '.mts', '.cts']).find(project.exists),
70
+ missing: (text) => VENDORED_DIRS.filter((dir) => !text.includes(`${dir}/`)),
71
+ patch: (text, dirs) => {
72
+ const match =
73
+ /globalIgnores\(\s*\[/.exec(text) ??
74
+ /\{\s*ignores\s*:\s*\[(?=[^[\]]*\]\s*,?\s*\})/.exec(text);
75
+ if (!match) return null;
76
+ const at = match.index + match[0].length;
77
+ const rest = text.slice(at);
78
+ const quote =
79
+ /^\s*(['"`])/.exec(rest)?.[1] ??
80
+ ((text.match(/"/g)?.length ?? 0) > (text.match(/'/g)?.length ?? 0) ? '"' : "'");
81
+ const entries = dirs.map((dir) => `${quote}${dir}/**${quote}`);
82
+ const indent = /^(\s*\n[ \t]*)/.exec(rest)?.[1];
83
+ const insertion = indent
84
+ ? entries.map((entry) => `${indent}${entry},`).join('')
85
+ : entries.join(', ') + (/^\s*\]/.test(rest) ? '' : ', ');
86
+ return text.slice(0, at) + insertion + rest;
87
+ },
88
+ advice: (file, dirs) =>
89
+ `add { ignores: [${dirs.map((dir) => `'${dir}/**'`).join(', ')}] } to the exported array in ${file}`
90
+ };
91
+
92
+ /** Biome 2.2+: force-ignore the folders through files.includes. */
93
+ const biome: ToolIgnore = {
94
+ file: (project) => ['biome.json', 'biome.jsonc'].find(project.exists),
95
+ missing: (text) => VENDORED_DIRS.filter((dir) => !text.includes(`**/${dir}`)),
96
+ patch: (text, dirs) => {
97
+ let config: Record<string, any>;
98
+ try {
99
+ config = JSON.parse(text); // a biome.jsonc with comments gets advice instead
100
+ } catch {
101
+ return null;
102
+ }
103
+ const includes = config.files?.includes ?? ['**'];
104
+ if (!Array.isArray(includes)) return null;
105
+ config.files = {
106
+ ...config.files,
107
+ includes: [...includes, ...dirs.map((dir) => `!!**/${dir}`)]
108
+ };
109
+ const json = JSON.stringify(config, null, /\n([ \t]+)"/.exec(text)?.[1] ?? 2) + '\n';
110
+ // Like Biome's formatter, keep the array on one line when it fits in 80 columns.
111
+ return json.replace(/^([ \t]*"includes": )\[[^\]]*\]/m, (block, key: string) => {
112
+ const inline = `${key}[${config.files.includes.map((p: string) => JSON.stringify(p)).join(', ')}]`;
113
+ return inline.replace(/\t/g, ' ').length <= 80 ? inline : block;
114
+ });
115
+ },
116
+ advice: (file, dirs) =>
117
+ `add ${dirs.map((dir) => `"!!**/${dir}"`).join(', ')} to files.includes in ${file}`
118
+ };
119
+
120
+ const TOOLS = [prettier, eslintIgnore, eslintFlat, biome];
121
+
122
+ function pendingIgnores(targetDir: string) {
123
+ const manifest = readPackageManifest(targetDir);
124
+ const project: Project = {
125
+ manifest,
126
+ has: (name) => Boolean(manifest?.dependencies?.[name] || manifest?.devDependencies?.[name]),
127
+ exists: (file) => fileExists(path.join(targetDir, file))
128
+ };
129
+ return TOOLS.flatMap((tool) => {
130
+ const file = tool.file(project);
131
+ if (!file) return [];
132
+ const text = readTextFile(path.join(targetDir, file)) ?? '';
133
+ const missing = tool.missing(text);
134
+ return missing.length > 0 ? [{ tool, file, text, missing }] : [];
135
+ });
136
+ }
137
+
138
+ /** Keeps the project's linters and formatters off vendored skills. Returns the
139
+ * advice for configs that could not be edited safely. */
140
+ export function ignoreVendoredSkills(
141
+ targetDir: string,
142
+ options: { dryRun?: boolean; silent?: boolean } = {}
143
+ ): string[] {
144
+ const { dryRun = false, silent = false } = options;
145
+ const advice: string[] = [];
146
+ for (const { tool, file, text, missing } of pendingIgnores(targetDir)) {
147
+ const patched = tool.patch(text, missing);
148
+ if (patched === null)
149
+ advice.push(`Linters check vendored skills: ${tool.advice(file, missing)}`);
150
+ else safeWriteFile(path.join(targetDir, file), patched, { overwrite: true, dryRun, silent });
151
+ }
152
+ return advice;
153
+ }
154
+
155
+ /** Doctor view: every config that still lets a linter or formatter see vendored skills. */
156
+ export function vendoredSkillsExposure(targetDir: string): string[] {
157
+ return pendingIgnores(targetDir).map(({ tool, file, missing }) => tool.advice(file, missing));
158
+ }
@@ -0,0 +1,176 @@
1
+ import path from 'node:path';
2
+ import pc from 'picocolors';
3
+ import { AGINIT_CONFIG_FILENAME, AginitConfig } from '../config/schema.js';
4
+ import { readProjectConfig, validateConfig } from '../config/validation.js';
5
+ import { resolvePlatformStack } from '../adapters/agent-docs.js';
6
+ import { installAllSkills, removeSkills, skillAgents } from '../adapters/skills.js';
7
+ import { uninstallGraft } from '../adapters/graft.js';
8
+ import { ignoreVendoredSkills } from '../adapters/tool-ignores.js';
9
+ import {
10
+ activeIds,
11
+ applyResolvedProfile,
12
+ CapabilityCategory,
13
+ CapabilityDescriptor,
14
+ catalogFor,
15
+ findCapability,
16
+ inferCapabilities,
17
+ ProfileFlags,
18
+ resolveCapabilities,
19
+ sourcesFor
20
+ } from '../registry/index.js';
21
+ import { SETUPS } from '../registry/setup.js';
22
+ import { writeJsonFile } from '../utils/fs.js';
23
+ import { logger } from '../utils/logger.js';
24
+ import { missingBinaries, syncAgentDocs } from './new.js';
25
+
26
+ const CATEGORY_ORDER: CapabilityCategory[] = [
27
+ 'codebase-graph',
28
+ 'methodology',
29
+ 'discipline',
30
+ 'design',
31
+ 'ui-driver',
32
+ 'testing-engine',
33
+ 'context-guard',
34
+ 'custom'
35
+ ];
36
+
37
+ /** Print the registry grouped by category, marking what the project has enabled. */
38
+ export function listCapabilities(targetDir: string = process.cwd()): void {
39
+ const config = readProjectConfig(targetDir);
40
+ const enabled = config ? activeIds(config) : new Set<string>();
41
+ logger.banner(
42
+ 'Aginit — Capability registry',
43
+ config ? `Enabled in ${AGINIT_CONFIG_FILENAME}: ●` : `No ${AGINIT_CONFIG_FILENAME} here; showing the catalog`
44
+ );
45
+ const catalog = catalogFor(config);
46
+ for (const category of CATEGORY_ORDER) {
47
+ const entries = catalog.filter((c) => c.category === category);
48
+ if (entries.length === 0) continue;
49
+ console.log(pc.bold(pc.underline(category)));
50
+ for (const c of entries) {
51
+ const mark = enabled.has(c.id) ? pc.green('●') : pc.dim('○');
52
+ const platforms = c.supportedPlatforms.join(', ');
53
+ console.log(` ${mark} ${pc.bold(c.id.padEnd(22))} ${c.description}`);
54
+ console.log(pc.dim(` ${platforms} · ${c.license} · ${c.repoUrl}`));
55
+ }
56
+ console.log();
57
+ }
58
+ }
59
+
60
+ export interface ChangeOptions {
61
+ dryRun?: boolean;
62
+ silent?: boolean;
63
+ }
64
+
65
+ function requireConfig(targetDir: string): AginitConfig {
66
+ const config = readProjectConfig(targetDir);
67
+ if (!config)
68
+ throw new Error(`No ${AGINIT_CONFIG_FILENAME} in ${targetDir}. Run \`aginit init\` first.`);
69
+ return config;
70
+ }
71
+
72
+ /** Skill names to remove; a source installed whole is removed by its repo. */
73
+ const skillsOf = (capabilities: CapabilityDescriptor[]) =>
74
+ capabilities.flatMap((c) =>
75
+ c.install.flatMap((s) => (s.kind !== 'skills-sh' ? [] : s.skills.length ? s.skills : [s.repo]))
76
+ );
77
+
78
+ /**
79
+ * Enable or disable capabilities, then bring the project in line: config,
80
+ * skills (installed or removed through the Skills CLI), upstream setups,
81
+ * AGENTS.md and docs/agents/. Picking a methodology, design or UI driver
82
+ * replaces the current one in that category.
83
+ */
84
+ export async function changeCapabilities(
85
+ targetDir: string,
86
+ change: { add?: string[]; remove?: string[] },
87
+ options: ChangeOptions = {}
88
+ ): Promise<boolean> {
89
+ const { dryRun = false, silent = false } = options;
90
+ const config = requireConfig(targetDir);
91
+ const { platform, stack } = resolvePlatformStack(config, targetDir);
92
+ const base = config.capabilities ?? inferCapabilities(config);
93
+ const previous = [...base, ...(config.customCapabilities ?? []).map((c) => c.id)];
94
+ const before = activeIds(config);
95
+
96
+ const flags: ProfileFlags = { addons: [], addSkills: [], without: [] };
97
+ for (const id of change.add ?? []) {
98
+ const capability = findCapability(id, config);
99
+ if (!capability) {
100
+ if (id.includes('/')) flags.addSkills!.push(id);
101
+ else throw new Error(`Unknown capability "${id}". Run \`aginit list\` to see the registry.`);
102
+ } else if (capability.category === 'methodology') flags.methodology = id;
103
+ else if (capability.category === 'design') flags.design = id;
104
+ else if (capability.category === 'ui-driver' && !capability.addon) flags.uiDriver = id;
105
+ else flags.addons!.push(id);
106
+ }
107
+ for (const id of change.remove ?? []) {
108
+ if (!findCapability(id, config))
109
+ throw new Error(`Unknown capability "${id}". Run \`aginit list\` to see the registry.`);
110
+ flags.without!.push(id);
111
+ }
112
+ const custom = config.customCapabilities ?? [];
113
+ const customIds = new Set(custom.map((c) => c.id));
114
+ const disabled = (config.disabledCapabilities ?? []).filter((id) => !(change.add ?? []).includes(id));
115
+ const resolved = resolveCapabilities(
116
+ { platform, stack, framework: config.framework, base, basePlatform: platform, flags, custom },
117
+ disabled
118
+ );
119
+ // A removed custom capability is gone, not disabled.
120
+ resolved.disabledCapabilities = resolved.disabledCapabilities.filter((id) => !customIds.has(id));
121
+ const next = validateConfig(
122
+ applyResolvedProfile({ ...config, platform, stack }, resolved, { skills: true, previous })
123
+ );
124
+ // Adding graft or playwright turns their legacy switch back on.
125
+ if (next.capabilities?.includes('graft')) next.codebase.graft = true;
126
+
127
+ const after = activeIds(next);
128
+ const nextCatalog = catalogFor(next);
129
+ const added = nextCatalog.filter((c) => after.has(c.id) && !before.has(c.id));
130
+ const removed = catalogFor(config).filter((c) => before.has(c.id) && !after.has(c.id));
131
+ if (added.length === 0 && removed.length === 0 && JSON.stringify(next) === JSON.stringify(config)) {
132
+ if (!silent) logger.info('Nothing to change.');
133
+ return true;
134
+ }
135
+ if (!silent) {
136
+ for (const c of added) logger.success(`Enabling ${c.id}`);
137
+ for (const c of removed) logger.warn(`Disabling ${c.id}`);
138
+ }
139
+
140
+ writeJsonFile(path.join(targetDir, AGINIT_CONFIG_FILENAME), next, { overwrite: true, dryRun, silent });
141
+ let ok = true;
142
+
143
+ ok = (await removeSkills(targetDir, skillsOf(removed), { dryRun, silent })) && ok;
144
+ if (removed.some((c) => c.id === 'graft')) await uninstallGraft(targetDir, { dryRun, silent });
145
+ const leftovers = removed.filter((c) =>
146
+ c.install.some((s) => s.kind === 'setup' && s.setup !== 'graft')
147
+ );
148
+ if (!silent)
149
+ for (const c of leftovers)
150
+ logger.dim(`${c.id}: files and global CLIs its setup created are left in place; remove them by hand.`);
151
+
152
+ const addedSources = sourcesFor(added);
153
+ if (addedSources.length > 0) {
154
+ const { failed } = await installAllSkills(targetDir, addedSources, {
155
+ agents: skillAgents(next),
156
+ dryRun,
157
+ silent
158
+ });
159
+ ok = failed.length === 0 && ok;
160
+ for (const line of ignoreVendoredSkills(targetDir, { dryRun, silent: true })) logger.warn(line);
161
+ }
162
+ for (const c of added)
163
+ for (const step of c.install)
164
+ if (step.kind === 'setup')
165
+ ok = (await SETUPS[step.setup]({ targetDir, config: next, platform, stack, dryRun, silent })) && ok;
166
+
167
+ syncAgentDocs(targetDir, next, { dryRun, silent });
168
+ if (!silent)
169
+ for (const { name, bin, install } of await missingBinaries(added, ['graft', 'agent-browser', 'e2e']))
170
+ logger.warn(`${name} needs the \`${bin}\` CLI: ${install}`);
171
+ if (!silent) {
172
+ if (ok) logger.success('Capabilities updated. AGENTS.md and docs/agents/ are in sync.');
173
+ else logger.warn('Capabilities updated with warnings. Resolve them and rerun `aginit init`.');
174
+ }
175
+ return ok;
176
+ }
package/src/cli/doctor.ts CHANGED
@@ -17,17 +17,67 @@ import {
17
17
  } from '../adapters/skills.js';
18
18
  import { claudeMarkdownImportsAgents } from '../adapters/agents-md.js';
19
19
  import {
20
- agentBrowserConfigured,
21
20
  agentBrowserStubInstalled,
22
21
  findUsernsRestriction,
23
22
  runAgentBrowserDiagnostics
24
23
  } from '../adapters/agent-browser.js';
25
24
  import { readProjectConfig } from '../config/validation.js';
25
+ import { vendoredSkillsExposure } from '../adapters/tool-ignores.js';
26
26
  import { logger } from '../utils/logger.js';
27
+ import { activeCapabilities, activeIds, doctorChecks, isNodeStack } from '../registry/index.js';
28
+ import { resolvePlatformStack } from '../adapters/agent-docs.js';
29
+
30
+ /**
31
+ * One row per active capability, built from its registry entry: every skill
32
+ * file it installs, every CLI it drives and any artifact it declares.
33
+ */
34
+ export async function capabilityChecks(
35
+ targetDir: string,
36
+ config: AginitConfig
37
+ ): Promise<CheckItem[]> {
38
+ const items: CheckItem[] = [];
39
+ for (const capability of activeCapabilities(config)) {
40
+ const problems: string[] = [];
41
+ const fixes: string[] = [];
42
+ const missingSkills: string[] = [];
43
+ const versions: string[] = [];
44
+ for (const check of doctorChecks(capability, config.skills.sources)) {
45
+ if (check.kind === 'file') {
46
+ if (fileExists(path.join(targetDir, check.path))) continue;
47
+ const skill = /^\.agents\/skills\/([^/]+)\/SKILL\.md$/.exec(check.path)?.[1];
48
+ if (skill) missingSkills.push(skill);
49
+ else {
50
+ problems.push(`${check.path} missing`);
51
+ fixes.push(check.fix);
52
+ }
53
+ } else if (!(await commandExists(check.bin))) {
54
+ problems.push(`${check.bin} CLI not found in PATH`);
55
+ fixes.push(check.fix);
56
+ } else {
57
+ const res = await runExecutable(check.bin, check.args, { silent: true, timeout: 30_000 });
58
+ versions.push(res.ok && res.stdout ? `${check.bin} ${res.stdout.split('\n')[0]}` : check.bin);
59
+ }
60
+ }
61
+ if (missingSkills.length) {
62
+ problems.unshift(`skills missing from .agents/skills/: ${missingSkills.join(', ')}`);
63
+ fixes.unshift('Run `aginit init` to reinstall the configured skills');
64
+ }
65
+ items.push({
66
+ name: `${capability.name} (${capability.category})`,
67
+ category: 'Capabilities',
68
+ ok: problems.length === 0,
69
+ detail: problems.length ? problems.join('; ') : `Ready${versions.length ? ` (${versions.join(', ')})` : ''}`,
70
+ suggestion: fixes.length ? fixes.join(' · ') : undefined
71
+ });
72
+ }
73
+ return items;
74
+ }
75
+
76
+ type CheckCategory = 'Environment' | 'AI Tooling' | 'Project Config' | 'Capabilities';
27
77
 
28
78
  interface CheckItem {
29
79
  name: string;
30
- category: 'Environment' | 'AI Tooling' | 'Project Config';
80
+ category: CheckCategory;
31
81
  ok: boolean;
32
82
  version?: string;
33
83
  detail: string;
@@ -87,9 +137,14 @@ export async function runDoctor(targetDir: string = process.cwd()): Promise<void
87
137
  suggestion: nodeSuggestion
88
138
  });
89
139
 
90
- // 2. Active Package Manager check (pnpm, npm, yarn, or bun)
91
- const pmRes = await runExecutable(activePm, ['-v'], { silent: true });
92
- checks.push({
140
+ // 2. Active Package Manager check (pnpm, npm, yarn, or bun); none for non-Node stacks
141
+ const { stack, platform } = projectConfig
142
+ ? resolvePlatformStack(projectConfig, targetDir)
143
+ : { stack: 'node' as const, platform: null };
144
+ const pmRes = isNodeStack(stack)
145
+ ? await runExecutable(activePm, ['-v'], { silent: true })
146
+ : null;
147
+ if (pmRes) checks.push({
93
148
  name: `${activePm} package manager`,
94
149
  category: 'Environment',
95
150
  ok: pmRes.ok,
@@ -132,22 +187,6 @@ export async function runDoctor(targetDir: string = process.cwd()): Promise<void
132
187
  : undefined
133
188
  });
134
189
 
135
- // 4. Graft CLI check
136
- const graftExists = await commandExists('graft');
137
- let graftVersion = '';
138
- if (graftExists) {
139
- const gv = await runExecutable('graft', ['--version'], { silent: true });
140
- graftVersion = gv.stdout;
141
- }
142
- checks.push({
143
- name: 'Graft codebase graph CLI',
144
- category: 'AI Tooling',
145
- ok: graftExists,
146
- version: graftVersion,
147
- detail: graftExists ? `Installed globally (${graftVersion})` : 'Graft CLI not found in PATH',
148
- suggestion: graftExists ? undefined : 'Install Graft: `npm install -g @nanonets/graft`'
149
- });
150
-
151
190
  // 5. Codex CLI check
152
191
  const codexExists = await commandExists('codex');
153
192
  checks.push({
@@ -175,7 +214,7 @@ export async function runDoctor(targetDir: string = process.cwd()): Promise<void
175
214
  category: 'Project Config',
176
215
  ok: !!projectConfig,
177
216
  detail: projectConfig
178
- ? `Preset: "${projectConfig.preset}" (v${projectConfig.schemaVersion || '1.0.0'}), pm: ${activePm}, agents: ${projectConfig.agents.primary}/${projectConfig.agents.secondary}${claudeCodeEnabled(projectConfig) ? '/claude-code' : ''}`
217
+ ? `Preset: "${projectConfig.preset}" (v${projectConfig.schemaVersion || '1.0.0'}), platform: ${platform} (${stack})${pmRes ? `, pm: ${activePm}` : ''}, agents: ${projectConfig.agents.primary}/${projectConfig.agents.secondary}${claudeCodeEnabled(projectConfig) ? '/claude-code' : ''}`
179
218
  : configError
180
219
  ? `Invalid configuration: ${configError}`
181
220
  : 'No aginit.config.json found in current directory',
@@ -226,6 +265,22 @@ export async function runDoctor(targetDir: string = process.cwd()): Promise<void
226
265
  : 'Run `aginit init` or `npx skills add ...` to install skills'
227
266
  });
228
267
 
268
+ // 9a. Vendored skills must stay out of the project's linters and formatters
269
+ if (installedSkills.length > 0) {
270
+ const exposure = vendoredSkillsExposure(targetDir);
271
+ checks.push({
272
+ name: 'Lint/format ignores for vendored skills',
273
+ category: 'Project Config',
274
+ ok: exposure.length === 0,
275
+ detail:
276
+ exposure.length === 0
277
+ ? 'Detected linters and formatters skip .agents/ and .claude/'
278
+ : 'Project linters or formatters will check vendored skill files',
279
+ suggestion:
280
+ exposure.length === 0 ? undefined : `Run \`aginit update\`, or ${exposure.join('; ')}`
281
+ });
282
+ }
283
+
229
284
  // 9b. Claude Code: skills linked into .claude/skills/ and AGENTS.md reachable
230
285
  if (projectConfig !== null && claudeCodeEnabled(projectConfig)) {
231
286
  if (!skillsDisabled && installedSkills.length > 0) {
@@ -273,47 +328,29 @@ export async function runDoctor(targetDir: string = process.cwd()): Promise<void
273
328
  });
274
329
  }
275
330
 
331
+ // 10. Registry: each active capability's checks (skill files, CLIs, artifacts)
332
+ if (projectConfig !== null) checks.push(...(await capabilityChecks(targetDir, projectConfig)));
333
+
276
334
  // 10. agent-browser runtime (only when the project ships the skill)
277
335
  const agentBrowserWanted =
278
336
  projectConfig !== null &&
279
- (agentBrowserConfigured(projectConfig.skills.sources) || agentBrowserStubInstalled(targetDir));
280
- if (agentBrowserWanted) {
281
- const abExists = await commandExists('agent-browser');
282
- let abVersion = '';
283
- if (abExists) {
284
- const av = await runExecutable('agent-browser', ['--version'], { silent: true });
285
- abVersion = av.ok ? av.stdout : '';
286
- }
287
- checks.push({
288
- name: 'agent-browser CLI',
289
- category: 'AI Tooling',
290
- ok: abExists,
291
- version: abVersion,
292
- detail: abExists
293
- ? `Installed globally (${abVersion || 'version unknown'})`
294
- : 'agent-browser skill is installed but the CLI is not in PATH',
295
- suggestion: abExists
296
- ? undefined
297
- : 'Install the runtime: `npm i -g agent-browser && agent-browser install`'
298
- });
299
-
300
- if (abExists) {
301
- const diagnostics = await runAgentBrowserDiagnostics();
302
- if (diagnostics !== null) {
303
- checks.push({
304
- name: 'agent-browser browser runtime',
305
- category: 'AI Tooling',
306
- ok: diagnostics.length === 0,
307
- detail:
308
- diagnostics.length === 0
309
- ? 'Upstream doctor reports browser and launch environment healthy'
310
- : diagnostics.map((d) => d.message).join('; '),
311
- suggestion:
312
- diagnostics.length === 0
313
- ? undefined
314
- : (diagnostics.find((d) => d.fix)?.fix ?? 'Run `agent-browser doctor` for details')
315
- });
316
- }
337
+ (activeIds(projectConfig).has('agent-browser') || agentBrowserStubInstalled(targetDir));
338
+ if (agentBrowserWanted && (await commandExists('agent-browser'))) {
339
+ const diagnostics = await runAgentBrowserDiagnostics();
340
+ if (diagnostics !== null) {
341
+ checks.push({
342
+ name: 'agent-browser browser runtime',
343
+ category: 'AI Tooling',
344
+ ok: diagnostics.length === 0,
345
+ detail:
346
+ diagnostics.length === 0
347
+ ? 'Upstream doctor reports browser and launch environment healthy'
348
+ : diagnostics.map((d) => d.message).join('; '),
349
+ suggestion:
350
+ diagnostics.length === 0
351
+ ? undefined
352
+ : (diagnostics.find((d) => d.fix)?.fix ?? 'Run `agent-browser doctor` for details')
353
+ });
317
354
  }
318
355
  }
319
356
 
@@ -341,15 +378,12 @@ export async function runDoctor(targetDir: string = process.cwd()): Promise<void
341
378
  }
342
379
 
343
380
  // Print grouped results
344
- const categories: Array<'Environment' | 'AI Tooling' | 'Project Config'> = [
345
- 'Environment',
346
- 'AI Tooling',
347
- 'Project Config'
348
- ];
381
+ const categories: CheckCategory[] = ['Environment', 'AI Tooling', 'Project Config', 'Capabilities'];
349
382
 
350
383
  for (const cat of categories) {
351
- console.log(pc.bold(pc.underline(cat)));
352
384
  const catChecks = checks.filter((c) => c.category === cat);
385
+ if (catChecks.length === 0) continue;
386
+ console.log(pc.bold(pc.underline(cat)));
353
387
  for (const c of catChecks) {
354
388
  const icon = c.ok ? pc.green('✓') : pc.yellow('⚠');
355
389
  const name = pc.bold(c.name);
package/src/cli/init.ts CHANGED
@@ -24,7 +24,7 @@ export async function initCurrentDirectory(options: NewProjectOptions = {}): Pro
24
24
  else projectName = sanitizeProjectName(projectName);
25
25
 
26
26
  const packageManager =
27
- options.packageManager ?? saved?.packageManager ?? detectPackageManager(currentDir) ?? 'pnpm';
27
+ options.packageManager ?? saved?.packageManager ?? detectPackageManager(currentDir) ?? undefined;
28
28
 
29
29
  return createProject(projectName, {
30
30
  ...options,