@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.
Files changed (79) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +7 -5
  3. package/dist/args.js +68 -18
  4. package/dist/browser.js +37 -3
  5. package/dist/cli.js +58 -12
  6. package/dist/config.js +17 -5
  7. package/dist/consent-browser.js +58 -0
  8. package/dist/daemon-authority-lifetime.js +171 -0
  9. package/dist/daemon-authority.js +216 -0
  10. package/dist/daemon-cache.js +348 -0
  11. package/dist/daemon-store.js +370 -0
  12. package/dist/daemon-supervisor.js +460 -0
  13. package/dist/env/onboard-draft-store.js +250 -0
  14. package/dist/env/onboard-draft.js +33 -0
  15. package/dist/env/selected-import-writer.js +163 -0
  16. package/dist/env/settings-authority.js +205 -0
  17. package/dist/events-cloud-tail.js +146 -0
  18. package/dist/events-cloud.js +619 -0
  19. package/dist/events.js +186 -31
  20. package/dist/oauth.js +129 -34
  21. package/dist/onboard-accounts.js +259 -0
  22. package/dist/onboard-automation-management.js +15 -0
  23. package/dist/onboard-bootstrap.js +162 -0
  24. package/dist/onboard-capabilities.js +15 -7
  25. package/dist/onboard-capacity.js +226 -0
  26. package/dist/onboard-existing.js +106 -32
  27. package/dist/onboard-file-snapshot.js +45 -0
  28. package/dist/onboard-github.js +14 -3
  29. package/dist/onboard-login-candidate.js +261 -0
  30. package/dist/onboard-next.js +139 -0
  31. package/dist/onboard-personal.js +14 -3
  32. package/dist/onboard-projects.js +350 -0
  33. package/dist/onboard-repositories.js +146 -37
  34. package/dist/onboard-runtime.js +108 -13
  35. package/dist/onboard-settings-context.js +141 -0
  36. package/dist/onboard-settings-import.js +127 -0
  37. package/dist/onboard-settings.js +234 -0
  38. package/dist/onboard-ui.js +85 -59
  39. package/dist/onboard-workflow.js +316 -0
  40. package/dist/onboard-workspace.js +14 -3
  41. package/dist/onboard.js +422 -39
  42. package/dist/owned-fetch.js +182 -0
  43. package/dist/pagination.js +13 -1
  44. package/dist/query.js +104 -31
  45. package/dist/ready.js +143 -35
  46. package/dist/replica.js +11 -4
  47. package/dist/repository-id.js +5 -0
  48. package/dist/sdk.js +33 -0
  49. package/dist/watch.js +2 -2
  50. package/package.json +3 -2
  51. package/skills/catalyst-github/SKILL.md +4 -2
  52. package/skills/catalyst-github/references/is-it-mergeable.md +1 -1
  53. package/skills/catalyst-github/references/local-replica.md +78 -0
  54. package/skills/catalyst-github/references/reading-from-the-cloud.md +24 -0
  55. package/skills/catalyst-github/references/what-a-pr-accumulates.md +1 -1
  56. package/skills/catalyst-github/scripts/is-it-mergeable.mjs +210 -45
  57. package/skills/catalyst-github/scripts/read-pr.mjs +51 -15
  58. package/skills/catalyst-linear/SKILL.md +5 -3
  59. package/skills/catalyst-linear/references/local-replica.md +91 -0
  60. package/skills/catalyst-linear/references/reading-a-ticket.md +3 -3
  61. package/skills/catalyst-linear/references/reading-from-the-cloud.md +38 -0
  62. package/skills/catalyst-linear/scripts/read-ticket.mjs +37 -12
  63. package/skills/catalyst-onboard/SKILL.md +6 -4
  64. package/skills/catalyst-onboard/references/local-replica.md +40 -0
  65. package/skills/catalyst-onboard/references/local-sync.md +8 -4
  66. package/skills/catalyst-onboard/scripts/local-sync.mjs +2 -2
  67. package/skills/catalyst-onboard/scripts/where-am-i.mjs +1 -1
  68. package/skills/run-this-project/SKILL.md +1 -1
  69. package/skills/unstick/SKILL.md +1 -1
  70. package/skills/what-needs-me/SKILL.md +1 -1
  71. package/skills/what-this-repo-needs/SKILL.md +1 -1
  72. package/skills/whats-happening/SKILL.md +4 -2
  73. package/skills/whats-happening/references/local-replica.md +51 -0
  74. package/skills/whats-happening/references/reading-from-the-cloud.md +21 -0
  75. package/skills/whats-happening/references/status-reply.md +1 -1
  76. package/skills/whats-happening/scripts/snapshot.mjs +104 -26
  77. package/vendor/settings/index.d.ts +4 -0
  78. package/vendor/settings/index.js +2937 -0
  79. package/vendor/settings/provenance.json +148 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.14.8
