create-anpunkit 2.0.0 → 2.0.1

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.js CHANGED
@@ -16,10 +16,56 @@ if (!fs.existsSync(SETUP)) {
16
16
  process.exit(1);
17
17
  }
18
18
 
19
+ // --- locate a REAL bash on Windows (never the System32 WSL relay).
20
+ // In PowerShell, bare `bash` resolves to C:\Windows\System32\bash.exe, which
21
+ // relays into WSL and fails with "execvpe(/bin/bash) failed" when no distro
22
+ // is installed. We must find Git Bash explicitly.
23
+ function findBash() {
24
+ if (process.platform !== 'win32') return 'bash';
25
+ const candidates = [];
26
+ if (process.env.ANPUNKIT_BASH) candidates.push(process.env.ANPUNKIT_BASH);
27
+ // Derive from git on PATH: <Git>\cmd\git.exe or <Git>\mingw64\bin\git.exe -> <Git>\bin\bash.exe
28
+ try {
29
+ const r = spawnSync('where', ['git'], { encoding: 'utf8' });
30
+ if (r.status === 0) {
31
+ for (const line of r.stdout.split(/\r?\n/).map(s => s.trim()).filter(Boolean)) {
32
+ const d1 = path.resolve(path.dirname(line), '..');
33
+ const d2 = path.resolve(d1, '..');
34
+ candidates.push(path.join(d1, 'bin', 'bash.exe'), path.join(d2, 'bin', 'bash.exe'));
35
+ }
36
+ }
37
+ } catch (_) { /* where.exe missing — fall through */ }
38
+ // Standard Git for Windows install locations
39
+ for (const base of [
40
+ process.env.ProgramFiles,
41
+ process.env['ProgramFiles(x86)'],
42
+ process.env.LocalAppData ? path.join(process.env.LocalAppData, 'Programs') : null
43
+ ]) {
44
+ if (base) candidates.push(path.join(base, 'Git', 'bin', 'bash.exe'));
45
+ }
46
+ for (const c of candidates) {
47
+ if (c && !/system32/i.test(c) && fs.existsSync(c)) return c;
48
+ }
49
+ return null;
50
+ }
51
+
52
+ const bash = findBash();
53
+ if (!bash) {
54
+ console.error('create-anpunkit: could not find Git Bash.');
55
+ console.error('The Windows `bash` on PATH is the WSL relay (System32), which is not usable here.');
56
+ console.error('Fix one of:');
57
+ console.error(' 1. Install Git for Windows (includes Git Bash): https://git-scm.com/download/win');
58
+ console.error(' 2. Run `npx create-anpunkit` from a Git Bash terminal instead of PowerShell.');
59
+ console.error(' 3. Set ANPUNKIT_BASH to the full path of a bash.exe.');
60
+ process.exit(1);
61
+ }
62
+
19
63
  // Pass through recognised flags only; setup.sh validates the rest.
20
64
  const passthrough = ['--kb-path', '--kb-remote', '--no-kb', '--force', '--dry-run'];
21
65
  const argv = process.argv.slice(2);
