@warnyin/sdlc 0.1.0 → 0.1.2

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/bin/cli.mjs CHANGED
@@ -465,6 +465,18 @@ export async function main(argv = process.argv.slice(2), projectRoot = process.c
465
465
  }
466
466
  }
467
467
 
468
- if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
468
+ // npx invokes the bin via a node_modules/.bin symlink, so argv[1] must be
469
+ // realpath-resolved before comparing with import.meta.url (which the ESM
470
+ // loader already resolves) — otherwise main() silently never runs.
471
+ function isEntrypoint() {
472
+ if (!process.argv[1]) return false;
473
+ try {
474
+ return fs.realpathSync.native(path.resolve(process.argv[1])) === fileURLToPath(import.meta.url);
475
+ } catch {
476
+ return false;
477
+ }
478
+ }
479
+
480
+ if (isEntrypoint()) {
469
481
  main();
470
482
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@warnyin/sdlc",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Spec-driven, AI-driven SDLC framework — token-lean specs, contract-first changes, autonomous pipeline with managed hooks. Operationalizes the Day-1 'New SDLC with Vibe Coding' work process.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -16,7 +16,7 @@
16
16
  "LICENSE"
17
17
  ],
18
18
  "scripts": {
19
- "test": "node --test \"tests/*.test.mjs\"",
19
+ "test": "node --test",
20
20
  "setup:dogfood": "node bin/cli.mjs init --tool claude && node bin/cli.mjs update"
21
21
  },
22
22
  "engines": {
@@ -15,14 +15,27 @@ export function resolveRoots(importMetaUrl) {
15
15
  return { hooksDir, sdlcRoot, projectRoot };
16
16
  }
17
17
 
18
- export async function readStdinJson() {
19
- try {
18
+ // Reads the hook payload from stdin. Must NEVER hang: when a playbook or a
19
+ // user script invokes a hook utility with stdin open-but-idle (no piped JSON),
20
+ // resolve null after a short grace period instead of blocking forever.
21
+ export function readStdinJson({ timeoutMs = 1000 } = {}) {
22
+ return new Promise((resolve) => {
20
23
  let data = '';
21
- for await (const chunk of process.stdin) data += chunk;
22
- return data.trim() ? JSON.parse(data) : null;
23
- } catch {
24
- return null;
25
- }
24
+ let done = false;
25
+ const finish = () => {
26
+ if (done) return;
27
+ done = true;
28
+ // Release stdin so an open-idle stream cannot keep the event loop alive.
29
+ process.stdin.pause();
30
+ if (typeof process.stdin.unref === 'function') process.stdin.unref();
31
+ try { resolve(data.trim() ? JSON.parse(data) : null); } catch { resolve(null); }
32
+ };
33
+ const timer = setTimeout(finish, timeoutMs);
34
+ if (typeof timer.unref === 'function') timer.unref();
35
+ process.stdin.on('data', (chunk) => { data += chunk; });
36
+ process.stdin.on('end', finish);
37
+ process.stdin.on('error', finish);
38
+ });
26
39
  }
27
40
 
28
41
  // Resolve symlinks on the deepest EXISTING ancestor, then re-attach the tail.
@@ -33,6 +33,15 @@ function main() {
33
33
  process.exit(errors > 0 ? 1 : 0);
34
34
  }
35
35
 
36
- if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
36
+ function isEntrypoint() {
37
+ if (!process.argv[1]) return false;
38
+ try {
39
+ return fs.realpathSync.native(path.resolve(process.argv[1])) === fileURLToPath(import.meta.url);
40
+ } catch {
41
+ return false;
42
+ }
43
+ }
44
+
45
+ if (isEntrypoint()) {
37
46
  main();
38
47
  }