@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.
- 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 +68 -14
- package/dist/cli_dispatch.test.d.ts +1 -0
- package/dist/cli_dispatch.test.js +90 -0
- package/dist/client.d.ts +63 -0
- package/dist/client.js +33 -0
- package/dist/package_commands.d.ts +37 -0
- package/dist/package_commands.js +182 -6
- package/dist/src/cli.js +636 -19
- 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/package_commands.js
CHANGED
|
@@ -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.
|
|
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,
|