@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
|
@@ -3,15 +3,20 @@ import fs from "node:fs/promises";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
|
|
5
5
|
import type { PluginDeclaration } from "../../../lib/orchestrator/types";
|
|
6
|
-
import {
|
|
7
|
-
import { PluginPinStore, type PluginPin } from "./pin-store";
|
|
6
|
+
import { dynamicPluginsWorkspaceRoot, pluginPackageRoot } from "./paths";
|
|
8
7
|
|
|
9
|
-
export type
|
|
10
|
-
|
|
8
|
+
export type NpmInstallRunner = (
|
|
9
|
+
workspaceRoot: string,
|
|
10
|
+
) => Promise<{ ok: boolean; detail?: string }> | { ok: boolean; detail?: string };
|
|
11
|
+
|
|
12
|
+
export type NpmUpdateRunner = (
|
|
13
|
+
workspaceRoot: string,
|
|
14
|
+
packages: string[],
|
|
11
15
|
) => Promise<{ ok: boolean; detail?: string }> | { ok: boolean; detail?: string };
|
|
12
16
|
|
|
13
17
|
export interface PluginInstallDeps {
|
|
14
|
-
|
|
18
|
+
runNpmInstall?: NpmInstallRunner;
|
|
19
|
+
runNpmUpdate?: NpmUpdateRunner;
|
|
15
20
|
}
|
|
16
21
|
|
|
17
22
|
export interface PluginInstallResult {
|
|
@@ -21,16 +26,38 @@ export interface PluginInstallResult {
|
|
|
21
26
|
error?: string;
|
|
22
27
|
}
|
|
23
28
|
|
|
24
|
-
/** Runs `
|
|
25
|
-
function
|
|
26
|
-
const result = spawnSync("
|
|
29
|
+
/** Runs `npm install` for the shared plugin workspace, honoring the user's ambient registry/auth config. */
|
|
30
|
+
function defaultNpmInstall(workspaceRoot: string): { ok: boolean; detail?: string } {
|
|
31
|
+
const result = spawnSync("npm", ["install", "--no-audit", "--no-fund", "--silent"], {
|
|
32
|
+
cwd: workspaceRoot,
|
|
33
|
+
encoding: "utf8",
|
|
34
|
+
});
|
|
27
35
|
if (result.error || result.status !== 0) {
|
|
28
36
|
return {
|
|
29
37
|
ok: false,
|
|
30
38
|
detail:
|
|
31
39
|
result.error?.message ??
|
|
32
40
|
result.stderr?.trim() ??
|
|
33
|
-
`
|
|
41
|
+
`npm install exited with ${result.status}`,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
return { ok: true };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Runs `npm update <pkg...>` to force re-resolution of `latest`-declared plugins every run. */
|
|
48
|
+
function defaultNpmUpdate(
|
|
49
|
+
workspaceRoot: string,
|
|
50
|
+
packages: string[],
|
|
51
|
+
): { ok: boolean; detail?: string } {
|
|
52
|
+
const result = spawnSync("npm", ["update", "--no-audit", "--no-fund", "--silent", ...packages], {
|
|
53
|
+
cwd: workspaceRoot,
|
|
54
|
+
encoding: "utf8",
|
|
55
|
+
});
|
|
56
|
+
if (result.error || result.status !== 0) {
|
|
57
|
+
return {
|
|
58
|
+
ok: false,
|
|
59
|
+
detail:
|
|
60
|
+
result.error?.message ?? result.stderr?.trim() ?? `npm update exited with ${result.status}`,
|
|
34
61
|
};
|
|
35
62
|
}
|
|
36
63
|
return { ok: true };
|
|
@@ -47,125 +74,113 @@ async function readInstalledVersion(packageRoot: string): Promise<string | null>
|
|
|
47
74
|
}
|
|
48
75
|
}
|
|
49
76
|
|
|
50
|
-
|
|
51
|
-
* Best-effort integrity extraction from bun's text lockfile. The lockfile is
|
|
52
|
-
* JSONC (trailing commas); package entries are tuples whose last string is
|
|
53
|
-
* the registry integrity hash. Absence is tolerated per spec.
|
|
54
|
-
*/
|
|
55
|
-
async function readIntegrityFromBunLock(
|
|
56
|
-
installDir: string,
|
|
57
|
-
packageName: string,
|
|
58
|
-
): Promise<string | undefined> {
|
|
77
|
+
async function readWorkspaceDependencies(workspaceRoot: string): Promise<Record<string, string>> {
|
|
59
78
|
try {
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
};
|
|
64
|
-
const entry = parsed.packages?.[packageName];
|
|
65
|
-
if (!Array.isArray(entry)) return undefined;
|
|
66
|
-
return entry.find(
|
|
67
|
-
(element): element is string => typeof element === "string" && element.startsWith("sha"),
|
|
68
|
-
);
|
|
79
|
+
const manifest = JSON.parse(
|
|
80
|
+
await fs.readFile(path.join(workspaceRoot, "package.json"), "utf8"),
|
|
81
|
+
) as { dependencies?: Record<string, string> };
|
|
82
|
+
return manifest.dependencies ?? {};
|
|
69
83
|
} catch {
|
|
70
|
-
return
|
|
84
|
+
return {};
|
|
71
85
|
}
|
|
72
86
|
}
|
|
73
87
|
|
|
88
|
+
async function writeWorkspaceManifest(
|
|
89
|
+
workspaceRoot: string,
|
|
90
|
+
dependencies: Record<string, string>,
|
|
91
|
+
): Promise<void> {
|
|
92
|
+
await fs.mkdir(workspaceRoot, { recursive: true });
|
|
93
|
+
await fs.writeFile(
|
|
94
|
+
path.join(workspaceRoot, "package.json"),
|
|
95
|
+
JSON.stringify({ private: true, dependencies }, null, 2) + "\n",
|
|
96
|
+
"utf8",
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
74
100
|
/**
|
|
75
|
-
* Installs
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
101
|
+
* Installs every declared plugin into one shared workspace at
|
|
102
|
+
* `.mate/plugins/`. Builds a single `dependencies` map (package
|
|
103
|
+
* → declared version, sorted by name) and diffs it against the workspace's
|
|
104
|
+
* current `package.json` plus each package's actual installed presence; an
|
|
105
|
+
* identical, fully-installed map with no `latest` declaration skips the
|
|
106
|
+
* install entirely. Otherwise the map is written and `npm install` runs once
|
|
107
|
+
* for the whole workspace. `latest`-declared plugins additionally get an
|
|
108
|
+
* `npm update` every run, regardless of whether the map changed, so they
|
|
109
|
+
* re-resolve on every run. The shared, committed `package-lock.json` is the
|
|
110
|
+
* sole reproducibility record; nothing else pins versions. Private
|
|
111
|
+
* registries are never Mate's concern: installs run through the operator's
|
|
112
|
+
* own ambient npm config, or a project-local, gitignored `.npmrc` dropped
|
|
113
|
+
* next to the workspace's `package.json`.
|
|
80
114
|
*/
|
|
81
115
|
export async function installDeclaredPlugins(
|
|
82
116
|
companionPath: string,
|
|
83
117
|
declarations: PluginDeclaration[],
|
|
84
118
|
deps: PluginInstallDeps = {},
|
|
85
119
|
): Promise<PluginInstallResult[]> {
|
|
86
|
-
const
|
|
87
|
-
const
|
|
88
|
-
const
|
|
89
|
-
const nextPins: PluginPin[] = [];
|
|
90
|
-
const results: PluginInstallResult[] = [];
|
|
91
|
-
|
|
92
|
-
for (const declaration of declarations) {
|
|
93
|
-
const pin = previousPins.find((candidate) => candidate.package === declaration.package);
|
|
94
|
-
const installDir = pluginInstallDir(companionPath, declaration.package);
|
|
95
|
-
const packageRoot = pluginPackageRoot(companionPath, declaration.package);
|
|
96
|
-
// oxlint-disable-next-line no-await-in-loop -- installs mutate a shared pin file sequentially
|
|
97
|
-
const installedVersion = await readInstalledVersion(packageRoot);
|
|
98
|
-
|
|
99
|
-
const pinMatches =
|
|
100
|
-
declaration.version !== "latest" &&
|
|
101
|
-
pin !== undefined &&
|
|
102
|
-
pin.declaredVersion === declaration.version &&
|
|
103
|
-
installedVersion === pin.resolvedVersion;
|
|
104
|
-
if (pinMatches) {
|
|
105
|
-
nextPins.push(pin);
|
|
106
|
-
results.push({
|
|
107
|
-
package: declaration.package,
|
|
108
|
-
status: "unchanged",
|
|
109
|
-
resolvedVersion: pin.resolvedVersion,
|
|
110
|
-
});
|
|
111
|
-
continue;
|
|
112
|
-
}
|
|
120
|
+
const runNpmInstall = deps.runNpmInstall ?? defaultNpmInstall;
|
|
121
|
+
const runNpmUpdate = deps.runNpmUpdate ?? defaultNpmUpdate;
|
|
122
|
+
const workspaceRoot = dynamicPluginsWorkspaceRoot(companionPath);
|
|
113
123
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
124
|
+
const sorted = declarations.toSorted((a, b) => a.package.localeCompare(b.package));
|
|
125
|
+
const desired: Record<string, string> = {};
|
|
126
|
+
for (const declaration of sorted) desired[declaration.package] = declaration.version;
|
|
127
|
+
|
|
128
|
+
const [current, installedBefore] = await Promise.all([
|
|
129
|
+
readWorkspaceDependencies(workspaceRoot),
|
|
130
|
+
Promise.all(
|
|
131
|
+
sorted.map((declaration) =>
|
|
132
|
+
readInstalledVersion(pluginPackageRoot(companionPath, declaration.package)),
|
|
133
|
+
),
|
|
134
|
+
),
|
|
135
|
+
]);
|
|
136
|
+
|
|
137
|
+
const latestPackages = sorted
|
|
138
|
+
.filter((declaration) => declaration.version === "latest")
|
|
139
|
+
.map((declaration) => declaration.package);
|
|
140
|
+
const manifestMatches = JSON.stringify(desired) === JSON.stringify(current);
|
|
141
|
+
const allInstalled = installedBefore.every((version) => version !== null);
|
|
142
|
+
const unchanged = latestPackages.length === 0 && manifestMatches && allInstalled;
|
|
143
|
+
|
|
144
|
+
if (unchanged) {
|
|
145
|
+
return sorted.map((declaration, index) => ({
|
|
146
|
+
package: declaration.package,
|
|
147
|
+
status: "unchanged",
|
|
148
|
+
resolvedVersion: installedBefore[index] ?? undefined,
|
|
149
|
+
}));
|
|
126
150
|
}
|
|
127
151
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
152
|
+
await writeWorkspaceManifest(workspaceRoot, desired);
|
|
153
|
+
const installOutcome = await runNpmInstall(workspaceRoot);
|
|
154
|
+
const installFailure = installOutcome.ok
|
|
155
|
+
? undefined
|
|
156
|
+
: (installOutcome.detail ?? "npm install failed");
|
|
157
|
+
|
|
158
|
+
let updateFailure: string | undefined;
|
|
159
|
+
if (latestPackages.length > 0) {
|
|
160
|
+
const updateOutcome = await runNpmUpdate(workspaceRoot, latestPackages);
|
|
161
|
+
updateFailure = updateOutcome.ok ? undefined : (updateOutcome.detail ?? "npm update failed");
|
|
131
162
|
}
|
|
132
|
-
return results;
|
|
133
|
-
}
|
|
134
163
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
await fs.writeFile(
|
|
147
|
-
path.join(installDir, "package.json"),
|
|
148
|
-
JSON.stringify(
|
|
149
|
-
{ private: true, dependencies: { [declaration.package]: declaration.version } },
|
|
150
|
-
null,
|
|
151
|
-
2,
|
|
152
|
-
) + "\n",
|
|
153
|
-
"utf8",
|
|
154
|
-
);
|
|
155
|
-
const outcome = await runBunInstall(installDir);
|
|
156
|
-
if (!outcome.ok) {
|
|
157
|
-
throw new Error(outcome.detail ?? "bun install failed");
|
|
164
|
+
const resolvedVersions = await Promise.all(
|
|
165
|
+
sorted.map((declaration) =>
|
|
166
|
+
readInstalledVersion(pluginPackageRoot(companionPath, declaration.package)),
|
|
167
|
+
),
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
return sorted.map((declaration, index) => {
|
|
171
|
+
const isLatest = declaration.version === "latest";
|
|
172
|
+
const failure = installFailure ?? (isLatest ? updateFailure : undefined);
|
|
173
|
+
if (failure) {
|
|
174
|
+
return { package: declaration.package, status: "failed", error: failure };
|
|
158
175
|
}
|
|
159
|
-
const resolvedVersion =
|
|
176
|
+
const resolvedVersion = resolvedVersions[index];
|
|
160
177
|
if (!resolvedVersion) {
|
|
161
|
-
|
|
178
|
+
return {
|
|
179
|
+
package: declaration.package,
|
|
180
|
+
status: "failed",
|
|
181
|
+
error: `installed tree is missing ${declaration.package}/package.json`,
|
|
182
|
+
};
|
|
162
183
|
}
|
|
163
184
|
return { package: declaration.package, status: "installed", resolvedVersion };
|
|
164
|
-
}
|
|
165
|
-
return {
|
|
166
|
-
package: declaration.package,
|
|
167
|
-
status: "failed",
|
|
168
|
-
error: error instanceof Error ? error.message : String(error),
|
|
169
|
-
};
|
|
170
|
-
}
|
|
185
|
+
});
|
|
171
186
|
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
|
+
import { createRequire } from "node:module";
|
|
2
3
|
import path from "node:path";
|
|
3
4
|
import { pathToFileURL } from "node:url";
|
|
4
5
|
|
|
5
|
-
import { FRAMEWORK_NAME
|
|
6
|
+
import { FRAMEWORK_NAME } from "../../../framework";
|
|
6
7
|
import type { PluginDeclaration } from "../../../lib/orchestrator/types";
|
|
7
8
|
import type { Plugin } from "../plugin";
|
|
8
9
|
import { resolveEffectivePluginConfig } from "./config-resolution";
|
|
@@ -12,14 +13,9 @@ import {
|
|
|
12
13
|
type CreatePlugin,
|
|
13
14
|
type PluginHost,
|
|
14
15
|
} from "./host";
|
|
15
|
-
import { pluginPackageRoot } from "./paths";
|
|
16
|
-
import type { PluginPin } from "./pin-store";
|
|
16
|
+
import { dynamicPluginsWorkspaceRoot, pluginPackageRoot } from "./paths";
|
|
17
17
|
|
|
18
18
|
interface PluginManifest {
|
|
19
|
-
version?: string;
|
|
20
|
-
main?: string;
|
|
21
|
-
module?: string;
|
|
22
|
-
exports?: unknown;
|
|
23
19
|
mate?: { pluginApiVersion?: unknown };
|
|
24
20
|
}
|
|
25
21
|
|
|
@@ -35,38 +31,17 @@ function isRecord(value: unknown): value is Record<string, unknown> {
|
|
|
35
31
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
36
32
|
}
|
|
37
33
|
|
|
38
|
-
function commandName(): string {
|
|
39
|
-
try {
|
|
40
|
-
return frameworkCommandName();
|
|
41
|
-
} catch {
|
|
42
|
-
return FRAMEWORK_NAME;
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function resolveEntryFromExports(exportsField: unknown): string | undefined {
|
|
47
|
-
if (typeof exportsField === "string") return exportsField;
|
|
48
|
-
if (!isRecord(exportsField)) return undefined;
|
|
49
|
-
const dot = "." in exportsField ? exportsField["."] : exportsField;
|
|
50
|
-
if (typeof dot === "string") return dot;
|
|
51
|
-
if (!isRecord(dot)) return undefined;
|
|
52
|
-
for (const condition of ["bun", "import", "default", "require"]) {
|
|
53
|
-
const candidate = dot[condition];
|
|
54
|
-
if (typeof candidate === "string") return candidate;
|
|
55
|
-
if (isRecord(candidate) && typeof candidate.default === "string") return candidate.default;
|
|
56
|
-
}
|
|
57
|
-
return undefined;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
34
|
/**
|
|
61
|
-
* Loads one declared plugin from
|
|
62
|
-
* version gate before import,
|
|
63
|
-
*
|
|
64
|
-
*
|
|
35
|
+
* Loads one declared plugin from the shared workspace: installed-presence
|
|
36
|
+
* check, API version gate before import, entry-point resolution via the
|
|
37
|
+
* shared workspace's own module resolver, dynamic import, factory
|
|
38
|
+
* resolution, effective config, factory invocation. Every failure class
|
|
39
|
+
* returns a single warning instead of throwing, so one broken plugin never
|
|
40
|
+
* takes down the CLI.
|
|
65
41
|
*/
|
|
66
42
|
export async function loadDynamicPlugin(
|
|
67
43
|
companionPath: string,
|
|
68
44
|
declaration: PluginDeclaration,
|
|
69
|
-
pins: PluginPin[],
|
|
70
45
|
deps: DynamicPluginLoadDeps = {},
|
|
71
46
|
): Promise<DynamicPluginLoadResult> {
|
|
72
47
|
const name = declaration.package;
|
|
@@ -80,27 +55,7 @@ export async function loadDynamicPlugin(
|
|
|
80
55
|
} catch {
|
|
81
56
|
return {
|
|
82
57
|
ok: false,
|
|
83
|
-
warning: `plugin "${name}" is not installed; run \`${
|
|
84
|
-
};
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
const pin = pins.find((candidate) => candidate.package === name);
|
|
88
|
-
if (!pin) {
|
|
89
|
-
return {
|
|
90
|
-
ok: false,
|
|
91
|
-
warning: `plugin "${name}" has no recorded pin; run \`${commandName()} install\`.`,
|
|
92
|
-
};
|
|
93
|
-
}
|
|
94
|
-
if (pin.declaredVersion !== declaration.version) {
|
|
95
|
-
return {
|
|
96
|
-
ok: false,
|
|
97
|
-
warning: `plugin "${name}" declares version "${declaration.version}" but was pinned from "${pin.declaredVersion}"; run \`${commandName()} install\`.`,
|
|
98
|
-
};
|
|
99
|
-
}
|
|
100
|
-
if (manifest.version !== pin.resolvedVersion) {
|
|
101
|
-
return {
|
|
102
|
-
ok: false,
|
|
103
|
-
warning: `plugin "${name}" has version ${manifest.version ?? "unknown"} installed but ${pin.resolvedVersion} pinned; run \`${commandName()} install\`.`,
|
|
58
|
+
warning: `plugin "${name}" is not installed; run \`${FRAMEWORK_NAME} install\`.`,
|
|
104
59
|
};
|
|
105
60
|
}
|
|
106
61
|
|
|
@@ -121,9 +76,19 @@ export async function loadDynamicPlugin(
|
|
|
121
76
|
};
|
|
122
77
|
}
|
|
123
78
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
79
|
+
let entryPath: string;
|
|
80
|
+
try {
|
|
81
|
+
const workspaceRequire = createRequire(
|
|
82
|
+
pathToFileURL(path.join(dynamicPluginsWorkspaceRoot(companionPath), "package.json")),
|
|
83
|
+
);
|
|
84
|
+
entryPath = workspaceRequire.resolve(name);
|
|
85
|
+
} catch (error) {
|
|
86
|
+
return {
|
|
87
|
+
ok: false,
|
|
88
|
+
warning: `plugin "${name}" could not be resolved: ${error instanceof Error ? error.message : String(error)}`,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
127
92
|
const importModule =
|
|
128
93
|
deps.importModule ??
|
|
129
94
|
((specifier: string) => import(specifier) as Promise<Record<string, unknown>>);
|
|
@@ -2,34 +2,20 @@ import path from "node:path";
|
|
|
2
2
|
|
|
3
3
|
import { FRAMEWORK_NAME } from "../../../framework";
|
|
4
4
|
|
|
5
|
-
/**
|
|
6
|
-
export function
|
|
7
|
-
return
|
|
5
|
+
/** Shared workspace root: one package.json + node_modules for every declared plugin. */
|
|
6
|
+
export function dynamicPluginsWorkspaceRoot(companionPath: string): string {
|
|
7
|
+
return path.join(companionPath, `.${FRAMEWORK_NAME}`, "plugins");
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
return path.join(companionPath, `.${FRAMEWORK_NAME}`, "dependencies", "plugins");
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
/** Per-plugin install workspace holding a private package.json and node_modules. */
|
|
15
|
-
export function pluginInstallDir(companionPath: string, packageName: string): string {
|
|
16
|
-
return path.join(dynamicPluginsRoot(companionPath), sanitizePluginDirName(packageName));
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
/** Root of the installed plugin package itself. */
|
|
10
|
+
/** Root of one installed plugin package inside the shared workspace's node_modules. */
|
|
20
11
|
export function pluginPackageRoot(companionPath: string, packageName: string): string {
|
|
21
12
|
return path.join(
|
|
22
|
-
|
|
13
|
+
dynamicPluginsWorkspaceRoot(companionPath),
|
|
23
14
|
"node_modules",
|
|
24
15
|
...packageName.split("/"),
|
|
25
16
|
);
|
|
26
17
|
}
|
|
27
18
|
|
|
28
|
-
/** Committed pin file recording resolved plugin versions. */
|
|
29
|
-
export function pluginPinFilePath(companionPath: string): string {
|
|
30
|
-
return path.join(companionPath, `.${FRAMEWORK_NAME}`, "config", "plugins.lock.yaml");
|
|
31
|
-
}
|
|
32
|
-
|
|
33
19
|
/** Gitignored per-machine override file deep-merged over committed plugin config. */
|
|
34
20
|
export function pluginLocalOverridesPath(companionPath: string): string {
|
|
35
21
|
return path.join(companionPath, `.${FRAMEWORK_NAME}`, "config", "plugins.local.yaml");
|
|
@@ -37,3 +23,6 @@ export function pluginLocalOverridesPath(companionPath: string): string {
|
|
|
37
23
|
|
|
38
24
|
/** Companion-relative gitignore entry for the local override file. */
|
|
39
25
|
export const PLUGIN_LOCAL_OVERRIDES_GITIGNORE_ENTRY = `.${FRAMEWORK_NAME}/config/plugins.local.yaml`;
|
|
26
|
+
|
|
27
|
+
/** Companion-relative gitignore entry for the shared workspace's local, never-committed registry credentials. */
|
|
28
|
+
export const PLUGIN_WORKSPACE_NPMRC_GITIGNORE_ENTRY = `.${FRAMEWORK_NAME}/plugins/.npmrc`;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A generic, actionable hint printed on install failure — never guesses at
|
|
3
|
+
* the actual registry URL/host. Private registries stay a plain, standard
|
|
4
|
+
* npm operation: a one-time `npm config set --global`, or (since the shared
|
|
5
|
+
* plugin workspace is a real npm project) a project-local, gitignored
|
|
6
|
+
* `.npmrc` dropped directly in it — either is picked up by `npm install`
|
|
7
|
+
* with nothing Mate-injected.
|
|
8
|
+
*/
|
|
9
|
+
export function registryConfigHint(packageName: string): string {
|
|
10
|
+
const scope = packageName.startsWith("@") ? packageName.split("/")[0] : undefined;
|
|
11
|
+
return scope
|
|
12
|
+
? `If "${scope}" lives in a private registry, configure it once with:\n npm config set "${scope}:registry" "<registry-url>" --global\n npm config set "//<registry-host>/:_authToken" "<token>" --global`
|
|
13
|
+
: `If this package lives in a private registry, configure it once via \`npm config set\` (see the npm docs for scoped registries).`;
|
|
14
|
+
}
|
|
@@ -2,7 +2,15 @@ import { getActiveDistribution } from "../../distribution";
|
|
|
2
2
|
import { FRAMEWORK_NAME } from "../../framework";
|
|
3
3
|
import type { FrameworkConfig } from "../../lib/orchestrator/types";
|
|
4
4
|
import { collectHostingProviders, ContextServiceMediator } from "./context-services";
|
|
5
|
-
import type {
|
|
5
|
+
import type {
|
|
6
|
+
CapabilityContributionInput,
|
|
7
|
+
CapabilityPlugin,
|
|
8
|
+
Plugin,
|
|
9
|
+
PluginRegistration,
|
|
10
|
+
SetupContext,
|
|
11
|
+
} from "./plugin";
|
|
12
|
+
import { reconcileClaudeContributions } from "./providers/claude";
|
|
13
|
+
import { reconcileOpenCodeContributions } from "./providers/opencode";
|
|
6
14
|
import { normalizeRegistration, type NormalizedRegistration } from "./registry";
|
|
7
15
|
|
|
8
16
|
export interface SetupInstallationPlanAction {
|
|
@@ -151,5 +159,61 @@ export async function executeSetupInstallationPlan(
|
|
|
151
159
|
|
|
152
160
|
await mediator.finalize();
|
|
153
161
|
|
|
162
|
+
await reconcileCapabilityContributions(ctx, plugins, plan);
|
|
163
|
+
|
|
154
164
|
return { executedActions, skippedActions, warnings };
|
|
155
165
|
}
|
|
166
|
+
|
|
167
|
+
// The Runtime Surface of each active Agent Runtime, by runtime id.
|
|
168
|
+
const RUNTIME_SURFACE_RECONCILERS: Record<
|
|
169
|
+
string,
|
|
170
|
+
(ctx: SetupContext, inputs: CapabilityContributionInput[]) => Promise<void>
|
|
171
|
+
> = {
|
|
172
|
+
claude: reconcileClaudeContributions,
|
|
173
|
+
opencode: reconcileOpenCodeContributions,
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Reconcile declared Capability contributions through every active runtime's
|
|
178
|
+
* Runtime Surface (spec: plugin-engine). Runs after all plugin phases, so
|
|
179
|
+
* providers are always done first. Disabled capabilities participate with
|
|
180
|
+
* `enabled: false` and drive teardown of their managed entries.
|
|
181
|
+
*/
|
|
182
|
+
async function reconcileCapabilityContributions(
|
|
183
|
+
ctx: SetupContext,
|
|
184
|
+
plugins: Plugin[],
|
|
185
|
+
plan: SetupInstallationPlan,
|
|
186
|
+
): Promise<void> {
|
|
187
|
+
// The plan already resolved enablement (policy, saved selection, package
|
|
188
|
+
// manager requirements); mirror it instead of recomputing.
|
|
189
|
+
const enabledByPluginId = new Map(
|
|
190
|
+
plan.actions
|
|
191
|
+
.filter((action) => action.phase === "capability" && action.providerId === undefined)
|
|
192
|
+
.map((action) => [action.pluginId, action.action === "apply"]),
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
const inputsByRuntime = new Map<string, CapabilityContributionInput[]>();
|
|
196
|
+
for (const plugin of plugins) {
|
|
197
|
+
if (plugin.kind !== "capability") continue;
|
|
198
|
+
const capability = plugin as CapabilityPlugin;
|
|
199
|
+
if (!capability.getRuntimeContributions) continue;
|
|
200
|
+
const enabled = enabledByPluginId.get(capability.id) ?? false;
|
|
201
|
+
const byRuntime = capability.getRuntimeContributions(ctx);
|
|
202
|
+
for (const [runtimeId, contributions] of Object.entries(byRuntime)) {
|
|
203
|
+
if (!contributions) continue;
|
|
204
|
+
// Inactive runtimes still reconcile — with everything disabled — so a
|
|
205
|
+
// deselected runtime's managed entries (skill trees, guidance blocks)
|
|
206
|
+
// are torn down. The surfaces never create files for disabled inputs.
|
|
207
|
+
const runtimeActive = plan.activeProviders.includes(runtimeId);
|
|
208
|
+
const inputs = inputsByRuntime.get(runtimeId) ?? [];
|
|
209
|
+
inputs.push({ pluginId: capability.id, enabled: enabled && runtimeActive, contributions });
|
|
210
|
+
inputsByRuntime.set(runtimeId, inputs);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
for (const [runtimeId, inputs] of inputsByRuntime) {
|
|
215
|
+
const reconcile = RUNTIME_SURFACE_RECONCILERS[runtimeId];
|
|
216
|
+
if (!reconcile || inputs.length === 0) continue;
|
|
217
|
+
await reconcile(ctx, inputs);
|
|
218
|
+
}
|
|
219
|
+
}
|
package/src/tools/setup/mate.ts
CHANGED
|
@@ -1,13 +1,11 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
|
|
4
|
-
import { frameworkCommandName } from "../../framework";
|
|
5
4
|
import { pruneEmptyAncestors } from "./utils";
|
|
6
5
|
|
|
7
6
|
export const MATE_ARTIFACT_SKILLS = ["mate-artifact-finish"] as const;
|
|
8
7
|
export const MATE_SKILLS = ["mate-artifact-finish", "mate-create-report"] as const;
|
|
9
8
|
|
|
10
|
-
const MATE_COMMAND_PLACEHOLDER = "{{MATE_COMMAND}}";
|
|
11
9
|
const MATE_SKILLS_SOURCE = path.join(
|
|
12
10
|
import.meta.dirname,
|
|
13
11
|
"../../templates/capabilities/openspec-cap/mate-skills",
|
|
@@ -16,9 +14,9 @@ const MATE_SKILLS_SOURCE = path.join(
|
|
|
16
14
|
const MATE_CREATE_REPORT_SKILL = `---
|
|
17
15
|
name: mate-create-report
|
|
18
16
|
description: Create a browser-first Mate report from an explicit structured ReportDocument. Use when a skill needs to present supplied metrics, tables, statuses, metadata, or narrative text.
|
|
19
|
-
allowed-tools: Bash(
|
|
17
|
+
allowed-tools: Bash(mate:*)
|
|
20
18
|
license: MIT
|
|
21
|
-
compatibility: Requires the
|
|
19
|
+
compatibility: Requires the mate CLI and the OpenSpec capability enabled.
|
|
22
20
|
metadata:
|
|
23
21
|
author: mate
|
|
24
22
|
version: "1.0"
|
|
@@ -57,22 +55,14 @@ Supported section types are \`metadata\`, \`metrics\`, \`key-value\`, \`table\`,
|
|
|
57
55
|
## Invocation
|
|
58
56
|
|
|
59
57
|
1. Serialize the complete document to a temporary file or pipe it to stdin.
|
|
60
|
-
2. Run \`
|
|
58
|
+
2. Run \`mate report --input <file-or->\`.
|
|
61
59
|
3. Add \`--json\` when the caller needs normalized JSON instead of browser delivery.
|
|
62
60
|
|
|
63
61
|
The default path writes self-contained HTML to a unique OS temporary directory and opens it in the default browser. The report includes a visible "Print / Save as PDF" control that calls the browser's native print dialog. If HTML delivery fails, the CLI warns on stderr and emits the complete report document as JSON on stdout.
|
|
64
62
|
|
|
65
|
-
The built-in \`
|
|
63
|
+
The built-in \`mate report\` path collects Mate usage data and adapts it to the same contract and renderer.
|
|
66
64
|
`;
|
|
67
65
|
|
|
68
|
-
function currentMateCommand(): string {
|
|
69
|
-
try {
|
|
70
|
-
return frameworkCommandName();
|
|
71
|
-
} catch {
|
|
72
|
-
return "mate";
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
66
|
export async function deployMateSkillDir(src: string, dest: string): Promise<void> {
|
|
77
67
|
await fs.mkdir(dest, { recursive: true });
|
|
78
68
|
const entries = await fs.readdir(src, { withFileTypes: true });
|
|
@@ -83,31 +73,38 @@ export async function deployMateSkillDir(src: string, dest: string): Promise<voi
|
|
|
83
73
|
await deployMateSkillDir(srcPath, destPath);
|
|
84
74
|
continue;
|
|
85
75
|
}
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
76
|
+
await fs.copyFile(srcPath, destPath);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const DEFAULT_MATE_SKILLS_BUCKET = "agents";
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* A skill's source lives under a provider bucket so its folder name always
|
|
84
|
+
* matches its `name:` frontmatter: `<tool>/<skill>/` when that tool needs its
|
|
85
|
+
* own behavior (e.g. Claude Code always confirms before `mate artifact
|
|
86
|
+
* finish` pushes, since a push is a shared-state action), falling back to
|
|
87
|
+
* `agents/<skill>/` — the shared default every other tool (e.g. opencode,
|
|
88
|
+
* which pushes automatically) uses.
|
|
89
|
+
*/
|
|
90
|
+
async function resolveMateSkillSource(skill: string, tool: string): Promise<string> {
|
|
91
|
+
const providerDir = path.join(MATE_SKILLS_SOURCE, tool, skill);
|
|
92
|
+
try {
|
|
93
|
+
await fs.access(providerDir);
|
|
94
|
+
return providerDir;
|
|
95
|
+
} catch {
|
|
96
|
+
return path.join(MATE_SKILLS_SOURCE, DEFAULT_MATE_SKILLS_BUCKET, skill);
|
|
96
97
|
}
|
|
97
98
|
}
|
|
98
99
|
|
|
99
|
-
export async function applyMateSkills(skillsDir: string): Promise<void> {
|
|
100
|
+
export async function applyMateSkills(skillsDir: string, tool: string): Promise<void> {
|
|
100
101
|
for (const skill of MATE_SKILLS) {
|
|
101
102
|
const destination = path.join(skillsDir, skill);
|
|
102
103
|
if (skill === "mate-create-report") {
|
|
103
104
|
await fs.mkdir(destination, { recursive: true });
|
|
104
|
-
await fs.writeFile(
|
|
105
|
-
path.join(destination, "SKILL.md"),
|
|
106
|
-
MATE_CREATE_REPORT_SKILL.replaceAll(MATE_COMMAND_PLACEHOLDER, currentMateCommand()),
|
|
107
|
-
"utf8",
|
|
108
|
-
);
|
|
105
|
+
await fs.writeFile(path.join(destination, "SKILL.md"), MATE_CREATE_REPORT_SKILL, "utf8");
|
|
109
106
|
} else {
|
|
110
|
-
await deployMateSkillDir(
|
|
107
|
+
await deployMateSkillDir(await resolveMateSkillSource(skill, tool), destination);
|
|
111
108
|
}
|
|
112
109
|
}
|
|
113
110
|
}
|