@lotics/cli 0.44.0 → 0.44.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,15 +1,18 @@
1
1
  import { LoticsClient } from "./client.js";
2
2
  /**
3
3
  * Manifest declaration for one workflow alias:
4
- * `"alias": { workflow_id, inputs?: { key: { type, … } } }`
4
+ * `"alias": { workflow_id, inputs?: { key: { type, … } }, outputs?: { key: { type, … } } }`
5
5
  *
6
6
  * When `inputs` is declared, the CLI codegen emits a typed `AppWorkflows[alias]`
7
- * entry and the server validates payloads against it at execute time. Omit
8
- * `inputs` for workflows that accept no typed inputs.
7
+ * entry and the server validates payloads against it at execute time. When
8
+ * `outputs` is declared, codegen emits a typed `AppWorkflowResults[alias]` so the
9
+ * app reads a typed `result.data`. Omit either for workflows that take no typed
10
+ * inputs / return no structured data.
9
11
  */
10
12
  export type AppWorkflowDeclaration = {
11
13
  workflow_id: string;
12
14
  inputs?: Record<string, unknown>;
15
+ outputs?: Record<string, unknown>;
13
16
  };
14
17
  /**
15
18
  * Manifest declaration for one named query:
@@ -27,4 +27,11 @@ export declare function generateAppWorkflowsDts(workflows: Record<string, AppWor
27
27
  * catches structurally-invalid declarations before they reach us.
28
28
  */
29
29
  export declare function inputsToType(inputs: Record<string, unknown>): string;
30
+ /**
31
+ * Map an output `fields` map (also the top-level `outputs`) to a TS object type.
32
+ * Mirrors the backend `outputObjectToTsType`; recursive for nested object/array.
33
+ * Zero-dep like `inputsToType` — pattern-matches on `type`, falls through to
34
+ * `unknown` for shapes the server's schema parse rejects before we see them.
35
+ */
36
+ export declare function objectFieldsToType(fields: Record<string, unknown>): string;
30
37
  export declare function isValidIdentifier(name: string): boolean;
@@ -34,22 +34,32 @@ declare module "@lotics/app-sdk" {
34
34
  }
35
35
  // Sort alphabetically for deterministic output across regenerations.
36
36
  entries.sort(([a], [b]) => a.localeCompare(b));
37
- const lines = [];
37
+ const inputLines = [];
38
+ const resultLines = [];
38
39
  for (const [alias, declaration] of entries) {
39
- // declaration is always `{ workflow_id, inputs? }` per manifest schema.
40
- // Workflows with no typed inputs omit `inputs:` and get an untyped
41
- // `Record<string, unknown>` callable surface.
40
+ // declaration is `{ workflow_id, inputs?, outputs? }` per manifest schema.
41
+ // No typed inputs untyped `Record<string, unknown>` callable. An alias with
42
+ // declared `outputs` also gets an `AppWorkflowResults` entry → typed result.data.
42
43
  const valueType = declaration.inputs
43
44
  ? inputsToType(declaration.inputs)
44
45
  : "Record<string, unknown>";
45
46
  const aliasKey = isValidIdentifier(alias) ? alias : JSON.stringify(alias);
46
- lines.push(` ${aliasKey}: ${valueType};`);
47
+ inputLines.push(` ${aliasKey}: ${valueType};`);
48
+ if (declaration.outputs) {
49
+ resultLines.push(` ${aliasKey}: ${objectFieldsToType(declaration.outputs)};`);
50
+ }
47
51
  }
52
+ const resultsBlock = resultLines.length > 0
53
+ ? `
54
+ interface AppWorkflowResults {
55
+ ${resultLines.join("\n")}
56
+ }`
57
+ : "";
48
58
  return `${HEADER}
49
59
  declare module "@lotics/app-sdk" {
50
60
  interface AppWorkflows {
51
- ${lines.join("\n")}
52
- }
61
+ ${inputLines.join("\n")}
62
+ }${resultsBlock}
53
63
  }
54
64
  `;
55
65
  }
@@ -119,6 +129,68 @@ function inputDeclToTsType(decl) {
119
129
  return "unknown";
120
130
  }
121
131
  }
