@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.
Files changed (43) hide show
  1. package/package.json +1 -1
  2. package/src/cli/commands/companion/companion.ts +8 -0
  3. package/src/cli/commands/companion/prepare.ts +76 -0
  4. package/src/cli/commands/companion/register.ts +47 -0
  5. package/src/cli/commands/launch/claude.ts +5 -3
  6. package/src/cli/commands/launch/opencode.ts +5 -3
  7. package/src/cli/commands/launch/shared.ts +41 -12
  8. package/src/cli/commands/shared/companion-selection.ts +3 -1
  9. package/src/cli/commands/studio/index.ts +85 -6
  10. package/src/cli/commands/studio/routes.ts +1 -1
  11. package/src/cli/commands/studio/selection.ts +11 -2
  12. package/src/cli/commands/studio/server.ts +212 -17
  13. package/src/cli/commands/studio/vault.ts +607 -0
  14. package/src/cli/commands/studio/views/client.ts +141 -1
  15. package/src/cli/commands/studio/views/companion-selector.tsx +1 -1
  16. package/src/cli/commands/studio/views/document.tsx +136 -3
  17. package/src/cli/commands/studio/views/model.ts +13 -0
  18. package/src/cli/commands/studio/views/styles.ts +16 -0
  19. package/src/cli/main.ts +33 -4
  20. package/src/cli/usage.ts +6 -3
  21. package/src/index.ts +1 -0
  22. package/src/lib/context-mode-package.ts +7 -4
  23. package/src/lib/opencode-plugin-package.ts +10 -4
  24. package/src/lib/orchestrator/adapters/base.ts +65 -10
  25. package/src/lib/orchestrator/adapters/opencode.ts +4 -2
  26. package/src/lib/orchestrator/companion-registration.ts +73 -0
  27. package/src/lib/orchestrator/framework-context.ts +29 -3
  28. package/src/lib/orchestrator/launcher.ts +46 -22
  29. package/src/lib/orchestrator/projection-record.ts +24 -0
  30. package/src/lib/orchestrator/types.ts +11 -0
  31. package/src/lib/prebuilt-workspace-prepare.ts +152 -0
  32. package/src/lib/prebuilt-workspace.ts +236 -0
  33. package/src/lib/preinstalled-plugins.ts +82 -0
  34. package/src/lib/update-checker.ts +39 -0
  35. package/src/opencode/companion-hooks.ts +45 -20
  36. package/src/playbooks/companion-guidance.ts +1 -0
  37. package/src/runtime/companion-guidance.ts +61 -15
  38. package/src/runtime/env.ts +3 -3
  39. package/src/runtime/index.ts +1 -0
  40. package/src/tools/setup/__snapshots__/runtime-surface-golden.test.ts.snap +127 -77
  41. package/src/tools/setup/capabilities/context-mode.ts +5 -0
  42. package/src/tools/setup/plugin.ts +7 -1
  43. 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.16.0",
3
+ "version": "0.17.0-canary.2",
4
4
  "description": "Core framework and plugin APIs for Mate.",
5
5
  "license": "MIT",
6
6
  "files": [
@@ -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. In a
6
- * TTY, launches directly; outside a TTY, launches via
7
- * {@link runLaunchToolCommand} with confirmation.
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. In a
6
- * TTY, launches directly; outside a TTY, launches via
7
- * {@link runLaunchToolCommand} with confirmation.
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 !== "--no-git")],
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 !== "--no-git"),
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
- * optionally confirms with the user, re-syncs capability indexes via
76
- * `runIndexCapCommand`, then executes and prints the JSON result.
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
- 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
- });
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} with confirmation skipped in a TTY.
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 (!(await ensureUnambiguousCompanion())) {
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: !!process.stdin.isTTY,
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 { serveUntilInterrupted, startStudioServer } from "./server";
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, read-only page over the Companion Repositories
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 Takes no arguments and resolves no Repository Link, so it runs from
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.length > 0) {
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
- /** Loopback only: a payload carries absolute companion paths and change names. */
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[] = ["dashboard", "workflow", "specs", "skills"];
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
  }