context-plugins 0.3.0 → 0.4.0
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/README.md +31 -0
- package/bin/cli.js +5 -0
- package/package.json +1 -1
- package/run.js +1 -0
- package/src/catalog.js +32 -1
- package/src/cli.js +33 -0
- package/src/doctor.js +171 -0
- package/src/harness/claude.js +33 -6
- package/src/harness/cursor.js +3 -1
- package/src/harness/vscode.js +3 -1
- package/src/install.js +26 -7
- package/src/require-node.js +23 -0
package/README.md
CHANGED
|
@@ -23,8 +23,39 @@ context-plugins uninstall <plugin> [options] # remove it again
|
|
|
23
23
|
context-plugins update # refresh everything already installed
|
|
24
24
|
context-plugins list # what the marketplace offers
|
|
25
25
|
context-plugins installed # what this machine has
|
|
26
|
+
context-plugins doctor # check this machine can install
|
|
26
27
|
```
|
|
27
28
|
|
|
29
|
+
## Something not working?
|
|
30
|
+
|
|
31
|
+
`doctor` checks everything an install depends on and says which part is unhappy:
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
$ npx context-plugins doctor
|
|
35
|
+
|
|
36
|
+
Environment
|
|
37
|
+
✓ Node.js v20.11.0
|
|
38
|
+
✓ git git version 2.43.0
|
|
39
|
+
|
|
40
|
+
Editors
|
|
41
|
+
✓ Claude Code claude on PATH
|
|
42
|
+
✓ Cursor ~/.cursor
|
|
43
|
+
! VS Code not installed (looked in ~/.config/Code/User)
|
|
44
|
+
|
|
45
|
+
Marketplace
|
|
46
|
+
✓ Reachable raw.githubusercontent.com
|
|
47
|
+
✓ Registry context-plugins, 44 plugins
|
|
48
|
+
✓ API budget 59 of 60 requests left
|
|
49
|
+
|
|
50
|
+
Local state
|
|
51
|
+
✓ State directory ~/.context-plugins (writable)
|
|
52
|
+
✓ Installed 2 plugins
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
`✓` fine, `!` works but worth knowing, `x` blocks an install. It exits non-zero
|
|
56
|
+
only when something blocks, so it can be used in a script; add `--json` for machine-readable
|
|
57
|
+
output.
|
|
58
|
+
|
|
28
59
|
| Option | Default | Description |
|
|
29
60
|
| --- | --- | --- |
|
|
30
61
|
| `--repo <owner/repo>` | the bundled marketplace | Install from a different marketplace |
|
package/bin/cli.js
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
'use strict';
|
|
3
3
|
|
|
4
|
+
// Checked before anything else loads. Node below 18 has no global fetch and no
|
|
5
|
+
// node:readline/promises, so without this the failure is a bare
|
|
6
|
+
// "fetch is not defined" from somewhere deep in the fetch path.
|
|
7
|
+
require('../src/require-node')();
|
|
8
|
+
|
|
4
9
|
require('../src/cli')
|
|
5
10
|
.run(process.argv.slice(2))
|
|
6
11
|
.then((code) => {
|
package/package.json
CHANGED
package/run.js
CHANGED
package/src/catalog.js
CHANGED
|
@@ -6,6 +6,9 @@ const { UserError, assertRepo, assertRef, stripBom, suggest } = require('./util'
|
|
|
6
6
|
// We prefer the Claude one and fall back to Cursor's.
|
|
7
7
|
const REGISTRY_FILES = ['.claude-plugin/marketplace.json', '.cursor-plugin/marketplace.json'];
|
|
8
8
|
|
|
9
|
+
// Claude Code's marketplace schema: kebab-case identifier, no spaces.
|
|
10
|
+
const MARKETPLACE_RE = /^[a-z0-9]+(?:[-_.][a-z0-9]+)*$/i;
|
|
11
|
+
|
|
9
12
|
const rawUrl = (repo, ref, filePath) =>
|
|
10
13
|
`https://raw.githubusercontent.com/${repo}/${ref}/${filePath}`;
|
|
11
14
|
|
|
@@ -16,13 +19,29 @@ function ghHeaders(env = process.env) {
|
|
|
16
19
|
return headers;
|
|
17
20
|
}
|
|
18
21
|
|
|
22
|
+
/**
|
|
23
|
+
* Node's built-in fetch ignores HTTP_PROXY / HTTPS_PROXY, so on a network that
|
|
24
|
+
* requires a proxy this fails even though `git` - which does read the proxy
|
|
25
|
+
* settings - would succeed. Worth saying, because the raw error is just a
|
|
26
|
+
* connect timeout.
|
|
27
|
+
*/
|
|
28
|
+
function networkHint(env = process.env) {
|
|
29
|
+
const proxied = env.HTTPS_PROXY || env.https_proxy || env.HTTP_PROXY || env.http_proxy;
|
|
30
|
+
if (proxied) {
|
|
31
|
+
return 'A proxy is configured, but Node does not apply it to its own requests. Check your network, or see the docs for NODE_USE_ENV_PROXY.';
|
|
32
|
+
}
|
|
33
|
+
return 'Check your network connection, or whether access to github.com is blocked.';
|
|
34
|
+
}
|
|
35
|
+
|
|
19
36
|
/** GET JSON, returning null for 404 so a missing registry is not an error. */
|
|
20
37
|
async function getJson(url, { env = process.env, fetchImpl = fetch } = {}) {
|
|
21
38
|
let res;
|
|
22
39
|
try {
|
|
23
40
|
res = await fetchImpl(url, { headers: ghHeaders(env), redirect: 'follow' });
|
|
24
41
|
} catch (err) {
|
|
25
|
-
throw new UserError(`
|
|
42
|
+
throw new UserError(`Could not reach ${new URL(url).host}: ${err.message}`, {
|
|
43
|
+
hint: networkHint(env),
|
|
44
|
+
});
|
|
26
45
|
}
|
|
27
46
|
if (res.status === 404) return null;
|
|
28
47
|
if (!res.ok) {
|
|
@@ -108,6 +127,18 @@ async function resolvePlugin({ repo, ref, plugin, marketplace = null, label, dep
|
|
|
108
127
|
});
|
|
109
128
|
}
|
|
110
129
|
|
|
130
|
+
// Claude Code requires a kebab-case marketplace id. Catching it here names the
|
|
131
|
+
// real problem; otherwise the failure surfaces much later as a bare
|
|
132
|
+
// "plugin not found in marketplace" from the Claude CLI.
|
|
133
|
+
if (!MARKETPLACE_RE.test(resolvedMarketplace)) {
|
|
134
|
+
throw new UserError(
|
|
135
|
+
`Marketplace name '${resolvedMarketplace}' is not a valid identifier.`,
|
|
136
|
+
{
|
|
137
|
+
hint: `It must be kebab-case with no spaces (e.g. my-marketplace). Fix 'name' in ${REGISTRY_FILES[0]}.`,
|
|
138
|
+
},
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
111
142
|
return {
|
|
112
143
|
plugin,
|
|
113
144
|
repo,
|
package/src/cli.js
CHANGED
|
@@ -7,6 +7,7 @@ const { resolveBrand } = require('./brand');
|
|
|
7
7
|
const { NAMES, byName } = require('./harness');
|
|
8
8
|
const { UserError } = require('./util');
|
|
9
9
|
const { installPlugin, uninstallPlugin, updateAll, listPlugins } = require('./install');
|
|
10
|
+
const { diagnose } = require('./doctor');
|
|
10
11
|
const pkg = require('../package.json');
|
|
11
12
|
|
|
12
13
|
const VALUE_FLAGS = new Set(['repo', 'ref', 'marketplace', 'targets']);
|
|
@@ -86,6 +87,7 @@ Usage
|
|
|
86
87
|
${bin} update
|
|
87
88
|
${bin} list
|
|
88
89
|
${bin} installed
|
|
90
|
+
${bin} doctor
|
|
89
91
|
|
|
90
92
|
Options
|
|
91
93
|
--repo <owner/repo> Use a different marketplace (default: ${brand.label})
|
|
@@ -241,6 +243,37 @@ async function run(argv = process.argv.slice(2), profile = {}) {
|
|
|
241
243
|
log.info(`Install one with \`${bin} install <plugin>\`.`);
|
|
242
244
|
return 0;
|
|
243
245
|
}
|
|
246
|
+
case 'doctor': {
|
|
247
|
+
const report = await diagnose({ brand });
|
|
248
|
+
if (flags.json) {
|
|
249
|
+
log.plain(JSON.stringify(report, null, 2));
|
|
250
|
+
return report.ok ? 0 : 1;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const labels = report.groups.flatMap((g) => g.checks.map((c) => c.label.length));
|
|
254
|
+
const labelWidth = Math.min(Math.max(...labels, 8), 22);
|
|
255
|
+
for (const group of report.groups) {
|
|
256
|
+
log.step(group.title);
|
|
257
|
+
for (const c of group.checks) {
|
|
258
|
+
const symbol = { ok: log.MARK, warn: '!', fail: 'x' }[c.status];
|
|
259
|
+
const paint = c.status === 'ok' ? log.dim : (s) => s;
|
|
260
|
+
log.plain(` ${symbol} ${c.label.padEnd(labelWidth)} ${paint(c.detail)}`);
|
|
261
|
+
if (c.hint) log.info(c.hint);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
log.plain('');
|
|
266
|
+
log.rule();
|
|
267
|
+
if (report.failures) {
|
|
268
|
+
log.error(`${log.plural(report.failures, 'problem')} found.`);
|
|
269
|
+
} else if (report.warnings) {
|
|
270
|
+
log.ok(`No problems. ${log.plural(report.warnings, 'warning')}.`);
|
|
271
|
+
} else {
|
|
272
|
+
log.ok('Everything checks out.');
|
|
273
|
+
}
|
|
274
|
+
log.plain('');
|
|
275
|
+
return report.ok ? 0 : 1;
|
|
276
|
+
}
|
|
244
277
|
case 'installed': {
|
|
245
278
|
const entries = manifest.list(paths.manifestPath());
|
|
246
279
|
if (flags.json) {
|
package/src/doctor.js
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
const paths = require('./paths');
|
|
7
|
+
const manifest = require('./manifest');
|
|
8
|
+
const { HARNESSES } = require('./harness');
|
|
9
|
+
const { loadCatalog, ghHeaders, rawUrl, REGISTRY_FILES } = require('./catalog');
|
|
10
|
+
const { which, run, shortPath, ensureDir, rmrf } = require('./util');
|
|
11
|
+
|
|
12
|
+
const MIN_NODE = 18;
|
|
13
|
+
const MARKETPLACE_RE = /^[a-z0-9]+(?:[-_.][a-z0-9]+)*$/i;
|
|
14
|
+
|
|
15
|
+
const ok = (label, detail) => ({ status: 'ok', label, detail });
|
|
16
|
+
const warn = (label, detail, hint) => ({ status: 'warn', label, detail, hint });
|
|
17
|
+
const fail = (label, detail, hint) => ({ status: 'fail', label, detail, hint });
|
|
18
|
+
|
|
19
|
+
async function checkEnvironment(deps) {
|
|
20
|
+
const whichImpl = deps.which || which;
|
|
21
|
+
const runImpl = deps.run || run;
|
|
22
|
+
const env = deps.env || process.env;
|
|
23
|
+
const checks = [];
|
|
24
|
+
|
|
25
|
+
const version = process.versions.node;
|
|
26
|
+
const major = parseInt(version.split('.')[0], 10);
|
|
27
|
+
checks.push(
|
|
28
|
+
major >= MIN_NODE
|
|
29
|
+
? ok('Node.js', `v${version}`)
|
|
30
|
+
: fail('Node.js', `v${version}`, `Version ${MIN_NODE} or newer is required.`),
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
const git = whichImpl('git', env);
|
|
34
|
+
if (git) {
|
|
35
|
+
let detail = shortPath(git);
|
|
36
|
+
try {
|
|
37
|
+
const res = await runImpl(git, ['--version']);
|
|
38
|
+
if (res.code === 0 && res.stdout.trim()) detail = res.stdout.trim();
|
|
39
|
+
} catch {
|
|
40
|
+
/* the path alone is enough */
|
|
41
|
+
}
|
|
42
|
+
checks.push(ok('git', detail));
|
|
43
|
+
} else {
|
|
44
|
+
checks.push(
|
|
45
|
+
warn('git', 'not found', 'Plugins download through the GitHub API instead, which is rate limited to 60 requests an hour.'),
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const proxy = env.HTTPS_PROXY || env.https_proxy || env.HTTP_PROXY || env.http_proxy;
|
|
50
|
+
if (proxy) {
|
|
51
|
+
checks.push(
|
|
52
|
+
warn(
|
|
53
|
+
'Proxy',
|
|
54
|
+
'configured for the shell',
|
|
55
|
+
'Node does not apply HTTP_PROXY or HTTPS_PROXY to its own requests, so downloads may fail here even where git succeeds.',
|
|
56
|
+
),
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return checks;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function checkEditors(pathOpts) {
|
|
64
|
+
const checks = HARNESSES.map((h) =>
|
|
65
|
+
h.detect(pathOpts)
|
|
66
|
+
? ok(h.title, h.location(pathOpts))
|
|
67
|
+
: warn(h.title, `not installed (looked in ${h.location(pathOpts)})`),
|
|
68
|
+
);
|
|
69
|
+
if (!checks.some((c) => c.status === 'ok')) {
|
|
70
|
+
checks.push(
|
|
71
|
+
fail(
|
|
72
|
+
'Any editor',
|
|
73
|
+
'none found',
|
|
74
|
+
'Install Claude Code, Cursor, or VS Code - there is nowhere to install a plugin.',
|
|
75
|
+
),
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
return checks;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function checkMarketplace(brand, deps) {
|
|
82
|
+
const fetchImpl = deps.fetchImpl || fetch;
|
|
83
|
+
const env = deps.env || process.env;
|
|
84
|
+
const checks = [];
|
|
85
|
+
|
|
86
|
+
let catalog = null;
|
|
87
|
+
try {
|
|
88
|
+
catalog = await loadCatalog({ repo: brand.repo, ref: brand.ref, deps });
|
|
89
|
+
checks.push(ok('Reachable', new URL(rawUrl(brand.repo, brand.ref, REGISTRY_FILES[0])).host));
|
|
90
|
+
} catch (err) {
|
|
91
|
+
checks.push(fail('Reachable', err.message, err.hint));
|
|
92
|
+
return checks;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (!catalog) {
|
|
96
|
+
checks.push(fail('Registry', `no ${REGISTRY_FILES[0]} found`, 'Check --repo and --ref.'));
|
|
97
|
+
return checks;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const name = catalog.marketplace;
|
|
101
|
+
checks.push(
|
|
102
|
+
name && MARKETPLACE_RE.test(name)
|
|
103
|
+
? ok('Registry', `${name}, ${catalog.plugins.length} plugins`)
|
|
104
|
+
: fail(
|
|
105
|
+
'Registry',
|
|
106
|
+
`name ${JSON.stringify(name)} is not a valid identifier`,
|
|
107
|
+
`It must be kebab-case with no spaces. Fix 'name' in ${REGISTRY_FILES[0]}.`,
|
|
108
|
+
),
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
// Only meaningful when the API is the download path; with git it is unused.
|
|
112
|
+
try {
|
|
113
|
+
const res = await fetchImpl('https://api.github.com/rate_limit', { headers: ghHeaders(env) });
|
|
114
|
+
if (res.ok) {
|
|
115
|
+
const body = await res.json();
|
|
116
|
+
const core = body.resources && body.resources.core;
|
|
117
|
+
if (core) {
|
|
118
|
+
const detail = `${core.remaining} of ${core.limit} requests left`;
|
|
119
|
+
checks.push(core.remaining > 10 ? ok('API budget', detail) : warn('API budget', detail, 'Set GITHUB_TOKEN, or install git to avoid the API entirely.'));
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
} catch {
|
|
123
|
+
/* advisory only */
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return checks;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function checkState(pathOpts) {
|
|
130
|
+
const dir = paths.stateDir(pathOpts);
|
|
131
|
+
const checks = [];
|
|
132
|
+
try {
|
|
133
|
+
ensureDir(dir);
|
|
134
|
+
const probe = path.join(dir, `.write-probe-${process.pid}`);
|
|
135
|
+
fs.writeFileSync(probe, 'ok');
|
|
136
|
+
rmrf(probe);
|
|
137
|
+
checks.push(ok('State directory', `${shortPath(dir)} (writable)`));
|
|
138
|
+
} catch (err) {
|
|
139
|
+
checks.push(
|
|
140
|
+
fail('State directory', `${shortPath(dir)} is not writable`, err.message),
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
try {
|
|
145
|
+
const entries = manifest.list(paths.manifestPath(pathOpts));
|
|
146
|
+
const detail = entries.length ? `${entries.length} ${entries.length === 1 ? 'plugin' : 'plugins'}` : 'none yet';
|
|
147
|
+
checks.push(ok('Installed', detail));
|
|
148
|
+
} catch (err) {
|
|
149
|
+
checks.push(warn('Installed', 'could not read installed.json', err.message));
|
|
150
|
+
}
|
|
151
|
+
return checks;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** Collect every check, grouped for display. Never throws: a broken check is a result. */
|
|
155
|
+
async function diagnose({ brand, deps = {}, pathOpts } = {}) {
|
|
156
|
+
const groups = [
|
|
157
|
+
{ title: 'Environment', checks: await checkEnvironment(deps) },
|
|
158
|
+
{ title: 'Editors', checks: checkEditors(pathOpts) },
|
|
159
|
+
{ title: 'Marketplace', checks: await checkMarketplace(brand, deps) },
|
|
160
|
+
{ title: 'Local state', checks: checkState(pathOpts) },
|
|
161
|
+
];
|
|
162
|
+
const all = groups.flatMap((g) => g.checks);
|
|
163
|
+
return {
|
|
164
|
+
groups,
|
|
165
|
+
failures: all.filter((c) => c.status === 'fail').length,
|
|
166
|
+
warnings: all.filter((c) => c.status === 'warn').length,
|
|
167
|
+
ok: !all.some((c) => c.status === 'fail'),
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
module.exports = { diagnose, MIN_NODE };
|
package/src/harness/claude.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const log = require('../log');
|
|
4
|
-
const { which, run, UserError } = require('../util');
|
|
4
|
+
const { which, run, UserError, stripBom } = require('../util');
|
|
5
5
|
|
|
6
6
|
// Claude Code installs from the marketplace itself - no local copy needed.
|
|
7
7
|
const name = 'claude';
|
|
@@ -15,6 +15,26 @@ const detect = (opts) => Boolean(cli(opts));
|
|
|
15
15
|
const tail = (res) =>
|
|
16
16
|
(res.stderr || res.stdout || '').trim().split('\n').slice(-3).join(' ').trim();
|
|
17
17
|
|
|
18
|
+
/**
|
|
19
|
+
* The name Claude Code knows this marketplace by.
|
|
20
|
+
*
|
|
21
|
+
* Claude keys a marketplace by the name it carried when it was added, which can
|
|
22
|
+
* drift from the current `name` in marketplace.json. Installing with the name
|
|
23
|
+
* from the file then fails with a bare "plugin not found in marketplace", so ask
|
|
24
|
+
* Claude what it calls the entry for this repository.
|
|
25
|
+
*/
|
|
26
|
+
async function registeredName(claude, repo) {
|
|
27
|
+
const res = await run(claude, ['plugin', 'marketplace', 'list', '--json']);
|
|
28
|
+
if (res.code !== 0) return null;
|
|
29
|
+
try {
|
|
30
|
+
const entries = JSON.parse(stripBom(res.stdout));
|
|
31
|
+
const hit = entries.find((m) => m.repo === repo || String(m.url || '').includes(repo));
|
|
32
|
+
return hit && hit.name ? hit.name : null;
|
|
33
|
+
} catch {
|
|
34
|
+
return null; // older CLI without --json
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
18
38
|
async function install({ plugin, marketplace, repo }, opts) {
|
|
19
39
|
const claude = cli(opts);
|
|
20
40
|
if (!claude) {
|
|
@@ -24,10 +44,14 @@ async function install({ plugin, marketplace, repo }, opts) {
|
|
|
24
44
|
|
|
25
45
|
const added = await run(claude, ['plugin', 'marketplace', 'add', repo]);
|
|
26
46
|
if (added.code !== 0) {
|
|
27
|
-
log.
|
|
47
|
+
log.debug(`marketplace add returned ${added.code} (likely already added) - continuing.`);
|
|
28
48
|
}
|
|
29
49
|
|
|
30
|
-
const
|
|
50
|
+
const known = await registeredName(claude, repo);
|
|
51
|
+
if (known && known !== marketplace) {
|
|
52
|
+
log.debug(`Claude knows this marketplace as '${known}', not '${marketplace}'.`);
|
|
53
|
+
}
|
|
54
|
+
const target = `${plugin}@${known || marketplace}`;
|
|
31
55
|
const res = await run(claude, ['plugin', 'install', target, '--scope', 'user']);
|
|
32
56
|
if (res.code !== 0) {
|
|
33
57
|
throw new UserError(`claude plugin install ${target} failed (exit ${res.code}). ${tail(res)}`.trim());
|
|
@@ -37,13 +61,14 @@ async function install({ plugin, marketplace, repo }, opts) {
|
|
|
37
61
|
return true;
|
|
38
62
|
}
|
|
39
63
|
|
|
40
|
-
async function uninstall({ plugin, marketplace }, opts) {
|
|
64
|
+
async function uninstall({ plugin, marketplace, repo }, opts) {
|
|
41
65
|
const claude = cli(opts);
|
|
42
66
|
if (!claude) {
|
|
43
67
|
log.warn("'claude' CLI not on PATH - skipping Claude Code.");
|
|
44
68
|
return false;
|
|
45
69
|
}
|
|
46
|
-
const
|
|
70
|
+
const known = await registeredName(claude, repo);
|
|
71
|
+
const target = `${plugin}@${known || marketplace}`;
|
|
47
72
|
const res = await run(claude, ['plugin', 'uninstall', target, '--scope', 'user']);
|
|
48
73
|
if (res.code !== 0) {
|
|
49
74
|
log.warn(`claude plugin uninstall ${target} returned ${res.code}. ${tail(res)}`.trim());
|
|
@@ -54,4 +79,6 @@ async function uninstall({ plugin, marketplace }, opts) {
|
|
|
54
79
|
return true;
|
|
55
80
|
}
|
|
56
81
|
|
|
57
|
-
|
|
82
|
+
const location = () => 'claude on PATH';
|
|
83
|
+
|
|
84
|
+
module.exports = { name, title, detect, location, install, uninstall, needsSource: false };
|
package/src/harness/cursor.js
CHANGED
|
@@ -44,4 +44,6 @@ async function uninstall({ plugin }, opts) {
|
|
|
44
44
|
return true;
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
|
|
47
|
+
const location = (opts) => shortPath(paths.cursorRoot(opts));
|
|
48
|
+
|
|
49
|
+
module.exports = { name, title, detect, location, install, uninstall, needsSource: true, destFor };
|
package/src/harness/vscode.js
CHANGED
|
@@ -56,4 +56,6 @@ async function uninstall({ plugin }, opts) {
|
|
|
56
56
|
return true;
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
-
|
|
59
|
+
const location = (opts) => shortPath(paths.vscodeUserDir(opts));
|
|
60
|
+
|
|
61
|
+
module.exports = { name, title, detect, location, install, uninstall, needsSource: true, destFor };
|
package/src/install.js
CHANGED
|
@@ -5,7 +5,7 @@ const paths = require('./paths');
|
|
|
5
5
|
const manifest = require('./manifest');
|
|
6
6
|
const { resolvePlugin, loadCatalog } = require('./catalog');
|
|
7
7
|
const { materialize } = require('./fetch');
|
|
8
|
-
const { byName, resolveTargets } = require('./harness');
|
|
8
|
+
const { byName, resolveTargets, NAMES } = require('./harness');
|
|
9
9
|
const { isInteractive, createPrompter } = require('./prompt');
|
|
10
10
|
const { assertPlugin, UserError } = require('./util');
|
|
11
11
|
|
|
@@ -94,18 +94,37 @@ async function installPlugin({
|
|
|
94
94
|
log.rule();
|
|
95
95
|
|
|
96
96
|
log.step('[Harnesses]');
|
|
97
|
+
const explicit = Array.isArray(targets) && targets.length > 0;
|
|
97
98
|
const available = requested.filter((name) => byName(name).detect(pathOpts));
|
|
98
|
-
|
|
99
|
-
|
|
99
|
+
const missing = requested.filter((name) => !available.includes(name));
|
|
100
|
+
|
|
101
|
+
for (const name of missing) {
|
|
102
|
+
const h = byName(name);
|
|
103
|
+
log.info(`${h.title} is not installed (looked in ${h.location(pathOpts)}).`);
|
|
100
104
|
}
|
|
105
|
+
|
|
106
|
+
// Nothing to install into is a failure, not a quiet no-op. Saying which
|
|
107
|
+
// editor was asked for beats a generic "no harness found" when the user
|
|
108
|
+
// named one explicitly.
|
|
101
109
|
if (!available.length) {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
110
|
+
const names = missing.map((n) => byName(n).title);
|
|
111
|
+
throw new UserError(
|
|
112
|
+
explicit
|
|
113
|
+
? `${names.join(' and ')} ${names.length === 1 ? 'is' : 'are'} not installed on this machine.`
|
|
114
|
+
: 'No supported editor found on this machine.',
|
|
115
|
+
{
|
|
116
|
+
hint: explicit
|
|
117
|
+
? `Install it first, or choose another with --targets ${NAMES.join(',')}.`
|
|
118
|
+
: 'Install Claude Code, Cursor, or VS Code, then run this again.',
|
|
119
|
+
},
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
if (missing.length) {
|
|
123
|
+
log.info(`Continuing with ${available.map((n) => byName(n).title).join(', ')}.`);
|
|
105
124
|
}
|
|
106
125
|
|
|
107
126
|
const want = await chooseHarnesses(available, {
|
|
108
|
-
explicit
|
|
127
|
+
explicit,
|
|
109
128
|
assumeYes,
|
|
110
129
|
confirm: deps.confirm,
|
|
111
130
|
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Deliberately plain, old-syntax JavaScript with no imports: this has to parse
|
|
4
|
+
// and run on whatever ancient Node the user happens to have.
|
|
5
|
+
|
|
6
|
+
var MINIMUM = 18;
|
|
7
|
+
|
|
8
|
+
module.exports = function requireNode(exit) {
|
|
9
|
+
var version = process.versions.node;
|
|
10
|
+
var major = parseInt(version.split('.')[0], 10);
|
|
11
|
+
if (major >= MINIMUM) return true;
|
|
12
|
+
|
|
13
|
+
process.stderr.write(
|
|
14
|
+
'context-plugins requires Node.js ' +
|
|
15
|
+
MINIMUM +
|
|
16
|
+
' or newer, but this is Node.js ' +
|
|
17
|
+
version +
|
|
18
|
+
'.\n' +
|
|
19
|
+
'Install a newer Node.js from https://nodejs.org and run this again.\n',
|
|
20
|
+
);
|
|
21
|
+
(exit || process.exit)(1);
|
|
22
|
+
return false;
|
|
23
|
+
};
|