@dashai/cli 0.28.1 → 0.30.1

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
@@ -4,7 +4,7 @@ import { existsSync, readFileSync, writeFileSync, rmSync, mkdirSync, watch, read
4
4
  import { hostname, tmpdir, platform, homedir } from 'os';
5
5
  import { resolve, join, relative, dirname, basename, win32, sep } from 'path';
6
6
  import { execFileSync, spawn, spawnSync } from 'child_process';
7
- import { intro, log, select, isCancel, cancel, spinner, outro, text, confirm as confirm$1, multiselect } from '@clack/prompts';
7
+ import { intro, log, select, isCancel, cancel, spinner, outro, confirm as confirm$1, password, text, multiselect } from '@clack/prompts';
8
8
  import { Command, Option } from 'commander';
9
9
  import open from 'open';
10
10
  import AdmZip from 'adm-zip';
@@ -364,6 +364,38 @@ async function setOrigins(installationId, allowedOrigins, options = {}) {
364
364
  options
365
365
  );
366
366
  }
367
+ async function getModuleBuildLog(moduleId, buildId, options = {}) {
368
+ return apiRequest(
369
+ "GET",
370
+ `/api/modules/${moduleId}/builds/${buildId}/log`,
371
+ void 0,
372
+ options
373
+ );
374
+ }
375
+ async function listInstallationSecrets(installationId, options = {}) {
376
+ return apiRequest(
377
+ "GET",
378
+ `/api/modules/installations/${installationId}/secrets`,
379
+ void 0,
380
+ options
381
+ );
382
+ }
383
+ async function setInstallationSecrets(installationId, secrets, options = {}) {
384
+ return apiRequest(
385
+ "PUT",
386
+ `/api/modules/installations/${installationId}/secrets`,
387
+ { secrets },
388
+ options
389
+ );
390
+ }
391
+ async function clearInstallationSecrets(installationId, options = {}) {
392
+ await apiRequest(
393
+ "DELETE",
394
+ `/api/modules/installations/${installationId}/secrets`,
395
+ void 0,
396
+ options
397
+ );
398
+ }
367
399
  async function createTable(body, options = {}) {
368
400
  return apiRequest(
369
401
  "POST",
@@ -14158,8 +14190,8 @@ function needsAdmin(what, module, ask) {
14158
14190
  const scope = module ? ` for ${module}` : "";
14159
14191
  return `${what}${scope} needs a Dashwise admin. Ask your Dashwise admin to ${ask}.`;
14160
14192
  }
14161
- function refusalHint(code2, context, subject = {}) {
14162
- const ctx = context ?? {};
14193
+ function refusalHint(code2, context2, subject = {}) {
14194
+ const ctx = context2 ?? {};
14163
14195
  const module = subject.module ?? str(ctx.module_slug);
14164
14196
  const installationId = subject.installationId ?? num(ctx.installation_id);
14165
14197
  const installLabel = installationId !== void 0 ? ` (install #${installationId})` : "";
@@ -15213,9 +15245,9 @@ function surfaceSchemaPushError(err, slug, json) {
15213
15245
  fail(`Schema push failed: ${err.message}`);
15214
15246
  return 1;
15215
15247
  }
15216
- function extractIssues2(context) {
15217
- if (!context) return [];
15218
- const raw = context.issues;
15248
+ function extractIssues2(context2) {
15249
+ if (!context2) return [];
15250
+ const raw = context2.issues;
15219
15251
  if (!Array.isArray(raw)) return [];
15220
15252
  return raw.map((i) => {
15221
15253
  if (typeof i === "string") return i;
@@ -19193,8 +19225,16 @@ var MARKERS = [
19193
19225
  ["CODEX_SANDBOX_NETWORK_DISABLED", (v) => v.length > 0],
19194
19226
  ["CURSOR_AGENT", (v) => v.length > 0],
19195
19227
  ["DW_AGENT", (v) => v.length > 0],
19196
- ["DASHWISE_AGENT_SESSION", (v) => v === "1" || v === "true"]
19228
+ ["DASHWISE_AGENT_SESSION", (v) => v === "1" || v === "true"],
19229
+ ["CLAUDE_CODE_ENTRYPOINT", (v) => v.length > 0],
19230
+ ["GEMINI_CLI", (v) => v === "1"],
19231
+ ["COPILOT_AGENT_SESSION_ID", (v) => v.length > 0],
19232
+ ["COPILOT_AGENT_JOB_ID", (v) => v.length > 0],
19233
+ ["AI_AGENT", (v) => v.length > 0]
19197
19234
  ];
19235
+ MARKERS.map(
19236
+ ([name]) => name
19237
+ );
19198
19238
  function agentSessionMarker(env = process.env) {
19199
19239
  for (const [name, test] of MARKERS) {
19200
19240
  const value = env[name];
@@ -20763,7 +20803,10 @@ async function moduleReleaseCommand(slugArg, options) {
20763
20803
  if (resuming && (options.commitSha !== void 0 || options.bump !== void 0 || options.noWait)) {
20764
20804
  return usage2("--build only reads an existing release; do not combine it with --commit-sha, --bump or --no-wait.");
20765
20805
  }
20766
- if (!resuming && (!options.commitSha || !FULL_SHA.test(options.commitSha))) {
20806
+ if (!resuming && options.commitSha !== void 0 && !FULL_SHA.test(options.commitSha)) {
20807
+ return usage2("--commit-sha requires the full 40-character SHA already pushed to the linked repository.");
20808
+ }
20809
+ if (!resuming && options.commitSha === void 0 && !options.defaultBranchHead) {
20767
20810
  return usage2("--commit-sha requires the full 40-character SHA already pushed to the linked repository.");
20768
20811
  }
20769
20812
  const bump = options.bump ?? "minor";
@@ -20786,7 +20829,7 @@ async function moduleReleaseCommand(slugArg, options) {
20786
20829
  return usage2("Release creates a version. Pass --yes to confirm in non-interactive or --json mode.");
20787
20830
  }
20788
20831
  const answer = await select({
20789
- message: `Release ${slug} at ${options.commitSha} with a ${bump} bump?`,
20832
+ message: `Release ${slug} at ${options.commitSha ?? "the head of its default branch"} with a ${bump} bump?`,
20790
20833
  options: [{ value: "cancel", label: "Cancel" }, { value: "release", label: "Release this revision" }],
20791
20834
  initialValue: "cancel"
20792
20835
  });
@@ -20801,7 +20844,7 @@ async function moduleReleaseCommand(slugArg, options) {
20801
20844
  if (!resuming) {
20802
20845
  try {
20803
20846
  initial = await apiRequest("POST", `/api/modules/${moduleId}/builds`, {
20804
- commit_sha: pinnedSha,
20847
+ ...pinnedSha !== void 0 ? { commit_sha: pinnedSha } : {},
20805
20848
  version_bump: bump
20806
20849
  }, auth);
20807
20850
  } catch (err) {
@@ -20815,12 +20858,13 @@ async function moduleReleaseCommand(slugArg, options) {
20815
20858
  if (!json && isTransientPollError(err)) info("Check the module build history in Dashwise before retrying; the release may already be queued.");
20816
20859
  return reportRefusal(err, { module: slug }, { json, action: "Release" });
20817
20860
  }
20818
- if (!isRepositoryBuild(initial, moduleId) || initial.commit_sha.toLowerCase() !== pinnedSha) {
20861
+ if (!isRepositoryBuild(initial, moduleId) || pinnedSha !== void 0 && initial.commit_sha.toLowerCase() !== pinnedSha) {
20819
20862
  return reportCliError("Release response did not confirm the requested repository revision. Check the module build history before retrying.", { json });
20820
20863
  }
20821
20864
  buildId = initial.id;
20822
20865
  }
20823
20866
  const id = buildId;
20867
+ options.onBuildId?.(id);
20824
20868
  const resume = `dashwise module release ${slug} --build ${id}`;
20825
20869
  if (!json) info(`Build #${id}. Resume checking: ${code(resume)}`);
20826
20870
  if (options.noWait && isRepositoryBuild(initial, moduleId) && (initial.status === "queued" || initial.status === "building")) {
@@ -20859,6 +20903,13 @@ async function moduleReleaseCommand(slugArg, options) {
20859
20903
  }
20860
20904
  if (json) out(JSON.stringify(value));
20861
20905
  else success(`Released ${slug} v${value.version} (build #${id}, revision ${pinnedSha}).`);
20906
+ options.onReleased?.({
20907
+ id,
20908
+ module_id: moduleId,
20909
+ commit_sha: pinnedSha,
20910
+ version: value.version,
20911
+ module_version_id: value.module_version_id
20912
+ });
20862
20913
  return 0;
20863
20914
  }
20864
20915
  if (value.status === "failed") {
@@ -20900,6 +20951,248 @@ async function moduleReleaseCommand(slugArg, options) {
20900
20951
  }
20901
20952
  }
20902
20953
 
20954
+ // src/commands/module/build-log.ts
20955
+ init_api_client();
20956
+ init_config();
20957
+ init_manifest_io();
20958
+ init_output();
20959
+ init_permission_hint();
20960
+ var RUNNING = /* @__PURE__ */ new Set(["queued", "building"]);
20961
+ var FOLLOW_BASE_MS = 5e3;
20962
+ var FOLLOW_MAX_MS = 1e4;
20963
+ var MIN_OVERLAP = 64;
20964
+ function newLogText(printed, next) {
20965
+ if (printed === "") return { text: next, moved: false };
20966
+ if (next.startsWith(printed)) return { text: next.slice(printed.length), moved: false };
20967
+ const min = Math.min(MIN_OVERLAP, printed.length);
20968
+ const head = next.slice(0, min);
20969
+ for (let j = printed.indexOf(head); j !== -1; j = printed.indexOf(head, j + 1)) {
20970
+ const tail = printed.slice(j);
20971
+ if (tail.length >= min && next.startsWith(tail)) {
20972
+ return { text: next.slice(tail.length), moved: false };
20973
+ }
20974
+ }
20975
+ return { text: next, moved: true };
20976
+ }
20977
+ function sanitizeForTerminal(text4, keepColour = false) {
20978
+ const ESCAPE = /\u001b(?:\[[0-?]*[ -/]*[@-~]|\][\s\S]*?(?:\u0007|\u001b\\|$)|[PX^_][\s\S]*?(?:\u001b\\|$)|[ -/]*[0-~]|$)/g;
20979
+ const SGR = /^\u001b\[[0-9;]*m$/;
20980
+ return text4.replace(/\r\n/g, "\n").replace(ESCAPE, (seq) => keepColour && SGR.test(seq) ? seq : "").replace(
20981
+ /[\u0000-\u0008\u000b-\u001f\u007f-\u009f]/g,
20982
+ (ch, at, all) => (
20983
+ // Keep the ESC that opens a kept SGR sequence; drop every other control.
20984
+ ch === "\x1B" && keepColour && SGR.test((/^\u001b\[[0-9;]*m/.exec(all.slice(at)) ?? [""])[0]) ? ch : ""
20985
+ )
20986
+ );
20987
+ }
20988
+ async function moduleBuildLogCommand(slugArg, buildIdArg, options) {
20989
+ const json = options.json;
20990
+ const usage2 = (message) => reportCliError(message, { json, code: "USAGE" });
20991
+ let slug = slugArg;
20992
+ let rawId = buildIdArg;
20993
+ if (rawId === void 0 && slug !== void 0 && /^\d+$/.test(slug)) {
20994
+ rawId = slug;
20995
+ slug = void 0;
20996
+ }
20997
+ const buildId = rawId === void 0 ? Number.NaN : Number(rawId);
20998
+ if (!Number.isSafeInteger(buildId) || buildId <= 0) {
20999
+ return usage2("Pass the build id: `dashwise module build-log [slug] <buildId>` (ids are in `dashwise module status`).");
21000
+ }
21001
+ if (json && options.follow) return usage2("--follow streams text; drop --json.");
21002
+ if (!slug) {
21003
+ try {
21004
+ const local = readManifest(options.dir)?.module?.slug;
21005
+ if (typeof local === "string" && local) slug = local;
21006
+ } catch {
21007
+ }
21008
+ }
21009
+ if (!slug) return usage2("Pass a module slug or run from a directory with a valid module.json.");
21010
+ const timeoutMs = (options.timeoutSec ?? 900) * 1e3;
21011
+ if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) return usage2("--timeout expects a positive number of seconds.");
21012
+ const cfg = readConfig();
21013
+ if (!isLoggedIn() || !cfg) return reportCliError("Not logged in. Run `dashwise login` first.", { json });
21014
+ const auth = { baseUrl: resolveApiUrl(), auth: { token: cfg.token } };
21015
+ let moduleId;
21016
+ try {
21017
+ const mod = await getModuleBySlug(slug, auth);
21018
+ moduleId = mod.id;
21019
+ slug = mod.slug;
21020
+ } catch (err) {
21021
+ return reportRefusal(err, { module: slug }, { json, action: "Resolving the module" });
21022
+ }
21023
+ const read = async () => {
21024
+ try {
21025
+ return await getModuleBuildLog(moduleId, buildId, auth);
21026
+ } catch (err) {
21027
+ if (err instanceof ApiError && err.code === "FEATURE_DISABLED") {
21028
+ return reportCliError(
21029
+ "Build logs are not available on this Dashwise yet (the backend serves them once its build-log scrubber is switched on).",
21030
+ { json }
21031
+ );
21032
+ }
21033
+ return reportRefusal(err, { module: slug }, { json, action: `Reading the log of build #${buildId}` });
21034
+ }
21035
+ };
21036
+ const first = await read();
21037
+ if (typeof first === "number") return first;
21038
+ if (json) {
21039
+ out(JSON.stringify(first));
21040
+ return 0;
21041
+ }
21042
+ const printLog = (view, printed2) => {
21043
+ if (!view.available || view.text === null) return printed2;
21044
+ const delta = newLogText(printed2, view.text);
21045
+ if (delta.moved) info(dim("\u2026 (earlier lines scrolled out of the window the server keeps)"));
21046
+ if (delta.text) {
21047
+ const safe = sanitizeForTerminal(delta.text, Boolean(process.stdout.isTTY));
21048
+ process.stdout.write(safe.endsWith("\n") ? safe : `${safe}
21049
+ `);
21050
+ }
21051
+ return view.text;
21052
+ };
21053
+ if (!first.available && !(options.follow && RUNNING.has(first.status))) {
21054
+ info(`Build #${buildId} (${first.status}) has no log: it did not run on the build service, or the log expired.`);
21055
+ return 0;
21056
+ }
21057
+ if (first.truncated) info(dim("(showing the newest part of the log)"));
21058
+ let printed = printLog(first, "");
21059
+ const footer = (view) => {
21060
+ if (view.redactions > 0) info(dim(`${view.redactions} value(s) were hidden by the server's scrubber.`));
21061
+ };
21062
+ if (!options.follow || !RUNNING.has(first.status)) {
21063
+ footer(first);
21064
+ return 0;
21065
+ }
21066
+ const deadline = Date.now() + timeoutMs;
21067
+ let last = first;
21068
+ const base = options.pollIntervalMs ?? FOLLOW_BASE_MS;
21069
+ const cap = options.pollIntervalMs !== void 0 ? options.pollIntervalMs * 2 : FOLLOW_MAX_MS;
21070
+ let interval = base;
21071
+ while (Date.now() < deadline) {
21072
+ await new Promise(
21073
+ (resolve16) => setTimeout(resolve16, Math.min(interval, Math.max(0, deadline - Date.now())))
21074
+ );
21075
+ let view;
21076
+ try {
21077
+ view = await getModuleBuildLog(moduleId, buildId, auth);
21078
+ } catch (err) {
21079
+ if (isTransientPollError(err)) continue;
21080
+ return reportRefusal(err, { module: slug }, { json, action: `Reading the log of build #${buildId}` });
21081
+ }
21082
+ const grew = view.text !== null && view.text !== last.text;
21083
+ interval = grew ? base : Math.min(cap, Math.round(interval * 1.5));
21084
+ printed = printLog(view, printed);
21085
+ last = view;
21086
+ if (!RUNNING.has(view.status)) {
21087
+ footer(view);
21088
+ if (view.status === "failed") {
21089
+ fail(`Build #${buildId} failed.`);
21090
+ return 1;
21091
+ }
21092
+ info(`Build #${buildId}: ${view.status}.`);
21093
+ return 0;
21094
+ }
21095
+ }
21096
+ warn(`Stopped following build #${buildId} (still ${last.status}). Resume: ${code(`dashwise module build-log ${slug} ${buildId} --follow`)}`);
21097
+ return EXIT_TIMED_OUT;
21098
+ }
21099
+
21100
+ // src/commands/module/ship.ts
21101
+ init_api_client();
21102
+ init_config();
21103
+ init_manifest_io();
21104
+ init_output();
21105
+ init_permission_hint();
21106
+ async function moduleShipCommand(slugArg, options) {
21107
+ const marker = agentSessionMarker();
21108
+ if (marker !== null) {
21109
+ fail(
21110
+ `\`dashwise module ship\` takes a module live, which a person does (detected an AI agent session: ${marker}). An agent may release (${code("dashwise module release --commit-sha <sha> --yes")}); a person then installs it.`
21111
+ );
21112
+ return 2;
21113
+ }
21114
+ if (options.build !== void 0 && options.commitSha !== void 0) {
21115
+ fail("--build resumes an existing release; do not combine it with --commit-sha.");
21116
+ return 1;
21117
+ }
21118
+ const commitSha = options.build !== void 0 ? void 0 : options.commitSha ?? await defaultBranchHead(slugArg, options.dir);
21119
+ if (options.commitSha !== void 0) {
21120
+ info(`Releasing ${options.commitSha.slice(0, 12)} (--commit-sha).`);
21121
+ }
21122
+ let released = null;
21123
+ let buildId = options.build;
21124
+ const releaseExit = await moduleReleaseCommand(slugArg, {
21125
+ ...options.dir !== void 0 ? { dir: options.dir } : {},
21126
+ ...commitSha !== void 0 ? { commitSha } : {},
21127
+ ...options.build !== void 0 ? { build: options.build } : {},
21128
+ ...options.bump !== void 0 && options.build === void 0 ? { bump: options.bump } : {},
21129
+ ...options.timeoutSec !== void 0 ? { timeoutSec: options.timeoutSec } : {},
21130
+ ...options.pollIntervalMs !== void 0 ? { pollIntervalMs: options.pollIntervalMs } : {},
21131
+ // `ship` is the person's go for the release; the install below asks.
21132
+ yes: true,
21133
+ defaultBranchHead: commitSha === void 0 && options.build === void 0,
21134
+ onBuildId: (id) => {
21135
+ buildId = id;
21136
+ },
21137
+ onReleased: (build2) => {
21138
+ released = build2;
21139
+ }
21140
+ });
21141
+ if (releaseExit === EXIT_TIMED_OUT && buildId !== void 0) {
21142
+ info(
21143
+ `Resume the ship (wait for build #${buildId}, then install it): ${code(["dashwise module ship", slugArg, `--build ${buildId}`].filter(Boolean).join(" "))}`
21144
+ );
21145
+ }
21146
+ if (releaseExit !== 0) return releaseExit;
21147
+ const build = released;
21148
+ if (!build) {
21149
+ fail("The release finished without naming its version; nothing was installed.");
21150
+ return 1;
21151
+ }
21152
+ info(`Installing v${build.version} (build #${build.id}, revision ${build.commit_sha.slice(0, 12)}).`);
21153
+ return moduleInstallCommand(slugArg, {
21154
+ ...options.dir !== void 0 ? { dir: options.dir } : {},
21155
+ ...options.workspace !== void 0 ? { workspace: options.workspace } : {},
21156
+ ...options.installation !== void 0 ? { installation: options.installation } : {},
21157
+ ...options.keepDevInstall !== void 0 ? { keepDevInstall: options.keepDevInstall } : {},
21158
+ ...options.replaceDevInstall !== void 0 ? { replaceDevInstall: options.replaceDevInstall } : {},
21159
+ ...options.yes !== void 0 ? { yes: options.yes } : {},
21160
+ version: build.version
21161
+ });
21162
+ }
21163
+ async function defaultBranchHead(slugArg, dir) {
21164
+ const cfg = readConfig();
21165
+ let slug = slugArg;
21166
+ if (!slug) {
21167
+ try {
21168
+ slug = readManifest(dir)?.module?.slug;
21169
+ } catch {
21170
+ slug = void 0;
21171
+ }
21172
+ }
21173
+ if (!slug || !cfg || !isLoggedIn()) return void 0;
21174
+ const auth = { baseUrl: resolveApiUrl(), auth: { token: cfg.token } };
21175
+ try {
21176
+ const mod = await getModuleBySlug(slug, auth);
21177
+ const plan = await apiRequest("GET", `/api/modules/${mod.id}/builds/release-plan`, void 0, auth);
21178
+ const sha = plan.default_branch_sha ?? void 0;
21179
+ if (!sha || !/^[0-9a-f]{40}$/i.test(sha)) {
21180
+ info("Releasing the head of the default branch.");
21181
+ return void 0;
21182
+ }
21183
+ info(`Releasing ${sha.slice(0, 12)} from ${plan.default_branch ?? "the default branch"}.`);
21184
+ if (plan.pending_branch) {
21185
+ warn(
21186
+ `Newer work on ${plan.pending_branch.name}${plan.pending_branch.commits_ahead ? ` (${plan.pending_branch.commits_ahead} commit(s) ahead)` : ""} is not in this release: merge it to ${plan.default_branch ?? "the default branch"} first to ship it.`
21187
+ );
21188
+ }
21189
+ return sha.toLowerCase();
21190
+ } catch {
21191
+ info("Releasing the head of the default branch.");
21192
+ return void 0;
21193
+ }
21194
+ }
21195
+
20903
21196
  // src/commands/module/status.ts
20904
21197
  init_api_client();
20905
21198
  init_config();
@@ -21760,9 +22053,9 @@ async function moduleValidateCommand(options) {
21760
22053
  return 1;
21761
22054
  }
21762
22055
  }
21763
- function readIssues(context) {
21764
- if (!context) return [];
21765
- const v = context["issues"];
22056
+ function readIssues(context2) {
22057
+ if (!context2) return [];
22058
+ const v = context2["issues"];
21766
22059
  if (!Array.isArray(v)) return [];
21767
22060
  return v.map((entry) => {
21768
22061
  if (!entry || typeof entry !== "object") return null;
@@ -23344,9 +23637,9 @@ async function pushManifest(ctx) {
23344
23637
  return { ok: false, rejected: false };
23345
23638
  }
23346
23639
  }
23347
- function extractIssues(context) {
23348
- if (!context) return [];
23349
- const raw = context.issues;
23640
+ function extractIssues(context2) {
23641
+ if (!context2) return [];
23642
+ const raw = context2.issues;
23350
23643
  if (!Array.isArray(raw)) return [];
23351
23644
  return raw.map((i) => {
23352
23645
  if (typeof i === "string") return i;
@@ -24130,6 +24423,356 @@ function surfaceOriginsError(err) {
24130
24423
  return 1;
24131
24424
  }
24132
24425
 
24426
+ // src/commands/secrets.ts
24427
+ init_api_client();
24428
+ init_output();
24429
+ var KEY_SHAPE = /^[A-Z][A-Z0-9_]{0,63}$/;
24430
+ function context(opts) {
24431
+ try {
24432
+ return loadContext5(opts);
24433
+ } catch (err) {
24434
+ fail(err.message);
24435
+ return null;
24436
+ }
24437
+ }
24438
+ function interactive() {
24439
+ return Boolean(process.stdin.isTTY && process.stdout.isTTY);
24440
+ }
24441
+ async function secretsListCommand(options) {
24442
+ const ctx = context(options);
24443
+ if (!ctx) return 1;
24444
+ let resp;
24445
+ try {
24446
+ resp = await listInstallationSecrets(ctx.installationId, ctx.requestOptions);
24447
+ } catch (err) {
24448
+ return surfaceSecretsError(err);
24449
+ }
24450
+ const set = new Set(resp.set);
24451
+ const declared = new Set(resp.declared.map((d) => d.key));
24452
+ if (options.json) {
24453
+ process.stdout.write(
24454
+ `${JSON.stringify(
24455
+ {
24456
+ installation_id: ctx.installationId,
24457
+ declared: resp.declared.map((d) => ({
24458
+ key: d.key,
24459
+ description: d.description,
24460
+ set: set.has(d.key)
24461
+ })),
24462
+ // Stored but no longer declared by the installed version: not
24463
+ // delivered to the app; `set` removes them.
24464
+ undeclared_set: resp.set.filter((k) => !declared.has(k))
24465
+ },
24466
+ null,
24467
+ 2
24468
+ )}
24469
+ `
24470
+ );
24471
+ return 0;
24472
+ }
24473
+ info(`Secrets of ${code(ctx.slug)} ${dim("(names only; values are never shown)")}:`);
24474
+ if (resp.declared.length === 0) {
24475
+ info(` ${dim("(the module declares none in module.json#runtime.secrets)")}`);
24476
+ }
24477
+ for (const d of resp.declared) {
24478
+ const state = set.has(d.key) ? "set" : dim("not set");
24479
+ process.stdout.write(
24480
+ ` ${d.key} ${state}${d.description ? ` ${dim(d.description)}` : ""}
24481
+ `
24482
+ );
24483
+ }
24484
+ const stray = resp.set.filter((k) => !declared.has(k));
24485
+ if (stray.length > 0) {
24486
+ warn(
24487
+ `Stored but no longer declared (not delivered): ${stray.join(", ")}. The next ${code("dashwise secrets set")} removes them.`
24488
+ );
24489
+ }
24490
+ return 0;
24491
+ }
24492
+ function parseEnvFile(text4) {
24493
+ const out2 = {};
24494
+ const lines = text4.split(/\r?\n/);
24495
+ for (let i = 0; i < lines.length; i++) {
24496
+ const n = i + 1;
24497
+ const line = lines[i].replace(/^\s+/, "");
24498
+ if (line === "" || line.startsWith("#")) continue;
24499
+ const m = /^(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)=(.*)$/.exec(line);
24500
+ if (!m) {
24501
+ throw new Error(`line ${n} is not KEY=value (no spaces around "=")`);
24502
+ }
24503
+ const [, key, raw] = m;
24504
+ out2[key] = parseValue(raw, n);
24505
+ }
24506
+ return out2;
24507
+ }
24508
+ function parseValue(raw, n) {
24509
+ const quote2 = raw[0];
24510
+ if (quote2 === '"' || quote2 === "'") {
24511
+ let value = "";
24512
+ let j = 1;
24513
+ for (; j < raw.length; j++) {
24514
+ const ch = raw[j];
24515
+ if (ch === quote2) break;
24516
+ if (quote2 === '"' && ch === "\\" && j + 1 < raw.length) {
24517
+ const next = raw[++j];
24518
+ value += next === "n" ? "\n" : next === '"' || next === "\\" ? next : `\\${next}`;
24519
+ continue;
24520
+ }
24521
+ value += ch;
24522
+ }
24523
+ if (j >= raw.length) throw new Error(`line ${n}: the quoted value is not closed`);
24524
+ const rest = raw.slice(j + 1);
24525
+ if (rest.trim() !== "" && !/^\s+#/.test(rest)) {
24526
+ throw new Error(`line ${n}: only a " # comment" may follow the closing quote`);
24527
+ }
24528
+ return value;
24529
+ }
24530
+ if (/^\s|\s$/.test(raw)) {
24531
+ throw new Error(
24532
+ `line ${n}: an unquoted value may not start or end with a space; quote it`
24533
+ );
24534
+ }
24535
+ if (/\s#/.test(raw)) {
24536
+ throw new Error(
24537
+ `line ${n}: an unquoted value may not contain " #" (comment or value?); quote it`
24538
+ );
24539
+ }
24540
+ return raw;
24541
+ }
24542
+ async function readStdin() {
24543
+ const chunks = [];
24544
+ for await (const chunk of process.stdin) chunks.push(chunk);
24545
+ return Buffer.concat(chunks).toString("utf8").replace(/\r?\n$/, "");
24546
+ }
24547
+ async function secretsSetCommand(keyArgs, options) {
24548
+ const inline = keyArgs.findIndex((k) => k.includes("=") || !KEY_SHAPE.test(k));
24549
+ if (inline !== -1) {
24550
+ fail(
24551
+ `Argument ${inline + 1} is not a secret key name; values must not be passed as arguments (shell history). Enter values at the prompt, or use ${code("--from-stdin")} or ${code("--from-env-file <path>")}.`
24552
+ );
24553
+ return 1;
24554
+ }
24555
+ if (options.fromStdin && options.fromEnvFile) {
24556
+ fail(`Use one of ${code("--from-stdin")} or ${code("--from-env-file")}, not both.`);
24557
+ return 1;
24558
+ }
24559
+ if (options.keys && !options.fromEnvFile) {
24560
+ fail(`${code("--keys")} goes with ${code("--from-env-file")}.`);
24561
+ return 1;
24562
+ }
24563
+ const ctx = context(options);
24564
+ if (!ctx) return 1;
24565
+ let current;
24566
+ try {
24567
+ current = await listInstallationSecrets(ctx.installationId, ctx.requestOptions);
24568
+ } catch (err) {
24569
+ return surfaceSecretsError(err);
24570
+ }
24571
+ const declared = current.declared.map((d) => d.key);
24572
+ let keys = [...new Set(keyArgs)];
24573
+ const values = {};
24574
+ if (options.fromEnvFile) {
24575
+ let fileValues;
24576
+ try {
24577
+ fileValues = parseEnvFile(readFileSync(options.fromEnvFile, "utf8"));
24578
+ } catch (err) {
24579
+ fail(`Could not read ${code(options.fromEnvFile)}: ${err.message}`);
24580
+ return 1;
24581
+ }
24582
+ const wanted = options.keys ? options.keys.split(",").map((k) => k.trim()).filter(Boolean) : keys.length > 0 ? keys : declared.filter((k) => k in fileValues);
24583
+ const undeclaredAt2 = wanted.findIndex(
24584
+ (k) => !KEY_SHAPE.test(k) || !declared.includes(k)
24585
+ );
24586
+ if (undeclaredAt2 !== -1) {
24587
+ fail(
24588
+ `${options.keys ? `Item ${undeclaredAt2 + 1} of --keys` : `Argument ${undeclaredAt2 + 1}`} is not a declared key; values must not be passed as arguments. Declared: ${declared.length > 0 ? declared.join(", ") : "none"}.`
24589
+ );
24590
+ return 1;
24591
+ }
24592
+ const missing = wanted.filter((k) => !(k in fileValues));
24593
+ if (missing.length > 0) {
24594
+ fail(`Not in ${code(options.fromEnvFile)}: ${missing.join(", ")}.`);
24595
+ return 1;
24596
+ }
24597
+ for (const k of wanted) values[k] = fileValues[k];
24598
+ keys = wanted;
24599
+ } else if (options.fromStdin) {
24600
+ if (keys.length !== 1) {
24601
+ fail(`${code("--from-stdin")} reads the value of exactly one key: ${code("dashwise secrets set KEY --from-stdin")}.`);
24602
+ return 1;
24603
+ }
24604
+ if (process.stdin.isTTY) {
24605
+ info(`Nothing is piped into stdin; asking at a hidden prompt instead.`);
24606
+ } else {
24607
+ values[keys[0]] = await readStdin();
24608
+ }
24609
+ } else {
24610
+ if (keys.length === 0) {
24611
+ fail(
24612
+ `Name the keys to set (${declared.length > 0 ? declared.join(", ") : "none declared"}), or use ${code("--from-env-file <path>")}.`
24613
+ );
24614
+ return 1;
24615
+ }
24616
+ if (!interactive()) {
24617
+ fail(`No terminal to prompt in. Use ${code("--from-stdin")} or ${code("--from-env-file <path>")}.`);
24618
+ return 1;
24619
+ }
24620
+ }
24621
+ if (keys.length === 0) {
24622
+ fail(`Nothing to set: no declared key was found.`);
24623
+ return 1;
24624
+ }
24625
+ const undeclaredAt = keys.findIndex(
24626
+ (k) => !KEY_SHAPE.test(k) || !declared.includes(k)
24627
+ );
24628
+ if (undeclaredAt !== -1) {
24629
+ const where = options.keys ? `Item ${undeclaredAt + 1} of --keys` : keyArgs.length > 0 ? `Argument ${keyArgs.indexOf(keys[undeclaredAt]) + 1}` : `Key ${undeclaredAt + 1}`;
24630
+ fail(
24631
+ `${where} is not a declared key; values must not be passed as arguments. Declared: ${declared.length > 0 ? declared.join(", ") : "none"}.`
24632
+ );
24633
+ return 1;
24634
+ }
24635
+ const removed = current.set.filter((k) => !keys.includes(k));
24636
+ if (removed.length > 0 && !options.yes) {
24637
+ if (!interactive()) {
24638
+ fail(
24639
+ `This replaces the whole map and would remove ${removed.join(", ")}. Include them, or pass ${code("--yes")} to remove them.`
24640
+ );
24641
+ return 1;
24642
+ }
24643
+ const ok = await confirm$1({
24644
+ message: `This replaces the whole map: ${removed.join(", ")} will be removed. Continue?`,
24645
+ initialValue: false
24646
+ });
24647
+ if (isCancel(ok) || ok !== true) {
24648
+ info("Nothing changed.");
24649
+ return 1;
24650
+ }
24651
+ }
24652
+ for (const k of keys) {
24653
+ if (k in values) continue;
24654
+ const entered = await password({ message: `Value for ${k}` });
24655
+ if (isCancel(entered)) {
24656
+ info("Nothing changed.");
24657
+ return 1;
24658
+ }
24659
+ values[k] = entered;
24660
+ }
24661
+ const empty = keys.filter((k) => values[k] === "");
24662
+ if (empty.length > 0) {
24663
+ fail(`Empty value for ${empty.join(", ")}; nothing changed. (To remove every value: ${code("dashwise secrets clear")}.)`);
24664
+ return 1;
24665
+ }
24666
+ try {
24667
+ const now = await listInstallationSecrets(ctx.installationId, ctx.requestOptions);
24668
+ const before = [...current.set].sort().join(",");
24669
+ if ([...now.set].sort().join(",") !== before) {
24670
+ fail(
24671
+ `The set keys changed while you were entering values (now: ${now.set.length > 0 ? [...now.set].sort().join(", ") : "none"}). Nothing changed; run ${code("dashwise secrets set")} again.`
24672
+ );
24673
+ return 1;
24674
+ }
24675
+ } catch (err) {
24676
+ return surfaceSecretsError(err);
24677
+ }
24678
+ let resp;
24679
+ try {
24680
+ resp = await setInstallationSecrets(ctx.installationId, values, ctx.requestOptions);
24681
+ } catch (err) {
24682
+ return surfaceSecretsError(err);
24683
+ }
24684
+ if (options.json) {
24685
+ process.stdout.write(
24686
+ `${JSON.stringify({ installation_id: ctx.installationId, keys_set: resp.keys_set, removed, runtime: resp.runtime ?? null }, null, 2)}
24687
+ `
24688
+ );
24689
+ return 0;
24690
+ }
24691
+ success(`Saved ${resp.keys_set.join(", ")} for ${code(ctx.slug)}.`);
24692
+ if (removed.length > 0) info(` ${dim(`Removed: ${removed.join(", ")}.`)}`);
24693
+ info(` ${dim(runtimeLine(resp.runtime))}`);
24694
+ return 0;
24695
+ }
24696
+ function runtimeLine(runtime) {
24697
+ const status = runtime?.status;
24698
+ switch (status) {
24699
+ case "queued":
24700
+ return "The running app picks the new values up on its next request (about a minute).";
24701
+ case "deferred":
24702
+ return "Another change of this app is in progress; the values are applied right after it.";
24703
+ case "no_runtime":
24704
+ return "Nothing is deployed yet; the next deploy carries the values.";
24705
+ case "frozen":
24706
+ return "This installation is held; the values apply once it is released.";
24707
+ case void 0:
24708
+ return "The values reach the hosted app at its next deploy (or are read at runtime, where the backend delivers them).";
24709
+ default:
24710
+ return `Runtime: ${status}.`;
24711
+ }
24712
+ }
24713
+ async function secretsClearCommand(options) {
24714
+ const ctx = context(options);
24715
+ if (!ctx) return 1;
24716
+ if (!options.yes) {
24717
+ if (!interactive()) {
24718
+ fail(`This removes every value. Pass ${code("--yes")} to confirm.`);
24719
+ return 1;
24720
+ }
24721
+ const ok = await confirm$1({
24722
+ message: `Remove every secret value of ${ctx.slug}?`,
24723
+ initialValue: false
24724
+ });
24725
+ if (isCancel(ok) || ok !== true) {
24726
+ info("Nothing changed.");
24727
+ return 1;
24728
+ }
24729
+ }
24730
+ try {
24731
+ await clearInstallationSecrets(ctx.installationId, ctx.requestOptions);
24732
+ } catch (err) {
24733
+ return surfaceSecretsError(err);
24734
+ }
24735
+ success(`Removed every secret value of ${code(ctx.slug)}.`);
24736
+ return 0;
24737
+ }
24738
+ function surfaceSecretsError(err) {
24739
+ if (err instanceof ApiError) {
24740
+ const ctx = err.context ?? {};
24741
+ switch (err.code) {
24742
+ case "UNAUTHENTICATED":
24743
+ fail(`Auth rejected by backend. Your CLI token may be stale \u2014 run ${code("dashwise login")} then retry.`);
24744
+ return 1;
24745
+ case "SECRETS_ENV_TOO_LARGE": {
24746
+ const largest = Array.isArray(ctx.largest_keys) ? ctx.largest_keys.map(String).join(", ") : "";
24747
+ fail(
24748
+ `These values take ${String(ctx.bytes ?? "?")} bytes; an installation may hold at most ${String(ctx.budget ?? 3072)}. Shorten or remove a value${largest ? ` (largest: ${largest})` : ""}. Nothing changed.`
24749
+ );
24750
+ return 1;
24751
+ }
24752
+ case "UNKNOWN_SECRET_KEY":
24753
+ fail(`The backend does not know these keys: ${Array.isArray(ctx.unknown) ? ctx.unknown.join(", ") : err.message}.`);
24754
+ return 1;
24755
+ case "SECRETS_NOT_CONFIGURED":
24756
+ fail("Secrets are not configured on this backend (its master key is unset).");
24757
+ return 1;
24758
+ case "FORBIDDEN":
24759
+ case "PERMISSION_DENIED":
24760
+ case "NOT_WORKSPACE_ADMIN":
24761
+ fail("Not authorized. Setting secrets needs a company admin or an installation manager.");
24762
+ return 1;
24763
+ case "NOT_FOUND":
24764
+ case "INSTALLATION_NOT_FOUND":
24765
+ fail(`Installation not found (or not visible to you). Check the id with ${code("dashwise module status <slug>")}.`);
24766
+ return 1;
24767
+ default:
24768
+ fail(`Backend rejected: ${err.code} ${err.status} \u2014 ${err.message}`);
24769
+ return 1;
24770
+ }
24771
+ }
24772
+ fail(`Secrets operation failed: ${err.message}`);
24773
+ return 1;
24774
+ }
24775
+
24133
24776
  // src/commands/env-export.ts
24134
24777
  init_api_client();
24135
24778
  init_config();
@@ -26418,6 +27061,55 @@ originsCmd.command("remove <url>").description("Remove an SSO callback origin fr
26418
27061
  ...cmdOpts.install !== void 0 ? { install: cmdOpts.install } : {}
26419
27062
  });
26420
27063
  });
27064
+ var secretsCmd = program.command("secrets").description(
27065
+ "Manage the current module install's runtime secrets (write-only: names are shown, values never are)."
27066
+ );
27067
+ secretsCmd.command("list").description("Show the declared secret keys and which have a value (names only).").option("--dir <path>", "Project root (default: current directory).").option(
27068
+ "--install <id>",
27069
+ "Target a specific installation id instead of the cached dev install (production installs included). Find ids via `dashwise module status <slug>`.",
27070
+ parseIntOption
27071
+ ).option("--json", "Emit JSON instead of the human-friendly list.").action(async (cmdOpts) => {
27072
+ process.exitCode = await secretsListCommand({
27073
+ ...cmdOpts.dir !== void 0 ? { dir: cmdOpts.dir } : {},
27074
+ ...cmdOpts.install !== void 0 ? { install: cmdOpts.install } : {},
27075
+ ...cmdOpts.json !== void 0 ? { json: cmdOpts.json } : {}
27076
+ });
27077
+ });
27078
+ secretsCmd.command("set [keys...]").description(
27079
+ "Set secret values, REPLACING the whole map (keys set now but not given are removed; you are asked first). Values come from a hidden prompt, --from-stdin or --from-env-file, never from the command line."
27080
+ ).option("--dir <path>", "Project root (default: current directory).").option(
27081
+ "--install <id>",
27082
+ "Target a specific installation id instead of the cached dev install (production installs included). Find ids via `dashwise module status <slug>`.",
27083
+ parseIntOption
27084
+ ).option("--from-stdin", "Read the value of exactly one key from stdin.").option("--from-env-file <path>", "Read values from a dotenv-style file (all declared keys found in it, or the keys named).").option("--keys <list>", "With --from-env-file: only these keys (comma-separated).").option("-y, --yes", "Remove keys that are set now but not in the new map without asking.").option("--json", "Emit a JSON summary (key names only).").addHelpText(
27085
+ "before",
27086
+ pc.bold(
27087
+ "set REPLACES THE WHOLE MAP: every key you do not give is removed (you are asked first; --yes skips the question)."
27088
+ )
27089
+ ).action(
27090
+ async (keys, cmdOpts) => {
27091
+ process.exitCode = await secretsSetCommand(keys ?? [], {
27092
+ ...cmdOpts.dir !== void 0 ? { dir: cmdOpts.dir } : {},
27093
+ ...cmdOpts.install !== void 0 ? { install: cmdOpts.install } : {},
27094
+ ...cmdOpts.fromStdin !== void 0 ? { fromStdin: cmdOpts.fromStdin } : {},
27095
+ ...cmdOpts.fromEnvFile !== void 0 ? { fromEnvFile: cmdOpts.fromEnvFile } : {},
27096
+ ...cmdOpts.keys !== void 0 ? { keys: cmdOpts.keys } : {},
27097
+ ...cmdOpts.yes !== void 0 ? { yes: cmdOpts.yes } : {},
27098
+ ...cmdOpts.json !== void 0 ? { json: cmdOpts.json } : {}
27099
+ });
27100
+ }
27101
+ );
27102
+ secretsCmd.command("clear").description("Remove every secret value of the install.").option("--dir <path>", "Project root (default: current directory).").option(
27103
+ "--install <id>",
27104
+ "Target a specific installation id instead of the cached dev install (production installs included). Find ids via `dashwise module status <slug>`.",
27105
+ parseIntOption
27106
+ ).option("-y, --yes", "Do not ask for confirmation.").action(async (cmdOpts) => {
27107
+ process.exitCode = await secretsClearCommand({
27108
+ ...cmdOpts.dir !== void 0 ? { dir: cmdOpts.dir } : {},
27109
+ ...cmdOpts.install !== void 0 ? { install: cmdOpts.install } : {},
27110
+ ...cmdOpts.yes !== void 0 ? { yes: cmdOpts.yes } : {}
27111
+ });
27112
+ });
26421
27113
  var envCmd = program.command("env").description("Environment-variable helpers for deploying a module.");
26422
27114
  envCmd.command("export").description(
26423
27115
  "Emit the env vars a deployed module needs to authenticate against Dashwise. By default emits .env-style KEY=value lines; --format = env | json | railway | vercel | fly to wrap in platform-native syntax. Use --new-key <name> to mint + inline an API key in one step."
@@ -26451,6 +27143,36 @@ moduleCmd.command("release [slug]").description("Release an exact pushed reposit
26451
27143
  timeoutSec: opts.timeout
26452
27144
  });
26453
27145
  });
27146
+ moduleCmd.command("build-log [slug] [buildId]").description(
27147
+ "Print a build's log, scrubbed by the server (credentials and internal hosts hidden) and bounded to its newest part. One positional is the build id (module from module.json). --follow prints new lines until the build ends."
27148
+ ).option("--dir <path>", "Project root for module.json slug lookup (default: current directory).").option("--follow", "Keep printing new lines while the build is queued or building.").option("--timeout <seconds>", "Max seconds to follow (default 900); exit 4 means still building.", Number).option("--json", "Emit the log response as JSON (not with --follow).").action(
27149
+ async (slug, buildId, opts) => {
27150
+ process.exitCode = await moduleBuildLogCommand(slug, buildId, {
27151
+ ...opts.dir !== void 0 ? { dir: opts.dir } : {},
27152
+ ...opts.follow !== void 0 ? { follow: opts.follow } : {},
27153
+ ...opts.timeout !== void 0 ? { timeoutSec: opts.timeout } : {},
27154
+ ...opts.json !== void 0 ? { json: opts.json } : {}
27155
+ });
27156
+ }
27157
+ );
27158
+ moduleCmd.command("ship [slug]").description(
27159
+ "Release and install in one command, for a person: releases the latest push to the default branch (or --commit-sha), waits for the build, then installs that version, showing what the update does to the data and asking once. Refused inside an AI agent session (an agent releases; a person takes it live)."
27160
+ ).option("--dir <path>", "Project root for module.json slug lookup (default: current directory).").option("--commit-sha <sha>", "Release this pushed revision (full 40-character SHA) instead of the default-branch head.").option("--build <id>", "Resume a ship whose release was still building: wait for that build, then install it. Never queues a release.", parsePositiveIdOption).option("--bump <direction>", "Semver bump: patch | minor | major (default minor).").option("--workspace <slug-or-id>", "Target workspace (as for module install).").option("--installation <id>", "Update exactly this production installation.", parsePositiveIdOption).option("--keep-dev-install", "Install alongside the workspace's dev install (as for module install).").option("--replace-dev-install", "Replace the workspace's dev install (as for module install).").option("-y, --yes", "Skip the install question when nothing is lost. Never accepts data loss.").option("--timeout <seconds>", "Max seconds to wait for the build (default 360); exit 4 means still building.", Number).action(
27161
+ async (slug, opts) => {
27162
+ process.exitCode = await moduleShipCommand(slug, {
27163
+ ...opts.dir !== void 0 ? { dir: opts.dir } : {},
27164
+ ...opts.commitSha !== void 0 ? { commitSha: opts.commitSha } : {},
27165
+ ...opts.build !== void 0 ? { build: opts.build } : {},
27166
+ ...opts.bump !== void 0 ? { bump: opts.bump } : {},
27167
+ ...opts.workspace !== void 0 ? { workspace: opts.workspace } : {},
27168
+ ...opts.installation !== void 0 ? { installation: opts.installation } : {},
27169
+ ...opts.keepDevInstall !== void 0 ? { keepDevInstall: opts.keepDevInstall } : {},
27170
+ ...opts.replaceDevInstall !== void 0 ? { replaceDevInstall: opts.replaceDevInstall } : {},
27171
+ ...opts.yes !== void 0 ? { yes: opts.yes } : {},
27172
+ ...opts.timeout !== void 0 ? { timeoutSec: opts.timeout } : {}
27173
+ });
27174
+ }
27175
+ );
26454
27176
  moduleCmd.command("publish").description("Pack the current directory + upload it as a new module version.").option("--dir <path>", "Project root (default: current directory).").option("--bump <direction>", "Semver bump: patch | minor | major.").option("--dry-run", "Validate + upload but do not persist the version row.").option("-y, --yes", "Skip the confirmation prompt (for CI / scripted use).").option(
26455
27177
  "--from-local",
26456
27178
  "Publish local files to a module that has a linked repository (the next repository build replaces them)."
@@ -26874,7 +27596,7 @@ fieldCmd.command("set-type <table-slug> <field-slug> <new-type>").description(
26874
27596
  }
26875
27597
  );
26876
27598
  function getVersion() {
26877
- return "0.28.1";
27599
+ return "0.30.1";
26878
27600
  }
26879
27601
  function parsePositiveIdOption(value) {
26880
27602
  if (!/^[1-9][0-9]*$/.test(value.trim())) {