first-tree 0.0.3 → 0.0.5

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 (59) hide show
  1. package/README.md +78 -27
  2. package/dist/cli.js +28 -13
  3. package/dist/{help-xEI-s9iN.js → help-5-WG9QFm.js} +1 -1
  4. package/dist/{init-DtOjj0wc.js → init-CAq0Uhq6.js} +187 -25
  5. package/dist/{installer-rcZpGLnM.js → installer-UgNasLjl.js} +20 -16
  6. package/dist/onboarding-3zYUeYQb.js +2 -0
  7. package/dist/onboarding-Dd63N-V1.js +10 -0
  8. package/dist/repo-DkR12VUv.js +369 -0
  9. package/dist/upgrade-DYzuvv1k.js +140 -0
  10. package/dist/{verify-CxN6JiV9.js → verify-C0IUSkMZ.js} +66 -6
  11. package/package.json +12 -10
  12. package/skills/first-tree/SKILL.md +18 -10
  13. package/skills/first-tree/assets/framework/VERSION +1 -1
  14. package/skills/first-tree/assets/framework/examples/claude-code/README.md +2 -2
  15. package/skills/first-tree/assets/framework/examples/claude-code/settings.json +1 -1
  16. package/skills/first-tree/assets/framework/helpers/generate-codeowners.ts +1 -1
  17. package/skills/first-tree/assets/framework/helpers/inject-tree-context.sh +0 -0
  18. package/skills/first-tree/assets/framework/helpers/run-review.ts +17 -3
  19. package/skills/first-tree/assets/framework/templates/{agent.md.template → agents.md.template} +3 -2
  20. package/skills/first-tree/assets/framework/templates/members-domain.md.template +1 -1
  21. package/skills/first-tree/assets/framework/templates/root-node.md.template +9 -6
  22. package/skills/first-tree/assets/framework/workflows/codeowners.yml +1 -1
  23. package/skills/first-tree/assets/framework/workflows/pr-review.yml +1 -1
  24. package/skills/first-tree/engine/commands/init.ts +1 -1
  25. package/skills/first-tree/engine/commands/upgrade.ts +1 -1
  26. package/skills/first-tree/engine/commands/verify.ts +1 -1
  27. package/skills/first-tree/engine/init.ts +288 -18
  28. package/skills/first-tree/engine/repo.ts +220 -11
  29. package/skills/first-tree/engine/rules/agent-instructions.ts +29 -7
  30. package/skills/first-tree/engine/rules/agent-integration.ts +3 -1
  31. package/skills/first-tree/engine/rules/framework.ts +2 -2
  32. package/skills/first-tree/engine/runtime/adapters.ts +6 -2
  33. package/skills/first-tree/engine/runtime/asset-loader.ts +143 -4
  34. package/skills/first-tree/engine/runtime/installer.ts +18 -12
  35. package/skills/first-tree/engine/upgrade.ts +99 -15
  36. package/skills/first-tree/engine/validators/nodes.ts +48 -3
  37. package/skills/first-tree/engine/verify.ts +61 -3
  38. package/skills/first-tree/references/maintainer-architecture.md +1 -1
  39. package/skills/first-tree/references/maintainer-build-and-distribution.md +3 -0
  40. package/skills/first-tree/references/maintainer-thin-cli.md +1 -1
  41. package/skills/first-tree/references/onboarding.md +57 -24
  42. package/skills/first-tree/references/source-map.md +3 -3
  43. package/skills/first-tree/references/upgrade-contract.md +62 -27
  44. package/skills/first-tree/scripts/check-skill-sync.sh +1 -1
  45. package/skills/first-tree/scripts/quick_validate.py +0 -0
  46. package/skills/first-tree/scripts/run-local-cli.sh +0 -0
  47. package/skills/first-tree/tests/asset-loader.test.ts +23 -1
  48. package/skills/first-tree/tests/helpers.ts +51 -7
  49. package/skills/first-tree/tests/init.test.ts +113 -8
  50. package/skills/first-tree/tests/repo.test.ts +113 -9
  51. package/skills/first-tree/tests/rules.test.ts +35 -14
  52. package/skills/first-tree/tests/skill-artifacts.test.ts +10 -0
  53. package/skills/first-tree/tests/thin-cli.test.ts +52 -7
  54. package/skills/first-tree/tests/upgrade.test.ts +39 -6
  55. package/skills/first-tree/tests/verify.test.ts +109 -10
  56. package/dist/onboarding-6Fr5Gkrk.js +0 -2
  57. package/dist/onboarding-B9zPGvvG.js +0 -10
  58. package/dist/repo-BTJG8BU1.js +0 -187
  59. package/dist/upgrade-COGgI7Rj.js +0 -96
