@dashai/cli 0.2.3 → 0.2.4

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.
package/dist/bin.js CHANGED
@@ -92,6 +92,94 @@ function isPlausibleConfig(v) {
92
92
  if (typeof u.email !== "string") return false;
93
93
  return true;
94
94
  }
95
+
96
+ // src/lib/api-client.ts
97
+ var ApiError = class extends Error {
98
+ status;
99
+ code;
100
+ context;
101
+ retriable;
102
+ constructor(status, envelope, fallbackMessage) {
103
+ const code2 = envelope?.code ?? codeFromStatus(status);
104
+ const message = envelope?.message ?? fallbackMessage;
105
+ super(message);
106
+ this.name = "ApiError";
107
+ this.status = status;
108
+ this.code = code2;
109
+ this.context = envelope?.context;
110
+ this.retriable = envelope?.retriable ?? status >= 500;
111
+ }
112
+ };
113
+ async function apiRequest(method, path, body, options = {}) {
114
+ const baseUrl = (options.baseUrl ?? resolveApiUrl()).replace(/\/+$/, "");
115
+ const url = `${baseUrl}${path.startsWith("/") ? path : `/${path}`}`;
116
+ const headers = {
117
+ Accept: "application/json",
118
+ ...options.headers ?? {}
119
+ };
120
+ if (body !== void 0) {
121
+ headers["Content-Type"] = "application/json";
122
+ }
123
+ const authMode = options.auth ?? "auto";
124
+ if (authMode === "auto") {
125
+ const saved = readSavedToken();
126
+ if (saved) headers["Authorization"] = `Bearer ${saved}`;
127
+ } else if (typeof authMode === "object" && authMode.token) {
128
+ headers["Authorization"] = `Bearer ${authMode.token}`;
129
+ }
130
+ let res;
131
+ try {
132
+ res = await fetch(url, {
133
+ method,
134
+ headers,
135
+ body: body === void 0 ? void 0 : JSON.stringify(body)
136
+ });
137
+ } catch (err) {
138
+ throw new ApiError(0, { code: "NETWORK_ERROR", message: err.message }, "Network error");
139
+ }
140
+ if (res.status === 204) {
141
+ return void 0;
142
+ }
143
+ const contentType = res.headers.get("content-type") ?? "";
144
+ let parsed = null;
145
+ if (contentType.includes("application/json")) {
146
+ try {
147
+ parsed = await res.json();
148
+ } catch {
149
+ parsed = null;
150
+ }
151
+ } else if (res.status >= 400) {
152
+ try {
153
+ parsed = { message: await res.text() };
154
+ } catch {
155
+ parsed = null;
156
+ }
157
+ }
158
+ if (!res.ok) {
159
+ throw new ApiError(res.status, asEnvelope(parsed), `HTTP ${res.status}`);
160
+ }
161
+ return parsed;
162
+ }
163
+ function asEnvelope(v) {
164
+ if (!v || typeof v !== "object") return null;
165
+ const env = v;
166
+ const out2 = {};
167
+ if (typeof env.code === "string") out2.code = env.code;
168
+ if (typeof env.message === "string") out2.message = env.message;
169
+ if (typeof env.retriable === "boolean") out2.retriable = env.retriable;
170
+ if (env.context && typeof env.context === "object") {
171
+ out2.context = env.context;
172
+ }
173
+ return out2;
174
+ }
175
+ function codeFromStatus(status) {
176
+ if (status === 401) return "UNAUTHENTICATED";
177
+ if (status === 403) return "FORBIDDEN";
178
+ if (status === 404) return "NOT_FOUND";
179
+ if (status === 0) return "NETWORK_ERROR";
180
+ if (status >= 500) return "INTERNAL_ERROR";
181
+ return "HTTP_ERROR";
182
+ }
95
183
  function success(msg) {
96
184
  process.stderr.write(`${pc.green("\u2713")} ${msg}
97
185
  `);
@@ -125,6 +213,7 @@ function dim(s) {
125
213
  }
126
214
 
127
215
  // src/commands/dev.ts
216
+ var DEFAULT_NEXT_PORT = 3e3;
128
217
  var PREFIX_SDK = `\x1B[36m[sdk]\x1B[0m `;
129
218
  var PREFIX_NEXT = `\x1B[35m[next]\x1B[0m `;
130
219
  async function devCommand(options) {
@@ -157,17 +246,60 @@ async function devCommand(options) {
157
246
  return 1;
158
247
  }
159
248
  const apiUrl = options.apiUrl ?? process.env.DASHWISE_API_URL ?? savedConfig?.apiUrl ?? "http://localhost:3000";
160
- const installationId = options.installationId ?? process.env.DASHWISE_INSTALLATION_ID ?? "local";
249
+ const port = options.port ?? DEFAULT_NEXT_PORT;
250
+ const explicitInstallationId = options.installationId ?? process.env.DASHWISE_INSTALLATION_ID;
251
+ const reuseDevSessionId = process.env.DASHWISE_DEV_SESSION_ID;
252
+ let installationIdEnv;
253
+ let devSessionIdEnv;
254
+ let revokeDevSession;
255
+ if (explicitInstallationId && explicitInstallationId !== "local") {
256
+ installationIdEnv = explicitInstallationId;
257
+ } else if (reuseDevSessionId) {
258
+ devSessionIdEnv = reuseDevSessionId;
259
+ } else {
260
+ let workspaceContext;
261
+ try {
262
+ workspaceContext = await resolveTargetWorkspace(apiUrl, apiToken, options.workspace);
263
+ } catch (err) {
264
+ fail(err.message);
265
+ return 1;
266
+ }
267
+ const callbackOrigins = computeCallbackOrigins(port, options.callbackOrigin ?? []);
268
+ try {
269
+ const session = await registerDevSession(apiUrl, apiToken, {
270
+ target_workspace_id: workspaceContext.id,
271
+ callback_origins: callbackOrigins
272
+ });
273
+ devSessionIdEnv = session.id;
274
+ revokeDevSession = () => revokeDevSessionBestEffort(apiUrl, apiToken, session.id);
275
+ info(` ${dim("dev session")} ${session.id}`);
276
+ info(` ${dim("workspace")} ${workspaceContext.name} (id=${workspaceContext.id}, slug=${workspaceContext.slug})`);
277
+ info(` ${dim("callbacks")} ${callbackOrigins.join(", ")}`);
278
+ info(` ${dim("expires")} ${session.expires_at}`);
279
+ } catch (err) {
280
+ if (err instanceof ApiError) {
281
+ fail(`Failed to register dev session: ${err.code} \u2014 ${err.message}`);
282
+ } else {
283
+ fail(`Failed to register dev session: ${err.message}`);
284
+ }
285
+ return 1;
286
+ }
287
+ }
161
288
  const childEnv = {
162
289
  ...process.env,
163
290
  DASHWISE_API_URL: apiUrl,
164
291
  DASHWISE_API_TOKEN: apiToken,
165
- DASHWISE_INSTALLATION_ID: installationId
292
+ DASHWISE_INSTALLATION_ID: installationIdEnv ?? "",
293
+ ...devSessionIdEnv !== void 0 ? { DASHWISE_DEV_SESSION_ID: devSessionIdEnv } : {}
166
294
  };
167
295
  info(`Dev loop starting \u2014 Ctrl-C to stop.`);
168
- info(` ${dim("api")} ${apiUrl}`);
169
- info(` ${dim("installation")} ${installationId}`);
170
- info(` ${dim("token")} ${maskedToken(apiToken)} ${dim(`(from ${apiToken === savedConfig?.token ? "~/.config/dashwise/auth.json" : "$DASHWISE_API_TOKEN"})`)}`);
296
+ info(` ${dim("api")} ${apiUrl}`);
297
+ if (installationIdEnv !== void 0) {
298
+ info(` ${dim("installation")} ${installationIdEnv} (prod-mode SSO)`);
299
+ } else {
300
+ info(` ${dim("mode")} dev (DASHWISE_DEV_SESSION_ID injected)`);
301
+ }
302
+ info(` ${dim("token")} ${maskedToken(apiToken)} ${dim(`(from ${apiToken === savedConfig?.token ? "~/.config/dashwise/auth.json" : "$DASHWISE_API_TOKEN"})`)}`);
171
303
  info("");
172
304
  const children = [];
173
305
  if (!options.nextOnly) {
@@ -175,7 +307,7 @@ async function devCommand(options) {
175
307
  children.push({ name: "sdk", child: sdkChild, prefix: PREFIX_SDK });
176
308
  }
177
309
  if (!options.sdkOnly) {
178
- const nextChild = spawnNextDev(projectRoot, childEnv, options.port);
310
+ const nextChild = spawnNextDev(projectRoot, childEnv, port);
179
311
  children.push({ name: "next", child: nextChild, prefix: PREFIX_NEXT });
180
312
  }
181
313
  for (const { child, prefix } of children) {
@@ -193,6 +325,10 @@ Received ${sig}, stopping children\u2026`);
193
325
  child.kill(sig);
194
326
  }
195
327
  }
328
+ if (revokeDevSession) {
329
+ revokeDevSession().catch(() => {
330
+ });
331
+ }
196
332
  };
197
333
  process.on("SIGINT", () => forwardSignal("SIGINT"));
198
334
  process.on("SIGTERM", () => forwardSignal("SIGTERM"));
@@ -286,96 +422,60 @@ function maskedToken(token) {
286
422
  const tail = token.slice(-4);
287
423
  return `${prefix}\u2026${tail}`;
288
424
  }
289
-
290
- // src/lib/api-client.ts
291
- var ApiError = class extends Error {
292
- status;
293
- code;
294
- context;
295
- retriable;
296
- constructor(status, envelope, fallbackMessage) {
297
- const code2 = envelope?.code ?? codeFromStatus(status);
298
- const message = envelope?.message ?? fallbackMessage;
299
- super(message);
300
- this.name = "ApiError";
301
- this.status = status;
302
- this.code = code2;
303
- this.context = envelope?.context;
304
- this.retriable = envelope?.retriable ?? status >= 500;
305
- }
306
- };
307
- async function apiRequest(method, path, body, options = {}) {
308
- const baseUrl = (options.baseUrl ?? resolveApiUrl()).replace(/\/+$/, "");
309
- const url = `${baseUrl}${path.startsWith("/") ? path : `/${path}`}`;
310
- const headers = {
311
- Accept: "application/json",
312
- ...options.headers ?? {}
313
- };
314
- if (body !== void 0) {
315
- headers["Content-Type"] = "application/json";
316
- }
317
- const authMode = options.auth ?? "auto";
318
- if (authMode === "auto") {
319
- const saved = readSavedToken();
320
- if (saved) headers["Authorization"] = `Bearer ${saved}`;
321
- } else if (typeof authMode === "object" && authMode.token) {
322
- headers["Authorization"] = `Bearer ${authMode.token}`;
323
- }
324
- let res;
325
- try {
326
- res = await fetch(url, {
327
- method,
328
- headers,
329
- body: body === void 0 ? void 0 : JSON.stringify(body)
330
- });
331
- } catch (err) {
332
- throw new ApiError(0, { code: "NETWORK_ERROR", message: err.message }, "Network error");
333
- }
334
- if (res.status === 204) {
335
- return void 0;
425
+ function computeCallbackOrigins(port, extra) {
426
+ const def = `http://localhost:${port}`;
427
+ return Array.from(/* @__PURE__ */ new Set([def, ...extra]));
428
+ }
429
+ async function resolveTargetWorkspace(apiUrl, apiToken, flag) {
430
+ const workspaces = await apiRequest(
431
+ "GET",
432
+ "/api/workspaces",
433
+ void 0,
434
+ { baseUrl: apiUrl, auth: { token: apiToken } }
435
+ );
436
+ if (workspaces.length === 0) {
437
+ throw new Error(
438
+ "No workspaces found for your account. Create a workspace at the DashWise dashboard first."
439
+ );
336
440
  }
337
- const contentType = res.headers.get("content-type") ?? "";
338
- let parsed = null;
339
- if (contentType.includes("application/json")) {
340
- try {
341
- parsed = await res.json();
342
- } catch {
343
- parsed = null;
344
- }
345
- } else if (res.status >= 400) {
346
- try {
347
- parsed = { message: await res.text() };
348
- } catch {
349
- parsed = null;
441
+ if (flag === void 0) {
442
+ if (workspaces.length === 1) {
443
+ return workspaces[0];
350
444
  }
445
+ const list = workspaces.map((w) => ` - ${w.slug} (id=${w.id}, name="${w.name}")`).join("\n");
446
+ throw new Error(
447
+ `You have ${workspaces.length} workspaces. Re-run with --workspace <slug-or-id> to pick one:
448
+ ${list}`
449
+ );
351
450
  }
352
- if (!res.ok) {
353
- throw new ApiError(res.status, asEnvelope(parsed), `HTTP ${res.status}`);
451
+ const asNumber = Number.parseInt(flag, 10);
452
+ const isNumeric = !Number.isNaN(asNumber) && String(asNumber) === flag;
453
+ const match = isNumeric ? workspaces.find((w) => w.id === asNumber) : workspaces.find((w) => w.slug === flag);
454
+ if (!match) {
455
+ const list = workspaces.map((w) => ` - ${w.slug} (id=${w.id})`).join("\n");
456
+ throw new Error(
457
+ `No workspace matching --workspace=${flag}. Available:
458
+ ${list}`
459
+ );
354
460
  }
355
- return parsed;
461
+ return match;
356
462
  }
357
- function asEnvelope(v) {
358
- if (!v || typeof v !== "object") return null;
359
- const env = v;
360
- const out2 = {};
361
- if (typeof env.code === "string") out2.code = env.code;
362
- if (typeof env.message === "string") out2.message = env.message;
363
- if (typeof env.retriable === "boolean") out2.retriable = env.retriable;
364
- if (env.context && typeof env.context === "object") {
365
- out2.context = env.context;
366
- }
367
- return out2;
463
+ async function registerDevSession(apiUrl, apiToken, body) {
464
+ return apiRequest(
465
+ "POST",
466
+ "/api/auth/dev/sessions",
467
+ body,
468
+ { baseUrl: apiUrl, auth: { token: apiToken } }
469
+ );
368
470
  }
369
- function codeFromStatus(status) {
370
- if (status === 401) return "UNAUTHENTICATED";
371
- if (status === 403) return "FORBIDDEN";
372
- if (status === 404) return "NOT_FOUND";
373
- if (status === 0) return "NETWORK_ERROR";
374
- if (status >= 500) return "INTERNAL_ERROR";
375
- return "HTTP_ERROR";
471
+ async function revokeDevSessionBestEffort(apiUrl, apiToken, id) {
472
+ await apiRequest(
473
+ "DELETE",
474
+ `/api/auth/dev/sessions/${encodeURIComponent(id)}`,
475
+ void 0,
476
+ { baseUrl: apiUrl, auth: { token: apiToken } }
477
+ );
376
478
  }
377
-
378
- // src/commands/login.ts
379
479
  var DEFAULT_TOKEN_NAME = "dashwise-cli";
380
480
  async function loginCommand(options) {
381
481
  const apiUrl = (options.apiUrl ?? resolveApiUrl()).replace(/\/+$/, "");
@@ -914,12 +1014,17 @@ function envLocal() {
914
1014
  return [
915
1015
  "# Local dev config \u2014 auto-generated by `dashwise module init`.",
916
1016
  "# Gitignored; safe to edit. `dashwise dev` (the unified loop) auto-",
917
- "# injects DASHWISE_API_TOKEN from ~/.config/dashwise/auth.json, so",
918
- "# leave it blank unless you want to override. Change DASHWISE_API_URL",
1017
+ "# injects DASHWISE_API_TOKEN from ~/.config/dashwise/auth.json AND",
1018
+ "# DASHWISE_DEV_SESSION_ID from the dev session it registers \u2014 so",
1019
+ "# you typically don't need to set either here. Change DASHWISE_API_URL",
919
1020
  "# to point at a non-default backend (e.g. https://api.dashwise.net).",
1021
+ "#",
1022
+ "# DASHWISE_INSTALLATION_ID is only needed AFTER you publish + install",
1023
+ "# the module in a workspace; until then dev mode (DASHWISE_DEV_SESSION_ID,",
1024
+ "# auto-set by `dashwise dev`) is the auth path.",
920
1025
  "DASHWISE_API_URL=http://localhost:3000",
921
1026
  "DASHWISE_API_TOKEN=",
922
- "DASHWISE_INSTALLATION_ID=local",
1027
+ "DASHWISE_INSTALLATION_ID=",
923
1028
  ""
924
1029
  ].join("\n");
925
1030
  }
@@ -1357,11 +1462,23 @@ function Row({ label, value }: { label: string; value: string }) {
1357
1462
  function appApiAuthSignInTs() {
1358
1463
  return `// /api/auth/sign-in \u2014 kick off the SSO flow.
1359
1464
  //
1360
- // Reads \`?return_to=\` from the caller (defaults to '/'), looks up
1361
- // the module's installation id from env, redirects the browser to
1362
- // the DashWise backend's \`/api/auth/sso/start\`. The backend mints
1363
- // an exchange code + 302s back to \`/api/auth/callback?code=...\`
1364
- // where we set the session cookie.
1465
+ // Reads \`?return_to=\` from the caller (defaults to '/'). Branches
1466
+ // on env:
1467
+ //
1468
+ // - DASHWISE_DEV_SESSION_ID set (set automatically by
1469
+ // \`dashwise dev\`): hits /api/auth/dev/sso/start with the dev
1470
+ // session id. No installation_id required \u2014 the module isn't
1471
+ // installed in a workspace yet.
1472
+ //
1473
+ // - DASHWISE_INSTALLATION_ID set (production / installed-module
1474
+ // path): hits /api/auth/sso/start with the installation id.
1475
+ //
1476
+ // In both cases the backend mints an exchange code + 302s back to
1477
+ // \`/api/auth/callback?code=...\` where we set the session cookie.
1478
+ //
1479
+ // Unauthenticated browser hits to either backend endpoint 302 to
1480
+ // the DashWise FE \`/login?return_to=...\` automatically \u2014 you don't
1481
+ // need to handle the "user not logged in" case here.
1365
1482
  import { NextResponse } from 'next/server';
1366
1483
 
1367
1484
  export function GET(request: Request) {
@@ -1369,19 +1486,43 @@ export function GET(request: Request) {
1369
1486
  const returnTo = url.searchParams.get('return_to') ?? '/';
1370
1487
  const apiBaseUrl = process.env.DASHWISE_API_URL;
1371
1488
  const installationId = process.env.DASHWISE_INSTALLATION_ID;
1372
- if (!apiBaseUrl || !installationId) {
1489
+ const devSessionId = process.env.DASHWISE_DEV_SESSION_ID;
1490
+
1491
+ if (!apiBaseUrl) {
1373
1492
  return new NextResponse(
1374
- 'DASHWISE_API_URL and DASHWISE_INSTALLATION_ID must be set in .env.local',
1493
+ 'DASHWISE_API_URL must be set in .env.local',
1375
1494
  { status: 500 },
1376
1495
  );
1377
1496
  }
1497
+ if (!installationId && !devSessionId) {
1498
+ return new NextResponse(
1499
+ 'Either DASHWISE_INSTALLATION_ID (installed module) or DASHWISE_DEV_SESSION_ID ' +
1500
+ '(local dev via \`dashwise dev\`) must be set. Run \`dashwise dev\` to register ' +
1501
+ 'a dev session, or set DASHWISE_INSTALLATION_ID after publishing your module.',
1502
+ { status: 500 },
1503
+ );
1504
+ }
1505
+
1378
1506
  // Absolute URL for return_to so the backend knows which host to
1379
1507
  // bounce back to (the callback path is fixed at /api/auth/callback
1380
1508
  // by SDK convention).
1381
1509
  const moduleOrigin = url.origin;
1382
- const ssoStart = new URL('/api/auth/sso/start', apiBaseUrl);
1383
- ssoStart.searchParams.set('return_to', \`\${moduleOrigin}\${returnTo}\`);
1384
- ssoStart.searchParams.set('client_id', installationId);
1510
+ const absoluteReturnTo = \`\${moduleOrigin}\${returnTo}\`;
1511
+
1512
+ let ssoStart;
1513
+ if (devSessionId) {
1514
+ // Dev mode wins when both are set \u2014 \`dashwise dev\` injects
1515
+ // DASHWISE_DEV_SESSION_ID into the child env, which overrides
1516
+ // whatever was in .env.local. The dev session id keys back to
1517
+ // the dev's workspace + callback origin allowlist.
1518
+ ssoStart = new URL('/api/auth/dev/sso/start', apiBaseUrl);
1519
+ ssoStart.searchParams.set('return_to', absoluteReturnTo);
1520
+ ssoStart.searchParams.set('dev_session_id', devSessionId);
1521
+ } else {
1522
+ ssoStart = new URL('/api/auth/sso/start', apiBaseUrl);
1523
+ ssoStart.searchParams.set('return_to', absoluteReturnTo);
1524
+ ssoStart.searchParams.set('client_id', installationId);
1525
+ }
1385
1526
  return NextResponse.redirect(ssoStart);
1386
1527
  }
1387
1528
  `;
@@ -4114,18 +4255,28 @@ workspace.command("use <slug>").description("Set the default workspace for subse
4114
4255
  process.exitCode = await workspaceUseCommand(slug);
4115
4256
  });
4116
4257
  program.command("dev").description(
4117
- "One-command local dev loop: runs the manifest watcher + Next.js dev server together, with `DASHWISE_API_URL`/`DASHWISE_API_TOKEN`/`DASHWISE_INSTALLATION_ID` auto-injected from your saved CLI config. Run `dashwise login` first."
4258
+ "One-command local dev loop: runs the manifest watcher + Next.js dev server together. Registers a dev-mode SSO session with the backend so workspace members can sign into your locally-running module via DashWise SSO (no installation_id required). Auth + dev-session env auto-injected into both child processes from your saved CLI config. Run `dashwise login` first."
4118
4259
  ).option("--dir <path>", "Project root containing `module.json` (default: current directory).").option(
4119
4260
  "--installation-id <id>",
4120
- 'Override the `DASHWISE_INSTALLATION_ID` env var passed to children (default: "local").'
4261
+ "Use prod-mode SSO with this installation id instead of registering a dev session. The installation must already exist in the target workspace."
4262
+ ).option(
4263
+ "--workspace <slug-or-id>",
4264
+ "Workspace to scope the dev session to (slug or numeric id). Required if your account belongs to more than one workspace; auto-picked if you have just one."
4265
+ ).option(
4266
+ "--callback-origin <url>",
4267
+ "Additional callback origin to allowlist on the dev session (repeatable). The dev server URL (http://localhost:<port>) is always allowlisted; pass this for custom-domain local dev (e.g. https://myapp.local).",
4268
+ collectRepeated,
4269
+ []
4121
4270
  ).option(
4122
4271
  "--api-url <url>",
4123
4272
  "Override the `DASHWISE_API_URL` env var passed to children (default: saved CLI config)."
4124
- ).option("--port <number>", "Port for `next dev` (default: Next.js picks 3000 / next free).", parseIntOption).option("--sdk-only", "Only run the manifest watcher; skip `next dev`.").option("--next-only", "Only run `next dev`; skip the manifest watcher.").action(
4273
+ ).option("--port <number>", "Port for `next dev` (default: 3000).", parseIntOption).option("--sdk-only", "Only run the manifest watcher; skip `next dev`.").option("--next-only", "Only run `next dev`; skip the manifest watcher.").action(
4125
4274
  async (cmdOpts) => {
4126
4275
  process.exitCode = await devCommand({
4127
4276
  ...cmdOpts.dir !== void 0 ? { dir: cmdOpts.dir } : {},
4128
4277
  ...cmdOpts.installationId !== void 0 ? { installationId: cmdOpts.installationId } : {},
4278
+ ...cmdOpts.workspace !== void 0 ? { workspace: cmdOpts.workspace } : {},
4279
+ ...cmdOpts.callbackOrigin !== void 0 && cmdOpts.callbackOrigin.length > 0 ? { callbackOrigin: cmdOpts.callbackOrigin } : {},
4129
4280
  ...cmdOpts.apiUrl !== void 0 ? { apiUrl: cmdOpts.apiUrl } : {},
4130
4281
  ...cmdOpts.port !== void 0 ? { port: cmdOpts.port } : {},
4131
4282
  ...cmdOpts.sdkOnly !== void 0 ? { sdkOnly: cmdOpts.sdkOnly } : {},
@@ -4344,7 +4495,7 @@ moduleCmd.command("action-log").description(
4344
4495
  }
4345
4496
  })();
4346
4497
  function getVersion() {
4347
- return "0.2.3";
4498
+ return "0.2.4";
4348
4499
  }
4349
4500
  function parseIntOption(value) {
4350
4501
  const n = Number.parseInt(value, 10);
@@ -4353,5 +4504,8 @@ function parseIntOption(value) {
4353
4504
  }
4354
4505
  return n;
4355
4506
  }
4507
+ function collectRepeated(value, previous) {
4508
+ return [...previous, value];
4509
+ }
4356
4510
  //# sourceMappingURL=bin.js.map
4357
4511
  //# sourceMappingURL=bin.js.map