@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
@@ -0,0 +1,171 @@
1
+ import { DaemonAuthorityError, readDaemonAuthority, } from "./daemon-authority.js";
2
+ function sleep(ms, signal) {
3
+ return new Promise((resolve, reject) => {
4
+ if (signal.aborted) {
5
+ reject(signal.reason);
6
+ return;
7
+ }
8
+ const abort = () => {
9
+ clearTimeout(timer);
10
+ signal.removeEventListener("abort", abort);
11
+ reject(signal.reason);
12
+ };
13
+ const timer = setTimeout(() => {
14
+ signal.removeEventListener("abort", abort);
15
+ resolve();
16
+ }, ms);
17
+ signal.addEventListener("abort", abort, { once: true });
18
+ });
19
+ }
20
+ /** One joined task owns fresh /me renewal; no config refresh/write or token publication shortcut.
21
+ * An expired/changed proof aborts peers through onFailure before waiting for held reader cleanup. */
22
+ export async function createDaemonAuthorityLifetime(ctx, parent) {
23
+ const owned = new AbortController(), lifetime = AbortSignal.any([parent, owned.signal]);
24
+ let proof = await readDaemonAuthority(ctx, lifetime);
25
+ const scope = proof.scope;
26
+ let running;
27
+ let fault;
28
+ let report;
29
+ let notice;
30
+ const failures = [];
31
+ const fail = (error) => {
32
+ if (fault)
33
+ return;
34
+ fault = error;
35
+ // Callback starts synchronously so the supervisor aborts both feeds before drain waits.
36
+ if (report) {
37
+ try {
38
+ notice = report(error.kind === "identity"
39
+ ? "identity_mismatch"
40
+ : "authentication_required");
41
+ }
42
+ catch (error) {
43
+ notice = Promise.reject(error);
44
+ }
45
+ void notice.catch(() => { });
46
+ }
47
+ owned.abort(error);
48
+ };
49
+ const assertCurrent = () => {
50
+ if (fault)
51
+ throw fault;
52
+ try {
53
+ proof.assertCurrent();
54
+ }
55
+ catch (error) {
56
+ if (error instanceof DaemonAuthorityError)
57
+ fail(error);
58
+ throw error;
59
+ }
60
+ };
61
+ return {
62
+ scope,
63
+ assertCurrent,
64
+ async getToken() {
65
+ assertCurrent();
66
+ const selected = proof;
67
+ try {
68
+ const token = await selected.getToken();
69
+ // Renewal may replace proof during the await; the returned bearer still needs its own
70
+ // ORIGINAL proof expiry, not the replacement's later authority window.
71
+ selected.assertCurrent();
72
+ assertCurrent();
73
+ return token;
74
+ }
75
+ catch (error) {
76
+ if (error instanceof DaemonAuthorityError)
77
+ fail(error);
78
+ throw error;
79
+ }
80
+ },
81
+ run(signal, onFailure) {
82
+ if (running)
83
+ throw new Error("daemon_authority_already_running");
84
+ report = onFailure;
85
+ // An earlier synchronous cache guard may already have latched a fault before run starts.
86
+ if (fault && !notice) {
87
+ try {
88
+ notice = report(fault.kind === "identity"
89
+ ? "identity_mismatch"
90
+ : "authentication_required");
91
+ }
92
+ catch (error) {
93
+ notice = Promise.reject(error);
94
+ }
95
+ void notice.catch(() => { });
96
+ }
97
+ const active = AbortSignal.any([lifetime, signal]);
98
+ running = (async () => {
99
+ let timer;
100
+ const expire = () => {
101
+ if (!active.aborted)
102
+ fail(new DaemonAuthorityError("authentication", "daemon_authority_expired"));
103
+ };
104
+ const arm = () => {
105
+ if (timer !== undefined)
106
+ clearTimeout(timer);
107
+ const remaining = proof.expiresAt - ctx.now().getTime();
108
+ if (remaining <= 0) {
109
+ expire();
110
+ return;
111
+ }
112
+ timer = setTimeout(expire, remaining);
113
+ };
114
+ try {
115
+ assertCurrent();
116
+ arm();
117
+ while (!active.aborted) {
118
+ await sleep(10_000, active);
119
+ if (active.aborted)
120
+ break;
121
+ // Old proof's timer stays armed throughout the read and its cleanup ACK.
122
+ const fresh = await readDaemonAuthority(ctx, active);
123
+ if (active.aborted)
124
+ break;
125
+ if (JSON.stringify(fresh.scope) !== JSON.stringify(scope))
126
+ throw new DaemonAuthorityError("identity", "daemon_authority_changed");
127
+ fresh.assertCurrent();
128
+ proof = fresh;
129
+ arm();
130
+ }
131
+ }
132
+ catch (error) {
133
+ if (error instanceof AggregateError) {
134
+ if (!active.aborted) {
135
+ const primary = error.errors[0];
136
+ fail(primary instanceof DaemonAuthorityError
137
+ ? primary
138
+ : new DaemonAuthorityError("authentication", "daemon_authority_cleanup_failed"));
139
+ }
140
+ failures.push(error);
141
+ throw error;
142
+ }
143
+ if (!active.aborted)
144
+ fail(error instanceof DaemonAuthorityError
145
+ ? error
146
+ : new DaemonAuthorityError("authentication", "daemon_authority_unavailable"));
147
+ // Only an actual parent/owned cancellation is an ordinary requested stop. The proof
148
+ // failure callback persists its reason; no automatic restart renews that latch.
149
+ }
150
+ finally {
151
+ if (timer !== undefined)
152
+ clearTimeout(timer);
153
+ if (notice)
154
+ await notice;
155
+ }
156
+ })();
157
+ void running.catch((error) => {
158
+ if (!failures.includes(error))
159
+ failures.push(error);
160
+ });
161
+ return running;
162
+ },
163
+ async stop() {
164
+ owned.abort(new Error("daemon_authority_stop"));
165
+ if (running)
166
+ await running.catch(() => { });
167
+ if (failures.length)
168
+ throw new AggregateError(failures, "daemon_authority_cleanup_failed");
169
+ },
170
+ };
171
+ }
@@ -0,0 +1,216 @@
1
+ import { configPathFor, } from "./config.js";
2
+ import { personCacheScope } from "./daemon-cache.js";
3
+ import { ownedFetch } from "./owned-fetch.js";
4
+ import { fetchMe } from "./transport.js";
5
+ import { onboardFileSnapshot } from "./onboard-file-snapshot.js";
6
+ export class DaemonAuthorityError extends Error {
7
+ kind;
8
+ constructor(kind, code) {
9
+ super(code);
10
+ this.kind = kind;
11
+ this.name = "DaemonAuthorityError";
12
+ }
13
+ }
14
+ const refused = (code) => new DaemonAuthorityError("authentication", code);
15
+ const changed = () => new DaemonAuthorityError("identity", "daemon_authority_changed");
16
+ const object = (value) => value !== null && typeof value === "object" && !Array.isArray(value)
17
+ ? value
18
+ : null;
19
+ function currentConfig(ctx) {
20
+ let bytes;
21
+ try {
22
+ bytes = onboardFileSnapshot(configPathFor(ctx.home));
23
+ }
24
+ catch {
25
+ throw refused("daemon_config_unverified");
26
+ }
27
+ if (bytes === null)
28
+ throw refused("daemon_login_required");
29
+ let parsed;
30
+ try {
31
+ parsed = JSON.parse(bytes);
32
+ }
33
+ catch {
34
+ throw refused("daemon_config_unverified");
35
+ }
36
+ const config = object(parsed), user = object(config?.user);
37
+ if (!config ||
38
+ !user ||
39
+ typeof config.account !== "string" ||
40
+ typeof config.baseUrl !== "string" ||
41
+ typeof config.slug !== "string" ||
42
+ typeof config.name !== "string" ||
43
+ typeof config.joinedAt !== "string" ||
44
+ typeof config.lastSkillBundleVersion !== "string" ||
45
+ config.principal !== "service" ||
46
+ typeof user.id !== "string" ||
47
+ typeof user.label !== "string" ||
48
+ !(user.email === null || typeof user.email === "string") ||
49
+ !(user.linearUserId === null || typeof user.linearUserId === "string") ||
50
+ (user.role !== "owner" && user.role !== "admin" && user.role !== "member"))
51
+ throw refused("daemon_config_unverified");
52
+ let permissions = null;
53
+ if (config.permissions !== null) {
54
+ if (!Array.isArray(config.permissions) || config.permissions.length > 128)
55
+ throw refused("daemon_config_unverified");
56
+ permissions = [];
57
+ for (const permission of config.permissions) {
58
+ if (typeof permission !== "string")
59
+ throw refused("daemon_config_unverified");
60
+ permissions.push(permission);
61
+ }
62
+ }
63
+ let credential;
64
+ if (typeof config.key === "string" &&
65
+ config.key.length > 0 &&
66
+ config.auth === undefined)
67
+ credential = { key: config.key };
68
+ else {
69
+ const auth = object(config.auth);
70
+ if (config.key !== undefined ||
71
+ !auth ||
72
+ auth.kind !== "oauth" ||
73
+ typeof auth.sessionId !== "string" ||
74
+ typeof auth.accessToken !== "string" ||
75
+ !auth.accessToken.length ||
76
+ auth.accessToken.length > 8192 ||
77
+ typeof auth.refreshToken !== "string" ||
78
+ typeof auth.expiresAt !== "string")
79
+ throw refused("daemon_config_unverified");
80
+ credential = {
81
+ auth: {
82
+ kind: "oauth",
83
+ sessionId: auth.sessionId,
84
+ accessToken: auth.accessToken,
85
+ refreshToken: auth.refreshToken,
86
+ expiresAt: auth.expiresAt,
87
+ },
88
+ };
89
+ }
90
+ return {
91
+ ...credential,
92
+ account: config.account,
93
+ baseUrl: config.baseUrl,
94
+ slug: config.slug,
95
+ name: config.name,
96
+ principal: config.principal,
97
+ permissions,
98
+ joinedAt: config.joinedAt,
99
+ lastSkillBundleVersion: config.lastSkillBundleVersion,
100
+ user: {
101
+ id: user.id,
102
+ label: user.label,
103
+ email: user.email,
104
+ linearUserId: user.linearUserId,
105
+ role: user.role,
106
+ },
107
+ };
108
+ }
109
+ function currentToken(config, now) {
110
+ if (config.key && !config.auth)
111
+ return config.key;
112
+ if (!config.key &&
113
+ config.auth &&
114
+ Date.parse(config.auth.expiresAt) - now > 30_000)
115
+ return config.auth.accessToken;
116
+ // This reader never refreshes or writes the saved login. Renewal needs separately owned IO.
117
+ throw refused("daemon_login_refresh_required");
118
+ }
119
+ function scopeOf(config, me) {
120
+ let scope;
121
+ try {
122
+ scope = personCacheScope(config, me);
123
+ }
124
+ catch {
125
+ throw changed();
126
+ }
127
+ if (scope.permissions !== null && !scope.permissions.includes("mirror:feed"))
128
+ throw refused("daemon_feed_permission_required");
129
+ return scope;
130
+ }
131
+ function sameScope(a, b) {
132
+ return JSON.stringify(a) === JSON.stringify(b);
133
+ }
134
+ /** A read-only proof expires 30s after request ENTRY, not after a potentially slow response.
135
+ * The owner must stop peers at expiry and join this promise before releasing any writer lease.
136
+ * Cancellation can remain pending beyond the deadline until actual upstream IO acknowledges it. */
137
+ export async function readDaemonAuthority(ctx, parent) {
138
+ if (parent.aborted)
139
+ throw parent.reason;
140
+ const enteredAt = ctx.now().getTime();
141
+ if (!Number.isSafeInteger(enteredAt))
142
+ throw refused("daemon_clock_invalid");
143
+ const config = currentConfig(ctx);
144
+ // Saved claims only preflight destination/credential shape; they never establish live authority.
145
+ scopeOf(config, config);
146
+ const bearer = currentToken(config, enteredAt);
147
+ const stopped = new AbortController();
148
+ const signal = AbortSignal.any([parent, stopped.signal]);
149
+ const request = ownedFetch(ctx.fetch, signal, { maxBodyBytes: 1024 * 1024 });
150
+ const timer = setTimeout(() => stopped.abort(refused("daemon_authority_expired")), 30_000);
151
+ let primary;
152
+ let failed = false;
153
+ let proof;
154
+ try {
155
+ const me = await fetchMe(config.baseUrl, bearer, request.fetch, 30_000);
156
+ if (signal.aborted)
157
+ throw signal.reason;
158
+ const scope = scopeOf(config, me);
159
+ const expiresAt = enteredAt + 30_000;
160
+ const assertCurrent = () => {
161
+ if (parent.aborted)
162
+ throw parent.reason;
163
+ const current = currentConfig(ctx);
164
+ if (!sameScope(scope, scopeOf(current, me)))
165
+ throw changed();
166
+ const now = ctx.now().getTime();
167
+ currentToken(current, now);
168
+ // No saved-file read follows the final clock/parent boundary.
169
+ if (parent.aborted)
170
+ throw parent.reason;
171
+ if (!Number.isSafeInteger(now) || now < enteredAt || now >= expiresAt)
172
+ throw refused("daemon_authority_expired");
173
+ };
174
+ assertCurrent();
175
+ proof = Object.freeze({
176
+ scope,
177
+ expiresAt,
178
+ assertCurrent,
179
+ async getToken() {
180
+ assertCurrent();
181
+ const current = currentConfig(ctx);
182
+ if (!sameScope(scope, scopeOf(current, me)))
183
+ throw changed();
184
+ // A saved rotation is not proof about that new bearer. Use only the /me-verified token
185
+ // until a separately owned fresh read replaces this proof.
186
+ assertCurrent();
187
+ return bearer;
188
+ },
189
+ });
190
+ }
191
+ catch (error) {
192
+ primary = signal.aborted
193
+ ? signal.reason
194
+ : error instanceof DaemonAuthorityError
195
+ ? error
196
+ : refused("daemon_authority_unavailable");
197
+ failed = true;
198
+ }
199
+ finally {
200
+ clearTimeout(timer);
201
+ // The wrapper joins entered headers, readers and FIRST cancel; a timeout is never an ACK.
202
+ try {
203
+ await request.settle();
204
+ }
205
+ catch (cleanup) {
206
+ throw new AggregateError(failed ? [primary, cleanup] : [cleanup], "daemon_authority_cleanup_failed");
207
+ }
208
+ }
209
+ if (failed)
210
+ throw primary;
211
+ if (!proof)
212
+ throw refused("daemon_authority_unverified");
213
+ // Even natural-response cleanup can be slow; publication checks the ORIGINAL expiry again.
214
+ proof.assertCurrent();
215
+ return proof;
216
+ }