@osovv/vv-opencode 1.4.5 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (143) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/README.md +47 -16
  3. package/dist/commands/patch-provider.d.ts +5 -5
  4. package/dist/commands/patch-provider.js +10 -6
  5. package/dist/commands/patch-provider.js.map +1 -1
  6. package/dist/lib/agent-tool-catalog.d.ts +144 -0
  7. package/dist/lib/agent-tool-catalog.js +2551 -0
  8. package/dist/lib/agent-tool-catalog.js.map +1 -0
  9. package/dist/lib/agent-tool-contract.d.ts +200 -0
  10. package/dist/lib/agent-tool-contract.js +407 -0
  11. package/dist/lib/agent-tool-contract.js.map +1 -0
  12. package/dist/lib/opencode/agent-registrations.d.ts +33 -0
  13. package/dist/lib/opencode/agent-registrations.js +469 -0
  14. package/dist/lib/opencode/agent-registrations.js.map +1 -0
  15. package/dist/lib/opencode/inspection.d.ts +84 -0
  16. package/dist/lib/opencode/inspection.js +367 -0
  17. package/dist/lib/opencode/inspection.js.map +1 -0
  18. package/dist/lib/opencode/model-overrides.d.ts +19 -0
  19. package/dist/lib/opencode/model-overrides.js +221 -0
  20. package/dist/lib/opencode/model-overrides.js.map +1 -0
  21. package/dist/lib/opencode/paths.d.ts +21 -0
  22. package/dist/lib/opencode/paths.js +62 -0
  23. package/dist/lib/opencode/paths.js.map +1 -0
  24. package/dist/lib/opencode/plugin-registration.d.ts +17 -0
  25. package/dist/lib/opencode/plugin-registration.js +210 -0
  26. package/dist/lib/opencode/plugin-registration.js.map +1 -0
  27. package/dist/lib/opencode/shared-utils.d.ts +30 -0
  28. package/dist/lib/opencode/shared-utils.js +183 -0
  29. package/dist/lib/opencode/shared-utils.js.map +1 -0
  30. package/dist/lib/opencode/vvoc-config-io.d.ts +9 -0
  31. package/dist/lib/opencode/vvoc-config-io.js +58 -0
  32. package/dist/lib/opencode/vvoc-config-io.js.map +1 -0
  33. package/dist/lib/opencode.d.ts +15 -174
  34. package/dist/lib/opencode.js +19 -1387
  35. package/dist/lib/opencode.js.map +1 -1
  36. package/dist/lib/spec-lint/cross-file-rules.d.ts +2 -0
  37. package/dist/lib/spec-lint/cross-file-rules.js +33 -0
  38. package/dist/lib/spec-lint/cross-file-rules.js.map +1 -0
  39. package/dist/lib/spec-lint/delegated-extraction.d.ts +8 -0
  40. package/dist/lib/spec-lint/delegated-extraction.js +112 -0
  41. package/dist/lib/spec-lint/delegated-extraction.js.map +1 -0
  42. package/dist/lib/spec-lint/lint.d.ts +7 -0
  43. package/dist/lib/spec-lint/lint.js +92 -0
  44. package/dist/lib/spec-lint/lint.js.map +1 -0
  45. package/dist/lib/spec-lint/parser.d.ts +101 -0
  46. package/dist/lib/spec-lint/parser.js +482 -0
  47. package/dist/lib/spec-lint/parser.js.map +1 -0
  48. package/dist/lib/spec-lint/plan-rules.d.ts +38 -0
  49. package/dist/lib/spec-lint/plan-rules.js +745 -0
  50. package/dist/lib/spec-lint/plan-rules.js.map +1 -0
  51. package/dist/lib/spec-lint/spec-rules.d.ts +8 -0
  52. package/dist/lib/spec-lint/spec-rules.js +188 -0
  53. package/dist/lib/spec-lint/spec-rules.js.map +1 -0
  54. package/dist/lib/spec-lint.d.ts +4 -63
  55. package/dist/lib/spec-lint.js +13 -1505
  56. package/dist/lib/spec-lint.js.map +1 -1
  57. package/dist/lib/vvoc-preset-registry.d.ts +7 -7
  58. package/dist/lib/vvoc-preset-registry.js +8 -8
  59. package/dist/lib/vvoc-preset-registry.js.map +1 -1
  60. package/dist/lib/workflow-contract.d.ts +12 -0
  61. package/dist/lib/workflow-contract.js +71 -18
  62. package/dist/lib/workflow-contract.js.map +1 -1
  63. package/dist/plugins/hashline-edit/index.js +102 -97
  64. package/dist/plugins/hashline-edit/index.js.map +1 -1
  65. package/dist/plugins/hashline-edit/normalize-edits.d.ts +2 -8
  66. package/dist/plugins/hashline-edit/normalize-edits.js +32 -17
  67. package/dist/plugins/hashline-edit/normalize-edits.js.map +1 -1
  68. package/dist/plugins/hashline-edit/schemas.d.ts +246 -0
  69. package/dist/plugins/hashline-edit/schemas.js +659 -0
  70. package/dist/plugins/hashline-edit/schemas.js.map +1 -0
  71. package/dist/plugins/hashline-edit/str-replace-editor.d.ts +6 -17
  72. package/dist/plugins/hashline-edit/str-replace-editor.js +19 -21
  73. package/dist/plugins/hashline-edit/str-replace-editor.js.map +1 -1
  74. package/dist/plugins/hashline-edit/tool-description.d.ts +1 -0
  75. package/dist/plugins/hashline-edit/tool-description.js +20 -4
  76. package/dist/plugins/hashline-edit/tool-description.js.map +1 -1
  77. package/dist/plugins/web-tools/fetch-service.d.ts +8 -5
  78. package/dist/plugins/web-tools/fetch-service.js +30 -45
  79. package/dist/plugins/web-tools/fetch-service.js.map +1 -1
  80. package/dist/plugins/web-tools/index.js +23 -5
  81. package/dist/plugins/web-tools/index.js.map +1 -1
  82. package/dist/plugins/web-tools/schemas.d.ts +469 -0
  83. package/dist/plugins/web-tools/schemas.js +523 -0
  84. package/dist/plugins/web-tools/schemas.js.map +1 -0
  85. package/dist/plugins/web-tools/search-service.d.ts +6 -3
  86. package/dist/plugins/web-tools/search-service.js +24 -29
  87. package/dist/plugins/web-tools/search-service.js.map +1 -1
  88. package/dist/plugins/workflow/authority.d.ts +6 -0
  89. package/dist/plugins/workflow/authority.js +45 -10
  90. package/dist/plugins/workflow/authority.js.map +1 -1
  91. package/dist/plugins/workflow/authorization.d.ts +30 -0
  92. package/dist/plugins/workflow/authorization.js +147 -0
  93. package/dist/plugins/workflow/authorization.js.map +1 -0
  94. package/dist/plugins/workflow/checkpoints.d.ts +41 -2
  95. package/dist/plugins/workflow/checkpoints.js +176 -47
  96. package/dist/plugins/workflow/checkpoints.js.map +1 -1
  97. package/dist/plugins/workflow/delegated.d.ts +15 -1
  98. package/dist/plugins/workflow/delegated.js +65 -35
  99. package/dist/plugins/workflow/delegated.js.map +1 -1
  100. package/dist/plugins/workflow/execution.d.ts +15 -30
  101. package/dist/plugins/workflow/execution.js +133 -33
  102. package/dist/plugins/workflow/execution.js.map +1 -1
  103. package/dist/plugins/workflow/index.js +197 -435
  104. package/dist/plugins/workflow/index.js.map +1 -1
  105. package/dist/plugins/workflow/input-validation.d.ts +48 -0
  106. package/dist/plugins/workflow/input-validation.js +536 -0
  107. package/dist/plugins/workflow/input-validation.js.map +1 -0
  108. package/dist/plugins/workflow/inspection.d.ts +60 -0
  109. package/dist/plugins/workflow/inspection.js +316 -0
  110. package/dist/plugins/workflow/inspection.js.map +1 -0
  111. package/dist/plugins/workflow/protocol.d.ts +2 -0
  112. package/dist/plugins/workflow/protocol.js +26 -4
  113. package/dist/plugins/workflow/protocol.js.map +1 -1
  114. package/dist/plugins/workflow/recovery.d.ts +18 -0
  115. package/dist/plugins/workflow/recovery.js +217 -0
  116. package/dist/plugins/workflow/recovery.js.map +1 -0
  117. package/dist/plugins/workflow/repair.d.ts +1 -1
  118. package/dist/plugins/workflow/repair.js +9 -10
  119. package/dist/plugins/workflow/repair.js.map +1 -1
  120. package/dist/plugins/workflow/results.d.ts +2566 -0
  121. package/dist/plugins/workflow/results.js +1111 -0
  122. package/dist/plugins/workflow/results.js.map +1 -0
  123. package/dist/plugins/workflow/schemas.d.ts +398 -0
  124. package/dist/plugins/workflow/schemas.js +399 -0
  125. package/dist/plugins/workflow/schemas.js.map +1 -0
  126. package/dist/plugins/workflow/state.d.ts +3 -2
  127. package/dist/plugins/workflow/state.js +15 -8
  128. package/dist/plugins/workflow/state.js.map +1 -1
  129. package/dist/plugins/workflow/system-instruction.md +17 -1
  130. package/dist/plugins/workflow/tooling.d.ts +11 -82
  131. package/dist/plugins/workflow/tooling.js +478 -461
  132. package/dist/plugins/workflow/tooling.js.map +1 -1
  133. package/dist/plugins/workflow/transactions.d.ts +13 -0
  134. package/dist/plugins/workflow/transactions.js +13 -3
  135. package/dist/plugins/workflow/transactions.js.map +1 -1
  136. package/package.json +6 -3
  137. package/schemas/vvoc/v3.json +1 -1
  138. package/templates/agents/vv-code-reviewer.md +13 -4
  139. package/templates/agents/vv-implementer.md +16 -9
  140. package/templates/agents/vv-spec-reviewer.md +13 -4
  141. package/templates/skills/vv-execute/SKILL.md +3 -3
  142. package/templates/skills/vv-execute/references/tool-contracts.md +1750 -0
  143. package/templates/skills/vv-review/SKILL.md +1 -1
@@ -1,1399 +1,31 @@
1
1
  // FILE: src/lib/opencode.ts
2
- // VERSION: 1.4.0
2
+ // VERSION: 1.5.0
3
3
  // START_MODULE_CONTRACT
