first-tree 0.0.3 → 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.
Files changed (43) hide show
  1. package/README.md +69 -27
  2. package/dist/cli.js +28 -13
  3. package/dist/{help-xEI-s9iN.js → help-Dtdj91HJ.js} +1 -1
  4. package/dist/{init-DtOjj0wc.js → init--VepFe6N.js} +171 -21
  5. package/dist/{installer-rcZpGLnM.js → installer-cH7N4RNj.js} +2 -2
  6. package/dist/onboarding-C9cYSE6F.js +2 -0
  7. package/dist/onboarding-CPP8fF4D.js +10 -0
  8. package/dist/{repo-BTJG8BU1.js → repo-DY57bMqr.js} +143 -12
  9. package/dist/{upgrade-COGgI7Rj.js → upgrade-Cgx_K2HM.js} +46 -7
  10. package/dist/{verify-CxN6JiV9.js → verify-mC9ZTd1f.js} +66 -6
  11. package/package.json +1 -1
  12. package/skills/first-tree/SKILL.md +8 -4
  13. package/skills/first-tree/assets/framework/VERSION +1 -1
  14. package/skills/first-tree/assets/framework/helpers/run-review.ts +16 -2
  15. package/skills/first-tree/assets/framework/templates/{agent.md.template → agents.md.template} +1 -0
  16. package/skills/first-tree/assets/framework/templates/root-node.md.template +6 -3
  17. package/skills/first-tree/engine/commands/init.ts +1 -1
  18. package/skills/first-tree/engine/commands/upgrade.ts +1 -1
  19. package/skills/first-tree/engine/commands/verify.ts +1 -1
  20. package/skills/first-tree/engine/init.ts +285 -16
  21. package/skills/first-tree/engine/repo.ts +185 -9
  22. package/skills/first-tree/engine/rules/agent-instructions.ts +29 -7
  23. package/skills/first-tree/engine/runtime/asset-loader.ts +7 -0
  24. package/skills/first-tree/engine/upgrade.ts +66 -9
  25. package/skills/first-tree/engine/validators/nodes.ts +48 -3
  26. package/skills/first-tree/engine/verify.ts +61 -3
  27. package/skills/first-tree/references/maintainer-architecture.md +1 -1
  28. package/skills/first-tree/references/maintainer-build-and-distribution.md +3 -0
  29. package/skills/first-tree/references/maintainer-thin-cli.md +1 -1
  30. package/skills/first-tree/references/onboarding.md +32 -9
  31. package/skills/first-tree/references/source-map.md +3 -3
  32. package/skills/first-tree/references/upgrade-contract.md +14 -5
  33. package/skills/first-tree/scripts/check-skill-sync.sh +1 -1
  34. package/skills/first-tree/tests/helpers.ts +24 -4
  35. package/skills/first-tree/tests/init.test.ts +103 -6
  36. package/skills/first-tree/tests/repo.test.ts +87 -9
  37. package/skills/first-tree/tests/rules.test.ts +26 -7
  38. package/skills/first-tree/tests/skill-artifacts.test.ts +4 -0
  39. package/skills/first-tree/tests/thin-cli.test.ts +52 -7
  40. package/skills/first-tree/tests/upgrade.test.ts +19 -5
  41. package/skills/first-tree/tests/verify.test.ts +106 -7
  42. package/dist/onboarding-6Fr5Gkrk.js +0 -2
  43. package/dist/onboarding-B9zPGvvG.js +0 -10
@@ -1,17 +1,27 @@
1
1
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
- import { join } from "node:path";
2
+ import { basename, dirname, join } from "node:path";
3
3
  import { describe, expect, it } from "vitest";
4
- import { formatTaskList, writeProgress, runInit } from "#skill/engine/init.js";
4
+ import {
5
+ formatTaskList,
6
+ parseInitArgs,
7
+ writeProgress,
8
+ runInit,
9
+ } from "#skill/engine/init.js";
5
10
  import { Repo } from "#skill/engine/repo.js";
6
11
  import {
12
+ AGENT_INSTRUCTIONS_FILE,
7
13
  FRAMEWORK_VERSION,
8
14
  INSTALLED_PROGRESS,
15
+ LEGACY_AGENT_INSTRUCTIONS_FILE,
9
16
  LEGACY_PROGRESS,
10
17
  } from "#skill/engine/runtime/asset-loader.js";
