@lotics/cli 0.76.1 → 0.86.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -195,78 +195,83 @@ lotics ui link card --remove # finalize: PR + publish, then drop t
195
195
 
196
196
  `app codegen` reads `package.json#lotics.queries` to decide which tables to put in `app_fields.ts`; widen the set with `package.json#lotics.codegen.tables` (an array of `tbl_…` ids) for tables the app only writes via workflows.
197
197
 
198
- ## App packages
198
+ ## Packages
199
199
 
200
- A maintained, versioned app library: author once, install into many workspaces, upgrade per-workspace. Any org authors its own packages (publishing is owner-org-only); Lotics-backed packages carry the `official` badge. See `docs/packages.md` for the model.
200
+ A maintained, versioned package library: author once, install into many workspaces, upgrade per-workspace. A package carries an app, content (knowledge docs and/or document templates), or both. **Nothing starts as a package** — build a bespoke app first (the `lotics app` loop above), then first-release it as a package when it proves worth distributing. Any org authors its own packages (publishing is owner-org-only); Lotics-backed packages carry the `official` badge. See `docs/packages.md` for the model.
201
201
 
202
202
  ```bash
203
- # Author (any org admin; the package is owned by your org)
204
- lotics package new my-crm # scaffold project + contract.json
205
- lotics package publish -m "v2: adds deals" # build + publish an immutable version
206
-
207
- # Dev loop against a dev workspace (created with: lotics workspace create <name> --dev)
208
- lotics package dev --workspace wsp_dev # scaffold-sync + run the app dev server
209
- lotics package sync --workspace wsp_dev # migrate the dev installation only
210
- lotics package reset --workspace wsp_dev # DEV-ONLY: drop scaffolded tables, re-scaffold clean
211
-
212
- # Operate installations (workspace admin)
213
- lotics package show apg_... # registry metadata + version history (trust badge, channel, yank)
214
- lotics package install apg_... --version 2 # scaffold + materialize + deploy + pin
215
- lotics package install apg_... --config heading="Ops board" # per-knob overrides over contract defaults
203
+ # 1. Make the app distributable first-release it as a package (one call: extract
204
+ # an alias-keyed contract, create the registry package, publish v1 from the
205
+ # deployed source + dist, pin the app as installation #1). Blocking findings
206
+ # (a hardcoded fld_/opt_ id in src/ or a workflow body, a button field) → 409.
207
+ lotics app publish # from the pulled app project (or: app publish app_...)
208
+ lotics app publish --rename item=deal -m "v1" # fix an auto-minted alias before v1 freezes it
209
+
210
+ # 2. Iterate IN PLACE the origin app is the permanent working copy
211
+ lotics app pull app_... # the same loop as any app: pull, edit, dev
212
+ lotics app deploy -m "Add quote drawer" # commit a new app version
213
+
214
+ # 3. Release the next package version from the origin you are happy with
215
+ lotics app release -m "v2: adds deals" # preview (new/changed aliases + findings)
216
+ lotics app release -m "v2: adds deals" --yes # publish v2 + re-pin the origin
217
+
218
+ # Take it off the shelf (existing installations keep working + upgrading)
219
+ lotics app unpublish app_... [--undo] # by app id (resolves the package) or package id
220
+
221
+ # Operate installations (workspace admin). The high-traffic verbs are top-level
222
+ # (install / uninstall / upgrade); low-traffic ops live on `lotics package`.
223
+ lotics package show apg_... # registry metadata + version history (trust badge, yank)
224
+ lotics install apg_... --version 2 # scaffold + materialize + deploy + pin
225
+ lotics install apg_... --config heading="Ops board" # per-knob overrides over contract defaults
216
226
  lotics package config app_... --set show_done=false # edit a live installation's config (partial merge)
217
- lotics package uninstall app_... [--archive-tables] # remove an installation; workflows stop firing
218
- lotics package retire apg_... [--undo] # owner-org: refuse NEW installs (existing keep working + upgrading)
227
+ lotics uninstall app_... [--archive-tables] # remove an installation; workflows stop firing
219
228
  lotics package doctor app_... # version pin vs latest + drift + local edits (exit 1 on findings)
220
- lotics package rebind-role app_... <alias> <grp_id> # re-point a package role at another group (re-materializes)
221
- lotics package upgrade app_... # preview, then apply (additive; overlay preserved)
222
- lotics package fleet-upgrade apg_... # upgrade EVERY org installation (clean ones apply; findings skip, exit 1)
223
- lotics package upgrade app_... --resolve fields.deal.stage=recreate # resolve reported drift
224
- lotics package upgrade app_... --resolve queries.tasks=keep # consent for a local edit (or =revert)
225
- lotics package upgrade app_... --resolve template.quote=revert # a locally-edited template (clean ones auto-update; or =keep)
229
+ lotics upgrade app_... # preview, then apply (additive; overlay preserved)
230
+ lotics upgrade apg_... # APP package FLEET: upgrade EVERY org installation (clean ones apply; findings skip, exit 1)
231
+ lotics upgrade app_... --resolve fields.deal.stage=recreate # resolve reported drift
232
+ lotics upgrade app_... --resolve queries.tasks=keep # consent for a local edit (or =revert)
233
+ lotics upgrade app_... --resolve template.quote=revert # a locally-edited template (clean ones auto-update; or =keep)
234
+ lotics upgrade app_... --resolve knowledge.playbook=apply # a bundled knowledge doc: apply|keep|archive|recreate|unbind
235
+ lotics upgrade app_... --resolve roles.approver=grp_... # re-point a LIVE package role at another group
236
+ lotics package yank apg_... 3 [--undo] # refuse new installs/upgrades of a broken version
226
237
  lotics package eject app_... # one-way: sever the package link
227
238
  lotics workspace doctor # dangling schema references across the workspace (exit 1 on findings)
228
-
229
- # Promote an EXISTING bespoke app to a package (build it in a real workspace first,
230
- # distribute once it proves itself — no re-authoring)
231
- lotics package extract app_... # → draft project: contract + binding + templates
232
- # prints a findings report; always writes the draft,
233
- # exits 1 on error findings (fix them, publish re-validates)
234
- cd <slug-of-app-name> # review contract.json aliases
235
- lotics package publish -m "v1" # publish the reviewed project
236
- lotics package adopt app_... # bind the package onto the origin app (same workspace);
237
- # verifies the binding is faithful, then pins installation #1
238
239
  ```
