@antomanc/aginit 0.1.0 → 0.3.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 (64) hide show
  1. package/CHANGELOG.md +55 -0
  2. package/LICENSE +1 -1
  3. package/README.md +96 -194
  4. package/THIRD_PARTY_NOTICES.md +11 -0
  5. package/dist/adapters/agent-browser.d.ts +54 -0
  6. package/dist/adapters/agent-browser.d.ts.map +1 -0
  7. package/dist/adapters/agent-browser.js +122 -0
  8. package/dist/adapters/agent-browser.js.map +1 -0
  9. package/dist/adapters/agents-md.d.ts.map +1 -1
  10. package/dist/adapters/agents-md.js +53 -23
  11. package/dist/adapters/agents-md.js.map +1 -1
  12. package/dist/adapters/browser.js.map +1 -1
  13. package/dist/adapters/git.d.ts.map +1 -1
  14. package/dist/adapters/git.js +2 -0
  15. package/dist/adapters/git.js.map +1 -1
  16. package/dist/adapters/graft.js.map +1 -1
  17. package/dist/adapters/package-manager.js.map +1 -1
  18. package/dist/adapters/skills.d.ts.map +1 -1
  19. package/dist/adapters/skills.js +11 -7
  20. package/dist/adapters/skills.js.map +1 -1
  21. package/dist/cli/doctor.d.ts.map +1 -1
  22. package/dist/cli/doctor.js +73 -7
  23. package/dist/cli/doctor.js.map +1 -1
  24. package/dist/cli/init.js.map +1 -1
  25. package/dist/cli/main.d.ts.map +1 -1
  26. package/dist/cli/main.js +17 -16
  27. package/dist/cli/main.js.map +1 -1
  28. package/dist/cli/new.d.ts +0 -1
  29. package/dist/cli/new.d.ts.map +1 -1
  30. package/dist/cli/new.js +22 -20
  31. package/dist/cli/new.js.map +1 -1
  32. package/dist/cli/wizard.d.ts +0 -4
  33. package/dist/cli/wizard.d.ts.map +1 -1
  34. package/dist/cli/wizard.js +4 -50
  35. package/dist/cli/wizard.js.map +1 -1
  36. package/dist/config/defaults.d.ts +16 -1
  37. package/dist/config/defaults.d.ts.map +1 -1
  38. package/dist/config/defaults.js +91 -10
  39. package/dist/config/defaults.js.map +1 -1
  40. package/dist/config/schema.d.ts +5 -0
  41. package/dist/config/schema.d.ts.map +1 -1
  42. package/dist/config/schema.js.map +1 -1
  43. package/dist/config/validation.d.ts +3 -3
  44. package/dist/config/validation.d.ts.map +1 -1
  45. package/dist/index.d.ts +1 -0
  46. package/dist/index.d.ts.map +1 -1
  47. package/dist/index.js +1 -0
  48. package/dist/index.js.map +1 -1
  49. package/dist/presets/adr.js.map +1 -1
  50. package/dist/utils/fs.js.map +1 -1
  51. package/dist/utils/logger.d.ts.map +1 -1
  52. package/dist/utils/shell.js.map +1 -1
  53. package/package.json +22 -14
  54. package/src/adapters/agent-browser.ts +162 -0
  55. package/src/adapters/agents-md.ts +60 -26
  56. package/src/adapters/git.ts +2 -0
  57. package/src/adapters/skills.ts +10 -7
  58. package/src/cli/doctor.ts +81 -6
  59. package/src/cli/main.ts +17 -25
  60. package/src/cli/new.ts +25 -24
  61. package/src/cli/wizard.ts +4 -56
  62. package/src/config/defaults.ts +109 -12
  63. package/src/config/schema.ts +5 -0
  64. package/src/index.ts +1 -0
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@antomanc/aginit",
3
- "version": "0.1.0",
4
- "description": "Minimal, modular AI-first project bootstrapper for T3 Code, Antigravity, and Codex",
3
+ "version": "0.3.0",
4
+ "description": "Set up a new or existing project for AI coding agents in one command: AGENTS.md, Matt Pocock skills, Graft codebase graph, Vitest/Playwright. Orchestrates upstream tools, idempotent.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
7
7
  "types": "dist/index.d.ts",
@@ -19,10 +19,11 @@
19
19
  "src",
20
20
  "bin",
