@delorenj/pjangler 1.5.35 → 1.5.37
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/dist/index.js +669 -87
- package/dist/mcp-server.js +13 -0
- package/dist/project-registry-service.js +2 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -343,8 +343,8 @@ var init_types = __esm({
|
|
|
343
343
|
CREDENTIAL_KEY = /(?:^|[_-])(?:password|passwd|api[_-]?key|access[_-]?token|refresh[_-]?token|client[_-]?secret|private[_-]?key|credential|authorization)(?:$|[_-])/iu;
|
|
344
344
|
SECRET_SHAPED_VALUE = /(?:-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----|(?<![A-Za-z0-9])eyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}(?![A-Za-z0-9])|AKIA[0-9A-Z]{16}|gh[pousr]_[A-Za-z0-9]{20,}|(?:password|api[_-]?key|access[_-]?token|client[_-]?secret)\s*[:=]\s*[^\s]{8,})/iu;
|
|
345
345
|
NotebookError = class extends Error {
|
|
346
|
-
constructor(code,
|
|
347
|
-
super(
|
|
346
|
+
constructor(code, message2, retryable = false, details = {}, options) {
|
|
347
|
+
super(message2, options);
|
|
348
348
|
this.code = code;
|
|
349
349
|
this.retryable = retryable;
|
|
350
350
|
this.details = details;
|
|
@@ -501,8 +501,8 @@ var init_manifestIndex = __esm({
|
|
|
501
501
|
init_registryClient();
|
|
502
502
|
init_notebookSettingsValidation();
|
|
503
503
|
RegistryError = class extends Error {
|
|
504
|
-
constructor(
|
|
505
|
-
super(
|
|
504
|
+
constructor(message2, status = 422, code = "invalid_manifest") {
|
|
505
|
+
super(message2);
|
|
506
506
|
this.status = status;
|
|
507
507
|
this.code = code;
|
|
508
508
|
}
|
|
@@ -1023,8 +1023,8 @@ var init_boardQuery = __esm({
|
|
|
1023
1023
|
init_version();
|
|
1024
1024
|
BOARD_PROVIDERS = ["plane", "trello", "linear"];
|
|
1025
1025
|
BoardError = class extends Error {
|
|
1026
|
-
constructor(
|
|
1027
|
-
super(
|
|
1026
|
+
constructor(message2) {
|
|
1027
|
+
super(message2);
|
|
1028
1028
|
this.name = "BoardError";
|
|
1029
1029
|
}
|
|
1030
1030
|
};
|
|
@@ -4561,14 +4561,14 @@ function parseNote(value, noteMaxBytes) {
|
|
|
4561
4561
|
if (note.content === null) throw new NotebookError("REMOTE_PROTOCOL_ERROR", "Open Notebook returned invalid note content");
|
|
4562
4562
|
return note;
|
|
4563
4563
|
}
|
|
4564
|
-
function errorForStatus(status,
|
|
4565
|
-
if (status === 400 || status === 422) return new NotebookError("INVALID_INPUT",
|
|
4566
|
-
if (status === 401 || status === 403) return new NotebookError("AUTHENTICATION_FAILED",
|
|
4567
|
-
if (status === 404) return new NotebookError("NOT_FOUND",
|
|
4568
|
-
if (status === 409) return new NotebookError("CONFLICT",
|
|
4569
|
-
if (status === 429) return new NotebookError("THROTTLED",
|
|
4570
|
-
if (status >= 500) return new NotebookError("SERVICE_UNAVAILABLE",
|
|
4571
|
-
return new NotebookError("REMOTE_PROTOCOL_ERROR",
|
|
4564
|
+
function errorForStatus(status, message2) {
|
|
4565
|
+
if (status === 400 || status === 422) return new NotebookError("INVALID_INPUT", message2, false, { http_status: status, definitive_rejection: true });
|
|
4566
|
+
if (status === 401 || status === 403) return new NotebookError("AUTHENTICATION_FAILED", message2);
|
|
4567
|
+
if (status === 404) return new NotebookError("NOT_FOUND", message2);
|
|
4568
|
+
if (status === 409) return new NotebookError("CONFLICT", message2);
|
|
4569
|
+
if (status === 429) return new NotebookError("THROTTLED", message2, true);
|
|
4570
|
+
if (status >= 500) return new NotebookError("SERVICE_UNAVAILABLE", message2, true);
|
|
4571
|
+
return new NotebookError("REMOTE_PROTOCOL_ERROR", message2);
|
|
4572
4572
|
}
|
|
4573
4573
|
async function readResponseBody(response, maxBytes) {
|
|
4574
4574
|
const declared = Number(response.headers.get("content-length") ?? "0");
|
|
@@ -6063,9 +6063,9 @@ var init_capture = __esm({
|
|
|
6063
6063
|
});
|
|
6064
6064
|
|
|
6065
6065
|
// src/index.ts
|
|
6066
|
-
import { spawnSync as
|
|
6067
|
-
import { existsSync as
|
|
6068
|
-
import { basename as basename11, join as
|
|
6066
|
+
import { spawnSync as spawnSync18 } from "node:child_process";
|
|
6067
|
+
import { existsSync as existsSync27, readdirSync as readdirSync13, readFileSync as readFileSync30, statSync as statSync9 } from "node:fs";
|
|
6068
|
+
import { basename as basename11, join as join35, resolve as resolve25 } from "node:path";
|
|
6069
6069
|
import { Command as Command3, CommanderError, Option } from "commander";
|
|
6070
6070
|
|
|
6071
6071
|
// src/recipes/Recipe.ts
|
|
@@ -6083,8 +6083,8 @@ var Command = class {
|
|
|
6083
6083
|
/**
|
|
6084
6084
|
* Format message with [DRY RUN] prefix if in dry-run mode
|
|
6085
6085
|
*/
|
|
6086
|
-
formatMessage(
|
|
6087
|
-
return this.context.dryRun ? `[DRY RUN] ${
|
|
6086
|
+
formatMessage(message2) {
|
|
6087
|
+
return this.context.dryRun ? `[DRY RUN] ${message2}` : message2;
|
|
6088
6088
|
}
|
|
6089
6089
|
fileExists(filePath) {
|
|
6090
6090
|
const fullPath = join(this.context.targetDir, filePath);
|
|
@@ -9884,11 +9884,11 @@ async function runDeclaredBmadUpdate(root, options, scope) {
|
|
|
9884
9884
|
try {
|
|
9885
9885
|
sb = captureScopedBackup(root, scope);
|
|
9886
9886
|
} catch (error) {
|
|
9887
|
-
const
|
|
9887
|
+
const message2 = error instanceof UnreadableSurfaceError ? error.message : `could not capture the BMAD update surface: ${error instanceof Error ? error.message : String(error)}`;
|
|
9888
9888
|
return {
|
|
9889
9889
|
ok: false,
|
|
9890
|
-
error:
|
|
9891
|
-
details: [
|
|
9890
|
+
error: message2,
|
|
9891
|
+
details: [message2, "No eviction or installer ran; nothing was written.", scopeNote],
|
|
9892
9892
|
changedFiles: [],
|
|
9893
9893
|
companion: preflight
|
|
9894
9894
|
};
|
|
@@ -10133,8 +10133,8 @@ function scanToml(text2) {
|
|
|
10133
10133
|
let i = 0;
|
|
10134
10134
|
let line = 0;
|
|
10135
10135
|
let table = -1;
|
|
10136
|
-
const fail2 = (
|
|
10137
|
-
throw new TomlScanError(`line ${line + 1}: ${
|
|
10136
|
+
const fail2 = (message2) => {
|
|
10137
|
+
throw new TomlScanError(`line ${line + 1}: ${message2}`);
|
|
10138
10138
|
};
|
|
10139
10139
|
const skipBlank = () => {
|
|
10140
10140
|
while (i < n && (text2[i] === " " || text2[i] === " " || text2[i] === "\r")) i++;
|
|
@@ -14598,6 +14598,18 @@ init_project();
|
|
|
14598
14598
|
import { spawnSync as spawnSync14 } from "node:child_process";
|
|
14599
14599
|
import { existsSync as existsSync18, lstatSync as lstatSync16, readFileSync as readFileSync22, rmSync as rmSync8 } from "node:fs";
|
|
14600
14600
|
import { dirname as dirname18, isAbsolute as isAbsolute10, join as join24, relative as relativePath, resolve as resolve19 } from "node:path";
|
|
14601
|
+
|
|
14602
|
+
// src/project/ticketScope.ts
|
|
14603
|
+
function ticketScope(ticket) {
|
|
14604
|
+
const block = ticket !== null && typeof ticket === "object" ? ticket : {};
|
|
14605
|
+
const label = typeof block.scope_label === "string" ? block.scope_label.trim() : "";
|
|
14606
|
+
return {
|
|
14607
|
+
...label ? { scope_label: label } : {},
|
|
14608
|
+
...block.scope_default === true ? { scope_default: true } : {}
|
|
14609
|
+
};
|
|
14610
|
+
}
|
|
14611
|
+
|
|
14612
|
+
// src/recipes/ProjectRecipe.ts
|
|
14601
14613
|
init_tree_diff();
|
|
14602
14614
|
|
|
14603
14615
|
// src/recipes/NotebookRecipe.ts
|
|
@@ -15666,8 +15678,8 @@ function installProjectNotebookIntegration(input = {}) {
|
|
|
15666
15678
|
return { skill, hooksChanged: /\bchanged\b/u.test(result2.stdout) };
|
|
15667
15679
|
}
|
|
15668
15680
|
function boundedDiagnostic(value, max = 512) {
|
|
15669
|
-
const
|
|
15670
|
-
return
|
|
15681
|
+
const message2 = value instanceof NotebookError ? value.message : "Project Notebook encountered an unexpected internal error";
|
|
15682
|
+
return message2.replace(/[\u0000-\u001f\u007f]/gu, " ").replace(/\s+/gu, " ").slice(0, max);
|
|
15671
15683
|
}
|
|
15672
15684
|
function parsePayload(value, maxBytes) {
|
|
15673
15685
|
if (Buffer.byteLength(value, "utf8") > maxBytes) throw new NotebookError("INVALID_INPUT", "Hook payload exceeds its configured ceiling");
|
|
@@ -16563,8 +16575,8 @@ var NotebookRecipe = class extends Recipe {
|
|
|
16563
16575
|
return { recipeId: this.metadata.id, ok: true, dryRun: Boolean(ctx.dryRun), changedFiles: [], logs: ["notebook: local skill/hooks skipped"], errors: [], phases: [{ id: "notebook.local", status: "skipped", changedFiles: [], message: "Notebook is disabled or this is a plan-only invocation" }] };
|
|
16564
16576
|
}
|
|
16565
16577
|
if (notebookPlan.remote_effect === "none") {
|
|
16566
|
-
const
|
|
16567
|
-
return { recipeId: this.metadata.id, ok: true, dryRun: false, changedFiles: [], logs: [`notebook: ${
|
|
16578
|
+
const message2 = `Notebook has no configured endpoint (${notebookPlan.reason}); global skill and hook projection deferred to \`pj notebook migrate --apply\``;
|
|
16579
|
+
return { recipeId: this.metadata.id, ok: true, dryRun: false, changedFiles: [], logs: [`notebook: ${message2}`], errors: [], phases: [{ id: "notebook.local", status: "skipped", changedFiles: [], message: message2 }] };
|
|
16568
16580
|
}
|
|
16569
16581
|
try {
|
|
16570
16582
|
const env2 = integrationEnvironment(this.module, ctx);
|
|
@@ -16593,8 +16605,8 @@ var NotebookRecipe = class extends Recipe {
|
|
|
16593
16605
|
phases: [{ id: "notebook.local", status: applied.changedFiles.length ? "changed" : "unchanged", changedFiles: applied.changedFiles, message: "Canonical Project Notebook skill and hooks verified" }]
|
|
16594
16606
|
};
|
|
16595
16607
|
} catch (error) {
|
|
16596
|
-
const
|
|
16597
|
-
return { recipeId: this.metadata.id, ok: false, dryRun: false, changedFiles: [], logs: [], errors: [
|
|
16608
|
+
const message2 = error instanceof Error ? error.message : String(error);
|
|
16609
|
+
return { recipeId: this.metadata.id, ok: false, dryRun: false, changedFiles: [], logs: [], errors: [message2], phases: [{ id: "notebook.local", status: "failed", changedFiles: [], message: message2 }] };
|
|
16598
16610
|
}
|
|
16599
16611
|
}
|
|
16600
16612
|
async applyExternal(plan, notebookPlan) {
|
|
@@ -16720,6 +16732,7 @@ function refreshPlanFromCanonicalManifest(plan) {
|
|
|
16720
16732
|
...carriesProvenance && identifierSource === "provider" && recorded?.identifier_fetched_at ? { identifier_fetched_at: recorded.identifier_fetched_at } : {},
|
|
16721
16733
|
board_id: manifestBoardId,
|
|
16722
16734
|
...boardConfirmedAt ? { board_confirmed_at: boardConfirmedAt } : {},
|
|
16735
|
+
...ticketScope(manifestTicket),
|
|
16723
16736
|
state: manifestState === "skipped" ? "skipped" : manifestBoardId && boardConfirmedAt && keyIsProven ? "linked" : "planned"
|
|
16724
16737
|
};
|
|
16725
16738
|
plan.manifest = manifest;
|
|
@@ -17077,8 +17090,8 @@ var ProjectRecipe = class extends Recipe {
|
|
|
17077
17090
|
} else try {
|
|
17078
17091
|
rmSync8(targetDir, { recursive: true, force: true });
|
|
17079
17092
|
const insideTarget = (path) => {
|
|
17080
|
-
const
|
|
17081
|
-
return
|
|
17093
|
+
const relative17 = relativePath(resolve19(targetDir), resolve19(path));
|
|
17094
|
+
return relative17 === "" || !relative17.startsWith("..") && !isAbsolute10(relative17);
|
|
17082
17095
|
};
|
|
17083
17096
|
const orphaned = [...new Set(changedFiles.filter((path) => !insideTarget(path)))].sort();
|
|
17084
17097
|
changedFiles.length = 0;
|
|
@@ -18231,6 +18244,43 @@ async function fetchPlaneBoards(workspace, env2 = process.env, home = homedir12(
|
|
|
18231
18244
|
}
|
|
18232
18245
|
return boards;
|
|
18233
18246
|
}
|
|
18247
|
+
async function patchPlaneBoardIdentifier(workspace, boardId, identifier, env2 = process.env, home = homedir12()) {
|
|
18248
|
+
const apiKey = planeApiKeyFor(workspace, env2, home);
|
|
18249
|
+
if (!apiKey) {
|
|
18250
|
+
throw new BoardError(
|
|
18251
|
+
`no Plane API key for workspace "${workspace}": set PLANE_API_KEY, or add one to ${env2.HERMES_FLEET_ENV?.trim() || join30(home, ".hermes", "fleet.env")}`
|
|
18252
|
+
);
|
|
18253
|
+
}
|
|
18254
|
+
const base = planeBase(env2, home);
|
|
18255
|
+
const url = `${base}/api/v1/workspaces/${encodeURIComponent(workspace)}/projects/${encodeURIComponent(boardId)}/`;
|
|
18256
|
+
let response;
|
|
18257
|
+
try {
|
|
18258
|
+
response = await fetch(url, {
|
|
18259
|
+
method: "PATCH",
|
|
18260
|
+
headers: {
|
|
18261
|
+
"X-API-Key": apiKey,
|
|
18262
|
+
"User-Agent": `pjangler/${PJANGLER_VERSION}`,
|
|
18263
|
+
"Content-Type": "application/json"
|
|
18264
|
+
},
|
|
18265
|
+
body: JSON.stringify({ identifier }),
|
|
18266
|
+
signal: AbortSignal.timeout(2e4)
|
|
18267
|
+
});
|
|
18268
|
+
} catch (error) {
|
|
18269
|
+
throw new BoardError(
|
|
18270
|
+
`could not reach Plane at ${base} to rename board ${boardId}: ` + (error instanceof Error ? error.message : String(error))
|
|
18271
|
+
);
|
|
18272
|
+
}
|
|
18273
|
+
if (!response.ok) {
|
|
18274
|
+
let detail = "";
|
|
18275
|
+
try {
|
|
18276
|
+
detail = (await response.text()).replace(/\s+/g, " ").trim().slice(0, 200);
|
|
18277
|
+
} catch {
|
|
18278
|
+
}
|
|
18279
|
+
throw new BoardError(
|
|
18280
|
+
`Plane refused identifier "${identifier}" for board ${boardId} (HTTP ${response.status})${detail ? `: ${detail}` : ""}`
|
|
18281
|
+
);
|
|
18282
|
+
}
|
|
18283
|
+
}
|
|
18234
18284
|
function trelloCredential(name, env2, home) {
|
|
18235
18285
|
const fleetEnv = env2.HERMES_FLEET_ENV?.trim() || join30(home, ".hermes", "fleet.env");
|
|
18236
18286
|
const raw = env2[name]?.trim() || dotenvValue(fleetEnv, name)?.trim();
|
|
@@ -18931,19 +18981,499 @@ async function linkProjectBoard(options) {
|
|
|
18931
18981
|
};
|
|
18932
18982
|
}
|
|
18933
18983
|
|
|
18984
|
+
// src/project/rename.ts
|
|
18985
|
+
init_boardUrl();
|
|
18986
|
+
import { spawnSync as spawnSync15 } from "node:child_process";
|
|
18987
|
+
import { existsSync as existsSync22, readdirSync as readdirSync11, readFileSync as readFileSync27, realpathSync as realpathSync11, statSync as statSync6 } from "node:fs";
|
|
18988
|
+
import { homedir as homedir13 } from "node:os";
|
|
18989
|
+
import { isAbsolute as isAbsolute11, join as join31, relative as relative16 } from "node:path";
|
|
18990
|
+
import YAML8 from "yaml";
|
|
18991
|
+
init_project();
|
|
18992
|
+
init_registryClient();
|
|
18993
|
+
var KEY_PATTERN = /^[a-z0-9]{1,12}$/;
|
|
18994
|
+
var RenameError = class extends Error {
|
|
18995
|
+
constructor(code, message2) {
|
|
18996
|
+
super(message2);
|
|
18997
|
+
this.code = code;
|
|
18998
|
+
this.name = "RenameError";
|
|
18999
|
+
}
|
|
19000
|
+
};
|
|
19001
|
+
function isRecord7(value) {
|
|
19002
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
19003
|
+
}
|
|
19004
|
+
function message(error) {
|
|
19005
|
+
return error instanceof Error ? error.message : String(error);
|
|
19006
|
+
}
|
|
19007
|
+
function readJsonObject(path) {
|
|
19008
|
+
let parsed;
|
|
19009
|
+
try {
|
|
19010
|
+
parsed = JSON.parse(readFileSync27(path, "utf8"));
|
|
19011
|
+
} catch {
|
|
19012
|
+
throw new RenameError("manifest_invalid", `${path} is not readable JSON; fix it before renaming`);
|
|
19013
|
+
}
|
|
19014
|
+
if (!isRecord7(parsed)) throw new RenameError("manifest_invalid", `${path} must hold a JSON object`);
|
|
19015
|
+
return parsed;
|
|
19016
|
+
}
|
|
19017
|
+
function escapeRegExp2(value) {
|
|
19018
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
19019
|
+
}
|
|
19020
|
+
var YAML_WORDS = /* @__PURE__ */ new Set(["Y", "N", "YES", "NO", "ON", "OFF", "NULL", "TRUE", "FALSE"]);
|
|
19021
|
+
function yamlScalarFor(identifier, originalSlice) {
|
|
19022
|
+
const plainSafe = /^[A-Z][A-Z0-9]*$/.test(identifier) && !YAML_WORDS.has(identifier);
|
|
19023
|
+
if (originalSlice.startsWith("'") && !originalSlice.startsWith("''")) return `'${identifier}'`;
|
|
19024
|
+
if (originalSlice.startsWith('"')) return `"${identifier}"`;
|
|
19025
|
+
return plainSafe ? identifier : `"${identifier}"`;
|
|
19026
|
+
}
|
|
19027
|
+
async function renameProject(options) {
|
|
19028
|
+
const newId = String(options.newId ?? "").trim().toLowerCase();
|
|
19029
|
+
if (!KEY_PATTERN.test(newId)) {
|
|
19030
|
+
throw new RenameError(
|
|
19031
|
+
"invalid_key",
|
|
19032
|
+
`invalid key ${JSON.stringify(String(options.newId ?? ""))}: use 1-12 lowercase letters and digits (Plane identifiers are alphanumeric, max 12)`
|
|
19033
|
+
);
|
|
19034
|
+
}
|
|
19035
|
+
const NEW = newId.toUpperCase();
|
|
19036
|
+
const oldInput = String(options.oldId ?? "").trim().toLowerCase();
|
|
19037
|
+
const env2 = options.env ?? process.env;
|
|
19038
|
+
const home = options.home ?? homedir13();
|
|
19039
|
+
const apply = options.apply ?? false;
|
|
19040
|
+
const now = (options.now ?? /* @__PURE__ */ new Date()).toISOString();
|
|
19041
|
+
const registryPath = resolveRegistryLocation(options.registryPath, env2);
|
|
19042
|
+
const service = isRegistryServiceLocation(registryPath);
|
|
19043
|
+
const warnings = [];
|
|
19044
|
+
let registry;
|
|
19045
|
+
try {
|
|
19046
|
+
registry = loadProjectRegistry(registryPath);
|
|
19047
|
+
} catch (error) {
|
|
19048
|
+
throw new RenameError("registry_unavailable", message(error));
|
|
19049
|
+
}
|
|
19050
|
+
let oldId = oldInput;
|
|
19051
|
+
let resumed = false;
|
|
19052
|
+
let renamedFromIdentifier = "";
|
|
19053
|
+
let project = oldInput ? getOwnRecordValue(registry.projects, oldInput) : void 0;
|
|
19054
|
+
if (project) {
|
|
19055
|
+
if (oldInput !== newId && getOwnRecordValue(registry.projects, newId)) {
|
|
19056
|
+
throw new RenameError("identity_collision", `project_id "${newId}" is already registered to another project`);
|
|
19057
|
+
}
|
|
19058
|
+
} else {
|
|
19059
|
+
const existing = getOwnRecordValue(registry.projects, newId);
|
|
19060
|
+
let from;
|
|
19061
|
+
if (existing && oldInput) {
|
|
19062
|
+
try {
|
|
19063
|
+
const candidate = readJsonObject(join31(existing.repo_path, ".project.json")).renamed_from;
|
|
19064
|
+
if (isRecord7(candidate)) from = candidate;
|
|
19065
|
+
} catch {
|
|
19066
|
+
}
|
|
19067
|
+
}
|
|
19068
|
+
if (!existing || !from || from.project_id !== oldInput) {
|
|
19069
|
+
throw new RenameError("unknown_project", `Project not found in registry: ${options.oldId}`);
|
|
19070
|
+
}
|
|
19071
|
+
project = existing;
|
|
19072
|
+
resumed = true;
|
|
19073
|
+
renamedFromIdentifier = typeof from.identifier === "string" ? from.identifier : "";
|
|
19074
|
+
}
|
|
19075
|
+
const record3 = project;
|
|
19076
|
+
const provider = record3.ticket_provider;
|
|
19077
|
+
const type = String(provider.type ?? "").trim().toLowerCase();
|
|
19078
|
+
const boardId = String(provider.board_id ?? "").trim();
|
|
19079
|
+
const isPlane = type === "plane";
|
|
19080
|
+
const workspace = isPlane ? String(provider.workspace ?? "").trim() || DEFAULT_PLANE_WORKSPACE : String(provider.workspace ?? "").trim();
|
|
19081
|
+
const repoPath = record3.repo_path;
|
|
19082
|
+
const manifestPath = join31(repoPath, ".project.json");
|
|
19083
|
+
const manifestExists = existsSync22(manifestPath);
|
|
19084
|
+
if (service && !manifestExists) {
|
|
19085
|
+
throw new RenameError("manifest_missing", `${manifestPath} is missing; the registry service indexes manifests, so there is nothing to rename`);
|
|
19086
|
+
}
|
|
19087
|
+
const manifestBefore = manifestExists ? readJsonObject(manifestPath) : void 0;
|
|
19088
|
+
const fetchBoards = options.fetchBoards ?? ((ws) => fetchPlaneBoards(ws, env2, home));
|
|
19089
|
+
let live;
|
|
19090
|
+
if (isPlane && boardId) {
|
|
19091
|
+
let boards;
|
|
19092
|
+
try {
|
|
19093
|
+
boards = await fetchBoards(workspace);
|
|
19094
|
+
} catch (error) {
|
|
19095
|
+
throw new RenameError("provider_unreachable", message(error));
|
|
19096
|
+
}
|
|
19097
|
+
live = boards.get(boardId);
|
|
19098
|
+
if (!live) throw new RenameError("board_not_found", `Plane workspace "${workspace}" has no board ${boardId}`);
|
|
19099
|
+
if (live.archived) {
|
|
19100
|
+
throw new RenameError("board_archived", `Plane board ${boardId} ("${live.name}") is archived; an archived board answers but is not usable`);
|
|
19101
|
+
}
|
|
19102
|
+
const owner = liveKeyIndex(boards).get(NEW);
|
|
19103
|
+
if (owner && owner.id !== boardId) {
|
|
19104
|
+
throw new RenameError("identity_collision", `board identifier ${NEW} already belongs to Plane board ${owner.id} ("${owner.name}") in workspace "${workspace}"`);
|
|
19105
|
+
}
|
|
19106
|
+
} else if (type === "trello" && boardId) {
|
|
19107
|
+
try {
|
|
19108
|
+
const facts = await (options.fetchTrello ?? ((id) => fetchTrelloBoard(id, env2, home)))(boardId);
|
|
19109
|
+
if (!facts) throw new RenameError("board_not_found", `Trello has no visible board ${boardId}`);
|
|
19110
|
+
if (facts.closed) throw new RenameError("board_archived", `Trello board ${boardId} ("${facts.name}") is archived`);
|
|
19111
|
+
} catch (error) {
|
|
19112
|
+
if (error instanceof RenameError) throw error;
|
|
19113
|
+
warnings.push(`Trello board ${boardId} could not be verified (${message(error)}); renamed locally`);
|
|
19114
|
+
}
|
|
19115
|
+
}
|
|
19116
|
+
const manifestProvider = manifestBefore?.ticket_provider;
|
|
19117
|
+
const manifestIdentifier = isRecord7(manifestProvider) ? String(manifestProvider.identifier ?? "") : "";
|
|
19118
|
+
const oldIdentifiers = [
|
|
19119
|
+
...new Set(
|
|
19120
|
+
[String(provider.identifier ?? ""), manifestIdentifier, live?.identifier ?? "", renamedFromIdentifier].map((value) => value.trim()).filter((value) => value && value.toUpperCase() !== NEW)
|
|
19121
|
+
)
|
|
19122
|
+
];
|
|
19123
|
+
const oldIdentifier = oldIdentifiers[0] ?? "";
|
|
19124
|
+
const providerConfirmed = isPlane && Boolean(boardId);
|
|
19125
|
+
function nextProvider(current) {
|
|
19126
|
+
if (!current.identifier && !providerConfirmed) return current;
|
|
19127
|
+
const next = { ...current, identifier: NEW };
|
|
19128
|
+
if (providerConfirmed) {
|
|
19129
|
+
const unchanged = String(current.identifier ?? "").toUpperCase() === NEW && current.identifier_source === "provider" && typeof current.identifier_fetched_at === "string" && current.identifier_fetched_at !== "";
|
|
19130
|
+
next.identifier_source = "provider";
|
|
19131
|
+
next.identifier_fetched_at = unchanged ? current.identifier_fetched_at : now;
|
|
19132
|
+
}
|
|
19133
|
+
return next;
|
|
19134
|
+
}
|
|
19135
|
+
const planeCall = options.patchBoard ?? ((ws, id, identifier) => patchPlaneBoardIdentifier(ws, id, identifier, env2, home));
|
|
19136
|
+
const planeStep = async (doApply) => {
|
|
19137
|
+
if (!isPlane) return { status: "skipped", detail: type === "trello" ? "trello assigns no key; no provider write" : "no Plane board; no provider write" };
|
|
19138
|
+
if (!boardId || !live) return { status: "skipped", detail: "no board bound; nothing to rename at the provider" };
|
|
19139
|
+
if (live.identifier.toUpperCase() === NEW) return { status: "already", detail: `board ${boardId} already reads ${live.identifier}` };
|
|
19140
|
+
if (!doApply) return { status: "planned", detail: `Plane board ${boardId}: ${live.identifier} -> ${NEW}` };
|
|
19141
|
+
let patchError;
|
|
19142
|
+
try {
|
|
19143
|
+
await planeCall(workspace, boardId, NEW);
|
|
19144
|
+
} catch (error) {
|
|
19145
|
+
patchError = message(error);
|
|
19146
|
+
}
|
|
19147
|
+
try {
|
|
19148
|
+
const after = (await fetchBoards(workspace)).get(boardId);
|
|
19149
|
+
if (after && after.identifier.toUpperCase() === NEW) {
|
|
19150
|
+
return { status: "done", detail: `Plane board ${boardId}: ${live.identifier} -> ${NEW}${patchError ? ` (the PATCH reported: ${patchError})` : ""}` };
|
|
19151
|
+
}
|
|
19152
|
+
if (patchError) {
|
|
19153
|
+
return { status: "failed", detail: `${patchError}; the board still reads ${after?.identifier ?? "(missing)"}, but the request may yet land: re-running is safe` };
|
|
19154
|
+
}
|
|
19155
|
+
return { status: "failed", detail: `Plane accepted the rename but board ${boardId} reads ${after?.identifier ?? "(missing)"}` };
|
|
19156
|
+
} catch (error) {
|
|
19157
|
+
return {
|
|
19158
|
+
status: "failed",
|
|
19159
|
+
detail: `${patchError ? `${patchError}; ` : ""}the read-back failed (${message(error)}), so the outcome is unknown; re-running is safe`
|
|
19160
|
+
};
|
|
19161
|
+
}
|
|
19162
|
+
};
|
|
19163
|
+
const manifestStep = async (doApply) => {
|
|
19164
|
+
if (!manifestExists) return { status: "skipped", detail: `no .project.json at ${repoPath}` };
|
|
19165
|
+
let current;
|
|
19166
|
+
try {
|
|
19167
|
+
current = readJsonObject(manifestPath);
|
|
19168
|
+
} catch (error) {
|
|
19169
|
+
return { status: "failed", detail: message(error) };
|
|
19170
|
+
}
|
|
19171
|
+
const next = { ...current, project_id: newId };
|
|
19172
|
+
delete next.project_slug;
|
|
19173
|
+
if (isRecord7(current.ticket_provider)) next.ticket_provider = nextProvider(current.ticket_provider);
|
|
19174
|
+
if (!resumed && (oldId !== newId || oldIdentifier !== "")) {
|
|
19175
|
+
const previous = current.renamed_from;
|
|
19176
|
+
const same = isRecord7(previous) && previous.project_id === oldId && String(previous.identifier ?? "") === oldIdentifier;
|
|
19177
|
+
next.renamed_from = same ? previous : { project_id: oldId, ...oldIdentifier ? { identifier: oldIdentifier } : {}, at: now };
|
|
19178
|
+
}
|
|
19179
|
+
if (JSON.stringify(next) === JSON.stringify(current)) {
|
|
19180
|
+
return { status: "already", detail: `project_id ${newId}, identifier ${NEW}` };
|
|
19181
|
+
}
|
|
19182
|
+
const detail = `project_id ${String(current.project_id ?? current.project_slug ?? "(none)")} -> ${newId}, identifier -> ${NEW}`;
|
|
19183
|
+
if (!doApply) return { status: "planned", detail, files: [manifestPath] };
|
|
19184
|
+
try {
|
|
19185
|
+
atomicWrite(manifestPath, `${JSON.stringify(next, null, 2)}
|
|
19186
|
+
`);
|
|
19187
|
+
} catch (error) {
|
|
19188
|
+
return { status: "failed", detail: `could not write ${manifestPath}: ${message(error)}` };
|
|
19189
|
+
}
|
|
19190
|
+
return { status: "done", detail, files: [manifestPath] };
|
|
19191
|
+
};
|
|
19192
|
+
const registryStep = async (doApply) => {
|
|
19193
|
+
const identifierNow = (r) => String(r?.ticket_provider?.identifier ?? "").toUpperCase();
|
|
19194
|
+
const keyed = getOwnRecordValue(registry.projects, newId);
|
|
19195
|
+
const keyedIdentifier = identifierNow(keyed);
|
|
19196
|
+
const settled = Boolean(keyed) && (oldId === newId || !getOwnRecordValue(registry.projects, oldId)) && (keyedIdentifier === NEW || keyedIdentifier === "" && !providerConfirmed);
|
|
19197
|
+
if (settled) return { status: "already", detail: `registry already keys ${newId}` };
|
|
19198
|
+
const detail = `${oldId} -> ${newId}${service ? " (service re-index)" : ""}`;
|
|
19199
|
+
if (!doApply) return { status: "planned", detail };
|
|
19200
|
+
try {
|
|
19201
|
+
if (service) {
|
|
19202
|
+
registryRequest(registryPath, "POST", "/v1/index", { manifest_path: manifestPath });
|
|
19203
|
+
const after = loadProjectRegistry(registryPath);
|
|
19204
|
+
if (!getOwnRecordValue(after.projects, newId)) throw new Error(`the service index does not list ${newId} after re-indexing`);
|
|
19205
|
+
if (oldId !== newId && getOwnRecordValue(after.projects, oldId)) throw new Error(`the service index still lists ${oldId} after re-indexing`);
|
|
19206
|
+
} else {
|
|
19207
|
+
const fresh = loadProjectRegistry(registryPath);
|
|
19208
|
+
const current = getOwnRecordValue(fresh.projects, oldId);
|
|
19209
|
+
if (!current) throw new Error(`${oldId} vanished from ${registryPath} mid-rename`);
|
|
19210
|
+
const renamed = {
|
|
19211
|
+
...current,
|
|
19212
|
+
slug: newId,
|
|
19213
|
+
...Object.hasOwn(current, "project_id") ? { project_id: newId } : {},
|
|
19214
|
+
ticket_provider: nextProvider(current.ticket_provider),
|
|
19215
|
+
updated_at: now
|
|
19216
|
+
};
|
|
19217
|
+
delete fresh.projects[oldId];
|
|
19218
|
+
fresh.projects[newId] = renamed;
|
|
19219
|
+
saveProjectRegistry(fresh, registryPath);
|
|
19220
|
+
}
|
|
19221
|
+
} catch (error) {
|
|
19222
|
+
return { status: "failed", detail: message(error) };
|
|
19223
|
+
}
|
|
19224
|
+
return { status: "done", detail, files: service ? [] : [registryPath] };
|
|
19225
|
+
};
|
|
19226
|
+
const roleStep = async (doApply) => {
|
|
19227
|
+
const hermesDir = join31(repoPath, "agents", "hermes");
|
|
19228
|
+
if (!existsSync22(hermesDir)) return { status: "skipped", detail: "no agents/hermes directory" };
|
|
19229
|
+
if (!isPlane) return { status: "skipped", detail: "role.yaml plane.identifier is a Plane field" };
|
|
19230
|
+
const files = [];
|
|
19231
|
+
let touched2 = 0;
|
|
19232
|
+
let settled = 0;
|
|
19233
|
+
let entries;
|
|
19234
|
+
try {
|
|
19235
|
+
entries = readdirSync11(hermesDir, { withFileTypes: true });
|
|
19236
|
+
} catch (error) {
|
|
19237
|
+
return { status: "failed", detail: `${hermesDir}: ${message(error)}` };
|
|
19238
|
+
}
|
|
19239
|
+
for (const entry of entries) {
|
|
19240
|
+
if (!entry.isDirectory()) continue;
|
|
19241
|
+
const path = join31(hermesDir, entry.name, "role.yaml");
|
|
19242
|
+
if (!existsSync22(path)) continue;
|
|
19243
|
+
try {
|
|
19244
|
+
const original = readFileSync27(path, "utf8");
|
|
19245
|
+
const document = YAML8.parseDocument(original);
|
|
19246
|
+
if (document.errors.length) throw new Error("YAML is invalid");
|
|
19247
|
+
const node = document.getIn(["plane", "identifier"], true);
|
|
19248
|
+
if (!YAML8.isScalar(node) || !node.range) continue;
|
|
19249
|
+
if (String(node.value) === NEW) {
|
|
19250
|
+
settled += 1;
|
|
19251
|
+
continue;
|
|
19252
|
+
}
|
|
19253
|
+
touched2 += 1;
|
|
19254
|
+
files.push(path);
|
|
19255
|
+
if (!doApply) continue;
|
|
19256
|
+
const [start, end] = node.range;
|
|
19257
|
+
const text2 = `${original.slice(0, start)}${yamlScalarFor(NEW, original.slice(start, end))}${original.slice(end)}`;
|
|
19258
|
+
const before = document.toJS();
|
|
19259
|
+
const after = YAML8.parse(text2);
|
|
19260
|
+
if (isRecord7(before.plane)) before.plane.identifier = NEW;
|
|
19261
|
+
if (JSON.stringify(after) !== JSON.stringify(before)) throw new Error("the edit changed more than plane.identifier");
|
|
19262
|
+
atomicWrite(path, text2);
|
|
19263
|
+
} catch (error) {
|
|
19264
|
+
return { status: "failed", detail: `${path}: ${message(error)}` };
|
|
19265
|
+
}
|
|
19266
|
+
}
|
|
19267
|
+
if (!touched2 && !settled) return { status: "skipped", detail: "no role.yaml carries plane.identifier" };
|
|
19268
|
+
if (!touched2) return { status: "already", detail: `${settled} role.yaml already read ${NEW}` };
|
|
19269
|
+
return { status: doApply ? "done" : "planned", detail: `${touched2} role.yaml: plane.identifier -> ${NEW}`, files };
|
|
19270
|
+
};
|
|
19271
|
+
const fleetStep = async (doApply) => {
|
|
19272
|
+
if (!isPlane || !boardId) return { status: "skipped", detail: "no Plane board bound" };
|
|
19273
|
+
const hermesPath = hermesAgentsRegistryPath(env2, home);
|
|
19274
|
+
if (!existsSync22(hermesPath)) return { status: "skipped", detail: `no fleet registry at ${hermesPath}` };
|
|
19275
|
+
try {
|
|
19276
|
+
const bound = readHermesAgentBoards(hermesPath).filter((agent) => agent.boardId === boardId);
|
|
19277
|
+
if (!bound.length) return { status: "skipped", detail: "no fleet agent is bound to this board" };
|
|
19278
|
+
const stale = bound.filter((agent) => agent.identifier !== NEW);
|
|
19279
|
+
if (!stale.length) return { status: "already", detail: `${bound.length} fleet agent(s) already read ${NEW}` };
|
|
19280
|
+
if (!doApply) return { status: "planned", detail: `${stale.length} fleet agent(s): plane.identifier -> ${NEW} (${hermesPath})`, files: [hermesPath] };
|
|
19281
|
+
const report = await reconcileProjectIdentity({
|
|
19282
|
+
target: newId,
|
|
19283
|
+
apply: true,
|
|
19284
|
+
hermesRegistryPath: hermesPath,
|
|
19285
|
+
registryPath,
|
|
19286
|
+
env: env2,
|
|
19287
|
+
home,
|
|
19288
|
+
now: new Date(now),
|
|
19289
|
+
...options.fetchBoards ? { fetchBoards: options.fetchBoards } : {}
|
|
19290
|
+
});
|
|
19291
|
+
if (!report.ok) return { status: "failed", detail: report.errors.join("; ") || "identity reconcile reported errors" };
|
|
19292
|
+
const lagging = readHermesAgentBoards(hermesPath).filter((agent) => agent.boardId === boardId && agent.identifier !== NEW);
|
|
19293
|
+
if (lagging.length) {
|
|
19294
|
+
return { status: "failed", detail: `fleet agent(s) still read another identifier: ${lagging.map((agent) => `${agent.agentId} (${agent.identifier || "empty"})`).join(", ")}` };
|
|
19295
|
+
}
|
|
19296
|
+
return { status: "done", detail: `${stale.length} fleet agent(s): plane.identifier -> ${NEW} (${hermesPath})`, files: [hermesPath] };
|
|
19297
|
+
} catch (error) {
|
|
19298
|
+
return { status: "failed", detail: message(error) };
|
|
19299
|
+
}
|
|
19300
|
+
};
|
|
19301
|
+
const agentsMdStep = async (doApply) => {
|
|
19302
|
+
const link = join31(repoPath, "AGENTS.md");
|
|
19303
|
+
if (!existsSync22(link)) return { status: "skipped", detail: "no AGENTS.md" };
|
|
19304
|
+
try {
|
|
19305
|
+
const path = realpathSync11(link);
|
|
19306
|
+
const inside = relative16(realpathSync11(repoPath), path);
|
|
19307
|
+
if (inside.startsWith("..") || isAbsolute11(inside)) {
|
|
19308
|
+
return { status: "skipped", detail: `AGENTS.md resolves outside the repo (${path}); left untouched` };
|
|
19309
|
+
}
|
|
19310
|
+
const original = readFileSync27(path, "utf8");
|
|
19311
|
+
const lines = original.split("\n");
|
|
19312
|
+
let boardLines = 0;
|
|
19313
|
+
let replaced = 0;
|
|
19314
|
+
let current = 0;
|
|
19315
|
+
const out = lines.map((line) => {
|
|
19316
|
+
if (!/^\s*Board:/.test(line)) return line;
|
|
19317
|
+
boardLines += 1;
|
|
19318
|
+
if (line.includes(`(${NEW})`)) current += 1;
|
|
19319
|
+
let next = line;
|
|
19320
|
+
for (const old of oldIdentifiers) {
|
|
19321
|
+
const swapped = next.replace(new RegExp(`\\(${escapeRegExp2(old)}\\)`, "g"), `(${NEW})`);
|
|
19322
|
+
if (swapped !== next) {
|
|
19323
|
+
replaced += 1;
|
|
19324
|
+
next = swapped;
|
|
19325
|
+
}
|
|
19326
|
+
}
|
|
19327
|
+
return next;
|
|
19328
|
+
});
|
|
19329
|
+
if (!boardLines) return { status: "skipped", detail: "AGENTS.md has no Board: line" };
|
|
19330
|
+
if (!replaced) {
|
|
19331
|
+
return current ? { status: "already", detail: `Board: line already reads (${NEW})` } : { status: "skipped", detail: `no (${oldIdentifier || "OLD"}) on the Board: line` };
|
|
19332
|
+
}
|
|
19333
|
+
if (doApply) atomicWrite(path, out.join("\n"));
|
|
19334
|
+
return { status: doApply ? "done" : "planned", detail: `Board: (${oldIdentifier}) -> (${NEW})`, files: [path] };
|
|
19335
|
+
} catch (error) {
|
|
19336
|
+
return { status: "failed", detail: message(error) };
|
|
19337
|
+
}
|
|
19338
|
+
};
|
|
19339
|
+
const hintsStep = async (doApply) => {
|
|
19340
|
+
const configDir = env2.PILOT_CONFIG_DIR?.trim() || join31(env2.XDG_CONFIG_HOME?.trim() || join31(home, ".config"), "pilot");
|
|
19341
|
+
const path = join31(configDir, "harvest", "project-hints.json");
|
|
19342
|
+
if (!existsSync22(path)) return { status: "skipped", detail: `no harvest hints at ${path}` };
|
|
19343
|
+
try {
|
|
19344
|
+
const hints = readJsonObject(path);
|
|
19345
|
+
const projects = hints.projects;
|
|
19346
|
+
if (!isRecord7(projects)) return { status: "skipped", detail: "hints file has no projects map" };
|
|
19347
|
+
if (oldId === newId || !Object.hasOwn(projects, oldId)) {
|
|
19348
|
+
return Object.hasOwn(projects, newId) ? { status: "already", detail: `hints already keyed ${newId}` } : { status: "skipped", detail: `no hints entry for ${oldId}` };
|
|
19349
|
+
}
|
|
19350
|
+
const collides = Object.hasOwn(projects, newId);
|
|
19351
|
+
const detail = collides ? `projects.${oldId} merged into the existing projects.${newId}; "${oldId}" kept as an alias` : `projects.${oldId} -> projects.${newId}, "${oldId}" kept as an alias`;
|
|
19352
|
+
if (!doApply) return { status: "planned", detail, files: [path] };
|
|
19353
|
+
const aliasesOf = (value) => isRecord7(value) && Array.isArray(value.aliases) ? value.aliases.map(String) : [];
|
|
19354
|
+
const moved = projects[oldId];
|
|
19355
|
+
const base = collides ? isRecord7(projects[newId]) ? { ...projects[newId] } : {} : isRecord7(moved) ? { ...moved } : {};
|
|
19356
|
+
base.aliases = [.../* @__PURE__ */ new Set([...aliasesOf(collides ? projects[newId] : moved), ...collides ? aliasesOf(moved) : [], oldId])];
|
|
19357
|
+
const rekeyed = {};
|
|
19358
|
+
for (const [key, value] of Object.entries(projects)) {
|
|
19359
|
+
if (key === oldId) {
|
|
19360
|
+
if (!collides) rekeyed[newId] = base;
|
|
19361
|
+
} else if (key === newId) rekeyed[newId] = base;
|
|
19362
|
+
else rekeyed[key] = value;
|
|
19363
|
+
}
|
|
19364
|
+
atomicWrite(path, `${JSON.stringify({ ...hints, projects: rekeyed }, null, 2)}
|
|
19365
|
+
`);
|
|
19366
|
+
return { status: "done", detail, files: [path] };
|
|
19367
|
+
} catch (error) {
|
|
19368
|
+
return { status: "failed", detail: message(error) };
|
|
19369
|
+
}
|
|
19370
|
+
};
|
|
19371
|
+
const candystoreStep = async (doApply) => {
|
|
19372
|
+
if (oldInput === newId && steps.every((step) => step.status === "already" || step.status === "skipped")) {
|
|
19373
|
+
return { status: "already", detail: "nothing changed; projection not re-synced" };
|
|
19374
|
+
}
|
|
19375
|
+
const cwd = join31(home, "code", "33GOD", "candystore");
|
|
19376
|
+
try {
|
|
19377
|
+
if (!statSync6(cwd).isDirectory()) throw new Error("not a directory");
|
|
19378
|
+
} catch {
|
|
19379
|
+
return { status: "skipped", detail: `no candystore checkout at ${cwd}` };
|
|
19380
|
+
}
|
|
19381
|
+
if (!doApply) return { status: "planned", detail: `mise run project:sync in ${cwd}` };
|
|
19382
|
+
const run = spawnSync15("mise", ["run", "project:sync"], { cwd, env: env2, shell: false, encoding: "utf8", timeout: 18e4, maxBuffer: 8 * 1024 * 1024 });
|
|
19383
|
+
if (run.error) {
|
|
19384
|
+
return run.error.code === "ENOENT" ? { status: "skipped", detail: "mise is not installed; run `mise run project:sync` in candystore" } : { status: "failed", detail: `mise run project:sync: ${run.error.message}` };
|
|
19385
|
+
}
|
|
19386
|
+
if (run.status !== 0) {
|
|
19387
|
+
const tail = `${run.stderr ?? ""}${run.stdout ?? ""}`.trim().split("\n").slice(-3).join(" | ").slice(0, 300);
|
|
19388
|
+
return { status: "failed", detail: `mise run project:sync exited ${run.status}${tail ? `: ${tail}` : ""}` };
|
|
19389
|
+
}
|
|
19390
|
+
return { status: "done", detail: "candystore project projection re-synced" };
|
|
19391
|
+
};
|
|
19392
|
+
const steps = [];
|
|
19393
|
+
const changedFiles = [];
|
|
19394
|
+
const note = (step, outcome) => {
|
|
19395
|
+
steps.push({ step, status: outcome.status, detail: outcome.detail });
|
|
19396
|
+
if (outcome.status === "done" || outcome.status === "planned") changedFiles.push(...outcome.files ?? []);
|
|
19397
|
+
};
|
|
19398
|
+
const critical = [
|
|
19399
|
+
["plane", planeStep],
|
|
19400
|
+
["manifest", manifestStep],
|
|
19401
|
+
["registry", registryStep]
|
|
19402
|
+
];
|
|
19403
|
+
let blockedBy;
|
|
19404
|
+
for (const [name, run] of critical) {
|
|
19405
|
+
if (blockedBy) {
|
|
19406
|
+
note(name, { status: "blocked", detail: `not attempted: ${blockedBy} failed` });
|
|
19407
|
+
continue;
|
|
19408
|
+
}
|
|
19409
|
+
const outcome = await run(apply);
|
|
19410
|
+
note(name, outcome);
|
|
19411
|
+
if (outcome.status === "failed") blockedBy = name;
|
|
19412
|
+
}
|
|
19413
|
+
const downstream = [
|
|
19414
|
+
["role.yaml", roleStep],
|
|
19415
|
+
["fleet-registry", fleetStep],
|
|
19416
|
+
["AGENTS.md", agentsMdStep],
|
|
19417
|
+
["harvest-hints", hintsStep],
|
|
19418
|
+
["candystore", candystoreStep]
|
|
19419
|
+
];
|
|
19420
|
+
for (const [name, run] of downstream) {
|
|
19421
|
+
if (blockedBy) note(name, { status: "blocked", detail: `not attempted: ${blockedBy} failed` });
|
|
19422
|
+
else note(name, await run(apply));
|
|
19423
|
+
}
|
|
19424
|
+
const changed = (name) => steps.some((step) => step.step === name && (step.status === "done" || step.status === "planned"));
|
|
19425
|
+
if (oldId !== newId && (changed("manifest") || changed("registry"))) {
|
|
19426
|
+
warnings.push("notebook state is keyed by the project hash and restarts fresh under the new id");
|
|
19427
|
+
}
|
|
19428
|
+
if (isPlane && changed("plane")) {
|
|
19429
|
+
warnings.push(`old ticket refs (${oldIdentifier || "OLD"}-n) stop resolving once the board prefix changes`);
|
|
19430
|
+
}
|
|
19431
|
+
const insideRepo = (file) => {
|
|
19432
|
+
const rel = relative16(repoPath, file);
|
|
19433
|
+
return rel !== "" && !rel.startsWith("..") && !isAbsolute11(rel);
|
|
19434
|
+
};
|
|
19435
|
+
if (changedFiles.some(insideRepo)) {
|
|
19436
|
+
warnings.push(`files changed inside ${repoPath} must be committed and pushed`);
|
|
19437
|
+
}
|
|
19438
|
+
const failed = steps.find((step) => step.status === "failed");
|
|
19439
|
+
const noop = steps.every((step) => step.status === "already" || step.status === "skipped");
|
|
19440
|
+
const result2 = {
|
|
19441
|
+
ok: !failed,
|
|
19442
|
+
apply,
|
|
19443
|
+
noop,
|
|
19444
|
+
resumed,
|
|
19445
|
+
oldId,
|
|
19446
|
+
newId,
|
|
19447
|
+
oldIdentifier,
|
|
19448
|
+
newIdentifier: NEW,
|
|
19449
|
+
provider: type || "none",
|
|
19450
|
+
workspace,
|
|
19451
|
+
boardId,
|
|
19452
|
+
registryPath,
|
|
19453
|
+
steps,
|
|
19454
|
+
warnings,
|
|
19455
|
+
changedFiles: [...new Set(changedFiles)]
|
|
19456
|
+
};
|
|
19457
|
+
if (failed) {
|
|
19458
|
+
result2.error = apply ? `${failed.step} failed: ${failed.detail}; re-run the same command to resume` : `${failed.step} would fail: ${failed.detail}; fix this before running with --apply`;
|
|
19459
|
+
result2.code = "step_failed";
|
|
19460
|
+
}
|
|
19461
|
+
return result2;
|
|
19462
|
+
}
|
|
19463
|
+
|
|
18934
19464
|
// src/index.ts
|
|
18935
19465
|
init_boardQuery();
|
|
18936
19466
|
|
|
18937
19467
|
// src/describe/activity.ts
|
|
18938
|
-
import { spawn as spawn3, spawnSync as
|
|
18939
|
-
import { statSync as
|
|
18940
|
-
import { join as
|
|
19468
|
+
import { spawn as spawn3, spawnSync as spawnSync16 } from "node:child_process";
|
|
19469
|
+
import { statSync as statSync7 } from "node:fs";
|
|
19470
|
+
import { join as join32 } from "node:path";
|
|
18941
19471
|
var ACTIVE_WINDOW_SECONDS = 24 * 60 * 60;
|
|
18942
19472
|
var MAX_DIRTY_STATS = 500;
|
|
18943
19473
|
var GIT_TIMEOUT_MS = 5e3;
|
|
18944
19474
|
var GIT_MAX_BUFFER = 16 * 1024 * 1024;
|
|
18945
19475
|
function git3(repo, args) {
|
|
18946
|
-
const result2 =
|
|
19476
|
+
const result2 = spawnSync16("git", ["-C", repo, ...args], {
|
|
18947
19477
|
encoding: "utf8",
|
|
18948
19478
|
timeout: GIT_TIMEOUT_MS,
|
|
18949
19479
|
maxBuffer: GIT_MAX_BUFFER
|
|
@@ -19102,7 +19632,7 @@ function uncommittedSource(repo, paths) {
|
|
|
19102
19632
|
let newest = 0;
|
|
19103
19633
|
for (const path of paths.slice(0, MAX_DIRTY_STATS)) {
|
|
19104
19634
|
try {
|
|
19105
|
-
const mtime = Math.floor(
|
|
19635
|
+
const mtime = Math.floor(statSync7(join32(repo, path)).mtimeMs / 1e3);
|
|
19106
19636
|
if (mtime > newest) newest = mtime;
|
|
19107
19637
|
} catch {
|
|
19108
19638
|
}
|
|
@@ -19300,8 +19830,8 @@ function openUrl(url, env2 = process.env, platform = process.platform) {
|
|
|
19300
19830
|
}
|
|
19301
19831
|
|
|
19302
19832
|
// src/describe/index.ts
|
|
19303
|
-
import { existsSync as
|
|
19304
|
-
import { join as
|
|
19833
|
+
import { existsSync as existsSync23, readFileSync as readFileSync28, readdirSync as readdirSync12, statSync as statSync8 } from "node:fs";
|
|
19834
|
+
import { join as join33, resolve as resolve23 } from "node:path";
|
|
19305
19835
|
init_project();
|
|
19306
19836
|
init_config();
|
|
19307
19837
|
init_state();
|
|
@@ -19357,7 +19887,7 @@ var CONFIG_FILES = [
|
|
|
19357
19887
|
];
|
|
19358
19888
|
function readJson(path) {
|
|
19359
19889
|
try {
|
|
19360
|
-
const parsed = JSON.parse(
|
|
19890
|
+
const parsed = JSON.parse(readFileSync28(path, "utf8"));
|
|
19361
19891
|
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : void 0;
|
|
19362
19892
|
} catch {
|
|
19363
19893
|
return void 0;
|
|
@@ -19379,23 +19909,23 @@ function describeType(repo) {
|
|
|
19379
19909
|
const evidence = [];
|
|
19380
19910
|
const note = (signal, file) => evidence.push(`${signal} (${file})`);
|
|
19381
19911
|
for (const marker of LANGUAGE_MARKERS) {
|
|
19382
|
-
if (!
|
|
19912
|
+
if (!existsSync23(join33(repo, marker.file))) continue;
|
|
19383
19913
|
if (!languages.includes(marker.language)) {
|
|
19384
19914
|
languages.push(marker.language);
|
|
19385
19915
|
note(marker.language, marker.file);
|
|
19386
19916
|
}
|
|
19387
19917
|
}
|
|
19388
19918
|
try {
|
|
19389
|
-
const dotnet =
|
|
19919
|
+
const dotnet = readdirSync12(repo).find((entry) => entry.endsWith(".csproj") || entry.endsWith(".sln"));
|
|
19390
19920
|
if (dotnet && !languages.includes("dotnet")) {
|
|
19391
19921
|
languages.push("dotnet");
|
|
19392
19922
|
note("dotnet", dotnet);
|
|
19393
19923
|
}
|
|
19394
19924
|
} catch {
|
|
19395
19925
|
}
|
|
19396
|
-
const pkg = readJson(
|
|
19926
|
+
const pkg = readJson(join33(repo, "package.json"));
|
|
19397
19927
|
if (pkg) {
|
|
19398
|
-
if (
|
|
19928
|
+
if (existsSync23(join33(repo, "tsconfig.json"))) {
|
|
19399
19929
|
const index = languages.indexOf("javascript");
|
|
19400
19930
|
if (index >= 0) languages.splice(index, 1);
|
|
19401
19931
|
if (!languages.includes("typescript")) {
|
|
@@ -19429,14 +19959,14 @@ function describeType(repo) {
|
|
|
19429
19959
|
["hermes-fleet-host", "agents/hermes"]
|
|
19430
19960
|
];
|
|
19431
19961
|
for (const [role, marker] of roleMarkers) {
|
|
19432
|
-
if (!
|
|
19962
|
+
if (!existsSync23(join33(repo, marker))) continue;
|
|
19433
19963
|
roles.push(role);
|
|
19434
19964
|
note(role, marker);
|
|
19435
19965
|
}
|
|
19436
19966
|
return { primaryLanguage: languages[0], languages, roles, evidence };
|
|
19437
19967
|
}
|
|
19438
19968
|
function describeIdentity(repo, registryPath) {
|
|
19439
|
-
const manifestPath =
|
|
19969
|
+
const manifestPath = join33(repo, ".project.json");
|
|
19440
19970
|
const manifest = readJson(manifestPath);
|
|
19441
19971
|
const drift = [];
|
|
19442
19972
|
let record3;
|
|
@@ -19529,12 +20059,12 @@ function describeSubsystems(repo, findings) {
|
|
|
19529
20059
|
const markers = SUBSYSTEM_MARKERS[metadata.id] ?? [];
|
|
19530
20060
|
const evidence = metadata.id === "notebook" ? (() => {
|
|
19531
20061
|
try {
|
|
19532
|
-
const manifest = JSON.parse(
|
|
20062
|
+
const manifest = JSON.parse(readFileSync28(join33(repo, ".project.json"), "utf8"));
|
|
19533
20063
|
return manifest.notebook && typeof manifest.notebook === "object" ? [".project.json#notebook"] : [];
|
|
19534
20064
|
} catch {
|
|
19535
20065
|
return [];
|
|
19536
20066
|
}
|
|
19537
|
-
})() : markers.filter((marker) =>
|
|
20067
|
+
})() : markers.filter((marker) => existsSync23(join33(repo, marker)));
|
|
19538
20068
|
const rules = (byRecipe.get(metadata.id) ?? []).map((finding2) => ({
|
|
19539
20069
|
id: finding2.id,
|
|
19540
20070
|
title: finding2.title,
|
|
@@ -19553,7 +20083,7 @@ function describeNotebook(repo, registryPath) {
|
|
|
19553
20083
|
try {
|
|
19554
20084
|
const registry = loadProjectRegistry(registryPath);
|
|
19555
20085
|
const project = Object.values(registry.projects).find((entry) => resolve23(entry.repo_path) === resolve23(repo));
|
|
19556
|
-
const manifest =
|
|
20086
|
+
const manifest = existsSync23(join33(repo, ".project.json")) ? JSON.parse(readFileSync28(join33(repo, ".project.json"), "utf8")) : void 0;
|
|
19557
20087
|
const declared = Boolean(project?.notebook || manifest?.notebook && typeof manifest.notebook === "object");
|
|
19558
20088
|
if (!declared) return { declared: false, bindingState: null, notebookId: null, overviewNoteId: null, health: null, remoteCheck: "skip", captureAdmission: null };
|
|
19559
20089
|
const config = loadEffectiveNotebookConfig(repo, registryPath);
|
|
@@ -19571,7 +20101,7 @@ function describeNotebook(repo, registryPath) {
|
|
|
19571
20101
|
}
|
|
19572
20102
|
}
|
|
19573
20103
|
function describeConfigFiles(repo) {
|
|
19574
|
-
return CONFIG_FILES.filter((spec) =>
|
|
20104
|
+
return CONFIG_FILES.filter((spec) => existsSync23(join33(repo, spec.path))).map((spec) => ({ path: spec.path, purpose: spec.purpose, subsystem: spec.subsystem }));
|
|
19575
20105
|
}
|
|
19576
20106
|
function describeNextSteps(description, findings) {
|
|
19577
20107
|
const steps = [];
|
|
@@ -19650,8 +20180,8 @@ function describeNextSteps(description, findings) {
|
|
|
19650
20180
|
}
|
|
19651
20181
|
async function describeProject(input = {}) {
|
|
19652
20182
|
const repo = resolve23(input.repoArg ?? process.cwd());
|
|
19653
|
-
if (!
|
|
19654
|
-
if (!
|
|
20183
|
+
if (!existsSync23(repo)) throw new Error(`Path does not exist: ${repo}`);
|
|
20184
|
+
if (!statSync8(repo).isDirectory()) throw new Error(`Not a directory: ${repo}`);
|
|
19655
20185
|
const registryPath = input.registryPath ?? projectRegistryPath();
|
|
19656
20186
|
const report = await recipeRegistry.auditRecipes(lifecycleContext(repo, true, false, { registryPath }));
|
|
19657
20187
|
const findings = report.rules;
|
|
@@ -19995,7 +20525,7 @@ init_style();
|
|
|
19995
20525
|
// src/notebook/cli.ts
|
|
19996
20526
|
init_project();
|
|
19997
20527
|
init_capture();
|
|
19998
|
-
import { closeSync as closeSync8, constants as constants6, existsSync as
|
|
20528
|
+
import { closeSync as closeSync8, constants as constants6, existsSync as existsSync24, fstatSync as fstatSync4, lstatSync as lstatSync18, openSync as openSync8, readSync as readSync4 } from "node:fs";
|
|
19999
20529
|
import { resolve as resolve24 } from "node:path";
|
|
20000
20530
|
import { createInterface } from "node:readline/promises";
|
|
20001
20531
|
|
|
@@ -20137,7 +20667,7 @@ async function execute(input) {
|
|
|
20137
20667
|
}
|
|
20138
20668
|
function readBoundedRegularFile(path, maxBytes) {
|
|
20139
20669
|
const absolute = resolve24(path);
|
|
20140
|
-
if (!
|
|
20670
|
+
if (!existsSync24(absolute)) throw new NotebookError("INVALID_INPUT", `File not found: ${absolute}`);
|
|
20141
20671
|
const before = lstatSync18(absolute);
|
|
20142
20672
|
if (!before.isFile() || before.isSymbolicLink()) throw new NotebookError("INVALID_INPUT", `File must be a regular non-symlink: ${absolute}`);
|
|
20143
20673
|
if (before.size > maxBytes) throw new NotebookError("INVALID_INPUT", `File exceeds the configured ceiling: ${absolute}`);
|
|
@@ -20321,7 +20851,7 @@ function registerNotebookCli(program2, module = new NotebookModule()) {
|
|
|
20321
20851
|
const registry = loadProjectRegistry(registryFile);
|
|
20322
20852
|
const root = notebookStateRoot();
|
|
20323
20853
|
const explicit = process.env.PJ_NOTEBOOK_WORKER_PROJECT_SLUG?.toLowerCase();
|
|
20324
|
-
const slug = explicit && registry.projects[explicit] ? explicit : Object.keys(registry.projects).find((candidate) =>
|
|
20854
|
+
const slug = explicit && registry.projects[explicit] ? explicit : Object.keys(registry.projects).find((candidate) => existsSync24(statePathForReceipt(root, candidate, options.receiptId)));
|
|
20325
20855
|
if (!slug) throw new NotebookError("NOT_FOUND", "Receipt does not belong to a registered project");
|
|
20326
20856
|
await runCaptureWorker(new NotebookModule({ registryPath: registryFile, stateRoot: root }), slug, options.receiptId);
|
|
20327
20857
|
} catch (error) {
|
|
@@ -20377,7 +20907,7 @@ init_project();
|
|
|
20377
20907
|
init_style();
|
|
20378
20908
|
|
|
20379
20909
|
// src/tui/load.ts
|
|
20380
|
-
import { existsSync as
|
|
20910
|
+
import { existsSync as existsSync25 } from "node:fs";
|
|
20381
20911
|
import { fileURLToPath as fileURLToPath6 } from "node:url";
|
|
20382
20912
|
var TuiUnavailableError = class extends Error {
|
|
20383
20913
|
};
|
|
@@ -20399,7 +20929,7 @@ async function loadScreen(relativeToDist, base) {
|
|
|
20399
20929
|
} catch (error) {
|
|
20400
20930
|
const err = error;
|
|
20401
20931
|
if (err?.code !== "ERR_MODULE_NOT_FOUND") throw error;
|
|
20402
|
-
if (!
|
|
20932
|
+
if (!existsSync25(fileURLToPath6(url))) {
|
|
20403
20933
|
throw new TuiUnavailableError(`TUI screen ${relativeToDist} is not built (run: npm run build); looked for ${url.pathname}`);
|
|
20404
20934
|
}
|
|
20405
20935
|
if (typeof err.stack !== "string" || !RESOLVER_FRAME.test(err.stack)) throw error;
|
|
@@ -20412,9 +20942,9 @@ async function loadScreen(relativeToDist, base) {
|
|
|
20412
20942
|
// src/crank/index.ts
|
|
20413
20943
|
init_boardQuery();
|
|
20414
20944
|
init_style();
|
|
20415
|
-
import { spawnSync as
|
|
20416
|
-
import { readFileSync as
|
|
20417
|
-
import { join as
|
|
20945
|
+
import { spawnSync as spawnSync17 } from "node:child_process";
|
|
20946
|
+
import { readFileSync as readFileSync29 } from "node:fs";
|
|
20947
|
+
import { join as join34 } from "node:path";
|
|
20418
20948
|
var ZCRANK_REDIS_HOST = "127.0.0.1";
|
|
20419
20949
|
var ZCRANK_REDIS_PORT = 6379;
|
|
20420
20950
|
function redisEndpoint(env2 = process.env) {
|
|
@@ -20428,7 +20958,7 @@ var ZCRANK_SESSION = "Workspace";
|
|
|
20428
20958
|
var ZCRANK_LOG_CAP = 500;
|
|
20429
20959
|
var TAB_STATUS_SUFFIX = /[\s🟢🟡🔴🔔⚫✳✻●·─]+$/u;
|
|
20430
20960
|
var defaultShell = (command, args) => {
|
|
20431
|
-
const result2 =
|
|
20961
|
+
const result2 = spawnSync17(command, args, { encoding: "utf8", timeout: 1e4 });
|
|
20432
20962
|
if (result2.error) return { stdout: "", status: 127 };
|
|
20433
20963
|
return { stdout: result2.stdout ?? "", status: result2.status ?? 1, stderr: result2.stderr ?? "" };
|
|
20434
20964
|
};
|
|
@@ -20694,7 +21224,7 @@ function resolveTabForProject(project, live, storedTabs) {
|
|
|
20694
21224
|
function projectNameFor(handle, registryName) {
|
|
20695
21225
|
if (registryName && registryName.trim()) return registryName.trim();
|
|
20696
21226
|
try {
|
|
20697
|
-
const manifest = JSON.parse(
|
|
21227
|
+
const manifest = JSON.parse(readFileSync29(join34(handle.root, ".project.json"), "utf8"));
|
|
20698
21228
|
const name = manifest.project_name;
|
|
20699
21229
|
if (typeof name === "string" && name.trim()) return name.trim();
|
|
20700
21230
|
} catch {
|
|
@@ -20705,7 +21235,7 @@ function projectAgentsFor(handle, registryAgents) {
|
|
|
20705
21235
|
const fromRecord = Object.entries(registryAgents ?? {}).map(([agentHandle, value]) => ({ handle: agentHandle, role: String(value?.role ?? "") }));
|
|
20706
21236
|
if (fromRecord.length) return fromRecord;
|
|
20707
21237
|
try {
|
|
20708
|
-
const manifest = JSON.parse(
|
|
21238
|
+
const manifest = JSON.parse(readFileSync29(join34(handle.root, ".project.json"), "utf8"));
|
|
20709
21239
|
const agents = manifest.agents;
|
|
20710
21240
|
if (agents && typeof agents === "object" && !Array.isArray(agents)) {
|
|
20711
21241
|
return Object.entries(agents).map(([agentHandle, value]) => ({ handle: agentHandle, role: typeof value?.role === "string" ? value.role : "" }));
|
|
@@ -21112,13 +21642,13 @@ function printMigrationReport(report, asJson) {
|
|
|
21112
21642
|
}
|
|
21113
21643
|
async function promptForRuleIds(rules) {
|
|
21114
21644
|
const fixable = rules.filter((rule) => rule.fixable);
|
|
21115
|
-
const { message, options } = formatRulePicker(fixable);
|
|
21645
|
+
const { message: message2, options } = formatRulePicker(fixable);
|
|
21116
21646
|
if (!options.length) {
|
|
21117
21647
|
return [];
|
|
21118
21648
|
}
|
|
21119
21649
|
const initialValues = fixable.filter((rule) => rule.status !== "pass" && rule.status !== "skip").map((rule) => rule.id);
|
|
21120
21650
|
const selected = await multiselect({
|
|
21121
|
-
message,
|
|
21651
|
+
message: message2,
|
|
21122
21652
|
options,
|
|
21123
21653
|
initialValues
|
|
21124
21654
|
});
|
|
@@ -21128,16 +21658,16 @@ async function promptForRuleIds(rules) {
|
|
|
21128
21658
|
return selected;
|
|
21129
21659
|
}
|
|
21130
21660
|
function readJson2(path) {
|
|
21131
|
-
if (!
|
|
21661
|
+
if (!existsSync27(path)) return void 0;
|
|
21132
21662
|
try {
|
|
21133
|
-
const parsed = JSON.parse(
|
|
21663
|
+
const parsed = JSON.parse(readFileSync30(path, "utf8"));
|
|
21134
21664
|
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : void 0;
|
|
21135
21665
|
} catch {
|
|
21136
21666
|
return void 0;
|
|
21137
21667
|
}
|
|
21138
21668
|
}
|
|
21139
21669
|
function findGitRoot(cwd) {
|
|
21140
|
-
const result2 =
|
|
21670
|
+
const result2 = spawnSync18("git", ["rev-parse", "--show-toplevel"], { cwd, encoding: "utf8" });
|
|
21141
21671
|
if (result2.status !== 0) return void 0;
|
|
21142
21672
|
return resolve25(result2.stdout.trim());
|
|
21143
21673
|
}
|
|
@@ -21147,8 +21677,8 @@ function packageNameToProjectName(value) {
|
|
|
21147
21677
|
return name.replace(/[-_]+/g, " ").replace(/\b\w/g, (letter) => letter.toUpperCase()).trim();
|
|
21148
21678
|
}
|
|
21149
21679
|
function deriveProjectDefaults(targetDir) {
|
|
21150
|
-
const manifest = readJson2(
|
|
21151
|
-
const pkg = readJson2(
|
|
21680
|
+
const manifest = readJson2(join35(targetDir, ".project.json"));
|
|
21681
|
+
const pkg = readJson2(join35(targetDir, "package.json"));
|
|
21152
21682
|
const name = String(manifest?.project_name ?? "").trim() || packageNameToProjectName(typeof pkg?.name === "string" ? pkg.name : void 0) || packageNameToProjectName(basename11(targetDir)) || "Project";
|
|
21153
21683
|
const ticketProvider = manifest?.ticket_provider && typeof manifest.ticket_provider === "object" ? manifest.ticket_provider : {};
|
|
21154
21684
|
return {
|
|
@@ -21164,9 +21694,9 @@ function deriveProjectDefaults(targetDir) {
|
|
|
21164
21694
|
function isInteractiveProjectInit(options) {
|
|
21165
21695
|
return !options.json && !options.yes && options.tui !== false && Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
21166
21696
|
}
|
|
21167
|
-
async function promptTextValue(
|
|
21697
|
+
async function promptTextValue(message2, initialValue) {
|
|
21168
21698
|
const value = await text({
|
|
21169
|
-
message,
|
|
21699
|
+
message: message2,
|
|
21170
21700
|
initialValue,
|
|
21171
21701
|
validate: (input) => input?.trim() ? void 0 : "Required"
|
|
21172
21702
|
});
|
|
@@ -21223,7 +21753,7 @@ function actionNeedsRun(plan, kind, syncMode) {
|
|
|
21223
21753
|
if (!action || action.kind !== "project.write-manifest") return false;
|
|
21224
21754
|
const next = `${JSON.stringify(action.manifest, null, 2)}
|
|
21225
21755
|
`;
|
|
21226
|
-
return !
|
|
21756
|
+
return !existsSync27(action.path) || readFileSync30(action.path, "utf8") !== next;
|
|
21227
21757
|
}
|
|
21228
21758
|
if (kind === "copier.copy.commonproject") return true;
|
|
21229
21759
|
if (kind === "ticket-provider.create-or-link") return plan.actions.some((action) => action.kind === kind && action.enabled);
|
|
@@ -21280,7 +21810,7 @@ async function resolveProjectInitTarget(name, options) {
|
|
|
21280
21810
|
if (!targetDir && cwdGitRoot) {
|
|
21281
21811
|
targetDir = cwdGitRoot;
|
|
21282
21812
|
}
|
|
21283
|
-
if (!targetDir && !name &&
|
|
21813
|
+
if (!targetDir && !name && existsSync27(join35(cwd, ".project.json"))) {
|
|
21284
21814
|
targetDir = cwd;
|
|
21285
21815
|
}
|
|
21286
21816
|
if (!targetDir && interactive) {
|
|
@@ -21291,14 +21821,14 @@ async function resolveProjectInitTarget(name, options) {
|
|
|
21291
21821
|
if (!targetDir) {
|
|
21292
21822
|
throw new Error("Project name or --target-dir is required when project init is not run inside a git repo or a directory carrying .project.json");
|
|
21293
21823
|
}
|
|
21294
|
-
const targetExists =
|
|
21295
|
-
if (targetExists && !
|
|
21824
|
+
const targetExists = existsSync27(targetDir);
|
|
21825
|
+
if (targetExists && !statSync9(targetDir).isDirectory()) throw new Error(`Target path is not a directory: ${targetDir}`);
|
|
21296
21826
|
const targetGitRoot = targetExists ? findGitRoot(targetDir) : void 0;
|
|
21297
|
-
const alreadyScaffolded = targetExists && (
|
|
21827
|
+
const alreadyScaffolded = targetExists && (existsSync27(join35(targetDir, ".project.json")) || existsSync27(join35(targetDir, ".copier-answers.yml")));
|
|
21298
21828
|
const syncMode = Boolean(
|
|
21299
21829
|
targetGitRoot && resolve25(targetGitRoot) === resolve25(targetDir) || alreadyScaffolded
|
|
21300
21830
|
);
|
|
21301
|
-
if (targetExists && !syncMode && !options.force &&
|
|
21831
|
+
if (targetExists && !syncMode && !options.force && readdirSync13(targetDir).length > 0) {
|
|
21302
21832
|
throw new Error(`Target already exists and is not empty: ${targetDir} (pass --force to render over it)`);
|
|
21303
21833
|
}
|
|
21304
21834
|
const defaults = targetExists ? deriveProjectDefaults(targetDir) : { name: packageNameToProjectName(basename11(targetDir)) ?? "Project", description: "" };
|
|
@@ -21384,8 +21914,8 @@ program.command("subsystems").description("List available subsystems").action(()
|
|
|
21384
21914
|
console.log("");
|
|
21385
21915
|
});
|
|
21386
21916
|
function boardFailure(error) {
|
|
21387
|
-
const
|
|
21388
|
-
console.error(`${xmark2} ${
|
|
21917
|
+
const message2 = error instanceof BoardError || error instanceof Error ? error.message : String(error);
|
|
21918
|
+
console.error(`${xmark2} ${message2}`);
|
|
21389
21919
|
process.exit(1);
|
|
21390
21920
|
}
|
|
21391
21921
|
function parseTicketCount(raw, fallback2) {
|
|
@@ -21601,6 +22131,39 @@ async function runProjectInit(name, options) {
|
|
|
21601
22131
|
process.exit(1);
|
|
21602
22132
|
}
|
|
21603
22133
|
}
|
|
22134
|
+
var RENAME_MARK = {
|
|
22135
|
+
planned: "plan",
|
|
22136
|
+
done: "done",
|
|
22137
|
+
already: "ok",
|
|
22138
|
+
skipped: "skip",
|
|
22139
|
+
blocked: "blocked",
|
|
22140
|
+
failed: "FAIL"
|
|
22141
|
+
};
|
|
22142
|
+
function formatRenameResult(result2) {
|
|
22143
|
+
const lines = [""];
|
|
22144
|
+
const headline = result2.noop ? "Nothing to rename" : result2.apply ? result2.ok ? "Renamed" : "Rename incomplete" : "Would rename";
|
|
22145
|
+
const mark = !result2.ok ? red(glyph.fail) : result2.apply && !result2.noop ? green(glyph.pass) : yellow(glyph.warn);
|
|
22146
|
+
lines.push(` ${mark} ${bold(headline)} ${dim(glyph.dot)} ${cyan(result2.oldId)} ${dim(glyph.pointer)} ${cyan(result2.newId)}${result2.resumed ? dim(" (resuming an earlier run)") : ""}`);
|
|
22147
|
+
lines.push(` ${dim("board".padEnd(14))} ${result2.oldIdentifier || dim("(none)")} ${dim(glyph.pointer)} ${result2.newIdentifier}${result2.boardId ? dim(` ${result2.provider}/${result2.workspace} ${result2.boardId}`) : ""}`);
|
|
22148
|
+
lines.push("");
|
|
22149
|
+
const width = Math.max(...result2.steps.map((step) => step.step.length));
|
|
22150
|
+
for (const step of result2.steps) {
|
|
22151
|
+
const label = RENAME_MARK[step.status].padEnd(7);
|
|
22152
|
+
const colored = step.status === "failed" ? red(label) : step.status === "done" ? green(label) : step.status === "planned" ? yellow(label) : dim(label);
|
|
22153
|
+
lines.push(` ${colored} ${step.step.padEnd(width)} ${step.status === "failed" ? step.detail : dim(step.detail)}`);
|
|
22154
|
+
}
|
|
22155
|
+
if (result2.changedFiles.length) {
|
|
22156
|
+
lines.push("");
|
|
22157
|
+
lines.push(` ${dim(result2.apply ? "files changed" : "files that would change")}`);
|
|
22158
|
+
for (const file of result2.changedFiles) lines.push(` ${dim(file)}`);
|
|
22159
|
+
}
|
|
22160
|
+
lines.push("");
|
|
22161
|
+
for (const warning of result2.warnings) lines.push(` ${yellow(glyph.warn)} ${dim(warning)}`);
|
|
22162
|
+
if (result2.error) lines.push(` ${red(glyph.fail)} ${result2.error}`);
|
|
22163
|
+
if (!result2.apply) lines.push(` ${dim("dry run \u2014 re-run with --apply to write")}`);
|
|
22164
|
+
lines.push("");
|
|
22165
|
+
return lines.join("\n");
|
|
22166
|
+
}
|
|
21604
22167
|
function registerProjectCommands(parent, legacy = false) {
|
|
21605
22168
|
parent.command("list").description("List projects in the pjangler registry").option("--registry <location>", `Registry service URL or fixture path (default: ${projectRegistryPath()})`).option("--json", "Output machine-parseable JSON").action(async (options) => {
|
|
21606
22169
|
try {
|
|
@@ -21658,9 +22221,9 @@ function registerProjectCommands(parent, legacy = false) {
|
|
|
21658
22221
|
if (!result2.apply) console.log(` ${dim("dry run \u2014 re-run with --apply to write")}`);
|
|
21659
22222
|
console.log("");
|
|
21660
22223
|
} catch (err) {
|
|
21661
|
-
const
|
|
21662
|
-
if (options.json) console.log(JSON.stringify({ ok: false, error:
|
|
21663
|
-
else console.error(`${xmark2} project remove failed: ${
|
|
22224
|
+
const message2 = err instanceof Error ? err.message : String(err);
|
|
22225
|
+
if (options.json) console.log(JSON.stringify({ ok: false, error: message2 }, null, 2));
|
|
22226
|
+
else console.error(`${xmark2} project remove failed: ${message2}`);
|
|
21664
22227
|
process.exit(1);
|
|
21665
22228
|
}
|
|
21666
22229
|
});
|
|
@@ -21690,9 +22253,28 @@ function registerProjectCommands(parent, legacy = false) {
|
|
|
21690
22253
|
if (!result2.apply) console.log(` ${dim("dry run \u2014 re-run with --apply to write")}`);
|
|
21691
22254
|
console.log("");
|
|
21692
22255
|
} catch (err) {
|
|
21693
|
-
const
|
|
21694
|
-
if (options.json) console.log(JSON.stringify({ ok: false, error:
|
|
21695
|
-
else console.error(`${xmark2} project link failed: ${
|
|
22256
|
+
const message2 = err instanceof Error ? err.message : String(err);
|
|
22257
|
+
if (options.json) console.log(JSON.stringify({ ok: false, error: message2 }, null, 2));
|
|
22258
|
+
else console.error(`${xmark2} project link failed: ${message2}`);
|
|
22259
|
+
process.exit(1);
|
|
22260
|
+
}
|
|
22261
|
+
});
|
|
22262
|
+
parent.command("rename").argument("<old-id>", "Current project ID").argument("<new-id>", "New project ID; also becomes the board identifier, upper-cased (1-12 lowercase letters/digits)").description("Rename a project: one key becomes the project ID and the board identifier, across registry, manifest and provider").option("--apply", "Write the rename (default is a dry run)").option("--registry <location>", `Registry service URL or fixture path (default: ${projectRegistryPath()})`).option("--json", "Output machine-parseable JSON").action(async (oldId, newId, options) => {
|
|
22263
|
+
try {
|
|
22264
|
+
const result2 = await renameProject({
|
|
22265
|
+
oldId,
|
|
22266
|
+
newId,
|
|
22267
|
+
apply: Boolean(options.apply),
|
|
22268
|
+
registryPath: options.registry
|
|
22269
|
+
});
|
|
22270
|
+
if (options.json) console.log(JSON.stringify(result2, null, 2));
|
|
22271
|
+
else console.log(formatRenameResult(result2));
|
|
22272
|
+
if (!result2.ok) process.exit(1);
|
|
22273
|
+
} catch (err) {
|
|
22274
|
+
const code = err instanceof RenameError ? err.code : "error";
|
|
22275
|
+
const text2 = err instanceof Error ? err.message : String(err);
|
|
22276
|
+
if (options.json) console.log(JSON.stringify({ ok: false, error: text2, code }, null, 2));
|
|
22277
|
+
else console.error(`${xmark2} project rename failed: [${code}] ${text2}`);
|
|
21696
22278
|
process.exit(1);
|
|
21697
22279
|
}
|
|
21698
22280
|
});
|
|
@@ -21764,7 +22346,7 @@ program.command("reindex").argument("[project-id]", "Canonical project ID (defau
|
|
|
21764
22346
|
else if (options.all) result2 = registryRequest(location, "POST", "/v1/reindex", {});
|
|
21765
22347
|
else {
|
|
21766
22348
|
const repo = projectId ? getProject(loadProjectRegistry(location), projectId).repo_path : resolveProject(process.cwd()).root;
|
|
21767
|
-
result2 = registryRequest(location, "POST", "/v1/index", { manifest_path:
|
|
22349
|
+
result2 = registryRequest(location, "POST", "/v1/index", { manifest_path: join35(repo, ".project.json") });
|
|
21768
22350
|
}
|
|
21769
22351
|
const statuses = result2.__registry_status;
|
|
21770
22352
|
const stale = Object.entries(statuses ?? {}).filter(([, item]) => item.status !== "ok");
|
package/dist/mcp-server.js
CHANGED
|
@@ -13329,6 +13329,18 @@ init_project();
|
|
|
13329
13329
|
import { spawnSync as spawnSync13 } from "node:child_process";
|
|
13330
13330
|
import { existsSync as existsSync18, lstatSync as lstatSync16, readFileSync as readFileSync20, rmSync as rmSync8 } from "node:fs";
|
|
13331
13331
|
import { dirname as dirname17, isAbsolute as isAbsolute10, join as join23, relative as relativePath, resolve as resolve18 } from "node:path";
|
|
13332
|
+
|
|
13333
|
+
// src/project/ticketScope.ts
|
|
13334
|
+
function ticketScope(ticket) {
|
|
13335
|
+
const block = ticket !== null && typeof ticket === "object" ? ticket : {};
|
|
13336
|
+
const label = typeof block.scope_label === "string" ? block.scope_label.trim() : "";
|
|
13337
|
+
return {
|
|
13338
|
+
...label ? { scope_label: label } : {},
|
|
13339
|
+
...block.scope_default === true ? { scope_default: true } : {}
|
|
13340
|
+
};
|
|
13341
|
+
}
|
|
13342
|
+
|
|
13343
|
+
// src/recipes/ProjectRecipe.ts
|
|
13332
13344
|
init_tree_diff();
|
|
13333
13345
|
|
|
13334
13346
|
// src/recipes/NotebookRecipe.ts
|
|
@@ -15214,6 +15226,7 @@ function refreshPlanFromCanonicalManifest(plan) {
|
|
|
15214
15226
|
...carriesProvenance && identifierSource === "provider" && recorded?.identifier_fetched_at ? { identifier_fetched_at: recorded.identifier_fetched_at } : {},
|
|
15215
15227
|
board_id: manifestBoardId,
|
|
15216
15228
|
...boardConfirmedAt ? { board_confirmed_at: boardConfirmedAt } : {},
|
|
15229
|
+
...ticketScope(manifestTicket),
|
|
15217
15230
|
state: manifestState === "skipped" ? "skipped" : manifestBoardId && boardConfirmedAt && keyIsProven ? "linked" : "planned"
|
|
15218
15231
|
};
|
|
15219
15232
|
plan.manifest = manifest;
|
|
@@ -225,7 +225,8 @@ function validateManifestFields(manifest, path) {
|
|
|
225
225
|
if (!object(artifact)) fail(`source_artifacts.${index}`, "an object");
|
|
226
226
|
strings(artifact, ["kind", "path", "package_name"], `source_artifacts.${index}.`);
|
|
227
227
|
}
|
|
228
|
-
if (manifest.ticket_provider) strings(manifest.ticket_provider, ["type", "workspace", "identifier", "identifier_source", "identifier_fetched_at", "board_id", "board_confirmed_at", "board_url", "state"], "ticket_provider.");
|
|
228
|
+
if (manifest.ticket_provider) strings(manifest.ticket_provider, ["type", "workspace", "identifier", "identifier_source", "identifier_fetched_at", "board_id", "board_confirmed_at", "board_url", "state", "scope_label"], "ticket_provider.");
|
|
229
|
+
if (manifest.ticket_provider?.scope_default !== void 0 && typeof manifest.ticket_provider.scope_default !== "boolean") fail("ticket_provider.scope_default", "a boolean");
|
|
229
230
|
for (const [name, agent] of Object.entries(manifest.agents ?? {})) {
|
|
230
231
|
if (!object(agent)) fail(`agents.${name}`, "an object");
|
|
231
232
|
strings(agent, ["role", "role_dir", "provisioning_state"], `agents.${name}.`);
|