@lotics/cli 0.57.0 → 0.62.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
@@ -12243,12 +12243,12 @@ var require_tokens = __commonJS({
12243
12243
  });
12244
12244
  }
12245
12245
  exports2.assignCategoriesMapProp = assignCategoriesMapProp;
12246
- function singleAssignCategoriesToksMap(path7, nextNode) {
12247
- utils_1.forEach(path7, function(pathNode) {
12246
+ function singleAssignCategoriesToksMap(path8, nextNode) {
12247
+ utils_1.forEach(path8, function(pathNode) {
12248
12248
  nextNode.categoryMatchesMap[pathNode.tokenTypeIdx] = true;
12249
12249
  });
12250
12250
  utils_1.forEach(nextNode.CATEGORIES, function(nextCategory) {
12251
- var newPath = path7.concat(nextNode);
12251
+ var newPath = path8.concat(nextNode);
12252
12252
  if (!utils_1.contains(newPath, nextCategory)) {
12253
12253
  singleAssignCategoriesToksMap(newPath, nextCategory);
12254
12254
  }
@@ -14011,10 +14011,10 @@ var require_interpreter = __commonJS({
14011
14011
  /** @class */
14012
14012
  (function(_super) {
14013
14013
  __extends(AbstractNextPossibleTokensWalker2, _super);
14014
- function AbstractNextPossibleTokensWalker2(topProd, path7) {
14014
+ function AbstractNextPossibleTokensWalker2(topProd, path8) {
14015
14015
  var _this = _super.call(this) || this;
14016
14016
  _this.topProd = topProd;
14017
- _this.path = path7;
14017
+ _this.path = path8;
14018
14018
  _this.possibleTokTypes = [];
14019
14019
  _this.nextProductionName = "";
14020
14020
  _this.nextProductionOccurrence = 0;
@@ -14068,9 +14068,9 @@ var require_interpreter = __commonJS({
14068
14068
  /** @class */
14069
14069
  (function(_super) {
14070
14070
  __extends(NextAfterTokenWalker2, _super);
14071
- function NextAfterTokenWalker2(topProd, path7) {
14072
- var _this = _super.call(this, topProd, path7) || this;
14073
- _this.path = path7;
14071
+ function NextAfterTokenWalker2(topProd, path8) {
14072
+ var _this = _super.call(this, topProd, path8) || this;
14073
+ _this.path = path8;
14074
14074
  _this.nextTerminalName = "";
14075
14075
  _this.nextTerminalOccurrence = 0;
14076
14076
  _this.nextTerminalName = _this.path.lastTok.name;
@@ -14767,10 +14767,10 @@ var require_lookahead = __commonJS({
14767
14767
  }
14768
14768
  return result;
14769
14769
  }
14770
- function pathToHashKeys(path7) {
14770
+ function pathToHashKeys(path8) {
14771
14771
  var keys2 = [""];
14772
- for (var i2 = 0; i2 < path7.length; i2++) {
14773
- var tokType = path7[i2];
14772
+ for (var i2 = 0; i2 < path8.length; i2++) {
14773
+ var tokType = path8[i2];
14774
14774
  var longerKeys = [];
14775
14775
  for (var j = 0; j < keys2.length; j++) {
14776
14776
  var currShorterKey = keys2[j];
@@ -15082,9 +15082,9 @@ var require_checks = __commonJS({
15082
15082
  return errors;
15083
15083
  }
15084
15084
  exports2.validateRuleIsOverridden = validateRuleIsOverridden;
15085
- function validateNoLeftRecursion(topRule, currRule, errMsgProvider, path7) {
15086
- if (path7 === void 0) {
15087
- path7 = [];
15085
+ function validateNoLeftRecursion(topRule, currRule, errMsgProvider, path8) {
15086
+ if (path8 === void 0) {
15087
+ path8 = [];
15088
15088
  }
15089
15089
  var errors = [];
15090
15090
  var nextNonTerminals = getFirstNoneTerminal(currRule.definition);
@@ -15097,15 +15097,15 @@ var require_checks = __commonJS({
15097
15097
  errors.push({
15098
15098
  message: errMsgProvider.buildLeftRecursionError({
15099
15099
  topLevelRule: topRule,
15100
- leftRecursionPath: path7
15100
+ leftRecursionPath: path8
15101
15101
  }),
15102
15102
  type: parser_1.ParserDefinitionErrorType.LEFT_RECURSION,
15103
15103
  ruleName
15104
15104
  });
15105
15105
  }
15106
- var validNextSteps = utils.difference(nextNonTerminals, path7.concat([topRule]));
15106
+ var validNextSteps = utils.difference(nextNonTerminals, path8.concat([topRule]));
15107
15107
  var errorsFromNextSteps = utils.map(validNextSteps, function(currRefRule) {
15108
- var newPath = utils.cloneArr(path7);
15108
+ var newPath = utils.cloneArr(path8);
15109
15109
  newPath.push(currRefRule);
15110
15110
  return validateNoLeftRecursion(topRule, currRefRule, errMsgProvider, newPath);
15111
15111
  });
@@ -22714,8 +22714,8 @@ var require_utils4 = __commonJS({
22714
22714
  var result = transform[inputType][outputType](input);
22715
22715
  return result;
22716
22716
  };
22717
- exports2.resolve = function(path7) {
22718
- var parts = path7.split("/");
22717
+ exports2.resolve = function(path8) {
22718
+ var parts = path8.split("/");
22719
22719
  var result = [];
22720
22720
  for (var index = 0; index < parts.length; index++) {
22721
22721
  var part = parts[index];
@@ -28568,18 +28568,18 @@ var require_object = __commonJS({
28568
28568
  var object = new ZipObject(name, zipObjectContent, o);
28569
28569
  this.files[name] = object;
28570
28570
  };
28571
- var parentFolder = function(path7) {
28572
- if (path7.slice(-1) === "/") {
28573
- path7 = path7.substring(0, path7.length - 1);
28571
+ var parentFolder = function(path8) {
28572
+ if (path8.slice(-1) === "/") {
28573
+ path8 = path8.substring(0, path8.length - 1);
28574
28574
  }
28575
- var lastSlash = path7.lastIndexOf("/");
28576
- return lastSlash > 0 ? path7.substring(0, lastSlash) : "";
28575
+ var lastSlash = path8.lastIndexOf("/");
28576
+ return lastSlash > 0 ? path8.substring(0, lastSlash) : "";
28577
28577
  };
28578
- var forceTrailingSlash = function(path7) {
28579
- if (path7.slice(-1) !== "/") {
28580
- path7 += "/";
28578
+ var forceTrailingSlash = function(path8) {
28579
+ if (path8.slice(-1) !== "/") {
28580
+ path8 += "/";
28581
28581
  }
28582
- return path7;
28582
+ return path8;
28583
28583
  };
28584
28584
  var folderAdd = function(name, createFolders) {
28585
28585
  createFolders = typeof createFolders !== "undefined" ? createFolders : defaults2.createFolders;
@@ -29579,8 +29579,10 @@ var require_lib3 = __commonJS({
29579
29579
  });
29580
29580
 
29581
29581
  // src/cli.ts
29582
- import fs7 from "node:fs";
29583
- import path6 from "node:path";
29582
+ import dns from "node:dns";
29583
+ import net2 from "node:net";
29584
+ import fs8 from "node:fs";
29585
+ import path7 from "node:path";
29584
29586
  import readline from "node:readline";
29585
29587
 
29586
29588
  // src/client.ts
@@ -29678,8 +29680,8 @@ var LoticsClient = class {
29678
29680
  }
29679
29681
  return headers;
29680
29682
  }
29681
- async request(method, path7, body) {
29682
- const url = `${this.baseUrl}${path7}`;
29683
+ async request(method, path8, body) {
29684
+ const url = `${this.baseUrl}${path8}`;
29683
29685
  const headers = this.buildHeaders();
29684
29686
  const init = { method, headers };
29685
29687
  if (body !== void 0) {
@@ -29744,6 +29746,38 @@ var LoticsClient = class {
29744
29746
  async createApp(body) {
29745
29747
  return this.request("POST", "/v1/apps", body);
29746
29748
  }
29749
+ /**
29750
+ * Resolve the display name + fields (incl. select options) of the given tables
29751
+ * — the schema `lotics app codegen` turns into the runtime `.lotics/app_fields.ts`
29752
+ * alias maps. One `get_table` call per id (the tool surface has no batch
29753
+ * variant); a missing/inaccessible table is dropped rather than throwing, so a
29754
+ * stale id in the scope set never fails codegen.
29755
+ */
29756
+ async getWorkspaceSchema(tableIds) {
29757
+ const tables = await Promise.all(
29758
+ tableIds.map(async (table_id) => {
29759
+ const res = await this.execute("get_table", { table_id });
29760
+ if (res.error || res.result === null || typeof res.result !== "object") return null;
29761
+ const table = res.result;
29762
+ if (typeof table.id !== "string" || typeof table.name !== "string" || !Array.isArray(table.fields)) {
29763
+ return null;
29764
+ }
29765
+ const fields = table.fields.flatMap((field) => {
29766
+ if (field === null || typeof field !== "object") return [];
29767
+ const f = field;
29768
+ if (typeof f.key !== "string" || typeof f.name !== "string") return [];
29769
+ const options = Array.isArray(f.options) ? f.options.flatMap((opt) => {
29770
+ if (opt === null || typeof opt !== "object") return [];
29771
+ const o = opt;
29772
+ return typeof o.key === "string" && typeof o.name === "string" ? [{ id: o.key, label: o.name }] : [];
29773
+ }) : void 0;
29774
+ return [{ id: f.key, name: f.name, ...options && options.length > 0 ? { options } : {} }];
29775
+ });
29776
+ return { id: table.id, name: table.name, fields };
29777
+ })
29778
+ );
29779
+ return tables.filter((t) => t !== null);
29780
+ }
29747
29781
  /**
29748
29782
  * Rename an app's public subdomain — its `<slug>.lotics.app` address.
29749
29783
  * Mirrors PUT /v1/apps/{app_id}/subdomain. The old subdomain stops
@@ -29818,6 +29852,52 @@ var LoticsClient = class {
29818
29852
  if (response.ok) return parsed ?? {};
29819
29853
  return { status: "error", message: transportErrorMessage(response.status, parsed) };
29820
29854
  }
29855
+ /**
29856
+ * Bind (create or replace) an app workflow by alias via the `set_app_workflow`
29857
+ * tool — the SINGLE author of `apps.workflows` + the workflow row. `source` is
29858
+ * the verbatim JS-subset body (no `on({...})` trigger). `inputs`/`outputs` are
29859
+ * the typed schemas declared in `package.json#lotics.workflows.<alias>`. The
29860
+ * server re-verifies the body and echoes the bound `outputs` (declared, else
29861
+ * DERIVED from `return({ data })`), so the CLI can show the author what shape
29862
+ * `result.data` will carry. Wraps the tool rather than a bespoke endpoint so
29863
+ * the file flow stays a convenience over the existing single-author contract.
29864
+ */
29865
+ async setAppWorkflow(app_id, alias, body) {
29866
+ return this.execute("set_app_workflow", {
29867
+ app_id,
29868
+ alias,
29869
+ source: body.source,
29870
+ ...body.inputs ? { inputs: body.inputs } : {},
29871
+ ...body.outputs ? { outputs: body.outputs } : {},
29872
+ ...body.name ? { name: body.name } : {},
29873
+ ...body.description ? { description: body.description } : {}
29874
+ });
29875
+ }
29876
+ /**
29877
+ * Fetch one app workflow's faithful source + bound input/output schemas via
29878
+ * `get_app_workflow`. `source` is the JS-subset body re-rendered from the
29879
+ * persisted step tree (incl. the `return({ data })` clause, opaque field/option
29880
+ * keys) — the exact text `lotics app workflow set` would push back. Feeds
29881
+ * `lotics app pull`, which writes it to `src/workflows/<alias>.ts`.
29882
+ */
29883
+ async getAppWorkflow(app_id, alias) {
29884
+ return this.execute("get_app_workflow", { app_id, alias });
29885
+ }
29886
+ /**
29887
+ * Fetch the server-generated workspace `.d.ts` + the wrapper envelope that
29888
+ * make a `src/workflows/<alias>.ts` body locally typecheckable (GAP-59).
29889
+ * The server is the single source of the type model — the CLI never
29890
+ * re-implements it. `envelope_prefix`/`envelope_suffix` are the exact
29891
+ * `async function __workflow(): …` wrapper the server compiles inside, so the
29892
+ * local typecheck mirrors the set-time verdict. Mirrors
29893
+ * POST /v1/apps/{app_id}/workflows/{alias}/dts.
29894
+ */
29895
+ async getAppWorkflowDts(app_id, alias) {
29896
+ return this.request(
29897
+ "POST",
29898
+ `/v1/apps/${encodeURIComponent(app_id)}/workflows/${encodeURIComponent(alias)}/dts`
29899
+ );
29900
+ }
29821
29901
  /**
29822
29902
  * Open a streaming agent run and return the RAW streamed `Response` (the
29823
29903
  * caller reads `res.body`). Unlike `request`, this does not buffer/parse the
@@ -30262,8 +30342,8 @@ var pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
30262
30342
  var VERSION = pkg.version;
30263
30343
 
30264
30344
  // src/app_commands.ts
30265
- import fs3 from "node:fs";
30266
- import path4 from "node:path";
30345
+ import fs4 from "node:fs";
30346
+ import path5 from "node:path";
30267
30347
  import { spawn as spawn2 } from "node:child_process";
30268
30348
  import { tmpdir } from "node:os";
30269
30349
 
@@ -30323,7 +30403,9 @@ function buildStarterTemplate(args) {
30323
30403
  "@vitejs/plugin-react": "^4.3.0",
30324
30404
  jsdom: "^25.0.0",
30325
30405
  oxlint: "^0.13.0",
30326
- typescript: "^5.6.0",
30406
+ // Track the platform's TS major so `lotics app workflow check` (which runs
30407
+ // the app's OWN typescript) gives the same verdict the server does at set-time.
30408
+ typescript: "~6.0.3",
30327
30409
  vite: "^5.4.0",
30328
30410
  vitest: "^2.1.0"
30329
30411
  },
@@ -30366,7 +30448,18 @@ function buildStarterTemplate(args) {
30366
30448
  // fall to `unknown`. The explicit glob makes the dot-dir the non-wildcard
30367
30449
  // base, which IS read. (GAP-38.)
30368
30450
  include: ["src", ".lotics/**/*"],
30369
- exclude: ["node_modules"]
30451
+ // src/workflows/<alias>.ts bodies run on the server (workflow globals:
30452
+ // trigger / runtime / tool calls), and each pulls its own per-alias
30453
+ // ambient globals under `.lotics/workflows/`. BOTH are excluded from
30454
+ // the main typecheck: the bodies would apply the app's DOM lib (the
30455
+ // server doesn't), and the per-alias `.globals.d.ts` files each declare
30456
+ // their OWN ambient `trigger` — loading several into one program
30457
+ // collides those declarations and reports false errors. Type-check the
30458
+ // bodies with `lotics app workflow check` instead — it builds ONE
30459
+ // isolated program per alias (mirroring the server's set-time verify),
30460
+ // so the ambient `trigger` is unambiguous. The server stays the single
30461
+ // verifier on `lotics app workflow set`.
30462
+ exclude: ["node_modules", "src/workflows", ".lotics/workflows"]
30370
30463
  },
30371
30464
  null,
30372
30465
  2
@@ -30705,6 +30798,63 @@ const routes = [
30705
30798
  export default function App() {
30706
30799
  return <AppRouter routes={routes} />;
30707
30800
  }
30801
+ `
30802
+ },
30803
+ {
30804
+ // The home for editable workflow bodies. `lotics app pull` writes one
30805
+ // `src/workflows/<alias>.ts` per bound workflow (faithful server source);
30806
+ // edit it, then `lotics app workflow set <alias>` pushes it back through
30807
+ // set_app_workflow (the server verifies). A new app has no bound workflows
30808
+ // yet, so this is just the docked directory + the loop reference. It's a
30809
+ // .md (not a .ts) so tsc's `include: ["src"]` never tries to compile it —
30810
+ // a workflow body is a JS-subset expression over server globals
30811
+ // (trigger / runtime / tool calls) that does NOT typecheck standalone.
30812
+ path: "src/workflows/README.md",
30813
+ content: `# Workflow bodies
30814
+
30815
+ Editable JS-subset bodies of this app's workflows live here, one file per alias:
30816
+ \`src/workflows/<alias>.ts\`.
30817
+
30818
+ ## Loop
30819
+
30820
+ \`\`\`bash
30821
+ lotics app workflow pull # write/refresh every src/workflows/<alias>.ts from the server
30822
+ # edit src/workflows/<alias>.ts
30823
+ lotics app workflow check <alias> # typecheck the body locally (same verdict as \`set\`)
30824
+ lotics app workflow set <alias> # push it back through set_app_workflow (the server verifies)
30825
+ \`\`\`
30826
+
30827
+ \`lotics app pull <app_id>\` also writes these files (alongside the rest of the project).
30828
+
30829
+ ## What a body is
30830
+
30831
+ A workflow body is a **JS-subset expression** that runs server-side using workflow
30832
+ globals (\`trigger\`, \`runtime\`, tool calls) and ends with \`return({ data })\`. Each
30833
+ pulled file wraps the body in the server's \`__workflow\` envelope and references its
30834
+ per-alias ambient globals at \`.lotics/workflows/<alias>.globals.d.ts\`, so it **is**
30835
+ locally typecheckable:
30836
+
30837
+ \`\`\`bash
30838
+ lotics app workflow check # check every body; [alias] to check one
30839
+ \`\`\`
30840
+
30841
+ \`check\` builds ONE isolated program per alias (the app's own \`typescript\`, the
30842
+ server's compile options \u2014 strict, lib es2022 with no DOM), so each body's ambient
30843
+ \`trigger\` is unambiguous and the verdict mirrors set-time verify. The main
30844
+ \`npm run typecheck\` excludes \`src/workflows\` + \`.lotics/workflows\` (the bodies would
30845
+ apply the app's DOM lib, and the per-alias globals collide on \`trigger\`). The
30846
+ verification that matters still runs on the **server** when you \`set\` \u2014 the same
30847
+ guarantee as authoring via \`set_app_workflow\` directly. Edit only the body BETWEEN
30848
+ the wrapper lines; the wrapper, the \`/// <reference>\`, and the \`export {};\` marker
30849
+ are CLI bookkeeping (stripped on \`set\`). Do not rename a file (the filename is the
30850
+ alias the binding is keyed by).
30851
+
30852
+ ## Authority
30853
+
30854
+ \`lotics app deploy\` never authors workflows \u2014 it carries code, queries, and
30855
+ capabilities only. \`apps.workflows\` has exactly one author: \`set_app_workflow\`
30856
+ (which \`lotics app workflow set\` calls). The typed \`inputs\`/\`outputs\` schema for
30857
+ each alias lives in \`package.json#lotics.workflows.<alias>\`.
30708
30858
  `
30709
30859
  },
30710
30860
  {
@@ -30894,6 +31044,18 @@ import http from "node:http";
30894
31044
  import net from "node:net";
30895
31045
  import { spawn } from "node:child_process";
30896
31046
 
31047
+ // src/child_env.ts
31048
+ function ipv4ChildEnv(env) {
31049
+ return {
31050
+ ...env,
31051
+ NODE_OPTIONS: [
31052
+ env.NODE_OPTIONS,
31053
+ "--dns-result-order=ipv4first",
31054
+ "--network-family-autoselection-attempt-timeout=2000"
31055
+ ].filter(Boolean).join(" ")
31056
+ };
31057
+ }
31058
+
30897
31059
  // src/dev/rpc_handler.ts
30898
31060
  var SUPPORTED_OPS = /* @__PURE__ */ new Set([
30899
31061
  "query",
@@ -31354,7 +31516,7 @@ async function startDevServer(args) {
31354
31516
  {
31355
31517
  cwd: args.projectDir,
31356
31518
  stdio: [process.stdin.isTTY ? "inherit" : "ignore", "inherit", "inherit"],
31357
- env: { ...process.env, FORCE_COLOR: "1" }
31519
+ env: ipv4ChildEnv({ ...process.env, FORCE_COLOR: "1" })
31358
31520
  }
31359
31521
  );
31360
31522
  let stopped = false;
@@ -31778,6 +31940,254 @@ ${lines.join("\n")}
31778
31940
  `;
31779
31941
  }
31780
31942
 
31943
+ // ../shared/src/app_query_ast.ts
31944
+ function collectQueryTableIds(node) {
31945
+ const result = /* @__PURE__ */ new Set();
31946
+ walk(node, (n) => {
31947
+ if (n.kind === "from_table") result.add(n.table_id);
31948
+ });
31949
+ return result;
31950
+ }
31951
+ function walk(node, visit) {
31952
+ visit(node);
31953
+ switch (node.kind) {
31954
+ case "from_table":
31955
+ return;
31956
+ case "project":
31957
+ case "filter":
31958
+ case "group":
31959
+ case "window":
31960
+ case "sort":
31961
+ case "limit":
31962
+ case "unpivot":
31963
+ walk(node.from, visit);
31964
+ return;
31965
+ case "join":
31966
+ walk(node.left, visit);
31967
+ walk(node.right, visit);
31968
+ return;
31969
+ case "union":
31970
+ for (const child of node.sources) walk(child, visit);
31971
+ return;
31972
+ default: {
31973
+ const _exhaustive = node;
31974
+ void _exhaustive;
31975
+ return;
31976
+ }
31977
+ }
31978
+ }
31979
+
31980
+ // src/generate_app_fields.ts
31981
+ var HEADER4 = `// Auto-generated by 'lotics app codegen' (and app pull/dev/deploy).
31982
+ // DO NOT EDIT \u2014 regenerated from the workspace schema.
31983
+ //
31984
+ // Runtime field + option ids addressed by stable display-name aliases:
31985
+ // record.data[F.<TABLE>.<field>] \u2192 "fld_\u2026"
31986
+ // value === OPT.<TABLE>.<field>.<option> \u2192 "opt_\u2026"
31987
+ // A rename on the platform re-runs codegen and moves these in lockstep.
31988
+ `;
31989
+ function slugifyAlias(name, upper) {
31990
+ const stripped = name.normalize("NFD").replace(/[̀-ͯ]/g, "").replace(/đ/g, "d").replace(/Đ/g, "D");
31991
+ const cased = upper ? stripped.toUpperCase() : stripped.toLowerCase();
31992
+ const slug = cased.replace(/[^a-zA-Z0-9]+/g, "_").replace(/^_+|_+$/g, "");
31993
+ if (slug === "") return "_";
31994
+ return /^[0-9]/.test(slug) ? `_${slug}` : slug;
31995
+ }
31996
+ function dedupeAliases(names, upper) {
31997
+ const used = /* @__PURE__ */ new Map();
31998
+ return names.map((name) => {
31999
+ const base = slugifyAlias(name, upper);
32000
+ const seen = used.get(base);
32001
+ if (seen === void 0) {
32002
+ used.set(base, 1);
32003
+ return base;
32004
+ }
32005
+ let n = seen + 1;
32006
+ while (used.has(`${base}_${n}`)) n++;
32007
+ used.set(base, n);
32008
+ used.set(`${base}_${n}`, 1);
32009
+ return `${base}_${n}`;
32010
+ });
32011
+ }
32012
+ function propKey(alias) {
32013
+ return isValidIdentifier(alias) ? alias : JSON.stringify(alias);
32014
+ }
32015
+ function aliasTables(tables) {
32016
+ const tableAliases = dedupeAliases(
32017
+ tables.map((t) => t.name),
32018
+ true
32019
+ );
32020
+ return tables.map((table, i2) => {
32021
+ const fieldAliases = dedupeAliases(
32022
+ table.fields.map((f) => f.name),
32023
+ false
32024
+ );
32025
+ return {
32026
+ alias: tableAliases[i2],
32027
+ table,
32028
+ fields: table.fields.map((field, j) => ({ alias: fieldAliases[j], field }))
32029
+ };
32030
+ });
32031
+ }
32032
+ function emitFieldMap(aliased) {
32033
+ const tableBlocks = aliased.map(({ alias, fields }) => {
32034
+ const fieldLines = fields.map(
32035
+ ({ alias: fieldAlias, field }) => ` ${propKey(fieldAlias)}: ${JSON.stringify(field.id)},`
32036
+ );
32037
+ return ` ${propKey(alias)}: {
32038
+ ${fieldLines.join("\n")}
32039
+ },`;
32040
+ });
32041
+ return `export const F = {
32042
+ ${tableBlocks.join("\n")}
32043
+ } as const;`;
32044
+ }
32045
+ function emitOptionMap(aliased) {
32046
+ const tableBlocks = [];
32047
+ for (const { alias, fields } of aliased) {
32048
+ const fieldBlocks = [];
32049
+ for (const { alias: fieldAlias, field } of fields) {
32050
+ const options = field.options ?? [];
32051
+ if (options.length === 0) continue;
32052
+ const optionAliases = dedupeAliases(
32053
+ options.map((o) => o.label),
32054
+ false
32055
+ );
32056
+ const optionLines = options.map(
32057
+ (option, i2) => ` ${propKey(optionAliases[i2])}: ${JSON.stringify(option.id)},`
32058
+ );
32059
+ fieldBlocks.push(` ${propKey(fieldAlias)}: {
32060
+ ${optionLines.join("\n")}
32061
+ },`);
32062
+ }
32063
+ if (fieldBlocks.length === 0) continue;
32064
+ tableBlocks.push(` ${propKey(alias)}: {
32065
+ ${fieldBlocks.join("\n")}
32066
+ },`);
32067
+ }
32068
+ if (tableBlocks.length === 0) return `export const OPT = {} as const;`;
32069
+ return `export const OPT = {
32070
+ ${tableBlocks.join("\n")}
32071
+ } as const;`;
32072
+ }
32073
+ function generateAppFields(tables) {
32074
+ if (tables.length === 0) {
32075
+ return `${HEADER4}
32076
+ export const F = {} as const;
32077
+
32078
+ export const OPT = {} as const;
32079
+
32080
+ /** Field-id alias map (empty \u2014 no tables in scope). */
32081
+ export type AppFields = typeof F;
32082
+ /** Select-option alias map (empty \u2014 no tables in scope). */
32083
+ export type AppOptions = typeof OPT;
32084
+ `;
32085
+ }
32086
+ const aliased = aliasTables(tables);
32087
+ return `${HEADER4}
32088
+ ${emitFieldMap(aliased)}
32089
+
32090
+ ${emitOptionMap(aliased)}
32091
+
32092
+ /** Field-id alias map: \`F[<TABLE>][<field>]\` is the \`fld_\u2026\` id (literal-typed). */
32093
+ export type AppFields = typeof F;
32094
+ /** Select-option alias map: \`OPT[<TABLE>][<field>][<option>]\` is the \`opt_\u2026\` id. */
32095
+ export type AppOptions = typeof OPT;
32096
+ `;
32097
+ }
32098
+
32099
+ // src/app_workflow_check.ts
32100
+ import fs3 from "node:fs";
32101
+ import path4 from "node:path";
32102
+ import { createRequire } from "node:module";
32103
+ function workflowCheckCompilerOptions(tsApi) {
32104
+ return {
32105
+ target: tsApi.ScriptTarget.ES2022,
32106
+ module: tsApi.ModuleKind.ESNext,
32107
+ lib: ["lib.es2022.d.ts"],
32108
+ strict: true,
32109
+ noImplicitAny: true,
32110
+ strictNullChecks: true,
32111
+ noImplicitReturns: false,
32112
+ noEmit: true,
32113
+ allowJs: false,
32114
+ isolatedModules: false,
32115
+ skipLibCheck: true,
32116
+ moduleResolution: tsApi.ModuleResolutionKind.NodeNext,
32117
+ types: [],
32118
+ declaration: false
32119
+ };
32120
+ }
32121
+ function bodyLineOffset(wrappedSource) {
32122
+ const lines = wrappedSource.split("\n");
32123
+ for (let i2 = 0; i2 < lines.length; i2++) {
32124
+ if (/^\s*async\s+function\s+__workflow\s*\(/.test(lines[i2])) return i2 + 1;
32125
+ }
32126
+ return 0;
32127
+ }
32128
+ async function loadProjectTypescript(projectDir) {
32129
+ const requireFromProject = createRequire(path4.join(projectDir, "package.json"));
32130
+ let resolved;
32131
+ try {
32132
+ resolved = requireFromProject.resolve("typescript");
32133
+ } catch {
32134
+ throw new Error(
32135
+ `Could not resolve 'typescript' from ${projectDir}. 'lotics app workflow check' type-checks bodies with the app's own compiler \u2014 run 'npm install' (the scaffold ships typescript as a devDependency).`
32136
+ );
32137
+ }
32138
+ const mod = await import(pathToImportUrl(resolved));
32139
+ return mod.default ?? mod;
32140
+ }
32141
+ function pathToImportUrl(p) {
32142
+ const resolved = path4.resolve(p);
32143
+ const prefixed = resolved.startsWith("/") ? resolved : `/${resolved}`;
32144
+ return `file://${prefixed.split(path4.sep).join("/")}`;
32145
+ }
32146
+ function checkOneWorkflowBody(tsApi, input) {
32147
+ const wrapped = fs3.readFileSync(input.bodyPath, "utf-8");
32148
+ const offset = bodyLineOffset(wrapped);
32149
+ const program = tsApi.createProgram({
32150
+ rootNames: [input.globalsPath, input.bodyPath],
32151
+ options: workflowCheckCompilerOptions(tsApi)
32152
+ });
32153
+ const bodyFile = program.getSourceFile(input.bodyPath);
32154
+ if (!bodyFile) {
32155
+ return [
32156
+ {
32157
+ line: 1,
32158
+ col: 1,
32159
+ code: "INTERNAL",
32160
+ message: `workflow check: body file was not loaded (${input.bodyPath})`
32161
+ }
32162
+ ];
32163
+ }
32164
+ const issues = [];
32165
+ for (const d of tsApi.getPreEmitDiagnostics(program)) {
32166
+ if (d.file !== bodyFile) continue;
32167
+ let line = 1;
32168
+ let col = 1;
32169
+ if (typeof d.start === "number") {
32170
+ const lc2 = bodyFile.getLineAndCharacterOfPosition(d.start);
32171
+ line = Math.max(1, lc2.line + 1 - offset);
32172
+ col = lc2.character + 1;
32173
+ }
32174
+ issues.push({
32175
+ line,
32176
+ col,
32177
+ code: `TS${d.code}`,
32178
+ message: tsApi.flattenDiagnosticMessageText(d.messageText, "\n")
32179
+ });
32180
+ }
32181
+ return issues;
32182
+ }
32183
+ function checkWorkflowBodies(tsApi, aliases) {
32184
+ return aliases.map(({ alias, input }) => ({
32185
+ alias,
32186
+ bodyPath: input.bodyPath,
32187
+ issues: checkOneWorkflowBody(tsApi, input)
32188
+ }));
32189
+ }
32190
+
31781
32191
  // src/app_commands.ts
31782
32192
  async function fetchLatestNpmVersion(packageName) {
31783
32193
  try {
@@ -31795,6 +32205,100 @@ async function fetchLatestNpmVersion(packageName) {
31795
32205
  return null;
31796
32206
  }
31797
32207
  }
32208
+ var WORKFLOWS_DIR = path5.join("src", "workflows");
32209
+ var WORKFLOW_GLOBALS_DIR = path5.join(".lotics", "workflows");
32210
+ var WORKFLOW_TSCONFIG_EXCLUDES = ["src/workflows", ".lotics/workflows"];
32211
+ var FALLBACK_ENVELOPE_PREFIX = "async function __workflow(): Promise<__WorkflowReturn | void> {\n";
32212
+ var FALLBACK_ENVELOPE_SUFFIX = "\n}";
32213
+ function workflowFileHeader(alias) {
32214
+ const refPath = path5.join("..", "..", WORKFLOW_GLOBALS_DIR, `${alias}.globals.d.ts`).split(path5.sep).join("/");
32215
+ return `/// <reference path="${refPath}" />
32216
+ // Auto-pulled workflow body for "${alias}". Edit the BODY between the wrapper
32217
+ // lines below, then check + push with:
32218
+ // lotics app workflow check ${alias}
32219
+ // lotics app workflow set ${alias}
32220
+ // The push goes through set_app_workflow, where the SERVER verifies the body.
32221
+ // The __workflow wrapper + the reference above are CLI bookkeeping (stripped on
32222
+ // set) \u2014 they only make the body typecheck locally against the workspace types.
32223
+ // Do NOT rename this file \u2014 the filename is the alias the binding is keyed by.
32224
+ export {};
32225
+ `;
32226
+ }
32227
+ function workflowFilePath(projectDir, alias) {
32228
+ return path5.join(projectDir, WORKFLOWS_DIR, `${alias}.ts`);
32229
+ }
32230
+ function workflowGlobalsPath(projectDir, alias) {
32231
+ return path5.join(projectDir, WORKFLOW_GLOBALS_DIR, `${alias}.globals.d.ts`);
32232
+ }
32233
+ function writeWorkflowGlobals(projectDir, alias, dts) {
32234
+ const dir = path5.join(projectDir, WORKFLOW_GLOBALS_DIR);
32235
+ fs4.mkdirSync(dir, { recursive: true });
32236
+ const file = workflowGlobalsPath(projectDir, alias);
32237
+ fs4.writeFileSync(file, `${dts.replace(/\s+$/, "")}
32238
+ `);
32239
+ return file;
32240
+ }
32241
+ function writeWorkflowFile(projectDir, alias, source, envelope = { prefix: FALLBACK_ENVELOPE_PREFIX, suffix: FALLBACK_ENVELOPE_SUFFIX }) {
32242
+ const dir = path5.join(projectDir, WORKFLOWS_DIR);
32243
+ fs4.mkdirSync(dir, { recursive: true });
32244
+ const file = workflowFilePath(projectDir, alias);
32245
+ const body = source.replace(/\s+$/, "");
32246
+ fs4.writeFileSync(file, `${workflowFileHeader(alias)}
32247
+ ${envelope.prefix}${body}${envelope.suffix}
32248
+ `);
32249
+ return file;
32250
+ }
32251
+ var WORKFLOW_WRAPPER_OPENER = /^\s*async\s+function\s+__workflow\s*\(/;
32252
+ function stripWorkflowHeader(content) {
32253
+ const lines = content.split("\n");
32254
+ let headerEnd = 0;
32255
+ while (headerEnd < lines.length && /^\s*\/\//.test(lines[headerEnd])) headerEnd++;
32256
+ while (headerEnd < lines.length && /^\s*export\s*\{\s*\}\s*;?\s*$/.test(lines[headerEnd])) headerEnd++;
32257
+ while (headerEnd < lines.length && lines[headerEnd].trim() === "") headerEnd++;
32258
+ let start;
32259
+ if (headerEnd < lines.length && WORKFLOW_WRAPPER_OPENER.test(lines[headerEnd])) {
32260
+ start = headerEnd;
32261
+ } else if (lines.length > 0 && WORKFLOW_WRAPPER_OPENER.test(lines[0])) {
32262
+ start = 0;
32263
+ } else {
32264
+ return content.replace(/\s+$/, "");
32265
+ }
32266
+ let end = lines.length - 1;
32267
+ while (end > start && lines[end].trim() === "") end--;
32268
+ if (lines[end]?.trim() === "}") {
32269
+ return lines.slice(start + 1, end).join("\n").replace(/\s+$/, "");
32270
+ }
32271
+ return lines.slice(start + 1).join("\n").replace(/\s+$/, "");
32272
+ }
32273
+ async function writeWorkflowFiles(client, projectDir, app_id, aliases) {
32274
+ const written = [];
32275
+ for (const alias of aliases) {
32276
+ const res = await client.getAppWorkflow(app_id, alias);
32277
+ const source = res.error || res.result === null || typeof res.result !== "object" ? null : res.result.source;
32278
+ if (typeof source !== "string" || source.trim() === "") {
32279
+ console.error(
32280
+ `\u26A0 Skipped src/workflows/${alias}.ts \u2014 the server returned no readable source (${res.error ?? "legacy workflow with no rendered body"}).`
32281
+ );
32282
+ continue;
32283
+ }
32284
+ const envelope = await fetchWorkflowGlobals(client, projectDir, app_id, alias);
32285
+ writeWorkflowFile(projectDir, alias, source, envelope);
32286
+ written.push(alias);
32287
+ }
32288
+ return written;
32289
+ }
32290
+ async function fetchWorkflowGlobals(client, projectDir, app_id, alias) {
32291
+ try {
32292
+ const { dts, envelope_prefix, envelope_suffix } = await client.getAppWorkflowDts(app_id, alias);
32293
+ writeWorkflowGlobals(projectDir, alias, dts);
32294
+ return { prefix: envelope_prefix, suffix: envelope_suffix };
32295
+ } catch (err2) {
32296
+ console.error(
32297
+ `\u26A0 Could not fetch workflow types for "${alias}" (${err2 instanceof Error ? err2.message : String(err2)}). Wrote the body with the fallback wrapper; its local typecheck may be degraded.`
32298
+ );
32299
+ return { prefix: FALLBACK_ENVELOPE_PREFIX, suffix: FALLBACK_ENVELOPE_SUFFIX };
32300
+ }
32301
+ }
31798
32302
  function runTar(args, cwd) {
31799
32303
  return new Promise((resolve, reject2) => {
31800
32304
  const proc = spawn2("tar", args, { cwd, stdio: ["ignore", "ignore", "pipe"] });
@@ -31811,7 +32315,7 @@ function runTar(args, cwd) {
31811
32315
  }
31812
32316
  function runNpm(args, cwd) {
31813
32317
  return new Promise((resolve, reject2) => {
31814
- const proc = spawn2("npm", args, { cwd, stdio: "inherit" });
32318
+ const proc = spawn2("npm", args, { cwd, stdio: "inherit", env: ipv4ChildEnv(process.env) });
31815
32319
  proc.on("error", reject2);
31816
32320
  proc.on("exit", (code) => {
31817
32321
  if (code === 0) resolve();
@@ -31820,11 +32324,11 @@ function runNpm(args, cwd) {
31820
32324
  });
31821
32325
  }
31822
32326
  function readAppMeta(projectDir) {
31823
- const pkgPath2 = path4.join(projectDir, "package.json");
31824
- if (!fs3.existsSync(pkgPath2)) {
32327
+ const pkgPath2 = path5.join(projectDir, "package.json");
32328
+ if (!fs4.existsSync(pkgPath2)) {
31825
32329
  throw new Error(`No package.json found at ${projectDir}. Run 'lotics app create' first.`);
31826
32330
  }
31827
- const pkg2 = JSON.parse(fs3.readFileSync(pkgPath2, "utf-8"));
32331
+ const pkg2 = JSON.parse(fs4.readFileSync(pkgPath2, "utf-8"));
31828
32332
  if (!pkg2.lotics?.app_id || !pkg2.lotics.workspace_id) {
31829
32333
  throw new Error(
31830
32334
  `package.json is missing the 'lotics' field with app_id and workspace_id. Was this folder created with 'lotics app create'?`
@@ -31842,27 +32346,109 @@ function readAppMeta(projectDir) {
31842
32346
  };
31843
32347
  }
31844
32348
  function writeAppMeta(projectDir, meta) {
31845
- const pkgPath2 = path4.join(projectDir, "package.json");
31846
- const pkg2 = JSON.parse(fs3.readFileSync(pkgPath2, "utf-8"));
32349
+ const pkgPath2 = path5.join(projectDir, "package.json");
32350
+ const pkg2 = JSON.parse(fs4.readFileSync(pkgPath2, "utf-8"));
31847
32351
  pkg2.lotics = meta;
31848
- fs3.writeFileSync(pkgPath2, JSON.stringify(pkg2, null, 2) + "\n");
32352
+ fs4.writeFileSync(pkgPath2, JSON.stringify(pkg2, null, 2) + "\n");
31849
32353
  }
31850
- function writeAppDts(projectDir, manifest) {
31851
- const dotLotics = path4.join(projectDir, ".lotics");
31852
- fs3.mkdirSync(dotLotics, { recursive: true });
31853
- fs3.writeFileSync(
31854
- path4.join(dotLotics, "app_workflows.d.ts"),
31855
- generateAppWorkflowsDts(manifest.workflows)
31856
- );
31857
- fs3.writeFileSync(
31858
- path4.join(dotLotics, "app_queries.d.ts"),
31859
- generateAppQueriesDts(manifest.queries)
31860
- );
31861
- fs3.writeFileSync(
31862
- path4.join(dotLotics, "app_agents.d.ts"),
31863
- generateAppAgentsDts(manifest.agents)
32354
+ function ensureWorkflowTsconfigExcludes(projectDir) {
32355
+ const tsconfigPath = path5.join(projectDir, "tsconfig.json");
32356
+ if (!fs4.existsSync(tsconfigPath)) {
32357
+ console.error(
32358
+ `\u26A0 No tsconfig.json at ${projectDir} \u2014 could not ensure ${WORKFLOW_TSCONFIG_EXCLUDES.join(", ")} are excluded. Add them to your tsconfig's "exclude" so npm run typecheck skips the workflow bodies.`
32359
+ );
32360
+ return;
32361
+ }
32362
+ let parsed;
32363
+ try {
32364
+ parsed = JSON.parse(fs4.readFileSync(tsconfigPath, "utf-8"));
32365
+ } catch (err2) {
32366
+ console.error(
32367
+ `\u26A0 Could not parse tsconfig.json (${err2 instanceof Error ? err2.message : String(err2)}) \u2014 add ${WORKFLOW_TSCONFIG_EXCLUDES.join(", ")} to its "exclude" manually so npm run typecheck skips the workflow bodies.`
32368
+ );
32369
+ return;
32370
+ }
32371
+ if (!parsed || typeof parsed !== "object") return;
32372
+ const current = Array.isArray(parsed.exclude) ? parsed.exclude : [];
32373
+ const currentStrings = current.filter((e) => typeof e === "string");
32374
+ const toAdd = WORKFLOW_TSCONFIG_EXCLUDES.filter((g) => !currentStrings.includes(g));
32375
+ if (toAdd.length === 0) return;
32376
+ parsed.exclude = [...currentStrings, ...toAdd];
32377
+ fs4.writeFileSync(tsconfigPath, JSON.stringify(parsed, null, 2) + "\n");
32378
+ console.error(
32379
+ `Patched tsconfig.json: added ${toAdd.join(", ")} to "exclude" so npm run typecheck skips the workflow bodies (check them with: lotics app workflow check).`
31864
32380
  );
31865
32381
  }
32382
+ function writeAppDts(projectDir, manifest) {
32383
+ const dotLotics = path5.join(projectDir, ".lotics");
32384
+ fs4.mkdirSync(dotLotics, { recursive: true });
32385
+ const written = [
32386
+ [path5.join(dotLotics, "app_workflows.d.ts"), generateAppWorkflowsDts(manifest.workflows)],
32387
+ [path5.join(dotLotics, "app_queries.d.ts"), generateAppQueriesDts(manifest.queries)],
32388
+ [path5.join(dotLotics, "app_agents.d.ts"), generateAppAgentsDts(manifest.agents)]
32389
+ ];
32390
+ for (const [file, content] of written) fs4.writeFileSync(file, content);
32391
+ return written.map(([file]) => file);
32392
+ }
32393
+ function readCodegenTablesAllowlist(projectDir) {
32394
+ const pkg2 = JSON.parse(fs4.readFileSync(path5.join(projectDir, "package.json"), "utf-8"));
32395
+ const tables = pkg2.lotics?.codegen?.tables;
32396
+ return Array.isArray(tables) ? tables.filter((t) => typeof t === "string") : [];
32397
+ }
32398
+ function resolveCodegenTableIds(projectDir, queries) {
32399
+ const ids = /* @__PURE__ */ new Set();
32400
+ for (const decl of Object.values(queries)) {
32401
+ for (const id of collectQueryTableIds(decl.ast)) ids.add(id);
32402
+ }
32403
+ for (const id of readCodegenTablesAllowlist(projectDir)) ids.add(id);
32404
+ return [...ids];
32405
+ }
32406
+ function writeAppFields(projectDir, tables) {
32407
+ const dotLotics = path5.join(projectDir, ".lotics");
32408
+ fs4.mkdirSync(dotLotics, { recursive: true });
32409
+ const file = path5.join(dotLotics, "app_fields.ts");
32410
+ fs4.writeFileSync(file, generateAppFields(tables));
32411
+ return file;
32412
+ }
32413
+ async function appCodegen(args) {
32414
+ const projectDir = path5.resolve(args.projectDir ?? process.cwd());
32415
+ const meta = readAppMeta(projectDir);
32416
+ const dtsPaths = writeAppDts(projectDir, {
32417
+ workflows: meta.workflows,
32418
+ queries: meta.queries,
32419
+ agents: meta.agents
32420
+ });
32421
+ for (const p of dtsPaths) console.error(`Regenerated ${p}`);
32422
+ if (!args.client) {
32423
+ console.error(
32424
+ "Skipped .lotics/app_fields.ts \u2014 no workspace credentials resolved. Run authenticated (or set LOTICS_API_KEY) to regenerate field/option ids."
32425
+ );
32426
+ return;
32427
+ }
32428
+ const tableIds = resolveCodegenTableIds(projectDir, meta.queries ?? {});
32429
+ try {
32430
+ const tables = await args.client.getWorkspaceSchema(tableIds);
32431
+ const fieldsPath = writeAppFields(projectDir, tables);
32432
+ console.error(`Regenerated ${fieldsPath} (${tables.length} table${tables.length === 1 ? "" : "s"})`);
32433
+ } catch (err2) {
32434
+ console.error(
32435
+ `\u26A0 Could not fetch the workspace schema (${err2 instanceof Error ? err2.message : String(err2)}). Kept the existing .lotics/app_fields.ts.`
32436
+ );
32437
+ }
32438
+ await refreshWorkflowGlobals(args.client, projectDir, meta.app_id, Object.keys(meta.workflows ?? {}));
32439
+ if (Object.keys(meta.workflows ?? {}).length > 0) ensureWorkflowTsconfigExcludes(projectDir);
32440
+ }
32441
+ async function refreshWorkflowGlobals(client, projectDir, app_id, aliases) {
32442
+ for (const alias of aliases) {
32443
+ const file = workflowFilePath(projectDir, alias);
32444
+ if (!fs4.existsSync(file)) continue;
32445
+ const body = stripWorkflowHeader(fs4.readFileSync(file, "utf-8"));
32446
+ if (body.trim() === "") continue;
32447
+ const envelope = await fetchWorkflowGlobals(client, projectDir, app_id, alias);
32448
+ writeWorkflowFile(projectDir, alias, body, envelope);
32449
+ console.error(`Refreshed ${path5.relative(projectDir, file)} + its workflow types`);
32450
+ }
32451
+ }
31866
32452
  function stampPulledManifest(projectDir, args) {
31867
32453
  writeAppMeta(projectDir, {
31868
32454
  app_id: args.app_id,
@@ -31881,21 +32467,21 @@ async function downloadToFile(url, destPath) {
31881
32467
  throw new Error(`Failed to download ${url}: ${response.status}`);
31882
32468
  }
31883
32469
  const buffer = Buffer.from(await response.arrayBuffer());
31884
- fs3.writeFileSync(destPath, buffer);
32470
+ fs4.writeFileSync(destPath, buffer);
31885
32471
  }
31886
32472
  function appDirName(name) {
31887
32473
  const cleaned = name.replace(/[/\\]+/g, "-").replace(/[<>:"|?*]/g, "").replace(/\s+/g, " ").trim().replace(/^[.\s-]+|[.\s-]+$/g, "");
31888
32474
  return cleaned || "app";
31889
32475
  }
31890
32476
  async function appCreate(client, args) {
31891
- const targetPath = path4.resolve(args.targetPath ?? appDirName(args.name));
31892
- if (fs3.existsSync(targetPath)) {
31893
- const entries = fs3.readdirSync(targetPath);
32477
+ const targetPath = path5.resolve(args.targetPath ?? appDirName(args.name));
32478
+ if (fs4.existsSync(targetPath)) {
32479
+ const entries = fs4.readdirSync(targetPath);
31894
32480
  if (entries.length > 0) {
31895
32481
  throw new Error(`Target directory ${targetPath} is not empty.`);
31896
32482
  }
31897
32483
  }
31898
- fs3.mkdirSync(targetPath, { recursive: true });
32484
+ fs4.mkdirSync(targetPath, { recursive: true });
31899
32485
  const app = await client.createApp({ name: args.name });
31900
32486
  console.error(`Created app: ${app.name} (${app.id})`);
31901
32487
  const [uiLatest, sdkLatest] = await Promise.all([
@@ -31910,9 +32496,9 @@ async function appCreate(client, args) {
31910
32496
  sdk_version: sdkLatest ? `^${sdkLatest}` : void 0
31911
32497
  });
31912
32498
  for (const file of files) {
31913
- const fullPath = path4.join(targetPath, file.path);
31914
- fs3.mkdirSync(path4.dirname(fullPath), { recursive: true });
31915
- fs3.writeFileSync(fullPath, file.content);
32499
+ const fullPath = path5.join(targetPath, file.path);
32500
+ fs4.mkdirSync(path5.dirname(fullPath), { recursive: true });
32501
+ fs4.writeFileSync(fullPath, file.content);
31916
32502
  }
31917
32503
  console.error(`Scaffolded ${files.length} files into ${targetPath}`);
31918
32504
  writeAppDts(targetPath, {});
@@ -31925,7 +32511,7 @@ async function appCreate(client, args) {
31925
32511
  });
31926
32512
  console.error(`
31927
32513
  Ready. Next steps:`);
31928
- console.error(` cd ${path4.relative(process.cwd(), targetPath) || "."}`);
32514
+ console.error(` cd ${path5.relative(process.cwd(), targetPath) || "."}`);
31929
32515
  console.error(` # edit src/App.tsx, then:`);
31930
32516
  console.error(` lotics app deploy`);
31931
32517
  }
@@ -31940,6 +32526,21 @@ async function appRename(client, args) {
31940
32526
  if (res.error) throw new Error(res.error);
31941
32527
  console.error(`App renamed: "${args.name}" (${meta.app_id})`);
31942
32528
  }
32529
+ function defaultPullTarget(appId, appName) {
32530
+ const cwdPkgPath = path5.join(process.cwd(), "package.json");
32531
+ if (fs4.existsSync(cwdPkgPath)) {
32532
+ let pkg2 = null;
32533
+ try {
32534
+ pkg2 = JSON.parse(fs4.readFileSync(cwdPkgPath, "utf-8"));
32535
+ } catch (err2) {
32536
+ console.error(
32537
+ `\u26A0 Could not parse ${cwdPkgPath} (${err2 instanceof Error ? err2.message : String(err2)}) \u2014 pulling into a fresh ${appDirName(appName)}/ subdir.`
32538
+ );
32539
+ }
32540
+ if (pkg2?.lotics?.app_id === appId) return process.cwd();
32541
+ }
32542
+ return appDirName(appName);
32543
+ }
31943
32544
  async function appPull(client, args) {
31944
32545
  const app = await client.getApp(args.app_id);
31945
32546
  if (!app.current_version_id) {
@@ -31949,16 +32550,16 @@ async function appPull(client, args) {
31949
32550
  }
31950
32551
  const version = await client.getAppVersion(app.id, app.current_version_id);
31951
32552
  const sourceUrl = await client.getAppVersionSourceUrl(app.id, version.id);
31952
- const targetPath = path4.resolve(args.targetPath ?? appDirName(app.name));
31953
- fs3.mkdirSync(targetPath, { recursive: true });
31954
- const tmpFile = path4.join(tmpdir(), `lotics-app-${app.id}-${Date.now()}.tar.gz`);
32553
+ const targetPath = path5.resolve(args.targetPath ?? defaultPullTarget(app.id, app.name));
32554
+ fs4.mkdirSync(targetPath, { recursive: true });
32555
+ const tmpFile = path5.join(tmpdir(), `lotics-app-${app.id}-${Date.now()}.tar.gz`);
31955
32556
  console.error(`Downloading source archive...`);
31956
32557
  await downloadToFile(sourceUrl, tmpFile);
31957
32558
  try {
31958
32559
  console.error(`Extracting to ${targetPath}...`);
31959
32560
  await runTar(["-xzf", tmpFile, "-C", targetPath], targetPath);
31960
32561
  } finally {
31961
- if (fs3.existsSync(tmpFile)) fs3.unlinkSync(tmpFile);
32562
+ if (fs4.existsSync(tmpFile)) fs4.unlinkSync(tmpFile);
31962
32563
  }
31963
32564
  stampPulledManifest(targetPath, {
31964
32565
  app_id: app.id,
@@ -31969,11 +32570,21 @@ async function appPull(client, args) {
31969
32570
  queries: app.queries ?? {},
31970
32571
  agents: app.agents ?? {}
31971
32572
  });
32573
+ const aliases = Object.keys(app.workflows ?? {});
32574
+ if (aliases.length > 0) {
32575
+ const written = await writeWorkflowFiles(client, targetPath, app.id, aliases);
32576
+ if (written.length > 0) {
32577
+ console.error(
32578
+ `Wrote ${written.length} workflow ${written.length === 1 ? "body" : "bodies"} to ${WORKFLOWS_DIR}/ (${written.join(", ")})`
32579
+ );
32580
+ }
32581
+ ensureWorkflowTsconfigExcludes(targetPath);
32582
+ }
31972
32583
  console.error(`Installing npm dependencies...`);
31973
32584
  await runNpm(["install"], targetPath);
31974
32585
  console.error(`
31975
32586
  Ready. Next steps:`);
31976
- console.error(` cd ${path4.relative(process.cwd(), targetPath) || "."}`);
32587
+ console.error(` cd ${path5.relative(process.cwd(), targetPath) || "."}`);
31977
32588
  console.error(` # edit src/App.tsx`);
31978
32589
  console.error(` lotics app deploy`);
31979
32590
  }
@@ -31989,21 +32600,21 @@ function undeclaredCapabilities(sourceText, declared) {
31989
32600
  return used;
31990
32601
  }
31991
32602
  function readAppSourceText(projectDir) {
31992
- const srcDir = path4.join(projectDir, "src");
31993
- if (!fs3.existsSync(srcDir)) return "";
32603
+ const srcDir = path5.join(projectDir, "src");
32604
+ if (!fs4.existsSync(srcDir)) return "";
31994
32605
  const parts = [];
31995
- const walk = (dir) => {
31996
- for (const entry of fs3.readdirSync(dir, { withFileTypes: true })) {
31997
- const full = path4.join(dir, entry.name);
31998
- if (entry.isDirectory()) walk(full);
31999
- else if (/\.(ts|tsx|js|jsx)$/.test(entry.name)) parts.push(fs3.readFileSync(full, "utf8"));
32606
+ const walk2 = (dir) => {
32607
+ for (const entry of fs4.readdirSync(dir, { withFileTypes: true })) {
32608
+ const full = path5.join(dir, entry.name);
32609
+ if (entry.isDirectory()) walk2(full);
32610
+ else if (/\.(ts|tsx|js|jsx)$/.test(entry.name)) parts.push(fs4.readFileSync(full, "utf8"));
32000
32611
  }
32001
32612
  };
32002
- walk(srcDir);
32613
+ walk2(srcDir);
32003
32614
  return parts.join("\n");
32004
32615
  }
32005
32616
  async function appDeploy(client, args) {
32006
- const projectDir = path4.resolve(args.projectDir ?? process.cwd());
32617
+ const projectDir = path5.resolve(args.projectDir ?? process.cwd());
32007
32618
  const meta = readAppMeta(projectDir);
32008
32619
  const undeclared = undeclaredCapabilities(readAppSourceText(projectDir), meta.capabilities);
32009
32620
  if (undeclared.length > 0) {
@@ -32017,14 +32628,14 @@ async function appDeploy(client, args) {
32017
32628
  writeAppDts(projectDir, { workflows: meta.workflows, queries: meta.queries, agents: meta.agents });
32018
32629
  console.error("Building...");
32019
32630
  await runNpm(["run", "build"], projectDir);
32020
- const distDir = path4.join(projectDir, "dist");
32021
- if (!fs3.existsSync(distDir)) {
32631
+ const distDir = path5.join(projectDir, "dist");
32632
+ if (!fs4.existsSync(distDir)) {
32022
32633
  throw new Error(
32023
32634
  `Build did not produce a dist/ directory in ${projectDir}. Check that 'npm run build' is configured correctly (Vite/etc. defaults to dist/).`
32024
32635
  );
32025
32636
  }
32026
- const tmpSource = path4.join(tmpdir(), `lotics-source-${Date.now()}.tar.gz`);
32027
- const tmpDist = path4.join(tmpdir(), `lotics-dist-${Date.now()}.tar.gz`);
32637
+ const tmpSource = path5.join(tmpdir(), `lotics-source-${Date.now()}.tar.gz`);
32638
+ const tmpDist = path5.join(tmpdir(), `lotics-dist-${Date.now()}.tar.gz`);
32028
32639
  try {
32029
32640
  console.error("Packaging source...");
32030
32641
  await runTar(
@@ -32046,8 +32657,8 @@ async function appDeploy(client, args) {
32046
32657
  try {
32047
32658
  const result = await client.deployAppVersion({
32048
32659
  app_id: meta.app_id,
32049
- source_archive: fs3.readFileSync(tmpSource),
32050
- dist_archive: fs3.readFileSync(tmpDist),
32660
+ source_archive: fs4.readFileSync(tmpSource),
32661
+ dist_archive: fs4.readFileSync(tmpDist),
32051
32662
  prev_version_id: meta.current_version_id,
32052
32663
  message: args.message,
32053
32664
  // Sync apps.queries from the manifest. Server validates each query
@@ -32081,8 +32692,8 @@ async function appDeploy(client, args) {
32081
32692
  throw err2;
32082
32693
  }
32083
32694
  } finally {
32084
- if (fs3.existsSync(tmpSource)) fs3.unlinkSync(tmpSource);
32085
- if (fs3.existsSync(tmpDist)) fs3.unlinkSync(tmpDist);
32695
+ if (fs4.existsSync(tmpSource)) fs4.unlinkSync(tmpSource);
32696
+ if (fs4.existsSync(tmpDist)) fs4.unlinkSync(tmpDist);
32086
32697
  }
32087
32698
  }
32088
32699
  async function warnIfUnbranded(client, appId) {
@@ -32103,7 +32714,7 @@ async function warnIfUnbranded(client, appId) {
32103
32714
  }
32104
32715
  }
32105
32716
  async function appDev(client, args) {
32106
- const projectDir = path4.resolve(args.projectDir ?? process.cwd());
32717
+ const projectDir = path5.resolve(args.projectDir ?? process.cwd());
32107
32718
  const meta = readAppMeta(projectDir);
32108
32719
  writeAppDts(projectDir, { workflows: meta.workflows, queries: meta.queries, agents: meta.agents });
32109
32720
  const app = await client.getApp(meta.app_id);
@@ -32146,6 +32757,284 @@ async function appDev(client, args) {
32146
32757
  console.error("\nStopping\u2026");
32147
32758
  await handle.stop();
32148
32759
  }
32760
+ function parseCreatedRecords(value) {
32761
+ if (!Array.isArray(value)) return [];
32762
+ const groups = [];
32763
+ for (const entry of value) {
32764
+ if (!entry || typeof entry !== "object") continue;
32765
+ const tableId = entry.table_id;
32766
+ const recordIds = entry.record_ids;
32767
+ if (typeof tableId !== "string" || !Array.isArray(recordIds)) continue;
32768
+ const ids = recordIds.filter((id) => typeof id === "string");
32769
+ if (ids.length > 0) groups.push({ table_id: tableId, record_ids: ids });
32770
+ }
32771
+ return groups;
32772
+ }
32773
+ function parseIrreversibleToolNames(value) {
32774
+ if (!Array.isArray(value)) return [];
32775
+ const names = [];
32776
+ for (const entry of value) {
32777
+ if (!entry || typeof entry !== "object") continue;
32778
+ const name = entry.tool_name;
32779
+ if (typeof name === "string" && !names.includes(name)) names.push(name);
32780
+ }
32781
+ return names;
32782
+ }
32783
+ function printSideEffects(summary) {
32784
+ const created = parseCreatedRecords(summary.created_records);
32785
+ const irreversibleTools = parseIrreversibleToolNames(summary.irreversible_tool_calls);
32786
+ const subWorkflows = summary.sub_workflows_possible === true;
32787
+ console.error("\nCreated records:");
32788
+ if (created.length === 0) {
32789
+ console.error(" (none with ids to clean up)");
32790
+ } else {
32791
+ for (const group of created) {
32792
+ console.error(` ${group.table_id}: ${group.record_ids.length} record(s)`);
32793
+ const payload = JSON.stringify({ table_id: group.table_id, record_ids: group.record_ids });
32794
+ console.error(` lotics run delete_records '${payload}'`);
32795
+ }
32796
+ }
32797
+ const irreversiblePart = irreversibleTools.length > 0 ? `Could NOT auto-undo (clean up manually): ${irreversibleTools.join(", ")}.` : "Could NOT auto-undo: none.";
32798
+ const subPart = subWorkflows ? " Sub-workflows may have run (after_* table workflows) \u2014 their effects are NOT in this list." : "";
32799
+ console.error(`
32800
+ ${irreversiblePart}${subPart}`);
32801
+ }
32802
+ async function cleanupCreatedRecords(client, created) {
32803
+ if (created.length === 0) {
32804
+ console.error("\nNo created records to clean up.");
32805
+ return true;
32806
+ }
32807
+ console.error("\nCleaning up created records (delete_records \u2014 records only):");
32808
+ let allDeleted = true;
32809
+ for (const group of created) {
32810
+ const res = await client.execute("delete_records", {
32811
+ table_id: group.table_id,
32812
+ record_ids: group.record_ids
32813
+ });
32814
+ if (res.error) {
32815
+ console.error(` \u2717 ${group.table_id}: ${res.error}`);
32816
+ allDeleted = false;
32817
+ } else {
32818
+ console.error(` \u2713 ${group.table_id}: deleted ${group.record_ids.length} record(s)`);
32819
+ }
32820
+ }
32821
+ return allDeleted;
32822
+ }
32823
+ async function appExecuteWorkflow(client, args) {
32824
+ const meta = readAppMeta(process.cwd());
32825
+ const result = await client.appWorkflow(meta.app_id, args.alias, args.inputs);
32826
+ console.log(JSON.stringify(result, null, 2));
32827
+ const status = typeof result.status === "string" ? result.status : "unknown";
32828
+ const message = typeof result.message === "string" ? result.message : "";
32829
+ console.error(`Workflow "${args.alias}" \u2192 ${status}${message ? `: ${message}` : ""}`);
32830
+ let cleanupFailed = false;
32831
+ if ((args.printCreated || args.cleanup) && result.side_effects) {
32832
+ printSideEffects(result.side_effects);
32833
+ if (args.cleanup) {
32834
+ const allDeleted = await cleanupCreatedRecords(
32835
+ client,
32836
+ parseCreatedRecords(result.side_effects.created_records)
32837
+ );
32838
+ cleanupFailed = !allDeleted;
32839
+ }
32840
+ } else if (args.printCreated || args.cleanup) {
32841
+ console.error("\n(no side-effect summary returned by the server)");
32842
+ }
32843
+ if (status === "error" || cleanupFailed) process.exit(1);
32844
+ }
32845
+ async function appWorkflowSet(client, args) {
32846
+ const projectDir = process.cwd();
32847
+ const meta = readAppMeta(projectDir);
32848
+ const declaration = meta.workflows?.[args.alias];
32849
+ if (!declaration) {
32850
+ console.error(
32851
+ `No workflow "${args.alias}" in package.json#lotics.workflows. Bind it first (set_app_workflow), then 'lotics app pull' to write its body and manifest entry.`
32852
+ );
32853
+ process.exit(1);
32854
+ }
32855
+ const file = workflowFilePath(projectDir, args.alias);
32856
+ if (!fs4.existsSync(file)) {
32857
+ console.error(
32858
+ `No workflow body at ${path5.relative(projectDir, file)}. Run 'lotics app pull ${meta.app_id}' to write src/workflows/${args.alias}.ts, then edit it.`
32859
+ );
32860
+ process.exit(1);
32861
+ }
32862
+ const source = stripWorkflowHeader(fs4.readFileSync(file, "utf-8"));
32863
+ if (source.trim() === "") {
32864
+ console.error(`Workflow body ${path5.relative(projectDir, file)} is empty after stripping the header.`);
32865
+ process.exit(1);
32866
+ }
32867
+ const res = await client.setAppWorkflow(meta.app_id, args.alias, {
32868
+ source,
32869
+ inputs: declaration.inputs,
32870
+ outputs: declaration.outputs
32871
+ });
32872
+ if (res.error) {
32873
+ console.error(`Failed to set workflow "${args.alias}": ${res.error}`);
32874
+ process.exit(1);
32875
+ }
32876
+ const result = res.result ?? {};
32877
+ const workflowId = typeof result.workflow_id === "string" ? result.workflow_id : "(unknown)";
32878
+ console.error(`Set workflow "${args.alias}" \u2192 ${workflowId}`);
32879
+ if (result.outputs && typeof result.outputs === "object") {
32880
+ console.error(` result.data schema: ${JSON.stringify(result.outputs)}`);
32881
+ }
32882
+ }
32883
+ async function appWorkflowPull(client) {
32884
+ const projectDir = process.cwd();
32885
+ const meta = readAppMeta(projectDir);
32886
+ const app = await client.getApp(meta.app_id);
32887
+ const aliases = Object.keys(app.workflows ?? {});
32888
+ if (aliases.length === 0) {
32889
+ console.error(`App ${meta.app_id} has no bound workflows.`);
32890
+ return;
32891
+ }
32892
+ const written = await writeWorkflowFiles(client, projectDir, meta.app_id, aliases);
32893
+ console.error(
32894
+ `Wrote ${written.length} workflow ${written.length === 1 ? "body" : "bodies"} to ${WORKFLOWS_DIR}/` + (written.length > 0 ? ` (${written.join(", ")})` : "")
32895
+ );
32896
+ ensureWorkflowTsconfigExcludes(projectDir);
32897
+ }
32898
+ async function appWorkflowCheck(args) {
32899
+ const projectDir = process.cwd();
32900
+ const meta = readAppMeta(projectDir);
32901
+ const bound = Object.keys(meta.workflows ?? {});
32902
+ let aliases;
32903
+ if (args.alias) {
32904
+ if (!bound.includes(args.alias)) {
32905
+ console.error(
32906
+ `No workflow "${args.alias}" in package.json#lotics.workflows. Bound aliases: ${bound.length > 0 ? bound.join(", ") : "(none)"}.`
32907
+ );
32908
+ process.exit(1);
32909
+ }
32910
+ aliases = [args.alias];
32911
+ } else {
32912
+ aliases = bound;
32913
+ }
32914
+ if (aliases.length === 0) {
32915
+ console.error(`App ${meta.app_id} has no bound workflows to check.`);
32916
+ return;
32917
+ }
32918
+ const toCheck = [];
32919
+ for (const alias of aliases) {
32920
+ const bodyPath = workflowFilePath(projectDir, alias);
32921
+ const globalsPath = workflowGlobalsPath(projectDir, alias);
32922
+ if (!fs4.existsSync(bodyPath)) {
32923
+ console.error(
32924
+ `\u26A0 Skipped "${alias}" \u2014 no body at ${path5.relative(projectDir, bodyPath)}. Run 'lotics app workflow pull' to write it.`
32925
+ );
32926
+ continue;
32927
+ }
32928
+ if (!fs4.existsSync(globalsPath)) {
32929
+ console.error(
32930
+ `Cannot check "${alias}" \u2014 missing types at ${path5.relative(projectDir, globalsPath)}. Run 'lotics app workflow pull' (or 'lotics app codegen') to fetch them.`
32931
+ );
32932
+ process.exit(1);
32933
+ }
32934
+ toCheck.push({ alias, input: { bodyPath, globalsPath } });
32935
+ }
32936
+ if (toCheck.length === 0) {
32937
+ console.error("No workflow bodies to check (every bound alias was skipped).");
32938
+ return;
32939
+ }
32940
+ const tsApi = await loadProjectTypescript(projectDir);
32941
+ const results = checkWorkflowBodies(tsApi, toCheck);
32942
+ printWorkflowCheckResults(projectDir, results);
32943
+ const failed = results.filter((r) => r.issues.length > 0);
32944
+ if (failed.length > 0) process.exit(1);
32945
+ }
32946
+ function printWorkflowCheckResults(projectDir, results) {
32947
+ let totalErrors = 0;
32948
+ for (const r of results) {
32949
+ const rel = path5.relative(projectDir, r.bodyPath);
32950
+ if (r.issues.length === 0) {
32951
+ console.error(`\u2713 ${r.alias} (${rel}) \u2014 no type errors`);
32952
+ continue;
32953
+ }
32954
+ totalErrors += r.issues.length;
32955
+ console.error(
32956
+ `\u2717 ${r.alias} (${rel}) \u2014 ${r.issues.length} error${r.issues.length === 1 ? "" : "s"}:`
32957
+ );
32958
+ for (const issue of r.issues) {
32959
+ const [first2, ...rest] = issue.message.split("\n");
32960
+ console.error(` ${rel}:${issue.line}:${issue.col} - ${issue.code}: ${first2}`);
32961
+ for (const line of rest) console.error(` ${line}`);
32962
+ }
32963
+ }
32964
+ const passed = results.length - results.filter((r) => r.issues.length > 0).length;
32965
+ console.error(
32966
+ totalErrors === 0 ? results.length === 1 ? `
32967
+ The workflow body type-checks clean.` : `
32968
+ All ${results.length} workflow bodies type-check clean.` : `
32969
+ ${totalErrors} error${totalErrors === 1 ? "" : "s"} across ${results.length - passed} of ${results.length} ${results.length === 1 ? "body" : "bodies"}.`
32970
+ );
32971
+ }
32972
+ function findUiSrcDir(start) {
32973
+ let dir = path5.resolve(start);
32974
+ for (; ; ) {
32975
+ const candidate = path5.join(dir, "packages", "ui", "src");
32976
+ if (fs4.existsSync(candidate) && fs4.statSync(candidate).isDirectory()) return candidate;
32977
+ const parent = path5.dirname(dir);
32978
+ if (parent === dir) return null;
32979
+ dir = parent;
32980
+ }
32981
+ }
32982
+ var UI_ALIAS_FIND_SOURCE = String.raw`/^@lotics\/ui\/(.+)$/`;
32983
+ function appUiLink(args) {
32984
+ const projectDir = path5.resolve(args.projectDir ?? process.cwd());
32985
+ const viteConfigPath = path5.join(projectDir, "vite.config.ts");
32986
+ if (!fs4.existsSync(viteConfigPath)) {
32987
+ throw new Error(
32988
+ `No vite.config.ts at ${projectDir}. Run inside a 'lotics app' project directory.`
32989
+ );
32990
+ }
32991
+ const uiSrc = findUiSrcDir(projectDir);
32992
+ if (!uiSrc) {
32993
+ throw new Error(
32994
+ "Cannot find packages/ui/src by walking up from this directory \u2014 `lotics ui link` requires a monorepo checkout. External apps consume @lotics/ui from npm; bump the package version and widen the app's dependency range instead."
32995
+ );
32996
+ }
32997
+ const hasComponent = fs4.existsSync(path5.join(uiSrc, `${args.component}.tsx`)) || fs4.existsSync(path5.join(uiSrc, `${args.component}.ts`)) || fs4.existsSync(path5.join(uiSrc, args.component));
32998
+ if (!hasComponent) {
32999
+ throw new Error(
33000
+ `No '@lotics/ui/${args.component}' under ${uiSrc} (expected ${args.component}.tsx/.ts). Check the component name.`
33001
+ );
33002
+ }
33003
+ const source = fs4.readFileSync(viteConfigPath, "utf-8");
33004
+ const aliasEntry = `{ find: ${UI_ALIAS_FIND_SOURCE}, replacement: ${JSON.stringify(`${uiSrc}/$1`)} },`;
33005
+ const alreadyLinked = source.includes(UI_ALIAS_FIND_SOURCE);
33006
+ if (args.remove) {
33007
+ if (!alreadyLinked) {
33008
+ console.error("No @lotics/ui dev-link alias present \u2014 nothing to remove.");
33009
+ return;
33010
+ }
33011
+ const stripped = source.replace(new RegExp(`^\\s*\\{ find: ${escapeRegExp(UI_ALIAS_FIND_SOURCE)}.*$\\n?`, "m"), "");
33012
+ fs4.writeFileSync(viteConfigPath, stripped);
33013
+ console.error(`Removed the @lotics/ui dev-link alias from ${viteConfigPath}.`);
33014
+ console.error("Restart `lotics app dev` and rm -rf node_modules/.vite to clear cached modules.");
33015
+ return;
33016
+ }
33017
+ if (alreadyLinked) {
33018
+ console.error(`@lotics/ui is already dev-linked in ${viteConfigPath}.`);
33019
+ return;
33020
+ }
33021
+ const aliasMatch = /resolve\s*:\s*\{[\s\S]*?alias\s*:\s*\[/.exec(source);
33022
+ if (!aliasMatch) {
33023
+ throw new Error(
33024
+ `Could not find a resolve.alias array literal in ${viteConfigPath}. Refresh vite.config.ts from the starter (packages/sdk/src/starter_template.ts) and retry.`
33025
+ );
33026
+ }
33027
+ const insertAt = aliasMatch.index + aliasMatch[0].length;
33028
+ const updated = `${source.slice(0, insertAt)}
33029
+ ${aliasEntry}${source.slice(insertAt)}`;
33030
+ fs4.writeFileSync(viteConfigPath, updated);
33031
+ console.error(`Dev-linked @lotics/ui \u2192 ${uiSrc} in ${viteConfigPath}.`);
33032
+ console.error("Restart `lotics app dev` and rm -rf node_modules/.vite to clear cached modules.");
33033
+ console.error("Finalize: PR the packages/ui change \u2192 publish \u2192 `lotics ui link <component> --remove` + bump the app's dep.");
33034
+ }
33035
+ function escapeRegExp(s) {
33036
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
33037
+ }
32149
33038
 
32150
33039
  // src/args.ts
32151
33040
  function parseArgs(argv) {
@@ -32162,6 +33051,8 @@ function parseArgs(argv) {
32162
33051
  message: void 0,
32163
33052
  local: false,
32164
33053
  all: false,
33054
+ printCreated: false,
33055
+ cleanup: false,
32165
33056
  version: false,
32166
33057
  help: false
32167
33058
  };
@@ -32212,6 +33103,13 @@ function parseArgs(argv) {
32212
33103
  case "--all":
32213
33104
  flags.all = true;
32214
33105
  break;
33106
+ case "--print-created":
33107
+ case "--report-effects":
33108
+ flags.printCreated = true;
33109
+ break;
33110
+ case "--cleanup":
33111
+ flags.cleanup = true;
33112
+ break;
32215
33113
  case "--version":
32216
33114
  case "-v":
32217
33115
  flags.version = true;
@@ -32237,12 +33135,40 @@ function parseArgs(argv) {
32237
33135
  return { command, subcommand, toolArgs, restArgs, flags };
32238
33136
  }
32239
33137
 
33138
+ // src/inputs.ts
33139
+ async function ingestJsonArgs(opts) {
33140
+ let raw = opts.rawArg;
33141
+ if (raw && raw.startsWith("@")) {
33142
+ const argsPath = raw.slice(1);
33143
+ try {
33144
+ raw = opts.readFile(argsPath);
33145
+ } catch (err2) {
33146
+ return {
33147
+ kind: "error",
33148
+ message: `Cannot read args file "${argsPath}": ${err2 instanceof Error ? err2.message : String(err2)}`
33149
+ };
33150
+ }
33151
+ } else if (!raw && !opts.stdinIsTTY) {
33152
+ raw = await opts.readStdin();
33153
+ }
33154
+ if (!raw) return { kind: "ok", args: {} };
33155
+ try {
33156
+ const parsed = JSON.parse(raw);
33157
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
33158
+ return { kind: "error", message: `JSON args must be an object, got: ${raw}` };
33159
+ }
33160
+ return { kind: "ok", args: parsed };
33161
+ } catch {
33162
+ return { kind: "error", message: `Invalid JSON: ${raw}` };
33163
+ }
33164
+ }
33165
+
32240
33166
  // src/xlsx.ts
32241
- import fs5 from "node:fs";
33167
+ import fs6 from "node:fs";
32242
33168
 
32243
33169
  // src/file_command_io.ts
32244
- import fs4 from "node:fs";
32245
- import path5 from "node:path";
33170
+ import fs5 from "node:fs";
33171
+ import path6 from "node:path";
32246
33172
  var CliError = class extends Error {
32247
33173
  constructor(message) {
32248
33174
  super(message);
@@ -32253,14 +33179,14 @@ function fail(message) {
32253
33179
  throw new CliError(message);
32254
33180
  }
32255
33181
  function writeFileAtomic(filePath, bytes) {
32256
- const dir = path5.dirname(path5.resolve(filePath));
32257
- const tmp = path5.join(dir, `.${path5.basename(filePath)}.${process.pid}.${Date.now()}.tmp`);
32258
- fs4.writeFileSync(tmp, bytes);
33182
+ const dir = path6.dirname(path6.resolve(filePath));
33183
+ const tmp = path6.join(dir, `.${path6.basename(filePath)}.${process.pid}.${Date.now()}.tmp`);
33184
+ fs5.writeFileSync(tmp, bytes);
32259
33185
  try {
32260
- fs4.renameSync(tmp, filePath);
33186
+ fs5.renameSync(tmp, filePath);
32261
33187
  } catch (error) {
32262
33188
  try {
32263
- fs4.unlinkSync(tmp);
33189
+ fs5.unlinkSync(tmp);
32264
33190
  } catch {
32265
33191
  }
32266
33192
  throw error;
@@ -35043,16 +35969,16 @@ var MatcherView = class {
35043
35969
  * @returns {string|undefined}
35044
35970
  */
35045
35971
  getCurrentTag() {
35046
- const path7 = this._matcher.path;
35047
- return path7.length > 0 ? path7[path7.length - 1].tag : void 0;
35972
+ const path8 = this._matcher.path;
35973
+ return path8.length > 0 ? path8[path8.length - 1].tag : void 0;
35048
35974
  }
35049
35975
  /**
35050
35976
  * Get current namespace.
35051
35977
  * @returns {string|undefined}
35052
35978
  */
35053
35979
  getCurrentNamespace() {
35054
- const path7 = this._matcher.path;
35055
- return path7.length > 0 ? path7[path7.length - 1].namespace : void 0;
35980
+ const path8 = this._matcher.path;
35981
+ return path8.length > 0 ? path8[path8.length - 1].namespace : void 0;
35056
35982
  }
35057
35983
  /**
35058
35984
  * Get current node's attribute value.
@@ -35060,9 +35986,9 @@ var MatcherView = class {
35060
35986
  * @returns {*}
35061
35987
  */
35062
35988
  getAttrValue(attrName) {
35063
- const path7 = this._matcher.path;
35064
- if (path7.length === 0) return void 0;
35065
- return path7[path7.length - 1].values?.[attrName];
35989
+ const path8 = this._matcher.path;
35990
+ if (path8.length === 0) return void 0;
35991
+ return path8[path8.length - 1].values?.[attrName];
35066
35992
  }
35067
35993
  /**
35068
35994
  * Check if current node has an attribute.
@@ -35070,9 +35996,9 @@ var MatcherView = class {
35070
35996
  * @returns {boolean}
35071
35997
  */
35072
35998
  hasAttr(attrName) {
35073
- const path7 = this._matcher.path;
35074
- if (path7.length === 0) return false;
35075
- const current = path7[path7.length - 1];
35999
+ const path8 = this._matcher.path;
36000
+ if (path8.length === 0) return false;
36001
+ const current = path8[path8.length - 1];
35076
36002
  return current.values !== void 0 && attrName in current.values;
35077
36003
  }
35078
36004
  /**
@@ -35080,18 +36006,18 @@ var MatcherView = class {
35080
36006
  * @returns {number}
35081
36007
  */
35082
36008
  getPosition() {
35083
- const path7 = this._matcher.path;
35084
- if (path7.length === 0) return -1;
35085
- return path7[path7.length - 1].position ?? 0;
36009
+ const path8 = this._matcher.path;
36010
+ if (path8.length === 0) return -1;
36011
+ return path8[path8.length - 1].position ?? 0;
35086
36012
  }
35087
36013
  /**
35088
36014
  * Get current node's repeat counter (occurrence count of this tag name).
35089
36015
  * @returns {number}
35090
36016
  */
35091
36017
  getCounter() {
35092
- const path7 = this._matcher.path;
35093
- if (path7.length === 0) return -1;
35094
- return path7[path7.length - 1].counter ?? 0;
36018
+ const path8 = this._matcher.path;
36019
+ if (path8.length === 0) return -1;
36020
+ return path8[path8.length - 1].counter ?? 0;
35095
36021
  }
35096
36022
  /**
35097
36023
  * Get current node's sibling index (alias for getPosition).
@@ -36946,8 +37872,8 @@ function isAttribute(name) {
36946
37872
  }
36947
37873
 
36948
37874
  // ../../node_modules/fflate/esm/index.mjs
36949
- import { createRequire } from "module";
36950
- var require2 = createRequire("/");
37875
+ import { createRequire as createRequire2 } from "module";
37876
+ var require2 = createRequire2("/");
36951
37877
  var Worker;
36952
37878
  try {
36953
37879
  Worker = require2("worker_threads").Worker;
@@ -40547,10 +41473,10 @@ function resolveRelativePath(basePath, relativePath) {
40547
41473
  }
40548
41474
  return resolved.join("/");
40549
41475
  }
40550
- function findZipEntry(zipEntries, path7) {
40551
- if (zipEntries[path7]) return zipEntries[path7];
40552
- if (zipEntries[`xl/${path7}`]) return zipEntries[`xl/${path7}`];
40553
- const noSlash = path7.replace(/^\//, "");
41476
+ function findZipEntry(zipEntries, path8) {
41477
+ if (zipEntries[path8]) return zipEntries[path8];
41478
+ if (zipEntries[`xl/${path8}`]) return zipEntries[`xl/${path8}`];
41479
+ const noSlash = path8.replace(/^\//, "");
40554
41480
  if (zipEntries[noSlash]) return zipEntries[noSlash];
40555
41481
  return void 0;
40556
41482
  }
@@ -41848,9 +42774,9 @@ function parseExcelFromZip(zip, options) {
41848
42774
  }
41849
42775
  return result;
41850
42776
  }
41851
- function findEntry(zip, path7) {
41852
- if (zip[path7]) return zip[path7];
41853
- const lower = path7.toLowerCase();
42777
+ function findEntry(zip, path8) {
42778
+ if (zip[path8]) return zip[path8];
42779
+ const lower = path8.toLowerCase();
41854
42780
  for (const key of Object.keys(zip)) {
41855
42781
  if (key.toLowerCase() === lower) return zip[key];
41856
42782
  }
@@ -42461,12 +43387,12 @@ function extractPivotRoundTripInfo(originalZip) {
42461
43387
  if (paths.length > 0) pivotTablesBySheetIndex.set(i2, paths);
42462
43388
  }
42463
43389
  const pivotXmlPaths = [];
42464
- for (const path7 of Object.keys(originalZip)) {
42465
- if (path7.startsWith("xl/pivotTables/") && path7.endsWith(".xml")) {
42466
- pivotXmlPaths.push(path7);
43390
+ for (const path8 of Object.keys(originalZip)) {
43391
+ if (path8.startsWith("xl/pivotTables/") && path8.endsWith(".xml")) {
43392
+ pivotXmlPaths.push(path8);
42467
43393
  }
42468
- if (path7.startsWith("xl/pivotCache/") && path7.endsWith(".xml")) {
42469
- pivotXmlPaths.push(path7);
43394
+ if (path8.startsWith("xl/pivotCache/") && path8.endsWith(".xml")) {
43395
+ pivotXmlPaths.push(path8);
42470
43396
  }
42471
43397
  }
42472
43398
  return { cachesByWorkbook, pivotTablesBySheetIndex, pivotXmlPaths };
@@ -42474,15 +43400,15 @@ function extractPivotRoundTripInfo(originalZip) {
42474
43400
  function decode(bytes) {
42475
43401
  return new TextDecoder().decode(bytes);
42476
43402
  }
42477
- function pivotContentTypeFor(path7) {
42478
- if (path7.startsWith("xl/pivotTables/") && path7.endsWith(".xml")) {
42479
- return `<Override PartName="/${path7}" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.pivotTable+xml"/>`;
43403
+ function pivotContentTypeFor(path8) {
43404
+ if (path8.startsWith("xl/pivotTables/") && path8.endsWith(".xml")) {
43405
+ return `<Override PartName="/${path8}" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.pivotTable+xml"/>`;
42480
43406
  }
42481
- if (path7.includes("/pivotCacheDefinition") && path7.endsWith(".xml")) {
42482
- return `<Override PartName="/${path7}" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.pivotCacheDefinition+xml"/>`;
43407
+ if (path8.includes("/pivotCacheDefinition") && path8.endsWith(".xml")) {
43408
+ return `<Override PartName="/${path8}" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.pivotCacheDefinition+xml"/>`;
42483
43409
  }
42484
- if (path7.includes("/pivotCacheRecords") && path7.endsWith(".xml")) {
42485
- return `<Override PartName="/${path7}" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.pivotCacheRecords+xml"/>`;
43410
+ if (path8.includes("/pivotCacheRecords") && path8.endsWith(".xml")) {
43411
+ return `<Override PartName="/${path8}" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.pivotCacheRecords+xml"/>`;
42486
43412
  }
42487
43413
  return void 0;
42488
43414
  }
@@ -42554,13 +43480,13 @@ function exportWorkbook(workbook, originalZip) {
42554
43480
  regeneratedPaths.add(`xl/worksheets/sheet${i2 + 1}.xml`);
42555
43481
  regeneratedPaths.add(`xl/worksheets/_rels/sheet${i2 + 1}.xml.rels`);
42556
43482
  }
42557
- for (const path7 of Object.keys(originalZip)) {
42558
- if (path7.startsWith("xl/drawings/") || path7.startsWith("xl/charts/") || path7.startsWith("xl/tables/") || path7.startsWith("xl/media/")) {
42559
- regeneratedPaths.add(path7);
43483
+ for (const path8 of Object.keys(originalZip)) {
43484
+ if (path8.startsWith("xl/drawings/") || path8.startsWith("xl/charts/") || path8.startsWith("xl/tables/") || path8.startsWith("xl/media/")) {
43485
+ regeneratedPaths.add(path8);
42560
43486
  }
42561
43487
  }
42562
- for (const [path7, data] of Object.entries(originalZip)) {
42563
- if (!regeneratedPaths.has(path7)) entries[path7] = data;
43488
+ for (const [path8, data] of Object.entries(originalZip)) {
43489
+ if (!regeneratedPaths.has(path8)) entries[path8] = data;
42564
43490
  }
42565
43491
  }
42566
43492
  const sharedStrings = buildSharedStrings(workbook);
@@ -42575,8 +43501,8 @@ function exportWorkbook(workbook, originalZip) {
42575
43501
  buildWorkbookRels(workbook, pivotInfo)
42576
43502
  );
42577
43503
  const extraContentTypes = [];
42578
- for (const path7 of pivotInfo.pivotXmlPaths) {
42579
- const ct = pivotContentTypeFor(path7);
43504
+ for (const path8 of pivotInfo.pivotXmlPaths) {
43505
+ const ct = pivotContentTypeFor(path8);
42580
43506
  if (ct) extraContentTypes.push(ct);
42581
43507
  }
42582
43508
  let globalChartIndex = 1;
@@ -44337,14 +45263,14 @@ var DependencyGraph = class {
44337
45263
  stack.push({ cell: p, path: [cell, p] });
44338
45264
  }
44339
45265
  while (stack.length > 0) {
44340
- const { cell: current, path: path7 } = stack.pop();
44341
- if (current === cell) return path7;
45266
+ const { cell: current, path: path8 } = stack.pop();
45267
+ if (current === cell) return path8;
44342
45268
  if (visited.has(current)) continue;
44343
45269
  visited.add(current);
44344
45270
  const nextPrec = this.precedents.get(current);
44345
45271
  if (!nextPrec) continue;
44346
45272
  for (const p of nextPrec) {
44347
- stack.push({ cell: p, path: [...path7, p] });
45273
+ stack.push({ cell: p, path: [...path8, p] });
44348
45274
  }
44349
45275
  }
44350
45276
  return null;
@@ -45335,8 +46261,8 @@ function adjustMergedCellsForColDelete(sheet, atCol, count) {
45335
46261
 
45336
46262
  // src/xlsx.ts
45337
46263
  function loadFile(filePath) {
45338
- if (!fs5.existsSync(filePath)) fail(`File not found: ${filePath}`);
45339
- const buffer = fs5.readFileSync(filePath);
46264
+ if (!fs6.existsSync(filePath)) fail(`File not found: ${filePath}`);
46265
+ const buffer = fs6.readFileSync(filePath);
45340
46266
  const arrayBuffer = buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength);
45341
46267
  const originalZip = unzipXlsx(arrayBuffer);
45342
46268
  const parsed = parseExcelBuffer(arrayBuffer);
@@ -45386,7 +46312,7 @@ function lazyEngine(workbook) {
45386
46312
  };
45387
46313
  }
45388
46314
  function readToJson(filePath) {
45389
- const buffer = fs5.readFileSync(filePath);
46315
+ const buffer = fs6.readFileSync(filePath);
45390
46316
  const arrayBuffer = buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength);
45391
46317
  const parsed = parseExcelBuffer(arrayBuffer);
45392
46318
  return {
@@ -45776,7 +46702,7 @@ async function runXlsxCommand(subcommand, toolArgs, restArgs) {
45776
46702
  }
45777
46703
 
45778
46704
  // src/docx.ts
45779
- import fs6 from "node:fs";
46705
+ import fs7 from "node:fs";
45780
46706
 
45781
46707
  // ../docx/src/parse/parser.ts
45782
46708
  var import_jszip = __toESM(require_lib3(), 1);
@@ -47547,11 +48473,11 @@ async function parseDocx(buffer) {
47547
48473
  const zip = await import_jszip.default.loadAsync(buffer);
47548
48474
  const parts = /* @__PURE__ */ new Map();
47549
48475
  const filePromises = [];
47550
- zip.forEach((path7, file) => {
48476
+ zip.forEach((path8, file) => {
47551
48477
  if (file.dir) return;
47552
48478
  filePromises.push(
47553
48479
  file.async("uint8array").then((bytes) => {
47554
- parts.set(path7, bytes);
48480
+ parts.set(path8, bytes);
47555
48481
  })
47556
48482
  );
47557
48483
  });
@@ -47780,9 +48706,9 @@ var DEFAULT_PARTS = [
47780
48706
  ["word/theme/theme1.xml", () => encoder.encode(THEME_XML)]
47781
48707
  ];
47782
48708
  function fillDefaultParts(parts) {
47783
- for (const [path7, makeBytes] of DEFAULT_PARTS) {
47784
- if (!parts.has(path7)) {
47785
- parts.set(path7, makeBytes());
48709
+ for (const [path8, makeBytes] of DEFAULT_PARTS) {
48710
+ if (!parts.has(path8)) {
48711
+ parts.set(path8, makeBytes());
47786
48712
  }
47787
48713
  }
47788
48714
  return parts;
@@ -47792,9 +48718,9 @@ function fillDefaultParts(parts) {
47792
48718
  async function serializeDocx(doc) {
47793
48719
  const zip = new import_jszip2.default();
47794
48720
  const parts = fillDefaultParts(new Map(doc.parts));
47795
- for (const [path7, bytes] of parts) {
47796
- if (path7 === "word/document.xml") continue;
47797
- zip.file(path7, bytes);
48721
+ for (const [path8, bytes] of parts) {
48722
+ if (path8 === "word/document.xml") continue;
48723
+ zip.file(path8, bytes);
47798
48724
  }
47799
48725
  const documentXml = serializeDocumentXml(doc);
47800
48726
  zip.file("word/document.xml", documentXml);
@@ -47880,8 +48806,8 @@ var DEFAULT_DOC_ATTRS = {
47880
48806
  "@_xmlns:w": "http://schemas.openxmlformats.org/wordprocessingml/2006/main"
47881
48807
  };
47882
48808
  async function loadFile2(filePath) {
47883
- if (!fs6.existsSync(filePath)) fail(`File not found: ${filePath}`);
47884
- const bytes = fs6.readFileSync(filePath);
48809
+ if (!fs7.existsSync(filePath)) fail(`File not found: ${filePath}`);
48810
+ const bytes = fs7.readFileSync(filePath);
47885
48811
  return parseDocx(new Uint8Array(bytes));
47886
48812
  }
47887
48813
  async function writeDoc(filePath, doc) {
@@ -48170,6 +49096,8 @@ async function runDocxCommand(subcommand, toolArgs, restArgs) {
48170
49096
  }
48171
49097
 
48172
49098
  // src/cli.ts
49099
+ dns.setDefaultResultOrder("ipv4first");
49100
+ net2.setDefaultAutoSelectFamilyAttemptTimeout(2e3);
48173
49101
  function printHelp() {
48174
49102
  console.log(`Lotics CLI v${VERSION} \u2014 AI agent interface for Lotics
48175
49103
 
@@ -48226,9 +49154,23 @@ COMMANDS
48226
49154
  lotics app deploy [-m <message>] Build + upload current dir as a new version
48227
49155
  (code + queries only \u2014 workflow bindings are
48228
49156
  managed by set_app_workflow / remove_app_workflow)
49157
+ lotics app codegen [path] Regenerate .lotics/* (types + field/option ids)
49158
+ from the manifest + workspace schema \u2014 no deploy
49159
+ lotics app workflow run <alias> '<json>' Execute a bound app workflow end-to-end
49160
+ (inputs: inline JSON, @file, or stdin;
49161
+ --print-created reports created records +
49162
+ a paste-ready cleanup plan; --cleanup also
49163
+ deletes those records \u2014 NOT a rollback)
49164
+ lotics app workflow set <alias> Push the edited src/workflows/<alias>.ts body
49165
+ through set_app_workflow (server verifies)
49166
+ lotics app workflow pull Rewrite src/workflows/*.ts from the server
49167
+ lotics app workflow check [alias] Typecheck src/workflows bodies locally (one
49168
+ isolated program per alias; the app's own tsc)
48229
49169
  lotics app subdomain <new-subdomain> Rename the app's public <slug>.lotics.app address
48230
49170
  lotics app rename "<new name>" Rename the app's display name (launcher title)
48231
49171
  lotics app dev [path] Run the app locally with HMR (RPC forwarded to prod)
49172
+ lotics ui link <component> [--remove] Dev-link @lotics/ui to the monorepo's
49173
+ packages/ui/src for live HMR (monorepo only)
48232
49174
  lotics xlsx <subcommand> ... Read/write/edit .xlsx files on your local filesystem
48233
49175
  (uses the bundled Lotics xlsx engine; prefer over
48234
49176
  npm xlsx/exceljs for round-trip fidelity)
@@ -48331,8 +49273,8 @@ function prompt(question) {
48331
49273
  });
48332
49274
  });
48333
49275
  }
48334
- async function publicPost(path7, body) {
48335
- const response = await fetch(`${API_BASE_URL}${path7}`, {
49276
+ async function publicPost(path8, body) {
49277
+ const response = await fetch(`${API_BASE_URL}${path8}`, {
48336
49278
  method: "POST",
48337
49279
  headers: { "Content-Type": "application/json" },
48338
49280
  body: JSON.stringify(body)
@@ -48478,13 +49420,13 @@ async function resolveWorkspace(client, ctx) {
48478
49420
  function resolveUploadPaths(rawPaths) {
48479
49421
  const result = [];
48480
49422
  for (const p of rawPaths) {
48481
- const resolved = path6.resolve(p);
48482
- const stat = fs7.statSync(resolved);
49423
+ const resolved = path7.resolve(p);
49424
+ const stat = fs8.statSync(resolved);
48483
49425
  if (stat.isDirectory()) {
48484
- const entries = fs7.readdirSync(resolved, { withFileTypes: true });
49426
+ const entries = fs8.readdirSync(resolved, { withFileTypes: true });
48485
49427
  for (const entry of entries) {
48486
49428
  if (entry.isFile()) {
48487
- result.push(path6.join(resolved, entry.name));
49429
+ result.push(path7.join(resolved, entry.name));
48488
49430
  }
48489
49431
  }
48490
49432
  } else {
@@ -48588,6 +49530,38 @@ async function main() {
48588
49530
  await runDocxCommand(subcommand, toolArgs, restArgs);
48589
49531
  return;
48590
49532
  }
49533
+ if (command === "ui") {
49534
+ if (subcommand === "link") {
49535
+ const component = toolArgs;
49536
+ if (!component) {
49537
+ console.error("Usage: lotics ui link <component> [--remove]");
49538
+ console.error("Dev-links @lotics/ui to the monorepo's packages/ui/src for live HMR.");
49539
+ process.exit(1);
49540
+ }
49541
+ appUiLink({ component, remove: restArgs.includes("--remove") });
49542
+ return;
49543
+ }
49544
+ console.error(`Unknown ui subcommand: ${subcommand ?? "(none)"}`);
49545
+ console.error("Usage: lotics ui link <component> [--remove]");
49546
+ process.exit(1);
49547
+ }
49548
+ if (command === "app" && subcommand === "workflow" && toolArgs === "check") {
49549
+ await appWorkflowCheck({ alias: restArgs[0] });
49550
+ return;
49551
+ }
49552
+ if (command === "app" && subcommand === "codegen") {
49553
+ const projectDir = toolArgs;
49554
+ const ctx2 = resolveContext(flags);
49555
+ if (!ctx2) {
49556
+ await appCodegen({ projectDir });
49557
+ return;
49558
+ }
49559
+ const viewAsMemberId = flags.viewAs ?? process.env.LOTICS_VIEW_AS;
49560
+ const client2 = new LoticsClient({ apiKey: ctx2.apiKey, workspaceId: ctx2.workspaceId, viewAsMemberId });
49561
+ await resolveWorkspace(client2, ctx2);
49562
+ await appCodegen({ projectDir, client: client2 });
49563
+ return;
49564
+ }
48591
49565
  if (command === "org") {
48592
49566
  const global2 = loadGlobalConfig() ?? {};
48593
49567
  const profiles = global2.profiles ?? {};
@@ -48655,6 +49629,11 @@ async function main() {
48655
49629
  console.error(" lotics app create <name> [path] Scaffold a new app locally");
48656
49630
  console.error(" lotics app pull <app_id> [path] Pull an existing app for local editing");
48657
49631
  console.error(" lotics app deploy [-m <message>] Build + upload the current directory");
49632
+ console.error(" lotics app codegen [path] Regenerate .lotics/* (types + field ids) \u2014 no deploy");
49633
+ console.error(" lotics app workflow run <alias> '<json>' Execute a bound app workflow end-to-end");
49634
+ console.error(" lotics app workflow set <alias> Push the edited src/workflows/<alias>.ts body");
49635
+ console.error(" lotics app workflow pull Rewrite src/workflows/*.ts from the server");
49636
+ console.error(" lotics app workflow check [alias] Typecheck src/workflows bodies locally");
48658
49637
  console.error(" lotics app subdomain <new-subdomain> Rename the app's public <slug>.lotics.app address");
48659
49638
  console.error(` lotics app rename "<new name>" Rename the app's display name (launcher title)`);
48660
49639
  console.error(" lotics app dev [path] Run the app locally with HMR + RPC forwarding");
@@ -48796,6 +49775,60 @@ Available workspaces:`);
48796
49775
  await appRename(client, { name: newName });
48797
49776
  return;
48798
49777
  }
49778
+ if (subcommand === "workflow") {
49779
+ const action = toolArgs;
49780
+ const workflowUsage = () => {
49781
+ console.error("Usage:");
49782
+ console.error(" lotics app workflow run <alias> '<json>' Execute a bound app workflow");
49783
+ console.error(" lotics app workflow set <alias> Push src/workflows/<alias>.ts");
49784
+ console.error(" lotics app workflow pull Rewrite src/workflows/*.ts from the server");
49785
+ console.error(" lotics app workflow check [alias] Typecheck src/workflows bodies locally");
49786
+ process.exit(1);
49787
+ };
49788
+ if (action === "run") {
49789
+ const alias = restArgs[0];
49790
+ if (!alias) {
49791
+ console.error("Usage: lotics app workflow run <alias> '<json>'");
49792
+ console.error(" lotics app workflow run <alias> @inputs.json (read inputs from a file)");
49793
+ console.error(" cat inputs.json | lotics app workflow run <alias> (read inputs from stdin)");
49794
+ console.error("Flags: --print-created (report created records + cleanup plan + caveat)");
49795
+ console.error(" --cleanup (also delete the created records \u2014 records only, NOT a rollback)");
49796
+ process.exit(1);
49797
+ }
49798
+ const ingested = await ingestJsonArgs({
49799
+ rawArg: restArgs[1],
49800
+ stdinIsTTY: process.stdin.isTTY ?? false,
49801
+ readFile: (p) => fs8.readFileSync(p, "utf-8"),
49802
+ readStdin
49803
+ });
49804
+ if (ingested.kind === "error") {
49805
+ console.error(ingested.message);
49806
+ process.exit(1);
49807
+ }
49808
+ await appExecuteWorkflow(client, {
49809
+ alias,
49810
+ inputs: ingested.args,
49811
+ printCreated: flags.printCreated,
49812
+ cleanup: flags.cleanup
49813
+ });
49814
+ return;
49815
+ }
49816
+ if (action === "set") {
49817
+ const alias = restArgs[0];
49818
+ if (!alias) {
49819
+ console.error("Usage: lotics app workflow set <alias>");
49820
+ console.error("Pushes the edited src/workflows/<alias>.ts body via set_app_workflow.");
49821
+ process.exit(1);
49822
+ }
49823
+ await appWorkflowSet(client, { alias });
49824
+ return;
49825
+ }
49826
+ if (action === "pull") {
49827
+ await appWorkflowPull(client);
49828
+ return;
49829
+ }
49830
+ workflowUsage();
49831
+ }
48799
49832
  if (subcommand === "dev") {
48800
49833
  const projectDir = toolArgs;
48801
49834
  let port;
@@ -48894,29 +49927,17 @@ ${JSON.stringify(info.input_schema, null, 2)}`);
48894
49927
  }
48895
49928
  if (command === "run") {
48896
49929
  const toolName = subcommand;
48897
- let rawArgs = toolArgs;
48898
- if (rawArgs && rawArgs.startsWith("@")) {
48899
- const argsPath = rawArgs.slice(1);
48900
- try {
48901
- rawArgs = fs7.readFileSync(argsPath, "utf-8");
48902
- } catch (err2) {
48903
- console.error(
48904
- `Cannot read args file "${argsPath}": ${err2 instanceof Error ? err2.message : String(err2)}`
48905
- );
48906
- process.exit(1);
48907
- }
48908
- } else if (!rawArgs && !process.stdin.isTTY) {
48909
- rawArgs = await readStdin();
48910
- }
48911
- let args = {};
48912
- if (rawArgs) {
48913
- try {
48914
- args = JSON.parse(rawArgs);
48915
- } catch {
48916
- console.error(`Invalid JSON: ${rawArgs}`);
48917
- process.exit(1);
48918
- }
49930
+ const ingested = await ingestJsonArgs({
49931
+ rawArg: toolArgs,
49932
+ stdinIsTTY: process.stdin.isTTY ?? false,
49933
+ readFile: (p) => fs8.readFileSync(p, "utf-8"),
49934
+ readStdin
49935
+ });
49936
+ if (ingested.kind === "error") {
49937
+ console.error(ingested.message);
49938
+ process.exit(1);
48919
49939
  }
49940
+ const args = ingested.args;
48920
49941
  const timeoutMs = flags.timeout ?? 6e4;
48921
49942
  const result = await client.execute(toolName, args, { format: "text", timeoutMs });
48922
49943
  if (result.error) {