@lotics/cli 0.88.0 → 0.88.1

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.
Files changed (87) hide show
  1. package/dist/render_page.js +100 -100
  2. package/dist/src/cli.js +100 -100
  3. package/dist/{client.d.ts → src/client.d.ts} +1 -1
  4. package/dist/src/package_content_types.d.ts +70 -0
  5. package/dist/src/package_content_types.js +12 -0
  6. package/package.json +2 -2
  7. package/dist/app_commands.d.ts +0 -401
  8. package/dist/app_commands.js +0 -1520
  9. package/dist/app_commands.test.d.ts +0 -1
  10. package/dist/app_commands.test.js +0 -898
  11. package/dist/app_workflow_check.d.ts +0 -77
  12. package/dist/app_workflow_check.js +0 -169
  13. package/dist/app_workflow_check.test.d.ts +0 -1
  14. package/dist/app_workflow_check.test.js +0 -166
  15. package/dist/args.d.ts +0 -42
  16. package/dist/args.js +0 -121
  17. package/dist/args.test.d.ts +0 -1
  18. package/dist/args.test.js +0 -88
  19. package/dist/child_env.d.ts +0 -13
  20. package/dist/child_env.js +0 -24
  21. package/dist/cli.d.ts +0 -2
  22. package/dist/cli.js +0 -1095
  23. package/dist/cli_dispatch.test.d.ts +0 -1
  24. package/dist/cli_dispatch.test.js +0 -89
  25. package/dist/client.test.d.ts +0 -1
  26. package/dist/client.test.js +0 -47
  27. package/dist/config.d.ts +0 -124
  28. package/dist/config.js +0 -315
  29. package/dist/config.test.d.ts +0 -1
  30. package/dist/config.test.js +0 -319
  31. package/dist/dev/file_relay.d.ts +0 -39
  32. package/dist/dev/file_relay.js +0 -87
  33. package/dist/dev/file_relay.test.d.ts +0 -1
  34. package/dist/dev/file_relay.test.js +0 -87
  35. package/dist/dev/rpc_handler.d.ts +0 -21
  36. package/dist/dev/rpc_handler.js +0 -166
  37. package/dist/dev/rpc_handler.test.d.ts +0 -1
  38. package/dist/dev/rpc_handler.test.js +0 -107
  39. package/dist/dev/server.d.ts +0 -75
  40. package/dist/dev/server.js +0 -443
  41. package/dist/dev/upload_relay.d.ts +0 -36
  42. package/dist/dev/upload_relay.js +0 -61
  43. package/dist/dev/upload_relay.test.d.ts +0 -1
  44. package/dist/dev/upload_relay.test.js +0 -76
  45. package/dist/dev/wrapper_page.d.ts +0 -40
  46. package/dist/dev/wrapper_page.js +0 -335
  47. package/dist/docx.d.ts +0 -2
  48. package/dist/docx.js +0 -341
  49. package/dist/docx.test.d.ts +0 -1
  50. package/dist/docx.test.js +0 -145
  51. package/dist/file_command_io.d.ts +0 -12
  52. package/dist/file_command_io.js +0 -34
  53. package/dist/generate_app_agents_dts.d.ts +0 -16
  54. package/dist/generate_app_agents_dts.js +0 -62
  55. package/dist/generate_app_agents_dts.test.d.ts +0 -1
  56. package/dist/generate_app_agents_dts.test.js +0 -45
  57. package/dist/generate_app_fields.d.ts +0 -56
  58. package/dist/generate_app_fields.js +0 -148
  59. package/dist/generate_app_fields.test.d.ts +0 -1
  60. package/dist/generate_app_fields.test.js +0 -108
  61. package/dist/generate_app_queries_dts.d.ts +0 -21
  62. package/dist/generate_app_queries_dts.js +0 -57
  63. package/dist/generate_app_workflows_dts.d.ts +0 -39
  64. package/dist/generate_app_workflows_dts.js +0 -229
  65. package/dist/generate_app_workflows_dts.test.d.ts +0 -1
  66. package/dist/generate_app_workflows_dts.test.js +0 -109
  67. package/dist/generate_package_fields.d.ts +0 -49
  68. package/dist/generate_package_fields.js +0 -157
  69. package/dist/generate_package_fields.test.d.ts +0 -1
  70. package/dist/generate_package_fields.test.js +0 -59
  71. package/dist/inputs.d.ts +0 -38
  72. package/dist/inputs.js +0 -50
  73. package/dist/inputs.test.d.ts +0 -1
  74. package/dist/inputs.test.js +0 -89
  75. package/dist/preview.d.ts +0 -3
  76. package/dist/preview.js +0 -233
  77. package/dist/starter_template.d.ts +0 -64
  78. package/dist/starter_template.js +0 -753
  79. package/dist/starter_template.test.d.ts +0 -1
  80. package/dist/starter_template.test.js +0 -115
  81. package/dist/version.d.ts +0 -1
  82. package/dist/version.js +0 -6
  83. package/dist/xlsx.d.ts +0 -2
  84. package/dist/xlsx.js +0 -489
  85. package/dist/xlsx.test.d.ts +0 -1
  86. package/dist/xlsx.test.js +0 -131
  87. /package/dist/{client.js → src/client.js} +0 -0
