@uniqbit/mate-core 0.15.5 → 0.16.0-canary.1
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/claude-plugin/.claude-plugin/plugin.json +2 -2
- package/claude-plugin/hooks/hooks.json +3 -6
- package/claude-plugin/hooks/session-guidance.mjs +8 -0
- package/claude-plugin/hooks/ts-loader.mjs +19 -0
- package/package.json +7 -5
- package/src/cli/commands/artifact/artifact.ts +19 -3
- package/src/cli/commands/artifact/finish/command.ts +183 -57
- package/src/cli/commands/artifact/finish/engine.ts +80 -91
- package/src/cli/commands/artifact/finish/finisher.ts +26 -33
- package/src/cli/commands/artifact/finish/git.ts +34 -23
- package/src/cli/commands/artifact/finish/index.ts +9 -3
- package/src/cli/commands/artifact/finish/openspec.ts +225 -97
- package/src/cli/commands/artifact/pending/command.ts +175 -0
- package/src/cli/commands/artifact/pending/discovery.ts +249 -0
- package/src/cli/commands/artifact/pending/index.ts +17 -0
- package/src/cli/commands/cap/index-cmd.ts +9 -1
- package/src/cli/commands/cap/index.ts +2 -6
- package/src/cli/commands/cap/tokensave.ts +4 -4
- package/src/cli/commands/companion/companion.ts +5 -1
- package/src/cli/commands/companion/link.ts +2 -2
- package/src/cli/commands/companion/sync.ts +92 -0
- package/src/cli/commands/doctor.ts +0 -3
- package/src/cli/commands/launch/shared.ts +23 -5
- package/src/cli/commands/report/collector.ts +72 -78
- package/src/cli/commands/report/contract.ts +40 -1
- package/src/cli/commands/report/highlight.ts +27 -0
- package/src/cli/commands/report/index.ts +11 -18
- package/src/cli/commands/report/renderer.ts +199 -2
- package/src/cli/commands/report/types.ts +26 -1
- package/src/cli/commands/shared/companion-selection.ts +107 -10
- package/src/cli/commands/studio/areas.ts +68 -0
- package/src/cli/commands/studio/index.ts +69 -0
- package/src/cli/commands/studio/inventory.ts +55 -0
- package/src/cli/commands/studio/mate-inventory.ts +43 -0
- package/src/cli/commands/studio/openspec-cli.ts +198 -0
- package/src/cli/commands/studio/payload.ts +184 -0
- package/src/cli/commands/studio/routes.ts +2 -0
- package/src/cli/commands/studio/selection.ts +61 -0
- package/src/cli/commands/studio/server.ts +201 -0
- package/src/cli/commands/studio/snapshot.ts +63 -0
- package/src/cli/commands/studio/topology.ts +199 -0
- package/src/cli/commands/studio/views/client.ts +197 -0
- package/src/cli/commands/studio/views/companion-picker.tsx +91 -0
- package/src/cli/commands/studio/views/companion-selector.tsx +90 -0
- package/src/cli/commands/studio/views/dashboard/changes.tsx +107 -0
- package/src/cli/commands/studio/views/dashboard/index.tsx +19 -0
- package/src/cli/commands/studio/views/document.tsx +256 -0
- package/src/cli/commands/studio/views/error.tsx +20 -0
- package/src/cli/commands/studio/views/model.ts +34 -0
- package/src/cli/commands/studio/views/pairings.tsx +38 -0
- package/src/cli/commands/studio/views/skills/index.tsx +87 -0
- package/src/cli/commands/studio/views/specs/index.tsx +101 -0
- package/src/cli/commands/studio/views/styles.ts +389 -0
- package/src/cli/commands/studio/views/warnings.tsx +21 -0
- package/src/cli/commands/studio/views/workflow/index.tsx +21 -0
- package/src/cli/commands/studio/views/workflow/steps.ts +329 -0
- package/src/cli/commands/studio/views/workflow/transcript.tsx +190 -0
- package/src/cli/commands/unwrap.ts +70 -0
- package/src/cli/commands/wrap.ts +164 -0
- package/src/cli/main.ts +68 -19
- package/src/cli/parse-flags.ts +36 -11
- package/src/cli/usage.ts +12 -3
- package/src/framework.ts +1 -7
- package/src/hooks/session-banner.ts +64 -11
- package/src/hooks/session-guidance.ts +40 -0
- package/src/hooks/validate-artifact-path.ts +108 -35
- package/src/lib/fs-utils.ts +9 -0
- package/src/lib/install.ts +33 -0
- package/src/lib/orchestrator/adapters/base.ts +14 -125
- package/src/lib/orchestrator/adapters/claude.ts +0 -11
- package/src/lib/orchestrator/adapters/opencode.ts +2 -32
- package/src/lib/orchestrator/companion-git-sync.ts +94 -84
- package/src/lib/orchestrator/config-store.ts +2 -21
- package/src/lib/orchestrator/editor.ts +12 -22
- package/src/lib/orchestrator/framework-context.ts +17 -6
- package/src/lib/orchestrator/global-config-store.ts +1 -1
- package/src/lib/orchestrator/launcher.ts +97 -7
- package/src/lib/orchestrator/opencode-guidance.ts +4 -56
- package/src/lib/orchestrator/projection-claude-entry.ts +198 -0
- package/src/lib/orchestrator/projection-claude-skills.ts +120 -0
- package/src/lib/orchestrator/projection-companion-link.ts +62 -0
- package/src/lib/orchestrator/projection-entries.ts +377 -0
- package/src/lib/orchestrator/projection-record.ts +56 -0
- package/src/lib/orchestrator/projection-runtime-documents.ts +424 -0
- package/src/lib/orchestrator/projection-types.ts +169 -0
- package/src/lib/orchestrator/repo-local-registry.ts +37 -133
- package/src/lib/orchestrator/repo-local-store.ts +96 -0
- package/src/lib/orchestrator/setup-compatibilities.ts +1 -9
- package/src/lib/orchestrator/types.ts +1 -0
- package/src/lib/orchestrator/working-repo-projection.ts +366 -0
- package/src/lib/orchestrator/workspace-inventory.ts +1 -1
- package/src/lib/package-paths.ts +11 -1
- package/src/lib/public-npm.ts +2 -1
- package/src/lib/update-checker.ts +15 -9
- package/src/opencode/companion-hooks.ts +89 -245
- package/src/opencode/companion-policy.ts +35 -10
- package/src/opencode/index.ts +1 -0
- package/src/opencode/projected-guidance.ts +56 -0
- package/src/opencode/tui.tsx +13 -4
- package/src/playbooks/companion-guidance.ts +32 -116
- package/src/plugins.ts +0 -1
- package/src/runtime/companion-git-state.ts +156 -0
- package/src/runtime/companion-git.ts +203 -0
- package/src/runtime/companion-guidance.ts +222 -0
- package/src/runtime/companion-sync.ts +298 -0
- package/src/runtime/env-names.ts +30 -0
- package/src/runtime/env.ts +67 -35
- package/src/runtime/framework.ts +10 -0
- package/src/runtime/freshness.ts +58 -0
- package/src/runtime/index.ts +104 -0
- package/src/runtime/install.ts +30 -0
- package/src/runtime/policy.ts +66 -0
- package/src/runtime/projected-guidance.ts +45 -0
- package/src/runtime/projection.ts +224 -0
- package/src/runtime/repo-local.ts +64 -0
- package/src/templates/capabilities/openspec-cap/mate-minimal/schema.yaml +56 -0
- package/src/templates/capabilities/openspec-cap/mate-minimal/templates/spec.md +48 -0
- package/src/templates/capabilities/openspec-cap/mate-minimal/templates/tasks.md +22 -0
- package/src/templates/capabilities/openspec-cap/mate-v1/schema.yaml +31 -90
- package/src/templates/capabilities/openspec-cap/mate-v1/templates/design.md +3 -0
- package/src/templates/capabilities/openspec-cap/mate-v1/templates/explore-brief.md +6 -6
- package/src/templates/capabilities/openspec-cap/mate-v1/templates/spec.md +3 -5
- package/src/templates/capabilities/openspec-cap/mate-v1/templates/tasks.md +3 -0
- package/src/templates/capabilities/openspec-cap/openspec-conventions.yaml +30 -0
- package/src/templates/capabilities/react-doctor/claude/hooks/react-doctor.sh +2 -2
- package/src/templates/mate-skills/agents/mate-artifact-publish/SKILL.md +185 -0
- package/src/templates/mate-skills/agents/mate-artifact-publish/references/openspec.md +227 -0
- package/src/templates/mate-skills/agents/mate-domain-modeling/SKILL.md +68 -0
- package/src/templates/mate-skills/agents/mate-domain-modeling/references/ADR-FORMAT.md +9 -0
- package/src/templates/mate-skills/agents/mate-domain-modeling/references/CONTEXT-FORMAT.md +18 -0
- package/src/templates/mate-skills/agents/mate-grill-me/SKILL.md +14 -0
- package/src/templates/mate-skills/agents/mate-grill-with-docs/SKILL.md +29 -0
- package/src/templates/mate-skills/agents/mate-grilling/SKILL.md +49 -0
- package/src/templates/mate-skills/agents/mate-interview-me/SKILL.md +156 -0
- package/src/templates/{capabilities/openspec-cap/mate-skills → mate-skills}/agents/mate-openspec-backfill/SKILL.md +4 -2
- package/src/templates/mate-skills/agents/mate-show-me/SKILL.md +139 -0
- package/src/templates/mate-skills/agents/mate-simplify-code/SKILL.md +503 -0
- package/src/templates/mate-skills/claude/mate-artifact-publish/SKILL.md +185 -0
- package/src/templates/mate-skills/claude/mate-artifact-publish/references/openspec.md +227 -0
- package/src/templates/mate-skills/claude/mate-domain-modeling/SKILL.md +68 -0
- package/src/templates/mate-skills/claude/mate-domain-modeling/references/ADR-FORMAT.md +9 -0
- package/src/templates/mate-skills/claude/mate-domain-modeling/references/CONTEXT-FORMAT.md +18 -0
- package/src/templates/mate-skills/claude/mate-grill-me/SKILL.md +14 -0
- package/src/templates/mate-skills/claude/mate-grill-with-docs/SKILL.md +29 -0
- package/src/templates/mate-skills/claude/mate-grilling/SKILL.md +49 -0
- package/src/templates/mate-skills/claude/mate-interview-me/SKILL.md +156 -0
- package/src/templates/mate-skills/claude/mate-openspec-backfill/SKILL.md +67 -0
- package/src/templates/mate-skills/claude/mate-show-me/SKILL.md +139 -0
- package/src/templates/mate-skills/claude/mate-simplify-code/SKILL.md +503 -0
- package/src/templates/report-assets/README.md +32 -0
- package/src/templates/report-assets/mermaid.LICENSE +21 -0
- package/src/templates/report-assets/mermaid.min.js +4376 -0
- package/src/templates/root/TEMPLATE_CLAUDE.md +1 -9
- package/src/tools/setup/__snapshots__/runtime-surface-golden.test.ts.snap +1476 -197
- package/src/tools/setup/capabilities/graphify.ts +16 -7
- package/src/tools/setup/capabilities/openspec.ts +63 -56
- package/src/tools/setup/capabilities/tokensave.ts +116 -2
- package/src/tools/setup/engine.ts +34 -6
- package/src/tools/setup/mate.ts +42 -13
- package/src/tools/setup/plugin.ts +9 -0
- package/src/tools/setup/plugins/guidance.ts +11 -1
- package/src/tools/setup/providers/claude-format.ts +49 -4
- package/src/tools/setup/providers/claude-plugin-hooks.ts +117 -0
- package/src/tools/setup/providers/claude.ts +55 -220
- package/src/tools/setup/providers/opencode.ts +41 -14
- package/src/tools/setup/runtime-documents.ts +174 -0
- package/src/tools/setup/surface-target.ts +50 -0
- package/src/tools/setup/working-repo-cleanup.ts +33 -26
- package/src/tools/setup/working-repo-local-state.ts +21 -1
- package/src/tools/setup.ts +25 -3
- package/wrappers/bin/graphify +57 -8
- package/wrappers/bin/openspec +50 -3
- package/claude-plugin/hooks/artifact-finish-nudge.mjs +0 -8
- package/src/cli/commands/cap/headroom.ts +0 -52
- package/src/cli/commands/workspace/list.ts +0 -25
- package/src/cli/commands/workspace/materialize.ts +0 -46
- package/src/cli/commands/workspace/workspace.ts +0 -22
- package/src/hooks/artifact-finish-nudge.ts +0 -244
- package/src/lib/orchestrator/headroom/proxy.ts +0 -116
- package/src/lib/orchestrator/workspace-materialize.ts +0 -80
- package/src/templates/capabilities/openspec-cap/mate-skills/agents/mate-artifact-finish/SKILL.md +0 -51
- package/src/templates/capabilities/openspec-cap/mate-skills/agents/mate-artifact-finish/references/openspec.md +0 -134
- package/src/templates/capabilities/openspec-cap/mate-skills/claude/mate-artifact-finish/SKILL.md +0 -58
- package/src/templates/capabilities/openspec-cap/mate-skills/claude/mate-artifact-finish/references/openspec.md +0 -139
- package/src/tools/setup/capabilities/headroom.ts +0 -57
- /package/src/cli/commands/{workspace → companion}/open.ts +0 -0
package/wrappers/bin/openspec
CHANGED
|
@@ -1,6 +1,53 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
-
[ -n "$MATE_ARTIFACT_PATH" ] || { echo "openspec: must be run via mate claude or mate opencode" >&2; exit 1; }
|
|
3
2
|
_this_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
3
|
+
|
|
4
|
+
# Mate context: the launch environment first, the Working Repository's
|
|
5
|
+
# Projection Root second. Projected values stay in private shell variables and
|
|
6
|
+
# no MATE_-prefixed variable is exported: the projection carries paths and
|
|
7
|
+
# never predicates, so a partial MATE_* environment would read to any child as
|
|
8
|
+
# authoritative with every predicate disabled.
|
|
9
|
+
_mate_projection_file() {
|
|
10
|
+
local dir="$PWD"
|
|
11
|
+
while :; do
|
|
12
|
+
if [ -f "$dir/.mate/config/registry.yaml" ]; then
|
|
13
|
+
[ -f "$dir/.mate/projection.env" ] && printf '%s' "$dir/.mate/projection.env"
|
|
14
|
+
return
|
|
15
|
+
fi
|
|
16
|
+
[ "$dir" = "/" ] && return
|
|
17
|
+
dir="$(dirname "$dir")"
|
|
18
|
+
done
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
_mate_projected() {
|
|
22
|
+
sed -n "s/^$2='\\(.*\\)'\$/\\1/p" "$1" | head -n 1 | sed "s/'\\\\''/'/g"
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
_mate_same_dir() {
|
|
26
|
+
local left="$1" right="$2"
|
|
27
|
+
[ -d "$left" ] && left="$(cd "$left" && pwd)"
|
|
28
|
+
[ -d "$right" ] && right="$(cd "$right" && pwd)"
|
|
29
|
+
[ "$left" = "$right" ]
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
_companion="$MATE_ARTIFACT_PATH"
|
|
33
|
+
if [ -z "$_companion" ]; then
|
|
34
|
+
_projection="$(_mate_projection_file)"
|
|
35
|
+
if [ -n "$_projection" ]; then
|
|
36
|
+
_companion="$(_mate_projected "$_projection" MATE_ARTIFACT_PATH)"
|
|
37
|
+
_projected_wrapper_dir="$(_mate_projected "$_projection" MATE_WRAPPER_BIN_PATH)"
|
|
38
|
+
if [ -n "$_companion" ] && ! _mate_same_dir "$_projected_wrapper_dir" "$_this_dir"; then
|
|
39
|
+
{
|
|
40
|
+
echo "openspec: this repository was wrapped by a different mate install."
|
|
41
|
+
echo " projected wrappers: $_projected_wrapper_dir"
|
|
42
|
+
echo " running wrapper: $_this_dir"
|
|
43
|
+
echo " re-run \`mate wrap\` with this install."
|
|
44
|
+
} >&2
|
|
45
|
+
exit 1
|
|
46
|
+
fi
|
|
47
|
+
fi
|
|
48
|
+
fi
|
|
49
|
+
[ -n "$_companion" ] || { echo "openspec: no mate context here — run \`mate wrap\` in this repository" >&2; exit 1; }
|
|
50
|
+
|
|
4
51
|
_real=""
|
|
5
52
|
IFS=:
|
|
6
53
|
for _dir in $PATH; do
|
|
@@ -22,7 +69,7 @@ if [ "$command" = "templates" ]; then
|
|
|
22
69
|
done
|
|
23
70
|
if [ "$has_schema" -eq 0 ]; then
|
|
24
71
|
_schema=""
|
|
25
|
-
for _cfg in "$
|
|
72
|
+
for _cfg in "$_companion/openspec/config.yaml" "$_companion/openspec/config.yml"; do
|
|
26
73
|
[ -f "$_cfg" ] || continue
|
|
27
74
|
while IFS= read -r _line; do
|
|
28
75
|
case "$_line" in
|
|
@@ -45,4 +92,4 @@ if [ "$command" = "templates" ]; then
|
|
|
45
92
|
fi
|
|
46
93
|
fi
|
|
47
94
|
fi
|
|
48
|
-
exec env -C "$
|
|
95
|
+
exec env -C "$_companion" "$_real" "$@"
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// Thin shim: hook logic lives in src/hooks/artifact-finish-nudge.ts, loaded
|
|
3
|
-
// through ts-loader.mjs because the installed plugin sits inside node_modules
|
|
4
|
-
// where node's native type stripping is disabled.
|
|
5
|
-
import "./ts-loader.mjs";
|
|
6
|
-
|
|
7
|
-
const { run } = await import("../../src/hooks/artifact-finish-nudge.ts");
|
|
8
|
-
process.exitCode = await run();
|
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
import { spawnSync } from "node:child_process";
|
|
2
|
-
|
|
3
|
-
import { resolveForCapability } from "../../../lib/orchestrator/framework-context";
|
|
4
|
-
import type { CapabilityConfig } from "../../../lib/orchestrator/types";
|
|
5
|
-
import { ensureUnambiguousCompanion } from "../shared/companion-selection";
|
|
6
|
-
|
|
7
|
-
export interface HeadroomCapDeps {
|
|
8
|
-
ensureUnambiguousCompanion?: (cwd: string) => Promise<boolean>;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* @command mate cap headroom [...args]
|
|
13
|
-
* @description Forwards `[...args]` to the `headroom` CLI, run from the
|
|
14
|
-
* companion directory. Requires the `headroom` capability to be enabled in
|
|
15
|
-
* `.mate/config/framework.yaml`.
|
|
16
|
-
* @remarks The child process's exit code is propagated via `process.exitCode`.
|
|
17
|
-
*/
|
|
18
|
-
export async function runHeadroomCapCommand(
|
|
19
|
-
args: string[],
|
|
20
|
-
deps: HeadroomCapDeps = {},
|
|
21
|
-
): Promise<void> {
|
|
22
|
-
const ensureCompanion = deps.ensureUnambiguousCompanion ?? ensureUnambiguousCompanion;
|
|
23
|
-
if (!(await ensureCompanion(process.cwd()))) {
|
|
24
|
-
process.exitCode = 1;
|
|
25
|
-
return;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
const { companionPath, configStore } = await resolveForCapability(process.cwd());
|
|
29
|
-
const config = await configStore.load();
|
|
30
|
-
|
|
31
|
-
const capability = (config.capabilities ?? []).find(
|
|
32
|
-
(entry: CapabilityConfig) => entry.name === "headroom",
|
|
33
|
-
);
|
|
34
|
-
|
|
35
|
-
if (!capability) {
|
|
36
|
-
process.stderr.write("headroom capability is not enabled in .mate/config/framework.yaml\n");
|
|
37
|
-
process.exitCode = 1;
|
|
38
|
-
return;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
const result = spawnSync("headroom", args, { cwd: companionPath, stdio: "inherit" });
|
|
42
|
-
|
|
43
|
-
if (result.error) {
|
|
44
|
-
process.stderr.write(`Failed to run headroom: ${result.error.message}\n`);
|
|
45
|
-
process.exitCode = 1;
|
|
46
|
-
return;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
if (result.status !== null) {
|
|
50
|
-
process.exitCode = result.status;
|
|
51
|
-
}
|
|
52
|
-
}
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
import { collectWorkspaceInventory } from "../../../lib/orchestrator/workspace-inventory";
|
|
2
|
-
import { writeJsonStdout } from "../../write-json-stdout";
|
|
3
|
-
|
|
4
|
-
export const workspaceListCommandDeps = {
|
|
5
|
-
collectWorkspaceInventory: () => collectWorkspaceInventory(),
|
|
6
|
-
};
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* @command mate workspace list
|
|
10
|
-
* @description Prints the aggregate inventory of registered companions and
|
|
11
|
-
* their linked working-repository pairings as one JSON document. Runs
|
|
12
|
-
* without a linked working repository or any active companion context.
|
|
13
|
-
* @flags
|
|
14
|
-
* - `--json` — required; this command only supports JSON output.
|
|
15
|
-
*/
|
|
16
|
-
export async function runWorkspaceListCommand(argv: string[] = []): Promise<void> {
|
|
17
|
-
if (!argv.includes("--json")) {
|
|
18
|
-
process.stderr.write("mate: `workspace list` requires --json.\n");
|
|
19
|
-
process.exitCode = 1;
|
|
20
|
-
return;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
const inventory = await workspaceListCommandDeps.collectWorkspaceInventory();
|
|
24
|
-
await writeJsonStdout(inventory);
|
|
25
|
-
}
|
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
import { materializeWorkspace } from "../../../lib/orchestrator/workspace-materialize";
|
|
2
|
-
import { writeJsonStdout } from "../../write-json-stdout";
|
|
3
|
-
|
|
4
|
-
export const workspaceMaterializeCommandDeps = {
|
|
5
|
-
materializeWorkspace: (repositoryId: string, companionPath: string) =>
|
|
6
|
-
materializeWorkspace({ repositoryId, companionPath }),
|
|
7
|
-
};
|
|
8
|
-
|
|
9
|
-
function readFlag(argv: string[], name: string): string | undefined {
|
|
10
|
-
const index = argv.indexOf(name);
|
|
11
|
-
return index >= 0 ? argv[index + 1] : undefined;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* @command mate workspace materialize
|
|
16
|
-
* @description Validates an explicit repository/companion pairing, writes
|
|
17
|
-
* the generated `.mate/workspace.code-workspace` file, and prints its path
|
|
18
|
-
* and folder order as one JSON document. Never spawns or focuses an editor.
|
|
19
|
-
* @flags
|
|
20
|
-
* - `--repository <id>` — required; the linked repository id to pair.
|
|
21
|
-
* - `--companion <path>` — required; the companion path that must link it.
|
|
22
|
-
* - `--json` — required; this command only supports JSON output.
|
|
23
|
-
*/
|
|
24
|
-
export async function runWorkspaceMaterializeCommand(argv: string[] = []): Promise<void> {
|
|
25
|
-
const repositoryId = readFlag(argv, "--repository");
|
|
26
|
-
const companionPath = readFlag(argv, "--companion");
|
|
27
|
-
|
|
28
|
-
if (!repositoryId || !companionPath || !argv.includes("--json")) {
|
|
29
|
-
process.stderr.write(
|
|
30
|
-
"mate: `workspace materialize` requires --repository <id>, --companion <path>, and --json.\n",
|
|
31
|
-
);
|
|
32
|
-
process.exitCode = 1;
|
|
33
|
-
return;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
try {
|
|
37
|
-
const result = await workspaceMaterializeCommandDeps.materializeWorkspace(
|
|
38
|
-
repositoryId,
|
|
39
|
-
companionPath,
|
|
40
|
-
);
|
|
41
|
-
await writeJsonStdout(result);
|
|
42
|
-
} catch (error) {
|
|
43
|
-
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
|
|
44
|
-
process.exitCode = 1;
|
|
45
|
-
}
|
|
46
|
-
}
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import { usage } from "../../usage";
|
|
2
|
-
import { runWorkspaceListCommand } from "./list";
|
|
3
|
-
import { runWorkspaceMaterializeCommand } from "./materialize";
|
|
4
|
-
|
|
5
|
-
/** Dispatches `mate workspace <subcommand>`. Distinct from `mate companion open`. */
|
|
6
|
-
export async function runWorkspaceCommand(
|
|
7
|
-
subcommand: string | undefined,
|
|
8
|
-
argv: string[],
|
|
9
|
-
): Promise<void> {
|
|
10
|
-
switch (subcommand) {
|
|
11
|
-
case "list":
|
|
12
|
-
await runWorkspaceListCommand(argv);
|
|
13
|
-
return;
|
|
14
|
-
case "materialize":
|
|
15
|
-
await runWorkspaceMaterializeCommand(argv);
|
|
16
|
-
return;
|
|
17
|
-
default:
|
|
18
|
-
console.error(`Unknown workspace command: ${subcommand ?? ""}`);
|
|
19
|
-
console.error(usage());
|
|
20
|
-
process.exitCode = 1;
|
|
21
|
-
}
|
|
22
|
-
}
|
|
@@ -1,244 +0,0 @@
|
|
|
1
|
-
// Nudge the agent into the mate-artifact-finish workflow after a Bash command
|
|
2
|
-
// visibly archived an OpenSpec change. Stateless PostToolUse hook: never denies
|
|
3
|
-
// or blocks; emits additionalContext only on apparent success.
|
|
4
|
-
import type { HookEnv } from "./validate-artifact-path";
|
|
5
|
-
|
|
6
|
-
export interface NudgeOutcome {
|
|
7
|
-
exitCode: number;
|
|
8
|
-
stdout: string;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
const ARCHIVE_PATH_PATTERN =
|
|
12
|
-
/(?:^|[\s/"'`,(])openspec\/changes\/archive\/(\d{4}-\d{2}-\d{2}-[^/\s"'`,)]+)/;
|
|
13
|
-
|
|
14
|
-
// The nudge only applies when the openspec capability is enabled and OpenSpec
|
|
15
|
-
// git auto mode is on; the plugin registers the hook unconditionally, so the
|
|
16
|
-
// gate is evaluated here from launch-injected environment before any parsing.
|
|
17
|
-
export function isGateEnabled(env: HookEnv): boolean {
|
|
18
|
-
return env.MATE_OPENSPEC_ENABLED === "1" && env.MATE_GIT_AUTO_MODE === "1";
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export function shellSplit(command: string): string[] {
|
|
22
|
-
const parts: string[] = [];
|
|
23
|
-
let current = "";
|
|
24
|
-
let quote: string | null = null;
|
|
25
|
-
let escaping = false;
|
|
26
|
-
for (let index = 0; index < command.length; index += 1) {
|
|
27
|
-
const char = command[index];
|
|
28
|
-
if (escaping) {
|
|
29
|
-
current += char;
|
|
30
|
-
escaping = false;
|
|
31
|
-
continue;
|
|
32
|
-
}
|
|
33
|
-
if (char === "\\" && quote !== "'") {
|
|
34
|
-
const next = command[index + 1] || "";
|
|
35
|
-
if (next === "\\" || /\s/.test(next) || next === "'" || next === '"') {
|
|
36
|
-
escaping = true;
|
|
37
|
-
continue;
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
if ((char === "'" || char === '"') && !quote) {
|
|
41
|
-
quote = char;
|
|
42
|
-
continue;
|
|
43
|
-
}
|
|
44
|
-
if (quote === char) {
|
|
45
|
-
quote = null;
|
|
46
|
-
continue;
|
|
47
|
-
}
|
|
48
|
-
const next = command[index + 1] || "";
|
|
49
|
-
const previous = command[index - 1] || "";
|
|
50
|
-
const isControlOperator =
|
|
51
|
-
!quote &&
|
|
52
|
-
(char === ";" ||
|
|
53
|
-
char === "|" ||
|
|
54
|
-
(char === "&" && next !== ">" && previous !== ">" && previous !== "<"));
|
|
55
|
-
if (isControlOperator) {
|
|
56
|
-
if (current) parts.push(current);
|
|
57
|
-
current = "";
|
|
58
|
-
if ((char === "|" || char === "&") && next === char) {
|
|
59
|
-
parts.push(char + next);
|
|
60
|
-
index += 1;
|
|
61
|
-
} else {
|
|
62
|
-
parts.push(char);
|
|
63
|
-
}
|
|
64
|
-
continue;
|
|
65
|
-
}
|
|
66
|
-
if (!quote && /\s/.test(char)) {
|
|
67
|
-
if (current) parts.push(current);
|
|
68
|
-
current = "";
|
|
69
|
-
continue;
|
|
70
|
-
}
|
|
71
|
-
current += char;
|
|
72
|
-
}
|
|
73
|
-
if (current) parts.push(current);
|
|
74
|
-
return parts;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
function extractChangeFromPath(value: string): string | null {
|
|
78
|
-
const match = value.replaceAll("\\", "/").match(ARCHIVE_PATH_PATTERN);
|
|
79
|
-
return match ? match[1].replace(/^\d{4}-\d{2}-\d{2}-/, "") : null;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
// Best-effort shell variable substitution, NOT a shell interpreter: a change
|
|
83
|
-
// name is often built up through variables (DEST="...archive/$TARGET"; mv a
|
|
84
|
-
// "$DEST") rather than appearing as a literal path next to mv/archive. This
|
|
85
|
-
// resolves $NAME/${NAME} references against preceding literal NAME=value
|
|
86
|
-
// assignment tokens in the same command string; unresolved references are
|
|
87
|
-
// left as-is, which keeps prior (silent) behavior for anything it can't follow.
|
|
88
|
-
const ASSIGNMENT_PATTERN = /^([A-Za-z_][A-Za-z0-9_]*)=([\s\S]*)$/;
|
|
89
|
-
const VARIABLE_REFERENCE_PATTERN = /\$\{([A-Za-z_][A-Za-z0-9_]*)\}|\$([A-Za-z_][A-Za-z0-9_]*)/g;
|
|
90
|
-
|
|
91
|
-
function substituteVariables(value: string, vars: Map<string, string>): string {
|
|
92
|
-
return value.replace(VARIABLE_REFERENCE_PATTERN, (match, braced: string, bare: string) => {
|
|
93
|
-
const name = braced ?? bare;
|
|
94
|
-
return vars.has(name) ? vars.get(name)! : match;
|
|
95
|
-
});
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
// Assignments are collected in left-to-right token order so a later
|
|
99
|
-
// assignment (e.g. DEST) can reference an earlier one (e.g. TARGET) the same
|
|
100
|
-
// way the shell would resolve it at that point in the script.
|
|
101
|
-
function collectVariableAssignments(parts: string[]): Map<string, string> {
|
|
102
|
-
const vars = new Map<string, string>();
|
|
103
|
-
for (const part of parts) {
|
|
104
|
-
const match = part.match(ASSIGNMENT_PATTERN);
|
|
105
|
-
if (!match) continue;
|
|
106
|
-
const [, name, rawValue] = match;
|
|
107
|
-
vars.set(name, substituteVariables(rawValue, vars));
|
|
108
|
-
}
|
|
109
|
-
return vars;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
// Shell syntax tokens are never change-name positionals: separators end the
|
|
113
|
-
// archive invocation, redirections are skipped (a bare operator also consumes
|
|
114
|
-
// its target token).
|
|
115
|
-
const SEPARATOR_PATTERN = /^(\|\|?|&&?|;)$/;
|
|
116
|
-
const REDIRECTION_PATTERN = /^(\d*(>>?|<)|&>>?)/;
|
|
117
|
-
const BARE_REDIRECTION_PATTERN = /^(\d*(>>?|<)|&>>?)$/;
|
|
118
|
-
|
|
119
|
-
export function extractArchiveCommand(command: string): string | null {
|
|
120
|
-
const parts = shellSplit(command);
|
|
121
|
-
const index = parts.findIndex(
|
|
122
|
-
(part, position) =>
|
|
123
|
-
part === "archive" &&
|
|
124
|
-
position > 0 &&
|
|
125
|
-
(parts[position - 1] === "openspec" || parts[position - 1].endsWith("/openspec")),
|
|
126
|
-
);
|
|
127
|
-
if (index < 0) return null;
|
|
128
|
-
const vars = collectVariableAssignments(parts);
|
|
129
|
-
for (let position = index + 1; position < parts.length; position += 1) {
|
|
130
|
-
const part = parts[position];
|
|
131
|
-
if (!part || part === "--") continue;
|
|
132
|
-
if (SEPARATOR_PATTERN.test(part)) return null;
|
|
133
|
-
if (REDIRECTION_PATTERN.test(part)) {
|
|
134
|
-
if (BARE_REDIRECTION_PATTERN.test(part)) position += 1;
|
|
135
|
-
continue;
|
|
136
|
-
}
|
|
137
|
-
if (part.startsWith("-")) {
|
|
138
|
-
if (part === "--store") position += 1;
|
|
139
|
-
continue;
|
|
140
|
-
}
|
|
141
|
-
return substituteVariables(part, vars);
|
|
142
|
-
}
|
|
143
|
-
return null;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
const MOVE_COMMANDS = new Set([
|
|
147
|
-
"mv",
|
|
148
|
-
"move",
|
|
149
|
-
"move-item",
|
|
150
|
-
"cmd",
|
|
151
|
-
"cmd.exe",
|
|
152
|
-
"powershell",
|
|
153
|
-
"powershell.exe",
|
|
154
|
-
"pwsh",
|
|
155
|
-
"pwsh.exe",
|
|
156
|
-
]);
|
|
157
|
-
|
|
158
|
-
function isPython(part: string): boolean {
|
|
159
|
-
return /^python(?:\d(?:\.\d+)*)?(?:\.exe)?$/i.test(part.split(/[\\/]/).pop() || part);
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
export function extractMoveCommand(command: string): string | null {
|
|
163
|
-
const parts = shellSplit(command);
|
|
164
|
-
const vars = collectVariableAssignments(parts);
|
|
165
|
-
for (let index = 0; index < parts.length; index += 1) {
|
|
166
|
-
if (MOVE_COMMANDS.has(parts[index].toLowerCase())) {
|
|
167
|
-
for (const part of parts.slice(index + 1)) {
|
|
168
|
-
const change = extractChangeFromPath(substituteVariables(part, vars));
|
|
169
|
-
if (change) return change;
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
if (isPython(parts[index]) && /(?:shutil\.move|os\.rename|\.rename\s*\()/.test(command)) {
|
|
173
|
-
return extractChangeFromPath(substituteVariables(command, vars));
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
return null;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
// Nudge only on apparent success: stay silent on a clear failure indication,
|
|
180
|
-
// nudge when success is indicated or indeterminate.
|
|
181
|
-
export function isClearFailure(response: unknown): boolean {
|
|
182
|
-
if (!response || typeof response !== "object") return false;
|
|
183
|
-
const value = response as Record<string, unknown>;
|
|
184
|
-
if (value.success === false || value.is_error === true) return true;
|
|
185
|
-
if (typeof value.error === "string" && value.error) return true;
|
|
186
|
-
if (value.interrupted === true) return true;
|
|
187
|
-
const exitCode = value.exit_code ?? value.exitCode;
|
|
188
|
-
return typeof exitCode === "number" && exitCode !== 0;
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
export function evaluate(payload: unknown, env: HookEnv): NudgeOutcome {
|
|
192
|
-
const silent: NudgeOutcome = { exitCode: 0, stdout: "" };
|
|
193
|
-
if (!isGateEnabled(env)) return silent;
|
|
194
|
-
|
|
195
|
-
const input = payload && typeof payload === "object" ? (payload as Record<string, unknown>) : {};
|
|
196
|
-
if (input.hook_event_name !== "PostToolUse" || input.tool_name !== "Bash") return silent;
|
|
197
|
-
|
|
198
|
-
const toolInput =
|
|
199
|
-
input.tool_input && typeof input.tool_input === "object"
|
|
200
|
-
? (input.tool_input as Record<string, unknown>)
|
|
201
|
-
: {};
|
|
202
|
-
const command = typeof toolInput.command === "string" ? toolInput.command : "";
|
|
203
|
-
const change = extractArchiveCommand(command) || extractMoveCommand(command);
|
|
204
|
-
if (!change) return silent;
|
|
205
|
-
if (isClearFailure(input.tool_response)) return silent;
|
|
206
|
-
|
|
207
|
-
const context =
|
|
208
|
-
`OpenSpec change ${change} was just archived. Invoke the mate-artifact-finish ` +
|
|
209
|
-
`skill, then run \`mate artifact finish "${change}" --json\` to complete the ` +
|
|
210
|
-
"finish workflow (commit, tag, and push).";
|
|
211
|
-
return {
|
|
212
|
-
exitCode: 0,
|
|
213
|
-
stdout: JSON.stringify({
|
|
214
|
-
hookSpecificOutput: {
|
|
215
|
-
hookEventName: "PostToolUse",
|
|
216
|
-
additionalContext: context,
|
|
217
|
-
},
|
|
218
|
-
}),
|
|
219
|
-
};
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
async function readStdin(): Promise<string> {
|
|
223
|
-
const chunks: Buffer[] = [];
|
|
224
|
-
for await (const chunk of process.stdin) {
|
|
225
|
-
chunks.push(Buffer.from(chunk));
|
|
226
|
-
}
|
|
227
|
-
return Buffer.concat(chunks).toString("utf8");
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
// Plugin-shim entry. The gate check precedes stdin parsing so a disabled gate
|
|
231
|
-
// costs one env read.
|
|
232
|
-
export async function run(): Promise<number> {
|
|
233
|
-
if (!isGateEnabled(process.env)) return 0;
|
|
234
|
-
|
|
235
|
-
let payload: unknown = {};
|
|
236
|
-
try {
|
|
237
|
-
payload = JSON.parse(await readStdin());
|
|
238
|
-
} catch {
|
|
239
|
-
payload = {};
|
|
240
|
-
}
|
|
241
|
-
const outcome = evaluate(payload, process.env);
|
|
242
|
-
if (outcome.stdout) process.stdout.write(outcome.stdout);
|
|
243
|
-
return outcome.exitCode;
|
|
244
|
-
}
|
|
@@ -1,116 +0,0 @@
|
|
|
1
|
-
import { spawn } from "node:child_process";
|
|
2
|
-
import { closeSync, existsSync, openSync, readFileSync } from "node:fs";
|
|
3
|
-
import http from "node:http";
|
|
4
|
-
import os from "node:os";
|
|
5
|
-
import path from "node:path";
|
|
6
|
-
|
|
7
|
-
export interface SpawnProxyOpts {
|
|
8
|
-
port: number;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export interface WaitForProxyReachableOptions {
|
|
12
|
-
attempts?: number;
|
|
13
|
-
delayMs?: number;
|
|
14
|
-
probe?: (port: number) => Promise<boolean>;
|
|
15
|
-
sleep?: (ms: number) => Promise<void>;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export function resolveHeadroomPort(): number {
|
|
19
|
-
const portStr = process.env.HEADROOM_PORT;
|
|
20
|
-
if (portStr) {
|
|
21
|
-
const parsed = parseInt(portStr, 10);
|
|
22
|
-
if (!isNaN(parsed) && parsed > 0) return parsed;
|
|
23
|
-
}
|
|
24
|
-
return 8787;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export function isProxyReachable(port: number): Promise<boolean> {
|
|
28
|
-
const readyFile = process.env.MATE_E2E_HEADROOM_PROXY_READY_FILE;
|
|
29
|
-
if (readyFile && existsSync(readyFile)) {
|
|
30
|
-
return Promise.resolve(true);
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
return new Promise((resolve) => {
|
|
34
|
-
const req = http.get({ hostname: "127.0.0.1", port, path: "/livez", timeout: 2000 }, (res) => {
|
|
35
|
-
const statusCode = res.statusCode ?? 0;
|
|
36
|
-
resolve(statusCode >= 200 && statusCode < 300);
|
|
37
|
-
res.resume();
|
|
38
|
-
});
|
|
39
|
-
req.on("error", () => resolve(false));
|
|
40
|
-
req.on("timeout", () => {
|
|
41
|
-
req.destroy();
|
|
42
|
-
resolve(false);
|
|
43
|
-
});
|
|
44
|
-
});
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
export async function waitForProxyReachable(
|
|
48
|
-
port: number,
|
|
49
|
-
options: WaitForProxyReachableOptions = {},
|
|
50
|
-
): Promise<boolean> {
|
|
51
|
-
const attempts = options.attempts ?? 20;
|
|
52
|
-
const delayMs = options.delayMs ?? 250;
|
|
53
|
-
const probe = options.probe ?? isProxyReachable;
|
|
54
|
-
const sleep =
|
|
55
|
-
options.sleep ?? ((ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms)));
|
|
56
|
-
|
|
57
|
-
for (let attempt = 0; attempt < attempts; attempt += 1) {
|
|
58
|
-
if (await probe(port)) {
|
|
59
|
-
return true;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
if (attempt < attempts - 1) {
|
|
63
|
-
await sleep(delayMs);
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
return false;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
// Global map to store proxy stderr for error reporting. Keyed by port number.
|
|
71
|
-
const proxyStderr = new Map<number, string>();
|
|
72
|
-
const proxyStderrLogPath = new Map<number, string>();
|
|
73
|
-
|
|
74
|
-
export function spawnProxyBackground(opts: SpawnProxyOpts, spawnFn: typeof spawn = spawn): void {
|
|
75
|
-
// Persistent memory is intentionally not enabled: headroom treats memory as
|
|
76
|
-
// opt-in (`--memory`), so a bare `proxy --port` runs compression-only with no
|
|
77
|
-
// memory DB writes.
|
|
78
|
-
const args = ["proxy", "--port", String(opts.port)];
|
|
79
|
-
|
|
80
|
-
let stderrBuffer = "";
|
|
81
|
-
const stderrLogPath = path.join(os.tmpdir(), `mate-headroom-proxy-${opts.port}.stderr.log`);
|
|
82
|
-
proxyStderrLogPath.set(opts.port, stderrLogPath);
|
|
83
|
-
const stderrFd = openSync(stderrLogPath, "w");
|
|
84
|
-
const child = spawnFn("headroom", args, {
|
|
85
|
-
detached: true,
|
|
86
|
-
stdio: ["ignore", "ignore", stderrFd],
|
|
87
|
-
env: { ...process.env, HEADROOM_OUTPUT_SHAPER: "0", HEADROOM_TELEMETRY: "off" },
|
|
88
|
-
});
|
|
89
|
-
closeSync(stderrFd);
|
|
90
|
-
|
|
91
|
-
// Tests may inject a mocked child.stderr; real detached launches use a log file.
|
|
92
|
-
if (child.stderr) {
|
|
93
|
-
child.stderr.on("data", (chunk) => {
|
|
94
|
-
stderrBuffer += chunk.toString();
|
|
95
|
-
});
|
|
96
|
-
child.stderr.on("end", () => {
|
|
97
|
-
proxyStderr.set(opts.port, stderrBuffer);
|
|
98
|
-
});
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
child.unref();
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
export function getProxyStderr(port: number): string {
|
|
105
|
-
const buffered = proxyStderr.get(port);
|
|
106
|
-
if (buffered) return buffered;
|
|
107
|
-
|
|
108
|
-
const logPath = proxyStderrLogPath.get(port);
|
|
109
|
-
if (!logPath || !existsSync(logPath)) return "";
|
|
110
|
-
|
|
111
|
-
try {
|
|
112
|
-
return readFileSync(logPath, "utf8");
|
|
113
|
-
} catch {
|
|
114
|
-
return "";
|
|
115
|
-
}
|
|
116
|
-
}
|
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-
import path from "node:path";
|
|
2
|
-
|
|
3
|
-
import { readCompanionRegistry } from "./companion-registry-reader";
|
|
4
|
-
import { pathIsDirectory } from "./repo-local-registry";
|
|
5
|
-
import { MateError } from "./types";
|
|
6
|
-
import { writeWorkspaceDocument } from "./editor";
|
|
7
|
-
|
|
8
|
-
/** Envelope version for `mate workspace materialize --json`. */
|
|
9
|
-
export const MATERIALIZED_WORKSPACE_SCHEMA_VERSION = 1;
|
|
10
|
-
|
|
11
|
-
export interface MaterializedWorkspaceV1 {
|
|
12
|
-
schemaVersion: 1;
|
|
13
|
-
workspacePath: string;
|
|
14
|
-
folders: [string, string];
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export class WorkspacePairingNotFoundError extends MateError {
|
|
18
|
-
constructor(repositoryId: string, companionPath: string) {
|
|
19
|
-
super(`mate: repository "${repositoryId}" is not linked by companion: ${companionPath}`);
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export class WorkspaceRootUnavailableError extends MateError {
|
|
24
|
-
constructor(
|
|
25
|
-
readonly root: "companion" | "repository",
|
|
26
|
-
readonly rootPath: string,
|
|
27
|
-
) {
|
|
28
|
-
super(`mate: ${root} path does not exist: ${rootPath}`);
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export interface MaterializeWorkspaceRequest {
|
|
33
|
-
repositoryId: string;
|
|
34
|
-
companionPath: string;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export interface MaterializeWorkspaceDeps {
|
|
38
|
-
isDirectory: (candidatePath: string) => Promise<boolean>;
|
|
39
|
-
readCompanionRegistry: typeof readCompanionRegistry;
|
|
40
|
-
writeWorkspaceDocument: typeof writeWorkspaceDocument;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export function defaultMaterializeWorkspaceDeps(): MaterializeWorkspaceDeps {
|
|
44
|
-
return { isDirectory: pathIsDirectory, readCompanionRegistry, writeWorkspaceDocument };
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* Resolves an explicit companion-path/repository-id pairing, validates both
|
|
49
|
-
* roots exist, and writes the shared workspace document — without touching
|
|
50
|
-
* the registry or spawning an editor. Distinct from `injectEditorFolder`,
|
|
51
|
-
* which additionally launches/reloads an editor CLI.
|
|
52
|
-
*/
|
|
53
|
-
export async function materializeWorkspace(
|
|
54
|
-
request: MaterializeWorkspaceRequest,
|
|
55
|
-
deps: MaterializeWorkspaceDeps = defaultMaterializeWorkspaceDeps(),
|
|
56
|
-
): Promise<MaterializedWorkspaceV1> {
|
|
57
|
-
const companionPath = path.resolve(request.companionPath);
|
|
58
|
-
|
|
59
|
-
if (!(await deps.isDirectory(companionPath))) {
|
|
60
|
-
throw new WorkspaceRootUnavailableError("companion", companionPath);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
const { repos } = await deps.readCompanionRegistry(companionPath).catch(() => ({ repos: [] }));
|
|
64
|
-
const repository = repos.find((repo) => repo.id === request.repositoryId);
|
|
65
|
-
if (!repository) {
|
|
66
|
-
throw new WorkspacePairingNotFoundError(request.repositoryId, companionPath);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
const repositoryPath = path.resolve(repository.path);
|
|
70
|
-
if (!(await deps.isDirectory(repositoryPath))) {
|
|
71
|
-
throw new WorkspaceRootUnavailableError("repository", repositoryPath);
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
const { workspacePath, folders } = await deps.writeWorkspaceDocument(
|
|
75
|
-
companionPath,
|
|
76
|
-
repositoryPath,
|
|
77
|
-
);
|
|
78
|
-
|
|
79
|
-
return { schemaVersion: MATERIALIZED_WORKSPACE_SCHEMA_VERSION, workspacePath, folders };
|
|
80
|
-
}
|