@lotics/cli 0.76.0 → 0.83.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 +66 -32
- package/dist/app_commands.d.ts +9 -4
- package/dist/app_commands.js +44 -10
- package/dist/app_commands.test.js +69 -0
- package/dist/args.d.ts +19 -5
- package/dist/args.js +36 -14
- package/dist/args.test.js +25 -17
- package/dist/cli.js +214 -215
- package/dist/cli_dispatch.test.js +77 -25
- package/dist/client.d.ts +240 -170
- package/dist/client.js +122 -111
- package/dist/generate_package_fields.d.ts +39 -38
- package/dist/generate_package_fields.js +113 -60
- package/dist/generate_package_fields.test.js +30 -22
- package/dist/package_commands.d.ts +148 -204
- package/dist/package_commands.js +586 -828
- package/dist/package_commands.test.js +99 -226
- package/dist/src/cli.js +18680 -2572
- package/dist/starter_template.d.ts +0 -19
- package/dist/starter_template.js +0 -389
- package/dist/starter_template.test.js +1 -69
- package/package.json +1 -1
package/dist/client.js
CHANGED
|
@@ -143,15 +143,6 @@ export class LoticsClient {
|
|
|
143
143
|
async listWorkspaces() {
|
|
144
144
|
return this.request("GET", "/v1/workspaces");
|
|
145
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
|
-
}
|
|
155
146
|
async createWorkspace(body) {
|
|
156
147
|
return this.request("POST", "/v1/workspaces", body);
|
|
157
148
|
}
|
|
@@ -204,16 +195,56 @@ export class LoticsClient {
|
|
|
204
195
|
return this.request("POST", "/v1/apps", body);
|
|
205
196
|
}
|
|
206
197
|
/**
|
|
207
|
-
* Install
|
|
208
|
-
*
|
|
209
|
-
*
|
|
210
|
-
*
|
|
198
|
+
* Install a package version into the current workspace — ONE endpoint,
|
|
199
|
+
* kind-branched into a discriminated response (`kind`). An `app` package
|
|
200
|
+
* scaffolds the data model / deploys / materializes and returns the installation
|
|
201
|
+
* app (+ advisory knowledge warnings); a `content` package installs only its
|
|
202
|
+
* doc corpus and returns the installation row. `bind_to` (content only)
|
|
203
|
+
* resolves a name collision by adopting an existing same-named doc as
|
|
204
|
+
* package-managed. `version` omitted installs the latest published version.
|
|
205
|
+
*/
|
|
206
|
+
async installPackage(package_id, body) {
|
|
207
|
+
return this.request("POST", `/v1/packages/${encodeURIComponent(package_id)}/installations`, body);
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Uninstall a standalone content package — delete the installation row. By
|
|
211
|
+
* default the package-bound docs are ARCHIVED; `keep_content` retains them as
|
|
212
|
+
* ordinary workspace docs. Admin-only. Backs `lotics uninstall`.
|
|
213
|
+
*/
|
|
214
|
+
async uninstallContentPackage(installation_id, opts = {}) {
|
|
215
|
+
const qs = opts.keep_content ? "?keep_content=true" : "";
|
|
216
|
+
return this.request("DELETE", `/v1/content-installations/${encodeURIComponent(installation_id)}${qs}`);
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Preview upgrading a standalone content installation — the knowledge namespace's
|
|
220
|
+
* per-alias `entries` (added/changed/removed/drifted + a `modified` flag) plus the
|
|
221
|
+
* consent-requiring `templates` (a template the target changed whose live content
|
|
222
|
+
* was locally edited; a clean one auto-updates and is absent). No writes.
|
|
223
|
+
* Admin-only.
|
|
224
|
+
*/
|
|
225
|
+
async previewContentInstallationUpgrade(installation_id, opts = {}) {
|
|
226
|
+
const qs = opts.version !== undefined ? `?version=${opts.version}` : "";
|
|
227
|
+
return this.request("GET", `/v1/content-installations/${encodeURIComponent(installation_id)}/upgrade${qs}`);
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Apply a standalone content upgrade — propagate the target version's content per
|
|
231
|
+
* the resolutions (`knowledge`: a modified change/removal or drift needs consent;
|
|
232
|
+
* `templates`: a locally-edited changed template takes revert|keep), then advance
|
|
233
|
+
* the pin + binding. Returns the updated installation row. Admin-only.
|
|
211
234
|
*/
|
|
212
|
-
async
|
|
213
|
-
return this.request("POST", `/v1/
|
|
235
|
+
async applyContentInstallationUpgrade(installation_id, body) {
|
|
236
|
+
return this.request("POST", `/v1/content-installations/${encodeURIComponent(installation_id)}/upgrade`, body);
|
|
214
237
|
}
|
|
215
238
|
/**
|
|
216
|
-
*
|
|
239
|
+
* List a workspace's package-managed knowledge installations, each folded with
|
|
240
|
+
* registry status (name, kind, official badge, latest version, update-available).
|
|
241
|
+
* Member-accessible (workspace-scoped); backs the settings "Managed by" badge.
|
|
242
|
+
*/
|
|
243
|
+
async listContentInstallations(workspace_id) {
|
|
244
|
+
return this.request("GET", `/v1/workspaces/${encodeURIComponent(workspace_id)}/content-installations`);
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Uninstall a package installation (backs `lotics uninstall`). Does
|
|
217
248
|
* everything DELETE does plus archives the installation's lifecycle
|
|
218
249
|
* artifacts; with `archive_tables` it also archives the scaffolded entity
|
|
219
250
|
* tables — refused server-side unless this installation created them
|
|
@@ -231,13 +262,14 @@ export class LoticsClient {
|
|
|
231
262
|
return this.request("PATCH", `/v1/apps/${encodeURIComponent(app_id)}/package-config`, body);
|
|
232
263
|
}
|
|
233
264
|
/**
|
|
234
|
-
* Retire (or `undo` un-retire) a registry package (backs `lotics
|
|
235
|
-
*
|
|
236
|
-
*
|
|
237
|
-
* Owner-org
|
|
265
|
+
* Retire (or `undo` un-retire) a registry package (backs `lotics app
|
|
266
|
+
* unpublish` — the endpoint/audit action keep the `retire` name to avoid API
|
|
267
|
+
* churn). Retiring refuses NEW installs and hides the package from non-owning
|
|
268
|
+
* orgs; existing installations keep working and may still upgrade. Owner-org
|
|
269
|
+
* admin-only.
|
|
238
270
|
*/
|
|
239
|
-
async
|
|
240
|
-
return this.request("POST", `/v1/
|
|
271
|
+
async retirePackage(package_id, body) {
|
|
272
|
+
return this.request("POST", `/v1/packages/${encodeURIComponent(package_id)}/retire`, body);
|
|
241
273
|
}
|
|
242
274
|
/**
|
|
243
275
|
* Eject an installation from its package — re-deploy the pinned version's
|
|
@@ -246,40 +278,15 @@ export class LoticsClient {
|
|
|
246
278
|
* app becomes a normal bespoke app and can no longer be upgraded. Returns the
|
|
247
279
|
* resulting app.
|
|
248
280
|
*/
|
|
249
|
-
async
|
|
281
|
+
async ejectPackage(app_id) {
|
|
250
282
|
return this.request("POST", `/v1/apps/${encodeURIComponent(app_id)}/eject`);
|
|
251
283
|
}
|
|
252
|
-
/**
|
|
253
|
-
* Extract a DRAFT app package from an existing bespoke app — the promotion
|
|
254
|
-
* read (docs/app_packages.md § Promotion). Pure: nothing is written. Returns
|
|
255
|
-
* the alias-keyed draft `contract` (opaque to the CLI — the server is the
|
|
256
|
-
* validating authority), the origin workspace's `binding` (which doubles as
|
|
257
|
-
* the adopt binding), a findings `report` (any `error` ⇒ not publishable
|
|
258
|
-
* as-is), and the file-backed `template_files` the CLI must stage into the
|
|
259
|
-
* project at their `bytes_ref` paths. Backs `lotics package extract`.
|
|
260
|
-
* Admin-only.
|
|
261
|
-
*/
|
|
262
|
-
async extractAppPackage(app_id) {
|
|
263
|
-
return this.request("GET", `/v1/apps/${encodeURIComponent(app_id)}/package-extract`);
|
|
264
|
-
}
|
|
265
|
-
/**
|
|
266
|
-
* Adopt a published package onto an EXISTING (bespoke or ejected) app — the
|
|
267
|
-
* final promotion step. The workspace already holds the concrete objects, so
|
|
268
|
-
* nothing is scaffolded or rewritten: the server verifies the `binding` is
|
|
269
|
-
* complete, live, and FAITHFUL to the version's contract, then writes only the
|
|
270
|
-
* installation pin (the app becomes installation #1, upgradeable again). A
|
|
271
|
-
* ConflictError names the aliases that diverge. `version` omitted adopts the
|
|
272
|
-
* latest. Backs `lotics package adopt`. Admin-only.
|
|
273
|
-
*/
|
|
274
|
-
async adoptAppPackage(app_id, body) {
|
|
275
|
-
return this.request("POST", `/v1/apps/${encodeURIComponent(app_id)}/package-adopt`, body);
|
|
276
|
-
}
|
|
277
284
|
/**
|
|
278
285
|
* Fleet upgrade — bring every installation of a package across the CALLER'S
|
|
279
286
|
* org to the target version (latest when omitted) in one call. Hands-off
|
|
280
287
|
* applies only where the preview is clean; installations with breaking/
|
|
281
288
|
* drift/modified-core findings are skipped and reported for the normal
|
|
282
|
-
* per-installation consent flow. Backs `lotics
|
|
289
|
+
* per-installation consent flow. Backs `lotics upgrade <package_id>` (fleet).
|
|
283
290
|
* Admin-only; org-scoped (no workspace header needed).
|
|
284
291
|
*/
|
|
285
292
|
/**
|
|
@@ -287,57 +294,26 @@ export class LoticsClient {
|
|
|
287
294
|
* adopts targeting it; pinned installations keep running. Owner-org
|
|
288
295
|
* admin-only. Backs `lotics package yank`.
|
|
289
296
|
*/
|
|
290
|
-
async
|
|
291
|
-
return this.request("POST", `/v1/
|
|
297
|
+
async yankPackageVersion(package_id, version, yanked) {
|
|
298
|
+
return this.request("POST", `/v1/packages/${encodeURIComponent(package_id)}/versions/${version}/yank`, { yanked });
|
|
292
299
|
}
|
|
293
|
-
async
|
|
294
|
-
return this.request("POST", `/v1/
|
|
295
|
-
}
|
|
296
|
-
// --- App packages (registry authoring + dev harness) ---
|
|
297
|
-
/**
|
|
298
|
-
* Create a registry app package — the Lotics-owned, workspace-agnostic
|
|
299
|
-
* blueprint. `lotics package publish` calls this on first publish (when the
|
|
300
|
-
* local manifest has no `package_id`), then publishes version 1 against the
|
|
301
|
-
* returned id. Admin-only.
|
|
302
|
-
*/
|
|
303
|
-
async createAppPackage(body) {
|
|
304
|
-
return this.request("POST", "/v1/app-packages", body);
|
|
300
|
+
async fleetUpgradePackage(package_id, body) {
|
|
301
|
+
return this.request("POST", `/v1/packages/${encodeURIComponent(package_id)}/fleet-upgrade`, body);
|
|
305
302
|
}
|
|
303
|
+
// --- Packages (registry reads + installations) ---
|
|
304
|
+
// Authoring is server-side: apps via `POST /v1/apps/{id}/package-publish|release`,
|
|
305
|
+
// content packages via the `publish_content`/`release_content` tools. There is no
|
|
306
|
+
// client-side create-package / upload-bundle path.
|
|
306
307
|
/**
|
|
307
|
-
* Fetch a registry app package's metadata (incl. `latest_version` and
|
|
308
|
-
* Lotics-backed `is_official` trust badge). Admin-only; cross-tenant by id.
|
|
308
|
+
* Fetch a registry app package's metadata (incl. `kind`, `latest_version` and
|
|
309
|
+
* the Lotics-backed `is_official` trust badge). Admin-only; cross-tenant by id.
|
|
309
310
|
*/
|
|
310
|
-
async
|
|
311
|
-
return this.request("GET", `/v1/
|
|
311
|
+
async getPackage(package_id) {
|
|
312
|
+
return this.request("GET", `/v1/packages/${encodeURIComponent(package_id)}`);
|
|
312
313
|
}
|
|
313
314
|
/** Version history newest-first (no contract payloads) — backs `lotics package show`. Admin-only. */
|
|
314
|
-
async
|
|
315
|
-
return this.request("GET", `/v1/
|
|
316
|
-
}
|
|
317
|
-
/**
|
|
318
|
-
* Publish a new immutable package version — multipart upload of the alias-keyed
|
|
319
|
-
* contract (JSON) + the prebuilt code bundle (a gzipped tarball carrying
|
|
320
|
-
* `source.tar.gz` + `dist.tar.gz` members). The server validates the contract +
|
|
321
|
-
* bundle, then allocates the next monotonic version. The `contract` is opaque
|
|
322
|
-
* JSON to the transport (the server is the validating authority). Admin-only.
|
|
323
|
-
*/
|
|
324
|
-
async publishAppPackageVersion(package_id, args) {
|
|
325
|
-
const formData = new FormData();
|
|
326
|
-
formData.append("contract", JSON.stringify(args.contract));
|
|
327
|
-
formData.append("bundle", new Blob([new Uint8Array(args.bundle)], { type: "application/gzip" }), "bundle.tar.gz");
|
|
328
|
-
if (args.changelog)
|
|
329
|
-
formData.append("changelog", args.changelog);
|
|
330
|
-
if (args.channel)
|
|
331
|
-
formData.append("channel", args.channel);
|
|
332
|
-
const url = `${this.baseUrl}/v1/app-packages/${encodeURIComponent(package_id)}/versions`;
|
|
333
|
-
const response = await fetch(url, {
|
|
334
|
-
method: "POST",
|
|
335
|
-
headers: this.buildHeaders(), // no Content-Type — fetch sets the multipart boundary
|
|
336
|
-
body: formData,
|
|
337
|
-
});
|
|
338
|
-
if (!response.ok)
|
|
339
|
-
await this.throwResponseError(response);
|
|
340
|
-
return response.json();
|
|
315
|
+
async listPackageVersions(package_id) {
|
|
316
|
+
return this.request("GET", `/v1/packages/${encodeURIComponent(package_id)}/versions`);
|
|
341
317
|
}
|
|
342
318
|
/**
|
|
343
319
|
* Upgrade a package installation to a newer published version — extends the
|
|
@@ -346,7 +322,7 @@ export class LoticsClient {
|
|
|
346
322
|
* package artifacts, and bumps the pin. `version` omitted upgrades to the
|
|
347
323
|
* latest. Returns the resulting app. Admin-only.
|
|
348
324
|
*/
|
|
349
|
-
async
|
|
325
|
+
async upgradePackage(app_id, body) {
|
|
350
326
|
return this.request("POST", `/v1/apps/${encodeURIComponent(app_id)}/package-upgrade`, body);
|
|
351
327
|
}
|
|
352
328
|
/**
|
|
@@ -354,18 +330,10 @@ export class LoticsClient {
|
|
|
354
330
|
* breaking contract changes, the binding drift report, and the modified-core
|
|
355
331
|
* report — with no writes. Admin-only.
|
|
356
332
|
*/
|
|
357
|
-
async
|
|
333
|
+
async previewPackageUpgrade(app_id, opts = {}) {
|
|
358
334
|
const query = opts.version !== undefined ? `?version=${opts.version}` : "";
|
|
359
335
|
return this.request("GET", `/v1/apps/${encodeURIComponent(app_id)}/package-upgrade${query}`);
|
|
360
336
|
}
|
|
361
|
-
/**
|
|
362
|
-
* Re-bind a package role to a different workspace group (the current group
|
|
363
|
-
* still exists). Re-materializes at the pinned version; refuses over
|
|
364
|
-
* modified-core findings. Admin-only.
|
|
365
|
-
*/
|
|
366
|
-
async rebindAppPackageRole(app_id, body) {
|
|
367
|
-
return this.request("POST", `/v1/apps/${encodeURIComponent(app_id)}/package-rebind-role`, body);
|
|
368
|
-
}
|
|
369
337
|
/**
|
|
370
338
|
* Workspace-wide dangling-reference sweep — active app/workflow artifacts
|
|
371
339
|
* whose prefixed schema ids no longer resolve. Backs
|
|
@@ -379,17 +347,60 @@ export class LoticsClient {
|
|
|
379
347
|
* binding drift, and locally modified core artifacts. Read-only; backs
|
|
380
348
|
* `lotics package doctor`. Admin-only.
|
|
381
349
|
*/
|
|
382
|
-
async
|
|
350
|
+
async getPackageHealth(app_id) {
|
|
383
351
|
return this.request("GET", `/v1/apps/${encodeURIComponent(app_id)}/package-health`);
|
|
384
352
|
}
|
|
385
353
|
/**
|
|
386
|
-
*
|
|
387
|
-
*
|
|
388
|
-
*
|
|
389
|
-
* Admin
|
|
354
|
+
* Preview a release — the dry run behind `lotics app release`. Runs the
|
|
355
|
+
* binding-aware extract of the origin (aliases stable through the app's current
|
|
356
|
+
* binding) and reports the next version number, the new + changed aliases, and
|
|
357
|
+
* any extract findings (an `error` blocks the apply). No writes. Admin,
|
|
358
|
+
* owning-org only.
|
|
359
|
+
*/
|
|
360
|
+
async previewPackageRelease(app_id) {
|
|
361
|
+
return this.request("GET", `/v1/apps/${encodeURIComponent(app_id)}/package-release`);
|
|
362
|
+
}
|
|
363
|
+
/**
|
|
364
|
+
* Release — snapshot the origin app into the next registry version. The server
|
|
365
|
+
* binding-aware-extracts it, repackages its deployed source + dist as the
|
|
366
|
+
* bundle, publishes the next `release`-channel version with the changelog, and
|
|
367
|
+
* re-pins the origin. Error findings from extract surface as a 409. Admin,
|
|
368
|
+
* owning-org only. Backs `lotics app release --yes`.
|
|
369
|
+
*/
|
|
370
|
+
async releasePackage(app_id, body) {
|
|
371
|
+
return this.request("POST", `/v1/apps/${encodeURIComponent(app_id)}/package-release`, body);
|
|
372
|
+
}
|
|
373
|
+
/**
|
|
374
|
+
* Dry-run preview of a first-release — the `GET` behind `lotics app publish`
|
|
375
|
+
* (no `--yes`), the publish-side analogue of `previewPackageRelease`. The
|
|
376
|
+
* server runs the same fresh-alias extract + `src/` scan the apply runs
|
|
377
|
+
* (through any `renames`) and returns the package name it would mint, the
|
|
378
|
+
* auto-minted RENAMABLE aliases (the exact `--rename` keys), and the extract
|
|
379
|
+
* findings (an `error` blocks the apply). No writes. Admin-only.
|
|
380
|
+
*/
|
|
381
|
+
async previewPublishAppPackage(app_id, opts = {}) {
|
|
382
|
+
const params = new URLSearchParams();
|
|
383
|
+
if (opts.knowledge !== undefined && opts.knowledge.length > 0) {
|
|
384
|
+
params.set("knowledge", JSON.stringify(opts.knowledge));
|
|
385
|
+
}
|
|
386
|
+
if (opts.renames !== undefined && opts.renames.length > 0) {
|
|
387
|
+
params.set("renames", JSON.stringify(opts.renames));
|
|
388
|
+
}
|
|
389
|
+
const query = params.toString();
|
|
390
|
+
return this.request("GET", `/v1/apps/${encodeURIComponent(app_id)}/package-publish${query ? `?${query}` : ""}`);
|
|
391
|
+
}
|
|
392
|
+
/**
|
|
393
|
+
* First-release apply — mint a package from a BESPOKE app and publish v1 in one
|
|
394
|
+
* call (the `POST` behind `lotics app publish --yes`). The server extracts an
|
|
395
|
+
* alias-keyed contract from the app (fresh aliases; `renames` fixes them before
|
|
396
|
+
* v1 freezes), creates the registry package (name/description from the app),
|
|
397
|
+
* publishes v1 from the app's deployed source + dist, and pins the origin as
|
|
398
|
+
* installation #1. Error findings from extract surface as a 409. An
|
|
399
|
+
* already-linked app must use `releasePackage` instead. Admin-only. Backs
|
|
400
|
+
* `lotics app publish <app_id>`.
|
|
390
401
|
*/
|
|
391
|
-
async
|
|
392
|
-
return this.request("POST", `/v1/apps/${encodeURIComponent(app_id)}/package-
|
|
402
|
+
async publishAppAsPackage(app_id, body) {
|
|
403
|
+
return this.request("POST", `/v1/apps/${encodeURIComponent(app_id)}/package-publish`, body);
|
|
393
404
|
}
|
|
394
405
|
/**
|
|
395
406
|
* Resolve the display name + fields (incl. select options) of the given tables
|
|
@@ -1,48 +1,49 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Codegen: emit a RUNTIME `.lotics/app_fields.ts` for a
|
|
3
|
-
* its
|
|
2
|
+
* Codegen: emit a RUNTIME `.lotics/app_fields.ts` for a PUBLISHED / linked app
|
|
3
|
+
* (a package installation — its own published origin, or an install of another
|
|
4
|
+
* org's package) from that installation's LIVE binding.
|
|
4
5
|
*
|
|
5
|
-
* The bespoke twin (`generate_app_fields.ts`) bakes
|
|
6
|
-
* `fld_…`/`opt_…` ids at
|
|
7
|
-
* co-live, fundamentally wrong for a
|
|
8
|
-
*
|
|
9
|
-
* `OPT` surface at MODULE LOAD from the
|
|
10
|
-
* `getAppBinding()` (the `binding` RPC op),
|
|
11
|
-
* graph waits for the binding before any
|
|
12
|
-
* `F.TASKS.title` is a plain string everywhere, including
|
|
13
|
-
* constants.
|
|
14
|
-
* import path, same shapes
|
|
15
|
-
* the same display names
|
|
6
|
+
* The bespoke twin (`generate_app_fields.ts`) bakes THIS workspace's concrete
|
|
7
|
+
* `fld_…`/`opt_…` ids at codegen time — correct when source and workspace
|
|
8
|
+
* co-live, fundamentally wrong for a linked app (the source is portable and
|
|
9
|
+
* installs into many workspaces, each with different ids). A linked app instead
|
|
10
|
+
* resolves the SAME `F` / `OPT` / `ROLE` surface at MODULE LOAD from the
|
|
11
|
+
* installation's binding via the SDK's `getAppBinding()` (the `binding` RPC op),
|
|
12
|
+
* using top-level await — the ESM graph waits for the binding before any
|
|
13
|
+
* importer evaluates, so `F.TASKS.title` is a plain string everywhere, including
|
|
14
|
+
* module-top-level constants. A published origin's source is portable by
|
|
15
|
+
* construction: same import path, same `F`/`OPT` shapes the bespoke codegen
|
|
16
|
+
* emitted (contract aliases derive from the same display names it slugifies), so
|
|
17
|
+
* the deployed source compiles unchanged, resolved per-install.
|
|
16
18
|
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
19
|
+
* The generator sources the alias SET from the app's LIVE binding (the `binding`
|
|
20
|
+
* RPC / `appBinding` client method), NOT a contract file: every bound field
|
|
21
|
+
* (`entity.field`), option (`entity.field:opt`), and role becomes an `F` / `OPT`
|
|
22
|
+
* / `ROLE` entry keyed by its stable alias.
|
|
23
|
+
*
|
|
24
|
+
* A missing alias fails LOUD at boot (the binding is install-verified complete,
|
|
25
|
+
* so a miss means the generated file is stale relative to the installed contract
|
|
26
|
+
* version) — never a silent undefined key in a write.
|
|
20
27
|
*
|
|
21
28
|
* Requires the app build to target es2022+ (the starter's `build.target`) —
|
|
22
29
|
* top-level await does not exist below it.
|
|
23
30
|
*/
|
|
24
|
-
/**
|
|
25
|
-
*
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
}>;
|
|
36
|
-
roles?: Array<{
|
|
37
|
-
alias: string;
|
|
38
|
-
}>;
|
|
31
|
+
/**
|
|
32
|
+
* A materialized app installation's binding — the exact shape the `binding` RPC
|
|
33
|
+
* / `client.appBinding()` returns (a subset of the full package binding: only
|
|
34
|
+
* the maps the client-side `F`/`OPT`/`ROLE` surface resolves through). Keys are
|
|
35
|
+
* fully-qualified contract aliases: `fields` keyed `entity.field`, `options`
|
|
36
|
+
* keyed `entity.field:opt`, `roles` keyed by role alias.
|
|
37
|
+
*/
|
|
38
|
+
export interface AppFieldsBinding {
|
|
39
|
+
fields: Record<string, string>;
|
|
40
|
+
options: Record<string, string>;
|
|
41
|
+
roles: Record<string, string>;
|
|
39
42
|
}
|
|
40
43
|
/**
|
|
41
|
-
* Generate the full
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
* identifier instead of emitting broken TypeScript into a "DO NOT EDIT" file.
|
|
46
|
-
* Pure; same contract → same bytes.
|
|
44
|
+
* Generate the full binding-resolved `.lotics/app_fields.ts` source from an
|
|
45
|
+
* installation's live binding. Pure — same binding → same bytes. Idempotent.
|
|
46
|
+
* An empty binding yields valid, empty `F`/`OPT`/`ROLE` maps so the file always
|
|
47
|
+
* compiles and imports resolve.
|
|
47
48
|
*/
|
|
48
|
-
export declare function generatePackageAppFields(
|
|
49
|
+
export declare function generatePackageAppFields(binding: AppFieldsBinding): string;
|
|
@@ -1,34 +1,41 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Codegen: emit a RUNTIME `.lotics/app_fields.ts` for a
|
|
3
|
-
* its
|
|
2
|
+
* Codegen: emit a RUNTIME `.lotics/app_fields.ts` for a PUBLISHED / linked app
|
|
3
|
+
* (a package installation — its own published origin, or an install of another
|
|
4
|
+
* org's package) from that installation's LIVE binding.
|
|
4
5
|
*
|
|
5
|
-
* The bespoke twin (`generate_app_fields.ts`) bakes
|
|
6
|
-
* `fld_…`/`opt_…` ids at
|
|
7
|
-
* co-live, fundamentally wrong for a
|
|
8
|
-
*
|
|
9
|
-
* `OPT` surface at MODULE LOAD from the
|
|
10
|
-
* `getAppBinding()` (the `binding` RPC op),
|
|
11
|
-
* graph waits for the binding before any
|
|
12
|
-
* `F.TASKS.title` is a plain string everywhere, including
|
|
13
|
-
* constants.
|
|
14
|
-
* import path, same shapes
|
|
15
|
-
* the same display names
|
|
6
|
+
* The bespoke twin (`generate_app_fields.ts`) bakes THIS workspace's concrete
|
|
7
|
+
* `fld_…`/`opt_…` ids at codegen time — correct when source and workspace
|
|
8
|
+
* co-live, fundamentally wrong for a linked app (the source is portable and
|
|
9
|
+
* installs into many workspaces, each with different ids). A linked app instead
|
|
10
|
+
* resolves the SAME `F` / `OPT` / `ROLE` surface at MODULE LOAD from the
|
|
11
|
+
* installation's binding via the SDK's `getAppBinding()` (the `binding` RPC op),
|
|
12
|
+
* using top-level await — the ESM graph waits for the binding before any
|
|
13
|
+
* importer evaluates, so `F.TASKS.title` is a plain string everywhere, including
|
|
14
|
+
* module-top-level constants. A published origin's source is portable by
|
|
15
|
+
* construction: same import path, same `F`/`OPT` shapes the bespoke codegen
|
|
16
|
+
* emitted (contract aliases derive from the same display names it slugifies), so
|
|
17
|
+
* the deployed source compiles unchanged, resolved per-install.
|
|
16
18
|
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
19
|
+
* The generator sources the alias SET from the app's LIVE binding (the `binding`
|
|
20
|
+
* RPC / `appBinding` client method), NOT a contract file: every bound field
|
|
21
|
+
* (`entity.field`), option (`entity.field:opt`), and role becomes an `F` / `OPT`
|
|
22
|
+
* / `ROLE` entry keyed by its stable alias.
|
|
23
|
+
*
|
|
24
|
+
* A missing alias fails LOUD at boot (the binding is install-verified complete,
|
|
25
|
+
* so a miss means the generated file is stale relative to the installed contract
|
|
26
|
+
* version) — never a silent undefined key in a write.
|
|
20
27
|
*
|
|
21
28
|
* Requires the app build to target es2022+ (the starter's `build.target`) —
|
|
22
29
|
* top-level await does not exist below it.
|
|
23
30
|
*/
|
|
24
31
|
import { propKey } from "./generate_app_fields.js";
|
|
25
|
-
const HEADER = `// Auto-generated by 'lotics
|
|
26
|
-
// DO NOT EDIT — regenerated
|
|
32
|
+
const HEADER = `// Auto-generated by 'lotics app codegen' (linked/published app; also app pull/dev/deploy).
|
|
33
|
+
// DO NOT EDIT — regenerated from the installation's live binding.
|
|
27
34
|
//
|
|
28
|
-
//
|
|
29
|
-
//
|
|
30
|
-
//
|
|
31
|
-
//
|
|
35
|
+
// A package installation resolves F/OPT/ROLE at MODULE LOAD from its binding
|
|
36
|
+
// (contract alias → THIS workspace's concrete id) via the SDK's \`binding\` RPC.
|
|
37
|
+
// Top-level await: the module graph waits for the binding before any importer
|
|
38
|
+
// evaluates, so every entry is a plain string.
|
|
32
39
|
import { getAppBinding } from "@lotics/app-sdk";
|
|
33
40
|
|
|
34
41
|
const binding = await getAppBinding();
|
|
@@ -45,54 +52,100 @@ function bound(map: Record<string, string>, key: string, kind: string): string {
|
|
|
45
52
|
}
|
|
46
53
|
`;
|
|
47
54
|
/**
|
|
48
|
-
*
|
|
49
|
-
* aliases are slug-grammar (
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
55
|
+
* Reconstruct the entity → field → option nesting (plus role list) from the flat
|
|
56
|
+
* binding maps. Entity/field/option aliases are slug-grammar (`[a-z][a-z0-9_]*`,
|
|
57
|
+
* no `.` or `:`), so a field key splits on its single `.` and an option key on
|
|
58
|
+
* its single `:`. First-seen order is preserved (the binding is written in
|
|
59
|
+
* contract order), so the output is deterministic for a given binding. An option
|
|
60
|
+
* whose owning field is absent from `fields` is skipped defensively — the binding
|
|
61
|
+
* is the authority, and a select field is always present among the fields.
|
|
54
62
|
*/
|
|
55
|
-
|
|
56
|
-
const entities =
|
|
57
|
-
const
|
|
58
|
-
const
|
|
59
|
-
for (const
|
|
60
|
-
const
|
|
61
|
-
if (
|
|
63
|
+
function parseBinding(binding) {
|
|
64
|
+
const entities = [];
|
|
65
|
+
const entityByAlias = new Map();
|
|
66
|
+
const fieldByKey = new Map();
|
|
67
|
+
for (const key of Object.keys(binding.fields)) {
|
|
68
|
+
const dot = key.indexOf(".");
|
|
69
|
+
if (dot <= 0 || dot >= key.length - 1)
|
|
70
|
+
continue;
|
|
71
|
+
const entityAlias = key.slice(0, dot);
|
|
72
|
+
const fieldAlias = key.slice(dot + 1);
|
|
73
|
+
let entity = entityByAlias.get(entityAlias);
|
|
74
|
+
if (entity === undefined) {
|
|
75
|
+
entity = { alias: entityAlias, fields: [] };
|
|
76
|
+
entityByAlias.set(entityAlias, entity);
|
|
77
|
+
entities.push(entity);
|
|
78
|
+
}
|
|
79
|
+
const field = { alias: fieldAlias, key, options: [] };
|
|
80
|
+
entity.fields.push(field);
|
|
81
|
+
fieldByKey.set(key, field);
|
|
82
|
+
}
|
|
83
|
+
for (const key of Object.keys(binding.options)) {
|
|
84
|
+
const colon = key.indexOf(":");
|
|
85
|
+
if (colon <= 0 || colon >= key.length - 1)
|
|
86
|
+
continue;
|
|
87
|
+
const fieldKey = key.slice(0, colon);
|
|
88
|
+
const optionAlias = key.slice(colon + 1);
|
|
89
|
+
const field = fieldByKey.get(fieldKey);
|
|
90
|
+
if (field === undefined)
|
|
62
91
|
continue;
|
|
63
|
-
|
|
92
|
+
field.options.push({ alias: optionAlias, key });
|
|
64
93
|
}
|
|
65
|
-
|
|
94
|
+
return { entities, roles: Object.keys(binding.roles) };
|
|
95
|
+
}
|
|
96
|
+
/** Emit the `F` map: `{ ENTITY: { field: bound(binding.fields, "entity.field", "field") } }`. */
|
|
97
|
+
function emitFieldMap(entities) {
|
|
98
|
+
if (entities.length === 0)
|
|
99
|
+
return `export const F = {} as const;`;
|
|
100
|
+
const blocks = entities.map((entity) => {
|
|
101
|
+
const lines = entity.fields.map((field) => ` ${propKey(field.alias)}: bound(binding.fields, ${JSON.stringify(field.key)}, "field"),`);
|
|
102
|
+
return ` ${propKey(entity.alias.toUpperCase())}: {\n${lines.join("\n")}\n },`;
|
|
103
|
+
});
|
|
104
|
+
return `export const F = {\n${blocks.join("\n")}\n} as const;`;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Emit the `OPT` map: `{ ENTITY: { field: { opt: bound(binding.options, …) } } }`.
|
|
108
|
+
* Only fields carrying options appear (an option-less field/entity is omitted).
|
|
109
|
+
*/
|
|
110
|
+
function emitOptionMap(entities) {
|
|
111
|
+
const entityBlocks = [];
|
|
66
112
|
for (const entity of entities) {
|
|
67
|
-
const
|
|
68
|
-
for (const field of entity.fields
|
|
69
|
-
|
|
70
|
-
if (options.length === 0)
|
|
113
|
+
const fieldBlocks = [];
|
|
114
|
+
for (const field of entity.fields) {
|
|
115
|
+
if (field.options.length === 0)
|
|
71
116
|
continue;
|
|
72
|
-
const lines = options.map((option) => ` ${propKey(option.alias)}: bound(binding.options, ${JSON.stringify(
|
|
73
|
-
|
|
74
|
-
}
|
|
75
|
-
if (perField.length > 0) {
|
|
76
|
-
optionBlocks.push(` ${propKey(entity.alias.toUpperCase())}: {\n${perField.join("\n")}\n },`);
|
|
117
|
+
const lines = field.options.map((option) => ` ${propKey(option.alias)}: bound(binding.options, ${JSON.stringify(option.key)}, "option"),`);
|
|
118
|
+
fieldBlocks.push(` ${propKey(field.alias)}: {\n${lines.join("\n")}\n },`);
|
|
77
119
|
}
|
|
120
|
+
if (fieldBlocks.length === 0)
|
|
121
|
+
continue;
|
|
122
|
+
entityBlocks.push(` ${propKey(entity.alias.toUpperCase())}: {\n${fieldBlocks.join("\n")}\n },`);
|
|
78
123
|
}
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
124
|
+
if (entityBlocks.length === 0)
|
|
125
|
+
return `export const OPT = {} as const;`;
|
|
126
|
+
return `export const OPT = {\n${entityBlocks.join("\n")}\n} as const;`;
|
|
127
|
+
}
|
|
128
|
+
/** Emit the `ROLE` map: `{ role: bound(binding.roles, "role", "role") }`. */
|
|
129
|
+
function emitRoleMap(roles) {
|
|
130
|
+
if (roles.length === 0)
|
|
131
|
+
return `export const ROLE = {} as const;`;
|
|
132
|
+
const lines = roles.map((role) => ` ${propKey(role)}: bound(binding.roles, ${JSON.stringify(role)}, "role"),`);
|
|
133
|
+
return `export const ROLE = {\n${lines.join("\n")}\n} as const;`;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Generate the full binding-resolved `.lotics/app_fields.ts` source from an
|
|
137
|
+
* installation's live binding. Pure — same binding → same bytes. Idempotent.
|
|
138
|
+
* An empty binding yields valid, empty `F`/`OPT`/`ROLE` maps so the file always
|
|
139
|
+
* compiles and imports resolve.
|
|
140
|
+
*/
|
|
141
|
+
export function generatePackageAppFields(binding) {
|
|
142
|
+
const { entities, roles } = parseBinding(binding);
|
|
90
143
|
return `${HEADER}
|
|
91
|
-
${
|
|
144
|
+
${emitFieldMap(entities)}
|
|
92
145
|
|
|
93
|
-
${
|
|
146
|
+
${emitOptionMap(entities)}
|
|
94
147
|
|
|
95
|
-
${
|
|
148
|
+
${emitRoleMap(roles)}
|
|
96
149
|
|
|
97
150
|
/** Field-id alias map: \`F[<ENTITY>][<field>]\` is this installation's \`fld_…\` id. */
|
|
98
151
|
export type AppFields = typeof F;
|