@spike-forms/cli 0.2.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,2287 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from 'commander';
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 chalk from 'chalk';
8
+ import { SpikeClient, SpikeError } from '@spike-forms/sdk';
9
+ import { spawn, exec, execSync } from 'child_process';
10
+ import * as http from 'http';
11
+ import * as crypto from 'crypto';
12
+
13
+ var DEFAULT_CONFIG = {
14
+ apiKey: "",
15
+ baseUrl: "https://api.spike.ac"
16
+ };
17
+ var ENV_VARS = {
18
+ API_KEY: "SPIKE_API_KEY",
19
+ TOKEN: "SPIKE_TOKEN",
20
+ API_URL: "SPIKE_API_URL"
21
+ };
22
+ function getConfigPath() {
23
+ const homeDir = os2.homedir();
24
+ return path3.join(homeDir, ".spike", "config.json");
25
+ }
26
+ function getConfigDir() {
27
+ const homeDir = os2.homedir();
28
+ return path3.join(homeDir, ".spike");
29
+ }
30
+ function loadConfigFromFile() {
31
+ const configPath = getConfigPath();
32
+ try {
33
+ if (!fs3.existsSync(configPath)) {
34
+ return {};
35
+ }
36
+ const fileContent = fs3.readFileSync(configPath, "utf-8");
37
+ const parsed = JSON.parse(fileContent);
38
+ const config = {};
39
+ if (typeof parsed.apiKey === "string") {
40
+ config.apiKey = parsed.apiKey;
41
+ }
42
+ if (typeof parsed.baseUrl === "string") {
43
+ config.baseUrl = parsed.baseUrl;
44
+ }
45
+ return config;
46
+ } catch {
47
+ return {};
48
+ }
49
+ }
50
+ function loadConfigFromEnv() {
51
+ const config = {};
52
+ const apiKey = process.env[ENV_VARS.API_KEY] || process.env[ENV_VARS.TOKEN];
53
+ if (apiKey) {
54
+ config.apiKey = apiKey;
55
+ }
56
+ const apiUrl = process.env[ENV_VARS.API_URL];
57
+ if (apiUrl) {
58
+ config.baseUrl = apiUrl;
59
+ }
60
+ return config;
61
+ }
62
+ function loadConfig() {
63
+ const fileConfig = loadConfigFromFile();
64
+ const envConfig = loadConfigFromEnv();
65
+ return {
66
+ ...DEFAULT_CONFIG,
67
+ ...fileConfig,
68
+ ...envConfig
69
+ };
70
+ }
71
+ function saveConfig(config) {
72
+ const configDir = getConfigDir();
73
+ const configPath = getConfigPath();
74
+ if (!fs3.existsSync(configDir)) {
75
+ fs3.mkdirSync(configDir, { recursive: true });
76
+ }
77
+ const existingConfig = loadConfigFromFile();
78
+ const mergedConfig = {
79
+ ...existingConfig,
80
+ ...config
81
+ };
82
+ const cleanConfig = {};
83
+ if (mergedConfig.apiKey) {
84
+ cleanConfig.apiKey = mergedConfig.apiKey;
85
+ }
86
+ if (mergedConfig.baseUrl) {
87
+ cleanConfig.baseUrl = mergedConfig.baseUrl;
88
+ }
89
+ fs3.writeFileSync(configPath, JSON.stringify(cleanConfig, null, 2) + "\n", "utf-8");
90
+ }
91
+ function output(data, format = "table") {
92
+ if (format === "json") {
93
+ outputJson(data);
94
+ } else {
95
+ outputTable(data);
96
+ }
97
+ }
98
+ function outputJson(data) {
99
+ console.log(JSON.stringify(data, null, 2));
100
+ }
101
+ function outputTable(data) {
102
+ if (data === null || data === void 0) {
103
+ console.log("No data");
104
+ return;
105
+ }
106
+ if (Array.isArray(data)) {
107
+ outputArrayAsTable(data);
108
+ } else if (typeof data === "object") {
109
+ outputObjectAsTable(data);
110
+ } else {
111
+ console.log(String(data));
112
+ }
113
+ }
114
+ function outputArrayAsTable(data) {
115
+ if (data.length === 0) {
116
+ console.log("No items found");
117
+ return;
118
+ }
119
+ const keys = /* @__PURE__ */ new Set();
120
+ for (const item of data) {
121
+ if (item && typeof item === "object") {
122
+ Object.keys(item).forEach((key) => keys.add(key));
123
+ }
124
+ }
125
+ const columns = Array.from(keys);
126
+ if (columns.length === 0) {
127
+ data.forEach((item) => console.log(String(item)));
128
+ return;
129
+ }
130
+ const columnWidths = /* @__PURE__ */ new Map();
131
+ for (const col of columns) {
132
+ let maxWidth = col.length;
133
+ for (const item of data) {
134
+ if (item && typeof item === "object") {
135
+ const value = formatCellValue(item[col]);
136
+ maxWidth = Math.max(maxWidth, value.length);
137
+ }
138
+ }
139
+ columnWidths.set(col, Math.min(maxWidth, 50));
140
+ }
141
+ const headerRow = columns.map((col) => padRight(col.toUpperCase(), columnWidths.get(col))).join(" ");
142
+ console.log(chalk.bold(headerRow));
143
+ const separator = columns.map((col) => "-".repeat(columnWidths.get(col))).join(" ");
144
+ console.log(separator);
145
+ for (const item of data) {
146
+ if (item && typeof item === "object") {
147
+ const row = columns.map((col) => {
148
+ const value = formatCellValue(item[col]);
149
+ return padRight(truncate(value, columnWidths.get(col)), columnWidths.get(col));
150
+ }).join(" ");
151
+ console.log(row);
152
+ }
153
+ }
154
+ console.log("");
155
+ console.log(chalk.dim(`${data.length} item${data.length === 1 ? "" : "s"}`));
156
+ }
157
+ function outputObjectAsTable(data) {
158
+ const entries = Object.entries(data);
159
+ if (entries.length === 0) {
160
+ console.log("No data");
161
+ return;
162
+ }
163
+ const maxKeyLength = Math.max(...entries.map(([key]) => key.length));
164
+ for (const [key, value] of entries) {
165
+ const formattedKey = chalk.bold(padRight(key, maxKeyLength));
166
+ const formattedValue = formatCellValue(value);
167
+ console.log(`${formattedKey} ${formattedValue}`);
168
+ }
169
+ }
170
+ function formatCellValue(value) {
171
+ if (value === null || value === void 0) {
172
+ return "-";
173
+ }
174
+ if (typeof value === "boolean") {
175
+ return value ? chalk.green("yes") : chalk.dim("no");
176
+ }
177
+ if (typeof value === "object") {
178
+ if (Array.isArray(value)) {
179
+ return `[${value.length} items]`;
180
+ }
181
+ return JSON.stringify(value);
182
+ }
183
+ return String(value);
184
+ }
185
+ function padRight(str, length) {
186
+ const visibleLength = stripAnsi(str).length;
187
+ if (visibleLength >= length) {
188
+ return str;
189
+ }
190
+ return str + " ".repeat(length - visibleLength);
191
+ }
192
+ function truncate(str, maxLength) {
193
+ const visibleLength = stripAnsi(str).length;
194
+ if (visibleLength <= maxLength) {
195
+ return str;
196
+ }
197
+ return str.slice(0, maxLength - 3) + "...";
198
+ }
199
+ function stripAnsi(str) {
200
+ return str.replace(/\x1B\[[0-9;]*[a-zA-Z]/g, "");
201
+ }
202
+ function success(message) {
203
+ console.log(chalk.green(`\u2713 ${message}`));
204
+ }
205
+ function error(message) {
206
+ console.error(chalk.red(`\u2717 ${message}`));
207
+ }
208
+ function warn(message) {
209
+ console.log(chalk.yellow(`\u26A0 ${message}`));
210
+ }
211
+ function info(message) {
212
+ console.log(chalk.cyan(`\u2139 ${message}`));
213
+ }
214
+ function maskApiKey(apiKey) {
215
+ if (!apiKey) {
216
+ return "";
217
+ }
218
+ if (apiKey.length <= 11) {
219
+ const visibleStart = Math.min(4, apiKey.length - 1);
220
+ return apiKey.slice(0, visibleStart) + "...";
221
+ }
222
+ const prefix = apiKey.slice(0, 7);
223
+ const suffix = apiKey.slice(-4);
224
+ return `${prefix}...${suffix}`;
225
+ }
226
+
227
+ // src/commands/config.ts
228
+ var VALID_KEYS = ["api-key", "base-url"];
229
+ var KEY_MAP = {
230
+ "api-key": "apiKey",
231
+ "base-url": "baseUrl"
232
+ };
233
+ function isValidKey(key) {
234
+ return VALID_KEYS.includes(key);
235
+ }
236
+ function createConfigCommand() {
237
+ const configCommand = new Command("config").description("Manage CLI configuration");
238
+ configCommand.command("set").description("Set a configuration value").argument("<key>", `Configuration key (${VALID_KEYS.join(", ")})`).argument("<value>", "Configuration value").action((key, value) => {
239
+ handleSet(key, value);
240
+ });
241
+ configCommand.command("get").description("Get configuration value(s)").argument("[key]", `Configuration key (${VALID_KEYS.join(", ")})`).option("-f, --format <format>", "Output format (json, table)", "table").action((key, options) => {
242
+ handleGet(key, options.format);
243
+ });
244
+ return configCommand;
245
+ }
246
+ function handleSet(key, value) {
247
+ if (!isValidKey(key)) {
248
+ error(`Invalid configuration key: ${key}`);
249
+ info(`Valid keys are: ${VALID_KEYS.join(", ")}`);
250
+ process.exit(1);
251
+ }
252
+ const internalKey = KEY_MAP[key];
253
+ try {
254
+ saveConfig({ [internalKey]: value });
255
+ success(`Configuration '${key}' has been set`);
256
+ } catch (err) {
257
+ error(`Failed to save configuration: ${err instanceof Error ? err.message : String(err)}`);
258
+ process.exit(1);
259
+ }
260
+ }
261
+ function handleGet(key, format) {
262
+ const config = loadConfig();
263
+ if (key !== void 0) {
264
+ if (!isValidKey(key)) {
265
+ error(`Invalid configuration key: ${key}`);
266
+ info(`Valid keys are: ${VALID_KEYS.join(", ")}`);
267
+ process.exit(1);
268
+ }
269
+ const internalKey = KEY_MAP[key];
270
+ let value = config[internalKey];
271
+ if (key === "api-key" && value) {
272
+ value = maskApiKey(value);
273
+ }
274
+ if (!value) {
275
+ info(`Configuration '${key}' is not set`);
276
+ return;
277
+ }
278
+ if (format === "json") {
279
+ output({ [key]: value }, format);
280
+ } else {
281
+ console.log(value);
282
+ }
283
+ } else {
284
+ const displayConfig = {};
285
+ if (config.apiKey) {
286
+ displayConfig["api-key"] = maskApiKey(config.apiKey);
287
+ } else {
288
+ displayConfig["api-key"] = "(not set)";
289
+ }
290
+ displayConfig["base-url"] = config.baseUrl || "(not set)";
291
+ info(`Configuration file: ${getConfigPath()}`);
292
+ console.log("");
293
+ output(displayConfig, format);
294
+ }
295
+ }
296
+ var SSE_SCRIPT = `<script>
297
+ (function() {
298
+ var es = new EventSource('/__live-reload');
299
+ es.onmessage = function(event) {
300
+ if (event.data === 'reload') {
301
+ location.reload();
302
+ }
303
+ };
304
+ es.onerror = function() {
305
+ console.log('[Live Preview] Connection lost, attempting to reconnect...');
306
+ };
307
+ })();
308
+ </script>`;
309
+ function injectLiveReload(html) {
310
+ const bodyCloseRegex = /<\/body>/i;
311
+ const match = html.match(bodyCloseRegex);
312
+ if (match && match.index !== void 0) {
313
+ return html.slice(0, match.index) + SSE_SCRIPT + html.slice(match.index);
314
+ }
315
+ return html + SSE_SCRIPT;
316
+ }
317
+ function startLivePreviewServer(filePath) {
318
+ return new Promise((resolve3, reject) => {
319
+ const absolutePath = path3.resolve(filePath);
320
+ const sseClients = [];
321
+ let debounceTimer = null;
322
+ function broadcastToClients(message) {
323
+ const data = `data: ${message}
324
+
325
+ `;
326
+ for (let i = sseClients.length - 1; i >= 0; i--) {
327
+ const client = sseClients[i];
328
+ if (!client) continue;
329
+ try {
330
+ if (!client.writableEnded) {
331
+ client.write(data);
332
+ } else {
333
+ sseClients.splice(i, 1);
334
+ }
335
+ } catch {
336
+ sseClients.splice(i, 1);
337
+ }
338
+ }
339
+ }
340
+ function onFileChange() {
341
+ if (debounceTimer) {
342
+ clearTimeout(debounceTimer);
343
+ }
344
+ debounceTimer = setTimeout(() => {
345
+ broadcastToClients("reload");
346
+ debounceTimer = null;
347
+ }, 100);
348
+ }
349
+ function handleRequest(req, res) {
350
+ const url = req.url || "/";
351
+ if (url === "/__live-reload") {
352
+ res.writeHead(200, {
353
+ "Content-Type": "text/event-stream",
354
+ "Cache-Control": "no-cache",
355
+ Connection: "keep-alive",
356
+ "Access-Control-Allow-Origin": "*"
357
+ });
358
+ res.write("data: connected\n\n");
359
+ sseClients.push(res);
360
+ req.on("close", () => {
361
+ const index = sseClients.indexOf(res);
362
+ if (index !== -1) {
363
+ sseClients.splice(index, 1);
364
+ }
365
+ });
366
+ return;
367
+ }
368
+ if (url === "/" && req.method === "GET") {
369
+ try {
370
+ const html = fs3.readFileSync(absolutePath, "utf-8");
371
+ const injectedHtml = injectLiveReload(html);
372
+ res.writeHead(200, {
373
+ "Content-Type": "text/html; charset=utf-8",
374
+ "Cache-Control": "no-cache"
375
+ });
376
+ res.end(injectedHtml);
377
+ } catch (err) {
378
+ const errorMessage = err instanceof Error ? err.message : "Unknown error";
379
+ res.writeHead(500, { "Content-Type": "text/plain" });
380
+ res.end(`Error reading file: ${errorMessage}`);
381
+ }
382
+ return;
383
+ }
384
+ res.writeHead(404, { "Content-Type": "text/plain" });
385
+ res.end("Not Found");
386
+ }
387
+ const server = http.createServer(handleRequest);
388
+ server.on("error", (err) => {
389
+ reject(err);
390
+ });
391
+ let watcher = null;
392
+ try {
393
+ watcher = fs3.watch(absolutePath, (eventType) => {
394
+ if (eventType === "change") {
395
+ onFileChange();
396
+ }
397
+ });
398
+ watcher.on("error", (err) => {
399
+ console.error("[Live Preview] File watcher error:", err.message);
400
+ });
401
+ } catch (err) {
402
+ console.warn("[Live Preview] Could not start file watcher:", err instanceof Error ? err.message : "Unknown error");
403
+ }
404
+ server.listen(0, "127.0.0.1", () => {
405
+ const address = server.address();
406
+ if (address && typeof address === "object") {
407
+ const port = address.port;
408
+ resolve3(port);
409
+ } else {
410
+ reject(new Error("Failed to get server address"));
411
+ }
412
+ });
413
+ });
414
+ }
415
+ async function openBrowser(url) {
416
+ const currentPlatform = platform();
417
+ let command;
418
+ switch (currentPlatform) {
419
+ case "darwin":
420
+ command = `open "${url}"`;
421
+ break;
422
+ case "linux":
423
+ command = `xdg-open "${url}"`;
424
+ break;
425
+ case "win32":
426
+ command = `start "" "${url}"`;
427
+ break;
428
+ default:
429
+ return false;
430
+ }
431
+ return new Promise((resolve3) => {
432
+ exec(command, (error2) => {
433
+ if (error2) {
434
+ resolve3(false);
435
+ } else {
436
+ resolve3(true);
437
+ }
438
+ });
439
+ });
440
+ }
441
+
442
+ // src/commands/forms.ts
443
+ function createClient() {
444
+ const config = loadConfig();
445
+ if (!config.apiKey) {
446
+ error("API key not configured. Run `spike config set api-key <your-key>` to set it.");
447
+ process.exit(1);
448
+ }
449
+ return new SpikeClient({
450
+ apiKey: config.apiKey,
451
+ baseUrl: config.baseUrl
452
+ });
453
+ }
454
+ function handleError(err) {
455
+ if (err instanceof SpikeError) {
456
+ error(err.message);
457
+ if (err.code) {
458
+ info(`Error code: ${err.code}`);
459
+ }
460
+ } else if (err instanceof Error) {
461
+ error(err.message);
462
+ } else {
463
+ error("An unexpected error occurred");
464
+ }
465
+ process.exit(1);
466
+ }
467
+ var PREVIEW_PID_FILE = path3.join(os2.tmpdir(), "spike-preview.pid");
468
+ var PREVIEW_PORT_FILE = path3.join(os2.tmpdir(), "spike-preview.port");
469
+ var PREVIEW_FORM_ID_FILE = path3.join(os2.tmpdir(), "spike-preview.formid");
470
+ function generateFormHtml(form) {
471
+ const config = loadConfig();
472
+ const baseUrl = config.baseUrl || "https://api.spike.ac";
473
+ const actionUrl = `${baseUrl}/f/${form.slug}`;
474
+ return `<!DOCTYPE html>
475
+ <html lang="en">
476
+ <head>
477
+ <meta charset="UTF-8">
478
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
479
+ <title>${form.name}</title>
480
+ <style>
481
+ body {
482
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
483
+ max-width: 600px;
484
+ margin: 40px auto;
485
+ padding: 20px;
486
+ background: #f5f5f5;
487
+ }
488
+ form {
489
+ background: white;
490
+ padding: 30px;
491
+ border-radius: 8px;
492
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
493
+ }
494
+ h1 {
495
+ margin-top: 0;
496
+ color: #333;
497
+ }
498
+ label {
499
+ display: block;
500
+ margin-bottom: 5px;
501
+ font-weight: 500;
502
+ color: #555;
503
+ }
504
+ input, textarea {
505
+ width: 100%;
506
+ padding: 10px;
507
+ margin-bottom: 20px;
508
+ border: 1px solid #ddd;
509
+ border-radius: 4px;
510
+ box-sizing: border-box;
511
+ font-size: 16px;
512
+ }
513
+ textarea {
514
+ min-height: 100px;
515
+ resize: vertical;
516
+ }
517
+ button {
518
+ background: #0070f3;
519
+ color: white;
520
+ padding: 12px 24px;
521
+ border: none;
522
+ border-radius: 4px;
523
+ font-size: 16px;
524
+ cursor: pointer;
525
+ transition: background 0.2s;
526
+ }
527
+ button:hover {
528
+ background: #0051a8;
529
+ }
530
+ </style>
531
+ </head>
532
+ <body>
533
+ <form action="${actionUrl}" method="POST">
534
+ <h1>${form.name}</h1>
535
+
536
+ <label for="name">Name</label>
537
+ <input type="text" id="name" name="name" required>
538
+
539
+ <label for="email">Email</label>
540
+ <input type="email" id="email" name="email" required>
541
+
542
+ <label for="message">Message</label>
543
+ <textarea id="message" name="message" required></textarea>
544
+
545
+ <button type="submit">Submit</button>
546
+ </form>
547
+ </body>
548
+ </html>`;
549
+ }
550
+ function killExistingPreviewServer() {
551
+ try {
552
+ if (!fs3.existsSync(PREVIEW_PID_FILE)) {
553
+ return false;
554
+ }
555
+ const pidStr = fs3.readFileSync(PREVIEW_PID_FILE, "utf-8").trim();
556
+ const pid = parseInt(pidStr, 10);
557
+ if (isNaN(pid)) {
558
+ cleanupPreviewTempFiles();
559
+ return false;
560
+ }
561
+ try {
562
+ process.kill(pid, "SIGTERM");
563
+ info(`Stopped existing preview server (PID: ${pid})`);
564
+ } catch {
565
+ }
566
+ cleanupPreviewTempFiles();
567
+ return true;
568
+ } catch {
569
+ return false;
570
+ }
571
+ }
572
+ function cleanupPreviewTempFiles() {
573
+ try {
574
+ if (fs3.existsSync(PREVIEW_PID_FILE)) {
575
+ fs3.unlinkSync(PREVIEW_PID_FILE);
576
+ }
577
+ } catch {
578
+ }
579
+ try {
580
+ if (fs3.existsSync(PREVIEW_PORT_FILE)) {
581
+ fs3.unlinkSync(PREVIEW_PORT_FILE);
582
+ }
583
+ } catch {
584
+ }
585
+ try {
586
+ if (fs3.existsSync(PREVIEW_FORM_ID_FILE)) {
587
+ fs3.unlinkSync(PREVIEW_FORM_ID_FILE);
588
+ }
589
+ } catch {
590
+ }
591
+ }
592
+ function createFormsCommand() {
593
+ const formsCommand = new Command("forms").description("Manage forms");
594
+ formsCommand.command("list").description("List all forms").option("-l, --limit <number>", "Maximum number of forms to return", parseInt).option("-p, --project-id <id>", "Filter forms by project ID").option("-i, --include-inactive", "Include inactive forms in the results").option("-f, --format <format>", "Output format (json, table)", "table").action(async (options) => {
595
+ await handleList(options);
596
+ });
597
+ formsCommand.command("get").description("Get a specific form by ID").argument("<id>", "Form ID").option("-f, --format <format>", "Output format (json, table)", "table").action(async (id, options) => {
598
+ await handleGet2(id, options.format);
599
+ });
600
+ formsCommand.command("create").description("Create a new form").requiredOption("-n, --name <name>", "Name for the form").option("-p, --project-id <id>", "Project ID to add the form to").option("-f, --format <format>", "Output format (json, table)", "table").action(async (options) => {
601
+ await handleCreate(options);
602
+ });
603
+ formsCommand.command("update").description("Update a form").argument("<id>", "Form ID").option("-n, --name <name>", "New name for the form").option("-p, --project-id <id>", "Project ID to move the form to").option("--is-active <boolean>", "Whether the form should be active", (value) => {
604
+ if (value === "true") return true;
605
+ if (value === "false") return false;
606
+ throw new Error('--is-active must be "true" or "false"');
607
+ }).option("-f, --format <format>", "Output format (json, table)", "table").action(async (id, options) => {
608
+ await handleUpdate(id, options);
609
+ });
610
+ formsCommand.command("delete").description("Delete a form").argument("<id>", "Form ID").action(async (id) => {
611
+ await handleDelete(id);
612
+ });
613
+ formsCommand.command("edit").description("Edit a form with live preview").argument("<id>", "Form ID").option("--file <path>", "Output file path for the HTML", "./form.html").option("--background", "Run preview server as a detached background process").action(async (id, options) => {
614
+ await handleEdit(id, options);
615
+ });
616
+ formsCommand.command("stop-preview").description("Stop the background preview server").action(async () => {
617
+ await handleStopPreview();
618
+ });
619
+ formsCommand.command("save").description("Save local HTML changes to server").argument("[id]", "Form ID (optional if preview server is running)").option("--file <path>", "Path to the HTML file", "./form.html").action(async (id, options) => {
620
+ await handleSave(id, options);
621
+ });
622
+ formsCommand.command("create-page").description("Create a new form and generate an HTML form page (beta)").requiredOption("-n, --name <name>", "Name for the form").option("-p, --project-id <id>", "Project ID to add the form to").option("--file <path>", "Output file path for the HTML", "./form.html").option("--preview", "Start preview server after creation").action(async (options) => {
623
+ await handleCreatePage(options);
624
+ });
625
+ return formsCommand;
626
+ }
627
+ async function handleList(options) {
628
+ try {
629
+ const client = createClient();
630
+ const forms = await client.forms.list({
631
+ limit: options.limit,
632
+ project_id: options.projectId,
633
+ include_inactive: options.includeInactive
634
+ });
635
+ if (forms.length === 0) {
636
+ info("No forms found");
637
+ return;
638
+ }
639
+ output(forms, options.format);
640
+ } catch (err) {
641
+ handleError(err);
642
+ }
643
+ }
644
+ async function handleGet2(id, format) {
645
+ try {
646
+ const client = createClient();
647
+ const form = await client.forms.get(id);
648
+ output(form, format);
649
+ } catch (err) {
650
+ handleError(err);
651
+ }
652
+ }
653
+ async function handleCreate(options) {
654
+ try {
655
+ const client = createClient();
656
+ const form = await client.forms.create({
657
+ name: options.name,
658
+ project_id: options.projectId
659
+ });
660
+ success(`Form "${form.name}" created successfully`);
661
+ console.log("");
662
+ output(form, options.format);
663
+ } catch (err) {
664
+ handleError(err);
665
+ }
666
+ }
667
+ async function handleUpdate(id, options) {
668
+ try {
669
+ if (options.name === void 0 && options.projectId === void 0 && options.isActive === void 0) {
670
+ error("No update options provided. Use --name, --project-id, or --is-active to update the form.");
671
+ process.exit(1);
672
+ }
673
+ const client = createClient();
674
+ const updateData = {};
675
+ if (options.name !== void 0) {
676
+ updateData.name = options.name;
677
+ }
678
+ if (options.projectId !== void 0) {
679
+ updateData.project_id = options.projectId;
680
+ }
681
+ if (options.isActive !== void 0) {
682
+ updateData.is_active = options.isActive;
683
+ }
684
+ const form = await client.forms.update(id, updateData);
685
+ success(`Form "${form.name}" updated successfully`);
686
+ console.log("");
687
+ output(form, options.format);
688
+ } catch (err) {
689
+ handleError(err);
690
+ }
691
+ }
692
+ async function handleDelete(id) {
693
+ try {
694
+ const client = createClient();
695
+ await client.forms.delete(id);
696
+ success(`Form "${id}" deleted successfully`);
697
+ } catch (err) {
698
+ handleError(err);
699
+ }
700
+ }
701
+ async function handleStopPreview() {
702
+ try {
703
+ if (!fs3.existsSync(PREVIEW_PID_FILE)) {
704
+ info("No preview server is currently running");
705
+ return;
706
+ }
707
+ const pidStr = fs3.readFileSync(PREVIEW_PID_FILE, "utf-8").trim();
708
+ const pid = parseInt(pidStr, 10);
709
+ if (isNaN(pid)) {
710
+ cleanupPreviewTempFiles();
711
+ info("No preview server is currently running");
712
+ return;
713
+ }
714
+ try {
715
+ process.kill(pid, "SIGTERM");
716
+ success(`Preview server stopped (PID: ${pid})`);
717
+ } catch (killError) {
718
+ if (killError instanceof Error && "code" in killError && killError.code === "ESRCH") {
719
+ info("Preview server was not running");
720
+ } else {
721
+ throw killError;
722
+ }
723
+ }
724
+ cleanupPreviewTempFiles();
725
+ } catch (err) {
726
+ handleError(err);
727
+ }
728
+ }
729
+ async function handleCreatePage(options) {
730
+ try {
731
+ console.log("\u26A0\uFE0F Note: The create-page command is currently in beta.\n");
732
+ const client = createClient();
733
+ info(`Creating form "${options.name}"...`);
734
+ const form = await client.forms.create({
735
+ name: options.name,
736
+ project_id: options.projectId
737
+ });
738
+ success(`Form "${form.name}" created successfully`);
739
+ const html = generateFormHtml(form);
740
+ info("Saving form HTML to server...");
741
+ const saveResult = await client.forms.saveHtml(form.id, html);
742
+ if (saveResult.success) {
743
+ success("Form HTML saved to server");
744
+ }
745
+ const filePath = path3.resolve(options.file);
746
+ fs3.writeFileSync(filePath, html, "utf-8");
747
+ success(`Form HTML saved locally to ${filePath}`);
748
+ fs3.writeFileSync(PREVIEW_FORM_ID_FILE, form.id, "utf-8");
749
+ if (options.preview) {
750
+ killExistingPreviewServer();
751
+ info("\nMake changes to the HTML file and save to see them in the browser.");
752
+ info("When done, run `spike forms save` to upload changes to the server.\n");
753
+ await startForegroundPreviewServer(filePath);
754
+ }
755
+ } catch (err) {
756
+ handleError(err);
757
+ }
758
+ }
759
+ async function handleEdit(id, options) {
760
+ try {
761
+ const client = createClient();
762
+ info(`Fetching form ${id}...`);
763
+ const form = await client.forms.get(id);
764
+ info("Fetching form HTML from server...");
765
+ const { html } = await client.forms.getHtml(id);
766
+ const filePath = path3.resolve(options.file);
767
+ fs3.writeFileSync(filePath, html, "utf-8");
768
+ success(`Form HTML saved to ${filePath}`);
769
+ fs3.writeFileSync(PREVIEW_FORM_ID_FILE, id, "utf-8");
770
+ killExistingPreviewServer();
771
+ info(`
772
+ Editing form: ${form.name}`);
773
+ info("Make changes to the HTML file and save to see them in the browser.");
774
+ info("When done, run `spike forms save` to upload changes to the server.\n");
775
+ if (options.background) {
776
+ await startBackgroundPreviewServer(filePath);
777
+ } else {
778
+ await startForegroundPreviewServer(filePath);
779
+ }
780
+ } catch (err) {
781
+ handleError(err);
782
+ }
783
+ }
784
+ async function handleSave(id, options) {
785
+ try {
786
+ let formId = id;
787
+ if (!formId) {
788
+ if (fs3.existsSync(PREVIEW_FORM_ID_FILE)) {
789
+ formId = fs3.readFileSync(PREVIEW_FORM_ID_FILE, "utf-8").trim();
790
+ }
791
+ if (!formId) {
792
+ error("No form ID provided and no active preview session found.");
793
+ info("Usage: spike forms save <form-id> --file ./form.html");
794
+ process.exit(1);
795
+ }
796
+ }
797
+ const filePath = path3.resolve(options.file);
798
+ if (!fs3.existsSync(filePath)) {
799
+ error(`File not found: ${filePath}`);
800
+ process.exit(1);
801
+ }
802
+ const html = fs3.readFileSync(filePath, "utf-8");
803
+ if (!html.trim()) {
804
+ error("HTML file is empty");
805
+ process.exit(1);
806
+ }
807
+ const client = createClient();
808
+ info(`Saving form HTML to server...`);
809
+ const result = await client.forms.saveHtml(formId, html);
810
+ if (result.success) {
811
+ success("Form HTML saved successfully!");
812
+ if (result.html_url) {
813
+ info(`Stored at: ${result.html_url}`);
814
+ }
815
+ } else {
816
+ error("Failed to save form HTML");
817
+ process.exit(1);
818
+ }
819
+ } catch (err) {
820
+ handleError(err);
821
+ }
822
+ }
823
+ async function startForegroundPreviewServer(filePath) {
824
+ const port = await startLivePreviewServer(filePath);
825
+ const previewUrl = `http://127.0.0.1:${port}`;
826
+ success(`Preview server running at ${previewUrl}`);
827
+ info("Press Ctrl+C to stop the server");
828
+ const browserOpened = await openBrowser(previewUrl);
829
+ if (!browserOpened) {
830
+ info(`Open ${previewUrl} in your browser to preview the form`);
831
+ }
832
+ await new Promise(() => {
833
+ process.on("SIGINT", () => {
834
+ info("\nStopping preview server...");
835
+ process.exit(0);
836
+ });
837
+ process.on("SIGTERM", () => {
838
+ info("\nStopping preview server...");
839
+ process.exit(0);
840
+ });
841
+ });
842
+ }
843
+ async function startBackgroundPreviewServer(filePath) {
844
+ const serverCode = `
845
+ const http = require('node:http');
846
+ const fs = require('node:fs');
847
+ const path = require('node:path');
848
+ const os = require('node:os');
849
+
850
+ const filePath = ${JSON.stringify(filePath)};
851
+ const pidFile = path.join(os.tmpdir(), 'spike-preview.pid');
852
+ const portFile = path.join(os.tmpdir(), 'spike-preview.port');
853
+
854
+ const SSE_SCRIPT = \`<script>
855
+ (function() {
856
+ var es = new EventSource('/__live-reload');
857
+ es.onmessage = function(event) {
858
+ if (event.data === 'reload') {
859
+ location.reload();
860
+ }
861
+ };
862
+ es.onerror = function() {
863
+ console.log('[Live Preview] Connection lost, attempting to reconnect...');
864
+ };
865
+ })();
866
+ </script>\`;
867
+
868
+ function injectLiveReload(html) {
869
+ const bodyCloseRegex = /<\\/body>/i;
870
+ const match = html.match(bodyCloseRegex);
871
+ if (match && match.index !== undefined) {
872
+ return html.slice(0, match.index) + SSE_SCRIPT + html.slice(match.index);
873
+ }
874
+ return html + SSE_SCRIPT;
875
+ }
876
+
877
+ const sseClients = [];
878
+ let debounceTimer = null;
879
+
880
+ function broadcastToClients(message) {
881
+ const data = 'data: ' + message + '\\n\\n';
882
+ for (let i = sseClients.length - 1; i >= 0; i--) {
883
+ const client = sseClients[i];
884
+ if (!client) continue;
885
+ try {
886
+ if (!client.writableEnded) {
887
+ client.write(data);
888
+ } else {
889
+ sseClients.splice(i, 1);
890
+ }
891
+ } catch {
892
+ sseClients.splice(i, 1);
893
+ }
894
+ }
895
+ }
896
+
897
+ function onFileChange() {
898
+ if (debounceTimer) clearTimeout(debounceTimer);
899
+ debounceTimer = setTimeout(() => {
900
+ broadcastToClients('reload');
901
+ debounceTimer = null;
902
+ }, 100);
903
+ }
904
+
905
+ function cleanup() {
906
+ try { fs.unlinkSync(pidFile); } catch {}
907
+ try { fs.unlinkSync(portFile); } catch {}
908
+ process.exit(0);
909
+ }
910
+
911
+ process.on('SIGTERM', cleanup);
912
+ process.on('SIGINT', cleanup);
913
+
914
+ const server = http.createServer((req, res) => {
915
+ const url = req.url || '/';
916
+
917
+ if (url === '/__live-reload') {
918
+ res.writeHead(200, {
919
+ 'Content-Type': 'text/event-stream',
920
+ 'Cache-Control': 'no-cache',
921
+ 'Connection': 'keep-alive',
922
+ 'Access-Control-Allow-Origin': '*'
923
+ });
924
+ res.write('data: connected\\n\\n');
925
+ sseClients.push(res);
926
+ req.on('close', () => {
927
+ const index = sseClients.indexOf(res);
928
+ if (index !== -1) sseClients.splice(index, 1);
929
+ });
930
+ return;
931
+ }
932
+
933
+ if (url === '/' && req.method === 'GET') {
934
+ try {
935
+ const html = fs.readFileSync(filePath, 'utf-8');
936
+ const injectedHtml = injectLiveReload(html);
937
+ res.writeHead(200, {
938
+ 'Content-Type': 'text/html; charset=utf-8',
939
+ 'Cache-Control': 'no-cache'
940
+ });
941
+ res.end(injectedHtml);
942
+ } catch (err) {
943
+ res.writeHead(500, { 'Content-Type': 'text/plain' });
944
+ res.end('Error reading file: ' + err.message);
945
+ }
946
+ return;
947
+ }
948
+
949
+ res.writeHead(404, { 'Content-Type': 'text/plain' });
950
+ res.end('Not Found');
951
+ });
952
+
953
+ try {
954
+ fs.watch(filePath, (eventType) => {
955
+ if (eventType === 'change') onFileChange();
956
+ });
957
+ } catch {}
958
+
959
+ server.listen(0, '127.0.0.1', () => {
960
+ const address = server.address();
961
+ const port = address.port;
962
+
963
+ // Write PID and port to temp files
964
+ fs.writeFileSync(pidFile, process.pid.toString(), 'utf-8');
965
+ fs.writeFileSync(portFile, port.toString(), 'utf-8');
966
+
967
+ // Output port for parent process to read
968
+ console.log('PORT:' + port);
969
+ });
970
+ `;
971
+ return new Promise((resolve3, reject) => {
972
+ const child = spawn("node", ["-e", serverCode], {
973
+ detached: true,
974
+ stdio: ["ignore", "pipe", "ignore"]
975
+ });
976
+ let portReceived = false;
977
+ let outputBuffer = "";
978
+ child.stdout?.on("data", (data) => {
979
+ outputBuffer += data.toString();
980
+ const match = outputBuffer.match(/PORT:(\d+)/);
981
+ if (match && !portReceived) {
982
+ portReceived = true;
983
+ const port = parseInt(match[1], 10);
984
+ const previewUrl = `http://127.0.0.1:${port}`;
985
+ success(`Background preview server started at ${previewUrl}`);
986
+ info(`PID: ${child.pid}`);
987
+ info("Run `spike forms stop-preview` to stop the server");
988
+ openBrowser(previewUrl).then((browserOpened) => {
989
+ if (!browserOpened) {
990
+ info(`Open ${previewUrl} in your browser to preview the form`);
991
+ }
992
+ child.unref();
993
+ resolve3();
994
+ });
995
+ }
996
+ });
997
+ child.on("error", (err) => {
998
+ reject(new Error(`Failed to start background server: ${err.message}`));
999
+ });
1000
+ setTimeout(() => {
1001
+ if (!portReceived) {
1002
+ child.kill();
1003
+ reject(new Error("Timeout waiting for background server to start"));
1004
+ }
1005
+ }, 1e4);
1006
+ });
1007
+ }
1008
+ function createClient2() {
1009
+ const config = loadConfig();
1010
+ if (!config.apiKey) {
1011
+ error("API key not configured. Run `spike config set api-key <your-key>` to set it.");
1012
+ process.exit(1);
1013
+ }
1014
+ return new SpikeClient({
1015
+ apiKey: config.apiKey,
1016
+ baseUrl: config.baseUrl
1017
+ });
1018
+ }
1019
+ function handleError2(err) {
1020
+ if (err instanceof SpikeError) {
1021
+ error(err.message);
1022
+ if (err.code) {
1023
+ info(`Error code: ${err.code}`);
1024
+ }
1025
+ } else if (err instanceof Error) {
1026
+ error(err.message);
1027
+ } else {
1028
+ error("An unexpected error occurred");
1029
+ }
1030
+ process.exit(1);
1031
+ }
1032
+ function createSubmissionsCommand() {
1033
+ const submissionsCommand = new Command("submissions").description("Manage form submissions");
1034
+ submissionsCommand.command("list").description("List submissions for a form").argument("<form-id>", "Form ID").option("-l, --limit <number>", "Maximum number of submissions to return", parseInt).option("-s, --status <status>", "Filter by status (read, unread, spam, starred)").option("--from <date>", "Filter submissions from this date (ISO 8601 format)").option("--to <date>", "Filter submissions to this date (ISO 8601 format)").option("-o, --order <order>", "Sort order (asc, desc)", "desc").option("-f, --format <format>", "Output format (json, table)", "table").action(async (formId, options) => {
1035
+ await handleList2(formId, options);
1036
+ });
1037
+ submissionsCommand.command("export").description("Export all submissions for a form").argument("<form-id>", "Form ID").option("-f, --format <format>", "Output format (json, csv)", "json").action(async (formId, options) => {
1038
+ await handleExport(formId, options.format);
1039
+ });
1040
+ submissionsCommand.command("stats").description("Display submission statistics for a form").argument("<form-id>", "Form ID").option("-f, --format <format>", "Output format (json, table)", "table").action(async (formId, options) => {
1041
+ await handleStats(formId, options.format);
1042
+ });
1043
+ return submissionsCommand;
1044
+ }
1045
+ async function handleList2(formId, options) {
1046
+ try {
1047
+ const client = createClient2();
1048
+ const params = {};
1049
+ if (options.limit !== void 0) {
1050
+ params.limit = options.limit;
1051
+ }
1052
+ if (options.from) {
1053
+ params.since = options.from;
1054
+ }
1055
+ if (options.order === "asc" || options.order === "desc") {
1056
+ params.order = options.order;
1057
+ }
1058
+ if (options.status) {
1059
+ switch (options.status.toLowerCase()) {
1060
+ case "read":
1061
+ params.is_read = true;
1062
+ break;
1063
+ case "unread":
1064
+ params.is_read = false;
1065
+ break;
1066
+ case "spam":
1067
+ params.is_spam = true;
1068
+ break;
1069
+ case "starred":
1070
+ break;
1071
+ default:
1072
+ error(`Invalid status: ${options.status}. Valid values are: read, unread, spam, starred`);
1073
+ process.exit(1);
1074
+ }
1075
+ }
1076
+ const submissions = await client.submissions.list(formId, params);
1077
+ let filteredSubmissions = submissions;
1078
+ if (options.status?.toLowerCase() === "starred") {
1079
+ filteredSubmissions = submissions.filter((s) => s.is_starred);
1080
+ }
1081
+ if (options.to) {
1082
+ const toDate = new Date(options.to);
1083
+ filteredSubmissions = filteredSubmissions.filter((s) => new Date(s.created_at) <= toDate);
1084
+ }
1085
+ if (filteredSubmissions.length === 0) {
1086
+ info("No submissions found");
1087
+ return;
1088
+ }
1089
+ const displayData = filteredSubmissions.map((s) => ({
1090
+ id: s.id,
1091
+ created_at: s.created_at,
1092
+ is_read: s.is_read,
1093
+ is_spam: s.is_spam,
1094
+ is_starred: s.is_starred,
1095
+ data: JSON.stringify(s.data).slice(0, 50) + (JSON.stringify(s.data).length > 50 ? "..." : "")
1096
+ }));
1097
+ output(displayData, options.format);
1098
+ } catch (err) {
1099
+ handleError2(err);
1100
+ }
1101
+ }
1102
+ async function handleExport(formId, format) {
1103
+ try {
1104
+ const client = createClient2();
1105
+ const submissions = await client.submissions.export(formId);
1106
+ if (submissions.length === 0) {
1107
+ info("No submissions to export");
1108
+ return;
1109
+ }
1110
+ if (format === "csv") {
1111
+ outputCsv(submissions);
1112
+ } else {
1113
+ console.log(JSON.stringify(submissions, null, 2));
1114
+ }
1115
+ success(`Exported ${submissions.length} submission${submissions.length === 1 ? "" : "s"}`);
1116
+ } catch (err) {
1117
+ handleError2(err);
1118
+ }
1119
+ }
1120
+ function outputCsv(submissions) {
1121
+ if (submissions.length === 0) {
1122
+ return;
1123
+ }
1124
+ const dataKeys = /* @__PURE__ */ new Set();
1125
+ for (const submission of submissions) {
1126
+ Object.keys(submission.data).forEach((key) => dataKeys.add(key));
1127
+ }
1128
+ const baseHeaders = ["id", "form_id", "is_spam", "is_read", "is_starred", "ip_address", "user_agent", "created_at"];
1129
+ const allHeaders = [...baseHeaders, ...Array.from(dataKeys)];
1130
+ console.log(allHeaders.map(escapeCsvValue).join(","));
1131
+ for (const submission of submissions) {
1132
+ const row = [
1133
+ submission.id,
1134
+ submission.form_id,
1135
+ String(submission.is_spam),
1136
+ String(submission.is_read),
1137
+ String(submission.is_starred),
1138
+ submission.ip_address || "",
1139
+ submission.user_agent || "",
1140
+ submission.created_at,
1141
+ ...Array.from(dataKeys).map((key) => {
1142
+ const value = submission.data[key];
1143
+ if (value === void 0 || value === null) {
1144
+ return "";
1145
+ }
1146
+ if (typeof value === "object") {
1147
+ return JSON.stringify(value);
1148
+ }
1149
+ return String(value);
1150
+ })
1151
+ ];
1152
+ console.log(row.map(escapeCsvValue).join(","));
1153
+ }
1154
+ }
1155
+ function escapeCsvValue(value) {
1156
+ if (value.includes(",") || value.includes('"') || value.includes("\n") || value.includes("\r")) {
1157
+ return `"${value.replace(/"/g, '""')}"`;
1158
+ }
1159
+ return value;
1160
+ }
1161
+ async function handleStats(formId, format) {
1162
+ try {
1163
+ const client = createClient2();
1164
+ const stats = await client.submissions.getStats(formId);
1165
+ output(stats, format);
1166
+ } catch (err) {
1167
+ handleError2(err);
1168
+ }
1169
+ }
1170
+ function createClient3() {
1171
+ const config = loadConfig();
1172
+ if (!config.apiKey) {
1173
+ error("API key not configured. Run `spike config set api-key <your-key>` to set it.");
1174
+ process.exit(1);
1175
+ }
1176
+ return new SpikeClient({
1177
+ apiKey: config.apiKey,
1178
+ baseUrl: config.baseUrl
1179
+ });
1180
+ }
1181
+ function handleError3(err) {
1182
+ if (err instanceof SpikeError) {
1183
+ error(err.message);
1184
+ if (err.code) {
1185
+ info(`Error code: ${err.code}`);
1186
+ }
1187
+ } else if (err instanceof Error) {
1188
+ error(err.message);
1189
+ } else {
1190
+ error("An unexpected error occurred");
1191
+ }
1192
+ process.exit(1);
1193
+ }
1194
+ function createProjectsCommand() {
1195
+ const projectsCommand = new Command("projects").description("Manage projects");
1196
+ projectsCommand.command("list").description("List all projects").option("-f, --format <format>", "Output format (json, table)", "table").action(async (options) => {
1197
+ await handleList3(options.format);
1198
+ });
1199
+ projectsCommand.command("get").description("Get a specific project by ID").argument("<id>", "Project ID").option("-f, --format <format>", "Output format (json, table)", "table").action(async (id, options) => {
1200
+ await handleGet3(id, options.format);
1201
+ });
1202
+ projectsCommand.command("create").description("Create a new project").requiredOption("-n, --name <name>", "Name for the project").option("-f, --format <format>", "Output format (json, table)", "table").action(async (options) => {
1203
+ await handleCreate2(options);
1204
+ });
1205
+ projectsCommand.command("update").description("Update a project").argument("<id>", "Project ID").option("-n, --name <name>", "New name for the project").option("-f, --format <format>", "Output format (json, table)", "table").action(async (id, options) => {
1206
+ await handleUpdate2(id, options);
1207
+ });
1208
+ projectsCommand.command("delete").description("Delete a project").argument("<id>", "Project ID").action(async (id) => {
1209
+ await handleDelete2(id);
1210
+ });
1211
+ return projectsCommand;
1212
+ }
1213
+ async function handleList3(format) {
1214
+ try {
1215
+ const client = createClient3();
1216
+ const projects = await client.projects.list();
1217
+ if (projects.length === 0) {
1218
+ info("No projects found");
1219
+ return;
1220
+ }
1221
+ output(projects, format);
1222
+ } catch (err) {
1223
+ handleError3(err);
1224
+ }
1225
+ }
1226
+ async function handleGet3(id, format) {
1227
+ try {
1228
+ const client = createClient3();
1229
+ const project = await client.projects.get(id);
1230
+ output(project, format);
1231
+ } catch (err) {
1232
+ handleError3(err);
1233
+ }
1234
+ }
1235
+ async function handleCreate2(options) {
1236
+ try {
1237
+ const client = createClient3();
1238
+ const project = await client.projects.create({
1239
+ name: options.name
1240
+ });
1241
+ success(`Project "${project.name}" created successfully`);
1242
+ console.log("");
1243
+ output(project, options.format);
1244
+ } catch (err) {
1245
+ handleError3(err);
1246
+ }
1247
+ }
1248
+ async function handleUpdate2(id, options) {
1249
+ try {
1250
+ if (options.name === void 0) {
1251
+ error("No update options provided. Use --name to update the project.");
1252
+ process.exit(1);
1253
+ }
1254
+ const client = createClient3();
1255
+ const updateData = {};
1256
+ if (options.name !== void 0) {
1257
+ updateData.name = options.name;
1258
+ }
1259
+ const project = await client.projects.update(id, updateData);
1260
+ success(`Project "${project.name}" updated successfully`);
1261
+ console.log("");
1262
+ output(project, options.format);
1263
+ } catch (err) {
1264
+ handleError3(err);
1265
+ }
1266
+ }
1267
+ async function handleDelete2(id) {
1268
+ try {
1269
+ const client = createClient3();
1270
+ await client.projects.delete(id);
1271
+ success(`Project "${id}" deleted successfully`);
1272
+ } catch (err) {
1273
+ handleError3(err);
1274
+ }
1275
+ }
1276
+ function createClient4() {
1277
+ const config = loadConfig();
1278
+ if (!config.apiKey) {
1279
+ error("API key not configured. Run `spike config set api-key <your-key>` to set it.");
1280
+ process.exit(1);
1281
+ }
1282
+ return new SpikeClient({
1283
+ apiKey: config.apiKey,
1284
+ baseUrl: config.baseUrl
1285
+ });
1286
+ }
1287
+ function handleError4(err) {
1288
+ if (err instanceof SpikeError) {
1289
+ error(err.message);
1290
+ if (err.code) {
1291
+ info(`Error code: ${err.code}`);
1292
+ }
1293
+ } else if (err instanceof Error) {
1294
+ error(err.message);
1295
+ } else {
1296
+ error("An unexpected error occurred");
1297
+ }
1298
+ process.exit(1);
1299
+ }
1300
+ function createTeamsCommand() {
1301
+ const teamsCommand = new Command("teams").description("Manage teams");
1302
+ teamsCommand.command("list").description("List all teams").option("-f, --format <format>", "Output format (json, table)", "table").action(async (options) => {
1303
+ await handleList4(options.format);
1304
+ });
1305
+ teamsCommand.command("get").description("Get a specific team by ID").argument("<id>", "Team ID").option("-f, --format <format>", "Output format (json, table)", "table").action(async (id, options) => {
1306
+ await handleGet4(id, options.format);
1307
+ });
1308
+ teamsCommand.command("create").description("Create a new team").requiredOption("-n, --name <name>", "Name for the team").option("-f, --format <format>", "Output format (json, table)", "table").action(async (options) => {
1309
+ await handleCreate3(options);
1310
+ });
1311
+ teamsCommand.command("members").description("List members of a team").argument("<id>", "Team ID").option("-f, --format <format>", "Output format (json, table)", "table").action(async (id, options) => {
1312
+ await handleMembers(id, options.format);
1313
+ });
1314
+ teamsCommand.command("invite").description("Invite a user to a team").argument("<id>", "Team ID").requiredOption("-e, --email <email>", "Email address of the user to invite").requiredOption("-r, --role <role>", "Role for the invited user (admin, member)").option("-f, --format <format>", "Output format (json, table)", "table").action(async (id, options) => {
1315
+ await handleInvite(id, options);
1316
+ });
1317
+ return teamsCommand;
1318
+ }
1319
+ async function handleList4(format) {
1320
+ try {
1321
+ const client = createClient4();
1322
+ const teams = await client.teams.list();
1323
+ if (teams.length === 0) {
1324
+ info("No teams found");
1325
+ return;
1326
+ }
1327
+ output(teams, format);
1328
+ } catch (err) {
1329
+ handleError4(err);
1330
+ }
1331
+ }
1332
+ async function handleGet4(id, format) {
1333
+ try {
1334
+ const client = createClient4();
1335
+ const team = await client.teams.get(id);
1336
+ output(team, format);
1337
+ } catch (err) {
1338
+ handleError4(err);
1339
+ }
1340
+ }
1341
+ async function handleCreate3(options) {
1342
+ try {
1343
+ const client = createClient4();
1344
+ const team = await client.teams.create({
1345
+ name: options.name
1346
+ });
1347
+ success(`Team "${team.name}" created successfully`);
1348
+ console.log("");
1349
+ output(team, options.format);
1350
+ } catch (err) {
1351
+ handleError4(err);
1352
+ }
1353
+ }
1354
+ async function handleMembers(id, format) {
1355
+ try {
1356
+ const client = createClient4();
1357
+ const members = await client.teams.listMembers(id);
1358
+ if (members.length === 0) {
1359
+ info("No members found");
1360
+ return;
1361
+ }
1362
+ const displayData = members.map((member) => ({
1363
+ id: member.id,
1364
+ user_id: member.user_id,
1365
+ name: member.user.name,
1366
+ email: member.user.email,
1367
+ role: member.role,
1368
+ created_at: member.created_at
1369
+ }));
1370
+ output(displayData, format);
1371
+ } catch (err) {
1372
+ handleError4(err);
1373
+ }
1374
+ }
1375
+ async function handleInvite(id, options) {
1376
+ try {
1377
+ const role = options.role.toLowerCase();
1378
+ if (role !== "admin" && role !== "member") {
1379
+ error("Invalid role. Valid values are: admin, member");
1380
+ process.exit(1);
1381
+ }
1382
+ const client = createClient4();
1383
+ const invitation = await client.teams.invite(id, {
1384
+ email: options.email,
1385
+ role
1386
+ });
1387
+ success(`Invitation sent to "${options.email}" as ${role}`);
1388
+ console.log("");
1389
+ output(invitation, options.format);
1390
+ } catch (err) {
1391
+ handleError4(err);
1392
+ }
1393
+ }
1394
+ function createClient5() {
1395
+ const config = loadConfig();
1396
+ if (!config.apiKey) {
1397
+ error("API key not configured. Run `spike config set api-key <your-key>` to set it.");
1398
+ process.exit(1);
1399
+ }
1400
+ return new SpikeClient({
1401
+ apiKey: config.apiKey,
1402
+ baseUrl: config.baseUrl
1403
+ });
1404
+ }
1405
+ function handleError5(err) {
1406
+ if (err instanceof SpikeError) {
1407
+ error(err.message);
1408
+ if (err.code) {
1409
+ info(`Error code: ${err.code}`);
1410
+ }
1411
+ } else if (err instanceof Error) {
1412
+ error(err.message);
1413
+ } else {
1414
+ error("An unexpected error occurred");
1415
+ }
1416
+ process.exit(1);
1417
+ }
1418
+ function createUserCommand() {
1419
+ const userCommand = new Command("user").description("Manage user profile and API keys");
1420
+ userCommand.option("-f, --format <format>", "Output format (json, table)", "table").action(async (options) => {
1421
+ await handleGetProfile(options.format);
1422
+ });
1423
+ userCommand.command("update").description("Update user profile").option("-n, --name <name>", "New name for the user").option("-e, --email <email>", "New email address for the user").option("-f, --format <format>", "Output format (json, table)", "table").action(async (options) => {
1424
+ await handleUpdateProfile(options);
1425
+ });
1426
+ const apiKeysCommand = new Command("api-keys").description("Manage user API keys");
1427
+ apiKeysCommand.option("-f, --format <format>", "Output format (json, table)", "table").action(async (options) => {
1428
+ await handleListApiKeys(options.format);
1429
+ });
1430
+ apiKeysCommand.command("create").description("Create a new API key").requiredOption("-n, --name <name>", "Name for the API key").option("-f, --format <format>", "Output format (json, table)", "table").action(async (options) => {
1431
+ await handleCreateApiKey(options);
1432
+ });
1433
+ apiKeysCommand.command("delete").description("Delete an API key").argument("<id>", "API key ID").action(async (id) => {
1434
+ await handleDeleteApiKey(id);
1435
+ });
1436
+ userCommand.addCommand(apiKeysCommand);
1437
+ return userCommand;
1438
+ }
1439
+ async function handleGetProfile(format) {
1440
+ try {
1441
+ const client = createClient5();
1442
+ const user = await client.user.get();
1443
+ output(user, format);
1444
+ } catch (err) {
1445
+ handleError5(err);
1446
+ }
1447
+ }
1448
+ async function handleUpdateProfile(options) {
1449
+ try {
1450
+ if (options.name === void 0 && options.email === void 0) {
1451
+ error("No update options provided. Use --name or --email to update the profile.");
1452
+ process.exit(1);
1453
+ }
1454
+ const client = createClient5();
1455
+ const updateData = {};
1456
+ if (options.name !== void 0) {
1457
+ updateData.name = options.name;
1458
+ }
1459
+ if (options.email !== void 0) {
1460
+ updateData.email = options.email;
1461
+ }
1462
+ const user = await client.user.update(updateData);
1463
+ success("Profile updated successfully");
1464
+ console.log("");
1465
+ output(user, options.format);
1466
+ } catch (err) {
1467
+ handleError5(err);
1468
+ }
1469
+ }
1470
+ async function handleListApiKeys(format) {
1471
+ try {
1472
+ const client = createClient5();
1473
+ const apiKeys = await client.user.listApiKeys();
1474
+ if (apiKeys.length === 0) {
1475
+ info("No API keys found");
1476
+ return;
1477
+ }
1478
+ const displayData = apiKeys.map((key) => ({
1479
+ id: key.id,
1480
+ name: key.name,
1481
+ key: key.key ? maskApiKey(key.key) : "-",
1482
+ last_used_at: key.last_used_at || "Never",
1483
+ created_at: key.created_at
1484
+ }));
1485
+ output(displayData, format);
1486
+ } catch (err) {
1487
+ handleError5(err);
1488
+ }
1489
+ }
1490
+ async function handleCreateApiKey(options) {
1491
+ try {
1492
+ const client = createClient5();
1493
+ const apiKey = await client.user.createApiKey({
1494
+ name: options.name
1495
+ });
1496
+ success(`API key "${apiKey.name}" created successfully`);
1497
+ console.log("");
1498
+ if (options.format === "json") {
1499
+ output(apiKey, "json");
1500
+ } else {
1501
+ console.log("Your new API key (save this - it will not be shown again):");
1502
+ console.log("");
1503
+ console.log(` ${apiKey.key}`);
1504
+ console.log("");
1505
+ output({
1506
+ id: apiKey.id,
1507
+ name: apiKey.name,
1508
+ created_at: apiKey.created_at
1509
+ }, "table");
1510
+ }
1511
+ } catch (err) {
1512
+ handleError5(err);
1513
+ }
1514
+ }
1515
+ async function handleDeleteApiKey(id) {
1516
+ try {
1517
+ const client = createClient5();
1518
+ await client.user.deleteApiKey(id);
1519
+ success(`API key "${id}" deleted successfully`);
1520
+ } catch (err) {
1521
+ handleError5(err);
1522
+ }
1523
+ }
1524
+ function generateState() {
1525
+ return crypto.randomBytes(32).toString("hex");
1526
+ }
1527
+ function verifyState(received, expected) {
1528
+ return received === expected;
1529
+ }
1530
+ function parseCallbackParams(url) {
1531
+ try {
1532
+ let searchParams;
1533
+ if (url.startsWith("http://") || url.startsWith("https://")) {
1534
+ const parsed = new URL(url);
1535
+ searchParams = parsed.searchParams;
1536
+ } else {
1537
+ const queryIndex = url.indexOf("?");
1538
+ if (queryIndex === -1) {
1539
+ return {};
1540
+ }
1541
+ searchParams = new URLSearchParams(url.slice(queryIndex + 1));
1542
+ }
1543
+ return {
1544
+ key: searchParams.get("key") ?? void 0,
1545
+ state: searchParams.get("state") ?? void 0,
1546
+ error: searchParams.get("error") ?? void 0,
1547
+ message: searchParams.get("message") ?? void 0
1548
+ };
1549
+ } catch {
1550
+ return {};
1551
+ }
1552
+ }
1553
+ var SUCCESS_HTML = `<!DOCTYPE html>
1554
+ <html lang="en">
1555
+ <head>
1556
+ <meta charset="UTF-8">
1557
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
1558
+ <title>Login Successful - Spike CLI</title>
1559
+ <style>
1560
+ body {
1561
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
1562
+ display: flex;
1563
+ justify-content: center;
1564
+ align-items: center;
1565
+ min-height: 100vh;
1566
+ margin: 0;
1567
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
1568
+ color: white;
1569
+ }
1570
+ .container {
1571
+ text-align: center;
1572
+ padding: 2rem;
1573
+ background: rgba(255, 255, 255, 0.1);
1574
+ border-radius: 16px;
1575
+ backdrop-filter: blur(10px);
1576
+ max-width: 400px;
1577
+ }
1578
+ .icon {
1579
+ font-size: 4rem;
1580
+ margin-bottom: 1rem;
1581
+ }
1582
+ h1 {
1583
+ margin: 0 0 0.5rem 0;
1584
+ font-size: 1.5rem;
1585
+ }
1586
+ p {
1587
+ margin: 0;
1588
+ opacity: 0.9;
1589
+ }
1590
+ </style>
1591
+ </head>
1592
+ <body>
1593
+ <div class="container">
1594
+ <div class="icon">\u2713</div>
1595
+ <h1>Login Successful!</h1>
1596
+ <p>You can close this window and return to the terminal.</p>
1597
+ </div>
1598
+ </body>
1599
+ </html>`;
1600
+ function getErrorHtml(errorMessage) {
1601
+ const escapedMessage = errorMessage.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
1602
+ return `<!DOCTYPE html>
1603
+ <html lang="en">
1604
+ <head>
1605
+ <meta charset="UTF-8">
1606
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
1607
+ <title>Login Failed - Spike CLI</title>
1608
+ <style>
1609
+ body {
1610
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
1611
+ display: flex;
1612
+ justify-content: center;
1613
+ align-items: center;
1614
+ min-height: 100vh;
1615
+ margin: 0;
1616
+ background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
1617
+ color: white;
1618
+ }
1619
+ .container {
1620
+ text-align: center;
1621
+ padding: 2rem;
1622
+ background: rgba(255, 255, 255, 0.1);
1623
+ border-radius: 16px;
1624
+ backdrop-filter: blur(10px);
1625
+ max-width: 400px;
1626
+ }
1627
+ .icon {
1628
+ font-size: 4rem;
1629
+ margin-bottom: 1rem;
1630
+ }
1631
+ h1 {
1632
+ margin: 0 0 0.5rem 0;
1633
+ font-size: 1.5rem;
1634
+ }
1635
+ p {
1636
+ margin: 0;
1637
+ opacity: 0.9;
1638
+ }
1639
+ .error-message {
1640
+ margin-top: 1rem;
1641
+ padding: 1rem;
1642
+ background: rgba(0, 0, 0, 0.2);
1643
+ border-radius: 8px;
1644
+ font-family: monospace;
1645
+ font-size: 0.9rem;
1646
+ }
1647
+ </style>
1648
+ </head>
1649
+ <body>
1650
+ <div class="container">
1651
+ <div class="icon">\u2717</div>
1652
+ <h1>Login Failed</h1>
1653
+ <p>Something went wrong during authentication.</p>
1654
+ <div class="error-message">${escapedMessage}</div>
1655
+ </div>
1656
+ </body>
1657
+ </html>`;
1658
+ }
1659
+ function createCallbackServer() {
1660
+ let server = null;
1661
+ let port = 0;
1662
+ let expectedState = null;
1663
+ let callbackResolve = null;
1664
+ let timeoutId = null;
1665
+ let callbackReceived = false;
1666
+ function handleRequest(req, res) {
1667
+ if (req.method !== "GET" || !req.url?.startsWith("/callback")) {
1668
+ res.writeHead(404, { "Content-Type": "text/plain" });
1669
+ res.end("Not Found");
1670
+ return;
1671
+ }
1672
+ const params = parseCallbackParams(req.url);
1673
+ if (params.error) {
1674
+ const errorMessage = params.message || params.error;
1675
+ res.writeHead(200, { "Content-Type": "text/html" });
1676
+ res.end(getErrorHtml(errorMessage));
1677
+ if (callbackResolve && !callbackReceived) {
1678
+ callbackReceived = true;
1679
+ callbackResolve({
1680
+ success: false,
1681
+ error: errorMessage
1682
+ });
1683
+ }
1684
+ return;
1685
+ }
1686
+ if (!params.state || !expectedState || !verifyState(params.state, expectedState)) {
1687
+ const errorMessage = "State verification failed. This may be a security issue.";
1688
+ res.writeHead(400, { "Content-Type": "text/html" });
1689
+ res.end(getErrorHtml(errorMessage));
1690
+ if (callbackResolve && !callbackReceived) {
1691
+ callbackReceived = true;
1692
+ callbackResolve({
1693
+ success: false,
1694
+ error: errorMessage
1695
+ });
1696
+ }
1697
+ return;
1698
+ }
1699
+ if (!params.key) {
1700
+ const errorMessage = "No API key received in callback.";
1701
+ res.writeHead(400, { "Content-Type": "text/html" });
1702
+ res.end(getErrorHtml(errorMessage));
1703
+ if (callbackResolve && !callbackReceived) {
1704
+ callbackReceived = true;
1705
+ callbackResolve({
1706
+ success: false,
1707
+ error: errorMessage
1708
+ });
1709
+ }
1710
+ return;
1711
+ }
1712
+ res.writeHead(200, { "Content-Type": "text/html" });
1713
+ res.end(SUCCESS_HTML);
1714
+ if (callbackResolve && !callbackReceived) {
1715
+ callbackReceived = true;
1716
+ callbackResolve({
1717
+ success: true,
1718
+ apiKey: params.key
1719
+ });
1720
+ }
1721
+ }
1722
+ return {
1723
+ async start() {
1724
+ return new Promise((resolve3, reject) => {
1725
+ server = http.createServer(handleRequest);
1726
+ server.on("error", (err) => {
1727
+ reject(err);
1728
+ });
1729
+ server.listen(0, "127.0.0.1", () => {
1730
+ const address = server.address();
1731
+ if (address && typeof address === "object") {
1732
+ port = address.port;
1733
+ const url = `http://127.0.0.1:${port}`;
1734
+ resolve3({ port, url });
1735
+ } else {
1736
+ reject(new Error("Failed to get server address"));
1737
+ }
1738
+ });
1739
+ });
1740
+ },
1741
+ async waitForCallback(state, timeoutMs) {
1742
+ expectedState = state;
1743
+ callbackReceived = false;
1744
+ return new Promise((resolve3) => {
1745
+ callbackResolve = resolve3;
1746
+ timeoutId = setTimeout(() => {
1747
+ if (!callbackReceived) {
1748
+ callbackReceived = true;
1749
+ resolve3({
1750
+ success: false,
1751
+ error: "Login timed out. Please try again."
1752
+ });
1753
+ }
1754
+ }, timeoutMs);
1755
+ });
1756
+ },
1757
+ async stop() {
1758
+ if (timeoutId) {
1759
+ clearTimeout(timeoutId);
1760
+ timeoutId = null;
1761
+ }
1762
+ if (server) {
1763
+ return new Promise((resolve3) => {
1764
+ server.close(() => {
1765
+ server = null;
1766
+ resolve3();
1767
+ });
1768
+ });
1769
+ }
1770
+ }
1771
+ };
1772
+ }
1773
+
1774
+ // src/commands/login.ts
1775
+ var DEFAULT_TIMEOUT_SECONDS = 120;
1776
+ var DEFAULT_DASHBOARD_URL = "https://app.spike.ac";
1777
+ function getDashboardUrl() {
1778
+ return process.env.SPIKE_DASHBOARD_URL || DEFAULT_DASHBOARD_URL;
1779
+ }
1780
+ function buildAuthorizationUrl(state, callbackPort) {
1781
+ const dashboardUrl = getDashboardUrl();
1782
+ const callbackUrl = `http://127.0.0.1:${callbackPort}/callback`;
1783
+ const url = new URL("/cli/authorize", dashboardUrl);
1784
+ url.searchParams.set("state", state);
1785
+ url.searchParams.set("callback_url", callbackUrl);
1786
+ return url.toString();
1787
+ }
1788
+ function createLoginCommand() {
1789
+ const loginCommand = new Command("login").description("Authenticate the CLI via browser").option(
1790
+ "-t, --timeout <seconds>",
1791
+ "Timeout for the login flow in seconds",
1792
+ String(DEFAULT_TIMEOUT_SECONDS)
1793
+ ).option(
1794
+ "--no-browser",
1795
+ "Skip automatic browser opening and display URL only"
1796
+ ).action(async (options) => {
1797
+ await handleLogin(options);
1798
+ });
1799
+ return loginCommand;
1800
+ }
1801
+ async function handleLogin(options) {
1802
+ const timeoutSeconds = parseInt(options.timeout, 10);
1803
+ if (isNaN(timeoutSeconds) || timeoutSeconds <= 0) {
1804
+ error("Invalid timeout value. Please provide a positive number of seconds.");
1805
+ process.exit(1);
1806
+ }
1807
+ const timeoutMs = timeoutSeconds * 1e3;
1808
+ info("Starting login flow...");
1809
+ const state = generateState();
1810
+ const callbackServer = createCallbackServer();
1811
+ let serverInfo;
1812
+ try {
1813
+ serverInfo = await callbackServer.start();
1814
+ } catch (err) {
1815
+ error(`Failed to start callback server: ${err instanceof Error ? err.message : String(err)}`);
1816
+ info("You can manually configure your API key with: spike config set api-key <your-key>");
1817
+ process.exit(1);
1818
+ }
1819
+ const authUrl = buildAuthorizationUrl(state, serverInfo.port);
1820
+ if (options.browser) {
1821
+ info("Opening browser for authentication...");
1822
+ const browserOpened = await openBrowser(authUrl);
1823
+ if (!browserOpened) {
1824
+ info("Could not open browser automatically.");
1825
+ console.log("");
1826
+ info("Please open the following URL in your browser:");
1827
+ console.log("");
1828
+ console.log(` ${authUrl}`);
1829
+ console.log("");
1830
+ }
1831
+ } else {
1832
+ info("Please open the following URL in your browser:");
1833
+ console.log("");
1834
+ console.log(` ${authUrl}`);
1835
+ console.log("");
1836
+ }
1837
+ info(`Waiting for authentication (timeout: ${timeoutSeconds}s)...`);
1838
+ try {
1839
+ const result = await callbackServer.waitForCallback(state, timeoutMs);
1840
+ if (result.success && result.apiKey) {
1841
+ saveConfig({ apiKey: result.apiKey });
1842
+ console.log("");
1843
+ success("Login successful! API key has been saved.");
1844
+ info("You can now use the CLI to manage your forms and submissions.");
1845
+ } else {
1846
+ console.log("");
1847
+ error(result.error || "Login failed. Please try again.");
1848
+ process.exit(1);
1849
+ }
1850
+ } catch (err) {
1851
+ error(`Login error: ${err instanceof Error ? err.message : String(err)}`);
1852
+ process.exit(1);
1853
+ } finally {
1854
+ await callbackServer.stop();
1855
+ }
1856
+ }
1857
+ var SKILL_FILE_CONTENT = `# Spike Forms CLI Skill
1858
+
1859
+ This skill provides knowledge about the Spike Forms CLI (\`spike\`) commands for managing forms, submissions, projects, teams, and configuration.
1860
+
1861
+ ## Overview
1862
+
1863
+ Spike Forms is a form backend service that handles form submissions. The CLI provides commands to manage all aspects of the service.
1864
+
1865
+ ## Authentication
1866
+
1867
+ ### Login via Browser
1868
+
1869
+ \`\`\`bash
1870
+ # Authenticate via browser (recommended)
1871
+ spike login
1872
+
1873
+ # Login with custom timeout (in seconds)
1874
+ spike login --timeout 60
1875
+
1876
+ # Login without automatic browser opening (displays URL to copy)
1877
+ spike login --no-browser
1878
+ \`\`\`
1879
+
1880
+ The login command opens your browser to authorize the CLI and automatically saves your API key.
1881
+
1882
+ ### Manual Configuration
1883
+
1884
+ \`\`\`bash
1885
+ # Set API key manually
1886
+ spike config set api-key <your-api-key>
1887
+
1888
+ # View current configuration
1889
+ spike config get api-key
1890
+ spike config get base-url
1891
+
1892
+ # Set custom API base URL
1893
+ spike config set base-url https://custom.api.url
1894
+ \`\`\`
1895
+
1896
+ ## Forms Management
1897
+
1898
+ ### List Forms
1899
+
1900
+ \`\`\`bash
1901
+ # List all forms
1902
+ spike forms list
1903
+
1904
+ # List with options
1905
+ spike forms list --limit 10
1906
+ spike forms list --project-id <project-id>
1907
+ spike forms list --include-inactive
1908
+ spike forms list --format json
1909
+ \`\`\`
1910
+
1911
+ ### Get Form Details
1912
+
1913
+ \`\`\`bash
1914
+ # Get a specific form
1915
+ spike forms get <form-id>
1916
+ spike forms get <form-id> --format json
1917
+ \`\`\`
1918
+
1919
+ ### Create Forms
1920
+
1921
+ \`\`\`bash
1922
+ # Create a new form
1923
+ spike forms create --name "Contact Form"
1924
+ spike forms create --name "Feedback" --project-id <project-id>
1925
+ \`\`\`
1926
+
1927
+ ### Update Forms
1928
+
1929
+ \`\`\`bash
1930
+ # Update form name
1931
+ spike forms update <form-id> --name "New Name"
1932
+
1933
+ # Update form status
1934
+ spike forms update <form-id> --is-active true
1935
+ spike forms update <form-id> --is-active false
1936
+
1937
+ # Move form to a project
1938
+ spike forms update <form-id> --project-id <project-id>
1939
+ \`\`\`
1940
+
1941
+ ### Delete Forms
1942
+
1943
+ \`\`\`bash
1944
+ # Delete a form
1945
+ spike forms delete <form-id>
1946
+ \`\`\`
1947
+
1948
+ ## Live Preview Workflow (Form Editing)
1949
+
1950
+ The CLI provides a live preview server with hot-reload for editing form HTML templates.
1951
+
1952
+ ### Edit Existing Form
1953
+
1954
+ \`\`\`bash
1955
+ # Fetch form and start live preview (foreground)
1956
+ spike forms edit <form-id>
1957
+
1958
+ # Save to custom file path
1959
+ spike forms edit <form-id> --file ./my-form.html
1960
+
1961
+ # Run preview server in background
1962
+ spike forms edit <form-id> --background
1963
+ \`\`\`
1964
+
1965
+ ### Create Form Page (Beta)
1966
+
1967
+ \`\`\`bash
1968
+ # Create a new form and generate HTML page
1969
+ spike forms create-page --name "Contact Form"
1970
+
1971
+ # Save to custom file path
1972
+ spike forms create-page --name "Contact Form" --file ./contact.html
1973
+
1974
+ # Create and immediately start preview
1975
+ spike forms create-page --name "Contact Form" --preview
1976
+
1977
+ # Create in a specific project
1978
+ spike forms create-page --name "Contact Form" --project-id <project-id>
1979
+ \`\`\`
1980
+
1981
+ ### Stop Background Preview
1982
+
1983
+ \`\`\`bash
1984
+ # Stop the background preview server
1985
+ spike forms stop-preview
1986
+ \`\`\`
1987
+
1988
+ ### Live Preview Features
1989
+
1990
+ - **Hot Reload**: Changes to the HTML file automatically refresh the browser
1991
+ - **SSE Connection**: Uses Server-Sent Events for instant updates
1992
+ - **Local Server**: Runs on http://127.0.0.1 with a dynamic port
1993
+ - **Background Mode**: Run the server detached and continue working
1994
+
1995
+ ## Submissions Management
1996
+
1997
+ ### List Submissions
1998
+
1999
+ \`\`\`bash
2000
+ # List submissions for a form
2001
+ spike submissions list --form-id <form-id>
2002
+
2003
+ # List with options
2004
+ spike submissions list --form-id <form-id> --limit 20
2005
+ spike submissions list --form-id <form-id> --format json
2006
+ \`\`\`
2007
+
2008
+ ### Get Submission Details
2009
+
2010
+ \`\`\`bash
2011
+ # Get a specific submission
2012
+ spike submissions get <submission-id>
2013
+ spike submissions get <submission-id> --format json
2014
+ \`\`\`
2015
+
2016
+ ### Delete Submissions
2017
+
2018
+ \`\`\`bash
2019
+ # Delete a submission
2020
+ spike submissions delete <submission-id>
2021
+ \`\`\`
2022
+
2023
+ ## Projects Management
2024
+
2025
+ ### List Projects
2026
+
2027
+ \`\`\`bash
2028
+ # List all projects
2029
+ spike projects list
2030
+ spike projects list --format json
2031
+ \`\`\`
2032
+
2033
+ ### Get Project Details
2034
+
2035
+ \`\`\`bash
2036
+ # Get a specific project
2037
+ spike projects get <project-id>
2038
+ \`\`\`
2039
+
2040
+ ### Create Projects
2041
+
2042
+ \`\`\`bash
2043
+ # Create a new project
2044
+ spike projects create --name "My Project"
2045
+ spike projects create --name "My Project" --team-id <team-id>
2046
+ \`\`\`
2047
+
2048
+ ### Update Projects
2049
+
2050
+ \`\`\`bash
2051
+ # Update project name
2052
+ spike projects update <project-id> --name "New Name"
2053
+ \`\`\`
2054
+
2055
+ ### Delete Projects
2056
+
2057
+ \`\`\`bash
2058
+ # Delete a project
2059
+ spike projects delete <project-id>
2060
+ \`\`\`
2061
+
2062
+ ## Teams Management
2063
+
2064
+ ### List Teams
2065
+
2066
+ \`\`\`bash
2067
+ # List all teams
2068
+ spike teams list
2069
+ spike teams list --format json
2070
+ \`\`\`
2071
+
2072
+ ### Get Team Details
2073
+
2074
+ \`\`\`bash
2075
+ # Get a specific team
2076
+ spike teams get <team-id>
2077
+ \`\`\`
2078
+
2079
+ ### Create Teams
2080
+
2081
+ \`\`\`bash
2082
+ # Create a new team
2083
+ spike teams create --name "My Team"
2084
+ \`\`\`
2085
+
2086
+ ### Update Teams
2087
+
2088
+ \`\`\`bash
2089
+ # Update team name
2090
+ spike teams update <team-id> --name "New Name"
2091
+ \`\`\`
2092
+
2093
+ ### Delete Teams
2094
+
2095
+ \`\`\`bash
2096
+ # Delete a team
2097
+ spike teams delete <team-id>
2098
+ \`\`\`
2099
+
2100
+ ## Configuration Commands
2101
+
2102
+ \`\`\`bash
2103
+ # Get configuration values
2104
+ spike config get api-key
2105
+ spike config get base-url
2106
+
2107
+ # Set configuration values
2108
+ spike config set api-key <your-api-key>
2109
+ spike config set base-url <api-url>
2110
+
2111
+ # Configuration file location: ~/.spike/config.json
2112
+ \`\`\`
2113
+
2114
+ ## Global Options
2115
+
2116
+ All commands support these global options:
2117
+
2118
+ \`\`\`bash
2119
+ # Output format (json or table)
2120
+ spike <command> --format json
2121
+ spike <command> --format table
2122
+
2123
+ # Help
2124
+ spike --help
2125
+ spike <command> --help
2126
+ \`\`\`
2127
+
2128
+ ## Common Workflows
2129
+
2130
+ ### Setting Up a New Form
2131
+
2132
+ 1. Login to authenticate: \`spike login\`
2133
+ 2. Create a form: \`spike forms create --name "Contact Form"\`
2134
+ 3. Edit with live preview: \`spike forms edit <form-id>\`
2135
+ 4. Make changes to the HTML file and see them instantly in the browser
2136
+
2137
+ ### Managing Form Submissions
2138
+
2139
+ 1. List forms: \`spike forms list\`
2140
+ 2. View submissions: \`spike submissions list --form-id <form-id>\`
2141
+ 3. Get submission details: \`spike submissions get <submission-id>\`
2142
+
2143
+ ### Organizing with Projects and Teams
2144
+
2145
+ 1. Create a team: \`spike teams create --name "Marketing"\`
2146
+ 2. Create a project: \`spike projects create --name "Website Forms" --team-id <team-id>\`
2147
+ 3. Create forms in the project: \`spike forms create --name "Contact" --project-id <project-id>\`
2148
+
2149
+ ## Environment Variables
2150
+
2151
+ - \`SPIKE_API_KEY\` or \`SPIKE_TOKEN\`: API key for authentication
2152
+ - \`SPIKE_API_URL\`: Custom API base URL
2153
+ - \`SPIKE_DASHBOARD_URL\`: Custom dashboard URL for login flow
2154
+ `;
2155
+ function getSkillsDir() {
2156
+ const homeDir = os2.homedir();
2157
+ return path3.join(homeDir, ".config", "opencode", "skills", "spike");
2158
+ }
2159
+ function getSkillFilePath() {
2160
+ return path3.join(getSkillsDir(), "SKILL.md");
2161
+ }
2162
+ function isOpenCodeInstalled() {
2163
+ try {
2164
+ const command = process.platform === "win32" ? "where opencode" : "which opencode";
2165
+ execSync(command, { stdio: "ignore" });
2166
+ return true;
2167
+ } catch {
2168
+ return false;
2169
+ }
2170
+ }
2171
+ function ensureSkillInstalled() {
2172
+ const skillsDir = getSkillsDir();
2173
+ const skillFilePath = getSkillFilePath();
2174
+ if (!fs3.existsSync(skillsDir)) {
2175
+ fs3.mkdirSync(skillsDir, { recursive: true });
2176
+ }
2177
+ fs3.writeFileSync(skillFilePath, SKILL_FILE_CONTENT, "utf-8");
2178
+ }
2179
+ function installOpenCode() {
2180
+ try {
2181
+ info("Installing OpenCode via npm...");
2182
+ execSync("npm install -g opencode", { stdio: "inherit" });
2183
+ return true;
2184
+ } catch {
2185
+ return false;
2186
+ }
2187
+ }
2188
+ function createAgentCommand() {
2189
+ const agentCommand = new Command("agent").description("Launch OpenCode AI agent with Spike Forms knowledge").option("--install", "Install OpenCode via npm if not already installed").option("--model <model>", "Model to use with OpenCode").action(async (options) => {
2190
+ await handleAgent(options);
2191
+ });
2192
+ return agentCommand;
2193
+ }
2194
+ async function handleAgent(options) {
2195
+ if (!isOpenCodeInstalled()) {
2196
+ if (options.install) {
2197
+ const installed = installOpenCode();
2198
+ if (!installed) {
2199
+ error("Failed to install OpenCode via npm.");
2200
+ info("Please install OpenCode manually:");
2201
+ console.log("");
2202
+ console.log(" npm install -g opencode");
2203
+ console.log("");
2204
+ info("Or visit: https://opencode.ai for more installation options.");
2205
+ process.exit(1);
2206
+ }
2207
+ success("OpenCode installed successfully");
2208
+ } else {
2209
+ error("OpenCode is not installed.");
2210
+ info("Install OpenCode using one of these methods:");
2211
+ console.log("");
2212
+ console.log(" # Install via npm");
2213
+ console.log(" npm install -g opencode");
2214
+ console.log("");
2215
+ console.log(" # Or use the --install flag");
2216
+ console.log(" spike agent --install");
2217
+ console.log("");
2218
+ info("Or visit: https://opencode.ai for more installation options.");
2219
+ process.exit(1);
2220
+ }
2221
+ }
2222
+ try {
2223
+ ensureSkillInstalled();
2224
+ info("Spike Forms skill installed for OpenCode");
2225
+ } catch (err) {
2226
+ warn(`Could not install skill file: ${err instanceof Error ? err.message : String(err)}`);
2227
+ }
2228
+ const args = [];
2229
+ if (options.model) {
2230
+ args.push("--model", options.model);
2231
+ }
2232
+ info("Launching OpenCode...");
2233
+ console.log("");
2234
+ const opencode = spawn("opencode", args, {
2235
+ stdio: "inherit"
2236
+ });
2237
+ opencode.on("close", (code) => {
2238
+ process.exit(code ?? 0);
2239
+ });
2240
+ opencode.on("error", (err) => {
2241
+ error(`Failed to launch OpenCode: ${err.message}`);
2242
+ process.exit(1);
2243
+ });
2244
+ }
2245
+
2246
+ // src/index.ts
2247
+ var program = new Command();
2248
+ program.name("spike").description("Command-line interface for the Spike Forms API").version("0.1.0");
2249
+ program.option(
2250
+ "-f, --format <format>",
2251
+ "Output format (json, table)",
2252
+ "table"
2253
+ );
2254
+ program.addCommand(createConfigCommand());
2255
+ program.addCommand(createFormsCommand());
2256
+ program.addCommand(createSubmissionsCommand());
2257
+ program.addCommand(createProjectsCommand());
2258
+ program.addCommand(createTeamsCommand());
2259
+ program.addCommand(createUserCommand());
2260
+ program.addCommand(createLoginCommand());
2261
+ program.addCommand(createAgentCommand());
2262
+ async function main() {
2263
+ try {
2264
+ await program.parseAsync(process.argv);
2265
+ } catch (err) {
2266
+ handleFatalError(err);
2267
+ }
2268
+ }
2269
+ function handleFatalError(err) {
2270
+ if (err instanceof Error) {
2271
+ error(err.message);
2272
+ } else {
2273
+ error("An unexpected error occurred");
2274
+ }
2275
+ process.exitCode = 1;
2276
+ }
2277
+ process.on("uncaughtException", (err) => {
2278
+ handleFatalError(err);
2279
+ process.exit(1);
2280
+ });
2281
+ process.on("unhandledRejection", (reason) => {
2282
+ handleFatalError(reason instanceof Error ? reason : new Error(String(reason)));
2283
+ process.exit(1);
2284
+ });
2285
+ main();
2286
+ //# sourceMappingURL=index.js.map
2287
+ //# sourceMappingURL=index.js.map