132
+ /**
133
+ * Map an output `fields` map (also the top-level `outputs`) to a TS object type.
134
+ * Mirrors the backend `outputObjectToTsType`; recursive for nested object/array.
135
+ * Zero-dep like `inputsToType` — pattern-matches on `type`, falls through to
136
+ * `unknown` for shapes the server's schema parse rejects before we see them.
137
+ */
138
+ export function objectFieldsToType(fields) {
139
+ const parts = [];
140
+ for (const [key, decl] of Object.entries(fields)) {
141
+ if (decl === null || typeof decl !== "object")
142
+ continue;
143
+ const d = decl;
144
+ const optional = d.required === false ? "?" : "";
145
+ const fieldKey = isValidIdentifier(key) ? key : JSON.stringify(key);
146
+ parts.push(`${fieldKey}${optional}: ${outputDeclToTsType(d)}`);
147
+ }
148
+ if (parts.length === 0)
149
+ return "Record<string, never>";
150
+ return `{ ${parts.join("; ")} }`;
151
+ }
152
+ function outputDeclToTsType(decl) {
153
+ const type = decl.type;
154
+ switch (type) {
155
+ case "text":
156
+ case "email":
157
+ case "date":
158
+ case "datetime":
159
+ return "string";
160
+ case "number":
161
+ return "number";
162
+ case "boolean":
163
+ return "boolean";
164
+ case "record_link":
165
+ return decl.multi === true ? "ReadonlyArray<string>" : "string";
166
+ case "select": {
167
+ const options = Array.isArray(decl.options) ? decl.options : [];
168
+ const literals = options
169
+ .map((o) => o !== null && typeof o === "object" && "value" in o && typeof o.value === "string"
170
+ ? JSON.stringify(o.value)
171
+ : null)
172
+ .filter((v) => v !== null);
173
+ const inner = literals.length > 0 ? literals.join(" | ") : "string";
174
+ return decl.multi === true ? `ReadonlyArray<${inner}>` : inner;
175
+ }
176
+ case "object": {
177
+ const fields = decl.fields !== null && typeof decl.fields === "object"
178
+ ? decl.fields
179
+ : {};
180
+ return objectFieldsToType(fields);
181
+ }
182
+ case "array": {
183
+ const items = decl.items !== null && typeof decl.items === "object"
184
+ ? decl.items
185
+ : null;
186
+ return items ? `ReadonlyArray<${outputDeclToTsType(items)}>` : "ReadonlyArray<unknown>";
187
+ }
188
+ case "json":
189
+ return "unknown";
190
+ default:
191
+ return "unknown";
192
+ }
193
+ }
122
194
  const IDENTIFIER_REGEX = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/;
