@lotics/cli 0.76.1 → 0.86.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/cli.js CHANGED
@@ -29709,15 +29709,6 @@ var LoticsClient = class {
29709
29709
  async listWorkspaces() {
29710
29710
  return this.request("GET", "/v1/workspaces");
29711
29711
  }
29712
- /**
29713
- * Resolve one workspace's info by id from the org's workspace list (the only
29714
- * API-key-accessible source carrying `is_dev`). Returns null when the
29715
- * workspace isn't visible to these credentials.
29716
- */
29717
- async getWorkspaceInfo(id) {
29718
- const workspaces = await this.listWorkspaces();
29719
- return workspaces.find((w) => w.id === id) ?? null;
29720
- }
29721
29712
  async createWorkspace(body) {
29722
29713
  return this.request("POST", "/v1/workspaces", body);
29723
29714
  }
@@ -29785,7 +29776,7 @@ var LoticsClient = class {
29785
29776
  /**
29786
29777
  * Uninstall a standalone content package — delete the installation row. By
29787
29778
  * default the package-bound docs are ARCHIVED; `keep_content` retains them as
29788
- * ordinary workspace docs. Admin-only. Backs `lotics package uninstall`.
29779
+ * ordinary workspace docs. Admin-only. Backs `lotics uninstall`.
29789
29780
  */
29790
29781
  async uninstallContentPackage(installation_id, opts = {}) {
29791
29782
  const qs = opts.keep_content ? "?keep_content=true" : "";
@@ -29833,7 +29824,7 @@ var LoticsClient = class {
29833
29824
  );
29834
29825
  }
29835
29826
  /**
29836
- * Uninstall a package installation (backs `lotics package uninstall`). Does
29827
+ * Uninstall a package installation (backs `lotics uninstall`). Does
29837
29828
  * everything DELETE does plus archives the installation's lifecycle
29838
29829
  * artifacts; with `archive_tables` it also archives the scaffolded entity
29839
29830
  * tables — refused server-side unless this installation created them
@@ -29851,10 +29842,11 @@ var LoticsClient = class {
29851
29842
  return this.request("PATCH", `/v1/apps/${encodeURIComponent(app_id)}/package-config`, body);
29852
29843
  }
29853
29844
  /**
29854
- * Retire (or `undo` un-retire) a registry package (backs `lotics package
29855
- * retire`). Retiring refuses NEW installs and hides the package from
29856
- * non-owning orgs; existing installations keep working and may still upgrade.
29857
- * Owner-org admin-only.
29845
+ * Retire (or `undo` un-retire) a registry package (backs `lotics app
29846
+ * unpublish` the endpoint/audit action keep the `retire` name to avoid API
29847
+ * churn). Retiring refuses NEW installs and hides the package from non-owning
29848
+ * orgs; existing installations keep working and may still upgrade. Owner-org
29849
+ * admin-only.
29858
29850
  */
29859
29851
  async retirePackage(package_id, body) {
29860
29852
  return this.request("POST", `/v1/packages/${encodeURIComponent(package_id)}/retire`, body);
@@ -29869,38 +29861,12 @@ var LoticsClient = class {
29869
29861
  async ejectPackage(app_id) {
29870
29862
  return this.request("POST", `/v1/apps/${encodeURIComponent(app_id)}/eject`);
29871
29863
  }
29872
- /**
29873
- * Extract a DRAFT app package from an existing bespoke app — the promotion
29874
- * read (docs/packages.md § Promotion). Pure: nothing is written. Returns
29875
- * the alias-keyed draft `contract` (opaque to the CLI — the server is the
29876
- * validating authority), the origin workspace's `binding` (which doubles as
29877
- * the adopt binding), a findings `report` (any `error` ⇒ not publishable
29878
- * as-is), and the file-backed `template_files` the CLI must stage into the
29879
- * project at their `bytes_ref` paths. Backs `lotics package extract`.
29880
- * Admin-only.
29881
- */
29882
- async extractPackage(app_id, opts = {}) {
29883
- const qs = opts.knowledge && opts.knowledge.length > 0 ? `?knowledge=${encodeURIComponent(JSON.stringify(opts.knowledge))}` : "";
29884
- return this.request("GET", `/v1/apps/${encodeURIComponent(app_id)}/package-extract${qs}`);
29885
- }
29886
- /**
29887
- * Adopt a published package onto an EXISTING (bespoke or ejected) app — the
29888
- * final promotion step. The workspace already holds the concrete objects, so
29889
- * nothing is scaffolded or rewritten: the server verifies the `binding` is
29890
- * complete, live, and FAITHFUL to the version's contract, then writes only the
29891
- * installation pin (the app becomes installation #1, upgradeable again). A
29892
- * ConflictError names the aliases that diverge. `version` omitted adopts the
29893
- * latest. Backs `lotics package adopt`. Admin-only.
29894
- */
29895
- async adoptPackage(app_id, body) {
29896
- return this.request("POST", `/v1/apps/${encodeURIComponent(app_id)}/package-adopt`, body);
29897
- }
29898
29864
  /**
29899
29865
  * Fleet upgrade — bring every installation of a package across the CALLER'S
29900
29866
  * org to the target version (latest when omitted) in one call. Hands-off
29901
29867
  * applies only where the preview is clean; installations with breaking/
29902
29868
  * drift/modified-core findings are skipped and reported for the normal
29903
- * per-installation consent flow. Backs `lotics package fleet-upgrade`.
29869
+ * per-installation consent flow. Backs `lotics upgrade <package_id>` (fleet).
29904
29870
  * Admin-only; org-scoped (no workspace header needed).
29905
29871
  */
29906
29872
  /**
@@ -29922,16 +29888,10 @@ var LoticsClient = class {
29922
29888
  body
29923
29889
  );
29924
29890
  }
29925
- // --- App packages (registry authoring + dev harness) ---
29926
- /**
29927
- * Create a registry app package the Lotics-owned, workspace-agnostic
29928
- * blueprint. `lotics package publish` calls this on first publish (when the
29929
- * local manifest has no `package_id`), then publishes version 1 against the
29930
- * returned id. Admin-only.
29931
- */
29932
- async createPackage(body) {
29933
- return this.request("POST", "/v1/packages", body);
29934
- }
29891
+ // --- Packages (registry reads + installations) ---
29892
+ // Authoring is server-side: apps via `POST /v1/apps/{id}/package-publish|release`,
29893
+ // content packages via the `publish_content`/`release_content` tools. There is no
29894
+ // client-side create-package / upload-bundle path.
29935
29895
  /**
29936
29896
  * Fetch a registry app package's metadata (incl. `kind`, `latest_version` and
29937
29897
  * the Lotics-backed `is_official` trust badge). Admin-only; cross-tenant by id.
@@ -29943,33 +29903,6 @@ var LoticsClient = class {
29943
29903
  async listPackageVersions(package_id) {
29944
29904
  return this.request("GET", `/v1/packages/${encodeURIComponent(package_id)}/versions`);
29945
29905
  }
29946
- /**
29947
- * Publish a new immutable package version — multipart upload of the alias-keyed
29948
- * contract (JSON) + the prebuilt code bundle (a gzipped tarball carrying
29949
- * `source.tar.gz` + `dist.tar.gz` members). The server validates the contract +
29950
- * bundle, then allocates the next monotonic version. The `contract` is opaque
29951
- * JSON to the transport (the server is the validating authority). Admin-only.
29952
- */
29953
- async publishPackageVersion(package_id, args) {
29954
- const formData = new FormData();
29955
- formData.append("contract", JSON.stringify(args.contract));
29956
- formData.append(
29957
- "bundle",
29958
- new Blob([new Uint8Array(args.bundle)], { type: "application/gzip" }),
29959
- "bundle.tar.gz"
29960
- );
29961
- if (args.changelog) formData.append("changelog", args.changelog);
29962
- if (args.channel) formData.append("channel", args.channel);
29963
- const url2 = `${this.baseUrl}/v1/packages/${encodeURIComponent(package_id)}/versions`;
29964
- const response = await fetch(url2, {
29965
- method: "POST",
29966
- headers: this.buildHeaders(),
29967
- // no Content-Type — fetch sets the multipart boundary
29968
- body: formData
29969
- });
29970
- if (!response.ok) await this.throwResponseError(response);
29971
- return response.json();
29972
- }
29973
29906
  /**
29974
29907
  * Upgrade a package installation to a newer published version — extends the
29975
29908
  * binding additively, re-materializes the target version's
@@ -29996,18 +29929,6 @@ var LoticsClient = class {
29996
29929
  `/v1/apps/${encodeURIComponent(app_id)}/package-upgrade${query}`
29997
29930
  );
29998
29931
  }
29999
- /**
30000
- * Re-bind a package role to a different workspace group (the current group
30001
- * still exists). Re-materializes at the pinned version; refuses over
30002
- * modified-core findings. Admin-only.
30003
- */
30004
- async rebindPackageRole(app_id, body) {
30005
- return this.request(
30006
- "POST",
30007
- `/v1/apps/${encodeURIComponent(app_id)}/package-rebind-role`,
30008
- body
30009
- );
30010
- }
30011
29932
  /**
30012
29933
  * Workspace-wide dangling-reference sweep — active app/workflow artifacts
30013
29934
  * whose prefixed schema ids no longer resolve. Backs
@@ -30025,13 +29946,73 @@ var LoticsClient = class {
30025
29946
  return this.request("GET", `/v1/apps/${encodeURIComponent(app_id)}/package-health`);
30026
29947
  }
30027
29948
  /**
30028
- * Reset a package installation in a DEV workspace drop the package-owned
30029
- * scaffolded tables and re-scaffold clean. Hard-gated server-side to dev
30030
- * workspaces (a non-dev workspace is refused). Returns the resulting app.
30031
- * Admin-only.
29949
+ * Preview a release the dry run behind `lotics app release`. Runs the
29950
+ * binding-aware extract of the origin (aliases stable through the app's current
29951
+ * binding) and reports the next version number, the new + changed aliases, the
29952
+ * bundled-knowledge delta, and any extract findings (an `error` blocks the
29953
+ * apply). An optional `knowledge` declaration (from the pulled app manifest)
29954
+ * re-declares the bundle set — added/dropped/changed docs surface in the delta;
29955
+ * omitted, the current corpus is reconstructed from the pin. No writes. Admin,
29956
+ * owning-org only.
29957
+ */
29958
+ async previewPackageRelease(app_id, opts = {}) {
29959
+ const params = new URLSearchParams();
29960
+ if (opts.knowledge !== void 0 && opts.knowledge.length > 0) {
29961
+ params.set("knowledge", JSON.stringify(opts.knowledge));
29962
+ }
29963
+ const query = params.toString();
29964
+ return this.request(
29965
+ "GET",
29966
+ `/v1/apps/${encodeURIComponent(app_id)}/package-release${query ? `?${query}` : ""}`
29967
+ );
29968
+ }
29969
+ /**
29970
+ * Release — snapshot the origin app into the next registry version. The server
29971
+ * binding-aware-extracts it, repackages its deployed source + dist as the
29972
+ * bundle, publishes the next `release`-channel version with the changelog, and
29973
+ * re-pins the origin. An optional `knowledge` declaration re-declares the
29974
+ * bundled-knowledge set (added/dropped/re-snapshotted docs; omitted preserves
29975
+ * the current corpus). Error findings from extract surface as a 409; a
29976
+ * missing/archived declared doc is a 400, a foreign-package doc a 409. Admin,
29977
+ * owning-org only. Backs `lotics app release --yes`.
29978
+ */
29979
+ async releasePackage(app_id, body) {
29980
+ return this.request("POST", `/v1/apps/${encodeURIComponent(app_id)}/package-release`, body);
29981
+ }
29982
+ /**
29983
+ * Dry-run preview of a first-release — the `GET` behind `lotics app publish`
29984
+ * (no `--yes`), the publish-side analogue of `previewPackageRelease`. The
29985
+ * server runs the same fresh-alias extract + `src/` scan the apply runs
29986
+ * (through any `renames`) and returns the package name it would mint, the
29987
+ * auto-minted RENAMABLE aliases (the exact `--rename` keys), and the extract
29988
+ * findings (an `error` blocks the apply). No writes. Admin-only.
29989
+ */
29990
+ async previewPublishAppPackage(app_id, opts = {}) {
29991
+ const params = new URLSearchParams();
29992
+ if (opts.knowledge !== void 0 && opts.knowledge.length > 0) {
29993
+ params.set("knowledge", JSON.stringify(opts.knowledge));
29994
+ }
29995
+ if (opts.renames !== void 0 && opts.renames.length > 0) {
29996
+ params.set("renames", JSON.stringify(opts.renames));
29997
+ }
29998
+ const query = params.toString();
29999
+ return this.request(
30000
+ "GET",
30001
+ `/v1/apps/${encodeURIComponent(app_id)}/package-publish${query ? `?${query}` : ""}`
30002
+ );
30003
+ }
30004
+ /**
30005
+ * First-release apply — mint a package from a BESPOKE app and publish v1 in one
30006
+ * call (the `POST` behind `lotics app publish --yes`). The server extracts an
30007
+ * alias-keyed contract from the app (fresh aliases; `renames` fixes them before
30008
+ * v1 freezes), creates the registry package (name/description from the app),
30009
+ * publishes v1 from the app's deployed source + dist, and pins the origin as
30010
+ * installation #1. Error findings from extract surface as a 409. An
30011
+ * already-linked app must use `releasePackage` instead. Admin-only. Backs
30012
+ * `lotics app publish <app_id>`.
30032
30013
  */
30033
- async resetPackage(app_id) {
30034
- return this.request("POST", `/v1/apps/${encodeURIComponent(app_id)}/package-reset`);
30014
+ async publishAppAsPackage(app_id, body) {
30015
+ return this.request("POST", `/v1/apps/${encodeURIComponent(app_id)}/package-publish`, body);
30035
30016
  }
30036
30017
  /**
30037
30018
  * Resolve the display name + fields (incl. select options) of the given tables
@@ -31360,379 +31341,6 @@ See https://lotics.ai/docs/app-sdk for the SDK reference.
31360
31341
  }
31361
31342
  ];
31362
31343
  }
31363
- function buildPackageStarterOverrides(args) {
31364
- const nameLit = JSON.stringify(args.app_name);
31365
- return [
31366
- {
31367
- // The package App.tsx imports `../.lotics/app_fields` — a DETERMINISTIC
31368
- // pure function of contract.json (generate_package_fields.ts) — so unlike
31369
- // the app starter's fully-ignored `.lotics`, it must be committed or the
31370
- // scaffold's own shipped CI (npm ci → typecheck/test/build on a clean
31371
- // clone) fails on the missing module. The sync-written `.d.ts` companions
31372
- // stay ignored: they need a live workspace and their absence only degrades
31373
- // hooks to untyped overloads, never breaks the build.
31374
- path: ".gitignore",
31375
- content: `node_modules
31376
- dist
31377
- *.tsbuildinfo
31378
- .DS_Store
31379
- .lotics/*
31380
- !.lotics/app_fields.ts
31381
- coverage
31382
- `
31383
- },
31384
- {
31385
- path: "src/App.tsx",
31386
- content: `import { useMemo, type ReactNode } from "react";
31387
- import { ScrollView, View } from "react-native";
31388
- import { useConfig, useQuery, readSelect, row } from "@lotics/app-sdk";
31389
- import { AppRouter } from "@lotics/app-sdk/router";
31390
- import { Text } from "@lotics/ui/text";
31391
- import { Card } from "@lotics/ui/card";
31392
- import { Button } from "@lotics/ui/button";
31393
- import { OptionBadge } from "@lotics/ui/option_badge";
31394
- import { Skeleton } from "@lotics/ui/skeleton";
31395
- import { EmptyState } from "@lotics/ui/empty_state";
31396
- import { Callout, CalloutTitle, CalloutText, CalloutActions } from "@lotics/ui/callout";
31397
- import { F, OPT } from "../.lotics/app_fields";
31398
-
31399
- // This is a PACKAGE starter \u2014 a workspace-agnostic blueprint installed into many
31400
- // workspaces, each binding the contract's aliases to DIFFERENT concrete ids. So a
31401
- // screen never hardcodes a \`fld_\u2026\`/\`opt_\u2026\` id: it addresses a field by contract
31402
- // alias through \`F\` and a select option through \`OPT\`, both from the generated
31403
- // \`.lotics/app_fields.ts\` (which resolves every alias to THIS installation's id at
31404
- // module load from the binding). The aliases come from contract.json \u2014 entity
31405
- // \`item\`, fields name/notes/status, options open/done, query \`items\`, config knob
31406
- // \`heading\`. Edit contract.json, then \`lotics package sync\` to regenerate F/OPT.
31407
-
31408
- interface Item {
31409
- id: string;
31410
- name: string;
31411
- notes: string;
31412
- status: ReturnType<typeof readSelect>[number] | null;
31413
- done: boolean;
31414
- }
31415
-
31416
- // The \`items\` query is a bare from_entity, so a row is keyed by FIELD id: read a
31417
- // cell as \`r[F.ITEM.<field>]\` and decode it with the SDK's pure readers (\`row.text\`,
31418
- // \`readSelect\`). \`OPT.ITEM.status.done\` is this install's \`opt_\u2026\` id, so comparing
31419
- // the cell's stored option key to it marks a done row.
31420
- function decode(r: Record<string, unknown>): Item {
31421
- const status = readSelect(r[F.ITEM.status])[0] ?? null;
31422
- return {
31423
- id: row.text(r.__source_record_id),
31424
- name: row.text(r[F.ITEM.name]),
31425
- notes: row.text(r[F.ITEM.notes]),
31426
- status,
31427
- done: status?.key === OPT.ITEM.status.done,
31428
- };
31429
- }
31430
-
31431
- // Outer <View flex:1> claims the iframe height (index.html sets html/body/#root to
31432
- // 100% + #root is a flex column); the list scrolls beneath the heading. Keep this
31433
- // flex chain plain (not @lotics/ui/stack) so a fill-remaining-space child gets height.
31434
- function Screen({ children }: { children: ReactNode }) {
31435
- return (
31436
- <View style={{ flex: 1 }}>
31437
- <ScrollView contentContainerStyle={{ padding: 24, alignItems: "center" }}>
31438
- <View style={{ maxWidth: 640, width: "100%", gap: 16 }}>{children}</View>
31439
- </ScrollView>
31440
- </View>
31441
- );
31442
- }
31443
-
31444
- function ItemsScreen() {
31445
- // \`heading\` is a contract config knob \u2014 an installation overrides it and
31446
- // useConfig() renders the customized value; the literal here is only the fallback.
31447
- const { config } = useConfig({ heading: ${nameLit} });
31448
- const itemsQ = useQuery("items");
31449
- const items = useMemo(() => itemsQ.rows.map(decode), [itemsQ.rows]);
31450
- // First-load only: a skeleton while the very first fetch is in flight with no rows
31451
- // yet \u2014 never blank already-loaded rows to a spinner on a background refetch.
31452
- const firstLoad = itemsQ.loading && items.length === 0;
31453
-
31454
- return (
31455
- <Screen>
31456
- <Text size="xxl" weight="semibold" level={1}>
31457
- {config.heading}
31458
- </Text>
31459
-
31460
- {itemsQ.error ? (
31461
- <Callout tone="error">
31462
- <CalloutTitle>Couldn't load items</CalloutTitle>
31463
- <CalloutText>{itemsQ.error}</CalloutText>
31464
- <CalloutActions>
31465
- <Button title="Try again" onPress={() => itemsQ.refetch()} />
31466
- </CalloutActions>
31467
- </Callout>
31468
- ) : firstLoad ? (
31469
- <View style={{ gap: 10 }}>
31470
- {[0, 1, 2].map((i) => (
31471
- <Skeleton key={i} height={64} radius={12} />
31472
- ))}
31473
- </View>
31474
- ) : items.length > 0 ? (
31475
- <View style={{ gap: 10 }}>
31476
- {items.map((item) => (
31477
- <Card key={item.id}>
31478
- <View style={{ flexDirection: "row", alignItems: "center", gap: 12 }}>
31479
- <View style={{ flex: 1, gap: 2 }}>
31480
- <Text weight="medium" color={item.done ? "muted" : undefined}>
31481
- {item.name}
31482
- </Text>
31483
- {item.notes.length > 0 ? (
31484
- <Text size="sm" color="muted" numberOfLines={1}>
31485
- {item.notes}
31486
- </Text>
31487
- ) : null}
31488
- </View>
31489
- <OptionBadge value={item.status} />
31490
- </View>
31491
- </Card>
31492
- ))}
31493
- </View>
31494
- ) : (
31495
- <EmptyState
31496
- icon="list-checks"
31497
- message="No items yet"
31498
- hint="Rows in this package's table appear here."
31499
- />
31500
- )}
31501
- </Screen>
31502
- );
31503
- }
31504
-
31505
- const routes = [{ path: "/", element: <ItemsScreen /> }];
31506
-
31507
- export default function App() {
31508
- return <AppRouter routes={routes} />;
31509
- }
31510
- `
31511
- },
31512
- {
31513
- path: "src/App.test.tsx",
31514
- content: `import { describe, test, expect, beforeEach, afterEach, vi } from "vitest";
31515
- import { render, screen, cleanup } from "@testing-library/react";
31516
- import App from "./App";
31517
-
31518
- // Mock @lotics/app-sdk \u2014 the app's one external boundary (data + RPC + the package
31519
- // binding). \`importOriginal\` keeps the PURE cell readers (\`row\`, \`readSelect\`) real,
31520
- // so mock rows decode exactly like wire rows; only the hooks and getAppBinding are
31521
- // stubbed. The binding ids below are what F.ITEM.* / OPT.ITEM.status.* resolve to;
31522
- // the query rows are keyed by the SAME ids.
31523
- const h = vi.hoisted(() => ({
31524
- state: {
31525
- rows: [] as Array<Record<string, unknown>>,
31526
- loading: false,
31527
- error: null as string | null,
31528
- },
31529
- binding: {
31530
- fields: { "item.name": "fld_name", "item.notes": "fld_notes", "item.status": "fld_status" },
31531
- options: { "item.status:open": "opt_open", "item.status:done": "opt_done" },
31532
- roles: {},
31533
- },
31534
- }));
31535
-
31536
- vi.mock("@lotics/app-sdk", async (importOriginal) => {
31537
- const actual = await importOriginal<typeof import("@lotics/app-sdk")>();
31538
- return {
31539
- ...actual,
31540
- getAppBinding: async () => h.binding,
31541
- useConfig: (defaults: Record<string, unknown>) => ({ config: defaults, loading: false }),
31542
- useQuery: () => ({
31543
- rows: h.state.rows,
31544
- loading: h.state.loading,
31545
- isValidating: false,
31546
- error: h.state.error,
31547
- refetch: () => {},
31548
- }),
31549
- };
31550
- });
31551
-
31552
- const rowOf = (id: string, name: string, notes: string, statusKey: string) => ({
31553
- __source_record_id: id,
31554
- fld_name: name,
31555
- fld_notes: notes,
31556
- fld_status: [{ key: statusKey, label: statusKey === "opt_done" ? "Done" : "Open" }],
31557
- });
31558
-
31559
- beforeEach(() => {
31560
- h.state.rows = [];
31561
- h.state.loading = false;
31562
- h.state.error = null;
31563
- });
31564
-
31565
- // vitest globals are off, so @testing-library/react's automatic afterEach cleanup is
31566
- // never registered \u2014 clean up explicitly or renders accumulate across tests and a
31567
- // second render's duplicate matches fail getByText.
31568
- afterEach(cleanup);
31569
-
31570
- describe("App", () => {
31571
- test("renders the heading and the loaded items with status badges", () => {
31572
- h.state.rows = [
31573
- rowOf("1", "First item", "with a note", "opt_open"),
31574
- rowOf("2", "Second item", "", "opt_done"),
31575
- ];
31576
- render(<App />);
31577
-
31578
- expect(screen.getByText(${nameLit})).toBeTruthy();
31579
- expect(screen.getByText("First item")).toBeTruthy();
31580
- expect(screen.getByText("Second item")).toBeTruthy();
31581
- expect(screen.getByText("with a note")).toBeTruthy();
31582
- expect(screen.getByText("Open")).toBeTruthy();
31583
- expect(screen.getByText("Done")).toBeTruthy();
31584
- expect(screen.queryByText("No items yet")).toBeNull();
31585
- });
31586
-
31587
- test("shows the empty state when there are no items", () => {
31588
- render(<App />);
31589
- expect(screen.getByText("No items yet")).toBeTruthy();
31590
- });
31591
-
31592
- test("does not flash the list or empty state on first load", () => {
31593
- h.state.loading = true;
31594
- render(<App />);
31595
- expect(screen.queryByText("No items yet")).toBeNull();
31596
- });
31597
-
31598
- test("surfaces a load error loudly", () => {
31599
- h.state.error = "Network unreachable";
31600
- render(<App />);
31601
- expect(screen.getByText("Couldn't load items")).toBeTruthy();
31602
- expect(screen.getByText("Network unreachable")).toBeTruthy();
31603
- });
31604
- });
31605
- `
31606
- },
31607
- {
31608
- path: "README.md",
31609
- content: `# ${escapeHtml(args.app_name)}
31610
-
31611
- A Lotics **app package** \u2014 a versioned, installable blueprint (a \`contract.json\`
31612
- data model + app source) that installs into many workspaces. Authored locally,
31613
- published and run through the \`lotics package\` CLI. See \`docs/app_packages.md\`.
31614
-
31615
- ## Dev loop
31616
-
31617
- \`\`\`bash
31618
- lotics workspace create "${escapeHtml(args.app_name)} dev" --dev # a throwaway dev workspace
31619
- lotics package dev --workspace <dev_ws> # sync into it + run the dev server
31620
- # edit contract.json \u2192 re-run \`lotics package sync\` to migrate + re-materialize
31621
- # edit src/* \u2192 hot reload
31622
- \`\`\`
31623
-
31624
- \`sync\` / \`dev\` regenerate \`.lotics/app_fields.ts\` (the runtime \`F\` / \`OPT\` / \`ROLE\`
31625
- surface) from \`contract.json\`, plus the typed \`.lotics/app_{queries,workflows,agents}.d.ts\`
31626
- companions from the live installation \u2014 so \`useQuery\` / \`useWorkflow\` stay typed in the
31627
- dev loop.
31628
-
31629
- ## Quality
31630
-
31631
- \`\`\`bash
31632
- npm run typecheck
31633
- npm run lint
31634
- npm test
31635
- \`\`\`
31636
-
31637
- ## Publish
31638
-
31639
- \`\`\`bash
31640
- lotics package publish -m "v1" # build + publish an immutable version
31641
- lotics package install <package_id> # install into any workspace
31642
- \`\`\`
31643
-
31644
- ## The app screen
31645
-
31646
- \`src/App.tsx\` reads the contract BY ALIAS through \`F\` / \`OPT\` (from the generated
31647
- \`.lotics/app_fields.ts\`) \u2014 never a raw \`fld_\u2026\` / \`opt_\u2026\` id, since every install binds
31648
- different concrete ids. It lists the \`items\` query and renders the \`heading\` config knob.
31649
- \`@lotics/ui\` primitives render via react-native-web (aliased in \`vite.config.ts\`).
31650
-
31651
- ## Contract reference
31652
-
31653
- \`contract.json\` is the package's alias-keyed data model
31654
- (schema: \`@lotics/shared/schemas/app_packages\`). Every cross-reference is by **alias**;
31655
- the materializer resolves alias \u2192 this workspace's concrete id at install.
31656
-
31657
- ### Aliases
31658
-
31659
- - Entity / field / option / role / template / config aliases are lowercase slugs
31660
- matching \`^[a-z][a-z0-9_]*$\`. A field's fully-qualified key is \`<entity>.<field>\`
31661
- (e.g. \`item.status\`); a select option's is \`<entity>.<field>:<option>\`
31662
- (e.g. \`item.status:done\`).
31663
- - Query / workflow / agent aliases are runtime lookup keys (1\u2013200 chars) \u2014 the app
31664
- source invokes them verbatim (\`useQuery("items")\`), so renaming one severs the call.
31665
-
31666
- ### Entities & fields
31667
-
31668
- \`\`\`jsonc
31669
- { "alias": "item", "label": "Item", "fields": [
31670
- { "alias": "name", "label": "Name", "type": "text", "required": true },
31671
- { "alias": "status", "label": "Status", "type": "select",
31672
- "options": [ { "alias": "open", "label": "Open", "color": "blue" },
31673
- { "alias": "done", "label": "Done", "color": "green" } ] } ] }
31674
- \`\`\`
31675
-
31676
- Field \`type\`: \`text\`, \`number\`, \`date\`, \`boolean\`, \`select\` (+ \`options\`),
31677
- \`select_member\`, \`select_record_link\` (\`target_entity\` = an entity alias), \`files\`,
31678
- \`formula\` (expression references same-entity fields as \`{alias}\`), \`rollup\`, \`lookup\`,
31679
- \`autonumber\`. \`required\` is advisory (app / workflow-layer UX only \u2014 the table model
31680
- has no required constraint). Select \`options\` are \`{ alias, label, color }\`.
31681
-
31682
- ### Queries
31683
-
31684
- Alias-form AST \u2014 the same node kinds as the runtime query engine, except a
31685
- \`from_table\` node carries \`from_entity\` (an entity alias) instead of a \`table_id\`:
31686
-
31687
- \`\`\`jsonc
31688
- { "alias": "items", "ast": {
31689
- "kind": "from_table", "from_entity": "item",
31690
- "sort": [ { "field_key": "name", "order": "asc" } ] } }
31691
- \`\`\`
31692
-
31693
- ### Workflows
31694
-
31695
- The sole app-side mutation path. Each workflow has a \`trigger\`, typed \`inputs\` /
31696
- \`outputs\`, and a JS-subset \`body\`.
31697
-
31698
- - Trigger \u2014 app-invoked or table-lifecycle:
31699
- - \`{ "type": "app" }\` \u2014 called by alias from the app (\`useWorkflow("<alias>")\`).
31700
- - \`{ "type": "entity_lifecycle", "entity": "<entity>", "event": "<event>" }\` \u2014 fires
31701
- on the bound table. \`event\` \u2208 \`before_create\`, \`after_create\`, \`before_update\`,
31702
- \`after_update\`, \`before_delete\`, \`after_delete\`.
31703
- - Typed \`inputs\` (\`text\` / \`number\` / \`date\` / \`member\` / \`select\` / \`record_link\`,
31704
- plus \`object\` / \`array\`):
31705
- - \`record_link\` \u2014 its \`table_id\` names an **entity alias**.
31706
- - \`member\` \u2014 its \`group\` names a **role alias**.
31707
- - \`select\` \u2014 an option \`value\` in \`entity.field:option\` form binds to that option's
31708
- id (a plain value is a literal, left as-is).
31709
- - Body **sentinel tokens** \u2014 a body addresses package objects by reserved tokens the
31710
- materializer rewrites to concrete ids at install. Always inside a **string literal**:
31711
- - \`@@entity:<entity>@@\`
31712
- - \`@@field:<entity>.<field>@@\`
31713
- - \`@@option:<entity>.<field>:<option>@@\`
31714
- - \`@@role:<role>@@\`
31715
- - \`@@template:<template>@@\`
31716
-
31717
- ### Config knobs
31718
-
31719
- Typed customization knobs the app reads via \`useConfig()\`. Each has an \`alias\`,
31720
- \`label\`, \`type\`, and \`default\`:
31721
-
31722
- - \`text\` \u2192 a string default; \`boolean\` \u2192 a boolean; \`number\` \u2192 a number;
31723
- \`color\` \u2192 a palette color token; \`select\` \u2192 \`options: [{ value, label }]\` with the
31724
- \`default\` being one of those \`value\`s.
31725
-
31726
- \`\`\`jsonc
31727
- { "alias": "heading", "label": "List heading", "type": "text", "default": "Items" }
31728
- \`\`\`
31729
-
31730
- Publish validates the whole contract (alias uniqueness, every cross-reference
31731
- resolves, sentinel tokens name declared objects) before storing the version.
31732
- `
31733
- }
31734
- ];
31735
- }
31736
31344
  function escapeHtml(s) {
31737
31345
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
31738
31346
  }
@@ -32491,7 +32099,7 @@ function inputDeclToTsType(decl) {
32491
32099
  }
32492
32100
  return null;
32493
32101
  }).filter((v) => v !== null);
32494
- const inner = literals.length > 0 ? literals.join(" | ") : "string";
32102
+ const inner = literals.length > 0 ? `${literals.join(" | ")} | (string & {})` : "string";
32495
32103
  return decl.multi === true ? `ReadonlyArray<${inner}>` : inner;
32496
32104
  }
32497
32105
  case "date_range":
@@ -32543,7 +32151,7 @@ function outputDeclToTsType(decl) {
32543
32151
  const literals = options.map(
32544
32152
  (o) => o !== null && typeof o === "object" && "value" in o && typeof o.value === "string" ? JSON.stringify(o.value) : null
32545
32153
  ).filter((v) => v !== null);
32546
- const inner = literals.length > 0 ? literals.join(" | ") : "string";
32154
+ const inner = literals.length > 0 ? `${literals.join(" | ")} | (string & {})` : "string";
32547
32155
  return decl.multi === true ? `ReadonlyArray<${inner}>` : inner;
32548
32156
  }
32549
32157
  case "object": {
@@ -32802,6 +32410,123 @@ export type AppOptions = typeof OPT;
32802
32410
  `;
32803
32411
  }
32804
32412
 
32413
+ // src/generate_package_fields.ts
32414
+ var HEADER5 = `// Auto-generated by 'lotics app codegen' (linked/published app; also app pull/dev/deploy).
32415
+ // DO NOT EDIT \u2014 regenerated from the installation's live binding.
32416
+ //
32417
+ // A package installation resolves F/OPT/ROLE at MODULE LOAD from its binding
32418
+ // (contract alias \u2192 THIS workspace's concrete id) via the SDK's \`binding\` RPC.
32419
+ // Top-level await: the module graph waits for the binding before any importer
32420
+ // evaluates, so every entry is a plain string.
32421
+ import { getAppBinding } from "@lotics/app-sdk";
32422
+
32423
+ const binding = await getAppBinding();
32424
+
32425
+ function bound(map: Record<string, string>, key: string, kind: string): string {
32426
+ const id = map[key];
32427
+ if (id === undefined) {
32428
+ throw new Error(
32429
+ \`app_fields: \${kind} "\${key}" is not in this installation's binding \u2014 \` +
32430
+ \`the generated app_fields.ts is stale relative to the installed contract version.\`,
32431
+ );
32432
+ }
32433
+ return id;
32434
+ }
32435
+ `;
32436
+ function parseBinding(binding) {
32437
+ const entities = [];
32438
+ const entityByAlias = /* @__PURE__ */ new Map();
32439
+ const fieldByKey = /* @__PURE__ */ new Map();
32440
+ for (const key of Object.keys(binding.fields)) {
32441
+ const dot = key.indexOf(".");
32442
+ if (dot <= 0 || dot >= key.length - 1) continue;
32443
+ const entityAlias = key.slice(0, dot);
32444
+ const fieldAlias = key.slice(dot + 1);
32445
+ let entity = entityByAlias.get(entityAlias);
32446
+ if (entity === void 0) {
32447
+ entity = { alias: entityAlias, fields: [] };
32448
+ entityByAlias.set(entityAlias, entity);
32449
+ entities.push(entity);
32450
+ }
32451
+ const field = { alias: fieldAlias, key, options: [] };
32452
+ entity.fields.push(field);
32453
+ fieldByKey.set(key, field);
32454
+ }
32455
+ for (const key of Object.keys(binding.options)) {
32456
+ const colon = key.indexOf(":");
32457
+ if (colon <= 0 || colon >= key.length - 1) continue;
32458
+ const fieldKey = key.slice(0, colon);
32459
+ const optionAlias = key.slice(colon + 1);
32460
+ const field = fieldByKey.get(fieldKey);
32461
+ if (field === void 0) continue;
32462
+ field.options.push({ alias: optionAlias, key });
32463
+ }
32464
+ return { entities, roles: Object.keys(binding.roles) };
32465
+ }
32466
+ function emitFieldMap2(entities) {
32467
+ if (entities.length === 0) return `export const F = {} as const;`;
32468
+ const blocks = entities.map((entity) => {
32469
+ const lines = entity.fields.map(
32470
+ (field) => ` ${propKey(field.alias)}: bound(binding.fields, ${JSON.stringify(field.key)}, "field"),`
32471
+ );
32472
+ return ` ${propKey(entity.alias.toUpperCase())}: {
32473
+ ${lines.join("\n")}
32474
+ },`;
32475
+ });
32476
+ return `export const F = {
32477
+ ${blocks.join("\n")}
32478
+ } as const;`;
32479
+ }
32480
+ function emitOptionMap2(entities) {
32481
+ const entityBlocks = [];
32482
+ for (const entity of entities) {
32483
+ const fieldBlocks = [];
32484
+ for (const field of entity.fields) {
32485
+ if (field.options.length === 0) continue;
32486
+ const lines = field.options.map(
32487
+ (option) => ` ${propKey(option.alias)}: bound(binding.options, ${JSON.stringify(option.key)}, "option"),`
32488
+ );
32489
+ fieldBlocks.push(` ${propKey(field.alias)}: {
32490
+ ${lines.join("\n")}
32491
+ },`);
32492
+ }
32493
+ if (fieldBlocks.length === 0) continue;
32494
+ entityBlocks.push(` ${propKey(entity.alias.toUpperCase())}: {
32495
+ ${fieldBlocks.join("\n")}
32496
+ },`);
32497
+ }
32498
+ if (entityBlocks.length === 0) return `export const OPT = {} as const;`;
32499
+ return `export const OPT = {
32500
+ ${entityBlocks.join("\n")}
32501
+ } as const;`;
32502
+ }
32503
+ function emitRoleMap(roles) {
32504
+ if (roles.length === 0) return `export const ROLE = {} as const;`;
32505
+ const lines = roles.map(
32506
+ (role) => ` ${propKey(role)}: bound(binding.roles, ${JSON.stringify(role)}, "role"),`
32507
+ );
32508
+ return `export const ROLE = {
32509
+ ${lines.join("\n")}
32510
+ } as const;`;
32511
+ }
32512
+ function generatePackageAppFields(binding) {
32513
+ const { entities, roles } = parseBinding(binding);
32514
+ return `${HEADER5}
32515
+ ${emitFieldMap2(entities)}
32516
+
32517
+ ${emitOptionMap2(entities)}
32518
+
32519
+ ${emitRoleMap(roles)}
32520
+
32521
+ /** Field-id alias map: \`F[<ENTITY>][<field>]\` is this installation's \`fld_\u2026\` id. */
32522
+ export type AppFields = typeof F;
32523
+ /** Select-option alias map: \`OPT[<ENTITY>][<field>][<option>]\` is this installation's \`opt_\u2026\` id. */
32524
+ export type AppOptions = typeof OPT;
32525
+ /** Role alias map: \`ROLE[<role>]\` is this installation's \`grp_\u2026\` id. */
32526
+ export type AppRoles = typeof ROLE;
32527
+ `;
32528
+ }
32529
+
32805
32530
  // src/app_workflow_check.ts
32806
32531
  import fs3 from "node:fs";
32807
32532
  import path4 from "node:path";
@@ -33128,6 +32853,13 @@ function writeAppFields(projectDir, tables) {
33128
32853
  fs4.writeFileSync(file2, generateAppFields(tables));
33129
32854
  return file2;
33130
32855
  }
32856
+ function writeBindingAppFields(projectDir, binding) {
32857
+ const dotLotics = path5.join(projectDir, ".lotics");
32858
+ fs4.mkdirSync(dotLotics, { recursive: true });
32859
+ const file2 = path5.join(dotLotics, "app_fields.ts");
32860
+ fs4.writeFileSync(file2, generatePackageAppFields(binding));
32861
+ return file2;
32862
+ }
33131
32863
  async function appCodegen(args) {
33132
32864
  const projectDir = path5.resolve(args.projectDir ?? process.cwd());
33133
32865
  const meta3 = readAppMeta(projectDir);
@@ -33143,14 +32875,24 @@ async function appCodegen(args) {
33143
32875
  );
33144
32876
  return;
33145
32877
  }
33146
- const tableIds = resolveCodegenTableIds(projectDir, meta3.queries ?? {});
33147
32878
  try {
33148
- const tables = await args.client.getWorkspaceSchema(tableIds);
33149
- const fieldsPath = writeAppFields(projectDir, tables);
33150
- console.error(`Regenerated ${fieldsPath} (${tables.length} table${tables.length === 1 ? "" : "s"})`);
32879
+ const app = await args.client.getApp(meta3.app_id);
32880
+ if (app.package_id) {
32881
+ const binding = await args.client.appBinding(meta3.app_id);
32882
+ const fieldsPath = writeBindingAppFields(projectDir, binding);
32883
+ const count = Object.keys(binding.fields).length;
32884
+ console.error(
32885
+ `Regenerated ${fieldsPath} (binding form \u2014 ${count} field alias${count === 1 ? "" : "es"} resolved per-install)`
32886
+ );
32887
+ } else {
32888
+ const tableIds = resolveCodegenTableIds(projectDir, meta3.queries ?? {});
32889
+ const tables = await args.client.getWorkspaceSchema(tableIds);
32890
+ const fieldsPath = writeAppFields(projectDir, tables);
32891
+ console.error(`Regenerated ${fieldsPath} (${tables.length} table${tables.length === 1 ? "" : "s"})`);
32892
+ }
33151
32893
  } catch (err2) {
33152
32894
  console.error(
33153
- `\u26A0 Could not fetch the workspace schema (${err2 instanceof Error ? err2.message : String(err2)}). Kept the existing .lotics/app_fields.ts.`
32895
+ `\u26A0 Could not regenerate .lotics/app_fields.ts (${err2 instanceof Error ? err2.message : String(err2)}). Kept the existing file.`
33154
32896
  );
33155
32897
  }
33156
32898
  await refreshWorkflowGlobals(args.client, projectDir, meta3.app_id, Object.keys(meta3.workflows ?? {}));
@@ -33834,10 +33576,8 @@ function escapeRegExp(s) {
33834
33576
  }
33835
33577
 
33836
33578
  // src/package_commands.ts
33837
- import fs6 from "node:fs";
33838
- import path7 from "node:path";
33839
- import { tmpdir as tmpdir2 } from "node:os";
33840
- import { createHash } from "node:crypto";
33579
+ import fs5 from "node:fs";
33580
+ import path6 from "node:path";
33841
33581
 
33842
33582
  // ../../node_modules/zod/v4/classic/external.js
33843
33583
  var external_exports = {};
@@ -49725,7 +49465,7 @@ var appSchema = zod_default.object({
49725
49465
  // avoid a circular import; the shapes mirror Binding / AppInstallationConfig.
49726
49466
  package_id: zod_default.string().nullable().optional().describe("App-package registry id this app was installed from; null for a bespoke app or after eject."),
49727
49467
  package_version: zod_default.number().int().nullable().optional().describe("Installed package version (the upgrade pin); null for a bespoke app or after eject."),
49728
- binding: zod_default.record(zod_default.string(), zod_default.record(zod_default.string(), zod_default.string())).nullable().optional().describe("App-package install join: per-namespace (entities/fields/options/templates/roles) alias \u2192 concrete-id maps. Null for a bespoke app."),
49468
+ binding: zod_default.record(zod_default.string(), zod_default.record(zod_default.string(), zod_default.string())).nullable().optional().describe("App-package install join: per-namespace (entities/fields/options/templates/roles/workflows/knowledge) alias \u2192 concrete-id maps. Null for a bespoke app."),
49729
49469
  config: zod_default.record(zod_default.string(), zod_default.union([zod_default.string(), zod_default.number(), zod_default.boolean()])).nullable().optional().describe("Installation-level customization config values (the customization-ladder first rung). Null for a bespoke app."),
49730
49470
  ejected_at: zod_default.string().nullable().optional().describe("When the app was ejected from its package (severing upgrades); null if never ejected."),
49731
49471
  created_at: zod_default.string().describe("Timestamp when app was created"),
@@ -49768,7 +49508,6 @@ var workspaceSchema = zod_default.object({
49768
49508
  default_currency: zod_default.string().describe("Default currency code for the workspace"),
49769
49509
  timezone: zod_default.string().describe("IANA timezone name (e.g., 'America/New_York', 'Asia/Tokyo')"),
49770
49510
  organization_id: zod_default.string().describe("Organization ID this workspace belongs to"),
49771
- is_dev: zod_default.boolean().optional().describe("Marks a throwaway app-package dev workspace \u2014 gates `lotics package dev/sync/reset`."),
49772
49511
  created_at: zod_default.string().describe("Timestamp when workspace was created")
49773
49512
  });
49774
49513
  var memberGroupSchema = zod_default.object({
@@ -50315,7 +50054,14 @@ var bindingSchema = zod_default.object({
50315
50054
  // it, and hashes look rows up through it. Excluded from BINDING_NAMESPACES:
50316
50055
  // a deleted row is a modified-core finding (fingerprints), never binding
50317
50056
  // drift, so validateBinding/applyDriftResolutions ignore it.
50318
- workflows: bindingMapSchema.default({})
50057
+ workflows: bindingMapSchema.default({}),
50058
+ // The app's BUNDLED knowledge corpus: contract knowledge alias → the
50059
+ // workspace's concrete `kdc_` id. One row, one pin — the app installation IS
50060
+ // the anchor for its bundled docs (`package_content_installations` rows are
50061
+ // standalone content packages only). Excluded from BINDING_NAMESPACES: a
50062
+ // gone/edited doc is detected by the knowledge upgrade preview (live doc +
50063
+ // content-sha reads), never by validateBinding's live-id sweep.
50064
+ knowledge: bindingMapSchema.default({})
50319
50065
  });
50320
50066
  var BINDING_NAMESPACES = ["entities", "fields", "options", "templates", "roles"];
50321
50067
  var contentBindingMapSchema = zod_default.record(zod_default.string().min(1), zod_default.string().min(1));
@@ -50349,10 +50095,6 @@ var packageArtifactHashesSchema = zod_default.object({
50349
50095
  agents: zod_default.record(zod_default.string(), zod_default.string())
50350
50096
  });
50351
50097
  var modificationResolutionSchema = zod_default.union([zod_default.literal("revert"), zod_default.literal("keep")]);
50352
- var upgradeResolutionsSchema = zod_default.record(
50353
- zod_default.string(),
50354
- zod_default.union([driftResolutionSchema, modificationResolutionSchema])
50355
- );
50356
50098
  var KNOWLEDGE_UPGRADE_CHANGES = ["added", "changed", "removed", "drifted"];
50357
50099
  var knowledgeUpgradeEntrySchema = zod_default.object({
50358
50100
  alias: zod_default.string(),
@@ -50381,15 +50123,15 @@ function validKnowledgeResolutions(change) {
50381
50123
  return [];
50382
50124
  }
50383
50125
  }
50126
+ var upgradeResolutionsSchema = zod_default.record(
50127
+ zod_default.string(),
50128
+ zod_default.union([driftResolutionSchema, modificationResolutionSchema, knowledgeResolutionSchema])
50129
+ );
50384
50130
  var knowledgeBindConsentSchema = zod_default.record(zod_default.string(), zod_default.string());
50385
50131
  var knowledgeUpgradeResolutionsSchema = zod_default.object({
50386
50132
  resolutions: zod_default.record(zod_default.string(), knowledgeResolutionSchema).default({}),
50387
50133
  bind_to: knowledgeBindConsentSchema.default({})
50388
50134
  });
50389
- var contentUpgradeResolutionsSchema = zod_default.object({
50390
- knowledge: knowledgeUpgradeResolutionsSchema.default({ resolutions: {}, bind_to: {} }),
50391
- templates: zod_default.record(zod_default.string(), modificationResolutionSchema).default({})
50392
- });
50393
50135
  var knowledgeBoundRefSchema = zod_default.object({
50394
50136
  alias: zod_default.string(),
50395
50137
  name: zod_default.string(),
@@ -50403,261 +50145,38 @@ var appInstallationConfigSchema = zod_default.record(
50403
50145
  );
50404
50146
  var appInstallationProvenanceSchema = zod_default.enum(["install", "adopt"]);
50405
50147
 
50406
- // src/generate_package_fields.ts
50407
- var HEADER5 = `// Auto-generated by 'lotics package new/extract/dev/sync' from contract.json.
50408
- // DO NOT EDIT \u2014 regenerated whenever the contract changes.
50409
- //
50410
- // Package apps resolve F/OPT/ROLE at MODULE LOAD from the installation's
50411
- // binding (contract alias \u2192 THIS workspace's concrete id) via the SDK's
50412
- // \`binding\` RPC. Top-level await: the module graph waits for the binding
50413
- // before any importer evaluates, so every entry is a plain string.
50414
- import { getAppBinding } from "@lotics/app-sdk";
50415
-
50416
- const binding = await getAppBinding();
50417
-
50418
- function bound(map: Record<string, string>, key: string, kind: string): string {
50419
- const id = map[key];
50420
- if (id === undefined) {
50421
- throw new Error(
50422
- \`app_fields: \${kind} "\${key}" is not in this installation's binding \u2014 \` +
50423
- \`the generated app_fields.ts is stale relative to the installed contract version.\`,
50424
- );
50425
- }
50426
- return id;
50427
- }
50428
- `;
50429
- function generatePackageAppFields(contract) {
50430
- const entities = contract.entities ?? [];
50431
- const roles = contract.roles ?? [];
50432
- const fieldBlocks = [];
50433
- for (const entity of entities) {
50434
- const lines = (entity.fields ?? []).map(
50435
- (field) => ` ${propKey(field.alias)}: bound(binding.fields, ${JSON.stringify(`${entity.alias}.${field.alias}`)}, "field"),`
50436
- );
50437
- if (lines.length === 0) continue;
50438
- fieldBlocks.push(` ${propKey(entity.alias.toUpperCase())}: {
50439
- ${lines.join("\n")}
50440
- },`);
50441
- }
50442
- const optionBlocks = [];
50443
- for (const entity of entities) {
50444
- const perField = [];
50445
- for (const field of entity.fields ?? []) {
50446
- const options = field.options ?? [];
50447
- if (options.length === 0) continue;
50448
- const lines = options.map(
50449
- (option) => ` ${propKey(option.alias)}: bound(binding.options, ${JSON.stringify(`${entity.alias}.${field.alias}:${option.alias}`)}, "option"),`
50450
- );
50451
- perField.push(` ${propKey(field.alias)}: {
50452
- ${lines.join("\n")}
50453
- },`);
50454
- }
50455
- if (perField.length > 0) {
50456
- optionBlocks.push(` ${propKey(entity.alias.toUpperCase())}: {
50457
- ${perField.join("\n")}
50458
- },`);
50459
- }
50460
- }
50461
- const fMap = fieldBlocks.length > 0 ? `export const F = {
50462
- ${fieldBlocks.join("\n")}
50463
- } as const;` : `export const F = {} as const;`;
50464
- const optMap = optionBlocks.length > 0 ? `export const OPT = {
50465
- ${optionBlocks.join("\n")}
50466
- } as const;` : `export const OPT = {} as const;`;
50467
- const roleMap = roles.length > 0 ? `export const ROLE = {
50468
- ${roles.map((role) => ` ${propKey(role.alias)}: bound(binding.roles, ${JSON.stringify(role.alias)}, "role"),`).join("\n")}
50469
- } as const;` : `export const ROLE = {} as const;`;
50470
- return `${HEADER5}
50471
- ${fMap}
50472
-
50473
- ${optMap}
50474
-
50475
- ${roleMap}
50476
-
50477
- /** Field-id alias map: \`F[<ENTITY>][<field>]\` is this installation's \`fld_\u2026\` id. */
50478
- export type AppFields = typeof F;
50479
- /** Select-option alias map: \`OPT[<ENTITY>][<field>][<option>]\` is this installation's \`opt_\u2026\` id. */
50480
- export type AppOptions = typeof OPT;
50481
- /** Role alias map: \`ROLE[<role>]\` is this installation's \`grp_\u2026\` id. */
50482
- export type AppRoles = typeof ROLE;
50483
- `;
50484
- }
50485
-
50486
- // src/file_command_io.ts
50487
- import fs5 from "node:fs";
50488
- import path6 from "node:path";
50489
- var CliError = class extends Error {
50490
- constructor(message) {
50491
- super(message);
50492
- this.name = "CliError";
50493
- }
50494
- };
50495
- function fail(message) {
50496
- throw new CliError(message);
50497
- }
50498
- function writeFileAtomic(filePath, bytes) {
50499
- const dir = path6.dirname(path6.resolve(filePath));
50500
- const tmp = path6.join(dir, `.${path6.basename(filePath)}.${process.pid}.${Date.now()}.tmp`);
50501
- fs5.writeFileSync(tmp, bytes);
50502
- try {
50503
- fs5.renameSync(tmp, filePath);
50504
- } catch (error51) {
50505
- try {
50506
- fs5.unlinkSync(tmp);
50507
- } catch {
50508
- }
50509
- throw error51;
50510
- }
50511
- console.error(`Wrote ${filePath}`);
50512
- }
50513
-
50514
50148
  // src/package_commands.ts
50515
- var CONTRACT_FILE = "contract.json";
50516
- var ADOPT_BINDING_FILE = "adopt_binding.json";
50517
- var PACKAGE_TEMPLATE_KINDS = /* @__PURE__ */ new Set([
50518
- "html",
50519
- "email",
50520
- "excel",
50521
- "word",
50522
- "pdf-form"
50523
- ]);
50524
50149
  function packageJsonPath(projectDir) {
50525
- return path7.join(projectDir, "package.json");
50150
+ return path6.join(projectDir, "package.json");
50526
50151
  }
50527
50152
  function isPlainObject2(value) {
50528
50153
  return typeof value === "object" && value !== null && !Array.isArray(value);
50529
50154
  }
50530
- function readPackageProject(projectDir) {
50155
+ function readLocalAppManifest(projectDir) {
50531
50156
  const pkgPath2 = packageJsonPath(projectDir);
50532
- if (!fs6.existsSync(pkgPath2)) {
50533
- throw new Error(
50534
- `No package.json in ${projectDir}. Run this inside a package project (lotics package new <name>).`
50535
- );
50536
- }
50537
- const parsed = JSON.parse(fs6.readFileSync(pkgPath2, "utf-8"));
50538
- if (!isPlainObject2(parsed)) throw new Error(`Malformed package.json in ${projectDir}.`);
50539
- const lotics = parsed.lotics;
50540
- const pkg2 = isPlainObject2(lotics) ? lotics.package : void 0;
50541
- if (!isPlainObject2(pkg2) || typeof pkg2.name !== "string") {
50542
- throw new Error(
50543
- `${pkgPath2} has no lotics.package manifest \u2014 not a package project. Use "lotics package new <name>".`
50544
- );
50545
- }
50546
- const dev = {};
50547
- if (isPlainObject2(pkg2.dev)) {
50548
- for (const [ws, entry] of Object.entries(pkg2.dev)) {
50549
- if (isPlainObject2(entry) && typeof entry.app_id === "string" && typeof entry.version === "number") {
50550
- dev[ws] = { app_id: entry.app_id, version: entry.version };
50551
- }
50552
- }
50553
- }
50554
- const knowledge = {};
50555
- if (isPlainObject2(pkg2.knowledge)) {
50556
- for (const [alias, entry] of Object.entries(pkg2.knowledge)) {
50557
- if (!isPlainObject2(entry) || typeof entry.name !== "string" || entry.name.length === 0) {
50558
- throw new Error(
50559
- `${pkgPath2}: lotics.package.knowledge.${alias} must be an object with a non-empty "name".`
50560
- );
50561
- }
50562
- knowledge[alias] = {
50563
- name: entry.name,
50564
- description: typeof entry.description === "string" ? entry.description : null,
50565
- active_by_default: typeof entry.active_by_default === "boolean" ? entry.active_by_default : true
50566
- };
50567
- }
50568
- }
50569
- const templates = {};
50570
- if (isPlainObject2(pkg2.templates)) {
50571
- for (const [alias, entry] of Object.entries(pkg2.templates)) {
50572
- if (!isPlainObject2(entry) || typeof entry.name !== "string" || entry.name.length === 0 || typeof entry.type !== "string" || !PACKAGE_TEMPLATE_KINDS.has(entry.type) || typeof entry.file !== "string" || entry.file.length === 0) {
50573
- throw new Error(
50574
- `${pkgPath2}: lotics.package.templates.${alias} must be an object with a non-empty "name", a "type" of html|email|excel|word|pdf-form, and a non-empty "file" (a name in templates/).`
50575
- );
50157
+ if (!fs5.existsSync(pkgPath2)) return null;
50158
+ const parsed = JSON.parse(fs5.readFileSync(pkgPath2, "utf-8"));
50159
+ if (!isPlainObject2(parsed)) return null;
50160
+ const lotics = isPlainObject2(parsed.lotics) ? parsed.lotics : {};
50161
+ const app_id = typeof lotics.app_id === "string" ? lotics.app_id : null;
50162
+ const knowledge = [];
50163
+ if (Array.isArray(lotics.knowledge)) {
50164
+ for (const entry of lotics.knowledge) {
50165
+ if (isPlainObject2(entry) && typeof entry.alias === "string" && typeof entry.doc_id === "string") {
50166
+ knowledge.push({ alias: entry.alias, doc_id: entry.doc_id });
50576
50167
  }
50577
- templates[alias] = {
50578
- name: entry.name,
50579
- type: entry.type,
50580
- file: entry.file
50581
- };
50582
50168
  }
50583
50169
  }
50584
- const knowledge_expects = Array.isArray(pkg2.knowledge_expects) ? pkg2.knowledge_expects.filter((v) => typeof v === "string") : [];
50585
- const manifest = {
50586
- id: typeof pkg2.id === "string" ? pkg2.id : null,
50587
- name: pkg2.name,
50588
- description: typeof pkg2.description === "string" ? pkg2.description : null,
50589
- kind: pkg2.kind === "content" ? "content" : "app",
50590
- version: typeof pkg2.version === "number" ? pkg2.version : null,
50591
- knowledge,
50592
- templates,
50593
- knowledge_expects,
50594
- dev
50595
- };
50596
- return { pkgJson: parsed, manifest };
50597
- }
50598
- function writePackageManifest(projectDir, project) {
50599
- const next = {
50600
- ...project.pkgJson,
50601
- lotics: {
50602
- ...isPlainObject2(project.pkgJson.lotics) ? project.pkgJson.lotics : {},
50603
- package: project.manifest
50604
- }
50605
- };
50606
- writeFileAtomic(
50607
- packageJsonPath(projectDir),
50608
- new TextEncoder().encode(JSON.stringify(next, null, 2) + "\n")
50609
- );
50610
- }
50611
- function sanitizePackageJsonForSource(pkgJson) {
50612
- const lotics = isPlainObject2(pkgJson.lotics) ? pkgJson.lotics : {};
50613
- const pkg2 = isPlainObject2(lotics.package) ? lotics.package : {};
50614
- const { dev: _dev, ...pkgWithoutDev } = pkg2;
50615
- return {
50616
- ...pkgJson,
50617
- lotics: { ...lotics, package: pkgWithoutDev }
50618
- };
50170
+ return { app_id, knowledge };
50619
50171
  }
50620
- function draftPackageProjectFromApp(appPkgJson, args) {
50621
- const { lotics: _appManifest, ...rest } = appPkgJson;
50622
- return {
50623
- pkgJson: rest,
50624
- manifest: {
50625
- id: null,
50626
- name: args.name,
50627
- description: args.description,
50628
- // Extraction promotes a bespoke APP to a package — always app kind.
50629
- kind: "app",
50630
- version: null,
50631
- knowledge: {},
50632
- templates: {},
50633
- knowledge_expects: [],
50634
- dev: {}
50635
- }
50636
- };
50637
- }
50638
- function parseAdoptBindingFile(raw, expectedAppId) {
50639
- if (!isPlainObject2(raw) || typeof raw.app_id !== "string" || typeof raw.workspace_id !== "string" || !isPlainObject2(raw.binding)) {
50640
- throw new Error(
50641
- "Malformed .lotics/adopt_binding.json \u2014 expected { app_id, workspace_id, binding }."
50642
- );
50643
- }
50644
- if (raw.app_id !== expectedAppId) {
50645
- throw new Error(
50646
- `.lotics/adopt_binding.json records app ${raw.app_id}, but you are adopting ${expectedAppId}. A binding maps one app's concrete ids and must never be applied to another \u2014 run "lotics package extract ${expectedAppId}" to produce the right pin.`
50647
- );
50648
- }
50649
- const knowledge_binding = {};
50650
- if (isPlainObject2(raw.knowledge_binding)) {
50651
- for (const [alias, id] of Object.entries(raw.knowledge_binding)) {
50652
- if (typeof id === "string") knowledge_binding[alias] = id;
50172
+ function parseRenameFlags(renames) {
50173
+ return renames.map((entry) => {
50174
+ const eq = entry.indexOf("=");
50175
+ if (eq <= 0 || eq === entry.length - 1) {
50176
+ throw new Error(`Invalid --rename "${entry}" \u2014 expected old=new (an alias to rename before v1 freezes it).`);
50653
50177
  }
50654
- }
50655
- return {
50656
- app_id: raw.app_id,
50657
- workspace_id: raw.workspace_id,
50658
- binding: raw.binding,
50659
- knowledge_binding
50660
- };
50178
+ return { from: entry.slice(0, eq), to: entry.slice(eq + 1) };
50179
+ });
50661
50180
  }
50662
50181
  function formatExtractReport(report) {
50663
50182
  const order = ["error", "warning", "info"];
@@ -50666,573 +50185,32 @@ function formatExtractReport(report) {
50666
50185
  );
50667
50186
  return { lines, hasError: report.some((f) => f.severity === "error") };
50668
50187
  }
50669
- function dotLoticsDirEnsured(projectDir) {
50670
- const dir = path7.join(projectDir, ".lotics");
50671
- fs6.mkdirSync(dir, { recursive: true });
50672
- return dir;
50673
- }
50674
- function cmpVersions(a, b) {
50675
- const pa = a.split(".").map(Number);
50676
- const pb = b.split(".").map(Number);
50677
- for (let i2 = 0; i2 < 3; i2++) {
50678
- const d = (pa[i2] || 0) - (pb[i2] || 0);
50679
- if (d !== 0) return d;
50680
- }
50681
- return 0;
50682
- }
50683
- function packageSdkRange(sdkLatest) {
50684
- const version2 = sdkLatest !== null && cmpVersions(sdkLatest, STARTER_FALLBACK_SDK_VERSION) > 0 ? sdkLatest : STARTER_FALLBACK_SDK_VERSION;
50685
- return `^${version2}`;
50686
- }
50687
- function readContract(projectDir) {
50688
- const contractPath = path7.join(projectDir, CONTRACT_FILE);
50689
- if (!fs6.existsSync(contractPath)) {
50690
- throw new Error(
50691
- `No ${CONTRACT_FILE} in ${projectDir}. A package project declares its data model there.`
50692
- );
50693
- }
50694
- return JSON.parse(fs6.readFileSync(contractPath, "utf-8"));
50695
- }
50696
- function foldKnowledgeIntoContract(projectDir, project, contract) {
50697
- const aliases = Object.keys(project.manifest.knowledge);
50698
- const expects = project.manifest.knowledge_expects;
50699
- if (aliases.length === 0 && expects.length === 0) return contract;
50700
- const knowledge = {};
50701
- for (const alias of aliases) {
50702
- const entry = project.manifest.knowledge[alias];
50703
- const contentRef = `knowledge/${alias}.md`;
50704
- const filePath = path7.join(projectDir, "knowledge", `${alias}.md`);
50705
- if (!fs6.existsSync(filePath)) {
50706
- throw new Error(
50707
- `Declared knowledge doc "${alias}" (lotics.package.knowledge) has no ${contentRef} in the project. Create the file, or remove the manifest entry.`
50708
- );
50709
- }
50710
- const bytes = fs6.readFileSync(filePath);
50711
- knowledge[alias] = {
50712
- name: entry.name,
50713
- description: entry.description ?? "",
50714
- content_ref: contentRef,
50715
- content_sha256: createHash("sha256").update(bytes).digest("hex"),
50716
- active_by_default: entry.active_by_default
50717
- };
50718
- }
50719
- return { ...contract, knowledge, knowledge_expects: expects };
50720
- }
50721
- function foldTemplatesIntoContract(projectDir, project, contract) {
50722
- if (!isPlainObject2(contract)) {
50723
- throw new Error(`${CONTRACT_FILE} in ${projectDir} must be a JSON object.`);
50724
- }
50725
- const aliases = Object.keys(project.manifest.templates);
50726
- if (aliases.length === 0) return contract;
50727
- const existing = Array.isArray(contract.templates) ? contract.templates : [];
50728
- const existingAliases = new Set(
50729
- existing.filter((t) => isPlainObject2(t)).map((t) => t.alias).filter((a) => typeof a === "string")
50730
- );
50731
- const folded = aliases.map((alias) => {
50732
- if (existingAliases.has(alias)) {
50733
- throw new Error(
50734
- `Template alias "${alias}" is declared in both lotics.package.templates and ${CONTRACT_FILE} \u2014 declare each template once.`
50735
- );
50736
- }
50737
- const entry = project.manifest.templates[alias];
50738
- const filePath = path7.join(projectDir, "templates", entry.file);
50739
- if (!fs6.existsSync(filePath)) {
50740
- throw new Error(
50741
- `Declared template "${alias}" (lotics.package.templates) has no templates/${entry.file} in the project. Stage the file, then rebuild.`
50742
- );
50743
- }
50744
- if (entry.type === "html" || entry.type === "email") {
50745
- return { alias, label: entry.name, type: entry.type, content: fs6.readFileSync(filePath, "utf-8") };
50746
- }
50747
- return { alias, label: entry.name, type: entry.type, bytes_ref: `templates/${entry.file}` };
50748
- });
50749
- return { ...contract, templates: [...existing, ...folded] };
50750
- }
50751
- function foldTemplateShasIntoContract(projectDir, contract) {
50752
- const templates = contract.templates;
50753
- if (!Array.isArray(templates) || templates.length === 0) return contract;
50754
- const withShas = templates.map((template) => {
50755
- if (!isPlainObject2(template) || typeof template.type !== "string" || typeof template.alias !== "string") {
50756
- return template;
50757
- }
50758
- if (template.type === "html" || template.type === "email") {
50759
- if (typeof template.content !== "string") {
50760
- throw new Error(`Inline template "${template.alias}" must carry a string "content".`);
50761
- }
50762
- return {
50763
- ...template,
50764
- content_sha256: createHash("sha256").update(Buffer.from(template.content, "utf-8")).digest("hex")
50765
- };
50766
- }
50767
- if (typeof template.bytes_ref !== "string") {
50768
- throw new Error(`File-backed template "${template.alias}" must carry a string "bytes_ref".`);
50769
- }
50770
- const filePath = path7.join(projectDir, template.bytes_ref);
50771
- if (!fs6.existsSync(filePath)) {
50772
- throw new Error(
50773
- `Template "${template.alias}" references "${template.bytes_ref}", which is not in the project. Stage the file, then rebuild.`
50774
- );
50775
- }
50776
- const bytes = fs6.readFileSync(filePath);
50777
- return { ...template, content_sha256: createHash("sha256").update(bytes).digest("hex") };
50778
- });
50779
- return { ...contract, templates: withShas };
50780
- }
50781
- function writePackageAppFields(projectDir) {
50782
- const contract = readContract(projectDir);
50783
- const dotLotics = path7.join(projectDir, ".lotics");
50784
- fs6.mkdirSync(dotLotics, { recursive: true });
50785
- fs6.writeFileSync(path7.join(dotLotics, "app_fields.ts"), generatePackageAppFields(contract));
50786
- console.error("Wrote .lotics/app_fields.ts (contract-derived, runtime-resolved)");
50787
- }
50788
- var SOURCE_STAGE_EXCLUDES = /* @__PURE__ */ new Set([
50789
- "node_modules",
50790
- "dist",
50791
- ".lotics",
50792
- ".git",
50793
- "bundle.tar.gz",
50794
- "package.json"
50795
- ]);
50796
- function stagePackageSource(projectDir, sourceStage) {
50797
- const project = readPackageProject(projectDir);
50798
- fs6.mkdirSync(sourceStage, { recursive: true });
50799
- for (const entry of fs6.readdirSync(projectDir)) {
50800
- if (SOURCE_STAGE_EXCLUDES.has(entry) || entry.endsWith(".tsbuildinfo")) continue;
50801
- fs6.cpSync(path7.join(projectDir, entry), path7.join(sourceStage, entry), { recursive: true });
50802
- }
50803
- fs6.writeFileSync(
50804
- path7.join(sourceStage, "package.json"),
50805
- JSON.stringify(sanitizePackageJsonForSource(project.pkgJson), null, 2) + "\n"
50806
- );
50807
- }
50808
- async function buildPackageBundle(projectDir) {
50809
- const { manifest } = readPackageProject(projectDir);
50810
- const stage = fs6.mkdtempSync(path7.join(tmpdir2(), "lotics-pkg-"));
50811
- const bundlePath = path7.join(tmpdir2(), `lotics-bundle-${Date.now()}.tar.gz`);
50812
- try {
50813
- let distDir;
50814
- if (manifest.kind === "content") {
50815
- console.error("Content package \u2014 skipping app build.");
50816
- distDir = path7.join(stage, "empty-dist");
50817
- fs6.mkdirSync(distDir, { recursive: true });
50818
- fs6.writeFileSync(
50819
- path7.join(distDir, ".content-package"),
50820
- "Content package \u2014 no frontend bundle. Content ships in source.tar.gz under knowledge/.\n"
50821
- );
50822
- } else {
50823
- console.error("Building...");
50824
- await runNpm(["run", "build"], projectDir);
50825
- distDir = path7.join(projectDir, "dist");
50826
- if (!fs6.existsSync(distDir)) {
50827
- throw new Error(
50828
- `Build did not produce a dist/ directory in ${projectDir}. Check that 'npm run build' is configured correctly.`
50829
- );
50830
- }
50831
- }
50832
- console.error("Packaging source...");
50833
- const sourceStage = path7.join(stage, "source");
50834
- stagePackageSource(projectDir, sourceStage);
50835
- await runTar(["-czf", path7.join(stage, "source.tar.gz"), "-C", sourceStage, "."], projectDir);
50836
- console.error("Packaging dist...");
50837
- await runTar(["-czf", path7.join(stage, "dist.tar.gz"), "-C", distDir, "."], projectDir);
50838
- console.error("Bundling...");
50839
- await runTar(["-czf", bundlePath, "source.tar.gz", "dist.tar.gz"], stage);
50840
- return fs6.readFileSync(bundlePath);
50841
- } finally {
50842
- fs6.rmSync(stage, { recursive: true, force: true });
50843
- if (fs6.existsSync(bundlePath)) fs6.unlinkSync(bundlePath);
50844
- }
50845
- }
50846
- function starterContract(name) {
50847
- return {
50848
- entities: [
50849
- {
50850
- alias: "item",
50851
- label: name,
50852
- description: `Records managed by the ${name} package.`,
50853
- fields: [
50854
- { alias: "name", label: "Name", type: "text", required: true },
50855
- { alias: "notes", label: "Notes", type: "text" },
50856
- {
50857
- alias: "status",
50858
- label: "Status",
50859
- type: "select",
50860
- options: [
50861
- { alias: "open", label: "Open", color: "blue" },
50862
- { alias: "done", label: "Done", color: "green" }
50863
- ]
50864
- }
50865
- ]
50866
- }
50867
- ],
50868
- roles: [],
50869
- templates: [],
50870
- queries: [
50871
- {
50872
- alias: "items",
50873
- ast: {
50874
- kind: "from_table",
50875
- from_entity: "item",
50876
- sort: [{ field_key: "name", order: "asc" }]
50877
- }
50878
- }
50879
- ],
50880
- workflows: [],
50881
- agents: [],
50882
- config: [
50883
- { alias: "heading", label: "List heading", type: "text", default: name }
50884
- ]
50885
- };
50886
- }
50887
- function emptyContract() {
50888
- return {
50889
- entities: [],
50890
- roles: [],
50891
- templates: [],
50892
- queries: [],
50893
- workflows: [],
50894
- agents: [],
50895
- config: []
50896
- };
50897
- }
50898
- function scaffoldContentPackage(name, targetPath) {
50899
- const exampleAlias = "overview";
50900
- const manifest = {
50901
- id: null,
50902
- name,
50903
- description: null,
50904
- kind: "content",
50905
- version: null,
50906
- knowledge: {
50907
- [exampleAlias]: {
50908
- name: `${name} Overview`,
50909
- description: "What this corpus covers \u2014 replace with your doc's summary.",
50910
- active_by_default: true
50911
- }
50912
- },
50913
- // A content package can also ship standalone document templates — declare them
50914
- // under `lotics.package.templates` with the files in a `templates/` dir. See
50915
- // the commented example printed by the scaffold + docs/packages.md § Content.
50916
- templates: {},
50917
- knowledge_expects: [],
50918
- dev: {}
50919
- };
50920
- const pkgJson = {
50921
- name: appDirName(name),
50922
- private: true,
50923
- version: "0.0.0",
50924
- lotics: { package: manifest }
50925
- };
50926
- fs6.writeFileSync(
50927
- packageJsonPath(targetPath),
50928
- JSON.stringify(pkgJson, null, 2) + "\n"
50929
- );
50930
- fs6.writeFileSync(
50931
- path7.join(targetPath, CONTRACT_FILE),
50932
- JSON.stringify(emptyContract(), null, 2) + "\n"
50933
- );
50934
- const knowledgeDir = path7.join(targetPath, "knowledge");
50935
- fs6.mkdirSync(knowledgeDir, { recursive: true });
50936
- fs6.writeFileSync(
50937
- path7.join(knowledgeDir, `${exampleAlias}.md`),
50938
- `# ${name}
50939
-
50940
- Replace this file with the corpus content. Structure it under clear
50941
- Markdown headers so an agent can outline and read it by section.
50942
- `
50943
- );
50944
- console.error(`Scaffolded a content package into ${targetPath}`);
50945
- console.error(`
50946
- Ready. Next steps:`);
50947
- console.error(` cd ${path7.relative(process.cwd(), targetPath) || "."}`);
50948
- console.error(` # add docs: create knowledge/<alias>.md + a lotics.package.knowledge.<alias> entry`);
50949
- console.error(` # (name/description/active_by_default), then publish:`);
50950
- console.error(` # add templates: create templates/<file> + a lotics.package.templates.<alias> entry:`);
50951
- console.error(` # "templates": { "quote": { "name": "Quote", "type": "html", "file": "quote.html" } }`);
50952
- console.error(` # inline kinds html|email read templates/<file> as content; file-backed`);
50953
- console.error(` # excel|word|pdf-form pack the bytes into the bundle (bytes_ref = templates/<file>).`);
50954
- console.error(` lotics package publish -m "v1"`);
50955
- console.error(` lotics package install <package_id>`);
50956
- }
50957
- async function packageNew(args) {
50958
- const targetPath = path7.resolve(args.targetPath ?? appDirName(args.name));
50959
- if (fs6.existsSync(targetPath) && fs6.readdirSync(targetPath).length > 0) {
50960
- throw new Error(`Target directory ${targetPath} is not empty.`);
50961
- }
50962
- fs6.mkdirSync(targetPath, { recursive: true });
50963
- if ((args.kind ?? "app") === "content") {
50964
- scaffoldContentPackage(args.name, targetPath);
50965
- return;
50966
- }
50967
- const [uiLatest, sdkLatest] = await Promise.all([
50968
- fetchLatestNpmVersion("@lotics/ui"),
50969
- fetchLatestNpmVersion("@lotics/app-sdk")
50970
- ]);
50971
- const files = buildStarterTemplate({
50972
- app_name: args.name,
50973
- app_id: "",
50974
- workspace_id: "",
50975
- ui_version: uiLatest ? `^${uiLatest}` : void 0,
50976
- sdk_version: packageSdkRange(sdkLatest)
50977
- });
50978
- const overrides = new Map(
50979
- buildPackageStarterOverrides({ app_name: args.name }).map((f) => [f.path, f.content])
50980
- );
50981
- for (const file2 of files) {
50982
- const fullPath = path7.join(targetPath, file2.path);
50983
- if (file2.path === "package.json") {
50984
- const pkg2 = JSON.parse(file2.content);
50985
- const manifest = {
50986
- id: null,
50987
- name: args.name,
50988
- description: null,
50989
- kind: "app",
50990
- version: null,
50991
- knowledge: {},
50992
- templates: {},
50993
- knowledge_expects: [],
50994
- dev: {}
50995
- };
50996
- pkg2.lotics = { package: manifest };
50997
- fs6.mkdirSync(path7.dirname(fullPath), { recursive: true });
50998
- fs6.writeFileSync(fullPath, JSON.stringify(pkg2, null, 2) + "\n");
50999
- continue;
51000
- }
51001
- fs6.mkdirSync(path7.dirname(fullPath), { recursive: true });
51002
- fs6.writeFileSync(fullPath, overrides.get(file2.path) ?? file2.content);
51003
- }
51004
- fs6.writeFileSync(
51005
- path7.join(targetPath, CONTRACT_FILE),
51006
- JSON.stringify(starterContract(args.name), null, 2) + "\n"
51007
- );
51008
- writePackageAppFields(targetPath);
51009
- console.error(`Scaffolded ${files.length + 1} files into ${targetPath}`);
51010
- console.error("Installing npm dependencies...");
51011
- await runNpm(["install"], targetPath);
51012
- console.error(`
51013
- Ready. Next steps:`);
51014
- console.error(` cd ${path7.relative(process.cwd(), targetPath) || "."}`);
51015
- console.error(` # edit contract.json + src/App.tsx, then run it against a dev workspace:`);
51016
- console.error(` lotics workspace create "<name> dev" --dev`);
51017
- console.error(` lotics package dev --workspace <dev_ws>`);
51018
- }
51019
- async function packageBuild(args) {
51020
- const projectDir = path7.resolve(args.projectDir ?? process.cwd());
51021
- readPackageProject(projectDir);
51022
- const bundle = await buildPackageBundle(projectDir);
51023
- const out = path7.join(projectDir, "bundle.tar.gz");
51024
- fs6.writeFileSync(out, bundle);
51025
- console.error(`Built ${out} (${(bundle.byteLength / 1024).toFixed(1)} KB)`);
51026
- }
51027
- async function publishVersion(client, projectDir, opts) {
51028
- const project = readPackageProject(projectDir);
51029
- const rawContract = readContract(projectDir);
51030
- if (!isPlainObject2(rawContract)) {
51031
- throw new Error(`${CONTRACT_FILE} in ${projectDir} must be a JSON object.`);
51032
- }
51033
- const contract = foldTemplateShasIntoContract(
51034
- projectDir,
51035
- foldKnowledgeIntoContract(
51036
- projectDir,
51037
- project,
51038
- foldTemplatesIntoContract(projectDir, project, rawContract)
51039
- )
51040
- );
51041
- let packageId = project.manifest.id;
51042
- if (packageId === null) {
51043
- const pkg2 = await client.createPackage({
51044
- name: project.manifest.name,
51045
- description: project.manifest.description,
51046
- // Kind is fixed at package creation and immutable after — a knowledge
51047
- // package must be created as such so publish enforces its constraint.
51048
- kind: project.manifest.kind
51049
- });
51050
- packageId = pkg2.id;
51051
- project.manifest.id = packageId;
51052
- writePackageManifest(projectDir, project);
51053
- console.error(`Created package ${pkg2.name} (${pkg2.id}).`);
51054
- }
51055
- const bundle = await buildPackageBundle(projectDir);
51056
- console.error("Publishing version...");
51057
- const version2 = await client.publishPackageVersion(packageId, {
51058
- contract,
51059
- bundle,
51060
- changelog: opts.changelog ?? null,
51061
- channel: opts.channel ?? "release"
51062
- });
51063
- project.manifest.version = version2.version;
51064
- writePackageManifest(projectDir, project);
51065
- return { package_id: packageId, version: version2.version };
51066
- }
51067
- async function packagePublish(client, args) {
51068
- const projectDir = path7.resolve(args.projectDir ?? process.cwd());
51069
- const { package_id, version: version2 } = await publishVersion(client, projectDir, {
51070
- changelog: args.changelog
51071
- });
51072
- console.error(`Published ${package_id} v${version2}.`);
51073
- console.error(` Install it: lotics package install ${package_id} --version ${version2}`);
51074
- }
51075
- function assertDevWorkspace(workspace) {
51076
- if (workspace.is_dev === true) return;
51077
- throw new Error(
51078
- `Workspace "${workspace.name}" (${workspace.id}) is not a dev workspace. "lotics package dev"/"sync" scaffold package tables into the target workspace, so it must be a throwaway dev workspace. Create one with "lotics workspace create <name> --dev" and pass --workspace <dev_ws>.`
51079
- );
51080
- }
51081
- async function syncToDevWorkspace(client, projectDir) {
51082
- const devWorkspaceId = client.getWorkspaceId();
51083
- if (!devWorkspaceId) {
51084
- throw new Error(
51085
- "No dev workspace selected. Pass --workspace <dev_ws> (a workspace created with --dev)."
51086
- );
51087
- }
51088
- const workspace = await client.getWorkspaceInfo(devWorkspaceId);
51089
- if (!workspace) {
51090
- throw new Error(
51091
- `Workspace ${devWorkspaceId} is not accessible with these credentials. Pass --workspace <dev_ws> (a workspace created with --dev).`
51092
- );
51093
- }
51094
- assertDevWorkspace(workspace);
51095
- writePackageAppFields(projectDir);
51096
- const { package_id, version: version2 } = await publishVersion(client, projectDir, {
51097
- changelog: "dev sync",
51098
- channel: "dev"
51099
- });
51100
- const project = readPackageProject(projectDir);
51101
- const existing = project.manifest.dev[devWorkspaceId];
51102
- let appId;
51103
- if (existing) {
51104
- console.error(`Upgrading dev installation ${existing.app_id} \u2192 v${version2}...`);
51105
- const app = await client.upgradePackage(existing.app_id, { version: version2 });
51106
- appId = app.id;
51107
- } else {
51108
- console.error(`Installing ${package_id} v${version2} into dev workspace ${devWorkspaceId}...`);
51109
- const result = await client.installPackage(package_id, { version: version2 });
51110
- if (result.kind !== "app") {
51111
- throw new Error(
51112
- `Package ${package_id} is a content package \u2014 "lotics package dev/sync" run an app installation. Install a content package with "lotics package install ${package_id}".`
51113
- );
51114
- }
51115
- appId = result.app.id;
51116
- }
51117
- project.manifest.dev[devWorkspaceId] = { app_id: appId, version: version2 };
51118
- writePackageManifest(projectDir, project);
51119
- const installed = await client.getApp(appId);
51120
- writeAppDts(projectDir, {
51121
- workflows: installed.workflows ?? void 0,
51122
- queries: installed.queries ?? void 0,
51123
- agents: installed.agents ?? void 0
51124
- });
51125
- return { app_id: appId, app_name: installed.name, workspace_id: devWorkspaceId, version: version2 };
51126
- }
51127
- async function packageSync(client, args) {
51128
- const projectDir = path7.resolve(args.projectDir ?? process.cwd());
51129
- const result = await syncToDevWorkspace(client, projectDir);
51130
- console.error(
51131
- `Synced ${result.app_name} v${result.version} \u2192 ${result.app_id} (workspace ${result.workspace_id}).`
51132
- );
51133
- }
51134
- async function packageDev(client, args) {
51135
- const projectDir = path7.resolve(args.projectDir ?? process.cwd());
51136
- const synced = await syncToDevWorkspace(client, projectDir);
51137
- const handle = await startDevServer({
51138
- projectDir,
51139
- app_id: synced.app_id,
51140
- app_name: synced.app_name,
51141
- workspace_id: synced.workspace_id,
51142
- api_url: client.baseUrl,
51143
- port: args.port,
51144
- vitePort: args.vitePort,
51145
- client
51146
- });
51147
- await handle.ready;
51148
- const url2 = `http://localhost:${handle.port}`;
51149
- console.error(`
51150
- lotics package dev`);
51151
- console.error(` package: ${synced.app_name} (dev installation ${synced.app_id} v${synced.version})`);
51152
- console.error(` workspace: ${synced.workspace_id} (dev)`);
51153
- if (client.viewAsMemberId) {
51154
- console.error(` view as: ${client.viewAsMemberId}`);
51155
- }
51156
- console.error(` vite: http://localhost:${handle.vitePort}/`);
51157
- console.error(` open: ${url2}`);
51158
- console.error(` rpc: ${client.baseUrl} (via Bearer API key)
51159
- `);
51160
- console.error(` Edit contract.json then re-run "lotics package dev" / "lotics package sync" to migrate.`);
51161
- console.error(` Ctrl-C to stop.
51162
- `);
51163
- openBrowser(url2);
51164
- await new Promise((resolve2) => {
51165
- const onSig = () => {
51166
- process.off("SIGINT", onSig);
51167
- process.off("SIGTERM", onSig);
51168
- resolve2();
51169
- };
51170
- process.on("SIGINT", onSig);
51171
- process.on("SIGTERM", onSig);
51172
- });
51173
- console.error("\nStopping\u2026");
51174
- await handle.stop();
51175
- }
51176
- async function packageReset(client, args) {
51177
- const projectDir = path7.resolve(args.projectDir ?? process.cwd());
51178
- const devWorkspaceId = client.getWorkspaceId();
51179
- if (!devWorkspaceId) {
51180
- throw new Error(
51181
- "No dev workspace selected. Pass --workspace <dev_ws> (a workspace created with --dev)."
51182
- );
51183
- }
51184
- const { manifest } = readPackageProject(projectDir);
51185
- const pin = manifest.dev[devWorkspaceId];
51186
- if (!pin) {
51187
- throw new Error(
51188
- `No dev installation recorded for workspace ${devWorkspaceId}. Run "lotics package dev --workspace ${devWorkspaceId}" first.`
51189
- );
51190
- }
51191
- console.error(`Resetting dev installation ${pin.app_id} in workspace ${devWorkspaceId}...`);
51192
- const app = await client.resetPackage(pin.app_id);
51193
- console.error(`Reset ${app.name} \u2192 ${app.id}. The scaffolded tables were dropped and re-created clean.`);
51194
- }
51195
- function resolveInstallationAppId(client, explicit) {
50188
+ function resolveInstallationAppId(explicit) {
51196
50189
  if (explicit) return explicit;
51197
- const workspaceId = client.getWorkspaceId();
51198
- if (!workspaceId) {
51199
- throw new Error("Pass an app id (lotics package doctor <app_id>) or select a workspace.");
51200
- }
51201
- let manifest;
51202
- try {
51203
- ({ manifest } = readPackageProject(path7.resolve(process.cwd())));
51204
- } catch {
51205
- throw new Error(
51206
- "No app id given and the current directory is not a package project. Pass one explicitly: lotics package doctor <app_id>."
51207
- );
51208
- }
51209
- const pin = manifest.dev[workspaceId];
51210
- if (!pin) {
51211
- throw new Error(
51212
- `No app id given and no dev installation recorded for workspace ${workspaceId}. Pass one explicitly: lotics package doctor <app_id>.`
51213
- );
51214
- }
51215
- return pin.app_id;
50190
+ throw new Error("Pass an app id \u2014 e.g. lotics package doctor <app_id>.");
51216
50191
  }
50192
+ var RESOLVE_VERBS = /* @__PURE__ */ new Set(["recreate", "revert", "keep", "apply", "archive", "unbind"]);
51217
50193
  function parseResolveFlags(resolve2) {
51218
50194
  const resolutions = {};
51219
50195
  for (const entry of resolve2) {
51220
50196
  const eq = entry.indexOf("=");
51221
50197
  if (eq <= 0 || eq === entry.length - 1) {
51222
50198
  throw new Error(
51223
- `Invalid --resolve "${entry}" \u2014 expected <key>=recreate|revert|keep or <key>=<existing_id>.`
50199
+ `Invalid --resolve "${entry}" \u2014 expected <key>=<verb> (recreate|revert|keep|apply|archive|unbind) or <key>=<existing_id>.`
51224
50200
  );
51225
50201
  }
51226
50202
  const key = entry.slice(0, eq);
51227
50203
  const value = entry.slice(eq + 1);
51228
- resolutions[key] = value === "recreate" || value === "revert" || value === "keep" ? value : { bind_to: value };
50204
+ resolutions[key] = RESOLVE_VERBS.has(value) ? value : { bind_to: value };
51229
50205
  }
51230
50206
  return resolutions;
51231
50207
  }
51232
50208
  async function packageDoctor(client, args) {
51233
- const app_id = resolveInstallationAppId(client, args.app_id);
50209
+ const app_id = resolveInstallationAppId(args.app_id);
51234
50210
  const health = await client.getPackageHealth(app_id);
51235
- console.error(`${health.package_name} \u2014 installation ${health.app_id}`);
50211
+ console.error(
50212
+ `${health.package_name} \u2014 installation ${health.app_id}` + (health.is_origin ? " (origin \u2014 this app IS installation #1, the release working copy)" : "")
50213
+ );
51236
50214
  console.error(
51237
50215
  ` Installed: v${health.installed_version} Latest: v${health.latest_version}` + (health.update_available ? " \u2192 update available" : "")
51238
50216
  );
@@ -51245,12 +50223,23 @@ async function packageDoctor(client, args) {
51245
50223
  }
51246
50224
  console.error(
51247
50225
  ` Resolve while upgrading:
51248
- lotics package upgrade ${app_id} --resolve <namespace.alias>=recreate (or =<existing_id> to re-point)`
50226
+ lotics upgrade ${app_id} --resolve <namespace.alias>=recreate (or =<existing_id> to re-point)`
51249
50227
  );
51250
50228
  process.exitCode = 1;
51251
50229
  }
51252
50230
  if (health.modified.length === 0) {
51253
- console.error(" Package artifacts: pristine \u2014 no local edits an upgrade would revert.");
50231
+ console.error(
50232
+ health.is_origin ? ` Package artifacts: no changes since the last release.
50233
+ (schema additions aren't fingerprinted \u2014 preview them with: lotics app release ${app_id})` : " Package artifacts: pristine \u2014 no local edits an upgrade would revert."
50234
+ );
50235
+ } else if (health.is_origin) {
50236
+ console.error(
50237
+ ` Changed since v${health.installed_version} (${health.modified.length}) \u2014 a release will publish these:`
50238
+ );
50239
+ for (const m of health.modified) {
50240
+ console.error(` - ${m.kind}.${m.alias}`);
50241
+ }
50242
+ console.error(` Cut the next version: lotics app release ${app_id} -m "<what changed>"`);
51254
50243
  } else {
51255
50244
  console.error(` Locally modified package artifacts (${health.modified.length}):`);
51256
50245
  for (const m of health.modified) {
@@ -51258,7 +50247,7 @@ async function packageDoctor(client, args) {
51258
50247
  }
51259
50248
  console.error(
51260
50249
  ` Consent while upgrading:
51261
- lotics package upgrade ${app_id} --resolve <kind.alias>=revert (or =keep to retain the edit)`
50250
+ lotics upgrade ${app_id} --resolve <kind.alias>=revert (or =keep to retain the edit)`
51262
50251
  );
51263
50252
  process.exitCode = 1;
51264
50253
  }
@@ -51269,9 +50258,18 @@ async function packageDoctor(client, args) {
51269
50258
  }
51270
50259
  }
51271
50260
  if (health.knowledge_modified.length > 0) {
51272
- console.error(` Locally edited package knowledge (${health.knowledge_modified.length}):`);
51273
- for (const m of health.knowledge_modified) {
51274
- console.error(` - ${m.alias} "${m.name}" (an upgrade overwrites this unless kept)`);
50261
+ if (health.is_origin) {
50262
+ console.error(
50263
+ ` Knowledge changed since v${health.installed_version} (${health.knowledge_modified.length}) \u2014 a release will re-snapshot these:`
50264
+ );
50265
+ for (const m of health.knowledge_modified) {
50266
+ console.error(` - ${m.alias} "${m.name}"`);
50267
+ }
50268
+ } else {
50269
+ console.error(` Locally edited package knowledge (${health.knowledge_modified.length}):`);
50270
+ for (const m of health.knowledge_modified) {
50271
+ console.error(` - ${m.alias} "${m.name}" (an upgrade overwrites this unless kept)`);
50272
+ }
51275
50273
  }
51276
50274
  }
51277
50275
  if (health.missing_expected_docs.length > 0) {
@@ -51280,14 +50278,14 @@ async function packageDoctor(client, args) {
51280
50278
  console.error(` - "${name}" (the package's agents route to this name; no matching doc exists)`);
51281
50279
  }
51282
50280
  }
51283
- if (health.knowledge_drift.length > 0 || health.knowledge_modified.length > 0) {
50281
+ if (health.knowledge_drift.length > 0 || health.knowledge_modified.length > 0 && !health.is_origin) {
51284
50282
  process.exitCode = 1;
51285
50283
  }
51286
50284
  if (health.knowledge_drift.length === 0 && health.knowledge_modified.length === 0 && health.missing_expected_docs.length === 0) {
51287
50285
  console.error(" Knowledge: healthy \u2014 bound docs resolve, none locally edited, expects met.");
51288
50286
  }
51289
50287
  if (health.update_available) {
51290
- console.error(` Upgrade: lotics package upgrade ${app_id}`);
50288
+ console.error(` Upgrade: lotics upgrade ${app_id}`);
51291
50289
  }
51292
50290
  }
51293
50291
  async function packageUpgrade(client, args) {
@@ -51322,27 +50320,17 @@ async function packageUpgrade(client, args) {
51322
50320
  );
51323
50321
  process.exit(1);
51324
50322
  }
51325
- const coreKeys = /* @__PURE__ */ new Set([
51326
- ...preview.drift.map((d) => `${d.namespace}.${d.alias}`),
51327
- ...preview.modified.map((m) => `${m.kind}.${m.alias}`)
51328
- ]);
51329
- const { coreResolve, knowledgeResolutions } = routeAppUpgradeResolve(
51330
- args.resolve,
51331
- preview.knowledge,
51332
- coreKeys
51333
- );
51334
- const resolutions = parseResolveFlags(coreResolve);
50323
+ const resolutions = parseResolveFlags(args.resolve);
50324
+ for (const [alias, id] of Object.entries(parseBindToFlags(args.bindTo))) {
50325
+ resolutions[`knowledge.${alias}`] = { bind_to: id };
50326
+ }
51335
50327
  if (args.applyAll) {
51336
50328
  for (const entry of preview.knowledge) {
51337
- if (knowledgeEntryNeedsConsent(entry) && knowledgeResolutions[entry.alias] === void 0) {
51338
- knowledgeResolutions[entry.alias] = knowledgeAcceptResolution(entry.change);
50329
+ if (knowledgeEntryNeedsConsent(entry) && resolutions[`knowledge.${entry.alias}`] === void 0) {
50330
+ resolutions[`knowledge.${entry.alias}`] = knowledgeAcceptResolution(entry.change);
51339
50331
  }
51340
50332
  }
51341
50333
  }
51342
- const knowledge_resolutions = {
51343
- resolutions: knowledgeResolutions,
51344
- bind_to: parseBindToFlags(args.bindTo)
51345
- };
51346
50334
  const unresolvedDrift = preview.drift.filter(
51347
50335
  (d) => resolutions[`${d.namespace}.${d.alias}`] === void 0
51348
50336
  );
@@ -51350,7 +50338,7 @@ async function packageUpgrade(client, args) {
51350
50338
  (m) => resolutions[`${m.kind}.${m.alias}`] === void 0
51351
50339
  );
51352
50340
  const unresolvedKnowledge = preview.knowledge.filter(
51353
- (e) => knowledgeEntryNeedsConsent(e) && knowledgeResolutions[e.alias] === void 0
50341
+ (e) => knowledgeEntryNeedsConsent(e) && resolutions[`knowledge.${e.alias}`] === void 0
51354
50342
  );
51355
50343
  if (unresolvedDrift.length > 0 || unresolvedModified.length > 0 || unresolvedKnowledge.length > 0) {
51356
50344
  if (unresolvedDrift.length > 0) {
@@ -51379,11 +50367,9 @@ async function packageUpgrade(client, args) {
51379
50367
  }
51380
50368
  process.exit(1);
51381
50369
  }
51382
- const hasKnowledgeResolutions = Object.keys(knowledge_resolutions.resolutions).length > 0 || Object.keys(knowledge_resolutions.bind_to).length > 0;
51383
50370
  const app = await client.upgradePackage(args.app_id, {
51384
50371
  ...args.version !== void 0 ? { version: args.version } : {},
51385
- ...Object.keys(resolutions).length > 0 ? { resolutions } : {},
51386
- ...hasKnowledgeResolutions ? { knowledge_resolutions } : {}
50372
+ ...Object.keys(resolutions).length > 0 ? { resolutions } : {}
51387
50373
  });
51388
50374
  console.error(`Upgraded ${app.name} \u2192 v${app.package_version} (${app.id}).`);
51389
50375
  }
@@ -51433,82 +50419,61 @@ function formatKnowledgeEntryLine(entry) {
51433
50419
  }
51434
50420
  function formatKnowledgeResolveHint(entry) {
51435
50421
  const note = entry.change === "changed" || entry.change === "removed" ? " (a local edit \u2014 apply/archive overwrites it; keep retains it)" : " (bound doc is gone; recreate from the package, or unbind)";
51436
- return ` --resolve ${entry.alias}=${validKnowledgeResolutions(entry.change).join("|")}${note}`;
50422
+ return ` --resolve knowledge.${entry.alias}=${validKnowledgeResolutions(entry.change).join("|")}${note}`;
51437
50423
  }
51438
- function routeAppUpgradeResolve(resolve2, knowledgeEntries, coreKeys) {
51439
- const byAlias = new Map(knowledgeEntries.map((e) => [e.alias, e]));
51440
- const coreResolve = [];
51441
- const knowledgeResolutions = {};
51442
- for (const entry of resolve2) {
51443
- const eq = entry.indexOf("=");
51444
- if (eq <= 0 || eq === entry.length - 1) {
51445
- throw new Error(`Invalid --resolve "${entry}" \u2014 expected <key>=<value>.`);
51446
- }
51447
- const key = entry.slice(0, eq);
51448
- const value = entry.slice(eq + 1);
51449
- const known = byAlias.get(key);
51450
- if (known && coreKeys.has(key)) {
51451
- throw new Error(
51452
- `--resolve "${key}" is ambiguous \u2014 it names both a bundled knowledge doc and a core artifact in this upgrade. Rename one alias so it is unambiguous; refusing to guess.`
51453
- );
51454
- }
51455
- if (known) {
51456
- const valid = validKnowledgeResolutions(known.change);
51457
- const match = valid.find((v) => v === value);
51458
- if (match === void 0) {
51459
- throw new Error(
51460
- `Invalid --resolve value "${value}" for knowledge doc "${key}" (${known.change}) \u2014 expected ${valid.join("|")}.`
51461
- );
51462
- }
51463
- knowledgeResolutions[key] = match;
51464
- } else {
51465
- coreResolve.push(entry);
51466
- }
50424
+ function formatTemplateResolveHint(entry) {
50425
+ const note = entry.baseline_unknown ? " (can't verify the local edit \u2014 older package version; revert overwrites, keep retains)" : " (a local edit \u2014 revert overwrites it with the package's version; keep retains it)";
50426
+ return ` --resolve template.${entry.alias}=revert|keep${note}`;
50427
+ }
50428
+ async function resolveWorkspaceContentInstallation(client, package_id) {
50429
+ const workspaceId = client.getWorkspaceId();
50430
+ if (!workspaceId) {
50431
+ throw new Error(
50432
+ "No workspace selected. Pass --workspace <ws> (or select one) to operate on its content installation."
50433
+ );
51467
50434
  }
51468
- return { coreResolve, knowledgeResolutions };
50435
+ const installations = await client.listContentInstallations(workspaceId);
50436
+ const match = installations.find((inst) => inst.package_id === package_id);
50437
+ if (!match) {
50438
+ throw new Error(
50439
+ `Package ${package_id} is not installed in this workspace \u2014 install it first: lotics install ${package_id}`
50440
+ );
50441
+ }
50442
+ return match;
51469
50443
  }
51470
- function routeContentResolveFlags(resolve2, knowledgeAliases, templateAliases) {
51471
- const knowledge = {};
51472
- const templates = {};
51473
- const knowledgeValid = /* @__PURE__ */ new Set(["apply", "keep", "archive", "recreate", "unbind"]);
51474
- const templateValid = /* @__PURE__ */ new Set(["revert", "keep"]);
51475
- for (const entry of resolve2) {
51476
- const eq = entry.indexOf("=");
51477
- if (eq <= 0 || eq === entry.length - 1) {
51478
- throw new Error(`Invalid --resolve "${entry}" \u2014 expected <alias>=<value>.`);
51479
- }
51480
- const key = entry.slice(0, eq);
51481
- const value = entry.slice(eq + 1);
51482
- const isKnowledge = knowledgeAliases.has(key);
51483
- const isTemplate = templateAliases.has(key);
51484
- if (isKnowledge && isTemplate) {
51485
- throw new Error(
51486
- `--resolve "${key}" is ambiguous \u2014 it names both a knowledge doc and a template in this upgrade. Rename one alias in the package so the two content namespaces are disjoint.`
51487
- );
51488
- }
51489
- if (isTemplate) {
51490
- if (!templateValid.has(value)) {
51491
- throw new Error(`Invalid --resolve value "${value}" for template "${key}" \u2014 expected revert|keep.`);
51492
- }
51493
- templates[key] = value;
51494
- continue;
51495
- }
51496
- if (isKnowledge) {
51497
- if (!knowledgeValid.has(value)) {
51498
- throw new Error(
51499
- `Invalid --resolve value "${value}" for knowledge alias "${key}" \u2014 expected apply|keep|archive|recreate|unbind.`
51500
- );
51501
- }
51502
- knowledge[key] = value;
51503
- continue;
51504
- }
51505
- throw new Error(`--resolve "${key}" does not name a knowledge doc or template in this upgrade.`);
50444
+ async function redirectContentPciForm(client, pci_id, verb) {
50445
+ console.error(
50446
+ `Content installations are addressed by their PACKAGE id now \u2014 ${pci_id} is a server-internal resource id.`
50447
+ );
50448
+ const workspaceId = client.getWorkspaceId();
50449
+ const match = workspaceId ? (await client.listContentInstallations(workspaceId)).find((inst) => inst.id === pci_id) : void 0;
50450
+ if (match) {
50451
+ console.error(` Run: lotics ${verb} ${match.package_id}`);
50452
+ } else {
50453
+ console.error(" Find the package id: lotics package list-content");
50454
+ console.error(` Then run: lotics ${verb} <package_id>`);
51506
50455
  }
51507
- return { knowledge, templates };
50456
+ process.exit(1);
51508
50457
  }
51509
- function formatTemplateResolveHint(entry) {
51510
- const note = entry.baseline_unknown ? " (can't verify the local edit \u2014 older package version; revert overwrites, keep retains)" : " (a local edit \u2014 revert overwrites it with the package's version; keep retains it)";
51511
- return ` --resolve ${entry.alias}=revert|keep${note}`;
50458
+ async function packageUpgradeByPackageId(client, args) {
50459
+ const pkg2 = await client.getPackage(args.package_id);
50460
+ if (pkg2.kind === "content") {
50461
+ const installation = await resolveWorkspaceContentInstallation(client, args.package_id);
50462
+ await packageUpgradeKnowledge(client, {
50463
+ installation_id: installation.id,
50464
+ package_id: pkg2.id,
50465
+ package_name: pkg2.name,
50466
+ version: args.version,
50467
+ resolve: args.resolve,
50468
+ bind_to: parseBindToFlags(args.bindTo),
50469
+ applyAll: args.applyAll
50470
+ });
50471
+ return;
50472
+ }
50473
+ await packageFleetUpgrade(client, {
50474
+ package_id: args.package_id,
50475
+ ...args.version !== void 0 ? { version: args.version } : {}
50476
+ });
51512
50477
  }
51513
50478
  async function packageUpgradeKnowledge(client, args) {
51514
50479
  const preview = await client.previewContentInstallationUpgrade(args.installation_id, {
@@ -51522,7 +50487,7 @@ async function packageUpgradeKnowledge(client, args) {
51522
50487
  ...args.version !== void 0 ? { version: args.version } : {},
51523
50488
  resolutions: resolutions2
51524
50489
  });
51525
- console.error(`Upgraded ${args.installation_id} \u2192 v${updated.package_version}.`);
50490
+ console.error(`Upgraded ${args.package_name} (${args.package_id}) \u2192 v${updated.package_version}.`);
51526
50491
  };
51527
50492
  if (preview.entries.length === 0 && preview.templates.length === 0) {
51528
50493
  if (preview.to_version === preview.from_version) {
@@ -51532,7 +50497,7 @@ async function packageUpgradeKnowledge(client, args) {
51532
50497
  console.error(
51533
50498
  " No changes needing consent \u2014 advancing the version pin; clean template updates apply automatically."
51534
50499
  );
51535
- await apply({ knowledge: { resolutions: {}, bind_to: {} }, templates: {} });
50500
+ await apply({});
51536
50501
  return;
51537
50502
  }
51538
50503
  for (const entry of preview.entries) {
@@ -51541,30 +50506,27 @@ async function packageUpgradeKnowledge(client, args) {
51541
50506
  for (const entry of preview.templates) {
51542
50507
  console.error(` [template] ${entry.alias} (modified \u2014 needs consent)`);
51543
50508
  }
51544
- const routed = routeContentResolveFlags(
51545
- args.resolve,
51546
- new Set(preview.entries.map((e) => e.alias)),
51547
- new Set(preview.templates.map((t) => t.alias))
51548
- );
51549
- const resolutions = {
51550
- knowledge: { resolutions: { ...routed.knowledge }, bind_to: { ...args.bind_to } },
51551
- templates: { ...routed.templates }
51552
- };
50509
+ const resolutions = parseResolveFlags(args.resolve);
50510
+ for (const [alias, id] of Object.entries(args.bind_to)) {
50511
+ resolutions[`knowledge.${alias}`] = { bind_to: id };
50512
+ }
51553
50513
  if (args.applyAll) {
51554
50514
  for (const entry of preview.entries) {
51555
- if (knowledgeEntryNeedsConsent(entry) && resolutions.knowledge.resolutions[entry.alias] === void 0) {
51556
- resolutions.knowledge.resolutions[entry.alias] = knowledgeAcceptResolution(entry.change);
50515
+ if (knowledgeEntryNeedsConsent(entry) && resolutions[`knowledge.${entry.alias}`] === void 0) {
50516
+ resolutions[`knowledge.${entry.alias}`] = knowledgeAcceptResolution(entry.change);
51557
50517
  }
51558
50518
  }
51559
50519
  for (const entry of preview.templates) {
51560
- if (resolutions.templates[entry.alias] === void 0) resolutions.templates[entry.alias] = "revert";
50520
+ if (resolutions[`template.${entry.alias}`] === void 0) {
50521
+ resolutions[`template.${entry.alias}`] = "revert";
50522
+ }
51561
50523
  }
51562
50524
  }
51563
50525
  const unresolvedKnowledge = preview.entries.filter(
51564
- (entry) => knowledgeEntryNeedsConsent(entry) && resolutions.knowledge.resolutions[entry.alias] === void 0
50526
+ (entry) => knowledgeEntryNeedsConsent(entry) && resolutions[`knowledge.${entry.alias}`] === void 0
51565
50527
  );
51566
50528
  const unresolvedTemplates = preview.templates.filter(
51567
- (entry) => resolutions.templates[entry.alias] === void 0
50529
+ (entry) => resolutions[`template.${entry.alias}`] === void 0
51568
50530
  );
51569
50531
  if (unresolvedKnowledge.length > 0 || unresolvedTemplates.length > 0) {
51570
50532
  console.error(
@@ -51597,8 +50559,8 @@ function warnMissingExpectedDocs(missing) {
51597
50559
  }
51598
50560
  async function packageInstall(client, args) {
51599
50561
  const pkg2 = await client.getPackage(args.package_id);
51600
- const kindLabel = pkg2.kind === "content" ? "content package" : "package";
51601
- console.error(`Installing ${pkg2.name} \u2014 ${trustBadge(pkg2)} (${kindLabel})...`);
50562
+ const kindSuffix = pkg2.kind === "content" ? " (content \u2014 docs/templates, no app)" : "";
50563
+ console.error(`Installing ${pkg2.name} \u2014 ${trustBadge(pkg2)}${kindSuffix}...`);
51602
50564
  const result = await client.installPackage(args.package_id, {
51603
50565
  ...args.version !== void 0 ? { version: args.version } : {},
51604
50566
  ...args.bind_to && Object.keys(args.bind_to).length > 0 ? { bind_to: args.bind_to } : {},
@@ -51609,7 +50571,7 @@ async function packageInstall(client, args) {
51609
50571
  const docs = installation.binding.knowledge;
51610
50572
  const templates = installation.binding.templates;
51611
50573
  console.error(
51612
- `Installed ${pkg2.name} v${installation.package_version} \u2192 content installation ${installation.id} (workspace ${installation.workspace_id}).`
50574
+ `Installed ${pkg2.name} v${installation.package_version} (workspace ${installation.workspace_id}).`
51613
50575
  );
51614
50576
  const docAliases = Object.keys(docs);
51615
50577
  if (docAliases.length > 0) {
@@ -51624,8 +50586,8 @@ async function packageInstall(client, args) {
51624
50586
  );
51625
50587
  }
51626
50588
  warnMissingExpectedDocs(warnings.missing_expected_docs);
51627
- console.error(` Upgrade later: lotics package upgrade ${installation.id}`);
51628
- console.error(` Uninstall: lotics package uninstall ${installation.id} [--keep-content]`);
50589
+ console.error(` Upgrade later: lotics upgrade ${args.package_id}`);
50590
+ console.error(` Uninstall: lotics uninstall ${args.package_id} [--keep-content]`);
51629
50591
  return;
51630
50592
  }
51631
50593
  const { app, knowledge_warnings } = result;
@@ -51633,25 +50595,34 @@ async function packageInstall(client, args) {
51633
50595
  console.error(`Installed ${app.name} ${versionLabel} \u2192 ${app.id} (workspace ${app.workspace_id}).`);
51634
50596
  console.error(" The data model, queries, workflows, and agents are live.");
51635
50597
  warnMissingExpectedDocs(knowledge_warnings.missing_expected_docs);
51636
- console.error(" Pull it for local editing: lotics app pull " + app.id);
50598
+ console.error(` Pull it for local editing: lotics app pull ${app.id}`);
50599
+ console.error(` Upgrade later: lotics upgrade ${app.id}`);
50600
+ console.error(` Health / uninstall: lotics package doctor ${app.id} \xB7 lotics uninstall ${app.id} [--archive-tables]`);
51637
50601
  }
51638
50602
  async function packageUninstall(client, args) {
51639
- if (args.id.startsWith("pci_")) {
50603
+ if (args.id.startsWith("apg_")) {
51640
50604
  if (args.archive_tables) {
51641
50605
  throw new Error(
51642
- "--archive-tables applies only to an app installation (<app_id>). A content installation (pci_) has no scaffolded tables \u2014 use --keep-content to retain its docs/templates."
50606
+ "--archive-tables applies only to an app installation (<app_id>). A content package has no scaffolded tables \u2014 use --keep-content to retain its docs/templates."
50607
+ );
50608
+ }
50609
+ const pkg2 = await client.getPackage(args.id);
50610
+ if (pkg2.kind !== "content") {
50611
+ throw new Error(
50612
+ `Package ${args.id} is an app package \u2014 uninstall an app installation by its app id: lotics uninstall <app_id>.`
51643
50613
  );
51644
50614
  }
51645
- const result2 = await client.uninstallContentPackage(args.id, {
50615
+ const installation = await resolveWorkspaceContentInstallation(client, args.id);
50616
+ const result2 = await client.uninstallContentPackage(installation.id, {
51646
50617
  keep_content: args.keep_content
51647
50618
  });
51648
50619
  if (args.keep_content) {
51649
50620
  console.error(
51650
- `Uninstalled content installation ${result2.installation_id} \u2014 its docs and templates were kept as ordinary workspace content.`
50621
+ `Uninstalled ${pkg2.name} (${pkg2.id}) \u2014 its docs and templates were kept as ordinary workspace content.`
51651
50622
  );
51652
50623
  } else {
51653
50624
  console.error(
51654
- `Uninstalled content installation ${result2.installation_id} \u2014 archived ${result2.archived_doc_ids.length} doc(s) and ${result2.archived_template_ids.length} template(s).`
50625
+ `Uninstalled ${pkg2.name} (${pkg2.id}) \u2014 archived ${result2.archived_doc_ids.length} doc(s) and ${result2.archived_template_ids.length} template(s).`
51655
50626
  );
51656
50627
  for (const docId of result2.archived_doc_ids) console.error(` ${docId}`);
51657
50628
  for (const templateId of result2.archived_template_ids) console.error(` ${templateId}`);
@@ -51660,7 +50631,7 @@ async function packageUninstall(client, args) {
51660
50631
  }
51661
50632
  if (args.keep_content) {
51662
50633
  throw new Error(
51663
- "--keep-content applies only to a standalone content installation (pci_). An app installation (<app_id>) uses --archive-tables to also archive its scaffolded tables."
50634
+ "--keep-content applies only to a content package (apg_). An app installation (<app_id>) uses --archive-tables to also archive its scaffolded tables."
51664
50635
  );
51665
50636
  }
51666
50637
  const app = await client.getApp(args.id);
@@ -51702,10 +50673,9 @@ async function packageListContent(client) {
51702
50673
  const name = inst.package_registry?.name ?? "(unknown package)";
51703
50674
  const latest = inst.package_registry?.latest_version;
51704
50675
  const updateAvailable = inst.package_registry?.update_available ?? false;
51705
- const source = inst.app_id === null ? "standalone" : `app-bundled (${inst.app_id})`;
51706
50676
  const versionLabel = latest !== void 0 && latest !== inst.package_version ? `v${inst.package_version} \u2192 latest v${latest}` : `v${inst.package_version}`;
51707
50677
  console.error(
51708
- ` ${inst.id} ${name} ${versionLabel} ${source}` + (updateAvailable ? " \u2192 update available" : "")
50678
+ ` ${inst.package_id} ${name} ${versionLabel}` + (updateAvailable ? " \u2192 update available" : "")
51709
50679
  );
51710
50680
  }
51711
50681
  }
@@ -51781,200 +50751,143 @@ async function packageConfig(client, args) {
51781
50751
  console.log(` ${key} = ${JSON.stringify(config2[key])}${marker}`);
51782
50752
  }
51783
50753
  }
51784
- async function packageRetire(client, args) {
51785
- const pkg2 = await client.retirePackage(args.package_id, { undo: args.undo });
50754
+ async function appUnpublish(client, args) {
50755
+ let packageId = args.id;
50756
+ if (args.id.startsWith("app_")) {
50757
+ const app = await client.getApp(args.id);
50758
+ if (!app.package_id) {
50759
+ throw new Error(
50760
+ `App ${args.id} is not a package installation \u2014 it has no package to unpublish. Pass the package id directly.`
50761
+ );
50762
+ }
50763
+ packageId = app.package_id;
50764
+ }
50765
+ const pkg2 = await client.retirePackage(packageId, { undo: args.undo });
51786
50766
  if (pkg2.retired_at !== null) {
51787
50767
  console.error(
51788
- `Retired ${pkg2.name} (${pkg2.id}). New installs refuse it and it is hidden from other orgs; existing installations keep working and may still upgrade.`
50768
+ `Unpublished ${pkg2.name} (${pkg2.id}). New installs refuse it and it is hidden from other orgs; existing installations keep working and may still upgrade. Undo: lotics app unpublish ${pkg2.id} --undo`
51789
50769
  );
51790
50770
  } else {
51791
- console.error(`Un-retired ${pkg2.name} (${pkg2.id}) \u2014 installable again.`);
50771
+ console.error(`Re-published ${pkg2.name} (${pkg2.id}) \u2014 installable again.`);
51792
50772
  }
51793
50773
  }
51794
- function readAppKnowledgeDeclaration(appPkgJson) {
51795
- const lotics = isPlainObject2(appPkgJson.lotics) ? appPkgJson.lotics : {};
51796
- const knowledge = [];
51797
- if (Array.isArray(lotics.knowledge)) {
51798
- for (const entry of lotics.knowledge) {
51799
- if (isPlainObject2(entry) && typeof entry.alias === "string" && typeof entry.doc_id === "string") {
51800
- knowledge.push({ alias: entry.alias, doc_id: entry.doc_id });
51801
- }
51802
- }
51803
- }
51804
- const knowledge_expects = Array.isArray(lotics.knowledge_expects) ? lotics.knowledge_expects.filter((v) => typeof v === "string") : [];
51805
- return { knowledge, knowledge_expects };
51806
- }
51807
- function knowledgeManifestFromContract(contract) {
51808
- const out = {};
51809
- if (!isPlainObject2(contract) || !isPlainObject2(contract.knowledge)) return out;
51810
- for (const [alias, entry] of Object.entries(contract.knowledge)) {
51811
- if (!isPlainObject2(entry) || typeof entry.name !== "string") continue;
51812
- out[alias] = {
51813
- name: entry.name,
51814
- description: typeof entry.description === "string" ? entry.description : null,
51815
- active_by_default: typeof entry.active_by_default === "boolean" ? entry.active_by_default : true
51816
- };
51817
- }
51818
- return out;
51819
- }
51820
- async function packageExtract(client, args) {
51821
- const app = await client.getApp(args.app_id);
51822
- if (!app.current_version_id) {
50774
+ async function appPublish(client, args) {
50775
+ const projectDir = path6.resolve(args.projectDir ?? process.cwd());
50776
+ const local = readLocalAppManifest(projectDir);
50777
+ const explicit = args.app_id !== void 0 && args.app_id !== "." ? args.app_id : void 0;
50778
+ const appId = explicit ?? local?.app_id ?? null;
50779
+ if (appId === null) {
51823
50780
  throw new Error(
51824
- `App ${app.id} has no deployed version \u2014 extract stages its source archive, so deploy it first.`
50781
+ "No app id. Run `lotics app publish` from a pulled app project (lotics app pull <app_id>), or pass one: lotics app publish <app_id>."
51825
50782
  );
51826
50783
  }
51827
- const targetPath = path7.resolve(args.targetPath ?? appDirName(app.name));
51828
- if (fs6.existsSync(targetPath) && fs6.readdirSync(targetPath).length > 0) {
51829
- throw new Error(`Target directory ${targetPath} is not empty.`);
51830
- }
51831
- fs6.mkdirSync(targetPath, { recursive: true });
51832
- const version2 = await client.getAppVersion(app.id, app.current_version_id);
51833
- const sourceUrl = await client.getAppVersionSourceUrl(app.id, version2.id);
51834
- const tmpFile = path7.join(tmpdir2(), `lotics-extract-${app.id}-${Date.now()}.tar.gz`);
51835
- console.error("Downloading source archive...");
51836
- await client.downloadFile(sourceUrl, tmpFile);
51837
- try {
51838
- console.error(`Extracting to ${targetPath}...`);
51839
- await runTar(["-xzf", tmpFile, "-C", targetPath], targetPath);
51840
- } finally {
51841
- if (fs6.existsSync(tmpFile)) fs6.unlinkSync(tmpFile);
51842
- }
51843
- const appPkgJson = JSON.parse(
51844
- fs6.readFileSync(packageJsonPath(targetPath), "utf-8")
51845
- );
51846
- const appKnowledge = readAppKnowledgeDeclaration(appPkgJson);
51847
- const extracted = await client.extractPackage(args.app_id, {
51848
- knowledge: appKnowledge.knowledge
51849
- });
51850
- const { lines, hasError } = formatExtractReport(extracted.report);
51851
- if (lines.length > 0) {
51852
- console.error(`Extraction report (${extracted.report.length}):`);
51853
- for (const line of lines) console.error(line);
51854
- } else {
51855
- console.error("Extraction report: no findings.");
51856
- }
51857
- const draft = draftPackageProjectFromApp(appPkgJson, { name: app.name, description: null });
51858
- draft.manifest.knowledge = knowledgeManifestFromContract(extracted.contract);
51859
- draft.manifest.knowledge_expects = appKnowledge.knowledge_expects;
51860
- const deps = isPlainObject2(draft.pkgJson.dependencies) ? draft.pkgJson.dependencies : {};
51861
- const originRange = typeof deps["@lotics/app-sdk"] === "string" ? deps["@lotics/app-sdk"] : null;
51862
- const originFloor = originRange?.replace(/^[\^~]/, "") ?? null;
51863
- if (originFloor === null || cmpVersions(originFloor, STARTER_FALLBACK_SDK_VERSION) < 0) {
51864
- const raised = packageSdkRange(await fetchLatestNpmVersion("@lotics/app-sdk"));
51865
- draft.pkgJson.dependencies = { ...deps, "@lotics/app-sdk": raised };
51866
- console.error(`Raised @lotics/app-sdk to ${raised} (generated app_fields needs getAppBinding)`);
51867
- }
51868
- writePackageManifest(targetPath, draft);
51869
- fs6.writeFileSync(
51870
- path7.join(targetPath, CONTRACT_FILE),
51871
- JSON.stringify(extracted.contract, null, 2) + "\n"
51872
- );
51873
- console.error(`Wrote ${CONTRACT_FILE}`);
51874
- writePackageAppFields(targetPath);
51875
- const starterViteConfig = buildStarterTemplate({
51876
- app_name: app.name,
51877
- app_id: "",
51878
- workspace_id: ""
51879
- }).find((f) => f.path === "vite.config.ts");
51880
- if (starterViteConfig === void 0) {
51881
- throw new Error("starter template is missing vite.config.ts \u2014 cannot refresh the package project");
51882
- }
51883
- const viteConfigPath = path7.join(targetPath, "vite.config.ts");
51884
- const originViteConfig = fs6.existsSync(viteConfigPath) ? fs6.readFileSync(viteConfigPath, "utf-8") : null;
51885
- fs6.writeFileSync(viteConfigPath, starterViteConfig.content);
51886
- if (originViteConfig !== null && originViteConfig !== starterViteConfig.content) {
51887
- const stash = path7.join(dotLoticsDirEnsured(targetPath), "vite.config.origin.ts");
51888
- fs6.writeFileSync(stash, originViteConfig);
50784
+ const knowledge = local && local.app_id === appId ? local.knowledge : [];
50785
+ const renames = parseRenameFlags(args.renames);
50786
+ const preview = await client.previewPublishAppPackage(appId, { renames, knowledge });
50787
+ const { lines, hasError } = formatExtractReport(preview.findings);
50788
+ console.error(`Publish preview \u2014 ${appId} as new package "${preview.package_name}" (v1):`);
50789
+ const groups = [
50790
+ ["entities", preview.renamable_aliases.entities],
50791
+ ["fields", preview.renamable_aliases.fields],
50792
+ ["options", preview.renamable_aliases.options],
50793
+ ["roles", preview.renamable_aliases.roles],
50794
+ ["templates", preview.renamable_aliases.templates],
50795
+ ["workflows", preview.renamable_aliases.workflows]
50796
+ ];
50797
+ if (groups.some(([, vals]) => vals.length > 0)) {
50798
+ console.error(" Auto-minted aliases \u2014 rename any with --rename <alias>=<new> before v1 freezes them:");
50799
+ for (const [label, vals] of groups) {
50800
+ if (vals.length > 0) console.error(` ${`${label}:`.padEnd(11)} ${vals.join(", ")}`);
50801
+ }
51889
50802
  console.error(
51890
- "Refreshed vite.config.ts from the starter (build.target es2022). The origin app's config differed \u2014 its original was saved to .lotics/vite.config.origin.ts; re-apply any custom optimizeDeps/plugins entries you still need (never dev-link aliases)."
50803
+ " (query / app-workflow / agent runtime aliases are fixed \u2014 the shipped source calls them verbatim.)"
51891
50804
  );
51892
50805
  } else {
51893
- console.error("Refreshed vite.config.ts from the starter (build.target es2022)");
51894
- }
51895
- for (const tf of extracted.template_files) {
51896
- const dest = path7.join(targetPath, tf.bytes_ref);
51897
- fs6.mkdirSync(path7.dirname(dest), { recursive: true });
51898
- const { path: downloaded } = await client.downloadFileById(tf.file_id, path7.dirname(dest));
51899
- if (path7.resolve(downloaded) !== path7.resolve(dest)) fs6.renameSync(downloaded, dest);
51900
- console.error(`Wrote ${tf.bytes_ref}`);
51901
- }
51902
- for (const kf of extracted.knowledge_files) {
51903
- const dest = path7.join(targetPath, kf.content_ref);
51904
- fs6.mkdirSync(path7.dirname(dest), { recursive: true });
51905
- fs6.writeFileSync(dest, kf.content);
51906
- console.error(`Wrote ${kf.content_ref}`);
51907
- }
51908
- const dotLotics = path7.join(targetPath, ".lotics");
51909
- fs6.mkdirSync(dotLotics, { recursive: true });
51910
- fs6.writeFileSync(
51911
- path7.join(dotLotics, ADOPT_BINDING_FILE),
51912
- JSON.stringify(
51913
- {
51914
- app_id: app.id,
51915
- workspace_id: app.workspace_id,
51916
- binding: extracted.binding,
51917
- knowledge_binding: extracted.knowledge_binding
51918
- },
51919
- null,
51920
- 2
51921
- ) + "\n"
51922
- );
51923
- console.error(`Wrote .lotics/${ADOPT_BINDING_FILE}`);
51924
- console.error("Installing npm dependencies...");
51925
- await runNpm(["install"], targetPath);
50806
+ console.error(" No renamable aliases.");
50807
+ }
50808
+ if (lines.length > 0) {
50809
+ console.error(` Findings (${preview.findings.length}):`);
50810
+ for (const line of lines) console.error(line);
50811
+ }
51926
50812
  if (hasError) {
51927
50813
  console.error(
51928
- "\nExtraction produced error findings \u2014 the draft is written but NOT publishable as-is."
51929
- );
51930
- console.error(
51931
- "Fix the reported items above, then publish (which re-validates the contract)."
50814
+ "\nExtract found error findings \u2014 the app cannot be published as-is. Fix them in the app, redeploy, and retry."
51932
50815
  );
51933
50816
  process.exitCode = 1;
51934
50817
  return;
51935
50818
  }
51936
- console.error("\nDraft package project written. Next steps:");
51937
- console.error(` cd ${path7.relative(process.cwd(), targetPath) || "."}`);
51938
- console.error(" # review the aliases in contract.json, then:");
51939
- console.error(` lotics package publish -m "v1"`);
51940
- console.error(` lotics package adopt ${app.id} (from this project, same workspace)`);
51941
- }
51942
- async function packageAdopt(client, args) {
51943
- const projectDir = path7.resolve(args.projectDir ?? process.cwd());
51944
- const { manifest } = readPackageProject(projectDir);
51945
- if (manifest.id === null) {
51946
- throw new Error(
51947
- "This package project has never been published (no package id). Publish first:\n lotics package publish"
51948
- );
50819
+ if (!args.yes) {
50820
+ const idArg = explicit ?? ".";
50821
+ const renameArgs = args.renames.map((r) => ` --rename ${r}`).join("");
50822
+ const mArg = args.changelog ? ` -m ${JSON.stringify(args.changelog)}` : "";
50823
+ console.error(`
50824
+ Re-run with --yes to publish v1:`);
50825
+ console.error(` lotics app publish ${idArg}${renameArgs}${mArg} --yes`);
50826
+ process.exitCode = 1;
50827
+ return;
51949
50828
  }
51950
- const version2 = args.version ?? manifest.version;
51951
- if (version2 === null) {
50829
+ const result = await client.publishAppAsPackage(appId, {
50830
+ renames,
50831
+ changelog: args.changelog ?? null,
50832
+ knowledge
50833
+ });
50834
+ console.error(`Published ${result.package_id} v${result.version} from app ${appId}.`);
50835
+ console.error(` The app is now installation #1 \u2014 develop it in place, then release the next version:`);
50836
+ console.error(` lotics app pull ${appId} # edit, then lotics app deploy`);
50837
+ console.error(` lotics app release ${appId} -m "<what changed>"`);
50838
+ console.error(` Install it elsewhere: lotics install ${result.package_id}`);
50839
+ }
50840
+ async function appRelease(client, args) {
50841
+ const projectDir = path6.resolve(args.projectDir ?? process.cwd());
50842
+ const local = readLocalAppManifest(projectDir);
50843
+ const explicit = args.app_id !== void 0 && args.app_id !== "." ? args.app_id : void 0;
50844
+ const appId = explicit ?? local?.app_id ?? null;
50845
+ if (appId === null) {
51952
50846
  throw new Error(
51953
- "No version to adopt \u2014 publish this project first (lotics package publish) or pass --version N."
50847
+ "No app id. Run this from a pulled app project (lotics app pull <app_id>), or pass an app id explicitly."
51954
50848
  );
51955
50849
  }
51956
- const bindingPath = path7.join(projectDir, ".lotics", ADOPT_BINDING_FILE);
51957
- if (!fs6.existsSync(bindingPath)) {
51958
- throw new Error(
51959
- `No .lotics/${ADOPT_BINDING_FILE} in ${projectDir}. Adopt binds the origin app that "lotics package extract" recorded \u2014 run extract to produce this project.`
51960
- );
50850
+ const knowledge = local && local.app_id === appId && local.knowledge.length > 0 ? local.knowledge : void 0;
50851
+ const preview = await client.previewPackageRelease(appId, { knowledge });
50852
+ const { lines, hasError } = formatExtractReport(preview.findings);
50853
+ console.error(`Release preview \u2014 ${appId} \u2192 ${preview.package_id} v${preview.version}:`);
50854
+ if (preview.added_aliases.length > 0) {
50855
+ console.error(` New (${preview.added_aliases.length}): ${preview.added_aliases.join(", ")}`);
51961
50856
  }
51962
- const pin = parseAdoptBindingFile(
51963
- JSON.parse(fs6.readFileSync(bindingPath, "utf-8")),
51964
- args.app_id
51965
- );
51966
- const app = await client.adoptPackage(args.app_id, {
51967
- package_id: manifest.id,
51968
- version: version2,
51969
- binding: pin.binding,
51970
- // The origin knowledge binding (empty when the app owns no package-managed
51971
- // docs) the server verifies it maps the contract's knowledge aliases faithfully.
51972
- ...Object.keys(pin.knowledge_binding).length > 0 ? { knowledge_binding: pin.knowledge_binding } : {}
51973
- });
51974
- console.error(
51975
- `Adopted ${app.name} \u2192 installation of ${app.package_id} v${app.package_version}.`
51976
- );
51977
- console.error(` Verify the installation: lotics package doctor ${app.id}`);
50857
+ if (preview.changed_artifacts.length > 0) {
50858
+ console.error(` Changed (${preview.changed_artifacts.length}): ${preview.changed_artifacts.join(", ")}`);
50859
+ }
50860
+ const k = preview.knowledge ?? { added: [], removed: [], changed: [] };
50861
+ if (k.added.length > 0 || k.removed.length > 0 || k.changed.length > 0) {
50862
+ const parts = [
50863
+ k.added.length > 0 ? `+${k.added.join(", ")}` : null,
50864
+ k.removed.length > 0 ? `dropped ${k.removed.join(", ")}` : null,
50865
+ k.changed.length > 0 ? `changed ${k.changed.join(", ")}` : null
50866
+ ].filter((p) => p !== null);
50867
+ console.error(` Knowledge: ${parts.join("; ")}`);
50868
+ }
50869
+ if (preview.added_aliases.length === 0 && preview.changed_artifacts.length === 0 && k.added.length === 0 && k.removed.length === 0 && k.changed.length === 0) {
50870
+ console.error(" No contract changes since the current version (a fresh code/dist snapshot still ships).");
50871
+ }
50872
+ if (lines.length > 0) {
50873
+ console.error(` Findings (${preview.findings.length}):`);
50874
+ for (const line of lines) console.error(line);
50875
+ }
50876
+ if (hasError) {
50877
+ console.error("\nExtract found error findings \u2014 the origin cannot be released as-is. Fix them in the app and retry.");
50878
+ process.exitCode = 1;
50879
+ return;
50880
+ }
50881
+ if (!args.yes) {
50882
+ console.error(`
50883
+ Re-run with --yes to publish v${preview.version}:`);
50884
+ console.error(` lotics app release ${args.app_id ?? "."} -m ${JSON.stringify(args.changelog)} --yes`);
50885
+ process.exitCode = 1;
50886
+ return;
50887
+ }
50888
+ const result = await client.releasePackage(appId, { changelog: args.changelog, knowledge });
50889
+ console.error(`Released ${result.package_id} v${result.version}.`);
50890
+ console.error(` The origin was re-pinned to v${result.version} \u2014 verify: lotics package doctor ${appId}`);
51978
50891
  }
51979
50892
  async function packageYank(client, args) {
51980
50893
  const result = await client.yankPackageVersion(args.package_id, args.version, !args.undo);
@@ -52006,7 +50919,7 @@ async function packageFleetUpgrade(client, args) {
52006
50919
  `${line}: breaking=${inst.blockers.breaking} drift=${inst.blockers.drift} modified=${inst.blockers.modified}`
52007
50920
  );
52008
50921
  console.error(
52009
- ` resolve via: lotics package upgrade ${inst.app_id} --version ${result.target_version} ...`
50922
+ ` resolve via: lotics upgrade ${inst.app_id} --version ${result.target_version} ...`
52010
50923
  );
52011
50924
  } else if (inst.message) {
52012
50925
  console.error(`${line}: ${inst.message}`);
@@ -52038,7 +50951,6 @@ function parseArgs(argv) {
52038
50951
  message: void 0,
52039
50952
  local: false,
52040
50953
  all: false,
52041
- dev: false,
52042
50954
  yes: false,
52043
50955
  printCreated: false,
52044
50956
  cleanup: false,
@@ -52047,7 +50959,7 @@ function parseArgs(argv) {
52047
50959
  bindTo: [],
52048
50960
  keepContent: false,
52049
50961
  applyAll: false,
52050
- kind: void 0,
50962
+ rename: [],
52051
50963
  config: [],
52052
50964
  set: [],
52053
50965
  archiveTables: false,
@@ -52102,9 +51014,6 @@ function parseArgs(argv) {
52102
51014
  case "--local":
52103
51015
  flags.local = true;
52104
51016
  break;
52105
- case "--dev":
52106
- flags.dev = true;
52107
- break;
52108
51017
  case "--all":
52109
51018
  flags.all = true;
52110
51019
  break;
@@ -52143,9 +51052,14 @@ function parseArgs(argv) {
52143
51052
  case "--apply-all":
52144
51053
  flags.applyAll = true;
52145
51054
  break;
52146
- case "--kind":
52147
- flags.kind = argv[++i2];
51055
+ case "--rename": {
51056
+ const value = argv[++i2];
51057
+ if (value === void 0 || value.startsWith("-")) {
51058
+ throw new Error("--rename requires a value: old=new (an alias to rename before v1 freezes it).");
51059
+ }
51060
+ flags.rename.push(value);
52148
51061
  break;
51062
+ }
52149
51063
  case "--config": {
52150
51064
  const value = argv[++i2];
52151
51065
  if (value === void 0 || value.startsWith("-")) {
@@ -52171,10 +51085,10 @@ function parseArgs(argv) {
52171
51085
  case "--version":
52172
51086
  case "-v": {
52173
51087
  const next = argv[i2 + 1];
52174
- if (command === "package") {
51088
+ if (command === "package" || command === "install" || command === "upgrade") {
52175
51089
  if (next === void 0 || next.startsWith("-")) {
52176
51090
  throw new Error(
52177
- "--version requires a version number for package commands (e.g. --version 2)."
51091
+ "--version requires a version number for install/upgrade/package commands (e.g. --version 2)."
52178
51092
  );
52179
51093
  }
52180
51094
  flags.packageVersion = next;
@@ -52236,6 +51150,34 @@ async function ingestJsonArgs(opts) {
52236
51150
  // src/xlsx.ts
52237
51151
  import fs7 from "node:fs";
52238
51152
 
51153
+ // src/file_command_io.ts
51154
+ import fs6 from "node:fs";
51155
+ import path7 from "node:path";
51156
+ var CliError = class extends Error {
51157
+ constructor(message) {
51158
+ super(message);
51159
+ this.name = "CliError";
51160
+ }
51161
+ };
51162
+ function fail(message) {
51163
+ throw new CliError(message);
51164
+ }
51165
+ function writeFileAtomic(filePath, bytes) {
51166
+ const dir = path7.dirname(path7.resolve(filePath));
51167
+ const tmp = path7.join(dir, `.${path7.basename(filePath)}.${process.pid}.${Date.now()}.tmp`);
51168
+ fs6.writeFileSync(tmp, bytes);
51169
+ try {
51170
+ fs6.renameSync(tmp, filePath);
51171
+ } catch (error51) {
51172
+ try {
51173
+ fs6.unlinkSync(tmp);
51174
+ } catch {
51175
+ }
51176
+ throw error51;
51177
+ }
51178
+ console.error(`Wrote ${filePath}`);
51179
+ }
51180
+
52239
51181
  // ../xlsx/node_modules/fast-xml-parser/src/util.js
52240
51182
  var nameStartChar = ":A-Za-z_\\u00C0-\\u00D6\\u00D8-\\u00F6\\u00F8-\\u02FF\\u0370-\\u037D\\u037F-\\u1FFF\\u200C-\\u200D\\u2070-\\u218F\\u2C00-\\u2FEF\\u3001-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFFD";
52241
51183
  var nameChar = nameStartChar + "\\-.\\d\\u00B7\\u0300-\\u036F\\u203F-\\u2040";
@@ -68566,7 +67508,7 @@ async function runDocxCommand(subcommand, toolArgs, restArgs) {
68566
67508
  import { spawn as spawn3 } from "node:child_process";
68567
67509
  import { createServer } from "node:http";
68568
67510
  import { readFileSync as readFileSync2, writeFileSync, existsSync, mkdtempSync, rmSync, readdirSync } from "node:fs";
68569
- import { tmpdir as tmpdir3 } from "node:os";
67511
+ import { tmpdir as tmpdir2 } from "node:os";
68570
67512
  import { join, dirname, resolve, extname, basename } from "node:path";
68571
67513
  import { fileURLToPath as fileURLToPath2 } from "node:url";
68572
67514
  import { setTimeout as sleep } from "node:timers/promises";
@@ -68659,7 +67601,7 @@ async function runPreviewCommand(filePath, flags) {
68659
67601
  });
68660
67602
  await new Promise((r) => server.listen(0, "127.0.0.1", () => r()));
68661
67603
  const httpPort = server.address().port;
68662
- const udd = mkdtempSync(join(tmpdir3(), "lotics-render-"));
67604
+ const udd = mkdtempSync(join(tmpdir2(), "lotics-render-"));
68663
67605
  const child = spawn3(chrome, [
68664
67606
  "--headless=new",
68665
67607
  "--disable-gpu",
@@ -68808,10 +67750,22 @@ COMMANDS
68808
67750
  cat args.json | lotics run <tool> Read JSON args from stdin (large payloads)
68809
67751
  lotics app create <name> [path] Create a new custom-code app + scaffold locally
68810
67752
  lotics app pull <app_id> [path] Bootstrap full local env (source + npm install + types)
68811
- lotics app deploy -m <message> Build + upload current dir as a new version
68812
- (-m is required \u2014 it's the version's audit trail)
68813
- (code + queries only \u2014 workflow bindings are
67753
+ lotics app deploy -m <message> Build + upload current dir as a new version \u2014 COMMIT
67754
+ (-m is required \u2014 it's the version's audit trail;
67755
+ carries code + queries only \u2014 workflow bindings are
68814
67756
  managed by set_app_workflow / remove_app_workflow)
67757
+ lotics app publish [app_id|.] [--rename old=new ...] [-m <changelog>] [--yes]
67758
+ Make the app DISTRIBUTABLE \u2014 first-release it (v1) as a
67759
+ package; later versions ship via "lotics app release".
67760
+ Previews the auto-minted aliases + findings; --yes applies
67761
+ (extract \u2192 create \u2192 publish v1 \u2192 pin origin). --rename fixes
67762
+ an alias before v1 freezes; -m is an optional v1 changelog
67763
+ lotics app release [app_id|.] -m <changelog> [--yes]
67764
+ RELEASE the next package version from the origin app you
67765
+ are happy with (preview; --yes publishes + re-pins)
67766
+ lotics app unpublish <app_id|package_id> [--undo]
67767
+ Take the published package off the shelf (installations
67768
+ keep working); --undo restores it
68815
67769
  lotics app versions [app_id] Show deploy history newest-first (version,
68816
67770
  timestamp, deployer, build status, -m message;
68817
67771
  * marks the currently served version)
@@ -68833,57 +67787,42 @@ COMMANDS
68833
67787
  lotics app subdomain <new-subdomain> Rename the app's public <slug>.lotics.app address
68834
67788
  lotics app rename "<new name>" Rename the app's display name (launcher title)
68835
67789
  lotics app dev [path] Run the app locally with HMR (RPC forwarded to prod)
68836
- lotics package new <name> [path] [--kind app|content]
68837
- Scaffold a package project \u2014 an app (contract + source)
68838
- or a content corpus (docs-only, no app source)
68839
- lotics package build [path] Build the publishable bundle (source + dist)
68840
- lotics package publish [path] Publish a new immutable package version
68841
- lotics package dev [path] Sync the package into a dev workspace + run the app
68842
- dev server (--workspace <dev_ws> selects it)
68843
- lotics package sync [path] Re-sync (additive migrate + materialize) into a dev ws
68844
- lotics package reset [path] DEV-ONLY: drop scaffolded tables + re-scaffold clean
68845
- lotics package install <package> [--version N] [--bind-to <alias>=<kdc_id> ...] [--config key=value ...]
67790
+ # PACKAGES \u2014 consumer verbs are top-level; low-traffic ops live on "lotics package"
67791
+ # (author verbs live on "lotics app")
67792
+ lotics install <package_id> [--version N] [--bind-to <alias>=<kdc_id> ...] [--config key=value ...]
68846
67793
  Install a package (app: scaffolds/deploys/materializes,
68847
67794
  --config sets its knobs; content: installs the doc corpus,
68848
67795
  --bind-to consents to adopt a same-named doc on a collision)
68849
- lotics package uninstall <app_id|pci_id> [--archive-tables] [--keep-content]
67796
+ lotics uninstall <app_id|package_id> [--archive-tables] [--keep-content]
68850
67797
  Uninstall \u2014 dispatched by id. App (app_id): archives
68851
67798
  artifacts (+ --archive-tables also archives scaffolded
68852
- tables). Content (pci_): archives its package-bound docs
68853
- unless --keep-content
68854
- lotics package config <app_id> [--set key=value ...]
68855
- Show or edit an installation's config knobs
68856
- lotics package show <package_id> Registry metadata + version history
68857
- lotics package retire <package_id> [--undo]
68858
- Retire a package (refuse new installs, hide from other
68859
- orgs); existing installations keep working + upgrade
68860
- lotics package list-content List the workspace's content installations
68861
- (standalone pci_ + app-bundled) \u2014 source for a pci_ id
68862
- lotics package upgrade <app_id|pci_id> [--version N] [--resolve <key>=... ] [--bind-to ...] [--apply-all]
68863
- Preview-then-apply an upgrade. App: refuses while any
68864
- drift/modified/bundled-knowledge finding lacks a --resolve
68865
- (a knowledge-alias --resolve routes to the doc). Content
68866
- (pci_): --resolve <alias>=apply|keep|archive|recreate|unbind;
68867
- --apply-all accepts the package's version for every doc
67799
+ tables). Content package (apg_): archives its package-bound
67800
+ docs unless --keep-content
67801
+ lotics upgrade <app_id|package_id> [--version N] [--resolve <key>=... ] [--bind-to ...] [--apply-all]
67802
+ Preview-then-apply an upgrade, dispatched by id. App
67803
+ (app_id): refuses while any drift/modified/bundled-knowledge
67804
+ finding lacks a --resolve. Package (apg_): a CONTENT package
67805
+ upgrades THIS workspace's install (--resolve
67806
+ <alias>=apply|keep|archive|recreate|unbind); an APP package
67807
+ FLEET-upgrades every installation across your org (clean
67808
+ apply, findings skip). --apply-all accepts the package's version
68868
67809
  lotics package doctor [app_id] Installation health: version pin vs latest, binding
68869
67810
  drift, locally modified core, knowledge drift/edits
68870
67811
  (exit 1 on findings)
68871
- lotics package rebind-role <app_id> <alias> <grp_id>
68872
- Re-point a package role at a different group
68873
- (re-materializes at the pinned version)
67812
+ lotics package config <app_id> [--set key=value ...]
67813
+ Show or edit an installation's config knobs
68874
67814
  lotics package eject <app_id> Sever an installation's package link
68875
67815
  (re-deploys the pinned source as a bespoke app)
68876
- lotics package extract <app_id> [path] Promote a bespoke app to a draft package
68877
- project (contract + binding + templates); always
68878
- writes the draft, exits 1 on error findings
68879
- lotics package adopt <app_id> [path] Bind the published package project onto the
68880
- origin app (reads .lotics/adopt_binding.json;
68881
- --version N pins a specific version)
68882
- lotics package fleet-upgrade <package_id> [--version N]
67816
+ lotics package show <package_id> Registry metadata + version history
67817
+ lotics package list-content List the workspace's content installations
67818
+ (standalone content installs) \u2014 each row leads with
67819
+ the package id for upgrade/uninstall
68883
67820
  lotics package yank <package_id> <version> [--undo]
68884
- Upgrade EVERY installation of the package across
68885
- your org: applies where the preview is clean,
68886
- skips + reports findings (exit 1 unless all current)
67821
+ Refuse new installs/upgrades of a broken published
67822
+ version (pinned installations keep running)
67823
+ lotics run publish_content '{"name":"\u2026","knowledge_doc_ids":[\u2026],"template_ids":[\u2026]}'
67824
+ Publish a SET of live docs + templates as a content
67825
+ package (v1 + origin pin); release_content cuts the next
68887
67826
  lotics ui link <component> [--ui-src <path>] [--remove]
68888
67827
  Dev-link @lotics/ui to packages/ui/src (Vite alias
68889
67828
  + tsc paths) for live HMR + typecheck. Monorepo apps
@@ -68912,13 +67851,12 @@ FLAGS
68912
67851
  is_current_member / row-scoping resolve to them (also
68913
67852
  LOTICS_VIEW_AS env; admin key only; writes stay yours)
68914
67853
  --local Pin the current directory (lotics org use / auth api-key)
68915
- --dev (lotics workspace create) Mark a throwaway app-package dev
68916
- workspace \u2014 required for "lotics package reset"
68917
- --kind <k> (lotics package new) Package kind: app (default) or content
68918
- --bind-to <a>=<id> (lotics package install/upgrade, repeatable) Adopt an existing
67854
+ --rename <old>=<new> (lotics app publish, repeatable) Fix an auto-minted alias before
67855
+ the package's v1 contract freezes it
67856
+ --bind-to <a>=<id> (lotics install/upgrade, repeatable) Adopt an existing
68919
67857
  same-named doc as package-managed (resolves a knowledge collision)
68920
- --keep-content (lotics package uninstall) Keep the docs instead of archiving them
68921
- --apply-all (lotics package upgrade, knowledge) Accept the package's version
67858
+ --keep-content (lotics uninstall) Keep the docs instead of archiving them
67859
+ --apply-all (lotics upgrade, knowledge) Accept the package's version
68922
67860
  for every doc (overwrites local edits)
68923
67861
  --all (lotics auth logout) Remove every saved credential
68924
67862
  --version Show version
@@ -69292,27 +68230,6 @@ async function main() {
69292
68230
  console.error("Usage: lotics ui link <component> [--ui-src <abs path>] [--remove]");
69293
68231
  process.exit(1);
69294
68232
  }
69295
- if (command === "package" && subcommand === "new") {
69296
- const name = toolArgs;
69297
- if (!name) {
69298
- console.error("Usage: lotics package new <name> [path] [--kind app|content]");
69299
- process.exit(1);
69300
- }
69301
- if (flags.kind !== void 0 && flags.kind !== "app" && flags.kind !== "content") {
69302
- console.error(`Invalid --kind "${flags.kind}" \u2014 expected "app" or "content".`);
69303
- process.exit(1);
69304
- }
69305
- await packageNew({
69306
- name,
69307
- targetPath: restArgs[0],
69308
- kind: flags.kind === "content" ? "content" : "app"
69309
- });
69310
- return;
69311
- }
69312
- if (command === "package" && subcommand === "build") {
69313
- await packageBuild({ projectDir: toolArgs });
69314
- return;
69315
- }
69316
68233
  if (command === "app" && subcommand === "workflow" && toolArgs === "check") {
69317
68234
  await appWorkflowCheck({ alias: restArgs[0] });
69318
68235
  return;
@@ -69387,7 +68304,7 @@ async function main() {
69387
68304
  }
69388
68305
  return;
69389
68306
  }
69390
- if (command !== "tools" && command !== "upload" && command !== "run" && command !== "download" && command !== "workspace" && command !== "app" && command !== "package") {
68307
+ if (command !== "tools" && command !== "upload" && command !== "run" && command !== "download" && command !== "workspace" && command !== "app" && command !== "package" && command !== "install" && command !== "uninstall" && command !== "upgrade") {
69391
68308
  console.error(`Unknown command: ${command}`);
69392
68309
  console.error('Run "lotics --help" for usage.');
69393
68310
  process.exit(1);
@@ -69407,30 +68324,27 @@ async function main() {
69407
68324
  console.error(" lotics app subdomain <new-subdomain> Rename the app's public <slug>.lotics.app address");
69408
68325
  console.error(` lotics app rename "<new name>" Rename the app's display name (launcher title)`);
69409
68326
  console.error(" lotics app dev [path] Run the app locally with HMR + RPC forwarding");
68327
+ console.error(" lotics app publish [app_id|.] [--rename old=new ...] [-m <changelog>] [--yes] First-release the app as a package (v1; preview, --yes applies)");
68328
+ console.error(" lotics app release [app_id|.] -m <changelog> [--yes] Release the next package version from the origin");
68329
+ console.error(" lotics app unpublish <app_id|package_id> [--undo] Take a published package off the shelf (any kind \u2014 app or content)");
69410
68330
  process.exit(1);
69411
68331
  }
69412
68332
  if (command === "package" && !subcommand) {
69413
- console.error("Usage:");
69414
- console.error(" lotics package new <name> [path] [--kind app|content] Scaffold a package project (app source, or docs-only)");
69415
- console.error(" lotics package build [path] Build the publishable bundle (source + dist)");
69416
- console.error(" lotics package publish [path] [-m <changelog>] Publish a new immutable package version");
69417
- console.error(" lotics package dev [path] [--workspace <ws>] Sync into a dev workspace + run the app dev server");
69418
- console.error(" lotics package sync [path] [--workspace <ws>] Re-sync (additive migrate + materialize) into a dev workspace");
69419
- console.error(" lotics package reset [path] [--workspace <ws>] DEV-ONLY: drop scaffolded tables + re-scaffold clean");
69420
- console.error(" lotics package install <package> [--version N] [--bind-to <alias>=<kdc_id>] [--config key=value ...] Install a package (app or content)");
69421
- console.error(" lotics package uninstall <app_id|pci_id> [--archive-tables] [--keep-content] Uninstall \u2014 dispatched by id (app vs content)");
69422
- console.error(" lotics package config <app_id> [--set key=value ...] Show or edit an installation's config knobs");
69423
- console.error(" lotics package show <package_id> Registry metadata + version history (channel, yank, changelog)");
69424
- console.error(" lotics package retire <package_id> [--undo] Retire a package (refuse new installs; installs keep working)");
69425
- console.error(" lotics package list-content List the workspace's content installations (standalone + app-bundled)");
69426
- console.error(" lotics package upgrade <app_id|pci_id> [--version N] [--resolve <key>=... ] [--bind-to ...] [--apply-all] Preview + apply an upgrade");
68333
+ console.error("Usage (low-traffic consumer / registry ops \u2014 author verbs live on `lotics app`):");
68334
+ console.error(" The high-traffic consumer verbs are top-level:");
68335
+ console.error(" lotics install <package_id> [--version N] [--bind-to <alias>=<kdc_id>] [--config key=value ...] Install a package (app or content)");
68336
+ console.error(" lotics uninstall <app_id|package_id> [--archive-tables] [--keep-content] Uninstall \u2014 dispatched by id (app vs content)");
68337
+ console.error(" lotics upgrade <app_id|package_id> [--version N] [--resolve <key>=... ] [--bind-to ...] [--apply-all] Upgrade \u2014 app install / content install / whole app fleet (apg_)");
69427
68338
  console.error(" lotics package doctor [app_id] Health: version pin vs latest + binding/knowledge drift");
69428
- console.error(" lotics package rebind-role <app_id> <alias> <grp_id> Re-point a package role at another group");
68339
+ console.error(" lotics package config <app_id> [--set key=value ...] Show or edit an installation's config knobs");
69429
68340
  console.error(" lotics package eject <app_id> Sever an installation's package link");
69430
- console.error(" lotics package extract <app_id> [path] Promote a bespoke app to a draft package project");
69431
- console.error(" lotics package adopt <app_id> [path] Bind the published project onto the origin app");
69432
- console.error(" lotics package fleet-upgrade <package_id> [--version N] Upgrade every org installation (clean ones apply; findings skip)");
68341
+ console.error(" lotics package show <package_id> Registry metadata + version history (channel, yank, changelog)");
68342
+ console.error(" lotics package list-content List the workspace's content installations (standalone content installs)");
69433
68343
  console.error(" lotics package yank <package_id> <version> [--undo] Refuse new installs/upgrades of a broken published version (pinned installations keep running)");
68344
+ console.error(" A content package (a SET of live docs + templates) \u2014 publish + release with the tools, retire on `lotics app`:");
68345
+ console.error(` lotics run publish_content '{"name":"\u2026","knowledge_doc_ids":[\u2026],"template_ids":[\u2026]}' Publish v1 (a fixed set)`);
68346
+ console.error(` lotics run release_content '{"package_id":"apg_\u2026","changelog":"\u2026"}' Cut the next version (add "dry_run":true to preview)`);
68347
+ console.error(" lotics app unpublish <package_id> Take the whole package off the shelf (kind-agnostic \u2014 retires content too)");
69434
68348
  process.exit(1);
69435
68349
  }
69436
68350
  if (command === "run" && !subcommand) {
@@ -69506,11 +68420,11 @@ Available workspaces:`);
69506
68420
  if (subcommand === "create") {
69507
68421
  const name = toolArgs;
69508
68422
  if (!name) {
69509
- console.error("Usage: lotics workspace create <name> [--timezone <tz>] [--dev]");
68423
+ console.error("Usage: lotics workspace create <name> [--timezone <tz>]");
69510
68424
  process.exit(1);
69511
68425
  }
69512
68426
  const timezone = flags.timezone;
69513
- const created = await client.createWorkspace({ name, timezone, is_dev: flags.dev || void 0 });
68427
+ const created = await client.createWorkspace({ name, timezone });
69514
68428
  setSelectedWorkspace(created.id);
69515
68429
  client.setWorkspaceId(created.id);
69516
68430
  if (flags.json) {
@@ -69572,48 +68486,92 @@ Available workspaces:`);
69572
68486
  return;
69573
68487
  }
69574
68488
  await resolveWorkspace(client, ctx);
69575
- if (command === "package") {
69576
- if (subcommand === "install") {
69577
- const packageId = toolArgs;
69578
- if (!packageId) {
69579
- console.error(
69580
- "Usage: lotics package install <package> [--version N] [--bind-to <alias>=<kdc_id> ...]"
69581
- );
68489
+ if (command === "install") {
68490
+ const packageId = subcommand;
68491
+ if (!packageId) {
68492
+ console.error("Usage: lotics install <package_id> [--version N] [--bind-to <alias>=<kdc_id> ...] [--config key=value ...]");
68493
+ process.exit(1);
68494
+ }
68495
+ let version2;
68496
+ if (flags.packageVersion !== void 0) {
68497
+ version2 = Number(flags.packageVersion);
68498
+ if (!Number.isInteger(version2) || version2 <= 0) {
68499
+ console.error(`Invalid --version "${flags.packageVersion}" \u2014 expected a positive integer.`);
69582
68500
  process.exit(1);
69583
68501
  }
69584
- let version2;
69585
- if (flags.packageVersion !== void 0) {
69586
- version2 = Number(flags.packageVersion);
69587
- if (!Number.isInteger(version2) || version2 <= 0) {
69588
- console.error(`Invalid --version "${flags.packageVersion}" \u2014 expected a positive integer.`);
69589
- process.exit(1);
69590
- }
69591
- }
69592
- const config2 = flags.config.length > 0 ? parseInstallConfigFlags(flags.config) : void 0;
69593
- await packageInstall(client, {
69594
- package_id: packageId,
69595
- version: version2,
69596
- bind_to: parseBindToFlags(flags.bindTo),
69597
- ...config2 !== void 0 ? { config: config2 } : {}
69598
- });
68502
+ }
68503
+ const config2 = flags.config.length > 0 ? parseInstallConfigFlags(flags.config) : void 0;
68504
+ await packageInstall(client, {
68505
+ package_id: packageId,
68506
+ version: version2,
68507
+ bind_to: parseBindToFlags(flags.bindTo),
68508
+ ...config2 !== void 0 ? { config: config2 } : {}
68509
+ });
68510
+ return;
68511
+ }
68512
+ if (command === "uninstall") {
68513
+ const id = subcommand;
68514
+ if (!id) {
68515
+ console.error("Usage: lotics uninstall <app_id|package_id> [--archive-tables] [--keep-content]");
68516
+ console.error(
68517
+ "Dispatched by id: an app installation (app_id; --archive-tables to also archive its scaffolded tables) or a content package (apg_ id from `lotics install` / lotics package list-content; --keep-content to retain its docs)."
68518
+ );
68519
+ process.exit(1);
68520
+ }
68521
+ if (id.startsWith("pci_")) {
68522
+ await redirectContentPciForm(client, id, "uninstall");
69599
68523
  return;
69600
68524
  }
69601
- if (subcommand === "uninstall") {
69602
- const id = toolArgs;
69603
- if (!id) {
69604
- console.error("Usage: lotics package uninstall <app_id|pci_id> [--archive-tables] [--keep-content]");
69605
- console.error(
69606
- "Dispatched by id: an app installation (app_id; --archive-tables to also archive its scaffolded tables) or a standalone content installation (pci_ id from install / lotics package list-content; --keep-content to retain its docs)."
69607
- );
68525
+ await packageUninstall(client, {
68526
+ id,
68527
+ keep_content: flags.keepContent,
68528
+ archive_tables: flags.archiveTables
68529
+ });
68530
+ return;
68531
+ }
68532
+ if (command === "upgrade") {
68533
+ const target = subcommand;
68534
+ if (!target) {
68535
+ console.error(
68536
+ "Usage: lotics upgrade <app_id | package_id> [--version N] [--resolve <key>=... ...] [--bind-to ...] [--apply-all]"
68537
+ );
68538
+ console.error(
68539
+ "Dispatched by id: an app installation (app_id), or a package id (apg_) \u2014 a CONTENT package upgrades this workspace's install; an APP package fleet-upgrades every installation across your org."
68540
+ );
68541
+ process.exit(1);
68542
+ }
68543
+ let version2;
68544
+ if (flags.packageVersion !== void 0) {
68545
+ version2 = Number(flags.packageVersion);
68546
+ if (!Number.isInteger(version2) || version2 <= 0) {
68547
+ console.error(`Invalid --version "${flags.packageVersion}" \u2014 expected a positive integer.`);
69608
68548
  process.exit(1);
69609
68549
  }
69610
- await packageUninstall(client, {
69611
- id,
69612
- keep_content: flags.keepContent,
69613
- archive_tables: flags.archiveTables
68550
+ }
68551
+ if (target.startsWith("pci_")) {
68552
+ await redirectContentPciForm(client, target, "upgrade");
68553
+ return;
68554
+ }
68555
+ if (target.startsWith("apg_")) {
68556
+ await packageUpgradeByPackageId(client, {
68557
+ package_id: target,
68558
+ version: version2,
68559
+ resolve: flags.resolve,
68560
+ bindTo: flags.bindTo,
68561
+ applyAll: flags.applyAll
69614
68562
  });
69615
68563
  return;
69616
68564
  }
68565
+ await packageUpgrade(client, {
68566
+ app_id: target,
68567
+ version: version2,
68568
+ resolve: flags.resolve,
68569
+ bindTo: flags.bindTo,
68570
+ applyAll: flags.applyAll
68571
+ });
68572
+ return;
68573
+ }
68574
+ if (command === "package") {
69617
68575
  if (subcommand === "list-content") {
69618
68576
  await packageListContent(client);
69619
68577
  return;
@@ -69627,15 +68585,6 @@ Available workspaces:`);
69627
68585
  await packageConfig(client, { app_id: appId, sets: flags.set });
69628
68586
  return;
69629
68587
  }
69630
- if (subcommand === "retire") {
69631
- const packageId = toolArgs;
69632
- if (!packageId) {
69633
- console.error("Usage: lotics package retire <package_id> [--undo]");
69634
- process.exit(1);
69635
- }
69636
- await packageRetire(client, { package_id: packageId, undo: flags.undo });
69637
- return;
69638
- }
69639
68588
  if (subcommand === "show") {
69640
68589
  const packageId = toolArgs;
69641
68590
  if (!packageId) {
@@ -69654,34 +68603,6 @@ Available workspaces:`);
69654
68603
  await packageEject(client, { app_id: appId });
69655
68604
  return;
69656
68605
  }
69657
- if (subcommand === "extract") {
69658
- const appId = toolArgs;
69659
- if (!appId) {
69660
- console.error("Usage: lotics package extract <app_id> [path]");
69661
- console.error("Promotes a bespoke app to a draft package project (contract + binding + templates).");
69662
- process.exit(1);
69663
- }
69664
- await packageExtract(client, { app_id: appId, targetPath: restArgs[0] });
69665
- return;
69666
- }
69667
- if (subcommand === "adopt") {
69668
- const appId = toolArgs;
69669
- if (!appId) {
69670
- console.error("Usage: lotics package adopt <app_id> [path]");
69671
- console.error("Binds the published package project (this dir, or [path]) onto the origin app.");
69672
- process.exit(1);
69673
- }
69674
- let version2;
69675
- if (flags.packageVersion !== void 0) {
69676
- version2 = Number(flags.packageVersion);
69677
- if (!Number.isInteger(version2) || version2 <= 0) {
69678
- console.error(`Invalid --version "${flags.packageVersion}" \u2014 expected a positive integer.`);
69679
- process.exit(1);
69680
- }
69681
- }
69682
- await packageAdopt(client, { app_id: appId, version: version2, projectDir: restArgs[0] });
69683
- return;
69684
- }
69685
68606
  if (subcommand === "yank") {
69686
68607
  const packageId = toolArgs;
69687
68608
  const version2 = Number(restArgs[0]);
@@ -69692,23 +68613,6 @@ Available workspaces:`);
69692
68613
  await packageYank(client, { package_id: packageId, version: version2, undo: flags.undo });
69693
68614
  return;
69694
68615
  }
69695
- if (subcommand === "fleet-upgrade") {
69696
- const packageId = toolArgs;
69697
- if (!packageId) {
69698
- console.error("Usage: lotics package fleet-upgrade <package_id> [--version N]");
69699
- process.exit(1);
69700
- }
69701
- let version2;
69702
- if (flags.packageVersion !== void 0) {
69703
- version2 = Number(flags.packageVersion);
69704
- if (!Number.isInteger(version2) || version2 <= 0) {
69705
- console.error(`Invalid --version "${flags.packageVersion}" \u2014 expected a positive integer.`);
69706
- process.exit(1);
69707
- }
69708
- }
69709
- await packageFleetUpgrade(client, { package_id: packageId, version: version2 });
69710
- return;
69711
- }
69712
68616
  if (subcommand === "doctor") {
69713
68617
  await packageDoctor(client, { app_id: toolArgs });
69714
68618
  return;
@@ -69716,78 +68620,23 @@ Available workspaces:`);
69716
68620
  if (subcommand === "rebind-role") {
69717
68621
  const appId = toolArgs;
69718
68622
  const [roleAlias, groupId] = restArgs;
69719
- if (!appId || !roleAlias || !groupId) {
69720
- console.error("Usage: lotics package rebind-role <app_id> <role_alias> <grp_id>");
69721
- process.exit(1);
69722
- }
69723
- const result = await client.rebindPackageRole(appId, {
69724
- role_alias: roleAlias,
69725
- group_id: groupId
69726
- });
69727
68623
  console.error(
69728
- `Rebound role "${result.role_alias}" \u2192 ${result.group_id}` + (result.previous_group_id ? ` (was ${result.previous_group_id})` : "") + ". Package workflows/agents were re-materialized against the new group."
68624
+ `\`lotics package rebind-role\` was folded into the upgrade grammar. Re-point a live role with:
68625
+ lotics upgrade ${appId ?? "<app_id>"} --resolve roles.${roleAlias ?? "<alias>"}=${groupId ?? "<grp_id>"}`
69729
68626
  );
69730
- return;
69731
- }
69732
- if (subcommand === "upgrade") {
69733
- const target = toolArgs;
69734
- if (!target) {
69735
- console.error(
69736
- "Usage: lotics package upgrade <app_id | pci_installation_id> [--version N] [--resolve <key>=... ...]"
69737
- );
69738
- process.exit(1);
69739
- }
69740
- let version2;
69741
- if (flags.packageVersion !== void 0) {
69742
- version2 = Number(flags.packageVersion);
69743
- if (!Number.isInteger(version2) || version2 <= 0) {
69744
- console.error(`Invalid --version "${flags.packageVersion}" \u2014 expected a positive integer.`);
69745
- process.exit(1);
69746
- }
69747
- }
69748
- if (target.startsWith("pci_")) {
69749
- await packageUpgradeKnowledge(client, {
69750
- installation_id: target,
69751
- version: version2,
69752
- resolve: flags.resolve,
69753
- bind_to: parseBindToFlags(flags.bindTo),
69754
- applyAll: flags.applyAll
69755
- });
69756
- return;
69757
- }
69758
- await packageUpgrade(client, {
69759
- app_id: target,
69760
- version: version2,
69761
- resolve: flags.resolve,
69762
- bindTo: flags.bindTo,
69763
- applyAll: flags.applyAll
69764
- });
69765
- return;
69766
- }
69767
- if (subcommand === "publish") {
69768
- await packagePublish(client, { projectDir: toolArgs, changelog: flags.message });
69769
- return;
69770
- }
69771
- if (subcommand === "sync") {
69772
- await packageSync(client, { projectDir: toolArgs });
69773
- return;
69774
- }
69775
- if (subcommand === "reset") {
69776
- await packageReset(client, { projectDir: toolArgs });
69777
- return;
68627
+ process.exit(1);
69778
68628
  }
69779
- if (subcommand === "dev") {
69780
- const projectDir = toolArgs;
69781
- let port;
69782
- let vitePort;
69783
- for (const a of restArgs) {
69784
- const portMatch = /^--port=(\d+)$/.exec(a);
69785
- const vitePortMatch = /^--vite-port=(\d+)$/.exec(a);
69786
- if (portMatch) port = Number(portMatch[1]);
69787
- else if (vitePortMatch) vitePort = Number(vitePortMatch[1]);
69788
- }
69789
- await packageDev(client, { projectDir, port, vitePort });
69790
- return;
68629
+ const movedVerbs = {
68630
+ install: "lotics install <package_id>",
68631
+ uninstall: "lotics uninstall <app_id|package_id>",
68632
+ upgrade: "lotics upgrade <app_id|package_id>",
68633
+ "fleet-upgrade": "lotics upgrade <package_id>"
68634
+ };
68635
+ if (subcommand !== void 0 && subcommand in movedVerbs) {
68636
+ console.error(
68637
+ `\`lotics package ${subcommand}\` moved to the top level \u2014 run \`${movedVerbs[subcommand]}\` instead.`
68638
+ );
68639
+ process.exit(1);
69791
68640
  }
69792
68641
  console.error(`Unknown package subcommand: ${subcommand}`);
69793
68642
  console.error("Run 'lotics package' for usage.");
@@ -69824,6 +68673,34 @@ Available workspaces:`);
69824
68673
  await appDeploy(client, { message });
69825
68674
  return;
69826
68675
  }
68676
+ if (subcommand === "publish") {
68677
+ await appPublish(client, {
68678
+ app_id: toolArgs,
68679
+ renames: flags.rename,
68680
+ changelog: flags.message,
68681
+ yes: flags.yes
68682
+ });
68683
+ return;
68684
+ }
68685
+ if (subcommand === "release") {
68686
+ if (!flags.message) {
68687
+ console.error("Usage: lotics app release [app_id|.] -m <changelog> [--yes]");
68688
+ console.error("Snapshots the origin app into its next package version (preview, then --yes to publish).");
68689
+ process.exit(1);
68690
+ }
68691
+ await appRelease(client, { app_id: toolArgs, changelog: flags.message, yes: flags.yes });
68692
+ return;
68693
+ }
68694
+ if (subcommand === "unpublish") {
68695
+ const id = toolArgs;
68696
+ if (!id) {
68697
+ console.error("Usage: lotics app unpublish <app_id|package_id> [--undo]");
68698
+ console.error("Takes a published package off the shelf; existing installations keep working.");
68699
+ process.exit(1);
68700
+ }
68701
+ await appUnpublish(client, { id, undo: flags.undo });
68702
+ return;
68703
+ }
69827
68704
  if (subcommand === "subdomain") {
69828
68705
  const newSubdomain = toolArgs;
69829
68706
  if (!newSubdomain) {
@@ -70039,8 +68916,6 @@ ${JSON.stringify(info.input_schema, null, 2)}`);
70039
68916
  console.log(JSON.stringify({ error: result.error }, null, 2));
70040
68917
  } else {
70041
68918
  console.error(result.error);
70042
- console.error(`
70043
- Hint: run "lotics tools ${toolName}" to see the expected input schema.`);
70044
68919
  }
70045
68920
  process.exit(1);
70046
68921
  }