4
+
5
+ - Onboarding on a computer with no browser, such as a Docker container, prints the Catalyst page that finishes each Linear and GitHub approval and keeps waiting, instead of stopping.
6
+ - Onboarding accepts cloud repository IDs such as `tenant-9:owner__repo`, so registering a project no longer fails on them.
7
+ - When the sign-in expires during a provider approval, onboarding stops and says to run `catalyst login`, instead of waiting for the approval to time out.
8
+ - Steps that wait name the next action and the page to use, and the end of a run lists what is left.
9
+ - `catalyst login` says the browser opened only when it did.
10
+ - Onboarding finishes once everything work needs is in place. Repository settings, values and the daily update are listed as next steps instead of blocking.
11
+
12
+ ## 0.14.7
13
+
14
+ - Setup uses cloud reads by default and offers local sync as an option. Onboarding preserves verified account and repository choices when it resumes.
15
+ - Settings drafts stay private for review; keeping a draft does not approve it or import values.
16
+
3
17
  ## 0.14.6
4
18
 
5
19
  - A current Catalyst data folder no longer triggers a legacy-runtime cleanup warning. Legacy checks and removal keep current shared data and report it separately.
package/README.md CHANGED
@@ -126,7 +126,7 @@ catalyst ready --onboarding --json
126
126
 
127
127
  The dry run writes no files. `--only <step>` runs one step and reports its scope separately from full onboarding. Exit 10 means a failed step, 11 means waiting, and 12 means a guard refused the action. The private receipt is `install/last-run.json` under your machine's Catalyst state directory; credentials stay in the login config. A fake HOME reports old services and keeps them intact.
128
128
 
129
- Local sync is optional. Include it in the plan with `--local-sync` when you need local SQL or offline access. A resume keeps that choice. Readiness distinguishes failed checks from missing evidence and reports observed project work separately from unfinished setup.
129
+ Local sync is optional. Include it in the plan with `--local-sync` when you need local SQL or sustained local reads. A resume keeps that choice. Readiness distinguishes failed checks from missing evidence and reports observed project work separately from unfinished setup.
130
130
 
131
131
  ## Requirements
132
132
 
@@ -153,12 +153,14 @@ Nothing, by default. After `login`, every read, write, ask and explanation goes
153
153
  | process | needed for | what it holds on disk | lifetime |
154
154
  | --- | --- | --- | --- |
155
155
  | none | every read, write, ask and explain | `customer.json` and `contract.json` | the default after login |
156
- | `catalyst replica start` | local SQL, cheap repeated reads, and local event `tail`, `wait-for`, and `query` | one SQLite file plus a bounded event cache under `$XDG_STATE_HOME/catalyst/events/` (fallback `~/.local/state/catalyst/events/`) | one long-running process; foreground by default, `--detach` writes a pidfile beside the database and returns; `login --start-replica` does the same at the end of login |
156
+ | `catalyst replica start` | local SQL, cheap repeated reads, and a shared local event cache read with `events ... --from-cache` | one SQLite file plus a bounded event cache under `$XDG_STATE_HOME/catalyst/events/` (fallback `~/.local/state/catalyst/events/`) | one long-running process; foreground by default, `--detach` writes a pidfile beside the database and returns; `login --start-replica` does the same at the end of login |
157
157
  | `catalyst watch` | a project owner reacting to its scope | one cursor file (`~/.config/catalyst-cloud/watch-cursor.json`) stamped with the account | lives inside the session that armed it; exits with it |
158
158
 
159
- The check every skill runs first is `catalyst replica status`, which needs no network: is the pidfile's process alive, is the writer-lock heartbeat younger than the staleness threshold, and is the cursor non-empty. It exits `0` for fresh, `1` for present but stale, `2` for not connected, `3` for absent, and prints one line either way (`--json` for scripts). A fresh replica is used; anything else falls back to the API and the skill says so in its answer. A skill never refuses to work because the replica is down and never silently reads a stale one. `replica status --probe` compares the local cursor against the cloud's head for the honest "how far behind" number; that is the only form that touches the network. `catalyst replica stop` stops a detached writer.
159
+ CLI and skill reads use the cloud API by default, including on a machine with a fresh local replica. No background writer or local database is needed. `catalyst query ... --source replica` explicitly chooses the optional local view; the first stderr line names the source. Local sync does not promise offline browsing.
160
160
 
