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