overlord-cli 0.2605101225.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/README.md +128 -0
- package/bin/_cli/attach.mjs +357 -0
- package/bin/_cli/auth.mjs +696 -0
- package/bin/_cli/cli-update.mjs +119 -0
- package/bin/_cli/credentials.mjs +723 -0
- package/bin/_cli/index.mjs +170 -0
- package/bin/_cli/launcher.mjs +474 -0
- package/bin/_cli/new-ticket.mjs +306 -0
- package/bin/_cli/postinstall.mjs +43 -0
- package/bin/_cli/protocol.mjs +1799 -0
- package/bin/_cli/setup.mjs +1634 -0
- package/bin/_cli/ticket.mjs +55 -0
- package/bin/_cli/tickets.mjs +121 -0
- package/bin/_cli/version.mjs +7 -0
- package/bin/ovld.mjs +13 -0
- package/package.json +36 -0
- package/plugins/claude/.claude-plugin/plugin.json +34 -0
- package/plugins/claude/README.md +42 -0
- package/plugins/claude/commands/connect.md +19 -0
- package/plugins/claude/commands/create.md +17 -0
- package/plugins/claude/commands/load.md +19 -0
- package/plugins/claude/commands/spawn.md +17 -0
- package/plugins/claude/hooks/hooks.json +15 -0
- package/plugins/claude/scripts/permission-hook.sh +21 -0
- package/plugins/claude/skills/overlord-ticket/SKILL.md +164 -0
- package/plugins/cursor/.cursor-plugin/plugin.json +14 -0
- package/plugins/cursor/commands/connect.md +3 -0
- package/plugins/cursor/commands/create.md +11 -0
- package/plugins/cursor/commands/load.md +3 -0
- package/plugins/cursor/commands/spawn.md +3 -0
- package/plugins/cursor/mcp.json +8 -0
- package/plugins/cursor/rules/overlord-local.mdc +10 -0
- package/plugins/cursor/scripts/overlord-mcp.mjs +150 -0
- package/plugins/cursor/skills/overlord-ticket/SKILL.md +49 -0
- package/plugins/overlord/.codex-plugin/plugin.json +35 -0
- package/plugins/overlord/.mcp.json +11 -0
- package/plugins/overlord/README.md +42 -0
- package/plugins/overlord/assets/icon.png +0 -0
- package/plugins/overlord/assets/logo.png +0 -0
- package/plugins/overlord/assets/screenshot.svg +28 -0
- package/plugins/overlord/scripts/overlord-mcp.mjs +688 -0
- package/plugins/overlord/skills/overlord-ticket/SKILL.md +146 -0
|
@@ -0,0 +1,1634 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Agent bundle setup commands (setup / doctor).
|
|
5
|
+
*
|
|
6
|
+
* Installs durable Overlord workflow configuration for Claude Code and OpenCode
|
|
7
|
+
* into their respective config directories.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import crypto from 'node:crypto';
|
|
11
|
+
import fs from 'node:fs';
|
|
12
|
+
import os from 'node:os';
|
|
13
|
+
import path from 'node:path';
|
|
14
|
+
import { fileURLToPath } from 'node:url';
|
|
15
|
+
import { checkForCliUpdate, getCurrentCliVersion, printCliUpdateNotice } from './cli-update.mjs';
|
|
16
|
+
|
|
17
|
+
const BUNDLE_VERSION = '1.11.0';
|
|
18
|
+
const MD_MARKER_START = '<!-- overlord:managed:start -->';
|
|
19
|
+
const MD_MARKER_END = '<!-- overlord:managed:end -->';
|
|
20
|
+
const MANIFEST_DIR = path.join(os.homedir(), '.ovld');
|
|
21
|
+
const MANIFEST_FILE = path.join(MANIFEST_DIR, 'bundle-manifest.json');
|
|
22
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
23
|
+
const PACKAGE_PLUGIN_DIR = path.resolve(__dirname, '..', '..', 'plugins', 'overlord');
|
|
24
|
+
const REPO_PLUGIN_DIR = path.resolve(__dirname, '..', '..', '..', '..', 'plugins', 'overlord');
|
|
25
|
+
const PACKAGE_CLAUDE_PLUGIN_DIR = path.resolve(__dirname, '..', '..', 'plugins', 'claude');
|
|
26
|
+
const REPO_CLAUDE_PLUGIN_DIR = path.resolve(__dirname, '..', '..', '..', '..', 'plugins', 'claude');
|
|
27
|
+
const PACKAGE_CURSOR_PLUGIN_DIR = path.resolve(__dirname, '..', '..', 'plugins', 'cursor');
|
|
28
|
+
const REPO_CURSOR_PLUGIN_DIR = path.resolve(__dirname, '..', '..', '..', '..', 'plugins', 'cursor');
|
|
29
|
+
const CODEX_TARGET_PLUGIN_DIR = path.join(os.homedir(), '.codex', 'plugins', 'overlord');
|
|
30
|
+
const CODEX_TARGET_PLUGIN_MANIFEST = path.join(
|
|
31
|
+
CODEX_TARGET_PLUGIN_DIR,
|
|
32
|
+
'.codex-plugin',
|
|
33
|
+
'plugin.json'
|
|
34
|
+
);
|
|
35
|
+
const CODEX_TARGET_MARKETPLACE = path.join(os.homedir(), '.agents', 'plugins', 'marketplace.json');
|
|
36
|
+
const CODEX_TARGET_RULES = path.join(os.homedir(), '.codex', 'rules', 'default.rules');
|
|
37
|
+
const CODEX_LEGACY_AGENTS = path.join(os.homedir(), '.codex', 'AGENTS.md');
|
|
38
|
+
const CODEX_RULES_START = '# overlord:permissions:start';
|
|
39
|
+
const CODEX_RULES_END = '# overlord:permissions:end';
|
|
40
|
+
const REQUIRED_NODE_MAJOR = 20;
|
|
41
|
+
|
|
42
|
+
const supportedAgents = ['claude', 'codex', 'cursor', 'gemini', 'opencode'];
|
|
43
|
+
|
|
44
|
+
// ---------------------------------------------------------------------------
|
|
45
|
+
// Templates (same content as electron/services/agent-bundle/templates.ts)
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
|
|
48
|
+
const CLAUDE_SKILL_CONTENT = `---
|
|
49
|
+
name: overlord-local
|
|
50
|
+
description: Overlord local workflow protocol — attach, update, deliver lifecycle for ticket-driven work.
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
# Overlord Local Workflow
|
|
54
|
+
|
|
55
|
+
If you receive a prompt with a specified ticket ID, adhere to the following. If the prompt does not have a ticket ID, the user may choose to add one later, but otherwise, proceed without it.
|
|
56
|
+
|
|
57
|
+
## Lifecycle
|
|
58
|
+
|
|
59
|
+
1. **Attach first** — Always call attach before doing any work:
|
|
60
|
+
\`\`\`bash
|
|
61
|
+
ovld protocol attach --ticket-id $TICKET_ID
|
|
62
|
+
\`\`\`
|
|
63
|
+
Store \`session.sessionKey\` from the response — it is required for all subsequent calls.
|
|
64
|
+
|
|
65
|
+
2. **Update during work** — Post at least one progress update before delivering:
|
|
66
|
+
\`\`\`bash
|
|
67
|
+
ovld protocol update --session-key <sessionKey> --ticket-id $TICKET_ID --summary "What you did and why." --phase execute
|
|
68
|
+
\`\`\`
|
|
69
|
+
Phases: \`draft\`, \`execute\`, \`review\`, \`deliver\`, \`complete\`, \`blocked\`, \`cancelled\`.
|
|
70
|
+
Use \`execute\` while working.
|
|
71
|
+
|
|
72
|
+
Pass \`--event-type <type>\` to publish a specific activity event (default: \`update\`):
|
|
73
|
+
- \`update\` — standard progress update (default)
|
|
74
|
+
- \`user_follow_up\` — a message or question from the human user (EXCLUDING THE INITIAL TICKET)
|
|
75
|
+
- \`alert\` — surface a warning or non-blocking alert
|
|
76
|
+
|
|
77
|
+
3. **Ask when blocked** — Stop working after calling:
|
|
78
|
+
\`\`\`bash
|
|
79
|
+
ovld protocol ask --session-key <sessionKey> --ticket-id $TICKET_ID --question "Specific question for the PM."
|
|
80
|
+
\`\`\`
|
|
81
|
+
|
|
82
|
+
4. **Deliver last** — Always deliver when done:
|
|
83
|
+
\`\`\`bash
|
|
84
|
+
ovld protocol deliver --session-key <sessionKey> \\\\
|
|
85
|
+
--ticket-id $TICKET_ID \\\\
|
|
86
|
+
--summary "Narrative: what you did, next steps." \\\\
|
|
87
|
+
--artifacts-json '[{"type":"next_steps","label":"Next steps","content":"..."}]' \\\\
|
|
88
|
+
--change-rationales-json '[{"label":"Short reviewer title","file_path":"path/to/file.ts","summary":"What changed.","why":"Why it changed.","impact":"Behavioral impact.","hunks":[{"header":"@@ -10,6 +10,14 @@"}]}]'
|
|
89
|
+
\`\`\`
|
|
90
|
+
|
|
91
|
+
For larger or quote-sensitive deliveries, prefer a single JSON payload on stdin:
|
|
92
|
+
\`\`\`bash
|
|
93
|
+
ovld protocol deliver --session-key <sessionKey> --ticket-id $TICKET_ID --payload-file -
|
|
94
|
+
\`\`\`
|
|
95
|
+
This avoids creating a scratch delivery file that needs cleanup. If your runtime cannot provide stdin directly, \`--payload-file ./deliver.json\` remains supported; treat that file as ephemeral scratch data, never commit it, and remove it after delivery.
|
|
96
|
+
|
|
97
|
+
## Change Rationales
|
|
98
|
+
|
|
99
|
+
Always include \`changeRationales\` when delivering. Optionally include them on updates during long-running work.
|
|
100
|
+
|
|
101
|
+
Before delivering, make sure every meaningful git-tracked file change is represented in \`changeRationales\`; do not send \`file_changes\` as an artifact.
|
|
102
|
+
|
|
103
|
+
These are structured protocol payloads that Overlord stores as first-class rows in the \`file_changes\` table. Prefer inline JSON or the dedicated command below. For larger full delivery payloads, prefer \`--payload-file -\` so summary, artifacts, and change rationales stay in one JSON document without creating a temporary file. Ordinary deliver artifacts should use \`next_steps\`, \`test_results\`, \`migration\`, \`note\`, \`url\`, or \`decision\`.
|
|
104
|
+
|
|
105
|
+
\`\`\`bash
|
|
106
|
+
ovld protocol record-change-rationales --session-key <sessionKey> --ticket-id $TICKET_ID \\\\
|
|
107
|
+
--summary "Recorded rationale details for the latest code changes." --phase execute \\\\
|
|
108
|
+
--change-rationales-json '[{"label":"Add backoff","file_path":"lib/api.ts","summary":"Added retry.","why":"Transient failures.","impact":"Retries 3x.","hunks":[{"header":"@@ -22,4 +22,18 @@"}]}]'
|
|
109
|
+
\`\`\`
|
|
110
|
+
|
|
111
|
+
Record only meaningful behavioral changes — skip formatting-only noise. Prefer 1–5 concise rationales per ticket, each tied to a specific file and diff hunk.
|
|
112
|
+
|
|
113
|
+
## Project Discovery & Ticket Creation
|
|
114
|
+
|
|
115
|
+
When creating tickets from within a repository:
|
|
116
|
+
- Prefer \`ovld protocol create --agent claude-code\` by default for draft ticket creation.
|
|
117
|
+
- Use \`ovld protocol prompt --agent claude-code\` only when the user explicitly asks to create and execute immediately.
|
|
118
|
+
- Both commands can resolve the project from the current working directory; use \`--working-directory\` to override.
|
|
119
|
+
|
|
120
|
+
\`\`\`bash
|
|
121
|
+
ovld protocol create --agent claude-code --objective "Capture follow-up work from this repository"
|
|
122
|
+
\`\`\`
|
|
123
|
+
|
|
124
|
+
\`\`\`bash
|
|
125
|
+
ovld protocol prompt --agent claude-code --objective "Implement feature X" --priority medium
|
|
126
|
+
\`\`\`
|
|
127
|
+
|
|
128
|
+
## Context & Artifacts
|
|
129
|
+
|
|
130
|
+
\`\`\`bash
|
|
131
|
+
ovld protocol read-context --session-key <sessionKey> --ticket-id $TICKET_ID
|
|
132
|
+
ovld protocol write-context --session-key <sessionKey> --ticket-id $TICKET_ID --key "key" --value '"json-value"'
|
|
133
|
+
ovld protocol attachment-upload-file --session-key <sessionKey> --ticket-id $TICKET_ID --objective-id <objective-id> --file ./spec.pdf --content-type application/pdf
|
|
134
|
+
\`\`\`
|
|
135
|
+
|
|
136
|
+
## Rules
|
|
137
|
+
|
|
138
|
+
- Always attach first; always deliver when done.
|
|
139
|
+
- Post at least one update before delivering.
|
|
140
|
+
- If blocked on human-only work, call \`ask\` and request a follow-up human ticket.
|
|
141
|
+
- The \`summary\` in deliver is what the PM reads first — write it as a narrative, not a command list.
|
|
142
|
+
- Use \`write-context\` for facts a future agent session should know.
|
|
143
|
+
- **If the user sends you a message during your session, immediately publish a \`user_follow_up\` activity event with the user's message recorded verbatim in the summary before doing anything else. This DOES NOT apply to the initial ticket.**
|
|
144
|
+
`;
|
|
145
|
+
|
|
146
|
+
const OPENCODE_AGENTS_SECTION = `# Overlord Local Workflow
|
|
147
|
+
|
|
148
|
+
If you receive a prompt with a specified ticket ID, adhere to the following. If the prompt does not have a ticket ID, the user may choose to add one later, but otherwise, proceed without it.
|
|
149
|
+
|
|
150
|
+
## Lifecycle
|
|
151
|
+
|
|
152
|
+
1. **Attach first** — Always call attach before doing any work:
|
|
153
|
+
\`\`\`bash
|
|
154
|
+
ovld protocol attach --ticket-id $TICKET_ID
|
|
155
|
+
\`\`\`
|
|
156
|
+
Store \`session.sessionKey\` from the response — it is required for all subsequent calls.
|
|
157
|
+
|
|
158
|
+
2. **Update during work** — Post at least one progress update before delivering:
|
|
159
|
+
\`\`\`bash
|
|
160
|
+
ovld protocol update --session-key <sessionKey> --ticket-id $TICKET_ID --summary "What you did and why." --phase execute
|
|
161
|
+
\`\`\`
|
|
162
|
+
Phases: \`draft\`, \`execute\`, \`review\`, \`deliver\`, \`complete\`, \`blocked\`, \`cancelled\`.
|
|
163
|
+
Use \`execute\` while working.
|
|
164
|
+
|
|
165
|
+
Pass \`--event-type <type>\` for activity events: \`update\`, \`user_follow_up\`, \`alert\`.
|
|
166
|
+
|
|
167
|
+
3. **Ask when blocked** — Stop working after calling:
|
|
168
|
+
\`\`\`bash
|
|
169
|
+
ovld protocol ask --session-key <sessionKey> --ticket-id $TICKET_ID --question "Specific question for the PM."
|
|
170
|
+
\`\`\`
|
|
171
|
+
|
|
172
|
+
4. **Deliver last** — Always deliver when done:
|
|
173
|
+
\`\`\`bash
|
|
174
|
+
ovld protocol deliver --session-key <sessionKey> \\\\
|
|
175
|
+
--ticket-id $TICKET_ID \\\\
|
|
176
|
+
--summary "Narrative: what you did, next steps." \\\\
|
|
177
|
+
--artifacts-json '[{"type":"next_steps","label":"Next steps","content":"..."}]' \\\\
|
|
178
|
+
--change-rationales-json '[{"label":"Short reviewer title","file_path":"path/to/file.ts","summary":"What changed.","why":"Why it changed.","impact":"Behavioral impact.","hunks":[{"header":"@@ -10,6 +10,14 @@"}]}]'
|
|
179
|
+
\`\`\`
|
|
180
|
+
For larger delivery JSON, prefer \`--payload-file -\` and stream the full payload on stdin so no scratch file needs to be created or removed. If you use \`--payload-file\`, \`--artifacts-file\`, or \`--change-rationales-file\` with a real path, treat that file as ephemeral scratch data outside the repository and remove it after delivery. Do not leave delivery JSON checked into the worktree.
|
|
181
|
+
|
|
182
|
+
## Change Rationales
|
|
183
|
+
|
|
184
|
+
Always include \`changeRationales\` when delivering. Before delivering, make sure every meaningful git-tracked file change is represented in \`changeRationales\`; do not send \`file_changes\` as an artifact. Record only meaningful behavioral changes. Overlord stores these as structured rows in the \`file_changes\` table. For larger delivery payloads, prefer \`--payload-file -\` with stdin. If you need a JSON file for transport, keep it ephemeral and out of the repository.
|
|
185
|
+
|
|
186
|
+
\`\`\`bash
|
|
187
|
+
ovld protocol record-change-rationales --session-key <sessionKey> --ticket-id $TICKET_ID \\\\
|
|
188
|
+
--summary "Recorded rationale details for the latest code changes." --phase execute \\\\
|
|
189
|
+
--change-rationales-json '[{"label":"Add backoff","file_path":"lib/api.ts","summary":"Added retry.","why":"Transient failures.","impact":"Retries 3x.","hunks":[{"header":"@@ -22,4 +22,18 @@"}]}]'
|
|
190
|
+
\`\`\`
|
|
191
|
+
|
|
192
|
+
## Project Discovery & Ticket Creation
|
|
193
|
+
|
|
194
|
+
When creating tickets from within a repository:
|
|
195
|
+
- Prefer \`ovld protocol create --agent opencode\` by default for draft ticket creation.
|
|
196
|
+
- Use \`ovld protocol prompt --agent opencode\` only when the user explicitly asks to create and execute immediately.
|
|
197
|
+
- Both commands can resolve the project from the current working directory; use \`--working-directory\` to override.
|
|
198
|
+
|
|
199
|
+
\`\`\`bash
|
|
200
|
+
ovld protocol create --agent opencode --objective "Capture follow-up work from this repository"
|
|
201
|
+
\`\`\`
|
|
202
|
+
|
|
203
|
+
\`\`\`bash
|
|
204
|
+
ovld protocol prompt --agent opencode --objective "Implement feature X" --priority medium
|
|
205
|
+
\`\`\`
|
|
206
|
+
|
|
207
|
+
## Context & Artifacts
|
|
208
|
+
|
|
209
|
+
\`\`\`bash
|
|
210
|
+
ovld protocol read-context --session-key <sessionKey> --ticket-id $TICKET_ID
|
|
211
|
+
ovld protocol write-context --session-key <sessionKey> --ticket-id $TICKET_ID --key "key" --value '"json-value"'
|
|
212
|
+
ovld protocol attachment-upload-file --session-key <sessionKey> --ticket-id $TICKET_ID --objective-id <objective-id> --file ./spec.pdf --content-type application/pdf
|
|
213
|
+
\`\`\`
|
|
214
|
+
|
|
215
|
+
## Rules
|
|
216
|
+
|
|
217
|
+
- Always attach first; always deliver when done.
|
|
218
|
+
- Post at least one update before delivering.
|
|
219
|
+
- If blocked on human-only work, call \`ask\` and request a follow-up human ticket.
|
|
220
|
+
- The \`summary\` in deliver is what the PM reads first — write it as a narrative.
|
|
221
|
+
- Use \`write-context\` for facts a future agent session should know.
|
|
222
|
+
- **If the user sends you a message during your session, immediately publish a \`user_follow_up\` activity event with the user's message recorded verbatim in the summary before doing anything else. This DOES NOT apply to the initial ticket.**
|
|
223
|
+
`;
|
|
224
|
+
|
|
225
|
+
const PERMISSION_HOOK_SCRIPT = `#!/bin/bash
|
|
226
|
+
# Overlord PermissionRequest notification hook (managed by Overlord)
|
|
227
|
+
BODY=$(cat -)
|
|
228
|
+
if [ -n "$TICKET_ID" ] && command -v ovld >/dev/null 2>&1; then
|
|
229
|
+
{ if [ -n "$BODY" ]; then printf '%s' "$BODY"; else printf '{}'; fi; } \\
|
|
230
|
+
| ovld protocol permission-request --ticket-id "$TICKET_ID" --payload-file - \\
|
|
231
|
+
>/dev/null 2>&1 &
|
|
232
|
+
disown
|
|
233
|
+
fi
|
|
234
|
+
exit 0
|
|
235
|
+
`;
|
|
236
|
+
|
|
237
|
+
const CURSOR_RULES_CONTENT = `---
|
|
238
|
+
description: Overlord local workflow protocol — attach, update, deliver lifecycle for ticket-driven work.
|
|
239
|
+
globs:
|
|
240
|
+
alwaysApply: true
|
|
241
|
+
---
|
|
242
|
+
|
|
243
|
+
# Overlord Local Workflow
|
|
244
|
+
|
|
245
|
+
If a prompt includes a TICKET_ID, attach first with:
|
|
246
|
+
\`\`\`bash
|
|
247
|
+
ovld protocol attach --ticket-id $TICKET_ID
|
|
248
|
+
\`\`\`
|
|
249
|
+
|
|
250
|
+
During work, post progress updates with:
|
|
251
|
+
\`\`\`bash
|
|
252
|
+
ovld protocol update --session-key <sessionKey> --ticket-id $TICKET_ID --summary "What you did and why." --phase execute
|
|
253
|
+
\`\`\`
|
|
254
|
+
|
|
255
|
+
If blocked on human input, ask with:
|
|
256
|
+
\`\`\`bash
|
|
257
|
+
ovld protocol ask --session-key <sessionKey> --ticket-id $TICKET_ID --question "Specific question for the PM."
|
|
258
|
+
\`\`\`
|
|
259
|
+
|
|
260
|
+
When done, deliver with artifacts and change rationales:
|
|
261
|
+
\`\`\`bash
|
|
262
|
+
ovld protocol deliver --session-key <sessionKey> --ticket-id $TICKET_ID --summary "Narrative: what you did, next steps." --artifacts-json '[{"type":"next_steps","label":"Next steps","content":"..."}]' --change-rationales-json '[{"label":"Short reviewer title","file_path":"path/to/file.ts","summary":"What changed.","why":"Why it changed.","impact":"Behavioral impact.","hunks":[{"header":"@@ -10,6 +10,14 @@"}]}]'
|
|
263
|
+
\`\`\`
|
|
264
|
+
For larger delivery JSON, prefer \`--payload-file -\` with stdin so no scratch file needs to be created or removed. If you use a JSON file for delivery transport, keep it ephemeral scratch data outside the repository and remove it after the protocol call.
|
|
265
|
+
|
|
266
|
+
Rules:
|
|
267
|
+
- Always attach first and deliver last.
|
|
268
|
+
- Use \`ovld protocol\` commands instead of ad hoc repo scripts for ticket lifecycle work.
|
|
269
|
+
- Prefer \`ovld protocol create --agent cursor\` for draft ticket creation; use \`prompt --agent cursor\` only for create-and-execute requests.
|
|
270
|
+
- If the user sends a new message during an active ticket session, publish a \`user_follow_up\` event before doing anything else.
|
|
271
|
+
`;
|
|
272
|
+
|
|
273
|
+
// ---------------------------------------------------------------------------
|
|
274
|
+
// Helpers
|
|
275
|
+
// ---------------------------------------------------------------------------
|
|
276
|
+
|
|
277
|
+
function contentHash(content) {
|
|
278
|
+
return crypto.createHash('sha256').update(content).digest('hex').slice(0, 16);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function backupFile(filePath) {
|
|
282
|
+
if (!fs.existsSync(filePath)) return null;
|
|
283
|
+
const dir = path.dirname(filePath);
|
|
284
|
+
const ext = path.extname(filePath);
|
|
285
|
+
const base = path.basename(filePath, ext);
|
|
286
|
+
const ts = new Date().toISOString().replace(/[:.]/g, '-');
|
|
287
|
+
const backupPath = path.join(dir, `${base}.backup-${ts}${ext}`);
|
|
288
|
+
fs.copyFileSync(filePath, backupPath);
|
|
289
|
+
return backupPath;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function readJsonFile(filePath) {
|
|
293
|
+
try {
|
|
294
|
+
const raw = fs.readFileSync(filePath, 'utf-8');
|
|
295
|
+
const parsed = JSON.parse(raw);
|
|
296
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) return parsed;
|
|
297
|
+
return {};
|
|
298
|
+
} catch {
|
|
299
|
+
return {};
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function readJsonFileOrNull(filePath) {
|
|
304
|
+
try {
|
|
305
|
+
if (!fs.existsSync(filePath)) return null;
|
|
306
|
+
const raw = fs.readFileSync(filePath, 'utf-8');
|
|
307
|
+
const parsed = JSON.parse(raw);
|
|
308
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) return parsed;
|
|
309
|
+
return null;
|
|
310
|
+
} catch {
|
|
311
|
+
return null;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function writeJsonFile(filePath, data) {
|
|
316
|
+
const dir = path.dirname(filePath);
|
|
317
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
318
|
+
fs.writeFileSync(filePath, JSON.stringify(data, null, 2) + '\n', 'utf-8');
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function readTextFile(filePath) {
|
|
322
|
+
try {
|
|
323
|
+
return fs.readFileSync(filePath, 'utf-8');
|
|
324
|
+
} catch {
|
|
325
|
+
return '';
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function writeTextFile(filePath, content, mode) {
|
|
330
|
+
const dir = path.dirname(filePath);
|
|
331
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
332
|
+
const options = { encoding: 'utf-8' };
|
|
333
|
+
if (mode !== undefined) options.mode = mode;
|
|
334
|
+
fs.writeFileSync(filePath, content, options);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function deepClone(obj) {
|
|
338
|
+
return JSON.parse(JSON.stringify(obj));
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function asStringArray(value) {
|
|
342
|
+
if (!Array.isArray(value)) return [];
|
|
343
|
+
return value.filter(entry => typeof entry === 'string');
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function mergeMarkdownSection(existing, newContent) {
|
|
347
|
+
const wrappedContent = `${MD_MARKER_START}\n${newContent.trim()}\n${MD_MARKER_END}`;
|
|
348
|
+
const startIdx = existing.indexOf(MD_MARKER_START);
|
|
349
|
+
const endIdx = existing.indexOf(MD_MARKER_END);
|
|
350
|
+
if (startIdx !== -1 && endIdx !== -1 && endIdx > startIdx) {
|
|
351
|
+
const before = existing.slice(0, startIdx);
|
|
352
|
+
const after = existing.slice(endIdx + MD_MARKER_END.length);
|
|
353
|
+
return `${before}${wrappedContent}${after}`;
|
|
354
|
+
}
|
|
355
|
+
const trimmed = existing.trimEnd();
|
|
356
|
+
if (trimmed.length === 0) return wrappedContent + '\n';
|
|
357
|
+
return `${trimmed}\n\n${wrappedContent}\n`;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function readManifest() {
|
|
361
|
+
return readJsonFile(MANIFEST_FILE);
|
|
362
|
+
}
|
|
363
|
+
function writeManifest(manifest) {
|
|
364
|
+
writeJsonFile(MANIFEST_FILE, manifest);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function slashCommandFiles(agent) {
|
|
368
|
+
if (agent === 'claude') {
|
|
369
|
+
const base = path.join(os.homedir(), '.claude', 'commands');
|
|
370
|
+
return [
|
|
371
|
+
{
|
|
372
|
+
path: path.join(base, 'connect.md'),
|
|
373
|
+
content: `---
|
|
374
|
+
description: Connect this session to another Overlord ticket by ticket identifier
|
|
375
|
+
argument-hint: <ticket_id>
|
|
376
|
+
disable-model-invocation: true
|
|
377
|
+
---
|
|
378
|
+
|
|
379
|
+
Run \`ovld protocol connect --ticket-id <ticket_id>\` using \`$ARGUMENTS\` as the ticket ID.`
|
|
380
|
+
},
|
|
381
|
+
{
|
|
382
|
+
path: path.join(base, 'load.md'),
|
|
383
|
+
content: `---
|
|
384
|
+
description: Load Overlord ticket context without creating a new session
|
|
385
|
+
argument-hint: <ticket_id>
|
|
386
|
+
disable-model-invocation: true
|
|
387
|
+
---
|
|
388
|
+
|
|
389
|
+
Run \`ovld protocol load-context --ticket-id <ticket_id>\` using \`$ARGUMENTS\` as the ticket ID.`
|
|
390
|
+
},
|
|
391
|
+
{
|
|
392
|
+
path: path.join(base, 'create.md'),
|
|
393
|
+
content: `---
|
|
394
|
+
description: Create a draft Overlord ticket from the current conversation
|
|
395
|
+
argument-hint: <objective or raw flags>
|
|
396
|
+
disable-model-invocation: true
|
|
397
|
+
---
|
|
398
|
+
|
|
399
|
+
Run \`ovld protocol create --agent claude-code\` with \`$ARGUMENTS\`. If no flags are present, treat the arguments as the objective and call \`ovld protocol create --agent claude-code --objective "<objective>"\`.`
|
|
400
|
+
},
|
|
401
|
+
{
|
|
402
|
+
path: path.join(base, 'prompt.md'),
|
|
403
|
+
content: `---
|
|
404
|
+
description: Create a new Overlord ticket from the current conversation
|
|
405
|
+
argument-hint: <objective or raw flags>
|
|
406
|
+
disable-model-invocation: true
|
|
407
|
+
---
|
|
408
|
+
|
|
409
|
+
Run \`ovld protocol prompt --agent claude-code\` with \`$ARGUMENTS\`. If no flags are present, treat the arguments as the objective and call \`ovld protocol prompt --agent claude-code --objective "<objective>"\`.`
|
|
410
|
+
}
|
|
411
|
+
];
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
if (agent === 'cursor') {
|
|
415
|
+
const base = path.join(os.homedir(), '.cursor', 'commands');
|
|
416
|
+
return [
|
|
417
|
+
{
|
|
418
|
+
path: path.join(base, 'connect.md'),
|
|
419
|
+
content:
|
|
420
|
+
'Connect this session to another Overlord ticket.\n\nRun `ovld protocol connect --ticket-id <ticketId>` using the text after `/connect` as the ticket ID.\n'
|
|
421
|
+
},
|
|
422
|
+
{
|
|
423
|
+
path: path.join(base, 'load.md'),
|
|
424
|
+
content:
|
|
425
|
+
'Load Overlord ticket context without attaching.\n\nRun `ovld protocol load-context --ticket-id <ticketId>` using the text after `/load` as the ticket ID.\n'
|
|
426
|
+
},
|
|
427
|
+
{
|
|
428
|
+
path: path.join(base, 'create.md'),
|
|
429
|
+
content:
|
|
430
|
+
'Create a draft Overlord ticket.\n\nRun `ovld protocol create --agent cursor --objective "<objective>"` using the text after `/create` unless raw flags were provided. If raw flags were provided, pass them after `ovld protocol create --agent cursor`.\n'
|
|
431
|
+
},
|
|
432
|
+
{
|
|
433
|
+
path: path.join(base, 'prompt.md'),
|
|
434
|
+
content:
|
|
435
|
+
'Create a new Overlord ticket.\n\nRun `ovld protocol prompt --agent cursor --objective "<objective>"` using the text after `/prompt` unless raw flags were provided. If raw flags were provided, pass them after `ovld protocol prompt --agent cursor`.\n'
|
|
436
|
+
}
|
|
437
|
+
];
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
if (agent === 'gemini') {
|
|
441
|
+
const base = path.join(os.homedir(), '.gemini', 'commands');
|
|
442
|
+
return [
|
|
443
|
+
{
|
|
444
|
+
path: path.join(base, 'connect.toml'),
|
|
445
|
+
content:
|
|
446
|
+
'description = "Connect this session to another Overlord ticket by ticket ID."\nprompt = """\nRun `ovld protocol connect --ticket-id <ticketId>` using `{{args}}` as the ticket ID.\n"""\n'
|
|
447
|
+
},
|
|
448
|
+
{
|
|
449
|
+
path: path.join(base, 'load.toml'),
|
|
450
|
+
content:
|
|
451
|
+
'description = "Load Overlord ticket context without creating a new session."\nprompt = """\nRun `ovld protocol load-context --ticket-id <ticketId>` using `{{args}}` as the ticket ID.\n"""\n'
|
|
452
|
+
},
|
|
453
|
+
{
|
|
454
|
+
path: path.join(base, 'create.toml'),
|
|
455
|
+
content:
|
|
456
|
+
'description = "Create a draft Overlord ticket from the current conversation."\nprompt = """\nRun `ovld protocol create --agent gemini --objective "<objective>"` using `{{args}}` as the objective unless raw flags were provided. If raw flags were provided, pass them after `ovld protocol create --agent gemini`.\n"""\n'
|
|
457
|
+
},
|
|
458
|
+
{
|
|
459
|
+
path: path.join(base, 'prompt.toml'),
|
|
460
|
+
content:
|
|
461
|
+
'description = "Create a new Overlord ticket from the current conversation."\nprompt = """\nRun `ovld protocol prompt --agent gemini --objective "<objective>"` using `{{args}}` as the objective unless raw flags were provided. If raw flags were provided, pass them after `ovld protocol prompt --agent gemini`.\n"""\n'
|
|
462
|
+
}
|
|
463
|
+
];
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
const base = path.join(os.homedir(), '.config', 'opencode', 'commands');
|
|
467
|
+
return [
|
|
468
|
+
{
|
|
469
|
+
path: path.join(base, 'connect.md'),
|
|
470
|
+
content: `---
|
|
471
|
+
description: Connect this session to another Overlord ticket by ticket ID
|
|
472
|
+
agent: build
|
|
473
|
+
---
|
|
474
|
+
|
|
475
|
+
Run \`ovld protocol connect --ticket-id <ticketId>\` using \`$ARGUMENTS\` as the ticket ID. If no ticket ID was provided, ask the user for one and stop.`
|
|
476
|
+
},
|
|
477
|
+
{
|
|
478
|
+
path: path.join(base, 'load.md'),
|
|
479
|
+
content: `---
|
|
480
|
+
description: Load Overlord ticket context without creating a new session
|
|
481
|
+
agent: build
|
|
482
|
+
---
|
|
483
|
+
|
|
484
|
+
Run \`ovld protocol load-context --ticket-id <ticketId>\` using \`$ARGUMENTS\` as the ticket ID. If no ticket ID was provided, ask the user for one and stop.`
|
|
485
|
+
},
|
|
486
|
+
{
|
|
487
|
+
path: path.join(base, 'create.md'),
|
|
488
|
+
content: `---
|
|
489
|
+
description: Create a draft Overlord ticket from the current conversation
|
|
490
|
+
agent: build
|
|
491
|
+
---
|
|
492
|
+
|
|
493
|
+
Run \`ovld protocol create --agent opencode\` with \`$ARGUMENTS\`. If no flags are present, treat the arguments as the objective and call \`ovld protocol create --agent opencode --objective "<objective>"\`.`
|
|
494
|
+
},
|
|
495
|
+
{
|
|
496
|
+
path: path.join(base, 'prompt.md'),
|
|
497
|
+
content: `---
|
|
498
|
+
description: Create a new Overlord ticket from the current conversation
|
|
499
|
+
agent: build
|
|
500
|
+
---
|
|
501
|
+
|
|
502
|
+
Run \`ovld protocol prompt --agent opencode\` with \`$ARGUMENTS\`. If no flags are present, treat the arguments as the objective and call \`ovld protocol prompt --agent opencode --objective "<objective>"\`.`
|
|
503
|
+
}
|
|
504
|
+
];
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
function installSlashCommands(agent) {
|
|
508
|
+
const backups = [];
|
|
509
|
+
const files = slashCommandFiles(agent);
|
|
510
|
+
for (const file of files) {
|
|
511
|
+
const backup = backupFile(file.path);
|
|
512
|
+
if (backup) backups.push(backup);
|
|
513
|
+
writeTextFile(file.path, `${file.content.trim()}\n`);
|
|
514
|
+
console.log(` ✓ Installed slash command: ${file.path}`);
|
|
515
|
+
}
|
|
516
|
+
return {
|
|
517
|
+
backups,
|
|
518
|
+
managedFiles: files.map(file => file.path)
|
|
519
|
+
};
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
function uninstallSlashCommands(agent) {
|
|
523
|
+
const removedFiles = [];
|
|
524
|
+
const files = slashCommandFiles(agent);
|
|
525
|
+
for (const file of files) {
|
|
526
|
+
if (!fs.existsSync(file.path)) continue;
|
|
527
|
+
const existing = readTextFile(file.path);
|
|
528
|
+
if (existing.trim() !== file.content.trim()) continue;
|
|
529
|
+
fs.rmSync(file.path, { force: true });
|
|
530
|
+
removedFiles.push(file.path);
|
|
531
|
+
}
|
|
532
|
+
return { removedFiles };
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
function currentContentHashForAgent(agent) {
|
|
536
|
+
if (agent === 'claude') {
|
|
537
|
+
return claudeContentHash();
|
|
538
|
+
}
|
|
539
|
+
if (agent === 'cursor') {
|
|
540
|
+
return contentHashForDirectory(cursorSourcePluginDir());
|
|
541
|
+
}
|
|
542
|
+
if (agent === 'gemini') {
|
|
543
|
+
return contentHash(
|
|
544
|
+
slashCommandFiles('gemini')
|
|
545
|
+
.map(file => file.content)
|
|
546
|
+
.join('\n')
|
|
547
|
+
);
|
|
548
|
+
}
|
|
549
|
+
if (agent === 'codex') return codexContentHash();
|
|
550
|
+
return contentHash(
|
|
551
|
+
[OPENCODE_AGENTS_SECTION, ...slashCommandFiles('opencode').map(file => file.content)].join('\n')
|
|
552
|
+
);
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
function codexSourcePluginDir() {
|
|
556
|
+
if (fs.existsSync(PACKAGE_PLUGIN_DIR)) return PACKAGE_PLUGIN_DIR;
|
|
557
|
+
if (fs.existsSync(REPO_PLUGIN_DIR)) return REPO_PLUGIN_DIR;
|
|
558
|
+
throw new Error(
|
|
559
|
+
`Codex plugin bundle not found. Checked ${PACKAGE_PLUGIN_DIR} and ${REPO_PLUGIN_DIR}.`
|
|
560
|
+
);
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
function claudeSourcePluginDir() {
|
|
564
|
+
if (fs.existsSync(PACKAGE_CLAUDE_PLUGIN_DIR)) return PACKAGE_CLAUDE_PLUGIN_DIR;
|
|
565
|
+
if (fs.existsSync(REPO_CLAUDE_PLUGIN_DIR)) return REPO_CLAUDE_PLUGIN_DIR;
|
|
566
|
+
throw new Error(
|
|
567
|
+
`Claude plugin bundle not found. Checked ${PACKAGE_CLAUDE_PLUGIN_DIR} and ${REPO_CLAUDE_PLUGIN_DIR}.`
|
|
568
|
+
);
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
function cursorSourcePluginDir() {
|
|
572
|
+
if (fs.existsSync(PACKAGE_CURSOR_PLUGIN_DIR)) return PACKAGE_CURSOR_PLUGIN_DIR;
|
|
573
|
+
if (fs.existsSync(REPO_CURSOR_PLUGIN_DIR)) return REPO_CURSOR_PLUGIN_DIR;
|
|
574
|
+
throw new Error(
|
|
575
|
+
`Cursor plugin bundle not found. Checked ${PACKAGE_CURSOR_PLUGIN_DIR} and ${REPO_CURSOR_PLUGIN_DIR}.`
|
|
576
|
+
);
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
function listFilesRecursive(dir) {
|
|
580
|
+
if (!fs.existsSync(dir)) return [];
|
|
581
|
+
return fs.readdirSync(dir, { withFileTypes: true }).flatMap(entry => {
|
|
582
|
+
const resolved = path.join(dir, entry.name);
|
|
583
|
+
if (entry.isDirectory()) return listFilesRecursive(resolved);
|
|
584
|
+
return [resolved];
|
|
585
|
+
});
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
function contentHashForDirectory(sourceDir) {
|
|
589
|
+
const hash = crypto.createHash('sha256');
|
|
590
|
+
|
|
591
|
+
for (const filePath of listFilesRecursive(sourceDir).sort()) {
|
|
592
|
+
hash.update(path.relative(sourceDir, filePath));
|
|
593
|
+
hash.update('\0');
|
|
594
|
+
hash.update(fs.readFileSync(filePath));
|
|
595
|
+
hash.update('\0');
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
return hash.digest('hex').slice(0, 16);
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
function codexContentHash() {
|
|
602
|
+
return contentHashForDirectory(codexSourcePluginDir());
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
function claudeContentHash() {
|
|
606
|
+
return contentHashForDirectory(claudeSourcePluginDir());
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
function mergeCodexRules(existingContent) {
|
|
610
|
+
const managedBlock = [
|
|
611
|
+
CODEX_RULES_START,
|
|
612
|
+
'prefix_rule(',
|
|
613
|
+
' pattern = ["npx", "overlord", "protocol"],',
|
|
614
|
+
' decision = "allow",',
|
|
615
|
+
' justification = "Allow all Overlord protocol commands without prompts.",',
|
|
616
|
+
')',
|
|
617
|
+
'',
|
|
618
|
+
'prefix_rule(',
|
|
619
|
+
' pattern = ["ovld", "protocol"],',
|
|
620
|
+
' decision = "allow",',
|
|
621
|
+
' justification = "Allow all Overlord protocol commands without prompts.",',
|
|
622
|
+
')',
|
|
623
|
+
'',
|
|
624
|
+
'prefix_rule(',
|
|
625
|
+
' pattern = ["curl", "-sS", "-X", "POST"],',
|
|
626
|
+
' decision = "allow",',
|
|
627
|
+
' justification = "Allow curl protocol POST commands without prompts.",',
|
|
628
|
+
')',
|
|
629
|
+
CODEX_RULES_END
|
|
630
|
+
].join('\n');
|
|
631
|
+
|
|
632
|
+
const startIndex = existingContent.indexOf(CODEX_RULES_START);
|
|
633
|
+
const endIndex = existingContent.indexOf(CODEX_RULES_END);
|
|
634
|
+
|
|
635
|
+
if (startIndex !== -1 && endIndex !== -1 && endIndex > startIndex) {
|
|
636
|
+
const before = existingContent.slice(0, startIndex).trimEnd();
|
|
637
|
+
const after = existingContent.slice(endIndex + CODEX_RULES_END.length).trimStart();
|
|
638
|
+
if (!before && !after) return `${managedBlock}\n`;
|
|
639
|
+
if (!before) return `${managedBlock}\n\n${after}`;
|
|
640
|
+
if (!after) return `${before}\n\n${managedBlock}\n`;
|
|
641
|
+
return `${before}\n\n${managedBlock}\n\n${after}`;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
const trimmed = existingContent.trimEnd();
|
|
645
|
+
if (!trimmed) return `${managedBlock}\n`;
|
|
646
|
+
return `${trimmed}\n\n${managedBlock}\n`;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
function pluginVersion(filePath) {
|
|
650
|
+
const parsed = readJsonFileOrNull(filePath);
|
|
651
|
+
return typeof parsed?.version === 'string' ? parsed.version : null;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
function upsertCodexMarketplaceEntry() {
|
|
655
|
+
const current = readJsonFileOrNull(CODEX_TARGET_MARKETPLACE) ?? {
|
|
656
|
+
name: 'overlord-local',
|
|
657
|
+
interface: { displayName: 'Overlord Local Plugins' },
|
|
658
|
+
plugins: []
|
|
659
|
+
};
|
|
660
|
+
|
|
661
|
+
const nextPlugins = Array.isArray(current.plugins) ? [...current.plugins] : [];
|
|
662
|
+
const entry = {
|
|
663
|
+
name: 'overlord',
|
|
664
|
+
source: {
|
|
665
|
+
source: 'local',
|
|
666
|
+
path: './.codex/plugins/overlord'
|
|
667
|
+
},
|
|
668
|
+
policy: {
|
|
669
|
+
installation: 'AVAILABLE',
|
|
670
|
+
authentication: 'ON_INSTALL'
|
|
671
|
+
},
|
|
672
|
+
category: 'Productivity'
|
|
673
|
+
};
|
|
674
|
+
|
|
675
|
+
const existingIndex = nextPlugins.findIndex(plugin => plugin?.name === 'overlord');
|
|
676
|
+
if (existingIndex === -1) nextPlugins.push(entry);
|
|
677
|
+
else nextPlugins[existingIndex] = entry;
|
|
678
|
+
|
|
679
|
+
writeJsonFile(CODEX_TARGET_MARKETPLACE, {
|
|
680
|
+
name: current.name ?? 'overlord-local',
|
|
681
|
+
interface: {
|
|
682
|
+
displayName: current.interface?.displayName ?? 'Overlord Local Plugins'
|
|
683
|
+
},
|
|
684
|
+
plugins: nextPlugins
|
|
685
|
+
});
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
function removeLegacyCodexBundle() {
|
|
689
|
+
if (fs.existsSync(CODEX_LEGACY_AGENTS)) {
|
|
690
|
+
const existing = readTextFile(CODEX_LEGACY_AGENTS);
|
|
691
|
+
const startIndex = existing.indexOf(MD_MARKER_START);
|
|
692
|
+
const endIndex = existing.indexOf(MD_MARKER_END);
|
|
693
|
+
|
|
694
|
+
if (startIndex !== -1 && endIndex !== -1 && endIndex > startIndex) {
|
|
695
|
+
const before = existing.slice(0, startIndex).trimEnd();
|
|
696
|
+
const after = existing.slice(endIndex + MD_MARKER_END.length).trimStart();
|
|
697
|
+
const cleaned =
|
|
698
|
+
!before && !after
|
|
699
|
+
? ''
|
|
700
|
+
: !before
|
|
701
|
+
? `${after}\n`
|
|
702
|
+
: !after
|
|
703
|
+
? `${before}\n`
|
|
704
|
+
: `${before}\n\n${after}\n`;
|
|
705
|
+
|
|
706
|
+
if (cleaned.trim().length > 0) {
|
|
707
|
+
writeTextFile(CODEX_LEGACY_AGENTS, cleaned);
|
|
708
|
+
} else {
|
|
709
|
+
fs.rmSync(CODEX_LEGACY_AGENTS, { force: true });
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
const manifest = readManifest();
|
|
715
|
+
if (!manifest.codex) return;
|
|
716
|
+
delete manifest.codex;
|
|
717
|
+
writeManifest(manifest);
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
function removeLegacyClaudeBundle() {
|
|
721
|
+
const paths = claudePaths();
|
|
722
|
+
const removed = [];
|
|
723
|
+
|
|
724
|
+
if (fs.existsSync(paths.skillDir)) {
|
|
725
|
+
fs.rmSync(paths.skillDir, { recursive: true, force: true });
|
|
726
|
+
removed.push(paths.skillDir);
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
if (fs.existsSync(paths.hookScript)) {
|
|
730
|
+
fs.rmSync(paths.hookScript, { force: true });
|
|
731
|
+
removed.push(paths.hookScript);
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
const slashResult = uninstallSlashCommands('claude');
|
|
735
|
+
removed.push(...slashResult.removedFiles);
|
|
736
|
+
|
|
737
|
+
if (fs.existsSync(paths.settingsFile)) {
|
|
738
|
+
const settings = readJsonFile(paths.settingsFile);
|
|
739
|
+
let changed = false;
|
|
740
|
+
|
|
741
|
+
if (settings.__overlord_managed) {
|
|
742
|
+
delete settings.__overlord_managed;
|
|
743
|
+
changed = true;
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
const hooks = settings.hooks && typeof settings.hooks === 'object' ? settings.hooks : {};
|
|
747
|
+
if (Array.isArray(hooks.PermissionRequest)) {
|
|
748
|
+
const nextPermissionHooks = hooks.PermissionRequest.filter(hook => {
|
|
749
|
+
if (hook && typeof hook === 'object' && Array.isArray(hook.hooks)) {
|
|
750
|
+
return !hook.hooks.some(
|
|
751
|
+
inner =>
|
|
752
|
+
typeof inner?.command === 'string' &&
|
|
753
|
+
inner.command.includes('overlord-permission-hook')
|
|
754
|
+
);
|
|
755
|
+
}
|
|
756
|
+
return true;
|
|
757
|
+
});
|
|
758
|
+
if (nextPermissionHooks.length !== hooks.PermissionRequest.length) {
|
|
759
|
+
changed = true;
|
|
760
|
+
if (nextPermissionHooks.length > 0) hooks.PermissionRequest = nextPermissionHooks;
|
|
761
|
+
else delete hooks.PermissionRequest;
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
if (changed) {
|
|
766
|
+
if (Object.keys(hooks).length > 0) settings.hooks = hooks;
|
|
767
|
+
else delete settings.hooks;
|
|
768
|
+
writeJsonFile(paths.settingsFile, settings);
|
|
769
|
+
removed.push(paths.settingsFile);
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
return removed;
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
// ---------------------------------------------------------------------------
|
|
777
|
+
// Install
|
|
778
|
+
// ---------------------------------------------------------------------------
|
|
779
|
+
|
|
780
|
+
function claudePaths() {
|
|
781
|
+
const base = path.join(os.homedir(), '.claude');
|
|
782
|
+
return {
|
|
783
|
+
skillDir: path.join(base, 'skills', 'overlord-local'),
|
|
784
|
+
skillFile: path.join(base, 'skills', 'overlord-local', 'SKILL.md'),
|
|
785
|
+
settingsFile: path.join(base, 'settings.json'),
|
|
786
|
+
hookScript: path.join(base, 'overlord-permission-hook.sh')
|
|
787
|
+
};
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
function openCodePaths() {
|
|
791
|
+
const base = path.join(os.homedir(), '.config', 'opencode');
|
|
792
|
+
return {
|
|
793
|
+
agentsFile: path.join(base, 'AGENTS.md'),
|
|
794
|
+
configFile: path.join(base, 'opencode.json'),
|
|
795
|
+
commandsDir: path.join(base, 'commands')
|
|
796
|
+
};
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
function cursorPaths() {
|
|
800
|
+
const base = path.join(os.homedir(), '.cursor');
|
|
801
|
+
return {
|
|
802
|
+
pluginDir: path.join(base, 'plugins', 'local', 'overlord'),
|
|
803
|
+
pluginManifest: path.join(
|
|
804
|
+
base,
|
|
805
|
+
'plugins',
|
|
806
|
+
'local',
|
|
807
|
+
'overlord',
|
|
808
|
+
'.cursor-plugin',
|
|
809
|
+
'plugin.json'
|
|
810
|
+
),
|
|
811
|
+
rulesFile: path.join(base, 'rules', 'overlord-local.mdc'),
|
|
812
|
+
settingsFile: path.join(base, 'settings.json')
|
|
813
|
+
};
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
function geminiPaths() {
|
|
817
|
+
const base = path.join(os.homedir(), '.gemini');
|
|
818
|
+
return {
|
|
819
|
+
policyFile: path.join(base, 'policies', 'overlord-protocol.toml')
|
|
820
|
+
};
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
function installClaude() {
|
|
824
|
+
const sourceDir = claudeSourcePluginDir();
|
|
825
|
+
const sourceManifest = path.join(sourceDir, '.claude-plugin', 'plugin.json');
|
|
826
|
+
const sourceVersion = pluginVersion(sourceManifest) ?? '0.0.0';
|
|
827
|
+
const removed = removeLegacyClaudeBundle();
|
|
828
|
+
|
|
829
|
+
console.log(` ✓ Found Claude plugin source: ${sourceDir}`);
|
|
830
|
+
if (removed.length > 0) {
|
|
831
|
+
console.log(' ✓ Migrated v3.25 Claude connector files:');
|
|
832
|
+
for (const filePath of removed) console.log(` ${filePath}`);
|
|
833
|
+
} else {
|
|
834
|
+
console.log(' ✓ No v3.25 Claude connector files needed migration.');
|
|
835
|
+
}
|
|
836
|
+
console.log(' ✓ `ovld launch claude` now loads this plugin with `claude --plugin-dir`.');
|
|
837
|
+
|
|
838
|
+
const manifest = readManifest();
|
|
839
|
+
manifest.claude = {
|
|
840
|
+
version: sourceVersion,
|
|
841
|
+
contentHash: currentContentHashForAgent('claude'),
|
|
842
|
+
installedAt: new Date().toISOString(),
|
|
843
|
+
files: listFilesRecursive(sourceDir)
|
|
844
|
+
};
|
|
845
|
+
writeManifest(manifest);
|
|
846
|
+
|
|
847
|
+
return { ok: true, backups: [] };
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
function installOpenCode() {
|
|
851
|
+
const paths = openCodePaths();
|
|
852
|
+
const backups = [];
|
|
853
|
+
|
|
854
|
+
const agentsBackup = backupFile(paths.agentsFile);
|
|
855
|
+
if (agentsBackup) {
|
|
856
|
+
backups.push(agentsBackup);
|
|
857
|
+
console.log(` ✓ Backed up: ${paths.agentsFile} → ${path.basename(agentsBackup)}`);
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
const existingAgents = readTextFile(paths.agentsFile);
|
|
861
|
+
const mergedAgents = mergeMarkdownSection(existingAgents, OPENCODE_AGENTS_SECTION);
|
|
862
|
+
writeTextFile(paths.agentsFile, mergedAgents);
|
|
863
|
+
console.log(` ✓ Installed agents config: ${paths.agentsFile}`);
|
|
864
|
+
|
|
865
|
+
const configBackup = backupFile(paths.configFile);
|
|
866
|
+
if (configBackup) {
|
|
867
|
+
backups.push(configBackup);
|
|
868
|
+
console.log(` ✓ Backed up: ${paths.configFile} → ${path.basename(configBackup)}`);
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
const existingConfig = readJsonFile(paths.configFile);
|
|
872
|
+
const existingInstructions = Array.isArray(existingConfig.instructions)
|
|
873
|
+
? existingConfig.instructions.filter(v => typeof v === 'string' && v.trim())
|
|
874
|
+
: [];
|
|
875
|
+
const existingPermission =
|
|
876
|
+
existingConfig.permission && typeof existingConfig.permission === 'object'
|
|
877
|
+
? existingConfig.permission
|
|
878
|
+
: {};
|
|
879
|
+
const existingBash =
|
|
880
|
+
existingPermission.bash && typeof existingPermission.bash === 'object'
|
|
881
|
+
? existingPermission.bash
|
|
882
|
+
: {};
|
|
883
|
+
|
|
884
|
+
writeJsonFile(paths.configFile, {
|
|
885
|
+
...existingConfig,
|
|
886
|
+
$schema: 'https://opencode.ai/config.json',
|
|
887
|
+
instructions: Array.from(new Set([...existingInstructions, paths.agentsFile])),
|
|
888
|
+
permission: {
|
|
889
|
+
...existingPermission,
|
|
890
|
+
bash: {
|
|
891
|
+
'*': 'ask',
|
|
892
|
+
...existingBash,
|
|
893
|
+
'ovld protocol *': 'allow'
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
});
|
|
897
|
+
console.log(` ✓ Updated config: ${paths.configFile}`);
|
|
898
|
+
|
|
899
|
+
const slashResult = installSlashCommands('opencode');
|
|
900
|
+
backups.push(...slashResult.backups);
|
|
901
|
+
|
|
902
|
+
const manifest = readManifest();
|
|
903
|
+
manifest.opencode = {
|
|
904
|
+
version: BUNDLE_VERSION,
|
|
905
|
+
contentHash: currentContentHashForAgent('opencode'),
|
|
906
|
+
installedAt: new Date().toISOString(),
|
|
907
|
+
files: [paths.agentsFile, paths.configFile, ...slashResult.managedFiles]
|
|
908
|
+
};
|
|
909
|
+
writeManifest(manifest);
|
|
910
|
+
|
|
911
|
+
return { ok: true, backups };
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
function installCodex() {
|
|
915
|
+
const sourceDir = codexSourcePluginDir();
|
|
916
|
+
fs.mkdirSync(path.dirname(CODEX_TARGET_PLUGIN_DIR), { recursive: true });
|
|
917
|
+
fs.rmSync(CODEX_TARGET_PLUGIN_DIR, { recursive: true, force: true });
|
|
918
|
+
fs.cpSync(sourceDir, CODEX_TARGET_PLUGIN_DIR, { recursive: true });
|
|
919
|
+
console.log(` ✓ Installed plugin: ${CODEX_TARGET_PLUGIN_DIR}`);
|
|
920
|
+
|
|
921
|
+
writeTextFile(CODEX_TARGET_RULES, mergeCodexRules(readTextFile(CODEX_TARGET_RULES)));
|
|
922
|
+
console.log(` ✓ Updated rules: ${CODEX_TARGET_RULES}`);
|
|
923
|
+
|
|
924
|
+
upsertCodexMarketplaceEntry();
|
|
925
|
+
console.log(` ✓ Updated marketplace: ${CODEX_TARGET_MARKETPLACE}`);
|
|
926
|
+
|
|
927
|
+
removeLegacyCodexBundle();
|
|
928
|
+
|
|
929
|
+
const installedFiles = [
|
|
930
|
+
...listFilesRecursive(CODEX_TARGET_PLUGIN_DIR),
|
|
931
|
+
CODEX_TARGET_MARKETPLACE,
|
|
932
|
+
CODEX_TARGET_RULES
|
|
933
|
+
];
|
|
934
|
+
const manifest = readManifest();
|
|
935
|
+
manifest.codex = {
|
|
936
|
+
version: pluginVersion(CODEX_TARGET_PLUGIN_MANIFEST) ?? '0.0.0',
|
|
937
|
+
contentHash: codexContentHash(),
|
|
938
|
+
installedAt: new Date().toISOString(),
|
|
939
|
+
files: installedFiles
|
|
940
|
+
};
|
|
941
|
+
writeManifest(manifest);
|
|
942
|
+
|
|
943
|
+
return { ok: true, installedFiles };
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
function installCursor() {
|
|
947
|
+
const paths = cursorPaths();
|
|
948
|
+
const sourceDir = cursorSourcePluginDir();
|
|
949
|
+
fs.mkdirSync(path.dirname(paths.pluginDir), { recursive: true });
|
|
950
|
+
fs.rmSync(paths.pluginDir, { recursive: true, force: true });
|
|
951
|
+
fs.cpSync(sourceDir, paths.pluginDir, { recursive: true });
|
|
952
|
+
console.log(` ✓ Installed plugin: ${paths.pluginDir}`);
|
|
953
|
+
|
|
954
|
+
if (fs.existsSync(paths.rulesFile)) {
|
|
955
|
+
fs.rmSync(paths.rulesFile, { force: true });
|
|
956
|
+
console.log(` ✓ Removed legacy rules file: ${paths.rulesFile}`);
|
|
957
|
+
}
|
|
958
|
+
const removedLegacySlash = uninstallSlashCommands('cursor');
|
|
959
|
+
if (removedLegacySlash.removedFiles.length > 0) {
|
|
960
|
+
console.log(' ✓ Removed legacy slash commands:');
|
|
961
|
+
for (const filePath of removedLegacySlash.removedFiles) console.log(` ${filePath}`);
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
const existingSettings = readJsonFile(paths.settingsFile);
|
|
965
|
+
const permissions =
|
|
966
|
+
existingSettings.permissions && typeof existingSettings.permissions === 'object'
|
|
967
|
+
? existingSettings.permissions
|
|
968
|
+
: {};
|
|
969
|
+
const mergedAllow = Array.from(
|
|
970
|
+
new Set([...asStringArray(permissions.allow), 'Shell(ovld protocol:*)'])
|
|
971
|
+
);
|
|
972
|
+
writeJsonFile(paths.settingsFile, {
|
|
973
|
+
...existingSettings,
|
|
974
|
+
permissions: {
|
|
975
|
+
...permissions,
|
|
976
|
+
allow: mergedAllow
|
|
977
|
+
}
|
|
978
|
+
});
|
|
979
|
+
console.log(` ✓ Updated permissions: ${paths.settingsFile}`);
|
|
980
|
+
|
|
981
|
+
const manifest = readManifest();
|
|
982
|
+
manifest.cursor = {
|
|
983
|
+
version: pluginVersion(paths.pluginManifest) ?? '0.0.0',
|
|
984
|
+
contentHash: currentContentHashForAgent('cursor'),
|
|
985
|
+
installedAt: new Date().toISOString(),
|
|
986
|
+
files: [...listFilesRecursive(paths.pluginDir), paths.settingsFile]
|
|
987
|
+
};
|
|
988
|
+
writeManifest(manifest);
|
|
989
|
+
|
|
990
|
+
return { ok: true };
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
function installGemini() {
|
|
994
|
+
const slashResult = installSlashCommands('gemini');
|
|
995
|
+
const paths = geminiPaths();
|
|
996
|
+
const policyContent = [
|
|
997
|
+
'# Managed by Overlord onboarding',
|
|
998
|
+
'[[rule]]',
|
|
999
|
+
'toolName = "run_shell_command"',
|
|
1000
|
+
'commandPrefix = "ovld protocol"',
|
|
1001
|
+
'decision = "allow"',
|
|
1002
|
+
'priority = 900',
|
|
1003
|
+
''
|
|
1004
|
+
].join('\n');
|
|
1005
|
+
writeTextFile(paths.policyFile, policyContent);
|
|
1006
|
+
console.log(` ✓ Installed policy: ${paths.policyFile}`);
|
|
1007
|
+
|
|
1008
|
+
const manifest = readManifest();
|
|
1009
|
+
manifest.gemini = {
|
|
1010
|
+
version: BUNDLE_VERSION,
|
|
1011
|
+
contentHash: currentContentHashForAgent('gemini'),
|
|
1012
|
+
installedAt: new Date().toISOString(),
|
|
1013
|
+
files: [paths.policyFile, ...slashResult.managedFiles]
|
|
1014
|
+
};
|
|
1015
|
+
writeManifest(manifest);
|
|
1016
|
+
|
|
1017
|
+
return { ok: true };
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
// ---------------------------------------------------------------------------
|
|
1021
|
+
// Doctor
|
|
1022
|
+
// ---------------------------------------------------------------------------
|
|
1023
|
+
|
|
1024
|
+
function doctorAgent(agent) {
|
|
1025
|
+
const manifest = readManifest();
|
|
1026
|
+
const entry = manifest[agent];
|
|
1027
|
+
|
|
1028
|
+
if (!entry) {
|
|
1029
|
+
console.log(` ✗ ${agent}: not installed`);
|
|
1030
|
+
return false;
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
const currentVersion =
|
|
1034
|
+
agent === 'claude'
|
|
1035
|
+
? pluginVersion(path.join(claudeSourcePluginDir(), '.claude-plugin', 'plugin.json'))
|
|
1036
|
+
: agent === 'codex'
|
|
1037
|
+
? pluginVersion(path.join(codexSourcePluginDir(), '.codex-plugin', 'plugin.json'))
|
|
1038
|
+
: agent === 'cursor'
|
|
1039
|
+
? pluginVersion(path.join(cursorSourcePluginDir(), '.cursor-plugin', 'plugin.json'))
|
|
1040
|
+
: BUNDLE_VERSION;
|
|
1041
|
+
const currentHash = currentContentHashForAgent(agent);
|
|
1042
|
+
|
|
1043
|
+
if (entry.version !== currentVersion || entry.contentHash !== currentHash) {
|
|
1044
|
+
console.log(
|
|
1045
|
+
` ⚠ ${agent}: stale (installed v${entry.version}, current v${currentVersion ?? 'unknown'})`
|
|
1046
|
+
);
|
|
1047
|
+
return false;
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
const missingFiles = entry.files.filter(f => !fs.existsSync(f));
|
|
1051
|
+
if (missingFiles.length > 0) {
|
|
1052
|
+
console.log(` ⚠ ${agent}: partial — missing files:`);
|
|
1053
|
+
for (const f of missingFiles) console.log(` ${f}`);
|
|
1054
|
+
return false;
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
console.log(` ✓ ${agent}: installed (v${entry.version})`);
|
|
1058
|
+
return true;
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
function currentNodeMajor() {
|
|
1062
|
+
return Number.parseInt(process.versions.node.split('.')[0] ?? '', 10);
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
// ---------------------------------------------------------------------------
|
|
1066
|
+
// Interactive checkbox prompt
|
|
1067
|
+
// ---------------------------------------------------------------------------
|
|
1068
|
+
|
|
1069
|
+
/**
|
|
1070
|
+
* Run an interactive checkbox list (multiselect with spacebar).
|
|
1071
|
+
*
|
|
1072
|
+
* @param {object} opts
|
|
1073
|
+
* @param {string} opts.message - Prompt message shown above the list
|
|
1074
|
+
* @param {string[]} opts.choices - List of choice labels
|
|
1075
|
+
* @param {string[]} [opts.defaults] - Initially selected choices
|
|
1076
|
+
* @returns {Promise<string[]>} - Array of selected choice labels
|
|
1077
|
+
*/
|
|
1078
|
+
function runCheckboxPrompt({ message, choices, defaults = [] }) {
|
|
1079
|
+
return new Promise(resolve => {
|
|
1080
|
+
const hide = '\x1b[?25l';
|
|
1081
|
+
const show = '\x1b[?25h';
|
|
1082
|
+
const saveCursor = '\x1b7';
|
|
1083
|
+
const restoreCursor = '\x1b8';
|
|
1084
|
+
const eraseBelow = '\x1b[J';
|
|
1085
|
+
const cyan = s => `\x1b[36m${s}\x1b[0m`;
|
|
1086
|
+
const bold = s => `\x1b[1m${s}\x1b[0m`;
|
|
1087
|
+
const dim = s => `\x1b[2m${s}\x1b[0m`;
|
|
1088
|
+
|
|
1089
|
+
let cursorIdx = 0;
|
|
1090
|
+
let selected = new Set(defaults);
|
|
1091
|
+
let hasRendered = false;
|
|
1092
|
+
|
|
1093
|
+
function render() {
|
|
1094
|
+
const lines = [];
|
|
1095
|
+
lines.push(bold(message));
|
|
1096
|
+
lines.push(dim(' ↑↓ navigate · Space toggle · Enter confirm · Esc cancel'));
|
|
1097
|
+
lines.push('');
|
|
1098
|
+
|
|
1099
|
+
for (let i = 0; i < choices.length; i++) {
|
|
1100
|
+
const choice = choices[i];
|
|
1101
|
+
const isSelected = selected.has(choice);
|
|
1102
|
+
const isCursor = i === cursorIdx;
|
|
1103
|
+
const checkbox = isSelected ? '[✓]' : '[ ]';
|
|
1104
|
+
const marker = isCursor ? cyan('▶') : ' ';
|
|
1105
|
+
const label = isCursor ? bold(choice) : choice;
|
|
1106
|
+
lines.push(` ${marker} ${checkbox} ${label}`);
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
if (hasRendered) {
|
|
1110
|
+
process.stdout.write(restoreCursor + eraseBelow);
|
|
1111
|
+
}
|
|
1112
|
+
process.stdout.write(saveCursor + lines.join('\n'));
|
|
1113
|
+
hasRendered = true;
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
function cleanup() {
|
|
1117
|
+
if (hasRendered) {
|
|
1118
|
+
process.stdout.write(restoreCursor + eraseBelow);
|
|
1119
|
+
}
|
|
1120
|
+
process.stdin.setRawMode(false);
|
|
1121
|
+
process.stdin.removeAllListeners('data');
|
|
1122
|
+
process.stdout.write(show);
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
process.stdin.setRawMode(true);
|
|
1126
|
+
process.stdin.resume();
|
|
1127
|
+
process.stdin.setEncoding('utf8');
|
|
1128
|
+
process.stdout.write(hide);
|
|
1129
|
+
render();
|
|
1130
|
+
|
|
1131
|
+
process.stdin.on('data', key => {
|
|
1132
|
+
// Ctrl-C / Ctrl-D → exit
|
|
1133
|
+
if (key === '\x03' || key === '\x04') {
|
|
1134
|
+
cleanup();
|
|
1135
|
+
process.exit(0);
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
// Escape → cancel
|
|
1139
|
+
if (key === '\x1b') {
|
|
1140
|
+
cleanup();
|
|
1141
|
+
resolve([]);
|
|
1142
|
+
return;
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
// Enter → confirm selection
|
|
1146
|
+
if (key === '\r' || key === '\n') {
|
|
1147
|
+
cleanup();
|
|
1148
|
+
resolve(Array.from(selected));
|
|
1149
|
+
return;
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
// Arrow up
|
|
1153
|
+
if (key === '\x1b[A') {
|
|
1154
|
+
cursorIdx = (cursorIdx - 1 + choices.length) % choices.length;
|
|
1155
|
+
render();
|
|
1156
|
+
return;
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
// Arrow down
|
|
1160
|
+
if (key === '\x1b[B') {
|
|
1161
|
+
cursorIdx = (cursorIdx + 1) % choices.length;
|
|
1162
|
+
render();
|
|
1163
|
+
return;
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
// Spacebar → toggle selection
|
|
1167
|
+
if (key === ' ') {
|
|
1168
|
+
const choice = choices[cursorIdx];
|
|
1169
|
+
if (selected.has(choice)) {
|
|
1170
|
+
selected.delete(choice);
|
|
1171
|
+
} else {
|
|
1172
|
+
selected.add(choice);
|
|
1173
|
+
}
|
|
1174
|
+
render();
|
|
1175
|
+
return;
|
|
1176
|
+
}
|
|
1177
|
+
});
|
|
1178
|
+
});
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
/**
|
|
1182
|
+
* Ask a yes/no question interactively.
|
|
1183
|
+
*
|
|
1184
|
+
* @param {string} question - The question to ask
|
|
1185
|
+
* @param {boolean} defaultYes - Default answer if user just presses Enter
|
|
1186
|
+
* @returns {Promise<boolean>} - true if yes, false if no
|
|
1187
|
+
*/
|
|
1188
|
+
function askYesNo(question, defaultYes = true) {
|
|
1189
|
+
return new Promise(resolve => {
|
|
1190
|
+
const hide = '\x1b[?25l';
|
|
1191
|
+
const show = '\x1b[?25h';
|
|
1192
|
+
const saveCursor = '\x1b7';
|
|
1193
|
+
const restoreCursor = '\x1b8';
|
|
1194
|
+
const eraseBelow = '\x1b[J';
|
|
1195
|
+
const cyan = s => `\x1b[36m${s}\x1b[0m`;
|
|
1196
|
+
const bold = s => `\x1b[1m${s}\x1b[0m`;
|
|
1197
|
+
const dim = s => `\x1b[2m${s}\x1b[0m`;
|
|
1198
|
+
|
|
1199
|
+
const choices = ['Yes', 'No'];
|
|
1200
|
+
let cursorIdx = defaultYes ? 0 : 1;
|
|
1201
|
+
let hasRendered = false;
|
|
1202
|
+
|
|
1203
|
+
function render() {
|
|
1204
|
+
const lines = [];
|
|
1205
|
+
lines.push(bold(question));
|
|
1206
|
+
lines.push('');
|
|
1207
|
+
|
|
1208
|
+
for (let i = 0; i < choices.length; i++) {
|
|
1209
|
+
const choice = choices[i];
|
|
1210
|
+
const isCursor = i === cursorIdx;
|
|
1211
|
+
const marker = isCursor ? cyan('▶') : ' ';
|
|
1212
|
+
const label = isCursor ? bold(choice) : choice;
|
|
1213
|
+
lines.push(` ${marker} ${label}`);
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
lines.push('');
|
|
1217
|
+
lines.push(dim(' ↑↓ navigate · Enter confirm · Esc cancel'));
|
|
1218
|
+
|
|
1219
|
+
if (hasRendered) {
|
|
1220
|
+
process.stdout.write(restoreCursor + eraseBelow);
|
|
1221
|
+
}
|
|
1222
|
+
process.stdout.write(saveCursor + lines.join('\n'));
|
|
1223
|
+
hasRendered = true;
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
function cleanup() {
|
|
1227
|
+
if (hasRendered) {
|
|
1228
|
+
process.stdout.write(restoreCursor + eraseBelow);
|
|
1229
|
+
}
|
|
1230
|
+
process.stdin.setRawMode(false);
|
|
1231
|
+
process.stdin.removeAllListeners('data');
|
|
1232
|
+
process.stdout.write(show);
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
process.stdin.setRawMode(true);
|
|
1236
|
+
process.stdin.resume();
|
|
1237
|
+
process.stdin.setEncoding('utf8');
|
|
1238
|
+
process.stdout.write(hide);
|
|
1239
|
+
render();
|
|
1240
|
+
|
|
1241
|
+
process.stdin.on('data', key => {
|
|
1242
|
+
// Ctrl-C / Ctrl-D → exit
|
|
1243
|
+
if (key === '\x03' || key === '\x04') {
|
|
1244
|
+
cleanup();
|
|
1245
|
+
process.exit(0);
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
// Escape → cancel (default to No)
|
|
1249
|
+
if (key === '\x1b') {
|
|
1250
|
+
cleanup();
|
|
1251
|
+
resolve(false);
|
|
1252
|
+
return;
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
// Enter → confirm selection
|
|
1256
|
+
if (key === '\r' || key === '\n') {
|
|
1257
|
+
cleanup();
|
|
1258
|
+
resolve(cursorIdx === 0);
|
|
1259
|
+
return;
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
// Arrow up
|
|
1263
|
+
if (key === '\x1b[A') {
|
|
1264
|
+
cursorIdx = (cursorIdx - 1 + choices.length) % choices.length;
|
|
1265
|
+
render();
|
|
1266
|
+
return;
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
// Arrow down
|
|
1270
|
+
if (key === '\x1b[B') {
|
|
1271
|
+
cursorIdx = (cursorIdx + 1) % choices.length;
|
|
1272
|
+
render();
|
|
1273
|
+
return;
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
// y/Y → yes
|
|
1277
|
+
if (key === 'y' || key === 'Y') {
|
|
1278
|
+
cleanup();
|
|
1279
|
+
resolve(true);
|
|
1280
|
+
return;
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
// n/N → no
|
|
1284
|
+
if (key === 'n' || key === 'N') {
|
|
1285
|
+
cleanup();
|
|
1286
|
+
resolve(false);
|
|
1287
|
+
return;
|
|
1288
|
+
}
|
|
1289
|
+
});
|
|
1290
|
+
});
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
// ---------------------------------------------------------------------------
|
|
1294
|
+
// Agent permissions installation
|
|
1295
|
+
// ---------------------------------------------------------------------------
|
|
1296
|
+
|
|
1297
|
+
function getPlatformUrl() {
|
|
1298
|
+
// Check for OVERLORD_URL env var first, otherwise default to localhost
|
|
1299
|
+
return process.env.OVERLORD_URL || 'http://localhost:3000';
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
function installAgentPermissions(agents, platformUrl) {
|
|
1303
|
+
console.log(`\nInstalling agent permissions for: ${agents.join(', ')}`);
|
|
1304
|
+
console.log(`Platform URL: ${platformUrl}\n`);
|
|
1305
|
+
|
|
1306
|
+
for (const agent of agents) {
|
|
1307
|
+
if (agent === 'claude') {
|
|
1308
|
+
installClaudePermissions(platformUrl);
|
|
1309
|
+
} else if (agent === 'opencode') {
|
|
1310
|
+
installOpenCodePermissions(platformUrl);
|
|
1311
|
+
} else if (agent === 'codex') {
|
|
1312
|
+
installCodexPermissions(platformUrl);
|
|
1313
|
+
}
|
|
1314
|
+
// cursor and gemini don't have permission configuration
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
function installClaudePermissions(platformUrl) {
|
|
1319
|
+
const settingsPath = path.join(process.cwd(), '.claude', 'settings.local.json');
|
|
1320
|
+
console.log(`--- Claude Code ---`);
|
|
1321
|
+
console.log(`Settings file: ${settingsPath}`);
|
|
1322
|
+
|
|
1323
|
+
let settings = { permissions: { allow: [] } };
|
|
1324
|
+
if (fs.existsSync(settingsPath)) {
|
|
1325
|
+
try {
|
|
1326
|
+
settings = JSON.parse(fs.readFileSync(settingsPath, 'utf-8'));
|
|
1327
|
+
} catch (e) {
|
|
1328
|
+
console.error(` ERROR: Could not parse ${settingsPath}: ${e.message}`);
|
|
1329
|
+
return false;
|
|
1330
|
+
}
|
|
1331
|
+
if (!settings.permissions) settings.permissions = {};
|
|
1332
|
+
if (!Array.isArray(settings.permissions.allow)) settings.permissions.allow = [];
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
const entries = ['Bash(ovld protocol:*)'];
|
|
1336
|
+
|
|
1337
|
+
const existing = new Set(settings.permissions.allow);
|
|
1338
|
+
const toAdd = entries.filter(e => !existing.has(e));
|
|
1339
|
+
|
|
1340
|
+
if (toAdd.length === 0) {
|
|
1341
|
+
console.log(' All required permissions already present. Nothing to do.\n');
|
|
1342
|
+
return true;
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
console.log(` Adding ${toAdd.length} permission entries:`);
|
|
1346
|
+
for (const entry of toAdd) {
|
|
1347
|
+
console.log(` + ${entry}`);
|
|
1348
|
+
}
|
|
1349
|
+
|
|
1350
|
+
// Backup
|
|
1351
|
+
if (fs.existsSync(settingsPath)) {
|
|
1352
|
+
const ts = new Date().toISOString().replace(/[:.]/g, '-');
|
|
1353
|
+
const backupPath = `${settingsPath}.backup-${ts}`;
|
|
1354
|
+
fs.copyFileSync(settingsPath, backupPath);
|
|
1355
|
+
console.log(` Backup: ${backupPath}`);
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1358
|
+
settings.permissions.allow = [...settings.permissions.allow, ...toAdd];
|
|
1359
|
+
|
|
1360
|
+
const dir = path.dirname(settingsPath);
|
|
1361
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
1362
|
+
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
|
|
1363
|
+
console.log(' Settings updated.\n');
|
|
1364
|
+
return true;
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
function installOpenCodePermissions(_platformUrl) {
|
|
1368
|
+
console.log(`--- OpenCode ---`);
|
|
1369
|
+
const configPath = path.join(os.homedir(), '.config', 'opencode', 'opencode.json');
|
|
1370
|
+
console.log(`Config file: ${configPath}`);
|
|
1371
|
+
|
|
1372
|
+
let config = {};
|
|
1373
|
+
if (fs.existsSync(configPath)) {
|
|
1374
|
+
try {
|
|
1375
|
+
config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
1376
|
+
} catch (e) {
|
|
1377
|
+
console.error(` ERROR: Could not parse ${configPath}: ${e.message}`);
|
|
1378
|
+
return false;
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1382
|
+
const existingPermission =
|
|
1383
|
+
config.permission && typeof config.permission === 'object' ? config.permission : {};
|
|
1384
|
+
const existingBash =
|
|
1385
|
+
existingPermission.bash && typeof existingPermission.bash === 'object'
|
|
1386
|
+
? existingPermission.bash
|
|
1387
|
+
: {};
|
|
1388
|
+
|
|
1389
|
+
const next = {
|
|
1390
|
+
...config,
|
|
1391
|
+
$schema: 'https://opencode.ai/config.json',
|
|
1392
|
+
permission: {
|
|
1393
|
+
...existingPermission,
|
|
1394
|
+
bash: {
|
|
1395
|
+
'*': 'ask',
|
|
1396
|
+
...existingBash,
|
|
1397
|
+
'ovld protocol *': 'allow'
|
|
1398
|
+
}
|
|
1399
|
+
}
|
|
1400
|
+
};
|
|
1401
|
+
|
|
1402
|
+
if (fs.existsSync(configPath)) {
|
|
1403
|
+
const ts = new Date().toISOString().replace(/[:.]/g, '-');
|
|
1404
|
+
const backupPath = `${configPath}.backup-${ts}`;
|
|
1405
|
+
fs.copyFileSync(configPath, backupPath);
|
|
1406
|
+
console.log(` Backup: ${backupPath}`);
|
|
1407
|
+
}
|
|
1408
|
+
|
|
1409
|
+
const dir = path.dirname(configPath);
|
|
1410
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
1411
|
+
fs.writeFileSync(configPath, JSON.stringify(next, null, 2) + '\n');
|
|
1412
|
+
console.log(' Config updated.\n');
|
|
1413
|
+
return true;
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1416
|
+
function installCodexPermissions() {
|
|
1417
|
+
console.log(`--- Codex ---`);
|
|
1418
|
+
console.log(' Codex does not support file-based permission configuration.');
|
|
1419
|
+
console.log(' To warm up permissions, run the following command once inside a Codex session:');
|
|
1420
|
+
console.log(' (Codex will prompt for approval; approve it to persist the prefix.)\n');
|
|
1421
|
+
console.log(' ovld protocol help');
|
|
1422
|
+
console.log();
|
|
1423
|
+
return true;
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1426
|
+
// ---------------------------------------------------------------------------
|
|
1427
|
+
// Public API
|
|
1428
|
+
// ---------------------------------------------------------------------------
|
|
1429
|
+
|
|
1430
|
+
export async function runSetupCommand(args) {
|
|
1431
|
+
const agent = args[0];
|
|
1432
|
+
|
|
1433
|
+
if (agent === '--help' || agent === '-h' || agent === 'help') {
|
|
1434
|
+
console.log(`Usage:
|
|
1435
|
+
ovld setup Interactive setup (select agents and configure permissions)
|
|
1436
|
+
ovld setup claude Prepare the Overlord Claude plugin and migrate v3.25 connector files
|
|
1437
|
+
ovld setup codex Install Overlord Codex plugin bundle
|
|
1438
|
+
ovld setup cursor Install Overlord Cursor local plugin and permissions
|
|
1439
|
+
ovld setup gemini Install Overlord slash commands and policy rules for Gemini CLI
|
|
1440
|
+
ovld setup opencode Install Overlord connector for OpenCode
|
|
1441
|
+
ovld setup all Prepare all supported agents
|
|
1442
|
+
ovld doctor Validate installed connectors and check for CLI updates`);
|
|
1443
|
+
return;
|
|
1444
|
+
}
|
|
1445
|
+
|
|
1446
|
+
// Interactive mode when called without arguments
|
|
1447
|
+
if (!agent) {
|
|
1448
|
+
console.log('Welcome to Overlord agent setup!\n');
|
|
1449
|
+
|
|
1450
|
+
// Step 1: Select agents to install
|
|
1451
|
+
const agentLabels = supportedAgents.map(a => {
|
|
1452
|
+
const descriptions = {
|
|
1453
|
+
claude: 'Claude Code',
|
|
1454
|
+
codex: 'Codex',
|
|
1455
|
+
cursor: 'Cursor',
|
|
1456
|
+
gemini: 'Gemini CLI',
|
|
1457
|
+
opencode: 'OpenCode'
|
|
1458
|
+
};
|
|
1459
|
+
return `${a.padEnd(10)} - ${descriptions[a] || a}`;
|
|
1460
|
+
});
|
|
1461
|
+
|
|
1462
|
+
const selectedLabels = await runCheckboxPrompt({
|
|
1463
|
+
message: 'Select agent plugins/connectors to prepare (Space to toggle, Enter to confirm):',
|
|
1464
|
+
choices: agentLabels,
|
|
1465
|
+
defaults: []
|
|
1466
|
+
});
|
|
1467
|
+
|
|
1468
|
+
if (selectedLabels.length === 0) {
|
|
1469
|
+
console.log('\nNo agents selected. Setup cancelled.');
|
|
1470
|
+
return;
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1473
|
+
// Extract agent names from selected labels
|
|
1474
|
+
const selectedAgents = selectedLabels.map(label => label.split('-')[0].trim());
|
|
1475
|
+
|
|
1476
|
+
// Step 2: Install selected agents
|
|
1477
|
+
console.log(
|
|
1478
|
+
`\nPreparing Overlord agent plugins/connectors for: ${selectedAgents.join(', ')}...\n`
|
|
1479
|
+
);
|
|
1480
|
+
|
|
1481
|
+
const installedAgents = [];
|
|
1482
|
+
for (const a of selectedAgents) {
|
|
1483
|
+
console.log(`[${a}]`);
|
|
1484
|
+
try {
|
|
1485
|
+
if (a === 'claude') installClaude();
|
|
1486
|
+
else if (a === 'codex') installCodex();
|
|
1487
|
+
else if (a === 'cursor') installCursor();
|
|
1488
|
+
else if (a === 'gemini') installGemini();
|
|
1489
|
+
else installOpenCode();
|
|
1490
|
+
installedAgents.push(a);
|
|
1491
|
+
} catch (err) {
|
|
1492
|
+
console.error(` ✗ Failed: ${err.message}`);
|
|
1493
|
+
}
|
|
1494
|
+
console.log();
|
|
1495
|
+
}
|
|
1496
|
+
|
|
1497
|
+
if (installedAgents.length === 0) {
|
|
1498
|
+
console.log('No agents were successfully installed.');
|
|
1499
|
+
return;
|
|
1500
|
+
}
|
|
1501
|
+
|
|
1502
|
+
// Step 3: Offer to configure agent permissions
|
|
1503
|
+
const agentsThatNeedPermissions = installedAgents.filter(a =>
|
|
1504
|
+
['claude', 'codex', 'opencode'].includes(a)
|
|
1505
|
+
);
|
|
1506
|
+
|
|
1507
|
+
if (agentsThatNeedPermissions.length > 0) {
|
|
1508
|
+
console.log('Agent plugins/connectors prepared successfully!\n');
|
|
1509
|
+
|
|
1510
|
+
const shouldInstallPermissions = await askYesNo(
|
|
1511
|
+
'Would you like to configure agent permissions for Overlord protocol access?',
|
|
1512
|
+
true
|
|
1513
|
+
);
|
|
1514
|
+
|
|
1515
|
+
if (shouldInstallPermissions) {
|
|
1516
|
+
const platformUrl = getPlatformUrl();
|
|
1517
|
+
installAgentPermissions(agentsThatNeedPermissions, platformUrl);
|
|
1518
|
+
console.log('✓ Agent permissions configured.\n');
|
|
1519
|
+
} else {
|
|
1520
|
+
console.log('\nSkipped agent permissions configuration.');
|
|
1521
|
+
console.log('You can run the permission installer later with:');
|
|
1522
|
+
console.log(' node scripts/install-agent-permissions.mjs\n');
|
|
1523
|
+
}
|
|
1524
|
+
}
|
|
1525
|
+
|
|
1526
|
+
console.log('Setup complete! Run `ovld doctor` to verify your installation.');
|
|
1527
|
+
return;
|
|
1528
|
+
}
|
|
1529
|
+
|
|
1530
|
+
if (agent === 'all') {
|
|
1531
|
+
console.log('Preparing Overlord agent plugins/connectors for all supported agents...\n');
|
|
1532
|
+
const installedAgents = [];
|
|
1533
|
+
|
|
1534
|
+
for (const a of supportedAgents) {
|
|
1535
|
+
console.log(`[${a}]`);
|
|
1536
|
+
try {
|
|
1537
|
+
if (a === 'claude') installClaude();
|
|
1538
|
+
else if (a === 'codex') installCodex();
|
|
1539
|
+
else if (a === 'cursor') installCursor();
|
|
1540
|
+
else if (a === 'gemini') installGemini();
|
|
1541
|
+
else installOpenCode();
|
|
1542
|
+
installedAgents.push(a);
|
|
1543
|
+
} catch (err) {
|
|
1544
|
+
console.error(` ✗ Failed: ${err.message}`);
|
|
1545
|
+
}
|
|
1546
|
+
console.log();
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1549
|
+
// Offer permissions setup for 'all' command too
|
|
1550
|
+
const agentsThatNeedPermissions = installedAgents.filter(a =>
|
|
1551
|
+
['claude', 'codex', 'opencode'].includes(a)
|
|
1552
|
+
);
|
|
1553
|
+
|
|
1554
|
+
if (agentsThatNeedPermissions.length > 0) {
|
|
1555
|
+
const shouldInstallPermissions = await askYesNo(
|
|
1556
|
+
'\nWould you like to configure agent permissions for Overlord protocol access?',
|
|
1557
|
+
true
|
|
1558
|
+
);
|
|
1559
|
+
|
|
1560
|
+
if (shouldInstallPermissions) {
|
|
1561
|
+
const platformUrl = getPlatformUrl();
|
|
1562
|
+
installAgentPermissions(agentsThatNeedPermissions, platformUrl);
|
|
1563
|
+
}
|
|
1564
|
+
}
|
|
1565
|
+
|
|
1566
|
+
console.log('Done.');
|
|
1567
|
+
return;
|
|
1568
|
+
}
|
|
1569
|
+
|
|
1570
|
+
if (!supportedAgents.includes(agent)) {
|
|
1571
|
+
console.error(
|
|
1572
|
+
`Unknown agent: ${agent ?? '(none)'}. Supported: ${supportedAgents.join(', ')}, all`
|
|
1573
|
+
);
|
|
1574
|
+
process.exit(1);
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1577
|
+
console.log(`Preparing Overlord agent plugin/connector for ${agent}...\n`);
|
|
1578
|
+
try {
|
|
1579
|
+
if (agent === 'claude') installClaude();
|
|
1580
|
+
else if (agent === 'codex') installCodex();
|
|
1581
|
+
else if (agent === 'cursor') installCursor();
|
|
1582
|
+
else if (agent === 'gemini') installGemini();
|
|
1583
|
+
else installOpenCode();
|
|
1584
|
+
console.log('\nDone.');
|
|
1585
|
+
|
|
1586
|
+
// Offer permissions setup for single agent install too
|
|
1587
|
+
if (['claude', 'codex', 'opencode'].includes(agent)) {
|
|
1588
|
+
const shouldInstallPermissions = await askYesNo(
|
|
1589
|
+
'\nWould you like to configure agent permissions for Overlord protocol access?',
|
|
1590
|
+
true
|
|
1591
|
+
);
|
|
1592
|
+
|
|
1593
|
+
if (shouldInstallPermissions) {
|
|
1594
|
+
const platformUrl = getPlatformUrl();
|
|
1595
|
+
installAgentPermissions([agent], platformUrl);
|
|
1596
|
+
}
|
|
1597
|
+
}
|
|
1598
|
+
} catch (err) {
|
|
1599
|
+
console.error(`\nFailed: ${err.message}`);
|
|
1600
|
+
process.exit(1);
|
|
1601
|
+
}
|
|
1602
|
+
}
|
|
1603
|
+
|
|
1604
|
+
export async function runDoctorCommand({ latestCliVersion = null } = {}) {
|
|
1605
|
+
console.log('Overlord agent bundle status:\n');
|
|
1606
|
+
let allOk = true;
|
|
1607
|
+
const nodeMajor = currentNodeMajor();
|
|
1608
|
+
if (Number.isNaN(nodeMajor) || nodeMajor < REQUIRED_NODE_MAJOR) {
|
|
1609
|
+
console.log(
|
|
1610
|
+
` ✗ node: unsupported runtime (${process.version}; requires Node.js ${REQUIRED_NODE_MAJOR}+)`
|
|
1611
|
+
);
|
|
1612
|
+
allOk = false;
|
|
1613
|
+
} else {
|
|
1614
|
+
console.log(` ✓ node: ${process.version}`);
|
|
1615
|
+
}
|
|
1616
|
+
console.log();
|
|
1617
|
+
for (const agent of supportedAgents) {
|
|
1618
|
+
if (!doctorAgent(agent)) allOk = false;
|
|
1619
|
+
}
|
|
1620
|
+
const updateVersion = latestCliVersion ?? (await checkForCliUpdate());
|
|
1621
|
+
console.log();
|
|
1622
|
+
if (allOk) {
|
|
1623
|
+
console.log('All bundles are up to date.');
|
|
1624
|
+
} else {
|
|
1625
|
+
console.log('Run `ovld setup <agent>` or `ovld setup all` to install/repair.');
|
|
1626
|
+
}
|
|
1627
|
+
if (updateVersion) {
|
|
1628
|
+
console.log();
|
|
1629
|
+
printCliUpdateNotice(updateVersion, {
|
|
1630
|
+
currentVersion: getCurrentCliVersion(),
|
|
1631
|
+
stream: process.stdout
|
|
1632
|
+
});
|
|
1633
|
+
}
|
|
1634
|
+
}
|