@uniqbit/mate-core 0.16.0 → 0.17.0-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/package.json +2 -2
- package/src/cli/commands/companion/companion.ts +8 -0
- package/src/cli/commands/companion/prepare.ts +76 -0
- package/src/cli/commands/companion/register.ts +47 -0
- package/src/cli/commands/launch/claude.ts +4 -3
- package/src/cli/commands/launch/opencode.ts +4 -3
- package/src/cli/commands/launch/shared.ts +41 -28
- package/src/cli/commands/shared/companion-selection.ts +3 -1
- package/src/cli/commands/studio/index.ts +85 -6
- package/src/cli/commands/studio/routes.ts +1 -1
- package/src/cli/commands/studio/selection.ts +11 -2
- package/src/cli/commands/studio/server.ts +212 -17
- package/src/cli/commands/studio/vault.ts +607 -0
- package/src/cli/commands/studio/views/client.ts +141 -1
- package/src/cli/commands/studio/views/companion-selector.tsx +1 -1
- package/src/cli/commands/studio/views/document.tsx +137 -3
- package/src/cli/commands/studio/views/model.ts +13 -0
- package/src/cli/commands/studio/views/styles.ts +17 -0
- package/src/cli/commands/studio/views/workflow/steps.ts +10 -3
- package/src/cli/commands/update.ts +6 -6
- package/src/cli/main.ts +56 -4
- package/src/cli/usage.ts +6 -3
- package/src/index.ts +1 -0
- package/src/lib/context-mode-package.ts +7 -4
- package/src/lib/node-engine-version.ts +15 -0
- package/src/lib/opencode-plugin-package.ts +10 -4
- package/src/lib/orchestrator/adapters/base.ts +65 -10
- package/src/lib/orchestrator/adapters/opencode.ts +27 -2
- package/src/lib/orchestrator/companion-registration.ts +73 -0
- package/src/lib/orchestrator/framework-context.ts +29 -3
- package/src/lib/orchestrator/launcher.ts +46 -22
- package/src/lib/orchestrator/projection-record.ts +24 -0
- package/src/lib/orchestrator/types.ts +11 -0
- package/src/lib/package-root.ts +43 -0
- package/src/lib/prebuilt-workspace-prepare.ts +152 -0
- package/src/lib/prebuilt-workspace.ts +246 -0
- package/src/lib/preinstalled-plugins.ts +106 -0
- package/src/lib/update-checker.ts +39 -0
- package/src/opencode/companion-hooks.ts +45 -20
- package/src/playbooks/companion-guidance.ts +1 -0
- package/src/runtime/companion-guidance.ts +61 -15
- package/src/runtime/env.ts +3 -3
- package/src/runtime/index.ts +1 -0
- package/src/templates/capabilities/openspec-cap/mate-minimal/schema.yaml +2 -1
- package/src/tools/setup/__snapshots__/runtime-surface-golden.test.ts.snap +127 -77
- package/src/tools/setup/capabilities/context-mode.ts +34 -2
- package/src/tools/setup/capabilities/openspec.ts +12 -2
- package/src/tools/setup/plugin.ts +7 -1
- package/src/tools/setup/providers/opencode.ts +24 -5
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uniqbit/mate-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.17.0-canary.10",
|
|
4
4
|
"description": "Core framework and plugin APIs for Mate.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"files": [
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
"registry": "https://registry.npmjs.org/"
|
|
24
24
|
},
|
|
25
25
|
"scripts": {
|
|
26
|
-
"test": "CI=1 bun test",
|
|
26
|
+
"test": "CI=1 bun ../../test/run-isolated.ts",
|
|
27
27
|
"typecheck": "bunx tsc --noEmit -p ../../tsconfig.json",
|
|
28
28
|
"lint": "oxlint src/",
|
|
29
29
|
"lint:fix": "oxlint src/ --fix",
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { usage } from "../../usage";
|
|
2
2
|
import { runCompanionLinkCommand } from "./link";
|
|
3
|
+
import { runCompanionRegisterCommand } from "./register";
|
|
4
|
+
import { runCompanionPrepareCommand } from "./prepare";
|
|
3
5
|
import { runSetupCommand } from "../setup";
|
|
4
6
|
import { runCompanionListCommand } from "./list";
|
|
5
7
|
import { runCompanionSyncCommand } from "./sync";
|
|
@@ -14,6 +16,12 @@ export async function runCompanionCommand(
|
|
|
14
16
|
case "link":
|
|
15
17
|
await runCompanionLinkCommand(argv);
|
|
16
18
|
return;
|
|
19
|
+
case "register":
|
|
20
|
+
await runCompanionRegisterCommand(argv);
|
|
21
|
+
return;
|
|
22
|
+
case "prepare":
|
|
23
|
+
await runCompanionPrepareCommand(argv);
|
|
24
|
+
return;
|
|
17
25
|
case "setup":
|
|
18
26
|
await runSetupCommand(argv);
|
|
19
27
|
return;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
|
|
3
|
+
import { FRAMEWORK_NAME } from "../../../framework";
|
|
4
|
+
import { preparePrebuiltWorkspace } from "../../../lib/prebuilt-workspace-prepare";
|
|
5
|
+
|
|
6
|
+
export const companionPrepareCommandDeps = {
|
|
7
|
+
preparePrebuiltWorkspace,
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
interface ParsedPrepareArgs {
|
|
11
|
+
bundle?: string;
|
|
12
|
+
companionPath?: string;
|
|
13
|
+
error?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function parsePrepareArgs(argv: string[]): ParsedPrepareArgs {
|
|
17
|
+
let bundle: string | undefined;
|
|
18
|
+
const positional: string[] = [];
|
|
19
|
+
|
|
20
|
+
for (let index = 0; index < argv.length; index++) {
|
|
21
|
+
const arg = argv[index]!;
|
|
22
|
+
if (arg === "--from") {
|
|
23
|
+
const value = argv[++index];
|
|
24
|
+
if (!value) return { error: "`--from` requires a path to a prebuilt dependency bundle." };
|
|
25
|
+
bundle = value;
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
if (arg.startsWith("--from=")) {
|
|
29
|
+
bundle = arg.slice("--from=".length);
|
|
30
|
+
if (!bundle) return { error: "`--from` requires a path to a prebuilt dependency bundle." };
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
if (arg.startsWith("-")) return { error: `unknown option: ${arg}` };
|
|
34
|
+
positional.push(arg);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (positional.length > 1) return { error: "`companion prepare` accepts at most one path." };
|
|
38
|
+
if (!bundle) return { error: "`companion prepare` requires `--from <bundle>`." };
|
|
39
|
+
return { bundle, companionPath: positional[0] };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* @command mate companion prepare --from <bundle> [companion-path]
|
|
44
|
+
* @description Prepares the companion's machine-local dependency workspace by
|
|
45
|
+
* validating and copying a fully installed prebuilt bundle. Runs no package
|
|
46
|
+
* manager, no dependency resolution, and no installation script; presents no
|
|
47
|
+
* selection and changes none.
|
|
48
|
+
* @flags
|
|
49
|
+
* - `--from PATH` — the prebuilt, fully installed workspace to copy.
|
|
50
|
+
*/
|
|
51
|
+
export async function runCompanionPrepareCommand(argv: string[] = []): Promise<void> {
|
|
52
|
+
const parsed = parsePrepareArgs(argv);
|
|
53
|
+
if (parsed.error) {
|
|
54
|
+
process.stderr.write(`${FRAMEWORK_NAME}: ${parsed.error}\n`);
|
|
55
|
+
process.exitCode = 1;
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const companionPath = path.resolve(parsed.companionPath ?? process.cwd());
|
|
60
|
+
const result = await companionPrepareCommandDeps.preparePrebuiltWorkspace(
|
|
61
|
+
companionPath,
|
|
62
|
+
path.resolve(parsed.bundle!),
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
if (!result.ok) {
|
|
66
|
+
process.stderr.write(
|
|
67
|
+
`${FRAMEWORK_NAME}: cannot prepare local dependencies:\n${result.failures.map((failure) => ` ${failure}`).join("\n")}\n`,
|
|
68
|
+
);
|
|
69
|
+
process.exitCode = 1;
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
process.stdout.write(
|
|
74
|
+
`${result.reused ? "Reused" : "Prepared"} local dependencies at ${result.workspacePath}\n`,
|
|
75
|
+
);
|
|
76
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
|
|
3
|
+
import { FRAMEWORK_NAME } from "../../../framework";
|
|
4
|
+
import { registerConfiguredCompanion } from "../../../lib/orchestrator/companion-registration";
|
|
5
|
+
import { GlobalConfigStore } from "../../../lib/orchestrator/global-config-store";
|
|
6
|
+
|
|
7
|
+
export const companionRegisterCommandDeps = {
|
|
8
|
+
registerConfiguredCompanion,
|
|
9
|
+
createGlobalConfigStore: () => new GlobalConfigStore(),
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @command mate companion register [path]
|
|
14
|
+
* @description Registers an already-configured Companion Repository in the
|
|
15
|
+
* global registry. Presents no selection, reads no input, and writes nothing
|
|
16
|
+
* inside the companion — the path a script or container entrypoint calls.
|
|
17
|
+
* @remarks Defaults to the current directory. Exits non-zero when the target
|
|
18
|
+
* carries no companion configuration.
|
|
19
|
+
*/
|
|
20
|
+
export async function runCompanionRegisterCommand(argv: string[] = []): Promise<void> {
|
|
21
|
+
const positional = argv.filter((arg) => !arg.startsWith("-"));
|
|
22
|
+
if (positional.length !== argv.length) {
|
|
23
|
+
process.stderr.write(`${FRAMEWORK_NAME}: \`companion register\` accepts no flags.\n`);
|
|
24
|
+
process.exitCode = 1;
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
if (positional.length > 1) {
|
|
28
|
+
process.stderr.write(`${FRAMEWORK_NAME}: \`companion register\` accepts at most one path.\n`);
|
|
29
|
+
process.exitCode = 1;
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const target = path.resolve(positional[0] ?? process.cwd());
|
|
34
|
+
const result = await companionRegisterCommandDeps.registerConfiguredCompanion(target, {
|
|
35
|
+
globalConfigStore: companionRegisterCommandDeps.createGlobalConfigStore(),
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
if (!result.ok) {
|
|
39
|
+
process.stderr.write(`${FRAMEWORK_NAME}: ${result.reason}\n`);
|
|
40
|
+
process.exitCode = 1;
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
process.stdout.write(
|
|
45
|
+
`${result.alreadyRegistered ? "Already registered" : "Registered"} companion: ${result.companionPath}\n`,
|
|
46
|
+
);
|
|
47
|
+
}
|
|
@@ -2,12 +2,13 @@ import { makeLaunchCommand } from "./shared";
|
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* @command mate claude [-- ...agentArgs]
|
|
5
|
-
* @description Launches Claude Code for the active working repository
|
|
6
|
-
*
|
|
7
|
-
* {@link runLaunchToolCommand} with confirmation.
|
|
5
|
+
* @description Launches Claude Code for the active working repository, or the
|
|
6
|
+
* Companion Repository with `-- --companion`, without an interactive confirmation.
|
|
8
7
|
* @flags
|
|
9
8
|
* - `-- <args>` — arguments forwarded to the launched `claude` process.
|
|
10
9
|
* - `-- --no-git` — skip companion Git synchronization for this launch.
|
|
10
|
+
* - `-- --companion` — launch against the Companion Repository without a Working Repository.
|
|
11
|
+
* - `-- --yes` — accepted as a Mate compatibility no-op; launches are always prompt-free.
|
|
11
12
|
* @remarks When invoked via `directPassthrough` (the top-level `mate claude`
|
|
12
13
|
* alias), all args before `--` are treated as agent args rather than being
|
|
13
14
|
* parsed as launch options.
|
|
@@ -2,12 +2,13 @@ import { makeLaunchCommand } from "./shared";
|
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* @command mate opencode [-- ...agentArgs]
|
|
5
|
-
* @description Launches OpenCode for the active working repository
|
|
6
|
-
*
|
|
7
|
-
* {@link runLaunchToolCommand} with confirmation.
|
|
5
|
+
* @description Launches OpenCode for the active working repository, or the
|
|
6
|
+
* Companion Repository with `-- --companion`, without an interactive confirmation.
|
|
8
7
|
* @flags
|
|
9
8
|
* - `-- <args>` — arguments forwarded to the launched `opencode` process.
|
|
10
9
|
* - `-- --no-git` — skip companion Git synchronization for this launch.
|
|
10
|
+
* - `-- --companion` — launch against the Companion Repository without a Working Repository.
|
|
11
|
+
* - `-- --yes` — accepted as a Mate compatibility no-op; launches are always prompt-free.
|
|
11
12
|
* @remarks When invoked via `directPassthrough` (the top-level `mate
|
|
12
13
|
* opencode` alias), all args before `--` are treated as agent args rather
|
|
13
14
|
* than being parsed as launch options.
|
|
@@ -4,11 +4,11 @@ import {
|
|
|
4
4
|
LaunchPreflightError,
|
|
5
5
|
RepositoryNotSelectedError,
|
|
6
6
|
ToolNotAllowedError,
|
|
7
|
+
type LaunchScope,
|
|
7
8
|
} from "../../../lib/orchestrator/types";
|
|
8
9
|
import { createStartupProgress } from "../../../lib/components/startup-progress";
|
|
9
10
|
import { ensureUnambiguousCompanion, launchAmbiguityDeps } from "../shared/companion-selection";
|
|
10
11
|
import { runIndexCapCommand } from "../cap/index-cmd";
|
|
11
|
-
import { confirm } from "../../confirm";
|
|
12
12
|
import type { LaunchTarget } from "../../launch-selector";
|
|
13
13
|
|
|
14
14
|
const STEP_LABELS = {
|
|
@@ -20,6 +20,20 @@ const STEP_LABELS = {
|
|
|
20
20
|
export interface ParsedLaunchArgs {
|
|
21
21
|
agentArgs: string[];
|
|
22
22
|
skipGit?: boolean;
|
|
23
|
+
scope?: LaunchScope;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const MATE_LAUNCH_OPTIONS = new Set(["--no-git", "--companion", "--yes"]);
|
|
27
|
+
|
|
28
|
+
function parseMateLaunchArgs(agentArgs: string[]): ParsedLaunchArgs {
|
|
29
|
+
const skipGit = agentArgs.includes("--no-git");
|
|
30
|
+
const companion = agentArgs.includes("--companion");
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
agentArgs: agentArgs.filter((arg) => !MATE_LAUNCH_OPTIONS.has(arg)),
|
|
34
|
+
...(skipGit ? { skipGit: true } : {}),
|
|
35
|
+
...(companion ? { scope: "companion" as const } : {}),
|
|
36
|
+
};
|
|
23
37
|
}
|
|
24
38
|
|
|
25
39
|
export { ensureUnambiguousCompanion, launchAmbiguityDeps };
|
|
@@ -28,14 +42,12 @@ export interface LaunchCommandDeps {
|
|
|
28
42
|
createLauncher: () => Pick<FrameworkLauncher, "prepare">;
|
|
29
43
|
createProgress: typeof createStartupProgress;
|
|
30
44
|
runIndexCapCommand: typeof runIndexCapCommand;
|
|
31
|
-
confirm: typeof confirm;
|
|
32
45
|
}
|
|
33
46
|
|
|
34
47
|
export const launchCommandDeps: LaunchCommandDeps = {
|
|
35
48
|
createLauncher: () => new FrameworkLauncher(),
|
|
36
49
|
createProgress: createStartupProgress,
|
|
37
50
|
runIndexCapCommand,
|
|
38
|
-
confirm,
|
|
39
51
|
};
|
|
40
52
|
|
|
41
53
|
export function parseDirectLaunchArgs(argv: string[]): ParsedLaunchArgs {
|
|
@@ -44,10 +56,10 @@ export function parseDirectLaunchArgs(argv: string[]): ParsedLaunchArgs {
|
|
|
44
56
|
|
|
45
57
|
const beforeSeparator = argv.slice(0, separatorIndex);
|
|
46
58
|
const afterSeparator = argv.slice(separatorIndex + 1);
|
|
47
|
-
const
|
|
59
|
+
const parsed = parseMateLaunchArgs(afterSeparator);
|
|
48
60
|
return {
|
|
49
|
-
|
|
50
|
-
|
|
61
|
+
...parsed,
|
|
62
|
+
agentArgs: [...beforeSeparator, ...parsed.agentArgs],
|
|
51
63
|
};
|
|
52
64
|
}
|
|
53
65
|
|
|
@@ -62,18 +74,14 @@ export function parseLaunchArgs(argv: string[]): ParsedLaunchArgs | null {
|
|
|
62
74
|
return null;
|
|
63
75
|
}
|
|
64
76
|
|
|
65
|
-
|
|
66
|
-
return {
|
|
67
|
-
agentArgs: agentArgs.filter((arg) => arg !== "--no-git"),
|
|
68
|
-
...(skipGit ? { skipGit: true } : {}),
|
|
69
|
-
};
|
|
77
|
+
return parseMateLaunchArgs(agentArgs);
|
|
70
78
|
}
|
|
71
79
|
|
|
72
80
|
/**
|
|
73
81
|
* @description Shared launch execution used by `mate claude`, `mate opencode`,
|
|
74
82
|
* and the direct `mate claude` / `mate opencode` commands: resolves the launch via {@link FrameworkLauncher.prepare},
|
|
75
|
-
*
|
|
76
|
-
*
|
|
83
|
+
* re-syncs capability indexes via `runIndexCapCommand`, then executes, prints the JSON result, and adopts the
|
|
84
|
+
* agent's outcome as the command's exit status (128 + signal for a signalled stop).
|
|
77
85
|
* @remarks Exits non-zero with a targeted message for `ToolNotAllowedError`
|
|
78
86
|
* (repo policy disallows this tool) and `RepositoryNotSelectedError` (no
|
|
79
87
|
* active repo — points the user at `mate companion link`); other errors propagate.
|
|
@@ -82,8 +90,8 @@ export async function runLaunchToolCommand(
|
|
|
82
90
|
tool: LaunchTarget,
|
|
83
91
|
args: string[],
|
|
84
92
|
options: {
|
|
85
|
-
skipConfirmation?: boolean;
|
|
86
93
|
skipGit?: boolean;
|
|
94
|
+
scope?: LaunchScope;
|
|
87
95
|
} = {},
|
|
88
96
|
): Promise<void> {
|
|
89
97
|
const interactiveGit = !!(process.stdin.isTTY && process.stdout.isTTY);
|
|
@@ -98,6 +106,7 @@ export async function runLaunchToolCommand(
|
|
|
98
106
|
tool,
|
|
99
107
|
args,
|
|
100
108
|
skipGit: options.skipGit,
|
|
109
|
+
scope: options.scope,
|
|
101
110
|
interactiveGit,
|
|
102
111
|
});
|
|
103
112
|
progress?.succeed("sync");
|
|
@@ -106,21 +115,20 @@ export async function runLaunchToolCommand(
|
|
|
106
115
|
// Ink's line tracking, corrupting later re-renders (e.g. duplicated title).
|
|
107
116
|
progress?.stop();
|
|
108
117
|
|
|
109
|
-
if (
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
process.
|
|
113
|
-
|
|
114
|
-
}
|
|
118
|
+
if (options.scope !== "companion") {
|
|
119
|
+
await launchCommandDeps.runIndexCapCommand([], {
|
|
120
|
+
onStepStart: (step) => process.stdout.write(`${STEP_LABELS[step]}...\n`),
|
|
121
|
+
onStepDone: (step, ok) => process.stdout.write(`${ok ? "✓" : "✗"} ${STEP_LABELS[step]}\n`),
|
|
122
|
+
});
|
|
115
123
|
}
|
|
116
124
|
|
|
117
|
-
await launchCommandDeps.runIndexCapCommand([], {
|
|
118
|
-
onStepStart: (step) => process.stdout.write(`${STEP_LABELS[step]}...\n`),
|
|
119
|
-
onStepDone: (step, ok) => process.stdout.write(`${ok ? "✓" : "✗"} ${STEP_LABELS[step]}\n`),
|
|
120
|
-
});
|
|
121
|
-
|
|
122
125
|
const result = await prepared.execute();
|
|
126
|
+
/**
|
|
127
|
+
* The printed result stays useful, but the status is what a supervisor
|
|
128
|
+
* reads: a failed agent must fail the launch.
|
|
129
|
+
*/
|
|
123
130
|
console.log(JSON.stringify(result, null, 2));
|
|
131
|
+
if (result.exitCode !== 0) process.exitCode = result.exitCode;
|
|
124
132
|
} catch (error) {
|
|
125
133
|
progress?.failCurrent();
|
|
126
134
|
progress?.stop();
|
|
@@ -153,7 +161,7 @@ export async function runLaunchToolCommand(
|
|
|
153
161
|
/**
|
|
154
162
|
* Builds the `mate <tool>` command handler: parses launch args (direct
|
|
155
163
|
* passthrough or flagged), guards against ambiguous companions, then launches
|
|
156
|
-
* via {@link runLaunchToolCommand}
|
|
164
|
+
* via {@link runLaunchToolCommand} without an interactive confirmation.
|
|
157
165
|
*/
|
|
158
166
|
export function makeLaunchCommand(tool: LaunchTarget) {
|
|
159
167
|
return async function runLaunchCommand(
|
|
@@ -163,14 +171,19 @@ export function makeLaunchCommand(tool: LaunchTarget) {
|
|
|
163
171
|
const parsed = options.directPassthrough ? parseDirectLaunchArgs(args) : parseLaunchArgs(args);
|
|
164
172
|
if (!parsed) return;
|
|
165
173
|
|
|
166
|
-
if (
|
|
174
|
+
if (
|
|
175
|
+
!(await ensureUnambiguousCompanion(
|
|
176
|
+
process.cwd(),
|
|
177
|
+
parsed.scope === "companion" ? { persistSelection: false } : {},
|
|
178
|
+
))
|
|
179
|
+
) {
|
|
167
180
|
process.exitCode = 1;
|
|
168
181
|
return;
|
|
169
182
|
}
|
|
170
183
|
|
|
171
184
|
await runLaunchToolCommand(tool, parsed.agentArgs, {
|
|
172
|
-
skipConfirmation: !!process.stdin.isTTY,
|
|
173
185
|
skipGit: parsed.skipGit,
|
|
186
|
+
scope: parsed.scope,
|
|
174
187
|
});
|
|
175
188
|
};
|
|
176
189
|
}
|
|
@@ -40,6 +40,8 @@ export interface CompanionSelectionOptions {
|
|
|
40
40
|
ignoreProjection?: boolean;
|
|
41
41
|
/** Outranks the environment, the projection, and the picker. */
|
|
42
42
|
companion?: string;
|
|
43
|
+
/** Pin an interactive answer for this process without writing a projection. */
|
|
44
|
+
persistSelection?: boolean;
|
|
43
45
|
}
|
|
44
46
|
|
|
45
47
|
function pin(match: Pick<CompanionMatch, "companionPath" | "repositoryId">): void {
|
|
@@ -137,6 +139,6 @@ export async function ensureUnambiguousCompanion(
|
|
|
137
139
|
}
|
|
138
140
|
|
|
139
141
|
pin(chosen);
|
|
140
|
-
await recordCompanion(cwd, chosen);
|
|
142
|
+
if (options.persistSelection !== false) await recordCompanion(cwd, chosen);
|
|
141
143
|
return true;
|
|
142
144
|
}
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import { FRAMEWORK_NAME } from "../../../framework";
|
|
2
2
|
import { openReportInBrowser } from "../report/delivery";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
serveUntilInterrupted,
|
|
5
|
+
startStudioServer,
|
|
6
|
+
STUDIO_HOSTNAME,
|
|
7
|
+
type StudioServerHandle,
|
|
8
|
+
type StudioServerOptions,
|
|
9
|
+
} from "./server";
|
|
4
10
|
|
|
5
11
|
export interface StudioCommandDeps {
|
|
6
12
|
startStudioServer?: typeof startStudioServer;
|
|
@@ -10,16 +16,52 @@ export interface StudioCommandDeps {
|
|
|
10
16
|
warn?: (message: string) => void;
|
|
11
17
|
}
|
|
12
18
|
|
|
19
|
+
export type StudioServeArgs = StudioServerOptions & { port: number; hostname: string };
|
|
20
|
+
|
|
21
|
+
export function parseStudioServeArgs(argv: string[]): StudioServeArgs | { error: string } {
|
|
22
|
+
let port: number | undefined;
|
|
23
|
+
let hostname = STUDIO_HOSTNAME;
|
|
24
|
+
|
|
25
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
26
|
+
const arg = argv[index]!;
|
|
27
|
+
if (arg === "--writable") {
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
if (arg === "--port" || arg.startsWith("--port=")) {
|
|
31
|
+
const value = arg === "--port" ? argv[index + 1] : arg.slice("--port=".length);
|
|
32
|
+
if (!value || value.startsWith("--")) return { error: "--port requires a value" };
|
|
33
|
+
const parsed = Number(value);
|
|
34
|
+
if (!/^\d+$/.test(value) || !Number.isInteger(parsed) || parsed < 1 || parsed > 65535) {
|
|
35
|
+
return { error: `invalid --port value: ${value}` };
|
|
36
|
+
}
|
|
37
|
+
port = parsed;
|
|
38
|
+
if (arg === "--port") index += 1;
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
if (arg === "--host" || arg.startsWith("--host=")) {
|
|
42
|
+
const value = arg === "--host" ? argv[index + 1] : arg.slice("--host=".length);
|
|
43
|
+
if (!value || value.startsWith("--")) return { error: "--host requires a value" };
|
|
44
|
+
hostname = value;
|
|
45
|
+
if (arg === "--host") index += 1;
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
return { error: `unknown studio serve option: ${arg}` };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (port === undefined) return { error: "studio serve requires --port" };
|
|
52
|
+
return { port, hostname, writable: argv.includes("--writable") };
|
|
53
|
+
}
|
|
54
|
+
|
|
13
55
|
/**
|
|
14
56
|
* @command mate studio
|
|
15
|
-
* @description Serves a local
|
|
57
|
+
* @description Serves a local page over the Companion Repositories
|
|
16
58
|
* registered on this machine — the workflow drawn from each companion's
|
|
17
59
|
* resolved schema with its OpenSpec state on it — and opens the platform
|
|
18
60
|
* browser at it. Runs in the foreground on an operating-system-assigned
|
|
19
61
|
* loopback port and dies with the process: no daemon, no stop command, and no
|
|
20
62
|
* state written anywhere.
|
|
21
|
-
* @remarks
|
|
22
|
-
* any directory.
|
|
63
|
+
* @remarks Both invocations are read-only unless passed `--writable`. Neither
|
|
64
|
+
* invocation resolves a Repository Link, so it runs from any directory.
|
|
23
65
|
*/
|
|
24
66
|
export async function runStudioCommand(
|
|
25
67
|
argv: string[] = [],
|
|
@@ -28,7 +70,12 @@ export async function runStudioCommand(
|
|
|
28
70
|
const log = deps.log ?? ((message: string) => process.stdout.write(`${message}\n`));
|
|
29
71
|
const warn = deps.warn ?? ((message: string) => process.stderr.write(`${message}\n`));
|
|
30
72
|
|
|
31
|
-
if (argv
|
|
73
|
+
if (argv[0] === "serve") {
|
|
74
|
+
await runStudioServeCommand(argv.slice(1), deps, log, warn);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (argv.some((arg) => arg !== "--writable")) {
|
|
32
79
|
warn(`${FRAMEWORK_NAME}: \`studio\` takes no arguments; unrecognized: ${argv.join(" ")}`);
|
|
33
80
|
process.exitCode = 1;
|
|
34
81
|
return;
|
|
@@ -40,7 +87,7 @@ export async function runStudioCommand(
|
|
|
40
87
|
|
|
41
88
|
let server;
|
|
42
89
|
try {
|
|
43
|
-
server = start();
|
|
90
|
+
server = start({}, { writable: argv.includes("--writable") });
|
|
44
91
|
} catch (error) {
|
|
45
92
|
warn(
|
|
46
93
|
`${FRAMEWORK_NAME}: studio could not bind a port: ${
|
|
@@ -67,3 +114,35 @@ export async function runStudioCommand(
|
|
|
67
114
|
|
|
68
115
|
await serve(server);
|
|
69
116
|
}
|
|
117
|
+
|
|
118
|
+
async function runStudioServeCommand(
|
|
119
|
+
argv: string[],
|
|
120
|
+
deps: StudioCommandDeps,
|
|
121
|
+
log: (message: string) => void,
|
|
122
|
+
warn: (message: string) => void,
|
|
123
|
+
): Promise<void> {
|
|
124
|
+
const parsed = parseStudioServeArgs(argv);
|
|
125
|
+
if ("error" in parsed) {
|
|
126
|
+
warn(`${FRAMEWORK_NAME}: ${parsed.error}`);
|
|
127
|
+
process.exitCode = 1;
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const start = deps.startStudioServer ?? startStudioServer;
|
|
132
|
+
const serve = deps.serveUntilInterrupted ?? serveUntilInterrupted;
|
|
133
|
+
let server: StudioServerHandle;
|
|
134
|
+
try {
|
|
135
|
+
server = start({}, parsed);
|
|
136
|
+
} catch (error) {
|
|
137
|
+
warn(
|
|
138
|
+
`${FRAMEWORK_NAME}: studio serve could not bind ${parsed.hostname}:${parsed.port}: ${
|
|
139
|
+
error instanceof Error ? error.message : String(error)
|
|
140
|
+
}`,
|
|
141
|
+
);
|
|
142
|
+
process.exitCode = 1;
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
log(server.url);
|
|
147
|
+
await serve(server);
|
|
148
|
+
}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
/**
|
|
1
|
+
/** Default bind stays loopback because payloads carry absolute paths and names. */
|
|
2
2
|
export const STUDIO_HOSTNAME = "127.0.0.1";
|
|
@@ -2,13 +2,20 @@ import { createHash } from "node:crypto";
|
|
|
2
2
|
|
|
3
3
|
import type { StudioInventory, StudioInventoryCompanion } from "./inventory";
|
|
4
4
|
|
|
5
|
-
export type StudioView = "dashboard" | "workflow" | "specs" | "skills";
|
|
5
|
+
export type StudioView = "dashboard" | "workflow" | "specs" | "skills" | "vault";
|
|
6
6
|
|
|
7
|
-
export const STUDIO_VIEWS: readonly StudioView[] = [
|
|
7
|
+
export const STUDIO_VIEWS: readonly StudioView[] = [
|
|
8
|
+
"dashboard",
|
|
9
|
+
"workflow",
|
|
10
|
+
"specs",
|
|
11
|
+
"skills",
|
|
12
|
+
"vault",
|
|
13
|
+
];
|
|
8
14
|
|
|
9
15
|
export const COMPANION_PARAM = "companion";
|
|
10
16
|
export const VIEW_PARAM = "view";
|
|
11
17
|
export const REFRESH_PARAM = "refresh";
|
|
18
|
+
export const FILE_PARAM = "path";
|
|
12
19
|
|
|
13
20
|
/** Filled into a prompt when no change is named, so a prompt is never half-written. */
|
|
14
21
|
export const CHANGE_PLACEHOLDER = "<change-name>";
|
|
@@ -23,6 +30,7 @@ export interface StudioSelection {
|
|
|
23
30
|
* into the URL another control builds. Switching a view is not a refresh.
|
|
24
31
|
*/
|
|
25
32
|
refresh: boolean;
|
|
33
|
+
openPath: string | null;
|
|
26
34
|
}
|
|
27
35
|
|
|
28
36
|
/**
|
|
@@ -57,5 +65,6 @@ export function parseStudioSelection(url: URL): StudioSelection {
|
|
|
57
65
|
companionDigest: url.searchParams.get(COMPANION_PARAM)?.trim() || null,
|
|
58
66
|
view: readView(url.searchParams.get(VIEW_PARAM)),
|
|
59
67
|
refresh: url.searchParams.get(REFRESH_PARAM) === "1",
|
|
68
|
+
openPath: url.searchParams.get(FILE_PARAM)?.trim() || null,
|
|
60
69
|
};
|
|
61
70
|
}
|