4
- // PURPOSE: Manage OpenCode runtime/TUI config mutation, host compatibility diagnostics, provider patching, and scoped vvoc.json config files.
5
- // SCOPE: Layer-aware path resolution, pinned runtime and TUI package writes, OpenCode version inspection, managed OpenCode defaults, local skills path registration, provider patching, managed prompts/skills, strict vvoc rendering, and source-aware installation inspection including orchestration.
6
- // DEPENDS: [jsonc-parser, node:fs/promises, node:path, src/lib/config-layers.ts, src/lib/managed-agents.ts, src/lib/managed-skills.ts, src/lib/orchestration.ts, src/lib/package.ts, src/lib/vvoc-config.ts, src/lib/vvoc-paths.ts]
4
+ // PURPOSE: Public re-export barrel for OpenCode runtime/TUI config mutation, host compatibility diagnostics, provider patching, and scoped vvoc.json config files.
5
+ // SCOPE: Re-export the complete public API over the concern-scoped zone modules in src/lib/opencode/ (shared-utils, paths, plugin-registration, agent-registrations, model-overrides, vvoc-config-io, inspection) so every ../lib/opencode.js and ./opencode.js import keeps resolving unchanged under moduleResolution NodeNext, where directory imports are not available.
6
+ // DEPENDS: [src/lib/package.ts, src/lib/vvoc-config.ts, src/lib/opencode/shared-utils.ts, src/lib/opencode/paths.ts, src/lib/opencode/plugin-registration.ts, src/lib/opencode/agent-registrations.ts, src/lib/opencode/model-overrides.ts, src/lib/opencode/vvoc-config-io.ts, src/lib/opencode/inspection.ts]
7
7
  // LINKS: [M-CLI-CONFIG, M-ORCHESTRATION-PROFILES]
8
- // ROLE: RUNTIME
9
- // MAP_MODE: EXPORTS
8
+ // ROLE: BARREL
9
+ // MAP_MODE: SUMMARY
10
10
  // END_MODULE_CONTRACT
11
11
  //
12
12
  // START_MODULE_MAP
13
- // CLI_NAME - Canonical vvoc CLI binary name.
14
- // PACKAGE_NAME - Canonical vvoc npm package name.
15
- // OPENCODE_SCHEMA_URL - OpenCode config schema URL.
16
- // OPENCODE_TUI_SCHEMA_URL - OpenCode TUI config schema URL.
17
- // TUI_PACKAGE_SPECIFIER - Pinned base package specifier registered in tui.json(c) so OpenCode selects its ./tui export.
18
- // MINIMUM_TUI_OPENCODE_VERSION - Minimum OpenCode host version supported by the managed TUI plugin.
19
- // OpenCodeDefaultModelKey - Supported top-level OpenCode default model fields.
20
- // Scope - Supported installation scopes for vvoc config writes.
21
- // ResolvedPaths - Scope-aware path bundle for OpenCode runtime/TUI and vvoc config locations.
22
- // WriteResult - Result shape returned by managed config write operations.
23
- // TuiPluginEntry - Supported TUI plugin string or tuple entry.
24
- // OpenCodeRuntimeInspection - Installed OpenCode version and TUI compatibility snapshot.
25
- // InstallationInspection - Current OpenCode runtime/TUI and vvoc installation status snapshot.
26
- // resolvePaths - Resolves OpenCode runtime/TUI and vvoc config paths for global/project scopes.
27
- // ensurePackageConfigText - Ensures OpenCode config contains the pinned vvoc plugin specifier.
28
- // ensureTuiPackageConfigText - Ensures TUI config contains the pinned vvoc base package while preserving tuple options.
29
- // readOpenCodeDefaultModel - Reads a top-level OpenCode model or small_model override.
30
- // writeOpenCodeDefaultModel - Writes or removes a top-level OpenCode model or small_model override.
31
- // writeOpenCodeProviderObject - Writes or merges a provider.<id> object override.
32
- // ensureProviderBaseUrlConfigText - Ensures OpenCode config contains the requested provider options.baseURL override.
33
- // ensureManagedAgentRegistrationsConfigText - Ensures OpenCode config contains the vvoc-managed default agent, agent registrations, and tool gating.
34
- // readVvocConfig - Loads the canonical vvoc.json document when present.
35
- // ensurePackageInstalled - Writes the pinned vvoc plugin specifier into OpenCode config.
36
- // ensureTuiPackageInstalled - Writes the pinned vvoc base package into dedicated tui.json(c).
37
- // installVvocConfig - Creates or refreshes the canonical vvoc.json document.
38
- // syncVvocConfig - Rewrites the canonical vvoc.json document while preserving valid current values.
39
- // writeProviderBaseUrl - Writes a provider options.baseURL override into OpenCode config.
40
- // syncManagedAgentRegistrations - Syncs the canonical vvoc-managed OpenCode agent registrations and tool gating into OpenCode config.
41
- // installManagedAgentPrompts - Creates managed vvoc prompt files for the bundled Guardian and managed OpenCode agents when missing.
42
- // syncManagedAgentPrompts - Rewrites managed vvoc prompt files for the bundled Guardian and managed OpenCode agents.
43
- // installManagedSkillFiles - Creates managed vvoc skill files from bundled templates.
44
- // syncManagedSkillFiles - Rewrites managed vvoc skill files from bundled templates.
45
- // ensureManagedSkillSymlink - Creates symlink from OpenCode skills dir to vvoc skills dir for skill discovery.
46
- // readOpenCodeAgentModel - Reads a model override for any OpenCode agent from config.
47
- // readOpenCodeAgentOverride - Reads a model override for any OpenCode agent from config.
48
- // writeOpenCodeAgentModel - Writes or removes a model override for any OpenCode agent in config.
49
- // readManagedAgentModels - Reads model overrides for the bundled vvoc-managed OpenCode agents from OpenCode config.
50
- // readManagedAgentOverrides - Reads model overrides for the bundled vvoc-managed OpenCode agents.
51
- // writeManagedAgentModel - Writes or removes a bundled vvoc-managed OpenCode agent model override in OpenCode config.
52
- // writeGuardianConfig - Writes the guardian section into the canonical vvoc.json document.
53
- // inspectOpenCodeRuntime - Reads the installed OpenCode version and evaluates TUI compatibility.
54
- // extractOpenCodeVersion - Extracts the first semantic version found in `opencode --version` output.
55
- // isTuiOpenCodeVersionCompatible - Compares an OpenCode version with the managed TUI minimum.
56
- // inspectInstallation - Reads current OpenCode/vvoc installation state for status and doctor commands.
57
- // inspectInstallationForScope - Reads installation state using strict/effective layered source resolution.
58
- // describeWriteResult - Formats config write outcomes for CLI output.
59
- // ManagedAgentModelMap - Map of managed agent names to model selections.
60
- // OpenCodeAgentOverride - Agent override config for OpenCode.
61
- // ManagedAgentOverrideMap - Map of agent override configs.
62
- // parseGuardianConfigText - Parse guardian section JSON.
63
- // renderGuardianConfig - Render guardian section JSON.
64
- // GuardianConfigOverrides - Guardian config override type.
13
+ // resolvePaths, ensurePackageConfigText, ensureTuiPackageConfigText, ensureTuiPackageInstalled, ensureManagedAgentRegistrationsConfigText, syncManagedAgentRegistrations, installManagedAgentPrompts, syncManagedAgentPrompts, installManagedSkillFiles, syncManagedSkillFiles, ensureManagedSkillSymlink, readManagedAgentModels, readManagedAgentOverrides, readOpenCodeAgentModel, readOpenCodeAgentOverride, readOpenCodeDefaultModel, writeOpenCodeAgentModel, writeOpenCodeDefaultModel, writeOpenCodeProviderObject, writeManagedAgentModel, ensurePackageInstalled, ensureProviderBaseUrlConfigText, writeProviderBaseUrl, readVvocConfig, installVvocConfig, syncVvocConfig, writeGuardianConfig, inspectOpenCodeRuntime, isTuiOpenCodeVersionCompatible, extractOpenCodeVersion, inspectInstallation, inspectInstallationForScope, describeWriteResult - Public functions of the former monolith, now owned by zone modules.
14
+ // CLI_NAME, PACKAGE_NAME, OPENCODE_SCHEMA_URL, OPENCODE_TUI_SCHEMA_URL, TUI_PACKAGE_SPECIFIER, MINIMUM_TUI_OPENCODE_VERSION - Public constants owned by zone modules and package.ts.
15
+ // Scope, ResolvedPaths, WriteResult, TuiPluginEntry, OpenCodeDefaultModelKey, ManagedAgentModelMap, OpenCodeAgentOverride, ManagedAgentOverrideMap, OpenCodeRuntimeInspection, InstallationInspection, GuardianConfigOverrides - Public types owned by zone modules and vvoc-config.ts.
65
16
  // END_MODULE_MAP
66
17
  //
67
18
  // START_CHANGE_SUMMARY
68
- // LAST_CHANGE: [C-SPEC-IDENTITY-LINT - Materialized the conservative spec-guard plugin entry during install, init, and sync writes.]
19
+ // LAST_CHANGE: [C-OPENCODE-MODULE-SPLIT-R1 - Replaced the 2045-line monolith body with a re-export-only barrel over concern-scoped zone modules in src/lib/opencode/; the public API and every consumer import are unchanged.]
69
20
  // END_CHANGE_SUMMARY
