@lotics/cli 0.25.0 → 0.28.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/app_commands.d.ts +15 -2
- package/dist/src/app_commands.js +21 -13
- package/dist/src/app_commands.test.js +3 -0
- package/dist/src/client.d.ts +24 -13
- package/dist/src/client.js +8 -19
- package/dist/src/dev/rpc_handler.d.ts +1 -1
- package/dist/src/dev/rpc_handler.js +4 -19
- package/dist/src/generate_app_queries_dts.d.ts +21 -0
- package/dist/src/generate_app_queries_dts.js +57 -0
- package/dist/src/generate_app_workflows_dts.d.ts +13 -0
- package/dist/src/generate_app_workflows_dts.js +2 -2
- package/dist/src/starter_template.d.ts +2 -1
- package/dist/src/starter_template.js +13 -5
- package/package.json +1 -1
|
@@ -11,6 +11,18 @@ export type AppWorkflowDeclaration = {
|
|
|
11
11
|
workflow_id: string;
|
|
12
12
|
inputs?: Record<string, unknown>;
|
|
13
13
|
};
|
|
14
|
+
/**
|
|
15
|
+
* Manifest declaration for one named query:
|
|
16
|
+
* `"alias": { ast, params?: { key: { type, … } } }`
|
|
17
|
+
*
|
|
18
|
+
* `ast` is a fixed query AST template; `params` declares the typed value
|
|
19
|
+
* holes (`{{params.<name>}}`) the app fills via `useQuery(alias, params)`.
|
|
20
|
+
* Custom-code apps never send a raw AST — this declaration is the contract.
|
|
21
|
+
*/
|
|
22
|
+
export type AppQueryDeclaration = {
|
|
23
|
+
ast: unknown;
|
|
24
|
+
params?: Record<string, unknown>;
|
|
25
|
+
};
|
|
14
26
|
/**
|
|
15
27
|
* Stamp the post-extraction manifest with server-authoritative meta. Called
|
|
16
28
|
* by `appPull` after the source archive lands on disk.
|
|
@@ -29,6 +41,7 @@ export declare function stampPulledManifest(projectDir: string, args: {
|
|
|
29
41
|
current_version_id: string;
|
|
30
42
|
version_number: number;
|
|
31
43
|
workflows: Record<string, AppWorkflowDeclaration>;
|
|
44
|
+
queries: Record<string, AppQueryDeclaration>;
|
|
32
45
|
}): void;
|
|
33
46
|
/**
|
|
34
47
|
* `lotics app create <name> [path]`
|
|
@@ -86,8 +99,8 @@ export declare function appDeploy(client: LoticsClient, args: {
|
|
|
86
99
|
* `lotics app dev [path] [--port=5174] [--vite-port=5173]`
|
|
87
100
|
*
|
|
88
101
|
* Local dev mode for iframe apps. Spawns Vite + a postMessage RPC forwarder
|
|
89
|
-
* that bridges the iframe's hooks (useQuery,
|
|
90
|
-
*
|
|
102
|
+
* that bridges the iframe's hooks (useQuery, useWorkflow) to api.lotics.ai
|
|
103
|
+
* using the CLI's stored API key. Wrapper
|
|
91
104
|
* iframe matches the production sandbox attributes exactly — null origin,
|
|
92
105
|
* allow-scripts — so prod-equivalent runtime behavior surfaces in dev.
|
|
93
106
|
*
|
package/dist/src/app_commands.js
CHANGED
|
@@ -18,6 +18,7 @@ import { tmpdir } from "node:os";
|
|
|
18
18
|
import { buildStarterTemplate } from "./starter_template.js";
|
|
19
19
|
import { startDevServer, openBrowser } from "./dev/server.js";
|
|
20
20
|
import { generateAppWorkflowsDts } from "./generate_app_workflows_dts.js";
|
|
21
|
+
import { generateAppQueriesDts } from "./generate_app_queries_dts.js";
|
|
21
22
|
/** Run `tar` and resolve when it exits cleanly. Throws with stderr on failure. */
|
|
22
23
|
function runTar(args, cwd) {
|
|
23
24
|
return new Promise((resolve, reject) => {
|
|
@@ -64,6 +65,7 @@ function readAppMeta(projectDir) {
|
|
|
64
65
|
current_version_id: pkg.lotics.current_version_id ?? null,
|
|
65
66
|
version_number: pkg.lotics.version_number ?? null,
|
|
66
67
|
workflows: pkg.lotics.workflows ?? {},
|
|
68
|
+
queries: pkg.lotics.queries ?? {},
|
|
67
69
|
};
|
|
68
70
|
}
|
|
69
71
|
function writeAppMeta(projectDir, meta) {
|
|
@@ -73,16 +75,17 @@ function writeAppMeta(projectDir, meta) {
|
|
|
73
75
|
fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
|
|
74
76
|
}
|
|
75
77
|
/**
|
|
76
|
-
* Write `.lotics/app_workflows.d.ts` from the
|
|
77
|
-
*
|
|
78
|
-
* `
|
|
79
|
-
* tsconfig.json includes `.lotics/` so
|
|
78
|
+
* Write `.lotics/app_workflows.d.ts` + `.lotics/app_queries.d.ts` from the
|
|
79
|
+
* manifest's `workflows` / `queries` maps. Called from `app create / pull /
|
|
80
|
+
* dev / deploy` so the augmented `AppWorkflows` / `AppQueries` types stay in
|
|
81
|
+
* sync with the manifest. The starter tsconfig.json includes `.lotics/` so
|
|
82
|
+
* tsc + Vite pick the files up automatically.
|
|
80
83
|
*/
|
|
81
|
-
function
|
|
84
|
+
function writeAppDts(projectDir, manifest) {
|
|
82
85
|
const dotLotics = path.join(projectDir, ".lotics");
|
|
83
86
|
fs.mkdirSync(dotLotics, { recursive: true });
|
|
84
|
-
|
|
85
|
-
fs.writeFileSync(path.join(dotLotics, "
|
|
87
|
+
fs.writeFileSync(path.join(dotLotics, "app_workflows.d.ts"), generateAppWorkflowsDts(manifest.workflows));
|
|
88
|
+
fs.writeFileSync(path.join(dotLotics, "app_queries.d.ts"), generateAppQueriesDts(manifest.queries));
|
|
86
89
|
}
|
|
87
90
|
/**
|
|
88
91
|
* Stamp the post-extraction manifest with server-authoritative meta. Called
|
|
@@ -103,8 +106,9 @@ export function stampPulledManifest(projectDir, args) {
|
|
|
103
106
|
current_version_id: args.current_version_id,
|
|
104
107
|
version_number: args.version_number,
|
|
105
108
|
workflows: args.workflows,
|
|
109
|
+
queries: args.queries,
|
|
106
110
|
});
|
|
107
|
-
|
|
111
|
+
writeAppDts(projectDir, { workflows: args.workflows, queries: args.queries });
|
|
108
112
|
}
|
|
109
113
|
async function downloadToFile(url, destPath) {
|
|
110
114
|
const response = await fetch(url);
|
|
@@ -155,7 +159,7 @@ export async function appCreate(client, args) {
|
|
|
155
159
|
// Codegen pass for AppWorkflows typing. Empty manifest on first create —
|
|
156
160
|
// emits a base augmentation file so future deploys produce idempotent diffs
|
|
157
161
|
// rather than introducing a new tracked file later.
|
|
158
|
-
|
|
162
|
+
writeAppDts(targetPath, {});
|
|
159
163
|
console.error("Installing npm dependencies...");
|
|
160
164
|
await runNpm(["install"], targetPath);
|
|
161
165
|
console.error("Building initial version...");
|
|
@@ -217,6 +221,7 @@ export async function appPull(client, args) {
|
|
|
217
221
|
current_version_id: app.current_version_id,
|
|
218
222
|
version_number: version.version,
|
|
219
223
|
workflows: app.workflows ?? {},
|
|
224
|
+
queries: app.queries ?? {},
|
|
220
225
|
});
|
|
221
226
|
console.error(`Installing npm dependencies...`);
|
|
222
227
|
await runNpm(["install"], targetPath);
|
|
@@ -239,7 +244,7 @@ export async function appDeploy(client, args) {
|
|
|
239
244
|
// Regenerate AppWorkflows typing before the build picks up source. Keeps
|
|
240
245
|
// .lotics/app_workflows.d.ts in sync with the manifest's workflows map
|
|
241
246
|
// every time the developer ships.
|
|
242
|
-
|
|
247
|
+
writeAppDts(projectDir, { workflows: meta.workflows, queries: meta.queries });
|
|
243
248
|
// Build locally so the server doesn't need a build sandbox in v1.
|
|
244
249
|
console.error("Building...");
|
|
245
250
|
await runNpm(["run", "build"], projectDir);
|
|
@@ -275,6 +280,9 @@ export async function appDeploy(client, args) {
|
|
|
275
280
|
// Sync apps.workflows from the manifest. Server validates each
|
|
276
281
|
// workflow_id exists in the workspace before committing.
|
|
277
282
|
workflows: meta.workflows ?? {},
|
|
283
|
+
// Sync apps.queries from the manifest. Server validates each query
|
|
284
|
+
// template (parseQueryNode, table access, param coverage).
|
|
285
|
+
queries: meta.queries ?? {},
|
|
278
286
|
// Opt into destructive workflow removal. Default false — the server
|
|
279
287
|
// rejects deploys whose manifest is missing aliases the App row has.
|
|
280
288
|
force_workflow_sync: args.forceWorkflowSync,
|
|
@@ -308,8 +316,8 @@ export async function appDeploy(client, args) {
|
|
|
308
316
|
* `lotics app dev [path] [--port=5174] [--vite-port=5173]`
|
|
309
317
|
*
|
|
310
318
|
* Local dev mode for iframe apps. Spawns Vite + a postMessage RPC forwarder
|
|
311
|
-
* that bridges the iframe's hooks (useQuery,
|
|
312
|
-
*
|
|
319
|
+
* that bridges the iframe's hooks (useQuery, useWorkflow) to api.lotics.ai
|
|
320
|
+
* using the CLI's stored API key. Wrapper
|
|
313
321
|
* iframe matches the production sandbox attributes exactly — null origin,
|
|
314
322
|
* allow-scripts — so prod-equivalent runtime behavior surfaces in dev.
|
|
315
323
|
*
|
|
@@ -323,7 +331,7 @@ export async function appDev(client, args) {
|
|
|
323
331
|
// Regenerate AppWorkflows typing before Vite spins up so the dev typecheck
|
|
324
332
|
// sees the current shape. Doesn't watch for manifest changes mid-session —
|
|
325
333
|
// re-running `lotics app dev` after editing the manifest is the loop.
|
|
326
|
-
|
|
334
|
+
writeAppDts(projectDir, { workflows: meta.workflows, queries: meta.queries });
|
|
327
335
|
// Sanity: confirm the app exists in the workspace the CLI is auth'd into.
|
|
328
336
|
// Surfaces a clear error if the project's app_id has been deleted or the
|
|
329
337
|
// CLI is pointed at the wrong workspace.
|
|
@@ -56,6 +56,7 @@ describe("stampPulledManifest", () => {
|
|
|
56
56
|
agent_authored: { workflow_id: "wfl_authored_by_agent" },
|
|
57
57
|
renamed: { workflow_id: "wfl_renamed_on_server" },
|
|
58
58
|
},
|
|
59
|
+
queries: {},
|
|
59
60
|
});
|
|
60
61
|
const stamped = readStampedManifest();
|
|
61
62
|
expect(stamped.app_id).toBe("app_live");
|
|
@@ -80,6 +81,7 @@ describe("stampPulledManifest", () => {
|
|
|
80
81
|
current_version_id: "apv_x",
|
|
81
82
|
version_number: 2,
|
|
82
83
|
workflows: {},
|
|
84
|
+
queries: {},
|
|
83
85
|
});
|
|
84
86
|
const stamped = readStampedManifest();
|
|
85
87
|
expect(stamped.workflows).toEqual({});
|
|
@@ -94,6 +96,7 @@ describe("stampPulledManifest", () => {
|
|
|
94
96
|
workflows: {
|
|
95
97
|
only_live: { workflow_id: "wfl_only_live" },
|
|
96
98
|
},
|
|
99
|
+
queries: {},
|
|
97
100
|
});
|
|
98
101
|
const dtsPath = path.join(workDir, ".lotics", "app_workflows.d.ts");
|
|
99
102
|
expect(fs.existsSync(dtsPath)).toBe(true);
|
package/dist/src/client.d.ts
CHANGED
|
@@ -86,6 +86,14 @@ export declare class LoticsClient {
|
|
|
86
86
|
workflow_id: string;
|
|
87
87
|
inputs?: Record<string, unknown>;
|
|
88
88
|
}> | null;
|
|
89
|
+
/**
|
|
90
|
+
* Live alias → query declaration map from `apps.queries`. Source of truth
|
|
91
|
+
* for `lotics app pull`. Null/undefined on apps with no declared queries.
|
|
92
|
+
*/
|
|
93
|
+
queries?: Record<string, {
|
|
94
|
+
ast: unknown;
|
|
95
|
+
params?: Record<string, unknown>;
|
|
96
|
+
}> | null;
|
|
89
97
|
}>;
|
|
90
98
|
createApp(body: {
|
|
91
99
|
name: string;
|
|
@@ -107,22 +115,15 @@ export declare class LoticsClient {
|
|
|
107
115
|
}>;
|
|
108
116
|
getAppVersionSourceUrl(app_id: string, version_id: string): Promise<string>;
|
|
109
117
|
/**
|
|
110
|
-
* Run a query
|
|
111
|
-
* Mirrors POST /v1/apps/{app_id}/query.
|
|
118
|
+
* Run a named query declared in the app's manifest, scoped to the app's IAM
|
|
119
|
+
* principal. Mirrors POST /v1/apps/{app_id}/query.
|
|
112
120
|
*/
|
|
113
|
-
appQuery(app_id: string,
|
|
121
|
+
appQuery(app_id: string, body: {
|
|
122
|
+
alias: string;
|
|
123
|
+
params?: Record<string, unknown>;
|
|
124
|
+
}): Promise<{
|
|
114
125
|
rows: unknown[];
|
|
115
126
|
}>;
|
|
116
|
-
/**
|
|
117
|
-
* Update records in a workspace table scoped to an app's IAM principal.
|
|
118
|
-
* Mirrors PATCH /v1/apps/{app_id}/tables/{table_id}/records.
|
|
119
|
-
*/
|
|
120
|
-
appMutate(app_id: string, table_id: string, records: unknown): Promise<unknown>;
|
|
121
|
-
/**
|
|
122
|
-
* Execute an app-declared action.
|
|
123
|
-
* Mirrors POST /v1/apps/{app_id}/actions/{action_id}.
|
|
124
|
-
*/
|
|
125
|
-
appAction(app_id: string, action_id: string, inputs: unknown): Promise<unknown>;
|
|
126
127
|
/**
|
|
127
128
|
* Execute a workflow by alias declared in package.json#lotics.workflows.
|
|
128
129
|
* Mirrors POST /v1/apps/{app_id}/workflows/{alias}/execute.
|
|
@@ -147,6 +148,16 @@ export declare class LoticsClient {
|
|
|
147
148
|
workflow_id: string;
|
|
148
149
|
inputs?: Record<string, unknown>;
|
|
149
150
|
}>;
|
|
151
|
+
/**
|
|
152
|
+
* Alias → query declaration map from `package.json#lotics.queries`. Each
|
|
153
|
+
* value is `{ ast, params? }` — a fixed query AST template and an optional
|
|
154
|
+
* typed param schema. Always sent (empty object when none declared) so the
|
|
155
|
+
* server overwrites apps.queries authoritatively.
|
|
156
|
+
*/
|
|
157
|
+
queries?: Record<string, {
|
|
158
|
+
ast: unknown;
|
|
159
|
+
params?: Record<string, unknown>;
|
|
160
|
+
}>;
|
|
150
161
|
/**
|
|
151
162
|
* Opt into destructive workflow removal. When false/absent, the server
|
|
152
163
|
* rejects a deploy whose `workflows` map is missing aliases that exist
|
package/dist/src/client.js
CHANGED
|
@@ -157,30 +157,16 @@ export class LoticsClient {
|
|
|
157
157
|
return result.url;
|
|
158
158
|
}
|
|
159
159
|
// ── App iframe RPC endpoints ──────────────────────────────────────────────
|
|
160
|
-
// These mirror the
|
|
160
|
+
// These mirror the two ops handled by frontend/features/app_ui/app_iframe_host.tsx.
|
|
161
161
|
// The deployed iframe sends postMessage to the parent frontend, which calls
|
|
162
162
|
// these same endpoints via the user's session cookie. `lotics app dev`
|
|
163
163
|
// forwards the iframe's postMessage to these methods using the CLI's API key.
|
|
164
164
|
/**
|
|
165
|
-
* Run a query
|
|
166
|
-
* Mirrors POST /v1/apps/{app_id}/query.
|
|
165
|
+
* Run a named query declared in the app's manifest, scoped to the app's IAM
|
|
166
|
+
* principal. Mirrors POST /v1/apps/{app_id}/query.
|
|
167
167
|
*/
|
|
168
|
-
async appQuery(app_id,
|
|
169
|
-
return this.request("POST", `/v1/apps/${encodeURIComponent(app_id)}/query`,
|
|
170
|
-
}
|
|
171
|
-
/**
|
|
172
|
-
* Update records in a workspace table scoped to an app's IAM principal.
|
|
173
|
-
* Mirrors PATCH /v1/apps/{app_id}/tables/{table_id}/records.
|
|
174
|
-
*/
|
|
175
|
-
async appMutate(app_id, table_id, records) {
|
|
176
|
-
return this.request("PATCH", `/v1/apps/${encodeURIComponent(app_id)}/tables/${encodeURIComponent(table_id)}/records`, { records });
|
|
177
|
-
}
|
|
178
|
-
/**
|
|
179
|
-
* Execute an app-declared action.
|
|
180
|
-
* Mirrors POST /v1/apps/{app_id}/actions/{action_id}.
|
|
181
|
-
*/
|
|
182
|
-
async appAction(app_id, action_id, inputs) {
|
|
183
|
-
return this.request("POST", `/v1/apps/${encodeURIComponent(app_id)}/actions/${encodeURIComponent(action_id)}`, { inputs });
|
|
168
|
+
async appQuery(app_id, body) {
|
|
169
|
+
return this.request("POST", `/v1/apps/${encodeURIComponent(app_id)}/query`, body);
|
|
184
170
|
}
|
|
185
171
|
/**
|
|
186
172
|
* Execute a workflow by alias declared in package.json#lotics.workflows.
|
|
@@ -205,6 +191,9 @@ export class LoticsClient {
|
|
|
205
191
|
// Always send workflows — empty object is meaningful (clears any
|
|
206
192
|
// previously-declared aliases). Server validates each entry.
|
|
207
193
|
formData.append("workflows", JSON.stringify(args.workflows ?? {}));
|
|
194
|
+
// Always send queries — empty object clears any previously-declared
|
|
195
|
+
// named queries. Server validates each template.
|
|
196
|
+
formData.append("queries", JSON.stringify(args.queries ?? {}));
|
|
208
197
|
// Only post the override flag when the user explicitly opts in. The
|
|
209
198
|
// server defaults to "honor the guard" — opt-in is loud, opt-out
|
|
210
199
|
// requires intent.
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* { message }. Same shape as the production iframe-host's error path.
|
|
7
7
|
*/
|
|
8
8
|
import { LoticsClient } from "../client.js";
|
|
9
|
-
export type RpcOp = "query" | "
|
|
9
|
+
export type RpcOp = "query" | "workflow";
|
|
10
10
|
export interface RpcRequest {
|
|
11
11
|
app_id: string;
|
|
12
12
|
op: RpcOp;
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* Errors are thrown — the HTTP server caller serializes them to a 500 with
|
|
6
6
|
* { message }. Same shape as the production iframe-host's error path.
|
|
7
7
|
*/
|
|
8
|
-
const SUPPORTED_OPS = new Set(["query", "
|
|
8
|
+
const SUPPORTED_OPS = new Set(["query", "workflow"]);
|
|
9
9
|
export async function dispatchRpc(client, body) {
|
|
10
10
|
if (!body || typeof body.app_id !== "string" || typeof body.op !== "string") {
|
|
11
11
|
throw new Error("RPC envelope must include app_id and op");
|
|
@@ -16,25 +16,10 @@ export async function dispatchRpc(client, body) {
|
|
|
16
16
|
switch (body.op) {
|
|
17
17
|
case "query": {
|
|
18
18
|
const p = body.payload;
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
throw new Error("query payload must include `ast`");
|
|
22
|
-
}
|
|
23
|
-
return client.appQuery(body.app_id, ast);
|
|
24
|
-
}
|
|
25
|
-
case "mutate": {
|
|
26
|
-
const p = body.payload;
|
|
27
|
-
if (!p || typeof p.table_id !== "string") {
|
|
28
|
-
throw new Error("mutate payload must include `table_id`");
|
|
29
|
-
}
|
|
30
|
-
return client.appMutate(body.app_id, p.table_id, p.records);
|
|
31
|
-
}
|
|
32
|
-
case "action": {
|
|
33
|
-
const p = body.payload;
|
|
34
|
-
if (!p || typeof p.action_id !== "string") {
|
|
35
|
-
throw new Error("action payload must include `action_id`");
|
|
19
|
+
if (!p || typeof p.alias !== "string") {
|
|
20
|
+
throw new Error("query payload must include `alias`");
|
|
36
21
|
}
|
|
37
|
-
return client.
|
|
22
|
+
return client.appQuery(body.app_id, { alias: p.alias, params: p.params });
|
|
38
23
|
}
|
|
39
24
|
case "workflow": {
|
|
40
25
|
const p = body.payload;
|
|
@@ -0,0 +1,21 @@
|
|
|
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 type { AppQueryDeclaration } from "./app_commands.js";
|
|
21
|
+
export declare function generateAppQueriesDts(queries: Record<string, AppQueryDeclaration> | undefined): string;
|
|
@@ -0,0 +1,57 @@
|
|
|
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
|
+
}
|
|
@@ -15,3 +15,16 @@
|
|
|
15
15
|
*/
|
|
16
16
|
import type { AppWorkflowDeclaration } from "./app_commands.js";
|
|
17
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>): string;
|
|
30
|
+
export declare function isValidIdentifier(name: string): boolean;
|
|
@@ -64,7 +64,7 @@ ${lines.join("\n")}
|
|
|
64
64
|
* to `unknown` rather than throwing — the server's deploy-time schema parse
|
|
65
65
|
* catches structurally-invalid declarations before they reach us.
|
|
66
66
|
*/
|
|
67
|
-
function inputsToType(inputs) {
|
|
67
|
+
export function inputsToType(inputs) {
|
|
68
68
|
const fields = [];
|
|
69
69
|
for (const [key, decl] of Object.entries(inputs)) {
|
|
70
70
|
if (decl === null || typeof decl !== "object")
|
|
@@ -118,6 +118,6 @@ function inputDeclToTsType(decl) {
|
|
|
118
118
|
}
|
|
119
119
|
}
|
|
120
120
|
const IDENTIFIER_REGEX = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/;
|
|
121
|
-
function isValidIdentifier(name) {
|
|
121
|
+
export function isValidIdentifier(name) {
|
|
122
122
|
return IDENTIFIER_REGEX.test(name);
|
|
123
123
|
}
|
|
@@ -11,7 +11,8 @@
|
|
|
11
11
|
* Conventions (locked decisions):
|
|
12
12
|
* - Vite + React + TypeScript (strict mode).
|
|
13
13
|
* - Entry: src/App.tsx with `export default`. main.tsx wires `mount(<App/>)`
|
|
14
|
-
* and imports @lotics/ui/index.css
|
|
14
|
+
* and imports @lotics/ui/index.css (base style reset) + @lotics/ui/fonts.css
|
|
15
|
+
* (path-independent Inter @font-face bundle — Text renders unstyled without it).
|
|
15
16
|
* - Vite default `base: "/"` so emitted asset URLs are absolute; the render
|
|
16
17
|
* endpoint rewrites root-relative paths to `/v1/apps/{id}/asset/...`.
|
|
17
18
|
* Don't switch to `base: "./"` without updating `rewriteAssetPaths`.
|
|
@@ -11,7 +11,8 @@
|
|
|
11
11
|
* Conventions (locked decisions):
|
|
12
12
|
* - Vite + React + TypeScript (strict mode).
|
|
13
13
|
* - Entry: src/App.tsx with `export default`. main.tsx wires `mount(<App/>)`
|
|
14
|
-
* and imports @lotics/ui/index.css
|
|
14
|
+
* and imports @lotics/ui/index.css (base style reset) + @lotics/ui/fonts.css
|
|
15
|
+
* (path-independent Inter @font-face bundle — Text renders unstyled without it).
|
|
15
16
|
* - Vite default `base: "/"` so emitted asset URLs are absolute; the render
|
|
16
17
|
* endpoint rewrites root-relative paths to `/v1/apps/{id}/asset/...`.
|
|
17
18
|
* Don't switch to `base: "./"` without updating `rewriteAssetPaths`.
|
|
@@ -47,8 +48,8 @@ export function buildStarterTemplate(args) {
|
|
|
47
48
|
test: "vitest run",
|
|
48
49
|
},
|
|
49
50
|
dependencies: {
|
|
50
|
-
"@lotics/app-sdk": "^0.
|
|
51
|
-
"@lotics/ui": "^
|
|
51
|
+
"@lotics/app-sdk": "^0.5.0",
|
|
52
|
+
"@lotics/ui": "^1.3.0",
|
|
52
53
|
"@react-native-picker/picker": "^2.7.0",
|
|
53
54
|
"expo-image": "~3.0.9",
|
|
54
55
|
"lucide-react": "^0.562.0",
|
|
@@ -135,6 +136,12 @@ export default defineConfig({
|
|
|
135
136
|
{ find: "react-native", replacement: "react-native-web" },
|
|
136
137
|
],
|
|
137
138
|
extensions: [".web.tsx", ".web.ts", ".tsx", ".ts", ".jsx", ".js"],
|
|
139
|
+
// @lotics/ui ships source and is consumed across many subpath entries
|
|
140
|
+
// (./card, ./metric, ./use_screen_size, …). Without dedupe, Vite can
|
|
141
|
+
// pre-bundle a subpath into its own chunk with a second React copy — a
|
|
142
|
+
// hook called from there hits a null dispatcher ("Invalid hook call").
|
|
143
|
+
// Pin React (and RN-Web) to a single instance shared by every chunk.
|
|
144
|
+
dedupe: ["react", "react-dom", "react-native-web"],
|
|
138
145
|
},
|
|
139
146
|
build: {
|
|
140
147
|
outDir: "dist",
|
|
@@ -186,6 +193,7 @@ export default defineConfig({
|
|
|
186
193
|
{
|
|
187
194
|
path: "src/main.tsx",
|
|
188
195
|
content: `import "@lotics/ui/index.css";
|
|
196
|
+
import "@lotics/ui/fonts.css";
|
|
189
197
|
import { mount } from "@lotics/app-sdk";
|
|
190
198
|
import App from "./App";
|
|
191
199
|
|
|
@@ -239,7 +247,7 @@ export default function App() {
|
|
|
239
247
|
</View>
|
|
240
248
|
</Card>
|
|
241
249
|
<Text color="muted" size="sm">
|
|
242
|
-
Hooks from @lotics/app-sdk:
|
|
250
|
+
Hooks from @lotics/app-sdk: useQuery, useWorkflow.
|
|
243
251
|
</Text>
|
|
244
252
|
<Button title="Get started" onPress={() => {}} color="primary" />
|
|
245
253
|
</View>
|
|
@@ -400,7 +408,7 @@ lotics app deploy -m "what" # with a commit-message-style note
|
|
|
400
408
|
## SDK + UI components
|
|
401
409
|
|
|
402
410
|
\`\`\`tsx
|
|
403
|
-
import { mount,
|
|
411
|
+
import { mount, useQuery, useWorkflow } from "@lotics/app-sdk";
|
|
404
412
|
import { Stack } from "@lotics/ui/stack";
|
|
405
413
|
import { Card } from "@lotics/ui/card";
|
|
406
414
|
import { Button } from "@lotics/ui/button";
|