@@ -6,8 +6,9 @@ description: Maintain the canonical `first-tree` skill and the thin `context-tre
6
6
  # First Tree
7
7
 
8
8
  Use this skill when the task depends on the exact behavior of the
9
- `context-tree` CLI or the installed `skills/first-tree/` payload that
10
- `context-tree init` ships to user repos.
9
+ `context-tree` CLI or the installed `.agents/skills/first-tree/` and
10
+ `.claude/skills/first-tree/` payloads that `context-tree init` ships to user
11
+ repos.
11
12
 
12
13
  ## Source Of Truth
13
14
 
@@ -17,9 +18,9 @@ Use this skill when the task depends on the exact behavior of the
17
18
  repos.
18
19
  - `engine/` holds the canonical framework and CLI behavior.
19
20
  - `scripts/` holds maintenance helpers for validating and running the skill.
20
- - In maintainer docs, use `context-tree` for the CLI and `skills/first-tree/`
21
- for the installed skill path so it is not confused with the `first-tree`
22
- npm package.
21
+ - In maintainer docs, use `context-tree` for the CLI, `skills/first-tree/` for
22
+ the bundled source path, and `.agents/skills/first-tree/` /
23
+ `.claude/skills/first-tree/` for installed user-repo paths.
23
24
 
24
25
  ## When To Read What
25
26
 
@@ -51,18 +52,22 @@ Use this skill when the task depends on the exact behavior of the
51
52
  thin CLI shell, not as a tree repo.
52
53
  - Keep command behavior, validator behavior, shipped assets, maintainer
53
54
  references, and package shell aligned.
54
- - If root README/AGENT/CI text explains something non-obvious, migrate that
55
+ - If root README/AGENTS/CI text explains something non-obvious, migrate that
55
56
  information into `references/` and trim the root file back down.
56
57
  - If you change runtime assets or skill references, run `pnpm validate:skill`.
57
58
 
58
59
  ### Working In A User Tree Repo
59
60
 
60
- - `context-tree init` installs this skill into the user's repo and scaffolds
61
- `NODE.md`, `AGENT.md`, and `members/NODE.md`.
61
+ - `context-tree init` defaults to creating or reusing a sibling dedicated tree
62
+ repo when invoked from a source/workspace repo. Use `--here` to initialize
63
+ the current repo in place when you are already inside the tree repo.
64
+ - `context-tree init` installs this skill into the target tree repo and
65
+ scaffolds `.agents/skills/first-tree/`, `.claude/skills/first-tree/`,
66
+ `NODE.md`, `AGENTS.md`, and `members/NODE.md`.
62
67
  - `context-tree upgrade` refreshes the installed skill from the copy bundled
63
68
  with the currently running `first-tree` package. To pick up a newer
64
69
  framework, run a newer package version first. It also migrates older repos
65
- that still use `skills/first-tree-cli-framework/`.
70
+ that still use `skills/first-tree/` or `skills/first-tree-cli-framework/`.
66
71
  - The user's tree content lives outside the skill; the skill only carries the
67
72
  reusable framework payload plus maintenance guidance.
68
73
  - The tree still stores decisions, constraints, and ownership; execution detail
@@ -76,8 +81,11 @@ Use this skill when the task depends on the exact behavior of the
76
81
  - Keep decision knowledge in the tree and execution detail in source systems.
77
82
  - Keep the skill as the only canonical knowledge source. The root CLI/package
78
83
  shell must not become a second source of framework semantics.
84
+ - Keep the CLI name written as `context-tree` in maintainer and user-facing
85
+ docs so it is not confused with the `first-tree` package that ships it.
79
86
  - Keep normal `init` / `upgrade` flows self-contained. They must work from the
80
- skill bundled in the current package without cloning the source repo.
87
+ skill bundled in the current package without cloning the source repo or
88
+ relying on network access.
81
89
  - Make upgrade behavior explicit. If you change installed paths, update
82
90
  `references/upgrade-contract.md`, task text, and tests together.
83
91
 
@@ -1 +1 @@
1
- 0.1.0
1
+ 0.2.1
@@ -6,9 +6,9 @@ Copy `settings.json` to your tree repo's `.claude/` directory:
6
6
 
7
7
  ```bash
