@lotics/cli 0.74.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
@@ -29818,6 +29845,18 @@ var LoticsClient = class {
29818
29845
  * per-installation consent flow. Backs `lotics package fleet-upgrade`.
29819
29846
  * Admin-only; org-scoped (no workspace header needed).
29820
29847
  */
29848
+ /**
29849
+ * Yank / unyank a published package version — refuses NEW installs/upgrades/
29850
+ * adopts targeting it; pinned installations keep running. Owner-org
29851
+ * admin-only. Backs `lotics package yank`.
29852
+ */
29853
+ async yankAppPackageVersion(package_id, version, yanked) {
29854
+ return this.request(
29855
+ "POST",
29856
+ `/v1/app-packages/${encodeURIComponent(package_id)}/versions/${version}/yank`,
29857
+ { yanked }
29858
+ );
29859
+ }
29821
29860
  async fleetUpgradeAppPackage(package_id, body) {
29822
29861
  return this.request(
29823
29862
  "POST",
@@ -29842,6 +29881,10 @@ var LoticsClient = class {
29842
29881
  async getAppPackage(package_id) {
29843
29882
  return this.request("GET", `/v1/app-packages/${encodeURIComponent(package_id)}`);
29844
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
+ }
29845
29888
  /**
29846
29889
  * Publish a new immutable package version — multipart upload of the alias-keyed
29847
29890
  * contract (JSON) + the prebuilt code bundle (a gzipped tarball carrying
@@ -29858,6 +29901,7 @@ var LoticsClient = class {
29858
29901
  "bundle.tar.gz"
29859
29902
  );
29860
29903
  if (args.changelog) formData.append("changelog", args.changelog);
29904
+ if (args.channel) formData.append("channel", args.channel);
29861
29905
  const url = `${this.baseUrl}/v1/app-packages/${encodeURIComponent(package_id)}/versions`;
29862
29906
  const response = await fetch(url, {
29863
29907
  method: "POST",
@@ -31258,6 +31302,379 @@ See https://lotics.ai/docs/app-sdk for the SDK reference.
31258
31302
  }
31259
31303
  ];
31260
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
+ }
31261
31678
  function escapeHtml(s) {
31262
31679
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
31263
31680
  }
@@ -33704,6 +34121,9 @@ async function packageNew(args) {
33704
34121
  ui_version: uiLatest ? `^${uiLatest}` : void 0,
33705
34122
  sdk_version: packageSdkRange(sdkLatest)
33706
34123
  });
34124
+ const overrides = new Map(
34125
+ buildPackageStarterOverrides({ app_name: args.name }).map((f) => [f.path, f.content])
34126
+ );
33707
34127
  for (const file of files) {
33708
34128
  const fullPath = path7.join(targetPath, file.path);
33709
34129
  if (file.path === "package.json") {
@@ -33721,7 +34141,7 @@ async function packageNew(args) {
33721
34141
  continue;
33722
34142
  }
33723
34143
  fs6.mkdirSync(path7.dirname(fullPath), { recursive: true });
33724
- fs6.writeFileSync(fullPath, file.content);
34144
+ fs6.writeFileSync(fullPath, overrides.get(file.path) ?? file.content);
33725
34145
  }
33726
34146
  fs6.writeFileSync(
33727
34147
  path7.join(targetPath, CONTRACT_FILE),
@@ -33765,7 +34185,8 @@ async function publishVersion(client, projectDir, opts) {
33765
34185
  const version = await client.publishAppPackageVersion(packageId, {
33766
34186
  contract,
33767
34187
  bundle,
33768
- changelog: opts.changelog ?? null
34188
+ changelog: opts.changelog ?? null,
34189
+ channel: opts.channel ?? "release"
33769
34190
  });
33770
34191
  project.manifest.version = version.version;
33771
34192
  writePackageManifest(projectDir, project);
@@ -33801,7 +34222,8 @@ async function syncToDevWorkspace(client, projectDir) {
33801
34222
  assertDevWorkspace(workspace);
33802
34223
  writePackageAppFields(projectDir);
33803
34224
  const { package_id, version } = await publishVersion(client, projectDir, {
33804
- changelog: "dev sync"
34225
+ changelog: "dev sync",
34226
+ channel: "dev"
33805
34227
  });
33806
34228
  const project = readPackageProject(projectDir);
33807
34229
  const existing = project.manifest.dev[devWorkspaceId];
@@ -33818,6 +34240,11 @@ async function syncToDevWorkspace(client, projectDir) {
33818
34240
  project.manifest.dev[devWorkspaceId] = { app_id: appId, version };
33819
34241
  writePackageManifest(projectDir, project);
33820
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
+ });
33821
34248
  return { app_id: appId, app_name: installed.name, workspace_id: devWorkspaceId, version };
33822
34249
  }
33823
34250
  async function packageSync(client, args) {
@@ -34019,13 +34446,37 @@ async function packageUpgrade(client, args) {
34019
34446
  });
34020
34447
  console.error(`Upgraded ${app.name} \u2192 v${app.package_version} (${app.id}).`);
34021
34448
  }
34022
- 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) {
34023
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}`);
34024
34460
  console.error(
34025
- 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"}`
34026
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)}...`);
34027
34477
  const app = await client.installAppPackage(args.package_id, {
34028
- ...args.version !== void 0 ? { version: args.version } : {}
34478
+ ...args.version !== void 0 ? { version: args.version } : {},
34479
+ ...args.config !== void 0 ? { config: args.config } : {}
34029
34480
  });
34030
34481
  const versionLabel = app.package_version !== null ? `v${app.package_version}` : "(unknown version)";
34031
34482
  console.error(
@@ -34041,6 +34492,104 @@ async function packageEject(client, args) {
34041
34492
  console.error(" Its data model, queries, workflows, and templates are unchanged.");
34042
34493
  console.error(" Pull the pinned source for local editing: lotics app pull " + app.id);
34043
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
+ }
34044
34593
  async function packageExtract(client, args) {
34045
34594
  const app = await client.getApp(args.app_id);
34046
34595
  if (!app.current_version_id) {
@@ -34181,6 +34730,17 @@ async function packageAdopt(client, args) {
34181
34730
  );
34182
34731
  console.error(` Verify the installation: lotics package doctor ${app.id}`);
34183
34732
  }
34733
+ async function packageYank(client, args) {
34734
+ const result = await client.yankAppPackageVersion(args.package_id, args.version, !args.undo);
34735
+ if (result.yanked_at !== null) {
34736
+ console.error(
34737
+ `Yanked ${result.package_id} v${result.version} (${result.yanked_at}). New installs/upgrades refuse it; pinned installations keep running.`
34738
+ );
34739
+ } else {
34740
+ console.error(`Restored ${result.package_id} v${result.version} \u2014 installable again.`);
34741
+ }
34742
+ console.error(` Latest installable version: ${result.latest_version === 0 ? "none" : `v${result.latest_version}`}`);
34743
+ }
34184
34744
  async function packageFleetUpgrade(client, args) {
34185
34745
  const result = await client.fleetUpgradeAppPackage(args.package_id, {
34186
34746
  ...args.version !== void 0 ? { version: args.version } : {}
@@ -34238,6 +34798,10 @@ function parseArgs(argv) {
34238
34798
  cleanup: false,
34239
34799
  packageVersion: void 0,
34240
34800
  resolve: [],
34801
+ config: [],
34802
+ set: [],
34803
+ archiveTables: false,
34804
+ undo: false,
34241
34805
  version: false,
34242
34806
  help: false
34243
34807
  };
@@ -34315,6 +34879,28 @@ function parseArgs(argv) {
34315
34879
  flags.resolve.push(value);
34316
34880
  break;
34317
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;
34318
34904
  case "--version":
34319
34905
  case "-v": {
34320
34906
  const next = argv[i2 + 1];
@@ -50987,9 +51573,18 @@ COMMANDS
50987
51573
  dev server (--workspace <dev_ws> selects it)
50988
51574
  lotics package sync [path] Re-sync (additive migrate + materialize) into a dev ws
50989
51575
  lotics package reset [path] DEV-ONLY: drop scaffolded tables + re-scaffold clean
50990
- lotics package install <package> [--version N]
51576
+ lotics package install <package> [--version N] [--config key=value ...]
50991
51577
  Install an app package into this workspace
50992
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
50993
51588
  lotics package upgrade <app_id> [--version N] [--resolve <key>=recreate|revert|keep|<id> ...]
50994
51589
  Preview-then-apply a package upgrade; refuses while
50995
51590
  any drift/modified finding lacks a --resolve, and
@@ -51008,6 +51603,7 @@ COMMANDS
51008
51603
  origin app (reads .lotics/adopt_binding.json;
51009
51604
  --version N pins a specific version)
51010
51605
  lotics package fleet-upgrade <package_id> [--version N]
51606
+ lotics package yank <package_id> <version> [--undo]
51011
51607
  Upgrade EVERY installation of the package across
51012
51608
  your org: applies where the preview is clean,
51013
51609
  skips + reports findings (exit 1 unless all current)
@@ -51530,7 +52126,11 @@ async function main() {
51530
52126
  console.error(" lotics package dev [path] [--workspace <ws>] Sync into a dev workspace + run the app dev server");
51531
52127
  console.error(" lotics package sync [path] [--workspace <ws>] Re-sync (additive migrate + materialize) into a dev workspace");
51532
52128
  console.error(" lotics package reset [path] [--workspace <ws>] DEV-ONLY: drop scaffolded tables + re-scaffold clean");
51533
- 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)");
51534
52134
  console.error(" lotics package upgrade <app_id> [--version N] [--resolve ns.alias=recreate|<id>] Preview + apply an upgrade");
51535
52135
  console.error(" lotics package doctor [app_id] Health: version pin vs latest + binding drift");
51536
52136
  console.error(" lotics package rebind-role <app_id> <alias> <grp_id> Re-point a package role at another group");
@@ -51538,6 +52138,7 @@ async function main() {
51538
52138
  console.error(" lotics package extract <app_id> [path] Promote a bespoke app to a draft package project");
51539
52139
  console.error(" lotics package adopt <app_id> [path] Bind the published project onto the origin app");
51540
52140
  console.error(" lotics package fleet-upgrade <package_id> [--version N] Upgrade every org installation (clean ones apply; findings skip)");
52141
+ console.error(" lotics package yank <package_id> <version> [--undo] Refuse new installs/upgrades of a broken published version (pinned installations keep running)");
51541
52142
  process.exit(1);
51542
52143
  }
51543
52144
  if (command === "run" && !subcommand) {
@@ -51694,7 +52295,48 @@ Available workspaces:`);
51694
52295
  process.exit(1);
51695
52296
  }
51696
52297
  }
51697
- 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 });
51698
52340
  return;
51699
52341
  }
51700
52342
  if (subcommand === "eject") {
@@ -51734,6 +52376,16 @@ Available workspaces:`);
51734
52376
  await packageAdopt(client, { app_id: appId, version, projectDir: restArgs[0] });
51735
52377
  return;
51736
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
+ }
51737
52389
  if (subcommand === "fleet-upgrade") {
51738
52390
  const packageId = toolArgs;
51739
52391
  if (!packageId) {