vibe-forge 0.4.0 → 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 (129) 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 -102
  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 -187
  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 -232
  13. package/agents/aegis/personality.md +303 -269
  14. package/agents/anvil/personality.md +278 -240
  15. package/agents/architect/personality.md +260 -234
  16. package/agents/crucible/personality.md +362 -309
  17. package/agents/crucible-x/personality.md +210 -0
  18. package/agents/ember/personality.md +293 -265
  19. package/agents/flux/personality.md +248 -0
  20. package/agents/furnace/personality.md +342 -291
  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 -251
  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 -851
  38. package/bin/forge-setup.sh +661 -645
  39. package/bin/forge-spawn.sh +164 -164
  40. package/bin/forge.cmd +83 -83
  41. package/bin/forge.sh +566 -387
  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 -313
  46. package/bin/lib/constants.sh +241 -206
  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 -305
  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 -258
  59. package/bin/lib/terminal.js +452 -446
  60. package/bin/lib/util.sh +126 -126
  61. package/bin/lib/vcs.js +349 -349
  62. package/config/agent-manifest.yaml +237 -243
  63. package/config/agents.json +207 -132
  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 -409
  72. package/docs/architecture.md +194 -162
  73. package/docs/commands.md +451 -388
  74. package/docs/security.md +195 -144
  75. package/package.json +77 -50
  76. package/.claude/settings.local.json +0 -33
  77. package/agents/forge-master/capabilities.md +0 -144
  78. package/agents/forge-master/context-template.md +0 -128
  79. package/agents/forge-master/personality.md +0 -138
  80. package/agents/sentinel/personality.md +0 -194
  81. package/context/forge-state.yaml +0 -19
  82. package/docs/TODO.md +0 -150
  83. package/docs/getting-started.md +0 -243
  84. package/docs/npm-publishing.md +0 -95
  85. package/docs/workflows/README.md +0 -32
  86. package/docs/workflows/azure-devops.md +0 -108
  87. package/docs/workflows/bitbucket.md +0 -104
  88. package/docs/workflows/git-only.md +0 -130
  89. package/docs/workflows/gitea.md +0 -168
  90. package/docs/workflows/github.md +0 -103
  91. package/docs/workflows/gitlab.md +0 -105
  92. package/docs/workflows.md +0 -454
  93. package/tasks/completed/ARCH-001-duplicate-agent-config.md +0 -121
  94. package/tasks/completed/ARCH-002-mixed-bash-node-implementation.md +0 -88
  95. package/tasks/completed/ARCH-003-worker-loop-hook-duplication.md +0 -77
  96. package/tasks/completed/ARCH-009-test-organization.md +0 -78
  97. package/tasks/completed/ARCH-011-jq-vs-nodejs-json.md +0 -94
  98. package/tasks/completed/ARCH-012-tmp-files-in-root.md +0 -71
  99. package/tasks/completed/ARCH-013-exit-code-constants.md +0 -65
  100. package/tasks/completed/ARCH-014-sed-incompatibility.md +0 -96
  101. package/tasks/completed/ARCH-015-docs-todo-tracking.md +0 -83
  102. package/tasks/completed/CLEAN-001.md +0 -38
  103. package/tasks/completed/CLEAN-003.md +0 -47
  104. package/tasks/completed/CLEAN-004.md +0 -56
  105. package/tasks/completed/CLEAN-005.md +0 -75
  106. package/tasks/completed/CLEAN-006.md +0 -47
  107. package/tasks/completed/CLEAN-007.md +0 -34
  108. package/tasks/completed/CLEAN-008.md +0 -49
  109. package/tasks/completed/CLEAN-012.md +0 -58
  110. package/tasks/completed/CLEAN-013.md +0 -45
  111. package/tasks/completed/SEC-001-sql-injection-fix.md +0 -58
  112. package/tasks/completed/SEC-002-notification-injection-fix.md +0 -45
  113. package/tasks/completed/SEC-003-eval-injection-fix.md +0 -54
  114. package/tasks/completed/SEC-004-pid-race-condition-fix.md +0 -49
  115. package/tasks/completed/SEC-005-worker-loop-path-fix.md +0 -51
  116. package/tasks/completed/SEC-006-eval-agent-names.md +0 -55
  117. package/tasks/completed/SEC-007-spawn-escaping.md +0 -67
  118. package/tasks/pending/ARCH-004-git-bash-detection-duplication.md +0 -72
  119. package/tasks/pending/ARCH-005-missing-src-directory.md +0 -95
  120. package/tasks/pending/ARCH-006-task-template-location.md +0 -64
  121. package/tasks/pending/ARCH-007-daemon-monolith.md +0 -91
  122. package/tasks/pending/ARCH-008-forge-master-vs-hub.md +0 -81
  123. package/tasks/pending/ARCH-010-missing-index-files.md +0 -84
  124. package/tasks/pending/CLEAN-002.md +0 -29
  125. package/tasks/pending/CLEAN-009.md +0 -31
  126. package/tasks/pending/CLEAN-010.md +0 -30
  127. package/tasks/pending/CLEAN-011.md +0 -30
  128. package/tasks/pending/CLEAN-014.md +0 -32
  129. package/tasks/review/task-001.md +0 -78
