codeforge-dev 1.9.0 → 1.11.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 (56) hide show
  1. package/.devcontainer/.env +3 -0
  2. package/.devcontainer/CHANGELOG.md +125 -0
  3. package/.devcontainer/CLAUDE.md +41 -11
  4. package/.devcontainer/README.md +73 -3
  5. package/.devcontainer/config/defaults/main-system-prompt.md +187 -201
  6. package/.devcontainer/config/defaults/rules/session-search.md +66 -0
  7. package/.devcontainer/config/defaults/rules/spec-workflow.md +48 -13
  8. package/.devcontainer/config/defaults/settings.json +2 -1
  9. package/.devcontainer/config/defaults/writing-system-prompt.md +143 -0
  10. package/.devcontainer/config/file-manifest.json +12 -0
  11. package/.devcontainer/connect-external-terminal.sh +17 -17
  12. package/.devcontainer/devcontainer.json +150 -144
  13. package/.devcontainer/features/ccms/README.md +50 -0
  14. package/.devcontainer/features/ccms/devcontainer-feature.json +21 -0
  15. package/.devcontainer/features/ccms/install.sh +105 -0
  16. package/.devcontainer/features/ccstatusline/install.sh +24 -2
  17. package/.devcontainer/plugins/devs-marketplace/.claude-plugin/marketplace.json +8 -1
  18. package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/agents/architect.md +5 -3
  19. package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/agents/claude-guide.md +1 -1
  20. package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/agents/doc-writer.md +7 -7
  21. package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/agents/generalist.md +1 -0
  22. package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/agents/spec-writer.md +22 -12
  23. package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/hooks/hooks.json +11 -1
  24. package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/scripts/__pycache__/skill-suggester.cpython-314.pyc +0 -0
  25. package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/scripts/advisory-test-runner.py +186 -13
  26. package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/scripts/git-state-injector.py +15 -4
  27. package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/scripts/inject-cwd.py +37 -0
  28. package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/scripts/skill-suggester.py +24 -0
  29. package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/scripts/spec-reminder.py +4 -2
  30. package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/skills/documentation-patterns/SKILL.md +1 -1
  31. package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/skills/spec-build/SKILL.md +353 -0
  32. package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/skills/spec-build/references/review-checklist.md +175 -0
  33. package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/skills/spec-check/SKILL.md +28 -15
  34. package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/skills/spec-init/SKILL.md +16 -13
  35. package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/skills/spec-init/references/backlog-template.md +19 -3
  36. package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/skills/spec-init/references/milestones-template.md +32 -0
  37. package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/skills/spec-new/SKILL.md +28 -20
  38. package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/skills/spec-new/references/template.md +35 -6
  39. package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/skills/spec-refine/SKILL.md +194 -0
  40. package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/skills/spec-review/SKILL.md +229 -0
  41. package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/skills/spec-update/SKILL.md +24 -2
  42. package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/skills/specification-writing/SKILL.md +20 -13
  43. package/.devcontainer/plugins/devs-marketplace/plugins/codeforge-lsp/.claude-plugin/plugin.json +38 -5
  44. package/.devcontainer/plugins/devs-marketplace/plugins/workspace-scope-guard/.claude-plugin/plugin.json +7 -0
  45. package/.devcontainer/plugins/devs-marketplace/plugins/workspace-scope-guard/hooks/hooks.json +17 -0
  46. package/.devcontainer/plugins/devs-marketplace/plugins/workspace-scope-guard/scripts/__pycache__/guard-workspace-scope.cpython-314.pyc +0 -0
  47. package/.devcontainer/plugins/devs-marketplace/plugins/workspace-scope-guard/scripts/guard-workspace-scope.py +132 -0
  48. package/.devcontainer/scripts/check-setup.sh +24 -25
  49. package/.devcontainer/scripts/setup-aliases.sh +95 -90
  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 +1 -1
  56. package/.devcontainer/plugins/devs-marketplace/plugins/code-directive/skills/spec-init/references/roadmap-template.md +0 -13
@@ -1,117 +1,122 @@
1
1
  #!/bin/bash
2
2
  # Setup cc/claude/ccraw aliases for claude with local system prompt support
3
+ #
4
+ # Idempotent: removes the entire managed block then re-writes it fresh.
5
+ # Safe to run on every container start via postStartCommand.
3
6
 
4
7
  CLAUDE_DIR="${CLAUDE_CONFIG_DIR:?CLAUDE_CONFIG_DIR not set}"
5
8
 
6
9
  echo "[setup-aliases] Configuring Claude aliases..."
