@serviceme/devtools-core 0.1.5 → 0.1.7

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 CHANGED
@@ -5,10 +5,316 @@ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require
5
5
  throw Error('Dynamic require of "' + x + '" is not supported');
6
6
  });
7
7
 
8
+ // src/agents/AgentCatalogClient.ts
9
+ import { createServicemeError } from "@serviceme/devtools-protocol";
10
+ var AgentCatalogClient = class {
11
+ constructor(options = {}) {
12
+ this.fetchImpl = options.fetchImpl ?? fetch;
13
+ this.baseUrl = options.baseUrl;
14
+ }
15
+ async getCatalog() {
16
+ if (!this.baseUrl) {
17
+ return {
18
+ agents: [],
19
+ fetchedAt: (/* @__PURE__ */ new Date()).toISOString()
20
+ };
21
+ }
22
+ const response = await this.fetchImpl(
23
+ `${this.baseUrl}/api/v1/marketplace/agents`
24
+ );
25
+ if (!response.ok) {
26
+ throw new Error(`Failed to fetch agents catalog: ${response.status}`);
27
+ }
28
+ const data = await response.json();
29
+ return {
30
+ agents: data.agents ?? [],
31
+ fetchedAt: (/* @__PURE__ */ new Date()).toISOString()
32
+ };
33
+ }
34
+ async downloadAgent(remoteId) {
35
+ if (!this.baseUrl) {
36
+ throw createServicemeError(
37
+ "workspace_not_found",
38
+ "Agent catalog baseUrl is not configured."
39
+ );
40
+ }
41
+ const response = await this.fetchImpl(
42
+ `${this.baseUrl}/api/v1/marketplace/agents/download/${remoteId}`
43
+ );
44
+ if (!response.ok) {
45
+ if (response.status === 404) {
46
+ throw createServicemeError(
47
+ "not_found",
48
+ `Agent '${remoteId}' not found`
49
+ );
50
+ }
51
+ throw new Error(
52
+ `Failed to download agent ${remoteId}: ${response.status}`
53
+ );
54
+ }
55
+ const payload = await response.json();
56
+ return payload.data?.files ?? [];
57
+ }
58
+ };
59
+
60
+ // src/permissions/agent-permissions.ts
61
+ var TOOL_RISK_MAP = {
62
+ shell: "high",
63
+ terminal: "high",
64
+ run_in_terminal: "high",
65
+ execution_subagent: "high",
66
+ filesystem: "medium",
67
+ fetch: "medium",
68
+ fetch_webpage: "medium",
69
+ create_file: "medium",
70
+ replace_string_in_file: "medium",
71
+ multi_replace_string_in_file: "medium",
72
+ read_file: "low",
73
+ search: "low",
74
+ grep_search: "low",
75
+ file_search: "low",
76
+ semantic_search: "low",
77
+ list_dir: "low"
78
+ };
79
+ var FRONTMATTER_REGEX = /^---\r?\n([\s\S]*?)\r?\n---/;
80
+ var TOOLS_LINE_REGEX = /^tools:\s*$/m;
81
+ var TOOLS_INLINE_REGEX = /^tools:\s*\[([^\]]*)\]/m;
82
+ var LIST_ITEM_REGEX = /^\s*-\s+(.+)$/;
83
+ function parseAgentToolPermissions(content) {
84
+ const fmMatch = content.match(FRONTMATTER_REGEX);
85
+ if (!fmMatch?.[1]) return [];
86
+ const frontmatter = fmMatch[1];
87
+ const inlineMatch = frontmatter.match(TOOLS_INLINE_REGEX);
88
+ if (inlineMatch?.[1] != null) {
89
+ const raw = inlineMatch[1];
90
+ return raw.split(",").map((t) => t.trim()).filter(Boolean).map((tool) => ({
91
+ tool,
92
+ riskLevel: TOOL_RISK_MAP[tool] ?? "medium"
93
+ }));
94
+ }
95
+ const blockMatch = frontmatter.match(TOOLS_LINE_REGEX);
96
+ if (!blockMatch?.[0]) return [];
97
+ const toolsStartIndex = frontmatter.indexOf(blockMatch[0]) + blockMatch[0].length;
98
+ const remaining = frontmatter.slice(toolsStartIndex);
99
+ const lines = remaining.split(/\r?\n/);
100
+ const tools = [];
101
+ for (const line of lines) {
102
+ const itemMatch = line.match(LIST_ITEM_REGEX);
103
+ if (itemMatch?.[1]) {
104
+ const tool = itemMatch[1].trim();
105
+ tools.push({ tool, riskLevel: TOOL_RISK_MAP[tool] ?? "medium" });
106
+ } else if (line.trim() !== "" && !line.startsWith(" ") && !line.startsWith(" ")) {
107
+ break;
108
+ }
109
+ }
110
+ return tools;
111
+ }
112
+
113
+ // src/agents/AgentReconciler.ts
114
+ var AgentReconciler = class {
115
+ constructor(deps) {
116
+ this.deps = deps;
117
+ }
118
+ async mutate(request) {
119
+ if (request.targetScope !== "workspace" && request.targetScope !== "user") {
120
+ throw new Error(`Invalid target scope: ${String(request.targetScope)}`);
121
+ }
122
+ if (request.action === "uninstall" || request.action === "move" || request.action === "removeExternal") {
123
+ return {
124
+ status: "success",
125
+ changed: true,
126
+ message: `Agent ${request.action} completed.`
127
+ };
128
+ }
129
+ if (request.action !== "install") {
130
+ return {
131
+ status: "blocked",
132
+ changed: false,
133
+ message: `Agent action is not supported by bridge reconciler: ${request.action}`
134
+ };
135
+ }
136
+ const catalog = await this.deps.catalogClient.getCatalog();
137
+ const remoteAgent = catalog.agents.find(
138
+ (agent) => this.deps.agentStore.normalizeRemoteAgentId(agent.id) === request.agentId
139
+ );
140
+ if (!remoteAgent) {
141
+ return {
142
+ status: "blocked",
143
+ changed: false,
144
+ message: "Agent not found in catalog."
145
+ };
146
+ }
147
+ if (!request.confirmed && this.hasHighRiskTool(remoteAgent.tools)) {
148
+ return {
149
+ status: "requires_confirmation",
150
+ changed: false,
151
+ message: "This agent uses high-risk tools that require confirmation.",
152
+ tools: remoteAgent.tools
153
+ };
154
+ }
155
+ return {
156
+ status: "success",
157
+ changed: true,
158
+ message: "Agent installed."
159
+ };
160
+ }
161
+ getPermissionSummary(agentId, agentName, content) {
162
+ const tools = parseAgentToolPermissions(content);
163
+ return {
164
+ agentId,
165
+ agentName,
166
+ tools,
167
+ highRiskCount: tools.filter((tool) => tool.riskLevel === "high").length,
168
+ mediumRiskCount: tools.filter((tool) => tool.riskLevel === "medium").length,
169
+ lowRiskCount: tools.filter((tool) => tool.riskLevel === "low").length
170
+ };
171
+ }
172
+ hasHighRiskTool(tools) {
173
+ return tools.some((tool) => TOOL_RISK_MAP[tool] === "high");
174
+ }
175
+ };
176
+
177
+ // src/agents/AgentStore.ts
178
+ import * as fs from "fs/promises";
179
+ import * as path from "path";
180
+ var WORKSPACE_AGENTS_ROOT_RELATIVE = ".github/agents";
181
+ var WORKSPACE_AGENTS_STATE_RELATIVE = ".github/.ms-devtools-agents.yml";
182
+ var SAFE_LOCAL_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
183
+ function assertSafeLocalAgentId(agentId) {
184
+ if (typeof agentId !== "string" || agentId.length === 0 || agentId === "." || agentId === ".." || agentId.includes("/") || agentId.includes("\\") || !SAFE_LOCAL_ID_PATTERN.test(agentId)) {
185
+ throw new Error(`Invalid agent id: ${agentId}`);
186
+ }
187
+ return agentId;
188
+ }
189
+ var AgentStore = class {
190
+ constructor(options) {
191
+ this.workspacePath = options.workspacePath;
192
+ this.userAgentsRoot = options.userAgentsRoot;
193
+ this.fileSystem = options.fileSystem ?? fs;
194
+ this.schemaVersion = options.schemaVersion ?? 1;
195
+ }
196
+ normalizeRemoteAgentId(remoteId) {
197
+ if (remoteId.startsWith("official/")) {
198
+ return assertSafeLocalAgentId(remoteId.slice("official/".length));
199
+ }
200
+ if (remoteId.startsWith("community/")) {
201
+ const lastSlash = remoteId.lastIndexOf("/");
202
+ return assertSafeLocalAgentId(remoteId.slice(lastSlash + 1));
203
+ }
204
+ return assertSafeLocalAgentId(remoteId);
205
+ }
206
+ getWorkspaceAgentsRootPath() {
207
+ return WORKSPACE_AGENTS_ROOT_RELATIVE;
208
+ }
209
+ getWorkspaceStateFilePath() {
210
+ return WORKSPACE_AGENTS_STATE_RELATIVE;
211
+ }
212
+ getUserAgentsRootPath() {
213
+ return this.userAgentsRoot;
214
+ }
215
+ async listWorkspaceAgentIds() {
216
+ return this.listAgentIds(
217
+ path.join(this.workspacePath, WORKSPACE_AGENTS_ROOT_RELATIVE)
218
+ );
219
+ }
220
+ async listUserAgentIds() {
221
+ return this.listAgentIds(this.userAgentsRoot);
222
+ }
223
+ async readState() {
224
+ try {
225
+ const raw = await this.fileSystem.readFile(
226
+ path.join(this.workspacePath, WORKSPACE_AGENTS_STATE_RELATIVE),
227
+ "utf-8"
228
+ );
229
+ const parsed = JSON.parse(raw);
230
+ if (typeof parsed.schemaVersion !== "number" || !Array.isArray(parsed.installedAgents)) {
231
+ return null;
232
+ }
233
+ return parsed;
234
+ } catch {
235
+ return null;
236
+ }
237
+ }
238
+ async writeState(state) {
239
+ const statePath = path.join(
240
+ this.workspacePath,
241
+ WORKSPACE_AGENTS_STATE_RELATIVE
242
+ );
243
+ await this.fileSystem.mkdir(path.dirname(statePath), { recursive: true });
244
+ await this.fileSystem.writeFile(
245
+ statePath,
246
+ JSON.stringify(state, null, 2),
247
+ "utf-8"
248
+ );
249
+ }
250
+ async addInstalledAgent(entry) {
251
+ const state = await this.readState() ?? {
252
+ schemaVersion: this.schemaVersion,
253
+ installedAgents: []
254
+ };
255
+ state.installedAgents = state.installedAgents.filter(
256
+ (agent) => agent.id !== entry.id
257
+ );
258
+ state.installedAgents.push(entry);
259
+ await this.writeState(state);
260
+ }
261
+ async removeInstalledAgent(agentId) {
262
+ const state = await this.readState();
263
+ if (!state) {
264
+ return;
265
+ }
266
+ state.installedAgents = state.installedAgents.filter(
267
+ (agent) => agent.id !== agentId
268
+ );
269
+ await this.writeState(state);
270
+ }
271
+ async writeAgentFiles(agentId, scope, files) {
272
+ const root = scope === "workspace" ? path.join(this.workspacePath, WORKSPACE_AGENTS_ROOT_RELATIVE) : this.userAgentsRoot;
273
+ const firstFile = files[0];
274
+ const isSingleFlatFile = files.length === 1 && firstFile !== void 0 && firstFile.path === `${agentId}.agent.md` && !firstFile.path.includes("/");
275
+ const targetDir = isSingleFlatFile ? root : path.join(root, agentId);
276
+ await this.fileSystem.mkdir(targetDir, { recursive: true });
277
+ for (const file of files) {
278
+ const filePath = path.join(targetDir, file.path);
279
+ await this.fileSystem.mkdir(path.dirname(filePath), { recursive: true });
280
+ await this.fileSystem.writeFile(filePath, file.content, "utf-8");
281
+ if (file.executable) {
282
+ try {
283
+ await this.fileSystem.chmod(filePath, 493);
284
+ } catch {
285
+ }
286
+ }
287
+ }
288
+ }
289
+ async listAgentIds(dir) {
290
+ try {
291
+ const entries = await this.fileSystem.readdir(dir, {
292
+ withFileTypes: true
293
+ });
294
+ const ids = [];
295
+ for (const entry of entries) {
296
+ if (entry.name.startsWith(".")) {
297
+ continue;
298
+ }
299
+ if (entry.isDirectory()) {
300
+ ids.push(entry.name);
301
+ continue;
302
+ }
303
+ if (entry.isFile() && entry.name.endsWith(".agent.md")) {
304
+ ids.push(entry.name.replace(/\.agent\.md$/, ""));
305
+ }
306
+ }
307
+ return ids.sort();
308
+ } catch {
309
+ return [];
310
+ }
311
+ }
312
+ };
313
+
8
314
  // src/copilot/doctor.ts
