From 5f914d59eff52f39f9c113ff841a275dad279cde Mon Sep 17 00:00:00 2001 From: Matthew Goodwin Date: Sun, 23 Aug 2026 10:32:39 -0500 Subject: [PATCH 1/2] rt-tray: fail on missing deps, prune node's dev tree, sign every nested file MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Found uncommitted in the shared checkout and committed on request so it is not lost while several lanes run in parallel. NOT authored in this session — recording that here so the history does not misattribute it. Three changes, per the reasoning in the diff's own comments: RT_REQUIRE_DEPS now defaults to fatal, because warn-and-continue silently produced a bundle with no helpers that still passed every gate asserting only the helpers it could find; node's include/, lib/node_modules/{npm,corepack} and share/ are pruned, since codesign must individually sign every file under Helpers and the dev distribution cost ~78MB and ~8 minutes of timestamp round-trips per release; and helper signing now covers every regular file rather than only Mach-O binaries, because codesign treats the whole Helpers tree as nested code and a script-only helper would otherwise break the outer seal. Both scripts pass `bash -n`; not otherwise exercised here. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01XgJS4skzDPSuMdPKyBG3zT --- rt-tray/build.sh | 56 +++++++++++++++++++++++++++++++++++++---- rt-tray/check-bundle.sh | 26 ++++++++++++++++++- 2 files changed, 76 insertions(+), 6 deletions(-) diff --git a/rt-tray/build.sh b/rt-tray/build.sh index ddaa6c23..f7027cd9 100755 --- a/rt-tray/build.sh +++ b/rt-tray/build.sh @@ -180,8 +180,11 @@ fi HELPER_ENTITLEMENTS=() # "pathjit|none" for the signing pass bundle_helpers() { if [ ! -d "$DEPS_DIR" ]; then - if [ "${RT_REQUIRE_DEPS:-0}" = 1 ]; then echo " ✗ $DEPS_DIR missing — run scripts/fetch-deps.sh arm64"; exit 1; fi - echo " ⚠ $DEPS_DIR missing — Helpers skipped (scripts/fetch-deps.sh arm64 to bundle them)" + # Fatal by default: a warn-and-continue here silently produces a bundle + # with NO helpers, which then passes every gate that only asserts the + # helpers it can find. Set RT_REQUIRE_DEPS=0 to opt out deliberately. + if [ "${RT_REQUIRE_DEPS:-1}" = 1 ]; then echo " ✗ $DEPS_DIR missing — run scripts/fetch-deps.sh arm64 (RT_REQUIRE_DEPS=0 to build without helpers)"; exit 1; fi + echo " ⚠ $DEPS_DIR missing — Helpers skipped (RT_REQUIRE_DEPS=0 set)" return fi local row name version bundlePath ent status src dest prune @@ -216,6 +219,19 @@ bundle_helpers() { rm -rf "$prune" echo " · pruned $name/${prune#"$dest"/}" done < <(find "$dest" -depth -type d \( -name '.claude-plugin' -o -name '.codex-plugin' \) -print0) + # node ships a full development distribution, but the bundle needs it + # only to run fast-browser's .mjs. include/ is 2726 C++ headers node-gyp + # uses to compile native addons at build time; lib/node_modules/{npm, + # corepack} and their bin/ symlinks are unreferenced here. Beyond the + # ~78MB, every file under Contents/Helpers must be individually signed, + # so this dead weight also costs ~8 minutes of timestamp round-trips per + # release build (4708 files → ~61). The symlinks go too: a dangling one + # left behind breaks the outer seal. + if [ "$name" = node ]; then + rm -rf "$dest/include" "$dest/lib/node_modules/npm" "$dest/lib/node_modules/corepack" "$dest/share" + rm -f "$dest/bin/npm" "$dest/bin/npx" "$dest/bin/corepack" + echo " · pruned node/{include,lib/node_modules,share} (dev distribution, unused in-bundle)" + fi HELPER_ENTITLEMENTS+=("$dest $ent") echo " ✓ Helpers/$name $version" done < "$tsv" @@ -339,19 +355,49 @@ if [ -d "$CORE_FW" ]; then echo " ✓ Signed MattstackCore.framework" fi -sign_helper_tree() { # root ent — signs every Mach-O under root (files or a dir like node/) - local root="$1" ent="$2" f +# Everything under Contents/Helpers is classified as nested code by codesign's +# bundle seal — not just the Mach-O binaries — so every regular file here must +# carry a signature or the outer `sign "$APP_BUNDLE"` refuses with "code object +# is not signed at all / In subcomponent: ". A pure-script +# helper (fast-browser: .mjs + LICENSE + package.json, zero Mach-O) has no +# binary to match, so a Mach-O-only pass signs nothing in it and the seal fails. +# Non-Mach-O files get a plain signature stored in an xattr; only real binaries +# take the JIT entitlement and the helper identifier. +sign_helper_tree() { # root ent — signs every regular file under root (files or a dir like node/) + local root="$1" ent="$2" f signed + signed=$(find "$root" -type f | wc -l | tr -d ' ') + # A helper that contributes zero files can only mean the tree was never + # staged — the seal would fail later and much less legibly. + [ "$signed" -gt 0 ] || { echo " ✗ $(basename "$root"): no files to sign under $root"; exit 1; } + + # Pass 1 — plain-sign EVERY regular file, in parallel. codesign's bundle + # seal treats everything under Contents/Helpers as nested code, not just + # the Mach-O binaries, so a single unsigned file (a .mjs, a LICENSE, one of + # node/'s thousands of headers) makes the outer `sign "$APP_BUNDLE"` fail + # with "code object is not signed at all / In subcomponent: ". + # Batched (many paths per codesign call), NOT `xargs -I{}`: the -I form + # runs one process per file, is ~6x slower, and was observed leaving files + # silently unsigned — which only surfaces later as an opaque outer-seal + # failure naming one arbitrary file. stderr is kept, not discarded: hiding + # it is what made the misses invisible the first time. + find "$root" -type f -print0 | xargs -0 -P 8 codesign "${SIGN_FLAGS[@]}" 2>&1 \ + | grep -v "replacing existing signature" || true + + # Pass 2 — re-sign just the Mach-O binaries with their identifier and + # entitlements, overwriting pass 1's plain signature. Must come second: + # whichever pass runs last is the signature that survives. while IFS= read -r -d '' f; do if file -b "$f" | grep -q "Mach-O"; then if [ "$ent" = jit ]; then sign -i "com.mattstack.helper.$(basename "$f")" --entitlements "$ENTITLEMENTS_JIT" "$f" else sign -i "com.mattstack.helper.$(basename "$f")" "$f"; fi fi done < <(find "$root" -type f -print0) + SIGNED_FILE_COUNT=$signed } for entry in "${HELPER_ENTITLEMENTS[@]+"${HELPER_ENTITLEMENTS[@]}"}"; do path="${entry%% *}"; ent="${entry##* }" sign_helper_tree "$path" "$ent" - echo " ✓ Signed Helpers/$(basename "$path") ($ent)" + echo " ✓ Signed Helpers/$(basename "$path") ($ent, $SIGNED_FILE_COUNT files)" done if [ -f "$CONTENTS/MacOS/rt" ]; then diff --git a/rt-tray/check-bundle.sh b/rt-tray/check-bundle.sh index cec6c385..25b359a1 100755 --- a/rt-tray/check-bundle.sh +++ b/rt-tray/check-bundle.sh @@ -267,13 +267,37 @@ check_helpers() { # app check_signed "$f" "$exe Helpers/$name/$(basename "$f")" "$ent" assert_eq "$exe $name identifier" "Identifier=com.mattstack.helper.$(basename "$f")" "$(codesign -dv "$f" 2>&1 | grep '^Identifier=' || true)" done < <(find "$p" -type f -print0) + # Every regular file under Helpers — not just the Mach-O ones — is + # nested code to codesign's seal, and an unsigned one makes the outer + # `sign` fail. Asserting only Mach-O files (as the loop above does) is + # blind to a pure-script helper like fast-browser, and is blind again + # if packaging drops the xattr the non-binary signatures live in. + unsigned=0; first_unsigned="" + while IFS= read -r -d '' f; do + codesign --verify --strict "$f" 2>/dev/null && continue + unsigned=$((unsigned + 1)); [ -n "$first_unsigned" ] || first_unsigned="${f#"$p"/}" + done < <(find "$p" -type f -print0) + [ "$unsigned" -eq 0 ] \ + && pass "$exe Helpers/$name: every file carries a signature" \ + || fail "$exe Helpers/$name: $unsigned unsigned file(s), first: $name/$first_unsigned — the outer bundle seal will refuse this" done <<< "$LOCK_TSV" # Every bundled helper answers --version from inside the bundle (signed, entitled). [ -x "$app/Contents/Helpers/fzf" ] && "$app/Contents/Helpers/fzf" --version >/dev/null 2>&1 && pass "$exe Helpers/fzf runs" || fail "$exe Helpers/fzf does not run" [ -x "$app/Contents/Helpers/jq" ] && "$app/Contents/Helpers/jq" --version >/dev/null 2>&1 && pass "$exe Helpers/jq runs" || fail "$exe Helpers/jq does not run" [ -x "$app/Contents/Helpers/bun" ] && "$app/Contents/Helpers/bun" --version >/dev/null 2>&1 && pass "$exe Helpers/bun runs (jit entitlement sufficient)" || fail "$exe Helpers/bun does not run under its entitlements" [ -x "$app/Contents/Helpers/node/bin/node" ] && "$app/Contents/Helpers/node/bin/node" -e 'process.exit(0)' >/dev/null 2>&1 && pass "$exe Helpers/node runs" || fail "$exe Helpers/node does not run under its entitlements" - [ -f "$app/Contents/Helpers/fast-browser/bin/fast-browser.mjs" ] && pass "$exe Helpers/fast-browser package present" || fail "$exe Helpers/fast-browser package missing" + # Actually RUN it, like every other helper above. Asserting the entry file + # merely exists is what let a bundled fast-browser that crashes at module + # load pass every gate: build.sh prunes .claude-plugin/ (a dotted dir the + # bundle seal rejects) and lib/hosts/claude.mjs readFileSync's + # ../../.claude-plugin/plugin.json unconditionally at import time. + if [ -f "$app/Contents/Helpers/fast-browser/bin/fast-browser.mjs" ]; then + "$app/Contents/Helpers/node/bin/node" "$app/Contents/Helpers/fast-browser/bin/fast-browser.mjs" --version >/dev/null 2>&1 \ + && pass "$exe Helpers/fast-browser runs" \ + || fail "$exe Helpers/fast-browser does not run from inside the bundle" + else + fail "$exe Helpers/fast-browser package missing" + fi } check_helpers "$PROD" [ -n "$DEV" ] && check_helpers "$DEV" From 3656a4e544add6882fa6da143f870107181c7940 Mon Sep 17 00:00:00 2001 From: Matthew Goodwin Date: Sun, 23 Aug 2026 11:22:28 -0500 Subject: [PATCH 2/2] setup: register gitq and console as mattstack-managed deck apps `deck add` parses only --port/--cmd/--dir and never forwards a registrar, so the API defaulted these records to managedBy:"user". `deck remove --managed` scopes to managedBy !== "user", which meant a gitq registered by add alone silently survived uninstall, leaving a record pointing at a binary the uninstall had just deleted. Every bundled app is now added THEN adopted: adopt is the only verb that sets a registrar. The id is "rt" -- deck renders that as "mattstack" via MANAGER_DISPLAY, and board already carries it. The previous `--managed-by mattstack` was wrong twice: dropped by add, and the display name rather than the id. gitq's bare argv is its CLI, so deck supervises `gitq board`; a helper whose default argv is not its server passes the serving subcommand. Repins fast-browser to 0.1.0-alpha.15, which reads its version from package.json instead of the pruned .claude-plugin/ -- the bundled copy threw ENOENT at import on every invocation before this. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01QMy7FiR4bcTt8GTNdmWALS --- lib/setup/__tests__/steps-b.test.ts | 55 +++++++++++++++++++------ lib/setup/steps/deck.ts | 64 +++++++++++++++++++++-------- rt-tray/deps.lock | 6 +-- 3 files changed, 92 insertions(+), 33 deletions(-) diff --git a/lib/setup/__tests__/steps-b.test.ts b/lib/setup/__tests__/steps-b.test.ts index 67dad209..aaf4cce8 100644 --- a/lib/setup/__tests__/steps-b.test.ts +++ b/lib/setup/__tests__/steps-b.test.ts @@ -113,7 +113,7 @@ describe("services B: services.register, proxy.install, deck.managed, skills.mat }); /** Writes the real bundle (deps.lock, rt binary, whichever of deck/gitq/board are requested) to disk and points `mattstack.appPath` at it, then returns a fakeProbes mirroring the same layout so the Probes-side `exists`/`readFile` calls agree with what's really on disk. `deck` is always included — every deck.managed test needs the gate to pass; omit "gitq" or "board" from `tools` to simulate either not being bundled yet. */ - function bundledProbes(opts: { tools?: ("gitq" | "board")[]; overrides?: Partial[0]> } = {}): ReturnType { + function bundledProbes(opts: { tools?: ("gitq" | "board" | "console")[]; overrides?: Partial[0]> } = {}): ReturnType { const names = ["deck", ...(opts.tools ?? ["gitq"])]; mkdirSync(join(appRoot, "Contents", "Resources"), { recursive: true }); mkdirSync(join(appRoot, "Contents", "MacOS"), { recursive: true }); @@ -354,9 +354,9 @@ describe("services B: services.register, proxy.install, deck.managed, skills.mat expect(p.calls.exec).toEqual([]); }); - test("healthy + board bundled: adopts, repoints via PATCH, registers gitq", async () => { + test("healthy + board bundled: adopts, repoints via PATCH, registers gitq and console as mattstack-managed", async () => { const p = bundledProbes({ - tools: ["gitq", "board"], + tools: ["gitq", "board", "console"], overrides: { files: { [join(home, ".mattstack", "deck", "api.json")]: JSON.stringify({ port: 4100 }) }, fetch: healthyFetch(4100), @@ -368,17 +368,26 @@ describe("services B: services.register, proxy.install, deck.managed, skills.mat const outcome = await deckManagedStep.run(ctx); expect(outcome.state).toBe("done"); expect(detailOf(outcome)).toContain("repointed"); - expect(detailOf(outcome)).toContain("gitq registered"); + expect(detailOf(outcome)).toContain("gitq registered (managed)"); + expect(detailOf(outcome)).toContain("console registered (managed)"); const deckBin = join(appRoot, HELPERS_DIR, "deck"); expect(p.calls.exec[0]).toEqual([deckBin, "adopt", "mrs", "--as", "board", "--json"]); - expect(p.calls.exec[1]).toEqual([deckBin, "add", "gitq", "--cmd", join(appRoot, HELPERS_DIR, "gitq"), "--managed-by", "mattstack", "--host", "gitq.mattstack"]); + // `deck add` alone leaves the record managedBy:"user" — invisible to + // `deck remove --managed` — so every app is added THEN adopted. The + // registrar id is "rt"; deck renders that as "mattstack". + // gitq's bare argv is its CLI — deck must supervise `gitq board`, the + // server verb, not a command that prints usage and exits. + expect(p.calls.exec[1]).toEqual([deckBin, "add", "gitq", "--cmd", `${join(appRoot, HELPERS_DIR, "gitq")} board`, "--dir", join(home, ".mattstack", "gitq")]); + expect(p.calls.exec[2]).toEqual([deckBin, "adopt", "gitq", "--managed-by", "rt", "--json"]); + expect(p.calls.exec[3]).toEqual([deckBin, "add", "console", "--cmd", join(appRoot, HELPERS_DIR, "console"), "--dir", join(home, ".mattstack", "console")]); + expect(p.calls.exec[4]).toEqual([deckBin, "adopt", "console", "--managed-by", "rt", "--json"]); expect(p.calls.fetch).toContain("http://127.0.0.1:4100/api/v1/apps/board"); }); test("healthy + board NOT bundled: adopts, skips the repoint honestly", async () => { const p = bundledProbes({ - tools: ["gitq"], + tools: ["gitq", "console"], overrides: { files: { [join(home, ".mattstack", "deck", "api.json")]: JSON.stringify({ port: 4100 }) }, fetch: healthyFetch(4100), @@ -438,9 +447,13 @@ describe("services B: services.register, proxy.install, deck.managed, skills.mat }); const { ctx, logs } = makeCtx(p); const outcome = await deckManagedStep.run(ctx); - expect(outcome).toEqual({ state: "done", detail: "board adopted (repointed); gitq not registered: not bundled" }); + expect(outcome).toEqual({ + state: "done", + detail: "board adopted (repointed); gitq not registered: not bundled; console not registered: not bundled", + }); expect(logs.some((l) => l.line.includes("gitq") && l.line.includes("not bundled"))).toBe(true); - expect(p.calls.exec).toHaveLength(1); // only the adopt — no `deck add gitq` with a null bin + expect(logs.some((l) => l.line.includes("console") && l.line.includes("not bundled"))).toBe(true); + expect(p.calls.exec).toHaveLength(1); // only the adopt — no `deck add` with a null bin }); test("gitq's real 'deck add' is a stub (MAT-384): a driver-fatal response is logged and tallied, never fails the run", async () => { @@ -470,7 +483,11 @@ describe("services B: services.register, proxy.install, deck.managed, skills.mat }); const { ctx } = makeCtx(p); const outcome = await deckManagedStep.run(ctx); - expect(outcome).toEqual({ state: "done", detail: "board adopted (repointed); gitq already registered" }); + // "name taken" on add is not the end of the story: the adopt still runs, + // because a record left over from an earlier add may still be + // managedBy:"user" and would survive uninstall unclaimed. + expect(detailOf(outcome)).toContain("gitq already registered (managed)"); + expect(p.calls.exec.some((argv) => argv.includes("adopt") && argv.includes("gitq"))).toBe(true); }); test("fresh install (no legacy 'mrs'): adopt answers 'unknown app' — skips the board leg honestly, run continues past deck.managed", async () => { @@ -479,13 +496,19 @@ describe("services B: services.register, proxy.install, deck.managed, skills.mat overrides: { files: { [join(home, ".mattstack", "deck", "api.json")]: JSON.stringify({ port: 4100 }) }, fetch: healthyFetch(4100), - exec: async (argv) => (argv.includes("adopt") ? { code: 1, stdout: '{"adopted":false,"error":"unknown app"}', stderr: "" } : ok("")), + // Scoped to the legacy-mrs adopt only: the gitq/console adopts that + // follow are a different call and must still succeed, or this test + // would assert the unknown-app path for all three at once. + exec: async (argv) => (argv.includes("adopt") && argv.includes("mrs") ? { code: 1, stdout: '{"adopted":false,"error":"unknown app"}', stderr: "" } : ok("")), }, }); const { ctx } = makeCtx(p); const outcome = await deckManagedStep.run(ctx); // "done", not "failed" — the run is free to proceed to skills.materialize/board.keys/cron.triage next. - expect(outcome).toEqual({ state: "done", detail: "board not adopted (no legacy mrs to adopt); gitq registered" }); + expect(outcome).toEqual({ + state: "done", + detail: "board not adopted (no legacy mrs to adopt); gitq registered (managed); console not registered: not bundled", + }); // No repoint PATCH was issued — there was nothing to repoint. expect(p.calls.fetch.some((u) => u.includes("/api/v1/apps/board"))).toBe(false); }); @@ -508,8 +531,14 @@ describe("services B: services.register, proxy.install, deck.managed, skills.mat }); const { ctx: first } = makeCtx(p); const { ctx: second } = makeCtx(p); - expect(await deckManagedStep.run(first)).toEqual({ state: "done", detail: "board adopted (repointed); gitq registered" }); - expect(await deckManagedStep.run(second)).toEqual({ state: "done", detail: "board adopted (repointed); gitq already registered" }); + expect(await deckManagedStep.run(first)).toEqual({ + state: "done", + detail: "board adopted (repointed); gitq registered (managed); console not registered: not bundled", + }); + expect(await deckManagedStep.run(second)).toEqual({ + state: "done", + detail: "board adopted (repointed); gitq already registered (managed); console not registered: not bundled", + }); }); }); diff --git a/lib/setup/steps/deck.ts b/lib/setup/steps/deck.ts index 47e214e4..e5db9a1f 100644 --- a/lib/setup/steps/deck.ts +++ b/lib/setup/steps/deck.ts @@ -91,27 +91,56 @@ async function repointBoard(ctx: ApplyContext, port: number): Promise { } /** - * Never throws and never fails the run: `deck add`'s real argv (MAT-384) is - * a stub — the shipped CLI parses only `--port`/`--cmd`/`--dir`, ignores - * `--managed-by`/`--host` entirely, and 400s the first call for want of - * `--dir` — so a from-scratch install would otherwise permanently wedge on - * a known-incomplete verb. Duplicate registration answers the frozen + * deck's registrar id for everything mattstack ships. The stored value is + * "rt"; deck renders it as "mattstack" via its own MANAGER_DISPLAY map, and + * board already carries this exact id. Passing the display name instead + * produces an unrecognized registrar whose 409 escape hatch tells the user to + * run `mattstack uninstall ` — a command that does not exist. + */ +const MATTSTACK_REGISTRAR = "rt"; + +/** + * Registers a bundled app and claims it for mattstack, in that order. + * + * Two calls, not one, because `deck add` never forwards a registrar: it parses + * only `--port`/`--cmd`/`--dir` (MAT-384) and the API defaults the record to + * `managedBy: "user"`. A user-owned record is invisible to + * `deck remove --managed`, which scopes to `managedBy !== "user"` — so an app + * registered by `add` alone silently survives uninstall. `adopt` is the only + * verb that sets a registrar, and it is how board became managed. + * + * Never throws and never fails the run: a from-scratch install must not wedge + * on a known-incomplete verb. Duplicate registration answers the frozen * "name taken", not a bare `/already/` match. */ -async function registerGitq(ctx: ApplyContext, deckBin: string): Promise { - const bin = bundledToolPath(ctx.p, "gitq"); +async function registerManagedApp(ctx: ApplyContext, deckBin: string, name: string, serveArgs: string[] = []): Promise { + const bin = bundledToolPath(ctx.p, name); if (bin === null) { - ctx.log("deck.managed", "gitq: not bundled — left unmanaged"); - return "gitq not registered: not bundled"; + ctx.log("deck.managed", `${name}: not bundled — left unmanaged`); + return `${name} not registered: not bundled`; } - const result = await ctx.p.exec([deckBin, "add", "gitq", "--cmd", bin, "--managed-by", "mattstack", "--host", "gitq.mattstack"]); - if (result.code === 0) return "gitq registered"; - if (`${result.stdout}\n${result.stderr}`.includes("name taken")) return "gitq already registered"; + const dir = join(ctx.p.home, ".mattstack", name); + // deck splits --cmd on whitespace into argv. A helper whose DEFAULT argv is + // its CLI rather than its server needs the serving subcommand here, or deck + // supervises a command that prints usage and exits. + const cmd = [bin, ...serveArgs].join(" "); + const added = await ctx.p.exec([deckBin, "add", name, "--cmd", cmd, "--dir", dir]); + const already = matchFrozenError(`${added.stdout}\n${added.stderr}`) === "name taken"; + if (added.code !== 0 && !already) { + const reason = added.stderr.trim() || added.stdout.trim() || `exit ${added.code}`; + ctx.log("deck.managed", `${name}: deck add failed — ${reason}`); + return `${name} not registered: ${reason}`; + } - const reason = result.stderr.trim() || result.stdout.trim() || `exit ${result.code}`; - ctx.log("deck.managed", `gitq: deck add failed — ${reason}`); - return `gitq not registered: ${reason}`; + // Idempotent: re-adopting an app this registrar already owns is exit 0. + const adopted = await ctx.p.exec([deckBin, "adopt", name, "--managed-by", MATTSTACK_REGISTRAR, "--json"]); + if (adopted.code !== 0) { + const reason = adopted.stderr.trim() || adopted.stdout.trim() || `exit ${adopted.code}`; + ctx.log("deck.managed", `${name}: registered but not adopted — ${reason}`); + return `${name} registered but left unmanaged: ${reason}`; + } + return already ? `${name} already registered (managed)` : `${name} registered (managed)`; } async function deckManagedRun(ctx: ApplyContext): Promise { @@ -128,9 +157,10 @@ async function deckManagedRun(ctx: ApplyContext): Promise { if (adopted.kind === "failed") return adopted.outcome; const boardDetail = adopted.kind === "skip" ? `board not adopted (${adopted.detail})` : `board adopted (${await repointBoard(ctx, port)})`; - const gitqDetail = await registerGitq(ctx, deckBin); + const gitqDetail = await registerManagedApp(ctx, deckBin, "gitq", ["board"]); + const consoleDetail = await registerManagedApp(ctx, deckBin, "console"); - return { state: "done", detail: `${boardDetail}; ${gitqDetail}` }; + return { state: "done", detail: `${boardDetail}; ${gitqDetail}; ${consoleDetail}` }; } async function deckManagedRunSafe(ctx: ApplyContext): Promise { diff --git a/rt-tray/deps.lock b/rt-tray/deps.lock index 93262ab0..ca49d5ec 100644 --- a/rt-tray/deps.lock +++ b/rt-tray/deps.lock @@ -32,9 +32,9 @@ "sha256": "8294b7aa9b03997481c06babf1e8b270c859358f27da57a11509afe537ac381d", "archive": "tar.gz", "extract": "node-v24.19.0-darwin-arm64", "bundlePath": "Contents/Helpers/node", "exec": ["Contents/Helpers/node/bin/node"], "exposeByDefault": false, "entitlements": "jit", "status": "bundled", "kind": "helper" }, - { "name": "fast-browser", "version": "0.1.0-alpha.11", "license": "MIT", - "url": "https://registry.npmjs.org/@mattstack/fast-browser/-/fast-browser-0.1.0-alpha.11.tgz", - "sha256": "43d0faf99e78d0a4ac5a72fcb201557ddb1fa1fe1180bb0e01a1e6df0286b728", + { "name": "fast-browser", "version": "0.1.0-alpha.15", "license": "MIT", + "url": "https://registry.npmjs.org/@mattstack/fast-browser/-/fast-browser-0.1.0-alpha.15.tgz", + "sha256": "549b7dd7a4ab3a14aadce65d5cdace6f22f0e61d7e567e7a718cc4b7c7348d2e", "archive": "npm", "extract": "package", "bundlePath": "Contents/Helpers/fast-browser", "exec": ["Contents/Helpers/node/bin/node", "Contents/Helpers/fast-browser/bin/fast-browser.mjs"], "exposeByDefault": true, "entitlements": "none", "status": "bundled", "kind": "helper" },