@curenorway/kode-cli 1.8.0 → 1.9.2

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.
@@ -426,6 +426,35 @@ Scripts have two properties: **scope** and **autoLoad**.
426
426
 
427
427
  **CLI flags**: \`kode push --auto-load\` or \`kode push --no-auto-load\`
428
428
 
429
+ ### Script Metadata (Documentation)
430
+
431
+ **ALWAYS provide metadata when creating or updating scripts.** This helps other AI agents and the Chrome extension understand what scripts do.
432
+
433
+ | Field | Description | Example |
434
+ |-------|-------------|---------|
435
+ | \`purpose\` | What the script does (1-2 sentences) | "Adds smooth scroll behavior to anchor links" |
436
+ | \`triggers\` | When the script runs | \`[{ event: 'domReady' }, { event: 'click', selector: '.nav-link' }]\` |
437
+ | \`dependencies\` | External libraries needed | \`[{ name: 'gsap', version: '3.12' }]\` |
438
+ | \`domTargets\` | CSS selectors the script affects | \`[{ selector: '.hero-title' }, { selector: '#contact-form' }]\` |
439
+
440
+ **MCP Tools**:
441
+ - \`kode_create_script\` / \`kode_update_script\` \u2192 include \`metadata: { purpose: "..." }\`
442
+ - \`kode_analyze_script\` \u2192 auto-detect triggers, dependencies, and DOM targets from code
443
+ - \`kode_get_script_metadata\` \u2192 get existing metadata and AI summary
444
+
445
+ **Example**:
446
+ \`\`\`json
447
+ {
448
+ "name": "smooth-scroll",
449
+ "content": "...",
450
+ "metadata": {
451
+ "purpose": "Adds smooth scroll animation to all anchor links",
452
+ "triggers": [{ "event": "click", "selector": "a[href^='#']" }],
453
+ "domTargets": [{ "selector": "a[href^='#']" }]
454
+ }
455
+ }
456
+ \`\`\`
457
+
429
458
  ### Context
430
459
 
431
460
  **Read \`.cure-kode/context.md\` before working on scripts** - it contains:
@@ -597,13 +626,119 @@ if (CK.isLoaded('my-modal-script')) {
597
626
  }
598
627
  \`\`\`
599
628
 
629
+ ## Script Metadata (Documentation)
630
+
631
+ **IMPORTANT: Always provide metadata when creating or updating scripts!**
632
+
633
+ Metadata helps:
634
+ - Other AI agents understand what scripts do
635
+ - The Chrome extension display script info
636
+ - Future debugging and maintenance
637
+
638
+ ### Metadata Fields
639
+
640
+ | Field | Type | Description |
641
+ |-------|------|-------------|
642
+ | \`purpose\` | string | 1-2 sentence description of what the script does |
643
+ | \`triggers\` | array | Events that activate the script |
644
+ | \`dependencies\` | array | External libraries the script needs |
645
+ | \`domTargets\` | array | CSS selectors the script manipulates |
646
+
647
+ ### Trigger Events
648
+
649
+ \`\`\`typescript
650
+ type TriggerEvent =
651
+ | 'domReady' // DOMContentLoaded
652
+ | 'load' // window.onload
653
+ | 'scroll' // scroll events
654
+ | 'click' // click events
655
+ | 'hover' // mouseenter/mouseleave
656
+ | 'resize' // window resize
657
+ | 'mutation' // MutationObserver
658
+ | 'interval' // setInterval
659
+ | 'timeout' // setTimeout
660
+ | 'formSubmit' // form submission
661
+ | 'visibility' // IntersectionObserver
662
+ | 'immediate' // runs immediately
663
+ | 'custom' // custom event
664
+
665
+ // Each trigger can have optional selector/target
666
+ { event: 'click', selector: '.btn-primary' }
667
+ { event: 'scroll', target: 'window' }
668
+ \`\`\`
669
+
670
+ ### Providing Metadata via MCP
671
+
672
+ When creating scripts:
673
+ \`\`\`javascript
674
+ kode_create_script({
675
+ name: 'accordion',
676
+ slug: 'accordion',
677
+ type: 'javascript',
678
+ content: '// accordion code...',
679
+ metadata: {
680
+ purpose: 'Expands and collapses FAQ accordion items on click',
681
+ triggers: [
682
+ { event: 'domReady' },
683
+ { event: 'click', selector: '.accordion-trigger' }
684
+ ],
685
+ domTargets: [
686
+ { selector: '.accordion-trigger' },
687
+ { selector: '.accordion-content' }
688
+ ]
689
+ }
690
+ })
691
+ \`\`\`
692
+
693
+ When updating scripts:
694
+ \`\`\`javascript
695
+ kode_update_script({
696
+ scriptId: 'xxx',
697
+ content: '// updated code...',
698
+ metadata: {
699
+ purpose: 'Updated description...',
700
+ // ... other fields
701
+ }
702
+ })
703
+ \`\`\`
704
+
705
+ ### Auto-Analyze Scripts
706
+
707
+ Use \`kode_analyze_script\` to automatically detect metadata from code:
708
+ \`\`\`javascript
709
+ kode_analyze_script({ scriptId: 'xxx' })
710
+ // Returns detected: triggers, dependencies, domTargets
711
+ \`\`\`
712
+
713
+ The analyzer detects:
714
+ - **DOM selectors**: querySelector, getElementById, getElementsByClassName
715
+ - **Event listeners**: addEventListener, on events
716
+ - **Dependencies**: Library globals (gsap, Swiper, jQuery, etc.)
717
+ - **Timing patterns**: DOMContentLoaded, load, intervals
718
+
719
+ ### Get Script Metadata
720
+
721
+ \`\`\`javascript
722
+ kode_get_script_metadata({ scriptId: 'xxx' })
723
+ // Returns: purpose, triggers, dependencies, domTargets, aiSummary
724
+ \`\`\`
725
+
726
+ ### Best Practice Workflow
727
+
728
+ 1. **Create script** with at least \`purpose\` in metadata
729
+ 2. **Run analyze** to auto-detect triggers and targets
730
+ 3. **Update metadata** with any corrections or additions
731
+ 4. **Deploy** - metadata is included in init.js for Chrome extension
732
+
600
733
  ## Best Practices
601
734
 
602
735
  1. **Always deploy to staging first** - Test before production
603
- 2. **Use page-specific scripts** for page-only functionality
604
- 3. **Use \`autoLoad: false\`** for lazy-loaded features (modals, etc.)
605
- 4. **Document your changes** - Update context.md
606
- 5. **Cache page HTML** - Use \`kode html <url> --save\` to understand structure
736
+ 2. **Always provide metadata** - At minimum, include \`purpose\` when creating scripts
737
+ 3. **Use page-specific scripts** for page-only functionality
738
+ 4. **Use \`autoLoad: false\`** for lazy-loaded features (modals, etc.)
739
+ 5. **Document your changes** - Update context.md
740
+ 6. **Cache page HTML** - Use \`kode html <url> --save\` to understand structure
741
+ 7. **Run analyze after creating** - Use \`kode_analyze_script\` to auto-detect metadata
607
742
 
608
743
  ## MCP Tools
609
744
 
@@ -611,10 +746,12 @@ If using the Kode MCP server, these tools are available:
611
746
 
612
747
  ### Script Management
613
748
  - \`kode_list_scripts\` - List all scripts with scope and autoLoad status
614
- - \`kode_get_script\` - Get script content
615
- - \`kode_create_script\` - Create new script (accepts \`scope\` and \`autoLoad\`)
616
- - \`kode_update_script\` - Update script content, scope, or autoLoad
749
+ - \`kode_get_script\` - Get script content (use \`includeContent: false\` to save tokens)
750
+ - \`kode_create_script\` - Create new script (accepts \`scope\`, \`autoLoad\`, \`metadata\`)
751
+ - \`kode_update_script\` - Update script content, scope, autoLoad, or metadata
617
752
  - \`kode_delete_script\` - Delete a script
753
+ - \`kode_analyze_script\` - Auto-detect metadata (triggers, dependencies, DOM targets) from code
754
+ - \`kode_get_script_metadata\` - Get script metadata and AI summary
618
755
 
619
756
  ### Page Assignment (for page-specific scripts)
620
757
  - \`kode_list_pages\` - List page definitions with URL patterns
@@ -1200,11 +1337,16 @@ async function initCommand(options) {
1200
1337
  console.log(chalk.bold("\n\u{1F680} Cure Kode Setup\n"));
1201
1338
  const { apiKey } = await prompt([
1202
1339
  {
1203
- type: "password",
1340
+ type: "input",
1204
1341
  name: "apiKey",
1205
1342
  message: "API Key (from Cure App \u2192 Tools \u2192 Kode \u2192 API Keys):",
1206
1343
  initial: options.apiKey,
1207
- validate: (value) => value.length > 0 ? true : "API key is required"
1344
+ validate: (value) => {
1345
+ if (value.length === 0) return "API key is required";
1346
+ if (!value.startsWith("ck_")) return "API key should start with ck_";
1347
+ if (value.length < 30) return "API key looks truncated - make sure you copied the full key";
1348
+ return true;
1349
+ }
1208
1350
  }
1209
1351
  ]);
1210
1352
  const spinner = ora("Validating API key...").start();
@@ -1268,7 +1410,7 @@ config.json
1268
1410
  mcpConfig.mcpServers["cure-kode"] = {
1269
1411
  type: "stdio",
1270
1412
  command: "npx",
1271
- args: ["-y", "@curenorway/kode-mcp"]
1413
+ args: ["-y", "@curenorway/kode-mcp@^1.3.0"]
1272
1414
  };
1273
1415
  let webflowToken = site.webflow_token;
1274
1416
  let webflowMcpMethod = null;
@@ -1319,7 +1461,7 @@ config.json
1319
1461
  mcpConfig.mcpServers["webflow"] = {
1320
1462
  type: "stdio",
1321
1463
  command: "npx",
1322
- args: ["-y", "webflow-mcp-server@latest"],
1464
+ args: ["-y", "webflow-mcp-server@^0.6.0"],
1323
1465
  env: {
1324
1466
  WEBFLOW_TOKEN: webflowToken
1325
1467
  }
@@ -1333,7 +1475,7 @@ config.json
1333
1475
  mcpConfig.mcpServers["playwright"] = {
1334
1476
  type: "stdio",
1335
1477
  command: "npx",
1336
- args: ["-y", "@playwright/mcp@latest"]
1478
+ args: ["-y", "@playwright/mcp@^0.0.21"]
1337
1479
  };
1338
1480
  spinner.start("Generating AI context files...");
1339
1481
  writeFileSync3(mcpConfigPath, JSON.stringify(mcpConfig, null, 2) + "\n");
@@ -1504,6 +1646,80 @@ config.json
1504
1646
  }
1505
1647
  }
1506
1648
 
1649
+ // src/lib/retry.ts
1650
+ function isRetryableError(error) {
1651
+ if (error instanceof TypeError && error.message.includes("fetch")) {
1652
+ return true;
1653
+ }
1654
+ const err = error;
1655
+ const statusCode = err.statusCode || err.status;
1656
+ if (statusCode && statusCode >= 400 && statusCode < 500) {
1657
+ return false;
1658
+ }
1659
+ if (statusCode && statusCode >= 500) {
1660
+ return true;
1661
+ }
1662
+ if (err.code === "ECONNRESET" || err.code === "ETIMEDOUT" || err.code === "ENOTFOUND") {
1663
+ return true;
1664
+ }
1665
+ if (error instanceof Error) {
1666
+ const message = error.message.toLowerCase();
1667
+ if (message.includes("network") || message.includes("timeout") || message.includes("connection") || message.includes("socket")) {
1668
+ return true;
1669
+ }
1670
+ }
1671
+ return false;
1672
+ }
1673
+ function calculateDelay(attempt, baseDelayMs, maxDelayMs, backoffMultiplier, jitter) {
1674
+ const exponentialDelay = baseDelayMs * Math.pow(backoffMultiplier, attempt - 1);
1675
+ const cappedDelay = Math.min(exponentialDelay, maxDelayMs);
1676
+ if (!jitter) {
1677
+ return cappedDelay;
1678
+ }
1679
+ const jitterRange = cappedDelay * 0.25;
1680
+ const jitterOffset = (Math.random() - 0.5) * 2 * jitterRange;
1681
+ return Math.max(0, Math.round(cappedDelay + jitterOffset));
1682
+ }
1683
+ function sleep(ms) {
1684
+ return new Promise((resolve) => setTimeout(resolve, ms));
1685
+ }
1686
+ async function withRetry(fn, options = {}) {
1687
+ const {
1688
+ maxAttempts = 3,
1689
+ baseDelayMs = 500,
1690
+ maxDelayMs = 5e3,
1691
+ backoffMultiplier = 2,
1692
+ jitter = true,
1693
+ isRetryable = isRetryableError,
1694
+ onRetry
1695
+ } = options;
1696
+ let lastError;
1697
+ let totalDelayMs = 0;
1698
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
1699
+ try {
1700
+ return await fn();
1701
+ } catch (error) {
1702
+ lastError = error instanceof Error ? error : new Error(String(error));
1703
+ if (attempt >= maxAttempts || !isRetryable(error)) {
1704
+ throw lastError;
1705
+ }
1706
+ const delayMs = calculateDelay(
1707
+ attempt,
1708
+ baseDelayMs,
1709
+ maxDelayMs,
1710
+ backoffMultiplier,
1711
+ jitter
1712
+ );
1713
+ totalDelayMs += delayMs;
1714
+ if (onRetry) {
1715
+ onRetry(attempt, error, delayMs);
1716
+ }
1717
+ await sleep(delayMs);
1718
+ }
1719
+ }
1720
+ throw lastError || new Error("Retry failed");
1721
+ }
1722
+
1507
1723
  // src/api.ts
1508
1724
  var KodeApiError = class extends Error {
1509
1725
  constructor(message, statusCode, response) {
@@ -1526,23 +1742,38 @@ var KodeApiClient = class {
1526
1742
  }
1527
1743
  async request(endpoint, options = {}) {
1528
1744
  const url = `${this.baseUrl}${endpoint}`;
1529
- const response = await fetch(url, {
1530
- ...options,
1531
- headers: {
1532
- "Content-Type": "application/json",
1533
- "X-API-Key": this.apiKey,
1534
- ...options.headers
1745
+ return withRetry(
1746
+ async () => {
1747
+ const response = await fetch(url, {
1748
+ ...options,
1749
+ headers: {
1750
+ "Content-Type": "application/json",
1751
+ "X-API-Key": this.apiKey,
1752
+ ...options.headers
1753
+ }
1754
+ });
1755
+ const data = await response.json();
1756
+ if (!response.ok) {
1757
+ throw new KodeApiError(
1758
+ data.error || `Request failed with status ${response.status}`,
1759
+ response.status,
1760
+ data
1761
+ );
1762
+ }
1763
+ return data;
1764
+ },
1765
+ {
1766
+ maxAttempts: 3,
1767
+ baseDelayMs: 500,
1768
+ // Custom retry check: retry on network errors and 5xx, but not 4xx
1769
+ isRetryable: (error) => {
1770
+ if (error instanceof KodeApiError) {
1771
+ return error.statusCode >= 500;
1772
+ }
1773
+ return isRetryableError(error);
1774
+ }
1535
1775
  }
1536
- });
1537
- const data = await response.json();
1538
- if (!response.ok) {
1539
- throw new KodeApiError(
1540
- data.error || `Request failed with status ${response.status}`,
1541
- response.status,
1542
- data
1543
- );
1544
- }
1545
- return data;
1776
+ );
1546
1777
  }
1547
1778
  // Sites
1548
1779
  async getSite(siteId) {
@@ -1613,6 +1844,25 @@ var KodeApiClient = class {
1613
1844
  async getDeploymentStatus(siteId) {
1614
1845
  return this.request(`/api/cdn/sites/${siteId}/deployments/status`);
1615
1846
  }
1847
+ async rollback(siteId, environment = "staging") {
1848
+ return this.request("/api/cdn/deploy/rollback", {
1849
+ method: "POST",
1850
+ body: JSON.stringify({
1851
+ siteId,
1852
+ environment
1853
+ })
1854
+ });
1855
+ }
1856
+ // Production enabled toggle (v2.3)
1857
+ async setProductionEnabled(siteId, enabled, productionDomain) {
1858
+ return this.request(`/api/cdn/sites/${siteId}/production`, {
1859
+ method: "POST",
1860
+ body: JSON.stringify({
1861
+ enabled,
1862
+ productionDomain
1863
+ })
1864
+ });
1865
+ }
1616
1866
  // HTML Fetch
1617
1867
  async fetchHtml(url) {
1618
1868
  return this.request("/api/cdn/fetch-html", {
@@ -1620,6 +1870,16 @@ var KodeApiClient = class {
1620
1870
  body: JSON.stringify({ url })
1621
1871
  });
1622
1872
  }
1873
+ // Lock management
1874
+ async getLockStatus(siteId) {
1875
+ return this.request(`/api/cdn/deploy/lock?siteId=${siteId}`);
1876
+ }
1877
+ async forceReleaseLock(siteId) {
1878
+ return this.request("/api/cdn/deploy/lock", {
1879
+ method: "DELETE",
1880
+ body: JSON.stringify({ siteId })
1881
+ });
1882
+ }
1623
1883
  };
1624
1884
  function createApiClient(config) {
1625
1885
  return new KodeApiClient(config);
@@ -1776,11 +2036,16 @@ async function pushCommand(options) {
1776
2036
  let skipped = 0;
1777
2037
  spinner.stop();
1778
2038
  console.log();
2039
+ let emptyScriptCount = 0;
1779
2040
  for (const file of filesToPush) {
1780
2041
  const filePath = join5(scriptsDir, file);
1781
2042
  const content = readFileSync4(filePath, "utf-8");
1782
2043
  const slug = basename(file, extname(file));
1783
2044
  const type = extname(file) === ".js" ? "javascript" : "css";
2045
+ if (content.trim().length === 0) {
2046
+ console.log(chalk3.yellow(` \u26A0 ${file}`) + chalk3.dim(" (empty file)"));
2047
+ emptyScriptCount++;
2048
+ }
1784
2049
  const remoteScript = remoteScripts.find((s) => s.slug === slug);
1785
2050
  const localMeta = metadata.find((m) => m.slug === slug);
1786
2051
  if (remoteScript) {
@@ -1827,6 +2092,11 @@ async function pushCommand(options) {
1827
2092
  if (skipped > 0) {
1828
2093
  console.log(chalk3.dim(` Skipped ${skipped} unchanged script(s)`));
1829
2094
  }
2095
+ if (emptyScriptCount > 0) {
2096
+ console.log(chalk3.yellow(`
2097
+ \u26A0\uFE0F ${emptyScriptCount} empty script(s) pushed`));
2098
+ console.log(chalk3.dim(" Empty scripts will have no effect when deployed."));
2099
+ }
1830
2100
  const updatedScripts = await client.listScripts(config.siteId);
1831
2101
  const updatedMetadata = updatedScripts.map((s) => ({
1832
2102
  id: s.id,
@@ -1895,15 +2165,59 @@ async function watchCommand(options) {
1895
2165
  }
1896
2166
  const pendingChanges = /* @__PURE__ */ new Map();
1897
2167
  const DEBOUNCE_MS = 500;
1898
- const handleChange = async (filePath) => {
2168
+ const failedSyncs = /* @__PURE__ */ new Map();
2169
+ let successCount = 0;
2170
+ let errorCount = 0;
2171
+ const RETRY_DELAY_MS = 3e4;
2172
+ const MAX_RETRY_ATTEMPTS = 3;
2173
+ const printStatus = () => {
2174
+ if (failedSyncs.size === 0 && successCount === 0) return;
2175
+ const statusParts = [];
2176
+ if (successCount > 0) {
2177
+ statusParts.push(chalk4.green(`${successCount} synced`));
2178
+ }
2179
+ if (failedSyncs.size > 0) {
2180
+ statusParts.push(chalk4.red(`${failedSyncs.size} pending errors`));
2181
+ }
2182
+ console.log(chalk4.dim(`
2183
+ \u2500\u2500\u2500 Status: ${statusParts.join(", ")} \u2500\u2500\u2500
2184
+ `));
2185
+ };
2186
+ const retryFailedSyncs = async () => {
2187
+ for (const [filePath, failed] of failedSyncs.entries()) {
2188
+ if (!existsSync6(filePath)) {
2189
+ failedSyncs.delete(filePath);
2190
+ continue;
2191
+ }
2192
+ if (failed.attempts >= MAX_RETRY_ATTEMPTS) {
2193
+ continue;
2194
+ }
2195
+ const timeSinceLastAttempt = Date.now() - failed.lastAttempt.getTime();
2196
+ if (timeSinceLastAttempt < RETRY_DELAY_MS) {
2197
+ continue;
2198
+ }
2199
+ const timestamp = (/* @__PURE__ */ new Date()).toLocaleTimeString("nb-NO");
2200
+ console.log(
2201
+ chalk4.dim(`[${timestamp}] `) + chalk4.yellow(`\u21BB Retrying ${failed.fileName}...`) + chalk4.dim(` (attempt ${failed.attempts + 1}/${MAX_RETRY_ATTEMPTS})`)
2202
+ );
2203
+ await handleChange(
2204
+ filePath,
2205
+ true
2206
+ /* isRetry */
2207
+ );
2208
+ }
2209
+ };
2210
+ const retryInterval = setInterval(retryFailedSyncs, RETRY_DELAY_MS);
2211
+ const handleChange = async (filePath, isRetry = false) => {
1899
2212
  const fileName = basename2(filePath);
1900
2213
  const slug = basename2(fileName, extname2(fileName));
1901
2214
  const type = extname2(fileName) === ".js" ? "javascript" : "css";
1902
- if (pendingChanges.has(filePath)) {
1903
- clearTimeout(pendingChanges.get(filePath));
2215
+ if (!isRetry) {
2216
+ if (pendingChanges.has(filePath)) {
2217
+ clearTimeout(pendingChanges.get(filePath));
2218
+ }
1904
2219
  }
1905
- const timeout = setTimeout(async () => {
1906
- pendingChanges.delete(filePath);
2220
+ const syncFile = async () => {
1907
2221
  try {
1908
2222
  const content = readFileSync5(filePath, "utf-8");
1909
2223
  const remoteScript = remoteScripts.find((s) => s.slug === slug);
@@ -1911,6 +2225,9 @@ async function watchCommand(options) {
1911
2225
  const timestamp = (/* @__PURE__ */ new Date()).toLocaleTimeString("nb-NO");
1912
2226
  if (remoteScript) {
1913
2227
  if (remoteScript.content === content) {
2228
+ if (failedSyncs.has(filePath)) {
2229
+ failedSyncs.delete(filePath);
2230
+ }
1914
2231
  return;
1915
2232
  }
1916
2233
  await client.updateScript(remoteScript.id, {
@@ -1919,9 +2236,24 @@ async function watchCommand(options) {
1919
2236
  });
1920
2237
  remoteScript.content = content;
1921
2238
  remoteScript.current_version++;
1922
- console.log(
1923
- chalk4.dim(`[${timestamp}] `) + chalk4.green(`\u2713 ${fileName}`) + chalk4.dim(` \u2192 v${remoteScript.current_version}`)
1924
- );
2239
+ if (failedSyncs.has(filePath)) {
2240
+ const wasRetry = failedSyncs.get(filePath).attempts > 0;
2241
+ failedSyncs.delete(filePath);
2242
+ if (wasRetry) {
2243
+ console.log(
2244
+ chalk4.dim(`[${timestamp}] `) + chalk4.green(`\u2713 ${fileName}`) + chalk4.dim(` \u2192 v${remoteScript.current_version}`) + chalk4.cyan(" (recovered)")
2245
+ );
2246
+ } else {
2247
+ console.log(
2248
+ chalk4.dim(`[${timestamp}] `) + chalk4.green(`\u2713 ${fileName}`) + chalk4.dim(` \u2192 v${remoteScript.current_version}`)
2249
+ );
2250
+ }
2251
+ } else {
2252
+ console.log(
2253
+ chalk4.dim(`[${timestamp}] `) + chalk4.green(`\u2713 ${fileName}`) + chalk4.dim(` \u2192 v${remoteScript.current_version}`)
2254
+ );
2255
+ }
2256
+ successCount++;
1925
2257
  if (options.deploy) {
1926
2258
  try {
1927
2259
  await client.deploy(config.siteId, config.environment || "staging");
@@ -1930,7 +2262,7 @@ async function watchCommand(options) {
1930
2262
  );
1931
2263
  } catch (deployError) {
1932
2264
  console.log(
1933
- chalk4.dim(`[${timestamp}] `) + chalk4.red(` \u21B3 Deploy failed`)
2265
+ chalk4.dim(`[${timestamp}] `) + chalk4.red(` \u21B3 Deploy failed: ${deployError.message || "Unknown error"}`)
1934
2266
  );
1935
2267
  }
1936
2268
  }
@@ -1943,9 +2275,17 @@ async function watchCommand(options) {
1943
2275
  content
1944
2276
  });
1945
2277
  remoteScripts.push(newScript);
1946
- console.log(
1947
- chalk4.dim(`[${timestamp}] `) + chalk4.green(`\u2713 ${fileName}`) + chalk4.cyan(" (created)")
1948
- );
2278
+ if (failedSyncs.has(filePath)) {
2279
+ failedSyncs.delete(filePath);
2280
+ console.log(
2281
+ chalk4.dim(`[${timestamp}] `) + chalk4.green(`\u2713 ${fileName}`) + chalk4.cyan(" (created, recovered)")
2282
+ );
2283
+ } else {
2284
+ console.log(
2285
+ chalk4.dim(`[${timestamp}] `) + chalk4.green(`\u2713 ${fileName}`) + chalk4.cyan(" (created)")
2286
+ );
2287
+ }
2288
+ successCount++;
1949
2289
  const updatedMetadata = remoteScripts.map((s) => ({
1950
2290
  id: s.id,
1951
2291
  slug: s.slug,
@@ -1960,10 +2300,36 @@ async function watchCommand(options) {
1960
2300
  }
1961
2301
  } catch (error) {
1962
2302
  const timestamp = (/* @__PURE__ */ new Date()).toLocaleTimeString("nb-NO");
1963
- console.log(
1964
- chalk4.dim(`[${timestamp}] `) + chalk4.red(`\u2717 ${fileName}`) + chalk4.dim(` - ${error.message || "Unknown error"}`)
1965
- );
2303
+ const errorMessage = error.message || "Unknown error";
2304
+ const existing = failedSyncs.get(filePath);
2305
+ const attempts = existing ? existing.attempts + 1 : 1;
2306
+ failedSyncs.set(filePath, {
2307
+ filePath,
2308
+ fileName,
2309
+ error: errorMessage,
2310
+ attempts,
2311
+ lastAttempt: /* @__PURE__ */ new Date()
2312
+ });
2313
+ errorCount++;
2314
+ if (attempts >= MAX_RETRY_ATTEMPTS) {
2315
+ console.log(
2316
+ chalk4.dim(`[${timestamp}] `) + chalk4.red(`\u2717 ${fileName}`) + chalk4.dim(` - ${errorMessage}`) + chalk4.red(` (gave up after ${MAX_RETRY_ATTEMPTS} attempts)`)
2317
+ );
2318
+ } else {
2319
+ console.log(
2320
+ chalk4.dim(`[${timestamp}] `) + chalk4.red(`\u2717 ${fileName}`) + chalk4.dim(` - ${errorMessage}`) + chalk4.yellow(` (will retry in ${RETRY_DELAY_MS / 1e3}s)`)
2321
+ );
2322
+ }
2323
+ printStatus();
1966
2324
  }
2325
+ };
2326
+ if (isRetry) {
2327
+ await syncFile();
2328
+ return;
2329
+ }
2330
+ const timeout = setTimeout(async () => {
2331
+ pendingChanges.delete(filePath);
2332
+ await syncFile();
1967
2333
  }, DEBOUNCE_MS);
1968
2334
  pendingChanges.set(filePath, timeout);
1969
2335
  };
@@ -1985,7 +2351,21 @@ async function watchCommand(options) {
1985
2351
  );
1986
2352
  });
1987
2353
  process.on("SIGINT", () => {
2354
+ clearInterval(retryInterval);
1988
2355
  console.log(chalk4.dim("\n\nStopping watch...\n"));
2356
+ if (successCount > 0 || failedSyncs.size > 0) {
2357
+ console.log(chalk4.bold("Session summary:"));
2358
+ if (successCount > 0) {
2359
+ console.log(chalk4.green(` \u2713 ${successCount} file(s) synced`));
2360
+ }
2361
+ if (failedSyncs.size > 0) {
2362
+ console.log(chalk4.red(` \u2717 ${failedSyncs.size} file(s) failed:`));
2363
+ for (const failed of failedSyncs.values()) {
2364
+ console.log(chalk4.dim(` - ${failed.fileName}: ${failed.error}`));
2365
+ }
2366
+ }
2367
+ console.log();
2368
+ }
1989
2369
  watcher.close();
1990
2370
  process.exit(0);
1991
2371
  });
@@ -2008,10 +2388,21 @@ async function deployCommand(environment, options) {
2008
2388
  }
2009
2389
  const shouldPromote = options?.promote || environment === "production";
2010
2390
  if (shouldPromote) {
2011
- const spinner2 = ora4("Promoting staging to production...").start();
2391
+ const client2 = createApiClient(config);
2392
+ const spinner2 = ora4("Sjekker produksjonsstatus...").start();
2012
2393
  try {
2013
- const client = createApiClient(config);
2014
- const deployment = await client.promoteToProduction(config.siteId);
2394
+ const status = await client2.getDeploymentStatus(config.siteId);
2395
+ if (!status.productionEnabled) {
2396
+ spinner2.fail("Produksjon er ikke aktivert");
2397
+ console.log();
2398
+ console.log(chalk5.yellow("\u26A0\uFE0F Produksjon er deaktivert for dette prosjektet."));
2399
+ console.log(chalk5.dim(" Aktiver produksjon f\xF8rst:"));
2400
+ console.log(chalk5.dim(" kode production enable [--domain <domain>]"));
2401
+ console.log();
2402
+ return;
2403
+ }
2404
+ spinner2.text = "Promoterer staging til produksjon...";
2405
+ const deployment = await client2.promoteToProduction(config.siteId);
2015
2406
  spinner2.succeed("Promoted to production");
2016
2407
  console.log();
2017
2408
  console.log(chalk5.dim("Deployment details:"));
@@ -2028,9 +2419,62 @@ async function deployCommand(environment, options) {
2028
2419
  }
2029
2420
  return;
2030
2421
  }
2422
+ const client = createApiClient(config);
2423
+ if (options?.force) {
2424
+ const forceSpinner = ora4("Sjekker l\xE5s...").start();
2425
+ try {
2426
+ const lockStatus = await client.getLockStatus(config.siteId);
2427
+ if (lockStatus.isLocked) {
2428
+ if (lockStatus.isStale) {
2429
+ forceSpinner.text = "Frigj\xF8r gammel l\xE5s...";
2430
+ } else {
2431
+ forceSpinner.warn("Aktiv l\xE5s funnet");
2432
+ console.log(chalk5.yellow("\n\u26A0\uFE0F Deployment er l\xE5st av en annen prosess."));
2433
+ console.log(chalk5.dim(` L\xE5st siden: ${lockStatus.acquiredAt ? new Date(lockStatus.acquiredAt).toLocaleString("nb-NO") : "ukjent"}`));
2434
+ console.log(chalk5.dim(` L\xE5s-ID: ${lockStatus.lockHolder}`));
2435
+ console.log();
2436
+ console.log(chalk5.yellow(" Hvis du er sikker p\xE5 at l\xE5sen er foreldet, kj\xF8r med --force igjen."));
2437
+ console.log(chalk5.dim(" L\xE5sen vil utl\xF8pe automatisk etter 10 minutter."));
2438
+ return;
2439
+ }
2440
+ const result = await client.forceReleaseLock(config.siteId);
2441
+ if (result.wasLocked) {
2442
+ forceSpinner.succeed("L\xE5s frigjort");
2443
+ console.log(chalk5.dim(` Tidligere l\xE5s fra: ${result.acquiredAt ? new Date(result.acquiredAt).toLocaleString("nb-NO") : "ukjent"}`));
2444
+ console.log();
2445
+ } else {
2446
+ forceSpinner.info("Ingen l\xE5s \xE5 frigj\xF8re");
2447
+ }
2448
+ } else {
2449
+ forceSpinner.info("Ingen l\xE5s aktiv");
2450
+ }
2451
+ } catch (error) {
2452
+ forceSpinner.fail("Kunne ikke sjekke/frigj\xF8re l\xE5s");
2453
+ console.error(chalk5.red("\nError:"), error.message || error);
2454
+ return;
2455
+ }
2456
+ }
2457
+ const preCheckSpinner = ora4("Sjekker scripts...").start();
2458
+ try {
2459
+ const scripts = await client.listScripts(config.siteId);
2460
+ const emptyScripts = scripts.filter(
2461
+ (s) => s.is_active && (!s.content || s.content.trim().length === 0)
2462
+ );
2463
+ if (emptyScripts.length > 0) {
2464
+ preCheckSpinner.warn(`${emptyScripts.length} tomme script(s) funnet`);
2465
+ console.log(chalk5.yellow("\n\u26A0\uFE0F F\xF8lgende scripts er tomme:"));
2466
+ emptyScripts.forEach((s) => {
2467
+ console.log(chalk5.dim(` - ${s.slug}.${s.type === "javascript" ? "js" : "css"}`));
2468
+ });
2469
+ console.log(chalk5.dim(" Tomme scripts har ingen effekt n\xE5r de er deployet.\n"));
2470
+ } else {
2471
+ preCheckSpinner.succeed(`${scripts.filter((s) => s.is_active).length} script(s) klare`);
2472
+ }
2473
+ } catch {
2474
+ preCheckSpinner.info("Kunne ikke sjekke scripts");
2475
+ }
2031
2476
  const spinner = ora4("Deploying to staging...").start();
2032
2477
  try {
2033
- const client = createApiClient(config);
2034
2478
  const deployment = await client.deploy(config.siteId, "staging");
2035
2479
  spinner.succeed("Deployed to staging");
2036
2480
  console.log();
@@ -2437,17 +2881,29 @@ async function statusCommand(options) {
2437
2881
  } else {
2438
2882
  console.log(chalk7.blue(" Staging: ") + chalk7.yellow("\u25CB") + chalk7.dim(" No deployments"));
2439
2883
  }
2884
+ const productionEnabled = deployStatus.productionEnabled ?? false;
2440
2885
  const prodStatus = deployStatus.production.lastSuccessful;
2441
- if (prodStatus) {
2886
+ if (!productionEnabled) {
2887
+ console.log(
2888
+ chalk7.gray(" Production: ") + chalk7.gray("\u25CB") + chalk7.gray(" Deaktivert") + chalk7.dim(" (kun staging)")
2889
+ );
2890
+ console.log();
2891
+ console.log(
2892
+ chalk7.dim(' \u{1F4A1} Run "kode production enable" to activate production environment.')
2893
+ );
2894
+ } else if (prodStatus) {
2442
2895
  console.log(
2443
2896
  chalk7.green(" Production: ") + chalk7.green("\u25CF") + chalk7.dim(` v${prodStatus.version}`) + chalk7.dim(` (${formatDate(prodStatus.completedAt)})`)
2444
2897
  );
2898
+ if (deployStatus.productionDomain) {
2899
+ console.log(chalk7.dim(` Domain: ${deployStatus.productionDomain}`));
2900
+ }
2445
2901
  } else {
2446
2902
  console.log(
2447
- chalk7.green(" Production: ") + chalk7.yellow("\u25CB") + chalk7.dim(" No deployments")
2903
+ chalk7.green(" Production: ") + chalk7.yellow("\u25CB") + chalk7.dim(" Aktivert, ingen deployments enda")
2448
2904
  );
2449
2905
  }
2450
- if (deployStatus.canPromote) {
2906
+ if (deployStatus.canPromote && productionEnabled) {
2451
2907
  console.log();
2452
2908
  console.log(
2453
2909
  chalk7.cyan(' \u{1F4A1} Staging is ahead of production. Run "kode deploy --promote" to update.')
package/dist/cli.js CHANGED
@@ -15,41 +15,101 @@ import {
15
15
  readPageContext,
16
16
  statusCommand,
17
17
  watchCommand
18
- } from "./chunk-P74WQ3SF.js";
18
+ } from "./chunk-CUZJE4JZ.js";
19
19
 
20
20
  // src/cli.ts
21
21
  import { Command } from "commander";
22
- import chalk4 from "chalk";
22
+ import chalk6 from "chalk";
23
23
  import { createRequire } from "module";
24
24
 
25
- // src/commands/pages.ts
25
+ // src/commands/rollback.ts
26
26
  import chalk from "chalk";
27
+ import ora from "ora";
28
+ async function rollbackCommand(environment = "staging") {
29
+ const projectRoot = findProjectRoot();
30
+ if (!projectRoot) {
31
+ console.log(chalk.red("\u274C Not in a Cure Kode project."));
32
+ console.log(chalk.dim(' Run "kode init" first.'));
33
+ return;
34
+ }
35
+ const config = getProjectConfig(projectRoot);
36
+ if (!config) {
37
+ console.log(chalk.red("\u274C Could not read project configuration."));
38
+ return;
39
+ }
40
+ if (!["staging", "production"].includes(environment)) {
41
+ console.log(chalk.red(`\u274C Invalid environment: ${environment}`));
42
+ console.log(chalk.dim(' Use "staging" or "production"'));
43
+ return;
44
+ }
45
+ const client = createApiClient(config);
46
+ const spinner = ora(`Ruller tilbake ${environment}...`).start();
47
+ try {
48
+ if (environment === "production") {
49
+ const status = await client.getDeploymentStatus(config.siteId);
50
+ if (!status.productionEnabled) {
51
+ spinner.fail("Produksjon er ikke aktivert");
52
+ console.log();
53
+ console.log(chalk.yellow("\u26A0\uFE0F Produksjon er deaktivert for dette prosjektet."));
54
+ console.log(chalk.dim(" Kan ikke rulle tilbake n\xE5r produksjon er deaktivert."));
55
+ return;
56
+ }
57
+ }
58
+ const result = await client.rollback(config.siteId, environment);
59
+ spinner.succeed(`Tilbakerulling fullf\xF8rt (${result.duration_ms}ms)`);
60
+ console.log();
61
+ console.log(chalk.dim("Tilbakerulling detaljer:"));
62
+ console.log(chalk.dim(` Fra: ${result.rolledBackFrom.version}`));
63
+ console.log(chalk.dim(` Til: ${result.rolledBackTo.version}`));
64
+ console.log();
65
+ console.log(chalk.bold("CDN URL:"));
66
+ console.log(chalk.cyan(` ${result.cdn_url}`));
67
+ console.log();
68
+ console.log(chalk.green(`\u2705 ${environment.charAt(0).toUpperCase() + environment.slice(1)} er n\xE5 tilbake til forrige versjon!`));
69
+ } catch (error) {
70
+ spinner.fail("Tilbakerulling feilet");
71
+ if (error.statusCode === 404) {
72
+ console.log();
73
+ console.log(chalk.yellow("\u26A0\uFE0F Ingen tidligere versjon \xE5 rulle tilbake til."));
74
+ console.log(chalk.dim(" Det m\xE5 v\xE6re minst 2 deployments for \xE5 kunne rulle tilbake."));
75
+ } else if (error.statusCode === 409) {
76
+ console.log();
77
+ console.log(chalk.yellow("\u26A0\uFE0F En annen deployment kj\xF8rer."));
78
+ console.log(chalk.dim(" Vent til den er ferdig og pr\xF8v igjen."));
79
+ } else {
80
+ console.error(chalk.red("\nError:"), error.message || error);
81
+ }
82
+ }
83
+ }
84
+
85
+ // src/commands/pages.ts
86
+ import chalk2 from "chalk";
27
87
  async function pagesCommand(options) {
28
88
  const projectRoot = findProjectRoot();
29
89
  if (!projectRoot) {
30
- console.log(chalk.red("Error: Not in a Cure Kode project."));
31
- console.log(chalk.dim('Run "kode init" first.'));
90
+ console.log(chalk2.red("Error: Not in a Cure Kode project."));
91
+ console.log(chalk2.dim('Run "kode init" first.'));
32
92
  return;
33
93
  }
34
94
  const config = getProjectConfig(projectRoot);
35
95
  if (!config) {
36
- console.log(chalk.red("Error: Invalid project configuration."));
96
+ console.log(chalk2.red("Error: Invalid project configuration."));
37
97
  return;
38
98
  }
39
99
  if (options.delete && options.page) {
40
100
  const deleted = deletePageContext(projectRoot, options.page);
41
101
  if (deleted) {
42
- console.log(chalk.green(`Deleted: ${options.page}`));
102
+ console.log(chalk2.green(`Deleted: ${options.page}`));
43
103
  } else {
44
- console.log(chalk.red(`Not found: ${options.page}`));
104
+ console.log(chalk2.red(`Not found: ${options.page}`));
45
105
  }
46
106
  return;
47
107
  }
48
108
  if (options.page) {
49
109
  const context = readPageContext(projectRoot, options.page);
50
110
  if (!context) {
51
- console.log(chalk.red(`Page not found: ${options.page}`));
52
- console.log(chalk.dim('Use "kode pages" to list cached pages'));
111
+ console.log(chalk2.red(`Page not found: ${options.page}`));
112
+ console.log(chalk2.dim('Use "kode pages" to list cached pages'));
53
113
  return;
54
114
  }
55
115
  if (options.json) {
@@ -61,80 +121,80 @@ async function pagesCommand(options) {
61
121
  }
62
122
  const pages = listCachedPages(projectRoot);
63
123
  if (pages.length === 0) {
64
- console.log(chalk.yellow("No cached pages."));
65
- console.log(chalk.dim('Use "kode html <url> --save" to cache page structures.'));
124
+ console.log(chalk2.yellow("No cached pages."));
125
+ console.log(chalk2.dim('Use "kode html <url> --save" to cache page structures.'));
66
126
  return;
67
127
  }
68
128
  if (options.json) {
69
129
  console.log(JSON.stringify(pages, null, 2));
70
130
  return;
71
131
  }
72
- console.log(chalk.bold(`Cached Pages (${pages.length})`));
132
+ console.log(chalk2.bold(`Cached Pages (${pages.length})`));
73
133
  console.log();
74
134
  for (const page of pages) {
75
135
  const path = new URL(page.url).pathname;
76
136
  const date = new Date(page.extractedAt).toLocaleDateString();
77
137
  const badges = [];
78
138
  if (page.sectionCount > 0) badges.push(`${page.sectionCount} sections`);
79
- if (page.cmsCollectionCount > 0) badges.push(chalk.cyan(`${page.cmsCollectionCount} CMS`));
80
- console.log(` ${chalk.bold(path)} ${chalk.dim(`[${page.slug}]`)}`);
139
+ if (page.cmsCollectionCount > 0) badges.push(chalk2.cyan(`${page.cmsCollectionCount} CMS`));
140
+ console.log(` ${chalk2.bold(path)} ${chalk2.dim(`[${page.slug}]`)}`);
81
141
  if (page.title) {
82
- console.log(chalk.dim(` "${page.title}"`));
142
+ console.log(chalk2.dim(` "${page.title}"`));
83
143
  }
84
- console.log(chalk.dim(` ${badges.join(", ")} \u2022 ${date}`));
144
+ console.log(chalk2.dim(` ${badges.join(", ")} \u2022 ${date}`));
85
145
  console.log();
86
146
  }
87
- console.log(chalk.dim(`Use "kode pages <slug>" to see details`));
88
- console.log(chalk.dim(`Use "kode html <url> --save --force" to refresh`));
147
+ console.log(chalk2.dim(`Use "kode pages <slug>" to see details`));
148
+ console.log(chalk2.dim(`Use "kode html <url> --save --force" to refresh`));
89
149
  }
90
150
  function printPageDetails(context) {
91
- console.log(chalk.bold(context.title || context.url));
92
- console.log(chalk.dim(context.url));
93
- console.log(chalk.dim(`Extracted: ${context.extractedAt}`));
151
+ console.log(chalk2.bold(context.title || context.url));
152
+ console.log(chalk2.dim(context.url));
153
+ console.log(chalk2.dim(`Extracted: ${context.extractedAt}`));
94
154
  console.log();
95
155
  if (context.sections.length > 0) {
96
- console.log(chalk.bold("Sections"));
156
+ console.log(chalk2.bold("Sections"));
97
157
  for (const section of context.sections) {
98
158
  const name = section.heading || section.id || section.className?.split(" ")[0] || "section";
99
159
  const badges = [];
100
- if (section.hasCms) badges.push(chalk.cyan("CMS"));
101
- if (section.hasForm) badges.push(chalk.yellow("Form"));
160
+ if (section.hasCms) badges.push(chalk2.cyan("CMS"));
161
+ if (section.hasForm) badges.push(chalk2.yellow("Form"));
102
162
  const badgeStr = badges.length > 0 ? ` [${badges.join(", ")}]` : "";
103
163
  console.log(` \u2022 ${name}${badgeStr}`);
104
164
  if (section.textSample) {
105
- console.log(chalk.dim(` "${section.textSample.slice(0, 80)}..."`));
165
+ console.log(chalk2.dim(` "${section.textSample.slice(0, 80)}..."`));
106
166
  }
107
167
  }
108
168
  console.log();
109
169
  }
110
170
  if (context.headings.length > 0) {
111
- console.log(chalk.bold("Headings"));
171
+ console.log(chalk2.bold("Headings"));
112
172
  for (const h of context.headings.slice(0, 10)) {
113
173
  const level = "H" + h.level;
114
- console.log(` ${chalk.dim(level)} ${h.text}`);
174
+ console.log(` ${chalk2.dim(level)} ${h.text}`);
115
175
  }
116
176
  if (context.headings.length > 10) {
117
- console.log(chalk.dim(` ... and ${context.headings.length - 10} more`));
177
+ console.log(chalk2.dim(` ... and ${context.headings.length - 10} more`));
118
178
  }
119
179
  console.log();
120
180
  }
121
181
  if (context.ctas.length > 0) {
122
- console.log(chalk.bold("CTAs"));
182
+ console.log(chalk2.bold("CTAs"));
123
183
  for (const cta of context.ctas) {
124
- const href = cta.href ? chalk.dim(` \u2192 ${cta.href}`) : "";
184
+ const href = cta.href ? chalk2.dim(` \u2192 ${cta.href}`) : "";
125
185
  console.log(` \u2022 "${cta.text}"${href}`);
126
- console.log(chalk.dim(` in ${cta.location}`));
186
+ console.log(chalk2.dim(` in ${cta.location}`));
127
187
  }
128
188
  console.log();
129
189
  }
130
190
  if (context.forms.length > 0) {
131
- console.log(chalk.bold("Forms"));
191
+ console.log(chalk2.bold("Forms"));
132
192
  for (const form of context.forms) {
133
- console.log(` ${chalk.bold(form.name || "form")}`);
193
+ console.log(` ${chalk2.bold(form.name || "form")}`);
134
194
  for (const field of form.fields) {
135
- const required = field.required ? chalk.red("*") : "";
195
+ const required = field.required ? chalk2.red("*") : "";
136
196
  const label = field.label || field.type;
137
- console.log(` \u2022 ${label}${required} ${chalk.dim(`(${field.type})`)}`);
197
+ console.log(` \u2022 ${label}${required} ${chalk2.dim(`(${field.type})`)}`);
138
198
  }
139
199
  if (form.submitText) {
140
200
  console.log(` \u2192 Submit: "${form.submitText}"`);
@@ -143,31 +203,31 @@ function printPageDetails(context) {
143
203
  console.log();
144
204
  }
145
205
  if (context.cmsPatterns.length > 0) {
146
- console.log(chalk.bold("CMS Collections"));
206
+ console.log(chalk2.bold("CMS Collections"));
147
207
  for (const cms of context.cmsPatterns) {
148
- console.log(` ${chalk.bold(cms.containerClass)}: ${chalk.cyan(`${cms.itemCount} items`)}`);
208
+ console.log(` ${chalk2.bold(cms.containerClass)}: ${chalk2.cyan(`${cms.itemCount} items`)}`);
149
209
  if (cms.templateFields.length > 0) {
150
- console.log(chalk.dim(` Template fields: ${cms.templateFields.join(", ")}`));
210
+ console.log(chalk2.dim(` Template fields: ${cms.templateFields.join(", ")}`));
151
211
  }
152
212
  }
153
213
  console.log();
154
214
  }
155
215
  if (context.navigation.length > 0) {
156
- console.log(chalk.bold("Navigation"));
216
+ console.log(chalk2.bold("Navigation"));
157
217
  for (const nav of context.navigation) {
158
- console.log(` ${chalk.bold(nav.type)}:`);
218
+ console.log(` ${chalk2.bold(nav.type)}:`);
159
219
  for (const item of nav.items.slice(0, 8)) {
160
- const href = item.href ? chalk.dim(` \u2192 ${item.href}`) : "";
220
+ const href = item.href ? chalk2.dim(` \u2192 ${item.href}`) : "";
161
221
  console.log(` \u2022 ${item.text}${href}`);
162
222
  }
163
223
  if (nav.items.length > 8) {
164
- console.log(chalk.dim(` ... and ${nav.items.length - 8} more`));
224
+ console.log(chalk2.dim(` ... and ${nav.items.length - 8} more`));
165
225
  }
166
226
  }
167
227
  console.log();
168
228
  }
169
229
  if (context.notes && context.notes.length > 0) {
170
- console.log(chalk.bold("Notes"));
230
+ console.log(chalk2.bold("Notes"));
171
231
  for (const note of context.notes) {
172
232
  console.log(` \u2022 ${note}`);
173
233
  }
@@ -176,40 +236,40 @@ function printPageDetails(context) {
176
236
  }
177
237
 
178
238
  // src/commands/set.ts
179
- import chalk2 from "chalk";
180
- import ora from "ora";
239
+ import chalk3 from "chalk";
240
+ import ora2 from "ora";
181
241
  async function setCommand(script, options) {
182
242
  const projectRoot = findProjectRoot();
183
243
  if (!projectRoot) {
184
- console.log(chalk2.red("\u274C Not in a Cure Kode project."));
185
- console.log(chalk2.dim(' Run "kode init" first.'));
244
+ console.log(chalk3.red("\u274C Not in a Cure Kode project."));
245
+ console.log(chalk3.dim(' Run "kode init" first.'));
186
246
  return;
187
247
  }
188
248
  const config = getProjectConfig(projectRoot);
189
249
  if (!config) {
190
- console.log(chalk2.red("\u274C Could not read project configuration."));
250
+ console.log(chalk3.red("\u274C Could not read project configuration."));
191
251
  return;
192
252
  }
193
253
  if (!options.scope && options.autoLoad === void 0) {
194
- console.log(chalk2.yellow("\u26A0\uFE0F No changes specified."));
195
- console.log(chalk2.dim(" Use --scope or --auto-load/--no-auto-load"));
254
+ console.log(chalk3.yellow("\u26A0\uFE0F No changes specified."));
255
+ console.log(chalk3.dim(" Use --scope or --auto-load/--no-auto-load"));
196
256
  console.log();
197
- console.log(chalk2.dim("Examples:"));
198
- console.log(chalk2.dim(" kode set my-script --scope page-specific"));
199
- console.log(chalk2.dim(" kode set my-script --scope global --auto-load"));
200
- console.log(chalk2.dim(" kode set my-script --no-auto-load"));
257
+ console.log(chalk3.dim("Examples:"));
258
+ console.log(chalk3.dim(" kode set my-script --scope page-specific"));
259
+ console.log(chalk3.dim(" kode set my-script --scope global --auto-load"));
260
+ console.log(chalk3.dim(" kode set my-script --no-auto-load"));
201
261
  return;
202
262
  }
203
- const spinner = ora(`Updating ${script}...`).start();
263
+ const spinner = ora2(`Updating ${script}...`).start();
204
264
  try {
205
265
  const client = createApiClient(config);
206
266
  const scripts = await client.listScripts(config.siteId);
207
267
  const targetScript = scripts.find((s) => s.slug === script || s.name === script);
208
268
  if (!targetScript) {
209
269
  spinner.fail(`Script "${script}" not found`);
210
- console.log(chalk2.dim("\nAvailable scripts:"));
270
+ console.log(chalk3.dim("\nAvailable scripts:"));
211
271
  scripts.forEach((s) => {
212
- console.log(chalk2.dim(` - ${s.slug}`));
272
+ console.log(chalk3.dim(` - ${s.slug}`));
213
273
  });
214
274
  return;
215
275
  }
@@ -229,46 +289,134 @@ async function setCommand(script, options) {
229
289
  }
230
290
  updates.changeSummary = `Updated settings: ${changes.join(", ")}`;
231
291
  const updated = await client.updateScript(targetScript.id, updates);
232
- spinner.succeed(chalk2.green(`Updated ${script}`));
292
+ spinner.succeed(chalk3.green(`Updated ${script}`));
233
293
  console.log();
234
294
  for (const change of changes) {
235
- console.log(chalk2.dim(` ${change}`));
295
+ console.log(chalk3.dim(` ${change}`));
236
296
  }
237
297
  console.log();
238
298
  if (options.scope === "page-specific") {
239
- console.log(chalk2.yellow("\u26A0\uFE0F Page-specific scripts need page assignments to load."));
240
- console.log(chalk2.dim(" Use app.cure.no \u2192 Kode to assign pages, or use MCP:"));
241
- console.log(chalk2.dim(" kode_assign_script_to_page(scriptSlug, pageSlug)"));
299
+ console.log(chalk3.yellow("\u26A0\uFE0F Page-specific scripts need page assignments to load."));
300
+ console.log(chalk3.dim(" Use app.cure.no \u2192 Kode to assign pages, or use MCP:"));
301
+ console.log(chalk3.dim(" kode_assign_script_to_page(scriptSlug, pageSlug)"));
242
302
  console.log();
243
303
  }
244
- console.log(chalk2.dim('Run "kode deploy" to make changes live.'));
304
+ console.log(chalk3.dim('Run "kode deploy" to make changes live.'));
245
305
  } catch (error) {
246
306
  spinner.fail("Failed to update script");
247
- console.error(chalk2.red("\nError:"), error);
307
+ console.error(chalk3.red("\nError:"), error);
308
+ }
309
+ }
310
+
311
+ // src/commands/production.ts
312
+ import chalk4 from "chalk";
313
+ import ora3 from "ora";
314
+ async function productionCommand(action, options) {
315
+ if (!["enable", "disable", "status"].includes(action)) {
316
+ console.log(chalk4.red("\u274C Invalid action. Use: enable, disable, or status"));
317
+ console.log(chalk4.dim(" kode production enable [--domain <domain>]"));
318
+ console.log(chalk4.dim(" kode production disable"));
319
+ console.log(chalk4.dim(" kode production status"));
320
+ return;
321
+ }
322
+ const projectRoot = findProjectRoot();
323
+ if (!projectRoot) {
324
+ console.log(chalk4.red("\u274C Not in a Cure Kode project."));
325
+ console.log(chalk4.dim(' Run "kode init" first.'));
326
+ return;
327
+ }
328
+ const config = getProjectConfig(projectRoot);
329
+ if (!config) {
330
+ console.log(chalk4.red("\u274C Could not read project configuration."));
331
+ return;
332
+ }
333
+ const client = createApiClient(config);
334
+ if (action === "status") {
335
+ const spinner2 = ora3("Fetching production status...").start();
336
+ try {
337
+ const status = await client.getDeploymentStatus(config.siteId);
338
+ spinner2.stop();
339
+ console.log();
340
+ console.log(chalk4.bold("Production Status"));
341
+ console.log();
342
+ if (status.productionEnabled) {
343
+ console.log(chalk4.green(" \u25CF Produksjon er aktivert"));
344
+ if (status.productionDomain) {
345
+ console.log(chalk4.dim(` Domain: ${status.productionDomain}`));
346
+ }
347
+ if (status.production.lastSuccessful) {
348
+ console.log(
349
+ chalk4.dim(
350
+ ` Siste deploy: v${status.production.lastSuccessful.version}`
351
+ )
352
+ );
353
+ }
354
+ } else {
355
+ console.log(chalk4.gray(" \u25CB Produksjon er deaktivert"));
356
+ console.log(chalk4.dim(" Kun staging er aktiv"));
357
+ }
358
+ console.log();
359
+ } catch (error) {
360
+ spinner2.fail("Failed to fetch status");
361
+ console.error(chalk4.red("\nError:"), error.message || error);
362
+ }
363
+ return;
364
+ }
365
+ const spinner = ora3(
366
+ action === "enable" ? "Aktiverer produksjon..." : "Deaktiverer produksjon..."
367
+ ).start();
368
+ try {
369
+ const result = await client.setProductionEnabled(
370
+ config.siteId,
371
+ action === "enable",
372
+ options?.domain
373
+ );
374
+ spinner.stop();
375
+ console.log();
376
+ if (action === "enable") {
377
+ console.log(chalk4.green("\u2713 Produksjon er n\xE5 aktivert"));
378
+ if (result.productionDomain) {
379
+ console.log(chalk4.dim(` Domain: ${result.productionDomain}`));
380
+ }
381
+ console.log();
382
+ console.log(chalk4.dim(" Neste steg:"));
383
+ console.log(chalk4.dim(" 1. Deploy til staging: kode deploy"));
384
+ console.log(chalk4.dim(" 2. Promoter til produksjon: kode deploy --promote"));
385
+ } else {
386
+ console.log(chalk4.yellow("\u2713 Produksjon er n\xE5 deaktivert"));
387
+ console.log(chalk4.dim(" Kun staging-milj\xF8et er aktivt."));
388
+ console.log(
389
+ chalk4.dim(" Produksjonsdomenet vil f\xE5 en tom script-respons.")
390
+ );
391
+ }
392
+ console.log();
393
+ } catch (error) {
394
+ spinner.fail(action === "enable" ? "Kunne ikke aktivere produksjon" : "Kunne ikke deaktivere produksjon");
395
+ console.error(chalk4.red("\nError:"), error.message || error);
248
396
  }
249
397
  }
250
398
 
251
399
  // src/commands/update-claude-md.ts
252
- import chalk3 from "chalk";
400
+ import chalk5 from "chalk";
253
401
  import { existsSync, readFileSync, writeFileSync } from "fs";
254
402
  import { join } from "path";
255
403
  async function updateClaudeMdCommand() {
256
404
  const projectRoot = findProjectRoot();
257
405
  if (!projectRoot) {
258
- console.log(chalk3.red("\u274C Not in a Cure Kode project."));
259
- console.log(chalk3.dim(' Run "kode init" first.'));
406
+ console.log(chalk5.red("\u274C Not in a Cure Kode project."));
407
+ console.log(chalk5.dim(' Run "kode init" first.'));
260
408
  return;
261
409
  }
262
410
  const config = getProjectConfig(projectRoot);
263
411
  if (!config) {
264
- console.log(chalk3.red("\u274C Could not read project configuration."));
412
+ console.log(chalk5.red("\u274C Could not read project configuration."));
265
413
  return;
266
414
  }
267
415
  const claudeMdPath = join(projectRoot, "CLAUDE.md");
268
416
  const newKodeSection = generateClaudeMdMinimal(config.siteName, config.siteSlug);
269
417
  if (!existsSync(claudeMdPath)) {
270
418
  writeFileSync(claudeMdPath, newKodeSection);
271
- console.log(chalk3.green("\u2705 Created CLAUDE.md with Cure Kode section"));
419
+ console.log(chalk5.green("\u2705 Created CLAUDE.md with Cure Kode section"));
272
420
  return;
273
421
  }
274
422
  let content = readFileSync(claudeMdPath, "utf-8");
@@ -287,18 +435,18 @@ async function updateClaudeMdCommand() {
287
435
  content = newKodeSection + "---\n\n" + content;
288
436
  writeFileSync(claudeMdPath, content);
289
437
  if (removedCount > 1) {
290
- console.log(chalk3.green(`\u2705 Cleaned up ${removedCount} duplicate Kode sections and added fresh one`));
438
+ console.log(chalk5.green(`\u2705 Cleaned up ${removedCount} duplicate Kode sections and added fresh one`));
291
439
  } else if (removedCount === 1) {
292
- console.log(chalk3.green("\u2705 Updated Cure Kode section in CLAUDE.md"));
440
+ console.log(chalk5.green("\u2705 Updated Cure Kode section in CLAUDE.md"));
293
441
  } else {
294
- console.log(chalk3.green("\u2705 Added Cure Kode section to CLAUDE.md"));
442
+ console.log(chalk5.green("\u2705 Added Cure Kode section to CLAUDE.md"));
295
443
  }
296
444
  console.log();
297
- console.log(chalk3.dim("The Cure Kode section now includes:"));
298
- console.log(chalk3.dim(" \u2022 What is Cure Kode (internal tool explanation)"));
299
- console.log(chalk3.dim(" \u2022 CDN URL with script tag for Webflow"));
300
- console.log(chalk3.dim(" \u2022 Workflow steps"));
301
- console.log(chalk3.dim(" \u2022 Command reference"));
445
+ console.log(chalk5.dim("The Cure Kode section now includes:"));
446
+ console.log(chalk5.dim(" \u2022 What is Cure Kode (internal tool explanation)"));
447
+ console.log(chalk5.dim(" \u2022 CDN URL with script tag for Webflow"));
448
+ console.log(chalk5.dim(" \u2022 Workflow steps"));
449
+ console.log(chalk5.dim(" \u2022 Command reference"));
302
450
  }
303
451
 
304
452
  // src/cli.ts
@@ -318,9 +466,12 @@ program.command("push").description("Upload local scripts to Cure").argument("[s
318
466
  program.command("watch").description("Watch for changes and auto-push").option("-d, --deploy", "Auto-deploy after each push").action((options) => {
319
467
  watchCommand(options);
320
468
  });
321
- program.command("deploy [environment]").description("Deploy to staging or production").option("-p, --promote", "Promote staging to production").action((environment, options) => {
469
+ program.command("deploy [environment]").description("Deploy to staging or production").option("-p, --promote", "Promote staging to production").option("-f, --force", "Force release stale deploy lock before deploying").action((environment, options) => {
322
470
  deployCommand(environment, options);
323
471
  });
472
+ program.command("rollback [environment]").description("Rollback to previous deployment").action((environment = "staging") => {
473
+ rollbackCommand(environment);
474
+ });
324
475
  program.command("html <url>").description("Fetch and analyze HTML from a URL").option("-j, --json", "Output as JSON").option("--scripts", "Show only scripts").option("--styles", "Show only styles").option("-s, --save", "Save page structure to context").option("-f, --force", "Force refresh when using --save").action((url, options) => {
325
476
  htmlCommand(url, options);
326
477
  });
@@ -336,12 +487,15 @@ program.command("context").description("View or edit project context for AI agen
336
487
  program.command("set <script>").description("Update script settings (scope, autoLoad)").option("--scope <scope>", "Set scope: global or page-specific").option("--auto-load", "Enable auto-loading").option("--no-auto-load", "Disable auto-loading").action((script, options) => {
337
488
  setCommand(script, options);
338
489
  });
490
+ program.command("production <action>").description("Enable or disable production environment (v2.3)").option("-d, --domain <domain>", "Set production domain when enabling").action((action, options) => {
491
+ productionCommand(action, options);
492
+ });
339
493
  program.command("update-claude-md").alias("ucm").description("Add or update Cure Kode section in CLAUDE.md").action(() => {
340
494
  updateClaudeMdCommand();
341
495
  });
342
496
  program.showHelpAfterError();
343
497
  console.log();
344
- console.log(chalk4.bold(" Cure Kode CLI"));
345
- console.log(chalk4.dim(" Manage JS/CSS for Webflow sites"));
498
+ console.log(chalk6.bold(" Cure Kode CLI"));
499
+ console.log(chalk6.dim(" Manage JS/CSS for Webflow sites"));
346
500
  console.log();
347
501
  program.parse();
package/dist/index.d.ts CHANGED
@@ -41,6 +41,9 @@ declare function getScriptsDir(projectRoot: string, projectConfig?: ProjectConfi
41
41
 
42
42
  /**
43
43
  * Cure Kode API Client
44
+ *
45
+ * All requests automatically retry on network/transient errors
46
+ * with exponential backoff (500ms → 1s → 2s).
44
47
  */
45
48
  interface CdnSite {
46
49
  id: string;
@@ -146,6 +149,8 @@ declare class KodeApiClient {
146
149
  deploy(siteId: string, environment?: 'staging' | 'production'): Promise<CdnDeployment>;
147
150
  promoteToProduction(siteId: string, stagingDeploymentId?: string): Promise<CdnDeployment>;
148
151
  getDeploymentStatus(siteId: string): Promise<{
152
+ productionEnabled: boolean;
153
+ productionDomain: string | null;
149
154
  staging: {
150
155
  latest: CdnDeployment | null;
151
156
  lastSuccessful: any;
@@ -156,7 +161,42 @@ declare class KodeApiClient {
156
161
  };
157
162
  canPromote: boolean;
158
163
  }>;
164
+ rollback(siteId: string, environment?: 'staging' | 'production'): Promise<{
165
+ id: string;
166
+ status: 'rolled_back';
167
+ environment: 'staging' | 'production';
168
+ rolledBackFrom: {
169
+ version: string;
170
+ deploymentId: string;
171
+ };
172
+ rolledBackTo: {
173
+ version: string;
174
+ deploymentId: string;
175
+ };
176
+ cdn_url: string;
177
+ duration_ms: number;
178
+ }>;
179
+ setProductionEnabled(siteId: string, enabled: boolean, productionDomain?: string): Promise<{
180
+ success: boolean;
181
+ productionEnabled: boolean;
182
+ productionDomain: string | null;
183
+ }>;
159
184
  fetchHtml(url: string): Promise<ParsedHtmlResult>;
185
+ getLockStatus(siteId: string): Promise<{
186
+ isLocked: boolean;
187
+ isStale: boolean;
188
+ lockHolder: string | null;
189
+ acquiredAt: string | null;
190
+ }>;
191
+ forceReleaseLock(siteId: string): Promise<{
192
+ success: boolean;
193
+ message: string;
194
+ wasLocked: boolean;
195
+ wasStale?: boolean;
196
+ previousLockHolder?: string;
197
+ acquiredAt?: string;
198
+ duration_ms?: number;
199
+ }>;
160
200
  }
161
201
  /**
162
202
  * Create API client from project config
@@ -196,6 +236,11 @@ declare function pushCommand(options: {
196
236
 
197
237
  /**
198
238
  * Watch for local changes and auto-push
239
+ *
240
+ * Features:
241
+ * - Error tracking with retry queue
242
+ * - Status summary on error/success
243
+ * - Automatic retry after 30 seconds
199
244
  */
200
245
  declare function watchCommand(options: {
201
246
  deploy?: boolean;
@@ -207,9 +252,11 @@ declare function watchCommand(options: {
207
252
  * Workflow:
208
253
  * - `kode deploy` or `kode deploy staging` → deploys to staging
209
254
  * - `kode deploy production` or `kode deploy --promote` → promotes staging to production
255
+ * - `kode deploy --force` → force release stale lock before deploying
210
256
  */
211
257
  declare function deployCommand(environment?: 'staging' | 'production', options?: {
212
258
  promote?: boolean;
259
+ force?: boolean;
213
260
  }): Promise<void>;
214
261
 
215
262
  /**
package/dist/index.js CHANGED
@@ -27,7 +27,7 @@ import {
27
27
  updateScriptPurpose,
28
28
  watchCommand,
29
29
  writeContext
30
- } from "./chunk-P74WQ3SF.js";
30
+ } from "./chunk-CUZJE4JZ.js";
31
31
  export {
32
32
  KodeApiClient,
33
33
  KodeApiError,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@curenorway/kode-cli",
3
- "version": "1.8.0",
3
+ "version": "1.9.2",
4
4
  "description": "CLI tool for Cure Kode - manage JS/CSS scripts for Webflow sites",
5
5
  "type": "module",
6
6
  "bin": {