codeforge-dev 1.8.0 → 1.10.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.
Files changed (55) hide show
  1. package/.devcontainer/.env +3 -0
  2. package/.devcontainer/CHANGELOG.md +107 -0
  3. package/.devcontainer/CLAUDE.md +30 -9
  4. package/.devcontainer/README.md +61 -2
  5. package/.devcontainer/config/defaults/main-system-prompt.md +162 -128
  6. package/.devcontainer/config/defaults/rules/spec-workflow.md +75 -0
  7. package/.devcontainer/config/defaults/rules/workspace-scope.md +7 -0
  8. package/.devcontainer/config/defaults/settings.json +63 -66
  9. package/.devcontainer/config/file-manifest.json +30 -18
  10. package/.devcontainer/connect-external-terminal.sh +17 -17
  11. package/.devcontainer/devcontainer.json +143 -144
  12. package/.devcontainer/plugins/devs-marketplace/.claude-plugin/marketplace.json +104 -97
  13. package/.devcontainer/plugins/devs-marketplace/plugins/auto-code-quality/.claude-plugin/plugin.json +7 -0
  14. package/.devcontainer/plugins/devs-marketplace/plugins/auto-code-quality/README.md +158 -0
  15. package/.devcontainer/plugins/devs-marketplace/plugins/auto-code-quality/hooks/hooks.json +39 -0
  16. package/.devcontainer/plugins/devs-marketplace/plugins/auto-code-quality/scripts/collect-edited-files.py +47 -0
  17. package/.devcontainer/plugins/devs-marketplace/plugins/auto-code-quality/scripts/format-on-stop.py +297 -0
  18. package/.devcontainer/plugins/devs-marketplace/plugins/auto-code-quality/scripts/lint-file.py +536 -0
  19. package/.devcontainer/plugins/devs-marketplace/plugins/auto-code-quality/scripts/syntax-validator.py +146 -0
  20. package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/agents/architect.md +81 -4
  21. package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/agents/debug-logs.md +18 -0
  22. package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/agents/dependency-analyst.md +18 -0
  23. package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/agents/doc-writer.md +89 -4
  24. package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/agents/explorer.md +18 -0
  25. package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/agents/generalist.md +142 -8
  26. package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/agents/git-archaeologist.md +18 -0
  27. package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/agents/migrator.md +108 -1
  28. package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/agents/perf-profiler.md +24 -0
  29. package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/agents/refactorer.md +97 -1
  30. package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/agents/researcher.md +33 -1
  31. package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/agents/security-auditor.md +24 -0
  32. package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/agents/spec-writer.md +50 -12
  33. package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/agents/test-writer.md +96 -1
  34. package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/hooks/hooks.json +100 -95
  35. package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/scripts/advisory-test-runner.py +186 -13
  36. package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/scripts/spec-reminder.py +122 -0
  37. package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/skills/documentation-patterns/SKILL.md +1 -1
  38. package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/skills/spec-check/SKILL.md +98 -0
  39. package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/skills/spec-init/SKILL.md +99 -0
  40. package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/skills/spec-init/references/backlog-template.md +23 -0
  41. package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/skills/spec-init/references/roadmap-template.md +33 -0
  42. package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/skills/spec-new/SKILL.md +110 -0
  43. package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/skills/spec-new/references/template.md +129 -0
  44. package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/skills/spec-refine/SKILL.md +194 -0
  45. package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/skills/spec-update/SKILL.md +142 -0
  46. package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/skills/specification-writing/SKILL.md +19 -12
  47. package/.devcontainer/scripts/check-setup.sh +24 -25
  48. package/.devcontainer/scripts/setup-aliases.sh +88 -76
  49. package/.devcontainer/scripts/setup-config.sh +86 -83
  50. package/.devcontainer/scripts/setup-projects.sh +172 -131
  51. package/.devcontainer/scripts/setup-terminal.sh +48 -0
  52. package/.devcontainer/scripts/setup-update-claude.sh +49 -107
  53. package/.devcontainer/scripts/setup.sh +4 -17
  54. package/README.md +2 -2
  55. package/package.json +42 -42
