@tablation/crew 0.61.0 → 0.63.0
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/cli.js +465 -267
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -5183,7 +5183,7 @@ function doctorSelftestLines(stored, toolchains) {
|
|
|
5183
5183
|
}
|
|
5184
5184
|
|
|
5185
5185
|
// src/cli.ts
|
|
5186
|
-
import { resolve as resolve5, dirname as
|
|
5186
|
+
import { resolve as resolve5, dirname as dirname18, join as join31, basename as basename10 } from "path";
|
|
5187
5187
|
import { fileURLToPath } from "url";
|
|
5188
5188
|
import { hostname as hostname2, homedir as homedir8 } from "os";
|
|
5189
5189
|
import { spawnSync as spawnSync6 } from "child_process";
|
|
@@ -5859,154 +5859,8 @@ function resetIncomplete(steps) {
|
|
|
5859
5859
|
return steps.some((s) => s.action === "failed" || s.action === "kept");
|
|
5860
5860
|
}
|
|
5861
5861
|
|
|
5862
|
-
// src/
|
|
5863
|
-
import {
|
|
5864
|
-
import { TablationClient as TablationClient3 } from "@tablation/client";
|
|
5865
|
-
|
|
5866
|
-
// src/contract.ts
|
|
5867
|
-
var DEFAULT_CONTRACT = {
|
|
5868
|
-
columns: {
|
|
5869
|
-
key: "issue_id",
|
|
5870
|
-
tag: "issue_tag",
|
|
5871
|
-
title: "title",
|
|
5872
|
-
status: "status",
|
|
5873
|
-
assignee: "assignee_id",
|
|
5874
|
-
priority: "priority",
|
|
5875
|
-
severity: "severity",
|
|
5876
|
-
slice: "project_id",
|
|
5877
|
-
repo: "repo_id",
|
|
5878
|
-
parent: "parent_id",
|
|
5879
|
-
blockedBy: "blocked_by",
|
|
5880
|
-
updatedAt: "updated_at",
|
|
5881
|
-
needsDesign: "needs_design",
|
|
5882
|
-
attachments: "attachments",
|
|
5883
|
-
workMode: "work_mode",
|
|
5884
|
-
reportType: "report_type",
|
|
5885
|
-
epic: "epic_id",
|
|
5886
|
-
author: "filed_by_id",
|
|
5887
|
-
ship: "ship_id",
|
|
5888
|
-
heldBy: "held_by_ship_id",
|
|
5889
|
-
resolutionNote: "resolution_note",
|
|
5890
|
-
verifiedSha: "verified_sha",
|
|
5891
|
-
prRef: "pr_ref",
|
|
5892
|
-
position: "position"
|
|
5893
|
-
},
|
|
5894
|
-
comments: {
|
|
5895
|
-
parent: "ticket_id",
|
|
5896
|
-
body: "body",
|
|
5897
|
-
author: "team_member_id",
|
|
5898
|
-
createdAt: "created_at",
|
|
5899
|
-
ship: "ship_id",
|
|
5900
|
-
publicReply: "is_public_reply",
|
|
5901
|
-
authorName: "author_name"
|
|
5902
|
-
},
|
|
5903
|
-
statuses: {
|
|
5904
|
-
open: ["new", "accepted", "blocked", "in_progress", "needs_info", "fixed", "qa", "verified"],
|
|
5905
|
-
resolved: [
|
|
5906
|
-
"verified",
|
|
5907
|
-
"closed_deployed",
|
|
5908
|
-
"closed_wont_fix",
|
|
5909
|
-
"closed_duplicate",
|
|
5910
|
-
"closed_completed",
|
|
5911
|
-
"closed_cant_reproduce"
|
|
5912
|
-
],
|
|
5913
|
-
approved: "accepted",
|
|
5914
|
-
parked: "blocked",
|
|
5915
|
-
building: "in_progress",
|
|
5916
|
-
handoff: "fixed",
|
|
5917
|
-
verifying: "qa",
|
|
5918
|
-
verified: "verified",
|
|
5919
|
-
deployed: "closed_deployed",
|
|
5920
|
-
needsHuman: "needs_info",
|
|
5921
|
-
reviewing: null,
|
|
5922
|
-
wontFix: "closed_wont_fix",
|
|
5923
|
-
draft: "draft"
|
|
5924
|
-
},
|
|
5925
|
-
epics: {
|
|
5926
|
-
statusColumn: "status",
|
|
5927
|
-
keyColumn: "epic_id",
|
|
5928
|
-
statuses: { planned: "planned", building: "in_progress", done: "done", cancelled: "cancelled" }
|
|
5929
|
-
},
|
|
5930
|
-
priorityOrder: ["p0", "p1", "p2", "p3"],
|
|
5931
|
-
severityOrder: ["s1", "s2", "s3", "s4"],
|
|
5932
|
-
unknownPriorityRank: 2,
|
|
5933
|
-
coordinatingValue: "coordinating",
|
|
5934
|
-
manualWorkMode: "manual",
|
|
5935
|
-
hybridWorkMode: "hybrid",
|
|
5936
|
-
repoExemptReportTypes: ["coordinating", "question", "investigation"]
|
|
5937
|
-
};
|
|
5938
|
-
function closedStatuses(c) {
|
|
5939
|
-
return c.statuses.resolved.filter((s) => s !== c.statuses.verified);
|
|
5940
|
-
}
|
|
5941
|
-
function resolveContract(override) {
|
|
5942
|
-
if (!override) return DEFAULT_CONTRACT;
|
|
5943
|
-
return {
|
|
5944
|
-
columns: { ...DEFAULT_CONTRACT.columns, ...override.columns },
|
|
5945
|
-
comments: { ...DEFAULT_CONTRACT.comments, ...override.comments },
|
|
5946
|
-
statuses: { ...DEFAULT_CONTRACT.statuses, ...override.statuses },
|
|
5947
|
-
epics: {
|
|
5948
|
-
...DEFAULT_CONTRACT.epics,
|
|
5949
|
-
...override.epics,
|
|
5950
|
-
statuses: { ...DEFAULT_CONTRACT.epics.statuses, ...override.epics?.statuses }
|
|
5951
|
-
},
|
|
5952
|
-
priorityOrder: override.priorityOrder ?? DEFAULT_CONTRACT.priorityOrder,
|
|
5953
|
-
severityOrder: override.severityOrder ?? DEFAULT_CONTRACT.severityOrder,
|
|
5954
|
-
unknownPriorityRank: override.unknownPriorityRank ?? DEFAULT_CONTRACT.unknownPriorityRank,
|
|
5955
|
-
coordinatingValue: override.coordinatingValue ?? DEFAULT_CONTRACT.coordinatingValue,
|
|
5956
|
-
manualWorkMode: override.manualWorkMode ?? DEFAULT_CONTRACT.manualWorkMode,
|
|
5957
|
-
hybridWorkMode: override.hybridWorkMode ?? DEFAULT_CONTRACT.hybridWorkMode,
|
|
5958
|
-
// A workspace overriding `coordinatingValue` alone (renaming the rollup
|
|
5959
|
-
// marker) still gets that renamed value exempted here by default,
|
|
5960
|
-
// without also having to repeat it — only an explicit
|
|
5961
|
-
// `repoExemptReportTypes` overrides this derived default.
|
|
5962
|
-
repoExemptReportTypes: override.repoExemptReportTypes ?? (override.coordinatingValue ? [override.coordinatingValue, "question", "investigation"] : DEFAULT_CONTRACT.repoExemptReportTypes)
|
|
5963
|
-
};
|
|
5964
|
-
}
|
|
5965
|
-
function validateContract(c) {
|
|
5966
|
-
const problems = [];
|
|
5967
|
-
const open = new Set(c.statuses.open);
|
|
5968
|
-
const resolved = new Set(c.statuses.resolved);
|
|
5969
|
-
for (const role of ["approved", "parked", "building", "handoff", "verifying", "needsHuman"]) {
|
|
5970
|
-
const v = c.statuses[role];
|
|
5971
|
-
if (!open.has(v)) problems.push(`statuses.${role} ("${v}") is not listed in statuses.open`);
|
|
5972
|
-
}
|
|
5973
|
-
if (c.statuses.reviewing && !open.has(c.statuses.reviewing)) {
|
|
5974
|
-
problems.push(`statuses.reviewing ("${c.statuses.reviewing}") is not listed in statuses.open`);
|
|
5975
|
-
}
|
|
5976
|
-
if (!resolved.has(c.statuses.deployed)) {
|
|
5977
|
-
problems.push(`statuses.deployed ("${c.statuses.deployed}") is not listed in statuses.resolved`);
|
|
5978
|
-
}
|
|
5979
|
-
if (resolved.has(c.statuses.handoff)) {
|
|
5980
|
-
problems.push(
|
|
5981
|
-
`statuses.handoff ("${c.statuses.handoff}") is listed as resolved \u2014 it means built-but-unchecked, so a ticket blocked by it is still blocked; calling it resolved would build on unverified work`
|
|
5982
|
-
);
|
|
5983
|
-
}
|
|
5984
|
-
if (!resolved.has(c.statuses.wontFix)) {
|
|
5985
|
-
problems.push(`statuses.wontFix ("${c.statuses.wontFix}") is not listed in statuses.resolved`);
|
|
5986
|
-
}
|
|
5987
|
-
if (c.statuses.reviewing && resolved.has(c.statuses.reviewing)) {
|
|
5988
|
-
problems.push(
|
|
5989
|
-
`statuses.reviewing ("${c.statuses.reviewing}") is listed as resolved \u2014 it is unmerged work under review, so a ticket blocked by it is still blocked`
|
|
5990
|
-
);
|
|
5991
|
-
}
|
|
5992
|
-
if (resolved.has(c.statuses.approved)) {
|
|
5993
|
-
problems.push(`statuses.approved ("${c.statuses.approved}") is listed as resolved`);
|
|
5994
|
-
}
|
|
5995
|
-
if (c.statuses.parked === c.statuses.approved) {
|
|
5996
|
-
problems.push("statuses.parked and statuses.approved are the same value \u2014 parking would be a no-op");
|
|
5997
|
-
}
|
|
5998
|
-
const epicValues = Object.values(c.epics.statuses);
|
|
5999
|
-
if (new Set(epicValues).size !== epicValues.length) {
|
|
6000
|
-
problems.push("epics.statuses names the same value twice \u2014 the epic sync could not tell those states apart");
|
|
6001
|
-
}
|
|
6002
|
-
if (c.priorityOrder.length === 0) problems.push("priorityOrder is empty");
|
|
6003
|
-
if (c.severityOrder.length === 0) problems.push("severityOrder is empty");
|
|
6004
|
-
for (const [name, order] of [["priorityOrder", c.priorityOrder], ["severityOrder", c.severityOrder]]) {
|
|
6005
|
-
if (new Set(order).size !== order.length) problems.push(`${name} contains duplicates`);
|
|
6006
|
-
}
|
|
6007
|
-
return problems;
|
|
6008
|
-
}
|
|
6009
|
-
var EVENT_LOG_MISSING = "the Issues table has no event log \u2014 crew writes its audit trail there (CREW-1574). In the app, open the Issues table's Data & lifecycle tab and turn on Event log, then rerun.";
|
|
5862
|
+
// src/crew-floor.ts
|
|
5863
|
+
import { TablationApiError as TablationApiError3 } from "@tablation/client";
|
|
6010
5864
|
|
|
6011
5865
|
// src/callout.ts
|
|
6012
5866
|
var calloutHeader = (kind, label) => `> [!${kind}]
|
|
@@ -7055,6 +6909,223 @@ var SHELL_WORDS = /* @__PURE__ */ new Set([
|
|
|
7055
6909
|
"ok"
|
|
7056
6910
|
]);
|
|
7057
6911
|
|
|
6912
|
+
// src/crew-floor.ts
|
|
6913
|
+
var CREW_OUTDATED_KEY = "crew_outdated";
|
|
6914
|
+
function parseVersion(v) {
|
|
6915
|
+
const m = /^v?(\d+)\.(\d+)\.(\d+)/.exec(v.trim());
|
|
6916
|
+
return m ? [Number(m[1]), Number(m[2]), Number(m[3])] : null;
|
|
6917
|
+
}
|
|
6918
|
+
function compareVersions(a, b) {
|
|
6919
|
+
const pa = parseVersion(a);
|
|
6920
|
+
const pb = parseVersion(b);
|
|
6921
|
+
if (!pa || !pb) return null;
|
|
6922
|
+
for (let i = 0; i < 3; i++) if (pa[i] !== pb[i]) return pa[i] - pb[i];
|
|
6923
|
+
return 0;
|
|
6924
|
+
}
|
|
6925
|
+
function belowFloor(have, floor) {
|
|
6926
|
+
if (!floor) return false;
|
|
6927
|
+
const c = compareVersions(have, floor);
|
|
6928
|
+
return c !== null && c < 0;
|
|
6929
|
+
}
|
|
6930
|
+
function outdatedItem(have, floor, nowIso = (/* @__PURE__ */ new Date()).toISOString()) {
|
|
6931
|
+
return {
|
|
6932
|
+
kind: "crew_outdated",
|
|
6933
|
+
key: CREW_OUTDATED_KEY,
|
|
6934
|
+
since: nowIso,
|
|
6935
|
+
message: `crew ${have} is below this workspace's minimum ${floor} \u2014 no ticket will be picked until it is updated (the Crew app updates it, or \`npm install -g @tablation/crew\`). Resumes automatically once the version satisfies the floor`
|
|
6936
|
+
};
|
|
6937
|
+
}
|
|
6938
|
+
function sentVersion(ship) {
|
|
6939
|
+
return /CrewAgent\/(\d+\.\d+\.\d+\S*)/.exec(ship.userAgent)?.[1] ?? null;
|
|
6940
|
+
}
|
|
6941
|
+
async function readCrewFloor(route, ship) {
|
|
6942
|
+
const r = route.resolved;
|
|
6943
|
+
if (!r?.projectId) return void 0;
|
|
6944
|
+
try {
|
|
6945
|
+
const res = await fetch(`${route.baseUrl}/api/projects/${encodeURIComponent(r.projectId)}?workspaceId=${r.workspaceId}`, {
|
|
6946
|
+
headers: { Authorization: `Bearer ${resolveApiKey(route)}`, "User-Agent": ship.userAgent }
|
|
6947
|
+
});
|
|
6948
|
+
if (!res.ok) return void 0;
|
|
6949
|
+
const body = await res.json();
|
|
6950
|
+
return body.crewMinVersion ?? null;
|
|
6951
|
+
} catch {
|
|
6952
|
+
return void 0;
|
|
6953
|
+
}
|
|
6954
|
+
}
|
|
6955
|
+
async function checkCrewFloor(o, deps = {}) {
|
|
6956
|
+
const have = deps.version ?? sentVersion(o.ship);
|
|
6957
|
+
if (!have) return null;
|
|
6958
|
+
const floor = await (deps.read ?? readCrewFloor)(o.route, o.ship);
|
|
6959
|
+
if (floor === void 0) return openOutdated(o.state);
|
|
6960
|
+
if (belowFloor(have, floor)) {
|
|
6961
|
+
const item = outdatedItem(have, floor);
|
|
6962
|
+
await raiseShipAttention(o, item);
|
|
6963
|
+
return item.message;
|
|
6964
|
+
}
|
|
6965
|
+
clearShipAttention(o.state, CREW_OUTDATED_KEY);
|
|
6966
|
+
return null;
|
|
6967
|
+
}
|
|
6968
|
+
function openOutdated(state) {
|
|
6969
|
+
return state.shipAttention().previous().find((i) => i.key === CREW_OUTDATED_KEY)?.message ?? null;
|
|
6970
|
+
}
|
|
6971
|
+
function isUpgradeRequired(err) {
|
|
6972
|
+
return err instanceof TablationApiError3 && err.status === 426;
|
|
6973
|
+
}
|
|
6974
|
+
async function raiseFromUpgradeRequired(o, err) {
|
|
6975
|
+
const have = sentVersion(o.ship) ?? "unknown";
|
|
6976
|
+
const floor = /(\d+\.\d+\.\d+)/.exec(err.message.replace(have, ""))?.[1] ?? "a newer version";
|
|
6977
|
+
await raiseShipAttention(o, outdatedItem(have, floor));
|
|
6978
|
+
}
|
|
6979
|
+
|
|
6980
|
+
// src/tracker.ts
|
|
6981
|
+
import { hostname } from "os";
|
|
6982
|
+
import { TablationClient as TablationClient3 } from "@tablation/client";
|
|
6983
|
+
|
|
6984
|
+
// src/contract.ts
|
|
6985
|
+
var DEFAULT_CONTRACT = {
|
|
6986
|
+
columns: {
|
|
6987
|
+
key: "issue_id",
|
|
6988
|
+
tag: "issue_tag",
|
|
6989
|
+
title: "title",
|
|
6990
|
+
status: "status",
|
|
6991
|
+
assignee: "assignee_id",
|
|
6992
|
+
priority: "priority",
|
|
6993
|
+
severity: "severity",
|
|
6994
|
+
slice: "project_id",
|
|
6995
|
+
repo: "repo_id",
|
|
6996
|
+
parent: "parent_id",
|
|
6997
|
+
blockedBy: "blocked_by",
|
|
6998
|
+
updatedAt: "updated_at",
|
|
6999
|
+
needsDesign: "needs_design",
|
|
7000
|
+
attachments: "attachments",
|
|
7001
|
+
workMode: "work_mode",
|
|
7002
|
+
reportType: "report_type",
|
|
7003
|
+
epic: "epic_id",
|
|
7004
|
+
author: "filed_by_id",
|
|
7005
|
+
ship: "ship_id",
|
|
7006
|
+
heldBy: "held_by_ship_id",
|
|
7007
|
+
resolutionNote: "resolution_note",
|
|
7008
|
+
verifiedSha: "verified_sha",
|
|
7009
|
+
prRef: "pr_ref",
|
|
7010
|
+
position: "position"
|
|
7011
|
+
},
|
|
7012
|
+
comments: {
|
|
7013
|
+
parent: "ticket_id",
|
|
7014
|
+
body: "body",
|
|
7015
|
+
author: "team_member_id",
|
|
7016
|
+
createdAt: "created_at",
|
|
7017
|
+
ship: "ship_id",
|
|
7018
|
+
publicReply: "is_public_reply",
|
|
7019
|
+
authorName: "author_name"
|
|
7020
|
+
},
|
|
7021
|
+
statuses: {
|
|
7022
|
+
open: ["new", "accepted", "blocked", "in_progress", "needs_info", "fixed", "qa", "verified"],
|
|
7023
|
+
resolved: [
|
|
7024
|
+
"verified",
|
|
7025
|
+
"closed_deployed",
|
|
7026
|
+
"closed_wont_fix",
|
|
7027
|
+
"closed_duplicate",
|
|
7028
|
+
"closed_completed",
|
|
7029
|
+
"closed_cant_reproduce"
|
|
7030
|
+
],
|
|
7031
|
+
approved: "accepted",
|
|
7032
|
+
parked: "blocked",
|
|
7033
|
+
building: "in_progress",
|
|
7034
|
+
handoff: "fixed",
|
|
7035
|
+
verifying: "qa",
|
|
7036
|
+
verified: "verified",
|
|
7037
|
+
deployed: "closed_deployed",
|
|
7038
|
+
needsHuman: "needs_info",
|
|
7039
|
+
reviewing: null,
|
|
7040
|
+
wontFix: "closed_wont_fix",
|
|
7041
|
+
draft: "draft"
|
|
7042
|
+
},
|
|
7043
|
+
epics: {
|
|
7044
|
+
statusColumn: "status",
|
|
7045
|
+
keyColumn: "epic_id",
|
|
7046
|
+
statuses: { planned: "planned", building: "in_progress", done: "done", cancelled: "cancelled" }
|
|
7047
|
+
},
|
|
7048
|
+
priorityOrder: ["p0", "p1", "p2", "p3"],
|
|
7049
|
+
severityOrder: ["s1", "s2", "s3", "s4"],
|
|
7050
|
+
unknownPriorityRank: 2,
|
|
7051
|
+
coordinatingValue: "coordinating",
|
|
7052
|
+
manualWorkMode: "manual",
|
|
7053
|
+
hybridWorkMode: "hybrid",
|
|
7054
|
+
repoExemptReportTypes: ["coordinating", "question", "investigation"]
|
|
7055
|
+
};
|
|
7056
|
+
function closedStatuses(c) {
|
|
7057
|
+
return c.statuses.resolved.filter((s) => s !== c.statuses.verified);
|
|
7058
|
+
}
|
|
7059
|
+
function resolveContract(override) {
|
|
7060
|
+
if (!override) return DEFAULT_CONTRACT;
|
|
7061
|
+
return {
|
|
7062
|
+
columns: { ...DEFAULT_CONTRACT.columns, ...override.columns },
|
|
7063
|
+
comments: { ...DEFAULT_CONTRACT.comments, ...override.comments },
|
|
7064
|
+
statuses: { ...DEFAULT_CONTRACT.statuses, ...override.statuses },
|
|
7065
|
+
epics: {
|
|
7066
|
+
...DEFAULT_CONTRACT.epics,
|
|
7067
|
+
...override.epics,
|
|
7068
|
+
statuses: { ...DEFAULT_CONTRACT.epics.statuses, ...override.epics?.statuses }
|
|
7069
|
+
},
|
|
7070
|
+
priorityOrder: override.priorityOrder ?? DEFAULT_CONTRACT.priorityOrder,
|
|
7071
|
+
severityOrder: override.severityOrder ?? DEFAULT_CONTRACT.severityOrder,
|
|
7072
|
+
unknownPriorityRank: override.unknownPriorityRank ?? DEFAULT_CONTRACT.unknownPriorityRank,
|
|
7073
|
+
coordinatingValue: override.coordinatingValue ?? DEFAULT_CONTRACT.coordinatingValue,
|
|
7074
|
+
manualWorkMode: override.manualWorkMode ?? DEFAULT_CONTRACT.manualWorkMode,
|
|
7075
|
+
hybridWorkMode: override.hybridWorkMode ?? DEFAULT_CONTRACT.hybridWorkMode,
|
|
7076
|
+
// A workspace overriding `coordinatingValue` alone (renaming the rollup
|
|
7077
|
+
// marker) still gets that renamed value exempted here by default,
|
|
7078
|
+
// without also having to repeat it — only an explicit
|
|
7079
|
+
// `repoExemptReportTypes` overrides this derived default.
|
|
7080
|
+
repoExemptReportTypes: override.repoExemptReportTypes ?? (override.coordinatingValue ? [override.coordinatingValue, "question", "investigation"] : DEFAULT_CONTRACT.repoExemptReportTypes)
|
|
7081
|
+
};
|
|
7082
|
+
}
|
|
7083
|
+
function validateContract(c) {
|
|
7084
|
+
const problems = [];
|
|
7085
|
+
const open = new Set(c.statuses.open);
|
|
7086
|
+
const resolved = new Set(c.statuses.resolved);
|
|
7087
|
+
for (const role of ["approved", "parked", "building", "handoff", "verifying", "needsHuman"]) {
|
|
7088
|
+
const v = c.statuses[role];
|
|
7089
|
+
if (!open.has(v)) problems.push(`statuses.${role} ("${v}") is not listed in statuses.open`);
|
|
7090
|
+
}
|
|
7091
|
+
if (c.statuses.reviewing && !open.has(c.statuses.reviewing)) {
|
|
7092
|
+
problems.push(`statuses.reviewing ("${c.statuses.reviewing}") is not listed in statuses.open`);
|
|
7093
|
+
}
|
|
7094
|
+
if (!resolved.has(c.statuses.deployed)) {
|
|
7095
|
+
problems.push(`statuses.deployed ("${c.statuses.deployed}") is not listed in statuses.resolved`);
|
|
7096
|
+
}
|
|
7097
|
+
if (resolved.has(c.statuses.handoff)) {
|
|
7098
|
+
problems.push(
|
|
7099
|
+
`statuses.handoff ("${c.statuses.handoff}") is listed as resolved \u2014 it means built-but-unchecked, so a ticket blocked by it is still blocked; calling it resolved would build on unverified work`
|
|
7100
|
+
);
|
|
7101
|
+
}
|
|
7102
|
+
if (!resolved.has(c.statuses.wontFix)) {
|
|
7103
|
+
problems.push(`statuses.wontFix ("${c.statuses.wontFix}") is not listed in statuses.resolved`);
|
|
7104
|
+
}
|
|
7105
|
+
if (c.statuses.reviewing && resolved.has(c.statuses.reviewing)) {
|
|
7106
|
+
problems.push(
|
|
7107
|
+
`statuses.reviewing ("${c.statuses.reviewing}") is listed as resolved \u2014 it is unmerged work under review, so a ticket blocked by it is still blocked`
|
|
7108
|
+
);
|
|
7109
|
+
}
|
|
7110
|
+
if (resolved.has(c.statuses.approved)) {
|
|
7111
|
+
problems.push(`statuses.approved ("${c.statuses.approved}") is listed as resolved`);
|
|
7112
|
+
}
|
|
7113
|
+
if (c.statuses.parked === c.statuses.approved) {
|
|
7114
|
+
problems.push("statuses.parked and statuses.approved are the same value \u2014 parking would be a no-op");
|
|
7115
|
+
}
|
|
7116
|
+
const epicValues = Object.values(c.epics.statuses);
|
|
7117
|
+
if (new Set(epicValues).size !== epicValues.length) {
|
|
7118
|
+
problems.push("epics.statuses names the same value twice \u2014 the epic sync could not tell those states apart");
|
|
7119
|
+
}
|
|
7120
|
+
if (c.priorityOrder.length === 0) problems.push("priorityOrder is empty");
|
|
7121
|
+
if (c.severityOrder.length === 0) problems.push("severityOrder is empty");
|
|
7122
|
+
for (const [name, order] of [["priorityOrder", c.priorityOrder], ["severityOrder", c.severityOrder]]) {
|
|
7123
|
+
if (new Set(order).size !== order.length) problems.push(`${name} contains duplicates`);
|
|
7124
|
+
}
|
|
7125
|
+
return problems;
|
|
7126
|
+
}
|
|
7127
|
+
var EVENT_LOG_MISSING = "the Issues table has no event log \u2014 crew writes its audit trail there (CREW-1574). In the app, open the Issues table's Data & lifecycle tab and turn on Event log, then rerun.";
|
|
7128
|
+
|
|
7058
7129
|
// src/work-mode.ts
|
|
7059
7130
|
function projectWorkModes(rows, contract) {
|
|
7060
7131
|
const out = /* @__PURE__ */ new Map();
|
|
@@ -8928,8 +8999,10 @@ async function decideCycle(o) {
|
|
|
8928
8999
|
emit.emit(`${what}: no longer missing from the scheduler PATH \u2014 stop cleared`, { step: "select" });
|
|
8929
9000
|
}
|
|
8930
9001
|
await checkRepoDisks({ state, emit, route, ship });
|
|
9002
|
+
const floorStop = await checkCrewFloor({ state, emit, route, ship });
|
|
9003
|
+
const itemStop = repoStopFor(openShipAttention(state), route);
|
|
8931
9004
|
const selectionInput = {
|
|
8932
|
-
repoStop:
|
|
9005
|
+
repoStop: floorStop ? () => floorStop : itemStop,
|
|
8933
9006
|
workModes,
|
|
8934
9007
|
tickets,
|
|
8935
9008
|
comments,
|
|
@@ -12162,24 +12235,49 @@ async function discover(o) {
|
|
|
12162
12235
|
o,
|
|
12163
12236
|
`/data-models/${out.shipsModelId}/records?limit=200`
|
|
12164
12237
|
);
|
|
12165
|
-
const mine = shipRows.filter((r) => (r.name ?? "").trim() === o.ship.name.trim());
|
|
12166
12238
|
const hostPassengers = o.ship.hostPassengers ?? false;
|
|
12167
12239
|
const sshPublicKey = o.ship.sshPublicKey;
|
|
12240
|
+
let shipColumns = /* @__PURE__ */ new Set();
|
|
12241
|
+
try {
|
|
12242
|
+
const shipsModel = await get(o, `/data-models/${out.shipsModelId}`);
|
|
12243
|
+
shipColumns = new Set((shipsModel.fields ?? []).map((f) => f.columnName));
|
|
12244
|
+
} catch {
|
|
12245
|
+
}
|
|
12168
12246
|
let ownerId;
|
|
12169
12247
|
if (out.meId) {
|
|
12170
|
-
|
|
12171
|
-
try {
|
|
12172
|
-
const shipsModel = await get(o, `/data-models/${out.shipsModelId}`);
|
|
12173
|
-
hasOwnerColumn = (shipsModel.fields ?? []).some((f) => f.columnName === "owner_id");
|
|
12174
|
-
} catch {
|
|
12175
|
-
}
|
|
12176
|
-
if (hasOwnerColumn) ownerId = out.meId;
|
|
12248
|
+
if (shipColumns.has("owner_id")) ownerId = out.meId;
|
|
12177
12249
|
else provisioning.push("could not record the ship owner \u2014 the Ships table has no owner_id column yet (update the Issues template)");
|
|
12178
12250
|
}
|
|
12251
|
+
const machineId = shipColumns.has("machine_id") ? o.ship.machineId : void 0;
|
|
12252
|
+
let mine;
|
|
12253
|
+
let matchedBy = "name";
|
|
12254
|
+
let writeMachineId = false;
|
|
12255
|
+
const byName = shipRows.filter((r) => (r.name ?? "").trim() === o.ship.name.trim());
|
|
12256
|
+
if (machineId) {
|
|
12257
|
+
const byId = shipRows.filter((r) => (r.machine_id ?? "") === machineId);
|
|
12258
|
+
if (byId.length > 0) {
|
|
12259
|
+
mine = byId;
|
|
12260
|
+
matchedBy = "machine_id";
|
|
12261
|
+
} else {
|
|
12262
|
+
mine = byName.filter((r) => !r.machine_id);
|
|
12263
|
+
writeMachineId = true;
|
|
12264
|
+
}
|
|
12265
|
+
} else {
|
|
12266
|
+
mine = byName;
|
|
12267
|
+
}
|
|
12179
12268
|
if (mine.length === 1) {
|
|
12180
12269
|
shipRowId = mine[0].id;
|
|
12181
|
-
out.shipRow = { id: shipRowId, name: o.ship.name, action: "matched" };
|
|
12270
|
+
out.shipRow = { id: shipRowId, name: o.ship.name, action: "matched", matchedBy };
|
|
12182
12271
|
const patchBody = {};
|
|
12272
|
+
if (matchedBy === "machine_id") {
|
|
12273
|
+
const keptName = (mine[0].name ?? "").trim();
|
|
12274
|
+
if (keptName !== o.ship.name.trim()) {
|
|
12275
|
+
out.shipRow.name = keptName;
|
|
12276
|
+
provisioning.push(`matched Ships row "${keptName}" by machine id \u2014 this host is now called "${o.ship.name}"; the row's name is kept`);
|
|
12277
|
+
}
|
|
12278
|
+
patchBody.last_seen = (/* @__PURE__ */ new Date()).toISOString();
|
|
12279
|
+
}
|
|
12280
|
+
if (writeMachineId) patchBody.machine_id = machineId;
|
|
12183
12281
|
if ((mine[0].host_passengers ?? false) !== hostPassengers) patchBody.host_passengers = hostPassengers;
|
|
12184
12282
|
if (sshPublicKey !== void 0 && (mine[0].ssh_public_key ?? "") !== sshPublicKey) {
|
|
12185
12283
|
patchBody.ssh_public_key = sshPublicKey;
|
|
@@ -12197,13 +12295,16 @@ async function discover(o) {
|
|
|
12197
12295
|
...o.ship.platform ? { platform: o.ship.platform } : {},
|
|
12198
12296
|
host_passengers: hostPassengers,
|
|
12199
12297
|
...sshPublicKey !== void 0 ? { ssh_public_key: sshPublicKey } : {},
|
|
12200
|
-
...ownerId !== void 0 ? { owner_id: ownerId } : {}
|
|
12298
|
+
...ownerId !== void 0 ? { owner_id: ownerId } : {},
|
|
12299
|
+
...writeMachineId ? { machine_id: machineId } : {}
|
|
12201
12300
|
});
|
|
12202
12301
|
shipRowId = created.id;
|
|
12203
|
-
out.shipRow = { id: shipRowId, name: o.ship.name, action: "created" };
|
|
12302
|
+
out.shipRow = { id: shipRowId, name: o.ship.name, action: "created", matchedBy };
|
|
12204
12303
|
provisioning.push(`created Ships row "${o.ship.name}"`);
|
|
12205
12304
|
} else {
|
|
12206
|
-
problems.push(
|
|
12305
|
+
problems.push(
|
|
12306
|
+
matchedBy === "machine_id" ? `${mine.length} Ships rows carry this machine's id \u2014 which one is this machine?` : `${mine.length} Ships rows named "${o.ship.name}" \u2014 which one is this machine?`
|
|
12307
|
+
);
|
|
12207
12308
|
}
|
|
12208
12309
|
}
|
|
12209
12310
|
let agentIdFor = () => void 0;
|
|
@@ -13581,7 +13682,7 @@ function planEventsSweep(stateDir, retentionDays, now = Date.now()) {
|
|
|
13581
13682
|
}
|
|
13582
13683
|
|
|
13583
13684
|
// src/cli.ts
|
|
13584
|
-
import { realpathSync as realpathSync2, readFileSync as
|
|
13685
|
+
import { realpathSync as realpathSync2, readFileSync as readFileSync24, existsSync as existsSync31, mkdirSync as mkdirSync17, writeFileSync as writeFileSync18, rmSync as rmSync12, unlinkSync as unlinkSync5, copyFileSync as copyFileSync2, renameSync as renameSync2, statSync as statSync10, readdirSync as readdirSync12 } from "fs";
|
|
13585
13686
|
import { createInterface } from "readline/promises";
|
|
13586
13687
|
import { dirname as dirOf, resolve as resolvePath } from "path";
|
|
13587
13688
|
|
|
@@ -13633,12 +13734,62 @@ function sshKeygenAvailable() {
|
|
|
13633
13734
|
}
|
|
13634
13735
|
}
|
|
13635
13736
|
|
|
13636
|
-
// src/
|
|
13737
|
+
// src/machine-id.ts
|
|
13637
13738
|
import { execFileSync as execFileSync5 } from "child_process";
|
|
13638
13739
|
import { createHash as createHash4 } from "crypto";
|
|
13740
|
+
import { existsSync as existsSync26, mkdirSync as mkdirSync13, readFileSync as readFileSync19, writeFileSync as writeFileSync14 } from "fs";
|
|
13741
|
+
import { dirname as dirname17, join as join26 } from "path";
|
|
13742
|
+
function machineIdPathFor(stateDir) {
|
|
13743
|
+
return join26(stateDir, "machine-id");
|
|
13744
|
+
}
|
|
13745
|
+
var sha256 = (s) => createHash4("sha256").update(s).digest("hex");
|
|
13746
|
+
function readRawMachineId(platform2 = process.platform) {
|
|
13747
|
+
try {
|
|
13748
|
+
if (platform2 === "darwin") {
|
|
13749
|
+
const out = execFileSync5("ioreg", ["-rd1", "-c", "IOPlatformExpertDevice"], {
|
|
13750
|
+
encoding: "utf8",
|
|
13751
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
13752
|
+
});
|
|
13753
|
+
return /"IOPlatformUUID"\s*=\s*"([^"]+)"/.exec(out)?.[1];
|
|
13754
|
+
}
|
|
13755
|
+
for (const p of ["/etc/machine-id", "/var/lib/dbus/machine-id"]) {
|
|
13756
|
+
if (existsSync26(p)) {
|
|
13757
|
+
const id = readFileSync19(p, "utf8").trim();
|
|
13758
|
+
if (id) return id;
|
|
13759
|
+
}
|
|
13760
|
+
}
|
|
13761
|
+
} catch {
|
|
13762
|
+
}
|
|
13763
|
+
return void 0;
|
|
13764
|
+
}
|
|
13765
|
+
function ensureMachineId(stateDir, opts = {}) {
|
|
13766
|
+
if (opts.override?.trim()) return sha256(opts.override.trim());
|
|
13767
|
+
const path = machineIdPathFor(stateDir);
|
|
13768
|
+
if (existsSync26(path)) {
|
|
13769
|
+
const cached = readFileSync19(path, "utf8").trim();
|
|
13770
|
+
if (/^[0-9a-f]{64}$/.test(cached)) return cached;
|
|
13771
|
+
}
|
|
13772
|
+
const raw = (opts.read ?? readRawMachineId)();
|
|
13773
|
+
if (!raw) return void 0;
|
|
13774
|
+
const hashed = sha256(raw);
|
|
13775
|
+
mkdirSync13(dirname17(path), { recursive: true });
|
|
13776
|
+
writeFileSync14(path, `${hashed}
|
|
13777
|
+
`, { mode: 384 });
|
|
13778
|
+
return hashed;
|
|
13779
|
+
}
|
|
13780
|
+
function matchShipRows(ships, shipName2, machineId) {
|
|
13781
|
+
const hasColumn = ships.some((r) => "machine_id" in r);
|
|
13782
|
+
const byId = hasColumn && machineId ? ships.filter((r) => r.machine_id === machineId) : [];
|
|
13783
|
+
if (byId.length > 0) return { rows: byId, by: "machine_id", hasColumn };
|
|
13784
|
+
return { rows: ships.filter((r) => (r.name ?? "").trim() === shipName2.trim()), by: "name", hasColumn };
|
|
13785
|
+
}
|
|
13786
|
+
|
|
13787
|
+
// src/passenger-containers.ts
|
|
13788
|
+
import { execFileSync as execFileSync6 } from "child_process";
|
|
13789
|
+
import { createHash as createHash5 } from "crypto";
|
|
13639
13790
|
var realExec = (cmd, args) => {
|
|
13640
13791
|
try {
|
|
13641
|
-
return
|
|
13792
|
+
return execFileSync6(cmd, args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
|
|
13642
13793
|
} catch (e) {
|
|
13643
13794
|
const err = e;
|
|
13644
13795
|
const why = [err.stderr, err.stdout].map((x) => (x ?? "").trim()).filter(Boolean).join(" / ");
|
|
@@ -13672,12 +13823,12 @@ function containerNameFor(workspaceId) {
|
|
|
13672
13823
|
return `crew-passenger-${workspaceId}`;
|
|
13673
13824
|
}
|
|
13674
13825
|
function portFor(workspaceId, base = 28765, span = 1e3) {
|
|
13675
|
-
const digest =
|
|
13826
|
+
const digest = createHash5("sha256").update(workspaceId).digest();
|
|
13676
13827
|
return base + digest.readUInt32BE(0) % span;
|
|
13677
13828
|
}
|
|
13678
13829
|
function mountsDigest(mounts, image) {
|
|
13679
13830
|
const sorted2 = [...mounts].sort((a, b) => a.containerPath.localeCompare(b.containerPath));
|
|
13680
|
-
return
|
|
13831
|
+
return createHash5("sha256").update(JSON.stringify({ mounts: sorted2, image })).digest("hex").slice(0, 16);
|
|
13681
13832
|
}
|
|
13682
13833
|
function planContainers(cfg) {
|
|
13683
13834
|
const byWorkspace = /* @__PURE__ */ new Map();
|
|
@@ -13837,9 +13988,9 @@ function syncAllPassengerCheckouts(cfg) {
|
|
|
13837
13988
|
|
|
13838
13989
|
// src/tunnel.ts
|
|
13839
13990
|
import { spawn as spawn4 } from "child_process";
|
|
13840
|
-
import { existsSync as
|
|
13991
|
+
import { existsSync as existsSync27, mkdirSync as mkdirSync14, readFileSync as readFileSync20, writeFileSync as writeFileSync15, unlinkSync as unlinkSync3, readdirSync as readdirSync8 } from "fs";
|
|
13841
13992
|
import { randomInt } from "crypto";
|
|
13842
|
-
import { join as
|
|
13993
|
+
import { join as join27 } from "path";
|
|
13843
13994
|
var DEFAULT_SPAWN_OPTIONS = { stdio: "ignore", detached: true };
|
|
13844
13995
|
var defaultSpawn = (cmd, args) => spawn4(cmd, args, DEFAULT_SPAWN_OPTIONS);
|
|
13845
13996
|
var SLUG_ALPHABET = "abcdefghijklmnopqrstuvwxyz0123456789";
|
|
@@ -13906,21 +14057,21 @@ function startTunnel(opts) {
|
|
|
13906
14057
|
};
|
|
13907
14058
|
}
|
|
13908
14059
|
function tunnelStatePath(stateDir, workspaceId) {
|
|
13909
|
-
return
|
|
14060
|
+
return join27(stateDir, "passengers", `${workspaceId}.json`);
|
|
13910
14061
|
}
|
|
13911
14062
|
function readPersistedTunnel(stateDir, workspaceId) {
|
|
13912
14063
|
const path = tunnelStatePath(stateDir, workspaceId);
|
|
13913
|
-
if (!
|
|
14064
|
+
if (!existsSync27(path)) return void 0;
|
|
13914
14065
|
try {
|
|
13915
|
-
return JSON.parse(
|
|
14066
|
+
return JSON.parse(readFileSync20(path, "utf8"));
|
|
13916
14067
|
} catch {
|
|
13917
14068
|
return void 0;
|
|
13918
14069
|
}
|
|
13919
14070
|
}
|
|
13920
14071
|
function writePersistedTunnel(stateDir, state) {
|
|
13921
14072
|
const path = tunnelStatePath(stateDir, state.workspaceId);
|
|
13922
|
-
|
|
13923
|
-
|
|
14073
|
+
mkdirSync14(join27(stateDir, "passengers"), { recursive: true });
|
|
14074
|
+
writeFileSync15(path, `${JSON.stringify(state, null, 2)}
|
|
13924
14075
|
`);
|
|
13925
14076
|
}
|
|
13926
14077
|
function updatePersistedTunnel(stateDir, workspaceId, patch2) {
|
|
@@ -14017,13 +14168,13 @@ function syncPassengerTunnels(plans, relayHost, relayPort, privateKeyPath, state
|
|
|
14017
14168
|
}
|
|
14018
14169
|
}
|
|
14019
14170
|
function readAllPersistedWorkspaceIds(stateDir) {
|
|
14020
|
-
const dir =
|
|
14021
|
-
if (!
|
|
14171
|
+
const dir = join27(stateDir, "passengers");
|
|
14172
|
+
if (!existsSync27(dir)) return [];
|
|
14022
14173
|
return readdirSync8(dir).filter((f) => f.endsWith(".json") && !f.endsWith("-sync.json")).map((f) => f.slice(0, -".json".length));
|
|
14023
14174
|
}
|
|
14024
14175
|
|
|
14025
14176
|
// src/mcp-endpoint-registry.ts
|
|
14026
|
-
import { createHash as
|
|
14177
|
+
import { createHash as createHash6 } from "crypto";
|
|
14027
14178
|
function endpointsUrl(api, workspaceId, id) {
|
|
14028
14179
|
const base = `${api.baseUrl.replace(/\/+$/, "")}/api/workspaces/${workspaceId}/mcp-endpoints`;
|
|
14029
14180
|
return id ? `${base}/${id}` : base;
|
|
@@ -14091,7 +14242,7 @@ async function probeEndpointAccess(api, workspaceId) {
|
|
|
14091
14242
|
}
|
|
14092
14243
|
var REGISTER_MCP_ENDPOINTS_HINT = "give this key's role the REGISTER_MCP_ENDPOINTS capability, e.g. via the group that holds the Crew project role";
|
|
14093
14244
|
function registrationKey(url, mcpSecret) {
|
|
14094
|
-
return
|
|
14245
|
+
return createHash6("sha256").update(`${url}
|
|
14095
14246
|
${mcpSecret ?? ""}`).digest("hex").slice(0, 16);
|
|
14096
14247
|
}
|
|
14097
14248
|
async function reconcileEndpoint(workspaceId, d) {
|
|
@@ -14139,8 +14290,8 @@ async function unregisterEndpoint(workspaceId, info, d) {
|
|
|
14139
14290
|
import { createServer as createHttpServer } from "http";
|
|
14140
14291
|
import { spawn as spawn5 } from "child_process";
|
|
14141
14292
|
import { randomBytes } from "crypto";
|
|
14142
|
-
import { existsSync as
|
|
14143
|
-
import { join as
|
|
14293
|
+
import { existsSync as existsSync28, mkdirSync as mkdirSync15, readFileSync as readFileSync21, writeFileSync as writeFileSync16, unlinkSync as unlinkSync4, readdirSync as readdirSync9 } from "fs";
|
|
14294
|
+
import { join as join28 } from "path";
|
|
14144
14295
|
|
|
14145
14296
|
// src/secret-compare.ts
|
|
14146
14297
|
import { timingSafeEqual } from "crypto";
|
|
@@ -14229,21 +14380,21 @@ var defaultSpawn2 = (cmd, args, env) => {
|
|
|
14229
14380
|
return child;
|
|
14230
14381
|
};
|
|
14231
14382
|
function syncDaemonStatePath(stateDir, workspaceId) {
|
|
14232
|
-
return
|
|
14383
|
+
return join28(stateDir, "passengers", `${workspaceId}-sync.json`);
|
|
14233
14384
|
}
|
|
14234
14385
|
function readPersistedSyncDaemon(stateDir, workspaceId) {
|
|
14235
14386
|
const path = syncDaemonStatePath(stateDir, workspaceId);
|
|
14236
|
-
if (!
|
|
14387
|
+
if (!existsSync28(path)) return void 0;
|
|
14237
14388
|
try {
|
|
14238
|
-
return JSON.parse(
|
|
14389
|
+
return JSON.parse(readFileSync21(path, "utf8"));
|
|
14239
14390
|
} catch {
|
|
14240
14391
|
return void 0;
|
|
14241
14392
|
}
|
|
14242
14393
|
}
|
|
14243
14394
|
function writePersistedSyncDaemon(stateDir, state) {
|
|
14244
14395
|
const path = syncDaemonStatePath(stateDir, state.workspaceId);
|
|
14245
|
-
|
|
14246
|
-
|
|
14396
|
+
mkdirSync15(join28(stateDir, "passengers"), { recursive: true });
|
|
14397
|
+
writeFileSync16(path, `${JSON.stringify(state, null, 2)}
|
|
14247
14398
|
`);
|
|
14248
14399
|
}
|
|
14249
14400
|
function removePersistedSyncDaemon(stateDir, workspaceId) {
|
|
@@ -14263,8 +14414,8 @@ function waitForExit(pid, alive, timeoutMs = 5e3) {
|
|
|
14263
14414
|
}
|
|
14264
14415
|
}
|
|
14265
14416
|
function readAllPersistedSyncWorkspaceIds(stateDir) {
|
|
14266
|
-
const dir =
|
|
14267
|
-
if (!
|
|
14417
|
+
const dir = join28(stateDir, "passengers");
|
|
14418
|
+
if (!existsSync28(dir)) return [];
|
|
14268
14419
|
const suffix = "-sync.json";
|
|
14269
14420
|
return readdirSync9(dir).filter((f) => f.endsWith(suffix)).map((f) => f.slice(0, -suffix.length));
|
|
14270
14421
|
}
|
|
@@ -14321,12 +14472,12 @@ function syncPassengerSyncDaemons(plans, cfg, stateDir, deps = {}) {
|
|
|
14321
14472
|
}
|
|
14322
14473
|
|
|
14323
14474
|
// src/daemon.ts
|
|
14324
|
-
import { existsSync as
|
|
14325
|
-
import { join as
|
|
14475
|
+
import { existsSync as existsSync30, mkdirSync as mkdirSync16, readFileSync as readFileSync23, readdirSync as readdirSync11, rmSync as rmSync11, statSync as statSync9, writeFileSync as writeFileSync17 } from "fs";
|
|
14476
|
+
import { join as join30 } from "path";
|
|
14326
14477
|
|
|
14327
14478
|
// src/interrupted.ts
|
|
14328
|
-
import { existsSync as
|
|
14329
|
-
import { join as
|
|
14479
|
+
import { existsSync as existsSync29, readdirSync as readdirSync10, readFileSync as readFileSync22 } from "fs";
|
|
14480
|
+
import { join as join29 } from "path";
|
|
14330
14481
|
var INTERRUPTED_MARKER = calloutHeader("NOTE", "Interrupted");
|
|
14331
14482
|
function interruptedComment(b) {
|
|
14332
14483
|
const tree = b.dirty === null ? "The worktree could not be checked." : b.dirty ? "The worktree has uncommitted changes \u2014 they are this seat's own, from that run." : "The worktree is clean.";
|
|
@@ -14345,21 +14496,21 @@ var pidAlive = (pid) => {
|
|
|
14345
14496
|
};
|
|
14346
14497
|
function runFinished(streamPath) {
|
|
14347
14498
|
const path = `${streamPath}.meta.json`;
|
|
14348
|
-
if (!
|
|
14499
|
+
if (!existsSync29(path)) return false;
|
|
14349
14500
|
try {
|
|
14350
|
-
return JSON.parse(
|
|
14501
|
+
return JSON.parse(readFileSync22(path, "utf8")).running !== true;
|
|
14351
14502
|
} catch {
|
|
14352
14503
|
return true;
|
|
14353
14504
|
}
|
|
14354
14505
|
}
|
|
14355
14506
|
function orphanedStreams(o) {
|
|
14356
|
-
const dir =
|
|
14357
|
-
if (!
|
|
14507
|
+
const dir = join29(o.stateDir, "streams");
|
|
14508
|
+
if (!existsSync29(dir)) return [];
|
|
14358
14509
|
const ownerAlive = o.isOwnerAlive ?? pidAlive;
|
|
14359
14510
|
const out = [];
|
|
14360
14511
|
for (const name of readdirSync10(dir)) {
|
|
14361
14512
|
if (!name.endsWith(".jsonl") || name.endsWith(".events.jsonl")) continue;
|
|
14362
|
-
const streamPath =
|
|
14513
|
+
const streamPath = join29(dir, name);
|
|
14363
14514
|
if (runFinished(streamPath)) continue;
|
|
14364
14515
|
const sidecar = readRunSidecar(streamPath);
|
|
14365
14516
|
if (!sidecar || sidecar.route !== o.routeLabel) continue;
|
|
@@ -14459,7 +14610,7 @@ function latestMtime(path) {
|
|
|
14459
14610
|
if (!stat.isDirectory()) return 0;
|
|
14460
14611
|
let latest = stat.mtimeMs;
|
|
14461
14612
|
for (const entry of readdirSync11(path, { withFileTypes: true })) {
|
|
14462
|
-
latest = Math.max(latest, latestMtime(
|
|
14613
|
+
latest = Math.max(latest, latestMtime(join30(path, entry.name)));
|
|
14463
14614
|
}
|
|
14464
14615
|
return latest;
|
|
14465
14616
|
}
|
|
@@ -14468,23 +14619,23 @@ function computeTreeSignature(paths) {
|
|
|
14468
14619
|
}
|
|
14469
14620
|
function makeStaleChecker(paths, opts = {}) {
|
|
14470
14621
|
const initial = computeTreeSignature(paths);
|
|
14471
|
-
return () => computeTreeSignature(paths) !== initial || opts.reloadSentinel !== void 0 &&
|
|
14622
|
+
return () => computeTreeSignature(paths) !== initial || opts.reloadSentinel !== void 0 && existsSync30(opts.reloadSentinel);
|
|
14472
14623
|
}
|
|
14473
14624
|
function reloadSentinelFor(stateDir) {
|
|
14474
|
-
return
|
|
14625
|
+
return join30(stateDir, "reload-requested");
|
|
14475
14626
|
}
|
|
14476
14627
|
function daemonStampFor(stateDir) {
|
|
14477
|
-
return
|
|
14628
|
+
return join30(stateDir, "daemon.started");
|
|
14478
14629
|
}
|
|
14479
14630
|
function noteDaemonStarted(stateDir, now = Date.now()) {
|
|
14480
|
-
|
|
14631
|
+
mkdirSync16(stateDir, { recursive: true });
|
|
14481
14632
|
rmSync11(reloadSentinelFor(stateDir), { force: true });
|
|
14482
|
-
|
|
14633
|
+
writeFileSync17(daemonStampFor(stateDir), `${JSON.stringify({ pid: process.pid, startedAt: now })}
|
|
14483
14634
|
`);
|
|
14484
14635
|
}
|
|
14485
14636
|
function readDaemonStarted(stateDir) {
|
|
14486
14637
|
try {
|
|
14487
|
-
const v = JSON.parse(
|
|
14638
|
+
const v = JSON.parse(readFileSync23(daemonStampFor(stateDir), "utf8")).startedAt;
|
|
14488
14639
|
return typeof v === "number" ? v : null;
|
|
14489
14640
|
} catch {
|
|
14490
14641
|
return null;
|
|
@@ -14530,6 +14681,16 @@ var SpawnBreaker = class {
|
|
|
14530
14681
|
}
|
|
14531
14682
|
};
|
|
14532
14683
|
async function runOnePass(o) {
|
|
14684
|
+
try {
|
|
14685
|
+
return await runOnePassUnguarded(o);
|
|
14686
|
+
} catch (e) {
|
|
14687
|
+
if (!isUpgradeRequired(e)) throw e;
|
|
14688
|
+
const emit = o.newEmitter();
|
|
14689
|
+
await raiseFromUpgradeRequired({ state: o.state, emit, route: o.route, ship: o.ship }, e);
|
|
14690
|
+
return { pending: [], started: [], skipped: [] };
|
|
14691
|
+
}
|
|
14692
|
+
}
|
|
14693
|
+
async function runOnePassUnguarded(o) {
|
|
14533
14694
|
if (o.state.isPaused()) return { pending: [], started: [], skipped: [] };
|
|
14534
14695
|
await hydrateApiKeys([o.route], o.getSessionStore);
|
|
14535
14696
|
const emit = o.newEmitter();
|
|
@@ -14840,7 +15001,7 @@ async function buildEnvironment(route, ship, ticket) {
|
|
|
14840
15001
|
}
|
|
14841
15002
|
|
|
14842
15003
|
// src/cli.ts
|
|
14843
|
-
var CREW_HOME = isCompiledBinary(import.meta.url) ?
|
|
15004
|
+
var CREW_HOME = isCompiledBinary(import.meta.url) ? dirname18(process.execPath) : resolve5(dirname18(fileURLToPath(import.meta.url)), "..");
|
|
14844
15005
|
var RELEASE_LOCK_TTL_MS = 20 * 60 * 1e3;
|
|
14845
15006
|
var CYCLE_SECONDS2 = 120;
|
|
14846
15007
|
var STALE_CYCLES = 3;
|
|
@@ -15002,7 +15163,7 @@ function usage() {
|
|
|
15002
15163
|
crew install write and load this platform's scheduler unit
|
|
15003
15164
|
crew uninstall unload and remove it
|
|
15004
15165
|
--workspace-id ID --key K [--project NAME] [--area NAME] [--dir PATH] [--name N]
|
|
15005
|
-
--json --ship-name N --agent-bin PATH --extra-path DIRS --host-passengers --no-wizard --choose KEY=VALUE (non-interactive: docs/commands.md)
|
|
15166
|
+
--json --machine-id ID --ship-name N --agent-bin PATH --extra-path DIRS --host-passengers --no-wizard --choose KEY=VALUE (non-interactive: docs/commands.md)
|
|
15006
15167
|
|
|
15007
15168
|
Options:
|
|
15008
15169
|
--dry-run decide everything, perform nothing
|
|
@@ -15109,13 +15270,19 @@ if (command === "passenger-sync-daemon") {
|
|
|
15109
15270
|
if (!why) return false;
|
|
15110
15271
|
emit.emit(`${why} \u2014 release skipped`, { step: "release" });
|
|
15111
15272
|
return true;
|
|
15273
|
+
}, doctorMachineId = function(stateDir) {
|
|
15274
|
+
try {
|
|
15275
|
+
return ensureMachineId(stateDir, { override: value("machine-id") });
|
|
15276
|
+
} catch {
|
|
15277
|
+
return void 0;
|
|
15278
|
+
}
|
|
15112
15279
|
}, baseHealth = function(r) {
|
|
15113
15280
|
const { remote, base } = r.config.branch;
|
|
15114
15281
|
const out = refreshBaseBranch(r.dir, remote, base, true);
|
|
15115
15282
|
const unsafe = baseBranchUnsafe(out);
|
|
15116
15283
|
return { action: out.action, unsafe, detail: unsafe ? describeUnsafeBase(out, remote, base) : out.detail };
|
|
15117
15284
|
};
|
|
15118
|
-
requireArmed2 = requireArmed, parseTicketNumber2 = parseTicketNumber, passengerApiKeys2 = passengerApiKeys, endpointApiFor2 = endpointApiFor, endpointReconcileDeps2 = endpointReconcileDeps, releaseRefused2 = releaseRefused, baseHealth2 = baseHealth;
|
|
15285
|
+
requireArmed2 = requireArmed, parseTicketNumber2 = parseTicketNumber, passengerApiKeys2 = passengerApiKeys, endpointApiFor2 = endpointApiFor, endpointReconcileDeps2 = endpointReconcileDeps, releaseRefused2 = releaseRefused, doctorMachineId2 = doctorMachineId, baseHealth2 = baseHealth;
|
|
15119
15286
|
let cfg;
|
|
15120
15287
|
let firstRun;
|
|
15121
15288
|
let firstRunFromFlags = false;
|
|
@@ -15401,8 +15568,8 @@ if (command === "passenger-sync-daemon") {
|
|
|
15401
15568
|
}
|
|
15402
15569
|
if (host === "linux" && hasSystemd) {
|
|
15403
15570
|
const label = labelFor(CREW_HOME, "run");
|
|
15404
|
-
const staleTimer =
|
|
15405
|
-
if (
|
|
15571
|
+
const staleTimer = join31(homedir8(), ".config", "systemd", "user", `${label}.timer`);
|
|
15572
|
+
if (existsSync31(staleTimer)) {
|
|
15406
15573
|
process.stdout.write(
|
|
15407
15574
|
`removing stale run timer from before the persistent-service cutover: ${staleTimer}${dryRun ? " (dry run)" : ""}
|
|
15408
15575
|
`
|
|
@@ -15428,14 +15595,14 @@ if (command === "passenger-sync-daemon") {
|
|
|
15428
15595
|
const tracker = new Tracker(c, cfg.ship);
|
|
15429
15596
|
problems.push(...validateContract(tracker.contract).map((p) => `contract: ${p}`));
|
|
15430
15597
|
for (const r of await resolvedRepos(c)) {
|
|
15431
|
-
if (!
|
|
15598
|
+
if (!existsSync31(r.dir)) problems.push(`repo ${r.name}: ${r.dir} is missing on disk`);
|
|
15432
15599
|
if (!satisfies(host, r.config.platform)) problems.push(`repo ${r.name}: requires ${r.config.platform}, this host is ${host}`);
|
|
15433
15600
|
problems.push(...validateEffective(r.config).map((p) => `repo ${r.name}: ${p}`));
|
|
15434
15601
|
}
|
|
15435
15602
|
try {
|
|
15436
15603
|
const ships = await tracker.shipRows();
|
|
15437
15604
|
if (ships.length > 0) {
|
|
15438
|
-
const mine = ships
|
|
15605
|
+
const mine = matchShipRows(ships, cfg.ship.name, doctorMachineId(cfg.ship.stateDir)).rows;
|
|
15439
15606
|
if (mine.length !== 1) problems.push(`ship record: ${mine.length} rows named "${cfg.ship.name}"`);
|
|
15440
15607
|
else if (mine[0].platform && mine[0].platform !== host) problems.push(`ship record: declares ${mine[0].platform}, this host is ${host}`);
|
|
15441
15608
|
}
|
|
@@ -15446,7 +15613,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
15446
15613
|
}
|
|
15447
15614
|
async function doctorFix(o) {
|
|
15448
15615
|
const routes = await Promise.all(routesInScope(cfg.routes, named).map((c) => gatherHealth({ route: c.route, enabled: c.enabled, hasRepos: reposOf(c).length > 0 }, () => routeProblems(c))));
|
|
15449
|
-
const unitExists = (p) =>
|
|
15616
|
+
const unitExists = (p) => existsSync31(p);
|
|
15450
15617
|
const crontab = o.host === "linux" ? spawnSync6("crontab", ["-l"], { encoding: "utf8" }).stdout ?? "" : "";
|
|
15451
15618
|
const schedulerAbsent = o.host !== "windows" ? !schedulerInstalled(planInstall(cfg.ship, CREW_HOME, o.host, detectSystemd(), "run"), unitExists, crontab) : true;
|
|
15452
15619
|
const plan = planFix({ routes, schedulerAbsent, host: o.host });
|
|
@@ -15463,7 +15630,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
15463
15630
|
for (const a of plan.actions) {
|
|
15464
15631
|
if (a.kind === "enable") {
|
|
15465
15632
|
if (prompter && !await prompter.confirm(`Route ${a.route} passes every check \u2014 set enabled: true in ${cfg.configFile}?`, false)) continue;
|
|
15466
|
-
|
|
15633
|
+
writeFileSync18(cfg.configFile, planEnable(readFileSync24(cfg.configFile, "utf8"), a.route));
|
|
15467
15634
|
enabledSound.add(a.route);
|
|
15468
15635
|
changed2.push(`enabled route ${a.route} in ${cfg.configFile}`);
|
|
15469
15636
|
} else {
|
|
@@ -16462,7 +16629,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
16462
16629
|
process.exit(1);
|
|
16463
16630
|
}
|
|
16464
16631
|
requireArmed("run the daemon loop");
|
|
16465
|
-
const watchPaths = isCompiledBinary(import.meta.url) ? [process.execPath] : [
|
|
16632
|
+
const watchPaths = isCompiledBinary(import.meta.url) ? [process.execPath] : [join31(CREW_HOME, "src"), join31(CREW_HOME, "dist"), join31(CREW_HOME, "bin")];
|
|
16466
16633
|
const configPaths = daemonConfigPaths(cfg);
|
|
16467
16634
|
watchPaths.push(...configPaths);
|
|
16468
16635
|
noteDaemonStarted(cfg.ship.stateDir);
|
|
@@ -16663,10 +16830,10 @@ ${changed2.map((c) => ` - ${c}
|
|
|
16663
16830
|
`);
|
|
16664
16831
|
if (!dryRun) {
|
|
16665
16832
|
const resolvedPath = resolvedPathFor(cfg.ship.stateDir, target.route);
|
|
16666
|
-
const raw =
|
|
16833
|
+
const raw = existsSync31(resolvedPath) ? JSON.parse(readFileSync24(resolvedPath, "utf8")) : {};
|
|
16667
16834
|
raw.agentSkills = result.agentSkills;
|
|
16668
|
-
|
|
16669
|
-
|
|
16835
|
+
mkdirSync17(dirOf(resolvedPath), { recursive: true });
|
|
16836
|
+
writeFileSync18(resolvedPath, `${JSON.stringify(raw, null, 2)}
|
|
16670
16837
|
`);
|
|
16671
16838
|
}
|
|
16672
16839
|
if (result.outcomes.some((o) => o.action === "diverged")) process.exitCode = 1;
|
|
@@ -16679,15 +16846,15 @@ ${changed2.map((c) => ` - ${c}
|
|
|
16679
16846
|
process.exit(2);
|
|
16680
16847
|
};
|
|
16681
16848
|
if (sub !== "add" && sub !== "list" && sub !== "clone" && sub !== "hooks") usage2();
|
|
16682
|
-
const configText =
|
|
16849
|
+
const configText = readFileSync24(cfg.configFile, "utf8");
|
|
16683
16850
|
if (sub === "hooks") {
|
|
16684
16851
|
try {
|
|
16685
16852
|
const a = parseHooksArgs(positional.slice(2), routeNamesOf(configText));
|
|
16686
|
-
const dir = repoDirOf(configText,
|
|
16687
|
-
if (!
|
|
16688
|
-
const files = { exists: (r) =>
|
|
16853
|
+
const dir = repoDirOf(configText, dirname18(cfg.configFile), a.route, a.name);
|
|
16854
|
+
if (!existsSync31(dir)) throw new ReposError(`${dir} does not exist on disk`);
|
|
16855
|
+
const files = { exists: (r) => existsSync31(resolvePath(dir, r)), read: (r) => {
|
|
16689
16856
|
try {
|
|
16690
|
-
return
|
|
16857
|
+
return readFileSync24(resolvePath(dir, r), "utf8");
|
|
16691
16858
|
} catch {
|
|
16692
16859
|
return void 0;
|
|
16693
16860
|
}
|
|
@@ -16699,13 +16866,13 @@ ${changed2.map((c) => ` - ${c}
|
|
|
16699
16866
|
if (write && suggestion.source === null) throw new ReposError(`nothing to write: ${suggestion.notes[0]}`);
|
|
16700
16867
|
if (write === "route") {
|
|
16701
16868
|
const plan = planRouteHooks(configText, a.route, a.name, suggestion.hooks);
|
|
16702
|
-
|
|
16869
|
+
writeFileSync18(cfg.configFile, plan.text);
|
|
16703
16870
|
written = { target: "route", file: cfg.configFile, hooks: plan.written, kept: plan.kept };
|
|
16704
16871
|
} else if (write === "repo") {
|
|
16705
16872
|
const have = existingRepoConfig(files);
|
|
16706
16873
|
if (have) throw new ReposError(`${resolvePath(dir, have)} already exists; not overwriting it`);
|
|
16707
16874
|
const file = resolvePath(dir, ".crew.yaml");
|
|
16708
|
-
|
|
16875
|
+
writeFileSync18(file, starterRepoConfig(suggestion.hooks), { flag: "wx" });
|
|
16709
16876
|
written = { target: "repo", file, hooks: Object.keys(suggestion.hooks), kept: [] };
|
|
16710
16877
|
}
|
|
16711
16878
|
if (flag("json")) {
|
|
@@ -16742,7 +16909,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
16742
16909
|
if (sub === "list" && !routeName) usage2();
|
|
16743
16910
|
if (sub === "add") {
|
|
16744
16911
|
try {
|
|
16745
|
-
const a = parseAddArgs(positional.slice(2), routeNamesOf(configText), (p) =>
|
|
16912
|
+
const a = parseAddArgs(positional.slice(2), routeNamesOf(configText), (p) => existsSync31(resolvePath(p.replace(/^~(?=\/|$)/, homedir8()))));
|
|
16746
16913
|
routeName = a.route;
|
|
16747
16914
|
addPath = a.path;
|
|
16748
16915
|
addName = a.name;
|
|
@@ -16771,15 +16938,15 @@ ${changed2.map((c) => ` - ${c}
|
|
|
16771
16938
|
}
|
|
16772
16939
|
const resolvedFile = resolvedPathFor(cfg.ship.stateDir, routeName);
|
|
16773
16940
|
let resolvedRemotes;
|
|
16774
|
-
if (
|
|
16941
|
+
if (existsSync31(resolvedFile)) {
|
|
16775
16942
|
try {
|
|
16776
|
-
resolvedRemotes = JSON.parse(
|
|
16943
|
+
resolvedRemotes = JSON.parse(readFileSync24(resolvedFile, "utf8"));
|
|
16777
16944
|
} catch {
|
|
16778
16945
|
}
|
|
16779
16946
|
}
|
|
16780
16947
|
const originOf = (dir) => gitOk(dir, ["remote", "get-url", "origin"]) ?? void 0;
|
|
16781
16948
|
const text = configText;
|
|
16782
|
-
const base =
|
|
16949
|
+
const base = dirname18(cfg.configFile);
|
|
16783
16950
|
try {
|
|
16784
16951
|
if (sub === "list") {
|
|
16785
16952
|
const repos = configuredRepos(text, routeName, base);
|
|
@@ -16788,7 +16955,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
16788
16955
|
for (const r of repos) {
|
|
16789
16956
|
const origin2 = originOf(r.dir);
|
|
16790
16957
|
const row = matchRepoName(origin2, resolvedRemotes);
|
|
16791
|
-
const note = !
|
|
16958
|
+
const note = !existsSync31(r.dir) ? "missing on disk" : row ? `Repos row: ${row}` : `no Repos row matches ${normalizeRemote(origin2) ?? "this checkout (no origin remote)"}`;
|
|
16792
16959
|
process.stdout.write(`${r.name} ${r.dir} ${note}
|
|
16793
16960
|
`);
|
|
16794
16961
|
}
|
|
@@ -16800,9 +16967,9 @@ ${changed2.map((c) => ` - ${c}
|
|
|
16800
16967
|
` : `${human}
|
|
16801
16968
|
`);
|
|
16802
16969
|
const plan2 = planRepoClone({ text, base, route: routeName, name: addName, resolved: resolvedRemotes, dir: value("dir") });
|
|
16803
|
-
if (
|
|
16970
|
+
if (existsSync31(plan2.target) && (existsSync31(`${plan2.target}/.git`) || readdirSync12(plan2.target).length > 0)) {
|
|
16804
16971
|
throw new ReposError(
|
|
16805
|
-
|
|
16972
|
+
existsSync31(`${plan2.target}/.git`) ? `${plan2.target} is already a checkout \u2014 attach it with \`crew repos add ${routeName} ${plan2.name} ${plan2.target}\`` : `${plan2.target} exists and is not empty \u2014 pass --dir to clone somewhere else`
|
|
16806
16973
|
);
|
|
16807
16974
|
}
|
|
16808
16975
|
if (dryRun) {
|
|
@@ -16820,12 +16987,12 @@ ${changed2.map((c) => ` - ${c}
|
|
|
16820
16987
|
throw new ReposError(`${e.message}${authHint}
|
|
16821
16988
|
crew.yaml was not changed`);
|
|
16822
16989
|
}
|
|
16823
|
-
|
|
16990
|
+
writeFileSync18(cfg.configFile, plan2.text);
|
|
16824
16991
|
say("attached", { route: routeName, name: plan2.name, dir: plan2.target, mode: plan2.mode }, `cloned and attached ${plan2.target} to ${routeName} as ${plan2.mode === "dir" ? "dir" : `repos.${plan2.name}`} in ${cfg.configFile}`);
|
|
16825
16992
|
break;
|
|
16826
16993
|
}
|
|
16827
16994
|
const target = resolvePath(addPath.replace(/^~(?=\/|$)/, homedir8()));
|
|
16828
|
-
const top =
|
|
16995
|
+
const top = existsSync31(target) ? gitOk(target, ["rev-parse", "--show-toplevel"]) : null;
|
|
16829
16996
|
if (!top) throw new ReposError(`${target} is not a git checkout`);
|
|
16830
16997
|
const origin = originOf(top);
|
|
16831
16998
|
const plan = planRepoAdd({
|
|
@@ -16874,15 +17041,15 @@ ${changed2.map((c) => ` - ${c}
|
|
|
16874
17041
|
const created = await rowTracker.createRepoRow(rowPayload);
|
|
16875
17042
|
process.stdout.write(`created Repos row ${String(rowPayload.name)} (${String(rowPayload.remote)}) ${created.id}
|
|
16876
17043
|
`);
|
|
16877
|
-
if (
|
|
16878
|
-
const raw = JSON.parse(
|
|
17044
|
+
if (existsSync31(resolvedFile)) {
|
|
17045
|
+
const raw = JSON.parse(readFileSync24(resolvedFile, "utf8"));
|
|
16879
17046
|
raw.repoNames = { ...raw.repoNames, [created.id]: String(rowPayload.name) };
|
|
16880
17047
|
raw.repoRemotes = { ...raw.repoRemotes, [created.id]: String(rowPayload.remote) };
|
|
16881
|
-
|
|
17048
|
+
writeFileSync18(resolvedFile, `${JSON.stringify(raw, null, 2)}
|
|
16882
17049
|
`);
|
|
16883
17050
|
}
|
|
16884
17051
|
}
|
|
16885
|
-
|
|
17052
|
+
writeFileSync18(cfg.configFile, plan.text);
|
|
16886
17053
|
process.stdout.write(`added ${top} to ${routeName} as ${plan.mode === "dir" ? "dir" : `repos.${plan.name}`} in ${cfg.configFile}
|
|
16887
17054
|
`);
|
|
16888
17055
|
} catch (e) {
|
|
@@ -17103,9 +17270,17 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
17103
17270
|
`);
|
|
17104
17271
|
}
|
|
17105
17272
|
}
|
|
17273
|
+
let machineId;
|
|
17274
|
+
try {
|
|
17275
|
+
machineId = ensureMachineId(cfg.ship.stateDir, { override: value("machine-id") });
|
|
17276
|
+
} catch (e) {
|
|
17277
|
+
process.stderr.write(`(no machine id \u2014 ${e.message})
|
|
17278
|
+
`);
|
|
17279
|
+
}
|
|
17106
17280
|
const ship = {
|
|
17107
17281
|
name: cfg.ship.name,
|
|
17108
17282
|
platform: cfg.ship.platform,
|
|
17283
|
+
machineId,
|
|
17109
17284
|
hostPassengers: firstRun?.answers.hostPassengers ?? existingRoute?.hostPassengers === true,
|
|
17110
17285
|
sshPublicKey
|
|
17111
17286
|
};
|
|
@@ -17202,10 +17377,10 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
17202
17377
|
}
|
|
17203
17378
|
}
|
|
17204
17379
|
let previous;
|
|
17205
|
-
const hadPreviousFile =
|
|
17380
|
+
const hadPreviousFile = existsSync31(resolvedPath);
|
|
17206
17381
|
if (hadPreviousFile) {
|
|
17207
17382
|
try {
|
|
17208
|
-
previous = JSON.parse(
|
|
17383
|
+
previous = JSON.parse(readFileSync24(resolvedPath, "utf8"));
|
|
17209
17384
|
} catch {
|
|
17210
17385
|
}
|
|
17211
17386
|
}
|
|
@@ -17314,10 +17489,10 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
17314
17489
|
process.stderr.write(`(dry run) would write resolved ids to ${resolvedPath}
|
|
17315
17490
|
`);
|
|
17316
17491
|
} else {
|
|
17317
|
-
|
|
17492
|
+
mkdirSync17(dirname18(resolvedPath), { recursive: true });
|
|
17318
17493
|
if (hadPreviousFile) copyFileSync2(resolvedPath, `${resolvedPath}.bak`);
|
|
17319
17494
|
const tmpPath = `${resolvedPath}.tmp${process.pid}`;
|
|
17320
|
-
|
|
17495
|
+
writeFileSync18(tmpPath, `${JSON.stringify(resolved, null, 2)}
|
|
17321
17496
|
`);
|
|
17322
17497
|
renameSync2(tmpPath, resolvedPath);
|
|
17323
17498
|
process.stderr.write(`Resolved ids written to ${resolvedPath}
|
|
@@ -17352,8 +17527,8 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
17352
17527
|
process.stderr.write(`(dry run) would write the minted API key to ${apiKeyFile}
|
|
17353
17528
|
`);
|
|
17354
17529
|
} else {
|
|
17355
|
-
|
|
17356
|
-
|
|
17530
|
+
mkdirSync17(dirname18(apiKeyFile), { recursive: true });
|
|
17531
|
+
writeFileSync18(apiKeyFile, `${apiKeyVar}=${apiKey}
|
|
17357
17532
|
`, { mode: 384 });
|
|
17358
17533
|
process.stderr.write(`API key written to ${apiKeyFile}
|
|
17359
17534
|
`);
|
|
@@ -17362,7 +17537,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
17362
17537
|
const keyPersisted = keychainBacked || apiKeyFile !== void 0;
|
|
17363
17538
|
const wizard = firstRun;
|
|
17364
17539
|
const writeFirstRun = wizard !== void 0 && !dryRun && keyPersisted;
|
|
17365
|
-
const routeDir = value("dir") ?? (writeFirstRun ?
|
|
17540
|
+
const routeDir = value("dir") ?? (writeFirstRun ? join31(DEFAULT_REPOS_BASE_PATH, found.workspaceSlug) : void 0);
|
|
17366
17541
|
const routeBlock = renderConnection(
|
|
17367
17542
|
found,
|
|
17368
17543
|
route2,
|
|
@@ -18006,7 +18181,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
18006
18181
|
};
|
|
18007
18182
|
const droppedRoutes = [];
|
|
18008
18183
|
try {
|
|
18009
|
-
const rawCfg = parseYaml2(
|
|
18184
|
+
const rawCfg = parseYaml2(readFileSync24(cfg.configFile, "utf8"));
|
|
18010
18185
|
for (const r of rawCfg?.routes ?? []) {
|
|
18011
18186
|
if (typeof r?.route !== "string" || cfg.routes.some((x) => x.route === r.route)) continue;
|
|
18012
18187
|
try {
|
|
@@ -18048,8 +18223,8 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
18048
18223
|
process.stdout.write("crew reload: no daemon unit is running \u2014 nothing to reload (the next run reads crew.yaml fresh)\n");
|
|
18049
18224
|
break;
|
|
18050
18225
|
}
|
|
18051
|
-
|
|
18052
|
-
|
|
18226
|
+
mkdirSync17(cfg.ship.stateDir, { recursive: true });
|
|
18227
|
+
writeFileSync18(sentinel, `${(/* @__PURE__ */ new Date()).toISOString()}
|
|
18053
18228
|
`);
|
|
18054
18229
|
process.stdout.write("crew reload: requested \u2014 the daemon finishes its in-flight pass, then relaunches with the new config\n");
|
|
18055
18230
|
if (flag("now") && control?.stopCommand && control.startCommand) {
|
|
@@ -18100,7 +18275,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
18100
18275
|
}
|
|
18101
18276
|
case "log": {
|
|
18102
18277
|
try {
|
|
18103
|
-
const text =
|
|
18278
|
+
const text = readFileSync24(cfg.ship.logFile, "utf8").split("\n");
|
|
18104
18279
|
process.stdout.write(`${text.slice(-(Number(value("tail")) || 40)).join("\n")}
|
|
18105
18280
|
`);
|
|
18106
18281
|
} catch {
|
|
@@ -18123,8 +18298,8 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
18123
18298
|
};
|
|
18124
18299
|
if (!route.resolved) fail(`route ${route.route} is not connected \u2014 run \`crew connect\` first`);
|
|
18125
18300
|
const tracker = new Tracker(route, cfg.ship);
|
|
18126
|
-
const configText =
|
|
18127
|
-
const cfgBase =
|
|
18301
|
+
const configText = readFileSync24(cfg.configFile, "utf8");
|
|
18302
|
+
const cfgBase = dirname18(cfg.configFile);
|
|
18128
18303
|
const configured = configuredRepos(configText, route.route, cfgBase);
|
|
18129
18304
|
const dir = configured.find((r) => r.name === SAMPLE_REPO)?.dir ?? sampleDir(configured.map((r) => r.dir), route.reposBasePath, route.route.split("/")[0]);
|
|
18130
18305
|
const resolvedFile = resolvedPathFor(cfg.ship.stateDir, route.route);
|
|
@@ -18152,7 +18327,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
18152
18327
|
}
|
|
18153
18328
|
if (configured.some((r) => r.dir === dir)) {
|
|
18154
18329
|
try {
|
|
18155
|
-
|
|
18330
|
+
writeFileSync18(cfg.configFile, planRepoRemove(configText, cfgBase, route.route, dir).text);
|
|
18156
18331
|
} catch (e) {
|
|
18157
18332
|
if (e instanceof ReposError) fail(e.message);
|
|
18158
18333
|
throw e;
|
|
@@ -18160,29 +18335,29 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
18160
18335
|
}
|
|
18161
18336
|
if (row) {
|
|
18162
18337
|
await tracker.deleteRepoRow(row.id);
|
|
18163
|
-
if (
|
|
18164
|
-
const raw = JSON.parse(
|
|
18338
|
+
if (existsSync31(resolvedFile)) {
|
|
18339
|
+
const raw = JSON.parse(readFileSync24(resolvedFile, "utf8"));
|
|
18165
18340
|
if (raw.repoNames) delete raw.repoNames[row.id];
|
|
18166
18341
|
if (raw.repoRemotes) delete raw.repoRemotes[row.id];
|
|
18167
|
-
|
|
18342
|
+
writeFileSync18(resolvedFile, `${JSON.stringify(raw, null, 2)}
|
|
18168
18343
|
`);
|
|
18169
18344
|
}
|
|
18170
18345
|
}
|
|
18171
|
-
if (
|
|
18346
|
+
if (existsSync31(join31(dir, ".git")) && basename10(dir) === SAMPLE_REPO) rmSync12(dir, { recursive: true, force: true });
|
|
18172
18347
|
say("removed", { dir, closed }, `removed the sample repo${closed.length ? `, closed ${closed.join(", ")}` : ""}`);
|
|
18173
18348
|
break;
|
|
18174
18349
|
}
|
|
18175
18350
|
const projectId = route.resolved.areaId ?? resolveRepoProject(await tracker.projectRows(), value("project"));
|
|
18176
|
-
const fresh = !
|
|
18177
|
-
if (
|
|
18351
|
+
const fresh = !existsSync31(join31(dir, ".git"));
|
|
18352
|
+
if (existsSync31(dir) && fresh && readdirSync12(dir).length > 0) fail(`${dir} exists and is not a git checkout \u2014 move it aside`);
|
|
18178
18353
|
if (dryRun) {
|
|
18179
18354
|
say("dry-run", { dir, newCheckout: fresh, repoRow: row?.id ?? null }, `would ${fresh ? "create" : "reuse"} ${dir}${row ? "" : ", create the Repos row"}, and file one ${tracker.contract.statuses.approved} ticket`);
|
|
18180
18355
|
break;
|
|
18181
18356
|
}
|
|
18182
18357
|
if (fresh) {
|
|
18183
|
-
|
|
18184
|
-
|
|
18185
|
-
|
|
18358
|
+
mkdirSync17(dir, { recursive: true });
|
|
18359
|
+
writeFileSync18(join31(dir, "README.md"), SAMPLE_README);
|
|
18360
|
+
writeFileSync18(join31(dir, ".crew.yaml"), SAMPLE_CREW_YAML);
|
|
18186
18361
|
const g = (...a) => {
|
|
18187
18362
|
const r = spawnSync6("git", a, { cwd: dir, encoding: "utf8" });
|
|
18188
18363
|
if (r.status !== 0) fail(`git ${a[0]} failed in ${dir}: ${(r.stderr || r.stdout).trim()}`);
|
|
@@ -18200,17 +18375,17 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
18200
18375
|
spec: { platform: "unix", releaseMode: "integrate", ciProvider: "none" }
|
|
18201
18376
|
});
|
|
18202
18377
|
repoId = (await tracker.createRepoRow(payload)).id;
|
|
18203
|
-
if (
|
|
18204
|
-
const raw = JSON.parse(
|
|
18378
|
+
if (existsSync31(resolvedFile)) {
|
|
18379
|
+
const raw = JSON.parse(readFileSync24(resolvedFile, "utf8"));
|
|
18205
18380
|
raw.repoNames = { ...raw.repoNames, [repoId]: SAMPLE_REPO };
|
|
18206
18381
|
raw.repoRemotes = { ...raw.repoRemotes, [repoId]: SAMPLE_REMOTE };
|
|
18207
|
-
|
|
18382
|
+
writeFileSync18(resolvedFile, `${JSON.stringify(raw, null, 2)}
|
|
18208
18383
|
`);
|
|
18209
18384
|
}
|
|
18210
18385
|
}
|
|
18211
18386
|
if (!configured.some((r) => r.dir === dir)) {
|
|
18212
18387
|
try {
|
|
18213
|
-
|
|
18388
|
+
writeFileSync18(cfg.configFile, planRepoAdd({ text: configText, base: cfgBase, route: route.route, path: dir, name: SAMPLE_REPO }).text);
|
|
18214
18389
|
} catch (e) {
|
|
18215
18390
|
if (e instanceof ReposError) fail(e.message);
|
|
18216
18391
|
throw e;
|
|
@@ -18312,6 +18487,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
18312
18487
|
const issueLabelById = new Map(engagedTickets.map((t) => [t.id, t.issue_id]));
|
|
18313
18488
|
const todo = await statusTracker.openTickets().then((open) => operatorTodo(open, statusTracker.contract, route.resolved?.operator ?? "")).catch(() => []);
|
|
18314
18489
|
const projects = projectModeLabels(await statusTracker.projectRowsWithModes().catch(() => []));
|
|
18490
|
+
const crewMinVersion = await readCrewFloor(route, cfg.ship) ?? null;
|
|
18315
18491
|
if (flag("json")) {
|
|
18316
18492
|
const repos = await resolvedRepos(route);
|
|
18317
18493
|
const seatIds = route.resolved?.seats ?? {};
|
|
@@ -18371,6 +18547,9 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
18371
18547
|
watermark: state.watermark(),
|
|
18372
18548
|
route: {
|
|
18373
18549
|
name: route.route,
|
|
18550
|
+
// Additive (CREW-2248): the running version and the workspace's floor. `version` stays 1.
|
|
18551
|
+
crewVersion: sentVersion(cfg.ship),
|
|
18552
|
+
crewMinVersion,
|
|
18374
18553
|
// Additive (CREW-1496): each project and its work mode. `version` stays 1.
|
|
18375
18554
|
projects: projects.map((p) => ({ id: p.id, name: p.name, workMode: p.mode })),
|
|
18376
18555
|
dir: route.dir,
|
|
@@ -18500,7 +18679,7 @@ watermark: ${state.watermark()}
|
|
|
18500
18679
|
}
|
|
18501
18680
|
for (const r of repos) {
|
|
18502
18681
|
const remote = r.config.branch.remote;
|
|
18503
|
-
if (
|
|
18682
|
+
if (existsSync31(r.dir)) {
|
|
18504
18683
|
pushAuth.push({
|
|
18505
18684
|
route: c.route,
|
|
18506
18685
|
repo: r.name,
|
|
@@ -18528,7 +18707,7 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
|
|
|
18528
18707
|
);
|
|
18529
18708
|
try {
|
|
18530
18709
|
const evFile = eventFileFor(cfg.ship.stateDir);
|
|
18531
|
-
const evSize =
|
|
18710
|
+
const evSize = existsSync31(evFile) ? statSync10(evFile).size : 0;
|
|
18532
18711
|
process.stdout.write(
|
|
18533
18712
|
evSize > cfg.ship.eventsRotateBytes * 4 ? `events log: WARN ${evFile} is ${Math.round(evSize / 1048576)} MB (> 4x eventsRotateBytes); run \`crew reap\` to rotate it
|
|
18534
18713
|
` : "events log: ok\n"
|
|
@@ -18539,7 +18718,7 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
|
|
|
18539
18718
|
const unitPlan = planInstall(cfg.ship, CREW_HOME, host, detectSystemd(), "run");
|
|
18540
18719
|
const abandons = unitAbandonsAgents(unitPlan, (p) => {
|
|
18541
18720
|
try {
|
|
18542
|
-
return
|
|
18721
|
+
return readFileSync24(p, "utf8");
|
|
18543
18722
|
} catch {
|
|
18544
18723
|
return null;
|
|
18545
18724
|
}
|
|
@@ -18561,6 +18740,19 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
|
|
|
18561
18740
|
}
|
|
18562
18741
|
}
|
|
18563
18742
|
const tracker = new Tracker(route, cfg.ship);
|
|
18743
|
+
{
|
|
18744
|
+
const have = sentVersion(cfg.ship);
|
|
18745
|
+
const floor = await readCrewFloor(route, cfg.ship);
|
|
18746
|
+
if (have && floor) {
|
|
18747
|
+
process.stdout.write(
|
|
18748
|
+
`crew version: ${belowFloor(have, floor) ? "WARN " : ""}${have} (workspace requires ${floor})${belowFloor(have, floor) ? " \u2014 update crew (the Crew app, or `npm install -g @tablation/crew`)" : ""}
|
|
18749
|
+
`
|
|
18750
|
+
);
|
|
18751
|
+
} else if (have) {
|
|
18752
|
+
process.stdout.write(`crew version: ${have}${floor === null ? " (workspace sets no minimum)" : ""}
|
|
18753
|
+
`);
|
|
18754
|
+
}
|
|
18755
|
+
}
|
|
18564
18756
|
const contractProblems = validateContract(tracker.contract);
|
|
18565
18757
|
process.stdout.write(
|
|
18566
18758
|
contractProblems.length ? `contract: ${contractProblems.length} problem(s)
|
|
@@ -18640,7 +18832,12 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
|
|
|
18640
18832
|
if (ships.length === 0) {
|
|
18641
18833
|
process.stdout.write("ship record: this workspace has no Ships table (fine)\n");
|
|
18642
18834
|
} else {
|
|
18643
|
-
const
|
|
18835
|
+
const matched = matchShipRows(ships, cfg.ship.name, doctorMachineId(cfg.ship.stateDir));
|
|
18836
|
+
const mine = matched.rows;
|
|
18837
|
+
process.stdout.write(
|
|
18838
|
+
matched.hasColumn ? `ship row: ${mine.length === 1 ? `matched by ${matched.by === "machine_id" ? "machine id" : "name"}` : "not matched"}
|
|
18839
|
+
` : "ship row: matched by name only \u2014 the Ships table has no machine_id column (update the Issues template)\n"
|
|
18840
|
+
);
|
|
18644
18841
|
if (mine.length === 0) {
|
|
18645
18842
|
process.stdout.write(
|
|
18646
18843
|
`ship record: NO row named "${cfg.ship.name}" \u2014 have: ${ships.map((s2) => s2.name).join(", ")}
|
|
@@ -18923,4 +19120,5 @@ var passengerApiKeys2;
|
|
|
18923
19120
|
var endpointApiFor2;
|
|
18924
19121
|
var endpointReconcileDeps2;
|
|
18925
19122
|
var releaseRefused2;
|
|
19123
|
+
var doctorMachineId2;
|
|
18926
19124
|
var baseHealth2;
|
package/package.json
CHANGED