#!/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. } }