@@ -4,117 +4,120 @@
4
4
  CONFIG_DIR="${CONFIG_SOURCE_DIR:?CONFIG_SOURCE_DIR not set}"
5
5
  MANIFEST="$CONFIG_DIR/file-manifest.json"
6
6
 
7
- log() { echo "[setup-config] $*"; }
7
+ log() { echo "[setup-config] $*"; }
8
8
  warn() { echo "[setup-config] WARNING: $*"; }
9
- err() { echo "[setup-config] ERROR: $*" >&2; }
9
+ err() { echo "[setup-config] ERROR: $*" >&2; }
10
10
 
11
11
  # Deprecation notice if legacy OVERWRITE_CONFIG is still set
12
12
  if [ -n "${OVERWRITE_CONFIG+x}" ]; then
13
- warn "OVERWRITE_CONFIG is deprecated. Use per-file 'overwrite' in config/file-manifest.json instead."
13
+ warn "OVERWRITE_CONFIG is deprecated. Use per-file 'overwrite' in config/file-manifest.json instead."
14
14
  fi
15
15
 
16
16
  # ── Legacy fallback ──────────────────────────────────────────────
17
17
  legacy_copy() {
18
- local target_dir="${CLAUDE_CONFIG_DIR:?CLAUDE_CONFIG_DIR not set}"
19
- warn "file-manifest.json not found, falling back to legacy copy"
20
- mkdir -p "$target_dir"
21
- for file in defaults/settings.json defaults/keybindings.json defaults/main-system-prompt.md; do
22
- if [ -f "$CONFIG_DIR/$file" ]; then
23
- local basename="${file##*/}"
24
- cp "$CONFIG_DIR/$file" "$target_dir/$basename"
25
- chown "$(id -un):$(id -gn)" "$target_dir/$basename" 2>/dev/null || true
26
- log "Copied $basename (legacy)"
27
- fi
28
- done
29
- log "Configuration complete (legacy)"
18
+ local target_dir="${CLAUDE_CONFIG_DIR:?CLAUDE_CONFIG_DIR not set}"
19
+ warn "file-manifest.json not found, falling back to legacy copy"
20
+ mkdir -p "$target_dir"
21
+ for file in defaults/settings.json defaults/keybindings.json defaults/main-system-prompt.md; do
22
+ if [ -f "$CONFIG_DIR/$file" ]; then
23
+ local basename="${file##*/}"
24
+ cp "$CONFIG_DIR/$file" "$target_dir/$basename"
25
+ chown "$(id -un):$(id -gn)" "$target_dir/$basename" 2>/dev/null || true
26
+ log "Copied $basename (legacy)"
27
+ fi
28
+ done
29
+ log "Configuration complete (legacy)"
30
30
  }
31
31
 
32
32
  if [ ! -f "$MANIFEST" ]; then
33
- legacy_copy
34
- exit 0
33
+ legacy_copy
34
+ exit 0
35
35
  fi
36
36
 
37
37
  # ── Validate manifest JSON ──────────────────────────────────────
38
38
  if ! jq empty "$MANIFEST" 2>/dev/null; then
39
- err "Invalid JSON in file-manifest.json"
40
- exit 1
39
+ err "Invalid JSON in file-manifest.json"
40
+ exit 1
41
41
  fi
42
42
 
43
43
  # ── Variable expansion ───────────────────────────────────────────
