@lotics/cli 0.75.0 → 0.76.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -25,9 +25,9 @@ import fs from "node:fs";
25
25
  import path from "node:path";
26
26
  import { tmpdir } from "node:os";
27
27
  import "./client.js";
28
- import { buildStarterTemplate, STARTER_FALLBACK_SDK_VERSION } from "./starter_template.js";
28
+ import { buildStarterTemplate, buildPackageStarterOverrides, STARTER_FALLBACK_SDK_VERSION, } from "./starter_template.js";
29
29
  import { generatePackageAppFields, } from "./generate_package_fields.js";
30
- import { appDirName, fetchLatestNpmVersion, runNpm, runTar } from "./app_commands.js";
30
+ import { appDirName, fetchLatestNpmVersion, runNpm, runTar, writeAppDts } from "./app_commands.js";
31
31
  import { writeFileAtomic } from "./file_command_io.js";
32
32
  import { startDevServer, openBrowser } from "./dev/server.js";
33
33
  const CONTRACT_FILE = "contract.json";
@@ -361,6 +361,12 @@ export async function packageNew(args) {
361
361
  ui_version: uiLatest ? `^${uiLatest}` : undefined,
362
362
  sdk_version: packageSdkRange(sdkLatest),
363
363
  });
364
+ // Package-specific code surface swapped in BY PATH (same mechanism as the
365
+ // package.json manifest swap below): the shared app starter's App.tsx demos
366
+ // in-app routing over static data, wrong for a package. The package starter's
367
+ // App.tsx exercises the binding (F/OPT + useQuery/useConfig), its test mocks the
368
+ // SDK boundary, and its README carries the contract reference.
369
+ const overrides = new Map(buildPackageStarterOverrides({ app_name: args.name }).map((f) => [f.path, f.content]));
364
370
  for (const file of files) {
365
371
  const fullPath = path.join(targetPath, file.path);
366
372
  if (file.path === "package.json") {
@@ -378,7 +384,7 @@ export async function packageNew(args) {
378
384
  continue;
379
385
  }
380
386
  fs.mkdirSync(path.dirname(fullPath), { recursive: true });
381
- fs.writeFileSync(fullPath, file.content);
387
+ fs.writeFileSync(fullPath, overrides.get(file.path) ?? file.content);
382
388
  }
383
389
  fs.writeFileSync(path.join(targetPath, CONTRACT_FILE), JSON.stringify(starterContract(args.name), null, 2) + "\n");
384
390
  writePackageAppFields(targetPath);
@@ -430,6 +436,7 @@ async function publishVersion(client, projectDir, opts) {
430
436
  contract,
431
437
  bundle,
432
438
  changelog: opts.changelog ?? null,
439
+ channel: opts.channel ?? "release",
433
440
  });
434
441
  project.manifest.version = version.version;
435
442
  writePackageManifest(projectDir, project);
