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 +8 -0
- package/dist/index.js +8 -9
- package/dist/installer.js +66 -75
- package/package.json +1 -1
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
|
|
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/
|
|
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
|
|
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
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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 =
|
|
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
|
|
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.
|
|
62
|
+
const pluginRoot = path.resolve(resolvedSource.root, plugin.source);
|
|
63
63
|
await assertDirectory(pluginRoot, `Plugin "${plugin.name}" points to missing directory "${plugin.source}".`);
|
|
64
|
-
|
|
64
|
+
candidates.push({ plugin, pluginRoot });
|
|
65
65
|
}
|
|
66
|
-
|
|
67
|
-
|
|
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(
|
|
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.
|
|
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(`
|
|
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
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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
|
-
|
|
156
|
+
return null;
|
|
169
157
|
}
|
|
170
|
-
function
|
|
171
|
-
const
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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
|
}
|