@uniqbit/mate-core 0.17.0-canary.11 → 0.17.0-canary.13
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 +6 -5
- package/src/cli/commands/artifact/pending/index.ts +0 -1
- package/src/cli/commands/cap/index-cmd.ts +1 -1
- package/src/cli/commands/companion/companion.ts +4 -0
- package/src/cli/commands/companion/update.ts +334 -0
- package/src/cli/commands/install.ts +59 -5
- package/src/cli/commands/plugin/plugin.ts +4 -0
- package/src/cli/commands/plugin/verify.ts +25 -0
- package/src/cli/commands/studio/access.ts +0 -2
- package/src/cli/commands/studio/selection.ts +128 -12
- package/src/cli/commands/studio/server.ts +75 -47
- package/src/cli/commands/studio/vault.ts +319 -69
- package/src/cli/commands/studio/views/client.ts +90 -9
- package/src/cli/commands/studio/views/companion-picker.tsx +9 -5
- package/src/cli/commands/studio/views/companion-selector.tsx +11 -7
- package/src/cli/commands/studio/views/document.tsx +403 -109
- package/src/cli/commands/studio/views/model.ts +4 -1
- package/src/cli/commands/studio/views/selection-fields.tsx +14 -0
- package/src/cli/commands/studio/views/styles.ts +103 -21
- package/src/cli/commands/studio/views/terminal.tsx +227 -33
- package/src/cli/commands/studio/views/vault/state.ts +42 -0
- package/src/cli/commands/update.ts +5 -2
- package/src/cli/main.ts +2 -1
- package/src/cli/usage.ts +3 -1
- package/src/lib/opencode-plugin-package.ts +3 -0
- package/src/lib/orchestrator/adapters/opencode.ts +39 -12
- package/src/opencode/companion-hooks.ts +131 -153
- package/src/opencode/index.ts +12 -6
- package/src/opencode/tui.tsx +46 -49
- package/src/templates/providers/opencode/opencode.json +6 -4
- package/src/tools/setup/__snapshots__/runtime-surface-golden.test.ts.snap +489 -408
- package/src/tools/setup/capabilities/context-mode.ts +45 -25
- package/src/tools/setup/capabilities/tokensave.ts +96 -9
- package/src/tools/setup/dynamic-plugins/declarations.ts +47 -0
- package/src/tools/setup/dynamic-plugins/frozen.ts +215 -0
- package/src/tools/setup/dynamic-plugins/hydrate.ts +1 -40
- 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 +53 -0
- package/src/tools/setup/dynamic-plugins/verify.ts +70 -0
- package/src/tools/setup/providers/opencode-format.ts +220 -25
- package/src/tools/setup/providers/opencode.ts +60 -19
- package/src/tools/setup/runtime-documents.ts +8 -4
- package/src/cli/commands/studio/views/pairings.tsx +0 -38
- package/src/templates/providers/opencode/tui.json +0 -3
|
@@ -13,24 +13,47 @@ export const STUDIO_VIEWS: readonly StudioView[] = [
|
|
|
13
13
|
];
|
|
14
14
|
|
|
15
15
|
export const COMPANION_PARAM = "companion";
|
|
16
|
-
const VIEW_PARAM = "view";
|
|
16
|
+
export const VIEW_PARAM = "view";
|
|
17
17
|
export const REFRESH_PARAM = "refresh";
|
|
18
18
|
export const FILE_PARAM = "path";
|
|
19
|
+
export const DIR_PARAM = "dir";
|
|
20
|
+
|
|
21
|
+
/** Every URL parameter Studio names; no other module writes one out. */
|
|
22
|
+
export const STUDIO_PARAMS: readonly string[] = [
|
|
23
|
+
COMPANION_PARAM,
|
|
24
|
+
VIEW_PARAM,
|
|
25
|
+
REFRESH_PARAM,
|
|
26
|
+
FILE_PARAM,
|
|
27
|
+
DIR_PARAM,
|
|
28
|
+
];
|
|
19
29
|
|
|
20
30
|
/** Filled into a prompt when no change is named, so a prompt is never half-written. */
|
|
21
31
|
export const CHANGE_PLACEHOLDER = "<change-name>";
|
|
22
32
|
|
|
23
33
|
const DIGEST_LENGTH = 10;
|
|
24
34
|
|
|
25
|
-
|
|
35
|
+
interface SelectionBase {
|
|
26
36
|
companionDigest: string | null;
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* One-shot: the refresh control asks for it, and it is never carried forward
|
|
30
|
-
* into the URL another control builds. Switching a view is not a refresh.
|
|
31
|
-
*/
|
|
37
|
+
/** One-shot: only the `refresh` move sets it, and every other move clears it. */
|
|
32
38
|
refresh: boolean;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface StudioVaultSelection extends SelectionBase {
|
|
42
|
+
view: "vault";
|
|
33
43
|
openPath: string | null;
|
|
44
|
+
/** A folder of the listed tree; never looked up on disk. */
|
|
45
|
+
openDir: string | null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface StudioPlainSelection extends SelectionBase {
|
|
49
|
+
view: Exclude<StudioView, "vault">;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export type StudioSelection = StudioVaultSelection | StudioPlainSelection;
|
|
53
|
+
|
|
54
|
+
export interface SelectionField {
|
|
55
|
+
name: string;
|
|
56
|
+
value: string;
|
|
34
57
|
}
|
|
35
58
|
|
|
36
59
|
/**
|
|
@@ -60,11 +83,104 @@ function readView(value: string | null): StudioView {
|
|
|
60
83
|
return STUDIO_VIEWS.includes(value as StudioView) ? (value as StudioView) : "dashboard";
|
|
61
84
|
}
|
|
62
85
|
|
|
63
|
-
|
|
86
|
+
function readText(value: string | null): string | null {
|
|
87
|
+
return value?.trim() || null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** An empty or malformed digest selects no companion, as an unmatched one already does. */
|
|
91
|
+
function readDigest(value: string | null): string | null {
|
|
92
|
+
const digest = readText(value);
|
|
93
|
+
return digest && COMPANION_DIGEST_PATTERN.test(digest) ? digest : null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function readBase(params: URLSearchParams): SelectionBase {
|
|
97
|
+
return {
|
|
98
|
+
companionDigest: readDigest(params.get(COMPANION_PARAM)),
|
|
99
|
+
refresh: params.get(REFRESH_PARAM) === "1",
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function readVault(params: URLSearchParams): StudioVaultSelection {
|
|
104
|
+
return {
|
|
105
|
+
...readBase(params),
|
|
106
|
+
view: "vault",
|
|
107
|
+
openPath: readText(params.get(FILE_PARAM)),
|
|
108
|
+
openDir: readText(params.get(DIR_PARAM)),
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Forgiving: parameters the named view does not have are ignored. */
|
|
113
|
+
export function parse(url: URL): StudioSelection {
|
|
114
|
+
const view = readView(url.searchParams.get(VIEW_PARAM));
|
|
115
|
+
if (view === "vault") return readVault(url.searchParams);
|
|
116
|
+
return { ...readBase(url.searchParams), view };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** For the `/api/vault/*` routes, which imply the vault view whatever `view` says. */
|
|
120
|
+
export function parseVaultSelection(url: URL): StudioVaultSelection {
|
|
121
|
+
return readVault(url.searchParams);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** Drops the vault's target. */
|
|
125
|
+
export function switchView(selection: StudioSelection, view: StudioView): StudioSelection {
|
|
126
|
+
const base = { companionDigest: selection.companionDigest, refresh: false };
|
|
127
|
+
if (view === "vault") return { ...base, view, openPath: null, openDir: null };
|
|
128
|
+
return { ...base, view };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** Keeps the view and drops the vault's target: it belongs to the companion being left. */
|
|
132
|
+
export function switchCompanion(
|
|
133
|
+
selection: StudioSelection,
|
|
134
|
+
digest: string | null,
|
|
135
|
+
): StudioSelection {
|
|
136
|
+
return { ...switchView(selection, selection.view), companionDigest: digest };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function openFile(selection: StudioSelection, path: string): StudioVaultSelection {
|
|
64
140
|
return {
|
|
65
|
-
companionDigest:
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
openPath:
|
|
141
|
+
companionDigest: selection.companionDigest,
|
|
142
|
+
refresh: false,
|
|
143
|
+
view: "vault",
|
|
144
|
+
openPath: path,
|
|
145
|
+
openDir: null,
|
|
69
146
|
};
|
|
70
147
|
}
|
|
148
|
+
|
|
149
|
+
/** `null` opens the companion root. */
|
|
150
|
+
export function openFolder(selection: StudioSelection, dir: string | null): StudioVaultSelection {
|
|
151
|
+
return {
|
|
152
|
+
companionDigest: selection.companionDigest,
|
|
153
|
+
refresh: false,
|
|
154
|
+
view: "vault",
|
|
155
|
+
openPath: null,
|
|
156
|
+
openDir: dir,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export function refresh(selection: StudioSelection): StudioSelection {
|
|
161
|
+
return { ...selection, refresh: true };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** Omits defaults (the Dashboard view, an unset target), so URLs stay short. */
|
|
165
|
+
export function toFields(selection: StudioSelection): SelectionField[] {
|
|
166
|
+
const fields: SelectionField[] = [];
|
|
167
|
+
if (selection.companionDigest) {
|
|
168
|
+
fields.push({ name: COMPANION_PARAM, value: selection.companionDigest });
|
|
169
|
+
}
|
|
170
|
+
if (selection.view !== "dashboard") fields.push({ name: VIEW_PARAM, value: selection.view });
|
|
171
|
+
if (selection.view === "vault") {
|
|
172
|
+
if (selection.openPath) fields.push({ name: FILE_PARAM, value: selection.openPath });
|
|
173
|
+
if (selection.openDir) fields.push({ name: DIR_PARAM, value: selection.openDir });
|
|
174
|
+
}
|
|
175
|
+
if (selection.refresh) fields.push({ name: REFRESH_PARAM, value: "1" });
|
|
176
|
+
return fields;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export function toSearchParams(selection: StudioSelection): URLSearchParams {
|
|
180
|
+
return new URLSearchParams(toFields(selection).map(({ name, value }) => [name, value]));
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export function toHref(selection: StudioSelection, route = "/"): string {
|
|
184
|
+
const query = toSearchParams(selection).toString();
|
|
185
|
+
return query ? `${route}?${query}` : route;
|
|
186
|
+
}
|
|
@@ -8,9 +8,15 @@ import {
|
|
|
8
8
|
import { collectStudioInventory, type StudioInventory } from "./inventory";
|
|
9
9
|
import { assembleCompanionPayload, type StudioCompanionResponse } from "./payload";
|
|
10
10
|
import { STUDIO_HOSTNAME } from "./routes";
|
|
11
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
companionDigest,
|
|
13
|
+
parse,
|
|
14
|
+
parseVaultSelection,
|
|
15
|
+
resolveCompanion,
|
|
16
|
+
type StudioVaultSelection,
|
|
17
|
+
} from "./selection";
|
|
12
18
|
import { createStudioSnapshotCache, type StudioSnapshotCache } from "./snapshot";
|
|
13
|
-
import type { StudioPage } from "./views/model";
|
|
19
|
+
import type { StudioPage, StudioVaultPage } from "./views/model";
|
|
14
20
|
import { createVaultManager, resolveVaultPath, VaultPathError, type VaultManager } from "./vault";
|
|
15
21
|
import {
|
|
16
22
|
DEFAULT_DETACH_MINUTES,
|
|
@@ -41,11 +47,17 @@ export interface StudioSocket {
|
|
|
41
47
|
|
|
42
48
|
export interface StudioUpgrader {
|
|
43
49
|
upgrade(request: Request, options: { data: StudioSocket["data"] }): boolean;
|
|
50
|
+
/** Per-request idle timeout in seconds; `0` disables it. */
|
|
51
|
+
timeout?(request: Request, seconds: number): void;
|
|
44
52
|
}
|
|
45
53
|
|
|
54
|
+
/** Bun's maximum; its 10 s default cut slow Vault listings into empty responses. */
|
|
55
|
+
export const STUDIO_IDLE_TIMEOUT_SECONDS = 255;
|
|
56
|
+
|
|
46
57
|
export interface StudioServeOptions {
|
|
47
58
|
port: number;
|
|
48
59
|
hostname: string;
|
|
60
|
+
idleTimeout: number;
|
|
49
61
|
fetch: (request: Request, server: StudioUpgrader) => Promise<Response | undefined>;
|
|
50
62
|
websocket?: {
|
|
51
63
|
open(ws: StudioSocket): void;
|
|
@@ -97,6 +109,8 @@ export interface StudioServerOptions {
|
|
|
97
109
|
detachMinutes?: number;
|
|
98
110
|
/** Pins every terminal launch to this registered companion. */
|
|
99
111
|
launchCompanion?: string | null;
|
|
112
|
+
/** Seconds; defaults to `STUDIO_IDLE_TIMEOUT_SECONDS`. */
|
|
113
|
+
idleTimeout?: number;
|
|
100
114
|
/** Terminal launches skip companion Git synchronization. */
|
|
101
115
|
noGit?: boolean;
|
|
102
116
|
}
|
|
@@ -110,6 +124,22 @@ async function renderStudioDocument(page: StudioPage): Promise<string> {
|
|
|
110
124
|
return views.renderStudioDocument(page);
|
|
111
125
|
}
|
|
112
126
|
|
|
127
|
+
async function renderVaultView(page: StudioPage): Promise<string> {
|
|
128
|
+
const views = await import("./views/document");
|
|
129
|
+
return views.renderVaultView(page);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Keep the CLI entry free of eager Studio view imports. */
|
|
133
|
+
async function collectVaultState(
|
|
134
|
+
vault: VaultManager,
|
|
135
|
+
companionPath: string,
|
|
136
|
+
selection: StudioVaultSelection,
|
|
137
|
+
options: { tree: "prefetch" | "await" },
|
|
138
|
+
): Promise<StudioVaultPage> {
|
|
139
|
+
const { vaultState } = await import("./views/vault/state");
|
|
140
|
+
return vaultState(vault, companionPath, selection, options);
|
|
141
|
+
}
|
|
142
|
+
|
|
113
143
|
function html(body: string): Response {
|
|
114
144
|
return new Response(body, {
|
|
115
145
|
headers: { "content-type": "text/html; charset=utf-8", "cache-control": "no-store" },
|
|
@@ -301,12 +331,11 @@ export function createStudioFetch(
|
|
|
301
331
|
}
|
|
302
332
|
|
|
303
333
|
if (url.pathname === "/api/vault/tree") {
|
|
304
|
-
const
|
|
334
|
+
const selection = parseVaultSelection(url);
|
|
335
|
+
const selected = await selectedCompanion(selection.companionDigest, inventory);
|
|
305
336
|
if (!selected) return respond(json({ reason: "no registered companion was selected" }, 400));
|
|
306
337
|
try {
|
|
307
|
-
return respond(
|
|
308
|
-
json(await vault.tree(selected.path, url.searchParams.get("refresh") === "1")),
|
|
309
|
-
);
|
|
338
|
+
return respond(json(await vault.tree(selected.path, selection.refresh)));
|
|
310
339
|
} catch (error) {
|
|
311
340
|
return respond(
|
|
312
341
|
json({ reason: error instanceof Error ? error.message : String(error) }, 400),
|
|
@@ -314,11 +343,36 @@ export function createStudioFetch(
|
|
|
314
343
|
}
|
|
315
344
|
}
|
|
316
345
|
|
|
346
|
+
if (url.pathname === "/api/vault/view") {
|
|
347
|
+
const collected = await inventory();
|
|
348
|
+
const selection = parseVaultSelection(url);
|
|
349
|
+
const companion = resolveCompanion(collected, selection.companionDigest);
|
|
350
|
+
if (!companion) return respond(json({ reason: "no registered companion was selected" }, 400));
|
|
351
|
+
const state = await collectVaultState(vault, companion.path, selection, { tree: "await" });
|
|
352
|
+
return respond(
|
|
353
|
+
new Response(
|
|
354
|
+
await renderVaultView({
|
|
355
|
+
inventory: collected,
|
|
356
|
+
selection,
|
|
357
|
+
companion,
|
|
358
|
+
payload: null,
|
|
359
|
+
error: null,
|
|
360
|
+
collectedAt: null,
|
|
361
|
+
writable,
|
|
362
|
+
vault: state,
|
|
363
|
+
terminal: null,
|
|
364
|
+
}),
|
|
365
|
+
{ headers: { "content-type": "text/html; charset=utf-8", "cache-control": "no-store" } },
|
|
366
|
+
),
|
|
367
|
+
);
|
|
368
|
+
}
|
|
369
|
+
|
|
317
370
|
if (url.pathname === "/api/vault/file") {
|
|
318
|
-
const
|
|
371
|
+
const selection = parseVaultSelection(url);
|
|
372
|
+
const selected = await selectedCompanion(selection.companionDigest, inventory);
|
|
319
373
|
if (!selected) return respond(json({ reason: "no registered companion was selected" }, 400));
|
|
320
374
|
try {
|
|
321
|
-
return respond(json(await vault.open(selected.path,
|
|
375
|
+
return respond(json(await vault.open(selected.path, selection.openPath ?? "")));
|
|
322
376
|
} catch (error) {
|
|
323
377
|
return respond(
|
|
324
378
|
json({ reason: error instanceof Error ? error.message : String(error) }, 400),
|
|
@@ -327,10 +381,12 @@ export function createStudioFetch(
|
|
|
327
381
|
}
|
|
328
382
|
|
|
329
383
|
if (url.pathname === "/api/vault/events") {
|
|
330
|
-
const
|
|
384
|
+
const selection = parseVaultSelection(url);
|
|
385
|
+
const selected = await selectedCompanion(selection.companionDigest, inventory);
|
|
331
386
|
if (!selected) return respond(json({ reason: "no registered companion was selected" }, 400));
|
|
332
387
|
try {
|
|
333
|
-
const opened = await resolveVaultPath(selected.path,
|
|
388
|
+
const opened = await resolveVaultPath(selected.path, selection.openPath ?? "");
|
|
389
|
+
server?.timeout?.(request, 0);
|
|
334
390
|
return respond(vaultEvents(vault, selected.path, opened.relative));
|
|
335
391
|
} catch (error) {
|
|
336
392
|
return respond(
|
|
@@ -355,7 +411,7 @@ export function createStudioFetch(
|
|
|
355
411
|
return respond(json({ reason: "save requires a JSON body" }, 400));
|
|
356
412
|
}
|
|
357
413
|
const selected = await selectedCompanion(
|
|
358
|
-
|
|
414
|
+
typeof body.companion === "string" ? body.companion : null,
|
|
359
415
|
inventory,
|
|
360
416
|
);
|
|
361
417
|
if (!selected || typeof body.path !== "string" || typeof body.content !== "string") {
|
|
@@ -403,11 +459,10 @@ export function createStudioFetch(
|
|
|
403
459
|
}
|
|
404
460
|
|
|
405
461
|
async function selectedCompanion(
|
|
406
|
-
|
|
462
|
+
digest: string | null,
|
|
407
463
|
collectInventory: () => Promise<StudioInventory>,
|
|
408
464
|
): Promise<ReturnType<typeof resolveCompanion>> {
|
|
409
|
-
|
|
410
|
-
return resolveCompanion(inventory, parseStudioSelection(url).companionDigest);
|
|
465
|
+
return resolveCompanion(await collectInventory(), digest);
|
|
411
466
|
}
|
|
412
467
|
|
|
413
468
|
function vaultEvents(vault: VaultManager, companionPath: string, requestedPath: string): Response {
|
|
@@ -448,7 +503,7 @@ async function collectStudioPage(
|
|
|
448
503
|
vault: VaultManager,
|
|
449
504
|
writable: boolean,
|
|
450
505
|
): Promise<StudioPage> {
|
|
451
|
-
const selection =
|
|
506
|
+
const selection = parse(url);
|
|
452
507
|
const inventory = await collectInventory();
|
|
453
508
|
const companion = resolveCompanion(inventory, selection.companionDigest);
|
|
454
509
|
const page: StudioPage = {
|
|
@@ -469,38 +524,10 @@ async function collectStudioPage(
|
|
|
469
524
|
}
|
|
470
525
|
|
|
471
526
|
if (selection.view === "vault") {
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
if (selection.openPath) {
|
|
477
|
-
try {
|
|
478
|
-
open = await vault.open(companion.path, selection.openPath);
|
|
479
|
-
} catch (error) {
|
|
480
|
-
refusal = error instanceof Error ? error.message : String(error);
|
|
481
|
-
}
|
|
482
|
-
}
|
|
483
|
-
return {
|
|
484
|
-
...page,
|
|
485
|
-
vault: {
|
|
486
|
-
tree: tree.tree,
|
|
487
|
-
open,
|
|
488
|
-
refusal,
|
|
489
|
-
incoming: null,
|
|
490
|
-
overwritten: null,
|
|
491
|
-
watching: tree.watching,
|
|
492
|
-
warning: tree.warning,
|
|
493
|
-
},
|
|
494
|
-
};
|
|
495
|
-
} catch (error) {
|
|
496
|
-
return {
|
|
497
|
-
...page,
|
|
498
|
-
error: {
|
|
499
|
-
companionPath: companion.path,
|
|
500
|
-
reason: error instanceof Error ? error.message : String(error),
|
|
501
|
-
},
|
|
502
|
-
};
|
|
503
|
-
}
|
|
527
|
+
return {
|
|
528
|
+
...page,
|
|
529
|
+
vault: await collectVaultState(vault, companion.path, selection, { tree: "prefetch" }),
|
|
530
|
+
};
|
|
504
531
|
}
|
|
505
532
|
|
|
506
533
|
vault.deactivate();
|
|
@@ -569,6 +596,7 @@ export function startStudioServer(
|
|
|
569
596
|
const server = serve({
|
|
570
597
|
port,
|
|
571
598
|
hostname,
|
|
599
|
+
idleTimeout: options.idleTimeout ?? STUDIO_IDLE_TIMEOUT_SECONDS,
|
|
572
600
|
fetch: fetchHandler,
|
|
573
601
|
...(registry
|
|
574
602
|
? {
|