@@ -487,8 +494,12 @@ async function syncToDevWorkspace(client, projectDir) {
487
494
  // Contract may have been edited since the last sync — regenerate the
488
495
  // runtime F/OPT/ROLE surface before the build that publishVersion runs.
489
496
  writePackageAppFields(projectDir);
497
+ // 'dev' channel: a real immutable version this dev installation pins by
498
+ // number, but never what "install latest" resolves to — a broken
499
+ // mid-iteration contract must not become the org-wide installable default.
490
500
  const { package_id, version } = await publishVersion(client, projectDir, {
491
501
  changelog: "dev sync",
502
+ channel: "dev",
492
503
  });
493
504
  // Re-read after publish (publishVersion may have stamped the package id).
494
505
  const project = readPackageProject(projectDir);
@@ -507,6 +518,18 @@ async function syncToDevWorkspace(client, projectDir) {
507
518
  project.manifest.dev[devWorkspaceId] = { app_id: appId, version };
508
519
  writePackageManifest(projectDir, project);
509
520
  const installed = await client.getApp(appId);
521
+ // Regenerate the typed `.lotics/app_{queries,workflows,agents}.d.ts` companions
522
+ // from the live installation — the SAME maps (`apps.queries`/`workflows`/`agents`,
523
+ // keyed by the contract's runtime aliases) `lotics app pull` codegens from — so
524
+ // `useQuery("items")` / `useWorkflow` are typed in the package dev loop, not just
525
+ // in pulled app projects. (`.lotics/app_fields.ts`, the runtime F/OPT surface, was
526
+ // already refreshed from the contract before publish.) `writeAppDts` also heals the
527
+ // tsconfig include/exclude so the generated `.d.ts` actually load.
528
+ writeAppDts(projectDir, {
529
+ workflows: installed.workflows ?? undefined,
530
+ queries: installed.queries ?? undefined,
531
+ agents: installed.agents ?? undefined,
532
+ });
510
533
  return { app_id: appId, app_name: installed.name, workspace_id: devWorkspaceId, version };
511
534
  }
512
535
  /**
@@ -730,15 +753,48 @@ export async function packageUpgrade(client, args) {
730
753
  });
731
754
  console.error(`Upgraded ${app.name} → v${app.package_version} (${app.id}).`);
732
755
  }
756
+ /** The install-consent trust line: official > your own org > third-party. */
757
+ function trustBadge(pkg) {
758
+ if (pkg.is_official)
759
+ return "official Lotics package";
760
+ if (pkg.owned_by_caller === true)
761
+ return "your organization's package";
762
+ return "third-party package (runs under your authority once installed)";
763
+ }
764
+ /**
765
+ * `lotics package show <package_id>` — registry metadata + version history
766
+ * (trust badge, retirement, per-version channel/yank/changelog). The read
767
+ * surface for "what is this package and what shipped when".
768
+ */
769
+ export async function packageShow(client, args) {
770
+ const pkg = await client.getAppPackage(args.package_id);
771
+ const { versions } = await client.listAppPackageVersions(args.package_id);
772
+ console.error(`${pkg.name} (${pkg.id}) — ${trustBadge(pkg)}`);
773
+ if (pkg.description)
774
+ console.error(` ${pkg.description}`);
775
+ if (pkg.retired_at)
776
+ console.error(` RETIRED ${pkg.retired_at}`);
777
+ console.error(` latest installable: ${pkg.latest_version > 0 ? `v${pkg.latest_version}` : "none"}`);
778
+ console.error("");
779
+ for (const v of versions) {
780
+ const marks = [
781
+ v.version === pkg.latest_version ? "*" : " ",
782
+ v.channel === "dev" ? "dev" : " ",
783
+ v.yanked_at ? "YANKED" : " ",
784
+ ].join(" ");
785
+ console.log(`${marks} v${v.version} ${v.created_at} ${v.changelog ?? ""}`.trimEnd());
786
+ }
787
+ if (versions.length === 0)
788
+ console.error(" (no published versions)");
789
+ }
733
790
  export async function packageInstall(client, args) {
734
791
  // Surface the trust badge at the consent point: installing materializes the
735
792
  // package's workflows/agents under YOUR authority, so say whose package it is.
736
793
  const pkg = await client.getAppPackage(args.package_id);
737
- console.error(pkg.is_official
738
- ? `Installing ${pkg.name} — official Lotics package...`
739
- : `Installing ${pkg.name} — third-party package (runs under your authority once installed)...`);
794
+ console.error(`Installing ${pkg.name} — ${trustBadge(pkg)}...`);
740
795
  const app = await client.installAppPackage(args.package_id, {
741
796
  ...(args.version !== undefined ? { version: args.version } : {}),
797
+ ...(args.config !== undefined ? { config: args.config } : {}),
742
798
  });
743
799
  const versionLabel = app.package_version !== null ? `v${app.package_version}` : "(unknown version)";
744
800
  console.error(`Installed ${app.name} ${versionLabel} → ${app.id} (workspace ${app.workspace_id}).`);
@@ -752,6 +808,126 @@ export async function packageEject(client, args) {
752
808
  console.error(" Its data model, queries, workflows, and templates are unchanged.");
753
809
  console.error(" Pull the pinned source for local editing: lotics app pull " + app.id);
754
810
  }
811
+ /**
812
+ * Parse one `key=value` config assignment. When the knob's type is known (from
813
+ * the stored value at `package config --set`) the value is parsed to that type
814
+ * loudly; otherwise (`package install --config`) it is inferred (true/false →
815
+ * boolean, numeric → number, else string) and the server validates it against
816
+ * the contract.
817
+ */
818
+ function parseConfigAssignment(entry, flag, knownType) {
819
+ const eq = entry.indexOf("=");
820
+ if (eq <= 0 || eq === entry.length - 1) {
821
+ throw new Error(`Invalid ${flag} "${entry}" — expected key=value.`);
822
+ }
823
+ const key = entry.slice(0, eq);
824
+ const raw = entry.slice(eq + 1);
825
+ if (knownType === "number") {
826
+ const n = Number(raw);
827
+ if (!Number.isFinite(n))
828
+ throw new Error(`Config key "${key}" is a number knob — "${raw}" is not numeric.`);
829
+ return { key, value: n };
830
+ }
831
+ if (knownType === "boolean") {
832
+ if (raw !== "true" && raw !== "false") {
833
+ throw new Error(`Config key "${key}" is a boolean knob — expected true or false, got "${raw}".`);
834
+ }
835
+ return { key, value: raw === "true" };
836
+ }
837
+ if (knownType === "string")
838
+ return { key, value: raw };
839
+ // Inferred (install override): the server re-validates against the contract.
840
+ if (raw === "true" || raw === "false")
841
+ return { key, value: raw === "true" };
842
+ if (/^-?\d+(\.\d+)?$/.test(raw))
843
+ return { key, value: Number(raw) };
844
+ return { key, value: raw };
845
+ }
846
+ /** `--config key=value` (install): inferred types, server-validated. */
847
+ export function parseInstallConfigFlags(config) {
848
+ const out = {};
849
+ for (const entry of config) {
850
+ const { key, value } = parseConfigAssignment(entry, "--config");
851
+ out[key] = value;
852
+ }
853
+ return out;
854
+ }
855
+ /**
856
+ * `lotics package config <app_id>` — show the installation's effective config;
857
+ * with `--set key=value` (repeatable) partial-merge edits, each value parsed by
858
+ * the knob's current type. No `--set` prints the values.
859
+ */
860
+ export async function packageConfig(client, args) {
861
+ const app = await client.getApp(args.app_id);
862
+ if (!app.package_id) {
863
+ throw new Error(`App ${args.app_id} is not a package installation — it has no config.`);
864
+ }
865
+ const current = app.config ?? {};
866
+ if (args.sets.length === 0) {
867
+ const keys = Object.keys(current).sort();
868
+ if (keys.length === 0) {
869
+ console.error(`${app.name} (${app.id}) — no config knobs.`);
870
+ return;
871
+ }
872
+ console.error(`${app.name} (${app.id}) config:`);
873
+ for (const key of keys)
874
+ console.log(` ${key} = ${JSON.stringify(current[key])}`);
875
+ return;
876
+ }
877
+ const overrides = {};
878
+ for (const entry of args.sets) {
879
+ const key = entry.slice(0, Math.max(0, entry.indexOf("=")));
880
+ const knownType = typeof current[key];
881
+ const { value } = parseConfigAssignment(entry, "--set", knownType === "number" || knownType === "boolean" || knownType === "string" ? knownType : undefined);
882
+ overrides[key] = value;
883
+ }
884
+ const { config } = await client.updateAppPackageConfig(args.app_id, { config: overrides });
885
+ console.error(`Updated config for ${app.name} (${app.id}):`);
886
+ for (const key of Object.keys(config).sort()) {
887
+ const marker = key in overrides ? " *" : "";
888
+ console.log(` ${key} = ${JSON.stringify(config[key])}${marker}`);
889
+ }
890
+ }
891
+ /**
892
+ * `lotics package uninstall <app_id> [--archive-tables]` — remove a package
893
+ * installation. Prints what will be archived (workflow artifacts, plus the
894
+ * scaffolded tables when the flag is set), then uninstalls.
895
+ */
896
+ export async function packageUninstall(client, args) {
897
+ const app = await client.getApp(args.app_id);
898
+ if (!app.package_id) {
899
+ throw new Error(`App ${args.app_id} is not a package installation — use the app delete flow for a bespoke app.`);
900
+ }
901
+ const lifecycleCount = Object.keys(app.binding?.workflows ?? {}).length;
902
+ const boundCount = Object.keys(app.workflows ?? {}).length;
903
+ const tableCount = Object.keys(app.binding?.entities ?? {}).length;
904
+ console.error(`Uninstalling ${app.name} (${app.id}):`);
905
+ console.error(` Archives ${boundCount + lifecycleCount} workflow(s) (${boundCount} app, ${lifecycleCount} lifecycle).`);
906
+ console.error(args.archive_tables
907
+ ? ` Archives ${tableCount} scaffolded table(s) — refused if they were adopted or are still referenced.`
908
+ : ` Leaves the data model (${tableCount} table(s)) intact. Pass --archive-tables to also archive them.`);
909
+ const result = await client.uninstallAppPackage(args.app_id, {
910
+ archive_tables: args.archive_tables,
911
+ });
912
+ console.error(`Uninstalled ${app.name} (${app.id}).`);
913
+ if (result.archived_table_ids.length > 0) {
914
+ console.error(` Archived tables: ${result.archived_table_ids.join(", ")}`);
915
+ }
916
+ }
917
+ /**
918
+ * `lotics package retire <package_id> [--undo]` — retire (or un-retire) a
919
+ * registry package. Owner-org admin-only.
920
+ */
921
+ export async function packageRetire(client, args) {
922
+ const pkg = await client.retireAppPackage(args.package_id, { undo: args.undo });
923
+ if (pkg.retired_at !== null) {
924
+ console.error(`Retired ${pkg.name} (${pkg.id}). New installs refuse it and it is hidden from other orgs; ` +
925
+ `existing installations keep working and may still upgrade.`);
926
+ }
927
+ else {
928
+ console.error(`Un-retired ${pkg.name} (${pkg.id}) — installable again.`);
929
+ }
930
+ }
755
931
  /**
756
932
  * `lotics package extract <app_id> [path]` — promote a bespoke app to a DRAFT
757
933
  * package project (docs/app_packages.md § Promotion). Calls the extract read,