@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
package/src/cli/main.ts CHANGED
@@ -3,12 +3,15 @@ import { readFileSync } from 'node:fs';
3
3
  import { createProject } from './new.js';
4
4
  import { initCurrentDirectory } from './init.js';
5
5
  import { runDoctor } from './doctor.js';
6
- import { claudeCodeEnabled, ensureClaudeSkillLinks, updateSkills } from '../adapters/skills.js';
7
- import { setupClaudeMarkdown } from '../adapters/agents-md.js';
6
+ import { ensureClaudeSkillLinks, updateSkills } from '../adapters/skills.js';
7
+ import { ignoreVendoredSkills } from '../adapters/tool-ignores.js';
8
8
  import { buildGraft } from '../adapters/graft.js';
9
9
  import { readProjectConfig } from '../config/validation.js';
10
10
  import { logger } from '../utils/logger.js';
11
11
  import { PresetType, WebFramework, PackageManager } from '../config/schema.js';
12
+ import { Platform, ProfileFlags, TestingChoice } from '../registry/index.js';
13
+ import { changeCapabilities, listCapabilities } from './capabilities.js';
14
+ import { syncAgentDocs } from './new.js';
12
15
  import {
13
16
  isInteractive,
14
17
  promptNewProject,
@@ -29,25 +32,85 @@ function readVersion(): string {
29
32
  }
30
33
  }
31
34
 
