@codemieai/cdk 0.1.270
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 +2856 -0
- package/dist/cli/index.js +3482 -0
- package/dist/index.d.ts +584 -0
- package/dist/index.js +3247 -0
- package/package.json +63 -0
|
@@ -0,0 +1,3482 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import * as path6 from 'path';
|
|
3
|
+
import path6__default from 'path';
|
|
4
|
+
import { program } from '@commander-js/extra-typings';
|
|
5
|
+
import * as fs2 from 'fs';
|
|
6
|
+
import { z } from 'zod';
|
|
7
|
+
import 'dotenv/config';
|
|
8
|
+
import * as crypto from 'crypto';
|
|
9
|
+
import * as yaml3 from 'yaml';
|
|
10
|
+
import { CodeMieClient, DataSourceType } from 'codemie-sdk';
|
|
11
|
+
import pLimit from 'p-limit';
|
|
12
|
+
|
|
13
|
+
// package.json
|
|
14
|
+
var package_default = {
|
|
15
|
+
version: "0.1.270"};
|
|
16
|
+
var appConfigSchema = z.object({
|
|
17
|
+
rootDir: z.string(),
|
|
18
|
+
codemieConfig: z.string(),
|
|
19
|
+
codemieState: z.string(),
|
|
20
|
+
backupsDirectory: z.string()
|
|
21
|
+
});
|
|
22
|
+
var externalConfigSchema = appConfigSchema.partial();
|
|
23
|
+
|
|
24
|
+
// src/appConfig/defaultConfig.ts
|
|
25
|
+
var defaultConfig = {
|
|
26
|
+
rootDir: "./",
|
|
27
|
+
codemieConfig: "codemie.yaml",
|
|
28
|
+
codemieState: ".codemie/state.json",
|
|
29
|
+
backupsDirectory: "backups"
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
// src/appConfig/configLoader.ts
|
|
33
|
+
function loadAppConfig(userConfigPath) {
|
|
34
|
+
if (!userConfigPath) {
|
|
35
|
+
return defaultConfig;
|
|
36
|
+
}
|
|
37
|
+
const userConfig = loadUserConfig(userConfigPath);
|
|
38
|
+
const sanitizedUserConfig = removeEmptyFields(userConfig);
|
|
39
|
+
const merged = { ...defaultConfig, ...sanitizedUserConfig };
|
|
40
|
+
return appConfigSchema.parse(merged);
|
|
41
|
+
}
|
|
42
|
+
function loadUserConfig(configPath) {
|
|
43
|
+
const resolvedPath = path6.resolve(process.cwd(), configPath);
|
|
44
|
+
if (!fs2.existsSync(resolvedPath)) {
|
|
45
|
+
throw new Error(`Config file not found: ${resolvedPath}`);
|
|
46
|
+
}
|
|
47
|
+
let raw;
|
|
48
|
+
try {
|
|
49
|
+
raw = fs2.readFileSync(resolvedPath, "utf8");
|
|
50
|
+
} catch (error) {
|
|
51
|
+
throw new Error(`Failed to read config file: ${resolvedPath}`, { cause: error });
|
|
52
|
+
}
|
|
53
|
+
let json;
|
|
54
|
+
try {
|
|
55
|
+
json = JSON.parse(raw);
|
|
56
|
+
} catch (error) {
|
|
57
|
+
throw new Error(`Malformed JSON in config file: ${resolvedPath}`, { cause: error });
|
|
58
|
+
}
|
|
59
|
+
try {
|
|
60
|
+
return externalConfigSchema.parse(json);
|
|
61
|
+
} catch (error) {
|
|
62
|
+
throw new Error(`Invalid config structure: ${error instanceof Error ? error.message : String(error)}`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
function removeEmptyFields(userConfig) {
|
|
66
|
+
return Object.fromEntries(
|
|
67
|
+
Object.entries(userConfig).filter(([_, value]) => {
|
|
68
|
+
if (value === null || value === void 0) {
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
return value.trim() !== "";
|
|
72
|
+
})
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// src/lib/constants.ts
|
|
77
|
+
var PAGINATION = {
|
|
78
|
+
DEFAULT_PAGE_SIZE: 100};
|
|
79
|
+
var TIMEOUTS_MS = {
|
|
80
|
+
ASSISTANT_FETCH: 3e4,
|
|
81
|
+
DATASOURCE_FETCH: 3e4,
|
|
82
|
+
WORKFLOW_FETCH: 3e4,
|
|
83
|
+
INTEGRATION_FETCH: 3e4
|
|
84
|
+
};
|
|
85
|
+
var RATE_LIMITING = {
|
|
86
|
+
MAX_CONCURRENT_REQUESTS: 5,
|
|
87
|
+
RETRY_ATTEMPTS: 3,
|
|
88
|
+
RETRY_DELAY_MS: 1e3
|
|
89
|
+
};
|
|
90
|
+
var BACKUP = {
|
|
91
|
+
TEMP_DIR_PREFIX: ".temp-",
|
|
92
|
+
TRANSACTION_SAVE_TIMEOUT_MS: 5e3
|
|
93
|
+
};
|
|
94
|
+
var BYTES_IN_GB = 1024 ** 3;
|
|
95
|
+
function sanitizeFileName(name, maxLength = 255) {
|
|
96
|
+
const nameWithHyphens = name.replaceAll(/[/\\]/g, "-");
|
|
97
|
+
const sanitized = nameWithHyphens.toLowerCase().replaceAll(/[^a-z0-9]+/g, "-").replaceAll(/^-+|-+$/g, "").slice(0, maxLength);
|
|
98
|
+
if (!sanitized) {
|
|
99
|
+
throw new Error(`Sanitized filename is empty for input: "${name}"`);
|
|
100
|
+
}
|
|
101
|
+
return sanitized;
|
|
102
|
+
}
|
|
103
|
+
function validateBackupDirectory(backupDir, minSpaceGB = 1) {
|
|
104
|
+
try {
|
|
105
|
+
const parentDir = path6.dirname(backupDir);
|
|
106
|
+
if (!fs2.existsSync(parentDir)) {
|
|
107
|
+
fs2.mkdirSync(parentDir, { recursive: true });
|
|
108
|
+
}
|
|
109
|
+
const testFile = path6.join(parentDir, ".write-test");
|
|
110
|
+
fs2.writeFileSync(testFile, "test");
|
|
111
|
+
fs2.unlinkSync(testFile);
|
|
112
|
+
try {
|
|
113
|
+
const stats = fs2.statfsSync(parentDir);
|
|
114
|
+
const availableGB = stats.bavail * stats.bsize / BYTES_IN_GB;
|
|
115
|
+
if (availableGB < minSpaceGB) {
|
|
116
|
+
throw new Error(`Insufficient disk space: ${availableGB.toFixed(2)}GB available, need ${minSpaceGB}GB`);
|
|
117
|
+
}
|
|
118
|
+
} catch (error) {
|
|
119
|
+
if (error.code !== "ERR_METHOD_NOT_SUPPORTED") {
|
|
120
|
+
throw error;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
} catch (error) {
|
|
124
|
+
throw new Error(`Cannot write to backup directory: ${error instanceof Error ? error.message : String(error)}`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
function moveAtomically(tempPath, finalPath) {
|
|
128
|
+
try {
|
|
129
|
+
fs2.renameSync(tempPath, finalPath);
|
|
130
|
+
} catch (error) {
|
|
131
|
+
const err = error;
|
|
132
|
+
if (err.code === "EEXIST") {
|
|
133
|
+
throw new Error(`Destination already exists: ${finalPath}`);
|
|
134
|
+
}
|
|
135
|
+
throw error;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
function cleanupDirectory(dirPath) {
|
|
139
|
+
if (fs2.existsSync(dirPath)) {
|
|
140
|
+
fs2.rmSync(dirPath, { recursive: true, force: true });
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
function ensureDirectoryExists(filePath) {
|
|
144
|
+
const dir = path6.dirname(filePath);
|
|
145
|
+
if (!fs2.existsSync(dir)) {
|
|
146
|
+
fs2.mkdirSync(dir, { recursive: true });
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// src/lib/logger.ts
|
|
151
|
+
var Logger = class _Logger {
|
|
152
|
+
static instance;
|
|
153
|
+
level = 1 /* INFO */;
|
|
154
|
+
constructor() {
|
|
155
|
+
}
|
|
156
|
+
static getInstance() {
|
|
157
|
+
if (!_Logger.instance) {
|
|
158
|
+
_Logger.instance = new _Logger();
|
|
159
|
+
}
|
|
160
|
+
return _Logger.instance;
|
|
161
|
+
}
|
|
162
|
+
setLevel(level) {
|
|
163
|
+
this.level = level;
|
|
164
|
+
}
|
|
165
|
+
debug(message, ...args) {
|
|
166
|
+
if (this.level <= 0 /* DEBUG */) {
|
|
167
|
+
console.debug(message, ...args);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
info(message, ...args) {
|
|
171
|
+
if (this.level <= 1 /* INFO */) {
|
|
172
|
+
console.log(message, ...args);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
warn(message, ...args) {
|
|
176
|
+
if (this.level <= 2 /* WARN */) {
|
|
177
|
+
console.warn(message, ...args);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
error(message, ...args) {
|
|
181
|
+
if (this.level <= 3 /* ERROR */) {
|
|
182
|
+
console.error(message, ...args);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
var logger = Logger.getInstance();
|
|
187
|
+
|
|
188
|
+
// src/lib/backupTransaction.ts
|
|
189
|
+
function busyWaitDelay(ms) {
|
|
190
|
+
}
|
|
191
|
+
var BackupTransaction = class {
|
|
192
|
+
data;
|
|
193
|
+
transactionPath;
|
|
194
|
+
isDirty = false;
|
|
195
|
+
saveTimer;
|
|
196
|
+
constructor(backupDir, transactionId) {
|
|
197
|
+
this.transactionPath = path6.join(backupDir, "transaction.json");
|
|
198
|
+
try {
|
|
199
|
+
const content = fs2.readFileSync(this.transactionPath, "utf8");
|
|
200
|
+
this.data = JSON.parse(content);
|
|
201
|
+
logger.info(`\u{1F4C2} Resuming backup transaction ${this.data.id}`);
|
|
202
|
+
} catch (error) {
|
|
203
|
+
const err = error;
|
|
204
|
+
if (err.code === "ENOENT") {
|
|
205
|
+
this.data = this.createNewTransaction(backupDir, transactionId);
|
|
206
|
+
} else {
|
|
207
|
+
throw error;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
createNewTransaction(backupDir, transactionId) {
|
|
212
|
+
const newData = {
|
|
213
|
+
id: transactionId || this.generateTransactionId(),
|
|
214
|
+
status: "in-progress",
|
|
215
|
+
startTime: (/* @__PURE__ */ new Date()).toISOString(),
|
|
216
|
+
backupDir,
|
|
217
|
+
resources: {
|
|
218
|
+
assistants: { total: 0, completed: [], failed: [] },
|
|
219
|
+
datasources: { total: 0, completed: [], failed: [] },
|
|
220
|
+
workflows: { total: 0, completed: [], failed: [] },
|
|
221
|
+
integrations: { total: 0, completed: [], failed: [] }
|
|
222
|
+
}
|
|
223
|
+
};
|
|
224
|
+
ensureDirectoryExists(this.transactionPath);
|
|
225
|
+
this.writeTransactionExclusively(newData);
|
|
226
|
+
return newData;
|
|
227
|
+
}
|
|
228
|
+
writeTransactionExclusively(data) {
|
|
229
|
+
try {
|
|
230
|
+
const fd = fs2.openSync(this.transactionPath, "wx");
|
|
231
|
+
fs2.writeSync(fd, JSON.stringify(data, null, 2));
|
|
232
|
+
fs2.closeSync(fd);
|
|
233
|
+
} catch (writeError) {
|
|
234
|
+
const writeErr = writeError;
|
|
235
|
+
if (writeErr.code === "EEXIST") {
|
|
236
|
+
this.readWithRetry();
|
|
237
|
+
} else {
|
|
238
|
+
throw writeError;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
readWithRetry() {
|
|
243
|
+
let retries = 3;
|
|
244
|
+
let lastReadError;
|
|
245
|
+
while (retries > 0) {
|
|
246
|
+
try {
|
|
247
|
+
const delayMs = 100 * (4 - retries);
|
|
248
|
+
if (delayMs > 0) {
|
|
249
|
+
busyWaitDelay(delayMs);
|
|
250
|
+
}
|
|
251
|
+
const content = fs2.readFileSync(this.transactionPath, "utf8");
|
|
252
|
+
this.data = JSON.parse(content);
|
|
253
|
+
logger.info(`\u{1F4C2} Resuming backup transaction ${this.data.id} (created by concurrent process)`);
|
|
254
|
+
break;
|
|
255
|
+
} catch (readError) {
|
|
256
|
+
lastReadError = readError instanceof Error ? readError : new Error(String(readError));
|
|
257
|
+
retries--;
|
|
258
|
+
if (retries === 0) {
|
|
259
|
+
throw new Error(
|
|
260
|
+
`Failed to read transaction file after retries: ${lastReadError.message}. File may be corrupted. Delete ${this.transactionPath} and retry.`
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
generateTransactionId() {
|
|
267
|
+
return crypto.randomBytes(8).toString("hex");
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* Save transaction state to disk (checkpoint) with timeout protection
|
|
271
|
+
*/
|
|
272
|
+
async save() {
|
|
273
|
+
ensureDirectoryExists(this.transactionPath);
|
|
274
|
+
const savePromise = fs2.promises.writeFile(this.transactionPath, JSON.stringify(this.data, null, 2), "utf8");
|
|
275
|
+
let timeoutId;
|
|
276
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
277
|
+
timeoutId = setTimeout(
|
|
278
|
+
() => reject(new Error(`Transaction save timeout after ${BACKUP.TRANSACTION_SAVE_TIMEOUT_MS}ms`)),
|
|
279
|
+
BACKUP.TRANSACTION_SAVE_TIMEOUT_MS
|
|
280
|
+
);
|
|
281
|
+
timeoutId.unref();
|
|
282
|
+
});
|
|
283
|
+
try {
|
|
284
|
+
await Promise.race([savePromise, timeoutPromise]);
|
|
285
|
+
this.isDirty = false;
|
|
286
|
+
} catch (error) {
|
|
287
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
288
|
+
throw new Error(`Failed to save transaction file: ${err.message}`);
|
|
289
|
+
} finally {
|
|
290
|
+
if (timeoutId !== void 0) {
|
|
291
|
+
clearTimeout(timeoutId);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* Internal synchronous save for batching
|
|
297
|
+
*/
|
|
298
|
+
saveSyncInternal() {
|
|
299
|
+
ensureDirectoryExists(this.transactionPath);
|
|
300
|
+
fs2.writeFileSync(this.transactionPath, JSON.stringify(this.data, null, 2), "utf8");
|
|
301
|
+
this.isDirty = false;
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* Schedule batched save (debounced to avoid excessive disk writes)
|
|
305
|
+
*/
|
|
306
|
+
scheduleSave() {
|
|
307
|
+
if (this.saveTimer) {
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
this.saveTimer = setTimeout(() => {
|
|
311
|
+
if (this.isDirty) {
|
|
312
|
+
this.saveSyncInternal();
|
|
313
|
+
}
|
|
314
|
+
this.saveTimer = void 0;
|
|
315
|
+
}, 1e3);
|
|
316
|
+
this.saveTimer.unref();
|
|
317
|
+
}
|
|
318
|
+
/**
|
|
319
|
+
* Internal method to finalize transaction with a specific status
|
|
320
|
+
*/
|
|
321
|
+
end(status) {
|
|
322
|
+
this.data.status = status;
|
|
323
|
+
this.data.endTime = (/* @__PURE__ */ new Date()).toISOString();
|
|
324
|
+
this.flush();
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* Force immediate save (flush pending changes)
|
|
328
|
+
*/
|
|
329
|
+
flush() {
|
|
330
|
+
if (this.saveTimer) {
|
|
331
|
+
clearTimeout(this.saveTimer);
|
|
332
|
+
this.saveTimer = void 0;
|
|
333
|
+
}
|
|
334
|
+
if (this.isDirty) {
|
|
335
|
+
this.saveSyncInternal();
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
/**
|
|
339
|
+
* Set total count for a resource type
|
|
340
|
+
*/
|
|
341
|
+
setTotal(resourceType, total) {
|
|
342
|
+
this.data.resources[resourceType].total = total;
|
|
343
|
+
this.isDirty = true;
|
|
344
|
+
this.scheduleSave();
|
|
345
|
+
}
|
|
346
|
+
/**
|
|
347
|
+
* Mark resource as completed
|
|
348
|
+
*/
|
|
349
|
+
markCompleted(resourceType, resourceId) {
|
|
350
|
+
if (!this.data.resources[resourceType].completed.includes(resourceId)) {
|
|
351
|
+
this.data.resources[resourceType].completed.push(resourceId);
|
|
352
|
+
this.isDirty = true;
|
|
353
|
+
this.scheduleSave();
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
/**
|
|
357
|
+
* Mark resource as failed
|
|
358
|
+
*/
|
|
359
|
+
markFailed(resourceType, resourceId, error) {
|
|
360
|
+
const failedEntry = { id: resourceId, error };
|
|
361
|
+
const failedList = this.data.resources[resourceType].failed;
|
|
362
|
+
if (!failedList.some((f) => f.id === resourceId)) {
|
|
363
|
+
failedList.push(failedEntry);
|
|
364
|
+
this.isDirty = true;
|
|
365
|
+
this.scheduleSave();
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
/**
|
|
369
|
+
* Check if resource was already backed up
|
|
370
|
+
*/
|
|
371
|
+
isCompleted(resourceType, resourceId) {
|
|
372
|
+
return this.data.resources[resourceType].completed.includes(resourceId);
|
|
373
|
+
}
|
|
374
|
+
/**
|
|
375
|
+
* Mark transaction as completed
|
|
376
|
+
*/
|
|
377
|
+
complete() {
|
|
378
|
+
this.end("completed");
|
|
379
|
+
}
|
|
380
|
+
/**
|
|
381
|
+
* Mark transaction as failed
|
|
382
|
+
*/
|
|
383
|
+
fail() {
|
|
384
|
+
this.end("failed");
|
|
385
|
+
}
|
|
386
|
+
/**
|
|
387
|
+
* Get transaction data (returns deep copy to prevent external modifications)
|
|
388
|
+
*/
|
|
389
|
+
getData() {
|
|
390
|
+
return structuredClone(this.data);
|
|
391
|
+
}
|
|
392
|
+
/**
|
|
393
|
+
* Get summary of backup progress
|
|
394
|
+
*/
|
|
395
|
+
getSummary() {
|
|
396
|
+
const { resources } = this.data;
|
|
397
|
+
const lines = [];
|
|
398
|
+
for (const [type, data] of Object.entries(resources)) {
|
|
399
|
+
const completed = data.completed.length;
|
|
400
|
+
const failed = data.failed.length;
|
|
401
|
+
const total = data.total;
|
|
402
|
+
const percent = total > 0 ? Math.round(completed / total * 100) : 0;
|
|
403
|
+
lines.push(` ${type}: ${completed}/${total} (${percent}%) ${failed > 0 ? `[${failed} failed]` : ""}`);
|
|
404
|
+
}
|
|
405
|
+
return lines.join("\n");
|
|
406
|
+
}
|
|
407
|
+
/**
|
|
408
|
+
* Clean up transaction file after successful completion
|
|
409
|
+
*/
|
|
410
|
+
cleanup() {
|
|
411
|
+
if (fs2.existsSync(this.transactionPath)) {
|
|
412
|
+
fs2.unlinkSync(this.transactionPath);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
};
|
|
416
|
+
var DEFAULT_LLM_MODEL = "gpt-4";
|
|
417
|
+
function normalizeIntegrationSettings(settings) {
|
|
418
|
+
if (!settings || typeof settings !== "object") {
|
|
419
|
+
return settings;
|
|
420
|
+
}
|
|
421
|
+
if ("$ref" in settings && typeof settings.$ref === "string") {
|
|
422
|
+
return { $ref: settings.$ref };
|
|
423
|
+
}
|
|
424
|
+
if ("alias" in settings && typeof settings.alias === "string") {
|
|
425
|
+
return { $ref: `imported.integrations.${settings.alias}` };
|
|
426
|
+
}
|
|
427
|
+
return settings;
|
|
428
|
+
}
|
|
429
|
+
function normalizeTool(tool) {
|
|
430
|
+
return {
|
|
431
|
+
name: tool.name,
|
|
432
|
+
label: tool.label,
|
|
433
|
+
settings_config: tool.settings_config,
|
|
434
|
+
user_description: tool.user_description,
|
|
435
|
+
settings: normalizeIntegrationSettings(tool.settings)
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
function normalizeToolkits(toolkits) {
|
|
439
|
+
if (!toolkits) {
|
|
440
|
+
return [];
|
|
441
|
+
}
|
|
442
|
+
return toolkits.map(({ toolkit, tools, label, settings_config, is_external, settings }) => ({
|
|
443
|
+
toolkit,
|
|
444
|
+
label,
|
|
445
|
+
settings_config,
|
|
446
|
+
is_external,
|
|
447
|
+
tools: tools?.map((tool) => normalizeTool(tool)),
|
|
448
|
+
settings: normalizeIntegrationSettings(settings)
|
|
449
|
+
}));
|
|
450
|
+
}
|
|
451
|
+
function normalizeMcpServers(mcpServers) {
|
|
452
|
+
if (!mcpServers) {
|
|
453
|
+
return [];
|
|
454
|
+
}
|
|
455
|
+
return mcpServers.map((mcp) => ({
|
|
456
|
+
...mcp,
|
|
457
|
+
settings: normalizeIntegrationSettings(mcp.settings),
|
|
458
|
+
mcp_connect_auth_token: normalizeIntegrationSettings(mcp.mcp_connect_auth_token)
|
|
459
|
+
}));
|
|
460
|
+
}
|
|
461
|
+
function calculateChecksum(content) {
|
|
462
|
+
if (typeof content !== "string") {
|
|
463
|
+
throw new TypeError(`calculateChecksum expects string, got ${typeof content}`);
|
|
464
|
+
}
|
|
465
|
+
if (content.length === 0) {
|
|
466
|
+
logger.warn("\u26A0\uFE0F Calculating checksum of empty string");
|
|
467
|
+
}
|
|
468
|
+
return crypto.createHash("sha256").update(content, "utf8").digest("hex");
|
|
469
|
+
}
|
|
470
|
+
function normalizeAssistantConfig(assistant, buildConfig = null) {
|
|
471
|
+
return {
|
|
472
|
+
description: assistant.description || "",
|
|
473
|
+
model: assistant.model || DEFAULT_LLM_MODEL,
|
|
474
|
+
temperature: assistant.temperature,
|
|
475
|
+
top_p: assistant.top_p,
|
|
476
|
+
shared: assistant.shared,
|
|
477
|
+
is_react: assistant.is_react,
|
|
478
|
+
is_global: assistant.is_global,
|
|
479
|
+
icon_url: assistant.icon_url,
|
|
480
|
+
toolkits: normalizeToolkits(assistant.toolkits),
|
|
481
|
+
context: assistant.context || [],
|
|
482
|
+
mcp_servers: normalizeMcpServers(assistant.mcp_servers),
|
|
483
|
+
assistant_ids: assistant.assistant_ids || [],
|
|
484
|
+
sub_assistants: assistant.sub_assistants || [],
|
|
485
|
+
conversation_starters: assistant.conversation_starters || [],
|
|
486
|
+
buildConfig
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
function calculateAssistantConfigChecksum(assistant, buildConfig = null) {
|
|
490
|
+
const normalized = normalizeAssistantConfig(assistant, buildConfig);
|
|
491
|
+
return calculateChecksum(JSON.stringify(normalized));
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// src/lib/codemieConfigChecksums.ts
|
|
495
|
+
function createExcludeSet(keys) {
|
|
496
|
+
return new Set(keys);
|
|
497
|
+
}
|
|
498
|
+
var DATASOURCE_EXCLUDED_FIELDS = createExcludeSet(["force_reindex"]);
|
|
499
|
+
var WORKFLOW_EXCLUDED_FIELDS = createExcludeSet(["definition"]);
|
|
500
|
+
function buildChecksumObject(src, excluded) {
|
|
501
|
+
const out = {};
|
|
502
|
+
const keys = Object.keys(src).sort();
|
|
503
|
+
for (const key of keys) {
|
|
504
|
+
if (excluded.has(key)) {
|
|
505
|
+
continue;
|
|
506
|
+
}
|
|
507
|
+
const value = src[key];
|
|
508
|
+
if (value !== void 0) {
|
|
509
|
+
out[key] = value;
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
return out;
|
|
513
|
+
}
|
|
514
|
+
function calculateDatasourceConfigChecksum(datasource) {
|
|
515
|
+
const filtered = buildChecksumObject(datasource, DATASOURCE_EXCLUDED_FIELDS);
|
|
516
|
+
return calculateChecksum(JSON.stringify(filtered));
|
|
517
|
+
}
|
|
518
|
+
function calculateWorkflowConfigChecksum(workflow) {
|
|
519
|
+
const filtered = buildChecksumObject(workflow, WORKFLOW_EXCLUDED_FIELDS);
|
|
520
|
+
return calculateChecksum(JSON.stringify(filtered));
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
// src/lib/typeGuards.ts
|
|
524
|
+
function hasName(obj) {
|
|
525
|
+
return typeof obj === "object" && obj !== null && "name" in obj;
|
|
526
|
+
}
|
|
527
|
+
function hasSettingId(obj) {
|
|
528
|
+
return typeof obj === "object" && obj !== null && "setting_id" in obj;
|
|
529
|
+
}
|
|
530
|
+
function isResolvedIntegration(obj) {
|
|
531
|
+
return typeof obj === "object" && obj !== null && "id" in obj && typeof obj.id === "string";
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// src/lib/converters.ts
|
|
535
|
+
function validateMcpCommandArgs(s, hasTopLevelCommand) {
|
|
536
|
+
if (hasTopLevelCommand) {
|
|
537
|
+
return true;
|
|
538
|
+
}
|
|
539
|
+
const config = s.config;
|
|
540
|
+
const configArgs = config.args;
|
|
541
|
+
if (!Array.isArray(configArgs)) {
|
|
542
|
+
return false;
|
|
543
|
+
}
|
|
544
|
+
return configArgs.every((arg) => typeof arg === "string");
|
|
545
|
+
}
|
|
546
|
+
function isValidMcpServer(server) {
|
|
547
|
+
if (!server || typeof server !== "object") {
|
|
548
|
+
return false;
|
|
549
|
+
}
|
|
550
|
+
const s = server;
|
|
551
|
+
if (typeof s.name !== "string" || s.name.length === 0) {
|
|
552
|
+
return false;
|
|
553
|
+
}
|
|
554
|
+
const hasTopLevelCommand = typeof s.command === "string" && s.command.length > 0;
|
|
555
|
+
const hasConfigCommand = s.config && typeof s.config === "object" && typeof s.config.command === "string" && s.config.command.length > 0;
|
|
556
|
+
const hasCommand = hasTopLevelCommand || hasConfigCommand;
|
|
557
|
+
const hasTopLevelUrl = typeof s.mcp_connect_url === "string" && s.mcp_connect_url.length > 0;
|
|
558
|
+
const hasConfigUrl = s.config && typeof s.config === "object" && typeof s.config.url === "string" && s.config.url.length > 0;
|
|
559
|
+
const hasUrl = hasTopLevelUrl || hasConfigUrl;
|
|
560
|
+
const hasSettings = s.settings != null;
|
|
561
|
+
if (hasCommand && hasUrl) {
|
|
562
|
+
return false;
|
|
563
|
+
}
|
|
564
|
+
if (!hasCommand && !hasUrl && !hasSettings) {
|
|
565
|
+
return false;
|
|
566
|
+
}
|
|
567
|
+
if (hasCommand && !validateMcpCommandArgs(s, hasTopLevelCommand)) {
|
|
568
|
+
return false;
|
|
569
|
+
}
|
|
570
|
+
if (s.description != null && typeof s.description !== "string") {
|
|
571
|
+
return false;
|
|
572
|
+
}
|
|
573
|
+
if (s.enabled !== void 0 && typeof s.enabled !== "boolean") {
|
|
574
|
+
return false;
|
|
575
|
+
}
|
|
576
|
+
return true;
|
|
577
|
+
}
|
|
578
|
+
function convertMcpServers(servers) {
|
|
579
|
+
if (!servers || servers.length === 0) {
|
|
580
|
+
return void 0;
|
|
581
|
+
}
|
|
582
|
+
const validServers = servers.filter((server) => isValidMcpServer(server));
|
|
583
|
+
if (validServers.length === 0) {
|
|
584
|
+
return void 0;
|
|
585
|
+
}
|
|
586
|
+
return validServers;
|
|
587
|
+
}
|
|
588
|
+
function hasValidCodeStructure(code) {
|
|
589
|
+
if (!code || typeof code !== "object") {
|
|
590
|
+
return false;
|
|
591
|
+
}
|
|
592
|
+
const c = code;
|
|
593
|
+
return typeof c.link === "string" && c.link.length > 0;
|
|
594
|
+
}
|
|
595
|
+
function assistantResponseToResource(assistant) {
|
|
596
|
+
const slug = assistant.slug || "";
|
|
597
|
+
const promptFileName = slug || assistant.name.toLowerCase().replaceAll(/\s+/g, "-");
|
|
598
|
+
const mcpServers = convertMcpServers(assistant.mcp_servers);
|
|
599
|
+
const nestedAssistants = assistant.nested_assistants;
|
|
600
|
+
const subAssistants = nestedAssistants?.map((nested) => nested.name);
|
|
601
|
+
const categoriesRaw = assistant.categories;
|
|
602
|
+
let categories;
|
|
603
|
+
if (categoriesRaw && Array.isArray(categoriesRaw)) {
|
|
604
|
+
if (categoriesRaw.length === 0) {
|
|
605
|
+
categories = [];
|
|
606
|
+
} else if (typeof categoriesRaw[0] === "object" && categoriesRaw[0] !== null && "id" in categoriesRaw[0]) {
|
|
607
|
+
categories = categoriesRaw.map((cat) => cat.id);
|
|
608
|
+
} else {
|
|
609
|
+
categories = categoriesRaw;
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
return {
|
|
613
|
+
name: assistant.name,
|
|
614
|
+
description: assistant.description || "",
|
|
615
|
+
prompt: `system_prompts/${promptFileName}.prompt.md`,
|
|
616
|
+
model: assistant.llm_model_type || DEFAULT_LLM_MODEL,
|
|
617
|
+
...assistant.temperature !== void 0 && assistant.temperature !== null && { temperature: assistant.temperature },
|
|
618
|
+
...assistant.top_p !== void 0 && assistant.top_p !== null && { top_p: assistant.top_p },
|
|
619
|
+
...assistant.shared !== void 0 && { shared: assistant.shared },
|
|
620
|
+
...assistant.is_react !== void 0 && { is_react: assistant.is_react },
|
|
621
|
+
...assistant.is_global !== void 0 && { is_global: assistant.is_global },
|
|
622
|
+
...assistant.icon_url && { icon_url: assistant.icon_url },
|
|
623
|
+
...assistant.conversation_starters && { conversation_starters: assistant.conversation_starters },
|
|
624
|
+
...assistant.toolkits && { toolkits: assistant.toolkits },
|
|
625
|
+
...assistant.context && { context: assistant.context },
|
|
626
|
+
...mcpServers && { mcp_servers: mcpServers },
|
|
627
|
+
...subAssistants && { sub_assistants: subAssistants },
|
|
628
|
+
...assistant.prompt_variables && { prompt_variables: assistant.prompt_variables },
|
|
629
|
+
...categories && { categories }
|
|
630
|
+
};
|
|
631
|
+
}
|
|
632
|
+
function convertCodeDatasource(datasource, base) {
|
|
633
|
+
const desc = datasource.description || "";
|
|
634
|
+
if (hasValidCodeStructure(datasource.code)) {
|
|
635
|
+
return {
|
|
636
|
+
...base,
|
|
637
|
+
type: "code",
|
|
638
|
+
description: desc,
|
|
639
|
+
link: datasource.code.link,
|
|
640
|
+
branch: datasource.code.branch,
|
|
641
|
+
index_type: datasource.code.indexType,
|
|
642
|
+
summarization_model: datasource.code.summarizationModel,
|
|
643
|
+
files_filter: datasource.code.filesFilter
|
|
644
|
+
};
|
|
645
|
+
}
|
|
646
|
+
return {
|
|
647
|
+
...base,
|
|
648
|
+
type: "code",
|
|
649
|
+
description: desc,
|
|
650
|
+
link: void 0
|
|
651
|
+
};
|
|
652
|
+
}
|
|
653
|
+
function datasourceResponseToResource(datasource, integrationAlias) {
|
|
654
|
+
const settingId = integrationAlias ? `$ref:imported.integrations.${integrationAlias}.id` : datasource.setting_id ?? "";
|
|
655
|
+
const allowedWithoutSettingId = /* @__PURE__ */ new Set(["knowledge_base_file", "llm_routing_google"]);
|
|
656
|
+
if (!settingId && !allowedWithoutSettingId.has(datasource.type)) {
|
|
657
|
+
logger.warn(`\u26A0\uFE0F Datasource "${datasource.name}" is missing setting_id (integration reference)`);
|
|
658
|
+
}
|
|
659
|
+
const base = {
|
|
660
|
+
name: datasource.name,
|
|
661
|
+
type: datasource.type,
|
|
662
|
+
embeddings_model: datasource.embeddings_model,
|
|
663
|
+
setting_id: settingId || void 0,
|
|
664
|
+
shared_with_project: datasource.shared_with_project
|
|
665
|
+
};
|
|
666
|
+
if (datasource.type === DataSourceType.CODE) {
|
|
667
|
+
return convertCodeDatasource(datasource, base);
|
|
668
|
+
}
|
|
669
|
+
if (datasource.type === DataSourceType.CONFLUENCE && datasource.confluence) {
|
|
670
|
+
return {
|
|
671
|
+
...base,
|
|
672
|
+
type: DataSourceType.CONFLUENCE,
|
|
673
|
+
description: datasource.description || "",
|
|
674
|
+
...datasource.confluence
|
|
675
|
+
};
|
|
676
|
+
}
|
|
677
|
+
if (datasource.type === DataSourceType.JIRA && datasource.jira) {
|
|
678
|
+
return {
|
|
679
|
+
...base,
|
|
680
|
+
type: DataSourceType.JIRA,
|
|
681
|
+
description: datasource.description || "",
|
|
682
|
+
...datasource.jira
|
|
683
|
+
};
|
|
684
|
+
}
|
|
685
|
+
if (datasource.type === DataSourceType.GOOGLE && datasource.google_doc_link) {
|
|
686
|
+
return {
|
|
687
|
+
...base,
|
|
688
|
+
type: DataSourceType.GOOGLE,
|
|
689
|
+
description: datasource.description || "",
|
|
690
|
+
google_doc: datasource.google_doc_link
|
|
691
|
+
};
|
|
692
|
+
}
|
|
693
|
+
if (datasource.type === DataSourceType.FILE) {
|
|
694
|
+
return {
|
|
695
|
+
...base,
|
|
696
|
+
type: DataSourceType.FILE,
|
|
697
|
+
description: datasource.description || ""
|
|
698
|
+
};
|
|
699
|
+
}
|
|
700
|
+
logger.warn(` \u26A0\uFE0F Unknown datasource type '${datasource.type}' - saving with basic fields only`);
|
|
701
|
+
return {
|
|
702
|
+
...base,
|
|
703
|
+
description: datasource.description || ""
|
|
704
|
+
};
|
|
705
|
+
}
|
|
706
|
+
function workflowResponseToResource(workflow) {
|
|
707
|
+
return {
|
|
708
|
+
name: workflow.name,
|
|
709
|
+
description: workflow.description || "",
|
|
710
|
+
definition: `workflows/${workflow.name.toLowerCase().replaceAll(/\s+/g, "-")}.yaml`,
|
|
711
|
+
...workflow.mode && { mode: workflow.mode },
|
|
712
|
+
...workflow.shared !== void 0 && { shared: workflow.shared },
|
|
713
|
+
...workflow.icon_url && { icon_url: workflow.icon_url }
|
|
714
|
+
};
|
|
715
|
+
}
|
|
716
|
+
function isValidCodeParams(params) {
|
|
717
|
+
return typeof params === "object" && params !== null && "link" in params && typeof params.link === "string" && params.link.length > 0;
|
|
718
|
+
}
|
|
719
|
+
function datasourceResourceToCreateParams(datasource, projectName) {
|
|
720
|
+
const {
|
|
721
|
+
$ref: _ref,
|
|
722
|
+
force_reindex: _forceReindex,
|
|
723
|
+
...sdkFields
|
|
724
|
+
} = datasource;
|
|
725
|
+
const params = {
|
|
726
|
+
...sdkFields,
|
|
727
|
+
project_name: projectName,
|
|
728
|
+
shared_with_project: datasource.shared_with_project ?? true
|
|
729
|
+
};
|
|
730
|
+
if (datasource.type === "code" && !isValidCodeParams(params)) {
|
|
731
|
+
throw new Error(
|
|
732
|
+
`Invalid code datasource "${datasource.name}": missing required field "link". Please add repository URL to datasource configuration.`
|
|
733
|
+
);
|
|
734
|
+
} else if (datasource.type === "knowledge_base_confluence" && (!("cql" in params) || !params.cql)) {
|
|
735
|
+
throw new Error(
|
|
736
|
+
`Invalid Confluence datasource "${datasource.name}": missing required field "cql". Please add CQL query to datasource configuration.`
|
|
737
|
+
);
|
|
738
|
+
} else if (datasource.type === "knowledge_base_jira" && (!("jql" in params) || !params.jql)) {
|
|
739
|
+
throw new Error(
|
|
740
|
+
`Invalid Jira datasource "${datasource.name}": missing required field "jql". Please add JQL query to datasource configuration.`
|
|
741
|
+
);
|
|
742
|
+
} else if (datasource.type === "llm_routing_google" && (!("google_doc" in params) || !params.google_doc)) {
|
|
743
|
+
throw new Error(
|
|
744
|
+
`Invalid Google Docs datasource "${datasource.name}": missing required field "google_doc". Please add Google Doc ID to datasource configuration.`
|
|
745
|
+
);
|
|
746
|
+
}
|
|
747
|
+
return params;
|
|
748
|
+
}
|
|
749
|
+
function iacToolToSdk(tool) {
|
|
750
|
+
return {
|
|
751
|
+
...tool,
|
|
752
|
+
settings_config: tool.settings_config ?? Boolean(tool.settings),
|
|
753
|
+
settings: isResolvedIntegration(tool.settings) ? tool.settings : void 0
|
|
754
|
+
};
|
|
755
|
+
}
|
|
756
|
+
function iacToolkitToSdk(toolkit) {
|
|
757
|
+
return {
|
|
758
|
+
...toolkit,
|
|
759
|
+
settings_config: toolkit.settings_config ?? Boolean(toolkit.settings),
|
|
760
|
+
is_external: toolkit.is_external ?? false,
|
|
761
|
+
tools: toolkit.tools.map((tool) => iacToolToSdk(tool)),
|
|
762
|
+
settings: isResolvedIntegration(toolkit.settings) ? toolkit.settings : void 0
|
|
763
|
+
};
|
|
764
|
+
}
|
|
765
|
+
function iacMcpServerToSdk(mcp) {
|
|
766
|
+
return {
|
|
767
|
+
...mcp,
|
|
768
|
+
enabled: mcp.enabled ?? true,
|
|
769
|
+
settings: isResolvedIntegration(mcp.settings) ? mcp.settings : void 0,
|
|
770
|
+
mcp_connect_auth_token: isResolvedIntegration(mcp.mcp_connect_auth_token) ? mcp.mcp_connect_auth_token : void 0
|
|
771
|
+
};
|
|
772
|
+
}
|
|
773
|
+
function assistantResourceToCreateParams(assistant, projectName, promptContent) {
|
|
774
|
+
const {
|
|
775
|
+
prompt: _prompt,
|
|
776
|
+
config: _config,
|
|
777
|
+
model,
|
|
778
|
+
sub_assistants: _subAssistants,
|
|
779
|
+
datasource_names: _datasourceNames,
|
|
780
|
+
toolkits,
|
|
781
|
+
mcp_servers: mcpServers,
|
|
782
|
+
...sdkFields
|
|
783
|
+
} = assistant;
|
|
784
|
+
return {
|
|
785
|
+
...sdkFields,
|
|
786
|
+
project: projectName,
|
|
787
|
+
llm_model_type: model,
|
|
788
|
+
system_prompt: promptContent,
|
|
789
|
+
name: assistant.name,
|
|
790
|
+
description: assistant.description,
|
|
791
|
+
conversation_starters: assistant.conversation_starters || [],
|
|
792
|
+
toolkits: (toolkits || []).map((toolkit) => iacToolkitToSdk(toolkit)),
|
|
793
|
+
context: assistant.context || [],
|
|
794
|
+
mcp_servers: (mcpServers || []).map((mcp) => iacMcpServerToSdk(mcp)),
|
|
795
|
+
assistant_ids: assistant.assistant_ids || [],
|
|
796
|
+
shared: assistant.shared ?? true,
|
|
797
|
+
prompt_variables: assistant.prompt_variables || []
|
|
798
|
+
};
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
// src/lib/backupTransformers.ts
|
|
802
|
+
function transformIntegrationSettings(settings, integrationIdToAlias, integrationSpecPaths, contextLabel = "integration") {
|
|
803
|
+
const integrationId = settings.id;
|
|
804
|
+
const alias = settings.alias || integrationId && integrationIdToAlias.get(integrationId);
|
|
805
|
+
if (alias) {
|
|
806
|
+
return { $ref: `imported.integrations.${alias}` };
|
|
807
|
+
}
|
|
808
|
+
if (integrationId) {
|
|
809
|
+
logger.warn(` \u26A0\uFE0F No alias found for ${contextLabel} ${integrationId}, keeping full settings`);
|
|
810
|
+
const specPath = integrationSpecPaths?.get(integrationId);
|
|
811
|
+
if (specPath && settings.credential_values) {
|
|
812
|
+
return {
|
|
813
|
+
...settings,
|
|
814
|
+
credential_values: settings.credential_values.map(
|
|
815
|
+
(cred) => cred.key === "openapi_spec" ? { key: "openapi_spec", value: specPath } : cred
|
|
816
|
+
)
|
|
817
|
+
};
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
return settings;
|
|
821
|
+
}
|
|
822
|
+
function transformTool(tool, integrationIdToAlias, integrationSpecPaths) {
|
|
823
|
+
const result = {
|
|
824
|
+
name: tool.name,
|
|
825
|
+
settings_config: tool.settings_config
|
|
826
|
+
};
|
|
827
|
+
if (tool.label !== void 0 && tool.label !== null) {
|
|
828
|
+
result.label = tool.label;
|
|
829
|
+
}
|
|
830
|
+
if (tool.settings) {
|
|
831
|
+
result.settings = transformIntegrationSettings(tool.settings, integrationIdToAlias, integrationSpecPaths, "tool");
|
|
832
|
+
}
|
|
833
|
+
return result;
|
|
834
|
+
}
|
|
835
|
+
function transformMcpServer(mcp, integrationIdToAlias) {
|
|
836
|
+
const result = {
|
|
837
|
+
name: mcp.name,
|
|
838
|
+
description: mcp.description,
|
|
839
|
+
enabled: mcp.enabled,
|
|
840
|
+
command: mcp.command,
|
|
841
|
+
arguments: mcp.arguments,
|
|
842
|
+
config: mcp.config,
|
|
843
|
+
mcp_connect_url: mcp.mcp_connect_url,
|
|
844
|
+
mcp_connect_auth_token: mcp.mcp_connect_auth_token,
|
|
845
|
+
tools_tokens_size_limit: mcp.tools_tokens_size_limit
|
|
846
|
+
};
|
|
847
|
+
if (mcp.settings && isResolvedIntegration(mcp.settings)) {
|
|
848
|
+
const alias = mcp.settings.alias || integrationIdToAlias.get(mcp.settings.id);
|
|
849
|
+
if (alias) {
|
|
850
|
+
result.settings = { $ref: `imported.integrations.${alias}` };
|
|
851
|
+
} else {
|
|
852
|
+
logger.warn(` \u26A0\uFE0F No alias found for MCP integration ${mcp.settings.id}, keeping full settings`);
|
|
853
|
+
result.settings = mcp.settings;
|
|
854
|
+
}
|
|
855
|
+
} else if (mcp.settings) {
|
|
856
|
+
result.settings = mcp.settings;
|
|
857
|
+
}
|
|
858
|
+
if (result.mcp_connect_auth_token && isResolvedIntegration(result.mcp_connect_auth_token)) {
|
|
859
|
+
const authAlias = result.mcp_connect_auth_token.alias || integrationIdToAlias.get(result.mcp_connect_auth_token.id);
|
|
860
|
+
if (authAlias) {
|
|
861
|
+
result.mcp_connect_auth_token = { $ref: `imported.integrations.${authAlias}` };
|
|
862
|
+
} else {
|
|
863
|
+
logger.warn(
|
|
864
|
+
` \u26A0\uFE0F No alias found for MCP auth token integration ${result.mcp_connect_auth_token.id}, keeping full object`
|
|
865
|
+
);
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
return result;
|
|
869
|
+
}
|
|
870
|
+
function transformToolkits(toolkits, integrationIdToAlias, integrationSpecPaths) {
|
|
871
|
+
return toolkits?.map(({ toolkit, tools, label, settings_config, is_external, settings }) => ({
|
|
872
|
+
toolkit,
|
|
873
|
+
tools: tools.map((tool) => transformTool(tool, integrationIdToAlias, integrationSpecPaths)),
|
|
874
|
+
label,
|
|
875
|
+
settings_config,
|
|
876
|
+
is_external,
|
|
877
|
+
...settings ? {
|
|
878
|
+
settings: transformIntegrationSettings(settings, integrationIdToAlias, integrationSpecPaths, "toolkit")
|
|
879
|
+
} : {}
|
|
880
|
+
}));
|
|
881
|
+
}
|
|
882
|
+
function prepareAssistantForYaml(assistant, state, integrationIdToAlias, integrationSpecPaths) {
|
|
883
|
+
const transformedToolkits = transformToolkits(assistant.toolkits, integrationIdToAlias, integrationSpecPaths);
|
|
884
|
+
const transformedMcpServers = assistant.mcp_servers?.map((mcp) => transformMcpServer(mcp, integrationIdToAlias));
|
|
885
|
+
const finalAssistant = {
|
|
886
|
+
...assistantResponseToResource(assistant),
|
|
887
|
+
...transformedToolkits && { toolkits: transformedToolkits },
|
|
888
|
+
...transformedMcpServers && { mcp_servers: transformedMcpServers }
|
|
889
|
+
};
|
|
890
|
+
state.resources.assistants[assistant.name] = {
|
|
891
|
+
id: assistant.id,
|
|
892
|
+
lastDeployed: (/* @__PURE__ */ new Date()).toISOString(),
|
|
893
|
+
promptChecksum: calculateChecksum(assistant.system_prompt || ""),
|
|
894
|
+
configChecksum: calculateAssistantConfigChecksum(finalAssistant)
|
|
895
|
+
};
|
|
896
|
+
return finalAssistant;
|
|
897
|
+
}
|
|
898
|
+
function prepareDatasourceForYaml(datasource, state, integrationIdToAlias) {
|
|
899
|
+
const settingId = hasSettingId(datasource) ? String(datasource.setting_id || "") : "";
|
|
900
|
+
const integrationAlias = integrationIdToAlias.get(settingId);
|
|
901
|
+
const finalDatasource = datasourceResponseToResource(datasource, integrationAlias);
|
|
902
|
+
state.resources.datasources[datasource.name] = {
|
|
903
|
+
id: datasource.id,
|
|
904
|
+
lastDeployed: (/* @__PURE__ */ new Date()).toISOString(),
|
|
905
|
+
configChecksum: calculateDatasourceConfigChecksum(finalDatasource)
|
|
906
|
+
};
|
|
907
|
+
return finalDatasource;
|
|
908
|
+
}
|
|
909
|
+
function prepareWorkflowForYaml(workflow, state, assistants, backupDir) {
|
|
910
|
+
const resource = workflowResponseToResource(workflow);
|
|
911
|
+
const yamlConfig = workflow.yaml_config;
|
|
912
|
+
let finalYamlContent = yamlConfig || "";
|
|
913
|
+
if (yamlConfig) {
|
|
914
|
+
try {
|
|
915
|
+
const workflowYaml = yaml3.parse(yamlConfig);
|
|
916
|
+
const transformedAssistants = workflowYaml.assistants?.map((assistant) => {
|
|
917
|
+
if (assistant.assistant_id && typeof assistant.assistant_id === "string") {
|
|
918
|
+
const assistantId = assistant.assistant_id;
|
|
919
|
+
const matchedAssistant = assistants.find(({ id }) => id === assistantId);
|
|
920
|
+
if (matchedAssistant && hasName(matchedAssistant)) {
|
|
921
|
+
const { assistant_id: _assistantId, ...rest } = assistant;
|
|
922
|
+
return { ...rest, assistant_name: matchedAssistant.name };
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
return assistant;
|
|
926
|
+
});
|
|
927
|
+
if (transformedAssistants) {
|
|
928
|
+
const transformedYaml = { ...workflowYaml, assistants: transformedAssistants };
|
|
929
|
+
const fileName = `${sanitizeFileName(workflow.name)}.yaml`;
|
|
930
|
+
const filePath = path6.join(backupDir, "workflows", fileName);
|
|
931
|
+
ensureDirectoryExists(filePath);
|
|
932
|
+
finalYamlContent = yaml3.stringify(transformedYaml);
|
|
933
|
+
fs2.writeFileSync(filePath, finalYamlContent, "utf8");
|
|
934
|
+
}
|
|
935
|
+
} catch (error) {
|
|
936
|
+
logger.warn(
|
|
937
|
+
` \u26A0\uFE0F Failed to transform workflow YAML for ${workflow.name}: ${error instanceof Error ? error.message : String(error)}`
|
|
938
|
+
);
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
state.resources.workflows[workflow.name] = {
|
|
942
|
+
id: workflow.id,
|
|
943
|
+
lastDeployed: (/* @__PURE__ */ new Date()).toISOString(),
|
|
944
|
+
workflowYamlChecksum: calculateChecksum(finalYamlContent),
|
|
945
|
+
configChecksum: calculateWorkflowConfigChecksum(resource)
|
|
946
|
+
};
|
|
947
|
+
return resource;
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
// src/lib/backupYamlGenerator.ts
|
|
951
|
+
function generateCodemieYaml(backup, projectName, backupDir, integrationSpecPaths = /* @__PURE__ */ new Map()) {
|
|
952
|
+
const integrationIdToAlias = /* @__PURE__ */ new Map();
|
|
953
|
+
const integrationArray = [];
|
|
954
|
+
for (const integration of backup.resources.integrations) {
|
|
955
|
+
const alias = integration.alias || `${integration.credential_type.toLowerCase()}_${integration.id.slice(0, 8)}`;
|
|
956
|
+
integrationIdToAlias.set(integration.id, alias);
|
|
957
|
+
const specPath = integrationSpecPaths.get(integration.id);
|
|
958
|
+
let credentialValues = integration.credential_values;
|
|
959
|
+
if (specPath && credentialValues) {
|
|
960
|
+
credentialValues = credentialValues.map(
|
|
961
|
+
(cred) => cred.key === "openapi_spec" ? { key: "openapi_spec", value: specPath } : cred
|
|
962
|
+
);
|
|
963
|
+
}
|
|
964
|
+
integrationArray.push({
|
|
965
|
+
id: integration.id,
|
|
966
|
+
alias,
|
|
967
|
+
credential_type: integration.credential_type,
|
|
968
|
+
project_name: integration.project_name,
|
|
969
|
+
setting_type: integration.setting_type,
|
|
970
|
+
...credentialValues && { credential_values: credentialValues }
|
|
971
|
+
});
|
|
972
|
+
}
|
|
973
|
+
const config = {
|
|
974
|
+
version: "1",
|
|
975
|
+
project: {
|
|
976
|
+
name: projectName,
|
|
977
|
+
description: `Backup from ${backup.metadata.timestamp}`
|
|
978
|
+
},
|
|
979
|
+
environment: {
|
|
980
|
+
// biome-ignore lint/suspicious/noTemplateCurlyInString: Not a template, but exact formatting
|
|
981
|
+
codemie_api_url: "${CODEMIE_API_URL}",
|
|
982
|
+
// biome-ignore lint/suspicious/noTemplateCurlyInString: Not a template, but exact formatting
|
|
983
|
+
auth_server_url: "${CODEMIE_AUTH_URL}",
|
|
984
|
+
// biome-ignore lint/suspicious/noTemplateCurlyInString: Not a template, but exact formatting
|
|
985
|
+
auth_realm_name: "${CODEMIE_REALM:-codemie}",
|
|
986
|
+
// biome-ignore lint/suspicious/noTemplateCurlyInString: Not a template, but exact formatting
|
|
987
|
+
client_id: "${CODEMIE_CLIENT_ID:-}",
|
|
988
|
+
// biome-ignore lint/suspicious/noTemplateCurlyInString: Not a template, but exact formatting
|
|
989
|
+
client_secret: "${CODEMIE_CLIENT_SECRET:-}",
|
|
990
|
+
// biome-ignore lint/suspicious/noTemplateCurlyInString: Not a template, but exact formatting
|
|
991
|
+
username: "${CODEMIE_USERNAME:-}",
|
|
992
|
+
// biome-ignore lint/suspicious/noTemplateCurlyInString: Not a template, but exact formatting
|
|
993
|
+
password: "${CODEMIE_PASSWORD:-}"
|
|
994
|
+
},
|
|
995
|
+
imported: {
|
|
996
|
+
assistants: [],
|
|
997
|
+
datasources: [],
|
|
998
|
+
integrations: integrationArray
|
|
999
|
+
},
|
|
1000
|
+
datasource_defaults: {
|
|
1001
|
+
code: { type: "code", index_type: "code", embeddings_model: "ada-002", shared_with_project: true },
|
|
1002
|
+
knowledge_base_confluence: {
|
|
1003
|
+
type: "knowledge_base_confluence",
|
|
1004
|
+
embeddings_model: "ada-002",
|
|
1005
|
+
shared_with_project: true
|
|
1006
|
+
},
|
|
1007
|
+
knowledge_base_jira: { type: "knowledge_base_jira", embeddings_model: "ada-002", shared_with_project: true }
|
|
1008
|
+
},
|
|
1009
|
+
resources: {
|
|
1010
|
+
assistants: backup.resources.assistants.map(
|
|
1011
|
+
(assistant) => prepareAssistantForYaml(assistant, backup.state, integrationIdToAlias, integrationSpecPaths)
|
|
1012
|
+
),
|
|
1013
|
+
datasources: backup.resources.datasources.map(
|
|
1014
|
+
(datasource) => prepareDatasourceForYaml(datasource, backup.state, integrationIdToAlias)
|
|
1015
|
+
),
|
|
1016
|
+
workflows: backup.resources.workflows.map(
|
|
1017
|
+
(workflow) => prepareWorkflowForYaml(workflow, backup.state, backup.resources.assistants, backupDir)
|
|
1018
|
+
)
|
|
1019
|
+
}
|
|
1020
|
+
};
|
|
1021
|
+
return yaml3.stringify(config);
|
|
1022
|
+
}
|
|
1023
|
+
function saveIntegrationOpenApiSpecs(backupData, backupDir) {
|
|
1024
|
+
const specsDir = path6.join(backupDir, "openapi_specs");
|
|
1025
|
+
let specsCount = 0;
|
|
1026
|
+
const integrationSpecPaths = /* @__PURE__ */ new Map();
|
|
1027
|
+
for (const integration of backupData.resources.integrations) {
|
|
1028
|
+
if (integration.credential_type !== "OpenAPI" || !integration.credential_values) {
|
|
1029
|
+
continue;
|
|
1030
|
+
}
|
|
1031
|
+
const specEntry = integration.credential_values.find(
|
|
1032
|
+
(cv) => cv.key === "openapi_spec"
|
|
1033
|
+
);
|
|
1034
|
+
if (specEntry && typeof specEntry.value === "string" && specEntry.value) {
|
|
1035
|
+
const alias = integration.alias || integration.id;
|
|
1036
|
+
const fileName = String(alias).toLowerCase().replaceAll(/[^a-z0-9]+/g, "-");
|
|
1037
|
+
if (!fs2.existsSync(specsDir)) {
|
|
1038
|
+
fs2.mkdirSync(specsDir, { recursive: true });
|
|
1039
|
+
}
|
|
1040
|
+
const specContent = specEntry.value;
|
|
1041
|
+
let fileExtension = ".yaml";
|
|
1042
|
+
let contentToSave = specContent;
|
|
1043
|
+
try {
|
|
1044
|
+
const parsed = JSON.parse(specContent);
|
|
1045
|
+
fileExtension = ".json";
|
|
1046
|
+
contentToSave = JSON.stringify(parsed, null, 2);
|
|
1047
|
+
} catch {
|
|
1048
|
+
fileExtension = ".yaml";
|
|
1049
|
+
}
|
|
1050
|
+
const specFileName = `${fileName}${fileExtension}`;
|
|
1051
|
+
const specPath = path6.join(specsDir, specFileName);
|
|
1052
|
+
fs2.writeFileSync(specPath, contentToSave, "utf8");
|
|
1053
|
+
integrationSpecPaths.set(integration.id, `openapi_specs/${specFileName}`);
|
|
1054
|
+
specsCount++;
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
if (specsCount > 0) {
|
|
1058
|
+
logger.info(` \u2713 Saved ${specsCount} integration OpenAPI spec(s)`);
|
|
1059
|
+
}
|
|
1060
|
+
return integrationSpecPaths;
|
|
1061
|
+
}
|
|
1062
|
+
function saveBackupFiles(backupData, backupDir, projectName) {
|
|
1063
|
+
logger.info("\u{1F4BE} Writing backup files...\n");
|
|
1064
|
+
const integrationSpecPaths = saveIntegrationOpenApiSpecs(backupData, backupDir);
|
|
1065
|
+
const codemieYamlPath = path6.join(backupDir, "codemie.yaml");
|
|
1066
|
+
const codemieYaml = generateCodemieYaml(backupData, projectName, backupDir, integrationSpecPaths);
|
|
1067
|
+
fs2.writeFileSync(codemieYamlPath, codemieYaml, "utf8");
|
|
1068
|
+
logger.info(` \u2713 Saved config file: codemie.yaml`);
|
|
1069
|
+
const backupJsonPath = path6.join(backupDir, "backup.json");
|
|
1070
|
+
fs2.writeFileSync(backupJsonPath, JSON.stringify(backupData, null, 2), "utf8");
|
|
1071
|
+
logger.info(` \u2713 Saved full backup: backup.json`);
|
|
1072
|
+
const statePath = path6.join(backupDir, "state.json");
|
|
1073
|
+
fs2.writeFileSync(statePath, JSON.stringify(backupData.state, null, 2), "utf8");
|
|
1074
|
+
logger.info(` \u2713 Saved state file: state.json`);
|
|
1075
|
+
}
|
|
1076
|
+
async function createClient(config) {
|
|
1077
|
+
const client = new CodeMieClient({
|
|
1078
|
+
auth_server_url: config.environment.auth_server_url,
|
|
1079
|
+
auth_realm_name: config.environment.auth_realm_name,
|
|
1080
|
+
codemie_api_domain: config.environment.codemie_api_url,
|
|
1081
|
+
auth_client_id: config.environment.client_id,
|
|
1082
|
+
auth_client_secret: config.environment.client_secret,
|
|
1083
|
+
username: config.environment.username,
|
|
1084
|
+
password: config.environment.password,
|
|
1085
|
+
verify_ssl: true
|
|
1086
|
+
});
|
|
1087
|
+
await client.initialize();
|
|
1088
|
+
return client;
|
|
1089
|
+
}
|
|
1090
|
+
var CodemieConfigLoader = class {
|
|
1091
|
+
appConfig;
|
|
1092
|
+
constructor(appConfig) {
|
|
1093
|
+
this.appConfig = appConfig;
|
|
1094
|
+
}
|
|
1095
|
+
/**
|
|
1096
|
+
* Load and parse the main codemie.yaml configuration file
|
|
1097
|
+
*/
|
|
1098
|
+
loadConfig() {
|
|
1099
|
+
const { rootDir, codemieConfig } = this.appConfig;
|
|
1100
|
+
const configPath = path6.join(rootDir, codemieConfig);
|
|
1101
|
+
if (!fs2.existsSync(configPath)) {
|
|
1102
|
+
throw new Error(`Configuration file not found: ${configPath}`);
|
|
1103
|
+
}
|
|
1104
|
+
const content = fs2.readFileSync(configPath, "utf8");
|
|
1105
|
+
const config = yaml3.parse(content);
|
|
1106
|
+
this.resolveImports(config, rootDir);
|
|
1107
|
+
this.substituteEnvVars(config);
|
|
1108
|
+
this.applyDatasourceDefaults(config);
|
|
1109
|
+
this.resolveReferencesRecursive(config, config);
|
|
1110
|
+
this.validateDatasourceIntegrationReferences(config);
|
|
1111
|
+
return config;
|
|
1112
|
+
}
|
|
1113
|
+
/**
|
|
1114
|
+
* Load assistant prompt file
|
|
1115
|
+
*/
|
|
1116
|
+
loadPrompt(promptPath) {
|
|
1117
|
+
const { rootDir } = this.appConfig;
|
|
1118
|
+
const fullPath = path6.join(rootDir, promptPath);
|
|
1119
|
+
if (!fs2.existsSync(fullPath)) {
|
|
1120
|
+
throw new Error(`Prompt file not found: ${fullPath}`);
|
|
1121
|
+
}
|
|
1122
|
+
return fs2.readFileSync(fullPath, "utf8");
|
|
1123
|
+
}
|
|
1124
|
+
/**
|
|
1125
|
+
* Load assistant configuration file
|
|
1126
|
+
*/
|
|
1127
|
+
loadAssistantConfig(configPath) {
|
|
1128
|
+
const { rootDir } = this.appConfig;
|
|
1129
|
+
const fullPath = path6.join(rootDir, configPath);
|
|
1130
|
+
if (!fs2.existsSync(fullPath)) {
|
|
1131
|
+
throw new Error(`Config file not found: ${fullPath}`);
|
|
1132
|
+
}
|
|
1133
|
+
const content = fs2.readFileSync(fullPath, "utf8");
|
|
1134
|
+
return yaml3.parse(content);
|
|
1135
|
+
}
|
|
1136
|
+
/**
|
|
1137
|
+
* Validate that all referenced files exist
|
|
1138
|
+
*/
|
|
1139
|
+
validateFiles(config) {
|
|
1140
|
+
const errors = [];
|
|
1141
|
+
const { rootDir } = this.appConfig;
|
|
1142
|
+
if (config.resources.assistants) {
|
|
1143
|
+
for (const assistant of config.resources.assistants) {
|
|
1144
|
+
const promptPath = path6.join(rootDir, assistant.prompt);
|
|
1145
|
+
if (!fs2.existsSync(promptPath)) {
|
|
1146
|
+
errors.push(`Prompt file not found for ${assistant.name}: ${assistant.prompt}`);
|
|
1147
|
+
}
|
|
1148
|
+
if (assistant.config) {
|
|
1149
|
+
const configPath = path6.join(rootDir, assistant.config);
|
|
1150
|
+
if (!fs2.existsSync(configPath)) {
|
|
1151
|
+
errors.push(`Config file not found for ${assistant.name}: ${assistant.config}`);
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
if (!assistant.description) {
|
|
1155
|
+
errors.push(`Missing 'description' for assistant: ${assistant.name}`);
|
|
1156
|
+
}
|
|
1157
|
+
if (!assistant.model) {
|
|
1158
|
+
errors.push(`Missing 'model' for assistant: ${assistant.name}`);
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
return {
|
|
1163
|
+
valid: errors.length === 0,
|
|
1164
|
+
errors
|
|
1165
|
+
};
|
|
1166
|
+
}
|
|
1167
|
+
/**
|
|
1168
|
+
* Resolve $import directives recursively
|
|
1169
|
+
* Automatically handles:
|
|
1170
|
+
* 1. Single resource import (object)
|
|
1171
|
+
* 2. Multiple resources import (array)
|
|
1172
|
+
* 3. Mixed inline + imports
|
|
1173
|
+
* Includes circular import detection to prevent infinite recursion
|
|
1174
|
+
*/
|
|
1175
|
+
resolveImports(obj, baseDir, visitedFiles = /* @__PURE__ */ new Set()) {
|
|
1176
|
+
if (!this.isPlainObject(obj)) {
|
|
1177
|
+
return;
|
|
1178
|
+
}
|
|
1179
|
+
for (const key in obj) {
|
|
1180
|
+
const value = obj[key];
|
|
1181
|
+
if (this.isPlainObject(value) && "$import" in value) {
|
|
1182
|
+
const importDirective = value;
|
|
1183
|
+
const importPath = path6.join(baseDir, importDirective.$import);
|
|
1184
|
+
const normalizedPath = path6.resolve(importPath);
|
|
1185
|
+
if (visitedFiles.has(normalizedPath)) {
|
|
1186
|
+
throw new Error(
|
|
1187
|
+
`Circular import detected: ${normalizedPath}
|
|
1188
|
+
Import chain: ${[...visitedFiles].join(" \u2192 ")} \u2192 ${normalizedPath}`
|
|
1189
|
+
);
|
|
1190
|
+
}
|
|
1191
|
+
visitedFiles.add(normalizedPath);
|
|
1192
|
+
const imported = this.loadYamlFile(importPath);
|
|
1193
|
+
obj[key] = imported;
|
|
1194
|
+
this.resolveImports(obj[key], path6.dirname(importPath), visitedFiles);
|
|
1195
|
+
visitedFiles.delete(normalizedPath);
|
|
1196
|
+
} else if (Array.isArray(value)) {
|
|
1197
|
+
obj[key] = this.resolveArrayImports(value, baseDir, visitedFiles);
|
|
1198
|
+
} else if (this.isPlainObject(value)) {
|
|
1199
|
+
this.resolveImports(value, baseDir, visitedFiles);
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
/**
|
|
1204
|
+
* Resolve imports in arrays
|
|
1205
|
+
* Intelligently handles:
|
|
1206
|
+
* - Inline objects (keep as-is)
|
|
1207
|
+
* - $import with single object (add to array)
|
|
1208
|
+
* - $import with array (flatten into parent array)
|
|
1209
|
+
* Includes circular import detection
|
|
1210
|
+
*/
|
|
1211
|
+
resolveArrayImports(arr, baseDir, visitedFiles = /* @__PURE__ */ new Set()) {
|
|
1212
|
+
const result = [];
|
|
1213
|
+
for (const item of arr) {
|
|
1214
|
+
if (this.isPlainObject(item) && "$import" in item) {
|
|
1215
|
+
const importDirective = item;
|
|
1216
|
+
const importPath = path6.join(baseDir, importDirective.$import);
|
|
1217
|
+
const normalizedPath = path6.resolve(importPath);
|
|
1218
|
+
if (visitedFiles.has(normalizedPath)) {
|
|
1219
|
+
throw new Error(
|
|
1220
|
+
`Circular import detected: ${normalizedPath}
|
|
1221
|
+
Import chain: ${[...visitedFiles].join(" \u2192 ")} \u2192 ${normalizedPath}`
|
|
1222
|
+
);
|
|
1223
|
+
}
|
|
1224
|
+
visitedFiles.add(normalizedPath);
|
|
1225
|
+
const imported = this.loadYamlFile(importPath);
|
|
1226
|
+
if (Array.isArray(imported)) {
|
|
1227
|
+
const resolvedArray = this.resolveArrayImports(imported, path6.dirname(importPath), visitedFiles);
|
|
1228
|
+
for (const resolvedItem of resolvedArray) {
|
|
1229
|
+
result.push(resolvedItem);
|
|
1230
|
+
}
|
|
1231
|
+
} else if (this.isPlainObject(imported)) {
|
|
1232
|
+
result.push(imported);
|
|
1233
|
+
this.resolveImports(imported, path6.dirname(importPath), visitedFiles);
|
|
1234
|
+
} else {
|
|
1235
|
+
throw new TypeError(
|
|
1236
|
+
`Import file ${importPath} must contain either an object or an array. Got: ${typeof imported}`
|
|
1237
|
+
);
|
|
1238
|
+
}
|
|
1239
|
+
visitedFiles.delete(normalizedPath);
|
|
1240
|
+
} else {
|
|
1241
|
+
result.push(item);
|
|
1242
|
+
if (this.isPlainObject(item)) {
|
|
1243
|
+
this.resolveImports(item, baseDir, visitedFiles);
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
}
|
|
1247
|
+
return result;
|
|
1248
|
+
}
|
|
1249
|
+
/**
|
|
1250
|
+
* Load YAML file with error handling
|
|
1251
|
+
*/
|
|
1252
|
+
loadYamlFile(filePath) {
|
|
1253
|
+
if (!fs2.existsSync(filePath)) {
|
|
1254
|
+
throw new Error(`Import file not found: ${filePath}`);
|
|
1255
|
+
}
|
|
1256
|
+
try {
|
|
1257
|
+
const content = fs2.readFileSync(filePath, "utf8");
|
|
1258
|
+
const parsed = yaml3.parse(content);
|
|
1259
|
+
if (parsed === null || parsed === void 0) {
|
|
1260
|
+
throw new TypeError(`Import file ${filePath} is empty`);
|
|
1261
|
+
}
|
|
1262
|
+
return parsed;
|
|
1263
|
+
} catch (error) {
|
|
1264
|
+
if (error instanceof Error) {
|
|
1265
|
+
throw new TypeError(`Failed to parse YAML file ${filePath}: ${error.message}`);
|
|
1266
|
+
}
|
|
1267
|
+
throw error;
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
/**
|
|
1271
|
+
* Recursively resolve all $ref in an object/array structure
|
|
1272
|
+
* Handles both object references { $ref: "path" } and string references "$ref:path"
|
|
1273
|
+
*
|
|
1274
|
+
* @param current - Current node being processed (starts with root config, then recurses into children)
|
|
1275
|
+
* @param rootConfig - Root config object (constant reference for resolving paths like "imported.integrations.xxx")
|
|
1276
|
+
* @param path - Current path in config tree (for error messages, e.g., "resources.assistants[0].toolkits")
|
|
1277
|
+
*/
|
|
1278
|
+
resolveReferencesRecursive(current, rootConfig, path13 = "") {
|
|
1279
|
+
if (!current || typeof current !== "object") {
|
|
1280
|
+
return;
|
|
1281
|
+
}
|
|
1282
|
+
if (Array.isArray(current)) {
|
|
1283
|
+
this.resolveArrayReferences(current, rootConfig, path13);
|
|
1284
|
+
return;
|
|
1285
|
+
}
|
|
1286
|
+
if ("$ref" in current && typeof current.$ref === "string") {
|
|
1287
|
+
this.resolveObjectReference(current, rootConfig, path13);
|
|
1288
|
+
return;
|
|
1289
|
+
}
|
|
1290
|
+
this.resolveObjectProperties(current, rootConfig, path13);
|
|
1291
|
+
}
|
|
1292
|
+
/**
|
|
1293
|
+
* Resolve $ref items in arrays and flatten if they point to arrays
|
|
1294
|
+
* Example: [{ $ref: "context_definitions.repos" }] where repos is [item1, item2]
|
|
1295
|
+
* becomes [item1, item2]
|
|
1296
|
+
*/
|
|
1297
|
+
resolveArrayReferences(arr, rootConfig, path13) {
|
|
1298
|
+
const result = arr.flatMap((item, i) => {
|
|
1299
|
+
const contextPath = `${path13}[${i}]`;
|
|
1300
|
+
if (!this.isRefObject(item) || item.$ref.startsWith("#")) {
|
|
1301
|
+
this.resolveReferencesRecursive(item, rootConfig, contextPath);
|
|
1302
|
+
return [item];
|
|
1303
|
+
}
|
|
1304
|
+
const resolved = this.resolveReference(rootConfig, item.$ref, contextPath);
|
|
1305
|
+
if (Array.isArray(resolved)) {
|
|
1306
|
+
return structuredClone(resolved);
|
|
1307
|
+
}
|
|
1308
|
+
this.resolveObjectReference(item, rootConfig, contextPath);
|
|
1309
|
+
return [item];
|
|
1310
|
+
});
|
|
1311
|
+
arr.splice(0, arr.length, ...result);
|
|
1312
|
+
}
|
|
1313
|
+
/**
|
|
1314
|
+
* Resolve object reference: { $ref: "path" }
|
|
1315
|
+
* Replaces object with resolved data (in-place mutation)
|
|
1316
|
+
*/
|
|
1317
|
+
resolveObjectReference(current, rootConfig, path13) {
|
|
1318
|
+
const refPath = current.$ref;
|
|
1319
|
+
const contextPath = path13 || "root";
|
|
1320
|
+
if (refPath.startsWith("#")) {
|
|
1321
|
+
return;
|
|
1322
|
+
}
|
|
1323
|
+
const resolved = this.resolveReference(rootConfig, refPath, contextPath);
|
|
1324
|
+
const filteredResolved = Object.fromEntries(Object.entries(resolved).filter(([, value]) => value !== void 0));
|
|
1325
|
+
for (const key of Object.keys(current)) {
|
|
1326
|
+
delete current[key];
|
|
1327
|
+
}
|
|
1328
|
+
Object.assign(current, filteredResolved);
|
|
1329
|
+
this.resolveReferencesRecursive(current, rootConfig, path13);
|
|
1330
|
+
}
|
|
1331
|
+
/**
|
|
1332
|
+
* Resolve object properties recursively
|
|
1333
|
+
* Handles both nested objects and "$ref:path" string references
|
|
1334
|
+
*/
|
|
1335
|
+
resolveObjectProperties(current, rootConfig, path13) {
|
|
1336
|
+
for (const [key, value] of Object.entries(current)) {
|
|
1337
|
+
if (typeof value === "string" && value.startsWith("$ref:")) {
|
|
1338
|
+
const refPath = value.slice(5);
|
|
1339
|
+
if (refPath.startsWith("#")) {
|
|
1340
|
+
continue;
|
|
1341
|
+
}
|
|
1342
|
+
const contextPath = path13 ? `${path13}.${key}` : key;
|
|
1343
|
+
const resolved = this.resolveReference(rootConfig, refPath, contextPath);
|
|
1344
|
+
current[key] = resolved;
|
|
1345
|
+
} else {
|
|
1346
|
+
this.resolveReferencesRecursive(value, rootConfig, path13 ? `${path13}.${key}` : key);
|
|
1347
|
+
}
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
/**
|
|
1351
|
+
* Validate that datasource setting_id fields are strings (not objects)
|
|
1352
|
+
* For datasources, setting_id must reference a string UUID, not an integration object
|
|
1353
|
+
* Users should use .id suffix: $ref:imported.integrations.git_conn.id
|
|
1354
|
+
*/
|
|
1355
|
+
validateDatasourceIntegrationReferences(config) {
|
|
1356
|
+
if (!config.resources.datasources) {
|
|
1357
|
+
return;
|
|
1358
|
+
}
|
|
1359
|
+
for (const datasource of config.resources.datasources) {
|
|
1360
|
+
if (datasource.setting_id && typeof datasource.setting_id !== "string") {
|
|
1361
|
+
throw new TypeError(
|
|
1362
|
+
`Datasource "${datasource.name}" setting_id must point to a string UUID value, but resolved to an object. Add ".id" suffix to reference the integration ID. Example: setting_id: $ref:imported.integrations.git_conn.id`
|
|
1363
|
+
);
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
}
|
|
1367
|
+
/**
|
|
1368
|
+
* Resolve a reference path like "imported.integrations.jira_main" or "tool_definitions.jira_tool"
|
|
1369
|
+
* Supports nested paths like "imported.integrations.jira_main.id" to access specific fields
|
|
1370
|
+
* Returns the referenced object or value from config
|
|
1371
|
+
*
|
|
1372
|
+
* Special handling for arrays:
|
|
1373
|
+
* - imported.integrations (array): searches by 'alias' field
|
|
1374
|
+
* - imported.assistants (array): searches by 'name' field
|
|
1375
|
+
* - imported.datasources (array): searches by 'name' field
|
|
1376
|
+
*/
|
|
1377
|
+
resolveReference(config, ref, context) {
|
|
1378
|
+
const parts = ref.split(".");
|
|
1379
|
+
if (parts.length < 2) {
|
|
1380
|
+
throw new Error(
|
|
1381
|
+
`Invalid $ref format: "${ref}". Expected format: "imported.integrations.name" or "tool_definitions.name". You can also use nested paths like "imported.integrations.name.id". Referenced in ${context}.`
|
|
1382
|
+
);
|
|
1383
|
+
}
|
|
1384
|
+
let current = config;
|
|
1385
|
+
const pathParts = [];
|
|
1386
|
+
for (let i = 0; i < parts.length; i++) {
|
|
1387
|
+
const part = parts[i];
|
|
1388
|
+
pathParts.push(part);
|
|
1389
|
+
if (!current || typeof current !== "object" || !(part in current)) {
|
|
1390
|
+
throw new Error(
|
|
1391
|
+
`Reference path "${ref}" not found: "${pathParts.join(".")}" does not exist. Referenced in ${context}.`
|
|
1392
|
+
);
|
|
1393
|
+
}
|
|
1394
|
+
current = current[part];
|
|
1395
|
+
const hasMorePathSegments = i + 1 < parts.length;
|
|
1396
|
+
if (Array.isArray(current) && hasMorePathSegments) {
|
|
1397
|
+
const nextPathSegment = parts[i + 1];
|
|
1398
|
+
const searchField = part === "integrations" ? "alias" : "name";
|
|
1399
|
+
const foundItem = current.find(
|
|
1400
|
+
(item) => typeof item === "object" && item !== null && item[searchField] === nextPathSegment
|
|
1401
|
+
);
|
|
1402
|
+
if (!foundItem) {
|
|
1403
|
+
throw new Error(
|
|
1404
|
+
`Reference path "${ref}" not found: no item with ${searchField}="${nextPathSegment}" in "${pathParts.join(".")}". Referenced in ${context}.`
|
|
1405
|
+
);
|
|
1406
|
+
}
|
|
1407
|
+
current = foundItem;
|
|
1408
|
+
pathParts.push(nextPathSegment);
|
|
1409
|
+
i++;
|
|
1410
|
+
}
|
|
1411
|
+
}
|
|
1412
|
+
return current;
|
|
1413
|
+
}
|
|
1414
|
+
/**
|
|
1415
|
+
* Apply datasource defaults based on $ref or type
|
|
1416
|
+
* Priority: $ref (explicit) > type (fallback)
|
|
1417
|
+
*/
|
|
1418
|
+
applyDatasourceDefaults(config) {
|
|
1419
|
+
if (!config.datasource_defaults || !config.resources.datasources) {
|
|
1420
|
+
return;
|
|
1421
|
+
}
|
|
1422
|
+
config.resources.datasources = config.resources.datasources.map((datasource) => {
|
|
1423
|
+
let defaults = null;
|
|
1424
|
+
if (datasource.$ref) {
|
|
1425
|
+
const refPath = datasource.$ref.startsWith("$ref:") ? datasource.$ref.slice(5) : datasource.$ref;
|
|
1426
|
+
defaults = this.resolveReference(config, refPath, `datasource "${datasource.name}".$ref`);
|
|
1427
|
+
if (!defaults || typeof defaults !== "object") {
|
|
1428
|
+
throw new Error(
|
|
1429
|
+
`Invalid $ref in datasource "${datasource.name}": "${refPath}" does not point to a valid datasource defaults object`
|
|
1430
|
+
);
|
|
1431
|
+
}
|
|
1432
|
+
} else if (datasource.type) {
|
|
1433
|
+
defaults = config.datasource_defaults[datasource.type];
|
|
1434
|
+
}
|
|
1435
|
+
if (!defaults) {
|
|
1436
|
+
return datasource;
|
|
1437
|
+
}
|
|
1438
|
+
const { $ref: _ref, ...datasourceWithoutRef } = datasource;
|
|
1439
|
+
return {
|
|
1440
|
+
...defaults,
|
|
1441
|
+
...datasourceWithoutRef
|
|
1442
|
+
};
|
|
1443
|
+
});
|
|
1444
|
+
}
|
|
1445
|
+
/**
|
|
1446
|
+
* Type guard to check if value is a plain object (not array, not null)
|
|
1447
|
+
*/
|
|
1448
|
+
isPlainObject(value) {
|
|
1449
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1450
|
+
}
|
|
1451
|
+
isRefObject(item) {
|
|
1452
|
+
return this.isPlainObject(item) && "$ref" in item && typeof item.$ref === "string";
|
|
1453
|
+
}
|
|
1454
|
+
/**
|
|
1455
|
+
* Substitute environment variables in configuration
|
|
1456
|
+
* Supports syntax:
|
|
1457
|
+
* - ${VAR_NAME} - required variable (throws if not set)
|
|
1458
|
+
* - ${VAR_NAME:-default} - optional with default value (shell-style)
|
|
1459
|
+
* - ${VAR_NAME:default} - optional with default value (simplified)
|
|
1460
|
+
* - ${VAR_NAME:-} or ${VAR_NAME:} - optional with empty string default
|
|
1461
|
+
*/
|
|
1462
|
+
substituteEnvVars(obj) {
|
|
1463
|
+
for (const key in obj) {
|
|
1464
|
+
if (typeof obj[key] === "string") {
|
|
1465
|
+
obj[key] = obj[key].replaceAll(
|
|
1466
|
+
/\$\{([^:}]+)(?::-?([^}]*))?\}/g,
|
|
1467
|
+
(match, varName, defaultValue) => {
|
|
1468
|
+
const value = process.env[varName];
|
|
1469
|
+
if (value === void 0) {
|
|
1470
|
+
if (defaultValue !== void 0) {
|
|
1471
|
+
return defaultValue;
|
|
1472
|
+
}
|
|
1473
|
+
throw new Error(`Environment variable ${varName} is not set`);
|
|
1474
|
+
}
|
|
1475
|
+
return value;
|
|
1476
|
+
}
|
|
1477
|
+
);
|
|
1478
|
+
} else if (this.isPlainObject(obj[key])) {
|
|
1479
|
+
this.substituteEnvVars(obj[key]);
|
|
1480
|
+
}
|
|
1481
|
+
}
|
|
1482
|
+
}
|
|
1483
|
+
};
|
|
1484
|
+
function createConcurrentLimiter(maxConcurrent = RATE_LIMITING.MAX_CONCURRENT_REQUESTS) {
|
|
1485
|
+
return pLimit(maxConcurrent);
|
|
1486
|
+
}
|
|
1487
|
+
async function withRetry(fn, operation, maxAttempts = RATE_LIMITING.RETRY_ATTEMPTS) {
|
|
1488
|
+
let lastError;
|
|
1489
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
1490
|
+
try {
|
|
1491
|
+
return await fn();
|
|
1492
|
+
} catch (error) {
|
|
1493
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
1494
|
+
const errorMsg = lastError.message.toLowerCase();
|
|
1495
|
+
const isRetryable = lastError.message.includes("429") || errorMsg.includes("too many requests") || /\b5\d{2}\b/.test(lastError.message) || // 5xx HTTP status codes only
|
|
1496
|
+
errorMsg.includes("timeout") || errorMsg.includes("econnreset") || errorMsg.includes("econnrefused");
|
|
1497
|
+
if (!isRetryable || attempt === maxAttempts) {
|
|
1498
|
+
throw lastError;
|
|
1499
|
+
}
|
|
1500
|
+
const delayMs = RATE_LIMITING.RETRY_DELAY_MS * 2 ** (attempt - 1);
|
|
1501
|
+
logger.warn(` \u26A0\uFE0F Retry ${attempt}/${maxAttempts} for ${operation} after ${delayMs}ms...`);
|
|
1502
|
+
await new Promise((resolve4) => {
|
|
1503
|
+
const timerId = setTimeout(resolve4, delayMs);
|
|
1504
|
+
timerId.unref();
|
|
1505
|
+
});
|
|
1506
|
+
}
|
|
1507
|
+
}
|
|
1508
|
+
throw lastError || new Error(`Failed after ${maxAttempts} attempts: ${operation}`);
|
|
1509
|
+
}
|
|
1510
|
+
|
|
1511
|
+
// src/lib/timeoutUtils.ts
|
|
1512
|
+
async function withTimeout(promise, timeoutMs, operation) {
|
|
1513
|
+
let timeoutId;
|
|
1514
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
1515
|
+
timeoutId = setTimeout(() => reject(new Error(`Operation timeout: ${operation} (${timeoutMs}ms)`)), timeoutMs);
|
|
1516
|
+
timeoutId.unref();
|
|
1517
|
+
});
|
|
1518
|
+
try {
|
|
1519
|
+
return await Promise.race([promise, timeoutPromise]);
|
|
1520
|
+
} finally {
|
|
1521
|
+
if (timeoutId !== void 0) {
|
|
1522
|
+
clearTimeout(timeoutId);
|
|
1523
|
+
}
|
|
1524
|
+
}
|
|
1525
|
+
}
|
|
1526
|
+
|
|
1527
|
+
// src/backup.ts
|
|
1528
|
+
async function* streamResources(fetchPage, resourceType) {
|
|
1529
|
+
let page = 0;
|
|
1530
|
+
let hasMore = true;
|
|
1531
|
+
let totalFetched = 0;
|
|
1532
|
+
logger.info(` Streaming ${resourceType} (with pagination)...`);
|
|
1533
|
+
while (hasMore) {
|
|
1534
|
+
const resources = await fetchPage({
|
|
1535
|
+
per_page: PAGINATION.DEFAULT_PAGE_SIZE,
|
|
1536
|
+
page
|
|
1537
|
+
});
|
|
1538
|
+
if (resources.length === 0) {
|
|
1539
|
+
break;
|
|
1540
|
+
}
|
|
1541
|
+
for (const resource of resources) {
|
|
1542
|
+
totalFetched++;
|
|
1543
|
+
yield resource;
|
|
1544
|
+
}
|
|
1545
|
+
if (resources.length < PAGINATION.DEFAULT_PAGE_SIZE) {
|
|
1546
|
+
hasMore = false;
|
|
1547
|
+
} else {
|
|
1548
|
+
page++;
|
|
1549
|
+
logger.info(` Streamed ${totalFetched} ${resourceType} so far...`);
|
|
1550
|
+
}
|
|
1551
|
+
}
|
|
1552
|
+
}
|
|
1553
|
+
async function saveAssistantToBackup(assistant, client, backupData, backupDir) {
|
|
1554
|
+
logger.info(` \u2022 ${assistant.name} (${assistant.id})`);
|
|
1555
|
+
const full = await withTimeout(
|
|
1556
|
+
client.assistants.get(assistant.id),
|
|
1557
|
+
TIMEOUTS_MS.ASSISTANT_FETCH,
|
|
1558
|
+
`Timeout fetching assistant ${assistant.id}`
|
|
1559
|
+
);
|
|
1560
|
+
backupData.resources.assistants.push(full);
|
|
1561
|
+
if (full.system_prompt) {
|
|
1562
|
+
const fileName = `${full.slug || sanitizeFileName(full.name)}.prompt.md`;
|
|
1563
|
+
const filePath = path6.join(backupDir, "system_prompts", fileName);
|
|
1564
|
+
ensureDirectoryExists(filePath);
|
|
1565
|
+
fs2.writeFileSync(filePath, full.system_prompt, "utf8");
|
|
1566
|
+
}
|
|
1567
|
+
}
|
|
1568
|
+
async function backupAssistants(client, backupData, backupDir, transaction) {
|
|
1569
|
+
logger.info("\u{1F916} Fetching assistants...");
|
|
1570
|
+
const allAssistants = [];
|
|
1571
|
+
for await (const assistant of streamResources((params) => client.assistants.list(params), "assistants")) {
|
|
1572
|
+
allAssistants.push(assistant);
|
|
1573
|
+
}
|
|
1574
|
+
logger.info(` Found ${allAssistants.length} assistant(s)`);
|
|
1575
|
+
transaction.setTotal("assistants", allAssistants.length);
|
|
1576
|
+
const limit = createConcurrentLimiter();
|
|
1577
|
+
for (const assistant of allAssistants) {
|
|
1578
|
+
if (transaction.isCompleted("assistants", assistant.id)) {
|
|
1579
|
+
logger.info(` \u21B7 Skipping ${assistant.name} (already backed up)`);
|
|
1580
|
+
continue;
|
|
1581
|
+
}
|
|
1582
|
+
try {
|
|
1583
|
+
await limit(
|
|
1584
|
+
() => withRetry(
|
|
1585
|
+
async () => saveAssistantToBackup(assistant, client, backupData, backupDir),
|
|
1586
|
+
`Backup assistant ${assistant.name}`
|
|
1587
|
+
)
|
|
1588
|
+
);
|
|
1589
|
+
transaction.markCompleted("assistants", assistant.id);
|
|
1590
|
+
} catch (error) {
|
|
1591
|
+
const errorDetails = error instanceof Error ? { message: error.message, stack: error.stack, name: error.name } : { message: String(error) };
|
|
1592
|
+
logger.error(` \u274C Failed to backup ${assistant.name}:`);
|
|
1593
|
+
logger.error(` ${errorDetails.message}`);
|
|
1594
|
+
if (errorDetails.stack) {
|
|
1595
|
+
logger.error(` Stack trace: ${errorDetails.stack.split("\n").slice(1, 3).join("\n ")}`);
|
|
1596
|
+
}
|
|
1597
|
+
transaction.markFailed("assistants", assistant.id, errorDetails.message);
|
|
1598
|
+
}
|
|
1599
|
+
}
|
|
1600
|
+
logger.info(`\u2713 Backed up ${transaction.getData().resources.assistants.completed.length} assistant(s)
|
|
1601
|
+
`);
|
|
1602
|
+
}
|
|
1603
|
+
async function processDatasourceBackup(datasource, client, backupData, transaction) {
|
|
1604
|
+
logger.info(` \u2022 ${datasource.name} (${datasource.id})`);
|
|
1605
|
+
const full = await withTimeout(
|
|
1606
|
+
client.datasources.get(datasource.id),
|
|
1607
|
+
TIMEOUTS_MS.DATASOURCE_FETCH,
|
|
1608
|
+
`Timeout fetching datasource ${datasource.id}`
|
|
1609
|
+
);
|
|
1610
|
+
backupData.resources.datasources.push(full);
|
|
1611
|
+
transaction.markCompleted("datasources", datasource.id);
|
|
1612
|
+
}
|
|
1613
|
+
async function backupDatasources(client, backupData, transaction) {
|
|
1614
|
+
logger.info("\u{1F4CA} Fetching datasources...");
|
|
1615
|
+
const datasources = [];
|
|
1616
|
+
for await (const datasource of streamResources((params) => client.datasources.list(params), "datasources")) {
|
|
1617
|
+
datasources.push(datasource);
|
|
1618
|
+
}
|
|
1619
|
+
logger.info(` Found ${datasources.length} datasource(s)`);
|
|
1620
|
+
transaction.setTotal("datasources", datasources.length);
|
|
1621
|
+
const limit = createConcurrentLimiter();
|
|
1622
|
+
for (const datasource of datasources) {
|
|
1623
|
+
if (transaction.isCompleted("datasources", datasource.id)) {
|
|
1624
|
+
logger.info(` \u21B7 Skipping ${datasource.name} (already backed up)`);
|
|
1625
|
+
continue;
|
|
1626
|
+
}
|
|
1627
|
+
try {
|
|
1628
|
+
await limit(
|
|
1629
|
+
() => withRetry(
|
|
1630
|
+
async () => processDatasourceBackup(datasource, client, backupData, transaction),
|
|
1631
|
+
`Backup datasource ${datasource.name}`
|
|
1632
|
+
)
|
|
1633
|
+
);
|
|
1634
|
+
} catch (error) {
|
|
1635
|
+
const errorDetails = error instanceof Error ? { message: error.message, stack: error.stack, name: error.name } : { message: String(error) };
|
|
1636
|
+
logger.error(` \u274C Failed to backup ${datasource.name}:`);
|
|
1637
|
+
logger.error(` ${errorDetails.message}`);
|
|
1638
|
+
if (errorDetails.stack) {
|
|
1639
|
+
logger.error(` Stack trace: ${errorDetails.stack.split("\n").slice(1, 3).join("\n ")}`);
|
|
1640
|
+
}
|
|
1641
|
+
transaction.markFailed("datasources", datasource.id, errorDetails.message);
|
|
1642
|
+
}
|
|
1643
|
+
}
|
|
1644
|
+
logger.info(`\u2713 Backed up ${transaction.getData().resources.datasources.completed.length} datasource(s)
|
|
1645
|
+
`);
|
|
1646
|
+
}
|
|
1647
|
+
async function backupWorkflows(client, backupData, backupDir, transaction) {
|
|
1648
|
+
logger.info("\u{1F504} Fetching workflows...");
|
|
1649
|
+
const workflows = [];
|
|
1650
|
+
for await (const workflow of streamResources((params) => client.workflows.list(params), "workflows")) {
|
|
1651
|
+
workflows.push(workflow);
|
|
1652
|
+
}
|
|
1653
|
+
logger.info(` Found ${workflows.length} workflow(s)`);
|
|
1654
|
+
transaction.setTotal("workflows", workflows.length);
|
|
1655
|
+
for (const workflow of workflows) {
|
|
1656
|
+
if (transaction.isCompleted("workflows", workflow.id)) {
|
|
1657
|
+
logger.info(` \u21B7 Skipping ${workflow.name} (already backed up)`);
|
|
1658
|
+
continue;
|
|
1659
|
+
}
|
|
1660
|
+
try {
|
|
1661
|
+
logger.info(` \u2022 ${workflow.name} (${workflow.id})`);
|
|
1662
|
+
const full = await withTimeout(
|
|
1663
|
+
client.workflows.get(workflow.id),
|
|
1664
|
+
TIMEOUTS_MS.WORKFLOW_FETCH,
|
|
1665
|
+
`Timeout fetching workflow ${workflow.id}`
|
|
1666
|
+
);
|
|
1667
|
+
backupData.resources.workflows.push(full);
|
|
1668
|
+
if (full.yaml_config) {
|
|
1669
|
+
const fileName = `${sanitizeFileName(full.name)}.yaml`;
|
|
1670
|
+
const filePath = path6.join(backupDir, "workflows", fileName);
|
|
1671
|
+
ensureDirectoryExists(filePath);
|
|
1672
|
+
fs2.writeFileSync(filePath, full.yaml_config, "utf8");
|
|
1673
|
+
}
|
|
1674
|
+
transaction.markCompleted("workflows", workflow.id);
|
|
1675
|
+
} catch (error) {
|
|
1676
|
+
const errorDetails = error instanceof Error ? { message: error.message, stack: error.stack, name: error.name } : { message: String(error) };
|
|
1677
|
+
logger.error(` \u274C Failed to backup ${workflow.name}:`);
|
|
1678
|
+
logger.error(` ${errorDetails.message}`);
|
|
1679
|
+
if (errorDetails.stack) {
|
|
1680
|
+
logger.error(` Stack trace: ${errorDetails.stack.split("\n").slice(1, 3).join("\n ")}`);
|
|
1681
|
+
}
|
|
1682
|
+
transaction.markFailed("workflows", workflow.id, errorDetails.message);
|
|
1683
|
+
}
|
|
1684
|
+
}
|
|
1685
|
+
logger.info(`\u2713 Backed up ${transaction.getData().resources.workflows.completed.length} workflow(s)
|
|
1686
|
+
`);
|
|
1687
|
+
}
|
|
1688
|
+
async function backupIntegrations(client, backupData) {
|
|
1689
|
+
logger.info("\u{1F50C} Fetching integrations...");
|
|
1690
|
+
const projectIntegrations = await withTimeout(
|
|
1691
|
+
client.integrations.list({
|
|
1692
|
+
per_page: PAGINATION.DEFAULT_PAGE_SIZE,
|
|
1693
|
+
page: 0,
|
|
1694
|
+
setting_type: "project"
|
|
1695
|
+
}),
|
|
1696
|
+
TIMEOUTS_MS.INTEGRATION_FETCH,
|
|
1697
|
+
"Timeout fetching project integrations"
|
|
1698
|
+
);
|
|
1699
|
+
logger.info(` Found ${projectIntegrations.length} project integration(s)`);
|
|
1700
|
+
for (const integration of projectIntegrations) {
|
|
1701
|
+
logger.info(` \u2022 ${integration.alias || integration.credential_type} (${integration.id}) [project]`);
|
|
1702
|
+
backupData.resources.integrations.push(integration);
|
|
1703
|
+
}
|
|
1704
|
+
const userIntegrations = await withTimeout(
|
|
1705
|
+
client.integrations.list({
|
|
1706
|
+
per_page: PAGINATION.DEFAULT_PAGE_SIZE,
|
|
1707
|
+
page: 0,
|
|
1708
|
+
setting_type: "user"
|
|
1709
|
+
}),
|
|
1710
|
+
TIMEOUTS_MS.INTEGRATION_FETCH,
|
|
1711
|
+
"Timeout fetching user integrations"
|
|
1712
|
+
);
|
|
1713
|
+
logger.info(` Found ${userIntegrations.length} user integration(s)`);
|
|
1714
|
+
for (const integration of userIntegrations) {
|
|
1715
|
+
logger.info(` \u2022 ${integration.alias || integration.credential_type} (${integration.id}) [user]`);
|
|
1716
|
+
backupData.resources.integrations.push(integration);
|
|
1717
|
+
}
|
|
1718
|
+
logger.info(`\u2713 Backed up ${projectIntegrations.length + userIntegrations.length} integration(s)
|
|
1719
|
+
`);
|
|
1720
|
+
}
|
|
1721
|
+
function getUniqueBackupDir(baseDir, timestamp) {
|
|
1722
|
+
let counter = 0;
|
|
1723
|
+
let finalBackupDir = path6.join(baseDir, timestamp);
|
|
1724
|
+
while (fs2.existsSync(finalBackupDir)) {
|
|
1725
|
+
counter++;
|
|
1726
|
+
finalBackupDir = path6.join(baseDir, `${timestamp}-${counter}`);
|
|
1727
|
+
}
|
|
1728
|
+
const dirSuffix = counter > 0 ? `${timestamp}-${counter}` : timestamp;
|
|
1729
|
+
const tempBackupDir = path6.join(baseDir, `${BACKUP.TEMP_DIR_PREFIX}${dirSuffix}`);
|
|
1730
|
+
return { finalDir: finalBackupDir, tempDir: tempBackupDir };
|
|
1731
|
+
}
|
|
1732
|
+
function performCleanup(tempBackupDir, transaction) {
|
|
1733
|
+
if (fs2.existsSync(tempBackupDir)) {
|
|
1734
|
+
try {
|
|
1735
|
+
logger.info(`\u{1F9F9} Rolling back: cleaning up ${tempBackupDir}...`);
|
|
1736
|
+
cleanupDirectory(tempBackupDir);
|
|
1737
|
+
logger.info("\u2713 Rollback completed");
|
|
1738
|
+
} catch (cleanupError) {
|
|
1739
|
+
logger.error(
|
|
1740
|
+
"\u26A0\uFE0F Failed to cleanup backup directory:",
|
|
1741
|
+
cleanupError instanceof Error ? cleanupError.message : String(cleanupError)
|
|
1742
|
+
);
|
|
1743
|
+
}
|
|
1744
|
+
}
|
|
1745
|
+
if (transaction) {
|
|
1746
|
+
try {
|
|
1747
|
+
transaction.cleanup();
|
|
1748
|
+
logger.info("\u2713 Transaction file cleaned up");
|
|
1749
|
+
} catch (cleanupError) {
|
|
1750
|
+
logger.error(
|
|
1751
|
+
"\u26A0\uFE0F Failed to cleanup transaction file:",
|
|
1752
|
+
cleanupError instanceof Error ? cleanupError.message : String(cleanupError)
|
|
1753
|
+
);
|
|
1754
|
+
}
|
|
1755
|
+
}
|
|
1756
|
+
}
|
|
1757
|
+
async function backupResources(options) {
|
|
1758
|
+
logger.info("\u{1F5C4}\uFE0F Creating backup of all Codemie resources...\n");
|
|
1759
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/\.\d{3}Z$/, "").replaceAll(":", "-");
|
|
1760
|
+
const { appConfig } = options;
|
|
1761
|
+
const { rootDir, backupsDirectory } = appConfig;
|
|
1762
|
+
const backupBaseDir = path6.join(rootDir, backupsDirectory);
|
|
1763
|
+
const { finalDir: finalBackupDir, tempDir: tempBackupDir } = getUniqueBackupDir(backupBaseDir, timestamp);
|
|
1764
|
+
let transaction = null;
|
|
1765
|
+
try {
|
|
1766
|
+
validateBackupDirectory(backupBaseDir);
|
|
1767
|
+
const loader = new CodemieConfigLoader(appConfig);
|
|
1768
|
+
const config = loader.loadConfig();
|
|
1769
|
+
logger.info("\u{1F50C} Connecting to Codemie API...");
|
|
1770
|
+
const client = await createClient(config);
|
|
1771
|
+
logger.info("\u2713 Connected to Codemie API\n");
|
|
1772
|
+
if (!fs2.existsSync(tempBackupDir)) {
|
|
1773
|
+
fs2.mkdirSync(tempBackupDir, { recursive: true });
|
|
1774
|
+
}
|
|
1775
|
+
logger.info(`\u{1F4C1} Temporary backup directory: ${tempBackupDir}
|
|
1776
|
+
`);
|
|
1777
|
+
transaction = new BackupTransaction(tempBackupDir);
|
|
1778
|
+
const backupData = {
|
|
1779
|
+
metadata: {
|
|
1780
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1781
|
+
projectName: config.project.name,
|
|
1782
|
+
codemieApiUrl: config.environment.codemie_api_url
|
|
1783
|
+
},
|
|
1784
|
+
resources: {
|
|
1785
|
+
assistants: [],
|
|
1786
|
+
datasources: [],
|
|
1787
|
+
workflows: [],
|
|
1788
|
+
integrations: []
|
|
1789
|
+
},
|
|
1790
|
+
state: {
|
|
1791
|
+
version: "1.0",
|
|
1792
|
+
project: config.project.name,
|
|
1793
|
+
lastSync: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1794
|
+
resources: {
|
|
1795
|
+
assistants: {},
|
|
1796
|
+
datasources: {},
|
|
1797
|
+
workflows: {}
|
|
1798
|
+
}
|
|
1799
|
+
}
|
|
1800
|
+
};
|
|
1801
|
+
await backupAssistants(client, backupData, tempBackupDir, transaction);
|
|
1802
|
+
await backupDatasources(client, backupData, transaction);
|
|
1803
|
+
await backupWorkflows(client, backupData, tempBackupDir, transaction);
|
|
1804
|
+
await backupIntegrations(client, backupData);
|
|
1805
|
+
const stats = transaction.getData();
|
|
1806
|
+
const totalFailed = stats.resources.assistants.failed.length + stats.resources.datasources.failed.length + stats.resources.workflows.failed.length;
|
|
1807
|
+
if (totalFailed > 0) {
|
|
1808
|
+
logger.warn(`
|
|
1809
|
+
\u26A0\uFE0F Backup completed with ${totalFailed} failed resource(s)`);
|
|
1810
|
+
logger.warn("Review transaction.json for details\n");
|
|
1811
|
+
const totalResources = stats.resources.assistants.total + stats.resources.datasources.total + stats.resources.workflows.total;
|
|
1812
|
+
const failureRate = totalResources > 0 ? totalFailed / totalResources * 100 : 0;
|
|
1813
|
+
if (failureRate > 20) {
|
|
1814
|
+
throw new Error(
|
|
1815
|
+
`Too many failures (${totalFailed}/${totalResources} = ${failureRate.toFixed(1)}%), backup aborted`
|
|
1816
|
+
);
|
|
1817
|
+
}
|
|
1818
|
+
}
|
|
1819
|
+
saveBackupFiles(backupData, tempBackupDir, config.project.name);
|
|
1820
|
+
transaction.complete();
|
|
1821
|
+
logger.info(`
|
|
1822
|
+
\u{1F504} Finalizing backup...`);
|
|
1823
|
+
moveAtomically(tempBackupDir, finalBackupDir);
|
|
1824
|
+
logger.info(`\u2713 Backup finalized at ${finalBackupDir}
|
|
1825
|
+
`);
|
|
1826
|
+
const separator = "=".repeat(50);
|
|
1827
|
+
logger.info(`${separator}`);
|
|
1828
|
+
logger.info("\u{1F4CA} Backup Summary:\n");
|
|
1829
|
+
logger.info(` \u{1F916} Assistants: ${backupData.resources.assistants.length}`);
|
|
1830
|
+
logger.info(` \u{1F4CA} Datasources: ${backupData.resources.datasources.length}`);
|
|
1831
|
+
logger.info(` \u{1F504} Workflows: ${backupData.resources.workflows.length}`);
|
|
1832
|
+
logger.info(` \u{1F50C} Integrations: ${backupData.resources.integrations.length}`);
|
|
1833
|
+
logger.info(`
|
|
1834
|
+
\u{1F4C1} Location: ${finalBackupDir}
|
|
1835
|
+
`);
|
|
1836
|
+
logger.info("\u{1F4C8} Detailed Progress:\n");
|
|
1837
|
+
logger.info(transaction.getSummary());
|
|
1838
|
+
logger.info("");
|
|
1839
|
+
transaction.cleanup();
|
|
1840
|
+
logger.info("\u2705 Backup completed successfully!");
|
|
1841
|
+
} catch (error) {
|
|
1842
|
+
logger.error("\n\u274C Backup failed:");
|
|
1843
|
+
logger.error(error instanceof Error ? error.message : String(error));
|
|
1844
|
+
performCleanup(tempBackupDir, transaction);
|
|
1845
|
+
throw error;
|
|
1846
|
+
}
|
|
1847
|
+
}
|
|
1848
|
+
async function main(options) {
|
|
1849
|
+
try {
|
|
1850
|
+
await backupResources(options);
|
|
1851
|
+
process.exit(0);
|
|
1852
|
+
} catch {
|
|
1853
|
+
process.exit(1);
|
|
1854
|
+
}
|
|
1855
|
+
}
|
|
1856
|
+
|
|
1857
|
+
// src/lib/paginationUtils.ts
|
|
1858
|
+
async function findResourceByName(listFn, name, resourceType) {
|
|
1859
|
+
let page = 0;
|
|
1860
|
+
const perPage = 100;
|
|
1861
|
+
let totalChecked = 0;
|
|
1862
|
+
while (true) {
|
|
1863
|
+
const resources = await listFn({ page, per_page: perPage });
|
|
1864
|
+
totalChecked += resources.length;
|
|
1865
|
+
const found = resources.find((r) => r.name === name);
|
|
1866
|
+
if (found && found.id) {
|
|
1867
|
+
return found;
|
|
1868
|
+
}
|
|
1869
|
+
if (resources.length < perPage) {
|
|
1870
|
+
logger.warn(
|
|
1871
|
+
` \u26A0\uFE0F ${resourceType} "${name}" not found after checking ${totalChecked} resources across ${page + 1} page(s)`
|
|
1872
|
+
);
|
|
1873
|
+
return null;
|
|
1874
|
+
}
|
|
1875
|
+
page++;
|
|
1876
|
+
if (page > 1e3) {
|
|
1877
|
+
logger.warn(` \u26A0\uFE0F Stopped pagination after 1000 pages (${totalChecked} resources checked)`);
|
|
1878
|
+
return null;
|
|
1879
|
+
}
|
|
1880
|
+
}
|
|
1881
|
+
}
|
|
1882
|
+
async function findDatasourceByName(client, name) {
|
|
1883
|
+
const datasource = await findResourceByName((params) => client.datasources.list(params), name, "datasource");
|
|
1884
|
+
return datasource?.id || null;
|
|
1885
|
+
}
|
|
1886
|
+
async function findAssistantByName(client, name) {
|
|
1887
|
+
const assistant = await findResourceByName((params) => client.assistants.list(params), name, "assistant");
|
|
1888
|
+
return assistant?.id || null;
|
|
1889
|
+
}
|
|
1890
|
+
|
|
1891
|
+
// src/lib/assistantHelpers.ts
|
|
1892
|
+
async function createAssistantAndGetId(client, params, slug) {
|
|
1893
|
+
await client.assistants.create(params);
|
|
1894
|
+
if (slug) {
|
|
1895
|
+
const created = await client.assistants.getBySlug(slug);
|
|
1896
|
+
if (!created?.id) {
|
|
1897
|
+
throw new Error(`Assistant created but not found by slug: ${slug}`);
|
|
1898
|
+
}
|
|
1899
|
+
return created.id;
|
|
1900
|
+
}
|
|
1901
|
+
const assistantId = await findAssistantByName(client, params.name);
|
|
1902
|
+
if (!assistantId) {
|
|
1903
|
+
throw new Error(`Assistant created but not found by name: ${params.name}`);
|
|
1904
|
+
}
|
|
1905
|
+
return assistantId;
|
|
1906
|
+
}
|
|
1907
|
+
|
|
1908
|
+
// src/lib/cleanupManager.ts
|
|
1909
|
+
var CleanupManager = class {
|
|
1910
|
+
constructor(client, stateManager) {
|
|
1911
|
+
this.client = client;
|
|
1912
|
+
this.stateManager = stateManager;
|
|
1913
|
+
}
|
|
1914
|
+
/**
|
|
1915
|
+
* Check if an error indicates that a resource was not found on the platform
|
|
1916
|
+
* Handles both proper 404 responses
|
|
1917
|
+
*/
|
|
1918
|
+
isNotFoundError(error) {
|
|
1919
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1920
|
+
if (errorMessage.includes("404") || errorMessage.includes("not found") || errorMessage.includes("Not Found")) {
|
|
1921
|
+
return true;
|
|
1922
|
+
}
|
|
1923
|
+
return false;
|
|
1924
|
+
}
|
|
1925
|
+
/**
|
|
1926
|
+
* Delete a resource with graceful handling for "not found" errors
|
|
1927
|
+
* Returns true if deleted or not found, false if other error occurred
|
|
1928
|
+
*/
|
|
1929
|
+
async deleteResourceSafely(resourceType, name, id, deleteFn) {
|
|
1930
|
+
try {
|
|
1931
|
+
await deleteFn();
|
|
1932
|
+
logger.info(` \u{1F5D1}\uFE0F Deleted orphaned ${resourceType}: ${name} (${id.slice(0, 8)}...)`);
|
|
1933
|
+
return { success: true, notFound: false };
|
|
1934
|
+
} catch (error) {
|
|
1935
|
+
if (this.isNotFoundError(error)) {
|
|
1936
|
+
logger.info(
|
|
1937
|
+
` \u26A0\uFE0F ${resourceType.charAt(0).toUpperCase() + resourceType.slice(1)} ${name} not found on platform (already deleted) - removing from state`
|
|
1938
|
+
);
|
|
1939
|
+
return { success: true, notFound: true };
|
|
1940
|
+
}
|
|
1941
|
+
throw error;
|
|
1942
|
+
}
|
|
1943
|
+
}
|
|
1944
|
+
/**
|
|
1945
|
+
* Find orphaned resources (in state but not in config)
|
|
1946
|
+
* These are resources that were managed by IaC but removed from codemie.yaml
|
|
1947
|
+
* Returns resource NAMES, not IDs
|
|
1948
|
+
*/
|
|
1949
|
+
findOrphanedResources(config) {
|
|
1950
|
+
const managedResources = this.stateManager.getAllManagedResources();
|
|
1951
|
+
const configAssistantNames = new Set((config.resources.assistants || []).map(({ name }) => name));
|
|
1952
|
+
const configDatasourceNames = new Set((config.resources.datasources || []).map(({ name }) => name));
|
|
1953
|
+
const configWorkflowNames = new Set((config.resources.workflows || []).map(({ name }) => name));
|
|
1954
|
+
return {
|
|
1955
|
+
assistants: managedResources.assistants.filter((name) => !configAssistantNames.has(name)),
|
|
1956
|
+
datasources: managedResources.datasources.filter((name) => !configDatasourceNames.has(name)),
|
|
1957
|
+
workflows: managedResources.workflows.filter((name) => !configWorkflowNames.has(name))
|
|
1958
|
+
};
|
|
1959
|
+
}
|
|
1960
|
+
/**
|
|
1961
|
+
* Delete orphaned resources from platform
|
|
1962
|
+
* SAFETY: Only deletes resources that are in state.json (managed by IaC)
|
|
1963
|
+
* @param orphaned Object with resource NAMES (not IDs)
|
|
1964
|
+
*/
|
|
1965
|
+
// eslint-disable-next-line max-lines-per-function
|
|
1966
|
+
async deleteOrphanedResources(orphaned) {
|
|
1967
|
+
const result = {
|
|
1968
|
+
deleted: {
|
|
1969
|
+
assistants: [],
|
|
1970
|
+
datasources: [],
|
|
1971
|
+
workflows: []
|
|
1972
|
+
},
|
|
1973
|
+
errors: []
|
|
1974
|
+
};
|
|
1975
|
+
for (const name of orphaned.assistants) {
|
|
1976
|
+
try {
|
|
1977
|
+
if (!this.stateManager.isManagedResource("assistant", name)) {
|
|
1978
|
+
logger.info(` \u26A0\uFE0F Skipping ${name} - not in state (safety check)`);
|
|
1979
|
+
continue;
|
|
1980
|
+
}
|
|
1981
|
+
const id = this.stateManager.getIdByName("assistant", name);
|
|
1982
|
+
if (!id) {
|
|
1983
|
+
logger.info(` \u26A0\uFE0F Skipping ${name} - no ID in state`);
|
|
1984
|
+
continue;
|
|
1985
|
+
}
|
|
1986
|
+
await this.deleteResourceSafely("assistant", name, id, () => this.client.assistants.delete(id));
|
|
1987
|
+
this.stateManager.deleteAssistantState(name);
|
|
1988
|
+
result.deleted.assistants.push(name);
|
|
1989
|
+
} catch (error) {
|
|
1990
|
+
result.errors.push({
|
|
1991
|
+
type: "assistant",
|
|
1992
|
+
name,
|
|
1993
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1994
|
+
});
|
|
1995
|
+
logger.error(
|
|
1996
|
+
` \u274C Failed to delete assistant ${name}: ${error instanceof Error ? error.message : String(error)}`
|
|
1997
|
+
);
|
|
1998
|
+
}
|
|
1999
|
+
}
|
|
2000
|
+
for (const name of orphaned.datasources) {
|
|
2001
|
+
try {
|
|
2002
|
+
if (!this.stateManager.isManagedResource("datasource", name)) {
|
|
2003
|
+
logger.info(` \u26A0\uFE0F Skipping ${name} - not in state (safety check)`);
|
|
2004
|
+
continue;
|
|
2005
|
+
}
|
|
2006
|
+
const id = this.stateManager.getIdByName("datasource", name);
|
|
2007
|
+
if (!id) {
|
|
2008
|
+
logger.info(` \u26A0\uFE0F Skipping ${name} - no ID in state`);
|
|
2009
|
+
continue;
|
|
2010
|
+
}
|
|
2011
|
+
await this.deleteResourceSafely("datasource", name, id, () => this.client.datasources.delete(id));
|
|
2012
|
+
this.stateManager.deleteDatasourceState(name);
|
|
2013
|
+
result.deleted.datasources.push(name);
|
|
2014
|
+
} catch (error) {
|
|
2015
|
+
result.errors.push({
|
|
2016
|
+
type: "datasource",
|
|
2017
|
+
name,
|
|
2018
|
+
error: error instanceof Error ? error.message : String(error)
|
|
2019
|
+
});
|
|
2020
|
+
logger.error(
|
|
2021
|
+
` \u274C Failed to delete datasource ${name}: ${error instanceof Error ? error.message : String(error)}`
|
|
2022
|
+
);
|
|
2023
|
+
}
|
|
2024
|
+
}
|
|
2025
|
+
for (const name of orphaned.workflows) {
|
|
2026
|
+
try {
|
|
2027
|
+
if (!this.stateManager.isManagedResource("workflow", name)) {
|
|
2028
|
+
logger.info(` \u26A0\uFE0F Skipping ${name} - not in state (safety check)`);
|
|
2029
|
+
continue;
|
|
2030
|
+
}
|
|
2031
|
+
const id = this.stateManager.getIdByName("workflow", name);
|
|
2032
|
+
if (!id) {
|
|
2033
|
+
logger.info(` \u26A0\uFE0F Skipping ${name} - no ID in state`);
|
|
2034
|
+
continue;
|
|
2035
|
+
}
|
|
2036
|
+
await this.deleteResourceSafely("workflow", name, id, () => this.client.workflows.delete(id));
|
|
2037
|
+
this.stateManager.deleteWorkflowState(name);
|
|
2038
|
+
result.deleted.workflows.push(name);
|
|
2039
|
+
} catch (error) {
|
|
2040
|
+
result.errors.push({
|
|
2041
|
+
type: "workflow",
|
|
2042
|
+
name,
|
|
2043
|
+
error: error instanceof Error ? error.message : String(error)
|
|
2044
|
+
});
|
|
2045
|
+
logger.error(
|
|
2046
|
+
` \u274C Failed to delete workflow ${name}: ${error instanceof Error ? error.message : String(error)}`
|
|
2047
|
+
);
|
|
2048
|
+
}
|
|
2049
|
+
}
|
|
2050
|
+
return result;
|
|
2051
|
+
}
|
|
2052
|
+
/**
|
|
2053
|
+
* Get summary of orphaned resources
|
|
2054
|
+
*/
|
|
2055
|
+
getOrphanedSummary(orphaned) {
|
|
2056
|
+
const total = orphaned.assistants.length + orphaned.datasources.length + orphaned.workflows.length;
|
|
2057
|
+
if (total === 0) {
|
|
2058
|
+
return "No orphaned resources found";
|
|
2059
|
+
}
|
|
2060
|
+
const parts = [];
|
|
2061
|
+
if (orphaned.assistants.length > 0) {
|
|
2062
|
+
parts.push(`${orphaned.assistants.length} assistant(s)`);
|
|
2063
|
+
}
|
|
2064
|
+
if (orphaned.datasources.length > 0) {
|
|
2065
|
+
parts.push(`${orphaned.datasources.length} datasource(s)`);
|
|
2066
|
+
}
|
|
2067
|
+
if (orphaned.workflows.length > 0) {
|
|
2068
|
+
parts.push(`${orphaned.workflows.length} workflow(s)`);
|
|
2069
|
+
}
|
|
2070
|
+
return `Found ${total} orphaned resource(s): ${parts.join(", ")}`;
|
|
2071
|
+
}
|
|
2072
|
+
};
|
|
2073
|
+
|
|
2074
|
+
// src/lib/resourceExistenceChecker.ts
|
|
2075
|
+
async function checkResourceExists(getState, getResource) {
|
|
2076
|
+
const existingState = getState();
|
|
2077
|
+
if (!existingState) {
|
|
2078
|
+
return false;
|
|
2079
|
+
}
|
|
2080
|
+
try {
|
|
2081
|
+
await getResource(existingState.id);
|
|
2082
|
+
return true;
|
|
2083
|
+
} catch {
|
|
2084
|
+
return false;
|
|
2085
|
+
}
|
|
2086
|
+
}
|
|
2087
|
+
function checkAssistantExists(client, name, stateManager) {
|
|
2088
|
+
return checkResourceExists(
|
|
2089
|
+
() => stateManager.getAssistantState(name),
|
|
2090
|
+
(id) => client.assistants.get(id)
|
|
2091
|
+
);
|
|
2092
|
+
}
|
|
2093
|
+
function checkDatasourceExists(client, name, stateManager) {
|
|
2094
|
+
return checkResourceExists(
|
|
2095
|
+
() => stateManager.getDatasourceState(name),
|
|
2096
|
+
(id) => client.datasources.get(id)
|
|
2097
|
+
);
|
|
2098
|
+
}
|
|
2099
|
+
function checkWorkflowExists(client, name, stateManager) {
|
|
2100
|
+
return checkResourceExists(
|
|
2101
|
+
() => stateManager.getWorkflowState(name),
|
|
2102
|
+
(id) => client.workflows.get(id)
|
|
2103
|
+
);
|
|
2104
|
+
}
|
|
2105
|
+
var StateManager = class {
|
|
2106
|
+
statePath;
|
|
2107
|
+
constructor(appConfig) {
|
|
2108
|
+
const { rootDir, codemieState } = appConfig;
|
|
2109
|
+
this.statePath = path6.join(rootDir, codemieState);
|
|
2110
|
+
}
|
|
2111
|
+
/**
|
|
2112
|
+
* Load state file
|
|
2113
|
+
*/
|
|
2114
|
+
loadState() {
|
|
2115
|
+
if (!fs2.existsSync(this.statePath)) {
|
|
2116
|
+
return this.createEmptyState();
|
|
2117
|
+
}
|
|
2118
|
+
const content = fs2.readFileSync(this.statePath, "utf8");
|
|
2119
|
+
const state = JSON.parse(content);
|
|
2120
|
+
if (!state.resources) {
|
|
2121
|
+
state.resources = {
|
|
2122
|
+
assistants: {},
|
|
2123
|
+
datasources: {},
|
|
2124
|
+
workflows: {}
|
|
2125
|
+
};
|
|
2126
|
+
}
|
|
2127
|
+
if (!state.resources.assistants) {
|
|
2128
|
+
state.resources.assistants = {};
|
|
2129
|
+
}
|
|
2130
|
+
if (!state.resources.datasources) {
|
|
2131
|
+
state.resources.datasources = {};
|
|
2132
|
+
}
|
|
2133
|
+
if (!state.resources.workflows) {
|
|
2134
|
+
state.resources.workflows = {};
|
|
2135
|
+
}
|
|
2136
|
+
return state;
|
|
2137
|
+
}
|
|
2138
|
+
/**
|
|
2139
|
+
* Save state file
|
|
2140
|
+
*/
|
|
2141
|
+
saveState(state) {
|
|
2142
|
+
const dir = path6.dirname(this.statePath);
|
|
2143
|
+
if (!fs2.existsSync(dir)) {
|
|
2144
|
+
fs2.mkdirSync(dir, { recursive: true });
|
|
2145
|
+
}
|
|
2146
|
+
state.lastSync = (/* @__PURE__ */ new Date()).toISOString();
|
|
2147
|
+
fs2.writeFileSync(this.statePath, JSON.stringify(state, null, 2));
|
|
2148
|
+
}
|
|
2149
|
+
/**
|
|
2150
|
+
* Create empty state structure
|
|
2151
|
+
*/
|
|
2152
|
+
createEmptyState() {
|
|
2153
|
+
return {
|
|
2154
|
+
version: "1.0",
|
|
2155
|
+
project: "",
|
|
2156
|
+
lastSync: null,
|
|
2157
|
+
resources: {
|
|
2158
|
+
assistants: {},
|
|
2159
|
+
datasources: {},
|
|
2160
|
+
workflows: {}
|
|
2161
|
+
}
|
|
2162
|
+
};
|
|
2163
|
+
}
|
|
2164
|
+
/**
|
|
2165
|
+
* Update assistant state (keyed by NAME)
|
|
2166
|
+
* @param name
|
|
2167
|
+
* @param id
|
|
2168
|
+
* @param promptContent
|
|
2169
|
+
* @param assistantResource - The assistant resource object (used to calculate consistent checksum)
|
|
2170
|
+
* @param buildConfig - Optional build-time configuration
|
|
2171
|
+
*/
|
|
2172
|
+
updateAssistantState(name, id, promptContent, assistantResource, buildConfig = null) {
|
|
2173
|
+
const state = this.loadState();
|
|
2174
|
+
state.resources.assistants[name] = {
|
|
2175
|
+
id,
|
|
2176
|
+
lastDeployed: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2177
|
+
promptChecksum: calculateChecksum(promptContent),
|
|
2178
|
+
configChecksum: calculateAssistantConfigChecksum(assistantResource, buildConfig)
|
|
2179
|
+
};
|
|
2180
|
+
this.saveState(state);
|
|
2181
|
+
}
|
|
2182
|
+
/**
|
|
2183
|
+
* Get assistant state by NAME
|
|
2184
|
+
*/
|
|
2185
|
+
getAssistantState(name) {
|
|
2186
|
+
const state = this.loadState();
|
|
2187
|
+
return state.resources.assistants[name];
|
|
2188
|
+
}
|
|
2189
|
+
/**
|
|
2190
|
+
* Delete assistant state by NAME
|
|
2191
|
+
*/
|
|
2192
|
+
deleteAssistantState(name) {
|
|
2193
|
+
const state = this.loadState();
|
|
2194
|
+
delete state.resources.assistants[name];
|
|
2195
|
+
this.saveState(state);
|
|
2196
|
+
}
|
|
2197
|
+
/**
|
|
2198
|
+
* Update datasource state (keyed by NAME)
|
|
2199
|
+
* @param datasourceResource - The datasource resource object (used to calculate consistent checksum)
|
|
2200
|
+
*/
|
|
2201
|
+
updateDatasourceState(name, id, datasourceResource) {
|
|
2202
|
+
const state = this.loadState();
|
|
2203
|
+
state.resources.datasources[name] = {
|
|
2204
|
+
id,
|
|
2205
|
+
lastDeployed: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2206
|
+
configChecksum: calculateDatasourceConfigChecksum(datasourceResource)
|
|
2207
|
+
};
|
|
2208
|
+
this.saveState(state);
|
|
2209
|
+
}
|
|
2210
|
+
/**
|
|
2211
|
+
* Get datasource state by NAME
|
|
2212
|
+
*/
|
|
2213
|
+
getDatasourceState(name) {
|
|
2214
|
+
const state = this.loadState();
|
|
2215
|
+
return state.resources.datasources[name];
|
|
2216
|
+
}
|
|
2217
|
+
/**
|
|
2218
|
+
* Delete datasource state by NAME
|
|
2219
|
+
*/
|
|
2220
|
+
deleteDatasourceState(name) {
|
|
2221
|
+
const state = this.loadState();
|
|
2222
|
+
delete state.resources.datasources[name];
|
|
2223
|
+
this.saveState(state);
|
|
2224
|
+
}
|
|
2225
|
+
/**
|
|
2226
|
+
* Update workflow state (keyed by NAME)
|
|
2227
|
+
*/
|
|
2228
|
+
updateWorkflowState(name, id, workflowYamlChecksum, configChecksum) {
|
|
2229
|
+
const state = this.loadState();
|
|
2230
|
+
state.resources.workflows[name] = {
|
|
2231
|
+
id,
|
|
2232
|
+
lastDeployed: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2233
|
+
workflowYamlChecksum,
|
|
2234
|
+
configChecksum
|
|
2235
|
+
};
|
|
2236
|
+
this.saveState(state);
|
|
2237
|
+
}
|
|
2238
|
+
/**
|
|
2239
|
+
* Get workflow state by NAME
|
|
2240
|
+
*/
|
|
2241
|
+
getWorkflowState(name) {
|
|
2242
|
+
const state = this.loadState();
|
|
2243
|
+
return state.resources.workflows[name];
|
|
2244
|
+
}
|
|
2245
|
+
/**
|
|
2246
|
+
* Delete workflow state by NAME
|
|
2247
|
+
*/
|
|
2248
|
+
deleteWorkflowState(name) {
|
|
2249
|
+
const state = this.loadState();
|
|
2250
|
+
delete state.resources.workflows[name];
|
|
2251
|
+
this.saveState(state);
|
|
2252
|
+
}
|
|
2253
|
+
/**
|
|
2254
|
+
* Get all managed resources (for cleanup/destroy)
|
|
2255
|
+
* Returns: { assistants: [name1, name2], datasources: [name1], workflows: [name1] }
|
|
2256
|
+
*/
|
|
2257
|
+
getAllManagedResources() {
|
|
2258
|
+
const state = this.loadState();
|
|
2259
|
+
return {
|
|
2260
|
+
assistants: Object.keys(state.resources.assistants),
|
|
2261
|
+
datasources: Object.keys(state.resources.datasources),
|
|
2262
|
+
workflows: Object.keys(state.resources.workflows)
|
|
2263
|
+
};
|
|
2264
|
+
}
|
|
2265
|
+
/**
|
|
2266
|
+
* Check if a resource is managed by IaC (exists in state.json)
|
|
2267
|
+
* @param type Resource type ('assistant', 'datasource', 'workflow')
|
|
2268
|
+
* @param name Resource name
|
|
2269
|
+
*/
|
|
2270
|
+
isManagedResource(type, name) {
|
|
2271
|
+
const state = this.loadState();
|
|
2272
|
+
switch (type) {
|
|
2273
|
+
case "assistant": {
|
|
2274
|
+
return name in state.resources.assistants;
|
|
2275
|
+
}
|
|
2276
|
+
case "datasource": {
|
|
2277
|
+
return name in state.resources.datasources;
|
|
2278
|
+
}
|
|
2279
|
+
case "workflow": {
|
|
2280
|
+
return name in state.resources.workflows;
|
|
2281
|
+
}
|
|
2282
|
+
default: {
|
|
2283
|
+
return false;
|
|
2284
|
+
}
|
|
2285
|
+
}
|
|
2286
|
+
}
|
|
2287
|
+
/**
|
|
2288
|
+
* Get ID by name for a specific resource type
|
|
2289
|
+
*/
|
|
2290
|
+
getIdByName(type, name) {
|
|
2291
|
+
const state = this.loadState();
|
|
2292
|
+
switch (type) {
|
|
2293
|
+
case "assistant": {
|
|
2294
|
+
return state.resources.assistants[name]?.id;
|
|
2295
|
+
}
|
|
2296
|
+
case "datasource": {
|
|
2297
|
+
return state.resources.datasources[name]?.id;
|
|
2298
|
+
}
|
|
2299
|
+
case "workflow": {
|
|
2300
|
+
return state.resources.workflows[name]?.id;
|
|
2301
|
+
}
|
|
2302
|
+
default: {
|
|
2303
|
+
return void 0;
|
|
2304
|
+
}
|
|
2305
|
+
}
|
|
2306
|
+
}
|
|
2307
|
+
};
|
|
2308
|
+
|
|
2309
|
+
// src/deploy.ts
|
|
2310
|
+
function sortAssistantsByDependencies(assistants) {
|
|
2311
|
+
const sorted = [];
|
|
2312
|
+
const visited = /* @__PURE__ */ new Set();
|
|
2313
|
+
const visiting = /* @__PURE__ */ new Set();
|
|
2314
|
+
const assistantMap = /* @__PURE__ */ new Map();
|
|
2315
|
+
for (const assistant of assistants) {
|
|
2316
|
+
assistantMap.set(assistant.name, assistant);
|
|
2317
|
+
}
|
|
2318
|
+
function visit(name) {
|
|
2319
|
+
if (visited.has(name)) {
|
|
2320
|
+
return;
|
|
2321
|
+
}
|
|
2322
|
+
if (visiting.has(name)) {
|
|
2323
|
+
throw new Error(`Circular dependency detected for assistant: ${name}`);
|
|
2324
|
+
}
|
|
2325
|
+
visiting.add(name);
|
|
2326
|
+
const assistant = assistantMap.get(name);
|
|
2327
|
+
if (assistant && assistant.sub_assistants) {
|
|
2328
|
+
for (const subName of assistant.sub_assistants) {
|
|
2329
|
+
if (!assistantMap.has(subName)) {
|
|
2330
|
+
throw new Error(
|
|
2331
|
+
`Sub-assistant "${subName}" referenced by "${name}" not found in config. Ensure all sub-assistants are defined in resources.assistants.`
|
|
2332
|
+
);
|
|
2333
|
+
}
|
|
2334
|
+
visit(subName);
|
|
2335
|
+
}
|
|
2336
|
+
}
|
|
2337
|
+
visiting.delete(name);
|
|
2338
|
+
visited.add(name);
|
|
2339
|
+
if (assistant) {
|
|
2340
|
+
sorted.push(assistant);
|
|
2341
|
+
}
|
|
2342
|
+
}
|
|
2343
|
+
for (const assistant of assistants) {
|
|
2344
|
+
visit(assistant.name);
|
|
2345
|
+
}
|
|
2346
|
+
return sorted;
|
|
2347
|
+
}
|
|
2348
|
+
async function deployAssistants(config, client, loader, stateManager) {
|
|
2349
|
+
const stats = { created: 0, updated: 0, unchanged: 0, failed: 0 };
|
|
2350
|
+
if (!config.resources.assistants) {
|
|
2351
|
+
return stats;
|
|
2352
|
+
}
|
|
2353
|
+
logger.info("\u{1F916} Processing assistants...\n");
|
|
2354
|
+
const sortedAssistants = sortAssistantsByDependencies(config.resources.assistants);
|
|
2355
|
+
logger.info(` Sorted ${sortedAssistants.length} assistant(s) by dependencies
|
|
2356
|
+
`);
|
|
2357
|
+
for (const assistant of sortedAssistants) {
|
|
2358
|
+
try {
|
|
2359
|
+
logger.info(`Processing: ${assistant.name}`);
|
|
2360
|
+
const promptContent = loader.loadPrompt(assistant.prompt);
|
|
2361
|
+
let buildConfig = null;
|
|
2362
|
+
if (assistant.config) {
|
|
2363
|
+
buildConfig = loader.loadAssistantConfig(assistant.config);
|
|
2364
|
+
}
|
|
2365
|
+
const configChecksum = calculateAssistantConfigChecksum(assistant, buildConfig);
|
|
2366
|
+
let resolvedAssistantIds = assistant.assistant_ids || [];
|
|
2367
|
+
if (assistant.sub_assistants && assistant.sub_assistants.length > 0) {
|
|
2368
|
+
logger.info(` Resolving ${assistant.sub_assistants.length} sub-assistant name(s)...`);
|
|
2369
|
+
const resolvedIds = [];
|
|
2370
|
+
for (const name of assistant.sub_assistants) {
|
|
2371
|
+
const subAssistantState = stateManager.getAssistantState(name);
|
|
2372
|
+
if (!subAssistantState) {
|
|
2373
|
+
throw new Error(`Sub-assistant "${name}" not found in state. Ensure the assistant is deployed first.`);
|
|
2374
|
+
}
|
|
2375
|
+
resolvedIds.push(subAssistantState.id);
|
|
2376
|
+
logger.info(` \u2713 Resolved "${name}" \u2192 ${subAssistantState.id}`);
|
|
2377
|
+
}
|
|
2378
|
+
resolvedAssistantIds = resolvedIds;
|
|
2379
|
+
}
|
|
2380
|
+
let resolvedContext = assistant.context || [];
|
|
2381
|
+
if (assistant.datasource_names && assistant.datasource_names.length > 0) {
|
|
2382
|
+
logger.info(` Resolving ${assistant.datasource_names.length} datasource name(s)...`);
|
|
2383
|
+
const datasourceContextEntries = [];
|
|
2384
|
+
const datasourcesInConfig = config.resources.datasources || [];
|
|
2385
|
+
const importedDatasources = config.imported?.datasources || [];
|
|
2386
|
+
const allDatasources = [...datasourcesInConfig, ...importedDatasources];
|
|
2387
|
+
for (const dsName of assistant.datasource_names) {
|
|
2388
|
+
const datasource = allDatasources.find((ds) => ds.name === dsName);
|
|
2389
|
+
if (!datasource) {
|
|
2390
|
+
throw new Error(`Datasource with name "${dsName}" not found in config (resources or imported)`);
|
|
2391
|
+
}
|
|
2392
|
+
const contextType = datasource.type === "code" ? "code" : "knowledge_base";
|
|
2393
|
+
datasourceContextEntries.push({
|
|
2394
|
+
context_type: contextType,
|
|
2395
|
+
name: datasource.name
|
|
2396
|
+
});
|
|
2397
|
+
logger.info(` \u2713 Resolved "${dsName}" \u2192 ${datasource.name} (${contextType})`);
|
|
2398
|
+
}
|
|
2399
|
+
resolvedContext = [...resolvedContext, ...datasourceContextEntries];
|
|
2400
|
+
}
|
|
2401
|
+
const assistantWithResolved = {
|
|
2402
|
+
...assistant,
|
|
2403
|
+
assistant_ids: resolvedAssistantIds,
|
|
2404
|
+
context: resolvedContext
|
|
2405
|
+
};
|
|
2406
|
+
const apiParams = assistantResourceToCreateParams(assistantWithResolved, config.project.name, promptContent);
|
|
2407
|
+
const existingState = stateManager.getAssistantState(assistant.name);
|
|
2408
|
+
if (existingState) {
|
|
2409
|
+
const existsOnPlatform = await checkAssistantExists(client, assistant.name, stateManager);
|
|
2410
|
+
if (existsOnPlatform) {
|
|
2411
|
+
const hasChanged = existingState.promptChecksum !== calculateChecksum(promptContent) || existingState.configChecksum !== configChecksum;
|
|
2412
|
+
if (hasChanged) {
|
|
2413
|
+
logger.info(` Updating assistant (ID: ${existingState.id})...`);
|
|
2414
|
+
if (process.env.DEBUG_API) {
|
|
2415
|
+
logger.debug("\n=== DEBUG: Update API Params ===");
|
|
2416
|
+
logger.debug(JSON.stringify(apiParams, null, 2));
|
|
2417
|
+
logger.debug("================================\n");
|
|
2418
|
+
}
|
|
2419
|
+
await client.assistants.update(existingState.id, apiParams);
|
|
2420
|
+
logger.info(`\u2713 Updated assistant: ${assistant.name} (${existingState.id})`);
|
|
2421
|
+
stateManager.updateAssistantState(assistant.name, existingState.id, promptContent, assistant, buildConfig);
|
|
2422
|
+
stats.updated++;
|
|
2423
|
+
} else {
|
|
2424
|
+
logger.info(` \u2713 No changes detected (ID: ${existingState.id})`);
|
|
2425
|
+
stats.unchanged++;
|
|
2426
|
+
}
|
|
2427
|
+
} else {
|
|
2428
|
+
logger.info(` \u26A0\uFE0F Assistant ID from state not found on platform, will create new`);
|
|
2429
|
+
logger.info(` Creating new assistant...`);
|
|
2430
|
+
if (process.env.DEBUG_API) {
|
|
2431
|
+
logger.debug("\n=== DEBUG: API Params ===");
|
|
2432
|
+
logger.debug(JSON.stringify(apiParams, null, 2));
|
|
2433
|
+
logger.debug("=========================\n");
|
|
2434
|
+
}
|
|
2435
|
+
const assistantId = await createAssistantAndGetId(client, apiParams, assistant.slug);
|
|
2436
|
+
logger.info(`\u2713 Created assistant: ${assistant.name} (${assistantId})`);
|
|
2437
|
+
stateManager.updateAssistantState(assistant.name, assistantId, promptContent, assistant, buildConfig);
|
|
2438
|
+
stats.created++;
|
|
2439
|
+
}
|
|
2440
|
+
} else {
|
|
2441
|
+
logger.info(` Creating new assistant...`);
|
|
2442
|
+
if (process.env.DEBUG_API) {
|
|
2443
|
+
logger.debug("\n=== DEBUG: API Params ===");
|
|
2444
|
+
logger.debug(JSON.stringify(apiParams, null, 2));
|
|
2445
|
+
logger.debug("=========================\n");
|
|
2446
|
+
}
|
|
2447
|
+
const assistantId = await createAssistantAndGetId(client, apiParams, assistant.slug);
|
|
2448
|
+
logger.info(`\u2713 Created assistant: ${assistant.name} (${assistantId})`);
|
|
2449
|
+
stateManager.updateAssistantState(assistant.name, assistantId, promptContent, assistant, buildConfig);
|
|
2450
|
+
stats.created++;
|
|
2451
|
+
}
|
|
2452
|
+
logger.info("");
|
|
2453
|
+
} catch (error) {
|
|
2454
|
+
logger.error(` \u274C Failed to deploy ${assistant.name}:`);
|
|
2455
|
+
if (error instanceof Error) {
|
|
2456
|
+
logger.error(` ${error.message}`);
|
|
2457
|
+
logger.debug(` Stack:`, error.stack);
|
|
2458
|
+
if ("response" in error) {
|
|
2459
|
+
const axiosError = error;
|
|
2460
|
+
logger.error(` Status: ${axiosError.response?.status}`);
|
|
2461
|
+
logger.error(` Data:`, JSON.stringify(axiosError.response?.data, null, 2));
|
|
2462
|
+
}
|
|
2463
|
+
} else {
|
|
2464
|
+
logger.error(` ${String(error)}`);
|
|
2465
|
+
}
|
|
2466
|
+
logger.info("");
|
|
2467
|
+
stats.failed++;
|
|
2468
|
+
}
|
|
2469
|
+
}
|
|
2470
|
+
return stats;
|
|
2471
|
+
}
|
|
2472
|
+
async function createDatasourceAndGetId(client, createParams, datasourceName) {
|
|
2473
|
+
await client.datasources.create(createParams);
|
|
2474
|
+
logger.info(`\u2713 Created datasource: ${datasourceName}`);
|
|
2475
|
+
logger.info(` Fetching datasource ID...`);
|
|
2476
|
+
const datasourceId = await findDatasourceByName(client, datasourceName);
|
|
2477
|
+
if (!datasourceId) {
|
|
2478
|
+
throw new Error(`Datasource created but not found by name: ${datasourceName}`);
|
|
2479
|
+
}
|
|
2480
|
+
logger.info(` Found ID: ${datasourceId}`);
|
|
2481
|
+
return datasourceId;
|
|
2482
|
+
}
|
|
2483
|
+
async function deployDatasources(config, client, stateManager) {
|
|
2484
|
+
const stats = { created: 0, updated: 0, unchanged: 0, failed: 0 };
|
|
2485
|
+
logger.info("\u{1F4CA} Processing datasources...\n");
|
|
2486
|
+
if (!config.resources.datasources) {
|
|
2487
|
+
return stats;
|
|
2488
|
+
}
|
|
2489
|
+
for (const datasource of config.resources.datasources) {
|
|
2490
|
+
try {
|
|
2491
|
+
logger.info(`Processing: ${datasource.name}`);
|
|
2492
|
+
const configChecksum = calculateDatasourceConfigChecksum(datasource);
|
|
2493
|
+
const createParams = datasourceResourceToCreateParams(datasource, config.project.name);
|
|
2494
|
+
const existingState = stateManager.getDatasourceState(datasource.name);
|
|
2495
|
+
if (existingState) {
|
|
2496
|
+
const existsOnPlatform = await checkDatasourceExists(client, datasource.name, stateManager);
|
|
2497
|
+
if (existsOnPlatform) {
|
|
2498
|
+
const hasChanged = existingState.configChecksum !== configChecksum;
|
|
2499
|
+
if (hasChanged || datasource.force_reindex) {
|
|
2500
|
+
if (datasource.force_reindex && !hasChanged) {
|
|
2501
|
+
logger.info(` Force reindexing datasource (ID: ${existingState.id})...`);
|
|
2502
|
+
} else {
|
|
2503
|
+
logger.info(` Updating datasource (ID: ${existingState.id})...`);
|
|
2504
|
+
}
|
|
2505
|
+
const updateParams = {
|
|
2506
|
+
...createParams,
|
|
2507
|
+
...datasource.force_reindex && { full_reindex: true }
|
|
2508
|
+
};
|
|
2509
|
+
await client.datasources.update(updateParams);
|
|
2510
|
+
logger.info(
|
|
2511
|
+
`\u2713 ${datasource.force_reindex && !hasChanged ? "Reindexed" : "Updated"} datasource: ${datasource.name} (${existingState.id})`
|
|
2512
|
+
);
|
|
2513
|
+
stateManager.updateDatasourceState(datasource.name, existingState.id, datasource);
|
|
2514
|
+
stats.updated++;
|
|
2515
|
+
} else {
|
|
2516
|
+
logger.info(` \u2713 No changes detected (ID: ${existingState.id})`);
|
|
2517
|
+
stats.unchanged++;
|
|
2518
|
+
}
|
|
2519
|
+
} else {
|
|
2520
|
+
logger.info(` \u26A0\uFE0F Datasource ID from state not found on platform, will create new`);
|
|
2521
|
+
logger.info(` Creating new datasource...`);
|
|
2522
|
+
const datasourceId = await createDatasourceAndGetId(client, createParams, datasource.name);
|
|
2523
|
+
stateManager.updateDatasourceState(datasource.name, datasourceId, datasource);
|
|
2524
|
+
stats.created++;
|
|
2525
|
+
}
|
|
2526
|
+
} else {
|
|
2527
|
+
logger.info(` Creating new datasource...`);
|
|
2528
|
+
const datasourceId = await createDatasourceAndGetId(client, createParams, datasource.name);
|
|
2529
|
+
stateManager.updateDatasourceState(datasource.name, datasourceId, datasource);
|
|
2530
|
+
stats.created++;
|
|
2531
|
+
}
|
|
2532
|
+
logger.info("");
|
|
2533
|
+
} catch (error) {
|
|
2534
|
+
logger.error(` \u274C Failed to deploy ${datasource.name}:`);
|
|
2535
|
+
if (error instanceof Error) {
|
|
2536
|
+
logger.error(` ${error.message}`);
|
|
2537
|
+
logger.debug(` Stack:`, error.stack);
|
|
2538
|
+
if ("response" in error) {
|
|
2539
|
+
const axiosError = error;
|
|
2540
|
+
logger.error(` Status: ${axiosError.response?.status}`);
|
|
2541
|
+
logger.error(` Data:`, JSON.stringify(axiosError.response?.data, null, 2));
|
|
2542
|
+
}
|
|
2543
|
+
} else {
|
|
2544
|
+
logger.error(` ${String(error)}`);
|
|
2545
|
+
}
|
|
2546
|
+
logger.info("");
|
|
2547
|
+
stats.failed++;
|
|
2548
|
+
}
|
|
2549
|
+
}
|
|
2550
|
+
return stats;
|
|
2551
|
+
}
|
|
2552
|
+
async function deployWorkflows(config, client, stateManager, rootDir = process.cwd()) {
|
|
2553
|
+
const stats = { created: 0, updated: 0, unchanged: 0, failed: 0 };
|
|
2554
|
+
logger.info("\u{1F504} Processing workflows...\n");
|
|
2555
|
+
if (!config.resources.workflows) {
|
|
2556
|
+
return stats;
|
|
2557
|
+
}
|
|
2558
|
+
for (const workflow of config.resources.workflows) {
|
|
2559
|
+
try {
|
|
2560
|
+
logger.info(`Processing: ${workflow.name}`);
|
|
2561
|
+
const yamlPath = path6.join(rootDir, workflow.definition);
|
|
2562
|
+
if (!fs2.existsSync(yamlPath)) {
|
|
2563
|
+
throw new Error(`Workflow definition file not found: ${workflow.definition}`);
|
|
2564
|
+
}
|
|
2565
|
+
const yamlConfigContent = fs2.readFileSync(yamlPath, "utf8");
|
|
2566
|
+
const workflowYaml = yaml3.parse(yamlConfigContent);
|
|
2567
|
+
const referencesToResolve = [];
|
|
2568
|
+
for (const assistant of workflowYaml.assistants) {
|
|
2569
|
+
const isInline = !assistant.assistant_name && assistant.model && assistant.system_prompt;
|
|
2570
|
+
if (isInline) {
|
|
2571
|
+
logger.info(` Skipping inline assistant: ${assistant.id}`);
|
|
2572
|
+
continue;
|
|
2573
|
+
}
|
|
2574
|
+
if (!assistant.assistant_id) {
|
|
2575
|
+
let resolvedId;
|
|
2576
|
+
if (assistant.assistant_name) {
|
|
2577
|
+
const assistantState = stateManager.getAssistantState(assistant.assistant_name);
|
|
2578
|
+
if (assistantState) {
|
|
2579
|
+
resolvedId = assistantState.id;
|
|
2580
|
+
logger.info(` Resolved (name): ${assistant.assistant_name} \u2192 ${resolvedId.slice(0, 8)}...`);
|
|
2581
|
+
} else {
|
|
2582
|
+
const importedAssistant = config.imported?.assistants?.find(
|
|
2583
|
+
(a) => a.name === assistant.assistant_name
|
|
2584
|
+
);
|
|
2585
|
+
if (importedAssistant && importedAssistant.id) {
|
|
2586
|
+
resolvedId = importedAssistant.id;
|
|
2587
|
+
logger.info(` Resolved (imported): ${assistant.assistant_name} \u2192 ${resolvedId.slice(0, 8)}...`);
|
|
2588
|
+
} else {
|
|
2589
|
+
throw new Error(
|
|
2590
|
+
`Assistant "${assistant.assistant_name}" not found in state or imported. Ensure the assistant is deployed first or added to imported.assistants.`
|
|
2591
|
+
);
|
|
2592
|
+
}
|
|
2593
|
+
}
|
|
2594
|
+
} else {
|
|
2595
|
+
throw new Error(`Workflow assistant reference must have 'assistant_name' (local id: ${assistant.id})`);
|
|
2596
|
+
}
|
|
2597
|
+
assistant.assistant_id = resolvedId;
|
|
2598
|
+
const referenceName = assistant.assistant_name;
|
|
2599
|
+
delete assistant.assistant_name;
|
|
2600
|
+
referencesToResolve.push({
|
|
2601
|
+
localId: assistant.id,
|
|
2602
|
+
reference: referenceName || assistant.id,
|
|
2603
|
+
uuid: resolvedId
|
|
2604
|
+
});
|
|
2605
|
+
}
|
|
2606
|
+
}
|
|
2607
|
+
const yamlConfig = yaml3.stringify(workflowYaml);
|
|
2608
|
+
const workflowYamlChecksum = calculateChecksum(yamlConfigContent);
|
|
2609
|
+
const configChecksum = calculateWorkflowConfigChecksum(workflow);
|
|
2610
|
+
const existingState = stateManager.getWorkflowState(workflow.name);
|
|
2611
|
+
if (existingState) {
|
|
2612
|
+
const existsOnPlatform = await checkWorkflowExists(client, workflow.name, stateManager);
|
|
2613
|
+
if (existsOnPlatform) {
|
|
2614
|
+
const hasChanged = existingState.workflowYamlChecksum !== workflowYamlChecksum || existingState.configChecksum !== configChecksum;
|
|
2615
|
+
if (hasChanged) {
|
|
2616
|
+
const apiParams = {
|
|
2617
|
+
project: config.project.name,
|
|
2618
|
+
name: workflow.name,
|
|
2619
|
+
description: workflow.description,
|
|
2620
|
+
yaml_config: yamlConfig,
|
|
2621
|
+
mode: workflow.mode,
|
|
2622
|
+
shared: workflow.shared,
|
|
2623
|
+
icon_url: workflow.icon_url
|
|
2624
|
+
};
|
|
2625
|
+
if (process.env.DEBUG_API) {
|
|
2626
|
+
logger.debug("\n=== DEBUG: API Params ===");
|
|
2627
|
+
logger.debug(JSON.stringify(apiParams, null, 2));
|
|
2628
|
+
logger.debug("=========================\n");
|
|
2629
|
+
}
|
|
2630
|
+
logger.info(` Updating workflow (ID: ${existingState.id})...`);
|
|
2631
|
+
await client.workflows.update(existingState.id, apiParams);
|
|
2632
|
+
logger.info(`\u2713 Updated workflow: ${workflow.name} (${existingState.id})`);
|
|
2633
|
+
stateManager.updateWorkflowState(workflow.name, existingState.id, workflowYamlChecksum, configChecksum);
|
|
2634
|
+
stats.updated++;
|
|
2635
|
+
} else {
|
|
2636
|
+
logger.info(` \u2713 No changes detected (ID: ${existingState.id})`);
|
|
2637
|
+
stats.unchanged++;
|
|
2638
|
+
}
|
|
2639
|
+
} else {
|
|
2640
|
+
logger.info(` \u26A0\uFE0F Workflow ID from state not found on platform, will create new`);
|
|
2641
|
+
logger.info(` Creating new workflow...`);
|
|
2642
|
+
const apiParams = {
|
|
2643
|
+
project: config.project.name,
|
|
2644
|
+
name: workflow.name,
|
|
2645
|
+
description: workflow.description,
|
|
2646
|
+
yaml_config: yamlConfig,
|
|
2647
|
+
mode: workflow.mode || "Sequential",
|
|
2648
|
+
shared: workflow.shared ?? true,
|
|
2649
|
+
icon_url: workflow.icon_url
|
|
2650
|
+
};
|
|
2651
|
+
if (process.env.DEBUG_API) {
|
|
2652
|
+
logger.debug("\n=== DEBUG: API Params ===");
|
|
2653
|
+
logger.debug(JSON.stringify(apiParams, null, 2));
|
|
2654
|
+
logger.debug("=========================\n");
|
|
2655
|
+
}
|
|
2656
|
+
const result = await client.workflows.create(apiParams);
|
|
2657
|
+
const workflowId = result.data?.id || result.id;
|
|
2658
|
+
if (!workflowId) {
|
|
2659
|
+
throw new Error("Workflow created but ID not returned");
|
|
2660
|
+
}
|
|
2661
|
+
logger.info(`\u2713 Created workflow: ${workflow.name} (${workflowId})`);
|
|
2662
|
+
stateManager.updateWorkflowState(workflow.name, workflowId, workflowYamlChecksum, configChecksum);
|
|
2663
|
+
stats.created++;
|
|
2664
|
+
}
|
|
2665
|
+
} else {
|
|
2666
|
+
logger.info(` Creating new workflow...`);
|
|
2667
|
+
const apiParams = {
|
|
2668
|
+
project: config.project.name,
|
|
2669
|
+
name: workflow.name,
|
|
2670
|
+
description: workflow.description,
|
|
2671
|
+
yaml_config: yamlConfig,
|
|
2672
|
+
mode: workflow.mode || "Sequential",
|
|
2673
|
+
shared: workflow.shared ?? true,
|
|
2674
|
+
icon_url: workflow.icon_url
|
|
2675
|
+
};
|
|
2676
|
+
if (process.env.DEBUG_API) {
|
|
2677
|
+
logger.debug("\n=== DEBUG: API Params ===");
|
|
2678
|
+
logger.debug(JSON.stringify(apiParams, null, 2));
|
|
2679
|
+
logger.debug("=========================\n");
|
|
2680
|
+
}
|
|
2681
|
+
const result = await client.workflows.create(apiParams);
|
|
2682
|
+
const workflowId = result.data?.id || result.id;
|
|
2683
|
+
if (!workflowId) {
|
|
2684
|
+
throw new Error("Workflow created but ID not returned");
|
|
2685
|
+
}
|
|
2686
|
+
logger.info(`\u2713 Created workflow: ${workflow.name} (${workflowId})`);
|
|
2687
|
+
stateManager.updateWorkflowState(workflow.name, workflowId, workflowYamlChecksum, configChecksum);
|
|
2688
|
+
stats.created++;
|
|
2689
|
+
}
|
|
2690
|
+
logger.info("");
|
|
2691
|
+
} catch (error) {
|
|
2692
|
+
logger.error(` \u274C Failed to deploy ${workflow.name}:`);
|
|
2693
|
+
if (error instanceof Error) {
|
|
2694
|
+
logger.error(` ${error.message}`);
|
|
2695
|
+
logger.debug(` Stack:`, error.stack);
|
|
2696
|
+
if ("response" in error) {
|
|
2697
|
+
const axiosError = error;
|
|
2698
|
+
logger.error(` Status: ${axiosError.response?.status}`);
|
|
2699
|
+
logger.error(` Data:`, JSON.stringify(axiosError.response?.data, null, 2));
|
|
2700
|
+
}
|
|
2701
|
+
} else {
|
|
2702
|
+
logger.error(` ${String(error)}`);
|
|
2703
|
+
}
|
|
2704
|
+
logger.info("");
|
|
2705
|
+
stats.failed++;
|
|
2706
|
+
}
|
|
2707
|
+
}
|
|
2708
|
+
return stats;
|
|
2709
|
+
}
|
|
2710
|
+
async function deployResources(options) {
|
|
2711
|
+
logger.info("\u{1F680} Deploying resources to Codemie...\n");
|
|
2712
|
+
try {
|
|
2713
|
+
const { appConfig, prune = false } = options;
|
|
2714
|
+
const loader = new CodemieConfigLoader(appConfig);
|
|
2715
|
+
const stateManager = new StateManager(appConfig);
|
|
2716
|
+
logger.info("\u{1F4C4} Loading configuration...");
|
|
2717
|
+
const config = loader.loadConfig();
|
|
2718
|
+
logger.info(`\u2713 Loaded config for project: ${config.project.name}
|
|
2719
|
+
`);
|
|
2720
|
+
logger.info("\u{1F50C} Connecting to Codemie API...");
|
|
2721
|
+
const client = await createClient(config);
|
|
2722
|
+
logger.info("\u2713 Connected to Codemie API\n");
|
|
2723
|
+
logger.info("\u{1F9F9} Checking for orphaned resources...");
|
|
2724
|
+
const cleanupManager = new CleanupManager(client, stateManager);
|
|
2725
|
+
const orphaned = cleanupManager.findOrphanedResources(config);
|
|
2726
|
+
const totalOrphaned = orphaned.assistants.length + orphaned.datasources.length + orphaned.workflows.length;
|
|
2727
|
+
let deleted = 0;
|
|
2728
|
+
if (totalOrphaned > 0) {
|
|
2729
|
+
logger.info(`
|
|
2730
|
+
\u26A0\uFE0F Found ${totalOrphaned} orphaned resource(s):`);
|
|
2731
|
+
if (orphaned.assistants.length > 0) {
|
|
2732
|
+
logger.info(` \u2022 ${orphaned.assistants.length} assistant(s)`);
|
|
2733
|
+
}
|
|
2734
|
+
if (orphaned.datasources.length > 0) {
|
|
2735
|
+
logger.info(` \u2022 ${orphaned.datasources.length} datasource(s)`);
|
|
2736
|
+
}
|
|
2737
|
+
if (orphaned.workflows.length > 0) {
|
|
2738
|
+
logger.info(` \u2022 ${orphaned.workflows.length} workflow(s)`);
|
|
2739
|
+
}
|
|
2740
|
+
if (process.env.SAMPLE_DEPLOY === "1") {
|
|
2741
|
+
logger.info("\n\u{1F50E} SAMPLE_DEPLOY=1 -> Skipping orphan deletion (simulation / partial deploy mode)\n");
|
|
2742
|
+
} else if (prune) {
|
|
2743
|
+
logger.info("\n\u{1F5D1}\uFE0F Deleting orphaned resources (removed from config)...\n");
|
|
2744
|
+
const cleanupResult = await cleanupManager.deleteOrphanedResources(orphaned);
|
|
2745
|
+
deleted = cleanupResult.deleted.assistants.length + cleanupResult.deleted.datasources.length + cleanupResult.deleted.workflows.length;
|
|
2746
|
+
if (deleted > 0) {
|
|
2747
|
+
logger.info(`
|
|
2748
|
+
\u2713 Deleted ${deleted} orphaned resource(s)
|
|
2749
|
+
`);
|
|
2750
|
+
}
|
|
2751
|
+
if (cleanupResult.errors.length > 0) {
|
|
2752
|
+
logger.info(`
|
|
2753
|
+
\u26A0\uFE0F ${cleanupResult.errors.length} error(s) during cleanup
|
|
2754
|
+
`);
|
|
2755
|
+
}
|
|
2756
|
+
} else {
|
|
2757
|
+
logger.info("\n\u26A0\uFE0F Orphaned resources found but not deleted.");
|
|
2758
|
+
logger.info(" Use --prune flag to delete them.\n");
|
|
2759
|
+
}
|
|
2760
|
+
} else {
|
|
2761
|
+
logger.info("\u2713 No orphaned resources found\n");
|
|
2762
|
+
}
|
|
2763
|
+
let created = 0;
|
|
2764
|
+
let updated = 0;
|
|
2765
|
+
let unchanged = 0;
|
|
2766
|
+
let failed = 0;
|
|
2767
|
+
const datasourceStats = await deployDatasources(config, client, stateManager);
|
|
2768
|
+
created += datasourceStats.created;
|
|
2769
|
+
updated += datasourceStats.updated;
|
|
2770
|
+
unchanged += datasourceStats.unchanged;
|
|
2771
|
+
failed += datasourceStats.failed;
|
|
2772
|
+
const assistantStats = await deployAssistants(config, client, loader, stateManager);
|
|
2773
|
+
created += assistantStats.created;
|
|
2774
|
+
updated += assistantStats.updated;
|
|
2775
|
+
unchanged += assistantStats.unchanged;
|
|
2776
|
+
failed += assistantStats.failed;
|
|
2777
|
+
const workflowStats = await deployWorkflows(config, client, stateManager, appConfig.rootDir);
|
|
2778
|
+
created += workflowStats.created;
|
|
2779
|
+
updated += workflowStats.updated;
|
|
2780
|
+
unchanged += workflowStats.unchanged;
|
|
2781
|
+
failed += workflowStats.failed;
|
|
2782
|
+
logger.info("=".repeat(50));
|
|
2783
|
+
logger.info("\u{1F4CA} Deployment Summary:\n");
|
|
2784
|
+
logger.info(` \u2705 Created: ${created}`);
|
|
2785
|
+
logger.info(` \u{1F504} Updated: ${updated}`);
|
|
2786
|
+
logger.info(` \u{1F4CB} Unchanged: ${unchanged}`);
|
|
2787
|
+
if (deleted > 0) {
|
|
2788
|
+
logger.info(` \u{1F5D1}\uFE0F Deleted: ${deleted}`);
|
|
2789
|
+
}
|
|
2790
|
+
if (failed > 0) {
|
|
2791
|
+
logger.info(` \u274C Failed: ${failed}`);
|
|
2792
|
+
}
|
|
2793
|
+
logger.info("");
|
|
2794
|
+
if (failed > 0) {
|
|
2795
|
+
throw new Error("Deployment completed with errors");
|
|
2796
|
+
} else {
|
|
2797
|
+
logger.info("\u2705 Deployment successful!");
|
|
2798
|
+
}
|
|
2799
|
+
} catch (error) {
|
|
2800
|
+
logger.error("\n\u274C Deployment failed:");
|
|
2801
|
+
logger.error(error instanceof Error ? error.message : String(error));
|
|
2802
|
+
throw error;
|
|
2803
|
+
}
|
|
2804
|
+
}
|
|
2805
|
+
async function main2(options) {
|
|
2806
|
+
try {
|
|
2807
|
+
await deployResources(options);
|
|
2808
|
+
process.exit(0);
|
|
2809
|
+
} catch {
|
|
2810
|
+
process.exit(1);
|
|
2811
|
+
}
|
|
2812
|
+
}
|
|
2813
|
+
async function destroyResources(options) {
|
|
2814
|
+
logger.info("\u{1F5D1}\uFE0F Destroy all managed resources\n");
|
|
2815
|
+
logger.info("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\n");
|
|
2816
|
+
try {
|
|
2817
|
+
const { appConfig, force = false } = options;
|
|
2818
|
+
const loader = new CodemieConfigLoader(appConfig);
|
|
2819
|
+
const stateManager = new StateManager(appConfig);
|
|
2820
|
+
const config = loader.loadConfig();
|
|
2821
|
+
logger.info(`Project: ${config.project.name}
|
|
2822
|
+
`);
|
|
2823
|
+
const managed = stateManager.getAllManagedResources();
|
|
2824
|
+
const total = managed.assistants.length + managed.datasources.length + managed.workflows.length;
|
|
2825
|
+
if (total === 0) {
|
|
2826
|
+
logger.info("\u2713 No managed resources found in state\n");
|
|
2827
|
+
logger.info("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\n");
|
|
2828
|
+
return;
|
|
2829
|
+
}
|
|
2830
|
+
logger.info(`Found ${total} managed resource(s):
|
|
2831
|
+
`);
|
|
2832
|
+
if (managed.assistants.length > 0) {
|
|
2833
|
+
logger.info(` \u{1F916} Assistants: ${managed.assistants.length}`);
|
|
2834
|
+
for (const name of managed.assistants) {
|
|
2835
|
+
logger.info(` \u2022 ${name}`);
|
|
2836
|
+
}
|
|
2837
|
+
}
|
|
2838
|
+
if (managed.datasources.length > 0) {
|
|
2839
|
+
logger.info(` \u{1F4CA} Datasources: ${managed.datasources.length}`);
|
|
2840
|
+
for (const name of managed.datasources) {
|
|
2841
|
+
logger.info(` \u2022 ${name}`);
|
|
2842
|
+
}
|
|
2843
|
+
}
|
|
2844
|
+
if (managed.workflows.length > 0) {
|
|
2845
|
+
logger.info(` \u{1F504} Workflows: ${managed.workflows.length}`);
|
|
2846
|
+
for (const name of managed.workflows) {
|
|
2847
|
+
logger.info(` \u2022 ${name}`);
|
|
2848
|
+
}
|
|
2849
|
+
}
|
|
2850
|
+
logger.warn("\n\u26A0\uFE0F WARNING: This will DELETE all resources listed above!");
|
|
2851
|
+
logger.warn("\u26A0\uFE0F These resources were created through IaC (in state.json)\n");
|
|
2852
|
+
logger.info("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\n");
|
|
2853
|
+
if (force) {
|
|
2854
|
+
logger.info("\u{1F680} --force flag detected, skipping confirmation\n");
|
|
2855
|
+
} else {
|
|
2856
|
+
const readline = await import('readline');
|
|
2857
|
+
const rl = readline.createInterface({
|
|
2858
|
+
input: process.stdin,
|
|
2859
|
+
output: process.stdout
|
|
2860
|
+
});
|
|
2861
|
+
const answer = await new Promise((resolve4) => {
|
|
2862
|
+
rl.question('Type "destroy" to confirm deletion: ', resolve4);
|
|
2863
|
+
});
|
|
2864
|
+
rl.close();
|
|
2865
|
+
if (answer.trim().toLowerCase() !== "destroy") {
|
|
2866
|
+
logger.info("\n\u274C Destruction cancelled\n");
|
|
2867
|
+
return;
|
|
2868
|
+
}
|
|
2869
|
+
}
|
|
2870
|
+
logger.info("\n\u{1F5D1}\uFE0F Deleting resources...\n");
|
|
2871
|
+
const client = await createClient(config);
|
|
2872
|
+
const cleanupManager = new CleanupManager(client, stateManager);
|
|
2873
|
+
const result = await cleanupManager.deleteOrphanedResources(managed);
|
|
2874
|
+
const totalDeleted = result.deleted.assistants.length + result.deleted.datasources.length + result.deleted.workflows.length;
|
|
2875
|
+
logger.info("\n\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\n");
|
|
2876
|
+
logger.info("\u{1F4CA} Destruction Summary:\n");
|
|
2877
|
+
logger.info(` \u2705 Deleted: ${totalDeleted}`);
|
|
2878
|
+
if (result.errors.length > 0) {
|
|
2879
|
+
logger.error(` \u274C Failed: ${result.errors.length}
|
|
2880
|
+
`);
|
|
2881
|
+
for (const err of result.errors) {
|
|
2882
|
+
logger.error(` \u2022 ${err.type} ${err.name}: ${err.error}`);
|
|
2883
|
+
}
|
|
2884
|
+
}
|
|
2885
|
+
logger.info("\n\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\n");
|
|
2886
|
+
if (result.errors.length > 0) {
|
|
2887
|
+
throw new Error("Destruction completed with errors");
|
|
2888
|
+
} else {
|
|
2889
|
+
logger.info("\u2705 All managed resources destroyed");
|
|
2890
|
+
}
|
|
2891
|
+
} catch (error) {
|
|
2892
|
+
logger.error("\n\u274C Destruction failed:");
|
|
2893
|
+
logger.error(error instanceof Error ? error.message : String(error));
|
|
2894
|
+
throw error;
|
|
2895
|
+
}
|
|
2896
|
+
}
|
|
2897
|
+
async function main3(options) {
|
|
2898
|
+
try {
|
|
2899
|
+
await destroyResources(options);
|
|
2900
|
+
process.exit(0);
|
|
2901
|
+
} catch (error) {
|
|
2902
|
+
logger.error("\n\u274C Destruction failed:");
|
|
2903
|
+
logger.error(error instanceof Error ? error.message : String(error));
|
|
2904
|
+
process.exit(1);
|
|
2905
|
+
}
|
|
2906
|
+
}
|
|
2907
|
+
async function previewResource(resource, resourceType, getState, checkExists, calculateChecksums) {
|
|
2908
|
+
const existingState = getState(resource.name);
|
|
2909
|
+
if (existingState) {
|
|
2910
|
+
const existsOnPlatform = await checkExists();
|
|
2911
|
+
if (existsOnPlatform) {
|
|
2912
|
+
const { hasChanged, updateDetails } = calculateChecksums();
|
|
2913
|
+
return hasChanged ? {
|
|
2914
|
+
type: "update",
|
|
2915
|
+
resourceType,
|
|
2916
|
+
name: resource.name,
|
|
2917
|
+
details: updateDetails
|
|
2918
|
+
} : {
|
|
2919
|
+
type: "no-change",
|
|
2920
|
+
resourceType,
|
|
2921
|
+
name: resource.name
|
|
2922
|
+
};
|
|
2923
|
+
} else {
|
|
2924
|
+
return {
|
|
2925
|
+
type: "create",
|
|
2926
|
+
resourceType,
|
|
2927
|
+
name: resource.name,
|
|
2928
|
+
details: `ID from state not found on platform, will create new`
|
|
2929
|
+
};
|
|
2930
|
+
}
|
|
2931
|
+
} else {
|
|
2932
|
+
const { createDetails } = calculateChecksums();
|
|
2933
|
+
return {
|
|
2934
|
+
type: "create",
|
|
2935
|
+
resourceType,
|
|
2936
|
+
name: resource.name,
|
|
2937
|
+
details: createDetails
|
|
2938
|
+
};
|
|
2939
|
+
}
|
|
2940
|
+
}
|
|
2941
|
+
function previewOrphanedResources(orphaned, stateManager) {
|
|
2942
|
+
const changes = [];
|
|
2943
|
+
for (const name of orphaned.assistants) {
|
|
2944
|
+
const assistantState = stateManager.getAssistantState(name);
|
|
2945
|
+
changes.push({
|
|
2946
|
+
type: "delete",
|
|
2947
|
+
resourceType: "assistant",
|
|
2948
|
+
name,
|
|
2949
|
+
details: assistantState ? `Removed from config (ID: ${assistantState.id})` : "Removed from config"
|
|
2950
|
+
});
|
|
2951
|
+
}
|
|
2952
|
+
for (const name of orphaned.datasources) {
|
|
2953
|
+
const datasourceState = stateManager.getDatasourceState(name);
|
|
2954
|
+
changes.push({
|
|
2955
|
+
type: "delete",
|
|
2956
|
+
resourceType: "datasource",
|
|
2957
|
+
name,
|
|
2958
|
+
details: datasourceState ? `Removed from config (ID: ${datasourceState.id})` : "Removed from config"
|
|
2959
|
+
});
|
|
2960
|
+
}
|
|
2961
|
+
for (const name of orphaned.workflows) {
|
|
2962
|
+
const workflowState = stateManager.getWorkflowState(name);
|
|
2963
|
+
changes.push({
|
|
2964
|
+
type: "delete",
|
|
2965
|
+
resourceType: "workflow",
|
|
2966
|
+
name,
|
|
2967
|
+
details: workflowState ? `Removed from config (ID: ${workflowState.id})` : "Removed from config"
|
|
2968
|
+
});
|
|
2969
|
+
}
|
|
2970
|
+
return changes;
|
|
2971
|
+
}
|
|
2972
|
+
async function previewAssistants(assistants, loader, stateManager, client) {
|
|
2973
|
+
const changes = [];
|
|
2974
|
+
for (const assistant of assistants) {
|
|
2975
|
+
const promptContent = loader.loadPrompt(assistant.prompt);
|
|
2976
|
+
let buildConfig = null;
|
|
2977
|
+
if (assistant.config) {
|
|
2978
|
+
buildConfig = loader.loadAssistantConfig(assistant.config);
|
|
2979
|
+
}
|
|
2980
|
+
const configChecksum = calculateAssistantConfigChecksum(assistant, buildConfig);
|
|
2981
|
+
const change = await previewResource(
|
|
2982
|
+
assistant,
|
|
2983
|
+
"assistant",
|
|
2984
|
+
(name) => stateManager.getAssistantState(name),
|
|
2985
|
+
() => checkAssistantExists(client, assistant.name, stateManager),
|
|
2986
|
+
() => {
|
|
2987
|
+
const existingState = stateManager.getAssistantState(assistant.name);
|
|
2988
|
+
const hasChanged = existingState ? existingState.promptChecksum !== calculateChecksum(promptContent) || existingState.configChecksum !== configChecksum : false;
|
|
2989
|
+
return {
|
|
2990
|
+
hasChanged,
|
|
2991
|
+
createDetails: `Model: ${assistant.model}`,
|
|
2992
|
+
updateDetails: "Prompt or configuration changed"
|
|
2993
|
+
};
|
|
2994
|
+
}
|
|
2995
|
+
);
|
|
2996
|
+
changes.push(change);
|
|
2997
|
+
}
|
|
2998
|
+
return changes;
|
|
2999
|
+
}
|
|
3000
|
+
async function previewDatasources(datasources, stateManager, client) {
|
|
3001
|
+
const changes = [];
|
|
3002
|
+
for (const datasource of datasources) {
|
|
3003
|
+
const change = await previewResource(
|
|
3004
|
+
datasource,
|
|
3005
|
+
"datasource",
|
|
3006
|
+
(name) => stateManager.getDatasourceState(name),
|
|
3007
|
+
() => checkDatasourceExists(client, datasource.name, stateManager),
|
|
3008
|
+
() => {
|
|
3009
|
+
const configChecksum = calculateDatasourceConfigChecksum(datasource);
|
|
3010
|
+
const existingState = stateManager.getDatasourceState(datasource.name);
|
|
3011
|
+
const hasChanged = existingState ? existingState.configChecksum !== configChecksum : false;
|
|
3012
|
+
if (hasChanged || datasource.force_reindex) {
|
|
3013
|
+
return {
|
|
3014
|
+
hasChanged: true,
|
|
3015
|
+
updateDetails: datasource.force_reindex ? "Force reindex" : "Configuration changed"
|
|
3016
|
+
};
|
|
3017
|
+
}
|
|
3018
|
+
return { hasChanged: false };
|
|
3019
|
+
}
|
|
3020
|
+
);
|
|
3021
|
+
if (!change.details && change.type === "create") {
|
|
3022
|
+
change.details = `Type: ${datasource.type}`;
|
|
3023
|
+
}
|
|
3024
|
+
changes.push(change);
|
|
3025
|
+
}
|
|
3026
|
+
return changes;
|
|
3027
|
+
}
|
|
3028
|
+
async function previewWorkflows(workflows, stateManager, client, rootDir = process.cwd()) {
|
|
3029
|
+
const changes = [];
|
|
3030
|
+
for (const workflow of workflows) {
|
|
3031
|
+
const yamlPath = path6.join(rootDir, workflow.definition);
|
|
3032
|
+
if (!fs2.existsSync(yamlPath)) {
|
|
3033
|
+
throw new Error(`Workflow definition file not found: ${workflow.definition}`);
|
|
3034
|
+
}
|
|
3035
|
+
const yamlConfig = fs2.readFileSync(yamlPath, "utf8");
|
|
3036
|
+
const change = await previewResource(
|
|
3037
|
+
workflow,
|
|
3038
|
+
"workflow",
|
|
3039
|
+
(name) => stateManager.getWorkflowState(name),
|
|
3040
|
+
() => checkWorkflowExists(client, workflow.name, stateManager),
|
|
3041
|
+
() => {
|
|
3042
|
+
const workflowYamlChecksum = calculateChecksum(yamlConfig);
|
|
3043
|
+
const configChecksum = calculateWorkflowConfigChecksum(workflow);
|
|
3044
|
+
const existingState = stateManager.getWorkflowState(workflow.name);
|
|
3045
|
+
const hasChanged = !!existingState && (existingState.workflowYamlChecksum !== workflowYamlChecksum || existingState.configChecksum !== configChecksum);
|
|
3046
|
+
return {
|
|
3047
|
+
hasChanged,
|
|
3048
|
+
...hasChanged ? { updateDetails: "Workflow YAML or configuration changed" } : {}
|
|
3049
|
+
};
|
|
3050
|
+
}
|
|
3051
|
+
);
|
|
3052
|
+
if (!change.details && change.type === "create") {
|
|
3053
|
+
change.details = `Mode: ${workflow.mode}`;
|
|
3054
|
+
}
|
|
3055
|
+
changes.push(change);
|
|
3056
|
+
}
|
|
3057
|
+
return changes;
|
|
3058
|
+
}
|
|
3059
|
+
|
|
3060
|
+
// src/preview.ts
|
|
3061
|
+
async function previewChanges(appConfig, existingClient) {
|
|
3062
|
+
const loader = new CodemieConfigLoader(appConfig);
|
|
3063
|
+
const stateManager = new StateManager(appConfig);
|
|
3064
|
+
const config = loader.loadConfig();
|
|
3065
|
+
const client = existingClient || await createClient(config);
|
|
3066
|
+
const changes = [];
|
|
3067
|
+
const cleanupManager = new CleanupManager(client, stateManager);
|
|
3068
|
+
const orphaned = cleanupManager.findOrphanedResources(config);
|
|
3069
|
+
changes.push(...previewOrphanedResources(orphaned, stateManager));
|
|
3070
|
+
if (config.resources.assistants) {
|
|
3071
|
+
const assistantChanges = await previewAssistants(config.resources.assistants, loader, stateManager, client);
|
|
3072
|
+
changes.push(...assistantChanges);
|
|
3073
|
+
}
|
|
3074
|
+
if (config.resources.datasources) {
|
|
3075
|
+
const datasourceChanges = await previewDatasources(config.resources.datasources, stateManager, client);
|
|
3076
|
+
changes.push(...datasourceChanges);
|
|
3077
|
+
}
|
|
3078
|
+
if (config.resources.workflows) {
|
|
3079
|
+
const workflowChanges = await previewWorkflows(config.resources.workflows, stateManager, client, appConfig.rootDir);
|
|
3080
|
+
changes.push(...workflowChanges);
|
|
3081
|
+
}
|
|
3082
|
+
const toDelete = changes.filter((c) => c.type === "delete");
|
|
3083
|
+
const toCreate = changes.filter((c) => c.type === "create");
|
|
3084
|
+
const toUpdate = changes.filter((c) => c.type === "update");
|
|
3085
|
+
const unchanged = changes.filter((c) => c.type === "no-change");
|
|
3086
|
+
return {
|
|
3087
|
+
changes,
|
|
3088
|
+
summary: {
|
|
3089
|
+
deleted: toDelete.length,
|
|
3090
|
+
created: toCreate.length,
|
|
3091
|
+
updated: toUpdate.length,
|
|
3092
|
+
unchanged: unchanged.length,
|
|
3093
|
+
total: changes.length
|
|
3094
|
+
}
|
|
3095
|
+
};
|
|
3096
|
+
}
|
|
3097
|
+
async function main4(options) {
|
|
3098
|
+
logger.info("\u{1F4CB} Generating deployment preview...\n");
|
|
3099
|
+
try {
|
|
3100
|
+
const { appConfig } = options;
|
|
3101
|
+
const loader = new CodemieConfigLoader(appConfig);
|
|
3102
|
+
const config = loader.loadConfig();
|
|
3103
|
+
logger.info("\u{1F517} Connection details:");
|
|
3104
|
+
logger.info(` API URL: ${config.environment.codemie_api_url}`);
|
|
3105
|
+
logger.info(` Auth URL: ${config.environment.auth_server_url}`);
|
|
3106
|
+
logger.info(` Realm: ${config.environment.auth_realm_name}`);
|
|
3107
|
+
if (config.environment.client_id && config.environment.client_secret) {
|
|
3108
|
+
logger.info(` Auth Method: Client Credentials`);
|
|
3109
|
+
logger.info(` Client ID: ${config.environment.client_id}`);
|
|
3110
|
+
} else if (config.environment.username && config.environment.password) {
|
|
3111
|
+
logger.info(` Auth Method: Username/Password`);
|
|
3112
|
+
logger.info(` Username: ${config.environment.username}`);
|
|
3113
|
+
} else {
|
|
3114
|
+
logger.info(` Auth Method: \u2717 Not configured`);
|
|
3115
|
+
}
|
|
3116
|
+
logger.info("");
|
|
3117
|
+
const client = await createClient(config);
|
|
3118
|
+
const { changes, summary } = await previewChanges(appConfig, client);
|
|
3119
|
+
logPreviewSummary(changes, summary);
|
|
3120
|
+
process.exit(0);
|
|
3121
|
+
} catch (error) {
|
|
3122
|
+
logger.error("\n\u274C Preview generation failed:");
|
|
3123
|
+
logger.error(error instanceof Error ? error.message : String(error));
|
|
3124
|
+
if (error instanceof Error && error.stack) {
|
|
3125
|
+
logger.error("\nStack trace:");
|
|
3126
|
+
logger.error(error.stack);
|
|
3127
|
+
}
|
|
3128
|
+
process.exit(1);
|
|
3129
|
+
}
|
|
3130
|
+
}
|
|
3131
|
+
function logPreviewSummary(changes, summary) {
|
|
3132
|
+
logger.info("\u{1F4CA} Deployment Preview:\n");
|
|
3133
|
+
if (summary.deleted > 0) {
|
|
3134
|
+
logger.info("\u{1F5D1}\uFE0F DELETE (orphaned resources):");
|
|
3135
|
+
for (const change of changes.filter((c) => c.type === "delete")) {
|
|
3136
|
+
logger.info(` - ${change.resourceType}:${change.name}`);
|
|
3137
|
+
if (change.details) {
|
|
3138
|
+
logger.info(` ${change.details}`);
|
|
3139
|
+
}
|
|
3140
|
+
}
|
|
3141
|
+
logger.info("");
|
|
3142
|
+
}
|
|
3143
|
+
if (summary.created > 0) {
|
|
3144
|
+
logger.info("\u2705 CREATE:");
|
|
3145
|
+
for (const change of changes.filter((c) => c.type === "create")) {
|
|
3146
|
+
logger.info(` - ${change.resourceType}:${change.name}`);
|
|
3147
|
+
if (change.details) {
|
|
3148
|
+
logger.info(` ${change.details}`);
|
|
3149
|
+
}
|
|
3150
|
+
}
|
|
3151
|
+
logger.info("");
|
|
3152
|
+
}
|
|
3153
|
+
if (summary.updated > 0) {
|
|
3154
|
+
logger.info("\u26A0\uFE0F UPDATE:");
|
|
3155
|
+
for (const change of changes.filter((c) => c.type === "update")) {
|
|
3156
|
+
logger.info(` - ${change.resourceType}:${change.name}`);
|
|
3157
|
+
if (change.details) {
|
|
3158
|
+
logger.info(` ${change.details}`);
|
|
3159
|
+
}
|
|
3160
|
+
}
|
|
3161
|
+
logger.info("");
|
|
3162
|
+
}
|
|
3163
|
+
if (summary.unchanged > 0) {
|
|
3164
|
+
logger.info("\u{1F4CA} UNCHANGED:");
|
|
3165
|
+
for (const change of changes.filter((c) => c.type === "no-change")) {
|
|
3166
|
+
logger.info(` - ${change.resourceType}:${change.name}`);
|
|
3167
|
+
}
|
|
3168
|
+
logger.info("");
|
|
3169
|
+
}
|
|
3170
|
+
logger.info("Summary:");
|
|
3171
|
+
logger.info(` Delete: ${summary.deleted}`);
|
|
3172
|
+
logger.info(` Create: ${summary.created}`);
|
|
3173
|
+
logger.info(` Update: ${summary.updated}`);
|
|
3174
|
+
logger.info(` Unchanged: ${summary.unchanged}`);
|
|
3175
|
+
logger.info(` Total: ${summary.total}`);
|
|
3176
|
+
if (summary.total === summary.unchanged) {
|
|
3177
|
+
logger.info("\n\u2728 No changes to apply");
|
|
3178
|
+
} else {
|
|
3179
|
+
logger.info('\n\u{1F449} Run "npm run deploy" to apply these changes');
|
|
3180
|
+
}
|
|
3181
|
+
}
|
|
3182
|
+
var DependencyValidator = class {
|
|
3183
|
+
/**
|
|
3184
|
+
* Validate assistant dependencies for cycles and nested sub-assistants
|
|
3185
|
+
*/
|
|
3186
|
+
static validateAssistantDependencies(assistants) {
|
|
3187
|
+
const errors = [];
|
|
3188
|
+
const assistantMap = /* @__PURE__ */ new Map();
|
|
3189
|
+
for (const assistant of assistants) {
|
|
3190
|
+
assistantMap.set(assistant.name, assistant);
|
|
3191
|
+
}
|
|
3192
|
+
const cyclicErrors = this.detectCycles(assistants, assistantMap);
|
|
3193
|
+
errors.push(...cyclicErrors);
|
|
3194
|
+
const nestedErrors = this.detectNestedSubAssistants(assistants);
|
|
3195
|
+
errors.push(...nestedErrors);
|
|
3196
|
+
return errors;
|
|
3197
|
+
}
|
|
3198
|
+
/**
|
|
3199
|
+
* Detect cyclic dependencies using DFS (name-based)
|
|
3200
|
+
*/
|
|
3201
|
+
static detectCycles(assistants, assistantMap) {
|
|
3202
|
+
const errors = [];
|
|
3203
|
+
const visited = /* @__PURE__ */ new Set();
|
|
3204
|
+
const recursionStack = /* @__PURE__ */ new Set();
|
|
3205
|
+
const dfs = (name, path13) => {
|
|
3206
|
+
if (recursionStack.has(name)) {
|
|
3207
|
+
const cycleStart = path13.indexOf(name);
|
|
3208
|
+
const cycle = [...path13.slice(cycleStart), name];
|
|
3209
|
+
errors.push(`Cyclic dependency detected: ${cycle.join(" \u2192 ")}`);
|
|
3210
|
+
return true;
|
|
3211
|
+
}
|
|
3212
|
+
if (visited.has(name)) {
|
|
3213
|
+
return false;
|
|
3214
|
+
}
|
|
3215
|
+
visited.add(name);
|
|
3216
|
+
recursionStack.add(name);
|
|
3217
|
+
path13.push(name);
|
|
3218
|
+
const assistant = assistantMap.get(name);
|
|
3219
|
+
const subAssistantRefs = assistant?.sub_assistants || [];
|
|
3220
|
+
for (const subRef of subAssistantRefs) {
|
|
3221
|
+
if (!assistantMap.has(subRef)) {
|
|
3222
|
+
continue;
|
|
3223
|
+
}
|
|
3224
|
+
if (dfs(subRef, [...path13])) {
|
|
3225
|
+
return true;
|
|
3226
|
+
}
|
|
3227
|
+
}
|
|
3228
|
+
recursionStack.delete(name);
|
|
3229
|
+
return false;
|
|
3230
|
+
};
|
|
3231
|
+
for (const assistant of assistants) {
|
|
3232
|
+
if (!visited.has(assistant.name)) {
|
|
3233
|
+
dfs(assistant.name, []);
|
|
3234
|
+
}
|
|
3235
|
+
}
|
|
3236
|
+
return errors;
|
|
3237
|
+
}
|
|
3238
|
+
/**
|
|
3239
|
+
* Detect nested sub-assistants (assistant with sub-assistants cannot be a sub-assistant)
|
|
3240
|
+
* Name-based validation
|
|
3241
|
+
*/
|
|
3242
|
+
static detectNestedSubAssistants(assistants) {
|
|
3243
|
+
const errors = [];
|
|
3244
|
+
const usedAsSubAssistant = /* @__PURE__ */ new Set();
|
|
3245
|
+
for (const assistant of assistants) {
|
|
3246
|
+
if (assistant.sub_assistants && assistant.sub_assistants.length > 0) {
|
|
3247
|
+
for (const subName of assistant.sub_assistants) {
|
|
3248
|
+
usedAsSubAssistant.add(subName);
|
|
3249
|
+
}
|
|
3250
|
+
}
|
|
3251
|
+
}
|
|
3252
|
+
for (const assistant of assistants) {
|
|
3253
|
+
const hasSubAssistants = assistant.sub_assistants && assistant.sub_assistants.length > 0;
|
|
3254
|
+
if (hasSubAssistants && usedAsSubAssistant.has(assistant.name)) {
|
|
3255
|
+
const parents = [];
|
|
3256
|
+
for (const potentialParent of assistants) {
|
|
3257
|
+
if (potentialParent.sub_assistants?.includes(assistant.name)) {
|
|
3258
|
+
parents.push(potentialParent.name);
|
|
3259
|
+
}
|
|
3260
|
+
}
|
|
3261
|
+
errors.push(
|
|
3262
|
+
`Assistant "${assistant.name}" has sub-assistants but is itself used as a sub-assistant by: ${parents.join(", ")}. Nested sub-assistants are not allowed.`
|
|
3263
|
+
);
|
|
3264
|
+
}
|
|
3265
|
+
}
|
|
3266
|
+
return errors;
|
|
3267
|
+
}
|
|
3268
|
+
/**
|
|
3269
|
+
* Validate workflow assistant references
|
|
3270
|
+
* Check that all assistant_name references in workflows point to existing assistants
|
|
3271
|
+
*/
|
|
3272
|
+
static validateWorkflowAssistantReferences(workflows, assistants, importedAssistants) {
|
|
3273
|
+
const errors = [];
|
|
3274
|
+
const availableAssistants = /* @__PURE__ */ new Set();
|
|
3275
|
+
for (const assistant of assistants) {
|
|
3276
|
+
availableAssistants.add(assistant.name);
|
|
3277
|
+
}
|
|
3278
|
+
for (const imported of importedAssistants) {
|
|
3279
|
+
availableAssistants.add(imported.name);
|
|
3280
|
+
}
|
|
3281
|
+
for (const workflow of workflows) {
|
|
3282
|
+
try {
|
|
3283
|
+
const workflowYaml = yaml3.parse(workflow.definition);
|
|
3284
|
+
if (!workflowYaml.assistants) {
|
|
3285
|
+
continue;
|
|
3286
|
+
}
|
|
3287
|
+
for (const assistant of workflowYaml.assistants) {
|
|
3288
|
+
if (assistant.model && assistant.system_prompt && !assistant.assistant_name) {
|
|
3289
|
+
continue;
|
|
3290
|
+
}
|
|
3291
|
+
if (assistant.assistant_name && !availableAssistants.has(assistant.assistant_name)) {
|
|
3292
|
+
errors.push(
|
|
3293
|
+
`Workflow "${workflow.name}": Assistant reference "${assistant.assistant_name}" (id: ${assistant.id}) not found in resources.assistants or imported.assistants`
|
|
3294
|
+
);
|
|
3295
|
+
}
|
|
3296
|
+
}
|
|
3297
|
+
} catch (error) {
|
|
3298
|
+
errors.push(
|
|
3299
|
+
`Workflow "${workflow.name}": Failed to parse workflow YAML: ${error instanceof Error ? error.message : String(error)}`
|
|
3300
|
+
);
|
|
3301
|
+
}
|
|
3302
|
+
}
|
|
3303
|
+
return errors;
|
|
3304
|
+
}
|
|
3305
|
+
};
|
|
3306
|
+
|
|
3307
|
+
// src/lib/validationUtils.ts
|
|
3308
|
+
function isAssistantWithSlug(obj) {
|
|
3309
|
+
return typeof obj === "object" && obj !== null && "slug" in obj && "name" in obj && typeof obj.slug === "string" && typeof obj.name === "string";
|
|
3310
|
+
}
|
|
3311
|
+
function isAssistant(obj) {
|
|
3312
|
+
return typeof obj === "object" && obj !== null && "id" in obj && "slug" in obj;
|
|
3313
|
+
}
|
|
3314
|
+
function validateSlugUniqueness(config) {
|
|
3315
|
+
const slugErrors = [];
|
|
3316
|
+
const slugMap = /* @__PURE__ */ new Map();
|
|
3317
|
+
if (config.resources.assistants) {
|
|
3318
|
+
for (const assistant of config.resources.assistants) {
|
|
3319
|
+
if (assistant.slug) {
|
|
3320
|
+
if (!slugMap.has(assistant.slug)) {
|
|
3321
|
+
slugMap.set(assistant.slug, []);
|
|
3322
|
+
}
|
|
3323
|
+
slugMap.get(assistant.slug).push(assistant.name);
|
|
3324
|
+
}
|
|
3325
|
+
}
|
|
3326
|
+
}
|
|
3327
|
+
for (const [slug, names] of slugMap.entries()) {
|
|
3328
|
+
if (names.length > 1) {
|
|
3329
|
+
slugErrors.push(`Duplicate slug "${slug}" for assistants: ${names.join(", ")}`);
|
|
3330
|
+
}
|
|
3331
|
+
}
|
|
3332
|
+
return slugErrors;
|
|
3333
|
+
}
|
|
3334
|
+
async function checkApiSlugConflicts({
|
|
3335
|
+
config,
|
|
3336
|
+
client,
|
|
3337
|
+
assistantState
|
|
3338
|
+
}) {
|
|
3339
|
+
const existingAssistants = await client.assistants.list({
|
|
3340
|
+
minimal_response: false,
|
|
3341
|
+
per_page: 100
|
|
3342
|
+
});
|
|
3343
|
+
const existingSlugs = /* @__PURE__ */ new Map();
|
|
3344
|
+
for (const assistant of existingAssistants) {
|
|
3345
|
+
if (isAssistantWithSlug(assistant) && assistant.slug) {
|
|
3346
|
+
existingSlugs.set(assistant.slug, assistant.name);
|
|
3347
|
+
}
|
|
3348
|
+
}
|
|
3349
|
+
const conflictErrors = [];
|
|
3350
|
+
const iacAssistantNames = new Set(Object.keys(assistantState));
|
|
3351
|
+
if (config.resources.assistants) {
|
|
3352
|
+
for (const assistant of config.resources.assistants) {
|
|
3353
|
+
if (!assistant.slug || iacAssistantNames.has(assistant.name)) {
|
|
3354
|
+
continue;
|
|
3355
|
+
}
|
|
3356
|
+
if (existingSlugs.has(assistant.slug)) {
|
|
3357
|
+
const existingName = existingSlugs.get(assistant.slug);
|
|
3358
|
+
const existingAssistant = existingAssistants.find((a) => isAssistant(a) && a.slug === assistant.slug);
|
|
3359
|
+
conflictErrors.push(
|
|
3360
|
+
`\u274C Slug "${assistant.slug}" for assistant "${assistant.name}" already exists in Codemie
|
|
3361
|
+
Existing assistant: "${existingName}" (ID: ${isAssistant(existingAssistant) ? existingAssistant.id : "unknown"})
|
|
3362
|
+
\u{1F4A1} Change slug to a unique value or omit slug (platform will generate)`
|
|
3363
|
+
);
|
|
3364
|
+
}
|
|
3365
|
+
}
|
|
3366
|
+
}
|
|
3367
|
+
return conflictErrors;
|
|
3368
|
+
}
|
|
3369
|
+
|
|
3370
|
+
// src/validate.ts
|
|
3371
|
+
async function validateConfig(options) {
|
|
3372
|
+
const errors = [];
|
|
3373
|
+
const { checkApi = false, appConfig } = options;
|
|
3374
|
+
const loader = new CodemieConfigLoader(appConfig);
|
|
3375
|
+
const config = loader.loadConfig();
|
|
3376
|
+
const fileValidation = loader.validateFiles(config);
|
|
3377
|
+
if (!fileValidation.valid) {
|
|
3378
|
+
errors.push(...fileValidation.errors);
|
|
3379
|
+
}
|
|
3380
|
+
const slugErrors = validateSlugUniqueness(config);
|
|
3381
|
+
errors.push(...slugErrors);
|
|
3382
|
+
if (config.resources.assistants && config.resources.assistants.length > 0) {
|
|
3383
|
+
const dependencyErrors = DependencyValidator.validateAssistantDependencies(config.resources.assistants);
|
|
3384
|
+
errors.push(...dependencyErrors);
|
|
3385
|
+
}
|
|
3386
|
+
if (config.resources.workflows && config.resources.workflows.length > 0) {
|
|
3387
|
+
const workflowsWithContent = config.resources.workflows.map((workflow) => {
|
|
3388
|
+
const workflowPath = path6.resolve(workflow.definition);
|
|
3389
|
+
if (!fs2.existsSync(workflowPath)) {
|
|
3390
|
+
errors.push(`Workflow definition file not found: ${workflow.definition}`);
|
|
3391
|
+
return { ...workflow, definition: "" };
|
|
3392
|
+
}
|
|
3393
|
+
const definition = fs2.readFileSync(workflowPath, "utf8");
|
|
3394
|
+
return { ...workflow, definition };
|
|
3395
|
+
});
|
|
3396
|
+
const workflowErrors = DependencyValidator.validateWorkflowAssistantReferences(
|
|
3397
|
+
workflowsWithContent,
|
|
3398
|
+
config.resources.assistants || [],
|
|
3399
|
+
config.imported?.assistants || []
|
|
3400
|
+
);
|
|
3401
|
+
errors.push(...workflowErrors);
|
|
3402
|
+
}
|
|
3403
|
+
if (checkApi && errors.length === 0) {
|
|
3404
|
+
const client = await createClient(config);
|
|
3405
|
+
const stateManager = new StateManager(appConfig);
|
|
3406
|
+
const state = stateManager.loadState();
|
|
3407
|
+
const conflictErrors = await checkApiSlugConflicts({ config, client, assistantState: state.resources.assistants });
|
|
3408
|
+
errors.push(...conflictErrors);
|
|
3409
|
+
}
|
|
3410
|
+
return { success: errors.length === 0, errors };
|
|
3411
|
+
}
|
|
3412
|
+
async function main5(options) {
|
|
3413
|
+
const { checkApi = false, appConfig } = options;
|
|
3414
|
+
logger.info("\u{1F50D} Validating configuration...");
|
|
3415
|
+
if (checkApi) {
|
|
3416
|
+
logger.info(" (with API connectivity check)\n");
|
|
3417
|
+
} else {
|
|
3418
|
+
logger.info(" (use --check-api to check for conflicts with existing assistants)\n");
|
|
3419
|
+
}
|
|
3420
|
+
try {
|
|
3421
|
+
const { success, errors } = await validateConfig(options);
|
|
3422
|
+
if (!success) {
|
|
3423
|
+
logger.error("\u274C Validation failed:\n");
|
|
3424
|
+
for (const error of errors) {
|
|
3425
|
+
logger.error(` - ${error}`);
|
|
3426
|
+
}
|
|
3427
|
+
process.exit(1);
|
|
3428
|
+
}
|
|
3429
|
+
const loader = new CodemieConfigLoader(appConfig);
|
|
3430
|
+
const config = loader.loadConfig();
|
|
3431
|
+
const assistantCount = config.resources.assistants?.length || 0;
|
|
3432
|
+
const datasourceCount = config.resources.datasources?.length || 0;
|
|
3433
|
+
const workflowCount = config.resources.workflows?.length || 0;
|
|
3434
|
+
logger.info("\u{1F4CA} Resources defined:");
|
|
3435
|
+
logger.info(` - Assistants: ${assistantCount}`);
|
|
3436
|
+
logger.info(` - Datasources: ${datasourceCount}`);
|
|
3437
|
+
logger.info(` - Workflows: ${workflowCount}`);
|
|
3438
|
+
if (assistantCount > 0) {
|
|
3439
|
+
logger.info("\n\u{1F916} Assistants:");
|
|
3440
|
+
if (config.resources.assistants) {
|
|
3441
|
+
for (const assistant of config.resources.assistants) {
|
|
3442
|
+
logger.info(` - ${assistant.name}`);
|
|
3443
|
+
}
|
|
3444
|
+
}
|
|
3445
|
+
}
|
|
3446
|
+
logger.info("\n\u2705 Validation successful!");
|
|
3447
|
+
process.exit(0);
|
|
3448
|
+
} catch (error) {
|
|
3449
|
+
logger.error("\n\u274C Validation failed:");
|
|
3450
|
+
logger.error("Validation failed:", error instanceof Error ? error.message : String(error));
|
|
3451
|
+
process.exit(1);
|
|
3452
|
+
}
|
|
3453
|
+
}
|
|
3454
|
+
|
|
3455
|
+
// src/cli/index.ts
|
|
3456
|
+
program.name("codemie").description("Infrastructure as Code for Codemie platform").version(package_default.version);
|
|
3457
|
+
program.command("deploy").description("Deploy resources to Codemie platform").option("-p, --prune", "Delete orphaned resources").option("-c, --config <path>", "Configuration file").action((options) => {
|
|
3458
|
+
const configPath = options.config ? path6__default.resolve(options.config) : void 0;
|
|
3459
|
+
const appConfig = loadAppConfig(configPath);
|
|
3460
|
+
void main2({ appConfig, prune: options.prune });
|
|
3461
|
+
});
|
|
3462
|
+
program.command("backup").description("Backup all resources from Codemie platform").option("-c, --config <path>", "Configuration file").action((options) => {
|
|
3463
|
+
const configPath = options.config ? path6__default.resolve(options.config) : void 0;
|
|
3464
|
+
const appConfig = loadAppConfig(configPath);
|
|
3465
|
+
void main({ appConfig });
|
|
3466
|
+
});
|
|
3467
|
+
program.command("destroy").description("Destroy all IaC-managed resources").option("-f, --force", "Skip confirmation prompt").option("-c, --config <path>", "Configuration file").action((options) => {
|
|
3468
|
+
const configPath = options.config ? path6__default.resolve(options.config) : void 0;
|
|
3469
|
+
const appConfig = loadAppConfig(configPath);
|
|
3470
|
+
void main3({ appConfig, force: options.force });
|
|
3471
|
+
});
|
|
3472
|
+
program.command("preview").description("Preview changes without applying them").option("-c, --config <path>", "Configuration file").action((options) => {
|
|
3473
|
+
const appConfigPath = options.config ? path6__default.resolve(options.config) : void 0;
|
|
3474
|
+
const appConfig = loadAppConfig(appConfigPath);
|
|
3475
|
+
void main4({ appConfig });
|
|
3476
|
+
});
|
|
3477
|
+
program.command("validate").description("Validate configuration").option("-a, --check-api", "Check for API conflicts").option("-c, --config <path>", "Configuration file").action((options) => {
|
|
3478
|
+
const configPath = options.config ? path6__default.resolve(options.config) : void 0;
|
|
3479
|
+
const appConfig = loadAppConfig(configPath);
|
|
3480
|
+
void main5({ appConfig, checkApi: options.checkApi });
|
|
3481
|
+
});
|
|
3482
|
+
program.parse();
|