@lotics/cli 0.57.0 → 0.60.1

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
@@ -29579,6 +29579,8 @@ var require_lib3 = __commonJS({
29579
29579
  });
29580
29580
 
29581
29581
  // src/cli.ts
29582
+ import dns from "node:dns";
29583
+ import net2 from "node:net";
29582
29584
  import fs7 from "node:fs";
29583
29585
  import path6 from "node:path";
29584
29586
  import readline from "node:readline";
@@ -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
@@ -30366,6 +30446,45 @@ function buildStarterTemplate(args) {
30366
30446
  // fall to `unknown`. The explicit glob makes the dot-dir the non-wildcard
30367
30447
  // base, which IS read. (GAP-38.)
30368
30448
  include: ["src", ".lotics/**/*"],
30449
+ // src/workflows/<alias>.ts bodies run on the server (workflow globals:
30450
+ // trigger / runtime / tool calls). They ARE locally typecheckable —
30451
+ // each is wrapped in the server's `__workflow` envelope and references
30452
+ // its per-alias `.lotics/workflows/<alias>.globals.d.ts` — but under a
30453
+ // DEDICATED config that mirrors the server's compile options (es2022
30454
+ // lib, NO DOM). The main `npm run typecheck` excludes them so it stays
30455
+ // green and DOM-lib-free for bodies; check a body with
30456
+ // `tsc -p tsconfig.workflows.json`. The server is the single verifier
30457
+ // on `lotics app workflow set`.
30458
+ exclude: ["node_modules", "src/workflows"]
30459
+ },
30460
+ null,
30461
+ 2
30462
+ ) + "\n"
30463
+ },
30464
+ {
30465
+ // Dedicated config for the editable workflow bodies (GAP-59). Mirrors the
30466
+ // SERVER's set-time typecheck options (strict, lib es2022 with NO DOM,
30467
+ // target ES2022, skipLibCheck) so a local `tsc -p tsconfig.workflows.json`
30468
+ // gives the same verdict the server would on `lotics app workflow set`.
30469
+ // Each body file is a module (the CLI writes a bookkeeping `export {};`),
30470
+ // so the per-file `__workflow` wrapper doesn't collide across bodies.
30471
+ path: "tsconfig.workflows.json",
30472
+ content: JSON.stringify(
30473
+ {
30474
+ compilerOptions: {
30475
+ target: "ES2022",
30476
+ lib: ["ES2022"],
30477
+ module: "ESNext",
30478
+ moduleResolution: "Bundler",
30479
+ strict: true,
30480
+ skipLibCheck: true,
30481
+ noEmit: true,
30482
+ types: []
30483
+ },
30484
+ // The bodies + their per-alias ambient globals. `.lotics/workflows`
30485
+ // uses the explicit `**/*` glob for the same dot-dir walk reason the
30486
+ // main config does (TS's include walk skips bare dot-dirs).
30487
+ include: ["src/workflows", ".lotics/workflows/**/*"],
30369
30488
  exclude: ["node_modules"]
30370
30489
  },
30371
30490
  null,
@@ -30705,6 +30824,60 @@ const routes = [
30705
30824
  export default function App() {
30706
30825
  return <AppRouter routes={routes} />;
30707
30826
  }
30827
+ `
30828
+ },
30829
+ {
30830
+ // The home for editable workflow bodies. `lotics app pull` writes one
30831
+ // `src/workflows/<alias>.ts` per bound workflow (faithful server source);
30832
+ // edit it, then `lotics app workflow set <alias>` pushes it back through
30833
+ // set_app_workflow (the server verifies). A new app has no bound workflows
30834
+ // yet, so this is just the docked directory + the loop reference. It's a
30835
+ // .md (not a .ts) so tsc's `include: ["src"]` never tries to compile it —
30836
+ // a workflow body is a JS-subset expression over server globals
30837
+ // (trigger / runtime / tool calls) that does NOT typecheck standalone.
30838
+ path: "src/workflows/README.md",
30839
+ content: `# Workflow bodies
30840
+
30841
+ Editable JS-subset bodies of this app's workflows live here, one file per alias:
30842
+ \`src/workflows/<alias>.ts\`.
30843
+
30844
+ ## Loop
30845
+
30846
+ \`\`\`bash
30847
+ lotics app workflow pull # write/refresh every src/workflows/<alias>.ts from the server
30848
+ # edit src/workflows/<alias>.ts
30849
+ lotics app workflow set <alias> # push it back through set_app_workflow (the server verifies)
30850
+ \`\`\`
30851
+
30852
+ \`lotics app pull <app_id>\` also writes these files (alongside the rest of the project).
30853
+
30854
+ ## What a body is
30855
+
30856
+ A workflow body is a **JS-subset expression** that runs server-side using workflow
30857
+ globals (\`trigger\`, \`runtime\`, tool calls) and ends with \`return({ data })\`. Each
30858
+ pulled file wraps the body in the server's \`__workflow\` envelope and references its
30859
+ per-alias ambient globals at \`.lotics/workflows/<alias>.globals.d.ts\`, so it **is**
30860
+ locally typecheckable \u2014 under the dedicated config that mirrors the server's compile
30861
+ options:
30862
+
30863
+ \`\`\`bash
30864
+ tsc -p tsconfig.workflows.json # same verdict the server gives on \`set\`
30865
+ \`\`\`
30866
+
30867
+ The main \`npm run typecheck\` excludes \`src/workflows\` (it would apply the app's
30868
+ DOM lib, which the server doesn't), so the bodies have their own config. The
30869
+ verification that matters still runs on the **server** when you \`set\` \u2014 the same
30870
+ guarantee as authoring via \`set_app_workflow\` directly. Edit only the body BETWEEN
30871
+ the wrapper lines; the wrapper, the \`/// <reference>\`, and the \`export {};\` marker
30872
+ are CLI bookkeeping (stripped on \`set\`). Do not rename a file (the filename is the
30873
+ alias the binding is keyed by).
30874
+
30875
+ ## Authority
30876
+
30877
+ \`lotics app deploy\` never authors workflows \u2014 it carries code, queries, and
30878
+ capabilities only. \`apps.workflows\` has exactly one author: \`set_app_workflow\`
30879
+ (which \`lotics app workflow set\` calls). The typed \`inputs\`/\`outputs\` schema for
30880
+ each alias lives in \`package.json#lotics.workflows.<alias>\`.
30708
30881
  `
30709
30882
  },
30710
30883
  {
@@ -30894,6 +31067,18 @@ import http from "node:http";
30894
31067
  import net from "node:net";
30895
31068
  import { spawn } from "node:child_process";
30896
31069
 
31070
+ // src/child_env.ts
31071
+ function ipv4ChildEnv(env) {
31072
+ return {
31073
+ ...env,
31074
+ NODE_OPTIONS: [
31075
+ env.NODE_OPTIONS,
31076
+ "--dns-result-order=ipv4first",
31077
+ "--network-family-autoselection-attempt-timeout=2000"
31078
+ ].filter(Boolean).join(" ")
31079
+ };
31080
+ }
31081
+
30897
31082
  // src/dev/rpc_handler.ts
30898
31083
  var SUPPORTED_OPS = /* @__PURE__ */ new Set([
30899
31084
  "query",
@@ -31354,7 +31539,7 @@ async function startDevServer(args) {
31354
31539
  {
31355
31540
  cwd: args.projectDir,
31356
31541
  stdio: [process.stdin.isTTY ? "inherit" : "ignore", "inherit", "inherit"],
31357
- env: { ...process.env, FORCE_COLOR: "1" }
31542
+ env: ipv4ChildEnv({ ...process.env, FORCE_COLOR: "1" })
31358
31543
  }
31359
31544
  );
31360
31545
  let stopped = false;
@@ -31778,6 +31963,162 @@ ${lines.join("\n")}
31778
31963
  `;
31779
31964
  }
31780
31965
 
31966
+ // ../shared/src/app_query_ast.ts
31967
+ function collectQueryTableIds(node) {
31968
+ const result = /* @__PURE__ */ new Set();
31969
+ walk(node, (n) => {
31970
+ if (n.kind === "from_table") result.add(n.table_id);
31971
+ });
31972
+ return result;
31973
+ }
31974
+ function walk(node, visit) {
31975
+ visit(node);
31976
+ switch (node.kind) {
31977
+ case "from_table":
31978
+ return;
31979
+ case "project":
31980
+ case "filter":
31981
+ case "group":
31982
+ case "window":
31983
+ case "sort":
31984
+ case "limit":
31985
+ case "unpivot":
31986
+ walk(node.from, visit);
31987
+ return;
31988
+ case "join":
31989
+ walk(node.left, visit);
31990
+ walk(node.right, visit);
31991
+ return;
31992
+ case "union":
31993
+ for (const child of node.sources) walk(child, visit);
31994
+ return;
31995
+ default: {
31996
+ const _exhaustive = node;
31997
+ void _exhaustive;
31998
+ return;
31999
+ }
32000
+ }
32001
+ }
32002
+
32003
+ // src/generate_app_fields.ts
32004
+ var HEADER4 = `// Auto-generated by 'lotics app codegen' (and app pull/dev/deploy).
32005
+ // DO NOT EDIT \u2014 regenerated from the workspace schema.
32006
+ //
32007
+ // Runtime field + option ids addressed by stable display-name aliases:
32008
+ // record.data[F.<TABLE>.<field>] \u2192 "fld_\u2026"
32009
+ // value === OPT.<TABLE>.<field>.<option> \u2192 "opt_\u2026"
32010
+ // A rename on the platform re-runs codegen and moves these in lockstep.
32011
+ `;
32012
+ function slugifyAlias(name, upper) {
32013
+ const stripped = name.normalize("NFD").replace(/[̀-ͯ]/g, "").replace(/đ/g, "d").replace(/Đ/g, "D");
32014
+ const cased = upper ? stripped.toUpperCase() : stripped.toLowerCase();
32015
+ const slug = cased.replace(/[^a-zA-Z0-9]+/g, "_").replace(/^_+|_+$/g, "");
32016
+ if (slug === "") return "_";
32017
+ return /^[0-9]/.test(slug) ? `_${slug}` : slug;
32018
+ }
32019
+ function dedupeAliases(names, upper) {
32020
+ const used = /* @__PURE__ */ new Map();
32021
+ return names.map((name) => {
32022
+ const base = slugifyAlias(name, upper);
32023
+ const seen = used.get(base);
32024
+ if (seen === void 0) {
32025
+ used.set(base, 1);
32026
+ return base;
32027
+ }
32028
+ let n = seen + 1;
32029
+ while (used.has(`${base}_${n}`)) n++;
32030
+ used.set(base, n);
32031
+ used.set(`${base}_${n}`, 1);
32032
+ return `${base}_${n}`;
32033
+ });
32034
+ }
32035
+ function propKey(alias) {
32036
+ return isValidIdentifier(alias) ? alias : JSON.stringify(alias);
32037
+ }
32038
+ function aliasTables(tables) {
32039
+ const tableAliases = dedupeAliases(
32040
+ tables.map((t) => t.name),
32041
+ true
32042
+ );
32043
+ return tables.map((table, i2) => {
32044
+ const fieldAliases = dedupeAliases(
32045
+ table.fields.map((f) => f.name),
32046
+ false
32047
+ );
32048
+ return {
32049
+ alias: tableAliases[i2],
32050
+ table,
32051
+ fields: table.fields.map((field, j) => ({ alias: fieldAliases[j], field }))
32052
+ };
32053
+ });
32054
+ }
32055
+ function emitFieldMap(aliased) {
32056
+ const tableBlocks = aliased.map(({ alias, fields }) => {
32057
+ const fieldLines = fields.map(
32058
+ ({ alias: fieldAlias, field }) => ` ${propKey(fieldAlias)}: ${JSON.stringify(field.id)},`
32059
+ );
32060
+ return ` ${propKey(alias)}: {
32061
+ ${fieldLines.join("\n")}
32062
+ },`;
32063
+ });
32064
+ return `export const F = {
32065
+ ${tableBlocks.join("\n")}
32066
+ } as const;`;
32067
+ }
32068
+ function emitOptionMap(aliased) {
32069
+ const tableBlocks = [];
32070
+ for (const { alias, fields } of aliased) {
32071
+ const fieldBlocks = [];
32072
+ for (const { alias: fieldAlias, field } of fields) {
32073
+ const options = field.options ?? [];
32074
+ if (options.length === 0) continue;
32075
+ const optionAliases = dedupeAliases(
32076
+ options.map((o) => o.label),
32077
+ false
32078
+ );
32079
+ const optionLines = options.map(
32080
+ (option, i2) => ` ${propKey(optionAliases[i2])}: ${JSON.stringify(option.id)},`
32081
+ );
32082
+ fieldBlocks.push(` ${propKey(fieldAlias)}: {
32083
+ ${optionLines.join("\n")}
32084
+ },`);
32085
+ }
32086
+ if (fieldBlocks.length === 0) continue;
32087
+ tableBlocks.push(` ${propKey(alias)}: {
32088
+ ${fieldBlocks.join("\n")}
32089
+ },`);
32090
+ }
32091
+ if (tableBlocks.length === 0) return `export const OPT = {} as const;`;
32092
+ return `export const OPT = {
32093
+ ${tableBlocks.join("\n")}
32094
+ } as const;`;
32095
+ }
32096
+ function generateAppFields(tables) {
32097
+ if (tables.length === 0) {
32098
+ return `${HEADER4}
32099
+ export const F = {} as const;
32100
+
32101
+ export const OPT = {} as const;
32102
+
32103
+ /** Field-id alias map (empty \u2014 no tables in scope). */
32104
+ export type AppFields = typeof F;
32105
+ /** Select-option alias map (empty \u2014 no tables in scope). */
32106
+ export type AppOptions = typeof OPT;
32107
+ `;
32108
+ }
32109
+ const aliased = aliasTables(tables);
32110
+ return `${HEADER4}
32111
+ ${emitFieldMap(aliased)}
32112
+
32113
+ ${emitOptionMap(aliased)}
32114
+
32115
+ /** Field-id alias map: \`F[<TABLE>][<field>]\` is the \`fld_\u2026\` id (literal-typed). */
32116
+ export type AppFields = typeof F;
32117
+ /** Select-option alias map: \`OPT[<TABLE>][<field>][<option>]\` is the \`opt_\u2026\` id. */
32118
+ export type AppOptions = typeof OPT;
32119
+ `;
32120
+ }
32121
+
31781
32122
  // src/app_commands.ts
31782
32123
  async function fetchLatestNpmVersion(packageName) {
31783
32124
  try {
@@ -31795,6 +32136,98 @@ async function fetchLatestNpmVersion(packageName) {
31795
32136
  return null;
31796
32137
  }
31797
32138
  }
32139
+ var WORKFLOWS_DIR = path4.join("src", "workflows");
32140
+ var WORKFLOW_GLOBALS_DIR = path4.join(".lotics", "workflows");
32141
+ var FALLBACK_ENVELOPE_PREFIX = "async function __workflow(): Promise<__WorkflowReturn | void> {\n";
32142
+ var FALLBACK_ENVELOPE_SUFFIX = "\n}";
32143
+ function workflowFileHeader(alias) {
32144
+ const refPath = path4.join("..", "..", WORKFLOW_GLOBALS_DIR, `${alias}.globals.d.ts`).split(path4.sep).join("/");
32145
+ return `/// <reference path="${refPath}" />
32146
+ // Auto-pulled workflow body for "${alias}". Edit the BODY between the wrapper
32147
+ // lines below, then push with:
32148
+ // lotics app workflow set ${alias}
32149
+ // The push goes through set_app_workflow, where the SERVER verifies the body.
32150
+ // The __workflow wrapper + the reference above are CLI bookkeeping (stripped on
32151
+ // set) \u2014 they only make the body typecheck locally against the workspace types.
32152
+ // Do NOT rename this file \u2014 the filename is the alias the binding is keyed by.
32153
+ export {};
32154
+ `;
32155
+ }
32156
+ function workflowFilePath(projectDir, alias) {
32157
+ return path4.join(projectDir, WORKFLOWS_DIR, `${alias}.ts`);
32158
+ }
32159
+ function workflowGlobalsPath(projectDir, alias) {
32160
+ return path4.join(projectDir, WORKFLOW_GLOBALS_DIR, `${alias}.globals.d.ts`);
32161
+ }
32162
+ function writeWorkflowGlobals(projectDir, alias, dts) {
32163
+ const dir = path4.join(projectDir, WORKFLOW_GLOBALS_DIR);
32164
+ fs3.mkdirSync(dir, { recursive: true });
32165
+ const file = workflowGlobalsPath(projectDir, alias);
32166
+ fs3.writeFileSync(file, `${dts.replace(/\s+$/, "")}
32167
+ `);
32168
+ return file;
32169
+ }
32170
+ function writeWorkflowFile(projectDir, alias, source, envelope = { prefix: FALLBACK_ENVELOPE_PREFIX, suffix: FALLBACK_ENVELOPE_SUFFIX }) {
32171
+ const dir = path4.join(projectDir, WORKFLOWS_DIR);
32172
+ fs3.mkdirSync(dir, { recursive: true });
32173
+ const file = workflowFilePath(projectDir, alias);
32174
+ const body = source.replace(/\s+$/, "");
32175
+ fs3.writeFileSync(file, `${workflowFileHeader(alias)}
32176
+ ${envelope.prefix}${body}${envelope.suffix}
32177
+ `);
32178
+ return file;
32179
+ }
32180
+ var WORKFLOW_WRAPPER_OPENER = /^\s*async\s+function\s+__workflow\s*\(/;
32181
+ function stripWorkflowHeader(content) {
32182
+ const lines = content.split("\n");
32183
+ let headerEnd = 0;
32184
+ while (headerEnd < lines.length && /^\s*\/\//.test(lines[headerEnd])) headerEnd++;
32185
+ while (headerEnd < lines.length && /^\s*export\s*\{\s*\}\s*;?\s*$/.test(lines[headerEnd])) headerEnd++;
32186
+ while (headerEnd < lines.length && lines[headerEnd].trim() === "") headerEnd++;
32187
+ let start;
32188
+ if (headerEnd < lines.length && WORKFLOW_WRAPPER_OPENER.test(lines[headerEnd])) {
32189
+ start = headerEnd;
32190
+ } else if (lines.length > 0 && WORKFLOW_WRAPPER_OPENER.test(lines[0])) {
32191
+ start = 0;
32192
+ } else {
32193
+ return content.replace(/\s+$/, "");
32194
+ }
32195
+ let end = lines.length - 1;
32196
+ while (end > start && lines[end].trim() === "") end--;
32197
+ if (lines[end]?.trim() === "}") {
32198
+ return lines.slice(start + 1, end).join("\n").replace(/\s+$/, "");
32199
+ }
32200
+ return lines.slice(start + 1).join("\n").replace(/\s+$/, "");
32201
+ }
32202
+ async function writeWorkflowFiles(client, projectDir, app_id, aliases) {
32203
+ const written = [];
32204
+ for (const alias of aliases) {
32205
+ const res = await client.getAppWorkflow(app_id, alias);
32206
+ const source = res.error || res.result === null || typeof res.result !== "object" ? null : res.result.source;
32207
+ if (typeof source !== "string" || source.trim() === "") {
32208
+ console.error(
32209
+ `\u26A0 Skipped src/workflows/${alias}.ts \u2014 the server returned no readable source (${res.error ?? "legacy workflow with no rendered body"}).`
32210
+ );
32211
+ continue;
32212
+ }
32213
+ const envelope = await fetchWorkflowGlobals(client, projectDir, app_id, alias);
32214
+ writeWorkflowFile(projectDir, alias, source, envelope);
32215
+ written.push(alias);
32216
+ }
32217
+ return written;
32218
+ }
32219
+ async function fetchWorkflowGlobals(client, projectDir, app_id, alias) {
32220
+ try {
32221
+ const { dts, envelope_prefix, envelope_suffix } = await client.getAppWorkflowDts(app_id, alias);
32222
+ writeWorkflowGlobals(projectDir, alias, dts);
32223
+ return { prefix: envelope_prefix, suffix: envelope_suffix };
32224
+ } catch (err2) {
32225
+ console.error(
32226
+ `\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.`
32227
+ );
32228
+ return { prefix: FALLBACK_ENVELOPE_PREFIX, suffix: FALLBACK_ENVELOPE_SUFFIX };
32229
+ }
32230
+ }
31798
32231
  function runTar(args, cwd) {
31799
32232
  return new Promise((resolve, reject2) => {
31800
32233
  const proc = spawn2("tar", args, { cwd, stdio: ["ignore", "ignore", "pipe"] });
@@ -31811,7 +32244,7 @@ function runTar(args, cwd) {
31811
32244
  }
31812
32245
  function runNpm(args, cwd) {
31813
32246
  return new Promise((resolve, reject2) => {
31814
- const proc = spawn2("npm", args, { cwd, stdio: "inherit" });
32247
+ const proc = spawn2("npm", args, { cwd, stdio: "inherit", env: ipv4ChildEnv(process.env) });
31815
32248
  proc.on("error", reject2);
31816
32249
  proc.on("exit", (code) => {
31817
32250
  if (code === 0) resolve();
@@ -31850,18 +32283,71 @@ function writeAppMeta(projectDir, meta) {
31850
32283
  function writeAppDts(projectDir, manifest) {
31851
32284
  const dotLotics = path4.join(projectDir, ".lotics");
31852
32285
  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)
31864
- );
32286
+ const written = [
32287
+ [path4.join(dotLotics, "app_workflows.d.ts"), generateAppWorkflowsDts(manifest.workflows)],
32288
+ [path4.join(dotLotics, "app_queries.d.ts"), generateAppQueriesDts(manifest.queries)],
32289
+ [path4.join(dotLotics, "app_agents.d.ts"), generateAppAgentsDts(manifest.agents)]
32290
+ ];
32291
+ for (const [file, content] of written) fs3.writeFileSync(file, content);
32292
+ return written.map(([file]) => file);
32293
+ }
32294
+ function readCodegenTablesAllowlist(projectDir) {
32295
+ const pkg2 = JSON.parse(fs3.readFileSync(path4.join(projectDir, "package.json"), "utf-8"));
32296
+ const tables = pkg2.lotics?.codegen?.tables;
32297
+ return Array.isArray(tables) ? tables.filter((t) => typeof t === "string") : [];
32298
+ }
32299
+ function resolveCodegenTableIds(projectDir, queries) {
32300
+ const ids = /* @__PURE__ */ new Set();
32301
+ for (const decl of Object.values(queries)) {
32302
+ for (const id of collectQueryTableIds(decl.ast)) ids.add(id);
32303
+ }
32304
+ for (const id of readCodegenTablesAllowlist(projectDir)) ids.add(id);
32305
+ return [...ids];
32306
+ }
32307
+ function writeAppFields(projectDir, tables) {
32308
+ const dotLotics = path4.join(projectDir, ".lotics");
32309
+ fs3.mkdirSync(dotLotics, { recursive: true });
32310
+ const file = path4.join(dotLotics, "app_fields.ts");
32311
+ fs3.writeFileSync(file, generateAppFields(tables));
32312
+ return file;
32313
+ }
32314
+ async function appCodegen(args) {
32315
+ const projectDir = path4.resolve(args.projectDir ?? process.cwd());
32316
+ const meta = readAppMeta(projectDir);
32317
+ const dtsPaths = writeAppDts(projectDir, {
32318
+ workflows: meta.workflows,
32319
+ queries: meta.queries,
32320
+ agents: meta.agents
32321
+ });
32322
+ for (const p of dtsPaths) console.error(`Regenerated ${p}`);
32323
+ if (!args.client) {
32324
+ console.error(
32325
+ "Skipped .lotics/app_fields.ts \u2014 no workspace credentials resolved. Run authenticated (or set LOTICS_API_KEY) to regenerate field/option ids."
32326
+ );
32327
+ return;
32328
+ }
32329
+ const tableIds = resolveCodegenTableIds(projectDir, meta.queries ?? {});
32330
+ try {
32331
+ const tables = await args.client.getWorkspaceSchema(tableIds);
32332
+ const fieldsPath = writeAppFields(projectDir, tables);
32333
+ console.error(`Regenerated ${fieldsPath} (${tables.length} table${tables.length === 1 ? "" : "s"})`);
32334
+ } catch (err2) {
32335
+ console.error(
32336
+ `\u26A0 Could not fetch the workspace schema (${err2 instanceof Error ? err2.message : String(err2)}). Kept the existing .lotics/app_fields.ts.`
32337
+ );
32338
+ }
32339
+ await refreshWorkflowGlobals(args.client, projectDir, meta.app_id, Object.keys(meta.workflows ?? {}));
32340
+ }
32341
+ async function refreshWorkflowGlobals(client, projectDir, app_id, aliases) {
32342
+ for (const alias of aliases) {
32343
+ const file = workflowFilePath(projectDir, alias);
32344
+ if (!fs3.existsSync(file)) continue;
32345
+ const body = stripWorkflowHeader(fs3.readFileSync(file, "utf-8"));
32346
+ if (body.trim() === "") continue;
32347
+ const envelope = await fetchWorkflowGlobals(client, projectDir, app_id, alias);
32348
+ writeWorkflowFile(projectDir, alias, body, envelope);
32349
+ console.error(`Refreshed ${path4.relative(projectDir, file)} + its workflow types`);
32350
+ }
31865
32351
  }
31866
32352
  function stampPulledManifest(projectDir, args) {
31867
32353
  writeAppMeta(projectDir, {
@@ -31969,6 +32455,15 @@ async function appPull(client, args) {
31969
32455
  queries: app.queries ?? {},
31970
32456
  agents: app.agents ?? {}
31971
32457
  });
32458
+ const aliases = Object.keys(app.workflows ?? {});
32459
+ if (aliases.length > 0) {
32460
+ const written = await writeWorkflowFiles(client, targetPath, app.id, aliases);
32461
+ if (written.length > 0) {
32462
+ console.error(
32463
+ `Wrote ${written.length} workflow ${written.length === 1 ? "body" : "bodies"} to ${WORKFLOWS_DIR}/ (${written.join(", ")})`
32464
+ );
32465
+ }
32466
+ }
31972
32467
  console.error(`Installing npm dependencies...`);
31973
32468
  await runNpm(["install"], targetPath);
31974
32469
  console.error(`
@@ -31992,14 +32487,14 @@ function readAppSourceText(projectDir) {
31992
32487
  const srcDir = path4.join(projectDir, "src");
31993
32488
  if (!fs3.existsSync(srcDir)) return "";
31994
32489
  const parts = [];
31995
- const walk = (dir) => {
32490
+ const walk2 = (dir) => {
31996
32491
  for (const entry of fs3.readdirSync(dir, { withFileTypes: true })) {
31997
32492
  const full = path4.join(dir, entry.name);
31998
- if (entry.isDirectory()) walk(full);
32493
+ if (entry.isDirectory()) walk2(full);
31999
32494
  else if (/\.(ts|tsx|js|jsx)$/.test(entry.name)) parts.push(fs3.readFileSync(full, "utf8"));
32000
32495
  }
32001
32496
  };
32002
- walk(srcDir);
32497
+ walk2(srcDir);
32003
32498
  return parts.join("\n");
32004
32499
  }
32005
32500
  async function appDeploy(client, args) {
@@ -32146,6 +32641,209 @@ async function appDev(client, args) {
32146
32641
  console.error("\nStopping\u2026");
32147
32642
  await handle.stop();
32148
32643
  }
32644
+ function parseCreatedRecords(value) {
32645
+ if (!Array.isArray(value)) return [];
32646
+ const groups = [];
32647
+ for (const entry of value) {
32648
+ if (!entry || typeof entry !== "object") continue;
32649
+ const tableId = entry.table_id;
32650
+ const recordIds = entry.record_ids;
32651
+ if (typeof tableId !== "string" || !Array.isArray(recordIds)) continue;
32652
+ const ids = recordIds.filter((id) => typeof id === "string");
32653
+ if (ids.length > 0) groups.push({ table_id: tableId, record_ids: ids });
32654
+ }
32655
+ return groups;
32656
+ }
32657
+ function parseIrreversibleToolNames(value) {
32658
+ if (!Array.isArray(value)) return [];
32659
+ const names = [];
32660
+ for (const entry of value) {
32661
+ if (!entry || typeof entry !== "object") continue;
32662
+ const name = entry.tool_name;
32663
+ if (typeof name === "string" && !names.includes(name)) names.push(name);
32664
+ }
32665
+ return names;
32666
+ }
32667
+ function printSideEffects(summary) {
32668
+ const created = parseCreatedRecords(summary.created_records);
32669
+ const irreversibleTools = parseIrreversibleToolNames(summary.irreversible_tool_calls);
32670
+ const subWorkflows = summary.sub_workflows_possible === true;
32671
+ console.error("\nCreated records:");
32672
+ if (created.length === 0) {
32673
+ console.error(" (none with ids to clean up)");
32674
+ } else {
32675
+ for (const group of created) {
32676
+ console.error(` ${group.table_id}: ${group.record_ids.length} record(s)`);
32677
+ const payload = JSON.stringify({ table_id: group.table_id, record_ids: group.record_ids });
32678
+ console.error(` lotics run delete_records '${payload}'`);
32679
+ }
32680
+ }
32681
+ const irreversiblePart = irreversibleTools.length > 0 ? `Could NOT auto-undo (clean up manually): ${irreversibleTools.join(", ")}.` : "Could NOT auto-undo: none.";
32682
+ const subPart = subWorkflows ? " Sub-workflows may have run (after_* table workflows) \u2014 their effects are NOT in this list." : "";
32683
+ console.error(`
32684
+ ${irreversiblePart}${subPart}`);
32685
+ }
32686
+ async function cleanupCreatedRecords(client, created) {
32687
+ if (created.length === 0) {
32688
+ console.error("\nNo created records to clean up.");
32689
+ return true;
32690
+ }
32691
+ console.error("\nCleaning up created records (delete_records \u2014 records only):");
32692
+ let allDeleted = true;
32693
+ for (const group of created) {
32694
+ const res = await client.execute("delete_records", {
32695
+ table_id: group.table_id,
32696
+ record_ids: group.record_ids
32697
+ });
32698
+ if (res.error) {
32699
+ console.error(` \u2717 ${group.table_id}: ${res.error}`);
32700
+ allDeleted = false;
32701
+ } else {
32702
+ console.error(` \u2713 ${group.table_id}: deleted ${group.record_ids.length} record(s)`);
32703
+ }
32704
+ }
32705
+ return allDeleted;
32706
+ }
32707
+ async function appExecuteWorkflow(client, args) {
32708
+ const meta = readAppMeta(process.cwd());
32709
+ const result = await client.appWorkflow(meta.app_id, args.alias, args.inputs);
32710
+ console.log(JSON.stringify(result, null, 2));
32711
+ const status = typeof result.status === "string" ? result.status : "unknown";
32712
+ const message = typeof result.message === "string" ? result.message : "";
32713
+ console.error(`Workflow "${args.alias}" \u2192 ${status}${message ? `: ${message}` : ""}`);
32714
+ let cleanupFailed = false;
32715
+ if ((args.printCreated || args.cleanup) && result.side_effects) {
32716
+ printSideEffects(result.side_effects);
32717
+ if (args.cleanup) {
32718
+ const allDeleted = await cleanupCreatedRecords(
32719
+ client,
32720
+ parseCreatedRecords(result.side_effects.created_records)
32721
+ );
32722
+ cleanupFailed = !allDeleted;
32723
+ }
32724
+ } else if (args.printCreated || args.cleanup) {
32725
+ console.error("\n(no side-effect summary returned by the server)");
32726
+ }
32727
+ if (status === "error" || cleanupFailed) process.exit(1);
32728
+ }
32729
+ async function appWorkflowSet(client, args) {
32730
+ const projectDir = process.cwd();
32731
+ const meta = readAppMeta(projectDir);
32732
+ const declaration = meta.workflows?.[args.alias];
32733
+ if (!declaration) {
32734
+ console.error(
32735
+ `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.`
32736
+ );
32737
+ process.exit(1);
32738
+ }
32739
+ const file = workflowFilePath(projectDir, args.alias);
32740
+ if (!fs3.existsSync(file)) {
32741
+ console.error(
32742
+ `No workflow body at ${path4.relative(projectDir, file)}. Run 'lotics app pull ${meta.app_id}' to write src/workflows/${args.alias}.ts, then edit it.`
32743
+ );
32744
+ process.exit(1);
32745
+ }
32746
+ const source = stripWorkflowHeader(fs3.readFileSync(file, "utf-8"));
32747
+ if (source.trim() === "") {
32748
+ console.error(`Workflow body ${path4.relative(projectDir, file)} is empty after stripping the header.`);
32749
+ process.exit(1);
32750
+ }
32751
+ const res = await client.setAppWorkflow(meta.app_id, args.alias, {
32752
+ source,
32753
+ inputs: declaration.inputs,
32754
+ outputs: declaration.outputs
32755
+ });
32756
+ if (res.error) {
32757
+ console.error(`Failed to set workflow "${args.alias}": ${res.error}`);
32758
+ process.exit(1);
32759
+ }
32760
+ const result = res.result ?? {};
32761
+ const workflowId = typeof result.workflow_id === "string" ? result.workflow_id : "(unknown)";
32762
+ console.error(`Set workflow "${args.alias}" \u2192 ${workflowId}`);
32763
+ if (result.outputs && typeof result.outputs === "object") {
32764
+ console.error(` result.data schema: ${JSON.stringify(result.outputs)}`);
32765
+ }
32766
+ }
32767
+ async function appWorkflowPull(client) {
32768
+ const projectDir = process.cwd();
32769
+ const meta = readAppMeta(projectDir);
32770
+ const app = await client.getApp(meta.app_id);
32771
+ const aliases = Object.keys(app.workflows ?? {});
32772
+ if (aliases.length === 0) {
32773
+ console.error(`App ${meta.app_id} has no bound workflows.`);
32774
+ return;
32775
+ }
32776
+ const written = await writeWorkflowFiles(client, projectDir, meta.app_id, aliases);
32777
+ console.error(
32778
+ `Wrote ${written.length} workflow ${written.length === 1 ? "body" : "bodies"} to ${WORKFLOWS_DIR}/` + (written.length > 0 ? ` (${written.join(", ")})` : "")
32779
+ );
32780
+ }
32781
+ function findUiSrcDir(start) {
32782
+ let dir = path4.resolve(start);
32783
+ for (; ; ) {
32784
+ const candidate = path4.join(dir, "packages", "ui", "src");
32785
+ if (fs3.existsSync(candidate) && fs3.statSync(candidate).isDirectory()) return candidate;
32786
+ const parent = path4.dirname(dir);
32787
+ if (parent === dir) return null;
32788
+ dir = parent;
32789
+ }
32790
+ }
32791
+ var UI_ALIAS_FIND_SOURCE = String.raw`/^@lotics\/ui\/(.+)$/`;
32792
+ function appUiLink(args) {
32793
+ const projectDir = path4.resolve(args.projectDir ?? process.cwd());
32794
+ const viteConfigPath = path4.join(projectDir, "vite.config.ts");
32795
+ if (!fs3.existsSync(viteConfigPath)) {
32796
+ throw new Error(
32797
+ `No vite.config.ts at ${projectDir}. Run inside a 'lotics app' project directory.`
32798
+ );
32799
+ }
32800
+ const uiSrc = findUiSrcDir(projectDir);
32801
+ if (!uiSrc) {
32802
+ throw new Error(
32803
+ "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."
32804
+ );
32805
+ }
32806
+ const hasComponent = fs3.existsSync(path4.join(uiSrc, `${args.component}.tsx`)) || fs3.existsSync(path4.join(uiSrc, `${args.component}.ts`)) || fs3.existsSync(path4.join(uiSrc, args.component));
32807
+ if (!hasComponent) {
32808
+ throw new Error(
32809
+ `No '@lotics/ui/${args.component}' under ${uiSrc} (expected ${args.component}.tsx/.ts). Check the component name.`
32810
+ );
32811
+ }
32812
+ const source = fs3.readFileSync(viteConfigPath, "utf-8");
32813
+ const aliasEntry = `{ find: ${UI_ALIAS_FIND_SOURCE}, replacement: ${JSON.stringify(`${uiSrc}/$1`)} },`;
32814
+ const alreadyLinked = source.includes(UI_ALIAS_FIND_SOURCE);
32815
+ if (args.remove) {
32816
+ if (!alreadyLinked) {
32817
+ console.error("No @lotics/ui dev-link alias present \u2014 nothing to remove.");
32818
+ return;
32819
+ }
32820
+ const stripped = source.replace(new RegExp(`^\\s*\\{ find: ${escapeRegExp(UI_ALIAS_FIND_SOURCE)}.*$\\n?`, "m"), "");
32821
+ fs3.writeFileSync(viteConfigPath, stripped);
32822
+ console.error(`Removed the @lotics/ui dev-link alias from ${viteConfigPath}.`);
32823
+ console.error("Restart `lotics app dev` and rm -rf node_modules/.vite to clear cached modules.");
32824
+ return;
32825
+ }
32826
+ if (alreadyLinked) {
32827
+ console.error(`@lotics/ui is already dev-linked in ${viteConfigPath}.`);
32828
+ return;
32829
+ }
32830
+ const aliasMatch = /resolve\s*:\s*\{[\s\S]*?alias\s*:\s*\[/.exec(source);
32831
+ if (!aliasMatch) {
32832
+ throw new Error(
32833
+ `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.`
32834
+ );
32835
+ }
32836
+ const insertAt = aliasMatch.index + aliasMatch[0].length;
32837
+ const updated = `${source.slice(0, insertAt)}
32838
+ ${aliasEntry}${source.slice(insertAt)}`;
32839
+ fs3.writeFileSync(viteConfigPath, updated);
32840
+ console.error(`Dev-linked @lotics/ui \u2192 ${uiSrc} in ${viteConfigPath}.`);
32841
+ console.error("Restart `lotics app dev` and rm -rf node_modules/.vite to clear cached modules.");
32842
+ console.error("Finalize: PR the packages/ui change \u2192 publish \u2192 `lotics ui link <component> --remove` + bump the app's dep.");
32843
+ }
32844
+ function escapeRegExp(s) {
32845
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
32846
+ }
32149
32847
 
32150
32848
  // src/args.ts
32151
32849
  function parseArgs(argv) {
@@ -32162,6 +32860,8 @@ function parseArgs(argv) {
32162
32860
  message: void 0,
32163
32861
  local: false,
32164
32862
  all: false,
32863
+ printCreated: false,
32864
+ cleanup: false,
32165
32865
  version: false,
32166
32866
  help: false
32167
32867
  };
@@ -32212,6 +32912,13 @@ function parseArgs(argv) {
32212
32912
  case "--all":
32213
32913
  flags.all = true;
32214
32914
  break;
32915
+ case "--print-created":
32916
+ case "--report-effects":
32917
+ flags.printCreated = true;
32918
+ break;
32919
+ case "--cleanup":
32920
+ flags.cleanup = true;
32921
+ break;
32215
32922
  case "--version":
32216
32923
  case "-v":
32217
32924
  flags.version = true;
@@ -32237,6 +32944,34 @@ function parseArgs(argv) {
32237
32944
  return { command, subcommand, toolArgs, restArgs, flags };
32238
32945
  }
32239
32946
 
32947
+ // src/inputs.ts
32948
+ async function ingestJsonArgs(opts) {
32949
+ let raw = opts.rawArg;
32950
+ if (raw && raw.startsWith("@")) {
32951
+ const argsPath = raw.slice(1);
32952
+ try {
32953
+ raw = opts.readFile(argsPath);
32954
+ } catch (err2) {
32955
+ return {
32956
+ kind: "error",
32957
+ message: `Cannot read args file "${argsPath}": ${err2 instanceof Error ? err2.message : String(err2)}`
32958
+ };
32959
+ }
32960
+ } else if (!raw && !opts.stdinIsTTY) {
32961
+ raw = await opts.readStdin();
32962
+ }
32963
+ if (!raw) return { kind: "ok", args: {} };
32964
+ try {
32965
+ const parsed = JSON.parse(raw);
32966
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
32967
+ return { kind: "error", message: `JSON args must be an object, got: ${raw}` };
32968
+ }
32969
+ return { kind: "ok", args: parsed };
32970
+ } catch {
32971
+ return { kind: "error", message: `Invalid JSON: ${raw}` };
32972
+ }
32973
+ }
32974
+
32240
32975
  // src/xlsx.ts
32241
32976
  import fs5 from "node:fs";
32242
32977
 
@@ -48170,6 +48905,8 @@ async function runDocxCommand(subcommand, toolArgs, restArgs) {
48170
48905
  }
48171
48906
 
48172
48907
  // src/cli.ts
48908
+ dns.setDefaultResultOrder("ipv4first");
48909
+ net2.setDefaultAutoSelectFamilyAttemptTimeout(2e3);
48173
48910
  function printHelp() {
48174
48911
  console.log(`Lotics CLI v${VERSION} \u2014 AI agent interface for Lotics
48175
48912
 
@@ -48226,9 +48963,21 @@ COMMANDS
48226
48963
  lotics app deploy [-m <message>] Build + upload current dir as a new version
48227
48964
  (code + queries only \u2014 workflow bindings are
48228
48965
  managed by set_app_workflow / remove_app_workflow)
48966
+ lotics app codegen [path] Regenerate .lotics/* (types + field/option ids)
48967
+ from the manifest + workspace schema \u2014 no deploy
48968
+ lotics app workflow run <alias> '<json>' Execute a bound app workflow end-to-end
48969
+ (inputs: inline JSON, @file, or stdin;
48970
+ --print-created reports created records +
48971
+ a paste-ready cleanup plan; --cleanup also
48972
+ deletes those records \u2014 NOT a rollback)
48973
+ lotics app workflow set <alias> Push the edited src/workflows/<alias>.ts body
48974
+ through set_app_workflow (server verifies)
48975
+ lotics app workflow pull Rewrite src/workflows/*.ts from the server
48229
48976
  lotics app subdomain <new-subdomain> Rename the app's public <slug>.lotics.app address
48230
48977
  lotics app rename "<new name>" Rename the app's display name (launcher title)
48231
48978
  lotics app dev [path] Run the app locally with HMR (RPC forwarded to prod)
48979
+ lotics ui link <component> [--remove] Dev-link @lotics/ui to the monorepo's
48980
+ packages/ui/src for live HMR (monorepo only)
48232
48981
  lotics xlsx <subcommand> ... Read/write/edit .xlsx files on your local filesystem
48233
48982
  (uses the bundled Lotics xlsx engine; prefer over
48234
48983
  npm xlsx/exceljs for round-trip fidelity)
@@ -48588,6 +49337,34 @@ async function main() {
48588
49337
  await runDocxCommand(subcommand, toolArgs, restArgs);
48589
49338
  return;
48590
49339
  }
49340
+ if (command === "ui") {
49341
+ if (subcommand === "link") {
49342
+ const component = toolArgs;
49343
+ if (!component) {
49344
+ console.error("Usage: lotics ui link <component> [--remove]");
49345
+ console.error("Dev-links @lotics/ui to the monorepo's packages/ui/src for live HMR.");
49346
+ process.exit(1);
49347
+ }
49348
+ appUiLink({ component, remove: restArgs.includes("--remove") });
49349
+ return;
49350
+ }
49351
+ console.error(`Unknown ui subcommand: ${subcommand ?? "(none)"}`);
49352
+ console.error("Usage: lotics ui link <component> [--remove]");
49353
+ process.exit(1);
49354
+ }
49355
+ if (command === "app" && subcommand === "codegen") {
49356
+ const projectDir = toolArgs;
49357
+ const ctx2 = resolveContext(flags);
49358
+ if (!ctx2) {
49359
+ await appCodegen({ projectDir });
49360
+ return;
49361
+ }
49362
+ const viewAsMemberId = flags.viewAs ?? process.env.LOTICS_VIEW_AS;
49363
+ const client2 = new LoticsClient({ apiKey: ctx2.apiKey, workspaceId: ctx2.workspaceId, viewAsMemberId });
49364
+ await resolveWorkspace(client2, ctx2);
49365
+ await appCodegen({ projectDir, client: client2 });
49366
+ return;
49367
+ }
48591
49368
  if (command === "org") {
48592
49369
  const global2 = loadGlobalConfig() ?? {};
48593
49370
  const profiles = global2.profiles ?? {};
@@ -48655,6 +49432,10 @@ async function main() {
48655
49432
  console.error(" lotics app create <name> [path] Scaffold a new app locally");
48656
49433
  console.error(" lotics app pull <app_id> [path] Pull an existing app for local editing");
48657
49434
  console.error(" lotics app deploy [-m <message>] Build + upload the current directory");
49435
+ console.error(" lotics app codegen [path] Regenerate .lotics/* (types + field ids) \u2014 no deploy");
49436
+ console.error(" lotics app workflow run <alias> '<json>' Execute a bound app workflow end-to-end");
49437
+ console.error(" lotics app workflow set <alias> Push the edited src/workflows/<alias>.ts body");
49438
+ console.error(" lotics app workflow pull Rewrite src/workflows/*.ts from the server");
48658
49439
  console.error(" lotics app subdomain <new-subdomain> Rename the app's public <slug>.lotics.app address");
48659
49440
  console.error(` lotics app rename "<new name>" Rename the app's display name (launcher title)`);
48660
49441
  console.error(" lotics app dev [path] Run the app locally with HMR + RPC forwarding");
@@ -48796,6 +49577,59 @@ Available workspaces:`);
48796
49577
  await appRename(client, { name: newName });
48797
49578
  return;
48798
49579
  }
49580
+ if (subcommand === "workflow") {
49581
+ const action = toolArgs;
49582
+ const workflowUsage = () => {
49583
+ console.error("Usage:");
49584
+ console.error(" lotics app workflow run <alias> '<json>' Execute a bound app workflow");
49585
+ console.error(" lotics app workflow set <alias> Push src/workflows/<alias>.ts");
49586
+ console.error(" lotics app workflow pull Rewrite src/workflows/*.ts from the server");
49587
+ process.exit(1);
49588
+ };
49589
+ if (action === "run") {
49590
+ const alias = restArgs[0];
49591
+ if (!alias) {
49592
+ console.error("Usage: lotics app workflow run <alias> '<json>'");
49593
+ console.error(" lotics app workflow run <alias> @inputs.json (read inputs from a file)");
49594
+ console.error(" cat inputs.json | lotics app workflow run <alias> (read inputs from stdin)");
49595
+ console.error("Flags: --print-created (report created records + cleanup plan + caveat)");
49596
+ console.error(" --cleanup (also delete the created records \u2014 records only, NOT a rollback)");
49597
+ process.exit(1);
49598
+ }
49599
+ const ingested = await ingestJsonArgs({
49600
+ rawArg: restArgs[1],
49601
+ stdinIsTTY: process.stdin.isTTY ?? false,
49602
+ readFile: (p) => fs7.readFileSync(p, "utf-8"),
49603
+ readStdin
49604
+ });
49605
+ if (ingested.kind === "error") {
49606
+ console.error(ingested.message);
49607
+ process.exit(1);
49608
+ }
49609
+ await appExecuteWorkflow(client, {
49610
+ alias,
49611
+ inputs: ingested.args,
49612
+ printCreated: flags.printCreated,
49613
+ cleanup: flags.cleanup
49614
+ });
49615
+ return;
49616
+ }
49617
+ if (action === "set") {
49618
+ const alias = restArgs[0];
49619
+ if (!alias) {
49620
+ console.error("Usage: lotics app workflow set <alias>");
49621
+ console.error("Pushes the edited src/workflows/<alias>.ts body via set_app_workflow.");
49622
+ process.exit(1);
49623
+ }
49624
+ await appWorkflowSet(client, { alias });
49625
+ return;
49626
+ }
49627
+ if (action === "pull") {
49628
+ await appWorkflowPull(client);
49629
+ return;
49630
+ }
49631
+ workflowUsage();
49632
+ }
48799
49633
  if (subcommand === "dev") {
48800
49634
  const projectDir = toolArgs;
48801
49635
  let port;
@@ -48894,29 +49728,17 @@ ${JSON.stringify(info.input_schema, null, 2)}`);
48894
49728
  }
48895
49729
  if (command === "run") {
48896
49730
  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
- }
49731
+ const ingested = await ingestJsonArgs({
49732
+ rawArg: toolArgs,
49733
+ stdinIsTTY: process.stdin.isTTY ?? false,
49734
+ readFile: (p) => fs7.readFileSync(p, "utf-8"),
49735
+ readStdin
49736
+ });
49737
+ if (ingested.kind === "error") {
49738
+ console.error(ingested.message);
49739
+ process.exit(1);
48919
49740
  }
49741
+ const args = ingested.args;
48920
49742
  const timeoutMs = flags.timeout ?? 6e4;
48921
49743
  const result = await client.execute(toolName, args, { format: "text", timeoutMs });
48922
49744
  if (result.error) {