context-plugins 0.2.0 → 0.3.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 +1 -1
- package/package.json +1 -1
- package/src/brand.js +15 -5
- package/src/catalog.js +10 -5
- package/src/cli.js +54 -13
- package/src/fetch.js +2 -1
- package/src/harness/cursor.js +4 -4
- package/src/harness/vscode.js +8 -8
- package/src/install.js +32 -13
- package/src/log.js +96 -9
- package/src/util.js +61 -1
package/README.md
CHANGED
|
@@ -27,7 +27,7 @@ context-plugins installed # what this machine has
|
|
|
27
27
|
|
|
28
28
|
| Option | Default | Description |
|
|
29
29
|
| --- | --- | --- |
|
|
30
|
-
| `--repo <owner/repo>` |
|
|
30
|
+
| `--repo <owner/repo>` | the bundled marketplace | Install from a different marketplace |
|
|
31
31
|
| `--ref <branch\|tag\|sha>` | `main` | Version to install from |
|
|
32
32
|
| `--marketplace <name>` | read from `marketplace.json` | Marketplace name |
|
|
33
33
|
| `--targets <list>` | ask | `claude`, `cursor`, `vscode`, or `all` — skips the prompt |
|
package/package.json
CHANGED
package/src/brand.js
CHANGED
|
@@ -49,6 +49,13 @@ function resolveBrand({
|
|
|
49
49
|
} = {}) {
|
|
50
50
|
const rc = readRc(cwd) || readRc(home) || {};
|
|
51
51
|
|
|
52
|
+
const displayName = firstSet(
|
|
53
|
+
env.CP_DISPLAY_NAME,
|
|
54
|
+
rc.displayName,
|
|
55
|
+
profile.displayName,
|
|
56
|
+
DEFAULT_PROFILE.displayName,
|
|
57
|
+
);
|
|
58
|
+
|
|
52
59
|
const brand = {
|
|
53
60
|
repo: firstSet(flags.repo, env.CP_REPO, rc.repo, profile.repo, DEFAULT_PROFILE.repo),
|
|
54
61
|
ref: firstSet(flags.ref, env.CP_REF, rc.ref, profile.ref, DEFAULT_PROFILE.ref),
|
|
@@ -60,11 +67,14 @@ function resolveBrand({
|
|
|
60
67
|
profile.id,
|
|
61
68
|
DEFAULT_PROFILE.id,
|
|
62
69
|
) || null,
|
|
63
|
-
displayName
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
70
|
+
displayName,
|
|
71
|
+
// What the user sees in place of the repository. The repository is an
|
|
72
|
+
// implementation detail of where plugins are stored.
|
|
73
|
+
label: firstSet(
|
|
74
|
+
env.CP_MARKETPLACE_LABEL,
|
|
75
|
+
rc.marketplaceLabel,
|
|
76
|
+
profile.label,
|
|
77
|
+
`${displayName} Marketplace`,
|
|
68
78
|
),
|
|
69
79
|
bin: firstSet(profile.bin, DEFAULT_PROFILE.bin),
|
|
70
80
|
};
|
package/src/catalog.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const { UserError, assertRepo, assertRef, stripBom } = require('./util');
|
|
3
|
+
const { UserError, assertRepo, assertRef, stripBom, suggest } = require('./util');
|
|
4
4
|
|
|
5
5
|
// Claude Code and Cursor read the same registry shape from different folders.
|
|
6
6
|
// We prefer the Claude one and fall back to Cursor's.
|
|
@@ -83,7 +83,9 @@ function sourcePathFor(entry, plugin) {
|
|
|
83
83
|
* Resolve everything the installers need for one plugin:
|
|
84
84
|
* the marketplace name (derived unless overridden) and the folder inside the repo.
|
|
85
85
|
*/
|
|
86
|
-
async function resolvePlugin({ repo, ref, plugin, marketplace = null, deps = {} }) {
|
|
86
|
+
async function resolvePlugin({ repo, ref, plugin, marketplace = null, label, deps = {} }) {
|
|
87
|
+
// `label` is what the user sees; the repository stays an internal detail.
|
|
88
|
+
const shown = label || `${repo}@${ref}`;
|
|
87
89
|
const catalog = await loadCatalog({ repo, ref, deps });
|
|
88
90
|
const entry = entryFor(catalog, plugin);
|
|
89
91
|
|
|
@@ -91,14 +93,17 @@ async function resolvePlugin({ repo, ref, plugin, marketplace = null, deps = {}
|
|
|
91
93
|
const known = catalog.plugins
|
|
92
94
|
.map((p) => (typeof p === 'string' ? p : p && p.name))
|
|
93
95
|
.filter(Boolean);
|
|
94
|
-
|
|
95
|
-
|
|
96
|
+
const close = suggest(plugin, known);
|
|
97
|
+
throw new UserError(`Plugin '${plugin}' is not listed in ${shown}.`, {
|
|
98
|
+
hint: close.length
|
|
99
|
+
? `Did you mean: ${close.join(', ')}? Run 'list' to see all ${known.length}.`
|
|
100
|
+
: `Run 'list' to see the ${known.length} available plugins.`,
|
|
96
101
|
});
|
|
97
102
|
}
|
|
98
103
|
|
|
99
104
|
const resolvedMarketplace = marketplace || (catalog && catalog.marketplace);
|
|
100
105
|
if (!resolvedMarketplace) {
|
|
101
|
-
throw new UserError(`Could not determine the marketplace name for ${
|
|
106
|
+
throw new UserError(`Could not determine the marketplace name for ${shown}.`, {
|
|
102
107
|
hint: `No 'name' in ${REGISTRY_FILES[0]}. Pass --marketplace <name>.`,
|
|
103
108
|
});
|
|
104
109
|
}
|
package/src/cli.js
CHANGED
|
@@ -4,13 +4,16 @@ const log = require('./log');
|
|
|
4
4
|
const paths = require('./paths');
|
|
5
5
|
const manifest = require('./manifest');
|
|
6
6
|
const { resolveBrand } = require('./brand');
|
|
7
|
-
const { NAMES } = require('./harness');
|
|
7
|
+
const { NAMES, byName } = require('./harness');
|
|
8
8
|
const { UserError } = require('./util');
|
|
9
9
|
const { installPlugin, uninstallPlugin, updateAll, listPlugins } = require('./install');
|
|
10
10
|
const pkg = require('../package.json');
|
|
11
11
|
|
|
12
12
|
const VALUE_FLAGS = new Set(['repo', 'ref', 'marketplace', 'targets']);
|
|
13
|
-
const BOOL_FLAGS = new Set(['force', 'yes', 'verbose', 'quiet', 'json', 'help', 'version']);
|
|
13
|
+
const BOOL_FLAGS = new Set(['force', 'yes', 'long', 'verbose', 'quiet', 'json', 'help', 'version']);
|
|
14
|
+
|
|
15
|
+
// A plugin id longer than this is treated as an outlier when sizing the list grid.
|
|
16
|
+
const OUTLIER_NAME = 36;
|
|
14
17
|
|
|
15
18
|
const camel = (s) => s.replace(/-([a-z])/g, (_m, c) => c.toUpperCase());
|
|
16
19
|
const lowerFirst = (s) => s.charAt(0).toLowerCase() + s.slice(1);
|
|
@@ -85,12 +88,13 @@ Usage
|
|
|
85
88
|
${bin} installed
|
|
86
89
|
|
|
87
90
|
Options
|
|
88
|
-
--repo <owner/repo>
|
|
91
|
+
--repo <owner/repo> Use a different marketplace (default: ${brand.label})
|
|
89
92
|
--ref <branch|tag|sha> Version to install from (default: ${brand.ref})
|
|
90
|
-
--marketplace <name> Marketplace name (default: read from marketplace
|
|
93
|
+
--marketplace <name> Marketplace name (default: read from the marketplace)
|
|
91
94
|
--targets <list> Comma-separated: ${NAMES.join(', ')}, all (skips the prompt)
|
|
92
95
|
-y, --yes Accept every detected harness without asking
|
|
93
96
|
--force Replace a plugin installed from another marketplace
|
|
97
|
+
--long Show plugin descriptions (list)
|
|
94
98
|
--json Machine-readable output (list, installed)
|
|
95
99
|
--verbose Show underlying git / CLI detail
|
|
96
100
|
--quiet Suppress progress output
|
|
@@ -196,13 +200,45 @@ async function run(argv = process.argv.slice(2), profile = {}) {
|
|
|
196
200
|
log.plain(JSON.stringify(result, null, 2));
|
|
197
201
|
return 0;
|
|
198
202
|
}
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
+
const plugins = [...result.plugins].sort((a, b) => a.name.localeCompare(b.name));
|
|
204
|
+
log.banner(`${log.plural(plugins.length, 'plugin')} in ${result.label}`);
|
|
205
|
+
log.plain('');
|
|
206
|
+
|
|
207
|
+
if (flags.long) {
|
|
208
|
+
// Full detail, one plugin per block.
|
|
209
|
+
for (const p of plugins) {
|
|
210
|
+
const mark = p.installed ? log.MARK : ' ';
|
|
211
|
+
log.plain(` ${mark} ${log.bold(p.name)}`);
|
|
212
|
+
if (p.description) log.info(p.description);
|
|
213
|
+
}
|
|
214
|
+
} else {
|
|
215
|
+
// A grid of names. Descriptions in a marketplace are long and largely
|
|
216
|
+
// boilerplate, so a truncated column of them shows the same prefix on
|
|
217
|
+
// every row and crowds out the one thing that differs.
|
|
218
|
+
// Sized to the longest name that is not an outlier. Using the true
|
|
219
|
+
// maximum lets one very long id set the width for every column and
|
|
220
|
+
// collapse the grid; using a percentile makes a third of the rows
|
|
221
|
+
// ragged. Ignoring only the outliers keeps every ordinary row aligned.
|
|
222
|
+
const lengths = plugins.map((p) => p.name.length);
|
|
223
|
+
const cell = Math.max(16, ...lengths.filter((l) => l <= OUTLIER_NAME)) + 3;
|
|
224
|
+
const cols = Math.max(1, Math.floor((log.width(120) - 2) / cell));
|
|
225
|
+
const rows = Math.ceil(plugins.length / cols);
|
|
226
|
+
for (let r = 0; r < rows; r += 1) {
|
|
227
|
+
let line = ' ';
|
|
228
|
+
for (let c = 0; c < cols; c += 1) {
|
|
229
|
+
const p = plugins[c * rows + r]; // column-major keeps A-Z reading down
|
|
230
|
+
if (!p) continue;
|
|
231
|
+
line += `${p.installed ? log.MARK : ' '} ${p.name.padEnd(cell - 2)}`;
|
|
232
|
+
}
|
|
233
|
+
log.plain(line.trimEnd());
|
|
234
|
+
}
|
|
203
235
|
}
|
|
236
|
+
|
|
204
237
|
log.plain('');
|
|
205
|
-
|
|
238
|
+
const count = plugins.filter((p) => p.installed).length;
|
|
239
|
+
if (count) log.info(`${log.MARK} installed on this machine (${count})`);
|
|
240
|
+
if (!flags.long) log.info(`Run \`${bin} list --long\` for descriptions.`);
|
|
241
|
+
log.info(`Install one with \`${bin} install <plugin>\`.`);
|
|
206
242
|
return 0;
|
|
207
243
|
}
|
|
208
244
|
case 'installed': {
|
|
@@ -212,14 +248,19 @@ async function run(argv = process.argv.slice(2), profile = {}) {
|
|
|
212
248
|
return 0;
|
|
213
249
|
}
|
|
214
250
|
if (!entries.length) {
|
|
215
|
-
log.info('No plugins installed
|
|
251
|
+
log.info('No plugins installed yet.');
|
|
252
|
+
log.info(`Browse what is available with: ${bin} list`);
|
|
216
253
|
return 0;
|
|
217
254
|
}
|
|
218
|
-
log.banner(`${entries.length
|
|
255
|
+
log.banner(`${log.plural(entries.length, 'plugin')} installed`);
|
|
256
|
+
log.plain('');
|
|
257
|
+
const idWidth = Math.min(Math.max(...entries.map((e) => e.plugin.length), 4), 42);
|
|
219
258
|
for (const e of entries) {
|
|
220
|
-
|
|
221
|
-
log.
|
|
259
|
+
const where = (e.targets || []).map((n) => (byName(n) ? byName(n).title : n));
|
|
260
|
+
log.plain(` ${e.plugin.padEnd(idWidth)} ${log.dim(where.join(', '))}`);
|
|
261
|
+
log.debug(`${e.repo}@${e.ref} (marketplace: ${e.marketplace})`);
|
|
222
262
|
}
|
|
263
|
+
log.plain('');
|
|
223
264
|
return 0;
|
|
224
265
|
}
|
|
225
266
|
default:
|
package/src/fetch.js
CHANGED
|
@@ -52,7 +52,8 @@ async function materialize({ repo, ref, sourcePath, deps = {} }) {
|
|
|
52
52
|
async function viaGit({ git, repo, ref, sourcePath, work }) {
|
|
53
53
|
const clone = path.join(work, 'repo');
|
|
54
54
|
const url = `https://github.com/${repo}.git`;
|
|
55
|
-
log.info(
|
|
55
|
+
log.info('Fetching marketplace via git ...');
|
|
56
|
+
log.debug(`${url} (${ref}), sparse path ${sourcePath}`);
|
|
56
57
|
|
|
57
58
|
const base = ['clone', '--quiet', '--depth', '1', '--filter=blob:none', '--sparse'];
|
|
58
59
|
|
package/src/harness/cursor.js
CHANGED
|
@@ -5,7 +5,7 @@ const path = require('path');
|
|
|
5
5
|
|
|
6
6
|
const log = require('../log');
|
|
7
7
|
const paths = require('../paths');
|
|
8
|
-
const { replaceDir, rmrf, exists } = require('../util');
|
|
8
|
+
const { replaceDir, rmrf, exists, shortPath } = require('../util');
|
|
9
9
|
|
|
10
10
|
const name = 'cursor';
|
|
11
11
|
const title = 'Cursor';
|
|
@@ -27,7 +27,7 @@ async function install({ plugin, srcDir }, opts) {
|
|
|
27
27
|
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
28
28
|
replaceDir(srcDir, dest);
|
|
29
29
|
|
|
30
|
-
log.ok(`Installed -> ${dest}`);
|
|
30
|
+
log.ok(`Installed -> ${shortPath(dest)}`);
|
|
31
31
|
log.info('Please reload Cursor: Ctrl+Shift+P (Cmd+Shift+P) -> Developer: Reload Window');
|
|
32
32
|
return true;
|
|
33
33
|
}
|
|
@@ -35,11 +35,11 @@ async function install({ plugin, srcDir }, opts) {
|
|
|
35
35
|
async function uninstall({ plugin }, opts) {
|
|
36
36
|
const dest = destFor(plugin, opts);
|
|
37
37
|
if (!exists(dest)) {
|
|
38
|
-
log.info(`Nothing to remove at ${dest}`);
|
|
38
|
+
log.info(`Nothing to remove at ${shortPath(dest)}`);
|
|
39
39
|
return false;
|
|
40
40
|
}
|
|
41
41
|
rmrf(dest);
|
|
42
|
-
log.ok(`Removed -> ${dest}`);
|
|
42
|
+
log.ok(`Removed -> ${shortPath(dest)}`);
|
|
43
43
|
log.info('Please reload Cursor: Ctrl+Shift+P (Cmd+Shift+P) -> Developer: Reload Window');
|
|
44
44
|
return true;
|
|
45
45
|
}
|
package/src/harness/vscode.js
CHANGED
|
@@ -5,7 +5,7 @@ const path = require('path');
|
|
|
5
5
|
const log = require('../log');
|
|
6
6
|
const paths = require('../paths');
|
|
7
7
|
const { addPluginLocation, removePluginLocation } = require('../settings-merge');
|
|
8
|
-
const { replaceDir, rmrf, exists } = require('../util');
|
|
8
|
+
const { replaceDir, rmrf, exists, shortPath } = require('../util');
|
|
9
9
|
|
|
10
10
|
// VS Code loads a plugin from anywhere on disk once the folder is listed in
|
|
11
11
|
// `chat.pluginLocations`, so we keep our own copy under the state dir rather
|
|
@@ -19,7 +19,7 @@ const destFor = (plugin, opts) => path.join(paths.vscodeStoreDir(opts), plugin);
|
|
|
19
19
|
|
|
20
20
|
async function install({ plugin, srcDir }, opts) {
|
|
21
21
|
if (!detect(opts)) {
|
|
22
|
-
log.warn(`${paths.vscodeUserDir(opts)} not found - VS Code not installed, skipping.`);
|
|
22
|
+
log.warn(`${shortPath(paths.vscodeUserDir(opts))} not found - VS Code not installed, skipping.`);
|
|
23
23
|
return false;
|
|
24
24
|
}
|
|
25
25
|
|
|
@@ -29,9 +29,9 @@ async function install({ plugin, srcDir }, opts) {
|
|
|
29
29
|
const settings = paths.vscodeSettingsPath(opts);
|
|
30
30
|
const result = addPluginLocation(settings, dest);
|
|
31
31
|
|
|
32
|
-
log.ok(`Installed -> ${dest}`);
|
|
33
|
-
if (result.action === 'already') log.info(`Already registered in ${settings}`);
|
|
34
|
-
else log.info(`Registered in chat.pluginLocations (${settings})`);
|
|
32
|
+
log.ok(`Installed -> ${shortPath(dest)}`);
|
|
33
|
+
if (result.action === 'already') log.info(`Already registered in ${shortPath(settings)}`);
|
|
34
|
+
else log.info(`Registered in chat.pluginLocations (${shortPath(settings)})`);
|
|
35
35
|
// The backup always happens; it is only worth mentioning when asked for detail.
|
|
36
36
|
if (result.backup) log.debug(`Backed up settings.json -> ${path.basename(result.backup)}`);
|
|
37
37
|
log.info('Please reload VS Code: Ctrl+Shift+P (Cmd+Shift+P) -> Developer: Reload Window');
|
|
@@ -46,11 +46,11 @@ async function uninstall({ plugin }, opts) {
|
|
|
46
46
|
if (had) rmrf(dest);
|
|
47
47
|
|
|
48
48
|
if (!had && result.action !== 'removed') {
|
|
49
|
-
log.info(`Nothing to remove at ${dest}`);
|
|
49
|
+
log.info(`Nothing to remove at ${shortPath(dest)}`);
|
|
50
50
|
return false;
|
|
51
51
|
}
|
|
52
|
-
log.ok(`Removed -> ${dest}`);
|
|
53
|
-
if (result.action === 'removed') log.info(`Unregistered from chat.pluginLocations (${settings})`);
|
|
52
|
+
log.ok(`Removed -> ${shortPath(dest)}`);
|
|
53
|
+
if (result.action === 'removed') log.info(`Unregistered from chat.pluginLocations (${shortPath(settings)})`);
|
|
54
54
|
if (result.backup) log.debug(`Backed up settings.json -> ${path.basename(result.backup)}`);
|
|
55
55
|
log.info('Please reload VS Code: Ctrl+Shift+P (Cmd+Shift+P) -> Developer: Reload Window');
|
|
56
56
|
return true;
|
package/src/install.js
CHANGED
|
@@ -55,10 +55,9 @@ function assertNoMarketplaceConflict(manifestFile, { plugin, repo }, force) {
|
|
|
55
55
|
.list(manifestFile)
|
|
56
56
|
.find((p) => p.plugin === plugin && (p.repo || '') !== repo);
|
|
57
57
|
if (clash) {
|
|
58
|
-
throw new UserError(
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
);
|
|
58
|
+
throw new UserError(`'${plugin}' is already installed from a different marketplace.`, {
|
|
59
|
+
hint: 'Uninstall it first, or re-run with --force to replace it.',
|
|
60
|
+
});
|
|
62
61
|
}
|
|
63
62
|
}
|
|
64
63
|
|
|
@@ -81,13 +80,16 @@ async function installPlugin({
|
|
|
81
80
|
ref: effectiveRef,
|
|
82
81
|
plugin,
|
|
83
82
|
marketplace: brand.id,
|
|
83
|
+
label: brand.label,
|
|
84
84
|
deps,
|
|
85
85
|
});
|
|
86
86
|
|
|
87
87
|
const requested = resolveTargets(targets);
|
|
88
88
|
assertNoMarketplaceConflict(manifestFile, { plugin, repo: brand.repo }, force);
|
|
89
89
|
|
|
90
|
-
|
|
90
|
+
const from = effectiveRef === 'main' ? brand.label : `${brand.label} (${effectiveRef})`;
|
|
91
|
+
log.banner(`Installing '${plugin}' from ${from}`);
|
|
92
|
+
log.debug(`source: ${brand.repo}@${effectiveRef}, marketplace: ${resolved.marketplace}`);
|
|
91
93
|
if (resolved.description) log.info(resolved.description);
|
|
92
94
|
log.rule();
|
|
93
95
|
|
|
@@ -180,7 +182,7 @@ async function uninstallPlugin({ brand, plugin, targets, deps = {}, pathOpts } =
|
|
|
180
182
|
).marketplace;
|
|
181
183
|
}
|
|
182
184
|
|
|
183
|
-
log.banner(`Uninstalling '${plugin}'
|
|
185
|
+
log.banner(`Uninstalling '${plugin}' from ${brand.label}`);
|
|
184
186
|
log.info(`Removing from: ${want.map((n) => byName(n).title).join(', ')}`);
|
|
185
187
|
log.rule();
|
|
186
188
|
|
|
@@ -209,13 +211,19 @@ async function updateAll({ brand, force = false, deps = {}, pathOpts } = {}) {
|
|
|
209
211
|
const manifestFile = paths.manifestPath(pathOpts);
|
|
210
212
|
const entries = manifest.list(manifestFile);
|
|
211
213
|
if (!entries.length) {
|
|
212
|
-
log.warn(
|
|
214
|
+
log.warn('No plugins installed yet - nothing to update.');
|
|
213
215
|
return { updated: [], failed: [] };
|
|
214
216
|
}
|
|
215
217
|
|
|
216
|
-
log.banner(`Updating ${entries.length
|
|
218
|
+
log.banner(`Updating ${log.plural(entries.length, 'plugin')}`);
|
|
219
|
+
log.plain('');
|
|
217
220
|
const updated = [];
|
|
218
221
|
const failed = [];
|
|
222
|
+
// Each plugin would otherwise print a full install report; at five plugins
|
|
223
|
+
// that is sixty lines of scrollback to find one failure in. Collapse to a
|
|
224
|
+
// line each unless --verbose asked for the detail.
|
|
225
|
+
const collapse = !log.isVerbose && !log.isQuiet;
|
|
226
|
+
const idWidth = Math.min(Math.max(...entries.map((e) => e.plugin.length), 4), 42);
|
|
219
227
|
|
|
220
228
|
for (const entry of entries) {
|
|
221
229
|
const entryBrand = Object.freeze({
|
|
@@ -224,8 +232,9 @@ async function updateAll({ brand, force = false, deps = {}, pathOpts } = {}) {
|
|
|
224
232
|
ref: entry.ref || brand.ref,
|
|
225
233
|
id: entry.marketplace || brand.id,
|
|
226
234
|
});
|
|
235
|
+
if (collapse) log.setQuiet(true);
|
|
227
236
|
try {
|
|
228
|
-
await installPlugin({
|
|
237
|
+
const result = await installPlugin({
|
|
229
238
|
brand: entryBrand,
|
|
230
239
|
plugin: entry.plugin,
|
|
231
240
|
ref: entry.ref,
|
|
@@ -235,23 +244,32 @@ async function updateAll({ brand, force = false, deps = {}, pathOpts } = {}) {
|
|
|
235
244
|
deps,
|
|
236
245
|
pathOpts,
|
|
237
246
|
});
|
|
247
|
+
if (collapse) log.setQuiet(false);
|
|
238
248
|
updated.push(entry.plugin);
|
|
249
|
+
const where = (result.targets || []).map((n) => byName(n).title).join(', ');
|
|
250
|
+
if (collapse) log.ok(`${entry.plugin.padEnd(idWidth)} ${log.dim(where)}`);
|
|
239
251
|
} catch (err) {
|
|
252
|
+
if (collapse) log.setQuiet(false);
|
|
240
253
|
failed.push({ plugin: entry.plugin, error: err.message });
|
|
241
|
-
log.error(`${entry.plugin}
|
|
254
|
+
log.error(`${entry.plugin.padEnd(idWidth)} ${err.message}`);
|
|
242
255
|
}
|
|
243
256
|
}
|
|
244
257
|
|
|
258
|
+
log.plain('');
|
|
245
259
|
log.rule();
|
|
246
|
-
|
|
247
|
-
|
|
260
|
+
if (failed.length) {
|
|
261
|
+
log.warn(`Updated ${updated.length} of ${entries.length}; failed: ${failed.map((f) => f.plugin).join(', ')}`);
|
|
262
|
+
} else {
|
|
263
|
+
log.ok(`Updated ${log.plural(updated.length, 'plugin')}`);
|
|
264
|
+
}
|
|
265
|
+
log.plain('');
|
|
248
266
|
return { updated, failed };
|
|
249
267
|
}
|
|
250
268
|
|
|
251
269
|
async function listPlugins({ brand, deps = {}, pathOpts } = {}) {
|
|
252
270
|
const catalog = await loadCatalog({ repo: brand.repo, ref: brand.ref, deps });
|
|
253
271
|
if (!catalog) {
|
|
254
|
-
throw new UserError(`
|
|
272
|
+
throw new UserError(`Could not read ${brand.label}.`, {
|
|
255
273
|
hint: 'Check --repo, or the branch you pointed at with --ref.',
|
|
256
274
|
});
|
|
257
275
|
}
|
|
@@ -262,6 +280,7 @@ async function listPlugins({ brand, deps = {}, pathOpts } = {}) {
|
|
|
262
280
|
.map((p) => p.plugin),
|
|
263
281
|
);
|
|
264
282
|
return {
|
|
283
|
+
label: brand.label,
|
|
265
284
|
marketplace: catalog.marketplace,
|
|
266
285
|
repo: brand.repo,
|
|
267
286
|
plugins: catalog.plugins.map((p) => {
|
package/src/log.js
CHANGED
|
@@ -20,9 +20,72 @@ function unicodeSupported() {
|
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
// Every prefix is padded to the same width so continuation lines line up.
|
|
23
|
+
const INDENT = 6;
|
|
23
24
|
const TICK = unicodeSupported() ? ` ${String.fromCharCode(0x2713)} ` : ' OK ';
|
|
24
25
|
const BANG = ' !! ';
|
|
25
26
|
const CROSS = ' XX ';
|
|
27
|
+
const MARK = unicodeSupported() ? String.fromCharCode(0x2713) : '*';
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Terminal width, clamped: prose past ~78 columns is harder to read, not easier.
|
|
31
|
+
* COLUMNS is honoured so redirected output and tests can pin a width.
|
|
32
|
+
*/
|
|
33
|
+
function width(max = 78) {
|
|
34
|
+
const cols = process.stdout.columns || Number(process.env.COLUMNS) || 80;
|
|
35
|
+
return Math.max(40, Math.min(cols - 1, max));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const ASCII_MAP = {
|
|
39
|
+
'—': '-', // em dash
|
|
40
|
+
'–': '-', // en dash
|
|
41
|
+
'‘': "'",
|
|
42
|
+
'’': "'",
|
|
43
|
+
'“': '"',
|
|
44
|
+
'”': '"',
|
|
45
|
+
'…': '...',
|
|
46
|
+
'•': '*',
|
|
47
|
+
'→': '->',
|
|
48
|
+
' ': ' ',
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Marketplace descriptions are third-party text and routinely contain em dashes
|
|
53
|
+
* and smart quotes. On a console that cannot render them they arrive as
|
|
54
|
+
* mojibake, so downgrade to ASCII rather than print garbage.
|
|
55
|
+
*/
|
|
56
|
+
function toAscii(text) {
|
|
57
|
+
let out = String(text).replace(/[—–‘’“”…•→ ]/g, (c) => ASCII_MAP[c]);
|
|
58
|
+
out = out.normalize('NFD').replace(/[̀-ͯ]/g, ''); // strip diacritics
|
|
59
|
+
// ESC is kept: colour codes must survive, they are not text.
|
|
60
|
+
return out.replace(/[^\x1b\x20-\x7e\t\r\n]/g, '?');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const ascii = (text) => (unicodeSupported() ? text : toAscii(text));
|
|
64
|
+
|
|
65
|
+
/** Wrap to the terminal, keeping long unbreakable tokens (paths, URLs) intact. */
|
|
66
|
+
function wrap(text, indent = INDENT, max = width()) {
|
|
67
|
+
const room = Math.max(20, max - indent);
|
|
68
|
+
const lines = [];
|
|
69
|
+
let line = '';
|
|
70
|
+
for (const word of String(text).split(/\s+/).filter(Boolean)) {
|
|
71
|
+
if (!line) line = word;
|
|
72
|
+
else if (line.length + 1 + word.length <= room) line += ` ${word}`;
|
|
73
|
+
else {
|
|
74
|
+
lines.push(line);
|
|
75
|
+
line = word;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
if (line) lines.push(line);
|
|
79
|
+
return lines.length ? lines : [''];
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function truncate(text, max) {
|
|
83
|
+
const clean = String(text).trim();
|
|
84
|
+
if (clean.length <= max) return clean;
|
|
85
|
+
return `${clean.slice(0, Math.max(1, max - 3)).trimEnd()}...`;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const plural = (n, word, suffix = 's') => `${n} ${word}${n === 1 ? '' : suffix}`;
|
|
26
89
|
|
|
27
90
|
function colorEnabled() {
|
|
28
91
|
if (process.env.NO_COLOR !== undefined) return false;
|
|
@@ -44,33 +107,57 @@ const log = {
|
|
|
44
107
|
get isVerbose() {
|
|
45
108
|
return state.verbose;
|
|
46
109
|
},
|
|
110
|
+
get isQuiet() {
|
|
111
|
+
return state.quiet;
|
|
112
|
+
},
|
|
47
113
|
banner(msg) {
|
|
48
|
-
if (!state.quiet) console.log(`\n${paint('36', msg)}`);
|
|
114
|
+
if (!state.quiet) console.log(`\n${paint('36', ascii(msg))}`);
|
|
49
115
|
},
|
|
50
116
|
rule() {
|
|
51
|
-
if (!state.quiet) console.log(paint('90', '-'.repeat(
|
|
117
|
+
if (!state.quiet) console.log(paint('90', '-'.repeat(width())));
|
|
52
118
|
},
|
|
53
119
|
step(msg) {
|
|
54
|
-
if (!state.quiet) console.log(`\n${paint('1', msg)}`);
|
|
120
|
+
if (!state.quiet) console.log(`\n${paint('1', ascii(msg))}`);
|
|
55
121
|
},
|
|
56
122
|
ok(msg) {
|
|
57
|
-
if (!state.quiet) console.log(`${paint('32', TICK)}${msg}`);
|
|
123
|
+
if (!state.quiet) console.log(`${paint('32', TICK)}${ascii(msg)}`);
|
|
58
124
|
},
|
|
125
|
+
/** Wrapped and indented, so long descriptions stay readable at any width. */
|
|
59
126
|
info(msg) {
|
|
60
|
-
if (
|
|
127
|
+
if (state.quiet) return;
|
|
128
|
+
for (const line of wrap(ascii(msg))) console.log(paint('90', ` ${line}`));
|
|
61
129
|
},
|
|
62
130
|
warn(msg) {
|
|
63
|
-
if (
|
|
131
|
+
if (state.quiet) return;
|
|
132
|
+
const [first, ...rest] = wrap(ascii(msg));
|
|
133
|
+
console.log(`${paint('33', BANG)}${first}`);
|
|
134
|
+
for (const line of rest) console.log(paint('33', ` ${line}`));
|
|
64
135
|
},
|
|
65
136
|
error(msg) {
|
|
66
|
-
|
|
137
|
+
const [first, ...rest] = wrap(ascii(msg));
|
|
138
|
+
console.error(`${paint('31', CROSS)}${first}`);
|
|
139
|
+
for (const line of rest) console.error(paint('31', ` ${line}`));
|
|
67
140
|
},
|
|
68
141
|
debug(msg) {
|
|
69
|
-
if (state.verbose && !state.quiet) console.log(paint('90', ` .. ${msg}`));
|
|
142
|
+
if (state.verbose && !state.quiet) console.log(paint('90', ` .. ${ascii(msg)}`));
|
|
70
143
|
},
|
|
71
144
|
plain(msg = '') {
|
|
72
|
-
if (!state.quiet) console.log(msg);
|
|
145
|
+
if (!state.quiet) console.log(ascii(msg));
|
|
146
|
+
},
|
|
147
|
+
dim(msg) {
|
|
148
|
+
return paint('90', msg);
|
|
149
|
+
},
|
|
150
|
+
bold(msg) {
|
|
151
|
+
return paint('1', msg);
|
|
73
152
|
},
|
|
153
|
+
MARK,
|
|
154
|
+
width,
|
|
155
|
+
wrap,
|
|
156
|
+
truncate,
|
|
157
|
+
plural,
|
|
158
|
+
ascii,
|
|
159
|
+
toAscii,
|
|
160
|
+
unicodeSupported,
|
|
74
161
|
};
|
|
75
162
|
|
|
76
163
|
module.exports = log;
|
package/src/util.js
CHANGED
|
@@ -36,7 +36,7 @@ function assertPlugin(id) {
|
|
|
36
36
|
function assertRepo(repo) {
|
|
37
37
|
if (typeof repo !== 'string' || !REPO_RE.test(repo)) {
|
|
38
38
|
throw new UserError(`Invalid repo: ${JSON.stringify(repo)}`, {
|
|
39
|
-
hint: 'Expected owner/repo, e.g.
|
|
39
|
+
hint: 'Expected owner/repo, e.g. acme/plugin-marketplace',
|
|
40
40
|
});
|
|
41
41
|
}
|
|
42
42
|
return repo;
|
|
@@ -208,6 +208,63 @@ function timestamp(date = new Date()) {
|
|
|
208
208
|
|
|
209
209
|
const stripBom = (s) => (s.charCodeAt(0) === 0xfeff ? s.slice(1) : s);
|
|
210
210
|
|
|
211
|
+
/** Display form of a path: `~` beats repeating the user's home directory back at them. */
|
|
212
|
+
function shortPath(target, home = require('os').homedir()) {
|
|
213
|
+
if (!target || !home) return target;
|
|
214
|
+
const normalized = String(target);
|
|
215
|
+
if (normalized.toLowerCase().startsWith(home.toLowerCase())) {
|
|
216
|
+
const rest = normalized.slice(home.length);
|
|
217
|
+
return `~${rest.startsWith(path.sep) || rest.startsWith('/') ? '' : path.sep}${rest}`;
|
|
218
|
+
}
|
|
219
|
+
return normalized;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/** Edit distance, capped work for the short strings we compare (plugin ids). */
|
|
223
|
+
function editDistance(a, b) {
|
|
224
|
+
const rows = a.length + 1;
|
|
225
|
+
const cols = b.length + 1;
|
|
226
|
+
let prev = Array.from({ length: cols }, (_v, i) => i);
|
|
227
|
+
for (let i = 1; i < rows; i += 1) {
|
|
228
|
+
const curr = [i];
|
|
229
|
+
for (let j = 1; j < cols; j += 1) {
|
|
230
|
+
curr[j] = Math.min(
|
|
231
|
+
prev[j] + 1,
|
|
232
|
+
curr[j - 1] + 1,
|
|
233
|
+
prev[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1),
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
prev = curr;
|
|
237
|
+
}
|
|
238
|
+
return prev[cols - 1];
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const sharedPrefix = (a, b) => {
|
|
242
|
+
let i = 0;
|
|
243
|
+
while (i < a.length && i < b.length && a[i] === b[i]) i += 1;
|
|
244
|
+
return i;
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Closest names to `query`. Substring hits rank first, then names sharing a
|
|
249
|
+
* substantial prefix - plugin ids in a marketplace tend to share a suffix, so
|
|
250
|
+
* plain edit distance alone rates `azure-cognitve` as too far from
|
|
251
|
+
* `azure-cognitive-sdk` to be worth offering.
|
|
252
|
+
*/
|
|
253
|
+
function suggest(query, candidates, limit = 3) {
|
|
254
|
+
const q = String(query).toLowerCase();
|
|
255
|
+
const threshold = Math.max(3, Math.ceil(q.length * 0.4));
|
|
256
|
+
const scored = candidates
|
|
257
|
+
.map((name) => {
|
|
258
|
+
const n = String(name).toLowerCase();
|
|
259
|
+
if (n.includes(q) || q.includes(n)) return { name, score: 0 };
|
|
260
|
+
if (sharedPrefix(q, n) >= Math.max(4, Math.ceil(q.length * 0.6))) return { name, score: 1 };
|
|
261
|
+
return { name, score: editDistance(q, n) };
|
|
262
|
+
})
|
|
263
|
+
.filter((c) => c.score <= threshold)
|
|
264
|
+
.sort((a, b) => a.score - b.score || a.name.length - b.name.length);
|
|
265
|
+
return scored.slice(0, limit).map((c) => c.name);
|
|
266
|
+
}
|
|
267
|
+
|
|
211
268
|
module.exports = {
|
|
212
269
|
UserError,
|
|
213
270
|
assertPlugin,
|
|
@@ -226,4 +283,7 @@ module.exports = {
|
|
|
226
283
|
pool,
|
|
227
284
|
timestamp,
|
|
228
285
|
stripBom,
|
|
286
|
+
shortPath,
|
|
287
|
+
editDistance,
|
|
288
|
+
suggest,
|
|
229
289
|
};
|