161
- `catalyst events status --probe` checks the event cache's own cursor and compares it with the cloud head; `replica status --probe` checks the separate entity-replica cursor. A fresh replica does not prove event freshness. `events tail` follows new cached events, `events wait-for --ticket ... --timeout ...` performs a bounded wait, and `events query` reads retained history. Those three commands only read the cache; `events status --probe` makes the cloud comparison. The replica process is the single writer and reports an event-sync failure without terminating a healthy entity replica. The event cache keeps closed daily segments for at most seven days or 256 MiB per account and reports an explicit gap when a requested sequence has retired.
161
+ For a local view, `catalyst replica status` checks the writer, heartbeat and cursor. It exits `0` for fresh, `1` for present but stale, `2` for not connected, `3` for absent. `replica status --probe` compares its cursor with the cloud head. These diagnostics do not change the default read source, and an absent replica does not make `catalyst ready` fail.
162
+
163
+ `catalyst events tail`, `wait-for`, `query` and `status` read from the cloud by default, with no local file. `events tail` prints one JSON line per new event and stays in the foreground; `events wait-for --ticket ... --timeout ...` prints the first match and exits 0, or exits 1 when none arrives in time; `events query` reads one page of filtered history (`--ticket`, `--type`, `--limit` up to 200, `--order desc|asc`, `--before`, `--after`); `events status` names the cloud head. Add `--from-cache` to read this machine's local event cache instead, which pays off when several agents on one machine share one cloud connection. `catalyst events status --from-cache --probe` checks the cache's own cursor against the cloud head; `replica status --probe` checks the separate entity-replica cursor. A fresh replica does not prove event freshness. The replica process is the single writer and reports an event-sync failure without terminating a healthy entity replica. The event cache keeps closed daily segments for at most seven days or 256 MiB per account and reports an explicit gap when a requested sequence has retired.
162
164
 
163
165
  The replica is a Node process, not a service: the supported path is the plain command, and `skills/catalyst-onboard/references/local-sync.md` gives launchd and systemd examples for people who want the writer to survive a reboot.
164
166
 
@@ -170,7 +172,7 @@ The replica is a Node process, not a service: the supported path is the plain co
170
172
  | `whats-happening` | "What's happening? Why is that stuck? How does this work? How does it prioritise?" | The desk for your account: reads the contract, what is running and queued, the eligibility explainer, the coding accounts and the open asks, and answers in one reply with ticket ids. Carries the facts behind the answers: the ladder, the stage map, failures, parks and holds, the queue order and every reason a ticket is excluded. Routes work to a project owner and decisions to `what-needs-me`. | [`SKILL.md`](skills/whats-happening/SKILL.md) |
171
173
  | `what-needs-me` | "What needs me? What am I blocking?" | The human's decision inbox, ranked by how much open work each ask holds, and the one way an agent raises a decision on their behalf: files an ask through the cloud's ask route with the account's own template and records the answer so the held work releases. | [`SKILL.md`](skills/what-needs-me/SKILL.md) |
172
174
  | `run-this-project` | "Run this project for me. Own it until it closes." | Single-threaded owner of one project: subscribes to the account's event stream for its scope, reacts to each change in the same turn, makes tickets ready and moves them to dispatch, parks what should stop, chases stalls, escalates inward, and keeps one status summary current. Never polls. | [`SKILL.md`](skills/run-this-project/SKILL.md) |
173
- | `catalyst-linear` | "Show me the ticket, the history, what Catalyst wrote on it." | Reads a ticket with its comments, relations, labels, linked pull requests and agent sessions inline, from the replica when fresh and the API otherwise, always naming the source; writes comments, card moves, labels and new tickets as the app actor; knows what a ticket accumulates as Catalyst works it. | [`SKILL.md`](skills/catalyst-linear/SKILL.md) |
175
+ | `catalyst-linear` | "Show me the ticket, the history, what Catalyst wrote on it." | Reads a ticket with its comments, relations, labels, linked pull requests and agent sessions inline, from the cloud API by default, with explicit local opt-in and the source named; writes comments, card moves, labels and new tickets as the app actor; knows what a ticket accumulates as Catalyst works it. | [`SKILL.md`](skills/catalyst-linear/SKILL.md) |
174
176
  | `catalyst-github` | "Show me the PR, the checks, the review, the queue." | A ticket's pull request with its checks, reviews and review threads; whether it is mergeable under the repository's policy; what a PR accumulates as the ticket moves (the branch, the draft, the rewrite, the force-pushes, the labels, the queue). | [`SKILL.md`](skills/catalyst-github/SKILL.md) |
175
177
  | `unstick` | "Why is this parked? Unpark it. Get things flowing again." | Reads why a ticket is not running and every park or hold on it, decides whether the recorded cause is fixed, previews the release and releases it the right way from your own login — or one failure class across a team — and raises an ask only for what a person has to do. | [`SKILL.md`](skills/unstick/SKILL.md) |
