@uniqbit/mate-core 0.18.1-canary.0 → 0.18.1-canary.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/claude-plugin/hooks/session-banner.mjs +1 -1
- package/opencode-plugin/server.ts +5 -0
- package/opencode-plugin/src/add-dir.ts +118 -0
- package/opencode-plugin/src/companion.ts +180 -0
- package/opencode-plugin/src/server.ts +26 -0
- package/opencode-plugin/src/tui.tsx +1 -0
- package/opencode-plugin/tui.tsx +2 -0
- package/package.json +13 -6
- package/src/cli/commands/artifact/finish/engine.ts +1 -1
- package/src/cli/commands/artifact/finish/git.ts +75 -51
- package/src/cli/commands/companion/hub.ts +1 -1
- package/src/cli/commands/companion/link.ts +13 -59
- package/src/cli/commands/doctor.ts +30 -4
- package/src/cli/commands/install.ts +17 -1
- package/src/cli/commands/plugin/plugin.ts +0 -4
- package/src/cli/commands/report/formatter.ts +0 -4
- package/src/cli/commands/studio/selection.ts +3 -0
- package/src/cli/commands/studio/terminal-launch.ts +1 -51
- package/src/cli/commands/studio/terminal.ts +2 -13
- package/src/cli/commands/studio/vault.ts +5 -3
- package/src/cli/commands/studio/views/workflow/steps.ts +5 -4
- package/src/cli/commands/update.ts +4 -20
- package/src/cli/commands/working/cleanup.ts +4 -1
- package/src/cli/launch-selector.tsx +0 -318
- package/src/cli/main.ts +0 -29
- package/src/cli/setup-selector.tsx +1 -1
- package/src/cli/usage.ts +0 -1
- package/src/hooks/session-banner.ts +7 -5
- package/src/hooks/validate-artifact-path.ts +17 -6
- package/src/lib/opencode-plugin-package.ts +7 -28
- package/src/lib/orchestrator/adapters/opencode.ts +28 -67
- package/src/lib/orchestrator/companion-git-sync.ts +29 -93
- package/src/lib/orchestrator/companion-hub.ts +71 -110
- package/src/lib/orchestrator/repo-local-registry.ts +0 -21
- package/src/lib/orchestrator/types.ts +0 -6
- package/src/lib/orchestrator/working-repo-projection.ts +13 -9
- package/src/lib/package-paths.ts +32 -0
- package/src/lib/prebuilt-workspace.ts +0 -3
- package/src/opencode/companion-hooks.ts +15 -10
- package/src/runtime/companion-git.ts +327 -84
- package/src/runtime/companion-sync.ts +34 -43
- package/src/runtime/index.ts +7 -5
- package/src/templates/mate-skills/agents/mate-domain-modeling/SKILL.md +2 -2
- package/src/templates/mate-skills/agents/mate-grill-me/SKILL.md +25 -0
- package/src/templates/mate-skills/agents/mate-grilling/SKILL.md +9 -7
- package/src/templates/mate-skills/agents/mate-interview-me/SKILL.md +34 -4
- package/src/templates/mate-skills/claude/mate-domain-modeling/SKILL.md +2 -2
- package/src/templates/mate-skills/claude/mate-grill-me/SKILL.md +25 -0
- package/src/templates/mate-skills/claude/mate-grilling/SKILL.md +9 -7
- package/src/templates/mate-skills/claude/mate-interview-me/SKILL.md +34 -4
- package/src/tools/setup/__snapshots__/runtime-surface-golden.test.ts.snap +68 -77
- package/src/tools/setup/capabilities/graphify.ts +0 -4
- package/src/tools/setup/dynamic-plugins/frozen.ts +24 -3
- package/src/tools/setup/dynamic-plugins/loader.ts +16 -0
- package/src/tools/setup/dynamic-plugins/policy.ts +55 -0
- package/src/tools/setup/dynamic-plugins/staging.ts +55 -0
- package/src/tools/setup/dynamic-plugins/verify.ts +22 -18
- package/src/tools/setup/providers/opencode-format.ts +4 -0
- package/src/tools/setup/providers/opencode.ts +9 -65
- package/src/tools/setup/runtime-documents.ts +8 -9
- package/src/cli/commands/plugin/verify.ts +0 -29
|
@@ -1,320 +1,2 @@
|
|
|
1
|
-
import { Box, Text, render as inkRender } from "ink";
|
|
2
|
-
import React, { useEffect } from "react";
|
|
3
|
-
import { SelectMenu, type SelectMenuItem } from "../lib/components/select-menu";
|
|
4
|
-
import { WizardFooter } from "../lib/components/wizard-footer";
|
|
5
|
-
import { WizardHeader } from "../lib/components/wizard-header";
|
|
6
|
-
|
|
7
1
|
export const LAUNCH_TARGETS = ["claude", "opencode"] as const;
|
|
8
2
|
export type LaunchTarget = (typeof LAUNCH_TARGETS)[number];
|
|
9
|
-
|
|
10
|
-
export interface LaunchPreviewData {
|
|
11
|
-
repositoryId: string;
|
|
12
|
-
repositoryPath: string;
|
|
13
|
-
companionPath: string;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export interface LaunchWizardResult {
|
|
17
|
-
target: LaunchTarget;
|
|
18
|
-
preview: LaunchPreviewData;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
type WizardStep = "target" | "review";
|
|
22
|
-
type PreviewState =
|
|
23
|
-
| { status: "idle" }
|
|
24
|
-
| { status: "loading" }
|
|
25
|
-
| { status: "ready"; data: LaunchPreviewData }
|
|
26
|
-
| { status: "error"; message: string };
|
|
27
|
-
|
|
28
|
-
interface LaunchWizardAppProps {
|
|
29
|
-
activeIndex: number;
|
|
30
|
-
hasPreselectedTarget: boolean;
|
|
31
|
-
items: SelectMenuItem[];
|
|
32
|
-
previewState: PreviewState;
|
|
33
|
-
selectedTarget: LaunchTarget | null;
|
|
34
|
-
step: WizardStep;
|
|
35
|
-
totalSteps: number;
|
|
36
|
-
onMounted?: () => void;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export interface LaunchWizardOptions {
|
|
40
|
-
stdin?: NodeJS.ReadStream;
|
|
41
|
-
stdout?: NodeJS.WriteStream;
|
|
42
|
-
stderr?: NodeJS.WriteStream;
|
|
43
|
-
allowedTargets?: Set<string>;
|
|
44
|
-
preselectedTarget?: LaunchTarget;
|
|
45
|
-
resolvePreview(target: LaunchTarget): Promise<LaunchPreviewData>;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
const launchSelectorDeps = {
|
|
49
|
-
render: inkRender,
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
const TARGET_ITEMS: SelectMenuItem[] = [
|
|
53
|
-
{ key: "claude", label: "claude" },
|
|
54
|
-
{ key: "opencode", label: "opencode" },
|
|
55
|
-
];
|
|
56
|
-
|
|
57
|
-
function parseLaunchSelectorInput(
|
|
58
|
-
input: string,
|
|
59
|
-
): "submit" | "cancel" | "up" | "down" | "back" | null {
|
|
60
|
-
if (input === "\r" || input === "\n") return "submit";
|
|
61
|
-
if (input === "\x1b[A" || input === "[A") return "up";
|
|
62
|
-
if (input === "\x1b[B" || input === "[B") return "down";
|
|
63
|
-
if (input === "\x7f" || input === "\b") return "back";
|
|
64
|
-
if (input === "\x1b" || input.toLowerCase() === "q") return "cancel";
|
|
65
|
-
return null;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
function ReviewPanel({
|
|
69
|
-
previewState,
|
|
70
|
-
selectedTarget,
|
|
71
|
-
}: {
|
|
72
|
-
previewState: PreviewState;
|
|
73
|
-
selectedTarget: LaunchTarget | null;
|
|
74
|
-
}) {
|
|
75
|
-
if (previewState.status === "loading" || previewState.status === "idle") {
|
|
76
|
-
return <Text dimColor>Resolving launch preview…</Text>;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
if (previewState.status === "error") {
|
|
80
|
-
return <Text color="red">{previewState.message}</Text>;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
return (
|
|
84
|
-
<Box flexDirection="column">
|
|
85
|
-
<Text>Target: {selectedTarget}</Text>
|
|
86
|
-
<Text>Repository: {previewState.data.repositoryId}</Text>
|
|
87
|
-
<Text dimColor>{previewState.data.repositoryPath}</Text>
|
|
88
|
-
<Text>Companion:</Text>
|
|
89
|
-
<Text dimColor>{previewState.data.companionPath}</Text>
|
|
90
|
-
</Box>
|
|
91
|
-
);
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
function LaunchWizardApp({
|
|
95
|
-
activeIndex,
|
|
96
|
-
hasPreselectedTarget,
|
|
97
|
-
items,
|
|
98
|
-
previewState,
|
|
99
|
-
selectedTarget,
|
|
100
|
-
step,
|
|
101
|
-
totalSteps,
|
|
102
|
-
onMounted,
|
|
103
|
-
}: LaunchWizardAppProps): React.JSX.Element {
|
|
104
|
-
useEffect(() => {
|
|
105
|
-
onMounted?.();
|
|
106
|
-
}, [onMounted]);
|
|
107
|
-
|
|
108
|
-
const stepNumber = hasPreselectedTarget ? 0 : step === "target" ? 0 : totalSteps - 1;
|
|
109
|
-
const title = step === "target" ? "Select launch target" : "Review launch";
|
|
110
|
-
const hint =
|
|
111
|
-
step === "target"
|
|
112
|
-
? "↑↓ navigate · enter continue · q cancel"
|
|
113
|
-
: previewState.status === "error"
|
|
114
|
-
? "enter retry · backspace previous · q cancel"
|
|
115
|
-
: previewState.status === "ready"
|
|
116
|
-
? "enter launch · backspace previous · q cancel"
|
|
117
|
-
: "backspace previous · q cancel";
|
|
118
|
-
|
|
119
|
-
return (
|
|
120
|
-
<Box flexDirection="column" width="100%">
|
|
121
|
-
<WizardHeader title={title} step={stepNumber} totalSteps={totalSteps} />
|
|
122
|
-
<Box marginTop={1} flexDirection="column">
|
|
123
|
-
{step === "target" ? <SelectMenu items={items} activeIndex={activeIndex} /> : null}
|
|
124
|
-
{step === "review" ? (
|
|
125
|
-
<ReviewPanel previewState={previewState} selectedTarget={selectedTarget} />
|
|
126
|
-
) : null}
|
|
127
|
-
</Box>
|
|
128
|
-
<WizardFooter hint={hint} />
|
|
129
|
-
</Box>
|
|
130
|
-
);
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
export async function runLaunchWizard(
|
|
134
|
-
options: LaunchWizardOptions,
|
|
135
|
-
): Promise<LaunchWizardResult | null> {
|
|
136
|
-
const stdin = options.stdin ?? process.stdin;
|
|
137
|
-
const stdout = options.stdout ?? process.stdout;
|
|
138
|
-
const stderr = options.stderr ?? process.stderr;
|
|
139
|
-
if (!stdin.isTTY || !stdout.isTTY) {
|
|
140
|
-
throw new Error("Launch wizard requires a TTY.");
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
const allowedTargets = options.allowedTargets;
|
|
144
|
-
const preselectedTarget = options.preselectedTarget ?? null;
|
|
145
|
-
const items: SelectMenuItem[] = TARGET_ITEMS.map((item) => ({
|
|
146
|
-
...item,
|
|
147
|
-
dimColor: allowedTargets ? !allowedTargets.has(item.key) : false,
|
|
148
|
-
}));
|
|
149
|
-
const firstEnabledIndex = items.findIndex((item) => !item.dimColor);
|
|
150
|
-
let activeIndex = preselectedTarget ? 0 : firstEnabledIndex < 0 ? 0 : firstEnabledIndex;
|
|
151
|
-
let step: WizardStep = preselectedTarget ? "review" : "target";
|
|
152
|
-
let selectedTarget: LaunchTarget | null = preselectedTarget;
|
|
153
|
-
let previewState: PreviewState = { status: "idle" };
|
|
154
|
-
let previewRequestId = 0;
|
|
155
|
-
let finalResult: LaunchWizardResult | null = null;
|
|
156
|
-
let resolved = false;
|
|
157
|
-
let rendered: ReturnType<typeof launchSelectorDeps.render> | undefined;
|
|
158
|
-
|
|
159
|
-
const totalSteps = preselectedTarget ? 1 : 2;
|
|
160
|
-
|
|
161
|
-
stdin.resume();
|
|
162
|
-
(stdin as NodeJS.ReadStream & { ref?: () => void }).ref?.();
|
|
163
|
-
const keepAlive = setInterval(() => {}, 1 << 30);
|
|
164
|
-
|
|
165
|
-
const cleanup = () => {
|
|
166
|
-
clearInterval(keepAlive);
|
|
167
|
-
stdin.off("data", onData);
|
|
168
|
-
stdin.pause();
|
|
169
|
-
stdin.setRawMode?.(false);
|
|
170
|
-
(stdin as NodeJS.ReadStream & { unref?: () => void }).unref?.();
|
|
171
|
-
};
|
|
172
|
-
|
|
173
|
-
const currentTarget = () => LAUNCH_TARGETS[activeIndex];
|
|
174
|
-
const isEnabled = (index: number) => !items[index]?.dimColor;
|
|
175
|
-
const nextEnabledIndex = (start: number, direction: 1 | -1): number => {
|
|
176
|
-
if (!items.some((item) => !item.dimColor)) return start;
|
|
177
|
-
let index = start;
|
|
178
|
-
for (let i = 0; i < items.length; i += 1) {
|
|
179
|
-
index = (index + direction + items.length) % items.length;
|
|
180
|
-
if (isEnabled(index)) return index;
|
|
181
|
-
}
|
|
182
|
-
return start;
|
|
183
|
-
};
|
|
184
|
-
|
|
185
|
-
const rerender = () => {
|
|
186
|
-
rendered?.rerender(
|
|
187
|
-
<LaunchWizardApp
|
|
188
|
-
activeIndex={activeIndex}
|
|
189
|
-
hasPreselectedTarget={!!preselectedTarget}
|
|
190
|
-
items={items}
|
|
191
|
-
previewState={previewState}
|
|
192
|
-
selectedTarget={selectedTarget}
|
|
193
|
-
step={step}
|
|
194
|
-
totalSteps={totalSteps}
|
|
195
|
-
onMounted={onMounted}
|
|
196
|
-
/>,
|
|
197
|
-
);
|
|
198
|
-
};
|
|
199
|
-
|
|
200
|
-
const settle = (
|
|
201
|
-
value: LaunchWizardResult | null,
|
|
202
|
-
resolve: (value: LaunchWizardResult | null) => void,
|
|
203
|
-
) => {
|
|
204
|
-
if (resolved) return;
|
|
205
|
-
resolved = true;
|
|
206
|
-
cleanup();
|
|
207
|
-
resolve(value);
|
|
208
|
-
};
|
|
209
|
-
|
|
210
|
-
const loadPreview = () => {
|
|
211
|
-
if (!selectedTarget) return;
|
|
212
|
-
const requestId = ++previewRequestId;
|
|
213
|
-
previewState = { status: "loading" };
|
|
214
|
-
rerender();
|
|
215
|
-
|
|
216
|
-
options
|
|
217
|
-
.resolvePreview(selectedTarget)
|
|
218
|
-
.then((preview) => {
|
|
219
|
-
if (requestId !== previewRequestId || resolved) return;
|
|
220
|
-
previewState = { status: "ready", data: preview };
|
|
221
|
-
rerender();
|
|
222
|
-
})
|
|
223
|
-
.catch((error: unknown) => {
|
|
224
|
-
if (requestId !== previewRequestId || resolved) return;
|
|
225
|
-
previewState = {
|
|
226
|
-
status: "error",
|
|
227
|
-
message: error instanceof Error ? error.message : String(error),
|
|
228
|
-
};
|
|
229
|
-
rerender();
|
|
230
|
-
});
|
|
231
|
-
};
|
|
232
|
-
|
|
233
|
-
const enterReview = () => {
|
|
234
|
-
step = "review";
|
|
235
|
-
loadPreview();
|
|
236
|
-
};
|
|
237
|
-
|
|
238
|
-
const onMounted = () => {
|
|
239
|
-
const buffered = stdin.read();
|
|
240
|
-
if (buffered) {
|
|
241
|
-
// Discard buffered input before the wizard becomes interactive.
|
|
242
|
-
}
|
|
243
|
-
stdin.on("data", onData);
|
|
244
|
-
if (step === "review") {
|
|
245
|
-
loadPreview();
|
|
246
|
-
}
|
|
247
|
-
};
|
|
248
|
-
|
|
249
|
-
const onData = (chunk: string) => {
|
|
250
|
-
const action = parseLaunchSelectorInput(chunk);
|
|
251
|
-
if (!action) return;
|
|
252
|
-
if (action === "cancel") {
|
|
253
|
-
finalResult = null;
|
|
254
|
-
rendered?.unmount();
|
|
255
|
-
return;
|
|
256
|
-
}
|
|
257
|
-
if (action === "back") {
|
|
258
|
-
if (step === "review") {
|
|
259
|
-
if (preselectedTarget) {
|
|
260
|
-
finalResult = null;
|
|
261
|
-
rendered?.unmount();
|
|
262
|
-
} else {
|
|
263
|
-
step = "target";
|
|
264
|
-
previewState = { status: "idle" };
|
|
265
|
-
rerender();
|
|
266
|
-
}
|
|
267
|
-
return;
|
|
268
|
-
}
|
|
269
|
-
return;
|
|
270
|
-
}
|
|
271
|
-
if (step === "review") {
|
|
272
|
-
if (action !== "submit") return;
|
|
273
|
-
if (previewState.status === "error") {
|
|
274
|
-
loadPreview();
|
|
275
|
-
return;
|
|
276
|
-
}
|
|
277
|
-
if (previewState.status !== "ready" || !selectedTarget) return;
|
|
278
|
-
finalResult = {
|
|
279
|
-
target: selectedTarget,
|
|
280
|
-
preview: previewState.data,
|
|
281
|
-
};
|
|
282
|
-
rendered?.unmount();
|
|
283
|
-
return;
|
|
284
|
-
}
|
|
285
|
-
if (action === "up" || action === "down") {
|
|
286
|
-
activeIndex = nextEnabledIndex(activeIndex, action === "up" ? -1 : 1);
|
|
287
|
-
rerender();
|
|
288
|
-
return;
|
|
289
|
-
}
|
|
290
|
-
if (action !== "submit") return;
|
|
291
|
-
if (!isEnabled(activeIndex)) return;
|
|
292
|
-
selectedTarget = currentTarget();
|
|
293
|
-
enterReview();
|
|
294
|
-
};
|
|
295
|
-
|
|
296
|
-
stdin.setEncoding("utf8");
|
|
297
|
-
stdin.setRawMode?.(true);
|
|
298
|
-
|
|
299
|
-
return new Promise<LaunchWizardResult | null>((resolve) => {
|
|
300
|
-
rendered = launchSelectorDeps.render(
|
|
301
|
-
<LaunchWizardApp
|
|
302
|
-
activeIndex={activeIndex}
|
|
303
|
-
hasPreselectedTarget={!!preselectedTarget}
|
|
304
|
-
items={items}
|
|
305
|
-
previewState={previewState}
|
|
306
|
-
selectedTarget={selectedTarget}
|
|
307
|
-
step={step}
|
|
308
|
-
totalSteps={totalSteps}
|
|
309
|
-
onMounted={onMounted}
|
|
310
|
-
/>,
|
|
311
|
-
{ stdin, stdout, stderr },
|
|
312
|
-
);
|
|
313
|
-
|
|
314
|
-
const finish = (value: LaunchWizardResult | null) => settle(value, resolve);
|
|
315
|
-
|
|
316
|
-
rendered.waitUntilExit().then(() => {
|
|
317
|
-
finish(finalResult);
|
|
318
|
-
});
|
|
319
|
-
});
|
|
320
|
-
}
|
package/src/cli/main.ts
CHANGED
|
@@ -200,12 +200,6 @@ export async function main(argv = process.argv, deps: MainDeps = mainDeps): Prom
|
|
|
200
200
|
case "prepare":
|
|
201
201
|
if (!(await gate({ updateGuard: true }))) return;
|
|
202
202
|
break;
|
|
203
|
-
// Hub commands establish and operate on a local hub root directly;
|
|
204
|
-
// they must not require a linked working repository or installation —
|
|
205
|
-
// but companions are never hubs.
|
|
206
|
-
case "hub":
|
|
207
|
-
if (!(await gate({ updateGuard: true, notCompanionRoot: true }))) return;
|
|
208
|
-
break;
|
|
209
203
|
// open/tui/sync/update consume a companion context.
|
|
210
204
|
case "open":
|
|
211
205
|
case "tui":
|
|
@@ -252,29 +246,6 @@ export async function main(argv = process.argv, deps: MainDeps = mainDeps): Prom
|
|
|
252
246
|
return;
|
|
253
247
|
await runLaunchOpenCodeCommand(argv.slice(3), { directPassthrough: true });
|
|
254
248
|
return;
|
|
255
|
-
case "launch": {
|
|
256
|
-
const launchArgs = argv.slice(4);
|
|
257
|
-
const companion = parseDirectLaunchArgs(launchArgs).scope === "companion";
|
|
258
|
-
if (subcommand !== "claude" && subcommand !== "opencode") {
|
|
259
|
-
console.error(`${FRAMEWORK_NAME}: expected launch claude or launch opencode.`);
|
|
260
|
-
process.exitCode = 1;
|
|
261
|
-
return;
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
const launchCommand =
|
|
265
|
-
subcommand === "claude" ? runLaunchClaudeCommand : runLaunchOpenCodeCommand;
|
|
266
|
-
if (
|
|
267
|
-
!(await gate({
|
|
268
|
-
updateGuard: true,
|
|
269
|
-
companion: companion ? { persistSelection: false } : true,
|
|
270
|
-
install: true,
|
|
271
|
-
}))
|
|
272
|
-
)
|
|
273
|
-
return;
|
|
274
|
-
|
|
275
|
-
await launchCommand(launchArgs, { directPassthrough: true });
|
|
276
|
-
return;
|
|
277
|
-
}
|
|
278
249
|
case "report":
|
|
279
250
|
if (!(await gate({ updateGuard: true, install: true }))) return;
|
|
280
251
|
await runReportCommand(argv.slice(3));
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Box, Text, render as inkRender } from "ink";
|
|
2
|
-
import
|
|
2
|
+
import { useEffect } from "react";
|
|
3
3
|
|
|
4
4
|
import { SelectMenu, type SelectMenuItem } from "../lib/components/select-menu";
|
|
5
5
|
import { WizardFooter } from "../lib/components/wizard-footer";
|
package/src/cli/usage.ts
CHANGED
|
@@ -42,7 +42,6 @@ export function usage(): string {
|
|
|
42
42
|
` ${n} cap graphify <subcommand> [args...]`,
|
|
43
43
|
` ${n} cap index [--graphify] [--tokensave]`,
|
|
44
44
|
` ${n} plugin install <package>[@version]`,
|
|
45
|
-
` ${n} plugin verify`,
|
|
46
45
|
...pluginCliCommandLines(),
|
|
47
46
|
` ${n} update`,
|
|
48
47
|
` ${n} update --check`,
|
|
@@ -30,12 +30,12 @@ export const PROJECTED_BANNER_FLAG = "--projected";
|
|
|
30
30
|
* With `mate doctor` out of scope this is one of only two surfaces where a
|
|
31
31
|
* drifted wrap becomes visible, so staleness is reported, never suppressed.
|
|
32
32
|
*/
|
|
33
|
-
export function buildBanner(
|
|
33
|
+
export async function buildBanner(
|
|
34
34
|
env: HookEnv,
|
|
35
35
|
cwd: string = process.cwd(),
|
|
36
36
|
argv: readonly string[] = [],
|
|
37
37
|
syncOptions: UnattendedSyncOptions = {},
|
|
38
|
-
): BannerOutcome {
|
|
38
|
+
): Promise<BannerOutcome> {
|
|
39
39
|
if (argv.includes(PROJECTED_BANNER_FLAG) && hasLaunchEnvironment(env)) {
|
|
40
40
|
return { exitCode: 0, stdout: "" };
|
|
41
41
|
}
|
|
@@ -52,7 +52,9 @@ export function buildBanner(
|
|
|
52
52
|
*/
|
|
53
53
|
const gitNotes = hasLaunchEnvironment(env)
|
|
54
54
|
? []
|
|
55
|
-
: unattendedSyncStalenessLines(
|
|
55
|
+
: unattendedSyncStalenessLines(
|
|
56
|
+
await syncCompanionUnattended(context.companionPath, syncOptions),
|
|
57
|
+
);
|
|
56
58
|
|
|
57
59
|
const lines = [
|
|
58
60
|
`mate v${env.MATE_VERSION || mateVersion()}`,
|
|
@@ -71,8 +73,8 @@ export function buildBanner(
|
|
|
71
73
|
}
|
|
72
74
|
|
|
73
75
|
// Plugin-shim entry.
|
|
74
|
-
export function run(argv: readonly string[] = process.argv.slice(2)): number {
|
|
75
|
-
const outcome = buildBanner(process.env, process.cwd(), argv);
|
|
76
|
+
export async function run(argv: readonly string[] = process.argv.slice(2)): Promise<number> {
|
|
77
|
+
const outcome = await buildBanner(process.env, process.cwd(), argv);
|
|
76
78
|
if (outcome.stdout) process.stdout.write(outcome.stdout);
|
|
77
79
|
return outcome.exitCode;
|
|
78
80
|
}
|
|
@@ -12,6 +12,7 @@ import fs from "node:fs";
|
|
|
12
12
|
import os from "node:os";
|
|
13
13
|
import path from "node:path";
|
|
14
14
|
|
|
15
|
+
import { gitEnvironment } from "../runtime/companion-git";
|
|
15
16
|
import { companionForkRefusal } from "../runtime/companion-sync";
|
|
16
17
|
import { readCompanionRuntimeContext } from "../runtime/env";
|
|
17
18
|
import { companionLinkPath } from "../runtime/repo-local";
|
|
@@ -143,6 +144,7 @@ function isGitignored(target: string, ctx: GuardContext): boolean {
|
|
|
143
144
|
const relPath = path.relative(root, target);
|
|
144
145
|
const result = spawnSync("git", ["-C", root, "check-ignore", "--no-index", "-q", "--", relPath], {
|
|
145
146
|
stdio: "ignore",
|
|
147
|
+
env: gitEnvironment(),
|
|
146
148
|
});
|
|
147
149
|
return result.status === 0;
|
|
148
150
|
}
|
|
@@ -156,6 +158,7 @@ function isGitTracked(target: string, ctx: GuardContext): boolean {
|
|
|
156
158
|
const relPath = path.relative(root, target);
|
|
157
159
|
const result = spawnSync("git", ["-C", root, "ls-files", "--error-unmatch", "--", relPath], {
|
|
158
160
|
stdio: "ignore",
|
|
161
|
+
env: gitEnvironment(),
|
|
159
162
|
});
|
|
160
163
|
return result.status === 0;
|
|
161
164
|
}
|
|
@@ -226,11 +229,14 @@ function checkFilePath(
|
|
|
226
229
|
* gated on the absence of a launch environment, and the path refusal keeps its
|
|
227
230
|
* input-only verdict and its double-load tolerance untouched.
|
|
228
231
|
*/
|
|
229
|
-
function checkForkedCompanion(
|
|
232
|
+
async function checkForkedCompanion(
|
|
233
|
+
filePath: string,
|
|
234
|
+
ctx: GuardContext,
|
|
235
|
+
): Promise<HookOutcome | null> {
|
|
230
236
|
if (!filePath || !artifactLikePath(filePath)) return null;
|
|
231
237
|
if (!isCompanionPath(normalizePath(filePath, ctx), ctx)) return null;
|
|
232
238
|
|
|
233
|
-
const refusal = companionForkRefusal(ctx.env, ctx.companion);
|
|
239
|
+
const refusal = await companionForkRefusal(ctx.env, ctx.companion);
|
|
234
240
|
return refusal ? { exitCode: 2, stderr: `${refusal}\n` } : null;
|
|
235
241
|
}
|
|
236
242
|
|
|
@@ -250,7 +256,11 @@ export function commandMatches(command: string): string[] {
|
|
|
250
256
|
* Fails open only when no Mate context resolves from either source; in a
|
|
251
257
|
* wrapped Working Repository the guard runs without a launch.
|
|
252
258
|
*/
|
|
253
|
-
export function evaluate(
|
|
259
|
+
export async function evaluate(
|
|
260
|
+
payload: unknown,
|
|
261
|
+
env: HookEnv,
|
|
262
|
+
cwd: string = process.cwd(),
|
|
263
|
+
): Promise<HookOutcome> {
|
|
254
264
|
const allow: HookOutcome = { exitCode: 0, stderr: "" };
|
|
255
265
|
|
|
256
266
|
const ctx = guardContext(env, cwd);
|
|
@@ -267,7 +277,7 @@ export function evaluate(payload: unknown, env: HookEnv, cwd: string = process.c
|
|
|
267
277
|
const filePath = String(toolInput.file_path ?? "");
|
|
268
278
|
/** The path refusal wins when both apply: its message is not replaced. */
|
|
269
279
|
const outcome = checkFilePath(filePath, ctx, toolName === "Edit" || toolName === "MultiEdit");
|
|
270
|
-
return outcome ?? checkForkedCompanion(filePath, ctx) ?? allow;
|
|
280
|
+
return outcome ?? (await checkForkedCompanion(filePath, ctx)) ?? allow;
|
|
271
281
|
}
|
|
272
282
|
|
|
273
283
|
if (toolName === "Bash") {
|
|
@@ -278,7 +288,8 @@ export function evaluate(payload: unknown, env: HookEnv, cwd: string = process.c
|
|
|
278
288
|
if (outcome) return outcome;
|
|
279
289
|
}
|
|
280
290
|
for (const target of targets) {
|
|
281
|
-
|
|
291
|
+
// oxlint-disable-next-line no-await-in-loop -- the first refusal wins
|
|
292
|
+
const outcome = await checkForkedCompanion(target, ctx);
|
|
282
293
|
if (outcome) return outcome;
|
|
283
294
|
}
|
|
284
295
|
return allow;
|
|
@@ -303,7 +314,7 @@ export async function run(): Promise<number> {
|
|
|
303
314
|
} catch {
|
|
304
315
|
payload = {};
|
|
305
316
|
}
|
|
306
|
-
const outcome = evaluate(payload, process.env);
|
|
317
|
+
const outcome = await evaluate(payload, process.env);
|
|
307
318
|
if (outcome.stderr) process.stderr.write(outcome.stderr);
|
|
308
319
|
return outcome.exitCode;
|
|
309
320
|
}
|
|
@@ -5,24 +5,18 @@ import path from "node:path";
|
|
|
5
5
|
|
|
6
6
|
import { isPreinstalledPluginPath } from "./preinstalled-plugins";
|
|
7
7
|
import { PUBLIC_NPM_REGISTRY } from "./public-npm";
|
|
8
|
-
import { getCurrentVersion } from "./update-checker";
|
|
9
8
|
|
|
10
|
-
|
|
9
|
+
/** The retired plugin package, bundled into mate-core as `opencode-plugin/`. */
|
|
10
|
+
const OPENCODE_PLUGIN_PACKAGE_NAME = "@uniqbit/mate-opencode-plugin";
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* compatibility range.
|
|
13
|
+
* Matches a reference to the retired plugin package: the published spec,
|
|
14
|
+
* a versioned spec, or a path bound to an installed copy. Only for stripping
|
|
15
|
+
* entries older releases wrote into committed config.
|
|
17
16
|
*/
|
|
18
|
-
export function
|
|
19
|
-
return `${OPENCODE_PLUGIN_PACKAGE_NAME}@${version}`;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/** Matches the published reference and a reference bound to an installed copy alike. */
|
|
23
|
-
export function isMateOpenCodePluginReference(value: unknown): boolean {
|
|
17
|
+
export function isLegacyMateOpenCodePluginReference(value: unknown): boolean {
|
|
24
18
|
if (typeof value === "object" && value !== null && !Array.isArray(value) && "package" in value) {
|
|
25
|
-
return
|
|
19
|
+
return isLegacyMateOpenCodePluginReference(value.package);
|
|
26
20
|
}
|
|
27
21
|
if (typeof value !== "string") return false;
|
|
28
22
|
if (
|
|
@@ -85,18 +79,3 @@ export async function warmOpenCodePackageCache(
|
|
|
85
79
|
return { ok: false, detail: (error as Error).message };
|
|
86
80
|
}
|
|
87
81
|
}
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* Pre-fetch the pinned plugin package into OpenCode's npm plugin environment
|
|
91
|
-
* (`<cache>/packages/<spec>/`), mirroring OpenCode's own on-demand install
|
|
92
|
-
* layout, so the first managed launch after a setup or update does not depend
|
|
93
|
-
* on registry access. Best effort: failures are reported, never thrown.
|
|
94
|
-
*/
|
|
95
|
-
export async function warmOpenCodePluginCache(
|
|
96
|
-
version: string = getCurrentVersion(),
|
|
97
|
-
env: NodeJS.ProcessEnv = process.env,
|
|
98
|
-
registry = PUBLIC_NPM_REGISTRY,
|
|
99
|
-
): Promise<WarmPluginCacheResult> {
|
|
100
|
-
const reference = getOpenCodePluginPackageReference(version);
|
|
101
|
-
return warmOpenCodePackageCache(OPENCODE_PLUGIN_PACKAGE_NAME, reference, env, registry);
|
|
102
|
-
}
|
|
@@ -7,14 +7,8 @@ const path = await import("node:path");
|
|
|
7
7
|
import { MATE_ENV } from "../../../runtime/env";
|
|
8
8
|
import { validateGuidanceData, type MateGuidanceFile } from "../../../runtime/guidance";
|
|
9
9
|
|
|
10
|
+
import { getOpenCodePluginRoot, validateOpenCodePluginAssets } from "../../package-paths";
|
|
10
11
|
import {
|
|
11
|
-
getOpenCodePluginPackageReference,
|
|
12
|
-
isMateOpenCodePluginReference,
|
|
13
|
-
OPENCODE_PLUGIN_PACKAGE_NAME,
|
|
14
|
-
} from "../../opencode-plugin-package";
|
|
15
|
-
import { installedVersionAt, isPreinstalledPluginPath } from "../../preinstalled-plugins";
|
|
16
|
-
import {
|
|
17
|
-
getOpenCodePluginReferences,
|
|
18
12
|
mergeOpenCodeConfigContent,
|
|
19
13
|
readOpenCodeConfig,
|
|
20
14
|
} from "../../../tools/setup/providers/opencode-format";
|
|
@@ -86,6 +80,7 @@ export class OpenCodeAdapter extends LaunchAdapter {
|
|
|
86
80
|
OPENCODE_CONFIG_CONTENT: mergeOpenCodeConfigContent(
|
|
87
81
|
{
|
|
88
82
|
permissions: renderCompanionExternalDirectoryPermissions(context.companionPath),
|
|
83
|
+
plugins: [getOpenCodePluginRoot()],
|
|
89
84
|
references: {
|
|
90
85
|
mate: context.companionPath,
|
|
91
86
|
},
|
|
@@ -130,75 +125,41 @@ export class OpenCodeAdapter extends LaunchAdapter {
|
|
|
130
125
|
);
|
|
131
126
|
}
|
|
132
127
|
|
|
133
|
-
|
|
134
|
-
const
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
path.join(".opencode", "opencode.json"),
|
|
138
|
-
expectedPluginReference,
|
|
139
|
-
)),
|
|
128
|
+
/** Asset errors carry their own reinstall hint; only companion errors are repaired by setup. */
|
|
129
|
+
const assetErrors = this.validatePluginAssets();
|
|
130
|
+
const companionErrors = [
|
|
131
|
+
...(await this.validateConfigReadable(context)),
|
|
140
132
|
...this.validateGuidance(context),
|
|
141
133
|
];
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
134
|
+
const errors = [...assetErrors, ...companionErrors];
|
|
135
|
+
if (errors.length === 0) return;
|
|
136
|
+
|
|
137
|
+
const lines = [
|
|
138
|
+
"OpenCode companion runtime is invalid.",
|
|
139
|
+
...errors.map((error) => `- ${error}`),
|
|
140
|
+
];
|
|
141
|
+
if (companionErrors.length > 0) {
|
|
142
|
+
lines.push(
|
|
143
|
+
"Repair the companion runtime by re-running `mate companion setup` in the companion repository or `mate opencode` from the working repository.",
|
|
150
144
|
);
|
|
151
145
|
}
|
|
146
|
+
throw new LaunchPreflightError(lines.join("\n"));
|
|
152
147
|
}
|
|
153
148
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
const { present, config } = await readOpenCodeConfig(configPath);
|
|
162
|
-
if (!present) {
|
|
163
|
-
return [`Unreadable OpenCode configuration: ${configPath}`];
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
const mateReferences = getOpenCodePluginReferences(config).filter(
|
|
167
|
-
isMateOpenCodePluginReference,
|
|
168
|
-
) as string[];
|
|
149
|
+
/** Launch-time sync leaves unparseable user config untouched; OpenCode would fail on it at startup. */
|
|
150
|
+
private async validateConfigReadable(context: AdapterContext): Promise<string[]> {
|
|
151
|
+
const configPath = path.join(context.companionPath, ".opencode", "opencode.json");
|
|
152
|
+
const { present } = await readOpenCodeConfig(configPath);
|
|
153
|
+
return present ? [] : [`Unreadable OpenCode configuration: ${configPath}`];
|
|
154
|
+
}
|
|
169
155
|
|
|
170
|
-
|
|
156
|
+
private validatePluginAssets(): string[] {
|
|
157
|
+
try {
|
|
158
|
+
validateOpenCodePluginAssets();
|
|
171
159
|
return [];
|
|
160
|
+
} catch (error) {
|
|
161
|
+
return [(error as Error).message];
|
|
172
162
|
}
|
|
173
|
-
|
|
174
|
-
// Setup writes one of two spellings, and both are current: the published
|
|
175
|
-
// spec on an ordinary workstation, and an absolute path into the
|
|
176
|
-
// machine-local workspace where the distribution supplied an installed
|
|
177
|
-
// copy. Comparing only against the spec would refuse the launch on exactly
|
|
178
|
-
// the deployments preinstalled binding exists for.
|
|
179
|
-
//
|
|
180
|
-
// A bound reference carries its version in the package it points at rather
|
|
181
|
-
// than in the string, so staleness is read off disk. It is still caught,
|
|
182
|
-
// and now names the version actually installed.
|
|
183
|
-
const expectedVersion = expectedPluginReference.slice(OPENCODE_PLUGIN_PACKAGE_NAME.length + 1);
|
|
184
|
-
const bound = mateReferences.find((reference) =>
|
|
185
|
-
isPreinstalledPluginPath(reference, OPENCODE_PLUGIN_PACKAGE_NAME),
|
|
186
|
-
);
|
|
187
|
-
if (bound !== undefined) {
|
|
188
|
-
const version = await installedVersionAt(bound);
|
|
189
|
-
if (version === expectedVersion) return [];
|
|
190
|
-
return [
|
|
191
|
-
`Stale Mate plugin package reference in ${configFile}: it points at ${bound}, which is ${version ?? "not installed"} rather than ${expectedVersion}.`,
|
|
192
|
-
];
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
if (mateReferences.length > 0) {
|
|
196
|
-
return [
|
|
197
|
-
`Stale Mate plugin package reference in ${configFile}: found ${mateReferences.join(", ")}.`,
|
|
198
|
-
];
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
return [`Missing Mate plugin package reference in ${configFile}.`];
|
|
202
163
|
}
|
|
203
164
|
|
|
204
165
|
// Self-check the guidance payload this launch is about to inject so a
|