@lotics/cli 0.34.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/app_commands.d.ts +9 -0
- package/dist/app_commands.js +13 -0
- package/dist/args.d.ts +2 -0
- package/dist/args.js +9 -0
- package/dist/args.test.js +28 -0
- package/dist/cli.js +237 -82
- package/dist/client.js +9 -4
- 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 +1287 -556
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -3,9 +3,9 @@ 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
|
-
import { appCreate, appPull, appDeploy, appDev, appSetSubdomain } from "./app_commands.js";
|
|
8
|
+
import { appCreate, appPull, appDeploy, appDev, appSetSubdomain, appRename } from "./app_commands.js";
|
|
9
9
|
import { parseArgs } from "./args.js";
|
|
10
10
|
import { runXlsxCommand } from "./xlsx.js";
|
|
11
11
|
import { runDocxCommand } from "./docx.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
|
|
@@ -54,6 +65,7 @@ COMMANDS
|
|
|
54
65
|
(--force-workflow-sync wipes server-only
|
|
55
66
|
workflow aliases; default refuses)
|
|
56
67
|
lotics app subdomain <new-subdomain> Rename the app's public <slug>.lotics.app address
|
|
68
|
+
lotics app rename "<new name>" Rename the app's display name (launcher title)
|
|
57
69
|
lotics app dev [path] Run the app locally with HMR (RPC forwarded to prod)
|
|
58
70
|
lotics xlsx <subcommand> ... Read/write/edit .xlsx files on your local filesystem
|
|
59
71
|
(uses the bundled Lotics xlsx engine; prefer over
|
|
@@ -67,15 +79,23 @@ FLAGS
|
|
|
67
79
|
--timeout <ms> Timeout for tool execution (default: 60000)
|
|
68
80
|
-o <path> Output dir for downloads
|
|
69
81
|
--as <name> Override upload filename
|
|
70
|
-
--api-key <key> API key (overrides saved config
|
|
71
|
-
--
|
|
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
|
|
72
86
|
--version Show version
|
|
73
87
|
|
|
74
88
|
CONFIG
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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.
|
|
79
99
|
|
|
80
100
|
OUTPUT
|
|
81
101
|
Default output is a compact text summary optimized for AI agents —
|
|
@@ -104,25 +124,27 @@ function printAuthHelp() {
|
|
|
104
124
|
console.log(`Authentication commands:
|
|
105
125
|
|
|
106
126
|
lotics auth signup <email> Create account, org, workspace, and API key
|
|
107
|
-
lotics auth api-key [key] Save an
|
|
127
|
+
lotics auth api-key [key] Save an API key — registers its org as a profile
|
|
108
128
|
lotics auth web Send a magic link email to access the web app
|
|
109
|
-
lotics auth whoami Show the
|
|
110
|
-
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
|
|
111
133
|
|
|
112
134
|
Auth flags:
|
|
113
|
-
--local
|
|
114
|
-
|
|
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.
|
|
115
138
|
--name <name> (signup) Display name (defaults to email prefix)
|
|
116
139
|
--timezone <timezone> (signup) Workspace timezone (defaults to UTC, e.g. Asia/Ho_Chi_Minh)
|
|
117
140
|
|
|
118
|
-
|
|
119
|
-
|
|
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>".
|
|
120
144
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
its own account and workspace; --local creates one. Note: an exported
|
|
125
|
-
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.`);
|
|
126
148
|
}
|
|
127
149
|
function readStdin() {
|
|
128
150
|
return new Promise((resolve, reject) => {
|
|
@@ -178,17 +200,25 @@ async function handleSignup(positionalEmail, flags) {
|
|
|
178
200
|
}
|
|
179
201
|
process.exit(1);
|
|
180
202
|
}
|
|
181
|
-
const
|
|
182
|
-
const
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
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");
|
|
189
216
|
console.error(`Account created. You can now use the CLI.`);
|
|
190
|
-
console.error(` Email: ${
|
|
191
|
-
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)`);
|
|
192
222
|
console.error(`\nCheck your email for a magic link to access the Lotics web app.`);
|
|
193
223
|
console.error(`Run "lotics auth web" to request a new link at any time.`);
|
|
194
224
|
}
|
|
@@ -199,27 +229,24 @@ async function handleSetup(providedKey, local) {
|
|
|
199
229
|
process.exit(1);
|
|
200
230
|
}
|
|
201
231
|
const client = new LoticsClient({ apiKey });
|
|
202
|
-
let
|
|
232
|
+
let info;
|
|
203
233
|
try {
|
|
204
|
-
|
|
205
|
-
email = info.email;
|
|
234
|
+
info = await client.whoami();
|
|
206
235
|
}
|
|
207
236
|
catch (error) {
|
|
208
237
|
const message = error instanceof Error ? error.message : String(error);
|
|
209
238
|
console.error(`Authentication failed: ${message}`);
|
|
210
239
|
process.exit(1);
|
|
211
240
|
}
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
const newConfig = { ...existing, api_key: apiKey, email };
|
|
215
|
-
// Auto-resolve workspace
|
|
241
|
+
// Auto-resolve workspace (only when the org has exactly one).
|
|
242
|
+
let workspaceId;
|
|
216
243
|
try {
|
|
217
244
|
const workspaces = await client.listWorkspaces();
|
|
218
245
|
if (workspaces.length === 1) {
|
|
219
|
-
|
|
246
|
+
workspaceId = workspaces[0].id;
|
|
220
247
|
}
|
|
221
248
|
else if (workspaces.length > 1) {
|
|
222
|
-
console.error(`\nMultiple workspaces
|
|
249
|
+
console.error(`\nMultiple workspaces in ${info.organization_name}. Run "lotics workspace select <id>" to choose one:`);
|
|
223
250
|
printWorkspaceList(workspaces);
|
|
224
251
|
}
|
|
225
252
|
}
|
|
@@ -227,29 +254,51 @@ async function handleSetup(providedKey, local) {
|
|
|
227
254
|
const msg = error instanceof Error ? error.message : String(error);
|
|
228
255
|
console.error(`Warning: could not resolve workspace: ${msg}`);
|
|
229
256
|
}
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
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
|
+
}
|
|
233
275
|
}
|
|
234
276
|
function requireClient(flags) {
|
|
235
|
-
const
|
|
236
|
-
if (!
|
|
237
|
-
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.');
|
|
238
280
|
process.exit(1);
|
|
239
281
|
}
|
|
240
|
-
|
|
241
|
-
return new LoticsClient({ apiKey: auth.apiKey, workspaceId: config?.workspace_id });
|
|
282
|
+
return { client: new LoticsClient({ apiKey: ctx.apiKey, workspaceId: ctx.workspaceId }), ctx };
|
|
242
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
|
+
};
|
|
243
291
|
function printWorkspaceList(workspaces, currentId) {
|
|
244
292
|
for (const ws of workspaces) {
|
|
245
293
|
const marker = ws.id === currentId ? " (current)" : "";
|
|
246
294
|
console.error(` ${ws.id} ${ws.name} ${ws.timezone} ${ws.default_currency}${marker}`);
|
|
247
295
|
}
|
|
248
296
|
}
|
|
249
|
-
async function resolveWorkspace(client) {
|
|
250
|
-
|
|
251
|
-
|
|
297
|
+
async function resolveWorkspace(client, ctx) {
|
|
298
|
+
if (ctx.workspaceId) {
|
|
299
|
+
client.setWorkspaceId(ctx.workspaceId);
|
|
252
300
|
return;
|
|
301
|
+
}
|
|
253
302
|
let workspaces;
|
|
254
303
|
try {
|
|
255
304
|
workspaces = await client.listWorkspaces();
|
|
@@ -264,8 +313,9 @@ async function resolveWorkspace(client) {
|
|
|
264
313
|
process.exit(1);
|
|
265
314
|
}
|
|
266
315
|
if (workspaces.length === 1) {
|
|
267
|
-
|
|
268
|
-
|
|
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);
|
|
269
319
|
client.setWorkspaceId(workspaces[0].id);
|
|
270
320
|
return;
|
|
271
321
|
}
|
|
@@ -319,33 +369,66 @@ async function main() {
|
|
|
319
369
|
return;
|
|
320
370
|
}
|
|
321
371
|
if (subcommand === "whoami") {
|
|
322
|
-
const
|
|
323
|
-
if (!
|
|
324
|
-
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.');
|
|
325
375
|
process.exit(1);
|
|
326
376
|
}
|
|
327
|
-
const client = new LoticsClient({ apiKey:
|
|
377
|
+
const client = new LoticsClient({ apiKey: ctx.apiKey });
|
|
328
378
|
const info = await client.whoami();
|
|
329
|
-
const existing =
|
|
330
|
-
|
|
379
|
+
const existing = loadGlobalConfig() ?? {};
|
|
380
|
+
saveGlobalConfig({ ...existing, email: info.email });
|
|
331
381
|
if (flags.json) {
|
|
332
|
-
console.log(JSON.stringify(info, null, 2));
|
|
382
|
+
console.log(JSON.stringify({ ...info, workspace_id: ctx.workspaceId ?? null, source: ctx.source }, null, 2));
|
|
333
383
|
}
|
|
334
384
|
else {
|
|
335
|
-
console.log(`Name:
|
|
336
|
-
console.log(`Email:
|
|
337
|
-
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]}`);
|
|
338
390
|
}
|
|
339
|
-
console.error(`Config: ${getConfigPath()}`);
|
|
340
391
|
return;
|
|
341
392
|
}
|
|
342
393
|
if (subcommand === "logout") {
|
|
343
|
-
|
|
344
|
-
|
|
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}).`);
|
|
345
428
|
return;
|
|
346
429
|
}
|
|
347
430
|
if (subcommand === "web") {
|
|
348
|
-
const client = requireClient(flags);
|
|
431
|
+
const { client } = requireClient(flags);
|
|
349
432
|
const { email } = await client.login();
|
|
350
433
|
console.error(`Magic link sent to ${email}. Check your email to access the Lotics web app.`);
|
|
351
434
|
return;
|
|
@@ -365,6 +448,63 @@ async function main() {
|
|
|
365
448
|
await runDocxCommand(subcommand, toolArgs, restArgs);
|
|
366
449
|
return;
|
|
367
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
|
+
}
|
|
368
508
|
// --- Validate command before auth ---
|
|
369
509
|
if (command !== "tools" && command !== "upload" && command !== "run" && command !== "download" && command !== "workspace" && command !== "app") {
|
|
370
510
|
console.error(`Unknown command: ${command}`);
|
|
@@ -378,6 +518,7 @@ async function main() {
|
|
|
378
518
|
console.error(" lotics app deploy [-m <message>] [--force-workflow-sync]");
|
|
379
519
|
console.error(" Build + upload the current directory");
|
|
380
520
|
console.error(" lotics app subdomain <new-subdomain> Rename the app's public <slug>.lotics.app address");
|
|
521
|
+
console.error(" lotics app rename \"<new name>\" Rename the app's display name (launcher title)");
|
|
381
522
|
console.error(" lotics app dev [path] Run the app locally with HMR + RPC forwarding");
|
|
382
523
|
process.exit(1);
|
|
383
524
|
}
|
|
@@ -403,11 +544,11 @@ async function main() {
|
|
|
403
544
|
console.error('Downloads every file on the given file field into the output dir.');
|
|
404
545
|
process.exit(1);
|
|
405
546
|
}
|
|
406
|
-
const client = requireClient(flags);
|
|
547
|
+
const { client, ctx } = requireClient(flags);
|
|
407
548
|
// lotics workspace / lotics workspace list / lotics workspace select <id>
|
|
408
549
|
if (command === "workspace") {
|
|
409
550
|
const workspaces = await client.listWorkspaces();
|
|
410
|
-
const
|
|
551
|
+
const currentWorkspaceId = ctx.workspaceId;
|
|
411
552
|
if (subcommand === "select") {
|
|
412
553
|
const targetId = toolArgs;
|
|
413
554
|
if (!targetId) {
|
|
@@ -417,12 +558,16 @@ async function main() {
|
|
|
417
558
|
const target = workspaces.find((ws) => ws.id === targetId);
|
|
418
559
|
if (!target) {
|
|
419
560
|
console.error(`Workspace not found: ${targetId}\n\nAvailable workspaces:`);
|
|
420
|
-
printWorkspaceList(workspaces,
|
|
561
|
+
printWorkspaceList(workspaces, currentWorkspaceId);
|
|
421
562
|
process.exit(1);
|
|
422
563
|
}
|
|
423
|
-
const
|
|
424
|
-
|
|
425
|
-
|
|
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}`);
|
|
426
571
|
return;
|
|
427
572
|
}
|
|
428
573
|
if (subcommand === "create") {
|
|
@@ -433,8 +578,7 @@ async function main() {
|
|
|
433
578
|
}
|
|
434
579
|
const timezone = flags.timezone;
|
|
435
580
|
const created = await client.createWorkspace({ name, timezone });
|
|
436
|
-
|
|
437
|
-
saveConfig({ ...existing, workspace_id: created.id });
|
|
581
|
+
setSelectedWorkspace(created.id);
|
|
438
582
|
client.setWorkspaceId(created.id);
|
|
439
583
|
if (flags.json) {
|
|
440
584
|
console.log(JSON.stringify(created, null, 2));
|
|
@@ -460,16 +604,17 @@ async function main() {
|
|
|
460
604
|
}
|
|
461
605
|
else {
|
|
462
606
|
for (const ws of workspaces) {
|
|
463
|
-
const marker = ws.id ===
|
|
607
|
+
const marker = ws.id === currentWorkspaceId ? " (current)" : "";
|
|
464
608
|
console.log(`${ws.id} ${ws.name} ${ws.timezone} ${ws.default_currency}${marker}`);
|
|
465
609
|
}
|
|
466
610
|
}
|
|
467
611
|
}
|
|
468
|
-
|
|
612
|
+
if (ctx.orgName)
|
|
613
|
+
console.error(`Org: ${ctx.orgName}`);
|
|
469
614
|
return;
|
|
470
615
|
}
|
|
471
616
|
// Ensure workspace is resolved for all remaining commands
|
|
472
|
-
await resolveWorkspace(client);
|
|
617
|
+
await resolveWorkspace(client, ctx);
|
|
473
618
|
// lotics app create / pull / deploy
|
|
474
619
|
if (command === "app") {
|
|
475
620
|
if (subcommand === "create") {
|
|
@@ -510,6 +655,16 @@ async function main() {
|
|
|
510
655
|
await appSetSubdomain(client, { subdomain: newSubdomain });
|
|
511
656
|
return;
|
|
512
657
|
}
|
|
658
|
+
if (subcommand === "rename") {
|
|
659
|
+
const newName = toolArgs;
|
|
660
|
+
if (!newName) {
|
|
661
|
+
console.error('Usage: lotics app rename "<new name>"');
|
|
662
|
+
console.error("Changes the app's display name (run inside the app directory).");
|
|
663
|
+
process.exit(1);
|
|
664
|
+
}
|
|
665
|
+
await appRename(client, { name: newName });
|
|
666
|
+
return;
|
|
667
|
+
}
|
|
513
668
|
if (subcommand === "dev") {
|
|
514
669
|
// First positional is an optional project path (defaults to cwd).
|
|
515
670
|
// --port and --vite-port can override the wrapper / Vite ports.
|
package/dist/client.js
CHANGED
|
@@ -262,10 +262,15 @@ export class LoticsClient {
|
|
|
262
262
|
return absolutePath;
|
|
263
263
|
}
|
|
264
264
|
async downloadFileById(fileId, outputDir, options) {
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
265
|
+
// Resolve a presigned URL (the file proxy is auth-gated and served by an
|
|
266
|
+
// edge Worker that doesn't accept the API key). The signed_url endpoint
|
|
267
|
+
// takes the API key and returns a short-lived, directly-fetchable URL
|
|
268
|
+
// whose Content-Disposition carries the original filename.
|
|
269
|
+
const signedUrlRes = await fetch(`${this.baseUrl}/v1/files/${encodeURIComponent(fileId)}/signed_url`, { headers: this.buildHeaders() });
|
|
270
|
+
if (!signedUrlRes.ok)
|
|
271
|
+
await this.throwResponseError(signedUrlRes);
|
|
272
|
+
const { url } = (await signedUrlRes.json());
|
|
273
|
+
const response = await fetch(url);
|
|
269
274
|
if (!response.ok)
|
|
270
275
|
await this.throwResponseError(response);
|
|
271
276
|
const disposition = response.headers.get("content-disposition") ?? "";
|