@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,182 @@
1
+ export function ownedFetch(fetchImpl, parent, options = {}) {
2
+ const limit = options.maxBodyBytes;
3
+ if (limit !== undefined && (!Number.isSafeInteger(limit) || limit < 1))
4
+ throw new Error("owned_fetch_body_limit_invalid");
5
+ const owned = new AbortController();
6
+ const requests = new Set();
7
+ const bodies = new Set();
8
+ const cleanupErrors = [];
9
+ let settling = false;
10
+ const stopped = () => owned.abort(parent.reason);
11
+ parent.addEventListener("abort", stopped, { once: true });
12
+ if (parent.aborted)
13
+ stopped();
14
+ const trackedFetch = (input, init) => {
15
+ if (owned.signal.aborted)
16
+ return Promise.reject(owned.signal.reason);
17
+ if (settling)
18
+ return Promise.reject(new Error("owned_fetch_stopped"));
19
+ const requestSignal = input instanceof Request ? input.signal : undefined;
20
+ const signal = AbortSignal.any([
21
+ owned.signal,
22
+ ...(requestSignal ? [requestSignal] : []),
23
+ ...(init?.signal ? [init.signal] : []),
24
+ ]);
25
+ if (signal.aborted)
26
+ return Promise.reject(signal.reason);
27
+ const network = new AbortController();
28
+ const stopHeaders = () => network.abort(signal.reason);
29
+ signal.addEventListener("abort", stopHeaders, { once: true });
30
+ const pending = (async () => {
31
+ let response;
32
+ try {
33
+ response = await fetchImpl(input, {
34
+ ...init,
35
+ signal: network.signal,
36
+ redirect: "error",
37
+ });
38
+ }
39
+ finally {
40
+ signal.removeEventListener("abort", stopHeaders);
41
+ }
42
+ if (!response.body) {
43
+ if (signal.aborted)
44
+ throw signal.reason;
45
+ return response;
46
+ }
47
+ const reader = response.body.getReader();
48
+ let bodyController;
49
+ let terminal = false;
50
+ let consumed = 0;
51
+ let completed = false;
52
+ let enteredRead;
53
+ let firstCancel;
54
+ let complete;
55
+ const joined = new Promise((resolve) => {
56
+ complete = resolve;
57
+ });
58
+ bodies.add(joined);
59
+ const finish = () => {
60
+ if (completed)
61
+ return;
62
+ completed = true;
63
+ signal.removeEventListener("abort", aborted);
64
+ try {
65
+ reader.releaseLock();
66
+ }
67
+ catch (error) {
68
+ cleanupErrors.push(error);
69
+ }
70
+ complete();
71
+ };
72
+ const cancel = (reason) => {
73
+ if (firstCancel)
74
+ return firstCancel;
75
+ if (completed)
76
+ return Promise.resolve();
77
+ terminal = true;
78
+ // Retain this FIRST promise: a second cancel can resolve before underlying IO acknowledges it.
79
+ firstCancel = reader.cancel(reason);
80
+ // Close/error the owned reader FIRST. Aborting native fetch first can instead error its
81
+ // upstream stream and turn an otherwise acknowledged cancellation into a rejection.
82
+ network.abort(reason);
83
+ const read = enteredRead;
84
+ void Promise.allSettled([firstCancel, ...(read ? [read] : [])]).then((results) => {
85
+ const cancellation = results[0];
86
+ if (cancellation?.status === "rejected")
87
+ cleanupErrors.push(cancellation.reason);
88
+ finish();
89
+ });
90
+ return firstCancel;
91
+ };
92
+ const aborted = () => {
93
+ if (completed)
94
+ return;
95
+ terminal = true;
96
+ // An abort is an error, never successful EOF that could publish a truncated page.
97
+ try {
98
+ bodyController?.error(signal.reason);
99
+ }
100
+ catch {
101
+ /* already terminal */
102
+ }
103
+ void cancel(signal.reason).catch(() => { });
104
+ };
105
+ const body = new ReadableStream({
106
+ start(controller) {
107
+ bodyController = controller;
108
+ signal.addEventListener("abort", aborted, { once: true });
109
+ if (signal.aborted)
110
+ aborted();
111
+ },
112
+ async pull(controller) {
113
+ if (terminal)
114
+ return;
115
+ const read = reader.read();
116
+ enteredRead = read;
117
+ try {
118
+ const result = await read;
119
+ if (terminal || signal.aborted)
120
+ return;
121
+ if (result.done) {
122
+ terminal = true;
123
+ controller.close();
124
+ finish();
125
+ }
126
+ else {
127
+ if (limit !== undefined &&
128
+ result.value.byteLength > limit - consumed)
129
+ throw new Error("owned_fetch_body_limit");
130
+ consumed += result.value.byteLength;
131
+ controller.enqueue(result.value);
132
+ }
133
+ }
134
+ catch (error) {
135
+ if (!terminal) {
136
+ terminal = true;
137
+ controller.error(error);
138
+ void cancel(error).catch(() => { });
139
+ }
140
+ }
141
+ finally {
142
+ if (enteredRead === read)
143
+ enteredRead = undefined;
144
+ }
145
+ },
146
+ cancel(reason) {
147
+ return cancel(reason);
148
+ },
149
+ });
150
+ if (signal.aborted) {
151
+ aborted();
152
+ await joined;
153
+ throw signal.reason;
154
+ }
155
+ return new Response(body, {
156
+ status: response.status,
157
+ statusText: response.statusText,
158
+ headers: response.headers,
159
+ });
160
+ })();
161
+ requests.add(pending);
162
+ void pending.then(() => requests.delete(pending), () => requests.delete(pending));
163
+ return pending;
164
+ };
165
+ return {
166
+ fetch: trackedFetch,
167
+ abort(reason) {
168
+ owned.abort(reason);
169
+ },
170
+ async settle() {
171
+ settling = true;
172
+ owned.abort(new Error("owned_fetch_settling"));
173
+ // A fetch that ignores its signal stays owned. Its eventual body is cancelled before return.
174
+ // This join does not pretend a deadline proves that non-cooperative IO has stopped.
175
+ await Promise.allSettled([...requests]);
176
+ await Promise.all([...bodies]);
177
+ parent.removeEventListener("abort", stopped);
178
+ if (cleanupErrors.length > 0)
179
+ throw new AggregateError(cleanupErrors, "owned_fetch_cleanup_failed");
180
+ },
181
+ };
182
+ }
@@ -12,6 +12,8 @@
12
12
  import { CliError } from "./errors.js";
