@curenorway/kode-cli 1.18.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,418 @@
1
+ // src/config.ts
2
+ import Conf from "conf";
3
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs";
4
+ import { join, dirname } from "path";
5
+ var globalConfig = new Conf({
6
+ projectName: "cure-kode",
7
+ defaults: {
8
+ apiUrl: "https://app.cure.no"
9
+ }
10
+ });
11
+ var PROJECT_CONFIG_DIR = ".cure-kode";
12
+ var PROJECT_CONFIG_FILE = "config.json";
13
+ var DEFAULT_SCRIPTS_DIR = ".cure-kode-scripts";
14
+ function findProjectRoot(startDir = process.cwd()) {
15
+ let currentDir = startDir;
16
+ while (currentDir !== dirname(currentDir)) {
17
+ const configPath = join(currentDir, PROJECT_CONFIG_DIR, PROJECT_CONFIG_FILE);
18
+ if (existsSync(configPath)) {
19
+ return currentDir;
20
+ }
21
+ currentDir = dirname(currentDir);
22
+ }
23
+ return null;
24
+ }
25
+ function getProjectConfig(projectRoot) {
26
+ const root = projectRoot || findProjectRoot();
27
+ if (!root) return null;
28
+ const configPath = join(root, PROJECT_CONFIG_DIR, PROJECT_CONFIG_FILE);
29
+ if (!existsSync(configPath)) return null;
30
+ try {
31
+ const content = readFileSync(configPath, "utf-8");
32
+ return JSON.parse(content);
33
+ } catch {
34
+ return null;
35
+ }
36
+ }
37
+ function saveProjectConfig(config, projectRoot = process.cwd()) {
38
+ const configDir = join(projectRoot, PROJECT_CONFIG_DIR);
39
+ const configPath = join(configDir, PROJECT_CONFIG_FILE);
40
+ if (!existsSync(configDir)) {
41
+ mkdirSync(configDir, { recursive: true });
42
+ }
43
+ writeFileSync(configPath, JSON.stringify(config, null, 2));
44
+ }
45
+ function getApiUrl(projectConfig) {
46
+ return projectConfig?.apiUrl || globalConfig.get("apiUrl");
47
+ }
48
+ function getApiKey(projectConfig) {
49
+ return projectConfig?.apiKey || globalConfig.get("defaultApiKey") || null;
50
+ }
51
+ function setGlobalConfig(key, value) {
52
+ globalConfig.set(key, value);
53
+ }
54
+ function getScriptsDir(projectRoot, projectConfig) {
55
+ return join(projectRoot, projectConfig?.scriptsDir || DEFAULT_SCRIPTS_DIR);
56
+ }
57
+
58
+ // src/lib/retry.ts
59
+ function isRetryableError(error) {
60
+ if (error instanceof TypeError && error.message.includes("fetch")) {
61
+ return true;
62
+ }
63
+ const err = error;
64
+ const statusCode = err.statusCode || err.status;
65
+ if (statusCode && statusCode >= 400 && statusCode < 500) {
66
+ return false;
67
+ }
68
+ if (statusCode && statusCode >= 500) {
69
+ return true;
70
+ }
71
+ if (err.code === "ECONNRESET" || err.code === "ETIMEDOUT" || err.code === "ENOTFOUND") {
72
+ return true;
73
+ }
74
+ if (error instanceof Error) {
75
+ const message = error.message.toLowerCase();
76
+ if (message.includes("network") || message.includes("timeout") || message.includes("connection") || message.includes("socket")) {
77
+ return true;
78
+ }
79
+ }
80
+ return false;
81
+ }
82
+ function calculateDelay(attempt, baseDelayMs, maxDelayMs, backoffMultiplier, jitter) {
83
+ const exponentialDelay = baseDelayMs * Math.pow(backoffMultiplier, attempt - 1);
84
+ const cappedDelay = Math.min(exponentialDelay, maxDelayMs);
85
+ if (!jitter) {
86
+ return cappedDelay;
87
+ }
88
+ const jitterRange = cappedDelay * 0.25;
89
+ const jitterOffset = (Math.random() - 0.5) * 2 * jitterRange;
90
+ return Math.max(0, Math.round(cappedDelay + jitterOffset));
91
+ }
92
+ function sleep(ms) {
93
+ return new Promise((resolve) => setTimeout(resolve, ms));
94
+ }
95
+ async function withRetry(fn, options = {}) {
96
+ const {
97
+ maxAttempts = 3,
98
+ baseDelayMs = 500,
99
+ maxDelayMs = 5e3,
100
+ backoffMultiplier = 2,
101
+ jitter = true,
102
+ isRetryable = isRetryableError,
103
+ onRetry
104
+ } = options;
105
+ let lastError;
106
+ let totalDelayMs = 0;
107
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
108
+ try {
109
+ return await fn();
110
+ } catch (error) {
111
+ lastError = error instanceof Error ? error : new Error(String(error));
112
+ if (attempt >= maxAttempts || !isRetryable(error)) {
113
+ throw lastError;
114
+ }
115
+ const delayMs = calculateDelay(
116
+ attempt,
117
+ baseDelayMs,
118
+ maxDelayMs,
119
+ backoffMultiplier,
120
+ jitter
121
+ );
122
+ totalDelayMs += delayMs;
123
+ if (onRetry) {
124
+ onRetry(attempt, error, delayMs);
125
+ }
126
+ await sleep(delayMs);
127
+ }
128
+ }
129
+ throw lastError || new Error("Retry failed");
130
+ }
131
+
132
+ // src/api.ts
133
+ var KodeApiError = class extends Error {
134
+ constructor(message, statusCode, response) {
135
+ super(message);
136
+ this.statusCode = statusCode;
137
+ this.response = response;
138
+ this.name = "KodeApiError";
139
+ }
140
+ };
141
+ var KodeApiClient = class {
142
+ baseUrl;
143
+ apiKey;
144
+ constructor(config) {
145
+ this.baseUrl = getApiUrl(config);
146
+ const apiKey = getApiKey(config);
147
+ if (!apiKey) {
148
+ throw new Error('API key is required. Run "kode init" first.');
149
+ }
150
+ this.apiKey = apiKey;
151
+ }
152
+ async request(endpoint, options = {}) {
153
+ const url = `${this.baseUrl}${endpoint}`;
154
+ return withRetry(
155
+ async () => {
156
+ const response = await fetch(url, {
157
+ ...options,
158
+ headers: {
159
+ "Content-Type": "application/json",
160
+ "X-API-Key": this.apiKey,
161
+ ...options.headers
162
+ }
163
+ });
164
+ const data = await response.json();
165
+ if (!response.ok) {
166
+ throw new KodeApiError(
167
+ data.error || `Request failed with status ${response.status}`,
168
+ response.status,
169
+ data
170
+ );
171
+ }
172
+ return data;
173
+ },
174
+ {
175
+ maxAttempts: 3,
176
+ baseDelayMs: 500,
177
+ // Custom retry check: retry on network errors and 5xx, but not 4xx
178
+ isRetryable: (error) => {
179
+ if (error instanceof KodeApiError) {
180
+ return error.statusCode >= 500;
181
+ }
182
+ return isRetryableError(error);
183
+ }
184
+ }
185
+ );
186
+ }
187
+ // Sites
188
+ async getSite(siteId) {
189
+ return this.request(`/api/cdn/sites/${siteId}`);
190
+ }
191
+ async listSites() {
192
+ return this.request("/api/cdn/sites");
193
+ }
194
+ // Scripts
195
+ async listScripts(siteId) {
196
+ return this.request(`/api/cdn/sites/${siteId}/scripts`);
197
+ }
198
+ async getScript(scriptId) {
199
+ return this.request(`/api/cdn/scripts/${scriptId}`);
200
+ }
201
+ async createScript(siteId, data) {
202
+ return this.request(`/api/cdn/sites/${siteId}/scripts`, {
203
+ method: "POST",
204
+ body: JSON.stringify(data)
205
+ });
206
+ }
207
+ async updateScript(scriptId, data) {
208
+ return this.request(`/api/cdn/scripts/${scriptId}`, {
209
+ method: "PUT",
210
+ body: JSON.stringify(data)
211
+ });
212
+ }
213
+ async deleteScript(scriptId) {
214
+ await this.request(`/api/cdn/scripts/${scriptId}`, {
215
+ method: "DELETE"
216
+ });
217
+ }
218
+ // Upload (alternative endpoint that works with API keys)
219
+ async uploadScript(data) {
220
+ return this.request("/api/cdn/upload", {
221
+ method: "POST",
222
+ body: JSON.stringify({
223
+ ...data,
224
+ apiKey: this.apiKey
225
+ })
226
+ });
227
+ }
228
+ // Pages
229
+ async listPages(siteId) {
230
+ return this.request(`/api/cdn/sites/${siteId}/pages`);
231
+ }
232
+ async getPage(pageId) {
233
+ return this.request(`/api/cdn/pages/${pageId}`);
234
+ }
235
+ async createPage(siteId, data) {
236
+ return this.request(`/api/cdn/sites/${siteId}/pages`, {
237
+ method: "POST",
238
+ body: JSON.stringify(data)
239
+ });
240
+ }
241
+ async updatePage(pageId, data) {
242
+ return this.request(`/api/cdn/pages/${pageId}`, {
243
+ method: "PATCH",
244
+ body: JSON.stringify(data)
245
+ });
246
+ }
247
+ async deletePage(pageId) {
248
+ return this.request(`/api/cdn/pages/${pageId}`, {
249
+ method: "DELETE"
250
+ });
251
+ }
252
+ async assignScriptToPage(pageId, scriptId, loadOrderOverride) {
253
+ return this.request(`/api/cdn/pages/${pageId}/scripts`, {
254
+ method: "POST",
255
+ body: JSON.stringify({ scriptId, loadOrderOverride })
256
+ });
257
+ }
258
+ async removeScriptFromPage(pageId, scriptId) {
259
+ return this.request(`/api/cdn/pages/${pageId}/scripts/${scriptId}`, {
260
+ method: "DELETE"
261
+ });
262
+ }
263
+ async getPageScripts(pageId) {
264
+ return this.request(`/api/cdn/pages/${pageId}/scripts`);
265
+ }
266
+ // Deployments
267
+ async deploy(siteId, environment = "staging") {
268
+ return this.request("/api/cdn/deploy", {
269
+ method: "POST",
270
+ body: JSON.stringify({
271
+ siteId,
272
+ environment,
273
+ apiKey: this.apiKey
274
+ })
275
+ });
276
+ }
277
+ async promoteToProduction(siteId, stagingDeploymentId) {
278
+ return this.request("/api/cdn/deploy/promote", {
279
+ method: "POST",
280
+ body: JSON.stringify({
281
+ siteId,
282
+ stagingDeploymentId,
283
+ apiKey: this.apiKey
284
+ })
285
+ });
286
+ }
287
+ async getDeploymentStatus(siteId) {
288
+ return this.request(`/api/cdn/sites/${siteId}/deployments/status`);
289
+ }
290
+ async rollback(siteId, environment = "staging") {
291
+ return this.request("/api/cdn/deploy/rollback", {
292
+ method: "POST",
293
+ body: JSON.stringify({
294
+ siteId,
295
+ environment
296
+ })
297
+ });
298
+ }
299
+ // Production enabled toggle (v2.3)
300
+ async setProductionEnabled(siteId, enabled, productionDomain) {
301
+ return this.request(`/api/cdn/sites/${siteId}/production`, {
302
+ method: "POST",
303
+ body: JSON.stringify({
304
+ enabled,
305
+ productionDomain
306
+ })
307
+ });
308
+ }
309
+ // HTML Fetch
310
+ async fetchHtml(url) {
311
+ return this.request("/api/cdn/fetch-html", {
312
+ method: "POST",
313
+ body: JSON.stringify({ url })
314
+ });
315
+ }
316
+ // Lock management
317
+ async getLockStatus(siteId) {
318
+ return this.request(`/api/cdn/deploy/lock?siteId=${siteId}`);
319
+ }
320
+ async forceReleaseLock(siteId) {
321
+ return this.request("/api/cdn/deploy/lock", {
322
+ method: "DELETE",
323
+ body: JSON.stringify({ siteId })
324
+ });
325
+ }
326
+ // Library (global snippets)
327
+ async listLibrary(search) {
328
+ const params = new URLSearchParams();
329
+ if (search) params.set("search", search);
330
+ const qs = params.toString();
331
+ return this.request(`/api/cdn/library${qs ? `?${qs}` : ""}`);
332
+ }
333
+ async getLibrarySnippet(slugOrId) {
334
+ return this.request(`/api/cdn/library/${encodeURIComponent(slugOrId)}`);
335
+ }
336
+ async createLibrarySnippet(data) {
337
+ return this.request("/api/cdn/library", {
338
+ method: "POST",
339
+ body: JSON.stringify(data)
340
+ });
341
+ }
342
+ async recordLibraryUsage(snippetId) {
343
+ await this.request(`/api/cdn/library/${snippetId}`);
344
+ }
345
+ async updateLibrarySnippet(slugOrId, data) {
346
+ return this.request(`/api/cdn/library/${encodeURIComponent(slugOrId)}`, {
347
+ method: "PATCH",
348
+ body: JSON.stringify(data)
349
+ });
350
+ }
351
+ async listLibraryFolders() {
352
+ return this.request("/api/cdn/library/folders");
353
+ }
354
+ async createLibraryFolder(data) {
355
+ return this.request("/api/cdn/library/folders", {
356
+ method: "POST",
357
+ body: JSON.stringify(data)
358
+ });
359
+ }
360
+ async listLibraryTrash() {
361
+ return this.request("/api/cdn/library/trash");
362
+ }
363
+ async restoreLibrarySnippet(snippetId) {
364
+ await this.request("/api/cdn/library/trash", {
365
+ method: "POST",
366
+ body: JSON.stringify({ action: "restore", snippetId })
367
+ });
368
+ }
369
+ async permanentDeleteLibrarySnippet(snippetId) {
370
+ await this.request(`/api/cdn/library/trash?snippetId=${encodeURIComponent(snippetId)}`, {
371
+ method: "DELETE"
372
+ });
373
+ }
374
+ // Webflow Custom Code injection
375
+ async getWebflowCustomCodeStatus(siteId) {
376
+ return this.request(`/api/cdn/sites/${siteId}/webflow/custom-code`);
377
+ }
378
+ async injectWebflowCustomCode(siteId, location = "header") {
379
+ return this.request(`/api/cdn/sites/${siteId}/webflow/custom-code`, {
380
+ method: "POST",
381
+ body: JSON.stringify({ location })
382
+ });
383
+ }
384
+ async removeWebflowCustomCode(siteId) {
385
+ return this.request(`/api/cdn/sites/${siteId}/webflow/custom-code`, {
386
+ method: "DELETE"
387
+ });
388
+ }
389
+ async getCmsTypes(siteId) {
390
+ return this.request(`/api/cdn/sites/${siteId}/cms-types`);
391
+ }
392
+ async getProjectFiles(siteId) {
393
+ return this.request(`/api/cdn/sites/${siteId}/files`);
394
+ }
395
+ async updateProjectFiles(siteId, files) {
396
+ return this.request(`/api/cdn/sites/${siteId}/files`, {
397
+ method: "PUT",
398
+ body: JSON.stringify({ files })
399
+ });
400
+ }
401
+ };
402
+ function createApiClient(config) {
403
+ return new KodeApiClient(config);
404
+ }
405
+
406
+ export {
407
+ DEFAULT_SCRIPTS_DIR,
408
+ findProjectRoot,
409
+ getProjectConfig,
410
+ saveProjectConfig,
411
+ getApiUrl,
412
+ getApiKey,
413
+ setGlobalConfig,
414
+ getScriptsDir,
415
+ KodeApiError,
416
+ KodeApiClient,
417
+ createApiClient
418
+ };