From 4699e35bc2f9653c4e0f51468648ddba260a4cb8 Mon Sep 17 00:00:00 2001 From: j4ckxyz Date: Sun, 19 Apr 2026 22:46:58 +0100 Subject: [PATCH] Add in-app update command Introduce to rerun the installer from the detected install root and preserve the current origin remote so users can refresh to latest from their existing GitHub or Tangled source. --- README.md | 1 + src/cli.ts | 10 +++++++ src/update.ts | 72 +++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 83 insertions(+) create mode 100644 src/update.ts diff --git a/README.md b/README.md index 8797081..989e591 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,7 @@ grain grain help grain login --handle j4ck.xyz grain whoami +grain update grain logout ``` diff --git a/src/cli.ts b/src/cli.ts index a03bbeb..fe34c35 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -7,6 +7,7 @@ import { normalizeLocalPathToken, parsePositionalMediaToken } from "./media-inpu import { getAuthorizedAgent, loginWithOAuth, logoutOAuth } from "./oauth"; import { promptForAltTextFallback, runUploadWizard } from "./tui"; import type { AltAiConfig, MediaInput } from "./types"; +import { runSelfUpdate } from "./update"; const LENS_FRAMES = ["[lens: . ]", "[lens: .. ]", "[lens: ...]", "[lens: clear]"]; @@ -42,6 +43,7 @@ Commands: grain help grain login [--handle ] grain whoami [--debug] + grain update grain logout grain upload-gallery --title [--description <text>] [--location-name <name>] [--location-value <h3>] [--country <code>] [--locality <name>] [--region <name>] [--street <value>] [--postal-code <code>] [--place-name <name>] [--cw <label1,label2>] [--alt <text> ...] [--image <path> ...] [--image-url <url> ...] [--exif include|exclude] [--alt-ai-endpoint <url> --alt-ai-api-key <key> --alt-ai-model <model>] <image-path-or-url ...> @@ -146,6 +148,10 @@ async function cmdLogout(): Promise<void> { console.log("Logged out and removed saved OAuth session."); } +async function cmdUpdate(): Promise<void> { + await runSelfUpdate(); +} + function parseAltAi(parsed: ReturnType<typeof parseArgs>): AltAiConfig | undefined { const endpoint = getOption(parsed.options, "alt-ai-endpoint") ?? process.env.GRAIN_ALT_AI_ENDPOINT; const apiKey = getOption(parsed.options, "alt-ai-api-key") ?? process.env.GRAIN_ALT_AI_API_KEY; @@ -338,6 +344,10 @@ async function run(): Promise<void> { await cmdLogout(); break; } + case "update": { + await cmdUpdate(); + break; + } case "upload-gallery": { await cmdUploadGallery(rest); break; diff --git a/src/update.ts b/src/update.ts new file mode 100644 index 0000000..d60b27b --- /dev/null +++ b/src/update.ts @@ -0,0 +1,72 @@ +import { dirname, join, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; +import { GrainError } from "./errors"; + +function currentProjectRoot(): string { + return resolve(dirname(fileURLToPath(import.meta.url)), ".."); +} + +async function fileExists(path: string): Promise<boolean> { + return Bun.file(path).exists(); +} + +async function detectInstallRoot(): Promise<string> { + const home = process.env.HOME; + const preferred = home ? join(home, ".grain-cli") : undefined; + + if (preferred && (await fileExists(join(preferred, "scripts", "install.sh")))) { + return preferred; + } + + const root = currentProjectRoot(); + if (await fileExists(join(root, "scripts", "install.sh"))) { + return root; + } + + throw new GrainError( + "update_script_missing", + "Could not find the installer script for updates.", + "Reinstall with the install script, then run `grain update` again.", + ); +} + +function readOriginUrl(repoRoot: string): string | undefined { + const result = Bun.spawnSync(["git", "-C", repoRoot, "remote", "get-url", "origin"], { + stdout: "pipe", + stderr: "ignore", + }); + + if (!result.success || !result.stdout) { + return undefined; + } + + const value = result.stdout.toString().trim(); + return value || undefined; +} + +export async function runSelfUpdate(): Promise<void> { + const installRoot = await detectInstallRoot(); + const scriptPath = join(installRoot, "scripts", "install.sh"); + + const env = { ...process.env }; + const originUrl = readOriginUrl(installRoot); + if (originUrl) { + env.GRAIN_REPO_URL = originUrl; + } + + console.log("Updating grain to the latest release..."); + const proc = Bun.spawn(["bash", scriptPath], { + cwd: installRoot, + env, + stdin: "ignore", + stdout: "inherit", + stderr: "inherit", + }); + + const exitCode = await proc.exited; + if (exitCode !== 0) { + throw new GrainError("update_failed", "Update failed.", "Re-run `grain update` or reinstall using the install script."); + } + + console.log("Update complete."); +} -- knotmirror 0.0.1