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.
Files changed (80) hide show
  1. package/README.md +116 -40
  2. package/dist/cli.js +46 -17
  3. package/dist/help-Dtdj91HJ.js +25 -0
  4. package/dist/init--VepFe6N.js +403 -0
  5. package/dist/installer-cH7N4RNj.js +47 -0
  6. package/dist/onboarding-C9cYSE6F.js +2 -0
  7. package/dist/onboarding-CPP8fF4D.js +10 -0
  8. package/dist/repo-DY57bMqr.js +318 -0
  9. package/dist/upgrade-Cgx_K2HM.js +135 -0
  10. package/dist/{verify-CSRIkuoM.js → verify-mC9ZTd1f.js} +118 -29
  11. package/package.json +33 -10
  12. package/skills/first-tree/SKILL.md +113 -0
  13. package/skills/first-tree/agents/openai.yaml +4 -0
  14. package/skills/first-tree/assets/framework/VERSION +1 -0
  15. package/skills/first-tree/assets/framework/examples/claude-code/README.md +14 -0
  16. package/skills/first-tree/assets/framework/examples/claude-code/settings.json +14 -0
  17. package/skills/first-tree/assets/framework/helpers/generate-codeowners.ts +224 -0
  18. package/skills/first-tree/assets/framework/helpers/inject-tree-context.sh +15 -0
  19. package/skills/first-tree/assets/framework/helpers/run-review.ts +193 -0
  20. package/skills/first-tree/assets/framework/manifest.json +11 -0
  21. package/skills/first-tree/assets/framework/prompts/pr-review.md +38 -0
  22. package/skills/first-tree/assets/framework/templates/agents.md.template +49 -0
  23. package/skills/first-tree/assets/framework/templates/member-node.md.template +18 -0
  24. package/skills/first-tree/assets/framework/templates/members-domain.md.template +45 -0
  25. package/skills/first-tree/assets/framework/templates/root-node.md.template +41 -0
  26. package/skills/first-tree/assets/framework/workflows/codeowners.yml +31 -0
  27. package/skills/first-tree/assets/framework/workflows/pr-review.yml +146 -0
  28. package/skills/first-tree/assets/framework/workflows/validate.yml +19 -0
  29. package/skills/first-tree/engine/commands/help.ts +32 -0
  30. package/skills/first-tree/engine/commands/init.ts +1 -0
  31. package/skills/first-tree/engine/commands/upgrade.ts +1 -0
  32. package/skills/first-tree/engine/commands/verify.ts +1 -0
  33. package/skills/first-tree/engine/init.ts +414 -0
  34. package/skills/first-tree/engine/onboarding.ts +10 -0
  35. package/skills/first-tree/engine/repo.ts +360 -0
  36. package/skills/first-tree/engine/rules/agent-instructions.ts +59 -0
  37. package/skills/first-tree/engine/rules/agent-integration.ts +19 -0
  38. package/skills/first-tree/engine/rules/ci-validation.ts +72 -0
  39. package/skills/first-tree/engine/rules/framework.ts +13 -0
  40. package/skills/first-tree/engine/rules/index.ts +41 -0
  41. package/skills/first-tree/engine/rules/members.ts +21 -0
  42. package/skills/first-tree/engine/rules/populate-tree.ts +36 -0
  43. package/skills/first-tree/engine/rules/root-node.ts +41 -0
  44. package/skills/first-tree/engine/runtime/adapters.ts +22 -0
  45. package/skills/first-tree/engine/runtime/asset-loader.ts +141 -0
  46. package/skills/first-tree/engine/runtime/installer.ts +82 -0
  47. package/skills/first-tree/engine/runtime/upgrader.ts +23 -0
  48. package/skills/first-tree/engine/upgrade.ts +233 -0
  49. package/skills/first-tree/engine/validators/members.ts +215 -0
  50. package/skills/first-tree/engine/validators/nodes.ts +559 -0
  51. package/skills/first-tree/engine/verify.ts +155 -0
  52. package/skills/first-tree/references/about.md +36 -0
  53. package/skills/first-tree/references/maintainer-architecture.md +59 -0
  54. package/skills/first-tree/references/maintainer-build-and-distribution.md +59 -0
  55. package/skills/first-tree/references/maintainer-testing.md +58 -0
  56. package/skills/first-tree/references/maintainer-thin-cli.md +38 -0
  57. package/skills/first-tree/references/onboarding.md +185 -0
  58. package/skills/first-tree/references/ownership-and-naming.md +94 -0
  59. package/skills/first-tree/references/principles.md +113 -0
  60. package/skills/first-tree/references/source-map.md +94 -0
  61. package/skills/first-tree/references/upgrade-contract.md +94 -0
  62. package/skills/first-tree/scripts/check-skill-sync.sh +133 -0
  63. package/skills/first-tree/scripts/quick_validate.py +95 -0
  64. package/skills/first-tree/scripts/run-local-cli.sh +35 -0
  65. package/skills/first-tree/tests/asset-loader.test.ts +75 -0
  66. package/skills/first-tree/tests/generate-codeowners.test.ts +94 -0
  67. package/skills/first-tree/tests/helpers.ts +169 -0
  68. package/skills/first-tree/tests/init.test.ts +250 -0
  69. package/skills/first-tree/tests/repo.test.ts +440 -0
  70. package/skills/first-tree/tests/rules.test.ts +413 -0
  71. package/skills/first-tree/tests/run-review.test.ts +155 -0
  72. package/skills/first-tree/tests/skill-artifacts.test.ts +311 -0
  73. package/skills/first-tree/tests/thin-cli.test.ts +104 -0
  74. package/skills/first-tree/tests/upgrade.test.ts +103 -0
  75. package/skills/first-tree/tests/validate-members.test.ts +224 -0
  76. package/skills/first-tree/tests/validate-nodes.test.ts +198 -0
  77. package/skills/first-tree/tests/verify.test.ts +241 -0
  78. package/dist/init-CE_944sb.js +0 -283
  79. package/dist/repo-BByc3VvM.js +0 -111
  80. package/dist/upgrade-Chr7z0CY.js +0 -82
