@serviceme/devtools-core 0.0.3
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/LICENSE.md +46 -0
- package/README.md +15 -0
- package/dist/index.d.mts +115 -0
- package/dist/index.d.ts +115 -0
- package/dist/index.js +1239 -0
- package/dist/index.mjs +1217 -0
- package/package.json +54 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1239 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/index.ts
|
|
31
|
+
var index_exports = {};
|
|
32
|
+
__export(index_exports, {
|
|
33
|
+
EnvironmentInspector: () => EnvironmentInspector,
|
|
34
|
+
ImageTools: () => ImageTools,
|
|
35
|
+
OpenCodeManager: () => OpenCodeManager,
|
|
36
|
+
ProjectTools: () => ProjectTools,
|
|
37
|
+
createConsoleLogger: () => createConsoleLogger,
|
|
38
|
+
createImageTools: () => createImageTools,
|
|
39
|
+
createJsonTools: () => createJsonTools,
|
|
40
|
+
createProjectTools: () => createProjectTools,
|
|
41
|
+
moveFiles: () => moveFiles,
|
|
42
|
+
noopLogger: () => noopLogger,
|
|
43
|
+
unzipFile: () => unzipFile
|
|
44
|
+
});
|
|
45
|
+
module.exports = __toCommonJS(index_exports);
|
|
46
|
+
|
|
47
|
+
// src/env/environmentInspector.ts
|
|
48
|
+
var import_devtools_protocol = require("@serviceme/devtools-protocol");
|
|
49
|
+
var import_devtools_protocol2 = require("@serviceme/devtools-protocol");
|
|
50
|
+
|
|
51
|
+
// src/process/runCommand.ts
|
|
52
|
+
var import_node_child_process = require("child_process");
|
|
53
|
+
async function runCommand(command, options = {}) {
|
|
54
|
+
return new Promise((resolve, reject) => {
|
|
55
|
+
const child = (0, import_node_child_process.spawn)(command, options.args ?? [], {
|
|
56
|
+
cwd: options.cwd,
|
|
57
|
+
env: options.env,
|
|
58
|
+
shell: options.shell,
|
|
59
|
+
stdio: "pipe"
|
|
60
|
+
});
|
|
61
|
+
let stdout = "";
|
|
62
|
+
let stderr = "";
|
|
63
|
+
let finished = false;
|
|
64
|
+
let timeoutId;
|
|
65
|
+
const finish = (handler) => {
|
|
66
|
+
if (finished) {
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
finished = true;
|
|
70
|
+
if (timeoutId) {
|
|
71
|
+
clearTimeout(timeoutId);
|
|
72
|
+
}
|
|
73
|
+
handler();
|
|
74
|
+
};
|
|
75
|
+
child.stdout?.setEncoding("utf8");
|
|
76
|
+
child.stderr?.setEncoding("utf8");
|
|
77
|
+
child.stdout?.on("data", (chunk) => {
|
|
78
|
+
stdout += chunk;
|
|
79
|
+
});
|
|
80
|
+
child.stderr?.on("data", (chunk) => {
|
|
81
|
+
stderr += chunk;
|
|
82
|
+
});
|
|
83
|
+
child.once("error", (error) => {
|
|
84
|
+
finish(() => reject(error));
|
|
85
|
+
});
|
|
86
|
+
child.once("close", (code) => {
|
|
87
|
+
finish(() => {
|
|
88
|
+
if (code === 0) {
|
|
89
|
+
resolve({
|
|
90
|
+
stdout,
|
|
91
|
+
stderr,
|
|
92
|
+
code: 0
|
|
93
|
+
});
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
const error = new Error(
|
|
97
|
+
stderr || stdout || `Command failed with exit code ${String(code ?? 1)}.`
|
|
98
|
+
);
|
|
99
|
+
error.code = code ?? 1;
|
|
100
|
+
error.stdout = stdout;
|
|
101
|
+
error.stderr = stderr;
|
|
102
|
+
reject(error);
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
if (options.stdin !== void 0) {
|
|
106
|
+
child.stdin?.end(options.stdin);
|
|
107
|
+
} else {
|
|
108
|
+
child.stdin?.end();
|
|
109
|
+
}
|
|
110
|
+
if (options.timeoutMs) {
|
|
111
|
+
timeoutId = setTimeout(() => {
|
|
112
|
+
finish(() => {
|
|
113
|
+
child.kill("SIGTERM");
|
|
114
|
+
const error = new Error(
|
|
115
|
+
`Command timed out after ${String(options.timeoutMs)}ms.`
|
|
116
|
+
);
|
|
117
|
+
error.code = "ETIMEDOUT";
|
|
118
|
+
error.stdout = stdout;
|
|
119
|
+
error.stderr = stderr;
|
|
120
|
+
reject(error);
|
|
121
|
+
});
|
|
122
|
+
}, options.timeoutMs);
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
async function commandExists(command) {
|
|
127
|
+
const isWindows = process.platform === "win32";
|
|
128
|
+
try {
|
|
129
|
+
await runCommand(isWindows ? "where" : "which", {
|
|
130
|
+
args: [command],
|
|
131
|
+
timeoutMs: 5e3
|
|
132
|
+
});
|
|
133
|
+
return true;
|
|
134
|
+
} catch {
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// src/env/environmentInspector.ts
|
|
140
|
+
var TOOL_CHECK_TIMEOUT_MS = 5e3;
|
|
141
|
+
var ERROR_CODE_NOT_FOUND = 127;
|
|
142
|
+
var EnvironmentInspector = class {
|
|
143
|
+
async checkEnvironment() {
|
|
144
|
+
const results = await Promise.all(
|
|
145
|
+
import_devtools_protocol.KNOWN_ENVIRONMENT_TOOLS.map(async (tool) => [tool, await this.checkTool(tool)])
|
|
146
|
+
);
|
|
147
|
+
return Object.fromEntries(results);
|
|
148
|
+
}
|
|
149
|
+
async checkTool(toolName) {
|
|
150
|
+
if (!/^[a-zA-Z0-9-]+$/.test(toolName)) {
|
|
151
|
+
throw (0, import_devtools_protocol2.createServicemeError)("invalid_params", "Invalid tool name.");
|
|
152
|
+
}
|
|
153
|
+
try {
|
|
154
|
+
if (toolName === "nvm") {
|
|
155
|
+
return await this.checkNvm();
|
|
156
|
+
}
|
|
157
|
+
if (toolName === "nuget") {
|
|
158
|
+
return await this.checkNuget();
|
|
159
|
+
}
|
|
160
|
+
const toolPath = await this.getToolPath(toolName);
|
|
161
|
+
const version = await this.getToolVersion(toolName);
|
|
162
|
+
return {
|
|
163
|
+
installed: true,
|
|
164
|
+
version,
|
|
165
|
+
path: toolPath
|
|
166
|
+
};
|
|
167
|
+
} catch (error) {
|
|
168
|
+
return this.handleToolCheckError(error);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
async getToolPath(toolName) {
|
|
172
|
+
try {
|
|
173
|
+
const isWindows = process.platform === "win32";
|
|
174
|
+
const result = await runCommand(isWindows ? "where" : "which", {
|
|
175
|
+
args: [toolName],
|
|
176
|
+
timeoutMs: TOOL_CHECK_TIMEOUT_MS
|
|
177
|
+
});
|
|
178
|
+
return result.stdout.trim().split("\n")[0];
|
|
179
|
+
} catch {
|
|
180
|
+
return void 0;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
async getToolVersion(toolName) {
|
|
184
|
+
const command = this.getVersionCommand(toolName);
|
|
185
|
+
const isWindows = process.platform === "win32";
|
|
186
|
+
const result = await runCommand(isWindows ? "cmd.exe" : "/bin/bash", {
|
|
187
|
+
args: isWindows ? ["/c", command] : ["-lc", command],
|
|
188
|
+
timeoutMs: TOOL_CHECK_TIMEOUT_MS
|
|
189
|
+
});
|
|
190
|
+
return this.parseVersion(toolName, result.stdout || result.stderr);
|
|
191
|
+
}
|
|
192
|
+
async checkNvm() {
|
|
193
|
+
if (process.platform === "win32") {
|
|
194
|
+
try {
|
|
195
|
+
const result = await runCommand("cmd.exe", {
|
|
196
|
+
args: ["/c", "nvm version"],
|
|
197
|
+
timeoutMs: TOOL_CHECK_TIMEOUT_MS
|
|
198
|
+
});
|
|
199
|
+
return {
|
|
200
|
+
installed: true,
|
|
201
|
+
version: result.stdout.trim(),
|
|
202
|
+
path: await this.getToolPath("nvm")
|
|
203
|
+
};
|
|
204
|
+
} catch {
|
|
205
|
+
return {
|
|
206
|
+
installed: false,
|
|
207
|
+
error: "Not installed"
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
try {
|
|
212
|
+
const result = await runCommand("/bin/bash", {
|
|
213
|
+
args: [
|
|
214
|
+
"-lc",
|
|
215
|
+
'export NVM_DIR="$HOME/.nvm"; [ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh"; nvm --version'
|
|
216
|
+
],
|
|
217
|
+
timeoutMs: TOOL_CHECK_TIMEOUT_MS
|
|
218
|
+
});
|
|
219
|
+
return {
|
|
220
|
+
installed: true,
|
|
221
|
+
version: result.stdout.trim(),
|
|
222
|
+
path: "$HOME/.nvm/nvm.sh"
|
|
223
|
+
};
|
|
224
|
+
} catch {
|
|
225
|
+
return {
|
|
226
|
+
installed: false,
|
|
227
|
+
error: "Not installed"
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
async checkNuget() {
|
|
232
|
+
const dotnetPath = await this.getToolPath("dotnet");
|
|
233
|
+
if (!dotnetPath) {
|
|
234
|
+
return {
|
|
235
|
+
installed: false,
|
|
236
|
+
error: "dotnet CLI is not installed"
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
try {
|
|
240
|
+
const result = await runCommand("dotnet", {
|
|
241
|
+
args: ["nuget", "list", "source"],
|
|
242
|
+
timeoutMs: TOOL_CHECK_TIMEOUT_MS
|
|
243
|
+
});
|
|
244
|
+
const privateSourceUrl = "http://192.168.20.209:10010/nuget";
|
|
245
|
+
if (result.stdout.includes(privateSourceUrl)) {
|
|
246
|
+
return {
|
|
247
|
+
installed: true,
|
|
248
|
+
version: "Configured",
|
|
249
|
+
path: privateSourceUrl
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
return {
|
|
253
|
+
installed: false,
|
|
254
|
+
error: "Private source not configured"
|
|
255
|
+
};
|
|
256
|
+
} catch (error) {
|
|
257
|
+
return this.handleToolCheckError(error);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
getVersionCommand(toolName) {
|
|
261
|
+
const commands = {
|
|
262
|
+
git: "git --version",
|
|
263
|
+
node: "node --version",
|
|
264
|
+
npm: "npm --version",
|
|
265
|
+
pnpm: "pnpm --version",
|
|
266
|
+
nvm: "nvm --version",
|
|
267
|
+
nrm: "nrm --version",
|
|
268
|
+
dotnet: "dotnet --version"
|
|
269
|
+
};
|
|
270
|
+
return commands[toolName] || `${toolName} --version`;
|
|
271
|
+
}
|
|
272
|
+
parseVersion(toolName, output) {
|
|
273
|
+
const cleaned = output.trim();
|
|
274
|
+
switch (toolName) {
|
|
275
|
+
case "git": {
|
|
276
|
+
const match = cleaned.match(/git version (\d+\.\d+\.\d+)/);
|
|
277
|
+
return match?.[1] || cleaned;
|
|
278
|
+
}
|
|
279
|
+
case "node": {
|
|
280
|
+
const match = cleaned.match(/v?(\d+\.\d+\.\d+)/);
|
|
281
|
+
return match?.[1] || cleaned;
|
|
282
|
+
}
|
|
283
|
+
default: {
|
|
284
|
+
const semver = cleaned.match(/(\d+\.\d+\.\d+)/);
|
|
285
|
+
if (semver?.[1]) {
|
|
286
|
+
return semver[1];
|
|
287
|
+
}
|
|
288
|
+
const simple = cleaned.match(/(\d+\.\d+)/);
|
|
289
|
+
return simple?.[1] || cleaned;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
handleToolCheckError(error) {
|
|
294
|
+
const execError = error;
|
|
295
|
+
const message = execError.message || String(error);
|
|
296
|
+
const code = execError.code;
|
|
297
|
+
const isNotFound = message.includes("command not found") || message.includes("not recognized") || code === ERROR_CODE_NOT_FOUND;
|
|
298
|
+
return {
|
|
299
|
+
installed: false,
|
|
300
|
+
error: isNotFound ? "Not installed" : message
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
// src/image/imageTools.ts
|
|
306
|
+
var fs = __toESM(require("fs/promises"));
|
|
307
|
+
var path = __toESM(require("path"));
|
|
308
|
+
var import_devtools_protocol3 = require("@serviceme/devtools-protocol");
|
|
309
|
+
var SUPPORTED_FORMATS = [".jpg", ".jpeg", ".png", ".webp", ".gif", ".bmp", ".tiff"];
|
|
310
|
+
var DEFAULT_MINIMUM_COMPRESSION_RATIO = 5;
|
|
311
|
+
var ImageTools = class {
|
|
312
|
+
getSupportedFormats() {
|
|
313
|
+
return [...SUPPORTED_FORMATS];
|
|
314
|
+
}
|
|
315
|
+
async validate(filePath, sharpModulePath) {
|
|
316
|
+
try {
|
|
317
|
+
if (!await this.pathExists(filePath)) {
|
|
318
|
+
return { valid: false };
|
|
319
|
+
}
|
|
320
|
+
if (!SUPPORTED_FORMATS.includes(path.extname(filePath).toLowerCase())) {
|
|
321
|
+
return { valid: false };
|
|
322
|
+
}
|
|
323
|
+
await this.getInfo(filePath, sharpModulePath);
|
|
324
|
+
return { valid: true };
|
|
325
|
+
} catch {
|
|
326
|
+
return { valid: false };
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
async getInfo(imagePath, sharpModulePath) {
|
|
330
|
+
const sharp = this.loadSharp(sharpModulePath);
|
|
331
|
+
const metadata = await sharp(imagePath).metadata();
|
|
332
|
+
const stats = await fs.stat(imagePath);
|
|
333
|
+
return {
|
|
334
|
+
width: metadata.width ?? 0,
|
|
335
|
+
height: metadata.height ?? 0,
|
|
336
|
+
format: metadata.format ?? "unknown",
|
|
337
|
+
size: stats.size,
|
|
338
|
+
colorSpace: metadata.space
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
async compress(imagePath, options) {
|
|
342
|
+
const validation = await this.validate(imagePath, options.sharpModulePath);
|
|
343
|
+
if (!validation.valid) {
|
|
344
|
+
throw (0, import_devtools_protocol3.createServicemeError)("invalid_params", `Invalid image file: ${imagePath}`);
|
|
345
|
+
}
|
|
346
|
+
const originalStats = await fs.stat(imagePath);
|
|
347
|
+
const originalSize = originalStats.size;
|
|
348
|
+
const outputPath = this.getOutputPath(imagePath, options);
|
|
349
|
+
const compressedBuffer = await this.compressWithSharp(imagePath, options);
|
|
350
|
+
const compressedSize = compressedBuffer.length;
|
|
351
|
+
const compressionRatio = (originalSize - compressedSize) / originalSize * 100;
|
|
352
|
+
const minimumCompressionRatio = options.minimumCompressionRatio ?? DEFAULT_MINIMUM_COMPRESSION_RATIO;
|
|
353
|
+
if (compressionRatio < minimumCompressionRatio) {
|
|
354
|
+
return {
|
|
355
|
+
originalSize,
|
|
356
|
+
compressedSize: originalSize,
|
|
357
|
+
compressionRatio: 0,
|
|
358
|
+
outputPath: imagePath
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
await fs.writeFile(outputPath, compressedBuffer);
|
|
362
|
+
return {
|
|
363
|
+
originalSize,
|
|
364
|
+
compressedSize,
|
|
365
|
+
compressionRatio,
|
|
366
|
+
outputPath
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
async compressWithSharp(imagePath, options) {
|
|
370
|
+
const sharp = this.loadSharp(options.sharpModulePath);
|
|
371
|
+
let pipeline = sharp(imagePath);
|
|
372
|
+
switch (options.format ?? path.extname(imagePath).toLowerCase().slice(1)) {
|
|
373
|
+
case "jpg":
|
|
374
|
+
case "jpeg":
|
|
375
|
+
pipeline = pipeline.jpeg({ quality: options.quality });
|
|
376
|
+
break;
|
|
377
|
+
case "png":
|
|
378
|
+
pipeline = pipeline.png({
|
|
379
|
+
quality: options.quality,
|
|
380
|
+
compressionLevel: 9
|
|
381
|
+
});
|
|
382
|
+
break;
|
|
383
|
+
case "webp":
|
|
384
|
+
pipeline = pipeline.webp({ quality: options.quality });
|
|
385
|
+
break;
|
|
386
|
+
default:
|
|
387
|
+
pipeline = pipeline.jpeg({ quality: options.quality });
|
|
388
|
+
}
|
|
389
|
+
return pipeline.toBuffer();
|
|
390
|
+
}
|
|
391
|
+
getOutputPath(inputPath, options) {
|
|
392
|
+
if (options.outputPath) {
|
|
393
|
+
return options.outputPath;
|
|
394
|
+
}
|
|
395
|
+
if (options.replaceOriginImage) {
|
|
396
|
+
return inputPath;
|
|
397
|
+
}
|
|
398
|
+
const dir = path.dirname(inputPath);
|
|
399
|
+
const ext = path.extname(inputPath);
|
|
400
|
+
const name = path.basename(inputPath, ext);
|
|
401
|
+
return path.join(dir, `${name}_compressed${ext}`);
|
|
402
|
+
}
|
|
403
|
+
async pathExists(targetPath) {
|
|
404
|
+
try {
|
|
405
|
+
await fs.access(targetPath);
|
|
406
|
+
return true;
|
|
407
|
+
} catch {
|
|
408
|
+
return false;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
loadSharp(sharpModulePath) {
|
|
412
|
+
try {
|
|
413
|
+
return sharpModulePath ? require(sharpModulePath) : require("sharp");
|
|
414
|
+
} catch (error) {
|
|
415
|
+
throw (0, import_devtools_protocol3.createServicemeError)(
|
|
416
|
+
"internal_error",
|
|
417
|
+
`sharp is required for image operations: ${error instanceof Error ? error.message : String(error)}`
|
|
418
|
+
);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
};
|
|
422
|
+
function createImageTools() {
|
|
423
|
+
return new ImageTools();
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// src/json/jsonTools.ts
|
|
427
|
+
var import_comment_json = require("comment-json");
|
|
428
|
+
var import_json5 = __toESM(require("json5"));
|
|
429
|
+
var import_devtools_protocol4 = require("@serviceme/devtools-protocol");
|
|
430
|
+
var import_devtools_protocol5 = require("@serviceme/devtools-protocol");
|
|
431
|
+
function createJsonTools() {
|
|
432
|
+
return {
|
|
433
|
+
sort(text, options) {
|
|
434
|
+
const finalOptions = { ...import_devtools_protocol4.DEFAULT_JSON_SORT_OPTIONS, ...options };
|
|
435
|
+
const json = parseJson(text);
|
|
436
|
+
const sorted = sortObject(json, finalOptions);
|
|
437
|
+
return JSON.stringify(sorted, null, detectIndent(text));
|
|
438
|
+
},
|
|
439
|
+
format(text, indent = 2) {
|
|
440
|
+
return JSON.stringify(parseJson(text), null, indent);
|
|
441
|
+
},
|
|
442
|
+
validate(text) {
|
|
443
|
+
try {
|
|
444
|
+
parseJson(text);
|
|
445
|
+
return { valid: true };
|
|
446
|
+
} catch (error) {
|
|
447
|
+
return {
|
|
448
|
+
valid: false,
|
|
449
|
+
error: error instanceof Error ? error.message : String(error)
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
},
|
|
453
|
+
minify(text) {
|
|
454
|
+
return JSON.stringify(parseJson(text));
|
|
455
|
+
}
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
function parseJson(text) {
|
|
459
|
+
const parsers = [
|
|
460
|
+
() => JSON.parse(text),
|
|
461
|
+
() => (0, import_comment_json.parse)(text),
|
|
462
|
+
() => import_json5.default.parse(text)
|
|
463
|
+
];
|
|
464
|
+
for (const parser of parsers) {
|
|
465
|
+
try {
|
|
466
|
+
return parser();
|
|
467
|
+
} catch {
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
throw (0, import_devtools_protocol5.createServicemeError)("json_invalid_input", "Invalid JSON format.");
|
|
471
|
+
}
|
|
472
|
+
function sortObject(obj, options) {
|
|
473
|
+
if (Array.isArray(obj)) {
|
|
474
|
+
return obj.map((item) => sortObject(item, options));
|
|
475
|
+
}
|
|
476
|
+
if (obj !== null && typeof obj === "object") {
|
|
477
|
+
const record = obj;
|
|
478
|
+
const sortedKeys = sortKeys(Object.keys(record), options);
|
|
479
|
+
const result = {};
|
|
480
|
+
for (const key of sortedKeys) {
|
|
481
|
+
result[key] = sortObject(record[key], options);
|
|
482
|
+
}
|
|
483
|
+
return result;
|
|
484
|
+
}
|
|
485
|
+
return obj;
|
|
486
|
+
}
|
|
487
|
+
function sortKeys(keys, options) {
|
|
488
|
+
let compareFn;
|
|
489
|
+
switch (options.sortAlgo) {
|
|
490
|
+
case "keyLength":
|
|
491
|
+
compareFn = (a, b) => a.length - b.length;
|
|
492
|
+
break;
|
|
493
|
+
case "alphaNum":
|
|
494
|
+
compareFn = (a, b) => a.localeCompare(b, void 0, { numeric: true });
|
|
495
|
+
break;
|
|
496
|
+
case "values":
|
|
497
|
+
case "type":
|
|
498
|
+
case "default":
|
|
499
|
+
default:
|
|
500
|
+
compareFn = (a, b) => a.localeCompare(b);
|
|
501
|
+
break;
|
|
502
|
+
}
|
|
503
|
+
const sorted = [...keys].sort(compareFn);
|
|
504
|
+
return options.sortOrder === "desc" ? sorted.reverse() : sorted;
|
|
505
|
+
}
|
|
506
|
+
function detectIndent(text) {
|
|
507
|
+
for (const line of text.split("\n")) {
|
|
508
|
+
const match = line.match(/^(\s+)/);
|
|
509
|
+
if (match?.[1]) {
|
|
510
|
+
return match[1].length;
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
return 2;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// src/logger.ts
|
|
517
|
+
var noopLogger = {
|
|
518
|
+
debug() {
|
|
519
|
+
},
|
|
520
|
+
info() {
|
|
521
|
+
},
|
|
522
|
+
warn() {
|
|
523
|
+
},
|
|
524
|
+
error() {
|
|
525
|
+
}
|
|
526
|
+
};
|
|
527
|
+
function formatArgs(args) {
|
|
528
|
+
return args.map((arg) => {
|
|
529
|
+
if (typeof arg === "string") {
|
|
530
|
+
return arg;
|
|
531
|
+
}
|
|
532
|
+
try {
|
|
533
|
+
return JSON.stringify(arg);
|
|
534
|
+
} catch {
|
|
535
|
+
return String(arg);
|
|
536
|
+
}
|
|
537
|
+
}).join(" ");
|
|
538
|
+
}
|
|
539
|
+
function createConsoleLogger(prefix = "serviceme") {
|
|
540
|
+
return {
|
|
541
|
+
debug(message, ...args) {
|
|
542
|
+
process.stderr.write(`[${prefix}] DEBUG ${message} ${formatArgs(args)}
|
|
543
|
+
`);
|
|
544
|
+
},
|
|
545
|
+
info(message, ...args) {
|
|
546
|
+
process.stderr.write(`[${prefix}] INFO ${message} ${formatArgs(args)}
|
|
547
|
+
`);
|
|
548
|
+
},
|
|
549
|
+
warn(message, ...args) {
|
|
550
|
+
process.stderr.write(`[${prefix}] WARN ${message} ${formatArgs(args)}
|
|
551
|
+
`);
|
|
552
|
+
},
|
|
553
|
+
error(message, ...args) {
|
|
554
|
+
process.stderr.write(`[${prefix}] ERROR ${message} ${formatArgs(args)}
|
|
555
|
+
`);
|
|
556
|
+
}
|
|
557
|
+
};
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
// src/opencode/OpenCodeManager.ts
|
|
561
|
+
var import_node_child_process2 = require("child_process");
|
|
562
|
+
var import_node_crypto = require("crypto");
|
|
563
|
+
var import_devtools_protocol6 = require("@serviceme/devtools-protocol");
|
|
564
|
+
var DEFAULT_OPTIONS = {
|
|
565
|
+
command: "opencode",
|
|
566
|
+
installUrl: "https://opencode.ai",
|
|
567
|
+
host: "127.0.0.1",
|
|
568
|
+
portMin: 16384,
|
|
569
|
+
portMax: 65535,
|
|
570
|
+
healthPath: "/app",
|
|
571
|
+
sessionPath: "/session",
|
|
572
|
+
sessionStatusPath: "/session/status",
|
|
573
|
+
startupPollIntervalMs: 200,
|
|
574
|
+
idleTimeoutMs: 10 * 60 * 1e3,
|
|
575
|
+
stdoutLogLimitBytes: 64 * 1024
|
|
576
|
+
};
|
|
577
|
+
var OpenCodeManager = class {
|
|
578
|
+
constructor(options = {}) {
|
|
579
|
+
this.stdoutBytes = 0;
|
|
580
|
+
this.options = {
|
|
581
|
+
...DEFAULT_OPTIONS,
|
|
582
|
+
...options
|
|
583
|
+
};
|
|
584
|
+
this.logger = options.logger ?? noopLogger;
|
|
585
|
+
}
|
|
586
|
+
async isAvailable() {
|
|
587
|
+
return commandExists(this.options.command);
|
|
588
|
+
}
|
|
589
|
+
isRunning() {
|
|
590
|
+
return Boolean(this.childProcess && !this.childProcess.killed && this.port);
|
|
591
|
+
}
|
|
592
|
+
async ensureServer(runtime = {}) {
|
|
593
|
+
await this.ensureServerStarted(runtime);
|
|
594
|
+
return this.getServerState();
|
|
595
|
+
}
|
|
596
|
+
async getServerState() {
|
|
597
|
+
const available = await this.isAvailable();
|
|
598
|
+
const running = this.isRunning();
|
|
599
|
+
const healthy = this.port ? await this.isServerHealthy(this.port) : false;
|
|
600
|
+
return {
|
|
601
|
+
available,
|
|
602
|
+
running,
|
|
603
|
+
healthy,
|
|
604
|
+
installUrl: this.options.installUrl,
|
|
605
|
+
pid: this.childProcess?.pid,
|
|
606
|
+
port: this.port
|
|
607
|
+
};
|
|
608
|
+
}
|
|
609
|
+
async createSession(runtime = {}) {
|
|
610
|
+
await this.ensureServerStarted(runtime);
|
|
611
|
+
const session = await this.request(
|
|
612
|
+
this.options.sessionPath,
|
|
613
|
+
{
|
|
614
|
+
method: "POST",
|
|
615
|
+
query: this.createQuery(runtime.workspaceRoot),
|
|
616
|
+
body: {}
|
|
617
|
+
}
|
|
618
|
+
);
|
|
619
|
+
this.resetIdleTimer();
|
|
620
|
+
return {
|
|
621
|
+
sessionId: session.id,
|
|
622
|
+
title: session.title,
|
|
623
|
+
status: "running"
|
|
624
|
+
};
|
|
625
|
+
}
|
|
626
|
+
async prompt(input) {
|
|
627
|
+
await this.ensureServerStarted({ workspaceRoot: input.workspaceRoot });
|
|
628
|
+
await this.request(
|
|
629
|
+
`${this.options.sessionPath}/${input.sessionId}/message`,
|
|
630
|
+
{
|
|
631
|
+
method: "POST",
|
|
632
|
+
query: this.createQuery(input.workspaceRoot),
|
|
633
|
+
body: {
|
|
634
|
+
parts: [
|
|
635
|
+
{
|
|
636
|
+
type: "text",
|
|
637
|
+
text: input.prompt
|
|
638
|
+
}
|
|
639
|
+
]
|
|
640
|
+
},
|
|
641
|
+
signal: input.signal
|
|
642
|
+
}
|
|
643
|
+
);
|
|
644
|
+
const latestMessage = await this.getLatestAssistantMessage(
|
|
645
|
+
input.sessionId,
|
|
646
|
+
input.workspaceRoot,
|
|
647
|
+
input.signal
|
|
648
|
+
);
|
|
649
|
+
input.onEvent?.({
|
|
650
|
+
type: "status",
|
|
651
|
+
status: "running"
|
|
652
|
+
});
|
|
653
|
+
if (latestMessage) {
|
|
654
|
+
input.onEvent?.({
|
|
655
|
+
type: "message",
|
|
656
|
+
text: latestMessage
|
|
657
|
+
});
|
|
658
|
+
}
|
|
659
|
+
input.onEvent?.({
|
|
660
|
+
type: "completed"
|
|
661
|
+
});
|
|
662
|
+
this.resetIdleTimer();
|
|
663
|
+
}
|
|
664
|
+
async getSessionStatus(sessionId, workspaceRoot) {
|
|
665
|
+
await this.ensureServerStarted({ workspaceRoot });
|
|
666
|
+
const response = await this.request(this.options.sessionStatusPath, {
|
|
667
|
+
method: "GET",
|
|
668
|
+
query: this.createQuery(workspaceRoot)
|
|
669
|
+
});
|
|
670
|
+
const session = response.find((item) => item.id === sessionId);
|
|
671
|
+
this.resetIdleTimer();
|
|
672
|
+
return this.mapStatus(session?.status);
|
|
673
|
+
}
|
|
674
|
+
async abortSession(sessionId, workspaceRoot) {
|
|
675
|
+
await this.ensureServerStarted({ workspaceRoot });
|
|
676
|
+
await this.request(`${this.options.sessionPath}/${sessionId}/abort`, {
|
|
677
|
+
method: "POST",
|
|
678
|
+
query: this.createQuery(workspaceRoot)
|
|
679
|
+
});
|
|
680
|
+
this.resetIdleTimer();
|
|
681
|
+
}
|
|
682
|
+
async dispose() {
|
|
683
|
+
if (this.idleTimer) {
|
|
684
|
+
clearTimeout(this.idleTimer);
|
|
685
|
+
this.idleTimer = void 0;
|
|
686
|
+
}
|
|
687
|
+
await this.terminateTrackedProcess("dispose");
|
|
688
|
+
this.startupPromise = void 0;
|
|
689
|
+
}
|
|
690
|
+
async ensureServerStarted(runtime) {
|
|
691
|
+
if (this.port) {
|
|
692
|
+
if (await this.isServerHealthy(this.port)) {
|
|
693
|
+
return;
|
|
694
|
+
}
|
|
695
|
+
await this.terminateTrackedProcess("restart-unhealthy");
|
|
696
|
+
}
|
|
697
|
+
if (this.startupPromise) {
|
|
698
|
+
await this.startupPromise;
|
|
699
|
+
return;
|
|
700
|
+
}
|
|
701
|
+
if (!await this.isAvailable()) {
|
|
702
|
+
throw (0, import_devtools_protocol6.createServicemeError)(
|
|
703
|
+
"opencode_not_installed",
|
|
704
|
+
"OpenCode CLI is not installed."
|
|
705
|
+
);
|
|
706
|
+
}
|
|
707
|
+
const MAX_PORT_RETRIES = 3;
|
|
708
|
+
let lastError;
|
|
709
|
+
for (let attempt = 0; attempt < MAX_PORT_RETRIES; attempt++) {
|
|
710
|
+
this.startupPromise = this.startServerProcess(runtime);
|
|
711
|
+
try {
|
|
712
|
+
await this.startupPromise;
|
|
713
|
+
return;
|
|
714
|
+
} catch (error) {
|
|
715
|
+
lastError = error;
|
|
716
|
+
this.logger.warn(
|
|
717
|
+
`OpenCode server start attempt ${String(attempt + 1)} failed, retrying...`
|
|
718
|
+
);
|
|
719
|
+
} finally {
|
|
720
|
+
this.startupPromise = void 0;
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
throw lastError;
|
|
724
|
+
}
|
|
725
|
+
async startServerProcess(runtime) {
|
|
726
|
+
const port = (0, import_node_crypto.randomInt)(this.options.portMin, this.options.portMax + 1);
|
|
727
|
+
const child = (0, import_node_child_process2.spawn)(this.options.command, ["--port", String(port)], {
|
|
728
|
+
cwd: runtime.workspaceRoot,
|
|
729
|
+
detached: process.platform !== "win32",
|
|
730
|
+
env: {
|
|
731
|
+
...process.env,
|
|
732
|
+
OPENCODE_CALLER: "serviceme-cli"
|
|
733
|
+
},
|
|
734
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
735
|
+
});
|
|
736
|
+
this.stdoutBytes = 0;
|
|
737
|
+
child.stdout?.on("data", (chunk) => {
|
|
738
|
+
if (this.stdoutBytes < this.options.stdoutLogLimitBytes) {
|
|
739
|
+
this.logger.debug("OpenCode stdout", chunk.toString());
|
|
740
|
+
this.stdoutBytes += chunk.length;
|
|
741
|
+
}
|
|
742
|
+
});
|
|
743
|
+
child.stderr?.on("data", (chunk) => {
|
|
744
|
+
this.logger.warn("OpenCode stderr", chunk.toString());
|
|
745
|
+
});
|
|
746
|
+
child.on("error", (error) => {
|
|
747
|
+
this.logger.error("OpenCode process error", error);
|
|
748
|
+
if (this.childProcess === child) {
|
|
749
|
+
this.childProcess = void 0;
|
|
750
|
+
this.port = void 0;
|
|
751
|
+
}
|
|
752
|
+
});
|
|
753
|
+
child.on("exit", () => {
|
|
754
|
+
if (this.childProcess === child) {
|
|
755
|
+
this.childProcess = void 0;
|
|
756
|
+
this.port = void 0;
|
|
757
|
+
}
|
|
758
|
+
});
|
|
759
|
+
if (process.platform !== "win32" && child.pid) {
|
|
760
|
+
(0, import_node_child_process2.spawn)("renice", ["+10", String(child.pid)], { stdio: "ignore" }).unref();
|
|
761
|
+
}
|
|
762
|
+
this.childProcess = child;
|
|
763
|
+
this.port = port;
|
|
764
|
+
const ready = await this.waitForHealth(
|
|
765
|
+
port,
|
|
766
|
+
runtime.startupTimeoutMs ?? 5e3
|
|
767
|
+
);
|
|
768
|
+
if (!ready) {
|
|
769
|
+
await this.terminateTrackedProcess("startup-timeout");
|
|
770
|
+
throw (0, import_devtools_protocol6.createServicemeError)(
|
|
771
|
+
"opencode_startup_timeout",
|
|
772
|
+
"OpenCode local server did not start in time."
|
|
773
|
+
);
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
async waitForHealth(port, timeoutMs) {
|
|
777
|
+
const startedAt = Date.now();
|
|
778
|
+
while (Date.now() - startedAt < timeoutMs) {
|
|
779
|
+
if (await this.isServerHealthy(port)) {
|
|
780
|
+
return true;
|
|
781
|
+
}
|
|
782
|
+
await new Promise((resolve) => {
|
|
783
|
+
setTimeout(resolve, this.options.startupPollIntervalMs);
|
|
784
|
+
});
|
|
785
|
+
}
|
|
786
|
+
return false;
|
|
787
|
+
}
|
|
788
|
+
async isServerHealthy(port) {
|
|
789
|
+
try {
|
|
790
|
+
const response = await fetch(
|
|
791
|
+
`http://${this.options.host}:${port}${this.options.healthPath}`
|
|
792
|
+
);
|
|
793
|
+
return response.ok;
|
|
794
|
+
} catch {
|
|
795
|
+
return false;
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
resetIdleTimer() {
|
|
799
|
+
if (this.idleTimer) {
|
|
800
|
+
clearTimeout(this.idleTimer);
|
|
801
|
+
}
|
|
802
|
+
this.idleTimer = setTimeout(() => {
|
|
803
|
+
void this.dispose();
|
|
804
|
+
}, this.options.idleTimeoutMs);
|
|
805
|
+
this.idleTimer.unref?.();
|
|
806
|
+
}
|
|
807
|
+
async terminateTrackedProcess(reason) {
|
|
808
|
+
const child = this.childProcess;
|
|
809
|
+
this.childProcess = void 0;
|
|
810
|
+
this.port = void 0;
|
|
811
|
+
if (!child || child.killed) {
|
|
812
|
+
return;
|
|
813
|
+
}
|
|
814
|
+
this.logger.info("Stopping tracked OpenCode process", {
|
|
815
|
+
reason,
|
|
816
|
+
pid: child.pid
|
|
817
|
+
});
|
|
818
|
+
if (process.platform === "win32") {
|
|
819
|
+
if (child.pid) {
|
|
820
|
+
await new Promise((resolve) => {
|
|
821
|
+
const killProcess = (0, import_node_child_process2.spawn)(
|
|
822
|
+
"taskkill",
|
|
823
|
+
["/pid", String(child.pid), "/T", "/F"],
|
|
824
|
+
{
|
|
825
|
+
stdio: "ignore",
|
|
826
|
+
windowsHide: true
|
|
827
|
+
}
|
|
828
|
+
);
|
|
829
|
+
killProcess.once("exit", () => resolve());
|
|
830
|
+
killProcess.once("error", () => {
|
|
831
|
+
child.kill();
|
|
832
|
+
resolve();
|
|
833
|
+
});
|
|
834
|
+
});
|
|
835
|
+
return;
|
|
836
|
+
}
|
|
837
|
+
child.kill();
|
|
838
|
+
return;
|
|
839
|
+
}
|
|
840
|
+
if (!child.pid) {
|
|
841
|
+
child.kill("SIGTERM");
|
|
842
|
+
return;
|
|
843
|
+
}
|
|
844
|
+
try {
|
|
845
|
+
process.kill(-child.pid, "SIGTERM");
|
|
846
|
+
} catch {
|
|
847
|
+
child.kill("SIGTERM");
|
|
848
|
+
return;
|
|
849
|
+
}
|
|
850
|
+
const childPid = child.pid;
|
|
851
|
+
await new Promise((resolve) => {
|
|
852
|
+
const escalationTimer = setTimeout(() => {
|
|
853
|
+
try {
|
|
854
|
+
process.kill(-childPid, "SIGKILL");
|
|
855
|
+
} catch {
|
|
856
|
+
}
|
|
857
|
+
resolve();
|
|
858
|
+
}, 2e3);
|
|
859
|
+
child.once("exit", () => {
|
|
860
|
+
clearTimeout(escalationTimer);
|
|
861
|
+
try {
|
|
862
|
+
process.kill(-childPid, "SIGKILL");
|
|
863
|
+
} catch {
|
|
864
|
+
}
|
|
865
|
+
resolve();
|
|
866
|
+
});
|
|
867
|
+
});
|
|
868
|
+
}
|
|
869
|
+
async request(path3, options) {
|
|
870
|
+
if (!this.port) {
|
|
871
|
+
throw (0, import_devtools_protocol6.createServicemeError)(
|
|
872
|
+
"opencode_backend_not_running",
|
|
873
|
+
"OpenCode local server is not running."
|
|
874
|
+
);
|
|
875
|
+
}
|
|
876
|
+
const url = new URL(`http://${this.options.host}:${this.port}${path3}`);
|
|
877
|
+
for (const [key, value] of Object.entries(options.query ?? {})) {
|
|
878
|
+
if (value) {
|
|
879
|
+
url.searchParams.set(key, value);
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
const response = await fetch(url, {
|
|
883
|
+
method: options.method,
|
|
884
|
+
headers: {
|
|
885
|
+
"Content-Type": "application/json"
|
|
886
|
+
},
|
|
887
|
+
body: options.body ? JSON.stringify(options.body) : void 0,
|
|
888
|
+
signal: options.signal
|
|
889
|
+
});
|
|
890
|
+
if (!response.ok) {
|
|
891
|
+
throw (0, import_devtools_protocol6.createServicemeError)(
|
|
892
|
+
"opencode_request_failed",
|
|
893
|
+
`OpenCode request failed: ${response.status} ${response.statusText}`
|
|
894
|
+
);
|
|
895
|
+
}
|
|
896
|
+
if (response.status === 204) {
|
|
897
|
+
return void 0;
|
|
898
|
+
}
|
|
899
|
+
return await response.json();
|
|
900
|
+
}
|
|
901
|
+
createQuery(workspaceRoot) {
|
|
902
|
+
return {
|
|
903
|
+
directory: workspaceRoot
|
|
904
|
+
};
|
|
905
|
+
}
|
|
906
|
+
async getLatestAssistantMessage(sessionId, workspaceRoot, signal) {
|
|
907
|
+
const messages = await this.request(
|
|
908
|
+
`${this.options.sessionPath}/${sessionId}/message`,
|
|
909
|
+
{
|
|
910
|
+
method: "GET",
|
|
911
|
+
query: {
|
|
912
|
+
...this.createQuery(workspaceRoot),
|
|
913
|
+
limit: "20"
|
|
914
|
+
},
|
|
915
|
+
signal
|
|
916
|
+
}
|
|
917
|
+
);
|
|
918
|
+
const latestAssistant = [...messages].reverse().find((message) => message.info?.role === "assistant");
|
|
919
|
+
if (!latestAssistant?.parts) {
|
|
920
|
+
return void 0;
|
|
921
|
+
}
|
|
922
|
+
return latestAssistant.parts.filter(
|
|
923
|
+
(part) => part.type === "text" && typeof part.text === "string"
|
|
924
|
+
).map((part) => part.text).join("\n\n");
|
|
925
|
+
}
|
|
926
|
+
mapStatus(status) {
|
|
927
|
+
switch (status) {
|
|
928
|
+
case "running":
|
|
929
|
+
case "active":
|
|
930
|
+
return "running";
|
|
931
|
+
case "needs_input":
|
|
932
|
+
return "needs-input";
|
|
933
|
+
case "completed":
|
|
934
|
+
return "completed";
|
|
935
|
+
case "failed":
|
|
936
|
+
return "failed";
|
|
937
|
+
case "aborted":
|
|
938
|
+
return "aborted";
|
|
939
|
+
default:
|
|
940
|
+
return "idle";
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
};
|
|
944
|
+
|
|
945
|
+
// src/project/projectTools.ts
|
|
946
|
+
var fs2 = __toESM(require("fs/promises"));
|
|
947
|
+
var path2 = __toESM(require("path"));
|
|
948
|
+
var import_devtools_protocol7 = require("@serviceme/devtools-protocol");
|
|
949
|
+
|
|
950
|
+
// src/utils/fileUtils.ts
|
|
951
|
+
var import_node_fs = require("fs");
|
|
952
|
+
var import_promises = require("fs/promises");
|
|
953
|
+
var import_node_path = require("path");
|
|
954
|
+
var import_yauzl = require("yauzl");
|
|
955
|
+
var unzipFile = (zipPath, dest) => {
|
|
956
|
+
return new Promise((resolve, reject) => {
|
|
957
|
+
(0, import_yauzl.open)(zipPath, { lazyEntries: true }, (err, zipfile) => {
|
|
958
|
+
if (err) return reject(err);
|
|
959
|
+
if (!zipfile) return reject(new Error("Failed to open zip file."));
|
|
960
|
+
zipfile.readEntry();
|
|
961
|
+
zipfile.on("entry", (entry) => {
|
|
962
|
+
if (/\/$/.test(entry.fileName)) {
|
|
963
|
+
void (0, import_promises.mkdir)((0, import_node_path.join)(dest, entry.fileName), { recursive: true }).then(() => {
|
|
964
|
+
zipfile.readEntry();
|
|
965
|
+
}).catch(reject);
|
|
966
|
+
} else {
|
|
967
|
+
const outputPath = (0, import_node_path.join)(dest, entry.fileName);
|
|
968
|
+
void (0, import_promises.mkdir)((0, import_node_path.dirname)(outputPath), { recursive: true }).then(() => {
|
|
969
|
+
zipfile.openReadStream(entry, (streamError, readStream) => {
|
|
970
|
+
if (streamError) return reject(streamError);
|
|
971
|
+
if (!readStream) return reject(new Error("Failed to open zip entry stream."));
|
|
972
|
+
const writeStream = (0, import_node_fs.createWriteStream)(outputPath);
|
|
973
|
+
readStream.on("error", reject);
|
|
974
|
+
writeStream.on("error", reject);
|
|
975
|
+
writeStream.on("close", () => {
|
|
976
|
+
zipfile.readEntry();
|
|
977
|
+
});
|
|
978
|
+
readStream.pipe(writeStream);
|
|
979
|
+
});
|
|
980
|
+
}).catch(reject);
|
|
981
|
+
}
|
|
982
|
+
});
|
|
983
|
+
zipfile.on("end", () => {
|
|
984
|
+
resolve();
|
|
985
|
+
});
|
|
986
|
+
zipfile.on("error", (zipError) => {
|
|
987
|
+
reject(zipError);
|
|
988
|
+
});
|
|
989
|
+
});
|
|
990
|
+
});
|
|
991
|
+
};
|
|
992
|
+
var tryLstat = async (targetPath) => {
|
|
993
|
+
try {
|
|
994
|
+
return await (0, import_promises.lstat)(targetPath);
|
|
995
|
+
} catch {
|
|
996
|
+
return null;
|
|
997
|
+
}
|
|
998
|
+
};
|
|
999
|
+
var mergeEntry = async (sourcePath, destPath, overwrite) => {
|
|
1000
|
+
const sourceStat = await (0, import_promises.lstat)(sourcePath);
|
|
1001
|
+
const destStat = await tryLstat(destPath);
|
|
1002
|
+
if (sourceStat.isDirectory()) {
|
|
1003
|
+
if (destStat && !destStat.isDirectory()) {
|
|
1004
|
+
if (!overwrite) {
|
|
1005
|
+
await (0, import_promises.rm)(sourcePath, { recursive: true, force: true });
|
|
1006
|
+
return;
|
|
1007
|
+
}
|
|
1008
|
+
await (0, import_promises.rm)(destPath, { recursive: true, force: true });
|
|
1009
|
+
}
|
|
1010
|
+
await (0, import_promises.mkdir)(destPath, { recursive: true });
|
|
1011
|
+
const children = await (0, import_promises.readdir)(sourcePath);
|
|
1012
|
+
for (const child of children) {
|
|
1013
|
+
await mergeEntry((0, import_node_path.join)(sourcePath, child), (0, import_node_path.join)(destPath, child), overwrite);
|
|
1014
|
+
}
|
|
1015
|
+
await (0, import_promises.rm)(sourcePath, { recursive: true, force: true });
|
|
1016
|
+
return;
|
|
1017
|
+
}
|
|
1018
|
+
if (destStat) {
|
|
1019
|
+
if (!overwrite) {
|
|
1020
|
+
await (0, import_promises.rm)(sourcePath, { recursive: true, force: true });
|
|
1021
|
+
return;
|
|
1022
|
+
}
|
|
1023
|
+
await (0, import_promises.rm)(destPath, { recursive: true, force: true });
|
|
1024
|
+
}
|
|
1025
|
+
try {
|
|
1026
|
+
await (0, import_promises.rename)(sourcePath, destPath);
|
|
1027
|
+
} catch {
|
|
1028
|
+
await (0, import_promises.copyFile)(sourcePath, destPath);
|
|
1029
|
+
await (0, import_promises.rm)(sourcePath, { recursive: true, force: true });
|
|
1030
|
+
}
|
|
1031
|
+
};
|
|
1032
|
+
var moveFiles = async (sourceDir, destDir, overwrite = false) => {
|
|
1033
|
+
await (0, import_promises.mkdir)(destDir, { recursive: true });
|
|
1034
|
+
const files = await (0, import_promises.readdir)(sourceDir);
|
|
1035
|
+
for (const file of files) {
|
|
1036
|
+
const sourceFile = (0, import_node_path.join)(sourceDir, file);
|
|
1037
|
+
const destFile = (0, import_node_path.join)(destDir, file);
|
|
1038
|
+
if (!overwrite) {
|
|
1039
|
+
try {
|
|
1040
|
+
await (0, import_promises.access)(destFile, import_node_fs.constants.F_OK);
|
|
1041
|
+
await (0, import_promises.rm)(sourceFile, { recursive: true, force: true });
|
|
1042
|
+
continue;
|
|
1043
|
+
} catch {
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
await mergeEntry(sourceFile, destFile, overwrite);
|
|
1047
|
+
}
|
|
1048
|
+
};
|
|
1049
|
+
|
|
1050
|
+
// src/project/projectTools.ts
|
|
1051
|
+
var ProjectTools = class {
|
|
1052
|
+
async extractTemplate(zipPath, workspacePath, tempExtractDir, input) {
|
|
1053
|
+
await unzipFile(zipPath, tempExtractDir);
|
|
1054
|
+
let sourceDir = path2.join(tempExtractDir, input.extractedDirName);
|
|
1055
|
+
let actualDirName = input.extractedDirName;
|
|
1056
|
+
if (!await this.pathExists(sourceDir)) {
|
|
1057
|
+
const entries = await fs2.readdir(tempExtractDir, { withFileTypes: true });
|
|
1058
|
+
const directories = entries.filter(
|
|
1059
|
+
(entry) => entry.isDirectory() && !entry.name.startsWith(".")
|
|
1060
|
+
);
|
|
1061
|
+
const selectedDirectory = await this.selectExtractedDirectory(
|
|
1062
|
+
directories.map((directory) => directory.name),
|
|
1063
|
+
tempExtractDir,
|
|
1064
|
+
input.projectFilePattern,
|
|
1065
|
+
input.extractedDirName
|
|
1066
|
+
);
|
|
1067
|
+
if (selectedDirectory) {
|
|
1068
|
+
actualDirName = selectedDirectory;
|
|
1069
|
+
sourceDir = path2.join(tempExtractDir, actualDirName);
|
|
1070
|
+
} else if (directories.length === 0) {
|
|
1071
|
+
throw new Error(
|
|
1072
|
+
`No directory found after extraction. Expected directory: ${input.extractedDirName}`
|
|
1073
|
+
);
|
|
1074
|
+
} else {
|
|
1075
|
+
throw new Error(
|
|
1076
|
+
`Multiple directories found after extraction: ${directories.map((directory) => directory.name).join(", ")}. Expected: ${input.extractedDirName}`
|
|
1077
|
+
);
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
if (!await this.pathExists(sourceDir)) {
|
|
1081
|
+
throw new Error(`Source directory not found: ${sourceDir}`);
|
|
1082
|
+
}
|
|
1083
|
+
await moveFiles(sourceDir, workspacePath, true);
|
|
1084
|
+
return {
|
|
1085
|
+
actualDirName
|
|
1086
|
+
};
|
|
1087
|
+
}
|
|
1088
|
+
async installDependencies(workspacePath, command) {
|
|
1089
|
+
if (!command) {
|
|
1090
|
+
throw (0, import_devtools_protocol7.createServicemeError)("invalid_params", "Expected install command.");
|
|
1091
|
+
}
|
|
1092
|
+
await runCommand(command, {
|
|
1093
|
+
cwd: workspacePath,
|
|
1094
|
+
shell: true
|
|
1095
|
+
});
|
|
1096
|
+
return {
|
|
1097
|
+
installed: true,
|
|
1098
|
+
command
|
|
1099
|
+
};
|
|
1100
|
+
}
|
|
1101
|
+
async makeScriptsExecutable(workspacePath) {
|
|
1102
|
+
const isWindows = process.platform === "win32";
|
|
1103
|
+
const scripts = await this.findScripts(
|
|
1104
|
+
workspacePath,
|
|
1105
|
+
isWindows ? [".ps1", ".bat"] : [".sh"]
|
|
1106
|
+
);
|
|
1107
|
+
let updatedCount = 0;
|
|
1108
|
+
if (isWindows) {
|
|
1109
|
+
for (const scriptPath of scripts.filter((script) => script.endsWith(".ps1"))) {
|
|
1110
|
+
try {
|
|
1111
|
+
await runCommand(`powershell -Command "Unblock-File -Path '${scriptPath}'"`, {
|
|
1112
|
+
cwd: workspacePath,
|
|
1113
|
+
shell: true
|
|
1114
|
+
});
|
|
1115
|
+
updatedCount += 1;
|
|
1116
|
+
} catch {
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
} else {
|
|
1120
|
+
for (const scriptPath of scripts) {
|
|
1121
|
+
try {
|
|
1122
|
+
await fs2.chmod(scriptPath, 493);
|
|
1123
|
+
updatedCount += 1;
|
|
1124
|
+
} catch {
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
return {
|
|
1129
|
+
processedCount: scripts.length,
|
|
1130
|
+
updatedCount,
|
|
1131
|
+
platform: process.platform,
|
|
1132
|
+
scripts
|
|
1133
|
+
};
|
|
1134
|
+
}
|
|
1135
|
+
async initializeGit(workspacePath) {
|
|
1136
|
+
const command = "git init";
|
|
1137
|
+
await runCommand(command, {
|
|
1138
|
+
cwd: workspacePath,
|
|
1139
|
+
shell: true
|
|
1140
|
+
});
|
|
1141
|
+
return {
|
|
1142
|
+
initialized: true,
|
|
1143
|
+
command
|
|
1144
|
+
};
|
|
1145
|
+
}
|
|
1146
|
+
async runScaffoldPrune(workspacePath, preset) {
|
|
1147
|
+
if (!preset) {
|
|
1148
|
+
throw (0, import_devtools_protocol7.createServicemeError)("invalid_params", "Expected scaffold preset.");
|
|
1149
|
+
}
|
|
1150
|
+
const commands = [
|
|
1151
|
+
`pnpm run scaffold:prune -- --preset ${preset} --project-root .`,
|
|
1152
|
+
`pnpm run scaffold:init-metadata -- --preset ${preset} --project-root . --force`
|
|
1153
|
+
];
|
|
1154
|
+
for (const command of commands) {
|
|
1155
|
+
await runCommand(command, {
|
|
1156
|
+
cwd: workspacePath,
|
|
1157
|
+
shell: true
|
|
1158
|
+
});
|
|
1159
|
+
}
|
|
1160
|
+
return {
|
|
1161
|
+
applied: true,
|
|
1162
|
+
preset,
|
|
1163
|
+
commands
|
|
1164
|
+
};
|
|
1165
|
+
}
|
|
1166
|
+
async findScripts(dir, extensions) {
|
|
1167
|
+
const results = [];
|
|
1168
|
+
let entries;
|
|
1169
|
+
try {
|
|
1170
|
+
entries = await fs2.readdir(dir, { withFileTypes: true });
|
|
1171
|
+
} catch {
|
|
1172
|
+
return results;
|
|
1173
|
+
}
|
|
1174
|
+
for (const entry of entries) {
|
|
1175
|
+
const fullPath = path2.join(dir, entry.name);
|
|
1176
|
+
if (entry.isDirectory() && entry.name !== "node_modules" && !entry.name.startsWith(".")) {
|
|
1177
|
+
results.push(...await this.findScripts(fullPath, extensions));
|
|
1178
|
+
} else if (entry.isFile() && extensions.some((ext) => entry.name.endsWith(ext))) {
|
|
1179
|
+
results.push(fullPath);
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
return results;
|
|
1183
|
+
}
|
|
1184
|
+
async pathExists(targetPath) {
|
|
1185
|
+
try {
|
|
1186
|
+
await fs2.access(targetPath);
|
|
1187
|
+
return true;
|
|
1188
|
+
} catch {
|
|
1189
|
+
return false;
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
async selectExtractedDirectory(directoryNames, tempExtractDir, projectFilePattern, expectedDirName) {
|
|
1193
|
+
if (directoryNames.length === 1) {
|
|
1194
|
+
return directoryNames[0] ?? null;
|
|
1195
|
+
}
|
|
1196
|
+
const exactMatch = directoryNames.find((directoryName) => directoryName === expectedDirName);
|
|
1197
|
+
if (exactMatch) {
|
|
1198
|
+
return exactMatch;
|
|
1199
|
+
}
|
|
1200
|
+
const matches = [];
|
|
1201
|
+
for (const directoryName of directoryNames) {
|
|
1202
|
+
if (await this.directoryMatchesProjectPattern(
|
|
1203
|
+
path2.join(tempExtractDir, directoryName),
|
|
1204
|
+
projectFilePattern
|
|
1205
|
+
)) {
|
|
1206
|
+
matches.push(directoryName);
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
if (matches.length === 1) {
|
|
1210
|
+
return matches[0] ?? null;
|
|
1211
|
+
}
|
|
1212
|
+
return null;
|
|
1213
|
+
}
|
|
1214
|
+
async directoryMatchesProjectPattern(directoryPath, projectFilePattern) {
|
|
1215
|
+
const entries = await fs2.readdir(directoryPath);
|
|
1216
|
+
if (projectFilePattern.includes("*")) {
|
|
1217
|
+
const regex = new RegExp(`^${projectFilePattern.replace("*", ".*")}$`);
|
|
1218
|
+
return entries.some((entry) => regex.test(entry));
|
|
1219
|
+
}
|
|
1220
|
+
return entries.includes(projectFilePattern);
|
|
1221
|
+
}
|
|
1222
|
+
};
|
|
1223
|
+
function createProjectTools() {
|
|
1224
|
+
return new ProjectTools();
|
|
1225
|
+
}
|
|
1226
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
1227
|
+
0 && (module.exports = {
|
|
1228
|
+
EnvironmentInspector,
|
|
1229
|
+
ImageTools,
|
|
1230
|
+
OpenCodeManager,
|
|
1231
|
+
ProjectTools,
|
|
1232
|
+
createConsoleLogger,
|
|
1233
|
+
createImageTools,
|
|
1234
|
+
createJsonTools,
|
|
1235
|
+
createProjectTools,
|
|
1236
|
+
moveFiles,
|
|
1237
|
+
noopLogger,
|
|
1238
|
+
unzipFile
|
|
1239
|
+
});
|