gsd-pi 2.5.0 → 2.6.0

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 (33) hide show
  1. package/README.md +1 -0
  2. package/dist/cli.js +7 -1
  3. package/dist/loader.js +21 -3
  4. package/dist/logo.d.ts +3 -3
  5. package/dist/logo.js +2 -2
  6. package/package.json +1 -1
  7. package/src/resources/extensions/get-secrets-from-user.ts +331 -59
  8. package/src/resources/extensions/gsd/auto.ts +80 -18
  9. package/src/resources/extensions/gsd/docs/preferences-reference.md +1 -0
  10. package/src/resources/extensions/gsd/doctor.ts +23 -4
  11. package/src/resources/extensions/gsd/files.ts +115 -1
  12. package/src/resources/extensions/gsd/git-service.ts +67 -105
  13. package/src/resources/extensions/gsd/gitignore.ts +1 -0
  14. package/src/resources/extensions/gsd/guided-flow.ts +6 -3
  15. package/src/resources/extensions/gsd/preferences.ts +8 -0
  16. package/src/resources/extensions/gsd/prompts/complete-slice.md +7 -5
  17. package/src/resources/extensions/gsd/prompts/discuss.md +7 -15
  18. package/src/resources/extensions/gsd/prompts/execute-task.md +2 -6
  19. package/src/resources/extensions/gsd/prompts/guided-plan-milestone.md +4 -0
  20. package/src/resources/extensions/gsd/prompts/plan-milestone.md +33 -1
  21. package/src/resources/extensions/gsd/prompts/plan-slice.md +24 -32
  22. package/src/resources/extensions/gsd/session-forensics.ts +19 -6
  23. package/src/resources/extensions/gsd/templates/plan.md +8 -10
  24. package/src/resources/extensions/gsd/templates/secrets-manifest.md +22 -0
  25. package/src/resources/extensions/gsd/templates/task-plan.md +6 -6
  26. package/src/resources/extensions/gsd/tests/auto-secrets-gate.test.ts +196 -0
  27. package/src/resources/extensions/gsd/tests/collect-from-manifest.test.ts +469 -0
  28. package/src/resources/extensions/gsd/tests/doctor-fixlevel.test.ts +170 -0
  29. package/src/resources/extensions/gsd/tests/git-service.test.ts +106 -0
  30. package/src/resources/extensions/gsd/tests/manifest-status.test.ts +283 -0
  31. package/src/resources/extensions/gsd/tests/parsers.test.ts +401 -65
  32. package/src/resources/extensions/gsd/tests/secure-env-collect.test.ts +185 -0
  33. package/src/resources/extensions/gsd/types.ts +27 -0
