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.
- package/README.md +69 -27
- package/dist/cli.js +28 -13
- package/dist/{help-xEI-s9iN.js → help-Dtdj91HJ.js} +1 -1
- package/dist/{init-DtOjj0wc.js → init--VepFe6N.js} +171 -21
- package/dist/{installer-rcZpGLnM.js → installer-cH7N4RNj.js} +2 -2
- package/dist/onboarding-C9cYSE6F.js +2 -0
- package/dist/onboarding-CPP8fF4D.js +10 -0
- package/dist/{repo-BTJG8BU1.js → repo-DY57bMqr.js} +143 -12
- package/dist/{upgrade-COGgI7Rj.js → upgrade-Cgx_K2HM.js} +46 -7
- package/dist/{verify-CxN6JiV9.js → verify-mC9ZTd1f.js} +66 -6
- package/package.json +1 -1
- package/skills/first-tree/SKILL.md +8 -4
- package/skills/first-tree/assets/framework/VERSION +1 -1
- package/skills/first-tree/assets/framework/helpers/run-review.ts +16 -2
- package/skills/first-tree/assets/framework/templates/{agent.md.template → agents.md.template} +1 -0
- package/skills/first-tree/assets/framework/templates/root-node.md.template +6 -3
- package/skills/first-tree/engine/commands/init.ts +1 -1
- package/skills/first-tree/engine/commands/upgrade.ts +1 -1
- package/skills/first-tree/engine/commands/verify.ts +1 -1
- package/skills/first-tree/engine/init.ts +285 -16
- package/skills/first-tree/engine/repo.ts +185 -9
- package/skills/first-tree/engine/rules/agent-instructions.ts +29 -7
- package/skills/first-tree/engine/runtime/asset-loader.ts +7 -0
- package/skills/first-tree/engine/upgrade.ts +66 -9
- package/skills/first-tree/engine/validators/nodes.ts +48 -3
- package/skills/first-tree/engine/verify.ts +61 -3
- package/skills/first-tree/references/maintainer-architecture.md +1 -1
- package/skills/first-tree/references/maintainer-build-and-distribution.md +3 -0
- package/skills/first-tree/references/maintainer-thin-cli.md +1 -1
- package/skills/first-tree/references/onboarding.md +32 -9
- package/skills/first-tree/references/source-map.md +3 -3
- package/skills/first-tree/references/upgrade-contract.md +14 -5
- package/skills/first-tree/scripts/check-skill-sync.sh +1 -1
- package/skills/first-tree/tests/helpers.ts +24 -4
- package/skills/first-tree/tests/init.test.ts +103 -6
- package/skills/first-tree/tests/repo.test.ts +87 -9
- package/skills/first-tree/tests/rules.test.ts +26 -7
- package/skills/first-tree/tests/skill-artifacts.test.ts +4 -0
- package/skills/first-tree/tests/thin-cli.test.ts +52 -7
- package/skills/first-tree/tests/upgrade.test.ts +19 -5
- package/skills/first-tree/tests/verify.test.ts +106 -7
- package/dist/onboarding-6Fr5Gkrk.js +0 -2
- 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 {
|
|
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
|
-
|
|
138
|
+
makeGitRepo(repoDir.path);
|
|
125
139
|
makeSourceSkill(sourceDir.path, "0.2.0");
|
|
126
140
|
|
|
127
|
-
const ret = runInit(new Repo(repoDir.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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
// ---
|
|
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
|
-
|
|
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,
|
|
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.
|
|
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,
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
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 {
|
|
4
|
-
import {
|
|
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
|
|
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
|
|
31
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
});
|