@lotics/cli 0.62.1 → 0.64.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
@@ -156,6 +156,8 @@ Run `lotics xlsx` or `lotics docx` with no subcommand for the full list.
156
156
  lotics app create "Sales Desk" # scaffold + deploy v1
157
157
  lotics app pull app_... # bootstrap an existing app locally
158
158
  lotics app deploy -m "Add quote drawer" # build + upload a new version
159
+ lotics app versions # deploy history: version, when, who, -m message (* = live)
160
+ lotics app versions app_... # ...for any app, without pulling it first
159
161
 
160
162
  # Regenerate .lotics/* WITHOUT a deploy: the .d.ts type companions (always) +
161
163
  # the runtime app_fields.ts (when authenticated) — F/OPT maps that address
@@ -196,6 +196,17 @@ export declare function appSetSubdomain(client: LoticsClient, args: {
196
196
  export declare function appRename(client: LoticsClient, args: {
197
197
  name: string;
198
198
  }): Promise<void>;
199
+ /**
200
+ * `lotics app versions [app_id]` — print the app's deploy history, newest
201
+ * first: version number, when, deployer, build status, and the `-m` message.
202
+ * With no `app_id`, reads it from the local `package.json` manifest (run inside
203
+ * the app dir); pass an explicit `app_id` to inspect any app without pulling it.
204
+ * The currently served version is marked with `*`. Admin-only server-side.
205
+ */
206
+ export declare function appVersions(client: LoticsClient, args: {
207
+ app_id?: string;
208
+ limit?: number;
209
+ }): Promise<void>;
199
210
  /**
200
211
  * Where `lotics app pull <app_id>` lands when given NO explicit path. If the cwd
201
212
  * IS already this app's own project (its manifest `app_id` matches), refresh in
@@ -224,7 +235,7 @@ export declare function undeclaredCapabilities(sourceText: string, declared: Rec
224
235
  */
225
236
  export declare function appDeploy(client: LoticsClient, args: {
226
237
  projectDir?: string;
227
- message?: string;
238
+ message: string;
228
239
  }): Promise<void>;
229
240
  /**
230
241
  * `lotics app dev [path] [--port=5174] [--vite-port=5173]`
@@ -630,6 +630,50 @@ export async function appRename(client, args) {
630
630
  throw new Error(res.error);
631
631
  console.error(`App renamed: "${args.name}" (${meta.app_id})`);
632
632
  }
633
+ /**
634
+ * `lotics app versions [app_id]` — print the app's deploy history, newest
635
+ * first: version number, when, deployer, build status, and the `-m` message.
636
+ * With no `app_id`, reads it from the local `package.json` manifest (run inside
637
+ * the app dir); pass an explicit `app_id` to inspect any app without pulling it.
638
+ * The currently served version is marked with `*`. Admin-only server-side.
639
+ */
640
+ export async function appVersions(client, args) {
641
+ const appId = args.app_id ?? readAppMeta(process.cwd()).app_id;
642
+ const { current_version_id, versions } = await client.listAppVersions(appId, {
643
+ limit: args.limit,
644
+ });
645
+ if (versions.length === 0) {
646
+ console.error(`No versions found for ${appId}.`);
647
+ return;
648
+ }
649
+ const fmtWhen = (iso) => {
650
+ const d = new Date(iso);
651
+ if (Number.isNaN(d.getTime()))
652
+ return iso;
653
+ const p = (n) => String(n).padStart(2, "0");
654
+ return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}`;
655
+ };
656
+ const rows = versions.map((v) => ({
657
+ mark: v.id === current_version_id ? "*" : " ",
658
+ ver: `v${v.version}`,
659
+ when: fmtWhen(v.created_at),
660
+ by: v.created_by_name ?? v.created_by ?? "—",
661
+ status: v.build_status,
662
+ message: v.message ?? "",
663
+ }));
664
+ const colWidth = (key, header) => Math.max(header.length, ...rows.map((r) => r[key].length));
665
+ const wVer = colWidth("ver", "VER");
666
+ const wWhen = colWidth("when", "WHEN");
667
+ const wBy = colWidth("by", "DEPLOYER");
668
+ const wStatus = colWidth("status", "STATUS");
669
+ const pad = (s, n) => s.padEnd(n);
670
+ // Title → stderr (status); the table → stdout (data), so it stays pipeable.
671
+ console.error(`Deploy history — ${appId} (${versions.length} version${versions.length === 1 ? "" : "s"}, newest first; * = currently served)`);
672
+ console.log(` ${pad("VER", wVer)} ${pad("WHEN", wWhen)} ${pad("DEPLOYER", wBy)} ${pad("STATUS", wStatus)} MESSAGE`);
673
+ for (const r of rows) {
674
+ console.log(`${r.mark} ${pad(r.ver, wVer)} ${pad(r.when, wWhen)} ${pad(r.by, wBy)} ${pad(r.status, wStatus)} ${r.message}`.trimEnd());
675
+ }
676
+ }
633
677
  /**
634
678
  * Where `lotics app pull <app_id>` lands when given NO explicit path. If the cwd
635
679
  * IS already this app's own project (its manifest `app_id` matches), refresh in
@@ -762,7 +806,11 @@ function readAppSourceText(projectDir) {
762
806
  * both archives in R2, creates an app_versions row, and atomically advances
763
807
  * the app's current_version pointer.
764
808
  */
765
- export async function appDeploy(client, args) {
809
+ export async function appDeploy(client,
810
+ // `message` is required — each deploy is a version row read back by
811
+ // `lotics app versions`; a blank message loses the audit trail. The CLI
812
+ // enforces non-empty at the dispatch; the type enforces it for every caller.
813
+ args) {
766
814
  const projectDir = path.resolve(args.projectDir ?? process.cwd());
767
815
  const meta = readAppMeta(projectDir);
768
816
  // Pre-flight (GAP-29): a capability the code calls but the manifest doesn't
@@ -2,7 +2,7 @@ import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
2
2
  import fs from "node:fs";
3
3
  import path from "node:path";
4
4
  import { tmpdir } from "node:os";
5
- import { stampPulledManifest, undeclaredCapabilities, appDirName, defaultPullTarget, ensureWorkflowTsconfigExcludes, appCodegen, appUiLink, appWorkflowSet, appWorkflowPull, appExecuteWorkflow, writeWorkflowFile, writeWorkflowGlobals, stripWorkflowHeader, FALLBACK_ENVELOPE_PREFIX, FALLBACK_ENVELOPE_SUFFIX, } from "./app_commands.js";
5
+ import { stampPulledManifest, undeclaredCapabilities, appDirName, defaultPullTarget, ensureWorkflowTsconfigExcludes, appCodegen, appUiLink, appVersions, appWorkflowSet, appWorkflowPull, appExecuteWorkflow, writeWorkflowFile, writeWorkflowGlobals, stripWorkflowHeader, FALLBACK_ENVELOPE_PREFIX, FALLBACK_ENVELOPE_SUFFIX, } from "./app_commands.js";
6
6
  /**
7
7
  * `appPull` reads workflows from the live App row (server response), NOT from
8
8
  * the manifest embedded in the extracted source archive. The frozen archive
@@ -724,3 +724,68 @@ describe("ensureWorkflowTsconfigExcludes", () => {
724
724
  expect(fs.readFileSync(tsconfigPath(), "utf-8")).toBe("{ not json,, }");
725
725
  });
726
726
  });
727
+ describe("appVersions", () => {
728
+ let logLines;
729
+ let errLines;
730
+ beforeEach(() => {
731
+ logLines = [];
732
+ errLines = [];
733
+ vi.spyOn(console, "log").mockImplementation((...args) => {
734
+ logLines.push(String(args[0]));
735
+ });
736
+ vi.spyOn(console, "error").mockImplementation((...args) => {
737
+ errLines.push(String(args[0]));
738
+ });
739
+ });
740
+ afterEach(() => {
741
+ vi.restoreAllMocks();
742
+ });
743
+ const clientWith = (payload) => ({ listAppVersions: vi.fn().mockResolvedValue(payload) });
744
+ it("renders history newest-first, marks the current version, and prefers the deployer name", async () => {
745
+ const client = clientWith({
746
+ current_version_id: "apv_2",
747
+ versions: [
748
+ {
749
+ id: "apv_2",
750
+ version: 2,
751
+ message: "add receipt gate",
752
+ build_status: "success",
753
+ bundle_size_bytes: 1234,
754
+ created_at: "2026-06-24T03:04:41Z",
755
+ created_by: "mem_a",
756
+ created_by_name: "Minh Vu",
757
+ },
758
+ {
759
+ id: "apv_1",
760
+ version: 1,
761
+ message: "initial deploy",
762
+ build_status: "success",
763
+ bundle_size_bytes: 1000,
764
+ created_at: "2026-06-23T08:00:00Z",
765
+ created_by: "mem_b",
766
+ created_by_name: null,
767
+ },
768
+ ],
769
+ });
770
+ // Explicit app_id → no manifest/package.json in cwd required.
771
+ await appVersions(client, { app_id: "app_x" });
772
+ // One header row + one row per version, all on stdout.
773
+ expect(logLines.length).toBe(3);
774
+ const v2Row = logLines.find((l) => l.includes("v2"));
775
+ const v1Row = logLines.find((l) => l.includes("v1"));
776
+ // Currently-served version is marked with `*`; the other is not.
777
+ expect(v2Row.startsWith("*")).toBe(true);
778
+ expect(v1Row.startsWith("*")).toBe(false);
779
+ // Messages render; the deployer name is preferred, falling back to the id.
780
+ expect(v2Row).toContain("add receipt gate");
781
+ expect(v2Row).toContain("Minh Vu");
782
+ expect(v1Row).toContain("initial deploy");
783
+ expect(v1Row).toContain("mem_b");
784
+ });
785
+ it("reports an empty history to stderr without printing a table", async () => {
786
+ const client = clientWith({ current_version_id: null, versions: [] });
787
+ await appVersions(client, { app_id: "app_empty" });
788
+ expect(logLines).toEqual([]);
789
+ expect(errLines.join("\n")).toContain("No versions found");
790
+ });
791
+ });
package/dist/cli.js CHANGED
@@ -13,7 +13,7 @@ net.setDefaultAutoSelectFamilyAttemptTimeout(2000);
13
13
  import { LoticsClient, API_BASE_URL } from "./client.js";
14
14
  import { resolveContext, deleteConfig, getConfigPath, loadGlobalConfig, saveGlobalConfig, loadLocalConfig, upsertProfile, removeProfile, setActiveOrg, setSelectedWorkspace, resolveProfileByNameOrId, checkForUpdate, } from "./config.js";
15
15
  import { VERSION } from "./version.js";
16
- import { appCreate, appPull, appDeploy, appDev, appSetSubdomain, appRename, appCodegen, appExecuteWorkflow, appWorkflowSet, appWorkflowPull, appWorkflowCheck, appUiLink, } from "./app_commands.js";
16
+ import { appCreate, appPull, appDeploy, appDev, appSetSubdomain, appRename, appVersions, appCodegen, appExecuteWorkflow, appWorkflowSet, appWorkflowPull, appWorkflowCheck, appUiLink, } from "./app_commands.js";
17
17
  import { parseArgs } from "./args.js";
18
18
  import { ingestJsonArgs } from "./inputs.js";
19
19
  import { runXlsxCommand } from "./xlsx.js";
@@ -71,9 +71,13 @@ COMMANDS
71
71
  Download all files on a record file field
72
72
  lotics app create <name> [path] Create a new custom-code app + scaffold locally
73
73
  lotics app pull <app_id> [path] Bootstrap full local env (source + npm install + types)
74
- lotics app deploy [-m <message>] Build + upload current dir as a new version
74
+ lotics app deploy -m <message> Build + upload current dir as a new version
75
+ (-m is required — it's the version's audit trail)
75
76
  (code + queries only — workflow bindings are
76
77
  managed by set_app_workflow / remove_app_workflow)
78
+ lotics app versions [app_id] Show deploy history newest-first (version,
79
+ timestamp, deployer, build status, -m message;
80
+ * marks the currently served version)
77
81
  lotics app codegen [path] Regenerate .lotics/* (types + field/option ids)
78
82
  from the manifest + workspace schema — no deploy
79
83
  lotics app workflow run <alias> '<json>' Execute a bound app workflow end-to-end
@@ -595,7 +599,8 @@ async function main() {
595
599
  console.error("Usage:");
596
600
  console.error(" lotics app create <name> [path] Scaffold a new app locally");
597
601
  console.error(" lotics app pull <app_id> [path] Pull an existing app for local editing");
598
- console.error(" lotics app deploy [-m <message>] Build + upload the current directory");
602
+ console.error(" lotics app deploy -m <message> Build + upload the current directory (-m required)");
603
+ console.error(" lotics app versions [app_id] Show deploy history (version, when, who, message)");
599
604
  console.error(" lotics app codegen [path] Regenerate .lotics/* (types + field ids) — no deploy");
600
605
  console.error(" lotics app workflow run <alias> '<json>' Execute a bound app workflow end-to-end");
601
606
  console.error(" lotics app workflow set <alias> Push the edited src/workflows/<alias>.ts body");
@@ -725,7 +730,14 @@ async function main() {
725
730
  }
726
731
  if (subcommand === "deploy") {
727
732
  // The message is either `-m <message>` or a bare positional arg after `deploy`.
728
- const message = flags.message ?? toolArgs;
733
+ // Required: every deploy is a version row, and `lotics app versions` reads
734
+ // the message back — a blank one makes the deploy history useless.
735
+ const message = (flags.message ?? toolArgs)?.trim();
736
+ if (!message) {
737
+ console.error('Usage: lotics app deploy -m "<what changed + why>"');
738
+ console.error("A deploy message is required — it's the version's audit trail (see `lotics app versions`).");
739
+ process.exit(1);
740
+ }
729
741
  await appDeploy(client, { message });
730
742
  return;
731
743
  }
@@ -749,6 +761,12 @@ async function main() {
749
761
  await appRename(client, { name: newName });
750
762
  return;
751
763
  }
764
+ if (subcommand === "versions") {
765
+ // Optional positional app_id (inspect any app); else read the local manifest.
766
+ const appId = toolArgs || undefined;
767
+ await appVersions(client, { app_id: appId });
768
+ return;
769
+ }
752
770
  if (subcommand === "workflow") {
753
771
  // `lotics app workflow <run|set|pull> …` — disambiguated subcommands so an
754
772
  // alias can never collide with the verb. `toolArgs` is the verb; `restArgs`
package/dist/client.d.ts CHANGED
@@ -199,6 +199,23 @@ export declare class LoticsClient {
199
199
  build_status: string;
200
200
  }>;
201
201
  getAppVersionSourceUrl(app_id: string, version_id: string): Promise<string>;
202
+ /** Deploy history for an app — newest first. Backs `lotics app versions`. */
203
+ listAppVersions(app_id: string, opts?: {
204
+ limit?: number;
205
+ offset?: number;
206
+ }): Promise<{
207
+ current_version_id: string | null;
208
+ versions: Array<{
209
+ id: string;
210
+ version: number;
211
+ message: string | null;
212
+ build_status: string;
213
+ bundle_size_bytes: number | null;
214
+ created_at: string;
215
+ created_by: string | null;
216
+ created_by_name: string | null;
217
+ }>;
218
+ }>;
202
219
  /**
203
220
  * Run a named query declared in the app's manifest, scoped to the app's IAM
204
221
  * principal. Mirrors POST /v1/apps/{app_id}/query.
package/dist/client.js CHANGED
@@ -244,6 +244,16 @@ export class LoticsClient {
244
244
  const result = await this.request("GET", `/v1/apps/${encodeURIComponent(app_id)}/versions/${encodeURIComponent(version_id)}/source`);
245
245
  return result.url;
246
246
  }
247
+ /** Deploy history for an app — newest first. Backs `lotics app versions`. */
248
+ async listAppVersions(app_id, opts) {
249
+ const qs = new URLSearchParams();
250
+ if (opts?.limit != null)
251
+ qs.set("limit", String(opts.limit));
252
+ if (opts?.offset != null)
253
+ qs.set("offset", String(opts.offset));
254
+ const suffix = qs.toString() ? `?${qs.toString()}` : "";
255
+ return this.request("GET", `/v1/apps/${encodeURIComponent(app_id)}/versions${suffix}`);
256
+ }
247
257
  // ── App iframe RPC endpoints ──────────────────────────────────────────────
248
258
  // These mirror the two ops handled by frontend/features/app_ui/app_iframe_host.tsx.
249
259
  // The deployed iframe sends postMessage to the parent frontend, which calls
package/dist/src/cli.js CHANGED
@@ -29803,6 +29803,14 @@ var LoticsClient = class {
29803
29803
  );
29804
29804
  return result.url;
29805
29805
  }
29806
+ /** Deploy history for an app — newest first. Backs `lotics app versions`. */
29807
+ async listAppVersions(app_id, opts) {
29808
+ const qs = new URLSearchParams();
29809
+ if (opts?.limit != null) qs.set("limit", String(opts.limit));
29810
+ if (opts?.offset != null) qs.set("offset", String(opts.offset));
29811
+ const suffix = qs.toString() ? `?${qs.toString()}` : "";
29812
+ return this.request("GET", `/v1/apps/${encodeURIComponent(app_id)}/versions${suffix}`);
29813
+ }
29806
29814
  // ── App iframe RPC endpoints ──────────────────────────────────────────────
29807
29815
  // These mirror the two ops handled by frontend/features/app_ui/app_iframe_host.tsx.
29808
29816
  // The deployed iframe sends postMessage to the parent frontend, which calls
@@ -32529,6 +32537,47 @@ async function appRename(client, args) {
32529
32537
  if (res.error) throw new Error(res.error);
32530
32538
  console.error(`App renamed: "${args.name}" (${meta.app_id})`);
32531
32539
  }
32540
+ async function appVersions(client, args) {
32541
+ const appId = args.app_id ?? readAppMeta(process.cwd()).app_id;
32542
+ const { current_version_id, versions } = await client.listAppVersions(appId, {
32543
+ limit: args.limit
32544
+ });
32545
+ if (versions.length === 0) {
32546
+ console.error(`No versions found for ${appId}.`);
32547
+ return;
32548
+ }
32549
+ const fmtWhen = (iso) => {
32550
+ const d = new Date(iso);
32551
+ if (Number.isNaN(d.getTime())) return iso;
32552
+ const p = (n) => String(n).padStart(2, "0");
32553
+ return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}`;
32554
+ };
32555
+ const rows = versions.map((v) => ({
32556
+ mark: v.id === current_version_id ? "*" : " ",
32557
+ ver: `v${v.version}`,
32558
+ when: fmtWhen(v.created_at),
32559
+ by: v.created_by_name ?? v.created_by ?? "\u2014",
32560
+ status: v.build_status,
32561
+ message: v.message ?? ""
32562
+ }));
32563
+ const colWidth = (key, header) => Math.max(header.length, ...rows.map((r) => r[key].length));
32564
+ const wVer = colWidth("ver", "VER");
32565
+ const wWhen = colWidth("when", "WHEN");
32566
+ const wBy = colWidth("by", "DEPLOYER");
32567
+ const wStatus = colWidth("status", "STATUS");
32568
+ const pad = (s, n) => s.padEnd(n);
32569
+ console.error(
32570
+ `Deploy history \u2014 ${appId} (${versions.length} version${versions.length === 1 ? "" : "s"}, newest first; * = currently served)`
32571
+ );
32572
+ console.log(
32573
+ ` ${pad("VER", wVer)} ${pad("WHEN", wWhen)} ${pad("DEPLOYER", wBy)} ${pad("STATUS", wStatus)} MESSAGE`
32574
+ );
32575
+ for (const r of rows) {
32576
+ console.log(
32577
+ `${r.mark} ${pad(r.ver, wVer)} ${pad(r.when, wWhen)} ${pad(r.by, wBy)} ${pad(r.status, wStatus)} ${r.message}`.trimEnd()
32578
+ );
32579
+ }
32580
+ }
32532
32581
  function defaultPullTarget(appId, appName) {
32533
32582
  const cwdPkgPath = path5.join(process.cwd(), "package.json");
32534
32583
  if (fs4.existsSync(cwdPkgPath)) {
@@ -49154,9 +49203,13 @@ COMMANDS
49154
49203
  Download all files on a record file field
49155
49204
  lotics app create <name> [path] Create a new custom-code app + scaffold locally
49156
49205
  lotics app pull <app_id> [path] Bootstrap full local env (source + npm install + types)
49157
- lotics app deploy [-m <message>] Build + upload current dir as a new version
49206
+ lotics app deploy -m <message> Build + upload current dir as a new version
49207
+ (-m is required \u2014 it's the version's audit trail)
49158
49208
  (code + queries only \u2014 workflow bindings are
49159
49209
  managed by set_app_workflow / remove_app_workflow)
49210
+ lotics app versions [app_id] Show deploy history newest-first (version,
49211
+ timestamp, deployer, build status, -m message;
49212
+ * marks the currently served version)
49160
49213
  lotics app codegen [path] Regenerate .lotics/* (types + field/option ids)
49161
49214
  from the manifest + workspace schema \u2014 no deploy
49162
49215
  lotics app workflow run <alias> '<json>' Execute a bound app workflow end-to-end
@@ -49631,7 +49684,8 @@ async function main() {
49631
49684
  console.error("Usage:");
49632
49685
  console.error(" lotics app create <name> [path] Scaffold a new app locally");
49633
49686
  console.error(" lotics app pull <app_id> [path] Pull an existing app for local editing");
49634
- console.error(" lotics app deploy [-m <message>] Build + upload the current directory");
49687
+ console.error(" lotics app deploy -m <message> Build + upload the current directory (-m required)");
49688
+ console.error(" lotics app versions [app_id] Show deploy history (version, when, who, message)");
49635
49689
  console.error(" lotics app codegen [path] Regenerate .lotics/* (types + field ids) \u2014 no deploy");
49636
49690
  console.error(" lotics app workflow run <alias> '<json>' Execute a bound app workflow end-to-end");
49637
49691
  console.error(" lotics app workflow set <alias> Push the edited src/workflows/<alias>.ts body");
@@ -49754,7 +49808,12 @@ Available workspaces:`);
49754
49808
  return;
49755
49809
  }
49756
49810
  if (subcommand === "deploy") {
49757
- const message = flags.message ?? toolArgs;
49811
+ const message = (flags.message ?? toolArgs)?.trim();
49812
+ if (!message) {
49813
+ console.error('Usage: lotics app deploy -m "<what changed + why>"');
49814
+ console.error("A deploy message is required \u2014 it's the version's audit trail (see `lotics app versions`).");
49815
+ process.exit(1);
49816
+ }
49758
49817
  await appDeploy(client, { message });
49759
49818
  return;
49760
49819
  }
@@ -49778,6 +49837,11 @@ Available workspaces:`);
49778
49837
  await appRename(client, { name: newName });
49779
49838
  return;
49780
49839
  }
49840
+ if (subcommand === "versions") {
49841
+ const appId = toolArgs || void 0;
49842
+ await appVersions(client, { app_id: appId });
49843
+ return;
49844
+ }
49781
49845
  if (subcommand === "workflow") {
49782
49846
  const action = toolArgs;
49783
49847
  const workflowUsage = () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lotics/cli",
3
- "version": "0.62.1",
3
+ "version": "0.64.0",
4
4
  "description": "Lotics SDK and CLI for AI agents",
5
5
  "type": "module",
6
6
  "bin": {