@@ -0,0 +1,185 @@
1
+ /**
2
+ * Tests for secure_env_collect utility functions:
3
+ * - checkExistingEnvKeys: detects keys already present in .env file or process.env
4
+ * - detectDestination: infers write destination from project files
5
+ *
6
+ * Uses temp directories for filesystem isolation.
7
+ */
8
+
9
+ import test from "node:test";
10
+ import assert from "node:assert/strict";
11
+ import { mkdirSync, writeFileSync, rmSync } from "node:fs";
12
+ import { join } from "node:path";
13
+ import { tmpdir } from "node:os";
14
+ import { checkExistingEnvKeys, detectDestination } from "../../get-secrets-from-user.ts";
15
+
16
+ function makeTempDir(prefix: string): string {
17
+ const dir = join(tmpdir(), `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2)}`);
18
+ mkdirSync(dir, { recursive: true });
19
+ return dir;
20
+ }
21
+
22
+ // ─── checkExistingEnvKeys ─────────────────────────────────────────────────────
23
+
24
+ test("secure_env_collect: checkExistingEnvKeys — key found in .env file", async () => {
25
+ const tmp = makeTempDir("sec-env-test");
26
+ try {
27
+ const envPath = join(tmp, ".env");
28
+ writeFileSync(envPath, "API_KEY=secret123\nOTHER=val\n");
29
+ const result = await checkExistingEnvKeys(["API_KEY"], envPath);
30
+ assert.deepStrictEqual(result, ["API_KEY"]);
31
+ } finally {
32
+ rmSync(tmp, { recursive: true, force: true });
33
+ }
34
+ });
35
+
36
+ test("secure_env_collect: checkExistingEnvKeys — key found in process.env", async () => {
37
+ const tmp = makeTempDir("sec-env-test");
38
+ const savedVal = process.env.GSD_TEST_ENV_KEY_12345;
39
+ try {
40
+ process.env.GSD_TEST_ENV_KEY_12345 = "some-value";
41
+ const envPath = join(tmp, ".env"); // file doesn't exist
42
+ const result = await checkExistingEnvKeys(["GSD_TEST_ENV_KEY_12345"], envPath);
43
+ assert.deepStrictEqual(result, ["GSD_TEST_ENV_KEY_12345"]);
44
+ } finally {
45
+ delete process.env.GSD_TEST_ENV_KEY_12345;
46
+ if (savedVal !== undefined) process.env.GSD_TEST_ENV_KEY_12345 = savedVal;
47
+ rmSync(tmp, { recursive: true, force: true });
48
+ }
49
+ });
50
+
51
+ test("secure_env_collect: checkExistingEnvKeys — key found in both .env and process.env", async () => {
52
+ const tmp = makeTempDir("sec-env-test");
53
+ const savedVal = process.env.GSD_TEST_BOTH_KEY;
54
+ try {
55
+ process.env.GSD_TEST_BOTH_KEY = "from-env";
56
+ const envPath = join(tmp, ".env");
57
+ writeFileSync(envPath, "GSD_TEST_BOTH_KEY=from-file\n");
58
+ const result = await checkExistingEnvKeys(["GSD_TEST_BOTH_KEY"], envPath);
59
+ assert.deepStrictEqual(result, ["GSD_TEST_BOTH_KEY"]);
60
+ } finally {
61
+ delete process.env.GSD_TEST_BOTH_KEY;
62
+ if (savedVal !== undefined) process.env.GSD_TEST_BOTH_KEY = savedVal;
63
+ rmSync(tmp, { recursive: true, force: true });
64
+ }
65
+ });
66
+
67
+ test("secure_env_collect: checkExistingEnvKeys — key not found anywhere", async () => {
68
+ const tmp = makeTempDir("sec-env-test");
69
+ try {
70
+ const envPath = join(tmp, ".env");
71
+ writeFileSync(envPath, "OTHER_KEY=val\n");
72
+ // Ensure it's not in process.env
73
+ delete process.env.DEFINITELY_NOT_SET_KEY_XYZ;
74
+ const result = await checkExistingEnvKeys(["DEFINITELY_NOT_SET_KEY_XYZ"], envPath);
75
+ assert.deepStrictEqual(result, []);
76
+ } finally {
77
+ rmSync(tmp, { recursive: true, force: true });
78
+ }
79
+ });
80
+
81
+ test("secure_env_collect: checkExistingEnvKeys — .env file doesn't exist (ENOENT), still checks process.env", async () => {
82
+ const tmp = makeTempDir("sec-env-test");
83
+ const savedVal = process.env.GSD_TEST_ENOENT_KEY;
84
+ try {
85
+ process.env.GSD_TEST_ENOENT_KEY = "exists-in-process";
86
+ const envPath = join(tmp, "nonexistent.env");
87
+ const result = await checkExistingEnvKeys(["GSD_TEST_ENOENT_KEY", "MISSING_KEY_XYZ"], envPath);
88
+ assert.deepStrictEqual(result, ["GSD_TEST_ENOENT_KEY"]);
89
+ } finally {
90
+ delete process.env.GSD_TEST_ENOENT_KEY;
91
+ if (savedVal !== undefined) process.env.GSD_TEST_ENOENT_KEY = savedVal;
92
+ rmSync(tmp, { recursive: true, force: true });
93
+ }
94
+ });
95
+
96
+ test("secure_env_collect: checkExistingEnvKeys — empty-string value in process.env counts as existing", async () => {
97
+ const tmp = makeTempDir("sec-env-test");
98
+ const savedVal = process.env.GSD_TEST_EMPTY_KEY;
99
+ try {
100
+ process.env.GSD_TEST_EMPTY_KEY = "";
101
+ const envPath = join(tmp, ".env");
102
+ writeFileSync(envPath, "");
103
+ const result = await checkExistingEnvKeys(["GSD_TEST_EMPTY_KEY"], envPath);
104
+ assert.deepStrictEqual(result, ["GSD_TEST_EMPTY_KEY"]);
105
+ } finally {
106
+ delete process.env.GSD_TEST_EMPTY_KEY;
107
+ if (savedVal !== undefined) process.env.GSD_TEST_EMPTY_KEY = savedVal;
108
+ rmSync(tmp, { recursive: true, force: true });
109
+ }
110
+ });
111
+
112
+ test("secure_env_collect: checkExistingEnvKeys — returns only existing keys from input list", async () => {
113
+ const tmp = makeTempDir("sec-env-test");
114
+ const saved1 = process.env.GSD_TEST_EXISTS_A;
115
+ const saved2 = process.env.GSD_TEST_EXISTS_B;
116
+ try {
117
+ process.env.GSD_TEST_EXISTS_A = "val-a";
118
+ delete process.env.GSD_TEST_EXISTS_B;
119
+ const envPath = join(tmp, ".env");
120
+ writeFileSync(envPath, "FILE_KEY=val\n");
121
+ const result = await checkExistingEnvKeys(
122
+ ["GSD_TEST_EXISTS_A", "GSD_TEST_EXISTS_B", "FILE_KEY", "NOPE_KEY"],
123
+ envPath,
124
+ );
125
+ assert.deepStrictEqual(result.sort(), ["FILE_KEY", "GSD_TEST_EXISTS_A"]);
126
+ } finally {
127
+ delete process.env.GSD_TEST_EXISTS_A;
128
+ delete process.env.GSD_TEST_EXISTS_B;
129
+ if (saved1 !== undefined) process.env.GSD_TEST_EXISTS_A = saved1;
130
+ if (saved2 !== undefined) process.env.GSD_TEST_EXISTS_B = saved2;
131
+ rmSync(tmp, { recursive: true, force: true });
132
+ }
133
+ });
134
+
135
+ // ─── detectDestination ────────────────────────────────────────────────────────
136
+
137
+ test("secure_env_collect: detectDestination — returns 'vercel' when vercel.json exists", () => {
138
+ const tmp = makeTempDir("sec-dest-test");
139
+ try {
140
+ writeFileSync(join(tmp, "vercel.json"), "{}");
141
+ assert.equal(detectDestination(tmp), "vercel");
142
+ } finally {
143
+ rmSync(tmp, { recursive: true, force: true });
144
+ }
145
+ });
146
+
147
+ test("secure_env_collect: detectDestination — returns 'convex' when convex/ dir exists", () => {
148
+ const tmp = makeTempDir("sec-dest-test");
149
+ try {
150
+ mkdirSync(join(tmp, "convex"));
151
+ assert.equal(detectDestination(tmp), "convex");
152
+ } finally {
153
+ rmSync(tmp, { recursive: true, force: true });
154
+ }
155
+ });
156
+
157
+ test("secure_env_collect: detectDestination — returns 'dotenv' when neither exists", () => {
158
+ const tmp = makeTempDir("sec-dest-test");
159
+ try {
160
+ assert.equal(detectDestination(tmp), "dotenv");
161
+ } finally {
162
+ rmSync(tmp, { recursive: true, force: true });
163
+ }
164
+ });
165
+
166
+ test("secure_env_collect: detectDestination — vercel takes priority when both exist", () => {
167
+ const tmp = makeTempDir("sec-dest-test");
168
+ try {
169
+ writeFileSync(join(tmp, "vercel.json"), "{}");
170
+ mkdirSync(join(tmp, "convex"));
171
+ assert.equal(detectDestination(tmp), "vercel");
172
+ } finally {
173
+ rmSync(tmp, { recursive: true, force: true });
174
+ }
175
+ });
176
+
177
+ test("secure_env_collect: detectDestination — convex file (not dir) does not trigger convex", () => {
178
+ const tmp = makeTempDir("sec-dest-test");
179
+ try {
180
+ writeFileSync(join(tmp, "convex"), "not a directory");
181
+ assert.equal(detectDestination(tmp), "dotenv");
182
+ } finally {
183
+ rmSync(tmp, { recursive: true, force: true });
184
+ }
185
+ });
@@ -116,6 +116,33 @@ export interface Continue {
116
116
  nextAction: string;
117
117
  }
