@lotics/cli 0.205.1 → 0.205.2

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
@@ -15559,8 +15559,15 @@ var LoticsClient = class {
15559
15559
  * no caller has to remember that a partial payload silently drops
15560
15560
  * `instructions`, `outputs` and the model pin.
15561
15561
  */
15562
- async setAppAgent(app_id, alias, patch) {
15563
- return this.execute("set_app_agent", { app_id, alias, ...patch });
15562
+ async setAppAgent(app_id, alias, patch, acknowledge_breaking_api_change) {
15563
+ return this.execute("set_app_agent", {
15564
+ app_id,
15565
+ alias,
15566
+ ...patch,
15567
+ // Sent only when the caller asked for it: absent means "refuse a break",
15568
+ // which is the answer a caller who said nothing gave.
15569
+ ...acknowledge_breaking_api_change ? { acknowledge_breaking_api_change: true } : {}
15570
+ });
15564
15571
  }
15565
15572
  /**
15566
15573
  * Fetch one app workflow's faithful source + bound input/output schemas via
@@ -34267,6 +34274,9 @@ var appAgentDeclarationSchema = zod_default.object({
34267
34274
  "Set by the server when a registry copy materialized this agent. An upgrade replaces the declaration only while it still matches this fingerprint."
34268
34275
  )
34269
34276
  });
34277
+ function declaresSchema(half) {
34278
+ return half !== void 0 && Object.keys(half).length > 0;
34279
+ }
34270
34280
  var appThemeSchema = zod_default.object({
34271
34281
  color: optionColorSchema.nullable().optional().describe("Theme color for the app")
34272
34282
  });
@@ -40111,7 +40121,7 @@ function resultSideEffects(result) {
40111
40121
  }
40112
40122
 
40113
40123
  // src/version.ts
40114
- var VERSION = "0.205.1";
40124
+ var VERSION = "0.205.2";
40115
40125
 
40116
40126
  // src/timezone.ts
40117
40127
  function machineTimezone() {
@@ -40671,6 +40681,12 @@ var COMMANDS = [
40671
40681
  " lotics app create <name> [path] Create a new custom-code app + scaffold locally",
40672
40682
  " [--from <model.json>#<app>] \u2026 with the plan's screens: each a registry shape",
40673
40683
  " over the live table its entity became",
40684
+ " [--api] \u2026 with NO screens: the app's declared queries and",
40685
+ " workflows are the whole of what it offers, called",
40686
+ " over HTTP by the customer's own site, server or",
40687
+ " agent. No bundle, so nothing is built or deployed \u2014",
40688
+ " app query set / app workflow set publish each",
40689
+ " declaration, and app api publish snapshots them",
40674
40690
  " lotics app pull <app_id> [path] Bootstrap full local env (source + npm install + types)",
40675
40691
  " lotics app deploy [--prune] [-m <msg>]",
40676
40692
  " Typecheck + build + upload current dir as a new version \u2014 COMMIT",
@@ -40692,7 +40708,7 @@ var COMMANDS = [
40692
40708
  " promises. Without it such a release is refused and",
40693
40709
  " every breaking change is named. It counts only from",
40694
40710
  " an organization admin. Same flag on app query set,",
40695
- " app workflow set and app upgrade",
40711
+ " app workflow set, app agent set and app upgrade",
40696
40712
  " lotics app versions [app_id] Show deploy history newest-first (version,",
40697
40713
  " timestamp, deployer, build status, -m message;",
40698
40714
  " * marks the currently served version)",
@@ -40767,6 +40783,9 @@ var COMMANDS = [
40767
40783
  " fields keep whatever is bound (a manifest is a",
40768
40784
  " snapshot; replaying it would revert them). Change",
40769
40785
  " those with set_app_agent, then pull.",
40786
+ " [--acknowledge-breaking-api] \u2026 even though it breaks what the app's published API",
40787
+ " promises: an agent's alias and its declared edges",
40788
+ " are in the contract too",
40770
40789
  " lotics app subdomain <new-subdomain> Rename the app's public address (its subdomain)",
40771
40790
  ` lotics app rename "<new name>" Rename the app's display name (launcher title)`,
40772
40791
  " lotics app dev [path] [--port=<n>] [--vite-port=<n>]",
@@ -41049,136 +41068,237 @@ var VITEST_SETUP_CONTENT = `// Runs before every test file (wired by vite.config
41049
41068
  // timezone pinning, anything your suite needs before it collects.
41050
41069
  export {};
41051
41070
  `;
41071
+ function packageNameFrom(appName) {
41072
+ const folded = appName.normalize("NFD").replace(/[\u{0300}-\u{036f}]/gu, "").replace(/[đĐ]/g, (c) => c === "\u0111" ? "d" : "D");
41073
+ return folded.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "").slice(0, 64) || "lotics-app";
41074
+ }
41075
+ function manifestFile(args) {
41076
+ return {
41077
+ path: "package.json",
41078
+ content: JSON.stringify(
41079
+ {
41080
+ name: packageNameFrom(args.app_name),
41081
+ version: "0.0.1",
41082
+ private: true,
41083
+ type: "module",
41084
+ scripts: args.scripts,
41085
+ ...args.dependencies === void 0 ? {} : { dependencies: args.dependencies },
41086
+ devDependencies: args.devDependencies,
41087
+ lotics: {
41088
+ app_id: args.app_id,
41089
+ workspace_id: args.workspace_id,
41090
+ current_version_id: null,
41091
+ version_number: null
41092
+ }
41093
+ },
41094
+ null,
41095
+ 2
41096
+ ) + "\n"
41097
+ };
41098
+ }
41099
+ function tsconfigFile() {
41100
+ return {
41101
+ path: "tsconfig.json",
41102
+ content: JSON.stringify(
41103
+ {
41104
+ compilerOptions: {
41105
+ target: "ES2022",
41106
+ useDefineForClassFields: true,
41107
+ lib: ["ES2022", "DOM", "DOM.Iterable"],
41108
+ module: "ESNext",
41109
+ skipLibCheck: true,
41110
+ moduleResolution: "Bundler",
41111
+ allowImportingTsExtensions: true,
41112
+ resolveJsonModule: true,
41113
+ isolatedModules: true,
41114
+ noEmit: true,
41115
+ jsx: "react-jsx",
41116
+ strict: true,
41117
+ noFallthroughCasesInSwitch: true
41118
+ },
41119
+ // Restrict tsc to the project's own sources — a `noEmit` program has
41120
+ // no reason to walk anything else, and under the `LOTICS_UI_SRC`
41121
+ // dev-link `@lotics/ui` resolves to a working copy of the kit's own
41122
+ // .tsx, which strict rules (noUnusedLocals etc.) would then fire on.
41123
+ // `.lotics/**/*` (not bare `.lotics`): TypeScript's include glob walk
41124
+ // SKIPS dot-directories, so a bare `.lotics` silently loads zero of the
41125
+ // generated `.d.ts` augmentations and typed useWorkflow/useQuery/useAgentRun
41126
+ // fall to `unknown`. The explicit glob makes the dot-dir the non-wildcard
41127
+ // base, which IS read. (GAP-38.)
41128
+ include: ["src", ".lotics/**/*"],
41129
+ // src/workflows/<alias>.ts bodies run on the server (workflow globals:
41130
+ // trigger / runtime / tool calls), and each pulls its own per-alias
41131
+ // ambient globals under `.lotics/workflows/`. BOTH are excluded from
41132
+ // the main typecheck: the bodies would apply the app's DOM lib (the
41133
+ // server doesn't), and the per-alias `.globals.d.ts` files each declare
41134
+ // their OWN ambient `trigger` — loading several into one program
41135
+ // collides those declarations and reports false errors. Type-check the
41136
+ // bodies with `lotics app workflow check` instead — it builds ONE
41137
+ // isolated program per alias (mirroring the server's set-time verify),
41138
+ // so the ambient `trigger` is unambiguous. The server stays the single
41139
+ // verifier on `lotics app workflow set`.
41140
+ exclude: ["node_modules", "src/workflows", ".lotics/workflows"]
41141
+ },
41142
+ null,
41143
+ 2
41144
+ ) + "\n"
41145
+ };
41146
+ }
41147
+ function gitignoreFile() {
41148
+ return {
41149
+ path: ".gitignore",
41150
+ content: `node_modules
41151
+ dist
41152
+ *.tsbuildinfo
41153
+ .DS_Store
41154
+ .lotics
41155
+ coverage
41156
+ `
41157
+ };
41158
+ }
41159
+ function ciWorkflowFile(steps) {
41160
+ return {
41161
+ path: ".github/workflows/ci.yml",
41162
+ content: `name: CI
41163
+
41164
+ on:
41165
+ push:
41166
+ branches: [main]
41167
+ pull_request:
41168
+
41169
+ jobs:
41170
+ ci:
41171
+ runs-on: ubuntu-latest
41172
+ steps:
41173
+ - uses: actions/checkout@v4
41174
+ - uses: actions/setup-node@v4
41175
+ with:
41176
+ node-version: 22
41177
+ - run: npm ci
41178
+ ${steps.map((step) => ` - run: ${step}
41179
+ `).join("")}`
41180
+ };
41181
+ }
41182
+ function workflowsReadmeFile() {
41183
+ return {
41184
+ path: "src/workflows/README.md",
41185
+ content: `# Workflow bodies
41186
+
41187
+ Editable JS-subset bodies of this app's workflows live here, one file per alias:
41188
+ \`src/workflows/<alias>.ts\`.
41189
+
41190
+ ## Loop
41191
+
41192
+ \`\`\`bash
41193
+ lotics app workflow pull # write/refresh every src/workflows/<alias>.ts from the server
41194
+ # edit src/workflows/<alias>.ts
41195
+ lotics app workflow check <alias> # typecheck the body locally (same verdict as \`set\`)
41196
+ lotics app workflow set <alias> # push it back through set_app_workflow (the server verifies)
41197
+ \`\`\`
41198
+
41199
+ \`lotics app pull <app_id>\` also writes these files (alongside the rest of the project).
41200
+
41201
+ ## What a body is
41202
+
41203
+ A workflow body is a **JS-subset expression** that runs server-side using workflow
41204
+ globals (\`trigger\`, \`runtime\`, tool calls) and ends with \`return({ data })\`. Each
41205
+ pulled file wraps the body in the server's \`__workflow\` envelope and references its
41206
+ per-alias ambient globals at \`.lotics/workflows/<alias>.globals.d.ts\`, so it **is**
41207
+ locally typecheckable:
41208
+
41209
+ \`\`\`bash
41210
+ lotics app workflow check # check every body; [alias] to check one
41211
+ \`\`\`
41212
+
41213
+ \`check\` builds ONE isolated program per alias (the app's own \`typescript\`, the
41214
+ server's compile options \u2014 strict, lib es2022 with no DOM), so each body's ambient
41215
+ \`trigger\` is unambiguous and the verdict mirrors set-time verify. The main
41216
+ \`npm run typecheck\` excludes \`src/workflows\` + \`.lotics/workflows\` (the bodies would
41217
+ apply the app's DOM lib, and the per-alias globals collide on \`trigger\`). The
41218
+ verification that matters still runs on the **server** when you \`set\` \u2014 the same
41219
+ guarantee as authoring via \`set_app_workflow\` directly. Edit only the body BETWEEN
41220
+ the wrapper lines; the wrapper, the \`/// <reference>\`, and the \`export {};\` marker
41221
+ are CLI bookkeeping (stripped on \`set\`). Do not rename a file (the filename is the
41222
+ alias the binding is keyed by).
41223
+
41224
+ ## Authority
41225
+
41226
+ \`lotics app deploy\` never authors workflows \u2014 it carries code, queries, and
41227
+ capabilities only. \`apps.workflows\` has exactly one author: \`set_app_workflow\`
41228
+ (which \`lotics app workflow set\` calls). The typed \`inputs\`/\`outputs\` schema for
41229
+ each alias lives in \`package.json#lotics.workflows.<alias>\`.
41230
+ `
41231
+ };
41232
+ }
41052
41233
  function buildStarterTemplate(args) {
41053
41234
  const uiVersion = args.ui_version;
41054
41235
  const sdkVersion = args.sdk_version;
41055
- const folded = args.app_name.normalize("NFD").replace(/[\u0300-\u036f]/g, "").replace(/[đĐ]/g, (c) => c === "\u0111" ? "d" : "D");
41056
- const sanitizedPkgName = folded.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "").slice(0, 64) || "lotics-app";
41057
41236
  return [
41058
- {
41059
- path: "package.json",
41060
- content: JSON.stringify(
41061
- {
41062
- name: sanitizedPkgName,
41063
- version: "0.0.1",
41064
- private: true,
41065
- type: "module",
41066
- scripts: {
41067
- dev: "vite",
41068
- build: "vite build",
41069
- preview: "vite preview",
41070
- typecheck: "tsc --noEmit",
41071
- // Scope to src/ — oxlint's default scan walks node_modules too.
41072
- lint: "oxlint src",
41073
- // No `--passWithNoTests`, deliberately. It would be justified by a
41074
- // TEMPORARY condition ("the scaffold seeds nothing") and then ship
41075
- // forever in every app's package.json and CI, where its only
41076
- // remaining effect is to turn "vitest discovered no test files" into
41077
- // success — a green suite over a renamed `src/`, a changed `include`
41078
- // or a broken vitest config, indefinitely. A detector that cannot
41079
- // fire when the thing is ABSENT rather than excessive is not one.
41080
- //
41081
- // What the scaffold seeds instead is `src/harness.test.tsx`, which
41082
- // asserts the HARNESS and nothing else. The file it replaces
41083
- // (`src/App.test.tsx`) asserted the scaffold's placeholder copy
41084
- // ("First item"), so it failed the moment the author did the very
41085
- // thing the closing instructions tell them to do — edit
41086
- // `src/App.tsx` — and a smoke test that renders `<App/>` would fail
41087
- // the same way for any real app whose first screen reaches the host
41088
- // bridge on mount. The harness test touches neither.
41089
- test: "vitest run"
41090
- },
41091
- dependencies: {
41092
- "@lotics/app-sdk": sdkVersion,
41093
- // The FilePreview engines (pdfjs-dist / @lotics/docx / @lotics/xlsx) are
41094
- // NOT listed here: they ship as @lotics/ui dependencies (7.14.0+), so
41095
- // every app gets them transitively — one version source, no skew. An app
41096
- // that IMPORTS @lotics/docx or @lotics/xlsx directly (its own doc/excel
41097
- // builders) declares its own dependency then.
41098
- "@lotics/ui": uiVersion,
41099
- // @base-ui/react is deliberately absent: @lotics/ui declares it as an
41100
- // EXACT peer, npm installs a peer, and a second declaration here is a
41101
- // version that drifts from the kit's — which Base UI punishes
41102
- // silently, since it passes state through React context and a nested
41103
- // copy renders parts that never open.
41104
- "lucide-react": "^0.562.0",
41105
- react: "^19.0.0",
41106
- "react-dom": "^19.0.0",
41107
- // In-app routing for the scaffolded list→detail example. The app owns
41108
- // its routing; the SDK's `isEmbedded()` picks memory vs browser history. `react-router` is the
41109
- // canonical package (react-router-dom is a shim over it).
41110
- "react-router": "^8.3.0"
41111
- },
41112
- devDependencies: {
41113
- "@testing-library/react": "^16.1.0",
41114
- "@types/react": "^19.0.0",
41115
- "@types/react-dom": "^19.0.0",
41116
- "@vitejs/plugin-react": "^4.3.0",
41117
- jsdom: "^25.0.0",
41118
- oxlint: "^0.13.0",
41119
- // Track the platform's TS major so `lotics app workflow check` (which runs
41120
- // the app's OWN typescript) gives the same verdict the server does at set-time.
41121
- typescript: "~6.0.3",
41122
- vite: "^5.4.0",
41123
- vitest: "^2.1.0"
41124
- },
41125
- lotics: {
41126
- app_id: args.app_id,
41127
- workspace_id: args.workspace_id,
41128
- current_version_id: null,
41129
- version_number: null
41130
- }
41131
- },
41132
- null,
41133
- 2
41134
- ) + "\n"
41135
- },
41136
- {
41137
- path: "tsconfig.json",
41138
- content: JSON.stringify(
41139
- {
41140
- compilerOptions: {
41141
- target: "ES2022",
41142
- useDefineForClassFields: true,
41143
- lib: ["ES2022", "DOM", "DOM.Iterable"],
41144
- module: "ESNext",
41145
- skipLibCheck: true,
41146
- moduleResolution: "Bundler",
41147
- allowImportingTsExtensions: true,
41148
- resolveJsonModule: true,
41149
- isolatedModules: true,
41150
- noEmit: true,
41151
- jsx: "react-jsx",
41152
- strict: true,
41153
- noFallthroughCasesInSwitch: true
41154
- },
41155
- // Restrict tsc to the project's own sources — a `noEmit` program has
41156
- // no reason to walk anything else, and under the `LOTICS_UI_SRC`
41157
- // dev-link `@lotics/ui` resolves to a working copy of the kit's own
41158
- // .tsx, which strict rules (noUnusedLocals etc.) would then fire on.
41159
- // `.lotics/**/*` (not bare `.lotics`): TypeScript's include glob walk
41160
- // SKIPS dot-directories, so a bare `.lotics` silently loads zero of the
41161
- // generated `.d.ts` augmentations and typed useWorkflow/useQuery/useAgentRun
41162
- // fall to `unknown`. The explicit glob makes the dot-dir the non-wildcard
41163
- // base, which IS read. (GAP-38.)
41164
- include: ["src", ".lotics/**/*"],
41165
- // src/workflows/<alias>.ts bodies run on the server (workflow globals:
41166
- // trigger / runtime / tool calls), and each pulls its own per-alias
41167
- // ambient globals under `.lotics/workflows/`. BOTH are excluded from
41168
- // the main typecheck: the bodies would apply the app's DOM lib (the
41169
- // server doesn't), and the per-alias `.globals.d.ts` files each declare
41170
- // their OWN ambient `trigger` — loading several into one program
41171
- // collides those declarations and reports false errors. Type-check the
41172
- // bodies with `lotics app workflow check` instead — it builds ONE
41173
- // isolated program per alias (mirroring the server's set-time verify),
41174
- // so the ambient `trigger` is unambiguous. The server stays the single
41175
- // verifier on `lotics app workflow set`.
41176
- exclude: ["node_modules", "src/workflows", ".lotics/workflows"]
41177
- },
41178
- null,
41179
- 2
41180
- ) + "\n"
41181
- },
41237
+ manifestFile({
41238
+ app_name: args.app_name,
41239
+ app_id: args.app_id,
41240
+ workspace_id: args.workspace_id,
41241
+ scripts: {
41242
+ dev: "vite",
41243
+ build: "vite build",
41244
+ preview: "vite preview",
41245
+ typecheck: "tsc --noEmit",
41246
+ // Scope to src/ — oxlint's default scan walks node_modules too.
41247
+ lint: "oxlint src",
41248
+ // No `--passWithNoTests`, deliberately. It would be justified by a
41249
+ // TEMPORARY condition ("the scaffold seeds nothing") and then ship
41250
+ // forever in every app's package.json and CI, where its only
41251
+ // remaining effect is to turn "vitest discovered no test files" into
41252
+ // success — a green suite over a renamed `src/`, a changed `include`
41253
+ // or a broken vitest config, indefinitely. A detector that cannot
41254
+ // fire when the thing is ABSENT rather than excessive is not one.
41255
+ //
41256
+ // What the scaffold seeds instead is `src/harness.test.tsx`, which
41257
+ // asserts the HARNESS and nothing else. The file it replaces
41258
+ // (`src/App.test.tsx`) asserted the scaffold's placeholder copy
41259
+ // ("First item"), so it failed the moment the author did the very
41260
+ // thing the closing instructions tell them to do — edit
41261
+ // `src/App.tsx` — and a smoke test that renders `<App/>` would fail
41262
+ // the same way for any real app whose first screen reaches the host
41263
+ // bridge on mount. The harness test touches neither.
41264
+ test: "vitest run"
41265
+ },
41266
+ dependencies: {
41267
+ "@lotics/app-sdk": sdkVersion,
41268
+ // The FilePreview engines (pdfjs-dist / @lotics/docx / @lotics/xlsx) are
41269
+ // NOT listed here: they ship as @lotics/ui dependencies (7.14.0+), so
41270
+ // every app gets them transitively — one version source, no skew. An app
41271
+ // that IMPORTS @lotics/docx or @lotics/xlsx directly (its own doc/excel
41272
+ // builders) declares its own dependency then.
41273
+ "@lotics/ui": uiVersion,
41274
+ // @base-ui/react is deliberately absent: @lotics/ui declares it as an
41275
+ // EXACT peer, npm installs a peer, and a second declaration here is a
41276
+ // version that drifts from the kit's — which Base UI punishes
41277
+ // silently, since it passes state through React context and a nested
41278
+ // copy renders parts that never open.
41279
+ "lucide-react": "^0.562.0",
41280
+ react: "^19.0.0",
41281
+ "react-dom": "^19.0.0",
41282
+ // In-app routing for the scaffolded list→detail example. The app owns
41283
+ // its routing; the SDK's `isEmbedded()` picks memory vs browser history. `react-router` is the
41284
+ // canonical package (react-router-dom is a shim over it).
41285
+ "react-router": "^8.3.0"
41286
+ },
41287
+ devDependencies: {
41288
+ "@testing-library/react": "^16.1.0",
41289
+ "@types/react": "^19.0.0",
41290
+ "@types/react-dom": "^19.0.0",
41291
+ "@vitejs/plugin-react": "^4.3.0",
41292
+ jsdom: "^25.0.0",
41293
+ oxlint: "^0.13.0",
41294
+ // Track the platform's TS major so `lotics app workflow check` (which runs
41295
+ // the app's OWN typescript) gives the same verdict the server does at set-time.
41296
+ typescript: "~6.0.3",
41297
+ vite: "^5.4.0",
41298
+ vitest: "^2.1.0"
41299
+ }
41300
+ }),
41301
+ tsconfigFile(),
41182
41302
  {
41183
41303
  path: "vite.config.ts",
41184
41304
  content: `/// <reference types="vitest" />
@@ -41508,63 +41628,7 @@ export default function App() {
41508
41628
  }
41509
41629
  `
41510
41630
  },
41511
- {
41512
- // The home for editable workflow bodies. `lotics app pull` writes one
41513
- // `src/workflows/<alias>.ts` per bound workflow (faithful server source);
41514
- // edit it, then `lotics app workflow set <alias>` pushes it back through
41515
- // set_app_workflow (the server verifies). A new app has no bound workflows
41516
- // yet, so this is just the docked directory + the loop reference. It's a
41517
- // .md (not a .ts) so tsc's `include: ["src"]` never tries to compile it —
41518
- // a workflow body is a JS-subset expression over server globals
41519
- // (trigger / runtime / tool calls) that does NOT typecheck standalone.
41520
- path: "src/workflows/README.md",
41521
- content: `# Workflow bodies
41522
-
41523
- Editable JS-subset bodies of this app's workflows live here, one file per alias:
41524
- \`src/workflows/<alias>.ts\`.
41525
-
41526
- ## Loop
41527
-
41528
- \`\`\`bash
41529
- lotics app workflow pull # write/refresh every src/workflows/<alias>.ts from the server
41530
- # edit src/workflows/<alias>.ts
41531
- lotics app workflow check <alias> # typecheck the body locally (same verdict as \`set\`)
41532
- lotics app workflow set <alias> # push it back through set_app_workflow (the server verifies)
41533
- \`\`\`
41534
-
41535
- \`lotics app pull <app_id>\` also writes these files (alongside the rest of the project).
41536
-
41537
- ## What a body is
41538
-
41539
- A workflow body is a **JS-subset expression** that runs server-side using workflow
41540
- globals (\`trigger\`, \`runtime\`, tool calls) and ends with \`return({ data })\`. Each
41541
- pulled file wraps the body in the server's \`__workflow\` envelope and references its
41542
- per-alias ambient globals at \`.lotics/workflows/<alias>.globals.d.ts\`, so it **is**
41543
- locally typecheckable:
41544
-
41545
- \`\`\`bash
41546
- lotics app workflow check # check every body; [alias] to check one
41547
- \`\`\`
41548
-
41549
- \`check\` builds ONE isolated program per alias (the app's own \`typescript\`, the
41550
- server's compile options \u2014 strict, lib es2022 with no DOM), so each body's ambient
41551
- \`trigger\` is unambiguous and the verdict mirrors set-time verify. The main
41552
- \`npm run typecheck\` excludes \`src/workflows\` + \`.lotics/workflows\` (the bodies would
41553
- apply the app's DOM lib, and the per-alias globals collide on \`trigger\`). The
41554
- verification that matters still runs on the **server** when you \`set\` \u2014 the same
41555
- guarantee as authoring via \`set_app_workflow\` directly. Edit only the body BETWEEN
41556
- the wrapper lines; the wrapper, the \`/// <reference>\`, and the \`export {};\` marker
41557
- are CLI bookkeeping (stripped on \`set\`). Do not rename a file (the filename is the
41558
- alias the binding is keyed by).
41559
-
41560
- ## Authority
41561
-
41562
- \`lotics app deploy\` never authors workflows \u2014 it carries code, queries, and
41563
- capabilities only. \`apps.workflows\` has exactly one author: \`set_app_workflow\`
41564
- (which \`lotics app workflow set\` calls). The typed \`inputs\`/\`outputs\` schema for
41565
- each alias lives in \`package.json#lotics.workflows.<alias>\`.
41566
- `
41567
- },
41631
+ workflowsReadmeFile(),
41568
41632
  {
41569
41633
  path: "src/css_modules.d.ts",
41570
41634
  content: `declare module "*.module.css" {
@@ -41610,40 +41674,8 @@ describe("test harness", () => {
41610
41674
  path: VITEST_SETUP_FILENAME,
41611
41675
  content: VITEST_SETUP_CONTENT
41612
41676
  },
41613
- {
41614
- path: ".github/workflows/ci.yml",
41615
- content: `name: CI
41616
-
41617
- on:
41618
- push:
41619
- branches: [main]
41620
- pull_request:
41621
-
41622
- jobs:
41623
- ci:
41624
- runs-on: ubuntu-latest
41625
- steps:
41626
- - uses: actions/checkout@v4
41627
- - uses: actions/setup-node@v4
41628
- with:
41629
- node-version: 22
41630
- - run: npm ci
41631
- - run: npm run typecheck
41632
- - run: npm run lint
41633
- - run: npm test
41634
- - run: npm run build
41635
- `
41636
- },
41637
- {
41638
- path: ".gitignore",
41639
- content: `node_modules
41640
- dist
41641
- *.tsbuildinfo
41642
- .DS_Store
41643
- .lotics
41644
- coverage
41645
- `
41646
- },
41677
+ ciWorkflowFile(["npm run typecheck", "npm run lint", "npm test", "npm run build"]),
41678
+ gitignoreFile(),
41647
41679
  {
41648
41680
  path: "AGENTS.md",
41649
41681
  content: `# ${args.app_name} \u2014 for the agent editing this app
@@ -41767,6 +41799,144 @@ building.
41767
41799
  }
41768
41800
  ];
