@sugar-crash-studios/vibe-forge 0.4.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 (201) hide show
  1. package/.claude/commands/clear-attention.md +63 -0
  2. package/.claude/commands/compact-context.md +52 -0
  3. package/.claude/commands/configure-vcs.md +102 -0
  4. package/.claude/commands/forge.md +171 -0
  5. package/.claude/commands/need-help.md +77 -0
  6. package/.claude/commands/update-status.md +64 -0
  7. package/.claude/commands/worker-loop.md +106 -0
  8. package/.claude/hooks/worker-loop.js +198 -0
  9. package/.claude/scripts/setup-worker-loop.sh +45 -0
  10. package/.claude/settings.local.json +46 -0
  11. package/LICENSE +21 -0
  12. package/README.md +238 -0
  13. package/agents/aegis/personality.md +294 -0
  14. package/agents/anvil/personality.md +276 -0
  15. package/agents/architect/personality.md +258 -0
  16. package/agents/crucible/personality.md +360 -0
  17. package/agents/ember/personality.md +291 -0
  18. package/agents/forge-master/capabilities.md +144 -0
  19. package/agents/forge-master/context-template.md +128 -0
  20. package/agents/forge-master/personality.md +138 -0
  21. package/agents/furnace/personality.md +340 -0
  22. package/agents/herald/personality.md +247 -0
  23. package/agents/loki/personality.md +108 -0
  24. package/agents/oracle/personality.md +283 -0
  25. package/agents/pixel/personality.md +113 -0
  26. package/agents/planning-hub/personality.md +320 -0
  27. package/agents/scribe/personality.md +251 -0
  28. package/agents/temper/personality.md +218 -0
  29. package/bin/cli.js +375 -0
  30. package/bin/dashboard/api/agents.js +333 -0
  31. package/bin/dashboard/api/dispatch.js +483 -0
  32. package/bin/dashboard/api/tasks.js +416 -0
  33. package/bin/dashboard/frontend/index.html +13 -0
  34. package/bin/dashboard/frontend/package.json +16 -0
  35. package/bin/dashboard/frontend/src/App.svelte +222 -0
  36. package/bin/dashboard/frontend/src/app.css +1777 -0
  37. package/bin/dashboard/frontend/src/lib/components/AgentCard.svelte +60 -0
  38. package/bin/dashboard/frontend/src/lib/components/AgentsPanel.svelte +57 -0
  39. package/bin/dashboard/frontend/src/lib/components/DispatchModal.svelte +180 -0
  40. package/bin/dashboard/frontend/src/lib/components/Footer.svelte +33 -0
  41. package/bin/dashboard/frontend/src/lib/components/Header.svelte +84 -0
  42. package/bin/dashboard/frontend/src/lib/components/IssueCard.svelte +33 -0
  43. package/bin/dashboard/frontend/src/lib/components/IssuesPanel.svelte +73 -0
  44. package/bin/dashboard/frontend/src/lib/components/KeyboardShortcutsModal.svelte +108 -0
  45. package/bin/dashboard/frontend/src/lib/components/MobileTabs.svelte +52 -0
  46. package/bin/dashboard/frontend/src/lib/components/NotificationCard.svelte +60 -0
  47. package/bin/dashboard/frontend/src/lib/components/NotificationsPanel.svelte +44 -0
  48. package/bin/dashboard/frontend/src/lib/components/TaskCard.svelte +63 -0
  49. package/bin/dashboard/frontend/src/lib/components/TasksPanel.svelte +82 -0
  50. package/bin/dashboard/frontend/src/lib/components/Toast.svelte +45 -0
  51. package/bin/dashboard/frontend/src/lib/stores/agents.js +34 -0
  52. package/bin/dashboard/frontend/src/lib/stores/issues.js +54 -0
  53. package/bin/dashboard/frontend/src/lib/stores/notifications.js +48 -0
  54. package/bin/dashboard/frontend/src/lib/stores/tasks.js +63 -0
  55. package/bin/dashboard/frontend/src/lib/stores/theme.js +33 -0
  56. package/bin/dashboard/frontend/src/lib/stores/toast.js +35 -0
  57. package/bin/dashboard/frontend/src/lib/stores/ui.js +25 -0
  58. package/bin/dashboard/frontend/src/lib/stores/voice.js +275 -0
  59. package/bin/dashboard/frontend/src/lib/stores/websocket.js +295 -0
  60. package/bin/dashboard/frontend/src/lib/utils/api.js +101 -0
  61. package/bin/dashboard/frontend/src/lib/utils/formatters.js +54 -0
  62. package/bin/dashboard/frontend/src/main.js +9 -0
  63. package/bin/dashboard/frontend/svelte.config.js +5 -0
  64. package/bin/dashboard/frontend/vite.config.js +20 -0
  65. package/bin/dashboard/public/assets/index-DnfVj9Ce.css +1 -0
  66. package/bin/dashboard/public/assets/index-Ze5h0kXQ.js +2 -0
  67. package/bin/dashboard/public/index.html +14 -0
  68. package/bin/dashboard/server.js +566 -0
  69. package/bin/forge-daemon.sh +463 -0
  70. package/bin/forge-setup.sh +645 -0
  71. package/bin/forge-spawn.sh +164 -0
  72. package/bin/forge.cmd +83 -0
  73. package/bin/forge.sh +533 -0
  74. package/bin/lib/agents.sh +177 -0
  75. package/bin/lib/colors.sh +44 -0
  76. package/bin/lib/config.sh +347 -0
  77. package/bin/lib/constants.sh +241 -0
  78. package/bin/lib/daemon/display.sh +128 -0
  79. package/bin/lib/daemon/notifications.sh +263 -0
  80. package/bin/lib/daemon/routing.sh +77 -0
  81. package/bin/lib/daemon/state.sh +115 -0
  82. package/bin/lib/daemon/sync.sh +95 -0
  83. package/bin/lib/database.sh +310 -0
  84. package/bin/lib/heimdall-setup.js +113 -0
  85. package/bin/lib/heimdall.js +265 -0
  86. package/bin/lib/json.sh +264 -0
  87. package/bin/lib/terminal.js +451 -0
  88. package/bin/lib/util.sh +126 -0
  89. package/bin/lib/vcs.js +349 -0
  90. package/config/agent-manifest.yaml +203 -0
  91. package/config/agents.json +168 -0
  92. package/config/task-template.md +159 -0
  93. package/config/task-types.yaml +106 -0
  94. package/context/agent-status/aegis.json +7 -0
  95. package/context/agent-status/anvil.json +7 -0
  96. package/context/agent-status/architect.json +7 -0
  97. package/context/agent-status/crucible.json +7 -0
  98. package/context/agent-status/ember.json +7 -0
  99. package/context/agent-status/furnace.json +7 -0
  100. package/context/agent-status/loki.json +7 -0
  101. package/context/agent-status/oracle.json +7 -0
  102. package/context/agent-status/pixel.json +7 -0
  103. package/context/agent-status/planning-hub.json +7 -0
  104. package/context/agent-status/scribe.json +7 -0
  105. package/context/agent-status/temper.json +7 -0
  106. package/context/feature-brainstorm.md +426 -0
  107. package/context/forge-state.yaml +19 -0
  108. package/context/modern-conventions.md +129 -0
  109. package/context/project-context-template.md +122 -0
  110. package/context/project-context.md +122 -0
  111. package/docs/TODO.md +150 -0
  112. package/docs/agents.md +409 -0
  113. package/docs/architecture/decisions/ADR-001-daemon-modularization.md +122 -0
  114. package/docs/architecture/vibe-lab-integration.md +684 -0
  115. package/docs/architecture.md +194 -0
  116. package/docs/bmad-gap-analysis-2026-03-31.md +444 -0
  117. package/docs/cleanup-workflow.md +329 -0
  118. package/docs/commands.md +451 -0
  119. package/docs/dashboard-mockup.html +989 -0
  120. package/docs/getting-started.md +261 -0
  121. package/docs/integration/forge-ownership-policy.md +112 -0
  122. package/docs/npm-publishing.md +132 -0
  123. package/docs/roadmap-2026.md +519 -0
  124. package/docs/security.md +144 -0
  125. package/docs/wireframes/dashboard-mvp.md +1164 -0
  126. package/docs/workflows/README.md +32 -0
  127. package/docs/workflows/azure-devops.md +108 -0
  128. package/docs/workflows/bitbucket.md +104 -0
  129. package/docs/workflows/git-only.md +130 -0
  130. package/docs/workflows/gitea.md +168 -0
  131. package/docs/workflows/github.md +103 -0
  132. package/docs/workflows/gitlab.md +105 -0
  133. package/docs/workflows.md +454 -0
  134. package/package.json +73 -0
  135. package/tasks/completed/ARCH-001-duplicate-agent-config.md +121 -0
  136. package/tasks/completed/ARCH-002-mixed-bash-node-implementation.md +88 -0
  137. package/tasks/completed/ARCH-003-worker-loop-hook-duplication.md +77 -0
  138. package/tasks/completed/ARCH-009-test-organization.md +78 -0
  139. package/tasks/completed/ARCH-011-jq-vs-nodejs-json.md +94 -0
  140. package/tasks/completed/ARCH-012-tmp-files-in-root.md +71 -0
  141. package/tasks/completed/ARCH-013-exit-code-constants.md +65 -0
  142. package/tasks/completed/ARCH-014-sed-incompatibility.md +96 -0
  143. package/tasks/completed/ARCH-015-docs-todo-tracking.md +83 -0
  144. package/tasks/completed/BUG-dash-001-tasks-filter-error.md +31 -0
  145. package/tasks/completed/BUG-dash-002-agents-unknown.md +41 -0
  146. package/tasks/completed/CLEAN-001.md +38 -0
  147. package/tasks/completed/CLEAN-002.md +43 -0
  148. package/tasks/completed/CLEAN-003.md +47 -0
  149. package/tasks/completed/CLEAN-004.md +56 -0
  150. package/tasks/completed/CLEAN-005.md +75 -0
  151. package/tasks/completed/CLEAN-006.md +47 -0
  152. package/tasks/completed/CLEAN-007.md +34 -0
  153. package/tasks/completed/CLEAN-008.md +49 -0
  154. package/tasks/completed/CLEAN-012.md +58 -0
  155. package/tasks/completed/CLEAN-013.md +45 -0
  156. package/tasks/completed/FEATURE-001a-dashboard-wireframes.md +162 -0
  157. package/tasks/completed/IMPL-007a-daemon-notifications-module.md +82 -0
  158. package/tasks/completed/IMPL-007b-daemon-sync-module.md +71 -0
  159. package/tasks/completed/IMPL-007c-daemon-state-module.md +80 -0
  160. package/tasks/completed/IMPL-007d-daemon-routing-module.md +77 -0
  161. package/tasks/completed/IMPL-007e-daemon-display-module.md +77 -0
  162. package/tasks/completed/IMPL-007f-daemon-integration.md +124 -0
  163. package/tasks/completed/PLAT-1-heimdall.md +420 -0
  164. package/tasks/completed/SEC-001-sql-injection-fix.md +58 -0
  165. package/tasks/completed/SEC-002-notification-injection-fix.md +45 -0
  166. package/tasks/completed/SEC-003-eval-injection-fix.md +54 -0
  167. package/tasks/completed/SEC-004-pid-race-condition-fix.md +49 -0
  168. package/tasks/completed/SEC-005-worker-loop-path-fix.md +51 -0
  169. package/tasks/completed/SEC-006-eval-agent-names.md +55 -0
  170. package/tasks/completed/SEC-007-spawn-escaping.md +67 -0
  171. package/tasks/completed/TASK-DASH-001-server-infrastructure.md +185 -0
  172. package/tasks/completed/TASK-anvil-001-dashboard-frontend.md +133 -0
  173. package/tasks/completed/review-bmad-aegis.md +89 -0
  174. package/tasks/completed/review-bmad-anvil.md +80 -0
  175. package/tasks/completed/review-bmad-crucible.md +81 -0
  176. package/tasks/completed/review-bmad-ember.md +90 -0
  177. package/tasks/completed/review-bmad-furnace.md +79 -0
  178. package/tasks/completed/review-bmad-pixel.md +82 -0
  179. package/tasks/completed/review-bmad-scribe.md +92 -0
  180. package/tasks/completed/review-bmad-sentinel.md +83 -0
  181. package/tasks/pending/ARCH-004-git-bash-detection-duplication.md +72 -0
  182. package/tasks/pending/ARCH-005-missing-src-directory.md +95 -0
  183. package/tasks/pending/ARCH-006-task-template-location.md +64 -0
  184. package/tasks/pending/ARCH-008-forge-master-vs-hub.md +81 -0
  185. package/tasks/pending/ARCH-010-missing-index-files.md +84 -0
  186. package/tasks/pending/CLEAN-009.md +31 -0
  187. package/tasks/pending/CLEAN-010.md +30 -0
  188. package/tasks/pending/CLEAN-011.md +30 -0
  189. package/tasks/pending/CLEAN-014.md +32 -0
  190. package/tasks/pending/DESIGN-dash-001-layout-review.md +45 -0
  191. package/tasks/pending/FEATURE-001-dashboard-mvp.md +268 -0
  192. package/tasks/review/ARCH-007-daemon-monolith.md +162 -0
  193. package/tasks/review/bmad-review-aegis.md +349 -0
  194. package/tasks/review/bmad-review-anvil.md +259 -0
  195. package/tasks/review/bmad-review-crucible.md +277 -0
  196. package/tasks/review/bmad-review-ember.md +307 -0
  197. package/tasks/review/bmad-review-furnace.md +285 -0
  198. package/tasks/review/bmad-review-pixel.md +329 -0
  199. package/tasks/review/bmad-review-scribe.md +361 -0
  200. package/tasks/review/bmad-review-sentinel.md +242 -0
  201. package/tasks/review/task-001.md +78 -0
