@catalyst-cloud/cli 0.16.25 → 0.16.27
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/CHANGELOG.md +8 -0
- package/README.md +33 -0
- package/dist/args.js +11 -4
- package/dist/capabilities.js +4 -0
- package/dist/cli.js +14 -2
- package/dist/hosts.js +1 -1
- package/dist/legacy.js +53 -12
- package/dist/local-sync-data.js +159 -0
- package/dist/machine-inventory.js +1 -0
- package/dist/onboard-runner-custody.js +38 -15
- package/dist/onboard-runner.js +4 -2
- package/dist/onboard-runtime.js +53 -14
- package/dist/onboard-ui.js +29 -0
- package/dist/replica.js +7 -1
- package/dist/repo-status.js +156 -0
- package/dist/repo.js +6 -1
- package/dist/runner-command.js +10 -1
- package/dist/runner.js +628 -0
- package/dist/transport.js +2 -1
- package/package.json +1 -1
- package/skills/catalyst-github/SKILL.md +1 -1
- package/skills/catalyst-linear/SKILL.md +1 -1
- package/skills/catalyst-onboard/SKILL.md +1 -1
- package/skills/run-this-project/SKILL.md +1 -1
- package/skills/unstick/SKILL.md +1 -1
- package/skills/what-needs-me/SKILL.md +1 -1
- package/skills/what-this-repo-needs/SKILL.md +1 -1
- package/skills/whats-happening/SKILL.md +1 -1
- package/skills/whats-happening/references/machines.md +1 -1
package/dist/onboard-runner.js
CHANGED
|
@@ -8,7 +8,7 @@ import { runnerAdmission, issueRunnerOrgKey, verifyRunnerRoutes } from "./onboar
|
|
|
8
8
|
import { verifyOnboardRoutes } from "./onboard-capabilities.js";
|
|
9
9
|
import { liveTeamKey, onboardTeamAdmission } from "./onboard-capacity.js";
|
|
10
10
|
import { runnerVmCapabilities } from "./onboard-runner-probes.js";
|
|
11
|
-
import { darwinThoughtsInstallException, darwinThoughtsInstallRefusal, installDarwinThoughtsCustody } from "./onboard-runner-custody.js";
|
|
11
|
+
import { darwinThoughtsInstallAdvice, darwinThoughtsInstallException, darwinThoughtsInstallRefusal, installDarwinThoughtsCustody } from "./onboard-runner-custody.js";
|
|
12
12
|
import { emitCustodyFailure } from "./onboard-runner-telemetry.js";
|
|
13
13
|
import { canonicalDarwinThoughtsJson, parseDarwinThoughtsAuthority } from "../vendor/self-host/darwin-thoughts-custody/verifier.mjs";
|
|
14
14
|
import { readExistingOnboardJson, readOnboardTeamInventory, selectedOnboardTeam, } from "./onboard-existing.js";
|
|
@@ -1161,7 +1161,9 @@ export function onboardRunnerAdapter(input = {}) {
|
|
|
1161
1161
|
const custodyReason = darwinThoughtsInstallRefusal(error);
|
|
1162
1162
|
const failureCase = custodyReturned ? "result_unverified" : "call_threw";
|
|
1163
1163
|
const details = { ...darwinThoughtsInstallException(error), ...(custodyReason ? { custodyReason } : {}) };
|
|
1164
|
-
|
|
1164
|
+
const advice = darwinThoughtsInstallAdvice(error);
|
|
1165
|
+
return refuseCustody(ctx, journal, failureCase, `Setup could not verify local storage: ${custodyReturned ? "recording the result failed" : "the native check threw"} (${details.custodyErrorClass}: ${details.custodyErrorMessage}).` +
|
|
1166
|
+
(advice ? " " + advice : ""), details, signal);
|
|
1165
1167
|
}
|
|
1166
1168
|
}
|
|
1167
1169
|
if (!writeEnv(want))
|
package/dist/onboard-runtime.js
CHANGED
|
@@ -5,6 +5,8 @@ import { openConsentBrowser } from "./consent-browser.js";
|
|
|
5
5
|
import { defaultSkillsDirFor, loadConfig, normalizeBaseUrl, readManifest, } from "./config.js";
|
|
6
6
|
import { CliError, UsageError } from "./errors.js";
|
|
7
7
|
import { cmdLegacy, findLegacy } from "./legacy.js";
|
|
8
|
+
import { findLocalSyncData, localSyncInventory } from "./local-sync-data.js";
|
|
9
|
+
import { replicaStatus } from "./replica.js";
|
|
8
10
|
import { personalConsentAdapter } from "./onboard-personal.js";
|
|
9
11
|
import { boundedOnboardSignin } from "./onboard-signin.js";
|
|
10
12
|
import { existingLinearAdapters } from "./onboard-existing.js";
|
|
@@ -134,8 +136,14 @@ export function createOnboardRuntime(args, ctx, hooks) {
|
|
|
134
136
|
},
|
|
135
137
|
};
|
|
136
138
|
};
|
|
137
|
-
const
|
|
138
|
-
|
|
139
|
+
const legacyDeps = (journal) => ({
|
|
140
|
+
...hooks.legacy,
|
|
141
|
+
preserveReplica: journal?.localSync ?? args.flags["local-sync"] === true,
|
|
142
|
+
});
|
|
143
|
+
const legacyCheck = async (stepCtx, journal) => {
|
|
144
|
+
const services = findLegacy(stepCtx.home, hooks.legacy?.platform ?? process.platform, legacyDeps(journal)).filter((item) => !item.data);
|
|
145
|
+
const files = legacyDeps(journal).preserveReplica ? [] : await findLocalSyncData(stepCtx);
|
|
146
|
+
const found = services.length + files.length;
|
|
139
147
|
if (!sameHome(stepCtx.home, (hooks.realHome ?? (() => userInfo().homedir))()))
|
|
140
148
|
return {
|
|
141
149
|
state: "skipped",
|
|
@@ -218,10 +226,40 @@ export function createOnboardRuntime(args, ctx, hooks) {
|
|
|
218
226
|
},
|
|
219
227
|
legacy: {
|
|
220
228
|
check: legacyCheck,
|
|
221
|
-
act: async (stepCtx) => {
|
|
222
|
-
const before = await legacyCheck(stepCtx);
|
|
229
|
+
act: async (stepCtx, journal) => {
|
|
230
|
+
const before = await legacyCheck(stepCtx, journal);
|
|
223
231
|
if (before.state !== "pending")
|
|
224
232
|
return before;
|
|
233
|
+
const options = legacyDeps(journal);
|
|
234
|
+
const files = options.preserveReplica ? [] : await findLocalSyncData(stepCtx);
|
|
235
|
+
const writers = findLegacy(stepCtx.home, options.platform ?? process.platform, options)
|
|
236
|
+
.filter((item) => item.kind === "job" && item.localSync);
|
|
237
|
+
if (files.length || writers.length) {
|
|
238
|
+
const inventory = [
|
|
239
|
+
...writers.map((writer) => `${writer.path} (${writer.size} bytes)`),
|
|
240
|
+
...localSyncInventory(files),
|
|
241
|
+
];
|
|
242
|
+
const show = (lines) => {
|
|
243
|
+
if (hooks.ui?.showLocalSyncInventory)
|
|
244
|
+
hooks.ui.showLocalSyncInventory(lines);
|
|
245
|
+
else
|
|
246
|
+
for (const line of lines)
|
|
247
|
+
hooks.ui ? hooks.ui.message(line) : stepCtx.stderr(line);
|
|
248
|
+
};
|
|
249
|
+
show(["Existing local sync files:", ...inventory]);
|
|
250
|
+
if (args.flags["remove-local-data"] !== true) {
|
|
251
|
+
if (!interactiveWait || (!options.prompt && !hooks.ui?.confirmLocalSyncRemoval)) {
|
|
252
|
+
const line = "Local data kept. Run catalyst setup --remove-local-data to stop the writer and remove the listed files.";
|
|
253
|
+
show([line]);
|
|
254
|
+
return { state: "waiting", reason: "local_sync_cleanup_confirmation_required", evidence: { files: inventory.join("\n"), requiredFlag: "--remove-local-data" } };
|
|
255
|
+
}
|
|
256
|
+
const accepted = options.prompt
|
|
257
|
+
? /^(y|yes)$/i.test((await options.prompt("Stop the local writer and delete the listed local sync files, keeping login, contract and settings? [y/N] ")).trim())
|
|
258
|
+
: await hooks.ui.confirmLocalSyncRemoval();
|
|
259
|
+
if (!accepted)
|
|
260
|
+
return waiting("local_sync_cleanup_declined");
|
|
261
|
+
}
|
|
262
|
+
}
|
|
225
263
|
// Provider/process output is not trusted to be secret-free; keep it out of the receipt/log.
|
|
226
264
|
const quiet = { ...stepCtx, stdout: () => { }, stderr: () => { } };
|
|
227
265
|
const code = await cmdLegacy({
|
|
@@ -229,11 +267,11 @@ export function createOnboardRuntime(args, ctx, hooks) {
|
|
|
229
267
|
command: "legacy",
|
|
230
268
|
subcommand: null,
|
|
231
269
|
rest: [],
|
|
232
|
-
flags: { remove: true, yes: true },
|
|
270
|
+
flags: { remove: true, data: !options.preserveReplica, yes: true },
|
|
233
271
|
json: false,
|
|
234
|
-
}, quiet,
|
|
272
|
+
}, quiet, { ...options, localSyncFiles: files });
|
|
235
273
|
return code === 0
|
|
236
|
-
? legacyCheck(stepCtx)
|
|
274
|
+
? legacyCheck(stepCtx, journal)
|
|
237
275
|
: { state: "failed", reason: "legacy_cleanup_failed" };
|
|
238
276
|
},
|
|
239
277
|
},
|
|
@@ -350,13 +388,14 @@ export function createOnboardRuntime(args, ctx, hooks) {
|
|
|
350
388
|
"linear.personal": personalAdapter("linear"),
|
|
351
389
|
"github.personal": personalAdapter("github"),
|
|
352
390
|
daemon: {
|
|
353
|
-
check: async (
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
evidence: { provider: "
|
|
359
|
-
|
|
391
|
+
check: async (stepCtx, journal) => {
|
|
392
|
+
if (!(journal.localSync ?? args.flags["local-sync"] === true))
|
|
393
|
+
return { state: "skipped", reason: "local_sync_not_selected", evidence: { provider: "cloud" } };
|
|
394
|
+
const status = replicaStatus(stepCtx, loadConfig(stepCtx.home));
|
|
395
|
+
return status.verdict === "fresh"
|
|
396
|
+
? { state: "done", evidence: { provider: "existing_replica", path: status.dbPath, cursor: status.cursor } }
|
|
397
|
+
: waiting("local_sync_capability_unavailable");
|
|
398
|
+
},
|
|
360
399
|
},
|
|
361
400
|
housekeeping: onboardHousekeepingAdapter(hooks.scheduler),
|
|
362
401
|
// CTC-4744: a settings read the cloud has not finished is waited on here, on one line.
|
package/dist/onboard-ui.js
CHANGED
|
@@ -402,6 +402,35 @@ export function createClackOnboardUi(prompts, streams, deps = {}) {
|
|
|
402
402
|
message("Use cloud reads by default. Local sync is optional for SQL or sustained local reads.");
|
|
403
403
|
message("Setup reads your AI accounts without sending them a request.");
|
|
404
404
|
},
|
|
405
|
+
showLocalSyncInventory(lines) {
|
|
406
|
+
stop();
|
|
407
|
+
for (const line of lines)
|
|
408
|
+
if (renderer)
|
|
409
|
+
renderer.detail(line);
|
|
410
|
+
else
|
|
411
|
+
prompts.log.message(line, { output: streams.output });
|
|
412
|
+
},
|
|
413
|
+
async confirmLocalSyncRemoval() {
|
|
414
|
+
stop();
|
|
415
|
+
if (abort.signal.aborted || !interactive)
|
|
416
|
+
return false;
|
|
417
|
+
if (renderer)
|
|
418
|
+
question();
|
|
419
|
+
const answer = await select({
|
|
420
|
+
...options,
|
|
421
|
+
message: "Stop the local writer and delete the listed files? Login, contract and settings are kept.",
|
|
422
|
+
initialValue: "keep",
|
|
423
|
+
options: [
|
|
424
|
+
{ value: "remove", label: "Yes, remove the listed local sync files" },
|
|
425
|
+
{ value: "keep", label: "No, keep them" },
|
|
426
|
+
],
|
|
427
|
+
});
|
|
428
|
+
if (prompts.isCancel(answer)) {
|
|
429
|
+
abort.abort();
|
|
430
|
+
return false;
|
|
431
|
+
}
|
|
432
|
+
return answer === "remove";
|
|
433
|
+
},
|
|
405
434
|
async confirmPlan(localSync, signin = "unavailable") {
|
|
406
435
|
if (deps.consentGiven || !interactive)
|
|
407
436
|
return { proceed: true, localSync };
|
package/dist/replica.js
CHANGED
|
@@ -253,6 +253,11 @@ export async function engineFor(sdk, dbPath, ctx, deps = {}) {
|
|
|
253
253
|
return deps.readonly ? sdk.nodeSqliteReadonlyEngine(dbPath) : sdk.nodeSqliteEngine(dbPath);
|
|
254
254
|
}
|
|
255
255
|
}
|
|
256
|
+
/** How long a snapshot pull may go with no progress before the SDK aborts it (CTC-6569). The mirror
|
|
257
|
+
* can spend up to 120 s preparing a large workspace's snapshot before its first byte, and the SDK's
|
|
258
|
+
* old 30 s default aborted nearly every such pull, so the replica never synced. host-sync and the web
|
|
259
|
+
* app pass the same 150 s. Still an idle bound: it re-arms on every chunk. */
|
|
260
|
+
export const SNAPSHOT_IDLE_TIMEOUT_MS = 150_000;
|
|
256
261
|
export const SNAPSHOT_BASE_BACKOFF_MS = 30_000;
|
|
257
262
|
export const SNAPSHOT_MAX_BACKOFF_MS = 900_000; // 15 minutes
|
|
258
263
|
export const SNAPSHOT_MAX_FAILURES = 5;
|
|
@@ -354,7 +359,7 @@ async function cmdStart(args, ctx, cfg, dbPath, deps) {
|
|
|
354
359
|
ctx.stdout(`replica writer started in the background (pid ${pid}, db ${dbPath}); check with: catalyst replica status`);
|
|
355
360
|
return 0;
|
|
356
361
|
}
|
|
357
|
-
const sdk = await loadSdk();
|
|
362
|
+
const sdk = await (deps.loadSdk ?? loadSdk)();
|
|
358
363
|
const sleep = deps.sleep ?? defaultSleep;
|
|
359
364
|
const random = deps.random ?? Math.random;
|
|
360
365
|
const baseMs = deps.snapshotRetry?.baseBackoffMs ?? SNAPSHOT_BASE_BACKOFF_MS;
|
|
@@ -404,6 +409,7 @@ async function cmdStart(args, ctx, cfg, dbPath, deps) {
|
|
|
404
409
|
engine,
|
|
405
410
|
fetchImpl: ctx.fetch,
|
|
406
411
|
wsFactory: deps.wsFactory,
|
|
412
|
+
snapshotIdleTimeoutMs: deps.snapshotIdleTimeoutMs ?? SNAPSHOT_IDLE_TIMEOUT_MS,
|
|
407
413
|
log: (level, msg, extra) => ctx.stderr(`[replica ${level}] ${msg}${extra ? ` ${safeJson(extra)}` : ""}`),
|
|
408
414
|
onStatus: (s) => {
|
|
409
415
|
ctx.stderr(`[replica] status=${s}`);
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
// Repository lifecycle commands use the key routes under /api/v1/agent/repos, independent of local checkouts.
|
|
2
|
+
import { flagString, positionals } from "./args.js";
|
|
3
|
+
import { requireConfig } from "./config.js";
|
|
4
|
+
import { MeError, UsageError } from "./errors.js";
|
|
5
|
+
import { apiClient } from "./transport.js";
|
|
6
|
+
const LIST_ROUTE = "/api/v1/agent/repos";
|
|
7
|
+
function object(value) {
|
|
8
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
9
|
+
}
|
|
10
|
+
function text(value) {
|
|
11
|
+
return typeof value === "string" && value.trim().length > 0;
|
|
12
|
+
}
|
|
13
|
+
function repository(value) {
|
|
14
|
+
return object(value) && text(value.id) && text(value.owner) && text(value.name) && text(value.status);
|
|
15
|
+
}
|
|
16
|
+
function decodeList(value) {
|
|
17
|
+
if (!object(value) || !Array.isArray(value.repositories) || !value.repositories.every(repository) || typeof value.canManage !== "boolean") {
|
|
18
|
+
throw new MeError(`GET ${LIST_ROUTE} returned an unexpected repository list shape`, "shape");
|
|
19
|
+
}
|
|
20
|
+
return { ...value, repositories: value.repositories, canManage: value.canManage };
|
|
21
|
+
}
|
|
22
|
+
function elapsedTime(value, now) {
|
|
23
|
+
if (typeof value !== "string" && typeof value !== "number")
|
|
24
|
+
return undefined;
|
|
25
|
+
const timestamp = new Date(value).getTime();
|
|
26
|
+
if (!Number.isFinite(timestamp))
|
|
27
|
+
return undefined;
|
|
28
|
+
const minutes = Math.max(0, Math.floor((now.getTime() - timestamp) / 60_000));
|
|
29
|
+
if (minutes === 0)
|
|
30
|
+
return "just now";
|
|
31
|
+
const [amount, unit] = minutes < 60 ? [minutes, "minute"] : minutes < 1440 ? [Math.floor(minutes / 60), "hour"] : [Math.floor(minutes / 1440), "day"];
|
|
32
|
+
return `${amount} ${unit}${amount === 1 ? "" : "s"} ago`;
|
|
33
|
+
}
|
|
34
|
+
function renderRow(row, ctx) {
|
|
35
|
+
let state = row.status;
|
|
36
|
+
if (state === "paused") {
|
|
37
|
+
const actor = text(row.pausedBy) ? row.pausedBy : object(row.pausedBy)
|
|
38
|
+
? [row.pausedBy.email, row.pausedBy.actor, row.pausedBy.name, row.pausedBy.label].find(text) ?? "an operator"
|
|
39
|
+
: "an operator";
|
|
40
|
+
const time = elapsedTime(row.pausedAt, ctx.now());
|
|
41
|
+
state += ` by ${actor}${time ? ` ${time}` : ""}${text(row.pauseReason) ? `: ${row.pauseReason}` : ""}`;
|
|
42
|
+
}
|
|
43
|
+
ctx.stdout(`${row.owner}/${row.name}: ${state}`);
|
|
44
|
+
const projects = Array.isArray(row.projects) ? row.projects.filter(object).filter((p) => text(p.name)).map((p) => `${p.name}${text(p.teamKey) ? ` (${p.teamKey})` : ""}`) : [];
|
|
45
|
+
ctx.stdout(` projects: ${projects.length ? projects.join(", ") : "none"}`);
|
|
46
|
+
}
|
|
47
|
+
function refuse(args, ctx, message, status, body = {}) {
|
|
48
|
+
if (args.json)
|
|
49
|
+
ctx.stdout(JSON.stringify({ ...(object(body) ? body : {}), ...(status === undefined ? {} : { status }), message }));
|
|
50
|
+
else
|
|
51
|
+
ctx.stdout(`refused${status === undefined ? "" : ` (${status})`}: ${message}`);
|
|
52
|
+
return 1;
|
|
53
|
+
}
|
|
54
|
+
function serverDetail(body) {
|
|
55
|
+
if (!object(body))
|
|
56
|
+
return "";
|
|
57
|
+
const details = [body.error, body.code, body.message, body.reason].filter(text);
|
|
58
|
+
return details.length ? ` (${[...new Set(details)].join(": ")})` : "";
|
|
59
|
+
}
|
|
60
|
+
export async function cmdRepoStatus(args, ctx) {
|
|
61
|
+
const [action, wanted, ...extra] = positionals(args);
|
|
62
|
+
if (action !== "status" && action !== "pause" && action !== "resume")
|
|
63
|
+
throw new UsageError("repo needs status, pause or resume");
|
|
64
|
+
if (["write", "apply", "with-check"].some((flag) => args.flags[flag] !== undefined)) {
|
|
65
|
+
throw new UsageError(`repo ${action} does not take local write flags`);
|
|
66
|
+
}
|
|
67
|
+
const rawReason = flagString(args, "reason");
|
|
68
|
+
const reason = rawReason?.trim();
|
|
69
|
+
if (action === "status") {
|
|
70
|
+
if (wanted !== undefined || extra.length || rawReason !== undefined)
|
|
71
|
+
throw new UsageError("repo status takes no arguments or --reason");
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
if (!wanted || !/^[A-Za-z0-9][A-Za-z0-9-]*\/[A-Za-z0-9_.-]+$/.test(wanted) || extra.length) {
|
|
75
|
+
throw new UsageError(`repo ${action} needs one repository as owner/name`);
|
|
76
|
+
}
|
|
77
|
+
if (action === "pause" && reason === undefined)
|
|
78
|
+
throw new UsageError("repo pause needs --reason with 1 to 500 characters");
|
|
79
|
+
if (reason !== undefined && (reason.length === 0 || reason.length > 500))
|
|
80
|
+
throw new UsageError("--reason must contain 1 to 500 characters after trimming");
|
|
81
|
+
}
|
|
82
|
+
const cfg = requireConfig(ctx);
|
|
83
|
+
const client = apiClient(cfg, ctx);
|
|
84
|
+
const accept = [400, 403, 404, 409];
|
|
85
|
+
const response = await client.getJson(LIST_ROUTE, { accept, parseAccepted: true });
|
|
86
|
+
if (response.status >= 400) {
|
|
87
|
+
const message = response.status === 404
|
|
88
|
+
? "this Catalyst Cloud does not serve repository status yet (needs a newer cloud)"
|
|
89
|
+
: response.status === 403 ? "your login cannot read repository status" : "repository status request failed";
|
|
90
|
+
return refuse(args, ctx, message + serverDetail(response.body), response.status, response.body);
|
|
91
|
+
}
|
|
92
|
+
const result = decodeList(response.body);
|
|
93
|
+
const role = cfg.user?.role ? `; your role is ${cfg.user.role}` : "";
|
|
94
|
+
if (action === "status") {
|
|
95
|
+
if (args.json)
|
|
96
|
+
ctx.stdout(JSON.stringify(result));
|
|
97
|
+
else {
|
|
98
|
+
for (const row of result.repositories)
|
|
99
|
+
renderRow(row, ctx);
|
|
100
|
+
if (result.repositories.length === 0)
|
|
101
|
+
ctx.stdout("No repositories are visible to your login.");
|
|
102
|
+
ctx.stdout(result.canManage
|
|
103
|
+
? "You can pause or resume repositories as a workspace owner or admin."
|
|
104
|
+
: `Pause or resume requires a workspace owner or admin${role}.`);
|
|
105
|
+
}
|
|
106
|
+
return 0;
|
|
107
|
+
}
|
|
108
|
+
const roleRefusal = `pausing or resuming a repository requires a workspace owner or admin${role}`;
|
|
109
|
+
if (!result.canManage)
|
|
110
|
+
return refuse(args, ctx, roleRefusal, 403, { error: "forbidden" });
|
|
111
|
+
const matches = result.repositories.filter((row) => `${row.owner}/${row.name}`.toLowerCase() === wanted?.toLowerCase());
|
|
112
|
+
const selected = matches[0];
|
|
113
|
+
if (!selected)
|
|
114
|
+
return refuse(args, ctx, `no repository ${wanted} is visible to your login; check catalyst repo status`, undefined, { error: "repository-not-visible" });
|
|
115
|
+
if (matches.length !== 1)
|
|
116
|
+
return refuse(args, ctx, `repository ${wanted} is ambiguous; check catalyst repo status`, undefined, { error: "repository-ambiguous" });
|
|
117
|
+
if (selected.status === "archived")
|
|
118
|
+
return refuse(args, ctx, `${wanted} is archived and cannot be paused or resumed`, 409, { error: "repository-archived" });
|
|
119
|
+
const written = await client.postJson(`${LIST_ROUTE}/${action}`, reason === undefined ? { repoId: selected.id } : { repoId: selected.id, reason }, { accept });
|
|
120
|
+
if (written.status >= 400) {
|
|
121
|
+
const detail = serverDetail(written.body);
|
|
122
|
+
let message = `could not ${action} ${wanted}`;
|
|
123
|
+
if (written.status === 403)
|
|
124
|
+
message = roleRefusal;
|
|
125
|
+
else if (written.status === 404) {
|
|
126
|
+
// Use descriptive evidence rather than requiring an undocumented server code spelling.
|
|
127
|
+
if (/route|endpoint/i.test(detail))
|
|
128
|
+
message = `this Catalyst Cloud does not serve repository ${action} yet (needs a newer cloud)`;
|
|
129
|
+
else if (/repo(sitory)?/i.test(detail))
|
|
130
|
+
message = `repository ${wanted} is no longer available; check catalyst repo status`;
|
|
131
|
+
else
|
|
132
|
+
message = `${wanted}: the repository or this route is no longer available`;
|
|
133
|
+
}
|
|
134
|
+
return refuse(args, ctx, message + detail, written.status, written.body);
|
|
135
|
+
}
|
|
136
|
+
// POST envelopes are not established by this fixture. Only a fresh read establishes the row.
|
|
137
|
+
// Never repeat a successful POST if verification fails.
|
|
138
|
+
let refreshed;
|
|
139
|
+
try {
|
|
140
|
+
const after = await client.getJson(LIST_ROUTE);
|
|
141
|
+
const found = decodeList(after.body).repositories.find((row) => row.id === selected.id);
|
|
142
|
+
if (!found)
|
|
143
|
+
throw new Error("the repository is no longer visible to your login");
|
|
144
|
+
refreshed = found;
|
|
145
|
+
}
|
|
146
|
+
catch (error) {
|
|
147
|
+
return refuse(args, ctx, `the ${action} write succeeded, but repository status could not be read: ${error instanceof Error ? error.message : String(error)}`, undefined, {
|
|
148
|
+
error: "repository-status-unavailable", writeSucceeded: true, repoId: selected.id,
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
if (args.json)
|
|
152
|
+
ctx.stdout(JSON.stringify(refreshed));
|
|
153
|
+
else
|
|
154
|
+
renderRow(refreshed, ctx);
|
|
155
|
+
return 0;
|
|
156
|
+
}
|
package/dist/repo.js
CHANGED
|
@@ -15,6 +15,7 @@ import { existsSync, lstatSync, mkdirSync, readFileSync, readlinkSync, renameSyn
|
|
|
15
15
|
import { dirname, join, relative, resolve } from "node:path";
|
|
16
16
|
import { flagBool, positionals } from "./args.js";
|
|
17
17
|
import { UsageError } from "./errors.js";
|
|
18
|
+
import { cmdRepoStatus } from "./repo-status.js";
|
|
18
19
|
export const BLOCK_START = "<!-- catalyst:start -->";
|
|
19
20
|
export const BLOCK_END = "<!-- catalyst:end -->";
|
|
20
21
|
/** The two skills the block names. ONE place each: the packs are being consolidated, and a rename
|
|
@@ -248,8 +249,12 @@ function checkoutRoot(args, sub) {
|
|
|
248
249
|
}
|
|
249
250
|
export async function cmdRepo(args, ctx) {
|
|
250
251
|
const [sub] = positionals(args);
|
|
252
|
+
if (sub === "status" || sub === "pause" || sub === "resume")
|
|
253
|
+
return cmdRepoStatus(args, ctx);
|
|
251
254
|
if (sub !== "agents-block" && sub !== "agent-setup")
|
|
252
|
-
throw new UsageError("repo needs agents-block <path> [--write] or agent-setup <path> [--apply] [--with-check]");
|
|
255
|
+
throw new UsageError("repo needs status, pause <owner/name> --reason <text>, resume <owner/name> [--reason <text>], agents-block <path> [--write] or agent-setup <path> [--apply] [--with-check]");
|
|
256
|
+
if (args.flags.reason !== undefined)
|
|
257
|
+
throw new UsageError("--reason belongs to repo pause or resume");
|
|
253
258
|
const root = checkoutRoot(args, sub);
|
|
254
259
|
const emit = (body, lines) => { if (args.json)
|
|
255
260
|
ctx.stdout(JSON.stringify(body));
|
package/dist/runner-command.js
CHANGED
|
@@ -4,7 +4,9 @@ import { UsageError } from "./errors.js";
|
|
|
4
4
|
import { autoUpdateStatus, disableAutoUpdate, enableAutoUpdate, scheduledPath, } from "./runner-auto-update.js";
|
|
5
5
|
import { dockerRunnerUpdateEngine, readReleaseState, runnerDir, runRunnerUpdate, updateResultLines, } from "./runner-update.js";
|
|
6
6
|
import { dockerFleetUpdateEngine, runFleetUpdate } from "./runner-update-fleet.js";
|
|
7
|
-
|
|
7
|
+
import { cmdRunnerLifecycle, RUNNER_LIFECYCLE_ACTIONS } from "./runner.js";
|
|
8
|
+
const USAGE = "catalyst runner [status] | stop | start | uninstall [--yes] [--delete-checkouts] | runner update [--check] [--dir <runner directory>] [--fleet] | runner auto-update [status|on|off]";
|
|
9
|
+
const UPDATE_FLAGS = ["check", "auto", "dir", "fleet"];
|
|
8
10
|
function schedulerDeps(ctx, deps) {
|
|
9
11
|
const cli = deps.scheduler?.cli ?? resolve(process.argv[1] ?? "catalyst");
|
|
10
12
|
return {
|
|
@@ -41,6 +43,13 @@ export function enableRunnerAutoUpdate(ctx, dir) {
|
|
|
41
43
|
export async function cmdRunner(args, ctx, deps = {}) {
|
|
42
44
|
const parts = positionals(args);
|
|
43
45
|
const action = parts[0];
|
|
46
|
+
if (action === undefined || RUNNER_LIFECYCLE_ACTIONS.includes(action)) {
|
|
47
|
+
if (UPDATE_FLAGS.some(flag => args.flags[flag] !== undefined))
|
|
48
|
+
throw new UsageError("--check, --auto, --dir and --fleet are only valid with runner update or auto-update");
|
|
49
|
+
return cmdRunnerLifecycle(args, ctx, deps);
|
|
50
|
+
}
|
|
51
|
+
if (args.flags.yes !== undefined || args.flags["delete-checkouts"] !== undefined)
|
|
52
|
+
throw new UsageError("--yes and --delete-checkouts are only valid with runner uninstall");
|
|
44
53
|
const dirFlag = flagString(args, "dir");
|
|
45
54
|
if (dirFlag !== undefined && !isAbsolute(dirFlag))
|
|
46
55
|
throw new UsageError("--dir must be an absolute path");
|