70
- import { applyEdits, format, modify, parse } from "jsonc-parser";
71
- import { lstat, mkdir, readFile, readlink, symlink, unlink, writeFile } from "node:fs/promises";
72
- import { dirname, join, relative, resolve } from "node:path";
73
- import { MANAGED_AGENT_PROMPT_NAMES, MANAGED_OPENCODE_AGENTS, getManagedAgentPromptPath, getManagedOpenCodeAgentDefinition, loadManagedAgentPromptTemplate, } from "./managed-agents.js";
74
- import { MANAGED_SKILL_NAMES, getManagedSkillFilePath, listManagedSkillReferenceNames, loadManagedSkillReference, loadManagedSkillTemplate, } from "./managed-skills.js";
75
- import { BUILTIN_ROLE_NAMES, getBuiltInRoleBindings, ROLE_REFERENCE_PREFIX, } from "./model-roles.js";
76
- import { resolveOrchestrationPolicy } from "./orchestration.js";
77
- import { createDefaultVvocConfig, createGuardianConfig, parseVvocConfigText, parseVersionedVvocConfigText, renderVvocConfig, } from "./vvoc-config.js";
78
- import { getPinnedPackageSpecifier, PACKAGE_NAME, PACKAGE_VERSION } from "./package.js";
79
- import { resolveConfigWriteTargets, resolveOpenCodeConfigSource, resolveOpenCodeTuiConfigSource, resolveVvocConfigSource, } from "./config-layers.js";
80
- import { materializeHashlineEditEntry, materializeToolHistoryCompactionEntry, materializePeakHoursEntry, materializeSpecGuardEntry, } from "./plugin-toggle-config.js";
81
- import { getConfigHome, getGlobalOpencodeSkillsDir, getGlobalVvocDir, getVvocAgentsDir, getVvocSkillsDir, } from "./vvoc-paths.js";
82
- export const CLI_NAME = "vvoc";
83
- export { PACKAGE_NAME };
84
- export const OPENCODE_SCHEMA_URL = "https://opencode.ai/config.json";
85
- export const OPENCODE_TUI_SCHEMA_URL = "https://opencode.ai/tui.json";
86
- export const TUI_PACKAGE_SPECIFIER = `${PACKAGE_NAME}@${PACKAGE_VERSION}`;
87
- export const MINIMUM_TUI_OPENCODE_VERSION = "1.18.2";
88
- const MANAGED_MARKER = "Managed by vvoc";
89
- const OPENCODE_CONFIG_FILE_NAMES = ["opencode.json", "opencode.jsonc"];
90
- const OPENCODE_TUI_CONFIG_FILE_NAMES = ["tui.json", "tui.jsonc"];
91
- const MANAGED_DEFAULT_AGENT = "vv-controller";
92
- const JSON_FORMAT = {
93
- insertSpaces: true,
94
- tabSize: 2,
95
- eol: "\n",
96
- };
97
- // START_BLOCK_RESOLVE_CONFIG_PATHS
98
- export async function resolvePaths(options) {
99
- const targets = await resolveConfigWriteTargets(options);
100
- const configHome = getConfigHome(options.configDir);
101
- const managedAgentsDirPath = getVvocAgentsDir(targets.vvocBaseDir);
102
- const managedSkillsDirPath = getVvocSkillsDir(targets.vvocBaseDir);
103
- return {
104
- scope: options.scope,
105
- cwd: options.cwd,
106
- configHome,
107
- projectRoot: targets.projectRoot,
108
- opencodeBaseDir: targets.opencodeBaseDir,
109
- vvocBaseDir: targets.vvocBaseDir,
110
- vvocConfigPath: targets.vvocConfigPath,
111
- managedAgentsDirPath,
112
- managedSkillsDirPath,
113
- opencodeConfigPath: targets.opencodeConfigPath,
114
- opencodeAlternatePaths: await resolveOpenCodeAlternates(targets.opencodeBaseDir, targets.opencodeConfigPath),
115
- opencodeTuiConfigPath: targets.opencodeTuiConfigPath,
116
- opencodeTuiAlternatePaths: await resolveConfigAlternates(targets.opencodeBaseDir, targets.opencodeTuiConfigPath, OPENCODE_TUI_CONFIG_FILE_NAMES),
117
- };
118
- }
119
- // END_BLOCK_RESOLVE_CONFIG_PATHS
120
- // START_BLOCK_ENSURE_OPENCODE_PLUGIN_CONFIG
121
- export function ensurePackageConfigText(text, packageSpecifier = PACKAGE_NAME) {
122
- if (!text?.trim()) {
123
- return renderJson({
124
- $schema: OPENCODE_SCHEMA_URL,
125
- plugin: [packageSpecifier],
126
- });
127
- }
128
- const document = parseObjectDocument(text, "OpenCode config");
129
- const currentPlugins = readPluginList(document, "OpenCode config");
130
- let nextText = text;
131
- if (!Object.hasOwn(document, "$schema")) {
132
- nextText = applyEdits(nextText, modify(nextText, ["$schema"], OPENCODE_SCHEMA_URL, {
133
- formattingOptions: JSON_FORMAT,
134
- getInsertionIndex: () => 0,
135
- }));
136
- }
137
- const nextPlugins = normalizePluginList(currentPlugins, packageSpecifier);
138
- if (JSON.stringify(nextPlugins) !== JSON.stringify(currentPlugins)) {
139
- nextText = applyEdits(nextText, modify(nextText, ["plugin"], nextPlugins, {
140
- formattingOptions: JSON_FORMAT,
141
- }));
142
- }
143
- return ensureTrailingNewline(applyEdits(nextText, format(nextText, undefined, JSON_FORMAT)));
144
- }
145
- // END_BLOCK_ENSURE_OPENCODE_PLUGIN_CONFIG
146
- // START_BLOCK_ENSURE_TUI_PLUGIN_CONFIG
147
- export function ensureTuiPackageConfigText(text, packageSpecifier = TUI_PACKAGE_SPECIFIER) {
148
- if (!text?.trim()) {
149
- return renderJson({
150
- $schema: OPENCODE_TUI_SCHEMA_URL,
151
- plugin: [packageSpecifier],
152
- });
153
- }
154
- const document = parseObjectDocument(text, "OpenCode TUI config");
155
- const currentPlugins = readTuiPluginList(document, "OpenCode TUI config");
156
- let nextText = text;
157
- if (!Object.hasOwn(document, "$schema")) {
158
- nextText = applyEdits(nextText, modify(nextText, ["$schema"], OPENCODE_TUI_SCHEMA_URL, {
159
- formattingOptions: JSON_FORMAT,
160
- getInsertionIndex: () => 0,
161
- }));
162
- }
163
- const nextPlugins = normalizeTuiPluginList(currentPlugins, packageSpecifier);
164
- if (JSON.stringify(nextPlugins) !== JSON.stringify(currentPlugins)) {
165
- nextText = applyEdits(nextText, modify(nextText, ["plugin"], nextPlugins, {
166
- formattingOptions: JSON_FORMAT,
167
- }));
168
- }
169
- return ensureTrailingNewline(applyEdits(nextText, format(nextText, undefined, JSON_FORMAT)));
170
- }
171
- export async function ensureTuiPackageInstalled(paths) {
172
- const currentText = await readOptionalText(paths.opencodeTuiConfigPath);
173
- const nextText = ensureTuiPackageConfigText(currentText);
174
- if ((currentText ?? "") === nextText) {
175
- return { action: "kept", path: paths.opencodeTuiConfigPath };
176
- }
177
- await writeText(paths.opencodeTuiConfigPath, nextText);
178
- return {
179
- action: currentText ? "updated" : "created",
180
- path: paths.opencodeTuiConfigPath,
181
- };
182
- }
183
- // END_BLOCK_ENSURE_TUI_PLUGIN_CONFIG
184
- // START_BLOCK_ENSURE_MANAGED_AGENT_CONFIG
185
- export function ensureManagedAgentRegistrationsConfigText(text, paths) {
186
- const builtInRoleBindings = getBuiltInRoleBindings();
187
- const rootRoleRefs = {
188
- model: createRoleReference(builtInRoleBindings.opencodeDefaults.model),
189
- small_model: createRoleReference(builtInRoleBindings.opencodeDefaults.smallModel),
190
- };
191
- const builtInAgentModelRefs = {
192
- explore: createRoleReference(builtInRoleBindings.opencodeAgents.explore),
193
- };
194
- if (!text?.trim()) {
195
- const managedRegistrations = Object.fromEntries(MANAGED_OPENCODE_AGENTS.map((definition) => [
196
- definition.name,
197
- getManagedOpenCodeAgentRegistration(paths, definition.name),
198
- ]));
199
- return renderJson({
200
- $schema: OPENCODE_SCHEMA_URL,
201
- model: rootRoleRefs.model,
202
- small_model: rootRoleRefs.small_model,
203
- default_agent: MANAGED_DEFAULT_AGENT,
204
- tools: {
205
- apply_patch: false,
206
- },
207
- agent: {
208
- ...Object.fromEntries(Object.entries(builtInAgentModelRefs).map(([name, model]) => [name, { model }])),
209
- ...managedRegistrations,
210
- },
211
- skills: {
212
- paths: [getManagedSkillsPathReference(paths)],
213
- },
214
- command: {},
215
- });
216
- }
217
- const document = parseObjectDocument(text, "OpenCode config");
218
- const currentAgents = readAgentMap(document, "OpenCode config");
219
- const currentTools = readOptionalObject(document, "tools", "OpenCode config");
220
- let nextText = text;
221
- if (!Object.hasOwn(document, "$schema")) {
222
- nextText = applyEdits(nextText, modify(nextText, ["$schema"], OPENCODE_SCHEMA_URL, {
223
- formattingOptions: JSON_FORMAT,
224
- getInsertionIndex: () => 0,
225
- }));
226
- }
227
- if (document.model !== rootRoleRefs.model) {
228
- nextText = applyEdits(nextText, modify(nextText, ["model"], rootRoleRefs.model, {
229
- formattingOptions: JSON_FORMAT,
230
- }));
231
- }
232
- if (document.small_model !== rootRoleRefs.small_model) {
233
- nextText = applyEdits(nextText, modify(nextText, ["small_model"], rootRoleRefs.small_model, {
234
- formattingOptions: JSON_FORMAT,
235
- }));
236
- }
237
- if (document.default_agent !== MANAGED_DEFAULT_AGENT) {
238
- nextText = applyEdits(nextText, modify(nextText, ["default_agent"], MANAGED_DEFAULT_AGENT, {
239
- formattingOptions: JSON_FORMAT,
240
- }));
241
- }
242
- if (currentTools?.apply_patch !== false) {
243
- nextText = applyEdits(nextText, modify(nextText, ["tools", "apply_patch"], false, {
244
- formattingOptions: JSON_FORMAT,
245
- }));
246
- }
247
- for (const [agentName, modelRef] of Object.entries(builtInAgentModelRefs)) {
248
- const currentEntry = currentAgents[agentName];
249
- if (!currentEntry) {
250
- nextText = applyEdits(nextText, modify(nextText, ["agent", agentName], { model: modelRef }, {
251
- formattingOptions: JSON_FORMAT,
252
- }));
253
- continue;
254
- }
255
- if (currentEntry.model !== modelRef) {
256
- nextText = applyEdits(nextText, modify(nextText, ["agent", agentName, "model"], modelRef, {
257
- formattingOptions: JSON_FORMAT,
258
- }));
259
- }
260
- }
261
- for (const definition of MANAGED_OPENCODE_AGENTS) {
262
- const currentEntry = currentAgents[definition.name];
263
- const registration = getManagedOpenCodeAgentRegistration(paths, definition.name);
264
- if (!currentEntry) {
265
- nextText = applyEdits(nextText, modify(nextText, ["agent", definition.name], registration, {
266
- formattingOptions: JSON_FORMAT,
267
- }));
268
- continue;
269
- }
270
- for (const [field, nextValue] of Object.entries(registration)) {
271
- if (JSON.stringify(currentEntry[field]) === JSON.stringify(nextValue)) {
272
- continue;
273
- }
274
- nextText = applyEdits(nextText, modify(nextText, ["agent", definition.name, field], nextValue, {
275
- formattingOptions: JSON_FORMAT,
276
- }));
277
- }
278
- }
279
- return ensureManagedSkillsPathConfigText(ensureTrailingNewline(applyEdits(nextText, format(nextText, undefined, JSON_FORMAT))), paths);
280
- }
281
- export async function syncManagedAgentRegistrations(paths) {
282
- const currentText = await readOptionalText(paths.opencodeConfigPath);
283
- const nextText = ensureManagedAgentRegistrationsConfigText(currentText, paths);
284
- if (currentText === nextText) {
285
- return { path: paths.opencodeConfigPath, changed: false };
286
- }
287
- await writeText(paths.opencodeConfigPath, nextText);
288
- return { path: paths.opencodeConfigPath, changed: true };
289
- }
290
- // END_BLOCK_ENSURE_MANAGED_AGENT_CONFIG
291
- export async function installManagedAgentPrompts(paths, options) {
292
- const results = [];
293
- for (const agentName of MANAGED_AGENT_PROMPT_NAMES) {
294
- const promptPath = getManagedPromptPath(paths, agentName);
295
- const currentText = await readOptionalText(promptPath);
296
- if (!currentText) {
297
- await writeText(promptPath, await renderManagedPrompt(agentName));
298
- results.push({ action: "created", path: promptPath });
299
- continue;
300
- }
301
- if (!options.force) {
302
- if (!isManagedFile(currentText)) {
303
- results.push({
304
- action: "skipped",
305
- path: promptPath,
306
- reason: "existing file is not managed by vvoc",
307
- });
308
- }
309
- else {
310
- results.push({ action: "kept", path: promptPath });
311
- }
312
- continue;
313
- }
314
- results.push(await syncManagedPrompt(paths, agentName, options));
315
- }
316
- return results;
317
- }
318
- export async function syncManagedAgentPrompts(paths, options) {
319
- const results = [];
320
- for (const agentName of MANAGED_AGENT_PROMPT_NAMES) {
321
- results.push(await syncManagedPrompt(paths, agentName, options));
322
- }
323
- return results;
324
- }
325
- // START_BLOCK_MANAGED_SKILL_FUNCTIONS
326
- export async function installManagedSkillFiles(paths, options) {
327
- const results = [];
328
- for (const skillName of MANAGED_SKILL_NAMES) {
329
- const skillPath = getManagedSkillFilePath(paths.managedSkillsDirPath, skillName);
330
- const currentText = await readOptionalText(skillPath);
331
- if (!currentText) {
332
- await writeText(skillPath, await renderManagedSkill(skillName));
333
- results.push({ action: "created", path: skillPath });
334
- }
335
- else if (!options.force) {
336
- if (!hasYamlFrontmatter(currentText)) {
337
- results.push({
338
- action: "skipped",
339
- path: skillPath,
340
- reason: "existing file has no YAML frontmatter — might not be a skill",
341
- });
342
- }
343
- else {
344
- results.push({ action: "kept", path: skillPath });
345
- }
346
- continue;
347
- }
348
- else {
349
- results.push(await syncManagedSkill(paths, skillName, options));
350
- }
351
- const refResults = await syncManagedSkillReferences(paths.managedSkillsDirPath, skillName);
352
- results.push(...refResults);
353
- }
354
- return results;
355
- }
356
- export async function syncManagedSkillFiles(paths, options) {
357
- const results = [];
358
- for (const skillName of MANAGED_SKILL_NAMES) {
359
- const skillResult = await syncManagedSkill(paths, skillName, options);
360
- results.push(skillResult);
361
- // Only sync references when the parent skill was not skipped (user-owned/custom)
362
- if (skillResult.action !== "skipped") {
363
- const refResults = await syncManagedSkillReferences(paths.managedSkillsDirPath, skillName);
364
- results.push(...refResults);
365
- }
366
- }
367
- return results;
368
- }
369
- // END_BLOCK_MANAGED_SKILL_FUNCTIONS
370
- // START_BLOCK_MANAGED_SKILL_SYMLINK
371
- export async function ensureManagedSkillSymlink(configDir) {
372
- const globalSkillsDir = getVvocSkillsDir(getGlobalVvocDir(configDir));
373
- const opencodeSkillsDir = getGlobalOpencodeSkillsDir(configDir);
374
- const symlinkPath = join(opencodeSkillsDir, "vvoc");
375
- // Create the OpenCode skills parent directory
376
- await mkdir(opencodeSkillsDir, { recursive: true });
377
- // Never destroy a non-symlink at this path: a regular file or directory here
378
- // is user-owned content, not a vvoc-managed link, and must not be clobbered.
379
- let wasStale = false;
380
- try {
381
- const stats = await lstat(symlinkPath);
382
- if (!stats.isSymbolicLink()) {
383
- return { action: "skipped", path: symlinkPath };
384
- }
385
- if (resolve(await readlink(symlinkPath)) === resolve(globalSkillsDir)) {
386
- // Already the current vvoc-managed link — nothing to do.
387
- return { action: "kept", path: symlinkPath };
388
- }
389
- // A stale or foreign symlink: replace it with the current vvoc link.
390
- await unlink(symlinkPath);
391
- wasStale = true;
392
- }
393
- catch {
394
- // Path does not exist — will be created fresh.
395
- }
396
- // Create symlink: opencode/skills/vvoc -> vvoc/skills
397
- await symlink(globalSkillsDir, symlinkPath);
398
- return { action: wasStale ? "updated" : "created", path: symlinkPath };
399
- }
400
- // END_BLOCK_MANAGED_SKILL_SYMLINK
401
- export async function readManagedAgentModels(paths) {
402
- const overrides = await readManagedAgentOverrides(paths);
403
- return Object.fromEntries(Object.entries(overrides).map(([name, entry]) => [name, entry.model]));
404
- }
405
- export async function readManagedAgentOverrides(paths) {
406
- const overrides = Object.fromEntries(MANAGED_OPENCODE_AGENTS.map((definition) => [definition.name, {}]));
407
- const currentText = await readOptionalText(paths.opencodeConfigPath);
408
- if (!currentText) {
409
- return overrides;
410
- }
411
- const document = parseObjectDocument(currentText, paths.opencodeConfigPath);
412
- const agentMap = readAgentMap(document, paths.opencodeConfigPath);
413
- for (const definition of MANAGED_OPENCODE_AGENTS) {
414
- overrides[definition.name] = readAgentOverride(agentMap[definition.name], definition.name);
415
- }
416
- return overrides;
417
- }
418
- export async function readOpenCodeAgentModel(paths, agentName) {
419
- return (await readOpenCodeAgentOverride(paths, agentName)).model;
420
- }
421
- export async function readOpenCodeAgentOverride(paths, agentName) {
422
- const currentText = await readOptionalText(paths.opencodeConfigPath);
423
- if (!currentText) {
424
- return {};
425
- }
426
- const document = parseObjectDocument(currentText, paths.opencodeConfigPath);
427
- const agentMap = readAgentMap(document, paths.opencodeConfigPath);
428
- return readAgentOverride(agentMap[agentName], `${paths.opencodeConfigPath}: agent.${agentName}`);
429
- }
430
- export async function readOpenCodeDefaultModel(paths, key) {
431
- const currentText = await readOptionalText(paths.opencodeConfigPath);
432
- if (!currentText) {
433
- return undefined;
434
- }
435
- const document = parseObjectDocument(currentText, paths.opencodeConfigPath);
436
- const value = document[key];
437
- if (value === undefined) {
438
- return undefined;
439
- }
440
- return readNonEmptyString(value, `${paths.opencodeConfigPath}: ${key}`);
441
- }
442
- export async function writeOpenCodeAgentModel(paths, agentName, options) {
443
- const currentText = await readOptionalText(paths.opencodeConfigPath);
444
- if (!currentText && !options.ensureEntry) {
445
- return { action: "kept", path: paths.opencodeConfigPath };
446
- }
447
- const baseText = options.ensureEntry ? ensureAgentConfigText(currentText) : currentText;
448
- if (!baseText) {
449
- return { action: "kept", path: paths.opencodeConfigPath };
450
- }
451
- const document = parseObjectDocument(baseText, paths.opencodeConfigPath);
452
- const agentMap = readAgentMap(document, paths.opencodeConfigPath);
453
- const currentEntry = agentMap[agentName];
454
- if (!currentEntry && !options.ensureEntry) {
455
- return { action: "kept", path: paths.opencodeConfigPath };
456
- }
457
- const nextEntry = currentEntry ? { ...currentEntry } : {};
458
- if (options.model) {
459
- nextEntry.model = options.model;
460
- }
461
- else {
462
- delete nextEntry.model;
463
- }
464
- const nextText = updateAgentEntryText(baseText, agentName, nextEntry);
465
- if ((currentText ?? "") === nextText) {
466
- return { action: "kept", path: paths.opencodeConfigPath };
467
- }
468
- await writeText(paths.opencodeConfigPath, nextText);
469
- return {
470
- action: currentText ? "updated" : "created",
471
- path: paths.opencodeConfigPath,
472
- };
473
- }
474
- export async function writeOpenCodeDefaultModel(paths, key, options) {
475
- const currentText = await readOptionalText(paths.opencodeConfigPath);
476
- if (!currentText && !options.ensureEntry) {
477
- return { action: "kept", path: paths.opencodeConfigPath };
478
- }
479
- const baseText = options.ensureEntry ? ensureOpenCodeConfigText(currentText) : currentText;
480
- if (!baseText) {
481
- return { action: "kept", path: paths.opencodeConfigPath };
482
- }
483
- const nextText = updateTopLevelStringFieldText(baseText, key, options.model);
484
- if ((currentText ?? "") === nextText) {
485
- return { action: "kept", path: paths.opencodeConfigPath };
486
- }
487
- await writeText(paths.opencodeConfigPath, nextText);
488
- return {
489
- action: currentText ? "updated" : "created",
490
- path: paths.opencodeConfigPath,
491
- };
492
- }
493
- export async function writeOpenCodeProviderObject(paths, providerID, value) {
494
- const currentText = await readOptionalText(paths.opencodeConfigPath);
495
- const nextText = ensureProviderObjectConfigText(currentText, providerID, value);
496
- if ((currentText ?? "") === nextText) {
497
- return { action: "kept", path: paths.opencodeConfigPath };
498
- }
499
- await writeText(paths.opencodeConfigPath, nextText);
500
- return {
501
- action: currentText ? "updated" : "created",
502
- path: paths.opencodeConfigPath,
503
- };
504
- }
505
- // START_BLOCK_MANAGED_AGENT_MODEL_IO
506
- export async function writeManagedAgentModel(paths, agentName, options) {
507
- const currentText = await readOptionalText(paths.opencodeConfigPath);
508
- if (!currentText && !options.ensureEntry) {
509
- return { action: "kept", path: paths.opencodeConfigPath };
510
- }
511
- const baseText = options.ensureEntry
512
- ? ensureManagedAgentRegistrationsConfigText(currentText, paths)
513
- : currentText;
514
- if (!baseText) {
515
- return { action: "kept", path: paths.opencodeConfigPath };
516
- }
517
- const document = parseObjectDocument(baseText, paths.opencodeConfigPath);
518
- const agentMap = readAgentMap(document, paths.opencodeConfigPath);
519
- const currentEntry = agentMap[agentName];
520
- if (!currentEntry && !options.ensureEntry) {
521
- return { action: "kept", path: paths.opencodeConfigPath };
522
- }
523
- const nextEntry = {
524
- ...getManagedOpenCodeAgentRegistration(paths, agentName),
525
- ...currentEntry,
526
- };
527
- if (options.model) {
528
- nextEntry.model = options.model;
529
- }
530
- else {
531
- delete nextEntry.model;
532
- }
533
- const nextText = updateAgentEntryText(baseText, agentName, nextEntry);
534
- if ((currentText ?? "") === nextText) {
535
- return { action: "kept", path: paths.opencodeConfigPath };
536
- }
537
- await writeText(paths.opencodeConfigPath, nextText);
538
- return {
539
- action: currentText ? "updated" : "created",
540
- path: paths.opencodeConfigPath,
541
- };
542
- }
543
- // END_BLOCK_MANAGED_AGENT_MODEL_IO
21
+ export { PACKAGE_NAME } from "./package.js";
544
22
  export { parseGuardianConfigText, renderGuardianConfig, } from "./vvoc-config.js";