123
195
  export function isValidIdentifier(name) {
124
196
  return IDENTIFIER_REGEX.test(name);
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,47 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { generateAppWorkflowsDts } from "./generate_app_workflows_dts.js";
3
+ describe("generateAppWorkflowsDts", () => {
4
+ it("emits no AppWorkflowResults block when no alias declares outputs", () => {
5
+ const dts = generateAppWorkflowsDts({
6
+ issue: { workflow_id: "wfl_1", inputs: { record_id: { type: "text" } } },
7
+ });
8
+ expect(dts).toContain("interface AppWorkflows {");
9
+ expect(dts).toContain("issue: {\n record_id: string;\n };");
10
+ expect(dts).not.toContain("AppWorkflowResults");
11
+ });
12
+ it("emits a typed AppWorkflowResults entry for an alias with a nested outputs schema", () => {
13
+ const dts = generateAppWorkflowsDts({
14
+ computeQuote: {
15
+ workflow_id: "wfl_2",
16
+ outputs: {
17
+ total: { type: "number" },
18
+ note: { type: "text", required: false },
19
+ lines: {
20
+ type: "array",
21
+ items: {
22
+ type: "object",
23
+ fields: { name: { type: "text" }, amount: { type: "number" } },
24
+ },
25
+ },
26
+ },
27
+ },
28
+ });
29
+ expect(dts).toContain("interface AppWorkflowResults {");
30
+ // Nested array-of-object + optional field render correctly.
31
+ expect(dts).toContain("computeQuote: { total: number; note?: string; lines: ReadonlyArray<{ name: string; amount: number }> };");
32
+ // No declared inputs → untyped callable in AppWorkflows.
33
+ expect(dts).toContain("computeQuote: Record<string, unknown>;");
34
+ });
35
+ it("maps record_link/select outputs the same way as inputs", () => {
36
+ const dts = generateAppWorkflowsDts({
37
+ lookup: {
38
+ workflow_id: "wfl_3",
39
+ outputs: {
40
+ owner: { type: "record_link", table_id: "tbl_x" },
41
+ tags: { type: "select", options: [{ label: "A", value: "a" }, { label: "B", value: "b" }], multi: true },
42
+ },
43
+ },
44
+ });
45
+ expect(dts).toContain('owner: string; tags: ReadonlyArray<"a" | "b">');
46
+ });
47
+ });
package/dist/src/cli.js CHANGED
@@ -30322,6 +30322,14 @@ export default defineConfig({
30322
30322
  dedupe: ["react", "react-dom", "react-native-web"],
30323
30323
  },
30324
30324
  optimizeDeps: {
30325
+ // Force-prebundle react-native-web: it does \`import normalizeColor from
30326
+ // "@react-native/normalize-colors"\` (a nested CJS module). Un-prebundled,
30327
+ // Vite's dev server serves that as ESM with no default export \u2192 "does not
30328
+ // provide an export named 'default'" and a blank iframe the moment a
30329
+ // colour-touching RN-Web component (DatePicker, Picker) mounts under
30330
+ // \`lotics app dev\`. Prebundling folds its CJS deps into one interop'd chunk.
30331
+ // The rollup build handles the interop on its own, so this is dev-only.
30332
+ include: ["react-native-web"],
30325
30333
  // The dep optimizer pre-bundles deps with a SEPARATE esbuild pass that
30326
30334
  // top-level \`define\` doesn't always reach, so a pre-bundled RN dep can
30327
30335
  // still hit \`__DEV__ is not defined\` under \`lotics app dev\`. Define it
@@ -30352,6 +30360,25 @@ export default defineConfig({
30352
30360
  },
30353
30361
  test: {
30354
30362
  environment: "jsdom",
30363
+ // RN packages ship Flow (\`import typeof\`) in their native source, reached
30364
+ // transitively by RN-Web components (pickers, calendars, anything touching
30365
+ // Animated). Vitest's web optimizer is OFF by default and ignores the
30366
+ // \`optimizeDeps.resolveExtensions\` above, so it pre-bundles the native
30367
+ // \`.js\` / \`src\` Flow files and esbuild can't parse them. Enable it with the
30368
+ // same \`.web.js\`-first resolution so it bundles the compiled web variants
30369
+ // (Picker.web.js, RN-Web dist) \u2014 without this, any test that renders a tree
30370
+ // pulling a Picker / DatePicker fails with "Unexpected token 'typeof'".
30371
+ deps: {
30372
+ optimizer: {
30373
+ web: {
30374
+ enabled: true,
30375
+ include: ["react-native", "react-native-web", "@react-native-picker/picker"],
30376
+ esbuildOptions: {
30377
+ resolveExtensions: [".web.tsx", ".web.ts", ".web.js", ".tsx", ".ts", ".jsx", ".js", ".json"],
30378
+ },
30379
+ },
30380
+ },
30381
+ },
30355
30382
  },
30356
30383
  });
30357
30384
  `
@@ -31078,17 +31105,25 @@ declare module "@lotics/app-sdk" {
31078
31105
  `;
31079
31106
  }
31080
31107
  entries.sort(([a], [b]) => a.localeCompare(b));
31081
- const lines = [];
31108
+ const inputLines = [];
31109
+ const resultLines = [];
31082
31110
  for (const [alias, declaration] of entries) {
31083
31111
  const valueType = declaration.inputs ? inputsToType(declaration.inputs) : "Record<string, unknown>";
31084
31112
  const aliasKey = isValidIdentifier(alias) ? alias : JSON.stringify(alias);
31085
- lines.push(` ${aliasKey}: ${valueType};`);
31113
+ inputLines.push(` ${aliasKey}: ${valueType};`);
31114
+ if (declaration.outputs) {
31115
+ resultLines.push(` ${aliasKey}: ${objectFieldsToType(declaration.outputs)};`);
31116
+ }
31086
31117
  }
31118
+ const resultsBlock = resultLines.length > 0 ? `
31119
+ interface AppWorkflowResults {
31120
+ ${resultLines.join("\n")}
31121
+ }` : "";
31087
31122
  return `${HEADER}
31088
31123
  declare module "@lotics/app-sdk" {
31089
31124
  interface AppWorkflows {
31090
- ${lines.join("\n")}
31091
- }
31125
+ ${inputLines.join("\n")}
31126
+ }${resultsBlock}
31092
31127
  }
31093
31128
  `;
31094
31129
  }
