create-anpunkit 2.0.1 → 2.1.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/template/setup.sh CHANGED
@@ -12,6 +12,8 @@
12
12
  # hybrid -> merged idempotently, never replaced
13
13
  #
14
14
  # Flags: --src DIR --kb-path P --kb-remote URL --no-kb --force --dry-run
15
+ # --tools LIST (comma/space list from: claude cursor; default both)
16
+ # --add-tool T (additive: install T's files into an existing project)
15
17
  set -euo pipefail
16
18
 
17
19
  SRC="."
@@ -20,6 +22,9 @@ KB_REMOTE="${ANPUNKIT_KB_REMOTE:-}"
20
22
  NO_KB=0
21
23
  FORCE=0
22
24
  DRY=0
25
+ TOOLS_FLAG=""
26
+ ADD_TOOL=""
27
+ AVAILABLE_TOOLS="claude cursor" # tools with real adapters; extend here
23
28
  while [ $# -gt 0 ]; do
24
29
  case "$1" in
25
30
  --src) SRC="$2"; shift 2;;
@@ -28,6 +33,8 @@ while [ $# -gt 0 ]; do
28
33
  --no-kb) NO_KB=1; shift;;
29
34
  --force) FORCE=1; shift;;
30
35
  --dry-run) DRY=1; shift;;
36
+ --tools) TOOLS_FLAG="$2"; shift 2;;
37
+ --add-tool) ADD_TOOL="$2"; shift 2;;
31
38
  *) echo "unknown flag: $1" >&2; exit 2;;
32
39
  esac
33
40
  done
@@ -63,6 +70,36 @@ fi
63
70
  say "mode: $MODE (installed: ${OLD_VER:-none} -> new: $NEW_VER)"
64
71
  [ "$MODE" = "newer-installed" ] && say " ! installed version is newer than this package — proceeding as repair, review carefully."
65
72
 
73
+ # ---- tool selection (v2.1): install only the selected tools' adapters/hooks/files
74
+ TOOLS_FILE="$DEST/.claude/anpunkit-tools.json"
75
+ norm_tools() { printf '%s' "$1" | tr ',' ' ' | tr -s ' ' | sed 's/^ //; s/ $//'; }
76
+ read_recorded_tools() {
77
+ [ -f "$TOOLS_FILE" ] || { printf ''; return; }
78
+ node -e "try{console.log((JSON.parse(require('fs').readFileSync(process.argv[1],'utf8')).tools||[]).join(' '))}catch(e){console.log('')}" "$TOOLS_FILE" 2>/dev/null || printf ''
79
+ }
80
+ valid_tool() { case " $AVAILABLE_TOOLS " in *" $1 "*) return 0;; *) return 1;; esac; }
81
+
82
+ RECORDED="$(read_recorded_tools)"
83
+ if [ -n "$TOOLS_FLAG" ]; then
84
+ TOOLS="$(norm_tools "$TOOLS_FLAG")"
85
+ elif [ -n "$RECORDED" ]; then
86
+ TOOLS="$RECORDED" # re-run: preserve prior selection
87
+ elif [ "$MODE" = "fresh" ] && [ -t 0 ]; then
88
+ printf " Select tools to install (space-separated from: %s) [claude cursor]: " "$AVAILABLE_TOOLS"
89
+ read -r _sel || true
90
+ TOOLS="$(norm_tools "${_sel:-claude cursor}")"
91
+ else
92
+ TOOLS="claude cursor" # non-interactive default = both (no regression)
93
+ fi
94
+ # additive: --add-tool unions into the selection (and is recorded)
95
+ [ -n "$ADD_TOOL" ] && TOOLS="$(norm_tools "$TOOLS $(norm_tools "$ADD_TOOL")" | tr ' ' '\n' | sort -u | tr '\n' ' ')"
96
+ TOOLS="$(norm_tools "$TOOLS")"
97
+ # validate
98
+ for t in $TOOLS; do valid_tool "$t" || { echo "FATAL: unknown tool '$t' (available: $AVAILABLE_TOOLS)"; exit 2; }; done
99
+ [ -z "$TOOLS" ] && { echo "FATAL: no tools selected"; exit 2; }
100
+ tool_selected() { case " $TOOLS " in *" $1 "*) return 0;; *) return 1;; esac; }
101
+ say "tools: $TOOLS$([ -n "$ADD_TOOL" ] && echo ' (+'"$ADD_TOOL"' added)')"
102
+
66
103
  BACKUP="$DEST/.anpunkit-backup-$(date +%Y%m%d-%H%M%S)"