21
21
  "README.md",
22
+ "CHANGELOG.md",
22
23
  "LICENSE",
23
24
  "THIRD_PARTY_NOTICES.md"
24
25
  ],
25
- "author": "Antonio Mancuso <an.manc25@gmail.com>",
26
+ "author": "Antonio Mancini <an.manc25@gmail.com>",
26
27
  "homepage": "https://github.com/antomanc/aginit#readme",
27
28
  "bugs": {
28
29
  "url": "https://github.com/antomanc/aginit/issues"
@@ -36,25 +37,31 @@
36
37
  },
37
38
  "keywords": [
38
39
  "ai",
39
- "agents",
40
- "antigravity",
40
+ "ai-agents",
41
+ "coding-agents",
42
+ "agents-md",
43
+ "agent-skills",
41
44
  "codex",
42
- "t3-code",
43
- "skills",
45
+ "antigravity",
44
46
  "graft",
47
+ "scaffolder",
45
48
  "bootstrap",
46
- "scaffolder"
49
+ "project-template",
50
+ "developer-tools",
51
+ "cli",
52
+ "vitest",
53
+ "playwright"
47
54
  ],
48
55
  "dependencies": {
49
56
  "@clack/prompts": "^1.8.1",
50
- "commander": "^12.1.0",
57
+ "commander": "^15.0.0",
51
58
  "picocolors": "^1.1.1"
52
59
  },
53
60
  "devDependencies": {
54
- "@types/node": "^22.8.0",
55
- "tsx": "^4.19.2",
56
- "typescript": "^5.6.3",
57
- "vitest": "^4.1.11"
61
+ "@types/node": "^22.20.5",
62
+ "tsx": "^4.23.15",
63
+ "typescript": "^7.0.2",
64
+ "vitest": "^5.0.3"
58
65
  },
59
66
  "license": "MIT",
60
67
  "engines": {
@@ -64,8 +71,9 @@
64
71
  "build": "tsc",
65
72
  "watch": "tsc --watch",
66
73
  "test": "vitest run",
67
- "typecheck": "tsc --noEmit",
74
+ "typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.test.json",
68
75
  "check": "pnpm typecheck && pnpm test && pnpm build && pnpm audit",
76
+ "check:skills": "node scripts/check-skill-drift.mjs",
69
77
  "smoke": "node scripts/smoke-test.mjs"
70
78
  }
71
79
  }
