@uniqbit/mate-core 0.17.0-canary.1 → 0.17.0-canary.11
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 +7 -2
- package/src/cli/commands/artifact/finish/index.ts +0 -9
- package/src/cli/commands/artifact/finish/openspec.ts +2 -2
- package/src/cli/commands/artifact/finish/registry.ts +1 -1
- package/src/cli/commands/artifact/pending/discovery.ts +1 -1
- package/src/cli/commands/artifact/pending/index.ts +0 -15
- package/src/cli/commands/cap/graphify.ts +1 -1
- package/src/cli/commands/companion/hub.ts +2 -1
- package/src/cli/commands/companion/list.ts +2 -1
- package/src/cli/commands/companion/prepare.ts +2 -2
- package/src/cli/commands/doctor.ts +2 -1
- package/src/cli/commands/launch/claude.ts +2 -3
- package/src/cli/commands/launch/opencode.ts +2 -3
- package/src/cli/commands/launch/shared.ts +21 -36
- package/src/cli/commands/report/highlight.ts +1 -4
- package/src/cli/commands/report/renderer.ts +0 -2
- package/src/cli/commands/studio/access.ts +203 -0
- package/src/cli/commands/studio/index.ts +207 -26
- package/src/cli/commands/studio/openspec-cli.ts +1 -1
- package/src/cli/commands/studio/selection.ts +1 -1
- package/src/cli/commands/studio/server.ts +281 -18
- package/src/cli/commands/studio/terminal-assets.ts +50 -0
- package/src/cli/commands/studio/terminal-launch.ts +71 -0
- package/src/cli/commands/studio/terminal.ts +593 -0
- package/src/cli/commands/studio/vault.ts +1 -6
- package/src/cli/commands/studio/views/document.tsx +13 -2
- package/src/cli/commands/studio/views/model.ts +10 -0
- package/src/cli/commands/studio/views/styles.ts +12 -0
- package/src/cli/commands/studio/views/terminal.tsx +232 -0
- package/src/cli/commands/studio/views/workflow/steps.ts +10 -3
- package/src/cli/commands/update.ts +6 -6
- package/src/cli/companion-link-wizard.tsx +1 -1
- package/src/cli/companion-selector.tsx +1 -1
- package/src/cli/launch-selector.tsx +1 -1
- package/src/cli/main.ts +23 -0
- package/src/cli/usage.ts +6 -4
- package/src/cli/write-json-stdout.ts +2 -2
- package/src/lib/components/spinner.tsx +2 -2
- package/src/lib/components/startup-progress.tsx +1 -1
- package/src/lib/context-mode-package.ts +1 -1
- package/src/lib/node-engine-version.ts +15 -0
- package/src/lib/orchestrator/adapters/opencode.ts +23 -0
- package/src/lib/orchestrator/companion-hub.ts +3 -3
- package/src/lib/orchestrator/companion-registry-store.ts +1 -1
- package/src/lib/orchestrator/config-store.ts +2 -2
- package/src/lib/orchestrator/editor.ts +4 -12
- package/src/lib/orchestrator/projection-entries.ts +1 -1
- package/src/lib/orchestrator/projection-record.ts +1 -1
- package/src/lib/orchestrator/projection-types.ts +0 -4
- package/src/lib/orchestrator/repo-local-registry.ts +0 -1
- package/src/lib/orchestrator/root-context.ts +1 -1
- package/src/lib/orchestrator/setup-compatibilities.ts +1 -1
- package/src/lib/orchestrator/setup-preflight.ts +1 -1
- package/src/lib/orchestrator/types.ts +1 -1
- package/src/lib/orchestrator/working-repo-projection.ts +1 -1
- package/src/lib/orchestrator/workspace-inventory.ts +2 -2
- package/src/lib/package-paths.ts +1 -11
- package/src/lib/package-root.ts +43 -0
- package/src/lib/prebuilt-workspace.ts +15 -5
- package/src/lib/preinstalled-plugins.ts +26 -2
- package/src/lib/public-npm.ts +1 -1
- package/src/playbooks/companion-guidance.ts +1 -28
- package/src/runtime/companion-sync.ts +0 -3
- package/src/templates/capabilities/openspec-cap/mate-minimal/schema.yaml +2 -1
- package/src/tools/setup/__snapshots__/runtime-surface-golden.test.ts.snap +43 -43
- package/src/tools/setup/capabilities/context-mode.ts +29 -2
- package/src/tools/setup/capabilities/graphify-shared.ts +3 -3
- package/src/tools/setup/capabilities/graphify.ts +2 -2
- package/src/tools/setup/capabilities/openspec.ts +14 -9
- package/src/tools/setup/capabilities/react-doctor.ts +1 -1
- package/src/tools/setup/capabilities/rtk.ts +2 -2
- package/src/tools/setup/context-services.ts +3 -6
- package/src/tools/setup/mate.ts +1 -1
- package/src/tools/setup/providers/claude-format.ts +1 -1
- package/src/tools/setup/providers/claude.ts +1 -1
- package/src/tools/setup/providers/opencode.ts +0 -2
- package/src/tools/setup/runtime-documents.ts +1 -7
- package/src/tools/setup.ts +0 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uniqbit/mate-core",
|
|
3
|
-
"version": "0.17.0-canary.
|
|
3
|
+
"version": "0.17.0-canary.11",
|
|
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",
|
|
@@ -36,6 +36,11 @@
|
|
|
36
36
|
"@opentui/keymap": "^0.4.5",
|
|
37
37
|
"@opentui/solid": "^0.4.5",
|
|
38
38
|
"@tanstack/highlight": "0.0.10",
|
|
39
|
+
"@xterm/addon-clipboard": "^0.2.0",
|
|
40
|
+
"@xterm/addon-fit": "^0.11.0",
|
|
41
|
+
"@xterm/addon-unicode11": "^0.9.0",
|
|
42
|
+
"@xterm/addon-web-links": "^0.12.0",
|
|
43
|
+
"@xterm/xterm": "^6.0.0",
|
|
39
44
|
"hono": "^4.13.5",
|
|
40
45
|
"ink": "^7.1.0",
|
|
41
46
|
"react": "^19.2.7",
|
|
@@ -1,10 +1 @@
|
|
|
1
1
|
export { runArtifactPublishCommand } from "./command";
|
|
2
|
-
export type { PublishCommandDeps } from "./command";
|
|
3
|
-
export type { FinishResult, FinishStep, FinishStatus } from "./engine";
|
|
4
|
-
export type {
|
|
5
|
-
ArtifactFinisher,
|
|
6
|
-
FinishContext,
|
|
7
|
-
FinisherFactory,
|
|
8
|
-
Produced,
|
|
9
|
-
ResolveResult,
|
|
10
|
-
} from "./finisher";
|
|
@@ -269,7 +269,7 @@ async function resolved(
|
|
|
269
269
|
}
|
|
270
270
|
|
|
271
271
|
/** Tag namespace segregating spec publications from dated change anchors. */
|
|
272
|
-
|
|
272
|
+
const SPEC_TAG_NAMESPACE = "openspec/specs";
|
|
273
273
|
/** Spec ids named individually before the remainder collapses into a count. */
|
|
274
274
|
const SPEC_LABEL_LIMIT = 3;
|
|
275
275
|
|
|
@@ -297,7 +297,7 @@ function specLabel(paths: string[]): string {
|
|
|
297
297
|
}
|
|
298
298
|
|
|
299
299
|
/** Commit subject for a spec publication; it belongs to no change, so it names specs, never an anchor. */
|
|
300
|
-
|
|
300
|
+
function specCommitSubject(paths: string[]): string {
|
|
301
301
|
const ids = specIds(paths);
|
|
302
302
|
const named = ids.slice(0, SPEC_LABEL_LIMIT);
|
|
303
303
|
const remaining = ids.length - named.length;
|
|
@@ -5,7 +5,7 @@ import { openspecFinisher } from "./openspec";
|
|
|
5
5
|
* Artifact-kind → finisher factory. openspec is the only kind for now; a future ADR
|
|
6
6
|
* finisher registers here and is selected via `--type`, with no engine changes.
|
|
7
7
|
*/
|
|
8
|
-
|
|
8
|
+
const FINISHERS: Record<string, FinisherFactory> = {
|
|
9
9
|
openspec: openspecFinisher,
|
|
10
10
|
};
|
|
11
11
|
|
|
@@ -52,7 +52,7 @@ export interface ArchiveEntry {
|
|
|
52
52
|
const ARCHIVE_ANCHOR_PATTERN = /^\d{4}-\d{2}-\d{2}-.+$/;
|
|
53
53
|
const ARCHIVE_DATE_PREFIX = /^\d{4}-\d{2}-\d{2}-/;
|
|
54
54
|
|
|
55
|
-
|
|
55
|
+
const ARCHIVE_RELATIVE_DIR = "openspec/changes/archive";
|
|
56
56
|
export const SPECS_RELATIVE_DIR = "openspec/specs";
|
|
57
57
|
|
|
58
58
|
/** The tag `mate artifact publish` creates, never a recomputed date. */
|
|
@@ -1,17 +1,2 @@
|
|
|
1
1
|
export { runArtifactPendingCommand } from "./command";
|
|
2
2
|
export type { PendingCommandDeps, PendingResult } from "./command";
|
|
3
|
-
export {
|
|
4
|
-
ARCHIVE_RELATIVE_DIR,
|
|
5
|
-
SPECS_RELATIVE_DIR,
|
|
6
|
-
discoverArchives,
|
|
7
|
-
finishMarker,
|
|
8
|
-
pendingArchives,
|
|
9
|
-
unattributedSpecs,
|
|
10
|
-
} from "./discovery";
|
|
11
|
-
export type {
|
|
12
|
-
ArchiveEntry,
|
|
13
|
-
CommitState,
|
|
14
|
-
SpecAttribution,
|
|
15
|
-
UnattributedSpec,
|
|
16
|
-
UncommittedSpecChange,
|
|
17
|
-
} from "./discovery";
|
|
@@ -29,7 +29,7 @@ const GRAPHIFY_PATH_COMMANDS = new Set([
|
|
|
29
29
|
|
|
30
30
|
const GRAPHIFY_GRAPH_COMMANDS = new Set(["query", "path", "explain", "affected", "tree"]);
|
|
31
31
|
|
|
32
|
-
|
|
32
|
+
function deriveGraphifyPaths(companionPath: string, repositoryId: string): GraphifyPaths {
|
|
33
33
|
const graphifyOut = path.join(
|
|
34
34
|
companionPath,
|
|
35
35
|
GRAPHIFY_STORE_SEGMENT,
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
updateHubPlugins,
|
|
9
9
|
} from "../../../lib/orchestrator/companion-hub";
|
|
10
10
|
import { GlobalConfigStore } from "../../../lib/orchestrator/global-config-store";
|
|
11
|
+
import { writeJsonStdout } from "../../write-json-stdout";
|
|
11
12
|
|
|
12
13
|
function positionalArgs(argv: string[]): string[] {
|
|
13
14
|
const values: string[] = [];
|
|
@@ -71,7 +72,7 @@ async function runHubSync(argv: string[]): Promise<void> {
|
|
|
71
72
|
const results = await syncHub(process.cwd());
|
|
72
73
|
const plugins = await updateHubPlugins(process.cwd());
|
|
73
74
|
if (argv.includes("--json")) {
|
|
74
|
-
|
|
75
|
+
await writeJsonStdout({ companions: results, plugins }, { space: 0 });
|
|
75
76
|
return;
|
|
76
77
|
}
|
|
77
78
|
for (const result of results) console.log(`${result.id}: ${result.status} (${result.message})`);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { CompanionStore } from "../../../lib/orchestrator/companion-store";
|
|
2
2
|
import { resolveFrameworkContext } from "../../../lib/orchestrator/framework-context";
|
|
3
3
|
import { renderRepoListTable } from "../../repo-list-table";
|
|
4
|
+
import { writeJsonStdout } from "../../write-json-stdout";
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* @command mate companion list
|
|
@@ -23,7 +24,7 @@ export async function runCompanionListCommand(argv: string[] = []): Promise<void
|
|
|
23
24
|
const forceJson = argv.includes("--json");
|
|
24
25
|
|
|
25
26
|
if (forceJson || !process.stdout.isTTY) {
|
|
26
|
-
|
|
27
|
+
await writeJsonStdout({ repositories });
|
|
27
28
|
return;
|
|
28
29
|
}
|
|
29
30
|
|
|
@@ -3,7 +3,7 @@ import path from "node:path";
|
|
|
3
3
|
import { FRAMEWORK_NAME } from "../../../framework";
|
|
4
4
|
import { preparePrebuiltWorkspace } from "../../../lib/prebuilt-workspace-prepare";
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
const companionPrepareCommandDeps = {
|
|
7
7
|
preparePrebuiltWorkspace,
|
|
8
8
|
};
|
|
9
9
|
|
|
@@ -13,7 +13,7 @@ interface ParsedPrepareArgs {
|
|
|
13
13
|
error?: string;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
function parsePrepareArgs(argv: string[]): ParsedPrepareArgs {
|
|
17
17
|
let bundle: string | undefined;
|
|
18
18
|
const positional: string[] = [];
|
|
19
19
|
|
|
@@ -10,6 +10,7 @@ import { resolveRootContext, type RootContext } from "../../lib/orchestrator/roo
|
|
|
10
10
|
import type { CapabilityConfig, FrameworkConfig, HubMember } from "../../lib/orchestrator/types";
|
|
11
11
|
import { getRequiredPluginDrift } from "../../tools/setup/policy";
|
|
12
12
|
import { resolveCommandOnPath } from "../../tools/setup/utils";
|
|
13
|
+
import { writeJsonStdout } from "../write-json-stdout";
|
|
13
14
|
import { printSection, renderKeyValueTable, renderTable } from "./status";
|
|
14
15
|
|
|
15
16
|
interface DoctorDeps {
|
|
@@ -354,7 +355,7 @@ function renderHumanReport(report: DoctorReport): void {
|
|
|
354
355
|
export async function runDoctorCommand(argv: string[] = [], deps: DoctorDeps = {}): Promise<void> {
|
|
355
356
|
const report = await collectDoctorReport(deps);
|
|
356
357
|
if (argv.includes("--json")) {
|
|
357
|
-
|
|
358
|
+
await writeJsonStdout(report);
|
|
358
359
|
return;
|
|
359
360
|
}
|
|
360
361
|
renderHumanReport(report);
|
|
@@ -3,13 +3,12 @@ import { makeLaunchCommand } from "./shared";
|
|
|
3
3
|
/**
|
|
4
4
|
* @command mate claude [-- ...agentArgs]
|
|
5
5
|
* @description Launches Claude Code for the active working repository, or the
|
|
6
|
-
* Companion Repository with `-- --companion
|
|
7
|
-
* non-interactive launches proceed without it.
|
|
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.
|
|
11
10
|
* - `-- --companion` — launch against the Companion Repository without a Working Repository.
|
|
12
|
-
* - `-- --yes` —
|
|
11
|
+
* - `-- --yes` — accepted as a Mate compatibility no-op; launches are always prompt-free.
|
|
13
12
|
* @remarks When invoked via `directPassthrough` (the top-level `mate claude`
|
|
14
13
|
* alias), all args before `--` are treated as agent args rather than being
|
|
15
14
|
* parsed as launch options.
|
|
@@ -3,13 +3,12 @@ import { makeLaunchCommand } from "./shared";
|
|
|
3
3
|
/**
|
|
4
4
|
* @command mate opencode [-- ...agentArgs]
|
|
5
5
|
* @description Launches OpenCode for the active working repository, or the
|
|
6
|
-
* Companion Repository with `-- --companion
|
|
7
|
-
* non-interactive launches proceed without it.
|
|
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.
|
|
11
10
|
* - `-- --companion` — launch against the Companion Repository without a Working Repository.
|
|
12
|
-
* - `-- --yes` —
|
|
11
|
+
* - `-- --yes` — accepted as a Mate compatibility no-op; launches are always prompt-free.
|
|
13
12
|
* @remarks When invoked via `directPassthrough` (the top-level `mate
|
|
14
13
|
* opencode` alias), all args before `--` are treated as agent args rather
|
|
15
14
|
* than being parsed as launch options.
|
|
@@ -9,8 +9,8 @@ import {
|
|
|
9
9
|
import { createStartupProgress } from "../../../lib/components/startup-progress";
|
|
10
10
|
import { ensureUnambiguousCompanion, launchAmbiguityDeps } from "../shared/companion-selection";
|
|
11
11
|
import { runIndexCapCommand } from "../cap/index-cmd";
|
|
12
|
-
import { confirm } from "../../confirm";
|
|
13
12
|
import type { LaunchTarget } from "../../launch-selector";
|
|
13
|
+
import { writeJsonStdout } from "../../write-json-stdout";
|
|
14
14
|
|
|
15
15
|
const STEP_LABELS = {
|
|
16
16
|
sync: "Syncing mate",
|
|
@@ -22,7 +22,19 @@ export interface ParsedLaunchArgs {
|
|
|
22
22
|
agentArgs: string[];
|
|
23
23
|
skipGit?: boolean;
|
|
24
24
|
scope?: LaunchScope;
|
|
25
|
-
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const MATE_LAUNCH_OPTIONS = new Set(["--no-git", "--companion", "--yes"]);
|
|
28
|
+
|
|
29
|
+
function parseMateLaunchArgs(agentArgs: string[]): ParsedLaunchArgs {
|
|
30
|
+
const skipGit = agentArgs.includes("--no-git");
|
|
31
|
+
const companion = agentArgs.includes("--companion");
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
agentArgs: agentArgs.filter((arg) => !MATE_LAUNCH_OPTIONS.has(arg)),
|
|
35
|
+
...(skipGit ? { skipGit: true } : {}),
|
|
36
|
+
...(companion ? { scope: "companion" as const } : {}),
|
|
37
|
+
};
|
|
26
38
|
}
|
|
27
39
|
|
|
28
40
|
export { ensureUnambiguousCompanion, launchAmbiguityDeps };
|
|
@@ -31,14 +43,12 @@ export interface LaunchCommandDeps {
|
|
|
31
43
|
createLauncher: () => Pick<FrameworkLauncher, "prepare">;
|
|
32
44
|
createProgress: typeof createStartupProgress;
|
|
33
45
|
runIndexCapCommand: typeof runIndexCapCommand;
|
|
34
|
-
confirm: typeof confirm;
|
|
35
46
|
}
|
|
36
47
|
|
|
37
48
|
export const launchCommandDeps: LaunchCommandDeps = {
|
|
38
49
|
createLauncher: () => new FrameworkLauncher(),
|
|
39
50
|
createProgress: createStartupProgress,
|
|
40
51
|
runIndexCapCommand,
|
|
41
|
-
confirm,
|
|
42
52
|
};
|
|
43
53
|
|
|
44
54
|
export function parseDirectLaunchArgs(argv: string[]): ParsedLaunchArgs {
|
|
@@ -47,15 +57,10 @@ export function parseDirectLaunchArgs(argv: string[]): ParsedLaunchArgs {
|
|
|
47
57
|
|
|
48
58
|
const beforeSeparator = argv.slice(0, separatorIndex);
|
|
49
59
|
const afterSeparator = argv.slice(separatorIndex + 1);
|
|
50
|
-
const
|
|
51
|
-
const companion = afterSeparator.includes("--companion");
|
|
52
|
-
const skipConfirmation = afterSeparator.includes("--yes");
|
|
53
|
-
const reserved = new Set(["--no-git", "--companion", "--yes"]);
|
|
60
|
+
const parsed = parseMateLaunchArgs(afterSeparator);
|
|
54
61
|
return {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
...(companion ? { scope: "companion" as const } : {}),
|
|
58
|
-
...(skipConfirmation ? { skipConfirmation: true } : {}),
|
|
62
|
+
...parsed,
|
|
63
|
+
agentArgs: [...beforeSeparator, ...parsed.agentArgs],
|
|
59
64
|
};
|
|
60
65
|
}
|
|
61
66
|
|
|
@@ -70,23 +75,13 @@ export function parseLaunchArgs(argv: string[]): ParsedLaunchArgs | null {
|
|
|
70
75
|
return null;
|
|
71
76
|
}
|
|
72
77
|
|
|
73
|
-
|
|
74
|
-
const companion = agentArgs.includes("--companion");
|
|
75
|
-
const skipConfirmation = agentArgs.includes("--yes");
|
|
76
|
-
const reserved = new Set(["--no-git", "--companion", "--yes"]);
|
|
77
|
-
return {
|
|
78
|
-
agentArgs: agentArgs.filter((arg) => !reserved.has(arg)),
|
|
79
|
-
...(skipGit ? { skipGit: true } : {}),
|
|
80
|
-
...(companion ? { scope: "companion" as const } : {}),
|
|
81
|
-
...(skipConfirmation ? { skipConfirmation: true } : {}),
|
|
82
|
-
};
|
|
78
|
+
return parseMateLaunchArgs(agentArgs);
|
|
83
79
|
}
|
|
84
80
|
|
|
85
81
|
/**
|
|
86
82
|
* @description Shared launch execution used by `mate claude`, `mate opencode`,
|
|
87
83
|
* and the direct `mate claude` / `mate opencode` commands: resolves the launch via {@link FrameworkLauncher.prepare},
|
|
88
|
-
*
|
|
89
|
-
* `runIndexCapCommand`, then executes, prints the JSON result, and adopts the
|
|
84
|
+
* re-syncs capability indexes via `runIndexCapCommand`, then executes, prints the JSON result, and adopts the
|
|
90
85
|
* agent's outcome as the command's exit status (128 + signal for a signalled stop).
|
|
91
86
|
* @remarks Exits non-zero with a targeted message for `ToolNotAllowedError`
|
|
92
87
|
* (repo policy disallows this tool) and `RepositoryNotSelectedError` (no
|
|
@@ -96,7 +91,6 @@ export async function runLaunchToolCommand(
|
|
|
96
91
|
tool: LaunchTarget,
|
|
97
92
|
args: string[],
|
|
98
93
|
options: {
|
|
99
|
-
skipConfirmation?: boolean;
|
|
100
94
|
skipGit?: boolean;
|
|
101
95
|
scope?: LaunchScope;
|
|
102
96
|
} = {},
|
|
@@ -122,14 +116,6 @@ export async function runLaunchToolCommand(
|
|
|
122
116
|
// Ink's line tracking, corrupting later re-renders (e.g. duplicated title).
|
|
123
117
|
progress?.stop();
|
|
124
118
|
|
|
125
|
-
if (!options.skipConfirmation && process.stdin.isTTY) {
|
|
126
|
-
const ok = await launchCommandDeps.confirm("Continue? [y/N] ");
|
|
127
|
-
if (!ok) {
|
|
128
|
-
process.stderr.write("Aborted.\n");
|
|
129
|
-
process.exit(1);
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
|
|
133
119
|
if (options.scope !== "companion") {
|
|
134
120
|
await launchCommandDeps.runIndexCapCommand([], {
|
|
135
121
|
onStepStart: (step) => process.stdout.write(`${STEP_LABELS[step]}...\n`),
|
|
@@ -142,7 +128,7 @@ export async function runLaunchToolCommand(
|
|
|
142
128
|
* The printed result stays useful, but the status is what a supervisor
|
|
143
129
|
* reads: a failed agent must fail the launch.
|
|
144
130
|
*/
|
|
145
|
-
|
|
131
|
+
await writeJsonStdout(result);
|
|
146
132
|
if (result.exitCode !== 0) process.exitCode = result.exitCode;
|
|
147
133
|
} catch (error) {
|
|
148
134
|
progress?.failCurrent();
|
|
@@ -176,7 +162,7 @@ export async function runLaunchToolCommand(
|
|
|
176
162
|
/**
|
|
177
163
|
* Builds the `mate <tool>` command handler: parses launch args (direct
|
|
178
164
|
* passthrough or flagged), guards against ambiguous companions, then launches
|
|
179
|
-
* via {@link runLaunchToolCommand}
|
|
165
|
+
* via {@link runLaunchToolCommand} without an interactive confirmation.
|
|
180
166
|
*/
|
|
181
167
|
export function makeLaunchCommand(tool: LaunchTarget) {
|
|
182
168
|
return async function runLaunchCommand(
|
|
@@ -197,7 +183,6 @@ export function makeLaunchCommand(tool: LaunchTarget) {
|
|
|
197
183
|
}
|
|
198
184
|
|
|
199
185
|
await runLaunchToolCommand(tool, parsed.agentArgs, {
|
|
200
|
-
skipConfirmation: !process.stdin.isTTY || parsed.skipConfirmation,
|
|
201
186
|
skipGit: parsed.skipGit,
|
|
202
187
|
scope: parsed.scope,
|
|
203
188
|
});
|
|
@@ -4,10 +4,7 @@ import githubLight from "@tanstack/highlight/themes/github-light";
|
|
|
4
4
|
|
|
5
5
|
/** Sole boundary against `@tanstack/highlight`, so an upstream API break stays a single-file fix. */
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
export const PLAIN_LANGUAGE = "plaintext";
|
|
9
|
-
|
|
10
|
-
export function resolveHighlightLanguage(candidate: string | undefined): string {
|
|
7
|
+
function resolveHighlightLanguage(candidate: string | undefined): string {
|
|
11
8
|
return normalizeLanguage(candidate);
|
|
12
9
|
}
|
|
13
10
|
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import { createHash, randomBytes, timingSafeEqual } from "node:crypto";
|
|
2
|
+
|
|
3
|
+
/** Shortest operator-supplied token accepted; a generated one is 43 characters. */
|
|
4
|
+
export const MIN_TOKEN_LENGTH = 32;
|
|
5
|
+
|
|
6
|
+
const LOOPBACK_HOSTNAMES = ["localhost", "127.0.0.1", "[::1]"];
|
|
7
|
+
|
|
8
|
+
export type StudioInvocation = "interactive" | "serve";
|
|
9
|
+
|
|
10
|
+
export interface StudioAccessOptions {
|
|
11
|
+
invocation: StudioInvocation;
|
|
12
|
+
terminal: boolean;
|
|
13
|
+
/** Bound interface as passed to the server. */
|
|
14
|
+
hostname: string;
|
|
15
|
+
/** Read at request time: the port is known only once the server has bound. */
|
|
16
|
+
port: () => number;
|
|
17
|
+
allowedHosts?: readonly string[];
|
|
18
|
+
publicOrigin?: string | null;
|
|
19
|
+
token?: string | null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export type StudioRequirement = "none" | "token";
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* One server's Host, Origin, and token policy. `null` token means the server
|
|
26
|
+
* is unguarded and issues none.
|
|
27
|
+
*/
|
|
28
|
+
export interface StudioAccess {
|
|
29
|
+
invocation: StudioInvocation;
|
|
30
|
+
guarded: boolean;
|
|
31
|
+
token: string | null;
|
|
32
|
+
cookieName(): string;
|
|
33
|
+
/** Accepted origin for this request, or `null` when its Host is not one Studio answers to. */
|
|
34
|
+
acceptedOrigin(request: Request): string | null;
|
|
35
|
+
originMatches(request: Request, acceptedOrigin: string): boolean;
|
|
36
|
+
hasToken(request: Request): boolean;
|
|
37
|
+
/** Whether a request of this kind needs the token under this invocation. */
|
|
38
|
+
requires(kind: "read" | "save" | "terminal"): boolean;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function generateStudioToken(): string {
|
|
42
|
+
return randomBytes(32).toString("base64url");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function validateStudioToken(value: string): string | null {
|
|
46
|
+
if (value.trim().length < MIN_TOKEN_LENGTH) {
|
|
47
|
+
return `the supplied token must be at least ${MIN_TOKEN_LENGTH} characters`;
|
|
48
|
+
}
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** `null` for anything other than an exact `http(s)://host[:port]` origin. */
|
|
53
|
+
export function normalizeOrigin(value: string): string | null {
|
|
54
|
+
let parsed: URL;
|
|
55
|
+
try {
|
|
56
|
+
parsed = new URL(value);
|
|
57
|
+
} catch {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return null;
|
|
61
|
+
if (parsed.username || parsed.password) return null;
|
|
62
|
+
if (parsed.pathname !== "/" || parsed.search || parsed.hash || value.endsWith("/")) return null;
|
|
63
|
+
return parsed.origin;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Lower-cased `host[:port]` with the HTTP default port dropped, or `null` when unparsable. */
|
|
67
|
+
export function normalizeAuthority(value: string): string | null {
|
|
68
|
+
const trimmed = value.trim();
|
|
69
|
+
if (!trimmed || /[\s/?#@\\]/.test(trimmed)) return null;
|
|
70
|
+
try {
|
|
71
|
+
const parsed = new URL(`http://${trimmed}`);
|
|
72
|
+
return parsed.host;
|
|
73
|
+
} catch {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function bracketed(hostname: string): string {
|
|
79
|
+
return hostname.includes(":") && !hostname.startsWith("[") ? `[${hostname}]` : hostname;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function digest(value: string): Buffer {
|
|
83
|
+
return createHash("sha256").update(value).digest();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Hashing first keeps the comparison constant-time for inputs of any length. */
|
|
87
|
+
export function tokensEqual(candidate: string, expected: string): boolean {
|
|
88
|
+
return timingSafeEqual(digest(candidate), digest(expected));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function readCookie(request: Request, name: string): string | null {
|
|
92
|
+
const header = request.headers.get("cookie");
|
|
93
|
+
if (!header) return null;
|
|
94
|
+
for (const part of header.split(";")) {
|
|
95
|
+
const index = part.indexOf("=");
|
|
96
|
+
if (index < 0) continue;
|
|
97
|
+
if (part.slice(0, index).trim() === name) return part.slice(index + 1).trim();
|
|
98
|
+
}
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function createStudioAccess(options: StudioAccessOptions): StudioAccess {
|
|
103
|
+
const guarded = options.invocation === "serve" || options.terminal;
|
|
104
|
+
const token = guarded ? (options.token ?? generateStudioToken()) : null;
|
|
105
|
+
const publicOrigin = options.publicOrigin ? normalizeOrigin(options.publicOrigin) : null;
|
|
106
|
+
const publicAuthority = publicOrigin ? new URL(publicOrigin).host : null;
|
|
107
|
+
const configured = new Set<string>();
|
|
108
|
+
for (const host of options.allowedHosts ?? []) {
|
|
109
|
+
const authority = normalizeAuthority(host);
|
|
110
|
+
if (authority) configured.add(authority);
|
|
111
|
+
}
|
|
112
|
+
if (publicAuthority) configured.add(publicAuthority);
|
|
113
|
+
|
|
114
|
+
const accepted = (): Set<string> => {
|
|
115
|
+
const port = options.port();
|
|
116
|
+
const hosts = new Set(configured);
|
|
117
|
+
for (const hostname of [...LOOPBACK_HOSTNAMES, bracketed(options.hostname)]) {
|
|
118
|
+
const authority = normalizeAuthority(`${hostname}:${port}`);
|
|
119
|
+
if (authority) hosts.add(authority);
|
|
120
|
+
}
|
|
121
|
+
return hosts;
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
return {
|
|
125
|
+
invocation: options.invocation,
|
|
126
|
+
guarded,
|
|
127
|
+
token,
|
|
128
|
+
cookieName: () => `mate_studio_${options.port()}`,
|
|
129
|
+
acceptedOrigin(request) {
|
|
130
|
+
const raw = request.headers.get("host") ?? new URL(request.url).host;
|
|
131
|
+
const authority = normalizeAuthority(raw);
|
|
132
|
+
if (!authority || !accepted().has(authority)) return null;
|
|
133
|
+
if (publicOrigin && authority === publicAuthority) return publicOrigin;
|
|
134
|
+
return `http://${authority}`;
|
|
135
|
+
},
|
|
136
|
+
originMatches(request, acceptedOrigin) {
|
|
137
|
+
const origin = request.headers.get("origin");
|
|
138
|
+
if (!origin || origin === "null") return false;
|
|
139
|
+
return normalizeOrigin(origin) === acceptedOrigin;
|
|
140
|
+
},
|
|
141
|
+
hasToken(request) {
|
|
142
|
+
if (!token) return false;
|
|
143
|
+
const value = readCookie(request, `mate_studio_${options.port()}`);
|
|
144
|
+
return value !== null && tokensEqual(value, token);
|
|
145
|
+
},
|
|
146
|
+
requires(kind) {
|
|
147
|
+
if (!guarded) return false;
|
|
148
|
+
if (options.invocation === "serve") return true;
|
|
149
|
+
return kind !== "read";
|
|
150
|
+
},
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const EXCHANGE_HEADERS = {
|
|
155
|
+
"cache-control": "no-store",
|
|
156
|
+
"referrer-policy": "no-referrer",
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Trades a `?token=` navigation for the cookie and a relative redirect, so the
|
|
161
|
+
* browser never leaves the Host it asked for. `null` when the request carries
|
|
162
|
+
* no token parameter.
|
|
163
|
+
*/
|
|
164
|
+
export function exchangeStudioToken(
|
|
165
|
+
request: Request,
|
|
166
|
+
access: StudioAccess,
|
|
167
|
+
acceptedOrigin: string,
|
|
168
|
+
): Response | null {
|
|
169
|
+
const url = new URL(request.url);
|
|
170
|
+
if (!url.searchParams.has("token")) return null;
|
|
171
|
+
if (request.method !== "GET" || !access.token) {
|
|
172
|
+
return new Response("the access token is exchanged only by opening its address", {
|
|
173
|
+
status: 400,
|
|
174
|
+
headers: EXCHANGE_HEADERS,
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
const candidate = url.searchParams.get("token") ?? "";
|
|
178
|
+
if (!tokensEqual(candidate, access.token)) {
|
|
179
|
+
return new Response("the access token does not match; open the address Studio printed", {
|
|
180
|
+
status: 403,
|
|
181
|
+
headers: EXCHANGE_HEADERS,
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
url.searchParams.delete("token");
|
|
185
|
+
const secure = acceptedOrigin.startsWith("https:") ? "; Secure" : "";
|
|
186
|
+
return new Response(null, {
|
|
187
|
+
status: 303,
|
|
188
|
+
headers: {
|
|
189
|
+
...EXCHANGE_HEADERS,
|
|
190
|
+
location: `${url.pathname}${url.search}`,
|
|
191
|
+
"set-cookie": `${access.cookieName()}=${access.token}; Path=/; HttpOnly; SameSite=Strict${secure}`,
|
|
192
|
+
},
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** Address the operator opens: loopback for wildcard or loopback binds, carrying the token only when it was generated. */
|
|
197
|
+
export function studioAddress(hostname: string, port: number, token: string | null): string {
|
|
198
|
+
const wildcard = hostname === "0.0.0.0" || hostname === "::" || hostname === "[::]";
|
|
199
|
+
const loopback = ["127.0.0.1", "localhost", "::1", "[::1]"].includes(hostname);
|
|
200
|
+
const host = wildcard || loopback ? "localhost" : bracketed(hostname);
|
|
201
|
+
const base = `http://${host}:${port}`;
|
|
202
|
+
return token ? `${base}/?token=${token}` : base;
|
|
203
|
+
}
|