11
18
  import {
19
+ makeGitRepo,
12
20
  useTmpDir,
21
+ makeAgentsMd,
13
22
  makeFramework,
14
23
  makeLegacyFramework,
24
+ makeSourceRepo,
15
25
  makeSourceSkill,
16
26
  } from "./helpers.js";
17
27
 
@@ -110,6 +120,10 @@ describe("writeProgress", () => {
110
120
 
111
121
  // --- runInit — guard logic (no network) ---
112
122
 
123
+ const fakeGitInitializer = (root: string): void => {
124
+ makeGitRepo(root);
125
+ };
126
+
113
127
  describe("runInit", () => {
114
128
  it("errors when not a git repo", () => {
115
129
  const tmp = useTmpDir();
@@ -121,10 +135,13 @@ describe("runInit", () => {
121
135
  it("installs the bundled skill and scaffolding when framework is missing", () => {
122
136
  const repoDir = useTmpDir();
123
137
  const sourceDir = useTmpDir();
124
- mkdirSync(join(repoDir.path, ".git"));
138
+ makeGitRepo(repoDir.path);
125
139
  makeSourceSkill(sourceDir.path, "0.2.0");
126
140
 
127
- const ret = runInit(new Repo(repoDir.path), { sourceRoot: sourceDir.path });
141
+ const ret = runInit(new Repo(repoDir.path), {
142
+ sourceRoot: sourceDir.path,
143
+ gitInitializer: fakeGitInitializer,
144
+ });
128
145
 
129
146
  expect(ret).toBe(0);
130
147
  expect(
@@ -132,15 +149,32 @@ describe("runInit", () => {
132
149
  ).toBe(true);
133
150
  expect(readFileSync(join(repoDir.path, FRAMEWORK_VERSION), "utf-8").trim()).toBe("0.2.0");
134
151
  expect(existsSync(join(repoDir.path, "NODE.md"))).toBe(true);
135
- expect(existsSync(join(repoDir.path, "AGENT.md"))).toBe(true);
152
+ expect(existsSync(join(repoDir.path, AGENT_INSTRUCTIONS_FILE))).toBe(true);
136
153
  expect(existsSync(join(repoDir.path, "members", "NODE.md"))).toBe(true);
137
154
  expect(existsSync(join(repoDir.path, INSTALLED_PROGRESS))).toBe(true);
138
155
  });
139
156
 
157
+ it("does not scaffold AGENTS.md when legacy AGENT.md already exists", () => {
158
+ const repoDir = useTmpDir();
159
+ const sourceDir = useTmpDir();
160
+ mkdirSync(join(repoDir.path, ".git"));
161
+ makeAgentsMd(repoDir.path, { legacyName: true, markers: true, userContent: true });
162
+ makeSourceSkill(sourceDir.path, "0.2.0");
163
+
164
+ const ret = runInit(new Repo(repoDir.path), { sourceRoot: sourceDir.path });
165
+
166
+ expect(ret).toBe(0);
167
+ expect(existsSync(join(repoDir.path, LEGACY_AGENT_INSTRUCTIONS_FILE))).toBe(true);
168
+ expect(existsSync(join(repoDir.path, AGENT_INSTRUCTIONS_FILE))).toBe(false);
169
+ expect(readFileSync(join(repoDir.path, INSTALLED_PROGRESS), "utf-8")).toContain(
170
+ "Rename `AGENT.md` to `AGENTS.md`",
171
+ );
172
+ });
173
+
140
174
  it("skips reinstall when framework exists", () => {
141
175
  const tmp = useTmpDir();
142
176
  const sourceDir = useTmpDir();
143
- mkdirSync(join(tmp.path, ".git"));
177
+ makeGitRepo(tmp.path);
144
178
  makeFramework(tmp.path, "0.1.0");
145
179
  makeSourceSkill(sourceDir.path, "0.2.0");
146
180
 
@@ -150,4 +184,67 @@ describe("runInit", () => {
150
184
  expect(ret).toBe(0);
151
185
  expect(readFileSync(join(tmp.path, FRAMEWORK_VERSION), "utf-8").trim()).toBe("0.1.0");
152
186
  });
187
+
188
+ it("creates a sibling tree repo by default when invoked from a source repo", () => {
189
+ const sourceRepoDir = useTmpDir();
190
+ const sourceSkillDir = useTmpDir();
191
+ makeSourceRepo(sourceRepoDir.path);
192
+ makeSourceSkill(sourceSkillDir.path, "0.2.0");
193
+
194
+ const ret = runInit(new Repo(sourceRepoDir.path), {
195
+ sourceRoot: sourceSkillDir.path,
196
+ gitInitializer: fakeGitInitializer,
197
+ });
198
+
199
+ const treeRepo = join(
200
+ dirname(sourceRepoDir.path),
201
+ `${basename(sourceRepoDir.path)}-context`,
202
+ );
203
+
204
+ expect(ret).toBe(0);
205
+ expect(existsSync(join(treeRepo, "skills", "first-tree", "SKILL.md"))).toBe(true);
206
+ expect(existsSync(join(treeRepo, "NODE.md"))).toBe(true);
207
+ expect(existsSync(join(treeRepo, AGENT_INSTRUCTIONS_FILE))).toBe(true);
208
+ expect(existsSync(join(treeRepo, "members", "NODE.md"))).toBe(true);
209
+ expect(existsSync(join(treeRepo, INSTALLED_PROGRESS))).toBe(true);
210
+ expect(existsSync(join(sourceRepoDir.path, "NODE.md"))).toBe(false);
211
+ expect(existsSync(join(sourceRepoDir.path, "members", "NODE.md"))).toBe(false);
212
+ expect(existsSync(join(sourceRepoDir.path, INSTALLED_PROGRESS))).toBe(false);
213
+ });
214
+
215
+ it("keeps supporting in-place init with --here", () => {
216
+ const sourceRepoDir = useTmpDir();
217
+ const sourceSkillDir = useTmpDir();
218
+ makeSourceRepo(sourceRepoDir.path);
219
+ makeSourceSkill(sourceSkillDir.path, "0.2.0");
220
+
221
+ const ret = runInit(new Repo(sourceRepoDir.path), {
222
+ here: true,
223
+ sourceRoot: sourceSkillDir.path,
224
+ gitInitializer: fakeGitInitializer,
225
+ });
226
+
227
+ expect(ret).toBe(0);
228
+ expect(existsSync(join(sourceRepoDir.path, "NODE.md"))).toBe(true);
229
+ expect(existsSync(join(sourceRepoDir.path, AGENT_INSTRUCTIONS_FILE))).toBe(true);
230
+ expect(existsSync(join(sourceRepoDir.path, "members", "NODE.md"))).toBe(true);
231
+ expect(existsSync(join(sourceRepoDir.path, INSTALLED_PROGRESS))).toBe(true);
232
+ });
233
+ });
234
+
235
+ describe("parseInitArgs", () => {
236
+ it("parses dedicated repo options", () => {
237
+ expect(parseInitArgs(["--tree-name", "acme-context"])).toEqual({
238
+ treeName: "acme-context",
239
+ });
240
+ expect(parseInitArgs(["--tree-path", "../acme-context"])).toEqual({
241
+ treePath: "../acme-context",
242
+ });
243
+ });
244
+
245
+ it("rejects incompatible init options", () => {
246
+ expect(parseInitArgs(["--here", "--tree-name", "acme-context"])).toEqual({
247
+ error: "Cannot combine --here with --tree-name",
248
+ });
249
+ });
153
250
  });
@@ -3,8 +3,10 @@ import { join } from "node:path";
3
3
  import { describe, expect, it } from "vitest";
4
4
  import { Repo } from "#skill/engine/repo.js";
5
5
  import {
6
+ AGENT_INSTRUCTIONS_FILE,
6
7
  FRAMEWORK_VERSION,
7
8
  INSTALLED_PROGRESS,
9
+ LEGACY_AGENT_INSTRUCTIONS_FILE,
8
10
  LEGACY_SKILL_PROGRESS,
9
11
  LEGACY_SKILL_VERSION,
10
12
  LEGACY_PROGRESS,
@@ -13,8 +15,11 @@ import {
13
15
  import {
14
16
  useTmpDir,
15
17
  makeFramework,
18
+ makeGitRepo,
16
19
  makeLegacyFramework,
17
20
  makeLegacyNamedFramework,
21
+ makeSourceRepo,
22
+ makeSourceSkill,
18
23
  } from "./helpers.js";
19
24
 
20
25
  // --- pathExists ---
@@ -134,7 +139,14 @@ describe("anyAgentConfig", () => {
134
139
  describe("isGitRepo", () => {
135
140
  it("returns true with .git dir", () => {
136
141
  const tmp = useTmpDir();
137
- mkdirSync(join(tmp.path, ".git"));
142
+ makeGitRepo(tmp.path);
143
+ const repo = new Repo(tmp.path);
144
+ expect(repo.isGitRepo()).toBe(true);
145
+ });
146
+
147
+ it("returns true with .git file", () => {
148
+ const tmp = useTmpDir();
149
+ writeFileSync(join(tmp.path, ".git"), "gitdir: /tmp/example\n");
138
150
  const repo = new Repo(tmp.path);
139
151
  expect(repo.isGitRepo()).toBe(true);
140
152
  });
@@ -235,33 +247,56 @@ describe("path preferences", () => {
235
247
  });
236
248
  });
237
249
 
238
- // --- hasAgentMdMarkers ---
250
+ // --- agent instructions helpers ---
251
+
252
+ describe("agent instructions helpers", () => {
253
+ it("prefers AGENTS.md when both filenames exist", () => {
254
+ const tmp = useTmpDir();
255
+ writeFileSync(
256
+ join(tmp.path, AGENT_INSTRUCTIONS_FILE),
257
+ "<!-- BEGIN CONTEXT-TREE FRAMEWORK -->\nstuff\n<!-- END CONTEXT-TREE FRAMEWORK -->\n",
258
+ );
259
+ writeFileSync(
260
+ join(tmp.path, LEGACY_AGENT_INSTRUCTIONS_FILE),
261
+ "# Legacy instructions\n",
262
+ );
263
+ const repo = new Repo(tmp.path);
264
+ expect(repo.agentInstructionsPath()).toBe(AGENT_INSTRUCTIONS_FILE);
265
+ expect(repo.hasCanonicalAgentInstructionsFile()).toBe(true);
266
+ expect(repo.hasLegacyAgentInstructionsFile()).toBe(true);
267
+ expect(repo.hasDuplicateAgentInstructionsFiles()).toBe(true);
268
+ expect(repo.hasAgentInstructionsMarkers()).toBe(true);
269
+ });
239
270
 
240
- describe("hasAgentMdMarkers", () => {
241
- it("returns true with markers", () => {
271
+ it("falls back to legacy AGENT.md while migrating", () => {
242
272
  const tmp = useTmpDir();
243
273
  writeFileSync(
244
- join(tmp.path, "AGENT.md"),
274
+ join(tmp.path, LEGACY_AGENT_INSTRUCTIONS_FILE),
245
275
  "<!-- BEGIN CONTEXT-TREE FRAMEWORK -->\nstuff\n<!-- END CONTEXT-TREE FRAMEWORK -->\n",
246
276
  );
247
277
  const repo = new Repo(tmp.path);
248
- expect(repo.hasAgentMdMarkers()).toBe(true);
278
+ expect(repo.agentInstructionsPath()).toBe(LEGACY_AGENT_INSTRUCTIONS_FILE);
279
+ expect(repo.hasCanonicalAgentInstructionsFile()).toBe(false);
280
+ expect(repo.hasLegacyAgentInstructionsFile()).toBe(true);
281
+ expect(repo.hasDuplicateAgentInstructionsFiles()).toBe(false);
282
+ expect(repo.hasAgentInstructionsMarkers()).toBe(true);
249
283
  });
250
284
 
251
285
  it("returns false without markers", () => {
252
286
  const tmp = useTmpDir();
253
287
  writeFileSync(
254
- join(tmp.path, "AGENT.md"),
288
+ join(tmp.path, AGENT_INSTRUCTIONS_FILE),
255
289
  "# Agent instructions\nNo markers here.\n",
256
290
  );
257
291
  const repo = new Repo(tmp.path);
258
- expect(repo.hasAgentMdMarkers()).toBe(false);
292
+ expect(repo.hasAgentInstructionsMarkers()).toBe(false);
259
293
  });
260
294
 
261
295
  it("returns false when file is missing", () => {
262
296
  const tmp = useTmpDir();
263
297
  const repo = new Repo(tmp.path);
264
- expect(repo.hasAgentMdMarkers()).toBe(false);
298
+ expect(repo.agentInstructionsPath()).toBeNull();
299
+ expect(repo.hasAgentInstructionsMarkers()).toBe(false);
265
300
  });
266
301
  });
267
302
 
@@ -360,3 +395,46 @@ describe("hasPlaceholderNode", () => {
360
395
  expect(repo.hasPlaceholderNode()).toBe(false);
361
396
  });
362
397
  });
398
+
399
+ // --- init heuristics ---
400
+
401
+ describe("init heuristics", () => {
402
+ it("treats a code repo as a likely source repo", () => {
403
+ const tmp = useTmpDir();
404
+ makeSourceRepo(tmp.path);
405
+ const repo = new Repo(tmp.path);
406
+ expect(repo.isLikelySourceRepo()).toBe(true);
407
+ expect(repo.isLikelyEmptyRepo()).toBe(false);
408
+ });
409
+
410
+ it("treats a fresh tree repo as empty enough for in-place init", () => {
411
+ const tmp = useTmpDir();
412
+ makeGitRepo(tmp.path);
413
+ writeFileSync(join(tmp.path, "README.md"), "# My Org Context\n");
414
+ const repo = new Repo(tmp.path);
415
+ expect(repo.isLikelyEmptyRepo()).toBe(true);
416
+ expect(repo.isLikelySourceRepo()).toBe(false);
417
+ });
418
+
419
+ it("recognizes a populated tree repo", () => {
420
+ const tmp = useTmpDir();
421
+ makeFramework(tmp.path);
422
+ writeFileSync(
423
+ join(tmp.path, "NODE.md"),
424
+ "---\ntitle: My Tree\nowners: [alice]\n---\n# Tree\n",
425
+ );
426
+ const repo = new Repo(tmp.path);
427
+ expect(repo.looksLikeTreeRepo()).toBe(true);
428
+ expect(repo.isLikelySourceRepo()).toBe(false);
429
+ });
430
+
431
+ it("does not mistake the framework source repo for a user tree repo", () => {
432
+ const tmp = useTmpDir();
433
+ makeSourceRepo(tmp.path);
434
+ makeSourceSkill(tmp.path, "0.2.0");
435
+ writeFileSync(join(tmp.path, "src", "cli.ts"), "export {};\n");
436
+ const repo = new Repo(tmp.path);
437
+ expect(repo.looksLikeTreeRepo()).toBe(false);
438
+ expect(repo.isLikelySourceRepo()).toBe(true);
439
+ });
440
+ });
@@ -16,7 +16,7 @@ import {
16
16
  useTmpDir,
17
17
  makeFramework,
18
18
  makeNode,
19
- makeAgentMd,
19
+ makeAgentsMd,
20
20
  makeMembers,
21
21
  } from "./helpers.js";
22
22
 
@@ -104,16 +104,35 @@ describe("rootNode rule", () => {
104
104
  // --- agent_instructions rule ---
105
105
 
106
106
  describe("agentInstructions rule", () => {
107
- it("reports missing AGENT.md", () => {
107
+ it("reports missing AGENTS.md", () => {
108
108
  const tmp = useTmpDir();
109
109
  const repo = new Repo(tmp.path);
110
110
  const result = agentInstructions.evaluate(repo);
111
- expect(result.tasks.some((t) => t.toLowerCase().includes("missing"))).toBe(true);
111
+ expect(result.tasks.some((t) => t.includes("AGENTS.md is missing"))).toBe(true);
112
+ });
113
+
114
+ it("reports legacy AGENT.md rename", () => {
115
+ const tmp = useTmpDir();
116
+ makeAgentsMd(tmp.path, { legacyName: true, markers: true, userContent: true });
117
+ const repo = new Repo(tmp.path);
118
+ const result = agentInstructions.evaluate(repo);
119
+ expect(result.tasks.some((t) => t.includes("Rename `AGENT.md` to `AGENTS.md`"))).toBe(
120
+ true,
121
+ );
122
+ });
123
+
124
+ it("reports duplicate cleanup when both filenames exist", () => {
125
+ const tmp = useTmpDir();
126
+ makeAgentsMd(tmp.path, { markers: true, userContent: true });
127
+ makeAgentsMd(tmp.path, { legacyName: true, markers: true, userContent: true });
128
+ const repo = new Repo(tmp.path);
129
+ const result = agentInstructions.evaluate(repo);
130
+ expect(result.tasks.some((t) => t.includes("delete the legacy file"))).toBe(true);
112
131
  });
113
132
 
114
133
  it("reports no markers", () => {
115
134
  const tmp = useTmpDir();
116
- makeAgentMd(tmp.path, { markers: false });
135
+ makeAgentsMd(tmp.path, { markers: false });
117
136
  const repo = new Repo(tmp.path);
118
137
  const result = agentInstructions.evaluate(repo);
119
138
  expect(result.tasks.some((t) => t.toLowerCase().includes("markers"))).toBe(true);
@@ -121,7 +140,7 @@ describe("agentInstructions rule", () => {
121
140
 
122
141
  it("reports no user content", () => {
123
142
  const tmp = useTmpDir();
124
- makeAgentMd(tmp.path, { markers: true, userContent: false });
143
+ makeAgentsMd(tmp.path, { markers: true, userContent: false });
125
144
  const repo = new Repo(tmp.path);
126
145
  const result = agentInstructions.evaluate(repo);
127
146
  expect(result.tasks.some((t) => t.toLowerCase().includes("project-specific"))).toBe(true);
@@ -129,7 +148,7 @@ describe("agentInstructions rule", () => {
129
148
 
130
149
  it("passes with markers and user content", () => {
131
150
  const tmp = useTmpDir();
132
- makeAgentMd(tmp.path, { markers: true, userContent: true });
151
+ makeAgentsMd(tmp.path, { markers: true, userContent: true });
133
152
  const repo = new Repo(tmp.path);
134
153
  const result = agentInstructions.evaluate(repo);
135
154
  expect(result.tasks).toEqual([]);
@@ -365,7 +384,7 @@ describe("evaluateAll", () => {
365
384
  const tmp = useTmpDir();
366
385
  makeFramework(tmp.path);
367
386
  makeNode(tmp.path);
368
- makeAgentMd(tmp.path, { markers: true, userContent: true });
387
+ makeAgentsMd(tmp.path, { markers: true, userContent: true });
369
388
  makeMembers(tmp.path, 1);
370
389
  mkdirSync(join(tmp.path, ".claude"));
371
390
  writeFileSync(
@@ -22,6 +22,7 @@ describe("skill artifacts", () => {
22
22
  expect(existsSync(join(ROOT, "skills", "first-tree", "references", "onboarding.md"))).toBe(true);
23
23
  expect(existsSync(join(ROOT, "skills", "first-tree", "assets", "framework", "manifest.json"))).toBe(true);
24
24
  expect(existsSync(join(ROOT, "skills", "first-tree", "engine", "init.ts"))).toBe(true);
25
+ expect(existsSync(join(ROOT, "AGENTS.md"))).toBe(true);
25
26
  expect(existsSync(join(ROOT, "skills", "first-tree", "tests", "init.test.ts"))).toBe(
26
27
  true,
27
28
  );
@@ -114,6 +115,7 @@ describe("skill artifacts", () => {
114
115
  expect(isTrackedInGit(".agents")).toBe(false);
115
116
  expect(isTrackedInGit(".claude")).toBe(false);
116
117
  expect(isTrackedInGit(".context-tree")).toBe(false);
118
+ expect(existsSync(join(ROOT, "AGENT.md"))).toBe(false);
117
119
  expect(isTrackedInGit("skills/first-tree-cli-framework")).toBe(false);
118
120
  expect(isTrackedInGit("docs")).toBe(false);
119
121
  expect(isTrackedInGit("tests")).toBe(false);
@@ -200,6 +202,7 @@ describe("skill artifacts", () => {
200
202
  expect(read("README.md")).toContain("references/source-map.md");
201
203
  expect(read("README.md")).toContain("skills/first-tree/");
202
204
  expect(read("README.md")).toContain("bundled canonical");
205
+ expect(read("README.md")).toContain("dedicated tree repo");
203
206
  expect(read("README.md")).toContain("`first-tree` skill");
204
207
  expect(read("AGENTS.md")).toContain("references/source-map.md");
205
208
  expect(read("AGENTS.md")).toContain("bundled skill path");
@@ -211,6 +214,7 @@ describe("skill artifacts", () => {
211
214
  const onboarding = read("skills/first-tree/references/onboarding.md");
212
215
  expect(onboarding).toContain("npx first-tree init");
213
216
  expect(onboarding).toContain("npm install -g first-tree");
217
+ expect(onboarding).toContain("context-tree init --here");
214
218
  expect(onboarding).toContain("installed CLI command is");
215
219
  expect(onboarding).toContain("currently running `first-tree` npm package");
216
220
  expect(onboarding).toContain("npx first-tree@latest upgrade");
@@ -1,9 +1,17 @@
1
- import { readFileSync } from "node:fs";
1
+ import {
2
+ mkdtempSync,
3
+ readFileSync,
4
+ rmSync,
5
+ symlinkSync,
6
+ writeFileSync,
7
+ } from "node:fs";
8
+ import { tmpdir } from "node:os";
2
9
  import { join } from "node:path";
3
- import { describe, expect, it } from "vitest";
4
- import { USAGE, runCli } from "../../../src/cli.ts";
10
+ import { fileURLToPath, pathToFileURL } from "node:url";
11
+ import { afterEach, describe, expect, it } from "vitest";
12
+ import { USAGE, isDirectExecution, runCli } from "../../../src/cli.ts";
5
13
 
6
- const ROOT = process.cwd();
14
+ const TEMP_DIRS: string[] = [];
7
15
 
8
16
  function captureOutput(): { lines: string[]; write: (text: string) => void } {
9
17
  const lines: string[] = [];
@@ -15,7 +23,45 @@ function captureOutput(): { lines: string[]; write: (text: string) => void } {
15
23
  };
16
24
  }
17
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
+
18
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
+
19
65
  it("prints usage with no args", async () => {
20
66
  const output = captureOutput();
21
67
 
@@ -27,9 +73,8 @@ describe("thin CLI shell", () => {
27
73
 
28
74
  it("prints the package version", async () => {
29
75
  const output = captureOutput();
30
- const pkg = JSON.parse(readFileSync(join(ROOT, "package.json"), "utf-8")) as {
31
- version: string;
32
- };
76
+ const pkgPath = fileURLToPath(new URL("../../../package.json", import.meta.url));
77
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf-8")) as { version: string };
33
78
 
34
79
  const code = await runCli(["--version"], output.write);
35
80
 
@@ -1,14 +1,18 @@
1
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
1
+ import { existsSync, readFileSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  import { describe, expect, it } from "vitest";
4
4
  import { Repo } from "#skill/engine/repo.js";
5
5
  import { runUpgrade } from "#skill/engine/upgrade.js";
6
6
  import {
7
+ AGENT_INSTRUCTIONS_FILE,
7
8
  FRAMEWORK_VERSION,
8
9
  INSTALLED_PROGRESS,
10
+ LEGACY_AGENT_INSTRUCTIONS_FILE,
9
11
  } from "#skill/engine/runtime/asset-loader.js";
10
12
  import {
13
+ makeAgentsMd,
11
14
  makeFramework,
15
+ makeSourceRepo,
12
16
  makeLegacyFramework,
13
17
  makeLegacyNamedFramework,
14
18
  makeSourceSkill,
@@ -20,10 +24,7 @@ describe("runUpgrade", () => {
20
24
  const repoDir = useTmpDir();
21
25
  const sourceDir = useTmpDir();
22
26
  makeLegacyFramework(repoDir.path, "0.1.0");
23
- writeFileSync(
24
- join(repoDir.path, "AGENT.md"),
25
- "<!-- BEGIN CONTEXT-TREE FRAMEWORK -->\nstuff\n<!-- END CONTEXT-TREE FRAMEWORK -->\n",
26
- );
27
+ makeAgentsMd(repoDir.path, { legacyName: true, markers: true, userContent: true });
27
28
  makeSourceSkill(sourceDir.path, "0.2.0");
28
29
 
29
30
  const result = runUpgrade(new Repo(repoDir.path), {
@@ -36,6 +37,12 @@ describe("runUpgrade", () => {
36
37
  expect(readFileSync(join(repoDir.path, INSTALLED_PROGRESS), "utf-8")).toContain(
37
38
  "skills/first-tree/assets/framework/VERSION",
38
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
+ );
39
46
  });
40
47
 
41
48
  it("returns early when the installed skill already matches the packaged skill", () => {
@@ -86,4 +93,11 @@ describe("runUpgrade", () => {
86
93
  expect(readFileSync(join(repoDir.path, FRAMEWORK_VERSION), "utf-8").trim()).toBe("0.3.0");
87
94
  expect(existsSync(join(repoDir.path, INSTALLED_PROGRESS))).toBe(false);
88
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
+ });
89
103
  });