vibe-forge 0.3.12 → 0.8.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.
Files changed (85) hide show
  1. package/.claude/commands/clear-attention.md +63 -63
  2. package/.claude/commands/compact-context.md +52 -0
  3. package/.claude/commands/configure-vcs.md +102 -0
  4. package/.claude/commands/forge.md +218 -171
  5. package/.claude/commands/need-help.md +77 -77
  6. package/.claude/commands/update-status.md +64 -64
  7. package/.claude/commands/worker-loop.md +106 -106
  8. package/.claude/hooks/worker-loop.js +217 -0
  9. package/.claude/scripts/setup-worker-loop.sh +45 -45
  10. package/.claude/settings.json +89 -0
  11. package/LICENSE +21 -21
  12. package/README.md +253 -230
  13. package/agents/aegis/personality.md +303 -269
  14. package/agents/anvil/personality.md +278 -211
  15. package/agents/architect/personality.md +260 -0
  16. package/agents/crucible/personality.md +362 -285
  17. package/agents/crucible-x/personality.md +210 -0
  18. package/agents/ember/personality.md +293 -245
  19. package/agents/flux/personality.md +248 -0
  20. package/agents/furnace/personality.md +342 -262
  21. package/agents/herald/personality.md +249 -247
  22. package/agents/loki/personality.md +108 -0
  23. package/agents/oracle/personality.md +284 -0
  24. package/agents/pixel/personality.md +140 -0
  25. package/agents/planning-hub/personality.md +473 -251
  26. package/agents/scribe/personality.md +253 -231
  27. package/agents/slag/personality.md +268 -0
  28. package/agents/temper/personality.md +270 -0
  29. package/bin/cli.js +372 -325
  30. package/bin/dashboard/api/agents.js +333 -0
  31. package/bin/dashboard/api/dispatch.js +507 -0
  32. package/bin/dashboard/api/tasks.js +416 -0
  33. package/bin/dashboard/public/assets/index-BpHfsx1r.js +2 -0
  34. package/bin/dashboard/public/assets/index-QODv4Zn9.css +1 -0
  35. package/bin/dashboard/public/index.html +14 -0
  36. package/bin/dashboard/server.js +645 -0
  37. package/bin/forge-daemon.sh +477 -775
  38. package/bin/forge-setup.sh +661 -532
  39. package/bin/forge-spawn.sh +164 -159
  40. package/bin/forge.cmd +83 -83
  41. package/bin/forge.sh +566 -393
  42. package/bin/lib/agents.sh +177 -177
  43. package/bin/lib/check-aliases.js +50 -0
  44. package/bin/lib/colors.sh +44 -44
  45. package/bin/lib/config.sh +347 -271
  46. package/bin/lib/constants.sh +241 -171
  47. package/bin/lib/daemon/budgets.sh +107 -0
  48. package/bin/lib/daemon/dependencies.sh +146 -0
  49. package/bin/lib/daemon/display.sh +128 -0
  50. package/bin/lib/daemon/notifications.sh +273 -0
  51. package/bin/lib/daemon/routing.sh +93 -0
  52. package/bin/lib/daemon/state.sh +163 -0
  53. package/bin/lib/daemon/sync.sh +103 -0
  54. package/bin/lib/database.sh +357 -224
  55. package/bin/lib/frontmatter.js +106 -0
  56. package/bin/lib/heimdall-setup.js +113 -0
  57. package/bin/lib/heimdall.js +265 -0
  58. package/bin/lib/json.sh +264 -0
  59. package/bin/lib/terminal.js +452 -0
  60. package/bin/lib/util.sh +126 -0
  61. package/bin/lib/vcs.js +349 -0
  62. package/config/agent-manifest.yaml +237 -230
  63. package/config/agents.json +207 -85
  64. package/config/task-template.md +159 -87
  65. package/config/task-types.yaml +111 -106
  66. package/config/templates/handoff-template.md +40 -0
  67. package/context/agent-overrides/README.md +41 -0
  68. package/context/architecture.md +42 -0
  69. package/context/modern-conventions.md +129 -129
  70. package/context/project-context-template.md +122 -122
  71. package/docs/agents.md +473 -0
  72. package/docs/architecture.md +194 -0
  73. package/docs/commands.md +451 -0
  74. package/docs/security.md +195 -144
  75. package/package.json +77 -48
  76. package/.claude/hooks/worker-loop.sh +0 -141
  77. package/.claude/settings.local.json +0 -29
  78. package/agents/forge-master/capabilities.md +0 -144
  79. package/agents/forge-master/context-template.md +0 -128
  80. package/agents/forge-master/personality.md +0 -138
  81. package/agents/sentinel/personality.md +0 -194
  82. package/context/forge-state.yaml +0 -19
  83. package/docs/TODO.md +0 -176
  84. package/docs/npm-publishing.md +0 -95
  85. package/tasks/review/task-001.md +0 -78
