@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.
- package/.claude/commands/clear-attention.md +63 -0
- package/.claude/commands/compact-context.md +52 -0
- package/.claude/commands/configure-vcs.md +102 -0
- package/.claude/commands/forge.md +171 -0
- package/.claude/commands/need-help.md +77 -0
- package/.claude/commands/update-status.md +64 -0
- package/.claude/commands/worker-loop.md +106 -0
- package/.claude/hooks/worker-loop.js +198 -0
- package/.claude/scripts/setup-worker-loop.sh +45 -0
- package/.claude/settings.local.json +46 -0
- package/LICENSE +21 -0
- package/README.md +238 -0
- package/agents/aegis/personality.md +294 -0
- package/agents/anvil/personality.md +276 -0
- package/agents/architect/personality.md +258 -0
- package/agents/crucible/personality.md +360 -0
- package/agents/ember/personality.md +291 -0
- package/agents/forge-master/capabilities.md +144 -0
- package/agents/forge-master/context-template.md +128 -0
- package/agents/forge-master/personality.md +138 -0
- package/agents/furnace/personality.md +340 -0
- package/agents/herald/personality.md +247 -0
- package/agents/loki/personality.md +108 -0
- package/agents/oracle/personality.md +283 -0
- package/agents/pixel/personality.md +113 -0
- package/agents/planning-hub/personality.md +320 -0
- package/agents/scribe/personality.md +251 -0
- package/agents/temper/personality.md +218 -0
- package/bin/cli.js +375 -0
- package/bin/dashboard/api/agents.js +333 -0
- package/bin/dashboard/api/dispatch.js +483 -0
- package/bin/dashboard/api/tasks.js +416 -0
- package/bin/dashboard/frontend/index.html +13 -0
- package/bin/dashboard/frontend/package.json +16 -0
- package/bin/dashboard/frontend/src/App.svelte +222 -0
- package/bin/dashboard/frontend/src/app.css +1777 -0
- package/bin/dashboard/frontend/src/lib/components/AgentCard.svelte +60 -0
- package/bin/dashboard/frontend/src/lib/components/AgentsPanel.svelte +57 -0
- package/bin/dashboard/frontend/src/lib/components/DispatchModal.svelte +180 -0
- package/bin/dashboard/frontend/src/lib/components/Footer.svelte +33 -0
- package/bin/dashboard/frontend/src/lib/components/Header.svelte +84 -0
- package/bin/dashboard/frontend/src/lib/components/IssueCard.svelte +33 -0
- package/bin/dashboard/frontend/src/lib/components/IssuesPanel.svelte +73 -0
- package/bin/dashboard/frontend/src/lib/components/KeyboardShortcutsModal.svelte +108 -0
- package/bin/dashboard/frontend/src/lib/components/MobileTabs.svelte +52 -0
- package/bin/dashboard/frontend/src/lib/components/NotificationCard.svelte +60 -0
- package/bin/dashboard/frontend/src/lib/components/NotificationsPanel.svelte +44 -0
- package/bin/dashboard/frontend/src/lib/components/TaskCard.svelte +63 -0
- package/bin/dashboard/frontend/src/lib/components/TasksPanel.svelte +82 -0
- package/bin/dashboard/frontend/src/lib/components/Toast.svelte +45 -0
- package/bin/dashboard/frontend/src/lib/stores/agents.js +34 -0
- package/bin/dashboard/frontend/src/lib/stores/issues.js +54 -0
- package/bin/dashboard/frontend/src/lib/stores/notifications.js +48 -0
- package/bin/dashboard/frontend/src/lib/stores/tasks.js +63 -0
- package/bin/dashboard/frontend/src/lib/stores/theme.js +33 -0
- package/bin/dashboard/frontend/src/lib/stores/toast.js +35 -0
- package/bin/dashboard/frontend/src/lib/stores/ui.js +25 -0
- package/bin/dashboard/frontend/src/lib/stores/voice.js +275 -0
- package/bin/dashboard/frontend/src/lib/stores/websocket.js +295 -0
- package/bin/dashboard/frontend/src/lib/utils/api.js +101 -0
- package/bin/dashboard/frontend/src/lib/utils/formatters.js +54 -0
- package/bin/dashboard/frontend/src/main.js +9 -0
- package/bin/dashboard/frontend/svelte.config.js +5 -0
- package/bin/dashboard/frontend/vite.config.js +20 -0
- package/bin/dashboard/public/assets/index-DnfVj9Ce.css +1 -0
- package/bin/dashboard/public/assets/index-Ze5h0kXQ.js +2 -0
- package/bin/dashboard/public/index.html +14 -0
- package/bin/dashboard/server.js +566 -0
- package/bin/forge-daemon.sh +463 -0
- package/bin/forge-setup.sh +645 -0
- package/bin/forge-spawn.sh +164 -0
- package/bin/forge.cmd +83 -0
- package/bin/forge.sh +533 -0
- package/bin/lib/agents.sh +177 -0
- package/bin/lib/colors.sh +44 -0
- package/bin/lib/config.sh +347 -0
- package/bin/lib/constants.sh +241 -0
- package/bin/lib/daemon/display.sh +128 -0
- package/bin/lib/daemon/notifications.sh +263 -0
- package/bin/lib/daemon/routing.sh +77 -0
- package/bin/lib/daemon/state.sh +115 -0
- package/bin/lib/daemon/sync.sh +95 -0
- package/bin/lib/database.sh +310 -0
- package/bin/lib/heimdall-setup.js +113 -0
- package/bin/lib/heimdall.js +265 -0
- package/bin/lib/json.sh +264 -0
- package/bin/lib/terminal.js +451 -0
- package/bin/lib/util.sh +126 -0
- package/bin/lib/vcs.js +349 -0
- package/config/agent-manifest.yaml +203 -0
- package/config/agents.json +168 -0
- package/config/task-template.md +159 -0
- package/config/task-types.yaml +106 -0
- package/context/agent-status/aegis.json +7 -0
- package/context/agent-status/anvil.json +7 -0
- package/context/agent-status/architect.json +7 -0
- package/context/agent-status/crucible.json +7 -0
- package/context/agent-status/ember.json +7 -0
- package/context/agent-status/furnace.json +7 -0
- package/context/agent-status/loki.json +7 -0
- package/context/agent-status/oracle.json +7 -0
- package/context/agent-status/pixel.json +7 -0
- package/context/agent-status/planning-hub.json +7 -0
- package/context/agent-status/scribe.json +7 -0
- package/context/agent-status/temper.json +7 -0
- package/context/feature-brainstorm.md +426 -0
- package/context/forge-state.yaml +19 -0
- package/context/modern-conventions.md +129 -0
- package/context/project-context-template.md +122 -0
- package/context/project-context.md +122 -0
- package/docs/TODO.md +150 -0
- package/docs/agents.md +409 -0
- package/docs/architecture/decisions/ADR-001-daemon-modularization.md +122 -0
- package/docs/architecture/vibe-lab-integration.md +684 -0
- package/docs/architecture.md +194 -0
- package/docs/bmad-gap-analysis-2026-03-31.md +444 -0
- package/docs/cleanup-workflow.md +329 -0
- package/docs/commands.md +451 -0
- package/docs/dashboard-mockup.html +989 -0
- package/docs/getting-started.md +261 -0
- package/docs/integration/forge-ownership-policy.md +112 -0
- package/docs/npm-publishing.md +132 -0
- package/docs/roadmap-2026.md +519 -0
- package/docs/security.md +144 -0
- package/docs/wireframes/dashboard-mvp.md +1164 -0
- package/docs/workflows/README.md +32 -0
- package/docs/workflows/azure-devops.md +108 -0
- package/docs/workflows/bitbucket.md +104 -0
- package/docs/workflows/git-only.md +130 -0
- package/docs/workflows/gitea.md +168 -0
- package/docs/workflows/github.md +103 -0
- package/docs/workflows/gitlab.md +105 -0
- package/docs/workflows.md +454 -0
- package/package.json +73 -0
- package/tasks/completed/ARCH-001-duplicate-agent-config.md +121 -0
- package/tasks/completed/ARCH-002-mixed-bash-node-implementation.md +88 -0
- package/tasks/completed/ARCH-003-worker-loop-hook-duplication.md +77 -0
- package/tasks/completed/ARCH-009-test-organization.md +78 -0
- package/tasks/completed/ARCH-011-jq-vs-nodejs-json.md +94 -0
- package/tasks/completed/ARCH-012-tmp-files-in-root.md +71 -0
- package/tasks/completed/ARCH-013-exit-code-constants.md +65 -0
- package/tasks/completed/ARCH-014-sed-incompatibility.md +96 -0
- package/tasks/completed/ARCH-015-docs-todo-tracking.md +83 -0
- package/tasks/completed/BUG-dash-001-tasks-filter-error.md +31 -0
- package/tasks/completed/BUG-dash-002-agents-unknown.md +41 -0
- package/tasks/completed/CLEAN-001.md +38 -0
- package/tasks/completed/CLEAN-002.md +43 -0
- package/tasks/completed/CLEAN-003.md +47 -0
- package/tasks/completed/CLEAN-004.md +56 -0
- package/tasks/completed/CLEAN-005.md +75 -0
- package/tasks/completed/CLEAN-006.md +47 -0
- package/tasks/completed/CLEAN-007.md +34 -0
- package/tasks/completed/CLEAN-008.md +49 -0
- package/tasks/completed/CLEAN-012.md +58 -0
- package/tasks/completed/CLEAN-013.md +45 -0
- package/tasks/completed/FEATURE-001a-dashboard-wireframes.md +162 -0
- package/tasks/completed/IMPL-007a-daemon-notifications-module.md +82 -0
- package/tasks/completed/IMPL-007b-daemon-sync-module.md +71 -0
- package/tasks/completed/IMPL-007c-daemon-state-module.md +80 -0
- package/tasks/completed/IMPL-007d-daemon-routing-module.md +77 -0
- package/tasks/completed/IMPL-007e-daemon-display-module.md +77 -0
- package/tasks/completed/IMPL-007f-daemon-integration.md +124 -0
- package/tasks/completed/PLAT-1-heimdall.md +420 -0
- package/tasks/completed/SEC-001-sql-injection-fix.md +58 -0
- package/tasks/completed/SEC-002-notification-injection-fix.md +45 -0
- package/tasks/completed/SEC-003-eval-injection-fix.md +54 -0
- package/tasks/completed/SEC-004-pid-race-condition-fix.md +49 -0
- package/tasks/completed/SEC-005-worker-loop-path-fix.md +51 -0
- package/tasks/completed/SEC-006-eval-agent-names.md +55 -0
- package/tasks/completed/SEC-007-spawn-escaping.md +67 -0
- package/tasks/completed/TASK-DASH-001-server-infrastructure.md +185 -0
- package/tasks/completed/TASK-anvil-001-dashboard-frontend.md +133 -0
- package/tasks/completed/review-bmad-aegis.md +89 -0
- package/tasks/completed/review-bmad-anvil.md +80 -0
- package/tasks/completed/review-bmad-crucible.md +81 -0
- package/tasks/completed/review-bmad-ember.md +90 -0
- package/tasks/completed/review-bmad-furnace.md +79 -0
- package/tasks/completed/review-bmad-pixel.md +82 -0
- package/tasks/completed/review-bmad-scribe.md +92 -0
- package/tasks/completed/review-bmad-sentinel.md +83 -0
- package/tasks/pending/ARCH-004-git-bash-detection-duplication.md +72 -0
- package/tasks/pending/ARCH-005-missing-src-directory.md +95 -0
- package/tasks/pending/ARCH-006-task-template-location.md +64 -0
- package/tasks/pending/ARCH-008-forge-master-vs-hub.md +81 -0
- package/tasks/pending/ARCH-010-missing-index-files.md +84 -0
- package/tasks/pending/CLEAN-009.md +31 -0
- package/tasks/pending/CLEAN-010.md +30 -0
- package/tasks/pending/CLEAN-011.md +30 -0
- package/tasks/pending/CLEAN-014.md +32 -0
- package/tasks/pending/DESIGN-dash-001-layout-review.md +45 -0
- package/tasks/pending/FEATURE-001-dashboard-mvp.md +268 -0
- package/tasks/review/ARCH-007-daemon-monolith.md +162 -0
- package/tasks/review/bmad-review-aegis.md +349 -0
- package/tasks/review/bmad-review-anvil.md +259 -0
- package/tasks/review/bmad-review-crucible.md +277 -0
- package/tasks/review/bmad-review-ember.md +307 -0
- package/tasks/review/bmad-review-furnace.md +285 -0
- package/tasks/review/bmad-review-pixel.md +329 -0
- package/tasks/review/bmad-review-scribe.md +361 -0
- package/tasks/review/bmad-review-sentinel.md +242 -0
- package/tasks/review/task-001.md +78 -0
package/bin/lib/json.sh
ADDED
|
@@ -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
|
+
}
|