22
- const args = ['--src', TEMPLATE];
66
+ // Git Bash is happiest with forward slashes; Windows APIs accept them too.
67
+ const fwd = p => p.split(path.sep).join('/');
68
+ const args = ['--src', fwd(TEMPLATE)];
23
69
  for (let i = 0; i < argv.length; i++) {
24
70
  const a = argv[i];
25
71
  if (passthrough.includes(a)) {
@@ -31,11 +77,10 @@ for (let i = 0; i < argv.length; i++) {
31
77
  }
32
78
  }
33
79
 
34
- const bash = process.platform === 'win32' ? 'bash' : 'bash'; // Git Bash / WSL on Windows
35
- console.log(`create-anpunkit -> running setup.sh (src: ${TEMPLATE})`);
36
- const r = spawnSync(bash, [SETUP, ...args], { stdio: 'inherit', cwd: process.cwd() });
80
+ console.log(`create-anpunkit -> running setup.sh (bash: ${bash})`);
81
+ const r = spawnSync(bash, [fwd(SETUP), ...args], { stdio: 'inherit', cwd: process.cwd() });
37
82
  if (r.error) {
38
- console.error('create-anpunkit: could not run bash. Install Git Bash (Windows) or ensure bash is on PATH.');
83
+ console.error(`create-anpunkit: failed to run ${bash}: ${r.error.message}`);
39
84
  process.exit(1);
40
85
  }
41
86
  process.exit(r.status === null ? 1 : r.status);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-anpunkit",
3
- "version": "2.0.0",
3
+ "version": "2.0.1",
4
4
  "description": "Scaffold the anpunkit AI-coding workflow (Claude Code + Cursor) into a project. Upgrade-safe, non-destructive.",
5
5
  "bin": {
6
6
  "create-anpunkit": "bin/cli.js"
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "2.0.0",
2
+ "version": "2.0.1",
3
3
  "files": [
4
4
  {
5
5
  "path": "AGENTS.md",
@@ -19,7 +19,7 @@
19
19
  },
20
20
  {
21
21
  "path": "setup.sh",
22
- "sha256": "6e833255c4aeb5f7824db3fe332535f8f0816a0cbf79a0f8f16ee07b606753cf"
22
+ "sha256": "b6b2f2adbff866f8ed0398006ab2eefb346d9a81b50dc9bccbb3a8a22f6a1b04"
23
23
  },
24
24
  {
25
25
  "path": ".claude/agents/debugger.md",
@@ -55,7 +55,7 @@
55
55
  },
56
56
  {
57
57
  "path": ".claude/hooks/cursor-session-start.sh",
58
- "sha256": "504aa12b63508c6926cda28da14fa8951234d49d4b4318da83eecd6f115402e3"
58
+ "sha256": "e08bcd14fbcfe1831b64e7d8f181b57b02c561cd8d0de6eb16fe06db259be6af"
59
59
  },
60
60
  {
61
61
  "path": ".claude/hooks/pre-compact.sh",
@@ -231,11 +231,11 @@
231
231
  },
232
232
  {
233
233
  "path": "create-anpunkit/package.json",
234
- "sha256": "a0cdb99a170ef7f4dc9f0aecc85e8d4c8fde3038d2b176366b17078deee09d09"
234
+ "sha256": "3fa34141b9698d4ceb6a17d036e86d115de8cd5371c63df3f25705fe2b518d14"
235
235
  },
236
236
  {
237
237
  "path": "create-anpunkit/bin/cli.js",
238
- "sha256": "3c12a18a3831c40ddb3633072a26e5f8be28954fe8d058388d7220d13cf4568e"
238
+ "sha256": "129dae04923b6d380d3ca4601b5dcd2baa6ad4c51c827dddae3e43ea6a9c50e2"
239
239
  },
240
240
  {
241
241
  "path": "create-anpunkit/build.sh",
@@ -5,10 +5,13 @@
5
5
  # session-start.sh body (single copy — anti-drift) and wraps its output in the
6
6
  # JSON envelope Cursor requires. Cursor sets CLAUDE_PROJECT_DIR (compat alias),
7
7
  # so the shared script needs no changes.
8
+ # Uses node for JSON (the kit assumes Node; python3 is a fallback only).
8
9
  # Verified against cursor.com/docs/hooks (sessionStart output schema), 2026-06.
9
10
  set -euo pipefail
10
11
  HERE="$(cd "$(dirname "$0")" && pwd)"
11
12
  OUT="$(bash "$HERE/session-start.sh" 2>/dev/null || true)"
12
- python3 -c 'import json,sys; print(json.dumps({"additional_context": sys.stdin.read()}))' <<EOF_CTX
13
- $OUT
14
- EOF_CTX
13
+ if command -v node >/dev/null 2>&1; then
14
+ printf '%s' "$OUT" | node -e 'let d="";process.stdin.on("data",c=>d+=c).on("end",()=>console.log(JSON.stringify({additional_context:d})))'
15
+ else
16
+ printf '%s' "$OUT" | python3 -c 'import json,sys; print(json.dumps({"additional_context": sys.stdin.read()}))'
17
+ fi
@@ -17,6 +17,16 @@
17
17
 
18
18
  Newest first. One entry per architectural change. Appended by `/log-decision`.
19
19
 
20
+ - **(v2.0.1)** — Windows portability fix for the npx installer. (1) `cli.js` no
21
+ longer trusts bare `bash` on win32: PowerShell resolves it to the System32 WSL
22
+ relay, which fails with `execvpe(/bin/bash)` when no distro is installed. The
23
+ bin now locates Git Bash explicitly (ANPUNKIT_BASH override -> derive from
24
+ `where git` -> standard install paths), refuses System32, and fails loudly with
25
+ install guidance. (2) `setup.sh` and `cursor-session-start.sh` no longer depend
26
+ on `python3` (absent or a Store stub on many Windows machines): all JSON and
27
+ checksum work now uses Node, which the kit already requires. Paths passed into
28
+ Git Bash are normalized to forward slashes.
29
+
20
30
  - **(v2.0)** — Major release. Project name: **anpunkit**; distributed
21
31
  as `npx create-anpunkit`. Five locked features: (1) **TDD-first** via a SCAFFOLD
22
32
  step (implementer writes stubs only; test-author writes the suite blind against
package/template/setup.sh CHANGED
@@ -39,7 +39,7 @@ act() { if [ "$DRY" = 1 ]; then say " [dry-run] $*"; else say " $*"; fi; }
39
39
  sha() {
40
40
  if command -v sha256sum >/dev/null 2>&1; then sha256sum "$1" | cut -d' ' -f1
41
41
  elif command -v shasum >/dev/null 2>&1; then shasum -a 256 "$1" | cut -d' ' -f1
42
- else python3 -c "import hashlib,sys;print(hashlib.sha256(open(sys.argv[1],'rb').read()).hexdigest())" "$1"; fi
42
+ else node -e "console.log(require('crypto').createHash('sha256').update(require('fs').readFileSync(process.argv[1])).digest('hex'))" "$1"; fi
43
43
  }
44
44
 
45
45
  say "anpunkit setup — dest: $DEST src: $SRC $([ "$DRY" = 1 ] && echo '(DRY-RUN)')"
@@ -47,12 +47,12 @@ say "anpunkit setup — dest: $DEST src: $SRC $([ "$DRY" = 1 ] && echo '(DRY-R
47
47
  MAN_SRC="$SRC/.claude/anpunkit-manifest.json"
48
48
  MAN_DEST="$DEST/.claude/anpunkit-manifest.json"
49
49
  [ -f "$MAN_SRC" ] || { echo "FATAL: missing $MAN_SRC (corrupt kit)"; exit 1; }
50
- NEW_VER=$(python3 -c "import json;print(json.load(open('$MAN_SRC'))['version'])")
50
+ NEW_VER=$(node -e "console.log(JSON.parse(require('fs').readFileSync(process.argv[1],'utf8')).version)" "$MAN_SRC")
51
51
 
52
52
  # ---- determine mode from installed manifest
53
53
  MODE="fresh"
54
54
  if [ -f "$MAN_DEST" ]; then
55
- OLD_VER=$(python3 -c "import json;print(json.load(open('$MAN_DEST')).get('version',''))" 2>/dev/null || echo "")
55
+ OLD_VER=$(node -e "try{console.log(JSON.parse(require('fs').readFileSync(process.argv[1],'utf8')).version||'')}catch(e){console.log('')}" "$MAN_DEST" 2>/dev/null || echo "")
56
56
  if [ "$OLD_VER" = "$NEW_VER" ]; then MODE="repair"
57
57
  elif [ -z "$OLD_VER" ]; then MODE="upgrade"
58
58
  else
@@ -82,19 +82,23 @@ install_kit_file() {
82
82
  mkdir -p "$(dirname "$d")"; cp -p "$s" "$d"; return 0
83
83
  fi
84
84
  # present — compare against the INSTALLED manifest's recorded checksum
85
- local recorded; recorded=$(python3 - "$MAN_DEST" "$rel" <<'PY' 2>/dev/null || true
86
- import json,sys
87
- try:
88
- m=json.load(open(sys.argv[1]))
89
- print(next((f["sha256"] for f in m["files"] if f["path"]==sys.argv[2]),""))
90
- except Exception: print("")
91
- PY
85
+ local recorded; recorded=$(node - "$MAN_DEST" "$rel" <<'NODE' 2>/dev/null || true
86
+ const fs=require('fs');
87
+ try{
88
+ const m=JSON.parse(fs.readFileSync(process.argv[2],'utf8'));
89
+ const f=(m.files||[]).find(f=>f.path===process.argv[3]);
90
+ console.log(f?f.sha256:'');
91
+ }catch(e){console.log('');}
92
+ NODE
92
93
  )
93
94
  local cur; cur=$(sha "$d")
94
- if [ "$cur" = "$recorded" ] || [ -z "$recorded" ] && [ "$SRC" = "." ]; then
95
- # unmodified since last install (or clone flow with file already in place) -> refresh
96
- if [ "$cur" = "$(sha "$s")" ]; then act "ok $rel (identical)"; else
97
- backup_one "$d"; act "refresh $rel"; [ "$DRY" = 1 ] || cp -p "$s" "$d"; fi
95
+ local new; new=$(sha "$s")
96
+ # already identical to the new version -> nothing to do (regardless of manifest)
97
+ if [ "$cur" = "$new" ]; then act "ok $rel (identical)"; return 0; fi
98
+ # NOTE: bash gives || and && EQUAL precedence (left-assoc) group explicitly.
99
+ if [ "$cur" = "$recorded" ] || { [ -z "$recorded" ] && [ "$SRC" = "." ]; }; then
100
+ # unmodified since last install (or clone flow) -> refresh to the new version
101
+ backup_one "$d"; act "refresh $rel"; [ "$DRY" = 1 ] || cp -p "$s" "$d"
98
102
  else
99
103
  # user-modified kit file
100
104
  if [ "$FORCE" = 1 ]; then backup_one "$d"; act "force $rel (overwritten)"; [ "$DRY" = 1 ] || cp -p "$s" "$d"
@@ -104,7 +108,7 @@ PY
104
108
 
105
109
  say ""; say "[1/6] kit-owned files (taxonomy):"
106
110
  if [ "$SRC" != "." ]; then
107
- while IFS= read -r rel; do install_kit_file "$rel"; done < <(python3 -c "import json;[print(f['path']) for f in json.load(open('$MAN_SRC'))['files']]")
111
+ while IFS= read -r rel; do install_kit_file "$rel"; done < <(node -e "JSON.parse(require('fs').readFileSync(process.argv[1],'utf8')).files.forEach(f=>console.log(f.path))" "$MAN_SRC")
108
112
  else
109
113
  say " clone flow (src=.) — kit files already in place; verifying + generating only."
110
114
  fi
@@ -131,44 +135,41 @@ say ""; say "[3/6] wire hooks (idempotent merge):"
131
135
  merge_settings() {
132
136
  [ "$DRY" = 1 ] && { act "merge .claude/settings.json + .cursor/hooks.json"; return 0; }
133
137
  backup_one "$DEST/.claude/settings.json"; backup_one "$DEST/.cursor/hooks.json"
134
- python3 - "$DEST" <<'PY'
135
- import json,os,sys
136
- dest=sys.argv[1]
137
- def load(p,default):
138
- try: return json.load(open(p))
139
- except Exception: return default
140
- # Claude settings.json
141
- sp=os.path.join(dest,".claude/settings.json")
142
- s=load(sp,{})
143
- s.setdefault("$schema","https://json.schemastore.org/claude-code-settings.json")
144
- hooks=s.setdefault("hooks",{})
145
- def ensure(event,matcher,cmd):
146
- arr=hooks.setdefault(event,[])
147
- for blk in arr:
148
- for h in blk.get("hooks",[]):
149
- if h.get("command")==cmd: return
150
- blk={"hooks":[{"type":"command","command":cmd}]}
151
- if matcher: blk={"matcher":matcher,**blk}
152
- arr.append(blk)
153
- ensure("SessionStart","startup|clear|compact","bash .claude/hooks/session-start.sh")
154
- ensure("PreCompact","auto|manual","bash .claude/hooks/pre-compact.sh")
155
- ensure("SubagentStop","","bash .claude/hooks/subagent-stop.sh")
156
- os.makedirs(os.path.dirname(sp),exist_ok=True)
157
- json.dump(s,open(sp,"w"),indent=2); open(sp,"a").write("\n")
158
- # Cursor hooks.json
159
- cp=os.path.join(dest,".cursor/hooks.json")
160
- c=load(cp,{"version":1,"hooks":{}})
161
- ch=c.setdefault("hooks",{})
162
- def cursor_ensure(event,cmd):
163
- arr=ch.setdefault(event,[])
164
- if not any(h.get("command")==cmd for h in arr): arr.append({"command":cmd})
165
- cursor_ensure("sessionStart","bash .claude/hooks/cursor-session-start.sh")
166
- cursor_ensure("preCompact","bash .claude/hooks/pre-compact.sh")
167
- cursor_ensure("subagentStop","bash .claude/hooks/subagent-stop.sh")
168
- os.makedirs(os.path.dirname(cp),exist_ok=True)
169
- json.dump(c,open(cp,"w"),indent=2); open(cp,"a").write("\n")
170
- print(" merged .claude/settings.json + .cursor/hooks.json")
171
- PY
138
+ node - "$DEST" <<'NODE'
139
+ const fs=require('fs'),path=require('path');
140
+ const dest=process.argv[2];
141
+ const load=(p,d)=>{try{return JSON.parse(fs.readFileSync(p,'utf8'))}catch(e){return d}};
142
+ const save=(p,o)=>{fs.mkdirSync(path.dirname(p),{recursive:true});fs.writeFileSync(p,JSON.stringify(o,null,2)+"\n")};
143
+ // Claude settings.json
144
+ const sp=path.join(dest,'.claude/settings.json');
145
+ const s=load(sp,{});
146
+ if(!s.$schema)s.$schema='https://json.schemastore.org/claude-code-settings.json';
147
+ s.hooks=s.hooks||{};
148
+ const ensure=(event,matcher,cmd)=>{
149
+ const arr=s.hooks[event]=s.hooks[event]||[];
150
+ for(const blk of arr)for(const h of (blk.hooks||[]))if(h.command===cmd)return;
151
+ const blk={hooks:[{type:'command',command:cmd}]};
152
+ if(matcher)blk.matcher=matcher;
153
+ arr.push(blk);
154
+ };
155
+ ensure('SessionStart','startup|clear|compact','bash .claude/hooks/session-start.sh');
156
+ ensure('PreCompact','auto|manual','bash .claude/hooks/pre-compact.sh');
157
+ ensure('SubagentStop','','bash .claude/hooks/subagent-stop.sh');
158
+ save(sp,s);
159
+ // Cursor hooks.json
160
+ const cp=path.join(dest,'.cursor/hooks.json');
161
+ const c=load(cp,{version:1,hooks:{}});
162
+ c.hooks=c.hooks||{};
163
+ const cursorEnsure=(event,cmd)=>{
164
+ const arr=c.hooks[event]=c.hooks[event]||[];
165
+ if(!arr.some(h=>h.command===cmd))arr.push({command:cmd});
166
+ };
167
+ cursorEnsure('sessionStart','bash .claude/hooks/cursor-session-start.sh');
168
+ cursorEnsure('preCompact','bash .claude/hooks/pre-compact.sh');
169
+ cursorEnsure('subagentStop','bash .claude/hooks/subagent-stop.sh');
170
+ save(cp,c);
171
+ console.log(' merged .claude/settings.json + .cursor/hooks.json');
172
+ NODE
172
173
  }
173
174
  merge_settings
174
175
 
@@ -220,7 +221,7 @@ configure_kb() {
220
221
  git ls-remote "$KB_REMOTE" >/dev/null 2>&1 || say " ! warning: cannot reach KB remote '$KB_REMOTE' (configuring anyway)."; fi
221
222
  [ "$DRY" = 1 ] && { act "write .claude/kb-config.json"; return 0; }
222
223
  mkdir -p "$DEST/.claude"
223
- python3 -c "import json;json.dump({'path':'$KB_PATH','remote':'$KB_REMOTE'},open('$cfg','w'),indent=2)"
224
+ node -e "require('fs').writeFileSync(process.argv[1],JSON.stringify({path:process.argv[2],remote:process.argv[3]},null,2)+'\n')" "$cfg" "$KB_PATH" "$KB_REMOTE"
224
225
  say " wrote .claude/kb-config.json"
225
226
  }
226
227
  configure_kb