@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/README.md +5 -0
- package/dist/app_commands.d.ts +16 -0
- package/dist/app_commands.js +1 -1
- package/dist/args.d.ts +8 -0
- package/dist/args.js +26 -0
- package/dist/cli.js +70 -4
- package/dist/cli_dispatch.test.d.ts +1 -0
- package/dist/cli_dispatch.test.js +90 -0
- package/dist/client.d.ts +74 -0
- package/dist/client.js +41 -0
- package/dist/package_commands.d.ts +48 -0
- package/dist/package_commands.js +199 -6
- package/dist/src/cli.js +661 -9
- package/dist/starter_template.d.ts +19 -0
- package/dist/starter_template.js +389 -0
- package/dist/starter_template.test.js +69 -1
- package/package.json +1 -1
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, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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) {
|