@curenorway/kode-cli 1.9.0 → 1.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +154 -67
- package/dist/{chunk-HKPZVOMY.js → chunk-CUZJE4JZ.js} +326 -40
- package/dist/cli.js +171 -108
- package/dist/index.d.ts +40 -0
- package/dist/index.js +1 -1
- package/package.json +2 -2
|
@@ -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: "
|
|
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) =>
|
|
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
|
|
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
|
|
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
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
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
|
|
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 (
|
|
2050
|
-
|
|
2215
|
+
if (!isRetry) {
|
|
2216
|
+
if (pendingChanges.has(filePath)) {
|
|
2217
|
+
clearTimeout(pendingChanges.get(filePath));
|
|
2218
|
+
}
|
|
2051
2219
|
}
|
|
2052
|
-
const
|
|
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
|
-
|
|
2070
|
-
|
|
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
|
-
|
|
2094
|
-
|
|
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
|
-
|
|
2111
|
-
|
|
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
|
|
2391
|
+
const client2 = createApiClient(config);
|
|
2159
2392
|
const spinner2 = ora4("Sjekker produksjonsstatus...").start();
|
|
2160
2393
|
try {
|
|
2161
|
-
const status = await
|
|
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
|
|
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();
|