545
- // START_BLOCK_INSTALL_PACKAGE_AND_GUARDIAN_CONFIG
546
- export async function ensurePackageInstalled(paths) {
547
- const currentText = await readOptionalText(paths.opencodeConfigPath);
548
- const nextText = ensurePackageConfigText(currentText, await getPinnedPackageSpecifier());
549
- if (currentText === nextText) {
550
- return { path: paths.opencodeConfigPath, changed: false };
551
- }
552
- await writeText(paths.opencodeConfigPath, nextText);
553
- return { path: paths.opencodeConfigPath, changed: true };
554
- }
555
- // END_BLOCK_INSTALL_PACKAGE_AND_GUARDIAN_CONFIG
556
- // START_BLOCK_ENSURE_PROVIDER_BASE_URL_CONFIG
557
- export function ensureProviderBaseUrlConfigText(text, providerID, baseURL) {
558
- if (!text?.trim()) {
559
- return renderJson({
560
- $schema: OPENCODE_SCHEMA_URL,
561
- provider: {
562
- [providerID]: {
563
- options: {
564
- baseURL,
565
- },
566
- },
567
- },
568
- });
569
- }
570
- const document = parseObjectDocument(text, "OpenCode config");
571
- const currentProviders = readProviderMap(document, "OpenCode config");
572
- const currentProvider = currentProviders[providerID];
573
- const currentOptions = currentProvider
574
- ? readOptionalObject(currentProvider, "options", `OpenCode config: provider.${providerID}`)
575
- : undefined;
576
- let nextText = text;
577
- if (!Object.hasOwn(document, "$schema")) {
578
- nextText = applyEdits(nextText, modify(nextText, ["$schema"], OPENCODE_SCHEMA_URL, {
579
- formattingOptions: JSON_FORMAT,
580
- getInsertionIndex: () => 0,
581
- }));
582
- }
583
- if (currentOptions?.baseURL !== baseURL) {
584
- nextText = applyEdits(nextText, modify(nextText, ["provider", providerID, "options", "baseURL"], baseURL, {
585
- formattingOptions: JSON_FORMAT,
586
- }));
587
- }
588
- return ensureTrailingNewline(applyEdits(nextText, format(nextText, undefined, JSON_FORMAT)));
589
- }
590
- export async function writeProviderBaseUrl(paths, providerID, baseURL) {
591
- const currentText = await readOptionalText(paths.opencodeConfigPath);
592
- const nextText = ensureProviderBaseUrlConfigText(currentText, providerID, baseURL);
593
- if ((currentText ?? "") === nextText) {
594
- return { action: "kept", path: paths.opencodeConfigPath };
595
- }
596
- await writeText(paths.opencodeConfigPath, nextText);
597
- return {
598
- action: currentText ? "updated" : "created",
599
- path: paths.opencodeConfigPath,
600
- };
601
- }
602
- // END_BLOCK_ENSURE_PROVIDER_BASE_URL_CONFIG
603
- // START_BLOCK_VVOC_CONFIG_IO
604
- export async function readVvocConfig(paths) {
605
- const currentText = await readOptionalText(paths.vvocConfigPath);
606
- return currentText ? parseVvocConfigText(currentText, paths.vvocConfigPath) : undefined;
607
- }
608
- export async function installVvocConfig(paths) {
609
- return syncVvocConfig(paths);
610
- }
611
- export async function syncVvocConfig(paths) {
612
- const currentText = await readOptionalText(paths.vvocConfigPath);
613
- const nextConfig = currentText
614
- ? parseVvocConfigText(currentText, paths.vvocConfigPath)
615
- : createDefaultVvocConfig();
616
- nextConfig.plugins["hashline-edit"] = materializeHashlineEditEntry(nextConfig.plugins["hashline-edit"]);
617
- nextConfig.plugins["tool-history-compaction"] = materializeToolHistoryCompactionEntry(nextConfig.plugins["tool-history-compaction"]);
618
- nextConfig.plugins["peak-hours"] = materializePeakHoursEntry(nextConfig.plugins["peak-hours"]);
619
- nextConfig.plugins["spec-guard"] = materializeSpecGuardEntry(nextConfig.plugins["spec-guard"]);
620
- return writeResolvedVvocConfig(paths.vvocConfigPath, currentText, nextConfig);
621
- }
622
- export async function writeGuardianConfig(paths, overrides, options = {}) {
623
- const currentText = await readOptionalText(paths.vvocConfigPath);
624
- const currentConfig = currentText
625
- ? parseVvocConfigText(currentText, paths.vvocConfigPath)
626
- : createDefaultVvocConfig();
627
- const nextConfig = {
628
- ...currentConfig,
629
- guardian: options.merge
630
- ? createGuardianConfig({ ...currentConfig.guardian, ...overrides })
631
- : createGuardianConfig(overrides),
632
- };
633
- return writeResolvedVvocConfig(paths.vvocConfigPath, currentText, nextConfig);
634
- }
635
- // END_BLOCK_VVOC_CONFIG_IO
636
- // START_BLOCK_INSPECT_OPENCODE_RUNTIME
637
- export async function inspectOpenCodeRuntime(run = runOpenCodeVersionCommand) {
638
- try {
639
- const result = await run();
640
- if (result.exitCode !== 0) {
641
- const detail = result.stderr.trim() || result.stdout.trim() || `exit code ${result.exitCode}`;
642
- return {
643
- minimumTuiVersion: MINIMUM_TUI_OPENCODE_VERSION,
644
- error: `opencode --version failed: ${detail}`,
645
- };
646
- }
647
- const version = extractOpenCodeVersion(`${result.stdout}\n${result.stderr}`);
648
- if (!version) {
649
- return {
650
- minimumTuiVersion: MINIMUM_TUI_OPENCODE_VERSION,
651
- error: "opencode --version did not return a semantic version",
652
- };
653
- }
654
- return {
655
- version,
656
- minimumTuiVersion: MINIMUM_TUI_OPENCODE_VERSION,
657
- tuiCompatible: isTuiOpenCodeVersionCompatible(version),
658
- };
659
- }
660
- catch (error) {
661
- return {
662
- minimumTuiVersion: MINIMUM_TUI_OPENCODE_VERSION,
663
- error: error instanceof Error ? error.message : String(error),
664
- };
665
- }
666
- }
667
- export function isTuiOpenCodeVersionCompatible(version) {
668
- const current = parseSemanticVersion(version);
669
- const minimum = parseSemanticVersion(MINIMUM_TUI_OPENCODE_VERSION);
670
- if (!current || !minimum)
671
- return false;
672
- for (let index = 0; index < 3; index += 1) {
673
- const currentPart = current.parts[index] ?? 0;
674
- const minimumPart = minimum.parts[index] ?? 0;
675
- if (currentPart > minimumPart)
676
- return true;
677
- if (currentPart < minimumPart)
678
- return false;
679
- }
680
- return !current.prerelease || Boolean(minimum.prerelease);
681
- }
682
- async function runOpenCodeVersionCommand() {
683
- const subprocess = Bun.spawn({
684
- cmd: ["opencode", "--version"],
685
- stdout: "pipe",
686
- stderr: "pipe",
687
- });
688
- const [stdout, stderr, exitCode] = await Promise.all([
689
- new Response(subprocess.stdout).text(),
690
- new Response(subprocess.stderr).text(),
691
- subprocess.exited,
692
- ]);
693
- return { exitCode, stdout, stderr };
694
- }
695
- /** Extracts the first semantic version found in `opencode --version` output. */
696
- export function extractOpenCodeVersion(output) {
697
- const match = output.match(/(?:^|\s)v?(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?)(?=\s|$)/);
698
- return match?.[1];
699
- }
700
- function parseSemanticVersion(value) {
701
- const match = value
702
- .trim()
703
- .match(/^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/);
704
- if (!match)
705
- return undefined;
706
- return {
707
- parts: [Number(match[1]), Number(match[2]), Number(match[3])],
708
- prerelease: match[4],
709
- };
710
- }
711
- // END_BLOCK_INSPECT_OPENCODE_RUNTIME
712
- // START_BLOCK_INSPECT_INSTALLATION_STATE
713
- export async function inspectInstallation(paths, options = {}) {
714
- const warnings = [];
715
- const problems = [];
716
- const runtime = options.runtime ?? {
717
- minimumTuiVersion: MINIMUM_TUI_OPENCODE_VERSION,
718
- };
719
- if (runtime.error) {
720
- problems.push(`OpenCode version unavailable: ${runtime.error}`);
721
- }
722
- else if (runtime.tuiCompatible === false) {
723
- problems.push(`OpenCode ${runtime.version ?? "unknown"} is incompatible with /context; ${runtime.minimumTuiVersion} or newer is required`);
724
- }
725
- if (paths.opencodeAlternatePaths.length > 0) {
726
- warnings.push(`multiple OpenCode config files exist: ${[paths.opencodeConfigPath, ...paths.opencodeAlternatePaths].join(", ")}`);
727
- }
728
- if (paths.opencodeTuiAlternatePaths.length > 0) {
729
- warnings.push(`multiple OpenCode TUI config files exist: ${[paths.opencodeTuiConfigPath, ...paths.opencodeTuiAlternatePaths].join(", ")}`);
730
- }
731
- const opencodeText = await readOptionalText(paths.opencodeConfigPath);
732
- let opencodeParseError;
733
- let plugins = [];
734
- let pluginConfigured = false;
735
- if (opencodeText) {
736
- try {
737
- const document = parseObjectDocument(opencodeText, paths.opencodeConfigPath);
738
- plugins = readPluginList(document, paths.opencodeConfigPath);
739
- pluginConfigured = plugins.some(isPackagePluginSpecifier);
740
- }
741
- catch (error) {
742
- opencodeParseError = error instanceof Error ? error.message : String(error);
743
- problems.push(opencodeParseError);
744
- }
745
- }
746
- const tuiText = await readOptionalText(paths.opencodeTuiConfigPath);
747
- let tuiParseError;
748
- let tuiPlugins = [];
749
- let tuiPluginConfigured = false;
750
- if (tuiText) {
751
- try {
752
- const document = parseObjectDocument(tuiText, paths.opencodeTuiConfigPath);
753
- tuiPlugins = readTuiPluginList(document, paths.opencodeTuiConfigPath);
754
- tuiPluginConfigured = tuiPlugins.some((entry) => isTuiPackageSpecifier(readTuiPluginName(entry)));
755
- }
756
- catch (error) {
757
- tuiParseError = error instanceof Error ? error.message : String(error);
758
- problems.push(tuiParseError);
759
- }
760
- }
761
- const vvocText = await readOptionalText(paths.vvocConfigPath);
762
- let vvocParseError;
763
- let vvocConfig;
764
- let vvocSourceSchema;
765
- let vvocSourceVersion;
766
- let roleAssignments = [];
767
- if (vvocText) {
768
- try {
769
- const parsedConfig = parseVersionedVvocConfigText(vvocText, paths.vvocConfigPath);
770
- vvocConfig = parsedConfig.config;
771
- vvocSourceSchema = parsedConfig.sourceSchema;
772
- vvocSourceVersion = parsedConfig.sourceVersion;
773
- roleAssignments = listRoleAssignments(vvocConfig.roles);
774
- }
775
- catch (error) {
776
- vvocParseError = error instanceof Error ? error.message : String(error);
777
- problems.push(vvocParseError);
778
- }
779
- }
780
- const unresolvedRoleReferences = opencodeText && !opencodeParseError
781
- ? collectUnresolvedRoleReferences(opencodeText, paths.opencodeConfigPath, vvocConfig?.roles ?? {})
782
- : [];
783
- for (const unresolved of unresolvedRoleReferences) {
784
- problems.push(`unresolved role reference at ${unresolved.fieldPath}: ${unresolved.roleRef} (missing role: ${unresolved.roleId})`);
785
- }
786
- if (!pluginConfigured) {
787
- problems.push(`${PACKAGE_NAME} is not configured in ${paths.opencodeConfigPath}`);
788
- }
789
- if (!tuiPluginConfigured) {
790
- problems.push(`${TUI_PACKAGE_SPECIFIER} is not configured in ${paths.opencodeTuiConfigPath}`);
791
- }
792
- if (!vvocText) {
793
- problems.push(`vvoc config is missing at ${paths.vvocConfigPath}`);
794
- }
795
- return {
796
- scope: paths.scope,
797
- runtime,
798
- opencode: {
799
- path: paths.opencodeConfigPath,
800
- exists: Boolean(opencodeText),
801
- alternates: paths.opencodeAlternatePaths,
802
- parseError: opencodeParseError,
803
- pluginConfigured,
804
- plugins,
805
- },
806
- tui: {
807
- path: paths.opencodeTuiConfigPath,
808
- exists: Boolean(tuiText),
809
- alternates: paths.opencodeTuiAlternatePaths,
810
- parseError: tuiParseError,
811
- pluginConfigured: tuiPluginConfigured,
812
- plugins: tuiPlugins,
813
- },
814
- vvoc: {
815
- path: paths.vvocConfigPath,
816
- exists: Boolean(vvocText),
817
- parseError: vvocParseError,
818
- schema: vvocSourceSchema,
819
- version: vvocSourceVersion,
820
- },
821
- guardian: {
822
- config: vvocConfig?.guardian,
823
- },
824
- secretsRedaction: {
825
- config: vvocConfig?.secretsRedaction,
826
- },
827
- orchestration: {
828
- profile: vvocConfig ? resolveOrchestrationPolicy(vvocConfig).profile : undefined,
829
- },
830
- roles: {
831
- assignments: roleAssignments,
832
- unresolvedReferences: unresolvedRoleReferences,
833
- },
834
- warnings,
835
- problems,
836
- };
837
- }
838
- export async function inspectInstallationForScope(options) {
839
- const [opencodeSource, opencodeTuiSource, vvocSource, runtime] = await Promise.all([
840
- resolveOpenCodeConfigSource({
841
- scope: options.scope,
842
- cwd: options.cwd,
843
- configDir: options.configDir,
844
- }),
845
- resolveOpenCodeTuiConfigSource({
846
- scope: options.scope,
847
- cwd: options.cwd,
848
- configDir: options.configDir,
849
- }),
850
- resolveVvocConfigSource({
851
- scope: options.scope,
852
- cwd: options.cwd,
853
- configDir: options.configDir,
854
- allowDefault: options.scope === "effective",
855
- }),
856
- (options.inspectRuntime ?? inspectOpenCodeRuntime)(),
857
- ]);
858
- if (options.scope === "project") {
859
- const missingSource = [opencodeSource, vvocSource].find((source) => source.kind === "missing");
860
- if (missingSource) {
861
- throw new Error(missingSource.reason ?? "project config missing; run vvoc install --scope project");
862
- }
863
- }
864
- const fallbackPaths = await resolvePaths({
865
- scope: options.scope === "global" ? "global" : "project",
866
- cwd: options.cwd,
867
- configDir: options.configDir,
868
- });
869
- const opencodeConfigPath = opencodeSource.path ?? fallbackPaths.opencodeConfigPath;
870
- const opencodeTuiConfigPath = opencodeTuiSource.path ?? fallbackPaths.opencodeTuiConfigPath;
871
- const vvocConfigPath = vvocSource.path ?? fallbackPaths.vvocConfigPath;
872
- const scopedPaths = {
873
- ...fallbackPaths,
874
- opencodeBaseDir: dirname(opencodeConfigPath),
875
- vvocBaseDir: dirname(vvocConfigPath),
876
- opencodeConfigPath,
877
- opencodeTuiConfigPath,
878
- vvocConfigPath,
879
- opencodeAlternatePaths: [],
880
- opencodeTuiAlternatePaths: [],
881
- managedAgentsDirPath: getVvocAgentsDir(dirname(vvocConfigPath)),
882
- managedSkillsDirPath: getVvocSkillsDir(dirname(vvocConfigPath)),
883
- };
884
- const inspection = await inspectInstallation(scopedPaths, { runtime });
885
- return {
886
- ...inspection,
887
- orchestration: {
888
- profile: inspection.orchestration.profile ??
889
- (vvocSource.kind === "default"
890
- ? resolveOrchestrationPolicy(createDefaultVvocConfig()).profile
891
- : undefined),
892
- },
893
- scope: options.scope,
894
- opencodeSource,
895
- opencodeTuiSource,
896
- vvocSource,
897
- };
898
- }
899
- // END_BLOCK_INSPECT_INSTALLATION_STATE
900
- export function describeWriteResult(result) {
901
- let message = "";
902
- switch (result.action) {
903
- case "created":
904
- message = `Created ${result.path}`;
905
- break;
906
- case "updated":
907
- message = `Updated ${result.path}`;
908
- break;
909
- case "kept":
910
- message = `Kept ${result.path}`;
911
- break;
912
- case "skipped":
913
- message = `Skipped ${result.path}`;
914
- break;
915
- }
916
- return result.reason ? `${message} (${result.reason})` : message;
917
- }
918
- // START_BLOCK_PARSE_AND_NORMALIZE_CONFIG_VALUES
919
- function parseObjectDocument(text, label) {
920
- const errors = [];
921
- const value = parse(text, errors, {
922
- allowEmptyContent: false,
923
- allowTrailingComma: true,
924
- disallowComments: false,
925
- });
926
- if (errors.length > 0) {
927
- throw new Error(`${label}: failed to parse JSONC (${errors.length} error(s))`);
928
- }
929
- if (!value || typeof value !== "object" || Array.isArray(value)) {
930
- throw new Error(`${label}: expected a top-level object`);
931
- }
932
- return value;
933
- }
934
- function readPluginList(document, label) {
935
- const raw = document.plugin;
936
- if (raw === undefined)
937
- return [];
938
- if (!Array.isArray(raw) || raw.some((entry) => typeof entry !== "string")) {
939
- throw new Error(`${label}: expected "plugin" to be an array of strings`);
940
- }
941
- return raw.slice();
942
- }
943
- function readTuiPluginList(document, label) {
944
- const raw = document.plugin;
945
- if (raw === undefined)
946
- return [];
947
- if (!Array.isArray(raw)) {
948
- throw new Error(`${label}: expected "plugin" to be an array`);
949
- }
950
- return raw.map((entry, index) => {
951
- if (typeof entry === "string") {
952
- return entry;
953
- }
954
- if (Array.isArray(entry) &&
955
- entry.length === 2 &&
956
- typeof entry[0] === "string" &&
957
- isJsonObject(entry[1])) {
958
- return [entry[0], entry[1]];
959
- }
960
- throw new Error(`${label}: expected "plugin[${index}]" to be a string or [string, options] tuple`);
961
- });
962
- }
963
- function readStringArray(value, label) {
964
- if (!Array.isArray(value) || value.some((entry) => typeof entry !== "string")) {
965
- throw new Error(`${label}: expected an array of strings`);
966
- }
967
- return value.slice();
968
- }
969
- function normalizePluginList(currentPlugins, packageSpecifier) {
970
- const nextPlugins = [];
971
- const seen = new Set();
972
- let insertedPackage = false;
973
- const push = (value) => {
974
- if (!seen.has(value)) {
975
- seen.add(value);
976
- nextPlugins.push(value);
977
- }
978
- };
979
- for (const plugin of currentPlugins) {
980
- if (isPackagePluginSpecifier(plugin)) {
981
- if (!insertedPackage) {
982
- push(packageSpecifier);
983
- insertedPackage = true;
984
- }
985
- continue;
986
- }
987
- push(plugin);
988
- }
989
- if (!insertedPackage) {
990
- push(packageSpecifier);
991
- }
992
- return nextPlugins;
993
- }
994
- function normalizeTuiPluginList(currentPlugins, packageSpecifier) {
995
- const nextPlugins = [];
996
- let insertedPackage = false;
997
- for (const entry of currentPlugins) {
998
- if (!isManagedTuiPackageSpecifier(readTuiPluginName(entry))) {
999
- nextPlugins.push(entry);
1000
- continue;
1001
- }
1002
- if (insertedPackage) {
1003
- continue;
1004
- }
1005
- nextPlugins.push(typeof entry === "string" ? packageSpecifier : [packageSpecifier, entry[1]]);
1006
- insertedPackage = true;
1007
- }
1008
- if (!insertedPackage) {
1009
- nextPlugins.push(packageSpecifier);
1010
- }
1011
- return nextPlugins;
1012
- }
1013
- function isPackagePluginSpecifier(value) {
1014
- return value === PACKAGE_NAME || value.startsWith(`${PACKAGE_NAME}@`);
1015
- }
1016
- function readTuiPluginName(entry) {
1017
- return typeof entry === "string" ? entry : entry[0];
1018
- }
1019
- function isManagedTuiPackageSpecifier(value) {
1020
- return (isBasePackageSpecifier(value) ||
1021
- value === `${PACKAGE_NAME}/tui` ||
1022
- (value.startsWith(`${PACKAGE_NAME}@`) && value.endsWith("/tui")));
1023
- }
1024
- function isTuiPackageSpecifier(value) {
1025
- return value === TUI_PACKAGE_SPECIFIER;
1026
- }
1027
- function isBasePackageSpecifier(value) {
1028
- if (value === PACKAGE_NAME)
1029
- return true;
1030
- const prefix = `${PACKAGE_NAME}@`;
1031
- return value.startsWith(prefix) && !value.slice(prefix.length).includes("/");
1032
- }
1033
- // END_BLOCK_PARSE_AND_NORMALIZE_CONFIG_VALUES
1034
- // START_BLOCK_MANAGED_AGENT_HELPERS
1035
- function readAgentMap(document, label) {
1036
- const raw = document.agent;
1037
- if (raw === undefined) {
1038
- return {};
1039
- }
1040
- if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
1041
- throw new Error(`${label}: expected "agent" to be an object`);
1042
- }
1043
- const entries = {};
1044
- for (const [name, value] of Object.entries(raw)) {
1045
- if (!value || typeof value !== "object" || Array.isArray(value)) {
1046
- throw new Error(`${label}: expected "agent.${name}" to be an object`);
1047
- }
1048
- entries[name] = value;
1049
- }
1050
- return entries;
1051
- }
1052
- function readProviderMap(document, label) {
1053
- const raw = document.provider;
1054
- if (raw === undefined) {
1055
- return {};
1056
- }
1057
- if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
1058
- throw new Error(`${label}: expected "provider" to be an object`);
1059
- }
1060
- const entries = {};
1061
- for (const [name, value] of Object.entries(raw)) {
1062
- if (!value || typeof value !== "object" || Array.isArray(value)) {
1063
- throw new Error(`${label}: expected "provider.${name}" to be an object`);
1064
- }
1065
- entries[name] = value;
1066
- }
1067
- return entries;
1068
- }
1069
- function listRoleAssignments(roles) {
1070
- const listed = [];
1071
- for (const roleId of BUILTIN_ROLE_NAMES) {
1072
- if (typeof roles[roleId] === "string") {
1073
- listed.push({ roleId, model: roles[roleId], builtIn: true });
1074
- }
1075
- }
1076
- const customRoleIds = Object.keys(roles)
1077
- .filter((roleId) => !BUILTIN_ROLE_NAMES.includes(roleId))
1078
- .sort((left, right) => left.localeCompare(right));
1079
- for (const roleId of customRoleIds) {
1080
- listed.push({ roleId, model: roles[roleId], builtIn: false });
1081
- }
1082
- return listed;
1083
- }
1084
- function collectUnresolvedRoleReferences(opencodeText, label, roleMap) {
1085
- const document = parseObjectDocument(opencodeText, label);
1086
- const unresolved = [];
1087
- const collectFromField = (fieldPath, value) => {
1088
- if (typeof value !== "string") {
1089
- return;
1090
- }
1091
- const trimmed = value.trim();
1092
- if (!trimmed.startsWith(ROLE_REFERENCE_PREFIX)) {
1093
- return;
1094
- }
1095
- const roleId = trimmed.slice(ROLE_REFERENCE_PREFIX.length).trim();
1096
- if (!roleId || !Object.hasOwn(roleMap, roleId)) {
1097
- unresolved.push({ fieldPath, roleRef: trimmed, roleId: roleId || "<missing>" });
1098
- }
1099
- };
1100
- collectFromField("model", document.model);
1101
- collectFromField("small_model", document.small_model);
1102
- for (const parentName of ["agent", "command"]) {
1103
- const parent = document[parentName];
1104
- if (!parent || typeof parent !== "object" || Array.isArray(parent)) {
1105
- continue;
1106
- }
1107
- for (const [entryName, entry] of Object.entries(parent)) {
1108
- if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
1109
- continue;
1110
- }
1111
- collectFromField(`${parentName}.${entryName}.model`, entry.model);
1112
- }
1113
- }
1114
- return unresolved;
1115
- }
1116
- function getManagedPromptPath(paths, agentName) {
1117
- return getManagedAgentPromptPath(paths.managedAgentsDirPath, agentName);
1118
- }
1119
- function ensureAgentConfigText(text) {
1120
- const nextText = ensureOpenCodeConfigText(text);
1121
- const document = parseObjectDocument(nextText, "OpenCode config");
1122
- const currentAgents = readAgentMap(document, "OpenCode config");
1123
- let nextAgentText = nextText;
1124
- if (!Object.hasOwn(document, "agent")) {
1125
- nextAgentText = applyEdits(nextAgentText, modify(nextAgentText, ["agent"], currentAgents, {
1126
- formattingOptions: JSON_FORMAT,
1127
- }));
1128
- }
1129
- return ensureTrailingNewline(applyEdits(nextAgentText, format(nextAgentText, undefined, JSON_FORMAT)));
1130
- }
1131
- function getManagedOpenCodeAgentPromptReference(paths, agentName) {
1132
- const promptPath = getManagedPromptPath(paths, agentName);
1133
- const promptRef = relative(dirname(paths.opencodeConfigPath), promptPath).replaceAll("\\", "/");
1134
- return `{file:${promptRef.startsWith(".") ? promptRef : `./${promptRef}`}}`;
1135
- }
1136
- function getManagedSkillsPathReference(paths) {
1137
- const skillsRef = relative(dirname(paths.opencodeConfigPath), paths.managedSkillsDirPath).replaceAll("\\", "/");
1138
- return skillsRef.startsWith(".") ? skillsRef : `./${skillsRef}`;
1139
- }
1140
- function ensureManagedSkillsPathConfigText(text, paths) {
1141
- const document = parseObjectDocument(text, "OpenCode config");
1142
- const skills = readOptionalObject(document, "skills", "OpenCode config");
1143
- const rawPaths = skills?.paths;
1144
- const currentPaths = rawPaths === undefined ? [] : readStringArray(rawPaths, "OpenCode config: skills.paths");
1145
- const managedSkillsPath = getManagedSkillsPathReference(paths);
1146
- if (currentPaths.includes(managedSkillsPath)) {
1147
- return text;
1148
- }
1149
- const nextPaths = [...currentPaths, managedSkillsPath];
1150
- const nextText = applyEdits(text, modify(text, ["skills", "paths"], nextPaths, {
1151
- formattingOptions: JSON_FORMAT,
1152
- }));
1153
- return ensureTrailingNewline(applyEdits(nextText, format(nextText, undefined, JSON_FORMAT)));
1154
- }
1155
- function getManagedOpenCodeAgentRegistration(paths, agentName) {
1156
- const definition = getManagedOpenCodeAgentDefinition(agentName);
1157
- const builtInBindings = getBuiltInRoleBindings();
1158
- const registration = {
1159
- description: definition.description,
1160
- mode: definition.mode,
1161
- prompt: getManagedOpenCodeAgentPromptReference(paths, agentName),
1162
- model: createRoleReference(builtInBindings.managedAgents[agentName]),
1163
- };
1164
- if (definition.permission) {
1165
- registration.permission = definition.permission;
1166
- }
1167
- return registration;
1168
- }
1169
- async function renderManagedPrompt(agentName) {
1170
- const template = stripMarkdownFrontmatter(await loadManagedAgentPromptTemplate(agentName)).trim();
1171
- const header = [
1172
- "<!-- Managed by vvoc.",
1173
- "`vvoc sync` rewrites files with this marker while preserving agent registration and model settings elsewhere.",
1174
- "Remove this comment if you want to manage the file manually.",
1175
- "-->",
1176
- "",
1177
- ].join("\n");
1178
- return `${header}${template}\n`;
1179
- }
1180
- async function syncManagedPrompt(paths, agentName, options) {
1181
- const promptPath = getManagedPromptPath(paths, agentName);
1182
- const currentText = await readOptionalText(promptPath);
1183
- if (!currentText) {
1184
- await writeText(promptPath, await renderManagedPrompt(agentName));
1185
- return { action: "created", path: promptPath };
1186
- }
1187
- if (!options.force && !isManagedFile(currentText)) {
1188
- return {
1189
- action: "skipped",
1190
- path: promptPath,
1191
- reason: "existing file is not managed by vvoc",
1192
- };
1193
- }
1194
- const nextText = await renderManagedPrompt(agentName);
1195
- if (currentText === nextText) {
1196
- return { action: "kept", path: promptPath };
1197
- }
1198
- await writeText(promptPath, nextText);
1199
- return { action: "updated", path: promptPath };
1200
- }
1201
- async function renderManagedSkill(skillName) {
1202
- return loadManagedSkillTemplate(skillName);
1203
- }
1204
- async function syncManagedSkillReferences(skillsDirPath, skillName) {
1205
- const results = [];
1206
- const referenceNames = await listManagedSkillReferenceNames(skillName);
1207
- for (const refName of referenceNames) {
1208
- const templateContent = await loadManagedSkillReference(skillName, refName);
1209
- const targetPath = join(skillsDirPath, skillName, "references", refName);
1210
- const currentContent = await readOptionalText(targetPath);
1211
- if (currentContent === templateContent) {
1212
- results.push({ action: "kept", path: targetPath });
1213
- continue;
1214
- }
1215
- await writeText(targetPath, templateContent);
1216
- results.push({
1217
- action: currentContent ? "updated" : "created",
1218
- path: targetPath,
1219
- });
1220
- }
1221
- return results;
1222
- }
1223
- async function syncManagedSkill(paths, skillName, options) {
1224
- const skillPath = getManagedSkillFilePath(paths.managedSkillsDirPath, skillName);
1225
- const currentText = await readOptionalText(skillPath);
1226
- if (!currentText) {
1227
- await writeText(skillPath, await renderManagedSkill(skillName));
1228
- return { action: "created", path: skillPath };
1229
- }
1230
- if (!options.force && !hasYamlFrontmatter(currentText)) {
1231
- return {
1232
- action: "skipped",
1233
- path: skillPath,
1234
- reason: "existing file has no YAML frontmatter — might not be a skill",
1235
- };
1236
- }
1237
- const nextText = await renderManagedSkill(skillName);
1238
- if (currentText === nextText) {
1239
- return { action: "kept", path: skillPath };
1240
- }
1241
- await writeText(skillPath, nextText);
1242
- return { action: "updated", path: skillPath };
1243
- }
1244
- function updateAgentEntryText(text, agentName, entry) {
1245
- const nextText = applyEdits(text, modify(text, ["agent", agentName], entry, {
1246
- formattingOptions: JSON_FORMAT,
1247
- }));
1248
- return ensureTrailingNewline(applyEdits(nextText, format(nextText, undefined, JSON_FORMAT)));
1249
- }
1250
- function readAgentOverride(entry, label) {
1251
- if (!entry) {
1252
- return {};
1253
- }
1254
- return {
1255
- model: entry.model === undefined ? undefined : readNonEmptyString(entry.model, `${label}.model`),
1256
- };
1257
- }
1258
- function ensureOpenCodeConfigText(text) {
1259
- if (!text?.trim()) {
1260
- return renderJson({
1261
- $schema: OPENCODE_SCHEMA_URL,
1262
- });
1263
- }
1264
- const document = parseObjectDocument(text, "OpenCode config");
1265
- let nextText = text;
1266
- if (!Object.hasOwn(document, "$schema")) {
1267
- nextText = applyEdits(nextText, modify(nextText, ["$schema"], OPENCODE_SCHEMA_URL, {
1268
- formattingOptions: JSON_FORMAT,
1269
- getInsertionIndex: () => 0,
1270
- }));
1271
- }
1272
- return ensureTrailingNewline(applyEdits(nextText, format(nextText, undefined, JSON_FORMAT)));
1273
- }
1274
- function ensureProviderObjectConfigText(text, providerID, value) {
1275
- if (!text?.trim()) {
1276
- return renderJson({
1277
- $schema: OPENCODE_SCHEMA_URL,
1278
- provider: {
1279
- [providerID]: value,
1280
- },
1281
- });
1282
- }
1283
- const document = parseObjectDocument(text, "OpenCode config");
1284
- const currentProviders = readProviderMap(document, "OpenCode config");
1285
- const currentValue = currentProviders[providerID];
1286
- const nextValue = currentValue ? mergeJsonObjects(currentValue, value) : value;
1287
- let nextText = text;
1288
- if (!Object.hasOwn(document, "$schema")) {
1289
- nextText = applyEdits(nextText, modify(nextText, ["$schema"], OPENCODE_SCHEMA_URL, {
1290
- formattingOptions: JSON_FORMAT,
1291
- getInsertionIndex: () => 0,
1292
- }));
1293
- }
1294
- if (JSON.stringify(currentValue) !== JSON.stringify(nextValue)) {
1295
- nextText = applyEdits(nextText, modify(nextText, ["provider", providerID], nextValue, {
1296
- formattingOptions: JSON_FORMAT,
1297
- }));
1298
- }
1299
- return ensureTrailingNewline(applyEdits(nextText, format(nextText, undefined, JSON_FORMAT)));
1300
- }
1301
- function updateTopLevelStringFieldText(text, key, value) {
1302
- const nextText = applyEdits(text, modify(text, [key], value, {
1303
- formattingOptions: JSON_FORMAT,
1304
- }));
1305
- return ensureTrailingNewline(applyEdits(nextText, format(nextText, undefined, JSON_FORMAT)));
1306
- }
1307
- function createRoleReference(roleId) {
1308
- return `${ROLE_REFERENCE_PREFIX}${roleId}`;
1309
- }
1310
- // END_BLOCK_MANAGED_AGENT_HELPERS
1311
- // START_BLOCK_JSON_VALUE_HELPERS
1312
- function readNonEmptyString(value, label) {
1313
- if (typeof value !== "string" || !value.trim()) {
1314
- throw new Error(`${label}: expected a non-empty string`);
1315
- }
1316
- return value.trim();
1317
- }
1318
- function isJsonObject(value) {
1319
- return Boolean(value) && typeof value === "object" && !Array.isArray(value);
1320
- }
1321
- function mergeJsonObjects(current, patch) {
1322
- const merged = { ...current };
1323
- for (const [key, patchValue] of Object.entries(patch)) {
1324
- const currentValue = merged[key];
1325
- merged[key] =
1326
- isJsonObject(currentValue) && isJsonObject(patchValue)
1327
- ? mergeJsonObjects(currentValue, patchValue)
1328
- : patchValue;
1329
- }
1330
- return merged;
1331
- }
1332
- function readOptionalObject(document, key, label) {
1333
- const value = document[key];
1334
- if (value === undefined) {
1335
- return undefined;
1336
- }
1337
- if (!value || typeof value !== "object" || Array.isArray(value)) {
1338
- throw new Error(`${label}: expected "${key}" to be an object`);
1339
- }
1340
- return value;
1341
- }
1342
- // END_BLOCK_JSON_VALUE_HELPERS
1343
- // START_BLOCK_FILESYSTEM_HELPERS
1344
- function isManagedFile(text) {
1345
- return text.includes(MANAGED_MARKER);
1346
- }
1347
- function hasYamlFrontmatter(text) {
1348
- return text.startsWith("---\n");
1349
- }
1350
- function renderJson(value) {
1351
- return `${JSON.stringify(value, null, 2)}\n`;
1352
- }
1353
- function stripMarkdownFrontmatter(text) {
1354
- const normalized = text.replaceAll("\r\n", "\n");
1355
- return normalized.replace(/^---\n[\s\S]*?\n---\n?/, "");
1356
- }
1357
- function ensureTrailingNewline(text) {
1358
- return text.endsWith("\n") ? text : `${text}\n`;
1359
- }
1360
- async function writeResolvedVvocConfig(path, currentText, config) {
1361
- const nextText = renderVvocConfig(config);
1362
- if ((currentText ?? "") === nextText) {
1363
- return { action: "kept", path };
1364
- }
1365
- await writeText(path, nextText);
1366
- return {
1367
- action: currentText ? "updated" : "created",
1368
- path,
1369
- };
1370
- }
1371
- async function readOptionalText(path) {
1372
- try {
1373
- return await readFile(path, "utf8");
1374
- }
1375
- catch (error) {
1376
- if (error.code === "ENOENT") {
1377
- return undefined;
1378
- }
1379
- throw error;
1380
- }
1381
- }
1382
- async function writeText(path, text) {
1383
- await mkdir(dirname(path), { recursive: true });
1384
- await writeFile(path, text, "utf8");
1385
- }
1386
- async function resolveOpenCodeAlternates(opencodeBaseDir, selectedPath) {
1387
- return resolveConfigAlternates(opencodeBaseDir, selectedPath, OPENCODE_CONFIG_FILE_NAMES);
1388
- }
1389
- async function resolveConfigAlternates(baseDir, selectedPath, fileNames) {
1390
- const alternates = [];
1391
- for (const candidate of fileNames.map((name) => join(baseDir, name))) {
1392
- if (candidate !== selectedPath && (await readOptionalText(candidate)) !== undefined) {
1393
- alternates.push(candidate);
1394
- }
1395
- }
1396
- return alternates;
1397
- }
1398
- // END_BLOCK_FILESYSTEM_HELPERS
23
+ export { CLI_NAME, OPENCODE_SCHEMA_URL, OPENCODE_TUI_SCHEMA_URL } from "./opencode/shared-utils.js";
24
+ export { resolvePaths } from "./opencode/paths.js";
25
+ export { ensurePackageConfigText, ensureTuiPackageConfigText, ensureTuiPackageInstalled, ensurePackageInstalled, } from "./opencode/plugin-registration.js";
26
+ export { TUI_PACKAGE_SPECIFIER, MINIMUM_TUI_OPENCODE_VERSION, } from "./opencode/plugin-registration.js";
27
+ export { ensureManagedAgentRegistrationsConfigText, syncManagedAgentRegistrations, installManagedAgentPrompts, syncManagedAgentPrompts, installManagedSkillFiles, syncManagedSkillFiles, ensureManagedSkillSymlink, readManagedAgentModels, readManagedAgentOverrides, writeManagedAgentModel, } from "./opencode/agent-registrations.js";
28
+ export { readOpenCodeAgentModel, readOpenCodeAgentOverride, readOpenCodeDefaultModel, writeOpenCodeAgentModel, writeOpenCodeDefaultModel, writeOpenCodeProviderObject, ensureProviderBaseUrlConfigText, writeProviderBaseUrl, } from "./opencode/model-overrides.js";
29
+ export { readVvocConfig, installVvocConfig, syncVvocConfig, writeGuardianConfig, } from "./opencode/vvoc-config-io.js";
30
+ export { inspectOpenCodeRuntime, isTuiOpenCodeVersionCompatible, extractOpenCodeVersion, inspectInstallation, inspectInstallationForScope, describeWriteResult, } from "./opencode/inspection.js";
1399
31
  //# sourceMappingURL=opencode.js.map