@first-tree-ai/context-tree 0.1.6-alpha.202608310559 → 0.1.7
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 +158 -128
- package/dist/cli/index.mjs +1404 -746
- package/package.json +7 -28
- package/scripts/postinstall.mjs +53 -0
- package/skills/context-tree-connect/SKILL.md +36 -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 +22 -0
- package/skills/context-tree-publish/agents/openai.yaml +4 -0
- package/skills/context-tree-read/SKILL.md +34 -37
- package/skills/context-tree-read/agents/openai.yaml +2 -2
- package/skills/context-tree-setup/SKILL.md +28 -0
- package/skills/context-tree-setup/agents/openai.yaml +4 -0
- package/skills/context-tree-write/SKILL.md +208 -125
- package/skills/context-tree-write/agents/openai.yaml +2 -2
- package/.agents/plugins/marketplace.json +0 -19
- package/.claude-plugin/marketplace.json +0 -21
- package/.claude-plugin/plugin.json +0 -16
- package/.codex-plugin/plugin.json +0 -36
- package/dist/cli/index.d.mts +0 -1
- package/dist/index.d.mts +0 -28
- package/dist/index.mjs +0 -1087
- package/dist/schemas-BWM6Q6iz.mjs +0 -278
- package/dist/schemas-C4bs-FkC.d.mts +0 -394
- package/dist/schemas.d.mts +0 -2
- package/dist/schemas.mjs +0 -2
- package/docs/specification.md +0 -146
- package/examples/basic/NODE.md +0 -15
- package/examples/basic/members/NODE.md +0 -8
- package/examples/basic/members/example-agent/NODE.md +0 -7
- package/examples/basic/members/example-agent/memory.md +0 -9
- package/examples/basic/systems/NODE.md +0 -10
- package/examples/basic/systems/runtime.md +0 -15
- package/hooks/hooks.json +0 -26
- package/hooks/session-start.mjs +0 -64
- package/policy/context-tree-policy.md +0 -158
- package/skills/context-tree-init/SKILL.md +0 -51
- package/skills/context-tree-init/agents/openai.yaml +0 -4
- package/skills/context-tree-init/scripts/context-tree.mjs +0 -41
- package/skills/context-tree-link/SKILL.md +0 -45
- package/skills/context-tree-link/agents/openai.yaml +0 -4
- package/skills/context-tree-link/scripts/context-tree.mjs +0 -41
- package/skills/context-tree-read/scripts/context-tree.mjs +0 -41
- package/skills/context-tree-write/scripts/context-tree.mjs +0 -41
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, copyFileSync, 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);
|
|
@@ -7240,6 +7240,448 @@ function refine(fn, _params = {}) {
|
|
|
7240
7240
|
function superRefine(fn, params) {
|
|
7241
7241
|
return /* @__PURE__ */ _superRefine(fn, params);
|
|
7242
7242
|
}
|
|
7243
|
+
const CONTEXT_TREE_ROOT_NODE_MAX_BYTES = 16 * 1024;
|
|
7244
|
+
const VALIDATION_CODES = {
|
|
7245
|
+
rootMissing: "TREE_ROOT_MISSING",
|
|
7246
|
+
rootNodeInvalid: "TREE_ROOT_NODE_INVALID",
|
|
7247
|
+
rootOnlyFields: "TREE_ROOT_ONLY_FIELDS",
|
|
7248
|
+
directoryNodeMissing: "TREE_DIRECTORY_NODE_MISSING",
|
|
7249
|
+
frontmatterMissing: "TREE_FRONTMATTER_MISSING",
|
|
7250
|
+
frontmatterParse: "TREE_FRONTMATTER_PARSE",
|
|
7251
|
+
titleMissing: "TREE_TITLE_MISSING",
|
|
7252
|
+
titleInvalid: "TREE_TITLE_INVALID",
|
|
7253
|
+
descriptionInvalid: "TREE_DESCRIPTION_INVALID",
|
|
7254
|
+
softLinksInvalid: "TREE_SOFT_LINKS_INVALID",
|
|
7255
|
+
softLinkBroken: "TREE_SOFT_LINK_BROKEN",
|
|
7256
|
+
softLinkPathEscape: "TREE_SOFT_LINK_PATH_ESCAPE",
|
|
7257
|
+
markdownPathEscape: "TREE_MARKDOWN_LINK_PATH_ESCAPE",
|
|
7258
|
+
markdownFileSymlinkBroken: "TREE_MARKDOWN_FILE_SYMLINK_BROKEN",
|
|
7259
|
+
markdownFileSymlinkUnsupported: "TREE_MARKDOWN_FILE_SYMLINK_UNSUPPORTED",
|
|
7260
|
+
markdownFilePathEscape: "TREE_MARKDOWN_FILE_PATH_ESCAPE",
|
|
7261
|
+
markdownFileContentClassMismatch: "TREE_MARKDOWN_FILE_CONTENT_CLASS_MISMATCH",
|
|
7262
|
+
directorySymlinkUnsupported: "TREE_DIRECTORY_SYMLINK_UNSUPPORTED",
|
|
7263
|
+
directorySymlinkPathEscape: "TREE_DIRECTORY_SYMLINK_PATH_ESCAPE"
|
|
7264
|
+
};
|
|
7265
|
+
const CLI_ERROR_CODES = {
|
|
7266
|
+
corruptConnection: "CORRUPT_CONNECTION",
|
|
7267
|
+
dirtyTree: "DIRTY_TREE",
|
|
7268
|
+
failed: "CONTEXT_TREE_FAILED",
|
|
7269
|
+
githubAuth: "GITHUB_AUTH",
|
|
7270
|
+
invalidTree: "INVALID_TREE",
|
|
7271
|
+
noConnection: "NO_CONNECTION",
|
|
7272
|
+
publishIncomplete: "PUBLISH_INCOMPLETE",
|
|
7273
|
+
repositoryExists: "REPOSITORY_EXISTS",
|
|
7274
|
+
staleConnection: "STALE_CONNECTION",
|
|
7275
|
+
writeOutdated: "WRITE_OUTDATED"
|
|
7276
|
+
};
|
|
7277
|
+
function hasUnsafeCharacter(value) {
|
|
7278
|
+
return [...value].some((character) => {
|
|
7279
|
+
const code = character.codePointAt(0);
|
|
7280
|
+
return code !== void 0 && (code <= 31 || code === 127 || code === 8232 || code === 8233);
|
|
7281
|
+
});
|
|
7282
|
+
}
|
|
7283
|
+
const absoluteSingleLinePathSchema = string$2().superRefine((value, context) => {
|
|
7284
|
+
if (!isAbsolute(value) || value.trim() !== value || hasUnsafeCharacter(value)) context.addIssue({
|
|
7285
|
+
code: "custom",
|
|
7286
|
+
message: "Paths must be absolute single-line values."
|
|
7287
|
+
});
|
|
7288
|
+
});
|
|
7289
|
+
const credentialFreeRepositoryUrlSchema = string$2().superRefine((value, context) => {
|
|
7290
|
+
if (value.trim() !== value || hasUnsafeCharacter(value) || value.includes("\\")) {
|
|
7291
|
+
context.addIssue({
|
|
7292
|
+
code: "custom",
|
|
7293
|
+
message: "Repository URLs must be canonical single-line values."
|
|
7294
|
+
});
|
|
7295
|
+
return;
|
|
7296
|
+
}
|
|
7297
|
+
if (value.includes("?") || value.includes("#")) {
|
|
7298
|
+
context.addIssue({
|
|
7299
|
+
code: "custom",
|
|
7300
|
+
message: "Repository URLs must not contain a query or fragment."
|
|
7301
|
+
});
|
|
7302
|
+
return;
|
|
7303
|
+
}
|
|
7304
|
+
if (/^(?:[a-z\d._-]+@)?[a-z\d.-]+:[^\s/][^\s]*$/iu.test(value)) return;
|
|
7305
|
+
try {
|
|
7306
|
+
const parsed = new URL(value);
|
|
7307
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:" && parsed.protocol !== "ssh:" || !parsed.hostname || parsed.pathname.split("/").every((part) => part.length === 0) || parsed.password || (parsed.protocol === "http:" || parsed.protocol === "https:") && parsed.username) throw new Error("invalid transport");
|
|
7308
|
+
} catch {
|
|
7309
|
+
context.addIssue({
|
|
7310
|
+
code: "custom",
|
|
7311
|
+
message: "Repository URLs must use credential-free HTTP(S), ssh://, or scp-like SSH syntax."
|
|
7312
|
+
});
|
|
7313
|
+
}
|
|
7314
|
+
});
|
|
7315
|
+
const treeNameSchema = string$2().superRefine((value, context) => {
|
|
7316
|
+
if (!/^[A-Za-z\d][A-Za-z\d._-]{0,99}$/u.test(value) || /\.git$/iu.test(value)) context.addIssue({
|
|
7317
|
+
code: "custom",
|
|
7318
|
+
message: "Tree name must be a safe single path segment."
|
|
7319
|
+
});
|
|
7320
|
+
});
|
|
7321
|
+
const githubRepositoryIdentitySchema = string$2().superRefine((value, context) => {
|
|
7322
|
+
const parts = value.split("/");
|
|
7323
|
+
const [owner, name] = parts;
|
|
7324
|
+
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({
|
|
7325
|
+
code: "custom",
|
|
7326
|
+
message: "Repository must be an explicit GitHub OWNER/REPO identity."
|
|
7327
|
+
});
|
|
7328
|
+
});
|
|
7329
|
+
const contextTreeRootNodeSchema = object({
|
|
7330
|
+
frontmatter: object({
|
|
7331
|
+
schemaVersion: literal(1),
|
|
7332
|
+
title: string$2().trim().min(1),
|
|
7333
|
+
description: string$2().trim().min(1).optional(),
|
|
7334
|
+
soft_links: array(string$2().trim().min(1)).min(1).optional()
|
|
7335
|
+
}).loose(),
|
|
7336
|
+
body: string$2().trim().min(1, "Root NODE.md must contain repository-wide context.")
|
|
7337
|
+
});
|
|
7338
|
+
const contextContentClassSchema = _enum([
|
|
7339
|
+
"normal",
|
|
7340
|
+
"member",
|
|
7341
|
+
"repo-infra"
|
|
7342
|
+
]);
|
|
7343
|
+
const treeValidationFindingSchema = object({
|
|
7344
|
+
code: _enum(VALIDATION_CODES),
|
|
7345
|
+
message: string$2(),
|
|
7346
|
+
path: string$2(),
|
|
7347
|
+
target: string$2().optional()
|
|
7348
|
+
}).strict();
|
|
7349
|
+
const contextContentClassCountsSchema = object({
|
|
7350
|
+
normal: number().int().nonnegative(),
|
|
7351
|
+
member: number().int().nonnegative(),
|
|
7352
|
+
"repo-infra": number().int().nonnegative()
|
|
7353
|
+
}).strict();
|
|
7354
|
+
const SKILL_HOSTS = ["claude", "codex"];
|
|
7355
|
+
const skillHostSchema = _enum(SKILL_HOSTS);
|
|
7356
|
+
const skillInstallationSchema = object({
|
|
7357
|
+
host: skillHostSchema,
|
|
7358
|
+
path: absoluteSingleLinePathSchema,
|
|
7359
|
+
skills: array(string$2().trim().min(1))
|
|
7360
|
+
}).strict();
|
|
7361
|
+
const skillInstallSkipSchema = object({
|
|
7362
|
+
host: skillHostSchema,
|
|
7363
|
+
reason: string$2().trim().min(1)
|
|
7364
|
+
}).strict();
|
|
7365
|
+
object({
|
|
7366
|
+
installed: array(skillInstallationSchema),
|
|
7367
|
+
schemaVersion: literal(1),
|
|
7368
|
+
skipped: array(skillInstallSkipSchema),
|
|
7369
|
+
version: string$2().trim().min(1)
|
|
7370
|
+
}).strict();
|
|
7371
|
+
const contextTreeReadCommonFields = {
|
|
7372
|
+
contentClass: contextContentClassSchema,
|
|
7373
|
+
kind: _enum(["directory", "file"]),
|
|
7374
|
+
path: string$2()
|
|
7375
|
+
};
|
|
7376
|
+
const contextTreeReadNodeSchema = object({
|
|
7377
|
+
body: string$2(),
|
|
7378
|
+
frontmatter: record(string$2(), unknown()),
|
|
7379
|
+
...contextTreeReadCommonFields
|
|
7380
|
+
}).strict();
|
|
7381
|
+
object({
|
|
7382
|
+
children: array(object({
|
|
7383
|
+
description: string$2().optional(),
|
|
7384
|
+
title: string$2(),
|
|
7385
|
+
...contextTreeReadCommonFields
|
|
7386
|
+
}).strict()),
|
|
7387
|
+
node: contextTreeReadNodeSchema,
|
|
7388
|
+
root: string$2(),
|
|
7389
|
+
schemaVersion: literal(1),
|
|
7390
|
+
target: string$2()
|
|
7391
|
+
}).strict();
|
|
7392
|
+
object({
|
|
7393
|
+
findings: array(treeValidationFindingSchema),
|
|
7394
|
+
ok: boolean(),
|
|
7395
|
+
root: string$2(),
|
|
7396
|
+
scannedByContentClass: contextContentClassCountsSchema,
|
|
7397
|
+
schemaVersion: literal(1)
|
|
7398
|
+
}).strict();
|
|
7399
|
+
const contextTreeStateSchema = discriminatedUnion("kind", [object({
|
|
7400
|
+
kind: literal("local"),
|
|
7401
|
+
path: absoluteSingleLinePathSchema
|
|
7402
|
+
}).strict(), object({
|
|
7403
|
+
kind: literal("github"),
|
|
7404
|
+
path: absoluteSingleLinePathSchema,
|
|
7405
|
+
repository: githubRepositoryIdentitySchema
|
|
7406
|
+
}).strict()]);
|
|
7407
|
+
const contextTreeConnectionSchema = object({
|
|
7408
|
+
projectPath: absoluteSingleLinePathSchema,
|
|
7409
|
+
tree: contextTreeStateSchema
|
|
7410
|
+
}).strict();
|
|
7411
|
+
object({
|
|
7412
|
+
schemaVersion: literal(1),
|
|
7413
|
+
tree: contextTreeStateSchema
|
|
7414
|
+
}).strict();
|
|
7415
|
+
/** Whether the project's AGENTS.md pointer was created, rewritten, or left alone. */
|
|
7416
|
+
const projectPointerOutcomeSchema = _enum([
|
|
7417
|
+
"written",
|
|
7418
|
+
"updated",
|
|
7419
|
+
"skipped"
|
|
7420
|
+
]);
|
|
7421
|
+
object({
|
|
7422
|
+
branch: string$2().trim().min(1),
|
|
7423
|
+
commitSha: string$2(),
|
|
7424
|
+
created: boolean(),
|
|
7425
|
+
pointer: projectPointerOutcomeSchema,
|
|
7426
|
+
schemaVersion: literal(1),
|
|
7427
|
+
title: string$2().trim().min(1),
|
|
7428
|
+
treePath: absoluteSingleLinePathSchema
|
|
7429
|
+
}).strict();
|
|
7430
|
+
object({
|
|
7431
|
+
pointer: projectPointerOutcomeSchema,
|
|
7432
|
+
schemaVersion: literal(1),
|
|
7433
|
+
tree: contextTreeStateSchema
|
|
7434
|
+
}).strict();
|
|
7435
|
+
const managedTreeListingEntrySchema = object({
|
|
7436
|
+
name: treeNameSchema,
|
|
7437
|
+
tree: contextTreeStateSchema
|
|
7438
|
+
}).strict();
|
|
7439
|
+
object({
|
|
7440
|
+
schemaVersion: literal(1),
|
|
7441
|
+
trees: array(managedTreeListingEntrySchema)
|
|
7442
|
+
}).strict();
|
|
7443
|
+
object({
|
|
7444
|
+
branch: string$2().trim().min(1),
|
|
7445
|
+
schemaVersion: literal(1),
|
|
7446
|
+
sha: string$2(),
|
|
7447
|
+
tree: contextTreeStateSchema
|
|
7448
|
+
}).strict();
|
|
7449
|
+
object({
|
|
7450
|
+
schemaVersion: literal(1),
|
|
7451
|
+
worktreePath: absoluteSingleLinePathSchema
|
|
7452
|
+
}).strict();
|
|
7453
|
+
object({
|
|
7454
|
+
branch: string$2().trim().min(1),
|
|
7455
|
+
schemaVersion: literal(1),
|
|
7456
|
+
sha: string$2()
|
|
7457
|
+
}).strict();
|
|
7458
|
+
object({
|
|
7459
|
+
branch: string$2().trim().min(1),
|
|
7460
|
+
repository: githubRepositoryIdentitySchema,
|
|
7461
|
+
schemaVersion: literal(1),
|
|
7462
|
+
sha: string$2(),
|
|
7463
|
+
url: credentialFreeRepositoryUrlSchema
|
|
7464
|
+
}).strict();
|
|
7465
|
+
object({
|
|
7466
|
+
error: object({
|
|
7467
|
+
code: _enum(CLI_ERROR_CODES),
|
|
7468
|
+
message: string$2()
|
|
7469
|
+
}).strict(),
|
|
7470
|
+
ok: literal(false),
|
|
7471
|
+
schemaVersion: literal(1)
|
|
7472
|
+
}).strict();
|
|
7473
|
+
//#endregion
|
|
7474
|
+
//#region src/core/internal/errors.ts
|
|
7475
|
+
/**
|
|
7476
|
+
* A failure the CLI reports with a specific machine-readable code. Anything
|
|
7477
|
+
* thrown as a plain Error is reported as CONTEXT_TREE_FAILED instead.
|
|
7478
|
+
*/
|
|
7479
|
+
var ContextTreeError = class extends Error {
|
|
7480
|
+
code;
|
|
7481
|
+
constructor(code, message) {
|
|
7482
|
+
super(message);
|
|
7483
|
+
this.name = "ContextTreeError";
|
|
7484
|
+
this.code = code;
|
|
7485
|
+
}
|
|
7486
|
+
};
|
|
7487
|
+
//#endregion
|
|
7488
|
+
//#region src/core/internal/git.ts
|
|
7489
|
+
function defaultRunner(command, args) {
|
|
7490
|
+
const result = spawnSync(command, args, {
|
|
7491
|
+
encoding: "utf8",
|
|
7492
|
+
stdio: [
|
|
7493
|
+
"ignore",
|
|
7494
|
+
"pipe",
|
|
7495
|
+
"pipe"
|
|
7496
|
+
]
|
|
7497
|
+
});
|
|
7498
|
+
return {
|
|
7499
|
+
status: result.status,
|
|
7500
|
+
stderr: typeof result.stderr === "string" ? result.stderr : "",
|
|
7501
|
+
stdout: typeof result.stdout === "string" ? result.stdout : ""
|
|
7502
|
+
};
|
|
7503
|
+
}
|
|
7504
|
+
/** A failed Git or `gh` operation. Messages never include the argv. */
|
|
7505
|
+
var CommandError = class extends Error {
|
|
7506
|
+
command;
|
|
7507
|
+
status;
|
|
7508
|
+
stderr;
|
|
7509
|
+
constructor(command, status, stderr, message) {
|
|
7510
|
+
const detail = sanitizeCommandOutput(stderr).trim();
|
|
7511
|
+
super(detail.length > 0 ? `${message}: ${detail}` : message);
|
|
7512
|
+
this.name = "CommandError";
|
|
7513
|
+
this.command = command;
|
|
7514
|
+
this.status = status;
|
|
7515
|
+
this.stderr = detail;
|
|
7516
|
+
}
|
|
7517
|
+
};
|
|
7518
|
+
/** Remove credentials and common access-token shapes before surfacing subprocess output. */
|
|
7519
|
+
function sanitizeCommandOutput(value) {
|
|
7520
|
+
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>");
|
|
7521
|
+
}
|
|
7522
|
+
function trimOutput(value) {
|
|
7523
|
+
return value.trim();
|
|
7524
|
+
}
|
|
7525
|
+
function execute(runner, command, args, message) {
|
|
7526
|
+
const result = runner(command, args);
|
|
7527
|
+
if (result.status !== 0) throw new CommandError(command, result.status, result.stderr, message);
|
|
7528
|
+
return trimOutput(result.stdout);
|
|
7529
|
+
}
|
|
7530
|
+
/** Run a Git command that is not scoped by `-C`, such as `git init <path>`. */
|
|
7531
|
+
function gitCommand(args, options = {}) {
|
|
7532
|
+
return execute(options.runner ?? defaultRunner, "git", args, options.message ?? "A Git operation failed.");
|
|
7533
|
+
}
|
|
7534
|
+
/** Run `git -C <root> <args>` and return trimmed stdout, throwing on failure. */
|
|
7535
|
+
function git(root, args, options = {}) {
|
|
7536
|
+
return execute(options.runner ?? defaultRunner, "git", [
|
|
7537
|
+
"-C",
|
|
7538
|
+
root,
|
|
7539
|
+
...args
|
|
7540
|
+
], options.message ?? "A Git operation failed.");
|
|
7541
|
+
}
|
|
7542
|
+
/** Run `git -C <root> <args>` and return trimmed stdout, or undefined on failure. */
|
|
7543
|
+
function optionalGit(root, args, runner = defaultRunner) {
|
|
7544
|
+
const result = runner("git", [
|
|
7545
|
+
"-C",
|
|
7546
|
+
root,
|
|
7547
|
+
...args
|
|
7548
|
+
]);
|
|
7549
|
+
if (result.status !== 0) return void 0;
|
|
7550
|
+
return trimOutput(result.stdout);
|
|
7551
|
+
}
|
|
7552
|
+
/** Run `gh <args>` and return trimmed stdout, throwing on failure. */
|
|
7553
|
+
function gh(args, options = {}) {
|
|
7554
|
+
return execute(options.runner ?? defaultRunner, "gh", args, options.message ?? "A GitHub CLI operation failed.");
|
|
7555
|
+
}
|
|
7556
|
+
//#endregion
|
|
7557
|
+
//#region src/core/internal/github-repository.ts
|
|
7558
|
+
const ORIGIN_MESSAGE = "Context Tree origin must identify a credential-free GitHub OWNER/REPO repository.";
|
|
7559
|
+
function canonicalGitHubRepositoryUrl(repository) {
|
|
7560
|
+
githubRepositoryIdentitySchema.parse(repository);
|
|
7561
|
+
return `https://github.com/${repository}.git`;
|
|
7562
|
+
}
|
|
7563
|
+
/** Derive OWNER/REPO from a github.com origin, rejecting anything else. */
|
|
7564
|
+
function gitHubRepositoryFromOriginUrl(origin) {
|
|
7565
|
+
if (!credentialFreeRepositoryUrlSchema.safeParse(origin).success) throw new Error(ORIGIN_MESSAGE);
|
|
7566
|
+
let owner;
|
|
7567
|
+
let name;
|
|
7568
|
+
const scp = /^(?:git@)?github\.com:([^/]+)\/(.+)$/iu.exec(origin);
|
|
7569
|
+
if (scp !== null) [, owner, name] = scp;
|
|
7570
|
+
else {
|
|
7571
|
+
const url = URL.parse(origin);
|
|
7572
|
+
if (url === null || url.hostname.toLowerCase() !== "github.com") throw new Error(ORIGIN_MESSAGE);
|
|
7573
|
+
[owner, name] = url.pathname.replace(/^\/+|\/+$/gu, "").split("/");
|
|
7574
|
+
}
|
|
7575
|
+
const repository = `${owner ?? ""}/${(name ?? "").replace(/\.git$/iu, "")}`;
|
|
7576
|
+
if (!githubRepositoryIdentitySchema.safeParse(repository).success) throw new Error(ORIGIN_MESSAGE);
|
|
7577
|
+
return repository;
|
|
7578
|
+
}
|
|
7579
|
+
//#endregion
|
|
7580
|
+
//#region src/core/path.ts
|
|
7581
|
+
function isPathInside(root, target) {
|
|
7582
|
+
const path = relative(root, target);
|
|
7583
|
+
return path === "" || !path.startsWith("..") && !isAbsolute(path);
|
|
7584
|
+
}
|
|
7585
|
+
function resolveTreeRoot(path) {
|
|
7586
|
+
const absolute = resolve(path);
|
|
7587
|
+
const entry = lstatSync(absolute);
|
|
7588
|
+
if (entry.isSymbolicLink() || !entry.isDirectory()) throw new Error(`Context Tree root must be a real directory: ${path}`);
|
|
7589
|
+
return realpathSync(absolute);
|
|
7590
|
+
}
|
|
7591
|
+
/** Resolve a directory while rejecting symlinks in user-controlled path components. */
|
|
7592
|
+
function realDirectoryWithoutSymlinks(path, label) {
|
|
7593
|
+
const absolute = resolve(path);
|
|
7594
|
+
const parsed = parse(absolute);
|
|
7595
|
+
const parts = absolute.slice(parsed.root.length).split(sep).filter(Boolean);
|
|
7596
|
+
let current = parsed.root;
|
|
7597
|
+
for (const [index, part] of parts.entries()) {
|
|
7598
|
+
current = resolve(current, part);
|
|
7599
|
+
if (lstatSync(current).isSymbolicLink()) if (index === 0) current = realpathSync(current);
|
|
7600
|
+
else throw new Error(`${label} must contain no symlink component.`);
|
|
7601
|
+
}
|
|
7602
|
+
if (!lstatSync(current).isDirectory()) throw new Error(`${label} must be a directory.`);
|
|
7603
|
+
return realpathSync(current);
|
|
7604
|
+
}
|
|
7605
|
+
function toPosixPath(path) {
|
|
7606
|
+
return path.replace(/\\/gu, "/");
|
|
7607
|
+
}
|
|
7608
|
+
//#endregion
|
|
7609
|
+
//#region src/core/internal/project.ts
|
|
7610
|
+
/**
|
|
7611
|
+
* Projects are identified solely by their canonical local root. A Git
|
|
7612
|
+
* repository without an origin, a non-Git directory, a Git worktree, and a
|
|
7613
|
+
* separate clone are all independent checkouts with their own canonical root.
|
|
7614
|
+
*/
|
|
7615
|
+
function canonicalProjectRoot(path, runner) {
|
|
7616
|
+
const directory = realDirectoryWithoutSymlinks(path, "Project path");
|
|
7617
|
+
const toplevel = optionalGit(directory, ["rev-parse", "--show-toplevel"], runner);
|
|
7618
|
+
if (toplevel === void 0 || toplevel.length === 0) return directory;
|
|
7619
|
+
return realpathSync(toplevel);
|
|
7620
|
+
}
|
|
7621
|
+
//#endregion
|
|
7622
|
+
//#region src/core/internal/project-pointer.ts
|
|
7623
|
+
const BEGIN = "<!-- context-tree:begin -->";
|
|
7624
|
+
const END = "<!-- context-tree:end -->";
|
|
7625
|
+
function pointerBlock(treePath) {
|
|
7626
|
+
return [
|
|
7627
|
+
BEGIN,
|
|
7628
|
+
"## Context Tree",
|
|
7629
|
+
"",
|
|
7630
|
+
`This project is connected to a Context Tree at \`${treePath}\`.`,
|
|
7631
|
+
"",
|
|
7632
|
+
"Read the decisions and constraints that bear on a task before planning or",
|
|
7633
|
+
"changing code, and record durable decisions there. Use the `context-tree-read`",
|
|
7634
|
+
"and `context-tree-write` skills rather than editing the tree by hand.",
|
|
7635
|
+
END
|
|
7636
|
+
].join("\n");
|
|
7637
|
+
}
|
|
7638
|
+
/** A real regular file, a real absent path, or a refusal. */
|
|
7639
|
+
function regularFileContent(path) {
|
|
7640
|
+
const entry = lstatSync(path, { throwIfNoEntry: false });
|
|
7641
|
+
if (entry === void 0) return void 0;
|
|
7642
|
+
if (entry.isSymbolicLink() || !entry.isFile()) throw new Error(`Refusing to write the Context Tree pointer through a symlink or non-file: ${path}`);
|
|
7643
|
+
return readFileSync(path, "utf8");
|
|
7644
|
+
}
|
|
7645
|
+
/**
|
|
7646
|
+
* Record the connected tree in the project's own `AGENTS.md`, so any agent that reads
|
|
7647
|
+
* instruction files knows the tree exists without a host-specific session hook.
|
|
7648
|
+
*
|
|
7649
|
+
* Idempotent: a delimited block is rewritten in place, never appended twice, so
|
|
7650
|
+
* switching the connected tree updates the existing pointer. All other content is
|
|
7651
|
+
* preserved, and an existing regular `CLAUDE.md` is left alone.
|
|
7652
|
+
*/
|
|
7653
|
+
function writeProjectPointer(projectPath, treePath) {
|
|
7654
|
+
const agentsPath = join(projectPath, "AGENTS.md");
|
|
7655
|
+
const block = pointerBlock(treePath);
|
|
7656
|
+
const existing = regularFileContent(agentsPath);
|
|
7657
|
+
if (existing === void 0) {
|
|
7658
|
+
writeFileSync(agentsPath, `# AGENTS.md\n\n${block}\n`, {
|
|
7659
|
+
encoding: "utf8",
|
|
7660
|
+
mode: 420
|
|
7661
|
+
});
|
|
7662
|
+
linkClaudeMarkdown(projectPath);
|
|
7663
|
+
return "written";
|
|
7664
|
+
}
|
|
7665
|
+
const start = existing.indexOf(BEGIN);
|
|
7666
|
+
const end = existing.indexOf(END);
|
|
7667
|
+
if (start !== -1 && end > start) {
|
|
7668
|
+
const replaced = `${existing.slice(0, start)}${block}${existing.slice(end + 25)}`;
|
|
7669
|
+
if (replaced === existing) return "skipped";
|
|
7670
|
+
writeFileSync(agentsPath, replaced, { encoding: "utf8" });
|
|
7671
|
+
return "updated";
|
|
7672
|
+
}
|
|
7673
|
+
writeFileSync(agentsPath, `${existing}${existing.endsWith("\n\n") ? "" : existing.endsWith("\n") ? "\n" : "\n\n"}${block}\n`, { encoding: "utf8" });
|
|
7674
|
+
linkClaudeMarkdown(projectPath);
|
|
7675
|
+
return "written";
|
|
7676
|
+
}
|
|
7677
|
+
/** Point CLAUDE.md at AGENTS.md only when the project has no CLAUDE.md of its own. */
|
|
7678
|
+
function linkClaudeMarkdown(projectPath) {
|
|
7679
|
+
const claudePath = join(projectPath, "CLAUDE.md");
|
|
7680
|
+
if (lstatSync(claudePath, { throwIfNoEntry: false }) !== void 0) return;
|
|
7681
|
+
try {
|
|
7682
|
+
symlinkSync("AGENTS.md", claudePath, "file");
|
|
7683
|
+
} catch {}
|
|
7684
|
+
}
|
|
7243
7685
|
//#endregion
|
|
7244
7686
|
//#region node_modules/.pnpm/yaml@2.9.0/node_modules/yaml/dist/nodes/identity.js
|
|
7245
7687
|
var require_identity = /* @__PURE__ */ __commonJSMin(((exports) => {
|
|
@@ -13912,90 +14354,20 @@ function parseMarkdownFrontmatter(source) {
|
|
|
13912
14354
|
};
|
|
13913
14355
|
}
|
|
13914
14356
|
}
|
|
13915
|
-
|
|
13916
|
-
|
|
13917
|
-
|
|
13918
|
-
|
|
13919
|
-
rootOnlyFields: "TREE_ROOT_ONLY_FIELDS",
|
|
13920
|
-
directoryNodeMissing: "TREE_DIRECTORY_NODE_MISSING",
|
|
13921
|
-
frontmatterMissing: "TREE_FRONTMATTER_MISSING",
|
|
13922
|
-
frontmatterParse: "TREE_FRONTMATTER_PARSE",
|
|
13923
|
-
titleMissing: "TREE_TITLE_MISSING",
|
|
13924
|
-
titleInvalid: "TREE_TITLE_INVALID",
|
|
13925
|
-
descriptionInvalid: "TREE_DESCRIPTION_INVALID",
|
|
13926
|
-
softLinksInvalid: "TREE_SOFT_LINKS_INVALID",
|
|
13927
|
-
softLinkBroken: "TREE_SOFT_LINK_BROKEN",
|
|
13928
|
-
softLinkPathEscape: "TREE_SOFT_LINK_PATH_ESCAPE",
|
|
13929
|
-
markdownPathEscape: "TREE_MARKDOWN_LINK_PATH_ESCAPE",
|
|
13930
|
-
markdownFileSymlinkBroken: "TREE_MARKDOWN_FILE_SYMLINK_BROKEN",
|
|
13931
|
-
markdownFileSymlinkUnsupported: "TREE_MARKDOWN_FILE_SYMLINK_UNSUPPORTED",
|
|
13932
|
-
markdownFilePathEscape: "TREE_MARKDOWN_FILE_PATH_ESCAPE",
|
|
13933
|
-
markdownFileContentClassMismatch: "TREE_MARKDOWN_FILE_CONTENT_CLASS_MISMATCH",
|
|
13934
|
-
directorySymlinkUnsupported: "TREE_DIRECTORY_SYMLINK_UNSUPPORTED",
|
|
13935
|
-
directorySymlinkPathEscape: "TREE_DIRECTORY_SYMLINK_PATH_ESCAPE"
|
|
13936
|
-
};
|
|
13937
|
-
const CLI_ERROR_CODES = {
|
|
13938
|
-
ambiguousLink: "AMBIGUOUS_LINK",
|
|
13939
|
-
corruptLink: "CORRUPT_LINK",
|
|
13940
|
-
failed: "CONTEXT_TREE_FAILED",
|
|
13941
|
-
noLink: "NO_LINK",
|
|
13942
|
-
staleLink: "STALE_LINK"
|
|
13943
|
-
};
|
|
13944
|
-
function hasUnsafeCharacter(value) {
|
|
13945
|
-
return [...value].some((character) => {
|
|
13946
|
-
const code = character.codePointAt(0);
|
|
13947
|
-
return code !== void 0 && (code <= 31 || code === 127 || code === 8232 || code === 8233);
|
|
13948
|
-
});
|
|
14357
|
+
//#endregion
|
|
14358
|
+
//#region src/core/internal/filesystem.ts
|
|
14359
|
+
function readUtf8File(path) {
|
|
14360
|
+
return new TextDecoder("utf-8", { fatal: true }).decode(readFileSync(path));
|
|
13949
14361
|
}
|
|
13950
|
-
|
|
13951
|
-
|
|
13952
|
-
|
|
13953
|
-
|
|
13954
|
-
|
|
13955
|
-
|
|
13956
|
-
|
|
13957
|
-
|
|
13958
|
-
|
|
13959
|
-
code: "custom",
|
|
13960
|
-
message: "Repository URLs must be canonical single-line values."
|
|
13961
|
-
});
|
|
13962
|
-
return;
|
|
13963
|
-
}
|
|
13964
|
-
if (value.includes("?") || value.includes("#")) {
|
|
13965
|
-
context.addIssue({
|
|
13966
|
-
code: "custom",
|
|
13967
|
-
message: "Repository URLs must not contain a query or fragment."
|
|
13968
|
-
});
|
|
13969
|
-
return;
|
|
13970
|
-
}
|
|
13971
|
-
if (/^(?:[a-z\d._-]+@)?[a-z\d.-]+:[^\s/][^\s]*$/iu.test(value)) return;
|
|
13972
|
-
try {
|
|
13973
|
-
const parsed = new URL(value);
|
|
13974
|
-
if (parsed.protocol !== "http:" && parsed.protocol !== "https:" && parsed.protocol !== "ssh:" || !parsed.hostname || parsed.pathname.split("/").every((part) => part.length === 0) || parsed.password || (parsed.protocol === "http:" || parsed.protocol === "https:") && parsed.username) throw new Error("invalid transport");
|
|
13975
|
-
} catch {
|
|
13976
|
-
context.addIssue({
|
|
13977
|
-
code: "custom",
|
|
13978
|
-
message: "Repository URLs must use credential-free HTTP(S), ssh://, or scp-like SSH syntax."
|
|
13979
|
-
});
|
|
13980
|
-
}
|
|
13981
|
-
});
|
|
13982
|
-
const githubRepositoryIdentitySchema = string$2().superRefine((value, context) => {
|
|
13983
|
-
const parts = value.split("/");
|
|
13984
|
-
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) || !/^[A-Za-z\d._-]{1,100}$/u.test(name) || name === "." || name === ".." || /\.git$/iu.test(name)) context.addIssue({
|
|
13986
|
-
code: "custom",
|
|
13987
|
-
message: "Repository must be an explicit GitHub OWNER/REPO identity."
|
|
13988
|
-
});
|
|
13989
|
-
});
|
|
13990
|
-
const contextTreeRootNodeSchema = object({
|
|
13991
|
-
frontmatter: object({
|
|
13992
|
-
schemaVersion: literal(1),
|
|
13993
|
-
title: string$2().trim().min(1),
|
|
13994
|
-
description: string$2().trim().min(1).optional(),
|
|
13995
|
-
soft_links: array(string$2().trim().min(1)).min(1).optional()
|
|
13996
|
-
}).loose(),
|
|
13997
|
-
body: string$2().trim().min(1, "Root NODE.md must contain repository-wide context.")
|
|
13998
|
-
});
|
|
14362
|
+
//#endregion
|
|
14363
|
+
//#region src/core/internal/root-node.ts
|
|
14364
|
+
/**
|
|
14365
|
+
* The one root NODE.md reader. Verification and tree-state resolution both need
|
|
14366
|
+
* it, and they must agree on the fail-closed file guard, so it lives here rather
|
|
14367
|
+
* than in either caller: tree-state already imports verify, so a shared helper
|
|
14368
|
+
* in verify would close a cycle.
|
|
14369
|
+
*/
|
|
14370
|
+
/** Parse root NODE.md content, bounding size before any YAML or Markdown work. */
|
|
13999
14371
|
function parseContextTreeRootNode(markdown) {
|
|
14000
14372
|
if (Buffer.byteLength(markdown, "utf8") > 16384) throw new Error(`Root NODE.md exceeds the ${CONTEXT_TREE_ROOT_NODE_MAX_BYTES}-byte limit.`);
|
|
14001
14373
|
const document = parseMarkdownFrontmatter(markdown);
|
|
@@ -14006,214 +14378,12 @@ function parseContextTreeRootNode(markdown) {
|
|
|
14006
14378
|
body: document.body
|
|
14007
14379
|
});
|
|
14008
14380
|
}
|
|
14009
|
-
|
|
14010
|
-
|
|
14011
|
-
"
|
|
14012
|
-
|
|
14013
|
-
|
|
14014
|
-
|
|
14015
|
-
code: _enum(VALIDATION_CODES),
|
|
14016
|
-
message: string$2(),
|
|
14017
|
-
path: string$2(),
|
|
14018
|
-
target: string$2().optional()
|
|
14019
|
-
}).strict();
|
|
14020
|
-
const contextContentClassCountsSchema = object({
|
|
14021
|
-
normal: number().int().nonnegative(),
|
|
14022
|
-
member: number().int().nonnegative(),
|
|
14023
|
-
"repo-infra": number().int().nonnegative()
|
|
14024
|
-
}).strict();
|
|
14025
|
-
object({
|
|
14026
|
-
content: string$2(),
|
|
14027
|
-
schemaVersion: literal(1)
|
|
14028
|
-
}).strict();
|
|
14029
|
-
const contextTreeReadCommonFields = {
|
|
14030
|
-
contentClass: contextContentClassSchema,
|
|
14031
|
-
kind: _enum(["directory", "file"]),
|
|
14032
|
-
path: string$2()
|
|
14033
|
-
};
|
|
14034
|
-
const contextTreeReadNodeSchema = object({
|
|
14035
|
-
body: string$2(),
|
|
14036
|
-
frontmatter: record(string$2(), unknown()),
|
|
14037
|
-
...contextTreeReadCommonFields
|
|
14038
|
-
}).strict();
|
|
14039
|
-
object({
|
|
14040
|
-
children: array(object({
|
|
14041
|
-
description: string$2().optional(),
|
|
14042
|
-
title: string$2(),
|
|
14043
|
-
...contextTreeReadCommonFields
|
|
14044
|
-
}).strict()),
|
|
14045
|
-
node: contextTreeReadNodeSchema,
|
|
14046
|
-
root: string$2(),
|
|
14047
|
-
schemaVersion: literal(1),
|
|
14048
|
-
target: string$2()
|
|
14049
|
-
}).strict();
|
|
14050
|
-
const verifyTreeReportSchema = object({
|
|
14051
|
-
findings: array(treeValidationFindingSchema),
|
|
14052
|
-
ok: boolean(),
|
|
14053
|
-
root: string$2(),
|
|
14054
|
-
scannedByContentClass: contextContentClassCountsSchema,
|
|
14055
|
-
schemaVersion: literal(1)
|
|
14056
|
-
}).strict();
|
|
14057
|
-
object({
|
|
14058
|
-
files: array(string$2()),
|
|
14059
|
-
root: string$2(),
|
|
14060
|
-
schemaVersion: literal(1),
|
|
14061
|
-
verification: verifyTreeReportSchema
|
|
14062
|
-
}).strict();
|
|
14063
|
-
const contextTreeLinkSchema = object({
|
|
14064
|
-
project: discriminatedUnion("kind", [object({
|
|
14065
|
-
kind: literal("git"),
|
|
14066
|
-
origin: credentialFreeRepositoryUrlSchema
|
|
14067
|
-
}).strict(), object({
|
|
14068
|
-
kind: literal("directory"),
|
|
14069
|
-
path: absoluteSingleLinePathSchema
|
|
14070
|
-
}).strict()]),
|
|
14071
|
-
tree: object({
|
|
14072
|
-
path: absoluteSingleLinePathSchema,
|
|
14073
|
-
repository: githubRepositoryIdentitySchema
|
|
14074
|
-
}).strict()
|
|
14075
|
-
}).strict();
|
|
14076
|
-
object({
|
|
14077
|
-
link: contextTreeLinkSchema,
|
|
14078
|
-
schemaVersion: literal(1)
|
|
14079
|
-
}).strict();
|
|
14080
|
-
object({
|
|
14081
|
-
link: contextTreeLinkSchema,
|
|
14082
|
-
defaultBranch: string$2().trim().min(1),
|
|
14083
|
-
refreshed: boolean(),
|
|
14084
|
-
schemaVersion: literal(1),
|
|
14085
|
-
sha: string$2()
|
|
14086
|
-
}).strict();
|
|
14087
|
-
object({
|
|
14088
|
-
link: contextTreeLinkSchema,
|
|
14089
|
-
defaultBranch: string$2().trim().min(1),
|
|
14090
|
-
baseSha: string$2(),
|
|
14091
|
-
schemaVersion: literal(1),
|
|
14092
|
-
taskBranch: string$2().trim().min(1),
|
|
14093
|
-
worktreePath: string$2()
|
|
14094
|
-
}).strict();
|
|
14095
|
-
object({
|
|
14096
|
-
base: string$2(),
|
|
14097
|
-
files: array(object({
|
|
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(),
|
|
14108
|
-
schemaVersion: literal(1),
|
|
14109
|
-
treePath: string$2()
|
|
14110
|
-
}).strict();
|
|
14111
|
-
object({
|
|
14112
|
-
error: object({
|
|
14113
|
-
code: _enum(CLI_ERROR_CODES),
|
|
14114
|
-
message: string$2()
|
|
14115
|
-
}).strict(),
|
|
14116
|
-
ok: literal(false),
|
|
14117
|
-
schemaVersion: literal(1)
|
|
14118
|
-
}).strict();
|
|
14119
|
-
//#endregion
|
|
14120
|
-
//#region src/core/internal/github-repository.ts
|
|
14121
|
-
function parseGitHubRepositoryIdentity(repository) {
|
|
14122
|
-
githubRepositoryIdentitySchema.parse(repository);
|
|
14123
|
-
return repository.split("/")[1] ?? "";
|
|
14124
|
-
}
|
|
14125
|
-
function repositoryIdentityFromGitHubUrl(repositoryUrl) {
|
|
14126
|
-
try {
|
|
14127
|
-
credentialFreeRepositoryUrlSchema.parse(repositoryUrl);
|
|
14128
|
-
} catch {
|
|
14129
|
-
throw new Error("Context Tree origin must be a safe credential-free github.com repository URL.");
|
|
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;
|
|
14147
|
-
}
|
|
14148
|
-
if (host.toLowerCase() !== "github.com") throw new Error("Context Tree origin must use github.com.");
|
|
14149
|
-
const identity = path.replace(/^\/+|\/+$/gu, "").replace(/\.git$/iu, "");
|
|
14150
|
-
try {
|
|
14151
|
-
parseGitHubRepositoryIdentity(identity);
|
|
14152
|
-
} catch {
|
|
14153
|
-
throw new Error("Context Tree origin must identify a safe GitHub OWNER/REPO repository.");
|
|
14154
|
-
}
|
|
14155
|
-
return identity;
|
|
14156
|
-
}
|
|
14157
|
-
function canonicalGitHubRepositoryUrl(repository) {
|
|
14158
|
-
parseGitHubRepositoryIdentity(repository);
|
|
14159
|
-
return `https://github.com/${repository}.git`;
|
|
14160
|
-
}
|
|
14161
|
-
//#endregion
|
|
14162
|
-
//#region src/core/internal/packaged-resource.ts
|
|
14163
|
-
const PACKAGE_NAME = "@first-tree-ai/context-tree";
|
|
14164
|
-
function isPackageRoot(path) {
|
|
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
|
-
}
|
|
14173
|
-
}
|
|
14174
|
-
function resolvePackagedResource(...segments) {
|
|
14175
|
-
let candidate = dirname(fileURLToPath(import.meta.url));
|
|
14176
|
-
const filesystemRoot = parse(candidate).root;
|
|
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
|
-
}
|
|
14188
|
-
function readPackageManifest() {
|
|
14189
|
-
const parsed = JSON.parse(readFileSync(resolvePackagedResource("package.json"), "utf8"));
|
|
14190
|
-
if (!isRecord(parsed)) throw new Error("Package metadata is invalid.");
|
|
14191
|
-
return parsed;
|
|
14192
|
-
}
|
|
14193
|
-
function readPackageVersion() {
|
|
14194
|
-
const manifest = readPackageManifest();
|
|
14195
|
-
if (typeof manifest.version !== "string") throw new Error("Package version is missing or invalid.");
|
|
14196
|
-
return manifest.version;
|
|
14197
|
-
}
|
|
14198
|
-
//#endregion
|
|
14199
|
-
//#region src/core/internal/filesystem.ts
|
|
14200
|
-
function readUtf8File(path) {
|
|
14201
|
-
return new TextDecoder("utf-8", { fatal: true }).decode(readFileSync(path));
|
|
14202
|
-
}
|
|
14203
|
-
//#endregion
|
|
14204
|
-
//#region src/core/path.ts
|
|
14205
|
-
function isPathInside(root, target) {
|
|
14206
|
-
const path = relative(root, target);
|
|
14207
|
-
return path === "" || !path.startsWith("..") && !isAbsolute(path);
|
|
14208
|
-
}
|
|
14209
|
-
function resolveTreeRoot(path) {
|
|
14210
|
-
const absolute = resolve(path);
|
|
14211
|
-
const entry = lstatSync(absolute);
|
|
14212
|
-
if (entry.isSymbolicLink() || !entry.isDirectory()) throw new Error(`Context Tree root must be a real directory: ${path}`);
|
|
14213
|
-
return realpathSync(absolute);
|
|
14214
|
-
}
|
|
14215
|
-
function toPosixPath(path) {
|
|
14216
|
-
return path.replace(/\\/gu, "/");
|
|
14381
|
+
/** Read and parse a tree's root NODE.md, refusing symlinked or irregular files. */
|
|
14382
|
+
function readRootNode(root) {
|
|
14383
|
+
const path = join(root, "NODE.md");
|
|
14384
|
+
const entry = lstatSync(path);
|
|
14385
|
+
if (entry.isSymbolicLink() || !entry.isFile()) throw new Error("Root NODE.md must be a regular file and must not be a symlink.");
|
|
14386
|
+
return parseContextTreeRootNode(readUtf8File(path));
|
|
14217
14387
|
}
|
|
14218
14388
|
//#endregion
|
|
14219
14389
|
//#region src/core/internal/content-class.ts
|
|
@@ -25199,16 +25369,13 @@ function collectNodeValidationFindings(treeRoot) {
|
|
|
25199
25369
|
//#endregion
|
|
25200
25370
|
//#region src/core/verify.ts
|
|
25201
25371
|
function rootNodeFindings(root) {
|
|
25202
|
-
|
|
25203
|
-
if (!existsSync(path)) return [{
|
|
25372
|
+
if (!existsSync(join(root, "NODE.md"))) return [{
|
|
25204
25373
|
code: VALIDATION_CODES.rootMissing,
|
|
25205
25374
|
message: "root NODE.md is missing",
|
|
25206
25375
|
path: "NODE.md"
|
|
25207
25376
|
}];
|
|
25208
25377
|
try {
|
|
25209
|
-
|
|
25210
|
-
if (entry.isSymbolicLink() || !entry.isFile()) throw new Error("Root NODE.md must be a regular file and must not be a symlink.");
|
|
25211
|
-
parseContextTreeRootNode(readUtf8File(path));
|
|
25378
|
+
readRootNode(root);
|
|
25212
25379
|
return [];
|
|
25213
25380
|
} catch (error) {
|
|
25214
25381
|
return [{
|
|
@@ -25240,417 +25407,720 @@ function verifyTree(treePath) {
|
|
|
25240
25407
|
};
|
|
25241
25408
|
}
|
|
25242
25409
|
//#endregion
|
|
25243
|
-
//#region src/core/
|
|
25244
|
-
|
|
25245
|
-
|
|
25410
|
+
//#region src/core/internal/tree-state.ts
|
|
25411
|
+
/**
|
|
25412
|
+
* The one tree-state resolver shared by create, connect, sync, publish, and
|
|
25413
|
+
* writes. It validates a checkout exactly once and reports the discriminated
|
|
25414
|
+
* tree state: a local-only tree, or a published tree with its GitHub
|
|
25415
|
+
* OWNER/REPO identity. Resolution never backfills or mutates stored state.
|
|
25416
|
+
*/
|
|
25417
|
+
/** Require a real directory with no symlink component that is an exact Git root. */
|
|
25418
|
+
function exactGitRoot(treePath, runner) {
|
|
25419
|
+
const root = realDirectoryWithoutSymlinks(treePath, "Context Tree path");
|
|
25420
|
+
const toplevel = optionalGit(root, ["rev-parse", "--show-toplevel"], runner);
|
|
25421
|
+
if (toplevel === void 0 || toplevel.length === 0) throw new Error("Context Tree path must be a Git repository.");
|
|
25422
|
+
if (realpathSync(toplevel) !== root) throw new Error("Context Tree path must be the real Git root.");
|
|
25423
|
+
return root;
|
|
25424
|
+
}
|
|
25425
|
+
/**
|
|
25426
|
+
* Validate a clean checkout without inferring state from mutable Git remotes.
|
|
25427
|
+
* Uncommitted changes and invalid content each get their own code so callers
|
|
25428
|
+
* can tell "commit your edits" apart from "this path is gone".
|
|
25429
|
+
*/
|
|
25430
|
+
function validateTreeCheckout(treePath, runner) {
|
|
25431
|
+
const root = exactGitRoot(treePath, runner);
|
|
25432
|
+
if (git(root, [
|
|
25433
|
+
"status",
|
|
25434
|
+
"--porcelain",
|
|
25435
|
+
"--untracked-files=all"
|
|
25436
|
+
], {
|
|
25437
|
+
message: "Failed to inspect Context Tree cleanliness.",
|
|
25438
|
+
runner
|
|
25439
|
+
}).trim().length !== 0) throw new ContextTreeError(CLI_ERROR_CODES.dirtyTree, `The Context Tree at ${root} has uncommitted changes; commit or discard them.`);
|
|
25440
|
+
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}.`);
|
|
25441
|
+
return root;
|
|
25442
|
+
}
|
|
25443
|
+
/** Validate a stored state without reclassifying it from mutable Git remotes. */
|
|
25444
|
+
function validateStoredTreeState(state, runner) {
|
|
25445
|
+
const path = validateTreeCheckout(state.path, runner);
|
|
25446
|
+
return state.kind === "local" ? {
|
|
25447
|
+
kind: "local",
|
|
25448
|
+
path
|
|
25449
|
+
} : {
|
|
25450
|
+
kind: "github",
|
|
25451
|
+
path,
|
|
25452
|
+
repository: state.repository
|
|
25453
|
+
};
|
|
25454
|
+
}
|
|
25455
|
+
//#endregion
|
|
25456
|
+
//#region src/core/connections.ts
|
|
25457
|
+
const connectionsFileSchema = object({
|
|
25458
|
+
connections: array(contextTreeConnectionSchema),
|
|
25246
25459
|
schemaVersion: literal(1)
|
|
25247
25460
|
}).strict();
|
|
25248
|
-
|
|
25249
|
-
|
|
25250
|
-
|
|
25251
|
-
|
|
25252
|
-
|
|
25253
|
-
|
|
25461
|
+
const DUPLICATE_MESSAGE = "Duplicate Context Tree connection records exist for this project.";
|
|
25462
|
+
const NO_CONNECTION_MESSAGE = "No Context Tree connection exists for this project; run context-tree create or connect.";
|
|
25463
|
+
function realHome$1() {
|
|
25464
|
+
try {
|
|
25465
|
+
return realpathSync(homedir());
|
|
25466
|
+
} catch {
|
|
25467
|
+
return homedir();
|
|
25254
25468
|
}
|
|
25255
|
-
};
|
|
25256
|
-
function git$1(path, args) {
|
|
25257
|
-
const result = spawnSync("git", [
|
|
25258
|
-
"-C",
|
|
25259
|
-
path,
|
|
25260
|
-
...args
|
|
25261
|
-
], {
|
|
25262
|
-
encoding: "utf8",
|
|
25263
|
-
stdio: [
|
|
25264
|
-
"ignore",
|
|
25265
|
-
"pipe",
|
|
25266
|
-
"ignore"
|
|
25267
|
-
]
|
|
25268
|
-
});
|
|
25269
|
-
if (result.error !== void 0 || result.status !== 0) return void 0;
|
|
25270
|
-
return result.stdout.replace(/\r?\n$/u, "");
|
|
25271
25469
|
}
|
|
25272
|
-
|
|
25273
|
-
|
|
25274
|
-
|
|
25275
|
-
|
|
25276
|
-
|
|
25277
|
-
|
|
25278
|
-
|
|
25279
|
-
|
|
25280
|
-
|
|
25281
|
-
|
|
25470
|
+
/** Create a managed application directory below the home directory, failing closed on symlinks. */
|
|
25471
|
+
function ensureManagedDirectory(...segments) {
|
|
25472
|
+
let current = realHome$1();
|
|
25473
|
+
for (const segment of segments) {
|
|
25474
|
+
current = join(current, segment);
|
|
25475
|
+
const entry = lstatSync(current, { throwIfNoEntry: false });
|
|
25476
|
+
if (entry === void 0) {
|
|
25477
|
+
mkdirSync(current, { mode: 448 });
|
|
25478
|
+
continue;
|
|
25479
|
+
}
|
|
25480
|
+
if (entry.isSymbolicLink() || !entry.isDirectory()) throw new Error(`Context Tree managed directory must be a real directory: ${current}`);
|
|
25282
25481
|
}
|
|
25283
|
-
|
|
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);
|
|
25482
|
+
return current;
|
|
25297
25483
|
}
|
|
25298
|
-
|
|
25299
|
-
|
|
25300
|
-
|
|
25301
|
-
if (gitRoot === void 0) return {
|
|
25302
|
-
kind: "directory",
|
|
25303
|
-
path
|
|
25304
|
-
};
|
|
25305
|
-
return {
|
|
25306
|
-
kind: "git",
|
|
25307
|
-
origin: normalizeRepositoryUrl(requireGit$1(realpathSync(gitRoot), [
|
|
25308
|
-
"remote",
|
|
25309
|
-
"get-url",
|
|
25310
|
-
"origin"
|
|
25311
|
-
], "Git project must have an origin remote."))
|
|
25312
|
-
};
|
|
25484
|
+
/** The managed namespace without creating it; listing must not create an absent directory. */
|
|
25485
|
+
function managedTreesPath() {
|
|
25486
|
+
return join(realHome$1(), ".context-tree", "trees");
|
|
25313
25487
|
}
|
|
25314
|
-
function
|
|
25315
|
-
return join(
|
|
25488
|
+
function connectionsPath() {
|
|
25489
|
+
return join(realHome$1(), ".context-tree", "connections.json");
|
|
25316
25490
|
}
|
|
25317
|
-
function
|
|
25318
|
-
return
|
|
25319
|
-
links: [],
|
|
25320
|
-
schemaVersion: 1
|
|
25321
|
-
};
|
|
25491
|
+
function managedTreesRoot() {
|
|
25492
|
+
return ensureManagedDirectory(".context-tree", "trees");
|
|
25322
25493
|
}
|
|
25323
|
-
function
|
|
25324
|
-
const path =
|
|
25494
|
+
function loadConnections(required) {
|
|
25495
|
+
const path = connectionsPath();
|
|
25325
25496
|
if (!existsSync(path)) {
|
|
25326
|
-
if (required) throw new
|
|
25327
|
-
return
|
|
25497
|
+
if (required) throw new ContextTreeError(CLI_ERROR_CODES.noConnection, NO_CONNECTION_MESSAGE);
|
|
25498
|
+
return {
|
|
25499
|
+
connections: [],
|
|
25500
|
+
schemaVersion: 1
|
|
25501
|
+
};
|
|
25328
25502
|
}
|
|
25329
25503
|
try {
|
|
25330
25504
|
const entry = lstatSync(path);
|
|
25331
25505
|
if (entry.isSymbolicLink() || !entry.isFile()) throw new Error("not a regular file");
|
|
25332
|
-
return
|
|
25506
|
+
return connectionsFileSchema.parse(JSON.parse(readFileSync(path, "utf8")));
|
|
25333
25507
|
} catch {
|
|
25334
|
-
throw new
|
|
25508
|
+
throw new ContextTreeError(CLI_ERROR_CODES.corruptConnection, "Context Tree connections are corrupt; remove connections.json and run context-tree connect again.");
|
|
25335
25509
|
}
|
|
25336
25510
|
}
|
|
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`, {
|
|
25511
|
+
function saveConnections(value) {
|
|
25512
|
+
const directory = ensureManagedDirectory(".context-tree");
|
|
25513
|
+
const path = join(directory, "connections.json");
|
|
25514
|
+
const temporary = join(directory, `.connections-${process.pid}-${Date.now()}.tmp`);
|
|
25515
|
+
writeFileSync(temporary, `${JSON.stringify(connectionsFileSchema.parse(value), null, 2)}\n`, {
|
|
25348
25516
|
encoding: "utf8",
|
|
25349
25517
|
flag: "wx",
|
|
25350
25518
|
mode: 384
|
|
25351
25519
|
});
|
|
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;
|
|
25520
|
+
try {
|
|
25521
|
+
renameSync(temporary, path);
|
|
25522
|
+
chmodSync(path, 384);
|
|
25523
|
+
} finally {
|
|
25524
|
+
rmSync(temporary, { force: true });
|
|
25525
|
+
}
|
|
25362
25526
|
}
|
|
25363
|
-
|
|
25364
|
-
|
|
25365
|
-
|
|
25366
|
-
|
|
25367
|
-
|
|
25368
|
-
], "Context Tree checkout must have an origin remote."));
|
|
25527
|
+
/** Exactly one record may exist per project; more than one is corruption. */
|
|
25528
|
+
function singleConnection(stored, canonical) {
|
|
25529
|
+
const matches = stored.connections.filter((connection) => connection.projectPath === canonical);
|
|
25530
|
+
if (matches.length > 1) throw new ContextTreeError(CLI_ERROR_CODES.corruptConnection, DUPLICATE_MESSAGE);
|
|
25531
|
+
return matches[0];
|
|
25369
25532
|
}
|
|
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.");
|
|
25533
|
+
function isManagedName(value) {
|
|
25534
|
+
return treeNameSchema.safeParse(value).success && value === value.toLowerCase();
|
|
25391
25535
|
}
|
|
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));
|
|
25536
|
+
function managedName(value) {
|
|
25537
|
+
if (!isManagedName(value)) throw new Error(`Managed Context Tree names must be safe lowercase path segments: ${value}`);
|
|
25538
|
+
return value;
|
|
25397
25539
|
}
|
|
25398
|
-
|
|
25399
|
-
|
|
25400
|
-
|
|
25401
|
-
|
|
25402
|
-
|
|
25540
|
+
/** List valid, clean managed trees, excluding unsafe or invalid candidates without failing the listing. */
|
|
25541
|
+
function listManagedTrees(runner) {
|
|
25542
|
+
const root = managedTreesPath();
|
|
25543
|
+
if (!existsSync(root)) return {
|
|
25544
|
+
schemaVersion: 1,
|
|
25545
|
+
trees: []
|
|
25546
|
+
};
|
|
25547
|
+
const rootEntry = lstatSync(root);
|
|
25548
|
+
if (rootEntry.isSymbolicLink() || !rootEntry.isDirectory()) throw new Error("Context Tree managed directory must be a real directory.");
|
|
25549
|
+
const trees = [];
|
|
25550
|
+
for (const candidate of readdirSync(root, { withFileTypes: true })) {
|
|
25551
|
+
if (!candidate.isDirectory() || !isManagedName(candidate.name)) continue;
|
|
25552
|
+
try {
|
|
25553
|
+
trees.push({
|
|
25554
|
+
name: candidate.name,
|
|
25555
|
+
tree: classifyCheckout(join(root, candidate.name), runner)
|
|
25556
|
+
});
|
|
25557
|
+
} catch {}
|
|
25558
|
+
}
|
|
25559
|
+
trees.sort((left, right) => left.name.localeCompare(right.name));
|
|
25403
25560
|
return {
|
|
25404
|
-
|
|
25405
|
-
|
|
25561
|
+
schemaVersion: 1,
|
|
25562
|
+
trees
|
|
25406
25563
|
};
|
|
25407
25564
|
}
|
|
25408
|
-
function
|
|
25409
|
-
return
|
|
25565
|
+
function findConnectionRecord(projectPath, runner) {
|
|
25566
|
+
return singleConnection(loadConnections(false), canonicalProjectRoot(projectPath, runner));
|
|
25410
25567
|
}
|
|
25411
|
-
function
|
|
25412
|
-
|
|
25413
|
-
if (
|
|
25414
|
-
return
|
|
25568
|
+
function validateManagedTreeState(tree, runner) {
|
|
25569
|
+
const validated = validateStoredTreeState(tree, runner);
|
|
25570
|
+
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)}`);
|
|
25571
|
+
return validated;
|
|
25415
25572
|
}
|
|
25416
|
-
function
|
|
25573
|
+
function resolveConnectionRecord(projectPath, runner) {
|
|
25574
|
+
const canonical = canonicalProjectRoot(projectPath, runner);
|
|
25575
|
+
const connection = singleConnection(loadConnections(true), canonical);
|
|
25576
|
+
if (connection === void 0) throw new ContextTreeError(CLI_ERROR_CODES.noConnection, NO_CONNECTION_MESSAGE);
|
|
25417
25577
|
try {
|
|
25418
|
-
const path = exactCheckoutRoot(link.tree.path);
|
|
25419
25578
|
return {
|
|
25420
|
-
|
|
25421
|
-
|
|
25579
|
+
projectPath: connection.projectPath,
|
|
25580
|
+
tree: validateManagedTreeState(connection.tree, runner)
|
|
25422
25581
|
};
|
|
25423
|
-
} catch {
|
|
25424
|
-
|
|
25582
|
+
} catch (error) {
|
|
25583
|
+
if (error instanceof ContextTreeError) throw error;
|
|
25584
|
+
const detail = error instanceof Error ? error.message : "unknown failure";
|
|
25585
|
+
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
25586
|
}
|
|
25426
25587
|
}
|
|
25427
|
-
function
|
|
25428
|
-
|
|
25429
|
-
|
|
25430
|
-
|
|
25431
|
-
const existing = stored.links.filter((link) => sameProject(link.project, project));
|
|
25432
|
-
if (existing.length > 1) throw new LinkError(CLI_ERROR_CODES.ambiguousLink, "Multiple Context Tree links match this project.");
|
|
25433
|
-
const previous = existing[0];
|
|
25434
|
-
if (previous !== void 0) {
|
|
25435
|
-
if (previous.tree.repository.toLowerCase() !== tree.repository.toLowerCase()) throw new Error("A project cannot link to a different Context Tree repository.");
|
|
25436
|
-
if (previous.tree.path !== tree.path) {
|
|
25437
|
-
const live = liveStoredCheckout(previous);
|
|
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
|
|
25588
|
+
function resolveConnection(projectPath, runner) {
|
|
25589
|
+
return {
|
|
25590
|
+
schemaVersion: 1,
|
|
25591
|
+
tree: resolveConnectionRecord(projectPath, runner).tree
|
|
25444
25592
|
};
|
|
25445
|
-
|
|
25446
|
-
|
|
25593
|
+
}
|
|
25594
|
+
function upsertConnection(connection, runner) {
|
|
25595
|
+
const canonical = canonicalProjectRoot(connection.projectPath, runner);
|
|
25596
|
+
const record = {
|
|
25597
|
+
projectPath: canonical,
|
|
25598
|
+
tree: validateManagedTreeState(contextTreeStateSchema.parse(connection.tree), runner)
|
|
25599
|
+
};
|
|
25600
|
+
const stored = loadConnections(false);
|
|
25601
|
+
const previous = singleConnection(stored, canonical);
|
|
25602
|
+
if (previous !== void 0 && JSON.stringify(previous.tree) === JSON.stringify(record.tree)) return {
|
|
25603
|
+
schemaVersion: 1,
|
|
25604
|
+
tree: record.tree
|
|
25605
|
+
};
|
|
25606
|
+
saveConnections({
|
|
25607
|
+
connections: [...stored.connections.filter((candidate) => candidate.projectPath !== canonical), record],
|
|
25447
25608
|
schemaVersion: 1
|
|
25448
25609
|
});
|
|
25449
25610
|
return {
|
|
25450
|
-
|
|
25611
|
+
schemaVersion: 1,
|
|
25612
|
+
tree: record.tree
|
|
25613
|
+
};
|
|
25614
|
+
}
|
|
25615
|
+
function updateConnectionTree(projectPath, tree, runner) {
|
|
25616
|
+
const canonical = canonicalProjectRoot(projectPath, runner);
|
|
25617
|
+
const stored = loadConnections(true);
|
|
25618
|
+
if (singleConnection(stored, canonical) === void 0) throw new ContextTreeError(CLI_ERROR_CODES.noConnection, NO_CONNECTION_MESSAGE);
|
|
25619
|
+
const validatedTree = validateManagedTreeState(contextTreeStateSchema.parse(tree), runner);
|
|
25620
|
+
saveConnections({
|
|
25621
|
+
connections: stored.connections.map((connection) => connection.projectPath === canonical ? {
|
|
25622
|
+
...connection,
|
|
25623
|
+
tree: validatedTree
|
|
25624
|
+
} : connection),
|
|
25451
25625
|
schemaVersion: 1
|
|
25626
|
+
});
|
|
25627
|
+
}
|
|
25628
|
+
/**
|
|
25629
|
+
* Validate an exact checkout and classify it from a safe origin: no origin
|
|
25630
|
+
* is local state, a credential-free GitHub origin is GitHub state, and any
|
|
25631
|
+
* other origin is rejected as unsafe or unsupported.
|
|
25632
|
+
*/
|
|
25633
|
+
function classifyCheckout(path, runner) {
|
|
25634
|
+
const root = validateTreeCheckout(path, runner);
|
|
25635
|
+
const origin = optionalGit(root, [
|
|
25636
|
+
"remote",
|
|
25637
|
+
"get-url",
|
|
25638
|
+
"origin"
|
|
25639
|
+
], runner);
|
|
25640
|
+
if (origin === void 0) return {
|
|
25641
|
+
kind: "local",
|
|
25642
|
+
path: root
|
|
25643
|
+
};
|
|
25644
|
+
return {
|
|
25645
|
+
kind: "github",
|
|
25646
|
+
path: root,
|
|
25647
|
+
repository: gitHubRepositoryFromOriginUrl(origin)
|
|
25452
25648
|
};
|
|
25453
25649
|
}
|
|
25454
|
-
function
|
|
25455
|
-
return
|
|
25650
|
+
function sameRepository(left, right) {
|
|
25651
|
+
return left.toLowerCase() === right.toLowerCase();
|
|
25456
25652
|
}
|
|
25457
|
-
|
|
25458
|
-
|
|
25653
|
+
/** An existing managed directory that must be a real directory, not a symlinked alias. */
|
|
25654
|
+
function realManagedDirectory(name, destination) {
|
|
25655
|
+
const entry = lstatSync(destination);
|
|
25656
|
+
if (entry.isSymbolicLink() || !entry.isDirectory()) throw new Error(`Managed Context Tree name ${name} is occupied by an unsafe destination.`);
|
|
25459
25657
|
}
|
|
25460
|
-
|
|
25461
|
-
|
|
25462
|
-
|
|
25463
|
-
|
|
25464
|
-
|
|
25465
|
-
|
|
25466
|
-
|
|
25467
|
-
|
|
25468
|
-
|
|
25469
|
-
|
|
25470
|
-
|
|
25471
|
-
if (repository.toLowerCase() !== link.tree.repository.toLowerCase()) throw new Error("The linked path now contains a different Context Tree repository.");
|
|
25472
|
-
parseRootNode(root);
|
|
25658
|
+
/**
|
|
25659
|
+
* Connect by exact managed name or GitHub OWNER/REPO, or attach an exact,
|
|
25660
|
+
* clean, fully valid Git checkout at an explicit disk path in place.
|
|
25661
|
+
*/
|
|
25662
|
+
function connectProject(options, runner) {
|
|
25663
|
+
/** Store the connection, then record it in the project so any agent can find it. */
|
|
25664
|
+
const connect = (tree) => {
|
|
25665
|
+
const result = upsertConnection({
|
|
25666
|
+
projectPath: options.projectPath,
|
|
25667
|
+
tree
|
|
25668
|
+
}, runner);
|
|
25473
25669
|
return {
|
|
25474
|
-
|
|
25475
|
-
|
|
25476
|
-
|
|
25477
|
-
path: root,
|
|
25478
|
-
repository
|
|
25479
|
-
}
|
|
25480
|
-
},
|
|
25481
|
-
schemaVersion: 1
|
|
25670
|
+
pointer: writeProjectPointer(canonicalProjectRoot(options.projectPath, runner), result.tree.path),
|
|
25671
|
+
schemaVersion: 1,
|
|
25672
|
+
tree: result.tree
|
|
25482
25673
|
};
|
|
25674
|
+
};
|
|
25675
|
+
if ("treePath" in options) return connect(classifyCheckout(options.treePath, runner));
|
|
25676
|
+
const treesRoot = managedTreesRoot();
|
|
25677
|
+
if (!options.target.includes("/")) {
|
|
25678
|
+
const name = managedName(options.target);
|
|
25679
|
+
const destination = join(treesRoot, name);
|
|
25680
|
+
if (!existsSync(destination)) throw new Error(`No managed Context Tree named ${name} exists.`);
|
|
25681
|
+
realManagedDirectory(name, destination);
|
|
25682
|
+
return connect(classifyCheckout(destination, runner));
|
|
25683
|
+
}
|
|
25684
|
+
const repository = githubRepositoryIdentitySchema.parse(options.target);
|
|
25685
|
+
const repositoryName = repository.split("/")[1];
|
|
25686
|
+
if (repositoryName === void 0) throw new Error("Repository must be OWNER/REPO.");
|
|
25687
|
+
const name = managedName(repositoryName.toLowerCase());
|
|
25688
|
+
const destination = join(treesRoot, name);
|
|
25689
|
+
if (existsSync(destination)) {
|
|
25690
|
+
realManagedDirectory(name, destination);
|
|
25691
|
+
const tree = classifyCheckout(destination, runner);
|
|
25692
|
+
if (tree.kind !== "github" || !sameRepository(tree.repository, repository)) throw new Error(`Managed Context Tree name ${name} is already used by a different tree.`);
|
|
25693
|
+
return connect(tree);
|
|
25694
|
+
}
|
|
25695
|
+
mkdirSync(destination, { mode: 448 });
|
|
25696
|
+
try {
|
|
25697
|
+
git(treesRoot, [
|
|
25698
|
+
"clone",
|
|
25699
|
+
"--quiet",
|
|
25700
|
+
"--origin",
|
|
25701
|
+
"origin",
|
|
25702
|
+
"--",
|
|
25703
|
+
canonicalGitHubRepositoryUrl(repository),
|
|
25704
|
+
destination
|
|
25705
|
+
], {
|
|
25706
|
+
message: "Cloning the Context Tree repository failed.",
|
|
25707
|
+
runner
|
|
25708
|
+
});
|
|
25709
|
+
const tree = classifyCheckout(destination, runner);
|
|
25710
|
+
if (tree.kind !== "github" || !sameRepository(tree.repository, repository)) throw new Error("The cloned Context Tree origin does not match the requested repository.");
|
|
25711
|
+
return connect(tree);
|
|
25483
25712
|
} catch (error) {
|
|
25484
|
-
|
|
25485
|
-
|
|
25713
|
+
rmSync(destination, {
|
|
25714
|
+
force: true,
|
|
25715
|
+
recursive: true
|
|
25716
|
+
});
|
|
25717
|
+
throw error;
|
|
25486
25718
|
}
|
|
25487
25719
|
}
|
|
25488
25720
|
//#endregion
|
|
25489
|
-
//#region src/core/
|
|
25490
|
-
|
|
25491
|
-
|
|
25492
|
-
|
|
25493
|
-
|
|
25494
|
-
|
|
25721
|
+
//#region src/core/internal/packaged-resource.ts
|
|
25722
|
+
const PACKAGE_NAME = "@first-tree-ai/context-tree";
|
|
25723
|
+
function isPackageRoot(path) {
|
|
25724
|
+
const manifestPath = join(path, "package.json");
|
|
25725
|
+
if (!existsSync(manifestPath)) return false;
|
|
25726
|
+
try {
|
|
25727
|
+
const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
|
|
25728
|
+
return isRecord(manifest) && manifest.name === PACKAGE_NAME;
|
|
25729
|
+
} catch {
|
|
25730
|
+
return false;
|
|
25731
|
+
}
|
|
25732
|
+
}
|
|
25733
|
+
function resolvePackagedResource(...segments) {
|
|
25734
|
+
let candidate = dirname(fileURLToPath(import.meta.url));
|
|
25735
|
+
const filesystemRoot = parse(candidate).root;
|
|
25736
|
+
while (true) {
|
|
25737
|
+
if (isPackageRoot(candidate)) {
|
|
25738
|
+
const resource = resolve(candidate, ...segments);
|
|
25739
|
+
if (existsSync(resource)) return resource;
|
|
25740
|
+
throw new Error(`Packaged resource is missing: ${segments.join("/")}`);
|
|
25741
|
+
}
|
|
25742
|
+
if (candidate === filesystemRoot) break;
|
|
25743
|
+
candidate = dirname(candidate);
|
|
25744
|
+
}
|
|
25745
|
+
throw new Error(`Package root is missing while resolving: ${segments.join("/")}`);
|
|
25746
|
+
}
|
|
25747
|
+
function readPackageManifest() {
|
|
25748
|
+
const parsed = JSON.parse(readFileSync(resolvePackagedResource("package.json"), "utf8"));
|
|
25749
|
+
if (!isRecord(parsed)) throw new Error("Package metadata is invalid.");
|
|
25750
|
+
return parsed;
|
|
25751
|
+
}
|
|
25752
|
+
function readPackageVersion() {
|
|
25753
|
+
const manifest = readPackageManifest();
|
|
25754
|
+
if (typeof manifest.version !== "string") throw new Error("Package version is missing or invalid.");
|
|
25755
|
+
return manifest.version;
|
|
25756
|
+
}
|
|
25757
|
+
//#endregion
|
|
25758
|
+
//#region src/core/scaffold.ts
|
|
25759
|
+
function template(name, values) {
|
|
25760
|
+
let result = readFileSync(resolvePackagedResource("templates", name), "utf8");
|
|
25761
|
+
for (const [key, value] of Object.entries(values)) result = result.replaceAll(`{{${key}}}`, value);
|
|
25762
|
+
return result;
|
|
25763
|
+
}
|
|
25764
|
+
/** Templated regular files, written before the CLAUDE.md -> AGENTS.md symlink. */
|
|
25765
|
+
const TEMPLATED_FILES = [
|
|
25766
|
+
["NODE.md", "root-node.md"],
|
|
25767
|
+
["AGENTS.md", "AGENTS.md"],
|
|
25768
|
+
[".github/workflows/validate-context-tree.yml", "validate-context-tree.yml"]
|
|
25769
|
+
];
|
|
25770
|
+
const SCAFFOLD_FILES = [
|
|
25771
|
+
"NODE.md",
|
|
25772
|
+
"AGENTS.md",
|
|
25773
|
+
"CLAUDE.md",
|
|
25774
|
+
".github/workflows/validate-context-tree.yml"
|
|
25775
|
+
];
|
|
25776
|
+
function initializeGitRepository(root, runner) {
|
|
25777
|
+
gitCommand([
|
|
25778
|
+
"init",
|
|
25779
|
+
"--quiet",
|
|
25780
|
+
"--",
|
|
25781
|
+
root
|
|
25495
25782
|
], {
|
|
25496
|
-
|
|
25497
|
-
|
|
25498
|
-
"ignore",
|
|
25499
|
-
"pipe",
|
|
25500
|
-
"ignore"
|
|
25501
|
-
]
|
|
25783
|
+
message: "Failed to initialize Git repository.",
|
|
25784
|
+
runner
|
|
25502
25785
|
});
|
|
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, [
|
|
25786
|
+
return git(root, [
|
|
25526
25787
|
"symbolic-ref",
|
|
25527
25788
|
"--short",
|
|
25528
25789
|
"HEAD"
|
|
25529
|
-
]
|
|
25530
|
-
|
|
25531
|
-
|
|
25532
|
-
|
|
25533
|
-
"--ff-only",
|
|
25534
|
-
"origin",
|
|
25535
|
-
defaultBranch
|
|
25536
|
-
]);
|
|
25537
|
-
const after = requireGit(root, ["rev-parse", "HEAD"]);
|
|
25538
|
-
return {
|
|
25539
|
-
link: {
|
|
25540
|
-
...result.link,
|
|
25541
|
-
tree: {
|
|
25542
|
-
...result.link.tree,
|
|
25543
|
-
path: realpathSync(root)
|
|
25544
|
-
}
|
|
25545
|
-
},
|
|
25546
|
-
defaultBranch,
|
|
25547
|
-
refreshed: before !== after,
|
|
25548
|
-
schemaVersion: 1,
|
|
25549
|
-
sha: after
|
|
25550
|
-
};
|
|
25790
|
+
], {
|
|
25791
|
+
message: "Failed to resolve the initial Git branch during repository initialization.",
|
|
25792
|
+
runner
|
|
25793
|
+
});
|
|
25551
25794
|
}
|
|
25552
|
-
function
|
|
25553
|
-
const
|
|
25554
|
-
const taskBranch = `context-tree/write/${basename(destination)}`;
|
|
25555
|
-
git(treePath, [
|
|
25556
|
-
"worktree",
|
|
25795
|
+
function commitScaffold(root, runner) {
|
|
25796
|
+
for (const file of SCAFFOLD_FILES) git(root, [
|
|
25557
25797
|
"add",
|
|
25558
|
-
"
|
|
25559
|
-
|
|
25560
|
-
|
|
25561
|
-
|
|
25562
|
-
|
|
25798
|
+
"--",
|
|
25799
|
+
file
|
|
25800
|
+
], {
|
|
25801
|
+
message: "Failed to stage the scaffold files.",
|
|
25802
|
+
runner
|
|
25803
|
+
});
|
|
25804
|
+
git(root, [
|
|
25805
|
+
"-c",
|
|
25806
|
+
"user.name=Context Tree",
|
|
25807
|
+
"-c",
|
|
25808
|
+
"user.email=context-tree@localhost",
|
|
25809
|
+
"-c",
|
|
25810
|
+
"commit.gpgsign=false",
|
|
25811
|
+
"commit",
|
|
25812
|
+
"--quiet",
|
|
25813
|
+
"-m",
|
|
25814
|
+
"Initialize Context Tree"
|
|
25815
|
+
], {
|
|
25816
|
+
message: "Failed to commit the scaffold.",
|
|
25817
|
+
runner
|
|
25818
|
+
});
|
|
25819
|
+
return git(root, ["rev-parse", "HEAD"], {
|
|
25820
|
+
message: "Failed to resolve the scaffold commit.",
|
|
25821
|
+
runner
|
|
25822
|
+
});
|
|
25823
|
+
}
|
|
25824
|
+
function scaffoldTree(options) {
|
|
25825
|
+
const name = treeNameSchema.parse(options.name);
|
|
25826
|
+
const root = resolve(options.path);
|
|
25827
|
+
const destination = lstatSync(root, { throwIfNoEntry: false });
|
|
25828
|
+
if (destination !== void 0) {
|
|
25829
|
+
if (destination.isSymbolicLink() || !destination.isDirectory()) throw new Error(`Refusing to scaffold into a symlink or non-directory destination: ${root}`);
|
|
25830
|
+
if (readdirSync(root).length > 0) throw new Error(`Refusing to scaffold into a non-empty directory: ${root}`);
|
|
25831
|
+
}
|
|
25832
|
+
const initialBranch = initializeGitRepository(root, options.runner);
|
|
25833
|
+
const values = {
|
|
25834
|
+
branchJson: JSON.stringify(initialBranch),
|
|
25835
|
+
packageVersion: readPackageVersion(),
|
|
25836
|
+
title: name,
|
|
25837
|
+
titleJson: JSON.stringify(name)
|
|
25838
|
+
};
|
|
25839
|
+
for (const [relativePath, source] of TEMPLATED_FILES) {
|
|
25840
|
+
const path = join(root, relativePath);
|
|
25841
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
25842
|
+
writeFileSync(path, template(source, values), {
|
|
25843
|
+
encoding: "utf8",
|
|
25844
|
+
flag: "wx",
|
|
25845
|
+
mode: 420
|
|
25846
|
+
});
|
|
25847
|
+
}
|
|
25848
|
+
symlinkSync("AGENTS.md", join(root, "CLAUDE.md"), "file");
|
|
25849
|
+
if (!verifyTree(root).ok) throw new Error("Refusing to commit an invalid Context Tree scaffold.");
|
|
25563
25850
|
return {
|
|
25564
|
-
|
|
25565
|
-
|
|
25851
|
+
branch: initialBranch,
|
|
25852
|
+
commit: commitScaffold(root, options.runner),
|
|
25853
|
+
root
|
|
25566
25854
|
};
|
|
25567
25855
|
}
|
|
25568
|
-
|
|
25569
|
-
|
|
25570
|
-
|
|
25571
|
-
const
|
|
25572
|
-
|
|
25573
|
-
|
|
25574
|
-
|
|
25575
|
-
"--short",
|
|
25576
|
-
"HEAD"
|
|
25577
|
-
]) !== defaultBranch) throw new Error(`The Context Tree checkout must be on the live default branch "${defaultBranch}".`);
|
|
25578
|
-
requireGit(treePath, [
|
|
25579
|
-
"fetch",
|
|
25580
|
-
"origin",
|
|
25581
|
-
defaultBranch
|
|
25582
|
-
], true);
|
|
25583
|
-
const baseSha = requireGit(treePath, ["rev-parse", `origin/${defaultBranch}`]);
|
|
25584
|
-
const { taskBranch, worktreePath } = createIsolatedWorktree(treePath, baseSha);
|
|
25856
|
+
//#endregion
|
|
25857
|
+
//#region src/core/create.ts
|
|
25858
|
+
function projectName(canonicalRoot) {
|
|
25859
|
+
const normalized = basename(canonicalRoot).toLowerCase().replace(/[^a-z\d._-]+/gu, "-").replace(/-{2,}/gu, "-").replace(/^[-.]+/u, "").replace(/[-.]+$/u, "").slice(0, 40);
|
|
25860
|
+
return /^[a-z\d]/u.test(normalized) ? normalized : "project";
|
|
25861
|
+
}
|
|
25862
|
+
function existingCreateResult(canonical, destination, runner) {
|
|
25585
25863
|
return {
|
|
25586
|
-
|
|
25587
|
-
|
|
25588
|
-
|
|
25589
|
-
|
|
25590
|
-
|
|
25591
|
-
|
|
25592
|
-
|
|
25593
|
-
},
|
|
25594
|
-
|
|
25864
|
+
branch: git(destination, [
|
|
25865
|
+
"symbolic-ref",
|
|
25866
|
+
"--short",
|
|
25867
|
+
"HEAD"
|
|
25868
|
+
], {
|
|
25869
|
+
message: "Failed to resolve the managed tree branch.",
|
|
25870
|
+
runner
|
|
25871
|
+
}),
|
|
25872
|
+
commitSha: git(destination, ["rev-parse", "HEAD"], {
|
|
25873
|
+
message: "Failed to resolve the managed tree commit.",
|
|
25874
|
+
runner
|
|
25875
|
+
}),
|
|
25876
|
+
created: false,
|
|
25877
|
+
pointer: writeProjectPointer(canonical, destination),
|
|
25595
25878
|
schemaVersion: 1,
|
|
25596
|
-
|
|
25597
|
-
|
|
25879
|
+
title: readRootNode(destination).frontmatter.title,
|
|
25880
|
+
treePath: destination
|
|
25598
25881
|
};
|
|
25599
25882
|
}
|
|
25600
|
-
|
|
25601
|
-
|
|
25602
|
-
|
|
25603
|
-
|
|
25604
|
-
|
|
25605
|
-
|
|
25606
|
-
|
|
25607
|
-
|
|
25608
|
-
|
|
25609
|
-
|
|
25610
|
-
|
|
25611
|
-
|
|
25612
|
-
|
|
25613
|
-
|
|
25614
|
-
const
|
|
25615
|
-
|
|
25616
|
-
|
|
25617
|
-
|
|
25618
|
-
files.push({
|
|
25619
|
-
path: match[2] ?? "",
|
|
25620
|
-
status
|
|
25883
|
+
/** Create and connect the project's uniquely named managed local Context Tree. */
|
|
25884
|
+
function createProject(projectPath, runner) {
|
|
25885
|
+
const canonical = canonicalProjectRoot(projectPath, runner);
|
|
25886
|
+
const name = treeNameSchema.parse(`${projectName(canonical)}-context-tree`);
|
|
25887
|
+
const destination = join(managedTreesRoot(), name);
|
|
25888
|
+
const current = findConnectionRecord(canonical, runner);
|
|
25889
|
+
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.`);
|
|
25890
|
+
if (existsSync(destination)) {
|
|
25891
|
+
const entry = lstatSync(destination);
|
|
25892
|
+
if (entry.isSymbolicLink() || !entry.isDirectory() || current === void 0) throw new Error(`Managed Context Tree name ${name} is occupied; run context-tree connect ${name}.`);
|
|
25893
|
+
return existingCreateResult(canonical, destination, runner);
|
|
25894
|
+
}
|
|
25895
|
+
mkdirSync(destination, { mode: 448 });
|
|
25896
|
+
try {
|
|
25897
|
+
const scaffold = scaffoldTree({
|
|
25898
|
+
name,
|
|
25899
|
+
path: destination,
|
|
25900
|
+
runner
|
|
25621
25901
|
});
|
|
25902
|
+
upsertConnection({
|
|
25903
|
+
projectPath: canonical,
|
|
25904
|
+
tree: {
|
|
25905
|
+
kind: "local",
|
|
25906
|
+
path: scaffold.root
|
|
25907
|
+
}
|
|
25908
|
+
}, runner);
|
|
25909
|
+
return {
|
|
25910
|
+
branch: scaffold.branch,
|
|
25911
|
+
commitSha: scaffold.commit,
|
|
25912
|
+
created: true,
|
|
25913
|
+
pointer: writeProjectPointer(canonical, scaffold.root),
|
|
25914
|
+
schemaVersion: 1,
|
|
25915
|
+
title: name,
|
|
25916
|
+
treePath: scaffold.root
|
|
25917
|
+
};
|
|
25918
|
+
} catch (error) {
|
|
25919
|
+
rmSync(destination, {
|
|
25920
|
+
force: true,
|
|
25921
|
+
recursive: true
|
|
25922
|
+
});
|
|
25923
|
+
throw error;
|
|
25622
25924
|
}
|
|
25623
|
-
|
|
25624
|
-
|
|
25625
|
-
|
|
25626
|
-
|
|
25627
|
-
|
|
25628
|
-
|
|
25629
|
-
|
|
25630
|
-
|
|
25631
|
-
|
|
25632
|
-
|
|
25925
|
+
}
|
|
25926
|
+
//#endregion
|
|
25927
|
+
//#region src/core/install.ts
|
|
25928
|
+
/** Per-host configuration directory, relative to the home directory or to a project root. */
|
|
25929
|
+
const HOST_CONFIG_DIRECTORY = {
|
|
25930
|
+
claude: ".claude",
|
|
25931
|
+
codex: ".codex"
|
|
25932
|
+
};
|
|
25933
|
+
/** Every supported host keeps user skills in the same subdirectory of its configuration directory. */
|
|
25934
|
+
const SKILLS_DIRECTORY = "skills";
|
|
25935
|
+
/** Only directories carrying this prefix are ever replaced or removed. */
|
|
25936
|
+
const OWNED_SKILL_PREFIX = "context-tree-";
|
|
25937
|
+
function realHome() {
|
|
25938
|
+
try {
|
|
25939
|
+
return realpathSync(homedir());
|
|
25940
|
+
} catch {
|
|
25941
|
+
return homedir();
|
|
25942
|
+
}
|
|
25943
|
+
}
|
|
25944
|
+
/** Create a directory below `root`, failing closed on symlinks and non-directories. */
|
|
25945
|
+
function ensureRealDirectory(root, segments) {
|
|
25946
|
+
let current = root;
|
|
25947
|
+
for (const segment of segments) {
|
|
25948
|
+
current = join(current, segment);
|
|
25949
|
+
const entry = lstatSync(current, { throwIfNoEntry: false });
|
|
25950
|
+
if (entry === void 0) {
|
|
25951
|
+
mkdirSync(current, { mode: 448 });
|
|
25952
|
+
continue;
|
|
25953
|
+
}
|
|
25954
|
+
if (entry.isSymbolicLink() || !entry.isDirectory()) throw new Error(`Context Tree skill directory must be a real directory: ${current}`);
|
|
25955
|
+
}
|
|
25956
|
+
return current;
|
|
25957
|
+
}
|
|
25958
|
+
/** Copy a packaged skill tree, refusing to read or write through symlinks. Skills are never executable. */
|
|
25959
|
+
function copyRealTree(source, destination) {
|
|
25960
|
+
const entry = lstatSync(source);
|
|
25961
|
+
if (entry.isSymbolicLink()) throw new Error(`Refusing to install a symlinked skill entry: ${source}`);
|
|
25962
|
+
if (entry.isDirectory()) {
|
|
25963
|
+
mkdirSync(destination, {
|
|
25964
|
+
mode: 448,
|
|
25965
|
+
recursive: true
|
|
25633
25966
|
});
|
|
25967
|
+
for (const child of readdirSync(source)) copyRealTree(join(source, child), join(destination, child));
|
|
25968
|
+
return;
|
|
25969
|
+
}
|
|
25970
|
+
if (!entry.isFile()) throw new Error(`Refusing to install a non-regular skill entry: ${source}`);
|
|
25971
|
+
copyFileSync(source, destination);
|
|
25972
|
+
chmodSync(destination, 420);
|
|
25973
|
+
}
|
|
25974
|
+
/** Packaged skill directory names, e.g. `context-tree-read`. */
|
|
25975
|
+
function packagedSkillNames(skillsRoot) {
|
|
25976
|
+
return readdirSync(skillsRoot, { withFileTypes: true }).filter((entry) => entry.isDirectory() && entry.name.startsWith(OWNED_SKILL_PREFIX)).map((entry) => entry.name).sort();
|
|
25977
|
+
}
|
|
25978
|
+
/** Resolve one host's destination, or the reason it was skipped. */
|
|
25979
|
+
function hostDestination(host, root, isProjectInstall) {
|
|
25980
|
+
const configDirectory = HOST_CONFIG_DIRECTORY[host];
|
|
25981
|
+
if (!isProjectInstall) {
|
|
25982
|
+
const hostRoot = join(root, configDirectory);
|
|
25983
|
+
const entry = lstatSync(hostRoot, { throwIfNoEntry: false });
|
|
25984
|
+
if (entry === void 0) return { reason: `${hostRoot} does not exist; install ${host} first, then run context-tree install.` };
|
|
25985
|
+
if (entry.isSymbolicLink() || !entry.isDirectory()) return { reason: `${hostRoot} is not a real directory.` };
|
|
25634
25986
|
}
|
|
25635
|
-
return
|
|
25987
|
+
return { destination: ensureRealDirectory(root, [configDirectory, SKILLS_DIRECTORY]) };
|
|
25636
25988
|
}
|
|
25637
|
-
|
|
25638
|
-
|
|
25639
|
-
|
|
25989
|
+
/**
|
|
25990
|
+
* Copy the packaged skills into each requested host's skill directory.
|
|
25991
|
+
*
|
|
25992
|
+
* Existing `context-tree-*` directories are replaced; that is the upgrade path. Skill
|
|
25993
|
+
* directories the package does not own are never touched. A project install always
|
|
25994
|
+
* creates its target, because the caller named it.
|
|
25995
|
+
*/
|
|
25996
|
+
function installSkills(options = {}) {
|
|
25997
|
+
const skillsRoot = resolvePackagedResource("skills");
|
|
25998
|
+
const skills = packagedSkillNames(skillsRoot);
|
|
25999
|
+
if (skills.length === 0) throw new Error("Packaged skills are missing; reinstall @first-tree-ai/context-tree.");
|
|
26000
|
+
const hosts = options.hosts === void 0 || options.hosts.length === 0 ? SKILL_HOSTS : options.hosts;
|
|
26001
|
+
const projectRoot = options.projectPath === void 0 ? void 0 : resolve(options.projectPath);
|
|
26002
|
+
const root = projectRoot ?? realHome();
|
|
26003
|
+
const installed = [];
|
|
26004
|
+
const skipped = [];
|
|
26005
|
+
for (const host of hosts) {
|
|
26006
|
+
const resolved = hostDestination(host, root, projectRoot !== void 0);
|
|
26007
|
+
if ("reason" in resolved) {
|
|
26008
|
+
skipped.push({
|
|
26009
|
+
host,
|
|
26010
|
+
reason: resolved.reason
|
|
26011
|
+
});
|
|
26012
|
+
continue;
|
|
26013
|
+
}
|
|
26014
|
+
for (const skill of skills) {
|
|
26015
|
+
const target = join(resolved.destination, skill);
|
|
26016
|
+
if (lstatSync(target, { throwIfNoEntry: false }) !== void 0) rmSync(target, {
|
|
26017
|
+
force: true,
|
|
26018
|
+
recursive: true
|
|
26019
|
+
});
|
|
26020
|
+
copyRealTree(join(skillsRoot, skill), target);
|
|
26021
|
+
}
|
|
26022
|
+
installed.push({
|
|
26023
|
+
host,
|
|
26024
|
+
path: resolved.destination,
|
|
26025
|
+
skills
|
|
26026
|
+
});
|
|
26027
|
+
}
|
|
25640
26028
|
return {
|
|
25641
|
-
|
|
25642
|
-
files: changedFiles(root, reference),
|
|
25643
|
-
patch: git(root, ["diff", reference]),
|
|
26029
|
+
installed,
|
|
25644
26030
|
schemaVersion: 1,
|
|
25645
|
-
|
|
26031
|
+
skipped,
|
|
26032
|
+
version: readPackageVersion()
|
|
25646
26033
|
};
|
|
25647
26034
|
}
|
|
25648
26035
|
//#endregion
|
|
25649
|
-
//#region src/core/
|
|
25650
|
-
function
|
|
26036
|
+
//#region src/core/publish.ts
|
|
26037
|
+
function authenticatedAccount(runner) {
|
|
26038
|
+
let login;
|
|
26039
|
+
try {
|
|
26040
|
+
login = gh([
|
|
26041
|
+
"api",
|
|
26042
|
+
"user",
|
|
26043
|
+
"--jq",
|
|
26044
|
+
".login"
|
|
26045
|
+
], {
|
|
26046
|
+
message: "GitHub account lookup failed.",
|
|
26047
|
+
runner
|
|
26048
|
+
});
|
|
26049
|
+
} catch (error) {
|
|
26050
|
+
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.");
|
|
26051
|
+
throw new ContextTreeError(CLI_ERROR_CODES.publishIncomplete, "GitHub account lookup failed; publication did not start and must not be retried automatically.");
|
|
26052
|
+
}
|
|
26053
|
+
if (login.trim().length === 0) throw new ContextTreeError(CLI_ERROR_CODES.publishIncomplete, "GitHub account lookup returned no repository owner.");
|
|
26054
|
+
return login.trim();
|
|
26055
|
+
}
|
|
26056
|
+
function classifyCreationFailure(stderr) {
|
|
26057
|
+
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.");
|
|
26058
|
+
return new ContextTreeError(CLI_ERROR_CODES.publishIncomplete, "GitHub repository creation has an uncertain or partial result; do not retry automatically.");
|
|
26059
|
+
}
|
|
26060
|
+
/**
|
|
26061
|
+
* Publish a clean, valid local tree as a private GitHub repository. The
|
|
26062
|
+
* default repository name derives from the authenticated account and managed
|
|
26063
|
+
* tree name; OWNER/REPO is accepted only as an explicit override. The initial
|
|
26064
|
+
* publication is one gh repo create operation, and the stored connection is
|
|
26065
|
+
* updated atomically to the published tree state.
|
|
26066
|
+
*/
|
|
26067
|
+
function publishProject(projectPath, options = {}, runner) {
|
|
26068
|
+
const connection = resolveConnectionRecord(projectPath, runner);
|
|
26069
|
+
const root = connection.tree.path;
|
|
26070
|
+
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.`);
|
|
26071
|
+
if (optionalGit(root, [
|
|
26072
|
+
"remote",
|
|
26073
|
+
"get-url",
|
|
26074
|
+
"origin"
|
|
26075
|
+
], runner) !== void 0) throw new ContextTreeError(CLI_ERROR_CODES.failed, "A local Context Tree must not already have an origin before publication.");
|
|
26076
|
+
const branch = git(root, [
|
|
26077
|
+
"symbolic-ref",
|
|
26078
|
+
"--short",
|
|
26079
|
+
"HEAD"
|
|
26080
|
+
], {
|
|
26081
|
+
message: "Failed to resolve the checked-out branch.",
|
|
26082
|
+
runner
|
|
26083
|
+
});
|
|
26084
|
+
const sha = git(root, ["rev-parse", "HEAD"], {
|
|
26085
|
+
message: "Failed to resolve the Context Tree commit.",
|
|
26086
|
+
runner
|
|
26087
|
+
});
|
|
26088
|
+
const repository = options.repository === void 0 ? `${authenticatedAccount(runner)}/${basename(root)}` : githubRepositoryIdentitySchema.parse(options.repository);
|
|
26089
|
+
const url = canonicalGitHubRepositoryUrl(repository);
|
|
26090
|
+
try {
|
|
26091
|
+
gh([
|
|
26092
|
+
"repo",
|
|
26093
|
+
"create",
|
|
26094
|
+
repository,
|
|
26095
|
+
"--private",
|
|
26096
|
+
"--source",
|
|
26097
|
+
root,
|
|
26098
|
+
"--remote",
|
|
26099
|
+
"origin",
|
|
26100
|
+
"--push"
|
|
26101
|
+
], {
|
|
26102
|
+
message: "GitHub repository creation failed.",
|
|
26103
|
+
runner
|
|
26104
|
+
});
|
|
26105
|
+
} catch (error) {
|
|
26106
|
+
if (error instanceof CommandError) throw classifyCreationFailure(error.stderr);
|
|
26107
|
+
throw new ContextTreeError(CLI_ERROR_CODES.publishIncomplete, "GitHub publication ended with an uncertain result.");
|
|
26108
|
+
}
|
|
26109
|
+
try {
|
|
26110
|
+
updateConnectionTree(connection.projectPath, {
|
|
26111
|
+
kind: "github",
|
|
26112
|
+
path: root,
|
|
26113
|
+
repository
|
|
26114
|
+
}, runner);
|
|
26115
|
+
} catch {
|
|
26116
|
+
throw new ContextTreeError(CLI_ERROR_CODES.publishIncomplete, "The private repository was created, but updating the local connection failed.");
|
|
26117
|
+
}
|
|
25651
26118
|
return {
|
|
25652
|
-
|
|
25653
|
-
|
|
26119
|
+
branch,
|
|
26120
|
+
repository,
|
|
26121
|
+
schemaVersion: 1,
|
|
26122
|
+
sha,
|
|
26123
|
+
url
|
|
25654
26124
|
};
|
|
25655
26125
|
}
|
|
25656
26126
|
//#endregion
|
|
@@ -25724,98 +26194,271 @@ function readTree(treePath, path) {
|
|
|
25724
26194
|
};
|
|
25725
26195
|
}
|
|
25726
26196
|
//#endregion
|
|
25727
|
-
//#region src/core/
|
|
25728
|
-
|
|
25729
|
-
|
|
25730
|
-
|
|
25731
|
-
|
|
25732
|
-
|
|
25733
|
-
|
|
25734
|
-
const
|
|
25735
|
-
|
|
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,
|
|
26197
|
+
//#region src/core/sync.ts
|
|
26198
|
+
/**
|
|
26199
|
+
* Local trees report their checked-out state without network access. GitHub
|
|
26200
|
+
* trees fast-forward the exact checked-out branch once, then revalidate.
|
|
26201
|
+
*/
|
|
26202
|
+
function syncProject(projectPath, runner) {
|
|
26203
|
+
const connection = resolveConnectionRecord(projectPath, runner);
|
|
26204
|
+
const root = connection.tree.path;
|
|
26205
|
+
const branch = git(root, [
|
|
25743
26206
|
"symbolic-ref",
|
|
25744
26207
|
"--short",
|
|
25745
26208
|
"HEAD"
|
|
25746
26209
|
], {
|
|
25747
|
-
|
|
25748
|
-
|
|
25749
|
-
"ignore",
|
|
25750
|
-
"pipe",
|
|
25751
|
-
"ignore"
|
|
25752
|
-
]
|
|
26210
|
+
message: "Failed to resolve the checked-out branch.",
|
|
26211
|
+
runner
|
|
25753
26212
|
});
|
|
25754
|
-
|
|
25755
|
-
|
|
25756
|
-
|
|
25757
|
-
|
|
25758
|
-
|
|
25759
|
-
|
|
25760
|
-
|
|
25761
|
-
|
|
25762
|
-
|
|
25763
|
-
|
|
25764
|
-
|
|
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}`);
|
|
26213
|
+
if (connection.tree.kind === "github") {
|
|
26214
|
+
git(root, [
|
|
26215
|
+
"pull",
|
|
26216
|
+
"--ff-only",
|
|
26217
|
+
"origin",
|
|
26218
|
+
branch
|
|
26219
|
+
], {
|
|
26220
|
+
message: "Fast-forwarding the Context Tree failed.",
|
|
26221
|
+
runner
|
|
26222
|
+
});
|
|
26223
|
+
validateStoredTreeState(connection.tree, runner);
|
|
25774
26224
|
}
|
|
25775
|
-
|
|
25776
|
-
|
|
25777
|
-
|
|
25778
|
-
|
|
25779
|
-
|
|
25780
|
-
|
|
26225
|
+
return {
|
|
26226
|
+
branch,
|
|
26227
|
+
schemaVersion: 1,
|
|
26228
|
+
sha: git(root, ["rev-parse", "HEAD"], {
|
|
26229
|
+
message: "Failed to resolve the Context Tree commit.",
|
|
26230
|
+
runner
|
|
26231
|
+
}),
|
|
26232
|
+
tree: connection.tree
|
|
25781
26233
|
};
|
|
25782
|
-
|
|
25783
|
-
|
|
25784
|
-
|
|
25785
|
-
|
|
25786
|
-
|
|
25787
|
-
|
|
25788
|
-
|
|
25789
|
-
|
|
25790
|
-
|
|
25791
|
-
|
|
25792
|
-
|
|
25793
|
-
|
|
25794
|
-
|
|
25795
|
-
|
|
25796
|
-
|
|
25797
|
-
|
|
25798
|
-
|
|
25799
|
-
|
|
26234
|
+
}
|
|
26235
|
+
//#endregion
|
|
26236
|
+
//#region src/core/write.ts
|
|
26237
|
+
const TASK_BRANCH_PREFIX = "context-tree/write/";
|
|
26238
|
+
/** A prepared worktree left untouched for longer than this is treated as abandoned. */
|
|
26239
|
+
const ABANDONED_WRITE_AGE_MS = 1440 * 60 * 1e3;
|
|
26240
|
+
/** Synchronize first, then create an isolated task worktree at the exact HEAD. */
|
|
26241
|
+
function prepareContextWrite(projectPath, runner) {
|
|
26242
|
+
const synchronized = syncProject(projectPath, runner);
|
|
26243
|
+
const root = synchronized.tree.path;
|
|
26244
|
+
reclaimAbandonedWrites(root, synchronized.branch, runner);
|
|
26245
|
+
const destination = mkdtempSync(join(tmpdir(), "context-tree-write-"));
|
|
26246
|
+
const taskBranch = `${TASK_BRANCH_PREFIX}${basename(destination)}`;
|
|
26247
|
+
try {
|
|
26248
|
+
git(root, [
|
|
26249
|
+
"worktree",
|
|
26250
|
+
"add",
|
|
26251
|
+
"--quiet",
|
|
26252
|
+
"-b",
|
|
26253
|
+
taskBranch,
|
|
26254
|
+
destination,
|
|
26255
|
+
synchronized.sha
|
|
26256
|
+
], {
|
|
26257
|
+
message: "Creating the isolated write worktree failed.",
|
|
26258
|
+
runner
|
|
26259
|
+
});
|
|
26260
|
+
return {
|
|
26261
|
+
schemaVersion: 1,
|
|
26262
|
+
worktreePath: realDirectoryWithoutSymlinks(destination, "Write worktree")
|
|
26263
|
+
};
|
|
26264
|
+
} catch (error) {
|
|
26265
|
+
rmSync(destination, {
|
|
26266
|
+
force: true,
|
|
26267
|
+
recursive: true
|
|
25800
26268
|
});
|
|
26269
|
+
throw error;
|
|
25801
26270
|
}
|
|
25802
|
-
|
|
25803
|
-
|
|
25804
|
-
|
|
25805
|
-
|
|
25806
|
-
|
|
25807
|
-
|
|
25808
|
-
|
|
25809
|
-
|
|
26271
|
+
}
|
|
26272
|
+
/** Commit every pending change, then fast-forward locally or push once. */
|
|
26273
|
+
function finishContextWrite(options, runner) {
|
|
26274
|
+
const connection = resolveConnectionRecord(options.projectPath, runner);
|
|
26275
|
+
const root = connection.tree.path;
|
|
26276
|
+
const { taskBranch, worktreePath } = validatePreparedWorktree(root, options.worktreePath, runner);
|
|
26277
|
+
const branch = git(root, [
|
|
26278
|
+
"symbolic-ref",
|
|
26279
|
+
"--short",
|
|
26280
|
+
"HEAD"
|
|
26281
|
+
], {
|
|
26282
|
+
message: "Failed to resolve the connected checkout branch.",
|
|
26283
|
+
runner
|
|
26284
|
+
});
|
|
26285
|
+
if (git(worktreePath, [
|
|
26286
|
+
"status",
|
|
26287
|
+
"--porcelain",
|
|
26288
|
+
"--untracked-files=all"
|
|
26289
|
+
], {
|
|
26290
|
+
message: "Failed to inspect the prepared worktree.",
|
|
26291
|
+
runner
|
|
26292
|
+
}).length === 0) throw new Error("The prepared worktree has no pending changes.");
|
|
26293
|
+
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}.`);
|
|
26294
|
+
git(worktreePath, ["add", "--all"], {
|
|
26295
|
+
message: "Staging the Context Tree changes failed.",
|
|
26296
|
+
runner
|
|
26297
|
+
});
|
|
26298
|
+
git(worktreePath, [
|
|
26299
|
+
"-c",
|
|
26300
|
+
"commit.gpgsign=false",
|
|
26301
|
+
"commit",
|
|
26302
|
+
"--quiet",
|
|
26303
|
+
"-m",
|
|
26304
|
+
options.message
|
|
26305
|
+
], {
|
|
26306
|
+
message: "Committing the Context Tree changes failed.",
|
|
26307
|
+
runner
|
|
26308
|
+
});
|
|
26309
|
+
const sha = git(worktreePath, ["rev-parse", "HEAD"], {
|
|
26310
|
+
message: "Failed to resolve the write commit.",
|
|
26311
|
+
runner
|
|
26312
|
+
});
|
|
26313
|
+
try {
|
|
26314
|
+
if (connection.tree.kind === "local") git(root, [
|
|
26315
|
+
"merge",
|
|
26316
|
+
"--ff-only",
|
|
26317
|
+
taskBranch
|
|
26318
|
+
], {
|
|
26319
|
+
message: "Fast-forwarding the local Context Tree failed.",
|
|
26320
|
+
runner
|
|
25810
26321
|
});
|
|
26322
|
+
else git(worktreePath, [
|
|
26323
|
+
"push",
|
|
26324
|
+
"origin",
|
|
26325
|
+
`HEAD:refs/heads/${branch}`
|
|
26326
|
+
], {
|
|
26327
|
+
message: "Publishing the Context Tree write failed.",
|
|
26328
|
+
runner
|
|
26329
|
+
});
|
|
26330
|
+
} catch (error) {
|
|
26331
|
+
if (isNonFastForward(error)) throw new ContextTreeError(CLI_ERROR_CODES.writeOutdated, `The Context Tree advanced; the prepared worktree is preserved at ${worktreePath}.`);
|
|
26332
|
+
throw error;
|
|
25811
26333
|
}
|
|
26334
|
+
removeWorktree(root, worktreePath, taskBranch, runner);
|
|
25812
26335
|
return {
|
|
25813
|
-
|
|
25814
|
-
root,
|
|
26336
|
+
branch,
|
|
25815
26337
|
schemaVersion: 1,
|
|
25816
|
-
|
|
26338
|
+
sha
|
|
25817
26339
|
};
|
|
25818
26340
|
}
|
|
26341
|
+
function gitCommonDirectory(root, runner) {
|
|
26342
|
+
const value = git(root, ["rev-parse", "--git-common-dir"], {
|
|
26343
|
+
message: "Failed to resolve the Git common directory.",
|
|
26344
|
+
runner
|
|
26345
|
+
});
|
|
26346
|
+
return realDirectoryWithoutSymlinks(isAbsolute(value) ? value : resolve(root, value), "Git common directory");
|
|
26347
|
+
}
|
|
26348
|
+
function validatePreparedWorktree(root, suppliedPath, runner) {
|
|
26349
|
+
const worktreePath = realDirectoryWithoutSymlinks(suppliedPath, "Prepared worktree");
|
|
26350
|
+
if (gitCommonDirectory(worktreePath, runner) !== gitCommonDirectory(root, runner)) throw new Error("The prepared worktree does not belong to the connected Context Tree.");
|
|
26351
|
+
const taskBranch = git(worktreePath, [
|
|
26352
|
+
"symbolic-ref",
|
|
26353
|
+
"--short",
|
|
26354
|
+
"HEAD"
|
|
26355
|
+
], {
|
|
26356
|
+
message: "Failed to resolve the worktree branch.",
|
|
26357
|
+
runner
|
|
26358
|
+
});
|
|
26359
|
+
if (!taskBranch.startsWith(TASK_BRANCH_PREFIX)) throw new Error("The prepared worktree is not on a reserved Context Tree write branch.");
|
|
26360
|
+
return {
|
|
26361
|
+
taskBranch,
|
|
26362
|
+
worktreePath
|
|
26363
|
+
};
|
|
26364
|
+
}
|
|
26365
|
+
function isNonFastForward(error) {
|
|
26366
|
+
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);
|
|
26367
|
+
}
|
|
26368
|
+
function removeWorktree(root, worktreePath, taskBranch, runner) {
|
|
26369
|
+
git(root, [
|
|
26370
|
+
"worktree",
|
|
26371
|
+
"remove",
|
|
26372
|
+
worktreePath
|
|
26373
|
+
], {
|
|
26374
|
+
message: "Removing the write worktree failed.",
|
|
26375
|
+
runner
|
|
26376
|
+
});
|
|
26377
|
+
git(root, [
|
|
26378
|
+
"branch",
|
|
26379
|
+
"-D",
|
|
26380
|
+
taskBranch
|
|
26381
|
+
], {
|
|
26382
|
+
message: "Deleting the write branch failed.",
|
|
26383
|
+
runner
|
|
26384
|
+
});
|
|
26385
|
+
}
|
|
26386
|
+
/** Map every reserved write branch that still has a registered worktree to its path. */
|
|
26387
|
+
function listWriteWorktrees(root, runner) {
|
|
26388
|
+
const paths = /* @__PURE__ */ new Map();
|
|
26389
|
+
const output = optionalGit(root, [
|
|
26390
|
+
"worktree",
|
|
26391
|
+
"list",
|
|
26392
|
+
"--porcelain"
|
|
26393
|
+
], runner);
|
|
26394
|
+
if (output === void 0) return paths;
|
|
26395
|
+
let path;
|
|
26396
|
+
for (const record of output.split("\n")) {
|
|
26397
|
+
if (record.startsWith("worktree ")) {
|
|
26398
|
+
path = record.slice(9).trim();
|
|
26399
|
+
continue;
|
|
26400
|
+
}
|
|
26401
|
+
if (!record.startsWith("branch refs/heads/")) continue;
|
|
26402
|
+
const branch = record.slice(18).trim();
|
|
26403
|
+
if (path !== void 0 && branch.startsWith(TASK_BRANCH_PREFIX)) paths.set(branch, path);
|
|
26404
|
+
}
|
|
26405
|
+
return paths;
|
|
26406
|
+
}
|
|
26407
|
+
function millisecondsSinceModification(path) {
|
|
26408
|
+
try {
|
|
26409
|
+
return Date.now() - statSync(path).mtimeMs;
|
|
26410
|
+
} catch {
|
|
26411
|
+
return;
|
|
26412
|
+
}
|
|
26413
|
+
}
|
|
26414
|
+
/**
|
|
26415
|
+
* A preparation is abandoned only when it carries no commit the connected
|
|
26416
|
+
* checkout lacks, has no pending edits, and has gone untouched. Every unknown
|
|
26417
|
+
* answer preserves the worktree, so a `WRITE_OUTDATED` commit awaiting its
|
|
26418
|
+
* retry and a concurrent preparation both survive.
|
|
26419
|
+
*/
|
|
26420
|
+
function isAbandonedWrite(root, branch, checkoutBranch, path, runner) {
|
|
26421
|
+
if (optionalGit(root, [
|
|
26422
|
+
"rev-list",
|
|
26423
|
+
"--count",
|
|
26424
|
+
branch,
|
|
26425
|
+
"--not",
|
|
26426
|
+
checkoutBranch
|
|
26427
|
+
], runner) !== "0") return false;
|
|
26428
|
+
if (path === void 0) return true;
|
|
26429
|
+
const age = millisecondsSinceModification(path);
|
|
26430
|
+
if (age === void 0 || age < ABANDONED_WRITE_AGE_MS) return false;
|
|
26431
|
+
return optionalGit(path, [
|
|
26432
|
+
"status",
|
|
26433
|
+
"--porcelain",
|
|
26434
|
+
"--untracked-files=all"
|
|
26435
|
+
], runner) === "";
|
|
26436
|
+
}
|
|
26437
|
+
/** Reclaim earlier preparations that were never finished. Every step is best effort. */
|
|
26438
|
+
function reclaimAbandonedWrites(root, checkoutBranch, runner) {
|
|
26439
|
+
optionalGit(root, ["worktree", "prune"], runner);
|
|
26440
|
+
const paths = listWriteWorktrees(root, runner);
|
|
26441
|
+
const branches = optionalGit(root, [
|
|
26442
|
+
"for-each-ref",
|
|
26443
|
+
"--format=%(refname:short)",
|
|
26444
|
+
`refs/heads/${TASK_BRANCH_PREFIX}`
|
|
26445
|
+
], runner);
|
|
26446
|
+
if (branches === void 0) return;
|
|
26447
|
+
for (const branch of branches.split("\n").filter((value) => value.length > 0)) {
|
|
26448
|
+
const path = paths.get(branch);
|
|
26449
|
+
if (!isAbandonedWrite(root, branch, checkoutBranch, path, runner)) continue;
|
|
26450
|
+
if (path !== void 0) optionalGit(root, [
|
|
26451
|
+
"worktree",
|
|
26452
|
+
"remove",
|
|
26453
|
+
path
|
|
26454
|
+
], runner);
|
|
26455
|
+
optionalGit(root, [
|
|
26456
|
+
"branch",
|
|
26457
|
+
"-D",
|
|
26458
|
+
branch
|
|
26459
|
+
], runner);
|
|
26460
|
+
}
|
|
26461
|
+
}
|
|
25819
26462
|
//#endregion
|
|
25820
26463
|
//#region src/cli/api.ts
|
|
25821
26464
|
const defaultIo = {
|
|
@@ -25826,57 +26469,72 @@ function line(io, value) {
|
|
|
25826
26469
|
io.stdout(`${value}\n`);
|
|
25827
26470
|
}
|
|
25828
26471
|
function createContextTreeCli(io = defaultIo) {
|
|
25829
|
-
const program = new Command().name("context-tree").description("
|
|
26472
|
+
const program = new Command().name("context-tree").description("Create, connect, list, read, write, and publish Context Trees.").addHelpCommand(false).version(readPackageVersion()).exitOverride().configureOutput({
|
|
25830
26473
|
writeErr: () => void 0,
|
|
25831
26474
|
writeOut: io.stdout
|
|
25832
26475
|
});
|
|
25833
|
-
program.command("
|
|
25834
|
-
line(io, JSON.stringify(
|
|
26476
|
+
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) => {
|
|
26477
|
+
line(io, JSON.stringify(createProject(resolve(io.cwd(), options.projectPath))));
|
|
26478
|
+
});
|
|
26479
|
+
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) => {
|
|
26480
|
+
const projectPath = resolve(io.cwd(), options.projectPath);
|
|
26481
|
+
if (target !== void 0 && options.treePath !== void 0) throw new Error("Connect requires exactly one of a name/repository or --tree-path.");
|
|
26482
|
+
if (target !== void 0) {
|
|
26483
|
+
line(io, JSON.stringify(connectProject({
|
|
26484
|
+
projectPath,
|
|
26485
|
+
target
|
|
26486
|
+
})));
|
|
26487
|
+
return;
|
|
26488
|
+
}
|
|
26489
|
+
if (options.treePath !== void 0) {
|
|
26490
|
+
line(io, JSON.stringify(connectProject({
|
|
26491
|
+
projectPath,
|
|
26492
|
+
treePath: resolve(io.cwd(), options.treePath)
|
|
26493
|
+
})));
|
|
26494
|
+
return;
|
|
26495
|
+
}
|
|
26496
|
+
throw new Error("Connect requires a managed tree name, GitHub OWNER/REPO, or --tree-path.");
|
|
25835
26497
|
});
|
|
25836
|
-
program.command("
|
|
25837
|
-
line(io, JSON.stringify(
|
|
26498
|
+
program.command("list").description("List valid clean managed Context Trees.").action(() => {
|
|
26499
|
+
line(io, JSON.stringify(listManagedTrees()));
|
|
25838
26500
|
});
|
|
25839
|
-
program.command("resolve").description("Resolve the
|
|
25840
|
-
line(io, JSON.stringify(
|
|
26501
|
+
program.command("resolve").description("Resolve the connected Context Tree for a project.").option("--project-path <path>", "project directory", ".").action((options) => {
|
|
26502
|
+
line(io, JSON.stringify(resolveConnection(resolve(io.cwd(), options.projectPath))));
|
|
25841
26503
|
});
|
|
25842
|
-
program.command("
|
|
25843
|
-
line(io, JSON.stringify(
|
|
26504
|
+
program.command("sync").description("Synchronize the connected Context Tree for a project.").option("--project-path <path>", "project directory", ".").action((options) => {
|
|
26505
|
+
line(io, JSON.stringify(syncProject(resolve(io.cwd(), options.projectPath))));
|
|
25844
26506
|
});
|
|
25845
|
-
program.command("
|
|
25846
|
-
line(io, JSON.stringify(
|
|
26507
|
+
program.command("prepare-write").description("Prepare an isolated Context Tree worktree for a write.").option("--project-path <path>", "project directory", ".").action((options) => {
|
|
26508
|
+
line(io, JSON.stringify(prepareContextWrite(resolve(io.cwd(), options.projectPath))));
|
|
25847
26509
|
});
|
|
25848
|
-
program.command("
|
|
25849
|
-
line(io, JSON.stringify(
|
|
26510
|
+
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) => {
|
|
26511
|
+
line(io, JSON.stringify(finishContextWrite({
|
|
26512
|
+
message: options.message,
|
|
26513
|
+
projectPath: resolve(io.cwd(), options.projectPath),
|
|
26514
|
+
worktreePath: resolve(io.cwd(), options.worktreePath)
|
|
26515
|
+
})));
|
|
25850
26516
|
});
|
|
25851
|
-
program.command("
|
|
25852
|
-
|
|
25853
|
-
const projectPath = resolve(io.cwd());
|
|
25854
|
-
let project;
|
|
25855
|
-
try {
|
|
25856
|
-
project = identifyProject(projectPath);
|
|
25857
|
-
} catch {}
|
|
25858
|
-
const result = scaffoldTree({
|
|
25859
|
-
path: resolve(io.cwd(), options.treePath ?? repositoryName),
|
|
25860
|
-
repository: options.repository
|
|
25861
|
-
});
|
|
25862
|
-
if (project !== void 0) linkScaffoldedProject(projectPath, result.root);
|
|
25863
|
-
line(io, JSON.stringify(result));
|
|
26517
|
+
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) => {
|
|
26518
|
+
line(io, JSON.stringify(publishProject(resolve(io.cwd(), options.projectPath), { repository })));
|
|
25864
26519
|
});
|
|
25865
26520
|
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(
|
|
26521
|
+
const treePath = resolve(io.cwd(), options.treePath);
|
|
26522
|
+
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}.`);
|
|
26523
|
+
line(io, JSON.stringify(readTree(treePath, path)));
|
|
25869
26524
|
});
|
|
25870
26525
|
program.command("verify").description("Validate Context Tree structure and safety.").option("--tree-path <path>", "Context Tree root", ".").action((options) => {
|
|
25871
26526
|
const result = verifyTree(resolve(io.cwd(), options.treePath));
|
|
25872
26527
|
line(io, JSON.stringify(result));
|
|
25873
26528
|
if (!result.ok) process.exitCode = 1;
|
|
25874
26529
|
});
|
|
26530
|
+
program.command("install").description("Install the packaged Context Tree skills into each agent's skill directory.").option("--host <host>", "restrict to one host: claude, codex, or all", "all").option("--project <path>", "install below this project root instead of the home directory").action((options) => {
|
|
26531
|
+
const request = {};
|
|
26532
|
+
if (options.host !== "all") request.hosts = [skillHostSchema.parse(options.host)];
|
|
26533
|
+
if (options.project !== void 0) request.projectPath = resolve(io.cwd(), options.project);
|
|
26534
|
+
line(io, JSON.stringify(installSkills(request)));
|
|
26535
|
+
});
|
|
25875
26536
|
return program;
|
|
25876
26537
|
}
|
|
25877
|
-
function sanitizeError(message) {
|
|
25878
|
-
return message.replace(/(?:https?|ssh):\/\/[^\s/@]+@/giu, "<redacted>@");
|
|
25879
|
-
}
|
|
25880
26538
|
async function runContextTreeCli(argv = process.argv, io = defaultIo) {
|
|
25881
26539
|
const program = createContextTreeCli(io);
|
|
25882
26540
|
try {
|
|
@@ -25886,8 +26544,8 @@ async function runContextTreeCli(argv = process.argv, io = defaultIo) {
|
|
|
25886
26544
|
if (error instanceof CommanderError && error.exitCode === 0) return 0;
|
|
25887
26545
|
const envelope = {
|
|
25888
26546
|
error: {
|
|
25889
|
-
code: error instanceof
|
|
25890
|
-
message:
|
|
26547
|
+
code: error instanceof ContextTreeError ? error.code : CLI_ERROR_CODES.failed,
|
|
26548
|
+
message: sanitizeCommandOutput(error instanceof Error ? error.message : String(error))
|
|
25891
26549
|
},
|
|
25892
26550
|
ok: false,
|
|
25893
26551
|
schemaVersion: 1
|