mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-05 13:29:44 +02:00
* feat(desktop): add macOS app icon Replace the default electron-vite scaffold icon with the Multica asterisk icon. Adds build/icon.icns so electron-builder picks it up automatically via the `buildResources: build` config — no YAML change needed. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(desktop): run electron-vite build inside package script The package wrapper only ran bundle-cli.mjs and electron-builder, so electron-builder silently packaged whatever was already in out/. On a fresh checkout (or after a partial build) this shipped an app with a missing renderer bundle, which white-screens on launch. Add an explicit `electron-vite build` step between bundle-cli and electron-builder so `pnpm package` is self-contained. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(desktop): restore shell PATH in main process for GUI launches macOS/Linux GUI launches inherit a minimal PATH from launchd that omits ~/.zshrc, Homebrew, nvm, ~/.local/bin, and other shell config. Child processes spawned from the main process — including the bundled multica CLI used by daemon-manager — inherit the same stripped PATH, so the CLI fails to locate agent binaries like claude, codex, opencode, etc. with "no agent CLI found: … ensure it is on PATH". Use `fix-path` to recover the real shell PATH at startup, then prepend common install locations (/opt/homebrew/bin, /usr/local/bin, ~/.local/bin) as a fallback for broken shell rc or non-interactive $SHELL. Runs before setupDaemonManager so every subsequent spawn sees the corrected PATH. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(desktop): show onboarding wizard when authed user has no workspace Desktop is a single-shell architecture — every route, including /onboarding, lives inside DashboardGuard. The guard returns its loading fallback whenever workspace is null, so a fresh account that logs in with no workspaces ends up stuck on the spinner forever: the `replace(onboardingPath)` redirect navigates the tab router, but DashboardGuard still blocks its children because workspace is still null. Handle the empty-workspace case in DesktopShell itself: render OnboardingWizard as a full-screen takeover, bypassing DashboardGuard. A ref-based flag freezes the "needs onboarding" decision at first mount so creating a workspace mid-wizard (step 0) doesn't unmount the wizard and dump the user into the main shell before steps 1-3 (runtime, agent, get started) finish. Also add a local `bootstrapping` flag in AppContent so DesktopShell doesn't mount until the deep-link login chain (loginWithToken → syncToken → listWorkspaces → hydrateWorkspace) fully resolves. Without it, the shell would briefly see `!workspace` before hydration lands, causing users with existing workspaces to flash the wizard (or, with the ref freeze, get stuck in it permanently). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refactor(desktop): extract OnboardingGate with test coverage Pull the "render onboarding wizard when authed user has no workspace" logic out of DesktopShell into a dedicated OnboardingGate component. Replaces the ref-based freeze with a lazy useState initializer (`useState(() => !hasWorkspace)`), which is React's idiomatic pattern for "capture a value once at mount". The freeze semantics are unchanged: creating a workspace in step 0 of the wizard must not unmount it, because steps 1-3 still need to run; only `onComplete` flips the gate back to the main shell. Also de-duplicates the wrapping DesktopNavigationProvider — both branches of the shell now share a single provider instead of re-mounting one per branch. Wire up jsdom + @testing-library/react in the desktop vitest config (mirroring packages/views) and add three deterministic tests covering: 1. children render when hasWorkspace is true at mount 2. wizard stays mounted when hasWorkspace flips to true mid-flow 3. onComplete transitions the gate to children Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refactor(desktop): drop redundant syncToken call in deep-link login daemonAPI.syncToken was called twice on a deep-link login: once inside the deep-link handler's bootstrapping chain, and again in the useEffect([user]) that reacts to the user state change. Both calls spawn a multica CLI subprocess over IPC, wasting ~1-2s of startup time on the critical login path. Keep the [user] effect (it covers the session-restore path too) and drop the explicit call from the deep-link handler. Net effect: login latency shrinks, behavior is unchanged. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
150 lines
5.4 KiB
JavaScript
150 lines
5.4 KiB
JavaScript
#!/usr/bin/env node
|
|
// Wrapper around `electron-builder` that keeps the Desktop version in
|
|
// lockstep with the CLI. Both are derived from `git describe --tags
|
|
// --always --dirty` — the same source GoReleaser reads for the CLI
|
|
// binary via the `main.version` ldflag — so a single `vX.Y.Z` tag push
|
|
// produces matching CLI and Desktop versions.
|
|
//
|
|
// Runs bundle-cli.mjs first (so the Go binary is compiled and copied
|
|
// into resources/bin/), then `electron-vite build` to produce the
|
|
// main/preload/renderer bundles under out/, then invokes electron-builder
|
|
// with `-c.extraMetadata.version=<derived>` so the override applies at
|
|
// build time without mutating the tracked package.json.
|
|
//
|
|
// The electron-vite step is important: electron-builder only packages
|
|
// whatever is already in out/, so skipping it (or relying on stale
|
|
// artifacts from a prior partial build) ships an app with missing
|
|
// renderer code and white-screens on launch.
|
|
//
|
|
// Extra CLI args after `pnpm package --` are forwarded to electron-builder
|
|
// unchanged (e.g. `--mac --arm64`). For an unsigned local smoke-test
|
|
// build, set `CSC_IDENTITY_AUTO_DISCOVERY=false` so electron-builder falls
|
|
// back to an ad-hoc signature instead of requiring a Developer ID cert.
|
|
//
|
|
// The `normalizeGitVersion` helper is exported so tests can cover the
|
|
// version-derivation logic without shelling out.
|
|
|
|
import { execFileSync, spawnSync, execSync } from "node:child_process";
|
|
import { dirname, resolve } from "node:path";
|
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
|
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
const desktopRoot = resolve(here, "..");
|
|
|
|
function sh(cmd) {
|
|
try {
|
|
return execSync(cmd, { encoding: "utf-8" }).trim();
|
|
} catch {
|
|
return "";
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Pure transformation from the `git describe --tags --always --dirty`
|
|
* output to the value we feed into electron-builder's extraMetadata.version.
|
|
*
|
|
* - empty input → null (caller should fall back)
|
|
* - "v0.1.36" → "0.1.36"
|
|
* - "v0.1.35-14-gf1415e96" → "0.1.35-14-gf1415e96" (semver prerelease)
|
|
* - "v0.1.35-…-dirty" → same, dirty suffix preserved
|
|
* - "f1415e96" (no tag) → "0.0.0-f1415e96" (fallback)
|
|
*
|
|
* Leading `v` is stripped so the result is valid semver for package.json.
|
|
*/
|
|
export function normalizeGitVersion(raw) {
|
|
if (!raw) return null;
|
|
const stripped = raw.replace(/^v/, "");
|
|
if (!/^\d/.test(stripped)) {
|
|
// No reachable tag — `git describe` fell back to just the commit hash.
|
|
return `0.0.0-${stripped}`;
|
|
}
|
|
return stripped;
|
|
}
|
|
|
|
function deriveVersion() {
|
|
return normalizeGitVersion(sh("git describe --tags --always --dirty"));
|
|
}
|
|
|
|
function main() {
|
|
// Step 1: build + bundle the Go CLI via the existing script.
|
|
execFileSync("node", [resolve(here, "bundle-cli.mjs")], {
|
|
stdio: "inherit",
|
|
cwd: desktopRoot,
|
|
});
|
|
|
|
// Step 2: build the Electron main/preload/renderer bundles. Without
|
|
// this step electron-builder silently packages whatever is already in
|
|
// out/, which on a fresh checkout (or after a partial build) ships an
|
|
// app that white-screens because the renderer bundle is missing.
|
|
const viteResult = spawnSync("electron-vite", ["build"], {
|
|
stdio: "inherit",
|
|
cwd: desktopRoot,
|
|
});
|
|
if (viteResult.error) {
|
|
console.error(
|
|
"[package] failed to spawn electron-vite:",
|
|
viteResult.error.message,
|
|
);
|
|
process.exit(1);
|
|
}
|
|
if (viteResult.status !== 0) {
|
|
process.exit(viteResult.status ?? 1);
|
|
}
|
|
|
|
// Step 3: derive the version that should be written into the app.
|
|
const version = deriveVersion();
|
|
if (version) {
|
|
console.log(`[package] Desktop version → ${version} (from git describe)`);
|
|
} else {
|
|
console.warn(
|
|
"[package] could not derive version from git; falling back to package.json",
|
|
);
|
|
}
|
|
|
|
// Step 4: assemble electron-builder args.
|
|
const passthrough = process.argv.slice(2);
|
|
const builderArgs = [];
|
|
if (version) builderArgs.push(`-c.extraMetadata.version=${version}`);
|
|
|
|
// Step 5: gracefully degrade for local dev builds. electron-builder.yml
|
|
// sets `notarize: true` so real releases notarize in-build (keeping the
|
|
// stapled .app consistent with latest-mac.yml's SHA512). But a mac dev
|
|
// who just wants to smoke-test a local package doesn't have Apple
|
|
// credentials, and would otherwise hit a hard failure at the notarize
|
|
// step. Detect the missing env and flip notarize off for this run only.
|
|
if (!process.env.APPLE_TEAM_ID) {
|
|
console.warn(
|
|
"[package] APPLE_TEAM_ID not set — skipping notarization (local dev build). " +
|
|
"Set APPLE_ID + APPLE_APP_SPECIFIC_PASSWORD + APPLE_TEAM_ID for a release build.",
|
|
);
|
|
builderArgs.push("-c.mac.notarize=false");
|
|
}
|
|
|
|
builderArgs.push(...passthrough);
|
|
|
|
// Step 6: invoke electron-builder. pnpm puts node_modules/.bin on PATH
|
|
// for the script run, so spawnSync finds the binary without needing a
|
|
// shell wrapper (avoids any risk of argv interpolation).
|
|
const result = spawnSync("electron-builder", builderArgs, {
|
|
stdio: "inherit",
|
|
cwd: desktopRoot,
|
|
});
|
|
|
|
if (result.error) {
|
|
console.error(
|
|
"[package] failed to spawn electron-builder:",
|
|
result.error.message,
|
|
);
|
|
process.exit(1);
|
|
}
|
|
process.exit(result.status ?? 1);
|
|
}
|
|
|
|
// Only run when invoked as a CLI, not when imported by a test file.
|
|
if (
|
|
process.argv[1] &&
|
|
import.meta.url === pathToFileURL(process.argv[1]).href
|
|
) {
|
|
main();
|
|
}
|