@lotics/cli 0.86.2 → 0.87.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.
@@ -1,19 +1,13 @@
1
1
  /**
2
- * Dispatch smoke test over the BUILT CLI artifact every `package` subcommand,
3
- * the top-level consumer verbs (`install`/`uninstall`/`upgrade`), and the app
4
- * author verbs (publish/release/unpublish) must route to their own handler,
5
- * proven by the usage/refusal line appearing on a bare invocation. The dispatch
6
- * in cli.ts is one hand-rolled argv walk with subcommand names repeated across
7
- * command families (`doctor` exists in both `workspace` and `package`) and split
8
- * positional conventions (top-level verbs read the id from `subcommand`; the
9
- * `package` family from `toolArgs`), and nothing else tests it: `package yank`
10
- * shipped dispatched inside the WORKSPACE family and surfaced only on the first
11
- * live invocation.
2
+ * Dispatch smoke test over the BUILT public CLI artifact, focused on the
3
+ * build-vs-operate split: the packages distribution surface moved WHOLESALE to
4
+ * the private operator CLI (`packages/opctl`), and the public CLI keeps ZERO
5
+ * knowledge of it every removed verb must fall through to the plain
6
+ * unknown-command refusal (no redirect, no hint), while the build surface
7
+ * (app/workspace/run/upload) keeps routing.
12
8
  *
13
9
  * The fake API key satisfies `requireClient` (client construction is offline);
14
- * every asserted path exits before any network call. Spawns run in an empty
15
- * temp cwd so path-defaulting subcommands fail on the missing project file —
16
- * which equally proves their dispatch reached the right handler.
10
+ * every asserted path exits before any network call.
17
11
  */
18
12
  import { beforeAll, describe, expect, it } from "vitest";
19
13
  import { execFileSync, spawnSync } from "node:child_process";