44
44
  expand_vars() {
45
- local val="$1"
46
- val="${val//\$\{CLAUDE_CONFIG_DIR\}/$CLAUDE_CONFIG_DIR}"
47
- val="${val//\$\{WORKSPACE_ROOT\}/$WORKSPACE_ROOT}"
48
- # Warn on any remaining unresolved ${...} tokens
49
- if [[ "$val" =~ \$\{[^}]+\} ]]; then
50
- warn "Unresolved variable in: $val"
51
- fi
52
- echo "$val"
45
+ local val="$1"
46
+ val="${val//\$\{CLAUDE_CONFIG_DIR\}/$CLAUDE_CONFIG_DIR}"
47
+ val="${val//\$\{WORKSPACE_ROOT\}/$WORKSPACE_ROOT}"
48
+ # Warn on any remaining unresolved ${...} tokens
49
+ if [[ "$val" =~ \$\{[^}]+\} ]]; then
50
+ warn "Unresolved variable in: $val"
51
+ fi
52
+ echo "$val"
53
53
  }
54
54
 
55
55
  # ── Change detection ─────────────────────────────────────────────
56
56
  should_copy() {
57
- local src="$1" dest="$2"
58
- [ ! -f "$dest" ] && return 0
59
- local src_hash dest_hash
60
- src_hash=$(sha256sum "$src" | cut -d' ' -f1)
61
- dest_hash=$(sha256sum "$dest" | cut -d' ' -f1)
62
- [ "$src_hash" != "$dest_hash" ]
57
+ local src="$1" dest="$2"
58
+ [ ! -f "$dest" ] && return 0
59
+ local src_hash dest_hash
60
+ src_hash=$(sha256sum "$src" | cut -d' ' -f1)
61
+ dest_hash=$(sha256sum "$dest" | cut -d' ' -f1)
62
+ [ "$src_hash" != "$dest_hash" ]
63
63
  }
64
64
 
65
65
  # ── Process manifest ─────────────────────────────────────────────
66
66
  log "Copying configuration files..."
67
67
 
68
68
  # Single jq invocation to extract all fields (reduces N×5 subprocess calls to 1)