package/bin/lib/config.sh CHANGED
@@ -1,271 +1,347 @@
1
- #!/usr/bin/env bash
2
- #
3
- # Vibe Forge - Configuration Management
4
- # Source this file in other scripts: source "$SCRIPT_DIR/lib/config.sh"
5
- #
6
- # SECURITY: This module provides safe JSON parsing without grep/cut vulnerabilities.
7
- #
8
-
9
- # Ensure colors are loaded for error messages
10
- if ! type log_error &>/dev/null; then
11
- echo "Error: colors.sh must be sourced before config.sh" >&2
12
- exit 1
13
- fi
14
-
15
- # =============================================================================
16
- # Agent Configuration Loading
17
- # =============================================================================
18
-
19
- # load_agents_from_json AGENTS_JSON_FILE
20
- # Loads agent configuration from JSON file into shell variables.
21
- # Sets: VALID_AGENTS array, AGENT_ALIASES associative array, AGENT_DISPLAY_NAMES
22
- #
23
- # SECURITY: Uses safe JSON parsing via Node.js
24
- load_agents_from_json() {
25
- local agents_file="$1"
26
-
27
- if [[ ! -f "$agents_file" ]]; then
28
- return 1
29
- fi
30
-
31
- if ! command -v node &>/dev/null; then
32
- log_error "Node.js required for agent configuration"
33
- return 1
34
- fi
35
-
36
- # Parse agents JSON and output shell variable assignments
37
- # SECURITY: File path passed as argument, not interpolated
38
- # NOTE: We output direct assignments (not declare -A) since arrays are pre-declared globally
39
- local agent_data
40
- agent_data=$(node -e '
41
- const fs = require("fs");
42
- const file = process.argv[1];
43
- try {
44
- const data = JSON.parse(fs.readFileSync(file, "utf8"));
45
- const agents = data.agents || {};
46
-
47
- // Output VALID_AGENTS array
48
- const validAgents = Object.keys(agents);
49
- console.log("VALID_AGENTS=(" + validAgents.map(a => `"${a}"`).join(" ") + ")");
50
-
51
- // Output AGENT_ALIASES assignments (array already declared globally)
52
- for (const [canonical, info] of Object.entries(agents)) {
53
- // Add self-mapping
54
- console.log(`AGENT_ALIASES["${canonical}"]="${canonical}"`);
55
- // Add aliases
56
- if (info.aliases) {
57
- for (const alias of info.aliases) {
58
- console.log(`AGENT_ALIASES["${alias}"]="${canonical}"`);
59
- }
60
- }
61
- }
62
-
63
- // Output AGENT_DISPLAY_NAMES assignments
64
- for (const [canonical, info] of Object.entries(agents)) {
65
- const displayName = info.name || canonical;
66
- console.log(`AGENT_DISPLAY_NAMES["${canonical}"]="${displayName}"`);
67
- }
68
-
69
- // Output AGENT_ROLES assignments
70
- for (const [canonical, info] of Object.entries(agents)) {
71
- const role = info.role || "";
72
- console.log(`AGENT_ROLES["${canonical}"]="${role}"`);
73
- }
74
-
75
- // Output AGENT_PERSONALITY_FILES assignments
76
- for (const [canonical, info] of Object.entries(agents)) {
77
- const pfile = info.personality_file || "";
78
- console.log(`AGENT_PERSONALITY_FILES["${canonical}"]="${pfile}"`);
79
- }
80
-
81
- // Output AGENT_ICONS assignments
82
- for (const [canonical, info] of Object.entries(agents)) {
83
- const icon = info.icon || "";
84
- console.log(`AGENT_ICONS["${canonical}"]="${icon}"`);
85
- }
86
-
87
- // Output AGENT_TAB_COLORS assignments
88
- for (const [canonical, info] of Object.entries(agents)) {
89
- const tabColor = info.tab_color || "";
90
- console.log(`AGENT_TAB_COLORS["${canonical}"]="${tabColor}"`);
91
- }
92
-
93
- } catch (e) {
94
- console.error("Error parsing agents.json:", e.message);
95
- process.exit(1);
96
- }
97
- ' -- "$agents_file" 2>/dev/null) || return 1
98
-
99
- # Evaluate the output to set variables
100
- eval "$agent_data"
101
-
102
- # Mark as loaded
103
- AGENTS_LOADED="true"
104
- return 0
105
- }
106
-
107
- # json_get_string FILE KEY
108
- # Safely extracts a string value from a JSON file.
109
- # Uses node.js for safe parsing (available since we require Node 16+)
110
- #
111
- # SECURITY: This avoids grep/cut vulnerabilities by using proper JSON parsing.
112
- # SECURITY: File and key are passed as command-line arguments, not interpolated.
113
- json_get_string() {
114
- local file="$1"
115
- local key="$2"
116
-
117
- if [[ ! -f "$file" ]]; then
118
- return 1
119
- fi
120
-
121
- # Use Node.js for safe JSON parsing
122
- # SECURITY: Pass file and key as arguments to avoid injection
123
- if command -v node &>/dev/null; then
124
- node -e '
125
- const fs = require("fs");
126
- const file = process.argv[1];
127
- const key = process.argv[2];
128
- try {
129
- const data = JSON.parse(fs.readFileSync(file, "utf8"));
130
- const value = data[key];
131
- if (value !== undefined && value !== null) {
132
- console.log(String(value));
133
- }
134
- } catch (e) {
135
- process.exit(1);
136
- }
137
- ' -- "$file" "$key" 2>/dev/null
138
- return $?
139
- fi
140
-
141
- # Fallback: Use Python if available
142
- # SECURITY: Pass file and key as arguments to avoid injection
143
- if command -v python3 &>/dev/null; then
144
- python3 -c '
145
- import json, sys
146
- try:
147
- file_path = sys.argv[1]
148
- key = sys.argv[2]
149
- with open(file_path) as f:
150
- data = json.load(f)
151
- value = data.get(key)
152
- if value is not None:
153
- print(str(value))
154
- except:
155
- sys.exit(1)
156
- ' "$file" "$key" 2>/dev/null
157
- return $?
158
- fi
159
-
160
- # No safe parser available - exit with error
161
- log_error "No JSON parser available. Install Node.js or Python 3."
162
- return 1
163
- }
164
-
165
- # load_forge_config CONFIG_FILE
166
- # Loads configuration from the forge config file into environment variables.
167
- # Sets: PLATFORM, GIT_BASH_PATH, TERMINAL_TYPE, FORGE_VALIDATED
168
- #
169
- # Returns: 0 on success, 1 on failure
170
- load_forge_config() {
171
- local config_file="$1"
172
-
173
- if [[ ! -f "$config_file" ]]; then
174
- log_error "Vibe Forge not initialized."
175
- echo "Run 'forge init' first." >&2
176
- return 1
177
- fi
178
-
179
- # Load config values safely
180
- PLATFORM=$(json_get_string "$config_file" "platform") || PLATFORM=""
181
- GIT_BASH_PATH=$(json_get_string "$config_file" "git_bash_path") || GIT_BASH_PATH=""
182
- TERMINAL_TYPE=$(json_get_string "$config_file" "terminal_type") || TERMINAL_TYPE="manual"
183
- FORGE_VALIDATED=$(json_get_string "$config_file" "validated") || FORGE_VALIDATED="false"
184
-
185
- # Validate required fields
186
- if [[ -z "$PLATFORM" ]]; then
187
- log_error "Invalid config: missing platform"
188
- return 1
189
- fi
190
-
191
- return 0
192
- }
193
-
194
- # setup_windows_env
195
- # Sets up Windows-specific environment variables and PATH.
196
- # Call this after load_forge_config on Windows.
197
- setup_windows_env() {
198
- if [[ "$PLATFORM" != "windows" ]]; then
199
- return 0
200
- fi
201
-
202
- # Export Git Bash path for Claude Code
203
- if [[ -n "$GIT_BASH_PATH" ]]; then
204
- # Convert forward slashes to backslashes for Windows
205
- local git_bash_win="${GIT_BASH_PATH//\//\\}"
206
- export CLAUDE_CODE_GIT_BASH_PATH="$git_bash_win"
207
- fi
208
-
209
- # Add npm global path if not already in PATH
210
- local npm_path=""
211
-
212
- # Try with USER variable
213
- if [[ -n "$USER" ]]; then
214
- npm_path="/c/Users/$USER/AppData/Roaming/npm"
215
- fi
216
-
217
- # Try with USERPROFILE
218
- if [[ -z "$npm_path" || ! -d "$npm_path" ]] && [[ -n "$USERPROFILE" ]]; then
219
- npm_path="${USERPROFILE//\\//}/AppData/Roaming/npm"
220
- fi
221
-
222
- # Add to PATH if exists and not already there
223
- if [[ -n "$npm_path" && -d "$npm_path" && ":$PATH:" != *":$npm_path:"* ]]; then
224
- export PATH="$npm_path:$PATH"
225
- fi
226
- }
227
-
228
- # require_forge_config FORGE_ROOT
229
- # Loads config and exits with error if not initialized.
230
- # Convenience function that combines load + validation.
231
- require_forge_config() {
232
- local forge_root="$1"
233
- local config_file="$forge_root/.forge/config.json"
234
-
235
- load_forge_config "$config_file" || exit 1
236
- setup_windows_env
237
- }
238
-
239
- # write_json_config FILE KEY VALUE
240
- # Safely writes/updates a key in a JSON config file.
241
- # Creates file if it doesn't exist.
242
- #
243
- # SECURITY: File, key, and value are passed as command-line arguments, not interpolated.
244
- write_json_config() {
245
- local file="$1"
246
- local key="$2"
247
- local value="$3"
248
-
249
- # Use Node.js for safe JSON manipulation
250
- # SECURITY: Pass all values as arguments to avoid injection
251
- if command -v node &>/dev/null; then
252
- node -e '
253
- const fs = require("fs");
254
- const file = process.argv[1];
255
- const key = process.argv[2];
256
- const value = process.argv[3];
257
- let data = {};
258
- try {
259
- if (fs.existsSync(file)) {
260
- data = JSON.parse(fs.readFileSync(file, "utf8"));
261
- }
262
- } catch (e) {}
263
- data[key] = value;
264
- fs.writeFileSync(file, JSON.stringify(data, null, 2) + "\n");
265
- ' -- "$file" "$key" "$value" 2>/dev/null
266
- return $?
267
- fi
268
-
269
- log_error "Node.js required for config writing"
270
- return 1
271
- }
1
+ #!/usr/bin/env bash
2
+ #
3
+ # Vibe Forge - Configuration Management
4
+ # Source this file in other scripts: source "$SCRIPT_DIR/lib/config.sh"
5
+ #
6
+ # SECURITY: This module provides safe JSON parsing without grep/cut vulnerabilities.
7
+ #
8
+
9
+ # Ensure colors are loaded for error messages
10
+ if ! type log_error &>/dev/null; then
11
+ echo "Error: colors.sh must be sourced before config.sh" >&2
12
+ exit 1
13
+ fi
14
+
15
+ # =============================================================================
16
+ # Agent Configuration Loading
17
+ # =============================================================================
18
+
19
+ # load_agents_from_json AGENTS_JSON_FILE
20
+ # Loads agent configuration from JSON file into shell variables.
21
+ # Sets: VALID_AGENTS array, AGENT_ALIASES associative array, AGENT_DISPLAY_NAMES
22
+ #
23
+ # SECURITY: Uses safe JSON parsing via Node.js
24
+ load_agents_from_json() {
25
+ local agents_file="$1"
26
+
27
+ if [[ ! -f "$agents_file" ]]; then
28
+ return 1
29
+ fi
30
+
31
+ if ! command -v node &>/dev/null; then
32
+ log_error "Node.js required for agent configuration"
33
+ return 1
34
+ fi
35
+
36
+ # Derive forge root from agents file location (agents_file is $FORGE_ROOT/config/agents.json)
37
+ local forge_root
38
+ forge_root="$(cd "$(dirname "$agents_file")/.." 2>/dev/null && pwd)"
39
+ local env_file="$forge_root/.forge/agents.env"
40
+
41
+ # Regenerate the static env file only when agents.json is newer or cache missing
42
+ # SECURITY: Writing to a file and sourcing it is auditable; eval of dynamic strings is not
43
+ if [[ ! -f "$env_file" || "$agents_file" -nt "$env_file" ]]; then
44
+ mkdir -p "$(dirname "$env_file")"
45
+
46
+ # Write header + validated shell assignments to the static cache file
47
+ # SECURITY: File path passed as argument, not interpolated into the script
48
+ # SECURITY: Agent names and aliases are validated to prevent shell injection
49
+ # NOTE: We output direct assignments (not declare -A) since arrays are pre-declared globally
50
+ {
51
+ printf '# AUTO-GENERATED by load_agents_from_json() -- DO NOT EDIT\n'
52
+ printf '# Source: config/agents.json\n'
53
+ printf '# Regenerated automatically when agents.json is newer than this file\n'
54
+ node -e '
55
+ const fs = require("fs");
56
+ const file = process.argv[1];
57
+
58
+ // SECURITY: Validate identifier contains only safe characters
59
+ // Allows: lowercase letters, numbers, underscore, hyphen
60
+ function isValidIdentifier(name) {
61
+ return /^[a-z0-9_-]+$/.test(name);
62
+ }
63
+
64
+ // SECURITY: Escape string for safe use in shell double-quoted string
65
+ // Escapes: $, `, ", \, newlines
66
+ function escapeForShell(str) {
67
+ if (typeof str !== "string") return "";
68
+ return str
69
+ .replace(/\\/g, "\\\\")
70
+ .replace(/"/g, "\\\"")
71
+ .replace(/\$/g, "\\$")
72
+ .replace(/`/g, "\\`")
73
+ .replace(/\n/g, "\\n")
74
+ .replace(/\r/g, "");
75
+ }
76
+
77
+ try {
78
+ const data = JSON.parse(fs.readFileSync(file, "utf8"));
79
+ const agents = data.agents || {};
80
+
81
+ // SECURITY: Validate all agent names before processing
82
+ for (const name of Object.keys(agents)) {
83
+ if (!isValidIdentifier(name)) {
84
+ console.error("SECURITY ERROR: Invalid agent name: " + name);
85
+ console.error("Agent names must contain only: a-z, 0-9, underscore, hyphen");
86
+ process.exit(1);
87
+ }
88
+ // Also validate aliases
89
+ const info = agents[name];
90
+ if (info.aliases) {
91
+ for (const alias of info.aliases) {
92
+ if (!isValidIdentifier(alias)) {
93
+ console.error("SECURITY ERROR: Invalid alias: " + alias);
94
+ console.error("Aliases must contain only: a-z, 0-9, underscore, hyphen");
95
+ process.exit(1);
96
+ }
97
+ }
98
+ }
99
+ }
100
+
101
+ // Output VALID_AGENTS array (names validated above)
102
+ const validAgents = Object.keys(agents);
103
+ console.log("VALID_AGENTS=(" + validAgents.map(a => `"${a}"`).join(" ") + ")");
104
+
105
+ // Output AGENT_ALIASES assignments (array already declared globally)
106
+ for (const [canonical, info] of Object.entries(agents)) {
107
+ // Add self-mapping
108
+ console.log(`AGENT_ALIASES["${canonical}"]="${canonical}"`);
109
+ // Add aliases (validated above)
110
+ if (info.aliases) {
111
+ for (const alias of info.aliases) {
112
+ console.log(`AGENT_ALIASES["${alias}"]="${canonical}"`);
113
+ }
114
+ }
115
+ }
116
+
117
+ // Output AGENT_DISPLAY_NAMES assignments
118
+ // SECURITY: Display names are escaped since they come from user input
119
+ for (const [canonical, info] of Object.entries(agents)) {
120
+ const displayName = escapeForShell(info.name || canonical);
121
+ console.log(`AGENT_DISPLAY_NAMES["${canonical}"]="${displayName}"`);
122
+ }
123
+
124
+ // Output AGENT_ROLES assignments
125
+ for (const [canonical, info] of Object.entries(agents)) {
126
+ const role = escapeForShell(info.role || "");
127
+ console.log(`AGENT_ROLES["${canonical}"]="${role}"`);
128
+ }
129
+
130
+ // Output AGENT_PERSONALITY_FILES assignments
131
+ for (const [canonical, info] of Object.entries(agents)) {
132
+ const pfile = escapeForShell(info.personality_file || "");
133
+ console.log(`AGENT_PERSONALITY_FILES["${canonical}"]="${pfile}"`);
134
+ }
135
+
136
+ // Output AGENT_ICONS assignments
137
+ for (const [canonical, info] of Object.entries(agents)) {
138
+ const icon = escapeForShell(info.icon || "");
139
+ console.log(`AGENT_ICONS["${canonical}"]="${icon}"`);
140
+ }
141
+
142
+ // Output AGENT_TAB_COLORS assignments
143
+ for (const [canonical, info] of Object.entries(agents)) {
144
+ const tabColor = escapeForShell(info.tab_color || "");
145
+ console.log(`AGENT_TAB_COLORS["${canonical}"]="${tabColor}"`);
146
+ }
147
+
148
+ } catch (e) {
149
+ console.error("Error parsing agents.json:", e.message);
150
+ process.exit(1);
151
+ }
152
+ ' -- "$agents_file" 2>/dev/null
153
+ } > "$env_file" || { rm -f "$env_file"; return 1; }
154
+ fi
155
+
156
+ # Source the static cache file instead of eval-ing dynamic Node.js output
157
+ # SECURITY: Auditable static file; Node.js only runs when agents.json changes
158
+ # shellcheck source=/dev/null
159
+ if ! source "$env_file"; then
160
+ # Corrupted or invalid cache file remove it so it regenerates on next call
161
+ rm -f "$env_file"
162
+ return 1
163
+ fi
164
+
165
+ # Mark as loaded
166
+ AGENTS_LOADED="true"
167
+ return 0
168
+ }
169
+
170
+ # json_get_string FILE KEY
171
+ # Safely extracts a string value from a JSON file.
172
+ # Uses node.js for safe parsing (available since we require Node 16+)
173
+ #
174
+ # SECURITY: This avoids grep/cut vulnerabilities by using proper JSON parsing.
175
+ # SECURITY: File and key are passed as command-line arguments, not interpolated.
176
+ json_get_string() {
177
+ local file="$1"
178
+ local key="$2"
179
+
180
+ if [[ ! -f "$file" ]]; then
181
+ return 1
182
+ fi
183
+
184
+ # Use Node.js for safe JSON parsing
185
+ # SECURITY: Pass file and key as arguments to avoid injection
186
+ if command -v node &>/dev/null; then
187
+ node -e '
188
+ const fs = require("fs");
189
+ const file = process.argv[1];
190
+ const key = process.argv[2];
191
+ try {
192
+ const data = JSON.parse(fs.readFileSync(file, "utf8"));
193
+ const value = data[key];
194
+ if (value !== undefined && value !== null) {
195
+ console.log(String(value));
196
+ }
197
+ } catch (e) {
198
+ process.exit(1);
199
+ }
200
+ ' -- "$file" "$key" 2>/dev/null
201
+ return $?
202
+ fi
203
+
204
+ # Fallback: Use Python if available
205
+ # SECURITY: Pass file and key as arguments to avoid injection
206
+ if command -v python3 &>/dev/null; then
207
+ python3 -c '
208
+ import json, sys
209
+ try:
210
+ file_path = sys.argv[1]
211
+ key = sys.argv[2]
212
+ with open(file_path) as f:
213
+ data = json.load(f)
214
+ value = data.get(key)
215
+ if value is not None:
216
+ print(str(value))
217
+ except:
218
+ sys.exit(1)
219
+ ' "$file" "$key" 2>/dev/null
220
+ return $?
221
+ fi
222
+
223
+ # No safe parser available - exit with error
224
+ log_error "No JSON parser available. Install Node.js or Python 3."
225
+ return 1
226
+ }
227
+
228
+ # load_forge_config CONFIG_FILE
229
+ # Loads configuration from the forge config file into environment variables.
230
+ # Sets: PLATFORM, GIT_BASH_PATH, TERMINAL_TYPE, FORGE_VALIDATED
231
+ #
232
+ # Returns: 0 on success, 1 on failure
233
+ load_forge_config() {
234
+ local config_file="$1"
235
+
236
+ if [[ ! -f "$config_file" ]]; then
237
+ log_error "Vibe Forge not initialized."
238
+ echo "Run 'forge init' first." >&2
239
+ return 1
240
+ fi
241
+
242
+ # Load config values safely
243
+ PLATFORM=$(json_get_string "$config_file" "platform") || PLATFORM=""
244
+ GIT_BASH_PATH=$(json_get_string "$config_file" "git_bash_path") || GIT_BASH_PATH=""
245
+ TERMINAL_TYPE=$(json_get_string "$config_file" "terminal_type") || TERMINAL_TYPE="manual"
246
+ FORGE_VALIDATED=$(json_get_string "$config_file" "validated") || FORGE_VALIDATED="false"
247
+
248
+ # Validate required fields
249
+ if [[ -z "$PLATFORM" ]]; then
250
+ log_error "Invalid config: missing platform"
251
+ return 1
252
+ fi
253
+
254
+ return 0
255
+ }
256
+
257
+ # setup_windows_env
258
+ # Sets up Windows-specific environment variables and PATH.
259
+ # Call this after load_forge_config on Windows.
260
+ setup_windows_env() {
261
+ if [[ "$PLATFORM" != "windows" ]]; then
262
+ return 0
263
+ fi
264
+
265
+ # Export Git Bash path for Claude Code
266
+ if [[ -n "$GIT_BASH_PATH" ]]; then
267
+ # Convert forward slashes to backslashes for Windows
268
+ local git_bash_win="${GIT_BASH_PATH//\//\\}"
269
+ export CLAUDE_CODE_GIT_BASH_PATH="$git_bash_win"
270
+ fi
271
+
272
+ # Add npm global path if not already in PATH
273
+ local npm_path=""
274
+
275
+ # Try with USER variable
276
+ if [[ -n "$USER" ]]; then
277
+ npm_path="/c/Users/$USER/AppData/Roaming/npm"
278
+ fi
279
+
280
+ # Try with USERPROFILE
281
+ if [[ -z "$npm_path" || ! -d "$npm_path" ]] && [[ -n "$USERPROFILE" ]]; then
282
+ npm_path="${USERPROFILE//\\//}/AppData/Roaming/npm"
283
+ fi
284
+
285
+ # Add to PATH if exists and not already there
286
+ if [[ -n "$npm_path" && -d "$npm_path" && ":$PATH:" != *":$npm_path:"* ]]; then
287
+ export PATH="$npm_path:$PATH"
288
+ fi
289
+ }
290
+
291
+ # require_forge_config FORGE_ROOT
292
+ # Loads config and exits with error if not initialized.
293
+ # Also applies local overrides from .forge/config.local.json if present.
294
+ # config.local.json is gitignored and safe for per-developer settings
295
+ # (e.g. custom terminal path, personal daemon preferences).
296
+ require_forge_config() {
297
+ local forge_root="$1"
298
+ local config_file="$forge_root/.forge/config.json"
299
+ local local_config_file="$forge_root/.forge/config.local.json"
300
+
301
+ load_forge_config "$config_file" || exit 1
302
+
303
+ # Apply local overrides if present (not committed, per-developer)
304
+ if [[ -f "$local_config_file" ]]; then
305
+ local local_terminal local_git_bash local_daemon local_loop
306
+ local_terminal=$(json_get_string "$local_config_file" "terminal_type") && TERMINAL_TYPE="$local_terminal"
307
+ local_git_bash=$(json_get_string "$local_config_file" "git_bash_path") && GIT_BASH_PATH="$local_git_bash"
308
+ local_daemon=$(json_get_string "$local_config_file" "daemon_enabled") && DAEMON_ENABLED="$local_daemon"
309
+ local_loop=$(json_get_string "$local_config_file" "worker_loop_enabled") && WORKER_LOOP_ENABLED="$local_loop"
310
+ fi
311
+
312
+ setup_windows_env
313
+ }
314
+
315
+ # write_json_config FILE KEY VALUE
316
+ # Safely writes/updates a key in a JSON config file.
317
+ # Creates file if it doesn't exist.
318
+ #
319
+ # SECURITY: File, key, and value are passed as command-line arguments, not interpolated.
320
+ write_json_config() {
321
+ local file="$1"
322
+ local key="$2"
323
+ local value="$3"
324
+
325
+ # Use Node.js for safe JSON manipulation
326
+ # SECURITY: Pass all values as arguments to avoid injection
327
+ if command -v node &>/dev/null; then
328
+ node -e '
329
+ const fs = require("fs");
330
+ const file = process.argv[1];
331
+ const key = process.argv[2];
332
+ const value = process.argv[3];
333
+ let data = {};
334
+ try {
335
+ if (fs.existsSync(file)) {
336
+ data = JSON.parse(fs.readFileSync(file, "utf8"));
337
+ }
338
+ } catch (e) {}
339
+ data[key] = value;
340
+ fs.writeFileSync(file, JSON.stringify(data, null, 2) + "\n");
341
+ ' -- "$file" "$key" "$value" 2>/dev/null
342
+ return $?
343
+ fi
344
+
345
+ log_error "Node.js required for config writing"
346
+ return 1
347
+ }