@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/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
+ };