@lotics/cli 0.162.0 → 0.163.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
@@ -45964,6 +45966,7 @@ var COMMANDS = [
45964
45966
  },
45965
45967
  {
45966
45968
  verbs: ["xlsx"],
45969
+ ownsTail: true,
45967
45970
  help: [
45968
45971
  " lotics xlsx <subcommand> ... Read/write/edit .xlsx files on your local filesystem",
45969
45972
  " (uses the bundled Lotics xlsx engine; prefer over",
@@ -45972,6 +45975,7 @@ var COMMANDS = [
45972
45975
  },
45973
45976
  {
45974
45977
  verbs: ["docx"],
45978
+ ownsTail: true,
45975
45979
  help: [
45976
45980
  " lotics docx <subcommand> ... Read/write/edit .docx files on your local filesystem",
45977
45981
  " (uses the bundled Lotics OOXML engine; preserves",
@@ -46003,6 +46007,32 @@ function renderCommandsHelp() {
46003
46007
  function knownVerbs() {
46004
46008
  return new Set(COMMANDS.flatMap((group) => [...group.verbs, ...group.aliases ?? []]));
46005
46009
  }
46010
+ var SUBCOMMAND_ARGS = /* @__PURE__ */ new Map([
46011
+ ["xlsx read", { valueFlags: ["--sheet", "--range"], flags: ["--with-format"] }],
46012
+ ["xlsx set-cell", { content: true }],
46013
+ ["docx read", { flags: ["--include-opaque"] }],
46014
+ ["docx append-paragraph", { flags: ["--style="], content: true }],
46015
+ ["docx insert-paragraph", { flags: ["--style=", "--at="], content: true }],
46016
+ ["docx delete-block", { flags: ["--at="] }],
46017
+ ["docx replace-text", { content: true }]
46018
+ ]);
46019
+ function commandOwnsTail(command) {
46020
+ if (command === void 0) return false;
46021
+ return COMMANDS.some(
46022
+ (group) => group.ownsTail === true && [...group.verbs, ...group.aliases ?? []].includes(command)
46023
+ );
46024
+ }
46025
+ function subcommandArgs(command, subcommand) {
46026
+ if (command === void 0 || subcommand === void 0) return void 0;
46027
+ return SUBCOMMAND_ARGS.get(`${command} ${subcommand}`);
46028
+ }
46029
+ function knownSubcommandFlags(command, subcommand) {
46030
+ const spec = subcommandArgs(command, subcommand);
46031
+ return [...spec?.valueFlags ?? [], ...spec?.flags ?? []];
46032
+ }
46033
+ function matchesKnownFlag(known, arg) {
46034
+ return known.some((k) => k.endsWith("=") ? arg.startsWith(k) : arg === k);
46035
+ }
46006
46036
 
46007
46037
  // src/report_command.ts
46008
46038
  import fs6 from "node:fs";
@@ -46141,6 +46171,22 @@ function buildStarterTemplate(args) {
46141
46171
  typecheck: "tsc --noEmit",
46142
46172
  // Scope to src/ — oxlint's default scan walks node_modules too.
46143
46173
  lint: "oxlint src",
46174
+ // No `--passWithNoTests`, deliberately. It would be justified by a
46175
+ // TEMPORARY condition ("the scaffold seeds nothing") and then ship
46176
+ // forever in every app's package.json and CI, where its only
46177
+ // remaining effect is to turn "vitest discovered no test files" into
46178
+ // success — a green suite over a renamed `src/`, a changed `include`
46179
+ // or a broken vitest config, indefinitely. A detector that cannot
46180
+ // fire when the thing is ABSENT rather than excessive is not one.
46181
+ //
46182
+ // What the scaffold seeds instead is `src/harness.test.ts`, which
46183
+ // asserts the HARNESS and nothing else. The file it replaces
46184
+ // (`src/App.test.tsx`) asserted the scaffold's placeholder copy
46185
+ // ("First item"), so it failed the moment the author did the very
46186
+ // thing the closing instructions tell them to do — edit
46187
+ // `src/App.tsx` — and a smoke test that renders `<App/>` would fail
46188
+ // the same way for any real app whose first screen reaches the host
46189
+ // bridge on mount. The harness test touches neither.
46144
46190
  test: "vitest run"
46145
46191
  },
46146
46192
  dependencies: {
@@ -46716,15 +46762,33 @@ declare module "*.css";
46716
46762
  `
46717
46763
  },
46718
46764
  {
46719
- path: "src/App.test.tsx",
46765
+ path: "src/harness.test.ts",
46720
46766
  content: `import { describe, test, expect } from "vitest";
46721
- import { render } from "@testing-library/react";
46722
- import App from "./App";
46767
+ import { getAppBinding } from "@lotics/app-sdk";
46768
+
46769
+ /**
46770
+ * The one test the scaffold seeds, and it is about the TEST SETUP rather than
46771
+ * about your app \u2014 so editing src/App.tsx can never break it, and \`npm test\`
46772
+ * never has to be green over zero tests.
46773
+ *
46774
+ * What it pins: tests run under jsdom, and vitest.setup.ts's echo stub answers
46775
+ * for \`getAppBinding\`. That second one is the whole reason the setup file
46776
+ * exists: a package-linked app's generated .lotics/app_fields.ts awaits it while
46777
+ * a test file is still COLLECTING, so without the stub every test fails before
46778
+ * any of them runs \u2014 including pure logic tests that never touch a field id.
46779
+ *
46780
+ * Delete it once you have tests of your own, or keep it; it costs nothing.
46781
+ */
46782
+ describe("test harness", () => {
46783
+ test("runs in a DOM", () => {
46784
+ expect(typeof document).toBe("object");
46785
+ });
46723
46786
 
46724
- describe("App", () => {
46725
- test("renders the default route", () => {
46726
- const { container } = render(<App />);
46727
- expect(container.textContent).toContain("First item");
46787
+ test("resolves the app binding from the setup stub, so the app graph collects", async () => {
46788
+ const binding = await getAppBinding();
46789
+ // The echo stub answers any alias with a self-identifying ':test:' id \u2014 one
46790
+ // that can never be mistaken for a real fld_\u2026 from the workspace.
46791
+ expect(binding.fields["anything.at.all"]).toContain(":test:");
46728
46792
  });
46729
46793
  });
46730
46794
  `
@@ -64810,7 +64874,14 @@ var formulaAggregateOperationSchema = external_exports.enum([
64810
64874
  "median",
64811
64875
  "min",
64812
64876
  "max",
64813
- "range"
64877
+ "range",
64878
+ "earliest",
64879
+ "latest",
64880
+ "date_range",
64881
+ "checked",
64882
+ "unchecked",
64883
+ "percent_checked",
64884
+ "percent_unchecked"
64814
64885
  ]);
64815
64886
  var rollupAggregateOperationSchema = external_exports.enum([
64816
64887
  "empty",
@@ -64910,25 +64981,25 @@ var aggregateOperationSchema = external_exports.enum([
64910
64981
  "string_agg"
64911
64982
  ]);
64912
64983
  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"],
64984
+ empty: ["text", "number", "date", "select", "select_member", "select_record_link", "files", "formula", "rollup", "autonumber"],
64985
+ filled: ["text", "number", "date", "select", "select_member", "select_record_link", "files", "formula", "rollup", "autonumber"],
64986
+ percent_empty: ["text", "number", "date", "select", "select_member", "select_record_link", "files", "formula", "rollup", "autonumber"],
64987
+ percent_filled: ["text", "number", "date", "select", "select_member", "select_record_link", "files", "formula", "rollup", "autonumber"],
64988
+ unique: ["text", "number", "date", "select", "select_member", "files", "formula", "rollup", "autonumber"],
64989
+ percent_unique: ["text", "number", "date", "select", "select_member", "files", "formula", "rollup", "autonumber"],
64919
64990
  sum: ["number", "formula", "rollup"],
64920
64991
  avg: ["number", "formula", "rollup"],
64921
64992
  median: ["number", "formula", "rollup"],
64922
64993
  min: ["number", "formula", "rollup"],
64923
64994
  max: ["number", "formula", "rollup"],
64924
64995
  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"]
64996
+ earliest: ["date", "formula", "rollup"],
64997
+ latest: ["date", "formula", "rollup"],
64998
+ date_range: ["date", "formula", "rollup"],
64999
+ checked: ["boolean", "formula"],
65000
+ unchecked: ["boolean", "formula"],
65001
+ percent_checked: ["boolean", "formula"],
65002
+ percent_unchecked: ["boolean", "formula"]
64932
65003
  };
64933
65004
  var FIELD_TYPE_OPERATIONS = /* @__PURE__ */ new Map();
64934
65005
  for (const [operation, fieldTypes] of Object.entries(OPERATION_FIELD_TYPES)) {
@@ -65608,8 +65679,7 @@ var appWorkflowInputRecordLinkSchema = appWorkflowInputBaseSchema.extend({
65608
65679
  table_id: zod_default.string().describe("Records must belong to this table. Validated at execute time + workflow save."),
65609
65680
  multi: zod_default.boolean().optional().describe("When true, accept an array of record_ids. Defaults to single.")
65610
65681
  });
65611
- var appWorkflowInputSelectSchema = appWorkflowInputBaseSchema.extend({
65612
- type: zod_default.literal("select"),
65682
+ var selectOptionSourceShape = {
65613
65683
  options: zod_default.array(
65614
65684
  zod_default.object({
65615
65685
  label: zod_default.string(),
@@ -65617,20 +65687,27 @@ var appWorkflowInputSelectSchema = appWorkflowInputBaseSchema.extend({
65617
65687
  })
65618
65688
  ).min(1).optional().describe("Inline option set. Provide EITHER `options` OR `field`, never both."),
65619
65689
  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."
65690
+ "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
65691
  ),
65622
65692
  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
- });
65693
+ };
65694
+ function checkExactlyOneOptionSource(kind) {
65695
+ return (ctx) => {
65696
+ const hasOptions = ctx.value.options !== void 0;
65697
+ const hasField = ctx.value.field !== void 0;
65698
+ if (hasOptions === hasField) {
65699
+ ctx.issues.push({
65700
+ code: "custom",
65701
+ input: ctx.value,
65702
+ 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`
65703
+ });
65704
+ }
65705
+ };
65706
+ }
65707
+ var appWorkflowInputSelectSchema = appWorkflowInputBaseSchema.extend({
65708
+ type: zod_default.literal("select"),
65709
+ ...selectOptionSourceShape
65710
+ }).check(checkExactlyOneOptionSource("input"));
65634
65711
  var appWorkflowInputDateRangeSchema = appWorkflowInputBaseSchema.extend({
65635
65712
  type: zod_default.literal("date_range"),
65636
65713
  include_time: zod_default.boolean().optional().describe("When true, the range includes time components (datetime). Defaults to false.")
@@ -65701,11 +65778,14 @@ var appWorkflowOutputSchema = zod_default.lazy(
65701
65778
  table_id: zod_default.string(),
65702
65779
  multi: zod_default.boolean().optional()
65703
65780
  }),
65781
+ // The SAME shape and the SAME check as the select INPUT, not a copy of
65782
+ // them: the two surfaces declare one vocabulary (GAP-294), so accepting
65783
+ // `field` on one and not the other — or validating it differently — just
65784
+ // moves the drift somewhere nobody looks.
65704
65785
  appWorkflowOutputBaseSchema.extend({
65705
65786
  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
- }),
65787
+ ...selectOptionSourceShape
65788
+ }).check(checkExactlyOneOptionSource("output")),
65709
65789
  appWorkflowOutputBaseSchema.extend({
65710
65790
  type: zod_default.literal("object"),
65711
65791
  fields: zod_default.record(zod_default.string(), appWorkflowOutputSchema)
@@ -66075,7 +66155,6 @@ __export(expression_array_exports, {
66075
66155
  includes: () => includes,
66076
66156
  intersection: () => intersection2,
66077
66157
  intersectionBy: () => intersectionBy,
66078
- isEmpty: () => isEmpty2,
66079
66158
  last: () => last2,
66080
66159
  list: () => list,
66081
66160
  maxBy: () => maxBy,
@@ -66530,13 +66609,6 @@ function size(arr) {
66530
66609
  }
66531
66610
  return arr.length;
66532
66611
  }
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
66612
  function includes(collection, value2) {
66541
66613
  if (collection == null) return false;
66542
66614
  if (Array.isArray(collection)) return collection.includes(value2);
@@ -66807,6 +66879,7 @@ function capitalize(str) {
66807
66879
  return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase();
66808
66880
  }
66809
66881
  function replace(str, search, replacement) {
66882
+ if (str == null) return "";
66810
66883
  if (typeof str !== "string") {
66811
66884
  throw new Error("replace: expected string, got " + typeof str);
66812
66885
  }
@@ -66819,6 +66892,7 @@ function replace(str, search, replacement) {
66819
66892
  return str.replace(search, replacement);
66820
66893
  }
66821
66894
  function replaceAll(str, search, replacement) {
66895
+ if (str == null) return "";
66822
66896
  if (typeof str !== "string") {
66823
66897
  throw new Error("replaceAll: expected string, got " + typeof str);
66824
66898
  }
@@ -66851,6 +66925,7 @@ function endsWith(str, suffix) {
66851
66925
  return str.endsWith(suffix);
66852
66926
  }
66853
66927
  function substring(str, start, end) {
66928
+ if (str == null) return "";
66854
66929
  if (typeof str !== "string") {
66855
66930
  throw new Error("substring: expected string, got " + typeof str);
66856
66931
  }
@@ -66863,6 +66938,7 @@ function substring(str, start, end) {
66863
66938
  return str.substring(start, end);
66864
66939
  }
66865
66940
  function padStart(str, length2, char) {
66941
+ if (str == null) return "";
66866
66942
  if (typeof str !== "string") {
66867
66943
  throw new Error("padStart: expected string, got " + typeof str);
66868
66944
  }
@@ -66875,6 +66951,7 @@ function padStart(str, length2, char) {
66875
66951
  return str.padStart(length2, char);
66876
66952
  }
66877
66953
  function padEnd(str, length2, char) {
66954
+ if (str == null) return "";
66878
66955
  if (typeof str !== "string") {
66879
66956
  throw new Error("padEnd: expected string, got " + typeof str);
66880
66957
  }
@@ -66980,6 +67057,7 @@ function numberToWords(value2) {
66980
67057
  return negative ? "\xE2m " + result : result;
66981
67058
  }
66982
67059
  function formatNumber(value2, decimals) {
67060
+ if (value2 == null) return "";
66983
67061
  if (typeof value2 !== "number" || Number.isNaN(value2)) {
66984
67062
  throw new Error("formatNumber: expected number, got " + typeof value2);
66985
67063
  }
@@ -67007,15 +67085,17 @@ __export(expression_math_exports, {
67007
67085
  sum: () => sum
67008
67086
  });
67009
67087
  function mean(arr) {
67010
- if (arr == null || !Array.isArray(arr)) {
67088
+ if (arr == null) return null;
67089
+ if (!Array.isArray(arr)) {
67011
67090
  throw new Error("mean: expected array, got " + typeof arr);
67012
67091
  }
67013
- if (arr.length === 0) {
67014
- throw new Error("mean: array cannot be empty");
67092
+ const present = arr.filter((item) => item != null);
67093
+ if (present.length === 0) {
67094
+ return null;
67015
67095
  }
67016
- const numArr = arr;
67017
- const sum2 = numArr.reduce((a, b) => a + b, 0);
67018
- const result = sum2 / numArr.length;
67096
+ const numArr = present;
67097
+ const total = numArr.reduce((a, b) => a + b, 0);
67098
+ const result = total / numArr.length;
67019
67099
  if (isNaN(result)) {
67020
67100
  throw new Error("mean: array contains non-numeric values");
67021
67101
  }
@@ -67028,7 +67108,7 @@ function min(...values3) {
67028
67108
  if (values3.length === 1 && Array.isArray(values3[0])) {
67029
67109
  const arr = values3[0];
67030
67110
  if (arr.length === 0) {
67031
- throw new Error("min: array cannot be empty");
67111
+ return null;
67032
67112
  }
67033
67113
  return arr.reduce((minVal, item) => {
67034
67114
  if (minVal == null) return item;
@@ -67037,9 +67117,9 @@ function min(...values3) {
67037
67117
  return item < minVal ? item : minVal;
67038
67118
  }
67039
67119
  return String(item) < String(minVal) ? item : minVal;
67040
- });
67120
+ }) ?? null;
67041
67121
  }
67042
- return values3.reduce((minVal, item) => {
67122
+ const smallest = values3.reduce((minVal, item) => {
67043
67123
  if (minVal == null) return item;
67044
67124
  if (item == null) return minVal;
67045
67125
  if (typeof minVal === "number" && typeof item === "number") {
@@ -67047,6 +67127,7 @@ function min(...values3) {
67047
67127
  }
67048
67128
  return String(item) < String(minVal) ? item : minVal;
67049
67129
  });
67130
+ return smallest ?? null;
67050
67131
  }
67051
67132
  function max(...values3) {
67052
67133
  if (values3.length === 0) {
@@ -67055,7 +67136,7 @@ function max(...values3) {
67055
67136
  if (values3.length === 1 && Array.isArray(values3[0])) {
67056
67137
  const arr = values3[0];
67057
67138
  if (arr.length === 0) {
67058
- throw new Error("max: array cannot be empty");
67139
+ return null;
67059
67140
  }
67060
67141
  return arr.reduce((maxVal, item) => {
67061
67142
  if (maxVal == null) return item;
@@ -67064,9 +67145,9 @@ function max(...values3) {
67064
67145
  return item > maxVal ? item : maxVal;
67065
67146
  }
67066
67147
  return String(item) > String(maxVal) ? item : maxVal;
67067
- });
67148
+ }) ?? null;
67068
67149
  }
67069
- return values3.reduce((maxVal, item) => {
67150
+ const largest = values3.reduce((maxVal, item) => {
67070
67151
  if (maxVal == null) return item;
67071
67152
  if (item == null) return maxVal;
67072
67153
  if (typeof maxVal === "number" && typeof item === "number") {
@@ -67074,8 +67155,10 @@ function max(...values3) {
67074
67155
  }
67075
67156
  return String(item) > String(maxVal) ? item : maxVal;
67076
67157
  });
67158
+ return largest ?? null;
67077
67159
  }
67078
67160
  function round(value2, precision) {
67161
+ if (value2 == null) return null;
67079
67162
  if (typeof value2 !== "number") {
67080
67163
  throw new Error("round: expected number, got " + typeof value2);
67081
67164
  }
@@ -67094,24 +67177,28 @@ function sum(arr) {
67094
67177
  return arr.reduce((a, b) => a + b, 0);
67095
67178
  }
67096
67179
  function abs(n) {
67180
+ if (n == null) return null;
67097
67181
  if (typeof n !== "number") {
67098
67182
  throw new Error("abs: expected number, got " + typeof n);
67099
67183
  }
67100
67184
  return Math.abs(n);
67101
67185
  }
67102
67186
  function ceil(n) {
67187
+ if (n == null) return null;
67103
67188
  if (typeof n !== "number") {
67104
67189
  throw new Error("ceil: expected number, got " + typeof n);
67105
67190
  }
67106
67191
  return Math.ceil(n);
67107
67192
  }
67108
67193
  function floor(n) {
67194
+ if (n == null) return null;
67109
67195
  if (typeof n !== "number") {
67110
67196
  throw new Error("floor: expected number, got " + typeof n);
67111
67197
  }
67112
67198
  return Math.floor(n);
67113
67199
  }
67114
67200
  function clamp(n, min2, max3) {
67201
+ if (n == null) return null;
67115
67202
  if (typeof n !== "number") {
67116
67203
  throw new Error("clamp: expected number, got " + typeof n);
67117
67204
  }
@@ -67129,6 +67216,7 @@ function clamp(n, min2, max3) {
67129
67216
  return n;
67130
67217
  }
67131
67218
  function percentage(part, total, decimals) {
67219
+ if (part == null || total == null) return null;
67132
67220
  if (typeof part !== "number") {
67133
67221
  throw new Error("percentage: part must be a number");
67134
67222
  }
@@ -67149,6 +67237,7 @@ function percentage(part, total, decimals) {
67149
67237
  return percent;
67150
67238
  }
67151
67239
  function pow(base, exponent) {
67240
+ if (base == null || exponent == null) return null;
67152
67241
  if (typeof base !== "number") {
67153
67242
  throw new Error("pow: base must be a number");
67154
67243
  }
@@ -67158,6 +67247,7 @@ function pow(base, exponent) {
67158
67247
  return Math.pow(base, exponent);
67159
67248
  }
67160
67249
  function sqrt(n) {
67250
+ if (n == null) return null;
67161
67251
  if (typeof n !== "number") {
67162
67252
  throw new Error("sqrt: expected number, got " + typeof n);
67163
67253
  }
@@ -67167,6 +67257,7 @@ function sqrt(n) {
67167
67257
  return Math.sqrt(n);
67168
67258
  }
67169
67259
  function mod(n, divisor) {
67260
+ if (n == null || divisor == null) return null;
67170
67261
  if (typeof n !== "number") {
67171
67262
  throw new Error("mod: expected number, got " + typeof n);
67172
67263
  }
@@ -67339,7 +67430,7 @@ __export(expression_type_exports, {
67339
67430
  coalesce: () => coalesce,
67340
67431
  isArray: () => isArray2,
67341
67432
  isBoolean: () => isBoolean,
67342
- isEmpty: () => isEmpty3,
67433
+ isEmpty: () => isEmpty2,
67343
67434
  isNotNull: () => isNotNull,
67344
67435
  isNull: () => isNull,
67345
67436
  isNumber: () => isNumber,
@@ -67351,6 +67442,22 @@ __export(expression_type_exports, {
67351
67442
  toString: () => toString,
67352
67443
  typeOf: () => typeOf
67353
67444
  });
67445
+
67446
+ // ../shared/src/schemas/database_types.ts
67447
+ function isEmptyFieldValue(value2) {
67448
+ if (value2 === null || value2 === void 0) {
67449
+ return true;
67450
+ }
67451
+ if (typeof value2 === "string") {
67452
+ return value2 === "";
67453
+ }
67454
+ if (Array.isArray(value2)) {
67455
+ return value2.length === 0;
67456
+ }
67457
+ return false;
67458
+ }
67459
+
67460
+ // ../shared/src/expression_type.ts
67354
67461
  function isArray2(val) {
67355
67462
  return Array.isArray(val);
67356
67463
  }
@@ -67373,6 +67480,7 @@ function coalesce(...values3) {
67373
67480
  return void 0;
67374
67481
  }
67375
67482
  function toNumber(val) {
67483
+ if (val == null) return null;
67376
67484
  if (typeof val === "number") return val;
67377
67485
  if (typeof val === "string") {
67378
67486
  const parsed = parseFloat(val);
@@ -67384,9 +67492,7 @@ function toNumber(val) {
67384
67492
  throw new Error("toNumber: expected number or string, got " + typeof val);
67385
67493
  }
67386
67494
  function toString(val) {
67387
- if (val == null) {
67388
- throw new Error("toString: cannot convert null or undefined to string");
67389
- }
67495
+ if (val == null) return "";
67390
67496
  return String(val);
67391
67497
  }
67392
67498
  function isNull(val) {
@@ -67395,13 +67501,11 @@ function isNull(val) {
67395
67501
  function isNotNull(val) {
67396
67502
  return !isNull(val);
67397
67503
  }
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;
67504
+ function isEmpty2(val) {
67505
+ return isEmptyFieldValue(val);
67403
67506
  }
67404
67507
  function parseJson(value2) {
67508
+ if (value2 == null) return null;
67405
67509
  if (typeof value2 !== "string") {
67406
67510
  throw new Error("parseJson: expected a JSON string, got " + typeof value2);
67407
67511
  }
@@ -71002,7 +71106,10 @@ ${args.instructions.replace(/\s+$/, "")}
71002
71106
  `;
71003
71107
  }
71004
71108
  function stripAgentHeader(text) {
71005
- return text.replace(/<!--\s*lotics:[\s\S]*?-->\n?/g, "").replace(/\n{3,}/g, "\n\n").trim();
71109
+ return normalizeAgentInstructions(text.replace(/<!--\s*lotics:[\s\S]*?-->\n?/g, ""));
71110
+ }
71111
+ function normalizeAgentInstructions(text) {
71112
+ return text.replace(/\n{3,}/g, "\n\n").trim();
71006
71113
  }
71007
71114
 
71008
71115
  // src/npm_registry.ts
@@ -71121,6 +71228,49 @@ function writeSynced(projectDir, kind, alias, entry) {
71121
71228
  pkg2.lotics.synced[kind] = { ...pkg2.lotics.synced[kind], [alias]: entry };
71122
71229
  fs8.writeFileSync(pkgPath2, JSON.stringify(pkg2, null, 2) + "\n");
71123
71230
  }
71231
+ function readManifestEntry(projectDir, kind, alias) {
71232
+ const pkg2 = readManifest(projectDir);
71233
+ if (!pkg2.lotics) return { found: false };
71234
+ const entry = { found: true };
71235
+ let found = false;
71236
+ const declarations = pkg2.lotics[kind];
71237
+ if (declarations && typeof declarations === "object" && !Array.isArray(declarations)) {
71238
+ const map3 = declarations;
71239
+ if (alias in map3) {
71240
+ entry.declaration = map3[alias];
71241
+ found = true;
71242
+ }
71243
+ }
71244
+ const baselines = pkg2.lotics.synced?.[kind];
71245
+ if (baselines && alias in baselines) {
71246
+ entry.synced = baselines[alias];
71247
+ found = true;
71248
+ }
71249
+ return found ? entry : { found: false };
71250
+ }
71251
+ function readManifest(projectDir) {
71252
+ const pkgPath2 = path10.join(projectDir, "package.json");
71253
+ return JSON.parse(fs8.readFileSync(pkgPath2, "utf-8"));
71254
+ }
71255
+ function removeFromManifest(projectDir, kind, alias) {
71256
+ const pkgPath2 = path10.join(projectDir, "package.json");
71257
+ const pkg2 = readManifest(projectDir);
71258
+ if (!pkg2.lotics) return;
71259
+ const declarations = pkg2.lotics[kind];
71260
+ if (declarations && typeof declarations === "object" && !Array.isArray(declarations)) {
71261
+ delete declarations[alias];
71262
+ }
71263
+ const baselines = pkg2.lotics.synced?.[kind];
71264
+ if (baselines) delete baselines[alias];
71265
+ fs8.writeFileSync(pkgPath2, JSON.stringify(pkg2, null, 2) + "\n");
71266
+ }
71267
+ function parkPrunedDeclaration(projectDir, kind, alias, removed) {
71268
+ const dir = path10.join(projectDir, ".lotics", "pruned", kind);
71269
+ fs8.mkdirSync(dir, { recursive: true });
71270
+ const file2 = path10.join(dir, `${alias}.json`);
71271
+ fs8.writeFileSync(file2, JSON.stringify(removed, null, 2) + "\n");
71272
+ return path10.relative(projectDir, file2);
71273
+ }
71124
71274
  var WORKFLOWS_DIR = path10.join("src", "workflows");
71125
71275
  var WORKFLOW_GLOBALS_DIR = path10.join(".lotics", "workflows");
71126
71276
  var AGENTS_DIR = path10.join("src", "agents");
@@ -71270,13 +71420,52 @@ function writeAgentFiles(projectDir, agents, opts) {
71270
71420
  }
71271
71421
  return written;
71272
71422
  }
71423
+ function parkLiveAgentProse(projectDir, alias, instructions) {
71424
+ const dir = path10.join(projectDir, ".lotics", "agents");
71425
+ const file2 = path10.join(dir, `${alias}.live.md`);
71426
+ try {
71427
+ fs8.mkdirSync(dir, { recursive: true });
71428
+ fs8.writeFileSync(file2, normalizeAgentInstructions(instructions) + "\n");
71429
+ return path10.relative(projectDir, file2);
71430
+ } catch (err2) {
71431
+ console.error(
71432
+ `\u26A0 Could not write the live prose for ${alias} to ${path10.relative(projectDir, file2)}: ${err2 instanceof Error ? err2.message : String(err2)}`
71433
+ );
71434
+ return null;
71435
+ }
71436
+ }
71437
+ function reportUnseenLive(kind, unseen, appId) {
71438
+ if (unseen.length === 0) return;
71439
+ const plural = unseen.length === 1;
71440
+ console.error(
71441
+ `
71442
+ \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"}.
71443
+ Your next deploy will be REFUSED for ${plural ? "this alias" : "these aliases"}, which is the point: pushing would overwrite their edit.
71444
+ 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.`
71445
+ );
71446
+ }
71447
+ function parkLiveWorkflowBody(projectDir, alias, source) {
71448
+ const dir = path10.join(projectDir, WORKFLOW_GLOBALS_DIR);
71449
+ const file2 = path10.join(dir, `${alias}.live.ts`);
71450
+ try {
71451
+ fs8.mkdirSync(dir, { recursive: true });
71452
+ fs8.writeFileSync(file2, normalizeWorkflowBody(source) + "\n");
71453
+ return path10.relative(projectDir, file2);
71454
+ } catch (err2) {
71455
+ console.error(
71456
+ `\u26A0 Could not write the live body for ${alias} to ${path10.relative(projectDir, file2)}: ${err2 instanceof Error ? err2.message : String(err2)}`
71457
+ );
71458
+ return null;
71459
+ }
71460
+ }
71273
71461
  function localPromptUnpushed(projectDir, alias, live) {
71274
71462
  const file2 = agentFilePath2(projectDir, alias);
71275
71463
  if (!fs8.existsSync(file2)) return false;
71276
- const onDisk = fs8.readFileSync(file2, "utf-8");
71464
+ const onDisk = stripAgentHeader(fs8.readFileSync(file2, "utf-8"));
71465
+ if (onDisk === normalizeAgentInstructions(live)) return false;
71277
71466
  const baseline = readSynced(projectDir).agents[alias]?.content;
71278
- if (baseline === void 0) return stripAgentHeader(onDisk).trim() !== live.trim();
71279
- return contentSha(stripAgentHeader(onDisk)) !== baseline;
71467
+ if (baseline === void 0) return true;
71468
+ return contentSha(onDisk) !== baseline;
71280
71469
  }
71281
71470
  function generatedWorkflowDescription(appName, alias) {
71282
71471
  return `Workflow invoked by app "${appName}" via useWorkflow("${alias}").`;
@@ -71285,14 +71474,15 @@ function localBodyUnpushed(projectDir, alias, live) {
71285
71474
  const file2 = workflowFilePath(projectDir, alias);
71286
71475
  if (!fs8.existsSync(file2)) return false;
71287
71476
  const onDisk = fs8.readFileSync(file2, "utf-8");
71477
+ if (stripWorkflowHeader(onDisk).trim() === normalizeWorkflowBody(live).trim()) return false;
71288
71478
  const baseline = readSynced(projectDir).workflows[alias]?.content;
71289
- if (baseline === void 0) {
71290
- return stripWorkflowHeader(onDisk).trim() !== normalizeWorkflowBody(live).trim();
71291
- }
71479
+ if (baseline === void 0) return true;
71292
71480
  return contentSha(stripWorkflowHeader(onDisk)) !== baseline;
71293
71481
  }
71294
71482
  async function writeWorkflowFiles(client, projectDir, app_id, appName, workflows, opts) {
71295
71483
  const written = [];
71484
+ const kept = [];
71485
+ const unseenLive = [];
71296
71486
  for (const [alias, declaration] of Object.entries(workflows)) {
71297
71487
  const res = await client.getAppWorkflow(app_id, alias);
71298
71488
  if (res.error && !isMissingWorkflowBodyError(res.error)) {
@@ -71317,7 +71507,16 @@ async function writeWorkflowFiles(client, projectDir, app_id, appName, workflows
71317
71507
  toWorkflowDtsDeclaration(declaration)
71318
71508
  );
71319
71509
  if (!opts.overwrite && localBodyUnpushed(projectDir, alias, source)) {
71320
- opts.kept.push(`src/workflows/${alias}.ts`);
71510
+ kept.push(`src/workflows/${alias}.ts`);
71511
+ const previous = readSynced(projectDir).workflows[alias];
71512
+ if (body_sha && (previous?.live === void 0 || previous.live === body_sha)) {
71513
+ writeSynced(projectDir, "workflows", alias, { ...previous, live: body_sha });
71514
+ } else if (body_sha) {
71515
+ unseenLive.push({
71516
+ alias,
71517
+ parked: parkLiveWorkflowBody(projectDir, alias, source)
71518
+ });
71519
+ }
71321
71520
  continue;
71322
71521
  }
71323
71522
  writeWorkflowFile(projectDir, alias, source, envelope2);
@@ -71341,7 +71540,7 @@ async function writeWorkflowFiles(client, projectDir, app_id, appName, workflows
71341
71540
  }
71342
71541
  written.push(alias);
71343
71542
  }
71344
- return written;
71543
+ return { written, kept, unseenLive };
71345
71544
  }
71346
71545
  function writeWorkflowDescription(projectDir, alias, description) {
71347
71546
  const pkgPath2 = path10.join(projectDir, "package.json");
@@ -72041,15 +72240,21 @@ async function hydrateAppProject(client, targetPath, app, opts) {
72041
72240
  );
72042
72241
  const workflows = app.workflows ?? {};
72043
72242
  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
- });
72243
+ const { written, kept, unseenLive } = await writeWorkflowFiles(
72244
+ client,
72245
+ targetPath,
72246
+ app.id,
72247
+ app.name,
72248
+ workflows,
72249
+ { overwrite: opts.force === true }
72250
+ );
72251
+ opts.kept.push(...kept);
72048
72252
  if (written.length > 0) {
72049
72253
  console.error(
72050
72254
  `Wrote ${written.length} workflow ${written.length === 1 ? "body" : "bodies"} to ${WORKFLOWS_DIR}/ (${written.join(", ")})`
72051
72255
  );
72052
72256
  }
72257
+ reportUnseenLive("workflow body", unseenLive, app.id);
72053
72258
  }
72054
72259
  const agents = app.agents ?? {};
72055
72260
  if (Object.keys(agents).length > 0) {
@@ -72057,17 +72262,31 @@ async function hydrateAppProject(client, targetPath, app, opts) {
72057
72262
  overwrite: opts.force === true,
72058
72263
  kept: opts.kept
72059
72264
  });
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 });
72265
+ const unseenLiveProse = [];
72266
+ for (const [alias, declaration] of Object.entries(agents)) {
72267
+ const instructions = declaration?.instructions;
72268
+ if (typeof instructions !== "string") continue;
72269
+ const live = contentSha(instructions);
72270
+ if (written.includes(alias)) {
72271
+ writeSynced(targetPath, "agents", alias, {
72272
+ content: contentSha(normalizeAgentInstructions(instructions)),
72273
+ live
72274
+ });
72275
+ continue;
72276
+ }
72277
+ const previous = readSynced(targetPath).agents[alias];
72278
+ if (previous?.live === void 0 || previous.live === live) {
72279
+ writeSynced(targetPath, "agents", alias, { ...previous, live });
72280
+ continue;
72281
+ }
72282
+ unseenLiveProse.push({ alias, parked: parkLiveAgentProse(targetPath, alias, instructions) });
72065
72283
  }
72066
72284
  if (written.length > 0) {
72067
72285
  console.error(
72068
72286
  `Wrote ${written.length} agent instruction ${written.length === 1 ? "file" : "files"} to ${AGENTS_DIR}/ (${written.join(", ")})`
72069
72287
  );
72070
72288
  }
72289
+ reportUnseenLive("prompt", unseenLiveProse, app.id);
72071
72290
  }
72072
72291
  await runNpmInstall(targetPath);
72073
72292
  writeAppDtsKitLinks(targetPath);
@@ -72090,16 +72309,17 @@ async function appPull(client, args) {
72090
72309
  console.error(`Downloading source archive...`);
72091
72310
  await downloadToFile(sourceUrl, tmpFile);
72092
72311
  const keptLocal = [];
72312
+ const restoredFromArchive = [];
72093
72313
  const stagingDir = fs8.mkdtempSync(path10.join(tmpdir(), `lotics-pull-${app.id}-`));
72094
72314
  try {
72095
72315
  await runTar(["-xzf", tmpFile, "-C", stagingDir], stagingDir);
72096
72316
  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
- );
72317
+ const copied = copyTree(stagingDir, targetPath, {
72318
+ overwrite: args.force === true,
72319
+ skip: [WORKFLOWS_DIR, AGENTS_DIR].map((d) => d.split(path10.sep).join("/"))
72320
+ });
72321
+ keptLocal.push(...copied.kept);
72322
+ restoredFromArchive.push(...copied.created);
72103
72323
  } finally {
72104
72324
  if (fs8.existsSync(tmpFile)) fs8.unlinkSync(tmpFile);
72105
72325
  fs8.rmSync(stagingDir, { recursive: true, force: true });
@@ -72114,6 +72334,7 @@ async function appPull(client, args) {
72114
72334
  ...args.force !== void 0 ? { force: args.force } : {},
72115
72335
  kept: keptLocal
72116
72336
  });
72337
+ reportRestoredFiles(restoredFromArchive, prior !== null);
72117
72338
  reportKeptLocalFiles(keptLocal, app.id, {
72118
72339
  // `prior` is non-null whenever the stamp was held — `stampAfterPull` cannot
72119
72340
  // hold against nothing — so the fallback only covers the un-held branch,
@@ -72275,7 +72496,8 @@ async function appDeploy(client, args) {
72275
72496
  if (args.prune) {
72276
72497
  await pruneOrphanedBindings(
72277
72498
  client,
72278
- meta3.app_id,
72499
+ projectDir,
72500
+ { app_id: meta3.app_id },
72279
72501
  orphanedBindings(called, liveAfter),
72280
72502
  called.dynamic
72281
72503
  );
@@ -72443,7 +72665,7 @@ function driftedAgentAliases(projectDir, liveAgents) {
72443
72665
  const file2 = agentFilePath2(projectDir, alias);
72444
72666
  if (!fs8.existsSync(file2)) continue;
72445
72667
  const local = stripAgentHeader(fs8.readFileSync(file2, "utf-8"));
72446
- if (local === "" || local === (live.instructions ?? "")) continue;
72668
+ if (local === "" || local === normalizeAgentInstructions(live.instructions ?? "")) continue;
72447
72669
  drifted.push(alias);
72448
72670
  }
72449
72671
  return drifted.sort();
@@ -72505,28 +72727,31 @@ function warnAboutOrphanedBindings(called, live) {
72505
72727
  );
72506
72728
  return lines.length;
72507
72729
  }
72508
- async function pruneOrphanedBindings(client, appId, orphans, dynamic) {
72730
+ async function pruneOrphanedBindings(client, projectDir, app, orphans, dynamic) {
72509
72731
  const targets = [
72510
72732
  ...orphans.queries.map((alias) => ({
72511
72733
  tool: "remove_app_query",
72734
+ kind: "queries",
72512
72735
  alias,
72513
72736
  label: `query ${alias}`,
72514
- restore: `lotics app query set ${alias}`
72737
+ setCommand: `lotics app query set ${alias}`
72515
72738
  })),
72516
72739
  ...orphans.workflows.map((alias) => ({
72517
72740
  tool: "remove_app_workflow",
72741
+ kind: "workflows",
72518
72742
  alias,
72519
72743
  label: `workflow ${alias}`,
72520
- restore: `lotics app workflow set ${alias}`
72744
+ setCommand: `lotics app workflow set ${alias}`
72521
72745
  })),
72522
72746
  ...orphans.agents.map((alias) => ({
72523
72747
  tool: "remove_app_agent",
72748
+ kind: "agents",
72524
72749
  alias,
72525
72750
  label: `agent ${alias}`,
72526
- restore: `lotics app agent set ${alias}`
72751
+ setCommand: `lotics app agent set ${alias}`
72527
72752
  }))
72528
72753
  ];
72529
- if (targets.length === 0) return 0;
72754
+ if (targets.length === 0) return;
72530
72755
  if (dynamic.length > 0) {
72531
72756
  console.error(
72532
72757
  `
@@ -72534,19 +72759,63 @@ async function pruneOrphanedBindings(client, appId, orphans, dynamic) {
72534
72759
  ` + targets.map((t) => ` \u2022 ${t.label}`).join("\n") + `
72535
72760
  Make those call sites literal and the next deploy will clean them up, or remove the ones you know are dead by hand.`
72536
72761
  );
72537
- return targets.length;
72762
+ return;
72538
72763
  }
72539
- let failed = 0;
72764
+ const tablesBefore = new Set(resolveCodegenTableIds(projectDir, readAppMeta(projectDir).queries ?? {}));
72765
+ let removedLocally = false;
72540
72766
  for (const target of targets) {
72541
- const res = await client.execute(target.tool, { app_id: appId, alias: target.alias });
72767
+ const res = await client.execute(target.tool, { app_id: app.app_id, alias: target.alias });
72542
72768
  if (res.error) {
72543
- failed += 1;
72544
72769
  console.error(` \u2717 could not unbind ${target.label} \u2014 ${res.error}`);
72545
72770
  continue;
72546
72771
  }
72547
- console.error(` \u2713 unbound ${target.label} \u2014 restore: ${target.restore}`);
72772
+ let line;
72773
+ try {
72774
+ const entry = readManifestEntry(projectDir, target.kind, target.alias);
72775
+ if (entry.found) {
72776
+ const parked = parkPrunedDeclaration(projectDir, target.kind, target.alias, {
72777
+ declaration: entry.declaration,
72778
+ synced: entry.synced
72779
+ });
72780
+ removeFromManifest(projectDir, target.kind, target.alias);
72781
+ removedLocally = true;
72782
+ 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}`;
72783
+ } else {
72784
+ 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>)`;
72785
+ }
72786
+ } catch (err2) {
72787
+ console.error(
72788
+ ` \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.`
72789
+ );
72790
+ continue;
72791
+ }
72792
+ console.error(line);
72793
+ }
72794
+ if (removedLocally) {
72795
+ try {
72796
+ const meta3 = readAppMeta(projectDir);
72797
+ writeAppDts(projectDir, {
72798
+ workflows: meta3.workflows,
72799
+ queries: meta3.queries,
72800
+ agents: meta3.agents
72801
+ });
72802
+ if (orphans.queries.length > 0) {
72803
+ const dropped = [...tablesBefore].filter(
72804
+ (id) => !resolveCodegenTableIds(projectDir, meta3.queries ?? {}).includes(id)
72805
+ );
72806
+ if (dropped.length > 0) {
72807
+ console.error(
72808
+ ` \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.`
72809
+ );
72810
+ }
72811
+ await writeGeneratedAppFields(client, projectDir, app, meta3.queries ?? {});
72812
+ }
72813
+ } catch (err2) {
72814
+ console.error(
72815
+ ` \u2717 could not refresh the generated companions after pruning \u2014 ${err2 instanceof Error ? err2.message : String(err2)}. Run lotics app codegen.`
72816
+ );
72817
+ }
72548
72818
  }
72549
- return failed;
72550
72819
  }
72551
72820
  function derivedDeployMessage(pending) {
72552
72821
  const parts = [
@@ -72558,6 +72827,7 @@ function derivedDeployMessage(pending) {
72558
72827
  }
72559
72828
  function copyTree(from, to, opts, relative = "") {
72560
72829
  const kept = [];
72830
+ const created = [];
72561
72831
  for (const entry of fs8.readdirSync(from, { withFileTypes: true })) {
72562
72832
  const src = path10.join(from, entry.name);
72563
72833
  const dest = path10.join(to, entry.name);
@@ -72565,17 +72835,32 @@ function copyTree(from, to, opts, relative = "") {
72565
72835
  if (opts.skip?.includes(rel.split(path10.sep).join("/"))) continue;
72566
72836
  if (entry.isDirectory()) {
72567
72837
  fs8.mkdirSync(dest, { recursive: true });
72568
- kept.push(...copyTree(src, dest, opts, rel));
72838
+ const child = copyTree(src, dest, opts, rel);
72839
+ kept.push(...child.kept);
72840
+ created.push(...child.created);
72569
72841
  continue;
72570
72842
  }
72571
- if (!opts.overwrite && fs8.existsSync(dest)) {
72843
+ const existed = fs8.existsSync(dest);
72844
+ if (!opts.overwrite && existed) {
72572
72845
  if (fs8.readFileSync(src).equals(fs8.readFileSync(dest))) continue;
72573
72846
  kept.push(rel);
72574
72847
  continue;
72575
72848
  }
72576
72849
  fs8.copyFileSync(src, dest);
72850
+ if (!existed) created.push(rel);
72577
72851
  }
72578
- return kept;
72852
+ return { kept, created };
72853
+ }
72854
+ function reportRestoredFiles(restored, isRefresh) {
72855
+ if (!isRefresh || restored.length === 0) return;
72856
+ const shown = restored.slice(0, 10);
72857
+ console.error(
72858
+ `
72859
+ Restored ${restored.length} file${restored.length === 1 ? "" : "s"} the app's last deployed source carries and your tree did not:
72860
+ ` + shown.map((f) => ` \u2022 ${f}`).join("\n") + (restored.length > shown.length ? `
72861
+ \u2022 \u2026and ${restored.length - shown.length} more` : "") + `
72862
+ 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.`
72863
+ );
72579
72864
  }
72580
72865
  function reportKeptLocalFiles(kept, appId, motion) {
72581
72866
  if (kept.length === 0) return;
@@ -73135,15 +73420,19 @@ async function appWorkflowPull(client, args = {}) {
73135
73420
  console.error(`App ${meta3.app_id} has no bound workflows.`);
73136
73421
  return;
73137
73422
  }
73138
- const kept = [];
73139
- const written = await writeWorkflowFiles(client, projectDir, meta3.app_id, app.name, workflows, {
73140
- overwrite: args.force === true,
73141
- kept
73142
- });
73423
+ const { written, kept, unseenLive } = await writeWorkflowFiles(
73424
+ client,
73425
+ projectDir,
73426
+ meta3.app_id,
73427
+ app.name,
73428
+ workflows,
73429
+ { overwrite: args.force === true }
73430
+ );
73143
73431
  console.error(
73144
73432
  `Wrote ${written.length} workflow ${written.length === 1 ? "body" : "bodies"} to ${WORKFLOWS_DIR}/` + (written.length > 0 ? ` (${written.join(", ")})` : "")
73145
73433
  );
73146
73434
  reportKeptLocalFiles(kept, meta3.app_id, null);
73435
+ reportUnseenLive("workflow body", unseenLive, meta3.app_id);
73147
73436
  ensureAppTsconfig(projectDir);
73148
73437
  }
73149
73438
  async function appWorkflowCheck(args) {
@@ -73291,6 +73580,23 @@ function parseArgs(argv) {
73291
73580
  let i2 = 0;
73292
73581
  while (i2 < argv.length) {
73293
73582
  const arg = argv[i2];
73583
+ if (subcommand !== void 0 && commandOwnsTail(command) && arg !== "--help" && arg !== "-h") {
73584
+ const local = subcommandArgs(command, subcommand);
73585
+ const looksLikeFlag = arg.length > 1 && arg.startsWith("-");
73586
+ const declared = looksLikeFlag && local !== void 0 && matchesKnownFlag([...local.valueFlags ?? [], ...local.flags ?? []], arg);
73587
+ if (declared) {
73588
+ restArgs.push(arg);
73589
+ if ((local?.valueFlags ?? []).includes(arg) && i2 + 1 < argv.length) restArgs.push(argv[++i2]);
73590
+ } else if (looksLikeFlag && local?.content !== true) {
73591
+ unknownFlag = arg;
73592
+ } else if (!toolArgs) {
73593
+ toolArgs = arg;
73594
+ } else {
73595
+ restArgs.push(arg);
73596
+ }
73597
+ i2++;
73598
+ continue;
73599
+ }
73294
73600
  switch (arg) {
73295
73601
  case "--json":
73296
73602
  flags.json = true;
@@ -73715,11 +74021,13 @@ var CliError = class extends Error {
73715
74021
  function fail2(message2) {
73716
74022
  throw new CliError(message2);
73717
74023
  }
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));
74024
+ function rejectUnknownFlags(rest2, command, subcommand) {
74025
+ const known = knownSubcommandFlags(command, subcommand);
74026
+ const unknown2 = rest2.filter((a) => a.startsWith("--") && !matchesKnownFlag(known, a));
73721
74027
  if (unknown2.length === 0) return;
73722
- fail2(`Unknown ${unknown2.length > 1 ? "flags" : "flag"} for ${usage}: ${unknown2.join(", ")}. Supported: ${known.join(", ")}.`);
74028
+ fail2(
74029
+ `Unknown ${unknown2.length > 1 ? "flags" : "flag"} for ${command} ${subcommand}: ${unknown2.join(", ")}. Supported: ${known.join(", ")}.`
74030
+ );
73723
74031
  }
73724
74032
  function rejectExtraArgs(rest2, arity, usage) {
73725
74033
  if (rest2.length <= arity) return;
@@ -94808,6 +95116,37 @@ function collectRecomputeReport(workbook, unreadable) {
94808
95116
  return { total, unevaluated };
94809
95117
  }
94810
95118
 
95119
+ // ../xlsx/src/numfmt_type.ts
95120
+ function classifyNumFmt(code) {
95121
+ if (!code || code === "General") return "general";
95122
+ const first3 = code.split(";")[0] ?? code;
95123
+ const currency = hasCurrencyTokens(first3);
95124
+ const stripped = stripQuotedLiterals(first3);
95125
+ if (stripped.trim() === "@") return "text";
95126
+ if (hasDateTokens(stripped)) return "date";
95127
+ if (stripped.includes("%")) return "percent";
95128
+ if (currency) return "currency";
95129
+ if (/[#0?]/.test(stripped)) return "numeric";
95130
+ return "general";
95131
+ }
95132
+ function stripQuotedLiterals(code) {
95133
+ return code.replace(/"[^"]*"/g, "").replace(/\\./g, " ");
95134
+ }
95135
+ function hasDateTokens(code) {
95136
+ if (/\b(y{1,4}|d{1,4}|h{1,2}|s{1,2}|am\/pm|a\/p)\b/i.test(code)) return true;
95137
+ if (/[ymdhs]\s*[-\/.:\s]\s*m/i.test(code)) return true;
95138
+ if (/m\s*[-\/.:\s]\s*[ymdhs]/i.test(code)) return true;
95139
+ return false;
95140
+ }
95141
+ function hasCurrencyTokens(code) {
95142
+ if (/[$¥€£₫¢]/.test(code)) return true;
95143
+ if (/\[\$/.test(code)) return true;
95144
+ return false;
95145
+ }
95146
+ function isPaddedIdentifier(input) {
95147
+ return /^[+-]?0\d/.test(input);
95148
+ }
95149
+
94811
95150
  // ../xlsx/src/clipboard.ts
94812
95151
  function adjustFormulaRefsForRowShift(formula, atRow, delta) {
94813
95152
  return formula.replace(
@@ -94835,17 +95174,18 @@ function adjustFormulaRefsForColShift(formula, atCol, delta) {
94835
95174
  }
94836
95175
 
94837
95176
  // ../xlsx/src/spreadsheet_commands.ts
94838
- function setCellValueCommand(workbook, engine, sheetIndex, row, col, newValue) {
95177
+ function setCellValueCommand(workbook, engine, sheetIndex, row, col, newValue, options) {
94839
95178
  const sheet = workbook.sheets[sheetIndex];
94840
95179
  const ref2 = rowColToRef(row, col);
94841
95180
  const oldCell = sheet.getCell(ref2);
94842
95181
  const oldValue = oldCell?.value ?? null;
94843
95182
  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
95183
  const styleIndex = oldCell?.styleIndex ?? 0;
94848
95184
  const numFmtCode = oldCell?.numFmtCode ?? "General";
95185
+ const isText = typeof newValue === "string" && options?.typed !== true;
95186
+ const isFormula = isText && newValue.startsWith("=");
95187
+ const formula = isFormula ? newValue.slice(1) : void 0;
95188
+ const value2 = isFormula ? null : isText ? parseInputValue(newValue, numFmtCode) : newValue;
94849
95189
  return {
94850
95190
  description: `Set ${ref2} = ${newValue}`,
94851
95191
  execute() {
@@ -95384,6 +95724,43 @@ function deleteColsCommand(workbook, sheetIndex, at2, count) {
95384
95724
  }
95385
95725
  };
95386
95726
  }
95727
+ function setRangeNumFmtCommand(workbook, sheetIndex, startRow, startCol, endRow, endCol, numFmtCode) {
95728
+ const sheet = workbook.sheets[sheetIndex];
95729
+ const snapshot = /* @__PURE__ */ new Map();
95730
+ for (let r = startRow; r <= endRow; r++) {
95731
+ for (let c = startCol; c <= endCol; c++) {
95732
+ const ref2 = rowColToRef(r, c);
95733
+ const cell = sheet.getCell(ref2);
95734
+ snapshot.set(ref2, cell ? { value: cell.value, styleIndex: cell.styleIndex, numFmtCode: cell.numFmtCode, formula: cell.formula } : null);
95735
+ }
95736
+ }
95737
+ const rangeRef = `${rowColToRef(startRow, startCol)}:${rowColToRef(endRow, endCol)}`;
95738
+ return {
95739
+ description: `Format ${rangeRef} as ${numFmtCode}`,
95740
+ execute() {
95741
+ for (let r = startRow; r <= endRow; r++) {
95742
+ for (let c = startCol; c <= endCol; c++) {
95743
+ const ref2 = rowColToRef(r, c);
95744
+ const cell = sheet.getCell(ref2);
95745
+ if (cell) {
95746
+ sheet.set(ref2, cell.value, sheet.styles?.get(cell.styleIndex), numFmtCode, cell.formula);
95747
+ } else {
95748
+ sheet.set(ref2, null, void 0, numFmtCode);
95749
+ }
95750
+ }
95751
+ }
95752
+ },
95753
+ undo() {
95754
+ for (const [ref2, snap] of snapshot) {
95755
+ if (snap) {
95756
+ sheet.set(ref2, snap.value, sheet.styles?.get(snap.styleIndex), snap.numFmtCode, snap.formula);
95757
+ } else {
95758
+ sheet.deleteCell(ref2);
95759
+ }
95760
+ }
95761
+ }
95762
+ };
95763
+ }
95387
95764
  function addSheetCommand(workbook, name2) {
95388
95765
  return {
95389
95766
  description: `Add sheet ${name2}`,
@@ -95426,10 +95803,23 @@ function renameSheetCommand(workbook, index, newName) {
95426
95803
  }
95427
95804
  };
95428
95805
  }
95429
- function parseInputValue(input) {
95806
+ function batchCommand(description, commands) {
95807
+ return {
95808
+ description,
95809
+ execute() {
95810
+ for (const cmd of commands) cmd.execute();
95811
+ },
95812
+ undo() {
95813
+ for (let i2 = commands.length - 1; i2 >= 0; i2--) commands[i2].undo();
95814
+ }
95815
+ };
95816
+ }
95817
+ function parseInputValue(input, numFmtCode) {
95430
95818
  if (input === "") return null;
95819
+ if (classifyNumFmt(numFmtCode) === "text") return input;
95431
95820
  if (input.toUpperCase() === "TRUE") return true;
95432
95821
  if (input.toUpperCase() === "FALSE") return false;
95822
+ if (isPaddedIdentifier(input.trim())) return input;
95433
95823
  const num3 = Number(input);
95434
95824
  if (!isNaN(num3) && input.trim() !== "") return num3;
95435
95825
  return input;
@@ -95702,21 +96092,20 @@ function applySheetCells(workbook, sheetIndex, def) {
95702
96092
  const sheet = workbook.sheets[sheetIndex];
95703
96093
  if (def.cells) {
95704
96094
  for (const [ref2, raw] of Object.entries(def.cells)) {
95705
- const cell = normalizeSimpleCell(raw, def.name, ref2);
95706
- const coord = refToRowCol(ref2.toUpperCase());
96095
+ const cell = normalizeSimpleCell(raw, `sheet "${def.name}" cell ${ref2}`);
96096
+ const upper2 = ref2.toUpperCase();
96097
+ const coord = refToRowCol(upper2);
95707
96098
  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();
96099
+ sheet.set(
96100
+ upper2,
96101
+ cell.formula ? null : cell.value ?? null,
96102
+ void 0,
96103
+ cell.numFmt,
96104
+ cell.formula ?? void 0
96105
+ );
95711
96106
  if (cell.style) {
95712
96107
  setRangeStyleCommand(workbook, sheetIndex, coord.row, coord.col, coord.row, coord.col, cell.style).execute();
95713
96108
  }
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
96109
  }
95721
96110
  }
95722
96111
  if (def.merges) {
@@ -95744,22 +96133,33 @@ function applySheetCells(workbook, sheetIndex, def) {
95744
96133
  }
95745
96134
  }
95746
96135
  }
95747
- function normalizeSimpleCell(raw, sheetName, ref2) {
96136
+ function normalizeSimpleCell(raw, label) {
95748
96137
  if (raw && typeof raw === "object" && !Array.isArray(raw)) {
95749
96138
  const unknown2 = Object.keys(raw).filter((k) => !SIMPLE_CELL_KEYS.has(k));
95750
96139
  if (unknown2.length > 0) {
95751
96140
  fail2(
95752
- `Unknown propert${unknown2.length > 1 ? "ies" : "y"} on sheet "${sheetName}" cell ${ref2}: ${unknown2.join(", ")}. Supported: ${[...SIMPLE_CELL_KEYS].join(", ")}.`
96141
+ `Unknown propert${unknown2.length > 1 ? "ies" : "y"} on ${label}: ${unknown2.join(", ")}. Supported: ${[...SIMPLE_CELL_KEYS].join(", ")}.`
95753
96142
  );
95754
96143
  }
96144
+ if (raw.formula === void 0 || raw.formula === null) {
96145
+ if (typeof raw.value === "string" && raw.value.startsWith("=")) {
96146
+ console.error(
96147
+ `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.`
96148
+ );
96149
+ }
96150
+ }
95755
96151
  return raw;
95756
96152
  }
96153
+ if (raw !== null && !["string", "number", "boolean"].includes(typeof raw)) {
96154
+ fail2(
96155
+ `Value of ${label} must be a string, number, boolean, null, or a { ${[...SIMPLE_CELL_KEYS].join(", ")} } object, got ${JSON.stringify(raw) ?? typeof raw}.`
96156
+ );
96157
+ }
96158
+ if (typeof raw === "string" && raw.startsWith("=")) {
96159
+ return { formula: raw.slice(1) };
96160
+ }
95757
96161
  return { value: raw };
95758
96162
  }
95759
- function valueToInput(value2) {
95760
- if (value2 === null || value2 === void 0) return "";
95761
- return String(value2);
95762
- }
95763
96163
  function readOptionFlag(rest2, name2) {
95764
96164
  const i2 = rest2.indexOf(name2);
95765
96165
  if (i2 < 0) return void 0;
@@ -95774,7 +96174,6 @@ function splitOptionalSheetRef(spec) {
95774
96174
  }
95775
96175
  function xlsxRead(filePath, rest2) {
95776
96176
  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
96177
  const rangeArg = readOptionFlag(rest2, "--range");
95779
96178
  const sheetArg = readOptionFlag(rest2, "--sheet");
95780
96179
  const withFormat = rest2.includes("--with-format");
@@ -95984,7 +96383,29 @@ function buildBatchCommand(workbook, engine, op, index) {
95984
96383
  const sheetIndex = resolveSheetIndex(workbook, op.sheet);
95985
96384
  const coord = refToRowCol(op.ref.toUpperCase());
95986
96385
  if (!coord) fail2(`[op ${index}] Invalid cell ref: ${op.ref}`);
95987
- return setCellValueCommand(workbook, engine.get(), sheetIndex, coord.row, coord.col, op.value);
96386
+ const cell = normalizeSimpleCell(op.value, `[op ${index}] set-cell ${op.sheet}!${op.ref}`);
96387
+ const commands = [
96388
+ cell.formula ? (
96389
+ // The entry path is the only one that writes a formula, and it takes
96390
+ // it spelled the way a person types it. Re-adding the `=` this
96391
+ // surface's reader just stripped is lossless and keeps the formula
96392
+ // write in one place.
96393
+ setCellValueCommand(workbook, engine.get(), sheetIndex, coord.row, coord.col, `=${cell.formula}`)
96394
+ ) : setCellValueCommand(workbook, engine.get(), sheetIndex, coord.row, coord.col, cell.value ?? null, {
96395
+ typed: true
96396
+ })
96397
+ ];
96398
+ if (cell.numFmt !== void 0) {
96399
+ commands.push(
96400
+ setRangeNumFmtCommand(workbook, sheetIndex, coord.row, coord.col, coord.row, coord.col, cell.numFmt)
96401
+ );
96402
+ }
96403
+ if (cell.style) {
96404
+ commands.push(
96405
+ setRangeStyleCommand(workbook, sheetIndex, coord.row, coord.col, coord.row, coord.col, cell.style)
96406
+ );
96407
+ }
96408
+ return commands.length === 1 ? commands[0] : batchCommand(`Set ${op.sheet}!${op.ref}`, commands);
95988
96409
  }
95989
96410
  case "clear-range": {
95990
96411
  const sheetIndex = resolveSheetIndex(workbook, op.sheet);
@@ -96041,6 +96462,8 @@ Uses Lotics' own xlsx engine; round-trips faithfully with the Lotics editor and
96041
96462
  lotics xlsx write <file> '<json>' Create .xlsx from {"sheets":[{"name","cells":{"A1":...}}]}
96042
96463
  A cell is a bare value, or {value|formula, numFmt, style}:
96043
96464
  {"A1":{"value":1234,"numFmt":"#,##0 \\"\u20AB\\"","style":{"fontBold":true}}}
96465
+ A bare "=A1+B1" is a formula; an object's "value" is
96466
+ always a literal, so use {"formula":"A1+B1"} there.
96044
96467
  A sheet also takes merges, freeze, colWidths ({"A":34}),
96045
96468
  rowHeights ({"1":44}). Unknown properties are rejected.
96046
96469
  lotics xlsx set-cell <file> <sheet>!<ref> '<v>' Set cell value (prefix '=' for formula)
@@ -101655,7 +102078,6 @@ function buildDoc(input) {
101655
102078
  }
101656
102079
  async function docxRead(filePath, rest2) {
101657
102080
  if (!filePath) fail2("Usage: lotics docx read <file> [--include-opaque]");
101658
- rejectUnknownFlags(rest2, ["--include-opaque"], "docx read");
101659
102081
  const includeOpaque = rest2.includes("--include-opaque");
101660
102082
  const doc = await loadFile2(filePath);
101661
102083
  console.log(JSON.stringify(readToJson2(doc, includeOpaque), null, 2));
@@ -101690,7 +102112,7 @@ function firstPositional(rest2) {
101690
102112
  return rest2.find((a) => !a.startsWith("--"));
101691
102113
  }
101692
102114
  async function docxAppendParagraph(filePath, rest2) {
101693
- rejectUnknownFlags(rest2, ["--style="], "docx append-paragraph");
102115
+ rejectUnknownFlags(rest2, "docx", "append-paragraph");
101694
102116
  const text = firstPositional(rest2);
101695
102117
  if (!filePath || text === void 0) fail2("Usage: lotics docx append-paragraph <file> '<text>' [--style=NAME]");
101696
102118
  const style = getStyleFlag(rest2);
@@ -101700,7 +102122,7 @@ async function docxAppendParagraph(filePath, rest2) {
101700
102122
  await writeDoc(filePath, replaceChildren(doc, newChildren));
101701
102123
  }
101702
102124
  async function docxInsertParagraph(filePath, rest2) {
101703
- rejectUnknownFlags(rest2, ["--style=", "--at="], "docx insert-paragraph");
102125
+ rejectUnknownFlags(rest2, "docx", "insert-paragraph");
101704
102126
  const text = firstPositional(rest2);
101705
102127
  const at2 = getAtFlag(rest2);
101706
102128
  if (!filePath || text === void 0 || at2 === void 0) {
@@ -101717,7 +102139,6 @@ async function docxInsertParagraph(filePath, rest2) {
101717
102139
  await writeDoc(filePath, replaceChildren(doc, newChildren));
101718
102140
  }
101719
102141
  async function docxDeleteBlock(filePath, rest2) {
101720
- rejectUnknownFlags(rest2, ["--at="], "docx delete-block");
101721
102142
  const at2 = getAtFlag(rest2);
101722
102143
  if (!filePath || at2 === void 0) fail2("Usage: lotics docx delete-block <file> --at=<index>");
101723
102144
  const doc = await loadFile2(filePath);