@uniqbit/mate-core 0.15.3 → 0.15.4-canary.3
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/package.json +3 -2
- package/src/cli/commands/companion/hub.ts +99 -0
- package/src/cli/commands/companion/tui.ts +3 -1
- package/src/cli/commands/doctor.ts +3 -6
- package/src/cli/commands/install.ts +10 -10
- 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 +2 -2
- package/src/cli/commands/update.ts +9 -11
- package/src/cli/main.ts +25 -9
- package/src/cli/plugin-commands.ts +4 -4
- package/src/cli/usage.ts +7 -3
- package/src/distribution.ts +3 -5
- package/src/framework.ts +4 -36
- package/src/index.ts +4 -0
- package/src/lib/install.ts +1 -5
- package/src/lib/orchestrator/adapters/base.ts +3 -4
- package/src/lib/orchestrator/companion-hub.ts +374 -0
- package/src/lib/orchestrator/config-store.ts +72 -0
- package/src/lib/orchestrator/editor.ts +4 -20
- package/src/lib/orchestrator/framework-context.ts +119 -23
- package/src/lib/orchestrator/global-config-store.ts +1 -5
- package/src/lib/orchestrator/migration.ts +0 -23
- package/src/lib/orchestrator/setup-preflight.ts +4 -17
- package/src/lib/orchestrator/types.ts +26 -4
- package/src/lib/orchestrator/working-repo-store.ts +2 -2
- package/src/lib/update-checker.ts +5 -5
- package/src/playbooks/companion-guidance.ts +6 -6
- package/src/runtime/env.ts +0 -1
- package/src/templates/capabilities/openspec-cap/claude/hooks/mate-artifact-finish.sh +79 -3
- 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 +5 -5
- package/src/tools/setup/capabilities/openspec.ts +1 -1
- package/src/tools/setup/capabilities/tokensave.ts +4 -4
- 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/mate.ts +28 -31
- package/src/tools/setup/plugins/gitignore.ts +15 -8
- package/src/tools/setup/plugins/guidance.ts +1 -5
- package/src/tools/setup/providers/claude.ts +20 -8
- package/src/tools/setup/providers/opencode.ts +3 -4
- package/src/tools/setup.ts +2 -3
- package/src/tui.ts +5 -0
- package/src/tools/setup/dynamic-plugins/pin-store.ts +0 -30
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uniqbit/mate-core",
|
|
3
|
-
"version": "0.15.3",
|
|
3
|
+
"version": "0.15.4-canary.3",
|
|
4
4
|
"description": "Core framework and plugin APIs for Mate.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"files": [
|
|
@@ -14,7 +14,8 @@
|
|
|
14
14
|
".": "./src/index.ts",
|
|
15
15
|
"./plugins": "./src/plugins.ts",
|
|
16
16
|
"./runtime": "./src/runtime/index.ts",
|
|
17
|
-
"./opencode": "./src/opencode/index.ts"
|
|
17
|
+
"./opencode": "./src/opencode/index.ts",
|
|
18
|
+
"./tui": "./src/tui.ts"
|
|
18
19
|
},
|
|
19
20
|
"publishConfig": {
|
|
20
21
|
"access": "public",
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import readline from "node:readline/promises";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
addHubMember,
|
|
5
|
+
discoverHubSource,
|
|
6
|
+
initializeCompanionHub,
|
|
7
|
+
syncHub,
|
|
8
|
+
updateHubPlugins,
|
|
9
|
+
} from "../../../lib/orchestrator/companion-hub";
|
|
10
|
+
import { GlobalConfigStore } from "../../../lib/orchestrator/global-config-store";
|
|
11
|
+
|
|
12
|
+
function positionalArgs(argv: string[]): string[] {
|
|
13
|
+
const values: string[] = [];
|
|
14
|
+
const valueFlags = new Set(["id", "path", "url", "companion", "ref"]);
|
|
15
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
16
|
+
const value = argv[index]!;
|
|
17
|
+
if (!value.startsWith("--")) {
|
|
18
|
+
values.push(value);
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
if (valueFlags.has(value.slice(2))) index += 1;
|
|
22
|
+
}
|
|
23
|
+
return values;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function flagValue(argv: string[], name: string): string | undefined {
|
|
27
|
+
const index = argv.indexOf(`--${name}`);
|
|
28
|
+
return index >= 0 ? argv[index + 1] : undefined;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function selectRegisteredCompanion(): Promise<string> {
|
|
32
|
+
const companions = await new GlobalConfigStore().list();
|
|
33
|
+
if (companions.length === 0) throw new Error("No registered companions are available.");
|
|
34
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
35
|
+
throw new Error("hub add requires a source path or Git URL outside a TTY.");
|
|
36
|
+
}
|
|
37
|
+
const prompt = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
38
|
+
try {
|
|
39
|
+
console.log("Registered companions:");
|
|
40
|
+
companions.forEach((candidate, index) => console.log(` ${index + 1}. ${candidate}`));
|
|
41
|
+
const answer = await prompt.question("Select a companion: ");
|
|
42
|
+
const selected = companions[Number.parseInt(answer, 10) - 1];
|
|
43
|
+
if (!selected) throw new Error("Invalid companion selection.");
|
|
44
|
+
return selected;
|
|
45
|
+
} finally {
|
|
46
|
+
prompt.close();
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function runHubInit(argv: string[]): Promise<void> {
|
|
51
|
+
const folder = positionalArgs(argv)[0] ?? process.cwd();
|
|
52
|
+
const hubPath = await initializeCompanionHub(folder);
|
|
53
|
+
console.log(`Initialized companion hub: ${hubPath}`);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function runHubAdd(argv: string[]): Promise<void> {
|
|
57
|
+
const sourceArg =
|
|
58
|
+
positionalArgs(argv)[0] ?? flagValue(argv, "url") ?? flagValue(argv, "companion");
|
|
59
|
+
const source = discoverHubSource(sourceArg ?? (await selectRegisteredCompanion()));
|
|
60
|
+
const ref = flagValue(argv, "ref");
|
|
61
|
+
if (ref && source.kind === "git") source.ref = ref;
|
|
62
|
+
const member = await addHubMember(process.cwd(), source, {
|
|
63
|
+
id: flagValue(argv, "id"),
|
|
64
|
+
memberPath: flagValue(argv, "path"),
|
|
65
|
+
});
|
|
66
|
+
console.log(`Added hub member ${member.id}: ${member.path}`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function runHubSync(argv: string[]): Promise<void> {
|
|
70
|
+
const results = await syncHub(process.cwd());
|
|
71
|
+
const plugins = await updateHubPlugins(process.cwd());
|
|
72
|
+
if (argv.includes("--json")) {
|
|
73
|
+
console.log(JSON.stringify({ companions: results, plugins }));
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
for (const result of results) console.log(`${result.id}: ${result.status} (${result.message})`);
|
|
77
|
+
for (const plugin of plugins) {
|
|
78
|
+
const detail = plugin.status === "failed" ? `: ${plugin.error ?? "failed"}` : "";
|
|
79
|
+
console.log(`hub plugin ${plugin.package}: ${plugin.status}${detail}`);
|
|
80
|
+
}
|
|
81
|
+
if (plugins.some((plugin) => plugin.status === "failed")) process.exitCode = 1;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export async function runHubCommand(argv: string[]): Promise<void> {
|
|
85
|
+
const [subcommand, ...rest] = argv;
|
|
86
|
+
switch (subcommand) {
|
|
87
|
+
case "init":
|
|
88
|
+
await runHubInit(rest);
|
|
89
|
+
return;
|
|
90
|
+
case "add":
|
|
91
|
+
await runHubAdd(rest);
|
|
92
|
+
return;
|
|
93
|
+
case "sync":
|
|
94
|
+
await runHubSync(rest);
|
|
95
|
+
return;
|
|
96
|
+
default:
|
|
97
|
+
throw new Error("Usage: mate hub <init|add|sync>");
|
|
98
|
+
}
|
|
99
|
+
}
|
|
@@ -2,12 +2,14 @@ import { spawn } from "node:child_process";
|
|
|
2
2
|
import { accessSync } from "node:fs";
|
|
3
3
|
|
|
4
4
|
import { getActiveDistribution } from "../../../distribution";
|
|
5
|
+
import { FRAMEWORK_NAME } from "../../../framework";
|
|
5
6
|
import { resolveFrameworkContext } from "../../../lib/orchestrator/framework-context";
|
|
6
7
|
import { findRepoLocalLinkedRepository } from "../../../lib/orchestrator/repo-local-registry";
|
|
7
8
|
import { ensureUnambiguousCompanion } from "../launch/shared";
|
|
8
9
|
|
|
9
10
|
export function renderBanner(repoPath: string, companionPath: string): string {
|
|
10
|
-
const {
|
|
11
|
+
const { version } = getActiveDistribution().config;
|
|
12
|
+
const name = FRAMEWORK_NAME;
|
|
11
13
|
const lines = [`${name} v${version}`, "", `repo: ${repoPath}`, `${name}: ${companionPath}`];
|
|
12
14
|
const contentWidth = Math.max(...lines.map((l) => l.length));
|
|
13
15
|
const pad = (text: string) => `│ ${text.padEnd(contentWidth)} │`;
|
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
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
6
|
import { CompanionResolver } from "../../lib/orchestrator/companion-resolver";
|
|
7
7
|
import { CompanionStore, resolvePolicyFromConfig } from "../../lib/orchestrator/companion-store";
|
|
8
8
|
import { ConfigStore } from "../../lib/orchestrator/config-store";
|
|
9
9
|
import { checkEngineRequirement } from "../../lib/orchestrator/engine-guard";
|
|
10
10
|
import { GlobalConfigStore } from "../../lib/orchestrator/global-config-store";
|
|
11
|
-
import { migrateConfigDir } from "../../lib/orchestrator/migration";
|
|
12
11
|
import { findRepoLocalLinkedRepository } from "../../lib/orchestrator/repo-local-registry";
|
|
13
12
|
import type { CapabilityConfig, FrameworkConfig } from "../../lib/orchestrator/types";
|
|
14
13
|
import { WorkingRepoStore } from "../../lib/orchestrator/working-repo-store";
|
|
@@ -43,8 +42,6 @@ function storesForCompanion(companionPath: string): CompanionStores {
|
|
|
43
42
|
|
|
44
43
|
async function hasLocalCompanionConfig(cwd: string): Promise<boolean> {
|
|
45
44
|
const localDir = path.join(cwd, `.${FRAMEWORK_NAME}`);
|
|
46
|
-
const localLegacyDirs = frameworkConfig.legacyNames.map((name) => path.join(cwd, `.${name}`));
|
|
47
|
-
await migrateConfigDir(localDir, localLegacyDirs);
|
|
48
45
|
|
|
49
46
|
try {
|
|
50
47
|
await fs.access(path.join(localDir, "config", "framework.yaml"));
|
|
@@ -74,10 +71,10 @@ function renderEngineRequirement(
|
|
|
74
71
|
// diagnostic; foreign distribution keys are ignored entirely.
|
|
75
72
|
function printEngineRequirement(config: FrameworkConfig): void {
|
|
76
73
|
const distribution = getActiveDistribution().config;
|
|
77
|
-
if (!config.engines?.[
|
|
74
|
+
if (!config.engines?.[FRAMEWORK_NAME]) return;
|
|
78
75
|
printSection(
|
|
79
76
|
"Version Requirement",
|
|
80
|
-
renderEngineRequirement(config,
|
|
77
|
+
renderEngineRequirement(config, FRAMEWORK_NAME, distribution.version),
|
|
81
78
|
);
|
|
82
79
|
}
|
|
83
80
|
|
|
@@ -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
|
}
|
|
@@ -72,7 +74,7 @@ export async function runInstallCommand(argv: string[], cwd = process.cwd()): Pr
|
|
|
72
74
|
const ok = await confirm("Install the missing requirements? [y/N] ");
|
|
73
75
|
if (!ok) {
|
|
74
76
|
process.stderr.write(
|
|
75
|
-
`Installation declined. Run \`${
|
|
77
|
+
`Installation declined. Run \`${FRAMEWORK_NAME} install --yes\` to continue.\n`,
|
|
76
78
|
);
|
|
77
79
|
process.exitCode = 1;
|
|
78
80
|
return false;
|
|
@@ -83,7 +85,7 @@ export async function runInstallCommand(argv: string[], cwd = process.cwd()): Pr
|
|
|
83
85
|
printPlanText(plan);
|
|
84
86
|
if (!skipConfirm) {
|
|
85
87
|
process.stderr.write(
|
|
86
|
-
`${
|
|
88
|
+
`${FRAMEWORK_NAME}: installation requires confirmation in a TTY. Re-run with \`${FRAMEWORK_NAME} install --yes\`.\n`,
|
|
87
89
|
);
|
|
88
90
|
process.exitCode = 1;
|
|
89
91
|
return false;
|
|
@@ -103,11 +105,11 @@ export async function runInstallCommand(argv: string[], cwd = process.cwd()): Pr
|
|
|
103
105
|
if (!execution.ok) {
|
|
104
106
|
for (const result of execution.results.filter((item) => item.status === "failed")) {
|
|
105
107
|
process.stderr.write(
|
|
106
|
-
`${
|
|
108
|
+
`${FRAMEWORK_NAME}: ${result.id} failed: ${result.error ?? "unknown error"}\n`,
|
|
107
109
|
);
|
|
108
110
|
}
|
|
109
111
|
process.stderr.write(
|
|
110
|
-
`Installation is incomplete. Re-run \`${
|
|
112
|
+
`Installation is incomplete. Re-run \`${FRAMEWORK_NAME} install\` after remediation.\n`,
|
|
111
113
|
);
|
|
112
114
|
process.exitCode = 1;
|
|
113
115
|
return false;
|
|
@@ -118,11 +120,9 @@ export async function runInstallCommand(argv: string[], cwd = process.cwd()): Pr
|
|
|
118
120
|
await saveCompleteInstallState(plan, execution.results);
|
|
119
121
|
} catch (error) {
|
|
120
122
|
process.stderr.write(
|
|
121
|
-
`${
|
|
122
|
-
);
|
|
123
|
-
process.stderr.write(
|
|
124
|
-
`Installation is incomplete. Re-run \`${frameworkCommandName()} install\`.\n`,
|
|
123
|
+
`${FRAMEWORK_NAME}: installation completed but companion reconciliation failed: ${error instanceof Error ? error.message : String(error)}\n`,
|
|
125
124
|
);
|
|
125
|
+
process.stderr.write(`Installation is incomplete. Re-run \`${FRAMEWORK_NAME} install\`.\n`);
|
|
126
126
|
process.exitCode = 1;
|
|
127
127
|
return false;
|
|
128
128
|
}
|
|
@@ -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
|
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
|
|
3
|
+
import { FRAMEWORK_NAME } from "../../../framework";
|
|
4
|
+
import { resolveInstallContext } from "../../../lib/install";
|
|
5
|
+
import { ConfigStore } from "../../../lib/orchestrator/config-store";
|
|
6
|
+
import type { PluginDeclaration } from "../../../lib/orchestrator/types";
|
|
7
|
+
import { hydrateDynamicPlugins } from "../../../tools/setup/dynamic-plugins/hydrate";
|
|
8
|
+
import {
|
|
9
|
+
installDeclaredPlugins,
|
|
10
|
+
type PluginInstallDeps,
|
|
11
|
+
} from "../../../tools/setup/dynamic-plugins/install";
|
|
12
|
+
import { reportPluginInstallResults } from "../install";
|
|
13
|
+
|
|
14
|
+
export function parsePackageSpec(spec: string): { package: string; version: string } {
|
|
15
|
+
const atIndex = spec.startsWith("@") ? spec.indexOf("@", 1) : spec.indexOf("@");
|
|
16
|
+
if (atIndex === -1) return { package: spec, version: "latest" };
|
|
17
|
+
return { package: spec.slice(0, atIndex), version: spec.slice(atIndex + 1) };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function withDeclaration(
|
|
21
|
+
declarations: PluginDeclaration[],
|
|
22
|
+
packageName: string,
|
|
23
|
+
version: string,
|
|
24
|
+
): PluginDeclaration[] {
|
|
25
|
+
const existing = declarations.find((declaration) => declaration.package === packageName);
|
|
26
|
+
if (existing?.version === version) return declarations;
|
|
27
|
+
if (!existing) return [...declarations, { package: packageName, version }];
|
|
28
|
+
return declarations.map((declaration) =>
|
|
29
|
+
declaration.package === packageName ? { ...declaration, version } : declaration,
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface PluginInstallCommandDeps {
|
|
34
|
+
cwd?: string;
|
|
35
|
+
installDeps?: PluginInstallDeps;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* @command mate plugin install <package>[@version]
|
|
40
|
+
* @description Declares `<package>` in the companion's `framework.yaml`
|
|
41
|
+
* `plugins:` list (adding it, or updating its version if already declared)
|
|
42
|
+
* and installs it immediately into the shared workspace, the same way
|
|
43
|
+
* `mate install` would on its next run.
|
|
44
|
+
*/
|
|
45
|
+
export async function runPluginInstallCommand(
|
|
46
|
+
argv: string[],
|
|
47
|
+
deps: PluginInstallCommandDeps = {},
|
|
48
|
+
): Promise<boolean> {
|
|
49
|
+
const spec = argv[0];
|
|
50
|
+
if (!spec) {
|
|
51
|
+
process.stderr.write(
|
|
52
|
+
`${FRAMEWORK_NAME}: usage: ${FRAMEWORK_NAME} plugin install <package>[@version]\n`,
|
|
53
|
+
);
|
|
54
|
+
process.exitCode = 1;
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
const { package: packageName, version } = parsePackageSpec(spec);
|
|
58
|
+
|
|
59
|
+
const context = await resolveInstallContext(deps.cwd ?? process.cwd());
|
|
60
|
+
if (context.kind === "ambiguous") {
|
|
61
|
+
process.stderr.write(`${context.message}\n`);
|
|
62
|
+
process.exitCode = 1;
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
if (context.kind !== "companion" || !context.companionPath) {
|
|
66
|
+
process.stderr.write(
|
|
67
|
+
`${FRAMEWORK_NAME}: \`plugin install\` requires a companion context; run it from inside a linked working repository.\n`,
|
|
68
|
+
);
|
|
69
|
+
process.exitCode = 1;
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const configPath = path.join(
|
|
74
|
+
context.companionPath,
|
|
75
|
+
`.${FRAMEWORK_NAME}`,
|
|
76
|
+
"config",
|
|
77
|
+
"framework.yaml",
|
|
78
|
+
);
|
|
79
|
+
const store = new ConfigStore(configPath);
|
|
80
|
+
const nextPlugins = withDeclaration(context.config.plugins ?? [], packageName, version);
|
|
81
|
+
if (nextPlugins !== context.config.plugins) {
|
|
82
|
+
await store.save({ ...context.config, plugins: nextPlugins });
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const results = await installDeclaredPlugins(
|
|
86
|
+
context.companionPath,
|
|
87
|
+
nextPlugins,
|
|
88
|
+
deps.installDeps,
|
|
89
|
+
);
|
|
90
|
+
reportPluginInstallResults(results);
|
|
91
|
+
await hydrateDynamicPlugins({ companionPath: context.companionPath });
|
|
92
|
+
|
|
93
|
+
return results.find((result) => result.package === packageName)?.status !== "failed";
|
|
94
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { usage } from "../../usage";
|
|
2
|
+
import { runPluginInstallCommand } from "./install";
|
|
3
|
+
|
|
4
|
+
export async function runPluginCommand(
|
|
5
|
+
subcommand: string | undefined,
|
|
6
|
+
argv: string[],
|
|
7
|
+
): Promise<void> {
|
|
8
|
+
switch (subcommand) {
|
|
9
|
+
case "install":
|
|
10
|
+
await runPluginInstallCommand(argv);
|
|
11
|
+
return;
|
|
12
|
+
default:
|
|
13
|
+
console.error(`Unknown plugin command: ${subcommand ?? ""}`);
|
|
14
|
+
console.error(usage());
|
|
15
|
+
process.exitCode = 1;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
|
|
3
|
-
import { FRAMEWORK_NAME
|
|
3
|
+
import { FRAMEWORK_NAME } from "../../framework";
|
|
4
4
|
import { ConfigStore, defaultConfig } from "../../lib/orchestrator/config-store";
|
|
5
5
|
import { executeSetup } from "../../tools/setup";
|
|
6
6
|
import {
|
|
@@ -98,7 +98,7 @@ async function runSetupFlow(
|
|
|
98
98
|
if (await checkWorkingRepo(cwd)) {
|
|
99
99
|
process.stderr.write(
|
|
100
100
|
`Warning: this directory looks like a working repository (project files detected).\n` +
|
|
101
|
-
`Initializing it as a ${
|
|
101
|
+
`Initializing it as a ${FRAMEWORK_NAME} companion here may not be what you want.\n`,
|
|
102
102
|
);
|
|
103
103
|
}
|
|
104
104
|
const ok = await askConfirm("Initialize this directory as a Mate companion repository? [y/N] ");
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import { spawnSync } from "node:child_process";
|
|
3
3
|
|
|
4
|
-
import {
|
|
4
|
+
import { FRAMEWORK_NAME } from "../../framework";
|
|
5
5
|
import {
|
|
6
6
|
OPENCODE_PLUGIN_PACKAGE_NAME,
|
|
7
7
|
warmOpenCodePluginCache,
|
|
@@ -20,7 +20,7 @@ type InstallResult = ReturnType<typeof installPublicPackageSync>;
|
|
|
20
20
|
function writeNpmOnlyRecoveryMessage(): void {
|
|
21
21
|
const { packageName, registry } = getUpdateConfig();
|
|
22
22
|
process.stderr.write(
|
|
23
|
-
`${
|
|
23
|
+
`${FRAMEWORK_NAME}: self-update is only supported for npm-installed Mate.\n`,
|
|
24
24
|
);
|
|
25
25
|
if (packageName.startsWith("@uniqbit/")) {
|
|
26
26
|
process.stderr.write(
|
|
@@ -87,16 +87,14 @@ export async function runUpdateCommand(argv: string[]): Promise<void> {
|
|
|
87
87
|
try {
|
|
88
88
|
latest = await updateCommandDeps.fetchLatestVersion();
|
|
89
89
|
} catch {
|
|
90
|
-
process.stderr.write(
|
|
91
|
-
`${frameworkCommandName()}: could not reach registry to check for updates\n`,
|
|
92
|
-
);
|
|
90
|
+
process.stderr.write(`${FRAMEWORK_NAME}: could not reach registry to check for updates\n`);
|
|
93
91
|
process.exitCode = 1;
|
|
94
92
|
return;
|
|
95
93
|
}
|
|
96
94
|
|
|
97
95
|
if (checkOnly) {
|
|
98
96
|
if (updateCommandDeps.isNewer(latest, current)) {
|
|
99
|
-
console.log(`${
|
|
97
|
+
console.log(`${FRAMEWORK_NAME}: update available (${current} → ${latest})`);
|
|
100
98
|
process.exitCode = 1;
|
|
101
99
|
} else {
|
|
102
100
|
console.log("Up to date.");
|
|
@@ -127,11 +125,11 @@ export async function runUpdateCommand(argv: string[]): Promise<void> {
|
|
|
127
125
|
} catch (error) {
|
|
128
126
|
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
|
|
129
127
|
process.stderr.write(
|
|
130
|
-
`${
|
|
128
|
+
`${FRAMEWORK_NAME}: npm is required for self-update but was not found on PATH\n`,
|
|
131
129
|
);
|
|
132
130
|
} else {
|
|
133
131
|
process.stderr.write(
|
|
134
|
-
`${
|
|
132
|
+
`${FRAMEWORK_NAME}: could not verify the npm installation used for self-update\n`,
|
|
135
133
|
);
|
|
136
134
|
}
|
|
137
135
|
writeNpmOnlyRecoveryMessage();
|
|
@@ -148,7 +146,7 @@ export async function runUpdateCommand(argv: string[]): Promise<void> {
|
|
|
148
146
|
const result = updateCommandDeps.installLatest(latest);
|
|
149
147
|
if (result.status !== 0 || result.error) {
|
|
150
148
|
process.stderr.write(
|
|
151
|
-
`${
|
|
149
|
+
`${FRAMEWORK_NAME}: upgrade command exited with status ${result.status ?? 1}\n`,
|
|
152
150
|
);
|
|
153
151
|
process.exitCode = 1;
|
|
154
152
|
return;
|
|
@@ -163,7 +161,7 @@ export async function runUpdateCommand(argv: string[]): Promise<void> {
|
|
|
163
161
|
if (!warmed.ok) {
|
|
164
162
|
process.stderr.write(
|
|
165
163
|
[
|
|
166
|
-
`${
|
|
164
|
+
`${FRAMEWORK_NAME}: could not pre-fetch ${OPENCODE_PLUGIN_PACKAGE_NAME}@${latest} for OpenCode.`,
|
|
167
165
|
"The next managed OpenCode launch will download it (requires registry access).",
|
|
168
166
|
...(warmed.detail ? [`Details: ${warmed.detail}`] : []),
|
|
169
167
|
].join("\n") + "\n",
|
|
@@ -173,7 +171,7 @@ export async function runUpdateCommand(argv: string[]): Promise<void> {
|
|
|
173
171
|
const postInstall = updateCommandDeps.runPostInstall(skipConfirm);
|
|
174
172
|
if (postInstall.status !== 0 || postInstall.error) {
|
|
175
173
|
process.stderr.write(
|
|
176
|
-
`\nUpgraded to ${latest}, but installation is incomplete. Run \`${
|
|
174
|
+
`\nUpgraded to ${latest}, but installation is incomplete. Run \`${FRAMEWORK_NAME} install\`.\n`,
|
|
177
175
|
);
|
|
178
176
|
process.exitCode = 1;
|
|
179
177
|
return;
|
package/src/cli/main.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { getActiveDistribution } from "../distribution";
|
|
2
|
-
import {
|
|
2
|
+
import { FRAMEWORK_NAME } from "../framework";
|
|
3
3
|
import {
|
|
4
4
|
enforceUpdateIfRequired,
|
|
5
5
|
scheduleBackgroundCheck,
|
|
@@ -9,10 +9,12 @@ import {
|
|
|
9
9
|
import { runArtifactCommand } from "./commands/artifact/artifact";
|
|
10
10
|
import { runCapCommand } from "./commands/cap";
|
|
11
11
|
import { runCompanionCommand } from "./commands/companion/companion";
|
|
12
|
+
import { runHubCommand } from "./commands/companion/hub";
|
|
12
13
|
import { runConfigCommand } from "./commands/config";
|
|
13
14
|
import { runDoctorCommand } from "./commands/doctor";
|
|
14
15
|
import { runLaunchClaudeCommand } from "./commands/launch/claude";
|
|
15
16
|
import { runLaunchOpenCodeCommand } from "./commands/launch/opencode";
|
|
17
|
+
import { runPluginCommand } from "./commands/plugin/plugin";
|
|
16
18
|
import { runReportCommand } from "./commands/report";
|
|
17
19
|
import { runUpdateCommand } from "./commands/update";
|
|
18
20
|
import { runInstallCommand } from "./commands/install";
|
|
@@ -51,10 +53,10 @@ export async function main(argv = process.argv, deps: MainDeps = mainDeps): Prom
|
|
|
51
53
|
return;
|
|
52
54
|
}
|
|
53
55
|
|
|
54
|
-
//
|
|
55
|
-
//
|
|
56
|
-
//
|
|
57
|
-
//
|
|
56
|
+
// Dynamic plugins register before cap-command detection and before help
|
|
57
|
+
// text is printed, so their commands route like compiled-in ones
|
|
58
|
+
// (including MCP servers whose command is their own cap subcommand) and
|
|
59
|
+
// show up in `mate help`. Diagnostics stay on stderr; a missing or
|
|
58
60
|
// ambiguous companion makes this a no-op.
|
|
59
61
|
await deps.hydrateDynamicPlugins();
|
|
60
62
|
|
|
@@ -92,10 +94,8 @@ export async function main(argv = process.argv, deps: MainDeps = mainDeps): Prom
|
|
|
92
94
|
if (needs.install) {
|
|
93
95
|
const preflight = await deps.inspectInstallPreflight();
|
|
94
96
|
if (!preflight.ok) {
|
|
95
|
-
console.error(
|
|
96
|
-
|
|
97
|
-
);
|
|
98
|
-
console.error(`Run \`${frameworkCommandName()} install\` before continuing.`);
|
|
97
|
+
console.error(`${FRAMEWORK_NAME}: ${preflight.reason ?? "installation is incomplete"}`);
|
|
98
|
+
console.error(`Run \`${FRAMEWORK_NAME} install\` before continuing.`);
|
|
99
99
|
process.exitCode = 1;
|
|
100
100
|
return false;
|
|
101
101
|
}
|
|
@@ -111,6 +111,13 @@ export async function main(argv = process.argv, deps: MainDeps = mainDeps): Prom
|
|
|
111
111
|
if (!(await gate({ companion: true }))) return;
|
|
112
112
|
await runInstallCommand(argv.slice(3));
|
|
113
113
|
return;
|
|
114
|
+
case "plugin":
|
|
115
|
+
// Declares into framework.yaml and installs on the spot, so it needs
|
|
116
|
+
// the same gating as `install` — an unambiguous companion, but must
|
|
117
|
+
// stay runnable when installation is otherwise incomplete.
|
|
118
|
+
if (!(await gate({ companion: true }))) return;
|
|
119
|
+
await runPluginCommand(subcommand, rest);
|
|
120
|
+
return;
|
|
114
121
|
case "artifact":
|
|
115
122
|
if (!(await gate({ updateGuard: true, companion: true, install: true }))) return;
|
|
116
123
|
await runArtifactCommand(subcommand, rest);
|
|
@@ -123,6 +130,11 @@ export async function main(argv = process.argv, deps: MainDeps = mainDeps): Prom
|
|
|
123
130
|
case "link":
|
|
124
131
|
if (!(await gate({}))) return;
|
|
125
132
|
break;
|
|
133
|
+
// Hub commands establish and operate on a local hub root directly;
|
|
134
|
+
// they must not require a linked working repository or installation.
|
|
135
|
+
case "hub":
|
|
136
|
+
if (!(await gate({ updateGuard: true }))) return;
|
|
137
|
+
break;
|
|
126
138
|
// open/tui consume a companion context.
|
|
127
139
|
case "open":
|
|
128
140
|
case "tui":
|
|
@@ -134,6 +146,10 @@ export async function main(argv = process.argv, deps: MainDeps = mainDeps): Prom
|
|
|
134
146
|
}
|
|
135
147
|
await runCompanionCommand(subcommand, rest);
|
|
136
148
|
return;
|
|
149
|
+
case "hub":
|
|
150
|
+
if (!(await gate({ updateGuard: true }))) return;
|
|
151
|
+
await runHubCommand(subcommand ? [subcommand, ...rest] : []);
|
|
152
|
+
return;
|
|
137
153
|
case "claude":
|
|
138
154
|
if (!(await gate({ updateGuard: true, companion: true, install: true }))) return;
|
|
139
155
|
await runLaunchClaudeCommand(argv.slice(3), { directPassthrough: true });
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { getActiveDistribution } from "../distribution";
|
|
2
|
-
import {
|
|
2
|
+
import { FRAMEWORK_NAME } from "../framework";
|
|
3
3
|
import { resolveInstallContext } from "../lib/install";
|
|
4
4
|
import type { FrameworkConfig } from "../lib/orchestrator/types";
|
|
5
5
|
import type { Plugin, PluginCliCommand } from "../tools/setup/plugin";
|
|
@@ -86,7 +86,7 @@ export async function ensureCapabilityEnabled(
|
|
|
86
86
|
const loadConfig = deps.loadConfig ?? (async () => (await resolveInstallContext()).config);
|
|
87
87
|
if (plugin?.isEnabled(await loadConfig())) return true;
|
|
88
88
|
|
|
89
|
-
const name =
|
|
89
|
+
const name = FRAMEWORK_NAME;
|
|
90
90
|
console.error(
|
|
91
91
|
`${name}: the "${pluginId}" capability is not enabled for this companion. ` +
|
|
92
92
|
`Enable it via \`${name} companion setup\`.`,
|
|
@@ -104,7 +104,7 @@ function formatCommand(
|
|
|
104
104
|
}
|
|
105
105
|
|
|
106
106
|
function usageFor(plugin: Plugin): string {
|
|
107
|
-
const distributionName =
|
|
107
|
+
const distributionName = FRAMEWORK_NAME;
|
|
108
108
|
const namespace = namespaceOf(plugin);
|
|
109
109
|
const lines = (plugin.cliCommands ?? []).map((command) =>
|
|
110
110
|
formatCommand(distributionName, namespace, command),
|
|
@@ -119,7 +119,7 @@ function usageFor(plugin: Plugin): string {
|
|
|
119
119
|
* commands show up in `mate help` alongside framework builtins.
|
|
120
120
|
*/
|
|
121
121
|
export function pluginCliCommandLines(): string[] {
|
|
122
|
-
const distributionName =
|
|
122
|
+
const distributionName = FRAMEWORK_NAME;
|
|
123
123
|
return getActiveDistribution()
|
|
124
124
|
.registry.getAll()
|
|
125
125
|
.flatMap((plugin) =>
|
package/src/cli/usage.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { getActiveDistribution } from "../distribution";
|
|
2
|
-
import { FRAMEWORK_NAME
|
|
2
|
+
import { FRAMEWORK_NAME } from "../framework";
|
|
3
3
|
import { pluginCliCommandLines } from "./plugin-commands";
|
|
4
4
|
|
|
5
5
|
export function usage(): string {
|
|
6
|
-
const n =
|
|
6
|
+
const n = FRAMEWORK_NAME;
|
|
7
7
|
const packageName =
|
|
8
8
|
getActiveDistribution().config.update?.packageName ?? `@uniqbit/${FRAMEWORK_NAME}`;
|
|
9
9
|
return [
|
|
10
|
-
|
|
10
|
+
`Mate CLI (${packageName})`,
|
|
11
11
|
"",
|
|
12
12
|
"Commands:",
|
|
13
13
|
` ${n} install [--yes]`,
|
|
@@ -16,6 +16,9 @@ export function usage(): string {
|
|
|
16
16
|
` ${n} companion list`,
|
|
17
17
|
` ${n} companion open`,
|
|
18
18
|
` ${n} companion tui`,
|
|
19
|
+
` ${n} hub init [folder]`,
|
|
20
|
+
` ${n} hub add [source] [--id ID] [--path PATH]`,
|
|
21
|
+
` ${n} hub sync [--json] (companions + hub plugins)`,
|
|
19
22
|
` ${n} artifact finish <change-name> [--type openspec] [--force] [--no-push] [--json]`,
|
|
20
23
|
` ${n} doctor`,
|
|
21
24
|
` ${n} report [--days N] [--input FILE|-] [--json]`,
|
|
@@ -25,6 +28,7 @@ export function usage(): string {
|
|
|
25
28
|
` ${n} cap openspec <subcommand> [args...]`,
|
|
26
29
|
` ${n} cap graphify <subcommand> [args...]`,
|
|
27
30
|
` ${n} cap index [--graphify] [--tokensave]`,
|
|
31
|
+
` ${n} plugin install <package>[@version]`,
|
|
28
32
|
...pluginCliCommandLines(),
|
|
29
33
|
` ${n} update`,
|
|
30
34
|
` ${n} update --check`,
|
package/src/distribution.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { PluginRegistry } from "./tools/setup/registry";
|
|
2
2
|
|
|
3
3
|
export interface DistributionUpdateConfig {
|
|
4
|
-
/** npm package whose versions are checked and installed by `mate update`. Defaults to `@uniqbit
|
|
4
|
+
/** npm package whose versions are checked and installed by `mate update`. Defaults to `@uniqbit/mate`. */
|
|
5
5
|
packageName?: string;
|
|
6
6
|
/** npm registry used for update checks and downloads. Defaults to public npm. */
|
|
7
7
|
registry?: string;
|
|
@@ -14,15 +14,13 @@ export interface DistributionUpdateConfig {
|
|
|
14
14
|
enforce?: boolean;
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
/**
|
|
17
|
+
/** Version and plugin registry of the distribution assembled through `createMate`. */
|
|
18
18
|
export interface DistributionConfig {
|
|
19
|
-
name: string;
|
|
20
|
-
legacyNames?: string[];
|
|
21
19
|
runtime: "bun";
|
|
22
20
|
version: string;
|
|
23
21
|
/** Asset directories that override the framework defaults, highest precedence first. */
|
|
24
22
|
assetRoots?: string[];
|
|
25
|
-
/** Optional update source; defaults to `@uniqbit
|
|
23
|
+
/** Optional update source; defaults to `@uniqbit/mate` on public npm. */
|
|
26
24
|
update?: DistributionUpdateConfig;
|
|
27
25
|
}
|
|
28
26
|
|