176
178
  | `what-this-repo-needs` | "What env vars does this repo need? What belongs in its environment declaration?" | Scans a repository offline — no login, no network — and lists the environment variable names it needs, grouped build/test, deploy-only and bindings, each with where it was found, what uses it, and where a local value would come from. Never reads or prints a value. Also checks TOML syntax and the environment variable table in `.catalyst/catalyst.toml`. | [`SKILL.md`](skills/what-this-repo-needs/SKILL.md) |
package/dist/args.js CHANGED
@@ -41,13 +41,29 @@ export const FLAG_TABLES = {
41
41
  },
42
42
  install: {},
43
43
  onboard: {
44
- team: { value: true, help: "explicitly select an existing Linear team by ID or unique key" },
45
- repo: { value: true, repeat: true, help: "explicitly select an accessible repository as owner/name (repeatable)" },
46
- "local-sync": { value: false, help: "include the optional local sync setup in this plan" },
47
- "resume-from": { value: true, help: "resume at a verified onboarding step" },
44
+ team: {
45
+ value: true,
46
+ help: "explicitly select an existing Linear team by ID or unique key",
47
+ },
48
+ repo: {
49
+ value: true,
50
+ repeat: true,
51
+ help: "explicitly select an accessible repository as owner/name (repeatable)",
52
+ },
53
+ "local-sync": {
54
+ value: false,
55
+ help: "include the optional local sync setup in this plan",
56
+ },
57
+ "resume-from": {
58
+ value: true,
59
+ help: "resume at a verified onboarding step",
60
+ },
48
61
  only: { value: true, help: "run only the named onboarding step" },
49
62
  yes: { value: false, help: "accept the plan's default choices" },
50
- "dry-run": { value: false, help: "print the plan without taking the lock or changing files" },
63
+ "dry-run": {
64
+ value: false,
65
+ help: "print the plan without taking the lock or changing files",
66
+ },
51
67
  },
52
68
  status: {},
53
69
  notice: {},