9
315
  import {
10
316
  COPILOT_ERROR_CODES,
11
- createServicemeError
317
+ createServicemeError as createServicemeError2
12
318
  } from "@serviceme/devtools-protocol";
13
319
 
14
320
  // src/process/runCommand.ts
@@ -153,13 +459,13 @@ async function copilotDoctor() {
153
459
  return { installed: true, version, authenticated };
154
460
  }
155
461
  function createCopilotNotInstalledError() {
156
- return createServicemeError(
462
+ return createServicemeError2(
157
463
  COPILOT_ERROR_CODES.NOT_INSTALLED,
158
464
  "GitHub Copilot CLI is not installed. Visit https://docs.github.com/en/copilot/using-github-copilot/using-github-copilot-in-the-command-line to install."
159
465
  );
160
466
  }
161
467
  function createCopilotAuthRequiredError() {
162
- return createServicemeError(
468
+ return createServicemeError2(
163
469
  COPILOT_ERROR_CODES.AUTH_REQUIRED,
164
470
  "GitHub Copilot CLI requires authentication. Run `copilot auth login` to sign in."
165
471
  );
@@ -168,7 +474,7 @@ function createCopilotAuthRequiredError() {
168
474
  // src/copilot/prompt.ts
169
475
  import {
170
476
  COPILOT_ERROR_CODES as COPILOT_ERROR_CODES2,
171
- createServicemeError as createServicemeError2
477
+ createServicemeError as createServicemeError3
172
478
  } from "@serviceme/devtools-protocol";
173
479
  var COPILOT_COMMAND2 = "copilot";
174
480
  var DEFAULT_TIMEOUT_MS = 12e4;
@@ -203,7 +509,7 @@ async function copilotPrompt(options) {
203
509
  });
204
510
  const output = stripAnsi(result.stdout);
205
511
  if (output.includes("I'm sorry, but I cannot assist") && result.stderr?.includes("Stream completed without a response")) {
206
- throw createServicemeError2(
512
+ throw createServicemeError3(
207
513
  COPILOT_ERROR_CODES2.AUTH_REQUIRED,
208
514
  "Copilot CLI returned a refusal response. This typically indicates an expired authentication token. Run `gh auth login` to re-authenticate.",
209
515
  { exitCode: 0, output, stderr: result.stderr }
@@ -213,7 +519,7 @@ async function copilotPrompt(options) {
213
519
  } catch (error) {
214
520
  const err = error;
215
521
  if (err.code === "ETIMEDOUT") {
216
- throw createServicemeError2(
522
+ throw createServicemeError3(
217
523
  COPILOT_ERROR_CODES2.TIMEOUT,
218
524
  `Copilot prompt timed out after ${String(timeout)}ms.`
219
525
  );
@@ -222,7 +528,7 @@ async function copilotPrompt(options) {
222
528
  const output = stripAnsi(err.stdout ?? "");
223
529
  const stderr = err.stderr ?? err.message ?? "";
224
530
  if (output.includes("I'm sorry, but I cannot assist") || stderr.includes("Stream completed without a response")) {
225
- throw createServicemeError2(
531
+ throw createServicemeError3(
226
532
  COPILOT_ERROR_CODES2.AUTH_REQUIRED,
227
533
  "Copilot CLI returned a refusal response. This typically indicates an expired authentication token. Run `gh auth login` to re-authenticate.",
228
534
  { exitCode, output, stderr }
@@ -231,7 +537,7 @@ async function copilotPrompt(options) {
231
537
  if (exitCode !== 0 && output) {
232
538
  return { output, exitCode };
233
539
  }
234
- throw createServicemeError2(
540
+ throw createServicemeError3(
235
541
  COPILOT_ERROR_CODES2.EXECUTION_FAILED,
236
542
  stderr || `Copilot exited with code ${String(exitCode)}.`,
237
543
  { exitCode, stderr }
@@ -241,14 +547,20 @@ async function copilotPrompt(options) {
241
547
 
242
548
  // src/env/environmentInspector.ts
243
549
  import {
244
- createServicemeError as createServicemeError3,
550
+ createServicemeError as createServicemeError4,
245
551
  KNOWN_ENVIRONMENT_TOOLS
246
552
  } from "@serviceme/devtools-protocol";
247
553
  var DEFAULT_TOOL_CHECK_TIMEOUT_MS = 5e3;
248
554
  var TOOL_CHECK_TIMEOUT_MS = {
249
555
  nuget: 12e3,
250
556
  nvm: 8e3,
251
- dotnet: 8e3
557
+ dotnet: 8e3,
558
+ // nvm4w/npm shims and globally-installed .cmd tools (pnpm, nrm) are slow to
559
+ // resolve on cold PATHs. Give them extra headroom so the version probe
560
+ // doesn't fall through to the generic 5s default.
561
+ npm: 15e3,
562
+ pnpm: 15e3,
563
+ nrm: 15e3
252
564
  };
253
565
  var ERROR_CODE_NOT_FOUND = 127;
254
566
  var ERROR_CODE_TIMEOUT = "ETIMEDOUT";
@@ -263,7 +575,7 @@ var EnvironmentInspector = class {
263
575
  }
264
576
  async checkTool(toolName) {
265
577
  if (!/^[a-zA-Z0-9-]+$/.test(toolName)) {
266
- throw createServicemeError3("invalid_params", "Invalid tool name.");
578
+ throw createServicemeError4("invalid_params", "Invalid tool name.");
267
579
  }
268
580
  try {
269
581
  if (toolName === "nvm") {
@@ -295,8 +607,29 @@ var EnvironmentInspector = class {
295
607
  return void 0;
296
608
  }
297
609
  }
610
+ /**
611
+ * On Windows, prefer the `.cmd` shim for npm/pnpm/nrm. nvm4w registers
612
+ * BOTH an extensionless entry (pointing to node.exe) and a `<tool>.cmd`
613
+ * wrapper; `where` returns them in that order, and the extensionless one
614
+ * would just print node's own version.
615
+ */
616
+ async getToolShimPath(toolName) {
617
+ try {
618
+ const result = await runCommand("where", {
619
+ args: [toolName],
620
+ timeoutMs: this.getToolTimeout(toolName)
621
+ });
622
+ const candidates = result.stdout.split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0);
623
+ const cmdShim = candidates.find(
624
+ (line) => line.toLowerCase().endsWith(".cmd")
625
+ );
626
+ return cmdShim ?? candidates[0];
627
+ } catch {
628
+ return void 0;
629
+ }
630
+ }
298
631
  async getToolVersion(toolName) {
299
- const invocation = this.getVersionInvocation(toolName);
632
+ const invocation = await this.getVersionInvocation(toolName);
300
633
  const result = await runCommand(invocation.command, {
301
634
  args: invocation.args,
302
635
  timeoutMs: this.getToolTimeout(toolName)
@@ -371,12 +704,19 @@ var EnvironmentInspector = class {
371
704
  return this.handleToolCheckError(error);
372
705
  }
373
706
  }
374
- getVersionInvocation(toolName) {
707
+ async getVersionInvocation(toolName) {
375
708
  const isWindows = process.platform === "win32";
376
709
  if (isWindows && ["npm", "pnpm", "nrm"].includes(toolName)) {
710
+ const shim = await this.getToolShimPath(toolName);
711
+ if (shim) {
712
+ return {
713
+ command: "cmd.exe",
714
+ args: ["/c", shim, "--version"]
715
+ };
716
+ }
377
717
  return {
378
718
  command: "cmd.exe",
379
- args: ["/d", "/s", "/c", `${toolName} --version`]
719
+ args: ["/c", toolName, "--version"]
380
720
  };
381
721
  }
382
722
  const commands = {
@@ -419,7 +759,7 @@ var EnvironmentInspector = class {
419
759
  const execError = error;
420
760
  const message = execError.message || String(error);
421
761
  const code = execError.code;
422
- const isNotFound = message.includes("command not found") || message.includes("not recognized") || message.includes("ENOENT") || code === "ENOENT" || code === ERROR_CODE_NOT_FOUND;
762
+ const isNotFound = message.includes("command not found") || message.includes("not recognized") || message.includes("ENOENT") || message.includes("EACCES") || code === "ENOENT" || code === "EACCES" || code === ERROR_CODE_NOT_FOUND;
423
763
  const isTimeout = code === ERROR_CODE_TIMEOUT || message.toLowerCase().includes("timed out");
424
764
  return {
425
765
  installed: false,
@@ -429,10 +769,10 @@ var EnvironmentInspector = class {
429
769
  };
430
770
 
431
771
  // src/image/imageTools.ts
432
- import * as fs from "fs/promises";
433
- import * as path from "path";
772
+ import * as fs2 from "fs/promises";
773
+ import * as path2 from "path";
434
774
  import {
435
- createServicemeError as createServicemeError4
775
+ createServicemeError as createServicemeError5
436
776
  } from "@serviceme/devtools-protocol";
437
777
  var SUPPORTED_FORMATS = [
438
778
  ".jpg",
@@ -453,7 +793,7 @@ var ImageTools = class {
453
793
  if (!await this.pathExists(filePath)) {
454
794
  return { valid: false };
455
795
  }
456
- if (!SUPPORTED_FORMATS.includes(path.extname(filePath).toLowerCase())) {
796
+ if (!SUPPORTED_FORMATS.includes(path2.extname(filePath).toLowerCase())) {
457
797
  return { valid: false };
458
798
  }
459
799
  await this.getInfo(filePath, sharpModulePath);
@@ -465,7 +805,7 @@ var ImageTools = class {
465
805
  async getInfo(imagePath, sharpModulePath) {
466
806
  const sharp = this.loadSharp(sharpModulePath);
467
807
  const metadata = await sharp(imagePath).metadata();
468
- const stats = await fs.stat(imagePath);
808
+ const stats = await fs2.stat(imagePath);
469
809
  return {
470
810
  width: metadata.width ?? 0,
471
811
  height: metadata.height ?? 0,
@@ -477,12 +817,12 @@ var ImageTools = class {
477
817
  async compress(imagePath, options) {
478
818
  const validation = await this.validate(imagePath, options.sharpModulePath);
479
819
  if (!validation.valid) {
480
- throw createServicemeError4(
820
+ throw createServicemeError5(
481
821
  "invalid_params",
482
822
  `Invalid image file: ${imagePath}`
483
823
  );
484
824
  }
485
- const originalStats = await fs.stat(imagePath);
825
+ const originalStats = await fs2.stat(imagePath);
486
826
  const originalSize = originalStats.size;
487
827
  const outputPath = this.getOutputPath(imagePath, options);
488
828
  const compressedBuffer = await this.compressWithSharp(imagePath, options);
@@ -497,7 +837,7 @@ var ImageTools = class {
497
837
  outputPath: imagePath
498
838
  };
499
839
  }
500
- await fs.writeFile(outputPath, compressedBuffer);
840
+ await fs2.writeFile(outputPath, compressedBuffer);
501
841
  return {
502
842
  originalSize,
503
843
  compressedSize,
@@ -508,7 +848,7 @@ var ImageTools = class {
508
848
  async compressWithSharp(imagePath, options) {
509
849
  const sharp = this.loadSharp(options.sharpModulePath);
510
850
  let pipeline = sharp(imagePath);
511
- switch (options.format ?? path.extname(imagePath).toLowerCase().slice(1)) {
851
+ switch (options.format ?? path2.extname(imagePath).toLowerCase().slice(1)) {
512
852
  case "jpg":
513
853
  case "jpeg":
514
854
  pipeline = pipeline.jpeg({ quality: options.quality });
@@ -534,14 +874,14 @@ var ImageTools = class {
534
874
  if (options.replaceOriginImage) {
535
875
  return inputPath;
536
876
  }
537
- const dir = path.dirname(inputPath);
538
- const ext = path.extname(inputPath);
539
- const name = path.basename(inputPath, ext);
540
- return path.join(dir, `${name}_compressed${ext}`);
877
+ const dir = path2.dirname(inputPath);
878
+ const ext = path2.extname(inputPath);
879
+ const name = path2.basename(inputPath, ext);
880
+ return path2.join(dir, `${name}_compressed${ext}`);
541
881
  }
542
882
  async pathExists(targetPath) {
543
883
  try {
544
- await fs.access(targetPath);
884
+ await fs2.access(targetPath);
545
885
  return true;
546
886
  } catch {
547
887
  return false;
@@ -551,7 +891,7 @@ var ImageTools = class {
551
891
  try {
552
892
  return sharpModulePath ? __require(sharpModulePath) : __require("sharp");
553
893
  } catch (error) {
554
- throw createServicemeError4(
894
+ throw createServicemeError5(
555
895
  "internal_error",
556
896
  `sharp is required for image operations: ${error instanceof Error ? error.message : String(error)}`
557
897
  );
@@ -564,7 +904,7 @@ function createImageTools() {
564
904
 
565
905
  // src/json/jsonTools.ts
566
906
  import {
567
- createServicemeError as createServicemeError5,
907
+ createServicemeError as createServicemeError6,
568
908
  DEFAULT_JSON_SORT_OPTIONS
569
909
  } from "@serviceme/devtools-protocol";
570
910
  import { parse as parseCommentJson } from "comment-json";
@@ -608,7 +948,7 @@ function parseJson(text) {
608
948
  } catch {
609
949
  }
610
950
  }
611
- throw createServicemeError5("json_invalid_input", "Invalid JSON format.");
951
+ throw createServicemeError6("json_invalid_input", "Invalid JSON format.");
612
952
  }
613
953
  function sortObject(obj, options) {
614
954
  if (Array.isArray(obj)) {
@@ -700,10 +1040,10 @@ function createConsoleLogger(prefix = "serviceme") {
700
1040
  }
701
1041
 
702
1042
  // src/project/projectTools.ts
703
- import * as fs2 from "fs/promises";
704
- import * as path2 from "path";
1043
+ import * as fs3 from "fs/promises";
1044
+ import * as path3 from "path";
705
1045
  import {
706
- createServicemeError as createServicemeError6
1046
+ createServicemeError as createServicemeError7
707
1047
  } from "@serviceme/devtools-protocol";
708
1048
 
709
1049
  // src/utils/fileUtils.ts
@@ -717,7 +1057,7 @@ import {
717
1057
  rename,
718
1058
  rm
719
1059
  } from "fs/promises";
720
- import { dirname as dirname2, join as join2 } from "path";
1060
+ import { dirname as dirname3, join as join3 } from "path";
721
1061
  import { open } from "yauzl";
722
1062
  var unzipFile = (zipPath, dest) => {
723
1063
  return new Promise((resolve, reject) => {
@@ -730,12 +1070,12 @@ var unzipFile = (zipPath, dest) => {
730
1070
  zipfile.readEntry();
731
1071
  zipfile.on("entry", (entry) => {
732
1072
  if (/\/$/.test(entry.fileName)) {
733
- void mkdir(join2(dest, entry.fileName), { recursive: true }).then(() => {
1073
+ void mkdir(join3(dest, entry.fileName), { recursive: true }).then(() => {
734
1074
  zipfile.readEntry();
735
1075
  }).catch(reject);
736
1076
  } else {
737
- const outputPath = join2(dest, entry.fileName);
738
- void mkdir(dirname2(outputPath), { recursive: true }).then(() => {
1077
+ const outputPath = join3(dest, entry.fileName);
1078
+ void mkdir(dirname3(outputPath), { recursive: true }).then(() => {
739
1079
  zipfile.openReadStream(
740
1080
  entry,
741
1081
  (streamError, readStream) => {
@@ -788,8 +1128,8 @@ var mergeEntry = async (sourcePath, destPath, overwrite) => {
788
1128
  const children = await readdir(sourcePath);
789
1129
  for (const child of children) {
790
1130
  await mergeEntry(
791
- join2(sourcePath, child),
792
- join2(destPath, child),
1131
+ join3(sourcePath, child),
1132
+ join3(destPath, child),
793
1133
  overwrite
794
1134
  );
795
1135
  }
@@ -814,8 +1154,8 @@ var moveFiles = async (sourceDir, destDir, overwrite = false) => {
814
1154
  await mkdir(destDir, { recursive: true });
815
1155
  const files = await readdir(sourceDir);
816
1156
  for (const file of files) {
817
- const sourceFile = join2(sourceDir, file);
818
- const destFile = join2(destDir, file);
1157
+ const sourceFile = join3(sourceDir, file);
1158
+ const destFile = join3(destDir, file);
819
1159
  if (!overwrite) {
820
1160
  try {
821
1161
  await access2(destFile, constants.F_OK);
@@ -832,10 +1172,10 @@ var moveFiles = async (sourceDir, destDir, overwrite = false) => {
832
1172
  var ProjectTools = class {
833
1173
  async extractTemplate(zipPath, workspacePath, tempExtractDir, input) {
834
1174
  await unzipFile(zipPath, tempExtractDir);
835
- let sourceDir = path2.join(tempExtractDir, input.extractedDirName);
1175
+ let sourceDir = path3.join(tempExtractDir, input.extractedDirName);
836
1176
  let actualDirName = input.extractedDirName;
837
1177
  if (!await this.pathExists(sourceDir)) {
838
- const entries = await fs2.readdir(tempExtractDir, { withFileTypes: true });
1178
+ const entries = await fs3.readdir(tempExtractDir, { withFileTypes: true });
839
1179
  const directories = entries.filter(
840
1180
  (entry) => entry.isDirectory() && !entry.name.startsWith(".")
841
1181
  );
@@ -847,7 +1187,7 @@ var ProjectTools = class {
847
1187
  );
848
1188
  if (selectedDirectory) {
849
1189
  actualDirName = selectedDirectory;
850
- sourceDir = path2.join(tempExtractDir, actualDirName);
1190
+ sourceDir = path3.join(tempExtractDir, actualDirName);
851
1191
  } else if (directories.length === 0) {
852
1192
  throw new Error(
853
1193
  `No directory found after extraction. Expected directory: ${input.extractedDirName}`
@@ -868,7 +1208,7 @@ var ProjectTools = class {
868
1208
  }
869
1209
  async installDependencies(workspacePath, command) {
870
1210
  if (!command) {
871
- throw createServicemeError6("invalid_params", "Expected install command.");
1211
+ throw createServicemeError7("invalid_params", "Expected install command.");
872
1212
  }
873
1213
  await runCommand(command, {
874
1214
  cwd: workspacePath,
@@ -905,7 +1245,7 @@ var ProjectTools = class {
905
1245
  } else {
906
1246
  for (const scriptPath of scripts) {
907
1247
  try {
908
- await fs2.chmod(scriptPath, 493);
1248
+ await fs3.chmod(scriptPath, 493);
909
1249
  updatedCount += 1;
910
1250
  } catch {
911
1251
  }
@@ -931,13 +1271,16 @@ var ProjectTools = class {
931
1271
  }
932
1272
  async runScaffoldPrune(workspacePath, preset) {
933
1273
  if (!preset) {
934
- throw createServicemeError6("invalid_params", "Expected scaffold preset.");
1274
+ throw createServicemeError7("invalid_params", "Expected scaffold preset.");
935
1275
  }
936
- const commands = [
937
- `pnpm run scaffold:prune -- --preset ${preset} --project-root .`,
938
- `pnpm run scaffold:init-metadata -- --preset ${preset} --project-root . --force`
939
- ];
1276
+ await this.ensurePresetManifest(workspacePath, preset);
1277
+ const pruneCommand = `pnpm run scaffold:prune -- --preset ${preset} --project-root .`;
1278
+ const initMetadataCommand = `pnpm run scaffold:init-metadata -- --preset ${preset} --project-root . --force`;
1279
+ const commands = [pruneCommand, initMetadataCommand];
940
1280
  for (const command of commands) {
1281
+ if (command === initMetadataCommand) {
1282
+ await this.ensurePresetManifest(workspacePath, preset);
1283
+ }
941
1284
  await runCommand(command, {
942
1285
  cwd: workspacePath,
943
1286
  shell: true
@@ -949,16 +1292,54 @@ var ProjectTools = class {
949
1292
  commands
950
1293
  };
951
1294
  }
1295
+ async ensurePresetManifest(workspacePath, preset) {
1296
+ const presetManifestPath = path3.join(
1297
+ workspacePath,
1298
+ ".ms-scaffold",
1299
+ "presets",
1300
+ `${preset}.json`
1301
+ );
1302
+ if (await this.pathExists(presetManifestPath)) {
1303
+ return;
1304
+ }
1305
+ const projectModePath = path3.join(
1306
+ workspacePath,
1307
+ ".ms-scaffold",
1308
+ "project-mode.json"
1309
+ );
1310
+ if (!await this.pathExists(projectModePath)) {
1311
+ return;
1312
+ }
1313
+ const projectModeRaw = await fs3.readFile(projectModePath, "utf8");
1314
+ const projectMode = JSON.parse(projectModeRaw);
1315
+ const synthesizedPreset = {
1316
+ preset,
1317
+ selectedModules: Array.isArray(projectMode.selectedModules) ? projectMode.selectedModules : [],
1318
+ prunedModules: Array.isArray(projectMode.prunedModules) ? projectMode.prunedModules : [],
1319
+ exclude: [],
1320
+ recommendedFollowUps: [],
1321
+ managedFiles: [],
1322
+ mergeManagedFiles: [],
1323
+ userOwnedPaths: []
1324
+ };
1325
+ await fs3.mkdir(path3.dirname(presetManifestPath), { recursive: true });
1326
+ await fs3.writeFile(
1327
+ presetManifestPath,
1328
+ `${JSON.stringify(synthesizedPreset, null, 2)}
1329
+ `,
1330
+ "utf8"
1331
+ );
1332
+ }
952
1333
  async findScripts(dir, extensions) {
953
1334
  const results = [];
954
1335
  let entries;
955
1336
  try {
956
- entries = await fs2.readdir(dir, { withFileTypes: true });
1337
+ entries = await fs3.readdir(dir, { withFileTypes: true });
957
1338
  } catch {
958
1339
  return results;
959
1340
  }
960
1341
  for (const entry of entries) {
961
- const fullPath = path2.join(dir, entry.name);
1342
+ const fullPath = path3.join(dir, entry.name);
962
1343
  if (entry.isDirectory() && entry.name !== "node_modules" && !entry.name.startsWith(".")) {
963
1344
  results.push(...await this.findScripts(fullPath, extensions));
964
1345
  } else if (entry.isFile() && extensions.some((ext) => entry.name.endsWith(ext))) {
@@ -969,7 +1350,7 @@ var ProjectTools = class {
969
1350
  }
970
1351
  async pathExists(targetPath) {
971
1352
  try {
972
- await fs2.access(targetPath);
1353
+ await fs3.access(targetPath);
973
1354
  return true;
974
1355
  } catch {
975
1356
  return false;
@@ -988,7 +1369,7 @@ var ProjectTools = class {
988
1369
  const matches = [];
989
1370
  for (const directoryName of directoryNames) {
990
1371
  if (await this.directoryMatchesProjectPattern(
991
- path2.join(tempExtractDir, directoryName),
1372
+ path3.join(tempExtractDir, directoryName),
992
1373
  projectFilePattern
993
1374
  )) {
994
1375
  matches.push(directoryName);
@@ -1000,7 +1381,7 @@ var ProjectTools = class {
1000
1381
  return null;
1001
1382
  }
1002
1383
  async directoryMatchesProjectPattern(directoryPath, projectFilePattern) {
1003
- const entries = await fs2.readdir(directoryPath);
1384
+ const entries = await fs3.readdir(directoryPath);
1004
1385
  if (projectFilePattern.includes("*")) {
1005
1386
  const regex = new RegExp(`^${projectFilePattern.replace("*", ".*")}$`);
1006
1387
  return entries.some((entry) => regex.test(entry));
@@ -1013,17 +1394,17 @@ function createProjectTools() {
1013
1394
  }
1014
1395
 
1015
1396
  // src/scheduled-tasks/daemon/DaemonLogger.ts
1016
- import * as fs3 from "fs";
1017
- import * as path3 from "path";
1397
+ import * as fs4 from "fs";
1398
+ import * as path4 from "path";
1018
1399
  var CONFIG_DIR = ".serviceme";
1019
1400
  var LOG_FILE = "scheduler.log";
1020
1401
  var MAX_LOG_SIZE = 1024 * 1024;
1021
1402
  var DaemonLogger = class {
1022
1403
  constructor(workspacePath) {
1023
- this.logPath = path3.join(workspacePath, CONFIG_DIR, LOG_FILE);
1024
- const dir = path3.dirname(this.logPath);
1025
- if (!fs3.existsSync(dir)) {
1026
- fs3.mkdirSync(dir, { recursive: true });
1404
+ this.logPath = path4.join(workspacePath, CONFIG_DIR, LOG_FILE);
1405
+ const dir = path4.dirname(this.logPath);
1406
+ if (!fs4.existsSync(dir)) {
1407
+ fs4.mkdirSync(dir, { recursive: true });
1027
1408
  }
1028
1409
  }
1029
1410
  getLogPath() {
@@ -1034,16 +1415,16 @@ var DaemonLogger = class {
1034
1415
  const line = `[${ts}] [${level.toUpperCase()}] ${message}
1035
1416
  `;
1036
1417
  this.rotateIfNeeded();
1037
- fs3.appendFileSync(this.logPath, line, "utf-8");
1418
+ fs4.appendFileSync(this.logPath, line, "utf-8");
1038
1419
  }
1039
1420
  rotateIfNeeded() {
1040
1421
  try {
1041
- const stats = fs3.statSync(this.logPath);
1422
+ const stats = fs4.statSync(this.logPath);
1042
1423
  if (stats.size > MAX_LOG_SIZE) {
1043
- const content = fs3.readFileSync(this.logPath, "utf-8");
1424
+ const content = fs4.readFileSync(this.logPath, "utf-8");
1044
1425
  const halfIdx = content.indexOf("\n", Math.floor(content.length / 2));
1045
1426
  if (halfIdx > 0) {
1046
- fs3.writeFileSync(this.logPath, content.slice(halfIdx + 1), "utf-8");
1427
+ fs4.writeFileSync(this.logPath, content.slice(halfIdx + 1), "utf-8");
1047
1428
  }
1048
1429
  }
1049
1430
  } catch {
@@ -1052,33 +1433,33 @@ var DaemonLogger = class {
1052
1433
  };
1053
1434
 
1054
1435
  // src/scheduled-tasks/daemon/PidManager.ts
1055
- import * as fs4 from "fs";
1056
- import * as path4 from "path";
1436
+ import * as fs5 from "fs";
1437
+ import * as path5 from "path";
1057
1438
  var CONFIG_DIR2 = ".serviceme";
1058
1439
  var PID_FILE = "scheduler.pid";
1059
1440
  var PidManager = class {
1060
1441
  constructor(workspacePath) {
1061
- this.pidPath = path4.join(workspacePath, CONFIG_DIR2, PID_FILE);
1442
+ this.pidPath = path5.join(workspacePath, CONFIG_DIR2, PID_FILE);
1062
1443
  }
1063
1444
  getPidPath() {
1064
1445
  return this.pidPath;
1065
1446
  }
1066
1447
  writePid(pid) {
1067
- const dir = path4.dirname(this.pidPath);
1068
- if (!fs4.existsSync(dir)) {
1069
- fs4.mkdirSync(dir, { recursive: true });
1448
+ const dir = path5.dirname(this.pidPath);
1449
+ if (!fs5.existsSync(dir)) {
1450
+ fs5.mkdirSync(dir, { recursive: true });
1070
1451
  }
1071
- fs4.writeFileSync(this.pidPath, String(pid), "utf-8");
1452
+ fs5.writeFileSync(this.pidPath, String(pid), "utf-8");
1072
1453
  }
1073
1454
  readPid() {
1074
- if (!fs4.existsSync(this.pidPath)) return null;
1075
- const raw = fs4.readFileSync(this.pidPath, "utf-8").trim();
1455
+ if (!fs5.existsSync(this.pidPath)) return null;
1456
+ const raw = fs5.readFileSync(this.pidPath, "utf-8").trim();
1076
1457
  const pid = Number.parseInt(raw, 10);
1077
1458
  return Number.isNaN(pid) ? null : pid;
1078
1459
  }
1079
1460
  removePid() {
1080
- if (fs4.existsSync(this.pidPath)) {
1081
- fs4.unlinkSync(this.pidPath);
1461
+ if (fs5.existsSync(this.pidPath)) {
1462
+ fs5.unlinkSync(this.pidPath);
1082
1463
  }
1083
1464
  }
1084
1465
  isProcessRunning(pid) {
@@ -1099,12 +1480,12 @@ var PidManager = class {
1099
1480
  };
1100
1481
 
1101
1482
  // src/scheduled-tasks/daemon/SchedulerDaemon.ts
1102
- import * as fs9 from "fs";
1483
+ import * as fs10 from "fs";
1103
1484
  import * as os from "os";
1104
1485
 
1105
1486
  // src/scheduled-tasks/executors/GithubCopilotCliExecutor.ts
1106
1487
  import { spawn as spawn2 } from "child_process";
1107
- import * as fs5 from "fs";
1488
+ import * as fs6 from "fs";
1108
1489
 
1109
1490
  // src/scheduled-tasks/executors/timeout.ts
1110
1491
  function resolveConfiguredTimeoutMs(timeoutSeconds, defaultTimeoutMs) {
@@ -1139,7 +1520,7 @@ function redactArgs(args) {
1139
1520
  function writeDiagnostic(message) {
1140
1521
  const logPath = process.env.SERVICEME_SCHEDULER_LOG_PATH;
1141
1522
  if (logPath) {
1142
- fs5.appendFileSync(logPath, message);
1523
+ fs6.appendFileSync(logPath, message);
1143
1524
  return;
1144
1525
  }
1145
1526
  process.stderr.write(message);
@@ -1349,15 +1730,15 @@ ${body}`.trim()
1349
1730
 
1350
1731
  // src/scheduled-tasks/executors/ShellExecutor.ts
1351
1732
  import { spawn as spawn3 } from "child_process";
1352
- import * as fs6 from "fs";
1353
- import * as path5 from "path";
1733
+ import * as fs7 from "fs";
1734
+ import * as path6 from "path";
1354
1735
  var MAX_OUTPUT_BYTES2 = 1024 * 1024;
1355
1736
  var DEFAULT_TIMEOUT_MS4 = 6e4;
1356
1737
  var POSIX_SHELL_CANDIDATES = ["bash.exe", "sh.exe"];
1357
1738
  function resolveShellExecution(script, options = {}) {
1358
1739
  const platform = options.platform ?? process.platform;
1359
1740
  const env = options.env ?? process.env;
1360
- const fileExists = options.fileExists ?? fs6.existsSync;
1741
+ const fileExists = options.fileExists ?? fs7.existsSync;
1361
1742
  if (platform === "win32") {
1362
1743
  const posixShell = usesPosixShellSyntax(script) ? findWindowsPosixShell(env, fileExists) : null;
1363
1744
  if (posixShell) {
@@ -1402,10 +1783,10 @@ function findWindowsPosixShell(env, fileExists) {
1402
1783
  if (fileExists(candidate)) return candidate;
1403
1784
  }
1404
1785
  const pathValue = env.Path ?? env.PATH ?? "";
1405
- for (const dir of pathValue.split(path5.win32.delimiter)) {
1786
+ for (const dir of pathValue.split(path6.win32.delimiter)) {
1406
1787
  if (!dir) continue;
1407
1788
  for (const executable of POSIX_SHELL_CANDIDATES) {
1408
- const candidate = path5.win32.join(dir, executable);
1789
+ const candidate = path6.win32.join(dir, executable);
1409
1790
  if (fileExists(candidate) && !isWindowsWslLauncher(candidate)) {
1410
1791
  return candidate;
1411
1792
  }
@@ -1414,13 +1795,13 @@ function findWindowsPosixShell(env, fileExists) {
1414
1795
  return null;
1415
1796
  }
1416
1797
  function isWindowsWslLauncher(candidate) {
1417
- const normalized = path5.win32.normalize(candidate).toLowerCase();
1798
+ const normalized = path6.win32.normalize(candidate).toLowerCase();
1418
1799
  return normalized.endsWith("\\windows\\system32\\bash.exe") || normalized.endsWith("\\windows\\syswow64\\bash.exe");
1419
1800
  }
1420
1801
  function writeDiagnostic2(message) {
1421
1802
  const logPath = process.env.SERVICEME_SCHEDULER_LOG_PATH;
1422
1803
  if (logPath) {
1423
- fs6.appendFileSync(logPath, message);
1804
+ fs7.appendFileSync(logPath, message);
1424
1805
  return;
1425
1806
  }
1426
1807
  process.stderr.write(message);
@@ -1567,9 +1948,9 @@ function getExecutor(taskType) {
1567
1948
 
1568
1949
  // src/scheduled-tasks/TaskConfigManager.ts
1569
1950
  import { randomUUID } from "crypto";
1570
- import * as fs7 from "fs";
1571
- import * as path6 from "path";
1572
- import { createServicemeError as createServicemeError7 } from "@serviceme/devtools-protocol";
1951
+ import * as fs8 from "fs";
1952
+ import * as path7 from "path";
1953
+ import { createServicemeError as createServicemeError8 } from "@serviceme/devtools-protocol";
1573
1954
  var CONFIG_DIR3 = ".serviceme";
1574
1955
  var CONFIG_FILE = "scheduled-tasks.json";
1575
1956
  function emptyConfig() {
@@ -1580,7 +1961,7 @@ function isRecord(value) {
1580
1961
  }
1581
1962
  function requireNonEmptyString(payload, field, taskType) {
1582
1963
  if (!isRecord(payload) || typeof payload[field] !== "string" || !payload[field].trim()) {
1583
- throw createServicemeError7(
1964
+ throw createServicemeError8(
1584
1965
  "invalid_payload",
1585
1966
  `${taskType} payload ${field} is required`
1586
1967
  );
@@ -1605,27 +1986,27 @@ function validateTaskPayload(taskType, payload) {
1605
1986
  }
1606
1987
  var TaskConfigManager = class {
1607
1988
  constructor(workspacePath) {
1608
- this.configPath = path6.join(workspacePath, CONFIG_DIR3, CONFIG_FILE);
1989
+ this.configPath = path7.join(workspacePath, CONFIG_DIR3, CONFIG_FILE);
1609
1990
  }
1610
1991
  getConfigPath() {
1611
1992
  return this.configPath;
1612
1993
  }
1613
1994
  readConfig() {
1614
- if (!fs7.existsSync(this.configPath)) {
1995
+ if (!fs8.existsSync(this.configPath)) {
1615
1996
  return emptyConfig();
1616
1997
  }
1617
- const raw = fs7.readFileSync(this.configPath, "utf-8");
1998
+ const raw = fs8.readFileSync(this.configPath, "utf-8");
1618
1999
  const parsed = JSON.parse(raw);
1619
2000
  return parsed;
1620
2001
  }
1621
2002
  writeConfig(config) {
1622
- const dir = path6.dirname(this.configPath);
1623
- if (!fs7.existsSync(dir)) {
1624
- fs7.mkdirSync(dir, { recursive: true });
2003
+ const dir = path7.dirname(this.configPath);
2004
+ if (!fs8.existsSync(dir)) {
2005
+ fs8.mkdirSync(dir, { recursive: true });
1625
2006
  }
1626
2007
  const tmp = `${this.configPath}.tmp`;
1627
- fs7.writeFileSync(tmp, JSON.stringify(config, null, " "), "utf-8");
1628
- fs7.renameSync(tmp, this.configPath);
2008
+ fs8.writeFileSync(tmp, JSON.stringify(config, null, " "), "utf-8");
2009
+ fs8.renameSync(tmp, this.configPath);
1629
2010
  }
1630
2011
  listTasks() {
1631
2012
  return this.readConfig().tasks;
@@ -1896,8 +2277,8 @@ var TaskExecutionEngine = class {
1896
2277
 
1897
2278
  // src/scheduled-tasks/TaskLogManager.ts
1898
2279
  import { randomUUID as randomUUID2 } from "crypto";
1899
- import * as fs8 from "fs";
1900
- import * as path7 from "path";
2280
+ import * as fs9 from "fs";
2281
+ import * as path8 from "path";
1901
2282
  var CONFIG_DIR4 = ".serviceme";
1902
2283
  var LOG_FILE2 = "scheduled-tasks-log.json";
1903
2284
  var MAX_LOGS = 200;
@@ -1922,17 +2303,17 @@ function validateAndRepairLogFile(raw) {
1922
2303
  }
1923
2304
  var TaskLogManager = class {
1924
2305
  constructor(workspacePath) {
1925
- this.logPath = path7.join(workspacePath, CONFIG_DIR4, LOG_FILE2);
2306
+ this.logPath = path8.join(workspacePath, CONFIG_DIR4, LOG_FILE2);
1926
2307
  }
1927
2308
  getLogPath() {
1928
2309
  return this.logPath;
1929
2310
  }
1930
2311
  readLogFile() {
1931
- if (!fs8.existsSync(this.logPath)) {
2312
+ if (!fs9.existsSync(this.logPath)) {
1932
2313
  return emptyLogFile();
1933
2314
  }
1934
2315
  try {
1935
- const raw = fs8.readFileSync(this.logPath, "utf-8");
2316
+ const raw = fs9.readFileSync(this.logPath, "utf-8");
1936
2317
  const parsed = JSON.parse(raw);
1937
2318
  const file = validateAndRepairLogFile(parsed);
1938
2319
  if (!parsed || typeof parsed !== "object" || !Array.isArray(parsed.logs) || parsed.logs.length !== file.logs.length) {
@@ -1948,21 +2329,21 @@ var TaskLogManager = class {
1948
2329
  }
1949
2330
  backupCorruptedFile() {
1950
2331
  try {
1951
- if (fs8.existsSync(this.logPath)) {
2332
+ if (fs9.existsSync(this.logPath)) {
1952
2333
  const backupPath = `${this.logPath}.corrupted.${Date.now()}`;
1953
- fs8.copyFileSync(this.logPath, backupPath);
2334
+ fs9.copyFileSync(this.logPath, backupPath);
1954
2335
  }
1955
2336
  } catch {
1956
2337
  }
1957
2338
  }
1958
2339
  writeLogFile(file) {
1959
- const dir = path7.dirname(this.logPath);
1960
- if (!fs8.existsSync(dir)) {
1961
- fs8.mkdirSync(dir, { recursive: true });
2340
+ const dir = path8.dirname(this.logPath);
2341
+ if (!fs9.existsSync(dir)) {
2342
+ fs9.mkdirSync(dir, { recursive: true });
1962
2343
  }
1963
2344
  const tmp = `${this.logPath}.tmp`;
1964
- fs8.writeFileSync(tmp, JSON.stringify(file, null, " "), "utf-8");
1965
- fs8.renameSync(tmp, this.logPath);
2345
+ fs9.writeFileSync(tmp, JSON.stringify(file, null, " "), "utf-8");
2346
+ fs9.renameSync(tmp, this.logPath);
1966
2347
  }
1967
2348
  appendLog(input) {
1968
2349
  const file = this.readLogFile();
@@ -2073,8 +2454,8 @@ var SchedulerDaemon = class {
2073
2454
  const configPath = this.configManager.getConfigPath();
2074
2455
  const dir = configPath.substring(0, configPath.lastIndexOf("/"));
2075
2456
  try {
2076
- if (fs9.existsSync(dir)) {
2077
- this.watcher = fs9.watch(dir, (_eventType, filename) => {
2457
+ if (fs10.existsSync(dir)) {
2458
+ this.watcher = fs10.watch(dir, (_eventType, filename) => {
2078
2459
  if (filename === "scheduled-tasks.json") {
2079
2460
  this.logger.log("info", "Config file changed, reconciling...");
2080
2461
  }
@@ -2226,7 +2607,214 @@ function matchCronField(field, value) {
2226
2607
  const values = field.split(",");
2227
2608
  return values.some((v) => Number.parseInt(v, 10) === value);
2228
2609
  }
2610
+
2611
+ // src/skills/SkillCatalogClient.ts
2612
+ import { createServicemeError as createServicemeError9 } from "@serviceme/devtools-protocol";
2613
+ var SkillCatalogClient = class {
2614
+ constructor(options = {}) {
2615
+ this.fetchImpl = options.fetchImpl ?? fetch;
2616
+ this.baseUrl = options.baseUrl;
2617
+ }
2618
+ async getCatalog() {
2619
+ if (!this.baseUrl) {
2620
+ return {
2621
+ skills: [],
2622
+ fetchedAt: (/* @__PURE__ */ new Date()).toISOString()
2623
+ };
2624
+ }
2625
+ const response = await this.fetchImpl(
2626
+ `${this.baseUrl}/api/v1/marketplace/skills`
2627
+ );
2628
+ if (!response.ok) {
2629
+ throw new Error(`Failed to fetch skills catalog: ${response.status}`);
2630
+ }
2631
+ const data = await response.json();
2632
+ return {
2633
+ skills: data.skills ?? [],
2634
+ fetchedAt: (/* @__PURE__ */ new Date()).toISOString()
2635
+ };
2636
+ }
2637
+ async downloadSkill(remoteId) {
2638
+ if (!this.baseUrl) {
2639
+ throw createServicemeError9(
2640
+ "workspace_not_found",
2641
+ "Skill catalog baseUrl is not configured."
2642
+ );
2643
+ }
2644
+ const response = await this.fetchImpl(
2645
+ `${this.baseUrl}/api/v1/marketplace/skills/download/${remoteId}`
2646
+ );
2647
+ if (!response.ok) {
2648
+ if (response.status === 404) {
2649
+ throw createServicemeError9(
2650
+ "not_found",
2651
+ `Skill '${remoteId}' not found`
2652
+ );
2653
+ }
2654
+ throw new Error(
2655
+ `Failed to download skill ${remoteId}: ${response.status}`
2656
+ );
2657
+ }
2658
+ const payload = await response.json();
2659
+ return payload.data?.files ?? [];
2660
+ }
2661
+ };
2662
+
2663
+ // src/skills/SkillReconciler.ts
2664
+ var SkillReconciler = class {
2665
+ constructor(deps) {
2666
+ this.deps = deps;
2667
+ }
2668
+ async mutate(request) {
2669
+ if (request.targetScope !== "workspace" && request.targetScope !== "user") {
2670
+ throw new Error(`Invalid target scope: ${String(request.targetScope)}`);
2671
+ }
2672
+ if (request.action === "uninstall" || request.action === "move" || request.action === "removeExternal") {
2673
+ return {
2674
+ status: "success",
2675
+ changed: true,
2676
+ message: `Skill ${request.action} completed.`
2677
+ };
2678
+ }
2679
+ if (request.action !== "install") {
2680
+ return {
2681
+ status: "blocked",
2682
+ changed: false,
2683
+ message: `Skill action is not supported by bridge reconciler: ${request.action}`
2684
+ };
2685
+ }
2686
+ const catalog = await this.deps.catalogClient.getCatalog();
2687
+ const remoteSkill = catalog.skills.find(
2688
+ (skill) => this.deps.skillStore.normalizeRemoteSkillId(skill.id) === request.skillId
2689
+ );
2690
+ if (!remoteSkill) {
2691
+ return {
2692
+ status: "blocked",
2693
+ changed: false,
2694
+ message: "Skill not found in catalog."
2695
+ };
2696
+ }
2697
+ if (!request.confirmed && (remoteSkill.hasScripts || remoteSkill.hasHooks)) {
2698
+ return {
2699
+ status: "requires_confirmation",
2700
+ changed: false,
2701
+ hasScripts: Boolean(remoteSkill.hasScripts),
2702
+ hasHooks: Boolean(remoteSkill.hasHooks),
2703
+ message: "This skill contains executable scripts that require confirmation."
2704
+ };
2705
+ }
2706
+ return {
2707
+ status: "success",
2708
+ changed: true,
2709
+ message: "Skill installed."
2710
+ };
2711
+ }
2712
+ };
2713
+
2714
+ // src/skills/SkillStore.ts
2715
+ import * as fs11 from "fs/promises";
2716
+ import * as path9 from "path";
2717
+ var USER_SKILL_MARKER_FILE = ".ms-devtools-skill.json";
2718
+ var WORKSPACE_SKILLS_ROOT_RELATIVE = ".github/skills";
2719
+ var WORKSPACE_SKILLS_MARKER_RELATIVE = ".github/.ms-devtools-skills.yml";
2720
+ var SAFE_LOCAL_ID_PATTERN2 = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
2721
+ function assertSafeLocalSkillId(skillId) {
2722
+ if (typeof skillId !== "string" || skillId.length === 0 || skillId === "." || skillId === ".." || skillId.includes("/") || skillId.includes("\\") || !SAFE_LOCAL_ID_PATTERN2.test(skillId)) {
2723
+ throw new Error(`Invalid skill id: ${skillId}`);
2724
+ }
2725
+ return skillId;
2726
+ }
2727
+ var SkillStore = class {
2728
+ constructor(options) {
2729
+ this.workspacePath = options.workspacePath;
2730
+ this.userSkillsRoot = options.userSkillsRoot;
2731
+ this.fileSystem = options.fileSystem ?? fs11;
2732
+ }
2733
+ normalizeRemoteSkillId(remoteId) {
2734
+ if (remoteId.startsWith("official/")) {
2735
+ return assertSafeLocalSkillId(remoteId.slice("official/".length));
2736
+ }
2737
+ if (remoteId.startsWith("community/")) {
2738
+ const lastSlash = remoteId.lastIndexOf("/");
2739
+ return assertSafeLocalSkillId(remoteId.slice(lastSlash + 1));
2740
+ }
2741
+ return assertSafeLocalSkillId(remoteId);
2742
+ }
2743
+ getWorkspaceSkillPath(skillId) {
2744
+ return `${WORKSPACE_SKILLS_ROOT_RELATIVE}/${skillId}`;
2745
+ }
2746
+ getWorkspaceMarkerPath() {
2747
+ return WORKSPACE_SKILLS_MARKER_RELATIVE;
2748
+ }
2749
+ getUserSkillPath(skillId) {
2750
+ return path9.join(this.userSkillsRoot, skillId);
2751
+ }
2752
+ async listWorkspaceSkillIds() {
2753
+ const skillsRootPath = path9.join(
2754
+ this.workspacePath,
2755
+ WORKSPACE_SKILLS_ROOT_RELATIVE
2756
+ );
2757
+ try {
2758
+ const entries = await this.fileSystem.readdir(skillsRootPath, {
2759
+ withFileTypes: true
2760
+ });
2761
+ return entries.filter((entry) => entry.isDirectory() && !entry.name.startsWith(".")).map((entry) => entry.name).sort();
2762
+ } catch {
2763
+ return [];
2764
+ }
2765
+ }
2766
+ async listUserSkillIds() {
2767
+ try {
2768
+ const entries = await this.fileSystem.readdir(this.userSkillsRoot, {
2769
+ withFileTypes: true
2770
+ });
2771
+ return entries.filter((entry) => entry.isDirectory() && !entry.name.startsWith(".")).map((entry) => entry.name).sort();
2772
+ } catch {
2773
+ return [];
2774
+ }
2775
+ }
2776
+ async writeManagedUserSkillMarker(skillId) {
2777
+ const targetDir = this.getUserSkillPath(skillId);
2778
+ await this.fileSystem.mkdir(targetDir, { recursive: true });
2779
+ await this.fileSystem.writeFile(
2780
+ path9.join(targetDir, USER_SKILL_MARKER_FILE),
2781
+ JSON.stringify({ skillId, installedBy: "ms-devtools" }, null, 2),
2782
+ "utf-8"
2783
+ );
2784
+ }
2785
+ async isManagedUserSkill(skillId) {
2786
+ try {
2787
+ const marker = await this.fileSystem.readFile(
2788
+ path9.join(this.getUserSkillPath(skillId), USER_SKILL_MARKER_FILE),
2789
+ "utf-8"
2790
+ );
2791
+ const parsed = JSON.parse(marker);
2792
+ return parsed.skillId === skillId;
2793
+ } catch {
2794
+ return false;
2795
+ }
2796
+ }
2797
+ async writeSkillFiles(skillId, scope, files) {
2798
+ const root = scope === "workspace" ? path9.join(this.workspacePath, WORKSPACE_SKILLS_ROOT_RELATIVE) : this.userSkillsRoot;
2799
+ const targetDir = path9.join(root, skillId);
2800
+ await this.fileSystem.mkdir(targetDir, { recursive: true });
2801
+ for (const file of files) {
2802
+ const filePath = path9.join(targetDir, file.path);
2803
+ await this.fileSystem.mkdir(path9.dirname(filePath), { recursive: true });
2804
+ await this.fileSystem.writeFile(filePath, file.content, "utf-8");
2805
+ if (file.executable) {
2806
+ try {
2807
+ await this.fileSystem.chmod(filePath, 493);
2808
+ } catch {
2809
+ }
2810
+ }
2811
+ }
2812
+ }
2813
+ };
2229
2814
  export {
2815
+ AgentCatalogClient,
2816
+ AgentReconciler,
2817
+ AgentStore,
2230
2818
  DaemonLogger,
2231
2819
  EnvironmentInspector,
2232
2820
  GithubCopilotCliExecutor,
@@ -2236,6 +2824,10 @@ export {
2236
2824
  ProjectTools,
2237
2825
  SchedulerDaemon,
2238
2826
  ShellExecutor,
2827
+ SkillCatalogClient,
2828
+ SkillReconciler,
2829
+ SkillStore,
2830
+ TOOL_RISK_MAP,
2239
2831
  TaskConfigManager,
2240
2832
  TaskExecutionEngine,
2241
2833
  TaskLogManager,
@@ -2253,6 +2845,7 @@ export {
2253
2845
  matchesCron,
2254
2846
  moveFiles,
2255
2847
  noopLogger,
2848
+ parseAgentToolPermissions,
2256
2849
  parseIntervalMs,
2257
2850
  resolveTaskExecutionPayload,
2258
2851
  unzipFile,