@lotics/cli 0.88.0 → 0.88.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.
Files changed (87) hide show
  1. package/dist/render_page.js +100 -100
  2. package/dist/src/cli.js +100 -100
  3. package/dist/{client.d.ts → src/client.d.ts} +1 -1
  4. package/dist/src/package_content_types.d.ts +70 -0
  5. package/dist/src/package_content_types.js +12 -0
  6. package/package.json +2 -2
  7. package/dist/app_commands.d.ts +0 -401
  8. package/dist/app_commands.js +0 -1520
  9. package/dist/app_commands.test.d.ts +0 -1
  10. package/dist/app_commands.test.js +0 -898
  11. package/dist/app_workflow_check.d.ts +0 -77
  12. package/dist/app_workflow_check.js +0 -169
  13. package/dist/app_workflow_check.test.d.ts +0 -1
  14. package/dist/app_workflow_check.test.js +0 -166
  15. package/dist/args.d.ts +0 -42
  16. package/dist/args.js +0 -121
  17. package/dist/args.test.d.ts +0 -1
  18. package/dist/args.test.js +0 -88
  19. package/dist/child_env.d.ts +0 -13
  20. package/dist/child_env.js +0 -24
  21. package/dist/cli.d.ts +0 -2
  22. package/dist/cli.js +0 -1095
  23. package/dist/cli_dispatch.test.d.ts +0 -1
  24. package/dist/cli_dispatch.test.js +0 -89
  25. package/dist/client.test.d.ts +0 -1
  26. package/dist/client.test.js +0 -47
  27. package/dist/config.d.ts +0 -124
  28. package/dist/config.js +0 -315
  29. package/dist/config.test.d.ts +0 -1
  30. package/dist/config.test.js +0 -319
  31. package/dist/dev/file_relay.d.ts +0 -39
  32. package/dist/dev/file_relay.js +0 -87
  33. package/dist/dev/file_relay.test.d.ts +0 -1
  34. package/dist/dev/file_relay.test.js +0 -87
  35. package/dist/dev/rpc_handler.d.ts +0 -21
  36. package/dist/dev/rpc_handler.js +0 -166
  37. package/dist/dev/rpc_handler.test.d.ts +0 -1
  38. package/dist/dev/rpc_handler.test.js +0 -107
  39. package/dist/dev/server.d.ts +0 -75
  40. package/dist/dev/server.js +0 -443
  41. package/dist/dev/upload_relay.d.ts +0 -36
  42. package/dist/dev/upload_relay.js +0 -61
  43. package/dist/dev/upload_relay.test.d.ts +0 -1
  44. package/dist/dev/upload_relay.test.js +0 -76
  45. package/dist/dev/wrapper_page.d.ts +0 -40
  46. package/dist/dev/wrapper_page.js +0 -335
  47. package/dist/docx.d.ts +0 -2
  48. package/dist/docx.js +0 -341
  49. package/dist/docx.test.d.ts +0 -1
  50. package/dist/docx.test.js +0 -145
  51. package/dist/file_command_io.d.ts +0 -12
  52. package/dist/file_command_io.js +0 -34
  53. package/dist/generate_app_agents_dts.d.ts +0 -16
  54. package/dist/generate_app_agents_dts.js +0 -62
  55. package/dist/generate_app_agents_dts.test.d.ts +0 -1
  56. package/dist/generate_app_agents_dts.test.js +0 -45
  57. package/dist/generate_app_fields.d.ts +0 -56
  58. package/dist/generate_app_fields.js +0 -148
  59. package/dist/generate_app_fields.test.d.ts +0 -1
  60. package/dist/generate_app_fields.test.js +0 -108
  61. package/dist/generate_app_queries_dts.d.ts +0 -21
  62. package/dist/generate_app_queries_dts.js +0 -57
  63. package/dist/generate_app_workflows_dts.d.ts +0 -39
  64. package/dist/generate_app_workflows_dts.js +0 -229
  65. package/dist/generate_app_workflows_dts.test.d.ts +0 -1
  66. package/dist/generate_app_workflows_dts.test.js +0 -109
  67. package/dist/generate_package_fields.d.ts +0 -49
  68. package/dist/generate_package_fields.js +0 -157
  69. package/dist/generate_package_fields.test.d.ts +0 -1
  70. package/dist/generate_package_fields.test.js +0 -59
  71. package/dist/inputs.d.ts +0 -38
  72. package/dist/inputs.js +0 -50
  73. package/dist/inputs.test.d.ts +0 -1
  74. package/dist/inputs.test.js +0 -89
  75. package/dist/preview.d.ts +0 -3
  76. package/dist/preview.js +0 -233
  77. package/dist/starter_template.d.ts +0 -64
  78. package/dist/starter_template.js +0 -753
  79. package/dist/starter_template.test.d.ts +0 -1
  80. package/dist/starter_template.test.js +0 -115
  81. package/dist/version.d.ts +0 -1
  82. package/dist/version.js +0 -6
  83. package/dist/xlsx.d.ts +0 -2
  84. package/dist/xlsx.js +0 -489
  85. package/dist/xlsx.test.d.ts +0 -1
  86. package/dist/xlsx.test.js +0 -131
  87. /package/dist/{client.js → src/client.js} +0 -0
