@catalyst-cloud/cli 0.9.4 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/README.md +38 -32
  3. package/dist/args.js +16 -2
  4. package/dist/ask.js +2 -2
  5. package/dist/capabilities.js +70 -0
  6. package/dist/cli.js +42 -29
  7. package/dist/config.js +1 -1
  8. package/dist/connections.js +5 -5
  9. package/dist/env/declaration-rules.js +163 -0
  10. package/dist/env/group.js +51 -0
  11. package/dist/env/inventory.js +102 -0
  12. package/dist/env/render.js +39 -0
  13. package/dist/env/scan-env-example.js +62 -0
  14. package/dist/env/scan-source.js +43 -0
  15. package/dist/env/scan-workflow.js +123 -0
  16. package/dist/env/scan-wrangler.js +120 -0
  17. package/dist/env/types.js +1 -0
  18. package/dist/env/walk.js +87 -0
  19. package/dist/env.js +76 -0
  20. package/dist/environment.js +4 -4
  21. package/dist/events.js +1 -1
  22. package/dist/execution.js +3 -2
  23. package/dist/identity.js +3 -3
  24. package/dist/oauth.js +3 -3
  25. package/dist/query.js +27 -1
  26. package/dist/ready.js +3 -3
  27. package/dist/release.js +1 -1
  28. package/dist/replica.js +9 -7
  29. package/dist/runtime-store.js +2 -2
  30. package/dist/runtime.js +1 -1
  31. package/dist/secret.js +1 -1
  32. package/dist/skills.js +1 -1
  33. package/dist/team.js +329 -0
  34. package/package.json +4 -4
  35. package/skills/catalyst-github/SKILL.md +1 -1
  36. package/skills/catalyst-linear/SKILL.md +1 -1
  37. package/skills/catalyst-onboard/SKILL.md +7 -7
  38. package/skills/catalyst-onboard/references/choosing-a-coding-account.md +2 -2
  39. package/skills/catalyst-onboard/references/declaring-a-repository.md +1 -1
  40. package/skills/catalyst-onboard/references/local-sync.md +3 -3
  41. package/skills/catalyst-onboard/references/skill-sources.md +1 -1
  42. package/skills/catalyst-onboard/references/the-one-path.md +12 -10
  43. package/skills/catalyst-onboard/references/what-a-phase-needs.md +5 -5
  44. package/skills/catalyst-onboard/references/what-the-browser-owns.md +6 -9
  45. package/skills/catalyst-onboard/references/who-fixes-what.md +35 -16
  46. package/skills/catalyst-onboard/scripts/local-sync.mjs +6 -6
  47. package/skills/catalyst-onboard/scripts/where-am-i.mjs +133 -54
  48. package/skills/catalyst-setup/SKILL.md +1 -1
  49. package/skills/connect-me/SKILL.md +3 -3
  50. package/skills/how-catalyst-works/SKILL.md +5 -3
  51. package/skills/how-catalyst-works/references/settings-and-where-they-live.md +20 -0
  52. package/skills/how-catalyst-works/references/what-runs-next.md +19 -1
  53. package/skills/run-this-project/SKILL.md +1 -1
  54. package/skills/unstick/SKILL.md +1 -1
  55. package/skills/what-needs-me/SKILL.md +1 -1
  56. package/skills/what-needs-me/references/reading-the-inbox.md +4 -0
  57. package/skills/what-this-repo-needs/SKILL.md +40 -0
  58. package/skills/what-this-repo-needs/agents/openai.yaml +6 -0
  59. package/skills/what-this-repo-needs/agents/portability.yaml +4 -0
  60. package/skills/what-this-repo-needs/references/what-each-group-means.md +68 -0
  61. package/skills/what-this-repo-needs/scripts/check.mjs +64 -0
  62. package/skills/what-this-repo-needs/scripts/inventory.mjs +61 -0
  63. package/skills/what-this-repo-needs/scripts/lib/cli.mjs +114 -0
  64. package/skills/what-this-repo-needs/scripts/lib/credential.mjs +29 -0
  65. package/skills/whats-happening/SKILL.md +1 -1
  66. package/skills/whats-happening/references/reading-the-board.md +1 -1
  67. package/skills/whats-happening/references/reprioritising.md +1 -1
  68. package/skills/whats-happening/references/status-reply.md +2 -0
  69. package/skills/whats-happening/references/why-is-it-stuck.md +41 -33
