vigiles 33.2.0 → 34.0.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/README.md +3 -2
- package/action.yml +45 -8
- package/dist/adapter.d.ts +1 -1
- package/dist/adapters/claude-code/runtime.js +70 -0
- package/dist/adapters/claude-code/skill-reachability.d.ts +42 -21
- package/dist/adapters/claude-code/skill-reachability.js +91 -28
- package/dist/cli-flag-check.js +1 -0
- package/dist/cli-main.js +401 -21
- package/dist/core/adapter.d.ts +7 -0
- package/dist/core/config-schema.d.ts +53 -34
- package/dist/core/config-schema.js +42 -0
- package/dist/core/install-reader.js +15 -0
- package/dist/core/layout.d.ts +10 -0
- package/dist/core/layout.js +17 -0
- package/dist/core/presets.d.ts +44 -0
- package/dist/core/presets.js +44 -0
- package/dist/core/run-env.d.ts +50 -0
- package/dist/core/run-env.js +51 -0
- package/dist/core/runtime.d.ts +44 -0
- package/dist/core/skill-link-target.d.ts +20 -0
- package/dist/core/skill-link-target.js +33 -0
- package/dist/core/validate.js +1 -1
- package/dist/eval.d.ts +39 -39
- package/dist/eval.js +62 -115
- package/dist/harness-test.d.ts +35 -0
- package/dist/harness-test.js +49 -7
- package/dist/output-style-arms.js +7 -2
- package/dist/plugin-declaration.d.ts +6 -6
- package/dist/plugin-declaration.js +6 -6
- package/dist/setup-plan.d.ts +142 -34
- package/dist/setup-plan.js +235 -135
- package/dist/skill-links.d.ts +183 -0
- package/dist/skill-links.js +255 -0
- package/dist/test-coverage.js +37 -1
- package/dist/vigilesrc.schema.json +6 -0
- package/package.json +1 -1
package/dist/cli-main.js
CHANGED
|
@@ -27,6 +27,7 @@ const generate_harness_js_1 = require("./core/generate-harness.js");
|
|
|
27
27
|
const capability_diff_js_1 = require("./core/capability-diff.js");
|
|
28
28
|
const validate_js_1 = require("./core/validate.js");
|
|
29
29
|
const eval_lock_js_1 = require("./eval-lock.js");
|
|
30
|
+
const zod_1 = require("zod");
|
|
30
31
|
const cli_flags_js_1 = require("./cli-flags.js");
|
|
31
32
|
const cli_commands_js_1 = require("./cli-commands.js");
|
|
32
33
|
const cli_flag_check_js_1 = require("./cli-flag-check.js");
|
|
@@ -39,11 +40,13 @@ const frame_js_1 = require("./core/frame.js");
|
|
|
39
40
|
const surface_discovery_js_1 = require("./core/surface-discovery.js");
|
|
40
41
|
const scan_trigger_suggest_js_1 = require("./scan-trigger-suggest.js");
|
|
41
42
|
const install_reader_js_1 = require("./core/install-reader.js");
|
|
43
|
+
const skill_links_js_1 = require("./skill-links.js");
|
|
42
44
|
const plugin_declaration_js_1 = require("./plugin-declaration.js");
|
|
43
45
|
const local_files_tracked_js_1 = require("./local-files-tracked.js");
|
|
44
46
|
const local_files_js_1 = require("./local-files.js");
|
|
45
47
|
const scan_behavioral_js_1 = require("./scan-behavioral.js");
|
|
46
48
|
const adapter_registry_js_1 = require("./adapter-registry.js");
|
|
49
|
+
const layout_js_1 = require("./core/layout.js");
|
|
47
50
|
const skill_harness_js_1 = require("./skill-harness.js");
|
|
48
51
|
const leaderboard_js_1 = require("./leaderboard.js");
|
|
49
52
|
const optimize_js_1 = require("./optimize.js");
|
|
@@ -2634,6 +2637,9 @@ function vigilesWorkflow(plan, harnesses, hasPackageJson) {
|
|
|
2634
2637
|
- uses: actions/setup-node@v4
|
|
2635
2638
|
with:
|
|
2636
2639
|
node-version: "20"
|
|
2640
|
+
# No \`version:\` on purpose: the Action runs the vigiles your
|
|
2641
|
+
# package.json declares, so CI and a local \`npx vigiles\` cannot
|
|
2642
|
+
# drift onto two different pins.
|
|
2637
2643
|
- uses: zernie/vigiles@v1
|
|
2638
2644
|
`
|
|
2639
2645
|
: "";
|
|
@@ -3034,6 +3040,9 @@ function redirectSyncToolTargets(cwd, targets) {
|
|
|
3034
3040
|
`staling the integrity hash.`);
|
|
3035
3041
|
return out;
|
|
3036
3042
|
}
|
|
3043
|
+
/** The two files `init`'s lint layer generates under `.vigiles/`. */
|
|
3044
|
+
const GENERATED_TYPES = ".vigiles/generated.d.ts";
|
|
3045
|
+
const GENERATED_SCHEMA = ".vigiles/schema.json";
|
|
3037
3046
|
/** Pillar 1 — specs + types + schema + compile. Scaffolds a spec for every
|
|
3038
3047
|
* instruction file (so `--lint` always delivers a spec), but never compiles
|
|
3039
3048
|
* OVER a hand-written file — that is left to the adopt-spec skill. */
|
|
@@ -3079,7 +3088,7 @@ async function setupPillar1(detected, targetValue, harnesses) {
|
|
|
3079
3088
|
const outDir = (0, node_path_1.resolve)(cwd, ".vigiles");
|
|
3080
3089
|
if (!(0, node_fs_1.existsSync)(outDir))
|
|
3081
3090
|
(0, node_fs_1.mkdirSync)(outDir, { recursive: true });
|
|
3082
|
-
(0, node_fs_1.writeFileSync)((0, node_path_1.resolve)(cwd,
|
|
3091
|
+
(0, node_fs_1.writeFileSync)((0, node_path_1.resolve)(cwd, GENERATED_TYPES), typesResult.dts);
|
|
3083
3092
|
for (const l of typesResult.linters) {
|
|
3084
3093
|
console.log(` ${l.linter}: ${String(l.rules.length)} rules`);
|
|
3085
3094
|
}
|
|
@@ -3087,14 +3096,14 @@ async function setupPillar1(detected, targetValue, harnesses) {
|
|
|
3087
3096
|
console.log(` npm scripts: ${String(typesResult.scripts.length)}`);
|
|
3088
3097
|
}
|
|
3089
3098
|
console.log("✓ Generated .vigiles/generated.d.ts");
|
|
3090
|
-
written.push(
|
|
3099
|
+
written.push(GENERATED_TYPES);
|
|
3091
3100
|
const schemaResult = (0, generate_schema_js_1.generateSchema)({
|
|
3092
3101
|
basePath: cwd,
|
|
3093
3102
|
linters: (0, validate_js_1.loadConfig)().linters,
|
|
3094
3103
|
});
|
|
3095
|
-
(0, node_fs_1.writeFileSync)((0, node_path_1.resolve)(cwd,
|
|
3104
|
+
(0, node_fs_1.writeFileSync)((0, node_path_1.resolve)(cwd, GENERATED_SCHEMA), schemaResult.json);
|
|
3096
3105
|
console.log("✓ Generated .vigiles/schema.json (YAML-LSP frontmatter schema)");
|
|
3097
|
-
written.push(
|
|
3106
|
+
written.push(GENERATED_SCHEMA);
|
|
3098
3107
|
// Compile — but NEVER overwrite an existing hand-written file during `init`:
|
|
3099
3108
|
// we compile only GREENFIELD targets (the file doesn't exist yet) and targets
|
|
3100
3109
|
// we already manage (carry our hash). An ADOPTED file is left untouched — the
|
|
@@ -3177,29 +3186,226 @@ function reportInstall(plan, outcome) {
|
|
|
3177
3186
|
}
|
|
3178
3187
|
/**
|
|
3179
3188
|
* Install vigiles's skills/hooks for the chosen harness(es) via the per-harness
|
|
3180
|
-
* `planPluginInstall` decision
|
|
3181
|
-
*
|
|
3182
|
-
*
|
|
3189
|
+
* `planPluginInstall` decision. Two carriers: the SKILLS are linked into the
|
|
3190
|
+
* repo (`linkVigilesSkills`), so every clone and container has them after
|
|
3191
|
+
* `npm install`; the GLOBAL install runs where it carries something the links
|
|
3192
|
+
* cannot (`shouldRunGlobalInstall` — Claude Code's plugin carries the hooks).
|
|
3193
|
+
* The decisions are pure and unit-tested; this is the thin IO.
|
|
3194
|
+
*
|
|
3195
|
+
* Returns the links it created or relinked, for the commit hint.
|
|
3183
3196
|
*/
|
|
3184
3197
|
function installPlugins(harnesses) {
|
|
3185
3198
|
const { execSync: exec } = require("node:child_process");
|
|
3186
3199
|
const plans = (0, setup_plan_js_1.planPluginInstall)(harnesses, {
|
|
3187
3200
|
hasClaude: harnesses.includes("claude") && harnessBinaryPresent("claude"),
|
|
3188
3201
|
});
|
|
3189
|
-
|
|
3202
|
+
const written = plans.flatMap((plan) => {
|
|
3190
3203
|
console.log("");
|
|
3191
|
-
|
|
3192
|
-
|
|
3204
|
+
const home = plan.linkSkills ? harnessSkillsHome(plan.harness) : null;
|
|
3205
|
+
const outcome = home === null ? null : linkVigilesSkills(home, process.cwd());
|
|
3206
|
+
if (outcome !== null)
|
|
3207
|
+
(0, skill_links_js_1.formatSkillLinks)(outcome).forEach((l) => {
|
|
3208
|
+
console.log(l);
|
|
3209
|
+
});
|
|
3210
|
+
const linked = outcome !== null && (0, skill_links_js_1.skillLinksUsable)(outcome);
|
|
3211
|
+
if ((0, setup_plan_js_1.shouldRunGlobalInstall)(plan, linked))
|
|
3212
|
+
reportInstall(plan, runInstall(plan, exec));
|
|
3213
|
+
else
|
|
3214
|
+
console.log(` No global ${plan.harness} skills install — the linked skills replace it, in every clone.`);
|
|
3215
|
+
return outcome === null ? [] : changedLinks(outcome);
|
|
3216
|
+
});
|
|
3193
3217
|
// Claude Code gets its hooks from the global marketplace plugin; Codex has no
|
|
3194
3218
|
// global store, so wire vigiles's proactive nudge hooks into the repo's
|
|
3195
3219
|
// .codex/config.toml directly (the idiomatic, repo-committed place).
|
|
3196
3220
|
if (harnesses.includes("codex"))
|
|
3197
3221
|
wireCodexHooks();
|
|
3198
3222
|
// Claude Code additionally gets a committed DECLARATION, so a collaborator who
|
|
3199
|
-
// clones and never runs `init` is told the project wants this plugin
|
|
3200
|
-
//
|
|
3223
|
+
// clones and never runs `init` is told the project wants this plugin (its
|
|
3224
|
+
// hooks) instead of silence. It does not install anything.
|
|
3201
3225
|
if (harnesses.includes("claude"))
|
|
3202
3226
|
declareVigilesPlugin();
|
|
3227
|
+
return written;
|
|
3228
|
+
}
|
|
3229
|
+
/** The repo-relative skills home of the harness `init` calls `name`, or null. */
|
|
3230
|
+
function harnessSkillsHome(name) {
|
|
3231
|
+
const adapter = (0, adapter_registry_js_1.getAdapter)(name);
|
|
3232
|
+
return adapter === undefined ? null : (0, layout_js_1.skillsHome)(adapter.layout);
|
|
3233
|
+
}
|
|
3234
|
+
/** The repo-relative paths of the links this run created or rewrote — the ones git sees change. */
|
|
3235
|
+
function changedLinks(outcome) {
|
|
3236
|
+
return outcome.kind === "linked"
|
|
3237
|
+
? outcome.results
|
|
3238
|
+
.filter((r) => r.status === "created" || r.status === "relinked")
|
|
3239
|
+
.map((r) => `${outcome.physicalHome}/${r.name}`)
|
|
3240
|
+
: [];
|
|
3241
|
+
}
|
|
3242
|
+
/** The manifest file name the skill-linking code reads. */
|
|
3243
|
+
const PACKAGE_JSON = "package.json";
|
|
3244
|
+
/** One top-level field of `<dir>/package.json`, or undefined. Never throws. */
|
|
3245
|
+
function packageField(dir, field) {
|
|
3246
|
+
try {
|
|
3247
|
+
const pkg = JSON.parse((0, node_fs_1.readFileSync)((0, node_path_1.join)(dir, PACKAGE_JSON), "utf-8"));
|
|
3248
|
+
return typeof pkg === "object" && pkg !== null
|
|
3249
|
+
? Object.entries(pkg).find(([k]) => k === field)?.[1]
|
|
3250
|
+
: undefined;
|
|
3251
|
+
}
|
|
3252
|
+
catch {
|
|
3253
|
+
return undefined;
|
|
3254
|
+
}
|
|
3255
|
+
}
|
|
3256
|
+
/** `package.json`'s `name`, or null. Never throws. */
|
|
3257
|
+
function packageNameAt(dir) {
|
|
3258
|
+
const name = packageField(dir, "name");
|
|
3259
|
+
return typeof name === "string" ? name : null;
|
|
3260
|
+
}
|
|
3261
|
+
/** A thrown value's message. */
|
|
3262
|
+
function errorText(e) {
|
|
3263
|
+
return e instanceof Error ? e.message : String(e);
|
|
3264
|
+
}
|
|
3265
|
+
/** A path's real location, or null when it does not resolve. Never throws. */
|
|
3266
|
+
function realOrNull(path) {
|
|
3267
|
+
try {
|
|
3268
|
+
return (0, node_fs_1.realpathSync)(path);
|
|
3269
|
+
}
|
|
3270
|
+
catch {
|
|
3271
|
+
return null;
|
|
3272
|
+
}
|
|
3273
|
+
}
|
|
3274
|
+
/** What occupies `path`, for `planSkillLinks`. */
|
|
3275
|
+
function observeSkillEntry(path) {
|
|
3276
|
+
const st = (0, node_fs_1.lstatSync)(path, { throwIfNoEntry: false });
|
|
3277
|
+
if (st === undefined)
|
|
3278
|
+
return { kind: "missing" };
|
|
3279
|
+
if (st.isSymbolicLink())
|
|
3280
|
+
return {
|
|
3281
|
+
kind: "link",
|
|
3282
|
+
target: (0, node_fs_1.readlinkSync)(path),
|
|
3283
|
+
resolvesTo: realOrNull(path),
|
|
3284
|
+
};
|
|
3285
|
+
return st.isDirectory() ? { kind: "directory" } : { kind: "file" };
|
|
3286
|
+
}
|
|
3287
|
+
const LINK_IO = {
|
|
3288
|
+
// "dir" matters only on Windows, where a directory link must say so.
|
|
3289
|
+
symlink: (target, path) => {
|
|
3290
|
+
(0, node_fs_1.symlinkSync)(target, path, "dir");
|
|
3291
|
+
},
|
|
3292
|
+
rename: node_fs_1.renameSync,
|
|
3293
|
+
unlink: node_fs_1.unlinkSync,
|
|
3294
|
+
};
|
|
3295
|
+
/** Carry out one decision. A failed `symlink` is reported, never thrown. */
|
|
3296
|
+
function applySkillLink(decision, absHome) {
|
|
3297
|
+
switch (decision.action) {
|
|
3298
|
+
case "keep":
|
|
3299
|
+
return { name: decision.name, status: "present" };
|
|
3300
|
+
case "refuse":
|
|
3301
|
+
return {
|
|
3302
|
+
name: decision.name,
|
|
3303
|
+
status: "skipped",
|
|
3304
|
+
reason: decision.reason,
|
|
3305
|
+
};
|
|
3306
|
+
case "create":
|
|
3307
|
+
case "replace":
|
|
3308
|
+
try {
|
|
3309
|
+
// Only a link `decideSkillLink` proved ours is ever replaced.
|
|
3310
|
+
(0, skill_links_js_1.writeSkillLink)(LINK_IO, (0, node_path_1.join)(absHome, decision.name), decision.target, decision.action);
|
|
3311
|
+
return {
|
|
3312
|
+
name: decision.name,
|
|
3313
|
+
status: decision.action === "replace" ? "relinked" : "created",
|
|
3314
|
+
};
|
|
3315
|
+
}
|
|
3316
|
+
catch (e) {
|
|
3317
|
+
const code = e instanceof Error && "code" in e && typeof e.code === "string"
|
|
3318
|
+
? e.code
|
|
3319
|
+
: "";
|
|
3320
|
+
return {
|
|
3321
|
+
name: decision.name,
|
|
3322
|
+
status: "skipped",
|
|
3323
|
+
reason: (0, skill_links_js_1.linkFailureReason)(code, errorText(e)),
|
|
3324
|
+
};
|
|
3325
|
+
}
|
|
3326
|
+
}
|
|
3327
|
+
}
|
|
3328
|
+
/**
|
|
3329
|
+
* Why `init` must not write under `absHome`, or null when it may. `.claude` or
|
|
3330
|
+
* its `skills` can be a link to a dotfiles directory outside the repo; creating
|
|
3331
|
+
* the home would follow it and put links where no commit of this repo reaches.
|
|
3332
|
+
* What `mkdir -p` would write through is the nearest part of the path that
|
|
3333
|
+
* already exists, so that is the part judged.
|
|
3334
|
+
*/
|
|
3335
|
+
function homeEscape(project, home, absHome) {
|
|
3336
|
+
const nearest = (p) => (0, node_fs_1.lstatSync)(p, { throwIfNoEntry: false }) === undefined
|
|
3337
|
+
? nearest((0, node_path_1.dirname)(p))
|
|
3338
|
+
: p;
|
|
3339
|
+
const anchor = nearest(absHome);
|
|
3340
|
+
const real = realOrNull(anchor);
|
|
3341
|
+
if (real === null)
|
|
3342
|
+
return `${(0, node_path_1.relative)(project, anchor)} is a link that does not resolve, so ${home} cannot be created`;
|
|
3343
|
+
return (0, skill_links_js_1.isWithinProject)(project, real)
|
|
3344
|
+
? null
|
|
3345
|
+
: `${home} leads to ${real}, outside the repository, and a link written there would not travel with a clone`;
|
|
3346
|
+
}
|
|
3347
|
+
/** The outcome when `init` makes no links, with the reason it prints. */
|
|
3348
|
+
function notLinked(reason) {
|
|
3349
|
+
return { kind: "not-linked", reason };
|
|
3350
|
+
}
|
|
3351
|
+
/**
|
|
3352
|
+
* Link vigiles's shipped skills into `home` (repo-relative) — the IO half of
|
|
3353
|
+
* `src/skill-links.ts`, which decides everything: where the package is, what
|
|
3354
|
+
* each entry becomes, and what the report says.
|
|
3355
|
+
*/
|
|
3356
|
+
function linkVigilesSkills(home, cwd) {
|
|
3357
|
+
// Physical, so the relative target computed against the (physical) skills
|
|
3358
|
+
// home is right even when the checkout sits under a symlink (macOS /tmp).
|
|
3359
|
+
const project = (0, node_fs_1.realpathSync)(cwd);
|
|
3360
|
+
const site = (0, skill_links_js_1.locatePackage)(project, {
|
|
3361
|
+
isPackage: (dir) => packageNameAt(dir) === skill_links_js_1.VIGILES_PACKAGE,
|
|
3362
|
+
isRepoRoot: (dir) => (0, node_fs_1.existsSync)((0, node_path_1.join)(dir, ".git")),
|
|
3363
|
+
isWorkspaceRoot: (dir) => (0, node_fs_1.existsSync)((0, node_path_1.join)(dir, "pnpm-workspace.yaml")) ||
|
|
3364
|
+
packageField(dir, "workspaces") !== undefined,
|
|
3365
|
+
});
|
|
3366
|
+
const pkgText = (0, node_fs_1.existsSync)((0, node_path_1.join)(project, PACKAGE_JSON))
|
|
3367
|
+
? (0, node_fs_1.readFileSync)((0, node_path_1.join)(project, PACKAGE_JSON), "utf-8")
|
|
3368
|
+
: null;
|
|
3369
|
+
const blocked = (0, skill_links_js_1.linkPrecondition)({
|
|
3370
|
+
isVigilesItself: packageNameAt(project) === skill_links_js_1.VIGILES_PACKAGE,
|
|
3371
|
+
dependsOnVigiles: pkgText !== null && (0, install_reader_js_1.declaresVigilesDependency)(pkgText),
|
|
3372
|
+
installed: site.kind === "installed",
|
|
3373
|
+
});
|
|
3374
|
+
if (blocked !== null)
|
|
3375
|
+
return notLinked(blocked);
|
|
3376
|
+
if (site.kind === "unsure")
|
|
3377
|
+
return notLinked(site.reason);
|
|
3378
|
+
// Names from the installed package when there is one, else from this CLI's
|
|
3379
|
+
// own — the version `init` just declared as the devDependency.
|
|
3380
|
+
const source = site.kind === "installed" ? site.dir : selfRoot();
|
|
3381
|
+
const names = setup_plan_js_1.SHIPPED_SKILLS.filter((n) => (0, node_fs_1.existsSync)((0, node_path_1.join)(source, "skills", n, "SKILL.md")));
|
|
3382
|
+
if (names.length === 0)
|
|
3383
|
+
return notLinked(`no shipped skills found under ${(0, node_path_1.join)(source, "skills")}`);
|
|
3384
|
+
const absHome = (0, node_path_1.resolve)(project, home);
|
|
3385
|
+
const escape = homeEscape(project, home, absHome);
|
|
3386
|
+
if (escape !== null)
|
|
3387
|
+
return notLinked(escape);
|
|
3388
|
+
try {
|
|
3389
|
+
(0, node_fs_1.mkdirSync)(absHome, { recursive: true });
|
|
3390
|
+
}
|
|
3391
|
+
catch (e) {
|
|
3392
|
+
return notLinked(`cannot create ${home}: ${errorText(e)}`);
|
|
3393
|
+
}
|
|
3394
|
+
const physicalHome = (0, node_fs_1.realpathSync)(absHome);
|
|
3395
|
+
const plan = (0, skill_links_js_1.planSkillLinks)({
|
|
3396
|
+
names,
|
|
3397
|
+
site,
|
|
3398
|
+
physicalHome,
|
|
3399
|
+
entries: (n) => observeSkillEntry((0, node_path_1.join)(absHome, n)),
|
|
3400
|
+
realOf: (n) => realOrNull((0, node_path_1.join)(site.dir, "skills", n)),
|
|
3401
|
+
});
|
|
3402
|
+
return {
|
|
3403
|
+
kind: "linked",
|
|
3404
|
+
home,
|
|
3405
|
+
physicalHome: (0, skill_links_js_1.portableTarget)((0, node_path_1.relative)(project, physicalHome)),
|
|
3406
|
+
pending: plan.pending,
|
|
3407
|
+
results: plan.decisions.map((d) => applySkillLink(d, absHome)),
|
|
3408
|
+
};
|
|
3203
3409
|
}
|
|
3204
3410
|
/**
|
|
3205
3411
|
* Write the project-level plugin declaration into `.claude/settings.json`.
|
|
@@ -3208,8 +3414,9 @@ function installPlugins(harnesses) {
|
|
|
3208
3414
|
* collaborator: an external-source plugin declared project-level does not load
|
|
3209
3415
|
* until each person installs it on their own machine (the boundary is
|
|
3210
3416
|
* deliberate — plugins run arbitrary code with the user's privileges). What it
|
|
3211
|
-
* buys is that Claude Code then PROMPTS them with the install command
|
|
3212
|
-
*
|
|
3417
|
+
* buys is that Claude Code then PROMPTS them with the install command — for the
|
|
3418
|
+
* HOOKS, which only the plugin carries. The skills reach them through the
|
|
3419
|
+
* committed links (`linkVigilesSkills`), with or without the plugin.
|
|
3213
3420
|
*
|
|
3214
3421
|
* MERGES, never clobbers: this file holds the user's hooks, permissions and
|
|
3215
3422
|
* other plugins. The merge rules are the pure, unit-tested
|
|
@@ -3249,8 +3456,9 @@ function declareVigilesPlugin() {
|
|
|
3249
3456
|
(0, node_fs_1.writeFileSync)(path, JSON.stringify(edit.settings, null, 2) + "\n");
|
|
3250
3457
|
console.log("✓ Declared the vigiles plugin in .claude/settings.json (commit it)\n" +
|
|
3251
3458
|
" This does NOT install it for anyone else — Claude Code will PROMPT a\n" +
|
|
3252
|
-
" collaborator to run `claude plugin install vigiles@vigiles`,
|
|
3253
|
-
" the
|
|
3459
|
+
" collaborator to run `claude plugin install vigiles@vigiles`, which is how\n" +
|
|
3460
|
+
" they get the hooks. The skills don't wait for it: they come with the\n" +
|
|
3461
|
+
" linked .claude/skills/ entries.\n" +
|
|
3254
3462
|
" Nothing is vendored: the entry is a reference; the plugin still lives in\n" +
|
|
3255
3463
|
" the global cache, one copy shared across your repos.");
|
|
3256
3464
|
}
|
|
@@ -3408,8 +3616,24 @@ function printSetupSummary(opts) {
|
|
|
3408
3616
|
console.log(`\n Commit:\n git add ${files.join(" ")} && git commit -m "Add vigiles"`);
|
|
3409
3617
|
}
|
|
3410
3618
|
}
|
|
3619
|
+
/** The project config file `init` reads and writes. */
|
|
3620
|
+
const VIGILESRC = ".vigilesrc.json";
|
|
3411
3621
|
async function setup(args) {
|
|
3412
3622
|
const parsed = (0, setup_plan_js_1.parseSetupArgs)(args);
|
|
3623
|
+
// An already-adopted repo gets the MINIMAL re-run unless a flag asks for more
|
|
3624
|
+
// (#338): re-running `init` to pick up one new piece must not also wire CI,
|
|
3625
|
+
// widen the rules, scaffold files or rewrite a pin. Decided from disk, before
|
|
3626
|
+
// any prompt — the minimal run asks nothing.
|
|
3627
|
+
const state = (0, setup_plan_js_1.adoptionState)({
|
|
3628
|
+
hasConfig: (0, node_fs_1.existsSync)((0, node_path_1.resolve)(process.cwd(), VIGILESRC)),
|
|
3629
|
+
specs: detectProject()
|
|
3630
|
+
.instructionFiles.filter((f) => f.hasSpec)
|
|
3631
|
+
.map((f) => `${f.path}.spec.ts`),
|
|
3632
|
+
});
|
|
3633
|
+
if ((0, setup_plan_js_1.rerunScope)(parsed, state) === "minimal" && state.kind === "adopted") {
|
|
3634
|
+
minimalRerun(parsed, state);
|
|
3635
|
+
return;
|
|
3636
|
+
}
|
|
3413
3637
|
// Plan: defaults → flags → interactive prompts (only a human at a TTY).
|
|
3414
3638
|
let plan = (0, setup_plan_js_1.resolvePlan)(parsed);
|
|
3415
3639
|
const prompted = (0, setup_plan_js_1.shouldPrompt)(parsed, process.stdin.isTTY ?? false);
|
|
@@ -3460,10 +3684,9 @@ async function setup(args) {
|
|
|
3460
3684
|
console.log("");
|
|
3461
3685
|
written.push(...wireGha(plan, harnesses));
|
|
3462
3686
|
}
|
|
3463
|
-
//
|
|
3464
|
-
|
|
3465
|
-
|
|
3466
|
-
}
|
|
3687
|
+
// Skills linked into the repo + the per-harness global install (Claude
|
|
3688
|
+
// marketplace for the hooks / Codex only when nothing could be linked).
|
|
3689
|
+
const linkedSkills = plan.plugin ? installPlugins(harnesses) : [];
|
|
3467
3690
|
// Agent-specific guidance.
|
|
3468
3691
|
if (targets.includes("AGENTS.md")) {
|
|
3469
3692
|
console.log("\n Codex / Copilot reads AGENTS.md directly — no hooks needed.");
|
|
@@ -3483,7 +3706,14 @@ async function setup(args) {
|
|
|
3483
3706
|
lint: plan.lint,
|
|
3484
3707
|
written,
|
|
3485
3708
|
});
|
|
3486
|
-
printSetupSummary({
|
|
3709
|
+
printSetupSummary({
|
|
3710
|
+
plan,
|
|
3711
|
+
strict,
|
|
3712
|
+
targets,
|
|
3713
|
+
adopted,
|
|
3714
|
+
// The skill links the plugin step created, so the commit hint names them.
|
|
3715
|
+
written: [...written, ...linkedSkills],
|
|
3716
|
+
});
|
|
3487
3717
|
// Surface the OTHER mode so both directions are discoverable — the two branches
|
|
3488
3718
|
// are mutually exclusive (a run is either gate-only or full):
|
|
3489
3719
|
// • a gate-only setup → a ONE-LINE nudge to graduate to the full layer;
|
|
@@ -3500,6 +3730,152 @@ async function setup(args) {
|
|
|
3500
3730
|
console.log("\nℹ Ran the standard setup. Already have a harness, or not a JS/Python repo, and want only the CI integrity gate (nothing installed)? Re-run `npx vigiles init --ci-only`.");
|
|
3501
3731
|
}
|
|
3502
3732
|
}
|
|
3733
|
+
/**
|
|
3734
|
+
* The minimal re-run on an adopted repo (#338): add what is missing AND safe and
|
|
3735
|
+
* local — the skill links and a missing devDependency — then say what a full
|
|
3736
|
+
* setup would add and the flag that does it. It writes NOTHING else: no CI
|
|
3737
|
+
* workflow, no `.vigilesrc.json` edit, no harness/schema scaffold, no spec for a
|
|
3738
|
+
* hand-written file, no edit to an existing version pin, no global install.
|
|
3739
|
+
* The decisions are pure (`setup-plan.ts`); this is the IO.
|
|
3740
|
+
*/
|
|
3741
|
+
function minimalRerun(parsed, state) {
|
|
3742
|
+
const plan = (0, setup_plan_js_1.resolvePlan)(parsed);
|
|
3743
|
+
const detected = detectProject();
|
|
3744
|
+
const harnesses = resolveHarnesses(parsed, detected, (0, adapter_registry_js_1.declaredHarnessNames)((0, validate_js_1.loadConfig)().harnesses));
|
|
3745
|
+
console.log(`vigiles is already set up here (found ${state.evidence}) — adding only what is missing.\n`);
|
|
3746
|
+
printDetection(detected, harnesses);
|
|
3747
|
+
const links = plan.plugin ? linkSkillsOnly(harnesses) : [];
|
|
3748
|
+
const devDep = plan.lint || plan.test ? addMissingVigilesDevDep() : [];
|
|
3749
|
+
const written = [...links, ...devDep];
|
|
3750
|
+
if (written.length === 0)
|
|
3751
|
+
console.log("\nNothing was missing.");
|
|
3752
|
+
const followUps = (0, setup_plan_js_1.fullSetupFollowUps)(fullSetupGap(plan, detected, harnesses));
|
|
3753
|
+
if (followUps.length > 0)
|
|
3754
|
+
console.log(`\n${followUps.join("\n")}`);
|
|
3755
|
+
if (written.length > 0)
|
|
3756
|
+
console.log(`\n Commit:\n git add ${written.join(" ")} && git commit -m "Update vigiles"`);
|
|
3757
|
+
}
|
|
3758
|
+
/**
|
|
3759
|
+
* Link the shipped skills for each harness that links them — the safe, local
|
|
3760
|
+
* half of `installPlugins`, without the global install or config wiring.
|
|
3761
|
+
* Returns the links created or rewritten.
|
|
3762
|
+
*/
|
|
3763
|
+
function linkSkillsOnly(harnesses) {
|
|
3764
|
+
return (0, setup_plan_js_1.planPluginInstall)(harnesses, { hasClaude: false }).flatMap((plan) => {
|
|
3765
|
+
const home = plan.linkSkills ? harnessSkillsHome(plan.harness) : null;
|
|
3766
|
+
if (home === null)
|
|
3767
|
+
return [];
|
|
3768
|
+
const outcome = linkVigilesSkills(home, process.cwd());
|
|
3769
|
+
console.log("");
|
|
3770
|
+
(0, skill_links_js_1.formatSkillLinks)(outcome).forEach((l) => {
|
|
3771
|
+
console.log(l);
|
|
3772
|
+
});
|
|
3773
|
+
return changedLinks(outcome);
|
|
3774
|
+
});
|
|
3775
|
+
}
|
|
3776
|
+
/** Add vigiles to `devDependencies` only when nothing declares it. */
|
|
3777
|
+
function addMissingVigilesDevDep() {
|
|
3778
|
+
const pkgPath = (0, node_path_1.resolve)(process.cwd(), PACKAGE_JSON);
|
|
3779
|
+
if (!(0, node_fs_1.existsSync)(pkgPath))
|
|
3780
|
+
return [];
|
|
3781
|
+
const pkg = readPackageJson(pkgPath);
|
|
3782
|
+
if (pkg === null || pkg.name === "vigiles")
|
|
3783
|
+
return [];
|
|
3784
|
+
const spec = vigilesDepSpec();
|
|
3785
|
+
const devDependencies = (0, setup_plan_js_1.missingVigilesDevDep)(pkg, spec);
|
|
3786
|
+
if (devDependencies === null)
|
|
3787
|
+
return [];
|
|
3788
|
+
(0, node_fs_1.writeFileSync)(pkgPath, JSON.stringify({ ...pkg, devDependencies }, null, 2) + "\n");
|
|
3789
|
+
console.log(`\n✓ Added vigiles@${spec} to devDependencies — run \`npm install\` to fetch it`);
|
|
3790
|
+
return [PACKAGE_JSON];
|
|
3791
|
+
}
|
|
3792
|
+
/**
|
|
3793
|
+
* The fields of package.json the dependency decisions read. LOOSE on purpose:
|
|
3794
|
+
* every other key is carried through untouched, because the result is written
|
|
3795
|
+
* back and a re-run must not drop a field it never looked at.
|
|
3796
|
+
*/
|
|
3797
|
+
const packageDepsSchema = zod_1.z.looseObject({
|
|
3798
|
+
name: zod_1.z.string().optional(),
|
|
3799
|
+
dependencies: zod_1.z.record(zod_1.z.string(), zod_1.z.string()).optional(),
|
|
3800
|
+
devDependencies: zod_1.z.record(zod_1.z.string(), zod_1.z.string()).optional(),
|
|
3801
|
+
});
|
|
3802
|
+
/** package.json parsed for its dependency fields; null when unreadable. */
|
|
3803
|
+
function readPackageJson(path) {
|
|
3804
|
+
try {
|
|
3805
|
+
const parsed = packageDepsSchema.safeParse(JSON.parse((0, node_fs_1.readFileSync)(path, "utf-8")));
|
|
3806
|
+
return parsed.success ? parsed.data : null;
|
|
3807
|
+
}
|
|
3808
|
+
catch {
|
|
3809
|
+
return null;
|
|
3810
|
+
}
|
|
3811
|
+
}
|
|
3812
|
+
/**
|
|
3813
|
+
* What a full setup would add here, read from disk — the input to the
|
|
3814
|
+
* follow-up lines. It mirrors the full path's own conditions (the plan's
|
|
3815
|
+
* layers, the existence checks those steps make), so the list names what
|
|
3816
|
+
* `--full` would actually do rather than everything it could.
|
|
3817
|
+
*/
|
|
3818
|
+
function fullSetupGap(plan, detected, harnesses) {
|
|
3819
|
+
const cwd = process.cwd();
|
|
3820
|
+
const has = (rel) => (0, node_fs_1.existsSync)((0, node_path_1.resolve)(cwd, rel));
|
|
3821
|
+
const config = readProjectConfig((0, node_path_1.resolve)(cwd, VIGILESRC));
|
|
3822
|
+
const pillar1 = plan.lint && plan.scaffoldSpecs;
|
|
3823
|
+
const unspecced = pillar1
|
|
3824
|
+
? [
|
|
3825
|
+
...determineTargets(detected, undefined, [...harnesses]),
|
|
3826
|
+
...discoverAdoptableSurfaces(cwd),
|
|
3827
|
+
].filter((t, i, all) => all.indexOf(t) === i && !has(`${t}.spec.ts`))
|
|
3828
|
+
: [];
|
|
3829
|
+
const pkg = has(PACKAGE_JSON)
|
|
3830
|
+
? readPackageJson((0, node_path_1.resolve)(cwd, PACKAGE_JSON))
|
|
3831
|
+
: null;
|
|
3832
|
+
const declared = pkg?.devDependencies?.vigiles ?? pkg?.dependencies?.vigiles ?? null;
|
|
3833
|
+
const spec = vigilesDepSpec();
|
|
3834
|
+
return {
|
|
3835
|
+
workflow: plan.gha && !has(".github/workflows/vigiles.yml"),
|
|
3836
|
+
config: configGap(config, plan.lint),
|
|
3837
|
+
harnessTest: plan.test && !has("vigiles.harness.mjs"),
|
|
3838
|
+
unspecced,
|
|
3839
|
+
generated: pillar1 && !(has(GENERATED_TYPES) && has(GENERATED_SCHEMA)),
|
|
3840
|
+
installs: plan.plugin ? fullInstallLines(harnesses) : [],
|
|
3841
|
+
devDep: declared !== null && declared !== spec && pkg?.name !== "vigiles"
|
|
3842
|
+
? { from: declared, to: spec }
|
|
3843
|
+
: null,
|
|
3844
|
+
};
|
|
3845
|
+
}
|
|
3846
|
+
/** Where `.vigilesrc.json` stands against the preset a full setup writes. */
|
|
3847
|
+
function configGap(config, lint) {
|
|
3848
|
+
if (config === null)
|
|
3849
|
+
return "missing";
|
|
3850
|
+
return config.extends === undefined && lint ? "no-preset" : "has-preset";
|
|
3851
|
+
}
|
|
3852
|
+
/** `.vigilesrc.json` as a plain object, or null when absent or unreadable. */
|
|
3853
|
+
function readProjectConfig(path) {
|
|
3854
|
+
if (!(0, node_fs_1.existsSync)(path))
|
|
3855
|
+
return null;
|
|
3856
|
+
try {
|
|
3857
|
+
const raw = JSON.parse((0, node_fs_1.readFileSync)(path, "utf-8"));
|
|
3858
|
+
return typeof raw === "object" && raw !== null && !Array.isArray(raw)
|
|
3859
|
+
? { ...raw }
|
|
3860
|
+
: null;
|
|
3861
|
+
}
|
|
3862
|
+
catch {
|
|
3863
|
+
return null;
|
|
3864
|
+
}
|
|
3865
|
+
}
|
|
3866
|
+
/** The per-machine installs and config wiring the full plugin step would run. */
|
|
3867
|
+
function fullInstallLines(harnesses) {
|
|
3868
|
+
return (0, setup_plan_js_1.planPluginInstall)(harnesses, { hasClaude: true }).flatMap((plan) => [
|
|
3869
|
+
...(plan.globalInstallCarriesHooks && plan.commands.length > 0
|
|
3870
|
+
? [
|
|
3871
|
+
`install the ${plan.harness} plugin for this machine (${plan.commands.join(" && ")}) and declare it in the project settings`,
|
|
3872
|
+
]
|
|
3873
|
+
: []),
|
|
3874
|
+
...(plan.vendors
|
|
3875
|
+
? [`wire vigiles's nudge hooks into the ${plan.harness} config`]
|
|
3876
|
+
: []),
|
|
3877
|
+
]);
|
|
3878
|
+
}
|
|
3503
3879
|
/** Canonical, de-duplicated harness names — the keys `harnesses` gets. */
|
|
3504
3880
|
function harnessConfigKeys(harnesses) {
|
|
3505
3881
|
return [...new Set(harnesses.map(adapter_registry_js_1.normalizeHarnessName))];
|
|
@@ -5239,6 +5615,10 @@ function capabilitiesOfReport(report, dialect) {
|
|
|
5239
5615
|
const COMMAND_HELP = {
|
|
5240
5616
|
init: {
|
|
5241
5617
|
usage: " vigiles init [flags] Set up this repo — specs, plugin, and CI.",
|
|
5618
|
+
detail: [
|
|
5619
|
+
" Already set up? A re-run only adds what is missing (skill links, the devDependency);",
|
|
5620
|
+
" --full run the whole setup again",
|
|
5621
|
+
],
|
|
5242
5622
|
},
|
|
5243
5623
|
compile: { usage: " vigiles compile [files...] Compile .spec.ts → .md" },
|
|
5244
5624
|
eject: {
|
package/dist/core/adapter.d.ts
CHANGED
|
@@ -182,6 +182,13 @@ export interface InstallReader {
|
|
|
182
182
|
* outside its own surface.
|
|
183
183
|
*/
|
|
184
184
|
readonly repo: (repoRelative: string) => string | null;
|
|
185
|
+
/**
|
|
186
|
+
* The stored target of a repo SYMLINK (what `readlink` returns), or null when
|
|
187
|
+
* the path is not a link, is missing, or is NOT a path this adapter claims.
|
|
188
|
+
* It answers the one question `repo` cannot: a committed link whose target is
|
|
189
|
+
* not installed yet reads as null through `repo`, exactly like no link at all.
|
|
190
|
+
*/
|
|
191
|
+
readonly repoLink: (repoRelative: string) => string | null;
|
|
185
192
|
/**
|
|
186
193
|
* A file under the user's HOME, or null. This is a read of the MACHINE, never
|
|
187
194
|
* of the repository — the plugin-install record lives there — and everything
|