lindoai-cli 1.0.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/dist/index.js ADDED
@@ -0,0 +1,2875 @@
1
+ #!/usr/bin/env node
2
+ import * as http from 'http';
3
+ import * as fs3 from 'fs';
4
+ import * as path3 from 'path';
5
+ import * as os2 from 'os';
6
+ import { platform } from 'os';
7
+ import { Command } from 'commander';
8
+ import { AuthenticationError, LindoClient } from 'lindoai';
9
+ import { spawn, exec, execSync } from 'child_process';
10
+ import { URL } from 'url';
11
+ import * as crypto from 'crypto';
12
+
13
+ var __defProp = Object.defineProperty;
14
+ var __getOwnPropNames = Object.getOwnPropertyNames;
15
+ var __esm = (fn, res) => function __init() {
16
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
17
+ };
18
+ var __export = (target, all) => {
19
+ for (var name in all)
20
+ __defProp(target, name, { get: all[name], enumerable: true });
21
+ };
22
+
23
+ // src/server/live-preview-server.ts
24
+ var live_preview_server_exports = {};
25
+ __export(live_preview_server_exports, {
26
+ LIVE_RELOAD_SCRIPT: () => LIVE_RELOAD_SCRIPT,
27
+ injectLiveReload: () => injectLiveReload,
28
+ startLivePreviewServer: () => startLivePreviewServer
29
+ });
30
+ function injectLiveReload(html) {
31
+ const bodyCloseTagRegex = /<\/body>/i;
32
+ const match = html.match(bodyCloseTagRegex);
33
+ if (match && match.index !== void 0) {
34
+ return html.slice(0, match.index) + LIVE_RELOAD_SCRIPT + html.slice(match.index);
35
+ }
36
+ return html + LIVE_RELOAD_SCRIPT;
37
+ }
38
+ function startLivePreviewServer(filePath) {
39
+ return new Promise((resolve4, reject) => {
40
+ const absolutePath = path3.resolve(filePath);
41
+ const sseClients = /* @__PURE__ */ new Set();
42
+ let debounceTimer = null;
43
+ const DEBOUNCE_MS = 100;
44
+ function notifyClients() {
45
+ for (const client of sseClients) {
46
+ try {
47
+ client.write("data: reload\n\n");
48
+ } catch {
49
+ sseClients.delete(client);
50
+ }
51
+ }
52
+ }
53
+ function handleFileChange() {
54
+ if (debounceTimer) {
55
+ clearTimeout(debounceTimer);
56
+ }
57
+ debounceTimer = setTimeout(() => {
58
+ notifyClients();
59
+ debounceTimer = null;
60
+ }, DEBOUNCE_MS);
61
+ }
62
+ function handleRequest(req, res) {
63
+ const url = req.url || "/";
64
+ const method = req.method || "GET";
65
+ if (method !== "GET") {
66
+ res.writeHead(405, { "Content-Type": "text/plain" });
67
+ res.end("Method Not Allowed");
68
+ return;
69
+ }
70
+ if (url === "/__live-reload") {
71
+ res.writeHead(200, {
72
+ "Content-Type": "text/event-stream",
73
+ "Cache-Control": "no-cache",
74
+ "Connection": "keep-alive",
75
+ "Access-Control-Allow-Origin": "*"
76
+ });
77
+ res.write("data: connected\n\n");
78
+ sseClients.add(res);
79
+ req.on("close", () => {
80
+ sseClients.delete(res);
81
+ });
82
+ return;
83
+ }
84
+ if (url === "/" || url === "/index.html") {
85
+ try {
86
+ const html = fs3.readFileSync(absolutePath, "utf-8");
87
+ const injectedHtml = injectLiveReload(html);
88
+ res.writeHead(200, {
89
+ "Content-Type": "text/html; charset=utf-8",
90
+ "Cache-Control": "no-cache"
91
+ });
92
+ res.end(injectedHtml);
93
+ } catch (err) {
94
+ const errorMessage = err instanceof Error ? err.message : "Unknown error";
95
+ res.writeHead(500, { "Content-Type": "text/html; charset=utf-8" });
96
+ res.end(`<!DOCTYPE html>
97
+ <html>
98
+ <head><title>Error</title></head>
99
+ <body>
100
+ <h1>Error loading file</h1>
101
+ <p>${errorMessage}</p>
102
+ </body>
103
+ </html>`);
104
+ }
105
+ return;
106
+ }
107
+ res.writeHead(404, { "Content-Type": "text/html; charset=utf-8" });
108
+ res.end(`<!DOCTYPE html>
109
+ <html>
110
+ <head><title>Not Found</title></head>
111
+ <body>
112
+ <h1>404 Not Found</h1>
113
+ <p>The requested resource was not found.</p>
114
+ </body>
115
+ </html>`);
116
+ }
117
+ const server = http.createServer(handleRequest);
118
+ let watcher = null;
119
+ server.on("error", (err) => {
120
+ reject(new Error(`Failed to start preview server: ${err.message}`));
121
+ });
122
+ server.listen(0, "127.0.0.1", () => {
123
+ const address = server.address();
124
+ if (!address || typeof address === "string") {
125
+ reject(new Error("Failed to get server address"));
126
+ return;
127
+ }
128
+ const port = address.port;
129
+ try {
130
+ watcher = fs3.watch(absolutePath, (eventType) => {
131
+ if (eventType === "change") {
132
+ handleFileChange();
133
+ }
134
+ });
135
+ watcher.on("error", (err) => {
136
+ console.error(`[Live Preview] File watcher error: ${err.message}`);
137
+ });
138
+ } catch (err) {
139
+ const errorMessage = err instanceof Error ? err.message : "Unknown error";
140
+ console.error(`[Live Preview] Failed to watch file: ${errorMessage}`);
141
+ }
142
+ const handleShutdown = () => {
143
+ if (watcher) {
144
+ watcher.close();
145
+ watcher = null;
146
+ }
147
+ if (debounceTimer) {
148
+ clearTimeout(debounceTimer);
149
+ debounceTimer = null;
150
+ }
151
+ for (const client of sseClients) {
152
+ try {
153
+ client.end();
154
+ } catch {
155
+ }
156
+ }
157
+ sseClients.clear();
158
+ const pidFilePath = path3.join(os2.tmpdir(), "lindoai-pages-preview.pid");
159
+ const portFilePath = path3.join(os2.tmpdir(), "lindoai-pages-preview.port");
160
+ try {
161
+ if (fs3.existsSync(pidFilePath)) {
162
+ fs3.unlinkSync(pidFilePath);
163
+ }
164
+ } catch {
165
+ }
166
+ try {
167
+ if (fs3.existsSync(portFilePath)) {
168
+ fs3.unlinkSync(portFilePath);
169
+ }
170
+ } catch {
171
+ }
172
+ server.close(() => {
173
+ process.exit(0);
174
+ });
175
+ setTimeout(() => {
176
+ process.exit(0);
177
+ }, 1e3);
178
+ };
179
+ process.on("SIGTERM", handleShutdown);
180
+ process.on("SIGINT", handleShutdown);
181
+ resolve4(port);
182
+ });
183
+ });
184
+ }
185
+ var LIVE_RELOAD_SCRIPT;
186
+ var init_live_preview_server = __esm({
187
+ "src/server/live-preview-server.ts"() {
188
+ LIVE_RELOAD_SCRIPT = `<script>
189
+ (function() {
190
+ var eventSource = new EventSource('/__live-reload');
191
+ eventSource.onmessage = function(event) {
192
+ if (event.data === 'reload') {
193
+ window.location.reload();
194
+ }
195
+ };
196
+ eventSource.onerror = function() {
197
+ console.log('[Live Reload] Connection lost, attempting to reconnect...');
198
+ };
199
+ })();
200
+ </script>`;
201
+ }
202
+ });
203
+ var ENV_API_KEY = "LINDO_API_KEY";
204
+ var ENV_BASE_URL = "LINDO_BASE_URL";
205
+ var CONFIG_DIR = ".lindo";
206
+ var CONFIG_FILE = "config.json";
207
+ var DEFAULT_BASE_URL = "https://api.lindo.ai";
208
+ function getConfigDir() {
209
+ return path3.join(os2.homedir(), CONFIG_DIR);
210
+ }
211
+ function getConfigPath() {
212
+ return path3.join(getConfigDir(), CONFIG_FILE);
213
+ }
214
+ function readConfigFile() {
215
+ const configPath = getConfigPath();
216
+ try {
217
+ if (fs3.existsSync(configPath)) {
218
+ const content = fs3.readFileSync(configPath, "utf-8");
219
+ return JSON.parse(content);
220
+ }
221
+ } catch {
222
+ }
223
+ return {};
224
+ }
225
+ function writeConfigFile(config) {
226
+ const configDir = getConfigDir();
227
+ const configPath = getConfigPath();
228
+ if (!fs3.existsSync(configDir)) {
229
+ fs3.mkdirSync(configDir, { recursive: true });
230
+ }
231
+ fs3.writeFileSync(configPath, JSON.stringify(config, null, 2), "utf-8");
232
+ }
233
+ function loadConfig() {
234
+ const fileConfig = readConfigFile();
235
+ const apiKey = process.env[ENV_API_KEY] || fileConfig.apiKey;
236
+ const baseUrl = process.env[ENV_BASE_URL] || fileConfig.baseUrl || DEFAULT_BASE_URL;
237
+ return {
238
+ apiKey,
239
+ baseUrl
240
+ };
241
+ }
242
+ function saveApiKey(apiKey) {
243
+ const config = readConfigFile();
244
+ config.apiKey = apiKey;
245
+ writeConfigFile(config);
246
+ }
247
+ function saveConfig(key, value) {
248
+ const config = readConfigFile();
249
+ switch (key) {
250
+ case "apiKey":
251
+ config.apiKey = value;
252
+ break;
253
+ case "baseUrl":
254
+ config.baseUrl = value;
255
+ break;
256
+ default:
257
+ throw new Error(`Unknown configuration key: ${key}`);
258
+ }
259
+ writeConfigFile(config);
260
+ }
261
+ function getConfigValue(key) {
262
+ const config = loadConfig();
263
+ switch (key) {
264
+ case "apiKey":
265
+ return config.apiKey;
266
+ case "baseUrl":
267
+ return config.baseUrl;
268
+ default:
269
+ return void 0;
270
+ }
271
+ }
272
+ function hasApiKey() {
273
+ const config = loadConfig();
274
+ return !!config.apiKey;
275
+ }
276
+
277
+ // src/output.ts
278
+ var colors = {
279
+ reset: "\x1B[0m",
280
+ red: "\x1B[31m",
281
+ green: "\x1B[32m",
282
+ blue: "\x1B[34m",
283
+ cyan: "\x1B[36m",
284
+ gray: "\x1B[90m",
285
+ bold: "\x1B[1m"
286
+ };
287
+ function useColors() {
288
+ return !process.env.NO_COLOR && process.stdout.isTTY !== false;
289
+ }
290
+ function colorize(text, color) {
291
+ if (!useColors()) {
292
+ return text;
293
+ }
294
+ return `${color}${text}${colors.reset}`;
295
+ }
296
+ function success(message) {
297
+ console.log(colorize(`\u2713 ${message}`, colors.green));
298
+ }
299
+ function error(message) {
300
+ console.error(colorize(`\u2717 ${message}`, colors.red));
301
+ }
302
+ function info(message) {
303
+ console.log(colorize(`\u2139 ${message}`, colors.blue));
304
+ }
305
+ function formatJson(data) {
306
+ return JSON.stringify(data, null, 2);
307
+ }
308
+ function formatTable(data) {
309
+ if (data === null || data === void 0) {
310
+ return "";
311
+ }
312
+ if (Array.isArray(data)) {
313
+ if (data.length === 0) {
314
+ return "No data";
315
+ }
316
+ const keys = /* @__PURE__ */ new Set();
317
+ for (const item of data) {
318
+ if (typeof item === "object" && item !== null) {
319
+ Object.keys(item).forEach((key) => keys.add(key));
320
+ }
321
+ }
322
+ if (keys.size === 0) {
323
+ return data.map((item) => String(item)).join("\n");
324
+ }
325
+ const columns = Array.from(keys);
326
+ return formatTableFromRows(columns, data);
327
+ }
328
+ if (typeof data === "object") {
329
+ const obj = data;
330
+ const entries = Object.entries(obj);
331
+ if (entries.length === 0) {
332
+ return "No data";
333
+ }
334
+ const maxKeyLength = Math.max(...entries.map(([key]) => key.length));
335
+ return entries.map(([key, value]) => {
336
+ const paddedKey = key.padEnd(maxKeyLength);
337
+ const formattedValue = formatValue(value);
338
+ return `${colorize(paddedKey, colors.cyan)} ${formattedValue}`;
339
+ }).join("\n");
340
+ }
341
+ return String(data);
342
+ }
343
+ function formatTableFromRows(columns, rows) {
344
+ const widths = {};
345
+ for (const col of columns) {
346
+ widths[col] = col.length;
347
+ }
348
+ for (const row of rows) {
349
+ if (typeof row === "object" && row !== null) {
350
+ const obj = row;
351
+ for (const col of columns) {
352
+ const value = formatValue(obj[col]);
353
+ widths[col] = Math.max(widths[col], value.length);
354
+ }
355
+ }
356
+ }
357
+ const header = columns.map((col) => colorize(col.padEnd(widths[col]), colors.bold)).join(" ");
358
+ const separator = columns.map((col) => "-".repeat(widths[col])).join(" ");
359
+ const dataRows = rows.map((row) => {
360
+ if (typeof row === "object" && row !== null) {
361
+ const obj = row;
362
+ return columns.map((col) => formatValue(obj[col]).padEnd(widths[col])).join(" ");
363
+ }
364
+ return String(row);
365
+ });
366
+ return [header, separator, ...dataRows].join("\n");
367
+ }
368
+ function formatValue(value) {
369
+ if (value === null || value === void 0) {
370
+ return colorize("-", colors.gray);
371
+ }
372
+ if (typeof value === "boolean") {
373
+ return value ? colorize("true", colors.green) : colorize("false", colors.red);
374
+ }
375
+ if (typeof value === "number") {
376
+ return String(value);
377
+ }
378
+ if (typeof value === "object") {
379
+ if (Array.isArray(value)) {
380
+ return `[${value.length} items]`;
381
+ }
382
+ return JSON.stringify(value);
383
+ }
384
+ return String(value);
385
+ }
386
+ function output(data, format) {
387
+ if (format === "json") {
388
+ console.log(formatJson(data));
389
+ } else {
390
+ console.log(formatTable(data));
391
+ }
392
+ }
393
+
394
+ // src/commands/config.ts
395
+ var VALID_KEYS = ["apiKey", "baseUrl"];
396
+ function createConfigCommand() {
397
+ const config = new Command("config").description("Manage CLI configuration");
398
+ config.command("set <key> <value>").description("Set a configuration value").action((key, value) => {
399
+ if (!VALID_KEYS.includes(key)) {
400
+ error(`Invalid configuration key: ${key}`);
401
+ info(`Valid keys: ${VALID_KEYS.join(", ")}`);
402
+ process.exit(1);
403
+ }
404
+ try {
405
+ saveConfig(key, value);
406
+ success(`Configuration saved: ${key}`);
407
+ info(`Config file: ${getConfigPath()}`);
408
+ } catch (err) {
409
+ error(`Failed to save configuration: ${err instanceof Error ? err.message : String(err)}`);
410
+ process.exit(1);
411
+ }
412
+ });
413
+ config.command("get <key>").description("Get a configuration value").option("-f, --format <format>", "Output format (json, table)", "table").action((key, options) => {
414
+ const value = getConfigValue(key);
415
+ if (value === void 0) {
416
+ if (options.format === "json") {
417
+ output({ key, value: null }, options.format);
418
+ } else {
419
+ info(`Configuration key '${key}' is not set`);
420
+ }
421
+ return;
422
+ }
423
+ if (options.format === "json") {
424
+ output({ key, value }, options.format);
425
+ } else {
426
+ const displayValue = key === "apiKey" ? maskApiKey(value) : value;
427
+ console.log(`${key}: ${displayValue}`);
428
+ }
429
+ });
430
+ config.command("list").description("List all configuration values").option("-f, --format <format>", "Output format (json, table)", "table").action((options) => {
431
+ const resolvedConfig = loadConfig();
432
+ const configData = {
433
+ apiKey: resolvedConfig.apiKey ? maskApiKey(resolvedConfig.apiKey) : "(not set)",
434
+ baseUrl: resolvedConfig.baseUrl,
435
+ configFile: getConfigPath()
436
+ };
437
+ if (options.format === "json") {
438
+ output(
439
+ {
440
+ apiKey: resolvedConfig.apiKey ? maskApiKey(resolvedConfig.apiKey) : null,
441
+ baseUrl: resolvedConfig.baseUrl,
442
+ configFile: getConfigPath()
443
+ },
444
+ options.format
445
+ );
446
+ } else {
447
+ output(configData, options.format);
448
+ }
449
+ });
450
+ config.command("path").description("Show the config file path").action(() => {
451
+ console.log(getConfigPath());
452
+ });
453
+ return config;
454
+ }
455
+ function maskApiKey(apiKey) {
456
+ if (apiKey.length <= 8) {
457
+ return "*".repeat(apiKey.length);
458
+ }
459
+ return `${apiKey.slice(0, 4)}${"*".repeat(apiKey.length - 8)}${apiKey.slice(-4)}`;
460
+ }
461
+ function createAgentsCommand() {
462
+ const agents = new Command("agents").description("Run AI agents");
463
+ agents.command("run <agent-id>").description("Run an AI agent").option("-i, --input <json>", "Input data as JSON string", "{}").option("-s, --stream", "Stream the response", false).option("-f, --format <format>", "Output format (json, table)", "table").action(async (agentId, options) => {
464
+ if (!hasApiKey()) {
465
+ error("API key not configured");
466
+ info("Run: lindo config set apiKey <your-api-key>");
467
+ info("Or set the LINDO_API_KEY environment variable");
468
+ process.exit(1);
469
+ }
470
+ const config = loadConfig();
471
+ const client = new LindoClient({
472
+ apiKey: config.apiKey,
473
+ baseUrl: config.baseUrl
474
+ });
475
+ let input;
476
+ try {
477
+ input = JSON.parse(options.input);
478
+ } catch {
479
+ error("Invalid JSON input");
480
+ info(`Example: --input '{"prompt": "Hello!"}'`);
481
+ process.exit(1);
482
+ }
483
+ try {
484
+ info(`Running agent: ${agentId}`);
485
+ const result = await client.agents.run({
486
+ agent_id: agentId,
487
+ input,
488
+ stream: options.stream
489
+ });
490
+ if (result.success) {
491
+ success("Agent run completed");
492
+ output(result, options.format);
493
+ } else {
494
+ error(`Agent run failed: ${result.error || "Unknown error"}`);
495
+ output(result, options.format);
496
+ process.exit(1);
497
+ }
498
+ } catch (err) {
499
+ handleError(err);
500
+ }
501
+ });
502
+ return agents;
503
+ }
504
+ function handleError(err) {
505
+ if (err instanceof AuthenticationError) {
506
+ error("Authentication failed");
507
+ info("Your API key may be invalid or expired");
508
+ info("Run: lindo config set apiKey <your-api-key>");
509
+ process.exit(1);
510
+ }
511
+ if (err instanceof Error) {
512
+ error(err.message);
513
+ } else {
514
+ error("An unexpected error occurred");
515
+ }
516
+ process.exit(1);
517
+ }
518
+ function createWorkflowsCommand() {
519
+ const workflows = new Command("workflows").description("Manage workflows");
520
+ workflows.command("list").description("List workflow logs").option("-n, --name <name>", "Filter by workflow name").option("-s, --status <status>", "Filter by status").option("-w, --website <id>", "Filter by website ID").option("-c, --client <id>", "Filter by client ID").option("-l, --limit <number>", "Maximum number of results", "50").option("-f, --format <format>", "Output format (json, table)", "table").action(async (options) => {
521
+ const client = getClient();
522
+ try {
523
+ const result = await client.workflows.list({
524
+ workflow_name: options.name,
525
+ status: options.status,
526
+ website_id: options.website,
527
+ client_id: options.client,
528
+ limit: parseInt(options.limit)
529
+ });
530
+ if (result.success) {
531
+ output(result.data, options.format);
532
+ } else {
533
+ error("Failed to list workflows");
534
+ process.exit(1);
535
+ }
536
+ } catch (err) {
537
+ handleError2(err);
538
+ }
539
+ });
540
+ workflows.command("start <workflow-name>").description("Start a workflow").option("-p, --params <json>", "Workflow parameters as JSON string", "{}").option("-f, --format <format>", "Output format (json, table)", "table").action(async (workflowName, options) => {
541
+ const client = getClient();
542
+ let params;
543
+ try {
544
+ params = JSON.parse(options.params);
545
+ } catch {
546
+ error("Invalid JSON params");
547
+ info(`Example: --params '{"page_id": "page-123"}'`);
548
+ process.exit(1);
549
+ }
550
+ try {
551
+ info(`Starting workflow: ${workflowName}`);
552
+ const result = await client.workflows.start({
553
+ workflow_name: workflowName,
554
+ params
555
+ });
556
+ if (result.success) {
557
+ success(`Workflow started: ${result.instance_id}`);
558
+ output(result, options.format);
559
+ } else {
560
+ error("Failed to start workflow");
561
+ output(result, options.format);
562
+ process.exit(1);
563
+ }
564
+ } catch (err) {
565
+ handleError2(err);
566
+ }
567
+ });
568
+ workflows.command("status <instance-id>").description("Get workflow status").option("-f, --format <format>", "Output format (json, table)", "table").action(async (instanceId, options) => {
569
+ const client = getClient();
570
+ try {
571
+ const status = await client.workflows.getStatus(instanceId);
572
+ output(status, options.format);
573
+ } catch (err) {
574
+ handleError2(err);
575
+ }
576
+ });
577
+ workflows.command("pause <instance-id>").description("Pause a running workflow").option("-f, --format <format>", "Output format (json, table)", "table").action(async (instanceId, options) => {
578
+ const client = getClient();
579
+ try {
580
+ info(`Pausing workflow: ${instanceId}`);
581
+ const result = await client.workflows.pause(instanceId);
582
+ if (result.success) {
583
+ success(result.message);
584
+ } else {
585
+ error(result.message);
586
+ process.exit(1);
587
+ }
588
+ output(result, options.format);
589
+ } catch (err) {
590
+ handleError2(err);
591
+ }
592
+ });
593
+ workflows.command("resume <instance-id>").description("Resume a paused workflow").option("-f, --format <format>", "Output format (json, table)", "table").action(async (instanceId, options) => {
594
+ const client = getClient();
595
+ try {
596
+ info(`Resuming workflow: ${instanceId}`);
597
+ const result = await client.workflows.resume(instanceId);
598
+ if (result.success) {
599
+ success(result.message);
600
+ } else {
601
+ error(result.message);
602
+ process.exit(1);
603
+ }
604
+ output(result, options.format);
605
+ } catch (err) {
606
+ handleError2(err);
607
+ }
608
+ });
609
+ workflows.command("terminate <instance-id>").description("Terminate a workflow").option("-f, --format <format>", "Output format (json, table)", "table").action(async (instanceId, options) => {
610
+ const client = getClient();
611
+ try {
612
+ info(`Terminating workflow: ${instanceId}`);
613
+ const result = await client.workflows.terminate(instanceId);
614
+ if (result.success) {
615
+ success(result.message);
616
+ } else {
617
+ error(result.message);
618
+ process.exit(1);
619
+ }
620
+ output(result, options.format);
621
+ } catch (err) {
622
+ handleError2(err);
623
+ }
624
+ });
625
+ return workflows;
626
+ }
627
+ function getClient() {
628
+ if (!hasApiKey()) {
629
+ error("API key not configured");
630
+ info("Run: lindo config set apiKey <your-api-key>");
631
+ info("Or set the LINDO_API_KEY environment variable");
632
+ process.exit(1);
633
+ }
634
+ const config = loadConfig();
635
+ return new LindoClient({
636
+ apiKey: config.apiKey,
637
+ baseUrl: config.baseUrl
638
+ });
639
+ }
640
+ function handleError2(err) {
641
+ if (err instanceof AuthenticationError) {
642
+ error("Authentication failed");
643
+ info("Your API key may be invalid or expired");
644
+ info("Run: lindo config set apiKey <your-api-key>");
645
+ process.exit(1);
646
+ }
647
+ if (err instanceof Error) {
648
+ error(err.message);
649
+ } else {
650
+ error("An unexpected error occurred");
651
+ }
652
+ process.exit(1);
653
+ }
654
+ function createWorkspaceCommand() {
655
+ const workspace = new Command("workspace").description("Workspace operations");
656
+ workspace.command("get").description("Get workspace details").option("-f, --format <format>", "Output format (json, table)", "table").action(async (options) => {
657
+ const client = getClient2();
658
+ try {
659
+ const response = await client.workspace.get();
660
+ output(response, options.format);
661
+ } catch (err) {
662
+ handleError3(err);
663
+ }
664
+ });
665
+ workspace.command("credits").description("Get workspace credit balance").option("-f, --format <format>", "Output format (json, table)", "table").action(async (options) => {
666
+ const client = getClient2();
667
+ try {
668
+ const credits = await client.workspace.getCredits();
669
+ output(credits, options.format);
670
+ } catch (err) {
671
+ handleError3(err);
672
+ }
673
+ });
674
+ workspace.command("client-credits").description("Get credit balance for a specific client").requiredOption("-c, --client <id>", "Client ID").option("-f, --format <format>", "Output format (json, table)", "table").action(async (options) => {
675
+ const client = getClient2();
676
+ try {
677
+ const credits = await client.workspace.getClientCredits(options.client);
678
+ output(credits, options.format);
679
+ } catch (err) {
680
+ handleError3(err);
681
+ }
682
+ });
683
+ workspace.command("update").description("Update workspace settings").option("-n, --name <name>", "Workspace name").option("-l, --language <lang>", "Workspace language").option("-w, --webhook <url>", "Webhook URL").option("-f, --format <format>", "Output format (json, table)", "table").action(async (options) => {
684
+ const client = getClient2();
685
+ try {
686
+ const response = await client.workspace.update({
687
+ workspace_name: options.name,
688
+ workspace_language: options.language,
689
+ webhook_url: options.webhook
690
+ });
691
+ if (response.success) {
692
+ success("Workspace updated");
693
+ }
694
+ output(response, options.format);
695
+ } catch (err) {
696
+ handleError3(err);
697
+ }
698
+ });
699
+ workspace.command("team-add").description("Add a team member to the workspace").requiredOption("-e, --email <email>", "Team member email").option("-r, --role <role>", "Role (Team)", "Team").option("-f, --format <format>", "Output format (json, table)", "table").action(async (options) => {
700
+ const client = getClient2();
701
+ try {
702
+ const response = await client.workspace.addTeamMember(options.email, options.role);
703
+ if (response.success) {
704
+ success("Team member added");
705
+ }
706
+ output(response, options.format);
707
+ } catch (err) {
708
+ handleError3(err);
709
+ }
710
+ });
711
+ workspace.command("team-remove").description("Remove a team member from the workspace").requiredOption("-m, --member <id>", "Member ID").option("-f, --format <format>", "Output format (json, table)", "table").action(async (options) => {
712
+ const client = getClient2();
713
+ try {
714
+ const response = await client.workspace.removeTeamMember(options.member);
715
+ if (response.success) {
716
+ success("Team member removed");
717
+ }
718
+ output(response, options.format);
719
+ } catch (err) {
720
+ handleError3(err);
721
+ }
722
+ });
723
+ workspace.command("integration-add").description("Add an integration to the workspace").requiredOption("-t, --type <type>", "Integration type (e.g., matomo)").requiredOption("-c, --config <json>", "Integration config as JSON").option("-f, --format <format>", "Output format (json, table)", "table").action(async (options) => {
724
+ const client = getClient2();
725
+ let config;
726
+ try {
727
+ config = JSON.parse(options.config);
728
+ } catch {
729
+ error("Invalid JSON config");
730
+ process.exit(1);
731
+ }
732
+ try {
733
+ const response = await client.workspace.addIntegration({
734
+ integration_type: options.type,
735
+ config
736
+ });
737
+ if (response.success) {
738
+ success("Integration added");
739
+ }
740
+ output(response, options.format);
741
+ } catch (err) {
742
+ handleError3(err);
743
+ }
744
+ });
745
+ workspace.command("integration-remove").description("Remove an integration from the workspace").requiredOption("-t, --type <type>", "Integration type").option("-f, --format <format>", "Output format (json, table)", "table").action(async (options) => {
746
+ const client = getClient2();
747
+ try {
748
+ const response = await client.workspace.removeIntegration(options.type);
749
+ if (response.success) {
750
+ success("Integration removed");
751
+ }
752
+ output(response, options.format);
753
+ } catch (err) {
754
+ handleError3(err);
755
+ }
756
+ });
757
+ workspace.command("whitelabel").description("Setup or update whitelabel settings").option("-d, --domain <domain>", "Custom domain").option("-s, --subdomain <domain>", "Subdomain domain").option("-e, --email-sender <email>", "Email sender address").option("--enable-register", "Enable client registration").option("--disable-register", "Disable client registration").option("-f, --format <format>", "Output format (json, table)", "table").action(async (options) => {
758
+ const client = getClient2();
759
+ try {
760
+ const response = await client.workspace.setupWhitelabel({
761
+ domain: options.domain,
762
+ subdomain_domain: options.subdomain,
763
+ email_sender: options.emailSender,
764
+ wl_client_register: options.enableRegister ? true : options.disableRegister ? false : void 0
765
+ });
766
+ if (response.success) {
767
+ success("Whitelabel settings updated");
768
+ }
769
+ output(response, options.format);
770
+ } catch (err) {
771
+ handleError3(err);
772
+ }
773
+ });
774
+ workspace.command("appearance").description("Update workspace appearance settings").option("-p, --primary <color>", "Primary color (hex)").option("-s, --secondary <color>", "Secondary color (hex)").option("-m, --mode <mode>", "Theme mode (light/dark)").option("-f, --format <format>", "Output format (json, table)", "table").action(async (options) => {
775
+ const client = getClient2();
776
+ try {
777
+ const response = await client.workspace.updateAppearance({
778
+ primary_color: options.primary,
779
+ secondary_color: options.secondary,
780
+ theme_mode: options.mode
781
+ });
782
+ if (response.success) {
783
+ success("Appearance settings updated");
784
+ }
785
+ output(response, options.format);
786
+ } catch (err) {
787
+ handleError3(err);
788
+ }
789
+ });
790
+ return workspace;
791
+ }
792
+ function getClient2() {
793
+ if (!hasApiKey()) {
794
+ error("API key not configured");
795
+ info("Run: lindo config set apiKey <your-api-key>");
796
+ info("Or set the LINDO_API_KEY environment variable");
797
+ process.exit(1);
798
+ }
799
+ const config = loadConfig();
800
+ return new LindoClient({
801
+ apiKey: config.apiKey,
802
+ baseUrl: config.baseUrl
803
+ });
804
+ }
805
+ function handleError3(err) {
806
+ if (err instanceof AuthenticationError) {
807
+ error("Authentication failed");
808
+ info("Your API key may be invalid or expired");
809
+ info("Run: lindo config set apiKey <your-api-key>");
810
+ process.exit(1);
811
+ }
812
+ if (err instanceof Error) {
813
+ error(err.message);
814
+ } else {
815
+ error("An unexpected error occurred");
816
+ }
817
+ process.exit(1);
818
+ }
819
+ function createAnalyticsCommand() {
820
+ const analytics = new Command("analytics").description("Analytics operations");
821
+ analytics.command("workspace").description("Get workspace analytics").option("--from <date>", "Start date (ISO format)").option("--to <date>", "End date (ISO format)").option("-f, --format <format>", "Output format (json, table)", "table").action(async (options) => {
822
+ const client = getClient3();
823
+ try {
824
+ const analytics2 = await client.analytics.getWorkspace({
825
+ from: options.from,
826
+ to: options.to
827
+ });
828
+ output(analytics2, options.format);
829
+ } catch (err) {
830
+ handleError4(err);
831
+ }
832
+ });
833
+ analytics.command("website").description("Get website analytics").requiredOption("-w, --website <id>", "Website ID (required)").option("--from <date>", "Start date (ISO format)").option("--to <date>", "End date (ISO format)").option("-f, --format <format>", "Output format (json, table)", "table").action(async (options) => {
834
+ const client = getClient3();
835
+ try {
836
+ const analytics2 = await client.analytics.getWebsite({
837
+ website_id: options.website,
838
+ from: options.from,
839
+ to: options.to
840
+ });
841
+ output(analytics2, options.format);
842
+ } catch (err) {
843
+ handleError4(err);
844
+ }
845
+ });
846
+ return analytics;
847
+ }
848
+ function getClient3() {
849
+ if (!hasApiKey()) {
850
+ error("API key not configured");
851
+ info("Run: lindo config set apiKey <your-api-key>");
852
+ info("Or set the LINDO_API_KEY environment variable");
853
+ process.exit(1);
854
+ }
855
+ const config = loadConfig();
856
+ return new LindoClient({
857
+ apiKey: config.apiKey,
858
+ baseUrl: config.baseUrl
859
+ });
860
+ }
861
+ function handleError4(err) {
862
+ if (err instanceof AuthenticationError) {
863
+ error("Authentication failed");
864
+ info("Your API key may be invalid or expired");
865
+ info("Run: lindo config set apiKey <your-api-key>");
866
+ process.exit(1);
867
+ }
868
+ if (err instanceof Error) {
869
+ error(err.message);
870
+ } else {
871
+ error("An unexpected error occurred");
872
+ }
873
+ process.exit(1);
874
+ }
875
+ function createClientsCommand() {
876
+ const clients = new Command("clients").description("Client management operations");
877
+ clients.command("list").description("List all workspace clients").option("-p, --page <page>", "Page number", "1").option("-s, --search <search>", "Search term").option("-f, --format <format>", "Output format (json, table)", "table").action(async (options) => {
878
+ const client = getClient4();
879
+ try {
880
+ const response = await client.clients.list({
881
+ page: parseInt(options.page, 10),
882
+ search: options.search
883
+ });
884
+ if (options.format === "json") {
885
+ output(response, "json");
886
+ } else {
887
+ if (response.clients && response.clients.length > 0) {
888
+ console.log("\nClients:");
889
+ console.log("--------");
890
+ for (const c of response.clients) {
891
+ console.log(` ID: ${c.record_id}`);
892
+ console.log(` Email: ${c.email}`);
893
+ console.log(` Website Limit: ${c.website_limit ?? "N/A"}`);
894
+ console.log(` Suspended: ${c.suspended ?? false}`);
895
+ console.log("");
896
+ }
897
+ console.log(`Total: ${response.total ?? response.clients.length}`);
898
+ } else {
899
+ info("No clients found");
900
+ }
901
+ }
902
+ } catch (err) {
903
+ handleError5(err);
904
+ }
905
+ });
906
+ clients.command("create").description("Create a new workspace client").requiredOption("-e, --email <email>", "Client email address").option("-l, --limit <limit>", "Website limit", "5").option("-f, --format <format>", "Output format (json, table)", "table").action(async (options) => {
907
+ const client = getClient4();
908
+ try {
909
+ const response = await client.clients.create({
910
+ email: options.email,
911
+ website_limit: parseInt(options.limit, 10)
912
+ });
913
+ if (response.success && response.client) {
914
+ success(`Client created: ${response.client.record_id}`);
915
+ output(response.client, options.format);
916
+ } else {
917
+ error("Failed to create client");
918
+ if (response.errors) {
919
+ for (const e of response.errors) {
920
+ error(` ${e}`);
921
+ }
922
+ }
923
+ }
924
+ } catch (err) {
925
+ handleError5(err);
926
+ }
927
+ });
928
+ clients.command("update").description("Update a workspace client").requiredOption("-i, --id <id>", "Client ID").option("-l, --limit <limit>", "Website limit").option("--suspend", "Suspend the client").option("--unsuspend", "Unsuspend the client").option("-f, --format <format>", "Output format (json, table)", "table").action(async (options) => {
929
+ const client = getClient4();
930
+ try {
931
+ const response = await client.clients.update({
932
+ client_id: options.id,
933
+ website_limit: options.limit ? parseInt(options.limit, 10) : void 0,
934
+ suspended: options.suspend ? true : options.unsuspend ? false : void 0
935
+ });
936
+ if (response.success) {
937
+ success("Client updated");
938
+ if (response.client) {
939
+ output(response.client, options.format);
940
+ }
941
+ } else {
942
+ error("Failed to update client");
943
+ if (response.errors) {
944
+ for (const e of response.errors) {
945
+ error(` ${e}`);
946
+ }
947
+ }
948
+ }
949
+ } catch (err) {
950
+ handleError5(err);
951
+ }
952
+ });
953
+ clients.command("delete").description("Delete a workspace client").requiredOption("-i, --id <id>", "Client ID").action(async (options) => {
954
+ const client = getClient4();
955
+ try {
956
+ const response = await client.clients.delete(options.id);
957
+ if (response.success) {
958
+ success("Client deleted");
959
+ } else {
960
+ error("Failed to delete client");
961
+ if (response.errors) {
962
+ for (const e of response.errors) {
963
+ error(` ${e}`);
964
+ }
965
+ }
966
+ }
967
+ } catch (err) {
968
+ handleError5(err);
969
+ }
970
+ });
971
+ clients.command("magic-link").description("Create a magic link for client authentication").requiredOption("-e, --email <email>", "Client email address").option("-f, --format <format>", "Output format (json, table)", "table").action(async (options) => {
972
+ const client = getClient4();
973
+ try {
974
+ const response = await client.clients.createMagicLink(options.email);
975
+ if (response.success) {
976
+ success("Magic link created");
977
+ output(response, options.format);
978
+ } else {
979
+ error("Failed to create magic link");
980
+ if (response.errors) {
981
+ for (const e of response.errors) {
982
+ error(` ${e}`);
983
+ }
984
+ }
985
+ }
986
+ } catch (err) {
987
+ handleError5(err);
988
+ }
989
+ });
990
+ return clients;
991
+ }
992
+ function getClient4() {
993
+ if (!hasApiKey()) {
994
+ error("API key not configured");
995
+ info("Run: lindo config set apiKey <your-api-key>");
996
+ info("Or set the LINDO_API_KEY environment variable");
997
+ process.exit(1);
998
+ }
999
+ const config = loadConfig();
1000
+ return new LindoClient({
1001
+ apiKey: config.apiKey,
1002
+ baseUrl: config.baseUrl
1003
+ });
1004
+ }
1005
+ function handleError5(err) {
1006
+ if (err instanceof AuthenticationError) {
1007
+ error("Authentication failed");
1008
+ info("Your API key may be invalid or expired");
1009
+ info("Run: lindo config set apiKey <your-api-key>");
1010
+ process.exit(1);
1011
+ }
1012
+ if (err instanceof Error) {
1013
+ error(err.message);
1014
+ } else {
1015
+ error("An unexpected error occurred");
1016
+ }
1017
+ process.exit(1);
1018
+ }
1019
+ function createWebsitesCommand() {
1020
+ const websites = new Command("websites").description("Website management operations");
1021
+ websites.command("list").description("List all workspace websites").option("-p, --page <page>", "Page number", "1").option("-s, --search <search>", "Search term").option("-f, --format <format>", "Output format (json, table)", "table").action(async (options) => {
1022
+ const client = getClient5();
1023
+ try {
1024
+ const response = await client.websites.list({
1025
+ page: parseInt(options.page, 10),
1026
+ search: options.search
1027
+ });
1028
+ if (options.format === "json") {
1029
+ output(response, "json");
1030
+ } else {
1031
+ if (response.websites && response.websites.length > 0) {
1032
+ console.log("\nWebsites:");
1033
+ console.log("---------");
1034
+ for (const w of response.websites) {
1035
+ console.log(` ID: ${w.record_id}`);
1036
+ console.log(` Business Name: ${w.business_name ?? "N/A"}`);
1037
+ console.log(` Preview URL: ${w.preview_url ?? "N/A"}`);
1038
+ console.log(` Activated: ${w.activated ?? false}`);
1039
+ console.log("");
1040
+ }
1041
+ console.log(`Total: ${response.total ?? response.websites.length}`);
1042
+ } else {
1043
+ info("No websites found");
1044
+ }
1045
+ }
1046
+ } catch (err) {
1047
+ handleError6(err);
1048
+ }
1049
+ });
1050
+ websites.command("get").description("Get website details").requiredOption("-i, --id <id>", "Website ID").option("-f, --format <format>", "Output format (json, table)", "table").action(async (options) => {
1051
+ const client = getClient5();
1052
+ try {
1053
+ const response = await client.websites.getDetails(options.id);
1054
+ output(response, options.format);
1055
+ } catch (err) {
1056
+ handleError6(err);
1057
+ }
1058
+ });
1059
+ websites.command("update").description("Update a website").requiredOption("-i, --id <id>", "Website ID").option("-n, --name <name>", "Business name").option("--activate", "Activate the website").option("--deactivate", "Deactivate the website").option("-f, --format <format>", "Output format (json, table)", "table").action(async (options) => {
1060
+ const client = getClient5();
1061
+ try {
1062
+ const response = await client.websites.update({
1063
+ website_id: options.id,
1064
+ business_name: options.name,
1065
+ activated: options.activate ? true : options.deactivate ? false : void 0
1066
+ });
1067
+ if (response.success) {
1068
+ success("Website updated");
1069
+ if (response.website) {
1070
+ output(response.website, options.format);
1071
+ }
1072
+ } else {
1073
+ error("Failed to update website");
1074
+ if (response.errors) {
1075
+ for (const e of response.errors) {
1076
+ error(` ${e}`);
1077
+ }
1078
+ }
1079
+ }
1080
+ } catch (err) {
1081
+ handleError6(err);
1082
+ }
1083
+ });
1084
+ websites.command("settings").description("Update website settings").requiredOption("-i, --id <id>", "Website ID").option("-n, --name <name>", "Business name").option("-l, --language <lang>", "Language").option("-d, --description <desc>", "Business description").option("-f, --format <format>", "Output format (json, table)", "table").action(async (options) => {
1085
+ const client = getClient5();
1086
+ try {
1087
+ const response = await client.websites.updateSettings(options.id, {
1088
+ business_name: options.name,
1089
+ language: options.language,
1090
+ business_description: options.description
1091
+ });
1092
+ if (response.success) {
1093
+ success("Website settings updated");
1094
+ }
1095
+ output(response, options.format);
1096
+ } catch (err) {
1097
+ handleError6(err);
1098
+ }
1099
+ });
1100
+ websites.command("delete").description("Delete a website").requiredOption("-i, --id <id>", "Website ID").action(async (options) => {
1101
+ const client = getClient5();
1102
+ try {
1103
+ const response = await client.websites.delete(options.id);
1104
+ if (response.success) {
1105
+ success("Website deleted");
1106
+ } else {
1107
+ error("Failed to delete website");
1108
+ if (response.errors) {
1109
+ for (const e of response.errors) {
1110
+ error(` ${e}`);
1111
+ }
1112
+ }
1113
+ }
1114
+ } catch (err) {
1115
+ handleError6(err);
1116
+ }
1117
+ });
1118
+ websites.command("assign").description("Assign a website to a client").requiredOption("-w, --website <id>", "Website ID").requiredOption("-c, --client <id>", "Client ID").action(async (options) => {
1119
+ const client = getClient5();
1120
+ try {
1121
+ const response = await client.websites.assign({
1122
+ website_id: options.website,
1123
+ client_id: options.client
1124
+ });
1125
+ if (response.success) {
1126
+ success("Website assigned to client");
1127
+ } else {
1128
+ error("Failed to assign website");
1129
+ if (response.errors) {
1130
+ for (const e of response.errors) {
1131
+ error(` ${e}`);
1132
+ }
1133
+ }
1134
+ }
1135
+ } catch (err) {
1136
+ handleError6(err);
1137
+ }
1138
+ });
1139
+ websites.command("domain-add").description("Add a custom domain to a website").requiredOption("-i, --id <id>", "Website ID").requiredOption("-d, --domain <domain>", "Custom domain").option("-f, --format <format>", "Output format (json, table)", "table").action(async (options) => {
1140
+ const client = getClient5();
1141
+ try {
1142
+ const response = await client.websites.addDomain(options.id, options.domain);
1143
+ if (response.success) {
1144
+ success("Domain added");
1145
+ if (response.result?.dns_records) {
1146
+ console.log("\nDNS Records to configure:");
1147
+ for (const record of response.result.dns_records) {
1148
+ console.log(` ${record.record_type} ${record.host} -> ${record.value}`);
1149
+ }
1150
+ }
1151
+ }
1152
+ output(response, options.format);
1153
+ } catch (err) {
1154
+ handleError6(err);
1155
+ }
1156
+ });
1157
+ websites.command("domain-remove").description("Remove a custom domain from a website").requiredOption("-i, --id <id>", "Website ID").option("-f, --format <format>", "Output format (json, table)", "table").action(async (options) => {
1158
+ const client = getClient5();
1159
+ try {
1160
+ const response = await client.websites.removeDomain(options.id);
1161
+ if (response.success) {
1162
+ success("Domain removed");
1163
+ }
1164
+ output(response, options.format);
1165
+ } catch (err) {
1166
+ handleError6(err);
1167
+ }
1168
+ });
1169
+ websites.command("integration-add").description("Add an integration to a website").requiredOption("-i, --id <id>", "Website ID").requiredOption("-t, --type <type>", "Integration type (e.g., matomo)").requiredOption("-c, --config <json>", "Integration config as JSON").option("-f, --format <format>", "Output format (json, table)", "table").action(async (options) => {
1170
+ const client = getClient5();
1171
+ let config;
1172
+ try {
1173
+ config = JSON.parse(options.config);
1174
+ } catch {
1175
+ error("Invalid JSON config");
1176
+ process.exit(1);
1177
+ }
1178
+ try {
1179
+ const response = await client.websites.addIntegration(options.id, {
1180
+ integration_type: options.type,
1181
+ config
1182
+ });
1183
+ if (response.success) {
1184
+ success("Integration added");
1185
+ }
1186
+ output(response, options.format);
1187
+ } catch (err) {
1188
+ handleError6(err);
1189
+ }
1190
+ });
1191
+ websites.command("integration-remove").description("Remove an integration from a website").requiredOption("-i, --id <id>", "Website ID").requiredOption("-t, --type <type>", "Integration type").option("-f, --format <format>", "Output format (json, table)", "table").action(async (options) => {
1192
+ const client = getClient5();
1193
+ try {
1194
+ const response = await client.websites.removeIntegration(options.id, options.type);
1195
+ if (response.success) {
1196
+ success("Integration removed");
1197
+ }
1198
+ output(response, options.format);
1199
+ } catch (err) {
1200
+ handleError6(err);
1201
+ }
1202
+ });
1203
+ websites.command("team-add").description("Add a team member to a website").requiredOption("-i, --id <id>", "Website ID").requiredOption("-e, --email <email>", "Team member email").requiredOption("-r, --role <role>", "Role (Editor or Commenter)").option("-f, --format <format>", "Output format (json, table)", "table").action(async (options) => {
1204
+ const client = getClient5();
1205
+ try {
1206
+ const response = await client.websites.addTeamMember(options.id, options.email, options.role);
1207
+ if (response.success) {
1208
+ success("Team member added");
1209
+ }
1210
+ output(response, options.format);
1211
+ } catch (err) {
1212
+ handleError6(err);
1213
+ }
1214
+ });
1215
+ websites.command("team-remove").description("Remove a team member from a website").requiredOption("-i, --id <id>", "Website ID").requiredOption("-m, --member <memberId>", "Member ID").option("-f, --format <format>", "Output format (json, table)", "table").action(async (options) => {
1216
+ const client = getClient5();
1217
+ try {
1218
+ const response = await client.websites.removeTeamMember(options.id, options.member);
1219
+ if (response.success) {
1220
+ success("Team member removed");
1221
+ }
1222
+ output(response, options.format);
1223
+ } catch (err) {
1224
+ handleError6(err);
1225
+ }
1226
+ });
1227
+ return websites;
1228
+ }
1229
+ function getClient5() {
1230
+ if (!hasApiKey()) {
1231
+ error("API key not configured");
1232
+ info("Run: lindo config set apiKey <your-api-key>");
1233
+ info("Or set the LINDO_API_KEY environment variable");
1234
+ process.exit(1);
1235
+ }
1236
+ const config = loadConfig();
1237
+ return new LindoClient({
1238
+ apiKey: config.apiKey,
1239
+ baseUrl: config.baseUrl
1240
+ });
1241
+ }
1242
+ function handleError6(err) {
1243
+ if (err instanceof AuthenticationError) {
1244
+ error("Authentication failed");
1245
+ info("Your API key may be invalid or expired");
1246
+ info("Run: lindo config set apiKey <your-api-key>");
1247
+ process.exit(1);
1248
+ }
1249
+ if (err instanceof Error) {
1250
+ error(err.message);
1251
+ } else {
1252
+ error("An unexpected error occurred");
1253
+ }
1254
+ process.exit(1);
1255
+ }
1256
+ function openBrowser(url) {
1257
+ return new Promise((resolve4) => {
1258
+ const currentPlatform = platform();
1259
+ let command;
1260
+ switch (currentPlatform) {
1261
+ case "darwin":
1262
+ command = `open "${url}"`;
1263
+ break;
1264
+ case "win32":
1265
+ command = `start "" "${url}"`;
1266
+ break;
1267
+ default:
1268
+ command = `xdg-open "${url}"`;
1269
+ break;
1270
+ }
1271
+ exec(command, (error2) => {
1272
+ if (error2) {
1273
+ resolve4(false);
1274
+ } else {
1275
+ resolve4(true);
1276
+ }
1277
+ });
1278
+ });
1279
+ }
1280
+
1281
+ // src/commands/pages.ts
1282
+ var PID_FILE = path3.join(os2.tmpdir(), "lindoai-pages-preview.pid");
1283
+ var PORT_FILE = path3.join(os2.tmpdir(), "lindoai-pages-preview.port");
1284
+ function createPagesCommand() {
1285
+ const pages = new Command("pages").description("Page management operations");
1286
+ pages.command("list").description("List all pages for a website").requiredOption("-w, --website <id>", "Website ID").option("-p, --page <page>", "Page number", "1").option("-s, --search <search>", "Search term").option("-f, --format <format>", "Output format (json, table)", "table").action(async (options) => {
1287
+ const client = getClient6();
1288
+ try {
1289
+ const response = await client.pages.list(options.website, {
1290
+ page: parseInt(options.page, 10),
1291
+ search: options.search
1292
+ });
1293
+ if (options.format === "json") {
1294
+ output(response, "json");
1295
+ } else {
1296
+ const result = response.result;
1297
+ if (result?.list && result.list.length > 0) {
1298
+ console.log("\nPages:");
1299
+ console.log("------");
1300
+ for (const p of result.list) {
1301
+ console.log(` ID: ${p.page_id}`);
1302
+ console.log(` Name: ${p.name ?? "N/A"}`);
1303
+ console.log(` Path: ${p.path ?? "N/A"}`);
1304
+ console.log(` Status: ${p.status ?? "N/A"}`);
1305
+ console.log(` Published: ${p.publish_date ? new Date(p.publish_date * 1e3).toISOString() : "No"}`);
1306
+ console.log("");
1307
+ }
1308
+ console.log(`Total: ${result.total ?? result.list.length}`);
1309
+ } else {
1310
+ info("No pages found");
1311
+ }
1312
+ }
1313
+ } catch (err) {
1314
+ handleError7(err);
1315
+ }
1316
+ });
1317
+ pages.command("get").description("Get details of a specific page").requiredOption("-w, --website <id>", "Website ID").requiredOption("-i, --id <id>", "Page ID").option("-f, --format <format>", "Output format (json, table)", "table").action(async (options) => {
1318
+ const client = getClient6();
1319
+ try {
1320
+ const response = await client.pages.get(options.website, options.id);
1321
+ if (options.format === "json") {
1322
+ output(response, "json");
1323
+ } else {
1324
+ const result = response.result;
1325
+ if (result) {
1326
+ console.log("\nPage Details:");
1327
+ console.log("-------------");
1328
+ console.log(` ID: ${result.page_id}`);
1329
+ console.log(` Name: ${result.name ?? "N/A"}`);
1330
+ console.log(` Path: ${result.path ?? "N/A"}`);
1331
+ console.log(` Status: ${result.status ?? "N/A"}`);
1332
+ console.log(` Published: ${result.publish_date ? new Date(result.publish_date * 1e3).toISOString() : "No"}`);
1333
+ console.log(` Created: ${result.created_date ?? "N/A"}`);
1334
+ } else {
1335
+ error("Page not found");
1336
+ }
1337
+ }
1338
+ } catch (err) {
1339
+ handleError7(err);
1340
+ }
1341
+ });
1342
+ pages.command("publish").description("Publish a page").requiredOption("-w, --website <id>", "Website ID").requiredOption("-i, --id <id>", "Page ID").option("-f, --format <format>", "Output format (json, table)", "table").action(async (options) => {
1343
+ const client = getClient6();
1344
+ try {
1345
+ const response = await client.pages.publish(options.website, options.id);
1346
+ if (options.format === "json") {
1347
+ output(response, "json");
1348
+ } else {
1349
+ if (response.success) {
1350
+ success("Page published successfully");
1351
+ console.log(` Page ID: ${response.result?.page_id}`);
1352
+ console.log(` Published at: ${response.result?.publish_date ? new Date(response.result.publish_date * 1e3).toISOString() : "N/A"}`);
1353
+ } else {
1354
+ error("Failed to publish page");
1355
+ }
1356
+ }
1357
+ } catch (err) {
1358
+ handleError7(err);
1359
+ }
1360
+ });
1361
+ pages.command("unpublish").description("Unpublish a page").requiredOption("-w, --website <id>", "Website ID").requiredOption("-i, --id <id>", "Page ID").option("-f, --format <format>", "Output format (json, table)", "table").action(async (options) => {
1362
+ const client = getClient6();
1363
+ try {
1364
+ const response = await client.pages.unpublish(options.website, options.id);
1365
+ if (options.format === "json") {
1366
+ output(response, "json");
1367
+ } else {
1368
+ if (response.success) {
1369
+ success("Page unpublished successfully");
1370
+ console.log(` Page ID: ${response.result?.page_id}`);
1371
+ } else {
1372
+ error("Failed to unpublish page");
1373
+ }
1374
+ }
1375
+ } catch (err) {
1376
+ handleError7(err);
1377
+ }
1378
+ });
1379
+ pages.command("edit").description("Edit a page with live preview").argument("<website_id>", "Website ID").argument("<page_id>", "Page ID").option("--file <path>", "Output file path", "./page.html").option("--background", "Run preview server in background").action(async (websiteId, pageId, options) => {
1380
+ const client = getClient6();
1381
+ try {
1382
+ info("Fetching page details...");
1383
+ const pageResponse = await client.pages.get(websiteId, pageId);
1384
+ if (!pageResponse.result) {
1385
+ error("Page not found");
1386
+ process.exit(1);
1387
+ }
1388
+ const pagePath = pageResponse.result.path;
1389
+ const websiteResponse = await client.websites.getDetails(websiteId);
1390
+ if (!websiteResponse.result) {
1391
+ error("Website not found");
1392
+ process.exit(1);
1393
+ }
1394
+ const baseUrl = websiteResponse.result.custom_domain || websiteResponse.result.verified_domain || websiteResponse.result.preview_url;
1395
+ if (!baseUrl) {
1396
+ error("Could not determine website URL");
1397
+ process.exit(1);
1398
+ }
1399
+ const pageUrl = `https://${baseUrl}${pagePath}`;
1400
+ info(`Fetching HTML from ${pageUrl}...`);
1401
+ const htmlResponse = await fetch(pageUrl);
1402
+ if (!htmlResponse.ok) {
1403
+ error(`Failed to fetch page HTML: ${htmlResponse.status} ${htmlResponse.statusText}`);
1404
+ info("Make sure the page is published before editing");
1405
+ process.exit(1);
1406
+ }
1407
+ const html = await htmlResponse.text();
1408
+ const outputPath = path3.resolve(options.file);
1409
+ fs3.writeFileSync(outputPath, html, "utf-8");
1410
+ success(`HTML saved to ${outputPath}`);
1411
+ await terminateExistingPreviewServer();
1412
+ if (options.background) {
1413
+ await startBackgroundPreviewServer(outputPath);
1414
+ } else {
1415
+ await startForegroundPreviewServer(outputPath);
1416
+ }
1417
+ } catch (err) {
1418
+ handleError7(err);
1419
+ }
1420
+ });
1421
+ pages.command("update").description("Update a page").argument("<website_id>", "Website ID").argument("<page_id>", "Page ID").option("--html-file <path>", "Path to local HTML file to upload").option("-f, --format <format>", "Output format (json, table)", "table").action(async (websiteId, pageId, options) => {
1422
+ const client = getClient6();
1423
+ const config = loadConfig();
1424
+ try {
1425
+ if (!options.htmlFile) {
1426
+ error("--html-file option is required");
1427
+ info("Usage: lindoai pages update <website_id> <page_id> --html-file <path>");
1428
+ process.exit(1);
1429
+ }
1430
+ const htmlPath = path3.resolve(options.htmlFile);
1431
+ if (!fs3.existsSync(htmlPath)) {
1432
+ error(`File not found: ${htmlPath}`);
1433
+ process.exit(1);
1434
+ }
1435
+ const html = fs3.readFileSync(htmlPath, "utf-8");
1436
+ info(`Read ${html.length} bytes from ${htmlPath}`);
1437
+ const pageResponse = await client.pages.get(websiteId, pageId);
1438
+ if (!pageResponse.result) {
1439
+ error("Page not found");
1440
+ process.exit(1);
1441
+ }
1442
+ const pagePath = pageResponse.result.path;
1443
+ info("Updating page...");
1444
+ const apiBaseUrl = config.baseUrl || "https://api.lindo.ai";
1445
+ const updateUrl = `${apiBaseUrl}/v1/workspace/website/${websiteId}/pages/${pageId}/update`;
1446
+ const response = await fetch(updateUrl, {
1447
+ method: "POST",
1448
+ headers: {
1449
+ "Content-Type": "application/json",
1450
+ "Authorization": `Bearer ${config.apiKey}`
1451
+ },
1452
+ body: JSON.stringify({
1453
+ html,
1454
+ path: pagePath
1455
+ })
1456
+ });
1457
+ const result = await response.json();
1458
+ if (options.format === "json") {
1459
+ output(result, "json");
1460
+ } else {
1461
+ if (result.success) {
1462
+ success("Page updated successfully");
1463
+ console.log(` Page ID: ${result.result?.page_id}`);
1464
+ if (result.result?.publish_date) {
1465
+ console.log(` Published at: ${new Date(result.result.publish_date * 1e3).toISOString()}`);
1466
+ }
1467
+ } else {
1468
+ error("Failed to update page");
1469
+ if (result.errors) {
1470
+ for (const err of result.errors) {
1471
+ console.log(` ${err}`);
1472
+ }
1473
+ }
1474
+ }
1475
+ }
1476
+ } catch (err) {
1477
+ handleError7(err);
1478
+ }
1479
+ });
1480
+ pages.command("stop-preview").description("Stop the background preview server").action(async () => {
1481
+ try {
1482
+ if (!fs3.existsSync(PID_FILE)) {
1483
+ info("No preview server is running");
1484
+ return;
1485
+ }
1486
+ const pid = parseInt(fs3.readFileSync(PID_FILE, "utf-8").trim(), 10);
1487
+ if (isNaN(pid)) {
1488
+ error("Invalid PID file");
1489
+ cleanupPidFiles();
1490
+ return;
1491
+ }
1492
+ try {
1493
+ process.kill(pid, "SIGTERM");
1494
+ success(`Preview server (PID ${pid}) stopped`);
1495
+ } catch (killErr) {
1496
+ if (killErr.code === "ESRCH") {
1497
+ info("Preview server process not found (may have already stopped)");
1498
+ } else {
1499
+ error(`Failed to stop preview server: ${killErr.message}`);
1500
+ }
1501
+ }
1502
+ cleanupPidFiles();
1503
+ } catch (err) {
1504
+ if (err instanceof Error) {
1505
+ error(err.message);
1506
+ } else {
1507
+ error("An unexpected error occurred");
1508
+ }
1509
+ process.exit(1);
1510
+ }
1511
+ });
1512
+ return pages;
1513
+ }
1514
+ function getClient6() {
1515
+ if (!hasApiKey()) {
1516
+ error("API key not configured");
1517
+ info("Run: lindo config set apiKey <your-api-key>");
1518
+ info("Or set the LINDO_API_KEY environment variable");
1519
+ process.exit(1);
1520
+ }
1521
+ const config = loadConfig();
1522
+ return new LindoClient({
1523
+ apiKey: config.apiKey,
1524
+ baseUrl: config.baseUrl
1525
+ });
1526
+ }
1527
+ function handleError7(err) {
1528
+ if (err instanceof AuthenticationError) {
1529
+ error("Authentication failed");
1530
+ info("Your API key may be invalid or expired");
1531
+ info("Run: lindo config set apiKey <your-api-key>");
1532
+ process.exit(1);
1533
+ }
1534
+ if (err instanceof Error) {
1535
+ error(err.message);
1536
+ } else {
1537
+ error("An unexpected error occurred");
1538
+ }
1539
+ process.exit(1);
1540
+ }
1541
+ function cleanupPidFiles() {
1542
+ try {
1543
+ if (fs3.existsSync(PID_FILE)) {
1544
+ fs3.unlinkSync(PID_FILE);
1545
+ }
1546
+ } catch {
1547
+ }
1548
+ try {
1549
+ if (fs3.existsSync(PORT_FILE)) {
1550
+ fs3.unlinkSync(PORT_FILE);
1551
+ }
1552
+ } catch {
1553
+ }
1554
+ }
1555
+ async function terminateExistingPreviewServer() {
1556
+ if (!fs3.existsSync(PID_FILE)) {
1557
+ return;
1558
+ }
1559
+ try {
1560
+ const pid = parseInt(fs3.readFileSync(PID_FILE, "utf-8").trim(), 10);
1561
+ if (!isNaN(pid)) {
1562
+ try {
1563
+ process.kill(pid, "SIGTERM");
1564
+ info(`Terminated existing preview server (PID ${pid})`);
1565
+ await new Promise((resolve4) => setTimeout(resolve4, 500));
1566
+ } catch {
1567
+ }
1568
+ }
1569
+ } catch {
1570
+ }
1571
+ cleanupPidFiles();
1572
+ }
1573
+ async function startBackgroundPreviewServer(filePath) {
1574
+ const absolutePath = path3.resolve(filePath);
1575
+ const serverCode = `
1576
+ const http = require('node:http');
1577
+ const fs = require('node:fs');
1578
+ const path = require('node:path');
1579
+ const os = require('node:os');
1580
+
1581
+ const LIVE_RELOAD_SCRIPT = \`<script>
1582
+ (function() {
1583
+ var eventSource = new EventSource('/__live-reload');
1584
+ eventSource.onmessage = function(event) {
1585
+ if (event.data === 'reload') {
1586
+ window.location.reload();
1587
+ }
1588
+ };
1589
+ eventSource.onerror = function() {
1590
+ console.log('[Live Reload] Connection lost, attempting to reconnect...');
1591
+ };
1592
+ })();
1593
+ </script>\`;
1594
+
1595
+ const filePath = ${JSON.stringify(absolutePath)};
1596
+ const pidFile = path.join(os.tmpdir(), 'lindoai-pages-preview.pid');
1597
+ const portFile = path.join(os.tmpdir(), 'lindoai-pages-preview.port');
1598
+ const sseClients = new Set();
1599
+ let debounceTimer = null;
1600
+
1601
+ function injectLiveReload(html) {
1602
+ const bodyCloseTagRegex = /<\\/body>/i;
1603
+ const match = html.match(bodyCloseTagRegex);
1604
+ if (match && match.index !== undefined) {
1605
+ return html.slice(0, match.index) + LIVE_RELOAD_SCRIPT + html.slice(match.index);
1606
+ }
1607
+ return html + LIVE_RELOAD_SCRIPT;
1608
+ }
1609
+
1610
+ function notifyClients() {
1611
+ for (const client of sseClients) {
1612
+ try {
1613
+ client.write('data: reload\\n\\n');
1614
+ } catch {
1615
+ sseClients.delete(client);
1616
+ }
1617
+ }
1618
+ }
1619
+
1620
+ function handleFileChange() {
1621
+ if (debounceTimer) clearTimeout(debounceTimer);
1622
+ debounceTimer = setTimeout(() => {
1623
+ notifyClients();
1624
+ debounceTimer = null;
1625
+ }, 100);
1626
+ }
1627
+
1628
+ const server = http.createServer((req, res) => {
1629
+ const url = req.url || '/';
1630
+ if (req.method !== 'GET') {
1631
+ res.writeHead(405, { 'Content-Type': 'text/plain' });
1632
+ res.end('Method Not Allowed');
1633
+ return;
1634
+ }
1635
+
1636
+ if (url === '/__live-reload') {
1637
+ res.writeHead(200, {
1638
+ 'Content-Type': 'text/event-stream',
1639
+ 'Cache-Control': 'no-cache',
1640
+ 'Connection': 'keep-alive',
1641
+ 'Access-Control-Allow-Origin': '*',
1642
+ });
1643
+ res.write('data: connected\\n\\n');
1644
+ sseClients.add(res);
1645
+ req.on('close', () => sseClients.delete(res));
1646
+ return;
1647
+ }
1648
+
1649
+ if (url === '/' || url === '/index.html') {
1650
+ try {
1651
+ const html = fs.readFileSync(filePath, 'utf-8');
1652
+ const injectedHtml = injectLiveReload(html);
1653
+ res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8', 'Cache-Control': 'no-cache' });
1654
+ res.end(injectedHtml);
1655
+ } catch (err) {
1656
+ res.writeHead(500, { 'Content-Type': 'text/html; charset=utf-8' });
1657
+ res.end('<h1>Error loading file</h1><p>' + err.message + '</p>');
1658
+ }
1659
+ return;
1660
+ }
1661
+
1662
+ res.writeHead(404, { 'Content-Type': 'text/html; charset=utf-8' });
1663
+ res.end('<h1>404 Not Found</h1>');
1664
+ });
1665
+
1666
+ let watcher = null;
1667
+
1668
+ const handleShutdown = () => {
1669
+ if (watcher) watcher.close();
1670
+ if (debounceTimer) clearTimeout(debounceTimer);
1671
+ for (const client of sseClients) {
1672
+ try { client.end(); } catch {}
1673
+ }
1674
+ sseClients.clear();
1675
+ try { if (fs.existsSync(pidFile)) fs.unlinkSync(pidFile); } catch {}
1676
+ try { if (fs.existsSync(portFile)) fs.unlinkSync(portFile); } catch {}
1677
+ server.close(() => process.exit(0));
1678
+ setTimeout(() => process.exit(0), 1000);
1679
+ };
1680
+
1681
+ process.on('SIGTERM', handleShutdown);
1682
+ process.on('SIGINT', handleShutdown);
1683
+
1684
+ server.listen(0, '127.0.0.1', () => {
1685
+ const port = server.address().port;
1686
+ fs.writeFileSync(pidFile, process.pid.toString(), 'utf-8');
1687
+ fs.writeFileSync(portFile, port.toString(), 'utf-8');
1688
+
1689
+ try {
1690
+ watcher = fs.watch(filePath, (eventType) => {
1691
+ if (eventType === 'change') handleFileChange();
1692
+ });
1693
+ } catch {}
1694
+ });
1695
+ `;
1696
+ const child = spawn(process.execPath, ["-e", serverCode], {
1697
+ detached: true,
1698
+ stdio: "ignore"
1699
+ });
1700
+ child.unref();
1701
+ let port = null;
1702
+ for (let i = 0; i < 50; i++) {
1703
+ await new Promise((resolve4) => setTimeout(resolve4, 100));
1704
+ if (fs3.existsSync(PORT_FILE)) {
1705
+ try {
1706
+ port = parseInt(fs3.readFileSync(PORT_FILE, "utf-8").trim(), 10);
1707
+ if (!isNaN(port)) break;
1708
+ } catch {
1709
+ }
1710
+ }
1711
+ }
1712
+ if (!port) {
1713
+ error("Failed to start preview server");
1714
+ process.exit(1);
1715
+ }
1716
+ const previewUrl = `http://127.0.0.1:${port}/`;
1717
+ const browserOpened = await openBrowser(previewUrl);
1718
+ if (!browserOpened) {
1719
+ info(`Could not open browser. Visit: ${previewUrl}`);
1720
+ }
1721
+ success("Preview server started in background");
1722
+ console.log(` URL: ${previewUrl}`);
1723
+ console.log(` PID: ${child.pid}`);
1724
+ console.log("");
1725
+ info("To update the page: lindoai pages update <website_id> <page_id> --html-file <path>");
1726
+ info("To stop the server: lindoai pages stop-preview");
1727
+ }
1728
+ async function startForegroundPreviewServer(filePath) {
1729
+ const { startLivePreviewServer: startLivePreviewServer2 } = await Promise.resolve().then(() => (init_live_preview_server(), live_preview_server_exports));
1730
+ info("Starting preview server...");
1731
+ const port = await startLivePreviewServer2(filePath);
1732
+ const previewUrl = `http://127.0.0.1:${port}/`;
1733
+ fs3.writeFileSync(PID_FILE, process.pid.toString(), "utf-8");
1734
+ fs3.writeFileSync(PORT_FILE, port.toString(), "utf-8");
1735
+ const browserOpened = await openBrowser(previewUrl);
1736
+ if (!browserOpened) {
1737
+ info(`Could not open browser. Visit: ${previewUrl}`);
1738
+ }
1739
+ success("Preview server started");
1740
+ console.log(` URL: ${previewUrl}`);
1741
+ console.log("");
1742
+ info("Press Ctrl+C to stop the server");
1743
+ info("Edit the HTML file and save to see changes in the browser");
1744
+ }
1745
+ var PID_FILE2 = path3.join(os2.tmpdir(), "lindoai-blogs-preview.pid");
1746
+ var PORT_FILE2 = path3.join(os2.tmpdir(), "lindoai-blogs-preview.port");
1747
+ function createBlogsCommand() {
1748
+ const blogs = new Command("blogs").description("Blog management operations");
1749
+ blogs.command("list").description("List all blogs for a website").requiredOption("-w, --website <id>", "Website ID").option("-p, --page <page>", "Page number", "1").option("-s, --search <search>", "Search term").option("-f, --format <format>", "Output format (json, table)", "table").action(async (options) => {
1750
+ const client = getClient7();
1751
+ try {
1752
+ const response = await client.blogs.list(options.website, {
1753
+ page: parseInt(options.page, 10),
1754
+ search: options.search
1755
+ });
1756
+ if (options.format === "json") {
1757
+ output(response, "json");
1758
+ } else {
1759
+ const result = response.result;
1760
+ if (result?.list && result.list.length > 0) {
1761
+ console.log("\nBlogs:");
1762
+ console.log("------");
1763
+ for (const b of result.list) {
1764
+ console.log(` ID: ${b.blog_id}`);
1765
+ console.log(` Name: ${b.name ?? "N/A"}`);
1766
+ console.log(` Path: ${b.path ?? "N/A"}`);
1767
+ console.log(` Status: ${b.status ?? "N/A"}`);
1768
+ console.log(` Published: ${b.publish_date ? new Date(b.publish_date * 1e3).toISOString() : "No"}`);
1769
+ console.log("");
1770
+ }
1771
+ console.log(`Total: ${result.total ?? result.list.length}`);
1772
+ } else {
1773
+ info("No blogs found");
1774
+ }
1775
+ }
1776
+ } catch (err) {
1777
+ handleError8(err);
1778
+ }
1779
+ });
1780
+ blogs.command("get").description("Get details of a specific blog").requiredOption("-w, --website <id>", "Website ID").requiredOption("-i, --id <id>", "Blog ID").option("-f, --format <format>", "Output format (json, table)", "table").action(async (options) => {
1781
+ const client = getClient7();
1782
+ try {
1783
+ const response = await client.blogs.get(options.website, options.id);
1784
+ if (options.format === "json") {
1785
+ output(response, "json");
1786
+ } else {
1787
+ const result = response.result;
1788
+ if (result) {
1789
+ console.log("\nBlog Details:");
1790
+ console.log("-------------");
1791
+ console.log(` ID: ${result.blog_id}`);
1792
+ console.log(` Name: ${result.name ?? "N/A"}`);
1793
+ console.log(` Path: ${result.path ?? "N/A"}`);
1794
+ console.log(` Status: ${result.status ?? "N/A"}`);
1795
+ console.log(` Published: ${result.publish_date ? new Date(result.publish_date * 1e3).toISOString() : "No"}`);
1796
+ console.log(` Created: ${result.created_date ?? "N/A"}`);
1797
+ } else {
1798
+ error("Blog not found");
1799
+ }
1800
+ }
1801
+ } catch (err) {
1802
+ handleError8(err);
1803
+ }
1804
+ });
1805
+ blogs.command("publish").description("Publish a blog").requiredOption("-w, --website <id>", "Website ID").requiredOption("-i, --id <id>", "Blog ID").option("-f, --format <format>", "Output format (json, table)", "table").action(async (options) => {
1806
+ const client = getClient7();
1807
+ try {
1808
+ const response = await client.blogs.publish(options.website, options.id);
1809
+ if (options.format === "json") {
1810
+ output(response, "json");
1811
+ } else {
1812
+ if (response.success) {
1813
+ success("Blog published successfully");
1814
+ console.log(` Blog ID: ${response.result?.blog_id}`);
1815
+ console.log(` Published at: ${response.result?.publish_date ? new Date(response.result.publish_date * 1e3).toISOString() : "N/A"}`);
1816
+ } else {
1817
+ error("Failed to publish blog");
1818
+ }
1819
+ }
1820
+ } catch (err) {
1821
+ handleError8(err);
1822
+ }
1823
+ });
1824
+ blogs.command("unpublish").description("Unpublish a blog").requiredOption("-w, --website <id>", "Website ID").requiredOption("-i, --id <id>", "Blog ID").option("-f, --format <format>", "Output format (json, table)", "table").action(async (options) => {
1825
+ const client = getClient7();
1826
+ try {
1827
+ const response = await client.blogs.unpublish(options.website, options.id);
1828
+ if (options.format === "json") {
1829
+ output(response, "json");
1830
+ } else {
1831
+ if (response.success) {
1832
+ success("Blog unpublished successfully");
1833
+ console.log(` Blog ID: ${response.result?.blog_id}`);
1834
+ } else {
1835
+ error("Failed to unpublish blog");
1836
+ }
1837
+ }
1838
+ } catch (err) {
1839
+ handleError8(err);
1840
+ }
1841
+ });
1842
+ blogs.command("edit").description("Edit a blog with live preview").argument("<website_id>", "Website ID").argument("<blog_id>", "Blog ID").option("--file <path>", "Output file path", "./blog.html").option("--background", "Run preview server in background").action(async (websiteId, blogId, options) => {
1843
+ const client = getClient7();
1844
+ try {
1845
+ info("Fetching blog details...");
1846
+ const blogResponse = await client.blogs.get(websiteId, blogId);
1847
+ if (!blogResponse.result) {
1848
+ error("Blog not found");
1849
+ process.exit(1);
1850
+ }
1851
+ const blogPath = blogResponse.result.path;
1852
+ const websiteResponse = await client.websites.getDetails(websiteId);
1853
+ if (!websiteResponse.result) {
1854
+ error("Website not found");
1855
+ process.exit(1);
1856
+ }
1857
+ const baseUrl = websiteResponse.result.custom_domain || websiteResponse.result.verified_domain || websiteResponse.result.preview_url;
1858
+ if (!baseUrl) {
1859
+ error("Could not determine website URL");
1860
+ process.exit(1);
1861
+ }
1862
+ const blogUrl = `https://${baseUrl}${blogPath}`;
1863
+ info(`Fetching HTML from ${blogUrl}...`);
1864
+ const htmlResponse = await fetch(blogUrl);
1865
+ if (!htmlResponse.ok) {
1866
+ error(`Failed to fetch blog HTML: ${htmlResponse.status} ${htmlResponse.statusText}`);
1867
+ info("Make sure the blog is published before editing");
1868
+ process.exit(1);
1869
+ }
1870
+ const html = await htmlResponse.text();
1871
+ const outputPath = path3.resolve(options.file);
1872
+ fs3.writeFileSync(outputPath, html, "utf-8");
1873
+ success(`HTML saved to ${outputPath}`);
1874
+ await terminateExistingPreviewServer2();
1875
+ if (options.background) {
1876
+ await startBackgroundPreviewServer2(outputPath);
1877
+ } else {
1878
+ await startForegroundPreviewServer2(outputPath);
1879
+ }
1880
+ } catch (err) {
1881
+ handleError8(err);
1882
+ }
1883
+ });
1884
+ blogs.command("update").description("Update a blog").argument("<website_id>", "Website ID").argument("<blog_id>", "Blog ID").option("--html-file <path>", "Path to local HTML file to upload").option("-f, --format <format>", "Output format (json, table)", "table").action(async (websiteId, blogId, options) => {
1885
+ const client = getClient7();
1886
+ const config = loadConfig();
1887
+ try {
1888
+ if (!options.htmlFile) {
1889
+ error("--html-file option is required");
1890
+ info("Usage: lindoai blogs update <website_id> <blog_id> --html-file <path>");
1891
+ process.exit(1);
1892
+ }
1893
+ const htmlPath = path3.resolve(options.htmlFile);
1894
+ if (!fs3.existsSync(htmlPath)) {
1895
+ error(`File not found: ${htmlPath}`);
1896
+ process.exit(1);
1897
+ }
1898
+ const html = fs3.readFileSync(htmlPath, "utf-8");
1899
+ info(`Read ${html.length} bytes from ${htmlPath}`);
1900
+ const blogResponse = await client.blogs.get(websiteId, blogId);
1901
+ if (!blogResponse.result) {
1902
+ error("Blog not found");
1903
+ process.exit(1);
1904
+ }
1905
+ const blogPath = blogResponse.result.path;
1906
+ info("Updating blog...");
1907
+ const apiBaseUrl = config.baseUrl || "https://api.lindo.ai";
1908
+ const updateUrl = `${apiBaseUrl}/v1/workspace/website/${websiteId}/blogs/${blogId}/update`;
1909
+ const response = await fetch(updateUrl, {
1910
+ method: "POST",
1911
+ headers: {
1912
+ "Content-Type": "application/json",
1913
+ "Authorization": `Bearer ${config.apiKey}`
1914
+ },
1915
+ body: JSON.stringify({
1916
+ html,
1917
+ path: blogPath
1918
+ })
1919
+ });
1920
+ const result = await response.json();
1921
+ if (options.format === "json") {
1922
+ output(result, "json");
1923
+ } else {
1924
+ if (result.success) {
1925
+ success("Blog updated successfully");
1926
+ console.log(` Blog ID: ${result.result?.blog_id}`);
1927
+ if (result.result?.publish_date) {
1928
+ console.log(` Published at: ${new Date(result.result.publish_date * 1e3).toISOString()}`);
1929
+ }
1930
+ } else {
1931
+ error("Failed to update blog");
1932
+ if (result.errors) {
1933
+ for (const err of result.errors) {
1934
+ console.log(` ${err}`);
1935
+ }
1936
+ }
1937
+ }
1938
+ }
1939
+ } catch (err) {
1940
+ handleError8(err);
1941
+ }
1942
+ });
1943
+ blogs.command("stop-preview").description("Stop the background preview server").action(async () => {
1944
+ try {
1945
+ if (!fs3.existsSync(PID_FILE2)) {
1946
+ info("No preview server is running");
1947
+ return;
1948
+ }
1949
+ const pid = parseInt(fs3.readFileSync(PID_FILE2, "utf-8").trim(), 10);
1950
+ if (isNaN(pid)) {
1951
+ error("Invalid PID file");
1952
+ cleanupPidFiles2();
1953
+ return;
1954
+ }
1955
+ try {
1956
+ process.kill(pid, "SIGTERM");
1957
+ success(`Preview server (PID ${pid}) stopped`);
1958
+ } catch (killErr) {
1959
+ if (killErr.code === "ESRCH") {
1960
+ info("Preview server process not found (may have already stopped)");
1961
+ } else {
1962
+ error(`Failed to stop preview server: ${killErr.message}`);
1963
+ }
1964
+ }
1965
+ cleanupPidFiles2();
1966
+ } catch (err) {
1967
+ if (err instanceof Error) {
1968
+ error(err.message);
1969
+ } else {
1970
+ error("An unexpected error occurred");
1971
+ }
1972
+ process.exit(1);
1973
+ }
1974
+ });
1975
+ return blogs;
1976
+ }
1977
+ function getClient7() {
1978
+ if (!hasApiKey()) {
1979
+ error("API key not configured");
1980
+ info("Run: lindo config set apiKey <your-api-key>");
1981
+ info("Or set the LINDO_API_KEY environment variable");
1982
+ process.exit(1);
1983
+ }
1984
+ const config = loadConfig();
1985
+ return new LindoClient({
1986
+ apiKey: config.apiKey,
1987
+ baseUrl: config.baseUrl
1988
+ });
1989
+ }
1990
+ function handleError8(err) {
1991
+ if (err instanceof AuthenticationError) {
1992
+ error("Authentication failed");
1993
+ info("Your API key may be invalid or expired");
1994
+ info("Run: lindo config set apiKey <your-api-key>");
1995
+ process.exit(1);
1996
+ }
1997
+ if (err instanceof Error) {
1998
+ error(err.message);
1999
+ } else {
2000
+ error("An unexpected error occurred");
2001
+ }
2002
+ process.exit(1);
2003
+ }
2004
+ function cleanupPidFiles2() {
2005
+ try {
2006
+ if (fs3.existsSync(PID_FILE2)) {
2007
+ fs3.unlinkSync(PID_FILE2);
2008
+ }
2009
+ } catch {
2010
+ }
2011
+ try {
2012
+ if (fs3.existsSync(PORT_FILE2)) {
2013
+ fs3.unlinkSync(PORT_FILE2);
2014
+ }
2015
+ } catch {
2016
+ }
2017
+ }
2018
+ async function terminateExistingPreviewServer2() {
2019
+ if (!fs3.existsSync(PID_FILE2)) {
2020
+ return;
2021
+ }
2022
+ try {
2023
+ const pid = parseInt(fs3.readFileSync(PID_FILE2, "utf-8").trim(), 10);
2024
+ if (!isNaN(pid)) {
2025
+ try {
2026
+ process.kill(pid, "SIGTERM");
2027
+ info(`Terminated existing preview server (PID ${pid})`);
2028
+ await new Promise((resolve4) => setTimeout(resolve4, 500));
2029
+ } catch {
2030
+ }
2031
+ }
2032
+ } catch {
2033
+ }
2034
+ cleanupPidFiles2();
2035
+ }
2036
+ async function startBackgroundPreviewServer2(filePath) {
2037
+ const absolutePath = path3.resolve(filePath);
2038
+ const serverCode = `
2039
+ const http = require('node:http');
2040
+ const fs = require('node:fs');
2041
+ const path = require('node:path');
2042
+ const os = require('node:os');
2043
+
2044
+ const LIVE_RELOAD_SCRIPT = \`<script>
2045
+ (function() {
2046
+ var eventSource = new EventSource('/__live-reload');
2047
+ eventSource.onmessage = function(event) {
2048
+ if (event.data === 'reload') {
2049
+ window.location.reload();
2050
+ }
2051
+ };
2052
+ eventSource.onerror = function() {
2053
+ console.log('[Live Reload] Connection lost, attempting to reconnect...');
2054
+ };
2055
+ })();
2056
+ </script>\`;
2057
+
2058
+ const filePath = ${JSON.stringify(absolutePath)};
2059
+ const pidFile = path.join(os.tmpdir(), 'lindoai-blogs-preview.pid');
2060
+ const portFile = path.join(os.tmpdir(), 'lindoai-blogs-preview.port');
2061
+ const sseClients = new Set();
2062
+ let debounceTimer = null;
2063
+
2064
+ function injectLiveReload(html) {
2065
+ const bodyCloseTagRegex = /<\\/body>/i;
2066
+ const match = html.match(bodyCloseTagRegex);
2067
+ if (match && match.index !== undefined) {
2068
+ return html.slice(0, match.index) + LIVE_RELOAD_SCRIPT + html.slice(match.index);
2069
+ }
2070
+ return html + LIVE_RELOAD_SCRIPT;
2071
+ }
2072
+
2073
+ function notifyClients() {
2074
+ for (const client of sseClients) {
2075
+ try {
2076
+ client.write('data: reload\\n\\n');
2077
+ } catch {
2078
+ sseClients.delete(client);
2079
+ }
2080
+ }
2081
+ }
2082
+
2083
+ function handleFileChange() {
2084
+ if (debounceTimer) clearTimeout(debounceTimer);
2085
+ debounceTimer = setTimeout(() => {
2086
+ notifyClients();
2087
+ debounceTimer = null;
2088
+ }, 100);
2089
+ }
2090
+
2091
+ const server = http.createServer((req, res) => {
2092
+ const url = req.url || '/';
2093
+ if (req.method !== 'GET') {
2094
+ res.writeHead(405, { 'Content-Type': 'text/plain' });
2095
+ res.end('Method Not Allowed');
2096
+ return;
2097
+ }
2098
+
2099
+ if (url === '/__live-reload') {
2100
+ res.writeHead(200, {
2101
+ 'Content-Type': 'text/event-stream',
2102
+ 'Cache-Control': 'no-cache',
2103
+ 'Connection': 'keep-alive',
2104
+ 'Access-Control-Allow-Origin': '*',
2105
+ });
2106
+ res.write('data: connected\\n\\n');
2107
+ sseClients.add(res);
2108
+ req.on('close', () => sseClients.delete(res));
2109
+ return;
2110
+ }
2111
+
2112
+ if (url === '/' || url === '/index.html') {
2113
+ try {
2114
+ const html = fs.readFileSync(filePath, 'utf-8');
2115
+ const injectedHtml = injectLiveReload(html);
2116
+ res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8', 'Cache-Control': 'no-cache' });
2117
+ res.end(injectedHtml);
2118
+ } catch (err) {
2119
+ res.writeHead(500, { 'Content-Type': 'text/html; charset=utf-8' });
2120
+ res.end('<h1>Error loading file</h1><p>' + err.message + '</p>');
2121
+ }
2122
+ return;
2123
+ }
2124
+
2125
+ res.writeHead(404, { 'Content-Type': 'text/html; charset=utf-8' });
2126
+ res.end('<h1>404 Not Found</h1>');
2127
+ });
2128
+
2129
+ let watcher = null;
2130
+
2131
+ const handleShutdown = () => {
2132
+ if (watcher) watcher.close();
2133
+ if (debounceTimer) clearTimeout(debounceTimer);
2134
+ for (const client of sseClients) {
2135
+ try { client.end(); } catch {}
2136
+ }
2137
+ sseClients.clear();
2138
+ try { if (fs.existsSync(pidFile)) fs.unlinkSync(pidFile); } catch {}
2139
+ try { if (fs.existsSync(portFile)) fs.unlinkSync(portFile); } catch {}
2140
+ server.close(() => process.exit(0));
2141
+ setTimeout(() => process.exit(0), 1000);
2142
+ };
2143
+
2144
+ process.on('SIGTERM', handleShutdown);
2145
+ process.on('SIGINT', handleShutdown);
2146
+
2147
+ server.listen(0, '127.0.0.1', () => {
2148
+ const port = server.address().port;
2149
+ fs.writeFileSync(pidFile, process.pid.toString(), 'utf-8');
2150
+ fs.writeFileSync(portFile, port.toString(), 'utf-8');
2151
+
2152
+ try {
2153
+ watcher = fs.watch(filePath, (eventType) => {
2154
+ if (eventType === 'change') handleFileChange();
2155
+ });
2156
+ } catch {}
2157
+ });
2158
+ `;
2159
+ const child = spawn(process.execPath, ["-e", serverCode], {
2160
+ detached: true,
2161
+ stdio: "ignore"
2162
+ });
2163
+ child.unref();
2164
+ let port = null;
2165
+ for (let i = 0; i < 50; i++) {
2166
+ await new Promise((resolve4) => setTimeout(resolve4, 100));
2167
+ if (fs3.existsSync(PORT_FILE2)) {
2168
+ try {
2169
+ port = parseInt(fs3.readFileSync(PORT_FILE2, "utf-8").trim(), 10);
2170
+ if (!isNaN(port)) break;
2171
+ } catch {
2172
+ }
2173
+ }
2174
+ }
2175
+ if (!port) {
2176
+ error("Failed to start preview server");
2177
+ process.exit(1);
2178
+ }
2179
+ const previewUrl = `http://127.0.0.1:${port}/`;
2180
+ const browserOpened = await openBrowser(previewUrl);
2181
+ if (!browserOpened) {
2182
+ info(`Could not open browser. Visit: ${previewUrl}`);
2183
+ }
2184
+ success("Preview server started in background");
2185
+ console.log(` URL: ${previewUrl}`);
2186
+ console.log(` PID: ${child.pid}`);
2187
+ console.log("");
2188
+ info("To update the blog: lindoai blogs update <website_id> <blog_id> --html-file <path>");
2189
+ info("To stop the server: lindoai blogs stop-preview");
2190
+ }
2191
+ async function startForegroundPreviewServer2(filePath) {
2192
+ const { startLivePreviewServer: startLivePreviewServer2 } = await Promise.resolve().then(() => (init_live_preview_server(), live_preview_server_exports));
2193
+ info("Starting preview server...");
2194
+ const port = await startLivePreviewServer2(filePath);
2195
+ const previewUrl = `http://127.0.0.1:${port}/`;
2196
+ fs3.writeFileSync(PID_FILE2, process.pid.toString(), "utf-8");
2197
+ fs3.writeFileSync(PORT_FILE2, port.toString(), "utf-8");
2198
+ const browserOpened = await openBrowser(previewUrl);
2199
+ if (!browserOpened) {
2200
+ info(`Could not open browser. Visit: ${previewUrl}`);
2201
+ }
2202
+ success("Preview server started");
2203
+ console.log(` URL: ${previewUrl}`);
2204
+ console.log("");
2205
+ info("Press Ctrl+C to stop the server");
2206
+ info("Edit the HTML file and save to see changes in the browser");
2207
+ }
2208
+ function generateState() {
2209
+ return crypto.randomBytes(32).toString("hex");
2210
+ }
2211
+ function verifyState(received, expected) {
2212
+ return received === expected;
2213
+ }
2214
+
2215
+ // src/server/callback.ts
2216
+ function parseCallbackParams(url) {
2217
+ try {
2218
+ const fullUrl = url.startsWith("http") ? url : `http://localhost${url}`;
2219
+ const parsed = new URL(fullUrl);
2220
+ const params = parsed.searchParams;
2221
+ return {
2222
+ key: params.get("key") ?? void 0,
2223
+ state: params.get("state") ?? void 0,
2224
+ error: params.get("error") ?? void 0,
2225
+ message: params.get("message") ?? void 0
2226
+ };
2227
+ } catch {
2228
+ return {};
2229
+ }
2230
+ }
2231
+ function generateSuccessHtml() {
2232
+ return `<!DOCTYPE html>
2233
+ <html lang="en">
2234
+ <head>
2235
+ <meta charset="UTF-8">
2236
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
2237
+ <title>Authorization Successful - Lindo CLI</title>
2238
+ <style>
2239
+ body {
2240
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
2241
+ display: flex;
2242
+ justify-content: center;
2243
+ align-items: center;
2244
+ min-height: 100vh;
2245
+ margin: 0;
2246
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
2247
+ color: #fff;
2248
+ }
2249
+ .container {
2250
+ text-align: center;
2251
+ padding: 2rem;
2252
+ background: rgba(255, 255, 255, 0.1);
2253
+ border-radius: 16px;
2254
+ backdrop-filter: blur(10px);
2255
+ max-width: 400px;
2256
+ }
2257
+ .icon {
2258
+ font-size: 4rem;
2259
+ margin-bottom: 1rem;
2260
+ }
2261
+ h1 {
2262
+ margin: 0 0 1rem 0;
2263
+ font-size: 1.5rem;
2264
+ }
2265
+ p {
2266
+ margin: 0;
2267
+ opacity: 0.9;
2268
+ }
2269
+ </style>
2270
+ </head>
2271
+ <body>
2272
+ <div class="container">
2273
+ <div class="icon">\u2713</div>
2274
+ <h1>Authorization Successful</h1>
2275
+ <p>You can close this window and return to your terminal.</p>
2276
+ </div>
2277
+ </body>
2278
+ </html>`;
2279
+ }
2280
+ function generateErrorHtml(errorMessage) {
2281
+ const escapedMessage = errorMessage.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#039;");
2282
+ return `<!DOCTYPE html>
2283
+ <html lang="en">
2284
+ <head>
2285
+ <meta charset="UTF-8">
2286
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
2287
+ <title>Authorization Failed - Lindo CLI</title>
2288
+ <style>
2289
+ body {
2290
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
2291
+ display: flex;
2292
+ justify-content: center;
2293
+ align-items: center;
2294
+ min-height: 100vh;
2295
+ margin: 0;
2296
+ background: linear-gradient(135deg, #e74c3c 0%, #c0392b 100%);
2297
+ color: #fff;
2298
+ }
2299
+ .container {
2300
+ text-align: center;
2301
+ padding: 2rem;
2302
+ background: rgba(255, 255, 255, 0.1);
2303
+ border-radius: 16px;
2304
+ backdrop-filter: blur(10px);
2305
+ max-width: 400px;
2306
+ }
2307
+ .icon {
2308
+ font-size: 4rem;
2309
+ margin-bottom: 1rem;
2310
+ }
2311
+ h1 {
2312
+ margin: 0 0 1rem 0;
2313
+ font-size: 1.5rem;
2314
+ }
2315
+ p {
2316
+ margin: 0;
2317
+ opacity: 0.9;
2318
+ }
2319
+ .error-message {
2320
+ margin-top: 1rem;
2321
+ padding: 1rem;
2322
+ background: rgba(0, 0, 0, 0.2);
2323
+ border-radius: 8px;
2324
+ font-family: monospace;
2325
+ font-size: 0.9rem;
2326
+ }
2327
+ </style>
2328
+ </head>
2329
+ <body>
2330
+ <div class="container">
2331
+ <div class="icon">\u2717</div>
2332
+ <h1>Authorization Failed</h1>
2333
+ <p>Something went wrong during authorization.</p>
2334
+ <div class="error-message">${escapedMessage}</div>
2335
+ </div>
2336
+ </body>
2337
+ </html>`;
2338
+ }
2339
+ function generate404Html() {
2340
+ return `<!DOCTYPE html>
2341
+ <html lang="en">
2342
+ <head>
2343
+ <meta charset="UTF-8">
2344
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
2345
+ <title>Not Found - Lindo CLI</title>
2346
+ <style>
2347
+ body {
2348
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
2349
+ display: flex;
2350
+ justify-content: center;
2351
+ align-items: center;
2352
+ min-height: 100vh;
2353
+ margin: 0;
2354
+ background: #f5f5f5;
2355
+ color: #333;
2356
+ }
2357
+ .container {
2358
+ text-align: center;
2359
+ padding: 2rem;
2360
+ }
2361
+ h1 {
2362
+ font-size: 3rem;
2363
+ margin: 0 0 1rem 0;
2364
+ color: #999;
2365
+ }
2366
+ p {
2367
+ margin: 0;
2368
+ color: #666;
2369
+ }
2370
+ </style>
2371
+ </head>
2372
+ <body>
2373
+ <div class="container">
2374
+ <h1>404</h1>
2375
+ <p>This endpoint is not available.</p>
2376
+ </div>
2377
+ </body>
2378
+ </html>`;
2379
+ }
2380
+ function createCallbackServer() {
2381
+ let server = null;
2382
+ let pendingCallback = null;
2383
+ function handleRequest(req, res) {
2384
+ const url = req.url || "/";
2385
+ const method = req.method || "GET";
2386
+ const isCallbackPath = url === "/callback" || url.startsWith("/callback?");
2387
+ if (method !== "GET" || !isCallbackPath) {
2388
+ res.writeHead(404, { "Content-Type": "text/html; charset=utf-8" });
2389
+ res.end(generate404Html());
2390
+ return;
2391
+ }
2392
+ const params = parseCallbackParams(url);
2393
+ if (!pendingCallback) {
2394
+ res.writeHead(400, { "Content-Type": "text/html; charset=utf-8" });
2395
+ res.end(generateErrorHtml("No pending authorization request"));
2396
+ return;
2397
+ }
2398
+ const { expectedState, resolve: resolve4, timeoutId } = pendingCallback;
2399
+ clearTimeout(timeoutId);
2400
+ pendingCallback = null;
2401
+ if (params.error) {
2402
+ const errorMessage = params.message || params.error;
2403
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
2404
+ res.end(generateErrorHtml(errorMessage));
2405
+ resolve4({
2406
+ success: false,
2407
+ error: errorMessage
2408
+ });
2409
+ return;
2410
+ }
2411
+ if (!params.state || !verifyState(params.state, expectedState)) {
2412
+ const errorMessage = "State token mismatch - possible CSRF attack";
2413
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
2414
+ res.end(generateErrorHtml(errorMessage));
2415
+ resolve4({
2416
+ success: false,
2417
+ error: errorMessage
2418
+ });
2419
+ return;
2420
+ }
2421
+ if (!params.key) {
2422
+ const errorMessage = "No API key received";
2423
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
2424
+ res.end(generateErrorHtml(errorMessage));
2425
+ resolve4({
2426
+ success: false,
2427
+ error: errorMessage
2428
+ });
2429
+ return;
2430
+ }
2431
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
2432
+ res.end(generateSuccessHtml());
2433
+ resolve4({
2434
+ success: true,
2435
+ apiKey: params.key
2436
+ });
2437
+ }
2438
+ return {
2439
+ async start() {
2440
+ return new Promise((resolve4, reject) => {
2441
+ server = http.createServer(handleRequest);
2442
+ server.on("error", (err) => {
2443
+ reject(new Error(`Failed to start callback server: ${err.message}`));
2444
+ });
2445
+ server.listen(0, "127.0.0.1", () => {
2446
+ const address = server.address();
2447
+ if (!address || typeof address === "string") {
2448
+ reject(new Error("Failed to get server address"));
2449
+ return;
2450
+ }
2451
+ const port = address.port;
2452
+ const url = `http://127.0.0.1:${port}/callback`;
2453
+ resolve4({ port, url });
2454
+ });
2455
+ });
2456
+ },
2457
+ waitForCallback(state, timeoutMs) {
2458
+ return new Promise((resolve4) => {
2459
+ const timeoutId = setTimeout(() => {
2460
+ if (pendingCallback) {
2461
+ pendingCallback = null;
2462
+ resolve4({
2463
+ success: false,
2464
+ error: "Login timed out waiting for authorization"
2465
+ });
2466
+ }
2467
+ }, timeoutMs);
2468
+ pendingCallback = {
2469
+ expectedState: state,
2470
+ resolve: resolve4,
2471
+ timeoutId
2472
+ };
2473
+ });
2474
+ },
2475
+ async stop() {
2476
+ return new Promise((resolve4) => {
2477
+ if (pendingCallback) {
2478
+ clearTimeout(pendingCallback.timeoutId);
2479
+ pendingCallback = null;
2480
+ }
2481
+ if (server) {
2482
+ server.close(() => {
2483
+ server = null;
2484
+ resolve4();
2485
+ });
2486
+ } else {
2487
+ resolve4();
2488
+ }
2489
+ });
2490
+ }
2491
+ };
2492
+ }
2493
+
2494
+ // src/commands/login.ts
2495
+ var WEBAPP_BASE_URL = "https://app.lindo.ai";
2496
+ var DEFAULT_TIMEOUT_SECONDS = 120;
2497
+ function buildAuthorizationUrl(state, callbackPort) {
2498
+ const callbackUrl = `http://127.0.0.1:${callbackPort}/callback`;
2499
+ const params = new URLSearchParams({
2500
+ state,
2501
+ callback_url: callbackUrl
2502
+ });
2503
+ return `${WEBAPP_BASE_URL}/cli/authorize?${params.toString()}`;
2504
+ }
2505
+ function createLoginCommand() {
2506
+ const login = new Command("login").description("Authenticate via browser to configure your API key").option(
2507
+ "-t, --timeout <seconds>",
2508
+ "Timeout in seconds for the login flow",
2509
+ String(DEFAULT_TIMEOUT_SECONDS)
2510
+ ).option(
2511
+ "--no-browser",
2512
+ "Display the authorization URL without opening the browser"
2513
+ ).action(async (options) => {
2514
+ const timeoutSeconds = parseInt(options.timeout, 10);
2515
+ const timeoutMs = timeoutSeconds * 1e3;
2516
+ const shouldOpenBrowser = options.browser;
2517
+ const server = createCallbackServer();
2518
+ let serverStarted = false;
2519
+ try {
2520
+ info("Starting authentication flow...");
2521
+ const { port } = await server.start();
2522
+ serverStarted = true;
2523
+ const state = generateState();
2524
+ const authUrl = buildAuthorizationUrl(state, port);
2525
+ if (shouldOpenBrowser) {
2526
+ info("Opening browser for authorization...");
2527
+ const browserOpened = await openBrowser(authUrl);
2528
+ if (!browserOpened) {
2529
+ info("Could not open browser automatically.");
2530
+ console.log("\nPlease open this URL in your browser:");
2531
+ console.log(`
2532
+ ${authUrl}
2533
+ `);
2534
+ }
2535
+ } else {
2536
+ console.log("\nPlease open this URL in your browser:");
2537
+ console.log(`
2538
+ ${authUrl}
2539
+ `);
2540
+ }
2541
+ info(`Waiting for authorization (timeout: ${timeoutSeconds}s)...`);
2542
+ const result = await server.waitForCallback(state, timeoutMs);
2543
+ if (result.success && result.apiKey) {
2544
+ saveApiKey(result.apiKey);
2545
+ success("Successfully authenticated!");
2546
+ info(`API key saved to: ${getConfigPath()}`);
2547
+ } else {
2548
+ error(result.error || "Authentication failed");
2549
+ process.exit(1);
2550
+ }
2551
+ } catch (err) {
2552
+ error(err instanceof Error ? err.message : "An unexpected error occurred");
2553
+ process.exit(1);
2554
+ } finally {
2555
+ if (serverStarted) {
2556
+ await server.stop();
2557
+ }
2558
+ }
2559
+ });
2560
+ return login;
2561
+ }
2562
+ var OPENCODE_SKILLS_DIR = path3.join(os2.homedir(), ".config", "opencode", "skills", "lindoai");
2563
+ var SKILL_FILE_PATH = path3.join(OPENCODE_SKILLS_DIR, "SKILL.md");
2564
+ var SKILL_FILE_CONTENT = `---
2565
+ name: lindoai
2566
+ description: Lindo AI CLI - Command-line interface for the Lindo API
2567
+ ---
2568
+
2569
+ # Lindo AI CLI
2570
+
2571
+ The \`lindoai\` CLI provides commands for interacting with the Lindo API, including website management, page editing, blog management, and more.
2572
+
2573
+ ## Authentication
2574
+
2575
+ ### Login via Browser
2576
+ \`\`\`bash
2577
+ lindoai login
2578
+ \`\`\`
2579
+ Opens your browser to authenticate and automatically configures your API key.
2580
+
2581
+ Options:
2582
+ - \`--timeout <seconds>\` - Timeout for the login flow (default: 120)
2583
+ - \`--no-browser\` - Display the authorization URL without opening the browser
2584
+
2585
+ ### Manual Configuration
2586
+ \`\`\`bash
2587
+ lindoai config set apiKey <your-api-key>
2588
+ \`\`\`
2589
+
2590
+ ## Website Management
2591
+
2592
+ ### List Websites
2593
+ \`\`\`bash
2594
+ lindoai websites list
2595
+ \`\`\`
2596
+
2597
+ ### Get Website Details
2598
+ \`\`\`bash
2599
+ lindoai websites get <website_id>
2600
+ \`\`\`
2601
+
2602
+ ## Page Management
2603
+
2604
+ ### List Pages
2605
+ \`\`\`bash
2606
+ lindoai pages list <website_id>
2607
+ \`\`\`
2608
+
2609
+ ### Get Page Details
2610
+ \`\`\`bash
2611
+ lindoai pages get <website_id> <page_id>
2612
+ \`\`\`
2613
+
2614
+ ### Live Preview Editing Workflow
2615
+
2616
+ 1. **Start editing with live preview:**
2617
+ \`\`\`bash
2618
+ lindoai pages edit <website_id> <page_id> --background
2619
+ \`\`\`
2620
+ This fetches the page HTML, saves it locally, starts a preview server, and opens your browser.
2621
+
2622
+ Options:
2623
+ - \`--file <path>\` - Save HTML to a specific path (default: ./page.html)
2624
+ - \`--background\` - Run the preview server in the background
2625
+
2626
+ 2. **Edit the HTML file** in your editor. The browser will automatically refresh when you save.
2627
+
2628
+ 3. **Update the page** when you're done:
2629
+ \`\`\`bash
2630
+ lindoai pages update <website_id> <page_id> --html-file ./page.html
2631
+ \`\`\`
2632
+
2633
+ 4. **Stop the preview server:**
2634
+ \`\`\`bash
2635
+ lindoai pages stop-preview
2636
+ \`\`\`
2637
+
2638
+ ## Blog Management
2639
+
2640
+ ### List Blogs
2641
+ \`\`\`bash
2642
+ lindoai blogs list <website_id>
2643
+ \`\`\`
2644
+
2645
+ ### Get Blog Details
2646
+ \`\`\`bash
2647
+ lindoai blogs get <website_id> <blog_id>
2648
+ \`\`\`
2649
+
2650
+ ### Live Preview Editing Workflow
2651
+
2652
+ 1. **Start editing with live preview:**
2653
+ \`\`\`bash
2654
+ lindoai blogs edit <website_id> <blog_id> --background
2655
+ \`\`\`
2656
+
2657
+ Options:
2658
+ - \`--file <path>\` - Save HTML to a specific path (default: ./blog.html)
2659
+ - \`--background\` - Run the preview server in the background
2660
+
2661
+ 2. **Edit the HTML file** in your editor. The browser will automatically refresh when you save.
2662
+
2663
+ 3. **Update the blog** when you're done:
2664
+ \`\`\`bash
2665
+ lindoai blogs update <website_id> <blog_id> --html-file ./blog.html
2666
+ \`\`\`
2667
+
2668
+ 4. **Stop the preview server:**
2669
+ \`\`\`bash
2670
+ lindoai blogs stop-preview
2671
+ \`\`\`
2672
+
2673
+ ## AI Agents
2674
+
2675
+ ### Run an Agent
2676
+ \`\`\`bash
2677
+ lindoai agents run <agent_id> --input '{"prompt": "Hello!"}'
2678
+ \`\`\`
2679
+
2680
+ Options:
2681
+ - \`--input <json>\` - Input data as JSON string
2682
+ - \`--stream\` - Stream the response
2683
+ - \`--format <format>\` - Output format (json, table)
2684
+
2685
+ ## Workflows
2686
+
2687
+ ### Start a Workflow
2688
+ \`\`\`bash
2689
+ lindoai workflows start <workflow_id> --params '{"key": "value"}'
2690
+ \`\`\`
2691
+
2692
+ ### Get Workflow Status
2693
+ \`\`\`bash
2694
+ lindoai workflows status <instance_id>
2695
+ \`\`\`
2696
+
2697
+ ## Workspace
2698
+
2699
+ ### Get Workspace Credits
2700
+ \`\`\`bash
2701
+ lindoai workspace credits
2702
+ \`\`\`
2703
+
2704
+ ## Analytics
2705
+
2706
+ ### Get Workspace Analytics
2707
+ \`\`\`bash
2708
+ lindoai analytics workspace --from 2024-01-01 --to 2024-01-31
2709
+ \`\`\`
2710
+
2711
+ ## Configuration
2712
+
2713
+ ### View Configuration
2714
+ \`\`\`bash
2715
+ lindoai config get apiKey
2716
+ lindoai config get baseUrl
2717
+ \`\`\`
2718
+
2719
+ ### Set Configuration
2720
+ \`\`\`bash
2721
+ lindoai config set apiKey <value>
2722
+ lindoai config set baseUrl <value>
2723
+ \`\`\`
2724
+
2725
+ ## Tips for AI Agents
2726
+
2727
+ 1. **Always authenticate first** using \`lindoai login\` or by setting the API key.
2728
+ 2. **Use the live preview workflow** for editing pages and blogs - it provides instant visual feedback.
2729
+ 3. **Run preview servers in background mode** (\`--background\`) to keep the terminal available.
2730
+ 4. **Remember to stop preview servers** when done editing with \`stop-preview\`.
2731
+ 5. **Use JSON format** (\`--format json\`) when you need to parse command output programmatically.
2732
+ `;
2733
+ function isOpenCodeInstalled() {
2734
+ try {
2735
+ const command = process.platform === "win32" ? "where opencode" : "which opencode";
2736
+ execSync(command, { stdio: "ignore" });
2737
+ return true;
2738
+ } catch {
2739
+ return false;
2740
+ }
2741
+ }
2742
+ function installOpenCode() {
2743
+ try {
2744
+ info("Installing OpenCode...");
2745
+ execSync("npm install -g opencode-ai@latest", { stdio: "inherit" });
2746
+ return true;
2747
+ } catch {
2748
+ return false;
2749
+ }
2750
+ }
2751
+ function ensureSkillInstalled() {
2752
+ if (!fs3.existsSync(OPENCODE_SKILLS_DIR)) {
2753
+ fs3.mkdirSync(OPENCODE_SKILLS_DIR, { recursive: true });
2754
+ }
2755
+ fs3.writeFileSync(SKILL_FILE_PATH, SKILL_FILE_CONTENT, "utf-8");
2756
+ }
2757
+ function createAgentCommand() {
2758
+ const agent = new Command("agent").description("Launch an AI agent that understands all CLI commands").option("--install", "Install OpenCode if not already installed").option("--model <model>", "Specify the model to use with OpenCode").action(async (options) => {
2759
+ if (!isOpenCodeInstalled()) {
2760
+ if (options.install) {
2761
+ const installed = installOpenCode();
2762
+ if (!installed) {
2763
+ error("Failed to install OpenCode");
2764
+ info("Please install manually: npm install -g opencode-ai@latest");
2765
+ process.exit(1);
2766
+ }
2767
+ success("OpenCode installed successfully");
2768
+ } else {
2769
+ error("OpenCode is not installed");
2770
+ info("");
2771
+ info("To install OpenCode, run one of the following:");
2772
+ info(" lindoai agent --install");
2773
+ info(" npm install -g opencode-ai@latest");
2774
+ process.exit(1);
2775
+ }
2776
+ }
2777
+ try {
2778
+ ensureSkillInstalled();
2779
+ info(`Skill file installed to: ${SKILL_FILE_PATH}`);
2780
+ } catch (err) {
2781
+ error(`Failed to install skill file: ${err instanceof Error ? err.message : "Unknown error"}`);
2782
+ process.exit(1);
2783
+ }
2784
+ const args = [];
2785
+ if (options.model) {
2786
+ args.push("--model", options.model);
2787
+ }
2788
+ info("Launching OpenCode...");
2789
+ const opencode = spawn("opencode", args, {
2790
+ stdio: "inherit",
2791
+ shell: true
2792
+ });
2793
+ opencode.on("error", (err) => {
2794
+ error(`Failed to launch OpenCode: ${err.message}`);
2795
+ process.exit(1);
2796
+ });
2797
+ opencode.on("close", (code) => {
2798
+ process.exit(code ?? 0);
2799
+ });
2800
+ });
2801
+ return agent;
2802
+ }
2803
+
2804
+ // src/index.ts
2805
+ var VERSION = "1.0.0";
2806
+ function createProgram() {
2807
+ const program = new Command();
2808
+ program.name("lindo").description("Command-line interface for the Lindo API").version(VERSION, "-v, --version", "Output the current version").helpOption("-h, --help", "Display help for command");
2809
+ program.addCommand(createConfigCommand());
2810
+ program.addCommand(createAgentsCommand());
2811
+ program.addCommand(createWorkflowsCommand());
2812
+ program.addCommand(createWorkspaceCommand());
2813
+ program.addCommand(createAnalyticsCommand());
2814
+ program.addCommand(createClientsCommand());
2815
+ program.addCommand(createWebsitesCommand());
2816
+ program.addCommand(createPagesCommand());
2817
+ program.addCommand(createBlogsCommand());
2818
+ program.addCommand(createLoginCommand());
2819
+ program.addCommand(createAgentCommand());
2820
+ program.exitOverride((err) => {
2821
+ if (err.code === "commander.help") {
2822
+ process.exit(0);
2823
+ }
2824
+ if (err.code === "commander.version") {
2825
+ process.exit(0);
2826
+ }
2827
+ if (err.code === "commander.missingArgument") {
2828
+ error(err.message);
2829
+ process.exit(1);
2830
+ }
2831
+ if (err.code === "commander.unknownCommand") {
2832
+ error(err.message);
2833
+ info('Run "lindo --help" for available commands');
2834
+ process.exit(1);
2835
+ }
2836
+ throw err;
2837
+ });
2838
+ return program;
2839
+ }
2840
+ function handleAuthenticationError(_err) {
2841
+ error("Authentication failed");
2842
+ info("");
2843
+ info("Your API key may be invalid or expired.");
2844
+ info("");
2845
+ info("To configure your API key:");
2846
+ info(" 1. Run: lindo config set apiKey <your-api-key>");
2847
+ info(" 2. Or set the LINDO_API_KEY environment variable");
2848
+ info("");
2849
+ info("To get an API key:");
2850
+ info(" Visit https://app.lindo.ai/settings/api-keys");
2851
+ }
2852
+ async function main() {
2853
+ const program = createProgram();
2854
+ try {
2855
+ await program.parseAsync(process.argv);
2856
+ } catch (err) {
2857
+ if (err instanceof AuthenticationError) {
2858
+ handleAuthenticationError();
2859
+ process.exit(1);
2860
+ }
2861
+ throw err;
2862
+ }
2863
+ }
2864
+ main().catch((err) => {
2865
+ if (err instanceof Error) {
2866
+ error(`Unexpected error: ${err.message}`);
2867
+ } else {
2868
+ error("An unexpected error occurred");
2869
+ }
2870
+ process.exit(1);
2871
+ });
2872
+
2873
+ export { createProgram };
2874
+ //# sourceMappingURL=index.js.map
2875
+ //# sourceMappingURL=index.js.map