@@ -0,0 +1,162 @@
1
+ import path from 'node:path';
2
+ import { fileExists, readTextFile } from '../utils/fs.js';
3
+ import { commandExists, runExecutable } from '../utils/shell.js';
4
+ import { logger } from '../utils/logger.js';
5
+ import { SkillSourceConfig } from '../config/schema.js';
6
+
7
+ const AGENT_BROWSER_SKILL = 'agent-browser';
8
+ const AGENT_BROWSER_CLI = 'agent-browser';
9
+ const AGENT_BROWSER_PACKAGE = 'vercel-labs/agent-browser';
10
+
11
+ /**
12
+ * Whether a configured skill source will install the agent-browser skill stub
13
+ * whose instructions assume the `agent-browser` CLI exists.
14
+ */
15
+ export function agentBrowserConfigured(sources: SkillSourceConfig[]): boolean {
16
+ return sources.some(
17
+ (source) =>
18
+ source.skills.includes(AGENT_BROWSER_SKILL) ||
19
+ (source.package === AGENT_BROWSER_PACKAGE &&
20
+ (source.skills.length === 0 || source.skills.includes('*')))
21
+ );
22
+ }
23
+
24
+ /** Whether the agent-browser skill stub is already on disk. */
25
+ export function agentBrowserStubInstalled(targetDir: string): boolean {
26
+ return fileExists(path.join(targetDir, '.agents', 'skills', AGENT_BROWSER_SKILL, 'SKILL.md'));
27
+ }
28
+
29
+ export interface UsernsRestriction {
30
+ sysctl: string;
31
+ value: string;
32
+ fixValue: string;
33
+ }
34
+
35
+ /**
36
+ * Detect the Linux sysctls that block Chrome's sandbox when set to their
37
+ * restrictive value: Chrome's renderer sandbox runs on unprivileged user
38
+ * namespaces, so agent-browser launches die with "No usable sandbox!".
39
+ *
40
+ * Returns null when the kernel does not expose the knobs (other distros,
41
+ * macOS, Windows) or none of them is restrictive.
42
+ */
43
+ export function findUsernsRestriction(procRoot = '/proc'): UsernsRestriction | null {
44
+ const knobs: Array<[sysctl: string, blockingValue: string, fixValue: string]> = [
45
+ ['kernel.apparmor_restrict_unprivileged_userns', '1', '0'],
46
+ ['kernel.unprivileged_userns_clone', '0', '1']
47
+ ];
48
+ for (const [sysctl, blockingValue, fixValue] of knobs) {
49
+ const raw = readTextFile(path.join(procRoot, 'sys', ...sysctl.split('.')));
50
+ if (raw !== null && raw.trim() === blockingValue) {
51
+ return { sysctl, value: raw.trim(), fixValue };
52
+ }
53
+ }
54
+ return null;
55
+ }
56
+
57
+ export interface AgentBrowserSetupOptions {
58
+ dryRun?: boolean;
59
+ silent?: boolean;
60
+ /** Working directory for the provisioning commands (defaults to cwd). */
61
+ cwd?: string;
62
+ /** Root of the proc filesystem; overridable for tests. */
63
+ procRoot?: string;
64
+ }
65
+
66
+ /**
67
+ * Provision the runtime the agent-browser skill stub depends on: the globally
68
+ * installed CLI (`npm i -g agent-browser`, matching the skill's own install
69
+ * line — npm ships with Node and works regardless of the project's package
70
+ * manager) and its browser binaries (`agent-browser install`, idempotent).
71
+ *
72
+ * A blocked Chrome sandbox is reported as a warning only: it needs a system
73
+ * change aginit must not make on the user's behalf. Returns false when the
74
+ * CLI or the browser binaries could not be provisioned.
75
+ */
76
+ export async function setupAgentBrowser(options: AgentBrowserSetupOptions = {}): Promise<boolean> {
77
+ const { dryRun = false, silent = false, cwd = process.cwd(), procRoot = '/proc' } = options;
78
+
79
+ const cliPresent = await commandExists('agent-browser');
80
+ if (!cliPresent) {
81
+ if (!silent) logger.dim('Installing agent-browser CLI globally (npm -g)...');
82
+ const cli = await runExecutable('npm', ['install', '-g', AGENT_BROWSER_SKILL], {
83
+ cwd,
84
+ dryRun,
85
+ silent: true,
86
+ timeout: 600_000
87
+ });
88
+ if (!cli.ok && !dryRun) {
89
+ if (!silent) {
90
+ logger.warn(`Failed installing agent-browser CLI: ${cli.stderr || cli.stdout}`);
91
+ logger.dim(' Install it manually: npm i -g agent-browser && agent-browser install');
92
+ }
93
+ return false;
94
+ }
95
+ }
96
+
97
+ if (!silent) logger.dim('Ensuring agent-browser browser binaries...');
98
+ const browser = await runExecutable(AGENT_BROWSER_CLI, ['install'], {
99
+ cwd,
100
+ dryRun,
101
+ silent: true,
102
+ timeout: 600_000
103
+ });
104
+ if (!browser.ok && !dryRun) {
105
+ if (!silent) {
106
+ logger.warn(`Failed downloading agent-browser browsers: ${browser.stderr || browser.stdout}`);
107
+ logger.dim(' Retry manually: agent-browser install');
108
+ }
109
+ return false;
110
+ }
111
+
112
+ const restriction = findUsernsRestriction(procRoot);
113
+ if (restriction && !silent) {
114
+ logger.warn(
115
+ `Chrome's sandbox is blocked: ${restriction.sysctl}=${restriction.value} restricts unprivileged user namespaces, so agent-browser launches fail with "No usable sandbox!".`
116
+ );
117
+ logger.dim(
118
+ ` → Fix (machine-wide): echo '${restriction.sysctl}=${restriction.fixValue}' | sudo tee /etc/sysctl.d/99-agent-browser-userns.conf && sudo sysctl --system`
119
+ );
120
+ logger.dim(
121
+ ' → Narrower option: pass --args "--no-sandbox" to agent-browser for that launch (Chrome sandbox disabled for that session)'
122
+ );
123
+ }
124
+ return true;
125
+ }
126
+
127
+ export interface AgentBrowserDiagnostic {
128
+ message: string;
129
+ fix?: string;
130
+ }
131
+
132
+ /**
133
+ * Run upstream's own diagnostics (`agent-browser doctor --quick --offline
134
+ * --json`) and return the failing checks, so aginit reports browser health
135
+ * instead of reimplementing it. Returns null when the report cannot be
136
+ * parsed (older CLI, unexpected output) — callers should then skip the check
137
+ * rather than guess.
138
+ */
139
+ export async function runAgentBrowserDiagnostics(): Promise<AgentBrowserDiagnostic[] | null> {
140
+ const res = await runExecutable(
141
+ AGENT_BROWSER_CLI,
142
+ ['doctor', '--quick', '--offline', '--json'],
143
+ {
144
+ silent: true,
145
+ timeout: 60_000
146
+ }
147
+ );
148
+ try {
149
+ const parsed = JSON.parse(res.stdout) as {
150
+ checks?: Array<{ status?: string; message?: string; fix?: string }>;
151
+ };
152
+ if (!Array.isArray(parsed.checks)) return null;
153
+ return parsed.checks
154
+ .filter((check) => check?.status === 'fail')
155
+ .map((check) => ({
156
+ message: String(check?.message || 'check failed'),
157
+ ...(check?.fix ? { fix: String(check.fix) } : {})
158
+ }));
159
+ } catch {
160
+ return null;
161
+ }
162
+ }
@@ -31,18 +31,51 @@ export function generateAgentsMarkdown(
31
31
  testE2E = `- Test (E2E / Browser): \`${pmAdapter.runCmd('test:e2e')}\``;
32
32
  }
