@serviceme/devtools-core 2.0.5 → 2.0.6

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
@@ -781,6 +781,31 @@ function isDirectLinkRoot(workspaceDir) {
781
781
  * suffix convention for agent files. File entries keep the SOURCE basename
782
782
  * (flat agents must carry the `.agent.md` suffix to be recognized).
783
783
  *
784
+ * Plugins that declare NO `extensions` (agent-plugins.org convention
785
+ * shape, e.g. cloud-arch-diagram-plugin) keep ALL their content inside
786
+ * the plugin dir under the same top-level conventions (`skills/`,
787
+ * `agents/*.agent.md`). The schema-less served manifest makes VS Code
788
+ * convention-scan exactly those paths, so they are projected wholesale
789
+ * (see copyConventionContent) even though nothing references them —
790
+ * without this such a plugin loads as an empty shell (manifest +
791
+ * mcp.json only).
792
+ *
793
+ * FORMAT DISCOVERY (verified in the VS Code workbench bundle): the
794
+ * format is picked from the LAYOUT, in this order — (1) root plugin.json
795
+ * with an agent-plugins.org `$schema` → format 3 (spec component paths,
796
+ * agents under `com.github.copilot/agents/`); (2) `.plugin/plugin.json`
797
+ * present → format 2 (Claude-style); (3) `.claude-plugin/plugin.json`
798
+ * → format 1; (4) manifest-less fallback → format 0. Our projection is
799
+ * always format 2: schema-less root + `.plugin/plugin.json`. Format 2
800
+ * convention paths are the TOP-LEVEL dirs — `commands/`, `skills/`,
801
+ * `agents/`, `rules/`, `automations/`, `hooks/hooks.json` — and MCP
802
+ * servers come from the `.mcp.json` FILE (dot-prefixed) or a
803
+ * `mcpServers` manifest field; bare `mcp.json` is a format-3-only path
804
+ * and would silently register NOTHING under format 2. `scripts/` is no
805
+ * discovery dir but is runtime support for MCP/hook commands: stdio
806
+ * servers run with the plugin root as defaultCwd and `${PLUGIN_ROOT}`
807
+ * resolves to the projection root, so their scripts must be present.
808
+ *
784
809
  * The served `plugin.json` is deliberately SCHEMA-LESS and identity-only
785
810
  * (see SERVED_SPEC_FIELDS): a schema-bearing manifest switches VS Code to
786
811
  * the Agent Plugins spec component paths, which read agents from
@@ -897,6 +922,41 @@ function servedPluginManifest(raw) {
897
922
  return served;
898
923
  }
899
924
  /**
925
+ * Copy the convention-based content directories VS Code recognizes for a
926
+ * schema-less (format 2, `.plugin/`-manifest) local plugin: the top-level
927
+ * `skills/`, `agents/`, `commands/`, `rules/` and `automations/` trees
928
+ * plus `hooks/` (whose `hooks/hooks.json` format 2 reads directly).
929
+ * `scripts/` is copied too — not a discovery dir, but the runtime support
930
+ * location MCP/hook commands execute from (defaultCwd = plugin root,
931
+ * `${PLUGIN_ROOT}` → projection root). Plugins following the
932
+ * agent-plugins.org convention carry ALL their content in these dirs with
933
+ * no `extensions` composition in plugin.json — and the schema-less served
934
+ * manifest routes VS Code to a convention scan of exactly these paths, so
935
+ * a projection without them has zero capabilities.
936
+ *
937
+ * Runs BEFORE the `extensions` loop: declared entries re-copy over with
938
+ * plugin-dir-first source resolution, so an explicitly declared entry at
939
+ * a convention path converges to the same bytes instead of being clobbered
940
+ * by a repo-root composition source.
941
+ */
942
+ const CONVENTION_DIRS = [
943
+ "skills",
944
+ "agents",
945
+ "commands",
946
+ "rules",
947
+ "automations",
948
+ "hooks",
949
+ "scripts"
950
+ ];
951
+ async function copyConventionContent(pluginDir, targetDir, result) {
952
+ for (const dir of CONVENTION_DIRS) {
953
+ const source = path.join(pluginDir, dir);
954
+ if (!(await fs$1.stat(source).catch(() => void 0))?.isDirectory()) continue;
955
+ await copyProjectionEntry(source, path.join(targetDir, dir));
956
+ if (!result.entries.includes(dir)) result.entries.push(dir);
957
+ }
958
+ }
959
+ /**
900
960
  * Build (or refresh) the loadable projection at `targetDir`. Idempotent:
901
961
  * the previous projection is replaced wholesale so re-registering after a
902
962
  * checkout pull re-points every entry. Returns `materialized: false` when
@@ -922,6 +982,7 @@ async function materializePluginProjection(input) {
922
982
  recursive: true
923
983
  });
924
984
  await fs$1.mkdir(targetDir, { recursive: true });
985
+ await copyConventionContent(pluginDir, targetDir, result);
925
986
  for (const ref of collectRefs(raw.extensions)) {
926
987
  const source = await resolveSource(ref.entry, pluginDir, repoRoot);
927
988
  if (!source) {
@@ -950,7 +1011,10 @@ async function materializePluginProjection(input) {
950
1011
  const pluginMarkerDir = inside(PLUGIN_MARKER_DIR);
951
1012
  await fs$1.mkdir(pluginMarkerDir, { recursive: true });
952
1013
  await fs$1.writeFile(path.join(pluginMarkerDir, "plugin.json"), `${JSON.stringify(servedPluginManifest(raw), null, 2)}\n`);
953
- if ((await fs$1.stat(path.join(pluginDir, "mcp.json")).catch(() => void 0))?.isFile()) await copyProjectionEntry(path.join(pluginDir, "mcp.json"), inside("mcp.json"));
1014
+ if ((await fs$1.stat(path.join(pluginDir, "mcp.json")).catch(() => void 0))?.isFile()) {
1015
+ await copyProjectionEntry(path.join(pluginDir, "mcp.json"), inside("mcp.json"));
1016
+ await copyProjectionEntry(path.join(pluginDir, "mcp.json"), inside(".mcp.json"));
1017
+ }
954
1018
  const marker = {
955
1019
  servicemeMaterialized: true,
956
1020
  pluginDir,
@@ -2909,7 +2973,7 @@ async function resolveWorkspaceContentPlan(input) {
2909
2973
  */
2910
2974
  async function resolvePluginEntriesLenient(input) {
2911
2975
  const namespace = input.manifest.extensions[AWESOME_COPILOT_NAMESPACE];
2912
- if (!namespace || typeof namespace !== "object") return resolveConventionEntries(input.repoRoot, path.join(input.repoRoot, "plugins", input.pluginId), input.repositoryId, input.pluginId, void 0, input.manifest);
2976
+ if (!namespace || typeof namespace !== "object") return resolveConventionEntries(path.join(input.repoRoot, "plugins", input.pluginId), input.repositoryId, input.pluginId, void 0, input.manifest);
2913
2977
  const entries = [];
2914
2978
  for (const [kind, manifestKey] of Object.entries(PATH_KINDS)) {
2915
2979
  if (kind === "mcp" || kind === "hook") continue;
@@ -2946,16 +3010,18 @@ function selectedKind(artifacts, kind) {
2946
3010
  * skills/<name>/SKILL.md · agents/<name>.agent.md ·
2947
3011
  * rules/<name>.instructions.md · commands/<name>.prompt.md ·
2948
3012
  * hooks/hooks.json
2949
- * Checked plugin-dir-relative first, then repo-root. Used when a
2950
- * manifest carries no `com.github.awesome-copilot` namespace at all.
3013
+ * Scoped to the plugin dir ONLY. Repo-root content is never attributed
3014
+ * to the package: awesome-copilot style root layouts are covered by the
3015
+ * declared-path branch (namespace manifests), and the legacy no-manifest
3016
+ * flow has its own plugin-id-named root lookups. Used when a manifest
3017
+ * carries no `com.github.awesome-copilot` namespace at all.
2951
3018
  */
2952
- async function resolveConventionEntries(repoRoot, pluginDir, repositoryId, pluginId, artifacts, manifest) {
3019
+ async function resolveConventionEntries(pluginDir, repositoryId, pluginId, artifacts, manifest) {
2953
3020
  const entries = [];
2954
- const seen = /* @__PURE__ */ new Set();
2955
3021
  const wants = artifacts === void 0 || Object.keys(artifacts).length === 0 ? () => true : (kind) => artifacts[kind] === true;
2956
- const push = async (kind, base, subdir, options = {}) => {
3022
+ const push = async (kind, subdir, options = {}) => {
2957
3023
  if (!wants(kind)) return;
2958
- const dir = path.join(base, subdir);
3024
+ const dir = path.join(pluginDir, subdir);
2959
3025
  const dirents = await fs$1.readdir(dir, { withFileTypes: true }).catch(() => []);
2960
3026
  for (const dirent of dirents) {
2961
3027
  let sourcePath;
@@ -2964,27 +3030,18 @@ async function resolveConventionEntries(repoRoot, pluginDir, repositoryId, plugi
2964
3030
  sourcePath = path.join(dir, dirent.name);
2965
3031
  } else if (options.files !== false && dirent.isFile()) sourcePath = path.join(dir, dirent.name);
2966
3032
  else continue;
2967
- if (seen.has(sourcePath)) continue;
2968
- seen.add(sourcePath);
2969
3033
  entries.push(await buildEntry(repositoryId, pluginId, kind, sourcePath, !options.dirs, manifest));
2970
3034
  }
2971
3035
  };
2972
- for (const base of [pluginDir, repoRoot]) {
2973
- await push("skill", base, "skills", {
2974
- dirs: true,
2975
- files: false
2976
- });
2977
- await push("agent", base, "agents");
2978
- await push("instruction", base, "rules");
2979
- await push("prompt", base, "commands");
2980
- await push("hook", base, "hooks");
2981
- }
2982
- const byTarget = /* @__PURE__ */ new Map();
2983
- for (const entry of entries) {
2984
- const key = `${entry.kind}:${entry.name}`;
2985
- if (!byTarget.has(key)) byTarget.set(key, entry);
2986
- }
2987
- return [...byTarget.values()];
3036
+ await push("skill", "skills", {
3037
+ dirs: true,
3038
+ files: false
3039
+ });
3040
+ await push("agent", "agents");
3041
+ await push("instruction", "rules");
3042
+ await push("prompt", "commands");
3043
+ await push("hook", "hooks");
3044
+ return entries;
2988
3045
  }
2989
3046
  /**
2990
3047
  * Repo-root-relative source for a declared plugin path, applying the
@@ -3005,7 +3062,7 @@ async function resolveDeclaredRelative(repoRoot, relative) {
3005
3062
  }
3006
3063
  async function resolvePluginManifestEntries(repoRoot, repositoryId, pluginId, artifacts, pluginManifest) {
3007
3064
  const namespace = pluginManifest.extensions[AWESOME_COPILOT_NAMESPACE];
3008
- if (!namespace || typeof namespace !== "object") return resolveConventionEntries(repoRoot, path.join(repoRoot, "plugins", pluginId), repositoryId, pluginId, artifacts, pluginManifest);
3065
+ if (!namespace || typeof namespace !== "object") return resolveConventionEntries(path.join(repoRoot, "plugins", pluginId), repositoryId, pluginId, artifacts, pluginManifest);
3009
3066
  const entries = [];
3010
3067
  for (const [kind, manifestKey] of Object.entries(PATH_KINDS)) {
3011
3068
  if (!selectedKind(artifacts, kind)) continue;