@lotics/cli 0.71.0 → 0.74.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/client.d.ts CHANGED
@@ -43,6 +43,14 @@ export interface WorkspaceInfo {
43
43
  default_currency: string;
44
44
  organization_id: string;
45
45
  created_at: string;
46
+ /**
47
+ * Whether this is a throwaway app-package dev workspace (set with
48
+ * `lotics workspace create <name> --dev`). Gates the destructive package
49
+ * dev/sync scaffold + the server-side `package reset`. Optional because the
50
+ * list endpoint only carries it once the server serializes `is_dev`; an absent
51
+ * value is treated as non-dev by the guard (fail closed).
52
+ */
53
+ is_dev?: boolean;
46
54
  }
47
55
  export interface ToolExecuteResult {
48
56
  result: unknown;
@@ -65,6 +73,28 @@ export interface FileUploadResult {
65
73
  error: string;
66
74
  }>;
67
75
  }
76
+ /**
77
+ * A materialized app package's binding — alias → this workspace's concrete id,
78
+ * one string-map per namespace. Mirrors the server's `bindingSchema`. The
79
+ * `workflows` map is the entity-lifecycle table-workflow ROW ledger (an artifact
80
+ * registry, not a schema binding), carried in the same shape. The CLI never
81
+ * interprets these maps — it round-trips them verbatim between `extract` (which
82
+ * emits the origin workspace's) and `adopt` (which replays it onto the app).
83
+ */
84
+ export interface PackageBinding {
85
+ entities: Record<string, string>;
86
+ fields: Record<string, string>;
87
+ options: Record<string, string>;
88
+ templates: Record<string, string>;
89
+ roles: Record<string, string>;
90
+ workflows: Record<string, string>;
91
+ }
92
+ /** One finding from a bespoke→package extraction (`extractAppPackage`). */
93
+ export interface ExtractFinding {
94
+ severity: "error" | "warning" | "info";
95
+ area: string;
96
+ message: string;
97
+ }
68
98
  export declare const API_BASE_URL: string;
