Files
gstack/bin/gstack-slug
T

94 lines
3.2 KiB
JavaScript
Executable File

#!/usr/bin/env node
/**
* gstack-slug — emit human and local project identities for shell callers.
*
* Usage: eval "$(gstack-slug)"
*
* SLUG remains the sanitized, human-facing repository slug used by remote
* namespaces. PROJECT_ID is the canonical local-state key from
* runtime/identity.js; unlike SLUG, it separates linked Git worktrees.
* Every emitted value is restricted to [a-zA-Z0-9._-] so the output remains
* safe to consume with eval/source.
*/
import fs from "node:fs/promises";
import path from "node:path";
import { execFile as execFileCallback } from "node:child_process";
import { promisify } from "node:util";
import { discoverProjectIdentity } from "../runtime/identity.js";
import { resolveGstackHome } from "../runtime/paths.js";
const execFile = promisify(execFileCallback);
const cwd = await canonicalPath(process.cwd());
const identity = await discoverProjectIdentity(cwd);
const home = resolveGstackHome({ cwd });
const cacheDir = path.join(home, "slug-cache");
// The canonical key is a safe, worktree-stable ID. Read the 1.x path-derived
// key once as a compatibility fallback (it was not valid on native Windows).
const cacheFile = path.join(cacheDir, identity.worktreeId);
const legacyCacheFile = path.join(cacheDir, cwd.replace(/[\\/]/g, "_"));
let slug = sanitize(await fs.readFile(cacheFile, "utf8").catch(() => ""));
if (!slug) slug = sanitize(await fs.readFile(legacyCacheFile, "utf8").catch(() => ""));
if (!slug) {
const remote = await git(["remote", "get-url", "origin"], cwd).catch(() => "");
slug = sanitize(slugFromRemote(remote));
}
if (!slug) slug = sanitize(path.basename(cwd)) || "unknown";
await writeCache(cacheDir, cacheFile, slug);
const rawBranch = await git(["rev-parse", "--abbrev-ref", "HEAD"], cwd).catch(() => "");
const branch = sanitize(rawBranch === "HEAD" ? "" : rawBranch) || "unknown";
for (const [name, value] of [
["SLUG", slug],
["BRANCH", branch],
["PROJECT_ID", identity.projectId],
["REPO_ID", identity.repoId],
["WORKTREE_ID", identity.worktreeId],
]) {
process.stdout.write(`${name}=${sanitize(value) || "unknown"}\n`);
}
function sanitize(value) {
return String(value ?? "").replace(/[^a-zA-Z0-9._-]/g, "");
}
function slugFromRemote(remote) {
const normalized = String(remote ?? "").trim().replace(/\/+$/, "").replace(/\.git$/, "");
const match = normalized.match(/(?:^|[:/])([^/:]+\/[^/]+)$/);
return match ? match[1].replace("/", "-") : "";
}
async function git(args, directory) {
const { stdout } = await execFile("git", args, {
cwd: directory,
encoding: "utf8",
timeout: 5_000,
maxBuffer: 1024 * 1024,
windowsHide: true,
});
return stdout.replace(/[\r\n]+$/, "");
}
async function canonicalPath(value) {
const absolute = path.resolve(value);
return fs.realpath(absolute).catch((error) => {
if (error?.code === "ENOENT") return absolute;
throw error;
});
}
async function writeCache(directory, file, value) {
try {
await fs.mkdir(directory, { recursive: true, mode: 0o700 });
const temporary = path.join(directory, `.slug-${process.pid}-${Date.now()}`);
await fs.writeFile(temporary, value, { mode: 0o600 });
await fs.rename(temporary, file);
} catch {
// Display-slug caching is a best-effort compatibility optimization.
}
}