7
10
 
8
- # Simple alias definitions (not functions functions don't behave reliably across shell contexts)
9
- ALIAS_CC='alias cc='"'"'CLAUDE_CODE_ADDITIONAL_DIRECTORIES_CLAUDE_MD=1 command claude --system-prompt-file "$CLAUDE_CONFIG_DIR/system-prompt.md" --permission-mode plan --allow-dangerously-skip-permissions'"'"''
10
- ALIAS_CLAUDE='alias claude='"'"'CLAUDE_CODE_ADDITIONAL_DIRECTORIES_CLAUDE_MD=1 command claude --system-prompt-file "$CLAUDE_CONFIG_DIR/system-prompt.md" --permission-mode plan --allow-dangerously-skip-permissions'"'"''
11
- ALIAS_CCRAW='alias ccraw="command claude"'
11
+ # Resolve check-setup path once (used inside the block we write)
12
+ DEVCONTAINER_SCRIPTS="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
13
+
14
+ BLOCK_START='# === CodeForge Claude aliases START (managed by setup-aliases.sh — do not edit) ==='
15
+ BLOCK_END='# === CodeForge Claude aliases END ==='
12
16
 
13
17
  for rc in ~/.bashrc ~/.zshrc; do
14
- if [ -f "$rc" ]; then
15
- # --- Backup before modifying ---
16
- cp "$rc" "${rc}.bak.$(date +%s)" 2>/dev/null || true
17
- # Clean old backups (keep last 3)
18
- ls -t "${rc}.bak."* 2>/dev/null | tail -n +4 | xargs rm -f 2>/dev/null || true
19
-
20
- # --- Cleanup old definitions ---
21
-
22
- # Remove old cc alias
23
- if grep -q "alias cc=" "$rc" 2>/dev/null; then
24
- sed -i '/alias cc=/d' "$rc"
25
- echo "[setup-aliases] Removed old cc alias from $(basename $rc)"
26
- fi
27
- # Remove old cc function (single-line or multi-line)
28
- if grep -q "^cc()" "$rc" 2>/dev/null; then
29
- sed -i '/^cc() {/,/^}/d' "$rc"
30
- echo "[setup-aliases] Removed old cc function from $(basename $rc)"
31
- fi
32
- # Remove old _claude_with_config function
33
- if grep -q "^_claude_with_config()" "$rc" 2>/dev/null; then
34
- sed -i '/^_claude_with_config() {/,/^}/d' "$rc"
35
- echo "[setup-aliases] Removed old _claude_with_config function from $(basename $rc)"
36
- fi
37
- # Remove old claude function override
38
- if grep -q "^claude() {" "$rc" 2>/dev/null; then
39
- sed -i '/^claude() { _claude_with_config/d' "$rc"
40
- echo "[setup-aliases] Removed old claude function from $(basename $rc)"
41
- fi
42
- # Remove old claude alias
43
- if grep -q "alias claude=" "$rc" 2>/dev/null; then
44
- sed -i '/alias claude=/d' "$rc"
45
- fi
46
- # Remove old ccraw alias
47
- if grep -q "alias ccraw=" "$rc" 2>/dev/null; then
48
- sed -i '/alias ccraw=/d' "$rc"
49
- fi
50
- # Remove old specwright alias
51
- if grep -q "alias specwright=" "$rc" 2>/dev/null; then
52
- sed -i '/alias specwright=/d' "$rc"
53
- fi
54
- # Remove old cc-tools/check-setup functions
55
- if grep -q "^cc-tools()" "$rc" 2>/dev/null; then
56
- sed -i '/^cc-tools() {/,/^}/d' "$rc"
57
- fi
58
- if grep -q "alias check-setup=" "$rc" 2>/dev/null; then
59
- sed -i '/alias check-setup=/d' "$rc"
60
- fi
61
-
62
- # --- Add environment and aliases (idempotent) ---
63
- # Guard: skip if aliases already present from a previous run
64
- if grep -q '# Claude Code environment and aliases' "$rc" 2>/dev/null; then
65
- echo "[setup-aliases] Aliases already present in $(basename $rc), skipping"
66
- continue
67
- fi
68
- echo "" >> "$rc"
69
- echo "# Claude Code environment and aliases (managed by setup-aliases.sh)" >> "$rc"
70
- # Export CLAUDE_CONFIG_DIR so it's available in all shells (not just VS Code remoteEnv)
71
- if ! grep -q 'export CLAUDE_CONFIG_DIR=' "$rc" 2>/dev/null; then
72
- echo "export CLAUDE_CONFIG_DIR=\"${CLAUDE_CONFIG_DIR}\"" >> "$rc"
73
- fi
74
- # Export UTF-8 locale so tmux renders Unicode correctly (docker exec doesn't inherit locale)
75
- if ! grep -q 'export LANG=en_US.UTF-8' "$rc" 2>/dev/null; then
76
- echo 'export LANG=en_US.UTF-8' >> "$rc"
77
- echo 'export LC_ALL=en_US.UTF-8' >> "$rc"
78
- fi
79
- echo "$ALIAS_CC" >> "$rc"
80
- echo "$ALIAS_CLAUDE" >> "$rc"
81
- echo "$ALIAS_CCRAW" >> "$rc"
82
-
83
- # cc-tools: list all available CodeForge tools with version info
84
- cat >> "$rc" << 'CCTOOLS_EOF'
18
+ if [ -f "$rc" ]; then
19
+ # --- 1. Backup before modifying ---
20
+ cp "$rc" "${rc}.bak.$(date +%s)" 2>/dev/null || true
21
+ # Clean old backups (keep last 3)
22
+ ls -t "${rc}.bak."* 2>/dev/null | tail -n +4 | xargs rm -f 2>/dev/null || true
23
+
24
+ # --- 2. Remove existing managed block (if present) ---
25
+ sed -i '/# === CodeForge Claude aliases START/,/# === CodeForge Claude aliases END/d' "$rc"
26
+
27
+ # --- 3. Legacy cleanup (pre-marker formats only) ---
28
+ # These remove remnants from versions that predated the block-marker system.
29
+ # After step 2, anything matching these patterns is orphaned from old formats.
30
+
31
+ # Old function forms (pre-v1.10.0)
32
+ if grep -q "^cc()" "$rc" 2>/dev/null; then
33
+ sed -i '/^cc() {/,/^}/d' "$rc"
34
+ echo "[setup-aliases] Removed legacy cc() function from $(basename "$rc")"
35
+ fi
36
+ if grep -q "^_claude_with_config()" "$rc" 2>/dev/null; then
37
+ sed -i '/^_claude_with_config() {/,/^}/d' "$rc"
38
+ echo "[setup-aliases] Removed legacy _claude_with_config() function from $(basename "$rc")"
39
+ fi
40
+ if grep -q "^claude() { _claude_with_config" "$rc" 2>/dev/null; then
41
+ sed -i '/^claude() { _claude_with_config/d' "$rc"
42
+ echo "[setup-aliases] Removed legacy claude() function from $(basename "$rc")"
43
+ fi
44
+ if grep -q "alias specwright=" "$rc" 2>/dev/null; then
45
+ sed -i '/alias specwright=/d' "$rc"
46
+ echo "[setup-aliases] Removed legacy specwright alias from $(basename "$rc")"
47
+ fi
48
+
49
+ # Old alias/export form (v1.10.0 — no block markers)
50
+ sed -i '/# Claude Code environment and aliases/d' "$rc"
51
+ sed -i '/^export CLAUDE_CONFIG_DIR="/d' "$rc"
52
+ sed -i '/^export LANG=en_US\.UTF-8$/d' "$rc"
53
+ sed -i '/^export LC_ALL=en_US\.UTF-8$/d' "$rc"
54
+ # _CLAUDE_BIN if-block (4 patterns: if, elif, else, fi + assignments)
55
+ sed -i '/^if \[ -x "\$HOME\/\.local\/bin\/claude" \]/,/^fi$/d' "$rc"
56
+ sed -i '/^ _CLAUDE_BIN=/d' "$rc"
57
+ # Standalone aliases from old format
58
+ sed -i "/^alias cc='/d" "$rc"
59
+ sed -i "/^alias claude='/d" "$rc"
60
+ sed -i "/^alias ccraw='/d" "$rc"
61
+ sed -i "/^alias ccw='/d" "$rc"
62
+ sed -i '/^alias check-setup=/d' "$rc"
63
+ # cc-tools function from old format
64
+ if grep -q "^cc-tools()" "$rc" 2>/dev/null; then
65
+ sed -i '/^cc-tools() {/,/^}/d' "$rc"
66
+ fi
67
+
68
+ # --- 5. Write fresh managed block ---
69
+ cat >>"$rc" <<BLOCK_EOF
70
+
71
+ ${BLOCK_START}
72
+ export CLAUDE_CONFIG_DIR="${CLAUDE_CONFIG_DIR}"
73
+ export LANG=en_US.UTF-8
74
+ export LC_ALL=en_US.UTF-8
75
+
76
+ # Prefer native binary over npm-installed version
77
+ if [ -x "\$HOME/.local/bin/claude" ]; then
78
+ _CLAUDE_BIN="\$HOME/.local/bin/claude"
79
+ elif [ -x /usr/local/bin/claude ]; then
80
+ _CLAUDE_BIN=/usr/local/bin/claude
81
+ else
82
+ _CLAUDE_BIN=claude
83
+ fi
84
+
85
+ alias cc='CLAUDE_CODE_ADDITIONAL_DIRECTORIES_CLAUDE_MD=1 command "\$_CLAUDE_BIN" --system-prompt-file "\$CLAUDE_CONFIG_DIR/main-system-prompt.md" --permission-mode plan --allow-dangerously-skip-permissions'
86
+ alias claude='CLAUDE_CODE_ADDITIONAL_DIRECTORIES_CLAUDE_MD=1 command "\$_CLAUDE_BIN" --system-prompt-file "\$CLAUDE_CONFIG_DIR/main-system-prompt.md" --permission-mode plan --allow-dangerously-skip-permissions'
87
+ alias ccraw='command "\$_CLAUDE_BIN"'
88
+ alias ccw='CLAUDE_CODE_ADDITIONAL_DIRECTORIES_CLAUDE_MD=1 command "\$_CLAUDE_BIN" --system-prompt-file "\$CLAUDE_CONFIG_DIR/writing-system-prompt.md" --permission-mode plan --allow-dangerously-skip-permissions'
89
+
85
90
  cc-tools() {
86
91
  echo "CodeForge Available Tools"
87
92
  echo "━━━━━━━━━━━━━━━━━━━━━━━━"
88
93
  printf " %-20s %s\n" "COMMAND" "STATUS"
89
94
  echo " ────────────────────────────────────"
90
- for cmd in claude cc ccraw ccusage ccburn claude-monitor \
91
- ruff biome dprint shfmt shellcheck hadolint \
92
- ast-grep tree-sitter pyright typescript-language-server \
95
+ for cmd in claude cc ccw ccraw ccusage ccburn claude-monitor \\
96
+ ccms cargo ruff biome dprint shfmt shellcheck hadolint \\
97
+ ast-grep tree-sitter pyright typescript-language-server \\
93
98
  agent-browser gh docker git jq tmux bun go; do
94
- if command -v "$cmd" >/dev/null 2>&1; then
95
- ver=$("$cmd" --version 2>/dev/null | head -1 || echo "installed")
96
- printf " %-20s ✓ %s\n" "$cmd" "$ver"
99
+ if command -v "\$cmd" >/dev/null 2>&1; then
100
+ ver=\$("\$cmd" --version 2>/dev/null | head -1 || echo "installed")
101
+ printf " %-20s ✓ %s\n" "\$cmd" "\$ver"
97
102
  else
98
- printf " %-20s ✗ not found\n" "$cmd"
103
+ printf " %-20s ✗ not found\n" "\$cmd"
99
104
  fi
100
105
  done
101
106
  }
