@lotics/cli 0.62.1 → 0.63.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
@@ -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
@@ -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";
@@ -74,6 +74,9 @@ COMMANDS
74
74
  lotics app deploy [-m <message>] Build + upload current dir as a new version
75
75
  (code + queries only — workflow bindings are
76
76
  managed by set_app_workflow / remove_app_workflow)
77
+ lotics app versions [app_id] Show deploy history newest-first (version,
78
+ timestamp, deployer, build status, -m message;
79
+ * marks the currently served version)
77
80
  lotics app codegen [path] Regenerate .lotics/* (types + field/option ids)
78
81
  from the manifest + workspace schema — no deploy
79
82
  lotics app workflow run <alias> '<json>' Execute a bound app workflow end-to-end
@@ -596,6 +599,7 @@ async function main() {
596
599
  console.error(" lotics app create <name> [path] Scaffold a new app locally");
597
600
  console.error(" lotics app pull <app_id> [path] Pull an existing app for local editing");
598
601
  console.error(" lotics app deploy [-m <message>] Build + upload the current directory");
602
+ console.error(" lotics app versions [app_id] Show deploy history (version, when, who, message)");
599
603
  console.error(" lotics app codegen [path] Regenerate .lotics/* (types + field ids) — no deploy");
600
604
  console.error(" lotics app workflow run <alias> '<json>' Execute a bound app workflow end-to-end");
601
605
  console.error(" lotics app workflow set <alias> Push the edited src/workflows/<alias>.ts body");
@@ -749,6 +753,12 @@ async function main() {
749
753
  await appRename(client, { name: newName });
750
754
  return;
751
755
  }
756
+ if (subcommand === "versions") {
757
+ // Optional positional app_id (inspect any app); else read the local manifest.
758
+ const appId = toolArgs || undefined;
759
+ await appVersions(client, { app_id: appId });
760
+ return;
761
+ }
752
762
  if (subcommand === "workflow") {
753
763
  // `lotics app workflow <run|set|pull> …` — disambiguated subcommands so an
754
764
  // 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)) {
@@ -49157,6 +49206,9 @@ COMMANDS
49157
49206
  lotics app deploy [-m <message>] Build + upload current dir as a new version
49158
49207
  (code + queries only \u2014 workflow bindings are
49159
49208
  managed by set_app_workflow / remove_app_workflow)
49209
+ lotics app versions [app_id] Show deploy history newest-first (version,
49210
+ timestamp, deployer, build status, -m message;
49211
+ * marks the currently served version)
49160
49212
  lotics app codegen [path] Regenerate .lotics/* (types + field/option ids)
49161
49213
  from the manifest + workspace schema \u2014 no deploy
49162
49214
  lotics app workflow run <alias> '<json>' Execute a bound app workflow end-to-end
@@ -49632,6 +49684,7 @@ async function main() {
49632
49684
  console.error(" lotics app create <name> [path] Scaffold a new app locally");
49633
49685
  console.error(" lotics app pull <app_id> [path] Pull an existing app for local editing");
49634
49686
  console.error(" lotics app deploy [-m <message>] Build + upload the current directory");
49687
+ console.error(" lotics app versions [app_id] Show deploy history (version, when, who, message)");
49635
49688
  console.error(" lotics app codegen [path] Regenerate .lotics/* (types + field ids) \u2014 no deploy");
49636
49689
  console.error(" lotics app workflow run <alias> '<json>' Execute a bound app workflow end-to-end");
49637
49690
  console.error(" lotics app workflow set <alias> Push the edited src/workflows/<alias>.ts body");
@@ -49778,6 +49831,11 @@ Available workspaces:`);
49778
49831
  await appRename(client, { name: newName });
49779
49832
  return;
49780
49833
  }
49834
+ if (subcommand === "versions") {
49835
+ const appId = toolArgs || void 0;
49836
+ await appVersions(client, { app_id: appId });
49837
+ return;
49838
+ }
49781
49839
  if (subcommand === "workflow") {
49782
49840
  const action = toolArgs;
49783
49841
  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.63.0",
4
4
  "description": "Lotics SDK and CLI for AI agents",
5
5
  "type": "module",
6
6
  "bin": {