69
99
  export declare class LoticsClient {
70
100
  private apiKey;
@@ -88,10 +118,19 @@ export declare class LoticsClient {
88
118
  organization_name: string;
89
119
  }>;
90
120
  setWorkspaceId(id: string): void;
121
+ /** The workspace id the client targets (the `x-workspace-id` header), if resolved. */
122
+ getWorkspaceId(): string | undefined;
91
123
  listWorkspaces(): Promise<WorkspaceInfo[]>;
124
+ /**
125
+ * Resolve one workspace's info by id from the org's workspace list (the only
126
+ * API-key-accessible source carrying `is_dev`). Returns null when the
127
+ * workspace isn't visible to these credentials.
128
+ */
129
+ getWorkspaceInfo(id: string): Promise<WorkspaceInfo | null>;
92
130
  createWorkspace(body: {
93
131
  name: string;
94
132
  timezone?: string;
133
+ is_dev?: boolean;
95
134
  }): Promise<WorkspaceInfo>;
96
135
  deleteWorkspace(id: string): Promise<{
97
136
  id: string;
@@ -155,6 +194,12 @@ export declare class LoticsClient {
155
194
  inputs?: Record<string, unknown>;
156
195
  outputs?: Record<string, unknown>;
157
196
  }> | null;
197
+ /**
198
+ * Installation-level customization config values (the `useConfig()`
199
+ * source). Null/undefined for a bespoke app. The dev harness serves these
200
+ * through the `context` op for production parity.
201
+ */
202
+ config?: Record<string, string | number | boolean> | null;
158
203
  }>;
159
204
  createApp(body: {
160
205
  name: string;
@@ -166,6 +211,276 @@ export declare class LoticsClient {
166
211
  workspace_id: string;
167
212
  current_version_id: string | null;
168
213
  }>;
214
+ /**
215
+ * Install an app package version into the current workspace — scaffolds the
216
+ * data model, deploys the package bundle, materializes its
217
+ * queries/workflows/agents, and pins the installation. Returns the resulting
218
+ * installation app. `version` omitted installs the latest published version.
219
+ */
220
+ installAppPackage(package_id: string, body: {
221
+ version?: number;
222
+ }): Promise<{
223
+ id: string;
224
+ name: string;
225
+ workspace_id: string;
226
+ package_id: string | null;
227
+ package_version: number | null;
228
+ current_version_id: string | null;
229
+ }>;
230
+ /**
231
+ * Eject an installation from its package — re-deploy the pinned version's
232
+ * source as a workspace-owned app version, then sever the package link
233
+ * (clears `package_id`/`package_version`, stamps `ejected_at`). One-way: the
234
+ * app becomes a normal bespoke app and can no longer be upgraded. Returns the
235
+ * resulting app.
236
+ */
237
+ ejectAppPackage(app_id: string): Promise<{
238
+ id: string;
239
+ name: string;
240
+ workspace_id: string;
241
+ current_version_id: string | null;
242
+ }>;
243
+ /**
244
+ * Extract a DRAFT app package from an existing bespoke app — the promotion
245
+ * read (docs/app_packages.md § Promotion). Pure: nothing is written. Returns
246
+ * the alias-keyed draft `contract` (opaque to the CLI — the server is the
247
+ * validating authority), the origin workspace's `binding` (which doubles as
248
+ * the adopt binding), a findings `report` (any `error` ⇒ not publishable
249
+ * as-is), and the file-backed `template_files` the CLI must stage into the
250
+ * project at their `bytes_ref` paths. Backs `lotics package extract`.
251
+ * Admin-only.
252
+ */
253
+ extractAppPackage(app_id: string): Promise<{
254
+ contract: unknown;
255
+ binding: PackageBinding;
256
+ report: ExtractFinding[];
257
+ template_files: Array<{
258
+ bytes_ref: string;
259
+ file_id: string;
260
+ filename: string;
261
+ }>;
262
+ }>;
263
+ /**
264
+ * Adopt a published package onto an EXISTING (bespoke or ejected) app — the
265
+ * final promotion step. The workspace already holds the concrete objects, so
266
+ * nothing is scaffolded or rewritten: the server verifies the `binding` is
267
+ * complete, live, and FAITHFUL to the version's contract, then writes only the
268
+ * installation pin (the app becomes installation #1, upgradeable again). A
269
+ * ConflictError names the aliases that diverge. `version` omitted adopts the
270
+ * latest. Backs `lotics package adopt`. Admin-only.
271
+ */
272
+ adoptAppPackage(app_id: string, body: {
273
+ package_id: string;
274
+ version?: number;
275
+ binding: PackageBinding;
276
+ }): Promise<{
277
+ id: string;
278
+ name: string;
279
+ package_id: string | null;
280
+ package_version: number | null;
281
+ }>;
282
+ /**
283
+ * Fleet upgrade — bring every installation of a package across the CALLER'S
284
+ * org to the target version (latest when omitted) in one call. Hands-off
285
+ * applies only where the preview is clean; installations with breaking/
286
+ * drift/modified-core findings are skipped and reported for the normal
287
+ * per-installation consent flow. Backs `lotics package fleet-upgrade`.
288
+ * Admin-only; org-scoped (no workspace header needed).
289
+ */
290
+ fleetUpgradeAppPackage(package_id: string, body: {
291
+ version?: number;
292
+ }): Promise<{
293
+ package_id: string;
294
+ target_version: number;
295
+ installations: Array<{
296
+ app_id: string;
297
+ app_name: string;
298
+ workspace_id: string;
299
+ workspace_name: string;
300
+ from_version: number | null;
301
+ outcome: "upgraded" | "up_to_date" | "skipped" | "failed";
302
+ blockers?: {
303
+ breaking: number;
304
+ drift: number;
305
+ modified: number;
306
+ };
307
+ message?: string;
308
+ }>;
309
+ }>;
310
+ /**
311
+ * Create a registry app package — the Lotics-owned, workspace-agnostic
312
+ * blueprint. `lotics package publish` calls this on first publish (when the
313
+ * local manifest has no `package_id`), then publishes version 1 against the
314
+ * returned id. Admin-only.
315
+ */
316
+ createAppPackage(body: {
317
+ name: string;
318
+ description?: string | null;
319
+ }): Promise<{
320
+ id: string;
321
+ name: string;
322
+ description: string | null;
323
+ latest_version: number;
324
+ is_official: boolean;
325
+ created_at: string;
326
+ updated_at: string;
327
+ }>;
328
+ /**
329
+ * Fetch a registry app package's metadata (incl. `latest_version` and the
330
+ * Lotics-backed `is_official` trust badge). Admin-only; cross-tenant by id.
331
+ */
332
+ getAppPackage(package_id: string): Promise<{
333
+ id: string;
334
+ name: string;
335
+ description: string | null;
336
+ latest_version: number;
337
+ is_official: boolean;
338
+ created_at: string;
339
+ updated_at: string;
340
+ }>;
341
+ /**
342
+ * Publish a new immutable package version — multipart upload of the alias-keyed
343
+ * contract (JSON) + the prebuilt code bundle (a gzipped tarball carrying
344
+ * `source.tar.gz` + `dist.tar.gz` members). The server validates the contract +
345
+ * bundle, then allocates the next monotonic version. The `contract` is opaque
346
+ * JSON to the transport (the server is the validating authority). Admin-only.
347
+ */
348
+ publishAppPackageVersion(package_id: string, args: {
349
+ contract: unknown;
350
+ bundle: Buffer;
351
+ changelog?: string | null;
352
+ }): Promise<{
353
+ id: string;
354
+ package_id: string;
355
+ version: number;
356
+ bundle_r2_prefix: string;
357
+ changelog: string | null;
358
+ created_at: string;
359
+ }>;
360
+ /**
361
+ * Upgrade a package installation to a newer published version — extends the
362
+ * binding additively, re-materializes the target version's
363
+ * queries/workflows/agents (preserving workspace overlay), prunes dropped
364
+ * package artifacts, and bumps the pin. `version` omitted upgrades to the
365
+ * latest. Returns the resulting app. Admin-only.
366
+ */
367
+ upgradeAppPackage(app_id: string, body: {
368
+ version?: number;
369
+ /**
370
+ * Per preview finding: drifted binding entries (`<namespace>.<alias>`)
371
+ * take `"recreate"` or `{ bind_to: "<id>" }`; locally modified
372
+ * artifacts (`<kind>.<alias>`) take `"revert"` or `"keep"`. Required
373
+ * for every finding — anything unresolved refuses the upgrade.
374
+ */
375
+ resolutions?: Record<string, "recreate" | "revert" | "keep" | {
376
+ bind_to: string;
377
+ }>;
378
+ }): Promise<{
379
+ id: string;
380
+ name: string;
381
+ workspace_id: string;
382
+ package_id: string | null;
383
+ package_version: number | null;
384
+ current_version_id: string | null;
385
+ }>;
386
+ /**
387
+ * Preview a package upgrade — the additive plan, informational removals,
388
+ * breaking contract changes, the binding drift report, and the modified-core
389
+ * report — with no writes. Admin-only.
390
+ */
391
+ previewAppPackageUpgrade(app_id: string, opts?: {
392
+ version?: number;
393
+ }): Promise<{
394
+ app_id: string;
395
+ package_id: string;
396
+ from_version: number;
397
+ to_version: number;
398
+ changelog: string | null;
399
+ diff: {
400
+ added: Record<string, unknown[]>;
401
+ removed: Record<string, unknown[]>;
402
+ breaking: Array<{
403
+ entity: string;
404
+ alias: string;
405
+ kind: "field_type" | "link_target";
406
+ from: string;
407
+ to: string;
408
+ }>;
409
+ };
410
+ drift: Array<{
411
+ namespace: string;
412
+ alias: string;
413
+ id: string;
414
+ }>;
415
+ modified: Array<{
416
+ kind: string;
417
+ alias: string;
418
+ }>;
419
+ }>;
420
+ /**
421
+ * Re-bind a package role to a different workspace group (the current group
422
+ * still exists). Re-materializes at the pinned version; refuses over
423
+ * modified-core findings. Admin-only.
424
+ */
425
+ rebindAppPackageRole(app_id: string, body: {
426
+ role_alias: string;
427
+ group_id: string;
428
+ }): Promise<{
429
+ app_id: string;
430
+ role_alias: string;
431
+ group_id: string;
432
+ previous_group_id: string | null;
433
+ }>;
434
+ /**
435
+ * Workspace-wide dangling-reference sweep — active app/workflow artifacts
436
+ * whose prefixed schema ids no longer resolve. Backs
437
+ * `lotics workspace doctor`. Admin-only.
438
+ */
439
+ getWorkspaceDanglingReferences(): Promise<Array<{
440
+ namespace: string;
441
+ id: string;
442
+ referent: {
443
+ kind: string;
444
+ id: string;
445
+ name: string;
446
+ };
447
+ }>>;
448
+ /**
449
+ * Health check for a package installation — version pin vs. registry latest,
450
+ * binding drift, and locally modified core artifacts. Read-only; backs
451
+ * `lotics package doctor`. Admin-only.
452
+ */
453
+ getAppPackageHealth(app_id: string): Promise<{
454
+ app_id: string;
455
+ package_id: string;
456
+ package_name: string;
457
+ installed_version: number;
458
+ latest_version: number;
459
+ update_available: boolean;
460
+ drift: Array<{
461
+ namespace: string;
462
+ alias: string;
463
+ id: string;
464
+ }>;
465
+ modified: Array<{
466
+ kind: string;
467
+ alias: string;
468
+ }>;
469
+ }>;
470
+ /**
471
+ * Reset a package installation in a DEV workspace — drop the package-owned
472
+ * scaffolded tables and re-scaffold clean. Hard-gated server-side to dev
473
+ * workspaces (a non-dev workspace is refused). Returns the resulting app.
474
+ * Admin-only.
475
+ */
476
+ resetAppPackage(app_id: string): Promise<{
477
+ id: string;
478
+ name: string;
479
+ workspace_id: string;
480
+ package_id: string | null;
481
+ package_version: number | null;
482
+ current_version_id: string | null;
483
+ }>;
169
484
  /**
170
485
  * Resolve the display name + fields (incl. select options) of the given tables
171
486
  * — the schema `lotics app codegen` turns into the runtime `.lotics/app_fields.ts`
@@ -244,6 +559,12 @@ export declare class LoticsClient {
244
559
  image: string | null;
245
560
  }>;
246
561
  }>;
562
+ /** A package installation's alias→id maps (fields/options/roles) — the app's runtime F/OPT resolution. */
563
+ appBinding(app_id: string): Promise<{
564
+ fields: Record<string, string>;
565
+ options: Record<string, string>;
566
+ roles: Record<string, string>;
567
+ }>;
247
568
  /**
248
569
  * Resolve the full option set (key, label, color) of a named query's select
249
570
  * columns — the picker companion to `appQuery`. Mirrors
package/dist/client.js CHANGED
@@ -136,9 +136,22 @@ export class LoticsClient {
136
136
  setWorkspaceId(id) {
137
137
  this.workspaceId = id;
138
138
  }
139
+ /** The workspace id the client targets (the `x-workspace-id` header), if resolved. */
140
+ getWorkspaceId() {
141
+ return this.workspaceId;
142
+ }
139
143
  async listWorkspaces() {
140
144
  return this.request("GET", "/v1/workspaces");
141
145
  }
146
+ /**
147
+ * Resolve one workspace's info by id from the org's workspace list (the only
148
+ * API-key-accessible source carrying `is_dev`). Returns null when the
149
+ * workspace isn't visible to these credentials.
150
+ */
151
+ async getWorkspaceInfo(id) {
152
+ const workspaces = await this.listWorkspaces();
153
+ return workspaces.find((w) => w.id === id) ?? null;
154
+ }
142
155
  async createWorkspace(body) {
143
156
  return this.request("POST", "/v1/workspaces", body);
144
157
  }
@@ -190,6 +203,153 @@ export class LoticsClient {
190
203
  async createApp(body) {
191
204
  return this.request("POST", "/v1/apps", body);
192
205
  }
206
+ /**
207
+ * Install an app package version into the current workspace — scaffolds the
208
+ * data model, deploys the package bundle, materializes its
209
+ * queries/workflows/agents, and pins the installation. Returns the resulting
210
+ * installation app. `version` omitted installs the latest published version.
211
+ */
212
+ async installAppPackage(package_id, body) {
213
+ return this.request("POST", `/v1/app-packages/${encodeURIComponent(package_id)}/installations`, body);
214
+ }
215
+ /**
216
+ * Eject an installation from its package — re-deploy the pinned version's
217
+ * source as a workspace-owned app version, then sever the package link
218
+ * (clears `package_id`/`package_version`, stamps `ejected_at`). One-way: the
219
+ * app becomes a normal bespoke app and can no longer be upgraded. Returns the
220
+ * resulting app.
221
+ */
222
+ async ejectAppPackage(app_id) {
223
+ return this.request("POST", `/v1/apps/${encodeURIComponent(app_id)}/eject`);
224
+ }
225
+ /**
226
+ * Extract a DRAFT app package from an existing bespoke app — the promotion
227
+ * read (docs/app_packages.md § Promotion). Pure: nothing is written. Returns
228
+ * the alias-keyed draft `contract` (opaque to the CLI — the server is the
229
+ * validating authority), the origin workspace's `binding` (which doubles as
230
+ * the adopt binding), a findings `report` (any `error` ⇒ not publishable
231
+ * as-is), and the file-backed `template_files` the CLI must stage into the
232
+ * project at their `bytes_ref` paths. Backs `lotics package extract`.
233
+ * Admin-only.
234
+ */
235
+ async extractAppPackage(app_id) {
236
+ return this.request("GET", `/v1/apps/${encodeURIComponent(app_id)}/package-extract`);
237
+ }
238
+ /**
239
+ * Adopt a published package onto an EXISTING (bespoke or ejected) app — the
240
+ * final promotion step. The workspace already holds the concrete objects, so
241
+ * nothing is scaffolded or rewritten: the server verifies the `binding` is
242
+ * complete, live, and FAITHFUL to the version's contract, then writes only the
243
+ * installation pin (the app becomes installation #1, upgradeable again). A
244
+ * ConflictError names the aliases that diverge. `version` omitted adopts the
245
+ * latest. Backs `lotics package adopt`. Admin-only.
246
+ */
247
+ async adoptAppPackage(app_id, body) {
248
+ return this.request("POST", `/v1/apps/${encodeURIComponent(app_id)}/package-adopt`, body);
249
+ }
250
+ /**
251
+ * Fleet upgrade — bring every installation of a package across the CALLER'S
252
+ * org to the target version (latest when omitted) in one call. Hands-off
253
+ * applies only where the preview is clean; installations with breaking/
254
+ * drift/modified-core findings are skipped and reported for the normal
255
+ * per-installation consent flow. Backs `lotics package fleet-upgrade`.
256
+ * Admin-only; org-scoped (no workspace header needed).
257
+ */
258
+ async fleetUpgradeAppPackage(package_id, body) {
259
+ return this.request("POST", `/v1/app-packages/${encodeURIComponent(package_id)}/fleet-upgrade`, body);
260
+ }
261
+ // --- App packages (registry authoring + dev harness) ---
262
+ /**
263
+ * Create a registry app package — the Lotics-owned, workspace-agnostic
264
+ * blueprint. `lotics package publish` calls this on first publish (when the
265
+ * local manifest has no `package_id`), then publishes version 1 against the
266
+ * returned id. Admin-only.
267
+ */
268
+ async createAppPackage(body) {
269
+ return this.request("POST", "/v1/app-packages", body);
270
+ }
271
+ /**
272
+ * Fetch a registry app package's metadata (incl. `latest_version` and the
273
+ * Lotics-backed `is_official` trust badge). Admin-only; cross-tenant by id.
274
+ */
275
+ async getAppPackage(package_id) {
276
+ return this.request("GET", `/v1/app-packages/${encodeURIComponent(package_id)}`);
277
+ }
278
+ /**
279
+ * Publish a new immutable package version — multipart upload of the alias-keyed
280
+ * contract (JSON) + the prebuilt code bundle (a gzipped tarball carrying
281
+ * `source.tar.gz` + `dist.tar.gz` members). The server validates the contract +
282
+ * bundle, then allocates the next monotonic version. The `contract` is opaque
283
+ * JSON to the transport (the server is the validating authority). Admin-only.
284
+ */
285
+ async publishAppPackageVersion(package_id, args) {
286
+ const formData = new FormData();
287
+ formData.append("contract", JSON.stringify(args.contract));
288
+ formData.append("bundle", new Blob([new Uint8Array(args.bundle)], { type: "application/gzip" }), "bundle.tar.gz");
289
+ if (args.changelog)
290
+ formData.append("changelog", args.changelog);
291
+ const url = `${this.baseUrl}/v1/app-packages/${encodeURIComponent(package_id)}/versions`;
292
+ const response = await fetch(url, {
293
+ method: "POST",
294
+ headers: this.buildHeaders(), // no Content-Type — fetch sets the multipart boundary
295
+ body: formData,
296
+ });
297
+ if (!response.ok)
298
+ await this.throwResponseError(response);
299
+ return response.json();
300
+ }
301
+ /**
302
+ * Upgrade a package installation to a newer published version — extends the
303
+ * binding additively, re-materializes the target version's
304
+ * queries/workflows/agents (preserving workspace overlay), prunes dropped
305
+ * package artifacts, and bumps the pin. `version` omitted upgrades to the
306
+ * latest. Returns the resulting app. Admin-only.
307
+ */
308
+ async upgradeAppPackage(app_id, body) {
309
+ return this.request("POST", `/v1/apps/${encodeURIComponent(app_id)}/package-upgrade`, body);
310
+ }
311
+ /**
312
+ * Preview a package upgrade — the additive plan, informational removals,
313
+ * breaking contract changes, the binding drift report, and the modified-core
314
+ * report — with no writes. Admin-only.
315
+ */
316
+ async previewAppPackageUpgrade(app_id, opts = {}) {
317
+ const query = opts.version !== undefined ? `?version=${opts.version}` : "";
318
+ return this.request("GET", `/v1/apps/${encodeURIComponent(app_id)}/package-upgrade${query}`);
319
+ }
320
+ /**
321
+ * Re-bind a package role to a different workspace group (the current group
322
+ * still exists). Re-materializes at the pinned version; refuses over
323
+ * modified-core findings. Admin-only.
324
+ */
325
+ async rebindAppPackageRole(app_id, body) {
326
+ return this.request("POST", `/v1/apps/${encodeURIComponent(app_id)}/package-rebind-role`, body);
327
+ }
328
+ /**
329
+ * Workspace-wide dangling-reference sweep — active app/workflow artifacts
330
+ * whose prefixed schema ids no longer resolve. Backs
331
+ * `lotics workspace doctor`. Admin-only.
332
+ */
333
+ async getWorkspaceDanglingReferences() {
334
+ return this.request("GET", "/v1/workspaces/dangling-references");
335
+ }
336
+ /**
337
+ * Health check for a package installation — version pin vs. registry latest,
338
+ * binding drift, and locally modified core artifacts. Read-only; backs
339
+ * `lotics package doctor`. Admin-only.
340
+ */
341
+ async getAppPackageHealth(app_id) {
342
+ return this.request("GET", `/v1/apps/${encodeURIComponent(app_id)}/package-health`);
343
+ }
344
+ /**
345
+ * Reset a package installation in a DEV workspace — drop the package-owned
346
+ * scaffolded tables and re-scaffold clean. Hard-gated server-side to dev
347
+ * workspaces (a non-dev workspace is refused). Returns the resulting app.
348
+ * Admin-only.
349
+ */
350
+ async resetAppPackage(app_id) {
351
+ return this.request("POST", `/v1/apps/${encodeURIComponent(app_id)}/package-reset`);
352
+ }
193
353
  /**
194
354
  * Resolve the display name + fields (incl. select options) of the given tables
195
355
  * — the schema `lotics app codegen` turns into the runtime `.lotics/app_fields.ts`
@@ -273,6 +433,10 @@ export class LoticsClient {
273
433
  const qs = group_id ? `?group_id=${encodeURIComponent(group_id)}` : "";
274
434
  return this.request("GET", `/v1/apps/${encodeURIComponent(app_id)}/members${qs}`);
275
435
  }
436
+ /** A package installation's alias→id maps (fields/options/roles) — the app's runtime F/OPT resolution. */
437
+ async appBinding(app_id) {
438
+ return this.request("GET", `/v1/apps/${encodeURIComponent(app_id)}/binding`);
439
+ }
276
440
  /**
277
441
  * Resolve the full option set (key, label, color) of a named query's select
278
442
  * columns — the picker companion to `appQuery`. Mirrors
@@ -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" | "field_options" | "workflow" | "members" | "context" | "upload_url" | "upload_complete" | "comments.list" | "comments.create" | "comments.update" | "comments.delete" | "comments.counts";
9
+ export type RpcOp = "query" | "field_options" | "workflow" | "members" | "context" | "binding" | "upload_url" | "upload_complete" | "comments.list" | "comments.create" | "comments.update" | "comments.delete" | "comments.counts";
10
10
  export interface RpcRequest {
11
11
  app_id: string;
12
12
  op: RpcOp;
@@ -11,6 +11,7 @@ const SUPPORTED_OPS = new Set([
11
11
  "workflow",
12
12
  "members",
13
13
  "context",
14
+ "binding",
14
15
  "upload_url",
15
16
  "upload_complete",
16
17
  "comments.list",
@@ -31,11 +32,16 @@ export async function dispatchRpc(client, body, opts) {
31
32
  // In production the iframe host supplies the context; here we resolve the
32
33
  // effective viewer from the CLI key (the view-as target when the client
33
34
  // carries `x-view-as-member-id`, else the key's owner), so `useViewer`
34
- // and `is_current_member` agree in the dev loop.
35
- const who = await client.whoami();
35
+ // and `is_current_member` agree in the dev loop. The installation's
36
+ // stored config is fetched live for the same parity: `useConfig()` must
37
+ // render the same values in the dev loop as in production, including
38
+ // edits made in the product mid-session (fresh on every app load, no
39
+ // dev-server restart).
40
+ const [who, app] = await Promise.all([client.whoami(), client.getApp(body.app_id)]);
36
41
  return {
37
42
  member_id: who.member_id,
38
43
  comments_enabled: opts?.commentsEnabled ?? false,
44
+ config: app.config ?? {},
39
45
  };
40
46
  }
41
47
  case "query": {
@@ -71,6 +77,8 @@ export async function dispatchRpc(client, body, opts) {
71
77
  const p = body.payload;
72
78
  return client.appMembers(body.app_id, p?.group);
73
79
  }
80
+ case "binding":
81
+ return client.appBinding(body.app_id);
74
82
  case "upload_url": {
75
83
  const p = body.payload;
76
84
  if (!p ||
@@ -12,15 +12,33 @@ const WHOAMI = {
12
12
  organization_name: "Org",
13
13
  };
14
14
  describe("dispatchRpc — context op", () => {
15
- it("resolves the viewer member_id from whoami + manifest comments_enabled", async () => {
16
- const client = mockClient({ whoami: async () => WHOAMI });
15
+ it("resolves the viewer from whoami and the installation's stored config from the live app row", async () => {
16
+ const client = mockClient({
17
+ whoami: async () => WHOAMI,
18
+ getApp: async () => ({
19
+ id: "app_x",
20
+ name: "App X",
21
+ workspace_id: "wsp_1",
22
+ current_version_id: null,
23
+ config: { page_size: 50 },
24
+ }),
25
+ });
17
26
  const result = await dispatchRpc(client, { app_id: "app_x", op: "context", payload: {} }, { commentsEnabled: true });
18
- expect(result).toEqual({ member_id: "mbr_viewer", comments_enabled: true });
27
+ expect(result).toEqual({ member_id: "mbr_viewer", comments_enabled: true, config: { page_size: 50 } });
19
28
  });
20
- it("defaults comments_enabled to false when the manifest declares none", async () => {
21
- const client = mockClient({ whoami: async () => WHOAMI });
29
+ it("defaults comments_enabled to false and config to empty for a bespoke app", async () => {
30
+ const client = mockClient({
31
+ whoami: async () => WHOAMI,
32
+ getApp: async () => ({
33
+ id: "app_x",
34
+ name: "App X",
35
+ workspace_id: "wsp_1",
36
+ current_version_id: null,
37
+ config: null,
38
+ }),
39
+ });
22
40
  const result = await dispatchRpc(client, { app_id: "app_x", op: "context", payload: {} });
23
- expect(result).toEqual({ member_id: "mbr_viewer", comments_enabled: false });
41
+ expect(result).toEqual({ member_id: "mbr_viewer", comments_enabled: false, config: {} });
24
42
  });
25
43
  it("rejects an op that isn't supported", async () => {
26
44
  const client = mockClient({});
@@ -46,6 +46,8 @@ export interface TableSchema {
46
46
  * `upper` uppercases the result (TABLE aliases read as constants).
47
47
  */
48
48
  export declare function slugifyAlias(name: string, upper: boolean): string;
49
+ /** A property key for an object literal — bare when a valid identifier, else quoted. */
50
+ export declare function propKey(alias: string): string;
49
51
  /**
50
52
  * Generate the full `.lotics/app_fields.ts` source. `tables` is the resolved
51
53
  * workspace schema (the subset the app touches). An empty list yields valid,
@@ -68,7 +68,7 @@ function dedupeAliases(names, upper) {
68
68
  });
69
69
  }
70
70
  /** A property key for an object literal — bare when a valid identifier, else quoted. */
71
- function propKey(alias) {
71
+ export function propKey(alias) {
72
72
  return isValidIdentifier(alias) ? alias : JSON.stringify(alias);
73
73
  }
74
74
  /** Resolve table + field aliases once, so the `F`/`OPT` maps and the union types agree. */