@uniqbit/mate-core 0.15.3 → 0.15.4-canary.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/claude-plugin/.claude-plugin/plugin.json +4 -0
- package/claude-plugin/hooks/artifact-finish-nudge.mjs +8 -0
- package/claude-plugin/hooks/hooks.json +37 -0
- package/claude-plugin/hooks/session-banner.mjs +8 -0
- package/claude-plugin/hooks/ts-loader.mjs +28 -0
- package/claude-plugin/hooks/validate-artifact-path.mjs +8 -0
- package/package.json +4 -2
- package/src/cli/commands/artifact/finish/openspec.ts +1 -5
- package/src/cli/commands/companion/hub.ts +99 -0
- package/src/cli/commands/companion/link.ts +0 -5
- package/src/cli/commands/companion/tui.ts +3 -2
- package/src/cli/commands/doctor.ts +254 -170
- package/src/cli/commands/install.ts +18 -14
- package/src/cli/commands/launch/shared.ts +4 -4
- package/src/cli/commands/plugin/install.ts +94 -0
- package/src/cli/commands/plugin/plugin.ts +17 -0
- package/src/cli/commands/setup.ts +4 -6
- package/src/cli/commands/update.ts +9 -11
- package/src/cli/companion-link-wizard.tsx +22 -78
- package/src/cli/main.ts +57 -13
- package/src/cli/plugin-commands.ts +4 -4
- package/src/cli/repo-list-table.tsx +2 -7
- package/src/cli/usage.ts +7 -3
- package/src/distribution.ts +3 -5
- package/src/framework.ts +4 -36
- package/src/hooks/artifact-finish-nudge.ts +212 -0
- package/src/hooks/session-banner.ts +25 -0
- package/src/hooks/validate-artifact-path.ts +236 -0
- package/src/index.ts +4 -0
- package/src/lib/context-mode-package.ts +5 -3
- package/src/lib/install.ts +31 -43
- package/src/lib/orchestrator/adapters/base.ts +7 -8
- package/src/lib/orchestrator/adapters/claude.ts +16 -0
- package/src/lib/orchestrator/adapters/opencode.ts +10 -60
- package/src/lib/orchestrator/companion-hub.ts +380 -0
- package/src/lib/orchestrator/companion-store.ts +1 -35
- package/src/lib/orchestrator/config-store.ts +94 -7
- package/src/lib/orchestrator/editor.ts +4 -20
- package/src/lib/orchestrator/framework-context.ts +119 -24
- package/src/lib/orchestrator/global-config-store.ts +1 -5
- package/src/lib/orchestrator/launcher.ts +3 -9
- package/src/lib/orchestrator/migration.ts +0 -23
- package/src/lib/orchestrator/opencode-guidance.ts +1 -2
- package/src/lib/orchestrator/repo-local-registry.ts +3 -1
- package/src/lib/orchestrator/root-context.ts +117 -0
- package/src/lib/orchestrator/setup-compatibilities.ts +1 -1
- package/src/lib/orchestrator/setup-preflight.ts +5 -18
- package/src/lib/orchestrator/types.ts +27 -19
- package/src/lib/orchestrator/working-repo-store.ts +9 -3
- package/src/lib/package-paths.ts +37 -0
- package/src/lib/update-checker.ts +5 -5
- package/src/playbooks/companion-guidance.ts +7 -7
- package/src/runtime/env.ts +0 -4
- package/src/templates/capabilities/openspec-cap/mate-skills/{mate-artifact-finish → agents/mate-artifact-finish}/SKILL.md +8 -8
- package/src/templates/capabilities/openspec-cap/mate-skills/{mate-artifact-finish → agents/mate-artifact-finish}/references/openspec.md +3 -3
- package/src/templates/capabilities/openspec-cap/mate-skills/claude/mate-artifact-finish/SKILL.md +58 -0
- package/src/templates/capabilities/openspec-cap/mate-skills/claude/mate-artifact-finish/references/openspec.md +139 -0
- package/src/templates/capabilities/openspec-cap/mate-v1/schema.yaml +6 -5
- package/src/templates/root/TEMPLATE_AGENTS.md +2 -0
- package/src/templates/root/TEMPLATE_CLAUDE.md +2 -0
- package/src/tools/setup/__snapshots__/runtime-surface-golden.test.ts.snap +3541 -0
- package/src/tools/setup/capabilities/context-mode.ts +57 -83
- package/src/tools/setup/capabilities/graphify-shared.ts +27 -0
- package/src/tools/setup/capabilities/graphify.ts +86 -295
- package/src/tools/setup/capabilities/openspec.ts +27 -30
- package/src/tools/setup/capabilities/react-doctor.ts +37 -37
- package/src/tools/setup/capabilities/rtk.ts +4 -1
- package/src/tools/setup/capabilities/tokensave-shared.ts +6 -0
- package/src/tools/setup/capabilities/tokensave.ts +50 -71
- package/src/tools/setup/context-services.ts +29 -0
- package/src/tools/setup/dynamic-plugins/host.ts +2 -1
- package/src/tools/setup/dynamic-plugins/hydrate.ts +3 -13
- package/src/tools/setup/dynamic-plugins/install.ts +124 -109
- package/src/tools/setup/dynamic-plugins/loader.ts +23 -58
- package/src/tools/setup/dynamic-plugins/paths.ts +8 -19
- package/src/tools/setup/dynamic-plugins/registry-hint.ts +14 -0
- package/src/tools/setup/engine.ts +65 -1
- package/src/tools/setup/mate.ts +28 -31
- package/src/tools/setup/plugin.ts +81 -0
- package/src/tools/setup/plugins/gitignore.ts +19 -9
- package/src/tools/setup/plugins/guidance.ts +2 -6
- package/src/tools/setup/policy.ts +4 -7
- package/src/tools/setup/providers/agent-file-sections.ts +78 -0
- package/src/tools/setup/providers/claude-format.ts +159 -0
- package/src/tools/setup/providers/claude.ts +283 -292
- package/src/tools/setup/providers/opencode-format.ts +146 -0
- package/src/tools/setup/providers/opencode.ts +171 -66
- package/src/tools/setup/providers/skill-tree.ts +38 -0
- package/src/tools/setup.ts +12 -7
- package/src/tui.ts +5 -0
- package/src/templates/capabilities/openspec-cap/claude/hooks/mate-artifact-finish.sh +0 -161
- package/src/templates/providers/claude/.claude/hooks/mate-session-banner +0 -28
- package/src/templates/providers/claude/.claude/hooks/validate-artifact-path +0 -242
- package/src/tools/setup/dynamic-plugins/pin-store.ts +0 -30
|
@@ -1,17 +1,13 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
|
|
4
|
-
import { FRAMEWORK_NAME
|
|
4
|
+
import { FRAMEWORK_NAME } from "../../framework";
|
|
5
5
|
import { getActiveDistribution } from "../../distribution";
|
|
6
|
-
import { CompanionResolver } from "../../lib/orchestrator/companion-resolver";
|
|
7
|
-
import { CompanionStore, resolvePolicyFromConfig } from "../../lib/orchestrator/companion-store";
|
|
8
|
-
import { ConfigStore } from "../../lib/orchestrator/config-store";
|
|
9
6
|
import { checkEngineRequirement } from "../../lib/orchestrator/engine-guard";
|
|
10
|
-
import { GlobalConfigStore } from "../../lib/orchestrator/global-config-store";
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import type { CapabilityConfig, FrameworkConfig } from "../../lib/orchestrator/types";
|
|
14
|
-
import { WorkingRepoStore } from "../../lib/orchestrator/working-repo-store";
|
|
7
|
+
import type { GlobalConfigStore } from "../../lib/orchestrator/global-config-store";
|
|
8
|
+
import { pathIsDirectory } from "../../lib/orchestrator/repo-local-registry";
|
|
9
|
+
import { resolveRootContext, type RootContext } from "../../lib/orchestrator/root-context";
|
|
10
|
+
import type { CapabilityConfig, FrameworkConfig, HubMember } from "../../lib/orchestrator/types";
|
|
15
11
|
import { getRequiredPluginDrift } from "../../tools/setup/policy";
|
|
16
12
|
import { resolveCommandOnPath } from "../../tools/setup/utils";
|
|
17
13
|
import { printSection, renderKeyValueTable, renderTable } from "./status";
|
|
@@ -20,11 +16,43 @@ interface DoctorDeps {
|
|
|
20
16
|
globalConfigStore?: GlobalConfigStore;
|
|
21
17
|
cwd?: string;
|
|
22
18
|
pathValue?: string;
|
|
19
|
+
/** Returns a member checkout's HEAD commit, or null when unreadable. */
|
|
20
|
+
gitHead?: (memberPath: string) => string | null;
|
|
23
21
|
}
|
|
24
22
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
23
|
+
export type DoctorKind = "core" | "working" | "companion" | "hub";
|
|
24
|
+
|
|
25
|
+
export interface DoctorToolInstallation {
|
|
26
|
+
tool: string;
|
|
27
|
+
source: string;
|
|
28
|
+
status: "ok" | "missing";
|
|
29
|
+
path: string | null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface DoctorHubMember {
|
|
33
|
+
id: string;
|
|
34
|
+
source: "git" | "local";
|
|
35
|
+
path: string;
|
|
36
|
+
exists: boolean;
|
|
37
|
+
commitStatus: "ok" | "drifted" | "missing" | "unknown";
|
|
38
|
+
materializedCommit: string | null;
|
|
39
|
+
actualCommit: string | null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface DoctorReport {
|
|
43
|
+
kind: DoctorKind;
|
|
44
|
+
cwd: string;
|
|
45
|
+
companionPath: string | null;
|
|
46
|
+
repositoryId: string | null;
|
|
47
|
+
multipleCompanions: Array<{ companionPath: string; repositoryId: string }>;
|
|
48
|
+
policy?: { allowedAgents: string[] };
|
|
49
|
+
policyError?: string;
|
|
50
|
+
capabilities?: string[];
|
|
51
|
+
toolInstallations?: DoctorToolInstallation[];
|
|
52
|
+
requiredPluginDrift?: Array<{ pluginId: string; kind: string; reason: string }>;
|
|
53
|
+
engineRequirement?: { range: string; ok: boolean; detail: string };
|
|
54
|
+
hub?: { members: DoctorHubMember[] };
|
|
55
|
+
resolutionFailures: Array<{ companionPath: string; message: string }>;
|
|
28
56
|
}
|
|
29
57
|
|
|
30
58
|
interface ToolCheck {
|
|
@@ -33,77 +61,26 @@ interface ToolCheck {
|
|
|
33
61
|
source: string;
|
|
34
62
|
}
|
|
35
63
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
return
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
64
|
+
/** Doctor's view of the shared root kind: registry/env resolution means cwd is a working repo. */
|
|
65
|
+
function doctorKind(root: RootContext): DoctorKind {
|
|
66
|
+
if (root.kind === "core") return "core";
|
|
67
|
+
if (root.kind === "hub") return "hub";
|
|
68
|
+
if (root.origin === "env" || root.origin === "registry") return "working";
|
|
69
|
+
return root.kind;
|
|
42
70
|
}
|
|
43
71
|
|
|
44
|
-
|
|
45
|
-
const localDir = path.join(cwd, `.${FRAMEWORK_NAME}`);
|
|
46
|
-
const localLegacyDirs = frameworkConfig.legacyNames.map((name) => path.join(cwd, `.${name}`));
|
|
47
|
-
await migrateConfigDir(localDir, localLegacyDirs);
|
|
48
|
-
|
|
72
|
+
function defaultGitHead(memberPath: string): string | null {
|
|
49
73
|
try {
|
|
50
|
-
|
|
51
|
-
|
|
74
|
+
return execFileSync("git", ["-C", memberPath, "rev-parse", "HEAD"], {
|
|
75
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
76
|
+
})
|
|
77
|
+
.toString()
|
|
78
|
+
.trim();
|
|
52
79
|
} catch {
|
|
53
|
-
return
|
|
80
|
+
return null;
|
|
54
81
|
}
|
|
55
82
|
}
|
|
56
83
|
|
|
57
|
-
function renderList(items: string[], empty: string): string {
|
|
58
|
-
return items.length > 0 ? items.map((item) => `- ${item}`).join("\n") : empty;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function renderEngineRequirement(
|
|
62
|
-
config: FrameworkConfig,
|
|
63
|
-
distributionName: string,
|
|
64
|
-
currentVersion: string,
|
|
65
|
-
): string {
|
|
66
|
-
const range = config.engines?.[distributionName];
|
|
67
|
-
const result = checkEngineRequirement(config, distributionName, currentVersion);
|
|
68
|
-
return result.ok
|
|
69
|
-
? `engines.${distributionName}: satisfied (requires ${range}, running ${currentVersion})`
|
|
70
|
-
: `engines.${distributionName}: ${result.reason}`;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
// Only the engines key matching the running distribution produces a
|
|
74
|
-
// diagnostic; foreign distribution keys are ignored entirely.
|
|
75
|
-
function printEngineRequirement(config: FrameworkConfig): void {
|
|
76
|
-
const distribution = getActiveDistribution().config;
|
|
77
|
-
if (!config.engines?.[distribution.name]) return;
|
|
78
|
-
printSection(
|
|
79
|
-
"Version Requirement",
|
|
80
|
-
renderEngineRequirement(config, distribution.name, distribution.version),
|
|
81
|
-
);
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
// Companions with no drift produce no section at all: required plugins are
|
|
85
|
-
// silent when healthy.
|
|
86
|
-
function printRequiredPluginDrift(config: FrameworkConfig): void {
|
|
87
|
-
const drift = getRequiredPluginDrift(config);
|
|
88
|
-
if (drift.length === 0) return;
|
|
89
|
-
printSection(
|
|
90
|
-
"Required Plugin Drift",
|
|
91
|
-
renderTable(
|
|
92
|
-
["Plugin", "Kind", "Issue"],
|
|
93
|
-
drift.map((entry) => [entry.pluginId, entry.kind, entry.reason]),
|
|
94
|
-
),
|
|
95
|
-
);
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
function renderCapabilities(capabilities: CapabilityConfig[]): string {
|
|
99
|
-
return capabilities.length > 0
|
|
100
|
-
? renderTable(
|
|
101
|
-
["Capability"],
|
|
102
|
-
capabilities.map((capability) => [capability.name]),
|
|
103
|
-
)
|
|
104
|
-
: "No capabilities configured.";
|
|
105
|
-
}
|
|
106
|
-
|
|
107
84
|
function getSelectedToolChecks(config: {
|
|
108
85
|
capabilities?: CapabilityConfig[];
|
|
109
86
|
packageManagers?: string[];
|
|
@@ -139,76 +116,183 @@ function getSelectedToolChecks(config: {
|
|
|
139
116
|
return checks;
|
|
140
117
|
}
|
|
141
118
|
|
|
142
|
-
function
|
|
143
|
-
config:
|
|
144
|
-
capabilities?: CapabilityConfig[];
|
|
145
|
-
packageManagers?: string[];
|
|
146
|
-
},
|
|
119
|
+
function collectToolInstallations(
|
|
120
|
+
config: FrameworkConfig,
|
|
147
121
|
pathValue: string,
|
|
148
|
-
):
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
return
|
|
122
|
+
): DoctorToolInstallation[] {
|
|
123
|
+
return getSelectedToolChecks(config).map((check) => {
|
|
124
|
+
const resolved = resolveCommandOnPath(check.command, pathValue);
|
|
125
|
+
return {
|
|
126
|
+
tool: check.name,
|
|
127
|
+
source: check.source,
|
|
128
|
+
status: resolved ? "ok" : "missing",
|
|
129
|
+
path: resolved ?? null,
|
|
130
|
+
};
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Only the engines key matching the running distribution produces a
|
|
135
|
+
// diagnostic; foreign distribution keys are ignored entirely.
|
|
136
|
+
function collectEngineRequirement(
|
|
137
|
+
config: FrameworkConfig,
|
|
138
|
+
): DoctorReport["engineRequirement"] | undefined {
|
|
139
|
+
const range = config.engines?.[FRAMEWORK_NAME];
|
|
140
|
+
if (!range) return undefined;
|
|
141
|
+
const currentVersion = getActiveDistribution().config.version;
|
|
142
|
+
const result = checkEngineRequirement(config, FRAMEWORK_NAME, currentVersion);
|
|
143
|
+
return {
|
|
144
|
+
range,
|
|
145
|
+
ok: result.ok,
|
|
146
|
+
detail: result.ok
|
|
147
|
+
? `engines.${FRAMEWORK_NAME}: satisfied (requires ${range}, running ${currentVersion})`
|
|
148
|
+
: `engines.${FRAMEWORK_NAME}: ${result.reason}`,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async function collectHubMember(
|
|
153
|
+
hubPath: string,
|
|
154
|
+
member: HubMember,
|
|
155
|
+
gitHead: (memberPath: string) => string | null,
|
|
156
|
+
): Promise<DoctorHubMember> {
|
|
157
|
+
const memberPath = path.resolve(hubPath, member.path);
|
|
158
|
+
const base = {
|
|
159
|
+
id: member.id,
|
|
160
|
+
source: member.source.kind,
|
|
161
|
+
path: memberPath,
|
|
162
|
+
materializedCommit: member.materializedCommit ?? null,
|
|
163
|
+
};
|
|
164
|
+
if (!(await pathIsDirectory(memberPath))) {
|
|
165
|
+
return { ...base, exists: false, commitStatus: "missing", actualCommit: null };
|
|
166
|
+
}
|
|
167
|
+
if (!member.materializedCommit) {
|
|
168
|
+
return { ...base, exists: true, commitStatus: "ok", actualCommit: null };
|
|
152
169
|
}
|
|
170
|
+
const actualCommit = gitHead(memberPath);
|
|
171
|
+
return {
|
|
172
|
+
...base,
|
|
173
|
+
exists: true,
|
|
174
|
+
commitStatus:
|
|
175
|
+
actualCommit === null
|
|
176
|
+
? "unknown"
|
|
177
|
+
: actualCommit === member.materializedCommit
|
|
178
|
+
? "ok"
|
|
179
|
+
: "drifted",
|
|
180
|
+
actualCommit,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
153
183
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
184
|
+
async function collectWorkingSections(
|
|
185
|
+
report: DoctorReport,
|
|
186
|
+
root: RootContext,
|
|
187
|
+
pathValue: string,
|
|
188
|
+
): Promise<void> {
|
|
189
|
+
if (!root.rootPath || !root.repositoryId || !root.config) {
|
|
190
|
+
report.policyError = "No repository ID was provided for the resolved companion.";
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
report.policy = { allowedAgents: root.config.allowedAgents ?? [] };
|
|
195
|
+
collectConfiguredRootSections(report, root.config, pathValue);
|
|
161
196
|
}
|
|
162
197
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
198
|
+
function collectConfiguredRootSections(
|
|
199
|
+
report: DoctorReport,
|
|
200
|
+
config: FrameworkConfig,
|
|
201
|
+
pathValue: string,
|
|
202
|
+
): void {
|
|
203
|
+
report.capabilities = (config.capabilities ?? []).map((capability) => capability.name);
|
|
204
|
+
report.toolInstallations = collectToolInstallations(config, pathValue);
|
|
205
|
+
report.requiredPluginDrift = getRequiredPluginDrift(config);
|
|
206
|
+
report.engineRequirement = collectEngineRequirement(config);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export async function collectDoctorReport(deps: DoctorDeps = {}): Promise<DoctorReport> {
|
|
210
|
+
const cwd = path.resolve(deps.cwd ?? process.cwd());
|
|
171
211
|
const pathValue = deps.pathValue ?? process.env.PATH ?? "";
|
|
172
|
-
const
|
|
173
|
-
const
|
|
174
|
-
const
|
|
212
|
+
const gitHead = deps.gitHead ?? defaultGitHead;
|
|
213
|
+
const root = await resolveRootContext(cwd, deps);
|
|
214
|
+
const kind = doctorKind(root);
|
|
175
215
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
216
|
+
const report: DoctorReport = {
|
|
217
|
+
kind,
|
|
218
|
+
cwd,
|
|
219
|
+
companionPath: root.rootPath ?? null,
|
|
220
|
+
repositoryId: root.repositoryId ?? null,
|
|
221
|
+
multipleCompanions:
|
|
222
|
+
root.resolution.ambiguousMatches.length > 1 ? root.resolution.ambiguousMatches : [],
|
|
223
|
+
resolutionFailures: root.resolution.failures,
|
|
224
|
+
};
|
|
183
225
|
|
|
184
|
-
if (
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
} else if (
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
226
|
+
if (kind === "working") {
|
|
227
|
+
await collectWorkingSections(report, root, pathValue);
|
|
228
|
+
} else if (kind === "companion" && root.config) {
|
|
229
|
+
collectConfiguredRootSections(report, root.config, pathValue);
|
|
230
|
+
} else if (kind === "hub" && root.rootPath && root.config) {
|
|
231
|
+
report.hub = {
|
|
232
|
+
members: await Promise.all(
|
|
233
|
+
(root.config.hub?.companions ?? []).map((member) =>
|
|
234
|
+
collectHubMember(root.rootPath!, member, gitHead),
|
|
235
|
+
),
|
|
236
|
+
),
|
|
237
|
+
};
|
|
238
|
+
report.requiredPluginDrift = getRequiredPluginDrift(root.config);
|
|
239
|
+
report.engineRequirement = collectEngineRequirement(root.config);
|
|
197
240
|
}
|
|
198
241
|
|
|
242
|
+
return report;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function renderList(items: string[], empty: string): string {
|
|
246
|
+
return items.length > 0 ? items.map((item) => `- ${item}`).join("\n") : empty;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function renderCapabilities(capabilities: string[]): string {
|
|
250
|
+
return capabilities.length > 0
|
|
251
|
+
? renderTable(
|
|
252
|
+
["Capability"],
|
|
253
|
+
capabilities.map((capability) => [capability]),
|
|
254
|
+
)
|
|
255
|
+
: "No capabilities configured.";
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function renderToolInstallations(installations: DoctorToolInstallation[]): string {
|
|
259
|
+
if (installations.length === 0) {
|
|
260
|
+
return "No selected capabilities or package managers require command checks.";
|
|
261
|
+
}
|
|
262
|
+
return renderTable(
|
|
263
|
+
["Tool", "Source", "Status", "Path"],
|
|
264
|
+
installations.map((item) => [item.tool, item.source, item.status, item.path ?? ""]),
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function renderHubMembers(members: DoctorHubMember[]): string {
|
|
269
|
+
if (members.length === 0) return "No hub members configured.";
|
|
270
|
+
return renderTable(
|
|
271
|
+
["Member", "Source", "Path", "Exists", "Commit"],
|
|
272
|
+
members.map((member) => [
|
|
273
|
+
member.id,
|
|
274
|
+
member.source,
|
|
275
|
+
member.path,
|
|
276
|
+
member.exists ? "yes" : "no",
|
|
277
|
+
member.commitStatus,
|
|
278
|
+
]),
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function renderHumanReport(report: DoctorReport): void {
|
|
199
283
|
console.log("Mate Doctor");
|
|
200
284
|
console.log("");
|
|
201
285
|
printSection(
|
|
202
286
|
"Context",
|
|
203
287
|
renderKeyValueTable([
|
|
204
|
-
["State",
|
|
205
|
-
["Current Working Directory", cwd],
|
|
206
|
-
["Companion Path", companionPath
|
|
207
|
-
["Repository ID", repositoryId
|
|
288
|
+
["State", report.kind],
|
|
289
|
+
["Current Working Directory", report.cwd],
|
|
290
|
+
["Companion Path", report.companionPath ?? "none"],
|
|
291
|
+
["Repository ID", report.repositoryId ?? "none"],
|
|
208
292
|
]),
|
|
209
293
|
);
|
|
210
294
|
|
|
211
|
-
if (
|
|
295
|
+
if (report.multipleCompanions.length > 1) {
|
|
212
296
|
printSection(
|
|
213
297
|
"Multiple Companions",
|
|
214
298
|
[
|
|
@@ -218,63 +302,63 @@ export async function runDoctorCommand(_argv: string[] = [], deps: DoctorDeps =
|
|
|
218
302
|
"",
|
|
219
303
|
renderTable(
|
|
220
304
|
["Companion Path", "Repository ID"],
|
|
221
|
-
|
|
305
|
+
report.multipleCompanions.map((match) => [match.companionPath, match.repositoryId]),
|
|
222
306
|
),
|
|
223
307
|
].join("\n"),
|
|
224
308
|
);
|
|
225
309
|
}
|
|
226
310
|
|
|
227
|
-
if (
|
|
228
|
-
const { configStore, workingRepoStore } = storesForCompanion(companionPath);
|
|
229
|
-
const config = await configStore.load();
|
|
230
|
-
let resolvedPolicy;
|
|
231
|
-
|
|
232
|
-
if (repository) {
|
|
233
|
-
resolvedPolicy = resolvePolicyFromConfig(config, [repository], repository.id);
|
|
234
|
-
} else {
|
|
235
|
-
resolvedPolicy = await new CompanionStore(configStore, workingRepoStore).resolvePolicy(
|
|
236
|
-
repositoryId,
|
|
237
|
-
);
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
if (!repository) {
|
|
241
|
-
const working = await workingRepoStore.load();
|
|
242
|
-
repository = working.repos.find((repo) => repo.id === repositoryId) ?? null;
|
|
243
|
-
}
|
|
244
|
-
|
|
311
|
+
if (report.policy) {
|
|
245
312
|
printSection(
|
|
246
|
-
"
|
|
247
|
-
|
|
248
|
-
["Profile", repository?.profile ?? "unknown"],
|
|
249
|
-
["Allowed Agents", resolvedPolicy.allowedAgents.join(", ") || "none"],
|
|
250
|
-
]),
|
|
313
|
+
"Allowed Agents",
|
|
314
|
+
renderList(report.policy.allowedAgents, "No allowed agents configured."),
|
|
251
315
|
);
|
|
316
|
+
}
|
|
317
|
+
if (report.policyError) printSection("Policy", report.policyError);
|
|
318
|
+
if (report.capabilities) printSection("Capabilities", renderCapabilities(report.capabilities));
|
|
319
|
+
if (report.toolInstallations) {
|
|
320
|
+
printSection("Tool Installations", renderToolInstallations(report.toolInstallations));
|
|
321
|
+
}
|
|
322
|
+
if (report.hub) printSection("Hub Members", renderHubMembers(report.hub.members));
|
|
323
|
+
// Companions with no drift produce no section at all: required plugins are
|
|
324
|
+
// silent when healthy.
|
|
325
|
+
if (report.requiredPluginDrift && report.requiredPluginDrift.length > 0) {
|
|
252
326
|
printSection(
|
|
253
|
-
"
|
|
254
|
-
|
|
327
|
+
"Required Plugin Drift",
|
|
328
|
+
renderTable(
|
|
329
|
+
["Plugin", "Kind", "Issue"],
|
|
330
|
+
report.requiredPluginDrift.map((entry) => [entry.pluginId, entry.kind, entry.reason]),
|
|
331
|
+
),
|
|
255
332
|
);
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
printEngineRequirement(config);
|
|
260
|
-
} else if (state === "linked-working-repository") {
|
|
261
|
-
printSection("Policy", "No repository ID was provided for the resolved companion.");
|
|
262
|
-
} else if (companionPath) {
|
|
263
|
-
const { configStore } = storesForCompanion(companionPath);
|
|
264
|
-
const config = await configStore.load();
|
|
265
|
-
printSection("Capabilities", renderCapabilities(config.capabilities ?? []));
|
|
266
|
-
printSection("Tool Installations", renderToolInstallations(config, pathValue));
|
|
267
|
-
printRequiredPluginDrift(config);
|
|
268
|
-
printEngineRequirement(config);
|
|
333
|
+
}
|
|
334
|
+
if (report.engineRequirement) {
|
|
335
|
+
printSection("Version Requirement", report.engineRequirement.detail);
|
|
269
336
|
}
|
|
270
337
|
|
|
271
338
|
printSection(
|
|
272
339
|
"Resolution Failures",
|
|
273
|
-
|
|
340
|
+
report.resolutionFailures.length > 0
|
|
274
341
|
? renderTable(
|
|
275
342
|
["Companion Path", "Error"],
|
|
276
|
-
|
|
343
|
+
report.resolutionFailures.map((failure) => [failure.companionPath, failure.message]),
|
|
277
344
|
)
|
|
278
345
|
: "No companion resolution failures encountered.",
|
|
279
346
|
);
|
|
280
347
|
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* @command mate doctor
|
|
351
|
+
* @description Reports the current directory's root kind — `working` (linked
|
|
352
|
+
* working repository), `companion`, `hub`, or `core` (not linked) — with
|
|
353
|
+
* kind-specific diagnostics: policy and capabilities for working repos,
|
|
354
|
+
* capabilities and tool checks for companions, member health for hubs.
|
|
355
|
+
* `--json` emits the same report as one JSON document.
|
|
356
|
+
*/
|
|
357
|
+
export async function runDoctorCommand(argv: string[] = [], deps: DoctorDeps = {}): Promise<void> {
|
|
358
|
+
const report = await collectDoctorReport(deps);
|
|
359
|
+
if (argv.includes("--json")) {
|
|
360
|
+
console.log(JSON.stringify(report, null, 2));
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
renderHumanReport(report);
|
|
364
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { confirm } from "../confirm";
|
|
2
|
-
import {
|
|
2
|
+
import { FRAMEWORK_NAME } from "../../framework";
|
|
3
3
|
import {
|
|
4
4
|
buildInstallPlan,
|
|
5
5
|
inspectInstallPlan,
|
|
@@ -14,13 +14,15 @@ import {
|
|
|
14
14
|
installDeclaredPlugins,
|
|
15
15
|
type PluginInstallResult,
|
|
16
16
|
} from "../../tools/setup/dynamic-plugins/install";
|
|
17
|
+
import { registryConfigHint } from "../../tools/setup/dynamic-plugins/registry-hint";
|
|
17
18
|
|
|
18
19
|
export function reportPluginInstallResults(results: PluginInstallResult[]): void {
|
|
19
20
|
for (const result of results) {
|
|
20
21
|
if (result.status === "failed") {
|
|
21
22
|
process.stderr.write(
|
|
22
|
-
`${
|
|
23
|
+
`${FRAMEWORK_NAME}: plugin "${result.package}" failed to install: ${result.error ?? "unknown error"}\n`,
|
|
23
24
|
);
|
|
25
|
+
process.stderr.write(`${registryConfigHint(result.package)}\n`);
|
|
24
26
|
} else if (result.status === "installed") {
|
|
25
27
|
console.log(` installed plugin ${result.package}@${result.resolvedVersion}`);
|
|
26
28
|
}
|
|
@@ -54,10 +56,14 @@ export async function runInstallCommand(argv: string[], cwd = process.cwd()): Pr
|
|
|
54
56
|
process.exitCode = 1;
|
|
55
57
|
return false;
|
|
56
58
|
}
|
|
57
|
-
//
|
|
58
|
-
// before the plan is built, so one run goes install → load →
|
|
59
|
-
// own install requirements are part of the plan.
|
|
60
|
-
if (
|
|
59
|
+
// Root-declared plugins (companion or hub) install first and hydrate into
|
|
60
|
+
// the registry before the plan is built, so one run goes install → load →
|
|
61
|
+
// plan and their own install requirements are part of the plan.
|
|
62
|
+
if (
|
|
63
|
+
(context.kind === "companion" || context.kind === "hub") &&
|
|
64
|
+
context.companionPath &&
|
|
65
|
+
context.config.plugins?.length
|
|
66
|
+
) {
|
|
61
67
|
reportPluginInstallResults(
|
|
62
68
|
await installDeclaredPlugins(context.companionPath, context.config.plugins),
|
|
63
69
|
);
|
|
@@ -72,7 +78,7 @@ export async function runInstallCommand(argv: string[], cwd = process.cwd()): Pr
|
|
|
72
78
|
const ok = await confirm("Install the missing requirements? [y/N] ");
|
|
73
79
|
if (!ok) {
|
|
74
80
|
process.stderr.write(
|
|
75
|
-
`Installation declined. Run \`${
|
|
81
|
+
`Installation declined. Run \`${FRAMEWORK_NAME} install --yes\` to continue.\n`,
|
|
76
82
|
);
|
|
77
83
|
process.exitCode = 1;
|
|
78
84
|
return false;
|
|
@@ -83,7 +89,7 @@ export async function runInstallCommand(argv: string[], cwd = process.cwd()): Pr
|
|
|
83
89
|
printPlanText(plan);
|
|
84
90
|
if (!skipConfirm) {
|
|
85
91
|
process.stderr.write(
|
|
86
|
-
`${
|
|
92
|
+
`${FRAMEWORK_NAME}: installation requires confirmation in a TTY. Re-run with \`${FRAMEWORK_NAME} install --yes\`.\n`,
|
|
87
93
|
);
|
|
88
94
|
process.exitCode = 1;
|
|
89
95
|
return false;
|
|
@@ -103,11 +109,11 @@ export async function runInstallCommand(argv: string[], cwd = process.cwd()): Pr
|
|
|
103
109
|
if (!execution.ok) {
|
|
104
110
|
for (const result of execution.results.filter((item) => item.status === "failed")) {
|
|
105
111
|
process.stderr.write(
|
|
106
|
-
`${
|
|
112
|
+
`${FRAMEWORK_NAME}: ${result.id} failed: ${result.error ?? "unknown error"}\n`,
|
|
107
113
|
);
|
|
108
114
|
}
|
|
109
115
|
process.stderr.write(
|
|
110
|
-
`Installation is incomplete. Re-run \`${
|
|
116
|
+
`Installation is incomplete. Re-run \`${FRAMEWORK_NAME} install\` after remediation.\n`,
|
|
111
117
|
);
|
|
112
118
|
process.exitCode = 1;
|
|
113
119
|
return false;
|
|
@@ -118,11 +124,9 @@ export async function runInstallCommand(argv: string[], cwd = process.cwd()): Pr
|
|
|
118
124
|
await saveCompleteInstallState(plan, execution.results);
|
|
119
125
|
} catch (error) {
|
|
120
126
|
process.stderr.write(
|
|
121
|
-
`${
|
|
122
|
-
);
|
|
123
|
-
process.stderr.write(
|
|
124
|
-
`Installation is incomplete. Re-run \`${frameworkCommandName()} install\`.\n`,
|
|
127
|
+
`${FRAMEWORK_NAME}: installation completed but companion reconciliation failed: ${error instanceof Error ? error.message : String(error)}\n`,
|
|
125
128
|
);
|
|
129
|
+
process.stderr.write(`Installation is incomplete. Re-run \`${FRAMEWORK_NAME} install\`.\n`);
|
|
126
130
|
process.exitCode = 1;
|
|
127
131
|
return false;
|
|
128
132
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { FRAMEWORK_NAME } from "../../../framework";
|
|
2
2
|
import { FrameworkLauncher } from "../../../lib/orchestrator/launcher";
|
|
3
3
|
import {
|
|
4
4
|
LaunchPreflightError,
|
|
@@ -43,7 +43,7 @@ export function parseLaunchArgs(argv: string[]): ParsedLaunchArgs | null {
|
|
|
43
43
|
const agentArgs = separatorIndex >= 0 ? argv.slice(separatorIndex + 1) : [];
|
|
44
44
|
|
|
45
45
|
for (const arg of optionArgs) {
|
|
46
|
-
process.stderr.write(`${
|
|
46
|
+
process.stderr.write(`${FRAMEWORK_NAME}: unknown launch option: ${arg}\n`);
|
|
47
47
|
process.exitCode = 1;
|
|
48
48
|
return null;
|
|
49
49
|
}
|
|
@@ -109,7 +109,7 @@ export async function runLaunchToolCommand(
|
|
|
109
109
|
|
|
110
110
|
if (error instanceof ToolNotAllowedError) {
|
|
111
111
|
process.stderr.write(
|
|
112
|
-
`${
|
|
112
|
+
`${FRAMEWORK_NAME}: \`${tool}\` is disallowed by active repository policy.\n`,
|
|
113
113
|
);
|
|
114
114
|
process.exitCode = 1;
|
|
115
115
|
return;
|
|
@@ -117,7 +117,7 @@ export async function runLaunchToolCommand(
|
|
|
117
117
|
|
|
118
118
|
if (error instanceof RepositoryNotSelectedError) {
|
|
119
119
|
process.stderr.write(`${error.message}\n`);
|
|
120
|
-
process.stderr.write(`Run \`${
|
|
120
|
+
process.stderr.write(`Run \`${FRAMEWORK_NAME} companion link\` first.\n`);
|
|
121
121
|
process.exitCode = 1;
|
|
122
122
|
return;
|
|
123
123
|
}
|