@lotics/cli 0.88.1 → 0.90.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
@@ -30643,8 +30643,40 @@ import { tmpdir } from "node:os";
30643
30643
 
30644
30644
  // src/starter_template.ts
30645
30645
  var STARTER_FALLBACK_UI_VERSION = "6.1.0";
30646
- var STARTER_FALLBACK_SDK_VERSION = "0.51.0";
30646
+ var STARTER_FALLBACK_SDK_VERSION = "0.52.0";
30647
30647
  var STARTER_REACT_NATIVE_VERSION = "0.85.3";
30648
+ var VITEST_SETUP_FILENAME = "vitest.setup.ts";
30649
+ var VITEST_SETUP_CONTENT = `import { vi } from "vitest";
30650
+
30651
+ // A package-linked app's generated .lotics/app_fields.ts resolves F/OPT/ROLE
30652
+ // from the installation's live binding at module load (\`await getAppBinding()\`,
30653
+ // a network call over the host bridge). Under vitest/jsdom there is no host, so
30654
+ // that call throws while a test is still COLLECTING \u2014 failing every test that
30655
+ // imports the app graph, even pure logic tests. getAppBinding is the network
30656
+ // boundary; mock it (and only it) so the module graph evaluates in tests.
30657
+ //
30658
+ // The stub returns an ECHO binding: any alias resolves to a synthetic,
30659
+ // self-identifying id (fld:test:<entity>.<field>, opt:test:\u2026, grp:test:\u2026). Echo,
30660
+ // not empty maps \u2014 app_fields's bound() throws on a missing key, so empty maps
30661
+ // would just re-break collection; and a ':test:' id can never be confused with a
30662
+ // real fld_\u2026/opt_\u2026/grp_\u2026 id, so a test that depends on a concrete binding fails
30663
+ // loud instead of silently reading undefined.
30664
+ vi.mock("@lotics/app-sdk", async (importOriginal) => {
30665
+ const actual = await importOriginal<typeof import("@lotics/app-sdk")>();
30666
+ const echoIds = (prefix: string): Record<string, string> =>
30667
+ new Proxy(
30668
+ {},
30669
+ {
30670
+ get: (_target, key) => (typeof key === "string" ? \`\${prefix}:test:\${key}\` : undefined),
30671
+ },
30672
+ ) as Record<string, string>;
30673
+ return {
30674
+ ...actual,
30675
+ getAppBinding: () =>
30676
+ Promise.resolve({ fields: echoIds("fld"), options: echoIds("opt"), roles: echoIds("grp") }),
30677
+ };
30678
+ });
30679
+ `;
30648
30680
  function buildStarterTemplate(args) {
30649
30681
  const uiVersion = args.ui_version ?? `^${STARTER_FALLBACK_UI_VERSION}`;
30650
30682
  const sdkVersion = args.sdk_version ?? `^${STARTER_FALLBACK_SDK_VERSION}`;
@@ -30761,8 +30793,32 @@ function buildStarterTemplate(args) {
30761
30793
  path: "vite.config.ts",
30762
30794
  content: `/// <reference types="vitest" />
30763
30795
  import { defineConfig } from "vite";
30796
+ import type { Plugin } from "vite";
30764
30797
  import react from "@vitejs/plugin-react";
30765
30798
 
30799
+ // @lotics/ui/icon.tsx deep-imports \`lucide-react-native/dist/esm/icons/<name>\`.
30800
+ // lucide-react-native's \`exports\` map lists only "." and "./icons", so a strict
30801
+ // resolver (the sandbox's vite dev optimizer) blocks the deep path with
30802
+ // \`Missing "./dist/esm/icons/<name>" specifier\`. lucide-react ships the same
30803
+ // per-icon files with NO exports map, so redirect there. A \`resolveId\` PLUGIN
30804
+ // (not \`resolve.alias\`) is required: the dep-optimizer SCAN runs plugin
30805
+ // resolveId hooks but does NOT apply regex \`resolve.alias\`, so the alias alone
30806
+ // fixes the production build but leaves the dev preview broken.
30807
+ function lucideIconsWebAlias(): Plugin {
30808
+ return {
30809
+ name: "lucide-icons-web-alias",
30810
+ enforce: "pre",
30811
+ async resolveId(source, importer, options) {
30812
+ const m = /^lucide-react-native\\/dist\\/esm\\/icons\\/(.+)$/.exec(source);
30813
+ if (!m) return null;
30814
+ return this.resolve(\`lucide-react/dist/esm/icons/\${m[1]}\`, importer, {
30815
+ ...options,
30816
+ skipSelf: true,
30817
+ });
30818
+ },
30819
+ };
30820
+ }
30821
+
30766
30822
  // Vite default base (/) emits absolute asset URLs in index.html. Lotics's
30767
30823
  // render endpoint rewrites those to /v1/apps/{id}/asset/... so the bundle
30768
30824
  // loads via the platform's asset proxy. Don't change \`base\` unless you
@@ -30772,14 +30828,8 @@ import react from "@vitejs/plugin-react";
30772
30828
  // (View, Text, Pressable, StyleSheet, etc.) render in a pure-web environment.
30773
30829
  // .web.tsx is prioritized in resolve.extensions so per-target variants
30774
30830
  // (avatar.web.tsx, wave_avatar.web.tsx) win over the native .tsx file.
30775
- //
30776
- // @lotics/ui/icon.tsx imports from \`lucide-react-native/dist/esm/icons/<name>\`
30777
- // \u2014 deep paths that lucide-react-native's package \`exports\` map blocks under
30778
- // Vite's strict resolution. lucide-react has the same per-icon files with no
30779
- // exports map, so we alias the deep path to use it. Web rendering of <svg>
30780
- // is identical to RN-SVG when running under RN-Web.
30781
30831
  export default defineConfig({
30782
- plugins: [react()],
30832
+ plugins: [lucideIconsWebAlias(), react()],
30783
30833
  // RN libraries reference globals Metro injects but Vite does not \u2014 undefined
30784
30834
  // \u21D2 the bundle throws. \`__DEV__\` (react-native-web, @react-native-picker's
30785
30835
  // UnimplementedView): throws at load, blank iframe. \`global\` (rn-web
@@ -30790,13 +30840,7 @@ export default defineConfig({
30790
30840
  // is safe. Define both so dev and the deployed build behave identically.
30791
30841
  define: { __DEV__: "false", global: "globalThis" },
30792
30842
  resolve: {
30793
- alias: [
30794
- {
30795
- find: /^lucide-react-native\\/dist\\/esm\\/icons\\/(.+)$/,
30796
- replacement: "lucide-react/dist/esm/icons/$1",
30797
- },
30798
- { find: "react-native", replacement: "react-native-web" },
30799
- ],
30843
+ alias: [{ find: "react-native", replacement: "react-native-web" }],
30800
30844
  // \`.web.js\` resolves the web build of RN packages that ship \`X.js\` (native)
30801
30845
  // beside \`X.web.js\` (web) \u2014 e.g. @react-native-picker/picker, whose compiled
30802
30846
  // \`Picker.web.js\` renders a real <select>. Without it the extensionless
@@ -30875,6 +30919,12 @@ export default defineConfig({
30875
30919
  // iframe loads modules from api.lotics.ai which already permits null
30876
30920
  // origin via CORS.
30877
30921
  cors: { origin: "*" },
30922
+ // Chat-authoring previews reach this dev server through the sandbox proxy
30923
+ // at \`5173-<id>-<token>.lotics-sandbox.app\`. Vite \u22655.4.12 rejects HMR
30924
+ // WebSocket upgrades whose Origin doesn't match a trusted host (its
30925
+ // cross-site WS hijack fix) \u2014 without this entry the proxied preview page
30926
+ // loads over HTTP but HMR never connects, so edits stop live-updating.
30927
+ allowedHosts: [".lotics-sandbox.app"],
30878
30928
  // @lotics/ui/fonts.css references the API's /iframe/fonts/*.woff2 files
30879
30929
  // by root-relative URL. A deployed app is served from the API origin so
30880
30930
  // they resolve directly; under \`lotics app dev\` the app runs on
@@ -30883,6 +30933,11 @@ export default defineConfig({
30883
30933
  },
30884
30934
  test: {
30885
30935
  environment: "jsdom",
30936
+ // Stub @lotics/app-sdk's getAppBinding (the network boundary) before any test
30937
+ // collects. A package-linked app's generated .lotics/app_fields.ts awaits it
30938
+ // at module load; without the stub that network call throws under jsdom and
30939
+ // every test that imports the app graph fails to collect. See vitest.setup.ts.
30940
+ setupFiles: ["./${VITEST_SETUP_FILENAME}"],
30886
30941
  // RN packages ship Flow (\`import typeof\`) in their native source, reached
30887
30942
  // transitively by RN-Web components (pickers, calendars, anything touching
30888
30943
  // Animated). Vitest's web optimizer is OFF by default and ignores the
@@ -31013,6 +31068,7 @@ mount(
31013
31068
  import { View } from "react-native";
31014
31069
  import { useNavigate, useParams } from "react-router-dom";
31015
31070
  import { AppRouter } from "@lotics/app-sdk/router";
31071
+ import { useAiContext } from "@lotics/app-sdk";
31016
31072
  import { Card } from "@lotics/ui/card";
31017
31073
  import { Text } from "@lotics/ui/text";
31018
31074
  import { Button } from "@lotics/ui/button";
@@ -31077,6 +31133,16 @@ function ItemDetailScreen() {
31077
31133
  const { id } = useParams();
31078
31134
  const navigate = useNavigate();
31079
31135
  const item = ITEMS.find((i) => i.id === id);
31136
+ // Tell the member's ambient chat agent what this screen is showing, so a
31137
+ // question like "summarise this" has context. This is PUSH-ONLY: a snapshot of
31138
+ // what the app rendered to the member, never a channel for chat to pull app
31139
+ // data \u2014 the text lands in the prompt as labeled data (not instructions) and
31140
+ // any record refs would act only within the member's own IAM. Pass null to
31141
+ // clear the slot (here: nothing to describe when the item isn't found).
31142
+ useAiContext(
31143
+ "item_detail",
31144
+ item ? { description: \`Viewing item "\${item.name}".\`, data: { id: item.id } } : null,
31145
+ );
31080
31146
  return (
31081
31147
  <Screen>
31082
31148
  {/* An in-app Back control; navigate(-1) walks the history (the browser Back
@@ -31166,9 +31232,9 @@ each alias lives in \`package.json#lotics.workflows.<alias>\`.
31166
31232
  path: "src/lucide-react-native.d.ts",
31167
31233
  content: `// Type shim for lucide-react-native's deep-icon imports
31168
31234
  // (\`lucide-react-native/dist/esm/icons/<name>\`). The package ships JS-only
31169
- // files without per-file .d.ts. The Vite alias in vite.config.ts rewrites
31170
- // these to lucide-react at bundle time; this declaration covers the
31171
- // TypeScript compile-time gap.
31235
+ // files without per-file .d.ts. The lucideIconsWebAlias() plugin in
31236
+ // vite.config.ts redirects these to lucide-react at resolve time; this
31237
+ // declaration covers the TypeScript compile-time gap.
31172
31238
  declare module "lucide-react-native/dist/esm/icons/*" {
31173
31239
  import type { ComponentType } from "react";
31174
31240
  const Icon: ComponentType<{
@@ -31248,6 +31314,10 @@ describe("App", () => {
31248
31314
  });
31249
31315
  `
31250
31316
  },
31317
+ {
31318
+ path: VITEST_SETUP_FILENAME,
31319
+ content: VITEST_SETUP_CONTENT
31320
+ },
31251
31321
  {
31252
31322
  path: ".github/workflows/ci.yml",
31253
31323
  content: `name: CI
@@ -32225,48 +32295,10 @@ function openBrowser(url) {
32225
32295
  child.unref();
32226
32296
  }
32227
32297
 
32228
- // src/generate_app_workflows_dts.ts
32229
- var HEADER = `// Auto-generated by 'lotics app pull/dev/deploy'.
32230
- // DO NOT EDIT \u2014 regenerated from package.json#lotics.workflows.
32231
- //
32232
- // This file gives \`useWorkflow("alias")\` a typed input parameter at call
32233
- // sites by augmenting the @lotics/app-sdk \`AppWorkflows\` interface.
32234
-
32235
- import "@lotics/app-sdk";
32236
- `;
32237
- function generateAppWorkflowsDts(workflows) {
32238
- const entries = Object.entries(workflows ?? {});
32239
- if (entries.length === 0) {
32240
- return `${HEADER}
32241
- // No workflows declared in package.json#lotics.workflows.
32242
- // Add an entry to enable typed useWorkflow<"alias"> at call sites.
32243
- declare module "@lotics/app-sdk" {
32244
- interface AppWorkflows {}
32245
- }
32246
- `;
32247
- }
32248
- entries.sort(([a], [b]) => a.localeCompare(b));
32249
- const inputLines = [];
32250
- const resultLines = [];
32251
- for (const [alias, declaration] of entries) {
32252
- const valueType = declaration.inputs ? inputsToType(declaration.inputs, { nullableOptional: true }) : "Record<string, unknown>";
32253
- const aliasKey = isValidIdentifier(alias) ? alias : JSON.stringify(alias);
32254
- inputLines.push(` ${aliasKey}: ${valueType};`);
32255
- if (declaration.outputs) {
32256
- resultLines.push(` ${aliasKey}: ${objectFieldsToType(declaration.outputs)};`);
32257
- }
32258
- }
32259
- const resultsBlock = resultLines.length > 0 ? `
32260
- interface AppWorkflowResults {
32261
- ${resultLines.join("\n")}
32262
- }` : "";
32263
- return `${HEADER}
32264
- declare module "@lotics/app-sdk" {
32265
- interface AppWorkflows {
32266
- ${inputLines.join("\n")}
32267
- }${resultsBlock}
32268
- }
32269
- `;
32298
+ // ../shared/src/app_dts.ts
32299
+ var IDENTIFIER_REGEX = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/;
32300
+ function isValidIdentifier(name) {
32301
+ return IDENTIFIER_REGEX.test(name);
32270
32302
  }
32271
32303
  function inputsToType(inputs, opts) {
32272
32304
  const nullableOptional = opts?.nullableOptional === true;
@@ -32380,28 +32412,56 @@ function outputDeclToTsType(decl) {
32380
32412
  return "unknown";
32381
32413
  }
32382
32414
  }
32383
- var IDENTIFIER_REGEX = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/;
32384
- function isValidIdentifier(name) {
32385
- return IDENTIFIER_REGEX.test(name);
32386
- }
32415
+ var QUERIES_HEADER = `// Auto-generated by 'lotics app pull/dev/deploy'.
32416
+ // DO NOT EDIT \u2014 regenerated from package.json#lotics.queries.
32417
+ //
32418
+ // This file gives \`useQuery("alias", params)\` typed params at call sites by
32419
+ // augmenting the @lotics/app-sdk \`AppQueries\` interface.
32387
32420
 
32388
- // src/generate_app_agents_dts.ts
32389
- var HEADER2 = `// Auto-generated by 'lotics app pull/dev/deploy'.
32390
- // DO NOT EDIT \u2014 regenerated from package.json#lotics.agents.
32421
+ import "@lotics/app-sdk";
32422
+ `;
32423
+ function generateAppQueriesDts(queries) {
32424
+ const entries = Object.entries(queries ?? {});
32425
+ if (entries.length === 0) {
32426
+ return `${QUERIES_HEADER}
32427
+ // No queries declared in package.json#lotics.queries.
32428
+ // Add an entry to enable typed useQuery("alias", params) at call sites.
32429
+ declare module "@lotics/app-sdk" {
32430
+ interface AppQueries {}
32431
+ }
32432
+ `;
32433
+ }
32434
+ entries.sort(([a], [b]) => a.localeCompare(b));
32435
+ const lines = [];
32436
+ for (const [alias, declaration] of entries) {
32437
+ const valueType = inputsToType(declaration.params ?? {});
32438
+ const aliasKey = isValidIdentifier(alias) ? alias : JSON.stringify(alias);
32439
+ lines.push(` ${aliasKey}: ${valueType};`);
32440
+ }
32441
+ return `${QUERIES_HEADER}
32442
+ declare module "@lotics/app-sdk" {
32443
+ interface AppQueries {
32444
+ ${lines.join("\n")}
32445
+ }
32446
+ }
32447
+ `;
32448
+ }
32449
+ var WORKFLOWS_HEADER = `// Auto-generated by 'lotics app pull/dev/deploy'.
32450
+ // DO NOT EDIT \u2014 regenerated from package.json#lotics.workflows.
32391
32451
  //
32392
- // This file gives \`useAgentRun("alias")\` typed input + output at call sites by
32393
- // augmenting the @lotics/app-sdk \`AppAgents\` / \`AppAgentResults\` interfaces.
32452
+ // This file gives \`useWorkflow("alias")\` a typed input parameter at call
32453
+ // sites by augmenting the @lotics/app-sdk \`AppWorkflows\` interface.
32394
32454
 
32395
32455
  import "@lotics/app-sdk";
32396
32456
  `;
32397
- function generateAppAgentsDts(agents) {
32398
- const entries = Object.entries(agents ?? {});
32457
+ function generateAppWorkflowsDts(workflows) {
32458
+ const entries = Object.entries(workflows ?? {});
32399
32459
  if (entries.length === 0) {
32400
- return `${HEADER2}
32401
- // No agents declared in package.json#lotics.agents.
32402
- // Add an entry to enable typed useAgentRun<"alias"> at call sites.
32460
+ return `${WORKFLOWS_HEADER}
32461
+ // No workflows declared in package.json#lotics.workflows.
32462
+ // Add an entry to enable typed useWorkflow<"alias"> at call sites.
32403
32463
  declare module "@lotics/app-sdk" {
32404
- interface AppAgents {}
32464
+ interface AppWorkflows {}
32405
32465
  }
32406
32466
  `;
32407
32467
  }
@@ -32409,7 +32469,7 @@ declare module "@lotics/app-sdk" {
32409
32469
  const inputLines = [];
32410
32470
  const resultLines = [];
32411
32471
  for (const [alias, declaration] of entries) {
32412
- const valueType = declaration.inputs ? inputsToType(declaration.inputs) : "Record<string, unknown>";
32472
+ const valueType = declaration.inputs ? inputsToType(declaration.inputs, { nullableOptional: true }) : "Record<string, unknown>";
32413
32473
  const aliasKey = isValidIdentifier(alias) ? alias : JSON.stringify(alias);
32414
32474
  inputLines.push(` ${aliasKey}: ${valueType};`);
32415
32475
  if (declaration.outputs) {
@@ -32417,53 +32477,78 @@ declare module "@lotics/app-sdk" {
32417
32477
  }
32418
32478
  }
32419
32479
  const resultsBlock = resultLines.length > 0 ? `
32420
- interface AppAgentResults {
32480
+ interface AppWorkflowResults {
32421
32481
  ${resultLines.join("\n")}
32422
32482
  }` : "";
32423
- return `${HEADER2}
32483
+ return `${WORKFLOWS_HEADER}
32424
32484
  declare module "@lotics/app-sdk" {
32425
- interface AppAgents {
32485
+ interface AppWorkflows {
32426
32486
  ${inputLines.join("\n")}
32427
32487
  }${resultsBlock}
32428
32488
  }
32429
32489
  `;
32430
32490
  }
32431
-
32432
- // src/generate_app_queries_dts.ts
32433
- var HEADER3 = `// Auto-generated by 'lotics app pull/dev/deploy'.
32434
- // DO NOT EDIT \u2014 regenerated from package.json#lotics.queries.
32491
+ var AGENTS_HEADER = `// Auto-generated by 'lotics app pull/dev/deploy'.
32492
+ // DO NOT EDIT \u2014 regenerated from package.json#lotics.agents.
32435
32493
  //
32436
- // This file gives \`useQuery("alias", params)\` typed params at call sites by
32437
- // augmenting the @lotics/app-sdk \`AppQueries\` interface.
32494
+ // This file gives \`useAgentRun("alias")\` typed input + output at call sites by
32495
+ // augmenting the @lotics/app-sdk \`AppAgents\` / \`AppAgentResults\` interfaces.
32438
32496
 
32439
32497
  import "@lotics/app-sdk";
32440
32498
  `;
32441
- function generateAppQueriesDts(queries) {
32442
- const entries = Object.entries(queries ?? {});
32499
+ function generateAppAgentsDts(agents) {
32500
+ const entries = Object.entries(agents ?? {});
32443
32501
  if (entries.length === 0) {
32444
- return `${HEADER3}
32445
- // No queries declared in package.json#lotics.queries.
32446
- // Add an entry to enable typed useQuery("alias", params) at call sites.
32502
+ return `${AGENTS_HEADER}
32503
+ // No agents declared in package.json#lotics.agents.
32504
+ // Add an entry to enable typed useAgentRun<"alias"> at call sites.
32447
32505
  declare module "@lotics/app-sdk" {
32448
- interface AppQueries {}
32506
+ interface AppAgents {}
32449
32507
  }
32450
32508
  `;
32451
32509
  }
32452
32510
  entries.sort(([a], [b]) => a.localeCompare(b));
32453
- const lines = [];
32511
+ const inputLines = [];
32512
+ const resultLines = [];
32454
32513
  for (const [alias, declaration] of entries) {
32455
- const valueType = inputsToType(declaration.params ?? {});
32514
+ const valueType = declaration.inputs ? inputsToType(declaration.inputs) : "Record<string, unknown>";
32456
32515
  const aliasKey = isValidIdentifier(alias) ? alias : JSON.stringify(alias);
32457
- lines.push(` ${aliasKey}: ${valueType};`);
32516
+ inputLines.push(` ${aliasKey}: ${valueType};`);
32517
+ if (declaration.outputs) {
32518
+ resultLines.push(` ${aliasKey}: ${objectFieldsToType(declaration.outputs)};`);
32519
+ }
32458
32520
  }
32459
- return `${HEADER3}
32521
+ const resultsBlock = resultLines.length > 0 ? `
32522
+ interface AppAgentResults {
32523
+ ${resultLines.join("\n")}
32524
+ }` : "";
32525
+ return `${AGENTS_HEADER}
32460
32526
  declare module "@lotics/app-sdk" {
32461
- interface AppQueries {
32462
- ${lines.join("\n")}
32463
- }
32527
+ interface AppAgents {
32528
+ ${inputLines.join("\n")}
32529
+ }${resultsBlock}
32464
32530
  }
32465
32531
  `;
32466
32532
  }
32533
+ var CAPABILITY_GATED_CALLS = {
32534
+ comments: ["useComments", "createComment", "updateComment", "deleteComment"]
32535
+ };
32536
+ function undeclaredCapabilities(sourceText, declared) {
32537
+ const used = [];
32538
+ for (const [capability, calls] of Object.entries(CAPABILITY_GATED_CALLS)) {
32539
+ const isCalled = calls.some((call) => new RegExp(`\\b${call}\\b`).test(sourceText));
32540
+ if (isCalled && declared?.[capability] !== true) used.push(capability);
32541
+ }
32542
+ return used;
32543
+ }
32544
+ function unboundAliases(declared, bound) {
32545
+ const boundWorkflows = new Set(bound.workflows ?? []);
32546
+ const boundAgents = new Set(bound.agents ?? []);
32547
+ return {
32548
+ workflows: [...declared.workflows ?? []].filter((a) => !boundWorkflows.has(a)),
32549
+ agents: [...declared.agents ?? []].filter((a) => !boundAgents.has(a))
32550
+ };
32551
+ }
32467
32552
 
32468
32553
  // ../shared/src/app_query_ast.ts
32469
32554
  function collectQueryTableIds(node) {
@@ -32504,7 +32589,7 @@ function walk(node, visit) {
32504
32589
  }
32505
32590
 
32506
32591
  // src/generate_app_fields.ts
32507
- var HEADER4 = `// Auto-generated by 'lotics app codegen' (and app pull/dev/deploy).
32592
+ var HEADER = `// Auto-generated by 'lotics app codegen' (and app pull/dev/deploy).
32508
32593
  // DO NOT EDIT \u2014 regenerated from the workspace schema.
32509
32594
  //
32510
32595
  // Runtime field + option ids addressed by stable display-name aliases:
@@ -32598,7 +32683,7 @@ ${tableBlocks.join("\n")}
32598
32683
  }
32599
32684
  function generateAppFields(tables) {
32600
32685
  if (tables.length === 0) {
32601
- return `${HEADER4}
32686
+ return `${HEADER}
32602
32687
  export const F = {} as const;
32603
32688
 
32604
32689
  export const OPT = {} as const;
@@ -32610,7 +32695,7 @@ export type AppOptions = typeof OPT;
32610
32695
  `;
32611
32696
  }
32612
32697
  const aliased = aliasTables(tables);
32613
- return `${HEADER4}
32698
+ return `${HEADER}
32614
32699
  ${emitFieldMap(aliased)}
32615
32700
 
32616
32701
  ${emitOptionMap(aliased)}
@@ -32623,7 +32708,7 @@ export type AppOptions = typeof OPT;
32623
32708
  }
32624
32709
 
32625
32710
  // src/generate_package_fields.ts
32626
- var HEADER5 = `// Auto-generated by 'lotics app codegen' (linked/published app; also app pull/dev/deploy).
32711
+ var HEADER2 = `// Auto-generated by 'lotics app codegen' (linked/published app; also app pull/dev/deploy).
32627
32712
  // DO NOT EDIT \u2014 regenerated from the installation's live binding.
32628
32713
  //
32629
32714
  // A package installation resolves F/OPT/ROLE at MODULE LOAD from its binding
@@ -32723,7 +32808,7 @@ ${lines.join("\n")}
32723
32808
  }
32724
32809
  function generatePackageAppFields(binding) {
32725
32810
  const { entities, roles } = parseBinding(binding);
32726
- return `${HEADER5}
32811
+ return `${HEADER2}
32727
32812
  ${emitFieldMap2(entities)}
32728
32813
 
32729
32814
  ${emitOptionMap2(entities)}
@@ -33070,8 +33155,29 @@ function writeBindingAppFields(projectDir, binding) {
33070
33155
  fs4.mkdirSync(dotLotics, { recursive: true });
33071
33156
  const file = path5.join(dotLotics, "app_fields.ts");
33072
33157
  fs4.writeFileSync(file, generatePackageAppFields(binding));
33158
+ ensureAppVitestSetup(projectDir);
33073
33159
  return file;
33074
33160
  }
33161
+ function ensureAppVitestSetup(projectDir) {
33162
+ const setupPath = path5.join(projectDir, VITEST_SETUP_FILENAME);
33163
+ if (!fs4.existsSync(setupPath)) {
33164
+ fs4.writeFileSync(setupPath, VITEST_SETUP_CONTENT);
33165
+ console.error(`Wrote ${VITEST_SETUP_FILENAME} (stubs getAppBinding so \`npm test\` collects).`);
33166
+ } else if (!fs4.readFileSync(setupPath, "utf-8").includes("getAppBinding")) {
33167
+ console.error(
33168
+ `\u26A0 ${VITEST_SETUP_FILENAME} exists but does not stub getAppBinding \u2014 \`npm test\` will fail to collect any test that imports the app graph (the binding-form .lotics/app_fields.ts awaits a network call). Add the stub to it:
33169
+ ${VITEST_SETUP_CONTENT}`
33170
+ );
33171
+ }
33172
+ const viteConfigPath = path5.join(projectDir, "vite.config.ts");
33173
+ if (!fs4.existsSync(viteConfigPath)) return;
33174
+ const viteConfig = fs4.readFileSync(viteConfigPath, "utf-8");
33175
+ if (/setupFiles\s*:/.test(viteConfig) && viteConfig.includes(VITEST_SETUP_FILENAME)) return;
33176
+ console.error(
33177
+ `\u26A0 vite.config.ts does not wire ${VITEST_SETUP_FILENAME} \u2014 \`npm test\` will fail to collect any test that imports the app graph (the binding-form .lotics/app_fields.ts awaits a network call). Add it to the \`test\` block:
33178
+ setupFiles: ["./${VITEST_SETUP_FILENAME}"],`
33179
+ );
33180
+ }
33075
33181
  async function appCodegen(args) {
33076
33182
  const projectDir = path5.resolve(args.projectDir ?? process.cwd());
33077
33183
  const meta = readAppMeta(projectDir);
@@ -33299,17 +33405,6 @@ Ready. Next steps:`);
33299
33405
  console.error(` # edit src/App.tsx`);
33300
33406
  console.error(` lotics app deploy`);
33301
33407
  }
33302
- var CAPABILITY_GATED_CALLS = {
33303
- comments: ["useComments", "createComment", "updateComment", "deleteComment"]
33304
- };
33305
- function undeclaredCapabilities(sourceText, declared) {
33306
- const used = [];
33307
- for (const [capability, calls] of Object.entries(CAPABILITY_GATED_CALLS)) {
33308
- const isCalled = calls.some((call) => new RegExp(`\\b${call}\\b`).test(sourceText));
33309
- if (isCalled && declared?.[capability] !== true) used.push(capability);
33310
- }
33311
- return used;
33312
- }
33313
33408
  function readAppSourceText(projectDir) {
33314
33409
  const srcDir = path5.join(projectDir, "src");
33315
33410
  if (!fs4.existsSync(srcDir)) return "";
@@ -33430,10 +33525,10 @@ function warnIfUnbranded(app) {
33430
33525
  );
33431
33526
  }
33432
33527
  function warnIfUnboundAliases(app, declaredWorkflows, declaredAgents) {
33433
- const boundWorkflows = new Set(Object.keys(app.workflows ?? {}));
33434
- const boundAgents = new Set(Object.keys(app.agents ?? {}));
33435
- const unboundWorkflows = Object.keys(declaredWorkflows).filter((a) => !boundWorkflows.has(a));
33436
- const unboundAgents = Object.keys(declaredAgents).filter((a) => !boundAgents.has(a));
33528
+ const { workflows: unboundWorkflows, agents: unboundAgents } = unboundAliases(
33529
+ { workflows: Object.keys(declaredWorkflows), agents: Object.keys(declaredAgents) },
33530
+ { workflows: Object.keys(app.workflows ?? {}), agents: Object.keys(app.agents ?? {}) }
33531
+ );
33437
33532
  if (unboundWorkflows.length === 0 && unboundAgents.length === 0) return;
33438
33533
  const lines = [
33439
33534
  "\n\u26A0 The manifest declares aliases that are NOT bound on the server. Deploy ships code +",
@@ -539,6 +539,14 @@ export declare class LoticsClient {
539
539
  }>;
540
540
  /** `knowledge_expects` names the package's agents route to but it does not own (advisory). */
541
541
  missing_expected_docs: string[];
542
+ /** Orphan duplicate link fields (unbound links between binding-covered tables). Always empty on an origin. */
543
+ orphan_duplicate_links: Array<{
544
+ field_id: string;
545
+ field_name: string;
546
+ table_id: string;
547
+ table_name: string;
548
+ target_table_id: string;
549
+ }>;
542
550
  }>;
543
551
  /**
544
552
  * Preview a release — the dry run behind `opctl app release`. Runs the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lotics/cli",
3
- "version": "0.88.1",
3
+ "version": "0.90.0",
4
4
  "description": "Lotics SDK and CLI for AI agents",
5
5
  "type": "module",
6
6
  "bin": {