@catalyst-cloud/cli 0.16.25 → 0.16.28
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 +12 -0
- package/README.md +49 -1
- package/dist/args.js +23 -6
- package/dist/capabilities.js +4 -0
- package/dist/cli.js +20 -2
- package/dist/events.js +146 -1
- 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/messages.js +47 -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 +4 -4
- package/skills/catalyst-github/SKILL.md +1 -1
- package/skills/catalyst-linear/SKILL.md +1 -1
- package/skills/catalyst-linear/references/reading-from-the-cloud.md +5 -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/skills/whats-happening/references/reading-from-the-cloud.md +3 -1
package/dist/legacy.js
CHANGED
|
@@ -6,17 +6,20 @@
|
|
|
6
6
|
//
|
|
7
7
|
// The list is fixed on purpose. A prefix match would one day catch a current job (the housekeeping
|
|
8
8
|
// job, the sandbox credit guard, jobs a person made); this matches names the old runtime installed,
|
|
9
|
-
// read at the source commit below, and nothing else. Shared data folders are always kept
|
|
9
|
+
// read at the source commit below, and nothing else. Shared data folders are always kept;
|
|
10
|
+
// --data separately selects the enumerated replica and event-cache files.
|
|
10
11
|
import { existsSync, lstatSync, readFileSync, readdirSync, unlinkSync, } from "node:fs";
|
|
11
12
|
import { spawnSync } from "node:child_process";
|
|
12
13
|
import { join } from "node:path";
|
|
13
14
|
import { flagBool, positionals } from "./args.js";
|
|
14
15
|
import { UsageError } from "./errors.js";
|
|
15
16
|
import { promptSecret, stdinIsTty } from "./prompt.js";
|
|
17
|
+
import { findLocalSyncData, localSyncInventory, removeLocalSyncData } from "./local-sync-data.js";
|
|
16
18
|
/** The archived repository's commit the list below was read at. */
|
|
17
19
|
export const LEGACY_SOURCE_COMMIT = "73bc0645252ce8be38f8c87be6b67b950b3f0b56";
|
|
18
20
|
/** The old runtime's marketplace source, as Claude Code records it in known_marketplaces.json. */
|
|
19
21
|
export const OLD_MARKETPLACE_REPO = "coalesce-labs/catalyst";
|
|
22
|
+
const OLD_REPLICA_LABEL = "ai.coalesce.catalyst-replica-sync";
|
|
20
23
|
/** launchd labels the old runtime installed under ~/Library/LaunchAgents (macOS). `com.catalyst.role.<name>`
|
|
21
24
|
* is the old role supervisor's per-role instance, matched by that exact prefix. Never `dev.catalystcloud.*`
|
|
22
25
|
* (the current housekeeping job and runner jobs) and never `dev.catalyst.*` (the sandbox credit guard). */
|
|
@@ -36,7 +39,7 @@ export const OLD_LAUNCHD_LABELS = [
|
|
|
36
39
|
"ai.coalesce.catalyst-usage-page",
|
|
37
40
|
"ai.coalesce.catalyst-claude-update",
|
|
38
41
|
"ai.coalesce.catalyst-updater",
|
|
39
|
-
|
|
42
|
+
OLD_REPLICA_LABEL,
|
|
40
43
|
"ai.coalesce.catalyst-channel-watcher",
|
|
41
44
|
"ai.coalesce.catalyst-event-mirror",
|
|
42
45
|
"ai.coalesce.catalyst-monitor",
|
|
@@ -129,6 +132,9 @@ function findJobs(home, platform) {
|
|
|
129
132
|
kind: "job",
|
|
130
133
|
name: label,
|
|
131
134
|
path: join(dir, `${label}.plist`),
|
|
135
|
+
...(label === OLD_REPLICA_LABEL || label === "ai.coalesce.catalyst-event-mirror"
|
|
136
|
+
? { localSync: true, size: lstatSync(join(dir, `${label}.plist`)).size }
|
|
137
|
+
: {}),
|
|
132
138
|
}));
|
|
133
139
|
}
|
|
134
140
|
if (platform === "linux") {
|
|
@@ -157,12 +163,12 @@ function findFiles(home) {
|
|
|
157
163
|
return [...bins, ...data];
|
|
158
164
|
}
|
|
159
165
|
/** Every old piece on this machine, in removal order: plugins, their marketplace, jobs, bins, data. Never writes. */
|
|
160
|
-
export function findLegacy(home, platform) {
|
|
166
|
+
export function findLegacy(home, platform, options = {}) {
|
|
161
167
|
return [
|
|
162
168
|
...findPlugins(home),
|
|
163
169
|
...findJobs(home, platform),
|
|
164
170
|
...findFiles(home),
|
|
165
|
-
];
|
|
171
|
+
].filter((item) => !(options.preserveReplica && "localSync" in item && item.localSync));
|
|
166
172
|
}
|
|
167
173
|
const defaultRun = (cmd, args) => {
|
|
168
174
|
const r = spawnSync(cmd, args, { encoding: "utf8", timeout: 60_000 });
|
|
@@ -224,8 +230,9 @@ export async function cmdLegacy(args, ctx, deps = {}) {
|
|
|
224
230
|
const platform = deps.platform ?? process.platform;
|
|
225
231
|
const uid = deps.uid ?? (typeof process.getuid === "function" ? process.getuid() : 0);
|
|
226
232
|
const run = deps.run ?? defaultRun;
|
|
227
|
-
const
|
|
228
|
-
const
|
|
233
|
+
const local = deps.preserveReplica ? [] : deps.localSyncFiles ?? await findLocalSyncData(ctx);
|
|
234
|
+
const found = [...findLegacy(ctx.home, platform, deps), ...local];
|
|
235
|
+
const legacy = found.filter((item) => !item.data && (item.kind === "job" || !item.localSync || withData));
|
|
229
236
|
const shared = found.filter((item) => item.data);
|
|
230
237
|
const emit = (body, lines) => {
|
|
231
238
|
if (args.json)
|
|
@@ -234,24 +241,34 @@ export async function cmdLegacy(args, ctx, deps = {}) {
|
|
|
234
241
|
for (const l of lines)
|
|
235
242
|
ctx.stdout(l);
|
|
236
243
|
};
|
|
244
|
+
const inventory = localSyncInventory(local);
|
|
237
245
|
if (legacy.length === 0) {
|
|
238
246
|
emit({
|
|
239
247
|
sourceCommit: LEGACY_SOURCE_COMMIT,
|
|
240
248
|
found: [],
|
|
241
249
|
removed: [],
|
|
242
250
|
remaining: [],
|
|
243
|
-
kept: shared,
|
|
251
|
+
kept: [...shared, ...local],
|
|
252
|
+
...(local.length ? { localSyncFiles: local } : {}),
|
|
253
|
+
...(local.length && !deps.localSyncSelected ? { requiredFlag: "--remove --data" } : {}),
|
|
244
254
|
}, [
|
|
245
255
|
"no leftovers of the old local Catalyst runtime on this machine",
|
|
246
256
|
...(shared.length
|
|
247
257
|
? ["shared current Catalyst data folders are present and kept"]
|
|
248
258
|
: []),
|
|
259
|
+
...inventory,
|
|
260
|
+
...(local.length
|
|
261
|
+
? [deps.localSyncSelected
|
|
262
|
+
? "Local sync is on, so these files are current data and kept."
|
|
263
|
+
: "Local sync data kept. If you no longer use local sync, run catalyst legacy --remove --data to review and remove it."]
|
|
264
|
+
: []),
|
|
249
265
|
]);
|
|
250
266
|
return 0;
|
|
251
267
|
}
|
|
252
268
|
const listLines = [
|
|
253
269
|
`Leftovers of the old local Catalyst runtime on this machine (${legacy.length}):`,
|
|
254
|
-
...legacy.map((f) => ` ${f.kind}: ${f.name}${f.kind === "job" ? ` — ${f.path}` : ""}`),
|
|
270
|
+
...legacy.map((f) => ` ${f.kind}: ${f.name}${f.kind === "job" ? ` — ${f.path} (${lstatSync(f.path).size} bytes)` : ""}`),
|
|
271
|
+
...inventory,
|
|
255
272
|
...(shared.length
|
|
256
273
|
? [
|
|
257
274
|
"Shared current data folders (kept):",
|
|
@@ -286,14 +303,16 @@ export async function cmdLegacy(args, ctx, deps = {}) {
|
|
|
286
303
|
}, [
|
|
287
304
|
...listLines,
|
|
288
305
|
RECOMMEND,
|
|
289
|
-
|
|
306
|
+
withData
|
|
307
|
+
? "nothing removed: no terminal to ask on; run catalyst legacy --remove --data --yes to remove the listed local sync files (login, contract, settings and shared data folders are kept)"
|
|
308
|
+
: "nothing removed: no terminal to ask on; run catalyst legacy --remove --yes to remove these without a question (all data folders are kept)",
|
|
290
309
|
]);
|
|
291
310
|
return 1;
|
|
292
311
|
}
|
|
293
312
|
for (const l of listLines)
|
|
294
313
|
ctx.stdout(l);
|
|
295
314
|
ctx.stdout(RECOMMEND);
|
|
296
|
-
const answer = (await (deps.prompt ?? ((q) => promptSecret(q)))(`Remove ${legacy.length} item${legacy.length === 1 ? "" : "s"}, keeping all data folders? [y/N] `))
|
|
315
|
+
const answer = (await (deps.prompt ?? ((q) => promptSecret(q)))(`Remove ${legacy.length} item${legacy.length === 1 ? "" : "s"}${withData && local.length ? ", including the listed local sync files" : ", keeping all data folders"}? [y/N] `))
|
|
297
316
|
.trim()
|
|
298
317
|
.toLowerCase();
|
|
299
318
|
go = answer === "y" || answer === "yes";
|
|
@@ -313,6 +332,11 @@ export async function cmdLegacy(args, ctx, deps = {}) {
|
|
|
313
332
|
const kept = [];
|
|
314
333
|
const lines = [];
|
|
315
334
|
for (const item of found) {
|
|
335
|
+
if (item.localSync && item.kind === "data") {
|
|
336
|
+
if (!withData)
|
|
337
|
+
kept.push(item);
|
|
338
|
+
continue;
|
|
339
|
+
}
|
|
316
340
|
if (item.data) {
|
|
317
341
|
kept.push(item);
|
|
318
342
|
lines.push(`kept: ${item.kind} ${item.name} (shared current state; always kept)`);
|
|
@@ -328,8 +352,25 @@ export async function cmdLegacy(args, ctx, deps = {}) {
|
|
|
328
352
|
lines.push(`still present: ${item.kind} ${item.name} (${why})`);
|
|
329
353
|
}
|
|
330
354
|
}
|
|
355
|
+
if (withData && local.length) {
|
|
356
|
+
try {
|
|
357
|
+
if (failed.some(({ item }) => item.name === OLD_REPLICA_LABEL || item.name === "ai.coalesce.catalyst-event-mirror"))
|
|
358
|
+
throw new Error("local sync service did not stop; local data kept");
|
|
359
|
+
await removeLocalSyncData(local, deps.stopWriter);
|
|
360
|
+
removed.push(...local);
|
|
361
|
+
lines.push(...local.map((item) => `removed: local sync data ${item.path}`));
|
|
362
|
+
}
|
|
363
|
+
catch (error) {
|
|
364
|
+
kept.push(...local);
|
|
365
|
+
failed.push({ item: local[0], why: error instanceof Error ? error.message : String(error) });
|
|
366
|
+
lines.push("local sync cleanup failed; remaining local data kept");
|
|
367
|
+
}
|
|
368
|
+
}
|
|
331
369
|
// re-check from the same list, so the report says what the machine holds now, not what was attempted
|
|
332
|
-
const remaining =
|
|
370
|
+
const remaining = [
|
|
371
|
+
...findLegacy(ctx.home, platform, deps).filter((f) => !f.data),
|
|
372
|
+
...(withData ? await findLocalSyncData(ctx) : []),
|
|
373
|
+
];
|
|
333
374
|
const keptNote = kept.length > 0 ? "; all shared data folders were kept" : "";
|
|
334
375
|
lines.push(remaining.length === 0
|
|
335
376
|
? `re-checked: nothing of the old runtime remains${keptNote}`
|
|
@@ -342,5 +383,5 @@ export async function cmdLegacy(args, ctx, deps = {}) {
|
|
|
342
383
|
kept,
|
|
343
384
|
failed,
|
|
344
385
|
}, lines);
|
|
345
|
-
return remaining.length === 0 ? 0 : 1;
|
|
386
|
+
return remaining.length === 0 && failed.length === 0 ? 0 : 1;
|
|
346
387
|
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { existsSync, lstatSync, readFileSync, readdirSync, unlinkSync } from "node:fs";
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
3
|
+
import { isAbsolute, join } from "node:path";
|
|
4
|
+
import { configPathFor, defaultReplicaDbFor, replicaDbPath } from "./config.js";
|
|
5
|
+
import { CliError } from "./errors.js";
|
|
6
|
+
import { loadMachinePaths } from "../vendor/paths/node.js";
|
|
7
|
+
function json(path) {
|
|
8
|
+
try {
|
|
9
|
+
const value = JSON.parse(readFileSync(path, "utf8"));
|
|
10
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
return {};
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
/** Read only known replica leaves and SDK event-cache segments, never a shared directory. */
|
|
17
|
+
export async function findLocalSyncData(ctx) {
|
|
18
|
+
const cfg = json(configPathFor(ctx.home));
|
|
19
|
+
const env = { ...ctx.env, HOME: ctx.home };
|
|
20
|
+
const machine = await loadMachinePaths({ env });
|
|
21
|
+
const databases = new Set([
|
|
22
|
+
defaultReplicaDbFor(ctx.home),
|
|
23
|
+
join(ctx.home, ".config", "catalyst", "replica.db"),
|
|
24
|
+
]);
|
|
25
|
+
if (typeof cfg.replicaDb === "string") {
|
|
26
|
+
if (!isAbsolute(cfg.replicaDb))
|
|
27
|
+
throw new CliError("the saved replica path is not absolute; local data kept", "local-sync-path-invalid");
|
|
28
|
+
databases.add(cfg.replicaDb);
|
|
29
|
+
}
|
|
30
|
+
try {
|
|
31
|
+
databases.add(replicaDbPath({ replicaDb: typeof cfg.replicaDb === "string" ? cfg.replicaDb : undefined }, ctx.home, ctx.env));
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
if (!(error instanceof CliError && error.code === "replica-not-configured"))
|
|
35
|
+
throw error;
|
|
36
|
+
}
|
|
37
|
+
const paths = new Set();
|
|
38
|
+
for (const db of databases) {
|
|
39
|
+
if (/^(customer|contract|settings|config|paths)\.(json|toml)$/.test(db.split("/").at(-1) ?? ""))
|
|
40
|
+
throw new CliError(`local sync cleanup cannot delete login, contract or settings: ${db}`, "local-sync-path-protected");
|
|
41
|
+
for (const suffix of ["", "-wal", "-shm", ".pid", ".writer.lock", ".writer.state"])
|
|
42
|
+
paths.add(`${db}${suffix}`);
|
|
43
|
+
}
|
|
44
|
+
const roots = new Set([
|
|
45
|
+
join(ctx.env.XDG_STATE_HOME ?? join(ctx.home, ".local", "state"), "catalyst", "events"),
|
|
46
|
+
...(ctx.env.CATALYST_EVENTS_DIR ? [ctx.env.CATALYST_EVENTS_DIR] : []),
|
|
47
|
+
...(machine?.paths.events ? [machine.paths.events] : []),
|
|
48
|
+
]);
|
|
49
|
+
const entries = (dir) => {
|
|
50
|
+
try {
|
|
51
|
+
if (!lstatSync(dir).isDirectory())
|
|
52
|
+
return [];
|
|
53
|
+
return readdirSync(dir);
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
if (error.code === "ENOENT")
|
|
57
|
+
return [];
|
|
58
|
+
throw error;
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
const cacheFiles = (dir) => {
|
|
62
|
+
for (const name of entries(dir))
|
|
63
|
+
if (/^\d{4}-\d{2}-\d{2}(?:-\d{3})?\.jsonl$/.test(name) || ["cursor.json", ".sync.writer.lock"].includes(name))
|
|
64
|
+
paths.add(join(dir, name));
|
|
65
|
+
};
|
|
66
|
+
for (const root of roots) {
|
|
67
|
+
cacheFiles(root);
|
|
68
|
+
for (const account of entries(root)) {
|
|
69
|
+
const dir = join(root, account);
|
|
70
|
+
if (lstatSync(dir).isDirectory())
|
|
71
|
+
cacheFiles(join(dir, "backbone"));
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
const files = [];
|
|
75
|
+
for (const path of paths) {
|
|
76
|
+
if (!existsSync(path))
|
|
77
|
+
continue;
|
|
78
|
+
const stat = lstatSync(path);
|
|
79
|
+
if (!stat.isFile())
|
|
80
|
+
throw new Error(`local sync cleanup requires a regular file: ${path}`);
|
|
81
|
+
files.push({ kind: "data", name: path, path, localSync: true, size: stat.size, dev: stat.dev, ino: stat.ino });
|
|
82
|
+
}
|
|
83
|
+
return files;
|
|
84
|
+
}
|
|
85
|
+
export function localSyncInventory(files) {
|
|
86
|
+
return files.map((file) => `${file.path} (${file.size} bytes)`);
|
|
87
|
+
}
|
|
88
|
+
/** Stop the replica writer a pidfile or lock names. A pid that is gone, belongs to another user
|
|
89
|
+
* (EPERM: a writer this person started runs as them) or runs another command is a stale record
|
|
90
|
+
* left by a crash or reboot: no signal is sent, and cleanup goes on. */
|
|
91
|
+
async function stopWriter(pid) {
|
|
92
|
+
if (pid === process.pid)
|
|
93
|
+
throw new Error("refusing to stop the cleanup process as a replica writer");
|
|
94
|
+
const alive = () => {
|
|
95
|
+
try {
|
|
96
|
+
process.kill(pid, 0);
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
99
|
+
catch (error) {
|
|
100
|
+
const code = error.code;
|
|
101
|
+
if (code === "ESRCH" || code === "EPERM")
|
|
102
|
+
return false;
|
|
103
|
+
throw error;
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
if (!alive())
|
|
107
|
+
return;
|
|
108
|
+
const command = spawnSync("ps", ["-p", String(pid), "-o", "command="], { encoding: "utf8", timeout: 1000 });
|
|
109
|
+
if (command.status !== 0 || !/(?:^|[/\s])catalyst(?:-skills)?(?:\.js)?\s+replica\s+start\b|(?:^|[/\s])catalyst-replica-sync(?:\s|$)/.test(command.stdout))
|
|
110
|
+
return;
|
|
111
|
+
process.kill(pid, "SIGTERM");
|
|
112
|
+
const deadline = Date.now() + 5000;
|
|
113
|
+
while (alive()) {
|
|
114
|
+
if (Date.now() >= deadline)
|
|
115
|
+
throw new Error(`replica writer ${pid} did not stop; local data kept`);
|
|
116
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
/** Internal test seam for the production writer stop, without a process override. */
|
|
120
|
+
export const stopReplicaWriterForTest = stopWriter;
|
|
121
|
+
/** Wait for every detached writer before unlinking. Refuse a replacement file after approval. */
|
|
122
|
+
export async function removeLocalSyncData(files, stop = stopWriter) {
|
|
123
|
+
const pids = new Set();
|
|
124
|
+
const present = (file) => {
|
|
125
|
+
let current;
|
|
126
|
+
try {
|
|
127
|
+
current = lstatSync(file.path);
|
|
128
|
+
}
|
|
129
|
+
catch (error) {
|
|
130
|
+
if (error.code === "ENOENT")
|
|
131
|
+
return false;
|
|
132
|
+
throw error;
|
|
133
|
+
}
|
|
134
|
+
if (!current.isFile() || current.dev !== file.dev || current.ino !== file.ino)
|
|
135
|
+
throw new Error(`local sync file changed after confirmation; kept: ${file.path}`);
|
|
136
|
+
return true;
|
|
137
|
+
};
|
|
138
|
+
// Check the whole approved inventory before reading ownership evidence or sending a signal.
|
|
139
|
+
for (const file of files)
|
|
140
|
+
present(file);
|
|
141
|
+
for (const file of files) {
|
|
142
|
+
if (!present(file))
|
|
143
|
+
continue;
|
|
144
|
+
let pid;
|
|
145
|
+
if (file.path.endsWith(".pid"))
|
|
146
|
+
pid = Number(readFileSync(file.path, "utf8").trim());
|
|
147
|
+
else if (file.path.endsWith(".writer.lock"))
|
|
148
|
+
pid = json(file.path).pid;
|
|
149
|
+
if (Number.isSafeInteger(pid) && Number(pid) > 0)
|
|
150
|
+
pids.add(Number(pid));
|
|
151
|
+
}
|
|
152
|
+
for (const pid of pids)
|
|
153
|
+
await stop(pid);
|
|
154
|
+
for (const file of files)
|
|
155
|
+
present(file);
|
|
156
|
+
for (const file of files)
|
|
157
|
+
if (present(file))
|
|
158
|
+
unlinkSync(file.path);
|
|
159
|
+
}
|
|
@@ -12,6 +12,7 @@ function machine(value) {
|
|
|
12
12
|
count(value.free) &&
|
|
13
13
|
(value.lastCheckInAtMs === null || count(value.lastCheckInAtMs)) &&
|
|
14
14
|
(value.status === "live" || value.status === "stale") &&
|
|
15
|
+
(value.stopped === undefined || typeof value.stopped === "boolean") &&
|
|
15
16
|
typeof value.removable === "boolean" &&
|
|
16
17
|
typeof value.renameable === "boolean");
|
|
17
18
|
}
|
package/dist/messages.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { flagString, positionals } from "./args.js";
|
|
2
|
+
import { requireConfig } from "./config.js";
|
|
3
|
+
import { CliError, UsageError } from "./errors.js";
|
|
4
|
+
import { bearerFor } from "./oauth.js";
|
|
5
|
+
import { loadHttpSdk } from "./sdk.js";
|
|
6
|
+
function failure(result) {
|
|
7
|
+
const reason = "reason" in result && typeof result.reason === "string" ? `: ${result.reason}` : "";
|
|
8
|
+
return `messages: ${result.outcome}${"status" in result && result.status ? ` (${result.status})` : ""}${reason}`;
|
|
9
|
+
}
|
|
10
|
+
export async function cmdMessages(args, ctx, deps = {}) {
|
|
11
|
+
const [action, ...extra] = positionals(args);
|
|
12
|
+
if (action !== "read" || extra.length)
|
|
13
|
+
throw new UsageError("messages takes: read --ticket CTC-N [--since SEQUENCE]");
|
|
14
|
+
const ticket = flagString(args, "ticket")?.trim().toUpperCase();
|
|
15
|
+
if (!ticket)
|
|
16
|
+
throw new UsageError("messages read needs --ticket CTC-N");
|
|
17
|
+
const rawSince = flagString(args, "since");
|
|
18
|
+
const since = rawSince === undefined ? undefined : Number(rawSince);
|
|
19
|
+
if (since !== undefined && (!Number.isSafeInteger(since) || since < 0))
|
|
20
|
+
throw new UsageError("--since must be a non-negative message sequence");
|
|
21
|
+
const cfg = requireConfig(ctx);
|
|
22
|
+
const createClient = deps.createClient ?? (async (options) => (await loadHttpSdk()).createTenantClient(options));
|
|
23
|
+
const client = await createClient({ key: await bearerFor(ctx, cfg), baseUrl: cfg.baseUrl, fetch: ctx.fetch });
|
|
24
|
+
if (!client.messages)
|
|
25
|
+
throw new CliError("the installed SDK cannot read messages; upgrade the catalyst CLI", "sdk-upgrade-required");
|
|
26
|
+
const result = await client.messages.read({ ticket, ...(since === undefined ? {} : { since }) });
|
|
27
|
+
if (result.outcome !== "ok") {
|
|
28
|
+
if (args.json)
|
|
29
|
+
ctx.stdout(JSON.stringify(result));
|
|
30
|
+
else
|
|
31
|
+
ctx.stderr(failure(result));
|
|
32
|
+
return 1;
|
|
33
|
+
}
|
|
34
|
+
if (args.json)
|
|
35
|
+
ctx.stdout(JSON.stringify({ ticket: result.ticket, messages: result.messages, next: result.next }));
|
|
36
|
+
else if (result.messages.length === 0)
|
|
37
|
+
ctx.stdout(`${result.ticket}: no messages${since === undefined ? "" : ` at or after ${since}`}`);
|
|
38
|
+
else
|
|
39
|
+
for (const message of result.messages) {
|
|
40
|
+
ctx.stdout(`#${message.seq} ${message.sentAt} ${message.from.label} (${message.kind})`);
|
|
41
|
+
for (const line of message.body.split("\n"))
|
|
42
|
+
ctx.stdout(` ${line}`);
|
|
43
|
+
}
|
|
44
|
+
if (result.next !== null)
|
|
45
|
+
ctx.stderr(`more: re-run with --since ${result.next}`);
|
|
46
|
+
return 0;
|
|
47
|
+
}
|
|
@@ -19,6 +19,14 @@ class CustodyInstallError extends Error {
|
|
|
19
19
|
export function darwinThoughtsInstallRefusal(error) {
|
|
20
20
|
return error instanceof CustodyInstallError ? error.reason : undefined;
|
|
21
21
|
}
|
|
22
|
+
/** What a person changes for a create refusal that names its cause; other refusals have no advice. */
|
|
23
|
+
const CREATE_REFUSAL_ADVICE = {
|
|
24
|
+
bootstrap_create_mount_refused: "Docker Desktop refused to share a storage folder with the check container. In Docker Desktop, open Settings, then Resources, then File sharing, make sure your home folder is listed, apply, and run setup again.",
|
|
25
|
+
};
|
|
26
|
+
export function darwinThoughtsInstallAdvice(error) {
|
|
27
|
+
const reason = darwinThoughtsInstallRefusal(error);
|
|
28
|
+
return reason === undefined ? undefined : CREATE_REFUSAL_ADVICE[reason];
|
|
29
|
+
}
|
|
22
30
|
/** Unknown exception text can contain credentials. Keep its class, and explicitly redact it. */
|
|
23
31
|
export function darwinThoughtsInstallException(error) {
|
|
24
32
|
const known = error instanceof CustodyInstallError;
|
|
@@ -620,6 +628,7 @@ async function actualBootstrap(input) {
|
|
|
620
628
|
let id;
|
|
621
629
|
let cachedImage;
|
|
622
630
|
let createAttempted = false;
|
|
631
|
+
let createRefused = false;
|
|
623
632
|
const receiveIdentity = (received, acknowledgedId) => {
|
|
624
633
|
if (received.code !== 0)
|
|
625
634
|
fail("bootstrap_cleanup_unproved");
|
|
@@ -681,8 +690,10 @@ async function actualBootstrap(input) {
|
|
|
681
690
|
"PATH=/usr/bin:/bin", "BUN_RUNTIME_TRANSPILER_CACHE_PATH=0", "/usr/local/bin/bun", "--config=/dev/null", "--no-env-file", "--cwd=/",
|
|
682
691
|
"-e", program, canonicalDarwinThoughtsJson({ authority, deadlineMs: input.deadlineMs })]);
|
|
683
692
|
// Docker's own refusal of a mount source is matched, never forwarded: stderr can carry secrets.
|
|
684
|
-
if (created.code !== 0 && /bind source path does not exist|[Mm]ounts denied/.test(created.stderr))
|
|
693
|
+
if (created.code !== 0 && /bind source path does not exist|[Mm]ounts denied/.test(created.stderr)) {
|
|
694
|
+
createRefused = true;
|
|
685
695
|
fail("bootstrap_create_mount_refused");
|
|
696
|
+
}
|
|
686
697
|
if (created.code !== 0 || !/^[a-f0-9]{64}$/.test(created.stdout))
|
|
687
698
|
fail("bootstrap_create_unreceived");
|
|
688
699
|
id = created.stdout;
|
|
@@ -726,7 +737,17 @@ async function actualBootstrap(input) {
|
|
|
726
737
|
image.stdout !== cachedImage)
|
|
727
738
|
fail("bootstrap_cleanup_unproved");
|
|
728
739
|
let received;
|
|
729
|
-
|
|
740
|
+
// A refused create persists nothing; prove that by our random token label, and only
|
|
741
|
+
// fall back to recovering the exact helper when the Engine lists one anyway.
|
|
742
|
+
let refusedAbsent = false;
|
|
743
|
+
if (createRefused && id === undefined) {
|
|
744
|
+
const persisted = await cleanupDocker(["ps", "--all", "--no-trunc", "--quiet",
|
|
745
|
+
"--filter", "label=dev.catalystcloud.custody-token=" + token]);
|
|
746
|
+
if (persisted.code !== 0)
|
|
747
|
+
fail("bootstrap_cleanup_unproved");
|
|
748
|
+
refusedAbsent = persisted.stdout === "";
|
|
749
|
+
}
|
|
750
|
+
for (let attempt = 0; !refusedAbsent && attempt < 32; attempt++) {
|
|
730
751
|
const current = await cleanupDocker(["inspect", name]);
|
|
731
752
|
if (current.code === 0) {
|
|
732
753
|
received = current;
|
|
@@ -736,19 +757,21 @@ async function actualBootstrap(input) {
|
|
|
736
757
|
fail("bootstrap_cleanup_unproved");
|
|
737
758
|
await new Promise(resolve => setTimeout(resolve, 25));
|
|
738
759
|
}
|
|
739
|
-
if (
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
760
|
+
if (!refusedAbsent) {
|
|
761
|
+
if (received === undefined)
|
|
762
|
+
fail("bootstrap_cleanup_unproved");
|
|
763
|
+
id = receiveIdentity(received, id);
|
|
764
|
+
const list = ["ps", "--all", "--no-trunc", "--quiet", "--filter", "id=" + id];
|
|
765
|
+
const present = await cleanupDocker(list);
|
|
766
|
+
if (present.code !== 0 || present.stdout !== id)
|
|
767
|
+
fail("bootstrap_cleanup_unproved");
|
|
768
|
+
const removed = await cleanupDocker(["rm", "--force", id]);
|
|
769
|
+
if (removed.code !== 0)
|
|
770
|
+
fail("bootstrap_cleanup_unproved");
|
|
771
|
+
const absent = await cleanupDocker(list);
|
|
772
|
+
if (absent.code !== 0 || absent.stdout !== "")
|
|
773
|
+
fail("bootstrap_cleanup_unproved");
|
|
774
|
+
}
|
|
752
775
|
}
|
|
753
776
|
catch {
|
|
754
777
|
// Keep the refusal that started cleanup; without it every create failure reads as cleanup.
|
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 };
|