@lotics/cli 0.65.0 → 0.67.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/README.md CHANGED
@@ -181,8 +181,14 @@ lotics app workflow pull # rewrite src/workflows/*.ts + globa
181
181
  lotics app workflow check # typecheck every body locally ([alias] for one)
182
182
  lotics app workflow set issueInvoice # push the edited src/workflows/issueInvoice.ts
183
183
 
184
- # Dev-link @lotics/ui to the monorepo's packages/ui/src for live HMR (monorepo only)
185
- lotics ui link card # edits to packages/ui/src go live
184
+ # Iterate on a named query WITHOUT a deploy: push package.json#lotics.queries.<alias>
185
+ # to apps.queries (server-validated like a deploy). apps.queries is manifest-
186
+ # authoritative, so the next `app deploy` re-syncs it — keep the manifest current.
187
+ lotics app query set openInvoices # push package.json#lotics.queries.openInvoices
188
+
189
+ # Dev-link @lotics/ui to packages/ui/src for live HMR (Vite alias; deploy bundles it)
190
+ lotics ui link card # monorepo: packages/ui/src found automatically
191
+ lotics ui link card --ui-src /abs/monorepo/packages/ui/src # external app (e.g. ~/lotics_apps)
186
192
  lotics ui link card --remove # finalize: PR + publish, then drop the alias
187
193
  ```
188
194
 
@@ -303,6 +303,23 @@ export declare function appExecuteWorkflow(client: LoticsClient, args: {
303
303
  export declare function appWorkflowSet(client: LoticsClient, args: {
304
304
  alias: string;
305
305
  }): Promise<void>;
306
+ /**
307
+ * `lotics app query set <alias>` — push `package.json#lotics.queries.<alias>` to
308
+ * `apps.queries` through `set_app_query`, WITHOUT a deploy. The deploy-free inner
309
+ * loop for named queries, parallel to `lotics app workflow set` for workflows.
310
+ *
311
+ * The declaration (`{ ast, params? }`) is read from the manifest — the same map
312
+ * `useQuery` codegen reads and `lotics app deploy` syncs authoritatively. The
313
+ * server validates it exactly as a deploy does (alias identifier, workspace-only
314
+ * tables, resolvable fields, declared params). Because `apps.queries` is
315
+ * manifest-authoritative, the next `lotics app deploy` overwrites this from the
316
+ * manifest — so keep the manifest as the source of truth; this only skips the
317
+ * build/upload round-trip while iterating. Errors (unbound alias, validation
318
+ * failure) print to stderr and exit non-zero.
319
+ */
320
+ export declare function appQuerySet(client: LoticsClient, args: {
321
+ alias: string;
322
+ }): Promise<void>;
306
323
  /**
307
324
  * `lotics app workflow pull` — rewrite every `src/workflows/<alias>.ts` from the
308
325
  * server without a full `lotics app pull` (no source archive, no npm install).
@@ -346,4 +363,5 @@ export declare function appUiLink(args: {
346
363
  projectDir?: string;
347
364
  component: string;
348
365
  remove?: boolean;
366
+ uiSrc?: string;
349
367
  }): void;
@@ -896,9 +896,13 @@ args) {
896
896
  // `capabilities` block turns every capability OFF on the next deploy
897
897
  // (fail-safe; the declaration is the grant).
898
898
  capabilities: meta.capabilities ?? {},
899
- // Workflow bindings are NOT a deploy concern — set_app_workflow /
900
- // remove_app_workflow own apps.workflows. The manifest's `workflows`
901
- // map is a pulled reflection used only for the .d.ts codegen above.
899
+ // Workflow BINDINGS are NOT a deploy concern — set_app_workflow /
900
+ // remove_app_workflow own apps.workflows. But the alias KEYS of the
901
+ // manifest's `workflows` map ARE sent (never the bindings): they record
902
+ // which aliases this bundle declares, so remove_app_workflow can refuse
903
+ // to unbind an alias the served version still calls. Drop an alias from
904
+ // the manifest + redeploy to lift that guard before removing its binding.
905
+ workflow_aliases: Object.keys(meta.workflows ?? {}),
902
906
  });
903
907
  writeAppMeta(projectDir, {
904
908
  ...meta,
@@ -907,7 +911,16 @@ args) {
907
911
  });
908
912
  console.error(`Deployed v${result.version_number} (${result.version_id})`);
909
913
  console.error(`Bundle size: ${(result.bundle_size_bytes / 1024).toFixed(1)} KB`);
910
- await warnIfUnbranded(client, meta.app_id);
914
+ // One post-deploy getApp feeds both advisory nudges (branding, unbound
915
+ // aliases). Deploy already succeeded — a failed check must not mask that.
916
+ try {
917
+ const app = await client.getApp(meta.app_id);
918
+ warnIfUnbranded(app);
919
+ warnIfUnboundAliases(app, meta.workflows ?? {}, meta.agents ?? {});
920
+ }
921
+ catch (err) {
922
+ console.error(`(skipped post-deploy checks: ${err.message})`);
923
+ }
911
924
  }
912
925
  catch (err) {
913
926
  const e = err;
@@ -931,24 +944,44 @@ args) {
931
944
  * generic tile in the launcher. Branding is set via `update_app` (the single
932
945
  * setter) — this only reminds; it never fails the deploy.
933
946
  */