35
+ const list = (value: string, previous: string[] = []) => [
36
+ ...previous,
37
+ ...value
38
+ .split(',')
39
+ .map((item) => item.trim())
40
+ .filter(Boolean)
41
+ ];
42
+
43
+ /** v2 profile options shared by the root command, `new` and `init`. */
44
+ function withProfileOptions(command: Command): Command {
45
+ return command
46
+ .option('--platform <platform>', 'Platform: auto | web | mobile-native | mobile-rn | backend | cli')
47
+ .option('--methodology <preset>', 'Workflow skills: pocock | superpowers | minimal')
48
+ .option('--design <design>', 'Design system: impeccable | none')
49
+ .option('--testing <engine>', 'Testing: e2e | playwright | vitest | native')
50
+ .option('--ui-driver <driver>', 'UI driver: auto | agent-browser | agent-device | none')
51
+ .option('--addons <ids>', 'Add-ons: context-mode, worktrunk, chrome-devtools-mcp, browserskill', list)
52
+ .option('--add-skill <owner/repo>', 'Add a skill repository as a custom capability (repeatable)', list)
53
+ .option('--without <id>', 'Disable a capability, e.g. graft or humanizer (repeatable)', list);
54
+ }
55
+
56
+ function profileFlags(options: any): ProfileFlags {
57
+ return {
58
+ platform: options.platform === 'auto' ? undefined : (options.platform as Platform | undefined),
59
+ methodology: options.methodology,
60
+ design: options.design,
61
+ testing: options.testing as TestingChoice | undefined,
62
+ uiDriver: options.uiDriver,
63
+ addons: options.addons,
64
+ addSkills: options.addSkill,
65
+ without: options.without
66
+ };
67
+ }
68
+
32
69
  export function createCliProgram(): Command {
33
70
  const program = new Command();
34
71
 
35
72
  program
36
73
  .name('aginit')
37
74
  .description('Set up a new or existing project for AI coding agents')
38
- .version(readVersion());
39
-
40
- // Root action: interactive wizard when in TTY, else display help
41
- program.action(async () => {
42
- if (isInteractive()) {
43
- await runInteractiveWizard();
44
- } else {
45
- program.outputHelp();
75
+ .version(readVersion())
76
+ .enablePositionalOptions()
77
+ .option('-p, --preset <preset>', 'Project preset: web | cli | generic')
78
+ .option('-f, --framework <framework>', 'Web framework: none | vite | next | existing')
79
+ .option('-m, --package-manager <pm>', 'Package manager: pnpm | npm | yarn | bun')
80
+ .option('--no-skills', 'Skip installing AI skills')
81
+ .option('--no-graft', 'Skip codebase graph (Graft) setup')
82
+ .option('--no-git', 'Skip git repository initialization')
83
+ .option('--dry-run', 'Simulate setup without writing changes to disk')
84
+ .option('-y, --yes', 'Initialize the current directory without prompts (saved or detected settings)');
85
+ withProfileOptions(program);
86
+
87
+ // Root action: `aginit [options]` initializes the current directory. Bare
88
+ // `aginit` opens the wizard in a TTY and prints help otherwise, so a stray
89
+ // invocation in CI never rewrites a project.
90
+ program.action(async (options: any, command: Command) => {
91
+ const explicit = command.options.some(
92
+ (option) => command.getOptionValueSource(option.attributeName()) === 'cli'
93
+ );
94
+ if (!explicit) {
95
+ if (isInteractive()) await runInteractiveWizard();
96
+ else program.outputHelp();
97
+ return;
46
98
  }
99
+ const complete = await initCurrentDirectory({
100
+ preset: options.preset as PresetType | undefined,
101
+ framework: options.framework as WebFramework | undefined,
102
+ packageManager: options.packageManager as PackageManager | undefined,
103
+ skills: options.skills,
104
+ graft: options.graft,
105
+ git: options.git,
106
+ dryRun: options.dryRun,
107
+ profile: profileFlags(options)
108
+ });
109
+ if (!complete && !options.dryRun) process.exitCode = 1;
47
110
  });
48
111
 
49
112
  // Command: new
50
- program
113
+ const newCommand = program
51
114
  .command('new [projectName]')
52
115
  .description('Create a brand new AI-first project')
53
116
  .option('-p, --preset <preset>', 'Project preset: web | cli | generic', 'generic')
@@ -56,12 +119,14 @@ export function createCliProgram(): Command {
56
119
  .option('--no-skills', 'Skip installing AI skills')
57
120
  .option('--no-graft', 'Skip codebase graph (Graft) setup')
58
121
  .option('--no-git', 'Skip git repository initialization')
59
- .option('--dry-run', 'Simulate creation without writing changes to disk')
122
+ .option('--dry-run', 'Simulate creation without writing changes to disk');
123
+ withProfileOptions(newCommand)
60
124
  .action(async (projectName: string | undefined, options: any, command: Command) => {
61
125
  let finalName = projectName;
62
126
  let finalPreset = options.preset;
63
127
  let finalFramework = options.framework;
64
128
  let finalPackageManager = options.packageManager;
129
+ let finalProfile = profileFlags(options);
65
130
 
66
131
  if (!finalName) {
67
132
  if (!isInteractive()) {
@@ -79,12 +144,14 @@ export function createCliProgram(): Command {
79
144
  packageManager:
80
145
  command.getOptionValueSource('packageManager') === 'cli'
81
146
  ? (options.packageManager as PackageManager)
82
- : undefined
147
+ : undefined,
148
+ profile: finalProfile
83
149
  });
84
150
  finalName = answers.projectName;
85
151
  finalPreset = answers.preset;
86
152
  finalFramework = answers.framework;
87
153
  finalPackageManager = answers.packageManager;
154
+ finalProfile = answers.profile;
88
155
  }
89
156
 
90
157
  const complete = await createProject(finalName, {
@@ -94,13 +161,14 @@ export function createCliProgram(): Command {
94
161
  skills: options.skills,
95
162
  graft: options.graft,
96
163
  git: options.git,
97
- dryRun: options.dryRun
164
+ dryRun: options.dryRun,
165
+ profile: finalProfile
98
166
  });
99
167
  if (!complete && !options.dryRun) process.exitCode = 1;
100
168
  });
101
169
 
102
170
  // Command: init
103
- program
171
+ const initCommand = program
104
172
  .command('init')
105
173
  .description('Initialize or update AI-first capabilities in the current directory')
106
174
  .option('-p, --preset <preset>', 'Project preset: web | cli | generic')
@@ -110,25 +178,30 @@ export function createCliProgram(): Command {
110
178
  .option('--no-graft', 'Skip codebase graph (Graft) setup')
111
179
  .option('--no-git', 'Skip git repository initialization')
112
180
  .option('--dry-run', 'Simulate setup without writing changes to disk')
181
+ .option('-y, --yes', 'Skip the prompts and use saved or detected settings');
182
+ withProfileOptions(initCommand)
113
183
  .action(async (options: any, command: Command) => {
114
184
  let finalPreset = options.preset;
115
185
  let finalFramework = options.framework;
116
186
  let finalPackageManager = options.packageManager;
187
+ let finalProfile = profileFlags(options);
117
188
 
118
189
  const isPresetExplicit = command.getOptionValueSource('preset') === 'cli';
119
190
  const isFrameworkExplicit = command.getOptionValueSource('framework') === 'cli';
120
191
  const isPmExplicit = command.getOptionValueSource('packageManager') === 'cli';
121
192
 
122
193
  const savedConfig = readProjectConfig(process.cwd());
123
- if (isInteractive() && !isPresetExplicit && !options.dryRun && !savedConfig) {
194
+ if (isInteractive() && !isPresetExplicit && !options.dryRun && !options.yes && !savedConfig) {
124
195
  const answers = await promptInitProject({
125
196
  preset: isPresetExplicit ? (options.preset as PresetType) : undefined,
126
197
  framework: isFrameworkExplicit ? (options.framework as WebFramework) : undefined,
127
- packageManager: isPmExplicit ? (options.packageManager as PackageManager) : undefined
198
+ packageManager: isPmExplicit ? (options.packageManager as PackageManager) : undefined,
199
+ profile: finalProfile
128
200
  });
129
201
  finalPreset = answers.preset;
130
202
  finalFramework = answers.framework;
131
203
  finalPackageManager = answers.packageManager;
204
+ finalProfile = answers.profile;
132
205
  }
133
206
 
134
207
  const complete = await initCurrentDirectory({
@@ -138,11 +211,45 @@ export function createCliProgram(): Command {
138
211
  skills: options.skills,
139
212
  graft: options.graft,
140
213
  git: options.git,
141
- dryRun: options.dryRun
214
+ dryRun: options.dryRun,
215
+ profile: finalProfile
142
216
  });
143
217
  if (!complete && !options.dryRun) process.exitCode = 1;
144
218
  });
145
219
 
220
+ // Command: list
221
+ program
222
+ .command('list')
223
+ .description('List every capability in the registry and which ones this project enables')
224
+ .option('-d, --dir <path>', 'Project directory', process.cwd())
225
+ .action((options: any) => listCapabilities(options.dir));
226
+
227
+ // Command: add
228
+ program
229
+ .command('add <capabilities...>')
230
+ .description('Enable capabilities (registry ids or owner/repo skill repositories)')
231
+ .option('--dry-run', 'Simulate the change')
232
+ .action(async (ids: string[], options: any) => {
233
+ const ok = await changeCapabilities(process.cwd(), { add: ids.flatMap((id) => list(id)) }, {
234
+ dryRun: options.dryRun
235
+ });
236
+ if (!ok && !options.dryRun) process.exitCode = 1;
237
+ });
238
+
239
+ // Command: remove
240
+ program
241
+ .command('remove <capabilities...>')
242
+ .description('Disable capabilities and remove the skills they installed')
243
+ .option('--dry-run', 'Simulate the change')
244
+ .action(async (ids: string[], options: any) => {
245
+ const ok = await changeCapabilities(
246
+ process.cwd(),
247
+ { remove: ids.flatMap((id) => list(id)) },
248
+ { dryRun: options.dryRun }
249
+ );
250
+ if (!ok && !options.dryRun) process.exitCode = 1;
251
+ });
252
+
146
253
  // Command: doctor
147
254
  program
148
255
  .command('doctor')
@@ -166,8 +273,11 @@ export function createCliProgram(): Command {
166
273
  ? true
167
274
  : await updateSkills(targetDir, { dryRun: options.dryRun })) &&
168
275
  (config ? await ensureClaudeSkillLinks(targetDir, config, { dryRun: options.dryRun }) : true);
169
- if (config && claudeCodeEnabled(config))
170
- setupClaudeMarkdown(targetDir, { dryRun: options.dryRun });
276
+ if (config) syncAgentDocs(targetDir, config, { dryRun: options.dryRun });
277
+ if (config?.skills.sources.length !== 0) {
278
+ const advice = ignoreVendoredSkills(targetDir, { dryRun: options.dryRun, silent: true });
279
+ for (const line of advice) logger.warn(line);
280
+ }
171
281
  const graftOk =
172
282
  config?.codebase.graft === false
173
283
  ? true
package/src/cli/new.ts CHANGED
@@ -1,7 +1,8 @@
1
1
  import path from 'node:path';
2
2
  import pc from 'picocolors';
3
- import { getDefaultConfig, upgradeLegacySkillConfig } from '../config/defaults.js';
3
+ import { getDefaultConfig, presetPlatform, upgradeLegacySkillConfig } from '../config/defaults.js';
4
4
  import {
5
+ AginitConfig,
5
6
  AGINIT_CONFIG_FILENAME,
6
7
  PresetType,
7
8
  WebFramework,
@@ -11,13 +12,28 @@ import { getPreset } from '../presets/registry.js';
11
12
  import { setupGit } from '../adapters/git.js';
12
13
  import { setupAgentsMarkdown, setupClaudeMarkdown } from '../adapters/agents-md.js';
13
14
  import { claudeCodeEnabled, installAllSkills, skillAgents } from '../adapters/skills.js';
14
- import { initGraft } from '../adapters/graft.js';
15
+ import { ignoreVendoredSkills } from '../adapters/tool-ignores.js';
16
+ import { agentBrowserStubInstalled } from '../adapters/agent-browser.js';
17
+ import { microDocPath, projectProfile, resolvePlatformStack, setupMicroDocs } from '../adapters/agent-docs.js';
18
+ import { detectProject } from '../detect/index.js';
15
19
  import {
16
- agentBrowserConfigured,
17
- agentBrowserStubInstalled,
18
- setupAgentBrowser
19
- } from '../adapters/agent-browser.js';
20
- import { ensureDir, writeJsonFile } from '../utils/fs.js';
20
+ activeCapabilities,
21
+ applyResolvedProfile,
22
+ CapabilityDescriptor,
23
+ e2eDirFor,
24
+ hasProfileFlags,
25
+ inferCapabilities,
26
+ isNodeStack,
27
+ MicroDoc,
28
+ Platform,
29
+ PLATFORMS,
30
+ ProfileFlags,
31
+ resolveCapabilities,
32
+ Stack
33
+ } from '../registry/index.js';
34
+ import { SETUP_LABELS, SETUPS } from '../registry/setup.js';
35
+ import { commandExists } from '../utils/shell.js';
36
+ import { ensureDir, fileExists, writeJsonFile } from '../utils/fs.js';
21
37
  import {
22
38
  readProjectConfig,
23
39
  readPackageManifest,
@@ -39,6 +55,103 @@ export interface NewProjectOptions {
39
55
  dryRun?: boolean;
40
56
  targetDir?: string;
41
57
  silent?: boolean;
58
+ /** v2 profile flags: --platform, --methodology, --design, --testing, --ui-driver, ... */
59
+ profile?: ProfileFlags;
60
+ }
61
+
62
+ /**
63
+ * Work out the config to save. A saved config is reused as written, so plain
64
+ * reruns are deterministic; profile flags (and --preset/--framework for the
65
+ * browser switches) are applied on top of it.
66
+ */
67
+ function resolveConfig(
68
+ projectName: string,
69
+ targetDir: string,
70
+ options: NewProjectOptions,
71
+ existingConfig: AginitConfig | null,
72
+ manifest: Record<string, any> | null
73
+ ): AginitConfig {
74
+ const flags = options.profile ?? {};
75
+ const detected = detectProject(targetDir);
76
+ const saved = existingConfig ? resolvePlatformStack(existingConfig, targetDir) : null;
77
+ const presetHint = options.preset ?? existingConfig?.preset;
78
+ const platform: Platform =
79
+ flags.platform ??
80
+ existingConfig?.platform ??
81
+ (options.preset && options.preset !== 'generic'
82
+ ? presetPlatform(options.preset)
83
+ : (saved?.platform ?? detected.platform ?? presetPlatform(presetHint ?? 'generic')));
84
+ const stack: Stack =
85
+ existingConfig?.stack ??
86
+ (detected.stack !== 'unknown'
87
+ ? detected.stack
88
+ : platform === 'mobile-native'
89
+ ? 'unknown'
90
+ : 'node');
91
+ const presetName = options.preset ?? existingConfig?.preset ?? 'generic';
92
+ const framework =
93
+ options.framework ?? (presetName === 'web' ? existingConfig?.framework : undefined);
94
+ if (presetName !== 'web' && framework && framework !== 'none' && framework !== 'existing')
95
+ throw new Error('A web framework requires --preset web.');
96
+ if (presetName === 'web' && framework === 'existing' && !manifest)
97
+ throw new Error(
98
+ 'An existing web framework requires an existing package.json. Use --framework none for a new baseline.'
99
+ );
100
+ if (!isNodeStack(stack) && options.preset && options.preset !== 'generic')
101
+ throw new Error(
102
+ `--preset ${options.preset} scaffolds a Node.js project, but this is a ${stack} project. ` +
103
+ 'Use --preset generic (the default) to keep the project root free of package.json.'
104
+ );
105
+ const packageManager = options.packageManager ?? existingConfig?.packageManager ?? 'pnpm';
106
+ const defaults = getDefaultConfig(projectName, presetName, {
107
+ framework,
108
+ packageManager,
109
+ platform,
110
+ stack,
111
+ flags,
112
+ skills: options.skills !== false,
113
+ // A generic preset leaves an existing package.json (and its test runner) alone.
114
+ harness: !(presetName === 'generic' && manifest !== null)
115
+ });
116
+ if (!existingConfig) return validateConfig(defaults);
117
+
118
+ let config: AginitConfig = {
119
+ ...existingConfig,
120
+ name: projectName,
121
+ preset: presetName,
122
+ ...(isNodeStack(stack) || existingConfig.packageManager !== undefined ? { packageManager } : {}),
123
+ ...(presetName === 'web' ? { framework: framework ?? 'none' } : {}),
124
+ browser:
125
+ options.framework !== undefined || options.preset !== undefined
126
+ ? { ...existingConfig.browser, ...defaults.browser }
127
+ : existingConfig.browser,
128
+ skills: upgradeLegacySkillConfig(existingConfig.skills)
129
+ };
130
+ if (hasProfileFlags(flags)) {
131
+ const base = existingConfig.capabilities ?? inferCapabilities(existingConfig);
132
+ const profile = resolveCapabilities(
133
+ {
134
+ platform,
135
+ stack,
136
+ framework,
137
+ base,
138
+ basePlatform: saved!.platform,
139
+ flags,
140
+ custom: existingConfig.customCapabilities
141
+ },
142
+ existingConfig.disabledCapabilities
143
+ );
144
+ config = applyResolvedProfile({ ...config, platform, stack }, profile, {
145
+ skills: true,
146
+ previous: [...base, ...(existingConfig.customCapabilities ?? []).map((c) => c.id)]
147
+ });
148
+ } else if (config.capabilities && config.browser.playwright !== existingConfig.browser.playwright) {
149
+ // --framework turned the Playwright switch; keep the capability list in step.
150
+ config.capabilities = config.browser.playwright
151
+ ? [...config.capabilities, 'playwright']
152
+ : config.capabilities.filter((id) => id !== 'playwright');
153
+ }
154
+ return validateConfig(config);
42
155
  }
43
156
 
44
157
  export async function createProject(
@@ -55,6 +168,8 @@ export async function createProject(
55
168
  !SUPPORTED_PACKAGE_MANAGERS.includes(options.packageManager)
56
169
  )
57
170
  throw new Error('Invalid package manager.');
171
+ if (options.profile?.platform !== undefined && !PLATFORMS.includes(options.profile.platform))
172
+ throw new Error(`Invalid platform. Valid platforms are: ${PLATFORMS.join(', ')}`);
58
173
  const defaultDir =
59
174
  projectName.startsWith('@') && projectName.includes('/')
60
175
  ? projectName.split('/').pop()!
@@ -62,54 +177,35 @@ export async function createProject(
62
177
  const targetDir = path.resolve(options.targetDir || path.join(process.cwd(), defaultDir));
63
178
  const existingConfig = readProjectConfig(targetDir);
64
179
  const manifest = readPackageManifest(targetDir); // Validate before creating or modifying anything.
65
- const presetName = options.preset ?? existingConfig?.preset ?? 'generic';
66
- const framework =
67
- options.framework ?? (presetName === 'web' ? existingConfig?.framework : undefined);
68
- if (presetName !== 'web' && framework && framework !== 'none' && framework !== 'existing')
69
- throw new Error('A web framework requires --preset web.');
70
- if (presetName === 'web' && framework === 'existing' && !manifest)
71
- throw new Error(
72
- 'An existing web framework requires an existing package.json. Use --framework none for a new baseline.'
73
- );
74
- const packageManager = options.packageManager ?? existingConfig?.packageManager ?? 'pnpm';
180
+ const config = resolveConfig(projectName, targetDir, options, existingConfig, manifest);
181
+ if (config.preset !== 'web') delete config.framework;
182
+ if (options.graft === false) config.codebase = { ...config.codebase, graft: false };
183
+ if (options.skills === false && !existingConfig) config.skills.sources = [];
184
+ const presetName = config.preset;
185
+ const packageManager = config.packageManager ?? 'pnpm';
186
+ const { platform, stack } = resolvePlatformStack(config, targetDir);
75
187
  const dryRun = !!options.dryRun;
76
188
  const silent = !!options.silent;
77
189
  const presetHandler = getPreset(presetName);
78
- const defaults = getDefaultConfig(projectName, presetName, {
79
- framework,
80
- packageManager
81
- });
82
- const config = validateConfig(
83
- existingConfig
84
- ? {
85
- ...existingConfig,
86
- name: projectName,
87
- preset: presetName,
88
- packageManager,
89
- ...(presetName === 'web' ? { framework: framework ?? 'none' } : {}),
90
- browser:
91
- options.framework !== undefined || options.preset !== undefined
92
- ? { ...existingConfig.browser, ...defaults.browser }
93
- : existingConfig.browser,
94
- skills: upgradeLegacySkillConfig(existingConfig.skills)
95
- }
96
- : defaults
97
- );
98
- if (presetName !== 'web') delete config.framework;
99
- if (options.graft === false) config.codebase = { ...config.codebase, graft: false };
100
- if (options.skills === false && !existingConfig) config.skills.sources = [];
190
+ const active = activeCapabilities(config);
101
191
  if (!silent)
102
192
  logger.banner(
103
193
  `Aginit — ${projectName}`,
104
- `Preset: ${presetName} | PM: ${packageManager} | Target: ${targetDir}${dryRun ? ' (DRY RUN)' : ''}`
194
+ `Preset: ${presetName} | Platform: ${platform} (${stack})${isNodeStack(stack) ? ` | PM: ${packageManager}` : ''} | Target: ${targetDir}${dryRun ? ' (DRY RUN)' : ''}`
105
195
  );
196
+ if (!silent) logger.dim(`Capabilities: ${active.map((c) => c.id).join(', ') || 'none'}`);
106
197
  let complete = true;
107
198
 
108
- // The agent-browser skill stub is inert without its CLI and browser
109
- // binaries, so provisioning them gets its own step when the skill applies.
110
- const agentBrowserWanted =
111
- agentBrowserConfigured(config.skills.sources) || agentBrowserStubInstalled(targetDir);
112
- const totalSteps = agentBrowserWanted ? 7 : 6;
199
+ const installSkills = options.skills !== false && config.skills.sources.length > 0;
200
+ // Upstream setups (agent-browser runtime, e2e, Graft) in registry order, Graft last
201
+ // so its first index sees every generated file.
202
+ const setups = active
203
+ .flatMap((capability) =>
204
+ capability.install.flatMap((step) => (step.kind === 'setup' ? [step.setup] : []))
205
+ )
206
+ .filter((setup) => !(setup === 'graft' && options.graft === false))
207
+ .sort((a, b) => Number(a === 'graft') - Number(b === 'graft'));
208
+ const totalSteps = 6 + setups.length;
113
209
  let currentStep = 1;
114
210
 
115
211
  // Scaffold into the clean directory before Git adds files that upstream tools reject.
@@ -134,15 +230,14 @@ export async function createProject(
134
230
  });
135
231
  if (!silent) logger.success(`Wrote ${AGINIT_CONFIG_FILENAME}`);
136
232
 
137
- // Step 4: Write or update AGENTS.md intake contract
138
- if (!silent) logger.step(currentStep++, totalSteps, 'Generating AGENTS.md intake guidance');
139
- setupAgentsMarkdown(targetDir, config, { dryRun, silent });
140
- if (claudeCodeEnabled(config)) setupClaudeMarkdown(targetDir, { dryRun, silent });
141
- if (!silent) logger.success('Configured AGENTS.md');
233
+ // Step 4: AGENTS.md (task routing) and the docs/agents/ micro-docs it routes to
234
+ if (!silent) logger.step(currentStep++, totalSteps, 'Generating AGENTS.md and docs/agents/');
235
+ const docs = syncAgentDocs(targetDir, config, { dryRun, silent });
236
+ if (!silent) logger.success(`Configured AGENTS.md${docs.length ? ` and ${docs.map(microDocPath).join(', ')}` : ''}`);
142
237
 
143
238
  // Step 5: Install skills via skills.sh engine
144
239
  if (!silent) logger.step(currentStep++, totalSteps, 'Installing upstream skills via skills.sh');
145
- if (options.skills !== false && config.skills.sources.length > 0) {
240
+ if (installSkills) {
146
241
  const { installed, failed } = await installAllSkills(targetDir, config.skills.sources, {
147
242
  agents: skillAgents(config),
148
243
  dryRun,
@@ -160,29 +255,28 @@ export async function createProject(
160
255
  } else if (!silent) {
161
256
  logger.dim('Skills installation skipped (--no-skills or empty configuration)');
162
257
  }
258
+ const agentBrowserStub = agentBrowserStubInstalled(targetDir);
259
+ if (installSkills || agentBrowserStub) {
260
+ const advice = ignoreVendoredSkills(targetDir, { dryRun, silent: true });
261
+ if (!silent) for (const line of advice) logger.warn(line);
262
+ }
263
+ // A stub installed earlier still needs its runtime, even when skills are skipped now.
264
+ if (agentBrowserStub && !setups.includes('agent-browser')) setups.unshift('agent-browser');
163
265
 
164
- // Provision the agent-browser runtime the skill stub drives
165
- if (agentBrowserWanted) {
166
- if (!silent) logger.step(currentStep++, totalSteps, 'Provisioning agent-browser runtime');
167
- const abOk = await setupAgentBrowser({ dryRun, silent, cwd: targetDir });
168
- complete &&= abOk;
169
- if (!silent && abOk) logger.success('agent-browser runtime ready (CLI + browser)');
266
+ for (const setup of setups) {
267
+ if (!silent) logger.step(currentStep++, totalSteps, SETUP_LABELS[setup]);
268
+ const ok = await SETUPS[setup]({ targetDir, config, platform, stack, dryRun, silent });
269
+ complete &&= ok;
270
+ if (ok && !silent) logger.success(`${setup} ready`);
170
271
  }
171
272
 
172
- // Codebase intelligence (Graft)
173
- if (!silent) logger.step(currentStep++, totalSteps, 'Configuring codebase intelligence');
174
- if (config.codebase.graft && options.graft !== false) {
175
- const graftOk = await initGraft(targetDir, {
176
- dryRun,
177
- silent,
178
- claude: claudeCodeEnabled(config)
179
- });
180
- complete &&= graftOk;
181
- if (graftOk && !silent) {
182
- logger.success('Graft context graph initialized');
183
- }
184
- } else if (!silent) {
185
- logger.dim('Graft setup skipped (--no-graft or codebase.graft: false)');
273
+ // CLIs aginit does not install: report the missing ones with their install command.
274
+ if (!silent) logger.step(currentStep++, totalSteps, 'Checking capability CLIs');
275
+ const missing = await missingBinaries(active, setups);
276
+ if (!silent) {
277
+ if (missing.length === 0) logger.success('Every capability CLI is available');
278
+ for (const { name, bin, install } of missing)
279
+ logger.warn(`${name} needs the \`${bin}\` CLI: ${install}`);
186
280
  }
187
281
 
188
282
  if (!silent) {
@@ -196,7 +290,10 @@ export async function createProject(
196
290
  console.log('Next steps:');
197
291
  const relativeTarget = path.relative(process.cwd(), targetDir);
198
292
  if (relativeTarget) console.log(pc.cyan(` cd ${relativeTarget}`));
199
- console.log(pc.cyan(` ${packageManager} install`));
293
+ if (isNodeStack(stack) && (manifest || fileExists(path.join(targetDir, 'package.json')) || dryRun))
294
+ console.log(pc.cyan(` ${packageManager} install`));
295
+ if (active.some((c) => c.id === 'e2e') && e2eDirFor(stack))
296
+ console.log(pc.cyan(` (cd ${e2eDirFor(stack)} && npm install)`));
200
297
  console.log(pc.cyan(' Open the repository in your AI coding agent'));
201
298
  console.log(pc.dim(' Prompt the agent: ') + pc.bold(pc.green('"bootstrap this project"')));
202
299
  console.log();
@@ -204,3 +301,32 @@ export async function createProject(
204
301
 
205
302
  return complete;
206
303
  }
304
+
305
+ /** Binaries of active capabilities that are not on PATH (setups report their own). */
306
+ export async function missingBinaries(
307
+ active: CapabilityDescriptor[],
308
+ setups: string[]
309
+ ): Promise<Array<{ name: string; bin: string; install: string }>> {
310
+ const missing: Array<{ name: string; bin: string; install: string }> = [];
311
+ for (const capability of active) {
312
+ if (capability.install.some((step) => step.kind === 'setup' && setups.includes(step.setup)))
313
+ continue;
314
+ for (const step of capability.install)
315
+ if (step.kind === 'binary' && !(await commandExists(step.bin)))
316
+ missing.push({ name: capability.name, bin: step.bin, install: step.install });
317
+ }
318
+ return missing;
319
+ }
320
+
321
+ /** Refresh AGENTS.md, CLAUDE.md and docs/agents/ from the config. */
322
+ export function syncAgentDocs(
323
+ targetDir: string,
324
+ config: AginitConfig,
325
+ options: { dryRun?: boolean; silent?: boolean } = {}
326
+ ): MicroDoc[] {
327
+ const scripts = readPackageManifest(targetDir)?.scripts;
328
+ const docs = setupMicroDocs(targetDir, projectProfile(config, { targetDir, scripts }), options);
329
+ setupAgentsMarkdown(targetDir, config, { ...options, docs });
330
+ if (claudeCodeEnabled(config)) setupClaudeMarkdown(targetDir, options);
331
+ return docs;
332
+ }