@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.
- package/CHANGELOG.md +55 -0
- package/LICENSE +1 -1
- package/README.md +96 -194
- package/THIRD_PARTY_NOTICES.md +11 -0
- package/dist/adapters/agent-browser.d.ts +54 -0
- package/dist/adapters/agent-browser.d.ts.map +1 -0
- package/dist/adapters/agent-browser.js +122 -0
- package/dist/adapters/agent-browser.js.map +1 -0
- package/dist/adapters/agents-md.d.ts.map +1 -1
- package/dist/adapters/agents-md.js +53 -23
- package/dist/adapters/agents-md.js.map +1 -1
- package/dist/adapters/browser.js.map +1 -1
- package/dist/adapters/git.d.ts.map +1 -1
- package/dist/adapters/git.js +2 -0
- package/dist/adapters/git.js.map +1 -1
- package/dist/adapters/graft.js.map +1 -1
- package/dist/adapters/package-manager.js.map +1 -1
- package/dist/adapters/skills.d.ts.map +1 -1
- package/dist/adapters/skills.js +11 -7
- package/dist/adapters/skills.js.map +1 -1
- package/dist/cli/doctor.d.ts.map +1 -1
- package/dist/cli/doctor.js +73 -7
- package/dist/cli/doctor.js.map +1 -1
- package/dist/cli/init.js.map +1 -1
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/main.js +17 -16
- package/dist/cli/main.js.map +1 -1
- package/dist/cli/new.d.ts +0 -1
- package/dist/cli/new.d.ts.map +1 -1
- package/dist/cli/new.js +22 -20
- package/dist/cli/new.js.map +1 -1
- package/dist/cli/wizard.d.ts +0 -4
- package/dist/cli/wizard.d.ts.map +1 -1
- package/dist/cli/wizard.js +4 -50
- package/dist/cli/wizard.js.map +1 -1
- package/dist/config/defaults.d.ts +16 -1
- package/dist/config/defaults.d.ts.map +1 -1
- package/dist/config/defaults.js +91 -10
- package/dist/config/defaults.js.map +1 -1
- package/dist/config/schema.d.ts +5 -0
- package/dist/config/schema.d.ts.map +1 -1
- package/dist/config/schema.js.map +1 -1
- package/dist/config/validation.d.ts +3 -3
- package/dist/config/validation.d.ts.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/presets/adr.js.map +1 -1
- package/dist/utils/fs.js.map +1 -1
- package/dist/utils/logger.d.ts.map +1 -1
- package/dist/utils/shell.js.map +1 -1
- package/package.json +22 -14
- package/src/adapters/agent-browser.ts +162 -0
- package/src/adapters/agents-md.ts +60 -26
- package/src/adapters/git.ts +2 -0
- package/src/adapters/skills.ts +10 -7
- package/src/cli/doctor.ts +81 -6
- package/src/cli/main.ts +17 -25
- package/src/cli/new.ts +25 -24
- package/src/cli/wizard.ts +4 -56
- package/src/config/defaults.ts +109 -12
- package/src/config/schema.ts +5 -0
- package/src/index.ts +1 -0
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@antomanc/aginit",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
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
|
|
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
|
-
"
|
|
40
|
+
"ai-agents",
|
|
41
|
+
"coding-agents",
|
|
42
|
+
"agents-md",
|
|
43
|
+
"agent-skills",
|
|
41
44
|
"codex",
|
|
42
|
-
"
|
|
43
|
-
"skills",
|
|
45
|
+
"antigravity",
|
|
44
46
|
"graft",
|
|
47
|
+
"scaffolder",
|
|
45
48
|
"bootstrap",
|
|
46
|
-
"
|
|
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": "^
|
|
57
|
+
"commander": "^15.0.0",
|
|
51
58
|
"picocolors": "^1.1.1"
|
|
52
59
|
},
|
|
53
60
|
"devDependencies": {
|
|
54
|
-
"@types/node": "^22.
|
|
55
|
-
"tsx": "^4.
|
|
56
|
-
"typescript": "^
|
|
57
|
-
"vitest": "^
|
|
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
|
-
|
|
35
|
-
const
|
|
36
|
-
|
|
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 &&
|
|
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 &&
|
|
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
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
|
|
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
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
);
|
package/src/adapters/git.ts
CHANGED
package/src/adapters/skills.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
97
|
+
for (const entry of fs.readdirSync(skillsDir)) {
|
|
98
|
+
if (!entry.startsWith('.')) installed.add(entry);
|
|
99
|
+
}
|
|
97
100
|
} catch {
|
|
98
|
-
|
|
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
|
-
|
|
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
|
|
211
|
-
?
|
|
212
|
-
:
|
|
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
|
-
|
|
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,
|