@catalyst-cloud/cli 0.14.6 → 0.14.8
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 +14 -0
- package/README.md +7 -5
- package/dist/args.js +68 -18
- package/dist/browser.js +37 -3
- package/dist/cli.js +58 -12
- package/dist/config.js +17 -5
- package/dist/consent-browser.js +58 -0
- package/dist/daemon-authority-lifetime.js +171 -0
- package/dist/daemon-authority.js +216 -0
- package/dist/daemon-cache.js +348 -0
- package/dist/daemon-store.js +370 -0
- package/dist/daemon-supervisor.js +460 -0
- package/dist/env/onboard-draft-store.js +250 -0
- package/dist/env/onboard-draft.js +33 -0
- package/dist/env/selected-import-writer.js +163 -0
- package/dist/env/settings-authority.js +205 -0
- package/dist/events-cloud-tail.js +146 -0
- package/dist/events-cloud.js +619 -0
- package/dist/events.js +186 -31
- package/dist/oauth.js +129 -34
- package/dist/onboard-accounts.js +259 -0
- package/dist/onboard-automation-management.js +15 -0
- package/dist/onboard-bootstrap.js +162 -0
- package/dist/onboard-capabilities.js +15 -7
- package/dist/onboard-capacity.js +226 -0
- package/dist/onboard-existing.js +106 -32
- package/dist/onboard-file-snapshot.js +45 -0
- package/dist/onboard-github.js +14 -3
- package/dist/onboard-login-candidate.js +261 -0
- package/dist/onboard-next.js +139 -0
- package/dist/onboard-personal.js +14 -3
- package/dist/onboard-projects.js +350 -0
- package/dist/onboard-repositories.js +146 -37
- package/dist/onboard-runtime.js +108 -13
- package/dist/onboard-settings-context.js +141 -0
- package/dist/onboard-settings-import.js +127 -0
- package/dist/onboard-settings.js +234 -0
- package/dist/onboard-ui.js +85 -59
- package/dist/onboard-workflow.js +316 -0
- package/dist/onboard-workspace.js +14 -3
- package/dist/onboard.js +422 -39
- package/dist/owned-fetch.js +182 -0
- package/dist/pagination.js +13 -1
- package/dist/query.js +104 -31
- package/dist/ready.js +143 -35
- package/dist/replica.js +11 -4
- package/dist/repository-id.js +5 -0
- package/dist/sdk.js +33 -0
- package/dist/watch.js +2 -2
- package/package.json +3 -2
- package/skills/catalyst-github/SKILL.md +4 -2
- package/skills/catalyst-github/references/is-it-mergeable.md +1 -1
- package/skills/catalyst-github/references/local-replica.md +78 -0
- package/skills/catalyst-github/references/reading-from-the-cloud.md +24 -0
- package/skills/catalyst-github/references/what-a-pr-accumulates.md +1 -1
- package/skills/catalyst-github/scripts/is-it-mergeable.mjs +210 -45
- package/skills/catalyst-github/scripts/read-pr.mjs +51 -15
- package/skills/catalyst-linear/SKILL.md +5 -3
- package/skills/catalyst-linear/references/local-replica.md +91 -0
- package/skills/catalyst-linear/references/reading-a-ticket.md +3 -3
- package/skills/catalyst-linear/references/reading-from-the-cloud.md +38 -0
- package/skills/catalyst-linear/scripts/read-ticket.mjs +37 -12
- package/skills/catalyst-onboard/SKILL.md +6 -4
- package/skills/catalyst-onboard/references/local-replica.md +40 -0
- package/skills/catalyst-onboard/references/local-sync.md +8 -4
- package/skills/catalyst-onboard/scripts/local-sync.mjs +2 -2
- package/skills/catalyst-onboard/scripts/where-am-i.mjs +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 +4 -2
- package/skills/whats-happening/references/local-replica.md +51 -0
- package/skills/whats-happening/references/reading-from-the-cloud.md +21 -0
- package/skills/whats-happening/references/status-reply.md +1 -1
- package/skills/whats-happening/scripts/snapshot.mjs +104 -26
- package/vendor/settings/index.d.ts +4 -0
- package/vendor/settings/index.js +2937 -0
- package/vendor/settings/provenance.json +148 -0
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
import { constants, closeSync, fstatSync, fsyncSync, lstatSync, mkdirSync, openSync, readFileSync, renameSync, unlinkSync, writeFileSync, } from "node:fs";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { hostname } from "node:os";
|
|
4
|
+
import { dirname, isAbsolute, join, resolve, sep } from "node:path";
|
|
5
|
+
const fail = () => new Error("daemon_state_unverified");
|
|
6
|
+
const object = (value) => value !== null && typeof value === "object" && !Array.isArray(value)
|
|
7
|
+
? value
|
|
8
|
+
: null;
|
|
9
|
+
const integer = (value) => Number.isSafeInteger(value) && Number(value) >= 0;
|
|
10
|
+
const states = new Set(["starting", "live", "backoff", "stopped"]);
|
|
11
|
+
const reasons = new Set([
|
|
12
|
+
"authentication_required",
|
|
13
|
+
"event_history_gap",
|
|
14
|
+
"writer_conflict",
|
|
15
|
+
"identity_mismatch",
|
|
16
|
+
"replica_failure_cap",
|
|
17
|
+
"event_failure_cap",
|
|
18
|
+
]);
|
|
19
|
+
const own = (stat) => process.getuid === undefined || stat.uid === process.getuid();
|
|
20
|
+
const identity = (a, b) => a.dev === b.dev && a.ino === b.ino;
|
|
21
|
+
const sameBinding = (a, b) => a.account === b.account &&
|
|
22
|
+
a.origin === b.origin &&
|
|
23
|
+
a.replicaDb === b.replicaDb &&
|
|
24
|
+
a.eventDirectory === b.eventDirectory;
|
|
25
|
+
/** Parse persisted state as untrusted input. A malformed stop record never clears a latch. */
|
|
26
|
+
export function parseDaemonRecord(value) {
|
|
27
|
+
const record = object(value);
|
|
28
|
+
const binding = object(record?.binding);
|
|
29
|
+
const replica = object(record?.replica);
|
|
30
|
+
const events = object(record?.events);
|
|
31
|
+
const stopped = object(record?.stopped);
|
|
32
|
+
if (!record ||
|
|
33
|
+
record.schema !== 1 ||
|
|
34
|
+
!binding ||
|
|
35
|
+
!replica ||
|
|
36
|
+
!events ||
|
|
37
|
+
typeof binding.account !== "string" ||
|
|
38
|
+
!binding.account ||
|
|
39
|
+
binding.account.length > 256 ||
|
|
40
|
+
typeof binding.origin !== "string" ||
|
|
41
|
+
typeof binding.replicaDb !== "string" ||
|
|
42
|
+
typeof binding.eventDirectory !== "string" ||
|
|
43
|
+
!isAbsolute(binding.replicaDb) ||
|
|
44
|
+
!isAbsolute(binding.eventDirectory) ||
|
|
45
|
+
typeof record.runId !== "string" ||
|
|
46
|
+
!record.runId ||
|
|
47
|
+
record.runId.length > 256 ||
|
|
48
|
+
!integer(record.pid) ||
|
|
49
|
+
record.pid === 0 ||
|
|
50
|
+
!integer(record.updatedAt) ||
|
|
51
|
+
typeof record.state !== "string" ||
|
|
52
|
+
!states.has(record.state) ||
|
|
53
|
+
typeof replica.state !== "string" ||
|
|
54
|
+
!states.has(replica.state) ||
|
|
55
|
+
!integer(replica.failures) ||
|
|
56
|
+
typeof events.state !== "string" ||
|
|
57
|
+
!states.has(events.state) ||
|
|
58
|
+
!integer(events.failures) ||
|
|
59
|
+
!(events.cursor === null || integer(events.cursor)) ||
|
|
60
|
+
!(events.head === null || integer(events.head)) ||
|
|
61
|
+
(typeof events.cursor === "number" &&
|
|
62
|
+
typeof events.head === "number" &&
|
|
63
|
+
events.cursor > events.head) ||
|
|
64
|
+
!(record.stopped === null ||
|
|
65
|
+
(stopped &&
|
|
66
|
+
integer(stopped.at) &&
|
|
67
|
+
typeof stopped.reason === "string" &&
|
|
68
|
+
reasons.has(stopped.reason) &&
|
|
69
|
+
stopped.restartWith === "catalyst daemon restart")))
|
|
70
|
+
throw fail();
|
|
71
|
+
try {
|
|
72
|
+
const origin = new URL(binding.origin);
|
|
73
|
+
if (origin.origin !== binding.origin || origin.protocol !== "https:")
|
|
74
|
+
throw fail();
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
throw fail();
|
|
78
|
+
}
|
|
79
|
+
// Validation above establishes shape; copy only the public state fields, never arbitrary extras.
|
|
80
|
+
return {
|
|
81
|
+
schema: 1,
|
|
82
|
+
binding: {
|
|
83
|
+
account: binding.account,
|
|
84
|
+
origin: binding.origin,
|
|
85
|
+
replicaDb: binding.replicaDb,
|
|
86
|
+
eventDirectory: binding.eventDirectory,
|
|
87
|
+
},
|
|
88
|
+
runId: record.runId,
|
|
89
|
+
pid: record.pid,
|
|
90
|
+
updatedAt: record.updatedAt,
|
|
91
|
+
state: record.state,
|
|
92
|
+
replica: {
|
|
93
|
+
state: replica.state,
|
|
94
|
+
failures: replica.failures,
|
|
95
|
+
},
|
|
96
|
+
events: {
|
|
97
|
+
state: events.state,
|
|
98
|
+
failures: events.failures,
|
|
99
|
+
cursor: events.cursor,
|
|
100
|
+
head: events.head,
|
|
101
|
+
},
|
|
102
|
+
stopped: stopped
|
|
103
|
+
? {
|
|
104
|
+
at: Number(stopped.at),
|
|
105
|
+
reason: stopped.reason,
|
|
106
|
+
restartWith: "catalyst daemon restart",
|
|
107
|
+
}
|
|
108
|
+
: null,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
function absent(error) {
|
|
112
|
+
return object(error)?.code === "ENOENT";
|
|
113
|
+
}
|
|
114
|
+
function stat(path) {
|
|
115
|
+
try {
|
|
116
|
+
return lstatSync(path);
|
|
117
|
+
}
|
|
118
|
+
catch (error) {
|
|
119
|
+
if (absent(error))
|
|
120
|
+
return null;
|
|
121
|
+
throw error;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
function privateDirectory(path, home) {
|
|
125
|
+
const root = resolve(home);
|
|
126
|
+
const target = resolve(path);
|
|
127
|
+
if (target !== root && !target.startsWith(root + sep))
|
|
128
|
+
throw fail();
|
|
129
|
+
const parts = [];
|
|
130
|
+
for (let current = target; current !== dirname(current); current = dirname(current))
|
|
131
|
+
parts.unshift(current);
|
|
132
|
+
for (const current of parts) {
|
|
133
|
+
let info = stat(current);
|
|
134
|
+
if (!info) {
|
|
135
|
+
mkdirSync(current, { mode: 0o700 });
|
|
136
|
+
info = lstatSync(current);
|
|
137
|
+
}
|
|
138
|
+
if (!info.isDirectory() ||
|
|
139
|
+
info.isSymbolicLink() ||
|
|
140
|
+
((current === root || current.startsWith(root + sep)) &&
|
|
141
|
+
(!own(info) || (info.mode & 0o022) !== 0)))
|
|
142
|
+
throw fail();
|
|
143
|
+
}
|
|
144
|
+
const info = lstatSync(target);
|
|
145
|
+
if ((info.mode & 0o077) !== 0)
|
|
146
|
+
throw fail();
|
|
147
|
+
}
|
|
148
|
+
function readPrivate(path) {
|
|
149
|
+
const before = stat(path);
|
|
150
|
+
if (!before)
|
|
151
|
+
return null;
|
|
152
|
+
if (!before.isFile() ||
|
|
153
|
+
before.isSymbolicLink() ||
|
|
154
|
+
!own(before) ||
|
|
155
|
+
before.nlink !== 1 ||
|
|
156
|
+
(before.mode & 0o077) !== 0 ||
|
|
157
|
+
before.size > 64 * 1024)
|
|
158
|
+
throw fail();
|
|
159
|
+
const fd = openSync(path, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
|
|
160
|
+
try {
|
|
161
|
+
const opened = fstatSync(fd);
|
|
162
|
+
if (!identity(before, opened) ||
|
|
163
|
+
!opened.isFile() ||
|
|
164
|
+
!own(opened) ||
|
|
165
|
+
opened.nlink !== 1 ||
|
|
166
|
+
(opened.mode & 0o077) !== 0)
|
|
167
|
+
throw fail();
|
|
168
|
+
const bytes = readFileSync(fd, "utf8");
|
|
169
|
+
const after = lstatSync(path);
|
|
170
|
+
const finished = fstatSync(fd);
|
|
171
|
+
if (!identity(opened, after) ||
|
|
172
|
+
opened.size !== Buffer.byteLength(bytes) ||
|
|
173
|
+
opened.mtimeMs !== after.mtimeMs ||
|
|
174
|
+
!identity(opened, finished) ||
|
|
175
|
+
finished.mtimeMs !== opened.mtimeMs)
|
|
176
|
+
throw fail();
|
|
177
|
+
return { bytes, stat: before };
|
|
178
|
+
}
|
|
179
|
+
finally {
|
|
180
|
+
closeSync(fd);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
/** Cooperative same-host lease. Only a positively dead, verified holder may be reclaimed;
|
|
184
|
+
* neither heartbeat age nor an owner-key match permits stealing a live process's lease. */
|
|
185
|
+
export function claimDaemonLease(input) {
|
|
186
|
+
const captured = {
|
|
187
|
+
...input,
|
|
188
|
+
binding: { ...input.binding },
|
|
189
|
+
directory: resolve(input.directory),
|
|
190
|
+
};
|
|
191
|
+
if (!Number.isSafeInteger(captured.pid) ||
|
|
192
|
+
captured.pid <= 0 ||
|
|
193
|
+
!captured.runId)
|
|
194
|
+
throw fail();
|
|
195
|
+
privateDirectory(captured.directory, captured.home);
|
|
196
|
+
const lock = join(captured.directory, "writer.lock");
|
|
197
|
+
const statePath = join(captured.directory, "state.json");
|
|
198
|
+
const token = randomUUID();
|
|
199
|
+
const lockBytes = JSON.stringify({
|
|
200
|
+
schema: 1,
|
|
201
|
+
pid: captured.pid,
|
|
202
|
+
host: hostname(),
|
|
203
|
+
token,
|
|
204
|
+
binding: captured.binding,
|
|
205
|
+
});
|
|
206
|
+
// Serialize the check/unlink/create sequence across cooperating processes. A crash in this
|
|
207
|
+
// short critical section leaves a guard that requires deliberate recovery; never guess stale
|
|
208
|
+
// ownership and recreate the same conditional-unlink race on the guard itself.
|
|
209
|
+
const claimGuard = join(captured.directory, "claim.lock");
|
|
210
|
+
const claimBytes = JSON.stringify({ pid: captured.pid, token });
|
|
211
|
+
let claimFd;
|
|
212
|
+
try {
|
|
213
|
+
claimFd = openSync(claimGuard, constants.O_WRONLY |
|
|
214
|
+
constants.O_CREAT |
|
|
215
|
+
constants.O_EXCL |
|
|
216
|
+
constants.O_NOFOLLOW, 0o600);
|
|
217
|
+
}
|
|
218
|
+
catch (error) {
|
|
219
|
+
if (object(error)?.code === "EEXIST")
|
|
220
|
+
throw new Error("daemon_claim_requires_recovery");
|
|
221
|
+
throw error;
|
|
222
|
+
}
|
|
223
|
+
const claimStat = fstatSync(claimFd);
|
|
224
|
+
let lockStat;
|
|
225
|
+
try {
|
|
226
|
+
try {
|
|
227
|
+
writeFileSync(claimFd, claimBytes);
|
|
228
|
+
fsyncSync(claimFd);
|
|
229
|
+
}
|
|
230
|
+
finally {
|
|
231
|
+
closeSync(claimFd);
|
|
232
|
+
}
|
|
233
|
+
const existing = readPrivate(lock);
|
|
234
|
+
if (existing) {
|
|
235
|
+
let previous;
|
|
236
|
+
try {
|
|
237
|
+
previous = object(JSON.parse(existing.bytes));
|
|
238
|
+
}
|
|
239
|
+
catch {
|
|
240
|
+
throw fail();
|
|
241
|
+
}
|
|
242
|
+
const binding = object(previous?.binding);
|
|
243
|
+
if (!previous ||
|
|
244
|
+
previous.schema !== 1 ||
|
|
245
|
+
!integer(previous.pid) ||
|
|
246
|
+
previous.pid === 0 ||
|
|
247
|
+
previous.host !== hostname() ||
|
|
248
|
+
typeof previous.token !== "string" ||
|
|
249
|
+
!binding ||
|
|
250
|
+
binding.account !== captured.binding.account ||
|
|
251
|
+
binding.origin !== captured.binding.origin ||
|
|
252
|
+
binding.replicaDb !== captured.binding.replicaDb ||
|
|
253
|
+
binding.eventDirectory !== captured.binding.eventDirectory)
|
|
254
|
+
throw fail();
|
|
255
|
+
try {
|
|
256
|
+
process.kill(previous.pid, 0);
|
|
257
|
+
throw new Error("daemon_writer_already_running");
|
|
258
|
+
}
|
|
259
|
+
catch (error) {
|
|
260
|
+
if (object(error)?.code !== "ESRCH")
|
|
261
|
+
throw error;
|
|
262
|
+
}
|
|
263
|
+
const current = readPrivate(lock);
|
|
264
|
+
if (!current ||
|
|
265
|
+
!identity(existing.stat, current.stat) ||
|
|
266
|
+
current.bytes !== existing.bytes)
|
|
267
|
+
throw fail();
|
|
268
|
+
unlinkSync(lock);
|
|
269
|
+
}
|
|
270
|
+
const fd = openSync(lock, constants.O_WRONLY |
|
|
271
|
+
constants.O_CREAT |
|
|
272
|
+
constants.O_EXCL |
|
|
273
|
+
constants.O_NOFOLLOW, 0o600);
|
|
274
|
+
try {
|
|
275
|
+
writeFileSync(fd, lockBytes);
|
|
276
|
+
fsyncSync(fd);
|
|
277
|
+
lockStat = lstatSync(lock);
|
|
278
|
+
}
|
|
279
|
+
finally {
|
|
280
|
+
closeSync(fd);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
finally {
|
|
284
|
+
const current = readPrivate(claimGuard);
|
|
285
|
+
if (current &&
|
|
286
|
+
identity(claimStat, current.stat) &&
|
|
287
|
+
current.bytes === claimBytes)
|
|
288
|
+
unlinkSync(claimGuard);
|
|
289
|
+
}
|
|
290
|
+
let released = false;
|
|
291
|
+
const assertOwned = () => {
|
|
292
|
+
privateDirectory(captured.directory, captured.home);
|
|
293
|
+
const current = readPrivate(lock);
|
|
294
|
+
if (released ||
|
|
295
|
+
!current ||
|
|
296
|
+
!identity(lockStat, current.stat) ||
|
|
297
|
+
current.bytes !== lockBytes)
|
|
298
|
+
throw fail();
|
|
299
|
+
};
|
|
300
|
+
return {
|
|
301
|
+
async read() {
|
|
302
|
+
assertOwned();
|
|
303
|
+
const value = readPrivate(statePath);
|
|
304
|
+
if (!value)
|
|
305
|
+
return null;
|
|
306
|
+
let record;
|
|
307
|
+
try {
|
|
308
|
+
record = parseDaemonRecord(JSON.parse(value.bytes));
|
|
309
|
+
}
|
|
310
|
+
catch {
|
|
311
|
+
throw fail();
|
|
312
|
+
}
|
|
313
|
+
if (!sameBinding(record.binding, captured.binding))
|
|
314
|
+
throw new Error("daemon_record_identity_mismatch");
|
|
315
|
+
return record;
|
|
316
|
+
},
|
|
317
|
+
async write(record) {
|
|
318
|
+
assertOwned();
|
|
319
|
+
const candidate = parseDaemonRecord(record);
|
|
320
|
+
if (!sameBinding(candidate.binding, captured.binding) ||
|
|
321
|
+
candidate.pid !== captured.pid ||
|
|
322
|
+
candidate.runId !== captured.runId)
|
|
323
|
+
throw fail();
|
|
324
|
+
const before = readPrivate(statePath);
|
|
325
|
+
const tmp = join(captured.directory, `.state-${token}-${randomUUID()}.tmp`);
|
|
326
|
+
const fd = openSync(tmp, constants.O_WRONLY |
|
|
327
|
+
constants.O_CREAT |
|
|
328
|
+
constants.O_EXCL |
|
|
329
|
+
constants.O_NOFOLLOW, 0o600);
|
|
330
|
+
const tmpStat = lstatSync(tmp);
|
|
331
|
+
try {
|
|
332
|
+
try {
|
|
333
|
+
writeFileSync(fd, JSON.stringify(candidate) + "\n");
|
|
334
|
+
fsyncSync(fd);
|
|
335
|
+
}
|
|
336
|
+
finally {
|
|
337
|
+
closeSync(fd);
|
|
338
|
+
}
|
|
339
|
+
assertOwned();
|
|
340
|
+
const current = readPrivate(statePath);
|
|
341
|
+
if (before
|
|
342
|
+
? !current ||
|
|
343
|
+
!identity(before.stat, current.stat) ||
|
|
344
|
+
current.bytes !== before.bytes
|
|
345
|
+
: current !== null)
|
|
346
|
+
throw fail();
|
|
347
|
+
renameSync(tmp, statePath);
|
|
348
|
+
const directoryFd = openSync(captured.directory, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
349
|
+
try {
|
|
350
|
+
fsyncSync(directoryFd);
|
|
351
|
+
}
|
|
352
|
+
finally {
|
|
353
|
+
closeSync(directoryFd);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
finally {
|
|
357
|
+
const remaining = stat(tmp);
|
|
358
|
+
if (remaining && identity(tmpStat, remaining))
|
|
359
|
+
unlinkSync(tmp);
|
|
360
|
+
}
|
|
361
|
+
},
|
|
362
|
+
async release() {
|
|
363
|
+
if (released)
|
|
364
|
+
return;
|
|
365
|
+
assertOwned();
|
|
366
|
+
unlinkSync(lock);
|
|
367
|
+
released = true;
|
|
368
|
+
},
|
|
369
|
+
};
|
|
370
|
+
}
|