@curenorway/kode-cli 1.9.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.
@@ -1337,11 +1337,16 @@ async function initCommand(options) {
1337
1337
  console.log(chalk.bold("\n\u{1F680} Cure Kode Setup\n"));
1338
1338
  const { apiKey } = await prompt([
1339
1339
  {
1340
- type: "password",
1340
+ type: "input",
1341
1341
  name: "apiKey",
1342
1342
  message: "API Key (from Cure App \u2192 Tools \u2192 Kode \u2192 API Keys):",
1343
1343
  initial: options.apiKey,
1344
- 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
+ }
1345
1350
  }
1346
1351
  ]);
1347
1352
  const spinner = ora("Validating API key...").start();
@@ -1405,7 +1410,7 @@ config.json
1405
1410
  mcpConfig.mcpServers["cure-kode"] = {
1406
1411
  type: "stdio",
1407
1412
  command: "npx",
1408
- args: ["-y", "@curenorway/kode-mcp"]
1413
+ args: ["-y", "@curenorway/kode-mcp@^1.3.0"]
1409
1414
  };
1410
1415
  let webflowToken = site.webflow_token;
1411
1416
  let webflowMcpMethod = null;
@@ -1456,7 +1461,7 @@ config.json
1456
1461
  mcpConfig.mcpServers["webflow"] = {
1457
1462
  type: "stdio",
1458
1463
  command: "npx",
1459
- args: ["-y", "webflow-mcp-server@latest"],
1464
+ args: ["-y", "webflow-mcp-server@^0.6.0"],
1460
1465
  env: {
1461
1466
  WEBFLOW_TOKEN: webflowToken
1462
1467
  }
@@ -1470,7 +1475,7 @@ config.json
1470
1475
  mcpConfig.mcpServers["playwright"] = {
1471
1476
  type: "stdio",
1472
1477
  command: "npx",
1473
- args: ["-y", "@playwright/mcp@latest"]
1478
+ args: ["-y", "@playwright/mcp@^0.0.21"]
1474
1479
  };
1475
1480
  spinner.start("Generating AI context files...");
1476
1481
  writeFileSync3(mcpConfigPath, JSON.stringify(mcpConfig, null, 2) + "\n");
@@ -1641,6 +1646,80 @@ config.json
1641
1646
  }
1642
1647
  }
1643
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
+
1644
1723
  // src/api.ts
1645
1724
  var KodeApiError = class extends Error {
1646
1725
  constructor(message, statusCode, response) {
@@ -1663,23 +1742,38 @@ var KodeApiClient = class {
1663
1742
  }
1664
1743
  async request(endpoint, options = {}) {
1665
1744
  const url = `${this.baseUrl}${endpoint}`;
1666
- const response = await fetch(url, {
1667
- ...options,
1668
- headers: {
1669
- "Content-Type": "application/json",
1670
- "X-API-Key": this.apiKey,
1671
- ...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
+ }
1672
1775
  }
1673
- });
1674
- const data = await response.json();
1675
- if (!response.ok) {
1676
- throw new KodeApiError(
1677
- data.error || `Request failed with status ${response.status}`,
1678
- response.status,
1679
- data
1680
- );
1681
- }
1682
- return data;
1776
+ );
1683
1777
  }
1684
1778
  // Sites
