@lotics/cli 0.35.0 → 0.36.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 +33 -15
- package/dist/args.d.ts +2 -0
- package/dist/args.js +9 -0
- package/dist/args.test.js +28 -0
- package/dist/cli.js +224 -81
- package/dist/config.d.ts +102 -16
- package/dist/config.js +214 -25
- package/dist/config.test.js +229 -22
- package/dist/src/cli.js +1262 -552
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -39,42 +39,57 @@ lotics auth signup a@b.com --name "Agent" # non-interactive
|
|
|
39
39
|
lotics auth web
|
|
40
40
|
```
|
|
41
41
|
|
|
42
|
-
**`lotics auth api-key`** — Saves an
|
|
42
|
+
**`lotics auth api-key`** — Saves an API key (e.g. one created in the Lotics web app). The key belongs to one org, so this **registers that org as a profile** — run it once per org. Registering a second key adds a profile; it does not overwrite the first.
|
|
43
43
|
|
|
44
44
|
```bash
|
|
45
45
|
lotics auth api-key # interactive prompt
|
|
46
|
-
lotics auth api-key ltk_... #
|
|
46
|
+
lotics auth api-key ltk_... # registers the key's org as a profile (now active)
|
|
47
47
|
```
|
|
48
48
|
|
|
49
|
-
|
|
49
|
+
Run `lotics auth logout [<name|id>]` to remove a profile (default: the active org), or `lotics auth logout --all` to wipe the store.
|
|
50
50
|
|
|
51
|
-
|
|
51
|
+
## Organizations
|
|
52
52
|
|
|
53
|
-
|
|
53
|
+
Each saved API key belongs to one org. Register a key per org once, then switch freely — no re-pasting:
|
|
54
54
|
|
|
55
|
-
|
|
55
|
+
```bash
|
|
56
|
+
lotics org # list saved orgs (marks the active one)
|
|
57
|
+
lotics org use acme # switch active org by name (or org id)
|
|
58
|
+
lotics org use "Acme Corp" # names are case-insensitive
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### Working in parallel (worktrees)
|
|
56
62
|
|
|
57
|
-
|
|
63
|
+
Pin a directory to its own org/workspace so a global `org use` elsewhere never disturbs it. The pin is a pointer — the key still comes from the global store, so there's nothing to paste:
|
|
58
64
|
|
|
59
65
|
```bash
|
|
60
66
|
cd my-worktree
|
|
61
|
-
lotics
|
|
62
|
-
lotics workspace select wks_...
|
|
67
|
+
lotics org use acme --local # writes ./.lotics/config.json { active_org }
|
|
68
|
+
lotics workspace select wks_... # records the workspace in the local pin
|
|
63
69
|
```
|
|
64
70
|
|
|
65
|
-
|
|
71
|
+
Each worktree resolves independently; switching the global default in another shell leaves pinned worktrees untouched. Use `lotics auth whoami` to see the active org, workspace, and which source resolved them. `.lotics/` should be gitignored.
|
|
72
|
+
|
|
73
|
+
### Resolution precedence
|
|
74
|
+
|
|
75
|
+
```
|
|
76
|
+
--api-key flag > LOTICS_API_KEY env > LOTICS_ORG env (name|id)
|
|
77
|
+
> local .lotics/config.json > global active profile
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
`LOTICS_WORKSPACE` (or `--workspace <id>` / `-w`) overrides the workspace at any level. For ephemeral or CI use, set `LOTICS_API_KEY` instead of saving anything.
|
|
66
81
|
|
|
67
82
|
## Workspaces
|
|
68
83
|
|
|
69
|
-
If
|
|
84
|
+
Workspaces live inside the active org. If the org has more than one, select before running tools:
|
|
70
85
|
|
|
71
86
|
```bash
|
|
72
|
-
lotics workspace # list workspaces (marks current)
|
|
73
|
-
lotics workspace select wks_... #
|
|
87
|
+
lotics workspace # list workspaces in the active org (marks current)
|
|
88
|
+
lotics workspace select wks_... # set the workspace for the active scope (pin or profile)
|
|
74
89
|
lotics workspace create "Sales" # create a new workspace (admin only)
|
|
75
90
|
```
|
|
76
91
|
|
|
77
|
-
Single-workspace organizations auto-select on first use.
|
|
92
|
+
Single-workspace organizations auto-select on first use. The selection is remembered per org, so switching back lands where you left off.
|
|
78
93
|
|
|
79
94
|
## CLI
|
|
80
95
|
|
|
@@ -97,8 +112,11 @@ lotics upload ./report.pdf ./data.csv ./documents/
|
|
|
97
112
|
lotics run generate_excel_from_template '{"..."}'
|
|
98
113
|
lotics download <file_id> -o ./reports/
|
|
99
114
|
|
|
100
|
-
# CI / non-interactive
|
|
115
|
+
# CI / non-interactive (key inline)
|
|
101
116
|
LOTICS_API_KEY=ltk_... lotics run query_tables '{}'
|
|
117
|
+
|
|
118
|
+
# One-off against a saved org/workspace, no switching
|
|
119
|
+
LOTICS_ORG=acme LOTICS_WORKSPACE=wks_... lotics run query_tables '{}'
|
|
102
120
|
```
|
|
103
121
|
|
|
104
122
|
## Local .xlsx and .docx authoring
|
package/dist/args.d.ts
CHANGED
|
@@ -21,11 +21,13 @@ export declare function parseArgs(argv: string[]): {
|
|
|
21
21
|
output?: string;
|
|
22
22
|
as?: string;
|
|
23
23
|
apiKey?: string;
|
|
24
|
+
workspace?: string;
|
|
24
25
|
name?: string;
|
|
25
26
|
timezone?: string;
|
|
26
27
|
message?: string;
|
|
27
28
|
forceWorkflowSync: boolean;
|
|
28
29
|
local: boolean;
|
|
30
|
+
all: boolean;
|
|
29
31
|
version: boolean;
|
|
30
32
|
help: boolean;
|
|
31
33
|
};
|
package/dist/args.js
CHANGED
|
@@ -17,11 +17,13 @@ export function parseArgs(argv) {
|
|
|
17
17
|
output: undefined,
|
|
18
18
|
as: undefined,
|
|
19
19
|
apiKey: undefined,
|
|
20
|
+
workspace: undefined,
|
|
20
21
|
name: undefined,
|
|
21
22
|
timezone: undefined,
|
|
22
23
|
message: undefined,
|
|
23
24
|
forceWorkflowSync: false,
|
|
24
25
|
local: false,
|
|
26
|
+
all: false,
|
|
25
27
|
version: false,
|
|
26
28
|
help: false,
|
|
27
29
|
};
|
|
@@ -49,6 +51,10 @@ export function parseArgs(argv) {
|
|
|
49
51
|
case "--api-key":
|
|
50
52
|
flags.apiKey = argv[++i];
|
|
51
53
|
break;
|
|
54
|
+
case "--workspace":
|
|
55
|
+
case "-w":
|
|
56
|
+
flags.workspace = argv[++i];
|
|
57
|
+
break;
|
|
52
58
|
case "--name":
|
|
53
59
|
flags.name = argv[++i];
|
|
54
60
|
break;
|
|
@@ -65,6 +71,9 @@ export function parseArgs(argv) {
|
|
|
65
71
|
case "--local":
|
|
66
72
|
flags.local = true;
|
|
67
73
|
break;
|
|
74
|
+
case "--all":
|
|
75
|
+
flags.all = true;
|
|
76
|
+
break;
|
|
68
77
|
case "--version":
|
|
69
78
|
case "-v":
|
|
70
79
|
flags.version = true;
|
package/dist/args.test.js
CHANGED
|
@@ -34,4 +34,32 @@ describe("parseArgs", () => {
|
|
|
34
34
|
expect(r.flags.forceWorkflowSync).toBe(false);
|
|
35
35
|
expect(r.flags.message).toBeUndefined();
|
|
36
36
|
});
|
|
37
|
+
it("parses --workspace as a value flag", () => {
|
|
38
|
+
const r = parseArgs(["run", "query_tables", "{}", "--workspace", "wsp_123"]);
|
|
39
|
+
expect(r.flags.workspace).toBe("wsp_123");
|
|
40
|
+
expect(r.command).toBe("run");
|
|
41
|
+
expect(r.subcommand).toBe("query_tables");
|
|
42
|
+
expect(r.toolArgs).toBe("{}");
|
|
43
|
+
});
|
|
44
|
+
it("parses -w as the workspace alias", () => {
|
|
45
|
+
const r = parseArgs(["run", "query_tables", "-w", "wsp_123"]);
|
|
46
|
+
expect(r.flags.workspace).toBe("wsp_123");
|
|
47
|
+
});
|
|
48
|
+
it("parses --all as a boolean flag", () => {
|
|
49
|
+
const r = parseArgs(["auth", "logout", "--all"]);
|
|
50
|
+
expect(r.flags.all).toBe(true);
|
|
51
|
+
expect(r.command).toBe("auth");
|
|
52
|
+
expect(r.subcommand).toBe("logout");
|
|
53
|
+
});
|
|
54
|
+
it("defaults workspace to undefined and all to false", () => {
|
|
55
|
+
const r = parseArgs(["org"]);
|
|
56
|
+
expect(r.flags.workspace).toBeUndefined();
|
|
57
|
+
expect(r.flags.all).toBe(false);
|
|
58
|
+
});
|
|
59
|
+
it("treats `org use <name>` as command / subcommand / positional", () => {
|
|
60
|
+
const r = parseArgs(["org", "use", "Acme Corp"]);
|
|
61
|
+
expect(r.command).toBe("org");
|
|
62
|
+
expect(r.subcommand).toBe("use");
|
|
63
|
+
expect(r.toolArgs).toBe("Acme Corp");
|
|
64
|
+
});
|
|
37
65
|
});
|
package/dist/cli.js
CHANGED
|
@@ -3,7 +3,7 @@ import fs from "node:fs";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import readline from "node:readline";
|
|
5
5
|
import { LoticsClient, API_BASE_URL } from "./client.js";
|
|
6
|
-
import {
|
|
6
|
+
import { resolveContext, deleteConfig, getConfigPath, loadGlobalConfig, saveGlobalConfig, loadLocalConfig, upsertProfile, removeProfile, setActiveOrg, setSelectedWorkspace, resolveProfileByNameOrId, checkForUpdate, } from "./config.js";
|
|
7
7
|
import { VERSION } from "./version.js";
|
|
8
8
|
import { appCreate, appPull, appDeploy, appDev, appSetSubdomain, appRename } from "./app_commands.js";
|
|
9
9
|
import { parseArgs } from "./args.js";
|
|
@@ -24,20 +24,31 @@ Lotics is an AI-powered operations platform. Through this CLI you can:
|
|
|
24
24
|
AUTHENTICATION
|
|
25
25
|
lotics auth Show auth help
|
|
26
26
|
lotics auth signup <email> Create account (run "lotics auth" for details)
|
|
27
|
+
lotics auth api-key <key> Save a key — registers its org as a profile
|
|
28
|
+
|
|
29
|
+
ORGANIZATIONS
|
|
30
|
+
Each saved API key belongs to one org. Register a key per org once, then
|
|
31
|
+
switch between them with no re-pasting. Workspaces live inside an org.
|
|
32
|
+
lotics org List saved orgs (marks active)
|
|
33
|
+
lotics org use <name|id> Switch the active org
|
|
34
|
+
lotics org use <name|id> --local Pin THIS directory to an org (worktrees)
|
|
27
35
|
|
|
28
36
|
USAGE
|
|
29
37
|
1. lotics auth signup <email> Create account or authenticate
|
|
30
|
-
2. lotics
|
|
31
|
-
3. lotics
|
|
32
|
-
4. lotics tools
|
|
33
|
-
5. lotics
|
|
38
|
+
2. lotics org See saved orgs / which one is active
|
|
39
|
+
3. lotics workspace Check current workspace (auto-selects if only one)
|
|
40
|
+
4. lotics tools List available tools by category
|
|
41
|
+
5. lotics tools <name> Show tool description and full input schema
|
|
42
|
+
6. lotics run <tool> '<json>' Execute a tool with JSON arguments
|
|
34
43
|
|
|
35
44
|
Always inspect the schema (step 4) before calling a tool.
|
|
36
45
|
Tools are grouped by category (tables, records, views, etc.).
|
|
37
46
|
Query tools return IDs used as arguments to other tools.
|
|
38
47
|
|
|
39
48
|
COMMANDS
|
|
40
|
-
lotics
|
|
49
|
+
lotics org List saved orgs (marks active)
|
|
50
|
+
lotics org use <name|id> [--local] Switch active org (--local pins this dir)
|
|
51
|
+
lotics workspace List workspaces in the active org (marks current)
|
|
41
52
|
lotics workspace select <id> Switch to a different workspace
|
|
42
53
|
lotics workspace create <name> Create a new workspace (admin only)
|
|
43
54
|
lotics tools List all available tools
|
|
@@ -68,15 +79,23 @@ FLAGS
|
|
|
68
79
|
--timeout <ms> Timeout for tool execution (default: 60000)
|
|
69
80
|
-o <path> Output dir for downloads
|
|
70
81
|
--as <name> Override upload filename
|
|
71
|
-
--api-key <key> API key (overrides saved config
|
|
72
|
-
--
|
|
82
|
+
--api-key <key> One-off API key (overrides saved config + env)
|
|
83
|
+
--workspace <id> One-off workspace override (alias: -w)
|
|
84
|
+
--local Pin the current directory (lotics org use / auth api-key)
|
|
85
|
+
--all (lotics auth logout) Remove every saved credential
|
|
73
86
|
--version Show version
|
|
74
87
|
|
|
75
88
|
CONFIG
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
89
|
+
API keys are stored once per org in the global ~/.lotics/config.json as named
|
|
90
|
+
profiles. Switch with "lotics org use <name|id>" — no re-pasting. A directory
|
|
91
|
+
(e.g. a git worktree) can pin its own org/workspace with "lotics org use
|
|
92
|
+
<name|id> --local", which writes a .lotics/config.json pointer (the key is
|
|
93
|
+
resolved from the global store) — so a global switch never disturbs it.
|
|
94
|
+
|
|
95
|
+
Resolution precedence (highest first):
|
|
96
|
+
--api-key flag > LOTICS_API_KEY env > LOTICS_ORG env >
|
|
97
|
+
local .lotics/config.json > global active profile
|
|
98
|
+
LOTICS_WORKSPACE (or --workspace) overrides the workspace at any level.
|
|
80
99
|
|
|
81
100
|
OUTPUT
|
|
82
101
|
Default output is a compact text summary optimized for AI agents —
|
|
@@ -105,25 +124,27 @@ function printAuthHelp() {
|
|
|
105
124
|
console.log(`Authentication commands:
|
|
106
125
|
|
|
107
126
|
lotics auth signup <email> Create account, org, workspace, and API key
|
|
108
|
-
lotics auth api-key [key] Save an
|
|
127
|
+
lotics auth api-key [key] Save an API key — registers its org as a profile
|
|
109
128
|
lotics auth web Send a magic link email to access the web app
|
|
110
|
-
lotics auth whoami Show the
|
|
111
|
-
lotics auth logout
|
|
129
|
+
lotics auth whoami Show the active account, org, workspace, and source
|
|
130
|
+
lotics auth logout [<name|id>] Remove a saved credential (default: the active org,
|
|
131
|
+
or unpin the current directory)
|
|
132
|
+
lotics auth logout --all Remove every saved credential
|
|
112
133
|
|
|
113
134
|
Auth flags:
|
|
114
|
-
--local
|
|
115
|
-
|
|
135
|
+
--local Pin the current directory instead of the global store.
|
|
136
|
+
signup/api-key write a self-contained ./.lotics/config.json
|
|
137
|
+
(inline key) — for a hermetic worktree or CI.
|
|
116
138
|
--name <name> (signup) Display name (defaults to email prefix)
|
|
117
139
|
--timezone <timezone> (signup) Workspace timezone (defaults to UTC, e.g. Asia/Ho_Chi_Minh)
|
|
118
140
|
|
|
119
|
-
|
|
120
|
-
|
|
141
|
+
API keys are stored once per org as named profiles in ~/.lotics/config.json.
|
|
142
|
+
Registering a second key adds a profile — it does not overwrite the first.
|
|
143
|
+
Switch active org with "lotics org use <name|id>".
|
|
121
144
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
its own account and workspace; --local creates one. Note: an exported
|
|
126
|
-
LOTICS_API_KEY env var overrides the config file's key.`);
|
|
145
|
+
Resolution precedence (highest first):
|
|
146
|
+
--api-key flag > LOTICS_API_KEY env > LOTICS_ORG env > local .lotics/config.json
|
|
147
|
+
> global active profile. LOTICS_WORKSPACE / --workspace override the workspace.`);
|
|
127
148
|
}
|
|
128
149
|
function readStdin() {
|
|
129
150
|
return new Promise((resolve, reject) => {
|
|
@@ -179,17 +200,25 @@ async function handleSignup(positionalEmail, flags) {
|
|
|
179
200
|
}
|
|
180
201
|
process.exit(1);
|
|
181
202
|
}
|
|
182
|
-
const
|
|
183
|
-
const
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
203
|
+
const apiKey = data.api_key;
|
|
204
|
+
const orgId = data.organization_id;
|
|
205
|
+
const workspaceId = data.workspace_id;
|
|
206
|
+
// The org is created with the display name; mirror the server's email-prefix
|
|
207
|
+
// default when none was supplied. Use `||` (not `??`) so an empty-string name
|
|
208
|
+
// falls through exactly as the server's `if (name)` truthiness check does.
|
|
209
|
+
const orgName = name || email.split("@")[0];
|
|
210
|
+
// The key always lives in the global store as a profile; --local additionally
|
|
211
|
+
// pins this directory to it (a pointer), instead of making it the global default.
|
|
212
|
+
upsertProfile(orgId, { api_key: apiKey, org_name: orgName, workspace_id: workspaceId });
|
|
213
|
+
const existing = loadGlobalConfig() ?? {};
|
|
214
|
+
saveGlobalConfig({ ...existing, email });
|
|
215
|
+
setActiveOrg(orgId, flags.local ? "local" : "global");
|
|
190
216
|
console.error(`Account created. You can now use the CLI.`);
|
|
191
|
-
console.error(` Email: ${
|
|
192
|
-
console.error(`
|
|
217
|
+
console.error(` Email: ${email}`);
|
|
218
|
+
console.error(` Org: ${orgName} (${orgId})`);
|
|
219
|
+
console.error(flags.local
|
|
220
|
+
? ` Pinned this directory: ${getConfigPath("local")}`
|
|
221
|
+
: ` Saved to the "${orgName}" profile in ~/.lotics (now active)`);
|
|
193
222
|
console.error(`\nCheck your email for a magic link to access the Lotics web app.`);
|
|
194
223
|
console.error(`Run "lotics auth web" to request a new link at any time.`);
|
|
195
224
|
}
|
|
@@ -200,27 +229,24 @@ async function handleSetup(providedKey, local) {
|
|
|
200
229
|
process.exit(1);
|
|
201
230
|
}
|
|
202
231
|
const client = new LoticsClient({ apiKey });
|
|
203
|
-
let
|
|
232
|
+
let info;
|
|
204
233
|
try {
|
|
205
|
-
|
|
206
|
-
email = info.email;
|
|
234
|
+
info = await client.whoami();
|
|
207
235
|
}
|
|
208
236
|
catch (error) {
|
|
209
237
|
const message = error instanceof Error ? error.message : String(error);
|
|
210
238
|
console.error(`Authentication failed: ${message}`);
|
|
211
239
|
process.exit(1);
|
|
212
240
|
}
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
const newConfig = { ...existing, api_key: apiKey, email };
|
|
216
|
-
// Auto-resolve workspace
|
|
241
|
+
// Auto-resolve workspace (only when the org has exactly one).
|
|
242
|
+
let workspaceId;
|
|
217
243
|
try {
|
|
218
244
|
const workspaces = await client.listWorkspaces();
|
|
219
245
|
if (workspaces.length === 1) {
|
|
220
|
-
|
|
246
|
+
workspaceId = workspaces[0].id;
|
|
221
247
|
}
|
|
222
248
|
else if (workspaces.length > 1) {
|
|
223
|
-
console.error(`\nMultiple workspaces
|
|
249
|
+
console.error(`\nMultiple workspaces in ${info.organization_name}. Run "lotics workspace select <id>" to choose one:`);
|
|
224
250
|
printWorkspaceList(workspaces);
|
|
225
251
|
}
|
|
226
252
|
}
|
|
@@ -228,29 +254,51 @@ async function handleSetup(providedKey, local) {
|
|
|
228
254
|
const msg = error instanceof Error ? error.message : String(error);
|
|
229
255
|
console.error(`Warning: could not resolve workspace: ${msg}`);
|
|
230
256
|
}
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
257
|
+
// The key always lives in the global store as a profile; --local additionally
|
|
258
|
+
// pins this directory to it (a pointer), instead of making it the global default.
|
|
259
|
+
upsertProfile(info.organization_id, {
|
|
260
|
+
api_key: apiKey,
|
|
261
|
+
org_name: info.organization_name,
|
|
262
|
+
workspace_id: workspaceId,
|
|
263
|
+
});
|
|
264
|
+
const existing = loadGlobalConfig() ?? {};
|
|
265
|
+
saveGlobalConfig({ ...existing, email: info.email });
|
|
266
|
+
setActiveOrg(info.organization_id, local ? "local" : "global");
|
|
267
|
+
if (local) {
|
|
268
|
+
console.error(`Authenticated as ${info.email} in ${info.organization_name}.`);
|
|
269
|
+
console.error(` Pinned this directory: ${getConfigPath("local")}`);
|
|
270
|
+
}
|
|
271
|
+
else {
|
|
272
|
+
console.error(`Authenticated as ${info.email} in ${info.organization_name} (now active).`);
|
|
273
|
+
console.error(` Saved to the "${info.organization_name}" profile in ~/.lotics`);
|
|
274
|
+
}
|
|
234
275
|
}
|
|
235
276
|
function requireClient(flags) {
|
|
236
|
-
const
|
|
237
|
-
if (!
|
|
238
|
-
console.error('Not authenticated. Run "lotics auth signup" or set LOTICS_API_KEY.');
|
|
277
|
+
const ctx = resolveContext(flags);
|
|
278
|
+
if (!ctx) {
|
|
279
|
+
console.error('Not authenticated. Run "lotics auth signup", "lotics auth api-key <key>", or set LOTICS_API_KEY.');
|
|
239
280
|
process.exit(1);
|
|
240
281
|
}
|
|
241
|
-
|
|
242
|
-
return new LoticsClient({ apiKey: auth.apiKey, workspaceId: config?.workspace_id });
|
|
282
|
+
return { client: new LoticsClient({ apiKey: ctx.apiKey, workspaceId: ctx.workspaceId }), ctx };
|
|
243
283
|
}
|
|
284
|
+
const SOURCE_LABELS = {
|
|
285
|
+
flag: "--api-key flag",
|
|
286
|
+
env_key: "LOTICS_API_KEY env",
|
|
287
|
+
env_org: "LOTICS_ORG env",
|
|
288
|
+
local_pointer: "local .lotics/config.json (pin)",
|
|
289
|
+
global_profile: "global active profile",
|
|
290
|
+
};
|
|
244
291
|
function printWorkspaceList(workspaces, currentId) {
|
|
245
292
|
for (const ws of workspaces) {
|
|
246
293
|
const marker = ws.id === currentId ? " (current)" : "";
|
|
247
294
|
console.error(` ${ws.id} ${ws.name} ${ws.timezone} ${ws.default_currency}${marker}`);
|
|
248
295
|
}
|
|
249
296
|
}
|
|
250
|
-
async function resolveWorkspace(client) {
|
|
251
|
-
|
|
252
|
-
|
|
297
|
+
async function resolveWorkspace(client, ctx) {
|
|
298
|
+
if (ctx.workspaceId) {
|
|
299
|
+
client.setWorkspaceId(ctx.workspaceId);
|
|
253
300
|
return;
|
|
301
|
+
}
|
|
254
302
|
let workspaces;
|
|
255
303
|
try {
|
|
256
304
|
workspaces = await client.listWorkspaces();
|
|
@@ -265,8 +313,9 @@ async function resolveWorkspace(client) {
|
|
|
265
313
|
process.exit(1);
|
|
266
314
|
}
|
|
267
315
|
if (workspaces.length === 1) {
|
|
268
|
-
|
|
269
|
-
|
|
316
|
+
// Persist the auto-resolution where the credential lives; null means it
|
|
317
|
+
// came from --api-key/env with no profile, so it's an in-memory selection.
|
|
318
|
+
setSelectedWorkspace(workspaces[0].id);
|
|
270
319
|
client.setWorkspaceId(workspaces[0].id);
|
|
271
320
|
return;
|
|
272
321
|
}
|
|
@@ -320,33 +369,66 @@ async function main() {
|
|
|
320
369
|
return;
|
|
321
370
|
}
|
|
322
371
|
if (subcommand === "whoami") {
|
|
323
|
-
const
|
|
324
|
-
if (!
|
|
325
|
-
console.error('Not authenticated. Run "lotics auth signup" or set LOTICS_API_KEY.');
|
|
372
|
+
const ctx = resolveContext(flags);
|
|
373
|
+
if (!ctx) {
|
|
374
|
+
console.error('Not authenticated. Run "lotics auth signup", "lotics auth api-key <key>", or set LOTICS_API_KEY.');
|
|
326
375
|
process.exit(1);
|
|
327
376
|
}
|
|
328
|
-
const client = new LoticsClient({ apiKey:
|
|
377
|
+
const client = new LoticsClient({ apiKey: ctx.apiKey });
|
|
329
378
|
const info = await client.whoami();
|
|
330
|
-
const existing =
|
|
331
|
-
|
|
379
|
+
const existing = loadGlobalConfig() ?? {};
|
|
380
|
+
saveGlobalConfig({ ...existing, email: info.email });
|
|
332
381
|
if (flags.json) {
|
|
333
|
-
console.log(JSON.stringify(info, null, 2));
|
|
382
|
+
console.log(JSON.stringify({ ...info, workspace_id: ctx.workspaceId ?? null, source: ctx.source }, null, 2));
|
|
334
383
|
}
|
|
335
384
|
else {
|
|
336
|
-
console.log(`Name:
|
|
337
|
-
console.log(`Email:
|
|
338
|
-
console.log(`Org:
|
|
385
|
+
console.log(`Name: ${info.name}`);
|
|
386
|
+
console.log(`Email: ${info.email}`);
|
|
387
|
+
console.log(`Org: ${info.organization_name} (${info.organization_id})`);
|
|
388
|
+
console.log(`Workspace: ${ctx.workspaceId ?? "(none selected)"}`);
|
|
389
|
+
console.log(`Source: ${SOURCE_LABELS[ctx.source]}`);
|
|
339
390
|
}
|
|
340
|
-
console.error(`Config: ${getConfigPath()}`);
|
|
341
391
|
return;
|
|
342
392
|
}
|
|
343
393
|
if (subcommand === "logout") {
|
|
344
|
-
|
|
345
|
-
|
|
394
|
+
const local = loadLocalConfig();
|
|
395
|
+
// Bare logout inside a pinned directory just unpins it.
|
|
396
|
+
if (local && !flags.all && !toolArgs) {
|
|
397
|
+
deleteConfig();
|
|
398
|
+
console.error("Removed the local pin for this directory.");
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
if (flags.all) {
|
|
402
|
+
const g = loadGlobalConfig();
|
|
403
|
+
saveGlobalConfig({ last_update_check: g?.last_update_check, latest_version: g?.latest_version });
|
|
404
|
+
console.error("Removed all saved credentials.");
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
const g = loadGlobalConfig() ?? {};
|
|
408
|
+
const profiles = g.profiles ?? {};
|
|
409
|
+
let resolved;
|
|
410
|
+
if (toolArgs) {
|
|
411
|
+
resolved = resolveProfileByNameOrId(profiles, toolArgs);
|
|
412
|
+
}
|
|
413
|
+
else if (g.active_org && profiles[g.active_org]) {
|
|
414
|
+
resolved = [g.active_org, profiles[g.active_org]];
|
|
415
|
+
}
|
|
416
|
+
else {
|
|
417
|
+
resolved = null;
|
|
418
|
+
}
|
|
419
|
+
if (!resolved) {
|
|
420
|
+
console.error(toolArgs
|
|
421
|
+
? `No saved credential matches "${toolArgs}".`
|
|
422
|
+
: 'No active credential to remove. Pass an org name/id, or use --all.');
|
|
423
|
+
process.exit(1);
|
|
424
|
+
}
|
|
425
|
+
const [orgId, profile] = resolved;
|
|
426
|
+
removeProfile(orgId);
|
|
427
|
+
console.error(`Removed the "${profile.org_name}" credential (${orgId}).`);
|
|
346
428
|
return;
|
|
347
429
|
}
|
|
348
430
|
if (subcommand === "web") {
|
|
349
|
-
const client = requireClient(flags);
|
|
431
|
+
const { client } = requireClient(flags);
|
|
350
432
|
const { email } = await client.login();
|
|
351
433
|
console.error(`Magic link sent to ${email}. Check your email to access the Lotics web app.`);
|
|
352
434
|
return;
|
|
@@ -366,6 +448,63 @@ async function main() {
|
|
|
366
448
|
await runDocxCommand(subcommand, toolArgs, restArgs);
|
|
367
449
|
return;
|
|
368
450
|
}
|
|
451
|
+
// --- lotics org [list | use <name|id> [--local]] — credential store, config-only ---
|
|
452
|
+
if (command === "org") {
|
|
453
|
+
const global = loadGlobalConfig() ?? {};
|
|
454
|
+
const profiles = global.profiles ?? {};
|
|
455
|
+
const local = loadLocalConfig();
|
|
456
|
+
if (subcommand === "use") {
|
|
457
|
+
if (!toolArgs) {
|
|
458
|
+
console.error("Usage: lotics org use <name|id> [--local]");
|
|
459
|
+
process.exit(1);
|
|
460
|
+
}
|
|
461
|
+
const resolved = resolveProfileByNameOrId(profiles, toolArgs);
|
|
462
|
+
if (!resolved) {
|
|
463
|
+
console.error(`No saved credential for "${toolArgs}". Run "lotics auth api-key <key>" while authenticated to that org, or "lotics org" to list.`);
|
|
464
|
+
process.exit(1);
|
|
465
|
+
}
|
|
466
|
+
const [orgId, profile] = resolved;
|
|
467
|
+
if (flags.local) {
|
|
468
|
+
setActiveOrg(orgId, "local");
|
|
469
|
+
console.error(`Pinned this directory to ${profile.org_name} (${orgId}).`);
|
|
470
|
+
}
|
|
471
|
+
else {
|
|
472
|
+
setActiveOrg(orgId, "global");
|
|
473
|
+
console.error(`Switched active org to ${profile.org_name} (${orgId}).`);
|
|
474
|
+
if (local?.active_org) {
|
|
475
|
+
console.error("Note: a local pin (.lotics/config.json) overrides the global default in this directory. Use --local to change the pin here.");
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
if (subcommand && subcommand !== "list") {
|
|
481
|
+
console.error(`Unknown org subcommand: ${subcommand}`);
|
|
482
|
+
console.error("Usage: lotics org [list | use <name|id> [--local]]");
|
|
483
|
+
process.exit(1);
|
|
484
|
+
}
|
|
485
|
+
const orgIds = Object.keys(profiles);
|
|
486
|
+
if (orgIds.length === 0) {
|
|
487
|
+
console.error('No saved orgs. Run "lotics auth api-key <key>" to add one.');
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
// The active org in THIS directory: a local pin wins over the global default.
|
|
491
|
+
const effectiveActive = local?.active_org ?? global.active_org;
|
|
492
|
+
if (flags.json) {
|
|
493
|
+
console.log(JSON.stringify(orgIds.map((id) => ({
|
|
494
|
+
org_id: id,
|
|
495
|
+
org_name: profiles[id].org_name,
|
|
496
|
+
workspace_id: profiles[id].workspace_id ?? null,
|
|
497
|
+
active: id === effectiveActive,
|
|
498
|
+
})), null, 2));
|
|
499
|
+
}
|
|
500
|
+
else {
|
|
501
|
+
for (const id of orgIds) {
|
|
502
|
+
const marker = id === effectiveActive ? " (active)" : "";
|
|
503
|
+
console.log(`${id} ${profiles[id].org_name}${marker}`);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
369
508
|
// --- Validate command before auth ---
|
|
370
509
|
if (command !== "tools" && command !== "upload" && command !== "run" && command !== "download" && command !== "workspace" && command !== "app") {
|
|
371
510
|
console.error(`Unknown command: ${command}`);
|
|
@@ -405,11 +544,11 @@ async function main() {
|
|
|
405
544
|
console.error('Downloads every file on the given file field into the output dir.');
|
|
406
545
|
process.exit(1);
|
|
407
546
|
}
|
|
408
|
-
const client = requireClient(flags);
|
|
547
|
+
const { client, ctx } = requireClient(flags);
|
|
409
548
|
// lotics workspace / lotics workspace list / lotics workspace select <id>
|
|
410
549
|
if (command === "workspace") {
|
|
411
550
|
const workspaces = await client.listWorkspaces();
|
|
412
|
-
const
|
|
551
|
+
const currentWorkspaceId = ctx.workspaceId;
|
|
413
552
|
if (subcommand === "select") {
|
|
414
553
|
const targetId = toolArgs;
|
|
415
554
|
if (!targetId) {
|
|
@@ -419,12 +558,16 @@ async function main() {
|
|
|
419
558
|
const target = workspaces.find((ws) => ws.id === targetId);
|
|
420
559
|
if (!target) {
|
|
421
560
|
console.error(`Workspace not found: ${targetId}\n\nAvailable workspaces:`);
|
|
422
|
-
printWorkspaceList(workspaces,
|
|
561
|
+
printWorkspaceList(workspaces, currentWorkspaceId);
|
|
423
562
|
process.exit(1);
|
|
424
563
|
}
|
|
425
|
-
const
|
|
426
|
-
|
|
427
|
-
|
|
564
|
+
const written = setSelectedWorkspace(target.id);
|
|
565
|
+
if (!written) {
|
|
566
|
+
console.error(`Switched to ${target.name} (${target.id}) for this command, but it could not be saved — credentials came from --api-key/LOTICS_API_KEY with no saved profile. Run "lotics auth api-key <key>" to persist a default.`);
|
|
567
|
+
return;
|
|
568
|
+
}
|
|
569
|
+
const where = written.scope === "local" ? " (pinned to this directory)" : "";
|
|
570
|
+
console.error(`Switched to workspace: ${target.name} (${target.id})${where}`);
|
|
428
571
|
return;
|
|
429
572
|
}
|
|
430
573
|
if (subcommand === "create") {
|
|
@@ -435,8 +578,7 @@ async function main() {
|
|
|
435
578
|
}
|
|
436
579
|
const timezone = flags.timezone;
|
|
437
580
|
const created = await client.createWorkspace({ name, timezone });
|
|
438
|
-
|
|
439
|
-
saveConfig({ ...existing, workspace_id: created.id });
|
|
581
|
+
setSelectedWorkspace(created.id);
|
|
440
582
|
client.setWorkspaceId(created.id);
|
|
441
583
|
if (flags.json) {
|
|
442
584
|
console.log(JSON.stringify(created, null, 2));
|
|
@@ -462,16 +604,17 @@ async function main() {
|
|
|
462
604
|
}
|
|
463
605
|
else {
|
|
464
606
|
for (const ws of workspaces) {
|
|
465
|
-
const marker = ws.id ===
|
|
607
|
+
const marker = ws.id === currentWorkspaceId ? " (current)" : "";
|
|
466
608
|
console.log(`${ws.id} ${ws.name} ${ws.timezone} ${ws.default_currency}${marker}`);
|
|
467
609
|
}
|
|
468
610
|
}
|
|
469
611
|
}
|
|
470
|
-
|
|
612
|
+
if (ctx.orgName)
|
|
613
|
+
console.error(`Org: ${ctx.orgName}`);
|
|
471
614
|
return;
|
|
472
615
|
}
|
|
473
616
|
// Ensure workspace is resolved for all remaining commands
|
|
474
|
-
await resolveWorkspace(client);
|
|
617
|
+
await resolveWorkspace(client, ctx);
|
|
475
618
|
// lotics app create / pull / deploy
|
|
476
619
|
if (command === "app") {
|
|
477
620
|
if (subcommand === "create") {
|