gsd-pi 2.38.0-dev.96dc7fb → 2.38.0-dev.e40f839

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/dist/app-paths.js +1 -1
  2. package/dist/extension-registry.js +2 -2
  3. package/dist/remote-questions-config.js +2 -2
  4. package/dist/resources/extensions/env-utils.js +29 -0
  5. package/dist/resources/extensions/get-secrets-from-user.js +5 -24
  6. package/dist/resources/extensions/gsd/auto-worktree-sync.js +2 -1
  7. package/dist/resources/extensions/gsd/commands-extensions.js +3 -2
  8. package/dist/resources/extensions/gsd/commands.js +2 -1
  9. package/dist/resources/extensions/gsd/detection.js +1 -2
  10. package/dist/resources/extensions/gsd/export.js +1 -1
  11. package/dist/resources/extensions/gsd/files.js +2 -2
  12. package/dist/resources/extensions/gsd/forensics.js +1 -1
  13. package/dist/resources/extensions/gsd/index.js +2 -1
  14. package/dist/resources/extensions/gsd/migrate/parsers.js +1 -1
  15. package/dist/resources/extensions/gsd/preferences-validation.js +1 -1
  16. package/dist/resources/extensions/gsd/preferences.js +4 -3
  17. package/dist/resources/extensions/gsd/repo-identity.js +2 -1
  18. package/dist/resources/extensions/gsd/resource-version.js +2 -1
  19. package/dist/resources/extensions/gsd/state.js +1 -1
  20. package/dist/resources/extensions/gsd/visualizer-data.js +1 -1
  21. package/dist/resources/extensions/remote-questions/status.js +2 -1
  22. package/dist/resources/extensions/remote-questions/store.js +2 -1
  23. package/dist/resources/extensions/search-the-web/provider.js +2 -1
  24. package/dist/resources/extensions/subagent/isolation.js +2 -1
  25. package/dist/resources/extensions/ttsr/rule-loader.js +2 -1
  26. package/package.json +1 -1
  27. package/src/resources/extensions/env-utils.ts +31 -0
  28. package/src/resources/extensions/get-secrets-from-user.ts +5 -24
  29. package/src/resources/extensions/gsd/auto-worktree-sync.ts +3 -1
  30. package/src/resources/extensions/gsd/commands-extensions.ts +4 -2
  31. package/src/resources/extensions/gsd/commands.ts +3 -1
  32. package/src/resources/extensions/gsd/detection.ts +2 -2
  33. package/src/resources/extensions/gsd/export.ts +1 -1
  34. package/src/resources/extensions/gsd/files.ts +2 -2
  35. package/src/resources/extensions/gsd/forensics.ts +1 -1
  36. package/src/resources/extensions/gsd/index.ts +3 -1
  37. package/src/resources/extensions/gsd/migrate/parsers.ts +1 -1
  38. package/src/resources/extensions/gsd/preferences-validation.ts +1 -1
  39. package/src/resources/extensions/gsd/preferences.ts +5 -3
  40. package/src/resources/extensions/gsd/repo-identity.ts +3 -1
  41. package/src/resources/extensions/gsd/resource-version.ts +3 -1
  42. package/src/resources/extensions/gsd/state.ts +1 -1
  43. package/src/resources/extensions/gsd/visualizer-data.ts +1 -1
  44. package/src/resources/extensions/remote-questions/status.ts +3 -1
  45. package/src/resources/extensions/remote-questions/store.ts +3 -1
  46. package/src/resources/extensions/search-the-web/provider.ts +2 -1
  47. package/src/resources/extensions/subagent/isolation.ts +3 -1
  48. package/src/resources/extensions/ttsr/rule-loader.ts +3 -1
package/dist/app-paths.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { homedir } from 'os';
2
2
  import { join } from 'path';
3
- export const appRoot = join(homedir(), '.gsd');
3
+ export const appRoot = process.env.GSD_HOME || join(homedir(), '.gsd');
4
4
  export const agentDir = join(appRoot, 'agent');
5
5
  export const sessionsDir = join(appRoot, 'sessions');
6
6
  export const authFilePath = join(agentDir, 'auth.json');
@@ -6,7 +6,7 @@
6
6
  * The only way an extension stops loading is an explicit `gsd extensions disable <id>`.
7
7
  */
8
8
  import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, writeFileSync } from "node:fs";
9
- import { homedir } from "node:os";
9
+ import { appRoot } from "./app-paths.js";
10
10
  import { dirname, join } from "node:path";
11
11
  // ─── Validation ─────────────────────────────────────────────────────────────