33
33
 
34
- const selectedSkills = config.skills.sources.flatMap((source) => source.skills);
35
- const engineering = selectedSkills.filter((skill) =>
36
- ['tdd', 'code-review', 'diagnosing-bugs'].includes(skill)
37
- );
34
+ // A source with an empty skill list (or `*`) installs every skill it ships.
35
+ const skillInstalled = (skill: string) =>
36
+ config.skills.sources.some(
37
+ (source) =>
38
+ source.skills.length === 0 || source.skills.includes('*') || source.skills.includes(skill)
39
+ );
40
+ const mattSkills = (() => {
41
+ const source = config.skills.sources.find((s) => s.package === 'mattpocock/skills');
42
+ if (!source) return [];
43
+ return source.skills.length ? source.skills : ['*'];
44
+ })();
45
+ const hasSkills = config.skills.sources.length > 0;
46
+
47
+ const specSkillNames = ['to-spec', 'to-tickets', 'implement-spec'];
48
+ let engineeringCapability = '';
49
+ if (mattSkills.includes('*')) {
50
+ engineeringCapability =
51
+ '- **Engineering**: the full Matt Pocock skill set installed in `.agents/skills/` — start with `ask-matt` to route to the right one.';
52
+ } else if (mattSkills.length) {
53
+ const engineering = mattSkills.filter((skill) => !specSkillNames.includes(skill));
54
+ if (engineering.length) {
55
+ engineeringCapability = `- **Engineering**: ${engineering.map((skill) => '`' + skill + '`').join(', ')}`;
56
+ }
57
+ }
58
+
59
+ let proseCapability = '';
60
+ if (skillInstalled('humanizer')) {
61
+ proseCapability =
62
+ '- **Prose & Copy**: Use `humanizer` skill to eliminate AI writing tells and refine documentation.';
63
+ }
64
+
65
+ let simplicityCapability = '';
66
+ if (skillInstalled('ponytail')) {
67
+ simplicityCapability =
68
+ '- **Simplicity**: Use `ponytail` skill for the smallest complete change and to cut over-engineering.';
69
+ }
70
+
38
71
  let uiCapability = '';