239
240
 
240
- Extract writes `.lotics/adopt_binding.json` (the origin pin, excluded from published bundles); `adopt` reads it back and refuses a pin recorded for a different app. After promotion the package project is the master iterate contract-first (`package dev`/`sync` `publish`), never extract again.
241
+ After the first publish **the origin is the master** develop it in its workspace (`app pull` / `app deploy` / `app workflow set` / table tools), and `lotics app release` snapshots each version. Its **binding-aware re-extract** keeps aliases stable through the origin's current binding, so re-extraction never churns the upgrade diff (which is why extract-again is the loop, not a hazard). Release is also the **bundled-knowledge declaration surface**: run from the app project, the manifest's `lotics.knowledge` (`[{alias,doc_id}]`) re-declares which docs the package bundles an alias added ships as a new doc, a bound alias dropped surfaces to consumers as `removed` (archive|keep), a present one re-snapshots; a bare id from elsewhere reconstructs the current corpus from the pin (never drops it). So a published app package evolves its bundled docs across versions without ejecting.
241
242
 
242
243
  ### Content packages
243
244
 
244
245
  A **content** package ships **knowledge docs and/or document templates** — no app, just the artifacts. Knowledge is the reference corpus an agent looks up by name; templates are standalone document templates. Content comes two ways: bundled *with an app* (installing the app delivers its agents' docs) or as a standalone content package (a versioned corpus). Install delivers, upgrade refreshes — preserving stable `kdc_`/`tmpl_` ids + a version snapshot per change. See `docs/packages.md` § Content.
245
246
 
246
247
  ```bash
247
- # Author a content package (scaffolds a knowledge/ dir + manifest; add templates/ + a
248
- # lotics.package.templates entry per template inline html/email or file-backed excel/word/pdf-form)
249
- lotics package new my-sops --kind content
250
- lotics package publish -m "v1: SOP corpus + templates"
248
+ # Publish a content package from a SET of live knowledge docs + templates the
249
+ # publish_content tool (there is no content-package project). Aliases + shas are
250
+ # computed from live content; v1 is created and the origin workspace is pinned.
251
+ lotics run publish_content '{"name":"My SOPs","knowledge_doc_ids":["kdc_..."],"template_ids":["tmpl_..."]}'
252
+
253
+ # Cut the next version after editing those docs/templates in place (add "dry_run":true to preview the diff)
254
+ lotics run release_content '{"package_id":"apg_...","changelog":"v2: revised SOPs"}'
251
255
 
252
256
  # Install into a workspace (a same-named local DOC collides — consent with --bind-to; templates never collide)
253
- lotics package install apg_... # app OR content, kind-branched
254
- lotics package install apg_... --bind-to policy=kdc_... # adopt an existing doc as package-managed
255
-
256
- # Discover installed content (the source for a pci_ id — install prints it once)
257
- lotics package list-content # id · package · version · standalone vs app-bundled
258
-
259
- # Refresh a standalone content install (pci_ id from install / list-content) — one gate over docs + templates
260
- lotics package upgrade pci_... # preview, then apply
261
- lotics package upgrade pci_... --resolve policy=apply # a doc: apply|keep|archive|recreate|unbind
262
- lotics package upgrade pci_... --resolve quote=revert # a locally-edited template: revert|keep (clean ones auto-update)
263
- lotics package upgrade pci_... --apply-all # accept upstream for every doc + template (OVERWRITES local edits)
264
- lotics package uninstall pci_... --keep-content # remove the pin; keep docs + templates (default archives them)
257
+ lotics install apg_... # app OR content, shape-branched
258
+ lotics install apg_... --bind-to policy=kdc_... # adopt an existing doc as package-managed
259
+
260
+ # Discover installed content (each row leads with the PACKAGE id)
261
+ lotics package list-content # package id · package · version (standalone content installs)
262
+
263
+ # Refresh this workspace's content install addressed by PACKAGE id (one install per
264
+ # package per workspace, so the package id is the address; one gate over docs + templates)
265
+ lotics upgrade apg_... # CONTENT package → preview, then apply (this workspace's install)
266
+ lotics upgrade apg_... --resolve knowledge.policy=apply # a doc: apply|keep|archive|recreate|unbind
267
+ lotics upgrade apg_... --resolve template.quote=revert # a locally-edited template: revert|keep (clean ones auto-update)
268
+ lotics upgrade apg_... --apply-all # accept upstream for every doc + template (OVERWRITES local edits)
269
+ lotics uninstall apg_... --keep-content # remove the pin; keep docs + templates (default archives them)
265
270
  ```
266
271
 
267
- A package's knowledge can also arrive **bundled with an app**. Upgrading such an app (`lotics package upgrade <app_id>`) previews its bundled-knowledge changes alongside the plan and resolves them with the SAME flags: a `--resolve <alias>=apply|keep|archive|recreate|unbind` naming a bundled doc routes to the doc (anything else stays a core-artifact resolution; a name that is both errors), `--bind-to` consents an added-doc collision, `--apply-all` takes the package's version for every consent-requiring doc.
272
+ A package's knowledge can also arrive **bundled with an app**. Upgrading such an app (`lotics upgrade <app_id>`) previews its bundled-knowledge changes alongside the plan and resolves them in the SAME namespaced grammar `--resolve knowledge.<alias>=apply|keep|archive|recreate|unbind` (the flags pass to the server verbatim; no routing, no ambiguity), `--bind-to` consents an added-doc collision, `--apply-all` takes the package's version for every consent-requiring doc.
268
273
 
269
- To package the knowledge an existing app's agents route to, declare it in the app project's `package.json#lotics.knowledge` (`[{ alias, doc_id }]`, + `lotics.knowledge_expects` for docs the agents assume but the package doesn't own), then `lotics package extract` / `adopt` as usual.
274
+ To package the knowledge an existing app's agents route to, declare it in the app project's `package.json#lotics.knowledge` (`[{ alias, doc_id }]`, + `lotics.knowledge_expects` for docs the agents assume but the package doesn't own), then `lotics app publish` from the app project — it forwards the declaration, and the docs ship with the package.
270
275
 
271
276
  ## SDK
272
277
 
@@ -137,10 +137,15 @@ export declare function writeAppDts(projectDir: string, manifest: {
137
137
  * `lotics app codegen [path]` — regenerate every `.lotics/` artifact from the
138
138
  * manifest + workspace schema, WITHOUT a deploy. The `.d.ts` companions are
139
139
  * always written (synchronous, no network). When a `client` is available, the
140
- * runtime `app_fields.ts` is also regenerated from the live schema of the tables
141
- * the app's queries reference (+ the allowlist); a network failure is non-fatal
142
- * (warn, keep the last-generated file) so codegen still does useful work
143
- * offline, mirroring `app create`'s tolerance of an offline npm registry.
140
+ * runtime `app_fields.ts` is also regenerated **branched on whether the app is
141
+ * a package installation** (`getApp().package_id` set): a **linked/published**
142
+ * app emits the BINDING-resolved form (`F`/`OPT`/`ROLE` resolved from the live
143
+ * binding at module load via `getAppBinding()`, so the source stays portable
144
+ * across every install), a **bespoke** app emits the baked form (this
145
+ * workspace's concrete `fld_…`/`opt_…` ids, from the live schema of the tables
146
+ * its queries reference + the allowlist). A network failure is non-fatal (warn,
147
+ * keep the last-generated file) — so codegen still does useful work offline,
148
+ * mirroring `app create`'s tolerance of an offline npm registry.
144
149
  */
145
150
  export declare function appCodegen(args: {
146
151
  projectDir?: string;
@@ -23,6 +23,7 @@ import { generateAppAgentsDts } from "./generate_app_agents_dts.js";
23
23
  import { generateAppQueriesDts } from "./generate_app_queries_dts.js";
24
24
  import { collectQueryTableIds } from "@lotics/shared/app_query_ast";
25
25
  import { generateAppFields } from "./generate_app_fields.js";
26
+ import { generatePackageAppFields } from "./generate_package_fields.js";
26
27
  import { loadProjectTypescript, checkWorkflowBodies, } from "./app_workflow_check.js";
27
28
  /**
28
29
  * Resolve the latest published version of a package from the npm registry.
@@ -442,14 +443,33 @@ function writeAppFields(projectDir, tables) {
442
443
  fs.writeFileSync(file, generateAppFields(tables));
443
444
  return file;
444
445
  }
446
+ /**
447
+ * Write the BINDING-resolved `.lotics/app_fields.ts` for a package installation
448
+ * — the portable twin of `writeAppFields`. Instead of baking this workspace's
449
+ * concrete ids, the module resolves `F`/`OPT`/`ROLE` from the installation's
450
+ * live binding at load (`getAppBinding()`), so a published origin's source runs
451
+ * unchanged in every workspace that installs the package. Returns the path.
452
+ */
453
+ function writeBindingAppFields(projectDir, binding) {
454
+ const dotLotics = path.join(projectDir, ".lotics");
455
+ fs.mkdirSync(dotLotics, { recursive: true });
456
+ const file = path.join(dotLotics, "app_fields.ts");
457
+ fs.writeFileSync(file, generatePackageAppFields(binding));
458
+ return file;
459
+ }
445
460
  /**
446
461
  * `lotics app codegen [path]` — regenerate every `.lotics/` artifact from the
447
462
  * manifest + workspace schema, WITHOUT a deploy. The `.d.ts` companions are
448
463
  * always written (synchronous, no network). When a `client` is available, the
449
- * runtime `app_fields.ts` is also regenerated from the live schema of the tables
450
- * the app's queries reference (+ the allowlist); a network failure is non-fatal
451
- * (warn, keep the last-generated file) so codegen still does useful work
452
- * offline, mirroring `app create`'s tolerance of an offline npm registry.
464
+ * runtime `app_fields.ts` is also regenerated **branched on whether the app is
465
+ * a package installation** (`getApp().package_id` set): a **linked/published**
466
+ * app emits the BINDING-resolved form (`F`/`OPT`/`ROLE` resolved from the live
467
+ * binding at module load via `getAppBinding()`, so the source stays portable
468
+ * across every install), a **bespoke** app emits the baked form (this
469
+ * workspace's concrete `fld_…`/`opt_…` ids, from the live schema of the tables
470
+ * its queries reference + the allowlist). A network failure is non-fatal (warn,
471
+ * keep the last-generated file) — so codegen still does useful work offline,
472
+ * mirroring `app create`'s tolerance of an offline npm registry.
453
473
  */
454
474
  export async function appCodegen(args) {
455
475
  const projectDir = path.resolve(args.projectDir ?? process.cwd());
@@ -467,18 +487,32 @@ export async function appCodegen(args) {
467
487
  "Run authenticated (or set LOTICS_API_KEY) to regenerate field/option ids.");
468
488
  return;
469
489
  }
470
- const tableIds = resolveCodegenTableIds(projectDir, meta.queries ?? {});
471
490
  try {
472
- const tables = await args.client.getWorkspaceSchema(tableIds);
473
- const fieldsPath = writeAppFields(projectDir, tables);
474
- console.error(`Regenerated ${fieldsPath} (${tables.length} table${tables.length === 1 ? "" : "s"})`);
491
+ // Branch on linked-ness: a package installation (its own published origin OR
492
+ // an install of another org's package) must ship PORTABLE source, so
493
+ // app_fields resolves F/OPT/ROLE from the live binding at module load; a
494
+ // bespoke app bakes this workspace's concrete ids. getApp is the authority —
495
+ // `package_id` is set iff the app is a package installation.
496
+ const app = await args.client.getApp(meta.app_id);
497
+ if (app.package_id) {
498
+ const binding = await args.client.appBinding(meta.app_id);
499
+ const fieldsPath = writeBindingAppFields(projectDir, binding);
500
+ const count = Object.keys(binding.fields).length;
501
+ console.error(`Regenerated ${fieldsPath} (binding form — ${count} field alias${count === 1 ? "" : "es"} resolved per-install)`);
502
+ }
503
+ else {
504
+ const tableIds = resolveCodegenTableIds(projectDir, meta.queries ?? {});
505
+ const tables = await args.client.getWorkspaceSchema(tableIds);
506
+ const fieldsPath = writeAppFields(projectDir, tables);
507
+ console.error(`Regenerated ${fieldsPath} (${tables.length} table${tables.length === 1 ? "" : "s"})`);
508
+ }
475
509
  }
476
510
  catch (err) {
477
511
  // Non-fatal: keep the last-generated app_fields.ts so an offline/transient
478
512
  // failure doesn't strip the app's field aliases (same tolerance as the npm
479
513
  // registry lookup on `app create`).
480
- console.error(`⚠ Could not fetch the workspace schema (${err instanceof Error ? err.message : String(err)}). ` +
481
- `Kept the existing .lotics/app_fields.ts.`);
514
+ console.error(`⚠ Could not regenerate .lotics/app_fields.ts (${err instanceof Error ? err.message : String(err)}). ` +
515
+ `Kept the existing file.`);
482
516
  }
483
517
  // Refresh each bound workflow's ambient globals + re-wrap its EXISTING local
484
518
  // body in the current envelope (GAP-59). Codegen never re-fetches the body
@@ -206,6 +206,75 @@ describe("appCodegen (.d.ts-only path)", () => {
206
206
  expect(second.equals(first)).toBe(true);
207
207
  });
208
208
  });
209
+ /**
210
+ * `appCodegen` (with a client) branches the runtime `app_fields.ts` on whether
211
+ * the app is a package installation: a LINKED app (`getApp().package_id` set)
212
+ * emits the binding-resolved form (portable — resolves through `getAppBinding()`
213
+ * at load); a BESPOKE app bakes this workspace's concrete ids from the schema.
214
+ */
215
+ describe("appCodegen app_fields branch (linked vs bespoke)", () => {
216
+ let workDir;
217
+ beforeEach(() => {
218
+ workDir = fs.mkdtempSync(path.join(tmpdir(), "lotics-codegen-branch-"));
219
+ vi.spyOn(console, "error").mockImplementation(() => undefined);
220
+ });
221
+ afterEach(() => {
222
+ fs.rmSync(workDir, { recursive: true, force: true });
223
+ vi.restoreAllMocks();
224
+ });
225
+ function writeManifest() {
226
+ fs.writeFileSync(path.join(workDir, "package.json"), JSON.stringify({
227
+ name: "sample-app",
228
+ lotics: {
229
+ app_id: "app_1",
230
+ workspace_id: "wks_1",
231
+ current_version_id: "apv_1",
232
+ version_number: 1,
233
+ queries: { all: { ast: { kind: "from_table", table_id: "tbl_1" } } },
234
+ },
235
+ }, null, 2));
236
+ }
237
+ const readFields = () => fs.readFileSync(path.join(workDir, ".lotics", "app_fields.ts"), "utf-8");
238
+ it("emits the BAKED form for a bespoke app (no package_id)", async () => {
239
+ writeManifest();
240
+ const client = {
241
+ getApp: async () => ({ package_id: null }),
242
+ getWorkspaceSchema: async () => [
243
+ { id: "tbl_1", name: "Tasks", fields: [{ id: "fld_title", name: "Title" }] },
244
+ ],
245
+ };
246
+ await appCodegen({ projectDir: workDir, client });
247
+ const src = readFields();
248
+ expect(src).not.toContain("getAppBinding");
249
+ // Concrete id baked into the value position.
250
+ expect(src).toContain('"fld_title"');
251
+ expect(src).toMatch(/TASKS: \{/);
252
+ });
253
+ it("emits the BINDING form for a linked/published app (package_id set)", async () => {
254
+ writeManifest();
255
+ const client = {
256
+ getApp: async () => ({ package_id: "apg_1" }),
257
+ appBinding: async () => ({
258
+ fields: { "tasks.title": "fld_title" },
259
+ options: {},
260
+ roles: { approver: "grp_x" },
261
+ }),
262
+ // Must NOT be consulted on the linked branch — a call would fail the type
263
+ // shape it returns, and the assertions below prove the baked path is dead.
264
+ getWorkspaceSchema: async () => {
265
+ throw new Error("getWorkspaceSchema must not run for a linked app");
266
+ },
267
+ };
268
+ await appCodegen({ projectDir: workDir, client });
269
+ const src = readFields();
270
+ expect(src).toContain('import { getAppBinding } from "@lotics/app-sdk";');
271
+ expect(src).toContain("const binding = await getAppBinding();");
272
+ expect(src).toContain('title: bound(binding.fields, "tasks.title", "field")');
273
+ expect(src).toContain('approver: bound(binding.roles, "approver", "role")');
274
+ // No concrete id baked into any value position.
275
+ expect(src).not.toMatch(/"(fld|opt|tbl|grp)_/);
276
+ });
277
+ });
209
278
  /**
210
279
  * `appUiLink` edits the app's vite.config.ts resolve.alias to dev-link
211
280
  * @lotics/ui at packages/ui/src — auto-found in a monorepo checkout, or given
package/dist/args.d.ts CHANGED
@@ -30,8 +30,6 @@ export declare function parseArgs(argv: string[]): {
30
30
  message?: string;
31
31
  local: boolean;
32
32
  all: boolean;
33
- /** `--dev`: mark a workspace as an app-package dev workspace (`workspace create`). */
34
- dev: boolean;
35
33
  /** `--yes` (alias `-y`): confirm a destructive command (e.g. `workspace delete`) non-interactively. */
36
34
  yes: boolean;
37
35
  /** `--print-created` (alias `--report-effects`): print the honest post-run side-effect harvest. */
@@ -39,7 +37,7 @@ export declare function parseArgs(argv: string[]): {
39
37
  /** `--cleanup`: also delete the harvested created records (records only). */
40
38
  cleanup: boolean;
41
39
  /**
42
- * `--version <n>`: a package version number (`lotics package install … --version 2`).
40
+ * `--version <n>`: a package version number (`lotics install … --version 2`).
43
41
  * Distinct from the boolean `version` flag below: `--version` followed by an
44
42
  * integer captures the number; a bare `--version` (or `-v`) prints the CLI
45
43
  * version. No other command takes a numeric value after `--version`, so the
@@ -64,8 +62,11 @@ export declare function parseArgs(argv: string[]): {
64
62
  keepContent: boolean;
65
63
  /** `--apply-all` (knowledge `package upgrade`): accept the package's version for every consent entry. */
66
64
  applyAll: boolean;
67
- /** `--kind <app|content>` (`package new`): the package project kind to scaffold (default app). */
68
- kind?: string;
65
+ /**
66
+ * `--rename <old>=<new>` (repeatable, `lotics app publish`): fix an auto-minted
67
+ * alias before the package's v1 contract freezes it.
68
+ */
69
+ rename: string[];
69
70
  /** `--config key=value` (repeatable): per-knob config overrides at `package install`. */
70
71
  config: string[];
71
72
  /** `--set key=value` (repeatable): per-knob config edits at `package config`. */
package/dist/args.js CHANGED
@@ -25,7 +25,6 @@ export function parseArgs(argv) {
25
25
  message: undefined,
26
26
  local: false,
27
27
  all: false,
28
- dev: false,
29
28
  yes: false,
30
29
  printCreated: false,
31
30
  cleanup: false,
@@ -34,7 +33,7 @@ export function parseArgs(argv) {
34
33
  bindTo: [],
35
34
  keepContent: false,
36
35
  applyAll: false,
37
- kind: undefined,
36
+ rename: [],
38
37
  config: [],
39
38
  set: [],
40
39
  archiveTables: false,
@@ -89,9 +88,6 @@ export function parseArgs(argv) {
89
88
  case "--local":
90
89
  flags.local = true;
91
90
  break;
92
- case "--dev":
93
- flags.dev = true;
94
- break;
95
91
  case "--all":
96
92
  flags.all = true;
97
93
  break;
@@ -128,9 +124,14 @@ export function parseArgs(argv) {
128
124
  case "--apply-all":
129
125
  flags.applyAll = true;
130
126
  break;
131
- case "--kind":
132
- flags.kind = argv[++i];
127
+ case "--rename": {
128
+ const value = argv[++i];
129
+ if (value === undefined || value.startsWith("-")) {
130
+ throw new Error("--rename requires a value: old=new (an alias to rename before v1 freezes it).");
131
+ }
132
+ flags.rename.push(value);
133
133
  break;
134
+ }
134
135
  case "--config": {
135
136
  const value = argv[++i];
136
137
  if (value === undefined || value.startsWith("-")) {
@@ -155,18 +156,18 @@ export function parseArgs(argv) {
155
156
  break;
156
157
  case "--version":
157
158
  case "-v": {
158
- // Inside a `package` command, `--version` carries a value (the
159
- // package version to install) and the value is REQUIRED — a bare or
160
- // malformed `--version` here must never fall back to the CLI-version
161
- // early-exit boolean, which would print the CLI version and exit 0
162
- // (a provisioning script reads that as a successful install). The
163
- // value is captured raw; the consuming command validates
164
- // integer-ness with a loud error. Outside `package`, this is the
165
- // CLI-version boolean.
159
+ // For a package version-taking command (`package …`, or the top-level
160
+ // consumer verbs `install` / `upgrade`), `--version` carries a value
161
+ // (the package version) and the value is REQUIRED a bare or malformed
162
+ // `--version` here must never fall back to the CLI-version early-exit
163
+ // boolean, which would print the CLI version and exit 0 (a provisioning
164
+ // script reads that as a successful install). The value is captured raw;
165
+ // the consuming command validates integer-ness with a loud error.
166
+ // Everywhere else, this is the CLI-version boolean.
166
167
  const next = argv[i + 1];
167
- if (command === "package") {
168
+ if (command === "package" || command === "install" || command === "upgrade") {
168
169
  if (next === undefined || next.startsWith("-")) {
169
- throw new Error("--version requires a version number for package commands (e.g. --version 2).");
170
+ throw new Error("--version requires a version number for install/upgrade/package commands (e.g. --version 2).");
170
171
  }
171
172
  flags.packageVersion = next;
172
173
  i++;
package/dist/args.test.js CHANGED
@@ -23,15 +23,21 @@ describe("parseArgs", () => {
23
23
  const r = parseArgs(["app", "deploy"]);
24
24
  expect(r.flags.message).toBeUndefined();
25
25
  });
26
- it("captures --version <int> as packageVersion, not the boolean version flag", () => {
27
- const r = parseArgs(["package", "install", "apg_123", "--version", "2"]);
28
- expect(r.command).toBe("package");
29
- expect(r.subcommand).toBe("install");
30
- expect(r.toolArgs).toBe("apg_123");
26
+ it("captures --version <int> as packageVersion on the top-level install verb", () => {
27
+ const r = parseArgs(["install", "apg_123", "--version", "2"]);
28
+ expect(r.command).toBe("install");
29
+ expect(r.subcommand).toBe("apg_123");
31
30
  expect(r.flags.packageVersion).toBe("2");
32
31
  expect(r.flags.version).toBe(false);
33
32
  expect(r.restArgs).toEqual([]);
34
33
  });
34
+ it("captures --version <int> as packageVersion on the top-level upgrade verb", () => {
35
+ const r = parseArgs(["upgrade", "app_1", "--version", "3"]);
36
+ expect(r.command).toBe("upgrade");
37
+ expect(r.subcommand).toBe("app_1");
38
+ expect(r.flags.packageVersion).toBe("3");
39
+ expect(r.flags.version).toBe(false);
40
+ });
35
41
  it("treats a bare --version as the boolean CLI-version flag", () => {
36
42
  const r = parseArgs(["--version"]);
37
43
  expect(r.flags.version).toBe(true);
@@ -42,14 +48,13 @@ describe("parseArgs", () => {
42
48
  expect(r.flags.version).toBe(true);
43
49
  expect(r.flags.packageVersion).toBeUndefined();
44
50
  });
45
- it("captures a non-integer --version value under package commands for loud command-level validation", () => {
46
- const r = parseArgs(["package", "install", "apg_123", "--version", "2.0"]);
51
+ it("captures a non-integer --version value under version-taking commands for loud command-level validation", () => {
52
+ const r = parseArgs(["install", "apg_123", "--version", "2.0"]);
47
53
  expect(r.flags.packageVersion).toBe("2.0");
48
54
  expect(r.flags.version).toBe(false);
49
55
  });
50
56
  it("collects repeated --resolve values", () => {
51
57
  const r = parseArgs([
52
- "package",
53
58
  "upgrade",
54
59
  "app_1",
55
60
  "--resolve",
@@ -60,11 +65,11 @@ describe("parseArgs", () => {
60
65
  expect(r.flags.resolve).toEqual(["fields.deal.stage=recreate", "templates.quote=dtl_abc"]);
61
66
  });
62
67
  it("errors on --resolve without a value", () => {
63
- expect(() => parseArgs(["package", "upgrade", "app_1", "--resolve"])).toThrow(/--resolve requires a value/);
68
+ expect(() => parseArgs(["upgrade", "app_1", "--resolve"])).toThrow(/--resolve requires a value/);
64
69
  });
65
- it("errors loudly on a bare --version under package commands instead of printing the CLI version", () => {
66
- expect(() => parseArgs(["package", "install", "apg_123", "--version"])).toThrow(/requires a version number/);
67
- expect(() => parseArgs(["package", "install", "apg_123", "--version", "--json"])).toThrow(/requires a version number/);
70
+ it("errors loudly on a bare --version under version-taking commands instead of printing the CLI version", () => {
71
+ expect(() => parseArgs(["install", "apg_123", "--version"])).toThrow(/requires a version number/);
72
+ expect(() => parseArgs(["install", "apg_123", "--version", "--json"])).toThrow(/requires a version number/);
68
73
  });
69
74
  it("parses --workspace as a value flag", () => {
70
75
  const r = parseArgs(["run", "query_tables", "{}", "--workspace", "wsp_123"]);
@@ -106,11 +111,14 @@ describe("parseArgs", () => {
106
111
  expect(parseArgs(["app", "workflow", "run", "wf"]).flags.cleanup).toBe(false);
107
112
  expect(parseArgs(["app", "workflow", "run", "wf"]).flags.printCreated).toBe(false);
108
113
  });
109
- it("parses --dev as a boolean flag (default false)", () => {
110
- expect(parseArgs(["workspace", "create", "Dev WS", "--dev"]).flags.dev).toBe(true);
111
- expect(parseArgs(["workspace", "create", "Dev WS"]).flags.dev).toBe(false);
112
- // The boolean flag does not consume the workspace name positional.
113
- expect(parseArgs(["workspace", "create", "Dev WS", "--dev"]).toolArgs).toBe("Dev WS");
114
+ it("collects repeated --rename old=new flags (default empty)", () => {
115
+ const r = parseArgs(["app", "publish", ".", "--rename", "item=deal", "--rename", "don_hang=orders"]);
116
+ expect(r.flags.rename).toEqual(["item=deal", "don_hang=orders"]);
117
+ expect(r.toolArgs).toBe(".");
118
+ expect(parseArgs(["app", "publish"]).flags.rename).toEqual([]);
119
+ });
120
+ it("throws when --rename has no value", () => {
121
+ expect(() => parseArgs(["app", "publish", "--rename"])).toThrow(/--rename requires a value/);
114
122
  });
115
123
  it("parses --yes and -y as the same boolean confirmation flag (default false)", () => {
116
124
  expect(parseArgs(["workspace", "delete", "wsp_1", "--yes"]).flags.yes).toBe(true);