@first-tree-ai/context-tree 0.1.5 → 0.1.7-alpha.202609010710
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/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +7 -3
- package/.codex-plugin/plugin.json +16 -9
- package/README.md +150 -117
- package/dist/cli/index.mjs +955 -573
- package/dist/index.d.mts +64 -15
- package/dist/index.mjs +833 -471
- package/dist/{schemas-C4bs-FkC.d.mts → schemas-C_7izpsa.d.mts} +130 -134
- package/dist/{schemas-BWM6Q6iz.mjs → schemas-DKHE1sWt.mjs} +62 -51
- package/dist/schemas.d.mts +2 -2
- package/dist/schemas.mjs +2 -2
- package/docs/specification.md +158 -144
- package/hooks/session-start.mjs +5 -14
- package/package.json +1 -1
- package/policy/context-tree-policy.md +10 -12
- package/skills/context-tree-connect/SKILL.md +34 -0
- package/skills/context-tree-connect/agents/openai.yaml +4 -0
- package/skills/context-tree-create/SKILL.md +32 -0
- package/skills/context-tree-create/agents/openai.yaml +4 -0
- package/skills/context-tree-publish/SKILL.md +26 -0
- package/skills/context-tree-publish/agents/openai.yaml +4 -0
- package/skills/context-tree-publish/scripts/context-tree.mjs +41 -0
- package/skills/context-tree-read/SKILL.md +20 -43
- package/skills/context-tree-read/agents/openai.yaml +2 -2
- package/skills/context-tree-setup/SKILL.md +33 -0
- package/skills/context-tree-setup/agents/openai.yaml +4 -0
- package/skills/context-tree-setup/scripts/context-tree.mjs +41 -0
- package/skills/context-tree-write/SKILL.md +29 -125
- package/skills/context-tree-write/agents/openai.yaml +2 -2
- package/skills/context-tree-init/SKILL.md +0 -51
- package/skills/context-tree-init/agents/openai.yaml +0 -4
- package/skills/context-tree-link/SKILL.md +0 -45
- package/skills/context-tree-link/agents/openai.yaml +0 -4
- /package/skills/{context-tree-init → context-tree-connect}/scripts/context-tree.mjs +0 -0
- /package/skills/{context-tree-link → context-tree-create}/scripts/context-tree.mjs +0 -0
package/dist/cli/index.mjs
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { createRequire } from "node:module";
|
|
3
|
-
import path, { basename, dirname, isAbsolute, join, parse, posix, relative, resolve } from "node:path";
|
|
3
|
+
import path, { basename, dirname, isAbsolute, join, parse, posix, relative, resolve, sep } from "node:path";
|
|
4
4
|
import { EventEmitter } from "node:events";
|
|
5
5
|
import childProcess, { spawnSync } from "node:child_process";
|
|
6
|
-
import fs, { chmodSync, existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, realpathSync, renameSync, statSync, symlinkSync, writeFileSync } from "node:fs";
|
|
6
|
+
import fs, { chmodSync, existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, statSync, symlinkSync, writeFileSync } from "node:fs";
|
|
7
7
|
import process$1 from "node:process";
|
|
8
8
|
import { stripVTControlCharacters } from "node:util";
|
|
9
|
-
import { fileURLToPath } from "node:url";
|
|
10
9
|
import { homedir, tmpdir } from "node:os";
|
|
10
|
+
import { fileURLToPath } from "node:url";
|
|
11
11
|
//#region \0rolldown/runtime.js
|
|
12
12
|
var __defProp = Object.defineProperty;
|
|
13
13
|
var __commonJSMin = (cb, mod) => () => (mod || (cb((mod = { exports: {} }).exports, mod), cb = null), mod.exports);
|
|
@@ -13935,11 +13935,16 @@ const VALIDATION_CODES = {
|
|
|
13935
13935
|
directorySymlinkPathEscape: "TREE_DIRECTORY_SYMLINK_PATH_ESCAPE"
|
|
13936
13936
|
};
|
|
13937
13937
|
const CLI_ERROR_CODES = {
|
|
13938
|
-
|
|
13939
|
-
|
|
13938
|
+
corruptConnection: "CORRUPT_CONNECTION",
|
|
13939
|
+
dirtyTree: "DIRTY_TREE",
|
|
13940
13940
|
failed: "CONTEXT_TREE_FAILED",
|
|
13941
|
-
|
|
13942
|
-
|
|
13941
|
+
githubAuth: "GITHUB_AUTH",
|
|
13942
|
+
invalidTree: "INVALID_TREE",
|
|
13943
|
+
noConnection: "NO_CONNECTION",
|
|
13944
|
+
publishIncomplete: "PUBLISH_INCOMPLETE",
|
|
13945
|
+
repositoryExists: "REPOSITORY_EXISTS",
|
|
13946
|
+
staleConnection: "STALE_CONNECTION",
|
|
13947
|
+
writeOutdated: "WRITE_OUTDATED"
|
|
13943
13948
|
};
|
|
13944
13949
|
function hasUnsafeCharacter(value) {
|
|
13945
13950
|
return [...value].some((character) => {
|
|
@@ -13979,10 +13984,16 @@ const credentialFreeRepositoryUrlSchema = string$2().superRefine((value, context
|
|
|
13979
13984
|
});
|
|
13980
13985
|
}
|
|
13981
13986
|
});
|
|
13987
|
+
const treeNameSchema = string$2().superRefine((value, context) => {
|
|
13988
|
+
if (!/^[A-Za-z\d][A-Za-z\d._-]{0,99}$/u.test(value) || /\.git$/iu.test(value)) context.addIssue({
|
|
13989
|
+
code: "custom",
|
|
13990
|
+
message: "Tree name must be a safe single path segment."
|
|
13991
|
+
});
|
|
13992
|
+
});
|
|
13982
13993
|
const githubRepositoryIdentitySchema = string$2().superRefine((value, context) => {
|
|
13983
13994
|
const parts = value.split("/");
|
|
13984
13995
|
const [owner, name] = parts;
|
|
13985
|
-
if (parts.length !== 2 || owner === void 0 || name === void 0 || !/^[A-Za-z\d](?:[A-Za-z\d-]{0,37}[A-Za-z\d])?$/u.test(owner) ||
|
|
13996
|
+
if (parts.length !== 2 || owner === void 0 || name === void 0 || !/^[A-Za-z\d](?:[A-Za-z\d-]{0,37}[A-Za-z\d])?$/u.test(owner) || !treeNameSchema.safeParse(name).success) context.addIssue({
|
|
13986
13997
|
code: "custom",
|
|
13987
13998
|
message: "Repository must be an explicit GitHub OWNER/REPO identity."
|
|
13988
13999
|
});
|
|
@@ -14047,66 +14058,66 @@ object({
|
|
|
14047
14058
|
schemaVersion: literal(1),
|
|
14048
14059
|
target: string$2()
|
|
14049
14060
|
}).strict();
|
|
14050
|
-
|
|
14061
|
+
object({
|
|
14051
14062
|
findings: array(treeValidationFindingSchema),
|
|
14052
14063
|
ok: boolean(),
|
|
14053
14064
|
root: string$2(),
|
|
14054
14065
|
scannedByContentClass: contextContentClassCountsSchema,
|
|
14055
14066
|
schemaVersion: literal(1)
|
|
14056
14067
|
}).strict();
|
|
14068
|
+
const contextTreeStateSchema = discriminatedUnion("kind", [object({
|
|
14069
|
+
kind: literal("local"),
|
|
14070
|
+
path: absoluteSingleLinePathSchema
|
|
14071
|
+
}).strict(), object({
|
|
14072
|
+
kind: literal("github"),
|
|
14073
|
+
path: absoluteSingleLinePathSchema,
|
|
14074
|
+
repository: githubRepositoryIdentitySchema
|
|
14075
|
+
}).strict()]);
|
|
14076
|
+
const contextTreeConnectionSchema = object({
|
|
14077
|
+
projectPath: absoluteSingleLinePathSchema,
|
|
14078
|
+
tree: contextTreeStateSchema
|
|
14079
|
+
}).strict();
|
|
14057
14080
|
object({
|
|
14058
|
-
files: array(string$2()),
|
|
14059
|
-
root: string$2(),
|
|
14060
14081
|
schemaVersion: literal(1),
|
|
14061
|
-
|
|
14082
|
+
tree: contextTreeStateSchema
|
|
14062
14083
|
}).strict();
|
|
14063
|
-
|
|
14064
|
-
|
|
14065
|
-
|
|
14066
|
-
|
|
14067
|
-
|
|
14068
|
-
|
|
14069
|
-
|
|
14070
|
-
|
|
14071
|
-
|
|
14072
|
-
|
|
14073
|
-
|
|
14074
|
-
}).strict()
|
|
14084
|
+
object({
|
|
14085
|
+
branch: string$2().trim().min(1),
|
|
14086
|
+
commitSha: string$2(),
|
|
14087
|
+
created: boolean(),
|
|
14088
|
+
schemaVersion: literal(1),
|
|
14089
|
+
title: string$2().trim().min(1),
|
|
14090
|
+
treePath: absoluteSingleLinePathSchema
|
|
14091
|
+
}).strict();
|
|
14092
|
+
const managedTreeListingEntrySchema = object({
|
|
14093
|
+
name: treeNameSchema,
|
|
14094
|
+
tree: contextTreeStateSchema
|
|
14075
14095
|
}).strict();
|
|
14076
14096
|
object({
|
|
14077
|
-
|
|
14078
|
-
|
|
14097
|
+
schemaVersion: literal(1),
|
|
14098
|
+
trees: array(managedTreeListingEntrySchema)
|
|
14079
14099
|
}).strict();
|
|
14080
14100
|
object({
|
|
14081
|
-
|
|
14082
|
-
defaultBranch: string$2().trim().min(1),
|
|
14083
|
-
refreshed: boolean(),
|
|
14101
|
+
branch: string$2().trim().min(1),
|
|
14084
14102
|
schemaVersion: literal(1),
|
|
14085
|
-
sha: string$2()
|
|
14103
|
+
sha: string$2(),
|
|
14104
|
+
tree: contextTreeStateSchema
|
|
14105
|
+
}).strict();
|
|
14106
|
+
object({
|
|
14107
|
+
schemaVersion: literal(1),
|
|
14108
|
+
worktreePath: absoluteSingleLinePathSchema
|
|
14086
14109
|
}).strict();
|
|
14087
14110
|
object({
|
|
14088
|
-
|
|
14089
|
-
defaultBranch: string$2().trim().min(1),
|
|
14090
|
-
baseSha: string$2(),
|
|
14111
|
+
branch: string$2().trim().min(1),
|
|
14091
14112
|
schemaVersion: literal(1),
|
|
14092
|
-
|
|
14093
|
-
worktreePath: string$2()
|
|
14113
|
+
sha: string$2()
|
|
14094
14114
|
}).strict();
|
|
14095
14115
|
object({
|
|
14096
|
-
|
|
14097
|
-
|
|
14098
|
-
path: string$2(),
|
|
14099
|
-
status: _enum([
|
|
14100
|
-
"added",
|
|
14101
|
-
"deleted",
|
|
14102
|
-
"modified",
|
|
14103
|
-
"renamed",
|
|
14104
|
-
"untracked"
|
|
14105
|
-
])
|
|
14106
|
-
}).strict()).max(4096),
|
|
14107
|
-
patch: string$2(),
|
|
14116
|
+
branch: string$2().trim().min(1),
|
|
14117
|
+
repository: githubRepositoryIdentitySchema,
|
|
14108
14118
|
schemaVersion: literal(1),
|
|
14109
|
-
|
|
14119
|
+
sha: string$2(),
|
|
14120
|
+
url: credentialFreeRepositoryUrlSchema
|
|
14110
14121
|
}).strict();
|
|
14111
14122
|
object({
|
|
14112
14123
|
error: object({
|
|
@@ -14117,88 +14128,110 @@ object({
|
|
|
14117
14128
|
schemaVersion: literal(1)
|
|
14118
14129
|
}).strict();
|
|
14119
14130
|
//#endregion
|
|
14120
|
-
//#region src/core/internal/
|
|
14121
|
-
|
|
14122
|
-
|
|
14123
|
-
|
|
14124
|
-
|
|
14125
|
-
|
|
14126
|
-
|
|
14127
|
-
|
|
14128
|
-
|
|
14129
|
-
|
|
14130
|
-
|
|
14131
|
-
let host;
|
|
14132
|
-
let path;
|
|
14133
|
-
const scp = /^(?:git@)?([^:]+):(.+)$/u.exec(repositoryUrl);
|
|
14134
|
-
if (scp !== null && !repositoryUrl.includes("://")) {
|
|
14135
|
-
host = scp[1] ?? "";
|
|
14136
|
-
path = scp[2] ?? "";
|
|
14137
|
-
} else {
|
|
14138
|
-
let parsed;
|
|
14139
|
-
try {
|
|
14140
|
-
parsed = new URL(repositoryUrl);
|
|
14141
|
-
} catch {
|
|
14142
|
-
throw new Error("Context Tree origin must be a safe credential-free github.com repository URL.");
|
|
14143
|
-
}
|
|
14144
|
-
if (parsed.password || (parsed.protocol === "http:" || parsed.protocol === "https:") && parsed.username) throw new Error("Context Tree origin must be a safe credential-free github.com repository URL.");
|
|
14145
|
-
host = parsed.hostname;
|
|
14146
|
-
path = parsed.pathname;
|
|
14131
|
+
//#region src/core/internal/errors.ts
|
|
14132
|
+
/**
|
|
14133
|
+
* A failure the CLI reports with a specific machine-readable code. Anything
|
|
14134
|
+
* thrown as a plain Error is reported as CONTEXT_TREE_FAILED instead.
|
|
14135
|
+
*/
|
|
14136
|
+
var ContextTreeError = class extends Error {
|
|
14137
|
+
code;
|
|
14138
|
+
constructor(code, message) {
|
|
14139
|
+
super(message);
|
|
14140
|
+
this.name = "ContextTreeError";
|
|
14141
|
+
this.code = code;
|
|
14147
14142
|
}
|
|
14148
|
-
|
|
14149
|
-
|
|
14150
|
-
|
|
14151
|
-
|
|
14152
|
-
|
|
14153
|
-
|
|
14143
|
+
};
|
|
14144
|
+
//#endregion
|
|
14145
|
+
//#region src/core/internal/git.ts
|
|
14146
|
+
function defaultRunner(command, args) {
|
|
14147
|
+
const result = spawnSync(command, args, {
|
|
14148
|
+
encoding: "utf8",
|
|
14149
|
+
stdio: [
|
|
14150
|
+
"ignore",
|
|
14151
|
+
"pipe",
|
|
14152
|
+
"pipe"
|
|
14153
|
+
]
|
|
14154
|
+
});
|
|
14155
|
+
return {
|
|
14156
|
+
status: result.status,
|
|
14157
|
+
stderr: typeof result.stderr === "string" ? result.stderr : "",
|
|
14158
|
+
stdout: typeof result.stdout === "string" ? result.stdout : ""
|
|
14159
|
+
};
|
|
14160
|
+
}
|
|
14161
|
+
/** A failed Git or `gh` operation. Messages never include the argv. */
|
|
14162
|
+
var CommandError = class extends Error {
|
|
14163
|
+
command;
|
|
14164
|
+
status;
|
|
14165
|
+
stderr;
|
|
14166
|
+
constructor(command, status, stderr, message) {
|
|
14167
|
+
const detail = sanitizeCommandOutput(stderr).trim();
|
|
14168
|
+
super(detail.length > 0 ? `${message}: ${detail}` : message);
|
|
14169
|
+
this.name = "CommandError";
|
|
14170
|
+
this.command = command;
|
|
14171
|
+
this.status = status;
|
|
14172
|
+
this.stderr = detail;
|
|
14154
14173
|
}
|
|
14155
|
-
|
|
14174
|
+
};
|
|
14175
|
+
/** Remove credentials and common access-token shapes before surfacing subprocess output. */
|
|
14176
|
+
function sanitizeCommandOutput(value) {
|
|
14177
|
+
return value.replace(/((?:https?|ssh):\/\/)[^\s/@]+@/giu, "$1<redacted>@").replace(/\b(?:gh[opsu]_[A-Za-z\d_]{20,}|github_pat_[A-Za-z\d_]{20,})\b/gu, "<redacted>").replace(/(authorization\s*:\s*(?:bearer|token)\s+)[^\s]+/giu, "$1<redacted>");
|
|
14156
14178
|
}
|
|
14157
|
-
function
|
|
14158
|
-
|
|
14159
|
-
return `https://github.com/${repository}.git`;
|
|
14179
|
+
function trimOutput(value) {
|
|
14180
|
+
return value.trim();
|
|
14160
14181
|
}
|
|
14161
|
-
|
|
14162
|
-
|
|
14163
|
-
|
|
14164
|
-
|
|
14165
|
-
const manifestPath = join(path, "package.json");
|
|
14166
|
-
if (!existsSync(manifestPath)) return false;
|
|
14167
|
-
try {
|
|
14168
|
-
const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
|
|
14169
|
-
return isRecord(manifest) && manifest.name === PACKAGE_NAME;
|
|
14170
|
-
} catch {
|
|
14171
|
-
return false;
|
|
14172
|
-
}
|
|
14182
|
+
function execute(runner, command, args, message) {
|
|
14183
|
+
const result = runner(command, args);
|
|
14184
|
+
if (result.status !== 0) throw new CommandError(command, result.status, result.stderr, message);
|
|
14185
|
+
return trimOutput(result.stdout);
|
|
14173
14186
|
}
|
|
14174
|
-
|
|
14175
|
-
|
|
14176
|
-
|
|
14177
|
-
while (true) {
|
|
14178
|
-
if (isPackageRoot(candidate)) {
|
|
14179
|
-
const resource = resolve(candidate, ...segments);
|
|
14180
|
-
if (existsSync(resource)) return resource;
|
|
14181
|
-
throw new Error(`Packaged resource is missing: ${segments.join("/")}`);
|
|
14182
|
-
}
|
|
14183
|
-
if (candidate === filesystemRoot) break;
|
|
14184
|
-
candidate = dirname(candidate);
|
|
14185
|
-
}
|
|
14186
|
-
throw new Error(`Package root is missing while resolving: ${segments.join("/")}`);
|
|
14187
|
+
/** Run a Git command that is not scoped by `-C`, such as `git init <path>`. */
|
|
14188
|
+
function gitCommand(args, options = {}) {
|
|
14189
|
+
return execute(options.runner ?? defaultRunner, "git", args, options.message ?? "A Git operation failed.");
|
|
14187
14190
|
}
|
|
14188
|
-
|
|
14189
|
-
|
|
14190
|
-
|
|
14191
|
-
|
|
14191
|
+
/** Run `git -C <root> <args>` and return trimmed stdout, throwing on failure. */
|
|
14192
|
+
function git(root, args, options = {}) {
|
|
14193
|
+
return execute(options.runner ?? defaultRunner, "git", [
|
|
14194
|
+
"-C",
|
|
14195
|
+
root,
|
|
14196
|
+
...args
|
|
14197
|
+
], options.message ?? "A Git operation failed.");
|
|
14192
14198
|
}
|
|
14193
|
-
|
|
14194
|
-
|
|
14195
|
-
|
|
14196
|
-
|
|
14199
|
+
/** Run `git -C <root> <args>` and return trimmed stdout, or undefined on failure. */
|
|
14200
|
+
function optionalGit(root, args, runner = defaultRunner) {
|
|
14201
|
+
const result = runner("git", [
|
|
14202
|
+
"-C",
|
|
14203
|
+
root,
|
|
14204
|
+
...args
|
|
14205
|
+
]);
|
|
14206
|
+
if (result.status !== 0) return void 0;
|
|
14207
|
+
return trimOutput(result.stdout);
|
|
14208
|
+
}
|
|
14209
|
+
/** Run `gh <args>` and return trimmed stdout, throwing on failure. */
|
|
14210
|
+
function gh(args, options = {}) {
|
|
14211
|
+
return execute(options.runner ?? defaultRunner, "gh", args, options.message ?? "A GitHub CLI operation failed.");
|
|
14197
14212
|
}
|
|
14198
14213
|
//#endregion
|
|
14199
|
-
//#region src/core/internal/
|
|
14200
|
-
|
|
14201
|
-
|
|
14214
|
+
//#region src/core/internal/github-repository.ts
|
|
14215
|
+
const ORIGIN_MESSAGE = "Context Tree origin must identify a credential-free GitHub OWNER/REPO repository.";
|
|
14216
|
+
function canonicalGitHubRepositoryUrl(repository) {
|
|
14217
|
+
githubRepositoryIdentitySchema.parse(repository);
|
|
14218
|
+
return `https://github.com/${repository}.git`;
|
|
14219
|
+
}
|
|
14220
|
+
/** Derive OWNER/REPO from a github.com origin, rejecting anything else. */
|
|
14221
|
+
function gitHubRepositoryFromOriginUrl(origin) {
|
|
14222
|
+
if (!credentialFreeRepositoryUrlSchema.safeParse(origin).success) throw new Error(ORIGIN_MESSAGE);
|
|
14223
|
+
let owner;
|
|
14224
|
+
let name;
|
|
14225
|
+
const scp = /^(?:git@)?github\.com:([^/]+)\/(.+)$/iu.exec(origin);
|
|
14226
|
+
if (scp !== null) [, owner, name] = scp;
|
|
14227
|
+
else {
|
|
14228
|
+
const url = URL.parse(origin);
|
|
14229
|
+
if (url === null || url.hostname.toLowerCase() !== "github.com") throw new Error(ORIGIN_MESSAGE);
|
|
14230
|
+
[owner, name] = url.pathname.replace(/^\/+|\/+$/gu, "").split("/");
|
|
14231
|
+
}
|
|
14232
|
+
const repository = `${owner ?? ""}/${(name ?? "").replace(/\.git$/iu, "")}`;
|
|
14233
|
+
if (!githubRepositoryIdentitySchema.safeParse(repository).success) throw new Error(ORIGIN_MESSAGE);
|
|
14234
|
+
return repository;
|
|
14202
14235
|
}
|
|
14203
14236
|
//#endregion
|
|
14204
14237
|
//#region src/core/path.ts
|
|
@@ -14212,10 +14245,42 @@ function resolveTreeRoot(path) {
|
|
|
14212
14245
|
if (entry.isSymbolicLink() || !entry.isDirectory()) throw new Error(`Context Tree root must be a real directory: ${path}`);
|
|
14213
14246
|
return realpathSync(absolute);
|
|
14214
14247
|
}
|
|
14248
|
+
/** Resolve a directory while rejecting symlinks in user-controlled path components. */
|
|
14249
|
+
function realDirectoryWithoutSymlinks(path, label) {
|
|
14250
|
+
const absolute = resolve(path);
|
|
14251
|
+
const parsed = parse(absolute);
|
|
14252
|
+
const parts = absolute.slice(parsed.root.length).split(sep).filter(Boolean);
|
|
14253
|
+
let current = parsed.root;
|
|
14254
|
+
for (const [index, part] of parts.entries()) {
|
|
14255
|
+
current = resolve(current, part);
|
|
14256
|
+
if (lstatSync(current).isSymbolicLink()) if (index === 0) current = realpathSync(current);
|
|
14257
|
+
else throw new Error(`${label} must contain no symlink component.`);
|
|
14258
|
+
}
|
|
14259
|
+
if (!lstatSync(current).isDirectory()) throw new Error(`${label} must be a directory.`);
|
|
14260
|
+
return realpathSync(current);
|
|
14261
|
+
}
|
|
14215
14262
|
function toPosixPath(path) {
|
|
14216
14263
|
return path.replace(/\\/gu, "/");
|
|
14217
14264
|
}
|
|
14218
14265
|
//#endregion
|
|
14266
|
+
//#region src/core/internal/project.ts
|
|
14267
|
+
/**
|
|
14268
|
+
* Projects are identified solely by their canonical local root. A Git
|
|
14269
|
+
* repository without an origin, a non-Git directory, a Git worktree, and a
|
|
14270
|
+
* separate clone are all independent checkouts with their own canonical root.
|
|
14271
|
+
*/
|
|
14272
|
+
function canonicalProjectRoot(path, runner) {
|
|
14273
|
+
const directory = realDirectoryWithoutSymlinks(path, "Project path");
|
|
14274
|
+
const toplevel = optionalGit(directory, ["rev-parse", "--show-toplevel"], runner);
|
|
14275
|
+
if (toplevel === void 0 || toplevel.length === 0) return directory;
|
|
14276
|
+
return realpathSync(toplevel);
|
|
14277
|
+
}
|
|
14278
|
+
//#endregion
|
|
14279
|
+
//#region src/core/internal/filesystem.ts
|
|
14280
|
+
function readUtf8File(path) {
|
|
14281
|
+
return new TextDecoder("utf-8", { fatal: true }).decode(readFileSync(path));
|
|
14282
|
+
}
|
|
14283
|
+
//#endregion
|
|
14219
14284
|
//#region src/core/internal/content-class.ts
|
|
14220
14285
|
const GENERATED_DIRECTORY_NAMES = new Set([
|
|
14221
14286
|
"node_modules",
|
|
@@ -25240,411 +25305,809 @@ function verifyTree(treePath) {
|
|
|
25240
25305
|
};
|
|
25241
25306
|
}
|
|
25242
25307
|
//#endregion
|
|
25243
|
-
//#region src/core/
|
|
25244
|
-
|
|
25245
|
-
|
|
25246
|
-
|
|
25247
|
-
|
|
25248
|
-
|
|
25249
|
-
|
|
25250
|
-
|
|
25251
|
-
|
|
25252
|
-
|
|
25253
|
-
|
|
25254
|
-
|
|
25255
|
-
|
|
25256
|
-
|
|
25257
|
-
|
|
25258
|
-
|
|
25259
|
-
|
|
25260
|
-
|
|
25308
|
+
//#region src/core/internal/tree-state.ts
|
|
25309
|
+
/**
|
|
25310
|
+
* The one tree-state resolver shared by create, connect, sync, publish, and
|
|
25311
|
+
* writes. It validates a checkout exactly once and reports the discriminated
|
|
25312
|
+
* tree state: a local-only tree, or a published tree with its GitHub
|
|
25313
|
+
* OWNER/REPO identity. Resolution never backfills or mutates stored state.
|
|
25314
|
+
*/
|
|
25315
|
+
/** Require a real directory with no symlink component that is an exact Git root. */
|
|
25316
|
+
function exactGitRoot(treePath, runner) {
|
|
25317
|
+
const root = realDirectoryWithoutSymlinks(treePath, "Context Tree path");
|
|
25318
|
+
const toplevel = optionalGit(root, ["rev-parse", "--show-toplevel"], runner);
|
|
25319
|
+
if (toplevel === void 0 || toplevel.length === 0) throw new Error("Context Tree path must be a Git repository.");
|
|
25320
|
+
if (realpathSync(toplevel) !== root) throw new Error("Context Tree path must be the real Git root.");
|
|
25321
|
+
return root;
|
|
25322
|
+
}
|
|
25323
|
+
/** Parse the root NODE.md, refusing symlinked or irregular files. */
|
|
25324
|
+
function parseRootNode(root) {
|
|
25325
|
+
const path = join(root, "NODE.md");
|
|
25326
|
+
const entry = lstatSync(path);
|
|
25327
|
+
if (entry.isSymbolicLink() || !entry.isFile()) throw new Error("Context Tree root NODE.md must be a regular file.");
|
|
25328
|
+
return parseContextTreeRootNode(readUtf8File(path));
|
|
25329
|
+
}
|
|
25330
|
+
/**
|
|
25331
|
+
* Validate a clean checkout without inferring state from mutable Git remotes.
|
|
25332
|
+
* Uncommitted changes and invalid content each get their own code so callers
|
|
25333
|
+
* can tell "commit your edits" apart from "this path is gone".
|
|
25334
|
+
*/
|
|
25335
|
+
function validateTreeCheckout(treePath, runner) {
|
|
25336
|
+
const root = exactGitRoot(treePath, runner);
|
|
25337
|
+
if (git(root, [
|
|
25338
|
+
"status",
|
|
25339
|
+
"--porcelain",
|
|
25340
|
+
"--untracked-files=all"
|
|
25261
25341
|
], {
|
|
25262
|
-
|
|
25263
|
-
|
|
25264
|
-
|
|
25265
|
-
|
|
25266
|
-
|
|
25267
|
-
]
|
|
25268
|
-
});
|
|
25269
|
-
if (result.error !== void 0 || result.status !== 0) return void 0;
|
|
25270
|
-
return result.stdout.replace(/\r?\n$/u, "");
|
|
25342
|
+
message: "Failed to inspect Context Tree cleanliness.",
|
|
25343
|
+
runner
|
|
25344
|
+
}).trim().length !== 0) throw new ContextTreeError(CLI_ERROR_CODES.dirtyTree, `The Context Tree at ${root} has uncommitted changes; commit or discard them.`);
|
|
25345
|
+
if (!verifyTree(root).ok) throw new ContextTreeError(CLI_ERROR_CODES.invalidTree, `The Context Tree at ${root} is invalid; run context-tree verify --tree-path ${root}.`);
|
|
25346
|
+
return root;
|
|
25271
25347
|
}
|
|
25272
|
-
|
|
25273
|
-
|
|
25274
|
-
|
|
25275
|
-
return
|
|
25348
|
+
/** Validate a stored state without reclassifying it from mutable Git remotes. */
|
|
25349
|
+
function validateStoredTreeState(state, runner) {
|
|
25350
|
+
const path = validateTreeCheckout(state.path, runner);
|
|
25351
|
+
return state.kind === "local" ? {
|
|
25352
|
+
kind: "local",
|
|
25353
|
+
path
|
|
25354
|
+
} : {
|
|
25355
|
+
kind: "github",
|
|
25356
|
+
path,
|
|
25357
|
+
repository: state.repository
|
|
25358
|
+
};
|
|
25276
25359
|
}
|
|
25277
|
-
|
|
25360
|
+
//#endregion
|
|
25361
|
+
//#region src/core/connections.ts
|
|
25362
|
+
const connectionsFileSchema = object({
|
|
25363
|
+
connections: array(contextTreeConnectionSchema),
|
|
25364
|
+
schemaVersion: literal(1)
|
|
25365
|
+
}).strict();
|
|
25366
|
+
const DUPLICATE_MESSAGE = "Duplicate Context Tree connection records exist for this project.";
|
|
25367
|
+
const NO_CONNECTION_MESSAGE = "No Context Tree connection exists for this project; run context-tree create or connect.";
|
|
25368
|
+
function realHome() {
|
|
25278
25369
|
try {
|
|
25279
|
-
|
|
25370
|
+
return realpathSync(homedir());
|
|
25280
25371
|
} catch {
|
|
25281
|
-
|
|
25372
|
+
return homedir();
|
|
25282
25373
|
}
|
|
25283
|
-
try {
|
|
25284
|
-
return canonicalGitHubRepositoryUrl(repositoryIdentityFromGitHubUrl(repositoryUrl).toLowerCase());
|
|
25285
|
-
} catch {}
|
|
25286
|
-
const scp = /^(?:([^@]+)@)?([^:]+):(.+)$/u.exec(repositoryUrl);
|
|
25287
|
-
if (scp !== null && !repositoryUrl.includes("://")) return `${scp[1] === void 0 ? "" : `${scp[1].toLowerCase()}@`}${(scp[2] ?? "").toLowerCase()}:${(scp[3] ?? "").replace(/\/+$/gu, "").replace(/\.git$/iu, "")}.git`;
|
|
25288
|
-
const parsed = new URL(repositoryUrl);
|
|
25289
|
-
parsed.hostname = parsed.hostname.toLowerCase();
|
|
25290
|
-
parsed.pathname = `${parsed.pathname.replace(/\/+$/gu, "").replace(/\.git$/iu, "")}.git`;
|
|
25291
|
-
return parsed.toString();
|
|
25292
|
-
}
|
|
25293
|
-
function realDirectory(path) {
|
|
25294
|
-
const absolute = resolve(path);
|
|
25295
|
-
if (!lstatSync(absolute).isDirectory()) throw new Error("Project path must be a directory.");
|
|
25296
|
-
return realpathSync(absolute);
|
|
25297
25374
|
}
|
|
25298
|
-
|
|
25299
|
-
|
|
25300
|
-
|
|
25301
|
-
|
|
25302
|
-
|
|
25303
|
-
|
|
25304
|
-
|
|
25305
|
-
|
|
25306
|
-
|
|
25307
|
-
|
|
25308
|
-
|
|
25309
|
-
|
|
25310
|
-
|
|
25311
|
-
], "Git project must have an origin remote."))
|
|
25312
|
-
};
|
|
25375
|
+
/** Create a managed application directory below the home directory, failing closed on symlinks. */
|
|
25376
|
+
function ensureManagedDirectory(...segments) {
|
|
25377
|
+
let current = realHome();
|
|
25378
|
+
for (const segment of segments) {
|
|
25379
|
+
current = join(current, segment);
|
|
25380
|
+
const entry = lstatSync(current, { throwIfNoEntry: false });
|
|
25381
|
+
if (entry === void 0) {
|
|
25382
|
+
mkdirSync(current, { mode: 448 });
|
|
25383
|
+
continue;
|
|
25384
|
+
}
|
|
25385
|
+
if (entry.isSymbolicLink() || !entry.isDirectory()) throw new Error(`Context Tree managed directory must be a real directory: ${current}`);
|
|
25386
|
+
}
|
|
25387
|
+
return current;
|
|
25313
25388
|
}
|
|
25314
|
-
|
|
25315
|
-
|
|
25389
|
+
/** The managed namespace without creating it; listing must not create an absent directory. */
|
|
25390
|
+
function managedTreesPath() {
|
|
25391
|
+
return join(realHome(), ".context-tree", "trees");
|
|
25316
25392
|
}
|
|
25317
|
-
function
|
|
25318
|
-
return
|
|
25319
|
-
links: [],
|
|
25320
|
-
schemaVersion: 1
|
|
25321
|
-
};
|
|
25393
|
+
function connectionsPath() {
|
|
25394
|
+
return join(realHome(), ".context-tree", "connections.json");
|
|
25322
25395
|
}
|
|
25323
|
-
function
|
|
25324
|
-
|
|
25396
|
+
function managedTreesRoot() {
|
|
25397
|
+
return ensureManagedDirectory(".context-tree", "trees");
|
|
25398
|
+
}
|
|
25399
|
+
function loadConnections(required) {
|
|
25400
|
+
const path = connectionsPath();
|
|
25325
25401
|
if (!existsSync(path)) {
|
|
25326
|
-
if (required) throw new
|
|
25327
|
-
return
|
|
25402
|
+
if (required) throw new ContextTreeError(CLI_ERROR_CODES.noConnection, NO_CONNECTION_MESSAGE);
|
|
25403
|
+
return {
|
|
25404
|
+
connections: [],
|
|
25405
|
+
schemaVersion: 1
|
|
25406
|
+
};
|
|
25328
25407
|
}
|
|
25329
25408
|
try {
|
|
25330
25409
|
const entry = lstatSync(path);
|
|
25331
25410
|
if (entry.isSymbolicLink() || !entry.isFile()) throw new Error("not a regular file");
|
|
25332
|
-
return
|
|
25411
|
+
return connectionsFileSchema.parse(JSON.parse(readFileSync(path, "utf8")));
|
|
25333
25412
|
} catch {
|
|
25334
|
-
throw new
|
|
25413
|
+
throw new ContextTreeError(CLI_ERROR_CODES.corruptConnection, "Context Tree connections are corrupt; remove connections.json and run context-tree connect again.");
|
|
25335
25414
|
}
|
|
25336
25415
|
}
|
|
25337
|
-
function
|
|
25338
|
-
const
|
|
25339
|
-
const
|
|
25340
|
-
|
|
25341
|
-
|
|
25342
|
-
mode: 448
|
|
25343
|
-
});
|
|
25344
|
-
const directoryEntry = lstatSync(directory);
|
|
25345
|
-
if (directoryEntry.isSymbolicLink() || !directoryEntry.isDirectory()) throw new Error("Context Tree links directory must be a real directory.");
|
|
25346
|
-
const temporary = join(directory, `.links-${process.pid}-${Date.now()}.tmp`);
|
|
25347
|
-
writeFileSync(temporary, `${JSON.stringify(contextTreeLinksFileSchema.parse(value), null, 2)}\n`, {
|
|
25416
|
+
function saveConnections(value) {
|
|
25417
|
+
const directory = ensureManagedDirectory(".context-tree");
|
|
25418
|
+
const path = join(directory, "connections.json");
|
|
25419
|
+
const temporary = join(directory, `.connections-${process.pid}-${Date.now()}.tmp`);
|
|
25420
|
+
writeFileSync(temporary, `${JSON.stringify(connectionsFileSchema.parse(value), null, 2)}\n`, {
|
|
25348
25421
|
encoding: "utf8",
|
|
25349
25422
|
flag: "wx",
|
|
25350
25423
|
mode: 384
|
|
25351
25424
|
});
|
|
25352
|
-
|
|
25353
|
-
|
|
25354
|
-
|
|
25355
|
-
|
|
25356
|
-
|
|
25357
|
-
|
|
25358
|
-
const root = realpathSync(absolute);
|
|
25359
|
-
if (!entry.isDirectory() || entry.isSymbolicLink() || absolute !== root) throw new Error("Context Tree checkout path must be a real directory with no symlink component.");
|
|
25360
|
-
if (realpathSync(requireGit$1(root, ["rev-parse", "--show-toplevel"], "Context Tree checkout must be a Git repository.")) !== root) throw new Error("Context Tree checkout must be the real Git root.");
|
|
25361
|
-
return root;
|
|
25425
|
+
try {
|
|
25426
|
+
renameSync(temporary, path);
|
|
25427
|
+
chmodSync(path, 384);
|
|
25428
|
+
} finally {
|
|
25429
|
+
rmSync(temporary, { force: true });
|
|
25430
|
+
}
|
|
25362
25431
|
}
|
|
25363
|
-
|
|
25364
|
-
|
|
25365
|
-
|
|
25366
|
-
|
|
25367
|
-
|
|
25368
|
-
], "Context Tree checkout must have an origin remote."));
|
|
25432
|
+
/** Exactly one record may exist per project; more than one is corruption. */
|
|
25433
|
+
function singleConnection(stored, canonical) {
|
|
25434
|
+
const matches = stored.connections.filter((connection) => connection.projectPath === canonical);
|
|
25435
|
+
if (matches.length > 1) throw new ContextTreeError(CLI_ERROR_CODES.corruptConnection, DUPLICATE_MESSAGE);
|
|
25436
|
+
return matches[0];
|
|
25369
25437
|
}
|
|
25370
|
-
function
|
|
25371
|
-
|
|
25372
|
-
"status",
|
|
25373
|
-
"--porcelain",
|
|
25374
|
-
"--untracked-files=all"
|
|
25375
|
-
], "Failed to inspect Context Tree cleanliness.", true);
|
|
25376
|
-
if (status.length === 0) return;
|
|
25377
|
-
if (mode === "scaffold") {
|
|
25378
|
-
const lines = status.split("\n").sort();
|
|
25379
|
-
if (git$1(root, [
|
|
25380
|
-
"rev-parse",
|
|
25381
|
-
"--verify",
|
|
25382
|
-
"HEAD"
|
|
25383
|
-
]) === void 0 && JSON.stringify(lines) === JSON.stringify([
|
|
25384
|
-
"?? .github/workflows/validate-context-tree.yml",
|
|
25385
|
-
"?? AGENTS.md",
|
|
25386
|
-
"?? CLAUDE.md",
|
|
25387
|
-
"?? NODE.md"
|
|
25388
|
-
])) return;
|
|
25389
|
-
}
|
|
25390
|
-
throw new Error("Context Tree checkout must be clean.");
|
|
25438
|
+
function isManagedName(value) {
|
|
25439
|
+
return treeNameSchema.safeParse(value).success && value === value.toLowerCase();
|
|
25391
25440
|
}
|
|
25392
|
-
function
|
|
25393
|
-
|
|
25394
|
-
|
|
25395
|
-
if (entry.isSymbolicLink() || !entry.isFile()) throw new Error("Context Tree root NODE.md must be a regular file.");
|
|
25396
|
-
return parseContextTreeRootNode(readUtf8File(path));
|
|
25441
|
+
function managedName(value) {
|
|
25442
|
+
if (!isManagedName(value)) throw new Error(`Managed Context Tree names must be safe lowercase path segments: ${value}`);
|
|
25443
|
+
return value;
|
|
25397
25444
|
}
|
|
25398
|
-
|
|
25399
|
-
|
|
25400
|
-
|
|
25401
|
-
|
|
25402
|
-
|
|
25445
|
+
/** List valid, clean managed trees, excluding unsafe or invalid candidates without failing the listing. */
|
|
25446
|
+
function listManagedTrees(runner) {
|
|
25447
|
+
const root = managedTreesPath();
|
|
25448
|
+
if (!existsSync(root)) return {
|
|
25449
|
+
schemaVersion: 1,
|
|
25450
|
+
trees: []
|
|
25451
|
+
};
|
|
25452
|
+
const rootEntry = lstatSync(root);
|
|
25453
|
+
if (rootEntry.isSymbolicLink() || !rootEntry.isDirectory()) throw new Error("Context Tree managed directory must be a real directory.");
|
|
25454
|
+
const trees = [];
|
|
25455
|
+
for (const candidate of readdirSync(root, { withFileTypes: true })) {
|
|
25456
|
+
if (!candidate.isDirectory() || !isManagedName(candidate.name)) continue;
|
|
25457
|
+
try {
|
|
25458
|
+
trees.push({
|
|
25459
|
+
name: candidate.name,
|
|
25460
|
+
tree: classifyCheckout(join(root, candidate.name), runner)
|
|
25461
|
+
});
|
|
25462
|
+
} catch {}
|
|
25463
|
+
}
|
|
25464
|
+
trees.sort((left, right) => left.name.localeCompare(right.name));
|
|
25403
25465
|
return {
|
|
25404
|
-
|
|
25405
|
-
|
|
25466
|
+
schemaVersion: 1,
|
|
25467
|
+
trees
|
|
25406
25468
|
};
|
|
25407
25469
|
}
|
|
25408
|
-
function
|
|
25409
|
-
return
|
|
25470
|
+
function findConnectionRecord(projectPath, runner) {
|
|
25471
|
+
return singleConnection(loadConnections(false), canonicalProjectRoot(projectPath, runner));
|
|
25410
25472
|
}
|
|
25411
|
-
function
|
|
25412
|
-
|
|
25413
|
-
if (
|
|
25414
|
-
return
|
|
25473
|
+
function validateManagedTreeState(tree, runner) {
|
|
25474
|
+
const validated = validateStoredTreeState(tree, runner);
|
|
25475
|
+
if (dirname(validated.path) === managedTreesPath() && !isManagedName(basename(validated.path))) throw new Error(`Managed Context Tree names must be safe lowercase path segments: ${basename(validated.path)}`);
|
|
25476
|
+
return validated;
|
|
25415
25477
|
}
|
|
25416
|
-
function
|
|
25478
|
+
function resolveConnectionRecord(projectPath, runner) {
|
|
25479
|
+
const canonical = canonicalProjectRoot(projectPath, runner);
|
|
25480
|
+
const connection = singleConnection(loadConnections(true), canonical);
|
|
25481
|
+
if (connection === void 0) throw new ContextTreeError(CLI_ERROR_CODES.noConnection, NO_CONNECTION_MESSAGE);
|
|
25417
25482
|
try {
|
|
25418
|
-
const path = exactCheckoutRoot(link.tree.path);
|
|
25419
25483
|
return {
|
|
25420
|
-
|
|
25421
|
-
|
|
25484
|
+
projectPath: connection.projectPath,
|
|
25485
|
+
tree: validateManagedTreeState(connection.tree, runner)
|
|
25422
25486
|
};
|
|
25423
|
-
} catch {
|
|
25424
|
-
|
|
25487
|
+
} catch (error) {
|
|
25488
|
+
if (error instanceof ContextTreeError) throw error;
|
|
25489
|
+
const detail = error instanceof Error ? error.message : "unknown failure";
|
|
25490
|
+
throw new ContextTreeError(CLI_ERROR_CODES.staleConnection, `The connected Context Tree is no longer usable at ${connection.tree.path}; run context-tree connect to point this project at its current location. ${detail}`);
|
|
25425
25491
|
}
|
|
25426
25492
|
}
|
|
25427
|
-
function
|
|
25428
|
-
|
|
25429
|
-
|
|
25430
|
-
|
|
25431
|
-
|
|
25432
|
-
|
|
25433
|
-
|
|
25434
|
-
|
|
25435
|
-
|
|
25436
|
-
|
|
25437
|
-
|
|
25438
|
-
if (live !== void 0 && live.repository.toLowerCase() === previous.tree.repository.toLowerCase()) throw new Error("The existing Context Tree checkout is still live; replacement is allowed only when it is stale.");
|
|
25439
|
-
}
|
|
25440
|
-
}
|
|
25441
|
-
const link = {
|
|
25442
|
-
project,
|
|
25443
|
-
tree
|
|
25493
|
+
function resolveConnection(projectPath, runner) {
|
|
25494
|
+
return {
|
|
25495
|
+
schemaVersion: 1,
|
|
25496
|
+
tree: resolveConnectionRecord(projectPath, runner).tree
|
|
25497
|
+
};
|
|
25498
|
+
}
|
|
25499
|
+
function upsertConnection(connection, runner) {
|
|
25500
|
+
const canonical = canonicalProjectRoot(connection.projectPath, runner);
|
|
25501
|
+
const record = {
|
|
25502
|
+
projectPath: canonical,
|
|
25503
|
+
tree: validateManagedTreeState(contextTreeStateSchema.parse(connection.tree), runner)
|
|
25444
25504
|
};
|
|
25445
|
-
|
|
25446
|
-
|
|
25505
|
+
const stored = loadConnections(false);
|
|
25506
|
+
const previous = singleConnection(stored, canonical);
|
|
25507
|
+
if (previous !== void 0 && JSON.stringify(previous.tree) === JSON.stringify(record.tree)) return {
|
|
25508
|
+
schemaVersion: 1,
|
|
25509
|
+
tree: record.tree
|
|
25510
|
+
};
|
|
25511
|
+
saveConnections({
|
|
25512
|
+
connections: [...stored.connections.filter((candidate) => candidate.projectPath !== canonical), record],
|
|
25447
25513
|
schemaVersion: 1
|
|
25448
25514
|
});
|
|
25449
25515
|
return {
|
|
25450
|
-
|
|
25516
|
+
schemaVersion: 1,
|
|
25517
|
+
tree: record.tree
|
|
25518
|
+
};
|
|
25519
|
+
}
|
|
25520
|
+
function updateConnectionTree(projectPath, tree, runner) {
|
|
25521
|
+
const canonical = canonicalProjectRoot(projectPath, runner);
|
|
25522
|
+
const stored = loadConnections(true);
|
|
25523
|
+
if (singleConnection(stored, canonical) === void 0) throw new ContextTreeError(CLI_ERROR_CODES.noConnection, NO_CONNECTION_MESSAGE);
|
|
25524
|
+
const validatedTree = validateManagedTreeState(contextTreeStateSchema.parse(tree), runner);
|
|
25525
|
+
saveConnections({
|
|
25526
|
+
connections: stored.connections.map((connection) => connection.projectPath === canonical ? {
|
|
25527
|
+
...connection,
|
|
25528
|
+
tree: validatedTree
|
|
25529
|
+
} : connection),
|
|
25451
25530
|
schemaVersion: 1
|
|
25531
|
+
});
|
|
25532
|
+
}
|
|
25533
|
+
/**
|
|
25534
|
+
* Validate an exact checkout and classify it from a safe origin: no origin
|
|
25535
|
+
* is local state, a credential-free GitHub origin is GitHub state, and any
|
|
25536
|
+
* other origin is rejected as unsafe or unsupported.
|
|
25537
|
+
*/
|
|
25538
|
+
function classifyCheckout(path, runner) {
|
|
25539
|
+
const root = validateTreeCheckout(path, runner);
|
|
25540
|
+
const origin = optionalGit(root, [
|
|
25541
|
+
"remote",
|
|
25542
|
+
"get-url",
|
|
25543
|
+
"origin"
|
|
25544
|
+
], runner);
|
|
25545
|
+
if (origin === void 0) return {
|
|
25546
|
+
kind: "local",
|
|
25547
|
+
path: root
|
|
25548
|
+
};
|
|
25549
|
+
return {
|
|
25550
|
+
kind: "github",
|
|
25551
|
+
path: root,
|
|
25552
|
+
repository: gitHubRepositoryFromOriginUrl(origin)
|
|
25452
25553
|
};
|
|
25453
25554
|
}
|
|
25454
|
-
function
|
|
25455
|
-
return
|
|
25555
|
+
function sameRepository(left, right) {
|
|
25556
|
+
return left.toLowerCase() === right.toLowerCase();
|
|
25456
25557
|
}
|
|
25457
|
-
|
|
25458
|
-
|
|
25558
|
+
/** An existing managed directory that must be a real directory, not a symlinked alias. */
|
|
25559
|
+
function realManagedDirectory(name, destination) {
|
|
25560
|
+
const entry = lstatSync(destination);
|
|
25561
|
+
if (entry.isSymbolicLink() || !entry.isDirectory()) throw new Error(`Managed Context Tree name ${name} is occupied by an unsafe destination.`);
|
|
25459
25562
|
}
|
|
25460
|
-
|
|
25461
|
-
|
|
25462
|
-
|
|
25463
|
-
|
|
25464
|
-
|
|
25465
|
-
|
|
25466
|
-
|
|
25563
|
+
/**
|
|
25564
|
+
* Connect by exact managed name or GitHub OWNER/REPO, or attach an exact,
|
|
25565
|
+
* clean, fully valid Git checkout at an explicit disk path in place.
|
|
25566
|
+
*/
|
|
25567
|
+
function connectProject(options, runner) {
|
|
25568
|
+
if ("treePath" in options) {
|
|
25569
|
+
const tree = classifyCheckout(options.treePath, runner);
|
|
25570
|
+
return upsertConnection({
|
|
25571
|
+
projectPath: options.projectPath,
|
|
25572
|
+
tree
|
|
25573
|
+
}, runner);
|
|
25574
|
+
}
|
|
25575
|
+
const treesRoot = managedTreesRoot();
|
|
25576
|
+
if (!options.target.includes("/")) {
|
|
25577
|
+
const name = managedName(options.target);
|
|
25578
|
+
const destination = join(treesRoot, name);
|
|
25579
|
+
if (!existsSync(destination)) throw new Error(`No managed Context Tree named ${name} exists.`);
|
|
25580
|
+
realManagedDirectory(name, destination);
|
|
25581
|
+
const tree = classifyCheckout(destination, runner);
|
|
25582
|
+
return upsertConnection({
|
|
25583
|
+
projectPath: options.projectPath,
|
|
25584
|
+
tree
|
|
25585
|
+
}, runner);
|
|
25586
|
+
}
|
|
25587
|
+
const repository = githubRepositoryIdentitySchema.parse(options.target);
|
|
25588
|
+
const repositoryName = repository.split("/")[1];
|
|
25589
|
+
if (repositoryName === void 0) throw new Error("Repository must be OWNER/REPO.");
|
|
25590
|
+
const name = managedName(repositoryName.toLowerCase());
|
|
25591
|
+
const destination = join(treesRoot, name);
|
|
25592
|
+
if (existsSync(destination)) {
|
|
25593
|
+
realManagedDirectory(name, destination);
|
|
25594
|
+
const tree = classifyCheckout(destination, runner);
|
|
25595
|
+
if (tree.kind !== "github" || !sameRepository(tree.repository, repository)) throw new Error(`Managed Context Tree name ${name} is already used by a different tree.`);
|
|
25596
|
+
return upsertConnection({
|
|
25597
|
+
projectPath: options.projectPath,
|
|
25598
|
+
tree
|
|
25599
|
+
}, runner);
|
|
25600
|
+
}
|
|
25601
|
+
mkdirSync(destination, { mode: 448 });
|
|
25467
25602
|
try {
|
|
25468
|
-
|
|
25469
|
-
|
|
25470
|
-
|
|
25471
|
-
|
|
25472
|
-
|
|
25473
|
-
|
|
25474
|
-
|
|
25475
|
-
|
|
25476
|
-
|
|
25477
|
-
|
|
25478
|
-
|
|
25479
|
-
|
|
25480
|
-
|
|
25481
|
-
|
|
25482
|
-
|
|
25603
|
+
git(treesRoot, [
|
|
25604
|
+
"clone",
|
|
25605
|
+
"--quiet",
|
|
25606
|
+
"--origin",
|
|
25607
|
+
"origin",
|
|
25608
|
+
"--",
|
|
25609
|
+
canonicalGitHubRepositoryUrl(repository),
|
|
25610
|
+
destination
|
|
25611
|
+
], {
|
|
25612
|
+
message: "Cloning the Context Tree repository failed.",
|
|
25613
|
+
runner
|
|
25614
|
+
});
|
|
25615
|
+
const tree = classifyCheckout(destination, runner);
|
|
25616
|
+
if (tree.kind !== "github" || !sameRepository(tree.repository, repository)) throw new Error("The cloned Context Tree origin does not match the requested repository.");
|
|
25617
|
+
return upsertConnection({
|
|
25618
|
+
projectPath: options.projectPath,
|
|
25619
|
+
tree
|
|
25620
|
+
}, runner);
|
|
25483
25621
|
} catch (error) {
|
|
25484
|
-
|
|
25485
|
-
|
|
25622
|
+
rmSync(destination, {
|
|
25623
|
+
force: true,
|
|
25624
|
+
recursive: true
|
|
25625
|
+
});
|
|
25626
|
+
throw error;
|
|
25486
25627
|
}
|
|
25487
25628
|
}
|
|
25488
25629
|
//#endregion
|
|
25489
|
-
//#region src/core/
|
|
25490
|
-
|
|
25491
|
-
|
|
25492
|
-
|
|
25493
|
-
|
|
25494
|
-
|
|
25630
|
+
//#region src/core/internal/packaged-resource.ts
|
|
25631
|
+
const PACKAGE_NAME = "@first-tree-ai/context-tree";
|
|
25632
|
+
function isPackageRoot(path) {
|
|
25633
|
+
const manifestPath = join(path, "package.json");
|
|
25634
|
+
if (!existsSync(manifestPath)) return false;
|
|
25635
|
+
try {
|
|
25636
|
+
const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
|
|
25637
|
+
return isRecord(manifest) && manifest.name === PACKAGE_NAME;
|
|
25638
|
+
} catch {
|
|
25639
|
+
return false;
|
|
25640
|
+
}
|
|
25641
|
+
}
|
|
25642
|
+
function resolvePackagedResource(...segments) {
|
|
25643
|
+
let candidate = dirname(fileURLToPath(import.meta.url));
|
|
25644
|
+
const filesystemRoot = parse(candidate).root;
|
|
25645
|
+
while (true) {
|
|
25646
|
+
if (isPackageRoot(candidate)) {
|
|
25647
|
+
const resource = resolve(candidate, ...segments);
|
|
25648
|
+
if (existsSync(resource)) return resource;
|
|
25649
|
+
throw new Error(`Packaged resource is missing: ${segments.join("/")}`);
|
|
25650
|
+
}
|
|
25651
|
+
if (candidate === filesystemRoot) break;
|
|
25652
|
+
candidate = dirname(candidate);
|
|
25653
|
+
}
|
|
25654
|
+
throw new Error(`Package root is missing while resolving: ${segments.join("/")}`);
|
|
25655
|
+
}
|
|
25656
|
+
function readPackageManifest() {
|
|
25657
|
+
const parsed = JSON.parse(readFileSync(resolvePackagedResource("package.json"), "utf8"));
|
|
25658
|
+
if (!isRecord(parsed)) throw new Error("Package metadata is invalid.");
|
|
25659
|
+
return parsed;
|
|
25660
|
+
}
|
|
25661
|
+
function readPackageVersion() {
|
|
25662
|
+
const manifest = readPackageManifest();
|
|
25663
|
+
if (typeof manifest.version !== "string") throw new Error("Package version is missing or invalid.");
|
|
25664
|
+
return manifest.version;
|
|
25665
|
+
}
|
|
25666
|
+
//#endregion
|
|
25667
|
+
//#region src/core/scaffold.ts
|
|
25668
|
+
function template(name, values) {
|
|
25669
|
+
let result = readFileSync(resolvePackagedResource("templates", name), "utf8");
|
|
25670
|
+
for (const [key, value] of Object.entries(values)) result = result.replaceAll(`{{${key}}}`, value);
|
|
25671
|
+
return result;
|
|
25672
|
+
}
|
|
25673
|
+
/** Templated regular files, written before the CLAUDE.md -> AGENTS.md symlink. */
|
|
25674
|
+
const TEMPLATED_FILES = [
|
|
25675
|
+
["NODE.md", "root-node.md"],
|
|
25676
|
+
["AGENTS.md", "AGENTS.md"],
|
|
25677
|
+
[".github/workflows/validate-context-tree.yml", "validate-context-tree.yml"]
|
|
25678
|
+
];
|
|
25679
|
+
const SCAFFOLD_FILES = [
|
|
25680
|
+
"NODE.md",
|
|
25681
|
+
"AGENTS.md",
|
|
25682
|
+
"CLAUDE.md",
|
|
25683
|
+
".github/workflows/validate-context-tree.yml"
|
|
25684
|
+
];
|
|
25685
|
+
function initializeGitRepository(root, runner) {
|
|
25686
|
+
gitCommand([
|
|
25687
|
+
"init",
|
|
25688
|
+
"--quiet",
|
|
25689
|
+
"--",
|
|
25690
|
+
root
|
|
25495
25691
|
], {
|
|
25496
|
-
|
|
25497
|
-
|
|
25498
|
-
"ignore",
|
|
25499
|
-
"pipe",
|
|
25500
|
-
"ignore"
|
|
25501
|
-
]
|
|
25692
|
+
message: "Failed to initialize Git repository.",
|
|
25693
|
+
runner
|
|
25502
25694
|
});
|
|
25503
|
-
|
|
25504
|
-
return typeof result.stdout === "string" ? result.stdout : "";
|
|
25505
|
-
}
|
|
25506
|
-
function requireGit(root, args, allowEmpty = false) {
|
|
25507
|
-
const output = git(root, args).trim();
|
|
25508
|
-
if (output.length === 0 && !allowEmpty) throw new Error("Unexpected empty Git output.");
|
|
25509
|
-
return output;
|
|
25510
|
-
}
|
|
25511
|
-
function discoverDefaultBranch(root) {
|
|
25512
|
-
const refs = git(root, [
|
|
25513
|
-
"ls-remote",
|
|
25514
|
-
"--symref",
|
|
25515
|
-
"origin",
|
|
25516
|
-
"HEAD"
|
|
25517
|
-
]).split("\n").map((line) => /^ref: refs\/heads\/([^\s\t]+)\tHEAD$/u.exec(line)?.[1]).filter((value) => value !== void 0 && value.length > 0);
|
|
25518
|
-
if (refs.length !== 1) throw new Error("The Context Tree origin must report exactly one live default branch.");
|
|
25519
|
-
return refs[0] ?? "";
|
|
25520
|
-
}
|
|
25521
|
-
function refreshProject(projectPath) {
|
|
25522
|
-
const result = resolveLink(projectPath);
|
|
25523
|
-
const root = result.link.tree.path;
|
|
25524
|
-
const defaultBranch = discoverDefaultBranch(root);
|
|
25525
|
-
if (requireGit(root, [
|
|
25695
|
+
return git(root, [
|
|
25526
25696
|
"symbolic-ref",
|
|
25527
25697
|
"--short",
|
|
25528
25698
|
"HEAD"
|
|
25529
|
-
]
|
|
25530
|
-
|
|
25531
|
-
|
|
25532
|
-
|
|
25533
|
-
|
|
25534
|
-
|
|
25535
|
-
|
|
25536
|
-
|
|
25537
|
-
|
|
25699
|
+
], {
|
|
25700
|
+
message: "Failed to resolve the initial Git branch during repository initialization.",
|
|
25701
|
+
runner
|
|
25702
|
+
});
|
|
25703
|
+
}
|
|
25704
|
+
function commitScaffold(root, runner) {
|
|
25705
|
+
for (const file of SCAFFOLD_FILES) git(root, [
|
|
25706
|
+
"add",
|
|
25707
|
+
"--",
|
|
25708
|
+
file
|
|
25709
|
+
], {
|
|
25710
|
+
message: "Failed to stage the scaffold files.",
|
|
25711
|
+
runner
|
|
25712
|
+
});
|
|
25713
|
+
git(root, [
|
|
25714
|
+
"-c",
|
|
25715
|
+
"user.name=Context Tree",
|
|
25716
|
+
"-c",
|
|
25717
|
+
"user.email=context-tree@localhost",
|
|
25718
|
+
"-c",
|
|
25719
|
+
"commit.gpgsign=false",
|
|
25720
|
+
"commit",
|
|
25721
|
+
"--quiet",
|
|
25722
|
+
"-m",
|
|
25723
|
+
"Initialize Context Tree"
|
|
25724
|
+
], {
|
|
25725
|
+
message: "Failed to commit the scaffold.",
|
|
25726
|
+
runner
|
|
25727
|
+
});
|
|
25728
|
+
return git(root, ["rev-parse", "HEAD"], {
|
|
25729
|
+
message: "Failed to resolve the scaffold commit.",
|
|
25730
|
+
runner
|
|
25731
|
+
});
|
|
25732
|
+
}
|
|
25733
|
+
function scaffoldTree(options) {
|
|
25734
|
+
const name = treeNameSchema.parse(options.name);
|
|
25735
|
+
const root = resolve(options.path);
|
|
25736
|
+
const destination = lstatSync(root, { throwIfNoEntry: false });
|
|
25737
|
+
if (destination !== void 0) {
|
|
25738
|
+
if (destination.isSymbolicLink() || !destination.isDirectory()) throw new Error(`Refusing to scaffold into a symlink or non-directory destination: ${root}`);
|
|
25739
|
+
if (readdirSync(root).length > 0) throw new Error(`Refusing to scaffold into a non-empty directory: ${root}`);
|
|
25740
|
+
}
|
|
25741
|
+
const initialBranch = initializeGitRepository(root, options.runner);
|
|
25742
|
+
const values = {
|
|
25743
|
+
branchJson: JSON.stringify(initialBranch),
|
|
25744
|
+
packageVersion: readPackageVersion(),
|
|
25745
|
+
title: name,
|
|
25746
|
+
titleJson: JSON.stringify(name)
|
|
25747
|
+
};
|
|
25748
|
+
for (const [relativePath, source] of TEMPLATED_FILES) {
|
|
25749
|
+
const path = join(root, relativePath);
|
|
25750
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
25751
|
+
writeFileSync(path, template(source, values), {
|
|
25752
|
+
encoding: "utf8",
|
|
25753
|
+
flag: "wx",
|
|
25754
|
+
mode: 420
|
|
25755
|
+
});
|
|
25756
|
+
}
|
|
25757
|
+
symlinkSync("AGENTS.md", join(root, "CLAUDE.md"), "file");
|
|
25758
|
+
if (!verifyTree(root).ok) throw new Error("Refusing to commit an invalid Context Tree scaffold.");
|
|
25538
25759
|
return {
|
|
25539
|
-
|
|
25540
|
-
|
|
25541
|
-
|
|
25542
|
-
|
|
25543
|
-
|
|
25544
|
-
|
|
25545
|
-
|
|
25546
|
-
|
|
25547
|
-
|
|
25760
|
+
branch: initialBranch,
|
|
25761
|
+
commit: commitScaffold(root, options.runner),
|
|
25762
|
+
root
|
|
25763
|
+
};
|
|
25764
|
+
}
|
|
25765
|
+
//#endregion
|
|
25766
|
+
//#region src/core/create.ts
|
|
25767
|
+
function projectName(canonicalRoot) {
|
|
25768
|
+
const normalized = basename(canonicalRoot).toLowerCase().replace(/[^a-z\d._-]+/gu, "-").replace(/-{2,}/gu, "-").replace(/^[-.]+/u, "").replace(/[-.]+$/u, "").slice(0, 40);
|
|
25769
|
+
return /^[a-z\d]/u.test(normalized) ? normalized : "project";
|
|
25770
|
+
}
|
|
25771
|
+
function existingCreateResult(destination, runner) {
|
|
25772
|
+
return {
|
|
25773
|
+
branch: git(destination, [
|
|
25774
|
+
"symbolic-ref",
|
|
25775
|
+
"--short",
|
|
25776
|
+
"HEAD"
|
|
25777
|
+
], {
|
|
25778
|
+
message: "Failed to resolve the managed tree branch.",
|
|
25779
|
+
runner
|
|
25780
|
+
}),
|
|
25781
|
+
commitSha: git(destination, ["rev-parse", "HEAD"], {
|
|
25782
|
+
message: "Failed to resolve the managed tree commit.",
|
|
25783
|
+
runner
|
|
25784
|
+
}),
|
|
25785
|
+
created: false,
|
|
25548
25786
|
schemaVersion: 1,
|
|
25549
|
-
|
|
25787
|
+
title: parseRootNode(destination).frontmatter.title,
|
|
25788
|
+
treePath: destination
|
|
25550
25789
|
};
|
|
25551
25790
|
}
|
|
25552
|
-
|
|
25553
|
-
|
|
25554
|
-
const
|
|
25555
|
-
|
|
25556
|
-
|
|
25557
|
-
|
|
25558
|
-
|
|
25559
|
-
|
|
25560
|
-
destination
|
|
25561
|
-
|
|
25562
|
-
|
|
25791
|
+
/** Create and connect the project's uniquely named managed local Context Tree. */
|
|
25792
|
+
function createProject(projectPath, runner) {
|
|
25793
|
+
const canonical = canonicalProjectRoot(projectPath, runner);
|
|
25794
|
+
const name = treeNameSchema.parse(`${projectName(canonical)}-context-tree`);
|
|
25795
|
+
const destination = join(managedTreesRoot(), name);
|
|
25796
|
+
const current = findConnectionRecord(canonical, runner);
|
|
25797
|
+
if (current !== void 0 && current.tree.path !== destination) throw new Error(`This project is already connected to a Context Tree at ${current.tree.path}; run context-tree connect ${name} to switch.`);
|
|
25798
|
+
if (existsSync(destination)) {
|
|
25799
|
+
const entry = lstatSync(destination);
|
|
25800
|
+
if (entry.isSymbolicLink() || !entry.isDirectory() || current === void 0) throw new Error(`Managed Context Tree name ${name} is occupied; run context-tree connect ${name}.`);
|
|
25801
|
+
return existingCreateResult(destination, runner);
|
|
25802
|
+
}
|
|
25803
|
+
mkdirSync(destination, { mode: 448 });
|
|
25804
|
+
try {
|
|
25805
|
+
const scaffold = scaffoldTree({
|
|
25806
|
+
name,
|
|
25807
|
+
path: destination,
|
|
25808
|
+
runner
|
|
25809
|
+
});
|
|
25810
|
+
upsertConnection({
|
|
25811
|
+
projectPath: canonical,
|
|
25812
|
+
tree: {
|
|
25813
|
+
kind: "local",
|
|
25814
|
+
path: scaffold.root
|
|
25815
|
+
}
|
|
25816
|
+
}, runner);
|
|
25817
|
+
return {
|
|
25818
|
+
branch: scaffold.branch,
|
|
25819
|
+
commitSha: scaffold.commit,
|
|
25820
|
+
created: true,
|
|
25821
|
+
schemaVersion: 1,
|
|
25822
|
+
title: name,
|
|
25823
|
+
treePath: scaffold.root
|
|
25824
|
+
};
|
|
25825
|
+
} catch (error) {
|
|
25826
|
+
rmSync(destination, {
|
|
25827
|
+
force: true,
|
|
25828
|
+
recursive: true
|
|
25829
|
+
});
|
|
25830
|
+
throw error;
|
|
25831
|
+
}
|
|
25832
|
+
}
|
|
25833
|
+
//#endregion
|
|
25834
|
+
//#region src/core/publish.ts
|
|
25835
|
+
function authenticatedAccount(runner) {
|
|
25836
|
+
let login;
|
|
25837
|
+
try {
|
|
25838
|
+
login = gh([
|
|
25839
|
+
"api",
|
|
25840
|
+
"user",
|
|
25841
|
+
"--jq",
|
|
25842
|
+
".login"
|
|
25843
|
+
], {
|
|
25844
|
+
message: "GitHub account lookup failed.",
|
|
25845
|
+
runner
|
|
25846
|
+
});
|
|
25847
|
+
} catch (error) {
|
|
25848
|
+
if (error instanceof CommandError && /gh auth login|not logged|authentication failed|http 401|bad credentials/iu.test(error.stderr)) throw new ContextTreeError(CLI_ERROR_CODES.githubAuth, "GitHub authentication failed; run gh auth login before publishing.");
|
|
25849
|
+
throw new ContextTreeError(CLI_ERROR_CODES.publishIncomplete, "GitHub account lookup failed; publication did not start and must not be retried automatically.");
|
|
25850
|
+
}
|
|
25851
|
+
if (login.trim().length === 0) throw new ContextTreeError(CLI_ERROR_CODES.publishIncomplete, "GitHub account lookup returned no repository owner.");
|
|
25852
|
+
return login.trim();
|
|
25853
|
+
}
|
|
25854
|
+
function classifyCreationFailure(stderr) {
|
|
25855
|
+
if (/already exists/iu.test(stderr)) return new ContextTreeError(CLI_ERROR_CODES.repositoryExists, "A GitHub repository with this name already exists; choose an explicit OWNER/REPO override.");
|
|
25856
|
+
return new ContextTreeError(CLI_ERROR_CODES.publishIncomplete, "GitHub repository creation has an uncertain or partial result; do not retry automatically.");
|
|
25857
|
+
}
|
|
25858
|
+
/**
|
|
25859
|
+
* Publish a clean, valid local tree as a private GitHub repository. The
|
|
25860
|
+
* default repository name derives from the authenticated account and managed
|
|
25861
|
+
* tree name; OWNER/REPO is accepted only as an explicit override. The initial
|
|
25862
|
+
* publication is one gh repo create operation, and the stored connection is
|
|
25863
|
+
* updated atomically to the published tree state.
|
|
25864
|
+
*/
|
|
25865
|
+
function publishProject(projectPath, options = {}, runner) {
|
|
25866
|
+
const connection = resolveConnectionRecord(projectPath, runner);
|
|
25867
|
+
const root = connection.tree.path;
|
|
25868
|
+
if (connection.tree.kind === "github") throw new ContextTreeError(CLI_ERROR_CODES.failed, `The Context Tree is already published as ${connection.tree.repository}; writes publish new commits automatically.`);
|
|
25869
|
+
if (optionalGit(root, [
|
|
25870
|
+
"remote",
|
|
25871
|
+
"get-url",
|
|
25872
|
+
"origin"
|
|
25873
|
+
], runner) !== void 0) throw new ContextTreeError(CLI_ERROR_CODES.failed, "A local Context Tree must not already have an origin before publication.");
|
|
25874
|
+
const branch = git(root, [
|
|
25875
|
+
"symbolic-ref",
|
|
25876
|
+
"--short",
|
|
25877
|
+
"HEAD"
|
|
25878
|
+
], {
|
|
25879
|
+
message: "Failed to resolve the checked-out branch.",
|
|
25880
|
+
runner
|
|
25881
|
+
});
|
|
25882
|
+
const sha = git(root, ["rev-parse", "HEAD"], {
|
|
25883
|
+
message: "Failed to resolve the Context Tree commit.",
|
|
25884
|
+
runner
|
|
25885
|
+
});
|
|
25886
|
+
const repository = options.repository === void 0 ? `${authenticatedAccount(runner)}/${basename(root)}` : githubRepositoryIdentitySchema.parse(options.repository);
|
|
25887
|
+
const url = canonicalGitHubRepositoryUrl(repository);
|
|
25888
|
+
try {
|
|
25889
|
+
gh([
|
|
25890
|
+
"repo",
|
|
25891
|
+
"create",
|
|
25892
|
+
repository,
|
|
25893
|
+
"--private",
|
|
25894
|
+
"--source",
|
|
25895
|
+
root,
|
|
25896
|
+
"--remote",
|
|
25897
|
+
"origin",
|
|
25898
|
+
"--push"
|
|
25899
|
+
], {
|
|
25900
|
+
message: "GitHub repository creation failed.",
|
|
25901
|
+
runner
|
|
25902
|
+
});
|
|
25903
|
+
} catch (error) {
|
|
25904
|
+
if (error instanceof CommandError) throw classifyCreationFailure(error.stderr);
|
|
25905
|
+
throw new ContextTreeError(CLI_ERROR_CODES.publishIncomplete, "GitHub publication ended with an uncertain result.");
|
|
25906
|
+
}
|
|
25907
|
+
try {
|
|
25908
|
+
updateConnectionTree(connection.projectPath, {
|
|
25909
|
+
kind: "github",
|
|
25910
|
+
path: root,
|
|
25911
|
+
repository
|
|
25912
|
+
}, runner);
|
|
25913
|
+
} catch {
|
|
25914
|
+
throw new ContextTreeError(CLI_ERROR_CODES.publishIncomplete, "The private repository was created, but updating the local connection failed.");
|
|
25915
|
+
}
|
|
25563
25916
|
return {
|
|
25564
|
-
|
|
25565
|
-
|
|
25917
|
+
branch,
|
|
25918
|
+
repository,
|
|
25919
|
+
schemaVersion: 1,
|
|
25920
|
+
sha,
|
|
25921
|
+
url
|
|
25566
25922
|
};
|
|
25567
25923
|
}
|
|
25568
|
-
|
|
25569
|
-
|
|
25570
|
-
|
|
25571
|
-
|
|
25572
|
-
|
|
25573
|
-
|
|
25924
|
+
//#endregion
|
|
25925
|
+
//#region src/core/sync.ts
|
|
25926
|
+
/**
|
|
25927
|
+
* Local trees report their checked-out state without network access. GitHub
|
|
25928
|
+
* trees fast-forward the exact checked-out branch once, then revalidate.
|
|
25929
|
+
*/
|
|
25930
|
+
function syncProject(projectPath, runner) {
|
|
25931
|
+
const connection = resolveConnectionRecord(projectPath, runner);
|
|
25932
|
+
const root = connection.tree.path;
|
|
25933
|
+
const branch = git(root, [
|
|
25574
25934
|
"symbolic-ref",
|
|
25575
25935
|
"--short",
|
|
25576
25936
|
"HEAD"
|
|
25577
|
-
]
|
|
25578
|
-
|
|
25579
|
-
|
|
25580
|
-
|
|
25581
|
-
|
|
25582
|
-
|
|
25583
|
-
|
|
25584
|
-
|
|
25937
|
+
], {
|
|
25938
|
+
message: "Failed to resolve the checked-out branch.",
|
|
25939
|
+
runner
|
|
25940
|
+
});
|
|
25941
|
+
if (connection.tree.kind === "github") {
|
|
25942
|
+
git(root, [
|
|
25943
|
+
"pull",
|
|
25944
|
+
"--ff-only",
|
|
25945
|
+
"origin",
|
|
25946
|
+
branch
|
|
25947
|
+
], {
|
|
25948
|
+
message: "Fast-forwarding the Context Tree failed.",
|
|
25949
|
+
runner
|
|
25950
|
+
});
|
|
25951
|
+
validateStoredTreeState(connection.tree, runner);
|
|
25952
|
+
}
|
|
25585
25953
|
return {
|
|
25586
|
-
|
|
25587
|
-
link: {
|
|
25588
|
-
project: result.link.project,
|
|
25589
|
-
tree: {
|
|
25590
|
-
path: treePath,
|
|
25591
|
-
repository: ownerRepository
|
|
25592
|
-
}
|
|
25593
|
-
},
|
|
25594
|
-
defaultBranch,
|
|
25954
|
+
branch,
|
|
25595
25955
|
schemaVersion: 1,
|
|
25596
|
-
|
|
25597
|
-
|
|
25956
|
+
sha: git(root, ["rev-parse", "HEAD"], {
|
|
25957
|
+
message: "Failed to resolve the Context Tree commit.",
|
|
25958
|
+
runner
|
|
25959
|
+
}),
|
|
25960
|
+
tree: connection.tree
|
|
25598
25961
|
};
|
|
25599
25962
|
}
|
|
25600
|
-
|
|
25601
|
-
|
|
25602
|
-
|
|
25603
|
-
|
|
25604
|
-
|
|
25605
|
-
|
|
25606
|
-
|
|
25607
|
-
const
|
|
25608
|
-
|
|
25609
|
-
|
|
25610
|
-
|
|
25611
|
-
|
|
25612
|
-
|
|
25613
|
-
|
|
25614
|
-
|
|
25615
|
-
|
|
25616
|
-
|
|
25617
|
-
|
|
25618
|
-
|
|
25619
|
-
|
|
25620
|
-
|
|
25963
|
+
//#endregion
|
|
25964
|
+
//#region src/core/write.ts
|
|
25965
|
+
const TASK_BRANCH_PREFIX = "context-tree/write/";
|
|
25966
|
+
/** Synchronize first, then create an isolated task worktree at the exact HEAD. */
|
|
25967
|
+
function prepareContextWrite(projectPath, runner) {
|
|
25968
|
+
const synchronized = syncProject(projectPath, runner);
|
|
25969
|
+
const root = synchronized.tree.path;
|
|
25970
|
+
const destination = mkdtempSync(join(tmpdir(), "context-tree-write-"));
|
|
25971
|
+
const taskBranch = `${TASK_BRANCH_PREFIX}${basename(destination)}`;
|
|
25972
|
+
try {
|
|
25973
|
+
git(root, [
|
|
25974
|
+
"worktree",
|
|
25975
|
+
"add",
|
|
25976
|
+
"--quiet",
|
|
25977
|
+
"-b",
|
|
25978
|
+
taskBranch,
|
|
25979
|
+
destination,
|
|
25980
|
+
synchronized.sha
|
|
25981
|
+
], {
|
|
25982
|
+
message: "Creating the isolated write worktree failed.",
|
|
25983
|
+
runner
|
|
25984
|
+
});
|
|
25985
|
+
return {
|
|
25986
|
+
schemaVersion: 1,
|
|
25987
|
+
worktreePath: realDirectoryWithoutSymlinks(destination, "Write worktree")
|
|
25988
|
+
};
|
|
25989
|
+
} catch (error) {
|
|
25990
|
+
rmSync(destination, {
|
|
25991
|
+
force: true,
|
|
25992
|
+
recursive: true
|
|
25621
25993
|
});
|
|
25994
|
+
throw error;
|
|
25622
25995
|
}
|
|
25623
|
-
|
|
25996
|
+
}
|
|
25997
|
+
/** Commit every pending change, then fast-forward locally or push once. */
|
|
25998
|
+
function finishContextWrite(options, runner) {
|
|
25999
|
+
const connection = resolveConnectionRecord(options.projectPath, runner);
|
|
26000
|
+
const root = connection.tree.path;
|
|
26001
|
+
const { taskBranch, worktreePath } = validatePreparedWorktree(root, options.worktreePath, runner);
|
|
26002
|
+
const branch = git(root, [
|
|
26003
|
+
"symbolic-ref",
|
|
26004
|
+
"--short",
|
|
26005
|
+
"HEAD"
|
|
26006
|
+
], {
|
|
26007
|
+
message: "Failed to resolve the connected checkout branch.",
|
|
26008
|
+
runner
|
|
26009
|
+
});
|
|
26010
|
+
if (git(worktreePath, [
|
|
25624
26011
|
"status",
|
|
25625
26012
|
"--porcelain",
|
|
25626
26013
|
"--untracked-files=all"
|
|
25627
|
-
]
|
|
25628
|
-
|
|
25629
|
-
|
|
25630
|
-
|
|
25631
|
-
|
|
25632
|
-
|
|
26014
|
+
], {
|
|
26015
|
+
message: "Failed to inspect the prepared worktree.",
|
|
26016
|
+
runner
|
|
26017
|
+
}).length === 0) throw new Error("The prepared worktree has no pending changes.");
|
|
26018
|
+
if (!verifyTree(worktreePath).ok) throw new ContextTreeError(CLI_ERROR_CODES.invalidTree, `Refusing to commit an invalid Context Tree; run context-tree verify --tree-path ${worktreePath}.`);
|
|
26019
|
+
git(worktreePath, ["add", "--all"], {
|
|
26020
|
+
message: "Staging the Context Tree changes failed.",
|
|
26021
|
+
runner
|
|
26022
|
+
});
|
|
26023
|
+
git(worktreePath, [
|
|
26024
|
+
"-c",
|
|
26025
|
+
"commit.gpgsign=false",
|
|
26026
|
+
"commit",
|
|
26027
|
+
"--quiet",
|
|
26028
|
+
"-m",
|
|
26029
|
+
options.message
|
|
26030
|
+
], {
|
|
26031
|
+
message: "Committing the Context Tree changes failed.",
|
|
26032
|
+
runner
|
|
26033
|
+
});
|
|
26034
|
+
const sha = git(worktreePath, ["rev-parse", "HEAD"], {
|
|
26035
|
+
message: "Failed to resolve the write commit.",
|
|
26036
|
+
runner
|
|
26037
|
+
});
|
|
26038
|
+
try {
|
|
26039
|
+
if (connection.tree.kind === "local") git(root, [
|
|
26040
|
+
"merge",
|
|
26041
|
+
"--ff-only",
|
|
26042
|
+
taskBranch
|
|
26043
|
+
], {
|
|
26044
|
+
message: "Fast-forwarding the local Context Tree failed.",
|
|
26045
|
+
runner
|
|
26046
|
+
});
|
|
26047
|
+
else git(worktreePath, [
|
|
26048
|
+
"push",
|
|
26049
|
+
"origin",
|
|
26050
|
+
`HEAD:refs/heads/${branch}`
|
|
26051
|
+
], {
|
|
26052
|
+
message: "Publishing the Context Tree write failed.",
|
|
26053
|
+
runner
|
|
25633
26054
|
});
|
|
26055
|
+
} catch (error) {
|
|
26056
|
+
if (isNonFastForward(error)) throw new ContextTreeError(CLI_ERROR_CODES.writeOutdated, `The Context Tree advanced; the prepared worktree is preserved at ${worktreePath}.`);
|
|
26057
|
+
throw error;
|
|
25634
26058
|
}
|
|
25635
|
-
|
|
25636
|
-
}
|
|
25637
|
-
function inspectContextTreeDiff(treePath, base) {
|
|
25638
|
-
const root = realpathSync(treePath);
|
|
25639
|
-
const reference = base ?? "HEAD";
|
|
26059
|
+
removeWorktree(root, worktreePath, taskBranch, runner);
|
|
25640
26060
|
return {
|
|
25641
|
-
|
|
25642
|
-
files: changedFiles(root, reference),
|
|
25643
|
-
patch: git(root, ["diff", reference]),
|
|
26061
|
+
branch,
|
|
25644
26062
|
schemaVersion: 1,
|
|
25645
|
-
|
|
26063
|
+
sha
|
|
26064
|
+
};
|
|
26065
|
+
}
|
|
26066
|
+
function gitCommonDirectory(root, runner) {
|
|
26067
|
+
const value = git(root, ["rev-parse", "--git-common-dir"], {
|
|
26068
|
+
message: "Failed to resolve the Git common directory.",
|
|
26069
|
+
runner
|
|
26070
|
+
});
|
|
26071
|
+
return realDirectoryWithoutSymlinks(isAbsolute(value) ? value : resolve(root, value), "Git common directory");
|
|
26072
|
+
}
|
|
26073
|
+
function validatePreparedWorktree(root, suppliedPath, runner) {
|
|
26074
|
+
const worktreePath = realDirectoryWithoutSymlinks(suppliedPath, "Prepared worktree");
|
|
26075
|
+
if (gitCommonDirectory(worktreePath, runner) !== gitCommonDirectory(root, runner)) throw new Error("The prepared worktree does not belong to the connected Context Tree.");
|
|
26076
|
+
const taskBranch = git(worktreePath, [
|
|
26077
|
+
"symbolic-ref",
|
|
26078
|
+
"--short",
|
|
26079
|
+
"HEAD"
|
|
26080
|
+
], {
|
|
26081
|
+
message: "Failed to resolve the worktree branch.",
|
|
26082
|
+
runner
|
|
26083
|
+
});
|
|
26084
|
+
if (!taskBranch.startsWith(TASK_BRANCH_PREFIX)) throw new Error("The prepared worktree is not on a reserved Context Tree write branch.");
|
|
26085
|
+
return {
|
|
26086
|
+
taskBranch,
|
|
26087
|
+
worktreePath
|
|
25646
26088
|
};
|
|
25647
26089
|
}
|
|
26090
|
+
function isNonFastForward(error) {
|
|
26091
|
+
return error instanceof CommandError && /non-fast-forward|fetch first|tip of your current branch is behind|not possible to fast-forward|diverg/i.test(error.stderr);
|
|
26092
|
+
}
|
|
26093
|
+
function removeWorktree(root, worktreePath, taskBranch, runner) {
|
|
26094
|
+
git(root, [
|
|
26095
|
+
"worktree",
|
|
26096
|
+
"remove",
|
|
26097
|
+
worktreePath
|
|
26098
|
+
], {
|
|
26099
|
+
message: "Removing the write worktree failed.",
|
|
26100
|
+
runner
|
|
26101
|
+
});
|
|
26102
|
+
git(root, [
|
|
26103
|
+
"branch",
|
|
26104
|
+
"-D",
|
|
26105
|
+
taskBranch
|
|
26106
|
+
], {
|
|
26107
|
+
message: "Deleting the write branch failed.",
|
|
26108
|
+
runner
|
|
26109
|
+
});
|
|
26110
|
+
}
|
|
25648
26111
|
//#endregion
|
|
25649
26112
|
//#region src/core/policy.ts
|
|
25650
26113
|
function readContextTreePolicy() {
|
|
@@ -25724,99 +26187,6 @@ function readTree(treePath, path) {
|
|
|
25724
26187
|
};
|
|
25725
26188
|
}
|
|
25726
26189
|
//#endregion
|
|
25727
|
-
//#region src/core/scaffold.ts
|
|
25728
|
-
function template(name, values) {
|
|
25729
|
-
let result = readFileSync(resolvePackagedResource("templates", name), "utf8");
|
|
25730
|
-
for (const [key, value] of Object.entries(values)) result = result.replaceAll(`{{${key}}}`, value);
|
|
25731
|
-
return result;
|
|
25732
|
-
}
|
|
25733
|
-
function initializeGitRepository(root, repository) {
|
|
25734
|
-
const initialized = spawnSync("git", [
|
|
25735
|
-
"init",
|
|
25736
|
-
"--quiet",
|
|
25737
|
-
root
|
|
25738
|
-
], { stdio: "ignore" });
|
|
25739
|
-
if (initialized.error !== void 0 || initialized.status !== 0) throw new Error("Failed to initialize Git repository.");
|
|
25740
|
-
const branch = spawnSync("git", [
|
|
25741
|
-
"-C",
|
|
25742
|
-
root,
|
|
25743
|
-
"symbolic-ref",
|
|
25744
|
-
"--short",
|
|
25745
|
-
"HEAD"
|
|
25746
|
-
], {
|
|
25747
|
-
encoding: "utf8",
|
|
25748
|
-
stdio: [
|
|
25749
|
-
"ignore",
|
|
25750
|
-
"pipe",
|
|
25751
|
-
"ignore"
|
|
25752
|
-
]
|
|
25753
|
-
});
|
|
25754
|
-
const name = branch.stdout.replace(/\r?\n$/u, "");
|
|
25755
|
-
if (branch.error !== void 0 || branch.status !== 0 || name.length === 0) throw new Error("Failed to resolve the initial Git branch during repository initialization.");
|
|
25756
|
-
const remote = spawnSync("git", [
|
|
25757
|
-
"-C",
|
|
25758
|
-
root,
|
|
25759
|
-
"remote",
|
|
25760
|
-
"add",
|
|
25761
|
-
"origin",
|
|
25762
|
-
canonicalGitHubRepositoryUrl(repository)
|
|
25763
|
-
], { stdio: "ignore" });
|
|
25764
|
-
if (remote.error !== void 0 || remote.status !== 0) throw new Error("Failed to configure the credential-free Context Tree origin.");
|
|
25765
|
-
return name;
|
|
25766
|
-
}
|
|
25767
|
-
function scaffoldTree(options) {
|
|
25768
|
-
const title = parseGitHubRepositoryIdentity(options.repository);
|
|
25769
|
-
const root = resolve(options.path);
|
|
25770
|
-
const destination = lstatSync(root, { throwIfNoEntry: false });
|
|
25771
|
-
if (destination !== void 0) {
|
|
25772
|
-
if (destination.isSymbolicLink() || !destination.isDirectory()) throw new Error(`Refusing to scaffold into a symlink or non-directory destination: ${root}`);
|
|
25773
|
-
if (readdirSync(root).length > 0) throw new Error(`Refusing to scaffold into a non-empty directory: ${root}`);
|
|
25774
|
-
}
|
|
25775
|
-
const initialBranch = initializeGitRepository(root, options.repository);
|
|
25776
|
-
const values = {
|
|
25777
|
-
branchJson: JSON.stringify(initialBranch),
|
|
25778
|
-
packageVersion: readPackageVersion(),
|
|
25779
|
-
title,
|
|
25780
|
-
titleJson: JSON.stringify(title)
|
|
25781
|
-
};
|
|
25782
|
-
const regularFiles = [
|
|
25783
|
-
["NODE.md", "root-node.md"],
|
|
25784
|
-
["AGENTS.md", "AGENTS.md"],
|
|
25785
|
-
[".github/workflows/validate-context-tree.yml", "validate-context-tree.yml"]
|
|
25786
|
-
];
|
|
25787
|
-
const files = [
|
|
25788
|
-
"NODE.md",
|
|
25789
|
-
"AGENTS.md",
|
|
25790
|
-
"CLAUDE.md",
|
|
25791
|
-
".github/workflows/validate-context-tree.yml"
|
|
25792
|
-
];
|
|
25793
|
-
for (const [relativePath, source] of regularFiles.slice(0, 2)) {
|
|
25794
|
-
const path = join(root, relativePath);
|
|
25795
|
-
mkdirSync(dirname(path), { recursive: true });
|
|
25796
|
-
writeFileSync(path, template(source, values), {
|
|
25797
|
-
encoding: "utf8",
|
|
25798
|
-
flag: "wx",
|
|
25799
|
-
mode: 420
|
|
25800
|
-
});
|
|
25801
|
-
}
|
|
25802
|
-
symlinkSync("AGENTS.md", join(root, "CLAUDE.md"), "file");
|
|
25803
|
-
for (const [relativePath, source] of regularFiles.slice(2)) {
|
|
25804
|
-
const path = join(root, relativePath);
|
|
25805
|
-
mkdirSync(dirname(path), { recursive: true });
|
|
25806
|
-
writeFileSync(path, template(source, values), {
|
|
25807
|
-
encoding: "utf8",
|
|
25808
|
-
flag: "wx",
|
|
25809
|
-
mode: 420
|
|
25810
|
-
});
|
|
25811
|
-
}
|
|
25812
|
-
return {
|
|
25813
|
-
files,
|
|
25814
|
-
root,
|
|
25815
|
-
schemaVersion: 1,
|
|
25816
|
-
verification: verifyTree(root)
|
|
25817
|
-
};
|
|
25818
|
-
}
|
|
25819
|
-
//#endregion
|
|
25820
26190
|
//#region src/cli/api.ts
|
|
25821
26191
|
const defaultIo = {
|
|
25822
26192
|
cwd: () => process.cwd(),
|
|
@@ -25826,57 +26196,69 @@ function line(io, value) {
|
|
|
25826
26196
|
io.stdout(`${value}\n`);
|
|
25827
26197
|
}
|
|
25828
26198
|
function createContextTreeCli(io = defaultIo) {
|
|
25829
|
-
const program = new Command().name("context-tree").description("
|
|
26199
|
+
const program = new Command().name("context-tree").description("Create, connect, list, read, write, and publish Context Trees.").addHelpCommand(false).version(readPackageVersion()).exitOverride().configureOutput({
|
|
25830
26200
|
writeErr: () => void 0,
|
|
25831
26201
|
writeOut: io.stdout
|
|
25832
26202
|
});
|
|
25833
|
-
program.command("
|
|
25834
|
-
line(io, JSON.stringify(
|
|
26203
|
+
program.command("create").description("Create and connect one uniquely named managed Context Tree for the current project.").option("--project-path <path>", "project directory", ".").action((options) => {
|
|
26204
|
+
line(io, JSON.stringify(createProject(resolve(io.cwd(), options.projectPath))));
|
|
25835
26205
|
});
|
|
25836
|
-
program.command("
|
|
25837
|
-
|
|
26206
|
+
program.command("connect").description("Connect the project by managed tree name, GitHub OWNER/REPO, or exact disk path.").argument("[name-or-repository]", "managed tree name or GitHub OWNER/REPO").option("--project-path <path>", "project directory", ".").option("--tree-path <path>", "exact Context Tree Git root to connect in place").action((target, options) => {
|
|
26207
|
+
const projectPath = resolve(io.cwd(), options.projectPath);
|
|
26208
|
+
if (target !== void 0 && options.treePath !== void 0) throw new Error("Connect requires exactly one of a name/repository or --tree-path.");
|
|
26209
|
+
if (target !== void 0) {
|
|
26210
|
+
line(io, JSON.stringify(connectProject({
|
|
26211
|
+
projectPath,
|
|
26212
|
+
target
|
|
26213
|
+
})));
|
|
26214
|
+
return;
|
|
26215
|
+
}
|
|
26216
|
+
if (options.treePath !== void 0) {
|
|
26217
|
+
line(io, JSON.stringify(connectProject({
|
|
26218
|
+
projectPath,
|
|
26219
|
+
treePath: resolve(io.cwd(), options.treePath)
|
|
26220
|
+
})));
|
|
26221
|
+
return;
|
|
26222
|
+
}
|
|
26223
|
+
throw new Error("Connect requires a managed tree name, GitHub OWNER/REPO, or --tree-path.");
|
|
25838
26224
|
});
|
|
25839
|
-
program.command("
|
|
25840
|
-
line(io, JSON.stringify(
|
|
26225
|
+
program.command("list").description("List valid clean managed Context Trees.").action(() => {
|
|
26226
|
+
line(io, JSON.stringify(listManagedTrees()));
|
|
25841
26227
|
});
|
|
25842
|
-
program.command("
|
|
25843
|
-
line(io, JSON.stringify(
|
|
26228
|
+
program.command("resolve").description("Resolve the connected Context Tree for a project.").option("--project-path <path>", "project directory", ".").action((options) => {
|
|
26229
|
+
line(io, JSON.stringify(resolveConnection(resolve(io.cwd(), options.projectPath))));
|
|
25844
26230
|
});
|
|
25845
|
-
program.command("
|
|
25846
|
-
line(io, JSON.stringify(
|
|
26231
|
+
program.command("sync").description("Synchronize the connected Context Tree for a project.").option("--project-path <path>", "project directory", ".").action((options) => {
|
|
26232
|
+
line(io, JSON.stringify(syncProject(resolve(io.cwd(), options.projectPath))));
|
|
25847
26233
|
});
|
|
25848
|
-
program.command("
|
|
25849
|
-
line(io, JSON.stringify(
|
|
26234
|
+
program.command("prepare-write").description("Prepare an isolated Context Tree worktree for a write.").option("--project-path <path>", "project directory", ".").action((options) => {
|
|
26235
|
+
line(io, JSON.stringify(prepareContextWrite(resolve(io.cwd(), options.projectPath))));
|
|
25850
26236
|
});
|
|
25851
|
-
program.command("
|
|
25852
|
-
|
|
25853
|
-
|
|
25854
|
-
|
|
25855
|
-
|
|
25856
|
-
|
|
25857
|
-
|
|
25858
|
-
|
|
25859
|
-
|
|
25860
|
-
repository: options.repository
|
|
25861
|
-
});
|
|
25862
|
-
if (project !== void 0) linkScaffoldedProject(projectPath, result.root);
|
|
25863
|
-
line(io, JSON.stringify(result));
|
|
26237
|
+
program.command("finish-write").description("Commit all pending changes in a prepared worktree and publish them.").requiredOption("--worktree-path <path>", "prepared worktree path").requiredOption("--message <message>", "commit message for the pending changes").option("--project-path <path>", "project directory", ".").action((options) => {
|
|
26238
|
+
line(io, JSON.stringify(finishContextWrite({
|
|
26239
|
+
message: options.message,
|
|
26240
|
+
projectPath: resolve(io.cwd(), options.projectPath),
|
|
26241
|
+
worktreePath: resolve(io.cwd(), options.worktreePath)
|
|
26242
|
+
})));
|
|
26243
|
+
});
|
|
26244
|
+
program.command("publish").description("Publish the local tree as a new private GitHub repository.").argument("[repository]", "GitHub OWNER/REPO override; defaults to the authenticated account and tree name").option("--project-path <path>", "project directory", ".").action((repository, options) => {
|
|
26245
|
+
line(io, JSON.stringify(publishProject(resolve(io.cwd(), options.projectPath), { repository })));
|
|
25864
26246
|
});
|
|
25865
26247
|
program.command("read").description("Read an indexed Context Tree directory or Markdown leaf.").argument("[path]", "tree-relative path", ".").option("--tree-path <path>", "Context Tree root", ".").action((path, options) => {
|
|
25866
|
-
|
|
25867
|
-
|
|
25868
|
-
line(io, JSON.stringify(
|
|
26248
|
+
const treePath = resolve(io.cwd(), options.treePath);
|
|
26249
|
+
if (!verifyTree(treePath).ok) throw new ContextTreeError(CLI_ERROR_CODES.invalidTree, `Refusing to read an invalid Context Tree; run context-tree verify --tree-path ${treePath}.`);
|
|
26250
|
+
line(io, JSON.stringify(readTree(treePath, path)));
|
|
25869
26251
|
});
|
|
25870
26252
|
program.command("verify").description("Validate Context Tree structure and safety.").option("--tree-path <path>", "Context Tree root", ".").action((options) => {
|
|
25871
26253
|
const result = verifyTree(resolve(io.cwd(), options.treePath));
|
|
25872
26254
|
line(io, JSON.stringify(result));
|
|
25873
26255
|
if (!result.ok) process.exitCode = 1;
|
|
25874
26256
|
});
|
|
26257
|
+
program.command("policy").description("Print the canonical packaged Context Tree policy.").action(() => {
|
|
26258
|
+
line(io, JSON.stringify(readContextTreePolicy()));
|
|
26259
|
+
});
|
|
25875
26260
|
return program;
|
|
25876
26261
|
}
|
|
25877
|
-
function sanitizeError(message) {
|
|
25878
|
-
return message.replace(/(?:https?|ssh):\/\/[^\s/@]+@/giu, "<redacted>@");
|
|
25879
|
-
}
|
|
25880
26262
|
async function runContextTreeCli(argv = process.argv, io = defaultIo) {
|
|
25881
26263
|
const program = createContextTreeCli(io);
|
|
25882
26264
|
try {
|
|
@@ -25886,8 +26268,8 @@ async function runContextTreeCli(argv = process.argv, io = defaultIo) {
|
|
|
25886
26268
|
if (error instanceof CommanderError && error.exitCode === 0) return 0;
|
|
25887
26269
|
const envelope = {
|
|
25888
26270
|
error: {
|
|
25889
|
-
code: error instanceof
|
|
25890
|
-
message:
|
|
26271
|
+
code: error instanceof ContextTreeError ? error.code : CLI_ERROR_CODES.failed,
|
|
26272
|
+
message: sanitizeCommandOutput(error instanceof Error ? error.message : String(error))
|
|
25891
26273
|
},
|
|
25892
26274
|
ok: false,
|
|
25893
26275
|
schemaVersion: 1
|