@lotics/cli 0.196.0 → 0.198.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/README.md +34 -9
- package/dist/probe_page.js +162 -24
- package/dist/render_page.js +152 -41
- package/dist/src/cli.js +5535 -937
- package/dist/src/client.d.ts +123 -24
- package/dist/src/client.js +64 -3
- package/docs/building_an_app.md +30 -4
- package/docs/cli_reference.md +20 -12
- package/package.json +1 -1
package/dist/src/client.d.ts
CHANGED
|
@@ -19,6 +19,34 @@ export interface AppQueryFilterGroup {
|
|
|
19
19
|
}
|
|
20
20
|
/** Runtime filter/sort the app query RPC applies AFTER the named query, bounded to its output columns. */
|
|
21
21
|
export type AppQueryFilter = AppQueryFilterCondition | AppQueryFilterGroup;
|
|
22
|
+
/**
|
|
23
|
+
* Body of the app query RPC — the one wire shape every transport carries, so
|
|
24
|
+
* the dev bridge forwards it whole rather than naming fields that then drift
|
|
25
|
+
* from what the deployed SDK sends.
|
|
26
|
+
*/
|
|
27
|
+
export interface AppQueryBody {
|
|
28
|
+
alias: string;
|
|
29
|
+
params?: Record<string, unknown>;
|
|
30
|
+
limit?: number;
|
|
31
|
+
offset?: number;
|
|
32
|
+
sort?: AppQuerySortKey[];
|
|
33
|
+
filter?: AppQueryFilter;
|
|
34
|
+
/** Return only `total` over the filtered set — no rows. */
|
|
35
|
+
count?: boolean;
|
|
36
|
+
/** Opt into keyset (seek) pagination, which answers with `next_cursor`. */
|
|
37
|
+
keyset?: boolean;
|
|
38
|
+
/** The previous page's `next_cursor`, in keyset mode. */
|
|
39
|
+
cursor?: string;
|
|
40
|
+
}
|
|
41
|
+
/** Result of the app query RPC. `total` answers a `count` read; `truncated` and
|
|
42
|
+
* `next_cursor` a rows read — `next_cursor` in keyset mode only, null on the
|
|
43
|
+
* last page. */
|
|
44
|
+
export interface AppQueryResult {
|
|
45
|
+
rows: unknown[];
|
|
46
|
+
total?: number;
|
|
47
|
+
truncated?: boolean;
|
|
48
|
+
next_cursor?: string | null;
|
|
49
|
+
}
|
|
22
50
|
/**
|
|
23
51
|
* Where a download's bytes go: the FILE to write, or the DIRECTORY to write the
|
|
24
52
|
* stored name into. Stated by the caller, never inferred from the path's shape —
|
|
@@ -52,6 +80,18 @@ export interface WorkspaceInfo {
|
|
|
52
80
|
organization_id: string;
|
|
53
81
|
created_at: string;
|
|
54
82
|
}
|
|
83
|
+
/**
|
|
84
|
+
* One hand-written report. Declared here rather than imported: this entry's
|
|
85
|
+
* published `.d.ts` must resolve for a consumer who has none of this package's
|
|
86
|
+
* other modules.
|
|
87
|
+
*/
|
|
88
|
+
export interface ReportFrame {
|
|
89
|
+
goal: string;
|
|
90
|
+
actual: string;
|
|
91
|
+
expected?: string;
|
|
92
|
+
tried?: string;
|
|
93
|
+
wanted?: string;
|
|
94
|
+
}
|
|
55
95
|
export interface ToolExecuteResult {
|
|
56
96
|
result: unknown;
|
|
57
97
|
model_output?: string;
|
|
@@ -313,11 +353,21 @@ export interface ScaffoldWorkspaceResult {
|
|
|
313
353
|
/** False when a table of that label already existed and was adopted. */
|
|
314
354
|
created: boolean;
|
|
315
355
|
}>;
|
|
356
|
+
/**
|
|
357
|
+
* Every role the model declares. `created` is false when a GROUP of that name
|
|
358
|
+
* was already here and the role bound to it — which is how a role silently
|
|
359
|
+
* inherits another workspace's membership, and why the line prints it the way
|
|
360
|
+
* an entity's does.
|
|
361
|
+
*/
|
|
316
362
|
roles: Array<{
|
|
317
363
|
alias: string;
|
|
318
364
|
group_id: string;
|
|
365
|
+
created: boolean;
|
|
319
366
|
}>;
|
|
320
|
-
/**
|
|
367
|
+
/**
|
|
368
|
+
* Ids of the rows written, per entity alias — the handle for deleting them
|
|
369
|
+
* again, and what `scaffold apply --documents` joins its rows to.
|
|
370
|
+
*/
|
|
321
371
|
record_ids: Record<string, string[]>;
|
|
322
372
|
/** Rows were sent and none were written, because the run adopted a table. */
|
|
323
373
|
rows_skipped: boolean;
|
|
@@ -576,24 +626,19 @@ export declare class LoticsClient {
|
|
|
576
626
|
organization_name: string;
|
|
577
627
|
}>;
|
|
578
628
|
/**
|
|
579
|
-
* File a hand-written report
|
|
580
|
-
* stamps are CORRECT: the invocation making
|
|
581
|
-
* is about, so the courier and the cargo are
|
|
629
|
+
* File a hand-written report, or a list of them in one call. Unlike a
|
|
630
|
+
* telemetry flush, the headers this stamps are CORRECT: the invocation making
|
|
631
|
+
* the request is the one the report is about, so the courier and the cargo are
|
|
632
|
+
* the same session.
|
|
582
633
|
*/
|
|
583
634
|
sendReport(body: {
|
|
584
|
-
report:
|
|
585
|
-
goal: string;
|
|
586
|
-
actual: string;
|
|
587
|
-
expected?: string;
|
|
588
|
-
tried?: string;
|
|
589
|
-
wanted?: string;
|
|
590
|
-
};
|
|
635
|
+
report: ReportFrame | ReportFrame[];
|
|
591
636
|
cli_session_id: string | null;
|
|
592
637
|
cli_version: string;
|
|
593
638
|
workspace_id?: string;
|
|
594
639
|
app_id?: string;
|
|
595
640
|
}): Promise<{
|
|
596
|
-
accepted:
|
|
641
|
+
accepted: number;
|
|
597
642
|
}>;
|
|
598
643
|
setWorkspaceId(id: string): void;
|
|
599
644
|
/** The workspace id the client targets (the `x-workspace-id` header), if resolved. */
|
|
@@ -602,6 +647,7 @@ export declare class LoticsClient {
|
|
|
602
647
|
createWorkspace(body: {
|
|
603
648
|
name: string;
|
|
604
649
|
timezone?: string;
|
|
650
|
+
default_currency?: string;
|
|
605
651
|
}): Promise<WorkspaceInfo>;
|
|
606
652
|
/**
|
|
607
653
|
* Apply a workspace MODEL to the current workspace — the from-scratch half of
|
|
@@ -708,10 +754,19 @@ export declare class LoticsClient {
|
|
|
708
754
|
* source archive so agent-authored bindings (via `set_app_workflow`)
|
|
709
755
|
* survive the pull/edit/deploy loop. Null/undefined on apps that have
|
|
710
756
|
* never had a workflow declared.
|
|
757
|
+
*
|
|
758
|
+
* `table_ids` is the set of tables the bound BODY touches, recorded when the
|
|
759
|
+
* body was verified. It is what makes a table an app only ever WRITES to
|
|
760
|
+
* addressable: codegen covers every table a query reads, and a workflow
|
|
761
|
+
* writing a table no screen reads had no `F`/`OPT` entry, so its body
|
|
762
|
+
* carried pasted `fld_`/`opt_` literals that a rename breaks silently
|
|
763
|
+
* instead of failing `tsc`. Absent on a binding written before the server
|
|
764
|
+
* recorded it — the set widens what codegen covers, it does not define it.
|
|
711
765
|
*/
|
|
712
766
|
workflows?: Record<string, {
|
|
713
767
|
workflow_id: string;
|
|
714
768
|
inputs?: Record<string, unknown>;
|
|
769
|
+
table_ids?: string[];
|
|
715
770
|
}> | null;
|
|
716
771
|
/**
|
|
717
772
|
* Live alias → query declaration map from `apps.queries`. Source of truth
|
|
@@ -748,6 +803,10 @@ export declare class LoticsClient {
|
|
|
748
803
|
name: string;
|
|
749
804
|
description?: string;
|
|
750
805
|
icon?: string;
|
|
806
|
+
/** The launcher tile's colour — the app's own, carried from whatever declared it. */
|
|
807
|
+
theme?: {
|
|
808
|
+
color?: string;
|
|
809
|
+
};
|
|
751
810
|
}): Promise<{
|
|
752
811
|
id: string;
|
|
753
812
|
name: string;
|
|
@@ -981,6 +1040,29 @@ export declare class LoticsClient {
|
|
|
981
1040
|
}): Promise<StarterPublish>;
|
|
982
1041
|
/** The state of a publish: which app is building, and the version once every dist is in. */
|
|
983
1042
|
getStarterPublish(publish_id: string): Promise<StarterPublish>;
|
|
1043
|
+
/**
|
|
1044
|
+
* The ROW RULE each of these tables declares — `private_filters` as stored, or
|
|
1045
|
+
* `null` where the table has none.
|
|
1046
|
+
*
|
|
1047
|
+
* `GET /v1/tables/{id}` rather than the `get_table` tool beside it: the rule is
|
|
1048
|
+
* an IAM fact about who may read a row, and the tool's output is the schema an
|
|
1049
|
+
* agent writes records against, which is why it carries fields and not this.
|
|
1050
|
+
* One call per id. A table this credential may not read — 403, or 404 for one
|
|
1051
|
+
* that is gone — is DROPPED: the only caller warns about rules it can see, and
|
|
1052
|
+
* a table it cannot read is not evidence of one. EVERY other failure throws.
|
|
1053
|
+
* An expired key, a 500, a timeout and an offline host all mean the scan has
|
|
1054
|
+
* no answer, and swallowing them would render as "this table declares no
|
|
1055
|
+
* rule" — the guard at its quietest exactly where it knows least.
|
|
1056
|
+
*
|
|
1057
|
+
* The filter is carried untyped: a published `.d.ts` cannot name
|
|
1058
|
+
* `@lotics/shared`'s filter schema, and the one reader asks a single question
|
|
1059
|
+
* of the tree rather than interpreting it.
|
|
1060
|
+
*/
|
|
1061
|
+
getTableRowRules(tableIds: string[]): Promise<Array<{
|
|
1062
|
+
id: string;
|
|
1063
|
+
name: string;
|
|
1064
|
+
private_filters: unknown;
|
|
1065
|
+
}>>;
|
|
984
1066
|
/**
|
|
985
1067
|
* Resolve the display name + fields (incl. select options) of the given tables
|
|
986
1068
|
* — the schema `lotics app codegen` turns into the runtime `.lotics/app_fields.ts`
|
|
@@ -1042,18 +1124,7 @@ export declare class LoticsClient {
|
|
|
1042
1124
|
* Run a named query declared in the app's manifest, scoped to the app's IAM
|
|
1043
1125
|
* principal. Mirrors POST /v1/apps/{app_id}/query.
|
|
1044
1126
|
*/
|
|
1045
|
-
appQuery(app_id: string, body:
|
|
1046
|
-
alias: string;
|
|
1047
|
-
params?: Record<string, unknown>;
|
|
1048
|
-
limit?: number;
|
|
1049
|
-
offset?: number;
|
|
1050
|
-
sort?: AppQuerySortKey[];
|
|
1051
|
-
filter?: AppQueryFilter;
|
|
1052
|
-
count?: boolean;
|
|
1053
|
-
}): Promise<{
|
|
1054
|
-
rows: unknown[];
|
|
1055
|
-
total?: number;
|
|
1056
|
-
}>;
|
|
1127
|
+
appQuery(app_id: string, body: AppQueryBody): Promise<AppQueryResult>;
|
|
1057
1128
|
appMembers(app_id: string, group_id?: string): Promise<{
|
|
1058
1129
|
members: Array<{
|
|
1059
1130
|
id: string;
|
|
@@ -1142,6 +1213,13 @@ export declare class LoticsClient {
|
|
|
1142
1213
|
* `lotics app pull`, which writes it to `src/workflows/<alias>.ts`.
|
|
1143
1214
|
*/
|
|
1144
1215
|
getAppWorkflow(app_id: string, alias: string): Promise<ToolExecuteResult>;
|
|
1216
|
+
/**
|
|
1217
|
+
* The app's capability catalog exactly as a chat or MCP caller reads it —
|
|
1218
|
+
* every query, workflow and agent alias the caller's scope reaches, with the
|
|
1219
|
+
* description each is chosen BY. One read for the whole app, so a check over
|
|
1220
|
+
* what those readers see costs one request rather than one per alias.
|
|
1221
|
+
*/
|
|
1222
|
+
getAppCapabilities(app_id: string): Promise<ToolExecuteResult>;
|
|
1145
1223
|
/**
|
|
1146
1224
|
* Fetch the server-generated workspace `.d.ts` + the wrapper envelope that
|
|
1147
1225
|
* make a `src/workflows/<alias>.ts` body locally typecheckable (GAP-59).
|
|
@@ -1294,6 +1372,27 @@ export declare class LoticsClient {
|
|
|
1294
1372
|
bundle_size_bytes: number;
|
|
1295
1373
|
}>;
|
|
1296
1374
|
downloadFile(url: string, outputPath: string): Promise<string>;
|
|
1375
|
+
/**
|
|
1376
|
+
* One page of this workspace's stored files, newest first — `GET /v1/files`.
|
|
1377
|
+
*
|
|
1378
|
+
* The only enumeration there is: every other file surface takes an id, so
|
|
1379
|
+
* without this "what is in the store" is answerable from Postgres alone, which
|
|
1380
|
+
* is exactly the escape hatch a CLI-first workflow exists to remove. Paged by a
|
|
1381
|
+
* keyset cursor the server mints; a caller walks until `next_cursor` is null.
|
|
1382
|
+
*/
|
|
1383
|
+
listFiles(opts?: {
|
|
1384
|
+
limit?: number;
|
|
1385
|
+
cursor?: string;
|
|
1386
|
+
}): Promise<{
|
|
1387
|
+
files: Array<{
|
|
1388
|
+
id: string;
|
|
1389
|
+
filename: string;
|
|
1390
|
+
mime_type: string;
|
|
1391
|
+
size?: number | null;
|
|
1392
|
+
created_at: string;
|
|
1393
|
+
}>;
|
|
1394
|
+
next_cursor: string | null;
|
|
1395
|
+
}>;
|
|
1297
1396
|
downloadFileById(fileId: string, destination?: DownloadDestination, options?: {
|
|
1298
1397
|
reserved?: Set<string>;
|
|
1299
1398
|
}): Promise<{
|
package/dist/src/client.js
CHANGED
|
@@ -323,9 +323,10 @@ var LoticsClient = class {
|
|
|
323
323
|
return this.request("GET", "/v1/cli/whoami");
|
|
324
324
|
}
|
|
325
325
|
/**
|
|
326
|
-
* File a hand-written report
|
|
327
|
-
* stamps are CORRECT: the invocation making
|
|
328
|
-
* is about, so the courier and the cargo are
|
|
326
|
+
* File a hand-written report, or a list of them in one call. Unlike a
|
|
327
|
+
* telemetry flush, the headers this stamps are CORRECT: the invocation making
|
|
328
|
+
* the request is the one the report is about, so the courier and the cargo are
|
|
329
|
+
* the same session.
|
|
329
330
|
*/
|
|
330
331
|
async sendReport(body) {
|
|
331
332
|
return this.request("POST", "/v1/cli/report", body);
|
|
@@ -613,6 +614,42 @@ var LoticsClient = class {
|
|
|
613
614
|
async getStarterPublish(publish_id) {
|
|
614
615
|
return this.request("GET", `/v1/starters/publishes/${encodeURIComponent(publish_id)}`);
|
|
615
616
|
}
|
|
617
|
+
/**
|
|
618
|
+
* The ROW RULE each of these tables declares — `private_filters` as stored, or
|
|
619
|
+
* `null` where the table has none.
|
|
620
|
+
*
|
|
621
|
+
* `GET /v1/tables/{id}` rather than the `get_table` tool beside it: the rule is
|
|
622
|
+
* an IAM fact about who may read a row, and the tool's output is the schema an
|
|
623
|
+
* agent writes records against, which is why it carries fields and not this.
|
|
624
|
+
* One call per id. A table this credential may not read — 403, or 404 for one
|
|
625
|
+
* that is gone — is DROPPED: the only caller warns about rules it can see, and
|
|
626
|
+
* a table it cannot read is not evidence of one. EVERY other failure throws.
|
|
627
|
+
* An expired key, a 500, a timeout and an offline host all mean the scan has
|
|
628
|
+
* no answer, and swallowing them would render as "this table declares no
|
|
629
|
+
* rule" — the guard at its quietest exactly where it knows least.
|
|
630
|
+
*
|
|
631
|
+
* The filter is carried untyped: a published `.d.ts` cannot name
|
|
632
|
+
* `@lotics/shared`'s filter schema, and the one reader asks a single question
|
|
633
|
+
* of the tree rather than interpreting it.
|
|
634
|
+
*/
|
|
635
|
+
async getTableRowRules(tableIds) {
|
|
636
|
+
const tables = await Promise.all(
|
|
637
|
+
tableIds.map(async (table_id) => {
|
|
638
|
+
try {
|
|
639
|
+
const table = await this.request(
|
|
640
|
+
"GET",
|
|
641
|
+
`/v1/tables/${encodeURIComponent(table_id)}`
|
|
642
|
+
);
|
|
643
|
+
return { id: table.id, name: table.name, private_filters: table.private_filters ?? null };
|
|
644
|
+
} catch (error) {
|
|
645
|
+
const notVisible = error instanceof LoticsRequestError && (error.status === 403 || error.status === 404);
|
|
646
|
+
if (!notVisible) throw error;
|
|
647
|
+
return null;
|
|
648
|
+
}
|
|
649
|
+
})
|
|
650
|
+
);
|
|
651
|
+
return tables.filter((table) => table !== null);
|
|
652
|
+
}
|
|
616
653
|
/**
|
|
617
654
|
* Resolve the display name + fields (incl. select options) of the given tables
|
|
618
655
|
* — the schema `lotics app codegen` turns into the runtime `.lotics/app_fields.ts`
|
|
@@ -790,6 +827,15 @@ var LoticsClient = class {
|
|
|
790
827
|
async getAppWorkflow(app_id, alias) {
|
|
791
828
|
return this.execute("get_app_workflow", { app_id, alias });
|
|
792
829
|
}
|
|
830
|
+
/**
|
|
831
|
+
* The app's capability catalog exactly as a chat or MCP caller reads it —
|
|
832
|
+
* every query, workflow and agent alias the caller's scope reaches, with the
|
|
833
|
+
* description each is chosen BY. One read for the whole app, so a check over
|
|
834
|
+
* what those readers see costs one request rather than one per alias.
|
|
835
|
+
*/
|
|
836
|
+
async getAppCapabilities(app_id) {
|
|
837
|
+
return this.execute("get_app_capabilities", { app_id });
|
|
838
|
+
}
|
|
793
839
|
/**
|
|
794
840
|
* Fetch the server-generated workspace `.d.ts` + the wrapper envelope that
|
|
795
841
|
* make a `src/workflows/<alias>.ts` body locally typecheckable (GAP-59).
|
|
@@ -983,6 +1029,21 @@ var LoticsClient = class {
|
|
|
983
1029
|
await fs.promises.writeFile(absolutePath, buffer);
|
|
984
1030
|
return absolutePath;
|
|
985
1031
|
}
|
|
1032
|
+
/**
|
|
1033
|
+
* One page of this workspace's stored files, newest first — `GET /v1/files`.
|
|
1034
|
+
*
|
|
1035
|
+
* The only enumeration there is: every other file surface takes an id, so
|
|
1036
|
+
* without this "what is in the store" is answerable from Postgres alone, which
|
|
1037
|
+
* is exactly the escape hatch a CLI-first workflow exists to remove. Paged by a
|
|
1038
|
+
* keyset cursor the server mints; a caller walks until `next_cursor` is null.
|
|
1039
|
+
*/
|
|
1040
|
+
async listFiles(opts) {
|
|
1041
|
+
const params = new URLSearchParams();
|
|
1042
|
+
if (opts?.limit !== void 0) params.set("limit", String(opts.limit));
|
|
1043
|
+
if (opts?.cursor !== void 0) params.set("cursor", opts.cursor);
|
|
1044
|
+
const query = params.toString();
|
|
1045
|
+
return this.request("GET", `/v1/files${query ? `?${query}` : ""}`);
|
|
1046
|
+
}
|
|
986
1047
|
async downloadFileById(fileId, destination, options) {
|
|
987
1048
|
const signedHeaders = this.buildHeaders();
|
|
988
1049
|
const signedUrlRes = await fetch(
|
package/docs/building_an_app.md
CHANGED
|
@@ -22,7 +22,10 @@ cd <dir> && lotics app pull <app_id> # existing: refresh to the latest first,
|
|
|
22
22
|
each screen as the registry shape over the live table its entity became — a `LifecycleDesk`, a
|
|
23
23
|
`PartyRegister`, … from `@lotics/ui`, its slots reading the fields the plan bound. The tables
|
|
24
24
|
have to exist (`lotics scaffold apply` first); a table or field the workspace lacks is refused by
|
|
25
|
-
name. A `custom` screen arrives as
|
|
25
|
+
name. A `custom` screen arrives as a register of the cells its roles name, cut to what still reads
|
|
26
|
+
at a phone's width — its slot list is a checklist of what the rows know, never a layout. The
|
|
27
|
+
project's `README.md` is the plan in prose: the app, its screens with their routes and shapes, and
|
|
28
|
+
the tables behind them.
|
|
26
29
|
|
|
27
30
|
A pull writes more than source: one `src/workflows/<alias>.ts` per bound workflow, one
|
|
28
31
|
`src/agents/<alias>.md` per bound agent, and the `.lotics/` type companions — so an existing app
|
|
@@ -105,9 +108,10 @@ Address fields by alias, never by a pasted id:
|
|
|
105
108
|
row.opt(r[F.SHIPMENT.direction]) === OPT.SHIPMENT.direction.export
|
|
106
109
|
```
|
|
107
110
|
|
|
108
|
-
|
|
109
|
-
fails `tsc`
|
|
110
|
-
only honest if the generated ids are current, and you never
|
|
111
|
+
An alias is slugified from the display name, so a rename on the platform MOVES it: re-run `codegen`
|
|
112
|
+
and every call site on the old alias fails `tsc` until it names the new one. **Re-run after any
|
|
113
|
+
schema change** — local typecheck is only honest if the generated ids are current, and you never
|
|
114
|
+
deploy to refresh types.
|
|
111
115
|
|
|
112
116
|
## 5 — Named queries
|
|
113
117
|
|
|
@@ -129,6 +133,22 @@ Details: `lotics docs queries`.
|
|
|
129
133
|
|
|
130
134
|
## 6 — Workflows, the only way an app writes
|
|
131
135
|
|
|
136
|
+
**A workflow binding is SOURCE, and it deploys with the app.** Its two halves are
|
|
137
|
+
`package.json#lotics.workflows.<alias>` and `src/workflows/<alias>.ts`; `lotics app pull` writes
|
|
138
|
+
both, and `lotics app deploy` pushes whatever differs from what it last saw live — through
|
|
139
|
+
`set_app_workflow`, before the bundle ships, so a version that shipped always reproduces what runs.
|
|
140
|
+
Nothing is bound by hand after a deploy, and nothing needs to be: an alias whose body and
|
|
141
|
+
declaration are in the repo is an alias the next clone can build. Three consequences worth knowing
|
|
142
|
+
before you write one:
|
|
143
|
+
|
|
144
|
+
- **A push refuses when the live body moved past the copy you edited.** The baseline rides as a
|
|
145
|
+
precondition, so two people editing one alias is a refusal naming it, never a silent clobber.
|
|
146
|
+
- **An unchanged alias is not pushed.** An update is a diff, so a deploy that changes one screen
|
|
147
|
+
does not re-push nine workflows.
|
|
148
|
+
- **Deleting the declaration and the file does not unbind it.** The binding keeps serving, and
|
|
149
|
+
keeps being published to chat and to MCP. `lotics app deploy --prune` unbinds what the project no
|
|
150
|
+
longer names, and it is opt-in because an alias can be invoked from outside the bundle.
|
|
151
|
+
|
|
132
152
|
A workflow body is a file you open and edit. The new-alias path is typed from the first line:
|
|
133
153
|
|
|
134
154
|
1. Declare it in `package.json#lotics.workflows.<alias>` — its `inputs`, and `outputs` only to
|
|
@@ -232,6 +252,12 @@ Vite plus an RPC-forwarding server, in a sandboxed iframe matching production, w
|
|
|
232
252
|
auth and HMR. File flows work too — the dev server relays the bytes, so upload, preview and
|
|
233
253
|
download are all exercisable locally.
|
|
234
254
|
|
|
255
|
+
**Navigate with `waitUntil: "domcontentloaded"`.** A dev app never goes network-quiet — the Vite
|
|
256
|
+
client and the app's own cross-origin frame each keep a connection open — so a driver that opts
|
|
257
|
+
into `networkidle` waits out its timeout on an app that rendered fine, and the timeout reads as the
|
|
258
|
+
app being broken. Any in-app path opens directly (`http://localhost:<port>/lo/rec_…`); the wrapper
|
|
259
|
+
serves every path that is not one of its own `/_…` routes.
|
|
260
|
+
|
|
235
261
|
**Drive it with a browser, in this order** — each step's failure means something different:
|
|
236
262
|
|
|
237
263
|
1. **Does it render at all?** A blank iframe is almost always a bundling problem, not your code.
|