@tablation/crew 0.76.0 → 0.78.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/README.md +1 -0
- package/dist/cli.js +530 -205
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -263,6 +263,7 @@ crew inbox [--member NAME] your tickets across every workspace
|
|
|
263
263
|
crew connect WS[/PROJECT] resolve a workspace/project's ids into the state tree
|
|
264
264
|
crew repos add [ROUTE] [NAME] [PATH] attach a local checkout to a route in crew.yaml, creating its Repos row if missing (--dry-run previews)
|
|
265
265
|
crew repos list ROUTE the checkouts a route has, and the tracker Repos row each matches
|
|
266
|
+
crew repos check [PATH] validate a checkout's .crew.yaml (parse, release readiness, hook syntax) without attaching it (--route ROUTE, --strict, --json)
|
|
266
267
|
crew repos hooks [ROUTE] NAME suggest test/build hooks for a checkout (--write route|repo [--replace] writes them, --clear route removes them, --recipes, --hook KEY=CMD, --agent, --json)
|
|
267
268
|
crew install [--replace] write and load this platform's scheduler unit (macOS: clears a crew installed from another path)
|
|
268
269
|
crew uninstall [--all] unload and remove it (--all: every crew launchd unit, whichever path it came from)
|
package/dist/cli.js
CHANGED
|
@@ -4853,6 +4853,158 @@ function renderEnvironment(i) {
|
|
|
4853
4853
|
return lines.join("\n");
|
|
4854
4854
|
}
|
|
4855
4855
|
|
|
4856
|
+
// src/contract.ts
|
|
4857
|
+
var DEFAULT_CONTRACT = {
|
|
4858
|
+
columns: {
|
|
4859
|
+
key: "issue_id",
|
|
4860
|
+
tag: "issue_tag",
|
|
4861
|
+
title: "title",
|
|
4862
|
+
status: "status",
|
|
4863
|
+
assignee: "assignee_id",
|
|
4864
|
+
priority: "priority",
|
|
4865
|
+
severity: "severity",
|
|
4866
|
+
slice: "project_id",
|
|
4867
|
+
repo: "repo_id",
|
|
4868
|
+
parent: "parent_id",
|
|
4869
|
+
blockedBy: "blocked_by",
|
|
4870
|
+
updatedAt: "updated_at",
|
|
4871
|
+
needsDesign: "needs_design",
|
|
4872
|
+
attachments: "attachments",
|
|
4873
|
+
workMode: "work_mode",
|
|
4874
|
+
reportType: "report_type",
|
|
4875
|
+
epic: "epic_id",
|
|
4876
|
+
author: "filed_by_id",
|
|
4877
|
+
ship: "ship_id",
|
|
4878
|
+
heldBy: "held_by_ship_id",
|
|
4879
|
+
resolutionNote: "resolution_note",
|
|
4880
|
+
verifiedSha: "verified_sha",
|
|
4881
|
+
prRef: "pr_ref",
|
|
4882
|
+
externalId: "external_id",
|
|
4883
|
+
position: "position",
|
|
4884
|
+
agentRuns: "agent_runs",
|
|
4885
|
+
tokensIn: "tokens_in",
|
|
4886
|
+
tokensOut: "tokens_out",
|
|
4887
|
+
cacheReadTokens: "cache_read_tokens",
|
|
4888
|
+
cacheWriteTokens: "cache_write_tokens",
|
|
4889
|
+
costUsd: "cost_usd"
|
|
4890
|
+
},
|
|
4891
|
+
comments: {
|
|
4892
|
+
parent: "ticket_id",
|
|
4893
|
+
body: "body",
|
|
4894
|
+
author: "team_member_id",
|
|
4895
|
+
createdAt: "created_at",
|
|
4896
|
+
ship: "ship_id",
|
|
4897
|
+
publicReply: "is_public_reply",
|
|
4898
|
+
authorName: "author_name"
|
|
4899
|
+
},
|
|
4900
|
+
statuses: {
|
|
4901
|
+
open: ["new", "accepted", "blocked", "in_progress", "needs_info", "fixed", "qa", "verified"],
|
|
4902
|
+
resolved: [
|
|
4903
|
+
"verified",
|
|
4904
|
+
"closed_deployed",
|
|
4905
|
+
"closed_wont_fix",
|
|
4906
|
+
"closed_duplicate",
|
|
4907
|
+
"closed_completed",
|
|
4908
|
+
"closed_cant_reproduce"
|
|
4909
|
+
],
|
|
4910
|
+
approved: "accepted",
|
|
4911
|
+
parked: "blocked",
|
|
4912
|
+
building: "in_progress",
|
|
4913
|
+
handoff: "fixed",
|
|
4914
|
+
verifying: "qa",
|
|
4915
|
+
verified: "verified",
|
|
4916
|
+
deployed: "closed_deployed",
|
|
4917
|
+
needsHuman: "needs_info",
|
|
4918
|
+
reviewing: null,
|
|
4919
|
+
wontFix: "closed_wont_fix",
|
|
4920
|
+
draft: "draft"
|
|
4921
|
+
},
|
|
4922
|
+
epics: {
|
|
4923
|
+
statusColumn: "status",
|
|
4924
|
+
keyColumn: "epic_id",
|
|
4925
|
+
statuses: { planned: "planned", building: "in_progress", done: "done", cancelled: "cancelled" }
|
|
4926
|
+
},
|
|
4927
|
+
priorityOrder: ["p0", "p1", "p2", "p3"],
|
|
4928
|
+
severityOrder: ["s1", "s2", "s3", "s4"],
|
|
4929
|
+
unknownPriorityRank: 2,
|
|
4930
|
+
coordinatingValue: "coordinating",
|
|
4931
|
+
manualWorkMode: "manual",
|
|
4932
|
+
hybridWorkMode: "hybrid",
|
|
4933
|
+
repoExemptReportTypes: ["coordinating", "question", "investigation"]
|
|
4934
|
+
};
|
|
4935
|
+
function closedStatuses(c) {
|
|
4936
|
+
return c.statuses.resolved.filter((s) => s !== c.statuses.verified);
|
|
4937
|
+
}
|
|
4938
|
+
function resolveContract(override) {
|
|
4939
|
+
if (!override) return DEFAULT_CONTRACT;
|
|
4940
|
+
return {
|
|
4941
|
+
columns: { ...DEFAULT_CONTRACT.columns, ...override.columns },
|
|
4942
|
+
comments: { ...DEFAULT_CONTRACT.comments, ...override.comments },
|
|
4943
|
+
statuses: { ...DEFAULT_CONTRACT.statuses, ...override.statuses },
|
|
4944
|
+
epics: {
|
|
4945
|
+
...DEFAULT_CONTRACT.epics,
|
|
4946
|
+
...override.epics,
|
|
4947
|
+
statuses: { ...DEFAULT_CONTRACT.epics.statuses, ...override.epics?.statuses }
|
|
4948
|
+
},
|
|
4949
|
+
priorityOrder: override.priorityOrder ?? DEFAULT_CONTRACT.priorityOrder,
|
|
4950
|
+
severityOrder: override.severityOrder ?? DEFAULT_CONTRACT.severityOrder,
|
|
4951
|
+
unknownPriorityRank: override.unknownPriorityRank ?? DEFAULT_CONTRACT.unknownPriorityRank,
|
|
4952
|
+
coordinatingValue: override.coordinatingValue ?? DEFAULT_CONTRACT.coordinatingValue,
|
|
4953
|
+
manualWorkMode: override.manualWorkMode ?? DEFAULT_CONTRACT.manualWorkMode,
|
|
4954
|
+
hybridWorkMode: override.hybridWorkMode ?? DEFAULT_CONTRACT.hybridWorkMode,
|
|
4955
|
+
// A workspace overriding `coordinatingValue` alone (renaming the rollup
|
|
4956
|
+
// marker) still gets that renamed value exempted here by default,
|
|
4957
|
+
// without also having to repeat it — only an explicit
|
|
4958
|
+
// `repoExemptReportTypes` overrides this derived default.
|
|
4959
|
+
repoExemptReportTypes: override.repoExemptReportTypes ?? (override.coordinatingValue ? [override.coordinatingValue, "question", "investigation"] : DEFAULT_CONTRACT.repoExemptReportTypes)
|
|
4960
|
+
};
|
|
4961
|
+
}
|
|
4962
|
+
function validateContract(c) {
|
|
4963
|
+
const problems = [];
|
|
4964
|
+
const open = new Set(c.statuses.open);
|
|
4965
|
+
const resolved = new Set(c.statuses.resolved);
|
|
4966
|
+
for (const role of ["approved", "parked", "building", "handoff", "verifying", "needsHuman"]) {
|
|
4967
|
+
const v = c.statuses[role];
|
|
4968
|
+
if (!open.has(v)) problems.push(`statuses.${role} ("${v}") is not listed in statuses.open`);
|
|
4969
|
+
}
|
|
4970
|
+
if (c.statuses.reviewing && !open.has(c.statuses.reviewing)) {
|
|
4971
|
+
problems.push(`statuses.reviewing ("${c.statuses.reviewing}") is not listed in statuses.open`);
|
|
4972
|
+
}
|
|
4973
|
+
if (!resolved.has(c.statuses.deployed)) {
|
|
4974
|
+
problems.push(`statuses.deployed ("${c.statuses.deployed}") is not listed in statuses.resolved`);
|
|
4975
|
+
}
|
|
4976
|
+
if (resolved.has(c.statuses.handoff)) {
|
|
4977
|
+
problems.push(
|
|
4978
|
+
`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`
|
|
4979
|
+
);
|
|
4980
|
+
}
|
|
4981
|
+
if (!resolved.has(c.statuses.wontFix)) {
|
|
4982
|
+
problems.push(`statuses.wontFix ("${c.statuses.wontFix}") is not listed in statuses.resolved`);
|
|
4983
|
+
}
|
|
4984
|
+
if (c.statuses.reviewing && resolved.has(c.statuses.reviewing)) {
|
|
4985
|
+
problems.push(
|
|
4986
|
+
`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`
|
|
4987
|
+
);
|
|
4988
|
+
}
|
|
4989
|
+
if (resolved.has(c.statuses.approved)) {
|
|
4990
|
+
problems.push(`statuses.approved ("${c.statuses.approved}") is listed as resolved`);
|
|
4991
|
+
}
|
|
4992
|
+
if (c.statuses.parked === c.statuses.approved) {
|
|
4993
|
+
problems.push("statuses.parked and statuses.approved are the same value \u2014 parking would be a no-op");
|
|
4994
|
+
}
|
|
4995
|
+
const epicValues = Object.values(c.epics.statuses);
|
|
4996
|
+
if (new Set(epicValues).size !== epicValues.length) {
|
|
4997
|
+
problems.push("epics.statuses names the same value twice \u2014 the epic sync could not tell those states apart");
|
|
4998
|
+
}
|
|
4999
|
+
if (c.priorityOrder.length === 0) problems.push("priorityOrder is empty");
|
|
5000
|
+
if (c.severityOrder.length === 0) problems.push("severityOrder is empty");
|
|
5001
|
+
for (const [name, order] of [["priorityOrder", c.priorityOrder], ["severityOrder", c.severityOrder]]) {
|
|
5002
|
+
if (new Set(order).size !== order.length) problems.push(`${name} contains duplicates`);
|
|
5003
|
+
}
|
|
5004
|
+
return problems;
|
|
5005
|
+
}
|
|
5006
|
+
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.";
|
|
5007
|
+
|
|
4856
5008
|
// src/agent-log.ts
|
|
4857
5009
|
import { existsSync as existsSync11, mkdirSync as mkdirSync6, readdirSync as readdirSync4, readFileSync as readFileSync11, rmSync as rmSync6, writeFileSync as writeFileSync7 } from "fs";
|
|
4858
5010
|
import { join as join12 } from "path";
|
|
@@ -4870,6 +5022,61 @@ var AgentLogHttpError = class extends Error {
|
|
|
4870
5022
|
this.status = status2;
|
|
4871
5023
|
}
|
|
4872
5024
|
};
|
|
5025
|
+
async function request(target, method, path, body) {
|
|
5026
|
+
const url = `${target.baseUrl.replace(/\/+$/, "")}/api${path}`;
|
|
5027
|
+
const res = await fetch(url, {
|
|
5028
|
+
method,
|
|
5029
|
+
headers: {
|
|
5030
|
+
Authorization: `Bearer ${target.apiKey}`,
|
|
5031
|
+
"User-Agent": target.userAgent,
|
|
5032
|
+
...body === void 0 ? {} : { "Content-Type": "application/json" }
|
|
5033
|
+
},
|
|
5034
|
+
body: body === void 0 ? void 0 : JSON.stringify(body)
|
|
5035
|
+
});
|
|
5036
|
+
if (!res.ok) throw new AgentLogHttpError(`${path}: ${res.status} ${res.statusText}`, res.status);
|
|
5037
|
+
return await res.json();
|
|
5038
|
+
}
|
|
5039
|
+
var issueColumns = /* @__PURE__ */ new Map();
|
|
5040
|
+
function columnsOf(target, modelId) {
|
|
5041
|
+
const key = `${target.baseUrl}|${target.workspaceId}|${modelId}`;
|
|
5042
|
+
let p = issueColumns.get(key);
|
|
5043
|
+
if (!p) {
|
|
5044
|
+
p = request(target, "GET", `/data-models/${modelId}`).then((m) => new Set((m.fields ?? []).map((f) => f.columnName)));
|
|
5045
|
+
p.catch(() => issueColumns.delete(key));
|
|
5046
|
+
issueColumns.set(key, p);
|
|
5047
|
+
}
|
|
5048
|
+
return p;
|
|
5049
|
+
}
|
|
5050
|
+
async function findTicketRecord(target, t, ref) {
|
|
5051
|
+
const filters = {
|
|
5052
|
+
combinator: "OR",
|
|
5053
|
+
conditions: [
|
|
5054
|
+
{ columnName: t.keyColumn, operator: "EQ", value: ref },
|
|
5055
|
+
{ columnName: t.tagColumn, operator: "EQ", value: ref }
|
|
5056
|
+
]
|
|
5057
|
+
};
|
|
5058
|
+
const rows = await request(
|
|
5059
|
+
target,
|
|
5060
|
+
"GET",
|
|
5061
|
+
`/data-models/${t.issuesModelId}/records?limit=2&filters=${encodeURIComponent(JSON.stringify(filters))}`
|
|
5062
|
+
);
|
|
5063
|
+
return rows.length === 1 ? rows[0].id : void 0;
|
|
5064
|
+
}
|
|
5065
|
+
function counterIncrements(run2, cols, have) {
|
|
5066
|
+
const figures = [
|
|
5067
|
+
[cols.agentRuns, 1],
|
|
5068
|
+
[cols.tokensIn, run2.tokensIn],
|
|
5069
|
+
[cols.tokensOut, run2.tokensOut],
|
|
5070
|
+
[cols.cacheReadTokens, run2.cacheReadTokens],
|
|
5071
|
+
[cols.cacheWriteTokens, run2.cacheWriteTokens],
|
|
5072
|
+
[cols.costUsd, run2.costUsd]
|
|
5073
|
+
];
|
|
5074
|
+
const out = {};
|
|
5075
|
+
for (const [column, value2] of figures) {
|
|
5076
|
+
if (value2 !== void 0 && Number.isFinite(value2) && have.has(column)) out[column] = { $op: "increment", value: value2 };
|
|
5077
|
+
}
|
|
5078
|
+
return out;
|
|
5079
|
+
}
|
|
4873
5080
|
async function post(target, path, body) {
|
|
4874
5081
|
const url = `${target.baseUrl.replace(/\/+$/, "")}/api/workspaces/${target.workspaceId}${path}`;
|
|
4875
5082
|
const res = await fetch(url, {
|
|
@@ -4886,6 +5093,13 @@ async function post(target, path, body) {
|
|
|
4886
5093
|
return await res.json();
|
|
4887
5094
|
}
|
|
4888
5095
|
async function reportAgentRun(target, run2) {
|
|
5096
|
+
let ticketRecordId;
|
|
5097
|
+
if (target.tickets && run2.ticketReference) {
|
|
5098
|
+
try {
|
|
5099
|
+
ticketRecordId = await findTicketRecord(target, target.tickets, run2.ticketReference);
|
|
5100
|
+
} catch {
|
|
5101
|
+
}
|
|
5102
|
+
}
|
|
4889
5103
|
const body = {
|
|
4890
5104
|
agentId: target.agentId,
|
|
4891
5105
|
ticketReference: run2.ticketReference,
|
|
@@ -4903,13 +5117,14 @@ async function reportAgentRun(target, run2) {
|
|
|
4903
5117
|
promptVersion: run2.promptVersion,
|
|
4904
5118
|
promptSha: run2.promptSha
|
|
4905
5119
|
};
|
|
5120
|
+
const workItem = target.tickets && ticketRecordId ? { workItemTable: target.tickets.issuesModelId, workItemRecordId: ticketRecordId } : {};
|
|
4906
5121
|
let entry;
|
|
4907
5122
|
try {
|
|
4908
|
-
entry = await post(target, "/agents/log", { ...body, harness: run2.harness, provider: run2.provider });
|
|
5123
|
+
entry = await post(target, "/agents/log", { ...body, ...workItem, harness: run2.harness, provider: run2.provider });
|
|
4909
5124
|
} catch (e) {
|
|
4910
5125
|
if (!(e instanceof AgentLogHttpError)) throw new AgentLogUnreachableError(e.message);
|
|
4911
5126
|
if (e.status >= 500) throw new AgentLogUnreachableError(e.message);
|
|
4912
|
-
if (
|
|
5127
|
+
if (e.status !== 400 || !run2.harness && !run2.provider && !ticketRecordId) throw e;
|
|
4913
5128
|
entry = await post(target, "/agents/log", body);
|
|
4914
5129
|
}
|
|
4915
5130
|
for (const cycle of run2.cycles) {
|
|
@@ -4920,7 +5135,37 @@ async function reportAgentRun(target, run2) {
|
|
|
4920
5135
|
...cycle.role ? { role: cycle.role } : {}
|
|
4921
5136
|
});
|
|
4922
5137
|
}
|
|
4923
|
-
|
|
5138
|
+
const counters = target.tickets && ticketRecordId ? await addRunToTicket(target, target.tickets, ticketRecordId, run2) : void 0;
|
|
5139
|
+
return {
|
|
5140
|
+
agentLogId: entry.id,
|
|
5141
|
+
...counters?.warning ? { counterWarning: counters.warning } : {},
|
|
5142
|
+
...counters?.retry ? { counterRetry: counters.retry } : {}
|
|
5143
|
+
};
|
|
5144
|
+
}
|
|
5145
|
+
var isRetryable = (e) => !(e instanceof AgentLogHttpError) || e.status >= 500;
|
|
5146
|
+
async function writeCounters(target, w) {
|
|
5147
|
+
const have = await columnsOf(target, w.issuesModelId);
|
|
5148
|
+
const body = Object.fromEntries(Object.entries(w.body).filter(([column]) => have.has(column)));
|
|
5149
|
+
if (Object.keys(body).length === 0) return;
|
|
5150
|
+
await request(target, "PATCH", `/data-models/${w.issuesModelId}/records/${w.recordId}`, body);
|
|
5151
|
+
}
|
|
5152
|
+
async function addRunToTicket(target, t, recordId, run2) {
|
|
5153
|
+
const write = {
|
|
5154
|
+
issuesModelId: t.issuesModelId,
|
|
5155
|
+
recordId,
|
|
5156
|
+
ticketReference: run2.ticketReference,
|
|
5157
|
+
counters: t.counters,
|
|
5158
|
+
body: counterIncrements(run2, t.counters, new Set(Object.values(t.counters)))
|
|
5159
|
+
};
|
|
5160
|
+
try {
|
|
5161
|
+
await writeCounters(target, write);
|
|
5162
|
+
return {};
|
|
5163
|
+
} catch (e) {
|
|
5164
|
+
return {
|
|
5165
|
+
warning: `could not add run spend to ${run2.ticketReference}: ${e.message}`,
|
|
5166
|
+
...isRetryable(e) ? { retry: write } : {}
|
|
5167
|
+
};
|
|
5168
|
+
}
|
|
4924
5169
|
}
|
|
4925
5170
|
var queueDir = (stateDir) => join12(stateDir, "pending-agent-logs");
|
|
4926
5171
|
function queueAgentRun(stateDir, name, q) {
|
|
@@ -4943,7 +5188,17 @@ async function replayQueuedAgentRuns(stateDir, route, keyFor, log, now = /* @__P
|
|
|
4943
5188
|
try {
|
|
4944
5189
|
const apiKey = await keyFor(q.role);
|
|
4945
5190
|
if (!apiKey) continue;
|
|
4946
|
-
await reportAgentRun({ ...q.target, apiKey }, q.report);
|
|
5191
|
+
const sentRun = await reportAgentRun({ ...q.target, apiKey }, q.report);
|
|
5192
|
+
if (sentRun.counterWarning) log.warn(sentRun.counterWarning);
|
|
5193
|
+
if (sentRun.counterRetry) queueTicketCounters(stateDir, f.replace(/\.json$/, ""), {
|
|
5194
|
+
version: 1,
|
|
5195
|
+
route: q.route,
|
|
5196
|
+
role: q.role,
|
|
5197
|
+
target: q.target,
|
|
5198
|
+
write: sentRun.counterRetry,
|
|
5199
|
+
attempts: 0,
|
|
5200
|
+
nextAttemptAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
5201
|
+
});
|
|
4947
5202
|
rmSync6(path, { force: true });
|
|
4948
5203
|
sent += 1;
|
|
4949
5204
|
log.emit(`sent a queued Agent Log report (${f})`);
|
|
@@ -4965,6 +5220,49 @@ async function replayQueuedAgentRuns(stateDir, route, keyFor, log, now = /* @__P
|
|
|
4965
5220
|
}
|
|
4966
5221
|
return sent;
|
|
4967
5222
|
}
|
|
5223
|
+
var countersDir = (stateDir) => join12(stateDir, "pending-ticket-counters");
|
|
5224
|
+
function queueTicketCounters(stateDir, name, q) {
|
|
5225
|
+
mkdirSync6(countersDir(stateDir), { recursive: true });
|
|
5226
|
+
writeFileSync7(join12(countersDir(stateDir), `${name}.json`), JSON.stringify(q));
|
|
5227
|
+
}
|
|
5228
|
+
async function replayQueuedTicketCounters(stateDir, route, keyFor, log, now = /* @__PURE__ */ new Date()) {
|
|
5229
|
+
const dir = countersDir(stateDir);
|
|
5230
|
+
if (!existsSync11(dir)) return 0;
|
|
5231
|
+
let sent = 0;
|
|
5232
|
+
for (const f of readdirSync4(dir).filter((n) => n.endsWith(".json")).sort()) {
|
|
5233
|
+
const path = join12(dir, f);
|
|
5234
|
+
let q;
|
|
5235
|
+
try {
|
|
5236
|
+
q = JSON.parse(readFileSync11(path, "utf8"));
|
|
5237
|
+
} catch {
|
|
5238
|
+
continue;
|
|
5239
|
+
}
|
|
5240
|
+
if (q.version !== 1 || q.route !== route || now.toISOString() < q.nextAttemptAt) continue;
|
|
5241
|
+
try {
|
|
5242
|
+
const apiKey = await keyFor(q.role);
|
|
5243
|
+
if (!apiKey) continue;
|
|
5244
|
+
await writeCounters({ ...q.target, apiKey }, q.write);
|
|
5245
|
+
rmSync6(path, { force: true });
|
|
5246
|
+
sent += 1;
|
|
5247
|
+
log.emit(`added a queued run's spend to ${q.write.ticketReference ?? q.write.recordId} (${f})`);
|
|
5248
|
+
} catch (e) {
|
|
5249
|
+
if (!isRetryable(e)) {
|
|
5250
|
+
rmSync6(path, { force: true });
|
|
5251
|
+
log.warn(`dropped a queued ticket counter update (${f}): ${e.message}`);
|
|
5252
|
+
continue;
|
|
5253
|
+
}
|
|
5254
|
+
const attempts = q.attempts + 1;
|
|
5255
|
+
writeFileSync7(path, JSON.stringify({
|
|
5256
|
+
...q,
|
|
5257
|
+
attempts,
|
|
5258
|
+
warned: true,
|
|
5259
|
+
nextAttemptAt: new Date(now.getTime() + sleepBackoff(attempts)).toISOString()
|
|
5260
|
+
}));
|
|
5261
|
+
if (!q.warned) log.warn(`a queued ticket counter update could not be sent yet (${e.message}); will retry`);
|
|
5262
|
+
}
|
|
5263
|
+
}
|
|
5264
|
+
return sent;
|
|
5265
|
+
}
|
|
4968
5266
|
|
|
4969
5267
|
// src/ticket-attribution.ts
|
|
4970
5268
|
var TICKET_KEY_RE = /\b[A-Z][A-Z0-9]{1,9}-\d+\b/g;
|
|
@@ -5145,152 +5443,6 @@ import { basename as basename6 } from "path";
|
|
|
5145
5443
|
import { hostname } from "os";
|
|
5146
5444
|
import { TablationClient } from "@tablation/client";
|
|
5147
5445
|
|
|
5148
|
-
// src/contract.ts
|
|
5149
|
-
var DEFAULT_CONTRACT = {
|
|
5150
|
-
columns: {
|
|
5151
|
-
key: "issue_id",
|
|
5152
|
-
tag: "issue_tag",
|
|
5153
|
-
title: "title",
|
|
5154
|
-
status: "status",
|
|
5155
|
-
assignee: "assignee_id",
|
|
5156
|
-
priority: "priority",
|
|
5157
|
-
severity: "severity",
|
|
5158
|
-
slice: "project_id",
|
|
5159
|
-
repo: "repo_id",
|
|
5160
|
-
parent: "parent_id",
|
|
5161
|
-
blockedBy: "blocked_by",
|
|
5162
|
-
updatedAt: "updated_at",
|
|
5163
|
-
needsDesign: "needs_design",
|
|
5164
|
-
attachments: "attachments",
|
|
5165
|
-
workMode: "work_mode",
|
|
5166
|
-
reportType: "report_type",
|
|
5167
|
-
epic: "epic_id",
|
|
5168
|
-
author: "filed_by_id",
|
|
5169
|
-
ship: "ship_id",
|
|
5170
|
-
heldBy: "held_by_ship_id",
|
|
5171
|
-
resolutionNote: "resolution_note",
|
|
5172
|
-
verifiedSha: "verified_sha",
|
|
5173
|
-
prRef: "pr_ref",
|
|
5174
|
-
externalId: "external_id",
|
|
5175
|
-
position: "position"
|
|
5176
|
-
},
|
|
5177
|
-
comments: {
|
|
5178
|
-
parent: "ticket_id",
|
|
5179
|
-
body: "body",
|
|
5180
|
-
author: "team_member_id",
|
|
5181
|
-
createdAt: "created_at",
|
|
5182
|
-
ship: "ship_id",
|
|
5183
|
-
publicReply: "is_public_reply",
|
|
5184
|
-
authorName: "author_name"
|
|
5185
|
-
},
|
|
5186
|
-
statuses: {
|
|
5187
|
-
open: ["new", "accepted", "blocked", "in_progress", "needs_info", "fixed", "qa", "verified"],
|
|
5188
|
-
resolved: [
|
|
5189
|
-
"verified",
|
|
5190
|
-
"closed_deployed",
|
|
5191
|
-
"closed_wont_fix",
|
|
5192
|
-
"closed_duplicate",
|
|
5193
|
-
"closed_completed",
|
|
5194
|
-
"closed_cant_reproduce"
|
|
5195
|
-
],
|
|
5196
|
-
approved: "accepted",
|
|
5197
|
-
parked: "blocked",
|
|
5198
|
-
building: "in_progress",
|
|
5199
|
-
handoff: "fixed",
|
|
5200
|
-
verifying: "qa",
|
|
5201
|
-
verified: "verified",
|
|
5202
|
-
deployed: "closed_deployed",
|
|
5203
|
-
needsHuman: "needs_info",
|
|
5204
|
-
reviewing: null,
|
|
5205
|
-
wontFix: "closed_wont_fix",
|
|
5206
|
-
draft: "draft"
|
|
5207
|
-
},
|
|
5208
|
-
epics: {
|
|
5209
|
-
statusColumn: "status",
|
|
5210
|
-
keyColumn: "epic_id",
|
|
5211
|
-
statuses: { planned: "planned", building: "in_progress", done: "done", cancelled: "cancelled" }
|
|
5212
|
-
},
|
|
5213
|
-
priorityOrder: ["p0", "p1", "p2", "p3"],
|
|
5214
|
-
severityOrder: ["s1", "s2", "s3", "s4"],
|
|
5215
|
-
unknownPriorityRank: 2,
|
|
5216
|
-
coordinatingValue: "coordinating",
|
|
5217
|
-
manualWorkMode: "manual",
|
|
5218
|
-
hybridWorkMode: "hybrid",
|
|
5219
|
-
repoExemptReportTypes: ["coordinating", "question", "investigation"]
|
|
5220
|
-
};
|
|
5221
|
-
function closedStatuses(c) {
|
|
5222
|
-
return c.statuses.resolved.filter((s) => s !== c.statuses.verified);
|
|
5223
|
-
}
|
|
5224
|
-
function resolveContract(override) {
|
|
5225
|
-
if (!override) return DEFAULT_CONTRACT;
|
|
5226
|
-
return {
|
|
5227
|
-
columns: { ...DEFAULT_CONTRACT.columns, ...override.columns },
|
|
5228
|
-
comments: { ...DEFAULT_CONTRACT.comments, ...override.comments },
|
|
5229
|
-
statuses: { ...DEFAULT_CONTRACT.statuses, ...override.statuses },
|
|
5230
|
-
epics: {
|
|
5231
|
-
...DEFAULT_CONTRACT.epics,
|
|
5232
|
-
...override.epics,
|
|
5233
|
-
statuses: { ...DEFAULT_CONTRACT.epics.statuses, ...override.epics?.statuses }
|
|
5234
|
-
},
|
|
5235
|
-
priorityOrder: override.priorityOrder ?? DEFAULT_CONTRACT.priorityOrder,
|
|
5236
|
-
severityOrder: override.severityOrder ?? DEFAULT_CONTRACT.severityOrder,
|
|
5237
|
-
unknownPriorityRank: override.unknownPriorityRank ?? DEFAULT_CONTRACT.unknownPriorityRank,
|
|
5238
|
-
coordinatingValue: override.coordinatingValue ?? DEFAULT_CONTRACT.coordinatingValue,
|
|
5239
|
-
manualWorkMode: override.manualWorkMode ?? DEFAULT_CONTRACT.manualWorkMode,
|
|
5240
|
-
hybridWorkMode: override.hybridWorkMode ?? DEFAULT_CONTRACT.hybridWorkMode,
|
|
5241
|
-
// A workspace overriding `coordinatingValue` alone (renaming the rollup
|
|
5242
|
-
// marker) still gets that renamed value exempted here by default,
|
|
5243
|
-
// without also having to repeat it — only an explicit
|
|
5244
|
-
// `repoExemptReportTypes` overrides this derived default.
|
|
5245
|
-
repoExemptReportTypes: override.repoExemptReportTypes ?? (override.coordinatingValue ? [override.coordinatingValue, "question", "investigation"] : DEFAULT_CONTRACT.repoExemptReportTypes)
|
|
5246
|
-
};
|
|
5247
|
-
}
|
|
5248
|
-
function validateContract(c) {
|
|
5249
|
-
const problems = [];
|
|
5250
|
-
const open = new Set(c.statuses.open);
|
|
5251
|
-
const resolved = new Set(c.statuses.resolved);
|
|
5252
|
-
for (const role of ["approved", "parked", "building", "handoff", "verifying", "needsHuman"]) {
|
|
5253
|
-
const v = c.statuses[role];
|
|
5254
|
-
if (!open.has(v)) problems.push(`statuses.${role} ("${v}") is not listed in statuses.open`);
|
|
5255
|
-
}
|
|
5256
|
-
if (c.statuses.reviewing && !open.has(c.statuses.reviewing)) {
|
|
5257
|
-
problems.push(`statuses.reviewing ("${c.statuses.reviewing}") is not listed in statuses.open`);
|
|
5258
|
-
}
|
|
5259
|
-
if (!resolved.has(c.statuses.deployed)) {
|
|
5260
|
-
problems.push(`statuses.deployed ("${c.statuses.deployed}") is not listed in statuses.resolved`);
|
|
5261
|
-
}
|
|
5262
|
-
if (resolved.has(c.statuses.handoff)) {
|
|
5263
|
-
problems.push(
|
|
5264
|
-
`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`
|
|
5265
|
-
);
|
|
5266
|
-
}
|
|
5267
|
-
if (!resolved.has(c.statuses.wontFix)) {
|
|
5268
|
-
problems.push(`statuses.wontFix ("${c.statuses.wontFix}") is not listed in statuses.resolved`);
|
|
5269
|
-
}
|
|
5270
|
-
if (c.statuses.reviewing && resolved.has(c.statuses.reviewing)) {
|
|
5271
|
-
problems.push(
|
|
5272
|
-
`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`
|
|
5273
|
-
);
|
|
5274
|
-
}
|
|
5275
|
-
if (resolved.has(c.statuses.approved)) {
|
|
5276
|
-
problems.push(`statuses.approved ("${c.statuses.approved}") is listed as resolved`);
|
|
5277
|
-
}
|
|
5278
|
-
if (c.statuses.parked === c.statuses.approved) {
|
|
5279
|
-
problems.push("statuses.parked and statuses.approved are the same value \u2014 parking would be a no-op");
|
|
5280
|
-
}
|
|
5281
|
-
const epicValues = Object.values(c.epics.statuses);
|
|
5282
|
-
if (new Set(epicValues).size !== epicValues.length) {
|
|
5283
|
-
problems.push("epics.statuses names the same value twice \u2014 the epic sync could not tell those states apart");
|
|
5284
|
-
}
|
|
5285
|
-
if (c.priorityOrder.length === 0) problems.push("priorityOrder is empty");
|
|
5286
|
-
if (c.severityOrder.length === 0) problems.push("severityOrder is empty");
|
|
5287
|
-
for (const [name, order] of [["priorityOrder", c.priorityOrder], ["severityOrder", c.severityOrder]]) {
|
|
5288
|
-
if (new Set(order).size !== order.length) problems.push(`${name} contains duplicates`);
|
|
5289
|
-
}
|
|
5290
|
-
return problems;
|
|
5291
|
-
}
|
|
5292
|
-
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.";
|
|
5293
|
-
|
|
5294
5446
|
// src/state.ts
|
|
5295
5447
|
import { readFileSync as readFileSync12, writeFileSync as writeFileSync8, existsSync as existsSync12, mkdirSync as mkdirSync7, rmSync as rmSync7, statSync as statSync4 } from "fs";
|
|
5296
5448
|
import { join as join13 } from "path";
|
|
@@ -8144,12 +8296,26 @@ function planAgentRun(o) {
|
|
|
8144
8296
|
// resolved yet has nothing to authenticate the MCP connection with either.
|
|
8145
8297
|
mcp: o.apiKey ? { url: `${o.route.baseUrl}/api/mcp`, keyVar: API_KEY_VAR } : void 0
|
|
8146
8298
|
});
|
|
8299
|
+
const columns = resolveContract(o.route.contract).columns;
|
|
8147
8300
|
const agentLog = o.route.resolved && o.apiKey ? {
|
|
8148
8301
|
baseUrl: o.route.baseUrl,
|
|
8149
8302
|
workspaceId: o.route.resolved.workspaceId,
|
|
8150
8303
|
apiKey: o.apiKey,
|
|
8151
8304
|
userAgent: o.ship.userAgent,
|
|
8152
|
-
agentId: o.resolvedAgentId
|
|
8305
|
+
agentId: o.resolvedAgentId,
|
|
8306
|
+
...o.route.resolved.models?.issues ? { tickets: {
|
|
8307
|
+
issuesModelId: o.route.resolved.models.issues,
|
|
8308
|
+
keyColumn: columns.key,
|
|
8309
|
+
tagColumn: columns.tag,
|
|
8310
|
+
counters: {
|
|
8311
|
+
agentRuns: columns.agentRuns,
|
|
8312
|
+
tokensIn: columns.tokensIn,
|
|
8313
|
+
tokensOut: columns.tokensOut,
|
|
8314
|
+
cacheReadTokens: columns.cacheReadTokens,
|
|
8315
|
+
cacheWriteTokens: columns.cacheWriteTokens,
|
|
8316
|
+
costUsd: columns.costUsd
|
|
8317
|
+
}
|
|
8318
|
+
} } : {}
|
|
8153
8319
|
} : void 0;
|
|
8154
8320
|
return {
|
|
8155
8321
|
role: o.role,
|
|
@@ -8519,7 +8685,22 @@ async function finishRun(id, obs, emit, startedAt, end, agentLogKey) {
|
|
|
8519
8685
|
promptSha: id.promptSha
|
|
8520
8686
|
};
|
|
8521
8687
|
try {
|
|
8522
|
-
await reportAgentRun({ ...id.agentLog, apiKey: agentLogKey }, report);
|
|
8688
|
+
const sent = await reportAgentRun({ ...id.agentLog, apiKey: agentLogKey }, report);
|
|
8689
|
+
if (sent.counterWarning) emit.warn(sent.counterWarning, { step: "agent" });
|
|
8690
|
+
if (sent.counterRetry) {
|
|
8691
|
+
try {
|
|
8692
|
+
queueTicketCounters(stateDirOfStream(id.streamPath), basename7(id.streamPath, ".jsonl"), {
|
|
8693
|
+
version: 1,
|
|
8694
|
+
route: id.route,
|
|
8695
|
+
role: id.role,
|
|
8696
|
+
target: id.agentLog,
|
|
8697
|
+
write: sent.counterRetry,
|
|
8698
|
+
attempts: 0,
|
|
8699
|
+
nextAttemptAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
8700
|
+
});
|
|
8701
|
+
} catch {
|
|
8702
|
+
}
|
|
8703
|
+
}
|
|
8523
8704
|
} catch (e) {
|
|
8524
8705
|
if (e instanceof AgentLogUnreachableError) {
|
|
8525
8706
|
try {
|
|
@@ -8860,10 +9041,10 @@ function doctorSelftestLines(stored, toolchains) {
|
|
|
8860
9041
|
}
|
|
8861
9042
|
|
|
8862
9043
|
// src/cli.ts
|
|
8863
|
-
import { resolve as resolve5, dirname as dirname20, join as join37, basename as
|
|
9044
|
+
import { resolve as resolve5, dirname as dirname20, join as join37, basename as basename12 } from "path";
|
|
8864
9045
|
import { fileURLToPath } from "url";
|
|
8865
9046
|
import { hostname as hostname2, homedir as homedir10 } from "os";
|
|
8866
|
-
import { spawnSync as
|
|
9047
|
+
import { spawnSync as spawnSync7 } from "child_process";
|
|
8867
9048
|
import { parse as parseYaml2 } from "yaml";
|
|
8868
9049
|
|
|
8869
9050
|
// src/argv.ts
|
|
@@ -14212,12 +14393,107 @@ async function showLogEntry(route, entryId, opts = {}) {
|
|
|
14212
14393
|
};
|
|
14213
14394
|
}
|
|
14214
14395
|
|
|
14396
|
+
// src/repos-check.ts
|
|
14397
|
+
import { spawnSync as spawnSync6 } from "child_process";
|
|
14398
|
+
import { readFileSync as readFileSync24 } from "fs";
|
|
14399
|
+
import { basename as basename8 } from "path";
|
|
14400
|
+
var POSIX_SHELL = /^(?:ba|z|da|k|a|mk)?sh$|^fish$/;
|
|
14401
|
+
function checkHookSyntax(script, shell) {
|
|
14402
|
+
const spec = resolveShell(shell);
|
|
14403
|
+
const name = basename8(spec.bin).replace(/\.exe$/i, "").toLowerCase();
|
|
14404
|
+
let run2;
|
|
14405
|
+
if (POSIX_SHELL.test(name)) {
|
|
14406
|
+
run2 = spawnSync6(spec.bin, ["-n"], { input: script, encoding: "utf8" });
|
|
14407
|
+
} else if (/^(pwsh|powershell)$/.test(name)) {
|
|
14408
|
+
const parse3 = "$e=$null;[void][System.Management.Automation.Language.Parser]::ParseInput($env:CREW_CHECK_SCRIPT,[ref]$null,[ref]$e);if($e){$e|ForEach-Object{[Console]::Error.WriteLine($_.Message)};exit 1}";
|
|
14409
|
+
run2 = spawnSync6(spec.bin, ["-NoProfile", "-NonInteractive", "-Command", parse3], {
|
|
14410
|
+
encoding: "utf8",
|
|
14411
|
+
env: { ...process.env, CREW_CHECK_SCRIPT: script }
|
|
14412
|
+
});
|
|
14413
|
+
} else {
|
|
14414
|
+
return { status: "unchecked", message: `no syntax check for ${spec.bin}` };
|
|
14415
|
+
}
|
|
14416
|
+
if (run2.error) return { status: "unchecked", message: `cannot run ${spec.bin}: ${run2.error.message}` };
|
|
14417
|
+
if (run2.status === 0) return { status: "ok" };
|
|
14418
|
+
const message = `${run2.stderr ?? ""}${run2.stdout ?? ""}`.trim() || `${spec.bin} exited ${run2.status}`;
|
|
14419
|
+
return { status: "error", message };
|
|
14420
|
+
}
|
|
14421
|
+
function renderSamples(cfg, dir) {
|
|
14422
|
+
const checkout = basename8(dir.replace(/[/\\]+$/, ""));
|
|
14423
|
+
const tagged = { key: "ISSUE-123", title: "Fix the widget", role: "dev", tag: "TABL-123", prefix: "tabl", lowercase: cfg.branch.lowercase };
|
|
14424
|
+
const untagged = { key: "ISSUE-123", title: "Fix the widget", role: "dev", lowercase: cfg.branch.lowercase };
|
|
14425
|
+
return [["with a project tag", tagged], ["without a project tag", untagged]].map(([label, ctx]) => {
|
|
14426
|
+
const branch = renderBranchName(effectiveBranchTemplate(cfg, ctx.prefix), ctx);
|
|
14427
|
+
const worktree = effectiveWorktreeDirName(cfg, dir, branch, "123", (t) => renderWorktreeName(t, { ...ctx, dir: checkout }));
|
|
14428
|
+
return { case: label, branch, worktree };
|
|
14429
|
+
});
|
|
14430
|
+
}
|
|
14431
|
+
function checkRepoSpec(dir, opts = {}) {
|
|
14432
|
+
const empty = { parsed: null, effective: null, readiness: [], hooks: [], shadowed: [], samples: [] };
|
|
14433
|
+
const file = findRepoConfig(dir);
|
|
14434
|
+
if (!file) {
|
|
14435
|
+
return { ...empty, file: null, error: `no .crew.yaml, .crew.yml or .crew.json in ${dir}`, exitCode: 1 };
|
|
14436
|
+
}
|
|
14437
|
+
let parsed;
|
|
14438
|
+
try {
|
|
14439
|
+
parsed = parseRepoConfig(readFileSync24(file, "utf8"), file);
|
|
14440
|
+
} catch (e) {
|
|
14441
|
+
if (!(e instanceof RepoConfigError)) throw e;
|
|
14442
|
+
return { ...empty, file, error: e.message, exitCode: 1 };
|
|
14443
|
+
}
|
|
14444
|
+
const effective = resolveRepoConfig(parsed, opts.ship, dir);
|
|
14445
|
+
const readiness = validateEffective(effective);
|
|
14446
|
+
const hooks = Object.keys(effective.hooks).map((name) => ({
|
|
14447
|
+
name,
|
|
14448
|
+
label: hookLabel(effective, name),
|
|
14449
|
+
syntax: checkHookSyntax(effective.hooks[name], effective.shell)
|
|
14450
|
+
}));
|
|
14451
|
+
const failed = hooks.some((h) => h.syntax.status === "error");
|
|
14452
|
+
return {
|
|
14453
|
+
file,
|
|
14454
|
+
parsed,
|
|
14455
|
+
effective,
|
|
14456
|
+
readiness,
|
|
14457
|
+
hooks,
|
|
14458
|
+
shadowed: effective.shadowed,
|
|
14459
|
+
samples: renderSamples(effective, dir),
|
|
14460
|
+
error: null,
|
|
14461
|
+
exitCode: failed || opts.strict && readiness.length > 0 ? 1 : 0
|
|
14462
|
+
};
|
|
14463
|
+
}
|
|
14464
|
+
function renderRepoCheck(r) {
|
|
14465
|
+
if (r.error) return `${r.file ?? "crew repos check"}: ${r.error}
|
|
14466
|
+
`;
|
|
14467
|
+
const e = r.effective;
|
|
14468
|
+
const lines = [
|
|
14469
|
+
`${r.file}`,
|
|
14470
|
+
` platform ${e.platform}`,
|
|
14471
|
+
` release.mode ${e.release.mode}`,
|
|
14472
|
+
` release.versioning ${e.release.versioning}`,
|
|
14473
|
+
` branch.base ${e.branch.base}`,
|
|
14474
|
+
` branch.name ${e.branch.name}`,
|
|
14475
|
+
` branch.push ${e.branch.push}`
|
|
14476
|
+
];
|
|
14477
|
+
for (const s of r.samples) lines.push(` ${s.case}: branch ${s.branch}, worktree ../${s.worktree}`);
|
|
14478
|
+
lines.push(r.hooks.length ? " hooks" : " hooks none");
|
|
14479
|
+
for (const h of r.hooks) {
|
|
14480
|
+
const named = h.label !== e.hooks[h.name] ? ` (${h.label})` : "";
|
|
14481
|
+
const syntax = h.syntax.status === "ok" ? "syntax ok" : h.syntax.status === "unchecked" ? `syntax not checked: ${h.syntax.message}` : "syntax ERROR";
|
|
14482
|
+
lines.push(` ${h.name}${named}: ${syntax}`);
|
|
14483
|
+
if (h.syntax.status === "error") lines.push(...(h.syntax.message ?? "").split("\n").map((l) => ` ${l}`));
|
|
14484
|
+
}
|
|
14485
|
+
if (r.shadowed.length) lines.push(` this ship declares ${r.shadowed.join(", ")}, and the file overrides it \u2014 the file wins`);
|
|
14486
|
+
for (const p of r.readiness) lines.push(` - release readiness: ${p}`);
|
|
14487
|
+
return `${lines.join("\n")}
|
|
14488
|
+
`;
|
|
14489
|
+
}
|
|
14490
|
+
|
|
14215
14491
|
// src/repos-hooks.ts
|
|
14216
14492
|
import { isMap as isMap2, parseDocument as parseDocument3, YAMLMap } from "yaml";
|
|
14217
14493
|
|
|
14218
14494
|
// src/repos-cmd.ts
|
|
14219
14495
|
import { homedir as homedir9 } from "os";
|
|
14220
|
-
import { basename as
|
|
14496
|
+
import { basename as basename9, join as join28, resolve as resolve3 } from "path";
|
|
14221
14497
|
import { isMap, isSeq, parseDocument as parseDocument2, stringify as stringify2 } from "yaml";
|
|
14222
14498
|
var ReposError = class extends Error {
|
|
14223
14499
|
};
|
|
@@ -14289,7 +14565,7 @@ function configuredRepos(text, route, base) {
|
|
|
14289
14565
|
});
|
|
14290
14566
|
}
|
|
14291
14567
|
const dir = m.get("dir");
|
|
14292
|
-
return dir ? [{ name:
|
|
14568
|
+
return dir ? [{ name: basename9(String(dir)), dir: expandDir(String(dir), base) }] : [];
|
|
14293
14569
|
}
|
|
14294
14570
|
function cloneBaseOf(text, route, base, isCheckout) {
|
|
14295
14571
|
const m = routeMapOf(parseDocument2(text), route);
|
|
@@ -14378,7 +14654,7 @@ function planRepoAdd(i) {
|
|
|
14378
14654
|
throw new ReposError(`${target} is already configured on route ${i.route}`);
|
|
14379
14655
|
}
|
|
14380
14656
|
const matched = matchRepoName(i.origin, i.resolved);
|
|
14381
|
-
const name = matched ?? i.name ?? normalizeRemote(i.origin)?.split("/").pop() ??
|
|
14657
|
+
const name = matched ?? i.name ?? normalizeRemote(i.origin)?.split("/").pop() ?? basename9(target);
|
|
14382
14658
|
let warning;
|
|
14383
14659
|
if (!matched) {
|
|
14384
14660
|
const known = Object.values(i.resolved?.repoNames ?? {});
|
|
@@ -14894,7 +15170,7 @@ ${run2.err}` });
|
|
|
14894
15170
|
}
|
|
14895
15171
|
|
|
14896
15172
|
// src/sample-ticket.ts
|
|
14897
|
-
import { basename as
|
|
15173
|
+
import { basename as basename10, dirname as dirname17, join as join29 } from "path";
|
|
14898
15174
|
var SAMPLE_REPO = "crew-sample";
|
|
14899
15175
|
var SAMPLE_REMOTE = `local/${SAMPLE_REPO}`;
|
|
14900
15176
|
var SAMPLE_MARKER = "sample: true";
|
|
@@ -15010,8 +15286,8 @@ function schedulerInstalled(plan, exists, crontab) {
|
|
|
15010
15286
|
}
|
|
15011
15287
|
|
|
15012
15288
|
// src/worktree-sweep.ts
|
|
15013
|
-
import { existsSync as existsSync27, readFileSync as
|
|
15014
|
-
import { basename as
|
|
15289
|
+
import { existsSync as existsSync27, readFileSync as readFileSync25, readdirSync as readdirSync8 } from "fs";
|
|
15290
|
+
import { basename as basename11, dirname as dirname18, join as join30, resolve as resolve4 } from "path";
|
|
15015
15291
|
function planWorktreeSweep(target, tickets, contract) {
|
|
15016
15292
|
const terminal = new Set(contract.statuses.resolved.filter((s) => s !== contract.statuses.verified));
|
|
15017
15293
|
const verified = contract.statuses.verified;
|
|
@@ -15042,7 +15318,7 @@ function ticketForWorktree(w, byNumber, contract) {
|
|
|
15042
15318
|
const t = num ? byNumber.get(num) : void 0;
|
|
15043
15319
|
if (t && ticketStems(t).has(stem)) return t;
|
|
15044
15320
|
}
|
|
15045
|
-
const dir =
|
|
15321
|
+
const dir = basename11(w.path);
|
|
15046
15322
|
const m = DIR_NUMBER.exec(dir);
|
|
15047
15323
|
if (m && byNumber.has(m[1])) return byNumber.get(m[1]);
|
|
15048
15324
|
if (!w.branch) {
|
|
@@ -15058,7 +15334,7 @@ function worktreeTicketNumbers(target) {
|
|
|
15058
15334
|
const n = /-(\d+)$/.exec(BRANCH_STEM.exec(w.branch)?.[1] ?? "")?.[1];
|
|
15059
15335
|
if (n) nums.add(n);
|
|
15060
15336
|
}
|
|
15061
|
-
const m = DIR_NUMBER_SUFFIXED.exec(
|
|
15337
|
+
const m = DIR_NUMBER_SUFFIXED.exec(basename11(w.path));
|
|
15062
15338
|
if (m) nums.add(m[1]);
|
|
15063
15339
|
}
|
|
15064
15340
|
for (const d of siblingWorktreeDirs(target)) nums.add(d.number);
|
|
@@ -15066,7 +15342,7 @@ function worktreeTicketNumbers(target) {
|
|
|
15066
15342
|
}
|
|
15067
15343
|
function siblingWorktreeDirs(target) {
|
|
15068
15344
|
const parent = dirname18(target.dir.replace(/[/\\]+$/, ""));
|
|
15069
|
-
const stem = `${
|
|
15345
|
+
const stem = `${basename11(target.dir.replace(/[/\\]+$/, ""))}-`;
|
|
15070
15346
|
const registered = new Set(worktrees(target.dir).map((w) => resolve4(w.path)));
|
|
15071
15347
|
let names;
|
|
15072
15348
|
try {
|
|
@@ -15091,7 +15367,7 @@ function orphanedWorktreeDirs(target, tickets, contract) {
|
|
|
15091
15367
|
}
|
|
15092
15368
|
function warnOrphanedWorktreeDirs(paths, log, held) {
|
|
15093
15369
|
for (const p of paths) {
|
|
15094
|
-
const number = DIR_NUMBER_SUFFIXED.exec(
|
|
15370
|
+
const number = DIR_NUMBER_SUFFIXED.exec(basename11(p))?.[1];
|
|
15095
15371
|
const when = held && number ? noteHeldWorktree(held.state, { repo: held.repo, path: p, ticket: number, status: "deployed", reason: "orphaned directory" }) : "first";
|
|
15096
15372
|
if (when) {
|
|
15097
15373
|
log.warn(`orphaned directory ${p}: its ticket is deployed but it is not a registered worktree; the sweep will not delete it \u2014 remove it by hand`, { step: "worktree" });
|
|
@@ -15181,7 +15457,7 @@ function isHandBackMerge(path, base) {
|
|
|
15181
15457
|
if (!msgPath) return false;
|
|
15182
15458
|
let first;
|
|
15183
15459
|
try {
|
|
15184
|
-
first =
|
|
15460
|
+
first = readFileSync25(resolve4(path, msgPath), "utf8").split("\n")[0] ?? "";
|
|
15185
15461
|
} catch {
|
|
15186
15462
|
return false;
|
|
15187
15463
|
}
|
|
@@ -15235,7 +15511,7 @@ function isTicketBranch(name, base, stems) {
|
|
|
15235
15511
|
}
|
|
15236
15512
|
function sweepCandidatesExist(cwd, base) {
|
|
15237
15513
|
const linked = worktrees(cwd).slice(1);
|
|
15238
|
-
if (linked.some((w) => DIR_NUMBER_SUFFIXED.test(
|
|
15514
|
+
if (linked.some((w) => DIR_NUMBER_SUFFIXED.test(basename11(w.path)) || w.branch && BRANCH_STEM.test(w.branch))) return true;
|
|
15239
15515
|
if (siblingWorktreeDirs({ name: "", dir: cwd }).length) return true;
|
|
15240
15516
|
return localBranches(cwd).some((b) => b !== base && /-\d+(?:-.*)?$/.test(b));
|
|
15241
15517
|
}
|
|
@@ -15331,13 +15607,13 @@ function planEventsSweep(stateDir, retentionDays, now = Date.now()) {
|
|
|
15331
15607
|
}
|
|
15332
15608
|
|
|
15333
15609
|
// src/cli.ts
|
|
15334
|
-
import { realpathSync as realpathSync3, readFileSync as
|
|
15610
|
+
import { realpathSync as realpathSync3, readFileSync as readFileSync31, existsSync as existsSync34, mkdirSync as mkdirSync22, writeFileSync as writeFileSync23, rmSync as rmSync15, unlinkSync as unlinkSync5, copyFileSync as copyFileSync2, renameSync as renameSync3, statSync as statSync10, readdirSync as readdirSync14 } from "fs";
|
|
15335
15611
|
import { createInterface } from "readline/promises";
|
|
15336
15612
|
import { dirname as dirOf, resolve as resolvePath } from "path";
|
|
15337
15613
|
|
|
15338
15614
|
// src/ssh-keys.ts
|
|
15339
15615
|
import { execFileSync as execFileSync6 } from "child_process";
|
|
15340
|
-
import { existsSync as existsSync29, mkdirSync as mkdirSync18, readFileSync as
|
|
15616
|
+
import { existsSync as existsSync29, mkdirSync as mkdirSync18, readFileSync as readFileSync26, chmodSync } from "fs";
|
|
15341
15617
|
import { dirname as dirname19, join as join32 } from "path";
|
|
15342
15618
|
var SshKeyError = class extends Error {
|
|
15343
15619
|
};
|
|
@@ -15371,7 +15647,7 @@ function ensureShipSshKeypair(stateDir) {
|
|
|
15371
15647
|
if (!existsSync29(publicKeyPath)) {
|
|
15372
15648
|
throw new SshKeyError(`${privateKeyPath} exists but ${publicKeyPath} does not \u2014 regenerate or restore it`);
|
|
15373
15649
|
}
|
|
15374
|
-
const publicKey =
|
|
15650
|
+
const publicKey = readFileSync26(publicKeyPath, "utf8").trim();
|
|
15375
15651
|
return { privateKeyPath, publicKeyPath, publicKey };
|
|
15376
15652
|
}
|
|
15377
15653
|
function sshKeygenAvailable() {
|
|
@@ -15588,7 +15864,7 @@ function syncAllPassengerCheckouts(cfg) {
|
|
|
15588
15864
|
|
|
15589
15865
|
// src/tunnel.ts
|
|
15590
15866
|
import { spawn as spawn5 } from "child_process";
|
|
15591
|
-
import { existsSync as existsSync30, mkdirSync as mkdirSync19, readFileSync as
|
|
15867
|
+
import { existsSync as existsSync30, mkdirSync as mkdirSync19, readFileSync as readFileSync27, writeFileSync as writeFileSync20, unlinkSync as unlinkSync3, readdirSync as readdirSync10 } from "fs";
|
|
15592
15868
|
import { randomInt } from "crypto";
|
|
15593
15869
|
import { join as join33 } from "path";
|
|
15594
15870
|
var DEFAULT_SPAWN_OPTIONS = { stdio: "ignore", detached: true };
|
|
@@ -15663,7 +15939,7 @@ function readPersistedTunnel(stateDir, workspaceId) {
|
|
|
15663
15939
|
const path = tunnelStatePath(stateDir, workspaceId);
|
|
15664
15940
|
if (!existsSync30(path)) return void 0;
|
|
15665
15941
|
try {
|
|
15666
|
-
return JSON.parse(
|
|
15942
|
+
return JSON.parse(readFileSync27(path, "utf8"));
|
|
15667
15943
|
} catch {
|
|
15668
15944
|
return void 0;
|
|
15669
15945
|
}
|
|
@@ -15890,7 +16166,7 @@ async function unregisterEndpoint(workspaceId, info, d) {
|
|
|
15890
16166
|
import { createServer as createHttpServer } from "http";
|
|
15891
16167
|
import { spawn as spawn6 } from "child_process";
|
|
15892
16168
|
import { randomBytes } from "crypto";
|
|
15893
|
-
import { existsSync as existsSync31, mkdirSync as mkdirSync20, readFileSync as
|
|
16169
|
+
import { existsSync as existsSync31, mkdirSync as mkdirSync20, readFileSync as readFileSync28, writeFileSync as writeFileSync21, unlinkSync as unlinkSync4, readdirSync as readdirSync11 } from "fs";
|
|
15894
16170
|
import { join as join34 } from "path";
|
|
15895
16171
|
|
|
15896
16172
|
// src/secret-compare.ts
|
|
@@ -15986,7 +16262,7 @@ function readPersistedSyncDaemon(stateDir, workspaceId) {
|
|
|
15986
16262
|
const path = syncDaemonStatePath(stateDir, workspaceId);
|
|
15987
16263
|
if (!existsSync31(path)) return void 0;
|
|
15988
16264
|
try {
|
|
15989
|
-
return JSON.parse(
|
|
16265
|
+
return JSON.parse(readFileSync28(path, "utf8"));
|
|
15990
16266
|
} catch {
|
|
15991
16267
|
return void 0;
|
|
15992
16268
|
}
|
|
@@ -16103,11 +16379,11 @@ function describeCause(err) {
|
|
|
16103
16379
|
}
|
|
16104
16380
|
|
|
16105
16381
|
// src/daemon.ts
|
|
16106
|
-
import { existsSync as existsSync33, mkdirSync as mkdirSync21, readFileSync as
|
|
16382
|
+
import { existsSync as existsSync33, mkdirSync as mkdirSync21, readFileSync as readFileSync30, readdirSync as readdirSync13, rmSync as rmSync14, statSync as statSync9, writeFileSync as writeFileSync22 } from "fs";
|
|
16107
16383
|
import { join as join36 } from "path";
|
|
16108
16384
|
|
|
16109
16385
|
// src/interrupted.ts
|
|
16110
|
-
import { existsSync as existsSync32, readdirSync as readdirSync12, readFileSync as
|
|
16386
|
+
import { existsSync as existsSync32, readdirSync as readdirSync12, readFileSync as readFileSync29 } from "fs";
|
|
16111
16387
|
import { join as join35 } from "path";
|
|
16112
16388
|
var INTERRUPTED_MARKER = calloutHeader("NOTE", "Interrupted");
|
|
16113
16389
|
function interruptedComment(b) {
|
|
@@ -16129,7 +16405,7 @@ function runFinished(streamPath) {
|
|
|
16129
16405
|
const path = `${streamPath}.meta.json`;
|
|
16130
16406
|
if (!existsSync32(path)) return false;
|
|
16131
16407
|
try {
|
|
16132
|
-
return JSON.parse(
|
|
16408
|
+
return JSON.parse(readFileSync29(path, "utf8")).running !== true;
|
|
16133
16409
|
} catch {
|
|
16134
16410
|
return true;
|
|
16135
16411
|
}
|
|
@@ -16286,7 +16562,7 @@ function noteDaemonStarted(stateDir, now = Date.now()) {
|
|
|
16286
16562
|
}
|
|
16287
16563
|
function readDaemonStarted(stateDir) {
|
|
16288
16564
|
try {
|
|
16289
|
-
const v = JSON.parse(
|
|
16565
|
+
const v = JSON.parse(readFileSync30(daemonStampFor(stateDir), "utf8")).startedAt;
|
|
16290
16566
|
return typeof v === "number" ? v : null;
|
|
16291
16567
|
} catch {
|
|
16292
16568
|
return null;
|
|
@@ -16467,6 +16743,12 @@ async function replayPendingWrites(ctx) {
|
|
|
16467
16743
|
async (role) => (await resolveSeatKey(route, role, ctx.getSessionStore)).apiKey,
|
|
16468
16744
|
emit
|
|
16469
16745
|
);
|
|
16746
|
+
await replayQueuedTicketCounters(
|
|
16747
|
+
ship.stateDir,
|
|
16748
|
+
routeSlug(route.route),
|
|
16749
|
+
async (role) => (await resolveSeatKey(route, role, ctx.getSessionStore)).apiKey,
|
|
16750
|
+
emit
|
|
16751
|
+
);
|
|
16470
16752
|
} catch (e) {
|
|
16471
16753
|
emit.warn(`could not replay pending writes: ${e.message}`, { step: "agent" });
|
|
16472
16754
|
}
|
|
@@ -16831,6 +17113,7 @@ function usage(problem, requested = false) {
|
|
|
16831
17113
|
crew skills sync [route] push only crew's skill files (e.g. grill-me) into the workspace Agent Skills table
|
|
16832
17114
|
crew ship rename NAME [--json] rename this ship in crew.yaml and on every workspace's Ships row
|
|
16833
17115
|
crew repos add [ROUTE] [NAME] [PATH] attach a local checkout to a route in crew.yaml (--dry-run previews)
|
|
17116
|
+
crew repos check [PATH] validate a checkout's .crew.yaml without attaching it (--route ROUTE, --strict, --json)
|
|
16834
17117
|
crew repos clone [ROUTE] NAME clone a Repos row's remote into the route directory and attach it (--dir PATH, --dry-run, --json)
|
|
16835
17118
|
crew repos hooks [ROUTE] NAME suggest test/build hooks for a checkout (--agent, --write route|repo, --recipes, --json)
|
|
16836
17119
|
crew repos list ROUTE the checkouts a route has, with the tracker's Repos row each matches (--json: every Repos row too)
|
|
@@ -16929,6 +17212,22 @@ var KNOWN_COMMANDS = /* @__PURE__ */ new Set([
|
|
|
16929
17212
|
"worktree"
|
|
16930
17213
|
]);
|
|
16931
17214
|
if (!KNOWN_COMMANDS.has(command)) usage(`unknown command "${command}"`);
|
|
17215
|
+
function runReposCheck(dir, ship) {
|
|
17216
|
+
const report = checkRepoSpec(dir, { ship, strict: flag("strict") });
|
|
17217
|
+
if (flag("json")) {
|
|
17218
|
+
const { exitCode: _exit, ...rest } = report;
|
|
17219
|
+
process.stdout.write(`${JSON.stringify(rest)}
|
|
17220
|
+
`);
|
|
17221
|
+
} else {
|
|
17222
|
+
process.stdout.write(renderRepoCheck(report));
|
|
17223
|
+
}
|
|
17224
|
+
process.exitCode = report.exitCode;
|
|
17225
|
+
}
|
|
17226
|
+
var reposCheckBare = command === "repos" && positional[1] === "check" && !value("route") && positional.length < 4;
|
|
17227
|
+
if (reposCheckBare) {
|
|
17228
|
+
runReposCheck(resolvePath(positional[2] ?? "."));
|
|
17229
|
+
process.exit(process.exitCode ?? 0);
|
|
17230
|
+
}
|
|
16932
17231
|
function findSyncRoute(c, name) {
|
|
16933
17232
|
if (!name && c.routes.length > 1) {
|
|
16934
17233
|
throw new ConfigError(`this ship has ${c.routes.length} routes \u2014 name one: ${c.routes.map((r) => r.route).join(", ")}`);
|
|
@@ -17315,7 +17614,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
17315
17614
|
`
|
|
17316
17615
|
);
|
|
17317
17616
|
if (!dryRun) {
|
|
17318
|
-
|
|
17617
|
+
spawnSync7("systemctl", ["--user", "disable", "--now", `${label}.timer`], { cwd: CREW_HOME });
|
|
17319
17618
|
try {
|
|
17320
17619
|
unlinkSync5(staleTimer);
|
|
17321
17620
|
} catch {
|
|
@@ -17355,7 +17654,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
17355
17654
|
async function doctorFix(o) {
|
|
17356
17655
|
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))));
|
|
17357
17656
|
const unitExists = (p) => existsSync34(p);
|
|
17358
|
-
const crontab = o.host === "linux" ?
|
|
17657
|
+
const crontab = o.host === "linux" ? spawnSync7("crontab", ["-l"], { encoding: "utf8" }).stdout ?? "" : "";
|
|
17359
17658
|
const schedulerAbsent = o.host !== "windows" ? !schedulerInstalled(planInstall(cfg.ship, CREW_HOME, o.host, detectSystemd(), "run"), unitExists, crontab) : true;
|
|
17360
17659
|
const plan = planFix({ routes, schedulerAbsent, host: o.host });
|
|
17361
17660
|
for (const n of plan.notes) process.stdout.write(`fix: ${n}
|
|
@@ -17371,7 +17670,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
17371
17670
|
for (const a of plan.actions) {
|
|
17372
17671
|
if (a.kind === "enable") {
|
|
17373
17672
|
if (prompter && !await prompter.confirm(`Route ${a.route} passes every check \u2014 set enabled: true in ${cfg.configFile}?`, false)) continue;
|
|
17374
|
-
writeFileSync23(cfg.configFile, planEnable(
|
|
17673
|
+
writeFileSync23(cfg.configFile, planEnable(readFileSync31(cfg.configFile, "utf8"), a.route));
|
|
17375
17674
|
enabledSound.add(a.route);
|
|
17376
17675
|
changed2.push(`enabled route ${a.route} in ${cfg.configFile}`);
|
|
17377
17676
|
} else {
|
|
@@ -18372,7 +18671,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
18372
18671
|
}
|
|
18373
18672
|
let failed = false;
|
|
18374
18673
|
for (const cmd2 of commands) {
|
|
18375
|
-
const res2 =
|
|
18674
|
+
const res2 = spawnSync7(cmd2[0], cmd2.slice(1), { cwd: CREW_HOME, encoding: "utf8" });
|
|
18376
18675
|
if (res2.stdout) process.stdout.write(res2.stdout);
|
|
18377
18676
|
if (res2.stderr) process.stderr.write(res2.stderr);
|
|
18378
18677
|
if (cmd2 === control.startCommand && (res2.error || res2.status !== 0)) {
|
|
@@ -18390,7 +18689,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
18390
18689
|
`);
|
|
18391
18690
|
break;
|
|
18392
18691
|
}
|
|
18393
|
-
const res =
|
|
18692
|
+
const res = spawnSync7(cmd[0], cmd.slice(1), { cwd: CREW_HOME, encoding: "utf8" });
|
|
18394
18693
|
if (res.stdout) process.stdout.write(res.stdout);
|
|
18395
18694
|
if (res.stderr) process.stderr.write(res.stderr);
|
|
18396
18695
|
if (daemonSub !== "status" && (res.error || res.status !== 0)) {
|
|
@@ -18641,7 +18940,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
18641
18940
|
`);
|
|
18642
18941
|
if (!dryRun) {
|
|
18643
18942
|
const resolvedPath = resolvedPathFor(cfg.ship.stateDir, target.route);
|
|
18644
|
-
const raw = existsSync34(resolvedPath) ? JSON.parse(
|
|
18943
|
+
const raw = existsSync34(resolvedPath) ? JSON.parse(readFileSync31(resolvedPath, "utf8")) : {};
|
|
18645
18944
|
raw.agentSkills = result.agentSkills;
|
|
18646
18945
|
mkdirSync22(dirOf(resolvedPath), { recursive: true });
|
|
18647
18946
|
writeFileSync23(resolvedPath, `${JSON.stringify(raw, null, 2)}
|
|
@@ -18667,7 +18966,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
18667
18966
|
throw e;
|
|
18668
18967
|
}
|
|
18669
18968
|
const oldName = cfg.ship.name;
|
|
18670
|
-
writeFileSync23(cfg.configFile, setShipNameInYaml(
|
|
18969
|
+
writeFileSync23(cfg.configFile, setShipNameInYaml(readFileSync31(cfg.configFile, "utf8"), newName));
|
|
18671
18970
|
cfg.ship.name = newName;
|
|
18672
18971
|
let renameMachineId;
|
|
18673
18972
|
try {
|
|
@@ -18710,11 +19009,37 @@ ${changed2.map((c) => ` - ${c}
|
|
|
18710
19009
|
case "repos": {
|
|
18711
19010
|
const sub = positional[1];
|
|
18712
19011
|
const usage2 = () => {
|
|
18713
|
-
process.stderr.write("crew repos add [workspace/project] [name] [path] [--name N] [--project P] [--dry-run] [--no-create]\ncrew repos clone [workspace/project] <name> [--dir PATH] [--dry-run] [--json]\ncrew repos list <workspace/project> [--json]\ncrew repos hooks [workspace/project] <name> [--suggest] [--agent] [--write route|repo [--replace]] [--clear route] [--recipes ID,ID] [--hook KEY=CMD ...] [--json]\n");
|
|
19012
|
+
process.stderr.write("crew repos add [workspace/project] [name] [path] [--name N] [--project P] [--dry-run] [--no-create]\ncrew repos clone [workspace/project] <name> [--dir PATH] [--dry-run] [--json]\ncrew repos list <workspace/project> [--json]\ncrew repos check [PATH | --route ROUTE [PATH] | ROUTE NAME] [--strict] [--json]\ncrew repos hooks [workspace/project] <name> [--suggest] [--agent] [--write route|repo [--replace]] [--clear route] [--recipes ID,ID] [--hook KEY=CMD ...] [--json]\n");
|
|
18714
19013
|
process.exit(2);
|
|
18715
19014
|
};
|
|
18716
|
-
if (sub !== "add" && sub !== "list" && sub !== "clone" && sub !== "hooks") usage2();
|
|
18717
|
-
const configText =
|
|
19015
|
+
if (sub !== "add" && sub !== "list" && sub !== "clone" && sub !== "hooks" && sub !== "check") usage2();
|
|
19016
|
+
const configText = readFileSync31(cfg.configFile, "utf8");
|
|
19017
|
+
if (sub === "check") {
|
|
19018
|
+
try {
|
|
19019
|
+
const routeArg = value("route") ?? positional[2];
|
|
19020
|
+
const found = cfg.routes.find((r) => r.route === routeArg);
|
|
19021
|
+
if (!found) throw new ReposError(`no route "${routeArg}" in the config (have: ${cfg.routes.map((r) => r.route).join(", ") || "none"})`);
|
|
19022
|
+
const named2 = value("route") ? void 0 : positional[3];
|
|
19023
|
+
const dir = named2 ? repoDirOf(configText, dirname20(cfg.configFile), found.route, named2) : resolvePath(positional[value("route") ? 2 : 3] ?? ".");
|
|
19024
|
+
const target = reposOf(found).find((t) => resolvePath(t.dir) === dir || t.name === named2);
|
|
19025
|
+
const o = target ? found.repoOverrides[target.name] : void 0;
|
|
19026
|
+
runReposCheck(dir, {
|
|
19027
|
+
hooks: { ...found.hooks, ...o?.hooks },
|
|
19028
|
+
labels: { ...found.labels, ...o?.labels },
|
|
19029
|
+
release: mergeRouteRelease(found.release, o?.release),
|
|
19030
|
+
branch: { ...found.branch, ...o?.branch },
|
|
19031
|
+
worktrees: { prefix: shipWorktreePrefixFor(found), nameOverride: o?.worktreeName }
|
|
19032
|
+
});
|
|
19033
|
+
} catch (e) {
|
|
19034
|
+
if (e instanceof ReposError) {
|
|
19035
|
+
process.stderr.write(`crew repos: ${e.message}
|
|
19036
|
+
`);
|
|
19037
|
+
process.exit(2);
|
|
19038
|
+
}
|
|
19039
|
+
throw e;
|
|
19040
|
+
}
|
|
19041
|
+
break;
|
|
19042
|
+
}
|
|
18718
19043
|
if (sub === "hooks") {
|
|
18719
19044
|
try {
|
|
18720
19045
|
const a = parseHooksArgs(positional.slice(2), routeNamesOf(configText));
|
|
@@ -18728,7 +19053,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
18728
19053
|
}
|
|
18729
19054
|
}, read: (r) => {
|
|
18730
19055
|
try {
|
|
18731
|
-
return
|
|
19056
|
+
return readFileSync31(resolvePath(dir, r), "utf8");
|
|
18732
19057
|
} catch {
|
|
18733
19058
|
return void 0;
|
|
18734
19059
|
}
|
|
@@ -18869,7 +19194,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
18869
19194
|
let resolvedRemotes;
|
|
18870
19195
|
if (existsSync34(resolvedFile)) {
|
|
18871
19196
|
try {
|
|
18872
|
-
resolvedRemotes = JSON.parse(
|
|
19197
|
+
resolvedRemotes = JSON.parse(readFileSync31(resolvedFile, "utf8"));
|
|
18873
19198
|
} catch {
|
|
18874
19199
|
}
|
|
18875
19200
|
}
|
|
@@ -18890,7 +19215,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
18890
19215
|
isCheckout,
|
|
18891
19216
|
repoHasHooks: (d) => REPO_CONFIG_FILENAMES.some((f) => {
|
|
18892
19217
|
try {
|
|
18893
|
-
return Object.keys(parseRepoConfig(
|
|
19218
|
+
return Object.keys(parseRepoConfig(readFileSync31(resolvePath(d, f), "utf8"), f).hooks).length > 0;
|
|
18894
19219
|
} catch {
|
|
18895
19220
|
return false;
|
|
18896
19221
|
}
|
|
@@ -18999,7 +19324,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
18999
19324
|
process.stdout.write(`created Repos row ${String(rowPayload.name)} (${String(rowPayload.remote)}) ${created.id}
|
|
19000
19325
|
`);
|
|
19001
19326
|
if (existsSync34(resolvedFile)) {
|
|
19002
|
-
const raw = JSON.parse(
|
|
19327
|
+
const raw = JSON.parse(readFileSync31(resolvedFile, "utf8"));
|
|
19003
19328
|
raw.repoNames = { ...raw.repoNames, [created.id]: String(rowPayload.name) };
|
|
19004
19329
|
raw.repoRemotes = { ...raw.repoRemotes, [created.id]: String(rowPayload.remote) };
|
|
19005
19330
|
writeFileSync23(resolvedFile, `${JSON.stringify(raw, null, 2)}
|
|
@@ -19357,7 +19682,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
19357
19682
|
process.exit(2);
|
|
19358
19683
|
}
|
|
19359
19684
|
if (found.shipNameAdopted && !dryRun) {
|
|
19360
|
-
writeFileSync23(cfg.configFile, setShipNameInYaml(
|
|
19685
|
+
writeFileSync23(cfg.configFile, setShipNameInYaml(readFileSync31(cfg.configFile, "utf8"), found.shipNameAdopted));
|
|
19361
19686
|
cfg.ship.name = found.shipNameAdopted;
|
|
19362
19687
|
}
|
|
19363
19688
|
if (found.shipRow) {
|
|
@@ -19403,7 +19728,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
19403
19728
|
const hadPreviousFile = existsSync34(resolvedPath);
|
|
19404
19729
|
if (hadPreviousFile) {
|
|
19405
19730
|
try {
|
|
19406
|
-
previous = JSON.parse(
|
|
19731
|
+
previous = JSON.parse(readFileSync31(resolvedPath, "utf8"));
|
|
19407
19732
|
} catch {
|
|
19408
19733
|
}
|
|
19409
19734
|
}
|
|
@@ -20288,7 +20613,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
20288
20613
|
};
|
|
20289
20614
|
const droppedRoutes = [];
|
|
20290
20615
|
try {
|
|
20291
|
-
const rawCfg = parseYaml2(
|
|
20616
|
+
const rawCfg = parseYaml2(readFileSync31(cfg.configFile, "utf8"));
|
|
20292
20617
|
for (const r of rawCfg?.routes ?? []) {
|
|
20293
20618
|
if (typeof r?.route !== "string" || cfg.routes.some((x) => x.route === r.route)) continue;
|
|
20294
20619
|
try {
|
|
@@ -20324,7 +20649,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
20324
20649
|
}
|
|
20325
20650
|
const host = hostPlatform();
|
|
20326
20651
|
const control = host === "windows" ? null : planDaemonControl(CREW_HOME, host, detectSystemd());
|
|
20327
|
-
const unitStatus = control?.statusCommand ?
|
|
20652
|
+
const unitStatus = control?.statusCommand ? spawnSync7(control.statusCommand[0], control.statusCommand.slice(1), { cwd: CREW_HOME, encoding: "utf8" }) : null;
|
|
20328
20653
|
const unitRunning = unitStatus !== null && !unitStatus.error && unitStatus.status === 0;
|
|
20329
20654
|
if (!unitRunning) {
|
|
20330
20655
|
process.stdout.write("crew reload: no daemon unit is running \u2014 nothing to reload (the next run reads crew.yaml fresh)\n");
|
|
@@ -20336,7 +20661,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
20336
20661
|
process.stdout.write("crew reload: requested \u2014 the daemon finishes its in-flight pass, then relaunches with the new config\n");
|
|
20337
20662
|
if (flag("now") && control?.stopCommand && control.startCommand) {
|
|
20338
20663
|
for (const cmd of [control.stopCommand, control.startCommand]) {
|
|
20339
|
-
const res =
|
|
20664
|
+
const res = spawnSync7(cmd[0], cmd.slice(1), { cwd: CREW_HOME, encoding: "utf8" });
|
|
20340
20665
|
if (cmd === control.startCommand && (res.error || res.status !== 0)) {
|
|
20341
20666
|
process.stderr.write(`crew reload: ${cmd.join(" ")} exited ${res.status ?? "error"}
|
|
20342
20667
|
`);
|
|
@@ -20382,7 +20707,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
20382
20707
|
}
|
|
20383
20708
|
case "log": {
|
|
20384
20709
|
try {
|
|
20385
|
-
const text =
|
|
20710
|
+
const text = readFileSync31(cfg.ship.logFile, "utf8").split("\n");
|
|
20386
20711
|
process.stdout.write(`${text.slice(-(Number(value("tail")) || 40)).join("\n")}
|
|
20387
20712
|
`);
|
|
20388
20713
|
} catch {
|
|
@@ -20405,7 +20730,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
20405
20730
|
};
|
|
20406
20731
|
if (!route.resolved) fail(`route ${route.route} is not connected \u2014 run \`crew connect\` first`);
|
|
20407
20732
|
const tracker = new Tracker(route, cfg.ship);
|
|
20408
|
-
const configText =
|
|
20733
|
+
const configText = readFileSync31(cfg.configFile, "utf8");
|
|
20409
20734
|
const cfgBase = dirname20(cfg.configFile);
|
|
20410
20735
|
const configured = configuredRepos(configText, route.route, cfgBase);
|
|
20411
20736
|
const dir = configured.find((r) => r.name === SAMPLE_REPO)?.dir ?? sampleDir(configured.map((r) => r.dir), route.reposBasePath, route.route.split("/")[0]);
|
|
@@ -20443,14 +20768,14 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
20443
20768
|
if (row) {
|
|
20444
20769
|
await tracker.deleteRepoRow(row.id);
|
|
20445
20770
|
if (existsSync34(resolvedFile)) {
|
|
20446
|
-
const raw = JSON.parse(
|
|
20771
|
+
const raw = JSON.parse(readFileSync31(resolvedFile, "utf8"));
|
|
20447
20772
|
if (raw.repoNames) delete raw.repoNames[row.id];
|
|
20448
20773
|
if (raw.repoRemotes) delete raw.repoRemotes[row.id];
|
|
20449
20774
|
writeFileSync23(resolvedFile, `${JSON.stringify(raw, null, 2)}
|
|
20450
20775
|
`);
|
|
20451
20776
|
}
|
|
20452
20777
|
}
|
|
20453
|
-
if (existsSync34(join37(dir, ".git")) &&
|
|
20778
|
+
if (existsSync34(join37(dir, ".git")) && basename12(dir) === SAMPLE_REPO) rmSync15(dir, { recursive: true, force: true });
|
|
20454
20779
|
say("removed", { dir, closed }, `removed the sample repo${closed.length ? `, closed ${closed.join(", ")}` : ""}`);
|
|
20455
20780
|
break;
|
|
20456
20781
|
}
|
|
@@ -20466,7 +20791,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
20466
20791
|
writeFileSync23(join37(dir, "README.md"), SAMPLE_README);
|
|
20467
20792
|
writeFileSync23(join37(dir, ".crew.yaml"), SAMPLE_CREW_YAML);
|
|
20468
20793
|
const g = (...a) => {
|
|
20469
|
-
const r =
|
|
20794
|
+
const r = spawnSync7("git", a, { cwd: dir, encoding: "utf8" });
|
|
20470
20795
|
if (r.status !== 0) fail(`git ${a[0]} failed in ${dir}: ${(r.stderr || r.stdout).trim()}`);
|
|
20471
20796
|
};
|
|
20472
20797
|
g("init", "-b", "main");
|
|
@@ -20483,7 +20808,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
20483
20808
|
});
|
|
20484
20809
|
repoId = (await tracker.createRepoRow(payload)).id;
|
|
20485
20810
|
if (existsSync34(resolvedFile)) {
|
|
20486
|
-
const raw = JSON.parse(
|
|
20811
|
+
const raw = JSON.parse(readFileSync31(resolvedFile, "utf8"));
|
|
20487
20812
|
raw.repoNames = { ...raw.repoNames, [repoId]: SAMPLE_REPO };
|
|
20488
20813
|
raw.repoRemotes = { ...raw.repoRemotes, [repoId]: SAMPLE_REMOTE };
|
|
20489
20814
|
writeFileSync23(resolvedFile, `${JSON.stringify(raw, null, 2)}
|
|
@@ -20714,7 +21039,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
20714
21039
|
// `synthesis-tabl-123`. `worktreePrefix` above is kept for
|
|
20715
21040
|
// config-provenance display; this is the field that actually
|
|
20716
21041
|
// describes where a worktree lands by default.
|
|
20717
|
-
worktreeDirPattern: r.config.worktrees.name ?? (r.config.provenance["worktrees.prefix"] !== "default" ? `${r.config.worktrees.prefix}<number>` : `${
|
|
21042
|
+
worktreeDirPattern: r.config.worktrees.name ?? (r.config.provenance["worktrees.prefix"] !== "default" ? `${r.config.worktrees.prefix}<number>` : `${basename12(r.dir.replace(/[/\\]+$/, ""))}-<branch>`),
|
|
20718
21043
|
worktreeParent: resolvePath(r.dir, "..")
|
|
20719
21044
|
})),
|
|
20720
21045
|
waiting: waiting ? { ticket: waiting.ticket, since: waiting.since, streak: fairness.streak() } : null
|
|
@@ -20881,7 +21206,7 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
|
|
|
20881
21206
|
const unitPlan = planInstall(cfg.ship, CREW_HOME, host, detectSystemd(), "run");
|
|
20882
21207
|
const abandons = unitAbandonsAgents(unitPlan, (p) => {
|
|
20883
21208
|
try {
|
|
20884
|
-
return
|
|
21209
|
+
return readFileSync31(p, "utf8");
|
|
20885
21210
|
} catch {
|
|
20886
21211
|
return null;
|
|
20887
21212
|
}
|
|
@@ -20924,7 +21249,7 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
|
|
|
20924
21249
|
);
|
|
20925
21250
|
for (const r of await resolvedRepos(route)) {
|
|
20926
21251
|
const { config } = r;
|
|
20927
|
-
const worktreePattern = config.worktrees.name ?? (config.provenance["worktrees.prefix"] !== "default" ? `${config.worktrees.prefix}<number>` : `${
|
|
21252
|
+
const worktreePattern = config.worktrees.name ?? (config.provenance["worktrees.prefix"] !== "default" ? `${config.worktrees.prefix}<number>` : `${basename12(r.dir.replace(/[/\\]+$/, ""))}-${effectiveBranchTemplate(config, void 0)}`);
|
|
20928
21253
|
process.stdout.write(
|
|
20929
21254
|
`${`repo ${r.name}:`.padEnd(19)}${r.dir}
|
|
20930
21255
|
worktrees at ../${worktreePattern} (${config.provenance["worktrees.name"] ?? config.provenance["worktrees.prefix"] ?? "default"}), branch ${config.branch.name}
|
package/package.json
CHANGED