@serviceme/devtools-cli 2.0.0 → 2.0.1

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.
@@ -59,6 +59,8 @@ var BRIDGE_METHODS = [
59
59
  "copilotContent.convertToSymlink",
60
60
  "copilotContent.uninstall",
61
61
  "copilotContent.setEntryEnabled",
62
+ "copilotContent.detectUnmanaged",
63
+ "copilotContent.adoptUnmanaged",
62
64
  "copilotContent.listLinked",
63
65
  "copilotContent.draft.create",
64
66
  "copilotContent.draft.commit",
@@ -244,7 +246,7 @@ function normalizeServicemeError(error, fallbackCode = "internal_error") {
244
246
  }
245
247
 
246
248
  // src/version.ts
247
- var SERVICEME_CLI_VERSION = "2.0.0";
249
+ var SERVICEME_CLI_VERSION = "2.0.1";
248
250
 
249
251
  // src/bridge/CopilotContentBridgeHandler.ts
250
252
  var fsp = __toESM(require("fs/promises"));
@@ -305,16 +307,12 @@ var CopilotContentBridgeHandler = class {
305
307
  * removed and never resurrected) while the manifest stays intact.
306
308
  */
307
309
  async makeReconciler(workspaceDir) {
308
- const disabledStore = new import_devtools_core.DisabledContentStore();
309
- const marks = await disabledStore.list();
310
- const matches = (identity) => {
311
- const segments = identity.split("::");
312
- if (segments.length < 3) return false;
313
- const [repoId, pluginId, kindName] = segments;
314
- return marks.some(
315
- (mark) => mark.scope === "workspace" && mark.workspaceDir === workspaceDir && mark.repoId === repoId && mark.name === pluginId && kindName === mark.kind
316
- );
317
- };
310
+ let marks = [];
311
+ try {
312
+ marks = await new import_devtools_core.DisabledContentStore().list();
313
+ } catch {
314
+ }
315
+ const matches = (0, import_devtools_core.buildDisabledIdentityMatcher)(workspaceDir, marks);
318
316
  return new import_devtools_core.WorkspaceCopilotContentReconciler({
319
317
  workspaceDir,
320
318
  ensureRepository: (manifest) => this.ensureRepositories(manifest),
@@ -483,7 +481,8 @@ var CopilotCustomizationsBridgeHandler = class {
483
481
  installations: snapshot.installations.map(toPublicInstallation),
484
482
  statesByArtifactId: toPublicStates(snapshot.statesByArtifactId),
485
483
  legacyCount: snapshot.legacyCount,
486
- generatedAt: snapshot.generatedAt
484
+ generatedAt: snapshot.generatedAt,
485
+ ...snapshot.disabledArtifactIds ? { disabledArtifactIds: snapshot.disabledArtifactIds } : {}
487
486
  }),
488
487
  ...availablePackages ? { availablePackages } : {}
489
488
  };
@@ -607,13 +606,35 @@ function createAvailablePackagesEnumerator(store) {
607
606
  repositories: [],
608
607
  plugins: []
609
608
  };
610
- const workspaceInstalled = new Set(
611
- manifest.plugins.map(
612
- (plugin) => `${(0, import_devtools_core2.getWorkspaceManifestPluginSourceId)(manifest, plugin)}::${plugin.id}`
613
- )
609
+ const coversPackage = (pkg, selectedIds, selectedKinds) => pkg.artifacts.every(
610
+ (artifact) => selectedIds !== void 0 ? selectedIds.includes(artifact.id) : selectedKinds?.[artifact.kind] === true
611
+ );
612
+ const manifestSelections = new Map(
613
+ manifest.plugins.map((plugin) => [
614
+ `${(0, import_devtools_core2.getWorkspaceManifestPluginSourceId)(manifest, plugin)}::${plugin.id}`,
615
+ plugin
616
+ ])
614
617
  );
615
- const personalInstalled = new Set(
616
- (personalIntent?.installations ?? []).filter((installation) => installation.scope === "personal").map((installation) => installation.packageId)
618
+ const machineMarks = await new import_devtools_core2.DisabledContentStore().list().catch(() => []);
619
+ const disabledTargets = /* @__PURE__ */ new Map();
620
+ const addDisabledTarget = (packageKey, target) => {
621
+ const targets = disabledTargets.get(packageKey) ?? /* @__PURE__ */ new Set();
622
+ targets.add(target);
623
+ disabledTargets.set(packageKey, targets);
624
+ };
625
+ for (const plugin of manifest.plugins) {
626
+ const packageKey = `${(0, import_devtools_core2.getWorkspaceManifestPluginSourceId)(manifest, plugin)}::${plugin.id}`;
627
+ for (const target of plugin.disabledArtifacts ?? []) {
628
+ addDisabledTarget(packageKey, target);
629
+ }
630
+ }
631
+ for (const mark of machineMarks) {
632
+ if (mark.scope === "user" || mark.workspaceDir === params.workspaceDir) {
633
+ addDisabledTarget(`${mark.repoId}::*`, `${mark.kind}:${mark.name}`);
634
+ }
635
+ }
636
+ const personalSelections = new Map(
637
+ (personalIntent?.installations ?? []).filter((installation) => installation.scope === "personal").map((installation) => [installation.packageId, installation])
617
638
  );
618
639
  const registeredScopes = /* @__PURE__ */ new Map();
619
640
  for (const reg of registrations) {
@@ -623,7 +644,20 @@ function createAvailablePackagesEnumerator(store) {
623
644
  registeredScopes.set(packageId, scopes);
624
645
  }
625
646
  return catalogPackages.map((pkg) => {
626
- const installed = registeredScopes.get(pkg.packageId) !== void 0 || workspaceInstalled.has(pkg.packageId) || personalInstalled.has(pkg.packageId);
647
+ const manifestPlugin = manifestSelections.get(pkg.packageId);
648
+ const personalInstallation = personalSelections.get(pkg.packageId);
649
+ const disabledTargetsForPkg = /* @__PURE__ */ new Set([
650
+ ...disabledTargets.get(pkg.packageId) ?? [],
651
+ ...disabledTargets.get(`${pkg.sourceId}::*`) ?? []
652
+ ]);
653
+ const hasDisabledArtifact = pkg.artifacts.find(
654
+ (artifact) => disabledTargetsForPkg.has(`${artifact.kind}:${artifact.displayName}`)
655
+ ) !== void 0;
656
+ const installed = registeredScopes.get(pkg.packageId) !== void 0 || hasDisabledArtifact || manifestPlugin !== void 0 && coversPackage(
657
+ pkg,
658
+ manifestPlugin.artifactIds,
659
+ manifestPlugin.artifactIds === void 0 ? manifestPlugin.artifacts : void 0
660
+ ) || personalInstallation !== void 0 && coversPackage(pkg, personalInstallation.selectedArtifactIds, void 0);
627
661
  return {
628
662
  ...pkg,
629
663
  installedScopes: installed ? ["personal"] : []
@@ -804,7 +838,8 @@ async function createPackageInstallationService(store) {
804
838
  packages: snapshot.packages,
805
839
  installations: snapshot.installations,
806
840
  statesByArtifactId: snapshot.statesByArtifactId,
807
- generatedAt: snapshot.generatedAt
841
+ generatedAt: snapshot.generatedAt,
842
+ ...snapshot.disabledArtifactIds ? { disabledArtifactIds: snapshot.disabledArtifactIds } : {}
808
843
  });
809
844
  },
810
845
  readWorkspaceInstallations: import_devtools_core2.readDeclaredWorkspaceInstallations
@@ -972,9 +1007,16 @@ async function createWorkspaceSnapshot(repoDisplayName, readers, workspaceDir) {
972
1007
  })
973
1008
  );
974
1009
  const state = await readers.readWorkspaceState(workspaceDir);
1010
+ let isDisabled = () => false;
1011
+ try {
1012
+ const marks = await new import_devtools_core2.DisabledContentStore().list();
1013
+ isDisabled = (0, import_devtools_core2.buildDisabledIdentityMatcher)(workspaceDir, marks);
1014
+ } catch {
1015
+ }
975
1016
  const packages = [];
976
1017
  const installations = [];
977
1018
  const statesByArtifactId = {};
1019
+ const disabledArtifactIds = [];
978
1020
  const resolvedPlugins = await Promise.all(
979
1021
  manifest.plugins.map(async (plugin) => {
980
1022
  const sourceId = (0, import_devtools_core2.getWorkspaceManifestPluginSourceId)(manifest, plugin);
@@ -1013,11 +1055,27 @@ async function createWorkspaceSnapshot(repoDisplayName, readers, workspaceDir) {
1013
1055
  });
1014
1056
  for (const entry of entries) {
1015
1057
  const previous = state.entries.find((candidate) => candidate.identity === entry.artifactId);
1016
- statesByArtifactId[entry.artifactId] = fallbackHealth ? { intent: "selected", health: fallbackHealth, gate: "ready" } : toArtifactState(entry, previous);
1058
+ if (isDisabled(entry.artifactId)) {
1059
+ disabledArtifactIds.push(entry.artifactId);
1060
+ statesByArtifactId[entry.artifactId] = {
1061
+ intent: "selected",
1062
+ health: "healthy",
1063
+ gate: "ready"
1064
+ };
1065
+ } else {
1066
+ statesByArtifactId[entry.artifactId] = fallbackHealth ? { intent: "selected", health: fallbackHealth, gate: "ready" } : toArtifactState(entry, previous);
1067
+ }
1017
1068
  }
1018
1069
  }
1019
1070
  await mergeRegistrarPackages(sources, packages, installations, statesByArtifactId);
1020
- return { sources, packages, installations, statesByArtifactId, workspaceManifest: manifest };
1071
+ return {
1072
+ sources,
1073
+ packages,
1074
+ installations,
1075
+ statesByArtifactId,
1076
+ disabledArtifactIds,
1077
+ workspaceManifest: manifest
1078
+ };
1021
1079
  }
1022
1080
  async function createFullPersonalSnapshot(options, legacyLinks) {
1023
1081
  const capabilities = options.personal?.capabilities ?? await (0, import_devtools_core2.createDefaultCopilotHostCapabilities)();
@@ -1076,20 +1134,29 @@ async function createFullPersonalSnapshot(options, legacyLinks) {
1076
1134
  }
1077
1135
  function fallbackEntries(plugin, packageId) {
1078
1136
  const kinds = Object.keys(plugin.artifacts);
1079
- return kinds.filter((kind) => plugin.artifacts[kind] !== false).map((kind) => ({
1080
- identity: `${packageId}::${kind}:${plugin.id}`,
1081
- artifactId: `${packageId}::${kind}:${plugin.id}`,
1082
- packageId,
1083
- packageDisplayName: plugin.id,
1084
- repositoryId: packageId.split("::")[0] ?? packageId,
1085
- pluginId: plugin.id,
1086
- kind,
1087
- sourcePath: "",
1088
- sourceIsFile: false,
1089
- name: plugin.id,
1090
- digest: "",
1091
- requiresApproval: kind === "hook" || kind === "mcp"
1092
- }));
1137
+ return kinds.filter((kind) => plugin.artifacts[kind] !== false).map((kind) => {
1138
+ const repoId = packageId.split("::")[0] ?? packageId;
1139
+ const identity = (0, import_devtools_core2.buildContentIdentity)({
1140
+ repoId,
1141
+ pluginId: plugin.id,
1142
+ kind,
1143
+ name: plugin.id
1144
+ });
1145
+ return {
1146
+ identity,
1147
+ artifactId: identity,
1148
+ packageId,
1149
+ packageDisplayName: plugin.id,
1150
+ repositoryId: repoId,
1151
+ pluginId: plugin.id,
1152
+ kind,
1153
+ sourcePath: "",
1154
+ sourceIsFile: false,
1155
+ name: plugin.id,
1156
+ digest: "",
1157
+ requiresApproval: kind === "hook" || kind === "mcp"
1158
+ };
1159
+ });
1093
1160
  }
1094
1161
  function toArtifactState(entry, previous) {
1095
1162
  if (!previous) {
@@ -1607,6 +1674,20 @@ var BridgeServer = class {
1607
1674
  this.writeSuccess(request.id, result);
1608
1675
  return;
1609
1676
  }
1677
+ case "copilotContent.detectUnmanaged": {
1678
+ const r = request;
1679
+ if (!this.skillRepoHandler) return this.unsupportedV2(request.id);
1680
+ const result = await this.skillRepoHandler.detectUnmanaged(r.params);
1681
+ this.writeSuccess(request.id, result);
1682
+ return;
1683
+ }
1684
+ case "copilotContent.adoptUnmanaged": {
1685
+ const r = request;
1686
+ if (!this.skillRepoHandler) return this.unsupportedV2(request.id);
1687
+ const result = await this.skillRepoHandler.adoptUnmanaged(r.params);
1688
+ this.writeSuccess(request.id, result);
1689
+ return;
1690
+ }
1610
1691
  case "copilotContent.listLinked":
1611
1692
  case "skillRepo.listLinked": {
1612
1693
  const r = request;
package/dist/cli.js CHANGED
@@ -123,16 +123,12 @@ var init_CopilotContentBridgeHandler = __esm({
123
123
  * removed and never resurrected) while the manifest stays intact.
124
124
  */
125
125
  async makeReconciler(workspaceDir) {
126
- const disabledStore = new import_devtools_core2.DisabledContentStore();
127
- const marks = await disabledStore.list();
128
- const matches = (identity) => {
129
- const segments = identity.split("::");
130
- if (segments.length < 3) return false;
131
- const [repoId, pluginId, kindName] = segments;
132
- return marks.some(
133
- (mark) => mark.scope === "workspace" && mark.workspaceDir === workspaceDir && mark.repoId === repoId && mark.name === pluginId && kindName === mark.kind
134
- );
135
- };
126
+ let marks = [];
127
+ try {
128
+ marks = await new import_devtools_core2.DisabledContentStore().list();
129
+ } catch {
130
+ }
131
+ const matches = (0, import_devtools_core2.buildDisabledIdentityMatcher)(workspaceDir, marks);
136
132
  return new import_devtools_core2.WorkspaceCopilotContentReconciler({
137
133
  workspaceDir,
138
134
  ensureRepository: (manifest) => this.ensureRepositories(manifest),
@@ -301,6 +297,8 @@ var BRIDGE_METHODS = [
301
297
  "copilotContent.convertToSymlink",
302
298
  "copilotContent.uninstall",
303
299
  "copilotContent.setEntryEnabled",
300
+ "copilotContent.detectUnmanaged",
301
+ "copilotContent.adoptUnmanaged",
304
302
  "copilotContent.listLinked",
305
303
  "copilotContent.draft.create",
306
304
  "copilotContent.draft.commit",
@@ -718,22 +716,38 @@ async function handleUninstall(store, workspacePath, parsed) {
718
716
  throw createServicemeError("invalid_params", "Expected --id <remoteAgentId>.");
719
717
  }
720
718
  const agentId = normalizeAgentIdOrThrow(store, remoteId);
721
- await fs.rm(path.join(workspacePath, ".github", "agents", `${agentId}.agent.md`), {
722
- recursive: true,
723
- force: true
724
- });
725
- await fs.rm(path.join(workspacePath, ".github", "agents", agentId), {
726
- recursive: true,
727
- force: true
728
- });
729
- await fs.rm(path.join(store.getUserAgentsRootPath(), `${agentId}.agent.md`), {
730
- recursive: true,
731
- force: true
732
- });
733
- await fs.rm(path.join(store.getUserAgentsRootPath(), agentId), {
734
- recursive: true,
735
- force: true
736
- });
719
+ const workspaceRoot = path.resolve(workspacePath);
720
+ const targets = [
721
+ path.join(workspaceRoot, ".github", "agents", `${agentId}.agent.md`),
722
+ path.join(workspaceRoot, ".github", "agents", agentId)
723
+ ];
724
+ for (const target of targets) {
725
+ const resolvedTarget = path.resolve(target);
726
+ if (!resolvedTarget.startsWith(workspaceRoot + path.sep)) {
727
+ throw createServicemeError("invalid_params", `Agent link escapes the workspace: ${agentId}`);
728
+ }
729
+ await fs.rm(resolvedTarget, {
730
+ recursive: true,
731
+ force: true
732
+ });
733
+ }
734
+ const userAgentsRoot = path.resolve(store.getUserAgentsRootPath());
735
+ for (const userTarget of [
736
+ path.join(userAgentsRoot, `${agentId}.agent.md`),
737
+ path.join(userAgentsRoot, agentId)
738
+ ]) {
739
+ const resolvedUserTarget = path.resolve(userTarget);
740
+ if (!resolvedUserTarget.startsWith(userAgentsRoot + path.sep)) {
741
+ throw createServicemeError(
742
+ "invalid_params",
743
+ `Agent link escapes the agents root: ${agentId}`
744
+ );
745
+ }
746
+ await fs.rm(resolvedUserTarget, {
747
+ recursive: true,
748
+ force: true
749
+ });
750
+ }
737
751
  await store.removeInstalledAgent(remoteId);
738
752
  return {
739
753
  changed: true,
@@ -1008,7 +1022,7 @@ var readline = __toESM(require("readline"));
1008
1022
 
1009
1023
  // src/version.ts
1010
1024
  var SERVICEME_CLI_NAME = "serviceme";
1011
- var SERVICEME_CLI_VERSION = "2.0.0";
1025
+ var SERVICEME_CLI_VERSION = "2.0.1";
1012
1026
 
1013
1027
  // src/bridge/BridgeServer.ts
1014
1028
  init_CopilotContentBridgeHandler();
@@ -1069,7 +1083,8 @@ var CopilotCustomizationsBridgeHandler = class {
1069
1083
  installations: snapshot.installations.map(toPublicInstallation),
1070
1084
  statesByArtifactId: toPublicStates(snapshot.statesByArtifactId),
1071
1085
  legacyCount: snapshot.legacyCount,
1072
- generatedAt: snapshot.generatedAt
1086
+ generatedAt: snapshot.generatedAt,
1087
+ ...snapshot.disabledArtifactIds ? { disabledArtifactIds: snapshot.disabledArtifactIds } : {}
1073
1088
  }),
1074
1089
  ...availablePackages ? { availablePackages } : {}
1075
1090
  };
@@ -1193,13 +1208,35 @@ function createAvailablePackagesEnumerator(store) {
1193
1208
  repositories: [],
1194
1209
  plugins: []
1195
1210
  };
1196
- const workspaceInstalled = new Set(
1197
- manifest.plugins.map(
1198
- (plugin) => `${(0, import_devtools_core3.getWorkspaceManifestPluginSourceId)(manifest, plugin)}::${plugin.id}`
1199
- )
1211
+ const coversPackage = (pkg, selectedIds, selectedKinds) => pkg.artifacts.every(
1212
+ (artifact) => selectedIds !== void 0 ? selectedIds.includes(artifact.id) : selectedKinds?.[artifact.kind] === true
1213
+ );
1214
+ const manifestSelections = new Map(
1215
+ manifest.plugins.map((plugin) => [
1216
+ `${(0, import_devtools_core3.getWorkspaceManifestPluginSourceId)(manifest, plugin)}::${plugin.id}`,
1217
+ plugin
1218
+ ])
1200
1219
  );
1201
- const personalInstalled = new Set(
1202
- (personalIntent?.installations ?? []).filter((installation) => installation.scope === "personal").map((installation) => installation.packageId)
1220
+ const machineMarks = await new import_devtools_core3.DisabledContentStore().list().catch(() => []);
1221
+ const disabledTargets = /* @__PURE__ */ new Map();
1222
+ const addDisabledTarget = (packageKey, target) => {
1223
+ const targets = disabledTargets.get(packageKey) ?? /* @__PURE__ */ new Set();
1224
+ targets.add(target);
1225
+ disabledTargets.set(packageKey, targets);
1226
+ };
1227
+ for (const plugin of manifest.plugins) {
1228
+ const packageKey = `${(0, import_devtools_core3.getWorkspaceManifestPluginSourceId)(manifest, plugin)}::${plugin.id}`;
1229
+ for (const target of plugin.disabledArtifacts ?? []) {
1230
+ addDisabledTarget(packageKey, target);
1231
+ }
1232
+ }
1233
+ for (const mark of machineMarks) {
1234
+ if (mark.scope === "user" || mark.workspaceDir === params.workspaceDir) {
1235
+ addDisabledTarget(`${mark.repoId}::*`, `${mark.kind}:${mark.name}`);
1236
+ }
1237
+ }
1238
+ const personalSelections = new Map(
1239
+ (personalIntent?.installations ?? []).filter((installation) => installation.scope === "personal").map((installation) => [installation.packageId, installation])
1203
1240
  );
1204
1241
  const registeredScopes = /* @__PURE__ */ new Map();
1205
1242
  for (const reg of registrations) {
@@ -1209,7 +1246,20 @@ function createAvailablePackagesEnumerator(store) {
1209
1246
  registeredScopes.set(packageId, scopes);
1210
1247
  }
1211
1248
  return catalogPackages.map((pkg) => {
1212
- const installed = registeredScopes.get(pkg.packageId) !== void 0 || workspaceInstalled.has(pkg.packageId) || personalInstalled.has(pkg.packageId);
1249
+ const manifestPlugin = manifestSelections.get(pkg.packageId);
1250
+ const personalInstallation = personalSelections.get(pkg.packageId);
1251
+ const disabledTargetsForPkg = /* @__PURE__ */ new Set([
1252
+ ...disabledTargets.get(pkg.packageId) ?? [],
1253
+ ...disabledTargets.get(`${pkg.sourceId}::*`) ?? []
1254
+ ]);
1255
+ const hasDisabledArtifact = pkg.artifacts.find(
1256
+ (artifact) => disabledTargetsForPkg.has(`${artifact.kind}:${artifact.displayName}`)
1257
+ ) !== void 0;
1258
+ const installed = registeredScopes.get(pkg.packageId) !== void 0 || hasDisabledArtifact || manifestPlugin !== void 0 && coversPackage(
1259
+ pkg,
1260
+ manifestPlugin.artifactIds,
1261
+ manifestPlugin.artifactIds === void 0 ? manifestPlugin.artifacts : void 0
1262
+ ) || personalInstallation !== void 0 && coversPackage(pkg, personalInstallation.selectedArtifactIds, void 0);
1213
1263
  return {
1214
1264
  ...pkg,
1215
1265
  installedScopes: installed ? ["personal"] : []
@@ -1390,7 +1440,8 @@ async function createPackageInstallationService(store) {
1390
1440
  packages: snapshot.packages,
1391
1441
  installations: snapshot.installations,
1392
1442
  statesByArtifactId: snapshot.statesByArtifactId,
1393
- generatedAt: snapshot.generatedAt
1443
+ generatedAt: snapshot.generatedAt,
1444
+ ...snapshot.disabledArtifactIds ? { disabledArtifactIds: snapshot.disabledArtifactIds } : {}
1394
1445
  });
1395
1446
  },
1396
1447
  readWorkspaceInstallations: import_devtools_core3.readDeclaredWorkspaceInstallations
@@ -1558,9 +1609,16 @@ async function createWorkspaceSnapshot(repoDisplayName, readers, workspaceDir) {
1558
1609
  })
1559
1610
  );
1560
1611
  const state = await readers.readWorkspaceState(workspaceDir);
1612
+ let isDisabled = () => false;
1613
+ try {
1614
+ const marks = await new import_devtools_core3.DisabledContentStore().list();
1615
+ isDisabled = (0, import_devtools_core3.buildDisabledIdentityMatcher)(workspaceDir, marks);
1616
+ } catch {
1617
+ }
1561
1618
  const packages = [];
1562
1619
  const installations = [];
1563
1620
  const statesByArtifactId = {};
1621
+ const disabledArtifactIds = [];
1564
1622
  const resolvedPlugins = await Promise.all(
1565
1623
  manifest.plugins.map(async (plugin) => {
1566
1624
  const sourceId = (0, import_devtools_core3.getWorkspaceManifestPluginSourceId)(manifest, plugin);
@@ -1599,11 +1657,27 @@ async function createWorkspaceSnapshot(repoDisplayName, readers, workspaceDir) {
1599
1657
  });
1600
1658
  for (const entry of entries) {
1601
1659
  const previous = state.entries.find((candidate) => candidate.identity === entry.artifactId);
1602
- statesByArtifactId[entry.artifactId] = fallbackHealth ? { intent: "selected", health: fallbackHealth, gate: "ready" } : toArtifactState(entry, previous);
1660
+ if (isDisabled(entry.artifactId)) {
1661
+ disabledArtifactIds.push(entry.artifactId);
1662
+ statesByArtifactId[entry.artifactId] = {
1663
+ intent: "selected",
1664
+ health: "healthy",
1665
+ gate: "ready"
1666
+ };
1667
+ } else {
1668
+ statesByArtifactId[entry.artifactId] = fallbackHealth ? { intent: "selected", health: fallbackHealth, gate: "ready" } : toArtifactState(entry, previous);
1669
+ }
1603
1670
  }
1604
1671
  }
1605
1672
  await mergeRegistrarPackages(sources, packages, installations, statesByArtifactId);
1606
- return { sources, packages, installations, statesByArtifactId, workspaceManifest: manifest };
1673
+ return {
1674
+ sources,
1675
+ packages,
1676
+ installations,
1677
+ statesByArtifactId,
1678
+ disabledArtifactIds,
1679
+ workspaceManifest: manifest
1680
+ };
1607
1681
  }
1608
1682
  async function createFullPersonalSnapshot(options, legacyLinks) {
1609
1683
  const capabilities = options.personal?.capabilities ?? await (0, import_devtools_core3.createDefaultCopilotHostCapabilities)();
@@ -1662,20 +1736,29 @@ async function createFullPersonalSnapshot(options, legacyLinks) {
1662
1736
  }
1663
1737
  function fallbackEntries(plugin, packageId) {
1664
1738
  const kinds = Object.keys(plugin.artifacts);
1665
- return kinds.filter((kind) => plugin.artifacts[kind] !== false).map((kind) => ({
1666
- identity: `${packageId}::${kind}:${plugin.id}`,
1667
- artifactId: `${packageId}::${kind}:${plugin.id}`,
1668
- packageId,
1669
- packageDisplayName: plugin.id,
1670
- repositoryId: packageId.split("::")[0] ?? packageId,
1671
- pluginId: plugin.id,
1672
- kind,
1673
- sourcePath: "",
1674
- sourceIsFile: false,
1675
- name: plugin.id,
1676
- digest: "",
1677
- requiresApproval: kind === "hook" || kind === "mcp"
1678
- }));
1739
+ return kinds.filter((kind) => plugin.artifacts[kind] !== false).map((kind) => {
1740
+ const repoId = packageId.split("::")[0] ?? packageId;
1741
+ const identity = (0, import_devtools_core3.buildContentIdentity)({
1742
+ repoId,
1743
+ pluginId: plugin.id,
1744
+ kind,
1745
+ name: plugin.id
1746
+ });
1747
+ return {
1748
+ identity,
1749
+ artifactId: identity,
1750
+ packageId,
1751
+ packageDisplayName: plugin.id,
1752
+ repositoryId: repoId,
1753
+ pluginId: plugin.id,
1754
+ kind,
1755
+ sourcePath: "",
1756
+ sourceIsFile: false,
1757
+ name: plugin.id,
1758
+ digest: "",
1759
+ requiresApproval: kind === "hook" || kind === "mcp"
1760
+ };
1761
+ });
1679
1762
  }
1680
1763
  function toArtifactState(entry, previous) {
1681
1764
  if (!previous) {
@@ -2049,12 +2132,12 @@ var BridgeServer = class {
2049
2132
  input: process.stdin,
2050
2133
  crlfDelay: Number.POSITIVE_INFINITY
2051
2134
  });
2052
- await new Promise((resolve4) => {
2135
+ await new Promise((resolve9) => {
2053
2136
  reader.on("line", (line) => {
2054
2137
  void this.handleLine(line);
2055
2138
  });
2056
2139
  reader.on("close", () => {
2057
- resolve4();
2140
+ resolve9();
2058
2141
  });
2059
2142
  });
2060
2143
  }
@@ -2193,6 +2276,20 @@ var BridgeServer = class {
2193
2276
  this.writeSuccess(request.id, result);
2194
2277
  return;
2195
2278
  }
2279
+ case "copilotContent.detectUnmanaged": {
2280
+ const r = request;
2281
+ if (!this.skillRepoHandler) return this.unsupportedV2(request.id);
2282
+ const result = await this.skillRepoHandler.detectUnmanaged(r.params);
2283
+ this.writeSuccess(request.id, result);
2284
+ return;
2285
+ }
2286
+ case "copilotContent.adoptUnmanaged": {
2287
+ const r = request;
2288
+ if (!this.skillRepoHandler) return this.unsupportedV2(request.id);
2289
+ const result = await this.skillRepoHandler.adoptUnmanaged(r.params);
2290
+ this.writeSuccess(request.id, result);
2291
+ return;
2292
+ }
2196
2293
  case "copilotContent.listLinked":
2197
2294
  case "skillRepo.listLinked": {
2198
2295
  const r = request;
@@ -2603,9 +2700,22 @@ var SkillRepoBridgeHandler = class {
2603
2700
  marks = await new import_devtools_core6.DisabledContentStore().list();
2604
2701
  } catch {
2605
2702
  }
2606
- const isDisabled = (entry) => marks.some(
2607
- (mark) => mark.repoId === entry.repoId && mark.name === entry.name && mark.kind === entry.kind && (mark.scope === "user" || params.workspaceDir !== void 0 && mark.workspaceDir === params.workspaceDir)
2608
- );
2703
+ const declaredDisabled = /* @__PURE__ */ new Set();
2704
+ if (params.workspaceDir) {
2705
+ const manifest = await (0, import_devtools_core6.loadWorkspaceCopilotManifest)(params.workspaceDir).catch(
2706
+ () => void 0
2707
+ );
2708
+ if (manifest) {
2709
+ for (const plugin of manifest.plugins) {
2710
+ for (const target of plugin.disabledArtifacts ?? []) {
2711
+ declaredDisabled.add(
2712
+ `${(0, import_devtools_core6.getWorkspaceManifestPluginSourceId)(manifest, plugin)}|${target}`
2713
+ );
2714
+ }
2715
+ }
2716
+ }
2717
+ }
2718
+ const isDisabled = (entry) => marks.some((mark) => disabledMarkApplies(mark, entry, params.workspaceDir)) || declaredDisabled.has(`${entry.repoId}|${entry.kind}:${entry.name}`);
2609
2719
  return {
2610
2720
  entries: filtered.map((entry) => ({
2611
2721
  ...toBridgeEntry(entry),
@@ -2647,10 +2757,22 @@ var SkillRepoBridgeHandler = class {
2647
2757
  async install(params) {
2648
2758
  const kind = params.kind ?? "skill";
2649
2759
  const source = await this.resolveEntrySource(params.repoId, params.name, kind);
2650
- if ((params.scope ?? "workspace") === "workspace") {
2651
- return this.installThroughDeclaration(params, kind, source);
2652
- }
2653
- return this.installUserScope(params, kind, source);
2760
+ const scope = (params.scope ?? "workspace") === "workspace" ? "workspace" : "user";
2761
+ const store = new import_devtools_core6.DisabledContentStore();
2762
+ await store.removeForArtifact({
2763
+ repoId: params.repoId,
2764
+ name: params.name,
2765
+ kind,
2766
+ scope: "user"
2767
+ });
2768
+ await store.removeForArtifact({
2769
+ repoId: params.repoId,
2770
+ name: params.name,
2771
+ kind,
2772
+ scope,
2773
+ ...scope === "workspace" ? { workspaceDir: params.workspaceDir } : {}
2774
+ });
2775
+ return scope === "workspace" ? this.installThroughDeclaration(params, kind, source) : this.installUserScope(params, kind, source);
2654
2776
  }
2655
2777
  /**
2656
2778
  * Workspace installs go through the shared declaration: pin the
@@ -2672,15 +2794,31 @@ var SkillRepoBridgeHandler = class {
2672
2794
  `Cannot pin repository ${params.repoId}: git returned an invalid commit '${commit}'`
2673
2795
  );
2674
2796
  }
2797
+ const coveringPlugin = await (0, import_devtools_core6.findPluginCoveringArtifact)(repoRoot, kind, params.name);
2675
2798
  await (0, import_devtools_core6.upsertWorkspaceContentSelection)({
2676
2799
  workspaceDir: params.workspaceDir,
2677
2800
  repository: { id: repo.id, url: repo.url, commit },
2678
- pluginId: params.name,
2801
+ pluginId: coveringPlugin ?? params.name,
2679
2802
  kind
2680
2803
  });
2804
+ await this.setDeclaredArtifactDisabled(params, kind, false);
2805
+ if (coveringPlugin && coveringPlugin !== params.name) {
2806
+ await (0, import_devtools_core6.removeWorkspaceContentSelection)({
2807
+ workspaceDir: params.workspaceDir,
2808
+ repositoryId: params.repoId,
2809
+ pluginId: params.name,
2810
+ kind
2811
+ });
2812
+ }
2681
2813
  const restored = await this.reconcileInstalledContent(params.workspaceDir);
2682
2814
  const linkBasename = source.sourceIsFile ? path5.basename(source.sourcePath) : params.name;
2683
- const expectedIdentity = `${params.repoId}::${params.name}::${kind}:${stripAgentSuffix(linkBasename)}`;
2815
+ const declaringPlugin = coveringPlugin ?? params.name;
2816
+ const expectedIdentity = (0, import_devtools_core6.buildContentIdentity)({
2817
+ repoId: params.repoId,
2818
+ pluginId: declaringPlugin,
2819
+ kind,
2820
+ name: stripAgentSuffix(linkBasename)
2821
+ });
2684
2822
  const entry = restored.entries.find((candidate) => candidate.identity === expectedIdentity);
2685
2823
  if (!entry || entry.status !== "restored" && entry.status !== "adopted") {
2686
2824
  throw createServicemeError(
@@ -2784,12 +2922,26 @@ var SkillRepoBridgeHandler = class {
2784
2922
  async uninstall(params) {
2785
2923
  const kind = params.kind ?? "skill";
2786
2924
  if ((params.scope ?? "workspace") === "workspace") {
2925
+ const coveringPlugin = await (0, import_devtools_core6.findPluginCoveringArtifact)(
2926
+ (0, import_devtools_core6.getRepoDir)(params.repoId),
2927
+ kind,
2928
+ params.name
2929
+ );
2787
2930
  await (0, import_devtools_core6.removeWorkspaceContentSelection)({
2788
2931
  workspaceDir: params.workspaceDir,
2789
2932
  repositoryId: params.repoId,
2790
- pluginId: params.name,
2933
+ pluginId: coveringPlugin ?? params.name,
2791
2934
  kind
2792
2935
  });
2936
+ if (coveringPlugin) {
2937
+ await (0, import_devtools_core6.removeWorkspaceContentSelection)({
2938
+ workspaceDir: params.workspaceDir,
2939
+ repositoryId: params.repoId,
2940
+ pluginId: params.name,
2941
+ kind
2942
+ });
2943
+ }
2944
+ await this.setDeclaredArtifactDisabled(params, kind, false);
2793
2945
  await this.reconcileInstalledContent(params.workspaceDir);
2794
2946
  }
2795
2947
  try {
@@ -2809,14 +2961,22 @@ var SkillRepoBridgeHandler = class {
2809
2961
  }
2810
2962
  throw err;
2811
2963
  }
2964
+ await new import_devtools_core6.DisabledContentStore().removeForArtifact({
2965
+ repoId: params.repoId,
2966
+ name: params.name,
2967
+ kind,
2968
+ scope: (params.scope ?? "workspace") === "workspace" ? "workspace" : "user",
2969
+ ...params.workspaceDir ? { workspaceDir: params.workspaceDir } : {}
2970
+ });
2812
2971
  return { removed: true };
2813
2972
  }
2814
2973
  /**
2815
2974
  * Enable/disable a per-artifact skill/agent WITHOUT dropping its
2816
- * installation. Disable records a machine-local mark and removes the
2817
- * materialized link (the workspace manifest declaration stays); the
2818
- * reconciler treats marked identities as undeclared, so a restore
2819
- * never resurrects them. Enable clears the mark and re-links.
2975
+ * installation. Disable records the target in the shared declaration
2976
+ * (`disabledArtifacts` survives reloads and machine state loss) plus
2977
+ * a machine-local mark, and removes the materialized link; the
2978
+ * reconciler treats both as undeclared, so a restore never
2979
+ * resurrects them. Enable clears both and re-links.
2820
2980
  */
2821
2981
  async setEntryEnabled(params) {
2822
2982
  const kind = params.kind ?? "skill";
@@ -2833,8 +2993,10 @@ var SkillRepoBridgeHandler = class {
2833
2993
  ...scope === "workspace" && params.workspaceDir ? { workspaceDir: params.workspaceDir } : {}
2834
2994
  };
2835
2995
  if (params.enabled) {
2996
+ await store.remove({ ...mark, scope: "user" });
2836
2997
  await store.remove(mark);
2837
2998
  if (scope === "workspace") {
2999
+ await this.setDeclaredArtifactDisabled(params, kind, false);
2838
3000
  await this.reconcileInstalledContent(params.workspaceDir);
2839
3001
  } else {
2840
3002
  const source = await this.resolveEntrySource(params.repoId, params.name, kind);
@@ -2846,21 +3008,9 @@ var SkillRepoBridgeHandler = class {
2846
3008
  }
2847
3009
  return { enabled: true };
2848
3010
  }
2849
- await store.add(mark);
2850
3011
  if (scope === "workspace") {
2851
- const stateStore = new import_devtools_core6.WorkspaceContentStateStore({
2852
- workspaceDir: params.workspaceDir
2853
- });
2854
- const state = await stateStore.read();
2855
- for (const previous of state.entries) {
2856
- if (previous.repositoryId !== params.repoId || previous.pluginId !== params.name || previous.kind !== kind || !previous.linkPath.replace(/\\/g, "/").includes(".github")) {
2857
- continue;
2858
- }
2859
- await fs3.rm(path5.resolve(params.workspaceDir, previous.linkPath), {
2860
- force: true,
2861
- recursive: true
2862
- });
2863
- }
3012
+ await store.add(mark);
3013
+ await this.setDeclaredArtifactDisabled(params, kind, true);
2864
3014
  await this.reconcileInstalledContent(params.workspaceDir);
2865
3015
  } else {
2866
3016
  let base = params.name;
@@ -2869,8 +3019,18 @@ var SkillRepoBridgeHandler = class {
2869
3019
  base = source.sourceIsFile ? path5.basename(source.sourcePath) : params.name;
2870
3020
  } catch {
2871
3021
  }
2872
- const userRoot = path5.join((0, import_devtools_core6.getHomeDir)(), ".copilot", kind === "agent" ? "agents" : "skills");
2873
- await fs3.rm(path5.join(userRoot, base), { force: true, recursive: true });
3022
+ const userRoot = path5.resolve(
3023
+ path5.join((0, import_devtools_core6.getHomeDir)(), ".copilot", kind === "agent" ? "agents" : "skills")
3024
+ );
3025
+ const resolvedUserTarget = path5.resolve(path5.join(userRoot, base));
3026
+ if (!resolvedUserTarget.startsWith(userRoot + path5.sep)) {
3027
+ throw createServicemeError(
3028
+ "invalid_params",
3029
+ `User ${kind} link escapes the user root: ${params.name}`
3030
+ );
3031
+ }
3032
+ await store.add(mark);
3033
+ await fs3.rm(resolvedUserTarget, { force: true, recursive: true });
2874
3034
  }
2875
3035
  return { enabled: false };
2876
3036
  }
@@ -2896,6 +3056,136 @@ var SkillRepoBridgeHandler = class {
2896
3056
  }))
2897
3057
  };
2898
3058
  }
3059
+ /**
3060
+ * `copilotContent.detectUnmanaged` — read-only scan of a workspace
3061
+ * that carries Copilot content without a declaration. Provenance
3062
+ * rules live in the core detector (symlink targets, fingerprints,
3063
+ * generated-config identity maps).
3064
+ */
3065
+ async detectUnmanaged(params) {
3066
+ const catalog = await this.buildSkillStore().listAll();
3067
+ return (0, import_devtools_core6.detectUnmanagedWorkspaceContent)({
3068
+ workspaceDir: params.workspaceDir,
3069
+ reposDir: (0, import_devtools_core6.getReposDir)(),
3070
+ catalog: catalog.map((entry) => ({
3071
+ repoId: entry.repoId,
3072
+ name: entry.name,
3073
+ kind: entry.kind,
3074
+ dir: entry.dir,
3075
+ manifestPath: entry.manifestPath
3076
+ })),
3077
+ knownRepoIds: new Set(
3078
+ this.reposStore.list().filter((repo) => repo.enabled).map((repo) => repo.id)
3079
+ )
3080
+ });
3081
+ }
3082
+ /**
3083
+ * `copilotContent.adoptUnmanaged` — initialize the declaration from
3084
+ * detected content. Skill/agent entries reuse the install pipeline
3085
+ * (pin commit → upsert selection → reconcile; the materializer
3086
+ * adopts an already-matching link instead of rewriting it); real
3087
+ * copies are converted to symlinks first when asked. Hook/MCP
3088
+ * integrations are upserted by kind and surface through the normal
3089
+ * approval gate on reconcile. Per-entry failures are reported as
3090
+ * conflict statuses instead of aborting the batch.
3091
+ */
3092
+ async adoptUnmanaged(params) {
3093
+ const failures = [];
3094
+ const pluginManifests = /* @__PURE__ */ new Map();
3095
+ for (const entry of params.entries) {
3096
+ const key = `${entry.repoId}::${entry.name}`;
3097
+ if (pluginManifests.has(key)) continue;
3098
+ try {
3099
+ const manifestPath = path5.join(
3100
+ (0, import_devtools_core6.getRepoDir)(entry.repoId),
3101
+ "plugins",
3102
+ entry.name,
3103
+ "plugin.json"
3104
+ );
3105
+ const raw = await fs3.readFile(manifestPath, "utf8");
3106
+ pluginManifests.set(key, JSON.parse(raw));
3107
+ } catch {
3108
+ }
3109
+ }
3110
+ const { kept, dropped } = (0, import_devtools_core6.dedupeAdoptionsByPluginManifests)(params.entries, pluginManifests);
3111
+ for (const { entry, coveredBy } of dropped) {
3112
+ failures.push({
3113
+ identity: (0, import_devtools_core6.buildContentIdentity)({
3114
+ repoId: entry.repoId,
3115
+ pluginId: entry.name,
3116
+ kind: entry.kind,
3117
+ name: entry.name
3118
+ }),
3119
+ status: "restored",
3120
+ message: `Covered by the plugin ${coveredBy} selection`
3121
+ });
3122
+ }
3123
+ for (const entry of kept) {
3124
+ try {
3125
+ if (entry.convertRealCopy === true) {
3126
+ await this.convertToSymlink({
3127
+ repoId: entry.repoId,
3128
+ name: entry.name,
3129
+ workspaceDir: params.workspaceDir,
3130
+ kind: entry.kind,
3131
+ scope: "workspace"
3132
+ });
3133
+ }
3134
+ await this.install({
3135
+ repoId: entry.repoId,
3136
+ name: entry.name,
3137
+ workspaceDir: params.workspaceDir,
3138
+ kind: entry.kind,
3139
+ scope: "workspace"
3140
+ });
3141
+ } catch (error) {
3142
+ failures.push({
3143
+ identity: (0, import_devtools_core6.buildContentIdentity)({
3144
+ repoId: entry.repoId,
3145
+ pluginId: entry.name,
3146
+ kind: entry.kind,
3147
+ name: entry.name
3148
+ }),
3149
+ status: "conflict",
3150
+ message: error instanceof Error ? error.message : String(error)
3151
+ });
3152
+ }
3153
+ }
3154
+ for (const integration of params.integrations ?? []) {
3155
+ try {
3156
+ const repo = this.reposStore.get(integration.repoId);
3157
+ if (!repo) {
3158
+ throw new Error(`Unknown repository ${integration.repoId}`);
3159
+ }
3160
+ const commit = await this.gitClient.revParseHead((0, import_devtools_core6.getRepoDir)(integration.repoId));
3161
+ if (!/^[0-9a-f]{40}$/.test(commit)) {
3162
+ throw new Error(`Cannot pin ${integration.repoId}: invalid commit '${commit}'`);
3163
+ }
3164
+ await (0, import_devtools_core6.upsertWorkspaceContentSelection)({
3165
+ workspaceDir: params.workspaceDir,
3166
+ repository: { id: repo.id, url: repo.url, commit },
3167
+ pluginId: integration.pluginId,
3168
+ kind: integration.kind
3169
+ });
3170
+ } catch (error) {
3171
+ failures.push({
3172
+ identity: (0, import_devtools_core6.buildContentIdentity)({
3173
+ repoId: integration.repoId,
3174
+ pluginId: integration.pluginId,
3175
+ kind: integration.kind,
3176
+ name: integration.pluginId
3177
+ }),
3178
+ status: "conflict",
3179
+ message: error instanceof Error ? error.message : String(error)
3180
+ });
3181
+ }
3182
+ }
3183
+ const restored = await this.reconcileInstalledContent(params.workspaceDir);
3184
+ return {
3185
+ changed: restored.changed,
3186
+ entries: [...restored.entries, ...failures]
3187
+ };
3188
+ }
2899
3189
  // ─────────────────────────────────────────────────────────────────
2900
3190
  // skillRepo.draft.*
2901
3191
  // ─────────────────────────────────────────────────────────────────
@@ -3150,6 +3440,29 @@ var SkillRepoBridgeHandler = class {
3150
3440
  invalidateSkillStoreCache() {
3151
3441
  this.skillStoreCache = void 0;
3152
3442
  }
3443
+ /**
3444
+ * Mirror an enable/disable toggle into the shared declaration's
3445
+ * `disabledArtifacts` field (same covering-plugin rule the install
3446
+ * pipeline uses). Best-effort for the REPO-side lookups: a missing
3447
+ * repo/checkout must not fail the toggle — the machine-local mark
3448
+ * still applies, and the declaration converges on the next install.
3449
+ */
3450
+ async setDeclaredArtifactDisabled(params, kind, disabled) {
3451
+ const { workspaceDir } = params;
3452
+ if (!workspaceDir) return;
3453
+ let pluginId = params.name;
3454
+ try {
3455
+ pluginId = await (0, import_devtools_core6.findPluginCoveringArtifact)((0, import_devtools_core6.getRepoDir)(params.repoId), kind, params.name) ?? params.name;
3456
+ } catch {
3457
+ }
3458
+ await (0, import_devtools_core6.setWorkspacePluginArtifactsDisabled)({
3459
+ workspaceDir,
3460
+ repositoryId: params.repoId,
3461
+ pluginId,
3462
+ target: `${kind}:${params.name}`,
3463
+ disabled
3464
+ }).catch(() => void 0);
3465
+ }
3153
3466
  /**
3154
3467
  * Resolve an entry's actual on-disk location via `SkillStore` for
3155
3468
  * `install`/`convertToSymlink`. Repos vary wildly in layout — flat
@@ -3189,6 +3502,9 @@ var SkillRepoBridgeHandler = class {
3189
3502
  function stripAgentSuffix(basename4) {
3190
3503
  return basename4.replace(/\.agent\.md$/, "");
3191
3504
  }
3505
+ function disabledMarkApplies(mark, entry, workspaceDir) {
3506
+ return mark.repoId === entry.repoId && mark.name === entry.name && mark.kind === entry.kind && (mark.scope === "user" || workspaceDir !== void 0 && mark.workspaceDir === workspaceDir);
3507
+ }
3192
3508
  function toBridgeEntry(e) {
3193
3509
  return {
3194
3510
  repoId: e.repoId,
@@ -3421,9 +3737,20 @@ var import_devtools_core11 = require("@serviceme/devtools-core");
3421
3737
 
3422
3738
  // src/input.ts
3423
3739
  var fs4 = __toESM(require("fs/promises"));
3740
+ var path6 = __toESM(require("path"));
3741
+ function sanitizeCliFilePath(value) {
3742
+ const hasControlCharacter = [...value].some((character) => {
3743
+ const code = character.charCodeAt(0);
3744
+ return code > 0 && code < 32;
3745
+ });
3746
+ if (hasControlCharacter) {
3747
+ throw new Error("Invalid --file path: control characters are not allowed.");
3748
+ }
3749
+ return path6.resolve(value);
3750
+ }
3424
3751
  async function readCommandInput(options) {
3425
3752
  if (options.filePath) {
3426
- return fs4.readFile(options.filePath, "utf8");
3753
+ return fs4.readFile(sanitizeCliFilePath(options.filePath), "utf8");
3427
3754
  }
3428
3755
  if (options.stdin) {
3429
3756
  return readStdin();
@@ -3701,7 +4028,7 @@ async function runReposCommand(parsed) {
3701
4028
 
3702
4029
  // src/commands/schedule.ts
3703
4030
  var fs5 = __toESM(require("fs"));
3704
- var path6 = __toESM(require("path"));
4031
+ var path7 = __toESM(require("path"));
3705
4032
  var import_devtools_core14 = require("@serviceme/devtools-core");
3706
4033
  async function runScheduleCommand(parsed) {
3707
4034
  const action = parsed.positionals[1];
@@ -3756,6 +4083,9 @@ function requireId(parsed) {
3756
4083
  if (!id) {
3757
4084
  throw createServicemeError("invalid_params", "Missing required flag: --id <task-id>");
3758
4085
  }
4086
+ if (!/^[A-Za-z0-9._-]+$/.test(id)) {
4087
+ throw createServicemeError("invalid_params", "Invalid --id: must match [A-Za-z0-9._-]");
4088
+ }
3759
4089
  return id;
3760
4090
  }
3761
4091
  function requireConfirmation(parsed) {
@@ -3786,6 +4116,16 @@ function parsePayload(parsed, taskType) {
3786
4116
  "invalid_payload",
3787
4117
  "Missing --script for shell task (or use --payload-json)"
3788
4118
  );
4119
+ const hasControlCharacter = [...script].some((character) => {
4120
+ const code = character.charCodeAt(0);
4121
+ return code > 0 && code < 32 && character !== " ";
4122
+ });
4123
+ if (hasControlCharacter) {
4124
+ throw createServicemeError(
4125
+ "invalid_payload",
4126
+ "Invalid --script: control characters are not allowed"
4127
+ );
4128
+ }
3789
4129
  return {
3790
4130
  script,
3791
4131
  cwd: getStringFlag(parsed, "cwd"),
@@ -3902,7 +4242,7 @@ async function handleCreate(parsed) {
3902
4242
  payload,
3903
4243
  workspace: {
3904
4244
  path: wp,
3905
- name: path6.basename(wp) || wp
4245
+ name: path7.basename(wp) || wp
3906
4246
  },
3907
4247
  enabled
3908
4248
  };
@@ -4237,7 +4577,7 @@ function writeDescribe(action) {
4237
4577
  // src/commands/scheduler.ts
4238
4578
  var import_node_child_process = require("child_process");
4239
4579
  var fs6 = __toESM(require("fs"));
4240
- var path7 = __toESM(require("path"));
4580
+ var path8 = __toESM(require("path"));
4241
4581
  var import_devtools_core15 = require("@serviceme/devtools-core");
4242
4582
  var REPO_SYNC_INTERVAL_MS = 5 * 60 * 1e3;
4243
4583
  async function runSchedulerCommand(parsed) {
@@ -4325,12 +4665,16 @@ function handleStart(_parsed) {
4325
4665
  throw createServicemeError("internal_error", "Cannot determine CLI path for daemon spawn");
4326
4666
  }
4327
4667
  const logPath = (0, import_devtools_core15.getSchedulerLogPath)();
4328
- const logDir = path7.dirname(logPath);
4668
+ const homeRoot = path8.resolve((0, import_devtools_core15.getServicemeHome)());
4669
+ if (!path8.resolve(logPath).startsWith(homeRoot + path8.sep)) {
4670
+ throw createServicemeError("internal_error", "Scheduler log path escapes the SERVICEME home");
4671
+ }
4672
+ const logDir = path8.dirname(logPath);
4329
4673
  if (!fs6.existsSync(logDir)) {
4330
4674
  fs6.mkdirSync(logDir, { recursive: true });
4331
4675
  }
4332
4676
  const spawnCmd = process.execPath;
4333
- const spawnArgs = [cliPath, "scheduler", "__daemon", "--logPath", logPath];
4677
+ const spawnArgs = [cliPath, "scheduler", "__daemon", `--logPath=${logPath}`];
4334
4678
  if (process.platform === "win32") {
4335
4679
  fs6.appendFileSync(
4336
4680
  logPath,
@@ -4463,6 +4807,10 @@ function handleLogs2(parsed) {
4463
4807
  async function runDaemon(parsed) {
4464
4808
  const logPath = getStringFlag(parsed, "logPath");
4465
4809
  if (logPath) {
4810
+ const homeRoot = path8.resolve((0, import_devtools_core15.getServicemeHome)());
4811
+ if (!path8.resolve(logPath).startsWith(homeRoot + path8.sep)) {
4812
+ throw createServicemeError("invalid_params", "--logPath must stay inside the SERVICEME home");
4813
+ }
4466
4814
  process.env.SERVICEME_SCHEDULER_LOG_PATH = logPath;
4467
4815
  }
4468
4816
  const pidMgr = new import_devtools_core15.PidManager("", { pidPath: (0, import_devtools_core15.getSchedulerPidPath)() });
@@ -4566,7 +4914,7 @@ function writeDescribe2(action) {
4566
4914
  // src/commands/skill.ts
4567
4915
  var fs7 = __toESM(require("fs/promises"));
4568
4916
  var os2 = __toESM(require("os"));
4569
- var path8 = __toESM(require("path"));
4917
+ var path9 = __toESM(require("path"));
4570
4918
  var import_devtools_core16 = require("@serviceme/devtools-core");
4571
4919
  function normalizeSkillIdOrThrow(store, remoteId) {
4572
4920
  try {
@@ -4583,7 +4931,7 @@ async function runSkillCommand(parsed) {
4583
4931
  }
4584
4932
  const store = new import_devtools_core16.SkillStore({
4585
4933
  workspacePath,
4586
- userSkillsRoot: path8.join(os2.homedir(), ".copilot", "skills")
4934
+ userSkillsRoot: path9.join(os2.homedir(), ".copilot", "skills")
4587
4935
  });
4588
4936
  const catalogClient = new import_devtools_core16.SkillCatalogClient({
4589
4937
  baseUrl: getStringFlag(parsed, "baseUrl")
@@ -4697,11 +5045,24 @@ async function handleUninstall2(store, workspacePath, parsed) {
4697
5045
  throw createServicemeError("invalid_params", "Expected --id <remoteSkillId>.");
4698
5046
  }
4699
5047
  const skillId = normalizeSkillIdOrThrow(store, remoteId);
4700
- await fs7.rm(path8.join(workspacePath, store.getWorkspaceSkillPath(skillId)), {
5048
+ const workspaceRoot = path9.resolve(workspacePath);
5049
+ const workspaceLinkPath = path9.resolve(workspaceRoot, store.getWorkspaceSkillPath(skillId));
5050
+ if (!workspaceLinkPath.startsWith(workspaceRoot + path9.sep)) {
5051
+ throw createServicemeError("invalid_params", `Skill link escapes the workspace: ${skillId}`);
5052
+ }
5053
+ await fs7.rm(workspaceLinkPath, {
4701
5054
  recursive: true,
4702
5055
  force: true
4703
5056
  });
4704
- await fs7.rm(store.getUserSkillPath(skillId), {
5057
+ const userSkillsRoot = path9.resolve(path9.join(os2.homedir(), ".copilot", "skills"));
5058
+ const userSkillPath = path9.resolve(store.getUserSkillPath(skillId));
5059
+ if (!userSkillPath.startsWith(userSkillsRoot + path9.sep)) {
5060
+ throw createServicemeError(
5061
+ "invalid_params",
5062
+ `User skill link escapes the skills root: ${skillId}`
5063
+ );
5064
+ }
5065
+ await fs7.rm(userSkillPath, {
4705
5066
  recursive: true,
4706
5067
  force: true
4707
5068
  });
@@ -4723,7 +5084,7 @@ async function handleMove2(store, workspacePath, parsed) {
4723
5084
  throw createServicemeError("invalid_params", "Expected --to value to be workspace or user.");
4724
5085
  }
4725
5086
  const skillId = normalizeSkillIdOrThrow(store, remoteId);
4726
- const workspacePathForSkill = path8.join(workspacePath, store.getWorkspaceSkillPath(skillId));
5087
+ const workspacePathForSkill = path9.join(workspacePath, store.getWorkspaceSkillPath(skillId));
4727
5088
  const userPathForSkill = store.getUserSkillPath(skillId);
4728
5089
  if (to === "user") {
4729
5090
  await moveDirectory(workspacePathForSkill, userPathForSkill);
@@ -4744,7 +5105,7 @@ async function handleMove2(store, workspacePath, parsed) {
4744
5105
  };
4745
5106
  }
4746
5107
  async function moveDirectory(fromPath, toPath) {
4747
- await fs7.mkdir(path8.dirname(toPath), { recursive: true });
5108
+ await fs7.mkdir(path9.dirname(toPath), { recursive: true });
4748
5109
  try {
4749
5110
  await fs7.rename(fromPath, toPath);
4750
5111
  } catch {
@@ -4850,14 +5211,14 @@ async function handlePublishable(store, workspacePath) {
4850
5211
  skills: workspaceSkillIds.map((skillId) => ({
4851
5212
  id: skillId,
4852
5213
  displayName: skillId,
4853
- path: path8.join(workspacePath, store.getWorkspaceSkillPath(skillId))
5214
+ path: path9.join(workspacePath, store.getWorkspaceSkillPath(skillId))
4854
5215
  }))
4855
5216
  };
4856
5217
  }
4857
5218
 
4858
5219
  // src/commands/skills.ts
4859
5220
  var fs8 = __toESM(require("fs/promises"));
4860
- var path9 = __toESM(require("path"));
5221
+ var path10 = __toESM(require("path"));
4861
5222
  var import_devtools_core17 = require("@serviceme/devtools-core");
4862
5223
  function requireFlag(parsed, name) {
4863
5224
  const value = getStringFlag(parsed, name);
@@ -4929,12 +5290,14 @@ async function readDraftFilesFromDir(dirPath) {
4929
5290
  }
4930
5291
  const files = [];
4931
5292
  let manifestFound = false;
5293
+ const dirRoot = path10.resolve(dirPath);
4932
5294
  async function walk(currentAbs, currentRel) {
4933
5295
  const entries = await fs8.readdir(currentAbs, { withFileTypes: true });
4934
5296
  for (const entry of entries) {
4935
5297
  if (entry.name.startsWith(".")) continue;
4936
5298
  if (entry.name === "node_modules") continue;
4937
- const childAbs = path9.join(currentAbs, entry.name);
5299
+ const childAbs = path10.resolve(currentAbs, entry.name);
5300
+ if (!childAbs.startsWith(dirRoot + path10.sep)) continue;
4938
5301
  const childRel = currentRel ? `${currentRel}/${entry.name}` : entry.name;
4939
5302
  if (entry.isSymbolicLink()) continue;
4940
5303
  if (entry.isDirectory()) {
@@ -4949,7 +5312,7 @@ async function readDraftFilesFromDir(dirPath) {
4949
5312
  files.push({ path: childRel, content });
4950
5313
  }
4951
5314
  }
4952
- await walk(dirPath, "");
5315
+ await walk(dirRoot, "");
4953
5316
  if (!manifestFound) {
4954
5317
  throw createServicemeError(
4955
5318
  "invalid_params",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@serviceme/devtools-cli",
3
- "version": "2.0.0",
3
+ "version": "2.0.1",
4
4
  "description": "Unified SERVICEME CLI for automation, project scaffolding, and bridge-based tooling.",
5
5
  "license": "SEE LICENSE IN LICENSE.md",
6
6
  "repository": {
@@ -39,9 +39,9 @@
39
39
  "registry": "https://registry.npmjs.org/"
40
40
  },
41
41
  "dependencies": {
42
- "@serviceme/devtools-core": "2.0.0",
43
- "@serviceme/devtools-protocol": "2.0.0",
44
- "@serviceme/devtools-shared": "2.0.0"
42
+ "@serviceme/devtools-core": "2.0.1",
43
+ "@serviceme/devtools-protocol": "2.0.1",
44
+ "@serviceme/devtools-shared": "2.0.1"
45
45
  },
46
46
  "devDependencies": {
47
47
  "@types/node": "^24",