@lotics/cli 0.162.0 → 0.164.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/src/cli.js CHANGED
@@ -44177,7 +44177,9 @@ function gatewayErrorMessage(status) {
44177
44177
  }
44178
44178
  function transportErrorMessage(status, parsed) {
44179
44179
  const jsonMessage = parsed && typeof parsed.message === "string" ? parsed.message : null;
44180
- return parsed === null || status >= 500 || jsonMessage === null ? gatewayErrorMessage(status) : jsonMessage;
44180
+ if (jsonMessage === null) return gatewayErrorMessage(status);
44181
+ const authored = typeof parsed.code === "string" && parsed.code.length > 0;
44182
+ return status < 500 || authored ? jsonMessage : gatewayErrorMessage(status);
44181
44183
  }
44182
44184
 
44183
44185
  // src/client.ts
@@ -44478,6 +44480,16 @@ var LoticsClient = class {
44478
44480
  async retirePackage(package_id, body) {
44479
44481
  return this.request("POST", `/v1/packages/${encodeURIComponent(package_id)}/retire`, body);
44480
44482
  }
44483
+ /**
44484
+ * Edit a starter's registry listing — the name and description a stranger
44485
+ * reads before copying, and what every copy's app row is created from.
44486
+ *
44487
+ * A version is an immutable snapshot; the listing is not. Omit a field to
44488
+ * leave it, pass `description: null` to clear it. Owner-org admin-only.
44489
+ */
44490
+ async editPackageListing(package_id, body) {
44491
+ return this.request("POST", `/v1/packages/${encodeURIComponent(package_id)}/listing`, body);
44492
+ }
44481
44493
  // --- Packages (registry reads + installations) ---
44482
44494
  // Authoring is server-side: apps via `POST /v1/apps/{id}/package-publish|release`,
44483
44495
  // content packages via the `publish_content`/`release_content` tools. There is no
@@ -45964,6 +45976,7 @@ var COMMANDS = [
45964
45976
  },
45965
45977
  {
45966
45978
  verbs: ["xlsx"],
45979
+ ownsTail: true,
45967
45980
  help: [
45968
45981
  " lotics xlsx <subcommand> ... Read/write/edit .xlsx files on your local filesystem",
45969
45982
  " (uses the bundled Lotics xlsx engine; prefer over",
@@ -45972,6 +45985,7 @@ var COMMANDS = [
45972
45985
  },
45973
45986
  {
45974
45987
  verbs: ["docx"],
45988
+ ownsTail: true,
45975
45989
  help: [
45976
45990
  " lotics docx <subcommand> ... Read/write/edit .docx files on your local filesystem",
45977
45991
  " (uses the bundled Lotics OOXML engine; preserves",
@@ -46003,6 +46017,32 @@ function renderCommandsHelp() {
46003
46017
  function knownVerbs() {
46004
46018
  return new Set(COMMANDS.flatMap((group) => [...group.verbs, ...group.aliases ?? []]));
46005
46019
  }
46020
+ var SUBCOMMAND_ARGS = /* @__PURE__ */ new Map([
46021
+ ["xlsx read", { valueFlags: ["--sheet", "--range"], flags: ["--with-format"] }],
46022
+ ["xlsx set-cell", { content: true }],
46023
+ ["docx read", { flags: ["--include-opaque"] }],
46024
+ ["docx append-paragraph", { flags: ["--style="], content: true }],
46025
+ ["docx insert-paragraph", { flags: ["--style=", "--at="], content: true }],
46026
+ ["docx delete-block", { flags: ["--at="] }],
46027
+ ["docx replace-text", { content: true }]
46028
+ ]);
46029
+ function commandOwnsTail(command) {
46030
+ if (command === void 0) return false;
46031
+ return COMMANDS.some(
46032
+ (group) => group.ownsTail === true && [...group.verbs, ...group.aliases ?? []].includes(command)
46033
+ );
46034
+ }
46035
+ function subcommandArgs(command, subcommand) {
46036
+ if (command === void 0 || subcommand === void 0) return void 0;
46037
+ return SUBCOMMAND_ARGS.get(`${command} ${subcommand}`);
46038
+ }
46039
+ function knownSubcommandFlags(command, subcommand) {
46040
+ const spec = subcommandArgs(command, subcommand);
46041
+ return [...spec?.valueFlags ?? [], ...spec?.flags ?? []];
46042
+ }
46043
+ function matchesKnownFlag(known, arg) {
46044
+ return known.some((k) => k.endsWith("=") ? arg.startsWith(k) : arg === k);
46045
+ }
46006
46046
 
46007
46047
  // src/report_command.ts
46008
46048
  import fs6 from "node:fs";
@@ -46141,6 +46181,22 @@ function buildStarterTemplate(args) {
46141
46181
  typecheck: "tsc --noEmit",
46142
46182
  // Scope to src/ — oxlint's default scan walks node_modules too.
46143
46183
  lint: "oxlint src",
46184
+ // No `--passWithNoTests`, deliberately. It would be justified by a
46185
+ // TEMPORARY condition ("the scaffold seeds nothing") and then ship
46186
+ // forever in every app's package.json and CI, where its only
46187
+ // remaining effect is to turn "vitest discovered no test files" into
46188
+ // success — a green suite over a renamed `src/`, a changed `include`
46189
+ // or a broken vitest config, indefinitely. A detector that cannot
46190
+ // fire when the thing is ABSENT rather than excessive is not one.
46191
+ //
46192
+ // What the scaffold seeds instead is `src/harness.test.ts`, which
46193
+ // asserts the HARNESS and nothing else. The file it replaces
46194
+ // (`src/App.test.tsx`) asserted the scaffold's placeholder copy
46195
+ // ("First item"), so it failed the moment the author did the very
46196
+ // thing the closing instructions tell them to do — edit
46197
+ // `src/App.tsx` — and a smoke test that renders `<App/>` would fail
46198
+ // the same way for any real app whose first screen reaches the host
46199
+ // bridge on mount. The harness test touches neither.
46144
46200
  test: "vitest run"
46145
46201
  },
46146
46202
  dependencies: {
@@ -46716,15 +46772,33 @@ declare module "*.css";
46716
46772
  `
46717
46773
  },
46718
46774
  {
46719
- path: "src/App.test.tsx",
46775
+ path: "src/harness.test.ts",
46720
46776
  content: `import { describe, test, expect } from "vitest";
46721
- import { render } from "@testing-library/react";
46722
- import App from "./App";
46777
+ import { getAppBinding } from "@lotics/app-sdk";
46778
+
46779
+ /**
46780
+ * The one test the scaffold seeds, and it is about the TEST SETUP rather than
46781
+ * about your app \u2014 so editing src/App.tsx can never break it, and \`npm test\`
46782
+ * never has to be green over zero tests.
46783
+ *
46784
+ * What it pins: tests run under jsdom, and vitest.setup.ts's echo stub answers
46785
+ * for \`getAppBinding\`. That second one is the whole reason the setup file
46786
+ * exists: a package-linked app's generated .lotics/app_fields.ts awaits it while
46787
+ * a test file is still COLLECTING, so without the stub every test fails before
46788
+ * any of them runs \u2014 including pure logic tests that never touch a field id.
46789
+ *
46790
+ * Delete it once you have tests of your own, or keep it; it costs nothing.
46791
+ */
46792
+ describe("test harness", () => {
46793
+ test("runs in a DOM", () => {
46794
+ expect(typeof document).toBe("object");
46795
+ });
46723
46796
 
46724
- describe("App", () => {
46725
- test("renders the default route", () => {
46726
- const { container } = render(<App />);
46727
- expect(container.textContent).toContain("First item");
46797
+ test("resolves the app binding from the setup stub, so the app graph collects", async () => {
46798
+ const binding = await getAppBinding();
46799
+ // The echo stub answers any alias with a self-identifying ':test:' id \u2014 one
46800
+ // that can never be mistaken for a real fld_\u2026 from the workspace.
46801
+ expect(binding.fields["anything.at.all"]).toContain(":test:");
46728
46802
  });
46729
46803
  });
46730
46804
  `
@@ -62911,7 +62985,20 @@ var readSourceSchema = zod_default.discriminatedUnion("from", [
62911
62985
  zod_default.object({ from: zod_default.literal("trigger_record") }),
62912
62986
  zod_default.object({ from: zod_default.literal("trigger_record_prev") }),
62913
62987
  zod_default.object({ from: zod_default.literal("trigger_changes") }),
62914
- zod_default.object({ from: zod_default.literal("foreach_item") }),
62988
+ // `name` is the loop's bind name, and it is OPTIONAL for one reason: every
62989
+ // body stored before names existed emits the bare form, and those resolve to
62990
+ // the innermost frame — today's behaviour, and correct for any loop that is
62991
+ // not nested. Without a name a read inside a NESTED loop cannot say which
62992
+ // loop it means, so both binds resolve to the inner item and a join between
62993
+ // two collections silently matches nothing. The parser emits the name now, so
62994
+ // a body gets correct nesting the first time it is saved through it.
62995
+ zod_default.object({ from: zod_default.literal("foreach_item"), name: zod_default.string().min(1).optional() }),
62996
+ // No `name` here, deliberately: the JS surface spells the index as one
62997
+ // reserved `index` keyword with no per-loop form, so the innermost loop is
62998
+ // the only thing a read can mean. A name would be representable, honoured at
62999
+ // run time, and silently rewritten to the innermost by the renderer — which
63000
+ // prints every index as `index` — reintroducing the corruption above for a
63001
+ // case nothing can author.
62915
63002
  zod_default.object({ from: zod_default.literal("foreach_index") }),
62916
63003
  zod_default.object({
62917
63004
  from: zod_default.literal("step_output"),
@@ -64810,7 +64897,14 @@ var formulaAggregateOperationSchema = external_exports.enum([
64810
64897
  "median",
64811
64898
  "min",
64812
64899
  "max",
64813
- "range"
64900
+ "range",
64901
+ "earliest",
64902
+ "latest",
64903
+ "date_range",
64904
+ "checked",
64905
+ "unchecked",
64906
+ "percent_checked",
64907
+ "percent_unchecked"
64814
64908
  ]);
64815
64909
  var rollupAggregateOperationSchema = external_exports.enum([
64816
64910
  "empty",
@@ -64910,25 +65004,25 @@ var aggregateOperationSchema = external_exports.enum([
64910
65004
  "string_agg"
64911
65005
  ]);
64912
65006
  var OPERATION_FIELD_TYPES = {
64913
- empty: ["text", "number", "date", "select", "select_member", "select_record_link", "files", "formula", "rollup"],
64914
- filled: ["text", "number", "date", "select", "select_member", "select_record_link", "files", "formula", "rollup"],
64915
- percent_empty: ["text", "number", "date", "select", "select_member", "select_record_link", "files", "formula", "rollup"],
64916
- percent_filled: ["text", "number", "date", "select", "select_member", "select_record_link", "files", "formula", "rollup"],
64917
- unique: ["text", "number", "date", "select", "select_member", "files", "formula", "rollup"],
64918
- percent_unique: ["text", "number", "date", "select", "select_member", "files", "formula", "rollup"],
65007
+ empty: ["text", "number", "date", "select", "select_member", "select_record_link", "files", "formula", "rollup", "autonumber"],
65008
+ filled: ["text", "number", "date", "select", "select_member", "select_record_link", "files", "formula", "rollup", "autonumber"],
65009
+ percent_empty: ["text", "number", "date", "select", "select_member", "select_record_link", "files", "formula", "rollup", "autonumber"],
65010
+ percent_filled: ["text", "number", "date", "select", "select_member", "select_record_link", "files", "formula", "rollup", "autonumber"],
65011
+ unique: ["text", "number", "date", "select", "select_member", "files", "formula", "rollup", "autonumber"],
65012
+ percent_unique: ["text", "number", "date", "select", "select_member", "files", "formula", "rollup", "autonumber"],
64919
65013
  sum: ["number", "formula", "rollup"],
64920
65014
  avg: ["number", "formula", "rollup"],
64921
65015
  median: ["number", "formula", "rollup"],
64922
65016
  min: ["number", "formula", "rollup"],
64923
65017
  max: ["number", "formula", "rollup"],
64924
65018
  range: ["number", "formula", "rollup"],
64925
- earliest: ["date", "rollup"],
64926
- latest: ["date", "rollup"],
64927
- date_range: ["date", "rollup"],
64928
- checked: ["boolean"],
64929
- unchecked: ["boolean"],
64930
- percent_checked: ["boolean"],
64931
- percent_unchecked: ["boolean"]
65019
+ earliest: ["date", "formula", "rollup"],
65020
+ latest: ["date", "formula", "rollup"],
65021
+ date_range: ["date", "formula", "rollup"],
65022
+ checked: ["boolean", "formula"],
65023
+ unchecked: ["boolean", "formula"],
65024
+ percent_checked: ["boolean", "formula"],
65025
+ percent_unchecked: ["boolean", "formula"]
64932
65026
  };
64933
65027
  var FIELD_TYPE_OPERATIONS = /* @__PURE__ */ new Map();
64934
65028
  for (const [operation, fieldTypes] of Object.entries(OPERATION_FIELD_TYPES)) {
@@ -65608,8 +65702,7 @@ var appWorkflowInputRecordLinkSchema = appWorkflowInputBaseSchema.extend({
65608
65702
  table_id: zod_default.string().describe("Records must belong to this table. Validated at execute time + workflow save."),
65609
65703
  multi: zod_default.boolean().optional().describe("When true, accept an array of record_ids. Defaults to single.")
65610
65704
  });
65611
- var appWorkflowInputSelectSchema = appWorkflowInputBaseSchema.extend({
65612
- type: zod_default.literal("select"),
65705
+ var selectOptionSourceShape = {
65613
65706
  options: zod_default.array(
65614
65707
  zod_default.object({
65615
65708
  label: zod_default.string(),
@@ -65617,20 +65710,27 @@ var appWorkflowInputSelectSchema = appWorkflowInputBaseSchema.extend({
65617
65710
  })
65618
65711
  ).min(1).optional().describe("Inline option set. Provide EITHER `options` OR `field`, never both."),
65619
65712
  field: zod_default.string().optional().describe(
65620
- "A select field's key (fld_\u2026) whose CURRENT options back this input \u2014 resolved at type-gen, validated at run time, drift-proof. Provide EITHER `field` OR `options`, never both."
65713
+ "A select field's key (fld_\u2026) whose CURRENT options back this declaration \u2014 resolved at type-gen, validated at run time, drift-proof. Provide EITHER `field` OR `options`, never both."
65621
65714
  ),
65622
65715
  multi: zod_default.boolean().optional()
65623
- }).check((ctx) => {
65624
- const hasOptions = ctx.value.options !== void 0;
65625
- const hasField = ctx.value.field !== void 0;
65626
- if (hasOptions === hasField) {
65627
- ctx.issues.push({
65628
- code: "custom",
65629
- input: ctx.value,
65630
- message: "select input must declare exactly one of `options` (inline) or `field` (a fld_\u2026 key resolved to the field's current options) \u2014 not both, not neither"
65631
- });
65632
- }
65633
- });
65716
+ };
65717
+ function checkExactlyOneOptionSource(kind) {
65718
+ return (ctx) => {
65719
+ const hasOptions = ctx.value.options !== void 0;
65720
+ const hasField = ctx.value.field !== void 0;
65721
+ if (hasOptions === hasField) {
65722
+ ctx.issues.push({
65723
+ code: "custom",
65724
+ input: ctx.value,
65725
+ message: `select ${kind} must declare exactly one of \`options\` (inline) or \`field\` (a fld_\u2026 key resolved to the field's current options) \u2014 not both, not neither`
65726
+ });
65727
+ }
65728
+ };
65729
+ }
65730
+ var appWorkflowInputSelectSchema = appWorkflowInputBaseSchema.extend({
65731
+ type: zod_default.literal("select"),
65732
+ ...selectOptionSourceShape
65733
+ }).check(checkExactlyOneOptionSource("input"));
65634
65734
  var appWorkflowInputDateRangeSchema = appWorkflowInputBaseSchema.extend({
65635
65735
  type: zod_default.literal("date_range"),
65636
65736
  include_time: zod_default.boolean().optional().describe("When true, the range includes time components (datetime). Defaults to false.")
@@ -65701,11 +65801,14 @@ var appWorkflowOutputSchema = zod_default.lazy(
65701
65801
  table_id: zod_default.string(),
65702
65802
  multi: zod_default.boolean().optional()
65703
65803
  }),
65804
+ // The SAME shape and the SAME check as the select INPUT, not a copy of
65805
+ // them: the two surfaces declare one vocabulary (GAP-294), so accepting
65806
+ // `field` on one and not the other — or validating it differently — just
65807
+ // moves the drift somewhere nobody looks.
65704
65808
  appWorkflowOutputBaseSchema.extend({
65705
65809
  type: zod_default.literal("select"),
65706
- options: zod_default.array(zod_default.object({ label: zod_default.string(), value: zod_default.string() })).min(1),
65707
- multi: zod_default.boolean().optional()
65708
- }),
65810
+ ...selectOptionSourceShape
65811
+ }).check(checkExactlyOneOptionSource("output")),
65709
65812
  appWorkflowOutputBaseSchema.extend({
65710
65813
  type: zod_default.literal("object"),
65711
65814
  fields: zod_default.record(zod_default.string(), appWorkflowOutputSchema)
@@ -66075,7 +66178,6 @@ __export(expression_array_exports, {
66075
66178
  includes: () => includes,
66076
66179
  intersection: () => intersection2,
66077
66180
  intersectionBy: () => intersectionBy,
66078
- isEmpty: () => isEmpty2,
66079
66181
  last: () => last2,
66080
66182
  list: () => list,
66081
66183
  maxBy: () => maxBy,
@@ -66530,13 +66632,6 @@ function size(arr) {
66530
66632
  }
66531
66633
  return arr.length;
66532
66634
  }
66533
- function isEmpty2(arr) {
66534
- if (arr == null) return true;
66535
- if (!Array.isArray(arr)) {
66536
- throw new Error("isEmpty: expected array, got " + typeof arr);
66537
- }
66538
- return arr.length === 0;
66539
- }
66540
66635
  function includes(collection, value2) {
66541
66636
  if (collection == null) return false;
66542
66637
  if (Array.isArray(collection)) return collection.includes(value2);
@@ -66807,6 +66902,7 @@ function capitalize(str) {
66807
66902
  return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase();
66808
66903
  }
66809
66904
  function replace(str, search, replacement) {
66905
+ if (str == null) return "";
66810
66906
  if (typeof str !== "string") {
66811
66907
  throw new Error("replace: expected string, got " + typeof str);
66812
66908
  }
@@ -66819,6 +66915,7 @@ function replace(str, search, replacement) {
66819
66915
  return str.replace(search, replacement);
66820
66916
  }
66821
66917
  function replaceAll(str, search, replacement) {
66918
+ if (str == null) return "";
66822
66919
  if (typeof str !== "string") {
66823
66920
  throw new Error("replaceAll: expected string, got " + typeof str);
66824
66921
  }
@@ -66851,6 +66948,7 @@ function endsWith(str, suffix) {
66851
66948
  return str.endsWith(suffix);
66852
66949
  }
66853
66950
  function substring(str, start, end) {
66951
+ if (str == null) return "";
66854
66952
  if (typeof str !== "string") {
66855
66953
  throw new Error("substring: expected string, got " + typeof str);
66856
66954
  }
@@ -66863,6 +66961,7 @@ function substring(str, start, end) {
66863
66961
  return str.substring(start, end);
66864
66962
  }
66865
66963
  function padStart(str, length2, char) {
66964
+ if (str == null) return "";
66866
66965
  if (typeof str !== "string") {
66867
66966
  throw new Error("padStart: expected string, got " + typeof str);
66868
66967
  }
@@ -66875,6 +66974,7 @@ function padStart(str, length2, char) {
66875
66974
  return str.padStart(length2, char);
66876
66975
  }
66877
66976
  function padEnd(str, length2, char) {
66977
+ if (str == null) return "";
66878
66978
  if (typeof str !== "string") {
66879
66979
  throw new Error("padEnd: expected string, got " + typeof str);
66880
66980
  }
@@ -66980,6 +67080,7 @@ function numberToWords(value2) {
66980
67080
  return negative ? "\xE2m " + result : result;
66981
67081
  }
66982
67082
  function formatNumber(value2, decimals) {
67083
+ if (value2 == null) return "";
66983
67084
  if (typeof value2 !== "number" || Number.isNaN(value2)) {
66984
67085
  throw new Error("formatNumber: expected number, got " + typeof value2);
66985
67086
  }
@@ -67007,15 +67108,17 @@ __export(expression_math_exports, {
67007
67108
  sum: () => sum
67008
67109
  });
67009
67110
  function mean(arr) {
67010
- if (arr == null || !Array.isArray(arr)) {
67111
+ if (arr == null) return null;
67112
+ if (!Array.isArray(arr)) {
67011
67113
  throw new Error("mean: expected array, got " + typeof arr);
67012
67114
  }
67013
- if (arr.length === 0) {
67014
- throw new Error("mean: array cannot be empty");
67115
+ const present = arr.filter((item) => item != null);
67116
+ if (present.length === 0) {
67117
+ return null;
67015
67118
  }
67016
- const numArr = arr;
67017
- const sum2 = numArr.reduce((a, b) => a + b, 0);
67018
- const result = sum2 / numArr.length;
67119
+ const numArr = present;
67120
+ const total = numArr.reduce((a, b) => a + b, 0);
67121
+ const result = total / numArr.length;
67019
67122
  if (isNaN(result)) {
67020
67123
  throw new Error("mean: array contains non-numeric values");
67021
67124
  }
@@ -67028,7 +67131,7 @@ function min(...values3) {
67028
67131
  if (values3.length === 1 && Array.isArray(values3[0])) {
67029
67132
  const arr = values3[0];
67030
67133
  if (arr.length === 0) {
67031
- throw new Error("min: array cannot be empty");
67134
+ return null;
67032
67135
  }
67033
67136
  return arr.reduce((minVal, item) => {
67034
67137
  if (minVal == null) return item;
@@ -67037,9 +67140,9 @@ function min(...values3) {
67037
67140
  return item < minVal ? item : minVal;
67038
67141
  }
67039
67142
  return String(item) < String(minVal) ? item : minVal;
67040
- });
67143
+ }) ?? null;
67041
67144
  }
67042
- return values3.reduce((minVal, item) => {
67145
+ const smallest = values3.reduce((minVal, item) => {
67043
67146
  if (minVal == null) return item;
67044
67147
  if (item == null) return minVal;
67045
67148
  if (typeof minVal === "number" && typeof item === "number") {
@@ -67047,6 +67150,7 @@ function min(...values3) {
67047
67150
  }
67048
67151
  return String(item) < String(minVal) ? item : minVal;
67049
67152
  });
67153
+ return smallest ?? null;
67050
67154
  }
67051
67155
  function max(...values3) {
67052
67156
  if (values3.length === 0) {
@@ -67055,7 +67159,7 @@ function max(...values3) {
67055
67159
  if (values3.length === 1 && Array.isArray(values3[0])) {
67056
67160
  const arr = values3[0];
67057
67161
  if (arr.length === 0) {
67058
- throw new Error("max: array cannot be empty");
67162
+ return null;
67059
67163
  }
67060
67164
  return arr.reduce((maxVal, item) => {
67061
67165
  if (maxVal == null) return item;
@@ -67064,9 +67168,9 @@ function max(...values3) {
67064
67168
  return item > maxVal ? item : maxVal;
67065
67169
  }
67066
67170
  return String(item) > String(maxVal) ? item : maxVal;
67067
- });
67171
+ }) ?? null;
67068
67172
  }
67069
- return values3.reduce((maxVal, item) => {
67173
+ const largest = values3.reduce((maxVal, item) => {
67070
67174
  if (maxVal == null) return item;
67071
67175
  if (item == null) return maxVal;
67072
67176
  if (typeof maxVal === "number" && typeof item === "number") {
@@ -67074,8 +67178,10 @@ function max(...values3) {
67074
67178
  }
67075
67179
  return String(item) > String(maxVal) ? item : maxVal;
67076
67180
  });
67181
+ return largest ?? null;
67077
67182
  }
67078
67183
  function round(value2, precision) {
67184
+ if (value2 == null) return null;
67079
67185
  if (typeof value2 !== "number") {
67080
67186
  throw new Error("round: expected number, got " + typeof value2);
67081
67187
  }
@@ -67094,24 +67200,28 @@ function sum(arr) {
67094
67200
  return arr.reduce((a, b) => a + b, 0);
67095
67201
  }
67096
67202
  function abs(n) {
67203
+ if (n == null) return null;
67097
67204
  if (typeof n !== "number") {
67098
67205
  throw new Error("abs: expected number, got " + typeof n);
67099
67206
  }
67100
67207
  return Math.abs(n);
67101
67208
  }
67102
67209
  function ceil(n) {
67210
+ if (n == null) return null;
67103
67211
  if (typeof n !== "number") {
67104
67212
  throw new Error("ceil: expected number, got " + typeof n);
67105
67213
  }
67106
67214
  return Math.ceil(n);
67107
67215
  }
67108
67216
  function floor(n) {
67217
+ if (n == null) return null;
67109
67218
  if (typeof n !== "number") {
67110
67219
  throw new Error("floor: expected number, got " + typeof n);
67111
67220
  }
67112
67221
  return Math.floor(n);
67113
67222
  }
67114
67223
  function clamp(n, min2, max3) {
67224
+ if (n == null) return null;
67115
67225
  if (typeof n !== "number") {
67116
67226
  throw new Error("clamp: expected number, got " + typeof n);
67117
67227
  }
@@ -67129,6 +67239,7 @@ function clamp(n, min2, max3) {
67129
67239
  return n;
67130
67240
  }
67131
67241
  function percentage(part, total, decimals) {
67242
+ if (part == null || total == null) return null;
67132
67243
  if (typeof part !== "number") {
67133
67244
  throw new Error("percentage: part must be a number");
67134
67245
  }
@@ -67149,6 +67260,7 @@ function percentage(part, total, decimals) {
67149
67260
  return percent;
67150
67261
  }
67151
67262
  function pow(base, exponent) {
67263
+ if (base == null || exponent == null) return null;
67152
67264
  if (typeof base !== "number") {
67153
67265
  throw new Error("pow: base must be a number");
67154
67266
  }
@@ -67158,6 +67270,7 @@ function pow(base, exponent) {
67158
67270
  return Math.pow(base, exponent);
67159
67271
  }
67160
67272
  function sqrt(n) {
67273
+ if (n == null) return null;
67161
67274
  if (typeof n !== "number") {
67162
67275
  throw new Error("sqrt: expected number, got " + typeof n);
67163
67276
  }
@@ -67167,6 +67280,7 @@ function sqrt(n) {
67167
67280
  return Math.sqrt(n);
67168
67281
  }
67169
67282
  function mod(n, divisor) {
67283
+ if (n == null || divisor == null) return null;
67170
67284
  if (typeof n !== "number") {
67171
67285
  throw new Error("mod: expected number, got " + typeof n);
67172
67286
  }
@@ -67339,7 +67453,7 @@ __export(expression_type_exports, {
67339
67453
  coalesce: () => coalesce,
67340
67454
  isArray: () => isArray2,
67341
67455
  isBoolean: () => isBoolean,
67342
- isEmpty: () => isEmpty3,
67456
+ isEmpty: () => isEmpty2,
67343
67457
  isNotNull: () => isNotNull,
67344
67458
  isNull: () => isNull,
67345
67459
  isNumber: () => isNumber,
@@ -67351,6 +67465,22 @@ __export(expression_type_exports, {
67351
67465
  toString: () => toString,
67352
67466
  typeOf: () => typeOf
67353
67467
  });
67468
+
67469
+ // ../shared/src/schemas/database_types.ts
67470
+ function isEmptyFieldValue(value2) {
67471
+ if (value2 === null || value2 === void 0) {
67472
+ return true;
67473
+ }
67474
+ if (typeof value2 === "string") {
67475
+ return value2 === "";
67476
+ }
67477
+ if (Array.isArray(value2)) {
67478
+ return value2.length === 0;
67479
+ }
67480
+ return false;
67481
+ }
67482
+
67483
+ // ../shared/src/expression_type.ts
67354
67484
  function isArray2(val) {
67355
67485
  return Array.isArray(val);
67356
67486
  }
@@ -67373,6 +67503,7 @@ function coalesce(...values3) {
67373
67503
  return void 0;
67374
67504
  }
67375
67505
  function toNumber(val) {
67506
+ if (val == null) return null;
67376
67507
  if (typeof val === "number") return val;
67377
67508
  if (typeof val === "string") {
67378
67509
  const parsed = parseFloat(val);
@@ -67384,9 +67515,7 @@ function toNumber(val) {
67384
67515
  throw new Error("toNumber: expected number or string, got " + typeof val);
67385
67516
  }
67386
67517
  function toString(val) {
67387
- if (val == null) {
67388
- throw new Error("toString: cannot convert null or undefined to string");
67389
- }
67518
+ if (val == null) return "";
67390
67519
  return String(val);
67391
67520
  }
67392
67521
  function isNull(val) {
@@ -67395,13 +67524,11 @@ function isNull(val) {
67395
67524
  function isNotNull(val) {
67396
67525
  return !isNull(val);
67397
67526
  }
67398
- function isEmpty3(val) {
67399
- if (val === null || val === void 0) return true;
67400
- if (typeof val === "string") return val.length === 0;
67401
- if (Array.isArray(val)) return val.length === 0;
67402
- return false;
67527
+ function isEmpty2(val) {
67528
+ return isEmptyFieldValue(val);
67403
67529
  }
67404
67530
  function parseJson(value2) {
67531
+ if (value2 == null) return null;
67405
67532
  if (typeof value2 !== "string") {
67406
67533
  throw new Error("parseJson: expected a JSON string, got " + typeof value2);
67407
67534
  }
@@ -69066,7 +69193,7 @@ function walkRead(node, scope) {
69066
69193
  return readFromLambdaParam(root, chain2);
69067
69194
  }
69068
69195
  if (scope.foreachBinds.includes(root)) {
69069
- return readFromForeachItem(chain2);
69196
+ return readFromForeachItem(root, chain2);
69070
69197
  }
69071
69198
  if (scope.stepIds.has(root)) {
69072
69199
  return readFromStepOutput(root, chain2);
@@ -69192,10 +69319,13 @@ function readFromRuntime(chain2) {
69192
69319
  return { kind: "read", source, path: path14 };
69193
69320
  });
69194
69321
  }
69195
- function readFromForeachItem(chain2) {
69322
+ function readFromForeachItem(name2, chain2) {
69196
69323
  return wrapOptionalGuards(chain2, (segments) => ({
69197
69324
  kind: "read",
69198
- source: { from: "foreach_item" },
69325
+ // NAMED, so a read inside a nested loop says which loop it means. Without
69326
+ // it both binds resolved to the innermost item, and a join between two
69327
+ // collections matched nothing while the workflow reported success.
69328
+ source: { from: "foreach_item", name: name2 },
69199
69329
  path: classifyRecordPath(segments)
69200
69330
  }));
69201
69331
  }
@@ -70151,7 +70281,11 @@ function walkForOf(stmt, scope, stepIds, opts, stepId, description, nextSyntheti
70151
70281
  type: "bind",
70152
70282
  expression: patternEntryExpression(entry, {
70153
70283
  kind: "read",
70154
- source: { from: "foreach_item" },
70284
+ // The loop's synthetic bind name, stated rather than left to the
70285
+ // innermost-frame fallback — these bind steps happen to run where
70286
+ // this loop IS innermost, and naming it keeps that true if the
70287
+ // lowering ever moves.
70288
+ source: { from: "foreach_item", name: bindName },
70155
70289
  path: [entry.segment]
70156
70290
  })
70157
70291
  });
@@ -71002,7 +71136,10 @@ ${args.instructions.replace(/\s+$/, "")}
71002
71136
  `;
71003
71137
  }
71004
71138
  function stripAgentHeader(text) {
71005
- return text.replace(/<!--\s*lotics:[\s\S]*?-->\n?/g, "").replace(/\n{3,}/g, "\n\n").trim();
71139
+ return normalizeAgentInstructions(text.replace(/<!--\s*lotics:[\s\S]*?-->\n?/g, ""));
71140
+ }
71141
+ function normalizeAgentInstructions(text) {
71142
+ return text.replace(/\n{3,}/g, "\n\n").trim();
71006
71143
  }
71007
71144
 
71008
71145
  // src/npm_registry.ts
@@ -71121,6 +71258,49 @@ function writeSynced(projectDir, kind, alias, entry) {
71121
71258
  pkg2.lotics.synced[kind] = { ...pkg2.lotics.synced[kind], [alias]: entry };
71122
71259
  fs8.writeFileSync(pkgPath2, JSON.stringify(pkg2, null, 2) + "\n");
71123
71260
  }
71261
+ function readManifestEntry(projectDir, kind, alias) {
71262
+ const pkg2 = readManifest(projectDir);
71263
+ if (!pkg2.lotics) return { found: false };
71264
+ const entry = { found: true };
71265
+ let found = false;
71266
+ const declarations = pkg2.lotics[kind];
71267
+ if (declarations && typeof declarations === "object" && !Array.isArray(declarations)) {
71268
+ const map3 = declarations;
71269
+ if (alias in map3) {
71270
+ entry.declaration = map3[alias];
71271
+ found = true;
71272
+ }
71273
+ }
71274
+ const baselines = pkg2.lotics.synced?.[kind];
71275
+ if (baselines && alias in baselines) {
71276
+ entry.synced = baselines[alias];
71277
+ found = true;
71278
+ }
71279
+ return found ? entry : { found: false };
71280
+ }
71281
+ function readManifest(projectDir) {
71282
+ const pkgPath2 = path10.join(projectDir, "package.json");
71283
+ return JSON.parse(fs8.readFileSync(pkgPath2, "utf-8"));
71284
+ }
71285
+ function removeFromManifest(projectDir, kind, alias) {
71286
+ const pkgPath2 = path10.join(projectDir, "package.json");
71287
+ const pkg2 = readManifest(projectDir);
71288
+ if (!pkg2.lotics) return;
71289
+ const declarations = pkg2.lotics[kind];
71290
+ if (declarations && typeof declarations === "object" && !Array.isArray(declarations)) {
71291
+ delete declarations[alias];
71292
+ }
71293
+ const baselines = pkg2.lotics.synced?.[kind];
71294
+ if (baselines) delete baselines[alias];
71295
+ fs8.writeFileSync(pkgPath2, JSON.stringify(pkg2, null, 2) + "\n");
71296
+ }
71297
+ function parkPrunedDeclaration(projectDir, kind, alias, removed) {
71298
+ const dir = path10.join(projectDir, ".lotics", "pruned", kind);
71299
+ fs8.mkdirSync(dir, { recursive: true });
71300
+ const file2 = path10.join(dir, `${alias}.json`);
71301
+ fs8.writeFileSync(file2, JSON.stringify(removed, null, 2) + "\n");
71302
+ return path10.relative(projectDir, file2);
71303
+ }
71124
71304
  var WORKFLOWS_DIR = path10.join("src", "workflows");
71125
71305
  var WORKFLOW_GLOBALS_DIR = path10.join(".lotics", "workflows");
71126
71306
  var AGENTS_DIR = path10.join("src", "agents");
@@ -71270,13 +71450,52 @@ function writeAgentFiles(projectDir, agents, opts) {
71270
71450
  }
71271
71451
  return written;
71272
71452
  }
71453
+ function parkLiveAgentProse(projectDir, alias, instructions) {
71454
+ const dir = path10.join(projectDir, ".lotics", "agents");
71455
+ const file2 = path10.join(dir, `${alias}.live.md`);
71456
+ try {
71457
+ fs8.mkdirSync(dir, { recursive: true });
71458
+ fs8.writeFileSync(file2, normalizeAgentInstructions(instructions) + "\n");
71459
+ return path10.relative(projectDir, file2);
71460
+ } catch (err2) {
71461
+ console.error(
71462
+ `\u26A0 Could not write the live prose for ${alias} to ${path10.relative(projectDir, file2)}: ${err2 instanceof Error ? err2.message : String(err2)}`
71463
+ );
71464
+ return null;
71465
+ }
71466
+ }
71467
+ function reportUnseenLive(kind, unseen, appId) {
71468
+ if (unseen.length === 0) return;
71469
+ const plural = unseen.length === 1;
71470
+ console.error(
71471
+ `
71472
+ \u26A0 Kept your ${plural ? kind : `${kind}s`} for ` + unseen.map((u) => u.parked ? `${u.alias} (live copy written to ${u.parked})` : u.alias).join(", ") + ` \u2014 and the live ${kind} has CHANGED since this checkout last saw it, so somebody else edited ${plural ? "it" : "them"}.
71473
+ Your next deploy will be REFUSED for ${plural ? "this alias" : "these aliases"}, which is the point: pushing would overwrite their edit.
71474
+ Merge what you want from the live copy into your file and deploy, or run 'lotics app pull ${appId} --force' to take the live version and discard yours.`
71475
+ );
71476
+ }
71477
+ function parkLiveWorkflowBody(projectDir, alias, source) {
71478
+ const dir = path10.join(projectDir, WORKFLOW_GLOBALS_DIR);
71479
+ const file2 = path10.join(dir, `${alias}.live.ts`);
71480
+ try {
71481
+ fs8.mkdirSync(dir, { recursive: true });
71482
+ fs8.writeFileSync(file2, normalizeWorkflowBody(source) + "\n");
71483
+ return path10.relative(projectDir, file2);
71484
+ } catch (err2) {
71485
+ console.error(
71486
+ `\u26A0 Could not write the live body for ${alias} to ${path10.relative(projectDir, file2)}: ${err2 instanceof Error ? err2.message : String(err2)}`
71487
+ );
71488
+ return null;
71489
+ }
71490
+ }
71273
71491
  function localPromptUnpushed(projectDir, alias, live) {
71274
71492
  const file2 = agentFilePath2(projectDir, alias);
71275
71493
  if (!fs8.existsSync(file2)) return false;
71276
- const onDisk = fs8.readFileSync(file2, "utf-8");
71494
+ const onDisk = stripAgentHeader(fs8.readFileSync(file2, "utf-8"));
71495
+ if (onDisk === normalizeAgentInstructions(live)) return false;
71277
71496
  const baseline = readSynced(projectDir).agents[alias]?.content;
71278
- if (baseline === void 0) return stripAgentHeader(onDisk).trim() !== live.trim();
71279
- return contentSha(stripAgentHeader(onDisk)) !== baseline;
71497
+ if (baseline === void 0) return true;
71498
+ return contentSha(onDisk) !== baseline;
71280
71499
  }
71281
71500
  function generatedWorkflowDescription(appName, alias) {
71282
71501
  return `Workflow invoked by app "${appName}" via useWorkflow("${alias}").`;
@@ -71285,14 +71504,15 @@ function localBodyUnpushed(projectDir, alias, live) {
71285
71504
  const file2 = workflowFilePath(projectDir, alias);
71286
71505
  if (!fs8.existsSync(file2)) return false;
71287
71506
  const onDisk = fs8.readFileSync(file2, "utf-8");
71507
+ if (stripWorkflowHeader(onDisk).trim() === normalizeWorkflowBody(live).trim()) return false;
71288
71508
  const baseline = readSynced(projectDir).workflows[alias]?.content;
71289
- if (baseline === void 0) {
71290
- return stripWorkflowHeader(onDisk).trim() !== normalizeWorkflowBody(live).trim();
71291
- }
71509
+ if (baseline === void 0) return true;
71292
71510
  return contentSha(stripWorkflowHeader(onDisk)) !== baseline;
71293
71511
  }
71294
71512
  async function writeWorkflowFiles(client, projectDir, app_id, appName, workflows, opts) {
71295
71513
  const written = [];
71514
+ const kept = [];
71515
+ const unseenLive = [];
71296
71516
  for (const [alias, declaration] of Object.entries(workflows)) {
71297
71517
  const res = await client.getAppWorkflow(app_id, alias);
71298
71518
  if (res.error && !isMissingWorkflowBodyError(res.error)) {
@@ -71317,7 +71537,16 @@ async function writeWorkflowFiles(client, projectDir, app_id, appName, workflows
71317
71537
  toWorkflowDtsDeclaration(declaration)
71318
71538
  );
71319
71539
  if (!opts.overwrite && localBodyUnpushed(projectDir, alias, source)) {
71320
- opts.kept.push(`src/workflows/${alias}.ts`);
71540
+ kept.push(`src/workflows/${alias}.ts`);
71541
+ const previous = readSynced(projectDir).workflows[alias];
71542
+ if (body_sha && (previous?.live === void 0 || previous.live === body_sha)) {
71543
+ writeSynced(projectDir, "workflows", alias, { ...previous, live: body_sha });
71544
+ } else if (body_sha) {
71545
+ unseenLive.push({
71546
+ alias,
71547
+ parked: parkLiveWorkflowBody(projectDir, alias, source)
71548
+ });
71549
+ }
71321
71550
  continue;
71322
71551
  }
71323
71552
  writeWorkflowFile(projectDir, alias, source, envelope2);
@@ -71341,7 +71570,7 @@ async function writeWorkflowFiles(client, projectDir, app_id, appName, workflows
71341
71570
  }
71342
71571
  written.push(alias);
71343
71572
  }
71344
- return written;
71573
+ return { written, kept, unseenLive };
71345
71574
  }
71346
71575
  function writeWorkflowDescription(projectDir, alias, description) {
71347
71576
  const pkgPath2 = path10.join(projectDir, "package.json");
@@ -72041,15 +72270,21 @@ async function hydrateAppProject(client, targetPath, app, opts) {
72041
72270
  );
72042
72271
  const workflows = app.workflows ?? {};
72043
72272
  if (Object.keys(workflows).length > 0) {
72044
- const written = await writeWorkflowFiles(client, targetPath, app.id, app.name, workflows, {
72045
- overwrite: opts.force === true,
72046
- kept: opts.kept
72047
- });
72273
+ const { written, kept, unseenLive } = await writeWorkflowFiles(
72274
+ client,
72275
+ targetPath,
72276
+ app.id,
72277
+ app.name,
72278
+ workflows,
72279
+ { overwrite: opts.force === true }
72280
+ );
72281
+ opts.kept.push(...kept);
72048
72282
  if (written.length > 0) {
72049
72283
  console.error(
72050
72284
  `Wrote ${written.length} workflow ${written.length === 1 ? "body" : "bodies"} to ${WORKFLOWS_DIR}/ (${written.join(", ")})`
72051
72285
  );
72052
72286
  }
72287
+ reportUnseenLive("workflow body", unseenLive, app.id);
72053
72288
  }
72054
72289
  const agents = app.agents ?? {};
72055
72290
  if (Object.keys(agents).length > 0) {
@@ -72057,17 +72292,31 @@ async function hydrateAppProject(client, targetPath, app, opts) {
72057
72292
  overwrite: opts.force === true,
72058
72293
  kept: opts.kept
72059
72294
  });
72060
- for (const alias of written) {
72061
- const instructions = agents[alias]?.instructions;
72062
- if (instructions === void 0) continue;
72063
- const sha = contentSha(instructions);
72064
- writeSynced(targetPath, "agents", alias, { content: sha, live: sha });
72295
+ const unseenLiveProse = [];
72296
+ for (const [alias, declaration] of Object.entries(agents)) {
72297
+ const instructions = declaration?.instructions;
72298
+ if (typeof instructions !== "string") continue;
72299
+ const live = contentSha(instructions);
72300
+ if (written.includes(alias)) {
72301
+ writeSynced(targetPath, "agents", alias, {
72302
+ content: contentSha(normalizeAgentInstructions(instructions)),
72303
+ live
72304
+ });
72305
+ continue;
72306
+ }
72307
+ const previous = readSynced(targetPath).agents[alias];
72308
+ if (previous?.live === void 0 || previous.live === live) {
72309
+ writeSynced(targetPath, "agents", alias, { ...previous, live });
72310
+ continue;
72311
+ }
72312
+ unseenLiveProse.push({ alias, parked: parkLiveAgentProse(targetPath, alias, instructions) });
72065
72313
  }
72066
72314
  if (written.length > 0) {
72067
72315
  console.error(
72068
72316
  `Wrote ${written.length} agent instruction ${written.length === 1 ? "file" : "files"} to ${AGENTS_DIR}/ (${written.join(", ")})`
72069
72317
  );
72070
72318
  }
72319
+ reportUnseenLive("prompt", unseenLiveProse, app.id);
72071
72320
  }
72072
72321
  await runNpmInstall(targetPath);
72073
72322
  writeAppDtsKitLinks(targetPath);
@@ -72090,16 +72339,17 @@ async function appPull(client, args) {
72090
72339
  console.error(`Downloading source archive...`);
72091
72340
  await downloadToFile(sourceUrl, tmpFile);
72092
72341
  const keptLocal = [];
72342
+ const restoredFromArchive = [];
72093
72343
  const stagingDir = fs8.mkdtempSync(path10.join(tmpdir(), `lotics-pull-${app.id}-`));
72094
72344
  try {
72095
72345
  await runTar(["-xzf", tmpFile, "-C", stagingDir], stagingDir);
72096
72346
  console.error(`Extracting to ${targetPath}...`);
72097
- keptLocal.push(
72098
- ...copyTree(stagingDir, targetPath, {
72099
- overwrite: args.force === true,
72100
- skip: [WORKFLOWS_DIR, AGENTS_DIR].map((d) => d.split(path10.sep).join("/"))
72101
- })
72102
- );
72347
+ const copied = copyTree(stagingDir, targetPath, {
72348
+ overwrite: args.force === true,
72349
+ skip: [WORKFLOWS_DIR, AGENTS_DIR].map((d) => d.split(path10.sep).join("/"))
72350
+ });
72351
+ keptLocal.push(...copied.kept);
72352
+ restoredFromArchive.push(...copied.created);
72103
72353
  } finally {
72104
72354
  if (fs8.existsSync(tmpFile)) fs8.unlinkSync(tmpFile);
72105
72355
  fs8.rmSync(stagingDir, { recursive: true, force: true });
@@ -72114,6 +72364,7 @@ async function appPull(client, args) {
72114
72364
  ...args.force !== void 0 ? { force: args.force } : {},
72115
72365
  kept: keptLocal
72116
72366
  });
72367
+ reportRestoredFiles(restoredFromArchive, prior !== null);
72117
72368
  reportKeptLocalFiles(keptLocal, app.id, {
72118
72369
  // `prior` is non-null whenever the stamp was held — `stampAfterPull` cannot
72119
72370
  // hold against nothing — so the fallback only covers the un-held branch,
@@ -72275,7 +72526,8 @@ async function appDeploy(client, args) {
72275
72526
  if (args.prune) {
72276
72527
  await pruneOrphanedBindings(
72277
72528
  client,
72278
- meta3.app_id,
72529
+ projectDir,
72530
+ { app_id: meta3.app_id },
72279
72531
  orphanedBindings(called, liveAfter),
72280
72532
  called.dynamic
72281
72533
  );
@@ -72443,7 +72695,7 @@ function driftedAgentAliases(projectDir, liveAgents) {
72443
72695
  const file2 = agentFilePath2(projectDir, alias);
72444
72696
  if (!fs8.existsSync(file2)) continue;
72445
72697
  const local = stripAgentHeader(fs8.readFileSync(file2, "utf-8"));
72446
- if (local === "" || local === (live.instructions ?? "")) continue;
72698
+ if (local === "" || local === normalizeAgentInstructions(live.instructions ?? "")) continue;
72447
72699
  drifted.push(alias);
72448
72700
  }
72449
72701
  return drifted.sort();
@@ -72505,28 +72757,31 @@ function warnAboutOrphanedBindings(called, live) {
72505
72757
  );
72506
72758
  return lines.length;
72507
72759
  }
72508
- async function pruneOrphanedBindings(client, appId, orphans, dynamic) {
72760
+ async function pruneOrphanedBindings(client, projectDir, app, orphans, dynamic) {
72509
72761
  const targets = [
72510
72762
  ...orphans.queries.map((alias) => ({
72511
72763
  tool: "remove_app_query",
72764
+ kind: "queries",
72512
72765
  alias,
72513
72766
  label: `query ${alias}`,
72514
- restore: `lotics app query set ${alias}`
72767
+ setCommand: `lotics app query set ${alias}`
72515
72768
  })),
72516
72769
  ...orphans.workflows.map((alias) => ({
72517
72770
  tool: "remove_app_workflow",
72771
+ kind: "workflows",
72518
72772
  alias,
72519
72773
  label: `workflow ${alias}`,
72520
- restore: `lotics app workflow set ${alias}`
72774
+ setCommand: `lotics app workflow set ${alias}`
72521
72775
  })),
72522
72776
  ...orphans.agents.map((alias) => ({
72523
72777
  tool: "remove_app_agent",
72778
+ kind: "agents",
72524
72779
  alias,
72525
72780
  label: `agent ${alias}`,
72526
- restore: `lotics app agent set ${alias}`
72781
+ setCommand: `lotics app agent set ${alias}`
72527
72782
  }))
72528
72783
  ];
72529
- if (targets.length === 0) return 0;
72784
+ if (targets.length === 0) return;
72530
72785
  if (dynamic.length > 0) {
72531
72786
  console.error(
72532
72787
  `
@@ -72534,19 +72789,63 @@ async function pruneOrphanedBindings(client, appId, orphans, dynamic) {
72534
72789
  ` + targets.map((t) => ` \u2022 ${t.label}`).join("\n") + `
72535
72790
  Make those call sites literal and the next deploy will clean them up, or remove the ones you know are dead by hand.`
72536
72791
  );
72537
- return targets.length;
72792
+ return;
72538
72793
  }
72539
- let failed = 0;
72794
+ const tablesBefore = new Set(resolveCodegenTableIds(projectDir, readAppMeta(projectDir).queries ?? {}));
72795
+ let removedLocally = false;
72540
72796
  for (const target of targets) {
72541
- const res = await client.execute(target.tool, { app_id: appId, alias: target.alias });
72797
+ const res = await client.execute(target.tool, { app_id: app.app_id, alias: target.alias });
72542
72798
  if (res.error) {
72543
- failed += 1;
72544
72799
  console.error(` \u2717 could not unbind ${target.label} \u2014 ${res.error}`);
72545
72800
  continue;
72546
72801
  }
72547
- console.error(` \u2713 unbound ${target.label} \u2014 restore: ${target.restore}`);
72802
+ let line;
72803
+ try {
72804
+ const entry = readManifestEntry(projectDir, target.kind, target.alias);
72805
+ if (entry.found) {
72806
+ const parked = parkPrunedDeclaration(projectDir, target.kind, target.alias, {
72807
+ declaration: entry.declaration,
72808
+ synced: entry.synced
72809
+ });
72810
+ removeFromManifest(projectDir, target.kind, target.alias);
72811
+ removedLocally = true;
72812
+ line = ` \u2713 unbound ${target.label} + removed package.json#lotics.${target.kind}.${target.alias} \u2014 restore: put ${parked} back under lotics.${target.kind}.${target.alias}, then ${target.setCommand}`;
72813
+ } else {
72814
+ line = ` \u2713 unbound ${target.label} \u2014 nothing was declared for it in package.json, so there is nothing to restore from this project (recover it with lotics app pull --from-version <apv_\u2026>)`;
72815
+ }
72816
+ } catch (err2) {
72817
+ console.error(
72818
+ ` \u2713 unbound ${target.label} server-side, but could not update the local project \u2014 ${err2 instanceof Error ? err2.message : String(err2)}. lotics.${target.kind}.${target.alias} is still declared: remove it by hand, or the next deploy re-creates the binding.`
72819
+ );
72820
+ continue;
72821
+ }
72822
+ console.error(line);
72823
+ }
72824
+ if (removedLocally) {
72825
+ try {
72826
+ const meta3 = readAppMeta(projectDir);
72827
+ writeAppDts(projectDir, {
72828
+ workflows: meta3.workflows,
72829
+ queries: meta3.queries,
72830
+ agents: meta3.agents
72831
+ });
72832
+ if (orphans.queries.length > 0) {
72833
+ const dropped = [...tablesBefore].filter(
72834
+ (id) => !resolveCodegenTableIds(projectDir, meta3.queries ?? {}).includes(id)
72835
+ );
72836
+ if (dropped.length > 0) {
72837
+ console.error(
72838
+ ` \u26A0 .lotics/app_fields.ts no longer covers ${dropped.join(", ")} \u2014 ${dropped.length === 1 ? "that table was" : "those tables were"} named only by the pruned quer${orphans.queries.length === 1 ? "y" : "ies"}. If your source still uses F/OPT for ${dropped.length === 1 ? "it" : "them"}, add ${dropped.map((id) => `"${id}"`).join(", ")} to package.json#lotics.codegen.tables and re-run lotics app codegen.`
72839
+ );
72840
+ }
72841
+ await writeGeneratedAppFields(client, projectDir, app, meta3.queries ?? {});
72842
+ }
72843
+ } catch (err2) {
72844
+ console.error(
72845
+ ` \u2717 could not refresh the generated companions after pruning \u2014 ${err2 instanceof Error ? err2.message : String(err2)}. Run lotics app codegen.`
72846
+ );
72847
+ }
72548
72848
  }
72549
- return failed;
72550
72849
  }
72551
72850
  function derivedDeployMessage(pending) {
72552
72851
  const parts = [
@@ -72558,6 +72857,7 @@ function derivedDeployMessage(pending) {
72558
72857
  }
72559
72858
  function copyTree(from, to, opts, relative = "") {
72560
72859
  const kept = [];
72860
+ const created = [];
72561
72861
  for (const entry of fs8.readdirSync(from, { withFileTypes: true })) {
72562
72862
  const src = path10.join(from, entry.name);
72563
72863
  const dest = path10.join(to, entry.name);
@@ -72565,17 +72865,32 @@ function copyTree(from, to, opts, relative = "") {
72565
72865
  if (opts.skip?.includes(rel.split(path10.sep).join("/"))) continue;
72566
72866
  if (entry.isDirectory()) {
72567
72867
  fs8.mkdirSync(dest, { recursive: true });
72568
- kept.push(...copyTree(src, dest, opts, rel));
72868
+ const child = copyTree(src, dest, opts, rel);
72869
+ kept.push(...child.kept);
72870
+ created.push(...child.created);
72569
72871
  continue;
72570
72872
  }
72571
- if (!opts.overwrite && fs8.existsSync(dest)) {
72873
+ const existed = fs8.existsSync(dest);
72874
+ if (!opts.overwrite && existed) {
72572
72875
  if (fs8.readFileSync(src).equals(fs8.readFileSync(dest))) continue;
72573
72876
  kept.push(rel);
72574
72877
  continue;
72575
72878
  }
72576
72879
  fs8.copyFileSync(src, dest);
72880
+ if (!existed) created.push(rel);
72577
72881
  }
72578
- return kept;
72882
+ return { kept, created };
72883
+ }
72884
+ function reportRestoredFiles(restored, isRefresh) {
72885
+ if (!isRefresh || restored.length === 0) return;
72886
+ const shown = restored.slice(0, 10);
72887
+ console.error(
72888
+ `
72889
+ Restored ${restored.length} file${restored.length === 1 ? "" : "s"} the app's last deployed source carries and your tree did not:
72890
+ ` + shown.map((f) => ` \u2022 ${f}`).join("\n") + (restored.length > shown.length ? `
72891
+ \u2022 \u2026and ${restored.length - shown.length} more` : "") + `
72892
+ A pull mirrors the last DEPLOYED source, so a file you deleted here comes back until the deletion ships. Delete it again and run 'lotics app deploy' to make it stick.`
72893
+ );
72579
72894
  }
72580
72895
  function reportKeptLocalFiles(kept, appId, motion) {
72581
72896
  if (kept.length === 0) return;
@@ -73135,15 +73450,19 @@ async function appWorkflowPull(client, args = {}) {
73135
73450
  console.error(`App ${meta3.app_id} has no bound workflows.`);
73136
73451
  return;
73137
73452
  }
73138
- const kept = [];
73139
- const written = await writeWorkflowFiles(client, projectDir, meta3.app_id, app.name, workflows, {
73140
- overwrite: args.force === true,
73141
- kept
73142
- });
73453
+ const { written, kept, unseenLive } = await writeWorkflowFiles(
73454
+ client,
73455
+ projectDir,
73456
+ meta3.app_id,
73457
+ app.name,
73458
+ workflows,
73459
+ { overwrite: args.force === true }
73460
+ );
73143
73461
  console.error(
73144
73462
  `Wrote ${written.length} workflow ${written.length === 1 ? "body" : "bodies"} to ${WORKFLOWS_DIR}/` + (written.length > 0 ? ` (${written.join(", ")})` : "")
73145
73463
  );
73146
73464
  reportKeptLocalFiles(kept, meta3.app_id, null);
73465
+ reportUnseenLive("workflow body", unseenLive, meta3.app_id);
73147
73466
  ensureAppTsconfig(projectDir);
73148
73467
  }
73149
73468
  async function appWorkflowCheck(args) {
@@ -73291,6 +73610,23 @@ function parseArgs(argv) {
73291
73610
  let i2 = 0;
73292
73611
  while (i2 < argv.length) {
73293
73612
  const arg = argv[i2];
73613
+ if (subcommand !== void 0 && commandOwnsTail(command) && arg !== "--help" && arg !== "-h") {
73614
+ const local = subcommandArgs(command, subcommand);
73615
+ const looksLikeFlag = arg.length > 1 && arg.startsWith("-");
73616
+ const declared = looksLikeFlag && local !== void 0 && matchesKnownFlag([...local.valueFlags ?? [], ...local.flags ?? []], arg);
73617
+ if (declared) {
73618
+ restArgs.push(arg);
73619
+ if ((local?.valueFlags ?? []).includes(arg) && i2 + 1 < argv.length) restArgs.push(argv[++i2]);
73620
+ } else if (looksLikeFlag && local?.content !== true) {
73621
+ unknownFlag = arg;
73622
+ } else if (!toolArgs) {
73623
+ toolArgs = arg;
73624
+ } else {
73625
+ restArgs.push(arg);
73626
+ }
73627
+ i2++;
73628
+ continue;
73629
+ }
73294
73630
  switch (arg) {
73295
73631
  case "--json":
73296
73632
  flags.json = true;
@@ -73715,11 +74051,13 @@ var CliError = class extends Error {
73715
74051
  function fail2(message2) {
73716
74052
  throw new CliError(message2);
73717
74053
  }
73718
- function rejectUnknownFlags(rest2, known, usage) {
73719
- const matches = (a) => known.some((k) => k.endsWith("=") ? a.startsWith(k) : a === k);
73720
- const unknown2 = rest2.filter((a) => a.startsWith("--") && !matches(a));
74054
+ function rejectUnknownFlags(rest2, command, subcommand) {
74055
+ const known = knownSubcommandFlags(command, subcommand);
74056
+ const unknown2 = rest2.filter((a) => a.startsWith("--") && !matchesKnownFlag(known, a));
73721
74057
  if (unknown2.length === 0) return;
73722
- fail2(`Unknown ${unknown2.length > 1 ? "flags" : "flag"} for ${usage}: ${unknown2.join(", ")}. Supported: ${known.join(", ")}.`);
74058
+ fail2(
74059
+ `Unknown ${unknown2.length > 1 ? "flags" : "flag"} for ${command} ${subcommand}: ${unknown2.join(", ")}. Supported: ${known.join(", ")}.`
74060
+ );
73723
74061
  }
73724
74062
  function rejectExtraArgs(rest2, arity, usage) {
73725
74063
  if (rest2.length <= arity) return;
@@ -94808,6 +95146,37 @@ function collectRecomputeReport(workbook, unreadable) {
94808
95146
  return { total, unevaluated };
94809
95147
  }
94810
95148
 
95149
+ // ../xlsx/src/numfmt_type.ts
95150
+ function classifyNumFmt(code) {
95151
+ if (!code || code === "General") return "general";
95152
+ const first3 = code.split(";")[0] ?? code;
95153
+ const currency = hasCurrencyTokens(first3);
95154
+ const stripped = stripQuotedLiterals(first3);
95155
+ if (stripped.trim() === "@") return "text";
95156
+ if (hasDateTokens(stripped)) return "date";
95157
+ if (stripped.includes("%")) return "percent";
95158
+ if (currency) return "currency";
95159
+ if (/[#0?]/.test(stripped)) return "numeric";
95160
+ return "general";
95161
+ }
95162
+ function stripQuotedLiterals(code) {
95163
+ return code.replace(/"[^"]*"/g, "").replace(/\\./g, " ");
95164
+ }
95165
+ function hasDateTokens(code) {
95166
+ if (/\b(y{1,4}|d{1,4}|h{1,2}|s{1,2}|am\/pm|a\/p)\b/i.test(code)) return true;
95167
+ if (/[ymdhs]\s*[-\/.:\s]\s*m/i.test(code)) return true;
95168
+ if (/m\s*[-\/.:\s]\s*[ymdhs]/i.test(code)) return true;
95169
+ return false;
95170
+ }
95171
+ function hasCurrencyTokens(code) {
95172
+ if (/[$¥€£₫¢]/.test(code)) return true;
95173
+ if (/\[\$/.test(code)) return true;
95174
+ return false;
95175
+ }
95176
+ function isPaddedIdentifier(input) {
95177
+ return /^[+-]?0\d/.test(input);
95178
+ }
95179
+
94811
95180
  // ../xlsx/src/clipboard.ts
94812
95181
  function adjustFormulaRefsForRowShift(formula, atRow, delta) {
94813
95182
  return formula.replace(
@@ -94835,17 +95204,18 @@ function adjustFormulaRefsForColShift(formula, atCol, delta) {
94835
95204
  }
94836
95205
 
94837
95206
  // ../xlsx/src/spreadsheet_commands.ts
94838
- function setCellValueCommand(workbook, engine, sheetIndex, row, col, newValue) {
95207
+ function setCellValueCommand(workbook, engine, sheetIndex, row, col, newValue, options) {
94839
95208
  const sheet = workbook.sheets[sheetIndex];
94840
95209
  const ref2 = rowColToRef(row, col);
94841
95210
  const oldCell = sheet.getCell(ref2);
94842
95211
  const oldValue = oldCell?.value ?? null;
94843
95212
  const oldFormula = oldCell?.formula;
94844
- const isFormula = newValue.startsWith("=");
94845
- const formula = isFormula ? newValue.slice(1) : void 0;
94846
- const value2 = isFormula ? null : parseInputValue(newValue);
94847
95213
  const styleIndex = oldCell?.styleIndex ?? 0;
94848
95214
  const numFmtCode = oldCell?.numFmtCode ?? "General";
95215
+ const isText = typeof newValue === "string" && options?.typed !== true;
95216
+ const isFormula = isText && newValue.startsWith("=");
95217
+ const formula = isFormula ? newValue.slice(1) : void 0;
95218
+ const value2 = isFormula ? null : isText ? parseInputValue(newValue, numFmtCode) : newValue;
94849
95219
  return {
94850
95220
  description: `Set ${ref2} = ${newValue}`,
94851
95221
  execute() {
@@ -95384,6 +95754,43 @@ function deleteColsCommand(workbook, sheetIndex, at2, count) {
95384
95754
  }
95385
95755
  };
95386
95756
  }
95757
+ function setRangeNumFmtCommand(workbook, sheetIndex, startRow, startCol, endRow, endCol, numFmtCode) {
95758
+ const sheet = workbook.sheets[sheetIndex];
95759
+ const snapshot = /* @__PURE__ */ new Map();
95760
+ for (let r = startRow; r <= endRow; r++) {
95761
+ for (let c = startCol; c <= endCol; c++) {
95762
+ const ref2 = rowColToRef(r, c);
95763
+ const cell = sheet.getCell(ref2);
95764
+ snapshot.set(ref2, cell ? { value: cell.value, styleIndex: cell.styleIndex, numFmtCode: cell.numFmtCode, formula: cell.formula } : null);
95765
+ }
95766
+ }
95767
+ const rangeRef = `${rowColToRef(startRow, startCol)}:${rowColToRef(endRow, endCol)}`;
95768
+ return {
95769
+ description: `Format ${rangeRef} as ${numFmtCode}`,
95770
+ execute() {
95771
+ for (let r = startRow; r <= endRow; r++) {
95772
+ for (let c = startCol; c <= endCol; c++) {
95773
+ const ref2 = rowColToRef(r, c);
95774
+ const cell = sheet.getCell(ref2);
95775
+ if (cell) {
95776
+ sheet.set(ref2, cell.value, sheet.styles?.get(cell.styleIndex), numFmtCode, cell.formula);
95777
+ } else {
95778
+ sheet.set(ref2, null, void 0, numFmtCode);
95779
+ }
95780
+ }
95781
+ }
95782
+ },
95783
+ undo() {
95784
+ for (const [ref2, snap] of snapshot) {
95785
+ if (snap) {
95786
+ sheet.set(ref2, snap.value, sheet.styles?.get(snap.styleIndex), snap.numFmtCode, snap.formula);
95787
+ } else {
95788
+ sheet.deleteCell(ref2);
95789
+ }
95790
+ }
95791
+ }
95792
+ };
95793
+ }
95387
95794
  function addSheetCommand(workbook, name2) {
95388
95795
  return {
95389
95796
  description: `Add sheet ${name2}`,
@@ -95426,10 +95833,23 @@ function renameSheetCommand(workbook, index, newName) {
95426
95833
  }
95427
95834
  };
95428
95835
  }
95429
- function parseInputValue(input) {
95836
+ function batchCommand(description, commands) {
95837
+ return {
95838
+ description,
95839
+ execute() {
95840
+ for (const cmd of commands) cmd.execute();
95841
+ },
95842
+ undo() {
95843
+ for (let i2 = commands.length - 1; i2 >= 0; i2--) commands[i2].undo();
95844
+ }
95845
+ };
95846
+ }
95847
+ function parseInputValue(input, numFmtCode) {
95430
95848
  if (input === "") return null;
95849
+ if (classifyNumFmt(numFmtCode) === "text") return input;
95431
95850
  if (input.toUpperCase() === "TRUE") return true;
95432
95851
  if (input.toUpperCase() === "FALSE") return false;
95852
+ if (isPaddedIdentifier(input.trim())) return input;
95433
95853
  const num3 = Number(input);
95434
95854
  if (!isNaN(num3) && input.trim() !== "") return num3;
95435
95855
  return input;
@@ -95702,21 +96122,20 @@ function applySheetCells(workbook, sheetIndex, def) {
95702
96122
  const sheet = workbook.sheets[sheetIndex];
95703
96123
  if (def.cells) {
95704
96124
  for (const [ref2, raw] of Object.entries(def.cells)) {
95705
- const cell = normalizeSimpleCell(raw, def.name, ref2);
95706
- const coord = refToRowCol(ref2.toUpperCase());
96125
+ const cell = normalizeSimpleCell(raw, `sheet "${def.name}" cell ${ref2}`);
96126
+ const upper2 = ref2.toUpperCase();
96127
+ const coord = refToRowCol(upper2);
95707
96128
  if (!coord) fail2(`Invalid cell ref in sheet "${def.name}": ${ref2}`);
95708
- const stringValue = cell.formula ? `=${cell.formula}` : valueToInput(cell.value);
95709
- const cmd = setCellValueCommand(workbook, void 0, sheetIndex, coord.row, coord.col, stringValue);
95710
- cmd.execute();
96129
+ sheet.set(
96130
+ upper2,
96131
+ cell.formula ? null : cell.value ?? null,
96132
+ void 0,
96133
+ cell.numFmt,
96134
+ cell.formula ?? void 0
96135
+ );
95711
96136
  if (cell.style) {
95712
96137
  setRangeStyleCommand(workbook, sheetIndex, coord.row, coord.col, coord.row, coord.col, cell.style).execute();
95713
96138
  }
95714
- if (cell.numFmt !== void 0) {
95715
- const upper2 = ref2.toUpperCase();
95716
- const target = sheet.getCell(upper2);
95717
- if (target) target.numFmtCode = cell.numFmt;
95718
- else sheet.set(upper2, null, cell.style, cell.numFmt);
95719
- }
95720
96139
  }
95721
96140
  }
95722
96141
  if (def.merges) {
@@ -95744,22 +96163,33 @@ function applySheetCells(workbook, sheetIndex, def) {
95744
96163
  }
95745
96164
  }
95746
96165
  }
95747
- function normalizeSimpleCell(raw, sheetName, ref2) {
96166
+ function normalizeSimpleCell(raw, label) {
95748
96167
  if (raw && typeof raw === "object" && !Array.isArray(raw)) {
95749
96168
  const unknown2 = Object.keys(raw).filter((k) => !SIMPLE_CELL_KEYS.has(k));
95750
96169
  if (unknown2.length > 0) {
95751
96170
  fail2(
95752
- `Unknown propert${unknown2.length > 1 ? "ies" : "y"} on sheet "${sheetName}" cell ${ref2}: ${unknown2.join(", ")}. Supported: ${[...SIMPLE_CELL_KEYS].join(", ")}.`
96171
+ `Unknown propert${unknown2.length > 1 ? "ies" : "y"} on ${label}: ${unknown2.join(", ")}. Supported: ${[...SIMPLE_CELL_KEYS].join(", ")}.`
95753
96172
  );
95754
96173
  }
96174
+ if (raw.formula === void 0 || raw.formula === null) {
96175
+ if (typeof raw.value === "string" && raw.value.startsWith("=")) {
96176
+ console.error(
96177
+ `Warning: ${label} was written as the text ${JSON.stringify(raw.value)}. An object cell's "value" is a literal; write {"formula": ${JSON.stringify(raw.value.slice(1))}} or the bare string ${JSON.stringify(raw.value)} for a formula.`
96178
+ );
96179
+ }
96180
+ }
95755
96181
  return raw;
95756
96182
  }
96183
+ if (raw !== null && !["string", "number", "boolean"].includes(typeof raw)) {
96184
+ fail2(
96185
+ `Value of ${label} must be a string, number, boolean, null, or a { ${[...SIMPLE_CELL_KEYS].join(", ")} } object, got ${JSON.stringify(raw) ?? typeof raw}.`
96186
+ );
96187
+ }
96188
+ if (typeof raw === "string" && raw.startsWith("=")) {
96189
+ return { formula: raw.slice(1) };
96190
+ }
95757
96191
  return { value: raw };
95758
96192
  }
95759
- function valueToInput(value2) {
95760
- if (value2 === null || value2 === void 0) return "";
95761
- return String(value2);
95762
- }
95763
96193
  function readOptionFlag(rest2, name2) {
95764
96194
  const i2 = rest2.indexOf(name2);
95765
96195
  if (i2 < 0) return void 0;
@@ -95774,7 +96204,6 @@ function splitOptionalSheetRef(spec) {
95774
96204
  }
95775
96205
  function xlsxRead(filePath, rest2) {
95776
96206
  if (!filePath) fail2("Usage: lotics xlsx read <file> [--sheet <name>] [--range <sheet>!<A1:G60>] [--with-format]");
95777
- rejectUnknownFlags(rest2, ["--sheet", "--range", "--with-format"], "xlsx read");
95778
96207
  const rangeArg = readOptionFlag(rest2, "--range");
95779
96208
  const sheetArg = readOptionFlag(rest2, "--sheet");
95780
96209
  const withFormat = rest2.includes("--with-format");
@@ -95984,7 +96413,29 @@ function buildBatchCommand(workbook, engine, op, index) {
95984
96413
  const sheetIndex = resolveSheetIndex(workbook, op.sheet);
95985
96414
  const coord = refToRowCol(op.ref.toUpperCase());
95986
96415
  if (!coord) fail2(`[op ${index}] Invalid cell ref: ${op.ref}`);
95987
- return setCellValueCommand(workbook, engine.get(), sheetIndex, coord.row, coord.col, op.value);
96416
+ const cell = normalizeSimpleCell(op.value, `[op ${index}] set-cell ${op.sheet}!${op.ref}`);
96417
+ const commands = [
96418
+ cell.formula ? (
96419
+ // The entry path is the only one that writes a formula, and it takes
96420
+ // it spelled the way a person types it. Re-adding the `=` this
96421
+ // surface's reader just stripped is lossless and keeps the formula
96422
+ // write in one place.
96423
+ setCellValueCommand(workbook, engine.get(), sheetIndex, coord.row, coord.col, `=${cell.formula}`)
96424
+ ) : setCellValueCommand(workbook, engine.get(), sheetIndex, coord.row, coord.col, cell.value ?? null, {
96425
+ typed: true
96426
+ })
96427
+ ];
96428
+ if (cell.numFmt !== void 0) {
96429
+ commands.push(
96430
+ setRangeNumFmtCommand(workbook, sheetIndex, coord.row, coord.col, coord.row, coord.col, cell.numFmt)
96431
+ );
96432
+ }
96433
+ if (cell.style) {
96434
+ commands.push(
96435
+ setRangeStyleCommand(workbook, sheetIndex, coord.row, coord.col, coord.row, coord.col, cell.style)
96436
+ );
96437
+ }
96438
+ return commands.length === 1 ? commands[0] : batchCommand(`Set ${op.sheet}!${op.ref}`, commands);
95988
96439
  }
95989
96440
  case "clear-range": {
95990
96441
  const sheetIndex = resolveSheetIndex(workbook, op.sheet);
@@ -96041,6 +96492,8 @@ Uses Lotics' own xlsx engine; round-trips faithfully with the Lotics editor and
96041
96492
  lotics xlsx write <file> '<json>' Create .xlsx from {"sheets":[{"name","cells":{"A1":...}}]}
96042
96493
  A cell is a bare value, or {value|formula, numFmt, style}:
96043
96494
  {"A1":{"value":1234,"numFmt":"#,##0 \\"\u20AB\\"","style":{"fontBold":true}}}
96495
+ A bare "=A1+B1" is a formula; an object's "value" is
96496
+ always a literal, so use {"formula":"A1+B1"} there.
96044
96497
  A sheet also takes merges, freeze, colWidths ({"A":34}),
96045
96498
  rowHeights ({"1":44}). Unknown properties are rejected.
96046
96499
  lotics xlsx set-cell <file> <sheet>!<ref> '<v>' Set cell value (prefix '=' for formula)
@@ -101655,7 +102108,6 @@ function buildDoc(input) {
101655
102108
  }
101656
102109
  async function docxRead(filePath, rest2) {
101657
102110
  if (!filePath) fail2("Usage: lotics docx read <file> [--include-opaque]");
101658
- rejectUnknownFlags(rest2, ["--include-opaque"], "docx read");
101659
102111
  const includeOpaque = rest2.includes("--include-opaque");
101660
102112
  const doc = await loadFile2(filePath);
101661
102113
  console.log(JSON.stringify(readToJson2(doc, includeOpaque), null, 2));
@@ -101690,7 +102142,7 @@ function firstPositional(rest2) {
101690
102142
  return rest2.find((a) => !a.startsWith("--"));
101691
102143
  }
101692
102144
  async function docxAppendParagraph(filePath, rest2) {
101693
- rejectUnknownFlags(rest2, ["--style="], "docx append-paragraph");
102145
+ rejectUnknownFlags(rest2, "docx", "append-paragraph");
101694
102146
  const text = firstPositional(rest2);
101695
102147
  if (!filePath || text === void 0) fail2("Usage: lotics docx append-paragraph <file> '<text>' [--style=NAME]");
101696
102148
  const style = getStyleFlag(rest2);
@@ -101700,7 +102152,7 @@ async function docxAppendParagraph(filePath, rest2) {
101700
102152
  await writeDoc(filePath, replaceChildren(doc, newChildren));
101701
102153
  }
101702
102154
  async function docxInsertParagraph(filePath, rest2) {
101703
- rejectUnknownFlags(rest2, ["--style=", "--at="], "docx insert-paragraph");
102155
+ rejectUnknownFlags(rest2, "docx", "insert-paragraph");
101704
102156
  const text = firstPositional(rest2);
101705
102157
  const at2 = getAtFlag(rest2);
101706
102158
  if (!filePath || text === void 0 || at2 === void 0) {
@@ -101717,7 +102169,6 @@ async function docxInsertParagraph(filePath, rest2) {
101717
102169
  await writeDoc(filePath, replaceChildren(doc, newChildren));
101718
102170
  }
101719
102171
  async function docxDeleteBlock(filePath, rest2) {
101720
- rejectUnknownFlags(rest2, ["--at="], "docx delete-block");
101721
102172
  const at2 = getAtFlag(rest2);
101722
102173
  if (!filePath || at2 === void 0) fail2("Usage: lotics docx delete-block <file> --at=<index>");
101723
102174
  const doc = await loadFile2(filePath);