@@ -0,0 +1,120 @@
1
+ const BINDING_KEY_BY_TABLE = {
2
+ kv_namespaces: "binding",
3
+ d1_databases: "binding",
4
+ r2_buckets: "binding",
5
+ "durable_objects.bindings": "name",
6
+ "queues.producers": "binding",
7
+ "queues.consumers": "binding",
8
+ };
9
+ // ⛔ TABLE_RE AND QUOTED_RE ARE END-ANCHORED, SO AN INLINE `#` COMMENT MUST BE STRIPPED FIRST
10
+ // (validate attempt 29, CR-1 / CR-4). `[vars] # public settings` never matched TABLE_RE, so
11
+ // `currentTable` stayed null and the scanner returned NOTHING for the whole file — zero bindings,
12
+ // zero vars, zero notes: a silent, total failure in a tool whose entire value is completeness. And
13
+ // `binding = "SESSIONS" # the session store` never matched QUOTED_RE, so the `: value` fallback
14
+ // below made the raw remainder — quotes, comment and all — the binding NAME the customer is asked to
15
+ // keep, drop or move. `stripInlineComment` is quote-aware, so a `#` inside a value (`id = "ab#cd"`)
16
+ // is still value material and is left alone.
17
+ const QUOTED_RE = /^"(.*)"$|^'(.*)'$/;
18
+ const KEY_VALUE_RE = /^([A-Za-z_][A-Za-z0-9_-]*)\s*=\s*(.*)$/;
19
+ const TABLE_RE = /^\[\[?([^\]]+)\]\]?$/;
20
+ const ENV_TABLE_RE = /^env\.([^.]+)\.(.+)$/;
21
+ export function scanWrangler(file, text) {
22
+ const sightings = [];
23
+ const lines = text.split("\n");
24
+ let currentTable = null;
25
+ let currentEnvironment;
26
+ // Tracks an open triple-quoted string, so a line INSIDE one (e.g. `a = not_a_name`) is never
27
+ // mined for a key — the key that opened the string (e.g. `MULTI = """`) already was.
28
+ let insideMultiline = null;
29
+ for (let i = 0; i < lines.length; i++) {
30
+ const trimmed = lines[i].trim();
31
+ if (insideMultiline) {
32
+ if (trimmed.includes(insideMultiline))
33
+ insideMultiline = null;
34
+ continue;
35
+ }
36
+ if (trimmed === "" || trimmed.startsWith("#"))
37
+ continue;
38
+ const uncommented = stripInlineComment(trimmed).trim();
39
+ if (uncommented === "")
40
+ continue;
41
+ const tableMatch = TABLE_RE.exec(uncommented);
42
+ if (tableMatch) {
43
+ let name = tableMatch[1].trim();
44
+ currentEnvironment = undefined;
45
+ const envMatch = ENV_TABLE_RE.exec(name);
46
+ if (envMatch) {
47
+ currentEnvironment = envMatch[1];
48
+ name = envMatch[2];
49
+ }
50
+ currentTable = name;
51
+ continue;
52
+ }
53
+ if (currentTable === null)
54
+ continue;
55
+ const kv = KEY_VALUE_RE.exec(uncommented);
56
+ if (!kv)
57
+ continue;
58
+ const key = kv[1];
59
+ const value = kv[2].trim();
60
+ if (isOpeningMultiline(value, '"""'))
61
+ insideMultiline = '"""';
62
+ else if (isOpeningMultiline(value, "'''"))
63
+ insideMultiline = "'''";
64
+ if (currentTable === "vars" || currentTable.endsWith(".vars")) {
65
+ sightings.push({
66
+ name: key,
67
+ finding: { file, line: i + 1, consumer: "wrangler.toml vars", consumerKind: "vars" },
68
+ isWranglerVar: true,
69
+ wranglerEnvironment: currentEnvironment,
70
+ localSource: "value committed in wrangler.toml — nothing to declare",
71
+ });
72
+ continue;
73
+ }
74
+ const bindingKey = BINDING_KEY_BY_TABLE[currentTable];
75
+ if (bindingKey && key === bindingKey) {
76
+ const qm = QUOTED_RE.exec(value);
77
+ const bindingName = qm ? (qm[1] ?? qm[2] ?? "") : value;
78
+ if (bindingName) {
79
+ sightings.push({
80
+ name: bindingName,
81
+ finding: { file, line: i + 1, consumer: `${currentTable} binding`, consumerKind: currentTable },
82
+ isBinding: true,
83
+ wranglerEnvironment: currentEnvironment,
84
+ localSource: "Cloudflare binding",
85
+ });
86
+ }
87
+ }
88
+ }
89
+ return sightings;
90
+ }
91
+ /**
92
+ * The line with any TOML inline comment removed. A `#` only starts a comment outside a string, so
93
+ * the scan tracks the quote it is inside. A line whose quotes do not balance — a `"""` opener, most
94
+ * importantly — is returned untouched rather than guessed at.
95
+ */
96
+ function stripInlineComment(line) {
97
+ let quote = null;
98
+ for (let i = 0; i < line.length; i++) {
99
+ const ch = line[i];
100
+ if (quote !== null) {
101
+ if (ch === "\\")
102
+ i++;
103
+ else if (ch === quote)
104
+ quote = null;
105
+ continue;
106
+ }
107
+ if (ch === '"' || ch === "'") {
108
+ quote = ch;
109
+ continue;
110
+ }
111
+ if (ch === "#")
112
+ return line.slice(0, i);
113
+ }
114
+ return line;
115
+ }
116
+ function isOpeningMultiline(value, marker) {
117
+ if (!value.startsWith(marker))
118
+ return false;
119
+ return !(value.length > marker.length && value.endsWith(marker));
120
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,87 @@
1
+ // walk.ts — the one definition of "the files this repository is made of", shared by every scanner
2
+ // that needs to enumerate a tree. Skips the directories no scan should ever descend into.
3
+ //
4
+ // ⛔ A SYMLINK IS REPOSITORY CONTENT, AND REPOSITORY CONTENT IS UNTRUSTED (validate attempt 29,
5
+ // M-2 / CR-5). `statSync` FOLLOWS a link, so the previous walk had no way to tell a real directory
6
+ // from a link out of the tree, and no visited set to stop a cycle. Measured: a single `pkg/up -> ..`
7
+ // link reported one name 41 times with absurd file:line pairs, and a committed `elsewhere -> /tmp/x`
8
+ // walked a directory outside the repository entirely. So every entry is classified with a DIRENT
9
+ // (never a followed stat), and a link is followed only when its real path is inside the repository
10
+ // root and has not been walked before.
11
+ import { readdirSync, realpathSync, statSync } from "node:fs";
12
+ import { join, relative, resolve, sep } from "node:path";
13
+ export const SKIP_DIRS = new Set(["node_modules", "dist", "coverage", ".git"]);
14
+ /** Every file under `root`, relative paths, POSIX-separated, skipping SKIP_DIRS. */
15
+ export function walkRepo(root) {
16
+ const out = [];
17
+ let realRoot;
18
+ try {
19
+ realRoot = realpathSync(root);
20
+ }
21
+ catch {
22
+ return out;
23
+ }
24
+ // Real paths already descended into. Seeded with the root so a link back to it is a cycle.
25
+ const walked = new Set([realRoot]);
26
+ const stack = [root];
27
+ while (stack.length > 0) {
28
+ const dir = stack.pop();
29
+ let entries;
30
+ try {
31
+ entries = readdirSync(dir, { withFileTypes: true });
32
+ }
33
+ catch {
34
+ continue;
35
+ }
36
+ for (const entry of entries) {
37
+ const name = entry.name;
38
+ const full = join(dir, name);
39
+ let isDirectory = entry.isDirectory();
40
+ if (entry.isSymbolicLink()) {
41
+ let real;
42
+ try {
43
+ real = realpathSync(full);
44
+ }
45
+ catch {
46
+ continue;
47
+ }
48
+ if (!contains(realRoot, real))
49
+ continue; // points out of the repository
50
+ try {
51
+ isDirectory = statSync(full).isDirectory();
52
+ }
53
+ catch {
54
+ continue;
55
+ }
56
+ }
57
+ if (isDirectory) {
58
+ if (SKIP_DIRS.has(name))
59
+ continue;
60
+ // Every directory is keyed by its REAL path, link or not: `pkg/up -> pkg` is a cycle only
61
+ // because the link and the directory resolve to the same place, and checking the link alone
62
+ // would still walk the tree one extra time under its second name.
63
+ let real;
64
+ try {
65
+ real = realpathSync(full);
66
+ }
67
+ catch {
68
+ continue;
69
+ }
70
+ if (walked.has(real))
71
+ continue; // a cycle, or a second route to the same tree
72
+ walked.add(real);
73
+ stack.push(full);
74
+ }
75
+ else {
76
+ out.push(relative(root, full).split("\\").join("/"));
77
+ }
78
+ }
79
+ }
80
+ return out.sort();
81
+ }
82
+ /** True when `child` is `root` itself or lies beneath it. Both must already be real paths. */
83
+ export function contains(root, child) {
84
+ const a = resolve(root);
85
+ const b = resolve(child);
86
+ return b === a || b.startsWith(a.endsWith(sep) ? a : a + sep);
87
+ }
package/dist/env.js ADDED
@@ -0,0 +1,76 @@
1
+ // env.ts — `catalyst env inventory` / `env check`: a LOCAL, OFFLINE, repo-scoped reporter and
2
+ // validator. Unlike every other verb in this file's siblings, `cmdEnv` never calls `requireConfig` or
3
+ // `apiClient` — it needs neither a login nor a network call, which is the whole point of the feature:
4
+ // a person reviews what a repository declares without connecting anything first.
5
+ import { readFileSync, statSync } from "node:fs";
6
+ import { resolve } from "node:path";
7
+ import { positionals } from "./args.js";
8
+ import { validateDeclaration } from "./env/declaration-rules.js";
9
+ import { inventoryRepo } from "./env/inventory.js";
10
+ import { inventoryToJson, renderCheck, renderInventory } from "./env/render.js";
11
+ import { UsageError } from "./errors.js";
12
+ export async function cmdEnv(args, ctx, deps = {}) {
13
+ const [sub, ...rest] = positionals(args);
14
+ if (sub === undefined)
15
+ throw new UsageError("env needs a subcommand: inventory | check");
16
+ if (sub === "inventory") {
17
+ if (rest.length > 1)
18
+ throw new UsageError(`env inventory takes at most one path (got an extra "${rest[1]}")`);
19
+ const root = resolve(rest[0] ?? ".");
20
+ // C-3: walkRepo swallows a readdir failure and returns [], so without this an unreadable or
21
+ // mistyped path printed three empty groups and exited 0 — a typo that reads as "declares nothing".
22
+ let stat;
23
+ try {
24
+ stat = statSync(root);
25
+ }
26
+ catch {
27
+ ctx.stderr(`${root} does not exist — env inventory takes a path to a repository`);
28
+ return 1;
29
+ }
30
+ if (!stat.isDirectory()) {
31
+ ctx.stderr(`${root} is not a directory — env inventory takes a path to a repository`);
32
+ return 1;
33
+ }
34
+ const inv = inventoryRepo(root, deps);
35
+ if (args.json)
36
+ ctx.stdout(JSON.stringify(inventoryToJson(inv)));
37
+ else
38
+ for (const line of renderInventory(inv))
39
+ ctx.stdout(line);
40
+ return 0;
41
+ }
42
+ if (sub === "check") {
43
+ const file = rest[0];
44
+ if (!file)
45
+ throw new UsageError("env check needs a file: catalyst env check <path to catalyst.env.json>");
46
+ if (rest.length > 1)
47
+ throw new UsageError(`env check takes exactly one file (got an extra "${rest[1]}")`);
48
+ let text;
49
+ try {
50
+ text = readFileSync(file, "utf8");
51
+ }
52
+ catch (err) {
53
+ ctx.stderr(`could not read ${file}: ${err instanceof Error ? err.message : String(err)}`);
54
+ return 1;
55
+ }
56
+ let doc;
57
+ try {
58
+ doc = JSON.parse(text);
59
+ }
60
+ catch {
61
+ // C-4/S-2: JSON.parse's own message embeds the first ten characters of the input, so
62
+ // forwarding it prints content from the file — on a verb documented as never printing a value.
63
+ ctx.stderr(`${file} is not valid JSON`);
64
+ return 1;
65
+ }
66
+ const errors = validateDeclaration(doc);
67
+ const state = errors.length === 0 ? "valid" : "invalid";
68
+ if (args.json)
69
+ ctx.stdout(JSON.stringify({ state, errors }));
70
+ else
71
+ for (const line of renderCheck({ state, errors }, file))
72
+ ctx.stdout(line);
73
+ return state === "valid" ? 0 : 1;
74
+ }
75
+ throw new UsageError(`unknown env subcommand "${sub}": inventory | check`);
76
+ }
@@ -131,7 +131,7 @@ export async function cmdEnvironment(args, ctx, deps = {}) {
131
131
  if (body.reason)
132
132
  ctx.stdout(` reason: ${body.reason}`);
133
133
  if (body.currentRevision !== undefined) {
134
- ctx.stdout(` the declaration is now at revision ${body.currentRevision} — run \`catalyst-skills environment read\` and propose again from that`);
134
+ ctx.stdout(` the declaration is now at revision ${body.currentRevision} — run \`catalyst environment read\` and propose again from that`);
135
135
  }
136
136
  }
137
137
  return 1;
@@ -144,14 +144,14 @@ export async function cmdEnvironment(args, ctx, deps = {}) {
144
144
  if (args.json)
145
145
  ctx.stdout(JSON.stringify(body));
146
146
  else if (body.status !== "unchanged")
147
- ctx.stdout("not delivered until it is approved: catalyst-skills environment approve");
147
+ ctx.stdout("not delivered until it is approved: catalyst environment approve");
148
148
  return 0;
149
149
  }
150
150
  // ⭐ Approve exactly what propose just returned. The whole point of the CAS is that the approver
151
151
  // saw the revision they are approving, and nothing here has to be copied by a human or a model.
152
152
  const state = body.state;
153
153
  if (!state) {
154
- ctx.stderr("the cloud returned no state to approve — run: catalyst-skills environment read");
154
+ ctx.stderr("the cloud returned no state to approve — run: catalyst environment read");
155
155
  return 1;
156
156
  }
157
157
  const approved = await approveExact(api, routes, state.revision, state.canonicalHash);
@@ -172,7 +172,7 @@ export async function cmdEnvironment(args, ctx, deps = {}) {
172
172
  if (revision === undefined || hash === undefined) {
173
173
  const current = (await readState(api, routes)).current;
174
174
  if (current === null || current === undefined) {
175
- ctx.stdout("there is no declaration to approve — propose one first: catalyst-skills environment propose --file <path>");
175
+ ctx.stdout("there is no declaration to approve — propose one first: catalyst environment propose --file <path>");
176
176
  return 1;
177
177
  }
178
178
  revision = current.revision;
package/dist/events.js CHANGED
@@ -71,7 +71,7 @@ export async function cmdEvents(args, ctx, deps = {}) {
71
71
  if (controller.signal.aborted)
72
72
  return sub === "wait-for" ? 1 : 0;
73
73
  if (error.code === "ENOENT")
74
- throw new CliError(`event cache is absent at ${directory} — start it with: catalyst-skills replica start --detach`, "events-absent", 3);
74
+ throw new CliError(`event cache is absent at ${directory} — start it with: catalyst replica start --detach`, "events-absent", 3);
75
75
  throw error;
76
76
  }
77
77
  finally {
package/dist/execution.js CHANGED
@@ -37,14 +37,15 @@ export const EXCLUSION_REASONS = {
37
37
  retry_backoff: "the failed phase is retrying in place and waiting out its backoff",
38
38
  routing_unavailable: "the unit was claimed and refused at kickoff (no route, no eligible coding-account slot, or the provider is unavailable)",
39
39
  repo_paused: "an operator paused the repository",
40
- remediate_parked: "the remediate phase is parked, so the failing phase has nowhere to be repaired; `catalyst-skills release <ticket>` releases the park once its cause is fixed",
41
- phase_parked: "the offered phase is parked after repeated failures or the repair-round cap; `catalyst-skills release <ticket>` releases it once its cause is fixed",
40
+ remediate_parked: "the remediate phase is parked, so the failing phase has nowhere to be repaired; `catalyst release <ticket>` releases the park once its cause is fixed",
41
+ phase_parked: "the offered phase is parked after repeated failures or the repair-round cap; `catalyst release <ticket>` releases it once its cause is fixed",
42
42
  later_phase_lease_held: "an earlier phase is offered while a live container still holds a later phase of this ticket",
43
43
  human_owned_pr: "a person's own pull request holds this ticket; it releases itself when that PR closes or merges",
44
44
  review_not_converging: "review and repair kept finding new problems without converging; a person reads the findings and comments on the ticket to resume",
45
45
  round_threshold: "the ticket spent its lifetime repair budget; answering its ask or pushing a fix buys one more cycle",
46
46
  claim_storm: "this unit was claimed too many times in the last hour, so it waits out the hour; nothing to release",
47
47
  repo_at_capacity: "the repository's runner seats are all in use; it starts when one frees",
48
+ wip_limit: "the project is at its WIP limit (tickets in progress, counting blocked, parked and waiting ones); this new start waits until one of them finishes, and the tickets in progress keep running",
48
49
  };
49
50
  /** Every fail-closed unknown the evaluator names. */
50
51
  export const UNKNOWN_REASONS = {
package/dist/identity.js CHANGED
@@ -14,7 +14,7 @@ function render(result, choices) {
14
14
  return `${summary}\nNo choices were offered. An automatic match cannot be replaced; an unmatched roster may need a retry.`;
15
15
  if (result.options.length === 0)
16
16
  return `${summary}\nThe workspace roster has no eligible people.`;
17
- return [summary, ...result.options.map((o) => `${o.id}\t${o.displayName ?? o.name ?? "(unnamed)"}`), "Select your own identity explicitly: catalyst-skills identity linear set <linearUserId>"].join("\n");
17
+ return [summary, ...result.options.map((o) => `${o.id}\t${o.displayName ?? o.name ?? "(unnamed)"}`), "Select your own identity explicitly: catalyst identity linear set <linearUserId>"].join("\n");
18
18
  }
19
19
  export async function cmdIdentity(args, ctx, deps = {}) {
20
20
  const parts = positionals(args);
@@ -25,11 +25,11 @@ export async function cmdIdentity(args, ctx, deps = {}) {
25
25
  }
26
26
  const cfg = requireConfig(ctx);
27
27
  if (!cfg.user)
28
- throw new CliError("this machine uses a tenant account key; run catalyst-skills login as yourself", "member-required");
28
+ throw new CliError("this machine uses a tenant account key; run catalyst login as yourself", "member-required");
29
29
  const createClient = deps.createClient ?? (async (options) => (await loadHttpSdk()).createTenantClient(options));
30
30
  const client = await createClient({ key: await bearerFor(ctx, cfg), baseUrl: cfg.baseUrl, fetch: ctx.fetch });
31
31
  if (!client.linearIdentity)
32
- throw new CliError("the installed SDK lacks identity recovery; upgrade catalyst-skills after the SDK release", "sdk-upgrade-required");
32
+ throw new CliError("the installed SDK lacks identity recovery; upgrade the catalyst CLI after the SDK release", "sdk-upgrade-required");
33
33
  if (action === "set") {
34
34
  // The arity check above requires a selection; never choose the first roster entry for a person.
35
35
  if (selected === undefined)
package/dist/oauth.js CHANGED
@@ -152,7 +152,7 @@ async function pollDeviceCode(ctx, discovery, auth, sleep) {
152
152
  continue;
153
153
  }
154
154
  if (err === "access_denied")
155
- throw new CliError("login was denied — run: catalyst-skills login to try again", "login-denied");
155
+ throw new CliError("login was denied — run: catalyst login to try again", "login-denied");
156
156
  if (err === "expired_token")
157
157
  return "expired";
158
158
  // A transient HTTP failure (request timeout, rate limit, server error) is retried, honouring
@@ -220,7 +220,7 @@ export async function bearerFor(ctx, cfg, deps = {}) {
220
220
  if (typeof cfg.key === "string" && cfg.key !== "")
221
221
  return cfg.key;
222
222
  if (!cfg.auth)
223
- throw new CliError("this machine is not connected — run: catalyst-skills login", "not-configured");
223
+ throw new CliError("this machine is not connected — run: catalyst login", "not-configured");
224
224
  const msToExpiry = Date.parse(cfg.auth.expiresAt) - ctx.now().getTime();
225
225
  if (Number.isFinite(msToExpiry) && msToExpiry > REFRESH_SKEW_MS)
226
226
  return cfg.auth.accessToken;
@@ -256,7 +256,7 @@ async function refreshAndPersist(ctx, cfg, deps) {
256
256
  const err = await oauthError(res, discovery.tokenUrl);
257
257
  if (err === "invalid_grant") {
258
258
  // revocation or > inactivity window — the ONLY session-expired case; NOT routine expiry.
259
- throw new CliError("your login expired or was revoked — run: catalyst-skills login", "session-expired");
259
+ throw new CliError("your login expired or was revoked — run: catalyst login", "session-expired");
260
260
  }
261
261
  const transient = res.status === 429 || res.status >= 500;
262
262
  if (!transient)
package/dist/query.js CHANGED
@@ -156,7 +156,7 @@ async function fromApi(ctx, cfg, sub, rest, f, args) {
156
156
  if (!q)
157
157
  throw new UsageError("search needs terms: query search <terms>");
158
158
  const res = await api.getJson("/api/v1/search", { query: { q, limit: f.limit } });
159
- return rowsOf(res.body);
159
+ return searchRows(res.body);
160
160
  }
161
161
  case "changes": {
162
162
  const since = flagString(args, "since");
@@ -224,6 +224,21 @@ export function applyFilters(rows, f) {
224
224
  return true;
225
225
  });
226
226
  }
227
+ /** The hub answers search in four groups (`issues`, `pulls`, `projects`, `initiatives`, the read
228
+ * model's SearchView). `rowsOf` returns only the first array it finds, so it dropped every match that
229
+ * was not a ticket. Every group comes back here, flattened, each row tagged with its `kind`. */
230
+ const SEARCH_GROUPS = [["issues", "issue"], ["pulls", "pull"], ["projects", "project"], ["initiatives", "initiative"]];
231
+ export function searchRows(body) {
232
+ if (Array.isArray(body))
233
+ return body;
234
+ if (!body || typeof body !== "object")
235
+ return [];
236
+ const b = body;
237
+ if (SEARCH_GROUPS.some(([group]) => Array.isArray(b[group]))) {
238
+ return SEARCH_GROUPS.flatMap(([group, kind]) => Array.isArray(b[group]) ? b[group].map((row) => ({ kind, ...row })) : []);
239
+ }
240
+ return rowsOf(body);
241
+ }
227
242
  function printResult(ctx, args, sub, result) {
228
243
  if (args.json || !Array.isArray(result)) {
229
244
  ctx.stdout(JSON.stringify(result, null, args.json ? 0 : 2));
@@ -242,6 +257,17 @@ export function summaryLine(sub, r) {
242
257
  return `${r.id} ${r.state ?? "?"} ${r.name ?? ""}`;
243
258
  case "cycles":
244
259
  return `cycle ${r.number ?? r.id} ${r.name ?? ""} ${r.starts_at ?? ""} → ${r.ends_at ?? ""}`;
260
+ case "search":
261
+ switch (r.kind) {
262
+ case "pull":
263
+ return `pull #${r.number ?? "?"} ${r.title ?? ""}${r.repo_id ? ` (${r.repo_id})` : ""}`;
264
+ case "project":
265
+ return `project ${r.name ?? r.id}`;
266
+ case "initiative":
267
+ return `initiative ${r.name ?? r.id}`;
268
+ default:
269
+ return `issue ${r.identifier ?? r.id} ${r.title ?? ""}`;
270
+ }
245
271
  default:
246
272
  return JSON.stringify(r);
247
273
  }
package/dist/ready.js CHANGED
@@ -184,7 +184,7 @@ export async function readyReport(ctx, deps) {
184
184
  ? `config: joined ${cfg.name} as ${cfg.user.label} (${cfg.user.role})`
185
185
  : `config: joined ${cfg.name} (${cfg.slug}) as ${cfg.principal}`,
186
186
  }
187
- : { id: "config", ok: false, line: "config: not connected", fix: "npx @catalyst-cloud/catalyst-skills login (keyless; or pass --key / set CATALYST_CLOUD_TOKEN)", who: "you (approve the login in your browser)" });
187
+ : { id: "config", ok: false, line: "config: not connected", fix: "npx -p @catalyst-cloud/cli catalyst login (keyless; or pass --key / set CATALYST_CLOUD_TOKEN)", who: "you (approve the login in your browser)" });
188
188
  }
189
189
  catch (err) {
190
190
  checks.push({ id: "config", ok: false, line: `config: ${err instanceof CliError ? err.message : String(err)}`, fix: "re-run login to rewrite it", who: "you" });
@@ -192,7 +192,7 @@ export async function readyReport(ctx, deps) {
192
192
  const cache = readContractCache(ctx.home);
193
193
  const range = readManifest().tenantContractRange;
194
194
  if (!cache) {
195
- checks.push({ id: "contract", ok: false, line: "contract: not cached", fix: "catalyst-skills contract --refresh", who: "you" });
195
+ checks.push({ id: "contract", ok: false, line: "contract: not cached", fix: "catalyst contract --refresh", who: "you" });
196
196
  }
197
197
  else if (contractVersionInRange(cache.contractVersion, range) !== true) {
198
198
  checks.push({ id: "contract", ok: false, line: `contract: version ${cache.contractVersion} is outside this bundle's range ${range}`, fix: upgradeCommand(), who: "you" });
@@ -290,7 +290,7 @@ export async function readyReport(ctx, deps) {
290
290
  checks.push({ id: "skills", ok: true, line: `skills: all ${deps.skillNames.length} present in ${skillsDir}` });
291
291
  }
292
292
  else {
293
- checks.push({ id: "skills", ok: false, line: `skills: ${present.length} of ${deps.skillNames.length} in ${skillsDir}, missing ${missing.join(", ")}`, fix: "catalyst-skills install", who: "you" });
293
+ checks.push({ id: "skills", ok: false, line: `skills: ${present.length} of ${deps.skillNames.length} in ${skillsDir}, missing ${missing.join(", ")}`, fix: "catalyst install", who: "you" });
294
294
  }
295
295
  try {
296
296
  await (deps.loadSdk ?? loadSdk)();
package/dist/release.js CHANGED
@@ -37,7 +37,7 @@ export function renderTicketRelease(ticket, r) {
37
37
  const lines = [];
38
38
  const dry = r.dryRun === true;
39
39
  if (r.outcome === "nothing-held") {
40
- lines.push(`${ticket}: nothing holds this ticket — run \`catalyst-skills explain ${ticket}\` for why it is not running`);
40
+ lines.push(`${ticket}: nothing holds this ticket — run \`catalyst explain ${ticket}\` for why it is not running`);
41
41
  }
42
42
  else if (r.outcome === "refused") {
43
43
  lines.push(dry ? `${ticket} (dry run): would be refused — nothing would be released:` : `${ticket}: refused — nothing was released:`);
package/dist/replica.js CHANGED
@@ -196,7 +196,7 @@ function baseStatusLine(s) {
196
196
  case "not-configured":
197
197
  return `replica: not configured (${s.reasons.join("; ")})`;
198
198
  case "absent":
199
- return `replica: absent at ${s.dbPath} — start it with: catalyst-skills replica start --detach`;
199
+ return `replica: absent at ${s.dbPath} — start it with: catalyst replica start --detach`;
200
200
  case "fresh":
201
201
  return `replica: fresh at ${s.dbPath} (cursor ${s.cursor}, heartbeat ${s.heartbeatAgeMs}ms ago${s.lag !== undefined ? `, ${s.lag} behind head ${s.head}` : ""})`;
202
202
  case "stale":
@@ -240,7 +240,9 @@ export async function engineFor(sdk, dbPath, ctx, deps = {}) {
240
240
  }
241
241
  catch (err) {
242
242
  const why = err instanceof Error ? err.message.split("\n")[0] : String(err);
243
- ctx.stderr(`[catalyst-skills] better-sqlite3 unavailable (${why}); using node:sqlite`);
243
+ // node:sqlite is the supported path (CTC-4272): say nothing when it works, except under CATALYST_DEBUG.
244
+ if (ctx.env?.CATALYST_DEBUG === "1")
245
+ ctx.stderr(`[catalyst] better-sqlite3 unavailable (${why}); using node:sqlite`);
244
246
  return deps.readonly ? sdk.nodeSqliteReadonlyEngine(dbPath) : sdk.nodeSqliteEngine(dbPath);
245
247
  }
246
248
  }
@@ -252,7 +254,7 @@ export const SNAPSHOT_MAX_FAILURES = 5;
252
254
  * count on; staying live is that signal. The failing warm loop never reaches it — its resync demand
253
255
  * fails and tears the attempt down first (CTC-2499). */
254
256
  export const SNAPSHOT_LIVE_STABLE_MS = 60_000;
255
- export const REPLICA_RESTART_COMMAND = "catalyst-skills replica start --detach";
257
+ export const REPLICA_RESTART_COMMAND = "catalyst replica start --detach";
256
258
  const defaultSleep = (ms) => new Promise((r) => setTimeout(r, ms));
257
259
  function defaultWaitForStop() {
258
260
  return new Promise((resolve) => {
@@ -342,7 +344,7 @@ async function cmdStart(args, ctx, cfg, dbPath, deps) {
342
344
  const argv = (deps.argv ?? process.argv.slice(1)).filter((a) => a !== "--detach");
343
345
  const { pid } = (deps.detach ?? detachSelf)(argv, ctx.env);
344
346
  writeFileSync(pidfilePath(dbPath), `${pid}\n`);
345
- ctx.stdout(`replica writer started in the background (pid ${pid}, db ${dbPath}); check with: catalyst-skills replica status`);
347
+ ctx.stdout(`replica writer started in the background (pid ${pid}, db ${dbPath}); check with: catalyst replica status`);
346
348
  return 0;
347
349
  }
348
350
  const sdk = await loadSdk();
@@ -503,7 +505,7 @@ async function cmdStart(args, ctx, cfg, dbPath, deps) {
503
505
  const lastError = attemptError ?? "the snapshot pull failed";
504
506
  const nowMs = ctx.now().getTime();
505
507
  const stopped = failures >= maxFailures
506
- ? { at: nowMs, reason: `${failures} consecutive snapshot failures`, restartWith: "catalyst-skills replica start --detach" }
508
+ ? { at: nowMs, reason: `${failures} consecutive snapshot failures`, restartWith: "catalyst replica start --detach" }
507
509
  : null;
508
510
  writeWriterState(dbPath, { pid: process.pid, consecutiveFailures: failures, lastError, lastFailureAt: nowMs, stopped }, nowMs);
509
511
  if (stopped) {
@@ -562,7 +564,7 @@ export function assertSingleSelect(sql) {
562
564
  async function cmdSql(args, ctx, dbPath, sql, deps) {
563
565
  const query = assertSingleSelect(sql);
564
566
  if (!existsSync(dbPath))
565
- throw new CliError(`no replica at ${dbPath} — start it with: catalyst-skills replica start --detach`, "replica-absent", 3);
567
+ throw new CliError(`no replica at ${dbPath} — start it with: catalyst replica start --detach`, "replica-absent", 3);
566
568
  const sdk = await loadSdk();
567
569
  const engine = await engineFor(sdk, dbPath, ctx, { ...deps.engineDeps, readonly: true });
568
570
  const replica = await sdk.CatalystReplica.openReadOnly({ dbPath, engine, log: () => { } });
@@ -577,7 +579,7 @@ async function cmdSql(args, ctx, dbPath, sql, deps) {
577
579
  }
578
580
  async function cmdSchema(args, ctx, dbPath, table) {
579
581
  if (!existsSync(dbPath))
580
- throw new CliError(`no replica at ${dbPath} — the schema is what the file holds; start it with: catalyst-skills replica start --detach`, "replica-absent", 3);
582
+ throw new CliError(`no replica at ${dbPath} — the schema is what the file holds; start it with: catalyst replica start --detach`, "replica-absent", 3);
581
583
  const db = new (loadSqlite().DatabaseSync)(dbPath, { readOnly: true });
582
584
  try {
583
585
  const tables = db.prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%' ORDER BY name").all().map((r) => r.name);
@@ -21,11 +21,11 @@ const PLATFORM_ARCH = {
21
21
  * needs `.zip` extraction, a separate mechanism this ticket does not build (D7). */
22
22
  export function tarballFor(version, platform, arch) {
23
23
  if (platform === "win32") {
24
- throw new CliError("catalyst-skills runtime install does not support Windows yet — supported: darwin (arm64, x64) and linux (x64, arm64). Use whatever Node 22.15+ or bun 1.4+ you already have on Windows.", "runtime-unsupported-platform");
24
+ throw new CliError("catalyst runtime install does not support Windows yet — supported: darwin (arm64, x64) and linux (x64, arm64). Use whatever Node 22.15+ or bun 1.4+ you already have on Windows.", "runtime-unsupported-platform");
25
25
  }
26
26
  const arches = PLATFORM_ARCH[platform];
27
27
  if (!arches || !arches.includes(arch)) {
28
- throw new CliError(`catalyst-skills runtime install does not support ${platform}/${arch} — supported: darwin (arm64, x64) and linux (x64, arm64)`, "runtime-unsupported-platform");
28
+ throw new CliError(`catalyst runtime install does not support ${platform}/${arch} — supported: darwin (arm64, x64) and linux (x64, arm64)`, "runtime-unsupported-platform");
29
29
  }
30
30
  const file = `node-v${version}-${platform}-${arch}.tar.gz`;
31
31
  return { file, url: `https://nodejs.org/dist/v${version}/${file}`, shasums: `https://nodejs.org/dist/v${version}/SHASUMS256.txt` };
package/dist/runtime.js CHANGED
@@ -13,7 +13,7 @@
13
13
  export const BUN_MIN = "1.4.0";
14
14
  /** The ONE copy-pasteable command. Installs a pinned Node under the CLI's own cache and pins it;
15
15
  * it never touches the machine's default Node and needs no admin rights. See src/runtime-store.ts. */
16
- export const FIX_COMMAND = "npx -y @catalyst-cloud/catalyst-skills runtime install";
16
+ export const FIX_COMMAND = "npx -y -p @catalyst-cloud/cli catalyst runtime install";
17
17
  /** Detect the runtime this process is running on. `process.versions.bun` is present only under bun. */
18
18
  export function detectRuntime(versions = process.versions) {
19
19
  if (versions.bun)
package/dist/secret.js CHANGED
@@ -70,7 +70,7 @@ function requireRepo(args, verb) {
70
70
  /** The cloud's refusal, in its own words, plus what to do for the two we can explain. */
71
71
  function refusalLine(status, body, repo) {
72
72
  if (status === 401) {
73
- return "refused (401): the cloud did not accept your credential here. If `catalyst-skills me` works, this cloud predates secret writes from the CLI and needs a newer release; otherwise log in again.";
73
+ return "refused (401): the cloud did not accept your credential here. If `catalyst me` works, this cloud predates secret writes from the CLI and needs a newer release; otherwise log in again.";
74
74
  }
75
75
  if (status === 404)
76
76
  return `refused (404): ${repo} is not a repository of this tenant (register it in the app first)`;
package/dist/skills.js CHANGED
@@ -137,5 +137,5 @@ export function updateNoticeLine(previous, current, entry) {
137
137
  // minor — so it never crosses 0.2.x → 0.3.0. Pin @latest via `npm install`, THEN re-run login: the
138
138
  // install alone does not rewrite customer.json.cliPath, so the skill helpers would keep spawning the
139
139
  // stale recorded bundle and repeat this notice forever; the new global bin records its own path.
140
- return `[catalyst-skills] updated ${previous} → ${current}: ${summary} · update with: ${upgradeCommand()}`;
140
+ return `[catalyst] updated ${previous} → ${current}: ${summary} · update with: ${upgradeCommand()}`;
141
141
  }