audit-tools 0.31.4 → 0.32.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.
@@ -0,0 +1,1187 @@
1
+ import { mkdir, mkdtemp, readFile, rm, stat, unlink, writeFile } from 'node:fs/promises';
2
+ import { tmpdir } from 'node:os';
3
+ import { dirname, join, relative, resolve } from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ import {
6
+ fileExists,
7
+ readJson,
8
+ readTextIfExists,
9
+ writeGeneratedJson,
10
+ writeGeneratedMarkdown,
11
+ writeManagedMarkdown,
12
+ writeMergedGeneratedJson,
13
+ } from './remediate-code-wrapper-io.mjs';
14
+ import {
15
+ assertOpenCodeRemediatePermissionConfig,
16
+ buildMergedOpenCodeProjectConfig,
17
+ } from './remediate-code-wrapper-opencode.mjs';
18
+ import {
19
+ findLegacyRemediateCodeSurfaceFiles,
20
+ removeLegacyRemediateCodeSurfaceFiles,
21
+ } from './remediate-code-wrapper-legacy.mjs';
22
+ import {
23
+ renderVSCodeAgentFile,
24
+ renderCodexAutomationRecipe,
25
+ renderAntigravityPlanningGuide,
26
+ renderGeminiCommandToml as _renderGeminiCommandTomlImpl,
27
+ } from './remediate-code-wrapper-install-renderers.mjs';
28
+
29
+ const repoRoot = join(dirname(fileURLToPath(import.meta.url)), '..');
30
+ const promptAssetPath = join(repoRoot, 'skills', 'remediate-code', 'remediate-code.prompt.md');
31
+ const skillAssetPath = join(repoRoot, 'skills', 'remediate-code', 'SKILL.md');
32
+ const INSTALL_MARKER_START = '<!-- remediate-code:begin -->';
33
+ const INSTALL_MARKER_END = '<!-- remediate-code:end -->';
34
+ const INSTALL_GUIDE_FILENAME = 'GETTING-STARTED.md';
35
+ const INSTALL_MANIFEST_FILENAME = 'manifest.json';
36
+ const DEFAULT_INSTALL_HOST = 'all';
37
+ const INSTALLED_PROMPT_FILENAME = 'remediate-code.import.md';
38
+ const INSTALL_DIR = '.remediate-code';
39
+
40
+ function hasFlag(argv, name) {
41
+ return argv.includes(name);
42
+ }
43
+
44
+ function getFlag(argv, name) {
45
+ const index = argv.indexOf(name);
46
+ if (index < 0) return undefined;
47
+ return argv[index + 1];
48
+ }
49
+
50
+ function requireFlagValue(argv, name) {
51
+ const value = getFlag(argv, name);
52
+ if (!value) {
53
+ throw new Error(`${name} requires a value.`);
54
+ }
55
+ return value;
56
+ }
57
+
58
+ function normalizeNewlines(value) {
59
+ return value.replace(/\r\n/g, '\n');
60
+ }
61
+
62
+ function splitFrontmatter(markdown) {
63
+ const normalized = normalizeNewlines(markdown);
64
+ const match = normalized.match(/^---\n([\s\S]*?)\n---\n?/u);
65
+ if (!match) {
66
+ return { frontmatter: null, body: normalized };
67
+ }
68
+
69
+ return {
70
+ frontmatter: match[1],
71
+ body: normalized.slice(match[0].length),
72
+ };
73
+ }
74
+
75
+ function renderFrontmatter(fields) {
76
+ const entries = Object.entries(fields).filter(([, value]) => {
77
+ if (value === undefined || value === null) {
78
+ return false;
79
+ }
80
+
81
+ if (typeof value === 'string') {
82
+ return value.length > 0;
83
+ }
84
+
85
+ return true;
86
+ });
87
+ if (entries.length === 0) {
88
+ return '';
89
+ }
90
+
91
+ return [
92
+ '---',
93
+ ...entries.map(([key, value]) => `${key}: ${value}`),
94
+ '---',
95
+ '',
96
+ ].join('\n');
97
+ }
98
+
99
+ function renderPromptFile(fields, body) {
100
+ return `${renderFrontmatter(fields)}${body.trimStart()}`;
101
+ }
102
+
103
+ function toRepoRelativePath(root, targetPath) {
104
+ const value = relative(root, targetPath).replace(/\\/g, '/');
105
+ return value.length > 0 ? value : '.';
106
+ }
107
+
108
+ function buildInstallDirective(relativePromptPath) {
109
+ return [
110
+ INSTALL_MARKER_START,
111
+ '## /remediate-code',
112
+ 'When the user enters `/remediate-code`, treat it as this repository\'s autonomous remediation workflow.',
113
+ `If your host does not automatically register the installed slash command file, load and follow [the repo-local remediate directive](${relativePromptPath.replace(/\\/g, '/')}).`,
114
+ 'Normal usage should stay conversation-first and avoid manual `--root`, provider flags, or model-selection arguments.',
115
+ INSTALL_MARKER_END,
116
+ ].join('\n');
117
+ }
118
+
119
+ // Host-asset render functions (renderVSCodeAgentFile, renderCodexAutomationRecipe,
120
+ // renderAntigravityPlanningGuide, renderGeminiCommandToml) are imported from
121
+ // remediate-code-wrapper-install-renderers.mjs. Each one is a thin wrapper that
122
+ // derives its asset from the ONE canonical loader prompt body via the shared
123
+ // renderHostAsset helper — no host re-authors loader prose, so no asset can drift
124
+ // from the body.
125
+ const renderGeminiCommandToml = _renderGeminiCommandTomlImpl;
126
+
127
+ // Exported for testing only — delegates to the canonical implementation in renderers.
128
+ export const _renderGeminiCommandToml = renderGeminiCommandToml;
129
+
130
+ export const INSTALL_PROFILE_FLAGS = [
131
+ 'writeVSCode',
132
+ 'writeCopilotInstructions',
133
+ 'writeOpenCode',
134
+ 'writeCodex',
135
+ 'writeAntigravity',
136
+ 'writeAgents',
137
+ ];
138
+
139
+ export const INSTALL_HOST_ORDER = [
140
+ 'codex',
141
+ 'opencode',
142
+ 'vscode',
143
+ 'antigravity',
144
+ ];
145
+
146
+ export const INSTALL_HOST_DEFINITIONS = {
147
+ codex: {
148
+ host: 'codex',
149
+ label: 'Codex',
150
+ support_level: 'supported',
151
+ setup_kind: 'global-skill+instructions',
152
+ summary:
153
+ 'Use the global Codex skill installed by npm plus AGENTS fallback instructions for this repository. Repo-local Codex skill bundles are intentionally not generated.',
154
+ primary_path_key: 'agentsInstructionsPath',
155
+ supporting_path_keys: [
156
+ 'installedPromptPath',
157
+ ],
158
+ steps: [
159
+ 'Open this repository in Codex.',
160
+ 'Use the global `/remediate-code` skill installed by `npm install -g audit-tools`.',
161
+ 'If the global skill is unavailable, follow the AGENTS fallback instructions that point at the repo-local prompt asset.',
162
+ ],
163
+ profile: {
164
+ writeAgents: true,
165
+ },
166
+ async verify({ checks, assetPaths, collectVerifyCheck: collect }) {
167
+ await collect(checks, 'codex_global_surface', async () => {
168
+ const content = await readFile(assetPaths.agentsInstructionsPath, 'utf8');
169
+ if (!content.includes('/remediate-code')) {
170
+ throw new Error(`AGENTS instructions do not reference /remediate-code: ${assetPaths.agentsInstructionsPath}`);
171
+ }
172
+ return {
173
+ summary: 'Codex uses the global skill surface with AGENTS fallback instructions.',
174
+ path: assetPaths.agentsInstructionsPath,
175
+ };
176
+ });
177
+ },
178
+ },
179
+ opencode: {
180
+ host: 'opencode',
181
+ label: 'OpenCode',
182
+ support_level: 'supported',
183
+ setup_kind: 'global-command+project-permissions',
184
+ summary:
185
+ 'Use the global OpenCode `/remediate-code` command installed by npm plus generated project permissions.',
186
+ primary_path_key: 'opencodeConfigPath',
187
+ supporting_path_keys: [
188
+ 'agentsInstructionsPath',
189
+ ],
190
+ steps: [
191
+ 'Open this repository in OpenCode.',
192
+ 'Use the global `/remediate-code` command installed by `npm install -g audit-tools`.',
193
+ 'Let OpenCode load the generated `opencode.json` for project permissions; the global command drives `remediate-code next-step` directly.',
194
+ ],
195
+ profile: {
196
+ writeOpenCode: true,
197
+ writeAgents: true,
198
+ },
199
+ async verify({ checks, assetPaths, collectVerifyCheck: collect }) {
200
+ await collect(checks, 'opencode_config', async () => {
201
+ const config = await readJson(assetPaths.opencodeConfigPath, 'OpenCode project config');
202
+ if (config?.command?.['remediate-code']) {
203
+ throw new Error('OpenCode project config must not define command["remediate-code"]; the slash command is global npm-installed state. Run "remediate-code install --host opencode" to remove the stale local command.');
204
+ }
205
+ if (config?.mcp?.remediator) {
206
+ throw new Error('OpenCode project config must not define mcp.remediator; the MCP server is supplied by the global npm-installed config. Run "remediate-code install --host opencode" to remove the stale project-level MCP entry.');
207
+ }
208
+ assertOpenCodeRemediatePermissionConfig(config?.permission, 'permission');
209
+ assertOpenCodeRemediatePermissionConfig(config?.agent?.remediator?.permission, 'agent.remediator.permission');
210
+ return {
211
+ summary: 'OpenCode project config has remediate permissions; /remediate-code is supplied by the global npm-installed config.',
212
+ path: assetPaths.opencodeConfigPath,
213
+ };
214
+ });
215
+ },
216
+ },
217
+ vscode: {
218
+ host: 'vscode',
219
+ label: 'VS Code',
220
+ support_level: 'supported',
221
+ setup_kind: 'prompt+agent',
222
+ summary:
223
+ 'Use the generated prompt file and custom agent for next-step-first VS Code integration.',
224
+ primary_path_key: 'vscodePromptPath',
225
+ supporting_path_keys: [
226
+ 'vscodeAgentPath',
227
+ 'copilotInstructionsPath',
228
+ ],
229
+ steps: [
230
+ 'Open this repository in VS Code with Copilot.',
231
+ 'Invoke `/remediate-code` from the generated prompt or chat so the workflow calls `remediate-code next-step` directly.',
232
+ ],
233
+ profile: {
234
+ writeVSCode: true,
235
+ writeCopilotInstructions: true,
236
+ },
237
+ async verify({ checks, assetPaths, collectVerifyCheck: collect }) {
238
+ await collect(checks, 'vscode_prompt', async () => {
239
+ const content = await readFile(assetPaths.vscodePromptPath, 'utf8');
240
+ if (!content.includes('name: remediate-code')) {
241
+ throw new Error(`VS Code prompt file is missing the expected frontmatter name: ${assetPaths.vscodePromptPath}`);
242
+ }
243
+ const { body: promptBody } = splitFrontmatter(content);
244
+ const { body: sourceBody } = splitFrontmatter(await readFile(promptAssetPath, 'utf8'));
245
+ if (promptBody !== sourceBody.trimStart()) {
246
+ throw new Error(
247
+ `VS Code prompt body is out of sync with the source prompt. Run "remediate-code install --host vscode" or "remediate-code install".`,
248
+ );
249
+ }
250
+ return {
251
+ summary: 'VS Code prompt file is present and uses the source prompt body.',
252
+ path: assetPaths.vscodePromptPath,
253
+ };
254
+ });
255
+ },
256
+ },
257
+ antigravity: {
258
+ host: 'antigravity',
259
+ label: 'Antigravity',
260
+ support_level: 'supported',
261
+ setup_kind: 'agent-skill+gemini-command+planning-guide',
262
+ summary:
263
+ 'Uses the project-scoped .agent/skills/remediate-code/SKILL.md skill, the .gemini/commands/remediate-code.toml slash command, the planning guide, and AGENTS instructions.',
264
+ primary_path_key: 'antigravitySkillPath',
265
+ supporting_path_keys: [
266
+ 'geminiCommandPath',
267
+ 'antigravityPlanningGuidePath',
268
+ 'agentsInstructionsPath',
269
+ 'installedPromptPath',
270
+ ],
271
+ steps: [
272
+ 'Open this repository in Antigravity.',
273
+ 'The remediate-code skill is automatically discovered from .agent/skills/remediate-code/SKILL.md.',
274
+ 'The /remediate-code slash command is also available from .gemini/commands/remediate-code.toml.',
275
+ 'Use `remediate-code next-step` directly.',
276
+ ],
277
+ profile: {
278
+ writeAntigravity: true,
279
+ writeAgents: true,
280
+ },
281
+ async verify({ checks, assetPaths, collectVerifyCheck: collect }) {
282
+ await collect(checks, 'antigravity_skill', async () => {
283
+ const content = await readFile(assetPaths.antigravitySkillPath, 'utf8');
284
+ if (!content.includes('name: remediate-code')) {
285
+ throw new Error('Antigravity skill SKILL.md must contain "name: remediate-code" in frontmatter.');
286
+ }
287
+ return {
288
+ summary: 'Antigravity .agent/skills/remediate-code/SKILL.md is present and valid.',
289
+ path: assetPaths.antigravitySkillPath,
290
+ };
291
+ });
292
+ await collect(checks, 'antigravity_guide', async () => {
293
+ const content = await readFile(assetPaths.antigravityPlanningGuidePath, 'utf8');
294
+ if (!content.includes('--host-models') || !content.includes('next-step')) {
295
+ throw new Error('Antigravity guide must embed the canonical loader body (next-step capability handshake including --host-models).');
296
+ }
297
+ return {
298
+ summary: 'Antigravity planning guide embeds the canonical loader body with the capability handshake.',
299
+ path: assetPaths.antigravityPlanningGuidePath,
300
+ };
301
+ });
302
+ },
303
+ },
304
+ };
305
+
306
+ function supportedInstallHostsMessage() {
307
+ return ['all', 'copilot', ...INSTALL_HOST_ORDER].join(', ');
308
+ }
309
+
310
+ export function getInstallHostKeys(host) {
311
+ if (host === 'all') {
312
+ return INSTALL_HOST_ORDER;
313
+ }
314
+
315
+ if (host === 'copilot') {
316
+ return ['vscode'];
317
+ }
318
+
319
+ if (INSTALL_HOST_DEFINITIONS[host]) {
320
+ return [host];
321
+ }
322
+
323
+ throw new Error(
324
+ `Unsupported host "${host}". Supported hosts: ${supportedInstallHostsMessage()}.`,
325
+ );
326
+ }
327
+
328
+ export function getInstallProfile(host) {
329
+ const profile = Object.fromEntries(
330
+ INSTALL_PROFILE_FLAGS.map((flag) => [flag, false]),
331
+ );
332
+
333
+ for (const hostKey of getInstallHostKeys(host)) {
334
+ const hostProfile = INSTALL_HOST_DEFINITIONS[hostKey].profile;
335
+ for (const flag of INSTALL_PROFILE_FLAGS) {
336
+ profile[flag] = profile[flag] || Boolean(hostProfile[flag]);
337
+ }
338
+ }
339
+
340
+ return profile;
341
+ }
342
+
343
+ export function buildInstallAssetPaths(root, profile) {
344
+ const installedPromptPath = join(root, INSTALL_DIR, 'install', INSTALLED_PROMPT_FILENAME);
345
+ const installedSkillPath = join(root, INSTALL_DIR, 'install', 'SKILL.md');
346
+ const installGuidePath = join(root, INSTALL_DIR, 'install', INSTALL_GUIDE_FILENAME);
347
+ const installManifestPath = join(root, INSTALL_DIR, 'install', INSTALL_MANIFEST_FILENAME);
348
+ return {
349
+ installedPromptPath,
350
+ installedSkillPath,
351
+ installGuidePath,
352
+ installManifestPath,
353
+ agentsInstructionsPath: profile.writeAgents ? join(root, 'AGENTS.md') : null,
354
+ copilotInstructionsPath: profile.writeCopilotInstructions
355
+ ? join(root, '.github', 'copilot-instructions.md')
356
+ : null,
357
+ codexSkillPath: profile.writeCodex
358
+ ? join(root, '.codex', 'skills', 'remediate-code', 'SKILL.md')
359
+ : null,
360
+ codexPromptPath: profile.writeCodex
361
+ ? join(root, '.codex', 'skills', 'remediate-code', 'remediate-code.prompt.md')
362
+ : null,
363
+ codexAutomationRecipePath: profile.writeCodex
364
+ ? join(root, INSTALL_DIR, 'install', 'codex', 'RE-REMEDIATE-AUTOMATION.md')
365
+ : null,
366
+ opencodeConfigPath: profile.writeOpenCode
367
+ ? join(root, 'opencode.json')
368
+ : null,
369
+ vscodePromptPath: profile.writeVSCode
370
+ ? join(root, '.github', 'prompts', 'remediate-code.prompt.md')
371
+ : null,
372
+ vscodeAgentPath: profile.writeVSCode
373
+ ? join(root, '.github', 'agents', 'remediator.agent.md')
374
+ : null,
375
+ antigravityPlanningGuidePath: profile.writeAntigravity
376
+ ? join(root, INSTALL_DIR, 'install', 'antigravity', 'PLANNING-MODE.md')
377
+ : null,
378
+ geminiCommandPath: profile.writeAntigravity
379
+ ? join(root, '.gemini', 'commands', 'remediate-code.toml')
380
+ : null,
381
+ antigravitySkillPath: profile.writeAntigravity
382
+ ? join(root, '.agent', 'skills', 'remediate-code', 'SKILL.md')
383
+ : null,
384
+ };
385
+ }
386
+
387
+ export function buildHostCatalog({ root, host, assets }) {
388
+ return getInstallHostKeys(host)
389
+ .map((hostKey) => {
390
+ const definition = INSTALL_HOST_DEFINITIONS[hostKey];
391
+ const primaryPath = assets[definition.primary_path_key];
392
+ if (!primaryPath) {
393
+ return null;
394
+ }
395
+
396
+ return {
397
+ host: definition.host,
398
+ label: definition.label,
399
+ support_level: definition.support_level,
400
+ setup_kind: definition.setup_kind,
401
+ summary: definition.summary,
402
+ primary_path: primaryPath,
403
+ supporting_paths: definition.supporting_path_keys
404
+ .map((key) => assets[key])
405
+ .filter(Boolean),
406
+ steps: definition.steps,
407
+ };
408
+ })
409
+ .filter(Boolean)
410
+ .map((entry) => ({
411
+ ...entry,
412
+ primary_path: entry.primary_path,
413
+ supporting_paths: entry.supporting_paths,
414
+ repo_relative_primary_path: toRepoRelativePath(root, entry.primary_path),
415
+ repo_relative_supporting_paths: entry.supporting_paths.map((path) => toRepoRelativePath(root, path)),
416
+ }));
417
+ }
418
+
419
+ export function renderInstallGuide({
420
+ root,
421
+ host,
422
+ installedPromptPath,
423
+ installedSkillPath,
424
+ installManifestPath,
425
+ hostGuidance,
426
+ }) {
427
+ const lines = [
428
+ '# remediate-code bootstrap guide',
429
+ '',
430
+ 'The canonical product route is `/remediate-code` in conversation.',
431
+ '',
432
+ 'Shared repo-local assets:',
433
+ `- prompt asset: \`${toRepoRelativePath(root, installedPromptPath)}\``,
434
+ `- skill asset: \`${toRepoRelativePath(root, installedSkillPath)}\``,
435
+ `- host manifest: \`${toRepoRelativePath(root, installManifestPath)}\``,
436
+ '',
437
+ 'Host-specific quick starts:',
438
+ ];
439
+
440
+ for (const guide of hostGuidance) {
441
+ lines.push(`- ${guide.label}: ${guide.summary}`);
442
+ }
443
+
444
+ for (const guide of hostGuidance) {
445
+ lines.push('', `## ${guide.label}`, '');
446
+ lines.push(`Support level: ${guide.support_level}`);
447
+ lines.push(`Setup kind: ${guide.setup_kind}`);
448
+ lines.push('');
449
+ lines.push(guide.summary);
450
+ lines.push('');
451
+ lines.push('Primary repo-local path:');
452
+ lines.push(`- \`${toRepoRelativePath(root, guide.primary_path)}\``);
453
+ if (guide.supporting_paths.length > 0) {
454
+ lines.push('', 'Supporting repo-local paths:');
455
+ for (const path of guide.supporting_paths) {
456
+ lines.push(`- \`${toRepoRelativePath(root, path)}\``);
457
+ }
458
+ }
459
+ lines.push('', 'Recommended steps:');
460
+ for (const step of guide.steps) {
461
+ lines.push(`- ${step}`);
462
+ }
463
+ }
464
+
465
+ lines.push('', 'Backend fallback:');
466
+ lines.push('- from the repository root, run `remediate-code` only when you intentionally need the repo-local backend wrapper');
467
+ lines.push('- run `remediate-code verify-install` after bootstrap when you want to smoke-test the generated launchers and host configs');
468
+ lines.push('- rerun `remediate-code install` to refresh every generated host surface from the shared prompt and skill assets together');
469
+
470
+ if (host !== 'all') {
471
+ lines.push('');
472
+ lines.push(`This install was scoped to \`${host}\`, so assets for other hosts may be intentionally omitted.`);
473
+ }
474
+
475
+ lines.push('');
476
+ return lines.join('\n');
477
+ }
478
+
479
+ async function assertDirectoryExists(path, description) {
480
+ let stats;
481
+ try {
482
+ stats = await stat(path);
483
+ } catch {
484
+ throw new Error(`${description} does not exist: ${path}`);
485
+ }
486
+
487
+ if (!stats.isDirectory()) {
488
+ throw new Error(`${description} is not a directory: ${path}`);
489
+ }
490
+ }
491
+
492
+ async function collectVerifyCheck(target, id, fn) {
493
+ try {
494
+ const details = await fn();
495
+ target.push({
496
+ id,
497
+ status: 'ok',
498
+ ...(details ?? {}),
499
+ });
500
+ } catch (error) {
501
+ target.push({
502
+ id,
503
+ status: 'error',
504
+ summary: error instanceof Error ? error.message : String(error),
505
+ });
506
+ }
507
+ }
508
+
509
+ async function ensureFile(path, description) {
510
+ let stats;
511
+ try {
512
+ stats = await stat(path);
513
+ } catch {
514
+ throw new Error(`${description} does not exist: ${path}`);
515
+ }
516
+
517
+ if (!stats.isFile()) {
518
+ throw new Error(`${description} is not a file: ${path}`);
519
+ }
520
+
521
+ return stats;
522
+ }
523
+
524
+ export async function writeCoreInstallAssets(root, assetPaths, promptSource, skillSource) {
525
+ const results = [];
526
+ const legacyInstalledPromptPath = join(root, INSTALL_DIR, 'install', 'remediate-code.prompt.md');
527
+ if (await fileExists(legacyInstalledPromptPath)) {
528
+ await unlink(legacyInstalledPromptPath).catch(() => {});
529
+ }
530
+ results.push(await writeGeneratedMarkdown(assetPaths.installedPromptPath, promptSource));
531
+ results.push(await writeGeneratedMarkdown(assetPaths.installedSkillPath, skillSource));
532
+
533
+ const compatibilityBlockTargets = [
534
+ assetPaths.agentsInstructionsPath,
535
+ assetPaths.copilotInstructionsPath,
536
+ ].filter(Boolean);
537
+ for (const targetPath of compatibilityBlockTargets) {
538
+ results.push(
539
+ await writeManagedMarkdown(
540
+ targetPath,
541
+ buildInstallDirective(
542
+ relative(dirname(targetPath), assetPaths.installedPromptPath) || `./${INSTALL_DIR}/install/${INSTALLED_PROMPT_FILENAME}`,
543
+ ),
544
+ ),
545
+ );
546
+ }
547
+
548
+ results.push(...await removeLegacyRemediateCodeSurfaceFiles(root));
549
+ return results;
550
+ }
551
+
552
+ export async function writeCodexAssets(assetPaths, promptSource, skillSource, promptBody) {
553
+ return [
554
+ await writeGeneratedMarkdown(assetPaths.codexSkillPath, skillSource),
555
+ await writeGeneratedMarkdown(assetPaths.codexPromptPath, promptSource),
556
+ await writeGeneratedMarkdown(assetPaths.codexAutomationRecipePath, renderCodexAutomationRecipe(promptBody)),
557
+ ];
558
+ }
559
+
560
+ export async function writeOpenCodeAssets(assetPaths, root) {
561
+ return [
562
+ await writeMergedGeneratedJson(
563
+ assetPaths.opencodeConfigPath,
564
+ 'OpenCode project config',
565
+ (existing) => buildMergedOpenCodeProjectConfig(existing, root),
566
+ ),
567
+ ];
568
+ }
569
+
570
+ export async function writeVSCodeAssets(assetPaths, promptBody) {
571
+ return [
572
+ await writeGeneratedMarkdown(
573
+ assetPaths.vscodePromptPath,
574
+ renderPromptFile(
575
+ {
576
+ name: 'remediate-code',
577
+ description: 'Autonomous local-loop remediation',
578
+ agent: 'remediator',
579
+ },
580
+ promptBody,
581
+ ),
582
+ ),
583
+ await writeGeneratedMarkdown(assetPaths.vscodeAgentPath, renderVSCodeAgentFile(promptBody)),
584
+ ];
585
+ }
586
+
587
+ export async function writeAntigravityAssets(assetPaths, promptBody, skillSource) {
588
+ return [
589
+ await writeGeneratedMarkdown(
590
+ assetPaths.antigravityPlanningGuidePath,
591
+ renderAntigravityPlanningGuide(promptBody),
592
+ ),
593
+ await writeGeneratedMarkdown(assetPaths.geminiCommandPath, renderGeminiCommandToml(promptBody)),
594
+ await writeGeneratedMarkdown(assetPaths.antigravitySkillPath, skillSource),
595
+ ];
596
+ }
597
+
598
+ export async function installBootstrap(argv, options = {}) {
599
+ const host = (getFlag(argv, '--host') ?? DEFAULT_INSTALL_HOST).toLowerCase();
600
+ const root = resolve(getFlag(argv, '--root') ?? '.');
601
+ await assertDirectoryExists(root, 'Target repository root');
602
+ const profile = getInstallProfile(host);
603
+ const promptSource = await readFile(promptAssetPath, 'utf8');
604
+ const skillSource = (await readFile(skillAssetPath, 'utf8')).replace(/\r\n/g, '\n');
605
+ const { body: promptBody } = splitFrontmatter(promptSource);
606
+ const assetPaths = buildInstallAssetPaths(root, profile);
607
+ const {
608
+ installedPromptPath,
609
+ installedSkillPath,
610
+ installGuidePath,
611
+ installManifestPath,
612
+ } = assetPaths;
613
+
614
+ const results = [];
615
+ results.push(...await writeCoreInstallAssets(root, assetPaths, promptSource, skillSource));
616
+
617
+ if (profile.writeCodex) {
618
+ results.push(...await writeCodexAssets(assetPaths, promptSource, skillSource, promptBody));
619
+ }
620
+ if (profile.writeOpenCode) {
621
+ results.push(...await writeOpenCodeAssets(assetPaths, root));
622
+ }
623
+ if (profile.writeVSCode) {
624
+ results.push(...await writeVSCodeAssets(assetPaths, promptBody));
625
+ }
626
+ if (profile.writeAntigravity) {
627
+ results.push(...await writeAntigravityAssets(assetPaths, promptBody, skillSource));
628
+ }
629
+
630
+ const hostGuidance = buildHostCatalog({
631
+ root,
632
+ host,
633
+ assets: assetPaths,
634
+ });
635
+
636
+ const installManifest = {
637
+ contract_version: 'remediate-code-install/v1alpha1',
638
+ host,
639
+ repo_root: root,
640
+ installed_prompt_path: installedPromptPath,
641
+ installed_skill_path: installedSkillPath,
642
+ install_guide_path: installGuidePath,
643
+ install_manifest_path: installManifestPath,
644
+ source_prompt_path: resolve(promptAssetPath),
645
+ source_skill_path: resolve(skillAssetPath),
646
+ asset_paths: assetPaths,
647
+ hosts: hostGuidance,
648
+ };
649
+
650
+ results.push(
651
+ await writeGeneratedMarkdown(
652
+ installGuidePath,
653
+ renderInstallGuide({
654
+ root,
655
+ host,
656
+ installedPromptPath,
657
+ installedSkillPath,
658
+ installManifestPath,
659
+ hostGuidance,
660
+ }),
661
+ ),
662
+ );
663
+ results.push(await writeGeneratedJson(installManifestPath, installManifest));
664
+
665
+ const sessionConfigPath = join(root, '.audit-tools', 'remediation', 'session-config.json');
666
+ if (!(await fileExists(sessionConfigPath))) {
667
+ const defaultConfig = { provider: 'local-subprocess' };
668
+ await mkdir(dirname(sessionConfigPath), { recursive: true });
669
+ await writeFile(sessionConfigPath, JSON.stringify(defaultConfig, null, 2) + '\n', 'utf8');
670
+ results.push({ path: sessionConfigPath, mode: 'created' });
671
+ }
672
+
673
+ const payload = {
674
+ host,
675
+ repo_root: root,
676
+ installed_prompt_path: installedPromptPath,
677
+ installed_skill_path: installedSkillPath,
678
+ install_guide_path: installGuidePath,
679
+ install_manifest_path: installManifestPath,
680
+ source_prompt_path: resolve(promptAssetPath),
681
+ source_skill_path: resolve(skillAssetPath),
682
+ files: results,
683
+ slash_command_surfaces: {
684
+ vscode_prompt: assetPaths.vscodePromptPath,
685
+ opencode_config: assetPaths.opencodeConfigPath,
686
+ gemini_command: assetPaths.geminiCommandPath,
687
+ antigravity_skill: assetPaths.antigravitySkillPath,
688
+ },
689
+ instruction_surfaces: {
690
+ agents: assetPaths.agentsInstructionsPath,
691
+ copilot_instructions: assetPaths.copilotInstructionsPath,
692
+ },
693
+ host_guidance: hostGuidance,
694
+ unsupported_hosts: [],
695
+ next_steps: [
696
+ 'Open the repository in your preferred host and follow the matching host_guidance entry.',
697
+ `Open ${installGuidePath} for repo-local quick-start steps for Codex, OpenCode, VS Code, and Antigravity.`,
698
+ 'Run `remediate-code verify-install` from the repository root to smoke-test the generated host configs.',
699
+ ],
700
+ };
701
+
702
+ if (!options.quiet) {
703
+ console.log(JSON.stringify(payload, null, 2));
704
+ }
705
+
706
+ return payload;
707
+ }
708
+
709
+ /**
710
+ * Run a single host's `verify()` handler from INSTALL_HOST_DEFINITIONS against
711
+ * already-deployed assets and collect its check results. Single-sourced so the
712
+ * `verify-install` CLI and the `verify:hosts` release gate exercise the exact
713
+ * same per-host handler — adding a host to the table auto-extends both.
714
+ *
715
+ * @param {string} hostKey
716
+ * @param {{ root: string, assetPaths: object, hostEntry?: object }} context
717
+ * @returns {Promise<{ host: string, status: 'ok' | 'error', checks: object[] }>}
718
+ */
719
+ export async function runHostVerifyChecks(hostKey, { root, assetPaths, hostEntry }) {
720
+ const checks = [];
721
+
722
+ if (hostEntry) {
723
+ await collectVerifyCheck(checks, 'host_manifest_entry', async () => ({
724
+ summary: `Host guidance exists for ${hostEntry.label}.`,
725
+ primary_path: hostEntry.primary_path,
726
+ }));
727
+ }
728
+
729
+ const hostDefinition = INSTALL_HOST_DEFINITIONS[hostKey];
730
+ if (hostDefinition?.verify) {
731
+ await hostDefinition.verify({ checks, root, assetPaths, collectVerifyCheck });
732
+ } else {
733
+ checks.push({
734
+ id: 'host_handler',
735
+ status: 'error',
736
+ summary: `No verification handler is implemented for host "${hostKey}".`,
737
+ });
738
+ }
739
+
740
+ return {
741
+ host: hostKey,
742
+ status: checks.some((check) => check.status === 'error') ? 'error' : 'ok',
743
+ checks,
744
+ };
745
+ }
746
+
747
+ /**
748
+ * Deploy every host's assets into an ISOLATED throwaway repo root under a
749
+ * redirected `$HOME`/`USERPROFILE` (so the real user config is never touched),
750
+ * then re-run each host's `verify()` handler from the SAME INSTALL_HOST_DEFINITIONS
751
+ * table that drives the deploy. The set of hosts verified is derived from
752
+ * INSTALL_HOST_ORDER, so adding a host to the table auto-extends verification.
753
+ *
754
+ * Returns a structured report; callers (the `verify:hosts` script, tests) decide
755
+ * how to surface it. Never mutates the caller's environment beyond restoring the
756
+ * redirected HOME/USERPROFILE on the way out.
757
+ *
758
+ * @param {{ keepArtifacts?: boolean }} [options]
759
+ * @returns {Promise<{
760
+ * status: 'ok' | 'error',
761
+ * issue_count: number,
762
+ * verified_hosts: string[],
763
+ * home_dir: string,
764
+ * repo_root: string,
765
+ * hosts: { host: string, status: 'ok' | 'error', checks: object[] }[],
766
+ * }>}
767
+ */
768
+ export async function verifyHostsIsolated(options = {}) {
769
+ // A temp $HOME guarantees a postinstall-style deploy (or any HOME-reading code
770
+ // path) can never write to the operator's real ~/.config — defense in depth on
771
+ // top of the throwaway repo root the bootstrap writes into.
772
+ const homeDir = await mkdtemp(join(tmpdir(), 'remediate-code-verify-hosts-home-'));
773
+ const isolatedRepoRoot = join(homeDir, 'repo');
774
+ const previousHome = process.env.HOME;
775
+ const previousUserProfile = process.env.USERPROFILE;
776
+ process.env.HOME = homeDir;
777
+ process.env.USERPROFILE = homeDir;
778
+
779
+ try {
780
+ await mkdir(isolatedRepoRoot, { recursive: true });
781
+ // Deploy every host surface into the throwaway root from the canonical assets.
782
+ await installBootstrap(['--host', 'all', '--root', isolatedRepoRoot], { quiet: true });
783
+
784
+ // Derive expectations from the SAME table the deploy uses: profile → asset
785
+ // paths → per-host verify(). INSTALL_HOST_ORDER is the source of truth for the
786
+ // verified host set.
787
+ const profile = getInstallProfile('all');
788
+ const assetPaths = buildInstallAssetPaths(isolatedRepoRoot, profile);
789
+ const hostCatalog = new Map(
790
+ buildHostCatalog({ root: isolatedRepoRoot, host: 'all', assets: assetPaths }).map(
791
+ (entry) => [entry.host, entry],
792
+ ),
793
+ );
794
+
795
+ const hosts = [];
796
+ for (const hostKey of INSTALL_HOST_ORDER) {
797
+ hosts.push(
798
+ await runHostVerifyChecks(hostKey, {
799
+ root: isolatedRepoRoot,
800
+ assetPaths,
801
+ hostEntry: hostCatalog.get(hostKey),
802
+ }),
803
+ );
804
+ }
805
+
806
+ const issueCount = hosts.reduce(
807
+ (sum, host) => sum + host.checks.filter((check) => check.status === 'error').length,
808
+ 0,
809
+ );
810
+
811
+ return {
812
+ status: issueCount > 0 ? 'error' : 'ok',
813
+ issue_count: issueCount,
814
+ verified_hosts: [...INSTALL_HOST_ORDER],
815
+ home_dir: homeDir,
816
+ repo_root: isolatedRepoRoot,
817
+ hosts,
818
+ };
819
+ } finally {
820
+ if (previousHome === undefined) delete process.env.HOME;
821
+ else process.env.HOME = previousHome;
822
+ if (previousUserProfile === undefined) delete process.env.USERPROFILE;
823
+ else process.env.USERPROFILE = previousUserProfile;
824
+ if (!options.keepArtifacts) {
825
+ await rm(homeDir, { recursive: true, force: true }).catch(() => {});
826
+ }
827
+ }
828
+ }
829
+
830
+ export async function verifyInstalledBootstrap(argv) {
831
+ const root = resolve(getFlag(argv, '--root') ?? '.');
832
+ const requestedHost = getFlag(argv, '--host')?.toLowerCase() ?? null;
833
+ const installManifestPath = join(
834
+ root,
835
+ INSTALL_DIR,
836
+ 'install',
837
+ INSTALL_MANIFEST_FILENAME,
838
+ );
839
+ const installGuidePath = join(
840
+ root,
841
+ INSTALL_DIR,
842
+ 'install',
843
+ INSTALL_GUIDE_FILENAME,
844
+ );
845
+
846
+ await assertDirectoryExists(root, 'Target repository root');
847
+
848
+ const generalChecks = [];
849
+ const hostResults = [];
850
+ let installManifest;
851
+
852
+ await collectVerifyCheck(generalChecks, 'install_manifest', async () => {
853
+ await ensureFile(installManifestPath, 'Install manifest');
854
+ installManifest = await readJson(installManifestPath, 'Install manifest');
855
+ if (installManifest?.contract_version !== 'remediate-code-install/v1alpha1') {
856
+ throw new Error(
857
+ `Unexpected install manifest contract version: ${installManifest?.contract_version ?? 'missing'}.`,
858
+ );
859
+ }
860
+ return {
861
+ summary: 'Install manifest parsed successfully.',
862
+ path: installManifestPath,
863
+ };
864
+ });
865
+
866
+ if (!installManifest) {
867
+ console.log(
868
+ JSON.stringify(
869
+ {
870
+ root,
871
+ requested_host: requestedHost ?? 'all',
872
+ status: 'error',
873
+ issue_count: generalChecks.filter((check) => check.status === 'error').length,
874
+ checks: generalChecks,
875
+ hosts: [],
876
+ },
877
+ null,
878
+ 2,
879
+ ),
880
+ );
881
+ process.exitCode = 1;
882
+ return;
883
+ }
884
+
885
+ const assetPaths = installManifest.asset_paths ?? {};
886
+ const hostCatalog = new Map(
887
+ (installManifest.hosts ?? []).map((entry) => [entry.host, entry]),
888
+ );
889
+ const selectedHosts = requestedHost && requestedHost !== 'all'
890
+ ? getInstallHostKeys(requestedHost)
891
+ : [...hostCatalog.keys()];
892
+
893
+ await collectVerifyCheck(generalChecks, 'install_guide', async () => {
894
+ const guide = await readFile(installGuidePath, 'utf8');
895
+ if (!guide.includes('# remediate-code bootstrap guide')) {
896
+ throw new Error(`Install guide does not look like a remediate-code bootstrap guide: ${installGuidePath}`);
897
+ }
898
+ return {
899
+ summary: 'Install guide is present and readable.',
900
+ path: installGuidePath,
901
+ };
902
+ });
903
+
904
+ await collectVerifyCheck(generalChecks, 'installed_prompt', async () => {
905
+ await ensureFile(assetPaths.installedPromptPath, 'Installed prompt asset');
906
+ const installedPrompt = await readFile(assetPaths.installedPromptPath, 'utf8');
907
+ const sourcePrompt = await readFile(promptAssetPath, 'utf8');
908
+ if (installedPrompt !== sourcePrompt) {
909
+ throw new Error(
910
+ `Installed prompt is out of sync with the source prompt. Run "remediate-code install" from ${root}.`,
911
+ );
912
+ }
913
+ return {
914
+ summary: 'Installed prompt asset is present and matches the source prompt.',
915
+ path: assetPaths.installedPromptPath,
916
+ };
917
+ });
918
+
919
+ await collectVerifyCheck(generalChecks, 'installed_skill', async () => {
920
+ await ensureFile(assetPaths.installedSkillPath, 'Installed skill asset');
921
+ const installedSkill = (await readFile(assetPaths.installedSkillPath, 'utf8')).replace(/\r\n/g, '\n');
922
+ const sourceSkill = (await readFile(skillAssetPath, 'utf8')).replace(/\r\n/g, '\n');
923
+ if (installedSkill !== sourceSkill) {
924
+ throw new Error(
925
+ `Installed skill is out of sync with the source skill. Run "remediate-code install" from ${root}.`,
926
+ );
927
+ }
928
+ return {
929
+ summary: 'Installed skill asset is present and matches the source skill.',
930
+ path: assetPaths.installedSkillPath,
931
+ };
932
+ });
933
+
934
+ await collectVerifyCheck(generalChecks, 'legacy_local_surfaces', async () => {
935
+ const legacySurfaces = await findLegacyRemediateCodeSurfaceFiles(root);
936
+ if (legacySurfaces.length > 0) {
937
+ throw new Error(
938
+ `Legacy local /remediate-code surfaces are still present: ${legacySurfaces.join(', ')}. Run "remediate-code install" from ${root}.`,
939
+ );
940
+ }
941
+ return {
942
+ summary: 'No legacy local /remediate-code command or skill surfaces were found.',
943
+ };
944
+ });
945
+
946
+ for (const hostKey of selectedHosts) {
947
+ const hostEntry = hostCatalog.get(hostKey);
948
+
949
+ if (!hostEntry) {
950
+ hostResults.push({
951
+ host: hostKey,
952
+ status: 'error',
953
+ checks: [{
954
+ id: 'host_manifest_entry',
955
+ status: 'error',
956
+ summary: `Install manifest does not contain host guidance for "${hostKey}".`,
957
+ }],
958
+ });
959
+ continue;
960
+ }
961
+
962
+ hostResults.push(
963
+ await runHostVerifyChecks(hostKey, { root, assetPaths, hostEntry }),
964
+ );
965
+ }
966
+
967
+ const issueCount =
968
+ generalChecks.filter((check) => check.status === 'error').length +
969
+ hostResults.reduce(
970
+ (sum, host) => sum + host.checks.filter((check) => check.status === 'error').length,
971
+ 0,
972
+ );
973
+
974
+ console.log(
975
+ JSON.stringify(
976
+ {
977
+ root,
978
+ requested_host: requestedHost ?? 'all',
979
+ manifest_path: installManifestPath,
980
+ status: issueCount > 0 ? 'error' : 'ok',
981
+ issue_count: issueCount,
982
+ checks: generalChecks,
983
+ hosts: hostResults,
984
+ },
985
+ null,
986
+ 2,
987
+ ),
988
+ );
989
+
990
+ process.exitCode = issueCount > 0 ? 1 : 0;
991
+ }
992
+
993
+ export async function detectBootstrapRefreshReason(root, host) {
994
+ const installManifestPath = join(
995
+ root,
996
+ INSTALL_DIR,
997
+ 'install',
998
+ INSTALL_MANIFEST_FILENAME,
999
+ );
1000
+
1001
+ if (!(await fileExists(installManifestPath))) {
1002
+ return 'missing_install_manifest';
1003
+ }
1004
+
1005
+ let installManifest;
1006
+ try {
1007
+ installManifest = JSON.parse(await readFile(installManifestPath, 'utf8'));
1008
+ } catch {
1009
+ return 'invalid_install_manifest';
1010
+ }
1011
+
1012
+ if (installManifest?.contract_version !== 'remediate-code-install/v1alpha1') {
1013
+ return 'stale_install_manifest_contract';
1014
+ }
1015
+
1016
+ const assetPaths = installManifest.asset_paths ?? {};
1017
+ const hostCatalog = new Set(
1018
+ (installManifest.hosts ?? []).map((entry) => entry.host),
1019
+ );
1020
+
1021
+ if (hostCatalog.has('codex') && (assetPaths.codexSkillPath || assetPaths.codexPromptPath)) {
1022
+ return 'legacy_local_remediate_code_surface';
1023
+ }
1024
+
1025
+ if ((await findLegacyRemediateCodeSurfaceFiles(root)).length > 0) {
1026
+ return 'legacy_local_remediate_code_surface';
1027
+ }
1028
+
1029
+ for (const hostKey of getInstallHostKeys(host)) {
1030
+ if (!hostCatalog.has(hostKey)) {
1031
+ return `missing_host_surface:${hostKey}`;
1032
+ }
1033
+
1034
+ const definition = INSTALL_HOST_DEFINITIONS[hostKey];
1035
+ const requiredPathKeys = [
1036
+ definition.primary_path_key,
1037
+ ...definition.supporting_path_keys,
1038
+ ];
1039
+ for (const pathKey of requiredPathKeys) {
1040
+ const targetPath = assetPaths[pathKey];
1041
+ if (targetPath && !(await fileExists(targetPath))) {
1042
+ return `missing_host_asset:${hostKey}:${pathKey}`;
1043
+ }
1044
+ }
1045
+ }
1046
+
1047
+ const installedPrompt = await readTextIfExists(assetPaths.installedPromptPath);
1048
+ if (installedPrompt === null) {
1049
+ return 'missing_installed_prompt';
1050
+ }
1051
+ const sourcePrompt = await readFile(promptAssetPath, 'utf8');
1052
+ if (installedPrompt !== sourcePrompt) {
1053
+ return 'stale_installed_prompt';
1054
+ }
1055
+ const { body: sourcePromptBody } = splitFrontmatter(sourcePrompt);
1056
+
1057
+ const installedSkill = await readTextIfExists(assetPaths.installedSkillPath);
1058
+ if (installedSkill === null) {
1059
+ return 'missing_installed_skill';
1060
+ }
1061
+ const sourceSkill = (await readFile(skillAssetPath, 'utf8')).replace(/\r\n/g, '\n');
1062
+ if (installedSkill.replace(/\r\n/g, '\n') !== sourceSkill) {
1063
+ return 'stale_installed_skill';
1064
+ }
1065
+
1066
+ for (const hostKey of getInstallHostKeys(host)) {
1067
+ switch (hostKey) {
1068
+ case 'codex': {
1069
+ break;
1070
+ }
1071
+ case 'opencode': {
1072
+ const opencodeConfig = await readJson(assetPaths.opencodeConfigPath, 'OpenCode config').catch(() => null);
1073
+ if (opencodeConfig?.command?.['remediate-code']) {
1074
+ return 'stale_host_asset:opencode:local_command';
1075
+ }
1076
+ if (opencodeConfig?.mcp?.remediator) {
1077
+ return 'stale_host_asset:opencode:project_mcp';
1078
+ }
1079
+ try {
1080
+ assertOpenCodeRemediatePermissionConfig(opencodeConfig?.permission, 'permission');
1081
+ assertOpenCodeRemediatePermissionConfig(opencodeConfig?.agent?.remediator?.permission, 'agent.remediator.permission');
1082
+ } catch {
1083
+ return 'stale_host_asset:opencode:permissions';
1084
+ }
1085
+ if (await fileExists(join(root, '.opencode', 'commands', 'remediate-code.md'))) {
1086
+ return 'stale_host_asset:opencode:legacy_command_file';
1087
+ }
1088
+ break;
1089
+ }
1090
+ case 'vscode': {
1091
+ const vscodePrompt = await readTextIfExists(assetPaths.vscodePromptPath);
1092
+ if (vscodePrompt === null) {
1093
+ return 'missing_host_asset:vscode:prompt';
1094
+ }
1095
+ if (splitFrontmatter(vscodePrompt).body !== sourcePromptBody.trimStart()) {
1096
+ return 'stale_host_asset:vscode:prompt';
1097
+ }
1098
+ break;
1099
+ }
1100
+ case 'antigravity': {
1101
+ const expectedSkillPath = join(root, '.agent', 'skills', 'remediate-code', 'SKILL.md');
1102
+ if (!(await fileExists(expectedSkillPath))) {
1103
+ return 'missing_host_asset:antigravity:skill';
1104
+ }
1105
+ const antigravitySkill = await readTextIfExists(expectedSkillPath);
1106
+ if (antigravitySkill !== null && antigravitySkill.replace(/\r\n/g, '\n') !== sourceSkill) {
1107
+ return 'stale_host_asset:antigravity:skill';
1108
+ }
1109
+ break;
1110
+ }
1111
+ default:
1112
+ break;
1113
+ }
1114
+ }
1115
+
1116
+ return null;
1117
+ }
1118
+
1119
+ export async function ensureBootstrap(argv) {
1120
+ const host = (getFlag(argv, '--host') ?? DEFAULT_INSTALL_HOST).toLowerCase();
1121
+ const root = resolve(getFlag(argv, '--root') ?? '.');
1122
+ const quiet = hasFlag(argv, '--quiet');
1123
+ const force = hasFlag(argv, '--force');
1124
+ await assertDirectoryExists(root, 'Target repository root');
1125
+
1126
+ const reason = force
1127
+ ? 'forced'
1128
+ : await detectBootstrapRefreshReason(root, host);
1129
+
1130
+ if (reason) {
1131
+ const installed = await installBootstrap(argv, { quiet: true });
1132
+ const payload = {
1133
+ status: 'ok',
1134
+ action: 'installed',
1135
+ reason,
1136
+ host: installed.host,
1137
+ repo_root: installed.repo_root,
1138
+ install_manifest_path: installed.install_manifest_path,
1139
+ host_count: installed.host_guidance.length,
1140
+ file_count: installed.files.length,
1141
+ };
1142
+ if (!quiet) {
1143
+ console.log(JSON.stringify(payload, null, 2));
1144
+ }
1145
+ return payload;
1146
+ }
1147
+
1148
+ const payload = {
1149
+ status: 'ok',
1150
+ action: 'skipped',
1151
+ reason: null,
1152
+ host,
1153
+ repo_root: root,
1154
+ install_manifest_path: join(
1155
+ root,
1156
+ INSTALL_DIR,
1157
+ 'install',
1158
+ INSTALL_MANIFEST_FILENAME,
1159
+ ),
1160
+ };
1161
+ if (!quiet) {
1162
+ console.log(JSON.stringify(payload, null, 2));
1163
+ }
1164
+ return payload;
1165
+ }
1166
+
1167
+ export async function installHostPrompt(argv) {
1168
+ const host = requireFlagValue(argv, '--host').toLowerCase();
1169
+
1170
+ if (host !== 'copilot') {
1171
+ throw new Error(
1172
+ `install-host currently supports only "copilot". Use "install --host ${host}" for the broader bootstrap flow.`,
1173
+ );
1174
+ }
1175
+
1176
+ await installBootstrap(argv);
1177
+ }
1178
+
1179
+ // Underscore-aliased re-exports consumed by host-bootstrap-descriptors tests
1180
+ // via remediate-code-wrapper-lib.mjs. Keep these so the test import chain resolves
1181
+ // without modification to the test file.
1182
+ export {
1183
+ INSTALL_HOST_ORDER as _INSTALL_HOST_ORDER,
1184
+ INSTALL_HOST_DEFINITIONS as _INSTALL_HOST_DEFINITIONS,
1185
+ getInstallHostKeys as _getInstallHostKeys,
1186
+ getInstallProfile as _getInstallProfile,
1187
+ };