69
- jq -r '.[] | [.src, .dest, (.destFilename // ""), (.enabled // true | tostring), (.overwrite // "if-changed")] | @tsv' "$MANIFEST" |
70
- while IFS=$'\t' read -r src dest dest_filename enabled overwrite; do
71
- # Skip disabled entries
72
- if [ "$enabled" = "false" ]; then
73
- log "Skipping $src (disabled)"
74
- continue
75
- fi
76
-
77
- # Resolve paths
78
- src_path="$CONFIG_DIR/$src"
79
- dest_dir=$(expand_vars "$dest")
80
- filename="${dest_filename:-${src##*/}}"
81
- dest_path="$dest_dir/$filename"
82
-
83
- # Validate source exists
84
- if [ ! -f "$src_path" ]; then
85
- warn "$src not found in config dir, skipping"
86
- continue
87
- fi
88
-
89
- # Ensure destination directory exists
90
- mkdir -p "$dest_dir"
91
-
92
- # Apply overwrite strategy
93
- case "$overwrite" in
94
- always)
95
- cp "$src_path" "$dest_path"
96
- log "Copied $src → $dest_path (always)"
97
- ;;
98
- never)
99
- if [ ! -f "$dest_path" ]; then
100
- cp "$src_path" "$dest_path"
101
- log "Copied $src → $dest_path (new)"
102
- else
103
- log "Skipping $src (exists, overwrite=never)"
104
- fi
105
- ;;
106
- if-changed|*)
107
- if should_copy "$src_path" "$dest_path"; then
108
- cp "$src_path" "$dest_path"
109
- log "Copied $src → $dest_path (changed)"
110
- else
111
- log "Skipping $src (unchanged)"
112
- fi
113
- ;;
114
- esac
115
-
116
- # Fix ownership
117
- chown "$(id -un):$(id -gn)" "$dest_path" 2>/dev/null || true
118
- done
69
+ # Note: empty destFilename uses "__NONE__" sentinel because bash read collapses
70
+ # consecutive tab delimiters, which shifts fields when destFilename is empty.
71
+ jq -r '.[] | [.src, .dest, (.destFilename // "__NONE__"), (.enabled // true | tostring), (.overwrite // "if-changed")] | @tsv' "$MANIFEST" |
72
+ while IFS=$'\t' read -r src dest dest_filename enabled overwrite; do
73
+ # Skip disabled entries
74
+ if [ "$enabled" = "false" ]; then
75
+ log "Skipping $src (disabled)"
76
+ continue
77
+ fi
78
+
79
+ # Resolve paths
80
+ src_path="$CONFIG_DIR/$src"
81
+ dest_dir=$(expand_vars "$dest")
82
+ [ "$dest_filename" = "__NONE__" ] && dest_filename=""
83
+ filename="${dest_filename:-${src##*/}}"
84
+ dest_path="$dest_dir/$filename"
85
+
86
+ # Validate source exists
87
+ if [ ! -f "$src_path" ]; then
88
+ warn "$src not found in config dir, skipping"
89
+ continue
90
+ fi
91
+
92
+ # Ensure destination directory exists
93
+ mkdir -p "$dest_dir"
94
+
95
+ # Apply overwrite strategy
96
+ case "$overwrite" in
97
+ always)
98
+ cp "$src_path" "$dest_path"
99
+ log "Copied $src $dest_path (always)"
100
+ ;;
101
+ never)
102
+ if [ ! -f "$dest_path" ]; then
103
+ cp "$src_path" "$dest_path"
104
+ log "Copied $src → $dest_path (new)"
105
+ else
106
+ log "Skipping $src (exists, overwrite=never)"
107
+ fi
108
+ ;;
109
+ if-changed | *)
110
+ if should_copy "$src_path" "$dest_path"; then
111
+ cp "$src_path" "$dest_path"
112
+ log "Copied $src → $dest_path (changed)"
113
+ else
114
+ log "Skipping $src (unchanged)"
115
+ fi
116
+ ;;
117
+ esac
118
+
119
+ # Fix ownership
120
+ chown "$(id -un):$(id -gn)" "$dest_path" 2>/dev/null || true
121
+ done
119
122
 
120
123
  log "Configuration complete"
@@ -17,146 +17,187 @@ EXCLUDED_DIRS=".claude .gh .tmp .devcontainer .config node_modules .git"
17
17
  # --- Helpers ---
18
18
 
19
19
  is_excluded() {
20
- local name="$1"
21
- # Skip hidden directories (start with .)
22
- [[ "$name" == .* ]] && return 0
23
- # Skip explicitly excluded names
24
- for excluded in $EXCLUDED_DIRS; do
25
- [[ "$name" == "$excluded" ]] && return 0
26
- done
27
- return 1
20
+ local name="$1"
21
+ # Skip hidden directories (start with .)
22
+ [[ "$name" == .* ]] && return 0
23
+ # Skip explicitly excluded names
24
+ for excluded in $EXCLUDED_DIRS; do
25
+ [[ "$name" == "$excluded" ]] && return 0
26
+ done
27
+ return 1
28
+ }
29
+
30
+ has_project_markers() {
31
+ local dir="$1"
32
+ [ -d "$dir/.git" ] || [ -f "$dir/package.json" ] || [ -f "$dir/pyproject.toml" ] ||
33
+ [ -f "$dir/Cargo.toml" ] || [ -f "$dir/go.mod" ] || [ -f "$dir/deno.json" ] ||
34
+ [ -f "$dir/Makefile" ] || [ -f "$dir/CLAUDE.md" ]
28
35
  }
29
36
 
