@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
package/dist/ready.js
CHANGED
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
// ready.ts — `ready`: the machine checks plus the tenant's own readiness vector from the contract,
|
|
2
2
|
// one verdict, and per failure the fix and who can apply it. The replica is optional, so its absence
|
|
3
|
-
// is a note, never a failure; the SDK loading
|
|
4
|
-
import { accessSync, constants, existsSync, readFileSync, realpathSync, statSync } from "node:fs";
|
|
3
|
+
// is a note, never a failure; the HTTP SDK loading is a check for cloud reads.
|
|
4
|
+
import { accessSync, constants, existsSync, readFileSync, realpathSync, statSync, } from "node:fs";
|
|
5
5
|
import { basename, delimiter, dirname, join } from "node:path";
|
|
6
|
-
import { LEGACY_PACKAGE_NAME, PACKAGE_NAME, defaultSkillsDirFor, loadConfig, normalizeBaseUrl, readManifest, upgradeCommand } from "./config.js";
|
|
6
|
+
import { LEGACY_PACKAGE_NAME, PACKAGE_NAME, defaultSkillsDirFor, loadConfig, normalizeBaseUrl, readManifest, upgradeCommand, } from "./config.js";
|
|
7
7
|
import { contractVersionInRange, readContractCache } from "./contract.js";
|
|
8
8
|
import { CliError } from "./errors.js";
|
|
9
9
|
import { latestPublishedVersion } from "./published.js";
|
|
10
|
-
import { replicaStatus, writerIsRunning } from "./replica.js";
|
|
11
|
-
import {
|
|
12
|
-
import { FIX_COMMAND, detectRuntime, runtimeVerdict, supportedRangeText } from "./runtime.js";
|
|
10
|
+
import { replicaStatus, writerIsRunning, } from "./replica.js";
|
|
11
|
+
import { loadHttpSdk } from "./sdk.js";
|
|
12
|
+
import { FIX_COMMAND, detectRuntime, runtimeVerdict, supportedRangeText, } from "./runtime.js";
|
|
13
13
|
import { semverOlder } from "./semver.js";
|
|
14
14
|
import { FIRST_STAMPED_VERSION } from "./skill-shape.js";
|
|
15
15
|
import { installedBundleVersion } from "./skills.js";
|
|
16
|
-
import { onboardingReadyReport, observeCloudOnboarding } from "./onboard-ready.js";
|
|
16
|
+
import { onboardingReadyReport, observeCloudOnboarding, } from "./onboard-ready.js";
|
|
17
17
|
import { selectedOnboardTeam } from "./onboard-existing.js";
|
|
18
18
|
import { onboardStatePath, readOnboardJournal } from "./onboard.js";
|
|
19
19
|
export { semverOlder };
|
|
@@ -98,7 +98,11 @@ export function catalystCommandCheck(env, platform = process.platform) {
|
|
|
98
98
|
if (found === null)
|
|
99
99
|
return null;
|
|
100
100
|
if (isThisCliLauncher(found))
|
|
101
|
-
return {
|
|
101
|
+
return {
|
|
102
|
+
id: "command",
|
|
103
|
+
ok: true,
|
|
104
|
+
line: `command: catalyst on PATH is this CLI (${found})`,
|
|
105
|
+
};
|
|
102
106
|
return {
|
|
103
107
|
id: "command",
|
|
104
108
|
ok: false,
|
|
@@ -108,7 +112,9 @@ export function catalystCommandCheck(env, platform = process.platform) {
|
|
|
108
112
|
};
|
|
109
113
|
}
|
|
110
114
|
function nameList(v) {
|
|
111
|
-
return Array.isArray(v)
|
|
115
|
+
return Array.isArray(v)
|
|
116
|
+
? v.filter((n) => typeof n === "string" && n.length > 0)
|
|
117
|
+
: [];
|
|
112
118
|
}
|
|
113
119
|
/** Entries whose name and references are both present; anything malformed is dropped, not guessed. */
|
|
114
120
|
function unresolvedList(v) {
|
|
@@ -119,7 +125,9 @@ function unresolvedList(v) {
|
|
|
119
125
|
return [];
|
|
120
126
|
const { name, references } = u;
|
|
121
127
|
const refs = nameList(references);
|
|
122
|
-
return typeof name === "string" && name.length > 0 && refs.length > 0
|
|
128
|
+
return typeof name === "string" && name.length > 0 && refs.length > 0
|
|
129
|
+
? [{ name, references: refs }]
|
|
130
|
+
: [];
|
|
123
131
|
});
|
|
124
132
|
}
|
|
125
133
|
function unresolvedLine(u) {
|
|
@@ -155,7 +163,10 @@ function teamCheckFix(label, c) {
|
|
|
155
163
|
for (const u of unresolved)
|
|
156
164
|
parts.push(unresolvedLine(u));
|
|
157
165
|
for (const note of Array.isArray(c.repos) ? c.repos : []) {
|
|
158
|
-
if (typeof note !== "object" ||
|
|
166
|
+
if (typeof note !== "object" ||
|
|
167
|
+
note === null ||
|
|
168
|
+
typeof note.repo !== "string" ||
|
|
169
|
+
note.repo.length === 0)
|
|
159
170
|
continue;
|
|
160
171
|
const repoUnresolved = unresolvedList(note.unresolved);
|
|
161
172
|
const skip = new Set(repoUnresolved.map((u) => u.name));
|
|
@@ -171,7 +182,9 @@ function teamCheckFix(label, c) {
|
|
|
171
182
|
}
|
|
172
183
|
function whoCanAnswer(doc) {
|
|
173
184
|
const roles = doc.humans.map((h) => `${h.role} ${h.linearUserId}`);
|
|
174
|
-
return roles.length
|
|
185
|
+
return roles.length
|
|
186
|
+
? roles.join(", ")
|
|
187
|
+
: "a tenant owner or admin (none resolved on the contract)";
|
|
175
188
|
}
|
|
176
189
|
export async function readyReport(ctx, deps) {
|
|
177
190
|
const checks = [];
|
|
@@ -197,21 +210,49 @@ export async function readyReport(ctx, deps) {
|
|
|
197
210
|
? `config: joined ${cfg.name} as ${cfg.user.label} (${cfg.user.role})`
|
|
198
211
|
: `config: joined ${cfg.name} (${cfg.slug}) as ${cfg.principal}`,
|
|
199
212
|
}
|
|
200
|
-
: {
|
|
213
|
+
: {
|
|
214
|
+
id: "config",
|
|
215
|
+
ok: false,
|
|
216
|
+
line: "config: not connected",
|
|
217
|
+
fix: "npx -p @catalyst-cloud/cli catalyst login (keyless; or pass --key / set CATALYST_CLOUD_TOKEN)",
|
|
218
|
+
who: "you (approve the login in your browser)",
|
|
219
|
+
});
|
|
201
220
|
}
|
|
202
221
|
catch (err) {
|
|
203
|
-
checks.push({
|
|
222
|
+
checks.push({
|
|
223
|
+
id: "config",
|
|
224
|
+
ok: false,
|
|
225
|
+
line: `config: ${err instanceof CliError ? err.message : String(err)}`,
|
|
226
|
+
fix: "re-run login to rewrite it",
|
|
227
|
+
who: "you",
|
|
228
|
+
});
|
|
204
229
|
}
|
|
205
230
|
const cache = readContractCache(ctx.home);
|
|
206
231
|
const range = readManifest().tenantContractRange;
|
|
207
232
|
if (!cache) {
|
|
208
|
-
checks.push({
|
|
233
|
+
checks.push({
|
|
234
|
+
id: "contract",
|
|
235
|
+
ok: false,
|
|
236
|
+
line: "contract: not cached",
|
|
237
|
+
fix: "catalyst contract --refresh",
|
|
238
|
+
who: "you",
|
|
239
|
+
});
|
|
209
240
|
}
|
|
210
241
|
else if (contractVersionInRange(cache.contractVersion, range) !== true) {
|
|
211
|
-
checks.push({
|
|
242
|
+
checks.push({
|
|
243
|
+
id: "contract",
|
|
244
|
+
ok: false,
|
|
245
|
+
line: `contract: version ${cache.contractVersion} is outside this bundle's range ${range}`,
|
|
246
|
+
fix: upgradeCommand(),
|
|
247
|
+
who: "you",
|
|
248
|
+
});
|
|
212
249
|
}
|
|
213
250
|
else {
|
|
214
|
-
checks.push({
|
|
251
|
+
checks.push({
|
|
252
|
+
id: "contract",
|
|
253
|
+
ok: true,
|
|
254
|
+
line: `contract: ${cache.contractVersion} cached ${cache.fetchedAt} (range ${range})`,
|
|
255
|
+
});
|
|
215
256
|
}
|
|
216
257
|
// The cloud MAY publish the bundle it expects (catalyst-cloud#3746). Read it defensively: warn (never
|
|
217
258
|
// refuse) when this installed bundle is older than the minimum, and emit nothing when the field is
|
|
@@ -265,7 +306,8 @@ export async function readyReport(ctx, deps) {
|
|
|
265
306
|
// names are always printed rather than hidden behind whichever branch happened to win.
|
|
266
307
|
const found = installedBundleVersion(skillsDir, deps.skillNames);
|
|
267
308
|
const stampBehind = found.version !== null && semverOlder(found.version, pub.latest);
|
|
268
|
-
const unstampedBehind = found.unstamped.length > 0 &&
|
|
309
|
+
const unstampedBehind = found.unstamped.length > 0 &&
|
|
310
|
+
!semverOlder(pub.latest, FIRST_STAMPED_VERSION);
|
|
269
311
|
if (stampBehind || unstampedBehind) {
|
|
270
312
|
const facts = [];
|
|
271
313
|
if (stampBehind)
|
|
@@ -286,7 +328,13 @@ export async function readyReport(ctx, deps) {
|
|
|
286
328
|
if (cfg) {
|
|
287
329
|
checks.push(cfg.cliPath && existsSync(cfg.cliPath)
|
|
288
330
|
? { id: "cliPath", ok: true, line: `cliPath: ${cfg.cliPath}` }
|
|
289
|
-
: {
|
|
331
|
+
: {
|
|
332
|
+
id: "cliPath",
|
|
333
|
+
ok: false,
|
|
334
|
+
line: `cliPath: ${cfg.cliPath ? `${cfg.cliPath} does not exist` : "not recorded"}`,
|
|
335
|
+
fix: "re-run login so the skill scripts can find this CLI",
|
|
336
|
+
who: "you",
|
|
337
|
+
});
|
|
290
338
|
}
|
|
291
339
|
const command = catalystCommandCheck(ctx.env);
|
|
292
340
|
if (command)
|
|
@@ -297,17 +345,36 @@ export async function readyReport(ctx, deps) {
|
|
|
297
345
|
const present = deps.skillNames.filter((n) => existsSync(join(skillsDir, n, "SKILL.md")));
|
|
298
346
|
const missing = deps.skillNames.filter((n) => !present.includes(n));
|
|
299
347
|
if (present.length === 0) {
|
|
300
|
-
checks.push({
|
|
348
|
+
checks.push({
|
|
349
|
+
id: "skills",
|
|
350
|
+
ok: true,
|
|
351
|
+
note: true,
|
|
352
|
+
line: `skills: none copied to ${skillsDir} — you are reading one, so your agent installed them its own way`,
|
|
353
|
+
});
|
|
301
354
|
}
|
|
302
355
|
else if (missing.length === 0) {
|
|
303
|
-
checks.push({
|
|
356
|
+
checks.push({
|
|
357
|
+
id: "skills",
|
|
358
|
+
ok: true,
|
|
359
|
+
line: `skills: all ${deps.skillNames.length} present in ${skillsDir}`,
|
|
360
|
+
});
|
|
304
361
|
}
|
|
305
362
|
else {
|
|
306
|
-
checks.push({
|
|
363
|
+
checks.push({
|
|
364
|
+
id: "skills",
|
|
365
|
+
ok: false,
|
|
366
|
+
line: `skills: ${present.length} of ${deps.skillNames.length} in ${skillsDir}, missing ${missing.join(", ")}`,
|
|
367
|
+
fix: "catalyst install",
|
|
368
|
+
who: "you",
|
|
369
|
+
});
|
|
307
370
|
}
|
|
308
371
|
try {
|
|
309
|
-
await (deps.loadSdk ??
|
|
310
|
-
checks.push({
|
|
372
|
+
await (deps.loadSdk ?? loadHttpSdk)();
|
|
373
|
+
checks.push({
|
|
374
|
+
id: "sdk",
|
|
375
|
+
ok: true,
|
|
376
|
+
line: "sdk: loads (cloud HTTP reads are available)",
|
|
377
|
+
});
|
|
311
378
|
}
|
|
312
379
|
catch (err) {
|
|
313
380
|
checks.push({
|
|
@@ -319,7 +386,12 @@ export async function readyReport(ctx, deps) {
|
|
|
319
386
|
});
|
|
320
387
|
}
|
|
321
388
|
const replica = replicaStatus(ctx, cfg);
|
|
322
|
-
checks.push({
|
|
389
|
+
checks.push({
|
|
390
|
+
id: "replica",
|
|
391
|
+
ok: replica.verdict === "fresh",
|
|
392
|
+
note: true,
|
|
393
|
+
line: readyReplicaLine(replica),
|
|
394
|
+
});
|
|
323
395
|
if (cache) {
|
|
324
396
|
const doc = cache.doc;
|
|
325
397
|
const who = whoCanAnswer(doc);
|
|
@@ -334,22 +406,36 @@ export async function readyReport(ctx, deps) {
|
|
|
334
406
|
if (dg && typeof dg.status === "string") {
|
|
335
407
|
const slots = (dg.missingSlots ?? []).join(", ");
|
|
336
408
|
checks.push(dg.status === "open"
|
|
337
|
-
? {
|
|
409
|
+
? {
|
|
410
|
+
id: `team:${label}:dispatchGate`,
|
|
411
|
+
ok: true,
|
|
412
|
+
line: `team ${label}: dispatch gate open`,
|
|
413
|
+
}
|
|
338
414
|
: {
|
|
339
415
|
id: `team:${label}:dispatchGate`,
|
|
340
416
|
ok: false,
|
|
341
417
|
line: `team ${label}: dispatch gate ${dg.status}${slots ? ` (${slots})` : ""}, blocking`,
|
|
342
|
-
fix: dg.remedy ??
|
|
418
|
+
fix: dg.remedy ??
|
|
419
|
+
`open settings for team ${label} and map its stages`,
|
|
343
420
|
who,
|
|
344
421
|
});
|
|
345
422
|
}
|
|
346
423
|
if (team.readiness.status === "unchecked") {
|
|
347
|
-
checks.push({
|
|
424
|
+
checks.push({
|
|
425
|
+
id: `team:${label}`,
|
|
426
|
+
ok: true,
|
|
427
|
+
note: true,
|
|
428
|
+
line: `team ${label}: readiness not checked yet`,
|
|
429
|
+
});
|
|
348
430
|
continue;
|
|
349
431
|
}
|
|
350
432
|
const bad = team.readiness.checks.filter((c) => c.state !== "pass");
|
|
351
433
|
if (bad.length === 0 && team.readiness.status === "ready") {
|
|
352
|
-
checks.push({
|
|
434
|
+
checks.push({
|
|
435
|
+
id: `team:${label}`,
|
|
436
|
+
ok: true,
|
|
437
|
+
line: `team ${label}: ready`,
|
|
438
|
+
});
|
|
353
439
|
continue;
|
|
354
440
|
}
|
|
355
441
|
for (const c of bad) {
|
|
@@ -365,7 +451,14 @@ export async function readyReport(ctx, deps) {
|
|
|
365
451
|
});
|
|
366
452
|
}
|
|
367
453
|
if (bad.length === 0 && team.readiness.status !== "ready") {
|
|
368
|
-
checks.push({
|
|
454
|
+
checks.push({
|
|
455
|
+
id: `team:${label}`,
|
|
456
|
+
ok: team.readiness.status !== "blocked",
|
|
457
|
+
note: team.readiness.status === "degraded",
|
|
458
|
+
line: `team ${label}: ${team.readiness.status}`,
|
|
459
|
+
fix: `open settings for team ${label}`,
|
|
460
|
+
who,
|
|
461
|
+
});
|
|
369
462
|
}
|
|
370
463
|
}
|
|
371
464
|
}
|
|
@@ -378,10 +471,19 @@ export async function cmdReady(args, ctx, deps) {
|
|
|
378
471
|
if (!onboarding) {
|
|
379
472
|
const cfg = loadConfig(ctx.home);
|
|
380
473
|
const journal = readOnboardJournal(onboardStatePath(ctx.home, ctx.env), readManifest().version, ctx.now());
|
|
381
|
-
const bound = cfg?.user &&
|
|
382
|
-
journal
|
|
474
|
+
const bound = cfg?.user &&
|
|
475
|
+
journal &&
|
|
476
|
+
(journal.account ?? journal.tenant) === cfg.account &&
|
|
477
|
+
journal.membershipId === cfg.user.id &&
|
|
478
|
+
typeof journal.baseUrl === "string" &&
|
|
479
|
+
normalizeBaseUrl(journal.baseUrl) === normalizeBaseUrl(cfg.baseUrl);
|
|
383
480
|
const team = bound ? selectedOnboardTeam(journal) : undefined;
|
|
384
|
-
onboarding = {
|
|
481
|
+
onboarding = {
|
|
482
|
+
teamIds: team ? [team] : undefined,
|
|
483
|
+
localSync: args.flags["local-sync"] === true ||
|
|
484
|
+
Boolean(bound && journal.localSync),
|
|
485
|
+
observe: observeCloudOnboarding,
|
|
486
|
+
};
|
|
385
487
|
}
|
|
386
488
|
const report = await onboardingReadyReport(ctx, onboarding);
|
|
387
489
|
if (args.json)
|
|
@@ -393,9 +495,15 @@ export async function cmdReady(args, ctx, deps) {
|
|
|
393
495
|
ctx.stdout(`Work observed${report.work.ticket ? ` on ${report.work.ticket}` : ""}.`);
|
|
394
496
|
else
|
|
395
497
|
ctx.stdout("Fresh project work has not been verified by this check.");
|
|
396
|
-
ctx.stdout(report.state === "complete"
|
|
498
|
+
ctx.stdout(report.state === "complete"
|
|
499
|
+
? "Onboarding complete."
|
|
500
|
+
: "Setup still needs checks. Resume: catalyst onboard");
|
|
397
501
|
}
|
|
398
|
-
return report.state === "complete"
|
|
502
|
+
return report.state === "complete"
|
|
503
|
+
? 0
|
|
504
|
+
: report.checks.some((check) => check.required && check.state === "fail")
|
|
505
|
+
? 10
|
|
506
|
+
: 11;
|
|
399
507
|
}
|
|
400
508
|
const report = await readyReport(ctx, deps);
|
|
401
509
|
if (args.json) {
|
package/dist/replica.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
import { existsSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
|
|
7
7
|
import { createRequire } from "node:module";
|
|
8
8
|
import { flagBool, flagInt, flagString, positionals } from "./args.js";
|
|
9
|
-
import { apiBase, loadConfig, replicaDbPath } from "./config.js";
|
|
9
|
+
import { apiBase, loadConfig, replicaDbPath, replicaOptedIn } from "./config.js";
|
|
10
10
|
import { detachSelf } from "./detach.js";
|
|
11
11
|
import { CliError, UsageError } from "./errors.js";
|
|
12
12
|
import { apiClient } from "./transport.js";
|
|
@@ -133,11 +133,14 @@ export function readCursor(dbPath) {
|
|
|
133
133
|
db?.close();
|
|
134
134
|
}
|
|
135
135
|
}
|
|
136
|
+
const NOT_CONNECTED = "not connected";
|
|
137
|
+
const CLOUD_READS_LINE = "replica: not configured (cloud reads; local sync is opt-in)";
|
|
136
138
|
/** The in-process status logic `query` and `ready` reuse. */
|
|
137
139
|
export function replicaStatus(ctx, cfg, opts = {}) {
|
|
138
140
|
const base = {
|
|
139
141
|
verdict: "not-configured",
|
|
140
142
|
exitCode: 2,
|
|
143
|
+
configured: false,
|
|
141
144
|
dbPath: null,
|
|
142
145
|
cursor: null,
|
|
143
146
|
heartbeatAgeMs: null,
|
|
@@ -148,7 +151,7 @@ export function replicaStatus(ctx, cfg, opts = {}) {
|
|
|
148
151
|
writer: null,
|
|
149
152
|
};
|
|
150
153
|
if (!cfg)
|
|
151
|
-
return { ...base, reasons: [
|
|
154
|
+
return { ...base, reasons: [NOT_CONNECTED] };
|
|
152
155
|
let dbPath;
|
|
153
156
|
try {
|
|
154
157
|
dbPath = opts.dbPath ?? replicaDbPath(cfg, ctx.home, ctx.env);
|
|
@@ -158,9 +161,10 @@ export function replicaStatus(ctx, cfg, opts = {}) {
|
|
|
158
161
|
return { ...base, reasons: [error.message] };
|
|
159
162
|
throw error;
|
|
160
163
|
}
|
|
164
|
+
const configured = replicaOptedIn(ctx.home, ctx.env);
|
|
161
165
|
const writer = readWriterState(dbPath);
|
|
162
166
|
if (!existsSync(dbPath))
|
|
163
|
-
return { ...base, verdict: "absent", exitCode: 3, dbPath, reasons: ["no replica file"], writer };
|
|
167
|
+
return { ...base, verdict: "absent", exitCode: 3, configured, dbPath, reasons: ["no replica file"], writer };
|
|
164
168
|
const staleMs = opts.staleMs ?? DEFAULT_STALE_MS;
|
|
165
169
|
const nowMs = opts.nowMs ?? ctx.now().getTime();
|
|
166
170
|
const lock = readLock(dbPath);
|
|
@@ -181,6 +185,7 @@ export function replicaStatus(ctx, cfg, opts = {}) {
|
|
|
181
185
|
return {
|
|
182
186
|
verdict: fresh ? "fresh" : "stale",
|
|
183
187
|
exitCode: fresh ? 0 : 1,
|
|
188
|
+
configured,
|
|
184
189
|
dbPath,
|
|
185
190
|
cursor,
|
|
186
191
|
heartbeatAgeMs,
|
|
@@ -194,8 +199,10 @@ export function replicaStatus(ctx, cfg, opts = {}) {
|
|
|
194
199
|
function baseStatusLine(s) {
|
|
195
200
|
switch (s.verdict) {
|
|
196
201
|
case "not-configured":
|
|
197
|
-
return `replica: not configured (${s.reasons.join("; ")})
|
|
202
|
+
return s.reasons[0] === NOT_CONNECTED ? `replica: not configured (${s.reasons.join("; ")})` : CLOUD_READS_LINE;
|
|
198
203
|
case "absent":
|
|
204
|
+
if (!s.configured)
|
|
205
|
+
return CLOUD_READS_LINE;
|
|
199
206
|
return `replica: absent at ${s.dbPath} — start it with: catalyst replica start --detach`;
|
|
200
207
|
case "fresh":
|
|
201
208
|
return `replica: fresh at ${s.dbPath} (cursor ${s.cursor}, heartbeat ${s.heartbeatAgeMs}ms ago${s.lag !== undefined ? `, ${s.lag} behind head ${s.head}` : ""})`;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/** Repository row IDs are opaque cloud identifiers, including tenant-prefixed legacy names.
|
|
2
|
+
* Syntax validation never establishes tenant ownership; callers must join fresh ACL and contract reads. */
|
|
3
|
+
export function isRepositoryId(value) {
|
|
4
|
+
return typeof value === "string" && /^[A-Za-z0-9][A-Za-z0-9._:-]{0,255}$/.test(value);
|
|
5
|
+
}
|
package/dist/sdk.js
CHANGED
|
@@ -4,8 +4,13 @@ import { readManifest } from "./config.js";
|
|
|
4
4
|
import { FIX_COMMAND, supportedRangeText } from "./runtime.js";
|
|
5
5
|
let cached = null;
|
|
6
6
|
let cachedHttp = null;
|
|
7
|
+
let cachedLive = null;
|
|
8
|
+
let cachedEvents = null;
|
|
7
9
|
const realImport = () => import("@catalyst-cloud/sdk/node");
|
|
8
10
|
const realHttpImport = () => import("@catalyst-cloud/sdk");
|
|
11
|
+
// SDK 0.13 has no `./live` entry; its root entry carries LiveSyncClient and no replica code. Point
|
|
12
|
+
// this at `@catalyst-cloud/sdk/live` once the pin reaches 0.14.
|
|
13
|
+
const realLiveImport = () => import("@catalyst-cloud/sdk");
|
|
9
14
|
/** The isomorphic typed HTTP client; keeps the SDK import in this module. */
|
|
10
15
|
export function loadHttpSdk(importer = realHttpImport) {
|
|
11
16
|
if (!cachedHttp) {
|
|
@@ -16,6 +21,16 @@ export function loadHttpSdk(importer = realHttpImport) {
|
|
|
16
21
|
}
|
|
17
22
|
return cachedHttp;
|
|
18
23
|
}
|
|
24
|
+
/** The live push client for `watch`, loaded without the replica-bearing node entry (CTC-4508). */
|
|
25
|
+
export function loadLiveSdk(importer = realLiveImport) {
|
|
26
|
+
if (!cachedLive) {
|
|
27
|
+
cachedLive = importer().catch((err) => {
|
|
28
|
+
cachedLive = null;
|
|
29
|
+
throw new CliError(`the Catalyst Cloud SDK live client could not be loaded: ${err instanceof Error ? err.message : String(err)}`, "sdk-unavailable");
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
return cachedLive;
|
|
33
|
+
}
|
|
19
34
|
/** Import the SDK's node entry, installing the type-stripping loader first. Cached per process. */
|
|
20
35
|
export function loadSdk(importer = realImport) {
|
|
21
36
|
if (!cached) {
|
|
@@ -35,10 +50,28 @@ export function loadSdk(importer = realImport) {
|
|
|
35
50
|
}
|
|
36
51
|
return cached;
|
|
37
52
|
}
|
|
53
|
+
/** Events use the same supported dependency loader; capability markers are checked by the caller. */
|
|
54
|
+
export function loadEventsSdk(importer = () => import("@catalyst-cloud/sdk/events")) {
|
|
55
|
+
if (!cachedEvents) {
|
|
56
|
+
cachedEvents = (async () => {
|
|
57
|
+
installTsDepsLoader();
|
|
58
|
+
try {
|
|
59
|
+
return await importer();
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
cachedEvents = null;
|
|
63
|
+
throw new CliError("the Catalyst Cloud event SDK could not be loaded; reinstall the skills bundle", "sdk-unavailable");
|
|
64
|
+
}
|
|
65
|
+
})();
|
|
66
|
+
}
|
|
67
|
+
return cachedEvents;
|
|
68
|
+
}
|
|
38
69
|
/** Test seam: forget the cached import. */
|
|
39
70
|
export function resetSdkCache() {
|
|
40
71
|
cached = null;
|
|
41
72
|
cachedHttp = null;
|
|
73
|
+
cachedLive = null;
|
|
74
|
+
cachedEvents = null;
|
|
42
75
|
}
|
|
43
76
|
/** HTTP tenant methods live on the SDK's root entry, separate from replica/node exports. */
|
|
44
77
|
export async function loadTenantSdk() {
|
package/dist/watch.js
CHANGED
|
@@ -9,7 +9,7 @@ import { loadContract } from "./contract.js";
|
|
|
9
9
|
import { CliError, UsageError } from "./errors.js";
|
|
10
10
|
import { apiClient } from "./transport.js";
|
|
11
11
|
import { authStrategyFor, bearerFor } from "./oauth.js";
|
|
12
|
-
import {
|
|
12
|
+
import { loadLiveSdk } from "./sdk.js";
|
|
13
13
|
import { createLiveEventsClient } from "./watch/consumer.js";
|
|
14
14
|
import { CursorFileError } from "./watch/cursor-file.js";
|
|
15
15
|
const ROW_ISSUE_REFS = ["identifier", "linear_issue_identifier", "issue_identifier", "issue_id", "linear_issue_id"];
|
|
@@ -109,7 +109,7 @@ export function execReaction(cmd, env) {
|
|
|
109
109
|
});
|
|
110
110
|
}
|
|
111
111
|
export async function runWatch(ctx, cfg, opts, deps = {}) {
|
|
112
|
-
const sdk = deps.sdk ?? (await
|
|
112
|
+
const sdk = deps.sdk ?? (await loadLiveSdk());
|
|
113
113
|
const resolve = deps.resolver ?? apiIssueResolver(cfg, ctx);
|
|
114
114
|
const react = opts.exec ? execReaction(opts.exec, ctx.env) : undefined;
|
|
115
115
|
let handle;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@catalyst-cloud/cli",
|
|
3
|
-
"version": "0.14.
|
|
3
|
+
"version": "0.14.8",
|
|
4
4
|
"description": "Customer skill bundle for Catalyst Cloud, named from the executive's seat \u2014 get set up, see what is happening, see what needs you, run a project, unstick what is stuck \u2014 plus the catalyst CLI that reads through the Catalyst Cloud SDK, writes through the agent proxy, and connects your machine to your cloud account with one command. The skills directory is the roster; this line never counts it.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -46,7 +46,8 @@
|
|
|
46
46
|
"skills:scan": "node scripts/scan-skills.mjs",
|
|
47
47
|
"evals": "node scripts/run-evals.mjs",
|
|
48
48
|
"paths:check": "node scripts/vendor-paths.mjs --check",
|
|
49
|
-
"prose:check": "node scripts/check-public-prose.mjs"
|
|
49
|
+
"prose:check": "node scripts/check-public-prose.mjs",
|
|
50
|
+
"settings:check": "node scripts/vendor-settings.mjs --check"
|
|
50
51
|
},
|
|
51
52
|
"dependencies": {
|
|
52
53
|
"@catalyst-cloud/sdk": "^0.13.1",
|
|
@@ -4,7 +4,7 @@ description: >-
|
|
|
4
4
|
Catalyst's GitHub: show a ticket's pull request with its checks, reviews and review threads, say whether it is mergeable under this repository's policy, and explain what a PR accumulates as the ticket moves (the branch, the draft, the rewrite, the force-pushes, the labels, the queue). Use when someone asks "show me the PR", "what are the checks saying", "why hasn't it merged", "what does the queue need", "what does that label mean", or wants the branch and merge conventions Catalyst follows.
|
|
5
5
|
allowed-tools: Bash(catalyst:*) Bash(npx -p @catalyst-cloud/cli catalyst:*)
|
|
6
6
|
---
|
|
7
|
-
<!-- vendored-from: @catalyst-cloud/catalyst-skills@0.14.
|
|
7
|
+
<!-- vendored-from: @catalyst-cloud/catalyst-skills@0.14.8 — written in this repository for customer accounts -->
|
|
8
8
|
|
|
9
9
|
# Catalyst's GitHub
|
|
10
10
|
|
|
@@ -15,7 +15,7 @@ You answer for the pull requests Catalyst opens and moves. The person asking wan
|
|
|
15
15
|
- `node scripts/read-pr.mjs --help`: a ticket's PR (or a PR by node id): state, branch, head, linked ticket, GitHub's mergeable state, every check, status and review. `--all` lists every PR for a ticket.
|
|
16
16
|
- `node scripts/is-it-mergeable.mjs --help`: the three legs (checks, reviewer signal, unresolved threads) judged under the repository's policy, plus the prerequisites; exit 1 when a leg is red.
|
|
17
17
|
|
|
18
|
-
Both read through `catalyst query
|
|
18
|
+
Both read through `catalyst query` and `contract` from the cloud by default; the first stderr line names the source, and your answer repeats it.
|
|
19
19
|
|
|
20
20
|
## Load on demand
|
|
21
21
|
|
|
@@ -23,6 +23,8 @@ Both read through `catalyst query`, `contract` and, when fresh, `replica`; the f
|
|
|
23
23
|
| -- | -- |
|
|
24
24
|
| the person asks what Catalyst did to the branch or PR, why the title changed, why history was rewritten, what a label means, or what happens on merge | `references/what-a-pr-accumulates.md` |
|
|
25
25
|
| the person asks why a PR has not merged, what the queue needs, what a clean review pass looks like, or what a policy requires | `references/is-it-mergeable.md` |
|
|
26
|
+
| which command answers a PR question from the cloud, and what the cloud detail cannot see | `references/reading-from-the-cloud.md` |
|
|
27
|
+
| local SQL or `--local-threads`, only when `catalyst replica status --json` reports `configured: true` | `references/local-replica.md` |
|
|
26
28
|
|
|
27
29
|
## Rules
|
|
28
30
|
|
|
@@ -42,7 +42,7 @@ The mirrored PR detail carries checks, legacy commit statuses, review objects wi
|
|
|
42
42
|
|
|
43
43
|
- checks: judged fully from the detail.
|
|
44
44
|
- reviewer: reported as inconclusive unless the policy waives it. The script says whether the reviewer left review objects, but whether a clean-pass reaction followed, and whether those reviews stand at the current head or an earlier one, is not mirrored.
|
|
45
|
-
- threads:
|
|
45
|
+
- threads: inconclusive when cloud PR detail carries no thread count. The optional `--local-threads` reads local review-thread rows only after the replica proves it is caught up to a probed cloud head. Thread ancestry against force-pushes is judged by the cloud, not here.
|
|
46
46
|
|
|
47
47
|
The exit code is 1 only for a proven-red leg or prerequisite. An inconclusive leg leaves exit 0 and a verdict that says "not proven mergeable from this read". The cloud's own evaluator, which holds the reaction and the ancestry, is the authority; a queue-ready label on the PR means it already said yes.
|
|
48
48
|
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# Reading pull requests from the local replica (opt-in)
|
|
2
|
+
|
|
3
|
+
Load this only when `catalyst replica status --json` reports `configured: true`. On any other machine, read from the cloud (`references/reading-from-the-cloud.md`) and do not mention the replica.
|
|
4
|
+
|
|
5
|
+
The replica is an on-machine SQLite copy of the account. It exists only when the person opted in to local sync. It adds ad hoc SQL over mirrored pull requests and one leg the cloud PR detail lacks: review-thread rows. It is never required.
|
|
6
|
+
|
|
7
|
+
## Check it is caught up first
|
|
8
|
+
|
|
9
|
+
Run `catalyst replica status --probe --json` before every local read. Read locally only when `verdict` is `fresh` and `cursor` equals `head`. `stale` or `absent` with `configured: true` means the person opted in and the writer is down. Answer from the cloud and say the local copy is behind.
|
|
10
|
+
|
|
11
|
+
Quote the cursor with any local answer, for example "from the local replica at cursor 4810".
|
|
12
|
+
|
|
13
|
+
## Threads: the one leg only the replica adds
|
|
14
|
+
|
|
15
|
+
**Zero thread rows does not mean zero open threads.** The thread table is filled only by resolve and unresolve events, and nothing seeds it from GitHub. A thread opened and never touched leaves no row. So zero rows can mean every thread is still open. A local "all resolved" answers only "of the threads seen resolving, which are resolved now". It never replaces the cloud's merge evaluator, which also judges a resolution against force-pushes.
|
|
16
|
+
|
|
17
|
+
`node scripts/is-it-mergeable.mjs <ticket> --local-threads` reads the thread leg from the replica. It does so only after a probe proves the replica is caught up with the cloud head. Without the flag, the thread leg is inconclusive and the cloud's evaluator decides.
|
|
18
|
+
|
|
19
|
+
The query the script runs, for one pull request:
|
|
20
|
+
|
|
21
|
+
```sql
|
|
22
|
+
select resolved, count(*) as n from pr_review_threads
|
|
23
|
+
where repo_id = 'acme/widgets' and pr_number = 42
|
|
24
|
+
group by resolved
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
`resolved` is 1 for resolved, 0 for unresolved, and null when no flag was mirrored.
|
|
28
|
+
|
|
29
|
+
## Running SQL yourself
|
|
30
|
+
|
|
31
|
+
`catalyst replica sql "<one SELECT>" --json` runs one read-only `SELECT` and prints rows. `catalyst replica schema <table>` prints a table's columns; read them before querying a table this page does not cover. Timestamps are milliseconds since the epoch.
|
|
32
|
+
|
|
33
|
+
| table | one row is | key |
|
|
34
|
+
| -- | -- | -- |
|
|
35
|
+
| `pull_requests` | a pull request: `state`, `draft`, `merged`, `merged_at`, `head_sha`, `head_ref`, `base_ref`, `mergeable`, `mergeable_state`, `auto_merge`, `linear_issue_identifier` | `repo_id`, `number`; also `node_id` |
|
|
36
|
+
| `check_runs` | one check at one commit: `name`, `status`, `conclusion` | `repo_id`, `head_sha` |
|
|
37
|
+
| `commit_statuses` | a legacy commit status: `context`, `state` | `repo_id`, `sha` |
|
|
38
|
+
| `reviews` | a submitted review: `user_id`, `state`, `submitted_at` | `repo_id`, `pr_number` |
|
|
39
|
+
| `pr_review_threads` | a review thread seen resolving: `resolved`, `resolved_at` | `repo_id`, `pr_number` |
|
|
40
|
+
| `pr_review_comments`, `pr_conversation_comments` | an inline comment, and a PR conversation comment | `repo_id`, `pr_number` |
|
|
41
|
+
|
|
42
|
+
Replace `acme/widgets`, `42` and `ENG-123` with real values.
|
|
43
|
+
|
|
44
|
+
Every pull request that names a ticket:
|
|
45
|
+
|
|
46
|
+
```sql
|
|
47
|
+
select repo_id, number, state, draft, merged, head_sha, mergeable_state
|
|
48
|
+
from pull_requests where linear_issue_identifier = 'ENG-123'
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
The checks at a pull request's current head:
|
|
52
|
+
|
|
53
|
+
```sql
|
|
54
|
+
select c.name, c.status, c.conclusion
|
|
55
|
+
from check_runs c join pull_requests p
|
|
56
|
+
on p.repo_id = c.repo_id and p.head_sha = c.head_sha
|
|
57
|
+
where p.repo_id = 'acme/widgets' and p.number = 42
|
|
58
|
+
order by c.name
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
The reviews on a pull request, newest first:
|
|
62
|
+
|
|
63
|
+
```sql
|
|
64
|
+
select user_id, state, submitted_at from reviews
|
|
65
|
+
where repo_id = 'acme/widgets' and pr_number = 42
|
|
66
|
+
order by submitted_at desc
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Open pull requests that GitHub reports in conflict:
|
|
70
|
+
|
|
71
|
+
```sql
|
|
72
|
+
select repo_id, number, linear_issue_identifier from pull_requests
|
|
73
|
+
where state = 'open' and mergeable_state = 'dirty'
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## What the replica does not answer
|
|
77
|
+
|
|
78
|
+
Reactions, PR labels and the commit each review was submitted against are not mirrored anywhere a read can reach. The cloud's evaluator holds them. `references/is-it-mergeable.md` explains what that leaves inconclusive.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Reading pull request facts from the cloud
|
|
2
|
+
|
|
3
|
+
Cloud reads are the default on every machine. They need no local writer and no local database. Every command below reads the cloud through the `catalyst` CLI.
|
|
4
|
+
|
|
5
|
+
| question | command |
|
|
6
|
+
| -- | -- |
|
|
7
|
+
| Which pull requests name this ticket? | `catalyst query pulls --ticket <KEY-123>` |
|
|
8
|
+
| What state is the PR in: draft, merged, conflict, auto-merge? | `node scripts/read-pr.mjs <KEY-123>` |
|
|
9
|
+
| Which checks are red or still running at the head? | `node scripts/read-pr.mjs <KEY-123>` |
|
|
10
|
+
| Is it mergeable under this repository's policy? | `node scripts/is-it-mergeable.mjs <KEY-123>`, which also reads `catalyst contract --path merge` |
|
|
11
|
+
| Which stage is the linked ticket in? | `catalyst query issue <KEY-123>` |
|
|
12
|
+
| What did the last pr or merge phase do? | `catalyst history <KEY-123>` |
|
|
13
|
+
|
|
14
|
+
The first stderr line names the source: `source: api (...)`. Quote it when freshness matters.
|
|
15
|
+
|
|
16
|
+
## What the cloud PR detail does not carry
|
|
17
|
+
|
|
18
|
+
The detail has no review-thread count. So the thread leg of `is-it-mergeable.mjs` is inconclusive from a cloud read, and the cloud's own merge evaluator decides. A queue-ready label on the PR means it already said yes. Reactions, PR labels and the commit each review was submitted against are not in the detail either.
|
|
19
|
+
|
|
20
|
+
A smaller ticket read is coming: the cloud will answer with only the status fields and the linked pull requests. No CLI flag selects it yet, so read the full record.
|
|
21
|
+
|
|
22
|
+
## The local replica
|
|
23
|
+
|
|
24
|
+
Only on a machine where `catalyst replica status --json` reports `configured: true`, `references/local-replica.md` adds local SQL and the opt-in `--local-threads` read. Everywhere else, these cloud reads are the whole answer.
|
|
@@ -58,4 +58,4 @@ The ticket moves to Done when the real merge webhook arrives, keyed to the pull
|
|
|
58
58
|
|
|
59
59
|
## Reading a PR through this skill
|
|
60
60
|
|
|
61
|
-
`node scripts/read-pr.mjs <ticket>` follows the ticket's own record to the pull requests that name it, picks the open one (else the merged one, else the newest), and prints the mirrored detail: state, draft and merged flags, branch and base, head commit, the linked ticket and its stage, GitHub's own mergeable state and auto-merge flag, every check with status and conclusion, legacy commit statuses, and every mirrored review with its state. `--all` lists every PR for the ticket; `--json` prints the raw document.
|
|
61
|
+
`node scripts/read-pr.mjs <ticket>` follows the ticket's own record to the pull requests that name it, picks the open one (else the merged one, else the newest), and prints the mirrored detail: state, draft and merged flags, branch and base, head commit, the linked ticket and its stage, GitHub's own mergeable state and auto-merge flag, every check with status and conclusion, legacy commit statuses, and every mirrored review with its state. `--all` lists every PR for the ticket; `--json` prints the raw document. Reads use the cloud by default. Repeat the source from the first stderr line.
|