@@ -31145,6 +31180,54 @@ function inputDeclToTsType(decl) {
31145
31180
  return "unknown";
31146
31181
  }
31147
31182
  }
31183
+ function objectFieldsToType(fields) {
31184
+ const parts = [];
31185
+ for (const [key, decl] of Object.entries(fields)) {
31186
+ if (decl === null || typeof decl !== "object") continue;
31187
+ const d = decl;
31188
+ const optional = d.required === false ? "?" : "";
31189
+ const fieldKey = isValidIdentifier(key) ? key : JSON.stringify(key);
31190
+ parts.push(`${fieldKey}${optional}: ${outputDeclToTsType(d)}`);
31191
+ }
31192
+ if (parts.length === 0) return "Record<string, never>";
31193
+ return `{ ${parts.join("; ")} }`;
31194
+ }
31195
+ function outputDeclToTsType(decl) {
31196
+ const type = decl.type;
31197
+ switch (type) {
31198
+ case "text":
31199
+ case "email":
31200
+ case "date":
31201
+ case "datetime":
31202
+ return "string";
31203
+ case "number":
31204
+ return "number";
31205
+ case "boolean":
31206
+ return "boolean";
31207
+ case "record_link":
31208
+ return decl.multi === true ? "ReadonlyArray<string>" : "string";
31209
+ case "select": {
31210
+ const options = Array.isArray(decl.options) ? decl.options : [];
31211
+ const literals = options.map(
31212
+ (o) => o !== null && typeof o === "object" && "value" in o && typeof o.value === "string" ? JSON.stringify(o.value) : null
31213
+ ).filter((v) => v !== null);
31214
+ const inner = literals.length > 0 ? literals.join(" | ") : "string";
31215
+ return decl.multi === true ? `ReadonlyArray<${inner}>` : inner;
31216
+ }
31217
+ case "object": {
31218
+ const fields = decl.fields !== null && typeof decl.fields === "object" ? decl.fields : {};
31219
+ return objectFieldsToType(fields);
31220
+ }
31221
+ case "array": {
31222
+ const items = decl.items !== null && typeof decl.items === "object" ? decl.items : null;
31223
+ return items ? `ReadonlyArray<${outputDeclToTsType(items)}>` : "ReadonlyArray<unknown>";
31224
+ }
31225
+ case "json":
31226
+ return "unknown";
31227
+ default:
31228
+ return "unknown";
31229
+ }
31230
+ }
31148
31231
  var IDENTIFIER_REGEX = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/;
