vibecoding-devtools 1.0.1
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/LICENSE +21 -0
- package/README.md +46 -0
- package/hooks/full-suite.mjs +156 -0
- package/hooks/lib/io.mjs +135 -0
- package/hooks/maestri-protocol.mjs +48 -0
- package/hooks/secret-scan.mjs +90 -0
- package/installer/cli.mjs +263 -0
- package/installer/clis.mjs +46 -0
- package/installer/convert.mjs +91 -0
- package/installer/detect.mjs +389 -0
- package/installer/execute.mjs +386 -0
- package/installer/hooks.mjs +67 -0
- package/installer/json-merge.mjs +211 -0
- package/installer/panel.mjs +99 -0
- package/installer/plan.mjs +255 -0
- package/installer/state.mjs +105 -0
- package/installer/third-party.json +16 -0
- package/installer/third-party.mjs +204 -0
- package/npm-shrinkwrap.json +80 -0
- package/package.json +43 -0
- package/rules/base.md +25 -0
- package/rules/context-on-demand.md +25 -0
- package/rules/maestri-messaging.md +10 -0
- package/rules/maestri-usage.md +12 -0
- package/rules/model-tiers.md +17 -0
- package/rules/risk-process.md +69 -0
- package/rules/testing.md +53 -0
- package/skills/maestri-agent-cli/SKILL.md +150 -0
- package/skills/maestri-agent-cli/references/_template.md +62 -0
- package/skills/maestri-agent-cli/references/antigravity.md +206 -0
- package/skills/maestri-agent-cli/references/claude-code.md +203 -0
- package/skills/maestri-agent-cli/references/codex.md +190 -0
- package/skills/maestri-budget/SKILL.md +118 -0
- package/skills/maestri-protocol/SKILL.md +72 -0
- package/skills/risk-triage/SKILL.md +44 -0
- package/skills/security-surfaces/SKILL.md +31 -0
- package/skills/security-surfaces/surfaces/accounts-and-data.md +41 -0
- package/skills/security-surfaces/surfaces/inputs-and-services.md +47 -0
- package/skills/security-surfaces/surfaces/money.md +57 -0
- package/skills/security-surfaces/surfaces/secrets-and-infra.md +43 -0
- package/templates/AGENTS.md +68 -0
- package/templates/CLAUDE.md +1 -0
- package/templates/coderabbit.yaml +127 -0
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import { detectAll } from './detect.mjs';
|
|
7
|
+
import { buildPlan, reconcileUpdate } from './plan.mjs';
|
|
8
|
+
import { execute, uninstall, START, END } from './execute.mjs';
|
|
9
|
+
import { readState, hashContent } from './state.mjs';
|
|
10
|
+
import { parseFlags, choicesFromFlags, panel, confirmExecution, Cancellation } from './panel.mjs';
|
|
11
|
+
import * as thirdPartyApi from './third-party.mjs';
|
|
12
|
+
import { jsonMergeSituation } from './json-merge.mjs';
|
|
13
|
+
import { CODEX_HOOK_APPROVAL } from './hooks.mjs';
|
|
14
|
+
|
|
15
|
+
export const HELP = `Usage: vibecoding-devtools [install|update|detect|uninstall] [flags]
|
|
16
|
+
Default command: install. Without a TTY: use --yes to apply or --dry-run to preview.
|
|
17
|
+
Update reuses saved install choices; explicit flags override them. Run install first if no choices are saved.
|
|
18
|
+
Update removes unchanged kit-owned items absent from the new plan; edited and third-party items are retained.
|
|
19
|
+
--clis claude,codex,agy Kit CLIs (default: detected; empty or none: no CLIs)
|
|
20
|
+
--scope global|project Global by default; project requires a git repository
|
|
21
|
+
--project Shortcut for --scope project
|
|
22
|
+
--maestri / --no-maestri Include or disable Maestri rules and skills
|
|
23
|
+
--hooks / --no-hooks Install kit hooks (default: enabled; Maestri hook requires --maestri)
|
|
24
|
+
--ecc claude,codex ECC CLIs (default: none)
|
|
25
|
+
--ecc-hooks / --no-ecc-hooks Enable or disable ECC hooks (default: disabled)
|
|
26
|
+
--ecc-stacks typescript,python Project ECC packages (default: detected stacks)
|
|
27
|
+
--impeccable claude,codex,agy Impeccable CLIs (default: none)
|
|
28
|
+
--impeccable-hooks / --no-impeccable-hooks Enable or disable Impeccable hooks (default: disabled)
|
|
29
|
+
--dry-run --yes --force --json --help
|
|
30
|
+
Without a cache, ECC rules are summarized without downloading; ECC is fetched only after confirmation.
|
|
31
|
+
Exit codes: 0 success; 1 something was skipped, blocked or failed; 2 usage error.`;
|
|
32
|
+
|
|
33
|
+
export function formatDetection(r) {
|
|
34
|
+
const tool = (name, f, url) => f.ok ? `${name} ${f.version ?? '?'} ok` : `${name}: ${f.error}. To install: ${url}`;
|
|
35
|
+
const lines = [r.node.ok ? `Node ${r.node.version} ok` : `Node ${r.node.version}: requires ${r.node.minimum} or later. To install: https://nodejs.org`,
|
|
36
|
+
tool('git', r.git, 'https://git-scm.com/downloads'), tool('gitleaks', r.gitleaks, 'https://github.com/gitleaks/gitleaks#installing')];
|
|
37
|
+
for (const c of r.clis) {
|
|
38
|
+
if (c.state === 'missing') lines.push(`${c.name}: not installed. To install: ${c.install}`);
|
|
39
|
+
else if (c.state === 'error') lines.push(`${c.name}: found at ${c.filePath}, but did not respond: ${c.error}`);
|
|
40
|
+
else if (c.state === 'off-path') lines.push(`${c.name} ${c.version ?? '?'}: installed in ${c.directory}, outside PATH. Add this directory to PATH.`);
|
|
41
|
+
else lines.push(`${c.name} ${c.version ?? '?'}: ${c.filePath}`);
|
|
42
|
+
}
|
|
43
|
+
lines.push(r.project.inRepository ? `Project: ${r.project.root}\nStacks: ${(r.project.stacks ?? []).join(', ') || 'none recognized'}` : `Project: ${r.project.reason}. Only global scope is available.`);
|
|
44
|
+
return lines.join('\n');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Read-only: execution validates paths and expands directories into individual files.
|
|
48
|
+
export function summarizePlan(plan, { home, force = false } = {}) {
|
|
49
|
+
// Planning already recognizes AGENTS templates with blocks and identical external installations.
|
|
50
|
+
// Only list identical items: the executor protects files that are not registered.
|
|
51
|
+
const identical = [];
|
|
52
|
+
const listSame = (action) => {
|
|
53
|
+
if (action.type === 'copy' && fs.lstatSync(action.source).isDirectory()) {
|
|
54
|
+
identical.push({ ...action, directory: true });
|
|
55
|
+
for (const name of fs.readdirSync(action.source).sort()) listSame({ ...action, source: path.join(action.source, name), destination: path.join(action.destination, name) });
|
|
56
|
+
} else identical.push(action);
|
|
57
|
+
};
|
|
58
|
+
for (const action of plan.filter((a) => a.type !== 'summary' && a.situation === 'same')) listSame(action);
|
|
59
|
+
// Force allows file replacement; conflicting third-party commands remain blocked.
|
|
60
|
+
const conflicts = plan.filter((a) => a.situation === 'conflict' && (!force || a.type === 'command'));
|
|
61
|
+
const deferred = plan.filter((a) => a.type === 'summary');
|
|
62
|
+
const preview = execute(plan.filter((a) => a.type !== 'summary' && a.situation !== 'same' && (a.situation !== 'conflict' || (force && a.type !== 'command'))), { home, dryRun: true, force });
|
|
63
|
+
const state = readState({ home });
|
|
64
|
+
const actions = preview.actions.map((action) => {
|
|
65
|
+
if (action.situation === 'remove') return action;
|
|
66
|
+
if (action.status === 'same') return { ...action, situation: 'same' };
|
|
67
|
+
if (action.type === 'command') {
|
|
68
|
+
const same = state.items.some((i) => i.type === 'command' && i.cmd === action.cmd && JSON.stringify(i.args) === JSON.stringify(action.args) && i.cwd === (action.cwd ?? home) && i.cli === action.cli && i.owner === action.owner && JSON.stringify(i.env) === JSON.stringify(action.env));
|
|
69
|
+
return { ...action, situation: action.situation === 'update' ? 'update' : same ? 'same' : action.situation ?? 'new' };
|
|
70
|
+
}
|
|
71
|
+
const st = fs.existsSync(action.destination) ? fs.lstatSync(action.destination) : null;
|
|
72
|
+
if (st?.isDirectory()) return { ...action, situation: 'same', directory: true };
|
|
73
|
+
if (action.type === 'copy' && fs.lstatSync(action.source).isDirectory()) return { ...action, situation: 'new', directory: true };
|
|
74
|
+
let current = st ? fs.readFileSync(action.destination) : null;
|
|
75
|
+
if (action.type === 'json-merge') {
|
|
76
|
+
const previous = state.items.find((i) => i.type === action.type && i.destination === action.destination);
|
|
77
|
+
return { ...action, situation: jsonMergeSituation(current, action, previous) };
|
|
78
|
+
}
|
|
79
|
+
let desired;
|
|
80
|
+
if (action.type === 'block') {
|
|
81
|
+
const text = current?.toString('utf8') ?? '';
|
|
82
|
+
const start = text.indexOf(START);
|
|
83
|
+
const end = text.indexOf(END, start);
|
|
84
|
+
current = start < 0 ? null : text.slice(start, end + END.length);
|
|
85
|
+
desired = `${START}\n${action.content}\n${END}`;
|
|
86
|
+
} else desired = action.type === 'copy' ? fs.readFileSync(action.source) : action.content;
|
|
87
|
+
const registered = state.items.some((i) => i.destination === action.destination && i.type === action.type);
|
|
88
|
+
return { ...action, situation: current === null ? 'new' : registered && hashContent(current) === hashContent(desired) ? 'same' : 'update' };
|
|
89
|
+
});
|
|
90
|
+
actions.push(...identical, ...deferred);
|
|
91
|
+
for (const conflict of [...conflicts, ...preview.conflicts]) actions.push({ ...conflict, situation: 'conflict' });
|
|
92
|
+
return { actions, errors: preview.errors, ok: preview.errors.length === 0, warnings: [...(plan.warnings ?? []), ...preview.warnings] };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Read-only: ECC fetching and validation wait until after confirmation.
|
|
96
|
+
export function prepareThirdParty(choices, detection, paths, { thirdParty = thirdPartyApi } = {}) {
|
|
97
|
+
const installed = thirdParty.detectThirdParty(paths.home, paths.project);
|
|
98
|
+
const actions = thirdParty.thirdPartyActions(choices, { ...detection, thirdParty: installed }, paths);
|
|
99
|
+
// Corrupt install-state never authorizes changes to commands or rules for that CLI.
|
|
100
|
+
const eccClis = choices.ecc.clis.filter((cli) => !actions.some((a) => a.owner === 'ecc' && a.cli === cli && a.situation === 'conflict'));
|
|
101
|
+
const cache = path.join(paths.home, '.vibecoding-devtools', 'cache');
|
|
102
|
+
const ecc = paths.ecc ?? path.join(cache, `ecc-${thirdPartyApi.thirdParty.ecc.version}`);
|
|
103
|
+
const available = fs.existsSync(path.join(ecc, 'rules'));
|
|
104
|
+
return { actions, warnings: [...new Set([...(installed.warnings ?? []), ...(actions.warnings ?? [])])],
|
|
105
|
+
paths: available ? { ecc } : {}, cache, fetchLater: eccClis.length > 0,
|
|
106
|
+
pendingRules: eccClis.length > 0 && !available,
|
|
107
|
+
choices: { ...choices, ecc: { ...choices.ecc, clis: eccClis } } };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export async function planInstallation(choices, detection, paths, { prepareThirdParty: prepare, planBuilder = buildPlan, thirdParty = thirdPartyApi } = {}) {
|
|
111
|
+
const prepared = prepare ? await prepare(choices, detection, paths) : prepareThirdParty(choices, detection, paths, { thirdParty });
|
|
112
|
+
let selection = prepared.choices ?? choices;
|
|
113
|
+
if (prepared.pendingRules) selection = { ...selection, ecc: { ...selection.ecc, clis: [] } };
|
|
114
|
+
const kitPlan = planBuilder(selection, detection, { ...paths, ...prepared.paths });
|
|
115
|
+
const plan = [...prepared.actions, ...kitPlan];
|
|
116
|
+
if (prepared.pendingRules) {
|
|
117
|
+
plan.push({ type: 'summary', owner: 'ecc', cli: prepared.choices.ecc.clis.join(','), situation: 'new',
|
|
118
|
+
description: `Download ECC v${thirdPartyApi.thirdParty.ecc.version} and copy rules to selected CLIs (files available after download)` });
|
|
119
|
+
}
|
|
120
|
+
plan.warnings = [...new Set([...(prepared.warnings ?? []), ...(prepared.actions.warnings ?? []), ...(kitPlan.warnings ?? [])])];
|
|
121
|
+
plan.preparation = prepared;
|
|
122
|
+
return plan;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const formatAction = (a) => a.type === 'command' ? JSON.stringify([a.cmd, ...a.args]) : a.destination ?? a.description;
|
|
126
|
+
const uniqueWarnings = (warnings) => {
|
|
127
|
+
const unique = new Map();
|
|
128
|
+
for (const warning of warnings) {
|
|
129
|
+
const destination = typeof warning === 'object' ? warning.item?.destination ?? warning.destination : undefined;
|
|
130
|
+
const key = (typeof warning === 'string' ? warning : warning.reason ?? JSON.stringify(warning))
|
|
131
|
+
+ (destination ? `\n${destination}` : '');
|
|
132
|
+
// Preserve item details when the same warning also appears as plan text.
|
|
133
|
+
if (!unique.has(key) || typeof unique.get(key) === 'string') unique.set(key, warning);
|
|
134
|
+
}
|
|
135
|
+
return [...unique.values()];
|
|
136
|
+
};
|
|
137
|
+
const printSummary = (summary, out) => {
|
|
138
|
+
out('Summary:');
|
|
139
|
+
for (const a of summary.actions) out(`[${a.situation ?? a.status}] ${a.type} ${formatAction(a)}${a.reason ? ` — ${a.reason}` : ''}`);
|
|
140
|
+
for (const e of summary.errors) out(`Error: ${e.message}`);
|
|
141
|
+
for (const a of summary.warnings ?? []) out(`Warning: ${warningText(a)}`);
|
|
142
|
+
};
|
|
143
|
+
const warningText = (warning) => `${warning.reason ?? warning}${warning.item?.destination ? ` (${warning.item.destination})` : ''}`;
|
|
144
|
+
|
|
145
|
+
// Return an exit code; injected dependencies support noninteractive tests with fake homes.
|
|
146
|
+
export async function main(argv = process.argv.slice(2), options = {}) {
|
|
147
|
+
const { home = os.homedir(), cwd = process.cwd(), kit = fileURLToPath(new URL('../', import.meta.url)),
|
|
148
|
+
isTTY = Boolean(process.stdin.isTTY && process.stdout.isTTY), out = console.log, err = console.error,
|
|
149
|
+
detect = detectAll, choose = panel, confirm = confirmExecution, prepareThirdParty: prepare, thirdParty = thirdPartyApi,
|
|
150
|
+
planBuilder = buildPlan, apply = execute, remove = uninstall } = options;
|
|
151
|
+
let flags;
|
|
152
|
+
let warnings = [];
|
|
153
|
+
let errorCode = 2;
|
|
154
|
+
const emit = (object) => out(JSON.stringify(object, null, 2));
|
|
155
|
+
try {
|
|
156
|
+
flags = parseFlags(argv);
|
|
157
|
+
if (flags.help) { out(HELP); return 0; }
|
|
158
|
+
errorCode = 1;
|
|
159
|
+
if (flags.command === 'uninstall') {
|
|
160
|
+
const directories = new Set();
|
|
161
|
+
for (const item of readState({ home }).items) if (item.destination) {
|
|
162
|
+
for (let p = path.dirname(item.destination); p !== path.dirname(p) && p !== path.resolve(home); p = path.dirname(p)) {
|
|
163
|
+
if (path.relative(home, p).startsWith('..')) break;
|
|
164
|
+
directories.add(p);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
const preview = remove({ home, dryRun: true });
|
|
168
|
+
if (!flags.json) printSummary(preview, out);
|
|
169
|
+
if (flags['dry-run'] || !preview.ok) { if (flags.json) emit(preview); return preview.ok ? 0 : 1; }
|
|
170
|
+
if (!flags.yes) {
|
|
171
|
+
if (!isTTY || flags.json) throw new Error('Use --yes to uninstall without interaction or --dry-run to preview');
|
|
172
|
+
if (!await confirm()) throw new Cancellation();
|
|
173
|
+
}
|
|
174
|
+
const result = remove({ home, dryRun: false });
|
|
175
|
+
const metadata = path.join(home, '.vibecoding-devtools');
|
|
176
|
+
if (fs.existsSync(metadata)) result.warnings.push({ destination: metadata, reason: `Installation metadata and cache retained at ${metadata}` });
|
|
177
|
+
for (const directory of directories) if (fs.existsSync(directory) && fs.lstatSync(directory).isDirectory() && fs.readdirSync(directory).length === 0) {
|
|
178
|
+
result.warnings.push({ destination: directory, reason: `Empty directory retained: ${directory}` });
|
|
179
|
+
}
|
|
180
|
+
if (flags.json) emit(result);
|
|
181
|
+
else printSummary(result, out);
|
|
182
|
+
return result.ok ? 0 : 1;
|
|
183
|
+
}
|
|
184
|
+
const saved = flags.command === 'update' ? readState({ home }).choices : undefined;
|
|
185
|
+
if (flags.command === 'update' && !saved) throw new Error('No saved installation choices. Run install first to record your choices, then run update.');
|
|
186
|
+
const reuseProject = saved?.scope === 'project' && !flags.project && flags.scope === undefined;
|
|
187
|
+
const detectionCwd = reuseProject ? saved.root : cwd;
|
|
188
|
+
if (reuseProject && (!saved.root || !path.isAbsolute(saved.root))) throw new Error('Invalid saved project root. Run install to record your choices again.');
|
|
189
|
+
const detection = await detect({ cwd: detectionCwd, home, env: { ...process.env, HOME: home, USERPROFILE: home } });
|
|
190
|
+
if (flags.command === 'detect') {
|
|
191
|
+
if (flags.json) emit(detection); else out(formatDetection(detection));
|
|
192
|
+
return detection.node.ok && detection.git.ok ? 0 : 1;
|
|
193
|
+
}
|
|
194
|
+
if (!flags.json) out(formatDetection(detection));
|
|
195
|
+
if (!detection.node.ok || !detection.git.ok) throw new Error(`Missing prerequisites: ${!detection.node.ok ? 'Node >=22 (https://nodejs.org) ' : ''}${!detection.git.ok ? 'git (https://git-scm.com/downloads)' : ''}`);
|
|
196
|
+
if (!isTTY && !flags.yes && !flags['dry-run']) throw new Error('Without a TTY: use --yes to apply or --dry-run to preview');
|
|
197
|
+
const choices = !saved && isTTY && !flags.hasChoices && !flags.yes && !flags['dry-run'] && !flags.json ? await choose(detection) : choicesFromFlags(flags, detection, saved);
|
|
198
|
+
if (reuseProject && choices.root !== saved.root) throw new Error('Saved project root is no longer a git repository. Run install again or override --scope.');
|
|
199
|
+
if (!choices.clis.length && !choices.ecc.clis.length && !choices.impeccable.clis.length) throw new Error('No CLI selected; use --clis claude,codex,agy');
|
|
200
|
+
// detectAll includes state, matching skills and plugins; preserve its detector contract.
|
|
201
|
+
detection.installed ??= { state: readState({ home }), skills: [], plugins: {} };
|
|
202
|
+
const paths = { home, kit, project: choices.root ?? detection.project.root };
|
|
203
|
+
const preparePlan = async () => {
|
|
204
|
+
const planned = await planInstallation(choices, detection, paths, { prepareThirdParty: prepare, planBuilder, thirdParty });
|
|
205
|
+
return flags.command === 'update' ? reconcileUpdate(planned, { home }) : planned;
|
|
206
|
+
};
|
|
207
|
+
let plan = await preparePlan();
|
|
208
|
+
let summary = summarizePlan(plan, { home, force: flags.force });
|
|
209
|
+
warnings = [...summary.warnings, ...(!detection.gitleaks.ok ? ['gitleaks is recommended but missing or unavailable; install: https://github.com/gitleaks/gitleaks#installing'] : [])];
|
|
210
|
+
if (!flags.json) printSummary(summary, out);
|
|
211
|
+
if (flags['dry-run'] || !summary.ok) {
|
|
212
|
+
if (flags.json) emit({ ok: summary.ok, dryRun: true, detection, choices, summary, warnings });
|
|
213
|
+
return summary.ok ? 0 : 1;
|
|
214
|
+
}
|
|
215
|
+
if (!flags.yes) {
|
|
216
|
+
if (!isTTY || flags.json) throw new Error('Use --yes to apply without interaction or --dry-run to preview');
|
|
217
|
+
if (!await confirm()) throw new Cancellation();
|
|
218
|
+
}
|
|
219
|
+
if (plan.preparation.fetchLater && summary.actions.some((a) => a.owner === 'ecc' && !['same', 'conflict'].includes(a.situation))) {
|
|
220
|
+
paths.ecc = await thirdParty.fetchEcc(plan.preparation.cache);
|
|
221
|
+
plan = await preparePlan();
|
|
222
|
+
summary = summarizePlan(plan, { home, force: flags.force });
|
|
223
|
+
warnings = [...new Set([...warnings, ...summary.warnings])];
|
|
224
|
+
if (!flags.json) printSummary(summary, out);
|
|
225
|
+
if (!summary.ok || plan.preparation.pendingRules) throw new Error('Could not prepare ECC rules after fetching the cache');
|
|
226
|
+
}
|
|
227
|
+
// Nonempty directories are already expanded; copying them again would rewrite skipped files.
|
|
228
|
+
const pendingActions = summary.actions.filter((a) => a.type !== 'summary' && a.situation !== 'same' && a.situation !== 'conflict'
|
|
229
|
+
&& (!a.directory || fs.readdirSync(a.source).length === 0));
|
|
230
|
+
const result = pendingActions.length || !summary.actions.some((a) => a.situation === 'conflict')
|
|
231
|
+
? await apply(pendingActions, { home, force: flags.force ?? false, dryRun: false, choices })
|
|
232
|
+
: { ok: true, dryRun: false, actions: [], conflicts: [], warnings: [], errors: [] };
|
|
233
|
+
if (!result || typeof result.ok !== 'boolean'
|
|
234
|
+
|| ['actions', 'conflicts', 'warnings', 'errors'].some((field) => !Array.isArray(result[field]))) {
|
|
235
|
+
throw new Error('Installation executor did not return a valid report');
|
|
236
|
+
}
|
|
237
|
+
result.conflicts.push(...summary.actions.filter((a) => a.situation === 'conflict'));
|
|
238
|
+
result.ok &&= result.conflicts.length === 0;
|
|
239
|
+
const codexHooks = (result.actions.some((a) => a.type === 'json-merge' && a.cli === 'codex'
|
|
240
|
+
&& ['executed', 'same'].includes(a.status)) || result.ok && summary.actions.some((a) => a.type === 'json-merge'
|
|
241
|
+
&& a.cli === 'codex' && a.situation !== 'conflict'))
|
|
242
|
+
&& !result.conflicts.some((a) => a.type === 'json-merge' && a.cli === 'codex');
|
|
243
|
+
const finalWarnings = uniqueWarnings([...warnings, ...result.warnings, ...(codexHooks ? [CODEX_HOOK_APPROVAL] : [])]);
|
|
244
|
+
if (flags.json) emit({ ...result, choices, summary, warnings: finalWarnings });
|
|
245
|
+
else {
|
|
246
|
+
for (const warning of finalWarnings) out(`Warning: ${warningText(warning)}`);
|
|
247
|
+
for (const e of result.errors) err(`Error: ${e.message}`);
|
|
248
|
+
out(`Applied: ${result.actions.length}; identical: ${summary.actions.filter((a) => a.situation === 'same').length}; conflicts: ${result.conflicts.length}.`);
|
|
249
|
+
}
|
|
250
|
+
return result.ok ? 0 : 1;
|
|
251
|
+
} catch (e) {
|
|
252
|
+
const cancelled = e instanceof Cancellation;
|
|
253
|
+
if (flags?.json || argv.includes('--json')) emit({ ok: false, ...(cancelled ? { cancelled: true } : {}), error: e.message, warnings });
|
|
254
|
+
else { for (const warning of warnings) out(`Warning: ${warning}`); err(e.message); }
|
|
255
|
+
return cancelled ? 0 : errorCode;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (process.argv[1]) {
|
|
260
|
+
let entryPath = path.resolve(process.argv[1]);
|
|
261
|
+
try { entryPath = fs.realpathSync(entryPath); } catch { /* Keep the resolved path if it cannot be read. */ }
|
|
262
|
+
if (entryPath === fileURLToPath(import.meta.url)) process.exitCode = await main();
|
|
263
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// Supported CLI binaries, official installation directories and installation commands.
|
|
2
|
+
// Source: docs/cli-compatibility.md, Binary and installation section.
|
|
3
|
+
|
|
4
|
+
export const CLIS = [
|
|
5
|
+
{
|
|
6
|
+
id: "claude",
|
|
7
|
+
name: "Claude Code",
|
|
8
|
+
bin: "claude",
|
|
9
|
+
directories: {
|
|
10
|
+
unix: ["~/.local/bin"],
|
|
11
|
+
win: ["%USERPROFILE%\\.local\\bin", "%LOCALAPPDATA%\\Microsoft\\WinGet\\Links"],
|
|
12
|
+
},
|
|
13
|
+
install: {
|
|
14
|
+
unix: "curl -fsSL https://claude.ai/install.sh | bash",
|
|
15
|
+
win: "irm https://claude.ai/install.ps1 | iex",
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
id: "codex",
|
|
20
|
+
name: "Codex",
|
|
21
|
+
bin: "codex",
|
|
22
|
+
directories: {
|
|
23
|
+
unix: ["~/.local/bin"],
|
|
24
|
+
win: ["%LOCALAPPDATA%\\Programs\\OpenAI\\Codex\\bin"],
|
|
25
|
+
},
|
|
26
|
+
install: {
|
|
27
|
+
unix: "curl -fsSL https://chatgpt.com/codex/install.sh | sh",
|
|
28
|
+
win: "irm https://chatgpt.com/codex/install.ps1 | iex",
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
id: "agy",
|
|
33
|
+
name: "Antigravity",
|
|
34
|
+
bin: "agy",
|
|
35
|
+
directories: {
|
|
36
|
+
unix: ["~/.local/bin"],
|
|
37
|
+
win: ["%LOCALAPPDATA%\\agy\\bin"],
|
|
38
|
+
},
|
|
39
|
+
install: {
|
|
40
|
+
unix: "curl -fsSL https://antigravity.google/cli/install.sh | bash",
|
|
41
|
+
win: "irm https://antigravity.google/cli/install.ps1 | iex",
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
export const NODE_MINIMUM = 22;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// Pure conversion supporting scalar, inline-list and multiline-list YAML paths.
|
|
2
|
+
const normalize = (text) => text.replace(/^\uFEFF/, "").replace(/\r\n/g, "\n");
|
|
3
|
+
|
|
4
|
+
function splitList(text) {
|
|
5
|
+
const items = [];
|
|
6
|
+
let start = 0;
|
|
7
|
+
let quotes = null;
|
|
8
|
+
let braces = 0;
|
|
9
|
+
for (let i = 0; i < text.length; i++) {
|
|
10
|
+
const c = text[i];
|
|
11
|
+
if (quotes) {
|
|
12
|
+
if (c === "\\" && quotes === '"') i++;
|
|
13
|
+
else if (c === quotes && text[i + 1] === quotes && quotes === "'") i++;
|
|
14
|
+
else if (c === quotes) quotes = null;
|
|
15
|
+
} else if (c === '"' || c === "'") quotes = c;
|
|
16
|
+
else if (c === "{") braces++;
|
|
17
|
+
else if (c === "}") braces--;
|
|
18
|
+
else if (c === "," && !braces) { items.push(text.slice(start, i)); start = i + 1; }
|
|
19
|
+
}
|
|
20
|
+
if (quotes || braces) throw new Error("Invalid paths list");
|
|
21
|
+
items.push(text.slice(start));
|
|
22
|
+
return items.map((item) => {
|
|
23
|
+
const value = item.trim();
|
|
24
|
+
if (value.startsWith('"')) return JSON.parse(value);
|
|
25
|
+
if (value.startsWith("'")) return value.slice(1, -1).replace(/''/g, "'");
|
|
26
|
+
return value.replace(/\s+#.*$/, "").trim();
|
|
27
|
+
}).filter(Boolean);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function readFrontmatter(text) {
|
|
31
|
+
const source = normalize(text);
|
|
32
|
+
if (!source.startsWith("---\n")) return { paths: [], body: source };
|
|
33
|
+
const end = /^---[ \t]*$/m.exec(source.slice(4));
|
|
34
|
+
if (!end) throw new Error("Unclosed frontmatter");
|
|
35
|
+
const header = source.slice(4, 4 + end.index);
|
|
36
|
+
const body = source.slice(4 + end.index + end[0].length).replace(/^\n/, "");
|
|
37
|
+
const lines = header.split("\n");
|
|
38
|
+
const index = lines.findIndex((line) => /^paths\s*:/.test(line));
|
|
39
|
+
if (index < 0) return { paths: [], body };
|
|
40
|
+
const value = lines[index].replace(/^paths\s*:\s*/, "").trim();
|
|
41
|
+
let paths;
|
|
42
|
+
if (value.startsWith("[")) {
|
|
43
|
+
if (!value.endsWith("]")) throw new Error("Invalid inline paths list");
|
|
44
|
+
paths = splitList(value.slice(1, -1));
|
|
45
|
+
} else if (value && !value.startsWith("#")) paths = splitList(value);
|
|
46
|
+
else {
|
|
47
|
+
paths = [];
|
|
48
|
+
for (const line of lines.slice(index + 1)) {
|
|
49
|
+
if (/^\S/.test(line) && !line.startsWith("-")) break;
|
|
50
|
+
const item = line.match(/^\s*-\s+(.+)$/);
|
|
51
|
+
if (item) paths.push(...splitList(item[1]));
|
|
52
|
+
else if (line.trim() && !line.trim().startsWith("#")) throw new Error("Invalid paths list");
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return { paths: [...new Set(paths)], body };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function convertAntigravityRule(text, defaultPaths = []) {
|
|
59
|
+
const { paths, body } = readFrontmatter(text);
|
|
60
|
+
const globs = paths.length ? paths : defaultPaths;
|
|
61
|
+
const trigger = globs.length
|
|
62
|
+
? `trigger: glob\nglobs: ${JSON.stringify(globs.join(", "))}`
|
|
63
|
+
: "trigger: always_on";
|
|
64
|
+
return `---\n${trigger}\n---\n${body}`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Rule input: { name, content, destination?, owner?, when? }; on-demand rules require destination.
|
|
68
|
+
// The executor adds block markers, as for other block actions.
|
|
69
|
+
export function buildCodexBlock(rules, { byPackage = false } = {}) {
|
|
70
|
+
const parts = ["## vibecoding-devtools rules"];
|
|
71
|
+
const packages = new Set();
|
|
72
|
+
for (const rule of rules) {
|
|
73
|
+
if (byPackage && rule.owner === "ecc" && rule.packageName) {
|
|
74
|
+
if (!packages.has(rule.packageName)) {
|
|
75
|
+
const when = rule.packageName === "common" ? "code" : `the stack ${rule.packageName}`;
|
|
76
|
+
parts.push(`- If the task involves ${when}, read the ECC package ${rule.packageName} at `
|
|
77
|
+
+ `\`${rule.destinationDirectory.replace(/\\/g, "/")}\`.`);
|
|
78
|
+
packages.add(rule.packageName);
|
|
79
|
+
}
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
const { paths, body } = readFrontmatter(rule.content);
|
|
83
|
+
if (paths.length || rule.owner === "ecc") {
|
|
84
|
+
if (!rule.destination) throw new Error(`On-demand rule without destination: ${rule.name}`);
|
|
85
|
+
const when = paths.length ? `files ${paths.map((p) => JSON.stringify(p)).join(", ")}`
|
|
86
|
+
: rule.when ?? `the ECC package ${rule.name}`;
|
|
87
|
+
parts.push(`- If the task involves ${when}, read \`${rule.destination.replace(/\\/g, "/")}\`.`);
|
|
88
|
+
} else parts.push(body.trim());
|
|
89
|
+
}
|
|
90
|
+
return `${parts.filter(Boolean).join("\n\n")}\n`;
|
|
91
|
+
}
|