@lotics/cli 0.75.0 → 0.76.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
@@ -29775,6 +29775,33 @@ var LoticsClient = class {
29775
29775
  body
29776
29776
  );
29777
29777
  }
29778
+ /**
29779
+ * Uninstall a package installation (backs `lotics package uninstall`). Does
29780
+ * everything DELETE does plus archives the installation's lifecycle
29781
+ * artifacts; with `archive_tables` it also archives the scaffolded entity
29782
+ * tables — refused server-side unless this installation created them
29783
+ * (provenance) and nothing else references them. Admin-only.
29784
+ */
29785
+ async uninstallAppPackage(app_id, body) {
29786
+ return this.request("POST", `/v1/apps/${encodeURIComponent(app_id)}/uninstall`, body);
29787
+ }
29788
+ /**
29789
+ * Partial-merge a package installation's config (backs `lotics package config
29790
+ * --set`). Only the provided keys change; validated against the installed
29791
+ * contract. Returns the full effective config. Admin-only.
29792
+ */
29793
+ async updateAppPackageConfig(app_id, body) {
29794
+ return this.request("PATCH", `/v1/apps/${encodeURIComponent(app_id)}/package-config`, body);
29795
+ }
29796
+ /**
29797
+ * Retire (or `undo` un-retire) a registry package (backs `lotics package
29798
+ * retire`). Retiring refuses NEW installs and hides the package from
29799
+ * non-owning orgs; existing installations keep working and may still upgrade.
29800
+ * Owner-org admin-only.
29801
+ */
29802
+ async retireAppPackage(package_id, body) {
29803
+ return this.request("POST", `/v1/app-packages/${encodeURIComponent(package_id)}/retire`, body);
29804
+ }
29778
29805
  /**
29779
29806
  * Eject an installation from its package — re-deploy the pinned version's
29780
29807
  * source as a workspace-owned app version, then sever the package link
@@ -29854,6 +29881,10 @@ var LoticsClient = class {
29854
29881
  async getAppPackage(package_id) {
29855
29882
  return this.request("GET", `/v1/app-packages/${encodeURIComponent(package_id)}`);
29856
29883
  }
29884
+ /** Version history newest-first (no contract payloads) — backs `lotics package show`. Admin-only. */
29885
+ async listAppPackageVersions(package_id) {
29886
+ return this.request("GET", `/v1/app-packages/${encodeURIComponent(package_id)}/versions`);
29887
+ }
29857
29888
  /**
29858
29889
  * Publish a new immutable package version — multipart upload of the alias-keyed
29859
29890
  * contract (JSON) + the prebuilt code bundle (a gzipped tarball carrying
@@ -29870,6 +29901,7 @@ var LoticsClient = class {
29870
29901
  "bundle.tar.gz"
29871
29902
  );
29872
29903
  if (args.changelog) formData.append("changelog", args.changelog);
29904
+ if (args.channel) formData.append("channel", args.channel);
29873
29905
  const url = `${this.baseUrl}/v1/app-packages/${encodeURIComponent(package_id)}/versions`;
29874
29906
  const response = await fetch(url, {
29875
29907
  method: "POST",
@@ -31270,6 +31302,379 @@ See https://lotics.ai/docs/app-sdk for the SDK reference.
31270
31302
  }
31271
31303
  ];
31272
31304
  }
31305
+ function buildPackageStarterOverrides(args) {
31306
+ const nameLit = JSON.stringify(args.app_name);
31307
+ return [
31308
+ {
31309
+ // The package App.tsx imports `../.lotics/app_fields` — a DETERMINISTIC
31310
+ // pure function of contract.json (generate_package_fields.ts) — so unlike
31311
+ // the app starter's fully-ignored `.lotics`, it must be committed or the
31312
+ // scaffold's own shipped CI (npm ci → typecheck/test/build on a clean
31313
+ // clone) fails on the missing module. The sync-written `.d.ts` companions
31314
+ // stay ignored: they need a live workspace and their absence only degrades
31315
+ // hooks to untyped overloads, never breaks the build.
31316
+ path: ".gitignore",
31317
+ content: `node_modules
31318
+ dist
31319
+ *.tsbuildinfo
31320
+ .DS_Store
31321
+ .lotics/*
31322
+ !.lotics/app_fields.ts
31323
+ coverage
31324
+ `
31325
+ },
31326
+ {
31327
+ path: "src/App.tsx",
31328
+ content: `import { useMemo, type ReactNode } from "react";
31329
+ import { ScrollView, View } from "react-native";
31330
+ import { useConfig, useQuery, readSelect, row } from "@lotics/app-sdk";
31331
+ import { AppRouter } from "@lotics/app-sdk/router";
31332
+ import { Text } from "@lotics/ui/text";
31333
+ import { Card } from "@lotics/ui/card";
31334
+ import { Button } from "@lotics/ui/button";
31335
+ import { OptionBadge } from "@lotics/ui/option_badge";
31336
+ import { Skeleton } from "@lotics/ui/skeleton";
31337
+ import { EmptyState } from "@lotics/ui/empty_state";
31338
+ import { Callout, CalloutTitle, CalloutText, CalloutActions } from "@lotics/ui/callout";
31339
+ import { F, OPT } from "../.lotics/app_fields";
31340
+
31341
+ // This is a PACKAGE starter \u2014 a workspace-agnostic blueprint installed into many
31342
+ // workspaces, each binding the contract's aliases to DIFFERENT concrete ids. So a
31343
+ // screen never hardcodes a \`fld_\u2026\`/\`opt_\u2026\` id: it addresses a field by contract
31344
+ // alias through \`F\` and a select option through \`OPT\`, both from the generated
31345
+ // \`.lotics/app_fields.ts\` (which resolves every alias to THIS installation's id at
31346
+ // module load from the binding). The aliases come from contract.json \u2014 entity
31347
+ // \`item\`, fields name/notes/status, options open/done, query \`items\`, config knob
31348
+ // \`heading\`. Edit contract.json, then \`lotics package sync\` to regenerate F/OPT.
31349
+
31350
+ interface Item {
31351
+ id: string;
31352
+ name: string;
31353
+ notes: string;
31354
+ status: ReturnType<typeof readSelect>[number] | null;
31355
+ done: boolean;
31356
+ }
31357
+
31358
+ // The \`items\` query is a bare from_entity, so a row is keyed by FIELD id: read a
31359
+ // cell as \`r[F.ITEM.<field>]\` and decode it with the SDK's pure readers (\`row.text\`,
31360
+ // \`readSelect\`). \`OPT.ITEM.status.done\` is this install's \`opt_\u2026\` id, so comparing
31361
+ // the cell's stored option key to it marks a done row.
31362
+ function decode(r: Record<string, unknown>): Item {
31363
+ const status = readSelect(r[F.ITEM.status])[0] ?? null;
31364
+ return {
31365
+ id: row.text(r.__source_record_id),
31366
+ name: row.text(r[F.ITEM.name]),
31367
+ notes: row.text(r[F.ITEM.notes]),
31368
+ status,
31369
+ done: status?.key === OPT.ITEM.status.done,
31370
+ };
31371
+ }
31372
+
31373
+ // Outer <View flex:1> claims the iframe height (index.html sets html/body/#root to
31374
+ // 100% + #root is a flex column); the list scrolls beneath the heading. Keep this
31375
+ // flex chain plain (not @lotics/ui/stack) so a fill-remaining-space child gets height.
31376
+ function Screen({ children }: { children: ReactNode }) {
31377
+ return (
31378
+ <View style={{ flex: 1 }}>
31379
+ <ScrollView contentContainerStyle={{ padding: 24, alignItems: "center" }}>
31380
+ <View style={{ maxWidth: 640, width: "100%", gap: 16 }}>{children}</View>
31381
+ </ScrollView>
31382
+ </View>
31383
+ );
31384
+ }
31385
+
31386
+ function ItemsScreen() {
31387
+ // \`heading\` is a contract config knob \u2014 an installation overrides it and
31388
+ // useConfig() renders the customized value; the literal here is only the fallback.
31389
+ const { config } = useConfig({ heading: ${nameLit} });
31390
+ const itemsQ = useQuery("items");
31391
+ const items = useMemo(() => itemsQ.rows.map(decode), [itemsQ.rows]);
31392
+ // First-load only: a skeleton while the very first fetch is in flight with no rows
31393
+ // yet \u2014 never blank already-loaded rows to a spinner on a background refetch.
31394
+ const firstLoad = itemsQ.loading && items.length === 0;
31395
+
31396
+ return (
31397
+ <Screen>
31398
+ <Text size="xxl" weight="semibold" level={1}>
31399
+ {config.heading}
31400
+ </Text>
31401
+
31402
+ {itemsQ.error ? (
31403
+ <Callout tone="error">
31404
+ <CalloutTitle>Couldn't load items</CalloutTitle>
31405
+ <CalloutText>{itemsQ.error}</CalloutText>
31406
+ <CalloutActions>
31407
+ <Button title="Try again" onPress={() => itemsQ.refetch()} />
31408
+ </CalloutActions>
31409
+ </Callout>
31410
+ ) : firstLoad ? (
31411
+ <View style={{ gap: 10 }}>
31412
+ {[0, 1, 2].map((i) => (
31413
+ <Skeleton key={i} height={64} radius={12} />
31414
+ ))}
31415
+ </View>
31416
+ ) : items.length > 0 ? (
31417
+ <View style={{ gap: 10 }}>
31418
+ {items.map((item) => (
31419
+ <Card key={item.id}>
31420
+ <View style={{ flexDirection: "row", alignItems: "center", gap: 12 }}>
31421
+ <View style={{ flex: 1, gap: 2 }}>
31422
+ <Text weight="medium" color={item.done ? "muted" : undefined}>
31423
+ {item.name}
31424
+ </Text>
31425
+ {item.notes.length > 0 ? (
31426
+ <Text size="sm" color="muted" numberOfLines={1}>
31427
+ {item.notes}
31428
+ </Text>
31429
+ ) : null}
31430
+ </View>
31431
+ <OptionBadge value={item.status} />
31432
+ </View>
31433
+ </Card>
31434
+ ))}
31435
+ </View>
31436
+ ) : (
31437
+ <EmptyState
31438
+ icon="list-checks"
31439
+ message="No items yet"
31440
+ hint="Rows in this package's table appear here."
31441
+ />
31442
+ )}
31443
+ </Screen>
31444
+ );
31445
+ }
31446
+
31447
+ const routes = [{ path: "/", element: <ItemsScreen /> }];
31448
+
31449
+ export default function App() {
31450
+ return <AppRouter routes={routes} />;
31451
+ }
31452
+ `
31453
+ },
31454
+ {
31455
+ path: "src/App.test.tsx",
31456
+ content: `import { describe, test, expect, beforeEach, afterEach, vi } from "vitest";
31457
+ import { render, screen, cleanup } from "@testing-library/react";
31458
+ import App from "./App";
31459
+
31460
+ // Mock @lotics/app-sdk \u2014 the app's one external boundary (data + RPC + the package
31461
+ // binding). \`importOriginal\` keeps the PURE cell readers (\`row\`, \`readSelect\`) real,
31462
+ // so mock rows decode exactly like wire rows; only the hooks and getAppBinding are
31463
+ // stubbed. The binding ids below are what F.ITEM.* / OPT.ITEM.status.* resolve to;
31464
+ // the query rows are keyed by the SAME ids.
31465
+ const h = vi.hoisted(() => ({
31466
+ state: {
31467
+ rows: [] as Array<Record<string, unknown>>,
31468
+ loading: false,
31469
+ error: null as string | null,
31470
+ },
31471
+ binding: {
31472
+ fields: { "item.name": "fld_name", "item.notes": "fld_notes", "item.status": "fld_status" },
31473
+ options: { "item.status:open": "opt_open", "item.status:done": "opt_done" },
31474
+ roles: {},
31475
+ },
31476
+ }));
31477
+
31478
+ vi.mock("@lotics/app-sdk", async (importOriginal) => {
31479
+ const actual = await importOriginal<typeof import("@lotics/app-sdk")>();
31480
+ return {
31481
+ ...actual,
31482
+ getAppBinding: async () => h.binding,
31483
+ useConfig: (defaults: Record<string, unknown>) => ({ config: defaults, loading: false }),
31484
+ useQuery: () => ({
31485
+ rows: h.state.rows,
31486
+ loading: h.state.loading,
31487
+ isValidating: false,
31488
+ error: h.state.error,
31489
+ refetch: () => {},
31490
+ }),
31491
+ };
31492
+ });
31493
+
31494
+ const rowOf = (id: string, name: string, notes: string, statusKey: string) => ({
31495
+ __source_record_id: id,
31496
+ fld_name: name,
31497
+ fld_notes: notes,
31498
+ fld_status: [{ key: statusKey, label: statusKey === "opt_done" ? "Done" : "Open" }],
31499
+ });
31500
+
31501
+ beforeEach(() => {
31502
+ h.state.rows = [];
31503
+ h.state.loading = false;
31504
+ h.state.error = null;
31505
+ });
31506
+
31507
+ // vitest globals are off, so @testing-library/react's automatic afterEach cleanup is
31508
+ // never registered \u2014 clean up explicitly or renders accumulate across tests and a
31509
+ // second render's duplicate matches fail getByText.
31510
+ afterEach(cleanup);
31511
+
31512
+ describe("App", () => {
31513
+ test("renders the heading and the loaded items with status badges", () => {
31514
+ h.state.rows = [
31515
+ rowOf("1", "First item", "with a note", "opt_open"),
31516
+ rowOf("2", "Second item", "", "opt_done"),
31517
+ ];
31518
+ render(<App />);
31519
+
31520
+ expect(screen.getByText(${nameLit})).toBeTruthy();
31521
+ expect(screen.getByText("First item")).toBeTruthy();
31522
+ expect(screen.getByText("Second item")).toBeTruthy();
31523
+ expect(screen.getByText("with a note")).toBeTruthy();
31524
+ expect(screen.getByText("Open")).toBeTruthy();
31525
+ expect(screen.getByText("Done")).toBeTruthy();
31526
+ expect(screen.queryByText("No items yet")).toBeNull();
31527
+ });
31528
+
31529
+ test("shows the empty state when there are no items", () => {
31530
+ render(<App />);
31531
+ expect(screen.getByText("No items yet")).toBeTruthy();
31532
+ });
31533
+
31534
+ test("does not flash the list or empty state on first load", () => {
31535
+ h.state.loading = true;
31536
+ render(<App />);
31537
+ expect(screen.queryByText("No items yet")).toBeNull();
31538
+ });
31539
+
31540
+ test("surfaces a load error loudly", () => {
31541
+ h.state.error = "Network unreachable";
31542
+ render(<App />);
31543
+ expect(screen.getByText("Couldn't load items")).toBeTruthy();
31544
+ expect(screen.getByText("Network unreachable")).toBeTruthy();
31545
+ });
31546
+ });
31547
+ `
31548
+ },
31549
+ {
31550
+ path: "README.md",
31551
+ content: `# ${escapeHtml(args.app_name)}
31552
+
31553
+ A Lotics **app package** \u2014 a versioned, installable blueprint (a \`contract.json\`
31554
+ data model + app source) that installs into many workspaces. Authored locally,
31555
+ published and run through the \`lotics package\` CLI. See \`docs/app_packages.md\`.
31556
+
31557
+ ## Dev loop
31558
+
31559
+ \`\`\`bash
31560
+ lotics workspace create "${escapeHtml(args.app_name)} dev" --dev # a throwaway dev workspace
31561
+ lotics package dev --workspace <dev_ws> # sync into it + run the dev server
31562
+ # edit contract.json \u2192 re-run \`lotics package sync\` to migrate + re-materialize
31563
+ # edit src/* \u2192 hot reload
31564
+ \`\`\`
31565
+
31566
+ \`sync\` / \`dev\` regenerate \`.lotics/app_fields.ts\` (the runtime \`F\` / \`OPT\` / \`ROLE\`
31567
+ surface) from \`contract.json\`, plus the typed \`.lotics/app_{queries,workflows,agents}.d.ts\`
31568
+ companions from the live installation \u2014 so \`useQuery\` / \`useWorkflow\` stay typed in the
31569
+ dev loop.
31570
+
31571
+ ## Quality
31572
+
31573
+ \`\`\`bash
31574
+ npm run typecheck
31575
+ npm run lint
31576
+ npm test
31577
+ \`\`\`
31578
+
31579
+ ## Publish
31580
+
31581
+ \`\`\`bash
31582
+ lotics package publish -m "v1" # build + publish an immutable version
31583
+ lotics package install <package_id> # install into any workspace
31584
+ \`\`\`
31585
+
31586
+ ## The app screen
31587
+
31588
+ \`src/App.tsx\` reads the contract BY ALIAS through \`F\` / \`OPT\` (from the generated
31589
+ \`.lotics/app_fields.ts\`) \u2014 never a raw \`fld_\u2026\` / \`opt_\u2026\` id, since every install binds
31590
+ different concrete ids. It lists the \`items\` query and renders the \`heading\` config knob.
31591
+ \`@lotics/ui\` primitives render via react-native-web (aliased in \`vite.config.ts\`).
31592
+
31593
+ ## Contract reference
31594
+
31595
+ \`contract.json\` is the package's alias-keyed data model
31596
+ (schema: \`@lotics/shared/schemas/app_packages\`). Every cross-reference is by **alias**;
31597
+ the materializer resolves alias \u2192 this workspace's concrete id at install.
31598
+
31599
+ ### Aliases
31600
+
31601
+ - Entity / field / option / role / template / config aliases are lowercase slugs
31602
+ matching \`^[a-z][a-z0-9_]*$\`. A field's fully-qualified key is \`<entity>.<field>\`
31603
+ (e.g. \`item.status\`); a select option's is \`<entity>.<field>:<option>\`
31604
+ (e.g. \`item.status:done\`).
31605
+ - Query / workflow / agent aliases are runtime lookup keys (1\u2013200 chars) \u2014 the app
31606
+ source invokes them verbatim (\`useQuery("items")\`), so renaming one severs the call.
31607
+
31608
+ ### Entities & fields
31609
+
31610
+ \`\`\`jsonc
31611
+ { "alias": "item", "label": "Item", "fields": [
31612
+ { "alias": "name", "label": "Name", "type": "text", "required": true },
31613
+ { "alias": "status", "label": "Status", "type": "select",
31614
+ "options": [ { "alias": "open", "label": "Open", "color": "blue" },
31615
+ { "alias": "done", "label": "Done", "color": "green" } ] } ] }
31616
+ \`\`\`
31617
+
31618
+ Field \`type\`: \`text\`, \`number\`, \`date\`, \`boolean\`, \`select\` (+ \`options\`),
31619
+ \`select_member\`, \`select_record_link\` (\`target_entity\` = an entity alias), \`files\`,
31620
+ \`formula\` (expression references same-entity fields as \`{alias}\`), \`rollup\`, \`lookup\`,
31621
+ \`autonumber\`. \`required\` is advisory (app / workflow-layer UX only \u2014 the table model
31622
+ has no required constraint). Select \`options\` are \`{ alias, label, color }\`.
31623
+
31624
+ ### Queries
31625
+
31626
+ Alias-form AST \u2014 the same node kinds as the runtime query engine, except a
31627
+ \`from_table\` node carries \`from_entity\` (an entity alias) instead of a \`table_id\`:
31628
+
31629
+ \`\`\`jsonc
31630
+ { "alias": "items", "ast": {
31631
+ "kind": "from_table", "from_entity": "item",
31632
+ "sort": [ { "field_key": "name", "order": "asc" } ] } }
31633
+ \`\`\`
31634
+
31635
+ ### Workflows
31636
+
31637
+ The sole app-side mutation path. Each workflow has a \`trigger\`, typed \`inputs\` /
31638
+ \`outputs\`, and a JS-subset \`body\`.
31639
+
31640
+ - Trigger \u2014 app-invoked or table-lifecycle:
31641
+ - \`{ "type": "app" }\` \u2014 called by alias from the app (\`useWorkflow("<alias>")\`).
31642
+ - \`{ "type": "entity_lifecycle", "entity": "<entity>", "event": "<event>" }\` \u2014 fires
31643
+ on the bound table. \`event\` \u2208 \`before_create\`, \`after_create\`, \`before_update\`,
31644
+ \`after_update\`, \`before_delete\`, \`after_delete\`.
31645
+ - Typed \`inputs\` (\`text\` / \`number\` / \`date\` / \`member\` / \`select\` / \`record_link\`,
31646
+ plus \`object\` / \`array\`):
31647
+ - \`record_link\` \u2014 its \`table_id\` names an **entity alias**.
31648
+ - \`member\` \u2014 its \`group\` names a **role alias**.
31649
+ - \`select\` \u2014 an option \`value\` in \`entity.field:option\` form binds to that option's
31650
+ id (a plain value is a literal, left as-is).
31651
+ - Body **sentinel tokens** \u2014 a body addresses package objects by reserved tokens the
31652
+ materializer rewrites to concrete ids at install. Always inside a **string literal**:
31653
+ - \`@@entity:<entity>@@\`
31654
+ - \`@@field:<entity>.<field>@@\`
31655
+ - \`@@option:<entity>.<field>:<option>@@\`
31656
+ - \`@@role:<role>@@\`
31657
+ - \`@@template:<template>@@\`
31658
+
31659
+ ### Config knobs
31660
+
31661
+ Typed customization knobs the app reads via \`useConfig()\`. Each has an \`alias\`,
31662
+ \`label\`, \`type\`, and \`default\`:
31663
+
31664
+ - \`text\` \u2192 a string default; \`boolean\` \u2192 a boolean; \`number\` \u2192 a number;
31665
+ \`color\` \u2192 a palette color token; \`select\` \u2192 \`options: [{ value, label }]\` with the
31666
+ \`default\` being one of those \`value\`s.
31667
+
31668
+ \`\`\`jsonc
31669
+ { "alias": "heading", "label": "List heading", "type": "text", "default": "Items" }
31670
+ \`\`\`
31671
+
31672
+ Publish validates the whole contract (alias uniqueness, every cross-reference
31673
+ resolves, sentinel tokens name declared objects) before storing the version.
31674
+ `
31675
+ }
31676
+ ];
31677
+ }
31273
31678
  function escapeHtml(s) {
31274
31679
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
31275
31680
  }
@@ -33716,6 +34121,9 @@ async function packageNew(args) {
33716
34121
  ui_version: uiLatest ? `^${uiLatest}` : void 0,
33717
34122
  sdk_version: packageSdkRange(sdkLatest)
33718
34123
  });
34124
+ const overrides = new Map(
34125
+ buildPackageStarterOverrides({ app_name: args.name }).map((f) => [f.path, f.content])
34126
+ );
33719
34127
  for (const file of files) {
33720
34128
  const fullPath = path7.join(targetPath, file.path);
33721
34129
  if (file.path === "package.json") {
@@ -33733,7 +34141,7 @@ async function packageNew(args) {
33733
34141
  continue;
33734
34142
  }
33735
34143
  fs6.mkdirSync(path7.dirname(fullPath), { recursive: true });
33736
- fs6.writeFileSync(fullPath, file.content);
34144
+ fs6.writeFileSync(fullPath, overrides.get(file.path) ?? file.content);
33737
34145
  }
33738
34146
  fs6.writeFileSync(
33739
34147
  path7.join(targetPath, CONTRACT_FILE),
@@ -33777,7 +34185,8 @@ async function publishVersion(client, projectDir, opts) {
33777
34185
  const version = await client.publishAppPackageVersion(packageId, {
33778
34186
  contract,
33779
34187
  bundle,
33780
- changelog: opts.changelog ?? null
34188
+ changelog: opts.changelog ?? null,
34189
+ channel: opts.channel ?? "release"
33781
34190
  });
33782
34191
  project.manifest.version = version.version;
33783
34192
  writePackageManifest(projectDir, project);
@@ -33813,7 +34222,8 @@ async function syncToDevWorkspace(client, projectDir) {
33813
34222
  assertDevWorkspace(workspace);
33814
34223
  writePackageAppFields(projectDir);
33815
34224
  const { package_id, version } = await publishVersion(client, projectDir, {
33816
- changelog: "dev sync"
34225
+ changelog: "dev sync",
34226
+ channel: "dev"
33817
34227
  });
33818
34228
  const project = readPackageProject(projectDir);
33819
34229
  const existing = project.manifest.dev[devWorkspaceId];
@@ -33830,6 +34240,11 @@ async function syncToDevWorkspace(client, projectDir) {
33830
34240
  project.manifest.dev[devWorkspaceId] = { app_id: appId, version };
33831
34241
  writePackageManifest(projectDir, project);
33832
34242
  const installed = await client.getApp(appId);
34243
+ writeAppDts(projectDir, {
34244
+ workflows: installed.workflows ?? void 0,
34245
+ queries: installed.queries ?? void 0,
34246
+ agents: installed.agents ?? void 0
34247
+ });
33833
34248
  return { app_id: appId, app_name: installed.name, workspace_id: devWorkspaceId, version };
33834
34249
  }
33835
34250
  async function packageSync(client, args) {
@@ -34031,13 +34446,37 @@ async function packageUpgrade(client, args) {
34031
34446
  });
34032
34447
  console.error(`Upgraded ${app.name} \u2192 v${app.package_version} (${app.id}).`);
34033
34448
  }
34034
- async function packageInstall(client, args) {
34449
+ function trustBadge(pkg2) {
34450
+ if (pkg2.is_official) return "official Lotics package";
34451
+ if (pkg2.owned_by_caller === true) return "your organization's package";
34452
+ return "third-party package (runs under your authority once installed)";
34453
+ }
34454
+ async function packageShow(client, args) {
34035
34455
  const pkg2 = await client.getAppPackage(args.package_id);
34456
+ const { versions } = await client.listAppPackageVersions(args.package_id);
34457
+ console.error(`${pkg2.name} (${pkg2.id}) \u2014 ${trustBadge(pkg2)}`);
34458
+ if (pkg2.description) console.error(` ${pkg2.description}`);
34459
+ if (pkg2.retired_at) console.error(` RETIRED ${pkg2.retired_at}`);
34036
34460
  console.error(
34037
- pkg2.is_official ? `Installing ${pkg2.name} \u2014 official Lotics package...` : `Installing ${pkg2.name} \u2014 third-party package (runs under your authority once installed)...`
34461
+ ` latest installable: ${pkg2.latest_version > 0 ? `v${pkg2.latest_version}` : "none"}`
34038
34462
  );
34463
+ console.error("");
34464
+ for (const v of versions) {
34465
+ const marks = [
34466
+ v.version === pkg2.latest_version ? "*" : " ",
34467
+ v.channel === "dev" ? "dev" : " ",
34468
+ v.yanked_at ? "YANKED" : " "
34469
+ ].join(" ");
34470
+ console.log(`${marks} v${v.version} ${v.created_at} ${v.changelog ?? ""}`.trimEnd());
34471
+ }
34472
+ if (versions.length === 0) console.error(" (no published versions)");
34473
+ }
34474
+ async function packageInstall(client, args) {
34475
+ const pkg2 = await client.getAppPackage(args.package_id);
34476
+ console.error(`Installing ${pkg2.name} \u2014 ${trustBadge(pkg2)}...`);
34039
34477
  const app = await client.installAppPackage(args.package_id, {
34040
- ...args.version !== void 0 ? { version: args.version } : {}
34478
+ ...args.version !== void 0 ? { version: args.version } : {},
34479
+ ...args.config !== void 0 ? { config: args.config } : {}
34041
34480
  });
34042
34481
  const versionLabel = app.package_version !== null ? `v${app.package_version}` : "(unknown version)";
34043
34482
  console.error(
@@ -34053,6 +34492,104 @@ async function packageEject(client, args) {
34053
34492
  console.error(" Its data model, queries, workflows, and templates are unchanged.");
34054
34493
  console.error(" Pull the pinned source for local editing: lotics app pull " + app.id);
34055
34494
  }
34495
+ function parseConfigAssignment(entry, flag, knownType) {
34496
+ const eq = entry.indexOf("=");
34497
+ if (eq <= 0 || eq === entry.length - 1) {
34498
+ throw new Error(`Invalid ${flag} "${entry}" \u2014 expected key=value.`);
34499
+ }
34500
+ const key = entry.slice(0, eq);
34501
+ const raw = entry.slice(eq + 1);
34502
+ if (knownType === "number") {
34503
+ const n = Number(raw);
34504
+ if (!Number.isFinite(n)) throw new Error(`Config key "${key}" is a number knob \u2014 "${raw}" is not numeric.`);
34505
+ return { key, value: n };
34506
+ }
34507
+ if (knownType === "boolean") {
34508
+ if (raw !== "true" && raw !== "false") {
34509
+ throw new Error(`Config key "${key}" is a boolean knob \u2014 expected true or false, got "${raw}".`);
34510
+ }
34511
+ return { key, value: raw === "true" };
34512
+ }
34513
+ if (knownType === "string") return { key, value: raw };
34514
+ if (raw === "true" || raw === "false") return { key, value: raw === "true" };
34515
+ if (/^-?\d+(\.\d+)?$/.test(raw)) return { key, value: Number(raw) };
34516
+ return { key, value: raw };
34517
+ }
34518
+ function parseInstallConfigFlags(config) {
34519
+ const out = {};
34520
+ for (const entry of config) {
34521
+ const { key, value } = parseConfigAssignment(entry, "--config");
34522
+ out[key] = value;
34523
+ }
34524
+ return out;
34525
+ }
34526
+ async function packageConfig(client, args) {
34527
+ const app = await client.getApp(args.app_id);
34528
+ if (!app.package_id) {
34529
+ throw new Error(`App ${args.app_id} is not a package installation \u2014 it has no config.`);
34530
+ }
34531
+ const current = app.config ?? {};
34532
+ if (args.sets.length === 0) {
34533
+ const keys2 = Object.keys(current).sort();
34534
+ if (keys2.length === 0) {
34535
+ console.error(`${app.name} (${app.id}) \u2014 no config knobs.`);
34536
+ return;
34537
+ }
34538
+ console.error(`${app.name} (${app.id}) config:`);
34539
+ for (const key of keys2) console.log(` ${key} = ${JSON.stringify(current[key])}`);
34540
+ return;
34541
+ }
34542
+ const overrides = {};
34543
+ for (const entry of args.sets) {
34544
+ const key = entry.slice(0, Math.max(0, entry.indexOf("=")));
34545
+ const knownType = typeof current[key];
34546
+ const { value } = parseConfigAssignment(
34547
+ entry,
34548
+ "--set",
34549
+ knownType === "number" || knownType === "boolean" || knownType === "string" ? knownType : void 0
34550
+ );
34551
+ overrides[key] = value;
34552
+ }
34553
+ const { config } = await client.updateAppPackageConfig(args.app_id, { config: overrides });
34554
+ console.error(`Updated config for ${app.name} (${app.id}):`);
34555
+ for (const key of Object.keys(config).sort()) {
34556
+ const marker = key in overrides ? " *" : "";
34557
+ console.log(` ${key} = ${JSON.stringify(config[key])}${marker}`);
34558
+ }
34559
+ }
34560
+ async function packageUninstall(client, args) {
34561
+ const app = await client.getApp(args.app_id);
34562
+ if (!app.package_id) {
34563
+ throw new Error(
34564
+ `App ${args.app_id} is not a package installation \u2014 use the app delete flow for a bespoke app.`
34565
+ );
34566
+ }
34567
+ const lifecycleCount = Object.keys(app.binding?.workflows ?? {}).length;
34568
+ const boundCount = Object.keys(app.workflows ?? {}).length;
34569
+ const tableCount = Object.keys(app.binding?.entities ?? {}).length;
34570
+ console.error(`Uninstalling ${app.name} (${app.id}):`);
34571
+ console.error(` Archives ${boundCount + lifecycleCount} workflow(s) (${boundCount} app, ${lifecycleCount} lifecycle).`);
34572
+ console.error(
34573
+ args.archive_tables ? ` Archives ${tableCount} scaffolded table(s) \u2014 refused if they were adopted or are still referenced.` : ` Leaves the data model (${tableCount} table(s)) intact. Pass --archive-tables to also archive them.`
34574
+ );
34575
+ const result = await client.uninstallAppPackage(args.app_id, {
34576
+ archive_tables: args.archive_tables
34577
+ });
34578
+ console.error(`Uninstalled ${app.name} (${app.id}).`);
34579
+ if (result.archived_table_ids.length > 0) {
34580
+ console.error(` Archived tables: ${result.archived_table_ids.join(", ")}`);
34581
+ }
34582
+ }
34583
+ async function packageRetire(client, args) {
34584
+ const pkg2 = await client.retireAppPackage(args.package_id, { undo: args.undo });
34585
+ if (pkg2.retired_at !== null) {
34586
+ console.error(
34587
+ `Retired ${pkg2.name} (${pkg2.id}). New installs refuse it and it is hidden from other orgs; existing installations keep working and may still upgrade.`
34588
+ );
34589
+ } else {
34590
+ console.error(`Un-retired ${pkg2.name} (${pkg2.id}) \u2014 installable again.`);
34591
+ }
34592
+ }
34056
34593
  async function packageExtract(client, args) {
34057
34594
  const app = await client.getApp(args.app_id);
34058
34595
  if (!app.current_version_id) {
@@ -34261,6 +34798,10 @@ function parseArgs(argv) {
34261
34798
  cleanup: false,
34262
34799
  packageVersion: void 0,
34263
34800
  resolve: [],
34801
+ config: [],
34802
+ set: [],
34803
+ archiveTables: false,
34804
+ undo: false,
34264
34805
  version: false,
34265
34806
  help: false
34266
34807
  };
@@ -34338,6 +34879,28 @@ function parseArgs(argv) {
34338
34879
  flags.resolve.push(value);
34339
34880
  break;
34340
34881
  }
34882
+ case "--config": {
34883
+ const value = argv[++i2];
34884
+ if (value === void 0 || value.startsWith("-")) {
34885
+ throw new Error("--config requires a value: key=value (repeatable).");
34886
+ }
34887
+ flags.config.push(value);
34888
+ break;
34889
+ }
34890
+ case "--set": {
34891
+ const value = argv[++i2];
34892
+ if (value === void 0 || value.startsWith("-")) {
34893
+ throw new Error("--set requires a value: key=value (repeatable).");
34894
+ }
34895
+ flags.set.push(value);
34896
+ break;
34897
+ }
34898
+ case "--archive-tables":
34899
+ flags.archiveTables = true;
34900
+ break;
34901
+ case "--undo":
34902
+ flags.undo = true;
34903
+ break;
34341
34904
  case "--version":
34342
34905
  case "-v": {
34343
34906
  const next = argv[i2 + 1];
@@ -51010,9 +51573,18 @@ COMMANDS
51010
51573
  dev server (--workspace <dev_ws> selects it)
51011
51574
  lotics package sync [path] Re-sync (additive migrate + materialize) into a dev ws
51012
51575
  lotics package reset [path] DEV-ONLY: drop scaffolded tables + re-scaffold clean
51013
- lotics package install <package> [--version N]
51576
+ lotics package install <package> [--version N] [--config key=value ...]
51014
51577
  Install an app package into this workspace
51015
51578
  (scaffolds the data model + deploys + materializes)
51579
+ lotics package uninstall <app_id> [--archive-tables]
51580
+ Remove an installation (archives artifacts; with the
51581
+ flag also archives the scaffolded tables it created)
51582
+ lotics package config <app_id> [--set key=value ...]
51583
+ Show or edit an installation's config knobs
51584
+ lotics package show <package_id> Registry metadata + version history
51585
+ lotics package retire <package_id> [--undo]
51586
+ Retire a package (refuse new installs, hide from other
51587
+ orgs); existing installations keep working + upgrade
51016
51588
  lotics package upgrade <app_id> [--version N] [--resolve <key>=recreate|revert|keep|<id> ...]
51017
51589
  Preview-then-apply a package upgrade; refuses while
51018
51590
  any drift/modified finding lacks a --resolve, and
@@ -51554,7 +52126,11 @@ async function main() {
51554
52126
  console.error(" lotics package dev [path] [--workspace <ws>] Sync into a dev workspace + run the app dev server");
51555
52127
  console.error(" lotics package sync [path] [--workspace <ws>] Re-sync (additive migrate + materialize) into a dev workspace");
51556
52128
  console.error(" lotics package reset [path] [--workspace <ws>] DEV-ONLY: drop scaffolded tables + re-scaffold clean");
51557
- console.error(" lotics package install <package> [--version N] Install a package into this workspace");
52129
+ console.error(" lotics package install <package> [--version N] [--config key=value ...] Install a package into this workspace");
52130
+ console.error(" lotics package uninstall <app_id> [--archive-tables] Remove an installation (opt-in table archival)");
52131
+ console.error(" lotics package config <app_id> [--set key=value ...] Show or edit an installation's config knobs");
52132
+ console.error(" lotics package show <package_id> Registry metadata + version history (channel, yank, changelog)");
52133
+ console.error(" lotics package retire <package_id> [--undo] Retire a package (refuse new installs; installs keep working)");
51558
52134
  console.error(" lotics package upgrade <app_id> [--version N] [--resolve ns.alias=recreate|<id>] Preview + apply an upgrade");
51559
52135
  console.error(" lotics package doctor [app_id] Health: version pin vs latest + binding drift");
51560
52136
  console.error(" lotics package rebind-role <app_id> <alias> <grp_id> Re-point a package role at another group");
@@ -51591,16 +52167,6 @@ async function main() {
51591
52167
  }
51592
52168
  const { client, ctx } = requireClient(flags);
51593
52169
  if (command === "workspace") {
51594
- if (subcommand === "yank") {
51595
- const [packageId, versionRaw, maybeUndo] = toolArgs ? toolArgs.split(/\s+/) : [];
51596
- const version = Number(versionRaw);
51597
- if (!packageId || !Number.isInteger(version) || version <= 0) {
51598
- console.error("Usage: lotics package yank <package_id> <version> [--undo]");
51599
- process.exit(1);
51600
- }
51601
- await packageYank(client, { package_id: packageId, version, undo: maybeUndo === "--undo" });
51602
- return;
51603
- }
51604
52170
  if (subcommand === "doctor") {
51605
52171
  await resolveWorkspace(client, ctx);
51606
52172
  const dangling = await client.getWorkspaceDanglingReferences();
@@ -51729,7 +52295,48 @@ Available workspaces:`);
51729
52295
  process.exit(1);
51730
52296
  }
51731
52297
  }
51732
- await packageInstall(client, { package_id: packageId, version });
52298
+ const config = flags.config.length > 0 ? parseInstallConfigFlags(flags.config) : void 0;
52299
+ await packageInstall(client, {
52300
+ package_id: packageId,
52301
+ version,
52302
+ ...config !== void 0 ? { config } : {}
52303
+ });
52304
+ return;
52305
+ }
52306
+ if (subcommand === "uninstall") {
52307
+ const appId = toolArgs;
52308
+ if (!appId) {
52309
+ console.error("Usage: lotics package uninstall <app_id> [--archive-tables]");
52310
+ process.exit(1);
52311
+ }
52312
+ await packageUninstall(client, { app_id: appId, archive_tables: flags.archiveTables });
52313
+ return;
52314
+ }
52315
+ if (subcommand === "config") {
52316
+ const appId = toolArgs;
52317
+ if (!appId) {
52318
+ console.error("Usage: lotics package config <app_id> [--set key=value ...]");
52319
+ process.exit(1);
52320
+ }
52321
+ await packageConfig(client, { app_id: appId, sets: flags.set });
52322
+ return;
52323
+ }
52324
+ if (subcommand === "retire") {
52325
+ const packageId = toolArgs;
52326
+ if (!packageId) {
52327
+ console.error("Usage: lotics package retire <package_id> [--undo]");
52328
+ process.exit(1);
52329
+ }
52330
+ await packageRetire(client, { package_id: packageId, undo: flags.undo });
52331
+ return;
52332
+ }
52333
+ if (subcommand === "show") {
52334
+ const packageId = toolArgs;
52335
+ if (!packageId) {
52336
+ console.error("Usage: lotics package show <package_id>");
52337
+ process.exit(1);
52338
+ }
52339
+ await packageShow(client, { package_id: packageId });
51733
52340
  return;
51734
52341
  }
51735
52342
  if (subcommand === "eject") {
@@ -51769,6 +52376,16 @@ Available workspaces:`);
51769
52376
  await packageAdopt(client, { app_id: appId, version, projectDir: restArgs[0] });
51770
52377
  return;
51771
52378
  }
52379
+ if (subcommand === "yank") {
52380
+ const packageId = toolArgs;
52381
+ const version = Number(restArgs[0]);
52382
+ if (!packageId || !Number.isInteger(version) || version <= 0) {
52383
+ console.error("Usage: lotics package yank <package_id> <version> [--undo]");
52384
+ process.exit(1);
52385
+ }
52386
+ await packageYank(client, { package_id: packageId, version, undo: flags.undo });
52387
+ return;
52388
+ }
51772
52389
  if (subcommand === "fleet-upgrade") {
51773
52390
  const packageId = toolArgs;
51774
52391
  if (!packageId) {