13
13
  export const NEXT_CURSOR_HEADER = "x-mirror-next-cursor";
14
14
  export const TOTAL_HEADER = "x-mirror-total";
15
+ /** CTC-4556 — the `/issues` params that narrowed the page, comma-separated; absent on older mirrors. */
16
+ export const SCOPE_HEADER = "x-mirror-scope";
15
17
  /** The server clamps every page to this regardless of the requested `limit` (MAX_VIEW_LIMIT). */
16
18
  export const SERVER_PAGE_CAP = 500;
17
19
  /** 100 pages × 500 rows = 50,000. Reaching it is a refusal, not an ending. */
@@ -29,6 +31,12 @@ export function rowsOf(body) {
29
31
  }
30
32
  return [];
31
33
  }
34
+ function readScope(headers) {
35
+ const raw = headers.get(SCOPE_HEADER);
36
+ if (raw === null)
37
+ return null;
38
+ return raw.split(",").map((p) => p.trim()).filter(Boolean);
39
+ }
32
40
  function readTotal(headers) {
33
41
  const raw = headers.get(TOTAL_HEADER);
34
42
  if (raw === null)
@@ -43,6 +51,7 @@ export async function fetchPage(api, path, query) {
43
51
  rows: rowsOf(res.body),
44
52
  nextCursor: next === null || next === "" ? null : next,
45
53
  total: readTotal(res.headers),
54
+ scope: readScope(res.headers),
46
55
  };
47
56
  }