8
8
  mkdir -p .claude
9
- cp skills/first-tree/assets/framework/examples/claude-code/settings.json .claude/settings.json
9
+ cp .claude/skills/first-tree/assets/framework/examples/claude-code/settings.json .claude/settings.json
10
10
  ```
11
11
 
12
12
  ## What It Does
13
13
 
14
- The `SessionStart` hook runs `./skills/first-tree/assets/framework/helpers/inject-tree-context.sh` when a Claude Code session begins. This injects the root `NODE.md` content as additional context, giving the agent an overview of the tree structure before any task.
14
+ The `SessionStart` hook runs `./.claude/skills/first-tree/assets/framework/helpers/inject-tree-context.sh` when a Claude Code session begins. This injects the root `NODE.md` content as additional context, giving the agent an overview of the tree structure before any task.
@@ -5,7 +5,7 @@
5
5
  "hooks": [
6
6
  {
7
7
  "type": "command",
8
- "command": "./skills/first-tree/assets/framework/helpers/inject-tree-context.sh"
8
+ "command": "./.claude/skills/first-tree/assets/framework/helpers/inject-tree-context.sh"
9
9
  }
10
10
  ]
11
11
  }
@@ -205,7 +205,7 @@ export function generate(
205
205
  return 0;
206
206
  }
207
207
  console.log(
208
- "CODEOWNERS is out-of-date. Run: npx tsx skills/first-tree/assets/framework/helpers/generate-codeowners.ts",
208
+ "CODEOWNERS is out-of-date. Run: npx tsx .agents/skills/first-tree/assets/framework/helpers/generate-codeowners.ts",
209
209
  );
210
210
  return 1;
211
211
  }
@@ -8,7 +8,7 @@
8
8
  */
9
9
 
10
10
  import { execFileSync } from "node:child_process";
11
- import { readFileSync, writeFileSync } from "node:fs";
11
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
12
12
  import { homedir } from "node:os";
13
13
  import { join } from "node:path";
14
14
 
@@ -17,15 +17,29 @@ const MAX_ATTEMPTS = 3;
17
17
  // Per-invocation budget cap. Worst case is $1.50 total (3 × $0.50),
18
18
  // though retries are cheap in practice due to cached context via --continue.
19
19
  const MAX_BUDGET_USD = 0.5;
20
+ const AGENT_INSTRUCTIONS_PATHS = ["AGENTS.md", "AGENT.md"] as const;
21
+
22
+ function resolveAgentInstructionsPath(): string {
23
+ for (const candidate of AGENT_INSTRUCTIONS_PATHS) {
24
+ if (existsSync(candidate)) {
25
+ return candidate;
26
+ }
27
+ }
28
+
29
+ throw new Error(
30
+ "Missing AGENTS.md in repo root (legacy AGENT.md is also accepted during migration).",
31
+ );
32
+ }
20
33
 
21
34
  function buildPrompt(diffPath: string): string {
22
35
  const parts: string[] = [];
36
+ const agentInstructionsPath = resolveAgentInstructionsPath();
23
37
  const files: [string, string][] = [
24
- ["AGENT.md", "AGENT.md"],
38
+ [agentInstructionsPath, agentInstructionsPath],
25
39
  ["Root NODE.md", "NODE.md"],
26
40
  [
27
41
  "Review Instructions",
28
- "skills/first-tree/assets/framework/prompts/pr-review.md",
42
+ ".agents/skills/first-tree/assets/framework/prompts/pr-review.md",
29
43
  ],
30
44
  ];
31
45
  for (const [heading, path] of files) {
@@ -13,7 +13,7 @@ You are working in a **Context Tree** — the living source of truth for decisio
13
13
 
14
14
  4. **Git-native tree structure.** Each node is a file; each domain is a directory. Soft links allow cross-references without the complexity of a full graph. History, ownership, and review follow Git conventions.
15
15
 
16
- See `skills/first-tree/references/principles.md` for detailed explanations and examples.
16
+ See `.agents/skills/first-tree/references/principles.md` for detailed explanations and examples.
17
17
 
18
18
  ## Before Every Task
19
19
 
@@ -28,6 +28,7 @@ Do not skip this. The tree is already a compression of expensive knowledge — c
28
28
 
29
29
  - **Decide in the tree, execute in source systems.** If the task involves a decision (not just a bug fix), draft or update the relevant tree node before executing.
30
30
  - **The tree is not for execution details.** Function signatures, DB schemas, API endpoints, ad copy — those live in source systems. The tree captures the *why* and *how things connect*.
31
+ - **Bring source repos in as additional working directories.** When you need to inspect or edit code, open the relevant source repositories alongside this tree instead of copying execution detail into the tree.
31
32
  - **Respect ownership.** Each node declares owners in its frontmatter. If your changes touch a domain you don't own, flag it — the owner needs to review.
32
33
 
33
34
  ## After Every Task
@@ -40,7 +41,7 @@ Ask yourself: **Does the tree need updating?**
40
41
 
41
42
  ## Reference
42
43
 
43
- For ownership rules, tree structure, and key files, see [NODE.md](NODE.md) and `skills/first-tree/references/ownership-and-naming.md`.
44
+ For ownership rules, tree structure, and key files, see [NODE.md](NODE.md) and `.agents/skills/first-tree/references/ownership-and-naming.md`.
44
45
  <!-- END CONTEXT-TREE FRAMEWORK -->
45
46
 
46
47
  # Project-Specific Instructions
@@ -34,7 +34,7 @@ domains:
34
34
  ---
35
35
  ```
36
36
 
37
- See `skills/first-tree/assets/framework/templates/member-node.md.template` for a full scaffold.
37
+ See `.agents/skills/first-tree/assets/framework/templates/member-node.md.template` for a full scaffold.
38
38
 
39
39
  ---
40
40
 
@@ -5,9 +5,9 @@ owners: [<your-github-username>]
5
5
 
6
6
  # <Your Organization>
7
7
 
8
- <!-- PLACEHOLDER: Replace this with a description of your organization and what this tree captures. -->
8
+ <!-- PLACEHOLDER: Replace this with a description of your organization and what this tree captures across your source repos and systems. -->
9
9
 
10
- The living source of truth for your organization. A structured knowledge base that agents and humans build and maintain together.
10
+ The living source of truth for your organization. A structured knowledge base that agents and humans build and maintain together across one or more source repositories and systems.
11
11
 
12
12
  ---
13
13
 
@@ -29,10 +29,13 @@ The living source of truth for your organization. A structured knowledge base th
29
29
 
30
30
  ## Working with the Tree
31
31
 
32
- See [AGENT.md](AGENT.md) for agent instructions the before/during/after workflow, ownership model, and tree maintenance.
32
+ Keep decision context here. Keep implementation detail in the source repos this
33
+ tree describes.
33
34
 
34
- See [about.md](skills/first-tree/references/about.md) for background — the problem, the idea, and who it's for.
35
+ See [AGENTS.md](AGENTS.md) for agent instructions — the before/during/after workflow, ownership model, and tree maintenance.
36
+
37
+ See [about.md](.agents/skills/first-tree/references/about.md) for background — the problem, the idea, and who it's for.
35
38
 
36
39
  See the installed framework documentation:
37
- - [principles.md](skills/first-tree/references/principles.md) — core principles with examples
38
- - [ownership-and-naming.md](skills/first-tree/references/ownership-and-naming.md) — node naming and ownership model
40
+ - [principles.md](.agents/skills/first-tree/references/principles.md) — core principles with examples
41
+ - [ownership-and-naming.md](.agents/skills/first-tree/references/ownership-and-naming.md) — node naming and ownership model
@@ -19,7 +19,7 @@ jobs:
19
19
  with:
20
20
  node-version: "22"
21
21
 
22
- - run: npx tsx skills/first-tree/assets/framework/helpers/generate-codeowners.ts
22
+ - run: npx tsx .agents/skills/first-tree/assets/framework/helpers/generate-codeowners.ts
23
23
 
24
24
  - name: Commit if changed
25
25
  run: |
