@catalyst-cloud/cli 0.14.3 → 0.14.5

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.
@@ -1,13 +1,17 @@
1
- import { createOnboardProgress } from "./onboard-progress.js";
2
- import { ONBOARD_STEPS, ONBOARD_TITLES } from "./onboard.js";
1
+ import { createOnboardProgress, } from "./onboard-progress.js";
2
+ import { ONBOARD_STEPS, ONBOARD_TITLES, } from "./onboard.js";
3
3
  export function shouldUseOnboardUi(args, stdoutTty) {
4
- return stdoutTty && !args.json && args.flags.yes !== true && args.flags["dry-run"] !== true;
4
+ return (stdoutTty &&
5
+ !args.json &&
6
+ args.flags.yes !== true &&
7
+ args.flags["dry-run"] !== true);
5
8
  }
6
9
  export function createClackOnboardUi(prompts, streams, deps = {}) {
7
10
  const abort = new AbortController();
8
11
  const options = { ...streams, signal: abort.signal };
9
12
  // Clack 1.8.1's spinner calls process.exit(0) on raw Ctrl-C, bypassing receipt/lock cleanup.
10
- const spin = deps.progress ?? createOnboardProgress(streams.output, abort.signal, () => abort.abort());
13
+ const spin = deps.progress ??
14
+ createOnboardProgress(streams.output, abort.signal, () => abort.abort());
11
15
  const signals = deps.signals ?? process;
12
16
  const interrupt = () => abort.abort();
13
17
  for (const event of ["SIGINT", "SIGTERM", "SIGHUP"])
@@ -15,15 +19,23 @@ export function createClackOnboardUi(prompts, streams, deps = {}) {
15
19
  let active = false;
16
20
  let introduced = false;
17
21
  let group;
18
- const stop = () => { if (active) {
19
- spin.stop();
20
- active = false;
21
- } };
22
- const message = (text) => { stop(); prompts.log.message(text, { output: streams.output }); };
23
- const start = (text) => { stop(); if (!abort.signal.aborted) {
24
- spin.start(text);
25
- active = true;
26
- } };
22
+ const stop = () => {
23
+ if (active) {
24
+ spin.stop();
25
+ active = false;
26
+ }
27
+ };
28
+ const message = (text) => {
29
+ stop();
30
+ prompts.log.message(text, { output: streams.output });
31
+ };
32
+ const start = (text) => {
33
+ stop();
34
+ if (!abort.signal.aborted) {
35
+ spin.start(text);
36
+ active = true;
37
+ }
38
+ };
27
39
  return {
28
40
  signal: abort.signal,
29
41
  plan(journal) {
@@ -32,52 +44,89 @@ export function createClackOnboardUi(prompts, streams, deps = {}) {
32
44
  prompts.intro("Catalyst setup", { output: streams.output });
33
45
  introduced = true;
34
46
  }
35
- const steps = new Map(journal.steps.map(step => [step.id, step]));
36
- message(ONBOARD_STEPS.map(id => `· ${ONBOARD_TITLES[id]}${steps.get(id)?.state === "done" ? " (recheck)" : ""}`).join("\n"));
47
+ const steps = new Map(journal.steps.map((step) => [step.id, step]));
48
+ message(ONBOARD_STEPS.map((id) => `· ${ONBOARD_TITLES[id]}${steps.get(id)?.state === "done" ? " (recheck)" : ""}`).join("\n"));
37
49
  message("Use cloud reads by default. Local sync is optional for SQL, offline work or sustained reads.");
38
50
  },
39
51
  async confirmPlan(localSync) {
40
52
  stop();
41
- const answer = await prompts.select({ ...options, message: "Continue with this plan?", initialValue: localSync ? "local" : "cloud", options: [
42
- { value: "cloud", label: "Continue using cloud reads", hint: "default" },
53
+ const answer = await prompts.select({
54
+ ...options,
55
+ message: "Continue with this plan?",
56
+ initialValue: localSync ? "local" : "cloud",
57
+ options: [
58
+ {
59
+ value: "cloud",
60
+ label: "Continue using cloud reads",
61
+ hint: "default",
62
+ },
43
63
  { value: "local", label: "Continue and set up local sync" },
44
64
  { value: "stop", label: "Stop without changes" },
45
- ] });
65
+ ],
66
+ });
46
67
  if (prompts.isCancel(answer)) {
47
68
  abort.abort();
48
69
  return { proceed: false, localSync };
49
70
  }
50
- return { proceed: answer === "cloud" || answer === "local", localSync: answer === "local" };
71
+ return {
72
+ proceed: answer === "cloud" || answer === "local",
73
+ localSync: answer === "local",
74
+ };
51
75
  },
52
76
  async chooseTeam(teams) {
53
77
  stop();
54
78
  if (!teams.length || abort.signal.aborted)
55
79
  return null;
56
- const answer = await prompts.select({ ...options, message: "Which Linear team should this project use?", initialValue: teams[0].id,
57
- options: teams.map(team => ({ value: team.id, label: `${team.name || team.key || team.id}${team.key && team.name ? ` (${team.key})` : ""}` })) });
80
+ const answer = await prompts.select({
81
+ ...options,
82
+ message: "Which Linear team should this project use?",
83
+ initialValue: teams[0].id,
84
+ options: teams.map((team) => ({
85
+ value: team.id,
86
+ label: `${team.name || team.key || team.id}${team.key && team.name ? ` (${team.key})` : ""}`,
87
+ })),
88
+ });
58
89
  if (prompts.isCancel(answer)) {
59
90
  abort.abort();
60
91
  return null;
61
92
  }
62
93
  return typeof answer === "string" ? answer : null;
63
94
  },
64
- ...(prompts.multiselect ? { async chooseRepositories(repositories) {
65
- stop();
66
- if (!repositories.length || abort.signal.aborted)
67
- return null;
68
- const answer = await prompts.multiselect({ ...options, message: "Which repositories should use this Linear team?", required: true,
69
- options: repositories.map(row => ({ value: `${row.owner}/${row.name}`, label: `${row.owner}/${row.name}` })) });
70
- if (prompts.isCancel(answer)) {
71
- abort.abort();
72
- return null;
73
- }
74
- return Array.isArray(answer) ? answer : null;
75
- } } : {}),
95
+ ...(prompts.multiselect
96
+ ? {
97
+ async chooseRepositories(repositories) {
98
+ stop();
99
+ if (!repositories.length || abort.signal.aborted)
100
+ return null;
101
+ const answer = await prompts.multiselect({
102
+ ...options,
103
+ message: "Which repositories should use this Linear team?",
104
+ required: true,
105
+ options: repositories.map((row) => ({
106
+ value: `${row.owner}/${row.name}`,
107
+ label: `${row.owner}/${row.name}`,
108
+ })),
109
+ });
110
+ if (prompts.isCancel(answer)) {
111
+ abort.abort();
112
+ return null;
113
+ }
114
+ return Array.isArray(answer) ? answer : null;
115
+ },
116
+ }
117
+ : {}),
76
118
  stepStart(id) {
77
- const next = ["machine", "cli", "skills", "legacy"].includes(id) ? "This computer"
78
- : id === "signin" ? "Catalyst sign-in" : id.startsWith("linear.") || id.startsWith("github.") ? "Connections"
79
- : ["projects", "accounts", "settings", "values"].includes(id) ? "Project setup"
80
- : ["capacity", "daemon", "housekeeping"].includes(id) ? "Runner and services" : "Work and readiness";
119
+ const next = ["machine", "cli", "skills", "legacy"].includes(id)
120
+ ? "This computer"
121
+ : id === "signin"
122
+ ? "Catalyst sign-in"
123
+ : id.startsWith("linear.") || id.startsWith("github.")
124
+ ? "Connections"
125
+ : ["projects", "accounts", "settings", "values"].includes(id)
126
+ ? "Project setup"
127
+ : ["capacity", "daemon", "housekeeping"].includes(id)
128
+ ? "Runner and services"
129
+ : "Work and readiness";
81
130
  if (next !== group) {
82
131
  message(next);
83
132
  group = next;
@@ -87,7 +136,24 @@ export function createClackOnboardUi(prompts, streams, deps = {}) {
87
136
  stepEnd(step) {
88
137
  stop();
89
138
  const reasons = {
90
- cloud_capability_unavailable: "Your cloud does not support this setup step yet.",
139
+ personal_consent_handoff: "The browser approval link could not be verified. Run catalyst onboard to try again.",
140
+ personal_identity_refused: "Your current login no longer matches this setup. Resume with the original workspace and person.",
141
+ personal_login_refresh_required: "Renew your login with catalyst login, then run catalyst onboard to resume.",
142
+ personal_status_unavailable: "Your personal connection could not be checked. Run catalyst onboard to try again.",
143
+ personal_browser_unavailable: "The browser could not open. Run catalyst onboard to try again.",
144
+ cloud_capability_unavailable: "This setup step is not available on this server yet. Use the web app link shown above, then resume after the server update.",
145
+ onboarding_capability_unavailable: "The server's setup capabilities could not be checked. Run catalyst onboard to try again.",
146
+ onboarding_capability_identity_unverified: "The server's setup capabilities belong to another login or workspace. Sign in to the original workspace to resume.",
147
+ onboarding_capability_login_refresh_required: "Your login needs a refresh before setup capabilities can be checked. Run catalyst login, then catalyst onboard.",
148
+ workspace_consent_refused: "The server did not accept this login for Linear workspace setup. Sign in again or ask your workspace administrator.",
149
+ github_installation_status_unavailable: "Your GitHub App installation could not be checked. Run catalyst onboard to try again.",
150
+ github_installation_status_shape: "The server returned a GitHub installation result that could not be verified.",
151
+ github_installation_consent_handoff: "The GitHub installation approval link could not be verified. Run catalyst onboard to try again.",
152
+ github_installation_consent_refused: "The server did not accept this login for GitHub App setup. Sign in again or ask your workspace administrator.",
153
+ github_installation_identity_refused: "GitHub setup belongs to another login or workspace. Sign in to the original workspace to resume.",
154
+ github_installation_admin_required: "A workspace owner or administrator approves the GitHub App installation.",
155
+ github_installation_login_refresh_required: "Your login needs a refresh. Run catalyst onboard to resume.",
156
+ github_installation_browser_unavailable: "The GitHub App approval page could not be opened. Use your workspace's Connections page, then resume.",
91
157
  step_not_available_in_this_release: "This setup step is not available yet.",
92
158
  prerequisite_not_ready: "Waiting for an earlier setup step.",
93
159
  local_sync_not_selected: "Using cloud reads. Local sync was not selected.",
@@ -115,27 +181,39 @@ export function createClackOnboardUi(prompts, streams, deps = {}) {
115
181
  repository_selection_unverified: "Your selected repositories could not be verified. Run catalyst onboard --repo <owner/name> to choose again.",
116
182
  };
117
183
  const text = `${ONBOARD_TITLES[step.id]}${step.reason ? `: ${reasons[step.reason] ?? step.reason.replaceAll("_", " ")}` : ""}`;
118
- const kind = step.state === "done" ? "info" : step.state === "failed" ? "error" : step.state === "waiting" || step.state === "pending" ? "warn" : "message";
119
- prompts.log[kind](`${step.state === "done" ? "✓ " : ""}${text}`, { output: streams.output });
184
+ const kind = step.state === "done"
185
+ ? "info"
186
+ : step.state === "failed"
187
+ ? "error"
188
+ : step.state === "waiting" || step.state === "pending"
189
+ ? "warn"
190
+ : "message";
191
+ prompts.log[kind](`${step.state === "done" ? "✓ " : ""}${text}`, {
192
+ output: streams.output,
193
+ });
120
194
  },
121
195
  message,
122
196
  finish(journal, only) {
123
197
  stop();
124
198
  let text = "Setup still needs checks. Run catalyst onboard to resume.";
125
199
  if (abort.signal.aborted)
126
- text = "Setup paused. Your progress is saved.\nresume: catalyst onboard";
200
+ text =
201
+ "Setup paused. Your progress is saved.\nresume: catalyst onboard";
127
202
  else if (journal.complete && journal.exit === 0 && !only)
128
203
  text = "Onboarding complete.";
129
204
  else if (only && journal.exit === 0)
130
205
  text = `${ONBOARD_TITLES[only]} finished. Onboarding still has other steps.\nresume: catalyst onboard`;
131
206
  prompts.outro(text, { output: streams.output });
132
207
  },
133
- async wait(text, run) { start(text); try {
134
- return await run();
135
- }
136
- finally {
137
- stop();
138
- } },
208
+ async wait(text, run) {
209
+ start(text);
210
+ try {
211
+ return await run();
212
+ }
213
+ finally {
214
+ stop();
215
+ }
216
+ },
139
217
  dispose() {
140
218
  try {
141
219
  stop();
@@ -3,49 +3,90 @@ import { pollConsent } from "./onboard-consent.js";
3
3
  const STATUS = "/api/v1/me/connections/linear/workspace";
4
4
  const HANDOFF = "/connect/linear/workspace/handoff";
5
5
  const idPattern = /^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/;
6
- const states = new Set(["connected", "missing-scope", "expired-or-revoked", "unreachable", "not-connected"]);
7
- const warmth = new Set(["warm", "absent", "expired", "renewal-due", "no-expiry", "unavailable"]);
8
- const object = (v) => v !== null && typeof v === "object" && !Array.isArray(v) ? v : null;
9
- const waiting = (reason) => ({ state: "waiting", reason });
6
+ const states = new Set([
7
+ "connected",
8
+ "missing-scope",
9
+ "expired-or-revoked",
10
+ "unreachable",
11
+ "not-connected",
12
+ ]);
13
+ const warmth = new Set([
14
+ "warm",
15
+ "absent",
16
+ "expired",
17
+ "renewal-due",
18
+ "no-expiry",
19
+ "unavailable",
20
+ ]);
21
+ const object = (v) => v !== null && typeof v === "object" && !Array.isArray(v)
22
+ ? v
23
+ : null;
24
+ const waiting = (reason) => ({
25
+ state: "waiting",
26
+ reason,
27
+ });
10
28
  const nullableText = (v) => v === null || typeof v === "string";
11
- const nullableTime = (v) => v === null || typeof v === "number" && Number.isFinite(v) && v >= 0;
29
+ const nullableTime = (v) => v === null || (typeof v === "number" && Number.isFinite(v) && v >= 0);
12
30
  /** A bounded read never rotates credentials or writes a token after the receipt lock is released. */
13
31
  async function read(ctx, journal, path, timeoutMs, external) {
14
32
  const cfg = loadConfig(ctx.home);
15
33
  if (!cfg?.user)
16
34
  return { reason: "personal_login_required" };
17
- if ((journal.account ?? journal.tenant) && (journal.account ?? journal.tenant) !== cfg.account ||
18
- journal.membershipId && journal.membershipId !== cfg.user.id ||
19
- journal.baseUrl && normalizeBaseUrl(journal.baseUrl) !== normalizeBaseUrl(cfg.baseUrl))
35
+ if (((journal.account ?? journal.tenant) &&
36
+ (journal.account ?? journal.tenant) !== cfg.account) ||
37
+ (journal.membershipId && journal.membershipId !== cfg.user.id) ||
38
+ (journal.baseUrl &&
39
+ normalizeBaseUrl(journal.baseUrl) !== normalizeBaseUrl(cfg.baseUrl)))
20
40
  return { reason: "workspace_identity_refused" };
21
- const expiry = cfg.auth ? Date.parse(cfg.auth.expiresAt) - ctx.now().getTime() : 0;
22
- const bearer = cfg.key || (cfg.auth && Number.isFinite(expiry) && expiry > 30_000 ? cfg.auth.accessToken : undefined);
41
+ const expiry = cfg.auth
42
+ ? Date.parse(cfg.auth.expiresAt) - ctx.now().getTime()
43
+ : 0;
44
+ const bearer = cfg.key ||
45
+ (cfg.auth && Number.isFinite(expiry) && expiry > 30_000
46
+ ? cfg.auth.accessToken
47
+ : undefined);
23
48
  if (!bearer)
24
49
  return { reason: "workspace_login_refresh_required" };
25
50
  const deadline = new AbortController();
26
51
  const timer = setTimeout(() => deadline.abort(), timeoutMs);
27
- const signal = external ? AbortSignal.any([external, deadline.signal]) : deadline.signal;
52
+ const signal = external
53
+ ? AbortSignal.any([external, deadline.signal])
54
+ : deadline.signal;
28
55
  let remove = () => { };
29
56
  try {
30
- return await new Promise(resolve => {
31
- const stopped = () => resolve({ reason: external?.aborted ? "interrupted" : "workspace_status_unavailable" });
57
+ return await new Promise((resolve) => {
58
+ const stopped = () => resolve({
59
+ reason: external?.aborted
60
+ ? "interrupted"
61
+ : "workspace_status_unavailable",
62
+ });
32
63
  if (signal.aborted) {
33
64
  stopped();
34
65
  return;
35
66
  }
36
67
  signal.addEventListener("abort", stopped, { once: true });
37
68
  remove = () => signal.removeEventListener("abort", stopped);
38
- Promise.resolve().then(async () => {
69
+ Promise.resolve()
70
+ .then(async () => {
39
71
  if (signal.aborted)
40
72
  return { reason: "interrupted" };
41
73
  const response = await ctx.fetch(`${normalizeBaseUrl(cfg.baseUrl)}${path}`, {
42
- method: "GET", redirect: "error", headers: { authorization: `Bearer ${bearer}`, accept: "application/json" }, signal,
74
+ method: "GET",
75
+ redirect: "error",
76
+ headers: {
77
+ authorization: `Bearer ${bearer}`,
78
+ accept: "application/json",
79
+ },
80
+ signal,
43
81
  });
44
82
  // Refusal bodies and provider diagnostics are never parsed, returned or printed.
45
83
  if (response.status !== 200)
46
84
  return { status: response.status };
47
- return { status: 200, body: await response.json() };
48
- }).then(value => signal.aborted ? stopped() : resolve(value), () => signal.aborted ? stopped() : resolve({ reason: "workspace_status_unavailable" }));
85
+ return { status: 200, body: (await response.json()) };
86
+ })
87
+ .then((value) => (signal.aborted ? stopped() : resolve(value)), () => signal.aborted
88
+ ? stopped()
89
+ : resolve({ reason: "workspace_status_unavailable" }));
49
90
  });
50
91
  }
51
92
  finally {
@@ -55,8 +96,10 @@ async function read(ctx, journal, path, timeoutMs, external) {
55
96
  }
56
97
  function readFailure(value) {
57
98
  if ("reason" in value)
58
- return value.reason === "workspace_identity_refused" || value.reason === "personal_login_required"
59
- ? { state: "refused", reason: value.reason } : waiting(value.reason);
99
+ return value.reason === "workspace_identity_refused" ||
100
+ value.reason === "personal_login_required"
101
+ ? { state: "refused", reason: value.reason }
102
+ : waiting(value.reason);
60
103
  if (value.status === 401 || value.status === 403)
61
104
  return { state: "refused", reason: "workspace_consent_refused" };
62
105
  return value.status === 200 ? null : waiting("workspace_status_unavailable");
@@ -67,16 +110,33 @@ function observation(body, now) {
67
110
  const workspace = object(row?.workspace);
68
111
  const credential = object(row?.credential);
69
112
  const verification = object(row?.verification);
70
- if (!row || typeof row.connected !== "boolean" || !workspace || typeof workspace.bound !== "boolean" ||
71
- !nullableText(workspace.workspaceId) || !nullableText(workspace.workspaceSlug) || !nullableText(workspace.workspaceName) ||
72
- !credential || ![true, false, null].includes(credential.stored) ||
73
- !nullableTime(credential.updatedAt) || !nullableTime(credential.expiresAt) || (typeof credential.warmth !== "string" || !warmth.has(credential.warmth)) || !verification)
113
+ if (!row ||
114
+ typeof row.connected !== "boolean" ||
115
+ !workspace ||
116
+ typeof workspace.bound !== "boolean" ||
117
+ !nullableText(workspace.workspaceId) ||
118
+ !nullableText(workspace.workspaceSlug) ||
119
+ !nullableText(workspace.workspaceName) ||
120
+ !credential ||
121
+ ![true, false, null].includes(credential.stored) ||
122
+ !nullableTime(credential.updatedAt) ||
123
+ !nullableTime(credential.expiresAt) ||
124
+ typeof credential.warmth !== "string" ||
125
+ !warmth.has(credential.warmth) ||
126
+ !verification)
74
127
  return waiting("workspace_status_shape");
75
- if (verification.source === "none" && verification.state === "not-run" && verification.checkedAt === null && row.connected === false)
128
+ if (verification.source === "none" &&
129
+ verification.state === "not-run" &&
130
+ verification.checkedAt === null &&
131
+ row.connected === false)
76
132
  return waiting("workspace_status_unavailable");
77
- if (verification.source !== "live-probe" || (typeof verification.state !== "string" || !states.has(verification.state)) ||
78
- typeof verification.checkedAt !== "number" || !Number.isFinite(verification.checkedAt) ||
79
- verification.checkedAt < now - 120_000 || verification.checkedAt > now + 30_000 ||
133
+ if (verification.source !== "live-probe" ||
134
+ typeof verification.state !== "string" ||
135
+ !states.has(verification.state) ||
136
+ typeof verification.checkedAt !== "number" ||
137
+ !Number.isFinite(verification.checkedAt) ||
138
+ verification.checkedAt < now - 120_000 ||
139
+ verification.checkedAt > now + 30_000 ||
80
140
  row.connected !== (verification.state === "connected"))
81
141
  return waiting("workspace_status_shape");
82
142
  if (verification.state === "unreachable")
@@ -84,15 +144,29 @@ function observation(body, now) {
84
144
  if (!row.connected)
85
145
  return { state: "pending" };
86
146
  // A working credential alone cannot prove this account has a bound Linear workspace.
87
- if (!workspace.bound || typeof workspace.workspaceId !== "string" || !idPattern.test(workspace.workspaceId))
147
+ if (!workspace.bound ||
148
+ typeof workspace.workspaceId !== "string" ||
149
+ !idPattern.test(workspace.workspaceId))
88
150
  return waiting("linear_workspace_unverified");
89
- return { state: "done", evidence: { provider: "linear", workspace: workspace.workspaceId, checkedAt: verification.checkedAt } };
151
+ return {
152
+ state: "done",
153
+ evidence: {
154
+ provider: "linear",
155
+ workspace: workspace.workspaceId,
156
+ checkedAt: verification.checkedAt,
157
+ },
158
+ };
90
159
  }
91
160
  function safeHandoff(body, ctx) {
92
161
  const row = object(body);
93
162
  const cfg = loadConfig(ctx.home);
94
- if (!cfg || !row || typeof row.authorizationUrl !== "string" || row.authorizationUrl.length > 8192 || /[\u0000-\u0020\u007f]/.test(row.authorizationUrl) ||
95
- typeof row.expiresAt !== "number" || !Number.isFinite(row.expiresAt))
163
+ if (!cfg ||
164
+ !row ||
165
+ typeof row.authorizationUrl !== "string" ||
166
+ row.authorizationUrl.length > 8192 ||
167
+ /[\u0000-\u0020\u007f]/.test(row.authorizationUrl) ||
168
+ typeof row.expiresAt !== "number" ||
169
+ !Number.isFinite(row.expiresAt))
96
170
  return null;
97
171
  const remaining = row.expiresAt - ctx.now().getTime();
98
172
  // Thirty seconds accommodates clock skew; an expired link is never launched.
@@ -102,9 +176,17 @@ function safeHandoff(body, ctx) {
102
176
  const url = new URL(row.authorizationUrl);
103
177
  const keys = [...url.searchParams.keys()];
104
178
  const handoff = url.searchParams.get("handoff");
105
- if (url.protocol !== "https:" || url.origin !== new URL(normalizeBaseUrl(cfg.baseUrl)).origin || url.username || url.password ||
106
- url.pathname !== HANDOFF || url.hash || keys.length !== 1 || keys[0] !== "handoff" ||
107
- !handoff || handoff.length > 4096 || /[\s\u0000-\u001f\u007f]/.test(handoff))
179
+ if (url.protocol !== "https:" ||
180
+ url.origin !== new URL(normalizeBaseUrl(cfg.baseUrl)).origin ||
181
+ url.username ||
182
+ url.password ||
183
+ url.pathname !== HANDOFF ||
184
+ url.hash ||
185
+ keys.length !== 1 ||
186
+ keys[0] !== "handoff" ||
187
+ !handoff ||
188
+ handoff.length > 4096 ||
189
+ /[\s\u0000-\u001f\u007f]/.test(handoff))
108
190
  return null;
109
191
  return url.toString();
110
192
  }
@@ -112,19 +194,29 @@ function safeHandoff(body, ctx) {
112
194
  return null;
113
195
  }
114
196
  }
115
- /** Uses the new personal bearer twins; old clouds retain their supported read-only readiness path. */
197
+ /** Uses personal bearer twins; missing routes wait without an ungated readiness fallback. */
116
198
  export function linearWorkspaceAdapter(options) {
117
199
  const timeoutMs = options.requestTimeoutMs ?? 30_000;
118
200
  const consentTimeoutMs = options.consentTimeoutMs ?? 600_000;
119
- if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 30_000 ||
120
- !Number.isSafeInteger(consentTimeoutMs) || consentTimeoutMs < 1 || consentTimeoutMs > 600_000)
201
+ if (!Number.isSafeInteger(timeoutMs) ||
202
+ timeoutMs < 1 ||
203
+ timeoutMs > 30_000 ||
204
+ !Number.isSafeInteger(consentTimeoutMs) ||
205
+ consentTimeoutMs < 1 ||
206
+ consentTimeoutMs > 600_000)
121
207
  throw new Error("workspace_timeout_invalid");
122
208
  const inspect = async (ctx, journal, signal) => {
123
209
  const result = await read(ctx, journal, STATUS, timeoutMs, signal);
124
- if ("status" in result && result.status === 404)
125
- return { result: await options.fallback.check(ctx, journal, signal), unsupported: true };
210
+ if ("status" in result && [404, 405].includes(result.status))
211
+ return {
212
+ result: waiting("cloud_capability_unavailable"),
213
+ unsupported: true,
214
+ };
126
215
  const failure = readFailure(result);
127
- return { result: failure ?? observation("body" in result ? result.body : undefined, ctx.now().getTime()) };
216
+ return {
217
+ result: failure ??
218
+ observation("body" in result ? result.body : undefined, ctx.now().getTime()),
219
+ };
128
220
  };
129
221
  return {
130
222
  check: async (ctx, journal, signal) => (await inspect(ctx, journal, signal)).result,
@@ -140,7 +232,7 @@ export function linearWorkspaceAdapter(options) {
140
232
  if (before.unsupported)
141
233
  return waiting("cloud_capability_unavailable");
142
234
  const start = await read(ctx, journal, `${STATUS}/start`, timeoutMs, signal);
143
- if ("status" in start && start.status === 404)
235
+ if ("status" in start && [404, 405].includes(start.status))
144
236
  return waiting("cloud_capability_unavailable");
145
237
  const failure = readFailure(start);
146
238
  if (failure)
@@ -151,7 +243,9 @@ export function linearWorkspaceAdapter(options) {
151
243
  if (signal?.aborted)
152
244
  return waiting("interrupted");
153
245
  const current = loadConfig(ctx.home);
154
- if (!current?.user || current.account !== cfg.account || current.user.id !== cfg.user.id ||
246
+ if (!current?.user ||
247
+ current.account !== cfg.account ||
248
+ current.user.id !== cfg.user.id ||
155
249
  normalizeBaseUrl(current.baseUrl) !== normalizeBaseUrl(cfg.baseUrl))
156
250
  return { state: "refused", reason: "workspace_identity_refused" };
157
251
  if (!["owner", "admin"].includes(current.user.role))
@@ -165,20 +259,30 @@ export function linearWorkspaceAdapter(options) {
165
259
  return waiting("workspace_browser_unavailable");
166
260
  }
167
261
  let latest = waiting("workspace_status_unavailable");
168
- const run = () => pollConsent({ timeoutMs: consentTimeoutMs, signal, sleep: options.sleep,
262
+ const run = () => pollConsent({
263
+ timeoutMs: consentTimeoutMs,
264
+ signal,
265
+ sleep: options.sleep,
169
266
  readStatus: async (pollSignal) => {
170
267
  const value = await inspect(ctx, journal, pollSignal);
171
268
  latest = value.result;
172
269
  if (value.unsupported)
173
- return { outcome: "failed", reason: "cloud_capability_unavailable" };
270
+ return {
271
+ outcome: "waiting",
272
+ reason: "cloud_capability_unavailable",
273
+ };
174
274
  if (latest.state === "done")
175
275
  return { outcome: "connected" };
176
276
  if (latest.state === "refused" || latest.state === "failed")
177
277
  return { outcome: latest.state, reason: latest.reason };
178
- return { outcome: latest.state === "pending" ? "absent" : "unavailable" };
278
+ return {
279
+ outcome: latest.state === "pending" ? "absent" : "unavailable",
280
+ };
179
281
  },
180
282
  });
181
- const result = options.wait ? await options.wait("Waiting for workspace approval", run) : await run();
283
+ const result = options.wait
284
+ ? await options.wait("Waiting for workspace approval", run)
285
+ : await run();
182
286
  return result.state === "done" ? latest : result;
183
287
  },
184
288
  };