copilot-ship 1.0.0-beta.1 → 1.0.0-beta.2

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/constants.js CHANGED
@@ -3,6 +3,14 @@ import path from "node:path";
3
3
  export const MARKETPLACE_MANIFEST = path.join(".claude-plugin", "marketplace.json");
4
4
  export const PROJECT_INSTALL_ROOT = ".github";
5
5
  export const GLOBAL_INSTALL_ROOT = path.join(os.homedir(), ".copilot");
6
+ // VS Code plugin.json discovery order — checked at the plugin root (no directory walk-up).
7
+ // Source: https://code.visualstudio.com/docs/copilot/customization/agent-plugins
8
+ export const PLUGIN_JSON_CANDIDATES = [
9
+ path.join(".plugin", "plugin.json"),
10
+ "plugin.json",
11
+ path.join(".github", "plugin", "plugin.json"),
12
+ path.join(".claude-plugin", "plugin.json"),
13
+ ];
6
14
  export const DIRECTORY_TARGETS = {
7
15
  agents: "agents",
8
16
  hooks: "hooks",
package/dist/index.js CHANGED
@@ -33,17 +33,17 @@ async function runAdd(parsedArgs) {
33
33
  activity.start("Resolving source and preparing install plan");
34
34
  let result;
35
35
  try {
36
- result = await installFromMarketplace(parsedArgs.source, parsedArgs.flags, async (plugins) => {
37
- activity.stop("Marketplace loaded");
36
+ result = await installFromMarketplace(parsedArgs.source, parsedArgs.flags, async (plugins, kind) => {
37
+ activity.stop(kind === "plugin" ? "Plugin loaded" : "Marketplace loaded");
38
38
  let selected = plugins;
39
39
  if (parsedArgs.flags.pluginName) {
40
40
  const plugin = plugins.find((candidate) => candidate.plugin.name === parsedArgs.flags.pluginName);
41
41
  if (!plugin) {
42
- throw new Error(`Plugin "${parsedArgs.flags.pluginName}" was not found in the selected marketplace scope.`);
42
+ throw new Error(`Plugin "${parsedArgs.flags.pluginName}" was not found in the selected scope.`);
43
43
  }
44
44
  selected = [plugin];
45
45
  }
46
- else if (!parsedArgs.flags.installAll && plugins.length > 1) {
46
+ else if (kind === "marketplace" && !parsedArgs.flags.installAll && plugins.length > 1) {
47
47
  if (!process.stdin.isTTY || !process.stdout.isTTY) {
48
48
  throw new Error("Interactive plugin selection requires a TTY. Use --plugin or --all instead.");
49
49
  }
@@ -182,11 +182,10 @@ Usage:
182
182
  copilot-ship list [-g]
183
183
 
184
184
  Sources:
185
- owner/repo
186
- https://github.com/owner/repo
187
- https://github.com/owner/repo/tree/main/plugins/my-plugin
188
- git@github.com:owner/repo.git
189
- ./local-path
185
+ owner/repo marketplace
186
+ https://github.com/owner/repo marketplace
187
+ https://github.com/owner/repo/tree/main/my-plugin single plugin
188
+ git@github.com:owner/repo.git marketplace
190
189
  `);
191
190
  }
192
191
  function resultTargetDescription(scope) {
package/dist/installer.js CHANGED
@@ -3,20 +3,12 @@ import { access, copyFile, mkdir, mkdtemp, readdir, readFile, rm } from "node:fs
3
3
  import os from "node:os";
4
4
  import path from "node:path";
5
5
  import { promisify } from "node:util";
6
- import { COPILOT_INSTRUCTIONS_FILE, DIRECTORY_TARGETS, GLOBAL_INSTALL_ROOT, MARKETPLACE_MANIFEST, PROJECT_INSTALL_ROOT, } from "./constants.js";
6
+ import { COPILOT_INSTRUCTIONS_FILE, DIRECTORY_TARGETS, GLOBAL_INSTALL_ROOT, MARKETPLACE_MANIFEST, PLUGIN_JSON_CANDIDATES, PROJECT_INSTALL_ROOT, } from "./constants.js";
7
7
  const execFileAsync = promisify(execFile);
8
8
  export async function resolveSource(source) {
9
- const localPath = await tryResolveLocalPath(source);
10
- if (localPath) {
11
- return {
12
- cleanup: undefined,
13
- marketplaceRoot: await findMarketplaceRoot(localPath),
14
- requestedPath: localPath,
15
- };
16
- }
17
9
  const parsedGitHubSource = parseGitHubSource(source);
18
10
  if (!parsedGitHubSource) {
19
- throw new Error(`Unsupported source "${source}". Use a local path, owner/repo shorthand, a GitHub URL, or a git@github.com URL.`);
11
+ throw new Error(`Unsupported source "${source}". Use owner/repo shorthand or a GitHub URL.`);
20
12
  }
21
13
  const tempRoot = await createTempDirectory();
22
14
  const checkoutRoot = path.join(tempRoot, "source");
@@ -32,43 +24,49 @@ export async function resolveSource(source) {
32
24
  await rm(tempRoot, { force: true, recursive: true });
33
25
  throw wrapExecError(`Failed to clone ${parsedGitHubSource.cloneUrl}`, error);
34
26
  }
35
- const requestedPath = path.resolve(checkoutRoot, parsedGitHubSource.subpath ?? ".");
36
- try {
37
- await access(requestedPath);
38
- }
39
- catch {
40
- await rm(tempRoot, { force: true, recursive: true });
41
- throw new Error(`Resolved source path "${parsedGitHubSource.subpath ?? "."}" does not exist in ${parsedGitHubSource.cloneUrl}.`);
27
+ const cleanup = async () => rm(tempRoot, { force: true, recursive: true });
28
+ if (parsedGitHubSource.subpath) {
29
+ const pluginRoot = path.resolve(checkoutRoot, parsedGitHubSource.subpath);
30
+ try {
31
+ await access(pluginRoot);
32
+ }
33
+ catch {
34
+ await cleanup();
35
+ throw new Error(`Path "${parsedGitHubSource.subpath}" does not exist in ${parsedGitHubSource.cloneUrl}.`);
36
+ }
37
+ const pluginJsonPath = await findPluginJsonAtRoot(pluginRoot);
38
+ if (!pluginJsonPath) {
39
+ await cleanup();
40
+ throw new Error(`No plugin.json found at "${parsedGitHubSource.subpath}" in ${parsedGitHubSource.cloneUrl}. ` +
41
+ `Expected one of: ${PLUGIN_JSON_CANDIDATES.join(", ")}.`);
42
+ }
43
+ return { kind: "plugin", root: pluginRoot, cleanup };
42
44
  }
43
- return {
44
- cleanup: async () => rm(tempRoot, { force: true, recursive: true }),
45
- marketplaceRoot: await findMarketplaceRoot(requestedPath),
46
- requestedPath,
47
- };
45
+ return { kind: "marketplace", root: checkoutRoot, cleanup };
48
46
  }
49
47
  export async function installFromMarketplace(source, flags, selectPlugins) {
50
48
  const resolvedSource = await resolveSource(source);
51
49
  try {
52
- const manifest = await readMarketplaceManifest(resolvedSource.marketplaceRoot);
50
+ const manifest = resolvedSource.kind === "marketplace"
51
+ ? await readMarketplaceManifest(resolvedSource.root)
52
+ : await readPluginJson(resolvedSource.root);
53
53
  const warnings = [];
54
54
  const skippedPlugins = [];
55
- const localCandidates = [];
55
+ const candidates = [];
56
56
  for (const plugin of manifest.plugins) {
57
57
  if (typeof plugin.source !== "string") {
58
58
  skippedPlugins.push(plugin.name);
59
59
  warnings.push(`Skipped plugin "${plugin.name}": external marketplace sources are deferred to MVP2.`);
60
60
  continue;
61
61
  }
62
- const pluginRoot = path.resolve(resolvedSource.marketplaceRoot, plugin.source);
62
+ const pluginRoot = path.resolve(resolvedSource.root, plugin.source);
63
63
  await assertDirectory(pluginRoot, `Plugin "${plugin.name}" points to missing directory "${plugin.source}".`);
64
- localCandidates.push({ plugin, pluginRoot });
64
+ candidates.push({ plugin, pluginRoot });
65
65
  }
66
- const scopedCandidates = narrowCandidatesByRequestedPath(localCandidates, resolvedSource.requestedPath);
67
- const candidatePool = scopedCandidates.length > 0 ? scopedCandidates : localCandidates;
68
- if (candidatePool.length === 0) {
69
- throw new Error("No installable plugins were found in the marketplace.");
66
+ if (candidates.length === 0) {
67
+ throw new Error("No installable plugins were found.");
70
68
  }
71
- const selectedCandidates = await selectPlugins(candidatePool);
69
+ const selectedCandidates = await selectPlugins(candidates, resolvedSource.kind);
72
70
  if (selectedCandidates.length === 0) {
73
71
  throw new Error("No plugins were selected.");
74
72
  }
@@ -93,7 +91,7 @@ export async function installFromMarketplace(source, flags, selectPlugins) {
93
91
  return {
94
92
  installedPlugins: installablePlans,
95
93
  skippedPlugins,
96
- sourceRoot: resolvedSource.marketplaceRoot,
94
+ sourceRoot: resolvedSource.root,
97
95
  targetRoot,
98
96
  warnings,
99
97
  };
@@ -134,7 +132,7 @@ async function readMarketplaceManifest(marketplaceRoot) {
134
132
  raw = await readFile(manifestPath, "utf8");
135
133
  }
136
134
  catch {
137
- throw new Error(`MVP1 requires a marketplace manifest at "${manifestPath}".`);
135
+ throw new Error(`No marketplace manifest found. Expected "${MARKETPLACE_MANIFEST}" at the repository root.`);
138
136
  }
139
137
  let parsed;
140
138
  try {
@@ -148,31 +146,44 @@ async function readMarketplaceManifest(marketplaceRoot) {
148
146
  }
149
147
  return parsed;
150
148
  }
151
- async function findMarketplaceRoot(startingPath) {
152
- let currentPath = path.resolve(startingPath);
153
- const stats = await readdirOrNull(currentPath);
154
- if (stats === null) {
155
- currentPath = path.dirname(currentPath);
156
- }
157
- while (true) {
158
- const candidate = path.join(currentPath, MARKETPLACE_MANIFEST);
159
- if (await pathExists(candidate)) {
160
- return currentPath;
161
- }
162
- const parent = path.dirname(currentPath);
163
- if (parent === currentPath) {
164
- break;
149
+ async function findPluginJsonAtRoot(pluginRoot) {
150
+ for (const candidate of PLUGIN_JSON_CANDIDATES) {
151
+ const fullPath = path.join(pluginRoot, candidate);
152
+ if (await pathExists(fullPath)) {
153
+ return fullPath;
165
154
  }
166
- currentPath = parent;
167
155
  }
168
- throw new Error(`MVP1 requires a marketplace manifest. None was found from "${startingPath}" upward.`);
156
+ return null;
169
157
  }
170
- function narrowCandidatesByRequestedPath(candidates, requestedPath) {
171
- const resolvedRequestedPath = path.resolve(requestedPath);
172
- return candidates.filter((candidate) => {
173
- const pluginRoot = path.resolve(candidate.pluginRoot);
174
- return isSamePath(pluginRoot, resolvedRequestedPath) || isDescendantOf(resolvedRequestedPath, pluginRoot);
175
- });
158
+ async function readPluginJson(pluginRoot) {
159
+ const pluginJsonPath = await findPluginJsonAtRoot(pluginRoot);
160
+ if (!pluginJsonPath) {
161
+ throw new Error(`No plugin.json found at the specified path. Expected one of: ${PLUGIN_JSON_CANDIDATES.join(", ")}.`);
162
+ }
163
+ let raw;
164
+ try {
165
+ raw = await readFile(pluginJsonPath, "utf8");
166
+ }
167
+ catch {
168
+ throw new Error(`Failed to read plugin.json at "${pluginJsonPath}".`);
169
+ }
170
+ let parsed;
171
+ try {
172
+ parsed = JSON.parse(raw);
173
+ }
174
+ catch (error) {
175
+ throw wrapExecError(`Failed to parse plugin.json at "${pluginJsonPath}"`, error);
176
+ }
177
+ if (typeof parsed !== "object" || parsed === null) {
178
+ throw new Error(`plugin.json at "${pluginJsonPath}" must be a JSON object.`);
179
+ }
180
+ const record = parsed;
181
+ const name = typeof record.name === "string" ? record.name : path.basename(pluginRoot);
182
+ const plugin = { name, source: "." };
183
+ if (typeof record.description === "string") {
184
+ plugin.description = record.description;
185
+ }
186
+ return { plugins: [plugin] };
176
187
  }
177
188
  async function discoverArtifacts(pluginRoot, targetRoot) {
178
189
  const entries = [];
@@ -276,19 +287,6 @@ async function readdirOrNull(targetPath) {
276
287
  return null;
277
288
  }
278
289
  }
279
- async function tryResolveLocalPath(source) {
280
- const expandedSource = source.startsWith("~/") ? path.join(os.homedir(), source.slice(2)) : source;
281
- const shouldTreatAsPath = expandedSource.startsWith(".") || expandedSource.startsWith("/") || expandedSource.startsWith("~");
282
- if (!shouldTreatAsPath) {
283
- const candidate = path.resolve(expandedSource);
284
- if (!(await pathExists(candidate))) {
285
- return null;
286
- }
287
- return candidate;
288
- }
289
- const resolved = path.resolve(expandedSource);
290
- return (await pathExists(resolved)) ? resolved : null;
291
- }
292
290
  function parseGitHubSource(source) {
293
291
  const shorthandMatch = source.match(/^([A-Za-z0-9_.-]+)\/([A-Za-z0-9_.-]+)$/);
294
292
  if (shorthandMatch) {
@@ -350,13 +348,6 @@ function isMarketplacePlugin(value) {
350
348
  const record = value;
351
349
  return typeof record.name === "string" && "source" in record;
352
350
  }
353
- function isSamePath(left, right) {
354
- return path.resolve(left) === path.resolve(right);
355
- }
356
- function isDescendantOf(child, parent) {
357
- const relative = path.relative(parent, child);
358
- return relative !== "" && !relative.startsWith("..") && !path.isAbsolute(relative);
359
- }
360
351
  async function createTempDirectory() {
361
352
  return mkdtemp(path.join(os.tmpdir(), "copilot-ship-"));
362
353
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "copilot-ship",
3
- "version": "1.0.0-beta.1",
3
+ "version": "1.0.0-beta.2",
4
4
  "description": "Ship GitHub Copilot plugin artifacts into canonical Copilot directories.",
5
5
  "type": "module",
6
6
  "bin": {