opencode-agenthub 0.1.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/LICENSE +21 -0
- package/README.md +373 -0
- package/dist/composer/bootstrap.js +493 -0
- package/dist/composer/builtin-assets.js +139 -0
- package/dist/composer/capabilities.js +20 -0
- package/dist/composer/compose.js +824 -0
- package/dist/composer/defaults.js +10 -0
- package/dist/composer/home-transfer.js +288 -0
- package/dist/composer/install-home.js +5 -0
- package/dist/composer/library/README.md +93 -0
- package/dist/composer/library/bundles/auto.json +18 -0
- package/dist/composer/library/bundles/build.json +17 -0
- package/dist/composer/library/bundles/hr-adapter.json +26 -0
- package/dist/composer/library/bundles/hr-cto.json +24 -0
- package/dist/composer/library/bundles/hr-evaluator.json +26 -0
- package/dist/composer/library/bundles/hr-planner.json +26 -0
- package/dist/composer/library/bundles/hr-sourcer.json +24 -0
- package/dist/composer/library/bundles/hr-verifier.json +26 -0
- package/dist/composer/library/bundles/hr.json +35 -0
- package/dist/composer/library/bundles/plan.json +19 -0
- package/dist/composer/library/instructions/hr-boundaries.md +38 -0
- package/dist/composer/library/instructions/hr-protocol.md +102 -0
- package/dist/composer/library/profiles/auto.json +9 -0
- package/dist/composer/library/profiles/hr.json +9 -0
- package/dist/composer/library/souls/auto.md +29 -0
- package/dist/composer/library/souls/build.md +21 -0
- package/dist/composer/library/souls/hr-adapter.md +64 -0
- package/dist/composer/library/souls/hr-cto.md +57 -0
- package/dist/composer/library/souls/hr-evaluator.md +64 -0
- package/dist/composer/library/souls/hr-planner.md +48 -0
- package/dist/composer/library/souls/hr-sourcer.md +70 -0
- package/dist/composer/library/souls/hr-verifier.md +62 -0
- package/dist/composer/library/souls/hr.md +186 -0
- package/dist/composer/library/souls/plan.md +23 -0
- package/dist/composer/library/workflow/auto-mode.json +139 -0
- package/dist/composer/model-utils.js +39 -0
- package/dist/composer/opencode-profile.js +2299 -0
- package/dist/composer/package-manager.js +75 -0
- package/dist/composer/package-version.js +20 -0
- package/dist/composer/platform.js +48 -0
- package/dist/composer/query.js +133 -0
- package/dist/composer/settings.js +400 -0
- package/dist/plugins/opencode-agenthub.js +310 -0
- package/dist/plugins/opencode-question.js +223 -0
- package/dist/plugins/plan-guidance.js +263 -0
- package/dist/plugins/runtime-config.js +57 -0
- package/dist/skills/agenthub-doctor/SKILL.md +238 -0
- package/dist/skills/agenthub-doctor/diagnose.js +213 -0
- package/dist/skills/agenthub-doctor/fix.js +293 -0
- package/dist/skills/agenthub-doctor/index.js +30 -0
- package/dist/skills/agenthub-doctor/interactive.js +756 -0
- package/dist/skills/hr-assembly/SKILL.md +121 -0
- package/dist/skills/hr-final-check/SKILL.md +98 -0
- package/dist/skills/hr-review/SKILL.md +100 -0
- package/dist/skills/hr-staffing/SKILL.md +85 -0
- package/dist/skills/hr-support/bin/sync_sources.py +560 -0
- package/dist/skills/hr-support/bin/validate_staged_package.py +290 -0
- package/dist/skills/hr-support/bin/vendor_stage_mcps.py +234 -0
- package/dist/skills/hr-support/bin/vendor_stage_skills.py +104 -0
- package/dist/types.js +11 -0
- package/package.json +54 -0
|
@@ -0,0 +1,2299 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
import { chmod, mkdir, readdir, readFile, stat, writeFile } from "node:fs/promises";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import readline from "node:readline/promises";
|
|
6
|
+
import {
|
|
7
|
+
agentHubHomeInitialized,
|
|
8
|
+
defaultAgentHubHome,
|
|
9
|
+
defaultHrHome,
|
|
10
|
+
hrHomeInitialized,
|
|
11
|
+
installAgentHubHome,
|
|
12
|
+
installHrOfficeHomeWithOptions,
|
|
13
|
+
promptHubInitAnswers,
|
|
14
|
+
syncBuiltInAgentHubAssets
|
|
15
|
+
} from "./bootstrap.js";
|
|
16
|
+
import {
|
|
17
|
+
getBuiltInManifestKeysForMode,
|
|
18
|
+
listBuiltInAssetNames
|
|
19
|
+
} from "./builtin-assets.js";
|
|
20
|
+
import {
|
|
21
|
+
composeCustomizedAgent,
|
|
22
|
+
composeToolInjection,
|
|
23
|
+
composeWorkspace,
|
|
24
|
+
getDefaultConfigRoot,
|
|
25
|
+
getWorkspaceRuntimeRoot
|
|
26
|
+
} from "./compose.js";
|
|
27
|
+
import { expandProfileAddSelections, listProfileAddCapabilityNames } from "./capabilities.js";
|
|
28
|
+
import {
|
|
29
|
+
exportAgentHubHome,
|
|
30
|
+
importAgentHubHome
|
|
31
|
+
} from "./home-transfer.js";
|
|
32
|
+
import {
|
|
33
|
+
inspectRuntimeConfig,
|
|
34
|
+
resolvePluginConfigRoot,
|
|
35
|
+
summarizeRuntimeFeatureState
|
|
36
|
+
} from "../plugins/runtime-config.js";
|
|
37
|
+
import { readPackageVersion } from "./package-version.js";
|
|
38
|
+
import {
|
|
39
|
+
mergeAgentHubSettingsDefaults,
|
|
40
|
+
hrPrimaryAgentName,
|
|
41
|
+
hrSubagentNames,
|
|
42
|
+
recommendedHrBootstrapModel,
|
|
43
|
+
recommendedHrBootstrapVariant,
|
|
44
|
+
readAgentHubSettings,
|
|
45
|
+
writeAgentHubSettings
|
|
46
|
+
} from "./settings.js";
|
|
47
|
+
import {
|
|
48
|
+
displayHomeConfigPath,
|
|
49
|
+
resolvePythonCommand,
|
|
50
|
+
shouldChmod,
|
|
51
|
+
shouldOfferEnvrc,
|
|
52
|
+
spawnOptions,
|
|
53
|
+
windowsStartupNotice
|
|
54
|
+
} from "./platform.js";
|
|
55
|
+
const cliCommand = "agenthub";
|
|
56
|
+
const compatibilityCliCommand = "opencode-agenthub";
|
|
57
|
+
const printHelp = () => {
|
|
58
|
+
const agentHubHomePath = displayHomeConfigPath("opencode-agenthub");
|
|
59
|
+
const hrHomePath = displayHomeConfigPath("opencode-agenthub-hr");
|
|
60
|
+
const hrSettingsPath = displayHomeConfigPath("opencode-agenthub-hr", ["settings.json"]);
|
|
61
|
+
const hrStagingPath = displayHomeConfigPath("opencode-agenthub-hr", ["staging"]);
|
|
62
|
+
process.stdout.write(`${cliCommand} \u2014 Agent Hub for opencode (requires Node \u2265 18.0.0)
|
|
63
|
+
|
|
64
|
+
USAGE
|
|
65
|
+
${cliCommand} <command> [options]
|
|
66
|
+
|
|
67
|
+
ALIAS
|
|
68
|
+
${compatibilityCliCommand} <command> [options]
|
|
69
|
+
|
|
70
|
+
COMMANDS (everyday)
|
|
71
|
+
start Start My Team (default profile > last profile > auto)
|
|
72
|
+
hr [profile] Enter HR Office or test an HR profile in this workspace
|
|
73
|
+
promote Promote an approved staged HR package into My Team
|
|
74
|
+
|
|
75
|
+
COMMANDS (maintenance)
|
|
76
|
+
backup Back up My Team (personal home only)
|
|
77
|
+
restore Restore My Team from a backup bundle
|
|
78
|
+
upgrade Preview or sync built-in managed assets
|
|
79
|
+
|
|
80
|
+
COMMANDS (advanced)
|
|
81
|
+
setup Initialize the Agent Hub home directory
|
|
82
|
+
list List installed assets (souls, bundles, profiles, skills, more)
|
|
83
|
+
new Create new souls, skills, bundles, profiles, and advanced assets
|
|
84
|
+
plugin Inspect plugin-only runtime health
|
|
85
|
+
doctor Inspect and fix Agent Hub home assets
|
|
86
|
+
compose Create a new profile or bundle scaffold (alias for new profile/bundle)
|
|
87
|
+
|
|
88
|
+
COMMANDS (compatibility aliases)
|
|
89
|
+
run Alias for 'start'
|
|
90
|
+
hub-doctor Alias for 'doctor'
|
|
91
|
+
hub-export Advanced: export a chosen Agent Hub home to a portable directory
|
|
92
|
+
hub-import Advanced: import a previously-exported Agent Hub home
|
|
93
|
+
|
|
94
|
+
BUILT-IN PROFILES (for use with 'start' / 'run')
|
|
95
|
+
auto Default coding agent (Auto + Plan + Build)
|
|
96
|
+
hr HR console with staffing subagents
|
|
97
|
+
|
|
98
|
+
EXAMPLES
|
|
99
|
+
${cliCommand} start
|
|
100
|
+
${cliCommand} start last
|
|
101
|
+
${cliCommand} start set reviewer-team
|
|
102
|
+
${cliCommand} hr
|
|
103
|
+
${cliCommand} hr last
|
|
104
|
+
${cliCommand} hr recruiter-team
|
|
105
|
+
${cliCommand} promote
|
|
106
|
+
${cliCommand} backup --output ./my-team-backup
|
|
107
|
+
${cliCommand} restore --source ./my-team-backup
|
|
108
|
+
${cliCommand} start auto --workspace /path/to/project
|
|
109
|
+
${cliCommand} list
|
|
110
|
+
${cliCommand} list bundles
|
|
111
|
+
${cliCommand} new soul reviewer
|
|
112
|
+
${cliCommand} new profile my-team --from auto --add hr-suite
|
|
113
|
+
${cliCommand} upgrade
|
|
114
|
+
${cliCommand} upgrade --force
|
|
115
|
+
${cliCommand} plugin doctor
|
|
116
|
+
${cliCommand} doctor --fix-all
|
|
117
|
+
${cliCommand} hub-export --output ./agenthub-backup
|
|
118
|
+
${cliCommand} hub-import --source ./agenthub-backup
|
|
119
|
+
|
|
120
|
+
FLAGS (global)
|
|
121
|
+
--help, -h Show this help message
|
|
122
|
+
--version, -v Print version
|
|
123
|
+
|
|
124
|
+
FLAGS (setup)
|
|
125
|
+
setup [auto|minimal] Setup mode (default: auto)
|
|
126
|
+
--target-root <path> Override Agent Hub home location
|
|
127
|
+
--import-souls <path> Import existing soul/agent prompt folder
|
|
128
|
+
--import-instructions <path> Import existing instructions folder
|
|
129
|
+
--import-skills <path> Import existing skills folder
|
|
130
|
+
--import-mcp-servers <path> Import existing MCP server folder
|
|
131
|
+
|
|
132
|
+
FLAGS (start / run)
|
|
133
|
+
--workspace <path> Target workspace (default: cwd)
|
|
134
|
+
--config-root <path> Override .opencode config directory
|
|
135
|
+
--assemble-only Write config files but do not launch opencode
|
|
136
|
+
--mode <tools-only|customized-agent>
|
|
137
|
+
Advanced: launch with a built-in bundle mode instead of a profile
|
|
138
|
+
start last Reuse the last profile used in this workspace (fallback: auto)
|
|
139
|
+
start set <profile> Save the default personal profile for future bare 'start'
|
|
140
|
+
-- <args> Pass remaining args to opencode
|
|
141
|
+
|
|
142
|
+
FLAGS (hr)
|
|
143
|
+
hr Enter the isolated HR Office (bootstraps it on first use)
|
|
144
|
+
hr <profile> Test an HR-home profile in the current workspace
|
|
145
|
+
hr last Reuse the last HR profile tested in this workspace
|
|
146
|
+
hr set <profile> Unsupported (use explicit '${cliCommand} hr <profile>' each time)
|
|
147
|
+
first bootstrap Interactive terminals can choose recommended / free / custom
|
|
148
|
+
defaults to openai/gpt-5.4-mini when left blank
|
|
149
|
+
|
|
150
|
+
FLAGS (new / compose profile)
|
|
151
|
+
--from <profile> Seed bundles/plugins from an existing profile
|
|
152
|
+
--add <bundle|cap> Add bundle(s) or capability shorthand (repeatable)
|
|
153
|
+
--reserved-ok Allow names that collide with built-in asset names
|
|
154
|
+
|
|
155
|
+
FLAGS (upgrade)
|
|
156
|
+
--target-root <path> Agent Hub home to inspect/sync
|
|
157
|
+
--dry-run Preview managed file changes (default)
|
|
158
|
+
--force Overwrite built-in managed files
|
|
159
|
+
|
|
160
|
+
FLAGS (plugin doctor)
|
|
161
|
+
--config-root <path> Inspect a specific opencode config root
|
|
162
|
+
|
|
163
|
+
FLAGS (doctor / hub-doctor)
|
|
164
|
+
--target-root <path> Agent Hub home to inspect (default: ${agentHubHomePath})
|
|
165
|
+
--fix-all Apply all safe automatic fixes
|
|
166
|
+
--dry-run Preview fixes without writing
|
|
167
|
+
--agent <name> Target a specific agent
|
|
168
|
+
--model <model> Override the agent's model
|
|
169
|
+
--clear-model Remove the agent's model override
|
|
170
|
+
--prompt-file <path> Set the agent's soul/prompt from a file
|
|
171
|
+
--clear-prompt Remove the agent's soul/prompt override
|
|
172
|
+
|
|
173
|
+
FLAGS (hub-export)
|
|
174
|
+
--output <path> Destination directory (required)
|
|
175
|
+
--source-root <path> Agent Hub home to export (default: ${agentHubHomePath})
|
|
176
|
+
|
|
177
|
+
FLAGS (hub-import)
|
|
178
|
+
--source <path> Exported Agent Hub directory to import (required)
|
|
179
|
+
--target-root <path> Destination Agent Hub home (default: ${agentHubHomePath})
|
|
180
|
+
--overwrite Overwrite matching entries
|
|
181
|
+
--settings <preserve|replace> How to handle settings (default: preserve)
|
|
182
|
+
|
|
183
|
+
FLAGS (backup)
|
|
184
|
+
--output <path> Destination directory (required; personal home only)
|
|
185
|
+
|
|
186
|
+
FLAGS (restore)
|
|
187
|
+
--source <path> Backup directory to restore from (required; personal home only)
|
|
188
|
+
--overwrite Overwrite matching entries
|
|
189
|
+
--settings <preserve|replace> How to handle settings (default: preserve)
|
|
190
|
+
|
|
191
|
+
FLAGS (promote)
|
|
192
|
+
[package-id] Staged package id under ${hrStagingPath}
|
|
193
|
+
|
|
194
|
+
PLUGIN-ONLY MODE
|
|
195
|
+
Agent Hub ships as both a CLI and an opencode plugin. When loaded as
|
|
196
|
+
a plugin without a configured hub runtime, it runs in degraded mode:
|
|
197
|
+
- Tool blocking (blockedTools) is disabled
|
|
198
|
+
- The call_omo_agent tool is blocked as a safety fallback
|
|
199
|
+
Run '${cliCommand} setup' once to initialize the hub runtime and exit
|
|
200
|
+
degraded mode. Features that require the hub runtime: profile composition,
|
|
201
|
+
agent souls, skill injection, and plan detection.
|
|
202
|
+
|
|
203
|
+
HR HOME
|
|
204
|
+
HR live state is stored separately at ${hrHomePath}
|
|
205
|
+
Override with OPENCODE_AGENTHUB_HR_HOME environment variable.
|
|
206
|
+
Use '${cliCommand} hr' to enter the HR Office. If HR is not installed yet, Agent Hub bootstraps it automatically.
|
|
207
|
+
Use '${cliCommand} hr <profile>' to test an HR-home profile or a staged profile in a workspace before promote.
|
|
208
|
+
HR model overrides live in ${hrSettingsPath}.
|
|
209
|
+
|
|
210
|
+
NOTE
|
|
211
|
+
This package requires Node on PATH.
|
|
212
|
+
Windows users should use WSL 2 for the best experience; native Windows remains best-effort in alpha.
|
|
213
|
+
`);
|
|
214
|
+
};
|
|
215
|
+
const fail = (message) => {
|
|
216
|
+
process.stderr.write(`${message}
|
|
217
|
+
`);
|
|
218
|
+
process.exit(1);
|
|
219
|
+
};
|
|
220
|
+
const formatWorkspaceAccessError = (workspace) => {
|
|
221
|
+
const quotedWorkspace = `'${workspace}'`;
|
|
222
|
+
const macHint = process.platform === "darwin" ? "\n\nOn macOS this usually means Terminal/Bun/opencode does not currently have permission to read that folder. Check Privacy & Security access for Documents/Desktop/iCloud-backed folders, then retry." : "";
|
|
223
|
+
return [
|
|
224
|
+
`Workspace ${quotedWorkspace} is not readable.`,
|
|
225
|
+
"Agent Hub can assemble the runtime, but opencode will fail to launch if it cannot scan the workspace.",
|
|
226
|
+
`Try: ls ${quotedWorkspace}`
|
|
227
|
+
].join("\n") + macHint;
|
|
228
|
+
};
|
|
229
|
+
const ensureWorkspaceReadable = async (workspace) => {
|
|
230
|
+
try {
|
|
231
|
+
await readdir(workspace);
|
|
232
|
+
} catch (error) {
|
|
233
|
+
if (error && typeof error === "object" && "code" in error && (error.code === "EACCES" || error.code === "EPERM")) {
|
|
234
|
+
fail(formatWorkspaceAccessError(workspace));
|
|
235
|
+
}
|
|
236
|
+
throw error;
|
|
237
|
+
}
|
|
238
|
+
};
|
|
239
|
+
const parseSetupMode = (value) => {
|
|
240
|
+
if (!value) return void 0;
|
|
241
|
+
const normalized = value.trim().toLowerCase();
|
|
242
|
+
if (normalized === "minimal" || normalized === "auto") {
|
|
243
|
+
return normalized;
|
|
244
|
+
}
|
|
245
|
+
return fail(`Invalid setup mode '${value}'. Use 'auto' or 'minimal'.`);
|
|
246
|
+
};
|
|
247
|
+
const normalizeRuntimeProfileName = (value) => {
|
|
248
|
+
return value;
|
|
249
|
+
};
|
|
250
|
+
const parseBundleName = (value) => {
|
|
251
|
+
const normalized = value.trim().toLowerCase();
|
|
252
|
+
if (normalized === "tools-only" || normalized === "customized-agent") {
|
|
253
|
+
return normalized;
|
|
254
|
+
}
|
|
255
|
+
return fail(
|
|
256
|
+
`Invalid bundle '${value}'. Use 'tools-only' or 'customized-agent'.`
|
|
257
|
+
);
|
|
258
|
+
};
|
|
259
|
+
const parseSettingsMode = (value) => {
|
|
260
|
+
if (!value || value === "preserve" || value === "replace")
|
|
261
|
+
return value || "preserve";
|
|
262
|
+
return fail(`Invalid settings mode '${value}'. Use 'preserve' or 'replace'.`);
|
|
263
|
+
};
|
|
264
|
+
const parseArgs = (argv) => {
|
|
265
|
+
let command = "run";
|
|
266
|
+
let runtimeSelection;
|
|
267
|
+
let composeSelection2;
|
|
268
|
+
let listTarget;
|
|
269
|
+
let pluginSubcommand;
|
|
270
|
+
let workspace = process.cwd();
|
|
271
|
+
let configRoot;
|
|
272
|
+
let assembleOnly = false;
|
|
273
|
+
const opencodeArgs = [];
|
|
274
|
+
const bootstrapOptions = {};
|
|
275
|
+
const profileCreateOptions = {
|
|
276
|
+
addBundles: [],
|
|
277
|
+
reservedOk: false
|
|
278
|
+
};
|
|
279
|
+
const upgradeOptions = {
|
|
280
|
+
force: false,
|
|
281
|
+
dryRun: true
|
|
282
|
+
};
|
|
283
|
+
const transferOptions = {
|
|
284
|
+
overwrite: false,
|
|
285
|
+
settingsMode: "preserve"
|
|
286
|
+
};
|
|
287
|
+
const doctorOptions = {
|
|
288
|
+
fixAll: false,
|
|
289
|
+
dryRun: false,
|
|
290
|
+
clearModel: false,
|
|
291
|
+
clearPrompt: false
|
|
292
|
+
};
|
|
293
|
+
let promotePackageId;
|
|
294
|
+
let startIntent;
|
|
295
|
+
let hrIntent;
|
|
296
|
+
let index = 0;
|
|
297
|
+
const maybeCommand = argv[0];
|
|
298
|
+
if (maybeCommand === "setup" || maybeCommand === "hr" || maybeCommand === "backup" || maybeCommand === "restore" || maybeCommand === "promote" || maybeCommand === "new" || maybeCommand === "upgrade" || maybeCommand === "plugin" || maybeCommand === "hub-export" || maybeCommand === "hub-import" || maybeCommand === "compose" || maybeCommand === "run" || maybeCommand === "start" || maybeCommand === "list" || maybeCommand === "doctor" || maybeCommand === "hub-doctor") {
|
|
299
|
+
command = maybeCommand;
|
|
300
|
+
index = 1;
|
|
301
|
+
const targetType = argv[index];
|
|
302
|
+
const targetName = argv[index + 1];
|
|
303
|
+
if (command === "hr") {
|
|
304
|
+
if (targetType === "set") {
|
|
305
|
+
fail(`'hr set <profile>' is not supported. Use '${cliCommand} hr <profile>' to test a temporary HR profile in this workspace.`);
|
|
306
|
+
}
|
|
307
|
+
if (targetType === "last") {
|
|
308
|
+
hrIntent = { kind: "compose", source: "last" };
|
|
309
|
+
index = 2;
|
|
310
|
+
} else {
|
|
311
|
+
const hrProfileArg = targetType && !targetType.startsWith("-") ? targetType : void 0;
|
|
312
|
+
if (hrProfileArg) {
|
|
313
|
+
hrIntent = {
|
|
314
|
+
kind: "compose",
|
|
315
|
+
profile: normalizeRuntimeProfileName(hrProfileArg),
|
|
316
|
+
source: "explicit"
|
|
317
|
+
};
|
|
318
|
+
runtimeSelection = {
|
|
319
|
+
kind: "profile",
|
|
320
|
+
profile: normalizeRuntimeProfileName(hrProfileArg)
|
|
321
|
+
};
|
|
322
|
+
index = 2;
|
|
323
|
+
} else {
|
|
324
|
+
hrIntent = { kind: "office" };
|
|
325
|
+
runtimeSelection = {
|
|
326
|
+
kind: "profile",
|
|
327
|
+
profile: "hr"
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
} else if (command === "promote" && targetType && !targetType.startsWith("-")) {
|
|
332
|
+
promotePackageId = targetType;
|
|
333
|
+
index = 2;
|
|
334
|
+
}
|
|
335
|
+
if ((command === "compose" || command === "new") && targetType === "profile") {
|
|
336
|
+
const name = targetName?.trim();
|
|
337
|
+
if (!name) {
|
|
338
|
+
fail(`'${command} profile' requires a profile name.`);
|
|
339
|
+
}
|
|
340
|
+
composeSelection2 = { kind: "profile", name };
|
|
341
|
+
index = 3;
|
|
342
|
+
} else if ((command === "compose" || command === "new") && targetType === "bundle") {
|
|
343
|
+
const name = targetName?.trim();
|
|
344
|
+
if (!name) {
|
|
345
|
+
fail(`'${command} bundle' requires a bundle name.`);
|
|
346
|
+
}
|
|
347
|
+
composeSelection2 = { kind: "bundle", name };
|
|
348
|
+
index = 3;
|
|
349
|
+
} else if (command === "new" && targetType === "soul") {
|
|
350
|
+
const name = targetName?.trim();
|
|
351
|
+
if (!name) fail("'new soul' requires a name.");
|
|
352
|
+
composeSelection2 = { kind: "soul", name };
|
|
353
|
+
index = 3;
|
|
354
|
+
} else if (command === "new" && targetType === "skill") {
|
|
355
|
+
const name = targetName?.trim();
|
|
356
|
+
if (!name) fail("'new skill' requires a name.");
|
|
357
|
+
composeSelection2 = { kind: "skill", name };
|
|
358
|
+
index = 3;
|
|
359
|
+
} else if (command === "new" && targetType === "instruction") {
|
|
360
|
+
const name = targetName?.trim();
|
|
361
|
+
if (!name) fail("'new instruction' requires a name.");
|
|
362
|
+
composeSelection2 = { kind: "instruction", name };
|
|
363
|
+
index = 3;
|
|
364
|
+
} else if (command === "plugin" && targetType === "doctor") {
|
|
365
|
+
pluginSubcommand = "doctor";
|
|
366
|
+
index = 2;
|
|
367
|
+
} else if (command === "setup" && targetType && !targetType.startsWith("-")) {
|
|
368
|
+
bootstrapOptions.mode = parseSetupMode(targetType);
|
|
369
|
+
index = 2;
|
|
370
|
+
} else if (command === "run" && targetType === "profile") {
|
|
371
|
+
startIntent = {
|
|
372
|
+
kind: "compose",
|
|
373
|
+
profile: normalizeRuntimeProfileName(targetName || "auto"),
|
|
374
|
+
source: "explicit"
|
|
375
|
+
};
|
|
376
|
+
runtimeSelection = {
|
|
377
|
+
kind: "profile",
|
|
378
|
+
profile: normalizeRuntimeProfileName(targetName || "auto")
|
|
379
|
+
};
|
|
380
|
+
index = 3;
|
|
381
|
+
} else if (command === "run" && targetType === "bundle") {
|
|
382
|
+
runtimeSelection = { kind: parseBundleName(targetName || "") };
|
|
383
|
+
index = 3;
|
|
384
|
+
} else if (command === "run" && targetType === "last") {
|
|
385
|
+
startIntent = { kind: "compose", source: "last" };
|
|
386
|
+
index = 2;
|
|
387
|
+
} else if (command === "run" && targetType === "set") {
|
|
388
|
+
const profile = normalizeRuntimeProfileName(targetName || "");
|
|
389
|
+
if (!profile) {
|
|
390
|
+
fail("'run set <profile>' requires a profile name.");
|
|
391
|
+
}
|
|
392
|
+
startIntent = { kind: "set-default", profile };
|
|
393
|
+
index = 3;
|
|
394
|
+
} else if (command === "run" && targetType && !targetType.startsWith("-")) {
|
|
395
|
+
startIntent = {
|
|
396
|
+
kind: "compose",
|
|
397
|
+
profile: normalizeRuntimeProfileName(targetType),
|
|
398
|
+
source: "explicit"
|
|
399
|
+
};
|
|
400
|
+
runtimeSelection = {
|
|
401
|
+
kind: "profile",
|
|
402
|
+
profile: normalizeRuntimeProfileName(targetType)
|
|
403
|
+
};
|
|
404
|
+
index = 2;
|
|
405
|
+
} else if (command === "start" && targetType === "profile") {
|
|
406
|
+
startIntent = {
|
|
407
|
+
kind: "compose",
|
|
408
|
+
profile: normalizeRuntimeProfileName(targetName || "auto"),
|
|
409
|
+
source: "explicit"
|
|
410
|
+
};
|
|
411
|
+
runtimeSelection = {
|
|
412
|
+
kind: "profile",
|
|
413
|
+
profile: normalizeRuntimeProfileName(targetName || "auto")
|
|
414
|
+
};
|
|
415
|
+
index = 3;
|
|
416
|
+
} else if (command === "start" && targetType === "bundle") {
|
|
417
|
+
runtimeSelection = { kind: parseBundleName(targetName || "") };
|
|
418
|
+
index = 3;
|
|
419
|
+
} else if (command === "start" && targetType === "last") {
|
|
420
|
+
startIntent = { kind: "compose", source: "last" };
|
|
421
|
+
index = 2;
|
|
422
|
+
} else if (command === "start" && targetType === "set") {
|
|
423
|
+
const profile = normalizeRuntimeProfileName(targetName || "");
|
|
424
|
+
if (!profile) {
|
|
425
|
+
fail("'start set <profile>' requires a profile name.");
|
|
426
|
+
}
|
|
427
|
+
startIntent = { kind: "set-default", profile };
|
|
428
|
+
index = 3;
|
|
429
|
+
} else if (command === "start" && targetType && !targetType.startsWith("-")) {
|
|
430
|
+
startIntent = {
|
|
431
|
+
kind: "compose",
|
|
432
|
+
profile: normalizeRuntimeProfileName(targetType),
|
|
433
|
+
source: "explicit"
|
|
434
|
+
};
|
|
435
|
+
runtimeSelection = {
|
|
436
|
+
kind: "profile",
|
|
437
|
+
profile: normalizeRuntimeProfileName(targetType)
|
|
438
|
+
};
|
|
439
|
+
index = 2;
|
|
440
|
+
} else if (command === "list" && targetType && !targetType.startsWith("-")) {
|
|
441
|
+
listTarget = targetType;
|
|
442
|
+
index = 2;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
if (maybeCommand && !maybeCommand.startsWith("-") && command === "run" && maybeCommand !== "run" && index === 0) {
|
|
446
|
+
process.stderr.write(
|
|
447
|
+
`Unknown command: '${maybeCommand}'
|
|
448
|
+
|
|
449
|
+
Run '${cliCommand} --help' to see available commands.
|
|
450
|
+
`
|
|
451
|
+
);
|
|
452
|
+
process.exit(1);
|
|
453
|
+
}
|
|
454
|
+
for (; index < argv.length; index += 1) {
|
|
455
|
+
const arg = argv[index];
|
|
456
|
+
if (arg === "--help" || arg === "-h") {
|
|
457
|
+
printHelp();
|
|
458
|
+
process.exit(0);
|
|
459
|
+
}
|
|
460
|
+
if (arg === "--version" || arg === "-v") {
|
|
461
|
+
process.stdout.write(`${readPackageVersion()}
|
|
462
|
+
`);
|
|
463
|
+
process.exit(0);
|
|
464
|
+
}
|
|
465
|
+
if (arg === "--profile") {
|
|
466
|
+
runtimeSelection = {
|
|
467
|
+
kind: "profile",
|
|
468
|
+
profile: normalizeRuntimeProfileName(argv[index + 1] || "auto")
|
|
469
|
+
};
|
|
470
|
+
index += 1;
|
|
471
|
+
continue;
|
|
472
|
+
}
|
|
473
|
+
if (arg === "--mode") {
|
|
474
|
+
runtimeSelection = { kind: parseBundleName(argv[index + 1] || "") };
|
|
475
|
+
index += 1;
|
|
476
|
+
continue;
|
|
477
|
+
}
|
|
478
|
+
if (arg === "--from") {
|
|
479
|
+
profileCreateOptions.fromProfile = argv[index + 1];
|
|
480
|
+
index += 1;
|
|
481
|
+
continue;
|
|
482
|
+
}
|
|
483
|
+
if (arg === "--add") {
|
|
484
|
+
profileCreateOptions.addBundles.push(argv[index + 1] || "");
|
|
485
|
+
index += 1;
|
|
486
|
+
continue;
|
|
487
|
+
}
|
|
488
|
+
if (arg === "--reserved-ok") {
|
|
489
|
+
profileCreateOptions.reservedOk = true;
|
|
490
|
+
continue;
|
|
491
|
+
}
|
|
492
|
+
if (arg === "--workspace") {
|
|
493
|
+
workspace = path.resolve(argv[index + 1] || workspace);
|
|
494
|
+
index += 1;
|
|
495
|
+
continue;
|
|
496
|
+
}
|
|
497
|
+
if (arg === "--config-root") {
|
|
498
|
+
configRoot = path.resolve(argv[index + 1] || workspace);
|
|
499
|
+
index += 1;
|
|
500
|
+
continue;
|
|
501
|
+
}
|
|
502
|
+
if (arg === "--assemble-only") {
|
|
503
|
+
assembleOnly = true;
|
|
504
|
+
continue;
|
|
505
|
+
}
|
|
506
|
+
if (arg === "--target-root") {
|
|
507
|
+
const resolved = path.resolve(argv[index + 1] || defaultAgentHubHome());
|
|
508
|
+
bootstrapOptions.targetRoot = resolved;
|
|
509
|
+
transferOptions.targetRoot = resolved;
|
|
510
|
+
index += 1;
|
|
511
|
+
continue;
|
|
512
|
+
}
|
|
513
|
+
if (arg === "--source-root" || arg === "--source") {
|
|
514
|
+
transferOptions.sourceRoot = path.resolve(argv[index + 1] || workspace);
|
|
515
|
+
index += 1;
|
|
516
|
+
continue;
|
|
517
|
+
}
|
|
518
|
+
if (arg === "--output") {
|
|
519
|
+
transferOptions.outputRoot = path.resolve(argv[index + 1] || workspace);
|
|
520
|
+
index += 1;
|
|
521
|
+
continue;
|
|
522
|
+
}
|
|
523
|
+
if (arg === "--overwrite") {
|
|
524
|
+
transferOptions.overwrite = true;
|
|
525
|
+
continue;
|
|
526
|
+
}
|
|
527
|
+
if (arg === "--force") {
|
|
528
|
+
upgradeOptions.force = true;
|
|
529
|
+
continue;
|
|
530
|
+
}
|
|
531
|
+
if (arg === "--settings") {
|
|
532
|
+
transferOptions.settingsMode = parseSettingsMode(argv[index + 1]);
|
|
533
|
+
index += 1;
|
|
534
|
+
continue;
|
|
535
|
+
}
|
|
536
|
+
if (arg === "--fix-all") {
|
|
537
|
+
doctorOptions.fixAll = true;
|
|
538
|
+
continue;
|
|
539
|
+
}
|
|
540
|
+
if (arg === "--dry-run") {
|
|
541
|
+
doctorOptions.dryRun = true;
|
|
542
|
+
upgradeOptions.dryRun = true;
|
|
543
|
+
continue;
|
|
544
|
+
}
|
|
545
|
+
if (arg === "--agent") {
|
|
546
|
+
doctorOptions.agent = argv[index + 1];
|
|
547
|
+
index += 1;
|
|
548
|
+
continue;
|
|
549
|
+
}
|
|
550
|
+
if (arg === "--model") {
|
|
551
|
+
doctorOptions.model = argv[index + 1];
|
|
552
|
+
index += 1;
|
|
553
|
+
continue;
|
|
554
|
+
}
|
|
555
|
+
if (arg === "--clear-model") {
|
|
556
|
+
doctorOptions.clearModel = true;
|
|
557
|
+
continue;
|
|
558
|
+
}
|
|
559
|
+
if (arg === "--prompt-file") {
|
|
560
|
+
doctorOptions.promptFile = path.resolve(argv[index + 1] || workspace);
|
|
561
|
+
index += 1;
|
|
562
|
+
continue;
|
|
563
|
+
}
|
|
564
|
+
if (arg === "--clear-prompt") {
|
|
565
|
+
doctorOptions.clearPrompt = true;
|
|
566
|
+
continue;
|
|
567
|
+
}
|
|
568
|
+
if (arg === "--import-souls") {
|
|
569
|
+
bootstrapOptions.importSoulsPath = path.resolve(
|
|
570
|
+
argv[index + 1] || workspace
|
|
571
|
+
);
|
|
572
|
+
index += 1;
|
|
573
|
+
continue;
|
|
574
|
+
}
|
|
575
|
+
if (arg === "--import-instructions") {
|
|
576
|
+
bootstrapOptions.importInstructionsPath = path.resolve(
|
|
577
|
+
argv[index + 1] || workspace
|
|
578
|
+
);
|
|
579
|
+
index += 1;
|
|
580
|
+
continue;
|
|
581
|
+
}
|
|
582
|
+
if (arg === "--import-skills") {
|
|
583
|
+
bootstrapOptions.importSkillsPath = path.resolve(
|
|
584
|
+
argv[index + 1] || workspace
|
|
585
|
+
);
|
|
586
|
+
index += 1;
|
|
587
|
+
continue;
|
|
588
|
+
}
|
|
589
|
+
if (arg === "--import-mcp-servers") {
|
|
590
|
+
bootstrapOptions.importMcpServersPath = path.resolve(
|
|
591
|
+
argv[index + 1] || workspace
|
|
592
|
+
);
|
|
593
|
+
index += 1;
|
|
594
|
+
continue;
|
|
595
|
+
}
|
|
596
|
+
if (arg === "--") {
|
|
597
|
+
opencodeArgs.push(...argv.slice(index + 1));
|
|
598
|
+
break;
|
|
599
|
+
}
|
|
600
|
+
if (command === "promote" && !arg.startsWith("-") && !promotePackageId) {
|
|
601
|
+
promotePackageId = arg;
|
|
602
|
+
continue;
|
|
603
|
+
}
|
|
604
|
+
opencodeArgs.push(arg);
|
|
605
|
+
}
|
|
606
|
+
if (command === "start" && !startIntent) {
|
|
607
|
+
startIntent = { kind: "compose" };
|
|
608
|
+
}
|
|
609
|
+
if (command === "start" && startIntent?.kind === "compose" && !runtimeSelection) {
|
|
610
|
+
runtimeSelection = { kind: "profile", profile: startIntent.profile || "auto" };
|
|
611
|
+
}
|
|
612
|
+
if (runtimeSelection && runtimeSelection.kind !== "profile" && command === "setup") {
|
|
613
|
+
fail(
|
|
614
|
+
"'setup' does not accept '--mode'. Use 'setup minimal' for a minimal home."
|
|
615
|
+
);
|
|
616
|
+
}
|
|
617
|
+
if ((command === "compose" || command === "new") && !composeSelection2) {
|
|
618
|
+
if (command === "compose") {
|
|
619
|
+
fail("Use 'compose profile <name>' or 'compose bundle <name>'.");
|
|
620
|
+
}
|
|
621
|
+
fail(
|
|
622
|
+
"Use 'new soul <name>', 'new skill <name>', 'new instruction <name>', 'new bundle <name>', or 'new profile <name>'."
|
|
623
|
+
);
|
|
624
|
+
}
|
|
625
|
+
if ((profileCreateOptions.fromProfile || profileCreateOptions.addBundles.length > 0) && composeSelection2?.kind !== "profile") {
|
|
626
|
+
fail("'--from' and '--add' can only be used with 'compose profile' or 'new profile'.");
|
|
627
|
+
}
|
|
628
|
+
if (command === "doctor" || command === "hub-doctor") {
|
|
629
|
+
const modelActionCount = Number(Boolean(doctorOptions.model)) + Number(doctorOptions.clearModel);
|
|
630
|
+
const promptActionCount = Number(Boolean(doctorOptions.promptFile)) + Number(doctorOptions.clearPrompt);
|
|
631
|
+
const totalActionCount = modelActionCount + promptActionCount;
|
|
632
|
+
if (totalActionCount > 1) {
|
|
633
|
+
fail(
|
|
634
|
+
"Use exactly one of '--model <value>', '--clear-model', '--prompt-file <path>', or '--clear-prompt' with '--agent'."
|
|
635
|
+
);
|
|
636
|
+
}
|
|
637
|
+
if (totalActionCount > 0 && !doctorOptions.agent) {
|
|
638
|
+
fail("'doctor' prompt/model override commands require '--agent <name>'.");
|
|
639
|
+
}
|
|
640
|
+
if (doctorOptions.agent && totalActionCount === 0) {
|
|
641
|
+
fail(
|
|
642
|
+
"'doctor --agent <name>' requires one of '--model <value>', '--clear-model', '--prompt-file <path>', or '--clear-prompt'."
|
|
643
|
+
);
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
if (command === "hub-export" && !transferOptions.outputRoot) {
|
|
647
|
+
fail("'hub-export' requires '--output <path>'.");
|
|
648
|
+
}
|
|
649
|
+
if (command === "hub-import" && !transferOptions.sourceRoot) {
|
|
650
|
+
fail("'hub-import' requires '--source <path>'.");
|
|
651
|
+
}
|
|
652
|
+
if (command === "upgrade") {
|
|
653
|
+
doctorOptions.dryRun = false;
|
|
654
|
+
if (upgradeOptions.force) {
|
|
655
|
+
upgradeOptions.dryRun = false;
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
return {
|
|
659
|
+
command,
|
|
660
|
+
runtimeSelection,
|
|
661
|
+
composeSelection: composeSelection2,
|
|
662
|
+
listTarget,
|
|
663
|
+
pluginSubcommand,
|
|
664
|
+
workspace,
|
|
665
|
+
configRoot,
|
|
666
|
+
assembleOnly,
|
|
667
|
+
opencodeArgs,
|
|
668
|
+
bootstrapOptions,
|
|
669
|
+
profileCreateOptions,
|
|
670
|
+
upgradeOptions,
|
|
671
|
+
transferOptions,
|
|
672
|
+
doctorOptions,
|
|
673
|
+
promotePackageId,
|
|
674
|
+
startIntent,
|
|
675
|
+
hrIntent
|
|
676
|
+
};
|
|
677
|
+
};
|
|
678
|
+
const printTransferReport = (action, report) => {
|
|
679
|
+
process.stdout.write(`${action} complete
|
|
680
|
+
`);
|
|
681
|
+
process.stdout.write(`- source: ${report.sourceRoot}
|
|
682
|
+
`);
|
|
683
|
+
if (report.sourceKind) {
|
|
684
|
+
process.stdout.write(`- source kind: ${report.sourceKind}
|
|
685
|
+
`);
|
|
686
|
+
}
|
|
687
|
+
process.stdout.write(`- target: ${report.targetRoot}
|
|
688
|
+
`);
|
|
689
|
+
process.stdout.write(`- copied: ${report.copied.length}
|
|
690
|
+
`);
|
|
691
|
+
process.stdout.write(`- skipped: ${report.skipped.length}
|
|
692
|
+
`);
|
|
693
|
+
process.stdout.write(`- overwritten: ${report.overwritten.length}
|
|
694
|
+
`);
|
|
695
|
+
process.stdout.write(`- settings: ${report.settingsAction}
|
|
696
|
+
`);
|
|
697
|
+
if (report.warnings.length > 0) {
|
|
698
|
+
process.stdout.write(`Warnings:
|
|
699
|
+
`);
|
|
700
|
+
for (const warning of report.warnings) {
|
|
701
|
+
process.stdout.write(`- ${warning}
|
|
702
|
+
`);
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
};
|
|
706
|
+
const printRuntimeBanner = (label, root) => {
|
|
707
|
+
process.stdout.write(`[agenthub] Environment: ${label}
|
|
708
|
+
`);
|
|
709
|
+
process.stdout.write(`[agenthub] Home: ${root}
|
|
710
|
+
`);
|
|
711
|
+
};
|
|
712
|
+
const listPromotablePackageIds = async (hrRoot = defaultHrHome()) => {
|
|
713
|
+
try {
|
|
714
|
+
const entries = await readdir(path.join(hrRoot, "staging"), { withFileTypes: true });
|
|
715
|
+
return entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name).sort();
|
|
716
|
+
} catch {
|
|
717
|
+
return [];
|
|
718
|
+
}
|
|
719
|
+
};
|
|
720
|
+
const readPromoteHandoff = async (sourceRoot) => {
|
|
721
|
+
try {
|
|
722
|
+
return JSON.parse(
|
|
723
|
+
await readFile(path.join(path.dirname(sourceRoot), "handoff.json"), "utf-8")
|
|
724
|
+
);
|
|
725
|
+
} catch {
|
|
726
|
+
return null;
|
|
727
|
+
}
|
|
728
|
+
};
|
|
729
|
+
const resolvePromoteDefaultProfile = (handoff) => {
|
|
730
|
+
if (!handoff?.promotion_preferences?.set_default_profile) return void 0;
|
|
731
|
+
const targetProfile = handoff.target_profile?.trim();
|
|
732
|
+
if (targetProfile) return targetProfile;
|
|
733
|
+
const proposedProfile = handoff.proposed_profile?.trim();
|
|
734
|
+
return proposedProfile || void 0;
|
|
735
|
+
};
|
|
736
|
+
const resolvePromoteSourceRoot = async (packageId, hrRoot = defaultHrHome()) => {
|
|
737
|
+
if (packageId) {
|
|
738
|
+
return path.join(hrRoot, "staging", packageId, "agenthub-home");
|
|
739
|
+
}
|
|
740
|
+
const packageIds = await listPromotablePackageIds(hrRoot);
|
|
741
|
+
if (packageIds.length === 0) {
|
|
742
|
+
fail(`No staged HR packages found in ${path.join(hrRoot, "staging")}.`);
|
|
743
|
+
}
|
|
744
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
745
|
+
fail(`'promote' requires a package id in non-interactive mode. Available: ${packageIds.join(", ")}`);
|
|
746
|
+
}
|
|
747
|
+
const rl = createPromptInterface();
|
|
748
|
+
try {
|
|
749
|
+
process.stdout.write(`Available HR packages: ${packageIds.join(", ")}
|
|
750
|
+
`);
|
|
751
|
+
const selected = await promptRequired(rl, "Package to promote", packageIds[0]);
|
|
752
|
+
if (!packageIds.includes(selected)) {
|
|
753
|
+
fail(`Unknown staged HR package '${selected}'. Available: ${packageIds.join(", ")}`);
|
|
754
|
+
}
|
|
755
|
+
return path.join(hrRoot, "staging", selected, "agenthub-home");
|
|
756
|
+
} finally {
|
|
757
|
+
rl.close();
|
|
758
|
+
}
|
|
759
|
+
};
|
|
760
|
+
const validatePromoteSourceRoot = async (sourceRoot, hrRoot = defaultHrHome()) => {
|
|
761
|
+
const resolvedSource = path.resolve(sourceRoot);
|
|
762
|
+
const stagingRoot = path.resolve(path.join(hrRoot, "staging"));
|
|
763
|
+
if (!resolvedSource.startsWith(`${stagingRoot}${path.sep}`)) {
|
|
764
|
+
fail(`Promote source must be inside ${stagingRoot}`);
|
|
765
|
+
}
|
|
766
|
+
const finalChecklistPath = path.join(path.dirname(resolvedSource), "final-checklist.md");
|
|
767
|
+
let finalChecklist;
|
|
768
|
+
try {
|
|
769
|
+
finalChecklist = await readFile(finalChecklistPath, "utf-8");
|
|
770
|
+
} catch {
|
|
771
|
+
finalChecklist = void 0;
|
|
772
|
+
}
|
|
773
|
+
if (!finalChecklist || !finalChecklist.includes("READY FOR HUMAN CONFIRMATION")) {
|
|
774
|
+
process.stderr.write(
|
|
775
|
+
`[agenthub] Warning: ${finalChecklistPath} is missing or not marked READY FOR HUMAN CONFIRMATION. Continuing with manual promote.
|
|
776
|
+
`
|
|
777
|
+
);
|
|
778
|
+
}
|
|
779
|
+
const handoffPath = path.join(path.dirname(resolvedSource), "handoff.json");
|
|
780
|
+
let handoff = null;
|
|
781
|
+
try {
|
|
782
|
+
handoff = JSON.parse(await readFile(handoffPath, "utf-8"));
|
|
783
|
+
} catch {
|
|
784
|
+
process.stderr.write(`[agenthub] Warning: ${handoffPath} is missing.
|
|
785
|
+
`);
|
|
786
|
+
return;
|
|
787
|
+
}
|
|
788
|
+
const instructions = handoff.operator_instructions;
|
|
789
|
+
if (!instructions?.test_current_workspace) {
|
|
790
|
+
process.stderr.write(
|
|
791
|
+
`[agenthub] Warning: ${handoffPath} is missing operator_instructions.test_current_workspace. Operators may not realize staged profiles can be tested before promote.
|
|
792
|
+
`
|
|
793
|
+
);
|
|
794
|
+
}
|
|
795
|
+
if (!instructions?.use_in_another_workspace) {
|
|
796
|
+
process.stderr.write(
|
|
797
|
+
`[agenthub] Warning: ${handoffPath} is missing operator_instructions.use_in_another_workspace. Operators may not realize staged profiles can be used in another workspace before promote.
|
|
798
|
+
`
|
|
799
|
+
);
|
|
800
|
+
}
|
|
801
|
+
if (!instructions?.promote) {
|
|
802
|
+
process.stderr.write(
|
|
803
|
+
`[agenthub] Warning: ${handoffPath} is missing operator_instructions.promote.
|
|
804
|
+
`
|
|
805
|
+
);
|
|
806
|
+
}
|
|
807
|
+
if (!instructions?.advanced_import) {
|
|
808
|
+
process.stderr.write(
|
|
809
|
+
`[agenthub] Warning: ${handoffPath} is missing operator_instructions.advanced_import.
|
|
810
|
+
`
|
|
811
|
+
);
|
|
812
|
+
}
|
|
813
|
+
const hostRequirements = handoff.host_requirements;
|
|
814
|
+
if (hostRequirements?.mcp_servers_bundled === false) {
|
|
815
|
+
const missing = hostRequirements.missing?.join(", ") || "MCP server artifacts";
|
|
816
|
+
fail(
|
|
817
|
+
`Staged package cannot be promoted yet because required MCP server artifacts are missing: ${missing}. Re-stage the package with bundled MCP servers first.`
|
|
818
|
+
);
|
|
819
|
+
}
|
|
820
|
+
};
|
|
821
|
+
const ensureHomeReadyOrFail = async (targetRoot = defaultAgentHubHome()) => {
|
|
822
|
+
if (await agentHubHomeInitialized(targetRoot)) return;
|
|
823
|
+
fail(`Agent Hub home is not initialized. Run:
|
|
824
|
+
${cliCommand} setup`);
|
|
825
|
+
};
|
|
826
|
+
const ensureSelectedHomeReadyOrFail = async (parsed2) => {
|
|
827
|
+
const targetRoot = resolveSelectedHomeRoot(parsed2);
|
|
828
|
+
if (parsed2.command === "hr") {
|
|
829
|
+
if (await hrHomeInitialized(targetRoot || defaultHrHome())) return;
|
|
830
|
+
fail(`HR Office is not initialized. Run:
|
|
831
|
+
${cliCommand} hr`);
|
|
832
|
+
}
|
|
833
|
+
await ensureHomeReadyOrFail(targetRoot);
|
|
834
|
+
};
|
|
835
|
+
const ensureHomeReadyOrBootstrap = async (targetRoot = defaultAgentHubHome()) => {
|
|
836
|
+
if (await agentHubHomeInitialized(targetRoot)) return;
|
|
837
|
+
await installAgentHubHome({ targetRoot, mode: "auto" });
|
|
838
|
+
process.stdout.write(`\u2713 First run \u2014 initialised coding system at ${targetRoot}
|
|
839
|
+
`);
|
|
840
|
+
};
|
|
841
|
+
const promptHrBootstrapModelSelection = async (hrRoot) => {
|
|
842
|
+
const rl = createPromptInterface();
|
|
843
|
+
try {
|
|
844
|
+
process.stdout.write("\nFirst-time HR Office setup\n");
|
|
845
|
+
process.stdout.write("Choose an HR model preset:\n");
|
|
846
|
+
process.stdout.write(
|
|
847
|
+
` recommended Use ${recommendedHrBootstrapModel} (${recommendedHrBootstrapVariant})
|
|
848
|
+
`
|
|
849
|
+
);
|
|
850
|
+
process.stdout.write(
|
|
851
|
+
" free Pick from current opencode free models (quality may drop)\n"
|
|
852
|
+
);
|
|
853
|
+
process.stdout.write(" custom Enter any model id yourself\n\n");
|
|
854
|
+
const subagentStrategy = await promptChoice(
|
|
855
|
+
rl,
|
|
856
|
+
"HR model preset",
|
|
857
|
+
["recommended", "free", "custom"],
|
|
858
|
+
"recommended"
|
|
859
|
+
);
|
|
860
|
+
let sharedSubagentModel;
|
|
861
|
+
let consoleModel;
|
|
862
|
+
if (subagentStrategy === "recommended") {
|
|
863
|
+
consoleModel = recommendedHrBootstrapModel;
|
|
864
|
+
sharedSubagentModel = recommendedHrBootstrapModel;
|
|
865
|
+
}
|
|
866
|
+
if (subagentStrategy === "free") {
|
|
867
|
+
const freeModels = await listOpencodeFreeModels();
|
|
868
|
+
const fallbackFreeModel = freeModels.includes("opencode/minimax-m2.5-free") ? "opencode/minimax-m2.5-free" : freeModels[0] || "opencode/minimax-m2.5-free";
|
|
869
|
+
process.stdout.write("Current opencode free models:\n");
|
|
870
|
+
sharedSubagentModel = await promptIndexedChoice(
|
|
871
|
+
rl,
|
|
872
|
+
"Choose a free model for HR",
|
|
873
|
+
freeModels.length > 0 ? freeModels : [fallbackFreeModel],
|
|
874
|
+
fallbackFreeModel
|
|
875
|
+
);
|
|
876
|
+
consoleModel = sharedSubagentModel;
|
|
877
|
+
} else if (subagentStrategy === "custom") {
|
|
878
|
+
sharedSubagentModel = await promptRequired(
|
|
879
|
+
rl,
|
|
880
|
+
"Custom HR model",
|
|
881
|
+
recommendedHrBootstrapModel
|
|
882
|
+
);
|
|
883
|
+
consoleModel = sharedSubagentModel;
|
|
884
|
+
}
|
|
885
|
+
process.stdout.write(`
|
|
886
|
+
[agenthub] HR settings will be written to ${path.join(hrRoot, "settings.json")}
|
|
887
|
+
`);
|
|
888
|
+
return {
|
|
889
|
+
consoleModel,
|
|
890
|
+
subagentStrategy,
|
|
891
|
+
sharedSubagentModel
|
|
892
|
+
};
|
|
893
|
+
} finally {
|
|
894
|
+
rl.close();
|
|
895
|
+
}
|
|
896
|
+
};
|
|
897
|
+
const printHrModelOverrideHint = (targetRoot) => {
|
|
898
|
+
process.stdout.write(
|
|
899
|
+
[
|
|
900
|
+
`[agenthub] HR model settings: ${path.join(targetRoot, "settings.json")}`,
|
|
901
|
+
`[agenthub] Change later with: ${cliCommand} doctor --target-root ${targetRoot} --agent ${hrPrimaryAgentName} --model <model>`,
|
|
902
|
+
`[agenthub] HR subagents: ${hrSubagentNames.join(", ")} (use the same doctor command with --agent <name>)`
|
|
903
|
+
].join("\n") + "\n"
|
|
904
|
+
);
|
|
905
|
+
};
|
|
906
|
+
const countConfiguredHrGithubSources = async (targetRoot) => {
|
|
907
|
+
try {
|
|
908
|
+
const raw = JSON.parse(
|
|
909
|
+
await readFile(path.join(targetRoot, "hr-config.json"), "utf-8")
|
|
910
|
+
);
|
|
911
|
+
const githubSources = [];
|
|
912
|
+
if (Array.isArray(raw.sources)) {
|
|
913
|
+
githubSources.push(...raw.sources);
|
|
914
|
+
} else if (raw.sources && typeof raw.sources === "object") {
|
|
915
|
+
const nestedGithub = raw.sources.github;
|
|
916
|
+
if (Array.isArray(nestedGithub)) githubSources.push(...nestedGithub);
|
|
917
|
+
}
|
|
918
|
+
if (Array.isArray(raw.github)) githubSources.push(...raw.github);
|
|
919
|
+
return githubSources.length;
|
|
920
|
+
} catch {
|
|
921
|
+
return null;
|
|
922
|
+
}
|
|
923
|
+
};
|
|
924
|
+
const countConfiguredHrModelCatalogSources = async (targetRoot) => {
|
|
925
|
+
try {
|
|
926
|
+
const raw = JSON.parse(
|
|
927
|
+
await readFile(path.join(targetRoot, "hr-config.json"), "utf-8")
|
|
928
|
+
);
|
|
929
|
+
const modelSources = [];
|
|
930
|
+
if (raw.sources && typeof raw.sources === "object") {
|
|
931
|
+
const nestedModels = raw.sources.models;
|
|
932
|
+
if (Array.isArray(nestedModels)) modelSources.push(...nestedModels);
|
|
933
|
+
}
|
|
934
|
+
if (Array.isArray(raw.models)) modelSources.push(...raw.models);
|
|
935
|
+
return modelSources.length;
|
|
936
|
+
} catch {
|
|
937
|
+
return null;
|
|
938
|
+
}
|
|
939
|
+
};
|
|
940
|
+
const syncHrSourceInventoryOnFirstRun = async (targetRoot) => {
|
|
941
|
+
const configuredSourceCount = await countConfiguredHrGithubSources(targetRoot);
|
|
942
|
+
const configuredModelSourceCount = await countConfiguredHrModelCatalogSources(targetRoot);
|
|
943
|
+
const sourceParts = [];
|
|
944
|
+
if (configuredSourceCount && configuredSourceCount > 0) {
|
|
945
|
+
sourceParts.push(
|
|
946
|
+
`${configuredSourceCount} GitHub repo${configuredSourceCount === 1 ? "" : "s"}`
|
|
947
|
+
);
|
|
948
|
+
}
|
|
949
|
+
if (configuredModelSourceCount && configuredModelSourceCount > 0) {
|
|
950
|
+
sourceParts.push(
|
|
951
|
+
`${configuredModelSourceCount} model catalog${configuredModelSourceCount === 1 ? "" : "s"}`
|
|
952
|
+
);
|
|
953
|
+
}
|
|
954
|
+
const sourceLabel = sourceParts.length > 0 ? sourceParts.join(" + ") : "configured HR sources";
|
|
955
|
+
process.stdout.write(`[agenthub] First run \u2014 syncing HR inventory from ${sourceLabel}...
|
|
956
|
+
`);
|
|
957
|
+
try {
|
|
958
|
+
const pythonCommand = resolvePythonCommand();
|
|
959
|
+
const scriptPath = path.join(targetRoot, "bin", "sync_sources.py");
|
|
960
|
+
const child = spawn(pythonCommand, [scriptPath], {
|
|
961
|
+
cwd: targetRoot,
|
|
962
|
+
env: {
|
|
963
|
+
...process.env,
|
|
964
|
+
OPENCODE_AGENTHUB_HR_HOME: targetRoot
|
|
965
|
+
},
|
|
966
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
967
|
+
...spawnOptions()
|
|
968
|
+
});
|
|
969
|
+
let stdout = "";
|
|
970
|
+
let stderr = "";
|
|
971
|
+
child.stdout.on("data", (chunk) => {
|
|
972
|
+
stdout += chunk.toString();
|
|
973
|
+
});
|
|
974
|
+
child.stderr.on("data", (chunk) => {
|
|
975
|
+
stderr += chunk.toString();
|
|
976
|
+
});
|
|
977
|
+
const code = await new Promise((resolve, reject) => {
|
|
978
|
+
child.on("error", reject);
|
|
979
|
+
child.on("close", (exitCode) => resolve(exitCode ?? 1));
|
|
980
|
+
});
|
|
981
|
+
const summary = stdout.trim();
|
|
982
|
+
if (code === 0) {
|
|
983
|
+
if (summary) process.stdout.write(`${summary}
|
|
984
|
+
`);
|
|
985
|
+
process.stdout.write(
|
|
986
|
+
`[agenthub] HR source status: ${path.join(targetRoot, "source-status.json")}
|
|
987
|
+
`
|
|
988
|
+
);
|
|
989
|
+
return;
|
|
990
|
+
}
|
|
991
|
+
process.stderr.write(
|
|
992
|
+
`[agenthub] Warning: first-run HR source sync did not complete. Continue using HR and retry later with '${pythonCommand} ${scriptPath}'.
|
|
993
|
+
`
|
|
994
|
+
);
|
|
995
|
+
if (stderr.trim()) process.stderr.write(`${stderr.trim()}
|
|
996
|
+
`);
|
|
997
|
+
} catch (error) {
|
|
998
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
999
|
+
const pythonCommand = resolvePythonCommand();
|
|
1000
|
+
process.stderr.write(
|
|
1001
|
+
`[agenthub] Warning: failed to launch first-run HR source sync (${reason}). Retry later with '${pythonCommand} ${path.join(targetRoot, "bin", "sync_sources.py")}'.
|
|
1002
|
+
`
|
|
1003
|
+
);
|
|
1004
|
+
}
|
|
1005
|
+
};
|
|
1006
|
+
const ensureHrOfficeReadyOrBootstrap = async (targetRoot = defaultHrHome(), options = {}) => {
|
|
1007
|
+
if (await hrHomeInitialized(targetRoot)) return;
|
|
1008
|
+
const shouldPrompt = process.stdin.isTTY && process.stdout.isTTY;
|
|
1009
|
+
const hrModelSelection = shouldPrompt ? await promptHrBootstrapModelSelection(targetRoot) : void 0;
|
|
1010
|
+
await installHrOfficeHomeWithOptions({
|
|
1011
|
+
hrRoot: targetRoot,
|
|
1012
|
+
hrModelSelection
|
|
1013
|
+
});
|
|
1014
|
+
process.stdout.write(`\u2713 First run \u2014 initialised HR Office at ${targetRoot}
|
|
1015
|
+
`);
|
|
1016
|
+
printHrModelOverrideHint(targetRoot);
|
|
1017
|
+
if (options.syncSourcesOnFirstRun ?? true) {
|
|
1018
|
+
await syncHrSourceInventoryOnFirstRun(targetRoot);
|
|
1019
|
+
}
|
|
1020
|
+
};
|
|
1021
|
+
const isHrRuntimeSelection = (selection) => selection?.kind === "profile" && selection.profile === "hr";
|
|
1022
|
+
const normalizeCsv = (value) => value.split(",").map((item) => item.trim()).filter(Boolean);
|
|
1023
|
+
const uniqueValues = (values) => [...new Set(values)];
|
|
1024
|
+
const normalizeOptional = (value) => {
|
|
1025
|
+
const trimmed = value.trim();
|
|
1026
|
+
return trimmed || void 0;
|
|
1027
|
+
};
|
|
1028
|
+
const toJsonFile = (root, directory, name) => path.join(root, directory, `${name}.json`);
|
|
1029
|
+
const listNamesByExt = async (dirPath, ext) => {
|
|
1030
|
+
try {
|
|
1031
|
+
const entries = await readdir(dirPath, { withFileTypes: true });
|
|
1032
|
+
return entries.filter((entry) => entry.isFile() && entry.name.endsWith(ext)).map((entry) => entry.name.slice(0, -ext.length)).sort();
|
|
1033
|
+
} catch {
|
|
1034
|
+
return [];
|
|
1035
|
+
}
|
|
1036
|
+
};
|
|
1037
|
+
const writeJsonFile = async (filePath, payload) => {
|
|
1038
|
+
await mkdir(path.dirname(filePath), { recursive: true });
|
|
1039
|
+
await writeFile(filePath, `${JSON.stringify(payload, null, 2)}
|
|
1040
|
+
`, "utf-8");
|
|
1041
|
+
};
|
|
1042
|
+
const workspacePreferencesPath = (workspace) => path.join(workspace, ".opencode-agenthub.user.json");
|
|
1043
|
+
const readJsonIfExists = async (filePath) => {
|
|
1044
|
+
try {
|
|
1045
|
+
const content = await readFile(filePath, "utf-8");
|
|
1046
|
+
const normalized = content.split("\n").filter((line) => !line.trim().startsWith("//")).join("\n");
|
|
1047
|
+
return JSON.parse(normalized);
|
|
1048
|
+
} catch (e) {
|
|
1049
|
+
const code = e.code;
|
|
1050
|
+
if (code === "ENOENT" || code === "EISDIR") return void 0;
|
|
1051
|
+
throw e;
|
|
1052
|
+
}
|
|
1053
|
+
};
|
|
1054
|
+
const loadWorkspacePreferences = async (workspace) => {
|
|
1055
|
+
const raw = await readJsonIfExists(
|
|
1056
|
+
workspacePreferencesPath(workspace)
|
|
1057
|
+
) || {};
|
|
1058
|
+
return {
|
|
1059
|
+
_version: 1,
|
|
1060
|
+
...raw
|
|
1061
|
+
};
|
|
1062
|
+
};
|
|
1063
|
+
const saveWorkspacePreferences = async (workspace, preferences) => {
|
|
1064
|
+
await writeJsonFile(workspacePreferencesPath(workspace), {
|
|
1065
|
+
_version: 1,
|
|
1066
|
+
...preferences
|
|
1067
|
+
});
|
|
1068
|
+
};
|
|
1069
|
+
const updateWorkspacePreferences = async (workspace, updater) => {
|
|
1070
|
+
const current = await loadWorkspacePreferences(workspace);
|
|
1071
|
+
await saveWorkspacePreferences(workspace, updater(current));
|
|
1072
|
+
};
|
|
1073
|
+
const readStartDefaultProfile = async (targetRoot = defaultAgentHubHome()) => {
|
|
1074
|
+
const settings = await readAgentHubSettings(targetRoot);
|
|
1075
|
+
return settings?.preferences?.defaultProfile?.trim() || void 0;
|
|
1076
|
+
};
|
|
1077
|
+
const setStartDefaultProfile = async (profile, targetRoot = defaultAgentHubHome()) => {
|
|
1078
|
+
const existingSettings = await readAgentHubSettings(targetRoot) || {};
|
|
1079
|
+
const mergedSettings = mergeAgentHubSettingsDefaults(existingSettings);
|
|
1080
|
+
await writeAgentHubSettings(targetRoot, {
|
|
1081
|
+
...mergedSettings,
|
|
1082
|
+
preferences: {
|
|
1083
|
+
...mergedSettings.preferences || {},
|
|
1084
|
+
defaultProfile: profile
|
|
1085
|
+
}
|
|
1086
|
+
});
|
|
1087
|
+
};
|
|
1088
|
+
const resolveStartProfilePreference = async (workspace, targetRoot = defaultAgentHubHome()) => {
|
|
1089
|
+
const defaultProfile = await readStartDefaultProfile(targetRoot);
|
|
1090
|
+
if (defaultProfile) {
|
|
1091
|
+
return { profile: defaultProfile, source: "default" };
|
|
1092
|
+
}
|
|
1093
|
+
const preferences = await loadWorkspacePreferences(workspace);
|
|
1094
|
+
const lastProfile = preferences.start?.lastProfile?.trim();
|
|
1095
|
+
if (lastProfile) {
|
|
1096
|
+
return { profile: lastProfile, source: "last" };
|
|
1097
|
+
}
|
|
1098
|
+
return { profile: "auto", source: "fallback" };
|
|
1099
|
+
};
|
|
1100
|
+
const resolveHrLastProfilePreference = async (workspace) => {
|
|
1101
|
+
const preferences = await loadWorkspacePreferences(workspace);
|
|
1102
|
+
return preferences.hr?.lastProfile?.trim() || void 0;
|
|
1103
|
+
};
|
|
1104
|
+
const resolveStartLastProfilePreference = async (workspace) => {
|
|
1105
|
+
const preferences = await loadWorkspacePreferences(workspace);
|
|
1106
|
+
const lastProfile = preferences.start?.lastProfile?.trim();
|
|
1107
|
+
if (lastProfile) {
|
|
1108
|
+
return { profile: lastProfile, source: "last" };
|
|
1109
|
+
}
|
|
1110
|
+
return { profile: "auto", source: "fallback" };
|
|
1111
|
+
};
|
|
1112
|
+
const noteProfileResolution = (command, source, profile) => {
|
|
1113
|
+
if (source === "explicit") return;
|
|
1114
|
+
if (command === "start" && source === "default") {
|
|
1115
|
+
process.stderr.write(`[agenthub] start -> using personal default profile '${profile}'.
|
|
1116
|
+
`);
|
|
1117
|
+
return;
|
|
1118
|
+
}
|
|
1119
|
+
if (source === "last") {
|
|
1120
|
+
process.stderr.write(`[agenthub] ${command} -> using last profile '${profile}'.
|
|
1121
|
+
`);
|
|
1122
|
+
return;
|
|
1123
|
+
}
|
|
1124
|
+
if (command === "start" && source === "fallback") {
|
|
1125
|
+
process.stderr.write(`[agenthub] start -> no default or previous profile found; using 'auto'.
|
|
1126
|
+
`);
|
|
1127
|
+
}
|
|
1128
|
+
};
|
|
1129
|
+
const warnIfWorkspaceRuntimeWillBeReplaced = async (workspace, label) => {
|
|
1130
|
+
const lockPath = path.join(getWorkspaceRuntimeRoot(workspace), "agenthub-lock.json");
|
|
1131
|
+
if (!await readJsonIfExists(lockPath)) return;
|
|
1132
|
+
process.stderr.write(
|
|
1133
|
+
`[agenthub] Replacing the current workspace runtime with ${label}. Plain 'opencode' in this folder will use the new runtime after compose.
|
|
1134
|
+
`
|
|
1135
|
+
);
|
|
1136
|
+
};
|
|
1137
|
+
const toWorkspaceEnvrc = (workspace, configRoot) => {
|
|
1138
|
+
const resolvedConfigRoot = path.resolve(configRoot);
|
|
1139
|
+
const relativeConfigRoot = path.relative(workspace, resolvedConfigRoot);
|
|
1140
|
+
const configRootRef = relativeConfigRoot && !relativeConfigRoot.startsWith("..") ? `$PWD/${relativeConfigRoot}` : resolvedConfigRoot;
|
|
1141
|
+
return `# Generated by opencode-agenthub. Remove this file to disable auto-activation.
|
|
1142
|
+
export XDG_CONFIG_HOME="${configRootRef}/xdg"
|
|
1143
|
+
export OPENCODE_DISABLE_PROJECT_CONFIG=true
|
|
1144
|
+
export OPENCODE_CONFIG_DIR="${configRootRef}"
|
|
1145
|
+
`;
|
|
1146
|
+
};
|
|
1147
|
+
const maybeConfigureEnvrc = async (workspace, configRoot) => {
|
|
1148
|
+
if (!shouldOfferEnvrc()) return;
|
|
1149
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) return;
|
|
1150
|
+
const preferences = await loadWorkspacePreferences(workspace);
|
|
1151
|
+
const envrcPath = path.join(workspace, ".envrc");
|
|
1152
|
+
const envrcExists = await stat(envrcPath).then((s) => s.isFile() || s.isFIFO()).catch((e) => e.code === "ENOENT" ? false : Promise.reject(e));
|
|
1153
|
+
if (envrcExists) {
|
|
1154
|
+
if (!preferences.envrc?.enabled || !preferences.envrc?.prompted) {
|
|
1155
|
+
await saveWorkspacePreferences(workspace, {
|
|
1156
|
+
...preferences,
|
|
1157
|
+
envrc: { prompted: true, enabled: true }
|
|
1158
|
+
});
|
|
1159
|
+
}
|
|
1160
|
+
return;
|
|
1161
|
+
}
|
|
1162
|
+
if (preferences.envrc?.prompted) return;
|
|
1163
|
+
const rl = createPromptInterface();
|
|
1164
|
+
try {
|
|
1165
|
+
const enableEnvrc = await promptBoolean(
|
|
1166
|
+
rl,
|
|
1167
|
+
"Enable Agent Hub auto-activation with .envrc so plain 'opencode' works here?",
|
|
1168
|
+
false
|
|
1169
|
+
);
|
|
1170
|
+
if (enableEnvrc) {
|
|
1171
|
+
await writeFile(
|
|
1172
|
+
envrcPath,
|
|
1173
|
+
toWorkspaceEnvrc(workspace, configRoot),
|
|
1174
|
+
"utf-8"
|
|
1175
|
+
);
|
|
1176
|
+
process.stdout.write(
|
|
1177
|
+
`Wrote ${envrcPath}. Run 'direnv allow' in this workspace to enable plain 'opencode'.
|
|
1178
|
+
`
|
|
1179
|
+
);
|
|
1180
|
+
}
|
|
1181
|
+
await saveWorkspacePreferences(workspace, {
|
|
1182
|
+
...preferences,
|
|
1183
|
+
envrc: { prompted: true, enabled: enableEnvrc }
|
|
1184
|
+
});
|
|
1185
|
+
} finally {
|
|
1186
|
+
rl.close();
|
|
1187
|
+
}
|
|
1188
|
+
};
|
|
1189
|
+
const createPromptInterface = () => readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
1190
|
+
const promptRequired = async (rl, question, defaultValue) => {
|
|
1191
|
+
while (true) {
|
|
1192
|
+
const answer = await rl.question(
|
|
1193
|
+
defaultValue ? `${question} [${defaultValue}]: ` : `${question}: `
|
|
1194
|
+
);
|
|
1195
|
+
const value = normalizeOptional(answer) || defaultValue;
|
|
1196
|
+
if (value) return value;
|
|
1197
|
+
process.stdout.write("This field is required.\n");
|
|
1198
|
+
}
|
|
1199
|
+
};
|
|
1200
|
+
const promptOptional = async (rl, question, defaultValue) => {
|
|
1201
|
+
const answer = await rl.question(
|
|
1202
|
+
defaultValue ? `${question} [${defaultValue}]: ` : `${question}: `
|
|
1203
|
+
);
|
|
1204
|
+
return normalizeOptional(answer) || defaultValue;
|
|
1205
|
+
};
|
|
1206
|
+
const promptCsv = async (rl, question, defaultValues = []) => {
|
|
1207
|
+
const defaultValue = defaultValues.join(", ");
|
|
1208
|
+
const answer = await rl.question(
|
|
1209
|
+
defaultValue ? `${question} [${defaultValue}]: ` : `${question}: `
|
|
1210
|
+
);
|
|
1211
|
+
return normalizeCsv(answer || defaultValue);
|
|
1212
|
+
};
|
|
1213
|
+
const promptBoolean = async (rl, question, defaultValue) => {
|
|
1214
|
+
const suffix = defaultValue ? "[Y/n]" : "[y/N]";
|
|
1215
|
+
while (true) {
|
|
1216
|
+
const answer = (await rl.question(`${question} ${suffix}: `)).trim().toLowerCase();
|
|
1217
|
+
if (!answer) return defaultValue;
|
|
1218
|
+
if (answer === "y" || answer === "yes") return true;
|
|
1219
|
+
if (answer === "n" || answer === "no") return false;
|
|
1220
|
+
process.stdout.write("Please answer y or n.\n");
|
|
1221
|
+
}
|
|
1222
|
+
};
|
|
1223
|
+
const promptChoice = async (rl, question, choices, defaultValue) => {
|
|
1224
|
+
const label = `${question} [${choices.join("/")}] (${defaultValue})`;
|
|
1225
|
+
while (true) {
|
|
1226
|
+
const answer = (await rl.question(`${label}: `)).trim().toLowerCase();
|
|
1227
|
+
if (!answer) return defaultValue;
|
|
1228
|
+
const match = choices.find((choice) => choice === answer);
|
|
1229
|
+
if (match) return match;
|
|
1230
|
+
process.stdout.write(`Choose one of: ${choices.join(", ")}
|
|
1231
|
+
`);
|
|
1232
|
+
}
|
|
1233
|
+
};
|
|
1234
|
+
const promptIndexedChoice = async (rl, question, choices, defaultValue) => {
|
|
1235
|
+
choices.forEach((choice, index) => {
|
|
1236
|
+
process.stdout.write(` ${index + 1}. ${choice}
|
|
1237
|
+
`);
|
|
1238
|
+
});
|
|
1239
|
+
const defaultIndex = Math.max(choices.indexOf(defaultValue), 0) + 1;
|
|
1240
|
+
while (true) {
|
|
1241
|
+
const answer = (await rl.question(`${question} [1-${choices.length}] (${defaultIndex}): `)).trim().toLowerCase();
|
|
1242
|
+
if (!answer) return defaultValue;
|
|
1243
|
+
const numeric = Number(answer);
|
|
1244
|
+
if (Number.isInteger(numeric) && numeric >= 1 && numeric <= choices.length) {
|
|
1245
|
+
return choices[numeric - 1] || defaultValue;
|
|
1246
|
+
}
|
|
1247
|
+
const exactMatch = choices.find((choice) => choice.toLowerCase() === answer);
|
|
1248
|
+
if (exactMatch) return exactMatch;
|
|
1249
|
+
process.stdout.write("Choose a listed number or exact model id.\n");
|
|
1250
|
+
}
|
|
1251
|
+
};
|
|
1252
|
+
const listOpencodeFreeModels = async () => new Promise((resolve) => {
|
|
1253
|
+
const child = spawn("opencode", ["models", "opencode"], {
|
|
1254
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
1255
|
+
...spawnOptions()
|
|
1256
|
+
});
|
|
1257
|
+
let stdout = "";
|
|
1258
|
+
child.stdout.on("data", (chunk) => {
|
|
1259
|
+
stdout += chunk.toString();
|
|
1260
|
+
});
|
|
1261
|
+
child.on("error", () => resolve([]));
|
|
1262
|
+
child.on("close", () => {
|
|
1263
|
+
const models = stdout.split("\n").map((line) => line.trim()).filter((line) => line.startsWith("opencode/") && line.includes("free"));
|
|
1264
|
+
resolve([...new Set(models)].sort());
|
|
1265
|
+
});
|
|
1266
|
+
});
|
|
1267
|
+
const promptOptionalCsvSelection = async (rl, question, available, defaultValues = []) => {
|
|
1268
|
+
const include = await promptBoolean(rl, question, false);
|
|
1269
|
+
if (!include) return [];
|
|
1270
|
+
if (available.length > 0) {
|
|
1271
|
+
process.stdout.write(`Available: ${available.join(", ")}
|
|
1272
|
+
`);
|
|
1273
|
+
}
|
|
1274
|
+
return promptCsv(rl, "Enter names (comma-separated)", defaultValues);
|
|
1275
|
+
};
|
|
1276
|
+
const listSkillNames = async (skillsDir) => {
|
|
1277
|
+
try {
|
|
1278
|
+
const entries = await readdir(skillsDir, { withFileTypes: true });
|
|
1279
|
+
return entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name).sort();
|
|
1280
|
+
} catch {
|
|
1281
|
+
return [];
|
|
1282
|
+
}
|
|
1283
|
+
};
|
|
1284
|
+
const assertNameNotReserved = async (kind, name, reservedOk) => {
|
|
1285
|
+
if (reservedOk) return;
|
|
1286
|
+
const builtIns = await listBuiltInAssetNames(kind);
|
|
1287
|
+
if (!builtIns.has(name)) return;
|
|
1288
|
+
fail(
|
|
1289
|
+
`'${name}' is a reserved built-in ${kind} name. Use a different name or pass '--reserved-ok' to override.`
|
|
1290
|
+
);
|
|
1291
|
+
};
|
|
1292
|
+
const detectSetupModeForHome = async (targetRoot) => {
|
|
1293
|
+
const settings = await readAgentHubSettings(targetRoot);
|
|
1294
|
+
const mode = settings?.meta?.onboarding?.mode;
|
|
1295
|
+
if (mode === "auto" || mode === "minimal") {
|
|
1296
|
+
return mode;
|
|
1297
|
+
}
|
|
1298
|
+
const legacyStarter = settings?.meta?.onboarding?.starter;
|
|
1299
|
+
if (legacyStarter === "none" || legacyStarter === "framework") {
|
|
1300
|
+
return "minimal";
|
|
1301
|
+
}
|
|
1302
|
+
return "auto";
|
|
1303
|
+
};
|
|
1304
|
+
const detectInstallModeForHome = async (targetRoot, isHrHome = false) => {
|
|
1305
|
+
if (isHrHome) return "hr-office";
|
|
1306
|
+
return detectSetupModeForHome(targetRoot);
|
|
1307
|
+
};
|
|
1308
|
+
const warnIfBuiltInsDrifted = async (targetRoot, mode) => {
|
|
1309
|
+
const settings = await readAgentHubSettings(targetRoot);
|
|
1310
|
+
const builtinVersion = settings?.meta?.builtinVersion;
|
|
1311
|
+
if (!builtinVersion || Object.keys(builtinVersion).length === 0) return;
|
|
1312
|
+
const effectiveMode = mode ?? await detectInstallModeForHome(targetRoot);
|
|
1313
|
+
const allowedKeys = new Set(getBuiltInManifestKeysForMode(effectiveMode));
|
|
1314
|
+
const currentVersion = readPackageVersion();
|
|
1315
|
+
const staleAssets = Object.entries(builtinVersion).filter(([asset]) => allowedKeys.has(asset)).filter(([, installedVersion]) => installedVersion !== currentVersion).map(([asset]) => asset).sort();
|
|
1316
|
+
if (staleAssets.length === 0) return;
|
|
1317
|
+
process.stderr.write(
|
|
1318
|
+
[
|
|
1319
|
+
`[agenthub] Built-in assets may be stale (${staleAssets.length} item${staleAssets.length === 1 ? "" : "s"}) relative to package ${currentVersion}.`,
|
|
1320
|
+
`Run '${cliCommand} upgrade${targetRoot !== defaultAgentHubHome() ? ` --target-root ${targetRoot}` : ""}' to preview or sync updates.`
|
|
1321
|
+
].join("\n") + "\n"
|
|
1322
|
+
);
|
|
1323
|
+
};
|
|
1324
|
+
const hrLegacyAssetNames = [
|
|
1325
|
+
"hr",
|
|
1326
|
+
"hr-sourcer",
|
|
1327
|
+
"hr-evaluator",
|
|
1328
|
+
"hr-cto",
|
|
1329
|
+
"hr-adapter",
|
|
1330
|
+
"hr-verifier"
|
|
1331
|
+
];
|
|
1332
|
+
const warnAboutLegacyHrAssets = async (personalRoot = defaultAgentHubHome()) => {
|
|
1333
|
+
const hrBundleDir = path.join(personalRoot, "bundles");
|
|
1334
|
+
const found = await Promise.all(
|
|
1335
|
+
hrLegacyAssetNames.map(async (name) => ({
|
|
1336
|
+
name,
|
|
1337
|
+
exists: await readFile(path.join(hrBundleDir, `${name}.json`), "utf-8").then(() => true).catch(() => false)
|
|
1338
|
+
}))
|
|
1339
|
+
);
|
|
1340
|
+
const present = found.filter((entry) => entry.exists).map((entry) => entry.name);
|
|
1341
|
+
if (present.length === 0) return;
|
|
1342
|
+
process.stderr.write(
|
|
1343
|
+
[
|
|
1344
|
+
`[agenthub] Notice: legacy HR assets were found in your personal home (${present.join(", ")}).`,
|
|
1345
|
+
`[agenthub] They are left in place for compatibility. Use '${cliCommand} hr' for new isolated HR work.`
|
|
1346
|
+
].join("\n") + "\n"
|
|
1347
|
+
);
|
|
1348
|
+
};
|
|
1349
|
+
const profileExistsInHome = async (targetRoot, profile) => {
|
|
1350
|
+
return Boolean(
|
|
1351
|
+
await readJsonIfExists(
|
|
1352
|
+
path.join(targetRoot, "profiles", `${profile}.json`)
|
|
1353
|
+
)
|
|
1354
|
+
);
|
|
1355
|
+
};
|
|
1356
|
+
const listStagedHrProfileMatches = async (profile, hrRoot = defaultHrHome()) => {
|
|
1357
|
+
try {
|
|
1358
|
+
const stagingRoot = path.join(hrRoot, "staging");
|
|
1359
|
+
const entries = await readdir(stagingRoot, { withFileTypes: true });
|
|
1360
|
+
const matches = await Promise.all(
|
|
1361
|
+
entries.filter((entry) => entry.isDirectory()).map(async (entry) => {
|
|
1362
|
+
const libraryRoot = path.join(stagingRoot, entry.name, "agenthub-home");
|
|
1363
|
+
const profilePath = path.join(libraryRoot, "profiles", `${profile}.json`);
|
|
1364
|
+
const profileStat = await stat(profilePath).catch(() => null);
|
|
1365
|
+
if (!profileStat?.isFile()) return null;
|
|
1366
|
+
return {
|
|
1367
|
+
packageId: entry.name,
|
|
1368
|
+
profile,
|
|
1369
|
+
libraryRoot,
|
|
1370
|
+
profilePath,
|
|
1371
|
+
modifiedAtMs: profileStat.mtimeMs
|
|
1372
|
+
};
|
|
1373
|
+
})
|
|
1374
|
+
);
|
|
1375
|
+
return matches.filter((match) => Boolean(match)).sort((a, b) => b.modifiedAtMs - a.modifiedAtMs || a.packageId.localeCompare(b.packageId));
|
|
1376
|
+
} catch {
|
|
1377
|
+
return [];
|
|
1378
|
+
}
|
|
1379
|
+
};
|
|
1380
|
+
const resolveHrProfileSource = async (profile, hrRoot = defaultHrHome()) => {
|
|
1381
|
+
if (await profileExistsInHome(hrRoot, profile)) {
|
|
1382
|
+
return {
|
|
1383
|
+
libraryRoot: hrRoot,
|
|
1384
|
+
settingsRoot: hrRoot,
|
|
1385
|
+
kind: "home"
|
|
1386
|
+
};
|
|
1387
|
+
}
|
|
1388
|
+
const stagedMatches = await listStagedHrProfileMatches(profile, hrRoot);
|
|
1389
|
+
if (stagedMatches.length > 0) {
|
|
1390
|
+
return {
|
|
1391
|
+
libraryRoot: stagedMatches[0].libraryRoot,
|
|
1392
|
+
settingsRoot: hrRoot,
|
|
1393
|
+
kind: "staged",
|
|
1394
|
+
match: stagedMatches[0]
|
|
1395
|
+
};
|
|
1396
|
+
}
|
|
1397
|
+
return {
|
|
1398
|
+
libraryRoot: hrRoot,
|
|
1399
|
+
settingsRoot: hrRoot,
|
|
1400
|
+
kind: "home"
|
|
1401
|
+
};
|
|
1402
|
+
};
|
|
1403
|
+
const printHrStagedProfileResolution = async (profile, match, hrRoot = defaultHrHome()) => {
|
|
1404
|
+
const stagedMatches = await listStagedHrProfileMatches(profile, hrRoot);
|
|
1405
|
+
process.stderr.write(
|
|
1406
|
+
`[agenthub] Staging test -> using profile '${profile}' from staged package '${match.packageId}'.
|
|
1407
|
+
`
|
|
1408
|
+
);
|
|
1409
|
+
process.stderr.write(`[agenthub] Source: ${match.libraryRoot}
|
|
1410
|
+
`);
|
|
1411
|
+
if (stagedMatches.length > 1) {
|
|
1412
|
+
const alternates = stagedMatches.slice(1).map((item) => item.packageId).join(", ");
|
|
1413
|
+
process.stderr.write(
|
|
1414
|
+
`[agenthub] Warning: profile '${profile}' also exists in other staged packages: ${alternates}. Using the most recently updated match.
|
|
1415
|
+
`
|
|
1416
|
+
);
|
|
1417
|
+
}
|
|
1418
|
+
};
|
|
1419
|
+
const printUpgradeReport = (targetRoot, report, options) => {
|
|
1420
|
+
const verb = options.dryRun ? "Would" : "Did";
|
|
1421
|
+
process.stdout.write(`Built-in asset sync ${options.dryRun ? "preview" : "complete"}
|
|
1422
|
+
`);
|
|
1423
|
+
process.stdout.write(`- target: ${targetRoot}
|
|
1424
|
+
`);
|
|
1425
|
+
process.stdout.write(`- mode: ${options.dryRun ? "dry-run" : options.force ? "force" : "safe"}
|
|
1426
|
+
`);
|
|
1427
|
+
process.stdout.write(`- add: ${report.added.length}
|
|
1428
|
+
`);
|
|
1429
|
+
process.stdout.write(`- update: ${report.updated.length}
|
|
1430
|
+
`);
|
|
1431
|
+
process.stdout.write(`- skip: ${report.skipped.length}
|
|
1432
|
+
`);
|
|
1433
|
+
for (const [label, entries] of [
|
|
1434
|
+
[`${verb} add`, report.added],
|
|
1435
|
+
[`${verb} update`, report.updated],
|
|
1436
|
+
[`${verb} skip`, report.skipped]
|
|
1437
|
+
]) {
|
|
1438
|
+
if (entries.length === 0) continue;
|
|
1439
|
+
process.stdout.write(`${label}:
|
|
1440
|
+
`);
|
|
1441
|
+
for (const entry of entries) {
|
|
1442
|
+
process.stdout.write(`- ${entry}
|
|
1443
|
+
`);
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
1446
|
+
if (options.dryRun) {
|
|
1447
|
+
process.stdout.write("Run again with '--force' to overwrite managed built-in files.\n");
|
|
1448
|
+
} else if (!options.force) {
|
|
1449
|
+
process.stdout.write("Existing managed files were left in place; re-run with '--force' to overwrite them.\n");
|
|
1450
|
+
}
|
|
1451
|
+
};
|
|
1452
|
+
const createSoulDefinition = async (root, name, reservedOk = false) => {
|
|
1453
|
+
await assertNameNotReserved("soul", name, reservedOk);
|
|
1454
|
+
const filePath = path.join(root, "souls", `${name}.md`);
|
|
1455
|
+
const rl = createPromptInterface();
|
|
1456
|
+
try {
|
|
1457
|
+
await maybeOverwrite(rl, filePath);
|
|
1458
|
+
const content = [
|
|
1459
|
+
`# ${name}`,
|
|
1460
|
+
"",
|
|
1461
|
+
"## Description",
|
|
1462
|
+
"Describe this soul's purpose and when to use it.",
|
|
1463
|
+
"",
|
|
1464
|
+
"## Behavior",
|
|
1465
|
+
"- Primary goals",
|
|
1466
|
+
"- Constraints",
|
|
1467
|
+
"- Expected output style",
|
|
1468
|
+
""
|
|
1469
|
+
].join("\n");
|
|
1470
|
+
await mkdir(path.dirname(filePath), { recursive: true });
|
|
1471
|
+
await writeFile(filePath, content, "utf-8");
|
|
1472
|
+
return filePath;
|
|
1473
|
+
} finally {
|
|
1474
|
+
rl.close();
|
|
1475
|
+
}
|
|
1476
|
+
};
|
|
1477
|
+
const createInstructionDefinition = async (root, name, reservedOk = false) => {
|
|
1478
|
+
await assertNameNotReserved("instruction", name, reservedOk);
|
|
1479
|
+
const filePath = path.join(root, "instructions", `${name}.md`);
|
|
1480
|
+
const rl = createPromptInterface();
|
|
1481
|
+
try {
|
|
1482
|
+
await maybeOverwrite(rl, filePath);
|
|
1483
|
+
const content = [
|
|
1484
|
+
`# ${name}`,
|
|
1485
|
+
"",
|
|
1486
|
+
"## Purpose",
|
|
1487
|
+
"State the instruction this file adds to an agent.",
|
|
1488
|
+
"",
|
|
1489
|
+
"## Rules",
|
|
1490
|
+
"- Add concrete rules here",
|
|
1491
|
+
""
|
|
1492
|
+
].join("\n");
|
|
1493
|
+
await mkdir(path.dirname(filePath), { recursive: true });
|
|
1494
|
+
await writeFile(filePath, content, "utf-8");
|
|
1495
|
+
return filePath;
|
|
1496
|
+
} finally {
|
|
1497
|
+
rl.close();
|
|
1498
|
+
}
|
|
1499
|
+
};
|
|
1500
|
+
const createSkillDefinition = async (root, name, reservedOk = false) => {
|
|
1501
|
+
await assertNameNotReserved("skill", name, reservedOk);
|
|
1502
|
+
const filePath = path.join(root, "skills", name, "SKILL.md");
|
|
1503
|
+
const rl = createPromptInterface();
|
|
1504
|
+
try {
|
|
1505
|
+
await maybeOverwrite(rl, filePath);
|
|
1506
|
+
const content = [
|
|
1507
|
+
`# ${name}`,
|
|
1508
|
+
"",
|
|
1509
|
+
"## When to use",
|
|
1510
|
+
"Describe when this skill should be loaded.",
|
|
1511
|
+
"",
|
|
1512
|
+
"## Instructions",
|
|
1513
|
+
"- Add the concrete workflow here",
|
|
1514
|
+
""
|
|
1515
|
+
].join("\n");
|
|
1516
|
+
await mkdir(path.dirname(filePath), { recursive: true });
|
|
1517
|
+
await writeFile(filePath, content, "utf-8");
|
|
1518
|
+
return filePath;
|
|
1519
|
+
} finally {
|
|
1520
|
+
rl.close();
|
|
1521
|
+
}
|
|
1522
|
+
};
|
|
1523
|
+
const readProfileDefinition = async (root, name) => readJsonIfExists(path.join(root, "profiles", `${name}.json`));
|
|
1524
|
+
const promptRecord = async (rl, question) => {
|
|
1525
|
+
const answer = await rl.question(
|
|
1526
|
+
`${question} (comma-separated key=value, blank to skip): `
|
|
1527
|
+
);
|
|
1528
|
+
const entries = normalizeCsv(answer);
|
|
1529
|
+
if (entries.length === 0) return void 0;
|
|
1530
|
+
const record = {};
|
|
1531
|
+
for (const entry of entries) {
|
|
1532
|
+
const separator = entry.indexOf("=");
|
|
1533
|
+
if (separator === -1) {
|
|
1534
|
+
fail(`Invalid entry '${entry}'. Use key=value format.`);
|
|
1535
|
+
}
|
|
1536
|
+
const key = entry.slice(0, separator).trim();
|
|
1537
|
+
const value = entry.slice(separator + 1).trim();
|
|
1538
|
+
if (!key || !value) {
|
|
1539
|
+
fail(`Invalid entry '${entry}'. Use key=value format.`);
|
|
1540
|
+
}
|
|
1541
|
+
record[key] = value;
|
|
1542
|
+
}
|
|
1543
|
+
return Object.keys(record).length > 0 ? record : void 0;
|
|
1544
|
+
};
|
|
1545
|
+
const maybeOverwrite = async (rl, filePath) => {
|
|
1546
|
+
try {
|
|
1547
|
+
await readFile(filePath, "utf-8");
|
|
1548
|
+
} catch {
|
|
1549
|
+
return;
|
|
1550
|
+
}
|
|
1551
|
+
const overwrite = await promptBoolean(
|
|
1552
|
+
rl,
|
|
1553
|
+
`${path.basename(filePath)} already exists. Overwrite it?`,
|
|
1554
|
+
false
|
|
1555
|
+
);
|
|
1556
|
+
if (!overwrite) {
|
|
1557
|
+
fail(`Aborted without changing ${filePath}`);
|
|
1558
|
+
}
|
|
1559
|
+
};
|
|
1560
|
+
const createProfileDefinition = async (root, name, options = { addBundles: [], reservedOk: false }) => {
|
|
1561
|
+
await assertNameNotReserved("profile", name, options.reservedOk);
|
|
1562
|
+
const filePath = toJsonFile(root, "profiles", name);
|
|
1563
|
+
const availableBundles = await listNamesByExt(path.join(root, "bundles"), ".json");
|
|
1564
|
+
const addCapabilities = listProfileAddCapabilityNames();
|
|
1565
|
+
const seededProfile = options.fromProfile ? await readProfileDefinition(root, options.fromProfile) : void 0;
|
|
1566
|
+
if (options.fromProfile && !seededProfile) {
|
|
1567
|
+
const availableProfiles = await listNamesByExt(path.join(root, "profiles"), ".json");
|
|
1568
|
+
fail(
|
|
1569
|
+
`Profile '${options.fromProfile}' was not found. Available profiles: ${availableProfiles.join(", ") || "(none)"}`
|
|
1570
|
+
);
|
|
1571
|
+
}
|
|
1572
|
+
const expandedAdds = expandProfileAddSelections(options.addBundles.filter(Boolean));
|
|
1573
|
+
const seededBundles = uniqueValues([
|
|
1574
|
+
...seededProfile?.bundles || [],
|
|
1575
|
+
...expandedAdds
|
|
1576
|
+
]);
|
|
1577
|
+
const invalidBundles = seededBundles.filter((bundle) => !availableBundles.includes(bundle));
|
|
1578
|
+
if (invalidBundles.length > 0) {
|
|
1579
|
+
fail(
|
|
1580
|
+
`Unknown bundle(s): ${invalidBundles.join(", ")}. Available bundles: ${availableBundles.join(", ") || "(none)"}. Capability shorthands: ${addCapabilities.join(", ") || "(none)"}`
|
|
1581
|
+
);
|
|
1582
|
+
}
|
|
1583
|
+
const rl = createPromptInterface();
|
|
1584
|
+
try {
|
|
1585
|
+
await maybeOverwrite(rl, filePath);
|
|
1586
|
+
if (availableBundles.length > 0) {
|
|
1587
|
+
process.stdout.write(
|
|
1588
|
+
`Available bundles: ${availableBundles.join(", ")}
|
|
1589
|
+
`
|
|
1590
|
+
);
|
|
1591
|
+
}
|
|
1592
|
+
if (addCapabilities.length > 0) {
|
|
1593
|
+
process.stdout.write(
|
|
1594
|
+
`Capability shorthands for --add: ${addCapabilities.join(", ")}
|
|
1595
|
+
`
|
|
1596
|
+
);
|
|
1597
|
+
}
|
|
1598
|
+
const description = await promptOptional(rl, "Profile description");
|
|
1599
|
+
const bundles = await promptCsv(
|
|
1600
|
+
rl,
|
|
1601
|
+
"Bundles to include",
|
|
1602
|
+
seededBundles
|
|
1603
|
+
);
|
|
1604
|
+
const defaultAgent = await promptOptional(
|
|
1605
|
+
rl,
|
|
1606
|
+
"Default agent (leave blank to let runtime decide)"
|
|
1607
|
+
);
|
|
1608
|
+
const plugins = await promptCsv(
|
|
1609
|
+
rl,
|
|
1610
|
+
"Plugins to enable (comma-separated package names or paths)",
|
|
1611
|
+
seededProfile?.plugins || []
|
|
1612
|
+
);
|
|
1613
|
+
const payload = {
|
|
1614
|
+
name,
|
|
1615
|
+
bundles,
|
|
1616
|
+
plugins
|
|
1617
|
+
};
|
|
1618
|
+
if (description) payload.description = description;
|
|
1619
|
+
if (defaultAgent) payload.defaultAgent = defaultAgent;
|
|
1620
|
+
await writeJsonFile(filePath, payload);
|
|
1621
|
+
return filePath;
|
|
1622
|
+
} finally {
|
|
1623
|
+
rl.close();
|
|
1624
|
+
}
|
|
1625
|
+
};
|
|
1626
|
+
const createBundleDefinition = async (root, name, reservedOk = false) => {
|
|
1627
|
+
await assertNameNotReserved("bundle", name, reservedOk);
|
|
1628
|
+
const filePath = toJsonFile(root, "bundles", name);
|
|
1629
|
+
const availableSouls = await listNamesByExt(path.join(root, "souls"), ".md");
|
|
1630
|
+
const availableInstructions = await listNamesByExt(
|
|
1631
|
+
path.join(root, "instructions"),
|
|
1632
|
+
".md"
|
|
1633
|
+
);
|
|
1634
|
+
const availableSkills = await listSkillNames(path.join(root, "skills"));
|
|
1635
|
+
const availableMcp = await listNamesByExt(path.join(root, "mcp"), ".json");
|
|
1636
|
+
const rl = createPromptInterface();
|
|
1637
|
+
try {
|
|
1638
|
+
await maybeOverwrite(rl, filePath);
|
|
1639
|
+
if (availableSouls.length > 0) {
|
|
1640
|
+
process.stdout.write(`Available souls: ${availableSouls.join(", ")}
|
|
1641
|
+
`);
|
|
1642
|
+
}
|
|
1643
|
+
if (availableInstructions.length > 0) {
|
|
1644
|
+
process.stdout.write(`Available instructions: ${availableInstructions.join(", ")}
|
|
1645
|
+
`);
|
|
1646
|
+
}
|
|
1647
|
+
if (availableSkills.length > 0) {
|
|
1648
|
+
process.stdout.write(`Available skills: ${availableSkills.join(", ")}
|
|
1649
|
+
`);
|
|
1650
|
+
}
|
|
1651
|
+
const runtime = await promptChoice(
|
|
1652
|
+
rl,
|
|
1653
|
+
"Runtime",
|
|
1654
|
+
["native", "omo"],
|
|
1655
|
+
"native"
|
|
1656
|
+
);
|
|
1657
|
+
const readOnly = runtime === "native" ? await promptBoolean(
|
|
1658
|
+
rl,
|
|
1659
|
+
"Read-only agent? (Y = plan-like, N = build-like)",
|
|
1660
|
+
true
|
|
1661
|
+
) : false;
|
|
1662
|
+
const soul = await promptRequired(rl, "Soul name", availableSouls[0]);
|
|
1663
|
+
const instructions = await promptOptionalCsvSelection(
|
|
1664
|
+
rl,
|
|
1665
|
+
"Attach instructions from instructions/?",
|
|
1666
|
+
availableInstructions
|
|
1667
|
+
);
|
|
1668
|
+
const skills = await promptOptionalCsvSelection(
|
|
1669
|
+
rl,
|
|
1670
|
+
"Attach skills from skills/?",
|
|
1671
|
+
availableSkills
|
|
1672
|
+
);
|
|
1673
|
+
const mcp = await promptOptionalCsvSelection(
|
|
1674
|
+
rl,
|
|
1675
|
+
"Attach custom MCP tools? (basic opencode tools stay available)",
|
|
1676
|
+
availableMcp
|
|
1677
|
+
);
|
|
1678
|
+
const guards = await promptCsv(rl, "Extra guards (comma-separated)");
|
|
1679
|
+
const categories = runtime === "omo" ? await promptRecord(rl, "Category model mapping") : void 0;
|
|
1680
|
+
const agentName = await promptRequired(rl, "Agent name", name);
|
|
1681
|
+
const agentMode = await promptChoice(
|
|
1682
|
+
rl,
|
|
1683
|
+
"Agent mode",
|
|
1684
|
+
["primary", "subagent"],
|
|
1685
|
+
"primary"
|
|
1686
|
+
);
|
|
1687
|
+
const hidden = await promptBoolean(
|
|
1688
|
+
rl,
|
|
1689
|
+
"Hide this agent from normal listings?",
|
|
1690
|
+
false
|
|
1691
|
+
);
|
|
1692
|
+
const model = await promptRequired(rl, "Agent model");
|
|
1693
|
+
const description = await promptOptional(rl, "Agent description");
|
|
1694
|
+
const payload = {
|
|
1695
|
+
name,
|
|
1696
|
+
runtime,
|
|
1697
|
+
soul,
|
|
1698
|
+
...instructions.length > 0 ? { instructions } : {},
|
|
1699
|
+
skills,
|
|
1700
|
+
agent: {
|
|
1701
|
+
name: agentName,
|
|
1702
|
+
mode: agentMode,
|
|
1703
|
+
hidden,
|
|
1704
|
+
model
|
|
1705
|
+
}
|
|
1706
|
+
};
|
|
1707
|
+
if (readOnly) {
|
|
1708
|
+
payload.guards = uniqueValues([...payload.guards || [], "no_task"]);
|
|
1709
|
+
}
|
|
1710
|
+
if (mcp.length > 0) payload.mcp = mcp;
|
|
1711
|
+
if (guards.length > 0) {
|
|
1712
|
+
payload.guards = uniqueValues([...payload.guards || [], ...guards]);
|
|
1713
|
+
}
|
|
1714
|
+
if (description) payload.agent.description = description;
|
|
1715
|
+
if (categories && Object.keys(categories).length > 0) {
|
|
1716
|
+
payload.categories = categories;
|
|
1717
|
+
const enableSpawn = await promptBoolean(
|
|
1718
|
+
rl,
|
|
1719
|
+
"Generate category-family spawn config from those categories?",
|
|
1720
|
+
true
|
|
1721
|
+
);
|
|
1722
|
+
if (enableSpawn) {
|
|
1723
|
+
payload.spawn = {
|
|
1724
|
+
strategy: "category-family",
|
|
1725
|
+
source: "categories",
|
|
1726
|
+
shared: {
|
|
1727
|
+
soul,
|
|
1728
|
+
skills
|
|
1729
|
+
}
|
|
1730
|
+
};
|
|
1731
|
+
}
|
|
1732
|
+
}
|
|
1733
|
+
await writeJsonFile(filePath, payload);
|
|
1734
|
+
return filePath;
|
|
1735
|
+
} finally {
|
|
1736
|
+
rl.close();
|
|
1737
|
+
}
|
|
1738
|
+
};
|
|
1739
|
+
const resolveConfigRoot = (parsed2) => {
|
|
1740
|
+
if (parsed2.configRoot) return parsed2.configRoot;
|
|
1741
|
+
if (!parsed2.runtimeSelection) {
|
|
1742
|
+
return parsed2.workspace;
|
|
1743
|
+
}
|
|
1744
|
+
if (parsed2.runtimeSelection.kind === "tools-only") {
|
|
1745
|
+
return getWorkspaceRuntimeRoot(parsed2.workspace);
|
|
1746
|
+
}
|
|
1747
|
+
if (parsed2.runtimeSelection.kind === "customized-agent") {
|
|
1748
|
+
return getWorkspaceRuntimeRoot(parsed2.workspace);
|
|
1749
|
+
}
|
|
1750
|
+
return getDefaultConfigRoot(
|
|
1751
|
+
parsed2.workspace,
|
|
1752
|
+
parsed2.runtimeSelection.profile
|
|
1753
|
+
);
|
|
1754
|
+
};
|
|
1755
|
+
const resolveSelectedHomeRoot = (parsed2) => {
|
|
1756
|
+
if (parsed2.bootstrapOptions.targetRoot) {
|
|
1757
|
+
return parsed2.bootstrapOptions.targetRoot;
|
|
1758
|
+
}
|
|
1759
|
+
if (parsed2.command === "hr") {
|
|
1760
|
+
return defaultHrHome();
|
|
1761
|
+
}
|
|
1762
|
+
return void 0;
|
|
1763
|
+
};
|
|
1764
|
+
const composeSelection = async (parsed2, configRoot) => {
|
|
1765
|
+
let homeRoot = resolveSelectedHomeRoot(parsed2);
|
|
1766
|
+
let settingsRoot = homeRoot;
|
|
1767
|
+
if (!parsed2.runtimeSelection) {
|
|
1768
|
+
return { workspace: parsed2.workspace, configRoot };
|
|
1769
|
+
}
|
|
1770
|
+
if (parsed2.command === "hr" && parsed2.hrIntent?.kind === "compose" && parsed2.runtimeSelection.kind === "profile") {
|
|
1771
|
+
const resolved = await resolveHrProfileSource(
|
|
1772
|
+
parsed2.runtimeSelection.profile,
|
|
1773
|
+
defaultHrHome()
|
|
1774
|
+
);
|
|
1775
|
+
homeRoot = resolved.libraryRoot;
|
|
1776
|
+
settingsRoot = resolved.settingsRoot;
|
|
1777
|
+
if (resolved.kind === "staged" && resolved.match) {
|
|
1778
|
+
await printHrStagedProfileResolution(
|
|
1779
|
+
parsed2.runtimeSelection.profile,
|
|
1780
|
+
resolved.match,
|
|
1781
|
+
defaultHrHome()
|
|
1782
|
+
);
|
|
1783
|
+
}
|
|
1784
|
+
}
|
|
1785
|
+
if (parsed2.runtimeSelection.kind === "tools-only") {
|
|
1786
|
+
return composeToolInjection(parsed2.workspace, configRoot, { homeRoot, settingsRoot });
|
|
1787
|
+
}
|
|
1788
|
+
if (parsed2.runtimeSelection.kind === "customized-agent") {
|
|
1789
|
+
return composeCustomizedAgent(parsed2.workspace, configRoot, { homeRoot, settingsRoot });
|
|
1790
|
+
}
|
|
1791
|
+
return composeWorkspace(
|
|
1792
|
+
parsed2.workspace,
|
|
1793
|
+
parsed2.runtimeSelection.profile,
|
|
1794
|
+
configRoot,
|
|
1795
|
+
{ homeRoot, settingsRoot }
|
|
1796
|
+
);
|
|
1797
|
+
};
|
|
1798
|
+
const runOpencode = async (workspace, configRoot, opencodeArgs) => {
|
|
1799
|
+
const env = configRoot ? {
|
|
1800
|
+
...process.env,
|
|
1801
|
+
XDG_CONFIG_HOME: path.join(configRoot, "xdg"),
|
|
1802
|
+
OPENCODE_DISABLE_PROJECT_CONFIG: "true",
|
|
1803
|
+
OPENCODE_CONFIG_DIR: configRoot
|
|
1804
|
+
} : process.env;
|
|
1805
|
+
const child = spawn("opencode", opencodeArgs, {
|
|
1806
|
+
cwd: workspace,
|
|
1807
|
+
stdio: "inherit",
|
|
1808
|
+
env,
|
|
1809
|
+
...spawnOptions()
|
|
1810
|
+
});
|
|
1811
|
+
child.on("exit", (code, signal) => {
|
|
1812
|
+
if (signal) {
|
|
1813
|
+
process.kill(process.pid, signal);
|
|
1814
|
+
return;
|
|
1815
|
+
}
|
|
1816
|
+
process.exit(code ?? 0);
|
|
1817
|
+
});
|
|
1818
|
+
};
|
|
1819
|
+
const parsed = parseArgs(process.argv.slice(2));
|
|
1820
|
+
const startupNotice = windowsStartupNotice();
|
|
1821
|
+
if (startupNotice) {
|
|
1822
|
+
process.stderr.write(`${startupNotice}
|
|
1823
|
+
`);
|
|
1824
|
+
}
|
|
1825
|
+
if (parsed.command === "start" || parsed.command === "run") {
|
|
1826
|
+
if (parsed.runtimeSelection?.kind === "profile" && parsed.runtimeSelection.profile === "hr") {
|
|
1827
|
+
fail(
|
|
1828
|
+
`'start hr' and 'run hr' are no longer supported. Use '${cliCommand} hr' for HR Office or '${cliCommand} hr <profile>' to test an HR profile in this workspace.`
|
|
1829
|
+
);
|
|
1830
|
+
}
|
|
1831
|
+
if (parsed.startIntent?.kind === "set-default") {
|
|
1832
|
+
const targetRoot = resolveSelectedHomeRoot(parsed) || defaultAgentHubHome();
|
|
1833
|
+
await ensureHomeReadyOrFail(targetRoot);
|
|
1834
|
+
if (!await profileExistsInHome(targetRoot, parsed.startIntent.profile)) {
|
|
1835
|
+
fail(
|
|
1836
|
+
`Profile '${parsed.startIntent.profile}' not found in ${path.join(targetRoot, "profiles")}.`
|
|
1837
|
+
);
|
|
1838
|
+
}
|
|
1839
|
+
await setStartDefaultProfile(parsed.startIntent.profile, targetRoot);
|
|
1840
|
+
process.stdout.write(
|
|
1841
|
+
`Set default start profile to '${parsed.startIntent.profile}' for ${targetRoot}
|
|
1842
|
+
`
|
|
1843
|
+
);
|
|
1844
|
+
process.exit(0);
|
|
1845
|
+
}
|
|
1846
|
+
if (parsed.startIntent?.kind === "compose" && !parsed.startIntent.profile) {
|
|
1847
|
+
const resolved = parsed.startIntent.source === "last" ? await resolveStartLastProfilePreference(parsed.workspace) : await resolveStartProfilePreference(
|
|
1848
|
+
parsed.workspace,
|
|
1849
|
+
resolveSelectedHomeRoot(parsed) || defaultAgentHubHome()
|
|
1850
|
+
);
|
|
1851
|
+
parsed.startIntent = {
|
|
1852
|
+
kind: "compose",
|
|
1853
|
+
profile: resolved.profile,
|
|
1854
|
+
source: resolved.source
|
|
1855
|
+
};
|
|
1856
|
+
parsed.runtimeSelection = {
|
|
1857
|
+
kind: "profile",
|
|
1858
|
+
profile: resolved.profile
|
|
1859
|
+
};
|
|
1860
|
+
noteProfileResolution("start", resolved.source, resolved.profile);
|
|
1861
|
+
}
|
|
1862
|
+
}
|
|
1863
|
+
if (parsed.command === "hr" && parsed.hrIntent?.kind === "compose" && parsed.hrIntent.source === "last") {
|
|
1864
|
+
const lastProfile = await resolveHrLastProfilePreference(parsed.workspace);
|
|
1865
|
+
if (!lastProfile) {
|
|
1866
|
+
fail(`No previous HR workspace profile for this folder. Use: ${cliCommand} hr <profile>`);
|
|
1867
|
+
}
|
|
1868
|
+
parsed.hrIntent = { kind: "compose", profile: lastProfile, source: "last" };
|
|
1869
|
+
parsed.runtimeSelection = {
|
|
1870
|
+
kind: "profile",
|
|
1871
|
+
profile: lastProfile
|
|
1872
|
+
};
|
|
1873
|
+
noteProfileResolution("hr", "last", lastProfile);
|
|
1874
|
+
}
|
|
1875
|
+
if (parsed.command === "setup") {
|
|
1876
|
+
const options = Object.keys(parsed.bootstrapOptions).length ? parsed.bootstrapOptions : await promptHubInitAnswers();
|
|
1877
|
+
const targetRoot = await installAgentHubHome(options);
|
|
1878
|
+
const mode = options.mode ?? "auto";
|
|
1879
|
+
if (mode === "auto") {
|
|
1880
|
+
process.stdout.write(`\u2713 Coding system ready. Run: ${cliCommand} start
|
|
1881
|
+
`);
|
|
1882
|
+
} else {
|
|
1883
|
+
process.stdout.write("\u2713 Minimal Agent Hub structure ready. Add your own assets anytime.\n");
|
|
1884
|
+
}
|
|
1885
|
+
process.stdout.write(`${targetRoot}
|
|
1886
|
+
`);
|
|
1887
|
+
process.exit(0);
|
|
1888
|
+
}
|
|
1889
|
+
if (parsed.command === "backup") {
|
|
1890
|
+
const outputRoot = parsed.transferOptions.outputRoot;
|
|
1891
|
+
if (!outputRoot) {
|
|
1892
|
+
fail("'backup' requires '--output <path>'.");
|
|
1893
|
+
}
|
|
1894
|
+
const report = await exportAgentHubHome({
|
|
1895
|
+
sourceRoot: defaultAgentHubHome(),
|
|
1896
|
+
outputRoot,
|
|
1897
|
+
pluginVersion: readPackageVersion()
|
|
1898
|
+
});
|
|
1899
|
+
printTransferReport("Backup", report);
|
|
1900
|
+
process.exit(0);
|
|
1901
|
+
}
|
|
1902
|
+
if (parsed.command === "restore") {
|
|
1903
|
+
const importSourceRoot = parsed.transferOptions.sourceRoot;
|
|
1904
|
+
if (!importSourceRoot) {
|
|
1905
|
+
fail("'restore' requires '--source <path>'.");
|
|
1906
|
+
}
|
|
1907
|
+
const report = await importAgentHubHome({
|
|
1908
|
+
sourceRoot: importSourceRoot,
|
|
1909
|
+
targetRoot: defaultAgentHubHome(),
|
|
1910
|
+
overwrite: parsed.transferOptions.overwrite,
|
|
1911
|
+
settingsMode: parsed.transferOptions.settingsMode
|
|
1912
|
+
});
|
|
1913
|
+
printTransferReport("Restore", report);
|
|
1914
|
+
process.exit(0);
|
|
1915
|
+
}
|
|
1916
|
+
if (parsed.command === "promote") {
|
|
1917
|
+
const hrRoot = defaultHrHome();
|
|
1918
|
+
const sourceRoot = await resolvePromoteSourceRoot(parsed.promotePackageId, hrRoot);
|
|
1919
|
+
const handoff = await readPromoteHandoff(sourceRoot);
|
|
1920
|
+
await validatePromoteSourceRoot(sourceRoot, hrRoot);
|
|
1921
|
+
const report = await importAgentHubHome({
|
|
1922
|
+
sourceRoot,
|
|
1923
|
+
targetRoot: defaultAgentHubHome(),
|
|
1924
|
+
overwrite: false,
|
|
1925
|
+
settingsMode: "preserve"
|
|
1926
|
+
});
|
|
1927
|
+
const defaultProfile = resolvePromoteDefaultProfile(handoff);
|
|
1928
|
+
if (defaultProfile) {
|
|
1929
|
+
await setStartDefaultProfile(defaultProfile);
|
|
1930
|
+
}
|
|
1931
|
+
printTransferReport("Promote", report);
|
|
1932
|
+
if (defaultProfile) {
|
|
1933
|
+
process.stdout.write(`- default profile updated: ${defaultProfile}
|
|
1934
|
+
`);
|
|
1935
|
+
}
|
|
1936
|
+
process.exit(0);
|
|
1937
|
+
}
|
|
1938
|
+
if (parsed.command === "hub-export") {
|
|
1939
|
+
const outputRoot = parsed.transferOptions.outputRoot;
|
|
1940
|
+
if (!outputRoot) {
|
|
1941
|
+
fail("'hub-export' requires '--output <path>'.");
|
|
1942
|
+
}
|
|
1943
|
+
const report = await exportAgentHubHome({
|
|
1944
|
+
sourceRoot: parsed.transferOptions.sourceRoot,
|
|
1945
|
+
outputRoot,
|
|
1946
|
+
pluginVersion: readPackageVersion()
|
|
1947
|
+
});
|
|
1948
|
+
printTransferReport("Export", report);
|
|
1949
|
+
process.exit(0);
|
|
1950
|
+
}
|
|
1951
|
+
if (parsed.command === "hub-import") {
|
|
1952
|
+
const importSourceRoot = parsed.transferOptions.sourceRoot;
|
|
1953
|
+
if (!importSourceRoot) {
|
|
1954
|
+
fail("'hub-import' requires '--source <path>'.");
|
|
1955
|
+
}
|
|
1956
|
+
const report = await importAgentHubHome({
|
|
1957
|
+
sourceRoot: importSourceRoot,
|
|
1958
|
+
targetRoot: parsed.transferOptions.targetRoot,
|
|
1959
|
+
overwrite: parsed.transferOptions.overwrite,
|
|
1960
|
+
settingsMode: parsed.transferOptions.settingsMode
|
|
1961
|
+
});
|
|
1962
|
+
printTransferReport("Import", report);
|
|
1963
|
+
process.exit(0);
|
|
1964
|
+
}
|
|
1965
|
+
if (parsed.command === "compose" || parsed.command === "new") {
|
|
1966
|
+
const agentHubHome = parsed.bootstrapOptions.targetRoot || defaultAgentHubHome();
|
|
1967
|
+
await ensureHomeReadyOrFail(agentHubHome);
|
|
1968
|
+
const composeSelection2 = parsed.composeSelection;
|
|
1969
|
+
if (!composeSelection2) {
|
|
1970
|
+
fail(
|
|
1971
|
+
parsed.command === "compose" ? "Use 'compose profile <name>' or 'compose bundle <name>'." : "Use 'new soul <name>', 'new skill <name>', 'new instruction <name>', 'new bundle <name>', or 'new profile <name>'."
|
|
1972
|
+
);
|
|
1973
|
+
}
|
|
1974
|
+
const filePath = composeSelection2.kind === "profile" ? await createProfileDefinition(
|
|
1975
|
+
agentHubHome,
|
|
1976
|
+
composeSelection2.name,
|
|
1977
|
+
parsed.profileCreateOptions
|
|
1978
|
+
) : composeSelection2.kind === "bundle" ? await createBundleDefinition(
|
|
1979
|
+
agentHubHome,
|
|
1980
|
+
composeSelection2.name,
|
|
1981
|
+
parsed.profileCreateOptions.reservedOk
|
|
1982
|
+
) : composeSelection2.kind === "soul" ? await createSoulDefinition(
|
|
1983
|
+
agentHubHome,
|
|
1984
|
+
composeSelection2.name,
|
|
1985
|
+
parsed.profileCreateOptions.reservedOk
|
|
1986
|
+
) : composeSelection2.kind === "skill" ? await createSkillDefinition(
|
|
1987
|
+
agentHubHome,
|
|
1988
|
+
composeSelection2.name,
|
|
1989
|
+
parsed.profileCreateOptions.reservedOk
|
|
1990
|
+
) : await createInstructionDefinition(
|
|
1991
|
+
agentHubHome,
|
|
1992
|
+
composeSelection2.name,
|
|
1993
|
+
parsed.profileCreateOptions.reservedOk
|
|
1994
|
+
);
|
|
1995
|
+
await warnIfBuiltInsDrifted(agentHubHome, await detectSetupModeForHome(agentHubHome));
|
|
1996
|
+
process.stdout.write(`${filePath}
|
|
1997
|
+
`);
|
|
1998
|
+
process.exit(0);
|
|
1999
|
+
}
|
|
2000
|
+
if (parsed.command === "plugin") {
|
|
2001
|
+
if (parsed.pluginSubcommand !== "doctor") {
|
|
2002
|
+
fail("Use 'plugin doctor'.");
|
|
2003
|
+
}
|
|
2004
|
+
const configRoot = resolvePluginConfigRoot(parsed.configRoot);
|
|
2005
|
+
const runtimeInspection = await inspectRuntimeConfig(configRoot);
|
|
2006
|
+
process.stdout.write(`Plugin doctor
|
|
2007
|
+
`);
|
|
2008
|
+
process.stdout.write(`- config root: ${configRoot}
|
|
2009
|
+
`);
|
|
2010
|
+
process.stdout.write(`- runtime file: ${runtimeInspection.runtimeConfigPath}
|
|
2011
|
+
`);
|
|
2012
|
+
if (!runtimeInspection.ok) {
|
|
2013
|
+
process.stdout.write(`- runtime config: missing or unreadable
|
|
2014
|
+
`);
|
|
2015
|
+
process.stdout.write(`- active mode: degraded
|
|
2016
|
+
`);
|
|
2017
|
+
process.stdout.write(`- blocked tools: call_omo_agent (safety fallback)
|
|
2018
|
+
`);
|
|
2019
|
+
process.stdout.write(`- plan detection: disabled
|
|
2020
|
+
`);
|
|
2021
|
+
process.stdout.write(`Next: run '${cliCommand} start auto' or compose a profile to generate agenthub-runtime.json.
|
|
2022
|
+
`);
|
|
2023
|
+
} else {
|
|
2024
|
+
const summary = summarizeRuntimeFeatureState(runtimeInspection.config);
|
|
2025
|
+
process.stdout.write(`- runtime config: ok
|
|
2026
|
+
`);
|
|
2027
|
+
process.stdout.write(`- active mode: composed runtime
|
|
2028
|
+
`);
|
|
2029
|
+
process.stdout.write(`- blocked tools: ${Array.from(summary.blockedTools).sort().join(", ") || "(none)"}
|
|
2030
|
+
`);
|
|
2031
|
+
process.stdout.write(`- plan detection: ${summary.planDetection?.enabled ? "enabled" : "disabled"}
|
|
2032
|
+
`);
|
|
2033
|
+
}
|
|
2034
|
+
const pluginConfigPath = path.join(configRoot, "opencode.jsonc");
|
|
2035
|
+
const xdgPluginConfigPath = path.join(configRoot, "xdg", "opencode", "opencode.json");
|
|
2036
|
+
let pluginRegistered = false;
|
|
2037
|
+
for (const filePath of [pluginConfigPath, xdgPluginConfigPath]) {
|
|
2038
|
+
const config = await readJsonIfExists(filePath);
|
|
2039
|
+
if (Array.isArray(config?.plugin) && config.plugin.includes("opencode-agenthub")) {
|
|
2040
|
+
pluginRegistered = true;
|
|
2041
|
+
break;
|
|
2042
|
+
}
|
|
2043
|
+
}
|
|
2044
|
+
process.stdout.write(`- plugin registered: ${pluginRegistered ? "yes" : "no"}
|
|
2045
|
+
`);
|
|
2046
|
+
if (!pluginRegistered) {
|
|
2047
|
+
process.stdout.write(`Next: compose a profile so opencode.jsonc includes 'opencode-agenthub'.
|
|
2048
|
+
`);
|
|
2049
|
+
}
|
|
2050
|
+
process.exit(0);
|
|
2051
|
+
}
|
|
2052
|
+
if (parsed.command === "upgrade") {
|
|
2053
|
+
const targetRoot = resolveSelectedHomeRoot(parsed) || defaultAgentHubHome();
|
|
2054
|
+
await ensureHomeReadyOrFail(targetRoot);
|
|
2055
|
+
const mode = await detectSetupModeForHome(targetRoot);
|
|
2056
|
+
const report = await syncBuiltInAgentHubAssets({
|
|
2057
|
+
targetRoot,
|
|
2058
|
+
mode,
|
|
2059
|
+
force: parsed.upgradeOptions.force,
|
|
2060
|
+
dryRun: parsed.upgradeOptions.dryRun
|
|
2061
|
+
});
|
|
2062
|
+
printUpgradeReport(targetRoot, report, parsed.upgradeOptions);
|
|
2063
|
+
process.exit(0);
|
|
2064
|
+
}
|
|
2065
|
+
if ((parsed.command === "run" || parsed.command === "start") && !parsed.runtimeSelection) {
|
|
2066
|
+
if (parsed.assembleOnly) {
|
|
2067
|
+
fail("'run'/'start' without a profile cannot be used with '--assemble-only'.");
|
|
2068
|
+
}
|
|
2069
|
+
if (process.stderr.isTTY) {
|
|
2070
|
+
process.stderr.write(
|
|
2071
|
+
"\u26A0 No profile selected \u2014 launching plain opencode (hub runtime is inactive)\n"
|
|
2072
|
+
);
|
|
2073
|
+
}
|
|
2074
|
+
await ensureWorkspaceReadable(parsed.workspace);
|
|
2075
|
+
await runOpencode(parsed.workspace, void 0, parsed.opencodeArgs);
|
|
2076
|
+
process.exit(0);
|
|
2077
|
+
}
|
|
2078
|
+
if (parsed.command === "doctor" || parsed.command === "hub-doctor") {
|
|
2079
|
+
const targetRoot = parsed.bootstrapOptions.targetRoot || defaultAgentHubHome();
|
|
2080
|
+
const {
|
|
2081
|
+
runDiagnostics,
|
|
2082
|
+
interactiveAssembly,
|
|
2083
|
+
interactiveDoctor,
|
|
2084
|
+
updateAgentModelOverride,
|
|
2085
|
+
updateAgentPromptOverride,
|
|
2086
|
+
fixMissingGuards,
|
|
2087
|
+
createBundleForSoul,
|
|
2088
|
+
createProfile,
|
|
2089
|
+
fixOmoMixedProfile,
|
|
2090
|
+
getAvailableBundles
|
|
2091
|
+
} = await import("../skills/agenthub-doctor/index.js");
|
|
2092
|
+
if (parsed.doctorOptions.agent) {
|
|
2093
|
+
const promptFilePath = parsed.doctorOptions.promptFile;
|
|
2094
|
+
const message = parsed.doctorOptions.model || parsed.doctorOptions.clearModel ? await updateAgentModelOverride(
|
|
2095
|
+
targetRoot,
|
|
2096
|
+
parsed.doctorOptions.agent,
|
|
2097
|
+
parsed.doctorOptions.clearModel ? "" : parsed.doctorOptions.model || ""
|
|
2098
|
+
) : await updateAgentPromptOverride(
|
|
2099
|
+
targetRoot,
|
|
2100
|
+
parsed.doctorOptions.agent,
|
|
2101
|
+
parsed.doctorOptions.clearPrompt ? "" : promptFilePath ? await readFile(promptFilePath, "utf-8") : fail(
|
|
2102
|
+
"'doctor --prompt-file <path>' requires a prompt file path."
|
|
2103
|
+
)
|
|
2104
|
+
);
|
|
2105
|
+
process.stdout.write(`${message}
|
|
2106
|
+
`);
|
|
2107
|
+
process.exit(0);
|
|
2108
|
+
}
|
|
2109
|
+
process.stdout.write("\u{1F50D} Running Agent Hub diagnostics...\n\n");
|
|
2110
|
+
const report = await runDiagnostics(targetRoot);
|
|
2111
|
+
if (report.healthy.length > 0) {
|
|
2112
|
+
process.stdout.write("\u2705 Healthy:\n");
|
|
2113
|
+
for (const item of report.healthy) {
|
|
2114
|
+
process.stdout.write(` - ${item}
|
|
2115
|
+
`);
|
|
2116
|
+
}
|
|
2117
|
+
process.stdout.write("\n");
|
|
2118
|
+
}
|
|
2119
|
+
if (report.issues.length === 0) {
|
|
2120
|
+
process.stdout.write("\u2705 No issues found! Agent Hub is ready to use.\n");
|
|
2121
|
+
if (parsed.doctorOptions.fixAll || parsed.doctorOptions.dryRun || !process.stdin.isTTY) {
|
|
2122
|
+
process.exit(0);
|
|
2123
|
+
}
|
|
2124
|
+
await interactiveDoctor(targetRoot, report);
|
|
2125
|
+
process.exit(0);
|
|
2126
|
+
}
|
|
2127
|
+
process.stdout.write("\u26A0\uFE0F Issues Found:\n");
|
|
2128
|
+
for (const issue of report.issues) {
|
|
2129
|
+
const icon = issue.severity === "error" ? "\u274C" : issue.severity === "warning" ? "\u26A0\uFE0F " : "\u2139\uFE0F ";
|
|
2130
|
+
process.stdout.write(` ${icon} ${issue.message}
|
|
2131
|
+
`);
|
|
2132
|
+
}
|
|
2133
|
+
process.stdout.write("\n");
|
|
2134
|
+
if (parsed.doctorOptions.dryRun) {
|
|
2135
|
+
process.stdout.write("(Dry run - no fixes applied)\n");
|
|
2136
|
+
process.exit(0);
|
|
2137
|
+
}
|
|
2138
|
+
if (parsed.doctorOptions.fixAll) {
|
|
2139
|
+
process.stdout.write("\u{1F527} Applying fixes...\n\n");
|
|
2140
|
+
const missingGuardsIssue = report.issues.find(
|
|
2141
|
+
(i) => i.type === "missing_guards"
|
|
2142
|
+
);
|
|
2143
|
+
if (missingGuardsIssue) {
|
|
2144
|
+
const guards = missingGuardsIssue.details.guards;
|
|
2145
|
+
const result2 = await fixMissingGuards(targetRoot, guards);
|
|
2146
|
+
process.stdout.write(
|
|
2147
|
+
` ${result2.success ? "\u2713" : "\u2717"} ${result2.message}
|
|
2148
|
+
`
|
|
2149
|
+
);
|
|
2150
|
+
}
|
|
2151
|
+
const orphanedSoulsIssue = report.issues.find(
|
|
2152
|
+
(i) => i.type === "orphaned_souls"
|
|
2153
|
+
);
|
|
2154
|
+
if (orphanedSoulsIssue) {
|
|
2155
|
+
const souls = orphanedSoulsIssue.details.souls;
|
|
2156
|
+
for (const soul of souls) {
|
|
2157
|
+
const result2 = await createBundleForSoul(targetRoot, soul);
|
|
2158
|
+
process.stdout.write(
|
|
2159
|
+
` ${result2.success ? "\u2713" : "\u2717"} ${result2.message}
|
|
2160
|
+
`
|
|
2161
|
+
);
|
|
2162
|
+
}
|
|
2163
|
+
}
|
|
2164
|
+
const noProfilesIssue = report.issues.find((i) => i.type === "no_profiles");
|
|
2165
|
+
if (noProfilesIssue) {
|
|
2166
|
+
const bundles = await getAvailableBundles(targetRoot);
|
|
2167
|
+
if (bundles.length > 0) {
|
|
2168
|
+
const result2 = await createProfile(targetRoot, "imported", {
|
|
2169
|
+
bundleNames: bundles
|
|
2170
|
+
});
|
|
2171
|
+
process.stdout.write(
|
|
2172
|
+
` ${result2.success ? "\u2713" : "\u2717"} ${result2.message}
|
|
2173
|
+
`
|
|
2174
|
+
);
|
|
2175
|
+
}
|
|
2176
|
+
}
|
|
2177
|
+
const omoIssue = report.issues.find((i) => i.type === "omo_mixed_profile");
|
|
2178
|
+
if (omoIssue) {
|
|
2179
|
+
const result2 = await fixOmoMixedProfile(
|
|
2180
|
+
targetRoot,
|
|
2181
|
+
omoIssue.details
|
|
2182
|
+
);
|
|
2183
|
+
process.stdout.write(
|
|
2184
|
+
` ${result2.success ? "\u2713" : "\u2717"} ${result2.message}
|
|
2185
|
+
`
|
|
2186
|
+
);
|
|
2187
|
+
}
|
|
2188
|
+
process.stdout.write("\n\u2705 All fixes applied!\n");
|
|
2189
|
+
process.exit(0);
|
|
2190
|
+
}
|
|
2191
|
+
await interactiveAssembly(targetRoot, report);
|
|
2192
|
+
process.exit(0);
|
|
2193
|
+
}
|
|
2194
|
+
if (parsed.command === "list") {
|
|
2195
|
+
const {
|
|
2196
|
+
labelSouls,
|
|
2197
|
+
labelBundles,
|
|
2198
|
+
labelProfiles,
|
|
2199
|
+
labelSkills,
|
|
2200
|
+
labelInstructions
|
|
2201
|
+
} = await import("./query.js");
|
|
2202
|
+
const hubHome = parsed.bootstrapOptions.targetRoot || defaultAgentHubHome();
|
|
2203
|
+
const target = parsed.listTarget;
|
|
2204
|
+
const printSection = (title, items) => {
|
|
2205
|
+
process.stdout.write(`
|
|
2206
|
+
${title} (${items.length}):
|
|
2207
|
+
`);
|
|
2208
|
+
if (items.length === 0) {
|
|
2209
|
+
process.stdout.write(" (none)\n");
|
|
2210
|
+
} else {
|
|
2211
|
+
for (const item of items) {
|
|
2212
|
+
process.stdout.write(` ${item.name} [${item.source}]
|
|
2213
|
+
`);
|
|
2214
|
+
}
|
|
2215
|
+
}
|
|
2216
|
+
};
|
|
2217
|
+
if (!target || target === "souls") {
|
|
2218
|
+
printSection("Souls", await labelSouls(hubHome));
|
|
2219
|
+
}
|
|
2220
|
+
if (!target || target === "bundles") {
|
|
2221
|
+
printSection("Bundles", await labelBundles(hubHome));
|
|
2222
|
+
}
|
|
2223
|
+
if (!target || target === "profiles") {
|
|
2224
|
+
printSection("Profiles", await labelProfiles(hubHome));
|
|
2225
|
+
}
|
|
2226
|
+
if (!target || target === "skills") {
|
|
2227
|
+
printSection("Skills", await labelSkills(hubHome));
|
|
2228
|
+
}
|
|
2229
|
+
if (!target || target === "instructions") {
|
|
2230
|
+
printSection("Instructions", await labelInstructions(hubHome));
|
|
2231
|
+
}
|
|
2232
|
+
if (target && !["souls", "bundles", "profiles", "skills", "instructions"].includes(target)) {
|
|
2233
|
+
fail(`Unknown list target '${target}'. Use: souls, bundles, profiles, skills, instructions`);
|
|
2234
|
+
}
|
|
2235
|
+
process.stdout.write("\n");
|
|
2236
|
+
process.exit(0);
|
|
2237
|
+
}
|
|
2238
|
+
if (parsed.command === "hr") {
|
|
2239
|
+
await ensureHrOfficeReadyOrBootstrap(resolveSelectedHomeRoot(parsed), {
|
|
2240
|
+
syncSourcesOnFirstRun: !parsed.assembleOnly
|
|
2241
|
+
});
|
|
2242
|
+
if (parsed.hrIntent?.kind === "office") {
|
|
2243
|
+
parsed.workspace = resolveSelectedHomeRoot(parsed) || defaultHrHome();
|
|
2244
|
+
} else if (parsed.hrIntent?.kind === "compose") {
|
|
2245
|
+
await warnIfWorkspaceRuntimeWillBeReplaced(
|
|
2246
|
+
parsed.workspace,
|
|
2247
|
+
`HR profile '${parsed.hrIntent.profile}'`
|
|
2248
|
+
);
|
|
2249
|
+
}
|
|
2250
|
+
} else if (parsed.command === "start" || parsed.command === "run") {
|
|
2251
|
+
await ensureHomeReadyOrBootstrap(resolveSelectedHomeRoot(parsed));
|
|
2252
|
+
await warnAboutLegacyHrAssets(resolveSelectedHomeRoot(parsed) || defaultAgentHubHome());
|
|
2253
|
+
} else {
|
|
2254
|
+
await ensureSelectedHomeReadyOrFail(parsed);
|
|
2255
|
+
}
|
|
2256
|
+
{
|
|
2257
|
+
const selectedHome = resolveSelectedHomeRoot(parsed) || defaultAgentHubHome();
|
|
2258
|
+
const selectedMode = await detectInstallModeForHome(selectedHome, parsed.command === "hr");
|
|
2259
|
+
await warnIfBuiltInsDrifted(selectedHome, selectedMode);
|
|
2260
|
+
}
|
|
2261
|
+
if (parsed.command === "run" || parsed.command === "start" || parsed.command === "hr") {
|
|
2262
|
+
await ensureWorkspaceReadable(parsed.workspace);
|
|
2263
|
+
}
|
|
2264
|
+
const finalConfigRoot = resolveConfigRoot(parsed);
|
|
2265
|
+
const result = await composeSelection(parsed, finalConfigRoot);
|
|
2266
|
+
printRuntimeBanner(
|
|
2267
|
+
parsed.command === "hr" ? "HR Office" : "My Team",
|
|
2268
|
+
resolveSelectedHomeRoot(parsed) || defaultAgentHubHome()
|
|
2269
|
+
);
|
|
2270
|
+
if (shouldChmod()) {
|
|
2271
|
+
await chmod(path.join(result.configRoot, "run.sh"), 493);
|
|
2272
|
+
}
|
|
2273
|
+
if ((parsed.command === "start" || parsed.command === "run") && parsed.runtimeSelection?.kind === "profile") {
|
|
2274
|
+
await updateWorkspacePreferences(parsed.workspace, (current) => ({
|
|
2275
|
+
...current,
|
|
2276
|
+
start: {
|
|
2277
|
+
...current.start || {},
|
|
2278
|
+
lastProfile: parsed.runtimeSelection?.kind === "profile" ? parsed.runtimeSelection.profile : void 0
|
|
2279
|
+
}
|
|
2280
|
+
}));
|
|
2281
|
+
}
|
|
2282
|
+
if (parsed.command === "hr" && parsed.hrIntent?.kind === "compose" && parsed.runtimeSelection?.kind === "profile") {
|
|
2283
|
+
await updateWorkspacePreferences(parsed.workspace, (current) => ({
|
|
2284
|
+
...current,
|
|
2285
|
+
hr: {
|
|
2286
|
+
...current.hr || {},
|
|
2287
|
+
lastProfile: parsed.runtimeSelection?.kind === "profile" ? parsed.runtimeSelection.profile : void 0
|
|
2288
|
+
}
|
|
2289
|
+
}));
|
|
2290
|
+
}
|
|
2291
|
+
if ((parsed.command === "run" || parsed.command === "start" || parsed.command === "hr" && parsed.hrIntent?.kind === "compose") && parsed.runtimeSelection?.kind === "profile" && !parsed.assembleOnly) {
|
|
2292
|
+
await maybeConfigureEnvrc(parsed.workspace, result.configRoot);
|
|
2293
|
+
}
|
|
2294
|
+
if (parsed.command === "compose" || parsed.assembleOnly) {
|
|
2295
|
+
process.stdout.write(`${result.configRoot}
|
|
2296
|
+
`);
|
|
2297
|
+
process.exit(0);
|
|
2298
|
+
}
|
|
2299
|
+
await runOpencode(parsed.workspace, result.configRoot, parsed.opencodeArgs);
|