@jaybeeuu/agent-cortex 1.38.1 → 1.40.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/skills/style-versioning/SKILL.md +1 -1
- package/.github/workflows/ci.yml +47 -14
- package/AGENTS.md +19 -3
- package/CHANGELOG.md +42 -0
- package/README.md +118 -63
- package/agents/README.md +13 -10
- package/agents/ralph-plan/agent.md +1 -1
- package/agents/ralph-plan.agent.md +1 -1
- package/bin/agent-cortex.mjs +29 -8
- package/bin/installers/claude.mjs +569 -105
- package/bin/installers/copilot.mjs +22 -12
- package/bin/installers/pi.mjs +37 -34
- package/docs/claude-hooks.md +3 -3
- package/docs/ideas/extension-manifests.md +75 -0
- package/docs/ideas/record-ideas-as-beads.md +115 -0
- package/docs/ideas/subagent-visibility.md +72 -0
- package/docs/ideas/task-session-context.md +89 -0
- package/docs/research/skill-design-research.md +4 -4
- package/docs/research/skill-improvements-analysis.md +1 -1
- package/docs/skills/skill-anatomy.md +38 -2
- package/docs/technical-direction/migration-execution-labels.md +132 -0
- package/extensions/agent-modes/README.md +3 -2
- package/extensions/agent-modes/discover.test.ts +30 -7
- package/extensions/agent-modes/discover.ts +1 -1
- package/extensions/skill-stats/index.test.ts +3 -2
- package/lib/cli.mjs +24 -6
- package/package.json +4 -4
- package/plugin.json +1 -1
- package/pnpm-workspace.yaml +1 -1
- package/scripts/build-copilot-agents.mjs +1 -1
- package/scripts/check-no-sync.mjs +49 -0
- package/scripts/lib/compose-agent.mjs +32 -14
- package/skills/engineering/design-an-interface/SKILL.md +78 -58
- package/skills/engineering/grill-with-docs/REFERENCE.md +78 -0
- package/skills/engineering/grill-with-docs/SKILL.md +66 -63
- package/skills/engineering/improve-codebase-architecture/SKILL.md +66 -48
- package/skills/engineering/technical-direction/FORMAT.md +38 -0
- package/skills/engineering/technical-direction/SKILL.md +70 -77
- package/skills/planning/classify-bead/SKILL.md +59 -35
- package/skills/planning/create-task/REFERENCE.md +40 -0
- package/skills/planning/create-task/SKILL.md +70 -66
- package/skills/planning/create-task/scripts/create-chores.ts +6 -1
- package/skills/planning/create-task/scripts/node_modules/.bin/tsc +21 -0
- package/skills/planning/create-task/scripts/node_modules/.bin/tsserver +21 -0
- package/skills/planning/create-task/scripts/node_modules/.bin/tsx +21 -0
- package/skills/planning/prd-to-tasks/REFERENCE.md +66 -0
- package/skills/planning/prd-to-tasks/SKILL.md +86 -170
- package/skills/planning/record-idea/FORMAT.md +44 -0
- package/skills/planning/record-idea/SKILL.md +70 -53
- package/skills/planning/request-refactor-plan/FORMAT.md +45 -0
- package/skills/planning/request-refactor-plan/SKILL.md +57 -46
- package/skills/planning/write-a-prd/FORMAT.md +54 -0
- package/skills/planning/write-a-prd/SKILL.md +49 -48
- package/skills/planning/write-a-ticket/SKILL.md +68 -53
- package/skills/productivity/handoff/SKILL.md +12 -0
- package/skills/productivity/init-beads/SKILL.md +48 -40
- package/skills/productivity/validate-agent-dir/scripts/node_modules/.bin/tsc +21 -0
- package/skills/productivity/validate-agent-dir/scripts/node_modules/.bin/tsserver +21 -0
- package/skills/productivity/validate-agent-dir/scripts/node_modules/.bin/tsx +21 -0
- package/skills/productivity/write-a-skill/SKILL.md +2 -1
- package/skills/review/maintain-agent-docs/SKILL.md +71 -51
- package/skills/workflow/hitl-collab/SKILL.md +86 -17
- package/skills/workflow/ralph/REFERENCE.md +22 -17
- package/skills/workflow/ralph/SKILL.md +88 -74
- package/skills/workflow/run-pipeline-stage/REFERENCE.md +22 -0
- package/skills/workflow/run-pipeline-stage/SKILL.md +91 -93
- package/skills/workflow/run-pipeline-stage/scripts/generate-progress.test.ts +6 -107
- package/skills/workflow/run-pipeline-stage/scripts/node_modules/.bin/tsc +21 -0
- package/skills/workflow/run-pipeline-stage/scripts/node_modules/.bin/tsserver +21 -0
- package/skills/workflow/run-pipeline-stage/scripts/node_modules/.bin/tsx +21 -0
- package/test/claude-installer.test.mjs +308 -0
- package/test/cli.test.mjs +40 -30
- package/test/copilot-installer.test.mjs +92 -82
- package/test/helpers/fake-claude.mjs +152 -0
- package/test/installer.test.mjs +309 -57
- package/test/pi-installer.test.mjs +115 -99
- package/token-map.README.md +21 -10
- package/token-map.json +7 -6
- package/.claude-plugin/marketplace.json +0 -16
- package/claude/.claude-plugin/plugin.json +0 -11
- package/claude/agents/plan.md +0 -58
- package/claude/agents/ralph-plan.md +0 -121
- package/claude/agents/ralph.md +0 -193
- package/claude/agents/strategy.md +0 -111
- package/claude/hooks/scripts/notify.mjs +0 -54
- package/claude/hooks.json +0 -38
- /package/{claude → claude-extras}/.mcp.json +0 -0
- /package/{claude → claude-extras}/scripts/statusline-command.sh +0 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
|
|
3
|
+
|
|
4
|
+
case `uname` in
|
|
5
|
+
*CYGWIN*|*MINGW*|*MSYS*)
|
|
6
|
+
if command -v cygpath > /dev/null 2>&1; then
|
|
7
|
+
basedir=`cygpath -w "$basedir"`
|
|
8
|
+
fi
|
|
9
|
+
;;
|
|
10
|
+
esac
|
|
11
|
+
|
|
12
|
+
if [ -z "$NODE_PATH" ]; then
|
|
13
|
+
export NODE_PATH="/home/runner/work/agent-cortex/agent-cortex/node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/node_modules:/home/runner/work/agent-cortex/agent-cortex/node_modules/.pnpm/typescript@5.9.3/node_modules:/home/runner/work/agent-cortex/agent-cortex/node_modules/.pnpm/node_modules"
|
|
14
|
+
else
|
|
15
|
+
export NODE_PATH="/home/runner/work/agent-cortex/agent-cortex/node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/node_modules:/home/runner/work/agent-cortex/agent-cortex/node_modules/.pnpm/typescript@5.9.3/node_modules:/home/runner/work/agent-cortex/agent-cortex/node_modules/.pnpm/node_modules:$NODE_PATH"
|
|
16
|
+
fi
|
|
17
|
+
if [ -x "$basedir/node" ]; then
|
|
18
|
+
exec "$basedir/node" "$basedir/../typescript/bin/tsc" "$@"
|
|
19
|
+
else
|
|
20
|
+
exec node "$basedir/../typescript/bin/tsc" "$@"
|
|
21
|
+
fi
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
|
|
3
|
+
|
|
4
|
+
case `uname` in
|
|
5
|
+
*CYGWIN*|*MINGW*|*MSYS*)
|
|
6
|
+
if command -v cygpath > /dev/null 2>&1; then
|
|
7
|
+
basedir=`cygpath -w "$basedir"`
|
|
8
|
+
fi
|
|
9
|
+
;;
|
|
10
|
+
esac
|
|
11
|
+
|
|
12
|
+
if [ -z "$NODE_PATH" ]; then
|
|
13
|
+
export NODE_PATH="/home/runner/work/agent-cortex/agent-cortex/node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/node_modules:/home/runner/work/agent-cortex/agent-cortex/node_modules/.pnpm/typescript@5.9.3/node_modules:/home/runner/work/agent-cortex/agent-cortex/node_modules/.pnpm/node_modules"
|
|
14
|
+
else
|
|
15
|
+
export NODE_PATH="/home/runner/work/agent-cortex/agent-cortex/node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/node_modules:/home/runner/work/agent-cortex/agent-cortex/node_modules/.pnpm/typescript@5.9.3/node_modules:/home/runner/work/agent-cortex/agent-cortex/node_modules/.pnpm/node_modules:$NODE_PATH"
|
|
16
|
+
fi
|
|
17
|
+
if [ -x "$basedir/node" ]; then
|
|
18
|
+
exec "$basedir/node" "$basedir/../typescript/bin/tsserver" "$@"
|
|
19
|
+
else
|
|
20
|
+
exec node "$basedir/../typescript/bin/tsserver" "$@"
|
|
21
|
+
fi
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
|
|
3
|
+
|
|
4
|
+
case `uname` in
|
|
5
|
+
*CYGWIN*|*MINGW*|*MSYS*)
|
|
6
|
+
if command -v cygpath > /dev/null 2>&1; then
|
|
7
|
+
basedir=`cygpath -w "$basedir"`
|
|
8
|
+
fi
|
|
9
|
+
;;
|
|
10
|
+
esac
|
|
11
|
+
|
|
12
|
+
if [ -z "$NODE_PATH" ]; then
|
|
13
|
+
export NODE_PATH="/home/runner/work/agent-cortex/agent-cortex/node_modules/.pnpm/tsx@4.22.4/node_modules/tsx/node_modules:/home/runner/work/agent-cortex/agent-cortex/node_modules/.pnpm/tsx@4.22.4/node_modules:/home/runner/work/agent-cortex/agent-cortex/node_modules/.pnpm/node_modules"
|
|
14
|
+
else
|
|
15
|
+
export NODE_PATH="/home/runner/work/agent-cortex/agent-cortex/node_modules/.pnpm/tsx@4.22.4/node_modules/tsx/node_modules:/home/runner/work/agent-cortex/agent-cortex/node_modules/.pnpm/tsx@4.22.4/node_modules:/home/runner/work/agent-cortex/agent-cortex/node_modules/.pnpm/node_modules:$NODE_PATH"
|
|
16
|
+
fi
|
|
17
|
+
if [ -x "$basedir/node" ]; then
|
|
18
|
+
exec "$basedir/node" "$basedir/../tsx/dist/cli.mjs" "$@"
|
|
19
|
+
else
|
|
20
|
+
exec node "$basedir/../tsx/dist/cli.mjs" "$@"
|
|
21
|
+
fi
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
// Tests for `agent-cortex install claude` runtime registration: after
|
|
2
|
+
// materialising the plugin into the home install root, the plain install
|
|
3
|
+
// registers the plugin with Claude Code by driving the `claude plugin` CLI.
|
|
4
|
+
// Registration is
|
|
5
|
+
// idempotent by STATE, not by exit code: `marketplace list --json` decides
|
|
6
|
+
// add-vs-update and `plugin list --json` decides install-vs-update against the
|
|
7
|
+
// materialised version. The real CLI is a genuine external system (and its
|
|
8
|
+
// home config must never be touched), so these tests drive the stateful fake
|
|
9
|
+
// from test/helpers/fake-claude.mjs; fixture manifests (makeMarketplaceRoot)
|
|
10
|
+
// stand in for the home install root's generated marketplace manifest.
|
|
11
|
+
|
|
12
|
+
import { describe, it } from "node:test";
|
|
13
|
+
import assert from "node:assert/strict";
|
|
14
|
+
import { execFile } from "node:child_process";
|
|
15
|
+
import { mkdtemp, mkdir, writeFile, rm, readFile, stat } from "node:fs/promises";
|
|
16
|
+
import { tmpdir } from "node:os";
|
|
17
|
+
import { fileURLToPath } from "node:url";
|
|
18
|
+
import { dirname, join } from "node:path";
|
|
19
|
+
import { registerClaude } from "../bin/installers/claude.mjs";
|
|
20
|
+
import { makeFakeClaude, registrationActions } from "./helpers/fake-claude.mjs";
|
|
21
|
+
|
|
22
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
23
|
+
const ROOT = join(__dirname, "..");
|
|
24
|
+
const CLI_PATH = join(ROOT, "bin", "agent-cortex.mjs");
|
|
25
|
+
|
|
26
|
+
function runCli(args, env = {}) {
|
|
27
|
+
return new Promise((resolve) => {
|
|
28
|
+
execFile(
|
|
29
|
+
process.execPath,
|
|
30
|
+
[CLI_PATH, ...args],
|
|
31
|
+
{ env: { ...process.env, ...env } },
|
|
32
|
+
(error, stdout, stderr) => {
|
|
33
|
+
resolve({ exitCode: error ? error.code ?? 1 : 0, stdout, stderr });
|
|
34
|
+
},
|
|
35
|
+
);
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** True when a path exists (stat succeeds). */
|
|
40
|
+
async function pathExists(p) {
|
|
41
|
+
try {
|
|
42
|
+
await stat(p);
|
|
43
|
+
return true;
|
|
44
|
+
} catch {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function makeTmp() {
|
|
50
|
+
return mkdtemp(join(tmpdir(), "agent-cortex-claude-installer-"));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function callLines(log) {
|
|
54
|
+
return (await readFile(log, "utf-8")).trim().split("\n").filter(Boolean);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Registration actions logged at/after line `since` (0 = whole log). */
|
|
58
|
+
async function actionsSince(log, since = 0) {
|
|
59
|
+
return registrationActions((await callLines(log)).slice(since));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** A minimal marketplace tree (as seen at the real install root): the
|
|
63
|
+
* marketplace manifest exposing ./claude plus the materialised plugin.json the
|
|
64
|
+
* fake reads for the installed version. */
|
|
65
|
+
async function makeMarketplaceRoot({ name = "jaybeeuu", plugin = "agent-cortex", version = "1.0.0" } = {}) {
|
|
66
|
+
const root = await mkdtemp(join(tmpdir(), "agent-cortex-marketplace-"));
|
|
67
|
+
await mkdir(join(root, ".claude-plugin"), { recursive: true });
|
|
68
|
+
await writeFile(
|
|
69
|
+
join(root, ".claude-plugin", "marketplace.json"),
|
|
70
|
+
JSON.stringify({
|
|
71
|
+
name,
|
|
72
|
+
owner: { name: "tester" },
|
|
73
|
+
plugins: [{ name: plugin, source: "./claude", description: "test plugin" }],
|
|
74
|
+
}),
|
|
75
|
+
);
|
|
76
|
+
await mkdir(join(root, "claude", ".claude-plugin"), { recursive: true });
|
|
77
|
+
await writeFile(
|
|
78
|
+
join(root, "claude", ".claude-plugin", "plugin.json"),
|
|
79
|
+
JSON.stringify({ name: plugin, version, description: "test plugin" }),
|
|
80
|
+
);
|
|
81
|
+
return root;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ─── registerClaude ──────────────────────────────────────────────────────────
|
|
85
|
+
|
|
86
|
+
describe("registerClaude", () => {
|
|
87
|
+
it("drives marketplace add → install on a fresh registration, skipping the market update/plugin update no-ops", async () => {
|
|
88
|
+
const tmp = await makeTmp();
|
|
89
|
+
const root = await makeMarketplaceRoot();
|
|
90
|
+
const fake = await makeFakeClaude(tmp);
|
|
91
|
+
try {
|
|
92
|
+
const result = await registerClaude({ root, claudeBin: fake.bin });
|
|
93
|
+
assert.equal(result.marketplace, "jaybeeuu");
|
|
94
|
+
assert.equal(result.plugin, "agent-cortex");
|
|
95
|
+
assert.equal(result.dryRun, false);
|
|
96
|
+
assert.equal(result.registered, true);
|
|
97
|
+
assert.deepEqual(result.commands, [
|
|
98
|
+
["plugin", "marketplace", "add", root],
|
|
99
|
+
["plugin", "install", "agent-cortex@jaybeeuu", "-y"],
|
|
100
|
+
]);
|
|
101
|
+
// The availability probe (`plugin --help`) and the state queries
|
|
102
|
+
// (`marketplace list --json`, `plugin list --json`) are not actions.
|
|
103
|
+
assert.deepEqual(await actionsSince(fake.log), [
|
|
104
|
+
`plugin marketplace add ${root}`,
|
|
105
|
+
"plugin install agent-cortex@jaybeeuu -y",
|
|
106
|
+
]);
|
|
107
|
+
} finally {
|
|
108
|
+
await rm(tmp, { recursive: true, force: true });
|
|
109
|
+
await rm(root, { recursive: true, force: true });
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("derives marketplace and plugin names from the manifest", async () => {
|
|
114
|
+
const tmp = await makeTmp();
|
|
115
|
+
const root = await makeMarketplaceRoot({ name: "scratchmkt", plugin: "scratchplug" });
|
|
116
|
+
const fake = await makeFakeClaude(tmp);
|
|
117
|
+
try {
|
|
118
|
+
const result = await registerClaude({ root, claudeBin: fake.bin });
|
|
119
|
+
assert.equal(result.marketplace, "scratchmkt");
|
|
120
|
+
assert.equal(result.plugin, "scratchplug");
|
|
121
|
+
const actions = await actionsSince(fake.log);
|
|
122
|
+
assert.ok(actions.some((c) => c === "plugin install scratchplug@scratchmkt -y"));
|
|
123
|
+
// Fresh registration has no update commands.
|
|
124
|
+
assert.ok(!actions.some((c) => c.startsWith("plugin update")));
|
|
125
|
+
} finally {
|
|
126
|
+
await rm(tmp, { recursive: true, force: true });
|
|
127
|
+
await rm(root, { recursive: true, force: true });
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it("re-running at the same materialised version is the update path: marketplace update only, no re-add or double-install", async () => {
|
|
132
|
+
const tmp = await makeTmp();
|
|
133
|
+
const root = await makeMarketplaceRoot();
|
|
134
|
+
const fake = await makeFakeClaude(tmp);
|
|
135
|
+
try {
|
|
136
|
+
await registerClaude({ root, claudeBin: fake.bin });
|
|
137
|
+
assert.deepEqual(await actionsSince(fake.log), [
|
|
138
|
+
`plugin marketplace add ${root}`,
|
|
139
|
+
"plugin install agent-cortex@jaybeeuu -y",
|
|
140
|
+
]);
|
|
141
|
+
const run1End = (await callLines(fake.log)).length;
|
|
142
|
+
|
|
143
|
+
const second = await registerClaude({ root, claudeBin: fake.bin });
|
|
144
|
+
assert.equal(second.registered, true);
|
|
145
|
+
// Second run: the marketplace is present so only the update path runs;
|
|
146
|
+
// the plugin is already at the materialised version so nothing installs.
|
|
147
|
+
assert.deepEqual(await actionsSince(fake.log, run1End), ["plugin marketplace update jaybeeuu"]);
|
|
148
|
+
assert.ok(second.skipped.some((s) => s.includes("already installed")));
|
|
149
|
+
|
|
150
|
+
// Across both runs the marketplace was added exactly once and the
|
|
151
|
+
// plugin installed exactly once.
|
|
152
|
+
const all = await actionsSince(fake.log);
|
|
153
|
+
assert.equal(all.filter((c) => c.startsWith("plugin marketplace add")).length, 1);
|
|
154
|
+
assert.equal(all.filter((c) => c === "plugin install agent-cortex@jaybeeuu -y").length, 1);
|
|
155
|
+
} finally {
|
|
156
|
+
await rm(tmp, { recursive: true, force: true });
|
|
157
|
+
await rm(root, { recursive: true, force: true });
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it("runs plugin update when a newer version is materialised, refreshing the installed version", async () => {
|
|
162
|
+
const tmp = await makeTmp();
|
|
163
|
+
const root = await makeMarketplaceRoot({ version: "1.0.0" });
|
|
164
|
+
const fake = await makeFakeClaude(tmp);
|
|
165
|
+
try {
|
|
166
|
+
await registerClaude({ root, claudeBin: fake.bin });
|
|
167
|
+
assert.deepEqual(await actionsSince(fake.log), [
|
|
168
|
+
`plugin marketplace add ${root}`,
|
|
169
|
+
"plugin install agent-cortex@jaybeeuu -y",
|
|
170
|
+
]);
|
|
171
|
+
const run1End = (await callLines(fake.log)).length;
|
|
172
|
+
|
|
173
|
+
// A new version is materialised into the source the marketplace exposes.
|
|
174
|
+
await writeFile(
|
|
175
|
+
join(root, "claude", ".claude-plugin", "plugin.json"),
|
|
176
|
+
JSON.stringify({ name: "agent-cortex", version: "2.0.0", description: "test plugin" }),
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
const updated = await registerClaude({ root, claudeBin: fake.bin });
|
|
180
|
+
assert.deepEqual(await actionsSince(fake.log, run1End), [
|
|
181
|
+
"plugin marketplace update jaybeeuu",
|
|
182
|
+
"plugin update agent-cortex -y",
|
|
183
|
+
]);
|
|
184
|
+
assert.ok(updated.skipped.some((s) => s.includes("already installed")));
|
|
185
|
+
const state = JSON.parse(await readFile(fake.state, "utf-8"));
|
|
186
|
+
assert.equal(state.installed[0].version, "2.0.0", "plugin update refreshed the installed version");
|
|
187
|
+
|
|
188
|
+
// At the new version another run is back to update-path-only.
|
|
189
|
+
const run2End = (await callLines(fake.log)).length;
|
|
190
|
+
await registerClaude({ root, claudeBin: fake.bin });
|
|
191
|
+
assert.deepEqual(await actionsSince(fake.log, run2End), ["plugin marketplace update jaybeeuu"]);
|
|
192
|
+
} finally {
|
|
193
|
+
await rm(tmp, { recursive: true, force: true });
|
|
194
|
+
await rm(root, { recursive: true, force: true });
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it("--dry-run plans the commands without spawning claude at all", async () => {
|
|
199
|
+
const tmp = await makeTmp();
|
|
200
|
+
const root = await makeMarketplaceRoot();
|
|
201
|
+
const fake = await makeFakeClaude(tmp);
|
|
202
|
+
try {
|
|
203
|
+
const result = await registerClaude({ root, claudeBin: fake.bin, dryRun: true });
|
|
204
|
+
assert.equal(result.dryRun, true);
|
|
205
|
+
assert.equal(result.registered, true);
|
|
206
|
+
// Dry-run cannot know live state without spawning — no commands decided.
|
|
207
|
+
assert.deepEqual(result.commands, []);
|
|
208
|
+
// No registration spawns happened at all in dry-run mode.
|
|
209
|
+
assert.equal(await pathExists(fake.log), false);
|
|
210
|
+
} finally {
|
|
211
|
+
await rm(tmp, { recursive: true, force: true });
|
|
212
|
+
await rm(root, { recursive: true, force: true });
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it("defaults the claude binary to `claude` from PATH (dry-run only — no spawn)", async () => {
|
|
217
|
+
const root = await makeMarketplaceRoot();
|
|
218
|
+
try {
|
|
219
|
+
const result = await registerClaude({ root, dryRun: true });
|
|
220
|
+
assert.equal(result.marketplace, "jaybeeuu");
|
|
221
|
+
assert.equal(result.plugin, "agent-cortex");
|
|
222
|
+
assert.equal(result.dryRun, true);
|
|
223
|
+
} finally {
|
|
224
|
+
await rm(root, { recursive: true, force: true });
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it("warns with manual commands when the claude binary lacks the plugin subcommand; --require-register fails the run", async () => {
|
|
229
|
+
const tmp = await makeTmp();
|
|
230
|
+
const root = await makeMarketplaceRoot();
|
|
231
|
+
const fake = await makeFakeClaude(tmp, { failWhen: "plugin --help" });
|
|
232
|
+
try {
|
|
233
|
+
const warnings = [];
|
|
234
|
+
const result = await registerClaude({ root, claudeBin: fake.bin, warn: (m) => warnings.push(m) });
|
|
235
|
+
assert.equal(result.registered, false);
|
|
236
|
+
const manual = warnings.join("\n");
|
|
237
|
+
assert.match(manual, /NOT registered/);
|
|
238
|
+
assert.match(manual, new RegExp(`claude plugin marketplace add ${root.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}`));
|
|
239
|
+
assert.match(manual, /claude plugin install agent-cortex@jaybeeuu -y/);
|
|
240
|
+
|
|
241
|
+
await assert.rejects(
|
|
242
|
+
registerClaude({ root, claudeBin: fake.bin, requireRegister: true }),
|
|
243
|
+
/require-register/,
|
|
244
|
+
);
|
|
245
|
+
} finally {
|
|
246
|
+
await rm(tmp, { recursive: true, force: true });
|
|
247
|
+
await rm(root, { recursive: true, force: true });
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
it("fails, naming the failing command, when a claude plugin command errors", async () => {
|
|
252
|
+
const tmp = await makeTmp();
|
|
253
|
+
const root = await makeMarketplaceRoot();
|
|
254
|
+
const fake = await makeFakeClaude(tmp, { failWhen: "plugin install" });
|
|
255
|
+
try {
|
|
256
|
+
await assert.rejects(
|
|
257
|
+
registerClaude({ root, claudeBin: fake.bin }),
|
|
258
|
+
/claude plugin install agent-cortex@jaybeeuu -y/,
|
|
259
|
+
);
|
|
260
|
+
} finally {
|
|
261
|
+
await rm(tmp, { recursive: true, force: true });
|
|
262
|
+
await rm(root, { recursive: true, force: true });
|
|
263
|
+
}
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
it("fails when the checkout has no marketplace manifest", async () => {
|
|
267
|
+
const tmp = await makeTmp();
|
|
268
|
+
const root = await mkdtemp(join(tmpdir(), "agent-cortex-no-market-"));
|
|
269
|
+
const fake = await makeFakeClaude(tmp);
|
|
270
|
+
try {
|
|
271
|
+
await assert.rejects(
|
|
272
|
+
registerClaude({ root, claudeBin: fake.bin }),
|
|
273
|
+
/marketplace\.json/,
|
|
274
|
+
);
|
|
275
|
+
// The failed manifest read happens before any claude invocation.
|
|
276
|
+
assert.equal(await pathExists(fake.log), false);
|
|
277
|
+
} finally {
|
|
278
|
+
await rm(tmp, { recursive: true, force: true });
|
|
279
|
+
await rm(root, { recursive: true, force: true });
|
|
280
|
+
}
|
|
281
|
+
});
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
// ─── CLI wiring ───────────────────────────────────────────────────────────────
|
|
285
|
+
|
|
286
|
+
describe("install claude CLI: registration", () => {
|
|
287
|
+
it("--dry-run without --output prints the registration plan and writes nothing", async () => {
|
|
288
|
+
const { exitCode, stdout } = await runCli(["install", "claude", "--dry-run"]);
|
|
289
|
+
assert.equal(exitCode, 0);
|
|
290
|
+
assert.ok(stdout.includes("would run: claude plugin marketplace add"));
|
|
291
|
+
assert.ok(stdout.includes("claude plugin install agent-cortex@jaybeeuu"));
|
|
292
|
+
assert.ok(stdout.includes("plugin marketplace update"));
|
|
293
|
+
assert.ok(stdout.includes("plugin update agent-cortex"));
|
|
294
|
+
assert.ok(/dry-run/i.test(stdout));
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
it("--output <dir> generates only — no registration plan is printed", async () => {
|
|
298
|
+
const out = await mkdtemp(join(tmpdir(), "agent-cortex-cli-out-"));
|
|
299
|
+
try {
|
|
300
|
+
const { exitCode, stdout } = await runCli(["install", "claude", "--output", out]);
|
|
301
|
+
assert.equal(exitCode, 0);
|
|
302
|
+
assert.doesNotMatch(stdout, /claude plugin/);
|
|
303
|
+
assert.ok(stdout.includes("Generated"));
|
|
304
|
+
} finally {
|
|
305
|
+
await rm(out, { recursive: true, force: true });
|
|
306
|
+
}
|
|
307
|
+
});
|
|
308
|
+
});
|
package/test/cli.test.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { describe, it } from "node:test";
|
|
2
2
|
import assert from "node:assert/strict";
|
|
3
3
|
import { execFile } from "node:child_process";
|
|
4
|
-
import {
|
|
4
|
+
import { mkdtemp, readFile, rm } from "node:fs/promises";
|
|
5
5
|
import { tmpdir } from "node:os";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
7
|
import { dirname, join } from "node:path";
|
|
@@ -10,6 +10,16 @@ import { parseArgs, buildHelpText, validateHarness } from "../lib/cli.mjs";
|
|
|
10
10
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
11
11
|
const CLI_PATH = join(__dirname, "..", "bin", "agent-cortex.mjs");
|
|
12
12
|
|
|
13
|
+
/** True when a path exists (stat succeeds). */
|
|
14
|
+
async function pathExists(p) {
|
|
15
|
+
try {
|
|
16
|
+
await stat(p);
|
|
17
|
+
return true;
|
|
18
|
+
} catch {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
13
23
|
function runCli(args) {
|
|
14
24
|
return new Promise((resolve) => {
|
|
15
25
|
execFile(process.execPath, [CLI_PATH, ...args], (error, stdout, stderr) => {
|
|
@@ -23,91 +33,91 @@ function runCli(args) {
|
|
|
23
33
|
}
|
|
24
34
|
|
|
25
35
|
describe("parseArgs", () => {
|
|
26
|
-
it("returns { command: 'help' } when no arguments given", () => {
|
|
36
|
+
it("returns { command: 'help' } when no arguments given", async () => {
|
|
27
37
|
const result = parseArgs([]);
|
|
28
38
|
assert.deepStrictEqual(result, { command: "help" });
|
|
29
39
|
});
|
|
30
40
|
|
|
31
|
-
it("returns { command: 'help' } for --help flag", () => {
|
|
41
|
+
it("returns { command: 'help' } for --help flag", async () => {
|
|
32
42
|
const result = parseArgs(["--help"]);
|
|
33
43
|
assert.deepStrictEqual(result, { command: "help" });
|
|
34
44
|
});
|
|
35
45
|
|
|
36
|
-
it("returns { command: 'help' } for -h flag", () => {
|
|
46
|
+
it("returns { command: 'help' } for -h flag", async () => {
|
|
37
47
|
const result = parseArgs(["-h"]);
|
|
38
48
|
assert.deepStrictEqual(result, { command: "help" });
|
|
39
49
|
});
|
|
40
50
|
|
|
41
|
-
it("returns { command: 'install', harness: 'copilot' } for install copilot", () => {
|
|
51
|
+
it("returns { command: 'install', harness: 'copilot' } for install copilot", async () => {
|
|
42
52
|
const result = parseArgs(["install", "copilot"]);
|
|
43
53
|
assert.deepStrictEqual(result, { command: "install", harness: "copilot" });
|
|
44
54
|
});
|
|
45
55
|
|
|
46
|
-
it("returns { command: 'install', harness: 'claude' } for install claude", () => {
|
|
56
|
+
it("returns { command: 'install', harness: 'claude' } for install claude", async () => {
|
|
47
57
|
const result = parseArgs(["install", "claude"]);
|
|
48
58
|
assert.deepStrictEqual(result, { command: "install", harness: "claude" });
|
|
49
59
|
});
|
|
50
60
|
|
|
51
|
-
it("returns { command: 'install', harness: 'pi' } for install pi", () => {
|
|
61
|
+
it("returns { command: 'install', harness: 'pi' } for install pi", async () => {
|
|
52
62
|
const result = parseArgs(["install", "pi"]);
|
|
53
63
|
assert.deepStrictEqual(result, { command: "install", harness: "pi" });
|
|
54
64
|
});
|
|
55
65
|
|
|
56
|
-
it("returns { command: 'install', harness: null } when install has no harness", () => {
|
|
66
|
+
it("returns { command: 'install', harness: null } when install has no harness", async () => {
|
|
57
67
|
const result = parseArgs(["install"]);
|
|
58
68
|
assert.deepStrictEqual(result, { command: "install", harness: null });
|
|
59
69
|
});
|
|
60
70
|
|
|
61
|
-
it("returns output for install claude --output <dir>", () => {
|
|
71
|
+
it("returns output for install claude --output <dir>", async () => {
|
|
62
72
|
const result = parseArgs(["install", "claude", "--output", "/tmp/x"]);
|
|
63
73
|
assert.deepStrictEqual(result, { command: "install", harness: "claude", output: "/tmp/x" });
|
|
64
74
|
});
|
|
65
75
|
|
|
66
|
-
it("returns output for install claude --output=<dir>", () => {
|
|
76
|
+
it("returns output for install claude --output=<dir>", async () => {
|
|
67
77
|
const result = parseArgs(["install", "claude", "--output=/tmp/x"]);
|
|
68
78
|
assert.deepStrictEqual(result, { command: "install", harness: "claude", output: "/tmp/x" });
|
|
69
79
|
});
|
|
70
80
|
|
|
71
|
-
it("returns dryRun for install claude --dry-run", () => {
|
|
81
|
+
it("returns dryRun for install claude --dry-run", async () => {
|
|
72
82
|
const result = parseArgs(["install", "claude", "--dry-run"]);
|
|
73
83
|
assert.deepStrictEqual(result, { command: "install", harness: "claude", dryRun: true });
|
|
74
84
|
});
|
|
75
85
|
|
|
76
|
-
it("combines --dry-run with --output", () => {
|
|
86
|
+
it("combines --dry-run with --output", async () => {
|
|
77
87
|
const result = parseArgs(["install", "claude", "--output", "/tmp/x", "--dry-run"]);
|
|
78
88
|
assert.deepStrictEqual(result, { command: "install", harness: "claude", output: "/tmp/x", dryRun: true });
|
|
79
89
|
});
|
|
80
90
|
|
|
81
|
-
it("reports an unknown option for claude", () => {
|
|
91
|
+
it("reports an unknown option for claude", async () => {
|
|
82
92
|
const result = parseArgs(["install", "claude", "--bogus"]);
|
|
83
93
|
assert.equal(result.command, "install");
|
|
84
94
|
assert.equal(result.harness, "claude");
|
|
85
95
|
assert.ok(result.optionError.includes("--bogus"));
|
|
86
96
|
});
|
|
87
97
|
|
|
88
|
-
it("reports a missing value for --output for claude", () => {
|
|
98
|
+
it("reports a missing value for --output for claude", async () => {
|
|
89
99
|
const result = parseArgs(["install", "claude", "--output"]);
|
|
90
100
|
assert.equal(result.command, "install");
|
|
91
101
|
assert.equal(result.harness, "claude");
|
|
92
102
|
assert.ok(result.optionError.includes("--output"));
|
|
93
103
|
});
|
|
94
104
|
|
|
95
|
-
it("parses --dry-run for install", () => {
|
|
105
|
+
it("parses --dry-run for install", async () => {
|
|
96
106
|
const result = parseArgs(["install", "pi", "--dry-run"]);
|
|
97
107
|
assert.deepStrictEqual(result, { command: "install", harness: "pi", dryRun: true });
|
|
98
108
|
});
|
|
99
109
|
|
|
100
|
-
it("parses --output <dir> for install", () => {
|
|
110
|
+
it("parses --output <dir> for install", async () => {
|
|
101
111
|
const result = parseArgs(["install", "pi", "--output", "/tmp/out"]);
|
|
102
112
|
assert.deepStrictEqual(result, { command: "install", harness: "pi", output: "/tmp/out" });
|
|
103
113
|
});
|
|
104
114
|
|
|
105
|
-
it("parses --plugin-root <dir> for install", () => {
|
|
115
|
+
it("parses --plugin-root <dir> for install", async () => {
|
|
106
116
|
const result = parseArgs(["install", "pi", "--plugin-root", "/tmp/plugin"]);
|
|
107
117
|
assert.deepStrictEqual(result, { command: "install", harness: "pi", pluginRoot: "/tmp/plugin" });
|
|
108
118
|
});
|
|
109
119
|
|
|
110
|
-
it("combines multiple install options", () => {
|
|
120
|
+
it("combines multiple install options", async () => {
|
|
111
121
|
const result = parseArgs(["install", "pi", "--dry-run", "--output", "/tmp/out", "--plugin-root", "/tmp/plugin"]);
|
|
112
122
|
assert.deepStrictEqual(result, {
|
|
113
123
|
command: "install",
|
|
@@ -118,32 +128,32 @@ describe("parseArgs", () => {
|
|
|
118
128
|
});
|
|
119
129
|
});
|
|
120
130
|
|
|
121
|
-
it("reports a missing value for --output", () => {
|
|
131
|
+
it("reports a missing value for --output", async () => {
|
|
122
132
|
const result = parseArgs(["install", "pi", "--output"]);
|
|
123
133
|
assert.equal(result.command, "install");
|
|
124
134
|
assert.equal(result.harness, "pi");
|
|
125
135
|
assert.ok(result.optionError.includes("--output"));
|
|
126
136
|
});
|
|
127
137
|
|
|
128
|
-
it("reports an unknown option", () => {
|
|
138
|
+
it("reports an unknown option", async () => {
|
|
129
139
|
const result = parseArgs(["install", "pi", "--bogus"]);
|
|
130
140
|
assert.equal(result.command, "install");
|
|
131
141
|
assert.equal(result.harness, "pi");
|
|
132
142
|
assert.ok(result.optionError.includes("--bogus"));
|
|
133
143
|
});
|
|
134
144
|
|
|
135
|
-
it("keeps the old shape when no options are given", () => {
|
|
145
|
+
it("keeps the old shape when no options are given", async () => {
|
|
136
146
|
assert.deepStrictEqual(parseArgs(["install", "pi"]), { command: "install", harness: "pi" });
|
|
137
147
|
});
|
|
138
148
|
|
|
139
|
-
it("returns { command: 'unknown', name: 'foo' } for unknown command", () => {
|
|
149
|
+
it("returns { command: 'unknown', name: 'foo' } for unknown command", async () => {
|
|
140
150
|
const result = parseArgs(["foo"]);
|
|
141
151
|
assert.deepStrictEqual(result, { command: "unknown", name: "foo" });
|
|
142
152
|
});
|
|
143
153
|
});
|
|
144
154
|
|
|
145
155
|
describe("buildHelpText", () => {
|
|
146
|
-
it("returns a string containing usage info", () => {
|
|
156
|
+
it("returns a string containing usage info", async () => {
|
|
147
157
|
const text = buildHelpText();
|
|
148
158
|
assert.ok(typeof text === "string");
|
|
149
159
|
assert.ok(text.includes("agent-cortex"));
|
|
@@ -152,19 +162,19 @@ describe("buildHelpText", () => {
|
|
|
152
162
|
});
|
|
153
163
|
|
|
154
164
|
describe("validateHarness", () => {
|
|
155
|
-
it("returns ok for supported harnesses", () => {
|
|
165
|
+
it("returns ok for supported harnesses", async () => {
|
|
156
166
|
for (const h of ["copilot", "claude", "pi"]) {
|
|
157
167
|
assert.deepStrictEqual(validateHarness(h), { ok: true });
|
|
158
168
|
}
|
|
159
169
|
});
|
|
160
170
|
|
|
161
|
-
it("returns error when harness is null", () => {
|
|
171
|
+
it("returns error when harness is null", async () => {
|
|
162
172
|
const result = validateHarness(null);
|
|
163
173
|
assert.equal(result.ok, false);
|
|
164
174
|
assert.ok(result.error.includes("Missing harness"));
|
|
165
175
|
});
|
|
166
176
|
|
|
167
|
-
it("returns error for unknown harness", () => {
|
|
177
|
+
it("returns error for unknown harness", async () => {
|
|
168
178
|
const result = validateHarness("docker");
|
|
169
179
|
assert.equal(result.ok, false);
|
|
170
180
|
assert.ok(result.error.includes("Unknown harness"));
|
|
@@ -198,7 +208,7 @@ describe("CLI integration", () => {
|
|
|
198
208
|
});
|
|
199
209
|
|
|
200
210
|
it("exits 0 for install claude (into a temp output dir)", async () => {
|
|
201
|
-
const dir =
|
|
211
|
+
const dir = await mkdtemp(join(tmpdir(), "agent-cortex-cli-"));
|
|
202
212
|
const { exitCode, stdout } = await runCli(["install", "claude", "--output", dir]);
|
|
203
213
|
assert.equal(exitCode, 0);
|
|
204
214
|
assert.ok(stdout.includes("claude"));
|
|
@@ -236,15 +246,15 @@ describe("CLI integration", () => {
|
|
|
236
246
|
});
|
|
237
247
|
|
|
238
248
|
it("exits 0 for install pi --output <tmp> and writes agent files", async () => {
|
|
239
|
-
const outDir =
|
|
249
|
+
const outDir = await mkdtemp(join(tmpdir(), "cli-pi-install-"));
|
|
240
250
|
try {
|
|
241
251
|
const { exitCode, stdout } = await runCli(["install", "pi", "--output", outDir]);
|
|
242
252
|
assert.equal(exitCode, 0);
|
|
243
253
|
assert.ok(stdout.includes("ralph.agent.md"));
|
|
244
|
-
const ralph =
|
|
254
|
+
const ralph = await readFile(join(outDir, "agents", "ralph.agent.md"), "utf-8");
|
|
245
255
|
assert.ok(ralph.includes("agent-cortex:ralph"));
|
|
246
256
|
} finally {
|
|
247
|
-
|
|
257
|
+
await rm(outDir, { recursive: true, force: true });
|
|
248
258
|
}
|
|
249
259
|
});
|
|
250
260
|
|