@uniqbit/mate-core 0.16.0 → 0.17.0-canary.2
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 +1 -1
- 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 +5 -3
- package/src/cli/commands/launch/opencode.ts +5 -3
- package/src/cli/commands/launch/shared.ts +41 -12
- 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 +136 -3
- package/src/cli/commands/studio/views/model.ts +13 -0
- package/src/cli/commands/studio/views/styles.ts +16 -0
- package/src/cli/main.ts +33 -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/opencode-plugin-package.ts +10 -4
- package/src/lib/orchestrator/adapters/base.ts +65 -10
- package/src/lib/orchestrator/adapters/opencode.ts +4 -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/prebuilt-workspace-prepare.ts +152 -0
- package/src/lib/prebuilt-workspace.ts +236 -0
- package/src/lib/preinstalled-plugins.ts +82 -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/tools/setup/__snapshots__/runtime-surface-golden.test.ts.snap +127 -77
- package/src/tools/setup/capabilities/context-mode.ts +5 -0
- package/src/tools/setup/plugin.ts +7 -1
- package/src/tools/setup/providers/opencode.ts +24 -5
package/package.json
CHANGED
|
@@ -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,14 @@ 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
|
-
*
|
|
5
|
+
* @description Launches Claude Code for the active working repository, or the
|
|
6
|
+
* Companion Repository with `-- --companion`. TTY launches ask for confirmation;
|
|
7
|
+
* non-interactive launches proceed without it.
|
|
8
8
|
* @flags
|
|
9
9
|
* - `-- <args>` — arguments forwarded to the launched `claude` process.
|
|
10
10
|
* - `-- --no-git` — skip companion Git synchronization for this launch.
|
|
11
|
+
* - `-- --companion` — launch against the Companion Repository without a Working Repository.
|
|
12
|
+
* - `-- --yes` — skip the TTY confirmation.
|
|
11
13
|
* @remarks When invoked via `directPassthrough` (the top-level `mate claude`
|
|
12
14
|
* alias), all args before `--` are treated as agent args rather than being
|
|
13
15
|
* parsed as launch options.
|
|
@@ -2,12 +2,14 @@ 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
|
-
*
|
|
5
|
+
* @description Launches OpenCode for the active working repository, or the
|
|
6
|
+
* Companion Repository with `-- --companion`. TTY launches ask for confirmation;
|
|
7
|
+
* non-interactive launches proceed without it.
|
|
8
8
|
* @flags
|
|
9
9
|
* - `-- <args>` — arguments forwarded to the launched `opencode` process.
|
|
10
10
|
* - `-- --no-git` — skip companion Git synchronization for this launch.
|
|
11
|
+
* - `-- --companion` — launch against the Companion Repository without a Working Repository.
|
|
12
|
+
* - `-- --yes` — skip the TTY confirmation.
|
|
11
13
|
* @remarks When invoked via `directPassthrough` (the top-level `mate
|
|
12
14
|
* opencode` alias), all args before `--` are treated as agent args rather
|
|
13
15
|
* than being parsed as launch options.
|
|
@@ -4,6 +4,7 @@ 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";
|
|
@@ -20,6 +21,8 @@ const STEP_LABELS = {
|
|
|
20
21
|
export interface ParsedLaunchArgs {
|
|
21
22
|
agentArgs: string[];
|
|
22
23
|
skipGit?: boolean;
|
|
24
|
+
scope?: LaunchScope;
|
|
25
|
+
skipConfirmation?: boolean;
|
|
23
26
|
}
|
|
24
27
|
|
|
25
28
|
export { ensureUnambiguousCompanion, launchAmbiguityDeps };
|
|
@@ -45,9 +48,14 @@ export function parseDirectLaunchArgs(argv: string[]): ParsedLaunchArgs {
|
|
|
45
48
|
const beforeSeparator = argv.slice(0, separatorIndex);
|
|
46
49
|
const afterSeparator = argv.slice(separatorIndex + 1);
|
|
47
50
|
const skipGit = afterSeparator.includes("--no-git");
|
|
51
|
+
const companion = afterSeparator.includes("--companion");
|
|
52
|
+
const skipConfirmation = afterSeparator.includes("--yes");
|
|
53
|
+
const reserved = new Set(["--no-git", "--companion", "--yes"]);
|
|
48
54
|
return {
|
|
49
|
-
agentArgs: [...beforeSeparator, ...afterSeparator.filter((arg) => arg
|
|
55
|
+
agentArgs: [...beforeSeparator, ...afterSeparator.filter((arg) => !reserved.has(arg))],
|
|
50
56
|
...(skipGit ? { skipGit: true } : {}),
|
|
57
|
+
...(companion ? { scope: "companion" as const } : {}),
|
|
58
|
+
...(skipConfirmation ? { skipConfirmation: true } : {}),
|
|
51
59
|
};
|
|
52
60
|
}
|
|
53
61
|
|
|
@@ -63,17 +71,23 @@ export function parseLaunchArgs(argv: string[]): ParsedLaunchArgs | null {
|
|
|
63
71
|
}
|
|
64
72
|
|
|
65
73
|
const skipGit = agentArgs.includes("--no-git");
|
|
74
|
+
const companion = agentArgs.includes("--companion");
|
|
75
|
+
const skipConfirmation = agentArgs.includes("--yes");
|
|
76
|
+
const reserved = new Set(["--no-git", "--companion", "--yes"]);
|
|
66
77
|
return {
|
|
67
|
-
agentArgs: agentArgs.filter((arg) => arg
|
|
78
|
+
agentArgs: agentArgs.filter((arg) => !reserved.has(arg)),
|
|
68
79
|
...(skipGit ? { skipGit: true } : {}),
|
|
80
|
+
...(companion ? { scope: "companion" as const } : {}),
|
|
81
|
+
...(skipConfirmation ? { skipConfirmation: true } : {}),
|
|
69
82
|
};
|
|
70
83
|
}
|
|
71
84
|
|
|
72
85
|
/**
|
|
73
86
|
* @description Shared launch execution used by `mate claude`, `mate opencode`,
|
|
74
87
|
* and the direct `mate claude` / `mate opencode` commands: resolves the launch via {@link FrameworkLauncher.prepare},
|
|
75
|
-
*
|
|
76
|
-
* `runIndexCapCommand`, then executes
|
|
88
|
+
* confirms with the user in a TTY unless `--yes` was supplied, re-syncs capability indexes via
|
|
89
|
+
* `runIndexCapCommand`, then executes, prints the JSON result, and adopts the
|
|
90
|
+
* agent's outcome as the command's exit status (128 + signal for a signalled stop).
|
|
77
91
|
* @remarks Exits non-zero with a targeted message for `ToolNotAllowedError`
|
|
78
92
|
* (repo policy disallows this tool) and `RepositoryNotSelectedError` (no
|
|
79
93
|
* active repo — points the user at `mate companion link`); other errors propagate.
|
|
@@ -84,6 +98,7 @@ export async function runLaunchToolCommand(
|
|
|
84
98
|
options: {
|
|
85
99
|
skipConfirmation?: boolean;
|
|
86
100
|
skipGit?: boolean;
|
|
101
|
+
scope?: LaunchScope;
|
|
87
102
|
} = {},
|
|
88
103
|
): Promise<void> {
|
|
89
104
|
const interactiveGit = !!(process.stdin.isTTY && process.stdout.isTTY);
|
|
@@ -98,6 +113,7 @@ export async function runLaunchToolCommand(
|
|
|
98
113
|
tool,
|
|
99
114
|
args,
|
|
100
115
|
skipGit: options.skipGit,
|
|
116
|
+
scope: options.scope,
|
|
101
117
|
interactiveGit,
|
|
102
118
|
});
|
|
103
119
|
progress?.succeed("sync");
|
|
@@ -106,7 +122,7 @@ export async function runLaunchToolCommand(
|
|
|
106
122
|
// Ink's line tracking, corrupting later re-renders (e.g. duplicated title).
|
|
107
123
|
progress?.stop();
|
|
108
124
|
|
|
109
|
-
if (!options.skipConfirmation) {
|
|
125
|
+
if (!options.skipConfirmation && process.stdin.isTTY) {
|
|
110
126
|
const ok = await launchCommandDeps.confirm("Continue? [y/N] ");
|
|
111
127
|
if (!ok) {
|
|
112
128
|
process.stderr.write("Aborted.\n");
|
|
@@ -114,13 +130,20 @@ export async function runLaunchToolCommand(
|
|
|
114
130
|
}
|
|
115
131
|
}
|
|
116
132
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
133
|
+
if (options.scope !== "companion") {
|
|
134
|
+
await launchCommandDeps.runIndexCapCommand([], {
|
|
135
|
+
onStepStart: (step) => process.stdout.write(`${STEP_LABELS[step]}...\n`),
|
|
136
|
+
onStepDone: (step, ok) => process.stdout.write(`${ok ? "✓" : "✗"} ${STEP_LABELS[step]}\n`),
|
|
137
|
+
});
|
|
138
|
+
}
|
|
121
139
|
|
|
122
140
|
const result = await prepared.execute();
|
|
141
|
+
/**
|
|
142
|
+
* The printed result stays useful, but the status is what a supervisor
|
|
143
|
+
* reads: a failed agent must fail the launch.
|
|
144
|
+
*/
|
|
123
145
|
console.log(JSON.stringify(result, null, 2));
|
|
146
|
+
if (result.exitCode !== 0) process.exitCode = result.exitCode;
|
|
124
147
|
} catch (error) {
|
|
125
148
|
progress?.failCurrent();
|
|
126
149
|
progress?.stop();
|
|
@@ -153,7 +176,7 @@ export async function runLaunchToolCommand(
|
|
|
153
176
|
/**
|
|
154
177
|
* Builds the `mate <tool>` command handler: parses launch args (direct
|
|
155
178
|
* passthrough or flagged), guards against ambiguous companions, then launches
|
|
156
|
-
* via {@link runLaunchToolCommand}
|
|
179
|
+
* via {@link runLaunchToolCommand}; non-interactive launches skip confirmation.
|
|
157
180
|
*/
|
|
158
181
|
export function makeLaunchCommand(tool: LaunchTarget) {
|
|
159
182
|
return async function runLaunchCommand(
|
|
@@ -163,14 +186,20 @@ export function makeLaunchCommand(tool: LaunchTarget) {
|
|
|
163
186
|
const parsed = options.directPassthrough ? parseDirectLaunchArgs(args) : parseLaunchArgs(args);
|
|
164
187
|
if (!parsed) return;
|
|
165
188
|
|
|
166
|
-
if (
|
|
189
|
+
if (
|
|
190
|
+
!(await ensureUnambiguousCompanion(
|
|
191
|
+
process.cwd(),
|
|
192
|
+
parsed.scope === "companion" ? { persistSelection: false } : {},
|
|
193
|
+
))
|
|
194
|
+
) {
|
|
167
195
|
process.exitCode = 1;
|
|
168
196
|
return;
|
|
169
197
|
}
|
|
170
198
|
|
|
171
199
|
await runLaunchToolCommand(tool, parsed.agentArgs, {
|
|
172
|
-
skipConfirmation:
|
|
200
|
+
skipConfirmation: !process.stdin.isTTY || parsed.skipConfirmation,
|
|
173
201
|
skipGit: parsed.skipGit,
|
|
202
|
+
scope: parsed.scope,
|
|
174
203
|
});
|
|
175
204
|
};
|
|
176
205
|
}
|
|
@@ -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
|
}
|