30
37
  detect_tags() {
31
- local dir="$1"
32
- local tags=()
33
-
34
- [ -d "$dir/.git" ] && tags+=("git")
35
- [ -f "$dir/package.json" ] && tags+=("node")
36
- [ -f "$dir/pyproject.toml" ] && tags+=("python")
37
- [ -f "$dir/Cargo.toml" ] && tags+=("rust")
38
- [ -f "$dir/go.mod" ] && tags+=("go")
39
- [ -f "$dir/deno.json" ] && tags+=("deno")
40
- [ -f "$dir/Makefile" ] && tags+=("make")
41
- [ -f "$dir/CLAUDE.md" ] && tags+=("claude")
42
-
43
- # Always add "auto" tag to mark as script-managed
44
- tags+=("auto")
45
-
46
- # If no markers found (only "auto"), add "folder" tag
47
- if [ ${#tags[@]} -eq 1 ]; then
48
- tags=("folder" "${tags[@]}")
49
- fi
50
-
51
- # Output as JSON array
52
- printf '%s\n' "${tags[@]}" | jq -R . | jq -s .
38
+ local dir="$1"
39
+ local tags=()
40
+
41
+ [ -d "$dir/.git" ] && tags+=("git")
42
+ [ -f "$dir/package.json" ] && tags+=("node")
43
+ [ -f "$dir/pyproject.toml" ] && tags+=("python")
44
+ [ -f "$dir/Cargo.toml" ] && tags+=("rust")
45
+ [ -f "$dir/go.mod" ] && tags+=("go")
46
+ [ -f "$dir/deno.json" ] && tags+=("deno")
47
+ [ -f "$dir/Makefile" ] && tags+=("make")
48
+ [ -f "$dir/CLAUDE.md" ] && tags+=("claude")
49
+
50
+ # Always add "auto" tag to mark as script-managed
51
+ tags+=("auto")
52
+
53
+ # If no markers found (only "auto"), add "folder" tag
54
+ if [ ${#tags[@]} -eq 1 ]; then
55
+ tags=("folder" "${tags[@]}")
56
+ fi
57
+
58
+ # Output as JSON array
59
+ printf '%s\n' "${tags[@]}" | jq -R . | jq -s .
60
+ }
61
+
62
+ register_project() {
63
+ local projects_json="$1"
64
+ local name="$2"
65
+ local dir="$3"
66
+ local tags
67
+ tags=$(detect_tags "$dir")
68
+ echo "$projects_json" | jq \
69
+ --arg name "$name" \
70
+ --arg path "$dir" \
71
+ --argjson tags "$tags" \
72
+ '. += [{"name": $name, "rootPath": ($path | rtrimstr("/")), "tags": $tags, "enabled": true}]'
53
73
  }
54
74
 
55
75
  scan_and_update() {
56
- # Build the new auto-detected projects array
57
- local new_projects="[]"
58
-
59
- for dir in "$WORKSPACE_ROOT"/*/; do
60
- [ -d "$dir" ] || continue
61
- local name
62
- name=$(basename "$dir")
63
-
64
- is_excluded "$name" && continue
65
-
66
- local tags
67
- tags=$(detect_tags "$dir")
68
-
69
- new_projects=$(echo "$new_projects" | jq \
70
- --arg name "$name" \
71
- --arg path "$dir" \
72
- --argjson tags "$tags" \
73
- '. += [{"name": $name, "rootPath": ($path | rtrimstr("/")), "tags": $tags, "enabled": true}]')
74
- done
75
-
76
- # Read existing projects.json (or empty array if missing/invalid)
77
- local existing="[]"
78
- if [ -f "$PROJECTS_FILE" ] && jq empty "$PROJECTS_FILE" 2>/dev/null; then
79
- existing=$(cat "$PROJECTS_FILE")
80
- fi
81
-
82
- # Separate user entries (no "auto" tag) from auto entries
83
- local user_entries
84
- user_entries=$(echo "$existing" | jq '[.[] | select(.tags | index("auto") | not)]')
85
-
86
- # Merge: user entries + new auto-detected entries
87
- local merged
88
- merged=$(jq -n --argjson user "$user_entries" --argjson auto "$new_projects" '$user + $auto')
89
-
90
- # Only write if content changed (avoid unnecessary file writes that trigger Project Manager reloads)
91
- local current_hash new_hash
92
- current_hash=""
93
- [ -f "$PROJECTS_FILE" ] && current_hash=$(md5sum "$PROJECTS_FILE" 2>/dev/null | cut -d' ' -f1)
94
-
95
- local tmp_file
96
- tmp_file=$(mktemp)
97
- echo "$merged" | jq '.' > "$tmp_file"
98
- new_hash=$(md5sum "$tmp_file" | cut -d' ' -f1)
99
-
100
- if [ "$current_hash" != "$new_hash" ]; then
101
- mv "$tmp_file" "$PROJECTS_FILE"
102
- chmod 644 "$PROJECTS_FILE"
103
- local count
104
- count=$(echo "$merged" | jq 'length')
105
- echo "$LOG_PREFIX Updated projects.json ($count projects)"
106
- else
107
- rm -f "$tmp_file"
108
- fi
76
+ # Build the new auto-detected projects array (two-pass: depth 1 + depth 2)
77
+ local new_projects="[]"
78
+
79
+ for dir in "$WORKSPACE_ROOT"/*/; do
80
+ [ -d "$dir" ] || continue
81
+ local name
82
+ name=$(basename "$dir")
83
+
84
+ is_excluded "$name" && continue
85
+
86
+ if has_project_markers "$dir"; then
87
+ # Depth 1: directory has project markers — register it directly
88
+ new_projects=$(register_project "$new_projects" "$name" "$dir")
89
+ else
90
+ # Depth 2: no markers — treat as container dir, scan its children
91
+ for subdir in "${dir%/}"/*/; do
92
+ [ -d "$subdir" ] || continue
93
+ local subname
94
+ subname=$(basename "$subdir")
95
+ is_excluded "$subname" && continue
96
+ new_projects=$(register_project "$new_projects" "$subname" "$subdir")
97
+ done
98
+ fi
99
+ done
100
+
101
+ # Read existing projects.json (or empty array if missing/invalid)
102
+ local existing="[]"
103
+ if [ -f "$PROJECTS_FILE" ] && jq empty "$PROJECTS_FILE" 2>/dev/null; then
104
+ existing=$(cat "$PROJECTS_FILE")
105
+ fi
106
+
107
+ # Separate user entries (no "auto" tag) from auto entries
108
+ local user_entries
109
+ user_entries=$(echo "$existing" | jq '[.[] | select(.tags | index("auto") | not)]')
110
+
111
+ # Merge: user entries + new auto-detected entries
112
+ local merged
113
+ merged=$(jq -n --argjson user "$user_entries" --argjson auto "$new_projects" '$user + $auto')
114
+
115
+ # Only write if content changed (avoid unnecessary file writes that trigger Project Manager reloads)
116
+ local current_hash new_hash
117
+ current_hash=""
118
+ [ -f "$PROJECTS_FILE" ] && current_hash=$(md5sum "$PROJECTS_FILE" 2>/dev/null | cut -d' ' -f1)
119
+
120
+ local tmp_file
121
+ tmp_file=$(mktemp)
122
+ echo "$merged" | jq '.' >"$tmp_file"
123
+ new_hash=$(md5sum "$tmp_file" | cut -d' ' -f1)
124
+
125
+ if [ "$current_hash" != "$new_hash" ]; then
126
+ mv "$tmp_file" "$PROJECTS_FILE"
127
+ chmod 644 "$PROJECTS_FILE"
128
+ local count
129
+ count=$(echo "$merged" | jq 'length')
130
+ echo "$LOG_PREFIX Updated projects.json ($count projects)"
131
+ else
132
+ rm -f "$tmp_file"
133
+ fi
134
+ }
135
+
136
+ stop_watcher() {
137
+ if [ -f "$PID_FILE" ]; then
138
+ local old_pid
139
+ old_pid=$(cat "$PID_FILE" 2>/dev/null)
140
+ if [ -n "$old_pid" ]; then
141
+ # Kill the process group to stop both the subshell and inotifywait pipeline
142
+ kill -- -"$old_pid" 2>/dev/null || kill "$old_pid" 2>/dev/null || true
143
+ fi
144
+ rm -f "$PID_FILE"
145
+ fi
109
146
  }
110
147
 
111
148
  start_watcher() {
112
- # Guard: don't start if already running
113
- if [ -f "$PID_FILE" ]; then
114
- local old_pid
115
- old_pid=$(cat "$PID_FILE" 2>/dev/null)
116
- if [ -n "$old_pid" ] && kill -0 "$old_pid" 2>/dev/null; then
117
- echo "$LOG_PREFIX Watcher already running (PID $old_pid), skipping"
118
- return 0
119
- fi
120
- # Stale PID file — clean up
121
- rm -f "$PID_FILE"
122
- fi
123
-
124
- # Check if inotifywait is available
125
- if ! command -v inotifywait &>/dev/null; then
126
- echo "$LOG_PREFIX Installing inotify-tools..."
127
- if command -v sudo &>/dev/null; then
128
- sudo apt-get update -qq && sudo apt-get install -y -qq inotify-tools >/dev/null 2>&1
129
- else
130
- apt-get update -qq && apt-get install -y -qq inotify-tools >/dev/null 2>&1
131
- fi
132
-
133
- if ! command -v inotifywait &>/dev/null; then
134
- echo "$LOG_PREFIX WARNING: Could not install inotify-tools, watcher disabled"
135
- return 1
136
- fi
137
- echo "$LOG_PREFIX inotify-tools installed"
138
- fi
139
-
140
- # Fork background watcher
141
- (
142
- # Watch for directory create/delete/move events in /workspaces/
143
- # --quiet suppresses header, --monitor keeps watching indefinitely
144
- inotifywait -m -q -e create,delete,moved_to,moved_from \
145
- --format '%f %e' "$WORKSPACE_ROOT" 2>/dev/null |
146
- while read -r name event; do
147
- # Small delay to let filesystem settle (e.g., move operations)
148
- sleep 1
149
- scan_and_update
150
- done
151
-
152
- # Cleanup on exit
153
- rm -f "$PID_FILE"
154
- ) &
155
- local watcher_pid=$!
156
- echo "$watcher_pid" > "$PID_FILE"
157
- disown
158
-
159
- echo "$LOG_PREFIX Background watcher started (PID $watcher_pid)"
149
+ # Guard: don't start if already running
150
+ if [ -f "$PID_FILE" ]; then
151
+ local old_pid
152
+ old_pid=$(cat "$PID_FILE" 2>/dev/null)
153
+ if [ -n "$old_pid" ] && kill -0 "$old_pid" 2>/dev/null; then
154
+ echo "$LOG_PREFIX Watcher already running (PID $old_pid), skipping"
155
+ return 0
156
+ fi
157
+ # Stale PID file — clean up
158
+ stop_watcher
159
+ fi
160
+
161
+ # Check if inotifywait is available
162
+ if ! command -v inotifywait &>/dev/null; then
163
+ echo "$LOG_PREFIX Installing inotify-tools..."
164
+ if command -v sudo &>/dev/null; then
165
+ sudo apt-get update -qq && sudo apt-get install -y -qq inotify-tools >/dev/null 2>&1
166
+ else
167
+ apt-get update -qq && apt-get install -y -qq inotify-tools >/dev/null 2>&1
168
+ fi
169
+
170
+ if ! command -v inotifywait &>/dev/null; then
171
+ echo "$LOG_PREFIX WARNING: Could not install inotify-tools, watcher disabled"
172
+ return 1
173
+ fi
174
+ echo "$LOG_PREFIX inotify-tools installed"
175
+ fi
176
+
177
+ # Fork background watcher in its own process group for clean shutdown
178
+ set -m
179
+ (
180
+ # Watch for directory create/delete/move events recursively under /workspaces/
181
+ # -r watches subdirectories (catches events inside container dirs like projects/)
182
+ # --exclude filters noisy dirs that generate frequent irrelevant events
183
+ inotifywait -m -r -q -e create,delete,moved_to,moved_from \
184
+ --exclude '(node_modules|\.git/|\.tmp|__pycache__|\.venv)' \
185
+ --format '%w%f %e' "$WORKSPACE_ROOT" 2>/dev/null |
186
+ while read -r _path event; do
187
+ # Small delay to let filesystem settle (e.g., move operations)
188
+ sleep 1
189
+ scan_and_update
190
+ done
191
+
192
+ # Cleanup on exit
193
+ rm -f "$PID_FILE"
194
+ ) &
195
+ local watcher_pid=$!
196
+ set +m
197
+ echo "$watcher_pid" >"$PID_FILE"
198
+ disown
199
+
200
+ echo "$LOG_PREFIX Background watcher started (PID $watcher_pid)"
160
201
  }
161
202
 
162
203
  # --- Main ---
@@ -0,0 +1,48 @@
1
+ #!/bin/bash
2
+ # Configure VS Code Shift+Enter → newline for Claude Code terminal input
3
+ # Writes to ~/.config/Code/User/keybindings.json (same path /terminal-setup uses)
4
+
5
+ echo "[setup-terminal] Configuring Shift+Enter keybinding for Claude Code..."
6
+
7
+ KEYBINDINGS_DIR="$HOME/.config/Code/User"
8
+ KEYBINDINGS_FILE="$KEYBINDINGS_DIR/keybindings.json"
9
+
10
+ # === Create directory if needed ===
11
+ mkdir -p "$KEYBINDINGS_DIR"
12
+
13
+ # === Check if already configured ===
14
+ if [ -f "$KEYBINDINGS_FILE" ] && grep -q "workbench.action.terminal.sendSequence" "$KEYBINDINGS_FILE" 2>/dev/null; then
15
+ echo "[setup-terminal] Shift+Enter binding already present, skipping"
16
+ exit 0
17
+ fi
18
+
19
+ # === Merge or create keybindings ===
20
+ BINDING='{"key":"shift+enter","command":"workbench.action.terminal.sendSequence","args":{"text":"\\u001b\\r"},"when":"terminalFocus"}'
21
+
22
+ if [ -f "$KEYBINDINGS_FILE" ] && command -v jq &>/dev/null; then
23
+ # Merge into existing keybindings
24
+ if jq empty "$KEYBINDINGS_FILE" 2>/dev/null; then
25
+ jq ". + [$BINDING]" "$KEYBINDINGS_FILE" >"$KEYBINDINGS_FILE.tmp" &&
26
+ mv "$KEYBINDINGS_FILE.tmp" "$KEYBINDINGS_FILE"
27
+ echo "[setup-terminal] Merged binding into existing keybindings"
28
+ else
29
+ # Invalid JSON — overwrite
30
+ echo "[$BINDING]" | jq '.' >"$KEYBINDINGS_FILE"
31
+ echo "[setup-terminal] Replaced invalid keybindings file"
32
+ fi
33
+ else
34
+ # No existing file — write fresh
35
+ cat >"$KEYBINDINGS_FILE" <<'EOF'
36
+ [
37
+ {
38
+ "key": "shift+enter",
39
+ "command": "workbench.action.terminal.sendSequence",
40
+ "args": {
41
+ "text": "\u001b\r"
42
+ },
43
+ "when": "terminalFocus"
44
+ }
45
+ ]
46
+ EOF
47
+ echo "[setup-terminal] Created keybindings file at $KEYBINDINGS_FILE"
48
+ fi