39
- if (isWeb && selectedSkills.includes('impeccable')) {
72
+ if (isWeb && skillInstalled('impeccable')) {
40
73
  uiCapability =
41
74
  '- **UI & Design**: Use `impeccable` skill for frontend polish, design critique, and token craft.';
42
75
  }
43
76
 
44
77
  let browserCapability = '';
45
- if (hasVisualAgent && selectedSkills.includes('agent-browser')) {
78
+ if (hasVisualAgent && skillInstalled('agent-browser')) {
46
79
  browserCapability =
47
80
  '- **Browser Verification**: Use `agent-browser` skill for interactive inspection and visual QA.';
48
81
  }
@@ -53,12 +86,11 @@ export function generateAgentsMarkdown(
53
86
  '- **Codebase Graph**: Use `graft ask "<query>" --source` or `graft map` for orientation before raw grep.';
54
87
  }
55
88
 
56
- let specWorkflow = '';
57
- if (config.skills.workflow === 'spec' && selectedSkills.includes('to-spec')) {
58
- specWorkflow = '- **Spec & Tickets Workflow**: `to-spec`, `to-tickets`, `implement-spec`';
59
- }
89
+ const specCapability = skillInstalled('to-spec')
90
+ ? '- **Spec & Tickets Workflow**: `to-spec`, `to-tickets`, `implement-spec`'
91
+ : '';
60
92
 
61
- const sections = [
93
+ const sections: Array<string | undefined> = [
62
94
  `# ${config.name}`,
63
95
  '',
64
96
  `AI-first ${config.preset} project configured with \`aginit\`.`,
@@ -70,24 +102,24 @@ export function generateAgentsMarkdown(
70
102
  '',
71
103
  '## Essential Commands',
72
104
  `- Package Manager: \`${pm}\``,
73
- devCommand,
74
- hasCommand('build') ? `- Build: \`${pmAdapter.runCmd('build')}\`` : '',
75
- hasCommand('typecheck') ? `- Typecheck: \`${pmAdapter.runCmd('typecheck')}\`` : '',
76
- hasCommand('test') ? `- Test: \`${pmAdapter.runCmd('test')}\`` : '',
77
- hasCommand('test:unit') ? `- Unit Test: \`${pmAdapter.runCmd('test:unit')}\`` : '',
78
- testE2E,
105
+ devCommand || undefined,
106
+ hasCommand('build') ? `- Build: \`${pmAdapter.runCmd('build')}\`` : undefined,
107
+ hasCommand('typecheck') ? `- Typecheck: \`${pmAdapter.runCmd('typecheck')}\`` : undefined,
108
+ hasCommand('test') ? `- Test: \`${pmAdapter.runCmd('test')}\`` : undefined,
109
+ hasCommand('test:unit') ? `- Unit Test: \`${pmAdapter.runCmd('test:unit')}\`` : undefined,
110
+ testE2E || undefined,
79
111
  '',
80
112
  '## AI Capabilities & Skills',
81
- selectedSkills.length
113
+ hasSkills
82
114
  ? 'Configured skills install into `.agents/skills/` (shared by Antigravity and Codex):'
83
115
  : 'Skills installation is disabled in this configuration.',
84
- engineering.length
85
- ? `- **Engineering**: ${engineering.map((skill) => '\`' + skill + '\`').join(', ')}`
86
- : '',
87
- specWorkflow,
88
- uiCapability,
89
- browserCapability,
90
- graftCapability,
116
+ engineeringCapability || undefined,
117
+ proseCapability || undefined,
118
+ simplicityCapability || undefined,
119
+ specCapability || undefined,
120
+ uiCapability || undefined,
121
+ browserCapability || undefined,
122
+ graftCapability || undefined,
91
123
  '',
92
124
  '## Bootstrap & First Session',
93
125
  'When the user asks to "bootstrap this project" or kicks off a new initiative:',
@@ -97,9 +129,11 @@ export function generateAgentsMarkdown(
97
129
  ''
98
130
  ];
99
131
 
132
+ // Conditional lines are `undefined` and dropped; `''` entries are the blank
133
+ // separators between sections and must survive.
100
134
  return (
101
135
  sections
102
- .filter((s) => s !== '')
136
+ .filter((s): s is string => s !== undefined)
103
137
  .join('\n')
104
138
  .trim() + '\n'
105
139
  );
@@ -33,6 +33,8 @@ blob-report/
33
33
 
34
34
  # Codebase intelligence cache (graft builds locally, git-ignored)
35
35
  /graft/
36
+ # ripgrep hint graft writes next to that cache; regenerated by \`graft build\`
37
+ .ignore
36
38
  `;
37
39
 
38
40
  export async function setupGit(
@@ -81,25 +81,28 @@ export async function installAllSkills(
81
81
  }
82
82
 
83
83
  export function getInstalledSkills(targetDir: string): string[] {
84
+ // The lockfile only records what the skills CLI installed through aginit;
85
+ // skills added by hand live on disk only, so report the union of both.
86
+ const installed = new Set<string>();
87
+
84
88
  const lockPath = path.join(targetDir, 'skills-lock.json');
85
89
  if (fileExists(lockPath)) {
86
90
  const lock = readJsonFile<SkillsLockFile>(lockPath);
87
- if (lock && lock.skills) {
88
- return Object.keys(lock.skills);
89
- }
91
+ for (const name of Object.keys(lock?.skills ?? {})) installed.add(name);
90
92
  }
91
93
 
92
- // Check .agents/skills directory directly
93
94
  const skillsDir = path.join(targetDir, '.agents', 'skills');
94
95
  if (fileExists(skillsDir)) {
95
96
  try {
96
- return fs.readdirSync(skillsDir).filter((f) => !f.startsWith('.'));
97
+ for (const entry of fs.readdirSync(skillsDir)) {
98
+ if (!entry.startsWith('.')) installed.add(entry);
99
+ }
97
100
  } catch {
98
- return [];
101
+ // Unreadable directory: keep whatever the lockfile reported.
99
102
  }
100
103
  }
101
104
 
102
- return [];
105
+ return [...installed];
103
106
  }
104
107
 
105
108
  export async function updateSkills(
package/src/cli/doctor.ts CHANGED
@@ -10,6 +10,12 @@ import {
10
10
  } from '../config/schema.js';
11
11
  import { detectPackageManager } from '../adapters/package-manager.js';
12
12
  import { getInstalledSkills } from '../adapters/skills.js';
13
+ import {
14
+ agentBrowserConfigured,
15
+ agentBrowserStubInstalled,
16
+ findUsernsRestriction,
17
+ runAgentBrowserDiagnostics
18
+ } from '../adapters/agent-browser.js';
13
19
  import { readProjectConfig } from '../config/validation.js';
14
20
  import { logger } from '../utils/logger.js';
15
21
 
@@ -198,20 +204,89 @@ export async function runDoctor(targetDir: string = process.cwd()): Promise<void
198
204
 
199
205
  // 9. Installed skills check
200
206
  const installedSkills = getInstalledSkills(targetDir);
207
+ const skillsDisabled = projectConfig !== null && projectConfig.skills.sources.length === 0;
201
208
  checks.push({
202
209
  name: 'Local Agent Skills (.agents/skills/)',
203
210
  category: 'Project Config',
204
- ok: installedSkills.length > 0,
205
- detail:
206
- installedSkills.length > 0
211
+ ok: skillsDisabled || installedSkills.length > 0,
212
+ detail: skillsDisabled
213
+ ? 'Disabled in configuration (skills.sources is empty)'
214
+ : installedSkills.length > 0
207
215
  ? `${installedSkills.length} skills installed: ${installedSkills.join(', ')}`
208
216
  : 'No skills found in .agents/skills/',
209
217
  suggestion:
210
- installedSkills.length === 0
211
- ? 'Run `aginit init` or `npx skills add ...` to install skills'
212
- : undefined
218
+ skillsDisabled || installedSkills.length > 0
219
+ ? undefined
220
+ : 'Run `aginit init` or `npx skills add ...` to install skills'
213
221
  });
214
222
 
223
+ // 10. agent-browser runtime (only when the project ships the skill)
224
+ const agentBrowserWanted =
225
+ projectConfig !== null &&
226
+ (agentBrowserConfigured(projectConfig.skills.sources) || agentBrowserStubInstalled(targetDir));
227
+ if (agentBrowserWanted) {
228
+ const abExists = await commandExists('agent-browser');
229
+ let abVersion = '';
230
+ if (abExists) {
231
+ const av = await runExecutable('agent-browser', ['--version'], { silent: true });
232
+ abVersion = av.ok ? av.stdout : '';
233
+ }
234
+ checks.push({
235
+ name: 'agent-browser CLI',
236
+ category: 'AI Tooling',
237
+ ok: abExists,
238
+ version: abVersion,
239
+ detail: abExists
240
+ ? `Installed globally (${abVersion || 'version unknown'})`
241
+ : 'agent-browser skill is installed but the CLI is not in PATH',
242
+ suggestion: abExists
243
+ ? undefined
244
+ : 'Install the runtime: `npm i -g agent-browser && agent-browser install`'
245
+ });
246
+
247
+ if (abExists) {
248
+ const diagnostics = await runAgentBrowserDiagnostics();
249
+ if (diagnostics !== null) {
250
+ checks.push({
251
+ name: 'agent-browser browser runtime',
252
+ category: 'AI Tooling',
253
+ ok: diagnostics.length === 0,
254
+ detail:
255
+ diagnostics.length === 0
256
+ ? 'Upstream doctor reports browser and launch environment healthy'
257
+ : diagnostics.map((d) => d.message).join('; '),
258
+ suggestion:
259
+ diagnostics.length === 0
260
+ ? undefined
261
+ : (diagnostics.find((d) => d.fix)?.fix ?? 'Run `agent-browser doctor` for details')
262
+ });
263
+ }
264
+ }
265
+ }
266
+
267
+ // 11. Chrome sandbox prerequisites — agent-browser and Playwright both
268
+ // launch Chrome from the same sandbox on Linux.
269
+ const sandboxRelevant = agentBrowserWanted || projectConfig?.browser.playwright === true;
270
+ if (sandboxRelevant) {
271
+ const restriction = findUsernsRestriction();
272
+ if (restriction) {
273
+ checks.push({
274
+ name: 'Chrome sandbox (unprivileged user namespaces)',
275
+ category: 'Environment',
276
+ ok: false,
277
+ detail: `${restriction.sysctl}=${restriction.value} blocks Chrome's sandbox: browser launches fail with "No usable sandbox!"`,
278
+ suggestion: `Lift it: echo '${restriction.sysctl}=${restriction.fixValue}' | sudo tee /etc/sysctl.d/99-agent-browser-userns.conf && sudo sysctl --system — or pass --args "--no-sandbox" per launch`
279
+ });
280
+ } else if (process.platform === 'linux') {
281
+ checks.push({
282
+ name: 'Chrome sandbox (unprivileged user namespaces)',
283
+ category: 'Environment',
284
+ ok: true,
285
+ detail: 'Unprivileged user namespaces available to Chrome'
286
+ });
287
+ }
288
+ }
289
+
215
290
  // Print grouped results
216
291
  const categories: Array<'Environment' | 'AI Tooling' | 'Project Config'> = [
217
292
  'Environment',
package/src/cli/main.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { Command } from 'commander';
2
+ import { readFileSync } from 'node:fs';
2
3
  import { createProject } from './new.js';
3
4
  import { initCurrentDirectory } from './init.js';
4
5
  import { runDoctor } from './doctor.js';
@@ -14,15 +15,26 @@ import {
14
15
  runInteractiveWizard
15
16
  } from './wizard.js';
16
17
 
18
+ /**
19
+ * Read the version from package.json so releases only bump one place.
20
+ * Resolves from `src/cli/main.ts` and from `dist/cli/main.js` alike.
21
+ */
22
+ function readVersion(): string {
23
+ try {
24
+ const pkg = JSON.parse(readFileSync(new URL('../../package.json', import.meta.url), 'utf8'));
25
+ return typeof pkg.version === 'string' ? pkg.version : '0.0.0';
26
+ } catch {
27
+ return '0.0.0';
28
+ }
29
+ }
30
+
17
31
  export function createCliProgram(): Command {
18
32
  const program = new Command();
19
33
 
20
34
  program
21
35
  .name('aginit')
22
- .description(
23
- 'Minimal, modular AI-first project bootstrapper for T3 Code, Antigravity, and Codex'
24
- )
25
- .version('0.1.0');
36
+ .description('Set up a new or existing project for AI coding agents')
37
+ .version(readVersion());
26
38
 
27
39
  // Root action: interactive wizard when in TTY, else display help
28
40
  program.action(async () => {
@@ -40,10 +52,6 @@ export function createCliProgram(): Command {
40
52
  .option('-p, --preset <preset>', 'Project preset: web | cli | generic', 'generic')
41
53
  .option('-f, --framework <framework>', 'Web framework: none | vite | next | existing', 'none')
42
54
  .option('-m, --package-manager <pm>', 'Package manager: pnpm | npm | yarn | bun', 'pnpm')
43
- .option(
44
- '--spec-workflow',
45
- 'Include full spec and tickets skills (to-spec, to-tickets, implement-spec)'
46
- )
47
55
  .option('--no-skills', 'Skip installing AI skills')
48
56
  .option('--no-graft', 'Skip codebase graph (Graft) setup')
49
57
  .option('--no-git', 'Skip git repository initialization')
@@ -53,7 +61,6 @@ export function createCliProgram(): Command {
53
61
  let finalPreset = options.preset;
54
62
  let finalFramework = options.framework;
55
63
  let finalPackageManager = options.packageManager;
56
- let finalSpecWorkflow = options.specWorkflow;
57
64
 
58
65
  if (!finalName) {
59
66
  if (!isInteractive()) {
@@ -71,24 +78,18 @@ export function createCliProgram(): Command {
71
78
  packageManager:
72
79
  command.getOptionValueSource('packageManager') === 'cli'
73
80
  ? (options.packageManager as PackageManager)
74
- : undefined,
75
- specWorkflow:
76
- command.getOptionValueSource('specWorkflow') === 'cli'
77
- ? options.specWorkflow
78
81
  : undefined
79
82
  });
80
83
  finalName = answers.projectName;
81
84
  finalPreset = answers.preset;
82
85
  finalFramework = answers.framework;
83
86
  finalPackageManager = answers.packageManager;
84
- finalSpecWorkflow = answers.specWorkflow;
85
87
  }
86
88
 
87
89
  const complete = await createProject(finalName, {
88
90
  preset: finalPreset as PresetType,
89
91
  framework: finalFramework as WebFramework,
90
92
  packageManager: finalPackageManager as PackageManager,
91
- specWorkflow: finalSpecWorkflow,
92
93
  skills: options.skills,
93
94
  graft: options.graft,
94
95
  git: options.git,
@@ -104,10 +105,6 @@ export function createCliProgram(): Command {
104
105
  .option('-p, --preset <preset>', 'Project preset: web | cli | generic')
105
106
  .option('-f, --framework <framework>', 'Web framework: none | vite | next | existing')
106
107
  .option('-m, --package-manager <pm>', 'Package manager: pnpm | npm | yarn | bun')
107
- .option(
108
- '--spec-workflow',
109
- 'Include full spec and tickets skills (to-spec, to-tickets, implement-spec)'
110
- )
111
108
  .option('--no-skills', 'Skip installing AI skills')
112
109
  .option('--no-graft', 'Skip codebase graph (Graft) setup')
113
110
  .option('--no-git', 'Skip git repository initialization')
@@ -116,32 +113,27 @@ export function createCliProgram(): Command {
116
113
  let finalPreset = options.preset;
117
114
  let finalFramework = options.framework;
118
115
  let finalPackageManager = options.packageManager;
119
- let finalSpecWorkflow = options.specWorkflow;
120
116
 
121
117
  const isPresetExplicit = command.getOptionValueSource('preset') === 'cli';
122
118
  const isFrameworkExplicit = command.getOptionValueSource('framework') === 'cli';
123
119
  const isPmExplicit = command.getOptionValueSource('packageManager') === 'cli';
124
- const isSpecExplicit = command.getOptionValueSource('specWorkflow') === 'cli';
125
120
 
126
121
  const savedConfig = readProjectConfig(process.cwd());
127
122
  if (isInteractive() && !isPresetExplicit && !options.dryRun && !savedConfig) {
128
123
  const answers = await promptInitProject({
129
124
  preset: isPresetExplicit ? (options.preset as PresetType) : undefined,
130
125
  framework: isFrameworkExplicit ? (options.framework as WebFramework) : undefined,
131
- packageManager: isPmExplicit ? (options.packageManager as PackageManager) : undefined,
132
- specWorkflow: isSpecExplicit ? options.specWorkflow : undefined
126
+ packageManager: isPmExplicit ? (options.packageManager as PackageManager) : undefined
133
127
  });
134
128
  finalPreset = answers.preset;
135
129
  finalFramework = answers.framework;
136
130
  finalPackageManager = answers.packageManager;
137
- finalSpecWorkflow = answers.specWorkflow;
138
131
  }
139
132
 
140
133
  const complete = await initCurrentDirectory({
141
134
  preset: finalPreset as PresetType,
142
135
  framework: finalFramework as WebFramework,
143
136
  packageManager: finalPackageManager as PackageManager,
144
- specWorkflow: finalSpecWorkflow,
145
137
  skills: options.skills,
146
138
  graft: options.graft,
147
139
  git: options.git,