mirror of
https://github.com/garrytan/gstack.git
synced 2026-09-21 04:10:47 +02:00
94 lines
3.2 KiB
JavaScript
Executable File
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.
|
|
}
|
|
}
|