@@ -1 +0,0 @@
1
- export {};
@@ -1,89 +0,0 @@
1
- /**
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.
8
- *
9
- * The fake API key satisfies `requireClient` (client construction is offline);
10
- * every asserted path exits before any network call.
11
- */
12
- import { beforeAll, describe, expect, it } from "vitest";
13
- import { execFileSync, spawnSync } from "node:child_process";
14
- import fs from "node:fs";
15
- import os from "node:os";
16
- import path from "node:path";
17
- import { fileURLToPath } from "node:url";
18
- const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
19
- const cliBin = path.join(packageRoot, "dist", "src", "cli.js");
20
- const emptyCwd = fs.mkdtempSync(path.join(os.tmpdir(), "lotics-cli-dispatch-"));
21
- function runCli(args) {
22
- const result = spawnSync(process.execPath, [cliBin, ...args], {
23
- cwd: emptyCwd,
24
- env: {
25
- ...process.env,
26
- LOTICS_API_KEY: "ltk_dispatch_smoke_fake_key_never_used_on_wire",
27
- LOTICS_WORKSPACE: "wsp_dispatch_smoke_fake",
28
- LOTICS_ORG: "",
29
- },
30
- encoding: "utf-8",
31
- timeout: 30_000,
32
- });
33
- return { status: result.status, stderr: result.stderr ?? "" };
34
- }
35
- beforeAll(() => {
36
- // Bundle the real artifact from src (esbuild only — vitest's transform is
37
- // already type-agnostic). ~1s.
38
- execFileSync(process.execPath, ["scripts/build_cli.mjs"], { cwd: packageRoot });
39
- }, 120_000);
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/);
48
- expect(status).toBe(1);
49
- });
50
- }
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/);
56
- expect(status).toBe(1);
57
- });
58
- }
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,
64
- });
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/);
70
- });
71
- });
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/);
76
- expect(status).toBe(1);
77
- });
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>/);
82
- expect(status).toBe(1);
83
- });
84
- it('routes "run" to its usage gate', () => {
85
- const { status, stderr } = runCli(["run"]);
86
- expect(stderr).toMatch(/Usage: lotics run <tool>/);
87
- expect(status).toBe(1);
88
- });
89
- });
@@ -1 +0,0 @@
1
- export {};
@@ -1,47 +0,0 @@
1
- import { describe, it, expect, vi, afterEach } from "vitest";
2
- import { LoticsClient, transportErrorMessage } from "./client.js";
3
- describe("transportErrorMessage", () => {
4
- it("returns a body-free gateway message for a non-JSON 524 (never the HTML)", () => {
5
- const msg = transportErrorMessage(524, null);
6
- expect(msg).not.toMatch(/<html|<!DOCTYPE/i);
7
- expect(msg.toLowerCase()).toContain("gateway timeout");
8
- });
9
- it("returns a friendly message for any 5xx, ignoring a JSON body", () => {
10
- expect(transportErrorMessage(503, { message: "internal detail" })).not.toBe("internal detail");
11
- expect(transportErrorMessage(503, { message: "internal detail" })).not.toMatch(/<html/i);
12
- });
13
- it("surfaces a genuine 4xx JSON error message verbatim", () => {
14
- expect(transportErrorMessage(400, { message: "record_id is required" })).toBe("record_id is required");
15
- });
16
- it("never leaks a non-JSON 4xx body (WAF / redirect HTML)", () => {
17
- expect(transportErrorMessage(403, null)).not.toMatch(/<html|<!DOCTYPE/i);
18
- });
19
- it("falls back to a friendly message for a JSON body with no message field", () => {
20
- expect(transportErrorMessage(409, { error_code: "CONFLICT" })).toBeTruthy();
21
- expect(transportErrorMessage(409, { error_code: "CONFLICT" })).not.toMatch(/<html/i);
22
- });
23
- });
24
- describe("LoticsClient.appWorkflow", () => {
25
- afterEach(() => vi.unstubAllGlobals());
26
- const client = new LoticsClient({ apiKey: "ltk_test", workspaceId: "wsp_test" });
27
- it("normalizes a 524 HTML gateway response to a WorkflowResult error (no raw HTML)", async () => {
28
- const html = "<!DOCTYPE html><html><head><title>error</title></head><body>524: A timeout occurred</body></html>";
29
- vi.stubGlobal("fetch", vi.fn(async () => new Response(html, { status: 524 })));
30
- const result = (await client.appWorkflow("app_1", "wf", {}));
31
- expect(result.status).toBe("error");
32
- expect(result.message).not.toMatch(/<html|<!DOCTYPE/i);
33
- });
34
- it("passes a 200 workflow result through unchanged", async () => {
35
- vi.stubGlobal("fetch", vi.fn(async () => new Response(JSON.stringify({ status: "success", data: { x: 1 } }), { status: 200 })));
36
- const result = await client.appWorkflow("app_1", "wf", {});
37
- expect(result).toEqual({ status: "success", data: { x: 1 } });
38
- });
39
- it("returns a WorkflowResult error (not a throw) on a network failure", async () => {
40
- vi.stubGlobal("fetch", vi.fn(async () => {
41
- throw new Error("ECONNREFUSED");
42
- }));
43
- const result = (await client.appWorkflow("app_1", "wf", {}));
44
- expect(result.status).toBe("error");
45
- expect(result.message).toContain("ECONNREFUSED");
46
- });
47
- });
package/dist/config.d.ts DELETED
@@ -1,124 +0,0 @@
1
- /**
2
- * A saved credential for one organization. The credential store keys these by
3
- * `org_id`, so registering a second key for the same org updates in place
4
- * rather than accumulating duplicates. `workspace_id` remembers the last
5
- * workspace used in this org, so switching back lands where you left off.
6
- */
7
- export interface Profile {
8
- api_key: string;
9
- org_name: string;
10
- workspace_id?: string;
11
- }
12
- /**
13
- * The on-disk config. One schema, two file roles:
14
- *
15
- * - **Global** (`~/.lotics/config.json`) is the credential store: `profiles`
16
- * (org_id → key) plus the `active_org` pointer, the account `email`, and the
17
- * update-check cache. This is the only place API keys live.
18
- * - **Local** (`.lotics/config.json` in a project/worktree) is a pin: an
19
- * `active_org` pointer (+ optional `workspace_id`) whose key is resolved from
20
- * the global store. It holds no key and never needs `profiles`.
21
- *
22
- * Ephemeral / CI access uses the `LOTICS_API_KEY` env var, not a file.
23
- */
24
- export interface LoticsConfig {
25
- profiles?: Record<string, Profile>;
26
- active_org?: string;
27
- workspace_id?: string;
28
- email?: string;
29
- last_update_check?: number;
30
- latest_version?: string;
31
- }
32
- /**
33
- * Where a config operation reads from or writes to.
34
- *
35
- * - `"auto"` — walk up from the current working directory; the first ancestor
36
- * containing `.lotics/config.json` wins, else the global `~/.lotics`.
37
- * - `"local"` — `.lotics/config.json` directly under the current working
38
- * directory. Used to pin the current directory exactly (`--local`), with no
39
- * walk and no inheriting an ancestor's config.
40
- */
41
- export type ConfigScope = "auto" | "local";
42
- export declare function loadConfig(scope?: ConfigScope): LoticsConfig | null;
43
- export declare function saveConfig(config: LoticsConfig, scope?: ConfigScope): void;
44
- export declare function deleteConfig(scope?: ConfigScope): void;
45
- export declare function getConfigPath(scope?: ConfigScope): string;
46
- /** The global credential store (`~/.lotics/config.json`), regardless of cwd. */
47
- export declare function loadGlobalConfig(): LoticsConfig | null;
48
- export declare function saveGlobalConfig(config: LoticsConfig): void;
49
- /**
50
- * The local pin governing the current directory, or null if there isn't one.
51
- * Walks up like `"auto"`, but returns null when the walk lands on the global
52
- * file — so callers can treat "no local pin" distinctly from "global store".
53
- */
54
- export declare function loadLocalConfig(): LoticsConfig | null;
55
- /**
56
- * Find a profile by org id (exact) or org name (case-insensitive). Throws if a
57
- * name is ambiguous across profiles — the caller must use the id. Returns null
58
- * when nothing matches.
59
- */
60
- export declare function resolveProfileByNameOrId(profiles: Record<string, Profile>, nameOrId: string): [string, Profile] | null;
61
- export type ContextSource = "flag" | "env_key" | "env_org" | "local_pointer" | "global_profile";
62
- export interface ResolvedContext {
63
- apiKey: string;
64
- workspaceId?: string;
65
- orgId?: string;
66
- orgName?: string;
67
- source: ContextSource;
68
- }
69
- /**
70
- * Resolve the effective `(apiKey, workspaceId)` for a command, plus where they
71
- * came from. Precedence (highest first):
72
- *
73
- * 1. `--api-key` flag
74
- * 2. `LOTICS_API_KEY` env
75
- * 3. `LOTICS_ORG` env (name|id → global profile)
76
- * 4. local `.lotics/config.json` `active_org` pointer (key from global store)
77
- * 5. global `active_org` profile
78
- *
79
- * `--workspace` flag and `LOTICS_WORKSPACE` env override the workspace at every
80
- * level. Throws (fails loud) when a pin names an org with no saved credential —
81
- * never silently falls back to a different org.
82
- */
83
- export declare function resolveContext(flags: {
84
- apiKey?: string;
85
- workspace?: string;
86
- }): ResolvedContext | null;
87
- /**
88
- * Add or update a profile in the global store. Does not change which org is
89
- * active — the caller decides that via `setActiveOrg`. Preserves the profile's
90
- * remembered workspace when the caller doesn't supply a fresh one. Writes only
91
- * the known fields, so any stray top-level keys in an older file are dropped.
92
- */
93
- export declare function upsertProfile(orgId: string, fields: {
94
- api_key: string;
95
- org_name: string;
96
- workspace_id?: string;
97
- }): void;
98
- /** Remove a profile. If it was active, the active pointer moves to any other. */
99
- export declare function removeProfile(orgId: string): void;
100
- /**
101
- * Set the active org at the requested scope. `"global"` changes the default
102
- * for every directory that isn't pinned; `"local"` writes a pointer into the
103
- * current directory's `.lotics/config.json`.
104
- */
105
- export declare function setActiveOrg(orgId: string, scope: "global" | "local"): void;
106
- /**
107
- * Persist the selected workspace into whichever scope is active: a local pin
108
- * if the current directory has one, else the active org's global profile.
109
- * Returns the scope written, or null when there's nowhere persistent to write
110
- * it (credentials came from `--api-key`/`LOTICS_API_KEY`, with no saved
111
- * profile) — the caller decides whether that's an error or an in-memory-only
112
- * selection.
113
- */
114
- export declare function setSelectedWorkspace(workspaceId: string): {
115
- scope: "local" | "global";
116
- orgId?: string;
117
- } | null;
118
- /**
119
- * Check for a newer CLI version. Synchronous — prints a warning to stderr
120
- * if the cached latest version is newer than current. Kicks off a background
121
- * fetch if the cache is stale (result is written to the global config for next
122
- * run — the update cadence is a machine-global concern, not per-directory).
123
- */
124
- export declare function checkForUpdate(currentVersion: string): void;
package/dist/config.js DELETED
@@ -1,315 +0,0 @@
1
- import fs from "node:fs";
2
- import path from "node:path";
3
- import os from "node:os";
4
- function globalConfigFile() {
5
- return path.join(os.homedir(), ".lotics", "config.json");
6
- }
7
- /**
8
- * Resolve the config file for a scope.
9
- *
10
- * Resolution keys on the `config.json` *file*, never the `.lotics` directory:
11
- * scaffolded custom-code apps already use `.lotics/` for generated types, so
12
- * directory presence cannot signal a local config.
13
- */
14
- function configFileForScope(scope) {
15
- if (scope === "local") {
16
- return path.join(process.cwd(), ".lotics", "config.json");
17
- }
18
- let dir = process.cwd();
19
- for (;;) {
20
- const candidate = path.join(dir, ".lotics", "config.json");
21
- if (fs.existsSync(candidate)) {
22
- return candidate;
23
- }
24
- const parent = path.dirname(dir);
25
- if (parent === dir) {
26
- break;
27
- }
28
- dir = parent;
29
- }
30
- return globalConfigFile();
31
- }
32
- function readConfigFile(file) {
33
- try {
34
- const raw = fs.readFileSync(file, "utf-8");
35
- return JSON.parse(raw);
36
- }
37
- catch {
38
- return null;
39
- }
40
- }
41
- function writeConfigFile(file, config) {
42
- // The config file holds API keys — keep it owner-only, and write atomically
43
- // (temp file + rename) so a concurrent reader never sees a half-written store
44
- // and a crash mid-write can't truncate the existing one. `mode` on
45
- // writeFileSync only applies on create, so chmod the temp before the rename.
46
- fs.mkdirSync(path.dirname(file), { recursive: true, mode: 0o700 });
47
- const tmp = `${file}.${process.pid}.tmp`;
48
- fs.writeFileSync(tmp, JSON.stringify(config, null, 2) + "\n", { encoding: "utf-8", mode: 0o600 });
49
- fs.chmodSync(tmp, 0o600);
50
- fs.renameSync(tmp, file);
51
- }
52
- export function loadConfig(scope = "auto") {
53
- return readConfigFile(configFileForScope(scope));
54
- }
55
- export function saveConfig(config, scope = "auto") {
56
- writeConfigFile(configFileForScope(scope), config);
57
- }
58
- export function deleteConfig(scope = "auto") {
59
- try {
60
- fs.unlinkSync(configFileForScope(scope));
61
- }
62
- catch {
63
- // Already deleted or never existed
64
- }
65
- }
66
- export function getConfigPath(scope = "auto") {
67
- return configFileForScope(scope);
68
- }
69
- /** The global credential store (`~/.lotics/config.json`), regardless of cwd. */
70
- export function loadGlobalConfig() {
71
- return readConfigFile(globalConfigFile());
72
- }
73
- export function saveGlobalConfig(config) {
74
- writeConfigFile(globalConfigFile(), config);
75
- }
76
- /**
77
- * The local pin governing the current directory, or null if there isn't one.
78
- * Walks up like `"auto"`, but returns null when the walk lands on the global
79
- * file — so callers can treat "no local pin" distinctly from "global store".
80
- */
81
- export function loadLocalConfig() {
82
- const file = configFileForScope("auto");
83
- if (file === globalConfigFile())
84
- return null;
85
- return readConfigFile(file);
86
- }
87
- /**
88
- * Find a profile by org id (exact) or org name (case-insensitive). Throws if a
89
- * name is ambiguous across profiles — the caller must use the id. Returns null
90
- * when nothing matches.
91
- */
92
- export function resolveProfileByNameOrId(profiles, nameOrId) {
93
- const byId = profiles[nameOrId];
94
- if (byId)
95
- return [nameOrId, byId];
96
- const matches = Object.entries(profiles).filter(([, p]) => p.org_name.toLowerCase() === nameOrId.toLowerCase());
97
- if (matches.length === 1)
98
- return matches[0];
99
- if (matches.length > 1) {
100
- throw new Error(`"${nameOrId}" matches multiple saved orgs: ${matches.map(([id]) => id).join(", ")}. Use the org id.`);
101
- }
102
- return null;
103
- }
104
- /**
105
- * Resolve the effective `(apiKey, workspaceId)` for a command, plus where they
106
- * came from. Precedence (highest first):
107
- *
108
- * 1. `--api-key` flag
109
- * 2. `LOTICS_API_KEY` env
110
- * 3. `LOTICS_ORG` env (name|id → global profile)
111
- * 4. local `.lotics/config.json` `active_org` pointer (key from global store)
112
- * 5. global `active_org` profile
113
- *
114
- * `--workspace` flag and `LOTICS_WORKSPACE` env override the workspace at every
115
- * level. Throws (fails loud) when a pin names an org with no saved credential —
116
- * never silently falls back to a different org.
117
- */
118
- export function resolveContext(flags) {
119
- const envKey = process.env.LOTICS_API_KEY;
120
- const envOrg = process.env.LOTICS_ORG;
121
- const envWorkspace = process.env.LOTICS_WORKSPACE;
122
- const wsOverride = flags.workspace ?? envWorkspace;
123
- if (flags.apiKey) {
124
- return { apiKey: flags.apiKey, workspaceId: wsOverride, source: "flag" };
125
- }
126
- if (envKey) {
127
- return { apiKey: envKey, workspaceId: wsOverride, source: "env_key" };
128
- }
129
- const global = loadGlobalConfig();
130
- const profiles = global?.profiles ?? {};
131
- if (envOrg) {
132
- const resolved = resolveProfileByNameOrId(profiles, envOrg);
133
- if (!resolved) {
134
- throw new Error(`LOTICS_ORG="${envOrg}" matches no saved credential. Run "lotics org" to list, or "lotics auth api-key <key>" to add one.`);
135
- }
136
- const [orgId, profile] = resolved;
137
- return {
138
- apiKey: profile.api_key,
139
- orgId,
140
- orgName: profile.org_name,
141
- workspaceId: wsOverride ?? profile.workspace_id,
142
- source: "env_org",
143
- };
144
- }
145
- const local = loadLocalConfig();
146
- if (local?.active_org) {
147
- const profile = profiles[local.active_org];
148
- if (!profile) {
149
- throw new Error(`This directory is pinned to org "${local.active_org}" (.lotics/config.json) but no saved credential exists for it. Run "lotics auth api-key <key>" while authenticated to that org.`);
150
- }
151
- return {
152
- apiKey: profile.api_key,
153
- orgId: local.active_org,
154
- orgName: profile.org_name,
155
- workspaceId: wsOverride ?? local.workspace_id ?? profile.workspace_id,
156
- source: "local_pointer",
157
- };
158
- }
159
- // A local file carrying an inline key is the obsolete self-contained format —
160
- // reject it loudly rather than silently falling through to the global active
161
- // org (which would run this directory against the wrong organization).
162
- if (local && "api_key" in local) {
163
- throw new Error(`This directory's .lotics/config.json uses the old self-contained format (inline key), which is no longer supported. Re-pin with "lotics auth api-key <key> --local" (or "lotics org use <name|id> --local").`);
164
- }
165
- if (global?.active_org) {
166
- const profile = profiles[global.active_org];
167
- if (!profile) {
168
- throw new Error(`Active org "${global.active_org}" has no saved credential. Run "lotics org use <name|id>" to pick one, or "lotics auth api-key <key>".`);
169
- }
170
- return {
171
- apiKey: profile.api_key,
172
- orgId: global.active_org,
173
- orgName: profile.org_name,
174
- workspaceId: wsOverride ?? profile.workspace_id,
175
- source: "global_profile",
176
- };
177
- }
178
- return null;
179
- }
180
- // --- Credential store mutators (always target the global store) ------------
181
- /**
182
- * Add or update a profile in the global store. Does not change which org is
183
- * active — the caller decides that via `setActiveOrg`. Preserves the profile's
184
- * remembered workspace when the caller doesn't supply a fresh one. Writes only
185
- * the known fields, so any stray top-level keys in an older file are dropped.
186
- */
187
- export function upsertProfile(orgId, fields) {
188
- const config = loadGlobalConfig() ?? {};
189
- const existing = config.profiles?.[orgId];
190
- const profile = {
191
- api_key: fields.api_key,
192
- org_name: fields.org_name,
193
- workspace_id: fields.workspace_id ?? existing?.workspace_id,
194
- };
195
- saveGlobalConfig({
196
- profiles: { ...config.profiles, [orgId]: profile },
197
- active_org: config.active_org,
198
- email: config.email,
199
- last_update_check: config.last_update_check,
200
- latest_version: config.latest_version,
201
- });
202
- }
203
- /** Remove a profile. If it was active, the active pointer moves to any other. */
204
- export function removeProfile(orgId) {
205
- const config = loadGlobalConfig();
206
- if (!config?.profiles?.[orgId])
207
- return;
208
- const { [orgId]: _removed, ...rest } = config.profiles;
209
- const remaining = Object.keys(rest);
210
- saveGlobalConfig({
211
- ...config,
212
- profiles: rest,
213
- active_org: config.active_org === orgId ? remaining[0] : config.active_org,
214
- });
215
- }
216
- /**
217
- * Set the active org at the requested scope. `"global"` changes the default
218
- * for every directory that isn't pinned; `"local"` writes a pointer into the
219
- * current directory's `.lotics/config.json`.
220
- */
221
- export function setActiveOrg(orgId, scope) {
222
- if (scope === "local") {
223
- // Write a clean pointer — dropping any stale fields from an older local
224
- // file. Workspace resets to the org's remembered default (the profile's).
225
- saveConfig({ active_org: orgId }, "local");
226
- return;
227
- }
228
- const config = loadGlobalConfig() ?? {};
229
- saveGlobalConfig({ ...config, active_org: orgId });
230
- }
231
- /**
232
- * Persist the selected workspace into whichever scope is active: a local pin
233
- * if the current directory has one, else the active org's global profile.
234
- * Returns the scope written, or null when there's nowhere persistent to write
235
- * it (credentials came from `--api-key`/`LOTICS_API_KEY`, with no saved
236
- * profile) — the caller decides whether that's an error or an in-memory-only
237
- * selection.
238
- */
239
- export function setSelectedWorkspace(workspaceId) {
240
- const local = loadLocalConfig();
241
- if (local?.active_org) {
242
- saveConfig({ ...local, workspace_id: workspaceId }, "auto");
243
- return { scope: "local", orgId: local.active_org };
244
- }
245
- const config = loadGlobalConfig() ?? {};
246
- const orgId = config.active_org;
247
- const profile = orgId ? config.profiles?.[orgId] : undefined;
248
- if (!orgId || !profile) {
249
- return null;
250
- }
251
- saveGlobalConfig({
252
- ...config,
253
- profiles: { ...config.profiles, [orgId]: { ...profile, workspace_id: workspaceId } },
254
- });
255
- return { scope: "global", orgId };
256
- }
257
- const UPDATE_CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000; // 24 hours
258
- /**
259
- * Check for a newer CLI version. Synchronous — prints a warning to stderr
260
- * if the cached latest version is newer than current. Kicks off a background
261
- * fetch if the cache is stale (result is written to the global config for next
262
- * run — the update cadence is a machine-global concern, not per-directory).
263
- */
264
- export function checkForUpdate(currentVersion) {
265
- const config = loadGlobalConfig();
266
- const lastCheck = config?.last_update_check ?? 0;
267
- if (Date.now() - lastCheck < UPDATE_CHECK_INTERVAL_MS) {
268
- if (config?.latest_version && config.latest_version !== currentVersion) {
269
- printUpdateWarning(currentVersion, config.latest_version);
270
- }
271
- return;
272
- }
273
- // Cache is stale — fetch in background, don't block exit
274
- fetchLatestVersion().then((latest) => {
275
- if (!latest)
276
- return;
277
- const existing = loadGlobalConfig() ?? {};
278
- saveGlobalConfig({ ...existing, last_update_check: Date.now(), latest_version: latest });
279
- }).catch(() => { });
280
- }
281
- async function fetchLatestVersion() {
282
- const controller = new AbortController();
283
- const timeout = setTimeout(() => controller.abort(), 3000);
284
- timeout.unref();
285
- try {
286
- const response = await fetch("https://registry.npmjs.org/@lotics/cli/latest", {
287
- signal: controller.signal,
288
- });
289
- if (!response.ok)
290
- return null;
291
- const data = (await response.json());
292
- return data.version ?? null;
293
- }
294
- catch {
295
- return null;
296
- }
297
- finally {
298
- clearTimeout(timeout);
299
- }
300
- }
301
- function isNewerVersion(latest, current) {
302
- const [lMaj, lMin, lPat] = latest.split(".").map(Number);
303
- const [cMaj, cMin, cPat] = current.split(".").map(Number);
304
- if (lMaj !== cMaj)
305
- return lMaj > cMaj;
306
- if (lMin !== cMin)
307
- return lMin > cMin;
308
- return lPat > cPat;
309
- }
310
- function printUpdateWarning(current, latest) {
311
- if (!isNewerVersion(latest, current))
312
- return;
313
- console.error(`\nUpdate available: ${current} → ${latest}`);
314
- console.error(`Run: npm i -g @lotics/cli\n`);
315
- }
@@ -1 +0,0 @@
1
- export {};