mirror of
https://github.com/profullstack/logicsrc.git
synced 2026-08-13 14:37:26 +00:00
fix(cli): make logicsrc update actually check for updates
`update` was three hardcoded console.log lines: it printed 0.1.0 as both current and latest, claimed "already up to date", and never checked or installed anything. `--version` was hardcoded the same way. A version comparison alone could not have worked either. install.sh ships a tarball of the master branch, not a tagged release, and packages/cli/package.json has been 0.1.0 since the repo began, so version equality says "up to date" no matter how far master has moved. The commit is the real signal. - install.sh records ref/commit/version/installed_at to $LOGICSRC_HOME/install.json. The sha comes from GitHub's Accept: application/vnd.github.sha media type, so this needs no jq. It is resolved before the download on purpose: if master moves mid-install we under-report (a spurious update) rather than falsely claim to be current. - update compares the installed commit against the remote ref head, falls back to version comparison for installs predating the manifest, and reports why it reached its verdict instead of just asserting one. --check reports without installing; otherwise it re-runs the installer. - --version now reads the package's real version. Verified against live GitHub in all three states: matching commit, stale commit, and no manifest. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
cf475f0f4c
commit
8c3c0bb12a
4 changed files with 339 additions and 8 deletions
|
|
@ -56,15 +56,40 @@ check_node() {
|
||||||
need npm
|
need npm
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Commit the tracked ref currently points at. The .sha media type returns it as
|
||||||
|
# bare text, so this needs no jq. Empty on failure — never fatal, since a missing
|
||||||
|
# sha only costs `logicsrc update` its precision.
|
||||||
|
resolve_sha() {
|
||||||
|
curl -fsSL -H "Accept: application/vnd.github.sha" \
|
||||||
|
"https://api.github.com/repos/$GH_REPO/commits/$LOGICSRC_REF" 2>/dev/null || true
|
||||||
|
}
|
||||||
|
|
||||||
|
# Records what we installed so `logicsrc update` can compare against the remote.
|
||||||
|
# Without this the CLI has no way to know which commit it is running, and can
|
||||||
|
# only ever guess that it is current.
|
||||||
|
write_manifest() {
|
||||||
|
_version="$(node -p "require('$SRC_DIR/packages/cli/package.json').version" 2>/dev/null || echo '')"
|
||||||
|
cat > "$LOGICSRC_HOME/install.json" <<EOF
|
||||||
|
{
|
||||||
|
"ref": "$LOGICSRC_REF",
|
||||||
|
"commit": "$1",
|
||||||
|
"version": "$_version",
|
||||||
|
"installed_at": "$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
do_install() {
|
do_install() {
|
||||||
detect_os; check_node
|
detect_os; check_node
|
||||||
need curl; need tar
|
need curl; need tar
|
||||||
info "fetching logicsrc@$LOGICSRC_REF from GitHub…"
|
info "fetching logicsrc@$LOGICSRC_REF from GitHub…"
|
||||||
mkdir -p "$SRC_DIR"
|
mkdir -p "$SRC_DIR"
|
||||||
|
sha="$(resolve_sha)"
|
||||||
|
short_sha="$(printf '%.7s' "$sha")"
|
||||||
tmp="$(mktemp -d)"
|
tmp="$(mktemp -d)"
|
||||||
curl -fsSL "$TARBALL_URL" | tar -xz -C "$tmp" --strip-components=1
|
curl -fsSL "$TARBALL_URL" | tar -xz -C "$tmp" --strip-components=1
|
||||||
rm -rf "$SRC_DIR"; mkdir -p "$(dirname "$SRC_DIR")"; mv "$tmp" "$SRC_DIR"
|
rm -rf "$SRC_DIR"; mkdir -p "$(dirname "$SRC_DIR")"; mv "$tmp" "$SRC_DIR"
|
||||||
ok "downloaded to $SRC_DIR"
|
ok "downloaded to $SRC_DIR${short_sha:+ ($short_sha)}"
|
||||||
|
|
||||||
info "installing dependencies (this can take a minute)…"
|
info "installing dependencies (this can take a minute)…"
|
||||||
( cd "$SRC_DIR" && npm install --no-audit --no-fund --ignore-scripts >/dev/null 2>&1 ) || fail "npm install failed — run it by hand in $SRC_DIR"
|
( cd "$SRC_DIR" && npm install --no-audit --no-fund --ignore-scripts >/dev/null 2>&1 ) || fail "npm install failed — run it by hand in $SRC_DIR"
|
||||||
|
|
@ -77,6 +102,7 @@ do_install() {
|
||||||
exec node "$SRC_DIR/packages/cli/dist/index.js" "\$@"
|
exec node "$SRC_DIR/packages/cli/dist/index.js" "\$@"
|
||||||
EOF
|
EOF
|
||||||
chmod +x "$WRAPPER"
|
chmod +x "$WRAPPER"
|
||||||
|
write_manifest "$sha"
|
||||||
ok "installed logicsrc → $WRAPPER"
|
ok "installed logicsrc → $WRAPPER"
|
||||||
|
|
||||||
case ":$PATH:" in
|
case ":$PATH:" in
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
|
import { spawnSync } from "node:child_process";
|
||||||
import { readFileSync } from "node:fs";
|
import { readFileSync } from "node:fs";
|
||||||
import { evaluateAccountPolicy, scoreAccountActionRisk } from "@logicsrc/account-core";
|
import { evaluateAccountPolicy, scoreAccountActionRisk } from "@logicsrc/account-core";
|
||||||
import { Command } from "commander";
|
import { Command } from "commander";
|
||||||
|
|
@ -30,6 +31,17 @@ import { exportOpenSpecSummary, importOpenSpec, writeOpenSpecChange } from "./op
|
||||||
import { registerOntologyCommands } from "./ontology.js";
|
import { registerOntologyCommands } from "./ontology.js";
|
||||||
import { registerPrdCommands } from "./prd.js";
|
import { registerPrdCommands } from "./prd.js";
|
||||||
import { defaultPluginRegistry } from "./registry.js";
|
import { defaultPluginRegistry } from "./registry.js";
|
||||||
|
import {
|
||||||
|
GH_REPO,
|
||||||
|
INSTALL_URL,
|
||||||
|
fetchRemoteState,
|
||||||
|
installHome,
|
||||||
|
localVersion,
|
||||||
|
readManifest,
|
||||||
|
short,
|
||||||
|
trackedRef,
|
||||||
|
updateStatus
|
||||||
|
} from "./update.js";
|
||||||
|
|
||||||
process.stdout.on("error", (error: NodeJS.ErrnoException) => {
|
process.stdout.on("error", (error: NodeJS.ErrnoException) => {
|
||||||
if (error.code === "EPIPE") {
|
if (error.code === "EPIPE") {
|
||||||
|
|
@ -51,7 +63,7 @@ program
|
||||||
.option("--waiting-arcade", "Alias for --arcade.")
|
.option("--waiting-arcade", "Alias for --arcade.")
|
||||||
.option("--waiting-game <game>", "Alias for --arcade=<game>.")
|
.option("--waiting-game <game>", "Alias for --arcade=<game>.")
|
||||||
.option("--no-arcade", "Disable Waiting Arcade.")
|
.option("--no-arcade", "Disable Waiting Arcade.")
|
||||||
.version("0.1.0");
|
.version(localVersion());
|
||||||
|
|
||||||
program.action(async (options) => {
|
program.action(async (options) => {
|
||||||
if (!options.yolo) {
|
if (!options.yolo) {
|
||||||
|
|
@ -744,12 +756,45 @@ program.command("tui").description("Launch the tmux-friendly TUI.").action(() =>
|
||||||
console.log("\nPlugin status:\n" + renderPluginStatus());
|
console.log("\nPlugin status:\n" + renderPluginStatus());
|
||||||
});
|
});
|
||||||
|
|
||||||
program.command("update").alias("upgrade").description("Update the local LogicSRC CLI.").action(() => {
|
program
|
||||||
console.log("Current version: 0.1.0");
|
.command("update")
|
||||||
console.log("Latest version: 0.1.0");
|
.alias("upgrade")
|
||||||
console.log("LogicSRC CLI is already up to date.");
|
.description("Update the local LogicSRC CLI.")
|
||||||
console.log("Config preserved at $HOME/.logicsrc");
|
.option("--check", "Report whether an update is available without installing it")
|
||||||
});
|
.action(async (options) => {
|
||||||
|
const manifest = readManifest();
|
||||||
|
const ref = trackedRef(manifest);
|
||||||
|
const local = { version: localVersion(), commit: manifest?.commit ?? null };
|
||||||
|
|
||||||
|
console.log(`Tracking: ${GH_REPO}@${ref}`);
|
||||||
|
const remote = await fetchRemoteState(ref);
|
||||||
|
const status = updateStatus(local, remote);
|
||||||
|
|
||||||
|
console.log(`Current version: ${status.currentVersion}${local.commit ? ` (${short(local.commit)})` : ""}`);
|
||||||
|
console.log(
|
||||||
|
`Latest version: ${status.latestVersion ?? "unknown"}${status.latestCommit ? ` (${short(status.latestCommit)})` : ""}`
|
||||||
|
);
|
||||||
|
|
||||||
|
if (status.upToDate) {
|
||||||
|
console.log(`LogicSRC CLI is already up to date — ${status.reason}.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Update available — ${status.reason}.`);
|
||||||
|
if (options.check) {
|
||||||
|
console.log(`Run 'logicsrc update' (or: curl -fsSL ${INSTALL_URL} | sh -s -- update) to install it.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Reinstalling from ${INSTALL_URL}…`);
|
||||||
|
const result = spawnSync("sh", ["-c", `curl -fsSL ${INSTALL_URL} | sh -s -- update`], { stdio: "inherit" });
|
||||||
|
if (result.status !== 0) {
|
||||||
|
console.error(`Update failed (exit ${result.status ?? "signal"}). Re-run by hand: curl -fsSL ${INSTALL_URL} | sh -s -- update`);
|
||||||
|
process.exitCode = 1;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.log(`Updated. Install root: ${installHome()} — config preserved at ~/.logicsrc`);
|
||||||
|
});
|
||||||
|
|
||||||
program.command("remove").alias("uninstall").option("--purge", "Remove config and auth tokens").description("Remove local LogicSRC CLI.").action((options) => {
|
program.command("remove").alias("uninstall").option("--purge", "Remove config and auth tokens").description("Remove local LogicSRC CLI.").action((options) => {
|
||||||
console.log("Removed LogicSRC CLI.");
|
console.log("Removed LogicSRC CLI.");
|
||||||
|
|
|
||||||
95
packages/cli/src/update.test.ts
Normal file
95
packages/cli/src/update.test.ts
Normal file
|
|
@ -0,0 +1,95 @@
|
||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
|
import { compareVersions, parseManifest, trackedRef, updateStatus } from "./update.js";
|
||||||
|
|
||||||
|
describe("compareVersions", () => {
|
||||||
|
it("orders releases numerically, not lexically", () => {
|
||||||
|
expect(compareVersions("0.2.0", "0.1.0")).toBe(1);
|
||||||
|
expect(compareVersions("0.1.0", "0.2.0")).toBe(-1);
|
||||||
|
expect(compareVersions("0.1.0", "0.1.0")).toBe(0);
|
||||||
|
// "10" > "9" numerically but sorts lower as a string.
|
||||||
|
expect(compareVersions("0.10.0", "0.9.0")).toBe(1);
|
||||||
|
expect(compareVersions("1.0.0", "0.99.99")).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("tolerates v-prefixes, prereleases, and short versions", () => {
|
||||||
|
expect(compareVersions("v1.2.3", "1.2.3")).toBe(0);
|
||||||
|
expect(compareVersions("1.2.0-beta.1", "1.2.0")).toBe(0);
|
||||||
|
expect(compareVersions("1.2", "1.2.0")).toBe(0);
|
||||||
|
expect(compareVersions("garbage", "0.0.0")).toBe(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("parseManifest", () => {
|
||||||
|
it("reads a manifest written by install.sh", () => {
|
||||||
|
const m = parseManifest(
|
||||||
|
JSON.stringify({ ref: "master", commit: "abc1234def", version: "0.1.0", installed_at: "2026-07-28T00:00:00Z" })
|
||||||
|
);
|
||||||
|
expect(m).toEqual({ ref: "master", commit: "abc1234def", version: "0.1.0", installed_at: "2026-07-28T00:00:00Z" });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("defaults the ref and nulls empty or missing fields", () => {
|
||||||
|
// install.sh writes empty strings when the sha lookup or version read fails.
|
||||||
|
expect(parseManifest(JSON.stringify({ commit: "", version: "" }))).toEqual({
|
||||||
|
ref: "master",
|
||||||
|
commit: null,
|
||||||
|
version: null,
|
||||||
|
installed_at: null
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns null for junk rather than throwing", () => {
|
||||||
|
expect(parseManifest("not json")).toBeNull();
|
||||||
|
expect(parseManifest("[]")).toBeNull();
|
||||||
|
expect(parseManifest("null")).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("trackedRef", () => {
|
||||||
|
it("prefers the environment, then the manifest, then master", () => {
|
||||||
|
const manifest = { ref: "next", commit: null, version: null, installed_at: null };
|
||||||
|
expect(trackedRef(manifest, { LOGICSRC_REF: "experiment" })).toBe("experiment");
|
||||||
|
expect(trackedRef(manifest, {})).toBe("next");
|
||||||
|
expect(trackedRef(null, {})).toBe("master");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("updateStatus", () => {
|
||||||
|
const local = { version: "0.1.0", commit: "aaaaaaaaaaaa" };
|
||||||
|
|
||||||
|
it("reports up to date only when the commit actually matches", () => {
|
||||||
|
const s = updateStatus(local, { version: "0.1.0", commit: "aaaaaaaaaaaa" });
|
||||||
|
expect(s.upToDate).toBe(true);
|
||||||
|
expect(s.reason).toContain("current commit");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("matches a short sha against a full one", () => {
|
||||||
|
expect(updateStatus({ version: "0.1.0", commit: "aaaaaaa" }, { version: "0.1.0", commit: "aaaaaaaaaaaa" }).upToDate).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("detects a moved branch even when the version is unchanged", () => {
|
||||||
|
// The bug this replaces: version-only comparison called this "up to date"
|
||||||
|
// forever, because the installer ships a branch tarball, not a release.
|
||||||
|
const s = updateStatus(local, { version: "0.1.0", commit: "bbbbbbbbbbbb" });
|
||||||
|
expect(s.upToDate).toBe(false);
|
||||||
|
expect(s.reason).toContain("moved on");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("detects a newer published version", () => {
|
||||||
|
const s = updateStatus(local, { version: "0.2.0", commit: "aaaaaaaaaaaa" });
|
||||||
|
expect(s.upToDate).toBe(false);
|
||||||
|
expect(s.reason).toContain("0.1.0 → 0.2.0");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("never claims to be current when the local commit is unknown", () => {
|
||||||
|
const s = updateStatus({ version: "0.1.0", commit: null }, { version: "0.1.0", commit: "bbbbbbbbbbbb" });
|
||||||
|
expect(s.upToDate).toBe(false);
|
||||||
|
expect(s.reason).toContain("predates update tracking");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not invent an update when GitHub is unreachable", () => {
|
||||||
|
const s = updateStatus(local, { version: null, commit: null });
|
||||||
|
expect(s.upToDate).toBe(true);
|
||||||
|
expect(s.reason).toContain("could not reach GitHub");
|
||||||
|
});
|
||||||
|
});
|
||||||
165
packages/cli/src/update.ts
Normal file
165
packages/cli/src/update.ts
Normal file
|
|
@ -0,0 +1,165 @@
|
||||||
|
import { readFileSync } from "node:fs";
|
||||||
|
import { homedir } from "node:os";
|
||||||
|
import { dirname, join } from "node:path";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
|
||||||
|
export const GH_REPO = "profullstack/logicsrc";
|
||||||
|
export const INSTALL_URL = "https://logicsrc.com/install.sh";
|
||||||
|
|
||||||
|
/** Written by install.sh so the CLI can tell which commit it was built from. */
|
||||||
|
export type InstallManifest = {
|
||||||
|
ref: string;
|
||||||
|
commit: string | null;
|
||||||
|
version: string | null;
|
||||||
|
installed_at: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type RemoteState = { version: string | null; commit: string | null };
|
||||||
|
|
||||||
|
export type UpdateStatus = {
|
||||||
|
upToDate: boolean;
|
||||||
|
/** Why we reached that verdict — shown to the user so it's never a bare claim. */
|
||||||
|
reason: string;
|
||||||
|
currentVersion: string;
|
||||||
|
latestVersion: string | null;
|
||||||
|
currentCommit: string | null;
|
||||||
|
latestCommit: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Install root the installer uses (not the config dir, which is ~/.logicsrc). */
|
||||||
|
export function installHome(env: NodeJS.ProcessEnv = process.env): string {
|
||||||
|
return env.LOGICSRC_HOME || join(env.HOME || homedir(), ".logicsrc-cli");
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Git ref this install tracks; install.sh defaults to master. */
|
||||||
|
export function trackedRef(manifest: InstallManifest | null, env: NodeJS.ProcessEnv = process.env): string {
|
||||||
|
return env.LOGICSRC_REF || manifest?.ref || "master";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The version of the CLI actually running, read from its own package.json
|
||||||
|
* rather than hardcoded — a literal here goes stale the moment anyone bumps
|
||||||
|
* the package and lies to every user who runs `logicsrc update`.
|
||||||
|
*/
|
||||||
|
export function localVersion(moduleUrl: string = import.meta.url): string {
|
||||||
|
// dist/update.js and src/update.ts are both one level under the package root.
|
||||||
|
const pkgPath = join(dirname(dirname(fileURLToPath(moduleUrl))), "package.json");
|
||||||
|
try {
|
||||||
|
const pkg = JSON.parse(readFileSync(pkgPath, "utf8")) as { version?: unknown };
|
||||||
|
return typeof pkg.version === "string" ? pkg.version : "unknown";
|
||||||
|
} catch {
|
||||||
|
return "unknown";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Parses $LOGICSRC_HOME/install.json; malformed or absent manifests are just "unknown". */
|
||||||
|
export function parseManifest(raw: string): InstallManifest | null {
|
||||||
|
let parsed: unknown;
|
||||||
|
try { parsed = JSON.parse(raw); } catch { return null; }
|
||||||
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return null;
|
||||||
|
const m = parsed as Record<string, unknown>;
|
||||||
|
return {
|
||||||
|
ref: typeof m.ref === "string" && m.ref ? m.ref : "master",
|
||||||
|
commit: typeof m.commit === "string" && m.commit ? m.commit : null,
|
||||||
|
version: typeof m.version === "string" && m.version ? m.version : null,
|
||||||
|
installed_at: typeof m.installed_at === "string" && m.installed_at ? m.installed_at : null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function readManifest(home: string = installHome()): InstallManifest | null {
|
||||||
|
try {
|
||||||
|
return parseManifest(readFileSync(join(home, "install.json"), "utf8"));
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Semver-ish compare. Returns -1 if a < b, 0 if equal, 1 if a > b. */
|
||||||
|
export function compareVersions(a: string, b: string): number {
|
||||||
|
const parts = (v: string) =>
|
||||||
|
v.replace(/^v/, "").split("-")[0]!.split(".").map((n) => Number.parseInt(n, 10) || 0);
|
||||||
|
const [x, y] = [parts(a), parts(b)];
|
||||||
|
for (let i = 0; i < Math.max(x.length, y.length); i++) {
|
||||||
|
const d = (x[i] ?? 0) - (y[i] ?? 0);
|
||||||
|
if (d !== 0) return d > 0 ? 1 : -1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decides whether an update is available.
|
||||||
|
*
|
||||||
|
* The installer ships a tarball of a branch, not a tagged release, so the
|
||||||
|
* version alone can't answer this: master moves constantly while
|
||||||
|
* packages/cli/package.json sits on the same number for months. The commit is
|
||||||
|
* the real signal, and the version is only a fallback for installs predating
|
||||||
|
* the manifest.
|
||||||
|
*/
|
||||||
|
export function updateStatus(local: { version: string; commit: string | null }, remote: RemoteState): UpdateStatus {
|
||||||
|
const base = {
|
||||||
|
currentVersion: local.version,
|
||||||
|
latestVersion: remote.version,
|
||||||
|
currentCommit: local.commit,
|
||||||
|
latestCommit: remote.commit
|
||||||
|
};
|
||||||
|
|
||||||
|
if (remote.version && compareVersions(remote.version, local.version) > 0) {
|
||||||
|
return { ...base, upToDate: false, reason: `a newer release is published (${local.version} → ${remote.version})` };
|
||||||
|
}
|
||||||
|
if (local.commit && remote.commit) {
|
||||||
|
const same = local.commit.startsWith(remote.commit) || remote.commit.startsWith(local.commit);
|
||||||
|
return same
|
||||||
|
? { ...base, upToDate: true, reason: "installed from the current commit" }
|
||||||
|
: { ...base, upToDate: false, reason: `the tracked branch has moved on (${short(local.commit)} → ${short(remote.commit)})` };
|
||||||
|
}
|
||||||
|
if (!remote.version && !remote.commit) {
|
||||||
|
return { ...base, upToDate: true, reason: "could not reach GitHub — assuming no update rather than guessing" };
|
||||||
|
}
|
||||||
|
if (!local.commit) {
|
||||||
|
return {
|
||||||
|
...base,
|
||||||
|
upToDate: false,
|
||||||
|
reason: "this install predates update tracking, so its commit is unknown — reinstalling is the only way to be sure"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return { ...base, upToDate: true, reason: "already on the latest published version" };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function short(commit: string): string {
|
||||||
|
return commit.slice(0, 7);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Latest commit sha for a ref. The .sha media type returns it as bare text. */
|
||||||
|
export async function fetchRemoteCommit(ref: string, repo = GH_REPO): Promise<string | null> {
|
||||||
|
try {
|
||||||
|
const res = await fetch(`https://api.github.com/repos/${repo}/commits/${encodeURIComponent(ref)}`, {
|
||||||
|
headers: { accept: "application/vnd.github.sha", "user-agent": "logicsrc-cli" },
|
||||||
|
signal: AbortSignal.timeout(10_000)
|
||||||
|
});
|
||||||
|
if (!res.ok) return null;
|
||||||
|
const sha = (await res.text()).trim();
|
||||||
|
return /^[0-9a-f]{7,40}$/i.test(sha) ? sha : null;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** CLI version declared on the tracked ref. */
|
||||||
|
export async function fetchRemoteVersion(ref: string, repo = GH_REPO): Promise<string | null> {
|
||||||
|
try {
|
||||||
|
const res = await fetch(
|
||||||
|
`https://raw.githubusercontent.com/${repo}/${encodeURIComponent(ref)}/packages/cli/package.json`,
|
||||||
|
{ headers: { "user-agent": "logicsrc-cli" }, signal: AbortSignal.timeout(10_000) }
|
||||||
|
);
|
||||||
|
if (!res.ok) return null;
|
||||||
|
const pkg = JSON.parse(await res.text()) as { version?: unknown };
|
||||||
|
return typeof pkg.version === "string" ? pkg.version : null;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchRemoteState(ref: string, repo = GH_REPO): Promise<RemoteState> {
|
||||||
|
const [version, commit] = await Promise.all([fetchRemoteVersion(ref, repo), fetchRemoteCommit(ref, repo)]);
|
||||||
|
return { version, commit };
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue