first-tree 0.0.1 → 0.0.3
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 +73 -39
- package/dist/cli.js +27 -13
- package/dist/help-xEI-s9iN.js +25 -0
- package/dist/init-DtOjj0wc.js +253 -0
- package/dist/installer-rcZpGLnM.js +47 -0
- package/dist/onboarding-6Fr5Gkrk.js +2 -0
- package/dist/onboarding-B9zPGvvG.js +10 -0
- package/dist/repo-BTJG8BU1.js +187 -0
- package/dist/upgrade-COGgI7Rj.js +96 -0
- package/dist/{verify-DIz6qmBX.js → verify-CxN6JiV9.js} +135 -8
- package/package.json +33 -10
- package/skills/first-tree/SKILL.md +109 -0
- package/skills/first-tree/agents/openai.yaml +4 -0
- package/skills/first-tree/assets/framework/VERSION +1 -0
- package/skills/first-tree/assets/framework/examples/claude-code/README.md +14 -0
- package/skills/first-tree/assets/framework/examples/claude-code/settings.json +14 -0
- package/skills/first-tree/assets/framework/helpers/generate-codeowners.ts +224 -0
- package/skills/first-tree/assets/framework/helpers/inject-tree-context.sh +15 -0
- package/skills/first-tree/assets/framework/helpers/run-review.ts +179 -0
- package/skills/first-tree/assets/framework/manifest.json +11 -0
- package/skills/first-tree/assets/framework/prompts/pr-review.md +38 -0
- package/skills/first-tree/assets/framework/templates/agent.md.template +48 -0
- package/skills/first-tree/assets/framework/templates/member-node.md.template +18 -0
- package/skills/first-tree/assets/framework/templates/members-domain.md.template +45 -0
- package/skills/first-tree/assets/framework/templates/root-node.md.template +38 -0
- package/skills/first-tree/assets/framework/workflows/codeowners.yml +31 -0
- package/skills/first-tree/assets/framework/workflows/pr-review.yml +146 -0
- package/skills/first-tree/assets/framework/workflows/validate.yml +19 -0
- package/skills/first-tree/engine/commands/help.ts +32 -0
- package/skills/first-tree/engine/commands/init.ts +1 -0
- package/skills/first-tree/engine/commands/upgrade.ts +1 -0
- package/skills/first-tree/engine/commands/verify.ts +1 -0
- package/skills/first-tree/engine/init.ts +145 -0
- package/skills/first-tree/engine/onboarding.ts +10 -0
- package/skills/first-tree/engine/repo.ts +184 -0
- package/skills/first-tree/engine/rules/agent-instructions.ts +37 -0
- package/skills/first-tree/engine/rules/agent-integration.ts +19 -0
- package/skills/first-tree/engine/rules/ci-validation.ts +72 -0
- package/skills/first-tree/engine/rules/framework.ts +13 -0
- package/skills/first-tree/engine/rules/index.ts +41 -0
- package/skills/first-tree/engine/rules/members.ts +21 -0
- package/skills/first-tree/engine/rules/populate-tree.ts +36 -0
- package/skills/first-tree/engine/rules/root-node.ts +41 -0
- package/skills/first-tree/engine/runtime/adapters.ts +22 -0
- package/skills/first-tree/engine/runtime/asset-loader.ts +134 -0
- package/skills/first-tree/engine/runtime/installer.ts +82 -0
- package/skills/first-tree/engine/runtime/upgrader.ts +23 -0
- package/skills/first-tree/engine/upgrade.ts +176 -0
- package/skills/first-tree/engine/validators/members.ts +215 -0
- package/skills/first-tree/engine/validators/nodes.ts +514 -0
- package/skills/first-tree/engine/verify.ts +97 -0
- package/skills/first-tree/references/about.md +36 -0
- package/skills/first-tree/references/maintainer-architecture.md +59 -0
- package/skills/first-tree/references/maintainer-build-and-distribution.md +56 -0
- package/skills/first-tree/references/maintainer-testing.md +58 -0
- package/skills/first-tree/references/maintainer-thin-cli.md +38 -0
- package/skills/first-tree/references/onboarding.md +162 -0
- package/skills/first-tree/references/ownership-and-naming.md +94 -0
- package/skills/first-tree/references/principles.md +113 -0
- package/skills/first-tree/references/source-map.md +94 -0
- package/skills/first-tree/references/upgrade-contract.md +85 -0
- package/skills/first-tree/scripts/check-skill-sync.sh +133 -0
- package/skills/first-tree/scripts/quick_validate.py +95 -0
- package/skills/first-tree/scripts/run-local-cli.sh +35 -0
- package/skills/first-tree/tests/asset-loader.test.ts +75 -0
- package/skills/first-tree/tests/generate-codeowners.test.ts +94 -0
- package/skills/first-tree/tests/helpers.ts +149 -0
- package/skills/first-tree/tests/init.test.ts +153 -0
- package/skills/first-tree/tests/repo.test.ts +362 -0
- package/skills/first-tree/tests/rules.test.ts +394 -0
- package/skills/first-tree/tests/run-review.test.ts +155 -0
- package/skills/first-tree/tests/skill-artifacts.test.ts +307 -0
- package/skills/first-tree/tests/thin-cli.test.ts +59 -0
- package/skills/first-tree/tests/upgrade.test.ts +89 -0
- package/skills/first-tree/tests/validate-members.test.ts +224 -0
- package/skills/first-tree/tests/validate-nodes.test.ts +198 -0
- package/skills/first-tree/tests/verify.test.ts +142 -0
- package/dist/init-CE_944sb.js +0 -283
- package/dist/repo-BByc3VvM.js +0 -111
- package/dist/upgrade-Chr7z0CY.js +0 -82
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Repo } from "#skill/engine/repo.js";
|
|
2
|
+
import type { RuleResult } from "#skill/engine/rules/index.js";
|
|
3
|
+
import { SKILL_ROOT } from "#skill/engine/runtime/asset-loader.js";
|
|
4
|
+
|
|
5
|
+
export function evaluate(repo: Repo): RuleResult {
|
|
6
|
+
const tasks: string[] = [];
|
|
7
|
+
if (!repo.hasFramework()) {
|
|
8
|
+
tasks.push(
|
|
9
|
+
`\`${SKILL_ROOT}/\` not found — run \`context-tree init\` to install the framework skill bundled with the current \`first-tree\` package`,
|
|
10
|
+
);
|
|
11
|
+
}
|
|
12
|
+
return { group: "Framework", order: 1, tasks };
|
|
13
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { Repo } from "#skill/engine/repo.js";
|
|
2
|
+
import * as agentInstructions from "#skill/engine/rules/agent-instructions.js";
|
|
3
|
+
import * as agentIntegration from "#skill/engine/rules/agent-integration.js";
|
|
4
|
+
import * as ciValidation from "#skill/engine/rules/ci-validation.js";
|
|
5
|
+
import * as framework from "#skill/engine/rules/framework.js";
|
|
6
|
+
import * as members from "#skill/engine/rules/members.js";
|
|
7
|
+
import * as populateTree from "#skill/engine/rules/populate-tree.js";
|
|
8
|
+
import * as rootNode from "#skill/engine/rules/root-node.js";
|
|
9
|
+
|
|
10
|
+
export interface RuleResult {
|
|
11
|
+
group: string;
|
|
12
|
+
order: number;
|
|
13
|
+
tasks: string[];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface Rule {
|
|
17
|
+
evaluate(repo: Repo): RuleResult;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const ALL_RULES: Rule[] = [
|
|
21
|
+
framework,
|
|
22
|
+
rootNode,
|
|
23
|
+
agentInstructions,
|
|
24
|
+
members,
|
|
25
|
+
agentIntegration,
|
|
26
|
+
ciValidation,
|
|
27
|
+
populateTree,
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
export function evaluateAll(repo: Repo): RuleResult[] {
|
|
31
|
+
const results: RuleResult[] = [];
|
|
32
|
+
for (const rule of ALL_RULES) {
|
|
33
|
+
const result = rule.evaluate(repo);
|
|
34
|
+
if (result.tasks.length > 0) {
|
|
35
|
+
results.push(result);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return results.sort((a, b) => a.order - b.order);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export { framework, rootNode, agentInstructions, members, agentIntegration, ciValidation, populateTree };
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { Repo } from "#skill/engine/repo.js";
|
|
2
|
+
import type { RuleResult } from "#skill/engine/rules/index.js";
|
|
3
|
+
|
|
4
|
+
export function evaluate(repo: Repo): RuleResult {
|
|
5
|
+
const tasks: string[] = [];
|
|
6
|
+
if (!repo.pathExists("members")) {
|
|
7
|
+
tasks.push("`members/` directory is missing — create it with a NODE.md");
|
|
8
|
+
} else if (!repo.pathExists("members/NODE.md")) {
|
|
9
|
+
tasks.push("`members/NODE.md` is missing — create it from the template");
|
|
10
|
+
}
|
|
11
|
+
if (repo.hasMembers() && repo.memberCount() === 0) {
|
|
12
|
+
tasks.push(
|
|
13
|
+
"Add at least one member node under `members/`. Analyze the user's code repositories (git history, CODEOWNERS, README contributors) to suggest members, then confirm with the user",
|
|
14
|
+
);
|
|
15
|
+
} else if (!repo.hasMembers()) {
|
|
16
|
+
tasks.push(
|
|
17
|
+
"Add at least one member node under `members/`. Analyze the user's code repositories (git history, CODEOWNERS, README contributors) to suggest members, then confirm with the user",
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
return { group: "Members", order: 4, tasks };
|
|
21
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { Repo } from "#skill/engine/repo.js";
|
|
2
|
+
import type { RuleResult } from "#skill/engine/rules/index.js";
|
|
3
|
+
import { INTERACTIVE_TOOL } from "#skill/engine/init.js";
|
|
4
|
+
|
|
5
|
+
export function evaluate(repo: Repo): RuleResult {
|
|
6
|
+
const tasks: string[] = [];
|
|
7
|
+
|
|
8
|
+
tasks.push(
|
|
9
|
+
`Ask the user whether they want to populate the full context tree now using the **${INTERACTIVE_TOOL}** tool. ` +
|
|
10
|
+
"Present two options: (1) **Yes — populate the full tree**: the agent will analyze source repositories, " +
|
|
11
|
+
"create sub-domains, and populate NODE.md files for each domain and sub-domain; " +
|
|
12
|
+
"(2) **No — I'll do it later**: skip deep population and finish init with just the top-level structure. " +
|
|
13
|
+
"If the user selects No, check off all remaining items in this section and move on.",
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
tasks.push(
|
|
17
|
+
"If the user selected Yes: analyze the codebase (and any additional repositories the user provides) to identify " +
|
|
18
|
+
"logical sub-domains within each top-level domain. For each sub-domain, create a directory with a NODE.md " +
|
|
19
|
+
"containing proper frontmatter (title, owners) and a description of the sub-domain's purpose, boundaries, " +
|
|
20
|
+
"and key decisions. Create deeper sub-domains when a domain is large enough to warrant further decomposition.",
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
tasks.push(
|
|
24
|
+
"Use **sub-tasks** (TaskCreate) to parallelize the population work — create one sub-task per top-level domain " +
|
|
25
|
+
"so each domain can be populated concurrently. Each sub-task should: read the relevant source code, identify " +
|
|
26
|
+
"sub-domains, create NODE.md files, and establish soft_links between related domains.",
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
tasks.push(
|
|
30
|
+
"After all domains are populated, update the root NODE.md to list every top-level domain with a one-line " +
|
|
31
|
+
"description. Ensure all NODE.md files pass `context-tree verify` — valid frontmatter, no placeholders, " +
|
|
32
|
+
"and soft_links that resolve correctly.",
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
return { group: "Populate Tree", order: 7, tasks };
|
|
36
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { Repo } from "#skill/engine/repo.js";
|
|
2
|
+
import type { RuleResult } from "#skill/engine/rules/index.js";
|
|
3
|
+
import { FRAMEWORK_TEMPLATES_DIR } from "#skill/engine/runtime/asset-loader.js";
|
|
4
|
+
|
|
5
|
+
export function evaluate(repo: Repo): RuleResult {
|
|
6
|
+
const tasks: string[] = [];
|
|
7
|
+
if (!repo.pathExists("NODE.md")) {
|
|
8
|
+
tasks.push(
|
|
9
|
+
`NODE.md is missing — create from \`${FRAMEWORK_TEMPLATES_DIR}/root-node.md.template\`. ` +
|
|
10
|
+
"Ask the user for their code repositories or project directories, then analyze the source to determine the project description and domain structure",
|
|
11
|
+
);
|
|
12
|
+
} else {
|
|
13
|
+
const fm = repo.frontmatter("NODE.md");
|
|
14
|
+
if (fm === null) {
|
|
15
|
+
tasks.push(
|
|
16
|
+
"NODE.md exists but has no frontmatter — add frontmatter with title and owners fields",
|
|
17
|
+
);
|
|
18
|
+
} else {
|
|
19
|
+
if (!fm.title || fm.title.startsWith("<")) {
|
|
20
|
+
tasks.push(
|
|
21
|
+
"NODE.md has a placeholder title — replace with your organization name",
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
if (
|
|
25
|
+
!fm.owners ||
|
|
26
|
+
fm.owners.length === 0 ||
|
|
27
|
+
(fm.owners.length === 1 && fm.owners[0].startsWith("<"))
|
|
28
|
+
) {
|
|
29
|
+
tasks.push(
|
|
30
|
+
"NODE.md has placeholder owners — set owners to your GitHub username(s)",
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
if (repo.hasPlaceholderNode()) {
|
|
35
|
+
tasks.push(
|
|
36
|
+
"NODE.md has placeholder content — ask the user for their code repositories or project directories, then analyze the source to fill in the project description and domain structure",
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return { group: "Root Node", order: 2, tasks };
|
|
41
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import {
|
|
3
|
+
FRAMEWORK_EXAMPLES_DIR,
|
|
4
|
+
FRAMEWORK_HELPERS_DIR,
|
|
5
|
+
LEGACY_SKILL_EXAMPLES_DIR,
|
|
6
|
+
LEGACY_EXAMPLES_DIR,
|
|
7
|
+
} from "#skill/engine/runtime/asset-loader.js";
|
|
8
|
+
|
|
9
|
+
export const CLAUDE_SETTINGS_PATH = ".claude/settings.json";
|
|
10
|
+
export const CODEX_CONFIG_PATH = ".codex/config.json";
|
|
11
|
+
|
|
12
|
+
export function claudeCodeExampleCandidates(): string[] {
|
|
13
|
+
return [
|
|
14
|
+
join(FRAMEWORK_EXAMPLES_DIR, "claude-code"),
|
|
15
|
+
join(LEGACY_SKILL_EXAMPLES_DIR, "claude-code"),
|
|
16
|
+
join(LEGACY_EXAMPLES_DIR, "claude-code"),
|
|
17
|
+
];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function injectTreeContextHint(): string {
|
|
21
|
+
return join(FRAMEWORK_HELPERS_DIR, "inject-tree-context.sh");
|
|
22
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
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
|
+
|
|
18
|
+
export const LEGACY_SKILL_NAME = "first-tree-cli-framework";
|
|
19
|
+
export const LEGACY_SKILL_ROOT = join("skills", LEGACY_SKILL_NAME);
|
|
20
|
+
export const LEGACY_SKILL_ASSET_ROOT = join(
|
|
21
|
+
LEGACY_SKILL_ROOT,
|
|
22
|
+
"assets",
|
|
23
|
+
"framework",
|
|
24
|
+
);
|
|
25
|
+
export const LEGACY_SKILL_VERSION = join(LEGACY_SKILL_ASSET_ROOT, "VERSION");
|
|
26
|
+
export const LEGACY_SKILL_TEMPLATES_DIR = join(
|
|
27
|
+
LEGACY_SKILL_ASSET_ROOT,
|
|
28
|
+
"templates",
|
|
29
|
+
);
|
|
30
|
+
export const LEGACY_SKILL_WORKFLOWS_DIR = join(
|
|
31
|
+
LEGACY_SKILL_ASSET_ROOT,
|
|
32
|
+
"workflows",
|
|
33
|
+
);
|
|
34
|
+
export const LEGACY_SKILL_PROMPTS_DIR = join(
|
|
35
|
+
LEGACY_SKILL_ASSET_ROOT,
|
|
36
|
+
"prompts",
|
|
37
|
+
);
|
|
38
|
+
export const LEGACY_SKILL_EXAMPLES_DIR = join(
|
|
39
|
+
LEGACY_SKILL_ASSET_ROOT,
|
|
40
|
+
"examples",
|
|
41
|
+
);
|
|
42
|
+
export const LEGACY_SKILL_PROGRESS = join(LEGACY_SKILL_ROOT, "progress.md");
|
|
43
|
+
|
|
44
|
+
export const LEGACY_FRAMEWORK_ROOT = ".context-tree";
|
|
45
|
+
export const LEGACY_VERSION = join(LEGACY_FRAMEWORK_ROOT, "VERSION");
|
|
46
|
+
export const LEGACY_PROGRESS = join(LEGACY_FRAMEWORK_ROOT, "progress.md");
|
|
47
|
+
export const LEGACY_TEMPLATES_DIR = join(LEGACY_FRAMEWORK_ROOT, "templates");
|
|
48
|
+
export const LEGACY_WORKFLOWS_DIR = join(LEGACY_FRAMEWORK_ROOT, "workflows");
|
|
49
|
+
export const LEGACY_PROMPTS_DIR = join(LEGACY_FRAMEWORK_ROOT, "prompts");
|
|
50
|
+
export const LEGACY_EXAMPLES_DIR = join(LEGACY_FRAMEWORK_ROOT, "examples");
|
|
51
|
+
|
|
52
|
+
export type FrameworkLayout = "skill" | "legacy-skill" | "legacy";
|
|
53
|
+
|
|
54
|
+
function pathExists(root: string, relPath: string): boolean {
|
|
55
|
+
const fullPath = join(root, relPath);
|
|
56
|
+
try {
|
|
57
|
+
return existsSync(fullPath);
|
|
58
|
+
} catch {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function frameworkVersionCandidates(): string[] {
|
|
64
|
+
return [FRAMEWORK_VERSION, LEGACY_SKILL_VERSION, LEGACY_VERSION];
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function progressFileCandidates(): string[] {
|
|
68
|
+
return [INSTALLED_PROGRESS, LEGACY_SKILL_PROGRESS, LEGACY_PROGRESS];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function frameworkTemplateDirCandidates(): string[] {
|
|
72
|
+
return [
|
|
73
|
+
FRAMEWORK_TEMPLATES_DIR,
|
|
74
|
+
LEGACY_SKILL_TEMPLATES_DIR,
|
|
75
|
+
LEGACY_TEMPLATES_DIR,
|
|
76
|
+
];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function frameworkWorkflowDirCandidates(): string[] {
|
|
80
|
+
return [
|
|
81
|
+
FRAMEWORK_WORKFLOWS_DIR,
|
|
82
|
+
LEGACY_SKILL_WORKFLOWS_DIR,
|
|
83
|
+
LEGACY_WORKFLOWS_DIR,
|
|
84
|
+
];
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function frameworkPromptDirCandidates(): string[] {
|
|
88
|
+
return [
|
|
89
|
+
FRAMEWORK_PROMPTS_DIR,
|
|
90
|
+
LEGACY_SKILL_PROMPTS_DIR,
|
|
91
|
+
LEGACY_PROMPTS_DIR,
|
|
92
|
+
];
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function frameworkExampleDirCandidates(): string[] {
|
|
96
|
+
return [
|
|
97
|
+
FRAMEWORK_EXAMPLES_DIR,
|
|
98
|
+
LEGACY_SKILL_EXAMPLES_DIR,
|
|
99
|
+
LEGACY_EXAMPLES_DIR,
|
|
100
|
+
];
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function resolveFirstExistingPath(
|
|
104
|
+
root: string,
|
|
105
|
+
candidates: string[],
|
|
106
|
+
): string | null {
|
|
107
|
+
for (const candidate of candidates) {
|
|
108
|
+
if (pathExists(root, candidate)) {
|
|
109
|
+
return candidate;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function detectFrameworkLayout(root: string): FrameworkLayout | null {
|
|
116
|
+
if (pathExists(root, FRAMEWORK_VERSION)) {
|
|
117
|
+
return "skill";
|
|
118
|
+
}
|
|
119
|
+
if (pathExists(root, LEGACY_SKILL_VERSION)) {
|
|
120
|
+
return "legacy-skill";
|
|
121
|
+
}
|
|
122
|
+
if (pathExists(root, LEGACY_VERSION)) {
|
|
123
|
+
return "legacy";
|
|
124
|
+
}
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function isDirectory(root: string, relPath: string): boolean {
|
|
129
|
+
try {
|
|
130
|
+
return statSync(join(root, relPath)).isDirectory();
|
|
131
|
+
} catch {
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
@@ -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,176 @@
|
|
|
1
|
+
import { mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { Repo } from "#skill/engine/repo.js";
|
|
4
|
+
import {
|
|
5
|
+
FRAMEWORK_WORKFLOWS_DIR,
|
|
6
|
+
FRAMEWORK_TEMPLATES_DIR,
|
|
7
|
+
FRAMEWORK_VERSION,
|
|
8
|
+
INSTALLED_PROGRESS,
|
|
9
|
+
LEGACY_FRAMEWORK_ROOT,
|
|
10
|
+
LEGACY_SKILL_ROOT,
|
|
11
|
+
SKILL_ROOT,
|
|
12
|
+
type FrameworkLayout,
|
|
13
|
+
} from "#skill/engine/runtime/asset-loader.js";
|
|
14
|
+
import {
|
|
15
|
+
copyCanonicalSkill,
|
|
16
|
+
resolveBundledPackageRoot,
|
|
17
|
+
} from "#skill/engine/runtime/installer.js";
|
|
18
|
+
import {
|
|
19
|
+
compareFrameworkVersions,
|
|
20
|
+
readSourceVersion,
|
|
21
|
+
} from "#skill/engine/runtime/upgrader.js";
|
|
22
|
+
|
|
23
|
+
function writeProgress(repo: Repo, content: string): void {
|
|
24
|
+
const progressPath = join(repo.root, repo.preferredProgressPath());
|
|
25
|
+
mkdirSync(dirname(progressPath), { recursive: true });
|
|
26
|
+
writeFileSync(progressPath, content);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function formatUpgradeTaskList(
|
|
30
|
+
repo: Repo,
|
|
31
|
+
localVersion: string,
|
|
32
|
+
packagedVersion: string,
|
|
33
|
+
layout: FrameworkLayout,
|
|
34
|
+
): string {
|
|
35
|
+
const lines: string[] = [
|
|
36
|
+
`# Context Tree Upgrade — v${localVersion} -> v${packagedVersion}\n`,
|
|
37
|
+
"## Installed Skill",
|
|
38
|
+
`- [ ] Review local customizations under \`${SKILL_ROOT}/\` and reapply them if needed`,
|
|
39
|
+
`- [ ] Re-copy any workflow updates you want from \`${FRAMEWORK_WORKFLOWS_DIR}/\` into \`.github/workflows/\``,
|
|
40
|
+
`- [ ] Re-check any local agent setup that references \`${SKILL_ROOT}/assets/framework/examples/\` or \`${SKILL_ROOT}/assets/framework/helpers/\``,
|
|
41
|
+
"",
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
if (layout === "legacy") {
|
|
45
|
+
lines.push(
|
|
46
|
+
"## Migration",
|
|
47
|
+
"- [ ] Remove any stale `.context-tree/` references from repo-specific docs, scripts, or workflow files",
|
|
48
|
+
"",
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (layout === "legacy-skill") {
|
|
53
|
+
lines.push(
|
|
54
|
+
"## Migration",
|
|
55
|
+
`- [ ] Remove any stale \`${LEGACY_SKILL_ROOT}/\` references from repo-specific docs, scripts, workflow files, or agent config`,
|
|
56
|
+
"",
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (repo.hasAgentMdMarkers()) {
|
|
61
|
+
lines.push(
|
|
62
|
+
"## Agent Instructions",
|
|
63
|
+
`- [ ] Compare the framework section in \`AGENT.md\` with \`${FRAMEWORK_TEMPLATES_DIR}/agent.md.template\` and update the content between the markers if needed`,
|
|
64
|
+
"",
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
lines.push(
|
|
69
|
+
"## Verification",
|
|
70
|
+
`- [ ] \`${FRAMEWORK_VERSION}\` reads \`${packagedVersion}\``,
|
|
71
|
+
"- [ ] `context-tree verify` passes",
|
|
72
|
+
"",
|
|
73
|
+
"---",
|
|
74
|
+
"",
|
|
75
|
+
"**Important:** As you complete each task, check it off in" +
|
|
76
|
+
` \`${INSTALLED_PROGRESS}\` by changing \`- [ ]\` to \`- [x]\`.` +
|
|
77
|
+
" Run `context-tree verify` when done — it will fail if any" +
|
|
78
|
+
" items remain unchecked.",
|
|
79
|
+
"",
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
return lines.join("\n");
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export interface UpgradeOptions {
|
|
86
|
+
sourceRoot?: string;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function runUpgrade(repo?: Repo, options?: UpgradeOptions): number {
|
|
90
|
+
const workingRepo = repo ?? new Repo();
|
|
91
|
+
|
|
92
|
+
if (!workingRepo.hasFramework()) {
|
|
93
|
+
console.error(
|
|
94
|
+
"Error: no installed framework skill found. Run `context-tree init` first.",
|
|
95
|
+
);
|
|
96
|
+
return 1;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const layout = workingRepo.frameworkLayout();
|
|
100
|
+
if (layout === null) {
|
|
101
|
+
console.error(
|
|
102
|
+
"Error: no installed framework skill found. Run `context-tree init` first.",
|
|
103
|
+
);
|
|
104
|
+
return 1;
|
|
105
|
+
}
|
|
106
|
+
const localVersion = workingRepo.readVersion() ?? "unknown";
|
|
107
|
+
console.log(`Local framework version: ${localVersion}\n`);
|
|
108
|
+
|
|
109
|
+
console.log(
|
|
110
|
+
"Checking the framework skill bundled with this first-tree package...",
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
let sourceRoot: string;
|
|
114
|
+
try {
|
|
115
|
+
sourceRoot = options?.sourceRoot ?? resolveBundledPackageRoot();
|
|
116
|
+
} catch (err) {
|
|
117
|
+
const message = err instanceof Error ? err.message : "unknown error";
|
|
118
|
+
console.error(`Error: ${message}`);
|
|
119
|
+
return 1;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const packagedVersion = readSourceVersion(sourceRoot);
|
|
123
|
+
if (packagedVersion === null) {
|
|
124
|
+
console.log(
|
|
125
|
+
"Could not read the bundled framework version. Reinstall or update `first-tree` and try again.",
|
|
126
|
+
);
|
|
127
|
+
return 1;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (
|
|
131
|
+
localVersion !== "unknown" &&
|
|
132
|
+
compareFrameworkVersions(localVersion, packagedVersion) > 0
|
|
133
|
+
) {
|
|
134
|
+
console.log(
|
|
135
|
+
"The installed framework is newer than the skill bundled with this `first-tree` package. Install a newer package version before running `context-tree upgrade`.",
|
|
136
|
+
);
|
|
137
|
+
return 1;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (layout === "skill" && packagedVersion === localVersion) {
|
|
141
|
+
console.log(
|
|
142
|
+
`Already up to date with the bundled skill (${FRAMEWORK_VERSION} = ${localVersion}).`,
|
|
143
|
+
);
|
|
144
|
+
return 0;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
copyCanonicalSkill(sourceRoot, workingRepo.root);
|
|
148
|
+
if (layout === "legacy") {
|
|
149
|
+
rmSync(join(workingRepo.root, LEGACY_FRAMEWORK_ROOT), {
|
|
150
|
+
recursive: true,
|
|
151
|
+
force: true,
|
|
152
|
+
});
|
|
153
|
+
console.log(
|
|
154
|
+
"Migrated legacy .context-tree/ layout to skills/first-tree/.",
|
|
155
|
+
);
|
|
156
|
+
} else if (layout === "legacy-skill") {
|
|
157
|
+
console.log(
|
|
158
|
+
"Migrated skills/first-tree-cli-framework/ to skills/first-tree/.",
|
|
159
|
+
);
|
|
160
|
+
} else {
|
|
161
|
+
console.log(
|
|
162
|
+
"Refreshed skills/first-tree/ from the bundled first-tree package.",
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const output = formatUpgradeTaskList(
|
|
167
|
+
workingRepo,
|
|
168
|
+
localVersion,
|
|
169
|
+
packagedVersion,
|
|
170
|
+
layout,
|
|
171
|
+
);
|
|
172
|
+
console.log(`\n${output}`);
|
|
173
|
+
writeProgress(workingRepo, output);
|
|
174
|
+
console.log(`Progress file written to ${workingRepo.preferredProgressPath()}`);
|
|
175
|
+
return 0;
|
|
176
|
+
}
|