@@ -1,57 +0,0 @@
1
- /**
2
- * Codegen: emit `.lotics/app_queries.d.ts` from the app's manifest.
3
- *
4
- * The starter tsconfig.json includes `.lotics/` in its `include` list, so this
5
- * file is auto-discovered by `tsc --noEmit` and the bundler. The augmentation
6
- * adds a typed entry to `AppQueries` for every alias declared in
7
- * `package.json#lotics.queries`, mapping each to its declared param shape:
8
- *
9
- * - `params` declared → `alias: { …declared shape… }`
10
- * - `params` omitted → `alias: Record<string, never>` (no params)
11
- *
12
- * Result: `useQuery("alias", params)` is typed against the manifest, and an
13
- * undeclared alias is a compile-time error.
14
- *
15
- * The typed-input → TS-type mappers are shared with the workflow codegen —
16
- * both surfaces use the same `AppWorkflowInput` vocabulary.
17
- *
18
- * Pure function. Same inputs → same output. Idempotent.
19
- */
20
- import { inputsToType, isValidIdentifier } from "./generate_app_workflows_dts.js";
21
- const HEADER = `// Auto-generated by 'lotics app pull/dev/deploy'.
22
- // DO NOT EDIT — regenerated from package.json#lotics.queries.
23
- //
24
- // This file gives \`useQuery("alias", params)\` typed params at call sites by
25
- // augmenting the @lotics/app-sdk \`AppQueries\` interface.
26
-
27
- import "@lotics/app-sdk";
28
- `;
29
- export function generateAppQueriesDts(queries) {
30
- const entries = Object.entries(queries ?? {});
31
- if (entries.length === 0) {
32
- return `${HEADER}
33
- // No queries declared in package.json#lotics.queries.
34
- // Add an entry to enable typed useQuery("alias", params) at call sites.
35
- declare module "@lotics/app-sdk" {
36
- interface AppQueries {}
37
- }
38
- `;
39
- }
40
- // Sort alphabetically for deterministic output across regenerations.
41
- entries.sort(([a], [b]) => a.localeCompare(b));
42
- const lines = [];
43
- for (const [alias, declaration] of entries) {
44
- // `inputsToType` returns `Record<string, never>` for an empty/absent
45
- // param schema — exactly the no-params placeholder `useQuery` expects.
46
- const valueType = inputsToType(declaration.params ?? {});
47
- const aliasKey = isValidIdentifier(alias) ? alias : JSON.stringify(alias);
48
- lines.push(` ${aliasKey}: ${valueType};`);
49
- }
50
- return `${HEADER}
51
- declare module "@lotics/app-sdk" {
52
- interface AppQueries {
53
- ${lines.join("\n")}
54
- }
55
- }
56
- `;
57
- }
@@ -1,39 +0,0 @@
1
- /**
2
- * Codegen: emit `.lotics/app_workflows.d.ts` from the app's manifest.
3
- *
4
- * The starter tsconfig.json includes `.lotics/` in its `include` list, so this
5
- * file is auto-discovered by `tsc --noEmit` and the bundler. The augmentation
6
- * adds a typed entry to `AppWorkflows` for every alias declared in
7
- * `package.json#lotics.workflows`:
8
- *
9
- * - Shorthand `"alias": "wfl_..."` → `alias: Record<string, unknown>`
10
- * (untyped — callsite accepts any payload)
11
- * - Full `"alias": { workflow_id, inputs }` → `alias: { …declared shape… }`
12
- * (typed — `useWorkflow<"alias">` returns a callable with shaped inputs)
13
- *
14
- * Pure function. Same inputs → same output. Idempotent.
15
- */
16
- import type { AppWorkflowDeclaration } from "./app_commands.js";
17
- export declare function generateAppWorkflowsDts(workflows: Record<string, AppWorkflowDeclaration> | undefined): string;
18
- /**
19
- * Map an `inputs: { key: AppWorkflowInput }` schema to a TS literal type
20
- * string. Mirrors the backend .d.ts generator's behavior — same vocabulary,
21
- * same conventions — so workflow body types and app call-site types agree.
22
- *
23
- * Note: `inputs` here is `Record<string, unknown>` rather than typed because
24
- * the CLI doesn't import zod or the apps schema (zero-dep policy in @lotics/sdk).
25
- * We pattern-match on the `type` field and let unknown shapes fall through
26
- * to `unknown` rather than throwing — the server's deploy-time schema parse
27
- * catches structurally-invalid declarations before they reach us.
28
- */
29
- export declare function inputsToType(inputs: Record<string, unknown>, opts?: {
30
- nullableOptional?: boolean;
31
- }): string;
32
- /**
33
- * Map an output `fields` map (also the top-level `outputs`) to a TS object type.
34
- * Mirrors the backend `outputObjectToTsType`; recursive for nested object/array.
35
- * Zero-dep like `inputsToType` — pattern-matches on `type`, falls through to
36
- * `unknown` for shapes the server's schema parse rejects before we see them.
37
- */
38
- export declare function objectFieldsToType(fields: Record<string, unknown>): string;
39
- export declare function isValidIdentifier(name: string): boolean;
@@ -1,229 +0,0 @@
1
- /**
2
- * Codegen: emit `.lotics/app_workflows.d.ts` from the app's manifest.
3
- *
4
- * The starter tsconfig.json includes `.lotics/` in its `include` list, so this
5
- * file is auto-discovered by `tsc --noEmit` and the bundler. The augmentation
6
- * adds a typed entry to `AppWorkflows` for every alias declared in
7
- * `package.json#lotics.workflows`:
8
- *
9
- * - Shorthand `"alias": "wfl_..."` → `alias: Record<string, unknown>`
10
- * (untyped — callsite accepts any payload)
11
- * - Full `"alias": { workflow_id, inputs }` → `alias: { …declared shape… }`
12
- * (typed — `useWorkflow<"alias">` returns a callable with shaped inputs)
13
- *
14
- * Pure function. Same inputs → same output. Idempotent.
15
- */
16
- const HEADER = `// Auto-generated by 'lotics app pull/dev/deploy'.
17
- // DO NOT EDIT — regenerated from package.json#lotics.workflows.
18
- //
19
- // This file gives \`useWorkflow("alias")\` a typed input parameter at call
20
- // sites by augmenting the @lotics/app-sdk \`AppWorkflows\` interface.
21
-
22
- import "@lotics/app-sdk";
23
- `;
24
- export function generateAppWorkflowsDts(workflows) {
25
- const entries = Object.entries(workflows ?? {});
26
- if (entries.length === 0) {
27
- return `${HEADER}
28
- // No workflows declared in package.json#lotics.workflows.
29
- // Add an entry to enable typed useWorkflow<"alias"> at call sites.
30
- declare module "@lotics/app-sdk" {
31
- interface AppWorkflows {}
32
- }
33
- `;
34
- }
35
- // Sort alphabetically for deterministic output across regenerations.
36
- entries.sort(([a], [b]) => a.localeCompare(b));
37
- const inputLines = [];
38
- const resultLines = [];
39
- for (const [alias, declaration] of entries) {
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.
43
- const valueType = declaration.inputs
44
- ? inputsToType(declaration.inputs, { nullableOptional: true })
45
- : "Record<string, unknown>";
46
- const aliasKey = isValidIdentifier(alias) ? alias : JSON.stringify(alias);
47
- inputLines.push(` ${aliasKey}: ${valueType};`);
48
- if (declaration.outputs) {
49
- resultLines.push(` ${aliasKey}: ${objectFieldsToType(declaration.outputs)};`);
50
- }
51
- }
52
- const resultsBlock = resultLines.length > 0
53
- ? `
54
- interface AppWorkflowResults {
55
- ${resultLines.join("\n")}
56
- }`
57
- : "";
58
- return `${HEADER}
59
- declare module "@lotics/app-sdk" {
60
- interface AppWorkflows {
61
- ${inputLines.join("\n")}
62
- }${resultsBlock}
63
- }
64
- `;
65
- }
66
- /**
67
- * Map an `inputs: { key: AppWorkflowInput }` schema to a TS literal type
68
- * string. Mirrors the backend .d.ts generator's behavior — same vocabulary,
69
- * same conventions — so workflow body types and app call-site types agree.
70
- *
71
- * Note: `inputs` here is `Record<string, unknown>` rather than typed because
72
- * the CLI doesn't import zod or the apps schema (zero-dep policy in @lotics/sdk).
73
- * We pattern-match on the `type` field and let unknown shapes fall through
74
- * to `unknown` rather than throwing — the server's deploy-time schema parse
75
- * catches structurally-invalid declarations before they reach us.
76
- */
77
- export function inputsToType(inputs, opts) {
78
- // A WORKFLOW optional input also accepts `null` — the diff-write "clear this
79
- // field" signal (the backend validates it, the body forwards it to
80
- // `update_records({ set: { fld: null } })`). So `useWorkflow` call sites can
81
- // pass `null` to unset a field without a loose `Record<string, unknown>` cast.
82
- // Query params + agent inputs have no "clear", so they stay `T` (the caller
83
- // omits the flag). Only the TOP level is nullable: a nested object's optional
84
- // field mirrors the backend's plain `.optional()` (the recursive call below
85
- // omits the flag), so the type never claims a nested clear the server rejects.
86
- const nullableOptional = opts?.nullableOptional === true;
87
- const fields = [];
88
- for (const [key, decl] of Object.entries(inputs)) {
89
- if (decl === null || typeof decl !== "object")
90
- continue;
91
- const d = decl;
92
- const tsType = inputDeclToTsType(d);
93
- const isOptional = d.required === false;
94
- const fieldType = isOptional && nullableOptional ? `${tsType} | null` : tsType;
95
- const optional = isOptional ? "?" : "";
96
- const fieldKey = isValidIdentifier(key) ? key : JSON.stringify(key);
97
- fields.push(` ${fieldKey}${optional}: ${fieldType};`);
98
- }
99
- if (fields.length === 0)
100
- return "Record<string, never>";
101
- return `{\n${fields.join("\n")}\n }`;
102
- }
103
- function inputDeclToTsType(decl) {
104
- const type = decl.type;
105
- switch (type) {
106
- case "text":
107
- case "email":
108
- case "date":
109
- case "datetime":
110
- return "string";
111
- case "number":
112
- return "number";
113
- case "boolean":
114
- return "boolean";
115
- case "record_link":
116
- case "member": {
117
- const inner = "string";
118
- return decl.multi === true ? `ReadonlyArray<${inner}>` : inner;
119
- }
120
- case "select": {
121
- const options = Array.isArray(decl.options) ? decl.options : [];
122
- const literals = options
123
- .map((o) => {
124
- if (o !== null && typeof o === "object" && "value" in o && typeof o.value === "string") {
125
- return JSON.stringify(o.value);
126
- }
127
- return null;
128
- })
129
- .filter((v) => v !== null);
130
- // The literal union keeps autocomplete for hand-typed ids, but a package
131
- // app addresses options through the runtime-resolved OPT map (typed
132
- // string — consumer workspaces bind DIFFERENT concrete ids), so the
133
- // input must also accept string. `(string & {})` widens without
134
- // collapsing the union in intellisense.
135
- const inner = literals.length > 0 ? `${literals.join(" | ")} | (string & {})` : "string";
136
- return decl.multi === true ? `ReadonlyArray<${inner}>` : inner;
137
- }
138
- case "date_range":
139
- return "{ start: string; end: string }";
140
- case "file":
141
- return decl.multi === true ? "ReadonlyArray<string>" : "string";
142
- case "object": {
143
- const fields = decl.fields !== null && typeof decl.fields === "object"
144
- ? decl.fields
145
- : {};
146
- return inputsToType(fields);
147
- }
148
- case "array": {
149
- const items = decl.items !== null && typeof decl.items === "object"
150
- ? decl.items
151
- : null;
152
- return items ? `ReadonlyArray<${inputDeclToTsType(items)}>` : "ReadonlyArray<unknown>";
153
- }
154
- case "json":
155
- return "unknown";
156
- default:
157
- return "unknown";
158
- }
159
- }
160
- /**
161
- * Map an output `fields` map (also the top-level `outputs`) to a TS object type.
162
- * Mirrors the backend `outputObjectToTsType`; recursive for nested object/array.
163
- * Zero-dep like `inputsToType` — pattern-matches on `type`, falls through to
164
- * `unknown` for shapes the server's schema parse rejects before we see them.
165
- */
166
- export function objectFieldsToType(fields) {
167
- const parts = [];
168
- for (const [key, decl] of Object.entries(fields)) {
169
- if (decl === null || typeof decl !== "object")
170
- continue;
171
- const d = decl;
172
- const optional = d.required === false ? "?" : "";
173
- const fieldKey = isValidIdentifier(key) ? key : JSON.stringify(key);
174
- parts.push(`${fieldKey}${optional}: ${outputDeclToTsType(d)}`);
175
- }
176
- if (parts.length === 0)
177
- return "Record<string, never>";
178
- return `{ ${parts.join("; ")} }`;
179
- }
180
- function outputDeclToTsType(decl) {
181
- const type = decl.type;
182
- switch (type) {
183
- case "text":
184
- case "email":
185
- case "date":
186
- case "datetime":
187
- return "string";
188
- case "number":
189
- return "number";
190
- case "boolean":
191
- return "boolean";
192
- case "record_link":
193
- return decl.multi === true ? "ReadonlyArray<string>" : "string";
194
- case "select": {
195
- const options = Array.isArray(decl.options) ? decl.options : [];
196
- const literals = options
197
- .map((o) => o !== null && typeof o === "object" && "value" in o && typeof o.value === "string"
198
- ? JSON.stringify(o.value)
199
- : null)
200
- .filter((v) => v !== null);
201
- // Same widening on the READ side: a package app's runtime output value is
202
- // the CONSUMER workspace's concrete opt_ id, not the origin's literal —
203
- // exhaustive narrowing over origin literals would be unsound. `(string & {})`
204
- // keeps autocomplete without lying about the value space.
205
- const inner = literals.length > 0 ? `${literals.join(" | ")} | (string & {})` : "string";
206
- return decl.multi === true ? `ReadonlyArray<${inner}>` : inner;
207
- }
208
- case "object": {
209
- const fields = decl.fields !== null && typeof decl.fields === "object"
210
- ? decl.fields
211
- : {};
212
- return objectFieldsToType(fields);
213
- }
214
- case "array": {
215
- const items = decl.items !== null && typeof decl.items === "object"
216
- ? decl.items
217
- : null;
218
- return items ? `ReadonlyArray<${outputDeclToTsType(items)}>` : "ReadonlyArray<unknown>";
219
- }
220
- case "json":
221
- return "unknown";
222
- default:
223
- return "unknown";
224
- }
225
- }
226
- const IDENTIFIER_REGEX = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/;
227
- export function isValidIdentifier(name) {
228
- return IDENTIFIER_REGEX.test(name);
229
- }
@@ -1 +0,0 @@
1
- export {};
@@ -1,109 +0,0 @@
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("types an array-of-objects INPUT as ReadonlyArray<{…}>", () => {
36
- const dts = generateAppWorkflowsDts({
37
- importLines: {
38
- workflow_id: "wfl_4",
39
- inputs: {
40
- lines: {
41
- type: "array",
42
- items: {
43
- type: "object",
44
- fields: {
45
- sku: { type: "text" },
46
- qty: { type: "number" },
47
- note: { type: "text", required: false },
48
- ref: { type: "record_link", table_id: "tbl_x" },
49
- },
50
- },
51
- },
52
- },
53
- },
54
- });
55
- expect(dts).toContain("lines: ReadonlyArray<{");
56
- expect(dts).toContain("sku: string;");
57
- expect(dts).toContain("qty: number;");
58
- expect(dts).toContain("note?: string;");
59
- expect(dts).toContain("ref: string;");
60
- });
61
- it("types a TOP-LEVEL optional workflow input as `T | null` (the diff-write clear signal), required stays `T`, nested stays plain optional", () => {
62
- const dts = generateAppWorkflowsDts({
63
- updateTask: {
64
- workflow_id: "wfl_u",
65
- inputs: {
66
- record_id: { type: "record_link", table_id: "tbl_x" },
67
- due: { type: "date", required: false },
68
- status: { type: "select", required: false, options: [{ label: "Done", value: "opt_done" }] },
69
- tags: { type: "select", multi: true, required: false, options: [{ label: "A", value: "opt_a" }] },
70
- meta: {
71
- type: "object",
72
- required: false,
73
- fields: { note: { type: "text", required: false } },
74
- },
75
- },
76
- },
77
- });
78
- expect(dts).toContain("record_id: string;"); // required → NOT nullable
79
- expect(dts).toContain("due?: string | null;"); // optional scalar → clearable
80
- expect(dts).toContain('status?: "opt_done" | (string & {}) | null;');
81
- expect(dts).toContain('tags?: ReadonlyArray<"opt_a" | (string & {})> | null;'); // optional multi
82
- // The object itself is a top-level optional → nullable; its NESTED optional
83
- // field mirrors the backend's plain `.optional()` (no `| null`).
84
- expect(dts).toContain("note?: string;");
85
- expect(dts).not.toContain("note?: string | null;");
86
- });
87
- it("maps record_link/select outputs the same way as inputs", () => {
88
- const dts = generateAppWorkflowsDts({
89
- lookup: {
90
- workflow_id: "wfl_3",
91
- outputs: {
92
- owner: { type: "record_link", table_id: "tbl_x" },
93
- tags: { type: "select", options: [{ label: "A", value: "a" }, { label: "B", value: "b" }], multi: true },
94
- },
95
- },
96
- });
97
- expect(dts).toContain('owner: string; tags: ReadonlyArray<"a" | "b" | (string & {})>');
98
- });
99
- it("types a multi file input as ReadonlyArray<string>, single file as string", () => {
100
- const dts = generateAppWorkflowsDts({
101
- attach: {
102
- workflow_id: "wfl_f",
103
- inputs: { one: { type: "file" }, many: { type: "file", multi: true } },
104
- },
105
- });
106
- expect(dts).toContain("one: string");
107
- expect(dts).toContain("many: ReadonlyArray<string>");
108
- });
109
- });
@@ -1,49 +0,0 @@
1
- /**
2
- * Codegen: emit a RUNTIME `.lotics/app_fields.ts` for a PUBLISHED / linked app
3
- * (a package installation — its own published origin, or an install of another
4
- * org's package) from that installation's LIVE binding.
5
- *
6
- * The bespoke twin (`generate_app_fields.ts`) bakes THIS workspace's concrete
7
- * `fld_…`/`opt_…` ids at codegen time — correct when source and workspace
8
- * co-live, fundamentally wrong for a linked app (the source is portable and
9
- * installs into many workspaces, each with different ids). A linked app instead
10
- * resolves the SAME `F` / `OPT` / `ROLE` surface at MODULE LOAD from the
11
- * installation's binding via the SDK's `getAppBinding()` (the `binding` RPC op),
12
- * using top-level await — the ESM graph waits for the binding before any
13
- * importer evaluates, so `F.TASKS.title` is a plain string everywhere, including
14
- * module-top-level constants. A published origin's source is portable by
15
- * construction: same import path, same `F`/`OPT` shapes the bespoke codegen
16
- * emitted (contract aliases derive from the same display names it slugifies), so
17
- * the deployed source compiles unchanged, resolved per-install.
18
- *
19
- * The generator sources the alias SET from the app's LIVE binding (the `binding`
20
- * RPC / `appBinding` client method), NOT a contract file: every bound field
21
- * (`entity.field`), option (`entity.field:opt`), and role becomes an `F` / `OPT`
22
- * / `ROLE` entry keyed by its stable alias.
23
- *
24
- * A missing alias fails LOUD at boot (the binding is install-verified complete,
25
- * so a miss means the generated file is stale relative to the installed contract
26
- * version) — never a silent undefined key in a write.
27
- *
28
- * Requires the app build to target es2022+ (the starter's `build.target`) —
29
- * top-level await does not exist below it.
30
- */
31
- /**
32
- * A materialized app installation's binding — the exact shape the `binding` RPC
33
- * / `client.appBinding()` returns (a subset of the full package binding: only
34
- * the maps the client-side `F`/`OPT`/`ROLE` surface resolves through). Keys are
35
- * fully-qualified contract aliases: `fields` keyed `entity.field`, `options`
36
- * keyed `entity.field:opt`, `roles` keyed by role alias.
37
- */
38
- export interface AppFieldsBinding {
39
- fields: Record<string, string>;
40
- options: Record<string, string>;
41
- roles: Record<string, string>;
42
- }
43
- /**
44
- * Generate the full binding-resolved `.lotics/app_fields.ts` source from an
45
- * installation's live binding. Pure — same binding → same bytes. Idempotent.
46
- * An empty binding yields valid, empty `F`/`OPT`/`ROLE` maps so the file always
47
- * compiles and imports resolve.
48
- */
49
- export declare function generatePackageAppFields(binding: AppFieldsBinding): string;
@@ -1,157 +0,0 @@
1
- /**
2
- * Codegen: emit a RUNTIME `.lotics/app_fields.ts` for a PUBLISHED / linked app
3
- * (a package installation — its own published origin, or an install of another
4
- * org's package) from that installation's LIVE binding.
5
- *
6
- * The bespoke twin (`generate_app_fields.ts`) bakes THIS workspace's concrete
7
- * `fld_…`/`opt_…` ids at codegen time — correct when source and workspace
8
- * co-live, fundamentally wrong for a linked app (the source is portable and
9
- * installs into many workspaces, each with different ids). A linked app instead
10
- * resolves the SAME `F` / `OPT` / `ROLE` surface at MODULE LOAD from the
11
- * installation's binding via the SDK's `getAppBinding()` (the `binding` RPC op),
12
- * using top-level await — the ESM graph waits for the binding before any
13
- * importer evaluates, so `F.TASKS.title` is a plain string everywhere, including
14
- * module-top-level constants. A published origin's source is portable by
15
- * construction: same import path, same `F`/`OPT` shapes the bespoke codegen
16
- * emitted (contract aliases derive from the same display names it slugifies), so
17
- * the deployed source compiles unchanged, resolved per-install.
18
- *
19
- * The generator sources the alias SET from the app's LIVE binding (the `binding`
20
- * RPC / `appBinding` client method), NOT a contract file: every bound field
21
- * (`entity.field`), option (`entity.field:opt`), and role becomes an `F` / `OPT`
22
- * / `ROLE` entry keyed by its stable alias.
23
- *
24
- * A missing alias fails LOUD at boot (the binding is install-verified complete,
25
- * so a miss means the generated file is stale relative to the installed contract
26
- * version) — never a silent undefined key in a write.
27
- *
28
- * Requires the app build to target es2022+ (the starter's `build.target`) —
29
- * top-level await does not exist below it.
30
- */
31
- import { propKey } from "./generate_app_fields.js";
32
- const HEADER = `// Auto-generated by 'lotics app codegen' (linked/published app; also app pull/dev/deploy).
33
- // DO NOT EDIT — regenerated from the installation's live binding.
34
- //
35
- // A package installation resolves F/OPT/ROLE at MODULE LOAD from its binding
36
- // (contract alias → THIS workspace's concrete id) via the SDK's \`binding\` RPC.
37
- // Top-level await: the module graph waits for the binding before any importer
38
- // evaluates, so every entry is a plain string.
39
- import { getAppBinding } from "@lotics/app-sdk";
40
-
41
- const binding = await getAppBinding();
42
-
43
- function bound(map: Record<string, string>, key: string, kind: string): string {
44
- const id = map[key];
45
- if (id === undefined) {
46
- throw new Error(
47
- \`app_fields: \${kind} "\${key}" is not in this installation's binding — \` +
48
- \`the generated app_fields.ts is stale relative to the installed contract version.\`,
49
- );
50
- }
51
- return id;
52
- }
53
- `;
54
- /**
55
- * Reconstruct the entity → field → option nesting (plus role list) from the flat
56
- * binding maps. Entity/field/option aliases are slug-grammar (`[a-z][a-z0-9_]*`,
57
- * no `.` or `:`), so a field key splits on its single `.` and an option key on
58
- * its single `:`. First-seen order is preserved (the binding is written in
59
- * contract order), so the output is deterministic for a given binding. An option
60
- * whose owning field is absent from `fields` is skipped defensively — the binding
61
- * is the authority, and a select field is always present among the fields.
62
- */
63
- function parseBinding(binding) {
64
- const entities = [];
65
- const entityByAlias = new Map();
66
- const fieldByKey = new Map();
67
- for (const key of Object.keys(binding.fields)) {
68
- const dot = key.indexOf(".");
69
- if (dot <= 0 || dot >= key.length - 1)
70
- continue;
71
- const entityAlias = key.slice(0, dot);
72
- const fieldAlias = key.slice(dot + 1);
73
- let entity = entityByAlias.get(entityAlias);
74
- if (entity === undefined) {
75
- entity = { alias: entityAlias, fields: [] };
76
- entityByAlias.set(entityAlias, entity);
77
- entities.push(entity);
78
- }
79
- const field = { alias: fieldAlias, key, options: [] };
80
- entity.fields.push(field);
81
- fieldByKey.set(key, field);
82
- }
83
- for (const key of Object.keys(binding.options)) {
84
- const colon = key.indexOf(":");
85
- if (colon <= 0 || colon >= key.length - 1)
86
- continue;
87
- const fieldKey = key.slice(0, colon);
88
- const optionAlias = key.slice(colon + 1);
89
- const field = fieldByKey.get(fieldKey);
90
- if (field === undefined)
91
- continue;
92
- field.options.push({ alias: optionAlias, key });
93
- }
94
- return { entities, roles: Object.keys(binding.roles) };
95
- }
96
- /** Emit the `F` map: `{ ENTITY: { field: bound(binding.fields, "entity.field", "field") } }`. */
97
- function emitFieldMap(entities) {
98
- if (entities.length === 0)
99
- return `export const F = {} as const;`;
100
- const blocks = entities.map((entity) => {
101
- const lines = entity.fields.map((field) => ` ${propKey(field.alias)}: bound(binding.fields, ${JSON.stringify(field.key)}, "field"),`);
102
- return ` ${propKey(entity.alias.toUpperCase())}: {\n${lines.join("\n")}\n },`;
103
- });
104
- return `export const F = {\n${blocks.join("\n")}\n} as const;`;
105
- }
106
- /**
107
- * Emit the `OPT` map: `{ ENTITY: { field: { opt: bound(binding.options, …) } } }`.
108
- * Only fields carrying options appear (an option-less field/entity is omitted).
109
- */
110
- function emitOptionMap(entities) {
111
- const entityBlocks = [];
112
- for (const entity of entities) {
113
- const fieldBlocks = [];
114
- for (const field of entity.fields) {
115
- if (field.options.length === 0)
116
- continue;
117
- const lines = field.options.map((option) => ` ${propKey(option.alias)}: bound(binding.options, ${JSON.stringify(option.key)}, "option"),`);
118
- fieldBlocks.push(` ${propKey(field.alias)}: {\n${lines.join("\n")}\n },`);
119
- }
120
- if (fieldBlocks.length === 0)
121
- continue;
122
- entityBlocks.push(` ${propKey(entity.alias.toUpperCase())}: {\n${fieldBlocks.join("\n")}\n },`);
123
- }
124
- if (entityBlocks.length === 0)
125
- return `export const OPT = {} as const;`;
126
- return `export const OPT = {\n${entityBlocks.join("\n")}\n} as const;`;
127
- }
128
- /** Emit the `ROLE` map: `{ role: bound(binding.roles, "role", "role") }`. */
129
- function emitRoleMap(roles) {
130
- if (roles.length === 0)
131
- return `export const ROLE = {} as const;`;
132
- const lines = roles.map((role) => ` ${propKey(role)}: bound(binding.roles, ${JSON.stringify(role)}, "role"),`);
133
- return `export const ROLE = {\n${lines.join("\n")}\n} as const;`;
134
- }
135
- /**
136
- * Generate the full binding-resolved `.lotics/app_fields.ts` source from an
137
- * installation's live binding. Pure — same binding → same bytes. Idempotent.
138
- * An empty binding yields valid, empty `F`/`OPT`/`ROLE` maps so the file always
139
- * compiles and imports resolve.
140
- */
141
- export function generatePackageAppFields(binding) {
142
- const { entities, roles } = parseBinding(binding);
143
- return `${HEADER}
144
- ${emitFieldMap(entities)}
145
-
146
- ${emitOptionMap(entities)}
147
-
148
- ${emitRoleMap(roles)}
149
-
150
- /** Field-id alias map: \`F[<ENTITY>][<field>]\` is this installation's \`fld_…\` id. */
151
- export type AppFields = typeof F;
152
- /** Select-option alias map: \`OPT[<ENTITY>][<field>][<option>]\` is this installation's \`opt_…\` id. */
153
- export type AppOptions = typeof OPT;
154
- /** Role alias map: \`ROLE[<role>]\` is this installation's \`grp_…\` id. */
155
- export type AppRoles = typeof ROLE;
156
- `;
157
- }
@@ -1 +0,0 @@
1
- export {};