31149
31232
  function isValidIdentifier(name) {
31150
31233
  return IDENTIFIER_REGEX.test(name);
@@ -184,6 +184,14 @@ export default defineConfig({
184
184
  dedupe: ["react", "react-dom", "react-native-web"],
185
185
  },
186
186
  optimizeDeps: {
187
+ // Force-prebundle react-native-web: it does \`import normalizeColor from
188
+ // "@react-native/normalize-colors"\` (a nested CJS module). Un-prebundled,
189
+ // Vite's dev server serves that as ESM with no default export → "does not
190
+ // provide an export named 'default'" and a blank iframe the moment a
191
+ // colour-touching RN-Web component (DatePicker, Picker) mounts under
192
+ // \`lotics app dev\`. Prebundling folds its CJS deps into one interop'd chunk.
193
+ // The rollup build handles the interop on its own, so this is dev-only.
194
+ include: ["react-native-web"],
187
195
  // The dep optimizer pre-bundles deps with a SEPARATE esbuild pass that
188
196
  // top-level \`define\` doesn't always reach, so a pre-bundled RN dep can
189
197
  // still hit \`__DEV__ is not defined\` under \`lotics app dev\`. Define it
@@ -214,6 +222,25 @@ export default defineConfig({
214
222
  },
215
223
  test: {
216
224
  environment: "jsdom",
225
+ // RN packages ship Flow (\`import typeof\`) in their native source, reached
226
+ // transitively by RN-Web components (pickers, calendars, anything touching
227
+ // Animated). Vitest's web optimizer is OFF by default and ignores the
228
+ // \`optimizeDeps.resolveExtensions\` above, so it pre-bundles the native
229
+ // \`.js\` / \`src\` Flow files and esbuild can't parse them. Enable it with the
230
+ // same \`.web.js\`-first resolution so it bundles the compiled web variants
231
+ // (Picker.web.js, RN-Web dist) — without this, any test that renders a tree
232
+ // pulling a Picker / DatePicker fails with "Unexpected token 'typeof'".
233
+ deps: {
234
+ optimizer: {
235
+ web: {
236
+ enabled: true,
237
+ include: ["react-native", "react-native-web", "@react-native-picker/picker"],
238
+ esbuildOptions: {
239
+ resolveExtensions: [".web.tsx", ".web.ts", ".web.js", ".tsx", ".ts", ".jsx", ".js", ".json"],
240
+ },
241
+ },
242
+ },
243
+ },
217
244
  },
218
245
  });
219
246
  `,
@@ -18,6 +18,14 @@ describe("buildStarterTemplate", () => {
18
18
  expect(config).toContain('__DEV__: "false"');
19
19
  expect(config).toContain('".web.js"');
20
20
  });
21
+ test("vite.config.ts force-prebundles react-native-web (else a DatePicker/Picker blanks the dev iframe)", () => {
22
+ // RN-Web does `import normalizeColor from "@react-native/normalize-colors"`
23
+ // (nested CJS). Un-prebundled, the dev server serves it as ESM with no
24
+ // default → "does not provide an export named 'default'" + blank iframe the
25
+ // moment a colour-touching component mounts. Prebundling interops it.
26
+ const config = fileNamed(buildStarterTemplate(baseArgs), "vite.config.ts");
27
+ expect(config).toMatch(/include:\s*\[[^\]]*"react-native-web"/);
28
+ });
21
29
  test("vite.config.ts defines the RN __DEV__ global (else the bundle throws at load → blank iframe)", () => {
22
30
  // `__DEV__` is a Metro-injected global that RN-ecosystem deps reference at
23
31
  // module-eval time (e.g. @react-native-picker's UnimplementedView, pulled in
@@ -31,6 +39,18 @@ describe("buildStarterTemplate", () => {
31
39
  // deps under `lotics app dev`).
32
40
  expect(config).toMatch(/esbuildOptions:\s*\{\s*define:\s*\{\s*__DEV__:\s*"false"/);
33
41
  });
42
+ test("vite.config.ts enables the vitest web optimizer with .web.js resolution (else tests rendering a Picker/DatePicker fail on Flow)", () => {
43
+ // Vitest pre-bundles deps with its OWN esbuild optimizer, which is OFF by
44
+ // default and ignores the top-level `optimizeDeps.resolveExtensions`. Without
45
+ // it enabled + given `.web.js` first, it grabs react-native's native `.js` /
46
+ // src Flow files (`import typeof`) that esbuild can't parse — so any test that
47
+ // renders a tree pulling a Picker/DatePicker dies with "Unexpected token 'typeof'".
48
+ const config = fileNamed(buildStarterTemplate(baseArgs), "vite.config.ts");
49
+ expect(config).toMatch(/test:\s*\{[\s\S]*environment:\s*"jsdom"/);
50
+ expect(config).toMatch(/optimizer:\s*\{\s*web:\s*\{\s*enabled:\s*true/);
51
+ // The web optimizer carries the same `.web.js`-first resolution as the build.
52
+ expect(config).toMatch(/web:\s*\{[\s\S]*resolveExtensions:[\s\S]*"\.web\.js"/);
53
+ });
34
54
  test("vite.config.ts maps the RN `global` to globalThis (else rn-web Animated throws when a spring is torn down)", () => {
35
55
  // react-native-web's Animated reads `global.cancelAnimationFrame` (free var)
36
56
  // when a spring animation is interrupted — e.g. toggling @lotics/ui's Switch.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lotics/cli",
3
- "version": "0.44.0",
3
+ "version": "0.44.1",
4
4
  "description": "Lotics SDK and CLI for AI agents",
5
5
  "type": "module",
6
6
  "bin": {