@jaybeeuu/agent-cortex 1.40.0 → 1.41.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/AGENTS.md +21 -8
- package/CHANGELOG.md +48 -0
- package/README.md +78 -38
- package/agents/README.md +5 -3
- package/bin/agent-cortex.mjs +36 -4
- package/bin/installers/claude.mjs +171 -21
- package/bin/installers/copilot.mjs +2 -17
- package/bin/installers/pi.mjs +261 -22
- package/claude/settings.json +4 -0
- package/claude-extras/scripts/statusline-command.sh +32 -4
- package/docs/claude-hooks.md +12 -0
- package/docs/ideas/ban-content-assertion-tests.md +73 -0
- package/docs/ideas/bead-progress-notes-for-session-resumption.md +75 -0
- package/docs/ideas/classify-beads-via-deterministic-script-with-agent-fallback.md +70 -0
- package/docs/ideas/ctx-alias-version-and-update-commands.md +65 -0
- package/docs/ideas/ctx-feedback-command-for-capturing-ideas-from-any-machine.md +67 -0
- package/docs/ideas/deployment-beads-for-post-merge-pipelines.md +36 -0
- package/docs/ideas/design-and-delivery-as-separate-bead-trees/design-and-delivery-as-separate-bead-trees.md +99 -0
- package/docs/ideas/enable-signed-commit-enforcement-changesets-bot-commits-must-be-signed.md +33 -0
- package/docs/ideas/extension-manifests.md +55 -14
- package/docs/ideas/improve-pi-system-prompt.md +5 -2
- package/docs/ideas/map-every-cortex-tool-to-a-real-pi-equivalent.md +95 -0
- package/docs/ideas/record-idea-implementation-questions.md +68 -0
- package/docs/ideas/self-flushing-session.md +72 -0
- package/docs/ideas/state-in-beads-not-md.md +69 -0
- package/docs/ideas/task-session-context.local.md +95 -0
- package/docs/ideas/test-data-creation-helpers.md +92 -0
- package/docs/ideas/test-planning-stage-bead-in-feature-planning.md +68 -0
- package/docs/ideas/worktree-housekeeping-for-closed-beads.md +39 -0
- package/docs/technical-direction/harness-config.md +161 -0
- package/docs/technical-direction/migration-execution-labels.md +60 -23
- package/extensions/agent-guardrails/README.md +45 -0
- package/extensions/agent-guardrails/guardrails.test.ts +42 -0
- package/extensions/agent-guardrails/guardrails.ts +66 -0
- package/extensions/agent-guardrails/index.ts +22 -0
- package/extensions/agent-guardrails/node_modules/.bin/pi +21 -0
- package/extensions/agent-guardrails/node_modules/.bin/tsc +21 -0
- package/extensions/agent-guardrails/node_modules/.bin/tsserver +21 -0
- package/extensions/agent-guardrails/node_modules/.bin/tsx +21 -0
- package/extensions/agent-guardrails/package.json +19 -0
- package/extensions/agent-guardrails/tsconfig.json +12 -0
- package/extensions/agent-modes/README.md +9 -3
- package/extensions/agent-modes/index.test.ts +208 -0
- package/extensions/agent-modes/index.ts +23 -6
- package/extensions/agent-modes/package.json +1 -1
- package/lib/cli.mjs +8 -2
- package/lib/pi-packages.mjs +169 -0
- package/package.json +5 -1
- package/pi/settings.json +23 -12
- package/plugin.json +1 -1
- package/scripts/lib/fs.mjs +31 -0
- package/skills/planning/classify-bead/REFERENCE.md +85 -0
- package/skills/planning/classify-bead/SKILL.md +32 -36
- package/skills/planning/classify-bead/scripts/classify-bead.mjs +292 -0
- package/skills/planning/create-task/SKILL.md +19 -10
- package/skills/planning/write-a-prd/SKILL.md +2 -2
- package/skills/style/style-code/REFERENCE.md +32 -2
- package/skills/style/style-code/SKILL.md +5 -21
- package/skills/style/style-tests/SKILL.md +69 -17
- package/skills/workflow/run-pipeline-stage/SKILL.md +13 -1
- package/skills/workflow/run-pipeline-stage/prompts/stage-runner.md +12 -0
- package/test/classify-bead.test.mjs +277 -0
- package/test/cli.test.mjs +55 -11
- package/test/fs.test.mjs +76 -0
- package/test/installer.test.mjs +237 -4
- package/test/pi-installer.test.mjs +343 -4
- package/test/pi-packages.test.mjs +295 -0
- package/token-map.README.md +4 -2
- package/token-map.json +3 -3
package/test/fs.test.mjs
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// Unit tests for scripts/lib/fs.mjs — the shared async stat wrappers. The
|
|
2
|
+
// helpers are exercised through their public API only; each case creates its
|
|
3
|
+
// own throwaway fixture under os.tmpdir().
|
|
4
|
+
|
|
5
|
+
import { describe, it } from "node:test";
|
|
6
|
+
import assert from "node:assert/strict";
|
|
7
|
+
import { mkdtemp, writeFile, rm } from "node:fs/promises";
|
|
8
|
+
import { tmpdir } from "node:os";
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
import { isDirectory, isFile } from "../scripts/lib/fs.mjs";
|
|
11
|
+
|
|
12
|
+
async function makeTmp() {
|
|
13
|
+
return await mkdtemp(join(tmpdir(), "agent-cortex-fs-"));
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
describe("isFile", () => {
|
|
17
|
+
it("is true for an existing regular file", async () => {
|
|
18
|
+
const dir = await makeTmp();
|
|
19
|
+
try {
|
|
20
|
+
const file = join(dir, "file.txt");
|
|
21
|
+
await writeFile(file, "hello");
|
|
22
|
+
assert.equal(await isFile(file), true);
|
|
23
|
+
} finally {
|
|
24
|
+
await rm(dir, { recursive: true, force: true });
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("is false for a directory", async () => {
|
|
29
|
+
const dir = await makeTmp();
|
|
30
|
+
try {
|
|
31
|
+
assert.equal(await isFile(dir), false);
|
|
32
|
+
} finally {
|
|
33
|
+
await rm(dir, { recursive: true, force: true });
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("is false for a missing path", async () => {
|
|
38
|
+
const dir = await makeTmp();
|
|
39
|
+
try {
|
|
40
|
+
assert.equal(await isFile(join(dir, "nope.txt")), false);
|
|
41
|
+
} finally {
|
|
42
|
+
await rm(dir, { recursive: true, force: true });
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
describe("isDirectory", () => {
|
|
48
|
+
it("is true for an existing directory", async () => {
|
|
49
|
+
const dir = await makeTmp();
|
|
50
|
+
try {
|
|
51
|
+
assert.equal(await isDirectory(dir), true);
|
|
52
|
+
} finally {
|
|
53
|
+
await rm(dir, { recursive: true, force: true });
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("is false for a regular file", async () => {
|
|
58
|
+
const dir = await makeTmp();
|
|
59
|
+
try {
|
|
60
|
+
const file = join(dir, "file.txt");
|
|
61
|
+
await writeFile(file, "hello");
|
|
62
|
+
assert.equal(await isDirectory(file), false);
|
|
63
|
+
} finally {
|
|
64
|
+
await rm(dir, { recursive: true, force: true });
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("is false for a missing path", async () => {
|
|
69
|
+
const dir = await makeTmp();
|
|
70
|
+
try {
|
|
71
|
+
assert.equal(await isDirectory(join(dir, "nope")), false);
|
|
72
|
+
} finally {
|
|
73
|
+
await rm(dir, { recursive: true, force: true });
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
});
|
package/test/installer.test.mjs
CHANGED
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
stat,
|
|
19
19
|
lstat,
|
|
20
20
|
readlink,
|
|
21
|
+
symlink,
|
|
21
22
|
} from "node:fs/promises";
|
|
22
23
|
import { tmpdir } from "node:os";
|
|
23
24
|
import { fileURLToPath } from "node:url";
|
|
@@ -294,14 +295,15 @@ describe("install claude", () => {
|
|
|
294
295
|
assert.equal(await pathExists(join(out, ".claude-plugin")), false);
|
|
295
296
|
});
|
|
296
297
|
|
|
297
|
-
it("leaves the canonical sources untouched (no
|
|
298
|
+
it("leaves the canonical sources untouched (no generated claude/ output)", async () => {
|
|
298
299
|
const out = await makeTmp();
|
|
299
300
|
const beforeExtras = (await readdir(join(ROOT, "claude-extras"))).sort();
|
|
300
301
|
const beforeExtrasScripts = (await readdir(join(ROOT, "claude-extras", "scripts"))).sort();
|
|
301
302
|
await runCli(["install", "claude", "--output", out]);
|
|
302
|
-
//
|
|
303
|
-
//
|
|
304
|
-
|
|
303
|
+
// the committed claude/ store carries only the settings template — the
|
|
304
|
+
// generated plugin subtree (agents/, skills/, .claude-plugin/, hooks.json)
|
|
305
|
+
// is never committed, and installs never write into the canonical sources
|
|
306
|
+
assert.deepStrictEqual((await readdir(join(ROOT, "claude"))).sort(), ["settings.json"]);
|
|
305
307
|
assert.deepStrictEqual((await readdir(join(ROOT, "claude-extras"))).sort(), beforeExtras);
|
|
306
308
|
assert.deepStrictEqual((await readdir(join(ROOT, "claude-extras", "scripts"))).sort(), beforeExtrasScripts);
|
|
307
309
|
});
|
|
@@ -346,6 +348,11 @@ describe("install claude", () => {
|
|
|
346
348
|
"plugin install agent-cortex@jaybeeuu -y",
|
|
347
349
|
]);
|
|
348
350
|
assert.ok(stdout.includes("Registering plugin"), "CLI reports registration");
|
|
351
|
+
|
|
352
|
+
// the default install also materialises ~/.claude/settings.json from the
|
|
353
|
+
// committed template, owning only the two declared keys
|
|
354
|
+
const settings = JSON.parse(await readFile(join(fakeHome, ".claude", "settings.json"), "utf-8"));
|
|
355
|
+
assert.deepEqual(Object.keys(settings).sort(), ["enabledPlugins", "extraKnownMarketplaces"]);
|
|
349
356
|
});
|
|
350
357
|
|
|
351
358
|
it("re-running a plain install is the update path: no marketplace re-add, no double-install", async () => {
|
|
@@ -407,8 +414,24 @@ describe("install claude", () => {
|
|
|
407
414
|
const { exitCode, stdout } = await runCli(["install", "claude", "--dry-run"], env);
|
|
408
415
|
assert.equal(exitCode, 0);
|
|
409
416
|
assert.ok(stdout.includes(join(fakeHome, ".agent-cortex", "claude")), "dry-run names the home target");
|
|
417
|
+
assert.ok(stdout.includes(join(fakeHome, ".claude", "settings.json")), "dry-run names the settings target");
|
|
410
418
|
assert.ok(/copied/i.test(stdout), "dry-run describes copied skills");
|
|
411
419
|
assert.equal(await pathExists(join(fakeHome, ".agent-cortex")), false, "nothing written in dry-run");
|
|
420
|
+
assert.equal(await pathExists(join(fakeHome, ".claude")), false, "no settings written in dry-run");
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
it("--output stays generate-only and never touches ~/.claude/settings.json", async () => {
|
|
424
|
+
const fakeHome = await makeTmp();
|
|
425
|
+
const out = await makeTmp();
|
|
426
|
+
try {
|
|
427
|
+
const { exitCode } = await runCli(["install", "claude", "--output", out], { HOME: fakeHome });
|
|
428
|
+
assert.equal(exitCode, 0);
|
|
429
|
+
assert.ok(await pathExists(join(out, ".claude-plugin", "plugin.json")), "plugin subtree still generated");
|
|
430
|
+
assert.equal(await pathExists(join(fakeHome, ".claude")), false, "no user settings written by --output");
|
|
431
|
+
} finally {
|
|
432
|
+
await rm(fakeHome, { recursive: true, force: true });
|
|
433
|
+
await rm(out, { recursive: true, force: true });
|
|
434
|
+
}
|
|
412
435
|
});
|
|
413
436
|
|
|
414
437
|
it("rejects token-map.json versions newer than the implemented contract", async () => {
|
|
@@ -428,4 +451,214 @@ describe("install claude", () => {
|
|
|
428
451
|
await rm(fixture, { recursive: true, force: true });
|
|
429
452
|
}
|
|
430
453
|
});
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
// ─── installClaude — ~/.claude/settings.json ─────────────────────────────────
|
|
457
|
+
|
|
458
|
+
/** A claude/settings.json template carrying the two CLI-owned keys. */
|
|
459
|
+
function settingsTemplate(overrides = {}) {
|
|
460
|
+
return { enabledPlugins: {}, extraKnownMarketplaces: {}, ...overrides };
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/** A realistic personal ~/.claude/settings.json (keys the installer must never own). */
|
|
464
|
+
function personalSettings(overrides = {}) {
|
|
465
|
+
return {
|
|
466
|
+
permissions: { allow: ["Bash(pnpm test:*)"] },
|
|
467
|
+
hooks: { SessionStart: [{ hooks: [{ type: "command", command: "echo personal" }] }] },
|
|
468
|
+
env: { PERSONAL_FLAG: "1" },
|
|
469
|
+
statusLine: { type: "command", command: "~/.claude/statusline.sh" },
|
|
470
|
+
...overrides,
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/** Minimal package root with a claude/settings.json template, enough for
|
|
475
|
+
* installClaude to materialise the plugin subtree into a throwaway output. */
|
|
476
|
+
async function makeSettingsFixture({ template = settingsTemplate(), settings } = {}) {
|
|
477
|
+
const root = await makeTmp();
|
|
478
|
+
await writeFile(
|
|
479
|
+
join(root, "package.json"),
|
|
480
|
+
JSON.stringify({ name: "agent-cortex", version: "1.2.3", license: "MIT" }),
|
|
481
|
+
);
|
|
482
|
+
await writeFile(
|
|
483
|
+
join(root, "token-map.json"),
|
|
484
|
+
JSON.stringify({ tools: {}, paths: { plugin_root: { claude: "PLUGIN_ROOT" } } }),
|
|
485
|
+
);
|
|
486
|
+
await mkdir(join(root, "agents"));
|
|
487
|
+
await mkdir(join(root, "skills"));
|
|
488
|
+
if (template !== null) {
|
|
489
|
+
await mkdir(join(root, "claude"), { recursive: true });
|
|
490
|
+
await writeFile(join(root, "claude", "settings.json"), JSON.stringify(template, null, 2) + "\n");
|
|
491
|
+
}
|
|
492
|
+
const output = join(root, "out");
|
|
493
|
+
const settingsPath = join(root, "claude-home", "settings.json");
|
|
494
|
+
if (settings !== undefined) {
|
|
495
|
+
await mkdir(dirname(settingsPath), { recursive: true });
|
|
496
|
+
await writeFile(settingsPath, JSON.stringify(settings, null, 2) + "\n");
|
|
497
|
+
}
|
|
498
|
+
return { root, output, settingsPath, cleanup: () => rm(root, { recursive: true, force: true }) };
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
describe("install claude — settings.json", () => {
|
|
502
|
+
it("materialises a real settings.json carrying the CLI-owned keys from the committed template", async () => {
|
|
503
|
+
const fx = await makeSettingsFixture({
|
|
504
|
+
template: settingsTemplate({
|
|
505
|
+
enabledPlugins: { "context7@claude-plugins-official": true },
|
|
506
|
+
extraKnownMarketplaces: {
|
|
507
|
+
"claude-plugins-official": { source: { source: "github", repo: "anthropics/claude-plugins" } },
|
|
508
|
+
},
|
|
509
|
+
}),
|
|
510
|
+
});
|
|
511
|
+
try {
|
|
512
|
+
const result = await installClaude({ root: fx.root, output: fx.output, settingsPath: fx.settingsPath });
|
|
513
|
+
|
|
514
|
+
assert.equal(result.settings.path, fx.settingsPath);
|
|
515
|
+
assert.equal(result.settings.action, "written");
|
|
516
|
+
assert.equal((await lstat(fx.settingsPath)).isSymbolicLink(), false, "settings.json is a real file");
|
|
517
|
+
const settings = JSON.parse(await readFile(fx.settingsPath, "utf-8"));
|
|
518
|
+
assert.deepEqual(settings.enabledPlugins, { "context7@claude-plugins-official": true });
|
|
519
|
+
assert.deepEqual(settings.extraKnownMarketplaces, {
|
|
520
|
+
"claude-plugins-official": { source: { source: "github", repo: "anthropics/claude-plugins" } },
|
|
521
|
+
});
|
|
522
|
+
assert.deepEqual(
|
|
523
|
+
Object.keys(settings).sort(),
|
|
524
|
+
["enabledPlugins", "extraKnownMarketplaces"],
|
|
525
|
+
"only the two CLI-owned keys are written",
|
|
526
|
+
);
|
|
527
|
+
} finally {
|
|
528
|
+
await fx.cleanup();
|
|
529
|
+
}
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
it("preserves personal and unknown keys on re-install (merge-keep-personal)", async () => {
|
|
533
|
+
const fx = await makeSettingsFixture({
|
|
534
|
+
template: settingsTemplate({
|
|
535
|
+
theme: { name: "dark", accent: "blue" },
|
|
536
|
+
enabledPlugins: { "team@official": true },
|
|
537
|
+
}),
|
|
538
|
+
settings: personalSettings({
|
|
539
|
+
theme: { name: "light" },
|
|
540
|
+
personalOnly: { nested: true },
|
|
541
|
+
}),
|
|
542
|
+
});
|
|
543
|
+
try {
|
|
544
|
+
await installClaude({ root: fx.root, output: fx.output, settingsPath: fx.settingsPath });
|
|
545
|
+
const settings = JSON.parse(await readFile(fx.settingsPath, "utf-8"));
|
|
546
|
+
|
|
547
|
+
assert.deepEqual(settings.permissions, { allow: ["Bash(pnpm test:*)"] });
|
|
548
|
+
assert.deepEqual(settings.hooks, { SessionStart: [{ hooks: [{ type: "command", command: "echo personal" }] }] });
|
|
549
|
+
assert.deepEqual(settings.env, { PERSONAL_FLAG: "1" });
|
|
550
|
+
assert.deepEqual(settings.statusLine, { type: "command", command: "~/.claude/statusline.sh" });
|
|
551
|
+
assert.deepEqual(settings.personalOnly, { nested: true }, "unknown key preserved");
|
|
552
|
+
assert.equal(settings.theme.name, "light", "personal value wins over the template default");
|
|
553
|
+
assert.equal(settings.theme.accent, "blue", "template default fills the unset nested key");
|
|
554
|
+
} finally {
|
|
555
|
+
await fx.cleanup();
|
|
556
|
+
}
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
it("owns only enabledPlugins + extraKnownMarketplaces — personal entries under them are replaced by the template", async () => {
|
|
560
|
+
const fx = await makeSettingsFixture({
|
|
561
|
+
template: settingsTemplate({
|
|
562
|
+
enabledPlugins: { "team@official": true },
|
|
563
|
+
extraKnownMarketplaces: {
|
|
564
|
+
official: { source: { source: "github", repo: "anthropics/plugins" } },
|
|
565
|
+
},
|
|
566
|
+
}),
|
|
567
|
+
settings: personalSettings({
|
|
568
|
+
enabledPlugins: { "personal@mine": true },
|
|
569
|
+
extraKnownMarketplaces: { mine: { source: { source: "github", repo: "me/mine" } } },
|
|
570
|
+
}),
|
|
571
|
+
});
|
|
572
|
+
try {
|
|
573
|
+
await installClaude({ root: fx.root, output: fx.output, settingsPath: fx.settingsPath });
|
|
574
|
+
const settings = JSON.parse(await readFile(fx.settingsPath, "utf-8"));
|
|
575
|
+
|
|
576
|
+
assert.deepEqual(settings.enabledPlugins, { "team@official": true }, "template owns enabledPlugins");
|
|
577
|
+
assert.deepEqual(
|
|
578
|
+
settings.extraKnownMarketplaces,
|
|
579
|
+
{ official: { source: { source: "github", repo: "anthropics/plugins" } } },
|
|
580
|
+
"template owns extraKnownMarketplaces",
|
|
581
|
+
);
|
|
582
|
+
assert.ok(!("personal@mine" in settings.enabledPlugins));
|
|
583
|
+
} finally {
|
|
584
|
+
await fx.cleanup();
|
|
585
|
+
}
|
|
586
|
+
});
|
|
587
|
+
|
|
588
|
+
it("dry-run plans the settings write without touching the filesystem", async () => {
|
|
589
|
+
const fx = await makeSettingsFixture({
|
|
590
|
+
template: settingsTemplate({ enabledPlugins: { "team@official": true } }),
|
|
591
|
+
});
|
|
592
|
+
try {
|
|
593
|
+
const result = await installClaude({
|
|
594
|
+
root: fx.root,
|
|
595
|
+
output: fx.output,
|
|
596
|
+
settingsPath: fx.settingsPath,
|
|
597
|
+
dryRun: true,
|
|
598
|
+
});
|
|
599
|
+
|
|
600
|
+
assert.equal(result.settings.action, "would-write");
|
|
601
|
+
assert.equal(await pathExists(fx.settingsPath), false, "nothing written in dry-run");
|
|
602
|
+
} finally {
|
|
603
|
+
await fx.cleanup();
|
|
604
|
+
}
|
|
605
|
+
});
|
|
606
|
+
|
|
607
|
+
it("re-running is idempotent — the second install leaves settings.json unchanged", async () => {
|
|
608
|
+
const fx = await makeSettingsFixture({
|
|
609
|
+
template: settingsTemplate({ enabledPlugins: { "team@official": true } }),
|
|
610
|
+
});
|
|
611
|
+
try {
|
|
612
|
+
await installClaude({ root: fx.root, output: fx.output, settingsPath: fx.settingsPath });
|
|
613
|
+
const first = await readFile(fx.settingsPath, "utf-8");
|
|
614
|
+
|
|
615
|
+
const second = await installClaude({ root: fx.root, output: fx.output, settingsPath: fx.settingsPath });
|
|
616
|
+
assert.equal(second.settings.action, "unchanged");
|
|
617
|
+
assert.equal(await readFile(fx.settingsPath, "utf-8"), first);
|
|
618
|
+
} finally {
|
|
619
|
+
await fx.cleanup();
|
|
620
|
+
}
|
|
621
|
+
});
|
|
622
|
+
|
|
623
|
+
it("replaces a legacy symlinked settings.json with a real file, never writing through the template", async () => {
|
|
624
|
+
const fx = await makeSettingsFixture({
|
|
625
|
+
template: settingsTemplate({ enabledPlugins: { "team@official": true } }),
|
|
626
|
+
});
|
|
627
|
+
try {
|
|
628
|
+
const templatePath = join(fx.root, "claude", "settings.json");
|
|
629
|
+
const templateBefore = await readFile(templatePath, "utf-8");
|
|
630
|
+
await mkdir(dirname(fx.settingsPath), { recursive: true });
|
|
631
|
+
await symlink(templatePath, fx.settingsPath);
|
|
632
|
+
|
|
633
|
+
const result = await installClaude({ root: fx.root, output: fx.output, settingsPath: fx.settingsPath });
|
|
634
|
+
|
|
635
|
+
assert.equal(result.settings.action, "removed-symlink");
|
|
636
|
+
assert.equal(result.settings.removedSymlink, true);
|
|
637
|
+
assert.equal((await lstat(fx.settingsPath)).isSymbolicLink(), false, "symlink replaced by a real file");
|
|
638
|
+
const settings = JSON.parse(await readFile(fx.settingsPath, "utf-8"));
|
|
639
|
+
assert.deepEqual(settings.enabledPlugins, { "team@official": true });
|
|
640
|
+
assert.equal(await readFile(templatePath, "utf-8"), templateBefore, "template never written through");
|
|
641
|
+
} finally {
|
|
642
|
+
await fx.cleanup();
|
|
643
|
+
}
|
|
644
|
+
});
|
|
645
|
+
|
|
646
|
+
it("warns and skips when the package ships no claude/settings.json template", async () => {
|
|
647
|
+
const fx = await makeSettingsFixture({ template: null });
|
|
648
|
+
try {
|
|
649
|
+
const warnings = [];
|
|
650
|
+
const result = await installClaude({
|
|
651
|
+
root: fx.root,
|
|
652
|
+
output: fx.output,
|
|
653
|
+
settingsPath: fx.settingsPath,
|
|
654
|
+
warn: (m) => warnings.push(m),
|
|
655
|
+
});
|
|
656
|
+
|
|
657
|
+
assert.equal(result.settings, null);
|
|
658
|
+
assert.ok(warnings.some((w) => w.includes("claude/settings.json")), "missing template is warned about");
|
|
659
|
+
assert.equal(await pathExists(fx.settingsPath), false, "nothing written without a template");
|
|
660
|
+
} finally {
|
|
661
|
+
await fx.cleanup();
|
|
662
|
+
}
|
|
663
|
+
});
|
|
431
664
|
});
|