@@ -0,0 +1,141 @@
1
+ import { existsSync, statSync } from "node:fs";
2
+ import { join } from "node:path";
3
+
4
+ export const SKILL_NAME = "first-tree";
5
+ export const SKILL_ROOT = join("skills", SKILL_NAME);
6
+ export const SKILL_AGENTS_DIR = join(SKILL_ROOT, "agents");
7
+ export const SKILL_REFERENCES_DIR = join(SKILL_ROOT, "references");
8
+ export const FRAMEWORK_ASSET_ROOT = join(SKILL_ROOT, "assets", "framework");
9
+ export const FRAMEWORK_MANIFEST = join(FRAMEWORK_ASSET_ROOT, "manifest.json");
10
+ export const FRAMEWORK_VERSION = join(FRAMEWORK_ASSET_ROOT, "VERSION");
11
+ export const FRAMEWORK_TEMPLATES_DIR = join(FRAMEWORK_ASSET_ROOT, "templates");
12
+ export const FRAMEWORK_WORKFLOWS_DIR = join(FRAMEWORK_ASSET_ROOT, "workflows");
13
+ export const FRAMEWORK_PROMPTS_DIR = join(FRAMEWORK_ASSET_ROOT, "prompts");
14
+ export const FRAMEWORK_EXAMPLES_DIR = join(FRAMEWORK_ASSET_ROOT, "examples");
15
+ export const FRAMEWORK_HELPERS_DIR = join(FRAMEWORK_ASSET_ROOT, "helpers");
16
+ export const INSTALLED_PROGRESS = join(SKILL_ROOT, "progress.md");
17
+ export const AGENT_INSTRUCTIONS_FILE = "AGENTS.md";
18
+ export const LEGACY_AGENT_INSTRUCTIONS_FILE = "AGENT.md";
19
+ export const AGENT_INSTRUCTIONS_TEMPLATE = "agents.md.template";
20
+
21
+ export const LEGACY_SKILL_NAME = "first-tree-cli-framework";
22
+ export const LEGACY_SKILL_ROOT = join("skills", LEGACY_SKILL_NAME);
23
+ export const LEGACY_SKILL_ASSET_ROOT = join(
24
+ LEGACY_SKILL_ROOT,
25
+ "assets",
26
+ "framework",
27
+ );
28
+ export const LEGACY_SKILL_VERSION = join(LEGACY_SKILL_ASSET_ROOT, "VERSION");
29
+ export const LEGACY_SKILL_TEMPLATES_DIR = join(
30
+ LEGACY_SKILL_ASSET_ROOT,
31
+ "templates",
32
+ );
33
+ export const LEGACY_SKILL_WORKFLOWS_DIR = join(
34
+ LEGACY_SKILL_ASSET_ROOT,
35
+ "workflows",
36
+ );
37
+ export const LEGACY_SKILL_PROMPTS_DIR = join(
38
+ LEGACY_SKILL_ASSET_ROOT,
39
+ "prompts",
40
+ );
41
+ export const LEGACY_SKILL_EXAMPLES_DIR = join(
42
+ LEGACY_SKILL_ASSET_ROOT,
43
+ "examples",
44
+ );
45
+ export const LEGACY_SKILL_PROGRESS = join(LEGACY_SKILL_ROOT, "progress.md");
46
+
47
+ export const LEGACY_FRAMEWORK_ROOT = ".context-tree";
48
+ export const LEGACY_VERSION = join(LEGACY_FRAMEWORK_ROOT, "VERSION");
49
+ export const LEGACY_PROGRESS = join(LEGACY_FRAMEWORK_ROOT, "progress.md");
50
+ export const LEGACY_TEMPLATES_DIR = join(LEGACY_FRAMEWORK_ROOT, "templates");
51
+ export const LEGACY_WORKFLOWS_DIR = join(LEGACY_FRAMEWORK_ROOT, "workflows");
52
+ export const LEGACY_PROMPTS_DIR = join(LEGACY_FRAMEWORK_ROOT, "prompts");
53
+ export const LEGACY_EXAMPLES_DIR = join(LEGACY_FRAMEWORK_ROOT, "examples");
54
+
55
+ export type FrameworkLayout = "skill" | "legacy-skill" | "legacy";
56
+
57
+ function pathExists(root: string, relPath: string): boolean {
58
+ const fullPath = join(root, relPath);
59
+ try {
60
+ return existsSync(fullPath);
61
+ } catch {
62
+ return false;
63
+ }
64
+ }
65
+
66
+ export function frameworkVersionCandidates(): string[] {
67
+ return [FRAMEWORK_VERSION, LEGACY_SKILL_VERSION, LEGACY_VERSION];
68
+ }
69
+
70
+ export function progressFileCandidates(): string[] {
71
+ return [INSTALLED_PROGRESS, LEGACY_SKILL_PROGRESS, LEGACY_PROGRESS];
72
+ }
73
+
74
+ export function agentInstructionsFileCandidates(): string[] {
75
+ return [AGENT_INSTRUCTIONS_FILE, LEGACY_AGENT_INSTRUCTIONS_FILE];
76
+ }
77
+
78
+ export function frameworkTemplateDirCandidates(): string[] {
79
+ return [
80
+ FRAMEWORK_TEMPLATES_DIR,
81
+ LEGACY_SKILL_TEMPLATES_DIR,
82
+ LEGACY_TEMPLATES_DIR,
83
+ ];
84
+ }
85
+
86
+ export function frameworkWorkflowDirCandidates(): string[] {
87
+ return [
88
+ FRAMEWORK_WORKFLOWS_DIR,
89
+ LEGACY_SKILL_WORKFLOWS_DIR,
90
+ LEGACY_WORKFLOWS_DIR,
91
+ ];
92
+ }
93
+
94
+ export function frameworkPromptDirCandidates(): string[] {
95
+ return [
96
+ FRAMEWORK_PROMPTS_DIR,
97
+ LEGACY_SKILL_PROMPTS_DIR,
98
+ LEGACY_PROMPTS_DIR,
99
+ ];
100
+ }
101
+
102
+ export function frameworkExampleDirCandidates(): string[] {
103
+ return [
104
+ FRAMEWORK_EXAMPLES_DIR,
105
+ LEGACY_SKILL_EXAMPLES_DIR,
106
+ LEGACY_EXAMPLES_DIR,
107
+ ];
108
+ }
109
+
110
+ export function resolveFirstExistingPath(
111
+ root: string,
112
+ candidates: string[],
113
+ ): string | null {
114
+ for (const candidate of candidates) {
115
+ if (pathExists(root, candidate)) {
116
+ return candidate;
117
+ }
118
+ }
119
+ return null;
120
+ }
121
+
122
+ export function detectFrameworkLayout(root: string): FrameworkLayout | null {
123
+ if (pathExists(root, FRAMEWORK_VERSION)) {
124
+ return "skill";
125
+ }
126
+ if (pathExists(root, LEGACY_SKILL_VERSION)) {
127
+ return "legacy-skill";
128
+ }
129
+ if (pathExists(root, LEGACY_VERSION)) {
130
+ return "legacy";
131
+ }
132
+ return null;
133
+ }
134
+
135
+ export function isDirectory(root: string, relPath: string): boolean {
136
+ try {
137
+ return statSync(join(root, relPath)).isDirectory();
138
+ } catch {
139
+ return false;
140
+ }
141
+ }
@@ -0,0 +1,82 @@
1
+ import { copyFileSync, cpSync, existsSync, mkdirSync, rmSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import {
5
+ FRAMEWORK_ASSET_ROOT,
6
+ LEGACY_SKILL_ROOT,
7
+ SKILL_ROOT,
8
+ } from "#skill/engine/runtime/asset-loader.js";
9
+
10
+ export function resolveBundledPackageRoot(startUrl = import.meta.url): string {
11
+ let dir = dirname(fileURLToPath(startUrl));
12
+ while (true) {
13
+ if (
14
+ existsSync(join(dir, "package.json")) &&
15
+ existsSync(join(dir, SKILL_ROOT, "SKILL.md"))
16
+ ) {
17
+ return dir;
18
+ }
19
+
20
+ const parent = dirname(dir);
21
+ if (parent === dir) {
22
+ break;
23
+ }
24
+ dir = parent;
25
+ }
26
+
27
+ throw new Error(
28
+ "Could not locate the bundled `first-tree` package root. Reinstall the package and try again.",
29
+ );
30
+ }
31
+
32
+ export function resolveCanonicalSkillRoot(sourceRoot: string): string {
33
+ const directSkillRoot = sourceRoot;
34
+ if (
35
+ existsSync(join(directSkillRoot, "SKILL.md")) &&
36
+ existsSync(join(directSkillRoot, "assets", "framework", "VERSION"))
37
+ ) {
38
+ return directSkillRoot;
39
+ }
40
+
41
+ const nestedSkillRoot = join(sourceRoot, SKILL_ROOT);
42
+ if (
43
+ existsSync(join(nestedSkillRoot, "SKILL.md")) &&
44
+ existsSync(join(nestedSkillRoot, "assets", "framework", "VERSION"))
45
+ ) {
46
+ return nestedSkillRoot;
47
+ }
48
+
49
+ throw new Error(
50
+ `Canonical skill not found under ${sourceRoot}. Reinstall the \`first-tree\` package and try again.`,
51
+ );
52
+ }
53
+
54
+ export function copyCanonicalSkill(sourceRoot: string, targetRoot: string): void {
55
+ const src = resolveCanonicalSkillRoot(sourceRoot);
56
+ const dst = join(targetRoot, SKILL_ROOT);
57
+ const legacyDst = join(targetRoot, LEGACY_SKILL_ROOT);
58
+ if (existsSync(dst)) {
59
+ rmSync(dst, { recursive: true, force: true });
60
+ }
61
+ if (legacyDst !== dst && existsSync(legacyDst)) {
62
+ rmSync(legacyDst, { recursive: true, force: true });
63
+ }
64
+ mkdirSync(dirname(dst), { recursive: true });
65
+ cpSync(src, dst, { recursive: true });
66
+ }
67
+
68
+ export function renderTemplateFile(
69
+ frameworkRoot: string,
70
+ templateName: string,
71
+ targetRoot: string,
72
+ targetPath: string,
73
+ ): boolean {
74
+ const src = join(frameworkRoot, "templates", templateName);
75
+ const dst = join(targetRoot, targetPath);
76
+ if (existsSync(dst) || !existsSync(src)) {
77
+ return false;
78
+ }
79
+ mkdirSync(dirname(dst), { recursive: true });
80
+ copyFileSync(src, dst);
81
+ return true;
82
+ }
@@ -0,0 +1,23 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { resolveCanonicalSkillRoot } from "#skill/engine/runtime/installer.js";
4
+
5
+ export function compareFrameworkVersions(left: string, right: string): number {
6
+ const result = left.localeCompare(right, undefined, {
7
+ numeric: true,
8
+ sensitivity: "base",
9
+ });
10
+ if (result < 0) return -1;
11
+ if (result > 0) return 1;
12
+ return 0;
13
+ }
14
+
15
+ export function readSourceVersion(sourceRoot: string): string | null {
16
+ const skillRoot = resolveCanonicalSkillRoot(sourceRoot);
17
+ const versionPath = join(skillRoot, "assets", "framework", "VERSION");
18
+ try {
19
+ return readFileSync(versionPath, "utf-8").trim();
20
+ } catch {
21
+ return null;
22
+ }
23
+ }
@@ -0,0 +1,233 @@
1
+ import { mkdirSync, rmSync, writeFileSync } from "node:fs";
2
+ import { dirname, join, resolve } from "node:path";
3
+ import { Repo } from "#skill/engine/repo.js";
4
+ import {
5
+ AGENT_INSTRUCTIONS_FILE,
6
+ AGENT_INSTRUCTIONS_TEMPLATE,
7
+ FRAMEWORK_WORKFLOWS_DIR,
8
+ FRAMEWORK_TEMPLATES_DIR,
9
+ FRAMEWORK_VERSION,
10
+ INSTALLED_PROGRESS,
11
+ LEGACY_AGENT_INSTRUCTIONS_FILE,
12
+ LEGACY_FRAMEWORK_ROOT,
13
+ LEGACY_SKILL_ROOT,
14
+ SKILL_ROOT,
15
+ type FrameworkLayout,
16
+ } from "#skill/engine/runtime/asset-loader.js";
17
+ import {
18
+ copyCanonicalSkill,
19
+ resolveBundledPackageRoot,
20
+ } from "#skill/engine/runtime/installer.js";
21
+ import {
22
+ compareFrameworkVersions,
23
+ readSourceVersion,
24
+ } from "#skill/engine/runtime/upgrader.js";
25
+
26
+ export const UPGRADE_USAGE = `usage: context-tree upgrade [--tree-path PATH]
27
+
28
+ Options:
29
+ --tree-path PATH Upgrade a tree repo from another working directory
30
+ --help Show this help message
31
+ `;
32
+
33
+ function writeProgress(repo: Repo, content: string): void {
34
+ const progressPath = join(repo.root, repo.preferredProgressPath());
35
+ mkdirSync(dirname(progressPath), { recursive: true });
36
+ writeFileSync(progressPath, content);
37
+ }
38
+
39
+ function formatUpgradeTaskList(
40
+ repo: Repo,
41
+ localVersion: string,
42
+ packagedVersion: string,
43
+ layout: FrameworkLayout,
44
+ ): string {
45
+ const lines: string[] = [
46
+ `# Context Tree Upgrade — v${localVersion} -> v${packagedVersion}\n`,
47
+ "## Installed Skill",
48
+ `- [ ] Review local customizations under \`${SKILL_ROOT}/\` and reapply them if needed`,
49
+ `- [ ] Re-copy any workflow updates you want from \`${FRAMEWORK_WORKFLOWS_DIR}/\` into \`.github/workflows/\``,
50
+ `- [ ] Re-check any local agent setup that references \`${SKILL_ROOT}/assets/framework/examples/\` or \`${SKILL_ROOT}/assets/framework/helpers/\``,
51
+ "",
52
+ ];
53
+
54
+ const migrationTasks: string[] = [];
55
+ if (layout === "legacy") {
56
+ migrationTasks.push(
57
+ "- [ ] Remove any stale `.context-tree/` references from repo-specific docs, scripts, or workflow files",
58
+ );
59
+ }
60
+
61
+ if (layout === "legacy-skill") {
62
+ migrationTasks.push(
63
+ `- [ ] Remove any stale \`${LEGACY_SKILL_ROOT}/\` references from repo-specific docs, scripts, workflow files, or agent config`,
64
+ );
65
+ }
66
+
67
+ if (repo.hasCanonicalAgentInstructionsFile() && repo.hasLegacyAgentInstructionsFile()) {
68
+ migrationTasks.push(
69
+ `- [ ] Merge any remaining user-authored content from \`${LEGACY_AGENT_INSTRUCTIONS_FILE}\` into \`${AGENT_INSTRUCTIONS_FILE}\`, then delete the legacy file`,
70
+ );
71
+ } else if (repo.hasLegacyAgentInstructionsFile()) {
72
+ migrationTasks.push(
73
+ `- [ ] Rename \`${LEGACY_AGENT_INSTRUCTIONS_FILE}\` to \`${AGENT_INSTRUCTIONS_FILE}\` to use the canonical agent instructions filename`,
74
+ );
75
+ }
76
+
77
+ if (migrationTasks.length > 0) {
78
+ lines.push("## Migration", ...migrationTasks, "");
79
+ }
80
+
81
+ if (repo.hasAgentInstructionsMarkers()) {
82
+ lines.push(
83
+ "## Agent Instructions",
84
+ `- [ ] Compare the framework section in \`${AGENT_INSTRUCTIONS_FILE}\` with \`${FRAMEWORK_TEMPLATES_DIR}/${AGENT_INSTRUCTIONS_TEMPLATE}\` and update the content between the markers if needed`,
85
+ "",
86
+ );
87
+ }
88
+
89
+ lines.push(
90
+ "## Verification",
91
+ `- [ ] \`${FRAMEWORK_VERSION}\` reads \`${packagedVersion}\``,
92
+ "- [ ] `context-tree verify` passes",
93
+ "",
94
+ "---",
95
+ "",
96
+ "**Important:** As you complete each task, check it off in" +
97
+ ` \`${INSTALLED_PROGRESS}\` by changing \`- [ ]\` to \`- [x]\`.` +
98
+ " Run `context-tree verify` when done — it will fail if any" +
99
+ " items remain unchecked.",
100
+ "",
101
+ );
102
+
103
+ return lines.join("\n");
104
+ }
105
+
106
+ export interface UpgradeOptions {
107
+ sourceRoot?: string;
108
+ }
109
+
110
+ export function runUpgrade(repo?: Repo, options?: UpgradeOptions): number {
111
+ const workingRepo = repo ?? new Repo();
112
+
113
+ if (workingRepo.isLikelySourceRepo() && !workingRepo.looksLikeTreeRepo()) {
114
+ console.error(
115
+ "Error: no installed framework skill found here. This looks like a source/workspace repo. Run `context-tree init` to create a dedicated tree repo, or pass `--tree-path` to upgrade an existing tree repo.",
116
+ );
117
+ return 1;
118
+ }
119
+
120
+ if (!workingRepo.hasFramework()) {
121
+ console.error(
122
+ "Error: no installed framework skill found. Run `context-tree init` first.",
123
+ );
124
+ return 1;
125
+ }
126
+
127
+ const layout = workingRepo.frameworkLayout();
128
+ if (layout === null) {
129
+ console.error(
130
+ "Error: no installed framework skill found. Run `context-tree init` first.",
131
+ );
132
+ return 1;
133
+ }
134
+ const localVersion = workingRepo.readVersion() ?? "unknown";
135
+ console.log(`Local framework version: ${localVersion}\n`);
136
+
137
+ console.log(
138
+ "Checking the framework skill bundled with this first-tree package...",
139
+ );
140
+
141
+ let sourceRoot: string;
142
+ try {
143
+ sourceRoot = options?.sourceRoot ?? resolveBundledPackageRoot();
144
+ } catch (err) {
145
+ const message = err instanceof Error ? err.message : "unknown error";
146
+ console.error(`Error: ${message}`);
147
+ return 1;
148
+ }
149
+
150
+ const packagedVersion = readSourceVersion(sourceRoot);
151
+ if (packagedVersion === null) {
152
+ console.log(
153
+ "Could not read the bundled framework version. Reinstall or update `first-tree` and try again.",
154
+ );
155
+ return 1;
156
+ }
157
+
158
+ if (
159
+ localVersion !== "unknown" &&
160
+ compareFrameworkVersions(localVersion, packagedVersion) > 0
161
+ ) {
162
+ console.log(
163
+ "The installed framework is newer than the skill bundled with this `first-tree` package. Install a newer package version before running `context-tree upgrade`.",
164
+ );
165
+ return 1;
166
+ }
167
+
168
+ if (layout === "skill" && packagedVersion === localVersion) {
169
+ console.log(
170
+ `Already up to date with the bundled skill (${FRAMEWORK_VERSION} = ${localVersion}).`,
171
+ );
172
+ return 0;
173
+ }
174
+
175
+ copyCanonicalSkill(sourceRoot, workingRepo.root);
176
+ if (layout === "legacy") {
177
+ rmSync(join(workingRepo.root, LEGACY_FRAMEWORK_ROOT), {
178
+ recursive: true,
179
+ force: true,
180
+ });
181
+ console.log(
182
+ "Migrated legacy .context-tree/ layout to skills/first-tree/.",
183
+ );
184
+ } else if (layout === "legacy-skill") {
185
+ console.log(
186
+ "Migrated skills/first-tree-cli-framework/ to skills/first-tree/.",
187
+ );
188
+ } else {
189
+ console.log(
190
+ "Refreshed skills/first-tree/ from the bundled first-tree package.",
191
+ );
192
+ }
193
+
194
+ const output = formatUpgradeTaskList(
195
+ workingRepo,
196
+ localVersion,
197
+ packagedVersion,
198
+ layout,
199
+ );
200
+ console.log(`\n${output}`);
201
+ writeProgress(workingRepo, output);
202
+ console.log(`Progress file written to ${workingRepo.preferredProgressPath()}`);
203
+ return 0;
204
+ }
205
+
206
+ export function runUpgradeCli(args: string[] = []): number {
207
+ if (args.includes("--help") || args.includes("-h")) {
208
+ console.log(UPGRADE_USAGE);
209
+ return 0;
210
+ }
211
+
212
+ let treePath: string | undefined;
213
+ for (let index = 0; index < args.length; index += 1) {
214
+ const arg = args[index];
215
+ if (arg === "--tree-path") {
216
+ const value = args[index + 1];
217
+ if (!value) {
218
+ console.error("Missing value for --tree-path");
219
+ console.log(UPGRADE_USAGE);
220
+ return 1;
221
+ }
222
+ treePath = value;
223
+ index += 1;
224
+ continue;
225
+ }
226
+
227
+ console.error(`Unknown upgrade option: ${arg}`);
228
+ console.log(UPGRADE_USAGE);
229
+ return 1;
230
+ }
231
+
232
+ return runUpgrade(treePath ? new Repo(resolve(process.cwd(), treePath)) : undefined);
233
+ }
@@ -0,0 +1,215 @@
1
+ import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
2
+ import { basename, join, relative } from "node:path";
3
+
4
+ const FRONTMATTER_RE = /^---\s*\n(.*?)\n---/s;
5
+ const VALID_TYPES = new Set(["human", "personal_assistant", "autonomous_agent"]);
6
+
7
+ function rel(path: string, root: string): string {
8
+ return relative(root, path);
9
+ }
10
+
11
+ function parseFrontmatter(path: string): string | null {
12
+ try {
13
+ const text = readFileSync(path, "utf-8");
14
+ const m = text.match(FRONTMATTER_RE);
15
+ return m ? m[1] : null;
16
+ } catch {
17
+ return null;
18
+ }
19
+ }
20
+
21
+ export function extractScalar(fm: string, key: string): string | null {
22
+ const re = new RegExp(`^${key}:\\s*"?([^"\\n]+?)"?\\s*$`, "m");
23
+ const m = fm.match(re);
24
+ return m ? m[1].trim() : null;
25
+ }
26
+
27
+ export function extractList(fm: string, key: string): string[] | null {
28
+ // Inline: key: [a, b]
29
+ const inlineRe = new RegExp(`^${key}:\\s*\\[([^\\]]*)\\]`, "m");
30
+ let m = fm.match(inlineRe);
31
+ if (m) {
32
+ const raw = m[1].trim();
33
+ if (!raw) return [];
34
+ return raw
35
+ .split(",")
36
+ .map((s) => s.trim().replace(/^['"]|['"]$/g, ""))
37
+ .filter(Boolean);
38
+ }
39
+
40
+ // Block: key:\n - a\n - b
41
+ const blockRe = new RegExp(`^${key}:\\s*\\n((?:\\s+-\\s+.+\\n?)+)`, "m");
42
+ m = fm.match(blockRe);
43
+ if (m) {
44
+ return m[1]
45
+ .trim()
46
+ .split("\n")
47
+ .filter((line) => line.trim())
48
+ .map((line) =>
49
+ line
50
+ .trim()
51
+ .replace(/^-\s*/, "")
52
+ .trim()
53
+ .replace(/^['"]|['"]$/g, ""),
54
+ );
55
+ }
56
+
57
+ return null;
58
+ }
59
+
60
+ export function validateMember(
61
+ nodePath: string,
62
+ treeRoot: string,
63
+ ): string[] {
64
+ const errors: string[] = [];
65
+ const loc = rel(nodePath, treeRoot);
66
+
67
+ const fm = parseFrontmatter(nodePath);
68
+ if (fm === null) return [`${loc}: no frontmatter found`];
69
+
70
+ // title
71
+ const title = extractScalar(fm, "title");
72
+ if (!title) errors.push(`${loc}: missing or empty 'title' field`);
73
+
74
+ // owners
75
+ const owners = extractList(fm, "owners");
76
+ if (owners === null) errors.push(`${loc}: missing 'owners' field`);
77
+
78
+ // type
79
+ const memberType = extractScalar(fm, "type");
80
+ if (!memberType) {
81
+ errors.push(`${loc}: missing 'type' field`);
82
+ } else if (!VALID_TYPES.has(memberType)) {
83
+ errors.push(
84
+ `${loc}: invalid type '${memberType}' — must be one of: ${[...VALID_TYPES].sort().join(", ")}`,
85
+ );
86
+ }
87
+
88
+ // role
89
+ const role = extractScalar(fm, "role");
90
+ if (!role) errors.push(`${loc}: missing or empty 'role' field`);
91
+
92
+ // domains
93
+ const domains = extractList(fm, "domains");
94
+ if (domains === null) {
95
+ errors.push(`${loc}: missing 'domains' field`);
96
+ } else if (domains.length === 0) {
97
+ errors.push(`${loc}: 'domains' must contain at least one entry`);
98
+ }
99
+
100
+ return errors;
101
+ }
102
+
103
+ /** Info collected per member for cross-validation. */
104
+ type MemberInfo = {
105
+ name: string;
106
+ relPath: string;
107
+ type: string | null;
108
+ delegateMention: string | null;
109
+ };
110
+
111
+ export function runValidateMembers(treeRoot: string): {
112
+ exitCode: number;
113
+ errors: string[];
114
+ } {
115
+ const membersDir = join(treeRoot, "members");
116
+ if (!existsSync(membersDir) || !statSync(membersDir).isDirectory()) {
117
+ console.log(`Members directory not found: ${membersDir}`);
118
+ return { exitCode: 1, errors: [] };
119
+ }
120
+
121
+ const allErrors: string[] = [];
122
+ let memberCount = 0;
123
+
124
+ // Collected for cross-validation (name uniqueness + delegate_mention)
125
+ const nameOccurrences = new Map<string, string[]>();
126
+ const members: MemberInfo[] = [];
127
+
128
+ function walk(dir: string): void {
129
+ for (const child of readdirSync(dir).sort()) {
130
+ const childPath = join(dir, child);
131
+
132
+ // Reject stray .md files
133
+ try {
134
+ const stat = statSync(childPath);
135
+ if (stat.isFile() && child.endsWith(".md") && child !== "NODE.md") {
136
+ allErrors.push(
137
+ `${rel(childPath, treeRoot)}: member must be a directory with NODE.md, not a standalone file — use ${rel(dir, treeRoot)}/${child.replace(/\.md$/, "")}/NODE.md instead`,
138
+ );
139
+ continue;
140
+ }
141
+ if (!stat.isDirectory()) continue;
142
+ } catch {
143
+ continue;
144
+ }
145
+
146
+ // Track directory name occurrences for uniqueness check
147
+ const relPath = relative(membersDir, childPath);
148
+ const occurrences = nameOccurrences.get(child) ?? [];
149
+ occurrences.push(relPath);
150
+ nameOccurrences.set(child, occurrences);
151
+
152
+ const nodePath = join(childPath, "NODE.md");
153
+ if (!existsSync(nodePath)) {
154
+ allErrors.push(`${rel(childPath, treeRoot)}/: directory exists but missing NODE.md`);
155
+ walk(childPath);
156
+ continue;
157
+ }
158
+
159
+ memberCount++;
160
+ allErrors.push(...validateMember(nodePath, treeRoot));
161
+
162
+ // Collect info for cross-validation
163
+ const fm = parseFrontmatter(nodePath);
164
+ if (fm) {
165
+ members.push({
166
+ name: child,
167
+ relPath,
168
+ type: extractScalar(fm, "type"),
169
+ delegateMention: extractScalar(fm, "delegate_mention"),
170
+ });
171
+ }
172
+
173
+ // Recurse into subdirectories
174
+ walk(childPath);
175
+ }
176
+ }
177
+
178
+ walk(membersDir);
179
+
180
+ // Cross-validation: directory name uniqueness
181
+ for (const [name, paths] of nameOccurrences) {
182
+ if (paths.length > 1) {
183
+ allErrors.push(
184
+ `Duplicate member directory name '${name}' found at: ${paths.map((p) => `members/${p}`).join(", ")} — directory names must be unique across all levels under members/`,
185
+ );
186
+ }
187
+ }
188
+
189
+ // Cross-validation: delegate_mention references
190
+ const memberByName = new Map(members.map((m) => [m.name, m]));
191
+ for (const member of members) {
192
+ if (!member.delegateMention) continue;
193
+ const target = memberByName.get(member.delegateMention);
194
+ if (!target) {
195
+ allErrors.push(
196
+ `members/${member.relPath}/NODE.md: delegate_mention '${member.delegateMention}' references non-existent member — the target must be a directory under members/`,
197
+ );
198
+ } else if (target.type !== "personal_assistant") {
199
+ allErrors.push(
200
+ `members/${member.relPath}/NODE.md: delegate_mention '${member.delegateMention}' must reference a member with type 'personal_assistant', but '${target.name}' has type '${target.type}'`,
201
+ );
202
+ }
203
+ }
204
+
205
+ if (allErrors.length > 0) {
206
+ console.log(`Found ${allErrors.length} member validation error(s):\n`);
207
+ for (const err of allErrors) {
208
+ console.log(` \u2717 ${err}`);
209
+ }
210
+ return { exitCode: 1, errors: allErrors };
211
+ }
212
+
213
+ console.log(`All ${memberCount} member(s) passed validation.`);
214
+ return { exitCode: 0, errors: allErrors };
215
+ }