@datarails-ai/dr-appspace-cli 0.12.11 → 0.12.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/cli.js +185 -37
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -13,7 +13,7 @@ import { fileURLToPath as fileURLToPath6 } from "url";
13
13
  // package.json
14
14
  var package_default = {
15
15
  name: "@datarails-ai/dr-appspace-cli",
16
- version: "0.12.11",
16
+ version: "0.12.13",
17
17
  description: "Datarails App Space CLI \u2014 build, try, push and publish App Space apps",
18
18
  type: "module",
19
19
  engines: {
@@ -57,7 +57,7 @@ var package_default = {
57
57
  };
58
58
 
59
59
  // src/buildInfo.ts
60
- var BUILD_TIME = "2026-10-14T19:55:58.887Z";
60
+ var BUILD_TIME = "2026-10-15T06:27:08.313Z";
61
61
  function formatBuildTime(iso) {
62
62
  if (iso === "") return "";
63
63
  const d = new Date(iso);
@@ -6435,7 +6435,7 @@ function endOfRegex(text3, start) {
6435
6435
  // ../flows/insights-builder/kit/contract/extract.mjs
6436
6436
  var CONTRACT_V = 1;
6437
6437
  var GENERATED_BY = "dashboard-kit/contract@1";
6438
- var KIT_REGIONS = /* @__PURE__ */ new Set(["header", "asof", "refresh", "nav", "filters", "controls", "band"]);
6438
+ var KIT_REGIONS = /* @__PURE__ */ new Set(["header", "asof", "refresh", "theme", "nav", "filters", "controls", "band"]);
6439
6439
  var DASHBOARD_KIND = "dashboard";
6440
6440
  var BAND_ROLE = "band";
6441
6441
  var GRID_CLASSES = /* @__PURE__ */ new Set(["dr-dash", "dr-grid"]);
@@ -16373,18 +16373,31 @@ async function writePackage(dir, appId, pkg) {
16373
16373
  ) + "\n"
16374
16374
  );
16375
16375
  }
16376
- async function writeStoredContract(client, appId, dir) {
16376
+ async function readStoredContract(client, appId, codeVersion) {
16377
+ const app = `/apps/${encodeURIComponent(appId)}`;
16378
+ const path3 = typeof codeVersion === "number" ? `${app}/versions/${String(codeVersion)}/contract` : `${app}/contract`;
16377
16379
  let stored;
16378
16380
  try {
16379
- stored = await client.request(`/apps/${encodeURIComponent(appId)}/contract`);
16380
- } catch {
16381
- return false;
16381
+ stored = await client.request(path3);
16382
+ } catch (err) {
16383
+ if (err instanceof AppSpaceHttpError) {
16384
+ const detail = err.body?.detail;
16385
+ return { missing: `${String(err.status)}${typeof detail === "string" ? ` ${detail}` : ""}` };
16386
+ }
16387
+ return { missing: err instanceof Error ? err.message.split("\n")[0] ?? "" : String(err) };
16388
+ }
16389
+ if (typeof stored !== "object" || stored === null || Array.isArray(stored)) {
16390
+ return { missing: "the contract read answered no document" };
16382
16391
  }
16383
- if (typeof stored !== "object" || stored === null || Array.isArray(stored)) return false;
16384
16392
  const held = stored["contract"];
16385
16393
  const document = typeof held === "object" && held !== null && !Array.isArray(held) ? held : stored;
16386
- if (!("v" in document)) return false;
16387
- await writeFile5(join10(dir, "contract.json"), JSON.stringify(document, null, 2) + "\n");
16394
+ if (!("v" in document)) return { missing: "the contract read answered no document" };
16395
+ return { document };
16396
+ }
16397
+ async function writeStoredContract(client, appId, dir, codeVersion) {
16398
+ const read = await readStoredContract(client, appId, codeVersion);
16399
+ if (!("document" in read)) return false;
16400
+ await writeFile5(join10(dir, "contract.json"), JSON.stringify(read.document, null, 2) + "\n");
16388
16401
  return true;
16389
16402
  }
16390
16403
  async function writeOwnedFiles(dir, document, { force = false } = {}) {
@@ -16443,20 +16456,52 @@ async function restoreOwnedFiles(dir, contractWritten, { force }) {
16443
16456
  ];
16444
16457
  return writeOwnedFiles(dir, { owned }, { force });
16445
16458
  }
16446
- async function pullInto(client, appId, dir) {
16447
- let pkg;
16448
- try {
16449
- pkg = await client.request(`/apps/${encodeURIComponent(appId)}/package`);
16450
- } catch (err) {
16451
- if (err instanceof AppSpaceHttpError && err.status === 404) return null;
16452
- throw err;
16459
+ var contractWait = { pulls: 7, baseMs: 250, capMs: 4e3 };
16460
+ function contractWaitMs(pull) {
16461
+ return Math.min(contractWait.capMs, contractWait.baseMs * 2 ** (pull - 1));
16462
+ }
16463
+ var LiveContractMissing = class extends Error {
16464
+ constructor(appId, revision, pulls, last) {
16465
+ super(
16466
+ `${appId} is live at revision ${String(revision)}, and its contract could not be read in ${String(pulls)} pulls (the last answered: ${last}). Without it this landing cannot see the cards already live: it would build the app again from the skeleton and replace every one of them. Nothing was landed. Run the same command again. If it fails the same way, the live version has no contract on record: \`app-space get ${appId} --dir <a new folder>\` and then \`app-space push <that folder>\` push the same package back with one, and the next landing builds on it.`
16467
+ );
16468
+ this.appId = appId;
16469
+ this.revision = revision;
16470
+ this.pulls = pulls;
16471
+ this.last = last;
16472
+ this.name = "LiveContractMissing";
16473
+ }
16474
+ appId;
16475
+ revision;
16476
+ pulls;
16477
+ last;
16478
+ };
16479
+ async function pullInto(client, appId, dir, {
16480
+ onWait,
16481
+ sleep: sleep2 = (ms) => new Promise((done) => setTimeout(done, ms))
16482
+ } = {}) {
16483
+ for (let pull = 1; ; pull += 1) {
16484
+ let pkg;
16485
+ try {
16486
+ pkg = await client.request(`/apps/${encodeURIComponent(appId)}/package`);
16487
+ } catch (err) {
16488
+ if (err instanceof AppSpaceHttpError && err.status === 404) return null;
16489
+ throw err;
16490
+ }
16491
+ await rm2(join10(dir, "contract.json"), { force: true });
16492
+ await writePackage(dir, appId, pkg);
16493
+ const read = await readStoredContract(client, appId, pkg.code_version);
16494
+ if ("document" in read) {
16495
+ await writeFile5(join10(dir, "contract.json"), JSON.stringify(read.document, null, 2) + "\n");
16496
+ await restoreOwnedFiles(dir, true, { force: true });
16497
+ await recordPulled(dir, true);
16498
+ return { revision: pkg.revision, stage: pkg.stage };
16499
+ }
16500
+ if (pull >= contractWait.pulls) throw new LiveContractMissing(appId, pkg.revision, pull, read.missing);
16501
+ const ms = contractWaitMs(pull);
16502
+ onWait?.(pull, ms, read.missing);
16503
+ await sleep2(ms);
16453
16504
  }
16454
- await rm2(join10(dir, "contract.json"), { force: true });
16455
- await writePackage(dir, appId, pkg);
16456
- const contract = await writeStoredContract(client, appId, dir);
16457
- await restoreOwnedFiles(dir, contract, { force: true });
16458
- await recordPulled(dir, contract);
16459
- return { revision: pkg.revision, stage: pkg.stage };
16460
16505
  }
16461
16506
  async function runGet(appId, opts) {
16462
16507
  const client = await connect(opts);
@@ -16473,7 +16518,7 @@ async function runGet(appId, opts) {
16473
16518
  return 1;
16474
16519
  }
16475
16520
  await writePackage(dir, appId, pkg);
16476
- const contract = await writeStoredContract(client, appId, dir);
16521
+ const contract = await writeStoredContract(client, appId, dir, pkg.code_version);
16477
16522
  const restored = await restoreOwnedFiles(dir, contract, { force: opts.force === true });
16478
16523
  await recordPulled(dir, contract);
16479
16524
  if (opts.json === true) {
@@ -17261,7 +17306,7 @@ var contract_schema_default = {
17261
17306
  properties: {
17262
17307
  file: { type: "string" },
17263
17308
  tokens: { type: "object", additionalProperties: { type: "string" } },
17264
- preset: { type: "string", enum: ["default", "slate", "warm", "contrast", "presentation"], description: "OPTIONAL. A named look from the pinned ui-kit (presentation is refused: ui-kit/v4 shipped without it); absent means default. The compiler fills pages[].theme/density where a page states none from this and app.theme.density, and drops both keys from the filled contract it compares (mirrors fillHeaders)." },
17309
+ preset: { type: "string", enum: ["default", "slate", "warm", "contrast", "presentation"], description: "OPTIONAL. A named look from the pinned ui-kit (presentation is refused: ui-kit/v4 shipped without it); absent means default. A dashboard is built light: slate composes as default, because dark is the viewer's Dark mode switch (the theme region). The compiler fills pages[].theme/density where a page states none from this and app.theme.density, and drops both keys from the filled contract it compares (mirrors fillHeaders)." },
17265
17310
  density: { type: "string", enum: ["comfortable", "compact"], description: "OPTIONAL. app.theme.density; absent means comfortable. Distinct from pages[].density, which is the compiled data-dr-density string." }
17266
17311
  }
17267
17312
  }
@@ -17277,7 +17322,7 @@ var contract_schema_default = {
17277
17322
  module: { type: "string", description: "dr-widgets, dr-insights, dr-fmt, echarts, ui-kit" },
17278
17323
  version: { type: "string", description: "1.1.1 for a vendor module, v3 for the ui-kit" },
17279
17324
  file: { type: "string" },
17280
- path: { type: "string", description: "the platform-root path, e.g. vendor/dr-widgets@1.2.1/widgets.js" },
17325
+ path: { type: "string", description: "the platform-root path, e.g. vendor/dr-widgets@1.2.3/widgets.js" },
17281
17326
  rel: { enum: ["script", "stylesheet"] },
17282
17327
  pages: { type: "array", items: { type: "string" } },
17283
17328
  order: { type: "integer", minimum: 0, description: "load order: where this module is first reached across the bundle" }
@@ -17326,7 +17371,7 @@ var contract_schema_default = {
17326
17371
  }
17327
17372
  },
17328
17373
  scripts: { type: "array", items: { type: "string" }, description: "the bundle scripts this page loads" },
17329
- theme: { type: ["string", "null"], description: "data-dr-theme: default, slate, warm, contrast" },
17374
+ theme: { type: ["string", "null"], description: "data-dr-theme: default, warm, contrast. A dashboard is built light, so slate composes as default (dark is the viewer's Dark mode switch); a page read back from a dashboard built before that may still say slate." },
17330
17375
  density: { type: ["string", "null"], description: "data-dr-density" },
17331
17376
  mount: {
17332
17377
  oneOf: [
@@ -17446,7 +17491,7 @@ var contract_schema_default = {
17446
17491
  enum: ["region", "grid", "widget", "control", "nav", "tab", "slot", "form", "list", "detail", "queue", "action", "filter", "stat", "chart", "custom", "field", "number_input", "tabs", "tab_panel", "select", "date_picker", "data_grid", "dialog", "variance_table", "toggle", "control_group", "callout", "scenario_columns", "slider", "button", "status", "badge", "tag", "empty_state", "stepper", "alert", "checkbox", "radio_group", "file_upload", "skeleton", "progress", "accordion", "tooltip_info", "filter_bar", "filter_chip", "date_range", "fiscal_month", "search_input", "split_pane", "file_viewer", "line_grid", "document", "panel"],
17447
17492
  description: "the dashboard vocabulary (region, grid, widget, control, nav, tab, slot) and the App Builder's component vocabulary (form, list, detail, queue, action, filter, the stat tile that reads one measure out of a summary handler, the chart that draws the rows one list, inbox or custom handler answers, the custom component a module under ui/components draws, and the field that sits inside one). widget is shared: on an app page it is a dashboard card too, drawn by dr-app-widgets in the kind data-dr-kind names, over the one widget payload the handler data-dr-source names answers"
17448
17493
  },
17449
- role: { type: ["string", "null"], description: "a region's data-dr-kit (header, asof, refresh, nav, filters, controls), a widget's kind, a control's kind, a slot's name. Null for a component: its kind IS its type, and the role it is meant for is props.role." },
17494
+ role: { type: ["string", "null"], description: "a region's data-dr-kit (header, asof, refresh, theme, nav, filters, controls), a widget's kind, a control's kind, a slot's name. Null for a component: its kind IS its type, and the role it is meant for is props.role." },
17450
17495
  id: { type: ["string", "null"] },
17451
17496
  tag: { type: ["string", "null"], description: "null for an element read out of a built bundle, which no longer says what it renders" },
17452
17497
  classes: { type: "array", items: { type: "string" } },
@@ -19808,6 +19853,9 @@ import { fileURLToPath as fileURLToPath2 } from "url";
19808
19853
  var THEME_FILE3 = "ui/theme.css";
19809
19854
  var PRESETS2 = ["default", "slate", "warm", "contrast"];
19810
19855
  var DEFAULT_PRESET = "default";
19856
+ var DARK_PRESET = "slate";
19857
+ var CANVAS_TOKENS = /* @__PURE__ */ new Set(["--dr-surface", "--dr-page", "--dr-border"]);
19858
+ var DARK_SURFACE_BELOW = 0.18;
19811
19859
  var SPAN_BY_KIND = {
19812
19860
  kpi: 3,
19813
19861
  line: 6,
@@ -19890,13 +19938,21 @@ function composeLayout(contract) {
19890
19938
  const app = isPlainObject7(out.app) ? out.app : out.app = {};
19891
19939
  const pages = Array.isArray(out.pages) ? out.pages : out.pages = [];
19892
19940
  const elements = isPlainObject7(out.elements) ? out.elements : out.elements = {};
19893
- const preset = presetOf(app, pages);
19941
+ const asked = presetOf(app, pages);
19942
+ const preset = asked === DARK_PRESET ? DEFAULT_PRESET : asked;
19894
19943
  if (isPlainObject7(app.theme) && "preset" in app.theme) delete app.theme.preset;
19895
19944
  for (const page of pages) {
19896
19945
  const path3 = page.path ?? "ui/index.html";
19897
19946
  if (page.theme === void 0 || page.theme === null) {
19898
19947
  page.theme = preset;
19899
19948
  record2("page.theme", path3, `${path3} opens on the "${preset}" theme`);
19949
+ } else if (page.theme === DARK_PRESET) {
19950
+ page.theme = DEFAULT_PRESET;
19951
+ record2(
19952
+ "page.theme",
19953
+ path3,
19954
+ `${path3} opens on the "${DEFAULT_PRESET}" theme: "${DARK_PRESET}" is the viewer's Dark mode switch, not a look a dashboard is built on`
19955
+ );
19900
19956
  }
19901
19957
  if (page.density === void 0) page.density = null;
19902
19958
  if (page.title === void 0 || page.title === null) {
@@ -19971,6 +20027,21 @@ function composeTheme(app, preset, record2) {
19971
20027
  const table = THEME_TOKENS[preset] ?? THEME_TOKENS[DEFAULT_PRESET];
19972
20028
  const theme = isPlainObject7(app.theme) ? app.theme : {};
19973
20029
  const tokens = isPlainObject7(theme.tokens) ? { ...theme.tokens } : {};
20030
+ const dark = THEME_TOKENS[DARK_PRESET];
20031
+ if (isDarkSurface(tokens["--dr-surface"], dark["--dr-surface"], tokens)) {
20032
+ let lightened = 0;
20033
+ for (const [token, value] of Object.entries(dark)) {
20034
+ if (table[token] === void 0 || tokens[token] === table[token]) continue;
20035
+ if (tokens[token] !== value && !CANVAS_TOKENS.has(token)) continue;
20036
+ tokens[token] = table[token];
20037
+ lightened += 1;
20038
+ }
20039
+ record2(
20040
+ "theme.tokens",
20041
+ THEME_FILE3,
20042
+ `${lightened} palette token(s) moved from the "${DARK_PRESET}" preset to "${preset}": a dashboard is built light, and dark is the viewer's Dark mode switch`
20043
+ );
20044
+ }
19974
20045
  let filled = 0;
19975
20046
  for (const [token, value] of Object.entries(table)) {
19976
20047
  if (token in tokens) continue;
@@ -19986,6 +20057,21 @@ function composeTheme(app, preset, record2) {
19986
20057
  );
19987
20058
  }
19988
20059
  }
20060
+ function isDarkSurface(value, darkSurface, tokens, seen = /* @__PURE__ */ new Set()) {
20061
+ if (typeof value !== "string") return false;
20062
+ const alias = /^var\(\s*(--[a-z0-9-]+)\s*(?:,[^)]*)?\)$/i.exec(value.trim());
20063
+ if (alias) {
20064
+ if (seen.has(alias[1])) return false;
20065
+ seen.add(alias[1]);
20066
+ return isDarkSurface(tokens[alias[1]], darkSurface, tokens, seen);
20067
+ }
20068
+ if (value === darkSurface) return true;
20069
+ try {
20070
+ return luminance(value) < DARK_SURFACE_BELOW;
20071
+ } catch {
20072
+ return false;
20073
+ }
20074
+ }
19989
20075
  function presetOf(app, pages) {
19990
20076
  const asked = isPlainObject7(app.theme) ? app.theme.preset : null;
19991
20077
  if (typeof asked === "string" && PRESETS2.includes(asked)) return asked;
@@ -20371,6 +20457,9 @@ function appbar(page, elements) {
20371
20457
  " </div>",
20372
20458
  ' <div class="dr-appbar__actions">',
20373
20459
  ` <button type="button" class="${classesOf2(elements, "region", "refresh", ["dr-btn", "dr-btn--secondary", "dr-btn--sm"])}" data-dr-kit="refresh">Refresh</button>`,
20460
+ // The viewer's light/dark switch: dr-widgets flips the body between this
20461
+ // page's own preset and slate, and remembers the choice per app.
20462
+ ` <button type="button" class="${classesOf2(elements, "region", "theme", ["dr-btn", "dr-btn--secondary", "dr-btn--sm"])}" data-dr-kit="theme" aria-pressed="false">Dark mode</button>`,
20374
20463
  " </div>",
20375
20464
  "</header>"
20376
20465
  ];
@@ -27131,7 +27220,7 @@ var kit_default = {
27131
27220
  fmt: "dr-fmt@2.0.1",
27132
27221
  markdown: "dr-md@1.0.0",
27133
27222
  uiKit: "ui-kit/v4",
27134
- widgets: "dr-widgets@1.2.1"
27223
+ widgets: "dr-widgets@1.2.3"
27135
27224
  };
27136
27225
 
27137
27226
  // ../flows/appspace-builder/kit/kit.json
@@ -27348,7 +27437,7 @@ async function runCompile(dir, opts) {
27348
27437
  if (plan.intruders.length > 0 || plan.strays.length > 0 || plan.edited.length > 0) {
27349
27438
  report.failure = "would-overwrite";
27350
27439
  await writeReport(dir, report);
27351
- process.stderr.write(refusalOf(dir, plan));
27440
+ process.stderr.write(refusalOf(dir, plan, opts.ontoLive === true));
27352
27441
  return 1;
27353
27442
  }
27354
27443
  const check5 = opts.check === true;
@@ -27687,12 +27776,13 @@ function customHandlerPaths(contract) {
27687
27776
  }
27688
27777
  return out;
27689
27778
  }
27690
- function refusalOf(dir, plan) {
27779
+ function refusalOf(dir, plan, ontoLive) {
27691
27780
  const list3 = (paths) => `${paths.slice(0, 3).join(", ")}${paths.length > 3 ? "\u2026" : ""}`;
27692
27781
  let out = "";
27693
27782
  if (plan.intruders.length > 0) {
27694
- out += `${dir} already holds ${list3(plan.intruders)}, which this compiler did not write. Move them aside, or pass --force to write over them.
27695
- `;
27783
+ out += `${dir} already holds ${list3(plan.intruders)}, ` + // `ship --onto-live` pulled these: they are the live app, and writing
27784
+ // over them is replacing other landings' cards. Never the way past it.
27785
+ (ontoLive ? "which this compiler did not write: they are the live app as pulled, and this landing could not build on them. Nothing was pushed. Run the same command again.\n" : "which this compiler did not write. Move them aside, or pass --force to write over them.\n");
27696
27786
  }
27697
27787
  const handlers = plan.strays.filter((path3) => path3.startsWith("handlers/"));
27698
27788
  const modules = plan.strays.filter((path3) => !path3.startsWith("handlers/"));
@@ -27721,7 +27811,7 @@ function roundTripDiffs(input, extracted, compilerProblems = [], { order: order2
27721
27811
  const stated2 = order2 === void 0 ? {} : { order: order2 };
27722
27812
  const read = order2 === void 0 ? {} : { order: "keys" };
27723
27813
  const left = stripPositional(normaliseApp(input), stated2);
27724
- const right = stripPositional(normaliseApp(extracted), read);
27814
+ const right = stripPositional(normaliseApp(withoutUnstatedChrome(input, extracted)), read);
27725
27815
  const declared = compilerProblems.some((problem5) => problem5.code === DECLARED_LOSS);
27726
27816
  const diffs = [];
27727
27817
  for (const section of ["app", "pages", "elements", "data"]) {
@@ -27746,6 +27836,40 @@ function roundTripDiffs(input, extracted, compilerProblems = [], { order: order2
27746
27836
  }
27747
27837
  return diffs.slice(0, 40);
27748
27838
  }
27839
+ var ADDED_CHROME = /* @__PURE__ */ new Set(["theme"]);
27840
+ function withoutUnstatedChrome(input, extracted) {
27841
+ if (!isPlain(input) || !isPlain(extracted)) return extracted;
27842
+ const want = input["elements"];
27843
+ const got = extracted["elements"];
27844
+ if (!isPlain(want) || !isPlain(got)) return extracted;
27845
+ const regionOf = (element) => `${String(element["page"] ?? "")}|${String(element["role"] ?? "")}`;
27846
+ const statedRegions = new Set(
27847
+ Object.values(want).filter((element) => isPlain(element) && element["type"] === "region").map(regionOf)
27848
+ );
27849
+ const unstated = new Set(
27850
+ Object.entries(got).filter(([, element]) => {
27851
+ if (!isPlain(element) || element["type"] !== "region") return false;
27852
+ if (!ADDED_CHROME.has(String(element["role"] ?? ""))) return false;
27853
+ const children = element["children"];
27854
+ return !statedRegions.has(regionOf(element)) && (!Array.isArray(children) || children.length === 0);
27855
+ }).map(([uid]) => uid)
27856
+ );
27857
+ if (unstated.size === 0) return extracted;
27858
+ const out = JSON.parse(JSON.stringify(extracted));
27859
+ const elements = out["elements"];
27860
+ for (const uid of unstated) delete elements[uid];
27861
+ for (const element of Object.values(elements)) {
27862
+ if (isPlain(element) && Array.isArray(element["children"])) {
27863
+ element["children"] = element["children"].filter((uid) => !unstated.has(String(uid)));
27864
+ }
27865
+ }
27866
+ for (const page of Array.isArray(out["pages"]) ? out["pages"] : []) {
27867
+ if (isPlain(page) && Array.isArray(page["tree"])) {
27868
+ page["tree"] = page["tree"].filter((uid) => !unstated.has(String(uid)));
27869
+ }
27870
+ }
27871
+ return out;
27872
+ }
27749
27873
  function normaliseApp(contract) {
27750
27874
  if (typeof contract !== "object" || contract === null) return contract;
27751
27875
  const out = JSON.parse(JSON.stringify(contract));
@@ -27971,6 +28095,12 @@ async function loadContract(dir, opts) {
27971
28095
  return { contract, assemble: null, headline: resolveExpectations(opts.expect, contract, {}), overlay: {} };
27972
28096
  }
27973
28097
  const baseRaw = await readIfPresent(join15(dir, "contract.json"));
28098
+ if (opts.ontoLive === true && baseRaw === void 0 && await readIfPresent(join15(dir, "manifest.json")) !== void 0) {
28099
+ process.stderr.write(
28100
+ "ship --onto-live: the folder holds the live package but not its contract.json, so this landing cannot see the cards already live, and would replace them. Nothing was written. Run the same command again.\n"
28101
+ );
28102
+ return { exit: 1 };
28103
+ }
27974
28104
  const assembled = assemble({
27975
28105
  drafts: (await Promise.all(drafts.map(readNamed))).flatMap(pageDrafts),
27976
28106
  ...opts.skeleton === void 0 ? {} : { skeleton: await readNamed(opts.skeleton) },
@@ -28338,7 +28468,7 @@ function registerShipCommand(parent) {
28338
28468
  withJsonOption(
28339
28469
  withServerOptions(
28340
28470
  withAssemblyOptions(
28341
- parent.command("ship [dir]").description("Assemble, compile, push and prove a dashboard pass; print the update block the reply ends with.").option("--seed <file>", "Seed rows, as JSON. Forwarded to the push.").option("--force", "Write over files this compiler did not write (never a custom handler).").option(
28471
+ parent.command("ship [dir]").description("Assemble, compile, push and prove a dashboard pass; print the update block the reply ends with.").option("--seed <file>", "Seed rows, as JSON. Forwarded to the push.").option("--force", "Write over files this compiler did not write (never a custom handler). Refused with --onto-live.").option(
28342
28472
  "--onto-live",
28343
28473
  "Land this pass's cards on the app as it is live now, keeping every card already there: pull it, add them, push against its revision, and start over when another push lands first. Needs --app-id and --skeleton, and a folder of its own."
28344
28474
  )
@@ -28373,6 +28503,11 @@ async function runShipOntoLive(dir, opts) {
28373
28503
  if ((opts.expect ?? []).length > 0) {
28374
28504
  return refuse("--expect holds the headline, which is the build's to confirm; a landing proves its own cards only.");
28375
28505
  }
28506
+ if (opts.force === true) {
28507
+ return refuse(
28508
+ "does not take --force: every landing builds on the cards already live, and --force would let this one replace them. Run the same command without it. If it refuses over files it did not write, the live app could not be read whole: run it again, and report the refusal if it repeats."
28509
+ );
28510
+ }
28376
28511
  const owned = await claimFolder(dir);
28377
28512
  if (owned !== null) return refuse(owned);
28378
28513
  await defaultServerFromSession(opts);
@@ -28384,7 +28519,20 @@ async function runShipOntoLive(dir, opts) {
28384
28519
  const client = await connect(opts);
28385
28520
  let pulled;
28386
28521
  try {
28387
- pulled = await pullInto(client, appId, dir);
28522
+ pulled = await pullInto(client, appId, dir, {
28523
+ onWait: (pull, ms, missing) => {
28524
+ if (opts.json === true) return;
28525
+ process.stdout.write(
28526
+ `onto ${appId}: the live version's contract is not readable yet (${missing}) \u2014 pulling again in ${String(ms)} ms (pull ${String(pull + 1)})
28527
+ `
28528
+ );
28529
+ }
28530
+ });
28531
+ } catch (err) {
28532
+ if (!(err instanceof LiveContractMissing)) throw err;
28533
+ process.stderr.write(`ship --onto-live: ${err.message}
28534
+ `);
28535
+ return { code: 1, lost: null, creating: false };
28388
28536
  } finally {
28389
28537
  client.cleanup();
28390
28538
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@datarails-ai/dr-appspace-cli",
3
- "version": "0.12.11",
3
+ "version": "0.12.13",
4
4
  "description": "Datarails App Space CLI — build, try, push and publish App Space apps",
5
5
  "type": "module",
6
6
  "engines": {