feat: add update channel status
Co-authored-by: Richard Poelderl <18185649+p6l-richard@users.noreply.github.com>
This commit is contained in:
@@ -20,4 +20,5 @@ Notes:
|
|||||||
- `--deep` runs live probes (WhatsApp Web + Telegram + Discord + Slack + Signal).
|
- `--deep` runs live probes (WhatsApp Web + Telegram + Discord + Slack + Signal).
|
||||||
- Output includes per-agent session stores when multiple agents are configured.
|
- Output includes per-agent session stores when multiple agents are configured.
|
||||||
- Overview includes Gateway + Node service install/runtime status when available.
|
- Overview includes Gateway + Node service install/runtime status when available.
|
||||||
|
- Overview includes update channel + git SHA (for source checkouts).
|
||||||
- Update info surfaces in the Overview; if an update is available, status prints a hint to run `clawdbot update` (see [Updating](/install/updating)).
|
- Update info surfaces in the Overview; if an update is available, status prints a hint to run `clawdbot update` (see [Updating](/install/updating)).
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ If you installed via **npm/pnpm** (global install, no git metadata), use the pac
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
clawdbot update
|
clawdbot update
|
||||||
|
clawdbot update status
|
||||||
clawdbot update --channel beta
|
clawdbot update --channel beta
|
||||||
clawdbot update --channel dev
|
clawdbot update --channel dev
|
||||||
clawdbot update --tag beta
|
clawdbot update --tag beta
|
||||||
@@ -33,6 +34,20 @@ clawdbot --update
|
|||||||
|
|
||||||
Note: downgrades require confirmation because older versions can break configuration.
|
Note: downgrades require confirmation because older versions can break configuration.
|
||||||
|
|
||||||
|
## `update status`
|
||||||
|
|
||||||
|
Show the active update channel + git tag/branch/SHA (for source checkouts), plus update availability.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
clawdbot update status
|
||||||
|
clawdbot update status --json
|
||||||
|
clawdbot update status --timeout 10
|
||||||
|
```
|
||||||
|
|
||||||
|
Options:
|
||||||
|
- `--json`: print machine-readable status JSON.
|
||||||
|
- `--timeout <seconds>`: timeout for checks (default is 3s).
|
||||||
|
|
||||||
## What it does (git checkout)
|
## What it does (git checkout)
|
||||||
|
|
||||||
Channels:
|
Channels:
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ vi.mock("../infra/update-check.js", async () => {
|
|||||||
);
|
);
|
||||||
return {
|
return {
|
||||||
...actual,
|
...actual,
|
||||||
|
checkUpdateStatus: vi.fn(),
|
||||||
fetchNpmTagVersion: vi.fn(),
|
fetchNpmTagVersion: vi.fn(),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@@ -72,13 +73,38 @@ describe("update-cli", () => {
|
|||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
const { resolveClawdbotPackageRoot } = await import("../infra/clawdbot-root.js");
|
const { resolveClawdbotPackageRoot } = await import("../infra/clawdbot-root.js");
|
||||||
const { readConfigFileSnapshot } = await import("../config/config.js");
|
const { readConfigFileSnapshot } = await import("../config/config.js");
|
||||||
const { fetchNpmTagVersion } = await import("../infra/update-check.js");
|
const { checkUpdateStatus, fetchNpmTagVersion } = await import("../infra/update-check.js");
|
||||||
vi.mocked(resolveClawdbotPackageRoot).mockResolvedValue(process.cwd());
|
vi.mocked(resolveClawdbotPackageRoot).mockResolvedValue(process.cwd());
|
||||||
vi.mocked(readConfigFileSnapshot).mockResolvedValue(baseSnapshot);
|
vi.mocked(readConfigFileSnapshot).mockResolvedValue(baseSnapshot);
|
||||||
vi.mocked(fetchNpmTagVersion).mockResolvedValue({
|
vi.mocked(fetchNpmTagVersion).mockResolvedValue({
|
||||||
tag: "latest",
|
tag: "latest",
|
||||||
version: "9999.0.0",
|
version: "9999.0.0",
|
||||||
});
|
});
|
||||||
|
vi.mocked(checkUpdateStatus).mockResolvedValue({
|
||||||
|
root: "/test/path",
|
||||||
|
installKind: "git",
|
||||||
|
packageManager: "pnpm",
|
||||||
|
git: {
|
||||||
|
root: "/test/path",
|
||||||
|
sha: "abcdef1234567890",
|
||||||
|
tag: "v1.2.3",
|
||||||
|
branch: "main",
|
||||||
|
upstream: "origin/main",
|
||||||
|
dirty: false,
|
||||||
|
ahead: 0,
|
||||||
|
behind: 0,
|
||||||
|
fetchOk: true,
|
||||||
|
},
|
||||||
|
deps: {
|
||||||
|
manager: "pnpm",
|
||||||
|
status: "ok",
|
||||||
|
lockfilePath: "/test/path/pnpm-lock.yaml",
|
||||||
|
markerPath: "/test/path/node_modules",
|
||||||
|
},
|
||||||
|
registry: {
|
||||||
|
latestVersion: "1.2.3",
|
||||||
|
},
|
||||||
|
});
|
||||||
setTty(false);
|
setTty(false);
|
||||||
setStdoutTty(false);
|
setStdoutTty(false);
|
||||||
});
|
});
|
||||||
@@ -120,6 +146,28 @@ describe("update-cli", () => {
|
|||||||
expect(defaultRuntime.log).toHaveBeenCalled();
|
expect(defaultRuntime.log).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("updateStatusCommand prints table output", async () => {
|
||||||
|
const { defaultRuntime } = await import("../runtime.js");
|
||||||
|
const { updateStatusCommand } = await import("./update-cli.js");
|
||||||
|
|
||||||
|
await updateStatusCommand({ json: false });
|
||||||
|
|
||||||
|
const logs = vi.mocked(defaultRuntime.log).mock.calls.map((call) => call[0]);
|
||||||
|
expect(logs.join("\n")).toContain("Clawdbot update status");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("updateStatusCommand emits JSON", async () => {
|
||||||
|
const { defaultRuntime } = await import("../runtime.js");
|
||||||
|
const { updateStatusCommand } = await import("./update-cli.js");
|
||||||
|
|
||||||
|
await updateStatusCommand({ json: true });
|
||||||
|
|
||||||
|
const last = vi.mocked(defaultRuntime.log).mock.calls.at(-1)?.[0];
|
||||||
|
expect(typeof last).toBe("string");
|
||||||
|
const parsed = JSON.parse(String(last));
|
||||||
|
expect(parsed.channel.value).toBe("stable");
|
||||||
|
});
|
||||||
|
|
||||||
it("defaults to dev channel for git installs when unset", async () => {
|
it("defaults to dev channel for git installs when unset", async () => {
|
||||||
const { runGatewayUpdate } = await import("../infra/update-runner.js");
|
const { runGatewayUpdate } = await import("../infra/update-runner.js");
|
||||||
const { updateCommand } = await import("./update-cli.js");
|
const { updateCommand } = await import("./update-cli.js");
|
||||||
|
|||||||
@@ -5,7 +5,11 @@ import type { Command } from "commander";
|
|||||||
|
|
||||||
import { readConfigFileSnapshot, writeConfigFile } from "../config/config.js";
|
import { readConfigFileSnapshot, writeConfigFile } from "../config/config.js";
|
||||||
import { resolveClawdbotPackageRoot } from "../infra/clawdbot-root.js";
|
import { resolveClawdbotPackageRoot } from "../infra/clawdbot-root.js";
|
||||||
import { compareSemverStrings, fetchNpmTagVersion } from "../infra/update-check.js";
|
import {
|
||||||
|
checkUpdateStatus,
|
||||||
|
compareSemverStrings,
|
||||||
|
fetchNpmTagVersion,
|
||||||
|
} from "../infra/update-check.js";
|
||||||
import { parseSemver } from "../infra/runtime-guard.js";
|
import { parseSemver } from "../infra/runtime-guard.js";
|
||||||
import {
|
import {
|
||||||
runGatewayUpdate,
|
runGatewayUpdate,
|
||||||
@@ -17,13 +21,22 @@ import {
|
|||||||
channelToNpmTag,
|
channelToNpmTag,
|
||||||
DEFAULT_GIT_CHANNEL,
|
DEFAULT_GIT_CHANNEL,
|
||||||
DEFAULT_PACKAGE_CHANNEL,
|
DEFAULT_PACKAGE_CHANNEL,
|
||||||
|
formatUpdateChannelLabel,
|
||||||
normalizeUpdateChannel,
|
normalizeUpdateChannel,
|
||||||
|
resolveEffectiveUpdateChannel,
|
||||||
|
type UpdateChannel,
|
||||||
} from "../infra/update-channels.js";
|
} from "../infra/update-channels.js";
|
||||||
import { defaultRuntime } from "../runtime.js";
|
import { defaultRuntime } from "../runtime.js";
|
||||||
import { formatDocsLink } from "../terminal/links.js";
|
import { formatDocsLink } from "../terminal/links.js";
|
||||||
import { formatCliCommand } from "./command-format.js";
|
import { formatCliCommand } from "./command-format.js";
|
||||||
import { stylePromptMessage } from "../terminal/prompt-style.js";
|
import { stylePromptMessage } from "../terminal/prompt-style.js";
|
||||||
import { theme } from "../terminal/theme.js";
|
import { theme } from "../terminal/theme.js";
|
||||||
|
import { renderTable } from "../terminal/table.js";
|
||||||
|
import {
|
||||||
|
formatUpdateAvailableHint,
|
||||||
|
formatUpdateOneLiner,
|
||||||
|
resolveUpdateAvailability,
|
||||||
|
} from "../commands/status.update.js";
|
||||||
|
|
||||||
export type UpdateCommandOptions = {
|
export type UpdateCommandOptions = {
|
||||||
json?: boolean;
|
json?: boolean;
|
||||||
@@ -32,6 +45,10 @@ export type UpdateCommandOptions = {
|
|||||||
tag?: string;
|
tag?: string;
|
||||||
timeout?: string;
|
timeout?: string;
|
||||||
};
|
};
|
||||||
|
export type UpdateStatusOptions = {
|
||||||
|
json?: boolean;
|
||||||
|
timeout?: string;
|
||||||
|
};
|
||||||
|
|
||||||
const STEP_LABELS: Record<string, string> = {
|
const STEP_LABELS: Record<string, string> = {
|
||||||
"clean check": "Working directory is clean",
|
"clean check": "Working directory is clean",
|
||||||
@@ -113,6 +130,125 @@ async function isGitCheckout(root: string): Promise<boolean> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatGitStatusLine(params: {
|
||||||
|
branch: string | null;
|
||||||
|
tag: string | null;
|
||||||
|
sha: string | null;
|
||||||
|
}): string {
|
||||||
|
const shortSha = params.sha ? params.sha.slice(0, 8) : null;
|
||||||
|
const branch = params.branch && params.branch !== "HEAD" ? params.branch : null;
|
||||||
|
const tag = params.tag;
|
||||||
|
const parts = [
|
||||||
|
branch ?? (tag ? "detached" : "git"),
|
||||||
|
tag ? `tag ${tag}` : null,
|
||||||
|
shortSha ? `@ ${shortSha}` : null,
|
||||||
|
].filter(Boolean);
|
||||||
|
return parts.join(" · ");
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateStatusCommand(opts: UpdateStatusOptions): Promise<void> {
|
||||||
|
const timeoutMs = opts.timeout ? Number.parseInt(opts.timeout, 10) * 1000 : undefined;
|
||||||
|
if (timeoutMs !== undefined && (Number.isNaN(timeoutMs) || timeoutMs <= 0)) {
|
||||||
|
defaultRuntime.error("--timeout must be a positive integer (seconds)");
|
||||||
|
defaultRuntime.exit(1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const root =
|
||||||
|
(await resolveClawdbotPackageRoot({
|
||||||
|
moduleUrl: import.meta.url,
|
||||||
|
argv1: process.argv[1],
|
||||||
|
cwd: process.cwd(),
|
||||||
|
})) ?? process.cwd();
|
||||||
|
const configSnapshot = await readConfigFileSnapshot();
|
||||||
|
const configChannel = configSnapshot.valid
|
||||||
|
? normalizeUpdateChannel(configSnapshot.config.update?.channel)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
const update = await checkUpdateStatus({
|
||||||
|
root,
|
||||||
|
timeoutMs: timeoutMs ?? 3500,
|
||||||
|
fetchGit: true,
|
||||||
|
includeRegistry: true,
|
||||||
|
});
|
||||||
|
const channelInfo = resolveEffectiveUpdateChannel({
|
||||||
|
configChannel,
|
||||||
|
installKind: update.installKind,
|
||||||
|
git: update.git ? { tag: update.git.tag, branch: update.git.branch } : undefined,
|
||||||
|
});
|
||||||
|
const channelLabel = formatUpdateChannelLabel({
|
||||||
|
channel: channelInfo.channel,
|
||||||
|
source: channelInfo.source,
|
||||||
|
gitTag: update.git?.tag ?? null,
|
||||||
|
gitBranch: update.git?.branch ?? null,
|
||||||
|
});
|
||||||
|
const gitLabel =
|
||||||
|
update.installKind === "git"
|
||||||
|
? formatGitStatusLine({
|
||||||
|
branch: update.git?.branch ?? null,
|
||||||
|
tag: update.git?.tag ?? null,
|
||||||
|
sha: update.git?.sha ?? null,
|
||||||
|
})
|
||||||
|
: null;
|
||||||
|
const updateAvailability = resolveUpdateAvailability(update);
|
||||||
|
const updateLine = formatUpdateOneLiner(update).replace(/^Update:\s*/i, "");
|
||||||
|
|
||||||
|
if (opts.json) {
|
||||||
|
defaultRuntime.log(
|
||||||
|
JSON.stringify(
|
||||||
|
{
|
||||||
|
update,
|
||||||
|
channel: {
|
||||||
|
value: channelInfo.channel,
|
||||||
|
source: channelInfo.source,
|
||||||
|
label: channelLabel,
|
||||||
|
config: configChannel,
|
||||||
|
},
|
||||||
|
availability: updateAvailability,
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
2,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tableWidth = Math.max(60, (process.stdout.columns ?? 120) - 1);
|
||||||
|
const installLabel =
|
||||||
|
update.installKind === "git"
|
||||||
|
? `git (${update.root ?? "unknown"})`
|
||||||
|
: update.installKind === "package"
|
||||||
|
? update.packageManager
|
||||||
|
: "unknown";
|
||||||
|
const rows = [
|
||||||
|
{ Item: "Install", Value: installLabel },
|
||||||
|
{ Item: "Channel", Value: channelLabel },
|
||||||
|
...(gitLabel ? [{ Item: "Git", Value: gitLabel }] : []),
|
||||||
|
{
|
||||||
|
Item: "Update",
|
||||||
|
Value: updateAvailability.available ? theme.warn(`available · ${updateLine}`) : updateLine,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
defaultRuntime.log(theme.heading("Clawdbot update status"));
|
||||||
|
defaultRuntime.log("");
|
||||||
|
defaultRuntime.log(
|
||||||
|
renderTable({
|
||||||
|
width: tableWidth,
|
||||||
|
columns: [
|
||||||
|
{ key: "Item", header: "Item", minWidth: 10 },
|
||||||
|
{ key: "Value", header: "Value", flex: true, minWidth: 24 },
|
||||||
|
],
|
||||||
|
rows,
|
||||||
|
}).trimEnd(),
|
||||||
|
);
|
||||||
|
defaultRuntime.log("");
|
||||||
|
const updateHint = formatUpdateAvailableHint(update);
|
||||||
|
if (updateHint) {
|
||||||
|
defaultRuntime.log(theme.warn(updateHint));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function getStepLabel(step: UpdateStepInfo): string {
|
function getStepLabel(step: UpdateStepInfo): string {
|
||||||
return STEP_LABELS[step.name] ?? step.name;
|
return STEP_LABELS[step.name] ?? step.name;
|
||||||
}
|
}
|
||||||
@@ -433,7 +569,7 @@ export async function updateCommand(opts: UpdateCommandOptions): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function registerUpdateCli(program: Command) {
|
export function registerUpdateCli(program: Command) {
|
||||||
program
|
const update = program
|
||||||
.command("update")
|
.command("update")
|
||||||
.description("Update Clawdbot to the latest version")
|
.description("Update Clawdbot to the latest version")
|
||||||
.option("--json", "Output result as JSON", false)
|
.option("--json", "Output result as JSON", false)
|
||||||
@@ -476,4 +612,36 @@ ${theme.muted("Docs:")} ${formatDocsLink("/cli/update", "docs.clawd.bot/cli/upda
|
|||||||
defaultRuntime.exit(1);
|
defaultRuntime.exit(1);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
update
|
||||||
|
.command("status")
|
||||||
|
.description("Show update channel and version status")
|
||||||
|
.option("--json", "Output result as JSON", false)
|
||||||
|
.option("--timeout <seconds>", "Timeout for update checks in seconds (default: 3)")
|
||||||
|
.addHelpText(
|
||||||
|
"after",
|
||||||
|
() =>
|
||||||
|
`
|
||||||
|
Examples:
|
||||||
|
clawdbot update status
|
||||||
|
clawdbot update status --json
|
||||||
|
clawdbot update status --timeout 10
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
- Shows current update channel (stable/beta/dev) and source
|
||||||
|
- Includes git tag/branch/SHA for source checkouts
|
||||||
|
|
||||||
|
${theme.muted("Docs:")} ${formatDocsLink("/cli/update", "docs.clawd.bot/cli/update")}`,
|
||||||
|
)
|
||||||
|
.action(async (opts) => {
|
||||||
|
try {
|
||||||
|
await updateStatusCommand({
|
||||||
|
json: Boolean(opts.json),
|
||||||
|
timeout: opts.timeout as string | undefined,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
defaultRuntime.error(String(err));
|
||||||
|
defaultRuntime.exit(1);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,11 @@ import { inspectPortUsage } from "../infra/ports.js";
|
|||||||
import { readRestartSentinel } from "../infra/restart-sentinel.js";
|
import { readRestartSentinel } from "../infra/restart-sentinel.js";
|
||||||
import { readTailscaleStatusJson } from "../infra/tailscale.js";
|
import { readTailscaleStatusJson } from "../infra/tailscale.js";
|
||||||
import { checkUpdateStatus, compareSemverStrings } from "../infra/update-check.js";
|
import { checkUpdateStatus, compareSemverStrings } from "../infra/update-check.js";
|
||||||
|
import {
|
||||||
|
formatUpdateChannelLabel,
|
||||||
|
normalizeUpdateChannel,
|
||||||
|
resolveEffectiveUpdateChannel,
|
||||||
|
} from "../infra/update-channels.js";
|
||||||
import { getRemoteSkillEligibility } from "../infra/skills-remote.js";
|
import { getRemoteSkillEligibility } from "../infra/skills-remote.js";
|
||||||
import { runExec } from "../process/exec.js";
|
import { runExec } from "../process/exec.js";
|
||||||
import type { RuntimeEnv } from "../runtime.js";
|
import type { RuntimeEnv } from "../runtime.js";
|
||||||
@@ -87,6 +92,33 @@ export async function statusAllCommand(
|
|||||||
fetchGit: true,
|
fetchGit: true,
|
||||||
includeRegistry: true,
|
includeRegistry: true,
|
||||||
});
|
});
|
||||||
|
const configChannel = normalizeUpdateChannel(cfg.update?.channel);
|
||||||
|
const channelInfo = resolveEffectiveUpdateChannel({
|
||||||
|
configChannel,
|
||||||
|
installKind: update.installKind,
|
||||||
|
git: update.git ? { tag: update.git.tag, branch: update.git.branch } : undefined,
|
||||||
|
});
|
||||||
|
const channelLabel = formatUpdateChannelLabel({
|
||||||
|
channel: channelInfo.channel,
|
||||||
|
source: channelInfo.source,
|
||||||
|
gitTag: update.git?.tag ?? null,
|
||||||
|
gitBranch: update.git?.branch ?? null,
|
||||||
|
});
|
||||||
|
const gitLabel =
|
||||||
|
update.installKind === "git"
|
||||||
|
? (() => {
|
||||||
|
const shortSha = update.git?.sha ? update.git.sha.slice(0, 8) : null;
|
||||||
|
const branch =
|
||||||
|
update.git?.branch && update.git.branch !== "HEAD" ? update.git.branch : null;
|
||||||
|
const tag = update.git?.tag ?? null;
|
||||||
|
const parts = [
|
||||||
|
branch ?? (tag ? "detached" : "git"),
|
||||||
|
tag ? `tag ${tag}` : null,
|
||||||
|
shortSha ? `@ ${shortSha}` : null,
|
||||||
|
].filter(Boolean);
|
||||||
|
return parts.join(" · ");
|
||||||
|
})()
|
||||||
|
: null;
|
||||||
progress.tick();
|
progress.tick();
|
||||||
|
|
||||||
progress.setLabel("Probing gateway…");
|
progress.setLabel("Probing gateway…");
|
||||||
@@ -333,6 +365,8 @@ export async function statusAllCommand(
|
|||||||
? `${tailscaleMode} · ${tailscale.backendState ?? "unknown"} · ${tailscale.dnsName} · ${tailscaleHttpsUrl}`
|
? `${tailscaleMode} · ${tailscale.backendState ?? "unknown"} · ${tailscale.dnsName} · ${tailscaleHttpsUrl}`
|
||||||
: `${tailscaleMode} · ${tailscale.backendState ?? "unknown"} · magicdns unknown`,
|
: `${tailscaleMode} · ${tailscale.backendState ?? "unknown"} · magicdns unknown`,
|
||||||
},
|
},
|
||||||
|
{ Item: "Channel", Value: channelLabel },
|
||||||
|
...(gitLabel ? [{ Item: "Git", Value: gitLabel }] : []),
|
||||||
{ Item: "Update", Value: updateLine },
|
{ Item: "Update", Value: updateLine },
|
||||||
{
|
{
|
||||||
Item: "Gateway",
|
Item: "Gateway",
|
||||||
|
|||||||
@@ -33,6 +33,11 @@ import {
|
|||||||
} from "./status.update.js";
|
} from "./status.update.js";
|
||||||
import { formatGatewayAuthUsed } from "./status-all/format.js";
|
import { formatGatewayAuthUsed } from "./status-all/format.js";
|
||||||
import { statusAllCommand } from "./status-all.js";
|
import { statusAllCommand } from "./status-all.js";
|
||||||
|
import {
|
||||||
|
formatUpdateChannelLabel,
|
||||||
|
normalizeUpdateChannel,
|
||||||
|
resolveEffectiveUpdateChannel,
|
||||||
|
} from "../infra/update-channels.js";
|
||||||
|
|
||||||
export async function statusCommand(
|
export async function statusCommand(
|
||||||
opts: {
|
opts: {
|
||||||
@@ -116,6 +121,13 @@ export async function statusCommand(
|
|||||||
)
|
)
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
|
const configChannel = normalizeUpdateChannel(cfg.update?.channel);
|
||||||
|
const channelInfo = resolveEffectiveUpdateChannel({
|
||||||
|
configChannel,
|
||||||
|
installKind: update.installKind,
|
||||||
|
git: update.git ? { tag: update.git.tag, branch: update.git.branch } : undefined,
|
||||||
|
});
|
||||||
|
|
||||||
if (opts.json) {
|
if (opts.json) {
|
||||||
const [daemon, nodeDaemon] = await Promise.all([
|
const [daemon, nodeDaemon] = await Promise.all([
|
||||||
getDaemonStatusSummary(),
|
getDaemonStatusSummary(),
|
||||||
@@ -127,6 +139,8 @@ export async function statusCommand(
|
|||||||
...summary,
|
...summary,
|
||||||
os: osSummary,
|
os: osSummary,
|
||||||
update,
|
update,
|
||||||
|
updateChannel: channelInfo.channel,
|
||||||
|
updateChannelSource: channelInfo.source,
|
||||||
memory,
|
memory,
|
||||||
memoryPlugin,
|
memoryPlugin,
|
||||||
gateway: {
|
gateway: {
|
||||||
@@ -295,6 +309,27 @@ export async function statusCommand(
|
|||||||
|
|
||||||
const updateAvailability = resolveUpdateAvailability(update);
|
const updateAvailability = resolveUpdateAvailability(update);
|
||||||
const updateLine = formatUpdateOneLiner(update).replace(/^Update:\s*/i, "");
|
const updateLine = formatUpdateOneLiner(update).replace(/^Update:\s*/i, "");
|
||||||
|
const channelLabel = formatUpdateChannelLabel({
|
||||||
|
channel: channelInfo.channel,
|
||||||
|
source: channelInfo.source,
|
||||||
|
gitTag: update.git?.tag ?? null,
|
||||||
|
gitBranch: update.git?.branch ?? null,
|
||||||
|
});
|
||||||
|
const gitLabel =
|
||||||
|
update.installKind === "git"
|
||||||
|
? (() => {
|
||||||
|
const shortSha = update.git?.sha ? update.git.sha.slice(0, 8) : null;
|
||||||
|
const branch =
|
||||||
|
update.git?.branch && update.git.branch !== "HEAD" ? update.git.branch : null;
|
||||||
|
const tag = update.git?.tag ?? null;
|
||||||
|
const parts = [
|
||||||
|
branch ?? (tag ? "detached" : "git"),
|
||||||
|
tag ? `tag ${tag}` : null,
|
||||||
|
shortSha ? `@ ${shortSha}` : null,
|
||||||
|
].filter(Boolean);
|
||||||
|
return parts.join(" · ");
|
||||||
|
})()
|
||||||
|
: null;
|
||||||
|
|
||||||
const overviewRows = [
|
const overviewRows = [
|
||||||
{ Item: "Dashboard", Value: dashboard },
|
{ Item: "Dashboard", Value: dashboard },
|
||||||
@@ -308,6 +343,8 @@ export async function statusCommand(
|
|||||||
? `${tailscaleMode} · ${tailscaleDns} · ${tailscaleHttpsUrl}`
|
? `${tailscaleMode} · ${tailscaleDns} · ${tailscaleHttpsUrl}`
|
||||||
: warn(`${tailscaleMode} · magicdns unknown`),
|
: warn(`${tailscaleMode} · magicdns unknown`),
|
||||||
},
|
},
|
||||||
|
{ Item: "Channel", Value: channelLabel },
|
||||||
|
...(gitLabel ? [{ Item: "Git", Value: gitLabel }] : []),
|
||||||
{
|
{
|
||||||
Item: "Update",
|
Item: "Update",
|
||||||
Value: updateAvailability.available ? warn(`available · ${updateLine}`) : updateLine,
|
Value: updateAvailability.available ? warn(`available · ${updateLine}`) : updateLine,
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
export type UpdateChannel = "stable" | "beta" | "dev";
|
export type UpdateChannel = "stable" | "beta" | "dev";
|
||||||
|
export type UpdateChannelSource = "config" | "git-tag" | "git-branch" | "default";
|
||||||
|
|
||||||
export const DEFAULT_PACKAGE_CHANNEL: UpdateChannel = "stable";
|
export const DEFAULT_PACKAGE_CHANNEL: UpdateChannel = "stable";
|
||||||
export const DEFAULT_GIT_CHANNEL: UpdateChannel = "dev";
|
export const DEFAULT_GIT_CHANNEL: UpdateChannel = "dev";
|
||||||
@@ -24,3 +25,49 @@ export function isBetaTag(tag: string): boolean {
|
|||||||
export function isStableTag(tag: string): boolean {
|
export function isStableTag(tag: string): boolean {
|
||||||
return !isBetaTag(tag);
|
return !isBetaTag(tag);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function resolveEffectiveUpdateChannel(params: {
|
||||||
|
configChannel?: UpdateChannel | null;
|
||||||
|
installKind: "git" | "package" | "unknown";
|
||||||
|
git?: { tag?: string | null; branch?: string | null };
|
||||||
|
}): { channel: UpdateChannel; source: UpdateChannelSource } {
|
||||||
|
if (params.configChannel) {
|
||||||
|
return { channel: params.configChannel, source: "config" };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (params.installKind === "git") {
|
||||||
|
const tag = params.git?.tag;
|
||||||
|
if (tag) {
|
||||||
|
return { channel: isBetaTag(tag) ? "beta" : "stable", source: "git-tag" };
|
||||||
|
}
|
||||||
|
const branch = params.git?.branch;
|
||||||
|
if (branch && branch !== "HEAD") {
|
||||||
|
return { channel: "dev", source: "git-branch" };
|
||||||
|
}
|
||||||
|
return { channel: DEFAULT_GIT_CHANNEL, source: "default" };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (params.installKind === "package") {
|
||||||
|
return { channel: DEFAULT_PACKAGE_CHANNEL, source: "default" };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { channel: DEFAULT_PACKAGE_CHANNEL, source: "default" };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatUpdateChannelLabel(params: {
|
||||||
|
channel: UpdateChannel;
|
||||||
|
source: UpdateChannelSource;
|
||||||
|
gitTag?: string | null;
|
||||||
|
gitBranch?: string | null;
|
||||||
|
}): string {
|
||||||
|
if (params.source === "config") return `${params.channel} (config)`;
|
||||||
|
if (params.source === "git-tag") {
|
||||||
|
return params.gitTag ? `${params.channel} (${params.gitTag})` : `${params.channel} (tag)`;
|
||||||
|
}
|
||||||
|
if (params.source === "git-branch") {
|
||||||
|
return params.gitBranch
|
||||||
|
? `${params.channel} (${params.gitBranch})`
|
||||||
|
: `${params.channel} (branch)`;
|
||||||
|
}
|
||||||
|
return `${params.channel} (default)`;
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ export type PackageManager = "pnpm" | "bun" | "npm" | "unknown";
|
|||||||
|
|
||||||
export type GitUpdateStatus = {
|
export type GitUpdateStatus = {
|
||||||
root: string;
|
root: string;
|
||||||
|
sha: string | null;
|
||||||
|
tag: string | null;
|
||||||
branch: string | null;
|
branch: string | null;
|
||||||
upstream: string | null;
|
upstream: string | null;
|
||||||
dirty: boolean | null;
|
dirty: boolean | null;
|
||||||
@@ -90,6 +92,8 @@ export async function checkGitUpdateStatus(params: {
|
|||||||
|
|
||||||
const base: GitUpdateStatus = {
|
const base: GitUpdateStatus = {
|
||||||
root,
|
root,
|
||||||
|
sha: null,
|
||||||
|
tag: null,
|
||||||
branch: null,
|
branch: null,
|
||||||
upstream: null,
|
upstream: null,
|
||||||
dirty: null,
|
dirty: null,
|
||||||
@@ -107,6 +111,17 @@ export async function checkGitUpdateStatus(params: {
|
|||||||
}
|
}
|
||||||
const branch = branchRes.stdout.trim() || null;
|
const branch = branchRes.stdout.trim() || null;
|
||||||
|
|
||||||
|
const shaRes = await runCommandWithTimeout(["git", "-C", root, "rev-parse", "HEAD"], {
|
||||||
|
timeoutMs,
|
||||||
|
}).catch(() => null);
|
||||||
|
const sha = shaRes && shaRes.code === 0 ? shaRes.stdout.trim() : null;
|
||||||
|
|
||||||
|
const tagRes = await runCommandWithTimeout(
|
||||||
|
["git", "-C", root, "describe", "--tags", "--exact-match"],
|
||||||
|
{ timeoutMs },
|
||||||
|
).catch(() => null);
|
||||||
|
const tag = tagRes && tagRes.code === 0 ? tagRes.stdout.trim() : null;
|
||||||
|
|
||||||
const upstreamRes = await runCommandWithTimeout(
|
const upstreamRes = await runCommandWithTimeout(
|
||||||
["git", "-C", root, "rev-parse", "--abbrev-ref", "@{upstream}"],
|
["git", "-C", root, "rev-parse", "--abbrev-ref", "@{upstream}"],
|
||||||
{ timeoutMs },
|
{ timeoutMs },
|
||||||
@@ -144,6 +159,8 @@ export async function checkGitUpdateStatus(params: {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
root,
|
root,
|
||||||
|
sha,
|
||||||
|
tag,
|
||||||
branch,
|
branch,
|
||||||
upstream,
|
upstream,
|
||||||
dirty,
|
dirty,
|
||||||
|
|||||||
Reference in New Issue
Block a user