@@ -30,9 +24,6 @@ function runCli(args) {
30
24
  env: {
31
25
  ...process.env,
32
26
  LOTICS_API_KEY: "ltk_dispatch_smoke_fake_key_never_used_on_wire",
33
- // A pinned (fake) workspace skips the pre-dispatch auto-resolution,
34
- // which would otherwise hit the network (`listWorkspaces`) before any
35
- // usage gate. Nothing below ever reaches the wire.
36
27
  LOTICS_WORKSPACE: "wsp_dispatch_smoke_fake",
37
28
  LOTICS_ORG: "",
38
29
  },
@@ -42,114 +33,57 @@ function runCli(args) {
42
33
  return { status: result.status, stderr: result.stderr ?? "" };
43
34
  }
44
35
  beforeAll(() => {
45
- // Bundle the real artifact from src (esbuild only — no tsgo; vitest's
46
- // transform already type-agnostic). ~1s.
36
+ // Bundle the real artifact from src (esbuild only — vitest's transform is
37
+ // already type-agnostic). ~1s.
47
38
  execFileSync(process.execPath, ["scripts/build_cli.mjs"], { cwd: packageRoot });
48
39
  }, 120_000);
49
- describe("lotics package <subcommand> dispatch", () => {
50
- // Positional-requiring subcommands: a bare invocation must print the
51
- // subcommand's OWN usage line reaching it proves family routing AND that
52
- // the usage gate fires before any network/filesystem work.
53
- const usageCases = [
54
- ["config", /Usage: lotics package config/],
55
- ["show", /Usage: lotics package show <package_id>/],
56
- ["yank", /Usage: lotics package yank <package_id> <version>/],
57
- // Folded into the upgrade grammar — the removed verb must redirect loudly.
58
- ["rebind-role", /folded into the upgrade grammar[\s\S]*--resolve roles\./],
59
- ];
60
- for (const [sub, usage] of usageCases) {
61
- it(`routes "package ${sub}" to its handler (usage on missing args)`, () => {
62
- const { status, stderr } = runCli(["package", sub]);
63
- expect(stderr).toMatch(usage);
64
- expect(stderr).not.toMatch(/Unknown package subcommand/);
40
+ describe("removed packages surface — unknown commands, zero knowledge", () => {
41
+ // The whole distribution surface lives in opctl now. The public CLI treats
42
+ // the old verbs as any other unknown command no "moved to opctl" hint.
43
+ for (const verb of ["install", "uninstall", "upgrade", "package"]) {
44
+ it(`rejects "${verb}" as an unknown command`, () => {
45
+ const { status, stderr } = runCli([verb, "apg_x"]);
46
+ expect(stderr).toMatch(new RegExp(`Unknown command: ${verb}`));
47
+ expect(stderr).not.toMatch(/opctl/);
65
48
  expect(status).toBe(1);
66
49
  });
67
50
  }
68
- // The high-traffic consumer verbs MOVED to the top level. The old
69
- // `lotics package <verb>` forms must REJECT with a redirect (never silently
70
- // do nothing) proving the removal is loud and points to the new form.
71
- const movedForms = [
72
- ["install", /moved to the top level.*lotics install/],
73
- ["uninstall", /moved to the top level.*lotics uninstall/],
74
- ["upgrade", /moved to the top level.*lotics upgrade/],
75
- ["fleet-upgrade", /moved to the top level.*lotics upgrade <package_id>/],
76
- ];
77
- for (const [sub, redirect] of movedForms) {
78
- it(`rejects the moved "package ${sub}" form with a top-level redirect`, () => {
79
- const { status, stderr } = runCli(["package", sub]);
80
- expect(stderr).toMatch(redirect);
51
+ for (const sub of ["publish", "release", "unpublish"]) {
52
+ it(`rejects "app ${sub}" as an unknown app subcommand`, () => {
53
+ const { status, stderr } = runCli(["app", sub]);
54
+ expect(stderr).toMatch(/Unknown app subcommand/);
55
+ expect(stderr).not.toMatch(/opctl/);
81
56
  expect(status).toBe(1);
82
57
  });
83
58
  }
84
- // Deleted author-loop + package-project surfaces must be GONE, not silently
85
- // absorbed: each now falls through to the unknown-subcommand refusal. Content
86
- // packages are published via the `publish_content`/`release_content` tools
87
- // (`lotics run …`), apps via `lotics app publish`/`release`.
88
- for (const sub of ["dev", "sync", "reset", "extract", "adopt", "retire", "release", "new", "build", "publish"]) {
89
- it(`rejects the removed "package ${sub}" subcommand`, () => {
90
- const { status, stderr } = runCli(["package", sub]);
91
- expect(stderr).toMatch(/Unknown package subcommand/);
92
- expect(status).toBe(1);
59
+ it("keeps --help free of package verbs", () => {
60
+ const result = spawnSync(process.execPath, [cliBin, "--help"], {
61
+ cwd: emptyCwd,
62
+ encoding: "utf-8",
63
+ timeout: 30_000,
93
64
  });
94
- }
95
- it("still rejects a genuinely unknown subcommand", () => {
96
- const { status, stderr } = runCli(["package", "frobnicate"]);
97
- expect(stderr).toMatch(/Unknown package subcommand/);
98
- expect(status).toBe(1);
65
+ const help = `${result.stdout}\n${result.stderr}`;
66
+ expect(help).not.toMatch(/opctl/);
67
+ expect(help).not.toMatch(/lotics install/);
68
+ expect(help).not.toMatch(/lotics package/);
69
+ expect(help).not.toMatch(/app publish/);
99
70
  });
100
71
  });
101
- describe("lotics top-level consumer-verb dispatch", () => {
102
- // The noun-less consumer verbs route from the top level (the package id is the
103
- // second positional → `subcommand`), each proven by its OWN usage line on a
104
- // bare invocation — reaching it fires before any network work.
105
- const usageCases = [
106
- ["install", /Usage: lotics install <package_id>/],
107
- ["uninstall", /Usage: lotics uninstall <app_id\|package_id>/],
108
- ["upgrade", /Usage: lotics upgrade <app_id \| package_id>/],
109
- ];
110
- for (const [verb, usage] of usageCases) {
111
- it(`routes top-level "${verb}" to its handler (usage on missing id)`, () => {
112
- const { status, stderr } = runCli([verb]);
113
- expect(stderr).toMatch(usage);
114
- expect(stderr).not.toMatch(/Unknown command/);
115
- expect(status).toBe(1);
116
- });
117
- }
118
- // CLEAN BREAK: content installs are addressed by their PACKAGE id — an explicit
119
- // `pci_` resource id must get a loud redirect to the package-id form. The redirect
120
- // header prints SYNCHRONOUSLY (before the best-effort list-content resolution that
121
- // spells out the exact command), so it is observable without a live registry; the
122
- // process exits 1 either way (best-effort resolution fails on the fake key).
123
- for (const verb of ["upgrade", "uninstall"]) {
124
- it(`redirects an explicit pci_ id on "${verb}" to the package-id form`, () => {
125
- const { status, stderr } = runCli([verb, "pci_dispatch_smoke_fake"]);
126
- expect(stderr).toMatch(/addressed by their PACKAGE id now/);
127
- expect(stderr).not.toMatch(/Unknown command/);
128
- expect(status).toBe(1);
129
- });
130
- }
131
- });
132
- describe("lotics app author-verb dispatch", () => {
133
- // `app publish` (no id, empty cwd) must fail on the missing local app
134
- // manifest — its handler's message, proving routing without any network.
135
- it('routes "app publish" to its handler (no app id in empty cwd)', () => {
136
- const { status, stderr } = runCli(["app", "publish"]);
137
- expect(stderr).toMatch(/No app id/);
138
- expect(stderr).not.toMatch(/Unknown app subcommand/);
72
+ describe("build surface keeps routing", () => {
73
+ it('routes "app deploy" to its handler (usage on missing -m)', () => {
74
+ const { status, stderr } = runCli(["app", "deploy"]);
75
+ expect(stderr).toMatch(/Usage: lotics app deploy/);
139
76
  expect(status).toBe(1);
140
77
  });
141
- // `app release` requires -m before any resolution.
142
- it('routes "app release" to its handler (usage on missing -m)', () => {
143
- const { status, stderr } = runCli(["app", "release"]);
144
- expect(stderr).toMatch(/Usage: lotics app release/);
145
- expect(stderr).not.toMatch(/Unknown app subcommand/);
78
+ it('routes "download" to its usage gate', () => {
79
+ const { status, stderr } = runCli(["download"]);
80
+ expect(stderr).toMatch(/Usage:/);
81
+ expect(stderr).toMatch(/lotics download <file_id>/);
146
82
  expect(status).toBe(1);
147
83
  });
148
- // `app unpublish` requires an id.
149
- it('routes "app unpublish" to its handler (usage on missing id)', () => {
150
- const { status, stderr } = runCli(["app", "unpublish"]);
151
- expect(stderr).toMatch(/Usage: lotics app unpublish/);
152
- expect(stderr).not.toMatch(/Unknown app subcommand/);
84
+ it('routes "run" to its usage gate', () => {
85
+ const { status, stderr } = runCli(["run"]);
86
+ expect(stderr).toMatch(/Usage: lotics run <tool>/);
153
87
  expect(status).toBe(1);
154
88
  });
155
89
  });
package/dist/client.d.ts CHANGED
@@ -246,7 +246,7 @@ export declare class LoticsClient {
246
246
  /**
247
247
  * Uninstall a standalone content package — delete the installation row. By
248
248
  * default the package-bound docs are ARCHIVED; `keep_content` retains them as
249
- * ordinary workspace docs. Admin-only. Backs `lotics uninstall`.
249
+ * ordinary workspace docs. Admin-only. Backs `opctl uninstall`.
250
250
  */
251
251
  uninstallContentPackage(installation_id: string, opts?: {
252
252
  keep_content?: boolean;
@@ -300,7 +300,7 @@ export declare class LoticsClient {
300
300
  } | null;
301
301
  }>>;
302
302
  /**
303
- * Uninstall a package installation (backs `lotics uninstall`). Does
303
+ * Uninstall a package installation (backs `opctl uninstall`). Does
304
304
  * everything DELETE does plus archives the installation's lifecycle
305
305
  * artifacts; with `archive_tables` it also archives the scaffolded entity
306
306
  * tables — refused server-side unless this installation created them
@@ -314,7 +314,7 @@ export declare class LoticsClient {
314
314
  archived_table_ids: string[];
315
315
  }>;
316
316
  /**
317
- * Partial-merge a package installation's config (backs `lotics package config
317
+ * Partial-merge a package installation's config (backs `opctl package config
318
318
  * --set`). Only the provided keys change; validated against the installed
319
319
  * contract. Returns the full effective config. Admin-only.
320
320
  */
@@ -355,13 +355,13 @@ export declare class LoticsClient {
355
355
  * org to the target version (latest when omitted) in one call. Hands-off
356
356
  * applies only where the preview is clean; installations with breaking/
357
357
  * drift/modified-core findings are skipped and reported for the normal
358
- * per-installation consent flow. Backs `lotics upgrade <package_id>` (fleet).
358
+ * per-installation consent flow. Backs `opctl upgrade <package_id>` (fleet).
359
359
  * Admin-only; org-scoped (no workspace header needed).
360
360
  */
361
361
  /**
362
362
  * Yank / unyank a published package version — refuses NEW installs/upgrades/
363
363
  * adopts targeting it; pinned installations keep running. Owner-org
364
- * admin-only. Backs `lotics package yank`.
364
+ * admin-only. Backs `opctl package yank`.
365
365
  */
366
366
  yankPackageVersion(package_id: string, version: number, yanked: boolean): Promise<{
367
367
  package_id: string;
@@ -407,7 +407,7 @@ export declare class LoticsClient {
407
407
  created_at: string;
408
408
  updated_at: string;
409
409
  }>;
410
- /** Version history newest-first (no contract payloads) — backs `lotics package show`. Admin-only. */
410
+ /** Version history newest-first (no contract payloads) — backs `opctl package show`. Admin-only. */
411
411
  listPackageVersions(package_id: string): Promise<{
412
412
  versions: Array<{
413
413
  version: number;
@@ -505,7 +505,7 @@ export declare class LoticsClient {
505
505
  /**
506
506
  * Health check for a package installation — version pin vs. registry latest,
507
507
  * binding drift, and locally modified core artifacts. Read-only; backs
508
- * `lotics package doctor`. Admin-only.
508
+ * `opctl package doctor`. Admin-only.
509
509
  */
510
510
  getPackageHealth(app_id: string): Promise<{
511
511
  app_id: string;
@@ -541,7 +541,7 @@ export declare class LoticsClient {
541
541
  missing_expected_docs: string[];
542
542
  }>;
543
543
  /**
544
- * Preview a release — the dry run behind `lotics app release`. Runs the
544
+ * Preview a release — the dry run behind `opctl app release`. Runs the
545
545
  * binding-aware extract of the origin (aliases stable through the app's current
546
546
  * binding) and reports the next version number, the new + changed aliases, the
547
547
  * bundled-knowledge delta, and any extract findings (an `error` blocks the
@@ -585,7 +585,7 @@ export declare class LoticsClient {
585
585
  * bundled-knowledge set (added/dropped/re-snapshotted docs; omitted preserves
586
586
  * the current corpus). Error findings from extract surface as a 409; a
587
587
  * missing/archived declared doc is a 400, a foreign-package doc a 409. Admin,
588
- * owning-org only. Backs `lotics app release --yes`.
588
+ * owning-org only. Backs `opctl app release --yes`.
589
589
  */
590
590
  releasePackage(app_id: string, body: {
591
591
  changelog: string;
@@ -615,7 +615,7 @@ export declare class LoticsClient {
615
615
  };
616
616
  }>;
617
617
  /**
618
- * Dry-run preview of a first-release — the `GET` behind `lotics app publish`
618
+ * Dry-run preview of a first-release — the `GET` behind `opctl app publish`
619
619
  * (no `--yes`), the publish-side analogue of `previewPackageRelease`. The
620
620
  * server runs the same fresh-alias extract + `src/` scan the apply runs
621
621
  * (through any `renames`) and returns the package name it would mint, the
@@ -649,13 +649,13 @@ export declare class LoticsClient {
649
649
  }>;
650
650
  /**
651
651
  * First-release apply — mint a package from a BESPOKE app and publish v1 in one
652
- * call (the `POST` behind `lotics app publish --yes`). The server extracts an
652
+ * call (the `POST` behind `opctl app publish --yes`). The server extracts an
653
653
  * alias-keyed contract from the app (fresh aliases; `renames` fixes them before
654
654
  * v1 freezes), creates the registry package (name/description from the app),
655
655
  * publishes v1 from the app's deployed source + dist, and pins the origin as
656
656
  * installation #1. Error findings from extract surface as a 409. An
657
657
  * already-linked app must use `releasePackage` instead. Admin-only. Backs
658
- * `lotics app publish <app_id>`.
658
+ * `opctl app publish <app_id>`.
659
659
  */
660
660
  publishAppAsPackage(app_id: string, body: {
661
661
  renames?: Array<{
package/dist/client.js CHANGED
@@ -209,7 +209,7 @@ export class LoticsClient {
209
209
  /**
210
210
  * Uninstall a standalone content package — delete the installation row. By
211
211
  * default the package-bound docs are ARCHIVED; `keep_content` retains them as
212
- * ordinary workspace docs. Admin-only. Backs `lotics uninstall`.
212
+ * ordinary workspace docs. Admin-only. Backs `opctl uninstall`.
213
213
  */
214
214
  async uninstallContentPackage(installation_id, opts = {}) {
215
215
  const qs = opts.keep_content ? "?keep_content=true" : "";
@@ -244,7 +244,7 @@ export class LoticsClient {
244
244
  return this.request("GET", `/v1/workspaces/${encodeURIComponent(workspace_id)}/content-installations`);
245
245
  }
246
246
  /**
247
- * Uninstall a package installation (backs `lotics uninstall`). Does
247
+ * Uninstall a package installation (backs `opctl uninstall`). Does
248
248
  * everything DELETE does plus archives the installation's lifecycle
249
249
  * artifacts; with `archive_tables` it also archives the scaffolded entity
250
250
  * tables — refused server-side unless this installation created them
@@ -254,7 +254,7 @@ export class LoticsClient {
254
254
  return this.request("POST", `/v1/apps/${encodeURIComponent(app_id)}/uninstall`, body);
255
255
  }
256
256
  /**
257
- * Partial-merge a package installation's config (backs `lotics package config
257
+ * Partial-merge a package installation's config (backs `opctl package config
258
258
  * --set`). Only the provided keys change; validated against the installed
259
259
  * contract. Returns the full effective config. Admin-only.
260
260
  */
@@ -286,13 +286,13 @@ export class LoticsClient {
286
286
  * org to the target version (latest when omitted) in one call. Hands-off
287
287
  * applies only where the preview is clean; installations with breaking/
288
288
  * drift/modified-core findings are skipped and reported for the normal
289
- * per-installation consent flow. Backs `lotics upgrade <package_id>` (fleet).
289
+ * per-installation consent flow. Backs `opctl upgrade <package_id>` (fleet).
290
290
  * Admin-only; org-scoped (no workspace header needed).
291
291
  */
292
292
  /**
293
293
  * Yank / unyank a published package version — refuses NEW installs/upgrades/
294
294
  * adopts targeting it; pinned installations keep running. Owner-org
295
- * admin-only. Backs `lotics package yank`.
295
+ * admin-only. Backs `opctl package yank`.
296
296
  */
297
297
  async yankPackageVersion(package_id, version, yanked) {
298
298
  return this.request("POST", `/v1/packages/${encodeURIComponent(package_id)}/versions/${version}/yank`, { yanked });
@@ -311,7 +311,7 @@ export class LoticsClient {
311
311
  async getPackage(package_id) {
312
312
  return this.request("GET", `/v1/packages/${encodeURIComponent(package_id)}`);
313
313
  }
314
- /** Version history newest-first (no contract payloads) — backs `lotics package show`. Admin-only. */
314
+ /** Version history newest-first (no contract payloads) — backs `opctl package show`. Admin-only. */
315
315
  async listPackageVersions(package_id) {
316
316
  return this.request("GET", `/v1/packages/${encodeURIComponent(package_id)}/versions`);
317
317
  }
@@ -345,13 +345,13 @@ export class LoticsClient {
345
345
  /**
346
346
  * Health check for a package installation — version pin vs. registry latest,
347
347
  * binding drift, and locally modified core artifacts. Read-only; backs
348
- * `lotics package doctor`. Admin-only.
348
+ * `opctl package doctor`. Admin-only.
349
349
  */
350
350
  async getPackageHealth(app_id) {
351
351
  return this.request("GET", `/v1/apps/${encodeURIComponent(app_id)}/package-health`);
352
352
  }
353
353
  /**
354
- * Preview a release — the dry run behind `lotics app release`. Runs the
354
+ * Preview a release — the dry run behind `opctl app release`. Runs the
355
355
  * binding-aware extract of the origin (aliases stable through the app's current
356
356
  * binding) and reports the next version number, the new + changed aliases, the
357
357
  * bundled-knowledge delta, and any extract findings (an `error` blocks the
@@ -379,13 +379,13 @@ export class LoticsClient {
379
379
  * bundled-knowledge set (added/dropped/re-snapshotted docs; omitted preserves
380
380
  * the current corpus). Error findings from extract surface as a 409; a
381
381
  * missing/archived declared doc is a 400, a foreign-package doc a 409. Admin,
382
- * owning-org only. Backs `lotics app release --yes`.
382
+ * owning-org only. Backs `opctl app release --yes`.
383
383
  */
384
384
  async releasePackage(app_id, body) {
385
385
  return this.request("POST", `/v1/apps/${encodeURIComponent(app_id)}/package-release`, body);
386
386
  }
387
387
  /**
388
- * Dry-run preview of a first-release — the `GET` behind `lotics app publish`
388
+ * Dry-run preview of a first-release — the `GET` behind `opctl app publish`
389
389
  * (no `--yes`), the publish-side analogue of `previewPackageRelease`. The
390
390
  * server runs the same fresh-alias extract + `src/` scan the apply runs
391
391
  * (through any `renames`) and returns the package name it would mint, the
@@ -408,13 +408,13 @@ export class LoticsClient {
408
408
  }
409
409
  /**
410
410
  * First-release apply — mint a package from a BESPOKE app and publish v1 in one
411
- * call (the `POST` behind `lotics app publish --yes`). The server extracts an
411
+ * call (the `POST` behind `opctl app publish --yes`). The server extracts an
412
412
  * alias-keyed contract from the app (fresh aliases; `renames` fixes them before
413
413
  * v1 freezes), creates the registry package (name/description from the app),
414
414
  * publishes v1 from the app's deployed source + dist, and pins the origin as
415
415
  * installation #1. Error findings from extract surface as a 409. An
416
416
  * already-linked app must use `releasePackage` instead. Admin-only. Backs
417
- * `lotics app publish <app_id>`.
417
+ * `opctl app publish <app_id>`.
418
418
  */
419
419
  async publishAppAsPackage(app_id, body) {
420
420
  return this.request("POST", `/v1/apps/${encodeURIComponent(app_id)}/package-publish`, body);