@@ -45,7 +45,7 @@ jobs:
45
45
  gh pr view "$PR_NUMBER" --json headRefOid -q .headRefOid > /tmp/pr-head-sha.txt
46
46
 
47
47
  - name: Run Claude review
48
- run: npx tsx skills/first-tree/assets/framework/helpers/run-review.ts
48
+ run: npx tsx .agents/skills/first-tree/assets/framework/helpers/run-review.ts
49
49
 
50
50
  - name: Parse and post review
51
51
  run: |
@@ -1 +1 @@
1
- export * from "#skill/engine/init.js";
1
+ export { INIT_USAGE, parseInitArgs, runInitCli as runInit } from "#skill/engine/init.js";
@@ -1 +1 @@
1
- export * from "#skill/engine/upgrade.js";
1
+ export { UPGRADE_USAGE, runUpgradeCli as runUpgrade } from "#skill/engine/upgrade.js";
@@ -1 +1 @@
1
- export * from "#skill/engine/verify.js";
1
+ export { VERIFY_USAGE, runVerifyCli as runVerify } from "#skill/engine/verify.js";
@@ -1,9 +1,12 @@
1
+ import { execFileSync } from "node:child_process";
1
2
  import {
2
3
  existsSync,
3
4
  mkdirSync,
5
+ readdirSync,
6
+ statSync,
4
7
  writeFileSync,
5
8
  } from "node:fs";
6
- import { dirname, join } from "node:path";
9
+ import { dirname, join, relative, resolve } from "node:path";
7
10
  import { Repo } from "#skill/engine/repo.js";
8
11
  import { ONBOARDING_TEXT } from "#skill/engine/onboarding.js";
9
12
  import { evaluateAll } from "#skill/engine/rules/index.js";
@@ -14,9 +17,13 @@ import {
14
17
  resolveBundledPackageRoot,
15
18
  } from "#skill/engine/runtime/installer.js";
16
19
  import {
20
+ AGENT_INSTRUCTIONS_FILE,
21
+ AGENT_INSTRUCTIONS_TEMPLATE,
17
22
  FRAMEWORK_ASSET_ROOT,
18
23
  FRAMEWORK_VERSION,
19
24
  INSTALLED_PROGRESS,
25
+ LEGACY_AGENT_INSTRUCTIONS_FILE,
26
+ installedSkillRootsDisplay,
20
27
  } from "#skill/engine/runtime/asset-loader.js";
21
28
 
22
29
  /**
@@ -25,34 +32,88 @@ import {
25
32
  * all generated task text at once.
26
33
  */
27
34
  export const INTERACTIVE_TOOL = "AskUserQuestion";
35
+ export const INIT_USAGE = `usage: context-tree init [--here] [--tree-name NAME] [--tree-path PATH]
28
36
 
29
- const TEMPLATE_MAP: [string, string][] = [
30
- ["root-node.md.template", "NODE.md"],
31
- ["agent.md.template", "AGENT.md"],
32
- ["members-domain.md.template", "members/NODE.md"],
37
+ By default, running \`context-tree init\` inside a source or workspace repo creates
38
+ a sibling dedicated tree repo named \`<repo>-context\`.
39
+
40
+ Options:
41
+ --here Initialize the current repo in place
42
+ --tree-name NAME Name the dedicated sibling tree repo to create
43
+ --tree-path PATH Use an explicit tree repo path
44
+ --help Show this help message
45
+ `;
46
+
47
+ interface TemplateTarget {
48
+ templateName: string;
49
+ targetPath: string;
50
+ skipIfExists?: string[];
51
+ }
52
+
53
+ const TEMPLATE_MAP: TemplateTarget[] = [
54
+ { templateName: "root-node.md.template", targetPath: "NODE.md" },
55
+ {
56
+ templateName: AGENT_INSTRUCTIONS_TEMPLATE,
57
+ targetPath: AGENT_INSTRUCTIONS_FILE,
58
+ skipIfExists: [AGENT_INSTRUCTIONS_FILE, LEGACY_AGENT_INSTRUCTIONS_FILE],
59
+ },
60
+ { templateName: "members-domain.md.template", targetPath: "members/NODE.md" },
33
61
  ];
34
62
 