102
- CCTOOLS_EOF
103
107
 
104
- # check-setup: alias to the health check script
105
- DEVCONTAINER_SCRIPTS="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
106
- echo "alias check-setup='bash ${DEVCONTAINER_SCRIPTS}/check-setup.sh'" >> "$rc"
108
+ alias check-setup='bash ${DEVCONTAINER_SCRIPTS}/check-setup.sh'
109
+ ${BLOCK_END}
110
+ BLOCK_EOF
107
111
 
108
- echo "[setup-aliases] Added aliases to $(basename $rc)"
109
- fi
112
+ echo "[setup-aliases] Added aliases to $(basename "$rc")"
113
+ fi
110
114
  done
111
115
 
112
116
  echo "[setup-aliases] Aliases configured:"
113
- echo " cc -> claude with \$CLAUDE_CONFIG_DIR/system-prompt.md"
114
- echo " claude -> claude with \$CLAUDE_CONFIG_DIR/system-prompt.md"
117
+ echo " cc -> claude with \$CLAUDE_CONFIG_DIR/main-system-prompt.md"
118
+ echo " claude -> claude with \$CLAUDE_CONFIG_DIR/main-system-prompt.md"
115
119
  echo " ccraw -> vanilla claude without any config"
120
+ echo " ccw -> claude with \$CLAUDE_CONFIG_DIR/writing-system-prompt.md"
116
121
  echo " cc-tools -> list all available CodeForge tools"
117
122
  echo " check-setup -> verify CodeForge setup health"
@@ -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