Files
multica/packages/core/runtimes/cli-version.ts
Jiayuan Zhang 1476c268dd refactor(quick-create): exempt git-describe daemons from CLI gate (#2108)
* refactor(quick-create): remove daemon CLI version gate

Local-source daemons report dev-suffixed versions (e.g.
v0.2.15-235-gdaf0e935) that the picker pre-check and server gate both
treat as too old, blocking quick-create during local testing.

Drops the gate end-to-end: removes MinQuickCreateCLIVersion +
CheckMinCLIVersion in pkg/agent, the checkQuickCreateDaemonVersion
handler and readRuntimeCLIVersion helper in handler/issue.go, and the
mirrored cli-version.ts plus the modal's pre-check, blocked-state UI,
and daemon_version_unsupported error branch.

Co-authored-by: multica-agent <github@multica.ai>

* refactor(quick-create): skip daemon CLI version gate in dev

Restores the gate (reverts the full-removal commit) and bypasses it in
non-production environments instead. The motivation for the original
removal — local source-built daemons report a `git describe` version
like v0.2.15-N-gHASH that parses below 0.2.20 and blocks dev testing —
is now handled by checking APP_ENV on the server and NODE_ENV on the
client. Production keeps the original "needs upgrade" UX.

Co-authored-by: multica-agent <github@multica.ai>

* refactor(quick-create): exempt git-describe daemons instead of env bypass

Replaces the per-environment bypass added in the previous commit with a
shared daemon-version signal. CheckMinCLIVersion / checkQuickCreateCliVersion
now treat any daemon whose CLI version matches the
`vX.Y.Z-N-gHASH[-dirty]` git-describe shape as OK; tagged releases keep
going through the normal min-version comparison.

Why: Emacs flagged that (a) NODE_ENV !== "production" also disables the
gate on staging and other non-prod deployments, undoing the protection
for the case the gate was originally written for, and (b) NODE_ENV (web
client) and APP_ENV (server) are not equivalent, so the modal pre-check
and server gate could disagree on the same request. Both go away when
the signal is intrinsic to the daemon's version string.

Co-authored-by: multica-agent <github@multica.ai>

---------

Co-authored-by: multica-agent <github@multica.ai>
2026-05-06 09:00:11 +08:00

75 lines
3.1 KiB
TypeScript

/**
* Frontend mirror of the server's MinQuickCreateCLIVersion gate. The
* agent-create flow (Quick Create modal) requires the daemon's bundled
* multica CLI to be at least this version — older daemons either
* double-create issues on partial CLI failures or mishandle pasted
* screenshot URLs (see PR #1851 / MUL-1496).
*
* Both the frontend pre-validation in the modal and the server's
* `/api/issues/quick-create` handler enforce this; the server is the
* authoritative trust boundary, the frontend just lets us tell the user
* "your daemon needs an upgrade" before they hit submit.
*/
export const MIN_QUICK_CREATE_CLI_VERSION = "0.2.20";
export type CliVersionState = "ok" | "too_old" | "missing";
export interface CliVersionCheck {
state: CliVersionState;
/** What the daemon reported, or empty if missing/unparsable. */
current: string;
/** The hard minimum we gate on. */
min: string;
}
const SEMVER_RE = /v?(\d+)\.(\d+)\.(\d+)/;
// Matches the `git describe --tags --always --dirty` output for a build past
// the latest tag, e.g. `v0.2.15-235-gdaf0e935` or `v0.2.15-235-gdaf0e935-dirty`.
// Daemons built from source (Makefile `make build` / `make daemon`) report this
// shape; tagged releases are bare semver. Treating dev-described daemons as OK
// is what keeps `pnpm dev:desktop` + `make daemon` unblocked without weakening
// the gate for staging or production users running stale stable releases.
const DEV_DESCRIBE_RE = /^v?\d+\.\d+\.\d+-\d+-g[0-9a-fA-F]+/;
function parseSemver(raw: string): [number, number, number] | null {
const m = SEMVER_RE.exec(raw.trim());
if (!m) return null;
return [Number(m[1]), Number(m[2]), Number(m[3])];
}
function lessThan(a: [number, number, number], b: [number, number, number]) {
if (a[0] !== b[0]) return a[0] < b[0];
if (a[1] !== b[1]) return a[1] < b[1];
return a[2] < b[2];
}
/**
* Check a daemon-reported CLI version string against the minimum. Returns
* `"missing"` for empty/unparsable input (fail closed — same policy as the
* server) and `"too_old"` for a parsable version below the threshold.
* Dev-built daemons (git-describe shape) are always OK — the version string
* itself is the shared signal, so frontend and server agree by construction.
*/
export function checkQuickCreateCliVersion(detected: string | undefined | null): CliVersionCheck {
const current = (detected ?? "").trim();
if (DEV_DESCRIBE_RE.test(current)) {
return { state: "ok", current, min: MIN_QUICK_CREATE_CLI_VERSION };
}
const parsed = current ? parseSemver(current) : null;
if (!parsed) {
return { state: "missing", current, min: MIN_QUICK_CREATE_CLI_VERSION };
}
const min = parseSemver(MIN_QUICK_CREATE_CLI_VERSION)!;
if (lessThan(parsed, min)) {
return { state: "too_old", current, min: MIN_QUICK_CREATE_CLI_VERSION };
}
return { state: "ok", current, min: MIN_QUICK_CREATE_CLI_VERSION };
}
/** Pull `cli_version` off a runtime row's loosely-typed metadata bag. */
export function readRuntimeCliVersion(metadata: Record<string, unknown> | undefined): string {
const v = metadata?.cli_version;
return typeof v === "string" ? v : "";
}