63
+ interface TaskListContext {
64
+ sourceRepoPath?: string;
65
+ dedicatedTreeRepo?: boolean;
66
+ }
67
+
35
68
  function installSkill(source: string, target: string): void {
36
69
  copyCanonicalSkill(source, target);
37
70
  console.log(
38
- " Installed skills/first-tree/ from the bundled first-tree package",
71
+ ` Installed ${installedSkillRootsDisplay()} from the bundled first-tree package`,
39
72
  );
40
73
  }
41
74
 
42
75
  function renderTemplates(target: string): void {
43
76
  const frameworkDir = join(target, FRAMEWORK_ASSET_ROOT);
44
- for (const [templateName, targetPath] of TEMPLATE_MAP) {
45
- if (existsSync(join(target, targetPath))) {
46
- console.log(` Skipped ${targetPath} (already exists)`);
77
+ for (const { templateName, targetPath, skipIfExists } of TEMPLATE_MAP) {
78
+ const existingPaths = skipIfExists ?? [targetPath];
79
+ const existingPath = existingPaths.find((candidate) =>
80
+ existsSync(join(target, candidate)),
81
+ );
82
+
83
+ if (existingPath !== undefined) {
84
+ console.log(` Skipped ${targetPath} (found existing ${existingPath})`);
47
85
  } else if (renderTemplateFile(frameworkDir, templateName, target, targetPath)) {
48
86
  console.log(` Created ${targetPath}`);
49
87
  }
50
88
  }
51
89
  }
52
90
 
53
- export function formatTaskList(groups: RuleResult[]): string {
91
+ export function formatTaskList(
92
+ groups: RuleResult[],
93
+ context?: TaskListContext,
94
+ ): string {
54
95
  const lines: string[] = [
55
96
  "# Context Tree Init\n",
97
+ ];
98
+
99
+ if (context?.dedicatedTreeRepo) {
100
+ lines.push(
101
+ "This repository is the dedicated Context Tree. Keep decisions, rationale," +
102
+ " cross-domain relationships, and ownership here; keep execution detail" +
103
+ " in your source repositories.",
104
+ "",
105
+ );
106
+ if (context.sourceRepoPath) {
107
+ lines.push(`**Bootstrap source repo:** \`${context.sourceRepoPath}\``, "");
108
+ }
109
+ lines.push(
110
+ "When you publish this tree repo, keep it in the same GitHub organization" +
111
+ " as the source repo unless you have a reason not to.",
112
+ "",
113
+ );
114
+ }
115
+
116
+ lines.push(
56
117
  "**Agent instructions:** Before starting work, analyze the full task list below and" +
57
118
  " identify all information you need from the user. Ask the user for their code" +
58
119
  " repositories or project directories so you can analyze the source yourself —" +
@@ -61,7 +122,7 @@ export function formatTaskList(groups: RuleResult[]): string {
61
122
  ` **${INTERACTIVE_TOOL}** tool with structured options — present selectable choices` +
62
123
  " (with label and description) so the user can pick instead of typing free-form" +
63
124
  ` answers. You may batch up to 4 questions per ${INTERACTIVE_TOOL} call.\n`,
64
- ];
125
+ );
65
126
  for (const group of groups) {
66
127
  lines.push(`## ${group.group}`);
67
128
  for (const task of group.tasks) {
@@ -75,7 +136,9 @@ export function formatTaskList(groups: RuleResult[]): string {
75
136
  );
76
137
  lines.push(`- [ ] \`${FRAMEWORK_VERSION}\` exists`);
77
138
  lines.push("- [ ] Root NODE.md has valid frontmatter (title, owners)");
78
- lines.push("- [ ] AGENT.md exists with framework markers");
139
+ lines.push(
140
+ `- [ ] \`${AGENT_INSTRUCTIONS_FILE}\` is the only agent instructions file and has framework markers`,
141
+ );
79
142
  lines.push("- [ ] `context-tree verify` passes with no errors");
80
143
  lines.push("- [ ] At least one member node exists");
81
144
  lines.push("");
@@ -99,19 +162,44 @@ export function writeProgress(repo: Repo, content: string): void {
99
162
 
100
163
  export interface InitOptions {
101
164
  sourceRoot?: string;
165
+ here?: boolean;
166
+ treeName?: string;
167
+ treePath?: string;
168
+ currentCwd?: string;
169
+ gitInitializer?: (root: string) => void;
102
170
  }
103
171
 
104
172
  export function runInit(repo?: Repo, options?: InitOptions): number {
105
- const r = repo ?? new Repo();
106
-
107
- if (!r.isGitRepo()) {
173
+ const sourceRepo = repo ?? new Repo();
174
+ const initTarget = resolveInitTarget(sourceRepo, options);
175
+ if (initTarget.ok === false) {
108
176
  console.error(
109
- "Error: not a git repository. Initialize one first:\n git init",
177
+ `Error: ${initTarget.message}`,
110
178
  );
111
179
  return 1;
112
180
  }
181
+ const r = initTarget.repo;
182
+ const taskListContext = initTarget.dedicatedTreeRepo
183
+ ? {
184
+ dedicatedTreeRepo: true,
185
+ sourceRepoPath: relativePathFrom(r.root, sourceRepo.root),
186
+ }
187
+ : undefined;
188
+
189
+ if (initTarget.dedicatedTreeRepo) {
190
+ console.log(
191
+ "Recommended workflow: keep the Context Tree in a dedicated repo separate" +
192
+ " from your source/workspace repo.",
193
+ );
194
+ console.log(` Source repo: ${sourceRepo.root}`);
195
+ console.log(` Tree repo: ${r.root}`);
196
+ if (initTarget.createdGitRepo) {
197
+ console.log(" Initialized a new git repo for the tree.");
198
+ }
199
+ console.log();
200
+ }
113
201
 
114
- if (!r.hasFramework()) {
202
+ if (!r.hasCurrentInstalledSkill()) {
115
203
  try {
116
204
  const sourceRoot = options?.sourceRoot ?? resolveBundledPackageRoot();
117
205
  console.log(
@@ -137,9 +225,191 @@ export function runInit(repo?: Repo, options?: InitOptions): number {
137
225
  return 0;
138
226
  }
139
227
 
140
- const output = formatTaskList(groups);
228
+ const output = formatTaskList(groups, taskListContext);
141
229
  console.log(output);
142
230
  writeProgress(r, output);
143
231
  console.log(`Progress file written to ${r.preferredProgressPath()}`);
232
+ if (initTarget.dedicatedTreeRepo) {
233
+ console.log(
234
+ `Continue in ${relativePathFrom(sourceRepo.root, r.root)} and keep your source repos available as additional working directories when you populate the tree.`,
235
+ );
236
+ }
144
237
  return 0;
145
238
  }
239
+
240
+ export interface ParsedInitArgs {
241
+ here?: boolean;
242
+ treeName?: string;
243
+ treePath?: string;
244
+ }
245
+
246
+ export function parseInitArgs(
247
+ args: string[],
248
+ ): ParsedInitArgs | { error: string } {
249
+ const parsed: ParsedInitArgs = {};
250
+
251
+ for (let index = 0; index < args.length; index += 1) {
252
+ const arg = args[index];
253
+ switch (arg) {
254
+ case "--here":
255
+ parsed.here = true;
256
+ break;
257
+ case "--tree-name": {
258
+ const value = args[index + 1];
259
+ if (!value) {
260
+ return { error: "Missing value for --tree-name" };
261
+ }
262
+ parsed.treeName = value;
263
+ index += 1;
264
+ break;
265
+ }
266
+ case "--tree-path": {
267
+ const value = args[index + 1];
268
+ if (!value) {
269
+ return { error: "Missing value for --tree-path" };
270
+ }
271
+ parsed.treePath = value;
272
+ index += 1;
273
+ break;
274
+ }
275
+ default:
276
+ return { error: `Unknown init option: ${arg}` };
277
+ }
278
+ }
279
+
280
+ if (parsed.here && parsed.treeName) {
281
+ return { error: "Cannot combine --here with --tree-name" };
282
+ }
283
+ if (parsed.here && parsed.treePath) {
284
+ return { error: "Cannot combine --here with --tree-path" };
285
+ }
286
+ if (parsed.treeName && parsed.treePath) {
287
+ return { error: "Cannot combine --tree-name with --tree-path" };
288
+ }
289
+
290
+ return parsed;
291
+ }
292
+
293
+ export function runInitCli(args: string[] = []): number {
294
+ if (args.includes("--help") || args.includes("-h")) {
295
+ console.log(INIT_USAGE);
296
+ return 0;
297
+ }
298
+
299
+ const parsed = parseInitArgs(args);
300
+ if ("error" in parsed) {
301
+ console.error(parsed.error);
302
+ console.log(INIT_USAGE);
303
+ return 1;
304
+ }
305
+
306
+ return runInit(undefined, parsed);
307
+ }
308
+
309
+ interface ResolvedInitTarget {
310
+ ok: true;
311
+ createdGitRepo: boolean;
312
+ dedicatedTreeRepo: boolean;
313
+ repo: Repo;
314
+ }
315
+
316
+ interface FailedInitTarget {
317
+ message: string;
318
+ ok: false;
319
+ }
320
+
321
+ function resolveInitTarget(
322
+ sourceRepo: Repo,
323
+ options?: InitOptions,
324
+ ): FailedInitTarget | ResolvedInitTarget {
325
+ if (!sourceRepo.isGitRepo()) {
326
+ return {
327
+ ok: false,
328
+ message:
329
+ "not a git repository. Run this from your source/workspace repo, or create a dedicated tree repo first:\n git init\n context-tree init --here",
330
+ };
331
+ }
332
+
333
+ const targetRoot = determineTargetRoot(sourceRepo, options);
334
+ const dedicatedTreeRepo = targetRoot !== sourceRepo.root;
335
+ let createdGitRepo = false;
336
+ try {
337
+ createdGitRepo = ensureGitRepo(targetRoot, options?.gitInitializer);
338
+ } catch (err) {
339
+ const message = err instanceof Error ? err.message : "unknown error";
340
+ return {
341
+ ok: false,
342
+ message,
343
+ };
344
+ }
345
+
346
+ return {
347
+ ok: true,
348
+ createdGitRepo,
349
+ dedicatedTreeRepo,
350
+ repo: new Repo(targetRoot),
351
+ };
352
+ }
353
+
354
+ function determineTargetRoot(sourceRepo: Repo, options?: InitOptions): string {
355
+ if (options?.treePath) {
356
+ return resolve(options.currentCwd ?? process.cwd(), options.treePath);
357
+ }
358
+
359
+ if (options?.here) {
360
+ return sourceRepo.root;
361
+ }
362
+
363
+ if (options?.treeName) {
364
+ return join(dirname(sourceRepo.root), options.treeName);
365
+ }
366
+
367
+ if (
368
+ sourceRepo.looksLikeTreeRepo()
369
+ || sourceRepo.isLikelyEmptyRepo()
370
+ || !sourceRepo.isLikelySourceRepo()
371
+ ) {
372
+ return sourceRepo.root;
373
+ }
374
+
375
+ return join(dirname(sourceRepo.root), `${sourceRepo.repoName()}-context`);
376
+ }
377
+
378
+ function ensureGitRepo(
379
+ targetRoot: string,
380
+ gitInitializer?: (root: string) => void,
381
+ ): boolean {
382
+ if (existsSync(targetRoot)) {
383
+ if (!statSync(targetRoot).isDirectory()) {
384
+ throw new Error(`Target path is not a directory: ${targetRoot}`);
385
+ }
386
+ if (new Repo(targetRoot).isGitRepo()) {
387
+ return false;
388
+ }
389
+ if (readdirSync(targetRoot).length !== 0) {
390
+ throw new Error(
391
+ `Target path exists and is not a git repository: ${targetRoot}. Run \`git init\` there first or choose a different tree path.`,
392
+ );
393
+ }
394
+ } else {
395
+ mkdirSync(targetRoot, { recursive: true });
396
+ }
397
+
398
+ (gitInitializer ?? defaultGitInitializer)(targetRoot);
399
+ return true;
400
+ }
401
+
402
+ function defaultGitInitializer(root: string): void {
403
+ execFileSync("git", ["init"], {
404
+ cwd: root,
405
+ stdio: "ignore",
406
+ });
407
+ }
408
+
409
+ function relativePathFrom(from: string, to: string): string {
410
+ const rel = relative(from, to);
411
+ if (rel === "") {
412
+ return ".";
413
+ }
414
+ return rel.startsWith("..") ? rel : `./${rel}`;
415
+ }