118
118
 
119
+ // ─── Secrets Manifest ──────────────────────────────────────────────────────
120
+
121
+ export type SecretsManifestEntryStatus = 'pending' | 'collected' | 'skipped';
122
+
123
+ export interface SecretsManifestEntry {
124
+ key: string; // e.g. "OPENAI_API_KEY"
125
+ service: string; // e.g. "OpenAI"
126
+ dashboardUrl: string; // e.g. "https://platform.openai.com/api-keys" — empty if unknown
127
+ guidance: string[]; // numbered setup steps
128
+ formatHint: string; // e.g. "starts with sk-" — empty if unknown
129
+ status: SecretsManifestEntryStatus;
130
+ destination: string; // e.g. "dotenv", "vercel", "convex"
131
+ }
132
+
133
+ export interface SecretsManifest {
134
+ milestone: string; // e.g. "M001"
135
+ generatedAt: string; // ISO 8601 timestamp
136
+ entries: SecretsManifestEntry[];
137
+ }
138
+
139
+ export interface ManifestStatus {
140
+ pending: string[]; // manifest status = pending AND not in env
141
+ collected: string[]; // manifest status = collected AND not in env
142
+ skipped: string[]; // manifest status = skipped
143
+ existing: string[]; // key present in .env or process.env (regardless of manifest status)
144
+ }
145
+
119
146
  // ─── GSD State (Derived Dashboard) ────────────────────────────────────────
120
147
 
121
148
  export interface ActiveRef {