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
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
inferCommitType,
|
|
8
8
|
GitServiceImpl,
|
|
9
9
|
RUNTIME_EXCLUSION_PATHS,
|
|
10
|
+
VALID_BRANCH_NAME,
|
|
10
11
|
runGit,
|
|
11
12
|
type GitPreferences,
|
|
12
13
|
type CommitOptions,
|
|
@@ -452,6 +453,49 @@ async function main(): Promise<void> {
|
|
|
452
453
|
rmSync(repo, { recursive: true, force: true });
|
|
453
454
|
}
|
|
454
455
|
|
|
456
|
+
// ─── GitServiceImpl: autoCommit with extraExclusions ───────────────────
|
|
457
|
+
|
|
458
|
+
console.log("\n=== GitServiceImpl: autoCommit with extraExclusions ===");
|
|
459
|
+
|
|
460
|
+
{
|
|
461
|
+
const repo = initTempRepo();
|
|
462
|
+
const svc = new GitServiceImpl(repo);
|
|
463
|
+
|
|
464
|
+
// Create both a .gsd/ planning file and a regular source file
|
|
465
|
+
createFile(repo, ".gsd/milestones/M001/M001-ROADMAP.md", "- [x] S01");
|
|
466
|
+
createFile(repo, "src/feature.ts", "export const y = 2;");
|
|
467
|
+
|
|
468
|
+
// Auto-commit with .gsd/ excluded (simulates pre-switch)
|
|
469
|
+
const msg = svc.autoCommit("pre-switch", "main", [".gsd/"]);
|
|
470
|
+
assertEq(msg, "chore(main): auto-commit after pre-switch", "pre-switch autoCommit with .gsd/ exclusion commits");
|
|
471
|
+
|
|
472
|
+
// Verify .gsd/ file was NOT committed
|
|
473
|
+
const show = run("git show --stat HEAD", repo);
|
|
474
|
+
assert(!show.includes("ROADMAP"), ".gsd/ files excluded from pre-switch auto-commit");
|
|
475
|
+
assert(show.includes("feature.ts"), "non-.gsd/ files included in pre-switch auto-commit");
|
|
476
|
+
|
|
477
|
+
rmSync(repo, { recursive: true, force: true });
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// ─── GitServiceImpl: autoCommit extraExclusions — only .gsd/ dirty ────
|
|
481
|
+
|
|
482
|
+
console.log("\n=== GitServiceImpl: autoCommit extraExclusions — only .gsd/ dirty ===");
|
|
483
|
+
|
|
484
|
+
{
|
|
485
|
+
const repo = initTempRepo();
|
|
486
|
+
const svc = new GitServiceImpl(repo);
|
|
487
|
+
|
|
488
|
+
// Create only .gsd/ planning files
|
|
489
|
+
createFile(repo, ".gsd/milestones/M001/M001-ROADMAP.md", "- [x] S01");
|
|
490
|
+
createFile(repo, ".gsd/STATE.md", "state content");
|
|
491
|
+
|
|
492
|
+
// Auto-commit with .gsd/ excluded — nothing else to commit
|
|
493
|
+
const result = svc.autoCommit("pre-switch", "main", [".gsd/"]);
|
|
494
|
+
assertEq(result, null, "autoCommit returns null when only .gsd/ files are dirty and excluded");
|
|
495
|
+
|
|
496
|
+
rmSync(repo, { recursive: true, force: true });
|
|
497
|
+
}
|
|
498
|
+
|
|
455
499
|
// ─── GitServiceImpl: commit returns null when nothing staged ───────────
|
|
456
500
|
|
|
457
501
|
console.log("\n=== GitServiceImpl: commit empty ===");
|
|
@@ -1230,6 +1274,68 @@ async function main(): Promise<void> {
|
|
|
1230
1274
|
rmSync(repo, { recursive: true, force: true });
|
|
1231
1275
|
}
|
|
1232
1276
|
|
|
1277
|
+
// ─── VALID_BRANCH_NAME regex ──────────────────────────────────────────
|
|
1278
|
+
|
|
1279
|
+
console.log("\n=== VALID_BRANCH_NAME regex ===");
|
|
1280
|
+
|
|
1281
|
+
{
|
|
1282
|
+
// Valid branch names
|
|
1283
|
+
assert(VALID_BRANCH_NAME.test("main"), "VALID_BRANCH_NAME accepts 'main'");
|
|
1284
|
+
assert(VALID_BRANCH_NAME.test("master"), "VALID_BRANCH_NAME accepts 'master'");
|
|
1285
|
+
assert(VALID_BRANCH_NAME.test("develop"), "VALID_BRANCH_NAME accepts 'develop'");
|
|
1286
|
+
assert(VALID_BRANCH_NAME.test("feature/foo"), "VALID_BRANCH_NAME accepts 'feature/foo'");
|
|
1287
|
+
assert(VALID_BRANCH_NAME.test("release-1.0"), "VALID_BRANCH_NAME accepts 'release-1.0'");
|
|
1288
|
+
assert(VALID_BRANCH_NAME.test("my_branch"), "VALID_BRANCH_NAME accepts 'my_branch'");
|
|
1289
|
+
assert(VALID_BRANCH_NAME.test("v2.0.1"), "VALID_BRANCH_NAME accepts 'v2.0.1'");
|
|
1290
|
+
|
|
1291
|
+
// Invalid / injection attempts
|
|
1292
|
+
assert(!VALID_BRANCH_NAME.test("main; rm -rf /"), "VALID_BRANCH_NAME rejects shell injection");
|
|
1293
|
+
assert(!VALID_BRANCH_NAME.test("main && echo pwned"), "VALID_BRANCH_NAME rejects && injection");
|
|
1294
|
+
assert(!VALID_BRANCH_NAME.test(""), "VALID_BRANCH_NAME rejects empty string");
|
|
1295
|
+
assert(!VALID_BRANCH_NAME.test("branch name"), "VALID_BRANCH_NAME rejects spaces");
|
|
1296
|
+
assert(!VALID_BRANCH_NAME.test("branch`cmd`"), "VALID_BRANCH_NAME rejects backticks");
|
|
1297
|
+
assert(!VALID_BRANCH_NAME.test("branch$(cmd)"), "VALID_BRANCH_NAME rejects $() subshell");
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
// ─── getMainBranch: configured main_branch preference ──────────────────
|
|
1301
|
+
|
|
1302
|
+
console.log("\n=== getMainBranch: configured main_branch ===");
|
|
1303
|
+
|
|
1304
|
+
{
|
|
1305
|
+
const repo = initBranchTestRepo();
|
|
1306
|
+
const svc = new GitServiceImpl(repo, { main_branch: "trunk" });
|
|
1307
|
+
|
|
1308
|
+
assertEq(svc.getMainBranch(), "trunk", "getMainBranch returns configured main_branch preference");
|
|
1309
|
+
|
|
1310
|
+
rmSync(repo, { recursive: true, force: true });
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
// ─── getMainBranch: falls back to auto-detection when not set ──────────
|
|
1314
|
+
|
|
1315
|
+
console.log("\n=== getMainBranch: fallback to auto-detection ===");
|
|
1316
|
+
|
|
1317
|
+
{
|
|
1318
|
+
const repo = initBranchTestRepo();
|
|
1319
|
+
const svc = new GitServiceImpl(repo, {});
|
|
1320
|
+
|
|
1321
|
+
assertEq(svc.getMainBranch(), "main", "getMainBranch falls back to auto-detection when main_branch not set");
|
|
1322
|
+
|
|
1323
|
+
rmSync(repo, { recursive: true, force: true });
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1326
|
+
// ─── getMainBranch: ignores invalid branch names ───────────────────────
|
|
1327
|
+
|
|
1328
|
+
console.log("\n=== getMainBranch: ignores invalid branch name ===");
|
|
1329
|
+
|
|
1330
|
+
{
|
|
1331
|
+
const repo = initBranchTestRepo();
|
|
1332
|
+
const svc = new GitServiceImpl(repo, { main_branch: "main; rm -rf /" });
|
|
1333
|
+
|
|
1334
|
+
assertEq(svc.getMainBranch(), "main", "getMainBranch ignores invalid branch name and falls back to auto-detection");
|
|
1335
|
+
|
|
1336
|
+
rmSync(repo, { recursive: true, force: true });
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1233
1339
|
// ─── PreMergeCheckResult type export compile check ─────────────────────
|
|
1234
1340
|
|
|
1235
1341
|
console.log("\n=== PreMergeCheckResult type export ===");
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for getManifestStatus() — the S01→S02 boundary contract.
|
|
3
|
+
*
|
|
4
|
+
* Verifies that manifest entries are correctly categorized into
|
|
5
|
+
* pending, collected, skipped, and existing arrays based on
|
|
6
|
+
* manifest status and environment presence.
|
|
7
|
+
*
|
|
8
|
+
* Uses temp directories with real .gsd/milestones/M001/ structure.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import test from 'node:test';
|
|
12
|
+
import assert from 'node:assert/strict';
|
|
13
|
+
import { mkdirSync, writeFileSync, rmSync } from 'node:fs';
|
|
14
|
+
import { join } from 'node:path';
|
|
15
|
+
import { tmpdir } from 'node:os';
|
|
16
|
+
import { getManifestStatus } from '../files.ts';
|
|
17
|
+
|
|
18
|
+
function makeTempDir(prefix: string): string {
|
|
19
|
+
const dir = join(tmpdir(), `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
|
20
|
+
mkdirSync(dir, { recursive: true });
|
|
21
|
+
return dir;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Create the .gsd/milestones/M001/ directory structure and write a secrets manifest. */
|
|
25
|
+
function writeManifest(base: string, content: string): void {
|
|
26
|
+
const mDir = join(base, '.gsd', 'milestones', 'M001');
|
|
27
|
+
mkdirSync(mDir, { recursive: true });
|
|
28
|
+
writeFileSync(join(mDir, 'M001-SECRETS.md'), content);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// ─── Mixed statuses ──────────────────────────────────────────────────────────
|
|
32
|
+
|
|
33
|
+
test('getManifestStatus: mixed statuses — categorizes entries correctly', async () => {
|
|
34
|
+
const tmp = makeTempDir('manifest-mixed');
|
|
35
|
+
const savedVal = process.env.GSD_TEST_EXISTING_KEY_001;
|
|
36
|
+
try {
|
|
37
|
+
process.env.GSD_TEST_EXISTING_KEY_001 = 'some-value';
|
|
38
|
+
|
|
39
|
+
writeManifest(tmp, `# Secrets Manifest
|
|
40
|
+
|
|
41
|
+
**Milestone:** M001
|
|
42
|
+
**Generated:** 2025-06-20T10:00:00Z
|
|
43
|
+
|
|
44
|
+
### PENDING_KEY
|
|
45
|
+
|
|
46
|
+
**Service:** SomeService
|
|
47
|
+
**Status:** pending
|
|
48
|
+
**Destination:** dotenv
|
|
49
|
+
|
|
50
|
+
1. Get the key
|
|
51
|
+
|
|
52
|
+
### COLLECTED_KEY
|
|
53
|
+
|
|
54
|
+
**Service:** AnotherService
|
|
55
|
+
**Status:** collected
|
|
56
|
+
**Destination:** dotenv
|
|
57
|
+
|
|
58
|
+
1. Already collected
|
|
59
|
+
|
|
60
|
+
### SKIPPED_KEY
|
|
61
|
+
|
|
62
|
+
**Service:** OptionalService
|
|
63
|
+
**Status:** skipped
|
|
64
|
+
**Destination:** dotenv
|
|
65
|
+
|
|
66
|
+
1. Not needed
|
|
67
|
+
|
|
68
|
+
### GSD_TEST_EXISTING_KEY_001
|
|
69
|
+
|
|
70
|
+
**Service:** EnvService
|
|
71
|
+
**Status:** pending
|
|
72
|
+
**Destination:** dotenv
|
|
73
|
+
|
|
74
|
+
1. Already in env
|
|
75
|
+
`);
|
|
76
|
+
|
|
77
|
+
const result = await getManifestStatus(tmp, 'M001');
|
|
78
|
+
assert.notStrictEqual(result, null, 'should not be null');
|
|
79
|
+
assert.deepStrictEqual(result!.pending, ['PENDING_KEY']);
|
|
80
|
+
assert.deepStrictEqual(result!.collected, ['COLLECTED_KEY']);
|
|
81
|
+
assert.deepStrictEqual(result!.skipped, ['SKIPPED_KEY']);
|
|
82
|
+
assert.deepStrictEqual(result!.existing, ['GSD_TEST_EXISTING_KEY_001']);
|
|
83
|
+
} finally {
|
|
84
|
+
delete process.env.GSD_TEST_EXISTING_KEY_001;
|
|
85
|
+
if (savedVal !== undefined) process.env.GSD_TEST_EXISTING_KEY_001 = savedVal;
|
|
86
|
+
rmSync(tmp, { recursive: true, force: true });
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
// ─── All pending ─────────────────────────────────────────────────────────────
|
|
91
|
+
|
|
92
|
+
test('getManifestStatus: all pending — 3 pending entries, none in env', async () => {
|
|
93
|
+
const tmp = makeTempDir('manifest-pending');
|
|
94
|
+
try {
|
|
95
|
+
// Ensure none of these are in process.env
|
|
96
|
+
delete process.env.PEND_A;
|
|
97
|
+
delete process.env.PEND_B;
|
|
98
|
+
delete process.env.PEND_C;
|
|
99
|
+
|
|
100
|
+
writeManifest(tmp, `# Secrets Manifest
|
|
101
|
+
|
|
102
|
+
**Milestone:** M001
|
|
103
|
+
**Generated:** 2025-06-20T10:00:00Z
|
|
104
|
+
|
|
105
|
+
### PEND_A
|
|
106
|
+
|
|
107
|
+
**Service:** A
|
|
108
|
+
**Status:** pending
|
|
109
|
+
**Destination:** dotenv
|
|
110
|
+
|
|
111
|
+
1. Step one
|
|
112
|
+
|
|
113
|
+
### PEND_B
|
|
114
|
+
|
|
115
|
+
**Service:** B
|
|
116
|
+
**Status:** pending
|
|
117
|
+
**Destination:** dotenv
|
|
118
|
+
|
|
119
|
+
1. Step one
|
|
120
|
+
|
|
121
|
+
### PEND_C
|
|
122
|
+
|
|
123
|
+
**Service:** C
|
|
124
|
+
**Status:** pending
|
|
125
|
+
**Destination:** dotenv
|
|
126
|
+
|
|
127
|
+
1. Step one
|
|
128
|
+
`);
|
|
129
|
+
|
|
130
|
+
const result = await getManifestStatus(tmp, 'M001');
|
|
131
|
+
assert.notStrictEqual(result, null);
|
|
132
|
+
assert.deepStrictEqual(result!.pending, ['PEND_A', 'PEND_B', 'PEND_C']);
|
|
133
|
+
assert.deepStrictEqual(result!.collected, []);
|
|
134
|
+
assert.deepStrictEqual(result!.skipped, []);
|
|
135
|
+
assert.deepStrictEqual(result!.existing, []);
|
|
136
|
+
} finally {
|
|
137
|
+
rmSync(tmp, { recursive: true, force: true });
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
// ─── All collected ───────────────────────────────────────────────────────────
|
|
142
|
+
|
|
143
|
+
test('getManifestStatus: all collected — 2 collected entries, none in env', async () => {
|
|
144
|
+
const tmp = makeTempDir('manifest-collected');
|
|
145
|
+
try {
|
|
146
|
+
delete process.env.COLL_X;
|
|
147
|
+
delete process.env.COLL_Y;
|
|
148
|
+
|
|
149
|
+
writeManifest(tmp, `# Secrets Manifest
|
|
150
|
+
|
|
151
|
+
**Milestone:** M001
|
|
152
|
+
**Generated:** 2025-06-20T10:00:00Z
|
|
153
|
+
|
|
154
|
+
### COLL_X
|
|
155
|
+
|
|
156
|
+
**Service:** X
|
|
157
|
+
**Status:** collected
|
|
158
|
+
**Destination:** dotenv
|
|
159
|
+
|
|
160
|
+
1. Done
|
|
161
|
+
|
|
162
|
+
### COLL_Y
|
|
163
|
+
|
|
164
|
+
**Service:** Y
|
|
165
|
+
**Status:** collected
|
|
166
|
+
**Destination:** dotenv
|
|
167
|
+
|
|
168
|
+
1. Done
|
|
169
|
+
`);
|
|
170
|
+
|
|
171
|
+
const result = await getManifestStatus(tmp, 'M001');
|
|
172
|
+
assert.notStrictEqual(result, null);
|
|
173
|
+
assert.deepStrictEqual(result!.pending, []);
|
|
174
|
+
assert.deepStrictEqual(result!.collected, ['COLL_X', 'COLL_Y']);
|
|
175
|
+
assert.deepStrictEqual(result!.skipped, []);
|
|
176
|
+
assert.deepStrictEqual(result!.existing, []);
|
|
177
|
+
} finally {
|
|
178
|
+
rmSync(tmp, { recursive: true, force: true });
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
// ─── Key in env overrides manifest status ────────────────────────────────────
|
|
183
|
+
|
|
184
|
+
test('getManifestStatus: key in env overrides manifest status — collected key in env goes to existing', async () => {
|
|
185
|
+
const tmp = makeTempDir('manifest-override');
|
|
186
|
+
const savedVal = process.env.GSD_TEST_OVERRIDE_KEY;
|
|
187
|
+
try {
|
|
188
|
+
process.env.GSD_TEST_OVERRIDE_KEY = 'already-here';
|
|
189
|
+
|
|
190
|
+
writeManifest(tmp, `# Secrets Manifest
|
|
191
|
+
|
|
192
|
+
**Milestone:** M001
|
|
193
|
+
**Generated:** 2025-06-20T10:00:00Z
|
|
194
|
+
|
|
195
|
+
### GSD_TEST_OVERRIDE_KEY
|
|
196
|
+
|
|
197
|
+
**Service:** Override
|
|
198
|
+
**Status:** collected
|
|
199
|
+
**Destination:** dotenv
|
|
200
|
+
|
|
201
|
+
1. Was collected but now in env
|
|
202
|
+
`);
|
|
203
|
+
|
|
204
|
+
const result = await getManifestStatus(tmp, 'M001');
|
|
205
|
+
assert.notStrictEqual(result, null);
|
|
206
|
+
assert.deepStrictEqual(result!.pending, []);
|
|
207
|
+
assert.deepStrictEqual(result!.collected, []);
|
|
208
|
+
assert.deepStrictEqual(result!.skipped, []);
|
|
209
|
+
assert.deepStrictEqual(result!.existing, ['GSD_TEST_OVERRIDE_KEY']);
|
|
210
|
+
} finally {
|
|
211
|
+
delete process.env.GSD_TEST_OVERRIDE_KEY;
|
|
212
|
+
if (savedVal !== undefined) process.env.GSD_TEST_OVERRIDE_KEY = savedVal;
|
|
213
|
+
rmSync(tmp, { recursive: true, force: true });
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
// ─── Missing manifest ────────────────────────────────────────────────────────
|
|
218
|
+
|
|
219
|
+
test('getManifestStatus: missing manifest — returns null', async () => {
|
|
220
|
+
const tmp = makeTempDir('manifest-missing');
|
|
221
|
+
try {
|
|
222
|
+
// No .gsd directory at all
|
|
223
|
+
const result = await getManifestStatus(tmp, 'M001');
|
|
224
|
+
assert.strictEqual(result, null);
|
|
225
|
+
} finally {
|
|
226
|
+
rmSync(tmp, { recursive: true, force: true });
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
// ─── Empty manifest (no entries) ─────────────────────────────────────────────
|
|
231
|
+
|
|
232
|
+
test('getManifestStatus: empty manifest — exists but no H3 sections', async () => {
|
|
233
|
+
const tmp = makeTempDir('manifest-empty');
|
|
234
|
+
try {
|
|
235
|
+
writeManifest(tmp, `# Secrets Manifest
|
|
236
|
+
|
|
237
|
+
**Milestone:** M001
|
|
238
|
+
**Generated:** 2025-06-20T10:00:00Z
|
|
239
|
+
`);
|
|
240
|
+
|
|
241
|
+
const result = await getManifestStatus(tmp, 'M001');
|
|
242
|
+
assert.notStrictEqual(result, null);
|
|
243
|
+
assert.deepStrictEqual(result!.pending, []);
|
|
244
|
+
assert.deepStrictEqual(result!.collected, []);
|
|
245
|
+
assert.deepStrictEqual(result!.skipped, []);
|
|
246
|
+
assert.deepStrictEqual(result!.existing, []);
|
|
247
|
+
} finally {
|
|
248
|
+
rmSync(tmp, { recursive: true, force: true });
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
// ─── Env via .env file (not just process.env) ────────────────────────────────
|
|
253
|
+
|
|
254
|
+
test('getManifestStatus: key in .env file counts as existing', async () => {
|
|
255
|
+
const tmp = makeTempDir('manifest-dotenv');
|
|
256
|
+
try {
|
|
257
|
+
delete process.env.DOTENV_ONLY_KEY;
|
|
258
|
+
|
|
259
|
+
writeManifest(tmp, `# Secrets Manifest
|
|
260
|
+
|
|
261
|
+
**Milestone:** M001
|
|
262
|
+
**Generated:** 2025-06-20T10:00:00Z
|
|
263
|
+
|
|
264
|
+
### DOTENV_ONLY_KEY
|
|
265
|
+
|
|
266
|
+
**Service:** DotenvService
|
|
267
|
+
**Status:** pending
|
|
268
|
+
**Destination:** dotenv
|
|
269
|
+
|
|
270
|
+
1. Get key
|
|
271
|
+
`);
|
|
272
|
+
|
|
273
|
+
// Write a .env file at the project root with the key
|
|
274
|
+
writeFileSync(join(tmp, '.env'), 'DOTENV_ONLY_KEY=from-dotenv-file\n');
|
|
275
|
+
|
|
276
|
+
const result = await getManifestStatus(tmp, 'M001');
|
|
277
|
+
assert.notStrictEqual(result, null);
|
|
278
|
+
assert.deepStrictEqual(result!.existing, ['DOTENV_ONLY_KEY']);
|
|
279
|
+
assert.deepStrictEqual(result!.pending, []);
|
|
280
|
+
} finally {
|
|
281
|
+
rmSync(tmp, { recursive: true, force: true });
|
|
282
|
+
}
|
|
283
|
+
});
|