48
57
  export async function fetchAllPages(api, path, query) {
@@ -50,13 +59,16 @@ export async function fetchAllPages(api, path, query) {
50
59
  const seen = new Set();
51
60
  let after;
52
61
  let total = null;
62
+ let scope = null;
53
63
  for (let pages = 1; pages <= MAX_PAGES; pages++) {
54
64
  const page = await fetchPage(api, path, { ...query, limit: SERVER_PAGE_CAP, after });
55
65
  rows.push(...page.rows);
56
66
  if (page.total !== null)
57
67
  total = page.total;
68
+ // A param counts as applied only if every page says so.
69
+ scope = pages === 1 ? page.scope : scope && page.scope && scope.filter((p) => page.scope?.includes(p));
58
70
  if (page.nextCursor === null)
59
- return { rows, total, pages };
71
+ return { rows, total, pages, scope };
60
72
  // ⛔ A cursor that does not advance is an infinite loop, not a long read.
61
73
  if (seen.has(page.nextCursor)) {
62
74
  throw new CliError(`GET ${path} repeated a page cursor after ${rows.length} rows — the cloud is not advancing the keyset, so --all cannot finish`, "pagination-stalled", 1);
package/dist/query.js CHANGED
@@ -1,12 +1,12 @@
1
1
  // query.ts — `query issues|issue|pulls|pull|projects|cycles|search|changes`: the replica when it is
2
- // fresh, else the API, with the source named on stderr every time. Output shape is the read-model
2
+ // explicitly selected, otherwise the API, with the source named on stderr every time. Output shape is the read-model
3
3
  // view either way. `pull`, `cycles`, `search` and `changes` are API-only (the SDK wraps no view for
4
4
  // them and the read-model package is not a declared dependency of this bundle).
5
- import { flagBool, flagInt, flagString, positionals } from "./args.js";
6
- import { requireConfig, replicaDbPath } from "./config.js";
5
+ import { flagBool, flagInt, flagString, positionals, } from "./args.js";
6
+ import { requireConfig, replicaDbPath, } from "./config.js";
7
7
  import { CliError, UsageError } from "./errors.js";
8
8
  import { apiClient } from "./transport.js";
9
- import { fetchAllPages, fetchPage, rowsOf, truncationNotice } from "./pagination.js";
9
+ import { fetchAllPages, fetchPage, rowsOf, truncationNotice, } from "./pagination.js";
10
10
  import { engineFor, replicaStatus } from "./replica.js";
11
11
  import { loadSdk } from "./sdk.js";
12
12
  export { rowsOf };
@@ -20,9 +20,10 @@ export async function cmdQuery(args, ctx, deps = {}) {
20
20
  throw new UsageError("query needs a subcommand: issues | issue <id> | pulls | pull <id> | projects | cycles | search <terms> | changes --since <n>");
21
21
  const cfg = requireConfig(ctx);
22
22
  const filters = {
23
- team: flagString(args, "team"),
24
- project: flagString(args, "project"),
25
- state: flagString(args, "state"),
23
+ // CTC-4556 — trimmed here, so the value sent and the value compared are the one the mirror reads.
24
+ team: trimmedFlag(args, "team"),
25
+ project: trimmedFlag(args, "project"),
26
+ state: trimmedFlag(args, "state"),
26
27
  limit: flagInt(args, "limit", 50),
27
28
  all: flagBool(args, "all"),
28
29
  };
@@ -38,7 +39,9 @@ export async function cmdQuery(args, ctx, deps = {}) {
38
39
  if (filters.all && flagString(args, "limit") !== undefined) {
39
40
  ctx.stderr("--all reads the whole scope, so --limit is ignored");
40
41
  }
41
- const status = replicaStatus(ctx, cfg);
42
+ const status = forced === "replica" && REPLICA_CAPABLE.has(sub)
43
+ ? replicaStatus(ctx, cfg)
44
+ : null;
42
45
  let source;
43
46
  let why;
44
47
  if (!REPLICA_CAPABLE.has(sub)) {
@@ -53,19 +56,17 @@ export async function cmdQuery(args, ctx, deps = {}) {
53
56
  source = "api";
54
57
  why = "--all follows the cloud's pages";
55
58
  }
56
- else if (status.verdict === "fresh") {
57
- source = "replica";
58
- why = `cursor ${status.cursor}`;
59
- }
60
59
  else {
61
60
  source = "api";
62
- why = `replica ${status.verdict}`;
61
+ why = "cloud reads by default";
63
62
  }
64
63
  ctx.stderr(source === "replica" ? `source: replica (${why})` : `source: api (${why})`);
65
64
  let result;
66
65
  if (source === "replica") {
67
- if (status.verdict === "absent" || status.verdict === "not-configured") {
68
- throw new UsageError(`--source replica but the replica is ${status.verdict}`);
66
+ if (!status ||
67
+ status.verdict === "absent" ||
68
+ status.verdict === "not-configured") {
69
+ throw new UsageError(`--source replica but the replica is ${status?.verdict ?? "unavailable"}`);
69
70
  }
70
71
  result = await fromReplica(ctx, cfg, sub, rest, filters, deps);
71
72
  }
@@ -82,8 +83,15 @@ export async function cmdQuery(args, ctx, deps = {}) {
82
83
  async function fromReplica(ctx, cfg, sub, rest, f, deps) {
83
84
  const sdk = await loadSdk();
84
85
  const dbPath = replicaDbPath(cfg, ctx.home, ctx.env);
85
- const engine = await engineFor(sdk, dbPath, ctx, { ...deps.engineDeps, readonly: true });
86
- const replica = await sdk.CatalystReplica.openReadOnly({ dbPath, engine, log: () => { } });
86
+ const engine = await engineFor(sdk, dbPath, ctx, {
87
+ ...deps.engineDeps,
88
+ readonly: true,
89
+ });
90
+ const replica = await sdk.CatalystReplica.openReadOnly({
91
+ dbPath,
92
+ engine,
93
+ log: () => { },
94
+ });
87
95
  try {
88
96
  switch (sub) {
89
97
  case "issues": {
@@ -95,7 +103,9 @@ async function fromReplica(ctx, cfg, sub, rest, f, deps) {
95
103
  case "pulls":
96
104
  return replica.pulls({ limit: f.limit });
97
105
  case "projects":
98
- return applyFilters(replica.projects({ limit: Math.max(f.limit, 200) }), { limit: f.limit, state: f.state }).slice(0, f.limit);
106
+ return applyFilters(replica.projects({
107
+ limit: Math.max(f.limit, 200),
108
+ }), { limit: f.limit, state: f.state }).slice(0, f.limit);
99
109
  default:
100
110
  throw new UsageError(`${sub} is not readable from the replica`);
101
111
  }
@@ -106,14 +116,25 @@ async function fromReplica(ctx, cfg, sub, rest, f, deps) {
106
116
  }
107
117
  async function fromApi(ctx, cfg, sub, rest, f, args) {
108
118
  const api = apiClient(cfg, ctx);
109
- const common = { team: f.team, project: f.project, state: f.state, limit: f.limit };
119
+ // CTC-4556 — the mirror narrows `/issues` by `team_key` and `state_name`; `team` and a state name
120
+ // in `state` narrow nothing there.
121
+ const issueScope = {
122
+ team_key: f.team,
123
+ project: f.project,
124
+ state_name: f.state,
125
+ };
110
126
  switch (sub) {
111
127
  case "issues": {
112
128
  if (f.all) {
113
- const { rows } = await fetchAllPages(api, "/api/v1/issues", { team: f.team, project: f.project, state: f.state });
129
+ const { rows, scope } = await fetchAllPages(api, "/api/v1/issues", issueScope);
130
+ requireIssueScope(scope, f);
114
131
  return applyFilters(rows, f); // no .slice — --all means the whole scope
115
132
  }
116
- const page = await fetchPage(api, "/api/v1/issues", common);
133
+ const page = await fetchPage(api, "/api/v1/issues", {
134
+ ...issueScope,
135
+ limit: f.limit,
136
+ });
137
+ requireIssueScope(page.scope, f);
117
138
  const rows = applyFilters(page.rows, f).slice(0, f.limit);
118
139
  const notice = truncationNotice(page);
119
140
  if (notice)
@@ -131,7 +152,10 @@ async function fromApi(ctx, cfg, sub, rest, f, args) {
131
152
  const { rows } = await fetchAllPages(api, "/api/v1/pulls", { ticket });
132
153
  return rows;
133
154
  }
134
- const page = await fetchPage(api, "/api/v1/pulls", { limit: f.limit, ticket });
155
+ const page = await fetchPage(api, "/api/v1/pulls", {
156
+ limit: f.limit,
157
+ ticket,
158
+ });
135
159
  const rows = page.rows.slice(0, f.limit);
136
160
  const notice = truncationNotice(page);
137
161
  if (notice)
@@ -144,18 +168,24 @@ async function fromApi(ctx, cfg, sub, rest, f, args) {
144
168
  return res.status === 404 ? null : res.body;
145
169
  }
146
170
  case "projects": {
147
- const res = await api.getJson("/api/v1/projects", { query: { limit: f.limit } });
171
+ const res = await api.getJson("/api/v1/projects", {
172
+ query: { limit: f.limit },
173
+ });
148
174
  return rowsOf(res.body).slice(0, f.limit);
149
175
  }
150
176
  case "cycles": {
151
- const res = await api.getJson("/api/v1/cycles", { query: { team: f.team, limit: f.limit } });
177
+ const res = await api.getJson("/api/v1/cycles", {
178
+ query: { team: f.team, limit: f.limit },
179
+ });
152
180
  return rowsOf(res.body);
153
181
  }
154
182
  case "search": {
155
183
  const q = rest.join(" ").trim();
156
184
  if (!q)
157
185
  throw new UsageError("search needs terms: query search <terms>");
158
- const res = await api.getJson("/api/v1/search", { query: { q, limit: f.limit } });
186
+ const res = await api.getJson("/api/v1/search", {
187
+ query: { q, limit: f.limit },
188
+ });
159
189
  return searchRows(res.body);
160
190
  }
161
191
  case "changes": {
@@ -191,7 +221,11 @@ async function readChanges(api, since, limit) {
191
221
  });
192
222
  const head = res.headers.get(HEAD_SEQ_HEADER);
193
223
  if (res.status !== 409) {
194
- return { since: Number(resolved), head: head === null ? null : Number(head), changes: res.body };
224
+ return {
225
+ since: Number(resolved),
226
+ head: head === null ? null : Number(head),
227
+ changes: res.body,
228
+ };
195
229
  }
196
230
  const where = head === null ? "" : ` The feed's live cursor is ${head}.`;
197
231
  throw new CliError(`cursor ${resolved} is not usable: it is either past the feed's head or no longer in the change log, which keeps only recent changes.${where} Re-run with \`--since head\` to start from now, or with a cursor the feed still holds.`, "changefeed-resync", 1, 409);
@@ -199,7 +233,10 @@ async function readChanges(api, since, limit) {
199
233
  /** The live head, read off any `/changes` response's own header — a 409 carries it too, which is
200
234
  * what makes this work on a tenant whose log has rotated past every cursor the caller could guess. */
201
235
  async function headCursor(api) {
202
- const probe = await api.getNdjson("/api/v1/changes", { query: { since: "0", limit: 1 }, accept: [409] });
236
+ const probe = await api.getNdjson("/api/v1/changes", {
237
+ query: { since: "0", limit: 1 },
238
+ accept: [409],
239
+ });
203
240
  const raw = probe.headers.get(HEAD_SEQ_HEADER);
204
241
  const head = raw === null ? Number.NaN : Number(raw);
205
242
  if (!Number.isFinite(head)) {
@@ -213,13 +250,39 @@ function needArg(rest, usage) {
213
250
  throw new UsageError(`query ${usage}`);
214
251
  return v;
215
252
  }
253
+ /** CTC-4556 — refuse a page the cloud did not narrow by a filter the user asked for. Filtering it
254
+ * here would print the matches among the newest rows and call that the answer. */
255
+ function requireIssueScope(scope, f) {
256
+ const asked = [
257
+ ["--team", "team_key", f.team],
258
+ ["--state", "state_name", f.state],
259
+ ["--project", "project", f.project],
260
+ ];
261
+ for (const [flag, param, value] of asked) {
262
+ if (!value || scope?.includes(param))
263
+ continue;
264
+ throw new CliError(`the cloud did not apply ${flag} (${scope === null ? "it sent no x-mirror-scope header" : `it applied: ${scope.join(", ") || "nothing"}`}), so the page is not that scope; nothing printed. Read it with --source replica, or drop ${flag}`, "filter-not-applied", 1);
265
+ }
266
+ }
267
+ function trimmedFlag(args, name) {
268
+ const value = flagString(args, name)?.trim();
269
+ return value === "" ? undefined : value;
270
+ }
271
+ /** The mirror's comparison for team keys and state names: trimmed and lowercased, nothing more.
272
+ * "In Progress" and "In_Progress" stay different names. */
273
+ const sameName = (a, b) => String(a ?? "").trim().toLowerCase() === b.trim().toLowerCase();
216
274
  export function applyFilters(rows, f) {
217
275
  return rows.filter((r) => {
218
- if (f.team && !(String(r.identifier ?? "").toUpperCase().startsWith(`${f.team.toUpperCase()}-`) || r.team_id === f.team || r.team_key === f.team))
276
+ if (f.team &&
277
+ !(String(r.identifier ?? "")
278
+ .toUpperCase()
279
+ .startsWith(`${f.team.toUpperCase()}-`) ||
280
+ r.team_id === f.team ||
281
+ sameName(r.team_key, f.team)))
219
282
  return false;
220
283
  if (f.project && r.project_id !== f.project)
221
284
  return false;
222
- if (f.state && String(r.state ?? "").toLowerCase() !== f.state.toLowerCase())
285
+ if (f.state && !sameName(r.state, f.state))
223
286
  return false;
224
287
  return true;
225
288
  });
@@ -227,7 +290,12 @@ export function applyFilters(rows, f) {
227
290
  /** The hub answers search in four groups (`issues`, `pulls`, `projects`, `initiatives`, the read
228
291
  * model's SearchView). `rowsOf` returns only the first array it finds, so it dropped every match that
229
292
  * 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"]];
293
+ const SEARCH_GROUPS = [
294
+ ["issues", "issue"],
295
+ ["pulls", "pull"],
296
+ ["projects", "project"],
297
+ ["initiatives", "initiative"],
298
+ ];
231
299
  export function searchRows(body) {
232
300
  if (Array.isArray(body))
233
301
  return body;
@@ -235,7 +303,12 @@ export function searchRows(body) {
235
303
  return [];
236
304
  const b = body;
237
305
  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 })) : []);
306
+ return SEARCH_GROUPS.flatMap(([group, kind]) => Array.isArray(b[group])
307
+ ? b[group].map((row) => ({
308
+ kind,
309
+ ...row,
310
+ }))
311
+ : []);
239
312
  }
240
313
  return rowsOf(body);
241
314
  }