first-tree 0.0.2 → 0.0.4
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 +116 -40
- package/dist/cli.js +46 -17
- package/dist/help-Dtdj91HJ.js +25 -0
- package/dist/init--VepFe6N.js +403 -0
- package/dist/installer-cH7N4RNj.js +47 -0
- package/dist/onboarding-C9cYSE6F.js +2 -0
- package/dist/onboarding-CPP8fF4D.js +10 -0
- package/dist/repo-DY57bMqr.js +318 -0
- package/dist/upgrade-Cgx_K2HM.js +135 -0
- package/dist/{verify-CSRIkuoM.js → verify-mC9ZTd1f.js} +118 -29
- package/package.json +33 -10
- package/skills/first-tree/SKILL.md +113 -0
- package/skills/first-tree/agents/openai.yaml +4 -0
- package/skills/first-tree/assets/framework/VERSION +1 -0
- package/skills/first-tree/assets/framework/examples/claude-code/README.md +14 -0
- package/skills/first-tree/assets/framework/examples/claude-code/settings.json +14 -0
- package/skills/first-tree/assets/framework/helpers/generate-codeowners.ts +224 -0
- package/skills/first-tree/assets/framework/helpers/inject-tree-context.sh +15 -0
- package/skills/first-tree/assets/framework/helpers/run-review.ts +193 -0
- package/skills/first-tree/assets/framework/manifest.json +11 -0
- package/skills/first-tree/assets/framework/prompts/pr-review.md +38 -0
- package/skills/first-tree/assets/framework/templates/agents.md.template +49 -0
- package/skills/first-tree/assets/framework/templates/member-node.md.template +18 -0
- package/skills/first-tree/assets/framework/templates/members-domain.md.template +45 -0
- package/skills/first-tree/assets/framework/templates/root-node.md.template +41 -0
- package/skills/first-tree/assets/framework/workflows/codeowners.yml +31 -0
- package/skills/first-tree/assets/framework/workflows/pr-review.yml +146 -0
- package/skills/first-tree/assets/framework/workflows/validate.yml +19 -0
- package/skills/first-tree/engine/commands/help.ts +32 -0
- package/skills/first-tree/engine/commands/init.ts +1 -0
- package/skills/first-tree/engine/commands/upgrade.ts +1 -0
- package/skills/first-tree/engine/commands/verify.ts +1 -0
- package/skills/first-tree/engine/init.ts +414 -0
- package/skills/first-tree/engine/onboarding.ts +10 -0
- package/skills/first-tree/engine/repo.ts +360 -0
- package/skills/first-tree/engine/rules/agent-instructions.ts +59 -0
- package/skills/first-tree/engine/rules/agent-integration.ts +19 -0
- package/skills/first-tree/engine/rules/ci-validation.ts +72 -0
- package/skills/first-tree/engine/rules/framework.ts +13 -0
- package/skills/first-tree/engine/rules/index.ts +41 -0
- package/skills/first-tree/engine/rules/members.ts +21 -0
- package/skills/first-tree/engine/rules/populate-tree.ts +36 -0
- package/skills/first-tree/engine/rules/root-node.ts +41 -0
- package/skills/first-tree/engine/runtime/adapters.ts +22 -0
- package/skills/first-tree/engine/runtime/asset-loader.ts +141 -0
- package/skills/first-tree/engine/runtime/installer.ts +82 -0
- package/skills/first-tree/engine/runtime/upgrader.ts +23 -0
- package/skills/first-tree/engine/upgrade.ts +233 -0
- package/skills/first-tree/engine/validators/members.ts +215 -0
- package/skills/first-tree/engine/validators/nodes.ts +559 -0
- package/skills/first-tree/engine/verify.ts +155 -0
- package/skills/first-tree/references/about.md +36 -0
- package/skills/first-tree/references/maintainer-architecture.md +59 -0
- package/skills/first-tree/references/maintainer-build-and-distribution.md +59 -0
- package/skills/first-tree/references/maintainer-testing.md +58 -0
- package/skills/first-tree/references/maintainer-thin-cli.md +38 -0
- package/skills/first-tree/references/onboarding.md +185 -0
- package/skills/first-tree/references/ownership-and-naming.md +94 -0
- package/skills/first-tree/references/principles.md +113 -0
- package/skills/first-tree/references/source-map.md +94 -0
- package/skills/first-tree/references/upgrade-contract.md +94 -0
- package/skills/first-tree/scripts/check-skill-sync.sh +133 -0
- package/skills/first-tree/scripts/quick_validate.py +95 -0
- package/skills/first-tree/scripts/run-local-cli.sh +35 -0
- package/skills/first-tree/tests/asset-loader.test.ts +75 -0
- package/skills/first-tree/tests/generate-codeowners.test.ts +94 -0
- package/skills/first-tree/tests/helpers.ts +169 -0
- package/skills/first-tree/tests/init.test.ts +250 -0
- package/skills/first-tree/tests/repo.test.ts +440 -0
- package/skills/first-tree/tests/rules.test.ts +413 -0
- package/skills/first-tree/tests/run-review.test.ts +155 -0
- package/skills/first-tree/tests/skill-artifacts.test.ts +311 -0
- package/skills/first-tree/tests/thin-cli.test.ts +104 -0
- package/skills/first-tree/tests/upgrade.test.ts +103 -0
- package/skills/first-tree/tests/validate-members.test.ts +224 -0
- package/skills/first-tree/tests/validate-nodes.test.ts +198 -0
- package/skills/first-tree/tests/verify.test.ts +241 -0
- package/dist/init-CE_944sb.js +0 -283
- package/dist/repo-BByc3VvM.js +0 -111
- package/dist/upgrade-Chr7z0CY.js +0 -82
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
import { execFileSync, spawnSync } from "node:child_process";
|
|
2
|
+
import { existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { describe, expect, it } from "vitest";
|
|
6
|
+
import { parse } from "yaml";
|
|
7
|
+
|
|
8
|
+
const ROOT = process.cwd();
|
|
9
|
+
|
|
10
|
+
/** Check if a path has any tracked files in git (handles dirs and files). */
|
|
11
|
+
function isTrackedInGit(relativePath: string): boolean {
|
|
12
|
+
const result = spawnSync("git", ["ls-files", relativePath], {
|
|
13
|
+
cwd: ROOT,
|
|
14
|
+
stdio: "pipe",
|
|
15
|
+
});
|
|
16
|
+
return (result.stdout?.toString().trim().length ?? 0) > 0;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
describe("skill artifacts", () => {
|
|
20
|
+
it("keeps only the canonical skill in the source repo", () => {
|
|
21
|
+
expect(existsSync(join(ROOT, "skills", "first-tree", "SKILL.md"))).toBe(true);
|
|
22
|
+
expect(existsSync(join(ROOT, "skills", "first-tree", "references", "onboarding.md"))).toBe(true);
|
|
23
|
+
expect(existsSync(join(ROOT, "skills", "first-tree", "assets", "framework", "manifest.json"))).toBe(true);
|
|
24
|
+
expect(existsSync(join(ROOT, "skills", "first-tree", "engine", "init.ts"))).toBe(true);
|
|
25
|
+
expect(existsSync(join(ROOT, "AGENTS.md"))).toBe(true);
|
|
26
|
+
expect(existsSync(join(ROOT, "skills", "first-tree", "tests", "init.test.ts"))).toBe(
|
|
27
|
+
true,
|
|
28
|
+
);
|
|
29
|
+
expect(
|
|
30
|
+
existsSync(
|
|
31
|
+
join(ROOT, "evals", "context-tree-eval.test.ts"),
|
|
32
|
+
),
|
|
33
|
+
).toBe(true);
|
|
34
|
+
expect(
|
|
35
|
+
existsSync(
|
|
36
|
+
join(
|
|
37
|
+
ROOT,
|
|
38
|
+
"skills",
|
|
39
|
+
"first-tree",
|
|
40
|
+
"engine",
|
|
41
|
+
"runtime",
|
|
42
|
+
"asset-loader.ts",
|
|
43
|
+
),
|
|
44
|
+
),
|
|
45
|
+
).toBe(true);
|
|
46
|
+
expect(
|
|
47
|
+
existsSync(
|
|
48
|
+
join(
|
|
49
|
+
ROOT,
|
|
50
|
+
"skills",
|
|
51
|
+
"first-tree",
|
|
52
|
+
"engine",
|
|
53
|
+
"rules",
|
|
54
|
+
"index.ts",
|
|
55
|
+
),
|
|
56
|
+
),
|
|
57
|
+
).toBe(true);
|
|
58
|
+
expect(
|
|
59
|
+
existsSync(
|
|
60
|
+
join(
|
|
61
|
+
ROOT,
|
|
62
|
+
"skills",
|
|
63
|
+
"first-tree",
|
|
64
|
+
"engine",
|
|
65
|
+
"validators",
|
|
66
|
+
"nodes.ts",
|
|
67
|
+
),
|
|
68
|
+
),
|
|
69
|
+
).toBe(true);
|
|
70
|
+
expect(
|
|
71
|
+
existsSync(
|
|
72
|
+
join(
|
|
73
|
+
ROOT,
|
|
74
|
+
"skills",
|
|
75
|
+
"first-tree",
|
|
76
|
+
"references",
|
|
77
|
+
"maintainer-architecture.md",
|
|
78
|
+
),
|
|
79
|
+
),
|
|
80
|
+
).toBe(true);
|
|
81
|
+
expect(
|
|
82
|
+
existsSync(
|
|
83
|
+
join(
|
|
84
|
+
ROOT,
|
|
85
|
+
"skills",
|
|
86
|
+
"first-tree",
|
|
87
|
+
"references",
|
|
88
|
+
"maintainer-thin-cli.md",
|
|
89
|
+
),
|
|
90
|
+
),
|
|
91
|
+
).toBe(true);
|
|
92
|
+
expect(
|
|
93
|
+
existsSync(
|
|
94
|
+
join(
|
|
95
|
+
ROOT,
|
|
96
|
+
"skills",
|
|
97
|
+
"first-tree",
|
|
98
|
+
"references",
|
|
99
|
+
"maintainer-build-and-distribution.md",
|
|
100
|
+
),
|
|
101
|
+
),
|
|
102
|
+
).toBe(true);
|
|
103
|
+
expect(
|
|
104
|
+
existsSync(
|
|
105
|
+
join(
|
|
106
|
+
ROOT,
|
|
107
|
+
"skills",
|
|
108
|
+
"first-tree",
|
|
109
|
+
"references",
|
|
110
|
+
"maintainer-testing.md",
|
|
111
|
+
),
|
|
112
|
+
),
|
|
113
|
+
).toBe(true);
|
|
114
|
+
// Legacy artifacts must not be tracked in git (untracked local files are OK)
|
|
115
|
+
expect(isTrackedInGit(".agents")).toBe(false);
|
|
116
|
+
expect(isTrackedInGit(".claude")).toBe(false);
|
|
117
|
+
expect(isTrackedInGit(".context-tree")).toBe(false);
|
|
118
|
+
expect(existsSync(join(ROOT, "AGENT.md"))).toBe(false);
|
|
119
|
+
expect(isTrackedInGit("skills/first-tree-cli-framework")).toBe(false);
|
|
120
|
+
expect(isTrackedInGit("docs")).toBe(false);
|
|
121
|
+
expect(isTrackedInGit("tests")).toBe(false);
|
|
122
|
+
expect(existsSync(join(ROOT, "evals"))).toBe(true);
|
|
123
|
+
expect(existsSync(join(ROOT, "src", "commands"))).toBe(false);
|
|
124
|
+
expect(existsSync(join(ROOT, "src", "runtime"))).toBe(false);
|
|
125
|
+
expect(existsSync(join(ROOT, "src", "rules"))).toBe(false);
|
|
126
|
+
expect(existsSync(join(ROOT, "src", "validators"))).toBe(false);
|
|
127
|
+
expect(existsSync(join(ROOT, "src", "init.ts"))).toBe(false);
|
|
128
|
+
expect(existsSync(join(ROOT, "src", "verify.ts"))).toBe(false);
|
|
129
|
+
expect(existsSync(join(ROOT, "src", "upgrade.ts"))).toBe(false);
|
|
130
|
+
expect(existsSync(join(ROOT, "src", "repo.ts"))).toBe(false);
|
|
131
|
+
expect(existsSync(join(ROOT, "src", "onboarding.ts"))).toBe(false);
|
|
132
|
+
expect(
|
|
133
|
+
existsSync(join(ROOT, "skills", "first-tree", "references", "repo-snapshot")),
|
|
134
|
+
).toBe(false);
|
|
135
|
+
expect(existsSync(join(ROOT, "skills", "first-tree", "evals"))).toBe(false);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it("passes skill validation helpers", () => {
|
|
139
|
+
execFileSync(
|
|
140
|
+
"python3",
|
|
141
|
+
["./skills/first-tree/scripts/quick_validate.py", "./skills/first-tree"],
|
|
142
|
+
{
|
|
143
|
+
cwd: ROOT,
|
|
144
|
+
stdio: "pipe",
|
|
145
|
+
encoding: "utf-8",
|
|
146
|
+
},
|
|
147
|
+
);
|
|
148
|
+
execFileSync("bash", ["./skills/first-tree/scripts/check-skill-sync.sh"], {
|
|
149
|
+
cwd: ROOT,
|
|
150
|
+
stdio: "pipe",
|
|
151
|
+
encoding: "utf-8",
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it("ships the canonical skill in the published tarball", () => {
|
|
156
|
+
const packDir = mkdtempSync(join(tmpdir(), "first-tree-pack-"));
|
|
157
|
+
try {
|
|
158
|
+
const pkg = JSON.parse(readFileSync(join(ROOT, "package.json"), "utf-8")) as {
|
|
159
|
+
name: string;
|
|
160
|
+
version: string;
|
|
161
|
+
};
|
|
162
|
+
execFileSync("pnpm", ["pack", "--pack-destination", packDir], {
|
|
163
|
+
cwd: ROOT,
|
|
164
|
+
stdio: "pipe",
|
|
165
|
+
encoding: "utf-8",
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
const tarball = join(packDir, `${pkg.name}-${pkg.version}.tgz`);
|
|
169
|
+
const listing = execFileSync("tar", ["-tf", tarball], {
|
|
170
|
+
cwd: ROOT,
|
|
171
|
+
stdio: "pipe",
|
|
172
|
+
encoding: "utf-8",
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
expect(listing).toContain("package/dist/cli.js");
|
|
176
|
+
expect(listing).toContain("package/skills/first-tree/SKILL.md");
|
|
177
|
+
expect(listing).toContain(
|
|
178
|
+
"package/skills/first-tree/agents/openai.yaml",
|
|
179
|
+
);
|
|
180
|
+
expect(listing).toContain(
|
|
181
|
+
"package/skills/first-tree/engine/init.ts",
|
|
182
|
+
);
|
|
183
|
+
expect(listing).toContain(
|
|
184
|
+
"package/skills/first-tree/tests/init.test.ts",
|
|
185
|
+
);
|
|
186
|
+
expect(listing).not.toContain("package/skills/first-tree/evals/");
|
|
187
|
+
expect(listing).not.toContain("package/evals/");
|
|
188
|
+
expect(listing).not.toContain("package/src/cli.ts");
|
|
189
|
+
expect(listing).not.toContain("package/docs/");
|
|
190
|
+
} finally {
|
|
191
|
+
rmSync(packDir, { recursive: true, force: true });
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it("keeps naming and installation guidance aligned", () => {
|
|
196
|
+
const read = (path: string) => readFileSync(join(ROOT, path), "utf-8");
|
|
197
|
+
|
|
198
|
+
expect(read("README.md")).not.toContain("seed-tree");
|
|
199
|
+
expect(read("AGENTS.md")).not.toContain("seed-tree");
|
|
200
|
+
expect(read("README.md")).toContain("Package Name vs Command");
|
|
201
|
+
expect(read("README.md")).toContain("Canonical Documentation");
|
|
202
|
+
expect(read("README.md")).toContain("references/source-map.md");
|
|
203
|
+
expect(read("README.md")).toContain("skills/first-tree/");
|
|
204
|
+
expect(read("README.md")).toContain("bundled canonical");
|
|
205
|
+
expect(read("README.md")).toContain("dedicated tree repo");
|
|
206
|
+
expect(read("README.md")).toContain("`first-tree` skill");
|
|
207
|
+
expect(read("AGENTS.md")).toContain("references/source-map.md");
|
|
208
|
+
expect(read("AGENTS.md")).toContain("bundled skill path");
|
|
209
|
+
expect(read("AGENTS.md")).not.toContain("### Running evals");
|
|
210
|
+
expect(read("AGENTS.md")).not.toContain("EVALS_TREE_REPO");
|
|
211
|
+
expect(read("src/cli.ts")).not.toContain("from upstream");
|
|
212
|
+
// Note: #evals/* import alias is in package.json but evals/ is excluded from "files" so it won't ship to npm
|
|
213
|
+
|
|
214
|
+
const onboarding = read("skills/first-tree/references/onboarding.md");
|
|
215
|
+
expect(onboarding).toContain("npx first-tree init");
|
|
216
|
+
expect(onboarding).toContain("npm install -g first-tree");
|
|
217
|
+
expect(onboarding).toContain("context-tree init --here");
|
|
218
|
+
expect(onboarding).toContain("installed CLI command is");
|
|
219
|
+
expect(onboarding).toContain("currently running `first-tree` npm package");
|
|
220
|
+
expect(onboarding).toContain("npx first-tree@latest upgrade");
|
|
221
|
+
expect(onboarding).not.toContain("This clones the framework into `.context-tree/`");
|
|
222
|
+
expect(onboarding).not.toContain("from upstream");
|
|
223
|
+
|
|
224
|
+
const skillMd = read("skills/first-tree/SKILL.md");
|
|
225
|
+
expect(skillMd).not.toContain("sync-skill-artifacts.sh");
|
|
226
|
+
expect(skillMd).not.toContain("portable-smoke-test.sh");
|
|
227
|
+
expect(skillMd).toContain("maintainer-build-and-distribution.md");
|
|
228
|
+
expect(skillMd).toContain("maintainer-testing.md");
|
|
229
|
+
expect(skillMd).toContain("currently running `first-tree` package");
|
|
230
|
+
expect(skillMd).toContain("so it is not confused with the `first-tree`");
|
|
231
|
+
expect(skillMd).not.toContain("canonical eval harness");
|
|
232
|
+
|
|
233
|
+
const sourceMap = read("skills/first-tree/references/source-map.md");
|
|
234
|
+
expect(sourceMap).not.toContain("repo-snapshot");
|
|
235
|
+
expect(sourceMap).not.toContain("sync-skill-artifacts.sh");
|
|
236
|
+
expect(sourceMap).toContain("maintainer-architecture.md");
|
|
237
|
+
expect(sourceMap).toContain("maintainer-thin-cli.md");
|
|
238
|
+
expect(sourceMap).toContain("maintainer-build-and-distribution.md");
|
|
239
|
+
expect(sourceMap).toContain("maintainer-testing.md");
|
|
240
|
+
expect(sourceMap).toContain("engine/commands/");
|
|
241
|
+
expect(sourceMap).toContain("engine/runtime/asset-loader.ts");
|
|
242
|
+
expect(sourceMap).toContain("tests/init.test.ts");
|
|
243
|
+
expect(sourceMap).toContain("tests/thin-cli.test.ts");
|
|
244
|
+
expect(sourceMap).not.toContain("evals/context-tree-eval.test.ts");
|
|
245
|
+
expect(sourceMap).toContain("package.json");
|
|
246
|
+
expect(sourceMap).not.toContain("vitest.eval.config.ts");
|
|
247
|
+
expect(sourceMap).toContain(".github/workflows/ci.yml");
|
|
248
|
+
|
|
249
|
+
const maintainerArchitecture = read(
|
|
250
|
+
"skills/first-tree/references/maintainer-architecture.md",
|
|
251
|
+
);
|
|
252
|
+
expect(maintainerArchitecture).toContain("maintainer-only developer tooling");
|
|
253
|
+
expect(maintainerArchitecture).toContain("`evals/`");
|
|
254
|
+
expect(maintainerArchitecture).not.toContain("tests, and evals");
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
it("keeps public OSS entrypoints and package metadata in place", () => {
|
|
258
|
+
const read = (path: string) => readFileSync(join(ROOT, path), "utf-8");
|
|
259
|
+
const pkg = JSON.parse(read("package.json")) as {
|
|
260
|
+
homepage?: string;
|
|
261
|
+
bugs?: { url?: string };
|
|
262
|
+
repository?: { type?: string; url?: string };
|
|
263
|
+
keywords?: string[];
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
expect(read("README.md")).toContain("Package Name vs Command");
|
|
267
|
+
expect(read("README.md")).toContain("CONTRIBUTING.md");
|
|
268
|
+
expect(read("README.md")).toContain("CODE_OF_CONDUCT.md");
|
|
269
|
+
expect(read("README.md")).toContain("SECURITY.md");
|
|
270
|
+
|
|
271
|
+
expect(read("CONTRIBUTING.md")).toContain("pnpm validate:skill");
|
|
272
|
+
expect(read("CONTRIBUTING.md")).toContain("pull request template");
|
|
273
|
+
expect(read("CONTRIBUTING.md")).toContain("source-map.md");
|
|
274
|
+
expect(read("CODE_OF_CONDUCT.md")).toContain("private maintainer follow-up");
|
|
275
|
+
expect(read("SECURITY.md")).toContain("Private Vulnerability Reporting");
|
|
276
|
+
expect(read("SECURITY.md")).toContain("do not post exploit details");
|
|
277
|
+
|
|
278
|
+
expect(existsSync(join(ROOT, ".github", "PULL_REQUEST_TEMPLATE.md"))).toBe(true);
|
|
279
|
+
|
|
280
|
+
const bugTemplate = parse(read(".github/ISSUE_TEMPLATE/bug-report.yml")) as {
|
|
281
|
+
name?: string;
|
|
282
|
+
body?: unknown[];
|
|
283
|
+
};
|
|
284
|
+
const featureTemplate = parse(read(".github/ISSUE_TEMPLATE/feature-request.yml")) as {
|
|
285
|
+
name?: string;
|
|
286
|
+
body?: unknown[];
|
|
287
|
+
};
|
|
288
|
+
const issueConfig = parse(read(".github/ISSUE_TEMPLATE/config.yml")) as {
|
|
289
|
+
contact_links?: unknown[];
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
expect(bugTemplate.name).toBe("Bug report");
|
|
293
|
+
expect(Array.isArray(bugTemplate.body)).toBe(true);
|
|
294
|
+
expect(featureTemplate.name).toBe("Feature request");
|
|
295
|
+
expect(Array.isArray(featureTemplate.body)).toBe(true);
|
|
296
|
+
expect(Array.isArray(issueConfig.contact_links)).toBe(true);
|
|
297
|
+
expect(issueConfig.contact_links).toHaveLength(3);
|
|
298
|
+
|
|
299
|
+
expect(pkg.homepage).toBe("https://github.com/agent-team-foundation/first-tree#readme");
|
|
300
|
+
expect(pkg.bugs).toEqual({
|
|
301
|
+
url: "https://github.com/agent-team-foundation/first-tree/issues",
|
|
302
|
+
});
|
|
303
|
+
expect(pkg.repository).toEqual({
|
|
304
|
+
type: "git",
|
|
305
|
+
url: "git+https://github.com/agent-team-foundation/first-tree.git",
|
|
306
|
+
});
|
|
307
|
+
expect(pkg.keywords).toEqual(
|
|
308
|
+
expect.arrayContaining(["context-tree", "cli", "agents"]),
|
|
309
|
+
);
|
|
310
|
+
});
|
|
311
|
+
});
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import {
|
|
2
|
+
mkdtempSync,
|
|
3
|
+
readFileSync,
|
|
4
|
+
rmSync,
|
|
5
|
+
symlinkSync,
|
|
6
|
+
writeFileSync,
|
|
7
|
+
} from "node:fs";
|
|
8
|
+
import { tmpdir } from "node:os";
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
11
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
12
|
+
import { USAGE, isDirectExecution, runCli } from "../../../src/cli.ts";
|
|
13
|
+
|
|
14
|
+
const TEMP_DIRS: string[] = [];
|
|
15
|
+
|
|
16
|
+
function captureOutput(): { lines: string[]; write: (text: string) => void } {
|
|
17
|
+
const lines: string[] = [];
|
|
18
|
+
return {
|
|
19
|
+
lines,
|
|
20
|
+
write: (text: string) => {
|
|
21
|
+
lines.push(text);
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function makeTempDir(): string {
|
|
27
|
+
const dir = mkdtempSync(join(tmpdir(), "first-tree-thin-cli-"));
|
|
28
|
+
TEMP_DIRS.push(dir);
|
|
29
|
+
return dir;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
afterEach(() => {
|
|
33
|
+
while (TEMP_DIRS.length > 0) {
|
|
34
|
+
const dir = TEMP_DIRS.pop();
|
|
35
|
+
if (dir) {
|
|
36
|
+
rmSync(dir, { recursive: true, force: true });
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
describe("thin CLI shell", () => {
|
|
42
|
+
it("treats a symlinked npm bin path as direct execution", () => {
|
|
43
|
+
const dir = makeTempDir();
|
|
44
|
+
const target = join(dir, "cli.js");
|
|
45
|
+
const symlinkPath = join(dir, "context-tree");
|
|
46
|
+
|
|
47
|
+
writeFileSync(target, "#!/usr/bin/env node\n");
|
|
48
|
+
symlinkSync(target, symlinkPath);
|
|
49
|
+
|
|
50
|
+
expect(isDirectExecution(symlinkPath, pathToFileURL(target).href)).toBe(true);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("does not treat unrelated argv[1] values as direct execution", () => {
|
|
54
|
+
const dir = makeTempDir();
|
|
55
|
+
const target = join(dir, "cli.js");
|
|
56
|
+
const other = join(dir, "other.js");
|
|
57
|
+
|
|
58
|
+
writeFileSync(target, "#!/usr/bin/env node\n");
|
|
59
|
+
writeFileSync(other, "#!/usr/bin/env node\n");
|
|
60
|
+
|
|
61
|
+
expect(isDirectExecution(other, pathToFileURL(target).href)).toBe(false);
|
|
62
|
+
expect(isDirectExecution(undefined, pathToFileURL(target).href)).toBe(false);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("prints usage with no args", async () => {
|
|
66
|
+
const output = captureOutput();
|
|
67
|
+
|
|
68
|
+
const code = await runCli([], output.write);
|
|
69
|
+
|
|
70
|
+
expect(code).toBe(0);
|
|
71
|
+
expect(output.lines).toEqual([USAGE]);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("prints the package version", async () => {
|
|
75
|
+
const output = captureOutput();
|
|
76
|
+
const pkgPath = fileURLToPath(new URL("../../../package.json", import.meta.url));
|
|
77
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8")) as { version: string };
|
|
78
|
+
|
|
79
|
+
const code = await runCli(["--version"], output.write);
|
|
80
|
+
|
|
81
|
+
expect(code).toBe(0);
|
|
82
|
+
expect(output.lines).toEqual([pkg.version]);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("routes help onboarding through the CLI entrypoint", async () => {
|
|
86
|
+
const output = captureOutput();
|
|
87
|
+
|
|
88
|
+
const code = await runCli(["help", "onboarding"], output.write);
|
|
89
|
+
|
|
90
|
+
expect(code).toBe(0);
|
|
91
|
+
expect(output.lines.join("\n")).toContain("# Context Tree Onboarding");
|
|
92
|
+
expect(output.lines.join("\n")).toContain("Node.js 18+");
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("fails with usage for an unknown command", async () => {
|
|
96
|
+
const output = captureOutput();
|
|
97
|
+
|
|
98
|
+
const code = await runCli(["wat"], output.write);
|
|
99
|
+
|
|
100
|
+
expect(code).toBe(1);
|
|
101
|
+
expect(output.lines[0]).toBe("Unknown command: wat");
|
|
102
|
+
expect(output.lines[1]).toBe(USAGE);
|
|
103
|
+
});
|
|
104
|
+
});
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { describe, expect, it } from "vitest";
|
|
4
|
+
import { Repo } from "#skill/engine/repo.js";
|
|
5
|
+
import { runUpgrade } from "#skill/engine/upgrade.js";
|
|
6
|
+
import {
|
|
7
|
+
AGENT_INSTRUCTIONS_FILE,
|
|
8
|
+
FRAMEWORK_VERSION,
|
|
9
|
+
INSTALLED_PROGRESS,
|
|
10
|
+
LEGACY_AGENT_INSTRUCTIONS_FILE,
|
|
11
|
+
} from "#skill/engine/runtime/asset-loader.js";
|
|
12
|
+
import {
|
|
13
|
+
makeAgentsMd,
|
|
14
|
+
makeFramework,
|
|
15
|
+
makeSourceRepo,
|
|
16
|
+
makeLegacyFramework,
|
|
17
|
+
makeLegacyNamedFramework,
|
|
18
|
+
makeSourceSkill,
|
|
19
|
+
useTmpDir,
|
|
20
|
+
} from "./helpers.js";
|
|
21
|
+
|
|
22
|
+
describe("runUpgrade", () => {
|
|
23
|
+
it("migrates a legacy repo to the installed skill layout", () => {
|
|
24
|
+
const repoDir = useTmpDir();
|
|
25
|
+
const sourceDir = useTmpDir();
|
|
26
|
+
makeLegacyFramework(repoDir.path, "0.1.0");
|
|
27
|
+
makeAgentsMd(repoDir.path, { legacyName: true, markers: true, userContent: true });
|
|
28
|
+
makeSourceSkill(sourceDir.path, "0.2.0");
|
|
29
|
+
|
|
30
|
+
const result = runUpgrade(new Repo(repoDir.path), {
|
|
31
|
+
sourceRoot: sourceDir.path,
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
expect(result).toBe(0);
|
|
35
|
+
expect(existsSync(join(repoDir.path, ".context-tree"))).toBe(false);
|
|
36
|
+
expect(readFileSync(join(repoDir.path, FRAMEWORK_VERSION), "utf-8").trim()).toBe("0.2.0");
|
|
37
|
+
expect(readFileSync(join(repoDir.path, INSTALLED_PROGRESS), "utf-8")).toContain(
|
|
38
|
+
"skills/first-tree/assets/framework/VERSION",
|
|
39
|
+
);
|
|
40
|
+
expect(readFileSync(join(repoDir.path, INSTALLED_PROGRESS), "utf-8")).toContain(
|
|
41
|
+
`Rename \`${LEGACY_AGENT_INSTRUCTIONS_FILE}\` to \`${AGENT_INSTRUCTIONS_FILE}\``,
|
|
42
|
+
);
|
|
43
|
+
expect(readFileSync(join(repoDir.path, INSTALLED_PROGRESS), "utf-8")).toContain(
|
|
44
|
+
"skills/first-tree/assets/framework/templates/agents.md.template",
|
|
45
|
+
);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("returns early when the installed skill already matches the packaged skill", () => {
|
|
49
|
+
const repoDir = useTmpDir();
|
|
50
|
+
const sourceDir = useTmpDir();
|
|
51
|
+
makeFramework(repoDir.path, "0.2.0");
|
|
52
|
+
makeSourceSkill(sourceDir.path, "0.2.0");
|
|
53
|
+
|
|
54
|
+
const result = runUpgrade(new Repo(repoDir.path), {
|
|
55
|
+
sourceRoot: sourceDir.path,
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
expect(result).toBe(0);
|
|
59
|
+
expect(existsSync(join(repoDir.path, INSTALLED_PROGRESS))).toBe(false);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("migrates repos that still use the previous installed skill name", () => {
|
|
63
|
+
const repoDir = useTmpDir();
|
|
64
|
+
const sourceDir = useTmpDir();
|
|
65
|
+
makeLegacyNamedFramework(repoDir.path, "0.2.0");
|
|
66
|
+
makeSourceSkill(sourceDir.path, "0.2.0");
|
|
67
|
+
|
|
68
|
+
const result = runUpgrade(new Repo(repoDir.path), {
|
|
69
|
+
sourceRoot: sourceDir.path,
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
expect(result).toBe(0);
|
|
73
|
+
expect(existsSync(join(repoDir.path, "skills", "first-tree-cli-framework"))).toBe(
|
|
74
|
+
false,
|
|
75
|
+
);
|
|
76
|
+
expect(readFileSync(join(repoDir.path, FRAMEWORK_VERSION), "utf-8").trim()).toBe("0.2.0");
|
|
77
|
+
expect(readFileSync(join(repoDir.path, INSTALLED_PROGRESS), "utf-8")).toContain(
|
|
78
|
+
"skills/first-tree-cli-framework/",
|
|
79
|
+
);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("refuses to replace a newer installed skill with an older packaged skill", () => {
|
|
83
|
+
const repoDir = useTmpDir();
|
|
84
|
+
const sourceDir = useTmpDir();
|
|
85
|
+
makeFramework(repoDir.path, "0.3.0");
|
|
86
|
+
makeSourceSkill(sourceDir.path, "0.2.0");
|
|
87
|
+
|
|
88
|
+
const result = runUpgrade(new Repo(repoDir.path), {
|
|
89
|
+
sourceRoot: sourceDir.path,
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
expect(result).toBe(1);
|
|
93
|
+
expect(readFileSync(join(repoDir.path, FRAMEWORK_VERSION), "utf-8").trim()).toBe("0.3.0");
|
|
94
|
+
expect(existsSync(join(repoDir.path, INSTALLED_PROGRESS))).toBe(false);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("gives a dedicated-tree hint when run from a source repo", () => {
|
|
98
|
+
const repoDir = useTmpDir();
|
|
99
|
+
makeSourceRepo(repoDir.path);
|
|
100
|
+
const result = runUpgrade(new Repo(repoDir.path));
|
|
101
|
+
expect(result).toBe(1);
|
|
102
|
+
});
|
|
103
|
+
});
|