934
- async function warnIfUnbranded(client, appId) {
935
- try {
936
- const app = await client.getApp(appId);
937
- const missing = [];
938
- if (!app.icon)
939
- missing.push("icon");
940
- if (!app.theme?.color)
941
- missing.push("color");
942
- if (missing.length === 0)
943
- return;
944
- console.error(`\n⚠ This app has no ${missing.join(" or ")} set — it shows a generic tile in the launcher.\n` +
945
- ` Set it: lotics run update_app '{"app_id":"${appId}","icon":"<lucide-name>","theme":{"color":"blue"}}'\n` +
946
- ` Find an icon: lotics run search_app_icons '{"query":"<word>"}'`);
947
- }
948
- catch (err) {
949
- // Deploy already succeeded; a failed branding check must not mask that.
950
- console.error(`(skipped branding check: ${err.message})`);
951
- }
947
+ function warnIfUnbranded(app) {
948
+ const missing = [];
949
+ if (!app.icon)
950
+ missing.push("icon");
951
+ if (!app.theme?.color)
952
+ missing.push("color");
953
+ if (missing.length === 0)
954
+ return;
955
+ console.error(`\n⚠ This app has no ${missing.join(" or ")} set — it shows a generic tile in the launcher.\n` +
956
+ ` Set it: lotics run update_app '{"app_id":"${app.id}","icon":"<lucide-name>","theme":{"color":"blue"}}'\n` +
957
+ ` Find an icon: lotics run search_app_icons '{"query":"<word>"}'`);
958
+ }
959
+ /**
960
+ * Non-blocking nudge after a successful deploy: catch manifest workflow/agent aliases
961
+ * that are NOT bound on the server. Deploy ships code + queries only — it never binds
962
+ * workflows/agents (`set_app_workflow` / `set_app_agent` own `apps.workflows`/`apps.agents`;
963
+ * the manifest maps are a pulled reflection). So an alias hand-added to `lotics.workflows`
964
+ * / `lotics.agents` (or bound then removed) compiles + deploys clean and only throws when
965
+ * the app first calls `useWorkflow` / `useAgentRun`. Surface that divergence HERE — at
966
+ * deploy time — instead of at the user's first click. Advisory only; never fails the deploy.
967
+ */
968
+ function warnIfUnboundAliases(app, declaredWorkflows, declaredAgents) {
969
+ const boundWorkflows = new Set(Object.keys(app.workflows ?? {}));
970
+ const boundAgents = new Set(Object.keys(app.agents ?? {}));
971
+ const unboundWorkflows = Object.keys(declaredWorkflows).filter((a) => !boundWorkflows.has(a));
972
+ const unboundAgents = Object.keys(declaredAgents).filter((a) => !boundAgents.has(a));
973
+ if (unboundWorkflows.length === 0 && unboundAgents.length === 0)
974
+ return;
975
+ const lines = [
976
+ "\n⚠ The manifest declares aliases that are NOT bound on the server. Deploy ships code +",
977
+ " queries only — it does NOT bind workflows/agents, so the app will throw \"has no … alias\"",
978
+ " the first time it calls them. Bind each one:",
979
+ ];
980
+ for (const alias of unboundWorkflows)
981
+ lines.push(` • workflow "${alias}" → lotics app workflow set ${alias}`);
982
+ for (const alias of unboundAgents)
983
+ lines.push(` • agent "${alias}" → bind with set_app_agent (lotics run set_app_agent …)`);
984
+ console.error(lines.join("\n"));
952
985
  }
953
986
  /**
954
987
  * `lotics app dev [path] [--port=5174] [--vite-port=5173]`
@@ -1198,6 +1231,37 @@ export async function appWorkflowSet(client, args) {
1198
1231
  console.error(` result.data schema: ${JSON.stringify(result.outputs)}`);
1199
1232
  }
1200
1233
  }
1234
+ /**
1235
+ * `lotics app query set <alias>` — push `package.json#lotics.queries.<alias>` to
1236
+ * `apps.queries` through `set_app_query`, WITHOUT a deploy. The deploy-free inner
1237
+ * loop for named queries, parallel to `lotics app workflow set` for workflows.
1238
+ *
1239
+ * The declaration (`{ ast, params? }`) is read from the manifest — the same map
1240
+ * `useQuery` codegen reads and `lotics app deploy` syncs authoritatively. The
1241
+ * server validates it exactly as a deploy does (alias identifier, workspace-only
1242
+ * tables, resolvable fields, declared params). Because `apps.queries` is
1243
+ * manifest-authoritative, the next `lotics app deploy` overwrites this from the
1244
+ * manifest — so keep the manifest as the source of truth; this only skips the
1245
+ * build/upload round-trip while iterating. Errors (unbound alias, validation
1246
+ * failure) print to stderr and exit non-zero.
1247
+ */
1248
+ export async function appQuerySet(client, args) {
1249
+ const projectDir = process.cwd();
1250
+ const meta = readAppMeta(projectDir);
1251
+ const declaration = meta.queries?.[args.alias];
1252
+ if (!declaration) {
1253
+ console.error(`No query "${args.alias}" in package.json#lotics.queries. ` +
1254
+ `Declare it there (alias → { ast, params? }) first.`);
1255
+ process.exit(1);
1256
+ }
1257
+ const res = await client.setAppQuery(meta.app_id, args.alias, declaration);
1258
+ if (res.error) {
1259
+ console.error(`Failed to set query "${args.alias}": ${res.error}`);
1260
+ process.exit(1);
1261
+ }
1262
+ console.error(`Set query "${args.alias}" on ${meta.app_id}. ` +
1263
+ `(apps.queries is manifest-authoritative — the next 'lotics app deploy' re-syncs it.)`);
1264
+ }
1201
1265
  /**
1202
1266
  * `lotics app workflow pull` — rewrite every `src/workflows/<alias>.ts` from the
1203
1267
  * server without a full `lotics app pull` (no source archive, no npm install).
@@ -1358,11 +1422,18 @@ export function appUiLink(args) {
1358
1422
  if (!fs.existsSync(viteConfigPath)) {
1359
1423
  throw new Error(`No vite.config.ts at ${projectDir}. Run inside a 'lotics app' project directory.`);
1360
1424
  }
1361
- const uiSrc = findUiSrcDir(projectDir);
1362
- if (!uiSrc) {
1363
- throw new Error("Cannot find packages/ui/src by walking up from this directory — `lotics ui link` " +
1364
- "requires a monorepo checkout. External apps consume @lotics/ui from npm; bump the " +
1365
- "package version and widen the app's dependency range instead.");
1425
+ // Resolve packages/ui/src. An explicit --ui-src / LOTICS_UI_SRC wins — that's how
1426
+ // an EXTERNAL app (one that consumes @lotics/ui from npm, with no monorepo above
1427
+ // it) links the local kit; otherwise walk up for a monorepo checkout.
1428
+ const explicit = args.uiSrc ?? process.env.LOTICS_UI_SRC;
1429
+ const uiSrc = explicit ? path.resolve(explicit) : findUiSrcDir(projectDir);
1430
+ if (!uiSrc || !fs.existsSync(uiSrc) || !fs.statSync(uiSrc).isDirectory()) {
1431
+ throw new Error(explicit
1432
+ ? `--ui-src / LOTICS_UI_SRC points at '${explicit}', which is not a directory. ` +
1433
+ `Pass the absolute path to the monorepo's packages/ui/src.`
1434
+ : "Cannot find packages/ui/src by walking up from this directory. For an EXTERNAL app " +
1435
+ "(consuming @lotics/ui from npm), pass --ui-src=<abs path to packages/ui/src> or set " +
1436
+ "LOTICS_UI_SRC; inside a monorepo checkout it is found automatically.");
1366
1437
  }
1367
1438
  // Validate the named component exists in src so a typo fails loud (the alias
1368
1439
  // itself stays package-wide — this is the advisory check the spec calls for).
@@ -1403,6 +1474,10 @@ export function appUiLink(args) {
1403
1474
  fs.writeFileSync(viteConfigPath, updated);
1404
1475
  console.error(`Dev-linked @lotics/ui → ${uiSrc} in ${viteConfigPath}.`);
1405
1476
  console.error("Restart `lotics app dev` and rm -rf node_modules/.vite to clear cached modules.");
1477
+ // The app's tsc still resolves @lotics/ui from node_modules (the published .d.ts) —
1478
+ // the kit `src` can't be typechecked in an app because it's RN-Web (uses
1479
+ // react-native-web types the app resolves as base react-native). Typecheck the kit
1480
+ // in packages/ui; the finalize publish restores the app's own typecheck.
1406
1481
  console.error("Finalize: PR the packages/ui change → publish → `lotics ui link <component> --remove` + bump the app's dep.");
1407
1482
  }
1408
1483
  /** Escape a string for literal use inside a RegExp. */
@@ -208,8 +208,9 @@ describe("appCodegen (.d.ts-only path)", () => {
208
208
  });
209
209
  /**
210
210
  * `appUiLink` edits the app's vite.config.ts resolve.alias to dev-link
211
- * @lotics/ui at the monorepo's packages/ui/src. It fails loud outside a
212
- * monorepo, validates the component exists, and insert/remove is idempotent.
211
+ * @lotics/ui at packages/ui/src auto-found in a monorepo checkout, or given
212
+ * explicitly via --ui-src for an external app. It validates the src + component,
213
+ * fails loud when neither resolves, and insert/remove is idempotent.
213
214
  */
214
215
  describe("appUiLink", () => {
215
216
  let root;
@@ -264,16 +265,30 @@ describe("appUiLink", () => {
264
265
  it("fails loud when the named component does not exist in packages/ui/src", () => {
265
266
  expect(() => appUiLink({ projectDir: appDir, component: "nonexistent" })).toThrow(/nonexistent/);
266
267
  });
267
- it("fails loud when there is no monorepo packages/ui/src above the project", () => {
268
+ it("fails loud when there is no packages/ui/src and no --ui-src override", () => {
268
269
  const lonely = fs.mkdtempSync(path.join(tmpdir(), "lotics-lonely-app-"));
269
270
  fs.writeFileSync(path.join(lonely, "vite.config.ts"), "export default { resolve: { alias: [] } };");
270
271
  try {
271
- expect(() => appUiLink({ projectDir: lonely, component: "card" })).toThrow(/monorepo checkout/);
272
+ expect(() => appUiLink({ projectDir: lonely, component: "card" })).toThrow(/--ui-src/);
272
273
  }
273
274
  finally {
274
275
  fs.rmSync(lonely, { recursive: true, force: true });
275
276
  }
276
277
  });
278
+ it("links an EXTERNAL app (no monorepo above) via an explicit --ui-src", () => {
279
+ const ext = fs.mkdtempSync(path.join(tmpdir(), "lotics-external-app-"));
280
+ fs.writeFileSync(path.join(ext, "vite.config.ts"), "export default { resolve: { alias: [] } };");
281
+ try {
282
+ appUiLink({ projectDir: ext, component: "card", uiSrc });
283
+ expect(fs.readFileSync(path.join(ext, "vite.config.ts"), "utf-8")).toContain(uiSrc);
284
+ }
285
+ finally {
286
+ fs.rmSync(ext, { recursive: true, force: true });
287
+ }
288
+ });
289
+ it("fails loud when --ui-src is not a directory", () => {
290
+ expect(() => appUiLink({ projectDir: appDir, component: "card", uiSrc: path.join(root, "nope") })).toThrow(/not a directory/);
291
+ });
277
292
  });
278
293
  /**
279
294
  * The workflow body files are how AA-1 Option B becomes "open a file → edit →
package/dist/args.d.ts CHANGED
@@ -23,6 +23,8 @@ export declare function parseArgs(argv: string[]): {
23
23
  apiKey?: string;
24
24
  workspace?: string;
25
25
  viewAs?: string;
26
+ /** `--ui-src <path>`: absolute packages/ui/src for `ui link` in an external app. */
27
+ uiSrc?: string;
26
28
  name?: string;
27
29
  timezone?: string;
28
30
  message?: string;
package/dist/args.js CHANGED
@@ -19,6 +19,7 @@ export function parseArgs(argv) {
19
19
  apiKey: undefined,
20
20
  workspace: undefined,
21
21
  viewAs: undefined,
22
+ uiSrc: undefined,
22
23
  name: undefined,
23
24
  timezone: undefined,
24
25
  message: undefined,
@@ -60,6 +61,9 @@ export function parseArgs(argv) {
60
61
  case "--view-as":
61
62
  flags.viewAs = argv[++i];
62
63
  break;
64
+ case "--ui-src":
65
+ flags.uiSrc = argv[++i];
66
+ break;
63
67
  case "--name":
64
68
  flags.name = argv[++i];
65
69
  break;
package/dist/cli.js CHANGED
@@ -13,7 +13,7 @@ net.setDefaultAutoSelectFamilyAttemptTimeout(2000);
13
13
  import { LoticsClient, API_BASE_URL } from "./client.js";
14
14
  import { resolveContext, deleteConfig, getConfigPath, loadGlobalConfig, saveGlobalConfig, loadLocalConfig, upsertProfile, removeProfile, setActiveOrg, setSelectedWorkspace, resolveProfileByNameOrId, checkForUpdate, } from "./config.js";
15
15
  import { VERSION } from "./version.js";
16
- import { appCreate, appPull, appDeploy, appDev, appSetSubdomain, appRename, appVersions, appCodegen, appExecuteWorkflow, appWorkflowSet, appWorkflowPull, appWorkflowCheck, appUiLink, } from "./app_commands.js";
16
+ import { appCreate, appPull, appDeploy, appDev, appSetSubdomain, appRename, appVersions, appCodegen, appExecuteWorkflow, appWorkflowSet, appWorkflowPull, appWorkflowCheck, appQuerySet, appUiLink, } from "./app_commands.js";
17
17
  import { parseArgs } from "./args.js";
18
18
  import { ingestJsonArgs } from "./inputs.js";
19
19
  import { runXlsxCommand } from "./xlsx.js";
@@ -90,11 +90,16 @@ COMMANDS
90
90
  lotics app workflow pull Rewrite src/workflows/*.ts from the server
91
91
  lotics app workflow check [alias] Typecheck src/workflows bodies locally (one
92
92
  isolated program per alias; the app's own tsc)
93
+ lotics app query set <alias> Push package.json#lotics.queries.<alias> to
94
+ apps.queries via set_app_query (no deploy;
95
+ re-synced by the next deploy from the manifest)
93
96
  lotics app subdomain <new-subdomain> Rename the app's public <slug>.lotics.app address
94
97
  lotics app rename "<new name>" Rename the app's display name (launcher title)
95
98
  lotics app dev [path] Run the app locally with HMR (RPC forwarded to prod)
96
- lotics ui link <component> [--remove] Dev-link @lotics/ui to the monorepo's
97
- packages/ui/src for live HMR (monorepo only)
99
+ lotics ui link <component> [--ui-src <path>] [--remove]
100
+ Dev-link @lotics/ui to packages/ui/src (Vite alias
101
+ + tsc paths) for live HMR + typecheck. Monorepo apps
102
+ auto-find it; external apps pass --ui-src / LOTICS_UI_SRC
98
103
  lotics xlsx <subcommand> ... Read/write/edit .xlsx files on your local filesystem
99
104
  (uses the bundled Lotics xlsx engine; prefer over
100
105
  npm xlsx/exceljs for round-trip fidelity)
@@ -493,17 +498,19 @@ async function main() {
493
498
  if (subcommand === "link") {
494
499
  const component = toolArgs;
495
500
  if (!component) {
496
- console.error("Usage: lotics ui link <component> [--remove]");
497
- console.error("Dev-links @lotics/ui to the monorepo's packages/ui/src for live HMR.");
501
+ console.error("Usage: lotics ui link <component> [--ui-src <abs path>] [--remove]");
502
+ console.error("Dev-links @lotics/ui to packages/ui/src (Vite + tsc) for live HMR + typecheck.");
503
+ console.error("Monorepo apps find packages/ui/src automatically; external apps pass --ui-src / LOTICS_UI_SRC.");
498
504
  process.exit(1);
499
505
  }
500
506
  // `--remove` isn't a value-taking flag, so the parser leaves it as a
501
- // trailing positional (the component took `toolArgs`).
502
- appUiLink({ component, remove: restArgs.includes("--remove") });
507
+ // trailing positional (the component took `toolArgs`); `--ui-src` IS a
508
+ // value flag (flags.uiSrc), falling back to LOTICS_UI_SRC inside appUiLink.
509
+ appUiLink({ component, uiSrc: flags.uiSrc, remove: restArgs.includes("--remove") });
503
510
  return;
504
511
  }
505
512
  console.error(`Unknown ui subcommand: ${subcommand ?? "(none)"}`);
506
- console.error("Usage: lotics ui link <component> [--remove]");
513
+ console.error("Usage: lotics ui link <component> [--ui-src <abs path>] [--remove]");
507
514
  process.exit(1);
508
515
  }
509
516
  // --- lotics app workflow check [alias] — local typecheck, no auth, no network ---
@@ -606,6 +613,7 @@ async function main() {
606
613
  console.error(" lotics app workflow set <alias> Push the edited src/workflows/<alias>.ts body");
607
614
  console.error(" lotics app workflow pull Rewrite src/workflows/*.ts from the server");
608
615
  console.error(" lotics app workflow check [alias] Typecheck src/workflows bodies locally");
616
+ console.error(" lotics app query set <alias> Push lotics.queries.<alias> to apps.queries (no deploy)");
609
617
  console.error(" lotics app subdomain <new-subdomain> Rename the app's public <slug>.lotics.app address");
610
618
  console.error(" lotics app rename \"<new name>\" Rename the app's display name (launcher title)");
611
619
  console.error(" lotics app dev [path] Run the app locally with HMR + RPC forwarding");
@@ -826,6 +834,25 @@ async function main() {
826
834
  }
827
835
  workflowUsage();
828
836
  }
837
+ if (subcommand === "query") {
838
+ // `lotics app query set <alias>` — push package.json#lotics.queries.<alias>
839
+ // to apps.queries via set_app_query, no deploy. `toolArgs` is the verb;
840
+ // `restArgs` carries the alias.
841
+ const action = toolArgs;
842
+ if (action === "set") {
843
+ const alias = restArgs[0];
844
+ if (!alias) {
845
+ console.error("Usage: lotics app query set <alias>");
846
+ console.error("Pushes package.json#lotics.queries.<alias> to apps.queries via set_app_query (no deploy).");
847
+ process.exit(1);
848
+ }
849
+ await appQuerySet(client, { alias });
850
+ return;
851
+ }
852
+ console.error("Usage:");
853
+ console.error(" lotics app query set <alias> Push package.json#lotics.queries.<alias> to apps.queries (no deploy)");
854
+ process.exit(1);
855
+ }
829
856
  if (subcommand === "dev") {
830
857
  // First positional is an optional project path (defaults to cwd).
831
858
  // --port and --vite-port can override the wrapper / Vite ports.
package/dist/client.d.ts CHANGED
@@ -277,6 +277,18 @@ export declare class LoticsClient {
277
277
  name?: string;
278
278
  description?: string;
279
279
  }): Promise<ToolExecuteResult>;
280
+ /**
281
+ * Bind (create or replace) an app query by alias via the `set_app_query` tool
282
+ * — the deploy-free authoring path for `apps.queries`, parallel to
283
+ * `setAppWorkflow`. `declaration` is the `{ ast, params? }` from
284
+ * `package.json#lotics.queries.<alias>`. The server validates it exactly as a
285
+ * deploy validates the manifest. Note: `apps.queries` is manifest-authoritative,
286
+ * so the next `lotics app deploy` overwrites this from the manifest.
287
+ */
288
+ setAppQuery(app_id: string, alias: string, declaration: {
289
+ ast: unknown;
290
+ params?: Record<string, unknown>;
291
+ }): Promise<ToolExecuteResult>;
280
292
  /**
281
293
  * Fetch one app workflow's faithful source + bound input/output schemas via
282
294
  * `get_app_workflow`. `source` is the JS-subset body re-rendered from the
@@ -375,6 +387,14 @@ export declare class LoticsClient {
375
387
  capabilities?: {
376
388
  comments?: boolean;
377
389
  };
390
+ /**
391
+ * The manifest's `lotics.workflows` KEYS — the workflow aliases the deployed
392
+ * code declares (NOT the bindings; `set_app_workflow` / `remove_app_workflow`
393
+ * own `apps.workflows`). Recorded on the new version so `remove_app_workflow`
394
+ * refuses to unbind an alias the served version still calls. Always sent
395
+ * (empty array when none declared).
396
+ */
397
+ workflow_aliases?: string[];
378
398
  }): Promise<{
379
399
  version_id: string;
380
400
  version_number: number;
package/dist/client.js CHANGED
@@ -333,6 +333,17 @@ export class LoticsClient {
333
333
  ...(body.description ? { description: body.description } : {}),
334
334
  });
335
335
  }
336
+ /**
337
+ * Bind (create or replace) an app query by alias via the `set_app_query` tool
338
+ * — the deploy-free authoring path for `apps.queries`, parallel to
339
+ * `setAppWorkflow`. `declaration` is the `{ ast, params? }` from
340
+ * `package.json#lotics.queries.<alias>`. The server validates it exactly as a
341
+ * deploy validates the manifest. Note: `apps.queries` is manifest-authoritative,
342
+ * so the next `lotics app deploy` overwrites this from the manifest.
343
+ */
344
+ async setAppQuery(app_id, alias, declaration) {
345
+ return this.execute("set_app_query", { app_id, alias, declaration });
346
+ }
336
347
  /**
337
348
  * Fetch one app workflow's faithful source + bound input/output schemas via
338
349
  * `get_app_workflow`. `source` is the JS-subset body re-rendered from the
@@ -427,6 +438,10 @@ export class LoticsClient {
427
438
  if (args.capabilities !== undefined) {
428
439
  formData.append("capabilities", JSON.stringify(args.capabilities));
429
440
  }
441
+ // Always send the declared workflow aliases (empty array when none) so the
442
+ // server records what the served version calls — the remove_app_workflow
443
+ // guard reads this back. These are the manifest KEYS only, never bindings.
444
+ formData.append("workflow_aliases", JSON.stringify(args.workflow_aliases ?? []));
430
445
  const url = `${this.baseUrl}/v1/apps/${encodeURIComponent(args.app_id)}/versions`;
431
446
  const response = await fetch(url, {
432
447
  method: "POST",
package/dist/src/cli.js CHANGED
@@ -29881,6 +29881,17 @@ var LoticsClient = class {
29881
29881
  ...body.description ? { description: body.description } : {}
29882
29882
  });
29883
29883
  }
29884
+ /**
29885
+ * Bind (create or replace) an app query by alias via the `set_app_query` tool
29886
+ * — the deploy-free authoring path for `apps.queries`, parallel to
29887
+ * `setAppWorkflow`. `declaration` is the `{ ast, params? }` from
29888
+ * `package.json#lotics.queries.<alias>`. The server validates it exactly as a
29889
+ * deploy validates the manifest. Note: `apps.queries` is manifest-authoritative,
29890
+ * so the next `lotics app deploy` overwrites this from the manifest.
29891
+ */
29892
+ async setAppQuery(app_id, alias, declaration) {
29893
+ return this.execute("set_app_query", { app_id, alias, declaration });
29894
+ }
29884
29895
  /**
29885
29896
  * Fetch one app workflow's faithful source + bound input/output schemas via
29886
29897
  * `get_app_workflow`. `source` is the JS-subset body re-rendered from the
@@ -30004,6 +30015,7 @@ var LoticsClient = class {
30004
30015
  if (args.capabilities !== void 0) {
30005
30016
  formData.append("capabilities", JSON.stringify(args.capabilities));
30006
30017
  }
30018
+ formData.append("workflow_aliases", JSON.stringify(args.workflow_aliases ?? []));
30007
30019
  const url = `${this.baseUrl}/v1/apps/${encodeURIComponent(args.app_id)}/versions`;
30008
30020
  const response = await fetch(url, {
30009
30021
  method: "POST",
@@ -32730,10 +32742,14 @@ async function appDeploy(client, args) {
32730
32742
  // defaulting to `{}` when the manifest declares none — so deleting the
32731
32743
  // `capabilities` block turns every capability OFF on the next deploy
32732
32744
  // (fail-safe; the declaration is the grant).
32733
- capabilities: meta.capabilities ?? {}
32734
- // Workflow bindings are NOT a deploy concern — set_app_workflow /
32735
- // remove_app_workflow own apps.workflows. The manifest's `workflows`
32736
- // map is a pulled reflection used only for the .d.ts codegen above.
32745
+ capabilities: meta.capabilities ?? {},
32746
+ // Workflow BINDINGS are NOT a deploy concern — set_app_workflow /
32747
+ // remove_app_workflow own apps.workflows. But the alias KEYS of the
32748
+ // manifest's `workflows` map ARE sent (never the bindings): they record
32749
+ // which aliases this bundle declares, so remove_app_workflow can refuse
32750
+ // to unbind an alias the served version still calls. Drop an alias from
32751
+ // the manifest + redeploy to lift that guard before removing its binding.
32752
+ workflow_aliases: Object.keys(meta.workflows ?? {})
32737
32753
  });
32738
32754
  writeAppMeta(projectDir, {
32739
32755
  ...meta,
@@ -32742,7 +32758,13 @@ async function appDeploy(client, args) {
32742
32758
  });
32743
32759
  console.error(`Deployed v${result.version_number} (${result.version_id})`);
32744
32760
  console.error(`Bundle size: ${(result.bundle_size_bytes / 1024).toFixed(1)} KB`);
32745
- await warnIfUnbranded(client, meta.app_id);
32761
+ try {
32762
+ const app = await client.getApp(meta.app_id);
32763
+ warnIfUnbranded(app);
32764
+ warnIfUnboundAliases(app, meta.workflows ?? {}, meta.agents ?? {});
32765
+ } catch (err2) {
32766
+ console.error(`(skipped post-deploy checks: ${err2.message})`);
32767
+ }
32746
32768
  } catch (err2) {
32747
32769
  const e = err2;
32748
32770
  if (e.code === "VERSION_CONFLICT") {
@@ -32758,22 +32780,32 @@ async function appDeploy(client, args) {
32758
32780
  if (fs4.existsSync(tmpDist)) fs4.unlinkSync(tmpDist);
32759
32781
  }
32760
32782
  }
32761
- async function warnIfUnbranded(client, appId) {
32762
- try {
32763
- const app = await client.getApp(appId);
32764
- const missing = [];
32765
- if (!app.icon) missing.push("icon");
32766
- if (!app.theme?.color) missing.push("color");
32767
- if (missing.length === 0) return;
32768
- console.error(
32769
- `
32783
+ function warnIfUnbranded(app) {
32784
+ const missing = [];
32785
+ if (!app.icon) missing.push("icon");
32786
+ if (!app.theme?.color) missing.push("color");
32787
+ if (missing.length === 0) return;
32788
+ console.error(
32789
+ `
32770
32790
  \u26A0 This app has no ${missing.join(" or ")} set \u2014 it shows a generic tile in the launcher.
32771
- Set it: lotics run update_app '{"app_id":"${appId}","icon":"<lucide-name>","theme":{"color":"blue"}}'
32791
+ Set it: lotics run update_app '{"app_id":"${app.id}","icon":"<lucide-name>","theme":{"color":"blue"}}'
32772
32792
  Find an icon: lotics run search_app_icons '{"query":"<word>"}'`
32773
- );
32774
- } catch (err2) {
32775
- console.error(`(skipped branding check: ${err2.message})`);
32776
- }
32793
+ );
32794
+ }
32795
+ function warnIfUnboundAliases(app, declaredWorkflows, declaredAgents) {
32796
+ const boundWorkflows = new Set(Object.keys(app.workflows ?? {}));
32797
+ const boundAgents = new Set(Object.keys(app.agents ?? {}));
32798
+ const unboundWorkflows = Object.keys(declaredWorkflows).filter((a) => !boundWorkflows.has(a));
32799
+ const unboundAgents = Object.keys(declaredAgents).filter((a) => !boundAgents.has(a));
32800
+ if (unboundWorkflows.length === 0 && unboundAgents.length === 0) return;
32801
+ const lines = [
32802
+ "\n\u26A0 The manifest declares aliases that are NOT bound on the server. Deploy ships code +",
32803
+ ' queries only \u2014 it does NOT bind workflows/agents, so the app will throw "has no \u2026 alias"',
32804
+ " the first time it calls them. Bind each one:"
32805
+ ];
32806
+ for (const alias of unboundWorkflows) lines.push(` \u2022 workflow "${alias}" \u2192 lotics app workflow set ${alias}`);
32807
+ for (const alias of unboundAgents) lines.push(` \u2022 agent "${alias}" \u2192 bind with set_app_agent (lotics run set_app_agent \u2026)`);
32808
+ console.error(lines.join("\n"));
32777
32809
  }
32778
32810
  async function appDev(client, args) {
32779
32811
  const projectDir = path5.resolve(args.projectDir ?? process.cwd());
@@ -32942,6 +32974,25 @@ async function appWorkflowSet(client, args) {
32942
32974
  console.error(` result.data schema: ${JSON.stringify(result.outputs)}`);
32943
32975
  }
32944
32976
  }
32977
+ async function appQuerySet(client, args) {
32978
+ const projectDir = process.cwd();
32979
+ const meta = readAppMeta(projectDir);
32980
+ const declaration = meta.queries?.[args.alias];
32981
+ if (!declaration) {
32982
+ console.error(
32983
+ `No query "${args.alias}" in package.json#lotics.queries. Declare it there (alias \u2192 { ast, params? }) first.`
32984
+ );
32985
+ process.exit(1);
32986
+ }
32987
+ const res = await client.setAppQuery(meta.app_id, args.alias, declaration);
32988
+ if (res.error) {
32989
+ console.error(`Failed to set query "${args.alias}": ${res.error}`);
32990
+ process.exit(1);
32991
+ }
32992
+ console.error(
32993
+ `Set query "${args.alias}" on ${meta.app_id}. (apps.queries is manifest-authoritative \u2014 the next 'lotics app deploy' re-syncs it.)`
32994
+ );
32995
+ }
32945
32996
  async function appWorkflowPull(client) {
32946
32997
  const projectDir = process.cwd();
32947
32998
  const meta = readAppMeta(projectDir);
@@ -33050,10 +33101,11 @@ function appUiLink(args) {
33050
33101
  `No vite.config.ts at ${projectDir}. Run inside a 'lotics app' project directory.`
33051
33102
  );
33052
33103
  }
33053
- const uiSrc = findUiSrcDir(projectDir);
33054
- if (!uiSrc) {
33104
+ const explicit = args.uiSrc ?? process.env.LOTICS_UI_SRC;
33105
+ const uiSrc = explicit ? path5.resolve(explicit) : findUiSrcDir(projectDir);
33106
+ if (!uiSrc || !fs4.existsSync(uiSrc) || !fs4.statSync(uiSrc).isDirectory()) {
33055
33107
  throw new Error(
33056
- "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."
33108
+ explicit ? `--ui-src / LOTICS_UI_SRC points at '${explicit}', which is not a directory. Pass the absolute path to the monorepo's packages/ui/src.` : "Cannot find packages/ui/src by walking up from this directory. For an EXTERNAL app (consuming @lotics/ui from npm), pass --ui-src=<abs path to packages/ui/src> or set LOTICS_UI_SRC; inside a monorepo checkout it is found automatically."
33057
33109
  );
33058
33110
  }
33059
33111
  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));
@@ -33108,6 +33160,7 @@ function parseArgs(argv) {
33108
33160
  apiKey: void 0,
33109
33161
  workspace: void 0,
33110
33162
  viewAs: void 0,
33163
+ uiSrc: void 0,
33111
33164
  name: void 0,
33112
33165
  timezone: void 0,
33113
33166
  message: void 0,
@@ -33149,6 +33202,9 @@ function parseArgs(argv) {
33149
33202
  case "--view-as":
33150
33203
  flags.viewAs = argv[++i2];
33151
33204
  break;
33205
+ case "--ui-src":
33206
+ flags.uiSrc = argv[++i2];
33207
+ break;
33152
33208
  case "--name":
33153
33209
  flags.name = argv[++i2];
33154
33210
  break;
@@ -42676,6 +42732,7 @@ function readCfbStreams(bytes) {
42676
42732
  throw new Error("Unsupported .xls: invalid OLE2 sector size");
42677
42733
  }
42678
42734
  const sectorOffset = (n) => sectorSize * (n + 1);
42735
+ const maxSectors = Math.ceil(bytes.length / sectorSize) + 1;
42679
42736
  const difat = [];
42680
42737
  for (let i2 = 0; i2 < 109; i2++) {
42681
42738
  const v = u32(76 + i2 * 4);
@@ -42683,7 +42740,9 @@ function readCfbStreams(bytes) {
42683
42740
  }
42684
42741
  let ds = firstDifatSector;
42685
42742
  const entriesPerSector = sectorSize / 4;
42686
- for (let guard = 0; ds !== ENDOFCHAIN && ds !== FREESECT && guard < 1 << 20; guard++) {
42743
+ const seenDifat = /* @__PURE__ */ new Set();
42744
+ while (ds !== ENDOFCHAIN && ds !== FREESECT && ds < maxSectors && !seenDifat.has(ds)) {
42745
+ seenDifat.add(ds);
42687
42746
  const base = sectorOffset(ds);
42688
42747
  for (let i2 = 0; i2 < entriesPerSector - 1; i2++) {
42689
42748
  const v = u32(base + i2 * 4);
@@ -42693,14 +42752,18 @@ function readCfbStreams(bytes) {
42693
42752
  }
42694
42753
  const fat = [];
42695
42754
  for (const fatSector of difat) {
42755
+ if (fat.length >= maxSectors) break;
42756
+ if (fatSector >= maxSectors) continue;
42696
42757
  const base = sectorOffset(fatSector);
42697
- for (let i2 = 0; i2 < entriesPerSector; i2++) fat.push(u32(base + i2 * 4));
42758
+ for (let i2 = 0; i2 < entriesPerSector && fat.length < maxSectors; i2++) {
42759
+ fat.push(u32(base + i2 * 4));
42760
+ }
42698
42761
  }
42699
42762
  const readChain = (src, start, size, ss, offOf, chain) => {
42700
- const maxSectors = Math.floor(src.length / ss) + 2;
42763
+ const maxSectors2 = Math.floor(src.length / ss) + 2;
42701
42764
  const parts = [];
42702
42765
  let s = start;
42703
- for (let guard = 0; s !== ENDOFCHAIN && s !== FREESECT && s < chain.length && guard < maxSectors; guard++) {
42766
+ for (let guard = 0; s !== ENDOFCHAIN && s !== FREESECT && s < chain.length && guard < maxSectors2; guard++) {
42704
42767
  const o = offOf(s);
42705
42768
  parts.push(src.subarray(o, o + ss));
42706
42769
  s = chain[s];
@@ -49650,11 +49713,16 @@ COMMANDS
49650
49713
  lotics app workflow pull Rewrite src/workflows/*.ts from the server
49651
49714
  lotics app workflow check [alias] Typecheck src/workflows bodies locally (one
49652
49715
  isolated program per alias; the app's own tsc)
49716
+ lotics app query set <alias> Push package.json#lotics.queries.<alias> to
49717
+ apps.queries via set_app_query (no deploy;
49718
+ re-synced by the next deploy from the manifest)
49653
49719
  lotics app subdomain <new-subdomain> Rename the app's public <slug>.lotics.app address
49654
49720
  lotics app rename "<new name>" Rename the app's display name (launcher title)
49655
49721
  lotics app dev [path] Run the app locally with HMR (RPC forwarded to prod)
49656
- lotics ui link <component> [--remove] Dev-link @lotics/ui to the monorepo's
49657
- packages/ui/src for live HMR (monorepo only)
49722
+ lotics ui link <component> [--ui-src <path>] [--remove]
49723
+ Dev-link @lotics/ui to packages/ui/src (Vite alias
49724
+ + tsc paths) for live HMR + typecheck. Monorepo apps
49725
+ auto-find it; external apps pass --ui-src / LOTICS_UI_SRC
49658
49726
  lotics xlsx <subcommand> ... Read/write/edit .xlsx files on your local filesystem
49659
49727
  (uses the bundled Lotics xlsx engine; prefer over
49660
49728
  npm xlsx/exceljs for round-trip fidelity)
@@ -50018,15 +50086,16 @@ async function main() {
50018
50086
  if (subcommand === "link") {
50019
50087
  const component = toolArgs;
50020
50088
  if (!component) {
50021
- console.error("Usage: lotics ui link <component> [--remove]");
50022
- console.error("Dev-links @lotics/ui to the monorepo's packages/ui/src for live HMR.");
50089
+ console.error("Usage: lotics ui link <component> [--ui-src <abs path>] [--remove]");
50090
+ console.error("Dev-links @lotics/ui to packages/ui/src (Vite + tsc) for live HMR + typecheck.");
50091
+ console.error("Monorepo apps find packages/ui/src automatically; external apps pass --ui-src / LOTICS_UI_SRC.");
50023
50092
  process.exit(1);
50024
50093
  }
50025
- appUiLink({ component, remove: restArgs.includes("--remove") });
50094
+ appUiLink({ component, uiSrc: flags.uiSrc, remove: restArgs.includes("--remove") });
50026
50095
  return;
50027
50096
  }
50028
50097
  console.error(`Unknown ui subcommand: ${subcommand ?? "(none)"}`);
50029
- console.error("Usage: lotics ui link <component> [--remove]");
50098
+ console.error("Usage: lotics ui link <component> [--ui-src <abs path>] [--remove]");
50030
50099
  process.exit(1);
50031
50100
  }
50032
50101
  if (command === "app" && subcommand === "workflow" && toolArgs === "check") {
@@ -50119,6 +50188,7 @@ async function main() {
50119
50188
  console.error(" lotics app workflow set <alias> Push the edited src/workflows/<alias>.ts body");
50120
50189
  console.error(" lotics app workflow pull Rewrite src/workflows/*.ts from the server");
50121
50190
  console.error(" lotics app workflow check [alias] Typecheck src/workflows bodies locally");
50191
+ console.error(" lotics app query set <alias> Push lotics.queries.<alias> to apps.queries (no deploy)");
50122
50192
  console.error(" lotics app subdomain <new-subdomain> Rename the app's public <slug>.lotics.app address");
50123
50193
  console.error(` lotics app rename "<new name>" Rename the app's display name (launcher title)`);
50124
50194
  console.error(" lotics app dev [path] Run the app locally with HMR + RPC forwarding");
@@ -50324,6 +50394,26 @@ Available workspaces:`);
50324
50394
  }
50325
50395
  workflowUsage();
50326
50396
  }
50397
+ if (subcommand === "query") {
50398
+ const action = toolArgs;
50399
+ if (action === "set") {
50400
+ const alias = restArgs[0];
50401
+ if (!alias) {
50402
+ console.error("Usage: lotics app query set <alias>");
50403
+ console.error(
50404
+ "Pushes package.json#lotics.queries.<alias> to apps.queries via set_app_query (no deploy)."
50405
+ );
50406
+ process.exit(1);
50407
+ }
50408
+ await appQuerySet(client, { alias });
50409
+ return;
50410
+ }
50411
+ console.error("Usage:");
50412
+ console.error(
50413
+ " lotics app query set <alias> Push package.json#lotics.queries.<alias> to apps.queries (no deploy)"
50414
+ );
50415
+ process.exit(1);
50416
+ }
50327
50417
  if (subcommand === "dev") {
50328
50418
  const projectDir = toolArgs;
50329
50419
  let port;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lotics/cli",
3
- "version": "0.65.0",
3
+ "version": "0.67.0",
4
4
  "description": "Lotics SDK and CLI for AI agents",
5
5
  "type": "module",
6
6
  "bin": {
@@ -26,7 +26,7 @@
26
26
  "@types/node": "^22.15.21",
27
27
  "esbuild": "^0.28.1",
28
28
  "typescript": "^6.0.3",
29
- "vitest": "^4.1.7"
29
+ "vitest": "^4.1.9"
30
30
  },
31
31
  "keywords": [
32
32
  "lotics",