@lotics/cli 0.32.0 → 0.33.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 +27 -0
- package/dist/cli.js +673 -0
- package/dist/{src/dev → dev}/wrapper_page.js +33 -5
- package/dist/docx.d.ts +2 -0
- package/dist/docx.js +341 -0
- package/dist/docx.test.js +145 -0
- package/dist/file_command_io.d.ts +12 -0
- package/dist/file_command_io.js +34 -0
- package/dist/src/cli.js +46757 -500
- package/dist/{src/starter_template.d.ts → starter_template.d.ts} +1 -1
- package/dist/{src/starter_template.js → starter_template.js} +1 -1
- package/dist/starter_template.test.d.ts +1 -0
- package/dist/xlsx.d.ts +2 -0
- package/dist/xlsx.js +489 -0
- package/dist/xlsx.test.d.ts +1 -0
- package/dist/xlsx.test.js +131 -0
- package/package.json +6 -2
- /package/dist/{src/app_commands.d.ts → app_commands.d.ts} +0 -0
- /package/dist/{src/app_commands.js → app_commands.js} +0 -0
- /package/dist/{src/app_commands.test.d.ts → app_commands.test.d.ts} +0 -0
- /package/dist/{src/app_commands.test.js → app_commands.test.js} +0 -0
- /package/dist/{src/args.d.ts → args.d.ts} +0 -0
- /package/dist/{src/args.js → args.js} +0 -0
- /package/dist/{src/args.test.d.ts → args.test.d.ts} +0 -0
- /package/dist/{src/args.test.js → args.test.js} +0 -0
- /package/dist/{src/cli.d.ts → cli.d.ts} +0 -0
- /package/dist/{src/client.d.ts → client.d.ts} +0 -0
- /package/dist/{src/client.js → client.js} +0 -0
- /package/dist/{src/config.d.ts → config.d.ts} +0 -0
- /package/dist/{src/config.js → config.js} +0 -0
- /package/dist/{src/config.test.d.ts → config.test.d.ts} +0 -0
- /package/dist/{src/config.test.js → config.test.js} +0 -0
- /package/dist/{src/dev → dev}/rpc_handler.d.ts +0 -0
- /package/dist/{src/dev → dev}/rpc_handler.js +0 -0
- /package/dist/{src/dev → dev}/server.d.ts +0 -0
- /package/dist/{src/dev → dev}/server.js +0 -0
- /package/dist/{src/dev → dev}/wrapper_page.d.ts +0 -0
- /package/dist/{src/starter_template.test.d.ts → docx.test.d.ts} +0 -0
- /package/dist/{src/generate_app_queries_dts.d.ts → generate_app_queries_dts.d.ts} +0 -0
- /package/dist/{src/generate_app_queries_dts.js → generate_app_queries_dts.js} +0 -0
- /package/dist/{src/generate_app_workflows_dts.d.ts → generate_app_workflows_dts.d.ts} +0 -0
- /package/dist/{src/generate_app_workflows_dts.js → generate_app_workflows_dts.js} +0 -0
- /package/dist/{src/starter_template.test.js → starter_template.test.js} +0 -0
- /package/dist/{src/version.d.ts → version.d.ts} +0 -0
- /package/dist/{src/version.js → version.js} +0 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,673 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import readline from "node:readline";
|
|
5
|
+
import { LoticsClient, API_BASE_URL } from "./client.js";
|
|
6
|
+
import { resolveAuth, loadConfig, saveConfig, deleteConfig, getConfigPath, checkForUpdate } from "./config.js";
|
|
7
|
+
import { VERSION } from "./version.js";
|
|
8
|
+
import { appCreate, appPull, appDeploy, appDev, appSetSubdomain } from "./app_commands.js";
|
|
9
|
+
import { parseArgs } from "./args.js";
|
|
10
|
+
import { runXlsxCommand } from "./xlsx.js";
|
|
11
|
+
import { runDocxCommand } from "./docx.js";
|
|
12
|
+
function printHelp() {
|
|
13
|
+
console.log(`Lotics CLI v${VERSION} — AI agent interface for Lotics
|
|
14
|
+
|
|
15
|
+
Lotics is an AI-powered operations platform. Through this CLI you can:
|
|
16
|
+
- Manage tables, records, and views (structured data with typed fields)
|
|
17
|
+
- Generate documents from templates (Excel, Word, PDF, Email)
|
|
18
|
+
- Build and run automations — schedules, webhooks, table lifecycle workflows,
|
|
19
|
+
button actions, and app-action workflows; inspect execution and version history
|
|
20
|
+
- Administer the workspace — invite members, manage groups, share resources,
|
|
21
|
+
transfer ownership, browse connected accounts (OAuth attach is web-only)
|
|
22
|
+
- Create and manage apps, knowledge docs, and files
|
|
23
|
+
|
|
24
|
+
AUTHENTICATION
|
|
25
|
+
lotics auth Show auth help
|
|
26
|
+
lotics auth signup <email> Create account (run "lotics auth" for details)
|
|
27
|
+
|
|
28
|
+
USAGE
|
|
29
|
+
1. lotics auth signup <email> Create account or authenticate
|
|
30
|
+
2. lotics workspace Check current workspace (auto-selects if only one)
|
|
31
|
+
3. lotics tools List available tools by category
|
|
32
|
+
4. lotics tools <name> Show tool description and full input schema
|
|
33
|
+
5. lotics run <tool> '<json>' Execute a tool with JSON arguments
|
|
34
|
+
|
|
35
|
+
Always inspect the schema (step 4) before calling a tool.
|
|
36
|
+
Tools are grouped by category (tables, records, views, etc.).
|
|
37
|
+
Query tools return IDs used as arguments to other tools.
|
|
38
|
+
|
|
39
|
+
COMMANDS
|
|
40
|
+
lotics workspace List workspaces (marks current)
|
|
41
|
+
lotics workspace select <id> Switch to a different workspace
|
|
42
|
+
lotics workspace create <name> Create a new workspace (admin only)
|
|
43
|
+
lotics tools List all available tools
|
|
44
|
+
lotics tools <name> Show tool description and input schema
|
|
45
|
+
lotics run <tool> '<json>' Execute a tool
|
|
46
|
+
lotics upload <file|dir...> Upload files (directories expand to their immediate files)
|
|
47
|
+
lotics download <file_id> Download a file by ID
|
|
48
|
+
lotics download record <record_id> <field_key>
|
|
49
|
+
Download all files on a record file field
|
|
50
|
+
lotics app create <name> [path] Create a new custom-code app + scaffold locally
|
|
51
|
+
lotics app pull <app_id> [path] Bootstrap full local env (source + npm install + types)
|
|
52
|
+
lotics app deploy [-m <message>] [--force-workflow-sync]
|
|
53
|
+
Build + upload current dir as a new version
|
|
54
|
+
(--force-workflow-sync wipes server-only
|
|
55
|
+
workflow aliases; default refuses)
|
|
56
|
+
lotics app subdomain <new-subdomain> Rename the app's public <slug>.lotics.app address
|
|
57
|
+
lotics app dev [path] Run the app locally with HMR (RPC forwarded to prod)
|
|
58
|
+
lotics xlsx <subcommand> ... Read/write/edit .xlsx files on your local filesystem
|
|
59
|
+
(uses the bundled Lotics xlsx engine; prefer over
|
|
60
|
+
npm xlsx/exceljs for round-trip fidelity)
|
|
61
|
+
lotics docx <subcommand> ... Read/write/edit .docx files on your local filesystem
|
|
62
|
+
(uses the bundled Lotics OOXML engine; preserves
|
|
63
|
+
tables, images, and unknown markup verbatim)
|
|
64
|
+
|
|
65
|
+
FLAGS
|
|
66
|
+
--json Full JSON output (default is human-readable text)
|
|
67
|
+
--timeout <ms> Timeout for tool execution (default: 60000)
|
|
68
|
+
-o <path> Output dir for downloads
|
|
69
|
+
--as <name> Override upload filename
|
|
70
|
+
--api-key <key> API key (overrides saved config and LOTICS_API_KEY)
|
|
71
|
+
--local (lotics auth) Save credentials to ./.lotics/config.json
|
|
72
|
+
--version Show version
|
|
73
|
+
|
|
74
|
+
CONFIG
|
|
75
|
+
Credentials resolve from a .lotics/config.json found by walking up from the
|
|
76
|
+
current directory, else the global ~/.lotics/config.json. Run "lotics auth
|
|
77
|
+
api-key <key> --local" inside a directory (e.g. a worktree) to pin it to its
|
|
78
|
+
own account and workspace.
|
|
79
|
+
|
|
80
|
+
OUTPUT
|
|
81
|
+
Default output is a compact text summary optimized for AI agents —
|
|
82
|
+
use it directly, no parsing needed. --json returns raw structured
|
|
83
|
+
JSON for machine-to-machine pipelines (scripts, CI). Errors print
|
|
84
|
+
to stderr and exit with code 1.
|
|
85
|
+
|
|
86
|
+
FILES
|
|
87
|
+
Some tools generate files and return { file_id, url, filename }.
|
|
88
|
+
Use lotics download to save locally:
|
|
89
|
+
|
|
90
|
+
lotics run generate_excel_from_template '{"..."}'
|
|
91
|
+
lotics download <file_id> -o ./output/
|
|
92
|
+
|
|
93
|
+
Upload files before referencing them in tool args:
|
|
94
|
+
|
|
95
|
+
lotics upload ./data.csv ./report.pdf ./documents/
|
|
96
|
+
lotics run create_records '{"table_id":"tbl_...","records":[{"fld_file":["fil_..."]}]}'
|
|
97
|
+
|
|
98
|
+
STDIN
|
|
99
|
+
Pipe JSON arguments via stdin instead of inline:
|
|
100
|
+
|
|
101
|
+
echo '{"table_id":"tbl_..."}' | lotics run query_records`);
|
|
102
|
+
}
|
|
103
|
+
function printAuthHelp() {
|
|
104
|
+
console.log(`Authentication commands:
|
|
105
|
+
|
|
106
|
+
lotics auth signup <email> Create account, org, workspace, and API key
|
|
107
|
+
lotics auth api-key [key] Save an existing API key (e.g. from the web app)
|
|
108
|
+
lotics auth web Send a magic link email to access the web app
|
|
109
|
+
lotics auth whoami Show the current account's name, email, and organization
|
|
110
|
+
lotics auth logout Remove saved credentials
|
|
111
|
+
|
|
112
|
+
Auth flags:
|
|
113
|
+
--local Save credentials to ./.lotics/config.json (this directory)
|
|
114
|
+
instead of the global ~/.lotics — applies to signup + api-key
|
|
115
|
+
--name <name> (signup) Display name (defaults to email prefix)
|
|
116
|
+
--timezone <timezone> (signup) Workspace timezone (defaults to UTC, e.g. Asia/Ho_Chi_Minh)
|
|
117
|
+
|
|
118
|
+
Signup sends a magic link email so you can access the Lotics web app.
|
|
119
|
+
Use lotics auth web to request a new magic link at any time.
|
|
120
|
+
|
|
121
|
+
Auth priority: --api-key flag > LOTICS_API_KEY env > config file.
|
|
122
|
+
Config file: .lotics/config.json found by walking up from the current directory,
|
|
123
|
+
else ~/.lotics/config.json. A per-directory config pins a project or worktree to
|
|
124
|
+
its own account and workspace; --local creates one. Note: an exported
|
|
125
|
+
LOTICS_API_KEY env var overrides the config file's key.`);
|
|
126
|
+
}
|
|
127
|
+
function readStdin() {
|
|
128
|
+
return new Promise((resolve, reject) => {
|
|
129
|
+
const chunks = [];
|
|
130
|
+
process.stdin.on("data", (chunk) => chunks.push(chunk));
|
|
131
|
+
process.stdin.on("end", () => resolve(Buffer.concat(chunks).toString("utf-8").trim()));
|
|
132
|
+
process.stdin.on("error", reject);
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
function prompt(question) {
|
|
136
|
+
const rl = readline.createInterface({
|
|
137
|
+
input: process.stdin,
|
|
138
|
+
output: process.stderr,
|
|
139
|
+
});
|
|
140
|
+
return new Promise((resolve) => {
|
|
141
|
+
rl.question(question, (answer) => {
|
|
142
|
+
rl.close();
|
|
143
|
+
resolve(answer.trim());
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
async function publicPost(path, body) {
|
|
148
|
+
const response = await fetch(`${API_BASE_URL}${path}`, {
|
|
149
|
+
method: "POST",
|
|
150
|
+
headers: { "Content-Type": "application/json" },
|
|
151
|
+
body: JSON.stringify(body),
|
|
152
|
+
});
|
|
153
|
+
const data = await response.json();
|
|
154
|
+
return { ok: response.ok, status: response.status, data };
|
|
155
|
+
}
|
|
156
|
+
async function handleSignup(positionalEmail, flags) {
|
|
157
|
+
const email = positionalEmail ?? (process.stdin.isTTY ? await prompt("Email: ") : "");
|
|
158
|
+
if (!email) {
|
|
159
|
+
console.error("Email is required. Usage: lotics auth signup <email>");
|
|
160
|
+
process.exit(1);
|
|
161
|
+
}
|
|
162
|
+
const name = flags.name ?? (process.stdin.isTTY ? await prompt("Name (enter to use email): ") : undefined);
|
|
163
|
+
const body = { email };
|
|
164
|
+
if (name)
|
|
165
|
+
body.name = name;
|
|
166
|
+
if (flags.timezone)
|
|
167
|
+
body.timezone = flags.timezone;
|
|
168
|
+
const { ok, status, data } = await publicPost("/v1/cli/signup", body);
|
|
169
|
+
if (!ok) {
|
|
170
|
+
if (status === 409) {
|
|
171
|
+
console.error("An account with this email already exists. Use: lotics auth api-key");
|
|
172
|
+
}
|
|
173
|
+
else if (status === 429) {
|
|
174
|
+
console.error("Too many signup attempts. Try again later.");
|
|
175
|
+
}
|
|
176
|
+
else {
|
|
177
|
+
console.error(`Signup failed: ${data.message ?? JSON.stringify(data)}`);
|
|
178
|
+
}
|
|
179
|
+
process.exit(1);
|
|
180
|
+
}
|
|
181
|
+
const scope = flags.local ? "local" : "auto";
|
|
182
|
+
const existing = loadConfig(scope) ?? {};
|
|
183
|
+
saveConfig({
|
|
184
|
+
...existing,
|
|
185
|
+
api_key: data.api_key,
|
|
186
|
+
email: data.email,
|
|
187
|
+
workspace_id: data.workspace_id,
|
|
188
|
+
}, scope);
|
|
189
|
+
console.error(`Account created. You can now use the CLI.`);
|
|
190
|
+
console.error(` Email: ${data.email}`);
|
|
191
|
+
console.error(` Config: ${getConfigPath(scope)}`);
|
|
192
|
+
console.error(`\nCheck your email for a magic link to access the Lotics web app.`);
|
|
193
|
+
console.error(`Run "lotics auth web" to request a new link at any time.`);
|
|
194
|
+
}
|
|
195
|
+
async function handleSetup(providedKey, local) {
|
|
196
|
+
const apiKey = providedKey ?? await prompt("Enter your API key: ");
|
|
197
|
+
if (!apiKey) {
|
|
198
|
+
console.error("No API key provided.");
|
|
199
|
+
process.exit(1);
|
|
200
|
+
}
|
|
201
|
+
const client = new LoticsClient({ apiKey });
|
|
202
|
+
let email;
|
|
203
|
+
try {
|
|
204
|
+
const info = await client.whoami();
|
|
205
|
+
email = info.email;
|
|
206
|
+
}
|
|
207
|
+
catch (error) {
|
|
208
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
209
|
+
console.error(`Authentication failed: ${message}`);
|
|
210
|
+
process.exit(1);
|
|
211
|
+
}
|
|
212
|
+
const scope = local ? "local" : "auto";
|
|
213
|
+
const existing = loadConfig(scope) ?? {};
|
|
214
|
+
const newConfig = { ...existing, api_key: apiKey, email };
|
|
215
|
+
// Auto-resolve workspace
|
|
216
|
+
try {
|
|
217
|
+
const workspaces = await client.listWorkspaces();
|
|
218
|
+
if (workspaces.length === 1) {
|
|
219
|
+
newConfig.workspace_id = workspaces[0].id;
|
|
220
|
+
}
|
|
221
|
+
else if (workspaces.length > 1) {
|
|
222
|
+
console.error(`\nMultiple workspaces found. Run "lotics workspace select <id>" to choose one:`);
|
|
223
|
+
printWorkspaceList(workspaces);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
catch (error) {
|
|
227
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
228
|
+
console.error(`Warning: could not resolve workspace: ${msg}`);
|
|
229
|
+
}
|
|
230
|
+
saveConfig(newConfig, scope);
|
|
231
|
+
console.error("Authenticated.");
|
|
232
|
+
console.error(` Config: ${getConfigPath(scope)}`);
|
|
233
|
+
}
|
|
234
|
+
function requireClient(flags) {
|
|
235
|
+
const auth = resolveAuth(flags);
|
|
236
|
+
if (!auth) {
|
|
237
|
+
console.error('Not authenticated. Run "lotics auth signup" or set LOTICS_API_KEY.');
|
|
238
|
+
process.exit(1);
|
|
239
|
+
}
|
|
240
|
+
const config = loadConfig();
|
|
241
|
+
return new LoticsClient({ apiKey: auth.apiKey, workspaceId: config?.workspace_id });
|
|
242
|
+
}
|
|
243
|
+
function printWorkspaceList(workspaces, currentId) {
|
|
244
|
+
for (const ws of workspaces) {
|
|
245
|
+
const marker = ws.id === currentId ? " (current)" : "";
|
|
246
|
+
console.error(` ${ws.id} ${ws.name} ${ws.timezone} ${ws.default_currency}${marker}`);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
async function resolveWorkspace(client) {
|
|
250
|
+
const config = loadConfig();
|
|
251
|
+
if (config?.workspace_id)
|
|
252
|
+
return;
|
|
253
|
+
let workspaces;
|
|
254
|
+
try {
|
|
255
|
+
workspaces = await client.listWorkspaces();
|
|
256
|
+
}
|
|
257
|
+
catch (error) {
|
|
258
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
259
|
+
console.error(`Failed to list workspaces: ${message}`);
|
|
260
|
+
process.exit(1);
|
|
261
|
+
}
|
|
262
|
+
if (workspaces.length === 0) {
|
|
263
|
+
console.error("No workspaces found for this organization.");
|
|
264
|
+
process.exit(1);
|
|
265
|
+
}
|
|
266
|
+
if (workspaces.length === 1) {
|
|
267
|
+
const existing = config ?? {};
|
|
268
|
+
saveConfig({ ...existing, workspace_id: workspaces[0].id });
|
|
269
|
+
client.setWorkspaceId(workspaces[0].id);
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
// Multiple workspaces, none selected
|
|
273
|
+
console.error("Multiple workspaces available. Select one with:\n");
|
|
274
|
+
console.error(' lotics workspace select <id>\n');
|
|
275
|
+
printWorkspaceList(workspaces);
|
|
276
|
+
console.error("");
|
|
277
|
+
process.exit(1);
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* Resolve upload paths: files pass through, directories expand to their immediate files.
|
|
281
|
+
*/
|
|
282
|
+
function resolveUploadPaths(rawPaths) {
|
|
283
|
+
const result = [];
|
|
284
|
+
for (const p of rawPaths) {
|
|
285
|
+
const resolved = path.resolve(p);
|
|
286
|
+
const stat = fs.statSync(resolved);
|
|
287
|
+
if (stat.isDirectory()) {
|
|
288
|
+
const entries = fs.readdirSync(resolved, { withFileTypes: true });
|
|
289
|
+
for (const entry of entries) {
|
|
290
|
+
if (entry.isFile()) {
|
|
291
|
+
result.push(path.join(resolved, entry.name));
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
else {
|
|
296
|
+
result.push(resolved);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
return result;
|
|
300
|
+
}
|
|
301
|
+
async function main() {
|
|
302
|
+
const { command, subcommand, toolArgs, restArgs, flags } = parseArgs(process.argv.slice(2));
|
|
303
|
+
if (flags.help || (!command && !flags.version)) {
|
|
304
|
+
printHelp();
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
if (flags.version) {
|
|
308
|
+
console.log(VERSION);
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
// --- lotics auth <subcommand> ---
|
|
312
|
+
if (command === "auth") {
|
|
313
|
+
if (subcommand === "signup") {
|
|
314
|
+
await handleSignup(toolArgs, flags);
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
if (subcommand === "api-key") {
|
|
318
|
+
await handleSetup(toolArgs ?? flags.apiKey, flags.local);
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
if (subcommand === "whoami") {
|
|
322
|
+
const auth = resolveAuth(flags);
|
|
323
|
+
if (!auth) {
|
|
324
|
+
console.error('Not authenticated. Run "lotics auth signup" or set LOTICS_API_KEY.');
|
|
325
|
+
process.exit(1);
|
|
326
|
+
}
|
|
327
|
+
const client = new LoticsClient({ apiKey: auth.apiKey });
|
|
328
|
+
const info = await client.whoami();
|
|
329
|
+
const existing = loadConfig() ?? {};
|
|
330
|
+
saveConfig({ ...existing, email: info.email });
|
|
331
|
+
if (flags.json) {
|
|
332
|
+
console.log(JSON.stringify(info, null, 2));
|
|
333
|
+
}
|
|
334
|
+
else {
|
|
335
|
+
console.log(`Name: ${info.name}`);
|
|
336
|
+
console.log(`Email: ${info.email}`);
|
|
337
|
+
console.log(`Org: ${info.organization_name} (${info.organization_id})`);
|
|
338
|
+
}
|
|
339
|
+
console.error(`Config: ${getConfigPath()}`);
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
if (subcommand === "logout") {
|
|
343
|
+
deleteConfig();
|
|
344
|
+
console.error("Logged out. Credentials removed.");
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
if (subcommand === "web") {
|
|
348
|
+
const client = requireClient(flags);
|
|
349
|
+
const { email } = await client.login();
|
|
350
|
+
console.error(`Magic link sent to ${email}. Check your email to access the Lotics web app.`);
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
if (subcommand) {
|
|
354
|
+
console.error(`Unknown auth subcommand: ${subcommand}\n`);
|
|
355
|
+
}
|
|
356
|
+
printAuthHelp();
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
// --- Local file commands — no auth required ---
|
|
360
|
+
if (command === "xlsx") {
|
|
361
|
+
await runXlsxCommand(subcommand, toolArgs, restArgs);
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
if (command === "docx") {
|
|
365
|
+
await runDocxCommand(subcommand, toolArgs, restArgs);
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
// --- Validate command before auth ---
|
|
369
|
+
if (command !== "tools" && command !== "upload" && command !== "run" && command !== "download" && command !== "workspace" && command !== "app") {
|
|
370
|
+
console.error(`Unknown command: ${command}`);
|
|
371
|
+
console.error('Run "lotics --help" for usage.');
|
|
372
|
+
process.exit(1);
|
|
373
|
+
}
|
|
374
|
+
if (command === "app" && !subcommand) {
|
|
375
|
+
console.error("Usage:");
|
|
376
|
+
console.error(" lotics app create <name> [path] Scaffold a new app locally");
|
|
377
|
+
console.error(" lotics app pull <app_id> [path] Pull an existing app for local editing");
|
|
378
|
+
console.error(" lotics app deploy [-m <message>] [--force-workflow-sync]");
|
|
379
|
+
console.error(" Build + upload the current directory");
|
|
380
|
+
console.error(" lotics app subdomain <new-subdomain> Rename the app's public <slug>.lotics.app address");
|
|
381
|
+
console.error(" lotics app dev [path] Run the app locally with HMR + RPC forwarding");
|
|
382
|
+
process.exit(1);
|
|
383
|
+
}
|
|
384
|
+
if (command === "run" && !subcommand) {
|
|
385
|
+
console.error('Usage: lotics run <tool> \'<json_args>\'');
|
|
386
|
+
console.error('Run "lotics tools" to see available tools.');
|
|
387
|
+
process.exit(1);
|
|
388
|
+
}
|
|
389
|
+
if (command === "upload" && !subcommand) {
|
|
390
|
+
console.error('Usage: lotics upload <file|dir...> [--as <name>]');
|
|
391
|
+
console.error('Uploads files and returns their file_ids. Directories expand to their immediate files.');
|
|
392
|
+
process.exit(1);
|
|
393
|
+
}
|
|
394
|
+
if (command === "download" && !subcommand) {
|
|
395
|
+
console.error('Usage:');
|
|
396
|
+
console.error(' lotics download <file_id> [-o <dir>]');
|
|
397
|
+
console.error(' lotics download record <record_id> <field_key> [-o <dir>]');
|
|
398
|
+
console.error('File IDs come from upload results or generate_* tool output (--json).');
|
|
399
|
+
process.exit(1);
|
|
400
|
+
}
|
|
401
|
+
if (command === "download" && subcommand === "record" && (!toolArgs || restArgs.length === 0)) {
|
|
402
|
+
console.error('Usage: lotics download record <record_id> <field_key> [-o <dir>]');
|
|
403
|
+
console.error('Downloads every file on the given file field into the output dir.');
|
|
404
|
+
process.exit(1);
|
|
405
|
+
}
|
|
406
|
+
const client = requireClient(flags);
|
|
407
|
+
// lotics workspace / lotics workspace list / lotics workspace select <id>
|
|
408
|
+
if (command === "workspace") {
|
|
409
|
+
const workspaces = await client.listWorkspaces();
|
|
410
|
+
const config = loadConfig();
|
|
411
|
+
if (subcommand === "select") {
|
|
412
|
+
const targetId = toolArgs;
|
|
413
|
+
if (!targetId) {
|
|
414
|
+
console.error('Usage: lotics workspace select <workspace_id>');
|
|
415
|
+
process.exit(1);
|
|
416
|
+
}
|
|
417
|
+
const target = workspaces.find((ws) => ws.id === targetId);
|
|
418
|
+
if (!target) {
|
|
419
|
+
console.error(`Workspace not found: ${targetId}\n\nAvailable workspaces:`);
|
|
420
|
+
printWorkspaceList(workspaces, config?.workspace_id);
|
|
421
|
+
process.exit(1);
|
|
422
|
+
}
|
|
423
|
+
const existing = config ?? {};
|
|
424
|
+
saveConfig({ ...existing, workspace_id: target.id });
|
|
425
|
+
console.error(`Switched to workspace: ${target.name} (${target.id})`);
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
if (subcommand === "create") {
|
|
429
|
+
const name = toolArgs;
|
|
430
|
+
if (!name) {
|
|
431
|
+
console.error('Usage: lotics workspace create <name> [--timezone <tz>]');
|
|
432
|
+
process.exit(1);
|
|
433
|
+
}
|
|
434
|
+
const timezone = flags.timezone;
|
|
435
|
+
const created = await client.createWorkspace({ name, timezone });
|
|
436
|
+
const existing = config ?? {};
|
|
437
|
+
saveConfig({ ...existing, workspace_id: created.id });
|
|
438
|
+
client.setWorkspaceId(created.id);
|
|
439
|
+
if (flags.json) {
|
|
440
|
+
console.log(JSON.stringify(created, null, 2));
|
|
441
|
+
}
|
|
442
|
+
else {
|
|
443
|
+
console.error(`Created workspace: ${created.name} (${created.id})`);
|
|
444
|
+
console.error(`Switched to ${created.id}`);
|
|
445
|
+
}
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
// Default: list workspaces
|
|
449
|
+
if (subcommand && subcommand !== "list") {
|
|
450
|
+
console.error(`Unknown workspace subcommand: ${subcommand}`);
|
|
451
|
+
console.error('Usage: lotics workspace [list | select <id> | create <name>]');
|
|
452
|
+
process.exit(1);
|
|
453
|
+
}
|
|
454
|
+
if (flags.json) {
|
|
455
|
+
console.log(JSON.stringify(workspaces, null, 2));
|
|
456
|
+
}
|
|
457
|
+
else {
|
|
458
|
+
if (workspaces.length === 0) {
|
|
459
|
+
console.error("No workspaces found.");
|
|
460
|
+
}
|
|
461
|
+
else {
|
|
462
|
+
for (const ws of workspaces) {
|
|
463
|
+
const marker = ws.id === config?.workspace_id ? " (current)" : "";
|
|
464
|
+
console.log(`${ws.id} ${ws.name} ${ws.timezone} ${ws.default_currency}${marker}`);
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
console.error(`Config: ${getConfigPath()}`);
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
// Ensure workspace is resolved for all remaining commands
|
|
472
|
+
await resolveWorkspace(client);
|
|
473
|
+
// lotics app create / pull / deploy
|
|
474
|
+
if (command === "app") {
|
|
475
|
+
if (subcommand === "create") {
|
|
476
|
+
const name = toolArgs;
|
|
477
|
+
if (!name) {
|
|
478
|
+
console.error("Usage: lotics app create <name> [path]");
|
|
479
|
+
process.exit(1);
|
|
480
|
+
}
|
|
481
|
+
const targetPath = restArgs[0];
|
|
482
|
+
await appCreate(client, { name, targetPath });
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
if (subcommand === "pull") {
|
|
486
|
+
const appId = toolArgs;
|
|
487
|
+
if (!appId) {
|
|
488
|
+
console.error("Usage: lotics app pull <app_id> [path]");
|
|
489
|
+
process.exit(1);
|
|
490
|
+
}
|
|
491
|
+
const targetPath = restArgs[0];
|
|
492
|
+
await appPull(client, { app_id: appId, targetPath });
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
495
|
+
if (subcommand === "deploy") {
|
|
496
|
+
// The message is either `-m <message>` or a bare positional arg after
|
|
497
|
+
// `deploy`. `--force-workflow-sync` is a parsed boolean flag, valid in
|
|
498
|
+
// any position.
|
|
499
|
+
const message = flags.message ?? toolArgs;
|
|
500
|
+
await appDeploy(client, { message, forceWorkflowSync: flags.forceWorkflowSync });
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
503
|
+
if (subcommand === "subdomain") {
|
|
504
|
+
const newSubdomain = toolArgs;
|
|
505
|
+
if (!newSubdomain) {
|
|
506
|
+
console.error("Usage: lotics app subdomain <new-subdomain>");
|
|
507
|
+
console.error("Renames the app's public address — https://<slug>.lotics.app");
|
|
508
|
+
process.exit(1);
|
|
509
|
+
}
|
|
510
|
+
await appSetSubdomain(client, { subdomain: newSubdomain });
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
513
|
+
if (subcommand === "dev") {
|
|
514
|
+
// First positional is an optional project path (defaults to cwd).
|
|
515
|
+
// --port and --vite-port can override the wrapper / Vite ports.
|
|
516
|
+
const projectDir = toolArgs;
|
|
517
|
+
let port;
|
|
518
|
+
let vitePort;
|
|
519
|
+
for (const a of restArgs) {
|
|
520
|
+
const portMatch = /^--port=(\d+)$/.exec(a);
|
|
521
|
+
const vitePortMatch = /^--vite-port=(\d+)$/.exec(a);
|
|
522
|
+
if (portMatch)
|
|
523
|
+
port = Number(portMatch[1]);
|
|
524
|
+
else if (vitePortMatch)
|
|
525
|
+
vitePort = Number(vitePortMatch[1]);
|
|
526
|
+
}
|
|
527
|
+
await appDev(client, { projectDir, port, vitePort });
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
530
|
+
console.error(`Unknown app subcommand: ${subcommand}`);
|
|
531
|
+
console.error("Run 'lotics app' for usage.");
|
|
532
|
+
process.exit(1);
|
|
533
|
+
}
|
|
534
|
+
// lotics tools / lotics tools <name>
|
|
535
|
+
if (command === "tools") {
|
|
536
|
+
if (subcommand) {
|
|
537
|
+
const info = await client.getTool(subcommand);
|
|
538
|
+
if (flags.json) {
|
|
539
|
+
console.log(JSON.stringify(info, null, 2));
|
|
540
|
+
}
|
|
541
|
+
else {
|
|
542
|
+
console.log(`${info.name}\n\n${info.description}\n\nInput schema:\n${JSON.stringify(info.input_schema, null, 2)}`);
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
else {
|
|
546
|
+
const { categories } = await client.listTools();
|
|
547
|
+
if (flags.json) {
|
|
548
|
+
console.log(JSON.stringify(categories, null, 2));
|
|
549
|
+
}
|
|
550
|
+
else {
|
|
551
|
+
for (const [category, { description, tools }] of Object.entries(categories)) {
|
|
552
|
+
console.log(description ? `\n## ${category} — ${description}` : `\n## ${category}`);
|
|
553
|
+
console.log(` ${tools.join(", ")}`);
|
|
554
|
+
}
|
|
555
|
+
console.log("");
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
return;
|
|
559
|
+
}
|
|
560
|
+
// lotics upload <file|dir...> [--as <name>]
|
|
561
|
+
if (command === "upload") {
|
|
562
|
+
const rawPaths = [subcommand, ...(toolArgs ? [toolArgs] : []), ...restArgs];
|
|
563
|
+
const filePaths = resolveUploadPaths(rawPaths);
|
|
564
|
+
if (filePaths.length === 0) {
|
|
565
|
+
console.error("No files found in the specified paths.");
|
|
566
|
+
process.exit(1);
|
|
567
|
+
}
|
|
568
|
+
if (flags.as && filePaths.length > 1) {
|
|
569
|
+
console.error("Cannot use --as with multiple files.");
|
|
570
|
+
process.exit(1);
|
|
571
|
+
}
|
|
572
|
+
const upload = await client.uploadFiles(filePaths, {
|
|
573
|
+
filenames: flags.as ? [flags.as] : undefined,
|
|
574
|
+
});
|
|
575
|
+
if (upload.files.length === 0 && upload.errors.length > 0) {
|
|
576
|
+
console.error(`Upload failed: ${upload.errors.map((e) => `${e.filename}: ${e.error}`).join(", ")}`);
|
|
577
|
+
process.exit(1);
|
|
578
|
+
}
|
|
579
|
+
if (flags.json) {
|
|
580
|
+
console.log(JSON.stringify({ files: upload.files, errors: upload.errors }, null, 2));
|
|
581
|
+
}
|
|
582
|
+
else {
|
|
583
|
+
for (let i = 0; i < upload.files.length; i++) {
|
|
584
|
+
const file = upload.files[i];
|
|
585
|
+
console.log(`[${i + 1}] ${file.id} ${file.filename} ${file.mime_type}`);
|
|
586
|
+
}
|
|
587
|
+
for (const error of upload.errors) {
|
|
588
|
+
console.error(`FAILED ${error.filename} ${error.error}`);
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
593
|
+
// lotics download <file_id> [-o <path>]
|
|
594
|
+
// lotics download record <record_id> <field_key> [-o <path>]
|
|
595
|
+
if (command === "download") {
|
|
596
|
+
if (subcommand === "record") {
|
|
597
|
+
const recordId = toolArgs;
|
|
598
|
+
const fieldKey = restArgs[0];
|
|
599
|
+
const files = await client.downloadRecordFiles(recordId, fieldKey, flags.output);
|
|
600
|
+
if (flags.json) {
|
|
601
|
+
console.log(JSON.stringify(files, null, 2));
|
|
602
|
+
}
|
|
603
|
+
else {
|
|
604
|
+
for (const f of files) {
|
|
605
|
+
console.log(`${f.file_id} ${f.path} ${f.filename}`);
|
|
606
|
+
}
|
|
607
|
+
console.error(`Downloaded ${files.length} file${files.length === 1 ? "" : "s"} from ${recordId}.${fieldKey}`);
|
|
608
|
+
}
|
|
609
|
+
return;
|
|
610
|
+
}
|
|
611
|
+
const { path: filePath, filename } = await client.downloadFileById(subcommand, flags.output);
|
|
612
|
+
console.error(`Downloaded: ${filePath} (${filename})`);
|
|
613
|
+
return;
|
|
614
|
+
}
|
|
615
|
+
// lotics run <tool> [json_args]
|
|
616
|
+
if (command === "run") {
|
|
617
|
+
const toolName = subcommand;
|
|
618
|
+
let rawArgs = toolArgs;
|
|
619
|
+
if (!rawArgs && !process.stdin.isTTY) {
|
|
620
|
+
rawArgs = await readStdin();
|
|
621
|
+
}
|
|
622
|
+
let args = {};
|
|
623
|
+
if (rawArgs) {
|
|
624
|
+
try {
|
|
625
|
+
args = JSON.parse(rawArgs);
|
|
626
|
+
}
|
|
627
|
+
catch {
|
|
628
|
+
console.error(`Invalid JSON: ${rawArgs}`);
|
|
629
|
+
process.exit(1);
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
const timeoutMs = flags.timeout ?? 60000;
|
|
633
|
+
// Always request text format so model_output is available; --json only affects CLI output
|
|
634
|
+
const result = await client.execute(toolName, args, { format: "text", timeoutMs });
|
|
635
|
+
if (result.error) {
|
|
636
|
+
if (flags.json) {
|
|
637
|
+
console.log(JSON.stringify({ error: result.error }, null, 2));
|
|
638
|
+
}
|
|
639
|
+
else {
|
|
640
|
+
console.error(result.error);
|
|
641
|
+
console.error(`\nHint: run "lotics tools ${toolName}" to see the expected input schema.`);
|
|
642
|
+
}
|
|
643
|
+
process.exit(1);
|
|
644
|
+
}
|
|
645
|
+
if (flags.json) {
|
|
646
|
+
console.log(JSON.stringify(result.result, null, 2));
|
|
647
|
+
}
|
|
648
|
+
else if (result.model_output) {
|
|
649
|
+
console.log(result.model_output);
|
|
650
|
+
}
|
|
651
|
+
else {
|
|
652
|
+
// Fallback: compact JSON when no text output is available
|
|
653
|
+
console.log(JSON.stringify(result.result, null, 2));
|
|
654
|
+
}
|
|
655
|
+
return;
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
main()
|
|
659
|
+
.then(() => checkForUpdate(VERSION))
|
|
660
|
+
.catch((error) => {
|
|
661
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
662
|
+
if (message.includes("fetch failed") || message.includes("ECONNREFUSED")) {
|
|
663
|
+
console.error("Could not connect to the Lotics API.");
|
|
664
|
+
}
|
|
665
|
+
else if (message.includes("ENOENT")) {
|
|
666
|
+
const pathMatch = message.match(/open '([^']+)'/);
|
|
667
|
+
console.error(`File not found: ${pathMatch?.[1] ?? message}`);
|
|
668
|
+
}
|
|
669
|
+
else {
|
|
670
|
+
console.error(message);
|
|
671
|
+
}
|
|
672
|
+
process.exit(1);
|
|
673
|
+
});
|