triad-plus 1.9.0 → 1.11.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/CHANGELOG.md +25 -0
- package/README.md +39 -0
- package/adapters/antigravity/README.md +1 -1
- package/adapters/antigravity/install.sh +1 -1
- package/adapters/claude-code/README.md +1 -1
- package/adapters/claude-code/install.sh +1 -1
- package/adapters/codex/README.md +1 -1
- package/adapters/codex/install.sh +3 -3
- package/adapters/hermes/install.sh +1 -1
- package/adapters/opencode/.opencode/agents/triad-developer.md +14 -0
- package/adapters/opencode/.opencode/agents/triad-orchestrator.md +1 -0
- package/adapters/opencode/.opencode/agents/triad-reviewer.md +15 -0
- package/adapters/opencode/README.md +15 -6
- package/adapters/opencode/install.sh +1 -1
- package/adapters/registry.mjs +22 -2
- package/bin/triad-plus.js +767 -56
- package/docs/configuration.md +57 -6
- package/docs/npx-installation.md +77 -4
- package/docs/opencode-replication.md +8 -0
- package/docs/runtimes.md +1 -1
- package/docs/troubleshooting.md +41 -0
- package/package.json +2 -2
- package/runtime/lib/assignment-packet.mjs +38 -1
- package/runtime/lib/installation-manifest.mjs +269 -0
- package/runtime/lib/model-config.mjs +165 -0
- package/runtime/lib/repository-context.mjs +192 -0
- package/runtime/lib/terminal.mjs +86 -0
- package/runtime/triad-runtime-context.mjs +88 -0
- package/runtime/triad-verify.mjs +26 -18
- package/skills/triad-loop-developer/SKILL.md +14 -0
- package/skills/triad-loop-orchestrator/SKILL.md +13 -0
- package/skills/triad-loop-reviewer/SKILL.md +16 -0
- package/skills/triad-model-configuration/SKILL.md +98 -0
- package/skills/triad-model-configuration/agents/openai.yaml +4 -0
package/bin/triad-plus.js
CHANGED
|
@@ -1,16 +1,41 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import { access, cp, mkdir, readFile, rm, stat, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { access, cp, mkdir, readFile, rm, stat, writeFile, lstat, readdir } from 'node:fs/promises';
|
|
4
4
|
import { spawnSync } from 'node:child_process';
|
|
5
|
+
import { readFileSync } from 'node:fs';
|
|
5
6
|
import { homedir } from 'node:os';
|
|
6
7
|
import { fileURLToPath } from 'node:url';
|
|
7
|
-
import { dirname, join, resolve } from 'node:path';
|
|
8
|
+
import { dirname, join, resolve, relative, sep } from 'node:path';
|
|
8
9
|
import process from 'node:process';
|
|
9
10
|
import { createInterface } from 'node:readline/promises';
|
|
10
11
|
import { getAdapter, listAdapters, roleDefinitions, sharedSkillNames } from '../adapters/registry.mjs';
|
|
11
12
|
import { writeImportedCard } from '../integrations/bmad/story-importer.mjs';
|
|
13
|
+
import {
|
|
14
|
+
hostModelField,
|
|
15
|
+
materializedHostRoleConfiguration,
|
|
16
|
+
supportsNativeReasoning,
|
|
17
|
+
validateTeamConfiguration
|
|
18
|
+
} from '../runtime/lib/model-config.mjs';
|
|
19
|
+
import {
|
|
20
|
+
formatDoctorLine,
|
|
21
|
+
formatDoctorSection,
|
|
22
|
+
formatSetupSummary
|
|
23
|
+
} from '../runtime/lib/terminal.mjs';
|
|
24
|
+
import {
|
|
25
|
+
buildInstallationManifest,
|
|
26
|
+
collectManagedAssetRecords,
|
|
27
|
+
installationManifestFingerprint,
|
|
28
|
+
installationManifestPath,
|
|
29
|
+
loadInstallationManifest,
|
|
30
|
+
manifestIsUninstalled,
|
|
31
|
+
manifestScopeStatus,
|
|
32
|
+
sha256Text,
|
|
33
|
+
sha256File,
|
|
34
|
+
writeInstallationManifest
|
|
35
|
+
} from '../runtime/lib/installation-manifest.mjs';
|
|
12
36
|
|
|
13
37
|
const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
|
38
|
+
const packageVersion = JSON.parse(readFileSync(join(packageRoot, 'package.json'), 'utf8')).version;
|
|
14
39
|
|
|
15
40
|
function usage(exitCode = 0) {
|
|
16
41
|
const stream = exitCode === 0 ? process.stdout : process.stderr;
|
|
@@ -18,15 +43,18 @@ function usage(exitCode = 0) {
|
|
|
18
43
|
|
|
19
44
|
Usage:
|
|
20
45
|
npx triad-plus
|
|
46
|
+
npx triad-plus --version
|
|
21
47
|
npx triad-plus init --host <adapter-id> --control <path> [--global] [--team-config <path>] [--allow-product-repo]
|
|
22
|
-
npx triad-plus doctor --host <adapter-id> --control <path> [--hook-config <path>]
|
|
48
|
+
npx triad-plus doctor --host <adapter-id> --control <path> [--global] [--hook-config <path>]
|
|
49
|
+
npx triad-plus version --control <path>
|
|
23
50
|
npx triad-plus upgrade --host <adapter-id> --control <path> [--global] [--apply]
|
|
51
|
+
npx triad-plus uninstall --host <adapter-id> --control <path> [--global] [--apply]
|
|
24
52
|
npx triad-plus import-bmad-story --source <story.md> --output <card.md> [--target-repository <id>] [--provenance <record.json>] [--required-gate <id>] [--depends-on <card-id>]
|
|
25
53
|
|
|
26
54
|
Adapters: ${listAdapters().map((adapter) => adapter.id).join(', ')}
|
|
27
55
|
|
|
28
56
|
The control path is a project-control workspace, not a product repository.
|
|
29
|
-
Installation refuses
|
|
57
|
+
Installation refuses conflicting asset overwrites; identical global assets may be reused. Upgrade is a dry run unless --apply is supplied.
|
|
30
58
|
`);
|
|
31
59
|
process.exit(exitCode);
|
|
32
60
|
}
|
|
@@ -66,6 +94,16 @@ async function requireDirectory(target) {
|
|
|
66
94
|
if (!(await stat(target)).isDirectory()) throw new Error(`Control path is not a directory: ${target}`);
|
|
67
95
|
}
|
|
68
96
|
|
|
97
|
+
async function requireExistingDirectory(target) {
|
|
98
|
+
let info;
|
|
99
|
+
try { info = await stat(target); }
|
|
100
|
+
catch (error) {
|
|
101
|
+
if (error.code === 'ENOENT') throw new Error(`Control workspace does not exist: ${target}`);
|
|
102
|
+
throw error;
|
|
103
|
+
}
|
|
104
|
+
if (!info.isDirectory()) throw new Error(`Control path is not a directory: ${target}`);
|
|
105
|
+
}
|
|
106
|
+
|
|
69
107
|
function codexHome() {
|
|
70
108
|
return process.env.CODEX_HOME || join(homedir(), '.codex');
|
|
71
109
|
}
|
|
@@ -111,6 +149,101 @@ async function installAssets(assets, root, installContext) {
|
|
|
111
149
|
for (const asset of assets) await copyAsset(asset, root, installContext);
|
|
112
150
|
}
|
|
113
151
|
|
|
152
|
+
function assetDestinations(assets, root, installContext) {
|
|
153
|
+
return assets.map((asset) => resolveDestination(asset.destination, root, installContext));
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
async function sourceFiles(source) {
|
|
157
|
+
const info = await lstat(source);
|
|
158
|
+
if (info.isFile()) return [source];
|
|
159
|
+
if (!info.isDirectory()) throw new Error(`Managed asset source is not a regular file or directory: ${source}`);
|
|
160
|
+
const files = [];
|
|
161
|
+
for (const name of (await readdir(source)).sort()) files.push(...await sourceFiles(join(source, name)));
|
|
162
|
+
return files;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** Expand registry assets to exact source/destination file pairs, never to a host directory. */
|
|
166
|
+
async function materializedAssetPairs(assets, root, installContext) {
|
|
167
|
+
const pairs = [];
|
|
168
|
+
for (const asset of assets) {
|
|
169
|
+
const destination = resolveDestination(asset.destination, root, installContext);
|
|
170
|
+
if (asset.source === 'shared-skills') {
|
|
171
|
+
for (const name of sharedSkillNames) {
|
|
172
|
+
const source = join(packageRoot, 'skills', name);
|
|
173
|
+
const sourceRoot = resolve(source);
|
|
174
|
+
for (const file of await sourceFiles(sourceRoot)) {
|
|
175
|
+
pairs.push({ source: file, target: join(destination, name, relative(sourceRoot, file)) });
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
const source = sourcePath(asset.source);
|
|
181
|
+
const sourceInfo = await lstat(source);
|
|
182
|
+
if (asset.file || sourceInfo.isFile()) {
|
|
183
|
+
pairs.push({ source, target: destination });
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
const sourceRoot = resolve(source);
|
|
187
|
+
for (const file of await sourceFiles(sourceRoot)) pairs.push({ source: file, target: join(destination, relative(sourceRoot, file)) });
|
|
188
|
+
}
|
|
189
|
+
return pairs.map((pair) => ({ source: resolve(pair.source), target: resolve(pair.target) }));
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** Expand registry assets to the exact files copied, never to a host directory. */
|
|
193
|
+
async function materializedAssetPaths(assets, root, installContext) {
|
|
194
|
+
return (await materializedAssetPairs(assets, root, installContext)).map((pair) => pair.target);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function generatedRoleAssetPaths(adapter, installContext, scope, team) {
|
|
198
|
+
if (!team || scope !== 'global' || adapter.modelBinding !== 'global-profiles' || typeof adapter.roleModelPaths !== 'function') return [];
|
|
199
|
+
return adapter.roleModelPaths(installContext);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
async function collectInstallationAssets(adapter, controlRoot, installContext, scope, team) {
|
|
203
|
+
const assets = scope === 'global' ? adapter.globalAssets : adapter.projectAssets;
|
|
204
|
+
const paths = [
|
|
205
|
+
...await materializedAssetPaths(assets, controlRoot, installContext),
|
|
206
|
+
...generatedRoleAssetPaths(adapter, installContext, scope, team)
|
|
207
|
+
];
|
|
208
|
+
return collectManagedAssetRecords(paths, {
|
|
209
|
+
scope,
|
|
210
|
+
baseRoot: scope === 'project' ? controlRoot : null
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function overallInstallationStatus(scopeStatus) {
|
|
215
|
+
const values = Object.values(scopeStatus);
|
|
216
|
+
if (values.every((value) => ['not_configured', 'uninstalled'].includes(value))) return 'uninstalled';
|
|
217
|
+
if (values.some((value) => ['partial', 'uninstalled'].includes(value))) return 'partial';
|
|
218
|
+
return 'installed';
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
async function installationManifestFor({ adapter, controlRoot, installContext, team, global = false, previous = null, installedAt = null }) {
|
|
222
|
+
const projectAssets = await collectInstallationAssets(adapter, controlRoot, installContext, 'project', team);
|
|
223
|
+
const previousManifest = previous?.manifest ?? previous;
|
|
224
|
+
const overlayAssets = await managedOverlayRecord(controlRoot, previousManifest, team);
|
|
225
|
+
const globalConfigured = Boolean(global || previousManifest?.scopes?.global);
|
|
226
|
+
const globalAssets = global
|
|
227
|
+
? await collectInstallationAssets(adapter, controlRoot, installContext, 'global', team)
|
|
228
|
+
: (previousManifest?.managed_assets ?? []).filter((asset) => asset.scope === 'global');
|
|
229
|
+
const scopeStatus = {
|
|
230
|
+
project: 'installed',
|
|
231
|
+
global: globalConfigured
|
|
232
|
+
? (global ? 'installed' : manifestScopeStatus(previousManifest, 'global'))
|
|
233
|
+
: 'not_configured'
|
|
234
|
+
};
|
|
235
|
+
return buildInstallationManifest({
|
|
236
|
+
triadVersion: packageVersion,
|
|
237
|
+
adapter: adapter.id,
|
|
238
|
+
installedAt: installedAt ?? previousManifest?.installed_at ?? new Date().toISOString(),
|
|
239
|
+
updatedAt: new Date().toISOString(),
|
|
240
|
+
scopes: { project: true, global: globalConfigured },
|
|
241
|
+
scopeStatus,
|
|
242
|
+
managedAssets: [...projectAssets, ...overlayAssets, ...globalAssets],
|
|
243
|
+
status: overallInstallationStatus(scopeStatus)
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
|
|
114
247
|
async function replaceManagedPath(source, destination, backup, apply) {
|
|
115
248
|
const present = await exists(destination);
|
|
116
249
|
process.stdout.write(` ${apply ? 'Update' : 'Would update'} ${destination}${present ? ' (backup)' : ''}\n`);
|
|
@@ -161,16 +294,26 @@ outside Triad+.
|
|
|
161
294
|
${overlayEnd}`;
|
|
162
295
|
}
|
|
163
296
|
|
|
297
|
+
function managedBlockIn(source, startMarker = overlayStart, endMarker = overlayEnd) {
|
|
298
|
+
const startCount = source.split(startMarker).length - 1;
|
|
299
|
+
const endCount = source.split(endMarker).length - 1;
|
|
300
|
+
if (startCount === 0 && endCount === 0) return null;
|
|
301
|
+
if (startCount !== 1 || endCount !== 1) throw new Error('Managed instruction block is ambiguous: duplicate or incomplete markers.');
|
|
302
|
+
const start = source.indexOf(startMarker);
|
|
303
|
+
const end = source.indexOf(endMarker, start + startMarker.length);
|
|
304
|
+
if (end < 0) throw new Error('Managed instruction block has invalid marker order.');
|
|
305
|
+
const blockEnd = end + endMarker.length;
|
|
306
|
+
return { start, end, blockEnd, content: source.slice(start, blockEnd) };
|
|
307
|
+
}
|
|
308
|
+
|
|
164
309
|
async function overlayPlan(controlRoot, team) {
|
|
165
310
|
const target = join(controlRoot, 'AGENTS.md');
|
|
166
311
|
const overlay = instructionOverlay(team);
|
|
167
312
|
if (!(await exists(target))) return { target, action: 'create', content: `# Project instructions\n\n${overlay}\n` };
|
|
168
313
|
const source = await readFile(target, 'utf8');
|
|
169
|
-
const
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
if (start < 0 || end < start) throw new Error(`Cannot safely update managed instruction block: ${target}`);
|
|
173
|
-
return { target, action: 'update', content: `${source.slice(0, start)}${overlay}${source.slice(end + overlayEnd.length)}` };
|
|
314
|
+
const managed = managedBlockIn(source);
|
|
315
|
+
if (!managed) return { target, action: 'append', content: `${source}${source.endsWith('\n') ? '\n' : '\n\n'}${overlay}\n` };
|
|
316
|
+
return { target, action: 'update', content: `${source.slice(0, managed.start)}${overlay}${source.slice(managed.blockEnd)}` };
|
|
174
317
|
}
|
|
175
318
|
|
|
176
319
|
async function applyOverlay(controlRoot, apply, team) {
|
|
@@ -179,20 +322,31 @@ async function applyOverlay(controlRoot, apply, team) {
|
|
|
179
322
|
if (apply) await writeFile(plan.target, plan.content, 'utf8');
|
|
180
323
|
}
|
|
181
324
|
|
|
325
|
+
async function managedOverlayRecord(controlRoot, previousManifest, team) {
|
|
326
|
+
const previousBlocks = (previousManifest?.managed_assets ?? []).filter((asset) => asset.kind === 'managed_block' && asset.scope === 'project');
|
|
327
|
+
if (!team) return previousBlocks;
|
|
328
|
+
const target = join(controlRoot, 'AGENTS.md');
|
|
329
|
+
const source = await readFile(target, 'utf8');
|
|
330
|
+
const managed = managedBlockIn(source);
|
|
331
|
+
if (!managed) throw new Error(`Managed instruction block is missing after materialization: ${target}`);
|
|
332
|
+
return [{
|
|
333
|
+
scope: 'project',
|
|
334
|
+
path: 'AGENTS.md',
|
|
335
|
+
kind: 'managed_block',
|
|
336
|
+
start_marker: overlayStart,
|
|
337
|
+
end_marker: overlayEnd,
|
|
338
|
+
sha256: sha256Text(managed.content)
|
|
339
|
+
}];
|
|
340
|
+
}
|
|
341
|
+
|
|
182
342
|
async function loadTeamConfig(options) {
|
|
183
|
-
if (options.team) return options.team;
|
|
343
|
+
if (options.team) return validateTeamConfiguration(options.team);
|
|
184
344
|
if (!options.teamConfig) return null;
|
|
185
345
|
let team;
|
|
186
346
|
try { team = JSON.parse(await readFile(resolve(options.teamConfig), 'utf8')); }
|
|
187
347
|
catch (error) { throw new Error(`Cannot read --team-config: ${error.message}`); }
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
const value = team.roles[role.id];
|
|
191
|
-
if (!value || typeof value.displayName !== 'string' || ![null, undefined].includes(value.model) && typeof value.model !== 'string') {
|
|
192
|
-
throw new Error(`--team-config has an invalid ${role.id} role definition.`);
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
return team;
|
|
348
|
+
try { return validateTeamConfiguration(team); }
|
|
349
|
+
catch (error) { throw new Error(`--team-config is invalid: ${error.message}`); }
|
|
196
350
|
}
|
|
197
351
|
|
|
198
352
|
async function writeTeamConfig(controlRoot, team) {
|
|
@@ -201,21 +355,22 @@ async function writeTeamConfig(controlRoot, team) {
|
|
|
201
355
|
await writeFile(target, `${JSON.stringify(team, null, 2)}\n`, 'utf8');
|
|
202
356
|
}
|
|
203
357
|
|
|
204
|
-
async function applyMarkdownModel(target, configuration, fields = ['model']) {
|
|
205
|
-
|
|
358
|
+
async function applyMarkdownModel(target, configuration, fields = ['model'], cleanupFields = []) {
|
|
359
|
+
const managedFields = [...new Set([...fields, ...cleanupFields])];
|
|
360
|
+
if (managedFields.length === 0) return;
|
|
206
361
|
const source = await readFile(target, 'utf8');
|
|
207
362
|
if (!source.startsWith('---\n')) throw new Error(`Agent definition has no YAML frontmatter: ${target}`);
|
|
208
363
|
const closing = source.indexOf('\n---\n', 4);
|
|
209
364
|
if (closing === -1) throw new Error(`Agent definition has invalid YAML frontmatter: ${target}`);
|
|
210
365
|
let frontmatter = source.slice(4, closing);
|
|
211
|
-
for (const field of
|
|
366
|
+
for (const field of managedFields) frontmatter = frontmatter.replace(new RegExp(`^${field}:\\s*.*\\n?`, 'm'), '');
|
|
212
367
|
const values = fields
|
|
213
|
-
.map((field) => ({ field, value:
|
|
368
|
+
.map((field) => ({ field, value: configuration?.[field] }))
|
|
214
369
|
.filter(({ value }) => value !== null && value !== undefined && value !== '');
|
|
215
|
-
if (values.length === 0) return;
|
|
216
370
|
const additions = values.map(({ field, value }) => `${field}: ${JSON.stringify(value)}`).join('\n');
|
|
217
371
|
const normalizedFrontmatter = frontmatter.replace(/\s*$/, '');
|
|
218
|
-
|
|
372
|
+
const next = `---\n${normalizedFrontmatter}${additions ? `\n${additions}` : ''}${source.slice(closing)}`;
|
|
373
|
+
if (next !== source) await writeFile(target, next, 'utf8');
|
|
219
374
|
}
|
|
220
375
|
|
|
221
376
|
async function writeRoleProfiles(paths, team) {
|
|
@@ -241,18 +396,29 @@ async function writeRoleProfiles(paths, team) {
|
|
|
241
396
|
}
|
|
242
397
|
}
|
|
243
398
|
|
|
244
|
-
async function applyTeamBinding(adapter, controlRoot, team, installContext) {
|
|
399
|
+
async function applyTeamBinding(adapter, controlRoot, team, installContext, scope = 'project') {
|
|
245
400
|
if (!team || adapter.modelBinding === 'team-record') return;
|
|
246
401
|
if (adapter.modelBinding === 'project-frontmatter') {
|
|
247
|
-
const
|
|
402
|
+
const pathFactory = scope === 'global' ? adapter.globalRoleModelPaths : adapter.roleModelPaths;
|
|
403
|
+
if (typeof pathFactory !== 'function') return;
|
|
404
|
+
const paths = scope === 'global'
|
|
405
|
+
? pathFactory(installContext)
|
|
406
|
+
: pathFactory(controlRoot, installContext);
|
|
248
407
|
const roles = (adapter.modelRoles ?? roleDefinitions.map((role) => role.id))
|
|
249
408
|
.map((roleId) => roleDefinitions.find((role) => role.id === roleId));
|
|
250
409
|
for (const [index, role] of roles.entries()) {
|
|
251
|
-
|
|
410
|
+
const fields = adapter.modelFields ?? ['model'];
|
|
411
|
+
await applyMarkdownModel(
|
|
412
|
+
paths[index],
|
|
413
|
+
materializedHostRoleConfiguration(adapter, role.id, team.roles[role.id]),
|
|
414
|
+
fields,
|
|
415
|
+
adapter.modelCleanupFields ?? []
|
|
416
|
+
);
|
|
252
417
|
}
|
|
253
418
|
return;
|
|
254
419
|
}
|
|
255
420
|
if (adapter.modelBinding === 'global-profiles') {
|
|
421
|
+
if (scope !== 'global') return;
|
|
256
422
|
await writeRoleProfiles(adapter.roleModelPaths(installContext), team);
|
|
257
423
|
return;
|
|
258
424
|
}
|
|
@@ -277,6 +443,232 @@ async function allExist(paths) {
|
|
|
277
443
|
return true;
|
|
278
444
|
}
|
|
279
445
|
|
|
446
|
+
function sharedSkillRoots(adapter, root, installContext, scope) {
|
|
447
|
+
const assets = scope === 'global' ? adapter.globalAssets : adapter.projectAssets;
|
|
448
|
+
return assets
|
|
449
|
+
.filter((asset) => asset.source === 'shared-skills')
|
|
450
|
+
.map((asset) => resolveDestination(asset.destination, root, installContext));
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
async function anyExist(paths) {
|
|
454
|
+
for (const target of paths) if (await exists(target)) return true;
|
|
455
|
+
return false;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
async function globalPathReusable(target, pairs) {
|
|
459
|
+
let info;
|
|
460
|
+
try { info = await lstat(target); } catch (error) {
|
|
461
|
+
if (error.code === 'ENOENT') return true;
|
|
462
|
+
throw error;
|
|
463
|
+
}
|
|
464
|
+
if (info.isDirectory()) {
|
|
465
|
+
const descendants = pairs.filter((pair) => pathWithin(target, pair.target));
|
|
466
|
+
if (descendants.length === 0) return false;
|
|
467
|
+
for (const pair of descendants) {
|
|
468
|
+
let pairInfo;
|
|
469
|
+
try { pairInfo = await lstat(pair.target); } catch (error) {
|
|
470
|
+
if (error.code === 'ENOENT') continue;
|
|
471
|
+
return false;
|
|
472
|
+
}
|
|
473
|
+
if (!pairInfo.isFile() || await sha256File(pair.target) !== await sha256File(pair.source)) return false;
|
|
474
|
+
}
|
|
475
|
+
return true;
|
|
476
|
+
}
|
|
477
|
+
const pair = pairs.find((candidate) => candidate.target === resolve(target));
|
|
478
|
+
return Boolean(pair && info.isFile() && await sha256File(target) === await sha256File(pair.source));
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
function versionTuple(value) {
|
|
482
|
+
const match = String(value ?? '').match(/^(?:v)?(\d+)\.(\d+)\.(\d+)(?:[-+].*)?$/);
|
|
483
|
+
return match ? match.slice(1).map(Number) : null;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
function compareVersions(left, right) {
|
|
487
|
+
const a = versionTuple(left);
|
|
488
|
+
const b = versionTuple(right);
|
|
489
|
+
if (!a || !b) return null;
|
|
490
|
+
for (let index = 0; index < a.length; index += 1) {
|
|
491
|
+
if (a[index] > b[index]) return 1;
|
|
492
|
+
if (a[index] < b[index]) return -1;
|
|
493
|
+
}
|
|
494
|
+
return 0;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
function pathWithin(root, target) {
|
|
498
|
+
const base = resolve(root);
|
|
499
|
+
const candidate = resolve(target);
|
|
500
|
+
return candidate === base || candidate.startsWith(`${base}${sep}`);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
async function managedAssetPaths(adapter, controlRoot, installContext, scope) {
|
|
504
|
+
const assets = scope === 'global' ? adapter.globalAssets : adapter.projectAssets;
|
|
505
|
+
const paths = await materializedAssetPaths(assets, controlRoot, installContext);
|
|
506
|
+
if (scope === 'global' && adapter.modelBinding === 'global-profiles' && typeof adapter.roleModelPaths === 'function') {
|
|
507
|
+
paths.push(...adapter.roleModelPaths(installContext));
|
|
508
|
+
}
|
|
509
|
+
return new Set(paths.map((target) => resolve(target)));
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
function manifestAssetPath(controlRoot, asset) {
|
|
513
|
+
return asset.scope === 'project' ? resolve(controlRoot, asset.path) : resolve(asset.path);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
function displayManagedAssetPath(asset) {
|
|
517
|
+
if (asset.scope !== 'global') return asset.path;
|
|
518
|
+
const home = resolve(homedir());
|
|
519
|
+
const target = resolve(asset.path);
|
|
520
|
+
return pathWithin(home, target) ? `~${relative(home, target) ? `/${relative(home, target)}` : ''}` : target;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
function assetAllowedByRegistry(managedPaths, controlRoot, asset) {
|
|
524
|
+
const target = manifestAssetPath(controlRoot, asset);
|
|
525
|
+
if (asset.kind === 'managed_block') return asset.scope === 'project' && target === resolve(controlRoot, 'AGENTS.md');
|
|
526
|
+
return managedPaths.has(target);
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
async function installationAssetIssues(controlRoot, adapter, installContext, manifest) {
|
|
530
|
+
const issues = [];
|
|
531
|
+
for (const asset of manifest.managed_assets) {
|
|
532
|
+
const target = manifestAssetPath(controlRoot, asset);
|
|
533
|
+
const expectedState = manifestScopeStatus(manifest, asset.scope);
|
|
534
|
+
let info;
|
|
535
|
+
try { info = await lstat(target); } catch (error) {
|
|
536
|
+
if (error.code === 'ENOENT') {
|
|
537
|
+
if (expectedState !== 'uninstalled') issues.push({ asset, status: 'missing' });
|
|
538
|
+
continue;
|
|
539
|
+
}
|
|
540
|
+
issues.push({ asset, status: `unreadable:${error.code ?? 'error'}` });
|
|
541
|
+
continue;
|
|
542
|
+
}
|
|
543
|
+
if (asset.kind === 'managed_block') {
|
|
544
|
+
if (!info.isFile()) {
|
|
545
|
+
issues.push({ asset, status: 'modified' });
|
|
546
|
+
continue;
|
|
547
|
+
}
|
|
548
|
+
let source;
|
|
549
|
+
try { source = await readFile(target, 'utf8'); } catch { issues.push({ asset, status: 'unreadable' }); continue; }
|
|
550
|
+
let managed;
|
|
551
|
+
try { managed = managedBlockIn(source, asset.start_marker, asset.end_marker); } catch { managed = null; }
|
|
552
|
+
if (managed && sha256Text(managed.content) !== asset.sha256) issues.push({ asset, status: 'modified' });
|
|
553
|
+
else if (managed && expectedState === 'uninstalled') issues.push({ asset, status: 'present_after_uninstall' });
|
|
554
|
+
else if (!managed && expectedState !== 'uninstalled') issues.push({ asset, status: 'modified' });
|
|
555
|
+
continue;
|
|
556
|
+
}
|
|
557
|
+
if (!info.isFile()) {
|
|
558
|
+
issues.push({ asset, status: 'modified' });
|
|
559
|
+
continue;
|
|
560
|
+
}
|
|
561
|
+
let observed;
|
|
562
|
+
try { observed = await sha256File(target); } catch { issues.push({ asset, status: 'unreadable' }); continue; }
|
|
563
|
+
if (observed !== asset.sha256) issues.push({ asset, status: 'modified' });
|
|
564
|
+
else if (expectedState === 'uninstalled') issues.push({ asset, status: 'present_after_uninstall' });
|
|
565
|
+
}
|
|
566
|
+
return issues;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
async function inspectInstallation(controlRoot, adapter, installContext) {
|
|
570
|
+
let loaded;
|
|
571
|
+
try { loaded = await loadInstallationManifest(controlRoot); }
|
|
572
|
+
catch (error) {
|
|
573
|
+
return { state: 'invalid', message: error.message, manifest: null, issues: [] };
|
|
574
|
+
}
|
|
575
|
+
if (!loaded) return { state: 'legacy', manifest: null, issues: [] };
|
|
576
|
+
const manifest = loaded.manifest;
|
|
577
|
+
if (manifest.adapter !== adapter.id) {
|
|
578
|
+
return {
|
|
579
|
+
state: 'invalid',
|
|
580
|
+
message: `manifest adapter ${manifest.adapter} does not match selected ${adapter.id}`,
|
|
581
|
+
manifest,
|
|
582
|
+
issues: []
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
const issues = await installationAssetIssues(controlRoot, adapter, installContext, manifest);
|
|
586
|
+
const versionRelation = compareVersions(packageVersion, manifest.triad_version);
|
|
587
|
+
return { state: manifestIsUninstalled(manifest) ? 'uninstalled' : 'valid', manifest, issues, versionRelation };
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
/** Return one status per shared skill so a partial install cannot look healthy. */
|
|
591
|
+
async function sharedSkillStatuses(adapter, root, installContext, scope) {
|
|
592
|
+
const roots = sharedSkillRoots(adapter, root, installContext, scope);
|
|
593
|
+
if (roots.length === 0) return [];
|
|
594
|
+
const configuredRoots = [];
|
|
595
|
+
for (const skillRoot of roots) if (await exists(skillRoot)) configuredRoots.push(skillRoot);
|
|
596
|
+
const statuses = [];
|
|
597
|
+
for (const name of sharedSkillNames) {
|
|
598
|
+
const paths = roots.map((skillRoot) => join(skillRoot, name));
|
|
599
|
+
if (configuredRoots.length === 0) {
|
|
600
|
+
statuses.push({ name, status: 'not configured', paths });
|
|
601
|
+
continue;
|
|
602
|
+
}
|
|
603
|
+
let present = 0;
|
|
604
|
+
for (const target of paths) if (await exists(target)) present += 1;
|
|
605
|
+
statuses.push({ name, status: present === paths.length ? 'OK' : 'MISSING', paths });
|
|
606
|
+
}
|
|
607
|
+
return statuses;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
function roleModelEntries(adapter, controlRoot, installContext, scope) {
|
|
611
|
+
let paths;
|
|
612
|
+
if (scope === 'global') {
|
|
613
|
+
if (adapter.modelBinding === 'global-profiles') paths = adapter.roleModelPaths?.(installContext) ?? [];
|
|
614
|
+
else paths = adapter.globalRoleModelPaths?.(installContext) ?? [];
|
|
615
|
+
} else {
|
|
616
|
+
if (adapter.modelBinding !== 'project-frontmatter') return [];
|
|
617
|
+
paths = adapter.roleModelPaths?.(controlRoot, installContext) ?? [];
|
|
618
|
+
}
|
|
619
|
+
const roleIds = adapter.modelRoles ?? roleDefinitions.map((role) => role.id);
|
|
620
|
+
return roleIds.map((roleId, index) => ({
|
|
621
|
+
role: roleDefinitions.find((definition) => definition.id === roleId),
|
|
622
|
+
path: paths[index]
|
|
623
|
+
})).filter((entry) => entry.role && entry.path);
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
function parseFrontmatterFields(source, fields) {
|
|
627
|
+
const result = {};
|
|
628
|
+
if (!source.startsWith('---\n')) return result;
|
|
629
|
+
const closing = source.indexOf('\n---\n', 4);
|
|
630
|
+
if (closing < 0) return result;
|
|
631
|
+
const frontmatter = source.slice(4, closing);
|
|
632
|
+
for (const field of fields) {
|
|
633
|
+
const match = frontmatter.match(new RegExp(`^${field}:\\s*(.*?)\\s*$`, 'm'));
|
|
634
|
+
if (!match) {
|
|
635
|
+
result[field] = null;
|
|
636
|
+
continue;
|
|
637
|
+
}
|
|
638
|
+
try { result[field] = JSON.parse(match[1]); }
|
|
639
|
+
catch { result[field] = match[1].replace(/^['"]|['"]$/g, ''); }
|
|
640
|
+
}
|
|
641
|
+
return result;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
async function modelBindingEvidence(adapter, controlRoot, installContext, team, scope) {
|
|
645
|
+
const entries = roleModelEntries(adapter, controlRoot, installContext, scope);
|
|
646
|
+
if (!entries.length) return [];
|
|
647
|
+
const evidence = [];
|
|
648
|
+
for (const { role, path: target } of entries) {
|
|
649
|
+
const configuration = team?.roles?.[role.id];
|
|
650
|
+
if (!configuration) {
|
|
651
|
+
evidence.push({ role, status: 'not configured', target });
|
|
652
|
+
continue;
|
|
653
|
+
}
|
|
654
|
+
const expected = materializedHostRoleConfiguration(adapter, role.id, configuration);
|
|
655
|
+
const fields = [...new Set([...(adapter.modelFields ?? Object.keys(expected)), ...(adapter.modelCleanupFields ?? [])])];
|
|
656
|
+
if (Object.keys(expected).length === 0) {
|
|
657
|
+
evidence.push({ role, status: 'UNSUPPORTED', target, expected, observed: null });
|
|
658
|
+
continue;
|
|
659
|
+
}
|
|
660
|
+
if (!(await exists(target))) {
|
|
661
|
+
evidence.push({ role, status: 'UNKNOWN / NOT OBSERVABLE', target, expected, observed: null });
|
|
662
|
+
continue;
|
|
663
|
+
}
|
|
664
|
+
const observed = parseFrontmatterFields(await readFile(target, 'utf8'), fields);
|
|
665
|
+
const matches = fields.every((field) => (expected[field] ?? null) === (observed[field] ?? null));
|
|
666
|
+
const allDefault = Object.values(expected).every((value) => value === null || value === undefined || value === '');
|
|
667
|
+
evidence.push({ role, status: matches && allDefault ? 'HOST DEFAULT' : matches ? 'VERIFIED' : 'MATERIALIZED MISMATCH', target, expected, observed });
|
|
668
|
+
}
|
|
669
|
+
return evidence;
|
|
670
|
+
}
|
|
671
|
+
|
|
280
672
|
function commandVersion(binary) {
|
|
281
673
|
const candidates = Array.isArray(binary) ? binary : [binary];
|
|
282
674
|
for (const candidate of candidates) {
|
|
@@ -308,8 +700,9 @@ async function collectTeamConfiguration(prompt, adapter) {
|
|
|
308
700
|
const displayName = (await prompt.question(`${role.label} display name [${role.label}]: `)).trim() || role.label;
|
|
309
701
|
const persona = (await prompt.question(`${role.label} persona [professional and role-focused]: `)).trim() || 'professional and role-focused';
|
|
310
702
|
const model = (await prompt.question(`${role.label} model ID [host default]: `)).trim();
|
|
311
|
-
const reasoningEffort =
|
|
312
|
-
? (await prompt.question(`${role.label} reasoning effort [${role.defaultEffort}]: `)).trim()
|
|
703
|
+
const reasoningEffort = supportsNativeReasoning(adapter, role.id)
|
|
704
|
+
? (await prompt.question(`${role.label} reasoning effort [${adapter.modelBinding === 'global-profiles' && model ? role.defaultEffort : 'host default'}]: `)).trim()
|
|
705
|
+
|| (adapter.modelBinding === 'global-profiles' && model ? role.defaultEffort : null)
|
|
313
706
|
: null;
|
|
314
707
|
roles[role.id] = { displayName, persona, model: model || null, reasoning_effort: reasoningEffort, enabled };
|
|
315
708
|
}
|
|
@@ -326,20 +719,52 @@ async function init(options) {
|
|
|
326
719
|
if (!options.allowProductRepo && productRepositoryAt(controlRoot)) {
|
|
327
720
|
throw new Error('Control path appears to be a product repository. Use a separate project-control workspace, or pass --allow-product-repo after reviewing the risk.');
|
|
328
721
|
}
|
|
722
|
+
let previousManifest = null;
|
|
723
|
+
if (await exists(installationManifestPath(controlRoot))) previousManifest = await loadInstallationManifest(controlRoot);
|
|
329
724
|
const installContext = context(controlRoot);
|
|
330
|
-
const
|
|
725
|
+
const projectPlanned = [
|
|
331
726
|
...adapter.projectPaths(controlRoot, installContext),
|
|
332
|
-
...(team ? [teamConfigPath(controlRoot)] : [])
|
|
333
|
-
...(options.global ? adapter.globalPaths(installContext) : []),
|
|
334
|
-
...(team && options.global && adapter.modelBinding === 'global-profiles' ? adapter.roleModelPaths(installContext) : [])
|
|
727
|
+
...(team ? [teamConfigPath(controlRoot)] : [])
|
|
335
728
|
];
|
|
336
|
-
const
|
|
337
|
-
if (
|
|
729
|
+
const existingProject = await collisions(projectPlanned);
|
|
730
|
+
if (existingProject.length > 0) throw new Error(`Installation aborted; existing paths would be overwritten:\n${existingProject.map((target) => ` ${target}`).join('\n')}`);
|
|
731
|
+
if (options.global) {
|
|
732
|
+
const globalPlanned = [
|
|
733
|
+
...adapter.globalPaths(installContext),
|
|
734
|
+
...(team && adapter.modelBinding === 'global-profiles' ? adapter.roleModelPaths(installContext) : []),
|
|
735
|
+
...(team && adapter.modelBinding === 'project-frontmatter' && adapter.globalRoleModelPaths
|
|
736
|
+
? adapter.globalRoleModelPaths(installContext)
|
|
737
|
+
: [])
|
|
738
|
+
];
|
|
739
|
+
const globalPairs = await materializedAssetPairs(adapter.globalAssets, controlRoot, installContext);
|
|
740
|
+
const teamModelPaths = team
|
|
741
|
+
? new Set([
|
|
742
|
+
...(adapter.modelBinding === 'global-profiles' && typeof adapter.roleModelPaths === 'function' ? adapter.roleModelPaths(installContext) : []),
|
|
743
|
+
...(adapter.modelBinding === 'project-frontmatter' && typeof adapter.globalRoleModelPaths === 'function' ? adapter.globalRoleModelPaths(installContext) : [])
|
|
744
|
+
].map((target) => resolve(target)))
|
|
745
|
+
: new Set();
|
|
746
|
+
const existingGlobal = [];
|
|
747
|
+
for (const target of globalPlanned) {
|
|
748
|
+
if (!(await exists(target))) continue;
|
|
749
|
+
if (teamModelPaths.has(resolve(target)) || !(await globalPathReusable(target, globalPairs))) existingGlobal.push(target);
|
|
750
|
+
}
|
|
751
|
+
if (existingGlobal.length > 0) throw new Error(`Installation aborted; existing global paths would be overwritten:\n${existingGlobal.map((target) => ` ${target}`).join('\n')}`);
|
|
752
|
+
}
|
|
338
753
|
await installAssets(adapter.projectAssets, controlRoot, installContext);
|
|
339
754
|
if (team) await writeTeamConfig(controlRoot, team);
|
|
340
755
|
if (team) await applyOverlay(controlRoot, true, team);
|
|
341
|
-
if (team) await applyTeamBinding(adapter, controlRoot, team, installContext);
|
|
342
756
|
if (options.global) await installAssets(adapter.globalAssets, controlRoot, installContext);
|
|
757
|
+
if (team) await applyTeamBinding(adapter, controlRoot, team, installContext, 'project');
|
|
758
|
+
if (team && options.global) await applyTeamBinding(adapter, controlRoot, team, installContext, 'global');
|
|
759
|
+
const manifest = await installationManifestFor({
|
|
760
|
+
adapter,
|
|
761
|
+
controlRoot,
|
|
762
|
+
installContext,
|
|
763
|
+
team,
|
|
764
|
+
global: options.global,
|
|
765
|
+
previous: previousManifest
|
|
766
|
+
});
|
|
767
|
+
await writeInstallationManifest(controlRoot, manifest);
|
|
343
768
|
process.stdout.write(`Triad+ installed for ${adapter.label} in ${controlRoot}\n`);
|
|
344
769
|
if (adapter.globalEntry) {
|
|
345
770
|
process.stdout.write(`Install user-level assets with --global to expose ${adapter.entry}.\n`);
|
|
@@ -350,7 +775,7 @@ async function init(options) {
|
|
|
350
775
|
async function currentTeam(controlRoot) {
|
|
351
776
|
const target = teamConfigPath(controlRoot);
|
|
352
777
|
if (!(await exists(target))) return null;
|
|
353
|
-
try { return JSON.parse(await readFile(target, 'utf8')); }
|
|
778
|
+
try { return validateTeamConfiguration(JSON.parse(await readFile(target, 'utf8'))); }
|
|
354
779
|
catch { throw new Error(`Cannot safely upgrade an invalid team config: ${target}`); }
|
|
355
780
|
}
|
|
356
781
|
|
|
@@ -388,7 +813,12 @@ async function upgrade(options) {
|
|
|
388
813
|
if (!adapter) throw new Error(`Choose --host ${listAdapters().map((item) => item.id).join(', ')}.`);
|
|
389
814
|
if (!options.control) throw new Error('Provide --control <project-control-path>.');
|
|
390
815
|
const controlRoot = resolve(options.control);
|
|
391
|
-
await
|
|
816
|
+
await requireExistingDirectory(controlRoot);
|
|
817
|
+
let previousManifest = null;
|
|
818
|
+
if (await exists(installationManifestPath(controlRoot))) previousManifest = await loadInstallationManifest(controlRoot);
|
|
819
|
+
if (previousManifest && previousManifest.manifest.adapter !== adapter.id) {
|
|
820
|
+
throw new Error(`Installation manifest belongs to ${previousManifest.manifest.adapter}, not ${adapter.id}.`);
|
|
821
|
+
}
|
|
392
822
|
const team = await currentTeam(controlRoot);
|
|
393
823
|
const installContext = context(controlRoot);
|
|
394
824
|
const stamp = new Date().toISOString().replaceAll(':', '-').replaceAll('.', '-');
|
|
@@ -396,16 +826,28 @@ async function upgrade(options) {
|
|
|
396
826
|
process.stdout.write(`Triad+ upgrade ${options.apply ? 'applying' : 'plan'} for ${adapter.label}\n`);
|
|
397
827
|
await refreshAssets(adapter.projectAssets, controlRoot, installContext, join(backupRoot, 'project'), options.apply);
|
|
398
828
|
if (team && options.apply && adapter.modelBinding === 'project-frontmatter') {
|
|
399
|
-
await applyTeamBinding(adapter, controlRoot, team, installContext);
|
|
829
|
+
await applyTeamBinding(adapter, controlRoot, team, installContext, 'project');
|
|
400
830
|
}
|
|
401
831
|
await upgradeRunStateDelivery(controlRoot, backupRoot, options.apply);
|
|
402
832
|
if (team) await applyOverlay(controlRoot, options.apply, team);
|
|
403
833
|
else process.stdout.write(' Instructions skipped: .triad-plus/team.json is not configured\n');
|
|
404
834
|
if (options.global) {
|
|
405
835
|
await refreshAssets(adapter.globalAssets, controlRoot, installContext, join(backupRoot, 'global'), options.apply);
|
|
406
|
-
if (team && options.apply
|
|
407
|
-
|
|
408
|
-
|
|
836
|
+
if (team && options.apply) await applyTeamBinding(adapter, controlRoot, team, installContext, 'global');
|
|
837
|
+
}
|
|
838
|
+
if (options.apply) {
|
|
839
|
+
const manifest = await installationManifestFor({
|
|
840
|
+
adapter,
|
|
841
|
+
controlRoot,
|
|
842
|
+
installContext,
|
|
843
|
+
team,
|
|
844
|
+
global: options.global,
|
|
845
|
+
previous: previousManifest
|
|
846
|
+
});
|
|
847
|
+
await writeInstallationManifest(controlRoot, manifest);
|
|
848
|
+
process.stdout.write(` Installation manifest updated ${installationManifestPath(controlRoot)}\n`);
|
|
849
|
+
} else {
|
|
850
|
+
process.stdout.write(` Installation manifest would be ${previousManifest ? 'updated' : 'created'} ${installationManifestPath(controlRoot)}\n`);
|
|
409
851
|
}
|
|
410
852
|
if (!options.apply) process.stdout.write('Dry run only. Re-run with --apply to update managed assets.\n');
|
|
411
853
|
}
|
|
@@ -425,6 +867,181 @@ async function importBmadStory(options) {
|
|
|
425
867
|
process.stdout.write(`Provenance ${result.provenancePath}\n`);
|
|
426
868
|
}
|
|
427
869
|
|
|
870
|
+
function installationScopeText(manifest, scope) {
|
|
871
|
+
const status = manifestScopeStatus(manifest, scope);
|
|
872
|
+
if (status === 'installed') return 'yes';
|
|
873
|
+
if (status === 'not_configured') return 'no';
|
|
874
|
+
return status;
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
async function versionCommand(options) {
|
|
878
|
+
if (!options.control) throw new Error('Provide --control <project-control-path>.');
|
|
879
|
+
const controlRoot = resolve(options.control);
|
|
880
|
+
await requireExistingDirectory(controlRoot);
|
|
881
|
+
process.stdout.write(`Triad+ CLI ${packageVersion}\n`);
|
|
882
|
+
process.stdout.write(`Workspace ${controlRoot}\n`);
|
|
883
|
+
let loaded;
|
|
884
|
+
try { loaded = await loadInstallationManifest(controlRoot); }
|
|
885
|
+
catch (error) {
|
|
886
|
+
process.stdout.write('Installed Triad unknown (manifest invalid)\n');
|
|
887
|
+
process.stdout.write(`Manifest invalid — ${error.message}\n`);
|
|
888
|
+
throw error;
|
|
889
|
+
}
|
|
890
|
+
if (!loaded) {
|
|
891
|
+
process.stdout.write('Installed Triad unknown (legacy installation)\n');
|
|
892
|
+
process.stdout.write('Manifest legacy / manifest missing\n');
|
|
893
|
+
process.stdout.write('Adapter unknown\n');
|
|
894
|
+
process.stdout.write('Project install unknown\n');
|
|
895
|
+
process.stdout.write('Global install unknown\n');
|
|
896
|
+
return;
|
|
897
|
+
}
|
|
898
|
+
const manifest = loaded.manifest;
|
|
899
|
+
const adapter = getAdapter(manifest.adapter);
|
|
900
|
+
const uninstalled = manifestIsUninstalled(manifest);
|
|
901
|
+
process.stdout.write(`Installed Triad ${uninstalled ? 'unknown (uninstalled)' : manifest.triad_version}\n`);
|
|
902
|
+
process.stdout.write(`Manifest ${uninstalled ? 'OK (uninstalled)' : 'OK'}\n`);
|
|
903
|
+
process.stdout.write(`Adapter ${adapter?.label ?? manifest.adapter}\n`);
|
|
904
|
+
process.stdout.write(`Project install ${installationScopeText(manifest, 'project')}\n`);
|
|
905
|
+
process.stdout.write(`Global install ${installationScopeText(manifest, 'global')}\n`);
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
async function uninstall(options) {
|
|
909
|
+
const adapter = getAdapter(options.host);
|
|
910
|
+
if (!adapter) throw new Error(`Choose --host ${listAdapters().map((item) => item.id).join(', ')}.`);
|
|
911
|
+
if (!options.control) throw new Error('Provide --control <project-control-path>.');
|
|
912
|
+
const controlRoot = resolve(options.control);
|
|
913
|
+
await requireExistingDirectory(controlRoot);
|
|
914
|
+
const loaded = await loadInstallationManifest(controlRoot);
|
|
915
|
+
process.stdout.write(`Triad+ uninstall — ${adapter.label}\n`);
|
|
916
|
+
if (!loaded) {
|
|
917
|
+
process.stdout.write('Manifest legacy / manifest missing\n');
|
|
918
|
+
process.stdout.write('No files were changed. Refusing to delete assets without an installation manifest.\n');
|
|
919
|
+
return;
|
|
920
|
+
}
|
|
921
|
+
const manifest = loaded.manifest;
|
|
922
|
+
if (manifest.adapter !== adapter.id) throw new Error(`Installation manifest belongs to ${manifest.adapter}, not ${adapter.id}.`);
|
|
923
|
+
const installContext = context(controlRoot);
|
|
924
|
+
const selectedScopes = options.global ? ['project', 'global'] : ['project'];
|
|
925
|
+
const scopeStats = Object.fromEntries(selectedScopes.map((scope) => [scope, { modified: 0, removed: 0 }]));
|
|
926
|
+
if (options.global) process.stdout.write('Global assets are preserved because cross-workspace ownership cannot be proven.\n');
|
|
927
|
+
for (const scope of selectedScopes) {
|
|
928
|
+
const registryPaths = await managedAssetPaths(adapter, controlRoot, installContext, scope);
|
|
929
|
+
process.stdout.write(`\n${scope === 'project' ? 'Project' : 'Global'} managed assets\n`);
|
|
930
|
+
const entries = manifest.managed_assets.filter((asset) => asset.scope === scope);
|
|
931
|
+
if (entries.length === 0) process.stdout.write(' (none)\n');
|
|
932
|
+
for (const asset of entries) {
|
|
933
|
+
const target = manifestAssetPath(controlRoot, asset);
|
|
934
|
+
const display = displayManagedAssetPath(asset);
|
|
935
|
+
let info;
|
|
936
|
+
try { info = await lstat(target); } catch (error) {
|
|
937
|
+
if (error.code === 'ENOENT') {
|
|
938
|
+
process.stdout.write(` ABSENT ${display}\n`);
|
|
939
|
+
continue;
|
|
940
|
+
}
|
|
941
|
+
process.stdout.write(` PRESERVE ${display}\n`);
|
|
942
|
+
process.stdout.write(` cannot inspect asset: ${error.message}\n`);
|
|
943
|
+
scopeStats[scope].modified += 1;
|
|
944
|
+
continue;
|
|
945
|
+
}
|
|
946
|
+
if (scope === 'global') {
|
|
947
|
+
process.stdout.write(` PRESERVE ${display}\n`);
|
|
948
|
+
process.stdout.write(' global ownership may be shared by another control workspace\n');
|
|
949
|
+
scopeStats[scope].modified += 1;
|
|
950
|
+
continue;
|
|
951
|
+
}
|
|
952
|
+
if (!assetAllowedByRegistry(registryPaths, controlRoot, asset)) {
|
|
953
|
+
process.stdout.write(` PRESERVE ${display}\n`);
|
|
954
|
+
process.stdout.write(' path is not in the current adapter managed plan\n');
|
|
955
|
+
scopeStats[scope].modified += 1;
|
|
956
|
+
continue;
|
|
957
|
+
}
|
|
958
|
+
if (asset.kind === 'managed_block') {
|
|
959
|
+
if (!info.isFile()) {
|
|
960
|
+
process.stdout.write(` PRESERVE ${display}\n`);
|
|
961
|
+
process.stdout.write(' managed block host file is not a regular file\n');
|
|
962
|
+
scopeStats[scope].modified += 1;
|
|
963
|
+
continue;
|
|
964
|
+
}
|
|
965
|
+
let source;
|
|
966
|
+
let managed;
|
|
967
|
+
try {
|
|
968
|
+
source = await readFile(target, 'utf8');
|
|
969
|
+
managed = managedBlockIn(source, asset.start_marker, asset.end_marker);
|
|
970
|
+
} catch {
|
|
971
|
+
managed = null;
|
|
972
|
+
}
|
|
973
|
+
if (!managed || sha256Text(managed.content) !== asset.sha256) {
|
|
974
|
+
process.stdout.write(` PRESERVE ${display}\n`);
|
|
975
|
+
process.stdout.write(' managed block is modified, ambiguous, or unverifiable\n');
|
|
976
|
+
scopeStats[scope].modified += 1;
|
|
977
|
+
continue;
|
|
978
|
+
}
|
|
979
|
+
process.stdout.write(` ${options.apply ? 'REMOVE' : 'WOULD REMOVE'} block ${display}\n`);
|
|
980
|
+
if (options.apply) {
|
|
981
|
+
await writeFile(target, `${source.slice(0, managed.start)}${source.slice(managed.blockEnd)}`, 'utf8');
|
|
982
|
+
scopeStats[scope].removed += 1;
|
|
983
|
+
}
|
|
984
|
+
continue;
|
|
985
|
+
}
|
|
986
|
+
if (!info.isFile()) {
|
|
987
|
+
process.stdout.write(` PRESERVE ${display}\n`);
|
|
988
|
+
process.stdout.write(' asset is no longer a regular file\n');
|
|
989
|
+
scopeStats[scope].modified += 1;
|
|
990
|
+
continue;
|
|
991
|
+
}
|
|
992
|
+
const observed = await sha256File(target);
|
|
993
|
+
if (observed !== asset.sha256) {
|
|
994
|
+
process.stdout.write(` PRESERVE ${display}\n`);
|
|
995
|
+
process.stdout.write(' content differs from installation manifest\n');
|
|
996
|
+
scopeStats[scope].modified += 1;
|
|
997
|
+
continue;
|
|
998
|
+
}
|
|
999
|
+
process.stdout.write(` ${options.apply ? 'REMOVE' : 'WOULD REMOVE'} ${display}\n`);
|
|
1000
|
+
if (options.apply) {
|
|
1001
|
+
await rm(target, { force: true });
|
|
1002
|
+
scopeStats[scope].removed += 1;
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
process.stdout.write('\nPreserved state\n');
|
|
1007
|
+
for (const preserved of ['.triad-plus/team.json', '.loop/', 'project.yaml', 'features/', 'artifacts/', 'evidence/']) {
|
|
1008
|
+
process.stdout.write(` KEEP ${preserved}\n`);
|
|
1009
|
+
}
|
|
1010
|
+
if (!options.apply) {
|
|
1011
|
+
process.stdout.write('\nNo files were changed. Re-run with --apply to uninstall managed assets.\n');
|
|
1012
|
+
return;
|
|
1013
|
+
}
|
|
1014
|
+
const scopeStatus = { project: manifestScopeStatus(manifest, 'project'), global: manifestScopeStatus(manifest, 'global') };
|
|
1015
|
+
for (const scope of selectedScopes) scopeStatus[scope] = scopeStats[scope].modified > 0 ? 'partial' : 'uninstalled';
|
|
1016
|
+
const status = overallInstallationStatus(scopeStatus);
|
|
1017
|
+
const updated = buildInstallationManifest({
|
|
1018
|
+
triadVersion: manifest.triad_version,
|
|
1019
|
+
adapter: manifest.adapter,
|
|
1020
|
+
installedAt: manifest.installed_at,
|
|
1021
|
+
updatedAt: new Date().toISOString(),
|
|
1022
|
+
uninstalledAt: status === 'uninstalled' ? new Date().toISOString() : undefined,
|
|
1023
|
+
scopes: manifest.scopes,
|
|
1024
|
+
scopeStatus,
|
|
1025
|
+
managedAssets: manifest.managed_assets,
|
|
1026
|
+
status
|
|
1027
|
+
});
|
|
1028
|
+
await writeInstallationManifest(controlRoot, updated);
|
|
1029
|
+
process.stdout.write(`\nInstallation manifest updated ${installationManifestPath(controlRoot)}\n`);
|
|
1030
|
+
const projectPartial = scopeStatus.project === 'partial';
|
|
1031
|
+
const globalPreserved = options.global && scopeStatus.global === 'partial';
|
|
1032
|
+
if (status === 'uninstalled') {
|
|
1033
|
+
process.stdout.write('Complete project uninstall; user state was preserved.\n');
|
|
1034
|
+
} else if (projectPartial && globalPreserved) {
|
|
1035
|
+
process.stdout.write('Partial uninstall; modified or unverifiable project assets and shared global assets were preserved.\n');
|
|
1036
|
+
} else if (projectPartial) {
|
|
1037
|
+
process.stdout.write('Partial project uninstall; modified or unverifiable assets were preserved.\n');
|
|
1038
|
+
} else if (globalPreserved) {
|
|
1039
|
+
process.stdout.write('Project uninstall complete; global assets were intentionally preserved because shared ownership could not be proven.\n');
|
|
1040
|
+
} else {
|
|
1041
|
+
process.stdout.write('Uninstall completed conservatively; preserved assets remain in place.\n');
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
|
|
428
1045
|
async function doctor(options) {
|
|
429
1046
|
if (!options.control) throw new Error('Provide --control <project-control-path>.');
|
|
430
1047
|
const controlRoot = resolve(options.control);
|
|
@@ -432,13 +1049,19 @@ async function doctor(options) {
|
|
|
432
1049
|
if (requested.some((adapter) => !adapter)) throw new Error(`Choose --host ${listAdapters().map((item) => item.id).join(', ')}.`);
|
|
433
1050
|
let team = null;
|
|
434
1051
|
if (await exists(teamConfigPath(controlRoot))) {
|
|
435
|
-
try { team = JSON.parse(await readFile(teamConfigPath(controlRoot), 'utf8')); }
|
|
1052
|
+
try { team = validateTeamConfiguration(JSON.parse(await readFile(teamConfigPath(controlRoot), 'utf8'))); }
|
|
436
1053
|
catch { team = 'invalid'; }
|
|
437
1054
|
}
|
|
438
1055
|
for (const adapter of requested) {
|
|
439
1056
|
const installContext = context(controlRoot);
|
|
1057
|
+
const installation = await inspectInstallation(controlRoot, adapter, installContext);
|
|
440
1058
|
const absent = [];
|
|
441
1059
|
for (const target of adapter.projectPaths(controlRoot, installContext)) if (!(await exists(target))) absent.push(target);
|
|
1060
|
+
const globalDetected = adapter.globalPaths ? await anyExist(adapter.globalPaths(installContext)) : false;
|
|
1061
|
+
const inspectGlobal = Boolean(options.global || globalDetected);
|
|
1062
|
+
const globalTargets = inspectGlobal && adapter.globalPaths ? adapter.globalPaths(installContext) : [];
|
|
1063
|
+
const globalAbsent = [];
|
|
1064
|
+
for (const target of globalTargets) if (!(await exists(target))) globalAbsent.push(target);
|
|
442
1065
|
const binary = commandVersion(adapter.binaryCandidates ?? adapter.binary);
|
|
443
1066
|
const node = commandVersion('node');
|
|
444
1067
|
const manifestPath = join(controlRoot, '.triad-runtime', 'adapter.json');
|
|
@@ -448,25 +1071,105 @@ async function doctor(options) {
|
|
|
448
1071
|
const roleTargets = targets.filter((target) => /[/\\]agents[/\\]triad-/.test(target));
|
|
449
1072
|
const triadSkillTargets = targets.filter((target) => /[/\\]skills[/\\]triad$/.test(target));
|
|
450
1073
|
const capability = manifest ? capabilitySnapshot(controlRoot, manifestPath) : null;
|
|
451
|
-
process.stdout.write(
|
|
452
|
-
process.stdout.write(
|
|
453
|
-
process.stdout.write(
|
|
454
|
-
process.stdout.write(`
|
|
455
|
-
if (
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
1074
|
+
process.stdout.write(`\n${formatDoctorSection(`Triad+ doctor — ${adapter.label}`)}\n`);
|
|
1075
|
+
process.stdout.write(`${formatDoctorLine(adapter.label, absent.length || globalAbsent.length || installation.issues.length ? 'incomplete' : 'OK')}\n`);
|
|
1076
|
+
process.stdout.write(`${formatDoctorSection('Triad+ installation')}\n`);
|
|
1077
|
+
process.stdout.write(` ${formatDoctorLine('CLI version', packageVersion)}\n`);
|
|
1078
|
+
if (installation.state === 'legacy') {
|
|
1079
|
+
process.stdout.write(` ${formatDoctorLine('Installed version', 'unknown (legacy installation)')}\n`);
|
|
1080
|
+
process.stdout.write(` ${formatDoctorLine('Manifest', 'legacy / manifest missing')}\n`);
|
|
1081
|
+
process.stdout.write(` ${formatDoctorLine('Adapter', adapter.label)}\n`);
|
|
1082
|
+
process.stdout.write(` ${formatDoctorLine('Project scope', 'unknown (legacy)')}\n`);
|
|
1083
|
+
process.stdout.write(` ${formatDoctorLine('Global scope', 'unknown (legacy)')}\n`);
|
|
1084
|
+
} else if (installation.state === 'invalid') {
|
|
1085
|
+
process.stdout.write(` ${formatDoctorLine('Installed version', 'unknown (manifest invalid)')}\n`);
|
|
1086
|
+
process.stdout.write(` ${formatDoctorLine('Manifest', 'invalid')} — ${installation.message}\n`);
|
|
1087
|
+
process.stdout.write(` ${formatDoctorLine('Adapter', adapter.label)}\n`);
|
|
1088
|
+
process.stdout.write(` ${formatDoctorLine('Project scope', 'unknown')}\n`);
|
|
1089
|
+
process.stdout.write(` ${formatDoctorLine('Global scope', 'unknown')}\n`);
|
|
1090
|
+
} else {
|
|
1091
|
+
const versionStatus = installation.state === 'uninstalled'
|
|
1092
|
+
? `unknown (uninstalled; last ${installation.manifest.triad_version})`
|
|
1093
|
+
: installation.manifest.triad_version;
|
|
1094
|
+
const versionWarning = installation.versionRelation === 1
|
|
1095
|
+
? ' — CLI newer / upgrade available'
|
|
1096
|
+
: installation.versionRelation === -1
|
|
1097
|
+
? ' — CLI older than installed version'
|
|
1098
|
+
: '';
|
|
1099
|
+
const issueStatus = installation.issues.some((issue) => issue.status === 'modified' || issue.status === 'present_after_uninstall')
|
|
1100
|
+
? 'managed asset modified'
|
|
1101
|
+
: installation.issues.some((issue) => issue.status === 'missing')
|
|
1102
|
+
? 'managed asset missing'
|
|
1103
|
+
: installation.state === 'uninstalled' ? 'OK (uninstalled)' : 'OK';
|
|
1104
|
+
process.stdout.write(` ${formatDoctorLine('Installed version', versionStatus)}${versionWarning}\n`);
|
|
1105
|
+
process.stdout.write(` ${formatDoctorLine('Manifest', issueStatus)}\n`);
|
|
1106
|
+
process.stdout.write(` ${formatDoctorLine('Adapter', adapter.label)}\n`);
|
|
1107
|
+
process.stdout.write(` ${formatDoctorLine('Project scope', installationScopeText(installation.manifest, 'project'))}\n`);
|
|
1108
|
+
process.stdout.write(` ${formatDoctorLine('Global scope', installationScopeText(installation.manifest, 'global'))}\n`);
|
|
1109
|
+
}
|
|
1110
|
+
process.stdout.write(`${formatDoctorSection('Runtime and installation')}\n`);
|
|
1111
|
+
process.stdout.write(` ${formatDoctorLine('Host runtime', binary ? `OK (${binary})` : 'not installed or version unavailable')}\n`);
|
|
1112
|
+
process.stdout.write(` ${formatDoctorLine('Verifier', node && await exists(join(controlRoot, '.triad-runtime', 'triad-verify.mjs')) ? 'OK' : 'incomplete')}\n`);
|
|
1113
|
+
process.stdout.write(` ${formatDoctorLine('Adapter', manifest ? 'OK' : 'missing or different adapter')}\n`);
|
|
1114
|
+
if (roleTargets.length) process.stdout.write(` ${formatDoctorLine('Role agents', await allExist(roleTargets) ? 'OK' : 'missing')}\n`);
|
|
1115
|
+
if (triadSkillTargets.length) process.stdout.write(` ${formatDoctorLine('Triad skill', await allExist(triadSkillTargets) ? 'OK' : 'missing')}\n`);
|
|
1116
|
+
const projectSharedSkills = await sharedSkillStatuses(adapter, controlRoot, installContext, 'project');
|
|
1117
|
+
if (projectSharedSkills.length) {
|
|
1118
|
+
process.stdout.write(`${formatDoctorSection('Shared skills')}\n`);
|
|
1119
|
+
for (const skill of projectSharedSkills) process.stdout.write(` ${formatDoctorLine(skill.name, skill.status)}\n`);
|
|
1120
|
+
}
|
|
1121
|
+
if (inspectGlobal) {
|
|
1122
|
+
const globalSharedSkills = await sharedSkillStatuses(adapter, controlRoot, installContext, 'global');
|
|
1123
|
+
if (globalSharedSkills.length) {
|
|
1124
|
+
process.stdout.write(`${formatDoctorSection('Global shared skills')}\n`);
|
|
1125
|
+
for (const skill of globalSharedSkills) process.stdout.write(` ${formatDoctorLine(skill.name, skill.status)}\n`);
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
process.stdout.write(`${formatDoctorSection('Verification and capabilities')}\n`);
|
|
1129
|
+
process.stdout.write(` ${formatDoctorLine('Verification', capability?.verification?.selected_mode ?? 'unavailable')}${capability?.verification?.reason ? ` (${capability.verification.reason})` : ''}\n`);
|
|
1130
|
+
process.stdout.write(`${formatDoctorSection('Configuration')}\n`);
|
|
1131
|
+
process.stdout.write(` ${formatDoctorLine('Team config', team === 'invalid' ? 'invalid' : team ? 'OK' : 'not configured')}\n`);
|
|
1132
|
+
process.stdout.write(` ${formatDoctorLine('Evaluator+', team?.roles?.evaluator?.enabled === true ? 'configured' : 'not configured')}\n`);
|
|
1133
|
+
const modelFields = adapter.modelBinding === 'global-profiles'
|
|
1134
|
+
? 'model, reasoning_effort'
|
|
1135
|
+
: Array.isArray(adapter.modelFields) && adapter.modelFields.length ? adapter.modelFields.join(', ') : 'team.json record / host-managed';
|
|
1136
|
+
process.stdout.write(` ${formatDoctorLine('Model binding', `${adapter.modelBinding ?? 'unavailable'} (${modelFields})`)}\n`);
|
|
1137
|
+
const modelScopes = [];
|
|
1138
|
+
const validTeam = team !== 'invalid' ? team : null;
|
|
1139
|
+
if (adapter.modelBinding === 'project-frontmatter') {
|
|
1140
|
+
modelScopes.push(['project', await modelBindingEvidence(adapter, controlRoot, installContext, validTeam, 'project')]);
|
|
1141
|
+
}
|
|
1142
|
+
if (inspectGlobal && (adapter.modelBinding === 'project-frontmatter' || adapter.modelBinding === 'global-profiles')) {
|
|
1143
|
+
modelScopes.push(['global', await modelBindingEvidence(adapter, controlRoot, installContext, validTeam, 'global')]);
|
|
1144
|
+
}
|
|
1145
|
+
if (modelScopes.some(([, entries]) => entries.length)) {
|
|
1146
|
+
process.stdout.write(`${formatDoctorSection('Model configuration evidence')}\n`);
|
|
1147
|
+
for (const [scope, entries] of modelScopes) {
|
|
1148
|
+
for (const entry of entries) {
|
|
1149
|
+
const field = hostModelField(adapter, entry.role.id, 'reasoning_effort');
|
|
1150
|
+
const desired = validTeam?.roles?.[entry.role.id];
|
|
1151
|
+
const desiredText = desired
|
|
1152
|
+
? `desired model=${desired.model || 'host default'}${field ? ` ${field}=${desired.reasoning_effort || 'host default'}` : ''}`
|
|
1153
|
+
: 'desired unavailable';
|
|
1154
|
+
const observedText = entry.observed ? ` — materialized ${JSON.stringify(entry.observed)}` : '';
|
|
1155
|
+
process.stdout.write(` ${formatDoctorLine(`${scope} ${entry.role.label}`, entry.status)} ${desiredText}${observedText}\n`);
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
if (adapter.modelBinding === 'project-frontmatter') {
|
|
1159
|
+
process.stdout.write(` ${formatDoctorLine('Host default', 'UNKNOWN / NOT OBSERVABLE')}\n`);
|
|
1160
|
+
process.stdout.write(` ${formatDoctorLine('Current session', 'UNKNOWN / NOT OBSERVABLE')}\n`);
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
460
1163
|
const overlay = await overlayPlan(controlRoot, team).catch(() => null);
|
|
461
|
-
process.stdout.write(` Instructions
|
|
1164
|
+
process.stdout.write(` ${formatDoctorLine('Instructions', overlay ? overlay.action === 'update' ? 'managed' : `needs ${overlay.action}` : 'invalid managed block')}\n`);
|
|
462
1165
|
const globalAgents = join(codexHome(), 'AGENTS.md');
|
|
463
1166
|
if (await exists(globalAgents)) {
|
|
464
1167
|
const globalText = await readFile(globalAgents, 'utf8');
|
|
465
1168
|
const fixedIdentity = /(?:identity|name)[\s\S]{0,100}(?:always|only|must)/i.test(globalText);
|
|
466
|
-
process.stdout.write(` Identity policy
|
|
1169
|
+
process.stdout.write(` ${formatDoctorLine('Identity policy', fixedIdentity ? 'host rule detected; review for Triad role conflicts' : 'no fixed host rule detected')}\n`);
|
|
467
1170
|
}
|
|
468
1171
|
if (options.hookConfig && adapter.lifecycle) {
|
|
469
|
-
process.stdout.write(` Hook config
|
|
1172
|
+
process.stdout.write(` ${formatDoctorLine('Hook config', 'declared; run runtime capability detection for detailed status')}\n`);
|
|
470
1173
|
}
|
|
471
1174
|
}
|
|
472
1175
|
}
|
|
@@ -486,6 +1189,7 @@ async function interactiveInit() {
|
|
|
486
1189
|
const control = (await prompt.question(`Project-control workspace [${join(process.cwd(), 'triad-control')}]: `)).trim() || join(process.cwd(), 'triad-control');
|
|
487
1190
|
const global = ['y', 'yes'].includes((await prompt.question(`Also install user-level ${adapter.entry} assets? [y/N]: `)).trim().toLowerCase());
|
|
488
1191
|
const team = await collectTeamConfiguration(prompt, adapter);
|
|
1192
|
+
process.stdout.write(`\n${formatSetupSummary({ adapter, control, global, team })}`);
|
|
489
1193
|
const confirm = (await prompt.question('Type install to continue: ')).trim().toLowerCase();
|
|
490
1194
|
if (confirm !== 'install') return process.stdout.write('Cancelled. No files were changed.\n');
|
|
491
1195
|
await init({ host: adapter.id, control, global, team });
|
|
@@ -494,10 +1198,17 @@ async function interactiveInit() {
|
|
|
494
1198
|
}
|
|
495
1199
|
|
|
496
1200
|
try {
|
|
497
|
-
const
|
|
1201
|
+
const argv = process.argv.slice(2);
|
|
1202
|
+
if (argv.length === 1 && ['--version', '-v'].includes(argv[0])) {
|
|
1203
|
+
process.stdout.write(`${packageVersion}\n`);
|
|
1204
|
+
process.exit(0);
|
|
1205
|
+
}
|
|
1206
|
+
const options = parseArgs(argv);
|
|
498
1207
|
if (options.command === 'init') await init(options);
|
|
499
1208
|
else if (options.command === 'doctor') await doctor(options);
|
|
1209
|
+
else if (options.command === 'version') await versionCommand(options);
|
|
500
1210
|
else if (options.command === 'upgrade') await upgrade(options);
|
|
1211
|
+
else if (options.command === 'uninstall') await uninstall(options);
|
|
501
1212
|
else if (options.command === 'import-bmad-story') await importBmadStory(options);
|
|
502
1213
|
else if (!options.command) await interactiveInit();
|
|
503
1214
|
else if (options.command === '--help' || options.command === '-h') usage(0);
|