@evref-bl/dev-nexus 0.1.0-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +677 -0
- package/dist/browserOpener.d.ts +9 -0
- package/dist/browserOpener.js +47 -0
- package/dist/cli.d.ts +18 -0
- package/dist/cli.js +2374 -0
- package/dist/gitWorktreeService.d.ts +57 -0
- package/dist/gitWorktreeService.js +157 -0
- package/dist/index.d.ts +47 -0
- package/dist/index.js +47 -0
- package/dist/nexusAgentMcpConfig.d.ts +30 -0
- package/dist/nexusAgentMcpConfig.js +228 -0
- package/dist/nexusAutomation.d.ts +103 -0
- package/dist/nexusAutomation.js +390 -0
- package/dist/nexusAutomationAgentLaunch.d.ts +148 -0
- package/dist/nexusAutomationAgentLaunch.js +855 -0
- package/dist/nexusAutomationAgentProfile.d.ts +39 -0
- package/dist/nexusAutomationAgentProfile.js +103 -0
- package/dist/nexusAutomationAgentSurface.d.ts +62 -0
- package/dist/nexusAutomationAgentSurface.js +90 -0
- package/dist/nexusAutomationCommandExecutor.d.ts +29 -0
- package/dist/nexusAutomationCommandExecutor.js +251 -0
- package/dist/nexusAutomationConfig.d.ts +114 -0
- package/dist/nexusAutomationConfig.js +547 -0
- package/dist/nexusAutomationEnqueue.d.ts +37 -0
- package/dist/nexusAutomationEnqueue.js +128 -0
- package/dist/nexusAutomationRunOnce.d.ts +91 -0
- package/dist/nexusAutomationRunOnce.js +586 -0
- package/dist/nexusAutomationScheduler.d.ts +50 -0
- package/dist/nexusAutomationScheduler.js +196 -0
- package/dist/nexusAutomationStatus.d.ts +55 -0
- package/dist/nexusAutomationStatus.js +462 -0
- package/dist/nexusAutomationTarget.d.ts +19 -0
- package/dist/nexusAutomationTarget.js +33 -0
- package/dist/nexusAutomationTargetCycle.d.ts +90 -0
- package/dist/nexusAutomationTargetCycle.js +282 -0
- package/dist/nexusAutomationTargetReport.d.ts +136 -0
- package/dist/nexusAutomationTargetReport.js +504 -0
- package/dist/nexusAutomationWorktreeSetup.d.ts +89 -0
- package/dist/nexusAutomationWorktreeSetup.js +661 -0
- package/dist/nexusCoordination.d.ts +198 -0
- package/dist/nexusCoordination.js +1018 -0
- package/dist/nexusExtension.d.ts +31 -0
- package/dist/nexusExtension.js +1 -0
- package/dist/nexusHomeConfig.d.ts +38 -0
- package/dist/nexusHomeConfig.js +133 -0
- package/dist/nexusMcpServer.d.ts +31 -0
- package/dist/nexusMcpServer.js +1036 -0
- package/dist/nexusPluginCapabilities.d.ts +197 -0
- package/dist/nexusPluginCapabilities.js +201 -0
- package/dist/nexusProjectConfig.d.ts +95 -0
- package/dist/nexusProjectConfig.js +880 -0
- package/dist/nexusProjectHomeService.d.ts +121 -0
- package/dist/nexusProjectHomeService.js +171 -0
- package/dist/nexusProjectLifecycle.d.ts +62 -0
- package/dist/nexusProjectLifecycle.js +205 -0
- package/dist/nexusProjectOperations.d.ts +101 -0
- package/dist/nexusProjectOperations.js +296 -0
- package/dist/nexusProjectRegistry.d.ts +42 -0
- package/dist/nexusProjectRegistry.js +91 -0
- package/dist/nexusProjectScaffold.d.ts +25 -0
- package/dist/nexusProjectScaffold.js +61 -0
- package/dist/nexusProjectTemplate.d.ts +34 -0
- package/dist/nexusProjectTemplate.js +354 -0
- package/dist/nexusSkills.d.ts +134 -0
- package/dist/nexusSkills.js +647 -0
- package/dist/nexusWorkerContextBundle.d.ts +142 -0
- package/dist/nexusWorkerContextBundle.js +375 -0
- package/dist/processSupervisor.d.ts +89 -0
- package/dist/processSupervisor.js +440 -0
- package/dist/vibeKanbanApi.d.ts +11 -0
- package/dist/vibeKanbanApi.js +14 -0
- package/dist/vibeKanbanAuth.d.ts +25 -0
- package/dist/vibeKanbanAuth.js +101 -0
- package/dist/vibeKanbanBoardAdapter.d.ts +36 -0
- package/dist/vibeKanbanBoardAdapter.js +196 -0
- package/dist/vibeKanbanMcpConfig.d.ts +36 -0
- package/dist/vibeKanbanMcpConfig.js +191 -0
- package/dist/vibeKanbanProjectAdapter.d.ts +39 -0
- package/dist/vibeKanbanProjectAdapter.js +113 -0
- package/dist/vibeKanbanWorkspaceSetup.d.ts +1 -0
- package/dist/vibeKanbanWorkspaceSetup.js +96 -0
- package/dist/workItemService.d.ts +60 -0
- package/dist/workItemService.js +163 -0
- package/dist/workTrackingGitHubProvider.d.ts +71 -0
- package/dist/workTrackingGitHubProvider.js +663 -0
- package/dist/workTrackingGitLabProvider.d.ts +62 -0
- package/dist/workTrackingGitLabProvider.js +523 -0
- package/dist/workTrackingJiraProvider.d.ts +67 -0
- package/dist/workTrackingJiraProvider.js +652 -0
- package/dist/workTrackingLocalProvider.d.ts +49 -0
- package/dist/workTrackingLocalProvider.js +463 -0
- package/dist/workTrackingProviderService.d.ts +21 -0
- package/dist/workTrackingProviderService.js +117 -0
- package/dist/workTrackingTypes.d.ts +202 -0
- package/dist/workTrackingTypes.js +1 -0
- package/dist/workTrackingVibeProvider.d.ts +35 -0
- package/dist/workTrackingVibeProvider.js +119 -0
- package/dist/worktreeExecutionMetadata.d.ts +76 -0
- package/dist/worktreeExecutionMetadata.js +239 -0
- package/package.json +37 -0
|
@@ -0,0 +1,880 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { nexusPluginWorkerFragmentBodyMaxLength, nexusPluginWorkerFragmentProvenanceMaxLength, nexusPluginWorkerFragmentTitleMaxLength, } from "./nexusPluginCapabilities.js";
|
|
4
|
+
import { validateNexusAutomationConfig, } from "./nexusAutomationConfig.js";
|
|
5
|
+
export const devNexusProjectConfigFileName = "dev-nexus.project.json";
|
|
6
|
+
export const nexusProjectWorktreesDirectoryName = "worktrees";
|
|
7
|
+
export class NexusConfigError extends Error {
|
|
8
|
+
constructor(message) {
|
|
9
|
+
super(message);
|
|
10
|
+
this.name = "NexusConfigError";
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
export function projectConfigPath(projectRootPath) {
|
|
14
|
+
return path.join(path.resolve(projectRootPath), devNexusProjectConfigFileName);
|
|
15
|
+
}
|
|
16
|
+
function resolveFromProject(projectRootPath, value) {
|
|
17
|
+
return path.resolve(projectRootPath, value);
|
|
18
|
+
}
|
|
19
|
+
export function projectWorktreesRootPath(projectRootPath, config) {
|
|
20
|
+
return resolveFromProject(projectRootPath, config?.worktreesRoot ?? nexusProjectWorktreesDirectoryName);
|
|
21
|
+
}
|
|
22
|
+
function assertRecord(value, pathName) {
|
|
23
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
24
|
+
throw new NexusConfigError(`${pathName} must be an object`);
|
|
25
|
+
}
|
|
26
|
+
return value;
|
|
27
|
+
}
|
|
28
|
+
function requiredString(record, key, pathName) {
|
|
29
|
+
const value = record[key];
|
|
30
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
31
|
+
throw new NexusConfigError(`${pathName}.${key} must be a non-empty string`);
|
|
32
|
+
}
|
|
33
|
+
return value;
|
|
34
|
+
}
|
|
35
|
+
function requiredBoundedString(record, key, pathName, maxLength) {
|
|
36
|
+
const value = requiredString(record, key, pathName);
|
|
37
|
+
if (value.length > maxLength) {
|
|
38
|
+
throw new NexusConfigError(`${pathName}.${key} must be at most ${maxLength} characters`);
|
|
39
|
+
}
|
|
40
|
+
return value;
|
|
41
|
+
}
|
|
42
|
+
function optionalString(record, key, pathName) {
|
|
43
|
+
const value = record[key];
|
|
44
|
+
if (value === undefined) {
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
48
|
+
throw new NexusConfigError(`${pathName}.${key} must be a non-empty string`);
|
|
49
|
+
}
|
|
50
|
+
return value;
|
|
51
|
+
}
|
|
52
|
+
function requiredProjectRelativePath(record, key, pathName) {
|
|
53
|
+
const value = requiredString(record, key, pathName).trim();
|
|
54
|
+
if (value.split(/[\\/]/u).some((part) => part === "..") ||
|
|
55
|
+
/^[A-Za-z]:/u.test(value) ||
|
|
56
|
+
value.startsWith("/") ||
|
|
57
|
+
value.startsWith("\\")) {
|
|
58
|
+
throw new NexusConfigError(`${pathName}.${key} must be a project-relative path`);
|
|
59
|
+
}
|
|
60
|
+
return value;
|
|
61
|
+
}
|
|
62
|
+
function nullableString(record, key, pathName) {
|
|
63
|
+
const value = record[key];
|
|
64
|
+
if (value === undefined || value === null) {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
68
|
+
throw new NexusConfigError(`${pathName}.${key} must be a non-empty string or null`);
|
|
69
|
+
}
|
|
70
|
+
return value;
|
|
71
|
+
}
|
|
72
|
+
export function validateNexusAgentConfig(value, pathName) {
|
|
73
|
+
if (value === undefined) {
|
|
74
|
+
return undefined;
|
|
75
|
+
}
|
|
76
|
+
const record = assertRecord(value, pathName);
|
|
77
|
+
const agent = compactAgentConfig({
|
|
78
|
+
executor: optionalString(record, "executor", pathName),
|
|
79
|
+
model: optionalString(record, "model", pathName),
|
|
80
|
+
reasoning: optionalString(record, "reasoning", pathName),
|
|
81
|
+
});
|
|
82
|
+
if (Object.keys(agent).length === 0) {
|
|
83
|
+
throw new NexusConfigError(`${pathName} must define executor, model, or reasoning`);
|
|
84
|
+
}
|
|
85
|
+
return agent;
|
|
86
|
+
}
|
|
87
|
+
function compactAgentConfig(config) {
|
|
88
|
+
const compacted = {};
|
|
89
|
+
if (config?.executor) {
|
|
90
|
+
compacted.executor = config.executor;
|
|
91
|
+
}
|
|
92
|
+
if (config?.model) {
|
|
93
|
+
compacted.model = config.model;
|
|
94
|
+
}
|
|
95
|
+
if (config?.reasoning) {
|
|
96
|
+
compacted.reasoning = config.reasoning;
|
|
97
|
+
}
|
|
98
|
+
return compacted;
|
|
99
|
+
}
|
|
100
|
+
function agentConfigFromSource(source) {
|
|
101
|
+
if (!source) {
|
|
102
|
+
return {};
|
|
103
|
+
}
|
|
104
|
+
if (Object.prototype.hasOwnProperty.call(source, "agent")) {
|
|
105
|
+
return compactAgentConfig(source.agent);
|
|
106
|
+
}
|
|
107
|
+
return compactAgentConfig(source);
|
|
108
|
+
}
|
|
109
|
+
export function resolveNexusAgentConfig(options) {
|
|
110
|
+
return compactAgentConfig({
|
|
111
|
+
...agentConfigFromSource(options.fallback),
|
|
112
|
+
...agentConfigFromSource(options.home),
|
|
113
|
+
...agentConfigFromSource(options.project),
|
|
114
|
+
...agentConfigFromSource(options.issue),
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
function validateKanbanConfig(value) {
|
|
118
|
+
const record = assertRecord(value, "kanban");
|
|
119
|
+
if (record.provider !== "vibe-kanban") {
|
|
120
|
+
throw new NexusConfigError("kanban.provider must be vibe-kanban");
|
|
121
|
+
}
|
|
122
|
+
return {
|
|
123
|
+
provider: "vibe-kanban",
|
|
124
|
+
projectId: nullableString(record, "projectId", "kanban"),
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
function validateProjectExtensionsConfig(value) {
|
|
128
|
+
if (value === undefined) {
|
|
129
|
+
return undefined;
|
|
130
|
+
}
|
|
131
|
+
const record = assertRecord(value, "project config.extensions");
|
|
132
|
+
const extensions = {};
|
|
133
|
+
for (const [key, extensionValue] of Object.entries(record)) {
|
|
134
|
+
if (!key.trim()) {
|
|
135
|
+
throw new NexusConfigError("project config.extensions keys must be non-empty strings");
|
|
136
|
+
}
|
|
137
|
+
if (!extensionValue ||
|
|
138
|
+
typeof extensionValue !== "object" ||
|
|
139
|
+
Array.isArray(extensionValue)) {
|
|
140
|
+
throw new NexusConfigError(`project config.extensions.${key} must be an object`);
|
|
141
|
+
}
|
|
142
|
+
extensions[key] = { ...extensionValue };
|
|
143
|
+
}
|
|
144
|
+
return extensions;
|
|
145
|
+
}
|
|
146
|
+
function validateSkillMaterialization(value, pathName) {
|
|
147
|
+
if (value === undefined) {
|
|
148
|
+
return undefined;
|
|
149
|
+
}
|
|
150
|
+
if (value === "copy" || value === "symlink" || value === "reference") {
|
|
151
|
+
return value;
|
|
152
|
+
}
|
|
153
|
+
throw new NexusConfigError(`${pathName} must be copy, symlink, or reference`);
|
|
154
|
+
}
|
|
155
|
+
function validateSkillSourceControl(value, pathName) {
|
|
156
|
+
if (value === undefined) {
|
|
157
|
+
return undefined;
|
|
158
|
+
}
|
|
159
|
+
if (value === "support" || value === "source") {
|
|
160
|
+
return value;
|
|
161
|
+
}
|
|
162
|
+
throw new NexusConfigError(`${pathName} must be support or source`);
|
|
163
|
+
}
|
|
164
|
+
function validateProjectSkillSelection(value, index) {
|
|
165
|
+
const pathName = `project config.skills.items[${index}]`;
|
|
166
|
+
const record = assertRecord(value, pathName);
|
|
167
|
+
const enabled = record.enabled;
|
|
168
|
+
if (enabled !== undefined && typeof enabled !== "boolean") {
|
|
169
|
+
throw new NexusConfigError(`${pathName}.enabled must be a boolean`);
|
|
170
|
+
}
|
|
171
|
+
const version = optionalString(record, "version", pathName);
|
|
172
|
+
const materialization = validateSkillMaterialization(record.materialization, `${pathName}.materialization`);
|
|
173
|
+
const sourceControl = validateSkillSourceControl(record.sourceControl, `${pathName}.sourceControl`);
|
|
174
|
+
return {
|
|
175
|
+
id: requiredString(record, "id", pathName),
|
|
176
|
+
...(enabled !== undefined ? { enabled } : {}),
|
|
177
|
+
...(version !== undefined ? { version } : {}),
|
|
178
|
+
...(materialization !== undefined ? { materialization } : {}),
|
|
179
|
+
...(sourceControl !== undefined ? { sourceControl } : {}),
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
function validateProjectSkillAgentTarget(value, index) {
|
|
183
|
+
const pathName = `project config.skills.agentTargets[${index}]`;
|
|
184
|
+
const record = assertRecord(value, pathName);
|
|
185
|
+
const enabled = record.enabled;
|
|
186
|
+
if (enabled !== undefined && typeof enabled !== "boolean") {
|
|
187
|
+
throw new NexusConfigError(`${pathName}.enabled must be a boolean`);
|
|
188
|
+
}
|
|
189
|
+
const sourceControl = validateSkillSourceControl(record.sourceControl, `${pathName}.sourceControl`);
|
|
190
|
+
const directory = optionalString(record, "directory", pathName);
|
|
191
|
+
return {
|
|
192
|
+
agent: requiredString(record, "agent", pathName),
|
|
193
|
+
...(enabled !== undefined ? { enabled } : {}),
|
|
194
|
+
...(directory !== undefined ? { directory } : {}),
|
|
195
|
+
...(sourceControl !== undefined ? { sourceControl } : {}),
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
function validateProjectSkillsConfig(value) {
|
|
199
|
+
if (value === undefined) {
|
|
200
|
+
return undefined;
|
|
201
|
+
}
|
|
202
|
+
const record = assertRecord(value, "project config.skills");
|
|
203
|
+
const defaultCorePack = record.defaultCorePack;
|
|
204
|
+
if (defaultCorePack !== undefined && typeof defaultCorePack !== "boolean") {
|
|
205
|
+
throw new NexusConfigError("project config.skills.defaultCorePack must be a boolean");
|
|
206
|
+
}
|
|
207
|
+
const items = record.items;
|
|
208
|
+
if (items !== undefined && !Array.isArray(items)) {
|
|
209
|
+
throw new NexusConfigError("project config.skills.items must be an array");
|
|
210
|
+
}
|
|
211
|
+
const agentTargets = record.agentTargets;
|
|
212
|
+
if (agentTargets !== undefined && !Array.isArray(agentTargets)) {
|
|
213
|
+
throw new NexusConfigError("project config.skills.agentTargets must be an array");
|
|
214
|
+
}
|
|
215
|
+
const materialization = validateSkillMaterialization(record.materialization, "project config.skills.materialization");
|
|
216
|
+
const sourceControl = validateSkillSourceControl(record.sourceControl, "project config.skills.sourceControl");
|
|
217
|
+
return {
|
|
218
|
+
...(defaultCorePack !== undefined ? { defaultCorePack } : {}),
|
|
219
|
+
...(materialization !== undefined ? { materialization } : {}),
|
|
220
|
+
...(sourceControl !== undefined ? { sourceControl } : {}),
|
|
221
|
+
...(agentTargets
|
|
222
|
+
? {
|
|
223
|
+
agentTargets: agentTargets.map((target, index) => validateProjectSkillAgentTarget(target, index)),
|
|
224
|
+
}
|
|
225
|
+
: {}),
|
|
226
|
+
...(items
|
|
227
|
+
? {
|
|
228
|
+
items: items.map((item, index) => validateProjectSkillSelection(item, index)),
|
|
229
|
+
}
|
|
230
|
+
: {}),
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
function validatePluginCapabilityKind(value, pathName) {
|
|
234
|
+
if (value === "projected_skill" ||
|
|
235
|
+
value === "mcp_server" ||
|
|
236
|
+
value === "setup_obligation" ||
|
|
237
|
+
value === "environment_hint" ||
|
|
238
|
+
value === "cleanup_hook" ||
|
|
239
|
+
value === "agent_affordance" ||
|
|
240
|
+
value === "dependency_projection" ||
|
|
241
|
+
value === "worker_context_fragment" ||
|
|
242
|
+
value === "worker_briefing_fragment") {
|
|
243
|
+
return value;
|
|
244
|
+
}
|
|
245
|
+
throw new NexusConfigError(`${pathName} must be projected_skill, mcp_server, setup_obligation, environment_hint, cleanup_hook, agent_affordance, dependency_projection, worker_context_fragment, or worker_briefing_fragment`);
|
|
246
|
+
}
|
|
247
|
+
function validatePluginCleanupHookTrigger(value, pathName) {
|
|
248
|
+
if (value === undefined) {
|
|
249
|
+
return undefined;
|
|
250
|
+
}
|
|
251
|
+
if (value === "before_run" || value === "after_run" || value === "manual") {
|
|
252
|
+
return value;
|
|
253
|
+
}
|
|
254
|
+
throw new NexusConfigError(`${pathName} must be before_run, after_run, or manual`);
|
|
255
|
+
}
|
|
256
|
+
function validatePluginDependencyProjectionSourceControl(value, pathName) {
|
|
257
|
+
if (value === undefined) {
|
|
258
|
+
return undefined;
|
|
259
|
+
}
|
|
260
|
+
if (value === "support" || value === "source") {
|
|
261
|
+
return value;
|
|
262
|
+
}
|
|
263
|
+
throw new NexusConfigError(`${pathName} must be support or source`);
|
|
264
|
+
}
|
|
265
|
+
function validatePluginMcpTools(value, pathName) {
|
|
266
|
+
if (value === undefined) {
|
|
267
|
+
return undefined;
|
|
268
|
+
}
|
|
269
|
+
if (!Array.isArray(value)) {
|
|
270
|
+
throw new NexusConfigError(`${pathName} must be an array`);
|
|
271
|
+
}
|
|
272
|
+
return value.map((tool, index) => {
|
|
273
|
+
const toolPath = `${pathName}[${index}]`;
|
|
274
|
+
const record = assertRecord(tool, toolPath);
|
|
275
|
+
const description = optionalString(record, "description", toolPath);
|
|
276
|
+
return {
|
|
277
|
+
name: requiredString(record, "name", toolPath),
|
|
278
|
+
...(description !== undefined ? { description } : {}),
|
|
279
|
+
};
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
function validatePluginCapabilityRecord(value, index, pluginPathName) {
|
|
283
|
+
const pathName = `${pluginPathName}.capabilities[${index}]`;
|
|
284
|
+
const record = assertRecord(value, pathName);
|
|
285
|
+
const kind = validatePluginCapabilityKind(record.kind, `${pathName}.kind`);
|
|
286
|
+
const id = requiredString(record, "id", pathName);
|
|
287
|
+
const description = optionalString(record, "description", pathName);
|
|
288
|
+
if (kind === "projected_skill") {
|
|
289
|
+
const targetAgents = optionalStringArray(record, "targetAgents", pathName);
|
|
290
|
+
return {
|
|
291
|
+
kind,
|
|
292
|
+
id,
|
|
293
|
+
...(description !== undefined ? { description } : {}),
|
|
294
|
+
skillId: requiredString(record, "skillId", pathName),
|
|
295
|
+
...(targetAgents !== undefined ? { targetAgents } : {}),
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
if (kind === "mcp_server") {
|
|
299
|
+
const tools = validatePluginMcpTools(record.tools, `${pathName}.tools`);
|
|
300
|
+
return {
|
|
301
|
+
kind,
|
|
302
|
+
id,
|
|
303
|
+
...(description !== undefined ? { description } : {}),
|
|
304
|
+
serverName: requiredString(record, "serverName", pathName),
|
|
305
|
+
...(tools !== undefined ? { tools } : {}),
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
if (kind === "setup_obligation") {
|
|
309
|
+
return {
|
|
310
|
+
kind,
|
|
311
|
+
id,
|
|
312
|
+
description: requiredString(record, "description", pathName),
|
|
313
|
+
required: optionalBoolean(record, "required", pathName) ?? false,
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
if (kind === "environment_hint") {
|
|
317
|
+
const valueHint = optionalString(record, "valueHint", pathName);
|
|
318
|
+
return {
|
|
319
|
+
kind,
|
|
320
|
+
id,
|
|
321
|
+
...(description !== undefined ? { description } : {}),
|
|
322
|
+
variable: requiredString(record, "variable", pathName),
|
|
323
|
+
...(valueHint !== undefined ? { valueHint } : {}),
|
|
324
|
+
required: optionalBoolean(record, "required", pathName) ?? false,
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
if (kind === "cleanup_hook") {
|
|
328
|
+
const trigger = validatePluginCleanupHookTrigger(record.trigger, `${pathName}.trigger`);
|
|
329
|
+
return {
|
|
330
|
+
kind,
|
|
331
|
+
id,
|
|
332
|
+
description: requiredString(record, "description", pathName),
|
|
333
|
+
...(trigger !== undefined ? { trigger } : {}),
|
|
334
|
+
required: optionalBoolean(record, "required", pathName) ?? false,
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
if (kind === "dependency_projection") {
|
|
338
|
+
const sourceControl = validatePluginDependencyProjectionSourceControl(record.sourceControl, `${pathName}.sourceControl`);
|
|
339
|
+
const targetAgents = optionalStringArray(record, "targetAgents", pathName);
|
|
340
|
+
const targetComponents = optionalStringArray(record, "targetComponents", pathName);
|
|
341
|
+
const reason = optionalString(record, "reason", pathName);
|
|
342
|
+
return {
|
|
343
|
+
kind,
|
|
344
|
+
id,
|
|
345
|
+
...(description !== undefined ? { description } : {}),
|
|
346
|
+
source: requiredProjectRelativePath(record, "source", pathName),
|
|
347
|
+
target: requiredProjectRelativePath(record, "target", pathName),
|
|
348
|
+
required: optionalBoolean(record, "required", pathName) ?? false,
|
|
349
|
+
sourceControl: sourceControl ?? "support",
|
|
350
|
+
...(targetAgents !== undefined ? { targetAgents } : {}),
|
|
351
|
+
...(targetComponents !== undefined ? { targetComponents } : {}),
|
|
352
|
+
...(reason !== undefined ? { reason } : {}),
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
if (kind === "worker_context_fragment" || kind === "worker_briefing_fragment") {
|
|
356
|
+
const targetAgents = optionalStringArray(record, "targetAgents", pathName);
|
|
357
|
+
const targetComponents = optionalStringArray(record, "targetComponents", pathName);
|
|
358
|
+
return {
|
|
359
|
+
kind,
|
|
360
|
+
id,
|
|
361
|
+
...(description !== undefined ? { description } : {}),
|
|
362
|
+
title: requiredBoundedString(record, "title", pathName, nexusPluginWorkerFragmentTitleMaxLength),
|
|
363
|
+
body: requiredBoundedString(record, "body", pathName, nexusPluginWorkerFragmentBodyMaxLength),
|
|
364
|
+
provenance: requiredBoundedString(record, "provenance", pathName, nexusPluginWorkerFragmentProvenanceMaxLength),
|
|
365
|
+
...(targetAgents !== undefined ? { targetAgents } : {}),
|
|
366
|
+
...(targetComponents !== undefined ? { targetComponents } : {}),
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
return {
|
|
370
|
+
kind,
|
|
371
|
+
id,
|
|
372
|
+
description: requiredString(record, "description", pathName),
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
function validateProjectPluginConfig(value, index) {
|
|
376
|
+
const pathName = `project config.plugins[${index}]`;
|
|
377
|
+
const record = assertRecord(value, pathName);
|
|
378
|
+
const enabled = optionalBoolean(record, "enabled", pathName) ?? true;
|
|
379
|
+
const name = optionalString(record, "name", pathName);
|
|
380
|
+
const version = optionalString(record, "version", pathName);
|
|
381
|
+
const capabilitiesValue = record.capabilities;
|
|
382
|
+
if (capabilitiesValue !== undefined && !Array.isArray(capabilitiesValue)) {
|
|
383
|
+
throw new NexusConfigError(`${pathName}.capabilities must be an array`);
|
|
384
|
+
}
|
|
385
|
+
const capabilities = (capabilitiesValue ?? []).map((capability, capabilityIndex) => validatePluginCapabilityRecord(capability, capabilityIndex, pathName));
|
|
386
|
+
const ids = new Set();
|
|
387
|
+
for (const capability of capabilities) {
|
|
388
|
+
if (ids.has(capability.id)) {
|
|
389
|
+
throw new NexusConfigError(`${pathName}.capabilities contains duplicate id: ${capability.id}`);
|
|
390
|
+
}
|
|
391
|
+
ids.add(capability.id);
|
|
392
|
+
}
|
|
393
|
+
return {
|
|
394
|
+
id: requiredString(record, "id", pathName),
|
|
395
|
+
enabled,
|
|
396
|
+
...(name !== undefined ? { name } : {}),
|
|
397
|
+
...(version !== undefined ? { version } : {}),
|
|
398
|
+
capabilities,
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
function validateProjectPluginsConfig(value) {
|
|
402
|
+
if (value === undefined) {
|
|
403
|
+
return undefined;
|
|
404
|
+
}
|
|
405
|
+
if (!Array.isArray(value)) {
|
|
406
|
+
throw new NexusConfigError("project config.plugins must be an array");
|
|
407
|
+
}
|
|
408
|
+
const plugins = value.map((plugin, index) => validateProjectPluginConfig(plugin, index));
|
|
409
|
+
const ids = new Set();
|
|
410
|
+
for (const plugin of plugins) {
|
|
411
|
+
if (ids.has(plugin.id)) {
|
|
412
|
+
throw new NexusConfigError(`project config.plugins contains duplicate id: ${plugin.id}`);
|
|
413
|
+
}
|
|
414
|
+
ids.add(plugin.id);
|
|
415
|
+
}
|
|
416
|
+
return plugins;
|
|
417
|
+
}
|
|
418
|
+
function validateProjectMcpAgentTarget(value, index) {
|
|
419
|
+
const pathName = `project config.mcp.agentTargets[${index}]`;
|
|
420
|
+
const record = assertRecord(value, pathName);
|
|
421
|
+
const enabled = optionalBoolean(record, "enabled", pathName);
|
|
422
|
+
const sourceControl = validateSkillSourceControl(record.sourceControl, `${pathName}.sourceControl`);
|
|
423
|
+
const configPath = optionalString(record, "configPath", pathName);
|
|
424
|
+
const serverName = optionalString(record, "serverName", pathName);
|
|
425
|
+
const command = optionalString(record, "command", pathName);
|
|
426
|
+
const args = optionalStringArray(record, "args", pathName);
|
|
427
|
+
return {
|
|
428
|
+
agent: requiredString(record, "agent", pathName),
|
|
429
|
+
...(enabled !== undefined ? { enabled } : {}),
|
|
430
|
+
...(configPath !== undefined ? { configPath } : {}),
|
|
431
|
+
...(sourceControl !== undefined ? { sourceControl } : {}),
|
|
432
|
+
...(serverName !== undefined ? { serverName } : {}),
|
|
433
|
+
...(command !== undefined ? { command } : {}),
|
|
434
|
+
...(args !== undefined ? { args } : {}),
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
function validateProjectMcpConfig(value) {
|
|
438
|
+
if (value === undefined) {
|
|
439
|
+
return undefined;
|
|
440
|
+
}
|
|
441
|
+
const record = assertRecord(value, "project config.mcp");
|
|
442
|
+
const enabled = optionalBoolean(record, "enabled", "project config.mcp");
|
|
443
|
+
const sourceControl = validateSkillSourceControl(record.sourceControl, "project config.mcp.sourceControl");
|
|
444
|
+
const serverName = optionalString(record, "serverName", "project config.mcp");
|
|
445
|
+
const command = optionalString(record, "command", "project config.mcp");
|
|
446
|
+
const args = optionalStringArray(record, "args", "project config.mcp");
|
|
447
|
+
const agentTargets = record.agentTargets;
|
|
448
|
+
if (agentTargets !== undefined && !Array.isArray(agentTargets)) {
|
|
449
|
+
throw new NexusConfigError("project config.mcp.agentTargets must be an array");
|
|
450
|
+
}
|
|
451
|
+
return {
|
|
452
|
+
...(enabled !== undefined ? { enabled } : {}),
|
|
453
|
+
...(sourceControl !== undefined ? { sourceControl } : {}),
|
|
454
|
+
...(serverName !== undefined ? { serverName } : {}),
|
|
455
|
+
...(command !== undefined ? { command } : {}),
|
|
456
|
+
...(args !== undefined ? { args } : {}),
|
|
457
|
+
...(agentTargets
|
|
458
|
+
? {
|
|
459
|
+
agentTargets: agentTargets.map((target, index) => validateProjectMcpAgentTarget(target, index)),
|
|
460
|
+
}
|
|
461
|
+
: {}),
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
function validateWorkTrackingProviderName(value) {
|
|
465
|
+
if (value === "local" ||
|
|
466
|
+
value === "vibe-kanban" ||
|
|
467
|
+
value === "github" ||
|
|
468
|
+
value === "gitlab" ||
|
|
469
|
+
value === "jira") {
|
|
470
|
+
return value;
|
|
471
|
+
}
|
|
472
|
+
throw new NexusConfigError("workTracking.provider must be local, vibe-kanban, github, gitlab, or jira");
|
|
473
|
+
}
|
|
474
|
+
function optionalNullableString(record, key, pathName) {
|
|
475
|
+
const value = record[key];
|
|
476
|
+
if (value === undefined) {
|
|
477
|
+
return undefined;
|
|
478
|
+
}
|
|
479
|
+
if (value === null) {
|
|
480
|
+
return null;
|
|
481
|
+
}
|
|
482
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
483
|
+
throw new NexusConfigError(`${pathName}.${key} must be a non-empty string or null`);
|
|
484
|
+
}
|
|
485
|
+
return value;
|
|
486
|
+
}
|
|
487
|
+
function optionalInteger(record, key, pathName) {
|
|
488
|
+
const value = record[key];
|
|
489
|
+
if (value === undefined) {
|
|
490
|
+
return undefined;
|
|
491
|
+
}
|
|
492
|
+
if (value === null) {
|
|
493
|
+
return null;
|
|
494
|
+
}
|
|
495
|
+
if (typeof value !== "number" || !Number.isInteger(value)) {
|
|
496
|
+
throw new NexusConfigError(`${pathName}.${key} must be an integer or null`);
|
|
497
|
+
}
|
|
498
|
+
return value;
|
|
499
|
+
}
|
|
500
|
+
function optionalStringRecord(record, key, pathName) {
|
|
501
|
+
const value = record[key];
|
|
502
|
+
if (value === undefined) {
|
|
503
|
+
return undefined;
|
|
504
|
+
}
|
|
505
|
+
const valueRecord = assertRecord(value, `${pathName}.${key}`);
|
|
506
|
+
for (const [recordKey, recordValue] of Object.entries(valueRecord)) {
|
|
507
|
+
if (typeof recordValue !== "string") {
|
|
508
|
+
throw new NexusConfigError(`${pathName}.${key}.${recordKey} must be a string`);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
return valueRecord;
|
|
512
|
+
}
|
|
513
|
+
function validateWorkTrackingRepositoryConfig(value, pathName) {
|
|
514
|
+
if (value === undefined) {
|
|
515
|
+
return undefined;
|
|
516
|
+
}
|
|
517
|
+
const record = assertRecord(value, pathName);
|
|
518
|
+
const owner = optionalString(record, "owner", pathName);
|
|
519
|
+
const name = optionalString(record, "name", pathName);
|
|
520
|
+
const id = optionalString(record, "id", pathName);
|
|
521
|
+
const repositoryPath = optionalString(record, "path", pathName);
|
|
522
|
+
return {
|
|
523
|
+
...(owner ? { owner } : {}),
|
|
524
|
+
...(name ? { name } : {}),
|
|
525
|
+
...(id ? { id } : {}),
|
|
526
|
+
...(repositoryPath ? { path: repositoryPath } : {}),
|
|
527
|
+
};
|
|
528
|
+
}
|
|
529
|
+
function validateRequiredWorkTrackingRepositoryConfig(value, pathName) {
|
|
530
|
+
const repository = validateWorkTrackingRepositoryConfig(value, pathName);
|
|
531
|
+
if (!repository) {
|
|
532
|
+
throw new NexusConfigError(`${pathName} must be an object`);
|
|
533
|
+
}
|
|
534
|
+
return repository;
|
|
535
|
+
}
|
|
536
|
+
function validateWorkTrackingBoardConfig(value) {
|
|
537
|
+
if (value === undefined) {
|
|
538
|
+
return undefined;
|
|
539
|
+
}
|
|
540
|
+
if (value === null) {
|
|
541
|
+
return null;
|
|
542
|
+
}
|
|
543
|
+
const pathName = "workTracking.board";
|
|
544
|
+
const record = assertRecord(value, pathName);
|
|
545
|
+
const id = optionalNullableString(record, "id", pathName);
|
|
546
|
+
const number = optionalInteger(record, "number", pathName);
|
|
547
|
+
const owner = optionalNullableString(record, "owner", pathName);
|
|
548
|
+
const ownerKind = optionalNullableString(record, "ownerKind", pathName);
|
|
549
|
+
const projectId = optionalNullableString(record, "projectId", pathName);
|
|
550
|
+
const statusFieldId = optionalNullableString(record, "statusFieldId", pathName);
|
|
551
|
+
const statusOptions = optionalStringRecord(record, "statusOptions", pathName);
|
|
552
|
+
return {
|
|
553
|
+
kind: requiredString(record, "kind", pathName),
|
|
554
|
+
...(id !== undefined ? { id } : {}),
|
|
555
|
+
...(number !== undefined ? { number } : {}),
|
|
556
|
+
...(owner !== undefined ? { owner } : {}),
|
|
557
|
+
...(ownerKind !== undefined ? { ownerKind } : {}),
|
|
558
|
+
...(projectId !== undefined ? { projectId } : {}),
|
|
559
|
+
...(statusFieldId !== undefined ? { statusFieldId } : {}),
|
|
560
|
+
...(statusOptions ? { statusOptions } : {}),
|
|
561
|
+
};
|
|
562
|
+
}
|
|
563
|
+
function validateWorkTrackingConfig(value) {
|
|
564
|
+
if (value === undefined) {
|
|
565
|
+
return undefined;
|
|
566
|
+
}
|
|
567
|
+
const record = assertRecord(value, "workTracking");
|
|
568
|
+
const provider = validateWorkTrackingProviderName(record.provider);
|
|
569
|
+
const host = optionalNullableString(record, "host", "workTracking");
|
|
570
|
+
const repository = validateWorkTrackingRepositoryConfig(record.repository, "workTracking.repository");
|
|
571
|
+
const board = validateWorkTrackingBoardConfig(record.board);
|
|
572
|
+
const common = {
|
|
573
|
+
...(host !== undefined ? { host } : {}),
|
|
574
|
+
...(repository ? { repository } : {}),
|
|
575
|
+
...(board !== undefined ? { board } : {}),
|
|
576
|
+
};
|
|
577
|
+
if (provider === "local") {
|
|
578
|
+
const storePath = optionalNullableString(record, "storePath", "workTracking");
|
|
579
|
+
return {
|
|
580
|
+
provider,
|
|
581
|
+
...common,
|
|
582
|
+
...(storePath !== undefined ? { storePath } : {}),
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
if (provider === "vibe-kanban") {
|
|
586
|
+
const projectId = optionalNullableString(record, "projectId", "workTracking");
|
|
587
|
+
const repoId = optionalNullableString(record, "repoId", "workTracking");
|
|
588
|
+
return {
|
|
589
|
+
provider,
|
|
590
|
+
...common,
|
|
591
|
+
...(projectId !== undefined ? { projectId } : {}),
|
|
592
|
+
...(repoId !== undefined ? { repoId } : {}),
|
|
593
|
+
};
|
|
594
|
+
}
|
|
595
|
+
if (provider === "github") {
|
|
596
|
+
const githubRepository = validateRequiredWorkTrackingRepositoryConfig(record.repository, "workTracking.repository");
|
|
597
|
+
if (!githubRepository.owner) {
|
|
598
|
+
throw new NexusConfigError("workTracking.repository.owner must be a non-empty string");
|
|
599
|
+
}
|
|
600
|
+
if (!githubRepository.name) {
|
|
601
|
+
throw new NexusConfigError("workTracking.repository.name must be a non-empty string");
|
|
602
|
+
}
|
|
603
|
+
return {
|
|
604
|
+
provider,
|
|
605
|
+
...common,
|
|
606
|
+
repository: {
|
|
607
|
+
...githubRepository,
|
|
608
|
+
owner: githubRepository.owner,
|
|
609
|
+
name: githubRepository.name,
|
|
610
|
+
},
|
|
611
|
+
};
|
|
612
|
+
}
|
|
613
|
+
if (provider === "gitlab") {
|
|
614
|
+
const gitlabRepository = validateRequiredWorkTrackingRepositoryConfig(record.repository, "workTracking.repository");
|
|
615
|
+
if (!gitlabRepository.id) {
|
|
616
|
+
throw new NexusConfigError("workTracking.repository.id must be a non-empty string");
|
|
617
|
+
}
|
|
618
|
+
return {
|
|
619
|
+
provider,
|
|
620
|
+
...common,
|
|
621
|
+
repository: {
|
|
622
|
+
...gitlabRepository,
|
|
623
|
+
id: gitlabRepository.id,
|
|
624
|
+
},
|
|
625
|
+
};
|
|
626
|
+
}
|
|
627
|
+
const issueType = optionalNullableString(record, "issueType", "workTracking");
|
|
628
|
+
return {
|
|
629
|
+
provider,
|
|
630
|
+
...common,
|
|
631
|
+
projectKey: requiredString(record, "projectKey", "workTracking"),
|
|
632
|
+
...(issueType !== undefined ? { issueType } : {}),
|
|
633
|
+
};
|
|
634
|
+
}
|
|
635
|
+
function optionalBoolean(record, key, pathName) {
|
|
636
|
+
const value = record[key];
|
|
637
|
+
if (value === undefined) {
|
|
638
|
+
return undefined;
|
|
639
|
+
}
|
|
640
|
+
if (typeof value !== "boolean") {
|
|
641
|
+
throw new NexusConfigError(`${pathName}.${key} must be a boolean`);
|
|
642
|
+
}
|
|
643
|
+
return value;
|
|
644
|
+
}
|
|
645
|
+
function optionalStringArray(record, key, pathName) {
|
|
646
|
+
const value = record[key];
|
|
647
|
+
if (value === undefined) {
|
|
648
|
+
return undefined;
|
|
649
|
+
}
|
|
650
|
+
if (!Array.isArray(value)) {
|
|
651
|
+
throw new NexusConfigError(`${pathName}.${key} must be an array`);
|
|
652
|
+
}
|
|
653
|
+
for (const [index, entry] of value.entries()) {
|
|
654
|
+
if (typeof entry !== "string" || entry.trim().length === 0) {
|
|
655
|
+
throw new NexusConfigError(`${pathName}.${key}[${index}] must be a non-empty string`);
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
return [...value];
|
|
659
|
+
}
|
|
660
|
+
function validateComponentRole(value, fallback, pathName) {
|
|
661
|
+
if (value === undefined) {
|
|
662
|
+
return fallback;
|
|
663
|
+
}
|
|
664
|
+
if (value === "primary" ||
|
|
665
|
+
value === "extension" ||
|
|
666
|
+
value === "addon" ||
|
|
667
|
+
value === "dependency" ||
|
|
668
|
+
value === "optional") {
|
|
669
|
+
return value;
|
|
670
|
+
}
|
|
671
|
+
throw new NexusConfigError(`${pathName} must be primary, extension, addon, dependency, or optional`);
|
|
672
|
+
}
|
|
673
|
+
function validateComponentRelationshipKind(value, pathName) {
|
|
674
|
+
if (value === "extends" ||
|
|
675
|
+
value === "depends_on" ||
|
|
676
|
+
value === "optional" ||
|
|
677
|
+
value === "related") {
|
|
678
|
+
return value;
|
|
679
|
+
}
|
|
680
|
+
throw new NexusConfigError(`${pathName} must be extends, depends_on, optional, or related`);
|
|
681
|
+
}
|
|
682
|
+
function validateComponentRelationships(value, pathName) {
|
|
683
|
+
if (value === undefined) {
|
|
684
|
+
return [];
|
|
685
|
+
}
|
|
686
|
+
if (!Array.isArray(value)) {
|
|
687
|
+
throw new NexusConfigError(`${pathName} must be an array`);
|
|
688
|
+
}
|
|
689
|
+
return value.map((entry, index) => {
|
|
690
|
+
const entryPath = `${pathName}[${index}]`;
|
|
691
|
+
const record = assertRecord(entry, entryPath);
|
|
692
|
+
return {
|
|
693
|
+
kind: validateComponentRelationshipKind(record.kind, `${entryPath}.kind`),
|
|
694
|
+
componentId: requiredString(record, "componentId", entryPath),
|
|
695
|
+
};
|
|
696
|
+
});
|
|
697
|
+
}
|
|
698
|
+
function validateComponentVerificationConfig(value, pathName) {
|
|
699
|
+
if (value === undefined) {
|
|
700
|
+
return undefined;
|
|
701
|
+
}
|
|
702
|
+
const record = assertRecord(value, pathName);
|
|
703
|
+
const focusedCommands = optionalStringArray(record, "focusedCommands", pathName);
|
|
704
|
+
const fullCommands = optionalStringArray(record, "fullCommands", pathName);
|
|
705
|
+
const requirePassing = optionalBoolean(record, "requirePassing", pathName);
|
|
706
|
+
return {
|
|
707
|
+
...(focusedCommands !== undefined ? { focusedCommands } : {}),
|
|
708
|
+
...(fullCommands !== undefined ? { fullCommands } : {}),
|
|
709
|
+
...(requirePassing !== undefined ? { requirePassing } : {}),
|
|
710
|
+
};
|
|
711
|
+
}
|
|
712
|
+
function validatePublicationStrategy(value, pathName) {
|
|
713
|
+
if (value === undefined) {
|
|
714
|
+
return undefined;
|
|
715
|
+
}
|
|
716
|
+
if (value === "local_only" ||
|
|
717
|
+
value === "direct_integration" ||
|
|
718
|
+
value === "review_handoff") {
|
|
719
|
+
return value;
|
|
720
|
+
}
|
|
721
|
+
throw new NexusConfigError(`${pathName} must be local_only, direct_integration, or review_handoff`);
|
|
722
|
+
}
|
|
723
|
+
function validateComponentPublicationConfig(value, pathName) {
|
|
724
|
+
if (value === undefined) {
|
|
725
|
+
return undefined;
|
|
726
|
+
}
|
|
727
|
+
const record = assertRecord(value, pathName);
|
|
728
|
+
const strategy = validatePublicationStrategy(record.strategy, `${pathName}.strategy`);
|
|
729
|
+
const remote = optionalNullableString(record, "remote", pathName);
|
|
730
|
+
const targetBranch = optionalNullableString(record, "targetBranch", pathName);
|
|
731
|
+
const push = optionalBoolean(record, "push", pathName);
|
|
732
|
+
return {
|
|
733
|
+
...(strategy !== undefined ? { strategy } : {}),
|
|
734
|
+
...(remote !== undefined ? { remote } : {}),
|
|
735
|
+
...(targetBranch !== undefined ? { targetBranch } : {}),
|
|
736
|
+
...(push !== undefined ? { push } : {}),
|
|
737
|
+
};
|
|
738
|
+
}
|
|
739
|
+
function validateRepoConfig(value) {
|
|
740
|
+
if (value === undefined) {
|
|
741
|
+
return {
|
|
742
|
+
kind: "local",
|
|
743
|
+
remoteUrl: null,
|
|
744
|
+
defaultBranch: null,
|
|
745
|
+
};
|
|
746
|
+
}
|
|
747
|
+
const record = assertRecord(value, "repo");
|
|
748
|
+
const kind = record.kind;
|
|
749
|
+
if (kind !== "local" && kind !== "git") {
|
|
750
|
+
throw new NexusConfigError("repo.kind must be local or git");
|
|
751
|
+
}
|
|
752
|
+
const sourceRoot = optionalString(record, "sourceRoot", "repo");
|
|
753
|
+
return {
|
|
754
|
+
kind,
|
|
755
|
+
remoteUrl: nullableString(record, "remoteUrl", "repo"),
|
|
756
|
+
defaultBranch: nullableString(record, "defaultBranch", "repo"),
|
|
757
|
+
...(sourceRoot ? { sourceRoot } : {}),
|
|
758
|
+
};
|
|
759
|
+
}
|
|
760
|
+
function validateProjectComponent(value, index) {
|
|
761
|
+
const pathName = `project config.components[${index}]`;
|
|
762
|
+
const record = assertRecord(value, pathName);
|
|
763
|
+
const id = requiredString(record, "id", pathName);
|
|
764
|
+
const kind = record.kind;
|
|
765
|
+
if (kind !== "local" && kind !== "git") {
|
|
766
|
+
throw new NexusConfigError(`${pathName}.kind must be local or git`);
|
|
767
|
+
}
|
|
768
|
+
const sourceRoot = optionalString(record, "sourceRoot", pathName) ?? `components/${id}`;
|
|
769
|
+
const worktreesRoot = optionalString(record, "worktreesRoot", pathName);
|
|
770
|
+
const workTracking = validateWorkTrackingConfig(record.workTracking);
|
|
771
|
+
const verification = validateComponentVerificationConfig(record.verification, `${pathName}.verification`);
|
|
772
|
+
const publication = validateComponentPublicationConfig(record.publication, `${pathName}.publication`);
|
|
773
|
+
return {
|
|
774
|
+
id,
|
|
775
|
+
name: optionalString(record, "name", pathName) ?? id,
|
|
776
|
+
kind,
|
|
777
|
+
role: validateComponentRole(record.role, index === 0 ? "primary" : "dependency", `${pathName}.role`),
|
|
778
|
+
remoteUrl: nullableString(record, "remoteUrl", pathName),
|
|
779
|
+
defaultBranch: nullableString(record, "defaultBranch", pathName),
|
|
780
|
+
...(sourceRoot ? { sourceRoot } : {}),
|
|
781
|
+
...(worktreesRoot ? { worktreesRoot } : {}),
|
|
782
|
+
...(workTracking ? { workTracking } : {}),
|
|
783
|
+
...(verification ? { verification } : {}),
|
|
784
|
+
...(publication ? { publication } : {}),
|
|
785
|
+
relationships: validateComponentRelationships(record.relationships, `${pathName}.relationships`),
|
|
786
|
+
};
|
|
787
|
+
}
|
|
788
|
+
function defaultPrimaryComponentFromRepo(config) {
|
|
789
|
+
return {
|
|
790
|
+
id: "primary",
|
|
791
|
+
name: config.name,
|
|
792
|
+
kind: config.repo.kind,
|
|
793
|
+
role: "primary",
|
|
794
|
+
remoteUrl: config.repo.remoteUrl,
|
|
795
|
+
defaultBranch: config.repo.defaultBranch,
|
|
796
|
+
sourceRoot: config.repo.sourceRoot ?? ".",
|
|
797
|
+
...(config.workTracking ? { workTracking: config.workTracking } : {}),
|
|
798
|
+
relationships: [],
|
|
799
|
+
};
|
|
800
|
+
}
|
|
801
|
+
function validateProjectComponentsConfig(value, fallback) {
|
|
802
|
+
if (value === undefined) {
|
|
803
|
+
return [defaultPrimaryComponentFromRepo(fallback)];
|
|
804
|
+
}
|
|
805
|
+
if (!Array.isArray(value)) {
|
|
806
|
+
throw new NexusConfigError("project config.components must be an array");
|
|
807
|
+
}
|
|
808
|
+
if (value.length === 0) {
|
|
809
|
+
throw new NexusConfigError("project config.components must not be empty");
|
|
810
|
+
}
|
|
811
|
+
const components = value.map((entry, index) => validateProjectComponent(entry, index));
|
|
812
|
+
const ids = new Set();
|
|
813
|
+
for (const component of components) {
|
|
814
|
+
if (ids.has(component.id)) {
|
|
815
|
+
throw new NexusConfigError(`project config.components contains duplicate id: ${component.id}`);
|
|
816
|
+
}
|
|
817
|
+
ids.add(component.id);
|
|
818
|
+
}
|
|
819
|
+
const primaryComponents = components.filter((component) => component.role === "primary");
|
|
820
|
+
if (primaryComponents.length !== 1) {
|
|
821
|
+
throw new NexusConfigError("project config.components must contain exactly one primary component");
|
|
822
|
+
}
|
|
823
|
+
for (const component of components) {
|
|
824
|
+
for (const relationship of component.relationships) {
|
|
825
|
+
if (!ids.has(relationship.componentId)) {
|
|
826
|
+
throw new NexusConfigError(`project config.components.${component.id} relationship references unknown component: ${relationship.componentId}`);
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
return components;
|
|
831
|
+
}
|
|
832
|
+
export function validateProjectConfig(value) {
|
|
833
|
+
const record = assertRecord(value, "project config");
|
|
834
|
+
if (record.version !== 1) {
|
|
835
|
+
throw new NexusConfigError("project config.version must be 1");
|
|
836
|
+
}
|
|
837
|
+
const agent = validateNexusAgentConfig(record.agent, "project config.agent");
|
|
838
|
+
const workTracking = validateWorkTrackingConfig(record.workTracking);
|
|
839
|
+
const extensions = validateProjectExtensionsConfig(record.extensions);
|
|
840
|
+
const skills = validateProjectSkillsConfig(record.skills);
|
|
841
|
+
const plugins = validateProjectPluginsConfig(record.plugins);
|
|
842
|
+
const mcp = validateProjectMcpConfig(record.mcp);
|
|
843
|
+
const automation = validateNexusAutomationConfig(record.automation);
|
|
844
|
+
const repo = validateRepoConfig(record.repo);
|
|
845
|
+
const worktreesRoot = optionalString(record, "worktreesRoot", "project config") ??
|
|
846
|
+
nexusProjectWorktreesDirectoryName;
|
|
847
|
+
const common = {
|
|
848
|
+
version: 1,
|
|
849
|
+
id: requiredString(record, "id", "project config"),
|
|
850
|
+
name: requiredString(record, "name", "project config"),
|
|
851
|
+
home: nullableString(record, "home", "project config"),
|
|
852
|
+
repo,
|
|
853
|
+
worktreesRoot,
|
|
854
|
+
kanban: validateKanbanConfig(record.kanban),
|
|
855
|
+
...(workTracking ? { workTracking } : {}),
|
|
856
|
+
};
|
|
857
|
+
return {
|
|
858
|
+
...common,
|
|
859
|
+
components: validateProjectComponentsConfig(record.components, common),
|
|
860
|
+
...(extensions ? { extensions } : {}),
|
|
861
|
+
...(agent ? { agent } : {}),
|
|
862
|
+
...(mcp ? { mcp } : {}),
|
|
863
|
+
...(skills ? { skills } : {}),
|
|
864
|
+
...(plugins ? { plugins } : {}),
|
|
865
|
+
...(automation ? { automation } : {}),
|
|
866
|
+
};
|
|
867
|
+
}
|
|
868
|
+
export function loadProjectConfig(projectRootPath) {
|
|
869
|
+
const configPath = projectConfigPath(projectRootPath);
|
|
870
|
+
if (!fs.existsSync(configPath)) {
|
|
871
|
+
throw new NexusConfigError(`DevNexus project is not initialized: ${configPath}`);
|
|
872
|
+
}
|
|
873
|
+
return validateProjectConfig(JSON.parse(fs.readFileSync(configPath, "utf8").replace(/^\uFEFF/, "")));
|
|
874
|
+
}
|
|
875
|
+
export function saveProjectConfig(projectRootPath, config) {
|
|
876
|
+
const configPath = projectConfigPath(projectRootPath);
|
|
877
|
+
fs.mkdirSync(projectRootPath, { recursive: true });
|
|
878
|
+
fs.writeFileSync(configPath, `${JSON.stringify(validateProjectConfig(config), null, 2)}\n`, "utf8");
|
|
879
|
+
return configPath;
|
|
880
|
+
}
|