harness-everything 0.18.0 → 0.19.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 +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/AGENTS.md +1 -1
- package/CHANGELOG.md +13 -0
- package/README.md +8 -1
- package/bin/cli.js +16 -0
- package/ci/invariant-routing.test.js +8 -5
- package/ci/mechanism-2ao-plugin-sync.test.js +163 -0
- package/docs/architecture.md +12 -9
- package/docs/platform-capabilities.md +22 -2
- package/docs/platform-compatibility.json +4 -4
- package/docs/routing.md +11 -8
- package/docs/troubleshooting.md +8 -0
- package/docs/workflow-runtime.md +11 -8
- package/harness-everything/SKILL.md +3 -3
- package/harness-everything/references/triage-and-tiers.md +14 -9
- package/harness-everything/scripts/kernel-router-core.js +12 -8
- package/opencode-plugin/plugin.json +1 -1
- package/package.json +4 -2
- package/plugins/harness-everything/.codex-plugin/plugin.json +1 -1
- package/plugins/harness-everything/plugin.json +1 -1
- package/plugins/harness-everything/scripts/lib/advisory-text.js +9 -8
- package/plugins/harness-everything/skills/harness-everything/SKILL.md +3 -3
- package/plugins/harness-everything/skills/harness-everything/references/triage-and-tiers.md +14 -9
- package/plugins/harness-everything/skills/harness-everything/scripts/kernel-router-core.js +12 -8
- package/scripts/lib/advisory-text.js +9 -8
- package/scripts/plugin-sync.js +465 -0
- package/scripts/plugin-sync.ps1 +5 -0
- package/scripts/plugin-sync.sh +5 -0
|
@@ -0,0 +1,465 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Synchronize the native Harness Everything plugin on Claude Code and Codex.
|
|
6
|
+
*
|
|
7
|
+
* This is deliberately separate from installer.js: installer.js manages the
|
|
8
|
+
* portable skills layout, while this command talks to each host's native
|
|
9
|
+
* marketplace/plugin CLI. The installed-state branch is intentionally
|
|
10
|
+
* explicit: an existing plugin is updated, never silently re-added.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const { spawnSync } = require('child_process');
|
|
14
|
+
|
|
15
|
+
const DEFAULTS = Object.freeze({
|
|
16
|
+
marketplace: 'harness-everything',
|
|
17
|
+
plugin: 'harness-everything',
|
|
18
|
+
repository: process.env.HARNESS_PLUGIN_REPOSITORY || 'dyphn1/Harness-everything',
|
|
19
|
+
ref: process.env.HARNESS_PLUGIN_REF || 'main'
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
function defaultRunner(command, args) {
|
|
23
|
+
const result = spawnSync(command, args, {
|
|
24
|
+
encoding: 'utf8',
|
|
25
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
26
|
+
windowsHide: true
|
|
27
|
+
});
|
|
28
|
+
return {
|
|
29
|
+
status: result.status === null ? 1 : result.status,
|
|
30
|
+
stdout: result.stdout || '',
|
|
31
|
+
stderr: result.stderr || '',
|
|
32
|
+
error: result.error || null
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function shellQuote(value) {
|
|
37
|
+
const text = String(value);
|
|
38
|
+
return /^[A-Za-z0-9_./:@%+=,-]+$/.test(text) ? text : JSON.stringify(text);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function commandLine(command, args) {
|
|
42
|
+
return [command, ...args].map(shellQuote).join(' ');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function parseJsonOutput(output) {
|
|
46
|
+
const text = String(output || '').trim();
|
|
47
|
+
if (!text) return null;
|
|
48
|
+
try {
|
|
49
|
+
return JSON.parse(text);
|
|
50
|
+
} catch (_) {
|
|
51
|
+
// Some host versions print a short status line before one-line JSON.
|
|
52
|
+
const lines = text.split(/\r?\n/).map(line => line.trim()).filter(Boolean);
|
|
53
|
+
for (let index = lines.length - 1; index >= 0; index--) {
|
|
54
|
+
try {
|
|
55
|
+
return JSON.parse(lines[index]);
|
|
56
|
+
} catch (__) {
|
|
57
|
+
// Keep looking for the machine-readable line.
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function collectObjects(value, out = [], keyHint = '') {
|
|
65
|
+
if (Array.isArray(value)) {
|
|
66
|
+
value.forEach((entry, index) => collectObjects(entry, out, String(index)));
|
|
67
|
+
} else if (value && typeof value === 'object') {
|
|
68
|
+
out.push({ value, keyHint });
|
|
69
|
+
for (const [key, child] of Object.entries(value)) collectObjects(child, out, key);
|
|
70
|
+
}
|
|
71
|
+
return out;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function collectStrings(value, out = []) {
|
|
75
|
+
if (typeof value === 'string') out.push(value);
|
|
76
|
+
else if (Array.isArray(value)) value.forEach(entry => collectStrings(entry, out));
|
|
77
|
+
else if (value && typeof value === 'object') Object.values(value).forEach(entry => collectStrings(entry, out));
|
|
78
|
+
return out;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function exactString(value, expected) {
|
|
82
|
+
return typeof value === 'string' && value.trim() === expected;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function findMarketplace(payload, marketplace) {
|
|
86
|
+
if (collectStrings(payload).some(value => exactString(value, marketplace))) return true;
|
|
87
|
+
return collectObjects(payload).some(({ value }) => {
|
|
88
|
+
const candidates = [
|
|
89
|
+
value.name,
|
|
90
|
+
value.marketplace,
|
|
91
|
+
value.marketplaceName,
|
|
92
|
+
value.slug,
|
|
93
|
+
value.id,
|
|
94
|
+
value.source && value.source.name,
|
|
95
|
+
value.source && value.source.id
|
|
96
|
+
];
|
|
97
|
+
return candidates.some(candidate => exactString(candidate, marketplace));
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function findPlugin(payload, plugin, marketplace) {
|
|
102
|
+
const pluginId = `${plugin}@${marketplace}`;
|
|
103
|
+
return collectStrings(payload).some(value => value.trim() === pluginId) || collectObjects(payload).some(({ value, keyHint }) => {
|
|
104
|
+
const identifiers = [value.id, value.pluginId, value.identifier, keyHint];
|
|
105
|
+
if (identifiers.some(candidate => exactString(candidate, pluginId))) return true;
|
|
106
|
+
const names = [value.name, value.plugin, value.pluginName];
|
|
107
|
+
if (!names.some(candidate => exactString(candidate, plugin))) return false;
|
|
108
|
+
const sources = [
|
|
109
|
+
value.marketplace,
|
|
110
|
+
value.marketplaceName,
|
|
111
|
+
value.source && value.source.name,
|
|
112
|
+
value.source && value.source.id
|
|
113
|
+
].filter(Boolean);
|
|
114
|
+
return sources.length === 0 || sources.some(source => exactString(source, marketplace));
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function escapeRegExp(value) {
|
|
119
|
+
return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function textHasMarketplace(output, marketplace) {
|
|
123
|
+
const expression = new RegExp(`(^|[\\s"'])${escapeRegExp(marketplace)}($|[\\s"'@])`, 'm');
|
|
124
|
+
return expression.test(String(output || ''));
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function textHasPlugin(output, plugin, marketplace) {
|
|
128
|
+
const text = String(output || '');
|
|
129
|
+
if (text.includes(`${plugin}@${marketplace}`)) return true;
|
|
130
|
+
const expression = new RegExp(`(^|[\\s"'])${escapeRegExp(plugin)}($|[\\s"'@])`, 'm');
|
|
131
|
+
return expression.test(text);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function resultError(result) {
|
|
135
|
+
const detail = [result && result.stderr, result && result.stdout, result && result.error && result.error.message]
|
|
136
|
+
.map(value => String(value || '').trim())
|
|
137
|
+
.find(Boolean);
|
|
138
|
+
return detail || `exit status ${result && result.status !== undefined ? result.status : 'unknown'}`;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function createContext(options = {}) {
|
|
142
|
+
const runner = options.runner || defaultRunner;
|
|
143
|
+
const log = options.log || (message => console.log(message));
|
|
144
|
+
const warn = options.warn || (message => console.warn(message));
|
|
145
|
+
const context = {
|
|
146
|
+
...DEFAULTS,
|
|
147
|
+
...options,
|
|
148
|
+
runner,
|
|
149
|
+
log,
|
|
150
|
+
warn,
|
|
151
|
+
dryRun: Boolean(options.dryRun),
|
|
152
|
+
commandAvailable: options.commandAvailable || (command => {
|
|
153
|
+
const result = runner(command, ['--version'], { probe: true });
|
|
154
|
+
return result && result.status === 0 && !result.error;
|
|
155
|
+
})
|
|
156
|
+
};
|
|
157
|
+
context.repositorySource = options.repositorySource || (
|
|
158
|
+
/^https?:\/\//.test(context.repository) || /^git@/.test(context.repository)
|
|
159
|
+
? context.repository
|
|
160
|
+
: `https://github.com/${context.repository}.git`
|
|
161
|
+
);
|
|
162
|
+
return context;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function invoke(context, command, args, { mutate = false, label = command } = {}) {
|
|
166
|
+
const rendered = commandLine(command, args);
|
|
167
|
+
if (mutate && context.dryRun) {
|
|
168
|
+
context.log(`[dry-run] ${rendered}`);
|
|
169
|
+
return { status: 0, stdout: '', stderr: '', dryRun: true };
|
|
170
|
+
}
|
|
171
|
+
const result = context.runner(command, args, { label });
|
|
172
|
+
if (!result || result.status !== 0) {
|
|
173
|
+
context.warn(`[failed] ${rendered}\n ${resultError(result)}`);
|
|
174
|
+
}
|
|
175
|
+
return result || { status: 1, stdout: '', stderr: 'runner returned no result' };
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function query(context, command, args) {
|
|
179
|
+
return context.runner(command, args, { query: true }) || { status: 1, stdout: '', stderr: 'runner returned no result' };
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function listState(context, { command, jsonArgs, plainArgs, detectJson, detectText }) {
|
|
183
|
+
const jsonResult = query(context, command, jsonArgs);
|
|
184
|
+
if (jsonResult.status === 0) {
|
|
185
|
+
const payload = parseJsonOutput(jsonResult.stdout);
|
|
186
|
+
if (payload !== null) return { known: true, value: detectJson(payload), result: jsonResult };
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const plainResult = query(context, command, plainArgs);
|
|
190
|
+
if (plainResult.status === 0 && String(plainResult.stdout || '').trim()) {
|
|
191
|
+
return { known: true, value: detectText(plainResult.stdout), result: plainResult };
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return {
|
|
195
|
+
known: false,
|
|
196
|
+
value: false,
|
|
197
|
+
error: `${resultError(jsonResult)}; fallback: ${resultError(plainResult)}`
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function queryClaudeMarketplace(context) {
|
|
202
|
+
return listState(context, {
|
|
203
|
+
command: 'claude',
|
|
204
|
+
jsonArgs: ['plugin', 'marketplace', 'list', '--json'],
|
|
205
|
+
plainArgs: ['plugin', 'marketplace', 'list'],
|
|
206
|
+
detectJson: payload => findMarketplace(payload, context.marketplace),
|
|
207
|
+
detectText: output => textHasMarketplace(output, context.marketplace)
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function queryClaudePlugin(context) {
|
|
212
|
+
return listState(context, {
|
|
213
|
+
command: 'claude',
|
|
214
|
+
jsonArgs: ['plugin', 'list', '--json'],
|
|
215
|
+
plainArgs: ['plugin', 'list'],
|
|
216
|
+
detectJson: payload => findPlugin(payload, context.plugin, context.marketplace),
|
|
217
|
+
detectText: output => textHasPlugin(output, context.plugin, context.marketplace)
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function queryCodexMarketplace(context) {
|
|
222
|
+
return listState(context, {
|
|
223
|
+
command: 'codex',
|
|
224
|
+
jsonArgs: ['plugin', 'marketplace', 'list', '--json'],
|
|
225
|
+
plainArgs: ['plugin', 'marketplace', 'list'],
|
|
226
|
+
detectJson: payload => findMarketplace(payload, context.marketplace),
|
|
227
|
+
detectText: output => textHasMarketplace(output, context.marketplace)
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function queryCodexPlugin(context) {
|
|
232
|
+
const state = listState(context, {
|
|
233
|
+
command: 'codex',
|
|
234
|
+
jsonArgs: ['plugin', 'list', '--marketplace', context.marketplace, '--json'],
|
|
235
|
+
plainArgs: ['plugin', 'list', '--marketplace', context.marketplace],
|
|
236
|
+
detectJson: payload => findPlugin(payload, context.plugin, context.marketplace),
|
|
237
|
+
detectText: output => textHasPlugin(output, context.plugin, context.marketplace)
|
|
238
|
+
});
|
|
239
|
+
if (state.known) return state;
|
|
240
|
+
|
|
241
|
+
// Older Codex builds may not accept --marketplace on plugin list. A full
|
|
242
|
+
// list still lets us fail closed while supporting those builds when JSON is
|
|
243
|
+
// available.
|
|
244
|
+
return listState(context, {
|
|
245
|
+
command: 'codex',
|
|
246
|
+
jsonArgs: ['plugin', 'list', '--json'],
|
|
247
|
+
plainArgs: ['plugin', 'list'],
|
|
248
|
+
detectJson: payload => findPlugin(payload, context.plugin, context.marketplace),
|
|
249
|
+
detectText: output => textHasPlugin(output, context.plugin, context.marketplace)
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function ensureClaudeMarketplace(context) {
|
|
254
|
+
const state = queryClaudeMarketplace(context);
|
|
255
|
+
if (!state.known) context.warn(`[notice] Claude marketplace state is unavailable; ensuring ${context.marketplace} is configured.`);
|
|
256
|
+
if (!state.known || !state.value) {
|
|
257
|
+
const added = invoke(context, 'claude', [
|
|
258
|
+
'plugin', 'marketplace', 'add', context.repository,
|
|
259
|
+
'--scope', 'user'
|
|
260
|
+
], { mutate: true, label: 'claude marketplace add' });
|
|
261
|
+
if (added.status !== 0) return { ok: false, error: resultError(added) };
|
|
262
|
+
}
|
|
263
|
+
return { ok: true };
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function ensureCodexMarketplace(context) {
|
|
267
|
+
const state = queryCodexMarketplace(context);
|
|
268
|
+
if (!state.known) context.warn(`[notice] Codex marketplace state is unavailable; ensuring ${context.marketplace} is configured.`);
|
|
269
|
+
if (!state.known || !state.value) {
|
|
270
|
+
const added = invoke(context, 'codex', [
|
|
271
|
+
'plugin', 'marketplace', 'add', context.repositorySource,
|
|
272
|
+
'--ref', context.ref,
|
|
273
|
+
'--json'
|
|
274
|
+
], { mutate: true, label: 'codex marketplace add' });
|
|
275
|
+
if (added.status !== 0) return { ok: false, error: resultError(added) };
|
|
276
|
+
}
|
|
277
|
+
return { ok: true };
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function refreshClaudeMarketplace(context) {
|
|
281
|
+
const updated = invoke(context, 'claude', [
|
|
282
|
+
'plugin', 'marketplace', 'update', context.marketplace
|
|
283
|
+
], { mutate: true, label: 'claude marketplace update' });
|
|
284
|
+
return updated.status === 0
|
|
285
|
+
? { ok: true }
|
|
286
|
+
: { ok: false, error: resultError(updated) };
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function refreshCodexMarketplace(context) {
|
|
290
|
+
const upgraded = invoke(context, 'codex', [
|
|
291
|
+
'plugin', 'marketplace', 'upgrade', context.marketplace,
|
|
292
|
+
'--json'
|
|
293
|
+
], { mutate: true, label: 'codex marketplace upgrade' });
|
|
294
|
+
return upgraded.status === 0
|
|
295
|
+
? { ok: true }
|
|
296
|
+
: { ok: false, error: resultError(upgraded) };
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function verifyPlugin(context, host, queryPlugin) {
|
|
300
|
+
if (context.dryRun) return { ok: true, skipped: true };
|
|
301
|
+
const state = queryPlugin(context);
|
|
302
|
+
if (!state.known) return { ok: false, error: `${host} plugin state could not be verified: ${state.error}` };
|
|
303
|
+
if (!state.value) return { ok: false, error: `${host} reports ${context.plugin}@${context.marketplace} is not installed after synchronization` };
|
|
304
|
+
return { ok: true };
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function syncClaude(context) {
|
|
308
|
+
if (!context.commandAvailable('claude')) return { host: 'claude', status: 'skipped', reason: 'claude CLI not found' };
|
|
309
|
+
const marketplace = ensureClaudeMarketplace(context);
|
|
310
|
+
if (!marketplace.ok) return { host: 'claude', status: 'failed', error: marketplace.error };
|
|
311
|
+
const installed = queryClaudePlugin(context);
|
|
312
|
+
if (!installed.known) return { host: 'claude', status: 'failed', error: `Claude plugin state could not be determined: ${installed.error}` };
|
|
313
|
+
const refreshed = refreshClaudeMarketplace(context);
|
|
314
|
+
if (!refreshed.ok) return { host: 'claude', status: 'failed', error: refreshed.error };
|
|
315
|
+
|
|
316
|
+
const action = installed.value ? 'update' : 'install';
|
|
317
|
+
const result = invoke(context, 'claude', [
|
|
318
|
+
'plugin', action, `${context.plugin}@${context.marketplace}`,
|
|
319
|
+
'--scope', 'user', '--yes', '--json'
|
|
320
|
+
], { mutate: true, label: `claude plugin ${action}` });
|
|
321
|
+
if (result.status !== 0) return { host: 'claude', status: 'failed', error: resultError(result) };
|
|
322
|
+
|
|
323
|
+
const verification = verifyPlugin(context, 'Claude', queryClaudePlugin);
|
|
324
|
+
if (!verification.ok) return { host: 'claude', status: 'failed', error: verification.error };
|
|
325
|
+
return { host: 'claude', status: 'synchronized', action };
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function syncCodex(context) {
|
|
329
|
+
if (!context.commandAvailable('codex')) return { host: 'codex', status: 'skipped', reason: 'codex CLI not found' };
|
|
330
|
+
const marketplace = ensureCodexMarketplace(context);
|
|
331
|
+
if (!marketplace.ok) return { host: 'codex', status: 'failed', error: marketplace.error };
|
|
332
|
+
const installed = queryCodexPlugin(context);
|
|
333
|
+
if (!installed.known) return { host: 'codex', status: 'failed', error: `Codex plugin state could not be determined: ${installed.error}` };
|
|
334
|
+
|
|
335
|
+
let action;
|
|
336
|
+
let result;
|
|
337
|
+
if (installed.value) {
|
|
338
|
+
// Codex currently refreshes an installed marketplace plugin through the
|
|
339
|
+
// marketplace upgrade command. Do not fall back to `plugin add` here:
|
|
340
|
+
// an installed plugin must take the update path.
|
|
341
|
+
action = 'update';
|
|
342
|
+
result = refreshCodexMarketplace(context);
|
|
343
|
+
if (result.ok) result = { status: 0, stdout: '', stderr: '' };
|
|
344
|
+
else result = { status: 1, stdout: '', stderr: result.error };
|
|
345
|
+
} else {
|
|
346
|
+
action = 'install';
|
|
347
|
+
result = refreshCodexMarketplace(context);
|
|
348
|
+
if (result.ok) {
|
|
349
|
+
result = invoke(context, 'codex', [
|
|
350
|
+
'plugin', 'add', `${context.plugin}@${context.marketplace}`, '--json'
|
|
351
|
+
], { mutate: true, label: 'codex plugin install' });
|
|
352
|
+
} else {
|
|
353
|
+
result = { status: 1, stdout: '', stderr: result.error };
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
if (result.status !== 0) return { host: 'codex', status: 'failed', error: resultError(result) };
|
|
357
|
+
|
|
358
|
+
const verification = verifyPlugin(context, 'Codex', queryCodexPlugin);
|
|
359
|
+
if (!verification.ok) return { host: 'codex', status: 'failed', error: verification.error };
|
|
360
|
+
return { host: 'codex', status: 'synchronized', action };
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function parseArgs(argv) {
|
|
364
|
+
const options = { host: 'all', dryRun: false, json: false };
|
|
365
|
+
for (let index = 0; index < argv.length; index++) {
|
|
366
|
+
const arg = argv[index];
|
|
367
|
+
if (arg === '--help' || arg === '-h') options.help = true;
|
|
368
|
+
else if (arg === '--dry-run') options.dryRun = true;
|
|
369
|
+
else if (arg === '--json') options.json = true;
|
|
370
|
+
else if (arg === '--host') {
|
|
371
|
+
options.host = argv[++index];
|
|
372
|
+
if (!options.host) throw new Error('--host requires all, claude, or codex');
|
|
373
|
+
} else if (arg === '--repository') {
|
|
374
|
+
options.repository = argv[++index];
|
|
375
|
+
if (!options.repository) throw new Error('--repository requires a repository or URL');
|
|
376
|
+
} else if (arg === '--ref') {
|
|
377
|
+
options.ref = argv[++index];
|
|
378
|
+
if (!options.ref) throw new Error('--ref requires a git ref');
|
|
379
|
+
} else {
|
|
380
|
+
throw new Error(`Unknown option: ${arg}`);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
options.host = String(options.host).toLowerCase();
|
|
384
|
+
if (!['all', 'claude', 'codex'].includes(options.host)) throw new Error(`Unsupported host: ${options.host}`);
|
|
385
|
+
return options;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function help() {
|
|
389
|
+
return `Usage: node scripts/plugin-sync.js [options]
|
|
390
|
+
|
|
391
|
+
Synchronize the native Harness Everything plugin on installed host CLIs.
|
|
392
|
+
Existing installations take the explicit update/upgrade path; absent
|
|
393
|
+
installations take the install path.
|
|
394
|
+
|
|
395
|
+
Options:
|
|
396
|
+
--host <all|claude|codex> Limit synchronization to one host (default: all)
|
|
397
|
+
--repository <repo|url> Marketplace source (default: dyphn1/Harness-everything)
|
|
398
|
+
--ref <git-ref> Codex marketplace ref (default: main)
|
|
399
|
+
--dry-run Detect state and print mutating commands
|
|
400
|
+
--json Print a machine-readable summary
|
|
401
|
+
-h, --help Show this help
|
|
402
|
+
`;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function sync(options, dependencies = {}) {
|
|
406
|
+
const context = createContext({ ...options, ...dependencies });
|
|
407
|
+
const hosts = context.host === 'all' ? ['claude', 'codex'] : [context.host];
|
|
408
|
+
const results = hosts.map(host => host === 'claude' ? syncClaude(context) : syncCodex(context));
|
|
409
|
+
const summary = {
|
|
410
|
+
repository: context.repository,
|
|
411
|
+
marketplace: context.marketplace,
|
|
412
|
+
plugin: context.plugin,
|
|
413
|
+
dryRun: context.dryRun,
|
|
414
|
+
results,
|
|
415
|
+
synchronized: results.filter(result => result.status === 'synchronized').length,
|
|
416
|
+
skipped: results.filter(result => result.status === 'skipped').length,
|
|
417
|
+
failed: results.filter(result => result.status === 'failed').length
|
|
418
|
+
};
|
|
419
|
+
return summary;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
function main(argv = process.argv.slice(2)) {
|
|
423
|
+
let options;
|
|
424
|
+
try {
|
|
425
|
+
options = parseArgs(argv);
|
|
426
|
+
} catch (error) {
|
|
427
|
+
console.error(`[Error] ${error.message}`);
|
|
428
|
+
console.error(help());
|
|
429
|
+
return 2;
|
|
430
|
+
}
|
|
431
|
+
if (options.help) {
|
|
432
|
+
console.log(help());
|
|
433
|
+
return 0;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
const summary = sync(options);
|
|
437
|
+
if (options.json) console.log(JSON.stringify(summary, null, 2));
|
|
438
|
+
else {
|
|
439
|
+
for (const result of summary.results) {
|
|
440
|
+
if (result.status === 'synchronized') console.log(`[ok] ${result.host}: ${result.action}`);
|
|
441
|
+
else if (result.status === 'skipped') console.log(`[skip] ${result.host}: ${result.reason}`);
|
|
442
|
+
else console.error(`[fail] ${result.host}: ${result.error}`);
|
|
443
|
+
}
|
|
444
|
+
if (summary.synchronized === 0 && summary.failed === 0) {
|
|
445
|
+
console.error('No supported host CLI was found. Install Claude Code and/or Codex, then retry.');
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
return summary.failed > 0 || summary.synchronized === 0 ? 1 : 0;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
if (require.main === module) process.exitCode = main();
|
|
452
|
+
|
|
453
|
+
module.exports = {
|
|
454
|
+
DEFAULTS,
|
|
455
|
+
commandLine,
|
|
456
|
+
createContext,
|
|
457
|
+
findMarketplace,
|
|
458
|
+
findPlugin,
|
|
459
|
+
main,
|
|
460
|
+
parseArgs,
|
|
461
|
+
parseJsonOutput,
|
|
462
|
+
sync,
|
|
463
|
+
textHasMarketplace,
|
|
464
|
+
textHasPlugin
|
|
465
|
+
};
|