@uniqbit/mate-core 0.16.0 → 0.17.0-canary.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/cli/commands/companion/companion.ts +8 -0
- package/src/cli/commands/companion/prepare.ts +76 -0
- package/src/cli/commands/companion/register.ts +47 -0
- package/src/cli/commands/launch/claude.ts +5 -3
- package/src/cli/commands/launch/opencode.ts +5 -3
- package/src/cli/commands/launch/shared.ts +41 -12
- package/src/cli/commands/shared/companion-selection.ts +3 -1
- package/src/cli/commands/studio/index.ts +85 -6
- package/src/cli/commands/studio/routes.ts +1 -1
- package/src/cli/commands/studio/selection.ts +11 -2
- package/src/cli/commands/studio/server.ts +212 -17
- package/src/cli/commands/studio/vault.ts +607 -0
- package/src/cli/commands/studio/views/client.ts +141 -1
- package/src/cli/commands/studio/views/companion-selector.tsx +1 -1
- package/src/cli/commands/studio/views/document.tsx +136 -3
- package/src/cli/commands/studio/views/model.ts +13 -0
- package/src/cli/commands/studio/views/styles.ts +16 -0
- package/src/cli/main.ts +33 -4
- package/src/cli/usage.ts +6 -3
- package/src/index.ts +1 -0
- package/src/lib/context-mode-package.ts +7 -4
- package/src/lib/opencode-plugin-package.ts +10 -4
- package/src/lib/orchestrator/adapters/base.ts +65 -10
- package/src/lib/orchestrator/adapters/opencode.ts +4 -2
- package/src/lib/orchestrator/companion-registration.ts +73 -0
- package/src/lib/orchestrator/framework-context.ts +29 -3
- package/src/lib/orchestrator/launcher.ts +46 -22
- package/src/lib/orchestrator/projection-record.ts +24 -0
- package/src/lib/orchestrator/types.ts +11 -0
- package/src/lib/prebuilt-workspace-prepare.ts +152 -0
- package/src/lib/prebuilt-workspace.ts +236 -0
- package/src/lib/preinstalled-plugins.ts +82 -0
- package/src/lib/update-checker.ts +39 -0
- package/src/opencode/companion-hooks.ts +45 -20
- package/src/playbooks/companion-guidance.ts +1 -0
- package/src/runtime/companion-guidance.ts +61 -15
- package/src/runtime/env.ts +3 -3
- package/src/runtime/index.ts +1 -0
- package/src/tools/setup/__snapshots__/runtime-surface-golden.test.ts.snap +127 -77
- package/src/tools/setup/capabilities/context-mode.ts +5 -0
- package/src/tools/setup/plugin.ts +7 -1
- package/src/tools/setup/providers/opencode.ts +24 -5
|
@@ -91,7 +91,11 @@ export const STUDIO_CLIENT_SCRIPT = `(function () {
|
|
|
91
91
|
}
|
|
92
92
|
|
|
93
93
|
function copy(text, label) {
|
|
94
|
-
|
|
94
|
+
if (typeof navigator === "undefined" || !navigator.clipboard || typeof navigator.clipboard.writeText !== "function") {
|
|
95
|
+
announce("copying is blocked in this browser");
|
|
96
|
+
return Promise.resolve();
|
|
97
|
+
}
|
|
98
|
+
return Promise.resolve().then(function () { return navigator.clipboard.writeText(text); }).then(
|
|
95
99
|
function () {
|
|
96
100
|
announce("copied " + label);
|
|
97
101
|
},
|
|
@@ -194,4 +198,140 @@ export const STUDIO_CLIENT_SCRIPT = `(function () {
|
|
|
194
198
|
if (text) copy(text, node.getAttribute("data-copy-label") || "prompt");
|
|
195
199
|
});
|
|
196
200
|
});
|
|
201
|
+
|
|
202
|
+
var savePromise = null;
|
|
203
|
+
function editor() { return document.getElementById("vault-editor"); }
|
|
204
|
+
function status(message) {
|
|
205
|
+
var node = document.getElementById("vault-status");
|
|
206
|
+
if (node) node.textContent = message;
|
|
207
|
+
}
|
|
208
|
+
function dirty() {
|
|
209
|
+
var node = editor();
|
|
210
|
+
return !!node && node.value !== (node.getAttribute("data-vault-original") || "");
|
|
211
|
+
}
|
|
212
|
+
function setIncoming(event) {
|
|
213
|
+
var box = document.getElementById("vault-incoming");
|
|
214
|
+
var content = document.getElementById("vault-incoming-content");
|
|
215
|
+
var recover = document.getElementById("vault-recover");
|
|
216
|
+
if (!box || !content || !recover) return;
|
|
217
|
+
box.hidden = false;
|
|
218
|
+
content.textContent = event.content || "";
|
|
219
|
+
recover.setAttribute("data-vault-content", event.content || "");
|
|
220
|
+
recover.setAttribute("data-vault-token", event.token || "");
|
|
221
|
+
}
|
|
222
|
+
function acceptIncoming(event) {
|
|
223
|
+
var node = editor();
|
|
224
|
+
if (!node) return;
|
|
225
|
+
node.value = event.content || "";
|
|
226
|
+
node.setAttribute("data-vault-original", node.value);
|
|
227
|
+
if (event.token) node.setAttribute("data-vault-token", event.token);
|
|
228
|
+
var box = document.getElementById("vault-incoming");
|
|
229
|
+
if (box) box.hidden = true;
|
|
230
|
+
status("incoming version loaded into the editor");
|
|
231
|
+
}
|
|
232
|
+
function askBeforeNavigation() {
|
|
233
|
+
return !dirty() || window.confirm("Discard unsaved changes?");
|
|
234
|
+
}
|
|
235
|
+
function waitForSave() {
|
|
236
|
+
return savePromise ? savePromise.then(function (result) { return result; }) : Promise.resolve(true);
|
|
237
|
+
}
|
|
238
|
+
function navigate(url) {
|
|
239
|
+
waitForSave().then(function (allowed) {
|
|
240
|
+
if (allowed && askBeforeNavigation()) location.assign(url);
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
function navigationUrl(form, submitter) {
|
|
244
|
+
var target = new URL(form.action || location.href, location.href);
|
|
245
|
+
new FormData(form).forEach(function (value, key) {
|
|
246
|
+
if (typeof value === "string") target.searchParams.append(key, value);
|
|
247
|
+
});
|
|
248
|
+
if (submitter && submitter.name && submitter.value) target.searchParams.set(submitter.name, submitter.value);
|
|
249
|
+
return target.toString();
|
|
250
|
+
}
|
|
251
|
+
function wireNavigation() {
|
|
252
|
+
document.querySelectorAll('form[method="get"]').forEach(function (form) {
|
|
253
|
+
form.addEventListener("submit", function (event) {
|
|
254
|
+
event.preventDefault();
|
|
255
|
+
navigate(navigationUrl(form, event.submitter));
|
|
256
|
+
});
|
|
257
|
+
form.querySelectorAll("select").forEach(function (select) {
|
|
258
|
+
select.addEventListener("change", function () { navigate(navigationUrl(form)); });
|
|
259
|
+
});
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
function wireVault() {
|
|
263
|
+
var node = editor();
|
|
264
|
+
var root = document.querySelector("[data-vault-root]");
|
|
265
|
+
if (!node || !root) return;
|
|
266
|
+
node.setAttribute("data-vault-original", node.value);
|
|
267
|
+
var save = document.getElementById("vault-save");
|
|
268
|
+
if (save) save.addEventListener("click", function () {
|
|
269
|
+
if (savePromise) return;
|
|
270
|
+
var companion = document.querySelector("[data-companion]");
|
|
271
|
+
var body = {
|
|
272
|
+
companion: companion ? companion.getAttribute("data-companion") : "",
|
|
273
|
+
path: node.getAttribute("data-vault-path"),
|
|
274
|
+
content: node.value,
|
|
275
|
+
token: node.getAttribute("data-vault-token"),
|
|
276
|
+
};
|
|
277
|
+
savePromise = fetch("/api/vault/save", {
|
|
278
|
+
method: "POST",
|
|
279
|
+
headers: { "content-type": "application/json" },
|
|
280
|
+
body: JSON.stringify(body),
|
|
281
|
+
}).then(function (response) {
|
|
282
|
+
return response.json().then(function (result) {
|
|
283
|
+
if (!response.ok) {
|
|
284
|
+
if (response.status === 409) setIncoming(result);
|
|
285
|
+
status(result.reason || "save refused");
|
|
286
|
+
return false;
|
|
287
|
+
}
|
|
288
|
+
node.setAttribute("data-vault-token", result.token || "");
|
|
289
|
+
node.setAttribute("data-vault-original", node.value);
|
|
290
|
+
status("saved");
|
|
291
|
+
return true;
|
|
292
|
+
});
|
|
293
|
+
}).catch(function () {
|
|
294
|
+
status("save failed");
|
|
295
|
+
return false;
|
|
296
|
+
}).finally(function () { savePromise = null; });
|
|
297
|
+
});
|
|
298
|
+
var recover = document.getElementById("vault-recover");
|
|
299
|
+
if (recover) recover.addEventListener("click", function () {
|
|
300
|
+
acceptIncoming({ content: recover.getAttribute("data-vault-content") || "", token: recover.getAttribute("data-vault-token") || "" });
|
|
301
|
+
});
|
|
302
|
+
var companion = document.querySelector("[data-companion]");
|
|
303
|
+
var path = node.getAttribute("data-vault-path");
|
|
304
|
+
if (companion && path && typeof EventSource !== "undefined") {
|
|
305
|
+
var events = new EventSource("/api/vault/events?companion=" + encodeURIComponent(companion.getAttribute("data-companion") || "") + "&path=" + encodeURIComponent(path));
|
|
306
|
+
events.onmessage = function (message) {
|
|
307
|
+
var event;
|
|
308
|
+
try { event = JSON.parse(message.data); } catch (error) { return; }
|
|
309
|
+
if (event.kind === "overwritten") {
|
|
310
|
+
setIncoming(event);
|
|
311
|
+
status("a version was overwritten; its content is available below");
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
if (event.kind === "removed") {
|
|
315
|
+
status("this file no longer exists on disk");
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
if (dirty()) {
|
|
319
|
+
setIncoming(event);
|
|
320
|
+
status("the file changed on disk; unsaved content is still in the editor");
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
node.value = event.content || "";
|
|
324
|
+
node.setAttribute("data-vault-original", node.value);
|
|
325
|
+
if (event.token) node.setAttribute("data-vault-token", event.token);
|
|
326
|
+
status("updated from disk");
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
if (typeof window !== "undefined") window.addEventListener("beforeunload", function (event) {
|
|
331
|
+
if (!dirty()) return;
|
|
332
|
+
event.preventDefault();
|
|
333
|
+
event.returnValue = "";
|
|
334
|
+
});
|
|
335
|
+
wireNavigation();
|
|
336
|
+
wireVault();
|
|
197
337
|
})();`;
|
|
@@ -25,7 +25,7 @@ export function CompanionSelector({ inventory, selection }: CompanionSelectorPro
|
|
|
25
25
|
<input type="hidden" name="view" value={selection.view} />
|
|
26
26
|
)}
|
|
27
27
|
{/* eslint-disable-next-line react/no-unknown-property -- raw HTML attribute: hono/jsx server-renders this inline handler */}
|
|
28
|
-
<select name="companion" aria-label="Companion Repository"
|
|
28
|
+
<select name="companion" aria-label="Companion Repository">
|
|
29
29
|
<PlaceholderOption companionCount={companions.length} selected={selected} />
|
|
30
30
|
{companions.map((companion) => {
|
|
31
31
|
const digest = companionDigest(companion.path);
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
/** @jsxImportSource hono/jsx */
|
|
2
2
|
|
|
3
|
-
import { companionDigest, REFRESH_PARAM, type StudioSelection } from "../selection";
|
|
3
|
+
import { companionDigest, FILE_PARAM, REFRESH_PARAM, type StudioSelection } from "../selection";
|
|
4
4
|
import { STUDIO_CLIENT_SCRIPT, STUDIO_PREPAINT_SCRIPT } from "./client";
|
|
5
5
|
import { CompanionPicker } from "./companion-picker";
|
|
6
6
|
import { CompanionSelector } from "./companion-selector";
|
|
7
7
|
import { Dashboard } from "./dashboard/index";
|
|
8
8
|
import { CompanionError } from "./error";
|
|
9
|
-
import { formatCollectedAt, type StudioPage } from "./model";
|
|
9
|
+
import { formatCollectedAt, type StudioPage, type StudioVaultPage } from "./model";
|
|
10
|
+
import type { VaultTreeNode } from "../vault";
|
|
10
11
|
import { Specs } from "./specs/index";
|
|
11
12
|
import { STUDIO_STYLES } from "./styles";
|
|
12
13
|
import { Skills } from "./skills/index";
|
|
@@ -31,6 +32,10 @@ const VIEW_DETAILS = {
|
|
|
31
32
|
eyebrow: "Agent guidance",
|
|
32
33
|
title: "Skills",
|
|
33
34
|
},
|
|
35
|
+
vault: {
|
|
36
|
+
eyebrow: "Companion files",
|
|
37
|
+
title: "Vault",
|
|
38
|
+
},
|
|
34
39
|
} as const;
|
|
35
40
|
|
|
36
41
|
/**
|
|
@@ -86,6 +91,7 @@ function Content({ page }: { page: StudioPage }) {
|
|
|
86
91
|
if (page.error) {
|
|
87
92
|
return <CompanionError companionPath={page.error.companionPath} reason={page.error.reason} />;
|
|
88
93
|
}
|
|
94
|
+
if (page.selection.view === "vault") return <Vault page={page} />;
|
|
89
95
|
if (!page.payload) {
|
|
90
96
|
return (
|
|
91
97
|
<section className="panel">
|
|
@@ -153,7 +159,7 @@ function ViewNav({ selection }: { selection: StudioSelection }) {
|
|
|
153
159
|
<nav className="sidebar-nav" aria-label="Studio views">
|
|
154
160
|
<span className="sidebar-label">Views</span>
|
|
155
161
|
<form method="get" action="/">
|
|
156
|
-
<SelectionFields selection={selection} omit="view" />
|
|
162
|
+
<SelectionFields selection={selection} omit="view" dropOpenPath />
|
|
157
163
|
<div className="sidebar-nav-list">
|
|
158
164
|
<button
|
|
159
165
|
type="submit"
|
|
@@ -163,6 +169,9 @@ function ViewNav({ selection }: { selection: StudioSelection }) {
|
|
|
163
169
|
>
|
|
164
170
|
<span>Overview</span>
|
|
165
171
|
</button>
|
|
172
|
+
<button type="submit" name="view" value="vault" aria-pressed={selection.view === "vault"}>
|
|
173
|
+
<span>Vault</span>
|
|
174
|
+
</button>
|
|
166
175
|
<button type="submit" name="view" value="specs" aria-pressed={selection.view === "specs"}>
|
|
167
176
|
<span>Specs</span>
|
|
168
177
|
</button>
|
|
@@ -239,9 +248,11 @@ function Footer({ page }: { page: StudioPage }) {
|
|
|
239
248
|
function SelectionFields({
|
|
240
249
|
selection,
|
|
241
250
|
omit,
|
|
251
|
+
dropOpenPath = false,
|
|
242
252
|
}: {
|
|
243
253
|
selection: StudioSelection;
|
|
244
254
|
omit?: keyof StudioSelection;
|
|
255
|
+
dropOpenPath?: boolean;
|
|
245
256
|
}) {
|
|
246
257
|
return (
|
|
247
258
|
<>
|
|
@@ -251,6 +262,128 @@ function SelectionFields({
|
|
|
251
262
|
{selection.view !== "dashboard" && omit !== "view" ? (
|
|
252
263
|
<input type="hidden" name="view" value={selection.view} />
|
|
253
264
|
) : null}
|
|
265
|
+
{selection.view === "vault" && selection.openPath && !dropOpenPath ? (
|
|
266
|
+
<input type="hidden" name={FILE_PARAM} value={selection.openPath} />
|
|
267
|
+
) : null}
|
|
254
268
|
</>
|
|
255
269
|
);
|
|
256
270
|
}
|
|
271
|
+
|
|
272
|
+
function Vault({ page }: { page: StudioPage }) {
|
|
273
|
+
const vault = page.vault;
|
|
274
|
+
if (!vault)
|
|
275
|
+
return (
|
|
276
|
+
<section className="panel">
|
|
277
|
+
<p className="empty">No vault state collected.</p>
|
|
278
|
+
</section>
|
|
279
|
+
);
|
|
280
|
+
return (
|
|
281
|
+
<section className="vault-layout">
|
|
282
|
+
<div className="panel vault-tree-panel">
|
|
283
|
+
<div className="section-header">
|
|
284
|
+
<div>
|
|
285
|
+
<h3>Markdown files</h3>
|
|
286
|
+
<p className="section-note">
|
|
287
|
+
{vault.tree.length === 0
|
|
288
|
+
? "This companion has no markdown files."
|
|
289
|
+
: "Every non-ignored markdown file in this companion."}
|
|
290
|
+
</p>
|
|
291
|
+
</div>
|
|
292
|
+
{!vault.watching ? (
|
|
293
|
+
<form method="get" action="/" data-studio-navigation>
|
|
294
|
+
<SelectionFields selection={page.selection} />
|
|
295
|
+
<input type="hidden" name={REFRESH_PARAM} value="1" />
|
|
296
|
+
<button type="submit">Refresh tree</button>
|
|
297
|
+
</form>
|
|
298
|
+
) : null}
|
|
299
|
+
</div>
|
|
300
|
+
{vault.warning ? (
|
|
301
|
+
<p className="note vault-warning">{vault.warning}. The tree may be out of date.</p>
|
|
302
|
+
) : null}
|
|
303
|
+
<nav aria-label="Markdown files">
|
|
304
|
+
{vault.tree.map((node) => (
|
|
305
|
+
<VaultNode key={node.path} node={node} selection={page.selection} />
|
|
306
|
+
))}
|
|
307
|
+
</nav>
|
|
308
|
+
</div>
|
|
309
|
+
<VaultEditor page={page} vault={vault} />
|
|
310
|
+
</section>
|
|
311
|
+
);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function VaultNode({ node, selection }: { node: VaultTreeNode; selection: StudioSelection }) {
|
|
315
|
+
if (node.kind === "directory") {
|
|
316
|
+
return (
|
|
317
|
+
<details open className="vault-directory">
|
|
318
|
+
<summary>{node.name}</summary>
|
|
319
|
+
<div className="vault-children">
|
|
320
|
+
{node.children?.map((child) => (
|
|
321
|
+
<VaultNode key={child.path} node={child} selection={selection} />
|
|
322
|
+
))}
|
|
323
|
+
</div>
|
|
324
|
+
</details>
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
return (
|
|
328
|
+
<form method="get" action="/" className="vault-file" data-studio-navigation>
|
|
329
|
+
<SelectionFields selection={selection} dropOpenPath />
|
|
330
|
+
<input type="hidden" name={FILE_PARAM} value={node.path} />
|
|
331
|
+
<button type="submit" aria-current={selection.openPath === node.path ? "page" : undefined}>
|
|
332
|
+
{node.name}
|
|
333
|
+
</button>
|
|
334
|
+
</form>
|
|
335
|
+
);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function VaultEditor({ page, vault }: { page: StudioPage; vault: StudioVaultPage }) {
|
|
339
|
+
if (!vault.open) {
|
|
340
|
+
return (
|
|
341
|
+
<div className="panel vault-editor-panel">
|
|
342
|
+
<h3>{vault.refusal ? "File refused" : "Open a markdown file"}</h3>
|
|
343
|
+
<p className={vault.refusal ? "note vault-warning" : "empty"}>
|
|
344
|
+
{vault.refusal ?? "Choose a file from the tree to begin."}
|
|
345
|
+
</p>
|
|
346
|
+
</div>
|
|
347
|
+
);
|
|
348
|
+
}
|
|
349
|
+
return (
|
|
350
|
+
<div
|
|
351
|
+
className="panel vault-editor-panel"
|
|
352
|
+
data-vault-root
|
|
353
|
+
data-vault-writable={page.writable ? "true" : "false"}
|
|
354
|
+
>
|
|
355
|
+
<div className="section-header">
|
|
356
|
+
<div>
|
|
357
|
+
<h3>{vault.open.path}</h3>
|
|
358
|
+
<p className="section-note">
|
|
359
|
+
{page.writable
|
|
360
|
+
? "Changes stay in the editor until you save."
|
|
361
|
+
: "Read-only mode. Start Studio with --writable to save."}
|
|
362
|
+
</p>
|
|
363
|
+
</div>
|
|
364
|
+
{page.writable ? (
|
|
365
|
+
<button type="button" id="vault-save">
|
|
366
|
+
Save
|
|
367
|
+
</button>
|
|
368
|
+
) : null}
|
|
369
|
+
</div>
|
|
370
|
+
<textarea
|
|
371
|
+
id="vault-editor"
|
|
372
|
+
data-vault-editor
|
|
373
|
+
data-vault-path={vault.open.path}
|
|
374
|
+
data-vault-token={vault.open.token}
|
|
375
|
+
spellCheck={false}
|
|
376
|
+
>
|
|
377
|
+
{vault.open.content}
|
|
378
|
+
</textarea>
|
|
379
|
+
<p id="vault-status" className="note" aria-live="polite" />
|
|
380
|
+
<div id="vault-incoming" className="vault-incoming" hidden>
|
|
381
|
+
<strong id="vault-incoming-title">Incoming version</strong>
|
|
382
|
+
<pre id="vault-incoming-content" />
|
|
383
|
+
<button type="button" id="vault-recover">
|
|
384
|
+
Put incoming content back
|
|
385
|
+
</button>
|
|
386
|
+
</div>
|
|
387
|
+
</div>
|
|
388
|
+
);
|
|
389
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { StudioInventory, StudioInventoryCompanion } from "../inventory";
|
|
2
2
|
import type { StudioCompanionPayload } from "../payload";
|
|
3
3
|
import type { StudioSelection } from "../selection";
|
|
4
|
+
import type { VaultFile, VaultTreeNode } from "../vault";
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Everything one rendered document is derived from. `payload` and `error` are
|
|
@@ -15,6 +16,18 @@ export interface StudioPage {
|
|
|
15
16
|
payload: StudioCompanionPayload | null;
|
|
16
17
|
error: { companionPath: string; reason: string } | null;
|
|
17
18
|
collectedAt: number | null;
|
|
19
|
+
writable: boolean;
|
|
20
|
+
vault: StudioVaultPage | null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface StudioVaultPage {
|
|
24
|
+
tree: VaultTreeNode[];
|
|
25
|
+
open: VaultFile | null;
|
|
26
|
+
refusal: string | null;
|
|
27
|
+
incoming: VaultFile | null;
|
|
28
|
+
overwritten: VaultFile | null;
|
|
29
|
+
watching: boolean;
|
|
30
|
+
warning: string | null;
|
|
18
31
|
}
|
|
19
32
|
|
|
20
33
|
/**
|
|
@@ -360,6 +360,22 @@ ul.plain li:last-child { border-bottom: 0; }
|
|
|
360
360
|
.workflow-option small { color: var(--muted); font-size: .65rem; line-height: 1.35; }
|
|
361
361
|
.workflow-option .runway-step-copy { justify-self: start; }
|
|
362
362
|
|
|
363
|
+
.vault-layout { display: grid; grid-template-columns: minmax(190px, .34fr) minmax(0, 1fr); gap: 16px; align-items: start; }
|
|
364
|
+
.vault-tree-panel, .vault-editor-panel { min-width: 0; }
|
|
365
|
+
.vault-tree-panel nav { display: grid; gap: 2px; }
|
|
366
|
+
.vault-directory { color: var(--muted); font-family: var(--mono); font-size: .75rem; }
|
|
367
|
+
.vault-directory summary { cursor: pointer; padding: 5px 4px; }
|
|
368
|
+
.vault-children { display: grid; gap: 2px; padding-left: 12px; }
|
|
369
|
+
.vault-file { display: contents; }
|
|
370
|
+
.vault-file button { width: 100%; overflow: hidden; padding: 6px 8px; border-color: transparent; background: transparent; color: var(--text); font-family: var(--mono); font-size: .72rem; text-overflow: ellipsis; white-space: nowrap; }
|
|
371
|
+
.vault-file button:hover, .vault-file button[aria-current="page"] { border-color: var(--border); background: var(--accent-soft); color: var(--accent); }
|
|
372
|
+
.vault-warning { color: var(--warn); }
|
|
373
|
+
#vault-editor { display: block; width: 100%; min-height: 62vh; resize: vertical; padding: 16px; border: 1px solid var(--border); border-radius: 8px; background: var(--prompt-bg); color: var(--prompt-text); font-family: var(--mono); font-size: .86rem; line-height: 1.6; }
|
|
374
|
+
#vault-editor:focus { outline: 2px solid var(--accent); outline-offset: 2px; }
|
|
375
|
+
.vault-incoming { display: grid; gap: 8px; margin-top: 16px; padding: 12px; border: 1px solid var(--warn); border-radius: 8px; background: var(--warn-soft); }
|
|
376
|
+
.vault-incoming pre { max-height: 220px; overflow: auto; margin: 0; white-space: pre-wrap; font-family: var(--mono); font-size: .78rem; }
|
|
377
|
+
@media (max-width: 800px) { .vault-layout { grid-template-columns: 1fr; } }
|
|
378
|
+
|
|
363
379
|
@media (max-width: 650px) {
|
|
364
380
|
.workflow-console-header { grid-template-columns: 1fr auto; }
|
|
365
381
|
.workflow-console-context, .workflow-console-tabs { margin-left: 0; }
|
package/src/cli/main.ts
CHANGED
|
@@ -22,6 +22,7 @@ import { runUnwrapCommand } from "./commands/unwrap";
|
|
|
22
22
|
import { parseWrapArgs, runWrapCommand } from "./commands/wrap";
|
|
23
23
|
import { runWorkingCommand } from "./commands/working/working";
|
|
24
24
|
import { runInstallCommand } from "./commands/install";
|
|
25
|
+
import { parseDirectLaunchArgs } from "./commands/launch/shared";
|
|
25
26
|
import {
|
|
26
27
|
inspectInstallPreflight,
|
|
27
28
|
isRepairableInstallPreflight,
|
|
@@ -191,6 +192,14 @@ export async function main(argv = process.argv, deps: MainDeps = mainDeps): Prom
|
|
|
191
192
|
case "link":
|
|
192
193
|
if (!(await gate({ notHubRoot: true }))) return;
|
|
193
194
|
break;
|
|
195
|
+
// Registration names its target explicitly, so it needs no companion
|
|
196
|
+
// context and no root classification of the current directory.
|
|
197
|
+
case "register":
|
|
198
|
+
// Preparation names its bundle and its target explicitly, and runs
|
|
199
|
+
// before a companion is usable at all.
|
|
200
|
+
case "prepare":
|
|
201
|
+
if (!(await gate({ updateGuard: true }))) return;
|
|
202
|
+
break;
|
|
194
203
|
// Hub commands establish and operate on a local hub root directly;
|
|
195
204
|
// they must not require a linked working repository or installation —
|
|
196
205
|
// but companions are never hubs.
|
|
@@ -215,11 +224,31 @@ export async function main(argv = process.argv, deps: MainDeps = mainDeps): Prom
|
|
|
215
224
|
await runHubCommand(subcommand ? [subcommand, ...rest] : []);
|
|
216
225
|
return;
|
|
217
226
|
case "claude":
|
|
218
|
-
if (
|
|
227
|
+
if (
|
|
228
|
+
!(await gate({
|
|
229
|
+
updateGuard: true,
|
|
230
|
+
companion:
|
|
231
|
+
parseDirectLaunchArgs(argv.slice(3)).scope === "companion"
|
|
232
|
+
? { persistSelection: false }
|
|
233
|
+
: true,
|
|
234
|
+
install: true,
|
|
235
|
+
}))
|
|
236
|
+
)
|
|
237
|
+
return;
|
|
219
238
|
await runLaunchClaudeCommand(argv.slice(3), { directPassthrough: true });
|
|
220
239
|
return;
|
|
221
240
|
case "opencode":
|
|
222
|
-
if (
|
|
241
|
+
if (
|
|
242
|
+
!(await gate({
|
|
243
|
+
updateGuard: true,
|
|
244
|
+
companion:
|
|
245
|
+
parseDirectLaunchArgs(argv.slice(3)).scope === "companion"
|
|
246
|
+
? { persistSelection: false }
|
|
247
|
+
: true,
|
|
248
|
+
install: true,
|
|
249
|
+
}))
|
|
250
|
+
)
|
|
251
|
+
return;
|
|
223
252
|
await runLaunchOpenCodeCommand(argv.slice(3), { directPassthrough: true });
|
|
224
253
|
return;
|
|
225
254
|
case "report":
|
|
@@ -228,8 +257,8 @@ export async function main(argv = process.argv, deps: MainDeps = mainDeps): Prom
|
|
|
228
257
|
return;
|
|
229
258
|
case "studio":
|
|
230
259
|
/**
|
|
231
|
-
* Serves the machine-wide inventory
|
|
232
|
-
*
|
|
260
|
+
* Serves the machine-wide inventory without companion context, so it
|
|
261
|
+
* stays runnable from any directory. Writes remain opt-in.
|
|
233
262
|
*/
|
|
234
263
|
if (!(await gate({ updateGuard: true }))) return;
|
|
235
264
|
await runStudioCommand(argv.slice(3));
|
package/src/cli/usage.ts
CHANGED
|
@@ -13,6 +13,8 @@ export function usage(): string {
|
|
|
13
13
|
` ${n} install [--yes]`,
|
|
14
14
|
` ${n} companion link`,
|
|
15
15
|
` ${n} companion setup`,
|
|
16
|
+
` ${n} companion register [path]`,
|
|
17
|
+
` ${n} companion prepare --from <bundle> [path]`,
|
|
16
18
|
` ${n} companion list`,
|
|
17
19
|
` ${n} companion sync`,
|
|
18
20
|
` ${n} companion open`,
|
|
@@ -28,10 +30,11 @@ export function usage(): string {
|
|
|
28
30
|
` ${n} artifact pending [--json]`,
|
|
29
31
|
` ${n} doctor`,
|
|
30
32
|
` ${n} report [--days N] [--input FILE|-] [--json]`,
|
|
31
|
-
` ${n} studio`,
|
|
33
|
+
` ${n} studio [--writable]`,
|
|
34
|
+
` ${n} studio serve --port <port> [--host <host>] [--writable]`,
|
|
32
35
|
` ${n} config`,
|
|
33
|
-
` ${n} claude [args...] (
|
|
34
|
-
` ${n} opencode [args...] (
|
|
36
|
+
` ${n} claude [args...] (after --: --no-git, --companion, --yes; TTY launches confirm by default)`,
|
|
37
|
+
` ${n} opencode [args...] (after --: --no-git, --companion, --yes; TTY launches confirm by default)`,
|
|
35
38
|
` ${n} cap openspec <subcommand> [args...]`,
|
|
36
39
|
` ${n} cap graphify <subcommand> [args...]`,
|
|
37
40
|
` ${n} cap index [--graphify] [--tokensave]`,
|
package/src/index.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { spawnSync } from "node:child_process";
|
|
|
2
2
|
import fs from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
|
|
5
|
+
import { isPreinstalledPluginPath } from "./preinstalled-plugins";
|
|
5
6
|
import { PUBLIC_NPM_REGISTRY } from "./public-npm";
|
|
6
7
|
|
|
7
8
|
export const CONTEXT_MODE_PACKAGE_NAME = "context-mode";
|
|
@@ -12,11 +13,13 @@ export function getContextModePackageReference(): string {
|
|
|
12
13
|
return `${CONTEXT_MODE_PACKAGE_NAME}@${CONTEXT_MODE_VERSION}`;
|
|
13
14
|
}
|
|
14
15
|
|
|
16
|
+
/** Matches the published reference and a reference bound to an installed copy alike. */
|
|
15
17
|
export function isContextModePackageReference(value: unknown): value is string {
|
|
16
|
-
return
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
if (typeof value !== "string") return false;
|
|
19
|
+
if (value === CONTEXT_MODE_PACKAGE_NAME || value.startsWith(`${CONTEXT_MODE_PACKAGE_NAME}@`)) {
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
22
|
+
return isPreinstalledPluginPath(value, CONTEXT_MODE_PACKAGE_NAME);
|
|
20
23
|
}
|
|
21
24
|
|
|
22
25
|
export function getContextModeInstallDir(companionPath: string): string {
|
|
@@ -3,6 +3,7 @@ import fs from "node:fs/promises";
|
|
|
3
3
|
import os from "node:os";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
|
|
6
|
+
import { isPreinstalledPluginPath } from "./preinstalled-plugins";
|
|
6
7
|
import { PUBLIC_NPM_REGISTRY } from "./public-npm";
|
|
7
8
|
import { getCurrentVersion } from "./update-checker";
|
|
8
9
|
|
|
@@ -18,11 +19,16 @@ export function getOpenCodePluginPackageReference(version: string = getCurrentVe
|
|
|
18
19
|
return `${OPENCODE_PLUGIN_PACKAGE_NAME}@${version}`;
|
|
19
20
|
}
|
|
20
21
|
|
|
22
|
+
/** Matches the published reference and a reference bound to an installed copy alike. */
|
|
21
23
|
export function isMateOpenCodePluginReference(value: unknown): boolean {
|
|
22
|
-
return
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
if (typeof value !== "string") return false;
|
|
25
|
+
if (
|
|
26
|
+
value === OPENCODE_PLUGIN_PACKAGE_NAME ||
|
|
27
|
+
value.startsWith(`${OPENCODE_PLUGIN_PACKAGE_NAME}@`)
|
|
28
|
+
) {
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
return isPreinstalledPluginPath(value, OPENCODE_PLUGIN_PACKAGE_NAME);
|
|
26
32
|
}
|
|
27
33
|
|
|
28
34
|
export function getOpenCodeCacheDir(env: NodeJS.ProcessEnv = process.env): string {
|