@curenorway/kode-cli 1.18.0 → 2.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,500 @@
1
+ import {
2
+ createApiClient,
3
+ findProjectRoot,
4
+ getProjectConfig,
5
+ getScriptsDir
6
+ } from "./chunk-TLLGB46I.js";
7
+
8
+ // src/commands/pull.ts
9
+ import chalk from "chalk";
10
+ import ora from "ora";
11
+ import { writeFileSync as writeFileSync2, readFileSync as readFileSync2, mkdirSync, existsSync as existsSync2 } from "fs";
12
+ import { join as join2, dirname } from "path";
13
+ import { createHash } from "crypto";
14
+
15
+ // src/lib/claude-md.ts
16
+ import { existsSync, readFileSync, writeFileSync } from "fs";
17
+ import { join } from "path";
18
+ var KODE_REFERENCE = `## Cure Kode
19
+
20
+ This project uses **Cure Kode** \u2014 a TypeScript/React/JS/CSS build system for Webflow.
21
+
22
+ **Supports:** \`.ts\`, \`.tsx\`, \`.jsx\`, \`.js\`, \`.css\` \u2014 bundled with esbuild, React auto-loaded from CDN.
23
+
24
+ **Before modifying scripts:**
25
+
26
+ 1. Run \`kode pull\` to get latest from server
27
+ 2. Make your changes in \`.cure-kode-scripts/\`
28
+ 3. Run \`kode push\` to upload, then \`kode deploy\` to publish
29
+
30
+ **Packages:** \`kode pkg add <name>\` to install, \`import { ... } from '@kode/<name>'\` to use.
31
+
32
+ **CMS Types:** Auto-generated in \`types/cms.d.ts\`. Refresh with \`kode types\`.
33
+
34
+ Full documentation: [.cure-kode/KODE.md](.cure-kode/KODE.md)
35
+
36
+ ---
37
+
38
+ `;
39
+ function hasKodeReference(content) {
40
+ return content.includes(".cure-kode/KODE.md") || content.includes("cure-kode/KODE.md") || content.includes("KODE.md");
41
+ }
42
+ function generateKodeMd(siteName, siteSlug, scripts, pages, options) {
43
+ const productionEnabled = options?.productionEnabled ?? false;
44
+ let md = `# Cure Kode: ${siteName}
45
+
46
+ This project uses **Cure Kode** CDN for JavaScript/CSS delivery to Webflow.
47
+
48
+ > **This file is auto-generated.** Do not edit manually - changes will be overwritten by \`kode pull\`, \`kode push\`, and \`kode sync\`.
49
+
50
+ **Produksjon:** ${productionEnabled ? "\u2713 Aktivert \u2014 deploy til staging og produksjon" : "\u25CB Deaktivert \u2014 kun staging. Ikke hent fra produksjons-URL."}
51
+
52
+ ---
53
+
54
+ `;
55
+ md += `## CDN URL
56
+
57
+ Add this to Webflow \u2192 Project Settings \u2192 Custom Code \u2192 Head:
58
+
59
+ \`\`\`html
60
+ <script src="https://app.cure.no/api/cdn/${siteSlug}/init.js"></script>
61
+ \`\`\`
62
+
63
+ ---
64
+
65
+ `;
66
+ md += `## Scripts
67
+
68
+ `;
69
+ if (scripts.length > 0) {
70
+ md += `| Script | Type | Scope | Auto | Purpose |
71
+ |--------|------|-------|------|---------|
72
+ `;
73
+ for (const script of scripts) {
74
+ const type = script.type === "javascript" ? "JS" : "CSS";
75
+ const scope = script.scope === "global" ? "Global" : "Page";
76
+ const auto = script.autoLoad ? "\u26A1" : "\u25CB";
77
+ const purpose = script.purpose || script.description || "\u2014";
78
+ const truncated = purpose.length > 50 ? purpose.slice(0, 47) + "..." : purpose;
79
+ md += `| \`${script.slug}\` | ${type} | ${scope} | ${auto} | ${truncated} |
80
+ `;
81
+ }
82
+ md += `
83
+ > \u26A1 = auto-loads on every page, \u25CB = manual load via \`CK.loadScript('slug')\`
84
+
85
+ `;
86
+ } else {
87
+ md += `No scripts yet. Create one with \`kode push\`.
88
+
89
+ `;
90
+ }
91
+ if (pages.length > 0) {
92
+ md += `---
93
+
94
+ ## Pages
95
+
96
+ Page-specific scripts only load when URL matches these patterns:
97
+
98
+ `;
99
+ for (const page of pages) {
100
+ md += `- **${page.name}** (\`${page.slug}\`) \u2192 \`${page.patterns.join("`, `")}\`
101
+ `;
102
+ }
103
+ md += `
104
+ `;
105
+ }
106
+ const pageSpecificScripts = scripts.filter(
107
+ (s) => s.scope === "page-specific" && s.pageAssignments && s.pageAssignments.length > 0
108
+ );
109
+ if (pageSpecificScripts.length > 0) {
110
+ md += `---
111
+
112
+ ## Script \u2192 Page Assignments
113
+
114
+ | Script | Pages |
115
+ |--------|-------|
116
+ `;
117
+ for (const script of pageSpecificScripts) {
118
+ const pageList = script.pageAssignments.map((pa) => {
119
+ const page = pages.find((p) => p.slug === pa.pageSlug);
120
+ const patterns = page ? ` (\`${page.patterns.join("`, `")}\`)` : "";
121
+ return `\`${pa.pageSlug}\`${patterns}`;
122
+ }).join(", ");
123
+ md += `| \`${script.slug}\` | ${pageList} |
124
+ `;
125
+ }
126
+ md += `
127
+ `;
128
+ }
129
+ md += `---
130
+
131
+ ## Workflow
132
+
133
+ **Before making changes:**
134
+ \`\`\`bash
135
+ kode pull # Get latest scripts, modules, packages, and CMS types
136
+ \`\`\`
137
+
138
+ **After making changes:**
139
+ \`\`\`bash
140
+ kode push # Upload scripts + module files (auto-bundles TS/React)
141
+ kode deploy # Deploy to staging
142
+ kode deploy --promote # When ready, promote to production
143
+ \`\`\`
144
+
145
+ **Packages:**
146
+ \`\`\`bash
147
+ kode pkg list # Browse available packages
148
+ kode pkg add <name> # Install a package
149
+ kode pkg remove <name> # Remove a package
150
+ kode pkg publish --path <path> --slug <name> --description "..." # Publish
151
+ \`\`\`
152
+
153
+ **CMS Types:**
154
+ \`\`\`bash
155
+ kode types # Regenerate CMS types from Webflow (also runs on pull/init)
156
+ \`\`\`
157
+
158
+ ---
159
+
160
+ ## CLI Commands
161
+
162
+ | Command | Description |
163
+ |---------|-------------|
164
+ | \`kode init\` | Setup project (use \`--dev\` for localhost) |
165
+ | \`kode pull\` | Download scripts, modules, packages, CMS types |
166
+ | \`kode push\` | Upload changes (bundles .ts/.tsx/.jsx with esbuild) |
167
+ | \`kode deploy\` | Deploy to staging |
168
+ | \`kode deploy --promote\` | Promote staging to production |
169
+ | \`kode types\` | Regenerate CMS types from Webflow |
170
+ | \`kode pkg list\` | List available packages |
171
+ | \`kode pkg add <slug>\` | Install package |
172
+ | \`kode pkg remove <slug>\` | Remove package |
173
+ | \`kode pkg publish\` | Publish package to registry |
174
+ | \`kode status\` | Show sync status |
175
+ | \`kode diff <script>\` | Compare local vs remote |
176
+ | \`kode doctor\` | Check config and CLI version |
177
+ | \`kode pages\` | Manage CDN pages |
178
+ | \`kode library\` | Legacy snippet library |
179
+
180
+ ## Supported File Types
181
+
182
+ | Extension | Bundled | React | Description |
183
+ |-----------|---------|-------|-------------|
184
+ | \`.js\` | No | No | Plain JavaScript (passthrough) |
185
+ | \`.css\` | No | No | Plain CSS (passthrough) |
186
+ | \`.ts\` | Yes | No | TypeScript |
187
+ | \`.tsx\` | Yes | Yes | TypeScript + React (auto-loads React CDN) |
188
+ | \`.jsx\` | Yes | Yes | JavaScript + React (auto-loads React CDN) |
189
+
190
+ ## MCP Tools
191
+
192
+ AI agents can use these tools directly:
193
+
194
+ - \`kode_list_scripts\` - List all scripts
195
+ - \`kode_push\` - Upload local files (bundles TS/React)
196
+ - \`kode_deploy\` / \`kode_promote\` - Deploy to staging/production
197
+ - \`kode_pkg\` - Package manager (add, remove, publish, list, info, update)
198
+ - \`kode_create_page\` - Create CDN page with URL patterns
199
+ - \`kode_assign_script_to_page\` - Assign script to page
200
+ - \`kode_library_list\` - Browse legacy snippet library
201
+
202
+ ## File Structure
203
+
204
+ \`\`\`
205
+ .cure-kode/
206
+ \u251C\u2500\u2500 config.json # Site configuration (contains API key - gitignored)
207
+ \u251C\u2500\u2500 scripts.json # Sync state for conflict detection
208
+ \u251C\u2500\u2500 context.md # Dynamic context for AI agents
209
+ \u2514\u2500\u2500 KODE.md # This file - auto-generated docs
210
+
211
+ .cure-kode-scripts/
212
+ \u251C\u2500\u2500 counter.tsx # Entry point (deployed, bundled with esbuild + React)
213
+ \u251C\u2500\u2500 form-handler.js # Entry point (deployed, plain JS passthrough)
214
+ \u251C\u2500\u2500 styles.css # Entry point (deployed, plain CSS)
215
+ \u251C\u2500\u2500 components/ # Modules (bundled into entry points, never deployed)
216
+ \u2502 \u2514\u2500\u2500 Button.tsx
217
+ \u251C\u2500\u2500 lib/ # Modules
218
+ \u2502 \u2514\u2500\u2500 utils.ts
219
+ \u251C\u2500\u2500 packages/@kode/ # Installed packages (import via @kode/<name>)
220
+ \u2502 \u251C\u2500\u2500 greet/
221
+ \u2502 \u2514\u2500\u2500 webflow/ # Auto-generated CMS helper
222
+ \u2514\u2500\u2500 types/
223
+ \u2514\u2500\u2500 cms.d.ts # Auto-generated Webflow CMS types
224
+
225
+ .claude/skills/ # Claude Code skills
226
+ \u251C\u2500\u2500 deploy/ # /deploy \u2014 push and deploy workflow
227
+ \u2514\u2500\u2500 webflow-patterns/ # Auto-loaded Webflow DOM reference
228
+ \`\`\`
229
+ `;
230
+ return md;
231
+ }
232
+ function ensureClaudeMdReference(projectRoot) {
233
+ const claudeMdPath = join(projectRoot, "CLAUDE.md");
234
+ if (!existsSync(claudeMdPath)) {
235
+ writeFileSync(claudeMdPath, KODE_REFERENCE);
236
+ return { created: true, updated: false, skipped: false };
237
+ }
238
+ const content = readFileSync(claudeMdPath, "utf-8");
239
+ if (hasKodeReference(content)) {
240
+ return { created: false, updated: false, skipped: true };
241
+ }
242
+ const newContent = KODE_REFERENCE + content;
243
+ writeFileSync(claudeMdPath, newContent);
244
+ return { created: false, updated: true, skipped: false };
245
+ }
246
+ function updateKodeMd(projectRoot, siteName, siteSlug, scripts, pages, options) {
247
+ const kodeMdPath = join(projectRoot, ".cure-kode", "KODE.md");
248
+ const existed = existsSync(kodeMdPath);
249
+ const content = generateKodeMd(siteName, siteSlug, scripts, pages, options);
250
+ writeFileSync(kodeMdPath, content);
251
+ return { created: !existed, updated: existed };
252
+ }
253
+ function updateKodeDocs(projectRoot, siteName, siteSlug, scripts, pages, options) {
254
+ const kodeMd = updateKodeMd(projectRoot, siteName, siteSlug, scripts, pages, options);
255
+ const claudeMd = ensureClaudeMdReference(projectRoot);
256
+ return { kodeMd, claudeMd };
257
+ }
258
+ function scriptsToDocsFormat(scripts, pages) {
259
+ return scripts.map((s) => {
260
+ const pageAssignments = (s.pages || []).filter((p) => p.is_enabled).map((p) => {
261
+ const page = pages?.find((pg) => pg.id === p.page_id);
262
+ return page ? { pageId: page.id, pageSlug: page.slug, pageName: page.name } : null;
263
+ }).filter((p) => p !== null);
264
+ return {
265
+ slug: s.slug,
266
+ name: s.name,
267
+ type: s.type,
268
+ scope: s.scope,
269
+ autoLoad: s.auto_load,
270
+ purpose: s.metadata?.purpose || s.description,
271
+ pageAssignments: pageAssignments.length > 0 ? pageAssignments : void 0
272
+ };
273
+ });
274
+ }
275
+ function pagesToInfoFormat(pages) {
276
+ return pages.filter((p) => p.is_active).map((p) => ({
277
+ name: p.name,
278
+ slug: p.slug,
279
+ patterns: p.url_patterns
280
+ }));
281
+ }
282
+ var updateClaudeMd = updateKodeDocs;
283
+
284
+ // src/commands/pull.ts
285
+ function hashContent(content) {
286
+ return createHash("sha256").update(content).digest("hex").substring(0, 16);
287
+ }
288
+ async function pullCommand(options) {
289
+ const projectRoot = findProjectRoot();
290
+ if (!projectRoot) {
291
+ console.log(chalk.red("Feil: Ikke i et Cure Kode-prosjekt."));
292
+ console.log(chalk.dim('Kj\xF8r "kode init" f\xF8rst.'));
293
+ return;
294
+ }
295
+ const config = getProjectConfig(projectRoot);
296
+ if (!config) {
297
+ console.log(chalk.red("Feil: Kunne ikke lese prosjektkonfigurasjon."));
298
+ return;
299
+ }
300
+ const spinner = ora("Henter skript...").start();
301
+ try {
302
+ const client = createApiClient(config);
303
+ const scripts = await client.listScripts(config.siteId);
304
+ if (scripts.length === 0) {
305
+ spinner.info("Ingen skript funnet p\xE5 server.");
306
+ console.log(chalk.dim('\nOpprett skript i Cure App eller bruk "kode push" for \xE5 laste opp.'));
307
+ return;
308
+ }
309
+ spinner.succeed(`Fant ${scripts.length} skript`);
310
+ const scriptsDir = getScriptsDir(projectRoot, config);
311
+ if (!existsSync2(scriptsDir)) {
312
+ mkdirSync(scriptsDir, { recursive: true });
313
+ }
314
+ const metadataPath = join2(projectRoot, ".cure-kode", "scripts.json");
315
+ let existingMetadata = [];
316
+ if (existsSync2(metadataPath)) {
317
+ try {
318
+ existingMetadata = JSON.parse(readFileSync2(metadataPath, "utf-8"));
319
+ } catch {
320
+ }
321
+ }
322
+ const scriptsToPull = options.script ? scripts.filter((s) => s.slug === options.script || s.name === options.script) : scripts;
323
+ if (options.script && scriptsToPull.length === 0) {
324
+ console.log(chalk.yellow(`
325
+ Fant ikke skript "${options.script}".`));
326
+ console.log(chalk.dim("Tilgjengelige skript:"));
327
+ scripts.forEach((s) => {
328
+ console.log(chalk.dim(` - ${s.slug} (${s.name})`));
329
+ });
330
+ return;
331
+ }
332
+ console.log();
333
+ let pulled = 0;
334
+ let skipped = 0;
335
+ let updated = 0;
336
+ for (const script of scriptsToPull) {
337
+ const hasSource = script.source_type && script.source_content;
338
+ const ext = hasSource ? script.source_type : script.type === "javascript" ? "js" : "css";
339
+ const fileContent = hasSource ? script.source_content : script.content;
340
+ const fileName = `${script.slug}.${ext}`;
341
+ const filePath = join2(scriptsDir, fileName);
342
+ const existingMeta = existingMetadata.find((m) => m.slug === script.slug);
343
+ const lastPulledVersion = existingMeta?.lastPulledVersion || 0;
344
+ if (hasSource) {
345
+ const oldExt = script.type === "javascript" ? "js" : "css";
346
+ if (oldExt !== ext) {
347
+ const oldPath = join2(scriptsDir, `${script.slug}.${oldExt}`);
348
+ if (existsSync2(oldPath)) {
349
+ const { unlinkSync } = await import("fs");
350
+ unlinkSync(oldPath);
351
+ }
352
+ }
353
+ }
354
+ if (existsSync2(filePath) && !options.force) {
355
+ const localContent = readFileSync2(filePath, "utf-8");
356
+ const localHash = hashContent(localContent);
357
+ const hasLocalChanges = existingMeta && localHash !== existingMeta.contentHash;
358
+ if (hasLocalChanges) {
359
+ if (script.current_version > lastPulledVersion) {
360
+ console.log(
361
+ chalk.yellow(` \u26A0 ${fileName}`) + chalk.dim(` (lokal v${lastPulledVersion} \u2192 server v${script.current_version}, konflikt)`)
362
+ );
363
+ console.log(chalk.dim(` Bruk --force for \xE5 overskrive lokale endringer`));
364
+ } else {
365
+ console.log(
366
+ chalk.yellow(` \u26A0 ${fileName}`) + chalk.dim(" (lokale endringer, bruk --force)")
367
+ );
368
+ }
369
+ skipped++;
370
+ continue;
371
+ }
372
+ if (fileContent === localContent) {
373
+ console.log(chalk.dim(` \u25CB ${fileName} (ingen endringer)`));
374
+ skipped++;
375
+ continue;
376
+ }
377
+ }
378
+ writeFileSync2(filePath, fileContent);
379
+ const scopeTag = script.scope === "global" ? chalk.blue("[G]") : chalk.magenta("[P]");
380
+ const loadTag = script.auto_load ? chalk.green("\u26A1") : chalk.dim("\u25CB");
381
+ if (lastPulledVersion > 0 && script.current_version > lastPulledVersion) {
382
+ console.log(
383
+ chalk.green(` \u2713 ${fileName}`) + chalk.dim(` v${lastPulledVersion} \u2192 v${script.current_version}`) + ` ${scopeTag} ${loadTag}`
384
+ );
385
+ updated++;
386
+ } else {
387
+ console.log(
388
+ chalk.green(` \u2713 ${fileName}`) + chalk.dim(` v${script.current_version}`) + ` ${scopeTag} ${loadTag}`
389
+ );
390
+ pulled++;
391
+ }
392
+ }
393
+ console.log();
394
+ if (pulled > 0) {
395
+ console.log(chalk.green(`Hentet ${pulled} skript til ${config.scriptsDir}/`));
396
+ }
397
+ if (updated > 0) {
398
+ console.log(chalk.cyan(`Oppdatert ${updated} skript med nye versjoner`));
399
+ }
400
+ if (skipped > 0) {
401
+ console.log(chalk.dim(`Hoppet over ${skipped} skript`));
402
+ }
403
+ let pages = [];
404
+ try {
405
+ pages = await client.listPages(config.siteId);
406
+ } catch {
407
+ }
408
+ const now = (/* @__PURE__ */ new Date()).toISOString();
409
+ const metadata = scripts.map((s) => {
410
+ const hasSource = s.source_type && s.source_content;
411
+ const localExt = hasSource ? s.source_type : s.type === "javascript" ? "js" : "css";
412
+ const localFileName = `${s.slug}.${localExt}`;
413
+ const filePath = join2(scriptsDir, localFileName);
414
+ const localContent = existsSync2(filePath) ? readFileSync2(filePath, "utf-8") : hasSource ? s.source_content : s.content;
415
+ const pageAssignments = (s.pages || []).filter((p) => p.is_enabled).map((p) => {
416
+ const page = pages.find((pg) => pg.id === p.page_id);
417
+ return page ? { pageId: page.id, pageSlug: page.slug, pageName: page.name } : null;
418
+ }).filter((p) => p !== null);
419
+ return {
420
+ id: s.id,
421
+ slug: s.slug,
422
+ name: s.name,
423
+ type: s.type,
424
+ scope: s.scope,
425
+ autoLoad: s.auto_load,
426
+ version: s.current_version,
427
+ loadOrder: s.load_order,
428
+ pageAssignments: pageAssignments.length > 0 ? pageAssignments : void 0,
429
+ lastPulledVersion: s.current_version,
430
+ lastPulledAt: now,
431
+ contentHash: hashContent(localContent),
432
+ localFileName
433
+ };
434
+ });
435
+ writeFileSync2(metadataPath, JSON.stringify(metadata, null, 2));
436
+ try {
437
+ const result = updateClaudeMd(
438
+ projectRoot,
439
+ config.siteName,
440
+ config.siteSlug,
441
+ scriptsToDocsFormat(scripts, pages),
442
+ pagesToInfoFormat(pages)
443
+ );
444
+ if (result.kodeMd.created) {
445
+ console.log(chalk.green("Opprettet .cure-kode/KODE.md"));
446
+ } else if (result.kodeMd.updated) {
447
+ console.log(chalk.dim("Oppdatert .cure-kode/KODE.md"));
448
+ }
449
+ if (result.claudeMd.created) {
450
+ console.log(chalk.green("Opprettet CLAUDE.md med referanse"));
451
+ } else if (result.claudeMd.updated) {
452
+ console.log(chalk.dim("La til Kode-referanse i CLAUDE.md"));
453
+ }
454
+ } catch {
455
+ }
456
+ console.log(chalk.dim("\n[G]=Global [P]=Sidespesifikk \u26A1=Auto-last \u25CB=Manuell"));
457
+ try {
458
+ const projectFiles = await client.getProjectFiles(config.siteId);
459
+ if (projectFiles && projectFiles.length > 0) {
460
+ let newFiles = 0;
461
+ let unchangedFiles = 0;
462
+ for (const file of projectFiles) {
463
+ const filePath = join2(scriptsDir, file.path);
464
+ mkdirSync(dirname(filePath), { recursive: true });
465
+ if (existsSync2(filePath)) {
466
+ const localHash = createHash("sha256").update(readFileSync2(filePath, "utf-8")).digest("hex");
467
+ if (localHash === file.contentHash) {
468
+ unchangedFiles++;
469
+ continue;
470
+ }
471
+ }
472
+ writeFileSync2(filePath, file.content, "utf-8");
473
+ console.log(chalk.green(` + ${file.path}`));
474
+ newFiles++;
475
+ }
476
+ if (newFiles > 0 || unchangedFiles > 0) {
477
+ console.log(chalk.dim(`
478
+ Hentet ${newFiles} prosjektfil(er)${unchangedFiles > 0 ? `, ${unchangedFiles} uendret` : ""}`));
479
+ }
480
+ }
481
+ } catch {
482
+ }
483
+ try {
484
+ const { generateCmsTypes } = await import("./types-PZ7GFJEK.js");
485
+ await generateCmsTypes({ silent: false });
486
+ } catch {
487
+ }
488
+ } catch (error) {
489
+ spinner.fail("Kunne ikke hente skript");
490
+ console.error(chalk.red("\nFeil:"), error);
491
+ }
492
+ }
493
+
494
+ export {
495
+ updateKodeDocs,
496
+ scriptsToDocsFormat,
497
+ pagesToInfoFormat,
498
+ updateClaudeMd,
499
+ pullCommand
500
+ };