@lotics/cli 0.28.0 → 0.30.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/args.d.ts +32 -0
- package/dist/src/args.js +94 -0
- package/dist/src/args.test.d.ts +1 -0
- package/dist/src/args.test.js +37 -0
- package/dist/src/cli.js +6 -80
- package/dist/src/client.d.ts +30 -0
- package/dist/src/client.js +14 -0
- package/dist/src/dev/rpc_handler.d.ts +1 -1
- package/dist/src/dev/rpc_handler.js +34 -1
- package/dist/src/dev/wrapper_page.js +47 -12
- package/dist/src/generate_app_workflows_dts.js +2 -0
- package/dist/src/starter_template.d.ts +4 -2
- package/dist/src/starter_template.js +13 -4
- package/package.json +1 -1
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI argument parser.
|
|
3
|
+
*
|
|
4
|
+
* Splits `process.argv.slice(2)` into a command / subcommand / positional
|
|
5
|
+
* (`toolArgs`) / remaining-positionals (`restArgs`) shape plus typed `flags`.
|
|
6
|
+
* Value-taking flags (`-m`, `--api-key`, …) consume the next token; boolean
|
|
7
|
+
* flags (`--force-workflow-sync`, `--json`, …) toggle. Anything not matching a
|
|
8
|
+
* known flag is positional.
|
|
9
|
+
*
|
|
10
|
+
* Lives apart from `cli.ts` so it can be unit-tested — `cli.ts` runs `main()`
|
|
11
|
+
* on import, so importing the parser from there would execute the CLI.
|
|
12
|
+
*/
|
|
13
|
+
export declare function parseArgs(argv: string[]): {
|
|
14
|
+
command?: string;
|
|
15
|
+
subcommand?: string;
|
|
16
|
+
toolArgs?: string;
|
|
17
|
+
restArgs: string[];
|
|
18
|
+
flags: {
|
|
19
|
+
json: boolean;
|
|
20
|
+
timeout?: number;
|
|
21
|
+
output?: string;
|
|
22
|
+
as?: string;
|
|
23
|
+
apiKey?: string;
|
|
24
|
+
name?: string;
|
|
25
|
+
timezone?: string;
|
|
26
|
+
message?: string;
|
|
27
|
+
forceWorkflowSync: boolean;
|
|
28
|
+
local: boolean;
|
|
29
|
+
version: boolean;
|
|
30
|
+
help: boolean;
|
|
31
|
+
};
|
|
32
|
+
};
|
package/dist/src/args.js
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI argument parser.
|
|
3
|
+
*
|
|
4
|
+
* Splits `process.argv.slice(2)` into a command / subcommand / positional
|
|
5
|
+
* (`toolArgs`) / remaining-positionals (`restArgs`) shape plus typed `flags`.
|
|
6
|
+
* Value-taking flags (`-m`, `--api-key`, …) consume the next token; boolean
|
|
7
|
+
* flags (`--force-workflow-sync`, `--json`, …) toggle. Anything not matching a
|
|
8
|
+
* known flag is positional.
|
|
9
|
+
*
|
|
10
|
+
* Lives apart from `cli.ts` so it can be unit-tested — `cli.ts` runs `main()`
|
|
11
|
+
* on import, so importing the parser from there would execute the CLI.
|
|
12
|
+
*/
|
|
13
|
+
export function parseArgs(argv) {
|
|
14
|
+
const flags = {
|
|
15
|
+
json: false,
|
|
16
|
+
timeout: undefined,
|
|
17
|
+
output: undefined,
|
|
18
|
+
as: undefined,
|
|
19
|
+
apiKey: undefined,
|
|
20
|
+
name: undefined,
|
|
21
|
+
timezone: undefined,
|
|
22
|
+
message: undefined,
|
|
23
|
+
forceWorkflowSync: false,
|
|
24
|
+
local: false,
|
|
25
|
+
version: false,
|
|
26
|
+
help: false,
|
|
27
|
+
};
|
|
28
|
+
let command;
|
|
29
|
+
let subcommand;
|
|
30
|
+
let toolArgs;
|
|
31
|
+
const restArgs = [];
|
|
32
|
+
let i = 0;
|
|
33
|
+
while (i < argv.length) {
|
|
34
|
+
const arg = argv[i];
|
|
35
|
+
switch (arg) {
|
|
36
|
+
case "--json":
|
|
37
|
+
flags.json = true;
|
|
38
|
+
break;
|
|
39
|
+
case "--timeout":
|
|
40
|
+
flags.timeout = parseInt(argv[++i], 10);
|
|
41
|
+
break;
|
|
42
|
+
case "--output":
|
|
43
|
+
case "-o":
|
|
44
|
+
flags.output = argv[++i];
|
|
45
|
+
break;
|
|
46
|
+
case "--as":
|
|
47
|
+
flags.as = argv[++i];
|
|
48
|
+
break;
|
|
49
|
+
case "--api-key":
|
|
50
|
+
flags.apiKey = argv[++i];
|
|
51
|
+
break;
|
|
52
|
+
case "--name":
|
|
53
|
+
flags.name = argv[++i];
|
|
54
|
+
break;
|
|
55
|
+
case "--timezone":
|
|
56
|
+
flags.timezone = argv[++i];
|
|
57
|
+
break;
|
|
58
|
+
case "-m":
|
|
59
|
+
case "--message":
|
|
60
|
+
flags.message = argv[++i];
|
|
61
|
+
break;
|
|
62
|
+
case "--force-workflow-sync":
|
|
63
|
+
flags.forceWorkflowSync = true;
|
|
64
|
+
break;
|
|
65
|
+
case "--local":
|
|
66
|
+
flags.local = true;
|
|
67
|
+
break;
|
|
68
|
+
case "--version":
|
|
69
|
+
case "-v":
|
|
70
|
+
flags.version = true;
|
|
71
|
+
break;
|
|
72
|
+
case "--help":
|
|
73
|
+
case "-h":
|
|
74
|
+
flags.help = true;
|
|
75
|
+
break;
|
|
76
|
+
default:
|
|
77
|
+
if (!command) {
|
|
78
|
+
command = arg;
|
|
79
|
+
}
|
|
80
|
+
else if (!subcommand) {
|
|
81
|
+
subcommand = arg;
|
|
82
|
+
}
|
|
83
|
+
else if (!toolArgs) {
|
|
84
|
+
toolArgs = arg;
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
restArgs.push(arg);
|
|
88
|
+
}
|
|
89
|
+
break;
|
|
90
|
+
}
|
|
91
|
+
i++;
|
|
92
|
+
}
|
|
93
|
+
return { command, subcommand, toolArgs, restArgs, flags };
|
|
94
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { parseArgs } from "./args.js";
|
|
3
|
+
describe("parseArgs", () => {
|
|
4
|
+
it("splits command / subcommand / positional", () => {
|
|
5
|
+
const r = parseArgs(["app", "deploy", "my message"]);
|
|
6
|
+
expect(r.command).toBe("app");
|
|
7
|
+
expect(r.subcommand).toBe("deploy");
|
|
8
|
+
expect(r.toolArgs).toBe("my message");
|
|
9
|
+
expect(r.restArgs).toEqual([]);
|
|
10
|
+
});
|
|
11
|
+
// `app deploy` regression: `-m` and `--force-workflow-sync` must be parsed
|
|
12
|
+
// as flags, not silently consumed as the positional message.
|
|
13
|
+
it("parses -m as the message flag", () => {
|
|
14
|
+
const r = parseArgs(["app", "deploy", "-m", "a message"]);
|
|
15
|
+
expect(r.flags.message).toBe("a message");
|
|
16
|
+
expect(r.toolArgs).toBeUndefined();
|
|
17
|
+
});
|
|
18
|
+
it("parses --message as the message flag", () => {
|
|
19
|
+
const r = parseArgs(["app", "deploy", "--message", "a message"]);
|
|
20
|
+
expect(r.flags.message).toBe("a message");
|
|
21
|
+
});
|
|
22
|
+
it("parses --force-workflow-sync as a boolean flag", () => {
|
|
23
|
+
const r = parseArgs(["app", "deploy", "msg", "--force-workflow-sync"]);
|
|
24
|
+
expect(r.flags.forceWorkflowSync).toBe(true);
|
|
25
|
+
expect(r.toolArgs).toBe("msg");
|
|
26
|
+
});
|
|
27
|
+
it("parses --force-workflow-sync before -m without eating the message", () => {
|
|
28
|
+
const r = parseArgs(["app", "deploy", "--force-workflow-sync", "-m", "msg"]);
|
|
29
|
+
expect(r.flags.forceWorkflowSync).toBe(true);
|
|
30
|
+
expect(r.flags.message).toBe("msg");
|
|
31
|
+
});
|
|
32
|
+
it("defaults forceWorkflowSync to false and message to undefined", () => {
|
|
33
|
+
const r = parseArgs(["app", "deploy"]);
|
|
34
|
+
expect(r.flags.forceWorkflowSync).toBe(false);
|
|
35
|
+
expect(r.flags.message).toBeUndefined();
|
|
36
|
+
});
|
|
37
|
+
});
|
package/dist/src/cli.js
CHANGED
|
@@ -6,6 +6,7 @@ import { LoticsClient, API_BASE_URL } from "./client.js";
|
|
|
6
6
|
import { resolveAuth, loadConfig, saveConfig, deleteConfig, getConfigPath, checkForUpdate } from "./config.js";
|
|
7
7
|
import { VERSION } from "./version.js";
|
|
8
8
|
import { appCreate, appPull, appDeploy, appDev } from "./app_commands.js";
|
|
9
|
+
import { parseArgs } from "./args.js";
|
|
9
10
|
function printHelp() {
|
|
10
11
|
console.log(`Lotics CLI v${VERSION} — AI agent interface for Lotics
|
|
11
12
|
|
|
@@ -114,79 +115,6 @@ else ~/.lotics/config.json. A per-directory config pins a project or worktree to
|
|
|
114
115
|
its own account and workspace; --local creates one. Note: an exported
|
|
115
116
|
LOTICS_API_KEY env var overrides the config file's key.`);
|
|
116
117
|
}
|
|
117
|
-
function parseArgs(argv) {
|
|
118
|
-
const flags = {
|
|
119
|
-
json: false,
|
|
120
|
-
timeout: undefined,
|
|
121
|
-
output: undefined,
|
|
122
|
-
as: undefined,
|
|
123
|
-
apiKey: undefined,
|
|
124
|
-
name: undefined,
|
|
125
|
-
timezone: undefined,
|
|
126
|
-
local: false,
|
|
127
|
-
version: false,
|
|
128
|
-
help: false,
|
|
129
|
-
};
|
|
130
|
-
let command;
|
|
131
|
-
let subcommand;
|
|
132
|
-
let toolArgs;
|
|
133
|
-
const restArgs = [];
|
|
134
|
-
let i = 0;
|
|
135
|
-
while (i < argv.length) {
|
|
136
|
-
const arg = argv[i];
|
|
137
|
-
switch (arg) {
|
|
138
|
-
case "--json":
|
|
139
|
-
flags.json = true;
|
|
140
|
-
break;
|
|
141
|
-
case "--timeout":
|
|
142
|
-
flags.timeout = parseInt(argv[++i], 10);
|
|
143
|
-
break;
|
|
144
|
-
case "--output":
|
|
145
|
-
case "-o":
|
|
146
|
-
flags.output = argv[++i];
|
|
147
|
-
break;
|
|
148
|
-
case "--as":
|
|
149
|
-
flags.as = argv[++i];
|
|
150
|
-
break;
|
|
151
|
-
case "--api-key":
|
|
152
|
-
flags.apiKey = argv[++i];
|
|
153
|
-
break;
|
|
154
|
-
case "--name":
|
|
155
|
-
flags.name = argv[++i];
|
|
156
|
-
break;
|
|
157
|
-
case "--timezone":
|
|
158
|
-
flags.timezone = argv[++i];
|
|
159
|
-
break;
|
|
160
|
-
case "--local":
|
|
161
|
-
flags.local = true;
|
|
162
|
-
break;
|
|
163
|
-
case "--version":
|
|
164
|
-
case "-v":
|
|
165
|
-
flags.version = true;
|
|
166
|
-
break;
|
|
167
|
-
case "--help":
|
|
168
|
-
case "-h":
|
|
169
|
-
flags.help = true;
|
|
170
|
-
break;
|
|
171
|
-
default:
|
|
172
|
-
if (!command) {
|
|
173
|
-
command = arg;
|
|
174
|
-
}
|
|
175
|
-
else if (!subcommand) {
|
|
176
|
-
subcommand = arg;
|
|
177
|
-
}
|
|
178
|
-
else if (!toolArgs) {
|
|
179
|
-
toolArgs = arg;
|
|
180
|
-
}
|
|
181
|
-
else {
|
|
182
|
-
restArgs.push(arg);
|
|
183
|
-
}
|
|
184
|
-
break;
|
|
185
|
-
}
|
|
186
|
-
i++;
|
|
187
|
-
}
|
|
188
|
-
return { command, subcommand, toolArgs, restArgs, flags };
|
|
189
|
-
}
|
|
190
118
|
function readStdin() {
|
|
191
119
|
return new Promise((resolve, reject) => {
|
|
192
120
|
const chunks = [];
|
|
@@ -546,13 +474,11 @@ async function main() {
|
|
|
546
474
|
return;
|
|
547
475
|
}
|
|
548
476
|
if (subcommand === "deploy") {
|
|
549
|
-
//
|
|
550
|
-
//
|
|
551
|
-
//
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
const forceWorkflowSync = restArgs.includes("--force-workflow-sync");
|
|
555
|
-
await appDeploy(client, { message, forceWorkflowSync });
|
|
477
|
+
// The message is either `-m <message>` or a bare positional arg after
|
|
478
|
+
// `deploy`. `--force-workflow-sync` is a parsed boolean flag, valid in
|
|
479
|
+
// any position.
|
|
480
|
+
const message = flags.message ?? toolArgs;
|
|
481
|
+
await appDeploy(client, { message, forceWorkflowSync: flags.forceWorkflowSync });
|
|
556
482
|
return;
|
|
557
483
|
}
|
|
558
484
|
if (subcommand === "dev") {
|
package/dist/src/client.d.ts
CHANGED
|
@@ -129,6 +129,36 @@ export declare class LoticsClient {
|
|
|
129
129
|
* Mirrors POST /v1/apps/{app_id}/workflows/{alias}/execute.
|
|
130
130
|
*/
|
|
131
131
|
appWorkflow(app_id: string, alias: string, inputs: unknown): Promise<unknown>;
|
|
132
|
+
/**
|
|
133
|
+
* Mint a presigned URL for uploading a file into an app. Mirrors
|
|
134
|
+
* POST /v1/apps/{app_id}/files/upload-url.
|
|
135
|
+
*/
|
|
136
|
+
appRequestFileUpload(app_id: string, body: {
|
|
137
|
+
filename: string;
|
|
138
|
+
mime_type: string;
|
|
139
|
+
file_size: number;
|
|
140
|
+
}): Promise<{
|
|
141
|
+
file_id: string;
|
|
142
|
+
file_storage_key: string;
|
|
143
|
+
upload_url: string;
|
|
144
|
+
}>;
|
|
145
|
+
/**
|
|
146
|
+
* Finalize a presigned upload once the bytes are in storage. Mirrors
|
|
147
|
+
* POST /v1/apps/{app_id}/files/complete.
|
|
148
|
+
*/
|
|
149
|
+
appCompleteFileUpload(app_id: string, body: {
|
|
150
|
+
file_id: string;
|
|
151
|
+
file_storage_key: string;
|
|
152
|
+
filename: string;
|
|
153
|
+
}): Promise<{
|
|
154
|
+
file: {
|
|
155
|
+
id: string;
|
|
156
|
+
filename: string;
|
|
157
|
+
mime_type: string;
|
|
158
|
+
url?: string;
|
|
159
|
+
thumbnail_url?: string;
|
|
160
|
+
};
|
|
161
|
+
}>;
|
|
132
162
|
deployAppVersion(args: {
|
|
133
163
|
app_id: string;
|
|
134
164
|
source_archive: Buffer;
|
package/dist/src/client.js
CHANGED
|
@@ -175,6 +175,20 @@ export class LoticsClient {
|
|
|
175
175
|
async appWorkflow(app_id, alias, inputs) {
|
|
176
176
|
return this.request("POST", `/v1/apps/${encodeURIComponent(app_id)}/workflows/${encodeURIComponent(alias)}/execute`, { inputs });
|
|
177
177
|
}
|
|
178
|
+
/**
|
|
179
|
+
* Mint a presigned URL for uploading a file into an app. Mirrors
|
|
180
|
+
* POST /v1/apps/{app_id}/files/upload-url.
|
|
181
|
+
*/
|
|
182
|
+
async appRequestFileUpload(app_id, body) {
|
|
183
|
+
return this.request("POST", `/v1/apps/${encodeURIComponent(app_id)}/files/upload-url`, body);
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Finalize a presigned upload once the bytes are in storage. Mirrors
|
|
187
|
+
* POST /v1/apps/{app_id}/files/complete.
|
|
188
|
+
*/
|
|
189
|
+
async appCompleteFileUpload(app_id, body) {
|
|
190
|
+
return this.request("POST", `/v1/apps/${encodeURIComponent(app_id)}/files/complete`, body);
|
|
191
|
+
}
|
|
178
192
|
async deployAppVersion(args) {
|
|
179
193
|
const formData = new FormData();
|
|
180
194
|
// Wrap Buffers as Uint8Array views so the Blob constructor accepts them
|
|
@@ -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" | "workflow";
|
|
9
|
+
export type RpcOp = "query" | "workflow" | "upload_url" | "upload_complete";
|
|
10
10
|
export interface RpcRequest {
|
|
11
11
|
app_id: string;
|
|
12
12
|
op: RpcOp;
|
|
@@ -5,7 +5,12 @@
|
|
|
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([
|
|
8
|
+
const SUPPORTED_OPS = new Set([
|
|
9
|
+
"query",
|
|
10
|
+
"workflow",
|
|
11
|
+
"upload_url",
|
|
12
|
+
"upload_complete",
|
|
13
|
+
]);
|
|
9
14
|
export async function dispatchRpc(client, body) {
|
|
10
15
|
if (!body || typeof body.app_id !== "string" || typeof body.op !== "string") {
|
|
11
16
|
throw new Error("RPC envelope must include app_id and op");
|
|
@@ -28,6 +33,34 @@ export async function dispatchRpc(client, body) {
|
|
|
28
33
|
}
|
|
29
34
|
return client.appWorkflow(body.app_id, p.alias, p.inputs);
|
|
30
35
|
}
|
|
36
|
+
case "upload_url": {
|
|
37
|
+
const p = body.payload;
|
|
38
|
+
if (!p ||
|
|
39
|
+
typeof p.filename !== "string" ||
|
|
40
|
+
typeof p.mime_type !== "string" ||
|
|
41
|
+
typeof p.file_size !== "number") {
|
|
42
|
+
throw new Error("upload_url payload must include filename, mime_type, file_size");
|
|
43
|
+
}
|
|
44
|
+
return client.appRequestFileUpload(body.app_id, {
|
|
45
|
+
filename: p.filename,
|
|
46
|
+
mime_type: p.mime_type,
|
|
47
|
+
file_size: p.file_size,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
case "upload_complete": {
|
|
51
|
+
const p = body.payload;
|
|
52
|
+
if (!p ||
|
|
53
|
+
typeof p.file_id !== "string" ||
|
|
54
|
+
typeof p.file_storage_key !== "string" ||
|
|
55
|
+
typeof p.filename !== "string") {
|
|
56
|
+
throw new Error("upload_complete payload must include file_id, file_storage_key, filename");
|
|
57
|
+
}
|
|
58
|
+
return client.appCompleteFileUpload(body.app_id, {
|
|
59
|
+
file_id: p.file_id,
|
|
60
|
+
file_storage_key: p.file_storage_key,
|
|
61
|
+
filename: p.filename,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
31
64
|
default: {
|
|
32
65
|
// Unreachable — SUPPORTED_OPS gates above.
|
|
33
66
|
throw new Error(`Unhandled RPC op: ${body.op}`);
|
|
@@ -59,24 +59,59 @@ export function buildWrapperPage(args) {
|
|
|
59
59
|
const APP_ID = ${JSON.stringify(app_id)};
|
|
60
60
|
const iframe = document.getElementById("app");
|
|
61
61
|
|
|
62
|
+
async function rpc(op, payload) {
|
|
63
|
+
const res = await fetch("/_rpc", {
|
|
64
|
+
method: "POST",
|
|
65
|
+
headers: { "content-type": "application/json" },
|
|
66
|
+
body: JSON.stringify({ app_id: APP_ID, op: op, payload: payload }),
|
|
67
|
+
});
|
|
68
|
+
const text = await res.text();
|
|
69
|
+
if (!res.ok) {
|
|
70
|
+
let detail = text;
|
|
71
|
+
try { detail = JSON.parse(text).message ?? text; } catch (_) {}
|
|
72
|
+
throw new Error(detail || ("HTTP " + res.status));
|
|
73
|
+
}
|
|
74
|
+
return JSON.parse(text);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// The iframe SDK sends one "upload" op carrying a File. A File can't
|
|
78
|
+
// cross the JSON /_rpc hop, so the upload runs here in the browser:
|
|
79
|
+
// mint a presigned URL, PUT the bytes to storage, then finalize.
|
|
80
|
+
async function handleUpload(payload) {
|
|
81
|
+
const file = payload && payload.file;
|
|
82
|
+
if (!(file instanceof File)) {
|
|
83
|
+
throw new Error("upload payload must include a File");
|
|
84
|
+
}
|
|
85
|
+
const init = await rpc("upload_url", {
|
|
86
|
+
filename: file.name,
|
|
87
|
+
mime_type: file.type,
|
|
88
|
+
file_size: file.size,
|
|
89
|
+
});
|
|
90
|
+
const putRes = await fetch(init.upload_url, {
|
|
91
|
+
method: "PUT",
|
|
92
|
+
body: file,
|
|
93
|
+
headers: { "Content-Type": file.type },
|
|
94
|
+
});
|
|
95
|
+
if (!putRes.ok) {
|
|
96
|
+
throw new Error("Storage upload failed (" + putRes.status + ")");
|
|
97
|
+
}
|
|
98
|
+
const completed = await rpc("upload_complete", {
|
|
99
|
+
file_id: init.file_id,
|
|
100
|
+
file_storage_key: init.file_storage_key,
|
|
101
|
+
filename: file.name,
|
|
102
|
+
});
|
|
103
|
+
return completed.file;
|
|
104
|
+
}
|
|
105
|
+
|
|
62
106
|
window.addEventListener("message", async function (event) {
|
|
63
107
|
if (event.source !== iframe.contentWindow) return;
|
|
64
108
|
const msg = event.data;
|
|
65
109
|
if (!msg || typeof msg.id !== "number" || typeof msg.op !== "string") return;
|
|
66
110
|
const startedAt = performance.now();
|
|
67
111
|
try {
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
body: JSON.stringify({ app_id: APP_ID, op: msg.op, payload: msg.payload }),
|
|
72
|
-
});
|
|
73
|
-
const text = await res.text();
|
|
74
|
-
if (!res.ok) {
|
|
75
|
-
let detail = text;
|
|
76
|
-
try { detail = JSON.parse(text).message ?? text; } catch (_) {}
|
|
77
|
-
throw new Error(detail || ("HTTP " + res.status));
|
|
78
|
-
}
|
|
79
|
-
const data = JSON.parse(text);
|
|
112
|
+
const data = msg.op === "upload"
|
|
113
|
+
? await handleUpload(msg.payload)
|
|
114
|
+
: await rpc(msg.op, msg.payload);
|
|
80
115
|
const ms = Math.round(performance.now() - startedAt);
|
|
81
116
|
console.debug("[lotics-dev] " + msg.op + " " + ms + "ms", data);
|
|
82
117
|
iframe.contentWindow.postMessage({ id: msg.id, type: "result", data: data }, "*");
|
|
@@ -10,8 +10,10 @@
|
|
|
10
10
|
*
|
|
11
11
|
* Conventions (locked decisions):
|
|
12
12
|
* - Vite + React + TypeScript (strict mode).
|
|
13
|
-
* - Entry: src/App.tsx with `export default`. main.tsx wires
|
|
14
|
-
*
|
|
13
|
+
* - Entry: src/App.tsx with `export default`. main.tsx wires
|
|
14
|
+
* `mount(<PortalHost><App/></PortalHost>)` — PortalHost is the render
|
|
15
|
+
* target @lotics/ui overlays (Popover/Tooltip/Dialog) need — and imports
|
|
16
|
+
* @lotics/ui/index.css (base style reset) + @lotics/ui/fonts.css
|
|
15
17
|
* (path-independent Inter @font-face bundle — Text renders unstyled without it).
|
|
16
18
|
* - Vite default `base: "/"` so emitted asset URLs are absolute; the render
|
|
17
19
|
* endpoint rewrites root-relative paths to `/v1/apps/{id}/asset/...`.
|
|
@@ -10,8 +10,10 @@
|
|
|
10
10
|
*
|
|
11
11
|
* Conventions (locked decisions):
|
|
12
12
|
* - Vite + React + TypeScript (strict mode).
|
|
13
|
-
* - Entry: src/App.tsx with `export default`. main.tsx wires
|
|
14
|
-
*
|
|
13
|
+
* - Entry: src/App.tsx with `export default`. main.tsx wires
|
|
14
|
+
* `mount(<PortalHost><App/></PortalHost>)` — PortalHost is the render
|
|
15
|
+
* target @lotics/ui overlays (Popover/Tooltip/Dialog) need — and imports
|
|
16
|
+
* @lotics/ui/index.css (base style reset) + @lotics/ui/fonts.css
|
|
15
17
|
* (path-independent Inter @font-face bundle — Text renders unstyled without it).
|
|
16
18
|
* - Vite default `base: "/"` so emitted asset URLs are absolute; the render
|
|
17
19
|
* endpoint rewrites root-relative paths to `/v1/apps/{id}/asset/...`.
|
|
@@ -48,7 +50,7 @@ export function buildStarterTemplate(args) {
|
|
|
48
50
|
test: "vitest run",
|
|
49
51
|
},
|
|
50
52
|
dependencies: {
|
|
51
|
-
"@lotics/app-sdk": "^0.
|
|
53
|
+
"@lotics/app-sdk": "^0.6.0",
|
|
52
54
|
"@lotics/ui": "^1.3.0",
|
|
53
55
|
"@react-native-picker/picker": "^2.7.0",
|
|
54
56
|
"expo-image": "~3.0.9",
|
|
@@ -194,10 +196,17 @@ export default defineConfig({
|
|
|
194
196
|
path: "src/main.tsx",
|
|
195
197
|
content: `import "@lotics/ui/index.css";
|
|
196
198
|
import "@lotics/ui/fonts.css";
|
|
199
|
+
import { PortalHost } from "@lotics/ui/portal";
|
|
197
200
|
import { mount } from "@lotics/app-sdk";
|
|
198
201
|
import App from "./App";
|
|
199
202
|
|
|
200
|
-
|
|
203
|
+
// PortalHost is the render target for @lotics/ui overlays (Popover, Tooltip,
|
|
204
|
+
// Dialog). Without it, Portal renders nothing — keep it wrapping the app.
|
|
205
|
+
mount(
|
|
206
|
+
<PortalHost>
|
|
207
|
+
<App />
|
|
208
|
+
</PortalHost>,
|
|
209
|
+
);
|
|
201
210
|
`,
|
|
202
211
|
},
|
|
203
212
|
{
|