41769
41801
  }
41802
+ function buildApiStarterTemplate(args) {
41803
+ return [
41804
+ manifestFile({
41805
+ app_name: args.app_name,
41806
+ app_id: args.app_id,
41807
+ workspace_id: args.workspace_id,
41808
+ // `typecheck` alone. There is no bundle to build, no component to render
41809
+ // under jsdom and no DOM source for a linter to scan — a script that
41810
+ // exists and does nothing is a green check that means nothing.
41811
+ scripts: { typecheck: "tsc --noEmit" },
41812
+ devDependencies: { typescript: "~6.0.3" }
41813
+ }),
41814
+ tsconfigFile(),
41815
+ workflowsReadmeFile(),
41816
+ // `.lotics/` is generated and gitignored, so a fresh clone holds no types at
41817
+ // all and `tsc` would refuse a program with no inputs. `app codegen` writes
41818
+ // them from the manifest with no credential and no network, which is exactly
41819
+ // what a CI runner has.
41820
+ ciWorkflowFile(["npx --yes @lotics/cli app codegen", "npm run typecheck"]),
41821
+ gitignoreFile(),
41822
+ {
41823
+ path: "AGENTS.md",
41824
+ content: `# ${args.app_name} \u2014 for the agent editing this app
41825
+
41826
+ A Lotics app with **no screens**. What it offers is its declared queries and workflows, called
41827
+ over HTTP by something outside Lotics \u2014 a website, a server, another agent. There is no React
41828
+ here, no bundle, and nothing to deploy: \`lotics app query set\` and \`lotics app workflow set\`
41829
+ publish each declaration on their own.
41830
+
41831
+ ## Read in this order, and stop when you have the answer
41832
+
41833
+ 1. **\`lotics docs data_model\`** \u2014 tables, fields and records as a query meets them. A query is
41834
+ written against the workspace's real schema, so read the schema before the query.
41835
+ 2. **\`lotics docs building_an_app\`** \u2014 the sequence: clarify the ask, model the data, declare
41836
+ the reads, declare the writes. Skip its screen sections; everything before them applies.
41837
+ 3. **\`lotics docs cli_reference\`** \u2014 every command's real behaviour, flags and refusals.
41838
+ 4. **\`lotics run query_tables '{}'\`** and \`lotics run query_records '{"table_id":"tbl_\u2026"}'\` \u2014
41839
+ the workspace itself. **Verify real VALUES, never just that a field exists.**
41840
+
41841
+ ## The rules that do not change
41842
+
41843
+ - **A query NAMES the columns it returns.** Declare a \`project\` over the fields the caller
41844
+ reads. Without one the app is promising whatever the table happens to hold, so the first
41845
+ \`lotics app api publish\` is refused by alias and never publishes a shape that can move.
41846
+ - **A workflow is the only way this app writes.** Queries read. Every write is a body in
41847
+ \`src/workflows/<alias>.ts\` with declared \`inputs\`, so a caller's bad value is a \`field_errors\`
41848
+ reply rather than a half-written row.
41849
+ - **Declare what it changes.** \`package.json#lotics.writes\` names the fields the workflows
41850
+ write; \`lotics app check\` refuses a body that writes outside it.
41851
+ - **No concrete id in source.** \`rec_\`, \`fld_\` and \`opt_\` literals belong in \`.lotics/\`, which
41852
+ is generated. \`lotics app codegen\` writes the \`F\`/\`OPT\`/\`TBL\` maps to type against.
41853
+ - **An alias is a promise once the API is published.** Renaming one, or narrowing what it takes,
41854
+ breaks a caller nobody here can redeploy \u2014 which is why such a write is refused unless it
41855
+ carries \`--acknowledge-breaking-api\`.
41856
+
41857
+ ## Before you hand it over
41858
+
41859
+ 1. **\`lotics app check\`** \u2014 bindings, declarations, workflow bodies, id portability. It exits
41860
+ non-zero on anything a release would refuse.
41861
+ 2. **Every query projects.** No alias returns a row shape nobody declared.
41862
+ 3. **Every workflow declares its inputs**, and its body reads each one.
41863
+ 4. **Call it the way the consumer will** \u2014 the query endpoint with a key, and again without one
41864
+ if the app is public. What you get back is what you are promising.
41865
+ 5. **\`lotics app api publish\`**, then read the warnings: they name what the published surface
41866
+ exposes that you may not have meant to.
41867
+ `
41868
+ },
41869
+ {
41870
+ path: "README.md",
41871
+ content: `# ${args.app_name}
41872
+
41873
+ A Lotics app with **no screens**. Its declared queries and workflows are the API a caller
41874
+ outside Lotics reads and writes through \u2014 your own site, your own server, an agent.
41875
+
41876
+ ## The loop
41877
+
41878
+ \`\`\`bash
41879
+ # 1. Declare a query in package.json#lotics.queries.<alias>, and NAME THE COLUMNS
41880
+ # it returns (a \`project\` node) \u2014 they are the shape you are promising.
41881
+ lotics app query set <alias>
41882
+
41883
+ # 2. Write a body in src/workflows/<alias>.ts, declare its inputs in
41884
+ # package.json#lotics.workflows.<alias>, then push it (the server verifies).
41885
+ lotics app workflow check <alias>
41886
+ lotics app workflow set <alias>
41887
+
41888
+ # 3. Everything a release would refuse, without shipping one.
41889
+ lotics app check
41890
+
41891
+ # 4. Let something outside reach it: share the app publicly (no key at all), or
41892
+ # give an API key access to this app \u2014 Settings \u2192 API keys \u2192 Only selected ones.
41893
+
41894
+ # 5. Snapshot what those aliases promise, as a numbered contract version.
41895
+ lotics app api publish
41896
+ lotics app api spec -o api.openapi.json # the OpenAPI 3.1 document of that snapshot
41897
+ lotics app api status # which version callers hold
41898
+ \`\`\`
41899
+
41900
+ ## Calling it
41901
+
41902
+ \`\`\`bash
41903
+ curl -X POST https://api.lotics.ai/v1/apps/<app_id>/queries/<alias> \\
41904
+ -H "Content-Type: application/json" \\
41905
+ -H "Authorization: Bearer ltk_\u2026" \\
41906
+ -d '{"params": {}, "limit": 50}'
41907
+
41908
+ curl -X POST https://api.lotics.ai/v1/apps/<app_id>/workflows/<alias>/execute \\
41909
+ -H "Content-Type: application/json" \\
41910
+ -H "Authorization: Bearer ltk_\u2026" \\
41911
+ -d '{"inputs": {}}'
41912
+ \`\`\`
41913
+
41914
+ A query answers \`rows\` (one object per row, keyed by the column names the query declares),
41915
+ \`columns\`, and \u2014 when asked for \u2014 \`total\`, \`next_cursor\` and \`truncated\`. A workflow answers
41916
+ \`status\`, an optional \`message\`, \`data\`, \`files\`, and \`field_errors\` keyed by input name when it
41917
+ refused a value. On a **public** app, send no \`Authorization\` header at all.
41918
+
41919
+ ## While you author
41920
+
41921
+ There is no \`npm run dev\` and no \`lotics app dev\` here \u2014 both serve a Vite bundle, and this app
41922
+ has none. Read the workspace with \`lotics run query_records '{"table_id":"tbl_\u2026"}'\`, and call the
41923
+ endpoints above once the app is shared or a key reaches it.
41924
+
41925
+ \`\`\`bash
41926
+ lotics app codegen # regenerate .lotics/* (types + field/option ids) from the manifest
41927
+ npm run typecheck # tsc --noEmit over the generated types
41928
+ \`\`\`
41929
+
41930
+ ## References
41931
+
41932
+ \`AGENTS.md\` in this directory is the reading order and the checks before you hand this over.
41933
+ \`lotics docs\` prints the rest one page at a time \u2014 \`lotics docs building_an_app\` for the
41934
+ sequence, \`lotics docs data_model\` for the schema, \`lotics docs cli_reference\` for every
41935
+ command's real behaviour.
41936
+ `
41937
+ }
41938
+ ];
41939
+ }
41770
41940
  function escapeHtml(s) {
41771
41941
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
41772
41942
  }
