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.
- package/README.md +1 -0
- package/dist/cli.js +7 -1
- package/dist/loader.js +21 -3
- package/dist/logo.d.ts +3 -3
- package/dist/logo.js +2 -2
- package/package.json +1 -1
- package/src/resources/extensions/get-secrets-from-user.ts +331 -59
- package/src/resources/extensions/gsd/auto.ts +80 -18
- package/src/resources/extensions/gsd/docs/preferences-reference.md +1 -0
- package/src/resources/extensions/gsd/doctor.ts +23 -4
- package/src/resources/extensions/gsd/files.ts +115 -1
- package/src/resources/extensions/gsd/git-service.ts +67 -105
- package/src/resources/extensions/gsd/gitignore.ts +1 -0
- package/src/resources/extensions/gsd/guided-flow.ts +6 -3
- package/src/resources/extensions/gsd/preferences.ts +8 -0
- package/src/resources/extensions/gsd/prompts/complete-slice.md +7 -5
- package/src/resources/extensions/gsd/prompts/discuss.md +7 -15
- package/src/resources/extensions/gsd/prompts/execute-task.md +2 -6
- package/src/resources/extensions/gsd/prompts/guided-plan-milestone.md +4 -0
- package/src/resources/extensions/gsd/prompts/plan-milestone.md +33 -1
- package/src/resources/extensions/gsd/prompts/plan-slice.md +24 -32
- package/src/resources/extensions/gsd/session-forensics.ts +19 -6
- package/src/resources/extensions/gsd/templates/plan.md +8 -10
- package/src/resources/extensions/gsd/templates/secrets-manifest.md +22 -0
- package/src/resources/extensions/gsd/templates/task-plan.md +6 -6
- package/src/resources/extensions/gsd/tests/auto-secrets-gate.test.ts +196 -0
- package/src/resources/extensions/gsd/tests/collect-from-manifest.test.ts +469 -0
- package/src/resources/extensions/gsd/tests/doctor-fixlevel.test.ts +170 -0
- package/src/resources/extensions/gsd/tests/git-service.test.ts +106 -0
- package/src/resources/extensions/gsd/tests/manifest-status.test.ts +283 -0
- package/src/resources/extensions/gsd/tests/parsers.test.ts +401 -65
- package/src/resources/extensions/gsd/tests/secure-env-collect.test.ts +185 -0
- 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 {
|