@nikelyh/metamorph 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/metamorph.js +18 -0
- package/dist/TechDetector-YCDEZ6OR.js +7 -0
- package/dist/chunk-3QN2Q75J.js +101 -0
- package/dist/chunk-DJO6FY4K.js +33 -0
- package/dist/chunk-IIEUXZPM.js +4792 -0
- package/dist/chunk-RCULELX2.js +241770 -0
- package/dist/chunk-VS2QQHAK.js +46 -0
- package/dist/chunk-XHQSMB6V.js +73585 -0
- package/dist/dist-63RNBEIQ.js +63 -0
- package/dist/dist-VCTPPAJN.js +1129 -0
- package/dist/index.js +2700 -0
- package/dist/multipart-parser-ALMYCQSC.js +371 -0
- package/dist/public/assets/index-Bq6f5VIA.css +1 -0
- package/dist/public/assets/index-CO0gJijU.js +66 -0
- package/dist/public/favicon.svg +1 -0
- package/dist/public/icons/angular.svg +1 -0
- package/dist/public/icons/expressjs.svg +1 -0
- package/dist/public/icons/fastify.svg +1 -0
- package/dist/public/icons/hapi.svg +14 -0
- package/dist/public/icons/koa.svg +1 -0
- package/dist/public/icons/nestjs.svg +1 -0
- package/dist/public/icons/nextjs.svg +1 -0
- package/dist/public/icons/react.svg +1 -0
- package/dist/public/icons/svelte.svg +21 -0
- package/dist/public/icons/vue.svg +1 -0
- package/dist/public/icons.svg +24 -0
- package/dist/public/index.html +14 -0
- package/dist/runtime-WTVS3IND.js +24 -0
- package/dist/src-ZMFD37HA.js +1408 -0
- package/dist/ts-morph-LFNGDRSW.js +5 -0
- package/package.json +40 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,2700 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
require_ts_morph
|
|
4
|
+
} from "./chunk-RCULELX2.js";
|
|
5
|
+
import {
|
|
6
|
+
detectTechnologies
|
|
7
|
+
} from "./chunk-3QN2Q75J.js";
|
|
8
|
+
import {
|
|
9
|
+
MetamorphState,
|
|
10
|
+
initializeRuntime,
|
|
11
|
+
join,
|
|
12
|
+
leave,
|
|
13
|
+
resolveRuntime,
|
|
14
|
+
runLoop,
|
|
15
|
+
sendEvent
|
|
16
|
+
} from "./chunk-DJO6FY4K.js";
|
|
17
|
+
import {
|
|
18
|
+
SituationSpecification,
|
|
19
|
+
createAgent
|
|
20
|
+
} from "./chunk-XHQSMB6V.js";
|
|
21
|
+
import {
|
|
22
|
+
__commonJS,
|
|
23
|
+
__require,
|
|
24
|
+
__toESM
|
|
25
|
+
} from "./chunk-VS2QQHAK.js";
|
|
26
|
+
|
|
27
|
+
// ../../../node_modules/dotenv/lib/main.js
|
|
28
|
+
var require_main = __commonJS({
|
|
29
|
+
"../../../node_modules/dotenv/lib/main.js"(exports, module) {
|
|
30
|
+
"use strict";
|
|
31
|
+
var fs7 = __require("fs");
|
|
32
|
+
var path6 = __require("path");
|
|
33
|
+
var os = __require("os");
|
|
34
|
+
var crypto = __require("crypto");
|
|
35
|
+
var TIPS = [
|
|
36
|
+
"\u25C8 encrypted .env [www.dotenvx.com]",
|
|
37
|
+
"\u25C8 secrets for agents [www.dotenvx.com]",
|
|
38
|
+
"\u2301 auth for agents [www.vestauth.com]",
|
|
39
|
+
"\u2318 custom filepath { path: '/custom/path/.env' }",
|
|
40
|
+
"\u2318 enable debugging { debug: true }",
|
|
41
|
+
"\u2318 override existing { override: true }",
|
|
42
|
+
"\u2318 suppress logs { quiet: true }",
|
|
43
|
+
"\u2318 multiple files { path: ['.env.local', '.env'] }"
|
|
44
|
+
];
|
|
45
|
+
function _getRandomTip() {
|
|
46
|
+
return TIPS[Math.floor(Math.random() * TIPS.length)];
|
|
47
|
+
}
|
|
48
|
+
function parseBoolean(value) {
|
|
49
|
+
if (typeof value === "string") {
|
|
50
|
+
return !["false", "0", "no", "off", ""].includes(value.toLowerCase());
|
|
51
|
+
}
|
|
52
|
+
return Boolean(value);
|
|
53
|
+
}
|
|
54
|
+
function supportsAnsi() {
|
|
55
|
+
return process.stdout.isTTY;
|
|
56
|
+
}
|
|
57
|
+
function dim(text) {
|
|
58
|
+
return supportsAnsi() ? `\x1B[2m${text}\x1B[0m` : text;
|
|
59
|
+
}
|
|
60
|
+
var LINE = /(?:^|^)\s*(?:export\s+)?([\w.-]+)(?:\s*=\s*?|:\s+?)(\s*'(?:\\'|[^'])*'|\s*"(?:\\"|[^"])*"|\s*`(?:\\`|[^`])*`|[^#\r\n]+)?\s*(?:#.*)?(?:$|$)/mg;
|
|
61
|
+
function parse(src) {
|
|
62
|
+
const obj = {};
|
|
63
|
+
let lines = src.toString();
|
|
64
|
+
lines = lines.replace(/\r\n?/mg, "\n");
|
|
65
|
+
let match;
|
|
66
|
+
while ((match = LINE.exec(lines)) != null) {
|
|
67
|
+
const key = match[1];
|
|
68
|
+
let value = match[2] || "";
|
|
69
|
+
value = value.trim();
|
|
70
|
+
const maybeQuote = value[0];
|
|
71
|
+
value = value.replace(/^(['"`])([\s\S]*)\1$/mg, "$2");
|
|
72
|
+
if (maybeQuote === '"') {
|
|
73
|
+
value = value.replace(/\\n/g, "\n");
|
|
74
|
+
value = value.replace(/\\r/g, "\r");
|
|
75
|
+
}
|
|
76
|
+
obj[key] = value;
|
|
77
|
+
}
|
|
78
|
+
return obj;
|
|
79
|
+
}
|
|
80
|
+
function _parseVault(options) {
|
|
81
|
+
options = options || {};
|
|
82
|
+
const vaultPath = _vaultPath(options);
|
|
83
|
+
options.path = vaultPath;
|
|
84
|
+
const result = DotenvModule.configDotenv(options);
|
|
85
|
+
if (!result.parsed) {
|
|
86
|
+
const err = new Error(`MISSING_DATA: Cannot parse ${vaultPath} for an unknown reason`);
|
|
87
|
+
err.code = "MISSING_DATA";
|
|
88
|
+
throw err;
|
|
89
|
+
}
|
|
90
|
+
const keys = _dotenvKey(options).split(",");
|
|
91
|
+
const length = keys.length;
|
|
92
|
+
let decrypted;
|
|
93
|
+
for (let i = 0; i < length; i++) {
|
|
94
|
+
try {
|
|
95
|
+
const key = keys[i].trim();
|
|
96
|
+
const attrs = _instructions(result, key);
|
|
97
|
+
decrypted = DotenvModule.decrypt(attrs.ciphertext, attrs.key);
|
|
98
|
+
break;
|
|
99
|
+
} catch (error) {
|
|
100
|
+
if (i + 1 >= length) {
|
|
101
|
+
throw error;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return DotenvModule.parse(decrypted);
|
|
106
|
+
}
|
|
107
|
+
function _warn(message) {
|
|
108
|
+
console.error(`\u26A0 ${message}`);
|
|
109
|
+
}
|
|
110
|
+
function _debug(message) {
|
|
111
|
+
console.log(`\u2506 ${message}`);
|
|
112
|
+
}
|
|
113
|
+
function _log(message) {
|
|
114
|
+
console.log(`\u25C7 ${message}`);
|
|
115
|
+
}
|
|
116
|
+
function _dotenvKey(options) {
|
|
117
|
+
if (options && options.DOTENV_KEY && options.DOTENV_KEY.length > 0) {
|
|
118
|
+
return options.DOTENV_KEY;
|
|
119
|
+
}
|
|
120
|
+
if (process.env.DOTENV_KEY && process.env.DOTENV_KEY.length > 0) {
|
|
121
|
+
return process.env.DOTENV_KEY;
|
|
122
|
+
}
|
|
123
|
+
return "";
|
|
124
|
+
}
|
|
125
|
+
function _instructions(result, dotenvKey) {
|
|
126
|
+
let uri;
|
|
127
|
+
try {
|
|
128
|
+
uri = new URL(dotenvKey);
|
|
129
|
+
} catch (error) {
|
|
130
|
+
if (error.code === "ERR_INVALID_URL") {
|
|
131
|
+
const err = new Error("INVALID_DOTENV_KEY: Wrong format. Must be in valid uri format like dotenv://:key_1234@dotenvx.com/vault/.env.vault?environment=development");
|
|
132
|
+
err.code = "INVALID_DOTENV_KEY";
|
|
133
|
+
throw err;
|
|
134
|
+
}
|
|
135
|
+
throw error;
|
|
136
|
+
}
|
|
137
|
+
const key = uri.password;
|
|
138
|
+
if (!key) {
|
|
139
|
+
const err = new Error("INVALID_DOTENV_KEY: Missing key part");
|
|
140
|
+
err.code = "INVALID_DOTENV_KEY";
|
|
141
|
+
throw err;
|
|
142
|
+
}
|
|
143
|
+
const environment = uri.searchParams.get("environment");
|
|
144
|
+
if (!environment) {
|
|
145
|
+
const err = new Error("INVALID_DOTENV_KEY: Missing environment part");
|
|
146
|
+
err.code = "INVALID_DOTENV_KEY";
|
|
147
|
+
throw err;
|
|
148
|
+
}
|
|
149
|
+
const environmentKey = `DOTENV_VAULT_${environment.toUpperCase()}`;
|
|
150
|
+
const ciphertext = result.parsed[environmentKey];
|
|
151
|
+
if (!ciphertext) {
|
|
152
|
+
const err = new Error(`NOT_FOUND_DOTENV_ENVIRONMENT: Cannot locate environment ${environmentKey} in your .env.vault file.`);
|
|
153
|
+
err.code = "NOT_FOUND_DOTENV_ENVIRONMENT";
|
|
154
|
+
throw err;
|
|
155
|
+
}
|
|
156
|
+
return { ciphertext, key };
|
|
157
|
+
}
|
|
158
|
+
function _vaultPath(options) {
|
|
159
|
+
let possibleVaultPath = null;
|
|
160
|
+
if (options && options.path && options.path.length > 0) {
|
|
161
|
+
if (Array.isArray(options.path)) {
|
|
162
|
+
for (const filepath of options.path) {
|
|
163
|
+
if (fs7.existsSync(filepath)) {
|
|
164
|
+
possibleVaultPath = filepath.endsWith(".vault") ? filepath : `${filepath}.vault`;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
} else {
|
|
168
|
+
possibleVaultPath = options.path.endsWith(".vault") ? options.path : `${options.path}.vault`;
|
|
169
|
+
}
|
|
170
|
+
} else {
|
|
171
|
+
possibleVaultPath = path6.resolve(process.cwd(), ".env.vault");
|
|
172
|
+
}
|
|
173
|
+
if (fs7.existsSync(possibleVaultPath)) {
|
|
174
|
+
return possibleVaultPath;
|
|
175
|
+
}
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
function _resolveHome(envPath) {
|
|
179
|
+
return envPath[0] === "~" ? path6.join(os.homedir(), envPath.slice(1)) : envPath;
|
|
180
|
+
}
|
|
181
|
+
function _configVault(options) {
|
|
182
|
+
const debug = parseBoolean(process.env.DOTENV_CONFIG_DEBUG || options && options.debug);
|
|
183
|
+
const quiet = parseBoolean(process.env.DOTENV_CONFIG_QUIET || options && options.quiet);
|
|
184
|
+
if (debug || !quiet) {
|
|
185
|
+
_log("loading env from encrypted .env.vault");
|
|
186
|
+
}
|
|
187
|
+
const parsed = DotenvModule._parseVault(options);
|
|
188
|
+
let processEnv = process.env;
|
|
189
|
+
if (options && options.processEnv != null) {
|
|
190
|
+
processEnv = options.processEnv;
|
|
191
|
+
}
|
|
192
|
+
DotenvModule.populate(processEnv, parsed, options);
|
|
193
|
+
return { parsed };
|
|
194
|
+
}
|
|
195
|
+
function configDotenv(options) {
|
|
196
|
+
const dotenvPath = path6.resolve(process.cwd(), ".env");
|
|
197
|
+
let encoding = "utf8";
|
|
198
|
+
let processEnv = process.env;
|
|
199
|
+
if (options && options.processEnv != null) {
|
|
200
|
+
processEnv = options.processEnv;
|
|
201
|
+
}
|
|
202
|
+
let debug = parseBoolean(processEnv.DOTENV_CONFIG_DEBUG || options && options.debug);
|
|
203
|
+
let quiet = parseBoolean(processEnv.DOTENV_CONFIG_QUIET || options && options.quiet);
|
|
204
|
+
if (options && options.encoding) {
|
|
205
|
+
encoding = options.encoding;
|
|
206
|
+
} else {
|
|
207
|
+
if (debug) {
|
|
208
|
+
_debug("no encoding is specified (UTF-8 is used by default)");
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
let optionPaths = [dotenvPath];
|
|
212
|
+
if (options && options.path) {
|
|
213
|
+
if (!Array.isArray(options.path)) {
|
|
214
|
+
optionPaths = [_resolveHome(options.path)];
|
|
215
|
+
} else {
|
|
216
|
+
optionPaths = [];
|
|
217
|
+
for (const filepath of options.path) {
|
|
218
|
+
optionPaths.push(_resolveHome(filepath));
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
let lastError;
|
|
223
|
+
const parsedAll = {};
|
|
224
|
+
for (const path7 of optionPaths) {
|
|
225
|
+
try {
|
|
226
|
+
const parsed = DotenvModule.parse(fs7.readFileSync(path7, { encoding }));
|
|
227
|
+
DotenvModule.populate(parsedAll, parsed, options);
|
|
228
|
+
} catch (e) {
|
|
229
|
+
if (debug) {
|
|
230
|
+
_debug(`failed to load ${path7} ${e.message}`);
|
|
231
|
+
}
|
|
232
|
+
lastError = e;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
const populated = DotenvModule.populate(processEnv, parsedAll, options);
|
|
236
|
+
debug = parseBoolean(processEnv.DOTENV_CONFIG_DEBUG || debug);
|
|
237
|
+
quiet = parseBoolean(processEnv.DOTENV_CONFIG_QUIET || quiet);
|
|
238
|
+
if (debug || !quiet) {
|
|
239
|
+
const keysCount = Object.keys(populated).length;
|
|
240
|
+
const shortPaths = [];
|
|
241
|
+
for (const filePath of optionPaths) {
|
|
242
|
+
try {
|
|
243
|
+
const relative2 = path6.relative(process.cwd(), filePath);
|
|
244
|
+
shortPaths.push(relative2);
|
|
245
|
+
} catch (e) {
|
|
246
|
+
if (debug) {
|
|
247
|
+
_debug(`failed to load ${filePath} ${e.message}`);
|
|
248
|
+
}
|
|
249
|
+
lastError = e;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
_log(`injected env (${keysCount}) from ${shortPaths.join(",")} ${dim(`// tip: ${_getRandomTip()}`)}`);
|
|
253
|
+
}
|
|
254
|
+
if (lastError) {
|
|
255
|
+
return { parsed: parsedAll, error: lastError };
|
|
256
|
+
} else {
|
|
257
|
+
return { parsed: parsedAll };
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
function config(options) {
|
|
261
|
+
if (_dotenvKey(options).length === 0) {
|
|
262
|
+
return DotenvModule.configDotenv(options);
|
|
263
|
+
}
|
|
264
|
+
const vaultPath = _vaultPath(options);
|
|
265
|
+
if (!vaultPath) {
|
|
266
|
+
_warn(`you set DOTENV_KEY but you are missing a .env.vault file at ${vaultPath}`);
|
|
267
|
+
return DotenvModule.configDotenv(options);
|
|
268
|
+
}
|
|
269
|
+
return DotenvModule._configVault(options);
|
|
270
|
+
}
|
|
271
|
+
function decrypt(encrypted, keyStr) {
|
|
272
|
+
const key = Buffer.from(keyStr.slice(-64), "hex");
|
|
273
|
+
let ciphertext = Buffer.from(encrypted, "base64");
|
|
274
|
+
const nonce = ciphertext.subarray(0, 12);
|
|
275
|
+
const authTag = ciphertext.subarray(-16);
|
|
276
|
+
ciphertext = ciphertext.subarray(12, -16);
|
|
277
|
+
try {
|
|
278
|
+
const aesgcm = crypto.createDecipheriv("aes-256-gcm", key, nonce);
|
|
279
|
+
aesgcm.setAuthTag(authTag);
|
|
280
|
+
return `${aesgcm.update(ciphertext)}${aesgcm.final()}`;
|
|
281
|
+
} catch (error) {
|
|
282
|
+
const isRange = error instanceof RangeError;
|
|
283
|
+
const invalidKeyLength = error.message === "Invalid key length";
|
|
284
|
+
const decryptionFailed = error.message === "Unsupported state or unable to authenticate data";
|
|
285
|
+
if (isRange || invalidKeyLength) {
|
|
286
|
+
const err = new Error("INVALID_DOTENV_KEY: It must be 64 characters long (or more)");
|
|
287
|
+
err.code = "INVALID_DOTENV_KEY";
|
|
288
|
+
throw err;
|
|
289
|
+
} else if (decryptionFailed) {
|
|
290
|
+
const err = new Error("DECRYPTION_FAILED: Please check your DOTENV_KEY");
|
|
291
|
+
err.code = "DECRYPTION_FAILED";
|
|
292
|
+
throw err;
|
|
293
|
+
} else {
|
|
294
|
+
throw error;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
function populate(processEnv, parsed, options = {}) {
|
|
299
|
+
const debug = Boolean(options && options.debug);
|
|
300
|
+
const override = Boolean(options && options.override);
|
|
301
|
+
const populated = {};
|
|
302
|
+
if (typeof parsed !== "object") {
|
|
303
|
+
const err = new Error("OBJECT_REQUIRED: Please check the processEnv argument being passed to populate");
|
|
304
|
+
err.code = "OBJECT_REQUIRED";
|
|
305
|
+
throw err;
|
|
306
|
+
}
|
|
307
|
+
for (const key of Object.keys(parsed)) {
|
|
308
|
+
if (Object.prototype.hasOwnProperty.call(processEnv, key)) {
|
|
309
|
+
if (override === true) {
|
|
310
|
+
processEnv[key] = parsed[key];
|
|
311
|
+
populated[key] = parsed[key];
|
|
312
|
+
}
|
|
313
|
+
if (debug) {
|
|
314
|
+
if (override === true) {
|
|
315
|
+
_debug(`"${key}" is already defined and WAS overwritten`);
|
|
316
|
+
} else {
|
|
317
|
+
_debug(`"${key}" is already defined and was NOT overwritten`);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
} else {
|
|
321
|
+
processEnv[key] = parsed[key];
|
|
322
|
+
populated[key] = parsed[key];
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
return populated;
|
|
326
|
+
}
|
|
327
|
+
var DotenvModule = {
|
|
328
|
+
configDotenv,
|
|
329
|
+
_configVault,
|
|
330
|
+
_parseVault,
|
|
331
|
+
config,
|
|
332
|
+
decrypt,
|
|
333
|
+
parse,
|
|
334
|
+
populate
|
|
335
|
+
};
|
|
336
|
+
module.exports.configDotenv = DotenvModule.configDotenv;
|
|
337
|
+
module.exports._configVault = DotenvModule._configVault;
|
|
338
|
+
module.exports._parseVault = DotenvModule._parseVault;
|
|
339
|
+
module.exports.config = DotenvModule.config;
|
|
340
|
+
module.exports.decrypt = DotenvModule.decrypt;
|
|
341
|
+
module.exports.parse = DotenvModule.parse;
|
|
342
|
+
module.exports.populate = DotenvModule.populate;
|
|
343
|
+
module.exports = DotenvModule;
|
|
344
|
+
}
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
// ../../../node_modules/dotenv/lib/env-options.js
|
|
348
|
+
var require_env_options = __commonJS({
|
|
349
|
+
"../../../node_modules/dotenv/lib/env-options.js"(exports, module) {
|
|
350
|
+
"use strict";
|
|
351
|
+
var options = {};
|
|
352
|
+
if (process.env.DOTENV_CONFIG_ENCODING != null) {
|
|
353
|
+
options.encoding = process.env.DOTENV_CONFIG_ENCODING;
|
|
354
|
+
}
|
|
355
|
+
if (process.env.DOTENV_CONFIG_PATH != null) {
|
|
356
|
+
options.path = process.env.DOTENV_CONFIG_PATH;
|
|
357
|
+
}
|
|
358
|
+
if (process.env.DOTENV_CONFIG_QUIET != null) {
|
|
359
|
+
options.quiet = process.env.DOTENV_CONFIG_QUIET;
|
|
360
|
+
}
|
|
361
|
+
if (process.env.DOTENV_CONFIG_DEBUG != null) {
|
|
362
|
+
options.debug = process.env.DOTENV_CONFIG_DEBUG;
|
|
363
|
+
}
|
|
364
|
+
if (process.env.DOTENV_CONFIG_OVERRIDE != null) {
|
|
365
|
+
options.override = process.env.DOTENV_CONFIG_OVERRIDE;
|
|
366
|
+
}
|
|
367
|
+
if (process.env.DOTENV_CONFIG_DOTENV_KEY != null) {
|
|
368
|
+
options.DOTENV_KEY = process.env.DOTENV_CONFIG_DOTENV_KEY;
|
|
369
|
+
}
|
|
370
|
+
module.exports = options;
|
|
371
|
+
}
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
// ../../../node_modules/dotenv/lib/cli-options.js
|
|
375
|
+
var require_cli_options = __commonJS({
|
|
376
|
+
"../../../node_modules/dotenv/lib/cli-options.js"(exports, module) {
|
|
377
|
+
"use strict";
|
|
378
|
+
var re = /^dotenv_config_(encoding|path|quiet|debug|override|DOTENV_KEY)=(.+)$/;
|
|
379
|
+
module.exports = function optionMatcher(args) {
|
|
380
|
+
const options = args.reduce(function(acc, cur) {
|
|
381
|
+
const matches = cur.match(re);
|
|
382
|
+
if (matches) {
|
|
383
|
+
acc[matches[1]] = matches[2];
|
|
384
|
+
}
|
|
385
|
+
return acc;
|
|
386
|
+
}, {});
|
|
387
|
+
if (!("quiet" in options)) {
|
|
388
|
+
options.quiet = "true";
|
|
389
|
+
}
|
|
390
|
+
return options;
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
// src/index.ts
|
|
396
|
+
import { Command } from "commander";
|
|
397
|
+
import chalk from "chalk";
|
|
398
|
+
import ora from "ora";
|
|
399
|
+
|
|
400
|
+
// ../../../node_modules/dotenv/config.js
|
|
401
|
+
(function() {
|
|
402
|
+
require_main().config(
|
|
403
|
+
Object.assign(
|
|
404
|
+
{},
|
|
405
|
+
require_env_options(),
|
|
406
|
+
require_cli_options()(process.argv)
|
|
407
|
+
)
|
|
408
|
+
);
|
|
409
|
+
})();
|
|
410
|
+
|
|
411
|
+
// ../infrastructure/src/db/SQLiteStateStore.ts
|
|
412
|
+
import { DatabaseSync } from "sqlite";
|
|
413
|
+
import { join as join2 } from "path";
|
|
414
|
+
import { mkdirSync } from "fs";
|
|
415
|
+
var SQLiteStateStore = class {
|
|
416
|
+
db;
|
|
417
|
+
constructor(storageDir = ".metamorph") {
|
|
418
|
+
try {
|
|
419
|
+
mkdirSync(storageDir, { recursive: true });
|
|
420
|
+
} catch (e) {
|
|
421
|
+
}
|
|
422
|
+
const dbPath = join2(storageDir, "history.db");
|
|
423
|
+
this.db = new DatabaseSync(dbPath);
|
|
424
|
+
this.initTables();
|
|
425
|
+
}
|
|
426
|
+
/**
|
|
427
|
+
* Creates the necessary tables if they do not exist.
|
|
428
|
+
*/
|
|
429
|
+
initTables() {
|
|
430
|
+
this.db.exec(`
|
|
431
|
+
CREATE TABLE IF NOT EXISTS plans (
|
|
432
|
+
id TEXT PRIMARY KEY,
|
|
433
|
+
run_id TEXT,
|
|
434
|
+
target_path TEXT,
|
|
435
|
+
source_framework TEXT,
|
|
436
|
+
target_framework TEXT,
|
|
437
|
+
rules_json TEXT,
|
|
438
|
+
created_at TEXT
|
|
439
|
+
);
|
|
440
|
+
`);
|
|
441
|
+
try {
|
|
442
|
+
this.db.exec("ALTER TABLE plans ADD COLUMN run_id TEXT");
|
|
443
|
+
} catch (e) {
|
|
444
|
+
}
|
|
445
|
+
try {
|
|
446
|
+
this.db.exec("ALTER TABLE plans ADD COLUMN target_path TEXT");
|
|
447
|
+
} catch (e) {
|
|
448
|
+
}
|
|
449
|
+
this.db.exec(`
|
|
450
|
+
CREATE TABLE IF NOT EXISTS tasks (
|
|
451
|
+
plan_id TEXT,
|
|
452
|
+
file_path TEXT,
|
|
453
|
+
status TEXT,
|
|
454
|
+
dependencies_json TEXT,
|
|
455
|
+
error TEXT,
|
|
456
|
+
PRIMARY KEY (plan_id, file_path),
|
|
457
|
+
FOREIGN KEY (plan_id) REFERENCES plans(id)
|
|
458
|
+
);
|
|
459
|
+
`);
|
|
460
|
+
this.db.exec(`
|
|
461
|
+
CREATE TABLE IF NOT EXISTS events (
|
|
462
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
463
|
+
event_name TEXT,
|
|
464
|
+
payload_json TEXT,
|
|
465
|
+
timestamp TEXT
|
|
466
|
+
);
|
|
467
|
+
`);
|
|
468
|
+
}
|
|
469
|
+
async savePlan(plan) {
|
|
470
|
+
const stmtPlan = this.db.prepare(`
|
|
471
|
+
INSERT OR REPLACE INTO plans (id, run_id, target_path, source_framework, target_framework, rules_json, created_at)
|
|
472
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
473
|
+
`);
|
|
474
|
+
stmtPlan.run(
|
|
475
|
+
plan.id,
|
|
476
|
+
plan.runId,
|
|
477
|
+
plan.targetPath || "",
|
|
478
|
+
plan.profile.source,
|
|
479
|
+
plan.profile.target,
|
|
480
|
+
JSON.stringify(plan.profile.rules || []),
|
|
481
|
+
plan.createdAt.toISOString()
|
|
482
|
+
);
|
|
483
|
+
const stmtTask = this.db.prepare(`
|
|
484
|
+
INSERT OR REPLACE INTO tasks (plan_id, file_path, status, dependencies_json, error)
|
|
485
|
+
VALUES (?, ?, ?, ?, ?)
|
|
486
|
+
`);
|
|
487
|
+
for (const task of plan.tasks) {
|
|
488
|
+
stmtTask.run(
|
|
489
|
+
plan.id,
|
|
490
|
+
task.filePath,
|
|
491
|
+
task.status,
|
|
492
|
+
JSON.stringify(task.dependencies || []),
|
|
493
|
+
task.error || null
|
|
494
|
+
);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
async getPlan(id) {
|
|
498
|
+
const stmtPlan = this.db.prepare(`SELECT * FROM plans WHERE id = ?`);
|
|
499
|
+
const planRow = stmtPlan.get(id);
|
|
500
|
+
if (!planRow) {
|
|
501
|
+
return null;
|
|
502
|
+
}
|
|
503
|
+
const stmtTasks = this.db.prepare(`SELECT * FROM tasks WHERE plan_id = ?`);
|
|
504
|
+
const taskRows = stmtTasks.all(id);
|
|
505
|
+
return {
|
|
506
|
+
id: planRow.id,
|
|
507
|
+
runId: planRow.run_id,
|
|
508
|
+
targetPath: planRow.target_path,
|
|
509
|
+
profile: {
|
|
510
|
+
source: planRow.source_framework,
|
|
511
|
+
target: planRow.target_framework,
|
|
512
|
+
rules: JSON.parse(planRow.rules_json)
|
|
513
|
+
},
|
|
514
|
+
createdAt: new Date(planRow.created_at),
|
|
515
|
+
tasks: taskRows.map((row) => ({
|
|
516
|
+
filePath: row.file_path,
|
|
517
|
+
status: row.status,
|
|
518
|
+
dependencies: JSON.parse(row.dependencies_json),
|
|
519
|
+
error: row.error || void 0
|
|
520
|
+
}))
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
async updateTaskStatus(planId, filePath, status, error) {
|
|
524
|
+
const stmt = this.db.prepare(`
|
|
525
|
+
INSERT INTO tasks (plan_id, file_path, status, error, dependencies_json)
|
|
526
|
+
VALUES (?, ?, ?, ?, '[]')
|
|
527
|
+
ON CONFLICT(plan_id, file_path) DO UPDATE
|
|
528
|
+
SET status = excluded.status, error = excluded.error
|
|
529
|
+
`);
|
|
530
|
+
stmt.run(planId, filePath, status, error || null);
|
|
531
|
+
}
|
|
532
|
+
async logEvent(eventName, payload) {
|
|
533
|
+
const stmt = this.db.prepare(`
|
|
534
|
+
INSERT INTO events (event_name, payload_json, timestamp)
|
|
535
|
+
VALUES (?, ?, ?)
|
|
536
|
+
`);
|
|
537
|
+
stmt.run(eventName, JSON.stringify(payload), (/* @__PURE__ */ new Date()).toISOString());
|
|
538
|
+
}
|
|
539
|
+
async getAllPlans() {
|
|
540
|
+
const stmtPlans = this.db.prepare(`SELECT * FROM plans ORDER BY created_at DESC`);
|
|
541
|
+
const planRows = stmtPlans.all();
|
|
542
|
+
const stmtTasks = this.db.prepare(`SELECT * FROM tasks`);
|
|
543
|
+
const taskRows = stmtTasks.all();
|
|
544
|
+
return planRows.map((planRow) => {
|
|
545
|
+
const planTasks = taskRows.filter((t) => t.plan_id === planRow.id);
|
|
546
|
+
return {
|
|
547
|
+
id: planRow.id,
|
|
548
|
+
runId: planRow.run_id,
|
|
549
|
+
targetPath: planRow.target_path,
|
|
550
|
+
profile: {
|
|
551
|
+
source: planRow.source_framework,
|
|
552
|
+
target: planRow.target_framework,
|
|
553
|
+
rules: JSON.parse(planRow.rules_json)
|
|
554
|
+
},
|
|
555
|
+
createdAt: new Date(planRow.created_at),
|
|
556
|
+
tasks: planTasks.map((row) => ({
|
|
557
|
+
filePath: row.file_path,
|
|
558
|
+
status: row.status,
|
|
559
|
+
dependencies: JSON.parse(row.dependencies_json),
|
|
560
|
+
error: row.error || void 0
|
|
561
|
+
}))
|
|
562
|
+
};
|
|
563
|
+
});
|
|
564
|
+
}
|
|
565
|
+
async getEvents() {
|
|
566
|
+
const stmt = this.db.prepare(`SELECT * FROM events ORDER BY id DESC LIMIT 100`);
|
|
567
|
+
const rows = stmt.all();
|
|
568
|
+
return rows.map((row) => ({
|
|
569
|
+
id: row.id,
|
|
570
|
+
eventName: row.event_name,
|
|
571
|
+
payload: JSON.parse(row.payload_json),
|
|
572
|
+
timestamp: new Date(row.timestamp)
|
|
573
|
+
}));
|
|
574
|
+
}
|
|
575
|
+
async reset() {
|
|
576
|
+
this.db.exec(`DELETE FROM tasks`);
|
|
577
|
+
this.db.exec(`DELETE FROM plans`);
|
|
578
|
+
this.db.exec(`DELETE FROM events`);
|
|
579
|
+
}
|
|
580
|
+
};
|
|
581
|
+
|
|
582
|
+
// ../infrastructure/src/tools/AstTools.ts
|
|
583
|
+
var import_ts_morph = __toESM(require_ts_morph(), 1);
|
|
584
|
+
import { existsSync } from "fs";
|
|
585
|
+
var project = new import_ts_morph.Project();
|
|
586
|
+
function createReadFileTool() {
|
|
587
|
+
return {
|
|
588
|
+
type: "function",
|
|
589
|
+
name: "read_file",
|
|
590
|
+
description: "Reads the content of a source file using ts-morph.",
|
|
591
|
+
parameters: {
|
|
592
|
+
type: "object",
|
|
593
|
+
properties: {
|
|
594
|
+
filePath: { type: "string", description: "The relative path to the file to read." }
|
|
595
|
+
},
|
|
596
|
+
required: ["filePath"],
|
|
597
|
+
additionalProperties: false
|
|
598
|
+
},
|
|
599
|
+
strict: true,
|
|
600
|
+
invoke: async ({ filePath }) => {
|
|
601
|
+
if (!existsSync(filePath)) {
|
|
602
|
+
return { error: `File not found: ${filePath}` };
|
|
603
|
+
}
|
|
604
|
+
let sourceFile = project.getSourceFile(filePath);
|
|
605
|
+
if (!sourceFile) {
|
|
606
|
+
project.addSourceFileAtPath(filePath);
|
|
607
|
+
sourceFile = project.getSourceFileOrThrow(filePath);
|
|
608
|
+
} else {
|
|
609
|
+
await sourceFile.refreshFromFileSystem();
|
|
610
|
+
}
|
|
611
|
+
return {
|
|
612
|
+
filePath,
|
|
613
|
+
content: sourceFile.getFullText()
|
|
614
|
+
};
|
|
615
|
+
}
|
|
616
|
+
};
|
|
617
|
+
}
|
|
618
|
+
function createWriteFileTool() {
|
|
619
|
+
return {
|
|
620
|
+
type: "function",
|
|
621
|
+
name: "write_file",
|
|
622
|
+
description: "Overwrites the content of a source file with the provided refactored code using ts-morph.",
|
|
623
|
+
parameters: {
|
|
624
|
+
type: "object",
|
|
625
|
+
properties: {
|
|
626
|
+
filePath: { type: "string", description: "The relative path to the file to modify." },
|
|
627
|
+
newContent: { type: "string", description: "The entire new source code to write to the file." }
|
|
628
|
+
},
|
|
629
|
+
required: ["filePath", "newContent"],
|
|
630
|
+
additionalProperties: false
|
|
631
|
+
},
|
|
632
|
+
strict: true,
|
|
633
|
+
invoke: async ({ filePath, newContent }) => {
|
|
634
|
+
let sourceFile = project.getSourceFile(filePath);
|
|
635
|
+
if (!sourceFile) {
|
|
636
|
+
if (existsSync(filePath)) {
|
|
637
|
+
project.addSourceFileAtPath(filePath);
|
|
638
|
+
sourceFile = project.getSourceFileOrThrow(filePath);
|
|
639
|
+
} else {
|
|
640
|
+
sourceFile = project.createSourceFile(filePath, "");
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
sourceFile.replaceWithText(newContent);
|
|
644
|
+
await sourceFile.save();
|
|
645
|
+
return {
|
|
646
|
+
success: true,
|
|
647
|
+
message: `File ${filePath} successfully updated.`
|
|
648
|
+
};
|
|
649
|
+
}
|
|
650
|
+
};
|
|
651
|
+
}
|
|
652
|
+
function createRenameFileTool() {
|
|
653
|
+
return {
|
|
654
|
+
type: "function",
|
|
655
|
+
name: "rename_file",
|
|
656
|
+
description: "Renames a file in the project.",
|
|
657
|
+
parameters: {
|
|
658
|
+
type: "object",
|
|
659
|
+
properties: {
|
|
660
|
+
oldPath: { type: "string", description: "Current relative path of the file." },
|
|
661
|
+
newPath: { type: "string", description: "New relative path for the file." }
|
|
662
|
+
},
|
|
663
|
+
required: ["oldPath", "newPath"],
|
|
664
|
+
additionalProperties: false
|
|
665
|
+
},
|
|
666
|
+
strict: true,
|
|
667
|
+
invoke: async ({ oldPath, newPath }) => {
|
|
668
|
+
const { renameSync, existsSync: existsSync5 } = await import("fs");
|
|
669
|
+
if (!existsSync5(oldPath)) {
|
|
670
|
+
return { error: `File not found: ${oldPath}` };
|
|
671
|
+
}
|
|
672
|
+
renameSync(oldPath, newPath);
|
|
673
|
+
const sourceFile = project.getSourceFile(oldPath);
|
|
674
|
+
if (sourceFile) {
|
|
675
|
+
project.removeSourceFile(sourceFile);
|
|
676
|
+
if (existsSync5(newPath)) {
|
|
677
|
+
project.addSourceFileAtPath(newPath);
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
return { success: true, message: `File renamed from ${oldPath} to ${newPath}.` };
|
|
681
|
+
}
|
|
682
|
+
};
|
|
683
|
+
}
|
|
684
|
+
function createCreateFileTool() {
|
|
685
|
+
return {
|
|
686
|
+
type: "function",
|
|
687
|
+
name: "create_file",
|
|
688
|
+
description: "Creates a new file with the specified content.",
|
|
689
|
+
parameters: {
|
|
690
|
+
type: "object",
|
|
691
|
+
properties: {
|
|
692
|
+
filePath: { type: "string", description: "Relative path of the new file to create." },
|
|
693
|
+
content: { type: "string", description: "The content of the new file." }
|
|
694
|
+
},
|
|
695
|
+
required: ["filePath", "content"],
|
|
696
|
+
additionalProperties: false
|
|
697
|
+
},
|
|
698
|
+
strict: true,
|
|
699
|
+
invoke: async ({ filePath, content }) => {
|
|
700
|
+
const { writeFileSync: writeFileSync2, mkdirSync: mkdirSync2 } = await import("fs");
|
|
701
|
+
const { dirname: dirname2 } = await import("path");
|
|
702
|
+
mkdirSync2(dirname2(filePath), { recursive: true });
|
|
703
|
+
writeFileSync2(filePath, content, "utf-8");
|
|
704
|
+
project.addSourceFileAtPath(filePath);
|
|
705
|
+
return { success: true, message: `File created at ${filePath}.` };
|
|
706
|
+
}
|
|
707
|
+
};
|
|
708
|
+
}
|
|
709
|
+
function createDeleteFileTool() {
|
|
710
|
+
return {
|
|
711
|
+
type: "function",
|
|
712
|
+
name: "delete_file",
|
|
713
|
+
description: "Deletes a file from the project.",
|
|
714
|
+
parameters: {
|
|
715
|
+
type: "object",
|
|
716
|
+
properties: {
|
|
717
|
+
filePath: { type: "string", description: "Relative path of the file to delete." }
|
|
718
|
+
},
|
|
719
|
+
required: ["filePath"],
|
|
720
|
+
additionalProperties: false
|
|
721
|
+
},
|
|
722
|
+
strict: true,
|
|
723
|
+
invoke: async ({ filePath }) => {
|
|
724
|
+
const { renameSync, existsSync: existsSync5 } = await import("fs");
|
|
725
|
+
if (!existsSync5(filePath)) {
|
|
726
|
+
return { error: `File not found: ${filePath}` };
|
|
727
|
+
}
|
|
728
|
+
const obsoletePath = `${filePath}.obsolete`;
|
|
729
|
+
renameSync(filePath, obsoletePath);
|
|
730
|
+
const sourceFile = project.getSourceFile(filePath);
|
|
731
|
+
if (sourceFile) {
|
|
732
|
+
project.removeSourceFile(sourceFile);
|
|
733
|
+
}
|
|
734
|
+
return { success: true, message: `File marked as obsolete at ${obsoletePath} instead of deleting.` };
|
|
735
|
+
}
|
|
736
|
+
};
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
// ../infrastructure/src/workspace/ShadowWorkspace.ts
|
|
740
|
+
import fs from "fs-extra";
|
|
741
|
+
import { join as join3, resolve } from "path";
|
|
742
|
+
var ShadowWorkspace = class {
|
|
743
|
+
baseDir;
|
|
744
|
+
constructor(basePath = ".metamorph/shadow") {
|
|
745
|
+
this.baseDir = resolve(process.cwd(), basePath);
|
|
746
|
+
}
|
|
747
|
+
/**
|
|
748
|
+
* Clones a target directory into the shadow workspace.
|
|
749
|
+
* @param targetPath The absolute or relative path to the directory to clone.
|
|
750
|
+
* @param runId A unique identifier for this run (e.g. timestamp).
|
|
751
|
+
* @returns The absolute path to the cloned directory inside the shadow workspace.
|
|
752
|
+
*/
|
|
753
|
+
cloneDirectory(targetPath, runId) {
|
|
754
|
+
const sourceDir = resolve(process.cwd(), targetPath);
|
|
755
|
+
if (!fs.existsSync(sourceDir)) {
|
|
756
|
+
throw new Error(`Target directory does not exist: ${sourceDir}`);
|
|
757
|
+
}
|
|
758
|
+
const shadowDir = join3(this.baseDir, runId);
|
|
759
|
+
if (fs.existsSync(shadowDir)) {
|
|
760
|
+
fs.emptyDirSync(shadowDir);
|
|
761
|
+
}
|
|
762
|
+
const ignoredDirs = ["node_modules", ".git", ".metamorph", "dist", "build", "out", "coverage", ".next"];
|
|
763
|
+
const items = fs.readdirSync(sourceDir);
|
|
764
|
+
for (const item of items) {
|
|
765
|
+
if (ignoredDirs.includes(item)) {
|
|
766
|
+
continue;
|
|
767
|
+
}
|
|
768
|
+
const itemSrc = join3(sourceDir, item);
|
|
769
|
+
const itemDest = join3(shadowDir, item);
|
|
770
|
+
fs.copySync(itemSrc, itemDest, {
|
|
771
|
+
filter: (src) => !ignoredDirs.some((dir) => src.includes(`/${dir}/`) || src.endsWith(`/${dir}`) || src.includes(`\\${dir}\\`) || src.endsWith(`\\${dir}`))
|
|
772
|
+
});
|
|
773
|
+
}
|
|
774
|
+
return shadowDir;
|
|
775
|
+
}
|
|
776
|
+
/**
|
|
777
|
+
* Removes the shadow workspace for a given run, effectively
|
|
778
|
+
* discarding any in-progress migration work.
|
|
779
|
+
*/
|
|
780
|
+
rollback(runId) {
|
|
781
|
+
const shadowDir = join3(this.baseDir, runId);
|
|
782
|
+
if (fs.existsSync(shadowDir)) {
|
|
783
|
+
fs.removeSync(shadowDir);
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
/**
|
|
787
|
+
* Returns the absolute path of the shadow dir for a given runId.
|
|
788
|
+
*/
|
|
789
|
+
getShadowPath(runId) {
|
|
790
|
+
return join3(this.baseDir, runId);
|
|
791
|
+
}
|
|
792
|
+
/**
|
|
793
|
+
* Cleans up old shadow runs to save disk space, keeping only the specified number of most recent runs.
|
|
794
|
+
* @param keepCount Number of recent runs to keep (default: 3)
|
|
795
|
+
*/
|
|
796
|
+
cleanupOldRuns(keepCount = 3) {
|
|
797
|
+
if (!fs.existsSync(this.baseDir)) return;
|
|
798
|
+
const runs = fs.readdirSync(this.baseDir).filter((f) => f.startsWith("run_") && fs.statSync(join3(this.baseDir, f)).isDirectory());
|
|
799
|
+
runs.sort();
|
|
800
|
+
if (runs.length > keepCount) {
|
|
801
|
+
const toDelete = runs.slice(0, runs.length - keepCount);
|
|
802
|
+
for (const run of toDelete) {
|
|
803
|
+
this.rollback(run);
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
};
|
|
808
|
+
|
|
809
|
+
// ../infrastructure/src/workspace/MigrationIntegrator.ts
|
|
810
|
+
import fs2 from "fs-extra";
|
|
811
|
+
import * as path from "path";
|
|
812
|
+
import { execSync } from "child_process";
|
|
813
|
+
var MigrationIntegrator = class {
|
|
814
|
+
shadowWorkspace;
|
|
815
|
+
constructor(shadowWorkspace) {
|
|
816
|
+
this.shadowWorkspace = shadowWorkspace;
|
|
817
|
+
}
|
|
818
|
+
/**
|
|
819
|
+
* Applies the migrated code from the shadow workspace back to the target directory.
|
|
820
|
+
* Prioritizes Git if available, otherwise falls back to a Zip backup.
|
|
821
|
+
*
|
|
822
|
+
* @param runId The ID of the migration run to apply.
|
|
823
|
+
* @param targetPath The original target path of the project.
|
|
824
|
+
*/
|
|
825
|
+
async applyMigration(runId, targetPath) {
|
|
826
|
+
const shadowDir = this.shadowWorkspace.getShadowPath(runId);
|
|
827
|
+
if (!fs2.existsSync(shadowDir)) {
|
|
828
|
+
throw new Error(`Shadow run not found: ${shadowDir}`);
|
|
829
|
+
}
|
|
830
|
+
const resolvedTarget = path.resolve(process.cwd(), targetPath);
|
|
831
|
+
if (this.isGitRepo(resolvedTarget)) {
|
|
832
|
+
return this.applyWithGit(runId, shadowDir, resolvedTarget);
|
|
833
|
+
} else {
|
|
834
|
+
return this.applyWithZipFallback(runId, shadowDir, resolvedTarget);
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
isGitRepo(targetPath) {
|
|
838
|
+
try {
|
|
839
|
+
execSync("git rev-parse --is-inside-work-tree", { cwd: targetPath, stdio: "ignore" });
|
|
840
|
+
return true;
|
|
841
|
+
} catch {
|
|
842
|
+
return false;
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
async applyWithGit(runId, shadowDir, targetPath) {
|
|
846
|
+
const branchName = `metamorph/${runId}`;
|
|
847
|
+
try {
|
|
848
|
+
execSync(`git checkout -B ${branchName}`, { cwd: targetPath, stdio: "ignore" });
|
|
849
|
+
} catch (e) {
|
|
850
|
+
if (e instanceof Error) {
|
|
851
|
+
throw new Error(`Failed to create/reset git branch ${branchName}: ${e.message}`);
|
|
852
|
+
}
|
|
853
|
+
throw new Error(`Failed to create/reset git branch ${branchName}: ${String(e)}`);
|
|
854
|
+
}
|
|
855
|
+
this.copyShadowToTarget(shadowDir, targetPath);
|
|
856
|
+
try {
|
|
857
|
+
execSync(`git add .`, { cwd: targetPath, stdio: "ignore" });
|
|
858
|
+
execSync(`git commit --allow-empty -m "chore: apply metamorph ai migration (${runId})"`, { cwd: targetPath, stdio: "ignore" });
|
|
859
|
+
} catch (e) {
|
|
860
|
+
if (e instanceof Error) {
|
|
861
|
+
throw new Error(`Failed to commit changes to branch ${branchName}: ${e.message}`);
|
|
862
|
+
}
|
|
863
|
+
throw new Error(`Failed to commit changes to branch ${branchName}: ${String(e)}`);
|
|
864
|
+
}
|
|
865
|
+
return `Successfully applied migration via Git to branch: ${branchName}`;
|
|
866
|
+
}
|
|
867
|
+
async applyWithZipFallback(runId, shadowDir, targetPath) {
|
|
868
|
+
const backupDir = path.resolve(process.cwd(), `.metamorph/backups/${runId}_backup`);
|
|
869
|
+
fs2.ensureDirSync(backupDir);
|
|
870
|
+
fs2.copySync(targetPath, backupDir, {
|
|
871
|
+
filter: (src) => !src.includes("node_modules") && !src.includes(".git") && !src.includes(".metamorph")
|
|
872
|
+
});
|
|
873
|
+
this.copyShadowToTarget(shadowDir, targetPath);
|
|
874
|
+
return `Successfully applied migration. Git was not detected, so a backup was saved at: ${backupDir}`;
|
|
875
|
+
}
|
|
876
|
+
copyShadowToTarget(shadowDir, targetPath) {
|
|
877
|
+
const ignoredDirs = ["node_modules", ".git", ".metamorph"];
|
|
878
|
+
if (fs2.existsSync(targetPath)) {
|
|
879
|
+
const targetItems = fs2.readdirSync(targetPath);
|
|
880
|
+
for (const item of targetItems) {
|
|
881
|
+
if (ignoredDirs.includes(item)) continue;
|
|
882
|
+
fs2.removeSync(path.join(targetPath, item));
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
const shadowItems = fs2.readdirSync(shadowDir);
|
|
886
|
+
for (const item of shadowItems) {
|
|
887
|
+
if (ignoredDirs.includes(item)) continue;
|
|
888
|
+
const itemSrc = path.join(shadowDir, item);
|
|
889
|
+
const itemDest = path.join(targetPath, item);
|
|
890
|
+
fs2.copySync(itemSrc, itemDest, { overwrite: true });
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
};
|
|
894
|
+
|
|
895
|
+
// ../domain/src/entities/catalogs/backendCatalog.ts
|
|
896
|
+
var backendCatalog = [
|
|
897
|
+
{
|
|
898
|
+
source: "nestjs",
|
|
899
|
+
target: "express",
|
|
900
|
+
description: "Migration from NestJS framework to a plain Express application structure.",
|
|
901
|
+
filesToDelete: ["nest-cli.json", "tsconfig.build.json"],
|
|
902
|
+
dependenciesToRemove: ["@nestjs/common", "@nestjs/core", "@nestjs/platform-express", "reflect-metadata", "rxjs"],
|
|
903
|
+
dependenciesToAdd: { "express": "^4.18.2" },
|
|
904
|
+
devDependenciesToRemove: ["@nestjs/cli", "@nestjs/schematics", "@nestjs/testing", "@types/express", "@types/supertest", "source-map-support", "supertest"],
|
|
905
|
+
devDependenciesToAdd: { "@types/express": "^4.17.17" },
|
|
906
|
+
architecturalRules: [
|
|
907
|
+
"Remove all NestJS decorators (@Module, @Controller, @Injectable, @Get, @Post, etc.).",
|
|
908
|
+
'Remove all NestJS core imports (e.g., from "@nestjs/common" or "@nestjs/core").',
|
|
909
|
+
"Controllers must be converted into Express Router factory functions or classes that return an Express Router.",
|
|
910
|
+
"Modules must be converted to plain dependency composition functions or startup scripts that wire Routers together.",
|
|
911
|
+
"Services/Providers must be converted to plain TypeScript classes or functions, with dependencies passed explicitly via constructors (manual dependency injection) since NestJS DI is gone.",
|
|
912
|
+
"Make sure to export the router or the configured Express application, not a Nest module."
|
|
913
|
+
],
|
|
914
|
+
examples: [
|
|
915
|
+
{
|
|
916
|
+
description: "Converting a NestJS Controller to an Express Router",
|
|
917
|
+
before: `import { Controller, Get } from '@nestjs/common';
|
|
918
|
+
import { AppService } from './app.service';
|
|
919
|
+
|
|
920
|
+
@Controller()
|
|
921
|
+
export class AppController {
|
|
922
|
+
constructor(private readonly appService: AppService) {}
|
|
923
|
+
|
|
924
|
+
@Get()
|
|
925
|
+
getHello(): string {
|
|
926
|
+
return this.appService.getHello();
|
|
927
|
+
}
|
|
928
|
+
}`,
|
|
929
|
+
after: `import { Router } from 'express';
|
|
930
|
+
import { AppService } from './app.service';
|
|
931
|
+
|
|
932
|
+
export function createAppRouter(appService: AppService): Router {
|
|
933
|
+
const router = Router();
|
|
934
|
+
|
|
935
|
+
router.get('/', (req, res) => {
|
|
936
|
+
const result = appService.getHello();
|
|
937
|
+
res.send(result);
|
|
938
|
+
});
|
|
939
|
+
|
|
940
|
+
return router;
|
|
941
|
+
}`
|
|
942
|
+
}
|
|
943
|
+
]
|
|
944
|
+
},
|
|
945
|
+
{
|
|
946
|
+
source: "express",
|
|
947
|
+
target: "nestjs",
|
|
948
|
+
description: "Migration from a plain Express application to the NestJS framework.",
|
|
949
|
+
dependenciesToRemove: ["express"],
|
|
950
|
+
dependenciesToAdd: {
|
|
951
|
+
"@nestjs/common": "latest",
|
|
952
|
+
"@nestjs/core": "latest",
|
|
953
|
+
"@nestjs/platform-express": "latest",
|
|
954
|
+
"reflect-metadata": "^0.1.13",
|
|
955
|
+
"rxjs": "^7.8.1"
|
|
956
|
+
},
|
|
957
|
+
devDependenciesToRemove: ["@types/express"],
|
|
958
|
+
devDependenciesToAdd: {
|
|
959
|
+
"@nestjs/cli": "latest",
|
|
960
|
+
"@nestjs/schematics": "latest",
|
|
961
|
+
"@nestjs/testing": "latest",
|
|
962
|
+
"@types/supertest": "^2.0.12",
|
|
963
|
+
"source-map-support": "^0.5.21",
|
|
964
|
+
"supertest": "^6.3.3"
|
|
965
|
+
},
|
|
966
|
+
architecturalRules: [
|
|
967
|
+
"Express routing logic must be encapsulated in classes decorated with @Controller().",
|
|
968
|
+
"Business logic must be encapsulated in classes decorated with @Injectable() (Providers/Services).",
|
|
969
|
+
"All Controllers and Providers must be registered in a class decorated with @Module().",
|
|
970
|
+
"Use NestJS decorators for routing (@Get(), @Post(), @Param(), @Body(), etc.) instead of Express route definitions.",
|
|
971
|
+
"Rely on NestJS dependency injection instead of manual class instantiation or function passing.",
|
|
972
|
+
"The entry point must use NestFactory.create() to bootstrap the application, replacing app.listen().",
|
|
973
|
+
"You are authorized to rename, create, or delete files to adhere strictly to the NestJS directory and file structure conventions (e.g., app.module.ts, app.controller.ts, app.service.ts, main.ts)."
|
|
974
|
+
],
|
|
975
|
+
examples: [
|
|
976
|
+
{
|
|
977
|
+
description: "Converting an Express Router to a NestJS Controller and Service",
|
|
978
|
+
before: `import { Router } from 'express';
|
|
979
|
+
import { getHelloLogic } from './logic';
|
|
980
|
+
|
|
981
|
+
export function createAppRouter(): Router {
|
|
982
|
+
const router = Router();
|
|
983
|
+
router.get('/', (req, res) => {
|
|
984
|
+
res.send(getHelloLogic());
|
|
985
|
+
});
|
|
986
|
+
return router;
|
|
987
|
+
}`,
|
|
988
|
+
after: `import { Controller, Get, Injectable } from '@nestjs/common';
|
|
989
|
+
|
|
990
|
+
@Injectable()
|
|
991
|
+
export class AppService {
|
|
992
|
+
getHello(): string {
|
|
993
|
+
return 'Hello from logic';
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
@Controller()
|
|
998
|
+
export class AppController {
|
|
999
|
+
constructor(private readonly appService: AppService) {}
|
|
1000
|
+
|
|
1001
|
+
@Get()
|
|
1002
|
+
getHello(): string {
|
|
1003
|
+
return this.appService.getHello();
|
|
1004
|
+
}
|
|
1005
|
+
}`
|
|
1006
|
+
}
|
|
1007
|
+
]
|
|
1008
|
+
},
|
|
1009
|
+
{
|
|
1010
|
+
source: "nestjs",
|
|
1011
|
+
target: "fastify",
|
|
1012
|
+
description: "Migration from NestJS framework to a Fastify application structure.",
|
|
1013
|
+
filesToDelete: ["nest-cli.json", "tsconfig.build.json"],
|
|
1014
|
+
dependenciesToRemove: ["@nestjs/common", "@nestjs/core", "@nestjs/platform-express", "reflect-metadata", "rxjs"],
|
|
1015
|
+
dependenciesToAdd: { "fastify": "latest" },
|
|
1016
|
+
devDependenciesToRemove: ["@nestjs/cli", "@nestjs/schematics", "@nestjs/testing", "@types/express", "@types/supertest", "source-map-support", "supertest"],
|
|
1017
|
+
devDependenciesToAdd: { "fastify-plugin": "latest" },
|
|
1018
|
+
architecturalRules: [
|
|
1019
|
+
"Remove all NestJS decorators (@Module, @Controller, @Injectable, @Get, @Post, etc.).",
|
|
1020
|
+
"Remove all NestJS core imports.",
|
|
1021
|
+
"The entry point (main.ts) must initialize a FastifyInstance (fastify()) instead of NestFactory.",
|
|
1022
|
+
"Controllers and routing logic must be converted to Fastify route declarations (fastify.get, fastify.post).",
|
|
1023
|
+
"Ideally, encapsulate routes and modules as Fastify plugins.",
|
|
1024
|
+
"Remove NestJS dependency injection; use standard JavaScript/TypeScript classes or functions passing dependencies directly.",
|
|
1025
|
+
"Ensure standard Fastify schema validation (JSON Schema) is added if types or DTOs are available.",
|
|
1026
|
+
"You are authorized to rename, create, or delete files to adhere to a standard fastify plugin-based directory structure."
|
|
1027
|
+
],
|
|
1028
|
+
examples: [
|
|
1029
|
+
{
|
|
1030
|
+
description: "Converting a NestJS Controller to a Fastify Plugin",
|
|
1031
|
+
before: `import { Controller, Get } from '@nestjs/common';
|
|
1032
|
+
import { AppService } from './app.service';
|
|
1033
|
+
|
|
1034
|
+
@Controller('users')
|
|
1035
|
+
export class UsersController {
|
|
1036
|
+
constructor(private readonly appService: AppService) {}
|
|
1037
|
+
|
|
1038
|
+
@Get()
|
|
1039
|
+
getUsers() {
|
|
1040
|
+
return this.appService.getUsers();
|
|
1041
|
+
}
|
|
1042
|
+
}`,
|
|
1043
|
+
after: `import { FastifyInstance } from 'fastify';
|
|
1044
|
+
import { AppService } from './app.service';
|
|
1045
|
+
|
|
1046
|
+
export default async function usersPlugin(fastify: FastifyInstance, options: any) {
|
|
1047
|
+
const appService = new AppService(); // Or inject via options
|
|
1048
|
+
|
|
1049
|
+
fastify.get('/users', async (request, reply) => {
|
|
1050
|
+
return appService.getUsers();
|
|
1051
|
+
});
|
|
1052
|
+
}`
|
|
1053
|
+
}
|
|
1054
|
+
]
|
|
1055
|
+
},
|
|
1056
|
+
{
|
|
1057
|
+
source: "express",
|
|
1058
|
+
target: "fastify",
|
|
1059
|
+
description: "Migration from a plain Express application to Fastify.",
|
|
1060
|
+
dependenciesToRemove: ["express"],
|
|
1061
|
+
dependenciesToAdd: { "fastify": "latest", "fastify-plugin": "latest" },
|
|
1062
|
+
devDependenciesToRemove: ["@types/express"],
|
|
1063
|
+
devDependenciesToAdd: {},
|
|
1064
|
+
architecturalRules: [
|
|
1065
|
+
"Replace all Express app instantiations with fastify().",
|
|
1066
|
+
"Replace Express middleware signatures (req, res, next) with Fastify hooks (onRequest, preHandler, etc.) or Fastify plugins.",
|
|
1067
|
+
"Replace Express routing (app.use(), app.get()) with Fastify routing (fastify.get(), fastify.register()).",
|
|
1068
|
+
"Replace Express response methods (res.send(), res.json()) with Fastify reply methods (reply.send()) or simply return the payload from the async handler.",
|
|
1069
|
+
"Migrate standard Express middlewares (like cors, helmet) to their Fastify native equivalents (@fastify/cors, @fastify/helmet) if they are present.",
|
|
1070
|
+
"You are authorized to rename, create, or delete files to restructure the Express app into modular Fastify plugins."
|
|
1071
|
+
],
|
|
1072
|
+
examples: [
|
|
1073
|
+
{
|
|
1074
|
+
description: "Converting an Express Route to a Fastify Route",
|
|
1075
|
+
before: `const express = require('express');
|
|
1076
|
+
const app = express();
|
|
1077
|
+
|
|
1078
|
+
app.get('/api/data', (req, res) => {
|
|
1079
|
+
res.json({ message: 'Hello Express' });
|
|
1080
|
+
});
|
|
1081
|
+
|
|
1082
|
+
app.listen(3000);`,
|
|
1083
|
+
after: `import Fastify from 'fastify';
|
|
1084
|
+
|
|
1085
|
+
const fastify = Fastify({ logger: true });
|
|
1086
|
+
|
|
1087
|
+
fastify.get('/api/data', async (request, reply) => {
|
|
1088
|
+
return { message: 'Hello Fastify' };
|
|
1089
|
+
});
|
|
1090
|
+
|
|
1091
|
+
fastify.listen({ port: 3000 }, (err) => {
|
|
1092
|
+
if (err) {
|
|
1093
|
+
fastify.log.error(err);
|
|
1094
|
+
process.exit(1);
|
|
1095
|
+
}
|
|
1096
|
+
});`
|
|
1097
|
+
}
|
|
1098
|
+
]
|
|
1099
|
+
},
|
|
1100
|
+
{
|
|
1101
|
+
source: "fastify",
|
|
1102
|
+
target: "express",
|
|
1103
|
+
description: "Migration from a Fastify application to Express.",
|
|
1104
|
+
dependenciesToRemove: ["fastify", "fastify-plugin"],
|
|
1105
|
+
dependenciesToAdd: { "express": "latest" },
|
|
1106
|
+
devDependenciesToRemove: [],
|
|
1107
|
+
devDependenciesToAdd: { "@types/express": "latest" },
|
|
1108
|
+
architecturalRules: [
|
|
1109
|
+
"Replace all fastify() app instantiations with express().",
|
|
1110
|
+
"Replace Fastify hooks (onRequest, preHandler, etc.) with standard Express middleware signatures (req, res, next).",
|
|
1111
|
+
"Replace Fastify routing (fastify.get, fastify.register) with Express routing (app.get, express.Router().use).",
|
|
1112
|
+
"Replace Fastify reply methods (reply.send) or implicit returns with Express response methods (res.send, res.json).",
|
|
1113
|
+
"CRITICAL: Fastify catches async errors automatically. When migrating to Express, ensure all asynchronous route handlers use try/catch blocks passing the error to next(err), or wrap them in an async wrapper.",
|
|
1114
|
+
"You are authorized to rename, create, or delete files to restructure the Fastify plugins into standard Express routers."
|
|
1115
|
+
],
|
|
1116
|
+
examples: [
|
|
1117
|
+
{
|
|
1118
|
+
description: "Converting a Fastify Route to an Express Route",
|
|
1119
|
+
before: `import Fastify from 'fastify';
|
|
1120
|
+
|
|
1121
|
+
const fastify = Fastify();
|
|
1122
|
+
|
|
1123
|
+
fastify.get('/api/data', async (request, reply) => {
|
|
1124
|
+
return { message: 'Hello Fastify' };
|
|
1125
|
+
});
|
|
1126
|
+
|
|
1127
|
+
fastify.listen({ port: 3000 });`,
|
|
1128
|
+
after: `const express = require('express');
|
|
1129
|
+
const app = express();
|
|
1130
|
+
|
|
1131
|
+
app.get('/api/data', async (req, res, next) => {
|
|
1132
|
+
try {
|
|
1133
|
+
res.json({ message: 'Hello Fastify' });
|
|
1134
|
+
} catch (err) {
|
|
1135
|
+
next(err);
|
|
1136
|
+
}
|
|
1137
|
+
});
|
|
1138
|
+
|
|
1139
|
+
app.listen(3000);`
|
|
1140
|
+
}
|
|
1141
|
+
]
|
|
1142
|
+
},
|
|
1143
|
+
{
|
|
1144
|
+
source: "fastify",
|
|
1145
|
+
target: "nestjs",
|
|
1146
|
+
description: "Migration from Fastify framework to NestJS (using platform-express by default).",
|
|
1147
|
+
dependenciesToRemove: ["fastify", "fastify-plugin"],
|
|
1148
|
+
dependenciesToAdd: {
|
|
1149
|
+
"@nestjs/common": "latest",
|
|
1150
|
+
"@nestjs/core": "latest",
|
|
1151
|
+
"@nestjs/platform-express": "latest",
|
|
1152
|
+
"reflect-metadata": "^0.1.13",
|
|
1153
|
+
"rxjs": "^7.8.1"
|
|
1154
|
+
},
|
|
1155
|
+
devDependenciesToRemove: [],
|
|
1156
|
+
devDependenciesToAdd: {
|
|
1157
|
+
"@nestjs/cli": "latest",
|
|
1158
|
+
"@nestjs/schematics": "latest",
|
|
1159
|
+
"@nestjs/testing": "latest",
|
|
1160
|
+
"@types/supertest": "^2.0.12",
|
|
1161
|
+
"source-map-support": "^0.5.21",
|
|
1162
|
+
"supertest": "^6.3.3"
|
|
1163
|
+
},
|
|
1164
|
+
architecturalRules: [
|
|
1165
|
+
"Fastify routing logic must be encapsulated in classes decorated with @Controller().",
|
|
1166
|
+
"Business logic must be encapsulated in classes decorated with @Injectable() (Providers/Services).",
|
|
1167
|
+
"All Controllers and Providers must be registered in a class decorated with @Module().",
|
|
1168
|
+
"Use NestJS decorators for routing (@Get(), @Post(), @Param(), @Body(), etc.) instead of fastify route definitions.",
|
|
1169
|
+
"Rely on NestJS dependency injection instead of Fastify decorators or options passing.",
|
|
1170
|
+
"The entry point must use NestFactory.create() to bootstrap the application, replacing fastify().",
|
|
1171
|
+
"You are authorized to rename, create, or delete files to adhere strictly to the NestJS directory and file structure conventions (e.g., app.module.ts, app.controller.ts, app.service.ts, main.ts)."
|
|
1172
|
+
],
|
|
1173
|
+
examples: [
|
|
1174
|
+
{
|
|
1175
|
+
description: "Converting a Fastify Plugin to a NestJS Controller and Service",
|
|
1176
|
+
before: `export default async function usersPlugin(fastify, options) {
|
|
1177
|
+
fastify.get('/users', async (request, reply) => {
|
|
1178
|
+
return { users: [] };
|
|
1179
|
+
});
|
|
1180
|
+
}`,
|
|
1181
|
+
after: `import { Controller, Get, Injectable } from '@nestjs/common';
|
|
1182
|
+
|
|
1183
|
+
@Injectable()
|
|
1184
|
+
export class UsersService {
|
|
1185
|
+
getUsers() {
|
|
1186
|
+
return { users: [] };
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
@Controller('users')
|
|
1191
|
+
export class UsersController {
|
|
1192
|
+
constructor(private readonly usersService: UsersService) {}
|
|
1193
|
+
|
|
1194
|
+
@Get()
|
|
1195
|
+
getUsers() {
|
|
1196
|
+
return this.usersService.getUsers();
|
|
1197
|
+
}
|
|
1198
|
+
}`
|
|
1199
|
+
}
|
|
1200
|
+
]
|
|
1201
|
+
}
|
|
1202
|
+
];
|
|
1203
|
+
|
|
1204
|
+
// ../domain/src/entities/catalogs/frontendCatalog.ts
|
|
1205
|
+
var frontendCatalog = [
|
|
1206
|
+
{
|
|
1207
|
+
source: "react",
|
|
1208
|
+
target: "next",
|
|
1209
|
+
description: "Migration from a plain React SPA (like Vite or Create React App) to Next.js using the App Router.",
|
|
1210
|
+
filesToDelete: ["vite.config.ts", "vite.config.js", "index.html"],
|
|
1211
|
+
dependenciesToRemove: ["react-router-dom", "vite", "@vitejs/plugin-react"],
|
|
1212
|
+
dependenciesToAdd: { "next": "latest", "react": "latest", "react-dom": "latest" },
|
|
1213
|
+
devDependenciesToRemove: [],
|
|
1214
|
+
devDependenciesToAdd: {},
|
|
1215
|
+
architecturalRules: [
|
|
1216
|
+
'Migrate standard React routing (e.g. react-router-dom) to Next.js file-system routing in the "app" directory.',
|
|
1217
|
+
"Convert the main entry point (usually main.tsx or index.tsx) into Next.js app/layout.tsx and app/page.tsx.",
|
|
1218
|
+
'For components that use browser-only APIs (like window, useEffect, useState), add the "use client" directive at the very top of the file.',
|
|
1219
|
+
"Remove any react-router-dom <BrowserRouter>, <Routes>, and <Route> components.",
|
|
1220
|
+
"Replace react-router-dom <Link> components with Next.js next/link <Link> components.",
|
|
1221
|
+
"You are authorized to rename, create, or delete files to restructure the source into the Next.js App Router structure (e.g. app/page.tsx, app/layout.tsx)."
|
|
1222
|
+
],
|
|
1223
|
+
examples: [
|
|
1224
|
+
{
|
|
1225
|
+
description: "Converting a React Router setup to Next.js App Router",
|
|
1226
|
+
before: `import { BrowserRouter, Routes, Route } from 'react-router-dom';
|
|
1227
|
+
import Home from './Home';
|
|
1228
|
+
import About from './About';
|
|
1229
|
+
|
|
1230
|
+
export default function App() {
|
|
1231
|
+
return (
|
|
1232
|
+
<BrowserRouter>
|
|
1233
|
+
<Routes>
|
|
1234
|
+
<Route path="/" element={<Home />} />
|
|
1235
|
+
<Route path="/about" element={<About />} />
|
|
1236
|
+
</Routes>
|
|
1237
|
+
</BrowserRouter>
|
|
1238
|
+
);
|
|
1239
|
+
}`,
|
|
1240
|
+
after: `// This routing logic is deleted.
|
|
1241
|
+
// Instead, create app/page.tsx for Home and app/about/page.tsx for About.`
|
|
1242
|
+
}
|
|
1243
|
+
]
|
|
1244
|
+
},
|
|
1245
|
+
{
|
|
1246
|
+
source: "next",
|
|
1247
|
+
target: "react",
|
|
1248
|
+
description: "Migration from Next.js to a plain React SPA using Vite.",
|
|
1249
|
+
filesToDelete: ["next.config.js", "next.config.mjs", "next.config.ts"],
|
|
1250
|
+
dependenciesToRemove: ["next"],
|
|
1251
|
+
dependenciesToAdd: { "react-router-dom": "latest" },
|
|
1252
|
+
devDependenciesToRemove: [],
|
|
1253
|
+
devDependenciesToAdd: { "vite": "latest", "@vitejs/plugin-react": "latest" },
|
|
1254
|
+
architecturalRules: [
|
|
1255
|
+
"Replace Next.js file-system routing (app/ or pages/) with declarative React Router (react-router-dom) components.",
|
|
1256
|
+
'Remove all "use client" and "use server" directives, as they are Next.js specific.',
|
|
1257
|
+
"Convert Next.js Server Components into standard React Client Components (they will need to fetch data via useEffect or a data-fetching library).",
|
|
1258
|
+
'Replace Next.js <Link href="..."> with React Router <Link to="...">.',
|
|
1259
|
+
"Replace Next.js next/image <Image> with standard HTML <img>.",
|
|
1260
|
+
"Create a standard Vite entry point (index.html at the root, and src/main.tsx) to bootstrap the React application."
|
|
1261
|
+
]
|
|
1262
|
+
},
|
|
1263
|
+
{
|
|
1264
|
+
source: "vue",
|
|
1265
|
+
target: "react",
|
|
1266
|
+
description: "Migration from Vue.js (Vue 3 Composition API) to React.",
|
|
1267
|
+
filesToDelete: ["vite.config.ts", "vue.config.js"],
|
|
1268
|
+
dependenciesToRemove: ["vue", "vue-router", "pinia"],
|
|
1269
|
+
dependenciesToAdd: { "react": "latest", "react-dom": "latest", "react-router-dom": "latest" },
|
|
1270
|
+
devDependenciesToRemove: ["@vitejs/plugin-vue"],
|
|
1271
|
+
devDependenciesToAdd: { "@vitejs/plugin-react": "latest" },
|
|
1272
|
+
architecturalRules: [
|
|
1273
|
+
"Convert all .vue Single File Components (SFC) into .tsx or .jsx React function components.",
|
|
1274
|
+
"Replace Vue <template> with React JSX/TSX return statements.",
|
|
1275
|
+
"Replace Vue ref() and reactive() with React useState().",
|
|
1276
|
+
"Replace Vue computed() with React useMemo().",
|
|
1277
|
+
"Replace Vue watch() and lifecycle hooks (onMounted, etc.) with React useEffect().",
|
|
1278
|
+
"Replace vue-router with react-router-dom.",
|
|
1279
|
+
"Convert Vue props definition (defineProps) into React function component parameters.",
|
|
1280
|
+
"Convert Vue emit (defineEmits) into React callback props (e.g. onAction={...})."
|
|
1281
|
+
]
|
|
1282
|
+
},
|
|
1283
|
+
{
|
|
1284
|
+
source: "react",
|
|
1285
|
+
target: "vue",
|
|
1286
|
+
description: "Migration from React to Vue.js (Vue 3 Composition API).",
|
|
1287
|
+
filesToDelete: ["vite.config.ts"],
|
|
1288
|
+
dependenciesToRemove: ["react", "react-dom", "react-router-dom"],
|
|
1289
|
+
dependenciesToAdd: { "vue": "latest", "vue-router": "latest" },
|
|
1290
|
+
devDependenciesToRemove: ["@vitejs/plugin-react"],
|
|
1291
|
+
devDependenciesToAdd: { "@vitejs/plugin-vue": "latest" },
|
|
1292
|
+
architecturalRules: [
|
|
1293
|
+
"Convert React .tsx/.jsx function components into Vue 3 .vue Single File Components (using <script setup>).",
|
|
1294
|
+
"Move the JSX return statement into the Vue <template> block.",
|
|
1295
|
+
"Replace React useState() with Vue ref() or reactive().",
|
|
1296
|
+
"Replace React useMemo() with Vue computed().",
|
|
1297
|
+
"Replace React useEffect() with Vue watch() or lifecycle hooks (onMounted).",
|
|
1298
|
+
"Replace react-router-dom with vue-router.",
|
|
1299
|
+
"Extract component props into defineProps().",
|
|
1300
|
+
"Extract callback props (e.g. onClick) into defineEmits() and emit events."
|
|
1301
|
+
]
|
|
1302
|
+
}
|
|
1303
|
+
];
|
|
1304
|
+
|
|
1305
|
+
// ../domain/src/entities/catalogs/index.ts
|
|
1306
|
+
var MigrationCatalog = [
|
|
1307
|
+
...backendCatalog,
|
|
1308
|
+
...frontendCatalog
|
|
1309
|
+
];
|
|
1310
|
+
function findMigrationCatalogEntry(source, target) {
|
|
1311
|
+
return MigrationCatalog.find(
|
|
1312
|
+
(entry) => entry.source.toLowerCase() === source.toLowerCase() && entry.target.toLowerCase() === target.toLowerCase()
|
|
1313
|
+
);
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
// ../infrastructure/src/server/api.ts
|
|
1317
|
+
async function createApiServer(store, migrationRunner) {
|
|
1318
|
+
const express = (await import("express")).default;
|
|
1319
|
+
const cors = (await import("cors")).default;
|
|
1320
|
+
const app = express();
|
|
1321
|
+
app.use(cors());
|
|
1322
|
+
app.use(express.json());
|
|
1323
|
+
app.get("/api/plans", async (_req, res) => {
|
|
1324
|
+
try {
|
|
1325
|
+
const plans = await store.getAllPlans();
|
|
1326
|
+
res.json(plans);
|
|
1327
|
+
} catch (error) {
|
|
1328
|
+
if (error instanceof Error) {
|
|
1329
|
+
res.status(500).json({ error: error.message });
|
|
1330
|
+
} else {
|
|
1331
|
+
res.status(500).json({ error: String(error) });
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
});
|
|
1335
|
+
app.get("/api/events", async (_req, res) => {
|
|
1336
|
+
try {
|
|
1337
|
+
const events = await store.getEvents();
|
|
1338
|
+
res.json(events);
|
|
1339
|
+
} catch (error) {
|
|
1340
|
+
if (error instanceof Error) {
|
|
1341
|
+
res.status(500).json({ error: error.message });
|
|
1342
|
+
} else {
|
|
1343
|
+
res.status(500).json({ error: String(error) });
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
});
|
|
1347
|
+
app.get("/api/browse", async (req, res) => {
|
|
1348
|
+
try {
|
|
1349
|
+
const { readdirSync: readdirSync2, statSync: statSync2 } = await import("fs");
|
|
1350
|
+
const { resolve: resolve3, join: join8 } = await import("path");
|
|
1351
|
+
const targetPath = req.query.path ? resolve3(String(req.query.path)) : resolve3(".");
|
|
1352
|
+
const entries = readdirSync2(targetPath).filter(
|
|
1353
|
+
(name) => !name.startsWith(".") && name !== "node_modules" && name !== "dist" && name !== "build"
|
|
1354
|
+
).map((name) => {
|
|
1355
|
+
const full = join8(targetPath, name);
|
|
1356
|
+
try {
|
|
1357
|
+
const stat = statSync2(full);
|
|
1358
|
+
return {
|
|
1359
|
+
name,
|
|
1360
|
+
path: full,
|
|
1361
|
+
isDirectory: stat.isDirectory()
|
|
1362
|
+
};
|
|
1363
|
+
} catch {
|
|
1364
|
+
return null;
|
|
1365
|
+
}
|
|
1366
|
+
}).filter(Boolean);
|
|
1367
|
+
res.json({
|
|
1368
|
+
current: targetPath,
|
|
1369
|
+
parent: resolve3(targetPath, ".."),
|
|
1370
|
+
entries
|
|
1371
|
+
});
|
|
1372
|
+
} catch (error) {
|
|
1373
|
+
if (error instanceof Error) {
|
|
1374
|
+
res.status(500).json({ error: error.message });
|
|
1375
|
+
} else {
|
|
1376
|
+
res.status(500).json({ error: String(error) });
|
|
1377
|
+
}
|
|
1378
|
+
}
|
|
1379
|
+
});
|
|
1380
|
+
app.get("/api/detect", async (req, res) => {
|
|
1381
|
+
try {
|
|
1382
|
+
const { detectTechnologies: detectTechnologies2 } = await import("./TechDetector-YCDEZ6OR.js");
|
|
1383
|
+
const targetPath = req.query.path ? String(req.query.path) : ".";
|
|
1384
|
+
const detected = detectTechnologies2(targetPath);
|
|
1385
|
+
res.json({
|
|
1386
|
+
detected,
|
|
1387
|
+
primary: detected.length > 0 ? detected[0].framework : null
|
|
1388
|
+
});
|
|
1389
|
+
} catch (error) {
|
|
1390
|
+
if (error instanceof Error) {
|
|
1391
|
+
res.status(500).json({ error: error.message });
|
|
1392
|
+
} else {
|
|
1393
|
+
res.status(500).json({ error: String(error) });
|
|
1394
|
+
}
|
|
1395
|
+
}
|
|
1396
|
+
});
|
|
1397
|
+
if (migrationRunner) {
|
|
1398
|
+
app.post("/api/migrations/start", async (req, res) => {
|
|
1399
|
+
try {
|
|
1400
|
+
const { targetPath, from, to } = req.body;
|
|
1401
|
+
if (!targetPath || !from || !to) {
|
|
1402
|
+
res.status(400).json({
|
|
1403
|
+
error: "Missing required fields: targetPath, from, to"
|
|
1404
|
+
});
|
|
1405
|
+
return;
|
|
1406
|
+
}
|
|
1407
|
+
const catalogEntry = findMigrationCatalogEntry(from, to);
|
|
1408
|
+
if (!catalogEntry) {
|
|
1409
|
+
res.status(400).json({
|
|
1410
|
+
error: `Unsupported migration profile: ${from} -> ${to}. Please check the supported catalog.`
|
|
1411
|
+
});
|
|
1412
|
+
return;
|
|
1413
|
+
}
|
|
1414
|
+
const result = await migrationRunner.startMigration({
|
|
1415
|
+
targetPath,
|
|
1416
|
+
from,
|
|
1417
|
+
to
|
|
1418
|
+
});
|
|
1419
|
+
res.json({ ok: true, ...result });
|
|
1420
|
+
} catch (error) {
|
|
1421
|
+
if (error instanceof Error) {
|
|
1422
|
+
res.status(500).json({ error: error.message });
|
|
1423
|
+
} else {
|
|
1424
|
+
res.status(500).json({ error: String(error) });
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1427
|
+
});
|
|
1428
|
+
app.post("/api/migrations/apply", async (req, res) => {
|
|
1429
|
+
try {
|
|
1430
|
+
const { runId, targetPath } = req.body;
|
|
1431
|
+
if (!runId || !targetPath) {
|
|
1432
|
+
res.status(400).json({
|
|
1433
|
+
error: "Missing required fields: runId, targetPath"
|
|
1434
|
+
});
|
|
1435
|
+
return;
|
|
1436
|
+
}
|
|
1437
|
+
const integrator = new MigrationIntegrator(migrationRunner.workspace);
|
|
1438
|
+
const message = await integrator.applyMigration(runId, targetPath);
|
|
1439
|
+
res.json({ ok: true, message });
|
|
1440
|
+
} catch (error) {
|
|
1441
|
+
if (error instanceof Error) {
|
|
1442
|
+
res.status(500).json({ error: error.message });
|
|
1443
|
+
} else {
|
|
1444
|
+
res.status(500).json({ error: String(error) });
|
|
1445
|
+
}
|
|
1446
|
+
}
|
|
1447
|
+
});
|
|
1448
|
+
app.post("/api/migrations/rollback", async (req, res) => {
|
|
1449
|
+
try {
|
|
1450
|
+
const { runId } = req.body;
|
|
1451
|
+
if (!runId) {
|
|
1452
|
+
res.status(400).json({
|
|
1453
|
+
error: "Missing required field: runId"
|
|
1454
|
+
});
|
|
1455
|
+
return;
|
|
1456
|
+
}
|
|
1457
|
+
await migrationRunner.rollbackMigration(runId);
|
|
1458
|
+
res.json({ ok: true, message: `Rolled back run: ${runId}` });
|
|
1459
|
+
} catch (error) {
|
|
1460
|
+
if (error instanceof Error) {
|
|
1461
|
+
res.status(500).json({ error: error.message });
|
|
1462
|
+
} else {
|
|
1463
|
+
res.status(500).json({ error: String(error) });
|
|
1464
|
+
}
|
|
1465
|
+
}
|
|
1466
|
+
});
|
|
1467
|
+
app.post("/api/migrations/reset", async (_req, res) => {
|
|
1468
|
+
try {
|
|
1469
|
+
await store.reset();
|
|
1470
|
+
res.json({ ok: true, message: "Migration state reset successfully" });
|
|
1471
|
+
} catch (error) {
|
|
1472
|
+
if (error instanceof Error) {
|
|
1473
|
+
res.status(500).json({ error: error.message });
|
|
1474
|
+
} else {
|
|
1475
|
+
res.status(500).json({ error: String(error) });
|
|
1476
|
+
}
|
|
1477
|
+
}
|
|
1478
|
+
});
|
|
1479
|
+
}
|
|
1480
|
+
const path6 = await import("path");
|
|
1481
|
+
const fs7 = await import("fs");
|
|
1482
|
+
const publicDir = path6.join(__dirname, "../public");
|
|
1483
|
+
if (fs7.existsSync(publicDir)) {
|
|
1484
|
+
app.use(express.static(publicDir));
|
|
1485
|
+
app.get("*", (_req, res) => {
|
|
1486
|
+
res.sendFile(path6.join(publicDir, "index.html"));
|
|
1487
|
+
});
|
|
1488
|
+
}
|
|
1489
|
+
return app;
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1492
|
+
// ../application/src/agents/MapperAgent.ts
|
|
1493
|
+
import * as fs3 from "fs";
|
|
1494
|
+
import * as path2 from "path";
|
|
1495
|
+
var WhenMigrationStarts = class extends SituationSpecification {
|
|
1496
|
+
isSatisfiedBy({ event }) {
|
|
1497
|
+
return event.type === "migration.started" /* MIGRATION_STARTED */;
|
|
1498
|
+
}
|
|
1499
|
+
};
|
|
1500
|
+
var mapFilesProcessor = {
|
|
1501
|
+
async apply({ event, participant }) {
|
|
1502
|
+
const payload = event.payload;
|
|
1503
|
+
console.log(`[MapperAgent] Migration started for Plan: ${payload.planId}`);
|
|
1504
|
+
const shadowDir = payload.shadowWorkspacePath || "";
|
|
1505
|
+
if (!shadowDir) {
|
|
1506
|
+
console.warn("[MapperAgent] No shadow workspace provided. Skipping.");
|
|
1507
|
+
return;
|
|
1508
|
+
}
|
|
1509
|
+
const filesToMigrate = [];
|
|
1510
|
+
const scanDir = (dir) => {
|
|
1511
|
+
if (!fs3.existsSync(dir)) return;
|
|
1512
|
+
const files = fs3.readdirSync(dir);
|
|
1513
|
+
const ignoredDirs = ["node_modules", "dist", "build", "out", "coverage", ".next"];
|
|
1514
|
+
for (const file of files) {
|
|
1515
|
+
if (ignoredDirs.includes(file) || file.startsWith(".")) continue;
|
|
1516
|
+
const fullPath = path2.join(dir, file);
|
|
1517
|
+
if (fs3.statSync(fullPath).isDirectory()) {
|
|
1518
|
+
scanDir(fullPath);
|
|
1519
|
+
} else if (file.endsWith(".ts") || file.endsWith(".js")) {
|
|
1520
|
+
filesToMigrate.push(fullPath);
|
|
1521
|
+
}
|
|
1522
|
+
}
|
|
1523
|
+
};
|
|
1524
|
+
scanDir(shadowDir);
|
|
1525
|
+
const runtime = resolveRuntime();
|
|
1526
|
+
const repository = runtime.state.repository;
|
|
1527
|
+
for (const discoveredFile of filesToMigrate) {
|
|
1528
|
+
console.log(`[MapperAgent] Discovered file: ${discoveredFile}`);
|
|
1529
|
+
await repository.updateTaskStatus(payload.planId, discoveredFile, "pending");
|
|
1530
|
+
sendEvent(
|
|
1531
|
+
{
|
|
1532
|
+
type: "file.discovered" /* FILE_DISCOVERED */,
|
|
1533
|
+
producerId: participant.getId(),
|
|
1534
|
+
occurredAt: /* @__PURE__ */ new Date(),
|
|
1535
|
+
payload: {
|
|
1536
|
+
planId: payload.planId,
|
|
1537
|
+
filePath: discoveredFile
|
|
1538
|
+
}
|
|
1539
|
+
},
|
|
1540
|
+
participant.getId()
|
|
1541
|
+
);
|
|
1542
|
+
}
|
|
1543
|
+
}
|
|
1544
|
+
};
|
|
1545
|
+
var mapFilesHandler = {
|
|
1546
|
+
specification: new WhenMigrationStarts(),
|
|
1547
|
+
processor: mapFilesProcessor
|
|
1548
|
+
};
|
|
1549
|
+
function createMapperAgent() {
|
|
1550
|
+
return createAgent({
|
|
1551
|
+
name: "Mapper",
|
|
1552
|
+
capabilities: ["code_analysis"],
|
|
1553
|
+
instruction: "You are the Architect Mapper. Your job is to analyze the source code and discover files that need migration.",
|
|
1554
|
+
tools: [],
|
|
1555
|
+
// AST scanning tools will go here later
|
|
1556
|
+
handlers: [mapFilesHandler]
|
|
1557
|
+
});
|
|
1558
|
+
}
|
|
1559
|
+
|
|
1560
|
+
// ../application/src/agents/WorkerAgent.ts
|
|
1561
|
+
import * as fs4 from "fs";
|
|
1562
|
+
|
|
1563
|
+
// ../application/src/utils/FileTreeBuilder.ts
|
|
1564
|
+
import * as path3 from "path";
|
|
1565
|
+
function getCommonBaseDir(filePaths) {
|
|
1566
|
+
if (!filePaths || filePaths.length === 0) return "";
|
|
1567
|
+
if (filePaths.length === 1) return path3.dirname(filePaths[0]);
|
|
1568
|
+
const splitPaths = filePaths.map((p) => path3.normalize(p).split(path3.sep));
|
|
1569
|
+
let commonPath = "";
|
|
1570
|
+
for (let i = 0; i < splitPaths[0].length; i++) {
|
|
1571
|
+
const part = splitPaths[0][i];
|
|
1572
|
+
const isCommon = splitPaths.every((parts) => parts[i] === part);
|
|
1573
|
+
if (isCommon) {
|
|
1574
|
+
commonPath = commonPath === "" ? part : commonPath + path3.sep + part;
|
|
1575
|
+
} else {
|
|
1576
|
+
break;
|
|
1577
|
+
}
|
|
1578
|
+
}
|
|
1579
|
+
return commonPath;
|
|
1580
|
+
}
|
|
1581
|
+
function buildFileTree(filePaths) {
|
|
1582
|
+
if (!filePaths || filePaths.length === 0) return "No files found.";
|
|
1583
|
+
const baseDir = getCommonBaseDir(filePaths);
|
|
1584
|
+
const relativePaths = filePaths.map((p) => path3.relative(baseDir, p)).filter((p) => p.length > 0).sort();
|
|
1585
|
+
if (relativePaths.length === 0) return "No files found.";
|
|
1586
|
+
const tree = {};
|
|
1587
|
+
for (const relPath of relativePaths) {
|
|
1588
|
+
const parts = relPath.split(path3.sep);
|
|
1589
|
+
let current = tree;
|
|
1590
|
+
for (let i = 0; i < parts.length; i++) {
|
|
1591
|
+
const part = parts[i];
|
|
1592
|
+
if (i === parts.length - 1) {
|
|
1593
|
+
current[part] = null;
|
|
1594
|
+
} else {
|
|
1595
|
+
current[part] = current[part] || {};
|
|
1596
|
+
current = current[part];
|
|
1597
|
+
}
|
|
1598
|
+
}
|
|
1599
|
+
}
|
|
1600
|
+
let output = `/
|
|
1601
|
+
`;
|
|
1602
|
+
function renderTree(node, prefix = "") {
|
|
1603
|
+
if (node === null) return;
|
|
1604
|
+
const keys = Object.keys(node);
|
|
1605
|
+
for (let i = 0; i < keys.length; i++) {
|
|
1606
|
+
const key = keys[i];
|
|
1607
|
+
const isLast = i === keys.length - 1;
|
|
1608
|
+
const isFile = node[key] === null;
|
|
1609
|
+
const connector = isLast ? "\u2514\u2500\u2500 " : "\u251C\u2500\u2500 ";
|
|
1610
|
+
output += `${prefix}${connector}${key}
|
|
1611
|
+
`;
|
|
1612
|
+
if (!isFile) {
|
|
1613
|
+
const nextPrefix = prefix + (isLast ? " " : "\u2502 ");
|
|
1614
|
+
renderTree(node[key], nextPrefix);
|
|
1615
|
+
}
|
|
1616
|
+
}
|
|
1617
|
+
}
|
|
1618
|
+
renderTree(tree);
|
|
1619
|
+
return output.trim();
|
|
1620
|
+
}
|
|
1621
|
+
|
|
1622
|
+
// ../application/src/agents/WorkerAgent.ts
|
|
1623
|
+
var retryCountMap = /* @__PURE__ */ new Map();
|
|
1624
|
+
var MAX_RETRIES = 2;
|
|
1625
|
+
var ConcurrencyQueue = class {
|
|
1626
|
+
constructor(concurrencyLimit) {
|
|
1627
|
+
this.concurrencyLimit = concurrencyLimit;
|
|
1628
|
+
}
|
|
1629
|
+
queue = [];
|
|
1630
|
+
activeCount = 0;
|
|
1631
|
+
async enqueue(task) {
|
|
1632
|
+
this.queue.push(task);
|
|
1633
|
+
this.pump();
|
|
1634
|
+
}
|
|
1635
|
+
async pump() {
|
|
1636
|
+
if (this.activeCount >= this.concurrencyLimit || this.queue.length === 0) {
|
|
1637
|
+
return;
|
|
1638
|
+
}
|
|
1639
|
+
const task = this.queue.shift();
|
|
1640
|
+
if (task) {
|
|
1641
|
+
this.activeCount++;
|
|
1642
|
+
try {
|
|
1643
|
+
await task();
|
|
1644
|
+
} catch (error) {
|
|
1645
|
+
console.error("Task error:", error);
|
|
1646
|
+
} finally {
|
|
1647
|
+
this.activeCount--;
|
|
1648
|
+
this.pump();
|
|
1649
|
+
}
|
|
1650
|
+
}
|
|
1651
|
+
}
|
|
1652
|
+
};
|
|
1653
|
+
var workerQueue = new ConcurrencyQueue(2);
|
|
1654
|
+
var WhenFileDiscovered = class extends SituationSpecification {
|
|
1655
|
+
isSatisfiedBy({ event }) {
|
|
1656
|
+
return event.type === "file.discovered" /* FILE_DISCOVERED */;
|
|
1657
|
+
}
|
|
1658
|
+
};
|
|
1659
|
+
var WhenFileRejected = class extends SituationSpecification {
|
|
1660
|
+
isSatisfiedBy({ event }) {
|
|
1661
|
+
return event.type === "file.rejected" /* FILE_REJECTED */;
|
|
1662
|
+
}
|
|
1663
|
+
};
|
|
1664
|
+
var WhenFileFatalMismatch = class extends SituationSpecification {
|
|
1665
|
+
isSatisfiedBy({ event }) {
|
|
1666
|
+
return event.type === "file.fatal_mismatch" /* FILE_FATAL_MISMATCH */;
|
|
1667
|
+
}
|
|
1668
|
+
};
|
|
1669
|
+
var WhenInferenceCompleted = class extends SituationSpecification {
|
|
1670
|
+
isSatisfiedBy({ event, participant }) {
|
|
1671
|
+
return event.type === "model.answer" && event.producerId === participant.getId();
|
|
1672
|
+
}
|
|
1673
|
+
};
|
|
1674
|
+
async function startWorkerLoop(planId, filePath, prompt, participant) {
|
|
1675
|
+
return new Promise(async (originalResolve) => {
|
|
1676
|
+
let isDone = false;
|
|
1677
|
+
const resolve3 = () => {
|
|
1678
|
+
if (!isDone) {
|
|
1679
|
+
isDone = true;
|
|
1680
|
+
originalResolve();
|
|
1681
|
+
}
|
|
1682
|
+
};
|
|
1683
|
+
const runtime = resolveRuntime();
|
|
1684
|
+
const repository = runtime.state.repository;
|
|
1685
|
+
console.log(`[WorkerAgent] Marking file as 'in_progress' in database...`);
|
|
1686
|
+
await repository.updateTaskStatus(planId, filePath, "in_progress");
|
|
1687
|
+
const { createAgent: createMozaikAgent } = await import("./dist-63RNBEIQ.js");
|
|
1688
|
+
const tempAgent = createMozaikAgent({
|
|
1689
|
+
name: `Worker-${Date.now()}-${Math.floor(Math.random() * 1e3)}`,
|
|
1690
|
+
capabilities: ["code_refactoring", "inference"],
|
|
1691
|
+
instruction: "You are the Programmer Worker. Your job is to refactor specific files based on the migration profile. If the target framework requires a different file structure, you are authorized to use your tools to rename, create, or delete files to adhere to architectural rules. When you are done modifying the file(s), simply finish your response.",
|
|
1692
|
+
tools: participant.getTools(),
|
|
1693
|
+
handlers: [
|
|
1694
|
+
{
|
|
1695
|
+
specification: new WhenInferenceCompleted(),
|
|
1696
|
+
processor: {
|
|
1697
|
+
async apply({ event: completionEvent, participant: tempParticipant }) {
|
|
1698
|
+
console.log(`[WorkerAgent:${tempParticipant.getId()}] Inference completed. Emitting FILE_MIGRATED.`);
|
|
1699
|
+
let fileContent = "No content available";
|
|
1700
|
+
try {
|
|
1701
|
+
if (fs4.existsSync(filePath)) {
|
|
1702
|
+
fileContent = fs4.readFileSync(filePath, "utf-8");
|
|
1703
|
+
console.log(`[WorkerAgent:${tempParticipant.getId()}] Read ${fileContent.length} chars from ${filePath}`);
|
|
1704
|
+
} else {
|
|
1705
|
+
console.warn(`[WorkerAgent:${tempParticipant.getId()}] File not found on disk: ${filePath}`);
|
|
1706
|
+
}
|
|
1707
|
+
} catch (e) {
|
|
1708
|
+
console.error(`[WorkerAgent:${tempParticipant.getId()}] Error reading file:`, e);
|
|
1709
|
+
}
|
|
1710
|
+
await repository.updateTaskStatus(planId, filePath, "completed");
|
|
1711
|
+
sendEvent(
|
|
1712
|
+
{
|
|
1713
|
+
type: "file.migrated" /* FILE_MIGRATED */,
|
|
1714
|
+
producerId: tempParticipant.getId(),
|
|
1715
|
+
occurredAt: /* @__PURE__ */ new Date(),
|
|
1716
|
+
payload: {
|
|
1717
|
+
planId,
|
|
1718
|
+
filePath,
|
|
1719
|
+
diff: fileContent
|
|
1720
|
+
}
|
|
1721
|
+
},
|
|
1722
|
+
tempParticipant.getId()
|
|
1723
|
+
);
|
|
1724
|
+
leave(tempParticipant);
|
|
1725
|
+
resolve3();
|
|
1726
|
+
}
|
|
1727
|
+
}
|
|
1728
|
+
}
|
|
1729
|
+
]
|
|
1730
|
+
});
|
|
1731
|
+
join(tempAgent);
|
|
1732
|
+
const modelToUse = process.env.METAMORPH_MODEL || "gpt-5.4";
|
|
1733
|
+
try {
|
|
1734
|
+
console.log(`[WorkerAgent:${tempAgent.getId()}] Starting inference loop for ${filePath}...`);
|
|
1735
|
+
runLoop(tempAgent.getId(), prompt, {
|
|
1736
|
+
model: modelToUse,
|
|
1737
|
+
context: tempAgent.getMemory().getContext(),
|
|
1738
|
+
tools: tempAgent.getTools()
|
|
1739
|
+
});
|
|
1740
|
+
setTimeout(async () => {
|
|
1741
|
+
if (!isDone) {
|
|
1742
|
+
console.error(`[WorkerAgent:${tempAgent.getId()}] Inference timed out.`);
|
|
1743
|
+
await repository.updateTaskStatus(planId, filePath, "failed", "Inference timed out");
|
|
1744
|
+
leave(tempAgent);
|
|
1745
|
+
resolve3();
|
|
1746
|
+
}
|
|
1747
|
+
}, 45e3);
|
|
1748
|
+
} catch (error) {
|
|
1749
|
+
console.error(`[WorkerAgent:${tempAgent.getId()}] Sync error:`, error);
|
|
1750
|
+
if (!isDone) {
|
|
1751
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1752
|
+
await repository.updateTaskStatus(planId, filePath, "failed", errorMessage || "Sync error");
|
|
1753
|
+
leave(tempAgent);
|
|
1754
|
+
resolve3();
|
|
1755
|
+
}
|
|
1756
|
+
}
|
|
1757
|
+
});
|
|
1758
|
+
}
|
|
1759
|
+
var workOnFileProcessor = {
|
|
1760
|
+
async apply({ event, participant }) {
|
|
1761
|
+
const payload = event.payload;
|
|
1762
|
+
console.log(`[WorkerAgent] Received file to migrate: ${payload.filePath}`);
|
|
1763
|
+
const runtime = resolveRuntime();
|
|
1764
|
+
const plan = await runtime.state.repository.getPlan(payload.planId);
|
|
1765
|
+
let prompt = `You need to migrate the file at path: ${payload.filePath}
|
|
1766
|
+
`;
|
|
1767
|
+
if (plan) {
|
|
1768
|
+
prompt += `Migration Profile: Transform from ${plan.profile.source} to ${plan.profile.target}.
|
|
1769
|
+
`;
|
|
1770
|
+
const catalogEntry = findMigrationCatalogEntry(plan.profile.source, plan.profile.target);
|
|
1771
|
+
if (catalogEntry) {
|
|
1772
|
+
prompt += `
|
|
1773
|
+
Strict Architectural Rules for this Migration:
|
|
1774
|
+
`;
|
|
1775
|
+
catalogEntry.architecturalRules.forEach((rule) => prompt += `- ${rule}
|
|
1776
|
+
`);
|
|
1777
|
+
if (catalogEntry.examples && catalogEntry.examples.length > 0) {
|
|
1778
|
+
prompt += `
|
|
1779
|
+
Reference Examples:
|
|
1780
|
+
`;
|
|
1781
|
+
catalogEntry.examples.forEach((ex) => {
|
|
1782
|
+
prompt += `Example (${ex.description}):
|
|
1783
|
+
Before:
|
|
1784
|
+
\`\`\`
|
|
1785
|
+
${ex.before}
|
|
1786
|
+
\`\`\`
|
|
1787
|
+
After:
|
|
1788
|
+
\`\`\`
|
|
1789
|
+
${ex.after}
|
|
1790
|
+
\`\`\`
|
|
1791
|
+
|
|
1792
|
+
`;
|
|
1793
|
+
});
|
|
1794
|
+
}
|
|
1795
|
+
}
|
|
1796
|
+
if (plan.profile.rules) {
|
|
1797
|
+
prompt += `Specific User Rules: ${plan.profile.rules.join(", ")}
|
|
1798
|
+
`;
|
|
1799
|
+
}
|
|
1800
|
+
if (plan.tasks && plan.tasks.length > 0) {
|
|
1801
|
+
const allFiles = plan.tasks.map((t) => t.filePath);
|
|
1802
|
+
const projectTree = buildFileTree(allFiles);
|
|
1803
|
+
prompt += `
|
|
1804
|
+
Global Project Architecture (Files being migrated):
|
|
1805
|
+
`;
|
|
1806
|
+
prompt += `\`\`\`text
|
|
1807
|
+
${projectTree}
|
|
1808
|
+
\`\`\`
|
|
1809
|
+
`;
|
|
1810
|
+
prompt += `Use this context to ensure your changes align with the overall project structure and other files.
|
|
1811
|
+
`;
|
|
1812
|
+
}
|
|
1813
|
+
}
|
|
1814
|
+
prompt += `Please read the file using your tools, rewrite it according to the rules, and write it back. If the rules require breaking this file into multiple files, use your create, rename, or delete file tools accordingly.`;
|
|
1815
|
+
workerQueue.enqueue(async () => {
|
|
1816
|
+
await startWorkerLoop(payload.planId, payload.filePath, prompt, participant);
|
|
1817
|
+
});
|
|
1818
|
+
}
|
|
1819
|
+
};
|
|
1820
|
+
var fixRejectedFileProcessor = {
|
|
1821
|
+
async apply({ event, participant }) {
|
|
1822
|
+
const payload = event.payload;
|
|
1823
|
+
const retryKey = `${payload.planId}:${payload.filePath}`;
|
|
1824
|
+
const currentRetries = retryCountMap.get(retryKey) || 0;
|
|
1825
|
+
if (currentRetries >= MAX_RETRIES) {
|
|
1826
|
+
console.warn(`[WorkerAgent] File ${payload.filePath} exceeded max retries (${MAX_RETRIES}). Marking as failed.`);
|
|
1827
|
+
const runtime2 = resolveRuntime();
|
|
1828
|
+
await runtime2.state.repository.updateTaskStatus(payload.planId, payload.filePath, "failed", `Exceeded max retries (${MAX_RETRIES})`);
|
|
1829
|
+
retryCountMap.delete(retryKey);
|
|
1830
|
+
return;
|
|
1831
|
+
}
|
|
1832
|
+
retryCountMap.set(retryKey, currentRetries + 1);
|
|
1833
|
+
console.log(`[WorkerAgent] File REJECTED (attempt ${currentRetries + 1}/${MAX_RETRIES}), initiating repair loop: ${payload.filePath}`);
|
|
1834
|
+
const runtime = resolveRuntime();
|
|
1835
|
+
const plan = await runtime.state.repository.getPlan(payload.planId);
|
|
1836
|
+
let prompt = `You need to FIX the file at path: ${payload.filePath}
|
|
1837
|
+
`;
|
|
1838
|
+
prompt += `Your previous migration was REJECTED by the Quality Assurance Reviewer.
|
|
1839
|
+
`;
|
|
1840
|
+
prompt += `Feedback/Errors:
|
|
1841
|
+
${payload.errors.join("\n")}
|
|
1842
|
+
|
|
1843
|
+
`;
|
|
1844
|
+
if (plan) {
|
|
1845
|
+
prompt += `Migration Profile: Transform from ${plan.profile.source} to ${plan.profile.target}.
|
|
1846
|
+
`;
|
|
1847
|
+
const catalogEntry = findMigrationCatalogEntry(plan.profile.source, plan.profile.target);
|
|
1848
|
+
if (catalogEntry) {
|
|
1849
|
+
prompt += `
|
|
1850
|
+
Strict Architectural Rules for this Migration:
|
|
1851
|
+
`;
|
|
1852
|
+
catalogEntry.architecturalRules.forEach((rule) => prompt += `- ${rule}
|
|
1853
|
+
`);
|
|
1854
|
+
}
|
|
1855
|
+
if (plan.tasks && plan.tasks.length > 0) {
|
|
1856
|
+
const allFiles = plan.tasks.map((t) => t.filePath);
|
|
1857
|
+
const projectTree = buildFileTree(allFiles);
|
|
1858
|
+
prompt += `
|
|
1859
|
+
Global Project Architecture (Files being migrated):
|
|
1860
|
+
`;
|
|
1861
|
+
prompt += `\`\`\`text
|
|
1862
|
+
${projectTree}
|
|
1863
|
+
\`\`\`
|
|
1864
|
+
`;
|
|
1865
|
+
prompt += `Ensure you respect this overall project structure while fixing the issues.
|
|
1866
|
+
`;
|
|
1867
|
+
}
|
|
1868
|
+
}
|
|
1869
|
+
prompt += `Please read the file, fix the issues mentioned, and write it back. You may restructure the files if the architectural rules demand it.`;
|
|
1870
|
+
workerQueue.enqueue(async () => {
|
|
1871
|
+
await startWorkerLoop(payload.planId, payload.filePath, prompt, participant);
|
|
1872
|
+
});
|
|
1873
|
+
}
|
|
1874
|
+
};
|
|
1875
|
+
var fatalMismatchProcessor = {
|
|
1876
|
+
async apply({ event }) {
|
|
1877
|
+
const payload = event.payload;
|
|
1878
|
+
console.error(`[WorkerAgent] \u{1F6D1} FATAL MISMATCH received for ${payload.filePath}. Reason: ${payload.reason}`);
|
|
1879
|
+
const retryKey = `${payload.planId}:${payload.filePath}`;
|
|
1880
|
+
retryCountMap.delete(retryKey);
|
|
1881
|
+
const runtime = resolveRuntime();
|
|
1882
|
+
await runtime.state.repository.updateTaskStatus(
|
|
1883
|
+
payload.planId,
|
|
1884
|
+
payload.filePath,
|
|
1885
|
+
"failed",
|
|
1886
|
+
`Fatal Architectural Mismatch: ${payload.reason}`
|
|
1887
|
+
);
|
|
1888
|
+
}
|
|
1889
|
+
};
|
|
1890
|
+
function createWorkerAgent(tools) {
|
|
1891
|
+
return createAgent({
|
|
1892
|
+
name: "Worker",
|
|
1893
|
+
capabilities: ["code_refactoring", "inference"],
|
|
1894
|
+
instruction: "You are the Programmer Worker. You migrate code and adapt file structures.",
|
|
1895
|
+
tools,
|
|
1896
|
+
handlers: [
|
|
1897
|
+
{ specification: new WhenFileDiscovered(), processor: workOnFileProcessor },
|
|
1898
|
+
{ specification: new WhenFileRejected(), processor: fixRejectedFileProcessor },
|
|
1899
|
+
{ specification: new WhenFileFatalMismatch(), processor: fatalMismatchProcessor }
|
|
1900
|
+
]
|
|
1901
|
+
});
|
|
1902
|
+
}
|
|
1903
|
+
|
|
1904
|
+
// ../application/src/agents/ReviewerAgent.ts
|
|
1905
|
+
var ConcurrencyQueue2 = class {
|
|
1906
|
+
constructor(concurrencyLimit) {
|
|
1907
|
+
this.concurrencyLimit = concurrencyLimit;
|
|
1908
|
+
}
|
|
1909
|
+
queue = [];
|
|
1910
|
+
activeCount = 0;
|
|
1911
|
+
async enqueue(task) {
|
|
1912
|
+
this.queue.push(task);
|
|
1913
|
+
this.pump();
|
|
1914
|
+
}
|
|
1915
|
+
async pump() {
|
|
1916
|
+
if (this.activeCount >= this.concurrencyLimit || this.queue.length === 0) {
|
|
1917
|
+
return;
|
|
1918
|
+
}
|
|
1919
|
+
const task = this.queue.shift();
|
|
1920
|
+
if (task) {
|
|
1921
|
+
this.activeCount++;
|
|
1922
|
+
try {
|
|
1923
|
+
await task();
|
|
1924
|
+
} catch (error) {
|
|
1925
|
+
console.error("Task error:", error);
|
|
1926
|
+
} finally {
|
|
1927
|
+
this.activeCount--;
|
|
1928
|
+
this.pump();
|
|
1929
|
+
}
|
|
1930
|
+
}
|
|
1931
|
+
}
|
|
1932
|
+
};
|
|
1933
|
+
var reviewerQueue = new ConcurrencyQueue2(2);
|
|
1934
|
+
var WhenFileMigrated = class extends SituationSpecification {
|
|
1935
|
+
isSatisfiedBy({ event }) {
|
|
1936
|
+
return event.type === "file.migrated" /* FILE_MIGRATED */;
|
|
1937
|
+
}
|
|
1938
|
+
};
|
|
1939
|
+
var WhenReviewCompleted = class extends SituationSpecification {
|
|
1940
|
+
isSatisfiedBy({ event, participant }) {
|
|
1941
|
+
console.log(`[DEBUG:WhenReviewCompleted] Event received -> type: "${event.type}", producerId: "${event.producerId}", participantId: "${participant.getId()}", match: ${event.type === "model.answer" && event.producerId === participant.getId()}`);
|
|
1942
|
+
return event.type === "model.answer" && event.producerId === participant.getId();
|
|
1943
|
+
}
|
|
1944
|
+
};
|
|
1945
|
+
var WhenAnythingHappens = class extends SituationSpecification {
|
|
1946
|
+
isSatisfiedBy() {
|
|
1947
|
+
return true;
|
|
1948
|
+
}
|
|
1949
|
+
};
|
|
1950
|
+
var reviewFileProcessor = {
|
|
1951
|
+
async apply({ event, participant }) {
|
|
1952
|
+
const payload = event.payload;
|
|
1953
|
+
console.log(`[ReviewerAgent] Reviewing migrated file: ${payload.filePath}`);
|
|
1954
|
+
reviewerQueue.enqueue(async () => {
|
|
1955
|
+
return new Promise(async (originalResolve) => {
|
|
1956
|
+
let isDone = false;
|
|
1957
|
+
const resolve3 = () => {
|
|
1958
|
+
if (!isDone) {
|
|
1959
|
+
isDone = true;
|
|
1960
|
+
console.log(`[DEBUG:ReviewerAgent] Promise resolved for ${payload.filePath}`);
|
|
1961
|
+
originalResolve();
|
|
1962
|
+
}
|
|
1963
|
+
};
|
|
1964
|
+
const { createAgent: createMozaikAgent } = await import("./dist-63RNBEIQ.js");
|
|
1965
|
+
const { sendEvent: sendEvent3 } = await import("./runtime-WTVS3IND.js");
|
|
1966
|
+
const reviewerId = `Reviewer-${Date.now()}-${Math.floor(Math.random() * 1e3)}`;
|
|
1967
|
+
const tempAgent = createMozaikAgent({
|
|
1968
|
+
name: reviewerId,
|
|
1969
|
+
capabilities: ["code_review", "inference"],
|
|
1970
|
+
instruction: "You are the Quality Assurance Reviewer. You must review the migrated code. Note that you are reviewing a SINGLE FILE within a larger migration. Evaluate if the file violates architectural rules, but DO NOT reject it simply because it does not demonstrate all architectural features (e.g. do not reject a test file or a bootstrap file just because it lacks a Controller). If the worker completely violated the architectural paradigm in a way that cannot be fixed by a simple code edit, use status FATAL_MISMATCH to abort.",
|
|
1971
|
+
tools: [],
|
|
1972
|
+
// No tools to avoid Gemini thought_signature crash
|
|
1973
|
+
handlers: [
|
|
1974
|
+
// DEBUG: Catch-all handler to log every event this agent receives
|
|
1975
|
+
{
|
|
1976
|
+
specification: new WhenAnythingHappens(),
|
|
1977
|
+
processor: {
|
|
1978
|
+
async apply({ event: anyEvent, participant: anyParticipant }) {
|
|
1979
|
+
console.log(`[DEBUG:ReviewerAgent:${reviewerId}:CATCH_ALL] Event -> type: "${anyEvent.type}", producerId: "${anyEvent.producerId}", myId: "${anyParticipant.getId()}", payload keys: ${Object.keys(anyEvent.payload || {}).join(",")}`);
|
|
1980
|
+
}
|
|
1981
|
+
}
|
|
1982
|
+
},
|
|
1983
|
+
{
|
|
1984
|
+
specification: new WhenReviewCompleted(),
|
|
1985
|
+
processor: {
|
|
1986
|
+
async apply({ event: event2, participant: tempParticipant }) {
|
|
1987
|
+
console.log(`[DEBUG:ReviewerAgent:${reviewerId}] model.answer RECEIVED!`);
|
|
1988
|
+
const answerItem = event2.payload.answer;
|
|
1989
|
+
const answerText = answerItem?.content?.text || answerItem?.text || null;
|
|
1990
|
+
console.log(`[DEBUG:ReviewerAgent:${reviewerId}] answerText:`, answerText);
|
|
1991
|
+
let reviewResult = null;
|
|
1992
|
+
if (answerText) {
|
|
1993
|
+
try {
|
|
1994
|
+
reviewResult = JSON.parse(answerText);
|
|
1995
|
+
console.log(`[DEBUG:ReviewerAgent:${reviewerId}] Parsed result:`, JSON.stringify(reviewResult));
|
|
1996
|
+
} catch (e) {
|
|
1997
|
+
console.error(`[ReviewerAgent:${reviewerId}] Failed to parse JSON:`, e);
|
|
1998
|
+
}
|
|
1999
|
+
} else {
|
|
2000
|
+
console.warn(`[DEBUG:ReviewerAgent:${reviewerId}] answerItem has no extractable text.`);
|
|
2001
|
+
}
|
|
2002
|
+
if (reviewResult) {
|
|
2003
|
+
const participantId = tempParticipant.getId();
|
|
2004
|
+
try {
|
|
2005
|
+
if (reviewResult.status === "APPROVED") {
|
|
2006
|
+
console.log(`[ReviewerAgent:${reviewerId}] File ${payload.filePath} APPROVED!`);
|
|
2007
|
+
sendEvent3({
|
|
2008
|
+
type: "file.reviewed" /* FILE_REVIEWED */,
|
|
2009
|
+
producerId: participantId,
|
|
2010
|
+
occurredAt: /* @__PURE__ */ new Date(),
|
|
2011
|
+
payload: { planId: payload.planId, filePath: payload.filePath }
|
|
2012
|
+
}, participantId);
|
|
2013
|
+
} else if (reviewResult.status === "FATAL_MISMATCH") {
|
|
2014
|
+
console.error(`[ReviewerAgent:${reviewerId}] File ${payload.filePath} FATAL_MISMATCH! Aborting repair. Errors:`, reviewResult.errors);
|
|
2015
|
+
sendEvent3({
|
|
2016
|
+
type: "file.fatal_mismatch" /* FILE_FATAL_MISMATCH */,
|
|
2017
|
+
producerId: participantId,
|
|
2018
|
+
occurredAt: /* @__PURE__ */ new Date(),
|
|
2019
|
+
payload: { planId: payload.planId, filePath: payload.filePath, reason: reviewResult.errors?.join(" | ") || "Fundamental architectural mismatch" }
|
|
2020
|
+
}, participantId);
|
|
2021
|
+
} else {
|
|
2022
|
+
console.log(`[ReviewerAgent:${reviewerId}] File ${payload.filePath} REJECTED. Errors:`, reviewResult.errors);
|
|
2023
|
+
sendEvent3({
|
|
2024
|
+
type: "file.rejected" /* FILE_REJECTED */,
|
|
2025
|
+
producerId: participantId,
|
|
2026
|
+
occurredAt: /* @__PURE__ */ new Date(),
|
|
2027
|
+
payload: { planId: payload.planId, filePath: payload.filePath, errors: reviewResult.errors || ["Migration rejected by reviewer"] }
|
|
2028
|
+
}, participantId);
|
|
2029
|
+
}
|
|
2030
|
+
} catch (e) {
|
|
2031
|
+
console.error(`[ReviewerAgent:${reviewerId}] Failed to send review event:`, e);
|
|
2032
|
+
}
|
|
2033
|
+
try {
|
|
2034
|
+
const runtime2 = await resolveRuntime();
|
|
2035
|
+
const plan2 = await runtime2.state.repository.getPlan(payload.planId);
|
|
2036
|
+
if (plan2) {
|
|
2037
|
+
const allTerminal = plan2.tasks.every((t) => t.status === "completed" || t.status === "failed");
|
|
2038
|
+
if (allTerminal) {
|
|
2039
|
+
console.log(`[App] All tasks reached terminal state. Emitting MIGRATION_COMPLETED.`);
|
|
2040
|
+
sendEvent3({
|
|
2041
|
+
type: "migration.completed" /* MIGRATION_COMPLETED */,
|
|
2042
|
+
producerId: participantId,
|
|
2043
|
+
occurredAt: /* @__PURE__ */ new Date(),
|
|
2044
|
+
payload: { planId: payload.planId }
|
|
2045
|
+
}, participantId);
|
|
2046
|
+
}
|
|
2047
|
+
}
|
|
2048
|
+
} catch (e) {
|
|
2049
|
+
console.error(`[ReviewerAgent:${reviewerId}] Failed to check migration completion:`, e);
|
|
2050
|
+
}
|
|
2051
|
+
}
|
|
2052
|
+
leave(tempParticipant);
|
|
2053
|
+
resolve3();
|
|
2054
|
+
}
|
|
2055
|
+
}
|
|
2056
|
+
}
|
|
2057
|
+
]
|
|
2058
|
+
});
|
|
2059
|
+
console.log(`[DEBUG:ReviewerAgent:${reviewerId}] tempAgent created with id: ${tempAgent.getId()}`);
|
|
2060
|
+
join(tempAgent);
|
|
2061
|
+
console.log(`[DEBUG:ReviewerAgent:${reviewerId}] tempAgent joined the runtime`);
|
|
2062
|
+
const runtime = resolveRuntime();
|
|
2063
|
+
if (payload.filePath.endsWith(".ts") || payload.filePath.endsWith(".tsx")) {
|
|
2064
|
+
try {
|
|
2065
|
+
const { Project: Project2 } = await import("./ts-morph-LFNGDRSW.js");
|
|
2066
|
+
const tsProject = new Project2();
|
|
2067
|
+
const sf = tsProject.addSourceFileAtPath(payload.filePath);
|
|
2068
|
+
const diagnostics = sf.getPreEmitDiagnostics();
|
|
2069
|
+
const syntaxErrors = diagnostics.filter((d) => d.getCode() >= 1e3 && d.getCode() < 2e3);
|
|
2070
|
+
if (syntaxErrors.length > 0) {
|
|
2071
|
+
console.error(`[ReviewerAgent:${reviewerId}] Syntax Error in ${payload.filePath}. Rejecting immediately.`);
|
|
2072
|
+
const { sendEvent: sendEvent4 } = await import("./runtime-WTVS3IND.js");
|
|
2073
|
+
sendEvent4({
|
|
2074
|
+
type: "file.rejected" /* FILE_REJECTED */,
|
|
2075
|
+
producerId: tempAgent.getId(),
|
|
2076
|
+
occurredAt: /* @__PURE__ */ new Date(),
|
|
2077
|
+
payload: {
|
|
2078
|
+
planId: payload.planId,
|
|
2079
|
+
filePath: payload.filePath,
|
|
2080
|
+
errors: syntaxErrors.map((d) => `TS${d.getCode()}: ${d.getMessageText()}`)
|
|
2081
|
+
}
|
|
2082
|
+
}, tempAgent.getId());
|
|
2083
|
+
leave(tempAgent);
|
|
2084
|
+
resolve3();
|
|
2085
|
+
return;
|
|
2086
|
+
}
|
|
2087
|
+
} catch (e) {
|
|
2088
|
+
console.warn(`[ReviewerAgent:${reviewerId}] Failed to run syntax check:`, e);
|
|
2089
|
+
}
|
|
2090
|
+
}
|
|
2091
|
+
const plan = await runtime.state.repository.getPlan(payload.planId);
|
|
2092
|
+
let prompt = `Review the following file migration: ${payload.filePath}
|
|
2093
|
+
`;
|
|
2094
|
+
if (plan) {
|
|
2095
|
+
prompt += `Migration Profile: Transform from ${plan.profile.source} to ${plan.profile.target}.
|
|
2096
|
+
`;
|
|
2097
|
+
const catalogEntry = findMigrationCatalogEntry(plan.profile.source, plan.profile.target);
|
|
2098
|
+
if (catalogEntry) {
|
|
2099
|
+
prompt += `
|
|
2100
|
+
Strict Architectural Rules for this Migration (apply only if relevant to this file's purpose):
|
|
2101
|
+
`;
|
|
2102
|
+
catalogEntry.architecturalRules.forEach((rule) => prompt += `- ${rule}
|
|
2103
|
+
`);
|
|
2104
|
+
}
|
|
2105
|
+
}
|
|
2106
|
+
prompt += `
|
|
2107
|
+
Diff or new content:
|
|
2108
|
+
${payload.diff}
|
|
2109
|
+
Does it meet the criteria? You MUST respond in JSON.`;
|
|
2110
|
+
const modelToUse = process.env.METAMORPH_REVIEWER_MODEL || process.env.METAMORPH_MODEL || "gpt-5.4-mini";
|
|
2111
|
+
console.log(`[ReviewerAgent:${reviewerId}] Starting inference loop for ${payload.filePath} with model ${modelToUse}...`);
|
|
2112
|
+
try {
|
|
2113
|
+
console.log(`[DEBUG:ReviewerAgent:${reviewerId}] Calling runLoop with agentId: ${tempAgent.getId()}`);
|
|
2114
|
+
runLoop(tempAgent.getId(), prompt, {
|
|
2115
|
+
model: modelToUse,
|
|
2116
|
+
context: tempAgent.getMemory().getContext(),
|
|
2117
|
+
structuredOutput: {
|
|
2118
|
+
name: "review_result",
|
|
2119
|
+
schema: {
|
|
2120
|
+
type: "object",
|
|
2121
|
+
properties: {
|
|
2122
|
+
status: { type: "string", enum: ["APPROVED", "REJECTED", "FATAL_MISMATCH"] },
|
|
2123
|
+
errors: { type: "array", items: { type: "string" } }
|
|
2124
|
+
},
|
|
2125
|
+
required: ["status", "errors"],
|
|
2126
|
+
additionalProperties: false
|
|
2127
|
+
},
|
|
2128
|
+
strict: true
|
|
2129
|
+
}
|
|
2130
|
+
});
|
|
2131
|
+
console.log(`[DEBUG:ReviewerAgent:${reviewerId}] runLoop called successfully (fire-and-forget)`);
|
|
2132
|
+
setTimeout(async () => {
|
|
2133
|
+
if (!isDone) {
|
|
2134
|
+
console.error(`[ReviewerAgent:${reviewerId}] \u23F0 TIMEOUT after 45s for ${payload.filePath}. Model never responded.`);
|
|
2135
|
+
const { resolveRuntime: resolveRuntime2 } = await import("./runtime-WTVS3IND.js");
|
|
2136
|
+
const runtime2 = resolveRuntime2();
|
|
2137
|
+
await runtime2.state.repository.updateTaskStatus(payload.planId, payload.filePath, "failed", "Inference timed out after 45s");
|
|
2138
|
+
leave(tempAgent);
|
|
2139
|
+
resolve3();
|
|
2140
|
+
}
|
|
2141
|
+
}, 45e3);
|
|
2142
|
+
} catch (error) {
|
|
2143
|
+
console.error(`[ReviewerAgent:${reviewerId}] Sync error:`, error);
|
|
2144
|
+
if (!isDone) {
|
|
2145
|
+
const { resolveRuntime: resolveRuntime2 } = await import("./runtime-WTVS3IND.js");
|
|
2146
|
+
const runtime2 = resolveRuntime2();
|
|
2147
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
2148
|
+
await runtime2.state.repository.updateTaskStatus(payload.planId, payload.filePath, "failed", errorMessage || "Sync error");
|
|
2149
|
+
leave(tempAgent);
|
|
2150
|
+
resolve3();
|
|
2151
|
+
}
|
|
2152
|
+
}
|
|
2153
|
+
});
|
|
2154
|
+
});
|
|
2155
|
+
}
|
|
2156
|
+
};
|
|
2157
|
+
var reviewFileHandler = {
|
|
2158
|
+
specification: new WhenFileMigrated(),
|
|
2159
|
+
processor: reviewFileProcessor
|
|
2160
|
+
};
|
|
2161
|
+
function createReviewerAgent(tools) {
|
|
2162
|
+
return createAgent({
|
|
2163
|
+
name: "Reviewer",
|
|
2164
|
+
capabilities: ["code_review", "inference"],
|
|
2165
|
+
instruction: "You are the Reviewer Worker.",
|
|
2166
|
+
tools: [],
|
|
2167
|
+
// No tools
|
|
2168
|
+
handlers: [reviewFileHandler]
|
|
2169
|
+
});
|
|
2170
|
+
}
|
|
2171
|
+
|
|
2172
|
+
// ../application/src/agents/PackageManagerAgent.ts
|
|
2173
|
+
import * as fs5 from "fs";
|
|
2174
|
+
import * as path4 from "path";
|
|
2175
|
+
var WhenMigrationStarts2 = class extends SituationSpecification {
|
|
2176
|
+
isSatisfiedBy({ event }) {
|
|
2177
|
+
return event.type === "migration.started" /* MIGRATION_STARTED */;
|
|
2178
|
+
}
|
|
2179
|
+
};
|
|
2180
|
+
var managePackagesProcessor = {
|
|
2181
|
+
async apply({ event, participant }) {
|
|
2182
|
+
const payload = event.payload;
|
|
2183
|
+
console.log(`[PackageManagerAgent] Managing dependencies and cleanup for Plan: ${payload.planId}`);
|
|
2184
|
+
const shadowDir = payload.shadowWorkspacePath || "";
|
|
2185
|
+
if (!shadowDir || !fs5.existsSync(shadowDir)) {
|
|
2186
|
+
console.warn("[PackageManagerAgent] No valid shadow workspace provided. Skipping.");
|
|
2187
|
+
return;
|
|
2188
|
+
}
|
|
2189
|
+
const runtime = resolveRuntime();
|
|
2190
|
+
const plan = await runtime.state.repository.getPlan(payload.planId);
|
|
2191
|
+
if (!plan) {
|
|
2192
|
+
console.warn("[PackageManagerAgent] Migration plan not found.");
|
|
2193
|
+
return;
|
|
2194
|
+
}
|
|
2195
|
+
const catalogEntry = findMigrationCatalogEntry(plan.profile.source, plan.profile.target);
|
|
2196
|
+
if (!catalogEntry) {
|
|
2197
|
+
console.log(`[PackageManagerAgent] No catalog entry found for ${plan.profile.source} -> ${plan.profile.target}`);
|
|
2198
|
+
return;
|
|
2199
|
+
}
|
|
2200
|
+
if (catalogEntry.filesToDelete && catalogEntry.filesToDelete.length > 0) {
|
|
2201
|
+
for (const fileToDelete of catalogEntry.filesToDelete) {
|
|
2202
|
+
const fullPath = path4.join(shadowDir, fileToDelete);
|
|
2203
|
+
if (fs5.existsSync(fullPath)) {
|
|
2204
|
+
fs5.unlinkSync(fullPath);
|
|
2205
|
+
console.log(`[PackageManagerAgent] Deleted deprecated file: ${fileToDelete}`);
|
|
2206
|
+
}
|
|
2207
|
+
}
|
|
2208
|
+
}
|
|
2209
|
+
const packageJsonPath = path4.join(shadowDir, "package.json");
|
|
2210
|
+
if (fs5.existsSync(packageJsonPath)) {
|
|
2211
|
+
const { execSync: execSync2 } = __require("child_process");
|
|
2212
|
+
const execOpts = { cwd: shadowDir, stdio: "inherit" };
|
|
2213
|
+
try {
|
|
2214
|
+
let toRemove = [];
|
|
2215
|
+
if (catalogEntry.dependenciesToRemove) toRemove.push(...catalogEntry.dependenciesToRemove);
|
|
2216
|
+
if (catalogEntry.devDependenciesToRemove) toRemove.push(...catalogEntry.devDependenciesToRemove);
|
|
2217
|
+
if (toRemove.length > 0) {
|
|
2218
|
+
console.log(`[PackageManagerAgent] Uninstalling: ${toRemove.join(" ")}`);
|
|
2219
|
+
execSync2(`npm uninstall ${toRemove.join(" ")}`, execOpts);
|
|
2220
|
+
}
|
|
2221
|
+
if (catalogEntry.dependenciesToAdd && Object.keys(catalogEntry.dependenciesToAdd).length > 0) {
|
|
2222
|
+
const deps = Object.entries(catalogEntry.dependenciesToAdd).map(([pkg, ver]) => `${pkg}@${ver}`);
|
|
2223
|
+
console.log(`[PackageManagerAgent] Installing dependencies: ${deps.join(" ")}`);
|
|
2224
|
+
execSync2(`npm install ${deps.join(" ")}`, execOpts);
|
|
2225
|
+
}
|
|
2226
|
+
if (catalogEntry.devDependenciesToAdd && Object.keys(catalogEntry.devDependenciesToAdd).length > 0) {
|
|
2227
|
+
const devDeps = Object.entries(catalogEntry.devDependenciesToAdd).map(([pkg, ver]) => `${pkg}@${ver}`);
|
|
2228
|
+
console.log(`[PackageManagerAgent] Installing devDependencies: ${devDeps.join(" ")}`);
|
|
2229
|
+
execSync2(`npm install -D ${devDeps.join(" ")}`, execOpts);
|
|
2230
|
+
}
|
|
2231
|
+
console.log(`[PackageManagerAgent] Successfully updated dependencies via npm.`);
|
|
2232
|
+
} catch (err) {
|
|
2233
|
+
console.error(`[PackageManagerAgent] Error running npm commands:`, err);
|
|
2234
|
+
}
|
|
2235
|
+
} else {
|
|
2236
|
+
console.log(`[PackageManagerAgent] No package.json found at ${packageJsonPath}`);
|
|
2237
|
+
}
|
|
2238
|
+
}
|
|
2239
|
+
};
|
|
2240
|
+
var managePackagesHandler = {
|
|
2241
|
+
specification: new WhenMigrationStarts2(),
|
|
2242
|
+
processor: managePackagesProcessor
|
|
2243
|
+
};
|
|
2244
|
+
function createPackageManagerAgent() {
|
|
2245
|
+
return createAgent({
|
|
2246
|
+
name: "PackageManager",
|
|
2247
|
+
capabilities: ["package_management"],
|
|
2248
|
+
instruction: "You are the Package Manager Agent. Your job is to resolve dependencies and clean up unused framework files.",
|
|
2249
|
+
tools: [],
|
|
2250
|
+
handlers: [managePackagesHandler]
|
|
2251
|
+
});
|
|
2252
|
+
}
|
|
2253
|
+
|
|
2254
|
+
// ../application/src/agents/ReporterAgent.ts
|
|
2255
|
+
import * as fs6 from "fs";
|
|
2256
|
+
import * as path5 from "path";
|
|
2257
|
+
var WhenMigrationStarted = class extends SituationSpecification {
|
|
2258
|
+
isSatisfiedBy({ event }) {
|
|
2259
|
+
return event.type === "migration.started" /* MIGRATION_STARTED */;
|
|
2260
|
+
}
|
|
2261
|
+
};
|
|
2262
|
+
var WhenFileMigrated2 = class extends SituationSpecification {
|
|
2263
|
+
isSatisfiedBy({ event }) {
|
|
2264
|
+
return event.type === "file.migrated" /* FILE_MIGRATED */;
|
|
2265
|
+
}
|
|
2266
|
+
};
|
|
2267
|
+
var WhenFileReviewed = class extends SituationSpecification {
|
|
2268
|
+
isSatisfiedBy({ event }) {
|
|
2269
|
+
return event.type === "file.reviewed" /* FILE_REVIEWED */;
|
|
2270
|
+
}
|
|
2271
|
+
};
|
|
2272
|
+
var WhenFileRejected2 = class extends SituationSpecification {
|
|
2273
|
+
isSatisfiedBy({ event }) {
|
|
2274
|
+
return event.type === "file.rejected" /* FILE_REJECTED */;
|
|
2275
|
+
}
|
|
2276
|
+
};
|
|
2277
|
+
var WhenFileFatalMismatch2 = class extends SituationSpecification {
|
|
2278
|
+
isSatisfiedBy({ event }) {
|
|
2279
|
+
return event.type === "file.fatal_mismatch" /* FILE_FATAL_MISMATCH */;
|
|
2280
|
+
}
|
|
2281
|
+
};
|
|
2282
|
+
var WhenMigrationCompleted = class extends SituationSpecification {
|
|
2283
|
+
isSatisfiedBy({ event }) {
|
|
2284
|
+
return event.type === "migration.completed" /* MIGRATION_COMPLETED */;
|
|
2285
|
+
}
|
|
2286
|
+
};
|
|
2287
|
+
function appendToReport(shadowDir, content) {
|
|
2288
|
+
if (!shadowDir) return;
|
|
2289
|
+
const reportPath = path5.join(shadowDir, "migration-report.md");
|
|
2290
|
+
try {
|
|
2291
|
+
fs6.appendFileSync(reportPath, content + "\n", "utf-8");
|
|
2292
|
+
} catch (e) {
|
|
2293
|
+
console.error("[ReporterAgent] Error writing to report:", e);
|
|
2294
|
+
}
|
|
2295
|
+
}
|
|
2296
|
+
var startedProcessor = {
|
|
2297
|
+
async apply({ event }) {
|
|
2298
|
+
const p = event.payload;
|
|
2299
|
+
if (!("shadowWorkspacePath" in p)) return;
|
|
2300
|
+
const shadowPath = p.shadowWorkspacePath;
|
|
2301
|
+
if (!shadowPath) return;
|
|
2302
|
+
const reportPath = path5.join(shadowPath, "migration-report.md");
|
|
2303
|
+
const header = `# Metamorph Migration Report
|
|
2304
|
+
**Plan ID**: ${p.planId}
|
|
2305
|
+
**Date Started**: ${(/* @__PURE__ */ new Date()).toLocaleString()}
|
|
2306
|
+
---
|
|
2307
|
+
|
|
2308
|
+
## Migration Log
|
|
2309
|
+
|
|
2310
|
+
`;
|
|
2311
|
+
fs6.writeFileSync(reportPath, header, "utf-8");
|
|
2312
|
+
console.log(`[ReporterAgent] Initialized report at ${reportPath}`);
|
|
2313
|
+
}
|
|
2314
|
+
};
|
|
2315
|
+
var migratedProcessor = {
|
|
2316
|
+
async apply({ event }) {
|
|
2317
|
+
const p = event.payload;
|
|
2318
|
+
const { resolveRuntime: resolveRuntime2 } = await import("./runtime-WTVS3IND.js");
|
|
2319
|
+
const runtime = resolveRuntime2();
|
|
2320
|
+
const plan = await runtime.state.repository.getPlan(p.planId);
|
|
2321
|
+
if (!plan) return;
|
|
2322
|
+
const shadowDir = plan.targetPath;
|
|
2323
|
+
appendToReport(shadowDir, `- \u23F3 **Refactored**: \`${p.filePath}\` (Pending Review)`);
|
|
2324
|
+
}
|
|
2325
|
+
};
|
|
2326
|
+
var reviewedProcessor = {
|
|
2327
|
+
async apply({ event }) {
|
|
2328
|
+
const p = event.payload;
|
|
2329
|
+
const { resolveRuntime: resolveRuntime2 } = await import("./runtime-WTVS3IND.js");
|
|
2330
|
+
const runtime = resolveRuntime2();
|
|
2331
|
+
const plan = await runtime.state.repository.getPlan(p.planId);
|
|
2332
|
+
if (!plan) return;
|
|
2333
|
+
appendToReport(plan.targetPath, `- \u2705 **Approved**: \`${p.filePath}\``);
|
|
2334
|
+
}
|
|
2335
|
+
};
|
|
2336
|
+
var rejectedProcessor = {
|
|
2337
|
+
async apply({ event }) {
|
|
2338
|
+
const p = event.payload;
|
|
2339
|
+
const { resolveRuntime: resolveRuntime2 } = await import("./runtime-WTVS3IND.js");
|
|
2340
|
+
const runtime = resolveRuntime2();
|
|
2341
|
+
const plan = await runtime.state.repository.getPlan(p.planId);
|
|
2342
|
+
if (!plan) return;
|
|
2343
|
+
appendToReport(plan.targetPath, `- \u26A0\uFE0F **Rejected**: \`${p.filePath}\`
|
|
2344
|
+
> Errors: ${p.errors?.join(", ")}`);
|
|
2345
|
+
}
|
|
2346
|
+
};
|
|
2347
|
+
var fatalProcessor = {
|
|
2348
|
+
async apply({ event }) {
|
|
2349
|
+
const p = event.payload;
|
|
2350
|
+
const { resolveRuntime: resolveRuntime2 } = await import("./runtime-WTVS3IND.js");
|
|
2351
|
+
const runtime = resolveRuntime2();
|
|
2352
|
+
const plan = await runtime.state.repository.getPlan(p.planId);
|
|
2353
|
+
if (!plan) return;
|
|
2354
|
+
appendToReport(plan.targetPath, `- \u274C **FATAL MISMATCH**: \`${p.filePath}\`
|
|
2355
|
+
> Reason: ${p.reason}`);
|
|
2356
|
+
}
|
|
2357
|
+
};
|
|
2358
|
+
var completedProcessor = {
|
|
2359
|
+
async apply({ event }) {
|
|
2360
|
+
const p = event.payload;
|
|
2361
|
+
const { resolveRuntime: resolveRuntime2 } = await import("./runtime-WTVS3IND.js");
|
|
2362
|
+
const runtime = resolveRuntime2();
|
|
2363
|
+
const plan = await runtime.state.repository.getPlan(p.planId);
|
|
2364
|
+
if (!plan) return;
|
|
2365
|
+
const totalFiles = plan.tasks ? plan.tasks.length : 0;
|
|
2366
|
+
const migratedFiles = plan.tasks ? plan.tasks.filter((t) => t.status === "completed").length : 0;
|
|
2367
|
+
const footer = `
|
|
2368
|
+
---
|
|
2369
|
+
## Migration Completed
|
|
2370
|
+
**Date Finished**: ${(/* @__PURE__ */ new Date()).toLocaleString()}
|
|
2371
|
+
**Total Files**: ${totalFiles}
|
|
2372
|
+
**Successfully Migrated**: ${migratedFiles}
|
|
2373
|
+
`;
|
|
2374
|
+
appendToReport(plan.targetPath, footer);
|
|
2375
|
+
}
|
|
2376
|
+
};
|
|
2377
|
+
function createReporterAgent() {
|
|
2378
|
+
return createAgent({
|
|
2379
|
+
name: "Reporter",
|
|
2380
|
+
capabilities: ["reporting"],
|
|
2381
|
+
instruction: "You are the Reporter Agent. Your job is to compile a human-readable markdown report of the migration process.",
|
|
2382
|
+
tools: [],
|
|
2383
|
+
handlers: [
|
|
2384
|
+
{ specification: new WhenMigrationStarted(), processor: startedProcessor },
|
|
2385
|
+
{ specification: new WhenFileMigrated2(), processor: migratedProcessor },
|
|
2386
|
+
{ specification: new WhenFileReviewed(), processor: reviewedProcessor },
|
|
2387
|
+
{ specification: new WhenFileRejected2(), processor: rejectedProcessor },
|
|
2388
|
+
{ specification: new WhenFileFatalMismatch2(), processor: fatalProcessor },
|
|
2389
|
+
{ specification: new WhenMigrationCompleted(), processor: completedProcessor }
|
|
2390
|
+
]
|
|
2391
|
+
});
|
|
2392
|
+
}
|
|
2393
|
+
|
|
2394
|
+
// ../application/src/MigrationRunner.ts
|
|
2395
|
+
import { randomUUID } from "crypto";
|
|
2396
|
+
var MigrationRunner = class {
|
|
2397
|
+
store;
|
|
2398
|
+
tools;
|
|
2399
|
+
workspace;
|
|
2400
|
+
initialized = false;
|
|
2401
|
+
constructor(store, tools) {
|
|
2402
|
+
this.store = store;
|
|
2403
|
+
this.tools = tools;
|
|
2404
|
+
this.workspace = new ShadowWorkspace();
|
|
2405
|
+
}
|
|
2406
|
+
/**
|
|
2407
|
+
* Bootstraps the Mozaik swarm if not already initialized.
|
|
2408
|
+
* Idempotent: safe to call multiple times.
|
|
2409
|
+
*/
|
|
2410
|
+
ensureInitialized() {
|
|
2411
|
+
if (!this.initialized) {
|
|
2412
|
+
bootstrapMetamorph(this.store, this.tools);
|
|
2413
|
+
this.initialized = true;
|
|
2414
|
+
}
|
|
2415
|
+
}
|
|
2416
|
+
/**
|
|
2417
|
+
* Starts a new migration run.
|
|
2418
|
+
* 1. Creates a Shadow Workspace copy of the target directory.
|
|
2419
|
+
* 2. Persists the MigrationPlan in the database.
|
|
2420
|
+
* 3. Emits MIGRATION_STARTED to wake the swarm.
|
|
2421
|
+
*/
|
|
2422
|
+
async startMigration(request) {
|
|
2423
|
+
this.ensureInitialized();
|
|
2424
|
+
await this.store.reset();
|
|
2425
|
+
this.workspace.cleanupOldRuns(3);
|
|
2426
|
+
const runId = `run_${Date.now()}`;
|
|
2427
|
+
const shadowPath = this.workspace.cloneDirectory(request.targetPath, runId);
|
|
2428
|
+
const planId = `plan_${randomUUID()}`;
|
|
2429
|
+
const plan = {
|
|
2430
|
+
id: planId,
|
|
2431
|
+
runId,
|
|
2432
|
+
profile: { source: request.from, target: request.to },
|
|
2433
|
+
targetPath: request.targetPath,
|
|
2434
|
+
tasks: [],
|
|
2435
|
+
createdAt: /* @__PURE__ */ new Date()
|
|
2436
|
+
};
|
|
2437
|
+
await this.store.savePlan(plan);
|
|
2438
|
+
const { createHuman } = await import("./dist-63RNBEIQ.js");
|
|
2439
|
+
const human = createHuman({ name: "System", capabilities: [], handlers: [] });
|
|
2440
|
+
join(human);
|
|
2441
|
+
sendEvent(
|
|
2442
|
+
{
|
|
2443
|
+
type: "migration.started" /* MIGRATION_STARTED */,
|
|
2444
|
+
producerId: human.getId(),
|
|
2445
|
+
occurredAt: /* @__PURE__ */ new Date(),
|
|
2446
|
+
payload: {
|
|
2447
|
+
planId,
|
|
2448
|
+
profile: plan.profile,
|
|
2449
|
+
shadowWorkspacePath: shadowPath
|
|
2450
|
+
}
|
|
2451
|
+
},
|
|
2452
|
+
human.getId()
|
|
2453
|
+
);
|
|
2454
|
+
return { planId, shadowPath, runId };
|
|
2455
|
+
}
|
|
2456
|
+
/**
|
|
2457
|
+
* Rolls back a migration by deleting its Shadow Workspace.
|
|
2458
|
+
* The original source code is never touched, so rollback
|
|
2459
|
+
* simply means discarding the shadow copy.
|
|
2460
|
+
*/
|
|
2461
|
+
async rollbackMigration(runId) {
|
|
2462
|
+
this.workspace.rollback(runId);
|
|
2463
|
+
}
|
|
2464
|
+
};
|
|
2465
|
+
|
|
2466
|
+
// ../application/src/index.ts
|
|
2467
|
+
function bootstrapMetamorph(repository, tools = []) {
|
|
2468
|
+
initializeRuntime({
|
|
2469
|
+
state: new MetamorphState(repository)
|
|
2470
|
+
});
|
|
2471
|
+
class AllEventsSpecification extends SituationSpecification {
|
|
2472
|
+
isSatisfiedBy() {
|
|
2473
|
+
return true;
|
|
2474
|
+
}
|
|
2475
|
+
}
|
|
2476
|
+
const loggerAgent = createAgent({
|
|
2477
|
+
name: "TelemetryLogger",
|
|
2478
|
+
capabilities: [],
|
|
2479
|
+
instruction: "You silently log everything.",
|
|
2480
|
+
tools: [],
|
|
2481
|
+
handlers: [
|
|
2482
|
+
{
|
|
2483
|
+
specification: new AllEventsSpecification(),
|
|
2484
|
+
processor: {
|
|
2485
|
+
async apply({ event }) {
|
|
2486
|
+
const { resolveRuntime: resolveRuntime2 } = await import("./runtime-WTVS3IND.js");
|
|
2487
|
+
const runtime = resolveRuntime2();
|
|
2488
|
+
const repository2 = runtime.state.repository;
|
|
2489
|
+
if (event.type.includes("migration") || event.type.includes("file")) {
|
|
2490
|
+
await repository2.logEvent(event.type, {
|
|
2491
|
+
...event.payload,
|
|
2492
|
+
producerId: event.producerId
|
|
2493
|
+
});
|
|
2494
|
+
}
|
|
2495
|
+
}
|
|
2496
|
+
}
|
|
2497
|
+
}
|
|
2498
|
+
]
|
|
2499
|
+
});
|
|
2500
|
+
join(loggerAgent);
|
|
2501
|
+
const mapper = createMapperAgent();
|
|
2502
|
+
const worker = createWorkerAgent(tools);
|
|
2503
|
+
const reviewer = createReviewerAgent(tools);
|
|
2504
|
+
const packageManager = createPackageManagerAgent();
|
|
2505
|
+
const reporter = createReporterAgent();
|
|
2506
|
+
join(mapper);
|
|
2507
|
+
join(worker);
|
|
2508
|
+
join(reviewer);
|
|
2509
|
+
join(packageManager);
|
|
2510
|
+
join(reporter);
|
|
2511
|
+
console.log(`[App] Mozaik initialized. Agents joined: Mapper, Worker, Reviewer, PackageManager, Reporter`);
|
|
2512
|
+
return {
|
|
2513
|
+
mapperId: mapper.getId(),
|
|
2514
|
+
workerId: worker.getId(),
|
|
2515
|
+
reviewerId: reviewer.getId()
|
|
2516
|
+
};
|
|
2517
|
+
}
|
|
2518
|
+
|
|
2519
|
+
// src/index.ts
|
|
2520
|
+
var program = new Command();
|
|
2521
|
+
program.name("metamorph").description("AI-powered technology migration tool using Mozaik Agents").version("1.0.0");
|
|
2522
|
+
program.command("run [path]").description("Run a migration on the specified directory").option("--from <source>", "Source framework (e.g. express)").option("--to <target>", "Target framework (e.g. fastify)").action(async (targetPathArg, options) => {
|
|
2523
|
+
let targetPath = targetPathArg || ".";
|
|
2524
|
+
const { select, input } = await import("@inquirer/prompts");
|
|
2525
|
+
let from = options.from;
|
|
2526
|
+
let to = options.to;
|
|
2527
|
+
const SUPPORTED_MIGRATIONS = {
|
|
2528
|
+
express: ["fastify", "nestjs"],
|
|
2529
|
+
fastify: ["nestjs", "express"],
|
|
2530
|
+
react: ["next"],
|
|
2531
|
+
vue: ["react", "next"],
|
|
2532
|
+
angular: ["react", "next"],
|
|
2533
|
+
svelte: ["react", "next"]
|
|
2534
|
+
};
|
|
2535
|
+
if (!from) {
|
|
2536
|
+
console.log(chalk.gray(`
|
|
2537
|
+
\u{1F50D} Scanning directory: ${targetPath}`));
|
|
2538
|
+
const detected = detectTechnologies(targetPath);
|
|
2539
|
+
const supportedDetected = detected.filter((d) => Object.keys(SUPPORTED_MIGRATIONS).includes(d.framework));
|
|
2540
|
+
if (supportedDetected.length > 0) {
|
|
2541
|
+
from = await select({
|
|
2542
|
+
message: "What technology do you want to migrate FROM?",
|
|
2543
|
+
choices: supportedDetected.map((d) => ({ name: `${d.framework} (Detected ${d.confidence}%)`, value: d.framework }))
|
|
2544
|
+
});
|
|
2545
|
+
} else {
|
|
2546
|
+
console.log(chalk.yellow(`No supported technologies detected in the current directory.`));
|
|
2547
|
+
process.exit(0);
|
|
2548
|
+
}
|
|
2549
|
+
}
|
|
2550
|
+
if (!to) {
|
|
2551
|
+
const validTargets = SUPPORTED_MIGRATIONS[from] || [];
|
|
2552
|
+
if (validTargets.length > 0) {
|
|
2553
|
+
to = await select({
|
|
2554
|
+
message: `What framework do you want to migrate TO from ${from}?`,
|
|
2555
|
+
choices: validTargets.map((t) => ({ name: t, value: t }))
|
|
2556
|
+
});
|
|
2557
|
+
} else {
|
|
2558
|
+
console.log(chalk.red(`No supported target frameworks for ${from}.`));
|
|
2559
|
+
process.exit(1);
|
|
2560
|
+
}
|
|
2561
|
+
}
|
|
2562
|
+
console.log(chalk.blue(`
|
|
2563
|
+
\u{1F680} Starting Metamorph Migration`));
|
|
2564
|
+
console.log(chalk.gray(`Target: ${targetPath} | ${from} -> ${to}
|
|
2565
|
+
`));
|
|
2566
|
+
if (!process.env.OPENAI_API_KEY && !process.env.ANTHROPIC_API_KEY) {
|
|
2567
|
+
console.log(chalk.yellow(`\u26A0\uFE0F WARNING: No LLM API key detected.
|
|
2568
|
+
`));
|
|
2569
|
+
}
|
|
2570
|
+
const spinner = ora("Initializing...").start();
|
|
2571
|
+
try {
|
|
2572
|
+
const store = new SQLiteStateStore(".metamorph");
|
|
2573
|
+
const tools = [
|
|
2574
|
+
createReadFileTool(),
|
|
2575
|
+
createWriteFileTool(),
|
|
2576
|
+
createRenameFileTool(),
|
|
2577
|
+
createCreateFileTool(),
|
|
2578
|
+
createDeleteFileTool()
|
|
2579
|
+
];
|
|
2580
|
+
const runner = new MigrationRunner(store, tools);
|
|
2581
|
+
const result = await runner.startMigration({
|
|
2582
|
+
targetPath,
|
|
2583
|
+
from,
|
|
2584
|
+
to
|
|
2585
|
+
});
|
|
2586
|
+
spinner.succeed(`Migration dispatched! Plan: ${result.planId}`);
|
|
2587
|
+
console.log(chalk.gray(`Shadow: ${result.shadowPath}`));
|
|
2588
|
+
console.log(chalk.blue(`
|
|
2589
|
+
\u23F3 Agents are now working...`));
|
|
2590
|
+
await new Promise((resolve3) => setTimeout(resolve3, 2e4));
|
|
2591
|
+
console.log(chalk.green(`
|
|
2592
|
+
\u2705 Migration simulation finished.`));
|
|
2593
|
+
process.exit(0);
|
|
2594
|
+
} catch (error) {
|
|
2595
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
2596
|
+
spinner.fail(`Migration failed: ${errorMessage}`);
|
|
2597
|
+
process.exit(1);
|
|
2598
|
+
}
|
|
2599
|
+
});
|
|
2600
|
+
program.command("apply <runId> <targetPath>").description("Apply a completed migration to the target project").action(async (runId, targetPath) => {
|
|
2601
|
+
try {
|
|
2602
|
+
const integrator = new MigrationIntegrator(new ShadowWorkspace());
|
|
2603
|
+
const message = await integrator.applyMigration(runId, targetPath);
|
|
2604
|
+
console.log(chalk.green(`\u2705 ${message}`));
|
|
2605
|
+
} catch (error) {
|
|
2606
|
+
console.error(chalk.red(`\u274C Failed to apply migration: ${error instanceof Error ? error.message : String(error)}`));
|
|
2607
|
+
process.exit(1);
|
|
2608
|
+
}
|
|
2609
|
+
});
|
|
2610
|
+
program.command("rollback <runId>").description("Rollback and discard an unapplied migration shadow workspace").action(async (runId) => {
|
|
2611
|
+
try {
|
|
2612
|
+
const store = new SQLiteStateStore(".metamorph");
|
|
2613
|
+
const runner = new MigrationRunner(store, []);
|
|
2614
|
+
await runner.rollbackMigration(runId);
|
|
2615
|
+
console.log(chalk.green(`\u2705 Rolled back run: ${runId}`));
|
|
2616
|
+
} catch (error) {
|
|
2617
|
+
console.error(chalk.red(`\u274C Failed to rollback: ${error instanceof Error ? error.message : String(error)}`));
|
|
2618
|
+
process.exit(1);
|
|
2619
|
+
}
|
|
2620
|
+
});
|
|
2621
|
+
program.command("reset").description("Clear all migration history and events from the database").action(async () => {
|
|
2622
|
+
try {
|
|
2623
|
+
const store = new SQLiteStateStore(".metamorph");
|
|
2624
|
+
await store.reset();
|
|
2625
|
+
console.log(chalk.green(`\u2705 Migration state reset successfully.`));
|
|
2626
|
+
} catch (error) {
|
|
2627
|
+
console.error(chalk.red(`\u274C Failed to reset: ${error instanceof Error ? error.message : String(error)}`));
|
|
2628
|
+
process.exit(1);
|
|
2629
|
+
}
|
|
2630
|
+
});
|
|
2631
|
+
program.command("detect [path]").description("Detect frameworks and libraries in a directory").action(async (targetPath) => {
|
|
2632
|
+
try {
|
|
2633
|
+
const detected = detectTechnologies(targetPath || ".");
|
|
2634
|
+
if (detected.length === 0) {
|
|
2635
|
+
console.log(chalk.yellow(`No known technologies detected.`));
|
|
2636
|
+
} else {
|
|
2637
|
+
console.log(chalk.blue(`
|
|
2638
|
+
\u{1F50D} Detected Technologies:`));
|
|
2639
|
+
detected.forEach((d) => console.log(` - ${chalk.green(d.framework)} (Confidence: ${d.confidence}%)
|
|
2640
|
+
Evidence: ${chalk.gray(d.evidence.join(", "))}`));
|
|
2641
|
+
}
|
|
2642
|
+
} catch (error) {
|
|
2643
|
+
console.error(chalk.red(`\u274C Detection failed: ${error instanceof Error ? error.message : String(error)}`));
|
|
2644
|
+
process.exit(1);
|
|
2645
|
+
}
|
|
2646
|
+
});
|
|
2647
|
+
program.command("list").description("List past migration plans").action(async () => {
|
|
2648
|
+
try {
|
|
2649
|
+
const store = new SQLiteStateStore(".metamorph");
|
|
2650
|
+
const plans = await store.getAllPlans();
|
|
2651
|
+
if (plans.length === 0) {
|
|
2652
|
+
console.log(chalk.yellow(`No migration plans found.`));
|
|
2653
|
+
return;
|
|
2654
|
+
}
|
|
2655
|
+
console.log(chalk.blue(`
|
|
2656
|
+
\u{1F4CB} Migration Plans:`));
|
|
2657
|
+
plans.forEach((p) => {
|
|
2658
|
+
console.log(`${chalk.green(p.id)} | ${p.profile.source} -> ${p.profile.target} | Target: ${p.targetPath} | Tasks: ${p.tasks.length}`);
|
|
2659
|
+
});
|
|
2660
|
+
} catch (error) {
|
|
2661
|
+
console.error(chalk.red(`\u274C Failed to list plans: ${error instanceof Error ? error.message : String(error)}`));
|
|
2662
|
+
process.exit(1);
|
|
2663
|
+
}
|
|
2664
|
+
});
|
|
2665
|
+
program.command("ui").description("Start the Metamorph Dashboard API server").option("-p, --port <number>", "Port for the API server", "9876").action(async (options) => {
|
|
2666
|
+
let desiredPort = parseInt(options.port, 10);
|
|
2667
|
+
const spinner = ora("Starting Metamorph API server...").start();
|
|
2668
|
+
try {
|
|
2669
|
+
const getPort = (await import("get-port")).default;
|
|
2670
|
+
const open = (await import("open")).default;
|
|
2671
|
+
const port = await getPort({ port: desiredPort });
|
|
2672
|
+
const store = new SQLiteStateStore(".metamorph");
|
|
2673
|
+
const tools = [
|
|
2674
|
+
createReadFileTool(),
|
|
2675
|
+
createWriteFileTool(),
|
|
2676
|
+
createRenameFileTool(),
|
|
2677
|
+
createCreateFileTool(),
|
|
2678
|
+
createDeleteFileTool()
|
|
2679
|
+
];
|
|
2680
|
+
const runner = new MigrationRunner(store, tools);
|
|
2681
|
+
const app = await createApiServer(store, runner);
|
|
2682
|
+
app.listen(port, () => {
|
|
2683
|
+
spinner.succeed(
|
|
2684
|
+
`Metamorph UI running on http://localhost:${port}`
|
|
2685
|
+
);
|
|
2686
|
+
console.log(chalk.blue(`
|
|
2687
|
+
Endpoints available:`));
|
|
2688
|
+
console.log(chalk.gray(` GET /api/plans`));
|
|
2689
|
+
console.log(chalk.gray(` GET /api/events`));
|
|
2690
|
+
console.log(chalk.gray(` POST /api/migrations/start`));
|
|
2691
|
+
console.log(chalk.gray(` POST /api/migrations/rollback`));
|
|
2692
|
+
open(`http://localhost:${port}`);
|
|
2693
|
+
});
|
|
2694
|
+
} catch (error) {
|
|
2695
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
2696
|
+
spinner.fail(`Failed to start: ${errorMessage}`);
|
|
2697
|
+
process.exit(1);
|
|
2698
|
+
}
|
|
2699
|
+
});
|
|
2700
|
+
program.parse(process.argv);
|