@devrik-tools/claude-gates 0.4.0 → 0.7.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/.claude-plugin/marketplace.json +2 -2
- package/README.es.md +39 -4
- package/README.md +34 -5
- package/cli/config.mjs +126 -124
- package/cli/init.mjs +303 -276
- package/cli/install.mjs +281 -175
- package/cli/materialize.mjs +103 -102
- package/cli/registry.mjs +139 -136
- package/cli/smoke-fixtures.json +65 -0
- package/cli/task.mjs +140 -140
- package/package.json +1 -1
- package/plugins/gates/.claude-plugin/plugin.json +1 -1
- package/plugins/gates/hooks/ask-adoption.mjs +147 -147
- package/plugins/gates/hooks/doctor.mjs +207 -207
- package/plugins/gates/hooks/gates/atomic-commit/index.mjs +229 -0
- package/plugins/gates/hooks/gates/audit-before-build/index.mjs +110 -88
- package/plugins/gates/hooks/gates/autonomous-mode/index.mjs +50 -50
- package/plugins/gates/hooks/gates/bash-commands/index.mjs +215 -215
- package/plugins/gates/hooks/gates/brief-approved/index.mjs +216 -0
- package/plugins/gates/hooks/gates/brief-before-delegate/index.mjs +269 -265
- package/plugins/gates/hooks/gates/capability-map/index.mjs +701 -0
- package/plugins/gates/hooks/gates/circuit-breaker/index.mjs +527 -501
- package/plugins/gates/hooks/gates/diagnosis-before-patch/index.mjs +48 -43
- package/plugins/gates/hooks/gates/feature-catalog/index.mjs +83 -83
- package/plugins/gates/hooks/gates/force-parallel/index.mjs +134 -119
- package/plugins/gates/hooks/gates/forge-flow/index.mjs +134 -134
- package/plugins/gates/hooks/gates/implementation-pipeline/index.mjs +187 -187
- package/plugins/gates/hooks/gates/intent-flow/index.mjs +260 -260
- package/plugins/gates/hooks/gates/lint-commit/index.mjs +152 -149
- package/plugins/gates/hooks/gates/mandatory-flow/index.mjs +180 -180
- package/plugins/gates/hooks/gates/never-assume/index.mjs +59 -58
- package/plugins/gates/hooks/gates/no-blocking/index.mjs +163 -148
- package/plugins/gates/hooks/gates/no-coauthor/index.mjs +127 -0
- package/plugins/gates/hooks/gates/no-lint-suppression/index.mjs +183 -0
- package/plugins/gates/hooks/gates/protected-paths/index.mjs +149 -144
- package/plugins/gates/hooks/gates/recurrence-lock/index.mjs +91 -89
- package/plugins/gates/hooks/gates/reuse-before-build/index.mjs +263 -159
- package/plugins/gates/hooks/gates/risk-level/index.mjs +265 -263
- package/plugins/gates/hooks/gates/root-cause-first/index.mjs +57 -56
- package/plugins/gates/hooks/gates/root-whitelist/index.mjs +211 -131
- package/plugins/gates/hooks/gates/rule-skill-autodiscovery/index.mjs +181 -184
- package/plugins/gates/hooks/gates/sdd-specs/index.mjs +256 -256
- package/plugins/gates/hooks/gates/staged-lint/index.mjs +187 -0
- package/plugins/gates/hooks/gates/stop-pending/index.mjs +169 -164
- package/plugins/gates/hooks/gates/test-matrix/index.mjs +187 -187
- package/plugins/gates/hooks/gates/tool-map/index.mjs +168 -143
- package/plugins/gates/hooks/hooks.json +61 -0
- package/plugins/gates/hooks/lib/config.mjs +179 -172
- package/plugins/gates/hooks/lib/hook-io.mjs +367 -357
- package/plugins/gates/hooks/lib/signals.mjs +172 -127
- package/plugins/gates/hooks/wiring-check.mjs +227 -227
- package/plugins/tasks/.claude-plugin/plugin.json +1 -1
- package/plugins/tasks/hooks/hooks.json +26 -26
- package/plugins/tasks/hooks/lib/task-store.mjs +217 -197
- package/plugins/tasks/hooks/register-requests.mjs +145 -145
- package/plugins/tasks/hooks/session-tasks.mjs +108 -108
- package/registry.json +192 -1
package/cli/install.mjs
CHANGED
|
@@ -1,175 +1,281 @@
|
|
|
1
|
-
// Installs the gates plugin into Claude Code from the selection `init` just wrote.
|
|
2
|
-
// Additive by design: `claude plugin install` merges the plugin's hooks alongside whatever
|
|
3
|
-
// the user already has — it never rewrites their settings. If the `claude` binary is not
|
|
4
|
-
// reachable, this degrades to printing the manual command, and `init` still succeeds.
|
|
5
|
-
|
|
6
|
-
import { execFileSync } from 'node:child_process';
|
|
7
|
-
import { readFileSync } from 'node:fs';
|
|
8
|
-
import { SCOPES } from './config.mjs';
|
|
9
|
-
import { MARKETPLACE_PATH, REPOSITORY_ROOT } from './constants.mjs';
|
|
10
|
-
|
|
11
|
-
const CLAUDE_BIN = 'claude';
|
|
12
|
-
|
|
13
|
-
// Config scope decides where the plugin is installed: a project selection stays local to
|
|
14
|
-
// this project (its .claude/settings.json); a global selection installs for every project.
|
|
15
|
-
const PLUGIN_SCOPE = Object.freeze({
|
|
16
|
-
[SCOPES.PROJECT]: 'project',
|
|
17
|
-
[SCOPES.GLOBAL]: 'user',
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
return
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
)
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
1
|
+
// Installs the gates plugin into Claude Code from the selection `init` just wrote.
|
|
2
|
+
// Additive by design: `claude plugin install` merges the plugin's hooks alongside whatever
|
|
3
|
+
// the user already has — it never rewrites their settings. If the `claude` binary is not
|
|
4
|
+
// reachable, this degrades to printing the manual command, and `init` still succeeds.
|
|
5
|
+
|
|
6
|
+
import { execFileSync } from 'node:child_process';
|
|
7
|
+
import { readFileSync } from 'node:fs';
|
|
8
|
+
import { SCOPES } from './config.mjs';
|
|
9
|
+
import { MARKETPLACE_PATH, REPOSITORY_ROOT } from './constants.mjs';
|
|
10
|
+
|
|
11
|
+
const CLAUDE_BIN = 'claude';
|
|
12
|
+
|
|
13
|
+
// Config scope decides where the plugin is installed: a project selection stays local to
|
|
14
|
+
// this project (its .claude/settings.json); a global selection installs for every project.
|
|
15
|
+
const PLUGIN_SCOPE = Object.freeze({
|
|
16
|
+
[SCOPES.PROJECT]: 'project',
|
|
17
|
+
[SCOPES.GLOBAL]: 'user',
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
const BOM_CODE_POINT = 0xfeff;
|
|
21
|
+
|
|
22
|
+
function stripBom(text) {
|
|
23
|
+
return text.charCodeAt(0) === BOM_CODE_POINT ? text.slice(1) : text;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function marketplaceManifest() {
|
|
27
|
+
return JSON.parse(stripBom(readFileSync(MARKETPLACE_PATH, 'utf8')));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// A marketplace block in `plugin marketplace list` prints a name line (optionally bulleted
|
|
31
|
+
// with ❯) then a `Source: <Kind> (<path>)` line. These match one line each — anchored and
|
|
32
|
+
// with bounded, non-overlapping character classes so there is no catastrophic backtracking.
|
|
33
|
+
const MARKETPLACE_NAME_LINE = /^[^\S\n]*(?:❯[^\S\n]*)?([\w-]+)[^\S\n]*$/;
|
|
34
|
+
const MARKETPLACE_DIRECTORY_SOURCE =
|
|
35
|
+
/^[^\S\n]*Source:[^\S\n]*Directory[^\S\n]*\(([^)]+)\)/i;
|
|
36
|
+
// A plain loop, not a regex: `/[\\/]+$/` is flagged as super-linear by the linter even
|
|
37
|
+
// though this use is bounded and safe, and trimming trailing slashes one character at a
|
|
38
|
+
// time from the end needs no backtracking-capable pattern at all.
|
|
39
|
+
function trimTrailingSlashes(text) {
|
|
40
|
+
let end = text.length;
|
|
41
|
+
while (end > 0 && (text[end - 1] === '/' || text[end - 1] === '\\')) end -= 1;
|
|
42
|
+
return text.slice(0, end);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Every registered marketplace as `{ name, source }`, parsed from `plugin marketplace list`.
|
|
47
|
+
* `source` is the directory path a Directory-source marketplace serves from (trailing slash
|
|
48
|
+
* stripped). Only Directory-source marketplaces are returned (GitHub sources have no local
|
|
49
|
+
* path to compare). Empty on any listing failure.
|
|
50
|
+
*/
|
|
51
|
+
function listRegisteredMarketplaces(runClaude) {
|
|
52
|
+
let listing;
|
|
53
|
+
try {
|
|
54
|
+
listing = runClaude(['plugin', 'marketplace', 'list']);
|
|
55
|
+
} catch {
|
|
56
|
+
return [];
|
|
57
|
+
}
|
|
58
|
+
const marketplaces = [];
|
|
59
|
+
let currentName = null;
|
|
60
|
+
for (const line of listing.split(/\r?\n/)) {
|
|
61
|
+
const nameMatch = MARKETPLACE_NAME_LINE.exec(line);
|
|
62
|
+
if (nameMatch) {
|
|
63
|
+
currentName = nameMatch[1];
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
const sourceMatch = MARKETPLACE_DIRECTORY_SOURCE.exec(line);
|
|
67
|
+
if (sourceMatch && currentName) {
|
|
68
|
+
marketplaces.push({
|
|
69
|
+
name: currentName,
|
|
70
|
+
source: trimTrailingSlashes(sourceMatch[1]),
|
|
71
|
+
});
|
|
72
|
+
currentName = null;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return marketplaces;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* The name Claude Code actually registered THIS directory's marketplace under. Usually the
|
|
80
|
+
* manifest's `name`, but not always: if the user added the same directory earlier under a
|
|
81
|
+
* different name (e.g. `devrik`), Claude Code keeps that original registration name, and
|
|
82
|
+
* `plugin marketplace add` is a no-op that does not rename it. Installing as `plugin@<name>`
|
|
83
|
+
* then fails with "not found in marketplace <name>". So we find the registered marketplace
|
|
84
|
+
* whose source path is our REPOSITORY_ROOT and use that name; we fall back to the manifest
|
|
85
|
+
* name when the listing is unavailable (e.g. no `claude` binary).
|
|
86
|
+
*/
|
|
87
|
+
function registeredMarketplaceName(runClaude, fallbackName) {
|
|
88
|
+
const root = trimTrailingSlashes(REPOSITORY_ROOT);
|
|
89
|
+
const here = listRegisteredMarketplaces(runClaude).find(
|
|
90
|
+
(entry) => entry.source.toLowerCase() === root.toLowerCase(),
|
|
91
|
+
);
|
|
92
|
+
return here ? here.name : fallbackName;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Every plugin the manifest declares, paired with the marketplace's registered name. */
|
|
96
|
+
function marketplaceAndPlugins(runClaude = realClaude) {
|
|
97
|
+
const manifest = marketplaceManifest();
|
|
98
|
+
const marketplace = registeredMarketplaceName(runClaude, manifest.name);
|
|
99
|
+
return manifest.plugins.map((plugin) => ({
|
|
100
|
+
marketplace,
|
|
101
|
+
plugin: plugin.name,
|
|
102
|
+
}));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* A marketplace registered from a DIRECTORY serves whatever version lives at that path. When
|
|
107
|
+
* the user updates the npm package, the new version lands in a NEW location (a fresh npx cache
|
|
108
|
+
* dir, a global install, a cloned repo), but the marketplace still points at the OLD path and
|
|
109
|
+
* `plugin marketplace add` on the new path is a no-op that does NOT re-point it — so Claude
|
|
110
|
+
* Code keeps serving the stale version, and `plugin update` finds nothing newer. This detects
|
|
111
|
+
* that mismatch and re-points the marketplace (remove + add) at THIS running package's root,
|
|
112
|
+
* so an update actually takes effect.
|
|
113
|
+
*
|
|
114
|
+
* It matches the existing registration by NAME (the manifest name, plus any name that already
|
|
115
|
+
* points at a claude-gates package path — resilient to the marketplace having been registered
|
|
116
|
+
* under a different name, the known `devrik` case). No-op when a registration already points
|
|
117
|
+
* here (the common case). Best-effort: any failure is swallowed and the normal add still runs.
|
|
118
|
+
*/
|
|
119
|
+
function repointMarketplaceIfStale(runClaude, manifestName) {
|
|
120
|
+
const here = trimTrailingSlashes(REPOSITORY_ROOT);
|
|
121
|
+
const registered = listRegisteredMarketplaces(runClaude);
|
|
122
|
+
if (registered.length === 0) return;
|
|
123
|
+
|
|
124
|
+
// Already pointing here under any name → nothing to do.
|
|
125
|
+
if (
|
|
126
|
+
registered.some(
|
|
127
|
+
(entry) => entry.source.toLowerCase() === here.toLowerCase(),
|
|
128
|
+
)
|
|
129
|
+
)
|
|
130
|
+
return;
|
|
131
|
+
|
|
132
|
+
// A stale registration to re-point: the one named like our manifest, or (fallback) one whose
|
|
133
|
+
// stale source path is itself a claude-gates package directory.
|
|
134
|
+
const stale =
|
|
135
|
+
registered.find((entry) => entry.name === manifestName) ??
|
|
136
|
+
registered.find((entry) =>
|
|
137
|
+
/[\\/]@devrik-tools[\\/]claude-gates$/i.test(entry.source),
|
|
138
|
+
);
|
|
139
|
+
if (!stale) return;
|
|
140
|
+
|
|
141
|
+
try {
|
|
142
|
+
runClaude(['plugin', 'marketplace', 'remove', stale.name]);
|
|
143
|
+
runClaude([
|
|
144
|
+
'plugin',
|
|
145
|
+
'marketplace',
|
|
146
|
+
'add',
|
|
147
|
+
REPOSITORY_ROOT,
|
|
148
|
+
'--scope',
|
|
149
|
+
'user',
|
|
150
|
+
]);
|
|
151
|
+
} catch {
|
|
152
|
+
// Re-pointing failed — non-fatal; the caller's own add attempt still follows.
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** The `claude plugin install …` lines, one per plugin the manifest declares. */
|
|
157
|
+
export function pluginInstallCommands() {
|
|
158
|
+
return marketplaceAndPlugins().map(
|
|
159
|
+
({ marketplace, plugin }) =>
|
|
160
|
+
`claude plugin install ${plugin}@${marketplace}`,
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** Back-compat single-line form: the first plugin's install command. */
|
|
165
|
+
export function pluginInstallCommand() {
|
|
166
|
+
return pluginInstallCommands()[0];
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function realClaude(commandArguments) {
|
|
170
|
+
return execFileSync(CLAUDE_BIN, commandArguments, {
|
|
171
|
+
encoding: 'utf8',
|
|
172
|
+
stdio: 'pipe',
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function reasonFor(error) {
|
|
177
|
+
return error?.code === 'ENOENT'
|
|
178
|
+
? 'the `claude` command was not found on PATH'
|
|
179
|
+
: (error?.stderr || error?.message || String(error)).trim().split('\n')[0];
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Best-effort removal of a previously installed version of each manifest plugin, at the
|
|
184
|
+
* given scope, before the fresh install runs. `claude plugin uninstall <plugin> --scope
|
|
185
|
+
* <scope>` is verified (via `claude plugin uninstall --help`) to take the bare plugin name
|
|
186
|
+
* (no `@marketplace`, unlike install) and the same `--scope` values as install. When there
|
|
187
|
+
* is no previous install, uninstall fails — that is expected and non-fatal, so failures here
|
|
188
|
+
* are swallowed and never stop the install that follows.
|
|
189
|
+
*/
|
|
190
|
+
function removePreviousInstalls(targets, scope, runClaude) {
|
|
191
|
+
for (const { plugin } of targets) {
|
|
192
|
+
try {
|
|
193
|
+
runClaude(['plugin', 'uninstall', plugin, '--scope', scope, '--yes']);
|
|
194
|
+
} catch {
|
|
195
|
+
// No previous install (or removal failed for some other reason) — non-fatal either way;
|
|
196
|
+
// the install below is what actually matters.
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Registers the marketplace (idempotent: a second add just reports it already exists, which
|
|
203
|
+
* is not fatal) once, then installs EVERY plugin the manifest declares at the scope matching
|
|
204
|
+
* the config choice. A project that adopts claude-gates gets all of its plugins (gates,
|
|
205
|
+
* tasks, …), not just the first — a single failed install does not stop the rest from being
|
|
206
|
+
* attempted, so one broken plugin never silently blocks another that would have worked.
|
|
207
|
+
*
|
|
208
|
+
* Returns { installed, scope, results } where `installed` is true only if every plugin
|
|
209
|
+
* installed; `results` is a per-plugin { plugin, installed, reason? } list so a caller can
|
|
210
|
+
* report exactly which ones need the manual command.
|
|
211
|
+
*
|
|
212
|
+
* `runClaude` is an injectable seam (defaults to the real `claude` binary) so tests can
|
|
213
|
+
* exercise the multi-plugin partial-failure logic without actually invoking the CLI and
|
|
214
|
+
* installing plugins on the machine running the test.
|
|
215
|
+
*
|
|
216
|
+
* `removePrevious` (default true) uninstalls each plugin's previous version at this scope
|
|
217
|
+
* before installing, so a stale version never lingers alongside the new one. It is best-effort
|
|
218
|
+
* and never fails the overall install.
|
|
219
|
+
*/
|
|
220
|
+
export function installPlugin(
|
|
221
|
+
configScope,
|
|
222
|
+
{ cwd = process.cwd(), runClaude = realClaude, removePrevious = true } = {},
|
|
223
|
+
) {
|
|
224
|
+
const scope = PLUGIN_SCOPE[configScope] ?? 'user';
|
|
225
|
+
|
|
226
|
+
// If a marketplace for our plugins is already registered but points at a STALE location (an
|
|
227
|
+
// older package version in a different npx-cache/global/clone path), re-point it here first
|
|
228
|
+
// — otherwise the add below is a no-op and Claude Code keeps serving the old version. Uses
|
|
229
|
+
// the manifest name to find the existing registration; best-effort, never fatal.
|
|
230
|
+
repointMarketplaceIfStale(runClaude, marketplaceManifest().name);
|
|
231
|
+
|
|
232
|
+
try {
|
|
233
|
+
runClaude([
|
|
234
|
+
'plugin',
|
|
235
|
+
'marketplace',
|
|
236
|
+
'add',
|
|
237
|
+
REPOSITORY_ROOT,
|
|
238
|
+
'--scope',
|
|
239
|
+
'user',
|
|
240
|
+
]);
|
|
241
|
+
} catch {
|
|
242
|
+
// Already registered, or the marketplace add is a no-op — install can still proceed.
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Resolve the registered marketplace name AFTER the add, so a fresh registration is seen and
|
|
246
|
+
// a pre-existing one (under any name) is matched by its source path. Doing it here, not at
|
|
247
|
+
// module top, means the name reflects the live registration this run just ensured.
|
|
248
|
+
const targets = marketplaceAndPlugins(runClaude);
|
|
249
|
+
|
|
250
|
+
if (removePrevious) removePreviousInstalls(targets, scope, runClaude);
|
|
251
|
+
|
|
252
|
+
const results = targets.map(({ marketplace, plugin }) => {
|
|
253
|
+
try {
|
|
254
|
+
runClaude([
|
|
255
|
+
'plugin',
|
|
256
|
+
'install',
|
|
257
|
+
`${plugin}@${marketplace}`,
|
|
258
|
+
'--yes',
|
|
259
|
+
'--scope',
|
|
260
|
+
scope,
|
|
261
|
+
]);
|
|
262
|
+
return { plugin, installed: true };
|
|
263
|
+
} catch (error) {
|
|
264
|
+
return { plugin, installed: false, reason: reasonFor(error) };
|
|
265
|
+
}
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
const installed = results.every((result) => result.installed);
|
|
269
|
+
if (installed) return { installed: true, scope, results };
|
|
270
|
+
|
|
271
|
+
const failed = results.filter((result) => !result.installed);
|
|
272
|
+
return {
|
|
273
|
+
installed: false,
|
|
274
|
+
scope,
|
|
275
|
+
results,
|
|
276
|
+
reason: failed
|
|
277
|
+
.map((result) => `${result.plugin}: ${result.reason}`)
|
|
278
|
+
.join('; '),
|
|
279
|
+
cwd,
|
|
280
|
+
};
|
|
281
|
+
}
|