67
104
  backup_one() {
68
105
  [ "$MODE" = "fresh" ] && return 0
@@ -77,6 +114,11 @@ backup_one() {
77
114
  install_kit_file() {
78
115
  local rel="$1" s="$SRC/$1" d="$DEST/$1"
79
116
  [ -f "$s" ] || return 0
117
+ # v2.1 tool gating: cursor-only files skipped unless cursor selected.
118
+ # (.claude/* is shared infra — Cursor reads .claude/agents + .claude/hooks too.)
119
+ case "$rel" in
120
+ .cursor/*) tool_selected cursor || { act "skip $rel (cursor not selected)"; return 0; };;
121
+ esac
80
122
  if [ ! -f "$d" ]; then
81
123
  act "write $rel"; [ "$DRY" = 1 ] && return 0
82
124
  mkdir -p "$(dirname "$d")"; cp -p "$s" "$d"; return 0
@@ -114,61 +156,76 @@ else
114
156
  fi
115
157
 
116
158
  # ---- generate command adapters from commands.src/ (manifest-owned output)
117
- say ""; say "[2/6] generate command adapters:"
159
+ say ""; say "[2/6] generate command adapters (tools: $TOOLS):"
118
160
  gen_adapters() {
119
- mkdir -p "$DEST/.claude/commands" "$DEST/.cursor/commands" "$DEST/.cursor/rules"
161
+ tool_selected claude && mkdir -p "$DEST/.claude/commands"
162
+ tool_selected cursor && mkdir -p "$DEST/.cursor/commands" "$DEST/.cursor/rules"
120
163
  for f in "$DEST"/commands.src/*.md; do
121
164
  [ -f "$f" ] || continue
122
165
  n=$(basename "$f")
123
- act "claude .claude/commands/$n"
124
- act "cursor .cursor/commands/$n"
125
- [ "$DRY" = 1 ] && continue
126
- cp -p "$f" "$DEST/.claude/commands/$n"
127
- awk 'BEGIN{fm=0} NR==1 && $0=="---"{fm=1; next} fm==1 && $0=="---"{fm=0; next} fm==0{print}' "$f" \
128
- | sed '/^$/{1d}' > "$DEST/.cursor/commands/$n"
166
+ if tool_selected claude; then
167
+ act "claude .claude/commands/$n"
168
+ [ "$DRY" = 1 ] || cp -p "$f" "$DEST/.claude/commands/$n"
169
+ fi
170
+ if tool_selected cursor; then
171
+ act "cursor .cursor/commands/$n"
172
+ [ "$DRY" = 1 ] || awk 'BEGIN{fm=0} NR==1 && $0=="---"{fm=1; next} fm==1 && $0=="---"{fm=0; next} fm==0{print}' "$f" \
173
+ | sed '/^$/{1d}' > "$DEST/.cursor/commands/$n"
174
+ fi
129
175
  done
130
176
  }
131
177
  gen_adapters
132
178
 
133
- # ---- hybrid JSON merge: ensure the 3 hook entries, never drop user keys
134
- say ""; say "[3/6] wire hooks (idempotent merge):"
179
+ # ---- hybrid JSON merge: ensure the hook entries for selected tools, never drop user keys
180
+ say ""; say "[3/6] wire hooks (idempotent merge, tools: $TOOLS):"
135
181
  merge_settings() {
136
- [ "$DRY" = 1 ] && { act "merge .claude/settings.json + .cursor/hooks.json"; return 0; }
137
- backup_one "$DEST/.claude/settings.json"; backup_one "$DEST/.cursor/hooks.json"
182
+ [ "$DRY" = 1 ] && { act "merge $(tool_selected claude && echo -n '.claude/settings.json ')$(tool_selected cursor && echo -n '.cursor/hooks.json')"; return 0; }
183
+ tool_selected claude && backup_one "$DEST/.claude/settings.json"
184
+ tool_selected cursor && backup_one "$DEST/.cursor/hooks.json"
185
+ CLAUDE_SEL=$(tool_selected claude && echo 1 || echo 0) \
186
+ CURSOR_SEL=$(tool_selected cursor && echo 1 || echo 0) \
138
187
  node - "$DEST" <<'NODE'
139
188
  const fs=require('fs'),path=require('path');
140
189
  const dest=process.argv[2];
190
+ const claudeSel=process.env.CLAUDE_SEL==='1', cursorSel=process.env.CURSOR_SEL==='1';
141
191
  const load=(p,d)=>{try{return JSON.parse(fs.readFileSync(p,'utf8'))}catch(e){return d}};
142
192
  const save=(p,o)=>{fs.mkdirSync(path.dirname(p),{recursive:true});fs.writeFileSync(p,JSON.stringify(o,null,2)+"\n")};
193
+ const out=[];
143
194
  // 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);
195
+ if(claudeSel){
196
+ const sp=path.join(dest,'.claude/settings.json');
197
+ const s=load(sp,{});
198
+ if(!s.$schema)s.$schema='https://json.schemastore.org/claude-code-settings.json';
199
+ s.hooks=s.hooks||{};
200
+ const ensure=(event,matcher,cmd)=>{
201
+ const arr=s.hooks[event]=s.hooks[event]||[];
202
+ for(const blk of arr)for(const h of (blk.hooks||[]))if(h.command===cmd)return;
203
+ const blk={hooks:[{type:'command',command:cmd}]};
204
+ if(matcher)blk.matcher=matcher;
205
+ arr.push(blk);
206
+ };
207
+ ensure('SessionStart','startup|clear|compact','bash .claude/hooks/session-start.sh');
208
+ ensure('PreCompact','auto|manual','bash .claude/hooks/pre-compact.sh');
209
+ ensure('SubagentStop','','bash .claude/hooks/subagent-stop.sh');
210
+ save(sp,s);
211
+ out.push('.claude/settings.json');
212
+ }
159
213
  // 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');
214
+ if(cursorSel){
215
+ const cp=path.join(dest,'.cursor/hooks.json');
216
+ const c=load(cp,{version:1,hooks:{}});
217
+ c.hooks=c.hooks||{};
218
+ const cursorEnsure=(event,cmd)=>{
219
+ const arr=c.hooks[event]=c.hooks[event]||[];
220
+ if(!arr.some(h=>h.command===cmd))arr.push({command:cmd});
221
+ };
222
+ cursorEnsure('sessionStart','bash .claude/hooks/cursor-session-start.sh');
223
+ cursorEnsure('preCompact','bash .claude/hooks/pre-compact.sh');
224
+ cursorEnsure('subagentStop','bash .claude/hooks/subagent-stop.sh');
225
+ save(cp,c);
226
+ out.push('.cursor/hooks.json');
227
+ }
228
+ console.log(' merged '+(out.join(' + ')||'(nothing — no tool selected hooks)'));
172
229
  NODE
173
230
  }
174
231
  merge_settings
@@ -180,13 +237,17 @@ say ""; say "[4/6] AGENTS.md / CLAUDE.md verify:"
180
237
  verify_import() {
181
238
  local CL="$DEST/CLAUDE.md" AG="$DEST/AGENTS.md"
182
239
  [ -f "$AG" ] || { echo "FATAL VERIFY: AGENTS.md not on disk"; exit 1; }
183
- # bare top-level @AGENTS.md import (not indented, not fenced)
184
- if ! grep -qE '^@AGENTS\.md[[:space:]]*$' "$CL"; then
185
- echo "FATAL VERIFY: CLAUDE.md is missing a bare top-level '@AGENTS.md' import line"; exit 1; fi
186
- # sentinel present in AGENTS.md
240
+ # sentinel present in AGENTS.md (shared both tools read AGENTS.md)
187
241
  if ! grep -q 'ANPUNKIT-AGENTS-SENTINEL' "$AG"; then
188
242
  echo "FATAL VERIFY: AGENTS.md sentinel missing (file clobbered or empty?)"; exit 1; fi
189
- say " VERIFY ok: bare @AGENTS.md import + AGENTS.md on disk + sentinel present."
243
+ # CLAUDE.md bare @AGENTS.md import is Claude-specific only verify if claude selected
244
+ if tool_selected claude; then
245
+ if ! grep -qE '^@AGENTS\.md[[:space:]]*$' "$CL"; then
246
+ echo "FATAL VERIFY: CLAUDE.md is missing a bare top-level '@AGENTS.md' import line"; exit 1; fi
247
+ say " VERIFY ok: bare @AGENTS.md import + AGENTS.md on disk + sentinel present."
248
+ else
249
+ say " VERIFY ok: AGENTS.md on disk + sentinel present (claude not selected — CLAUDE.md import not required)."
250
+ fi
190
251
  }
191
252
  verify_import
192
253
 
@@ -194,7 +255,7 @@ verify_import
194
255
  say ""; say "[5/6] .gitignore patch:"
195
256
  patch_gitignore() {
196
257
  local gi="$DEST/.gitignore"
197
- local -a needles=("docs/.snapshots/" "docs/.kb-snapshot.md" ".anpunkit-backup-*/" "*.anpunkit-new")
258
+ local -a needles=("docs/.snapshots/" "docs/.kb-snapshot.md" "docs/evidence/" ".anpunkit-backup-*/" "*.anpunkit-new")
198
259
  [ "$DRY" = 1 ] && { act "patch .gitignore (${#needles[@]} entries)"; return 0; }
199
260
  touch "$gi"
200
261
  for n in "${needles[@]}"; do grep -qxF "$n" "$gi" || printf '%s\n' "$n" >> "$gi"; done
@@ -207,31 +268,51 @@ say ""; say "[6/6] shared KB:"
207
268
  configure_kb() {
208
269
  local cfg="$DEST/.claude/kb-config.json"
209
270
  if [ "$NO_KB" = 1 ]; then say " --no-kb: skipping KB."; return 0; fi
210
- if [ -z "$KB_PATH" ] && [ -z "$KB_REMOTE" ]; then
271
+
272
+ # Path-first by design: YOU clone the KB repo (you own the git auth);
273
+ # anpunkit only records where it lives. After that, the session-start hook
274
+ # pulls and /store-wisdom commits+pushes (human-gated) — no manual git needed.
275
+ if [ -z "$KB_PATH" ]; then
211
276
  if [ -t 0 ]; then
212
- printf " Local path to cloned anpunkit-kb repo (blank to skip): "; read -r KB_PATH || true
213
- [ -z "$KB_PATH" ] && { say " skipped KB (no path given)."; return 0; }
277
+ printf " Local path to your cloned anpunkit-kb repo (blank to skip): "
278
+ read -r KB_PATH || true
279
+ [ -z "$KB_PATH" ] && { say " skipped KB."; return 0; }
214
280
  else
215
- say " no KB flags + no TTY -> skipping KB (re-run with --kb-path / --kb-remote)."; return 0
281
+ say " no KB flags + no TTY -> skipping KB (re-run with --kb-path <dir>)."; return 0
216
282
  fi
217
283
  fi
218
- if [ -n "$KB_PATH" ] && [ ! -d "$KB_PATH" ]; then
219
- echo " ! KB path '$KB_PATH' not a directory. Clone your anpunkit-kb repo first."; return 0; fi
284
+ if [ ! -d "$KB_PATH" ]; then
285
+ echo " ! KB path '$KB_PATH' is not a directory. Clone your KB repo first, e.g.:"
286
+ echo " git clone git@github.com:<you>/anpunkit-kb.git ~/anpunkit-kb"
287
+ echo " then re-run with --kb-path ~/anpunkit-kb"
288
+ return 0
289
+ fi
290
+
291
+ # remote is metadata: explicit --kb-remote wins, else read the clone's origin
292
+ if [ -z "$KB_REMOTE" ] && [ -d "$KB_PATH/.git" ]; then
293
+ KB_REMOTE=$(git -C "$KB_PATH" remote get-url origin 2>/dev/null || echo "")
294
+ fi
220
295
  if [ -n "$KB_REMOTE" ]; then
221
- git ls-remote "$KB_REMOTE" >/dev/null 2>&1 || say " ! warning: cannot reach KB remote '$KB_REMOTE' (configuring anyway)."; fi
222
- [ "$DRY" = 1 ] && { act "write .claude/kb-config.json"; return 0; }
296
+ git ls-remote "$KB_REMOTE" >/dev/null 2>&1 \
297
+ || say " ! warning: cannot reach KB remote '$KB_REMOTE' right now (recording anyway; pull/push will need it)."
298
+ fi
299
+
300
+ [ "$DRY" = 1 ] && { act "write .claude/kb-config.json (path: $KB_PATH)"; return 0; }
223
301
  mkdir -p "$DEST/.claude"
224
302
  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"
225
- say " wrote .claude/kb-config.json"
303
+ say " wrote .claude/kb-config.json (path: $KB_PATH${KB_REMOTE:+, remote: $KB_REMOTE})"
226
304
  }
305
+
227
306
  configure_kb
228
307
 
229
- # ---- finalize: stamp the installed manifest (copy the new one)
308
+ # ---- finalize: stamp the installed manifest (copy the new one) + record tools
230
309
  if [ "$DRY" != 1 ]; then
231
310
  cp -p "$MAN_SRC" "$MAN_DEST" 2>/dev/null || true
311
+ mkdir -p "$DEST/.claude"
312
+ node -e "require('fs').writeFileSync(process.argv[1],JSON.stringify({tools:process.argv[2].split(' ').filter(Boolean)},null,2)+'\n')" "$TOOLS_FILE" "$TOOLS" 2>/dev/null || true
232
313
  [ -d "$BACKUP" ] && say "" && say "backup written: ${BACKUP#$DEST/}"
233
314
  fi
234
315
 
235
- say ""; say "anpunkit setup complete ($MODE). Open Claude Code or Cursor — the SessionStart/sessionStart hook fires automatically."
316
+ say ""; say "anpunkit setup complete ($MODE, tools: $TOOLS). Open Claude Code or Cursor — the SessionStart/sessionStart hook fires automatically."
236
317
  if [ "$DRY" = 1 ]; then say "(dry-run: no files were written.)"; fi
237
318
  exit 0