package/bin/lib/json.sh CHANGED
@@ -1,258 +1,264 @@
1
- #!/usr/bin/env bash
2
- #
3
- # Vibe Forge - JSON Utilities (Node.js based)
4
- #
5
- # Provides safe JSON operations using Node.js instead of jq.
6
- # This standardizes JSON handling across the codebase and removes
7
- # the implicit jq dependency.
8
- #
9
- # SECURITY: All functions pass values as command-line arguments,
10
- # never interpolated into the Node.js code.
11
- #
12
-
13
- # =============================================================================
14
- # JSON Reading Functions
15
- # =============================================================================
16
-
17
- # json_read FILE KEY [DEFAULT]
18
- # Reads a single key from a JSON file.
19
- # Returns the value or default (empty string if not provided).
20
- #
21
- # Example: value=$(json_read config.json "name" "unknown")
22
- json_read() {
23
- local file="$1"
24
- local key="$2"
25
- local default="${3:-}"
26
-
27
- if [[ ! -f "$file" ]]; then
28
- echo "$default"
29
- return 1
30
- fi
31
-
32
- local result
33
- result=$(node -e '
34
- const fs = require("fs");
35
- const file = process.argv[1];
36
- const key = process.argv[2];
37
- const defaultVal = process.argv[3] || "";
38
- try {
39
- const data = JSON.parse(fs.readFileSync(file, "utf8"));
40
- const value = data[key];
41
- if (value !== undefined && value !== null) {
42
- console.log(String(value));
43
- } else {
44
- console.log(defaultVal);
45
- }
46
- } catch (e) {
47
- console.log(defaultVal);
48
- process.exit(1);
49
- }
50
- ' -- "$file" "$key" "$default" 2>/dev/null)
51
-
52
- echo "$result"
53
- }
54
-
55
- # json_read_multi FILE KEY1 KEY2 ...
56
- # Reads multiple keys from a JSON file efficiently (single Node.js call).
57
- # Outputs tab-separated values in the order of keys provided.
58
- # Missing keys return empty string.
59
- #
60
- # Example: read -r name status task <<< "$(json_read_multi file.json name status task)"
61
- json_read_multi() {
62
- local file="$1"
63
- shift
64
- local keys=("$@")
65
-
66
- if [[ ! -f "$file" ]]; then
67
- # Output empty strings for each key
68
- local empty=""
69
- for ((i=0; i<${#keys[@]}; i++)); do
70
- empty+="\t"
71
- done
72
- echo "${empty:1}" # Remove leading tab
73
- return 1
74
- fi
75
-
76
- # Pass keys as JSON array for clean parsing
77
- local keys_json
78
- keys_json=$(printf '%s\n' "${keys[@]}" | node -e '
79
- const lines = require("fs").readFileSync(0, "utf8").trim().split("\n");
80
- console.log(JSON.stringify(lines));
81
- ')
82
-
83
- node -e '
84
- const fs = require("fs");
85
- const file = process.argv[1];
86
- const keys = JSON.parse(process.argv[2]);
87
- try {
88
- const data = JSON.parse(fs.readFileSync(file, "utf8"));
89
- const values = keys.map(k => {
90
- const v = data[k];
91
- return (v !== undefined && v !== null) ? String(v) : "";
92
- });
93
- console.log(values.join("\t"));
94
- } catch (e) {
95
- console.log(keys.map(() => "").join("\t"));
96
- process.exit(1);
97
- }
98
- ' -- "$file" "$keys_json" 2>/dev/null
99
- }
100
-
101
- # json_read_all FILE
102
- # Reads all keys from a JSON file and outputs key=value lines.
103
- # Useful for loading all config values at once.
104
- #
105
- # Example: eval "$(json_read_all config.json | sed 's/^/export /')"
106
- json_read_all() {
107
- local file="$1"
108
-
109
- if [[ ! -f "$file" ]]; then
110
- return 1
111
- fi
112
-
113
- node -e '
114
- const fs = require("fs");
115
- const file = process.argv[1];
116
- try {
117
- const data = JSON.parse(fs.readFileSync(file, "utf8"));
118
- for (const [key, value] of Object.entries(data)) {
119
- if (typeof value !== "object") {
120
- // Escape for shell: single quotes, backslashes
121
- const escaped = String(value).replace(/\\/g, "\\\\").replace(/"/g, "\\\"");
122
- console.log(`${key}="${escaped}"`);
123
- }
124
- }
125
- } catch (e) {
126
- process.exit(1);
127
- }
128
- ' -- "$file" 2>/dev/null
129
- }
130
-
131
- # =============================================================================
132
- # JSON Writing Functions
133
- # =============================================================================
134
-
135
- # json_write FILE KEY VALUE
136
- # Writes/updates a single key in a JSON file.
137
- # Creates the file if it does not exist.
138
- #
139
- # Example: json_write config.json "enabled" "true"
140
- json_write() {
141
- local file="$1"
142
- local key="$2"
143
- local value="$3"
144
-
145
- node -e '
146
- const fs = require("fs");
147
- const file = process.argv[1];
148
- const key = process.argv[2];
149
- const value = process.argv[3];
150
- let data = {};
151
- try {
152
- if (fs.existsSync(file)) {
153
- data = JSON.parse(fs.readFileSync(file, "utf8"));
154
- }
155
- } catch (e) {}
156
-
157
- // Try to parse value as boolean or number
158
- if (value === "true") {
159
- data[key] = true;
160
- } else if (value === "false") {
161
- data[key] = false;
162
- } else if (value === "null") {
163
- data[key] = null;
164
- } else if (!isNaN(value) && value.trim() !== "") {
165
- data[key] = Number(value);
166
- } else {
167
- data[key] = value;
168
- }
169
-
170
- fs.writeFileSync(file, JSON.stringify(data, null, 2) + "\n");
171
- ' -- "$file" "$key" "$value" 2>/dev/null
172
- }
173
-
174
- # json_write_bool FILE KEY BOOL_VALUE
175
- # Writes a boolean value (ensures proper true/false, not string).
176
- #
177
- # Example: json_write_bool config.json "enabled" true
178
- json_write_bool() {
179
- local file="$1"
180
- local key="$2"
181
- local value="$3"
182
-
183
- # Normalize to "true" or "false"
184
- case "$value" in
185
- true|1|on|yes|enabled) value="true" ;;
186
- *) value="false" ;;
187
- esac
188
-
189
- node -e '
190
- const fs = require("fs");
191
- const file = process.argv[1];
192
- const key = process.argv[2];
193
- const value = process.argv[3] === "true";
194
- let data = {};
195
- try {
196
- if (fs.existsSync(file)) {
197
- data = JSON.parse(fs.readFileSync(file, "utf8"));
198
- }
199
- } catch (e) {}
200
- data[key] = value;
201
- fs.writeFileSync(file, JSON.stringify(data, null, 2) + "\n");
202
- ' -- "$file" "$key" "$value" 2>/dev/null
203
- }
204
-
205
- # =============================================================================
206
- # JSON Display Functions
207
- # =============================================================================
208
-
209
- # json_pretty FILE
210
- # Pretty-prints a JSON file with 2-space indentation.
211
- #
212
- # Example: json_pretty config.json
213
- json_pretty() {
214
- local file="$1"
215
-
216
- if [[ ! -f "$file" ]]; then
217
- echo "{}"
218
- return 1
219
- fi
220
-
221
- node -e '
222
- const fs = require("fs");
223
- const file = process.argv[1];
224
- try {
225
- const data = JSON.parse(fs.readFileSync(file, "utf8"));
226
- console.log(JSON.stringify(data, null, 2));
227
- } catch (e) {
228
- console.error("Invalid JSON:", e.message);
229
- process.exit(1);
230
- }
231
- ' -- "$file" 2>/dev/null
232
- }
233
-
234
- # json_has_key FILE KEY
235
- # Checks if a key exists in a JSON file.
236
- # Returns 0 if key exists, 1 otherwise.
237
- #
238
- # Example: if json_has_key config.json "enabled"; then ...
239
- json_has_key() {
240
- local file="$1"
241
- local key="$2"
242
-
243
- if [[ ! -f "$file" ]]; then
244
- return 1
245
- fi
246
-
247
- node -e '
248
- const fs = require("fs");
249
- const file = process.argv[1];
250
- const key = process.argv[2];
251
- try {
252
- const data = JSON.parse(fs.readFileSync(file, "utf8"));
253
- process.exit(key in data ? 0 : 1);
254
- } catch (e) {
255
- process.exit(1);
256
- }
257
- ' -- "$file" "$key" 2>/dev/null
258
- }
1
+ #!/usr/bin/env bash
2
+ #
3
+ # Vibe Forge - JSON Utilities (Node.js based)
4
+ #
5
+ # Provides safe JSON operations using Node.js instead of jq.
6
+ # This standardizes JSON handling across the codebase and removes
7
+ # the implicit jq dependency.
8
+ #
9
+ # SECURITY: All functions pass values as command-line arguments,
10
+ # never interpolated into the Node.js code.
11
+ #
12
+
13
+ # =============================================================================
14
+ # JSON Reading Functions
15
+ # =============================================================================
16
+
17
+ # json_read FILE KEY [DEFAULT]
18
+ # Reads a single key from a JSON file.
19
+ # Returns the value or default (empty string if not provided).
20
+ #
21
+ # Example: value=$(json_read config.json "name" "unknown")
22
+ json_read() {
23
+ local file="$1"
24
+ local key="$2"
25
+ local default="${3:-}"
26
+
27
+ if [[ ! -f "$file" ]]; then
28
+ echo "$default"
29
+ return 1
30
+ fi
31
+
32
+ local result
33
+ result=$(node -e '
34
+ const fs = require("fs");
35
+ const file = process.argv[1];
36
+ const key = process.argv[2];
37
+ const defaultVal = process.argv[3] || "";
38
+ try {
39
+ const data = JSON.parse(fs.readFileSync(file, "utf8"));
40
+ const value = data[key];
41
+ if (value !== undefined && value !== null) {
42
+ console.log(String(value));
43
+ } else {
44
+ console.log(defaultVal);
45
+ }
46
+ } catch (e) {
47
+ console.log(defaultVal);
48
+ process.exit(1);
49
+ }
50
+ ' -- "$file" "$key" "$default" 2>/dev/null)
51
+
52
+ echo "$result"
53
+ }
54
+
55
+ # json_read_multi FILE KEY1 KEY2 ...
56
+ # Reads multiple keys from a JSON file efficiently (single Node.js call).
57
+ # Outputs tab-separated values in the order of keys provided.
58
+ # Missing keys return empty string.
59
+ #
60
+ # Example: read -r name status task <<< "$(json_read_multi file.json name status task)"
61
+ json_read_multi() {
62
+ local file="$1"
63
+ shift
64
+ local keys=("$@")
65
+
66
+ if [[ ! -f "$file" ]]; then
67
+ # Output empty strings for each key
68
+ local empty=""
69
+ for ((i=0; i<${#keys[@]}; i++)); do
70
+ empty+="\t"
71
+ done
72
+ echo "${empty:1}" # Remove leading tab
73
+ return 1
74
+ fi
75
+
76
+ # Pass keys as JSON array for clean parsing
77
+ local keys_json
78
+ keys_json=$(printf '%s\n' "${keys[@]}" | node -e '
79
+ const lines = require("fs").readFileSync(0, "utf8").trim().split("\n");
80
+ console.log(JSON.stringify(lines));
81
+ ')
82
+
83
+ node -e '
84
+ const fs = require("fs");
85
+ const file = process.argv[1];
86
+ const keys = JSON.parse(process.argv[2]);
87
+ try {
88
+ const data = JSON.parse(fs.readFileSync(file, "utf8"));
89
+ const values = keys.map(k => {
90
+ const v = data[k];
91
+ return (v !== undefined && v !== null) ? String(v) : "";
92
+ });
93
+ console.log(values.join("\t"));
94
+ } catch (e) {
95
+ console.log(keys.map(() => "").join("\t"));
96
+ process.exit(1);
97
+ }
98
+ ' -- "$file" "$keys_json" 2>/dev/null
99
+ }
100
+
101
+ # json_read_all FILE
102
+ # Reads all keys from a JSON file and outputs key=value lines.
103
+ # Useful for loading all config values at once.
104
+ #
105
+ # SECURITY WARNING: Do NOT use this function with eval on untrusted JSON files.
106
+ # JSON key names are NOT validated or escaped - a malicious key name like
107
+ # "FOO=1; evil_command; export BAR" would result in arbitrary code execution.
108
+ # Only use with fully trusted, version-controlled config files.
109
+ #
110
+ # Safe usage (read individual values): json_read FILE KEY
111
+ # Unsafe pattern to avoid: eval "$(json_read_all untrusted.json | sed 's/^/export /')"
112
+ json_read_all() {
113
+ local file="$1"
114
+
115
+ if [[ ! -f "$file" ]]; then
116
+ return 1
117
+ fi
118
+
119
+ node -e '
120
+ const fs = require("fs");
121
+ const file = process.argv[1];
122
+ try {
123
+ const data = JSON.parse(fs.readFileSync(file, "utf8"));
124
+ for (const [key, value] of Object.entries(data)) {
125
+ if (typeof value !== "object") {
126
+ // Escape for shell: single quotes, backslashes
127
+ const escaped = String(value).replace(/\\/g, "\\\\").replace(/"/g, "\\\"");
128
+ console.log(`${key}="${escaped}"`);
129
+ }
130
+ }
131
+ } catch (e) {
132
+ process.exit(1);
133
+ }
134
+ ' -- "$file" 2>/dev/null
135
+ }
136
+
137
+ # =============================================================================
138
+ # JSON Writing Functions
139
+ # =============================================================================
140
+
141
+ # json_write FILE KEY VALUE
142
+ # Writes/updates a single key in a JSON file.
143
+ # Creates the file if it does not exist.
144
+ #
145
+ # Example: json_write config.json "enabled" "true"
146
+ json_write() {
147
+ local file="$1"
148
+ local key="$2"
149
+ local value="$3"
150
+
151
+ node -e '
152
+ const fs = require("fs");
153
+ const file = process.argv[1];
154
+ const key = process.argv[2];
155
+ const value = process.argv[3];
156
+ let data = {};
157
+ try {
158
+ if (fs.existsSync(file)) {
159
+ data = JSON.parse(fs.readFileSync(file, "utf8"));
160
+ }
161
+ } catch (e) {}
162
+
163
+ // Try to parse value as boolean or number
164
+ if (value === "true") {
165
+ data[key] = true;
166
+ } else if (value === "false") {
167
+ data[key] = false;
168
+ } else if (value === "null") {
169
+ data[key] = null;
170
+ } else if (!isNaN(value) && value.trim() !== "") {
171
+ data[key] = Number(value);
172
+ } else {
173
+ data[key] = value;
174
+ }
175
+
176
+ fs.writeFileSync(file, JSON.stringify(data, null, 2) + "\n");
177
+ ' -- "$file" "$key" "$value" 2>/dev/null
178
+ }
179
+
180
+ # json_write_bool FILE KEY BOOL_VALUE
181
+ # Writes a boolean value (ensures proper true/false, not string).
182
+ #
183
+ # Example: json_write_bool config.json "enabled" true
184
+ json_write_bool() {
185
+ local file="$1"
186
+ local key="$2"
187
+ local value="$3"
188
+
189
+ # Normalize to "true" or "false"
190
+ case "$value" in
191
+ true|1|on|yes|enabled) value="true" ;;
192
+ *) value="false" ;;
193
+ esac
194
+
195
+ node -e '
196
+ const fs = require("fs");
197
+ const file = process.argv[1];
198
+ const key = process.argv[2];
199
+ const value = process.argv[3] === "true";
200
+ let data = {};
201
+ try {
202
+ if (fs.existsSync(file)) {
203
+ data = JSON.parse(fs.readFileSync(file, "utf8"));
204
+ }
205
+ } catch (e) {}
206
+ data[key] = value;
207
+ fs.writeFileSync(file, JSON.stringify(data, null, 2) + "\n");
208
+ ' -- "$file" "$key" "$value" 2>/dev/null
209
+ }
210
+
211
+ # =============================================================================
212
+ # JSON Display Functions
213
+ # =============================================================================
214
+
215
+ # json_pretty FILE
216
+ # Pretty-prints a JSON file with 2-space indentation.
217
+ #
218
+ # Example: json_pretty config.json
219
+ json_pretty() {
220
+ local file="$1"
221
+
222
+ if [[ ! -f "$file" ]]; then
223
+ echo "{}"
224
+ return 1
225
+ fi
226
+
227
+ node -e '
228
+ const fs = require("fs");
229
+ const file = process.argv[1];
230
+ try {
231
+ const data = JSON.parse(fs.readFileSync(file, "utf8"));
232
+ console.log(JSON.stringify(data, null, 2));
233
+ } catch (e) {
234
+ console.error("Invalid JSON:", e.message);
235
+ process.exit(1);
236
+ }
237
+ ' -- "$file" 2>/dev/null
238
+ }
239
+
240
+ # json_has_key FILE KEY
241
+ # Checks if a key exists in a JSON file.
242
+ # Returns 0 if key exists, 1 otherwise.
243
+ #
244
+ # Example: if json_has_key config.json "enabled"; then ...
245
+ json_has_key() {
246
+ local file="$1"
247
+ local key="$2"
248
+
249
+ if [[ ! -f "$file" ]]; then
250
+ return 1
251
+ fi
252
+
253
+ node -e '
254
+ const fs = require("fs");
255
+ const file = process.argv[1];
256
+ const key = process.argv[2];
257
+ try {
258
+ const data = JSON.parse(fs.readFileSync(file, "utf8"));
259
+ process.exit(key in data ? 0 : 1);
260
+ } catch (e) {
261
+ process.exit(1);
262
+ }
263
+ ' -- "$file" "$key" 2>/dev/null
264
+ }