create-anpunkit 2.0.0 → 2.0.3

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.3",
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.3",
3
3
  "files": [
4
4
  {
5
5
  "path": "AGENTS.md",
@@ -11,7 +11,7 @@
11
11
  },
12
12
  {
13
13
  "path": "README.md",
14
- "sha256": "56a76a3fefacbf5ffc37d5b1f43748a628e0e4a8142ce2d6058957f8a22781a0"
14
+ "sha256": "0335a0cecb86b57d01e63f564206cdf6d189ac873fce5c69d0f5081facfec964"
15
15
  },
16
16
  {
17
17
  "path": ".gitattributes",
@@ -19,7 +19,7 @@
19
19
  },
20
20
  {
21
21
  "path": "setup.sh",
22
- "sha256": "6e833255c4aeb5f7824db3fe332535f8f0816a0cbf79a0f8f16ee07b606753cf"
22
+ "sha256": "85c4aab58c83bf7b62344bbc04b94a4b49f90df0840b5e20660546b540889d6e"
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": "4b16cdfd4d9a22379147338664530fd7ce7f0e7566bf63d097f7b8f8f76dcdd8"
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
@@ -26,7 +26,7 @@ npx create-anpunkit # non-destructive: never clobbers your files
26
26
  /overview # bootstrap the project (includes Phase 0 infra)
27
27
  ```
28
28
  Useful flags: `--dry-run` (print the plan, write nothing), `--force` (overwrite
29
- user-modified kit files), `--kb-path <dir>` / `--kb-remote <url>` / `--no-kb`.
29
+ user-modified kit files), `--kb-path <dir>` (your pre-cloned KB repo; remote auto-recorded from its origin) / `--no-kb`.
30
30
 
31
31
  Upgrading an existing anpunkit project? Re-run `npx create-anpunkit`. It refreshes
32
32
  kit-owned files, preserves anything you modified as `<file>.anpunkit-new`, merges
@@ -157,14 +157,20 @@ re-discovering the same Azure/Databricks gotchas project after project.
157
157
  **Setup (one-time per machine):**
158
158
 
159
159
  ```bash
160
- # 1. Create a GitHub repo: anpunkit-kb (empty)
161
- # 2. Clone it somewhere permanent on your machine
162
- git clone git@github.com:your-username/anpunkit-kb.git ~/anpunkit-kb
160
+ # 1. Create a GitHub repo: anpunkit-kb (empty is fine)
161
+ # 2. Clone it yourself, somewhere permanent you own the git auth
162
+ git clone git@github.com:<you>/anpunkit-kb.git ~/anpunkit-kb
163
163
 
164
- # 3. During setup.sh, provide the path when prompted
165
- bash setup.sh
164
+ # 3. Point anpunkit at it
165
+ npx create-anpunkit --kb-path ~/anpunkit-kb
166
+ # (or paste the path at the interactive prompt)
166
167
  ```
167
168
 
169
+ That's the last manual git you'll run against the KB. From then on the
170
+ **session-start hook pulls** each session and **`/store-wisdom` commits and
171
+ pushes** (always with your approval). The remote URL is recorded automatically
172
+ from the clone's `origin`; `--kb-remote <url>` overrides it if needed.
173
+
168
174
  **Usage:**
169
175
 
170
176
  ```
@@ -17,6 +17,27 @@
17
17
 
18
18
  Newest first. One entry per architectural change. Appended by `/log-decision`.
19
19
 
20
+ - **(v2.0.2)** — KB setup stays PATH-FIRST: the user clones the KB repo
21
+ themselves (`git clone ... ~/anpunkit-kb`) and passes `--kb-path` (or the
22
+ interactive prompt). New: the remote URL is auto-recorded from the clone's
23
+ `origin`, so `--kb-remote` is optional metadata. After setup, no manual git:
24
+ the session-start hook pulls; `/store-wisdom` commits and pushes (human-gated).
25
+ REJECTED: URL-first setup where anpunkit runs `git clone` itself — it was built
26
+ and tested, then dropped: it entangles a fresh install with the user's GitHub
27
+ authentication (SSH keys / gh auth), and a clone failure mid-setup is a worse
28
+ failure mode than asking a developer to run one clone command they already
29
+ understand. Setup records state; it does not acquire credentials.
30
+
31
+ - **(v2.0.1)** — Windows portability fix for the npx installer. (1) `cli.js` no
32
+ longer trusts bare `bash` on win32: PowerShell resolves it to the System32 WSL
33
+ relay, which fails with `execvpe(/bin/bash)` when no distro is installed. The
34
+ bin now locates Git Bash explicitly (ANPUNKIT_BASH override -> derive from
35
+ `where git` -> standard install paths), refuses System32, and fails loudly with
36
+ install guidance. (2) `setup.sh` and `cursor-session-start.sh` no longer depend
37
+ on `python3` (absent or a Store stub on many Windows machines): all JSON and
38
+ checksum work now uses Node, which the kit already requires. Paths passed into
39
+ Git Bash are normalized to forward slashes.
40
+
20
41
  - **(v2.0)** — Major release. Project name: **anpunkit**; distributed
21
42
  as `npx create-anpunkit`. Five locked features: (1) **TDD-first** via a SCAFFOLD
22
43
  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
 
@@ -206,23 +207,41 @@ say ""; say "[6/6] shared KB:"
206
207
  configure_kb() {
207
208
  local cfg="$DEST/.claude/kb-config.json"
208
209
  if [ "$NO_KB" = 1 ]; then say " --no-kb: skipping KB."; return 0; fi
209
- if [ -z "$KB_PATH" ] && [ -z "$KB_REMOTE" ]; then
210
+
211
+ # Path-first by design: YOU clone the KB repo (you own the git auth);
212
+ # anpunkit only records where it lives. After that, the session-start hook
213
+ # pulls and /store-wisdom commits+pushes (human-gated) — no manual git needed.
214
+ if [ -z "$KB_PATH" ]; then
210
215
  if [ -t 0 ]; then
211
- printf " Local path to cloned anpunkit-kb repo (blank to skip): "; read -r KB_PATH || true
212
- [ -z "$KB_PATH" ] && { say " skipped KB (no path given)."; return 0; }
216
+ printf " Local path to your cloned anpunkit-kb repo (blank to skip): "
217
+ read -r KB_PATH || true
218
+ [ -z "$KB_PATH" ] && { say " skipped KB."; return 0; }
213
219
  else
214
- say " no KB flags + no TTY -> skipping KB (re-run with --kb-path / --kb-remote)."; return 0
220
+ say " no KB flags + no TTY -> skipping KB (re-run with --kb-path <dir>)."; return 0
215
221
  fi
216
222
  fi
217
- if [ -n "$KB_PATH" ] && [ ! -d "$KB_PATH" ]; then
218
- echo " ! KB path '$KB_PATH' not a directory. Clone your anpunkit-kb repo first."; return 0; fi
223
+ if [ ! -d "$KB_PATH" ]; then
224
+ echo " ! KB path '$KB_PATH' is not a directory. Clone your KB repo first, e.g.:"
225
+ echo " git clone git@github.com:<you>/anpunkit-kb.git ~/anpunkit-kb"
226
+ echo " then re-run with --kb-path ~/anpunkit-kb"
227
+ return 0
228
+ fi
229
+
230
+ # remote is metadata: explicit --kb-remote wins, else read the clone's origin
231
+ if [ -z "$KB_REMOTE" ] && [ -d "$KB_PATH/.git" ]; then
232
+ KB_REMOTE=$(git -C "$KB_PATH" remote get-url origin 2>/dev/null || echo "")
233
+ fi
219
234
  if [ -n "$KB_REMOTE" ]; then
220
- git ls-remote "$KB_REMOTE" >/dev/null 2>&1 || say " ! warning: cannot reach KB remote '$KB_REMOTE' (configuring anyway)."; fi
221
- [ "$DRY" = 1 ] && { act "write .claude/kb-config.json"; return 0; }
235
+ git ls-remote "$KB_REMOTE" >/dev/null 2>&1 \
236
+ || say " ! warning: cannot reach KB remote '$KB_REMOTE' right now (recording anyway; pull/push will need it)."
237
+ fi
238
+
239
+ [ "$DRY" = 1 ] && { act "write .claude/kb-config.json (path: $KB_PATH)"; return 0; }
222
240
  mkdir -p "$DEST/.claude"
223
- python3 -c "import json;json.dump({'path':'$KB_PATH','remote':'$KB_REMOTE'},open('$cfg','w'),indent=2)"
224
- say " wrote .claude/kb-config.json"
241
+ 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"
242
+ say " wrote .claude/kb-config.json (path: $KB_PATH${KB_REMOTE:+, remote: $KB_REMOTE})"
225
243
  }
244
+
226
245
  configure_kb
227
246
 
228
247
  # ---- finalize: stamp the installed manifest (copy the new one)