@lotics/cli 0.88.0 → 0.89.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.
- package/dist/render_page.js +100 -100
- package/dist/src/cli.js +162 -100
- package/dist/{client.d.ts → src/client.d.ts} +9 -1
- package/dist/src/package_content_types.d.ts +70 -0
- package/dist/src/package_content_types.js +12 -0
- package/package.json +2 -2
- package/dist/app_commands.d.ts +0 -401
- package/dist/app_commands.js +0 -1520
- package/dist/app_commands.test.d.ts +0 -1
- package/dist/app_commands.test.js +0 -898
- package/dist/app_workflow_check.d.ts +0 -77
- package/dist/app_workflow_check.js +0 -169
- package/dist/app_workflow_check.test.d.ts +0 -1
- package/dist/app_workflow_check.test.js +0 -166
- package/dist/args.d.ts +0 -42
- package/dist/args.js +0 -121
- package/dist/args.test.d.ts +0 -1
- package/dist/args.test.js +0 -88
- package/dist/child_env.d.ts +0 -13
- package/dist/child_env.js +0 -24
- package/dist/cli.d.ts +0 -2
- package/dist/cli.js +0 -1095
- package/dist/cli_dispatch.test.d.ts +0 -1
- package/dist/cli_dispatch.test.js +0 -89
- package/dist/client.test.d.ts +0 -1
- package/dist/client.test.js +0 -47
- package/dist/config.d.ts +0 -124
- package/dist/config.js +0 -315
- package/dist/config.test.d.ts +0 -1
- package/dist/config.test.js +0 -319
- package/dist/dev/file_relay.d.ts +0 -39
- package/dist/dev/file_relay.js +0 -87
- package/dist/dev/file_relay.test.d.ts +0 -1
- package/dist/dev/file_relay.test.js +0 -87
- package/dist/dev/rpc_handler.d.ts +0 -21
- package/dist/dev/rpc_handler.js +0 -166
- package/dist/dev/rpc_handler.test.d.ts +0 -1
- package/dist/dev/rpc_handler.test.js +0 -107
- package/dist/dev/server.d.ts +0 -75
- package/dist/dev/server.js +0 -443
- package/dist/dev/upload_relay.d.ts +0 -36
- package/dist/dev/upload_relay.js +0 -61
- package/dist/dev/upload_relay.test.d.ts +0 -1
- package/dist/dev/upload_relay.test.js +0 -76
- package/dist/dev/wrapper_page.d.ts +0 -40
- package/dist/dev/wrapper_page.js +0 -335
- package/dist/docx.d.ts +0 -2
- package/dist/docx.js +0 -341
- package/dist/docx.test.d.ts +0 -1
- package/dist/docx.test.js +0 -145
- package/dist/file_command_io.d.ts +0 -12
- package/dist/file_command_io.js +0 -34
- package/dist/generate_app_agents_dts.d.ts +0 -16
- package/dist/generate_app_agents_dts.js +0 -62
- package/dist/generate_app_agents_dts.test.d.ts +0 -1
- package/dist/generate_app_agents_dts.test.js +0 -45
- package/dist/generate_app_fields.d.ts +0 -56
- package/dist/generate_app_fields.js +0 -148
- package/dist/generate_app_fields.test.d.ts +0 -1
- package/dist/generate_app_fields.test.js +0 -108
- package/dist/generate_app_queries_dts.d.ts +0 -21
- package/dist/generate_app_queries_dts.js +0 -57
- package/dist/generate_app_workflows_dts.d.ts +0 -39
- package/dist/generate_app_workflows_dts.js +0 -229
- package/dist/generate_app_workflows_dts.test.d.ts +0 -1
- package/dist/generate_app_workflows_dts.test.js +0 -109
- package/dist/generate_package_fields.d.ts +0 -49
- package/dist/generate_package_fields.js +0 -157
- package/dist/generate_package_fields.test.d.ts +0 -1
- package/dist/generate_package_fields.test.js +0 -59
- package/dist/inputs.d.ts +0 -38
- package/dist/inputs.js +0 -50
- package/dist/inputs.test.d.ts +0 -1
- package/dist/inputs.test.js +0 -89
- package/dist/preview.d.ts +0 -3
- package/dist/preview.js +0 -233
- package/dist/starter_template.d.ts +0 -64
- package/dist/starter_template.js +0 -753
- package/dist/starter_template.test.d.ts +0 -1
- package/dist/starter_template.test.js +0 -115
- package/dist/version.d.ts +0 -1
- package/dist/version.js +0 -6
- package/dist/xlsx.d.ts +0 -2
- package/dist/xlsx.js +0 -489
- package/dist/xlsx.test.d.ts +0 -1
- package/dist/xlsx.test.js +0 -131
- /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 {};
|