@lotics/cli 0.88.0 → 0.89.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 +162 -100
- package/dist/{client.d.ts → src/client.d.ts} +9 -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
package/dist/cli.js
DELETED
|
@@ -1,1095 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import dns from "node:dns";
|
|
3
|
-
import net from "node:net";
|
|
4
|
-
import fs from "node:fs";
|
|
5
|
-
import path from "node:path";
|
|
6
|
-
import readline from "node:readline";
|
|
7
|
-
// WSL2 + Node 24's Happy-Eyeballs races IPv4/IPv6 and intermittently stalls on a
|
|
8
|
-
// dead IPv6 route to api.lotics.ai (curl works; node fetch ETIMEDOUTs). Prefer
|
|
9
|
-
// IPv4 and cap the per-family attempt so a bad IPv6 path fails fast to IPv4.
|
|
10
|
-
// Bin-only side effect — client.ts stays pure for SDK consumers.
|
|
11
|
-
dns.setDefaultResultOrder("ipv4first");
|
|
12
|
-
net.setDefaultAutoSelectFamilyAttemptTimeout(2000);
|
|
13
|
-
import { LoticsClient, API_BASE_URL } from "./client.js";
|
|
14
|
-
import { resolveContext, deleteConfig, getConfigPath, loadGlobalConfig, saveGlobalConfig, loadLocalConfig, upsertProfile, removeProfile, setActiveOrg, setSelectedWorkspace, resolveProfileByNameOrId, checkForUpdate, } from "./config.js";
|
|
15
|
-
import { VERSION } from "./version.js";
|
|
16
|
-
import { appCreate, appPull, appDeploy, appDev, appSetSubdomain, appRename, appVersions, appCodegen, appExecuteWorkflow, appWorkflowSet, appWorkflowPull, appWorkflowCheck, appQuerySet, appUiLink, } from "./app_commands.js";
|
|
17
|
-
import { parseArgs } from "./args.js";
|
|
18
|
-
import { ingestJsonArgs } from "./inputs.js";
|
|
19
|
-
import { runXlsxCommand } from "./xlsx.js";
|
|
20
|
-
import { runDocxCommand } from "./docx.js";
|
|
21
|
-
import { runPreviewCommand } from "./preview.js";
|
|
22
|
-
function printHelp() {
|
|
23
|
-
console.log(`Lotics CLI v${VERSION} — AI agent interface for Lotics
|
|
24
|
-
|
|
25
|
-
Lotics is an AI-powered operations platform. Through this CLI you can:
|
|
26
|
-
- Manage tables, records, and views (structured data with typed fields)
|
|
27
|
-
- Generate documents from templates (Excel, Word, PDF, Email)
|
|
28
|
-
- Build and run automations — schedules, webhooks, table lifecycle workflows,
|
|
29
|
-
button actions, and app-action workflows; inspect execution and version history
|
|
30
|
-
- Administer the workspace — invite members, manage groups, share resources,
|
|
31
|
-
transfer ownership, browse connected accounts (OAuth attach is web-only)
|
|
32
|
-
- Create and manage apps, knowledge docs, and files
|
|
33
|
-
|
|
34
|
-
AUTHENTICATION
|
|
35
|
-
lotics auth Show auth help
|
|
36
|
-
lotics auth signup <email> Create account (run "lotics auth" for details)
|
|
37
|
-
lotics auth api-key <key> Save a key — registers its org as a profile
|
|
38
|
-
|
|
39
|
-
ORGANIZATIONS
|
|
40
|
-
Each saved API key belongs to one org. Register a key per org once, then
|
|
41
|
-
switch between them with no re-pasting. Workspaces live inside an org.
|
|
42
|
-
lotics org List saved orgs (marks active)
|
|
43
|
-
lotics org use <name|id> Switch the active org
|
|
44
|
-
lotics org use <name|id> --local Pin THIS directory to an org (worktrees)
|
|
45
|
-
|
|
46
|
-
USAGE
|
|
47
|
-
1. lotics auth signup <email> Create account or authenticate
|
|
48
|
-
2. lotics org See saved orgs / which one is active
|
|
49
|
-
3. lotics workspace Check current workspace (auto-selects if only one)
|
|
50
|
-
4. lotics tools List available tools by category
|
|
51
|
-
5. lotics tools <name> Show tool description and full input schema
|
|
52
|
-
6. lotics run <tool> '<json>' Execute a tool with JSON arguments
|
|
53
|
-
|
|
54
|
-
Always inspect the schema (step 4) before calling a tool.
|
|
55
|
-
Tools are grouped by category (tables, records, views, etc.).
|
|
56
|
-
Query tools return IDs used as arguments to other tools.
|
|
57
|
-
|
|
58
|
-
COMMANDS
|
|
59
|
-
lotics org List saved orgs (marks active)
|
|
60
|
-
lotics org use <name|id> [--local] Switch active org (--local pins this dir)
|
|
61
|
-
lotics workspace List workspaces in the active org (marks current)
|
|
62
|
-
lotics workspace select <id> Switch to a different workspace
|
|
63
|
-
lotics workspace create <name> Create a new workspace (admin only)
|
|
64
|
-
lotics workspace delete <id> --yes Delete a workspace (admin only; soft delete, recoverable)
|
|
65
|
-
lotics workspace doctor Report dangling schema references (admin only)
|
|
66
|
-
lotics tools List all available tools
|
|
67
|
-
lotics tools <name> Show tool description and input schema
|
|
68
|
-
lotics run <tool> '<json>' Execute a tool
|
|
69
|
-
lotics run <tool> @args.json Read JSON args from a file (large payloads)
|
|
70
|
-
cat args.json | lotics run <tool> Read JSON args from stdin (large payloads)
|
|
71
|
-
lotics app create <name> [path] Create a new custom-code app + scaffold locally
|
|
72
|
-
lotics app pull <app_id> [path] Bootstrap full local env (source + npm install + types)
|
|
73
|
-
lotics app deploy -m <message> Build + upload current dir as a new version — COMMIT
|
|
74
|
-
(-m is required — it's the version's audit trail;
|
|
75
|
-
carries code + queries only — workflow bindings are
|
|
76
|
-
managed by set_app_workflow / remove_app_workflow)
|
|
77
|
-
lotics app versions [app_id] Show deploy history newest-first (version,
|
|
78
|
-
timestamp, deployer, build status, -m message;
|
|
79
|
-
* marks the currently served version)
|
|
80
|
-
lotics app codegen [path] Regenerate .lotics/* (types + field/option ids)
|
|
81
|
-
from the manifest + workspace schema — no deploy
|
|
82
|
-
lotics app workflow run <alias> '<json>' Execute a bound app workflow end-to-end
|
|
83
|
-
(inputs: inline JSON, @file, or stdin;
|
|
84
|
-
--print-created reports created records +
|
|
85
|
-
a paste-ready cleanup plan; --cleanup also
|
|
86
|
-
deletes those records — NOT a rollback)
|
|
87
|
-
lotics app workflow set <alias> Push the edited src/workflows/<alias>.ts body
|
|
88
|
-
through set_app_workflow (server verifies)
|
|
89
|
-
lotics app workflow pull Rewrite src/workflows/*.ts from the server
|
|
90
|
-
lotics app workflow check [alias] Typecheck src/workflows bodies locally (one
|
|
91
|
-
isolated program per alias; the app's own tsc)
|
|
92
|
-
lotics app query set <alias> Push package.json#lotics.queries.<alias> to
|
|
93
|
-
apps.queries via set_app_query (no deploy;
|
|
94
|
-
re-synced by the next deploy from the manifest)
|
|
95
|
-
lotics app subdomain <new-subdomain> Rename the app's public <slug>.lotics.app address
|
|
96
|
-
lotics app rename "<new name>" Rename the app's display name (launcher title)
|
|
97
|
-
lotics app dev [path] Run the app locally with HMR (RPC forwarded to prod)
|
|
98
|
-
lotics ui link <component> [--ui-src <path>] [--remove]
|
|
99
|
-
Dev-link @lotics/ui to packages/ui/src (Vite alias
|
|
100
|
-
+ tsc paths) for live HMR + typecheck. Monorepo apps
|
|
101
|
-
auto-find it; external apps pass --ui-src / LOTICS_UI_SRC
|
|
102
|
-
lotics xlsx <subcommand> ... Read/write/edit .xlsx files on your local filesystem
|
|
103
|
-
(uses the bundled Lotics xlsx engine; prefer over
|
|
104
|
-
npm xlsx/exceljs for round-trip fidelity)
|
|
105
|
-
lotics docx <subcommand> ... Read/write/edit .docx files on your local filesystem
|
|
106
|
-
(uses the bundled Lotics OOXML engine; preserves
|
|
107
|
-
tables, images, and unknown markup verbatim)
|
|
108
|
-
lotics file upload <file|dir...> Upload files (alias: lotics upload)
|
|
109
|
-
lotics file download <file_id> Download a file by ID (alias: lotics download)
|
|
110
|
-
lotics file download record <record_id> <field_key>
|
|
111
|
-
Download all files on a record file field
|
|
112
|
-
lotics file preview <file> [-o png] Render a .docx/.xlsx to a PNG (frontend engines;
|
|
113
|
-
needs a Chrome/Chromium on the machine)
|
|
114
|
-
|
|
115
|
-
FLAGS
|
|
116
|
-
--json Full JSON output (default is human-readable text)
|
|
117
|
-
--timeout <ms> Timeout for tool execution (default: 60000)
|
|
118
|
-
-o <path> Output dir for downloads
|
|
119
|
-
--as <name> Override upload filename
|
|
120
|
-
--api-key <key> One-off API key (overrides saved config + env)
|
|
121
|
-
--workspace <id> One-off workspace override (alias: -w)
|
|
122
|
-
--view-as <id> Admin "View as": run every request as this member, so
|
|
123
|
-
is_current_member / row-scoping resolve to them (also
|
|
124
|
-
LOTICS_VIEW_AS env; admin key only; writes stay yours)
|
|
125
|
-
--local Pin the current directory (lotics org use / auth api-key)
|
|
126
|
-
--all (lotics auth logout) Remove every saved credential
|
|
127
|
-
--version Show version
|
|
128
|
-
|
|
129
|
-
CONFIG
|
|
130
|
-
API keys are stored once per org in the global ~/.lotics/config.json as named
|
|
131
|
-
profiles. Switch with "lotics org use <name|id>" — no re-pasting. A directory
|
|
132
|
-
(e.g. a git worktree) can pin its own org/workspace with "lotics org use
|
|
133
|
-
<name|id> --local", which writes a .lotics/config.json pointer (the key is
|
|
134
|
-
resolved from the global store) — so a global switch never disturbs it.
|
|
135
|
-
|
|
136
|
-
Resolution precedence (highest first):
|
|
137
|
-
--api-key flag > LOTICS_API_KEY env > LOTICS_ORG env >
|
|
138
|
-
local .lotics/config.json > global active profile
|
|
139
|
-
LOTICS_WORKSPACE (or --workspace) overrides the workspace at any level.
|
|
140
|
-
|
|
141
|
-
OUTPUT
|
|
142
|
-
Default output is a compact text summary optimized for AI agents —
|
|
143
|
-
use it directly, no parsing needed. --json returns raw structured
|
|
144
|
-
JSON for machine-to-machine pipelines (scripts, CI). Errors print
|
|
145
|
-
to stderr and exit with code 1.
|
|
146
|
-
|
|
147
|
-
FILES
|
|
148
|
-
Some tools generate files and return { file_id, url, filename }.
|
|
149
|
-
Use lotics download to save locally:
|
|
150
|
-
|
|
151
|
-
lotics run generate_excel_from_template '{"..."}'
|
|
152
|
-
lotics download <file_id> -o ./output/
|
|
153
|
-
|
|
154
|
-
Upload files before referencing them in tool args:
|
|
155
|
-
|
|
156
|
-
lotics upload ./data.csv ./report.pdf ./documents/
|
|
157
|
-
lotics run create_records '{"table_id":"tbl_...","records":[{"fld_file":["fil_..."]}]}'
|
|
158
|
-
|
|
159
|
-
STDIN
|
|
160
|
-
Pipe JSON arguments via stdin instead of inline:
|
|
161
|
-
|
|
162
|
-
echo '{"table_id":"tbl_..."}' | lotics run query_records`);
|
|
163
|
-
}
|
|
164
|
-
function printAuthHelp() {
|
|
165
|
-
console.log(`Authentication commands:
|
|
166
|
-
|
|
167
|
-
lotics auth signup <email> Create account, org, workspace, and API key
|
|
168
|
-
lotics auth api-key [key] Save an API key — registers its org as a profile
|
|
169
|
-
lotics auth web Send a magic link email to access the web app
|
|
170
|
-
lotics auth whoami Show the active account, org, workspace, and source
|
|
171
|
-
lotics auth logout [<name|id>] Remove a saved credential (default: the active org,
|
|
172
|
-
or unpin the current directory)
|
|
173
|
-
lotics auth logout --all Remove every saved credential
|
|
174
|
-
|
|
175
|
-
Auth flags:
|
|
176
|
-
--local Pin the current directory instead of the global store.
|
|
177
|
-
signup/api-key write a self-contained ./.lotics/config.json
|
|
178
|
-
(inline key) — for a hermetic worktree or CI.
|
|
179
|
-
--name <name> (signup) Display name (defaults to email prefix)
|
|
180
|
-
--timezone <timezone> (signup) Workspace timezone (defaults to UTC, e.g. Asia/Ho_Chi_Minh)
|
|
181
|
-
|
|
182
|
-
API keys are stored once per org as named profiles in ~/.lotics/config.json.
|
|
183
|
-
Registering a second key adds a profile — it does not overwrite the first.
|
|
184
|
-
Switch active org with "lotics org use <name|id>".
|
|
185
|
-
|
|
186
|
-
Resolution precedence (highest first):
|
|
187
|
-
--api-key flag > LOTICS_API_KEY env > LOTICS_ORG env > local .lotics/config.json
|
|
188
|
-
> global active profile. LOTICS_WORKSPACE / --workspace override the workspace.`);
|
|
189
|
-
}
|
|
190
|
-
function readStdin() {
|
|
191
|
-
return new Promise((resolve, reject) => {
|
|
192
|
-
const chunks = [];
|
|
193
|
-
process.stdin.on("data", (chunk) => chunks.push(chunk));
|
|
194
|
-
process.stdin.on("end", () => resolve(Buffer.concat(chunks).toString("utf-8").trim()));
|
|
195
|
-
process.stdin.on("error", reject);
|
|
196
|
-
});
|
|
197
|
-
}
|
|
198
|
-
function prompt(question) {
|
|
199
|
-
const rl = readline.createInterface({
|
|
200
|
-
input: process.stdin,
|
|
201
|
-
output: process.stderr,
|
|
202
|
-
});
|
|
203
|
-
return new Promise((resolve) => {
|
|
204
|
-
rl.question(question, (answer) => {
|
|
205
|
-
rl.close();
|
|
206
|
-
resolve(answer.trim());
|
|
207
|
-
});
|
|
208
|
-
});
|
|
209
|
-
}
|
|
210
|
-
async function publicPost(path, body) {
|
|
211
|
-
const response = await fetch(`${API_BASE_URL}${path}`, {
|
|
212
|
-
method: "POST",
|
|
213
|
-
headers: { "Content-Type": "application/json" },
|
|
214
|
-
body: JSON.stringify(body),
|
|
215
|
-
});
|
|
216
|
-
const data = await response.json();
|
|
217
|
-
return { ok: response.ok, status: response.status, data };
|
|
218
|
-
}
|
|
219
|
-
async function handleSignup(positionalEmail, flags) {
|
|
220
|
-
const email = positionalEmail ?? (process.stdin.isTTY ? await prompt("Email: ") : "");
|
|
221
|
-
if (!email) {
|
|
222
|
-
console.error("Email is required. Usage: lotics auth signup <email>");
|
|
223
|
-
process.exit(1);
|
|
224
|
-
}
|
|
225
|
-
const name = flags.name ?? (process.stdin.isTTY ? await prompt("Name (enter to use email): ") : undefined);
|
|
226
|
-
const body = { email };
|
|
227
|
-
if (name)
|
|
228
|
-
body.name = name;
|
|
229
|
-
if (flags.timezone)
|
|
230
|
-
body.timezone = flags.timezone;
|
|
231
|
-
const { ok, status, data } = await publicPost("/v1/cli/signup", body);
|
|
232
|
-
if (!ok) {
|
|
233
|
-
if (status === 409) {
|
|
234
|
-
console.error("An account with this email already exists. Use: lotics auth api-key");
|
|
235
|
-
}
|
|
236
|
-
else if (status === 429) {
|
|
237
|
-
console.error("Too many signup attempts. Try again later.");
|
|
238
|
-
}
|
|
239
|
-
else {
|
|
240
|
-
console.error(`Signup failed: ${data.message ?? JSON.stringify(data)}`);
|
|
241
|
-
}
|
|
242
|
-
process.exit(1);
|
|
243
|
-
}
|
|
244
|
-
const apiKey = data.api_key;
|
|
245
|
-
const orgId = data.organization_id;
|
|
246
|
-
const workspaceId = data.workspace_id;
|
|
247
|
-
// The org is created with the display name; mirror the server's email-prefix
|
|
248
|
-
// default when none was supplied. Use `||` (not `??`) so an empty-string name
|
|
249
|
-
// falls through exactly as the server's `if (name)` truthiness check does.
|
|
250
|
-
const orgName = name || email.split("@")[0];
|
|
251
|
-
// The key always lives in the global store as a profile; --local additionally
|
|
252
|
-
// pins this directory to it (a pointer), instead of making it the global default.
|
|
253
|
-
upsertProfile(orgId, { api_key: apiKey, org_name: orgName, workspace_id: workspaceId });
|
|
254
|
-
const existing = loadGlobalConfig() ?? {};
|
|
255
|
-
saveGlobalConfig({ ...existing, email });
|
|
256
|
-
setActiveOrg(orgId, flags.local ? "local" : "global");
|
|
257
|
-
console.error(`Account created. You can now use the CLI.`);
|
|
258
|
-
console.error(` Email: ${email}`);
|
|
259
|
-
console.error(` Org: ${orgName} (${orgId})`);
|
|
260
|
-
console.error(flags.local
|
|
261
|
-
? ` Pinned this directory: ${getConfigPath("local")}`
|
|
262
|
-
: ` Saved to the "${orgName}" profile in ~/.lotics (now active)`);
|
|
263
|
-
console.error(`\nCheck your email for a magic link to access the Lotics web app.`);
|
|
264
|
-
console.error(`Run "lotics auth web" to request a new link at any time.`);
|
|
265
|
-
}
|
|
266
|
-
async function handleSetup(providedKey, local) {
|
|
267
|
-
const apiKey = providedKey ?? await prompt("Enter your API key: ");
|
|
268
|
-
if (!apiKey) {
|
|
269
|
-
console.error("No API key provided.");
|
|
270
|
-
process.exit(1);
|
|
271
|
-
}
|
|
272
|
-
const client = new LoticsClient({ apiKey });
|
|
273
|
-
let info;
|
|
274
|
-
try {
|
|
275
|
-
info = await client.whoami();
|
|
276
|
-
}
|
|
277
|
-
catch (error) {
|
|
278
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
279
|
-
console.error(`Authentication failed: ${message}`);
|
|
280
|
-
process.exit(1);
|
|
281
|
-
}
|
|
282
|
-
// Auto-resolve workspace (only when the org has exactly one).
|
|
283
|
-
let workspaceId;
|
|
284
|
-
try {
|
|
285
|
-
const workspaces = await client.listWorkspaces();
|
|
286
|
-
if (workspaces.length === 1) {
|
|
287
|
-
workspaceId = workspaces[0].id;
|
|
288
|
-
}
|
|
289
|
-
else if (workspaces.length > 1) {
|
|
290
|
-
console.error(`\nMultiple workspaces in ${info.organization_name}. Run "lotics workspace select <id>" to choose one:`);
|
|
291
|
-
printWorkspaceList(workspaces);
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
catch (error) {
|
|
295
|
-
const msg = error instanceof Error ? error.message : String(error);
|
|
296
|
-
console.error(`Warning: could not resolve workspace: ${msg}`);
|
|
297
|
-
}
|
|
298
|
-
// The key always lives in the global store as a profile; --local additionally
|
|
299
|
-
// pins this directory to it (a pointer), instead of making it the global default.
|
|
300
|
-
upsertProfile(info.organization_id, {
|
|
301
|
-
api_key: apiKey,
|
|
302
|
-
org_name: info.organization_name,
|
|
303
|
-
workspace_id: workspaceId,
|
|
304
|
-
});
|
|
305
|
-
const existing = loadGlobalConfig() ?? {};
|
|
306
|
-
saveGlobalConfig({ ...existing, email: info.email });
|
|
307
|
-
setActiveOrg(info.organization_id, local ? "local" : "global");
|
|
308
|
-
if (local) {
|
|
309
|
-
console.error(`Authenticated as ${info.email} in ${info.organization_name}.`);
|
|
310
|
-
console.error(` Pinned this directory: ${getConfigPath("local")}`);
|
|
311
|
-
}
|
|
312
|
-
else {
|
|
313
|
-
console.error(`Authenticated as ${info.email} in ${info.organization_name} (now active).`);
|
|
314
|
-
console.error(` Saved to the "${info.organization_name}" profile in ~/.lotics`);
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
function requireClient(flags) {
|
|
318
|
-
const ctx = resolveContext(flags);
|
|
319
|
-
if (!ctx) {
|
|
320
|
-
console.error('Not authenticated. Run "lotics auth signup", "lotics auth api-key <key>", or set LOTICS_API_KEY.');
|
|
321
|
-
process.exit(1);
|
|
322
|
-
}
|
|
323
|
-
// Admin "View as": flag wins over env. The server enforces admin-only; a
|
|
324
|
-
// non-admin key gets a clear 403 on the first request.
|
|
325
|
-
const viewAsMemberId = flags.viewAs ?? process.env.LOTICS_VIEW_AS;
|
|
326
|
-
return {
|
|
327
|
-
client: new LoticsClient({ apiKey: ctx.apiKey, workspaceId: ctx.workspaceId, viewAsMemberId }),
|
|
328
|
-
ctx,
|
|
329
|
-
};
|
|
330
|
-
}
|
|
331
|
-
const SOURCE_LABELS = {
|
|
332
|
-
flag: "--api-key flag",
|
|
333
|
-
env_key: "LOTICS_API_KEY env",
|
|
334
|
-
env_org: "LOTICS_ORG env",
|
|
335
|
-
local_pointer: "local .lotics/config.json (pin)",
|
|
336
|
-
global_profile: "global active profile",
|
|
337
|
-
};
|
|
338
|
-
function printWorkspaceList(workspaces, currentId) {
|
|
339
|
-
for (const ws of workspaces) {
|
|
340
|
-
const marker = ws.id === currentId ? " (current)" : "";
|
|
341
|
-
console.error(` ${ws.id} ${ws.name} ${ws.timezone} ${ws.default_currency}${marker}`);
|
|
342
|
-
}
|
|
343
|
-
}
|
|
344
|
-
async function resolveWorkspace(client, ctx) {
|
|
345
|
-
if (ctx.workspaceId) {
|
|
346
|
-
client.setWorkspaceId(ctx.workspaceId);
|
|
347
|
-
return;
|
|
348
|
-
}
|
|
349
|
-
let workspaces;
|
|
350
|
-
try {
|
|
351
|
-
workspaces = await client.listWorkspaces();
|
|
352
|
-
}
|
|
353
|
-
catch (error) {
|
|
354
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
355
|
-
console.error(`Failed to list workspaces: ${message}`);
|
|
356
|
-
process.exit(1);
|
|
357
|
-
}
|
|
358
|
-
if (workspaces.length === 0) {
|
|
359
|
-
console.error("No workspaces found for this organization.");
|
|
360
|
-
process.exit(1);
|
|
361
|
-
}
|
|
362
|
-
if (workspaces.length === 1) {
|
|
363
|
-
// Persist the auto-resolution where the credential lives; null means it
|
|
364
|
-
// came from --api-key/env with no profile, so it's an in-memory selection.
|
|
365
|
-
setSelectedWorkspace(workspaces[0].id);
|
|
366
|
-
client.setWorkspaceId(workspaces[0].id);
|
|
367
|
-
return;
|
|
368
|
-
}
|
|
369
|
-
// Multiple workspaces, none selected
|
|
370
|
-
console.error("Multiple workspaces available. Select one with:\n");
|
|
371
|
-
console.error(' lotics workspace select <id>\n');
|
|
372
|
-
printWorkspaceList(workspaces);
|
|
373
|
-
console.error("");
|
|
374
|
-
process.exit(1);
|
|
375
|
-
}
|
|
376
|
-
/**
|
|
377
|
-
* Resolve upload paths: files pass through, directories expand to their immediate files.
|
|
378
|
-
*/
|
|
379
|
-
function resolveUploadPaths(rawPaths) {
|
|
380
|
-
const result = [];
|
|
381
|
-
for (const p of rawPaths) {
|
|
382
|
-
const resolved = path.resolve(p);
|
|
383
|
-
const stat = fs.statSync(resolved);
|
|
384
|
-
if (stat.isDirectory()) {
|
|
385
|
-
const entries = fs.readdirSync(resolved, { withFileTypes: true });
|
|
386
|
-
for (const entry of entries) {
|
|
387
|
-
if (entry.isFile()) {
|
|
388
|
-
result.push(path.join(resolved, entry.name));
|
|
389
|
-
}
|
|
390
|
-
}
|
|
391
|
-
}
|
|
392
|
-
else {
|
|
393
|
-
result.push(resolved);
|
|
394
|
-
}
|
|
395
|
-
}
|
|
396
|
-
return result;
|
|
397
|
-
}
|
|
398
|
-
async function main() {
|
|
399
|
-
const parsed = parseArgs(process.argv.slice(2));
|
|
400
|
-
const { restArgs, flags } = parsed;
|
|
401
|
-
let { command, subcommand, toolArgs } = parsed;
|
|
402
|
-
// `file` is a thin noun-group over the file verbs: `lotics file <upload|download|preview>
|
|
403
|
-
// <args…>` strips the prefix and re-dispatches to the same handlers as the top-level
|
|
404
|
-
// verbs (which stay as aliases). Positional shift only — flags are untouched, and
|
|
405
|
-
// `download record <id> <field>` still works as `file download record <id> <field>`.
|
|
406
|
-
if (command === "file") {
|
|
407
|
-
if (!subcommand) {
|
|
408
|
-
printHelp();
|
|
409
|
-
return;
|
|
410
|
-
}
|
|
411
|
-
command = subcommand;
|
|
412
|
-
subcommand = toolArgs;
|
|
413
|
-
toolArgs = restArgs.shift();
|
|
414
|
-
}
|
|
415
|
-
if (flags.help || (!command && !flags.version)) {
|
|
416
|
-
printHelp();
|
|
417
|
-
return;
|
|
418
|
-
}
|
|
419
|
-
if (flags.version) {
|
|
420
|
-
console.log(VERSION);
|
|
421
|
-
return;
|
|
422
|
-
}
|
|
423
|
-
// --- lotics auth <subcommand> ---
|
|
424
|
-
if (command === "auth") {
|
|
425
|
-
if (subcommand === "signup") {
|
|
426
|
-
await handleSignup(toolArgs, flags);
|
|
427
|
-
return;
|
|
428
|
-
}
|
|
429
|
-
if (subcommand === "api-key") {
|
|
430
|
-
await handleSetup(toolArgs ?? flags.apiKey, flags.local);
|
|
431
|
-
return;
|
|
432
|
-
}
|
|
433
|
-
if (subcommand === "whoami") {
|
|
434
|
-
const ctx = resolveContext(flags);
|
|
435
|
-
if (!ctx) {
|
|
436
|
-
console.error('Not authenticated. Run "lotics auth signup", "lotics auth api-key <key>", or set LOTICS_API_KEY.');
|
|
437
|
-
process.exit(1);
|
|
438
|
-
}
|
|
439
|
-
const client = new LoticsClient({ apiKey: ctx.apiKey });
|
|
440
|
-
const info = await client.whoami();
|
|
441
|
-
const existing = loadGlobalConfig() ?? {};
|
|
442
|
-
saveGlobalConfig({ ...existing, email: info.email });
|
|
443
|
-
const viewAs = flags.viewAs ?? process.env.LOTICS_VIEW_AS;
|
|
444
|
-
if (flags.json) {
|
|
445
|
-
console.log(JSON.stringify({ ...info, workspace_id: ctx.workspaceId ?? null, source: ctx.source, view_as: viewAs ?? null }, null, 2));
|
|
446
|
-
}
|
|
447
|
-
else {
|
|
448
|
-
console.log(`Name: ${info.name}`);
|
|
449
|
-
console.log(`Email: ${info.email}`);
|
|
450
|
-
console.log(`Org: ${info.organization_name} (${info.organization_id})`);
|
|
451
|
-
console.log(`Workspace: ${ctx.workspaceId ?? "(none selected)"}`);
|
|
452
|
-
console.log(`Source: ${SOURCE_LABELS[ctx.source]}`);
|
|
453
|
-
if (viewAs)
|
|
454
|
-
console.log(`View as: ${viewAs} (admin preview; requires an admin key)`);
|
|
455
|
-
}
|
|
456
|
-
return;
|
|
457
|
-
}
|
|
458
|
-
if (subcommand === "logout") {
|
|
459
|
-
const local = loadLocalConfig();
|
|
460
|
-
// Bare logout inside a pinned directory just unpins it.
|
|
461
|
-
if (local && !flags.all && !toolArgs) {
|
|
462
|
-
deleteConfig();
|
|
463
|
-
console.error("Removed the local pin for this directory.");
|
|
464
|
-
return;
|
|
465
|
-
}
|
|
466
|
-
if (flags.all) {
|
|
467
|
-
const g = loadGlobalConfig();
|
|
468
|
-
saveGlobalConfig({ last_update_check: g?.last_update_check, latest_version: g?.latest_version });
|
|
469
|
-
console.error("Removed all saved credentials.");
|
|
470
|
-
return;
|
|
471
|
-
}
|
|
472
|
-
const g = loadGlobalConfig() ?? {};
|
|
473
|
-
const profiles = g.profiles ?? {};
|
|
474
|
-
let resolved;
|
|
475
|
-
if (toolArgs) {
|
|
476
|
-
resolved = resolveProfileByNameOrId(profiles, toolArgs);
|
|
477
|
-
}
|
|
478
|
-
else if (g.active_org && profiles[g.active_org]) {
|
|
479
|
-
resolved = [g.active_org, profiles[g.active_org]];
|
|
480
|
-
}
|
|
481
|
-
else {
|
|
482
|
-
resolved = null;
|
|
483
|
-
}
|
|
484
|
-
if (!resolved) {
|
|
485
|
-
console.error(toolArgs
|
|
486
|
-
? `No saved credential matches "${toolArgs}".`
|
|
487
|
-
: 'No active credential to remove. Pass an org name/id, or use --all.');
|
|
488
|
-
process.exit(1);
|
|
489
|
-
}
|
|
490
|
-
const [orgId, profile] = resolved;
|
|
491
|
-
removeProfile(orgId);
|
|
492
|
-
console.error(`Removed the "${profile.org_name}" credential (${orgId}).`);
|
|
493
|
-
return;
|
|
494
|
-
}
|
|
495
|
-
if (subcommand === "web") {
|
|
496
|
-
const { client } = requireClient(flags);
|
|
497
|
-
const { email } = await client.login();
|
|
498
|
-
console.error(`Magic link sent to ${email}. Check your email to access the Lotics web app.`);
|
|
499
|
-
return;
|
|
500
|
-
}
|
|
501
|
-
if (subcommand) {
|
|
502
|
-
console.error(`Unknown auth subcommand: ${subcommand}\n`);
|
|
503
|
-
}
|
|
504
|
-
printAuthHelp();
|
|
505
|
-
return;
|
|
506
|
-
}
|
|
507
|
-
// --- Local file commands — no auth required ---
|
|
508
|
-
if (command === "xlsx") {
|
|
509
|
-
await runXlsxCommand(subcommand, toolArgs, restArgs);
|
|
510
|
-
return;
|
|
511
|
-
}
|
|
512
|
-
if (command === "docx") {
|
|
513
|
-
await runDocxCommand(subcommand, toolArgs, restArgs);
|
|
514
|
-
return;
|
|
515
|
-
}
|
|
516
|
-
if (command === "preview") {
|
|
517
|
-
await runPreviewCommand(subcommand, flags);
|
|
518
|
-
return;
|
|
519
|
-
}
|
|
520
|
-
// --- lotics ui link <component> [--remove] — local vite.config edit, no auth ---
|
|
521
|
-
if (command === "ui") {
|
|
522
|
-
if (subcommand === "link") {
|
|
523
|
-
const component = toolArgs;
|
|
524
|
-
if (!component) {
|
|
525
|
-
console.error("Usage: lotics ui link <component> [--ui-src <abs path>] [--remove]");
|
|
526
|
-
console.error("Dev-links @lotics/ui to packages/ui/src (Vite + tsc) for live HMR + typecheck.");
|
|
527
|
-
console.error("Monorepo apps find packages/ui/src automatically; external apps pass --ui-src / LOTICS_UI_SRC.");
|
|
528
|
-
process.exit(1);
|
|
529
|
-
}
|
|
530
|
-
// `--remove` isn't a value-taking flag, so the parser leaves it as a
|
|
531
|
-
// trailing positional (the component took `toolArgs`); `--ui-src` IS a
|
|
532
|
-
// value flag (flags.uiSrc), falling back to LOTICS_UI_SRC inside appUiLink.
|
|
533
|
-
appUiLink({ component, uiSrc: flags.uiSrc, remove: restArgs.includes("--remove") });
|
|
534
|
-
return;
|
|
535
|
-
}
|
|
536
|
-
console.error(`Unknown ui subcommand: ${subcommand ?? "(none)"}`);
|
|
537
|
-
console.error("Usage: lotics ui link <component> [--ui-src <abs path>] [--remove]");
|
|
538
|
-
process.exit(1);
|
|
539
|
-
}
|
|
540
|
-
// --- lotics app workflow check [alias] — local typecheck, no auth, no network ---
|
|
541
|
-
// Reads src/workflows/*.ts + .lotics/workflows/*.globals.d.ts and the app's own
|
|
542
|
-
// typescript; builds one isolated program per alias. Handled before the auth
|
|
543
|
-
// gate (like `ui link`) since it never touches the API.
|
|
544
|
-
if (command === "app" && subcommand === "workflow" && toolArgs === "check") {
|
|
545
|
-
await appWorkflowCheck({ alias: restArgs[0] });
|
|
546
|
-
return;
|
|
547
|
-
}
|
|
548
|
-
// --- lotics app codegen [path] — regenerate .lotics/* without a deploy ---
|
|
549
|
-
// The .d.ts companions need no auth; app_fields.ts needs a workspace, resolved
|
|
550
|
-
// when credentials are available (offline/unauth still does the .d.ts work).
|
|
551
|
-
if (command === "app" && subcommand === "codegen") {
|
|
552
|
-
const projectDir = toolArgs;
|
|
553
|
-
const ctx = resolveContext(flags);
|
|
554
|
-
if (!ctx) {
|
|
555
|
-
await appCodegen({ projectDir });
|
|
556
|
-
return;
|
|
557
|
-
}
|
|
558
|
-
const viewAsMemberId = flags.viewAs ?? process.env.LOTICS_VIEW_AS;
|
|
559
|
-
const client = new LoticsClient({ apiKey: ctx.apiKey, workspaceId: ctx.workspaceId, viewAsMemberId });
|
|
560
|
-
await resolveWorkspace(client, ctx);
|
|
561
|
-
await appCodegen({ projectDir, client });
|
|
562
|
-
return;
|
|
563
|
-
}
|
|
564
|
-
// --- lotics org [list | use <name|id> [--local]] — credential store, config-only ---
|
|
565
|
-
if (command === "org") {
|
|
566
|
-
const global = loadGlobalConfig() ?? {};
|
|
567
|
-
const profiles = global.profiles ?? {};
|
|
568
|
-
const local = loadLocalConfig();
|
|
569
|
-
if (subcommand === "use") {
|
|
570
|
-
if (!toolArgs) {
|
|
571
|
-
console.error("Usage: lotics org use <name|id> [--local]");
|
|
572
|
-
process.exit(1);
|
|
573
|
-
}
|
|
574
|
-
const resolved = resolveProfileByNameOrId(profiles, toolArgs);
|
|
575
|
-
if (!resolved) {
|
|
576
|
-
console.error(`No saved credential for "${toolArgs}". Run "lotics auth api-key <key>" while authenticated to that org, or "lotics org" to list.`);
|
|
577
|
-
process.exit(1);
|
|
578
|
-
}
|
|
579
|
-
const [orgId, profile] = resolved;
|
|
580
|
-
if (flags.local) {
|
|
581
|
-
setActiveOrg(orgId, "local");
|
|
582
|
-
console.error(`Pinned this directory to ${profile.org_name} (${orgId}).`);
|
|
583
|
-
}
|
|
584
|
-
else {
|
|
585
|
-
setActiveOrg(orgId, "global");
|
|
586
|
-
console.error(`Switched active org to ${profile.org_name} (${orgId}).`);
|
|
587
|
-
if (local?.active_org) {
|
|
588
|
-
console.error("Note: a local pin (.lotics/config.json) overrides the global default in this directory. Use --local to change the pin here.");
|
|
589
|
-
}
|
|
590
|
-
}
|
|
591
|
-
return;
|
|
592
|
-
}
|
|
593
|
-
if (subcommand && subcommand !== "list") {
|
|
594
|
-
console.error(`Unknown org subcommand: ${subcommand}`);
|
|
595
|
-
console.error("Usage: lotics org [list | use <name|id> [--local]]");
|
|
596
|
-
process.exit(1);
|
|
597
|
-
}
|
|
598
|
-
const orgIds = Object.keys(profiles);
|
|
599
|
-
if (orgIds.length === 0) {
|
|
600
|
-
console.error('No saved orgs. Run "lotics auth api-key <key>" to add one.');
|
|
601
|
-
return;
|
|
602
|
-
}
|
|
603
|
-
// The active org in THIS directory: a local pin wins over the global default.
|
|
604
|
-
const effectiveActive = local?.active_org ?? global.active_org;
|
|
605
|
-
if (flags.json) {
|
|
606
|
-
console.log(JSON.stringify(orgIds.map((id) => ({
|
|
607
|
-
org_id: id,
|
|
608
|
-
org_name: profiles[id].org_name,
|
|
609
|
-
workspace_id: profiles[id].workspace_id ?? null,
|
|
610
|
-
active: id === effectiveActive,
|
|
611
|
-
})), null, 2));
|
|
612
|
-
}
|
|
613
|
-
else {
|
|
614
|
-
for (const id of orgIds) {
|
|
615
|
-
const marker = id === effectiveActive ? " (active)" : "";
|
|
616
|
-
console.log(`${id} ${profiles[id].org_name}${marker}`);
|
|
617
|
-
}
|
|
618
|
-
}
|
|
619
|
-
return;
|
|
620
|
-
}
|
|
621
|
-
// --- Validate command before auth ---
|
|
622
|
-
if (command !== "tools" &&
|
|
623
|
-
command !== "upload" &&
|
|
624
|
-
command !== "run" &&
|
|
625
|
-
command !== "download" &&
|
|
626
|
-
command !== "workspace" &&
|
|
627
|
-
command !== "app") {
|
|
628
|
-
console.error(`Unknown command: ${command}`);
|
|
629
|
-
console.error('Run "lotics --help" for usage.');
|
|
630
|
-
process.exit(1);
|
|
631
|
-
}
|
|
632
|
-
// `ui` and `app codegen` are handled above (no-auth / optional-client) and
|
|
633
|
-
// return before this guard — they never reach the requireClient path.
|
|
634
|
-
if (command === "app" && !subcommand) {
|
|
635
|
-
console.error("Usage:");
|
|
636
|
-
console.error(" lotics app create <name> [path] Scaffold a new app locally");
|
|
637
|
-
console.error(" lotics app pull <app_id> [path] Pull an existing app for local editing");
|
|
638
|
-
console.error(" lotics app deploy -m <message> Build + upload the current directory (-m required)");
|
|
639
|
-
console.error(" lotics app versions [app_id] Show deploy history (version, when, who, message)");
|
|
640
|
-
console.error(" lotics app codegen [path] Regenerate .lotics/* (types + field ids) — no deploy");
|
|
641
|
-
console.error(" lotics app workflow run <alias> '<json>' Execute a bound app workflow end-to-end");
|
|
642
|
-
console.error(" lotics app workflow set <alias> Push the edited src/workflows/<alias>.ts body");
|
|
643
|
-
console.error(" lotics app workflow pull Rewrite src/workflows/*.ts from the server");
|
|
644
|
-
console.error(" lotics app workflow check [alias] Typecheck src/workflows bodies locally");
|
|
645
|
-
console.error(" lotics app query set <alias> Push lotics.queries.<alias> to apps.queries (no deploy)");
|
|
646
|
-
console.error(" lotics app subdomain <new-subdomain> Rename the app's public <slug>.lotics.app address");
|
|
647
|
-
console.error(" lotics app rename \"<new name>\" Rename the app's display name (launcher title)");
|
|
648
|
-
console.error(" lotics app dev [path] Run the app locally with HMR + RPC forwarding");
|
|
649
|
-
process.exit(1);
|
|
650
|
-
}
|
|
651
|
-
if (command === "run" && !subcommand) {
|
|
652
|
-
console.error("Usage: lotics run <tool> '<json_args>'\n" +
|
|
653
|
-
" lotics run <tool> @args.json (read args from a file — for large payloads)\n" +
|
|
654
|
-
" cat args.json | lotics run <tool> (read args from stdin)");
|
|
655
|
-
console.error('Run "lotics tools" to see available tools.');
|
|
656
|
-
process.exit(1);
|
|
657
|
-
}
|
|
658
|
-
if (command === "upload" && !subcommand) {
|
|
659
|
-
console.error('Usage: lotics upload <file|dir...> [--as <name>]');
|
|
660
|
-
console.error('Uploads files and returns their file_ids. Directories expand to their immediate files.');
|
|
661
|
-
process.exit(1);
|
|
662
|
-
}
|
|
663
|
-
if (command === "download" && !subcommand) {
|
|
664
|
-
console.error('Usage:');
|
|
665
|
-
console.error(' lotics download <file_id> [-o <dir>]');
|
|
666
|
-
console.error(' lotics download record <record_id> <field_key> [-o <dir>]');
|
|
667
|
-
console.error('File IDs come from upload results or generate_* tool output (--json).');
|
|
668
|
-
process.exit(1);
|
|
669
|
-
}
|
|
670
|
-
if (command === "download" && subcommand === "record" && (!toolArgs || restArgs.length === 0)) {
|
|
671
|
-
console.error('Usage: lotics download record <record_id> <field_key> [-o <dir>]');
|
|
672
|
-
console.error('Downloads every file on the given file field into the output dir.');
|
|
673
|
-
process.exit(1);
|
|
674
|
-
}
|
|
675
|
-
const { client, ctx } = requireClient(flags);
|
|
676
|
-
// lotics workspace / lotics workspace list / lotics workspace select <id>
|
|
677
|
-
if (command === "workspace") {
|
|
678
|
-
if (subcommand === "doctor") {
|
|
679
|
-
// The workspace command family runs before the global workspace
|
|
680
|
-
// resolution — doctor needs the same first-workspace fallback every
|
|
681
|
-
// data command gets.
|
|
682
|
-
await resolveWorkspace(client, ctx);
|
|
683
|
-
const dangling = await client.getWorkspaceDanglingReferences();
|
|
684
|
-
if (dangling.length === 0) {
|
|
685
|
-
console.error("Workspace references: healthy — every referenced schema id resolves.");
|
|
686
|
-
return;
|
|
687
|
-
}
|
|
688
|
-
console.error(`Dangling schema references (${dangling.length}):`);
|
|
689
|
-
for (const d of dangling) {
|
|
690
|
-
console.error(` - ${d.referent.kind} "${d.referent.name}" (${d.referent.id}) → ${d.namespace} ${d.id} (missing)`);
|
|
691
|
-
}
|
|
692
|
-
console.error("Each referent points at schema that no longer exists — edit or archive it, or recreate the missing schema.");
|
|
693
|
-
process.exitCode = 1;
|
|
694
|
-
return;
|
|
695
|
-
}
|
|
696
|
-
const workspaces = await client.listWorkspaces();
|
|
697
|
-
const currentWorkspaceId = ctx.workspaceId;
|
|
698
|
-
if (subcommand === "select") {
|
|
699
|
-
const targetId = toolArgs;
|
|
700
|
-
if (!targetId) {
|
|
701
|
-
console.error('Usage: lotics workspace select <workspace_id>');
|
|
702
|
-
process.exit(1);
|
|
703
|
-
}
|
|
704
|
-
const target = workspaces.find((ws) => ws.id === targetId);
|
|
705
|
-
if (!target) {
|
|
706
|
-
console.error(`Workspace not found: ${targetId}\n\nAvailable workspaces:`);
|
|
707
|
-
printWorkspaceList(workspaces, currentWorkspaceId);
|
|
708
|
-
process.exit(1);
|
|
709
|
-
}
|
|
710
|
-
const written = setSelectedWorkspace(target.id);
|
|
711
|
-
if (!written) {
|
|
712
|
-
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.`);
|
|
713
|
-
return;
|
|
714
|
-
}
|
|
715
|
-
const where = written.scope === "local" ? " (pinned to this directory)" : "";
|
|
716
|
-
console.error(`Switched to workspace: ${target.name} (${target.id})${where}`);
|
|
717
|
-
return;
|
|
718
|
-
}
|
|
719
|
-
if (subcommand === "create") {
|
|
720
|
-
const name = toolArgs;
|
|
721
|
-
if (!name) {
|
|
722
|
-
console.error('Usage: lotics workspace create <name> [--timezone <tz>]');
|
|
723
|
-
process.exit(1);
|
|
724
|
-
}
|
|
725
|
-
const timezone = flags.timezone;
|
|
726
|
-
const created = await client.createWorkspace({ name, timezone });
|
|
727
|
-
setSelectedWorkspace(created.id);
|
|
728
|
-
client.setWorkspaceId(created.id);
|
|
729
|
-
if (flags.json) {
|
|
730
|
-
console.log(JSON.stringify(created, null, 2));
|
|
731
|
-
}
|
|
732
|
-
else {
|
|
733
|
-
console.error(`Created workspace: ${created.name} (${created.id})`);
|
|
734
|
-
console.error(`Switched to ${created.id}`);
|
|
735
|
-
}
|
|
736
|
-
return;
|
|
737
|
-
}
|
|
738
|
-
if (subcommand === "delete") {
|
|
739
|
-
const targetId = toolArgs;
|
|
740
|
-
if (!targetId) {
|
|
741
|
-
console.error('Usage: lotics workspace delete <workspace_id> --yes');
|
|
742
|
-
process.exit(1);
|
|
743
|
-
}
|
|
744
|
-
const target = workspaces.find((ws) => ws.id === targetId);
|
|
745
|
-
if (!target) {
|
|
746
|
-
console.error(`Workspace not found: ${targetId}\n\nAvailable workspaces:`);
|
|
747
|
-
printWorkspaceList(workspaces, currentWorkspaceId);
|
|
748
|
-
process.exit(1);
|
|
749
|
-
}
|
|
750
|
-
// Destructive + irreversible-looking to the user — require an explicit
|
|
751
|
-
// confirmation (this CLI is used non-interactively by agents and scripts).
|
|
752
|
-
if (!flags.yes) {
|
|
753
|
-
console.error(`This deletes workspace "${target.name}" (${target.id}) — every table, app, record, and template inside it becomes inaccessible (soft delete: retained and recoverable by an admin).`);
|
|
754
|
-
console.error(`Re-run with --yes to confirm: lotics workspace delete ${target.id} --yes`);
|
|
755
|
-
process.exit(1);
|
|
756
|
-
}
|
|
757
|
-
const deleted = await client.deleteWorkspace(target.id);
|
|
758
|
-
if (flags.json) {
|
|
759
|
-
console.log(JSON.stringify(deleted, null, 2));
|
|
760
|
-
}
|
|
761
|
-
else {
|
|
762
|
-
console.error(`Deleted workspace: ${target.name} (${target.id})`);
|
|
763
|
-
if (currentWorkspaceId === target.id) {
|
|
764
|
-
console.error('This was your selected workspace — select another with "lotics workspace select <id>".');
|
|
765
|
-
}
|
|
766
|
-
}
|
|
767
|
-
return;
|
|
768
|
-
}
|
|
769
|
-
// Default: list workspaces
|
|
770
|
-
if (subcommand && subcommand !== "list") {
|
|
771
|
-
console.error(`Unknown workspace subcommand: ${subcommand}`);
|
|
772
|
-
console.error('Usage: lotics workspace [list | select <id> | create <name> | delete <id> --yes]');
|
|
773
|
-
process.exit(1);
|
|
774
|
-
}
|
|
775
|
-
if (flags.json) {
|
|
776
|
-
console.log(JSON.stringify(workspaces, null, 2));
|
|
777
|
-
}
|
|
778
|
-
else {
|
|
779
|
-
if (workspaces.length === 0) {
|
|
780
|
-
console.error("No workspaces found.");
|
|
781
|
-
}
|
|
782
|
-
else {
|
|
783
|
-
for (const ws of workspaces) {
|
|
784
|
-
const marker = ws.id === currentWorkspaceId ? " (current)" : "";
|
|
785
|
-
console.log(`${ws.id} ${ws.name} ${ws.timezone} ${ws.default_currency}${marker}`);
|
|
786
|
-
}
|
|
787
|
-
}
|
|
788
|
-
}
|
|
789
|
-
if (ctx.orgName)
|
|
790
|
-
console.error(`Org: ${ctx.orgName}`);
|
|
791
|
-
return;
|
|
792
|
-
}
|
|
793
|
-
// Ensure workspace is resolved for all remaining commands
|
|
794
|
-
await resolveWorkspace(client, ctx);
|
|
795
|
-
// lotics app create / pull / deploy
|
|
796
|
-
if (command === "app") {
|
|
797
|
-
if (subcommand === "create") {
|
|
798
|
-
const name = toolArgs;
|
|
799
|
-
if (!name) {
|
|
800
|
-
console.error("Usage: lotics app create <name> [path]");
|
|
801
|
-
process.exit(1);
|
|
802
|
-
}
|
|
803
|
-
const targetPath = restArgs[0];
|
|
804
|
-
await appCreate(client, { name, targetPath });
|
|
805
|
-
return;
|
|
806
|
-
}
|
|
807
|
-
if (subcommand === "pull") {
|
|
808
|
-
const appId = toolArgs;
|
|
809
|
-
if (!appId) {
|
|
810
|
-
console.error("Usage: lotics app pull <app_id> [path]");
|
|
811
|
-
process.exit(1);
|
|
812
|
-
}
|
|
813
|
-
const targetPath = restArgs[0];
|
|
814
|
-
await appPull(client, { app_id: appId, targetPath });
|
|
815
|
-
return;
|
|
816
|
-
}
|
|
817
|
-
if (subcommand === "deploy") {
|
|
818
|
-
// The message is either `-m <message>` or a bare positional arg after `deploy`.
|
|
819
|
-
// Required: every deploy is a version row, and `lotics app versions` reads
|
|
820
|
-
// the message back — a blank one makes the deploy history useless.
|
|
821
|
-
const message = (flags.message ?? toolArgs)?.trim();
|
|
822
|
-
if (!message) {
|
|
823
|
-
console.error('Usage: lotics app deploy -m "<what changed + why>"');
|
|
824
|
-
console.error("A deploy message is required — it's the version's audit trail (see `lotics app versions`).");
|
|
825
|
-
process.exit(1);
|
|
826
|
-
}
|
|
827
|
-
await appDeploy(client, { message });
|
|
828
|
-
return;
|
|
829
|
-
}
|
|
830
|
-
if (subcommand === "subdomain") {
|
|
831
|
-
const newSubdomain = toolArgs;
|
|
832
|
-
if (!newSubdomain) {
|
|
833
|
-
console.error("Usage: lotics app subdomain <new-subdomain>");
|
|
834
|
-
console.error("Renames the app's public address — https://<slug>.lotics.app");
|
|
835
|
-
process.exit(1);
|
|
836
|
-
}
|
|
837
|
-
await appSetSubdomain(client, { subdomain: newSubdomain });
|
|
838
|
-
return;
|
|
839
|
-
}
|
|
840
|
-
if (subcommand === "rename") {
|
|
841
|
-
const newName = toolArgs;
|
|
842
|
-
if (!newName) {
|
|
843
|
-
console.error('Usage: lotics app rename "<new name>"');
|
|
844
|
-
console.error("Changes the app's display name (run inside the app directory).");
|
|
845
|
-
process.exit(1);
|
|
846
|
-
}
|
|
847
|
-
await appRename(client, { name: newName });
|
|
848
|
-
return;
|
|
849
|
-
}
|
|
850
|
-
if (subcommand === "versions") {
|
|
851
|
-
// Optional positional app_id (inspect any app); else read the local manifest.
|
|
852
|
-
const appId = toolArgs || undefined;
|
|
853
|
-
await appVersions(client, { app_id: appId });
|
|
854
|
-
return;
|
|
855
|
-
}
|
|
856
|
-
if (subcommand === "workflow") {
|
|
857
|
-
// `lotics app workflow <run|set|pull> …` — disambiguated subcommands so an
|
|
858
|
-
// alias can never collide with the verb. `toolArgs` is the verb; `restArgs`
|
|
859
|
-
// carries the alias (+ inputs for run). No bare `workflow <alias>` form.
|
|
860
|
-
const action = toolArgs;
|
|
861
|
-
const workflowUsage = () => {
|
|
862
|
-
console.error("Usage:");
|
|
863
|
-
console.error(" lotics app workflow run <alias> '<json>' Execute a bound app workflow");
|
|
864
|
-
console.error(" lotics app workflow set <alias> Push src/workflows/<alias>.ts");
|
|
865
|
-
console.error(" lotics app workflow pull Rewrite src/workflows/*.ts from the server");
|
|
866
|
-
console.error(" lotics app workflow check [alias] Typecheck src/workflows bodies locally");
|
|
867
|
-
process.exit(1);
|
|
868
|
-
};
|
|
869
|
-
if (action === "run") {
|
|
870
|
-
// `run <alias> '<json>'` — inputs use the SAME ingestion as `lotics run`
|
|
871
|
-
// (inline / @file / stdin) so bulk inputs bypass ARG_MAX.
|
|
872
|
-
const alias = restArgs[0];
|
|
873
|
-
if (!alias) {
|
|
874
|
-
console.error("Usage: lotics app workflow run <alias> '<json>'");
|
|
875
|
-
console.error(" lotics app workflow run <alias> @inputs.json (read inputs from a file)");
|
|
876
|
-
console.error(" cat inputs.json | lotics app workflow run <alias> (read inputs from stdin)");
|
|
877
|
-
console.error("Flags: --print-created (report created records + cleanup plan + caveat)");
|
|
878
|
-
console.error(" --cleanup (also delete the created records — records only, NOT a rollback)");
|
|
879
|
-
process.exit(1);
|
|
880
|
-
}
|
|
881
|
-
const ingested = await ingestJsonArgs({
|
|
882
|
-
rawArg: restArgs[1],
|
|
883
|
-
stdinIsTTY: process.stdin.isTTY ?? false,
|
|
884
|
-
readFile: (p) => fs.readFileSync(p, "utf-8"),
|
|
885
|
-
readStdin,
|
|
886
|
-
});
|
|
887
|
-
if (ingested.kind === "error") {
|
|
888
|
-
console.error(ingested.message);
|
|
889
|
-
process.exit(1);
|
|
890
|
-
}
|
|
891
|
-
await appExecuteWorkflow(client, {
|
|
892
|
-
alias,
|
|
893
|
-
inputs: ingested.args,
|
|
894
|
-
printCreated: flags.printCreated,
|
|
895
|
-
cleanup: flags.cleanup,
|
|
896
|
-
});
|
|
897
|
-
return;
|
|
898
|
-
}
|
|
899
|
-
if (action === "set") {
|
|
900
|
-
const alias = restArgs[0];
|
|
901
|
-
if (!alias) {
|
|
902
|
-
console.error("Usage: lotics app workflow set <alias>");
|
|
903
|
-
console.error("Pushes the edited src/workflows/<alias>.ts body via set_app_workflow.");
|
|
904
|
-
process.exit(1);
|
|
905
|
-
}
|
|
906
|
-
await appWorkflowSet(client, { alias });
|
|
907
|
-
return;
|
|
908
|
-
}
|
|
909
|
-
if (action === "pull") {
|
|
910
|
-
await appWorkflowPull(client);
|
|
911
|
-
return;
|
|
912
|
-
}
|
|
913
|
-
workflowUsage();
|
|
914
|
-
}
|
|
915
|
-
if (subcommand === "query") {
|
|
916
|
-
// `lotics app query set <alias>` — push package.json#lotics.queries.<alias>
|
|
917
|
-
// to apps.queries via set_app_query, no deploy. `toolArgs` is the verb;
|
|
918
|
-
// `restArgs` carries the alias.
|
|
919
|
-
const action = toolArgs;
|
|
920
|
-
if (action === "set") {
|
|
921
|
-
const alias = restArgs[0];
|
|
922
|
-
if (!alias) {
|
|
923
|
-
console.error("Usage: lotics app query set <alias>");
|
|
924
|
-
console.error("Pushes package.json#lotics.queries.<alias> to apps.queries via set_app_query (no deploy).");
|
|
925
|
-
process.exit(1);
|
|
926
|
-
}
|
|
927
|
-
await appQuerySet(client, { alias });
|
|
928
|
-
return;
|
|
929
|
-
}
|
|
930
|
-
console.error("Usage:");
|
|
931
|
-
console.error(" lotics app query set <alias> Push package.json#lotics.queries.<alias> to apps.queries (no deploy)");
|
|
932
|
-
process.exit(1);
|
|
933
|
-
}
|
|
934
|
-
if (subcommand === "dev") {
|
|
935
|
-
// First positional is an optional project path (defaults to cwd).
|
|
936
|
-
// --port and --vite-port can override the wrapper / Vite ports.
|
|
937
|
-
const projectDir = toolArgs;
|
|
938
|
-
let port;
|
|
939
|
-
let vitePort;
|
|
940
|
-
for (const a of restArgs) {
|
|
941
|
-
const portMatch = /^--port=(\d+)$/.exec(a);
|
|
942
|
-
const vitePortMatch = /^--vite-port=(\d+)$/.exec(a);
|
|
943
|
-
if (portMatch)
|
|
944
|
-
port = Number(portMatch[1]);
|
|
945
|
-
else if (vitePortMatch)
|
|
946
|
-
vitePort = Number(vitePortMatch[1]);
|
|
947
|
-
}
|
|
948
|
-
await appDev(client, { projectDir, port, vitePort });
|
|
949
|
-
return;
|
|
950
|
-
}
|
|
951
|
-
console.error(`Unknown app subcommand: ${subcommand}`);
|
|
952
|
-
console.error("Run 'lotics app' for usage.");
|
|
953
|
-
process.exit(1);
|
|
954
|
-
}
|
|
955
|
-
// lotics tools / lotics tools <name>
|
|
956
|
-
if (command === "tools") {
|
|
957
|
-
if (subcommand) {
|
|
958
|
-
const info = await client.getTool(subcommand);
|
|
959
|
-
if (flags.json) {
|
|
960
|
-
console.log(JSON.stringify(info, null, 2));
|
|
961
|
-
}
|
|
962
|
-
else {
|
|
963
|
-
console.log(`${info.name}\n\n${info.description}\n\nInput schema:\n${JSON.stringify(info.input_schema, null, 2)}`);
|
|
964
|
-
}
|
|
965
|
-
}
|
|
966
|
-
else {
|
|
967
|
-
const { categories } = await client.listTools();
|
|
968
|
-
if (flags.json) {
|
|
969
|
-
console.log(JSON.stringify(categories, null, 2));
|
|
970
|
-
}
|
|
971
|
-
else {
|
|
972
|
-
for (const [category, { description, tools }] of Object.entries(categories)) {
|
|
973
|
-
console.log(description ? `\n## ${category} — ${description}` : `\n## ${category}`);
|
|
974
|
-
console.log(` ${tools.join(", ")}`);
|
|
975
|
-
}
|
|
976
|
-
console.log("");
|
|
977
|
-
}
|
|
978
|
-
}
|
|
979
|
-
return;
|
|
980
|
-
}
|
|
981
|
-
// lotics upload <file|dir...> [--as <name>]
|
|
982
|
-
if (command === "upload") {
|
|
983
|
-
const rawPaths = [subcommand, ...(toolArgs ? [toolArgs] : []), ...restArgs];
|
|
984
|
-
const filePaths = resolveUploadPaths(rawPaths);
|
|
985
|
-
if (filePaths.length === 0) {
|
|
986
|
-
console.error("No files found in the specified paths.");
|
|
987
|
-
process.exit(1);
|
|
988
|
-
}
|
|
989
|
-
if (flags.as && filePaths.length > 1) {
|
|
990
|
-
console.error("Cannot use --as with multiple files.");
|
|
991
|
-
process.exit(1);
|
|
992
|
-
}
|
|
993
|
-
const upload = await client.uploadFiles(filePaths, {
|
|
994
|
-
filenames: flags.as ? [flags.as] : undefined,
|
|
995
|
-
});
|
|
996
|
-
if (upload.files.length === 0 && upload.errors.length > 0) {
|
|
997
|
-
console.error(`Upload failed: ${upload.errors.map((e) => `${e.filename}: ${e.error}`).join(", ")}`);
|
|
998
|
-
process.exit(1);
|
|
999
|
-
}
|
|
1000
|
-
if (flags.json) {
|
|
1001
|
-
console.log(JSON.stringify({ files: upload.files, errors: upload.errors }, null, 2));
|
|
1002
|
-
}
|
|
1003
|
-
else {
|
|
1004
|
-
for (let i = 0; i < upload.files.length; i++) {
|
|
1005
|
-
const file = upload.files[i];
|
|
1006
|
-
console.log(`[${i + 1}] ${file.id} ${file.filename} ${file.mime_type}`);
|
|
1007
|
-
}
|
|
1008
|
-
for (const error of upload.errors) {
|
|
1009
|
-
console.error(`FAILED ${error.filename} ${error.error}`);
|
|
1010
|
-
}
|
|
1011
|
-
}
|
|
1012
|
-
return;
|
|
1013
|
-
}
|
|
1014
|
-
// lotics download <file_id> [-o <path>]
|
|
1015
|
-
// lotics download record <record_id> <field_key> [-o <path>]
|
|
1016
|
-
if (command === "download") {
|
|
1017
|
-
if (subcommand === "record") {
|
|
1018
|
-
const recordId = toolArgs;
|
|
1019
|
-
const fieldKey = restArgs[0];
|
|
1020
|
-
const files = await client.downloadRecordFiles(recordId, fieldKey, flags.output);
|
|
1021
|
-
if (flags.json) {
|
|
1022
|
-
console.log(JSON.stringify(files, null, 2));
|
|
1023
|
-
}
|
|
1024
|
-
else {
|
|
1025
|
-
for (const f of files) {
|
|
1026
|
-
console.log(`${f.file_id} ${f.path} ${f.filename}`);
|
|
1027
|
-
}
|
|
1028
|
-
console.error(`Downloaded ${files.length} file${files.length === 1 ? "" : "s"} from ${recordId}.${fieldKey}`);
|
|
1029
|
-
}
|
|
1030
|
-
return;
|
|
1031
|
-
}
|
|
1032
|
-
const { path: filePath, filename } = await client.downloadFileById(subcommand, flags.output);
|
|
1033
|
-
console.error(`Downloaded: ${filePath} (${filename})`);
|
|
1034
|
-
return;
|
|
1035
|
-
}
|
|
1036
|
-
// lotics run <tool> [json_args]
|
|
1037
|
-
if (command === "run") {
|
|
1038
|
-
const toolName = subcommand;
|
|
1039
|
-
// `@file` / piped stdin carry payloads too large for an inline arg (the OS
|
|
1040
|
-
// caps ARG_MAX). Shared with `lotics app workflow` so both ingest identically.
|
|
1041
|
-
const ingested = await ingestJsonArgs({
|
|
1042
|
-
rawArg: toolArgs,
|
|
1043
|
-
stdinIsTTY: process.stdin.isTTY ?? false,
|
|
1044
|
-
readFile: (p) => fs.readFileSync(p, "utf-8"),
|
|
1045
|
-
readStdin,
|
|
1046
|
-
});
|
|
1047
|
-
if (ingested.kind === "error") {
|
|
1048
|
-
console.error(ingested.message);
|
|
1049
|
-
process.exit(1);
|
|
1050
|
-
}
|
|
1051
|
-
const args = ingested.args;
|
|
1052
|
-
const timeoutMs = flags.timeout ?? 60000;
|
|
1053
|
-
// Always request text format so model_output is available; --json only affects CLI output
|
|
1054
|
-
const result = await client.execute(toolName, args, { format: "text", timeoutMs });
|
|
1055
|
-
if (result.error) {
|
|
1056
|
-
if (flags.json) {
|
|
1057
|
-
console.log(JSON.stringify({ error: result.error }, null, 2));
|
|
1058
|
-
}
|
|
1059
|
-
else {
|
|
1060
|
-
// Execution errors (not-found, refusals) carry the server's message;
|
|
1061
|
-
// the input already passed schema validation, so no schema hint —
|
|
1062
|
-
// validation 400s bring the server's own hint via throwResponseError.
|
|
1063
|
-
console.error(result.error);
|
|
1064
|
-
}
|
|
1065
|
-
process.exit(1);
|
|
1066
|
-
}
|
|
1067
|
-
if (flags.json) {
|
|
1068
|
-
console.log(JSON.stringify(result.result, null, 2));
|
|
1069
|
-
}
|
|
1070
|
-
else if (result.model_output) {
|
|
1071
|
-
console.log(result.model_output);
|
|
1072
|
-
}
|
|
1073
|
-
else {
|
|
1074
|
-
// Fallback: compact JSON when no text output is available
|
|
1075
|
-
console.log(JSON.stringify(result.result, null, 2));
|
|
1076
|
-
}
|
|
1077
|
-
return;
|
|
1078
|
-
}
|
|
1079
|
-
}
|
|
1080
|
-
main()
|
|
1081
|
-
.then(() => checkForUpdate(VERSION))
|
|
1082
|
-
.catch((error) => {
|
|
1083
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
1084
|
-
if (message.includes("fetch failed") || message.includes("ECONNREFUSED")) {
|
|
1085
|
-
console.error("Could not connect to the Lotics API.");
|
|
1086
|
-
}
|
|
1087
|
-
else if (message.includes("ENOENT")) {
|
|
1088
|
-
const pathMatch = message.match(/open '([^']+)'/);
|
|
1089
|
-
console.error(`File not found: ${pathMatch?.[1] ?? message}`);
|
|
1090
|
-
}
|
|
1091
|
-
else {
|
|
1092
|
-
console.error(message);
|
|
1093
|
-
}
|
|
1094
|
-
process.exit(1);
|
|
1095
|
-
});
|