@@ -67,7 +83,7 @@ export const FLAG_TABLES = {
67
83
  since: { value: true, help: "changes: the cursor to read after" },
68
84
  source: {
69
85
  value: true,
70
- help: "replica | api (default: replica when fresh, else api)",
86
+ help: "replica | api (default: api; replica is an explicit local opt-in)",
71
87
  },
72
88
  ticket: { value: true, help: "pulls: only PRs linked to this ticket" },
73
89
  all: {
@@ -91,27 +107,39 @@ export const FLAG_TABLES = {
91
107
  },
92
108
  },
93
109
  events: {
110
+ "from-cache": {
111
+ value: false,
112
+ help: "read the local event cache instead of the cloud (opt-in local sync)",
113
+ },
94
114
  probe: {
95
115
  value: false,
96
- help: "status: compare the local event cursor with the cloud event head",
116
+ help: "status --from-cache: compare the local event cursor with the cloud event head",
97
117
  },
98
118
  type: { value: true, help: "exact event type" },
99
119
  ticket: {
100
120
  value: true,
101
- help: "ticket identifier found in the event payload",
121
+ help: "ticket identifier found in the event",
102
122
  },
103
123
  after: {
104
124
  value: true,
105
- help: "event sequence to read after (tail/wait default to local head)",
125
+ help: "event sequence to read after (tail and wait-for default to new events only)",
126
+ },
127
+ before: {
128
+ value: true,
129
+ help: "query: event sequence to read before (newest-first paging)",
130
+ },
131
+ order: { value: true, help: "query: desc (newest first, default) or asc" },
132
+ limit: {
133
+ value: true,
134
+ help: "query: maximum matching events (default 50; at most 200 from the cloud)",
106
135
  },
107
- limit: { value: true, help: "query: maximum matching events (default 50)" },
108
136
  timeout: {
109
137
  value: true,
110
138
  help: "wait-for: bounded wait in seconds (default 300)",
111
139
  },
112
140
  directory: {
113
141
  value: true,
114
- help: "event cache directory (default: SDK XDG path)",
142
+ help: "with --from-cache: event cache directory (default: SDK XDG path)",
115
143
  },
116
144
  },
117
145
  explain: {
@@ -235,8 +263,14 @@ export const FLAG_TABLES = {
235
263
  role: { value: true, help: "accept: the role recording the answer" },
236
264
  },
237
265
  ready: {
238
- onboarding: { value: false, help: "check current onboarding evidence and observed project work" },
239
- "local-sync": { value: false, help: "include the selected local sync mode" },
266
+ onboarding: {
267
+ value: false,
268
+ help: "check current onboarding evidence and observed project work",
269
+ },
270
+ "local-sync": {
271
+ value: false,
272
+ help: "include the selected local sync mode",
273
+ },
240
274
  offline: {
241
275
  value: false,
242
276
  help: "skip the published-release check (no network)",
@@ -244,13 +278,29 @@ export const FLAG_TABLES = {
244
278
  },
245
279
  accounts: {},
246
280
  env: {
247
- root: { value: true, help: "draft: repository directory (default: current directory)" },
248
- write: { value: false, help: "draft: write a new .catalyst/catalyst.toml; refuse to overwrite" },
249
- diff: { value: false, help: "draft: show the generated TOML as an additions-only diff" },
281
+ root: {
282
+ value: true,
283
+ help: "draft: repository directory (default: current directory)",
284
+ },
285
+ write: {
286
+ value: false,
287
+ help: "draft: write a new .catalyst/catalyst.toml; refuse to overwrite",
288
+ },
289
+ diff: {
290
+ value: false,
291
+ help: "draft: show the generated TOML as an additions-only diff",
292
+ },
250
293
  },
251
294
  var: {
252
- repo: { value: true, help: "repository scope (owner/name); omit for tenant scope" },
253
- names: { value: true, repeat: true, help: "import: only send these names (repeatable or comma-separated)" },
295
+ repo: {
296
+ value: true,
297
+ help: "repository scope (owner/name); omit for tenant scope",
298
+ },
299
+ names: {
300
+ value: true,
301
+ repeat: true,
302
+ help: "import: only send these names (repeatable or comma-separated)",
303
+ },
254
304
  },
255
305
  team: {
256
306
  all: { value: false, help: "check every team, one request at a time" },
package/dist/browser.js CHANGED
@@ -3,12 +3,15 @@
3
3
  // this never throws into the login flow — the caller wraps it, and a spawn failure is swallowed here.
4
4
  import { spawn } from "node:child_process";
5
5
  /** The platform's "open this URL" command. `open` on macOS, `start` via cmd on Windows, else xdg-open. */
6
- export function openBrowser(url) {
7
- const [cmd, args] = process.platform === "darwin"
6
+ function opener(url, platform = process.platform) {
7
+ return platform === "darwin"
8
8
  ? ["open", [url]]
9
- : process.platform === "win32"
9
+ : platform === "win32"
10
10
  ? ["cmd", ["/c", "start", "", url]]
11
11
  : ["xdg-open", [url]];
12
+ }
13
+ export function openBrowser(url) {
14
+ const [cmd, args] = opener(url);
12
15
  try {
13
16
  const child = spawn(cmd, [...args], { stdio: "ignore", detached: true });
14
17
  child.on("error", () => { }); // no opener installed — the printed URL is the fallback
@@ -18,3 +21,34 @@ export function openBrowser(url) {
18
21
  // spawn itself threw (unusual) — the code and URL on screen are the fallback
19
22
  }
20
23
  }
24
+ /** Login's opener: rejects when no opener ran (none installed, or it exited non-zero, as xdg-open does
25
+ * with no browser), so login claims an opened browser only when one did. An opener still running
26
+ * after `waitMs` launched something and counts as opened. */
27
+ export function openBrowserChecked(url, options = {}) {
28
+ const [cmd, args] = opener(url, options.platform);
29
+ const launch = options.launch ??
30
+ ((command, list) => spawn(command, [...list], { stdio: "ignore", detached: true }));
31
+ return new Promise((resolve, reject) => {
32
+ const fail = () => reject(new Error("browser-unavailable"));
33
+ let child;
34
+ try {
35
+ child = launch(cmd, args);
36
+ }
37
+ catch {
38
+ return fail();
39
+ }
40
+ child.unref();
41
+ const timer = setTimeout(resolve, options.waitMs ?? 3_000);
42
+ child.once("error", () => {
43
+ clearTimeout(timer);
44
+ fail();
45
+ });
46
+ child.once("exit", (code) => {
47
+ clearTimeout(timer);
48
+ if (code === 0)
49
+ resolve();
50
+ else
51
+ fail();
52
+ });
53
+ });
54
+ }
package/dist/cli.js CHANGED
@@ -8,13 +8,14 @@ import { loadContract, pickPath } from "./contract.js";
8
8
  import { CliError, MeError, UsageError } from "./errors.js";
9
9
  import { fetchMe } from "./transport.js";
10
10
  import { bearerFor, deviceFlowLogin } from "./oauth.js";
11
- import { openBrowser as defaultOpenBrowser } from "./browser.js";
11
+ import { openBrowserChecked as defaultOpenBrowser } from "./browser.js";
12
12
  import { cmdAccounts, cmdExplain, cmdHistory, cmdQueue, cmdRunning, } from "./execution.js";
13
13
  import { cmdQuery } from "./query.js";
14
14
  import { cmdReady } from "./ready.js";
15
15
  import { cmdReplica } from "./replica.js";
16
16
  import { cmdRuntime } from "./runtime-verb.js";
17
17
  import { cmdEvents } from "./events.js";
18
+ import { stageOnboardLogin } from "./onboard-login-candidate.js";
18
19
  import { stdinIsTty } from "./prompt.js";
19
20
  import { FIRST_STAMPED_VERSION, PROVENANCE_MARKER, parseProvenanceVersion, } from "./skill-shape.js";
20
21
  import { installedBundleVersion, installSkills, parseChangelogEntry, readChangelog, resolveSkillsDir, skillsSourceDir, updateNoticeLine, } from "./skills.js";
@@ -35,9 +36,10 @@ import { cmdLegacy } from "./legacy.js";
35
36
  import { cmdIdentity } from "./identity.js";
36
37
  import { cmdConnections } from "./connections.js";
37
38
  import { cmdOnboard, onboardErrorJournal } from "./onboard.js";
38
- import { createClackOnboardUi, shouldUseOnboardUi } from "./onboard-ui.js";
39
+ import { createClackOnboardUi, shouldUseOnboardUi, } from "./onboard-ui.js";
39
40
  import { createOnboardRuntime } from "./onboard-runtime.js";
40
- import { onboardingReadyReport, observeCloudOnboarding } from "./onboard-ready.js";
41
+ import { savedOnboardBaseUrl } from "./onboard-next.js";
42
+ import { onboardingReadyReport, observeCloudOnboarding, } from "./onboard-ready.js";
41
43
  import { selectedOnboardTeam } from "./onboard-existing.js";
42
44
  export { CONFIG_MODE, DEFAULT_BASE_URL, FIRST_STAMPED_VERSION, LEGACY_PACKAGE_NAME, PACKAGE_NAME, PROVENANCE_MARKER, CliError, MeError, UsageError, configPathFor, contractPathFor, defaultCtx, defaultReplicaDbFor, defaultSkillsDirFor, fetchMe, formatMode, installedBundleVersion, installSkills, loadConfig, modernCliPath, normalizeBaseUrl, parseArgs, parseChangelogEntry, parseProvenanceVersion, readChangelog, readManifest, resolveSkillsDir, saveConfig, skillsSourceDir, updateNoticeLine, writeConfig, };
43
45
  export const CUSTOMER_SKILLS = [
@@ -71,7 +73,7 @@ export function usageText() {
71
73
  " catalyst query <issues|issue <id>|pulls|pull <id>|projects|cycles|search <terms>|changes --since <cursor|head>>",
72
74
  ' catalyst replica <start [--detach]|stop|status [--probe]|sql "<select>"|schema [table]>',
73
75
  " catalyst runtime <status [--json]|install|path|uninstall> (a pinned Node this CLI manages itself)",
74
- " catalyst events <tail|wait-for|query|status [--probe]> [--type NAME] [--ticket CTC-N] [--after SEQUENCE]",
76
+ " catalyst events <tail|wait-for|query|status> [--type NAME] [--ticket CTC-N] [--after SEQUENCE] [--from-cache]",
75
77
  " catalyst explain <ticket> | history <ticket> | running [--ticket T --phase P] | queue [--team K]",
76
78
  " catalyst watch [--team K] [--ticket T]... [--project P] [--exec CMD]",
77
79
  " catalyst write <comment|state|label|create|reaction|attachment|session> ...",
@@ -252,7 +254,7 @@ export async function main(argv, ctx = defaultCtx(), deps = {}) {
252
254
  }
253
255
  if (input.isTTY) {
254
256
  try {
255
- ui = createClackOnboardUi(await import("@clack/prompts"), { input, output: process.stdout });
257
+ ui = createClackOnboardUi(await import("@clack/prompts"), { input, output: process.stdout }, { baseUrl: () => savedOnboardBaseUrl(ctx.home) });
256
258
  }
257
259
  catch (error) {
258
260
  terminal?.destroy();
@@ -265,23 +267,64 @@ export async function main(argv, ctx = defaultCtx(), deps = {}) {
265
267
  ...createOnboardRuntime(args, ctx, {
266
268
  ui,
267
269
  signinTimeoutMs: deps.onboardSigninTimeoutMs,
270
+ stageSignin: (signal) => {
271
+ if (args.key || ctx.env.CATALYST_CLOUD_TOKEN)
272
+ throw new CliError("Connect your personal key with catalyst login, then run catalyst onboard. Setup will not replace a supplied key with browser sign-in.", "onboard-person-required", 12);
273
+ return stageOnboardLogin({ ...ctx, stdout: ctx.stderr }, {
274
+ baseUrl: args.baseUrl,
275
+ signal,
276
+ timeoutMs: deps.onboardSigninTimeoutMs,
277
+ device: {
278
+ isTty: deps.isTty ?? (() => Boolean(process.stdout.isTTY)),
279
+ openBrowser: deps.openBrowser ?? defaultOpenBrowser,
280
+ sleep: deps.sleep,
281
+ waitForApproval: ui
282
+ ? (run) => ui.wait("Waiting for Catalyst approval", run)
283
+ : undefined,
284
+ },
285
+ });
286
+ },
268
287
  login: (stepCtx, signal) => cmdLogin({ ...args, command: "login", flags: {}, json: false }, stepCtx, {
269
- ...deps, signal, isTty: () => true,
270
- waitForApproval: ui ? run => ui.wait("Waiting for Catalyst approval", run) : undefined,
288
+ ...deps,
289
+ signal,
290
+ isTty: () => true,
291
+ waitForApproval: ui
292
+ ? (run) => ui.wait("Waiting for Catalyst approval", run)
293
+ : undefined,
271
294
  }, true),
272
295
  ready: async (stepCtx, journal) => {
273
296
  const team = selectedOnboardTeam(journal);
274
- const report = await onboardingReadyReport(stepCtx, { teamIds: team ? [team] : undefined, localSync: journal?.localSync ?? args.flags["local-sync"] === true, observe: observeCloudOnboarding });
275
- const failed = report.checks.some(check => check.required && check.state === "fail");
276
- return { state: report.state === "complete" ? "done" : failed ? "failed" : "waiting", reason: report.state === "complete" ? undefined : "onboarding_checks_pending",
277
- evidence: { checks: report.checks.length, passed: report.checks.filter(check => check.state === "pass").length } };
297
+ const report = await onboardingReadyReport(stepCtx, {
298
+ teamIds: team ? [team] : undefined,
299
+ localSync: journal?.localSync ?? args.flags["local-sync"] === true,
300
+ observe: observeCloudOnboarding,
301
+ });
302
+ const failed = report.checks.some((check) => check.required && check.state === "fail");
303
+ return {
304
+ state: report.state === "complete"
305
+ ? "done"
306
+ : failed
307
+ ? "failed"
308
+ : "waiting",
309
+ reason: report.state === "complete"
310
+ ? undefined
311
+ : "onboarding_checks_pending",
312
+ evidence: {
313
+ checks: report.checks.length,
314
+ passed: report.checks.filter((check) => check.state === "pass").length,
315
+ },
316
+ };
278
317
  },
279
318
  legacy: deps.legacy,
280
319
  openBrowser: deps.openBrowser,
281
320
  sleep: deps.sleep,
282
321
  skillNames: CUSTOMER_SKILLS,
283
322
  }),
323
+ ...(deps.onboardBootstrap
324
+ ? { bootstrap: deps.onboardBootstrap }
325
+ : {}),
284
326
  ...(deps.isTty ? { isTty: deps.isTty } : {}),
327
+ ...(deps.signal ? { signal: deps.signal } : {}),
285
328
  }, readManifest().version);
286
329
  }
287
330
  finally {
@@ -485,7 +528,10 @@ async function cmdLogin(args, ctx, deps, personalOnly = false) {
485
528
  : ` — expected ${formatMode(CONFIG_MODE)}; chmod it by hand`}`);
486
529
  ctx.stdout(`Tenant contract range: ${manifest.tenantContractRange}`);
487
530
  try {
488
- const loaded = await loadContract(ctx, config, { refresh: true, signal: deps.signal });
531
+ const loaded = await loadContract(ctx, config, {
532
+ refresh: true,
533
+ signal: deps.signal,
534
+ });
489
535
  checkCancelled();
490
536
  ctx.stdout(`Tenant contract ${loaded.doc.contractVersion} cached at ${loaded.path}`);
491
537
  }
package/dist/config.js CHANGED
@@ -1,5 +1,3 @@
1
- // config.ts — ~/.config/catalyst-cloud/customer.json and its siblings. The ONLY place the base URL
2
- // and the `/api/v1` prefix are joined: the SDK wants the base with the prefix, GET /me without.
3
1
  import { chmodSync, existsSync, lstatSync, mkdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
4
2
  import { randomBytes } from "node:crypto";
5
3
  import { dirname, join } from "node:path";
@@ -22,6 +20,7 @@ export function defaultCtx() {
22
20
  env: process.env,
23
21
  home: process.env.CATALYST_SKILLS_HOME ?? process.env.HOME ?? "/",
24
22
  stdout: (line) => console.log(line),
23
+ stdoutStream: process.stdout,
25
24
  stderr: (line) => console.error(line),
26
25
  fetch: fetch,
27
26
  now: () => new Date(),
@@ -144,12 +143,25 @@ function machineFilePresent(file) {
144
143
  throw error;
145
144
  }
146
145
  }
146
+ /** The machine paths record, or null on a machine that has not run paths setup. */
147
+ function declaredMachinePaths(home, env) {
148
+ const file = machinePathsFile({ env: { ...env, HOME: home } });
149
+ if (!file || (env.CATALYST_PATHS_FILE === undefined && !machineFilePresent(file)))
150
+ return null;
151
+ return parseMachinePaths(JSON.parse(readFileSync(file, "utf8")));
152
+ }
153
+ /** True only when this machine opted in to the local replica: CATALYST_REPLICA_DB is set, or the
154
+ * machine paths file declares replicaDb. Every other machine reads the cloud (CTC-4508). */
155
+ export function replicaOptedIn(home, env = {}) {
156
+ if (env.CATALYST_REPLICA_DB !== undefined)
157
+ return true;
158
+ return declaredMachinePaths(home, env)?.paths.replicaDb !== undefined;
159
+ }
147
160
  export function replicaDbPath(cfg, home, env = {}) {
148
161
  if (env.CATALYST_REPLICA_DB !== undefined)
149
162
  return resolveCatalystPath("replicaDb", { env });
150
- const file = machinePathsFile({ env: { ...env, HOME: home } });
151
- if (file && (env.CATALYST_PATHS_FILE !== undefined || machineFilePresent(file))) {
152
- const machine = parseMachinePaths(JSON.parse(readFileSync(file, "utf8")));
163
+ const machine = declaredMachinePaths(home, env);
164
+ if (machine) {
153
165
  if (machine.paths.replicaDb === undefined)
154
166
  throw new CliError("optional replica is not configured; set CATALYST_REPLICA_DB or declare replicaDb in the machine paths file", "replica-not-configured");
155
167
  return resolveCatalystPath("replicaDb", { env, machine });
@@ -0,0 +1,58 @@
1
+ import { spawn } from "node:child_process";
2
+ /** Consent links are credentials. Keep them out of errors and join the opener before continuing. */
3
+ export function createConsentBrowserOpener(options = {}) {
4
+ const platform = options.platform ?? process.platform;
5
+ const timeoutMs = options.timeoutMs ?? 5_000;
6
+ const killGraceMs = options.killGraceMs ?? 500;
7
+ const launch = options.launch ?? ((command, args) => spawn(command, [...args], { stdio: "ignore", detached: false }));
8
+ return async (url, signal) => {
9
+ if (signal?.aborted)
10
+ throw new DOMException("Browser opening interrupted", "AbortError");
11
+ const [command, args] = platform === "darwin"
12
+ ? ["open", [url]]
13
+ : platform === "win32"
14
+ ? ["cmd", ["/c", "start", "", url]]
15
+ : ["xdg-open", [url]];
16
+ let child;
17
+ try {
18
+ child = launch(command, args);
19
+ }
20
+ catch {
21
+ throw new Error("Browser could not open");
22
+ }
23
+ await new Promise((resolve, reject) => {
24
+ let interrupted = false;
25
+ let expired = false;
26
+ let failed = false;
27
+ let killTimer;
28
+ const stop = () => {
29
+ child.kill("SIGTERM");
30
+ killTimer ??= setTimeout(() => { child.kill("SIGKILL"); }, killGraceMs);
31
+ };
32
+ const abort = () => { interrupted = true; stop(); };
33
+ const timer = setTimeout(() => { expired = true; stop(); }, timeoutMs);
34
+ child.once("error", () => { failed = true; });
35
+ child.once("close", (code) => {
36
+ clearTimeout(timer);
37
+ if (killTimer)
38
+ clearTimeout(killTimer);
39
+ signal?.removeEventListener("abort", abort);
40
+ if (interrupted)
41
+ reject(new DOMException("Browser opening interrupted", "AbortError"));
42
+ else if (failed || expired || code !== 0)
43
+ reject(new Error("Browser could not open"));
44
+ else
45
+ resolve();
46
+ });
47
+ signal?.addEventListener("abort", abort, { once: true });
48
+ if (signal?.aborted)
49
+ abort();
50
+ });
51
+ };
52
+ }
53
+ export const openConsentBrowser = createConsentBrowserOpener();
54
+ /** The web page that finishes a consent step when no browser opens here. The signed link stays private. */
55
+ export function finishOnTheWeb(baseUrl, page, action) {
56
+ const origin = baseUrl.replace(/\/+$/, "");
57
+ return `No browser opened on this computer. In a browser signed in to Catalyst, open ${origin}/settings/${page} and ${action}. Setup keeps waiting here.`;
58
+ }