@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.
- package/dist/render_page.js +100 -100
- package/dist/src/cli.js +100 -100
- package/dist/{client.d.ts → src/client.d.ts} +1 -1
- package/dist/src/package_content_types.d.ts +70 -0
- package/dist/src/package_content_types.js +12 -0
- package/package.json +2 -2
- package/dist/app_commands.d.ts +0 -401
- package/dist/app_commands.js +0 -1520
- package/dist/app_commands.test.d.ts +0 -1
- package/dist/app_commands.test.js +0 -898
- package/dist/app_workflow_check.d.ts +0 -77
- package/dist/app_workflow_check.js +0 -169
- package/dist/app_workflow_check.test.d.ts +0 -1
- package/dist/app_workflow_check.test.js +0 -166
- package/dist/args.d.ts +0 -42
- package/dist/args.js +0 -121
- package/dist/args.test.d.ts +0 -1
- package/dist/args.test.js +0 -88
- package/dist/child_env.d.ts +0 -13
- package/dist/child_env.js +0 -24
- package/dist/cli.d.ts +0 -2
- package/dist/cli.js +0 -1095
- package/dist/cli_dispatch.test.d.ts +0 -1
- package/dist/cli_dispatch.test.js +0 -89
- package/dist/client.test.d.ts +0 -1
- package/dist/client.test.js +0 -47
- package/dist/config.d.ts +0 -124
- package/dist/config.js +0 -315
- package/dist/config.test.d.ts +0 -1
- package/dist/config.test.js +0 -319
- package/dist/dev/file_relay.d.ts +0 -39
- package/dist/dev/file_relay.js +0 -87
- package/dist/dev/file_relay.test.d.ts +0 -1
- package/dist/dev/file_relay.test.js +0 -87
- package/dist/dev/rpc_handler.d.ts +0 -21
- package/dist/dev/rpc_handler.js +0 -166
- package/dist/dev/rpc_handler.test.d.ts +0 -1
- package/dist/dev/rpc_handler.test.js +0 -107
- package/dist/dev/server.d.ts +0 -75
- package/dist/dev/server.js +0 -443
- package/dist/dev/upload_relay.d.ts +0 -36
- package/dist/dev/upload_relay.js +0 -61
- package/dist/dev/upload_relay.test.d.ts +0 -1
- package/dist/dev/upload_relay.test.js +0 -76
- package/dist/dev/wrapper_page.d.ts +0 -40
- package/dist/dev/wrapper_page.js +0 -335
- package/dist/docx.d.ts +0 -2
- package/dist/docx.js +0 -341
- package/dist/docx.test.d.ts +0 -1
- package/dist/docx.test.js +0 -145
- package/dist/file_command_io.d.ts +0 -12
- package/dist/file_command_io.js +0 -34
- package/dist/generate_app_agents_dts.d.ts +0 -16
- package/dist/generate_app_agents_dts.js +0 -62
- package/dist/generate_app_agents_dts.test.d.ts +0 -1
- package/dist/generate_app_agents_dts.test.js +0 -45
- package/dist/generate_app_fields.d.ts +0 -56
- package/dist/generate_app_fields.js +0 -148
- package/dist/generate_app_fields.test.d.ts +0 -1
- package/dist/generate_app_fields.test.js +0 -108
- package/dist/generate_app_queries_dts.d.ts +0 -21
- package/dist/generate_app_queries_dts.js +0 -57
- package/dist/generate_app_workflows_dts.d.ts +0 -39
- package/dist/generate_app_workflows_dts.js +0 -229
- package/dist/generate_app_workflows_dts.test.d.ts +0 -1
- package/dist/generate_app_workflows_dts.test.js +0 -109
- package/dist/generate_package_fields.d.ts +0 -49
- package/dist/generate_package_fields.js +0 -157
- package/dist/generate_package_fields.test.d.ts +0 -1
- package/dist/generate_package_fields.test.js +0 -59
- package/dist/inputs.d.ts +0 -38
- package/dist/inputs.js +0 -50
- package/dist/inputs.test.d.ts +0 -1
- package/dist/inputs.test.js +0 -89
- package/dist/preview.d.ts +0 -3
- package/dist/preview.js +0 -233
- package/dist/starter_template.d.ts +0 -64
- package/dist/starter_template.js +0 -753
- package/dist/starter_template.test.d.ts +0 -1
- package/dist/starter_template.test.js +0 -115
- package/dist/version.d.ts +0 -1
- package/dist/version.js +0 -6
- package/dist/xlsx.d.ts +0 -2
- package/dist/xlsx.js +0 -489
- package/dist/xlsx.test.d.ts +0 -1
- package/dist/xlsx.test.js +0 -131
- /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
|
-
});
|
package/dist/client.test.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
package/dist/client.test.js
DELETED
|
@@ -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
|
-
}
|
package/dist/config.test.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|