@@ -42281,10 +42451,10 @@ declare module "@lotics/app-sdk" {
42281
42451
  const inputLines = [];
42282
42452
  const resultLines = [];
42283
42453
  for (const [alias, declaration] of entries2) {
42284
- const valueType = declaration.inputs ? inputsToType(declaration.inputs, { nullableOptional: true, optionsByFieldKey }) : "Record<string, unknown>";
42454
+ const valueType = declaresSchema(declaration.inputs) ? inputsToType(declaration.inputs, { nullableOptional: true, optionsByFieldKey }) : "Record<string, unknown>";
42285
42455
  const aliasKey = isValidIdentifier(alias) ? alias : JSON.stringify(alias);
42286
42456
  inputLines.push(` ${aliasKey}: ${valueType};`);
42287
- if (declaration.outputs) {
42457
+ if (declaresSchema(declaration.outputs)) {
42288
42458
  resultLines.push(
42289
42459
  ` ${aliasKey}: ${objectFieldsToType(declaration.outputs, optionsByFieldKey)};`
42290
42460
  );
@@ -42325,10 +42495,10 @@ declare module "@lotics/app-sdk" {
42325
42495
  const inputLines = [];
42326
42496
  const resultLines = [];
42327
42497
  for (const [alias, declaration] of entries2) {
42328
- const valueType = declaration.inputs ? inputsToType(declaration.inputs, { optionsByFieldKey }) : "Record<string, unknown>";
42498
+ const valueType = declaresSchema(declaration.inputs) ? inputsToType(declaration.inputs, { optionsByFieldKey }) : "Record<string, unknown>";
42329
42499
  const aliasKey = isValidIdentifier(alias) ? alias : JSON.stringify(alias);
42330
42500
  inputLines.push(` ${aliasKey}: ${valueType};`);
42331
- if (declaration.outputs) {
42501
+ if (declaresSchema(declaration.outputs)) {
42332
42502
  resultLines.push(
42333
42503
  ` ${aliasKey}: ${objectFieldsToType(declaration.outputs, optionsByFieldKey)};`
42334
42504
  );
@@ -46112,6 +46282,12 @@ async function postModel(client, model, options) {
46112
46282
  }
46113
46283
  }
46114
46284
 
46285
+ // ../shared/src/app_reach.ts
46286
+ function reachedOnlyThroughApi(app) {
46287
+ if (app.current_version_id != null) return false;
46288
+ return Object.keys(app.queries ?? {}).length > 0 || Object.keys(app.workflows ?? {}).length > 0 || Object.keys(app.agents ?? {}).length > 0;
46289
+ }
46290
+
46115
46291
  // src/dev/server.ts
46116
46292
  import http from "node:http";
46117
46293
  import net from "node:net";
@@ -54287,8 +54463,8 @@ async function fetchWorkflowGlobals(client, projectDir, app_id, alias, declarati
54287
54463
  }
54288
54464
  }
54289
54465
  async function fetchWorkflowDts(client, app_id, alias, declaration) {
54290
- const declaresSchema = declaration !== void 0 && (declaration.inputs !== void 0 || declaration.outputs !== void 0);
54291
- if (declaresSchema) return client.getAppWorkflowDts(app_id, alias, declaration);
54466
+ const statesSchema = declaration !== void 0 && (declaration.inputs !== void 0 || declaration.outputs !== void 0);
54467
+ if (statesSchema) return client.getAppWorkflowDts(app_id, alias, declaration);
54292
54468
  return client.getAppWorkflowDts(app_id, alias);
54293
54469
  }
54294
54470
  function runTar(args, cwd) {
@@ -54525,10 +54701,11 @@ function ensureAppTsconfig(projectDir) {
54525
54701
  changes.push(`added ${toAdd.join(", ")} to "exclude"`);
54526
54702
  }
54527
54703
  const existingExtends = parsed.extends;
54528
- if (existingExtends === void 0) {
54704
+ const linked = fs11.existsSync(path12.join(projectDir, LINK_TSCONFIG_PATH));
54705
+ if (linked && existingExtends === void 0) {
54529
54706
  parsed.extends = `./${LINK_TSCONFIG_PATH}`;
54530
54707
  changes.push(`added "extends": "./${LINK_TSCONFIG_PATH}" (so tsc resolves the same @lotics/ui as Vite)`);
54531
- } else if (existingExtends !== `./${LINK_TSCONFIG_PATH}`) {
54708
+ } else if (linked && existingExtends !== `./${LINK_TSCONFIG_PATH}`) {
54532
54709
  console.error(
54533
54710
  `\u26A0 tsconfig.json already extends ${JSON.stringify(existingExtends)}, so the dev-link config was not added \u2014 \`tsc\` will keep resolving @lotics/ui from node_modules even under LOTICS_UI_SRC. Add "./${LINK_TSCONFIG_PATH}" to "extends" (it accepts an array) to fix it.`
54534
54711
  );
@@ -54582,9 +54759,16 @@ function readKitPeerNames(projectDir) {
54582
54759
  return peers && typeof peers === "object" ? Object.keys(peers) : [];
54583
54760
  }
54584
54761
  function writeAppDtsKitLinks(projectDir) {
54762
+ if (!declaresKit(projectDir)) return;
54585
54763
  writeDevLinkTsconfig(projectDir);
54586
54764
  writeKitTypeAugmentation(projectDir);
54587
54765
  }
54766
+ function declaresKit(projectDir) {
54767
+ const manifest = path12.join(projectDir, "package.json");
54768
+ if (!fs11.existsSync(manifest)) return false;
54769
+ const pkg = JSON.parse(fs11.readFileSync(manifest, "utf-8"));
54770
+ return "@lotics/ui" in { ...pkg.dependencies, ...pkg.devDependencies };
54771
+ }
54588
54772
  function writeAppDts(projectDir, manifest, optionsByFieldKey) {
54589
54773
  const dotLotics = path12.join(projectDir, ".lotics");
54590
54774
  fs11.mkdirSync(dotLotics, { recursive: true });
@@ -54667,8 +54851,9 @@ function warnAppFieldsUnwritten(projectDir, err) {
54667
54851
  function warnAboutDevLink(projectDir, command) {
54668
54852
  const uiSrc = process.env.LOTICS_UI_SRC;
54669
54853
  if (!uiSrc) return;
54670
- const viteConfigPath = path12.join(projectDir, "vite.config.ts");
54671
- const config2 = fs11.existsSync(viteConfigPath) ? fs11.readFileSync(viteConfigPath, "utf-8") : "";
54854
+ const viteConfigPath = viteConfigIn(projectDir);
54855
+ if (viteConfigPath === null) return;
54856
+ const config2 = fs11.readFileSync(viteConfigPath, "utf-8");
54672
54857
  const linked = config2.includes("loticsResolve") || config2.includes("LOTICS_UI_SRC");
54673
54858
  if (!linked) {
54674
54859
  console.error(
@@ -54823,14 +55008,20 @@ function inlinesKitUnderVitest(viteConfig) {
54823
55008
  }
54824
55009
  });
54825
55010
  }
55011
+ function viteConfigIn(projectDir) {
55012
+ const name = VITE_CONFIG_NAMES.find(
55013
+ (candidate) => fs11.existsSync(path12.join(projectDir, candidate))
55014
+ );
55015
+ return name === void 0 ? null : path12.join(projectDir, name);
55016
+ }
54826
55017
  function ensureAppVitestSetup(projectDir) {
55018
+ const viteConfigPath = viteConfigIn(projectDir);
55019
+ if (viteConfigPath === null) return;
54827
55020
  const setupPath = path12.join(projectDir, VITEST_SETUP_FILENAME);
54828
55021
  if (!fs11.existsSync(setupPath)) {
54829
55022
  fs11.writeFileSync(setupPath, VITEST_SETUP_CONTENT);
54830
55023
  console.error(`Wrote ${VITEST_SETUP_FILENAME} (wired by vite.config.ts; empty until you fill it).`);
54831
55024
  }
54832
- const viteConfigPath = path12.join(projectDir, "vite.config.ts");
54833
- if (!fs11.existsSync(viteConfigPath)) return;
54834
55025
  const viteConfig = fs11.readFileSync(viteConfigPath, "utf-8");
54835
55026
  if (!(/setupFiles\s*:/.test(viteConfig) && viteConfig.includes(VITEST_SETUP_FILENAME))) {
54836
55027
  console.error(
@@ -55005,7 +55196,19 @@ async function materializeScaffold(targetPath, files) {
55005
55196
  await runNpmInstall(targetPath);
55006
55197
  writeAppDtsKitLinks(targetPath);
55007
55198
  }
55199
+ async function starterDependencyRanges() {
55200
+ const [ui, sdk] = await Promise.all([
55201
+ requireLatestNpmVersion("@lotics/ui"),
55202
+ requireLatestNpmVersion("@lotics/app-sdk")
55203
+ ]);
55204
+ return { ui, sdk };
55205
+ }
55008
55206
  async function appCreate(client, args) {
55207
+ if (args.api === true && args.from !== void 0) {
55208
+ throw new Error(
55209
+ `--api and --from are different apps: a plan describes screens, and an --api app has none. Drop one.`
55210
+ );
55211
+ }
55009
55212
  const targetPath = path12.resolve(args.targetPath ?? appDirName(args.name));
55010
55213
  if (fs11.existsSync(targetPath)) {
55011
55214
  const entries2 = fs11.readdirSync(targetPath);
@@ -55016,10 +55219,7 @@ async function appCreate(client, args) {
55016
55219
  const plan = args.from === void 0 ? null : await resolvePlanForCreate(client, args.from);
55017
55220
  if (plan !== null) await reportPlanCoverage(client, plan);
55018
55221
  fs11.mkdirSync(targetPath, { recursive: true });
55019
- const [uiLatest, sdkLatest] = await Promise.all([
55020
- requireLatestNpmVersion("@lotics/ui"),
55021
- requireLatestNpmVersion("@lotics/app-sdk")
55022
- ]);
55222
+ const ranges = args.api === true ? null : await starterDependencyRanges();
55023
55223
  const app = await client.createApp({
55024
55224
  name: args.name,
55025
55225
  ...plan?.app.description == null ? {} : { description: plan.app.description },
@@ -55027,12 +55227,16 @@ async function appCreate(client, args) {
55027
55227
  ...plan?.app.theme?.color == null ? {} : { theme: { color: plan.app.theme.color } }
55028
55228
  });
55029
55229
  console.error(`Created app: ${app.name} (${app.id})`);
55030
- const starter = buildStarterTemplate({
55230
+ const starter = ranges === null ? buildApiStarterTemplate({
55231
+ app_name: args.name,
55232
+ app_id: app.id,
55233
+ workspace_id: app.workspace_id
55234
+ }) : buildStarterTemplate({
55031
55235
  app_name: args.name,
55032
55236
  app_id: app.id,
55033
55237
  workspace_id: app.workspace_id,
55034
- ui_version: `^${uiLatest}`,
55035
- sdk_version: `^${sdkLatest}`
55238
+ ui_version: `^${ranges.ui}`,
55239
+ sdk_version: `^${ranges.sdk}`
55036
55240
  });
55037
55241
  const files = plan === null ? starter : withPlanFiles(starter, plan);
55038
55242
  await materializeScaffold(targetPath, files);
@@ -55045,23 +55249,34 @@ async function appCreate(client, args) {
55045
55249
  );
55046
55250
  }
55047
55251
  }
55048
- console.error("Building initial version...");
55049
- await appDeploy(client, {
55050
- projectDir: targetPath,
55051
- message: "Initial version"
55052
- });
55252
+ if (ranges !== null) {
55253
+ console.error("Building initial version...");
55254
+ await appDeploy(client, {
55255
+ projectDir: targetPath,
55256
+ message: "Initial version"
55257
+ });
55258
+ }
55053
55259
  console.error(`
55054
55260
  Ready. Next steps:`);
55055
55261
  console.error(` cd ${path12.relative(process.cwd(), targetPath) || "."}`);
55056
55262
  console.error(` lotics docs # the contracts, at the versions installed here`);
55057
- if (plan === null) {
55263
+ if (ranges === null) {
55264
+ console.error(` # declare a query in package.json#lotics.queries \u2014 name the columns it returns \u2014 then:`);
55265
+ console.error(` lotics app query set <alias> # publish that read, no deploy`);
55266
+ console.error(` # write src/workflows/<alias>.ts for each write, then:`);
55267
+ console.error(` lotics app workflow set <alias> # push the body (the server verifies it)`);
55268
+ console.error(` lotics app check # every pre-flight a release would run`);
55269
+ console.error(` # share the app publicly, or give an API key access to it, then:`);
55270
+ console.error(` lotics app api publish # snapshot what those aliases promise`);
55271
+ } else if (plan === null) {
55058
55272
  console.error(` # edit src/App.tsx, then:`);
55059
55273
  console.error(` lotics app check # every pre-flight a deploy runs`);
55274
+ console.error(` lotics app deploy`);
55060
55275
  } else {
55061
55276
  console.error(` # edit src/screens/*.tsx \u2014 one per plan screen \u2014 then:`);
55062
55277
  console.error(` lotics app check --screens # every pre-flight a deploy runs, and the probes over each screen`);
55278
+ console.error(` lotics app deploy`);
55063
55279
  }
55064
- console.error(` lotics app deploy`);
55065
55280
  }
55066
55281
  async function resolvePlanForCreate(client, from) {
55067
55282
  const hash2 = from.lastIndexOf("#");
@@ -55549,7 +55764,7 @@ async function appPull(client, args) {
55549
55764
  const app = await client.getApp(args.app_id);
55550
55765
  if (!app.current_version_id && args.version === void 0) {
55551
55766
  throw new Error(
55552
- `App ${app.id} has no published version yet. Deploy from another machine first, or use 'lotics app create' to scaffold a new app.`
55767
+ reachedOnlyThroughApi(app) ? `App ${app.id} has no bundle \u2014 it is reached through its API. Check out its project from version control; 'lotics app codegen' rebuilds .lotics/ there.` : `App ${app.id} has no published version yet. Deploy from another machine first, or use 'lotics app create' to scaffold a new app.`
55553
55768
  );
55554
55769
  }
55555
55770
  const versionId = args.version ?? app.current_version_id;
@@ -55677,6 +55892,12 @@ function reportPortabilityIds(findings) {
55677
55892
  async function appDeploy(client, args) {
55678
55893
  const projectDir = path12.resolve(args.projectDir ?? process.cwd());
55679
55894
  const meta3 = readAppMeta(projectDir);
55895
+ const scripts = JSON.parse(fs11.readFileSync(path12.join(projectDir, "package.json"), "utf-8")).scripts;
55896
+ if (!scripts?.build) {
55897
+ fail(
55898
+ `package.json declares no \`build\` script, so there is no bundle to deploy \u2014 an app reached through its API goes live through \`lotics app query set\` and \`app workflow set\`, and \`lotics app api publish\` promises them.`
55899
+ );
55900
+ }
55680
55901
  warnAboutDevLink(projectDir, "deploy");
55681
55902
  const liveApp = await client.getApp(meta3.app_id);
55682
55903
  assertProjectIsCurrent(meta3, liveApp);
@@ -56040,6 +56261,11 @@ async function readLiveQueries(client, appId) {
56040
56261
  async function appCheck(client, args = {}) {
56041
56262
  const projectDir = path12.resolve(args.projectDir ?? process.cwd());
56042
56263
  const meta3 = readAppMeta(projectDir);
56264
+ if (args.screens === true && viteConfigIn(projectDir) === null) {
56265
+ fail(
56266
+ `--screens renders this app's screens, and ${path12.basename(projectDir)} has no vite.config \u2014 it is reached through its API, not through a bundle. Run \`lotics app check\` on its own.`
56267
+ );
56268
+ }
56043
56269
  const app = await client.getApp(meta3.app_id);
56044
56270
  assertProjectIsCurrent(meta3, app);
56045
56271
  writeAppDts(projectDir, { workflows: meta3.workflows, queries: meta3.queries, agents: meta3.agents });
@@ -56214,7 +56440,7 @@ Pushing ${plan.length} binding${plan.length === 1 ? "" : "s"} ahead of the app:
56214
56440
  await appWorkflowSet(client, { alias, projectDir, acknowledgeBreakingApi });
56215
56441
  }
56216
56442
  for (const alias of pending.agents) {
56217
- await appAgentSet(client, { alias, projectDir });
56443
+ await appAgentSet(client, { alias, projectDir, acknowledgeBreakingApi });
56218
56444
  }
56219
56445
  }
56220
56446
  function pendingBindings(args) {
@@ -56270,7 +56496,10 @@ function divergedAgentAliases(manifestAgents, liveAgents) {
56270
56496
  for (const [alias, declared] of Object.entries(manifestAgents ?? {})) {
56271
56497
  const live = (liveAgents ?? {})[alias];
56272
56498
  if (!live) continue;
56273
- if (AGENT_TYPED_KEYS.some((key) => !sameSchema(declared[key], live[key]))) out.push(alias);
56499
+ const half = (value) => declaresSchema(value) ? value : void 0;
56500
+ if (AGENT_TYPED_KEYS.some((key) => !sameSchema(half(declared[key]), half(live[key])))) {
56501
+ out.push(alias);
56502
+ }
56274
56503
  }
56275
56504
  return out;
56276
56505
  }
@@ -56440,8 +56669,7 @@ async function pruneOrphanedBindings(client, projectDir, app, orphans, undeclare
56440
56669
  // guard: a run recorded against an alias means something outside the
56441
56670
  // bundle reaches it.
56442
56671
  ...overrideInvocationGuard ? { even_if_invoked: true } : {},
56443
- // Agents carry no published entry, so the field is not theirs to take.
56444
- ...acknowledgeBreakingApi && target.kind !== "agents" ? { acknowledge_breaking_api_change: true } : {}
56672
+ ...acknowledgeBreakingApi ? { acknowledge_breaking_api_change: true } : {}
56445
56673
  });
56446
56674
  if (res.error) {
56447
56675
  console.error(` \u2717 could not unbind ${target.label} \u2014 ${res.error}`);
@@ -56868,6 +57096,11 @@ function resolveDeclaredVitePort(projectDir, flagPort) {
56868
57096
  async function appDev(client, args) {
56869
57097
  const projectDir = path12.resolve(args.projectDir ?? process.cwd());
56870
57098
  const meta3 = readAppMeta(projectDir);
57099
+ if (viteConfigIn(projectDir) === null) {
57100
+ fail(
57101
+ `${path12.basename(projectDir)} has no vite.config, so there is no bundle to serve \u2014 it is reached through its API. Call its queries and workflows directly, or \`lotics app check\`.`
57102
+ );
57103
+ }
56871
57104
  warnAboutDevLink(projectDir, "dev");
56872
57105
  const vitePort = resolveDeclaredVitePort(projectDir, args.vitePort);
56873
57106
  writeAppDts(projectDir, { workflows: meta3.workflows, queries: meta3.queries, agents: meta3.agents });
@@ -57015,16 +57248,29 @@ async function appAgentSet(client, args) {
57015
57248
  process.exit(1);
57016
57249
  }
57017
57250
  const proseBaseline = readSynced(projectDir).agents[args.alias]?.live;
57018
- const res = await client.setAppAgent(meta3.app_id, args.alias, {
57019
- // Prose and its precondition ride together. Gating a declaration-only push on
57020
- // the prose baseline would refuse it because someone edited a PROMPT — a
57021
- // precondition must guard what this call actually writes.
57022
- ...instructions !== void 0 ? { instructions, ...proseBaseline ? { expected_instructions_sha: proseBaseline } : {} } : {},
57023
- ...declared?.inputs !== void 0 ? { inputs: declared.inputs } : {},
57024
- ...declared?.outputs !== void 0 ? { outputs: declared.outputs } : {}
57025
- });
57251
+ const res = await client.setAppAgent(
57252
+ meta3.app_id,
57253
+ args.alias,
57254
+ {
57255
+ // Prose and its precondition ride together. Gating a declaration-only push on
57256
+ // the prose baseline would refuse it because someone edited a PROMPT — a
57257
+ // precondition must guard what this call actually writes.
57258
+ ...instructions !== void 0 ? { instructions, ...proseBaseline ? { expected_instructions_sha: proseBaseline } : {} } : {},
57259
+ ...declared?.inputs !== void 0 ? { inputs: declared.inputs } : {},
57260
+ ...declared?.outputs !== void 0 ? { outputs: declared.outputs } : {}
57261
+ },
57262
+ // The author's answer to a published contract this declaration would break.
57263
+ // Absent, the server refuses such a write and names each change.
57264
+ args.acknowledgeBreakingApi
57265
+ );
57026
57266
  if (res.error) {
57027
- console.error(`Failed to set agent "${args.alias}": ${res.error}`);
57267
+ console.error(
57268
+ `Failed to set agent "${args.alias}": ` + refusalAtThisSurface(
57269
+ res,
57270
+ `lotics app agent set ${args.alias}`,
57271
+ args.acknowledgeBreakingApi
57272
+ )
57273
+ );
57028
57274
  process.exit(1);
57029
57275
  }
57030
57276
  if (instructions !== void 0) {
@@ -57608,6 +57854,7 @@ function parseArgs(argv) {
57608
57854
  prune: false,
57609
57855
  pruneInvoked: [],
57610
57856
  acknowledgeBreakingApi: false,
57857
+ api: false,
57611
57858
  screens: false,
57612
57859
  screen: [],
57613
57860
  width: [],
@@ -57788,6 +58035,9 @@ function parseArgs(argv) {
57788
58035
  case "--acknowledge-breaking-api":
57789
58036
  flags.acknowledgeBreakingApi = true;
57790
58037
  break;
58038
+ case "--api":
58039
+ flags.api = true;
58040
+ break;
57791
58041
  case "--screens":
57792
58042
  flags.screens = true;
57793
58043
  break;
@@ -59652,6 +59902,8 @@ async function main() {
59652
59902
  if (command === "app" && !subcommand) {
59653
59903
  console.error("Usage:");
59654
59904
  console.error(" lotics app create <name> [path] Scaffold a new app locally");
59905
+ console.error(" lotics app create <name> --api ...with no screens: its queries and workflows");
59906
+ console.error(" are the API something outside Lotics calls");
59655
59907
  console.error(" lotics app pull <app_id> [path] Pull an existing app for local editing");
59656
59908
  console.error(" lotics app deploy -m <message> Build + upload the current directory (-m required)");
59657
59909
  console.error(" lotics app versions [app_id] Show deploy history (version, when, who, message)");
@@ -59956,7 +60208,7 @@ Available workspaces:`);
59956
60208
  process.exit(1);
59957
60209
  }
59958
60210
  const targetPath = restArgs[0];
59959
- await appCreate(client, { name, targetPath, from: flags.from });
60211
+ await appCreate(client, { name, targetPath, from: flags.from, api: flags.api });
59960
60212
  return;
59961
60213
  }
59962
60214
  if (subcommand === "pull") {
@@ -60119,7 +60371,7 @@ Available workspaces:`);
60119
60371
  console.error(" Edits src/agents/<alias>.md (written by 'lotics app pull').");
60120
60372
  process.exit(1);
60121
60373
  }
60122
- await appAgentSet(client, { alias });
60374
+ await appAgentSet(client, { alias, acknowledgeBreakingApi: flags.acknowledgeBreakingApi });
60123
60375
  return;
60124
60376
  }
60125
60377
  agentUsage();