@@ -0,0 +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
+ # 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
+ }
@@ -0,0 +1,451 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Vibe Forge - Cross-Platform Terminal Detection & Spawning
4
+ *
5
+ * Detects available terminals and spawns new tabs/windows for worker agents.
6
+ *
7
+ * Supported terminals:
8
+ * Windows: Windows Terminal, PowerShell
9
+ * macOS: Terminal.app, iTerm2, Kitty
10
+ * Linux: gnome-terminal, konsole, kitty, xterm
11
+ *
12
+ * Usage:
13
+ * node terminal.js detect List available terminals
14
+ * node terminal.js spawn <terminal> <title> <command>
15
+ */
16
+
17
+ const { execSync, spawn } = require('child_process');
18
+ const fs = require('fs');
19
+ const path = require('path');
20
+ const os = require('os');
21
+
22
+ // =============================================================================
23
+ // Terminal Definitions
24
+ // =============================================================================
25
+
26
+ const TERMINALS = {
27
+ // Windows
28
+ 'windows-terminal': {
29
+ name: 'Windows Terminal',
30
+ platform: 'win32',
31
+ detect: () => commandExists('wt') || commandExists('wt.exe'),
32
+ spawn: (title, command, opts) => spawnWindowsTerminal(title, command, opts),
33
+ },
34
+ 'powershell': {
35
+ name: 'PowerShell',
36
+ platform: 'win32',
37
+ detect: () => commandExists('pwsh') || commandExists('powershell'),
38
+ spawn: (title, command, opts) => spawnPowerShell(title, command, opts),
39
+ },
40
+
41
+ // macOS
42
+ 'terminal-app': {
43
+ name: 'Terminal.app',
44
+ platform: 'darwin',
45
+ detect: () => fs.existsSync('/System/Applications/Utilities/Terminal.app'),
46
+ spawn: (title, command, opts) => spawnTerminalApp(title, command, opts),
47
+ },
48
+ 'iterm2': {
49
+ name: 'iTerm2',
50
+ platform: 'darwin',
51
+ detect: () => fs.existsSync('/Applications/iTerm.app'),
52
+ spawn: (title, command, opts) => spawnITerm2(title, command, opts),
53
+ },
54
+ 'kitty-mac': {
55
+ name: 'Kitty',
56
+ platform: 'darwin',
57
+ detect: () => commandExists('kitty') || fs.existsSync('/Applications/kitty.app'),
58
+ spawn: (title, command, opts) => spawnKitty(title, command, opts),
59
+ },
60
+
61
+ // Linux
62
+ 'gnome-terminal': {
63
+ name: 'GNOME Terminal',
64
+ platform: 'linux',
65
+ detect: () => commandExists('gnome-terminal'),
66
+ spawn: (title, command, opts) => spawnGnomeTerminal(title, command, opts),
67
+ },
68
+ 'konsole': {
69
+ name: 'Konsole',
70
+ platform: 'linux',
71
+ detect: () => commandExists('konsole'),
72
+ spawn: (title, command, opts) => spawnKonsole(title, command, opts),
73
+ },
74
+ 'kitty-linux': {
75
+ name: 'Kitty',
76
+ platform: 'linux',
77
+ detect: () => commandExists('kitty'),
78
+ spawn: (title, command, opts) => spawnKitty(title, command, opts),
79
+ },
80
+ 'xterm': {
81
+ name: 'XTerm',
82
+ platform: 'linux',
83
+ detect: () => commandExists('xterm'),
84
+ spawn: (title, command, opts) => spawnXterm(title, command, opts),
85
+ },
86
+ };
87
+
88
+ // =============================================================================
89
+ // Detection Helpers
90
+ // =============================================================================
91
+
92
+ function commandExists(cmd) {
93
+ try {
94
+ if (os.platform() === 'win32') {
95
+ execSync(`where ${cmd}`, { stdio: 'pipe' });
96
+ } else {
97
+ execSync(`which ${cmd}`, { stdio: 'pipe' });
98
+ }
99
+ return true;
100
+ } catch {
101
+ return false;
102
+ }
103
+ }
104
+
105
+ function getPlatform() {
106
+ const p = os.platform();
107
+ if (p === 'win32') return 'win32';
108
+ if (p === 'darwin') return 'darwin';
109
+ return 'linux'; // Treat all other Unix-likes as Linux
110
+ }
111
+
112
+ function detectTerminals() {
113
+ const platform = getPlatform();
114
+ const available = [];
115
+
116
+ for (const [id, terminal] of Object.entries(TERMINALS)) {
117
+ if (terminal.platform === platform) {
118
+ try {
119
+ if (terminal.detect()) {
120
+ available.push({
121
+ id,
122
+ name: terminal.name,
123
+ platform: terminal.platform,
124
+ });
125
+ }
126
+ } catch {
127
+ // Detection failed, skip this terminal
128
+ }
129
+ }
130
+ }
131
+
132
+ return available;
133
+ }
134
+
135
+ function getBestTerminal() {
136
+ const available = detectTerminals();
137
+ return available.length > 0 ? available[0] : null;
138
+ }
139
+
140
+ // =============================================================================
141
+ // Spawn Functions - Windows
142
+ // =============================================================================
143
+
144
+ function spawnWindowsTerminal(title, command, opts = {}) {
145
+ const { cwd, tabColor, suppressApplicationTitle = true } = opts;
146
+
147
+ // Build wt arguments
148
+ const args = ['-w', '0', 'new-tab', '--title', title];
149
+
150
+ // FORGE-9: Prevent the spawned process from overwriting the tab title
151
+ if (suppressApplicationTitle) {
152
+ args.push('--suppressApplicationTitle');
153
+ }
154
+
155
+ if (tabColor) {
156
+ args.push('--tabColor', tabColor);
157
+ }
158
+
159
+ // Determine bash path
160
+ const bashPath = findGitBash();
161
+
162
+ if (bashPath) {
163
+ args.push(bashPath, '-c', command);
164
+ } else {
165
+ args.push('bash', '-c', command);
166
+ }
167
+
168
+ const child = spawn('wt', args, {
169
+ detached: true,
170
+ stdio: 'ignore',
171
+ cwd: cwd || process.cwd(),
172
+ shell: true,
173
+ });
174
+
175
+ child.unref();
176
+ return { success: true, terminal: 'windows-terminal' };
177
+ }
178
+
179
+ function spawnPowerShell(title, command, opts = {}) {
180
+ const { cwd } = opts;
181
+
182
+ // PowerShell: Start-Process to open new window
183
+ // We wrap in bash since the command expects bash
184
+ const bashPath = findGitBash();
185
+ const bashCmd = bashPath ? `& '${bashPath}' -c '${command.replace(/'/g, "''")}'` : `bash -c '${command.replace(/'/g, "''")}'`;
186
+
187
+ const psCommand = `Start-Process pwsh -ArgumentList '-NoExit', '-Command', '${bashCmd.replace(/'/g, "''")}'`;
188
+
189
+ const child = spawn('pwsh', ['-Command', psCommand], {
190
+ detached: true,
191
+ stdio: 'ignore',
192
+ cwd: cwd || process.cwd(),
193
+ shell: true,
194
+ });
195
+
196
+ child.unref();
197
+ return { success: true, terminal: 'powershell' };
198
+ }
199
+
200
+ function findGitBash() {
201
+ if (os.platform() !== 'win32') return null;
202
+
203
+ const paths = [
204
+ 'C:\\Program Files\\Git\\bin\\bash.exe',
205
+ 'C:\\Program Files (x86)\\Git\\bin\\bash.exe',
206
+ path.join(process.env.LOCALAPPDATA || '', 'Programs', 'Git', 'bin', 'bash.exe'),
207
+ ];
208
+
209
+ for (const p of paths) {
210
+ if (fs.existsSync(p)) {
211
+ return p;
212
+ }
213
+ }
214
+
215
+ // Try to find via where command
216
+ try {
217
+ const gitPath = execSync('where git', { stdio: 'pipe' }).toString().trim().split('\n')[0];
218
+ const gitDir = path.dirname(path.dirname(gitPath));
219
+ const bashPath = path.join(gitDir, 'bin', 'bash.exe');
220
+ if (fs.existsSync(bashPath)) {
221
+ return bashPath;
222
+ }
223
+ } catch {
224
+ // Ignore
225
+ }
226
+
227
+ return null;
228
+ }
229
+
230
+ // =============================================================================
231
+ // Spawn Functions - macOS
232
+ // =============================================================================
233
+
234
+ function spawnTerminalApp(title, command, opts = {}) {
235
+ const { cwd } = opts;
236
+ const workDir = cwd || process.cwd();
237
+
238
+ // AppleScript to open new Terminal tab
239
+ const script = `
240
+ tell application "Terminal"
241
+ activate
242
+ do script "cd '${workDir.replace(/'/g, "'\\''")}' && ${command.replace(/"/g, '\\"')}"
243
+ set custom title of front window to "${title}"
244
+ end tell
245
+ `;
246
+
247
+ try {
248
+ execSync(`osascript -e '${script.replace(/'/g, "'\\''")}'`, { stdio: 'pipe' });
249
+ return { success: true, terminal: 'terminal-app' };
250
+ } catch (err) {
251
+ return { success: false, error: err.message, terminal: 'terminal-app' };
252
+ }
253
+ }
254
+
255
+ function spawnITerm2(title, command, opts = {}) {
256
+ const { cwd } = opts;
257
+ const workDir = cwd || process.cwd();
258
+
259
+ // AppleScript for iTerm2
260
+ const script = `
261
+ tell application "iTerm2"
262
+ activate
263
+ tell current window
264
+ create tab with default profile
265
+ tell current session
266
+ write text "cd '${workDir.replace(/'/g, "'\\''")}' && ${command.replace(/"/g, '\\"')}"
267
+ set name to "${title}"
268
+ end tell
269
+ end tell
270
+ end tell
271
+ `;
272
+
273
+ try {
274
+ execSync(`osascript -e '${script.replace(/'/g, "'\\''")}'`, { stdio: 'pipe' });
275
+ return { success: true, terminal: 'iterm2' };
276
+ } catch (err) {
277
+ return { success: false, error: err.message, terminal: 'iterm2' };
278
+ }
279
+ }
280
+
281
+ // =============================================================================
282
+ // Spawn Functions - Linux & Cross-Platform
283
+ // =============================================================================
284
+
285
+ function spawnGnomeTerminal(title, command, opts = {}) {
286
+ const { cwd } = opts;
287
+
288
+ const child = spawn('gnome-terminal', [
289
+ '--tab',
290
+ '--title', title,
291
+ '--', 'bash', '-c', `${command}; exec bash`
292
+ ], {
293
+ detached: true,
294
+ stdio: 'ignore',
295
+ cwd: cwd || process.cwd(),
296
+ });
297
+
298
+ child.unref();
299
+ return { success: true, terminal: 'gnome-terminal' };
300
+ }
301
+
302
+ function spawnKonsole(title, command, opts = {}) {
303
+ const { cwd } = opts;
304
+
305
+ const child = spawn('konsole', [
306
+ '--new-tab',
307
+ '-p', `tabtitle=${title}`,
308
+ '-e', 'bash', '-c', `${command}; exec bash`
309
+ ], {
310
+ detached: true,
311
+ stdio: 'ignore',
312
+ cwd: cwd || process.cwd(),
313
+ });
314
+
315
+ child.unref();
316
+ return { success: true, terminal: 'konsole' };
317
+ }
318
+
319
+ function spawnKitty(title, command, opts = {}) {
320
+ const { cwd } = opts;
321
+
322
+ // Kitty can work in single-instance mode with remote control
323
+ // or spawn new windows
324
+ const child = spawn('kitty', [
325
+ '--title', title,
326
+ '--directory', cwd || process.cwd(),
327
+ 'bash', '-c', `${command}; exec bash`
328
+ ], {
329
+ detached: true,
330
+ stdio: 'ignore',
331
+ });
332
+
333
+ child.unref();
334
+ return { success: true, terminal: 'kitty' };
335
+ }
336
+
337
+ function spawnXterm(title, command, opts = {}) {
338
+ const { cwd } = opts;
339
+
340
+ const child = spawn('xterm', [
341
+ '-T', title,
342
+ '-e', 'bash', '-c', `cd '${cwd || process.cwd()}' && ${command}; exec bash`
343
+ ], {
344
+ detached: true,
345
+ stdio: 'ignore',
346
+ });
347
+
348
+ child.unref();
349
+ return { success: true, terminal: 'xterm' };
350
+ }
351
+
352
+ // =============================================================================
353
+ // Main Spawn Function
354
+ // =============================================================================
355
+
356
+ function spawnTerminal(terminalId, title, command, opts = {}) {
357
+ const terminal = TERMINALS[terminalId];
358
+
359
+ if (!terminal) {
360
+ return { success: false, error: `Unknown terminal: ${terminalId}` };
361
+ }
362
+
363
+ try {
364
+ return terminal.spawn(title, command, opts);
365
+ } catch (err) {
366
+ return { success: false, error: err.message, terminal: terminalId };
367
+ }
368
+ }
369
+
370
+ // =============================================================================
371
+ // CLI Interface
372
+ // =============================================================================
373
+
374
+ function main() {
375
+ const args = process.argv.slice(2);
376
+ const command = args[0];
377
+
378
+ switch (command) {
379
+ case 'detect': {
380
+ const terminals = detectTerminals();
381
+ console.log(JSON.stringify({ platform: getPlatform(), terminals }, null, 2));
382
+ break;
383
+ }
384
+
385
+ case 'best': {
386
+ const best = getBestTerminal();
387
+ if (best) {
388
+ console.log(JSON.stringify(best, null, 2));
389
+ } else {
390
+ console.log(JSON.stringify({ error: 'No supported terminal found' }));
391
+ process.exit(1);
392
+ }
393
+ break;
394
+ }
395
+
396
+ case 'spawn': {
397
+ const terminalId = args[1];
398
+ const title = args[2];
399
+ const cmd = args[3];
400
+ const cwd = args[4];
401
+ const tabColor = args[5];
402
+
403
+ if (!terminalId || !title || !cmd) {
404
+ console.error('Usage: terminal.js spawn <terminal-id> <title> <command> [cwd] [tabColor]');
405
+ process.exit(1);
406
+ }
407
+
408
+ const result = spawnTerminal(terminalId, title, cmd, { cwd, tabColor });
409
+ console.log(JSON.stringify(result));
410
+ process.exit(result.success ? 0 : 1);
411
+ break;
412
+ }
413
+
414
+ case 'list': {
415
+ const platform = getPlatform();
416
+ const all = Object.entries(TERMINALS)
417
+ .filter(([, t]) => t.platform === platform)
418
+ .map(([id, t]) => ({ id, name: t.name }));
419
+ console.log(JSON.stringify({ platform, terminals: all }, null, 2));
420
+ break;
421
+ }
422
+
423
+ default:
424
+ console.log(`Vibe Forge Terminal Utility
425
+
426
+ Usage:
427
+ node terminal.js detect Detect available terminals (JSON)
428
+ node terminal.js best Get best available terminal (JSON)
429
+ node terminal.js list List all terminals for this platform
430
+ node terminal.js spawn <id> <title> <command> [cwd] [tabColor]
431
+
432
+ Examples:
433
+ node terminal.js detect
434
+ node terminal.js spawn windows-terminal "Anvil" "claude --chat"
435
+ `);
436
+ }
437
+ }
438
+
439
+ // Export for use as module
440
+ module.exports = {
441
+ detectTerminals,
442
+ getBestTerminal,
443
+ spawnTerminal,
444
+ TERMINALS,
445
+ getPlatform,
446
+ };
447
+
448
+ // Run CLI if executed directly
449
+ if (require.main === module) {
450
+ main();
451
+ }