@curenorway/kode-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.
@@ -0,0 +1,980 @@
1
+ // src/config.ts
2
+ import Conf from "conf";
3
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs";
4
+ import { join, dirname } from "path";
5
+ var globalConfig = new Conf({
6
+ projectName: "cure-kode",
7
+ defaults: {
8
+ apiUrl: "https://cure-app-v2-production.up.railway.app"
9
+ }
10
+ });
11
+ var PROJECT_CONFIG_DIR = ".cure-kode";
12
+ var PROJECT_CONFIG_FILE = "config.json";
13
+ function findProjectRoot(startDir = process.cwd()) {
14
+ let currentDir = startDir;
15
+ while (currentDir !== dirname(currentDir)) {
16
+ const configPath = join(currentDir, PROJECT_CONFIG_DIR, PROJECT_CONFIG_FILE);
17
+ if (existsSync(configPath)) {
18
+ return currentDir;
19
+ }
20
+ currentDir = dirname(currentDir);
21
+ }
22
+ return null;
23
+ }
24
+ function getProjectConfig(projectRoot) {
25
+ const root = projectRoot || findProjectRoot();
26
+ if (!root) return null;
27
+ const configPath = join(root, PROJECT_CONFIG_DIR, PROJECT_CONFIG_FILE);
28
+ if (!existsSync(configPath)) return null;
29
+ try {
30
+ const content = readFileSync(configPath, "utf-8");
31
+ return JSON.parse(content);
32
+ } catch {
33
+ return null;
34
+ }
35
+ }
36
+ function saveProjectConfig(config, projectRoot = process.cwd()) {
37
+ const configDir = join(projectRoot, PROJECT_CONFIG_DIR);
38
+ const configPath = join(configDir, PROJECT_CONFIG_FILE);
39
+ if (!existsSync(configDir)) {
40
+ mkdirSync(configDir, { recursive: true });
41
+ }
42
+ writeFileSync(configPath, JSON.stringify(config, null, 2));
43
+ }
44
+ function getApiUrl(projectConfig) {
45
+ return projectConfig?.apiUrl || globalConfig.get("apiUrl");
46
+ }
47
+ function getApiKey(projectConfig) {
48
+ return projectConfig?.apiKey || globalConfig.get("defaultApiKey") || null;
49
+ }
50
+ function setGlobalConfig(key, value) {
51
+ globalConfig.set(key, value);
52
+ }
53
+ function getScriptsDir(projectRoot, projectConfig) {
54
+ return join(projectRoot, projectConfig?.scriptsDir || "scripts");
55
+ }
56
+
57
+ // src/commands/init.ts
58
+ import chalk from "chalk";
59
+ import ora from "ora";
60
+ import { prompt } from "enquirer";
61
+ import { existsSync as existsSync2, mkdirSync as mkdirSync2 } from "fs";
62
+ import { join as join2 } from "path";
63
+ async function initCommand(options) {
64
+ const cwd = process.cwd();
65
+ const existingRoot = findProjectRoot(cwd);
66
+ if (existingRoot && !options.force) {
67
+ console.log(
68
+ chalk.yellow("\u26A0\uFE0F Cure Kode is already initialized in this directory tree.")
69
+ );
70
+ console.log(chalk.dim(` Config found at: ${existingRoot}/.cure-kode/config.json`));
71
+ console.log(chalk.dim(" Use --force to reinitialize."));
72
+ return;
73
+ }
74
+ console.log(chalk.bold("\n\u{1F680} Cure Kode Setup\n"));
75
+ const answers = await prompt([
76
+ {
77
+ type: "password",
78
+ name: "apiKey",
79
+ message: "API Key (from Cure App \u2192 Tools \u2192 Kode \u2192 API Keys):",
80
+ initial: options.apiKey,
81
+ validate: (value) => value.length > 0 ? true : "API key is required"
82
+ },
83
+ {
84
+ type: "input",
85
+ name: "siteSlug",
86
+ message: 'Site slug (e.g., "swipp"):',
87
+ initial: options.siteSlug,
88
+ validate: (value) => /^[a-z0-9-]+$/.test(value) ? true : "Slug must be lowercase letters, numbers, and hyphens only"
89
+ },
90
+ {
91
+ type: "input",
92
+ name: "siteName",
93
+ message: "Site name (for display):",
94
+ initial: (answers2) => answers2.siteSlug ? answers2.siteSlug.charAt(0).toUpperCase() + answers2.siteSlug.slice(1) : ""
95
+ },
96
+ {
97
+ type: "input",
98
+ name: "siteId",
99
+ message: "Site ID (UUID from Cure App):",
100
+ validate: (value) => /^[0-9a-f-]{36}$/.test(value) ? true : "Must be a valid UUID"
101
+ },
102
+ {
103
+ type: "input",
104
+ name: "scriptsDir",
105
+ message: "Scripts directory:",
106
+ initial: "scripts"
107
+ },
108
+ {
109
+ type: "select",
110
+ name: "environment",
111
+ message: "Default environment:",
112
+ choices: [
113
+ { name: "staging", message: "Staging (recommended for development)" },
114
+ { name: "production", message: "Production" }
115
+ ],
116
+ initial: 0
117
+ }
118
+ ]);
119
+ const spinner = ora("Validating configuration...").start();
120
+ try {
121
+ const response = await fetch(
122
+ `https://cure-app-v2-production.up.railway.app/api/cdn/sites/${answers.siteId}`,
123
+ {
124
+ headers: {
125
+ "X-API-Key": answers.apiKey
126
+ }
127
+ }
128
+ );
129
+ if (!response.ok) {
130
+ spinner.fail("Invalid API key or site ID");
131
+ console.log(chalk.red("\nCould not validate your credentials."));
132
+ console.log(chalk.dim("Make sure your API key has access to this site."));
133
+ return;
134
+ }
135
+ spinner.succeed("Configuration validated");
136
+ const config = {
137
+ siteId: answers.siteId,
138
+ siteSlug: answers.siteSlug,
139
+ siteName: answers.siteName,
140
+ apiKey: answers.apiKey,
141
+ scriptsDir: answers.scriptsDir,
142
+ environment: answers.environment
143
+ };
144
+ saveProjectConfig(config, cwd);
145
+ const scriptsPath = join2(cwd, answers.scriptsDir);
146
+ if (!existsSync2(scriptsPath)) {
147
+ mkdirSync2(scriptsPath, { recursive: true });
148
+ }
149
+ const gitignorePath = join2(cwd, ".cure-kode", ".gitignore");
150
+ const gitignoreContent = `# Keep config.json private - contains API key
151
+ config.json
152
+ `;
153
+ const fs = await import("fs");
154
+ fs.writeFileSync(gitignorePath, gitignoreContent);
155
+ console.log(chalk.green("\n\u2705 Cure Kode initialized successfully!\n"));
156
+ console.log(chalk.dim("Project structure:"));
157
+ console.log(chalk.dim(` ${cwd}/`));
158
+ console.log(chalk.dim(` \u251C\u2500\u2500 .cure-kode/`));
159
+ console.log(chalk.dim(` \u2502 \u251C\u2500\u2500 config.json (your configuration)`));
160
+ console.log(chalk.dim(` \u2502 \u2514\u2500\u2500 .gitignore (protects API key)`));
161
+ console.log(chalk.dim(` \u2514\u2500\u2500 ${answers.scriptsDir}/`));
162
+ console.log(chalk.dim(` \u2514\u2500\u2500 (your scripts will be here)`));
163
+ console.log(chalk.bold("\nNext steps:"));
164
+ console.log(chalk.cyan(" 1. kode pull ") + chalk.dim("Download existing scripts"));
165
+ console.log(chalk.cyan(" 2. kode watch ") + chalk.dim("Watch for changes and auto-push"));
166
+ console.log(chalk.cyan(" 3. kode deploy ") + chalk.dim("Deploy to staging/production"));
167
+ } catch (error) {
168
+ spinner.fail("Initialization failed");
169
+ console.error(chalk.red("\nError:"), error);
170
+ }
171
+ }
172
+
173
+ // src/api.ts
174
+ var KodeApiError = class extends Error {
175
+ constructor(message, statusCode, response) {
176
+ super(message);
177
+ this.statusCode = statusCode;
178
+ this.response = response;
179
+ this.name = "KodeApiError";
180
+ }
181
+ };
182
+ var KodeApiClient = class {
183
+ baseUrl;
184
+ apiKey;
185
+ constructor(config) {
186
+ this.baseUrl = getApiUrl(config);
187
+ const apiKey = getApiKey(config);
188
+ if (!apiKey) {
189
+ throw new Error('API key is required. Run "kode init" first.');
190
+ }
191
+ this.apiKey = apiKey;
192
+ }
193
+ async request(endpoint, options = {}) {
194
+ const url = `${this.baseUrl}${endpoint}`;
195
+ const response = await fetch(url, {
196
+ ...options,
197
+ headers: {
198
+ "Content-Type": "application/json",
199
+ "X-API-Key": this.apiKey,
200
+ ...options.headers
201
+ }
202
+ });
203
+ const data = await response.json();
204
+ if (!response.ok) {
205
+ throw new KodeApiError(
206
+ data.error || `Request failed with status ${response.status}`,
207
+ response.status,
208
+ data
209
+ );
210
+ }
211
+ return data;
212
+ }
213
+ // Sites
214
+ async getSite(siteId) {
215
+ return this.request(`/api/cdn/sites/${siteId}`);
216
+ }
217
+ async listSites() {
218
+ return this.request("/api/cdn/sites");
219
+ }
220
+ // Scripts
221
+ async listScripts(siteId) {
222
+ return this.request(`/api/cdn/sites/${siteId}/scripts`);
223
+ }
224
+ async getScript(scriptId) {
225
+ return this.request(`/api/cdn/scripts/${scriptId}`);
226
+ }
227
+ async createScript(siteId, data) {
228
+ return this.request(`/api/cdn/sites/${siteId}/scripts`, {
229
+ method: "POST",
230
+ body: JSON.stringify(data)
231
+ });
232
+ }
233
+ async updateScript(scriptId, data) {
234
+ return this.request(`/api/cdn/scripts/${scriptId}`, {
235
+ method: "PUT",
236
+ body: JSON.stringify(data)
237
+ });
238
+ }
239
+ async deleteScript(scriptId) {
240
+ await this.request(`/api/cdn/scripts/${scriptId}`, {
241
+ method: "DELETE"
242
+ });
243
+ }
244
+ // Upload (alternative endpoint that works with API keys)
245
+ async uploadScript(data) {
246
+ return this.request("/api/cdn/upload", {
247
+ method: "POST",
248
+ body: JSON.stringify({
249
+ ...data,
250
+ apiKey: this.apiKey
251
+ })
252
+ });
253
+ }
254
+ // Pages
255
+ async listPages(siteId) {
256
+ return this.request(`/api/cdn/sites/${siteId}/pages`);
257
+ }
258
+ // Deployments
259
+ async deploy(siteId, environment = "staging") {
260
+ return this.request("/api/cdn/deploy", {
261
+ method: "POST",
262
+ body: JSON.stringify({
263
+ siteId,
264
+ environment,
265
+ apiKey: this.apiKey
266
+ })
267
+ });
268
+ }
269
+ async promoteToProduction(siteId, stagingDeploymentId) {
270
+ return this.request("/api/cdn/deploy/promote", {
271
+ method: "POST",
272
+ body: JSON.stringify({
273
+ siteId,
274
+ stagingDeploymentId,
275
+ apiKey: this.apiKey
276
+ })
277
+ });
278
+ }
279
+ async getDeploymentStatus(siteId) {
280
+ return this.request(`/api/cdn/sites/${siteId}/deployments/status`);
281
+ }
282
+ // HTML Fetch
283
+ async fetchHtml(url) {
284
+ return this.request("/api/cdn/fetch-html", {
285
+ method: "POST",
286
+ body: JSON.stringify({ url })
287
+ });
288
+ }
289
+ };
290
+ function createApiClient(config) {
291
+ return new KodeApiClient(config);
292
+ }
293
+
294
+ // src/commands/pull.ts
295
+ import chalk2 from "chalk";
296
+ import ora2 from "ora";
297
+ import { writeFileSync as writeFileSync2, mkdirSync as mkdirSync3, existsSync as existsSync3 } from "fs";
298
+ import { join as join3 } from "path";
299
+ async function pullCommand(options) {
300
+ const projectRoot = findProjectRoot();
301
+ if (!projectRoot) {
302
+ console.log(chalk2.red("\u274C Not in a Cure Kode project."));
303
+ console.log(chalk2.dim(' Run "kode init" first.'));
304
+ return;
305
+ }
306
+ const config = getProjectConfig(projectRoot);
307
+ if (!config) {
308
+ console.log(chalk2.red("\u274C Could not read project configuration."));
309
+ return;
310
+ }
311
+ const spinner = ora2("Fetching scripts...").start();
312
+ try {
313
+ const client = createApiClient(config);
314
+ const scripts = await client.listScripts(config.siteId);
315
+ if (scripts.length === 0) {
316
+ spinner.info("No scripts found on remote.");
317
+ console.log(chalk2.dim('\nCreate scripts in Cure App or use "kode push" to upload local scripts.'));
318
+ return;
319
+ }
320
+ spinner.succeed(`Found ${scripts.length} script(s)`);
321
+ const scriptsDir = getScriptsDir(projectRoot, config);
322
+ if (!existsSync3(scriptsDir)) {
323
+ mkdirSync3(scriptsDir, { recursive: true });
324
+ }
325
+ const scriptsToPull = options.script ? scripts.filter((s) => s.slug === options.script || s.name === options.script) : scripts;
326
+ if (options.script && scriptsToPull.length === 0) {
327
+ console.log(chalk2.yellow(`
328
+ \u26A0\uFE0F Script "${options.script}" not found.`));
329
+ console.log(chalk2.dim("Available scripts:"));
330
+ scripts.forEach((s) => {
331
+ console.log(chalk2.dim(` - ${s.slug} (${s.name})`));
332
+ });
333
+ return;
334
+ }
335
+ console.log();
336
+ let pulled = 0;
337
+ let skipped = 0;
338
+ for (const script of scriptsToPull) {
339
+ const ext = script.type === "javascript" ? "js" : "css";
340
+ const fileName = `${script.slug}.${ext}`;
341
+ const filePath = join3(scriptsDir, fileName);
342
+ if (existsSync3(filePath) && !options.force) {
343
+ const localContent = await import("fs").then(
344
+ (fs) => fs.readFileSync(filePath, "utf-8")
345
+ );
346
+ if (localContent !== script.content) {
347
+ console.log(
348
+ chalk2.yellow(` \u26A0 ${fileName}`) + chalk2.dim(" (local changes exist, use --force to overwrite)")
349
+ );
350
+ skipped++;
351
+ continue;
352
+ }
353
+ }
354
+ writeFileSync2(filePath, script.content);
355
+ const scopeTag = script.scope === "global" ? chalk2.blue("[G]") : chalk2.magenta("[P]");
356
+ console.log(
357
+ chalk2.green(` \u2713 ${fileName}`) + chalk2.dim(` v${script.current_version} ${scopeTag}`)
358
+ );
359
+ pulled++;
360
+ }
361
+ console.log();
362
+ if (pulled > 0) {
363
+ console.log(chalk2.green(`\u2705 Pulled ${pulled} script(s) to ${config.scriptsDir}/`));
364
+ }
365
+ if (skipped > 0) {
366
+ console.log(chalk2.yellow(`\u26A0\uFE0F Skipped ${skipped} script(s) with local changes`));
367
+ }
368
+ const metadataPath = join3(projectRoot, ".cure-kode", "scripts.json");
369
+ const metadata = scripts.map((s) => ({
370
+ id: s.id,
371
+ slug: s.slug,
372
+ name: s.name,
373
+ type: s.type,
374
+ scope: s.scope,
375
+ version: s.current_version,
376
+ loadOrder: s.load_order
377
+ }));
378
+ writeFileSync2(metadataPath, JSON.stringify(metadata, null, 2));
379
+ } catch (error) {
380
+ spinner.fail("Failed to pull scripts");
381
+ console.error(chalk2.red("\nError:"), error);
382
+ }
383
+ }
384
+
385
+ // src/commands/push.ts
386
+ import chalk3 from "chalk";
387
+ import ora3 from "ora";
388
+ import { readFileSync as readFileSync2, existsSync as existsSync4, readdirSync } from "fs";
389
+ import { join as join4, basename, extname } from "path";
390
+ async function pushCommand(options) {
391
+ const projectRoot = findProjectRoot();
392
+ if (!projectRoot) {
393
+ console.log(chalk3.red("\u274C Not in a Cure Kode project."));
394
+ console.log(chalk3.dim(' Run "kode init" first.'));
395
+ return;
396
+ }
397
+ const config = getProjectConfig(projectRoot);
398
+ if (!config) {
399
+ console.log(chalk3.red("\u274C Could not read project configuration."));
400
+ return;
401
+ }
402
+ const scriptsDir = getScriptsDir(projectRoot, config);
403
+ if (!existsSync4(scriptsDir)) {
404
+ console.log(chalk3.yellow("\u26A0\uFE0F Scripts directory not found."));
405
+ console.log(chalk3.dim(` Expected: ${scriptsDir}`));
406
+ console.log(chalk3.dim(' Run "kode pull" first or create scripts manually.'));
407
+ return;
408
+ }
409
+ const metadataPath = join4(projectRoot, ".cure-kode", "scripts.json");
410
+ let metadata = [];
411
+ if (existsSync4(metadataPath)) {
412
+ metadata = JSON.parse(readFileSync2(metadataPath, "utf-8"));
413
+ }
414
+ const files = readdirSync(scriptsDir).filter(
415
+ (f) => f.endsWith(".js") || f.endsWith(".css")
416
+ );
417
+ if (files.length === 0) {
418
+ console.log(chalk3.yellow("\u26A0\uFE0F No script files found."));
419
+ console.log(chalk3.dim(` Add .js or .css files to ${scriptsDir}/`));
420
+ return;
421
+ }
422
+ const filesToPush = options.script ? files.filter(
423
+ (f) => basename(f, extname(f)) === options.script || f === options.script || f === `${options.script}.js` || f === `${options.script}.css`
424
+ ) : files;
425
+ if (options.script && filesToPush.length === 0) {
426
+ console.log(chalk3.yellow(`\u26A0\uFE0F Script "${options.script}" not found locally.`));
427
+ console.log(chalk3.dim("Available local scripts:"));
428
+ files.forEach((f) => {
429
+ console.log(chalk3.dim(` - ${f}`));
430
+ });
431
+ return;
432
+ }
433
+ const spinner = ora3("Pushing scripts...").start();
434
+ try {
435
+ const client = createApiClient(config);
436
+ const remoteScripts = await client.listScripts(config.siteId);
437
+ let pushed = 0;
438
+ let created = 0;
439
+ let skipped = 0;
440
+ spinner.stop();
441
+ console.log();
442
+ for (const file of filesToPush) {
443
+ const filePath = join4(scriptsDir, file);
444
+ const content = readFileSync2(filePath, "utf-8");
445
+ const slug = basename(file, extname(file));
446
+ const type = extname(file) === ".js" ? "javascript" : "css";
447
+ const remoteScript = remoteScripts.find((s) => s.slug === slug);
448
+ const localMeta = metadata.find((m) => m.slug === slug);
449
+ if (remoteScript) {
450
+ if (remoteScript.content === content && !options.all) {
451
+ console.log(chalk3.dim(` - ${file} (no changes)`));
452
+ skipped++;
453
+ continue;
454
+ }
455
+ const updateSpinner = ora3(`Updating ${file}...`).start();
456
+ await client.updateScript(remoteScript.id, {
457
+ content,
458
+ changeSummary: options.message || `Updated via CLI`
459
+ });
460
+ updateSpinner.succeed(
461
+ chalk3.green(` \u2713 ${file}`) + chalk3.dim(` \u2192 v${remoteScript.current_version + 1}`)
462
+ );
463
+ pushed++;
464
+ } else {
465
+ const createSpinner = ora3(`Creating ${file}...`).start();
466
+ const newScript = await client.createScript(config.siteId, {
467
+ name: slug.charAt(0).toUpperCase() + slug.slice(1).replace(/-/g, " "),
468
+ slug,
469
+ type,
470
+ scope: localMeta?.scope || "global",
471
+ content
472
+ });
473
+ createSpinner.succeed(
474
+ chalk3.green(` \u2713 ${file}`) + chalk3.cyan(" (new)")
475
+ );
476
+ created++;
477
+ }
478
+ }
479
+ console.log();
480
+ if (pushed > 0) {
481
+ console.log(chalk3.green(`\u2705 Updated ${pushed} script(s)`));
482
+ }
483
+ if (created > 0) {
484
+ console.log(chalk3.cyan(`\u2705 Created ${created} new script(s)`));
485
+ }
486
+ if (skipped > 0) {
487
+ console.log(chalk3.dim(` Skipped ${skipped} unchanged script(s)`));
488
+ }
489
+ const updatedScripts = await client.listScripts(config.siteId);
490
+ const updatedMetadata = updatedScripts.map((s) => ({
491
+ id: s.id,
492
+ slug: s.slug,
493
+ name: s.name,
494
+ type: s.type,
495
+ scope: s.scope,
496
+ version: s.current_version,
497
+ loadOrder: s.load_order
498
+ }));
499
+ const fs = await import("fs");
500
+ fs.writeFileSync(metadataPath, JSON.stringify(updatedMetadata, null, 2));
501
+ } catch (error) {
502
+ spinner.fail("Failed to push scripts");
503
+ console.error(chalk3.red("\nError:"), error);
504
+ }
505
+ }
506
+
507
+ // src/commands/watch.ts
508
+ import chalk4 from "chalk";
509
+ import chokidar from "chokidar";
510
+ import { readFileSync as readFileSync3, existsSync as existsSync5 } from "fs";
511
+ import { join as join5, basename as basename2, extname as extname2 } from "path";
512
+ async function watchCommand(options) {
513
+ const projectRoot = findProjectRoot();
514
+ if (!projectRoot) {
515
+ console.log(chalk4.red("\u274C Not in a Cure Kode project."));
516
+ console.log(chalk4.dim(' Run "kode init" first.'));
517
+ return;
518
+ }
519
+ const config = getProjectConfig(projectRoot);
520
+ if (!config) {
521
+ console.log(chalk4.red("\u274C Could not read project configuration."));
522
+ return;
523
+ }
524
+ const scriptsDir = getScriptsDir(projectRoot, config);
525
+ if (!existsSync5(scriptsDir)) {
526
+ console.log(chalk4.yellow("\u26A0\uFE0F Scripts directory not found."));
527
+ console.log(chalk4.dim(` Expected: ${scriptsDir}`));
528
+ console.log(chalk4.dim(' Run "kode pull" first.'));
529
+ return;
530
+ }
531
+ console.log(chalk4.bold("\n\u{1F440} Watching for changes...\n"));
532
+ console.log(chalk4.dim(` Directory: ${scriptsDir}`));
533
+ console.log(chalk4.dim(` Site: ${config.siteName} (${config.siteSlug})`));
534
+ console.log(chalk4.dim(` Environment: ${config.environment || "staging"}`));
535
+ if (options.deploy) {
536
+ console.log(chalk4.cyan(` Auto-deploy: enabled`));
537
+ }
538
+ console.log();
539
+ console.log(chalk4.dim("Press Ctrl+C to stop.\n"));
540
+ const client = createApiClient(config);
541
+ const metadataPath = join5(projectRoot, ".cure-kode", "scripts.json");
542
+ let metadata = [];
543
+ if (existsSync5(metadataPath)) {
544
+ metadata = JSON.parse(readFileSync3(metadataPath, "utf-8"));
545
+ }
546
+ let remoteScripts = [];
547
+ try {
548
+ remoteScripts = await client.listScripts(config.siteId);
549
+ console.log(chalk4.dim(`Loaded ${remoteScripts.length} remote script(s)
550
+ `));
551
+ } catch (error) {
552
+ console.log(chalk4.yellow("\u26A0\uFE0F Could not load remote scripts. Will create new on change."));
553
+ }
554
+ const pendingChanges = /* @__PURE__ */ new Map();
555
+ const DEBOUNCE_MS = 500;
556
+ const handleChange = async (filePath) => {
557
+ const fileName = basename2(filePath);
558
+ const slug = basename2(fileName, extname2(fileName));
559
+ const type = extname2(fileName) === ".js" ? "javascript" : "css";
560
+ if (pendingChanges.has(filePath)) {
561
+ clearTimeout(pendingChanges.get(filePath));
562
+ }
563
+ const timeout = setTimeout(async () => {
564
+ pendingChanges.delete(filePath);
565
+ try {
566
+ const content = readFileSync3(filePath, "utf-8");
567
+ const remoteScript = remoteScripts.find((s) => s.slug === slug);
568
+ const localMeta = metadata.find((m) => m.slug === slug);
569
+ const timestamp = (/* @__PURE__ */ new Date()).toLocaleTimeString("nb-NO");
570
+ if (remoteScript) {
571
+ if (remoteScript.content === content) {
572
+ return;
573
+ }
574
+ await client.updateScript(remoteScript.id, {
575
+ content,
576
+ changeSummary: "Updated via watch"
577
+ });
578
+ remoteScript.content = content;
579
+ remoteScript.current_version++;
580
+ console.log(
581
+ chalk4.dim(`[${timestamp}] `) + chalk4.green(`\u2713 ${fileName}`) + chalk4.dim(` \u2192 v${remoteScript.current_version}`)
582
+ );
583
+ if (options.deploy) {
584
+ try {
585
+ await client.deploy(config.siteId, config.environment || "staging");
586
+ console.log(
587
+ chalk4.dim(`[${timestamp}] `) + chalk4.cyan(` \u21B3 Deployed to ${config.environment || "staging"}`)
588
+ );
589
+ } catch (deployError) {
590
+ console.log(
591
+ chalk4.dim(`[${timestamp}] `) + chalk4.red(` \u21B3 Deploy failed`)
592
+ );
593
+ }
594
+ }
595
+ } else {
596
+ const newScript = await client.createScript(config.siteId, {
597
+ name: slug.charAt(0).toUpperCase() + slug.slice(1).replace(/-/g, " "),
598
+ slug,
599
+ type,
600
+ scope: localMeta?.scope || "global",
601
+ content
602
+ });
603
+ remoteScripts.push(newScript);
604
+ console.log(
605
+ chalk4.dim(`[${timestamp}] `) + chalk4.green(`\u2713 ${fileName}`) + chalk4.cyan(" (created)")
606
+ );
607
+ const updatedMetadata = remoteScripts.map((s) => ({
608
+ id: s.id,
609
+ slug: s.slug,
610
+ name: s.name,
611
+ type: s.type,
612
+ scope: s.scope,
613
+ version: s.current_version,
614
+ loadOrder: s.load_order
615
+ }));
616
+ const fs = await import("fs");
617
+ fs.writeFileSync(metadataPath, JSON.stringify(updatedMetadata, null, 2));
618
+ }
619
+ } catch (error) {
620
+ const timestamp = (/* @__PURE__ */ new Date()).toLocaleTimeString("nb-NO");
621
+ console.log(
622
+ chalk4.dim(`[${timestamp}] `) + chalk4.red(`\u2717 ${fileName}`) + chalk4.dim(` - ${error.message || "Unknown error"}`)
623
+ );
624
+ }
625
+ }, DEBOUNCE_MS);
626
+ pendingChanges.set(filePath, timeout);
627
+ };
628
+ const watcher = chokidar.watch(join5(scriptsDir, "**/*.{js,css}"), {
629
+ persistent: true,
630
+ ignoreInitial: true,
631
+ awaitWriteFinish: {
632
+ stabilityThreshold: 300,
633
+ pollInterval: 100
634
+ }
635
+ });
636
+ watcher.on("change", handleChange);
637
+ watcher.on("add", handleChange);
638
+ watcher.on("unlink", (filePath) => {
639
+ const timestamp = (/* @__PURE__ */ new Date()).toLocaleTimeString("nb-NO");
640
+ const fileName = basename2(filePath);
641
+ console.log(
642
+ chalk4.dim(`[${timestamp}] `) + chalk4.yellow(`\u26A0 ${fileName} deleted locally`) + chalk4.dim(" (remote not affected)")
643
+ );
644
+ });
645
+ process.on("SIGINT", () => {
646
+ console.log(chalk4.dim("\n\nStopping watch...\n"));
647
+ watcher.close();
648
+ process.exit(0);
649
+ });
650
+ }
651
+
652
+ // src/commands/deploy.ts
653
+ import chalk5 from "chalk";
654
+ import ora4 from "ora";
655
+ async function deployCommand(environment, options) {
656
+ const projectRoot = findProjectRoot();
657
+ if (!projectRoot) {
658
+ console.log(chalk5.red("\u274C Not in a Cure Kode project."));
659
+ console.log(chalk5.dim(' Run "kode init" first.'));
660
+ return;
661
+ }
662
+ const config = getProjectConfig(projectRoot);
663
+ if (!config) {
664
+ console.log(chalk5.red("\u274C Could not read project configuration."));
665
+ return;
666
+ }
667
+ const env = environment || config.environment || "staging";
668
+ if (options?.promote) {
669
+ const spinner2 = ora4("Promoting staging to production...").start();
670
+ try {
671
+ const client = createApiClient(config);
672
+ const deployment = await client.promoteToProduction(config.siteId);
673
+ spinner2.succeed("Promoted to production");
674
+ console.log();
675
+ console.log(chalk5.dim("Deployment details:"));
676
+ console.log(chalk5.dim(` Version: ${deployment.version}`));
677
+ console.log(chalk5.dim(` Status: ${deployment.status}`));
678
+ console.log();
679
+ console.log(chalk5.green("\u2705 Production is now running the latest staging version!"));
680
+ } catch (error) {
681
+ spinner2.fail("Promotion failed");
682
+ console.error(chalk5.red("\nError:"), error.message || error);
683
+ }
684
+ return;
685
+ }
686
+ const spinner = ora4(`Deploying to ${env}...`).start();
687
+ try {
688
+ const client = createApiClient(config);
689
+ const deployment = await client.deploy(config.siteId, env);
690
+ spinner.succeed(`Deployed to ${env}`);
691
+ console.log();
692
+ console.log(chalk5.dim("Deployment details:"));
693
+ console.log(chalk5.dim(` Version: ${deployment.version}`));
694
+ console.log(chalk5.dim(` Status: ${deployment.status}`));
695
+ console.log(chalk5.dim(` Started: ${new Date(deployment.started_at).toLocaleString("nb-NO")}`));
696
+ console.log();
697
+ if (env === "staging") {
698
+ console.log(chalk5.cyan('\u{1F4A1} Tip: Use "kode deploy production" or "kode deploy --promote" to go live.'));
699
+ } else {
700
+ console.log(chalk5.green("\u2705 Changes are now live!"));
701
+ }
702
+ } catch (error) {
703
+ spinner.fail("Deployment failed");
704
+ console.error(chalk5.red("\nError:"), error.message || error);
705
+ }
706
+ }
707
+
708
+ // src/commands/html.ts
709
+ import chalk6 from "chalk";
710
+ import ora5 from "ora";
711
+ async function htmlCommand(url, options) {
712
+ const projectRoot = findProjectRoot();
713
+ if (!projectRoot) {
714
+ console.log(chalk6.red("\u274C Not in a Cure Kode project."));
715
+ console.log(chalk6.dim(' Run "kode init" first.'));
716
+ return;
717
+ }
718
+ const config = getProjectConfig(projectRoot);
719
+ if (!config) {
720
+ console.log(chalk6.red("\u274C Could not read project configuration."));
721
+ return;
722
+ }
723
+ let parsedUrl;
724
+ try {
725
+ parsedUrl = new URL(url.startsWith("http") ? url : `https://${url}`);
726
+ } catch {
727
+ console.log(chalk6.red("\u274C Invalid URL."));
728
+ return;
729
+ }
730
+ const spinner = ora5(`Fetching ${parsedUrl.hostname}${parsedUrl.pathname}...`).start();
731
+ try {
732
+ const client = createApiClient(config);
733
+ const result = await client.fetchHtml(parsedUrl.toString());
734
+ spinner.succeed("HTML fetched");
735
+ if (options?.json) {
736
+ console.log(JSON.stringify(result, null, 2));
737
+ return;
738
+ }
739
+ console.log();
740
+ console.log(chalk6.bold(result.title || parsedUrl.hostname));
741
+ console.log(chalk6.dim(result.url));
742
+ console.log();
743
+ const hasCureKode = result.scripts.cureKode.length > 0;
744
+ if (hasCureKode) {
745
+ console.log(chalk6.green("\u2705 Cure Kode installed"));
746
+ } else {
747
+ console.log(chalk6.yellow("\u26A0\uFE0F Cure Kode not found"));
748
+ console.log(chalk6.dim(" Add this to your Webflow site:"));
749
+ console.log(chalk6.cyan(` <script src="https://cure-app-v2-production.up.railway.app/api/cdn/${config.siteSlug}/init.js"></script>`));
750
+ }
751
+ console.log();
752
+ console.log(chalk6.dim("\u2500".repeat(50)));
753
+ console.log(
754
+ ` Scripts: ${result.stats.totalScripts}` + chalk6.dim(` (${result.stats.externalScripts} external, ${result.stats.inlineScripts} inline)`)
755
+ );
756
+ console.log(` Styles: ${result.stats.totalStyles}`);
757
+ console.log(chalk6.dim("\u2500".repeat(50)));
758
+ console.log();
759
+ if (!options?.styles) {
760
+ console.log(chalk6.bold("Scripts"));
761
+ console.log();
762
+ if (result.scripts.webflow.length > 0) {
763
+ console.log(chalk6.blue(" Webflow:"));
764
+ result.scripts.webflow.forEach((s) => {
765
+ console.log(chalk6.dim(` ${s.src || "(inline)"}`));
766
+ });
767
+ }
768
+ if (result.scripts.cureKode.length > 0) {
769
+ console.log(chalk6.green(" Cure Kode:"));
770
+ result.scripts.cureKode.forEach((s) => {
771
+ console.log(chalk6.dim(` ${s.src || "(inline)"}`));
772
+ });
773
+ }
774
+ if (result.scripts.thirdParty.length > 0) {
775
+ console.log(chalk6.yellow(" Third-party:"));
776
+ result.scripts.thirdParty.forEach((s) => {
777
+ console.log(chalk6.dim(` ${s.src || "(inline)"}`));
778
+ });
779
+ }
780
+ if (result.scripts.custom.length > 0) {
781
+ console.log(chalk6.magenta(" Custom:"));
782
+ result.scripts.custom.forEach((s) => {
783
+ const label = s.src || (s.inline ? `(inline, ${s.content?.length || 0} chars)` : "(inline)");
784
+ console.log(chalk6.dim(` ${label}`));
785
+ });
786
+ }
787
+ console.log();
788
+ }
789
+ if (result.detectedComponents.length > 0) {
790
+ console.log(chalk6.bold("Detected Components"));
791
+ console.log();
792
+ result.detectedComponents.forEach((comp) => {
793
+ console.log(chalk6.dim(` \u2022 ${comp}`));
794
+ });
795
+ console.log();
796
+ }
797
+ if (options?.styles) {
798
+ console.log(chalk6.bold("Stylesheets"));
799
+ console.log();
800
+ result.styles.forEach((style) => {
801
+ if (style.href) {
802
+ const isWebflow = style.isWebflow ? chalk6.blue(" [WF]") : "";
803
+ console.log(chalk6.dim(` ${style.href}${isWebflow}`));
804
+ } else {
805
+ console.log(chalk6.dim(" (inline style)"));
806
+ }
807
+ });
808
+ console.log();
809
+ }
810
+ } catch (error) {
811
+ spinner.fail("Failed to fetch HTML");
812
+ console.error(chalk6.red("\nError:"), error.message || error);
813
+ }
814
+ }
815
+
816
+ // src/commands/status.ts
817
+ import chalk7 from "chalk";
818
+ import ora6 from "ora";
819
+ import { readFileSync as readFileSync4, existsSync as existsSync6, readdirSync as readdirSync2, statSync } from "fs";
820
+ import { join as join6, basename as basename3, extname as extname3 } from "path";
821
+ async function statusCommand(options) {
822
+ const projectRoot = findProjectRoot();
823
+ if (!projectRoot) {
824
+ console.log(chalk7.red("\u274C Not in a Cure Kode project."));
825
+ console.log(chalk7.dim(' Run "kode init" first.'));
826
+ return;
827
+ }
828
+ const config = getProjectConfig(projectRoot);
829
+ if (!config) {
830
+ console.log(chalk7.red("\u274C Could not read project configuration."));
831
+ return;
832
+ }
833
+ console.log();
834
+ console.log(chalk7.bold(`\u{1F4E6} ${config.siteName}`));
835
+ console.log(chalk7.dim(` Slug: ${config.siteSlug}`));
836
+ console.log(chalk7.dim(` ID: ${config.siteId}`));
837
+ console.log();
838
+ const spinner = ora6("Fetching status...").start();
839
+ try {
840
+ const client = createApiClient(config);
841
+ const [remoteScripts, deployStatus] = await Promise.all([
842
+ client.listScripts(config.siteId),
843
+ client.getDeploymentStatus(config.siteId)
844
+ ]);
845
+ spinner.stop();
846
+ console.log(chalk7.bold("Environments"));
847
+ console.log();
848
+ const stagingStatus = deployStatus.staging.lastSuccessful;
849
+ if (stagingStatus) {
850
+ console.log(
851
+ chalk7.blue(" Staging: ") + chalk7.green("\u25CF") + chalk7.dim(` v${stagingStatus.version}`) + chalk7.dim(` (${formatDate(stagingStatus.completedAt)})`)
852
+ );
853
+ } else {
854
+ console.log(chalk7.blue(" Staging: ") + chalk7.yellow("\u25CB") + chalk7.dim(" No deployments"));
855
+ }
856
+ const prodStatus = deployStatus.production.lastSuccessful;
857
+ if (prodStatus) {
858
+ console.log(
859
+ chalk7.green(" Production: ") + chalk7.green("\u25CF") + chalk7.dim(` v${prodStatus.version}`) + chalk7.dim(` (${formatDate(prodStatus.completedAt)})`)
860
+ );
861
+ } else {
862
+ console.log(
863
+ chalk7.green(" Production: ") + chalk7.yellow("\u25CB") + chalk7.dim(" No deployments")
864
+ );
865
+ }
866
+ if (deployStatus.canPromote) {
867
+ console.log();
868
+ console.log(
869
+ chalk7.cyan(' \u{1F4A1} Staging is ahead of production. Run "kode deploy --promote" to update.')
870
+ );
871
+ }
872
+ console.log();
873
+ console.log(chalk7.bold("Scripts"));
874
+ console.log();
875
+ const scriptsDir = getScriptsDir(projectRoot, config);
876
+ const localFiles = existsSync6(scriptsDir) ? readdirSync2(scriptsDir).filter((f) => f.endsWith(".js") || f.endsWith(".css")) : [];
877
+ const localBySlug = /* @__PURE__ */ new Map();
878
+ for (const file of localFiles) {
879
+ const slug = basename3(file, extname3(file));
880
+ const filePath = join6(scriptsDir, file);
881
+ const content = readFileSync4(filePath, "utf-8");
882
+ const stats = statSync(filePath);
883
+ localBySlug.set(slug, { file, content, modified: stats.mtime });
884
+ }
885
+ const remoteBySlug = /* @__PURE__ */ new Map();
886
+ for (const script of remoteScripts) {
887
+ remoteBySlug.set(script.slug, script);
888
+ }
889
+ const allSlugs = /* @__PURE__ */ new Set([...localBySlug.keys(), ...remoteBySlug.keys()]);
890
+ for (const slug of allSlugs) {
891
+ const local = localBySlug.get(slug);
892
+ const remote = remoteBySlug.get(slug);
893
+ if (local && remote) {
894
+ const isChanged = local.content !== remote.content;
895
+ const scopeTag = remote.scope === "global" ? chalk7.blue("[G]") : chalk7.magenta("[P]");
896
+ if (isChanged) {
897
+ console.log(
898
+ chalk7.yellow(" M ") + `${local.file}` + chalk7.dim(` v${remote.current_version} ${scopeTag}`) + chalk7.yellow(" (modified)")
899
+ );
900
+ } else {
901
+ console.log(
902
+ chalk7.green(" \u2713 ") + chalk7.dim(`${local.file}`) + chalk7.dim(` v${remote.current_version} ${scopeTag}`)
903
+ );
904
+ }
905
+ } else if (local && !remote) {
906
+ console.log(
907
+ chalk7.cyan(" + ") + `${local.file}` + chalk7.cyan(" (new, not pushed)")
908
+ );
909
+ } else if (!local && remote) {
910
+ const ext = remote.type === "javascript" ? "js" : "css";
911
+ const fileName = `${slug}.${ext}`;
912
+ console.log(
913
+ chalk7.red(" - ") + chalk7.dim(`${fileName}`) + chalk7.red(" (remote only, not pulled)")
914
+ );
915
+ }
916
+ }
917
+ if (allSlugs.size === 0) {
918
+ console.log(chalk7.dim(" No scripts yet."));
919
+ }
920
+ console.log();
921
+ const modified = [...allSlugs].filter((slug) => {
922
+ const local = localBySlug.get(slug);
923
+ const remote = remoteBySlug.get(slug);
924
+ return local && remote && local.content !== remote.content;
925
+ }).length;
926
+ const newLocal = [...allSlugs].filter(
927
+ (slug) => localBySlug.has(slug) && !remoteBySlug.has(slug)
928
+ ).length;
929
+ const remoteOnly = [...allSlugs].filter(
930
+ (slug) => !localBySlug.has(slug) && remoteBySlug.has(slug)
931
+ ).length;
932
+ if (modified > 0 || newLocal > 0) {
933
+ console.log(chalk7.bold("Actions"));
934
+ console.log();
935
+ if (modified > 0) {
936
+ console.log(chalk7.yellow(` ${modified} modified`) + chalk7.dim(' \u2192 Run "kode push" to upload'));
937
+ }
938
+ if (newLocal > 0) {
939
+ console.log(chalk7.cyan(` ${newLocal} new local`) + chalk7.dim(' \u2192 Run "kode push" to create'));
940
+ }
941
+ if (remoteOnly > 0) {
942
+ console.log(chalk7.red(` ${remoteOnly} remote only`) + chalk7.dim(' \u2192 Run "kode pull" to download'));
943
+ }
944
+ console.log();
945
+ }
946
+ } catch (error) {
947
+ spinner.fail("Failed to fetch status");
948
+ console.error(chalk7.red("\nError:"), error.message || error);
949
+ }
950
+ }
951
+ function formatDate(dateStr) {
952
+ if (!dateStr) return "Unknown";
953
+ const date = new Date(dateStr);
954
+ return date.toLocaleString("nb-NO", {
955
+ day: "2-digit",
956
+ month: "2-digit",
957
+ hour: "2-digit",
958
+ minute: "2-digit"
959
+ });
960
+ }
961
+
962
+ export {
963
+ findProjectRoot,
964
+ getProjectConfig,
965
+ saveProjectConfig,
966
+ getApiUrl,
967
+ getApiKey,
968
+ setGlobalConfig,
969
+ getScriptsDir,
970
+ initCommand,
971
+ KodeApiError,
972
+ KodeApiClient,
973
+ createApiClient,
974
+ pullCommand,
975
+ pushCommand,
976
+ watchCommand,
977
+ deployCommand,
978
+ htmlCommand,
979
+ statusCommand
980
+ };