1685
1779
  async getSite(siteId) {
@@ -1750,6 +1844,15 @@ var KodeApiClient = class {
1750
1844
  async getDeploymentStatus(siteId) {
1751
1845
  return this.request(`/api/cdn/sites/${siteId}/deployments/status`);
1752
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
+ }
1753
1856
  // Production enabled toggle (v2.3)
1754
1857
  async setProductionEnabled(siteId, enabled, productionDomain) {
1755
1858
  return this.request(`/api/cdn/sites/${siteId}/production`, {
@@ -1767,6 +1870,16 @@ var KodeApiClient = class {
1767
1870
  body: JSON.stringify({ url })
1768
1871
  });
1769
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
+ }
1770
1883
  };
1771
1884
  function createApiClient(config) {
1772
1885
  return new KodeApiClient(config);
@@ -1923,11 +2036,16 @@ async function pushCommand(options) {
1923
2036
  let skipped = 0;
1924
2037
  spinner.stop();
1925
2038
  console.log();
2039
+ let emptyScriptCount = 0;
1926
2040
  for (const file of filesToPush) {
1927
2041
  const filePath = join5(scriptsDir, file);
1928
2042
  const content = readFileSync4(filePath, "utf-8");
1929
2043
  const slug = basename(file, extname(file));
1930
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
+ }
1931
2049
  const remoteScript = remoteScripts.find((s) => s.slug === slug);
1932
2050
  const localMeta = metadata.find((m) => m.slug === slug);
1933
2051
  if (remoteScript) {
@@ -1974,6 +2092,11 @@ async function pushCommand(options) {
1974
2092
  if (skipped > 0) {
1975
2093
  console.log(chalk3.dim(` Skipped ${skipped} unchanged script(s)`));
1976
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
+ }
1977
2100
  const updatedScripts = await client.listScripts(config.siteId);
1978
2101
  const updatedMetadata = updatedScripts.map((s) => ({
1979
2102
  id: s.id,
@@ -2042,15 +2165,59 @@ async function watchCommand(options) {
2042
2165
  }
2043
2166
  const pendingChanges = /* @__PURE__ */ new Map();
2044
2167
  const DEBOUNCE_MS = 500;
2045
- 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) => {
2046
2212
  const fileName = basename2(filePath);
2047
2213
  const slug = basename2(fileName, extname2(fileName));
2048
2214
  const type = extname2(fileName) === ".js" ? "javascript" : "css";
2049
- if (pendingChanges.has(filePath)) {
2050
- clearTimeout(pendingChanges.get(filePath));
2215
+ if (!isRetry) {
2216
+ if (pendingChanges.has(filePath)) {
2217
+ clearTimeout(pendingChanges.get(filePath));
2218
+ }
2051
2219
  }
2052
- const timeout = setTimeout(async () => {
2053
- pendingChanges.delete(filePath);
2220
+ const syncFile = async () => {
2054
2221
  try {
2055
2222
  const content = readFileSync5(filePath, "utf-8");
2056
2223
  const remoteScript = remoteScripts.find((s) => s.slug === slug);
@@ -2058,6 +2225,9 @@ async function watchCommand(options) {
2058
2225
  const timestamp = (/* @__PURE__ */ new Date()).toLocaleTimeString("nb-NO");
2059
2226
  if (remoteScript) {
2060
2227
  if (remoteScript.content === content) {
2228
+ if (failedSyncs.has(filePath)) {
2229
+ failedSyncs.delete(filePath);
2230
+ }
2061
2231
  return;
2062
2232
  }
2063
2233
  await client.updateScript(remoteScript.id, {
@@ -2066,9 +2236,24 @@ async function watchCommand(options) {
2066
2236
  });
2067
2237
  remoteScript.content = content;
2068
2238
  remoteScript.current_version++;
2069
- console.log(
2070
- chalk4.dim(`[${timestamp}] `) + chalk4.green(`\u2713 ${fileName}`) + chalk4.dim(` \u2192 v${remoteScript.current_version}`)
2071
- );
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++;
2072
2257
  if (options.deploy) {
2073
2258
  try {
2074
2259
  await client.deploy(config.siteId, config.environment || "staging");
@@ -2077,7 +2262,7 @@ async function watchCommand(options) {
2077
2262
  );
2078
2263
  } catch (deployError) {
2079
2264
  console.log(
2080
- chalk4.dim(`[${timestamp}] `) + chalk4.red(` \u21B3 Deploy failed`)
2265
+ chalk4.dim(`[${timestamp}] `) + chalk4.red(` \u21B3 Deploy failed: ${deployError.message || "Unknown error"}`)
2081
2266
  );
2082
2267
  }
2083
2268
  }
@@ -2090,9 +2275,17 @@ async function watchCommand(options) {
2090
2275
  content
2091
2276
  });
2092
2277
  remoteScripts.push(newScript);
2093
- console.log(
2094
- chalk4.dim(`[${timestamp}] `) + chalk4.green(`\u2713 ${fileName}`) + chalk4.cyan(" (created)")
2095
- );
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++;
2096
2289
  const updatedMetadata = remoteScripts.map((s) => ({
2097
2290
  id: s.id,
2098
2291
  slug: s.slug,
@@ -2107,10 +2300,36 @@ async function watchCommand(options) {
2107
2300
  }
2108
2301
  } catch (error) {
2109
2302
  const timestamp = (/* @__PURE__ */ new Date()).toLocaleTimeString("nb-NO");
2110
- console.log(
2111
- chalk4.dim(`[${timestamp}] `) + chalk4.red(`\u2717 ${fileName}`) + chalk4.dim(` - ${error.message || "Unknown error"}`)
2112
- );
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();
2113
2324
  }
2325
+ };
2326
+ if (isRetry) {
2327
+ await syncFile();
2328
+ return;
2329
+ }
2330
+ const timeout = setTimeout(async () => {
2331
+ pendingChanges.delete(filePath);
2332
+ await syncFile();
2114
2333
  }, DEBOUNCE_MS);
2115
2334
  pendingChanges.set(filePath, timeout);
2116
2335
  };
@@ -2132,7 +2351,21 @@ async function watchCommand(options) {
2132
2351
  );
2133
2352
  });
2134
2353
  process.on("SIGINT", () => {
2354
+ clearInterval(retryInterval);
2135
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
+ }
2136
2369
  watcher.close();
2137
2370
  process.exit(0);
2138
2371
  });
@@ -2155,10 +2388,10 @@ async function deployCommand(environment, options) {
2155
2388
  }
2156
2389
  const shouldPromote = options?.promote || environment === "production";
2157
2390
  if (shouldPromote) {
2158
- const client = createApiClient(config);
2391
+ const client2 = createApiClient(config);
2159
2392
  const spinner2 = ora4("Sjekker produksjonsstatus...").start();
2160
2393
  try {
2161
- const status = await client.getDeploymentStatus(config.siteId);
2394
+ const status = await client2.getDeploymentStatus(config.siteId);
2162
2395
  if (!status.productionEnabled) {
2163
2396
  spinner2.fail("Produksjon er ikke aktivert");
2164
2397
  console.log();
@@ -2169,7 +2402,7 @@ async function deployCommand(environment, options) {
2169
2402
  return;
2170
2403
  }
2171
2404
  spinner2.text = "Promoterer staging til produksjon...";
2172
- const deployment = await client.promoteToProduction(config.siteId);
2405
+ const deployment = await client2.promoteToProduction(config.siteId);
2173
2406
  spinner2.succeed("Promoted to production");
2174
2407
  console.log();
2175
2408
  console.log(chalk5.dim("Deployment details:"));
@@ -2186,9 +2419,62 @@ async function deployCommand(environment, options) {
2186
2419
  }
2187
2420
  return;
2188
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
+ }
2189
2476
  const spinner = ora4("Deploying to staging...").start();
2190
2477
  try {
2191
- const client = createApiClient(config);
2192
2478
  const deployment = await client.deploy(config.siteId, "staging");
2193
2479
  spinner.succeed("Deployed to staging");
2194
2480
  console.log();
package/dist/cli.js CHANGED
@@ -15,41 +15,101 @@ import {
15
15
  readPageContext,
16
16
  statusCommand,
17
17
  watchCommand
18
- } from "./chunk-HKPZVOMY.js";
18
+ } from "./chunk-CUZJE4JZ.js";
19
19
 
20
20
  // src/cli.ts
21
21
  import { Command } from "commander";
22
- import chalk5 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,80 +289,80 @@ 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);
248
308
  }
249
309
  }
250
310
 
251
311
  // src/commands/production.ts
252
- import chalk3 from "chalk";
253
- import ora2 from "ora";
312
+ import chalk4 from "chalk";
313
+ import ora3 from "ora";
254
314
  async function productionCommand(action, options) {
255
315
  if (!["enable", "disable", "status"].includes(action)) {
256
- console.log(chalk3.red("\u274C Invalid action. Use: enable, disable, or status"));
257
- console.log(chalk3.dim(" kode production enable [--domain <domain>]"));
258
- console.log(chalk3.dim(" kode production disable"));
259
- console.log(chalk3.dim(" kode production status"));
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"));
260
320
  return;
261
321
  }
262
322
  const projectRoot = findProjectRoot();
263
323
  if (!projectRoot) {
264
- console.log(chalk3.red("\u274C Not in a Cure Kode project."));
265
- console.log(chalk3.dim(' Run "kode init" first.'));
324
+ console.log(chalk4.red("\u274C Not in a Cure Kode project."));
325
+ console.log(chalk4.dim(' Run "kode init" first.'));
266
326
  return;
267
327
  }
268
328
  const config = getProjectConfig(projectRoot);
269
329
  if (!config) {
270
- console.log(chalk3.red("\u274C Could not read project configuration."));
330
+ console.log(chalk4.red("\u274C Could not read project configuration."));
271
331
  return;
272
332
  }
273
333
  const client = createApiClient(config);
274
334
  if (action === "status") {
275
- const spinner2 = ora2("Fetching production status...").start();
335
+ const spinner2 = ora3("Fetching production status...").start();
276
336
  try {
277
337
  const status = await client.getDeploymentStatus(config.siteId);
278
338
  spinner2.stop();
279
339
  console.log();
280
- console.log(chalk3.bold("Production Status"));
340
+ console.log(chalk4.bold("Production Status"));
281
341
  console.log();
282
342
  if (status.productionEnabled) {
283
- console.log(chalk3.green(" \u25CF Produksjon er aktivert"));
343
+ console.log(chalk4.green(" \u25CF Produksjon er aktivert"));
284
344
  if (status.productionDomain) {
285
- console.log(chalk3.dim(` Domain: ${status.productionDomain}`));
345
+ console.log(chalk4.dim(` Domain: ${status.productionDomain}`));
286
346
  }
287
347
  if (status.production.lastSuccessful) {
288
348
  console.log(
289
- chalk3.dim(
349
+ chalk4.dim(
290
350
  ` Siste deploy: v${status.production.lastSuccessful.version}`
291
351
  )
292
352
  );
293
353
  }
294
354
  } else {
295
- console.log(chalk3.gray(" \u25CB Produksjon er deaktivert"));
296
- console.log(chalk3.dim(" Kun staging er aktiv"));
355
+ console.log(chalk4.gray(" \u25CB Produksjon er deaktivert"));
356
+ console.log(chalk4.dim(" Kun staging er aktiv"));
297
357
  }
298
358
  console.log();
299
359
  } catch (error) {
300
360
  spinner2.fail("Failed to fetch status");
301
- console.error(chalk3.red("\nError:"), error.message || error);
361
+ console.error(chalk4.red("\nError:"), error.message || error);
302
362
  }
303
363
  return;
304
364
  }
305
- const spinner = ora2(
365
+ const spinner = ora3(
306
366
  action === "enable" ? "Aktiverer produksjon..." : "Deaktiverer produksjon..."
307
367
  ).start();
308
368
  try {
@@ -314,49 +374,49 @@ async function productionCommand(action, options) {
314
374
  spinner.stop();
315
375
  console.log();
316
376
  if (action === "enable") {
317
- console.log(chalk3.green("\u2713 Produksjon er n\xE5 aktivert"));
377
+ console.log(chalk4.green("\u2713 Produksjon er n\xE5 aktivert"));
318
378
  if (result.productionDomain) {
319
- console.log(chalk3.dim(` Domain: ${result.productionDomain}`));
379
+ console.log(chalk4.dim(` Domain: ${result.productionDomain}`));
320
380
  }
321
381
  console.log();
322
- console.log(chalk3.dim(" Neste steg:"));
323
- console.log(chalk3.dim(" 1. Deploy til staging: kode deploy"));
324
- console.log(chalk3.dim(" 2. Promoter til produksjon: kode deploy --promote"));
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"));
325
385
  } else {
326
- console.log(chalk3.yellow("\u2713 Produksjon er n\xE5 deaktivert"));
327
- console.log(chalk3.dim(" Kun staging-milj\xF8et er aktivt."));
386
+ console.log(chalk4.yellow("\u2713 Produksjon er n\xE5 deaktivert"));
387
+ console.log(chalk4.dim(" Kun staging-milj\xF8et er aktivt."));
328
388
  console.log(
329
- chalk3.dim(" Produksjonsdomenet vil f\xE5 en tom script-respons.")
389
+ chalk4.dim(" Produksjonsdomenet vil f\xE5 en tom script-respons.")
330
390
  );
331
391
  }
332
392
  console.log();
333
393
  } catch (error) {
334
394
  spinner.fail(action === "enable" ? "Kunne ikke aktivere produksjon" : "Kunne ikke deaktivere produksjon");
335
- console.error(chalk3.red("\nError:"), error.message || error);
395
+ console.error(chalk4.red("\nError:"), error.message || error);
336
396
  }
337
397
  }
338
398
 
339
399
  // src/commands/update-claude-md.ts
340
- import chalk4 from "chalk";
400
+ import chalk5 from "chalk";
341
401
  import { existsSync, readFileSync, writeFileSync } from "fs";
342
402
  import { join } from "path";
343
403
  async function updateClaudeMdCommand() {
344
404
  const projectRoot = findProjectRoot();
345
405
  if (!projectRoot) {
346
- console.log(chalk4.red("\u274C Not in a Cure Kode project."));
347
- console.log(chalk4.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.'));
348
408
  return;
349
409
  }
350
410
  const config = getProjectConfig(projectRoot);
351
411
  if (!config) {
352
- console.log(chalk4.red("\u274C Could not read project configuration."));
412
+ console.log(chalk5.red("\u274C Could not read project configuration."));
353
413
  return;
354
414
  }
355
415
  const claudeMdPath = join(projectRoot, "CLAUDE.md");
356
416
  const newKodeSection = generateClaudeMdMinimal(config.siteName, config.siteSlug);
357
417
  if (!existsSync(claudeMdPath)) {
358
418
  writeFileSync(claudeMdPath, newKodeSection);
359
- console.log(chalk4.green("\u2705 Created CLAUDE.md with Cure Kode section"));
419
+ console.log(chalk5.green("\u2705 Created CLAUDE.md with Cure Kode section"));
360
420
  return;
361
421
  }
362
422
  let content = readFileSync(claudeMdPath, "utf-8");
@@ -375,18 +435,18 @@ async function updateClaudeMdCommand() {
375
435
  content = newKodeSection + "---\n\n" + content;
376
436
  writeFileSync(claudeMdPath, content);
377
437
  if (removedCount > 1) {
378
- console.log(chalk4.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`));
379
439
  } else if (removedCount === 1) {
380
- console.log(chalk4.green("\u2705 Updated Cure Kode section in CLAUDE.md"));
440
+ console.log(chalk5.green("\u2705 Updated Cure Kode section in CLAUDE.md"));
381
441
  } else {
382
- console.log(chalk4.green("\u2705 Added Cure Kode section to CLAUDE.md"));
442
+ console.log(chalk5.green("\u2705 Added Cure Kode section to CLAUDE.md"));
383
443
  }
384
444
  console.log();
385
- console.log(chalk4.dim("The Cure Kode section now includes:"));
386
- console.log(chalk4.dim(" \u2022 What is Cure Kode (internal tool explanation)"));
387
- console.log(chalk4.dim(" \u2022 CDN URL with script tag for Webflow"));
388
- console.log(chalk4.dim(" \u2022 Workflow steps"));
389
- console.log(chalk4.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"));
390
450
  }
391
451
 
392
452
  // src/cli.ts
@@ -406,9 +466,12 @@ program.command("push").description("Upload local scripts to Cure").argument("[s
406
466
  program.command("watch").description("Watch for changes and auto-push").option("-d, --deploy", "Auto-deploy after each push").action((options) => {
407
467
  watchCommand(options);
408
468
  });
409
- 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) => {
410
470
  deployCommand(environment, options);
411
471
  });
472
+ program.command("rollback [environment]").description("Rollback to previous deployment").action((environment = "staging") => {
473
+ rollbackCommand(environment);
474
+ });
412
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) => {
413
476
  htmlCommand(url, options);
414
477
  });
@@ -432,7 +495,7 @@ program.command("update-claude-md").alias("ucm").description("Add or update Cure
432
495
  });
433
496
  program.showHelpAfterError();
434
497
  console.log();
435
- console.log(chalk5.bold(" Cure Kode CLI"));
436
- console.log(chalk5.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"));
437
500
  console.log();
438
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;
@@ -158,12 +161,42 @@ declare class KodeApiClient {
158
161
  };
159
162
  canPromote: boolean;
160
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
+ }>;
161
179
  setProductionEnabled(siteId: string, enabled: boolean, productionDomain?: string): Promise<{
162
180
  success: boolean;
163
181
  productionEnabled: boolean;
164
182
  productionDomain: string | null;
165
183
  }>;
166
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
+ }>;
167
200
  }
168
201
  /**
169
202
  * Create API client from project config
@@ -203,6 +236,11 @@ declare function pushCommand(options: {
203
236
 
204
237
  /**
205
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
206
244
  */
207
245
  declare function watchCommand(options: {
208
246
  deploy?: boolean;
@@ -214,9 +252,11 @@ declare function watchCommand(options: {
214
252
  * Workflow:
215
253
  * - `kode deploy` or `kode deploy staging` → deploys to staging
216
254
  * - `kode deploy production` or `kode deploy --promote` → promotes staging to production
255
+ * - `kode deploy --force` → force release stale lock before deploying
217
256
  */
218
257
  declare function deployCommand(environment?: 'staging' | 'production', options?: {
219
258
  promote?: boolean;
259
+ force?: boolean;
220
260
  }): Promise<void>;
221
261
 
222
262
  /**
package/dist/index.js CHANGED
@@ -27,7 +27,7 @@ import {
27
27
  updateScriptPurpose,
28
28
  watchCommand,
29
29
  writeContext
30
- } from "./chunk-HKPZVOMY.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.9.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": {