12
12
  function isRegistry(data) {
@@ -26,7 +26,7 @@ function isManifest(data) {
26
26
  }
27
27
  // ─── Registry Path ──────────────────────────────────────────────────────────
28
28
  export function getRegistryPath() {
29
- return join(homedir(), ".gsd", "extensions", "registry.json");
29
+ return join(appRoot, "extensions", "registry.json");
30
30
  }
31
31
  // ─── Registry I/O ───────────────────────────────────────────────────────────
32
32
  function defaultRegistry() {
@@ -9,12 +9,12 @@
9
9
  */
10
10
  import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
11
11
  import { dirname, join } from "node:path";
12
- import { homedir } from "node:os";
12
+ import { appRoot } from "./app-paths.js";
13
13
  // Inlined from preferences.ts to avoid crossing the compiled/uncompiled
14
14
  // boundary — this file is compiled by tsc, but preferences.ts is loaded
15
15
  // via jiti at runtime. Importing it as .js fails because no .js exists
16
16
  // in dist/. See #592, #1110.
17
- const GLOBAL_PREFERENCES_PATH = join(homedir(), ".gsd", "preferences.md");
17
+ const GLOBAL_PREFERENCES_PATH = join(appRoot, "preferences.md");
18
18
  export function saveRemoteQuestionsConfig(channel, channelId) {
19
19
  const prefsPath = GLOBAL_PREFERENCES_PATH;
20
20
  const block = [
@@ -0,0 +1,29 @@
1
+ // GSD Extension — Environment variable utilities
2
+ // Copyright (c) 2026 Jeremy McSpadden <jeremy@fluxlabs.net>
3
+ //
4
+ // Pure utility for checking existing env keys in .env files and process.env.
5
+ // Extracted from get-secrets-from-user.ts to avoid pulling in @gsd/pi-tui
6
+ // when only env-checking is needed (e.g. from files.ts during report generation).
7
+ import { readFile } from "node:fs/promises";
8
+ /**
9
+ * Check which keys already exist in a .env file or process.env.
10
+ * Returns the subset of `keys` that are already set.
11
+ */
12
+ export async function checkExistingEnvKeys(keys, envFilePath) {
13
+ let fileContent = "";
14
+ try {
15
+ fileContent = await readFile(envFilePath, "utf8");
16
+ }
17
+ catch {
18
+ // ENOENT or other read error — proceed with empty content
19
+ }
20
+ const existing = [];
21
+ for (const key of keys) {
22
+ const escaped = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
23
+ const regex = new RegExp(`^${escaped}\\s*=`, "m");
24
+ if (regex.test(fileContent) || key in process.env) {
25
+ existing.push(key);
26
+ }
27
+ }
28
+ return existing;
29
+ }
@@ -46,30 +46,11 @@ async function writeEnvKey(filePath, key, value) {
46
46
  await writeFile(filePath, content, "utf8");
47
47
  }
48
48
  // ─── Exported utilities ───────────────────────────────────────────────────────
49
- /**
50
- * Check which keys already exist in the .env file or process.env.
51
- * Returns the subset of `keys` that are already set.
52
- * Handles ENOENT gracefully (still checks process.env).
53
- * Empty-string values count as existing.
54
- */
55
- export async function checkExistingEnvKeys(keys, envFilePath) {
56
- let fileContent = "";
57
- try {
58
- fileContent = await readFile(envFilePath, "utf8");
59
- }
60
- catch {
61
- // ENOENT or other read error — proceed with empty content
62
- }
63
- const existing = [];
64
- for (const key of keys) {
65
- const escaped = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
66
- const regex = new RegExp(`^${escaped}\\s*=`, "m");
67
- if (regex.test(fileContent) || key in process.env) {
68
- existing.push(key);
69
- }
70
- }
71
- return existing;
72
- }
49
+ // Re-export from env-utils.ts so existing consumers still work.
50
+ // The implementation lives in env-utils.ts to avoid pulling @gsd/pi-tui
51
+ // into modules that only need env-checking (e.g. files.ts during reports).
52
+ import { checkExistingEnvKeys } from "./env-utils.js";
53
+ export { checkExistingEnvKeys };
73
54
  /**
74
55
  * Detect the write destination based on project files in basePath.
75
56
  * Priority: vercel.json → convex/ dir → fallback "dotenv".
@@ -13,6 +13,7 @@ import { existsSync, readFileSync, unlinkSync, readdirSync, } from "node:fs";
13
13
  import { join, sep as pathSep } from "node:path";
14
14
  import { homedir } from "node:os";
15
15
  import { safeCopy, safeCopyRecursive } from "./safe-fs.js";
16
+ const gsdHome = process.env.GSD_HOME || join(homedir(), ".gsd");
16
17
  // ─── Project Root → Worktree Sync ─────────────────────────────────────────
17
18
  /**
18
19
  * Sync milestone artifacts from project root INTO worktree before deriveState.
@@ -75,7 +76,7 @@ export function syncStateToProjectRoot(worktreePath, projectRoot, milestoneId) {
75
76
  * doesn't falsely trigger staleness (#804).
76
77
  */
77
78
  export function readResourceVersion() {
78
- const agentDir = process.env.GSD_CODING_AGENT_DIR || join(homedir(), ".gsd", "agent");
79
+ const agentDir = process.env.GSD_CODING_AGENT_DIR || join(gsdHome, "agent");
79
80
  const manifestPath = join(agentDir, "managed-resources.json");
80
81
  try {
81
82
  const manifest = JSON.parse(readFileSync(manifestPath, "utf-8"));
@@ -8,12 +8,13 @@
8
8
  import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, writeFileSync } from "node:fs";
9
9
  import { dirname, join } from "node:path";
10
10
  import { homedir } from "node:os";
11
+ const gsdHome = process.env.GSD_HOME || join(homedir(), ".gsd");
11
12
  // ─── Registry I/O ───────────────────────────────────────────────────────────
12
13
  function getRegistryPath() {
13
- return join(homedir(), ".gsd", "extensions", "registry.json");
14
+ return join(gsdHome, "extensions", "registry.json");
14
15
  }
15
16
  function getAgentExtensionsDir() {
16
- return join(homedir(), ".gsd", "agent", "extensions");
17
+ return join(gsdHome, "agent", "extensions");
17
18
  }
18
19
  function loadRegistry() {
19
20
  const filePath = getRegistryPath();
@@ -7,6 +7,7 @@ import { existsSync, readFileSync, readdirSync, unlinkSync } from "node:fs";
7
7
  import { homedir } from "node:os";
8
8
  import { join } from "node:path";
9
9
  import { gsdRoot } from "./paths.js";
10
+ const gsdHome = process.env.GSD_HOME || join(homedir(), ".gsd");
10
11
  import { enableDebug } from "./debug-logger.js";
11
12
  import { deriveState } from "./state.js";
12
13
  import { GSDDashboardOverlay } from "./dashboard-overlay.js";
@@ -437,7 +438,7 @@ export function registerGSDCommand(pi) {
437
438
  if (parts.length === 3 && ["enable", "disable", "info"].includes(parts[1])) {
438
439
  const idPrefix = parts[2] ?? "";
439
440
  try {
440
- const extDir = join(homedir(), ".gsd", "agent", "extensions");
441
+ const extDir = join(gsdHome, "agent", "extensions");
441
442
  const ids = [];
442
443
  for (const entry of readdirSync(extDir, { withFileTypes: true })) {
443
444
  if (!entry.isDirectory())
@@ -9,6 +9,7 @@ import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
9
9
  import { join } from "node:path";
10
10
  import { homedir } from "node:os";
11
11
  import { gsdRoot } from "./paths.js";
12
+ const gsdHome = process.env.GSD_HOME || join(homedir(), ".gsd");
12
13
  // ─── Project File Markers ───────────────────────────────────────────────────────
13
14
  const PROJECT_FILES = [
14
15
  "package.json",
@@ -309,7 +310,6 @@ function detectVerificationCommands(basePath, detectedFiles, packageManager) {
309
310
  * Check if global GSD setup exists (has ~/.gsd/ with preferences).
310
311
  */
311
312
  export function hasGlobalSetup() {
312
- const gsdHome = join(homedir(), ".gsd");
313
313
  return (existsSync(join(gsdHome, "preferences.md")) ||
314
314
  existsSync(join(gsdHome, "PREFERENCES.md")));
315
315
  }
@@ -318,7 +318,6 @@ export function hasGlobalSetup() {
318
318
  * Returns true if ~/.gsd/ doesn't exist or has no preferences or auth.
319
319
  */
320
320
  export function isFirstEverLaunch() {
321
- const gsdHome = join(homedir(), ".gsd");
322
321
  if (!existsSync(gsdHome))
323
322
  return true;
324
323
  // If we have preferences, not first launch
@@ -5,7 +5,7 @@ import { join, basename } from "node:path";
5
5
  import { exec } from "node:child_process";
6
6
  import { getLedger, getProjectTotals, aggregateByPhase, aggregateBySlice, aggregateByModel, formatCost, formatTokenCount, loadLedgerFromDisk, } from "./metrics.js";
7
7
  import { gsdRoot } from "./paths.js";
8
- import { formatDuration, fileLink } from "../shared/mod.js";
8
+ import { formatDuration, fileLink } from "../shared/format-utils.js";
9
9
  import { getErrorMessage } from "./error-utils.js";
10
10
  /**
11
11
  * Open a file in the user's default browser.
@@ -6,8 +6,8 @@ import { promises as fs } from 'node:fs';
6
6
  import { resolve } from 'node:path';
7
7
  import { atomicWriteAsync } from './atomic-write.js';
8
8
  import { resolveMilestoneFile, relMilestoneFile, resolveGsdRootFile } from './paths.js';
9
- import { findMilestoneIds } from './guided-flow.js';
10
- import { checkExistingEnvKeys } from '../get-secrets-from-user.js';
9
+ import { findMilestoneIds } from './milestone-ids.js';
10
+ import { checkExistingEnvKeys } from '../env-utils.js';
11
11
  import { parseRoadmapSlices } from './roadmap-slices.js';
12
12
  import { nativeParseRoadmap, nativeExtractSection, nativeParsePlanFile, nativeParseSummaryFile, NATIVE_UNAVAILABLE } from './native-parser-bridge.js';
13
13
  import { debugTime, debugCount } from './debug-logger.js';
@@ -21,7 +21,7 @@ import { deriveState } from "./state.js";
21
21
  import { isAutoActive } from "./auto.js";
22
22
  import { loadPrompt } from "./prompt-loader.js";
23
23
  import { gsdRoot } from "./paths.js";
24
- import { formatDuration } from "../shared/mod.js";
24
+ import { formatDuration } from "../shared/format-utils.js";
25
25
  import { getAutoWorktreePath } from "./auto-worktree.js";
26
26
  // ─── Entry Point ──────────────────────────────────────────────────────────────
27
27
  export async function handleForensics(args, ctx, pi) {
@@ -41,6 +41,7 @@ import { join } from "node:path";
41
41
  import { existsSync, readFileSync } from "node:fs";
42
42
  import { homedir } from "node:os";
43
43
  import { shortcutDesc } from "../shared/mod.js";
44
+ const gsdHome = process.env.GSD_HOME || join(homedir(), ".gsd");
44
45
  import { Text } from "@gsd/pi-tui";
45
46
  import { pauseAutoForProviderError, classifyProviderError } from "./provider-error-pause.js";
46
47
  import { toPosixPath } from "../shared/mod.js";
@@ -52,7 +53,7 @@ import { markCmuxPromptShown, shouldPromptToEnableCmux } from "../cmux/index.js"
52
53
  // Pi core natively supports AGENTS.md (with CLAUDE.md fallback) per directory.
53
54
  function warnDeprecatedAgentInstructions() {
54
55
  const paths = [
55
- join(homedir(), ".gsd", "agent-instructions.md"),
56
+ join(gsdHome, "agent-instructions.md"),
56
57
  join(process.cwd(), ".gsd", "agent-instructions.md"),
57
58
  ];
58
59
  for (const p of paths) {
@@ -2,7 +2,7 @@
2
2
  // Pure functions that take file content (string) and return typed data.
3
3
  // Zero Pi dependencies — uses only exported helpers from files.ts.
4
4
  import { splitFrontmatter, parseFrontmatterMap, extractBoldField } from '../files.js';
5
- import { normalizeStringArray } from '../../shared/mod.js';
5
+ import { normalizeStringArray } from '../../shared/format-utils.js';
6
6
  // Re-export PlanningProjectMeta — not in types.ts yet, use string for project field
7
7
  // Actually PlanningProjectMeta isn't in types.ts — project is stored as string | null.
8
8
  // We'll keep parseOldProject returning a simple shape.
@@ -6,7 +6,7 @@
6
6
  * together with any errors and warnings.
7
7
  */
8
8
  import { VALID_BRANCH_NAME } from "./git-service.js";
9
- import { normalizeStringArray } from "../shared/mod.js";
9
+ import { normalizeStringArray } from "../shared/format-utils.js";
10
10
  import { KNOWN_PREFERENCE_KEYS, KNOWN_UNIT_TYPES, SKILL_ACTIONS, } from "./preferences-types.js";
11
11
  const VALID_TOKEN_PROFILES = new Set(["budget", "balanced", "quality"]);
12
12
  export function validatePreferences(preferences) {
@@ -12,9 +12,10 @@
12
12
  import { existsSync, readFileSync } from "node:fs";
13
13
  import { homedir } from "node:os";
14
14
  import { join } from "node:path";
15
+ const gsdHome = process.env.GSD_HOME || join(homedir(), ".gsd");
15
16
  import { gsdRoot } from "./paths.js";
16
17
  import { parse as parseYaml } from "yaml";
17
- import { normalizeStringArray } from "../shared/mod.js";
18
+ import { normalizeStringArray } from "../shared/format-utils.js";
18
19
  import { resolveProfileDefaults as _resolveProfileDefaults } from "./preferences-models.js";
19
20
  import { MODE_DEFAULTS, } from "./preferences-types.js";
20
21
  import { validatePreferences } from "./preferences-validation.js";
@@ -26,14 +27,14 @@ export { resolveAllSkillReferences, resolveSkillDiscoveryMode, resolveSkillStale
26
27
  // ─── Re-exports: models ─────────────────────────────────────────────────────
27
28
  export { resolveModelForUnit, resolveModelWithFallbacksForUnit, getNextFallbackModel, isTransientNetworkError, validateModelId, updatePreferencesModels, resolveDynamicRoutingConfig, resolveAutoSupervisorConfig, resolveProfileDefaults, resolveEffectiveProfile, resolveInlineLevel, resolveCompressionStrategy, resolveContextSelection, resolveSearchProviderFromPreferences, } from "./preferences-models.js";
28
29
  // ─── Path Constants & Getters ───────────────────────────────────────────────
29
- const GLOBAL_PREFERENCES_PATH = join(homedir(), ".gsd", "preferences.md");
30
+ const GLOBAL_PREFERENCES_PATH = join(gsdHome, "preferences.md");
30
31
  const LEGACY_GLOBAL_PREFERENCES_PATH = join(homedir(), ".pi", "agent", "gsd-preferences.md");
31
32
  function projectPreferencesPath() {
32
33
  return join(gsdRoot(process.cwd()), "preferences.md");
33
34
  }
34
35
  // Bootstrap in gitignore.ts historically created PREFERENCES.md (uppercase) by mistake.
35
36
  // Check uppercase as a fallback so those files aren't silently ignored.
36
- const GLOBAL_PREFERENCES_PATH_UPPERCASE = join(homedir(), ".gsd", "PREFERENCES.md");
37
+ const GLOBAL_PREFERENCES_PATH_UPPERCASE = join(gsdHome, "PREFERENCES.md");
37
38
  function projectPreferencesPathUppercase() {
38
39
  return join(gsdRoot(process.cwd()), "PREFERENCES.md");
39
40
  }
@@ -10,6 +10,7 @@ import { execFileSync } from "node:child_process";
10
10
  import { existsSync, lstatSync, mkdirSync, readFileSync, realpathSync, rmSync, symlinkSync } from "node:fs";
11
11
  import { homedir } from "node:os";
12
12
  import { join, resolve } from "node:path";
13
+ const gsdHome = process.env.GSD_HOME || join(homedir(), ".gsd");
13
14
  // ─── Repo Identity ──────────────────────────────────────────────────────────
14
15
  /**
15
16
  * Get the git remote URL for "origin", or "" if no remote is configured.
@@ -105,7 +106,7 @@ export function repoIdentity(basePath) {
105
106
  * otherwise `~/.gsd/projects/<hash>`.
106
107
  */
107
108
  export function externalGsdRoot(basePath) {
108
- const base = process.env.GSD_STATE_DIR || join(homedir(), ".gsd");
109
+ const base = process.env.GSD_STATE_DIR || gsdHome;
109
110
  return join(base, "projects", repoIdentity(basePath));
110
111
  }
111
112
  // ─── Symlink Management ─────────────────────────────────────────────────────
@@ -9,6 +9,7 @@ import { loadJsonFileOrNull } from "./json-persistence.js";
9
9
  import { join } from "node:path";
10
10
  import { homedir } from "node:os";
11
11
  import { resolveProjectRoot } from "./worktree.js";
12
+ const gsdHome = process.env.GSD_HOME || join(homedir(), ".gsd");
12
13
  // ─── Resource Staleness ───────────────────────────────────────────────────
13
14
  /**
14
15
  * Read the resource version (semver) from the managed-resources manifest.
@@ -19,7 +20,7 @@ function isManifestWithVersion(data) {
19
20
  return data !== null && typeof data === "object" && "gsdVersion" in data && typeof data.gsdVersion === "string";
20
21
  }
21
22
  export function readResourceVersion() {
22
- const agentDir = process.env.GSD_CODING_AGENT_DIR || join(homedir(), ".gsd", "agent");
23
+ const agentDir = process.env.GSD_CODING_AGENT_DIR || join(gsdHome, "agent");
23
24
  const manifestPath = join(agentDir, "managed-resources.json");
24
25
  const manifest = loadJsonFileOrNull(manifestPath, isManifestWithVersion);
25
26
  return manifest?.gsdVersion ?? null;
@@ -3,7 +3,7 @@
3
3
  // Pure TypeScript, zero Pi dependencies.
4
4
  import { parseRoadmap, parsePlan, parseSummary, loadFile, parseRequirementCounts, parseContextDependsOn, } from './files.js';
5
5
  import { resolveMilestoneFile, resolveSlicePath, resolveSliceFile, resolveTaskFile, resolveTasksDir, resolveGsdRootFile, gsdRoot, } from './paths.js';
6
- import { findMilestoneIds } from './guided-flow.js';
6
+ import { findMilestoneIds } from './milestone-ids.js';
7
7
  import { nativeBatchParseGsdFiles } from './native-parser-bridge.js';
8
8
  import { join, resolve } from 'path';
9
9
  import { existsSync, readdirSync } from 'node:fs';
@@ -2,7 +2,7 @@
2
2
  import { existsSync, readFileSync, statSync } from 'node:fs';
3
3
  import { deriveState } from './state.js';
4
4
  import { parseRoadmap, parsePlan, parseSummary, loadFile } from './files.js';
5
- import { findMilestoneIds } from './guided-flow.js';
5
+ import { findMilestoneIds } from './milestone-ids.js';
6
6
  import { resolveMilestoneFile, resolveSliceFile, resolveGsdRootFile } from './paths.js';
7
7
  import { getLedger, getProjectTotals, aggregateByPhase, aggregateBySlice, aggregateByModel, aggregateByTier, formatTierSavings, loadLedgerFromDisk, } from './metrics.js';
8
8
  import { loadAllCaptures, countPendingCaptures } from './captures.js';
@@ -5,8 +5,9 @@ import { existsSync, readdirSync } from "node:fs";
5
5
  import { join } from "node:path";
6
6
  import { homedir } from "node:os";
7
7
  import { readPromptRecord } from "./store.js";
8
+ const gsdHome = process.env.GSD_HOME || join(homedir(), ".gsd");
8
9
  export function getLatestPromptSummary() {
9
- const runtimeDir = join(homedir(), ".gsd", "runtime", "remote-questions");
10
+ const runtimeDir = join(gsdHome, "runtime", "remote-questions");
10
11
  if (!existsSync(runtimeDir))
11
12
  return null;
12
13
  const files = readdirSync(runtimeDir).filter((f) => f.endsWith(".json"));
@@ -4,8 +4,9 @@
4
4
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
5
5
  import { join } from "node:path";
6
6
  import { homedir } from "node:os";
7
+ const gsdHome = process.env.GSD_HOME || join(homedir(), ".gsd");
7
8
  function runtimeDir() {
8
- return join(homedir(), ".gsd", "runtime", "remote-questions");
9
+ return join(gsdHome, "runtime", "remote-questions");
9
10
  }
10
11
  function recordPath(id) {
11
12
  return join(runtimeDir(), `${id}.json`);
@@ -15,7 +15,8 @@ import { resolveSearchProviderFromPreferences } from '../gsd/preferences.js';
15
15
  // Compute authFilePath locally instead of importing from app-paths.ts,
16
16
  // because extensions are copied to ~/.gsd/agent/extensions/ at runtime
17
17
  // where the relative import '../../../app-paths.ts' doesn't resolve.
18
- const authFilePath = join(homedir(), '.gsd', 'agent', 'auth.json');
18
+ const gsdHome = process.env.GSD_HOME || join(homedir(), '.gsd');
19
+ const authFilePath = join(gsdHome, 'agent', 'auth.json');
19
20
  const VALID_PREFERENCES = new Set(['tavily', 'brave', 'ollama', 'auto']);
20
21
  const PREFERENCE_KEY = 'search_provider';
21
22
  /** Returns the Tavily API key from the environment, or empty string if not set. */
@@ -17,8 +17,9 @@ const execFile = promisify(execFileCb);
17
17
  function encodeCwd(cwd) {
18
18
  return cwd.replace(/\//g, "--");
19
19
  }
20
+ const gsdHome = process.env.GSD_HOME || path.join(os.homedir(), ".gsd");
20
21
  function getIsolationBaseDir(cwd, taskId) {
21
- return path.join(os.homedir(), ".gsd", "wt", encodeCwd(cwd), taskId);
22
+ return path.join(gsdHome, "wt", encodeCwd(cwd), taskId);
22
23
  }
23
24
  // Track active isolation dirs for cleanup on exit
24
25
  const activeIsolations = new Set();
@@ -8,6 +8,7 @@
8
8
  import { readdirSync, readFileSync, existsSync } from "node:fs";
9
9
  import { join, basename } from "node:path";
10
10
  import { homedir } from "node:os";
11
+ const gsdHome = process.env.GSD_HOME || join(homedir(), ".gsd");
11
12
  import { splitFrontmatter, parseFrontmatterMap } from "../shared/frontmatter.js";
12
13
  function parseRuleFile(filePath) {
13
14
  let content;
@@ -56,7 +57,7 @@ function scanDir(dir) {
56
57
  * Project rules override global rules with the same name.
57
58
  */
58
59
  export function loadRules(cwd) {
59
- const globalDir = join(homedir(), ".gsd", "agent", "rules");
60
+ const globalDir = join(gsdHome, "agent", "rules");
60
61
  const projectDir = join(cwd, ".gsd", "rules");
61
62
  const globalRules = scanDir(globalDir);
62
63
  const projectRules = scanDir(projectDir);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gsd-pi",
3
- "version": "2.38.0-dev.96dc7fb",
3
+ "version": "2.38.0-dev.e40f839",
4
4
  "description": "GSD — Get Shit Done coding agent",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -0,0 +1,31 @@
1
+ // GSD Extension — Environment variable utilities
2
+ // Copyright (c) 2026 Jeremy McSpadden <jeremy@fluxlabs.net>
3
+ //
4
+ // Pure utility for checking existing env keys in .env files and process.env.
5
+ // Extracted from get-secrets-from-user.ts to avoid pulling in @gsd/pi-tui
6
+ // when only env-checking is needed (e.g. from files.ts during report generation).
7
+
8
+ import { readFile } from "node:fs/promises";
9
+
10
+ /**
11
+ * Check which keys already exist in a .env file or process.env.
12
+ * Returns the subset of `keys` that are already set.
13
+ */
14
+ export async function checkExistingEnvKeys(keys: string[], envFilePath: string): Promise<string[]> {
15
+ let fileContent = "";
16
+ try {
17
+ fileContent = await readFile(envFilePath, "utf8");
18
+ } catch {
19
+ // ENOENT or other read error — proceed with empty content
20
+ }
21
+
22
+ const existing: string[] = [];
23
+ for (const key of keys) {
24
+ const escaped = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
25
+ const regex = new RegExp(`^${escaped}\\s*=`, "m");
26
+ if (regex.test(fileContent) || key in process.env) {
27
+ existing.push(key);
28
+ }
29
+ }
30
+ return existing;
31
+ }
@@ -67,30 +67,11 @@ async function writeEnvKey(filePath: string, key: string, value: string): Promis
67
67
 
68
68
  // ─── Exported utilities ───────────────────────────────────────────────────────
69
69
 
70
- /**
71
- * Check which keys already exist in the .env file or process.env.
72
- * Returns the subset of `keys` that are already set.
73
- * Handles ENOENT gracefully (still checks process.env).
74
- * Empty-string values count as existing.
75
- */
76
- export async function checkExistingEnvKeys(keys: string[], envFilePath: string): Promise<string[]> {
77
- let fileContent = "";
78
- try {
79
- fileContent = await readFile(envFilePath, "utf8");
80
- } catch {
81
- // ENOENT or other read error — proceed with empty content
82
- }
83
-
84
- const existing: string[] = [];
85
- for (const key of keys) {
86
- const escaped = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
87
- const regex = new RegExp(`^${escaped}\\s*=`, "m");
88
- if (regex.test(fileContent) || key in process.env) {
89
- existing.push(key);
90
- }
91
- }
92
- return existing;
93
- }
70
+ // Re-export from env-utils.ts so existing consumers still work.
71
+ // The implementation lives in env-utils.ts to avoid pulling @gsd/pi-tui
72
+ // into modules that only need env-checking (e.g. files.ts during reports).
73
+ import { checkExistingEnvKeys } from "./env-utils.js";
74
+ export { checkExistingEnvKeys };
94
75
 
95
76
  /**
96
77
  * Detect the write destination based on project files in basePath.
@@ -22,6 +22,8 @@ import { join, sep as pathSep } from "node:path";
22
22
  import { homedir } from "node:os";
23
23
  import { safeCopy, safeCopyRecursive } from "./safe-fs.js";
24
24
 
25
+ const gsdHome = process.env.GSD_HOME || join(homedir(), ".gsd");
26
+
25
27
  // ─── Project Root → Worktree Sync ─────────────────────────────────────────
26
28
 
27
29
  /**
@@ -111,7 +113,7 @@ export function syncStateToProjectRoot(
111
113
  */
112
114
  export function readResourceVersion(): string | null {
113
115
  const agentDir =
114
- process.env.GSD_CODING_AGENT_DIR || join(homedir(), ".gsd", "agent");
116
+ process.env.GSD_CODING_AGENT_DIR || join(gsdHome, "agent");
115
117
  const manifestPath = join(agentDir, "managed-resources.json");
116
118
  try {
117
119
  const manifest = JSON.parse(readFileSync(manifestPath, "utf-8"));
@@ -11,6 +11,8 @@ import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, writeFile
11
11
  import { dirname, join } from "node:path";
12
12
  import { homedir } from "node:os";
13
13
 
14
+ const gsdHome = process.env.GSD_HOME || join(homedir(), ".gsd");
15
+
14
16
  // ─── Types (mirrored from extension-registry.ts) ────────────────────────────
15
17
 
16
18
  interface ExtensionManifest {
@@ -48,11 +50,11 @@ interface ExtensionRegistry {
48
50
  // ─── Registry I/O ───────────────────────────────────────────────────────────
49
51
 
50
52
  function getRegistryPath(): string {
51
- return join(homedir(), ".gsd", "extensions", "registry.json");
53
+ return join(gsdHome, "extensions", "registry.json");
52
54
  }
53
55
 
54
56
  function getAgentExtensionsDir(): string {
55
- return join(homedir(), ".gsd", "agent", "extensions");
57
+ return join(gsdHome, "agent", "extensions");
56
58
  }
57
59
 
58
60
  function loadRegistry(): ExtensionRegistry {
@@ -10,6 +10,8 @@ import { existsSync, readFileSync, readdirSync, unlinkSync } from "node:fs";
10
10
  import { homedir } from "node:os";
11
11
  import { join } from "node:path";
12
12
  import { gsdRoot } from "./paths.js";
13
+
14
+ const gsdHome = process.env.GSD_HOME || join(homedir(), ".gsd");
13
15
  import { enableDebug } from "./debug-logger.js";
14
16
  import { deriveState } from "./state.js";
15
17
  import { GSDDashboardOverlay } from "./dashboard-overlay.js";
@@ -482,7 +484,7 @@ export function registerGSDCommand(pi: ExtensionAPI): void {
482
484
  if (parts.length === 3 && ["enable", "disable", "info"].includes(parts[1])) {
483
485
  const idPrefix = parts[2] ?? "";
484
486
  try {
485
- const extDir = join(homedir(), ".gsd", "agent", "extensions");
487
+ const extDir = join(gsdHome, "agent", "extensions");
486
488
  const ids: { id: string; name: string }[] = [];
487
489
  for (const entry of readdirSync(extDir, { withFileTypes: true })) {
488
490
  if (!entry.isDirectory()) continue;
@@ -11,6 +11,8 @@ import { join } from "node:path";
11
11
  import { homedir } from "node:os";
12
12
  import { gsdRoot } from "./paths.js";
13
13
 
14
+ const gsdHome = process.env.GSD_HOME || join(homedir(), ".gsd");
15
+
14
16
  // ─── Types ──────────────────────────────────────────────────────────────────────
15
17
 
16
18
  export interface ProjectDetection {
@@ -400,7 +402,6 @@ function detectVerificationCommands(
400
402
  * Check if global GSD setup exists (has ~/.gsd/ with preferences).
401
403
  */
402
404
  export function hasGlobalSetup(): boolean {
403
- const gsdHome = join(homedir(), ".gsd");
404
405
  return (
405
406
  existsSync(join(gsdHome, "preferences.md")) ||
406
407
  existsSync(join(gsdHome, "PREFERENCES.md"))
@@ -412,7 +413,6 @@ export function hasGlobalSetup(): boolean {
412
413
  * Returns true if ~/.gsd/ doesn't exist or has no preferences or auth.
413
414
  */
414
415
  export function isFirstEverLaunch(): boolean {
415
- const gsdHome = join(homedir(), ".gsd");
416
416
  if (!existsSync(gsdHome)) return true;
417
417
 
418
418
  // If we have preferences, not first launch
@@ -11,7 +11,7 @@ import {
11
11
  } from "./metrics.js";
12
12
  import type { UnitMetrics } from "./metrics.js";
13
13
  import { gsdRoot } from "./paths.js";
14
- import { formatDuration, fileLink } from "../shared/mod.js";
14
+ import { formatDuration, fileLink } from "../shared/format-utils.js";
15
15
  import { getErrorMessage } from "./error-utils.js";
16
16
 
17
17
  /**
@@ -7,7 +7,7 @@ import { promises as fs } from 'node:fs';
7
7
  import { resolve } from 'node:path';
8
8
  import { atomicWriteAsync } from './atomic-write.js';
9
9
  import { resolveMilestoneFile, relMilestoneFile, resolveGsdRootFile } from './paths.js';
10
- import { milestoneIdSort, findMilestoneIds } from './guided-flow.js';
10
+ import { milestoneIdSort, findMilestoneIds } from './milestone-ids.js';
11
11
 
12
12
  import type {
13
13
  Roadmap, BoundaryMapEntry,
@@ -20,7 +20,7 @@ import type {
20
20
  ManifestStatus,
21
21
  } from './types.js';
22
22
 
23
- import { checkExistingEnvKeys } from '../get-secrets-from-user.js';
23
+ import { checkExistingEnvKeys } from '../env-utils.js';
24
24
  import { parseRoadmapSlices } from './roadmap-slices.js';
25
25
  import { nativeParseRoadmap, nativeExtractSection, nativeParsePlanFile, nativeParseSummaryFile, NATIVE_UNAVAILABLE } from './native-parser-bridge.js';
26
26
  import { debugTime, debugCount } from './debug-logger.js';
@@ -27,7 +27,7 @@ import { deriveState } from "./state.js";
27
27
  import { isAutoActive } from "./auto.js";
28
28
  import { loadPrompt } from "./prompt-loader.js";
29
29
  import { gsdRoot } from "./paths.js";
30
- import { formatDuration } from "../shared/mod.js";
30
+ import { formatDuration } from "../shared/format-utils.js";
31
31
  import { getAutoWorktreePath } from "./auto-worktree.js";
32
32
 
33
33
  // ─── Types ────────────────────────────────────────────────────────────────────
@@ -60,6 +60,8 @@ import { join } from "node:path";
60
60
  import { existsSync, readFileSync } from "node:fs";
61
61
  import { homedir } from "node:os";
62
62
  import { shortcutDesc } from "../shared/mod.js";
63
+
64
+ const gsdHome = process.env.GSD_HOME || join(homedir(), ".gsd");
63
65
  import { Text } from "@gsd/pi-tui";
64
66
  import { pauseAutoForProviderError, classifyProviderError } from "./provider-error-pause.js";
65
67
  import { toPosixPath } from "../shared/mod.js";
@@ -73,7 +75,7 @@ import { markCmuxPromptShown, shouldPromptToEnableCmux } from "../cmux/index.js"
73
75
 
74
76
  function warnDeprecatedAgentInstructions(): void {
75
77
  const paths = [
76
- join(homedir(), ".gsd", "agent-instructions.md"),
78
+ join(gsdHome, "agent-instructions.md"),
77
79
  join(process.cwd(), ".gsd", "agent-instructions.md"),
78
80
  ];
79
81
  for (const p of paths) {
@@ -3,7 +3,7 @@
3
3
  // Zero Pi dependencies — uses only exported helpers from files.ts.
4
4
 
5
5
  import { splitFrontmatter, parseFrontmatterMap, extractBoldField } from '../files.js';
6
- import { normalizeStringArray } from '../../shared/mod.js';
6
+ import { normalizeStringArray } from '../../shared/format-utils.js';
7
7
 
8
8
  import type {
9
9
  PlanningRoadmap,
@@ -10,7 +10,7 @@ import type { GitPreferences } from "./git-service.js";
10
10
  import type { PostUnitHookConfig, PreDispatchHookConfig, TokenProfile, PhaseSkipPreferences } from "./types.js";
11
11
  import type { DynamicRoutingConfig } from "./model-router.js";
12
12
  import { VALID_BRANCH_NAME } from "./git-service.js";
13
- import { normalizeStringArray } from "../shared/mod.js";
13
+ import { normalizeStringArray } from "../shared/format-utils.js";
14
14
 
15
15
  import {
16
16
  KNOWN_PREFERENCE_KEYS,
@@ -13,11 +13,13 @@
13
13
  import { existsSync, readFileSync } from "node:fs";
14
14
  import { homedir } from "node:os";
15
15
  import { join } from "node:path";
16
+
17
+ const gsdHome = process.env.GSD_HOME || join(homedir(), ".gsd");
16
18
  import { gsdRoot } from "./paths.js";
17
19
  import { parse as parseYaml } from "yaml";
18
20
  import type { PostUnitHookConfig, PreDispatchHookConfig, TokenProfile } from "./types.js";
19
21
  import type { DynamicRoutingConfig } from "./model-router.js";
20
- import { normalizeStringArray } from "../shared/mod.js";
22
+ import { normalizeStringArray } from "../shared/format-utils.js";
21
23
  import { resolveProfileDefaults as _resolveProfileDefaults } from "./preferences-models.js";
22
24
 
23
25
  import {
@@ -82,14 +84,14 @@ export {
82
84
 
83
85
  // ─── Path Constants & Getters ───────────────────────────────────────────────
84
86
 
85
- const GLOBAL_PREFERENCES_PATH = join(homedir(), ".gsd", "preferences.md");
87
+ const GLOBAL_PREFERENCES_PATH = join(gsdHome, "preferences.md");
86
88
  const LEGACY_GLOBAL_PREFERENCES_PATH = join(homedir(), ".pi", "agent", "gsd-preferences.md");
87
89
  function projectPreferencesPath(): string {
88
90
  return join(gsdRoot(process.cwd()), "preferences.md");
89
91
  }
90
92
  // Bootstrap in gitignore.ts historically created PREFERENCES.md (uppercase) by mistake.
91
93
  // Check uppercase as a fallback so those files aren't silently ignored.
92
- const GLOBAL_PREFERENCES_PATH_UPPERCASE = join(homedir(), ".gsd", "PREFERENCES.md");
94
+ const GLOBAL_PREFERENCES_PATH_UPPERCASE = join(gsdHome, "PREFERENCES.md");
93
95
  function projectPreferencesPathUppercase(): string {
94
96
  return join(gsdRoot(process.cwd()), "PREFERENCES.md");
95
97
  }
@@ -12,6 +12,8 @@ import { existsSync, lstatSync, mkdirSync, readFileSync, realpathSync, rmSync, s
12
12
  import { homedir } from "node:os";
13
13
  import { join, resolve } from "node:path";
14
14
 
15
+ const gsdHome = process.env.GSD_HOME || join(homedir(), ".gsd");
16
+
15
17
  // ─── Repo Identity ──────────────────────────────────────────────────────────
16
18
 
17
19
  /**
@@ -113,7 +115,7 @@ export function repoIdentity(basePath: string): string {
113
115
  * otherwise `~/.gsd/projects/<hash>`.
114
116
  */
115
117
  export function externalGsdRoot(basePath: string): string {
116
- const base = process.env.GSD_STATE_DIR || join(homedir(), ".gsd");
118
+ const base = process.env.GSD_STATE_DIR || gsdHome;
117
119
  return join(base, "projects", repoIdentity(basePath));
118
120
  }
119
121
 
@@ -11,6 +11,8 @@ import { join } from "node:path";
11
11
  import { homedir } from "node:os";
12
12
  import { resolveProjectRoot } from "./worktree.js";
13
13
 
14
+ const gsdHome = process.env.GSD_HOME || join(homedir(), ".gsd");
15
+
14
16
  // ─── Resource Staleness ───────────────────────────────────────────────────
15
17
 
16
18
  /**
@@ -23,7 +25,7 @@ function isManifestWithVersion(data: unknown): data is { gsdVersion: string } {
23
25
  }
24
26
 
25
27
  export function readResourceVersion(): string | null {
26
- const agentDir = process.env.GSD_CODING_AGENT_DIR || join(homedir(), ".gsd", "agent");
28
+ const agentDir = process.env.GSD_CODING_AGENT_DIR || join(gsdHome, "agent");
27
29
  const manifestPath = join(agentDir, "managed-resources.json");
28
30
  const manifest = loadJsonFileOrNull(manifestPath, isManifestWithVersion);
29
31
  return manifest?.gsdVersion ?? null;
@@ -31,7 +31,7 @@ import {
31
31
  gsdRoot,
32
32
  } from './paths.js';
33
33
 
34
- import { milestoneIdSort, findMilestoneIds } from './guided-flow.js';
34
+ import { milestoneIdSort, findMilestoneIds } from './milestone-ids.js';
35
35
  import { nativeBatchParseGsdFiles, type BatchParsedFile } from './native-parser-bridge.js';
36
36
 
37
37
  import { join, resolve } from 'path';
@@ -3,7 +3,7 @@
3
3
  import { existsSync, readFileSync, statSync } from 'node:fs';
4
4
  import { deriveState } from './state.js';
5
5
  import { parseRoadmap, parsePlan, parseSummary, loadFile } from './files.js';
6
- import { findMilestoneIds } from './guided-flow.js';
6
+ import { findMilestoneIds } from './milestone-ids.js';
7
7
  import { resolveMilestoneFile, resolveSliceFile, resolveGsdRootFile } from './paths.js';
8
8
  import {
9
9
  getLedger,
@@ -7,6 +7,8 @@ import { join } from "node:path";
7
7
  import { homedir } from "node:os";
8
8
  import { readPromptRecord } from "./store.js";
9
9
 
10
+ const gsdHome = process.env.GSD_HOME || join(homedir(), ".gsd");
11
+
10
12
  export interface LatestPromptSummary {
11
13
  id: string;
12
14
  status: string;
@@ -14,7 +16,7 @@ export interface LatestPromptSummary {
14
16
  }
15
17
 
16
18
  export function getLatestPromptSummary(): LatestPromptSummary | null {
17
- const runtimeDir = join(homedir(), ".gsd", "runtime", "remote-questions");
19
+ const runtimeDir = join(gsdHome, "runtime", "remote-questions");
18
20
  if (!existsSync(runtimeDir)) return null;
19
21
  const files = readdirSync(runtimeDir).filter((f) => f.endsWith(".json"));
20
22
  if (files.length === 0) return null;
@@ -7,8 +7,10 @@ import { join } from "node:path";
7
7
  import { homedir } from "node:os";
8
8
  import type { RemotePrompt, RemotePromptRecord, RemotePromptRef, RemoteAnswer, RemotePromptStatus } from "./types.js";
9
9
 
10
+ const gsdHome = process.env.GSD_HOME || join(homedir(), ".gsd");
11
+
10
12
  function runtimeDir(): string {
11
- return join(homedir(), ".gsd", "runtime", "remote-questions");
13
+ return join(gsdHome, "runtime", "remote-questions");
12
14
  }
13
15
 
14
16
  function recordPath(id: string): string {
@@ -17,7 +17,8 @@ import { resolveSearchProviderFromPreferences } from '../gsd/preferences.js'
17
17
  // Compute authFilePath locally instead of importing from app-paths.ts,
18
18
  // because extensions are copied to ~/.gsd/agent/extensions/ at runtime
19
19
  // where the relative import '../../../app-paths.ts' doesn't resolve.
20
- const authFilePath = join(homedir(), '.gsd', 'agent', 'auth.json')
20
+ const gsdHome = process.env.GSD_HOME || join(homedir(), '.gsd')
21
+ const authFilePath = join(gsdHome, 'agent', 'auth.json')
21
22
 
22
23
  export type SearchProvider = 'tavily' | 'brave' | 'ollama'
23
24
  export type SearchProviderPreference = SearchProvider | 'auto'
@@ -57,8 +57,10 @@ function encodeCwd(cwd: string): string {
57
57
  return cwd.replace(/\//g, "--");
58
58
  }
59
59
 
60
+ const gsdHome = process.env.GSD_HOME || path.join(os.homedir(), ".gsd");
61
+
60
62
  function getIsolationBaseDir(cwd: string, taskId: string): string {
61
- return path.join(os.homedir(), ".gsd", "wt", encodeCwd(cwd), taskId);
63
+ return path.join(gsdHome, "wt", encodeCwd(cwd), taskId);
62
64
  }
63
65
 
64
66
  // Track active isolation dirs for cleanup on exit
@@ -8,6 +8,8 @@
8
8
  import { readdirSync, readFileSync, existsSync } from "node:fs";
9
9
  import { join, basename } from "node:path";
10
10
  import { homedir } from "node:os";
11
+
12
+ const gsdHome = process.env.GSD_HOME || join(homedir(), ".gsd");
11
13
  import type { Rule } from "./ttsr-manager.js";
12
14
  import { splitFrontmatter, parseFrontmatterMap } from "../shared/frontmatter.js";
13
15
 
@@ -59,7 +61,7 @@ function scanDir(dir: string): Rule[] {
59
61
  * Project rules override global rules with the same name.
60
62
  */
61
63
  export function loadRules(cwd: string): Rule[] {
62
- const globalDir = join(homedir(), ".gsd", "agent", "rules");
64
+ const globalDir = join(gsdHome, "agent", "rules");
63
65
  const projectDir = join(cwd, ".gsd", "rules");
64
66
 
65
67
  const globalRules = scanDir(globalDir);