@dashai/cli 0.30.1 → 0.31.0

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
@@ -5,7 +5,7 @@ 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
7
  import { intro, log, select, isCancel, cancel, spinner, outro, confirm as confirm$1, password, text, multiselect } from '@clack/prompts';
8
- import { Command, Option } from 'commander';
8
+ import { Command, Option, InvalidArgumentError } from 'commander';
9
9
  import open from 'open';
10
10
  import AdmZip from 'adm-zip';
11
11
  import * as tar from 'tar';
@@ -13013,6 +13013,10 @@ generate\` and the schema commands. When the task needs them:
13013
13013
  a command reports that the login is missing.
13014
13014
  `;
13015
13015
  }
13016
+ function hostedAppOrigin(apiUrl) {
13017
+ const host = companyApiHost(apiUrl);
13018
+ return host ? `https://${HOSTED_API_HOSTS[host]}` : null;
13019
+ }
13016
13020
  function companyApiHost(apiUrl) {
13017
13021
  if (!apiUrl) return null;
13018
13022
  try {
@@ -13913,325 +13917,68 @@ var init_init = __esm({
13913
13917
  }
13914
13918
  });
13915
13919
 
13916
- // src/lib/manifest-evolution.ts
13917
- function compareManifests(base, head, history = []) {
13918
- const violations = [];
13919
- const additions = [];
13920
- const warnings = [];
13921
- const v = (rule, where, detail) => violations.push({ rule, where, detail });
13922
- const baseTables = tablesOf(base);
13923
- const headTables = tablesOf(head);
13924
- const everTables = /* @__PURE__ */ new Set();
13925
- const everFields = /* @__PURE__ */ new Set();
13926
- for (const m of history) {
13927
- for (const [slug, t] of tablesOf(m)) {
13928
- everTables.add(slug);
13929
- for (const f of t.fields ?? []) everFields.add(`${slug}.${f.slug}`);
13930
- }
13931
- }
13932
- for (const [slug, bt] of baseTables) {
13933
- const ht = headTables.get(slug);
13934
- if (!ht) {
13935
- v("removed_table", slug, "tables are never removed; deprecate instead");
13936
- continue;
13937
- }
13938
- const bf = fieldsOf(bt);
13939
- const hf = fieldsOf(ht);
13940
- for (const [fs, f] of bf) {
13941
- const g = hf.get(fs);
13942
- const where = `${slug}.${fs}`;
13943
- if (!g) {
13944
- v("removed_field", where, "fields are never removed; stop writing it and set human_edit: open");
13945
- continue;
13946
- }
13947
- if (f.type !== g.type) v("retyped_field", where, `${f.type} \u2192 ${g.type}`);
13948
- if ((f.decimal_places ?? null) !== (g.decimal_places ?? null)) {
13949
- v("retyped_field", where, `decimal_places ${f.decimal_places ?? "none"} \u2192 ${g.decimal_places ?? "none"}`);
13950
- }
13951
- if ((f.target ?? null) !== (g.target ?? null)) v("retyped_field", where, `link target ${f.target} \u2192 ${g.target}`);
13952
- if (f.required !== true && g.required === true) {
13953
- v("required_tightened", where, "old rows are empty; enforce it in the tool layer instead");
13954
- }
13955
- const bo = f.options ?? [];
13956
- const ho = g.options ?? [];
13957
- for (const [i, o] of bo.entries()) {
13958
- if (!ho.includes(o)) v("removed_option", where, `option "${String(o)}" removed`);
13959
- else if (ho[i] !== o) {
13960
- v("reordered_option", where, `option "${String(o)}" moved from ${i} to ${ho.indexOf(o)}; options are append-only`);
13961
- }
13962
- }
13963
- for (const o of ho.slice(bo.length)) if (!bo.includes(o)) additions.push(`option ${where} "${String(o)}"`);
13964
- }
13965
- for (const [fs] of hf) {
13966
- if (bf.has(fs)) continue;
13967
- if (everFields.has(`${slug}.${fs}`)) {
13968
- v("slug_reused", `${slug}.${fs}`, "this slug was removed in an earlier version and may not come back");
13969
- } else additions.push(`field ${slug}.${fs}`);
13970
- }
13971
- const hu = new Set(uniqueIndexes(ht));
13972
- for (const idx of uniqueIndexes(bt)) {
13973
- if (!hu.has(idx)) v("dropped_unique_index", `${slug}(${idx})`, "a unique index is a protocol lock; it may not be dropped or changed");
13974
- }
13975
- const bu = new Set(uniqueIndexes(bt));
13976
- for (const idx of hu) {
13977
- if (bu.has(idx)) continue;
13978
- const overExisting = idx.split(",").some((f) => f.length > 0 && bf.has(f));
13979
- if (overExisting) {
13980
- v("unique_on_populated", `${slug}(${idx})`, "a new unique index (indexes[].unique) over existing fields fails on duplicates already stored; add a new field instead");
13981
- } else additions.push(`unique index ${slug}(${idx})`);
13982
- }
13983
- }
13984
- for (const [slug] of headTables) {
13985
- if (baseTables.has(slug)) continue;
13986
- if (everTables.has(slug)) v("slug_reused", slug, "this table slug was removed in an earlier version and may not come back");
13987
- else additions.push(`table ${slug}`);
13988
- }
13989
- const baseViews = new Set((base?.owns?.views ?? []).map((x) => x.slug));
13990
- const headViews = new Set((head?.owns?.views ?? []).map((x) => x.slug));
13991
- for (const s of baseViews) if (!headViews.has(s)) warnings.push(`view ${s} removed (callers of qbViews.${s} break)`);
13992
- for (const s of headViews) if (!baseViews.has(s)) additions.push(`view ${s}`);
13993
- return { violations, additions, warnings };
13920
+ // src/lib/permission-hint.ts
13921
+ function str(v) {
13922
+ return typeof v === "string" && v.length > 0 ? v : void 0;
13994
13923
  }
13995
- var tablesOf, fieldsOf, uniqueIndexes;
13996
- var init_manifest_evolution = __esm({
13997
- "src/lib/manifest-evolution.ts"() {
13998
- tablesOf = (m) => new Map((m?.owns?.tables ?? []).map((t) => [t.slug, t]));
13999
- fieldsOf = (t) => new Map((t?.fields ?? []).map((f) => [f.slug, f]));
14000
- uniqueIndexes = (t) => (t?.indexes ?? []).filter((i) => i.unique === true).map((i) => (i.fields ?? []).join(","));
14001
- }
14002
- });
14003
-
14004
- // src/commands/module/diff-check.ts
14005
- var diff_check_exports = {};
14006
- __export(diff_check_exports, {
14007
- EvolutionCheckError: () => EvolutionCheckError,
14008
- moduleDiffCheckCommand: () => moduleDiffCheckCommand,
14009
- printEvolutionReport: () => printEvolutionReport,
14010
- publishEvolutionGate: () => publishEvolutionGate,
14011
- runEvolutionCheck: () => runEvolutionCheck
14012
- });
14013
- function readJson(path) {
14014
- try {
14015
- return JSON.parse(readFileSync(path, "utf8"));
14016
- } catch (err) {
14017
- throw new EvolutionCheckError(`could not read ${path}: ${err.message}`);
14018
- }
13924
+ function num(v) {
13925
+ return typeof v === "number" && Number.isFinite(v) ? v : void 0;
14019
13926
  }
14020
- function git(cwd, args) {
14021
- try {
14022
- return execFileSync("git", ["-C", cwd, ...args], {
14023
- encoding: "utf8",
14024
- stdio: ["ignore", "pipe", "pipe"],
14025
- maxBuffer: 64 * 1024 * 1024
14026
- });
14027
- } catch (err) {
14028
- const stderr = err.stderr?.trim();
14029
- throw new EvolutionCheckError(`git ${args.join(" ")} failed${stderr ? `: ${stderr}` : ""}`);
14030
- }
13927
+ function featureFromPermission(permission) {
13928
+ const m = permission ? /^workspace\.feature\.([a-z0-9_]+)\.use$/.exec(permission) : null;
13929
+ return m?.[1];
14031
13930
  }
14032
- async function runEvolutionCheck(options) {
14033
- const projectRoot = resolve(options.dir ?? process.cwd());
14034
- const headPath = options.head ? resolve(options.head) : join(projectRoot, "module.json");
14035
- if (!existsSync(headPath)) throw new EvolutionCheckError(`no manifest at ${headPath}`);
14036
- const head = readJson(headPath);
14037
- const baseFile = options.baseFile ?? (options.base && existsSync(options.base) ? options.base : void 0);
14038
- if (baseFile) {
14039
- const base = readJson(baseFile);
14040
- return { ...compareManifests(base, head), base: baseFile, head: headPath, history: 0, nothingReleased: false };
14041
- }
14042
- if (options.base) {
14043
- const ref = options.base;
14044
- const base = JSON.parse(git(projectRoot, ["show", `${ref}:./module.json`]));
14045
- let history2 = [];
14046
- if (!options.noHistory) {
14047
- const shas = git(projectRoot, ["log", "--format=%H", "--reverse", ref, "--", "module.json"]).split("\n").filter(Boolean);
14048
- history2 = shas.slice(0, -1).map((sha) => JSON.parse(git(projectRoot, ["show", `${sha}:./module.json`])));
14049
- }
13931
+ function featureSubject(ctx) {
13932
+ const feature = str(ctx.feature) ?? featureFromPermission(str(ctx.permission));
13933
+ if (feature && FEATURE_SUBJECTS[feature]) return FEATURE_SUBJECTS[feature];
13934
+ return str(ctx.feature_label) ?? (feature ? `"${feature}"` : void 0);
13935
+ }
13936
+ function needsAdmin(what, module, ask) {
13937
+ const scope = module ? ` for ${module}` : "";
13938
+ return `${what}${scope} needs a Dashwise admin. Ask your Dashwise admin to ${ask}.`;
13939
+ }
13940
+ function refusalHint(code2, context2, subject = {}) {
13941
+ const ctx = context2 ?? {};
13942
+ const module = subject.module ?? str(ctx.module_slug);
13943
+ const installationId = subject.installationId ?? num(ctx.installation_id);
13944
+ const installLabel = installationId !== void 0 ? ` (install #${installationId})` : "";
13945
+ const named = module ?? "this module";
13946
+ const reason = str(ctx.reason);
13947
+ if (code2 === "MODULE_CAPABILITY_DENIED" && reason === "authoring_access_required") {
14050
13948
  return {
14051
- ...compareManifests(base, head, history2),
14052
- base: `${ref}:module.json`,
14053
- head: headPath,
14054
- history: history2.length,
14055
- nothingReleased: false
13949
+ message: "You need authoring access to export development data; ask someone who manages module access.",
13950
+ needsGrant: true
14056
13951
  };
14057
13952
  }
14058
- const slug = head.module?.slug;
14059
- if (!slug) throw new EvolutionCheckError(`${headPath} has no module.slug`);
14060
- let mod;
14061
- let versions;
14062
- try {
14063
- mod = await apiRequest(
14064
- "GET",
14065
- `/api/modules/by-slug/${encodeURIComponent(slug)}`
14066
- );
14067
- if (!mod.current_version) {
13953
+ if (code2 === "RAW_DATA_NOT_ALLOWED") {
13954
+ if (reason === "raw_data_write_required") {
14068
13955
  return {
14069
- violations: [],
14070
- additions: [],
14071
- warnings: [],
14072
- base: "(nothing released)",
14073
- head: headPath,
14074
- history: 0,
14075
- nothingReleased: true
13956
+ message: "You don't have access to change this data. Ask a data list Admin for Editor access in the Data Hub Permissions tab.",
13957
+ needsGrant: true
14076
13958
  };
14077
13959
  }
14078
- versions = await apiRequest("GET", `/api/modules/${mod.id}/versions`);
14079
- } catch (err) {
14080
- const why = err instanceof ApiError ? `${err.code || err.status} ${err.message}` : err.message;
14081
- throw new EvolutionCheckError(
14082
- `could not read the released manifest of ${slug} (${why}); pass --base <ref|file> to check offline`
14083
- );
14084
- }
14085
- const released = versions.find((v) => v.version === mod.current_version);
14086
- if (!released?.manifest_jsonb) {
14087
- throw new EvolutionCheckError(
14088
- `the platform returned no manifest for ${slug} v${mod.current_version}; pass --base <ref|file> to check offline`
14089
- );
14090
- }
14091
- const releasedAt = released.created_on ? Date.parse(released.created_on) : Number.POSITIVE_INFINITY;
14092
- const history = options.noHistory ? [] : versions.filter((v) => v !== released && v.manifest_jsonb && v.created_on && Date.parse(v.created_on) < releasedAt).sort((a, b) => Date.parse(a.created_on) - Date.parse(b.created_on)).map((v) => v.manifest_jsonb);
14093
- return {
14094
- ...compareManifests(released.manifest_jsonb, head, history),
14095
- base: `${slug} v${mod.current_version} (released)`,
14096
- head: headPath,
14097
- history: history.length,
14098
- nothingReleased: false
14099
- };
14100
- }
14101
- function printEvolutionReport(r, print = out) {
14102
- print(`module diff --check: ${r.head} against ${r.base} (${r.history} older version${r.history === 1 ? "" : "s"} scanned)`);
14103
- for (const a of r.additions) print(` + ${a}`);
14104
- for (const w of r.warnings) print(` ! ${w}`);
14105
- for (const x of r.violations) print(` \u2717 ${x.rule}: ${x.where}: ${x.detail}`);
14106
- }
14107
- async function moduleDiffCheckCommand(options) {
14108
- let r;
14109
- try {
14110
- r = await runEvolutionCheck(options);
14111
- } catch (err) {
14112
- if (err instanceof EvolutionCheckError) {
14113
- if (options.json) out(JSON.stringify({ ok: false, error: { code: "CHECK_FAILED", message: err.message } }));
14114
- else fail(`module diff --check: ${err.message}`);
14115
- return 2;
13960
+ if (reason === "installation_access_required") {
13961
+ return {
13962
+ message: "You cannot open this installation with your current access; ask someone who manages installation access.",
13963
+ needsGrant: true
13964
+ };
14116
13965
  }
14117
- throw err;
14118
- }
14119
- if (options.json) {
14120
- out(JSON.stringify({ ok: r.violations.length === 0, ...r }, null, 2));
14121
- return r.violations.length === 0 ? 0 : 1;
13966
+ return {
13967
+ message: "You don't have access to view this data. Ask a data list Admin for Viewer access in the Data Hub Permissions tab.",
13968
+ needsGrant: true
13969
+ };
14122
13970
  }
14123
- if (r.nothingReleased) {
14124
- success("Nothing released yet: every change is additive.");
14125
- return 0;
13971
+ if (code2 === "VALIDATION_ERROR" && reason === "module_context_required") {
13972
+ return {
13973
+ message: "Open the intended module project and select the company and installation.",
13974
+ needsGrant: false
13975
+ };
14126
13976
  }
14127
- printEvolutionReport(r);
14128
- if (r.violations.length === 0) {
14129
- success("additive");
14130
- return 0;
14131
- }
14132
- fail(`${r.violations.length} violation(s): not additive. Released copies would lose data or fail to upgrade.`);
14133
- info(`Add a new field or table instead, and stop writing the old one (${code('human_edit: "open"')}).`);
14134
- return 1;
14135
- }
14136
- async function publishEvolutionGate(projectRoot, strict) {
14137
- let r;
14138
- try {
14139
- r = await runEvolutionCheck({ dir: projectRoot });
14140
- } catch (err) {
14141
- const msg = err instanceof Error ? err.message : String(err);
14142
- if (strict) {
14143
- fail(`Evolution check could not run (${msg}). Refusing with --strict. Nothing was published.`);
14144
- return 1;
14145
- }
14146
- warn(`Evolution check skipped: ${msg}.`);
14147
- return 0;
14148
- }
14149
- if (r.nothingReleased || r.violations.length === 0) return 0;
14150
- printEvolutionReport(r, (s) => (strict ? fail : warn)(s));
14151
- if (strict) {
14152
- fail(
14153
- `module.json is not an additive evolution of the released version (${r.violations.length} violation(s)). Refusing with --strict. Nothing was published.`
14154
- );
14155
- return 1;
14156
- }
14157
- warn(
14158
- `module.json is not an additive evolution of the released version: installs that upgrade lose the data above. ${code("--strict")} turns this into a refusal; ${code("dashwise module diff --check")} shows it on its own.`
14159
- );
14160
- return 0;
14161
- }
14162
- var EvolutionCheckError;
14163
- var init_diff_check = __esm({
14164
- "src/commands/module/diff-check.ts"() {
14165
- init_api_client();
14166
- init_manifest_evolution();
14167
- init_output();
14168
- EvolutionCheckError = class extends Error {
14169
- };
14170
- }
14171
- });
14172
-
14173
- // src/lib/permission-hint.ts
14174
- function str(v) {
14175
- return typeof v === "string" && v.length > 0 ? v : void 0;
14176
- }
14177
- function num(v) {
14178
- return typeof v === "number" && Number.isFinite(v) ? v : void 0;
14179
- }
14180
- function featureFromPermission(permission) {
14181
- const m = permission ? /^workspace\.feature\.([a-z0-9_]+)\.use$/.exec(permission) : null;
14182
- return m?.[1];
14183
- }
14184
- function featureSubject(ctx) {
14185
- const feature = str(ctx.feature) ?? featureFromPermission(str(ctx.permission));
14186
- if (feature && FEATURE_SUBJECTS[feature]) return FEATURE_SUBJECTS[feature];
14187
- return str(ctx.feature_label) ?? (feature ? `"${feature}"` : void 0);
14188
- }
14189
- function needsAdmin(what, module, ask) {
14190
- const scope = module ? ` for ${module}` : "";
14191
- return `${what}${scope} needs a Dashwise admin. Ask your Dashwise admin to ${ask}.`;
14192
- }
14193
- function refusalHint(code2, context2, subject = {}) {
14194
- const ctx = context2 ?? {};
14195
- const module = subject.module ?? str(ctx.module_slug);
14196
- const installationId = subject.installationId ?? num(ctx.installation_id);
14197
- const installLabel = installationId !== void 0 ? ` (install #${installationId})` : "";
14198
- const named = module ?? "this module";
14199
- const reason = str(ctx.reason);
14200
- if (code2 === "MODULE_CAPABILITY_DENIED" && reason === "authoring_access_required") {
14201
- return {
14202
- message: "You need authoring access to export development data; ask someone who manages module access.",
14203
- needsGrant: true
14204
- };
14205
- }
14206
- if (code2 === "RAW_DATA_NOT_ALLOWED") {
14207
- if (reason === "raw_data_write_required") {
14208
- return {
14209
- message: "You don't have access to change this data. Ask a data list Admin for Editor access in the Data Hub Permissions tab.",
14210
- needsGrant: true
14211
- };
14212
- }
14213
- if (reason === "installation_access_required") {
14214
- return {
14215
- message: "You cannot open this installation with your current access; ask someone who manages installation access.",
14216
- needsGrant: true
14217
- };
14218
- }
14219
- return {
14220
- message: "You don't have access to view this data. Ask a data list Admin for Viewer access in the Data Hub Permissions tab.",
14221
- needsGrant: true
14222
- };
14223
- }
14224
- if (code2 === "VALIDATION_ERROR" && reason === "module_context_required") {
14225
- return {
14226
- message: "Open the intended module project and select the company and installation.",
14227
- needsGrant: false
14228
- };
14229
- }
14230
- if (code2 === "VALIDATION_ERROR" && reason === "context_mismatch") {
14231
- return {
14232
- message: "The selected company and installation do not match; check your selection.",
14233
- needsGrant: false
14234
- };
13977
+ if (code2 === "VALIDATION_ERROR" && reason === "context_mismatch") {
13978
+ return {
13979
+ message: "The selected company and installation do not match; check your selection.",
13980
+ needsGrant: false
13981
+ };
14235
13982
  }
14236
13983
  if (code2 === "UNAUTHENTICATED" && reason === "module_api_credential_missing") {
14237
13984
  return {
@@ -14398,107 +14145,364 @@ function reportFailedOutcome(failure, hint, opts) {
14398
14145
  ...opts.extra
14399
14146
  })
14400
14147
  );
14401
- } else {
14402
- fail(hint?.message ?? opts.fallback);
14148
+ } else {
14149
+ fail(hint?.message ?? opts.fallback);
14150
+ }
14151
+ return hint?.needsGrant ? EXIT_NEEDS_GRANT : 1;
14152
+ }
14153
+ function isTransientPollError(err) {
14154
+ if (!(err instanceof ApiError)) return true;
14155
+ return err.status === 0 || err.status === 429 || err.status >= 500;
14156
+ }
14157
+ function isTypedRequestError(err) {
14158
+ if (err instanceof ApiError) return true;
14159
+ if (!(err instanceof Error)) return false;
14160
+ const candidate = err;
14161
+ return typeof candidate.code === "string" && typeof candidate.status === "number" && (candidate.context === void 0 || candidate.context !== null && typeof candidate.context === "object" && !Array.isArray(candidate.context));
14162
+ }
14163
+ function reportRefusal(err, subject, opts) {
14164
+ const writeJson = (message) => {
14165
+ if (opts.stderr) process.stderr.write(`${message}
14166
+ `);
14167
+ else out(message);
14168
+ };
14169
+ if (!isTypedRequestError(err)) {
14170
+ const message = err?.message ?? String(err);
14171
+ if (opts.json) {
14172
+ writeJson(
14173
+ JSON.stringify({
14174
+ error: {
14175
+ code: "CLI_ERROR",
14176
+ message: message.replace(ANSI, ""),
14177
+ status: null,
14178
+ context: null
14179
+ },
14180
+ hint: null,
14181
+ needs_grant: false
14182
+ })
14183
+ );
14184
+ } else {
14185
+ fail(`${opts.action} failed: ${message}`);
14186
+ }
14187
+ return 1;
14188
+ }
14189
+ const hint = err.status === 401 && err.context?.reason !== "module_api_credential_missing" ? {
14190
+ message: opts.moduleKey ? MODULE_KEY_REFUSED[opts.moduleKey] : "Your login has expired. Run: dashwise login",
14191
+ needsGrant: false
14192
+ } : refusalHint(err.code, err.context, subject);
14193
+ if (opts.json) {
14194
+ writeJson(
14195
+ JSON.stringify({
14196
+ error: {
14197
+ code: err.code,
14198
+ message: err.message,
14199
+ status: err.status,
14200
+ context: err.context ?? null
14201
+ },
14202
+ hint: hint?.message ?? null,
14203
+ needs_grant: hint?.needsGrant ?? false
14204
+ })
14205
+ );
14206
+ } else if (hint) {
14207
+ fail(
14208
+ `${hint.message} (${err.code})${err.context ? ` context=${JSON.stringify(err.context)}` : ""}`
14209
+ );
14210
+ } else {
14211
+ fail(
14212
+ `${opts.action} failed (${err.code}): ${err.message}${err.context ? ` context=${JSON.stringify(err.context)}` : ""}`
14213
+ );
14214
+ }
14215
+ return hint?.needsGrant ? EXIT_NEEDS_GRANT : 1;
14216
+ }
14217
+ var EXIT_NEEDS_GRANT, EXIT_TIMED_OUT, REBUILD_BUMP_OPERATOR_ONLY, FEATURE_SUBJECTS, OPTION_SUBJECTS, MODULE_RIGHT_CODES, ANSI, MODULE_KEY_REFUSED;
14218
+ var init_permission_hint = __esm({
14219
+ "src/lib/permission-hint.ts"() {
14220
+ init_api_client();
14221
+ init_output();
14222
+ EXIT_NEEDS_GRANT = 3;
14223
+ EXIT_TIMED_OUT = 4;
14224
+ REBUILD_BUMP_OPERATOR_ONLY = "REBUILD_BUMP_OPERATOR_ONLY";
14225
+ FEATURE_SUBJECTS = {
14226
+ module_runtime_streaming: "Streaming",
14227
+ module_rebuild: "Rebuild",
14228
+ module_building: "Building modules",
14229
+ module_runtime_sizing: "Memory size"
14230
+ };
14231
+ OPTION_SUBJECTS = {
14232
+ streaming: "Streaming",
14233
+ memory_mb: "Memory size",
14234
+ reapply: "Re-applying the runtime",
14235
+ timeout: "The timeout",
14236
+ reserved_concurrency: "Reserved concurrency",
14237
+ log_retention: "Log retention"
14238
+ };
14239
+ MODULE_RIGHT_CODES = /* @__PURE__ */ new Set([
14240
+ "MODULE_CAPABILITY_DENIED",
14241
+ "PUBLISH_REQUIRES_ADMIN",
14242
+ "NOT_COPY_OWNER"
14243
+ ]);
14244
+ ANSI = /\x1b\[[0-9;]*m/g;
14245
+ MODULE_KEY_REFUSED = {
14246
+ env: "The module key in DASHWISE_API_KEY was refused (expired or revoked). Replace it with a current key, or unset it to use this module's saved key. dashwise login does not renew module keys.",
14247
+ env_file: "The module key in .env.local (DASHWISE_API_KEY) was refused (expired or revoked). Replace it, or remove that line to use this module's saved key. dashwise login does not renew module keys.",
14248
+ saved: `This module's saved key was refused (expired or revoked). In your CLI auth.json, set only modules[<slug>].apiKey to an empty string (""), keeping installationId and all other fields. Then run dashwise dev from this module's directory with DASHWISE_API_KEY unset to mint and save a replacement key for the same installation. dashwise login does not renew module keys.`
14249
+ };
14250
+ }
14251
+ });
14252
+
14253
+ // src/lib/manifest-evolution.ts
14254
+ function compareManifests(base, head, history = []) {
14255
+ const violations = [];
14256
+ const additions = [];
14257
+ const warnings = [];
14258
+ const v = (rule, where, detail) => violations.push({ rule, where, detail });
14259
+ const baseTables = tablesOf(base);
14260
+ const headTables = tablesOf(head);
14261
+ const everTables = /* @__PURE__ */ new Set();
14262
+ const everFields = /* @__PURE__ */ new Set();
14263
+ for (const m of history) {
14264
+ for (const [slug, t] of tablesOf(m)) {
14265
+ everTables.add(slug);
14266
+ for (const f of t.fields ?? []) everFields.add(`${slug}.${f.slug}`);
14267
+ }
14268
+ }
14269
+ for (const [slug, bt] of baseTables) {
14270
+ const ht = headTables.get(slug);
14271
+ if (!ht) {
14272
+ v("removed_table", slug, "tables are never removed; deprecate instead");
14273
+ continue;
14274
+ }
14275
+ const bf = fieldsOf(bt);
14276
+ const hf = fieldsOf(ht);
14277
+ for (const [fs, f] of bf) {
14278
+ const g = hf.get(fs);
14279
+ const where = `${slug}.${fs}`;
14280
+ if (!g) {
14281
+ v("removed_field", where, "fields are never removed; stop writing it and set human_edit: open");
14282
+ continue;
14283
+ }
14284
+ if (f.type !== g.type) v("retyped_field", where, `${f.type} \u2192 ${g.type}`);
14285
+ if ((f.decimal_places ?? null) !== (g.decimal_places ?? null)) {
14286
+ v("retyped_field", where, `decimal_places ${f.decimal_places ?? "none"} \u2192 ${g.decimal_places ?? "none"}`);
14287
+ }
14288
+ if ((f.target ?? null) !== (g.target ?? null)) v("retyped_field", where, `link target ${f.target} \u2192 ${g.target}`);
14289
+ if (f.required !== true && g.required === true) {
14290
+ v("required_tightened", where, "old rows are empty; enforce it in the tool layer instead");
14291
+ }
14292
+ const bo = f.options ?? [];
14293
+ const ho = g.options ?? [];
14294
+ for (const [i, o] of bo.entries()) {
14295
+ if (!ho.includes(o)) v("removed_option", where, `option "${String(o)}" removed`);
14296
+ else if (ho[i] !== o) {
14297
+ v("reordered_option", where, `option "${String(o)}" moved from ${i} to ${ho.indexOf(o)}; options are append-only`);
14298
+ }
14299
+ }
14300
+ for (const o of ho.slice(bo.length)) if (!bo.includes(o)) additions.push(`option ${where} "${String(o)}"`);
14301
+ }
14302
+ for (const [fs] of hf) {
14303
+ if (bf.has(fs)) continue;
14304
+ if (everFields.has(`${slug}.${fs}`)) {
14305
+ v("slug_reused", `${slug}.${fs}`, "this slug was removed in an earlier version and may not come back");
14306
+ } else additions.push(`field ${slug}.${fs}`);
14307
+ }
14308
+ const hu = new Set(uniqueIndexes(ht));
14309
+ for (const idx of uniqueIndexes(bt)) {
14310
+ if (!hu.has(idx)) v("dropped_unique_index", `${slug}(${idx})`, "a unique index is a protocol lock; it may not be dropped or changed");
14311
+ }
14312
+ const bu = new Set(uniqueIndexes(bt));
14313
+ for (const idx of hu) {
14314
+ if (bu.has(idx)) continue;
14315
+ const overExisting = idx.split(",").some((f) => f.length > 0 && bf.has(f));
14316
+ if (overExisting) {
14317
+ v("unique_on_populated", `${slug}(${idx})`, "a new unique index (indexes[].unique) over existing fields fails on duplicates already stored; add a new field instead");
14318
+ } else additions.push(`unique index ${slug}(${idx})`);
14319
+ }
14320
+ }
14321
+ for (const [slug] of headTables) {
14322
+ if (baseTables.has(slug)) continue;
14323
+ if (everTables.has(slug)) v("slug_reused", slug, "this table slug was removed in an earlier version and may not come back");
14324
+ else additions.push(`table ${slug}`);
14325
+ }
14326
+ const baseViews = new Set((base?.owns?.views ?? []).map((x) => x.slug));
14327
+ const headViews = new Set((head?.owns?.views ?? []).map((x) => x.slug));
14328
+ for (const s of baseViews) if (!headViews.has(s)) warnings.push(`view ${s} removed (callers of qbViews.${s} break)`);
14329
+ for (const s of headViews) if (!baseViews.has(s)) additions.push(`view ${s}`);
14330
+ return { violations, additions, warnings };
14331
+ }
14332
+ var tablesOf, fieldsOf, uniqueIndexes;
14333
+ var init_manifest_evolution = __esm({
14334
+ "src/lib/manifest-evolution.ts"() {
14335
+ tablesOf = (m) => new Map((m?.owns?.tables ?? []).map((t) => [t.slug, t]));
14336
+ fieldsOf = (t) => new Map((t?.fields ?? []).map((f) => [f.slug, f]));
14337
+ uniqueIndexes = (t) => (t?.indexes ?? []).filter((i) => i.unique === true).map((i) => (i.fields ?? []).join(","));
14338
+ }
14339
+ });
14340
+
14341
+ // src/commands/module/diff-check.ts
14342
+ var diff_check_exports = {};
14343
+ __export(diff_check_exports, {
14344
+ EvolutionCheckError: () => EvolutionCheckError,
14345
+ moduleDiffCheckCommand: () => moduleDiffCheckCommand,
14346
+ printEvolutionReport: () => printEvolutionReport,
14347
+ publishEvolutionGate: () => publishEvolutionGate,
14348
+ runEvolutionCheck: () => runEvolutionCheck
14349
+ });
14350
+ function readJson(path) {
14351
+ try {
14352
+ return JSON.parse(readFileSync(path, "utf8"));
14353
+ } catch (err) {
14354
+ throw new EvolutionCheckError(`could not read ${path}: ${err.message}`);
14355
+ }
14356
+ }
14357
+ function git(cwd, args) {
14358
+ try {
14359
+ return execFileSync("git", ["-C", cwd, ...args], {
14360
+ encoding: "utf8",
14361
+ stdio: ["ignore", "pipe", "pipe"],
14362
+ maxBuffer: 64 * 1024 * 1024
14363
+ });
14364
+ } catch (err) {
14365
+ const stderr = err.stderr?.trim();
14366
+ throw new EvolutionCheckError(`git ${args.join(" ")} failed${stderr ? `: ${stderr}` : ""}`);
14367
+ }
14368
+ }
14369
+ async function runEvolutionCheck(options) {
14370
+ const projectRoot = resolve(options.dir ?? process.cwd());
14371
+ const headPath = options.head ? resolve(options.head) : join(projectRoot, "module.json");
14372
+ if (!existsSync(headPath)) throw new EvolutionCheckError(`no manifest at ${headPath}`);
14373
+ const head = readJson(headPath);
14374
+ const baseFile = options.baseFile ?? (options.base && existsSync(options.base) ? options.base : void 0);
14375
+ if (baseFile) {
14376
+ const base = readJson(baseFile);
14377
+ return { ...compareManifests(base, head), base: baseFile, head: headPath, history: 0, nothingReleased: false };
14378
+ }
14379
+ if (options.base) {
14380
+ const ref = options.base;
14381
+ const base = JSON.parse(git(projectRoot, ["show", `${ref}:./module.json`]));
14382
+ let history2 = [];
14383
+ if (!options.noHistory) {
14384
+ const shas = git(projectRoot, ["log", "--format=%H", "--reverse", ref, "--", "module.json"]).split("\n").filter(Boolean);
14385
+ history2 = shas.slice(0, -1).map((sha) => JSON.parse(git(projectRoot, ["show", `${sha}:./module.json`])));
14386
+ }
14387
+ return {
14388
+ ...compareManifests(base, head, history2),
14389
+ base: `${ref}:module.json`,
14390
+ head: headPath,
14391
+ history: history2.length,
14392
+ nothingReleased: false
14393
+ };
14394
+ }
14395
+ const slug = head.module?.slug;
14396
+ if (!slug) throw new EvolutionCheckError(`${headPath} has no module.slug`);
14397
+ let mod;
14398
+ let versions;
14399
+ try {
14400
+ mod = await apiRequest(
14401
+ "GET",
14402
+ `/api/modules/by-slug/${encodeURIComponent(slug)}`
14403
+ );
14404
+ if (!mod.current_version) {
14405
+ return {
14406
+ violations: [],
14407
+ additions: [],
14408
+ warnings: [],
14409
+ base: "(nothing released)",
14410
+ head: headPath,
14411
+ history: 0,
14412
+ nothingReleased: true
14413
+ };
14414
+ }
14415
+ versions = await apiRequest("GET", `/api/modules/${mod.id}/versions`);
14416
+ } catch (err) {
14417
+ const why = err instanceof ApiError ? `${err.code || err.status} ${err.message}` : err.message;
14418
+ throw new EvolutionCheckError(
14419
+ `could not read the released manifest of ${slug} (${why}); pass --base <ref|file> to check offline`
14420
+ );
14403
14421
  }
14404
- return hint?.needsGrant ? EXIT_NEEDS_GRANT : 1;
14422
+ const released = versions.find((v) => v.version === mod.current_version);
14423
+ if (!released?.manifest_jsonb) {
14424
+ throw new EvolutionCheckError(
14425
+ `the platform returned no manifest for ${slug} v${mod.current_version}; pass --base <ref|file> to check offline`
14426
+ );
14427
+ }
14428
+ const releasedAt = released.created_on ? Date.parse(released.created_on) : Number.POSITIVE_INFINITY;
14429
+ const history = options.noHistory ? [] : versions.filter((v) => v !== released && v.manifest_jsonb && v.created_on && Date.parse(v.created_on) < releasedAt).sort((a, b) => Date.parse(a.created_on) - Date.parse(b.created_on)).map((v) => v.manifest_jsonb);
14430
+ return {
14431
+ ...compareManifests(released.manifest_jsonb, head, history),
14432
+ base: `${slug} v${mod.current_version} (released)`,
14433
+ head: headPath,
14434
+ history: history.length,
14435
+ nothingReleased: false
14436
+ };
14405
14437
  }
14406
- function isTransientPollError(err) {
14407
- if (!(err instanceof ApiError)) return true;
14408
- return err.status === 0 || err.status === 429 || err.status >= 500;
14438
+ function printEvolutionReport(r, print = out) {
14439
+ print(`module diff --check: ${r.head} against ${r.base} (${r.history} older version${r.history === 1 ? "" : "s"} scanned)`);
14440
+ for (const a of r.additions) print(` + ${a}`);
14441
+ for (const w of r.warnings) print(` ! ${w}`);
14442
+ for (const x of r.violations) print(` \u2717 ${x.rule}: ${x.where}: ${x.detail}`);
14409
14443
  }
14410
- function isTypedRequestError(err) {
14411
- if (err instanceof ApiError) return true;
14412
- if (!(err instanceof Error)) return false;
14413
- const candidate = err;
14414
- return typeof candidate.code === "string" && typeof candidate.status === "number" && (candidate.context === void 0 || candidate.context !== null && typeof candidate.context === "object" && !Array.isArray(candidate.context));
14444
+ async function moduleDiffCheckCommand(options) {
14445
+ let r;
14446
+ try {
14447
+ r = await runEvolutionCheck(options);
14448
+ } catch (err) {
14449
+ if (err instanceof EvolutionCheckError) {
14450
+ if (options.json) out(JSON.stringify({ ok: false, error: { code: "CHECK_FAILED", message: err.message } }));
14451
+ else fail(`module diff --check: ${err.message}`);
14452
+ return 2;
14453
+ }
14454
+ throw err;
14455
+ }
14456
+ if (options.json) {
14457
+ out(JSON.stringify({ ok: r.violations.length === 0, ...r }, null, 2));
14458
+ return r.violations.length === 0 ? 0 : 1;
14459
+ }
14460
+ if (r.nothingReleased) {
14461
+ success("Nothing released yet: every change is additive.");
14462
+ return 0;
14463
+ }
14464
+ printEvolutionReport(r);
14465
+ if (r.violations.length === 0) {
14466
+ success("additive");
14467
+ return 0;
14468
+ }
14469
+ fail(`${r.violations.length} violation(s): not additive. Released copies would lose data or fail to upgrade.`);
14470
+ info(`Add a new field or table instead, and stop writing the old one (${code('human_edit: "open"')}).`);
14471
+ return 1;
14415
14472
  }
14416
- function reportRefusal(err, subject, opts) {
14417
- const writeJson = (message) => {
14418
- if (opts.stderr) process.stderr.write(`${message}
14419
- `);
14420
- else out(message);
14421
- };
14422
- if (!isTypedRequestError(err)) {
14423
- const message = err?.message ?? String(err);
14424
- if (opts.json) {
14425
- writeJson(
14426
- JSON.stringify({
14427
- error: {
14428
- code: "CLI_ERROR",
14429
- message: message.replace(ANSI, ""),
14430
- status: null,
14431
- context: null
14432
- },
14433
- hint: null,
14434
- needs_grant: false
14435
- })
14436
- );
14437
- } else {
14438
- fail(`${opts.action} failed: ${message}`);
14473
+ async function publishEvolutionGate(projectRoot, strict) {
14474
+ let r;
14475
+ try {
14476
+ r = await runEvolutionCheck({ dir: projectRoot });
14477
+ } catch (err) {
14478
+ const msg = err instanceof Error ? err.message : String(err);
14479
+ if (strict) {
14480
+ fail(`Evolution check could not run (${msg}). Refusing with --strict. Nothing was published.`);
14481
+ return 1;
14439
14482
  }
14440
- return 1;
14483
+ warn(`Evolution check skipped: ${msg}.`);
14484
+ return 0;
14441
14485
  }
14442
- const hint = err.status === 401 && err.context?.reason !== "module_api_credential_missing" ? {
14443
- message: opts.moduleKey ? MODULE_KEY_REFUSED[opts.moduleKey] : "Your login has expired. Run: dashwise login",
14444
- needsGrant: false
14445
- } : refusalHint(err.code, err.context, subject);
14446
- if (opts.json) {
14447
- writeJson(
14448
- JSON.stringify({
14449
- error: {
14450
- code: err.code,
14451
- message: err.message,
14452
- status: err.status,
14453
- context: err.context ?? null
14454
- },
14455
- hint: hint?.message ?? null,
14456
- needs_grant: hint?.needsGrant ?? false
14457
- })
14458
- );
14459
- } else if (hint) {
14460
- fail(
14461
- `${hint.message} (${err.code})${err.context ? ` context=${JSON.stringify(err.context)}` : ""}`
14462
- );
14463
- } else {
14486
+ if (r.nothingReleased || r.violations.length === 0) return 0;
14487
+ printEvolutionReport(r, (s) => (strict ? fail : warn)(s));
14488
+ if (strict) {
14464
14489
  fail(
14465
- `${opts.action} failed (${err.code}): ${err.message}${err.context ? ` context=${JSON.stringify(err.context)}` : ""}`
14490
+ `module.json is not an additive evolution of the released version (${r.violations.length} violation(s)). Refusing with --strict. Nothing was published.`
14466
14491
  );
14492
+ return 1;
14467
14493
  }
14468
- return hint?.needsGrant ? EXIT_NEEDS_GRANT : 1;
14494
+ warn(
14495
+ `module.json is not an additive evolution of the released version: installs that upgrade lose the data above. ${code("--strict")} turns this into a refusal; ${code("dashwise module diff --check")} shows it on its own.`
14496
+ );
14497
+ return 0;
14469
14498
  }
14470
- var EXIT_NEEDS_GRANT, EXIT_TIMED_OUT, REBUILD_BUMP_OPERATOR_ONLY, FEATURE_SUBJECTS, OPTION_SUBJECTS, MODULE_RIGHT_CODES, ANSI, MODULE_KEY_REFUSED;
14471
- var init_permission_hint = __esm({
14472
- "src/lib/permission-hint.ts"() {
14499
+ var EvolutionCheckError;
14500
+ var init_diff_check = __esm({
14501
+ "src/commands/module/diff-check.ts"() {
14473
14502
  init_api_client();
14503
+ init_manifest_evolution();
14474
14504
  init_output();
14475
- EXIT_NEEDS_GRANT = 3;
14476
- EXIT_TIMED_OUT = 4;
14477
- REBUILD_BUMP_OPERATOR_ONLY = "REBUILD_BUMP_OPERATOR_ONLY";
14478
- FEATURE_SUBJECTS = {
14479
- module_runtime_streaming: "Streaming",
14480
- module_rebuild: "Rebuild",
14481
- module_building: "Building modules",
14482
- module_runtime_sizing: "Memory size"
14483
- };
14484
- OPTION_SUBJECTS = {
14485
- streaming: "Streaming",
14486
- memory_mb: "Memory size",
14487
- reapply: "Re-applying the runtime",
14488
- timeout: "The timeout",
14489
- reserved_concurrency: "Reserved concurrency",
14490
- log_retention: "Log retention"
14491
- };
14492
- MODULE_RIGHT_CODES = /* @__PURE__ */ new Set([
14493
- "MODULE_CAPABILITY_DENIED",
14494
- "PUBLISH_REQUIRES_ADMIN",
14495
- "NOT_COPY_OWNER"
14496
- ]);
14497
- ANSI = /\x1b\[[0-9;]*m/g;
14498
- MODULE_KEY_REFUSED = {
14499
- env: "The module key in DASHWISE_API_KEY was refused (expired or revoked). Replace it with a current key, or unset it to use this module's saved key. dashwise login does not renew module keys.",
14500
- env_file: "The module key in .env.local (DASHWISE_API_KEY) was refused (expired or revoked). Replace it, or remove that line to use this module's saved key. dashwise login does not renew module keys.",
14501
- saved: `This module's saved key was refused (expired or revoked). In your CLI auth.json, set only modules[<slug>].apiKey to an empty string (""), keeping installationId and all other fields. Then run dashwise dev from this module's directory with DASHWISE_API_KEY unset to mint and save a replacement key for the same installation. dashwise login does not renew module keys.`
14505
+ EvolutionCheckError = class extends Error {
14502
14506
  };
14503
14507
  }
14504
14508
  });
@@ -19243,6 +19247,150 @@ function agentSessionMarker(env = process.env) {
19243
19247
  return null;
19244
19248
  }
19245
19249
 
19250
+ // src/lib/go-live.ts
19251
+ init_api_client();
19252
+ init_output();
19253
+ init_permission_hint();
19254
+ init_scaffold();
19255
+ var EXIT_AWAITING_PERSON = 5;
19256
+ var DEFAULT_GO_LIVE_WAIT_SEC = 900;
19257
+ var AGENT_GO_LIVE_WAIT_SEC = 240;
19258
+ function isGoLiveNeedsPerson(err) {
19259
+ return err instanceof ApiError && err.code === "GO_LIVE_NEEDS_PERSON";
19260
+ }
19261
+ function liveTabLink(apiUrl, moduleId, installationId) {
19262
+ const path = `/modules?module=${moduleId}&tab=live&copy=${installationId}`;
19263
+ const origin = hostedAppOrigin(apiUrl);
19264
+ return origin ? `${origin}${path}` : path;
19265
+ }
19266
+ async function proposeGoLive(args, auth) {
19267
+ const emit = (exit2, request2, extra = {}) => {
19268
+ if (args.json)
19269
+ out(
19270
+ JSON.stringify({
19271
+ go_live_request_id: request2?.id ?? null,
19272
+ installation_id: args.installationId,
19273
+ version: args.version,
19274
+ status: request2?.status ?? null,
19275
+ outcome: request2?.outcome ?? null,
19276
+ reason: request2?.reason ?? null,
19277
+ approve_at: liveTabLink(args.apiUrl, args.moduleId, args.installationId),
19278
+ awaiting_person: exit2 === EXIT_AWAITING_PERSON,
19279
+ ...extra
19280
+ })
19281
+ );
19282
+ return exit2;
19283
+ };
19284
+ let filed;
19285
+ try {
19286
+ filed = await apiRequest(
19287
+ "POST",
19288
+ `/api/modules/${args.moduleId}/go-live-requests`,
19289
+ { installation_id: args.installationId, version: args.version },
19290
+ auth
19291
+ );
19292
+ } catch (err) {
19293
+ if (err instanceof ApiError && err.code === "ALREADY_LIVE") {
19294
+ success(`Installation ${args.installationId} already runs v${args.version}.`);
19295
+ return emit(0, null, { already_live: true });
19296
+ }
19297
+ const why = err instanceof ApiError ? `${err.code || err.status} ${err.message}` : err.message;
19298
+ fail(`Could not propose v${args.version} for installation ${args.installationId}: ${why}`);
19299
+ return emit(1, null, { error: err instanceof ApiError ? err.code || err.status : "NETWORK" });
19300
+ }
19301
+ const link = liveTabLink(args.apiUrl, args.moduleId, args.installationId);
19302
+ info(
19303
+ `Proposed v${args.version} for installation ${args.installationId}` + (filed.from_version ? ` (runs v${filed.from_version} now)` : "") + `. A person takes it live in Dashwise: ${code(link)}`
19304
+ );
19305
+ if (args.waitSec <= 0) {
19306
+ info(awaitingLine(filed.request.id, args.checkCommand));
19307
+ return emit(EXIT_AWAITING_PERSON, filed.request);
19308
+ }
19309
+ const { exit, request } = await waitForGoLive(filed.request.id, args, auth);
19310
+ return emit(exit, request ?? filed.request);
19311
+ }
19312
+ function awaitingLine(requestId, checkCommand) {
19313
+ return `Go-live request #${requestId} waits for a person; nothing was installed. ${checkAgain(checkCommand)}`;
19314
+ }
19315
+ function checkAgain(checkCommand) {
19316
+ return checkCommand ? `Check again with ${code(checkCommand)} (it files nothing new while this one is open).` : "Run the same command again to check (it files nothing new while this one is open).";
19317
+ }
19318
+ function isFinalClientError(err) {
19319
+ return err instanceof ApiError && err.status >= 400 && err.status < 500 && err.status !== 429;
19320
+ }
19321
+ async function waitForGoLive(requestId, args, auth) {
19322
+ const interval = args.pollIntervalMs ?? 5e3;
19323
+ const deadline = Date.now() + args.waitSec * 1e3;
19324
+ info(`Waiting for the approval (go-live request #${requestId}, up to ${formatWait(args.waitSec)})\u2026`);
19325
+ let last = null;
19326
+ let announcedApproval = false;
19327
+ for (; ; ) {
19328
+ let request = null;
19329
+ try {
19330
+ ({ request } = await apiRequest(
19331
+ "GET",
19332
+ `/api/modules/${args.moduleId}/go-live-requests/${requestId}`,
19333
+ void 0,
19334
+ auth
19335
+ ));
19336
+ last = request;
19337
+ } catch (err) {
19338
+ const why = err instanceof ApiError ? `${err.code || err.status}` : err.message;
19339
+ if (isFinalClientError(err)) {
19340
+ fail(`Could not read go-live request #${requestId} (${why}). Check it on the module's Live tab.`);
19341
+ return { exit: 1, request: last };
19342
+ }
19343
+ warn(`Could not read go-live request #${requestId} (${why}); trying again.`);
19344
+ }
19345
+ if (request && request.status !== "pending") {
19346
+ if (request.status !== "approved" || request.outcome !== null) {
19347
+ return { exit: reportOutcome(request, args), request };
19348
+ }
19349
+ if (!announcedApproval) {
19350
+ info(`A person approved it; v${args.version} is going live on installation ${args.installationId}\u2026`);
19351
+ announcedApproval = true;
19352
+ }
19353
+ }
19354
+ if (Date.now() + interval > deadline) {
19355
+ warn(
19356
+ announcedApproval ? `Go-live request #${requestId} was approved and is still going live. ${checkAgain(args.checkCommand)}` : awaitingLine(requestId, args.checkCommand)
19357
+ );
19358
+ return { exit: EXIT_AWAITING_PERSON, request: last };
19359
+ }
19360
+ await new Promise((r) => setTimeout(r, interval));
19361
+ }
19362
+ }
19363
+ function formatWait(sec) {
19364
+ return sec < 120 ? `${sec} s` : `${Math.round(sec / 60)} min`;
19365
+ }
19366
+ function parseWaitSeconds(value) {
19367
+ const trimmed = value.trim();
19368
+ if (!/^[0-9]+$/.test(trimmed)) {
19369
+ throw new InvalidArgumentError("Expected whole seconds, 0 or more.");
19370
+ }
19371
+ return Number(trimmed);
19372
+ }
19373
+ function reportOutcome(request, args) {
19374
+ switch (request.outcome) {
19375
+ case "live":
19376
+ success(`v${args.version} is live on installation ${args.installationId}.`);
19377
+ return 0;
19378
+ case "declined":
19379
+ fail(
19380
+ `A person declined v${args.version} for installation ${args.installationId}` + (request.reason ? `: ${request.reason}` : ".")
19381
+ );
19382
+ return EXIT_NEEDS_GRANT;
19383
+ case "superseded":
19384
+ warn("A newer proposal for this installation replaced this one.");
19385
+ return EXIT_NEEDS_GRANT;
19386
+ default:
19387
+ fail(
19388
+ `The update did not go through after the approval (${request.reason ?? request.outcome ?? request.status}). Nothing changed; propose again.`
19389
+ );
19390
+ return 1;
19391
+ }
19392
+ }
19393
+
19246
19394
  // src/lib/drift-preview.ts
19247
19395
  init_api_client();
19248
19396
  function redactedConsumer(redacted) {
@@ -19578,6 +19726,17 @@ async function moduleInstallCommand(slugArg, options) {
19578
19726
  );
19579
19727
  }
19580
19728
  }
19729
+ const agentMarker = agentSessionMarker();
19730
+ if (agentMarker !== null) {
19731
+ if (!existing) {
19732
+ fail(
19733
+ `This is an AI agent session (${code(agentMarker)} is set), and ${code(mod.slug)} has no production installation in ${code(workspace2.slug)} yet: a person creates the first one in Dashwise.`
19734
+ );
19735
+ return 1;
19736
+ }
19737
+ info(`AI agent session (${code(agentMarker)}): proposing instead of installing; a person approves it.`);
19738
+ return goLiveByProposal(mod, existing.id, targetVersion, options, auth);
19739
+ }
19581
19740
  let preview;
19582
19741
  let lossy = false;
19583
19742
  let lossAccepted = false;
@@ -19639,6 +19798,25 @@ async function moduleInstallCommand(slugArg, options) {
19639
19798
  );
19640
19799
  break;
19641
19800
  } catch (err) {
19801
+ if (isGoLiveNeedsPerson(err)) {
19802
+ if (!existing) {
19803
+ fail(
19804
+ `Taking ${code(mod.slug)} live in ${code(workspace2.slug)} is done by a person in Dashwise: create its first installation there.`
19805
+ );
19806
+ if (devInstallReplaced) printDevInstallAftermath(mod.slug, workspace2.slug);
19807
+ return 1;
19808
+ }
19809
+ info("Dashwise takes modules live through a person: proposing this update instead.");
19810
+ const proposed = await goLiveByProposal(
19811
+ mod,
19812
+ existing.id,
19813
+ confirmedPlan?.to_version ?? targetVersion,
19814
+ options,
19815
+ auth
19816
+ );
19817
+ if (devInstallReplaced) printDevInstallAftermath(mod.slug, workspace2.slug);
19818
+ return proposed;
19819
+ }
19642
19820
  let plan = null;
19643
19821
  if (confirmedPlan && err instanceof ApiError && err.code === "DRIFT_CHANGED" && round < MAX_PLAN_ROUNDS) {
19644
19822
  warn(
@@ -20094,6 +20272,22 @@ function rerunLine(slug, workspaceSlug, targetVersion, installationId, options,
20094
20272
  `--accept-data-loss ${hash}`
20095
20273
  ].join(" ");
20096
20274
  }
20275
+ function goLiveByProposal(mod, installationId, version, options, auth) {
20276
+ return proposeGoLive(
20277
+ {
20278
+ moduleId: mod.id,
20279
+ moduleSlug: mod.slug,
20280
+ installationId,
20281
+ version,
20282
+ apiUrl: auth.baseUrl,
20283
+ waitSec: options.waitSec ?? (agentSessionMarker() !== null ? AGENT_GO_LIVE_WAIT_SEC : DEFAULT_GO_LIVE_WAIT_SEC),
20284
+ ...options.pollIntervalMs !== void 0 ? { pollIntervalMs: options.pollIntervalMs } : {},
20285
+ ...options.json ? { json: true } : {},
20286
+ ...options.goLiveCheckCommand ? { checkCommand: options.goLiveCheckCommand } : {}
20287
+ },
20288
+ auth
20289
+ );
20290
+ }
20097
20291
 
20098
20292
  // src/commands/module/list.ts
20099
20293
  init_api_client();
@@ -21106,10 +21300,9 @@ init_permission_hint();
21106
21300
  async function moduleShipCommand(slugArg, options) {
21107
21301
  const marker = agentSessionMarker();
21108
21302
  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.`
21303
+ info(
21304
+ `AI agent session (${code(marker)}): \`ship\` releases, then proposes the update; a person takes it live in Dashwise.`
21111
21305
  );
21112
- return 2;
21113
21306
  }
21114
21307
  if (options.build !== void 0 && options.commitSha !== void 0) {
21115
21308
  fail("--build resumes an existing release; do not combine it with --commit-sha.");
@@ -21150,15 +21343,30 @@ async function moduleShipCommand(slugArg, options) {
21150
21343
  return 1;
21151
21344
  }
21152
21345
  info(`Installing v${build.version} (build #${build.id}, revision ${build.commit_sha.slice(0, 12)}).`);
21153
- return moduleInstallCommand(slugArg, {
21346
+ const checkCommand = [
21347
+ "dashwise module install",
21348
+ slugArg,
21349
+ `--version ${build.version}`,
21350
+ options.dir !== void 0 ? `--dir ${shellWord(options.dir)}` : void 0,
21351
+ options.workspace !== void 0 ? `--workspace ${shellWord(options.workspace)}` : void 0,
21352
+ options.installation !== void 0 ? `--installation ${options.installation}` : void 0
21353
+ ].filter(Boolean).join(" ");
21354
+ const installExit = await moduleInstallCommand(slugArg, {
21154
21355
  ...options.dir !== void 0 ? { dir: options.dir } : {},
21155
21356
  ...options.workspace !== void 0 ? { workspace: options.workspace } : {},
21156
21357
  ...options.installation !== void 0 ? { installation: options.installation } : {},
21157
21358
  ...options.keepDevInstall !== void 0 ? { keepDevInstall: options.keepDevInstall } : {},
21158
21359
  ...options.replaceDevInstall !== void 0 ? { replaceDevInstall: options.replaceDevInstall } : {},
21159
21360
  ...options.yes !== void 0 ? { yes: options.yes } : {},
21361
+ ...options.waitSec !== void 0 ? { waitSec: options.waitSec } : {},
21362
+ ...options.pollIntervalMs !== void 0 ? { pollIntervalMs: options.pollIntervalMs } : {},
21363
+ goLiveCheckCommand: checkCommand,
21160
21364
  version: build.version
21161
21365
  });
21366
+ if (installExit === EXIT_AWAITING_PERSON) {
21367
+ info(`v${build.version} is released; only its go-live waits for a person (not a build: do not ship again).`);
21368
+ }
21369
+ return installExit;
21162
21370
  }
21163
21371
  async function defaultBranchHead(slugArg, dir) {
21164
21372
  const cfg = readConfig();
@@ -21192,6 +21400,9 @@ async function defaultBranchHead(slugArg, dir) {
21192
21400
  return void 0;
21193
21401
  }
21194
21402
  }
21403
+ function shellWord(value) {
21404
+ return /^[\w./@:=-]+$/.test(value) ? value : `'${value.replace(/'/g, `'\\''`)}'`;
21405
+ }
21195
21406
 
21196
21407
  // src/commands/module/status.ts
21197
21408
  init_api_client();
@@ -27156,8 +27367,12 @@ moduleCmd.command("build-log [slug] [buildId]").description(
27156
27367
  }
27157
27368
  );
27158
27369
  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(
27370
+ "Release and install in one command: 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. Inside an AI agent session (or when Dashwise requires it) it proposes the update instead and waits for a person to take it live on the Live tab."
27371
+ ).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).option(
27372
+ "--wait <seconds>",
27373
+ "Max seconds to wait for a person to approve a go-live proposal (default 900, 240 inside an AI agent session; 0 proposes and returns). Exit 5: the proposal still waits; run the same command again to check (exit 4 is a release still building).",
27374
+ parseWaitSeconds
27375
+ ).action(
27161
27376
  async (slug, opts) => {
27162
27377
  process.exitCode = await moduleShipCommand(slug, {
27163
27378
  ...opts.dir !== void 0 ? { dir: opts.dir } : {},
@@ -27169,7 +27384,8 @@ moduleCmd.command("ship [slug]").description(
27169
27384
  ...opts.keepDevInstall !== void 0 ? { keepDevInstall: opts.keepDevInstall } : {},
27170
27385
  ...opts.replaceDevInstall !== void 0 ? { replaceDevInstall: opts.replaceDevInstall } : {},
27171
27386
  ...opts.yes !== void 0 ? { yes: opts.yes } : {},
27172
- ...opts.timeout !== void 0 ? { timeoutSec: opts.timeout } : {}
27387
+ ...opts.timeout !== void 0 ? { timeoutSec: opts.timeout } : {},
27388
+ ...opts.wait !== void 0 ? { waitSec: opts.wait } : {}
27173
27389
  });
27174
27390
  }
27175
27391
  );
@@ -27230,7 +27446,11 @@ moduleCmd.command("install [slug]").alias("deploy").alias("upgrade").description
27230
27446
  ).option("--dir <path>", "Project root for the module.json slug lookup (default: current directory).").option("-y, --yes", "Skip the confirmation prompt (for CI / scripted use). Never accepts data loss: see --accept-data-loss.").option(
27231
27447
  "--accept-data-loss <drift_hash>",
27232
27448
  "Accept the data loss of this upgrade: the drift_hash its preview printed. Only that exact hash accepts it (a person decides; an agent stops and reports the hash)."
27233
- ).option("--json", "Emit the final result as machine-readable JSON.").action(
27449
+ ).option("--json", "Emit the final result as machine-readable JSON.").option(
27450
+ "--wait <seconds>",
27451
+ "Max seconds to wait for a person to approve a go-live proposal (made inside an AI agent session or when Dashwise requires a person; default 900, 240 inside an AI agent session; 0 proposes and returns). Exit 5: the proposal still waits; run the same command again to check, it files nothing new.",
27452
+ parseWaitSeconds
27453
+ ).action(
27234
27454
  async (slug, cmdOpts) => {
27235
27455
  process.exitCode = await moduleInstallCommand(slug, {
27236
27456
  ...cmdOpts.installation !== void 0 ? { installation: cmdOpts.installation } : {},
@@ -27242,7 +27462,8 @@ moduleCmd.command("install [slug]").alias("deploy").alias("upgrade").description
27242
27462
  ...cmdOpts.dir !== void 0 ? { dir: cmdOpts.dir } : {},
27243
27463
  ...cmdOpts.yes !== void 0 ? { yes: cmdOpts.yes } : {},
27244
27464
  ...cmdOpts.acceptDataLoss !== void 0 ? { acceptDataLoss: cmdOpts.acceptDataLoss } : {},
27245
- ...cmdOpts.json !== void 0 ? { json: cmdOpts.json } : {}
27465
+ ...cmdOpts.json !== void 0 ? { json: cmdOpts.json } : {},
27466
+ ...cmdOpts.wait !== void 0 ? { waitSec: cmdOpts.wait } : {}
27246
27467
  });
27247
27468
  }
27248
27469
  );
@@ -27596,7 +27817,7 @@ fieldCmd.command("set-type <table-slug> <field-slug> <new-type>").description(
27596
27817
  }
27597
27818
  );
27598
27819
  function getVersion() {
27599
- return "0.30.1";
27820
+ return "0.31.0";
27600
27821
  }
27601
27822
  function parsePositiveIdOption(value) {
27602
27823
  if (!/^[1-9][0-9]*$/.test(value.trim())) {