@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,661 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import process from "node:process";
|
|
4
|
+
import { defaultGitRunner, } from "./gitWorktreeService.js";
|
|
5
|
+
import { materializeNexusWorkerContextBundle, } from "./nexusWorkerContextBundle.js";
|
|
6
|
+
import { nexusSkillManifestFileName, nexusSkillMarkdownFileName, nexusSkillsDirectoryName, nexusSkillSupportDirectoryName, } from "./nexusSkills.js";
|
|
7
|
+
export class NexusAutomationWorktreeSetupError extends Error {
|
|
8
|
+
constructor(message) {
|
|
9
|
+
super(message);
|
|
10
|
+
this.name = "NexusAutomationWorktreeSetupError";
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
export function preflightNexusAutomationWorktreeSetup(options) {
|
|
14
|
+
const sourceRoot = path.resolve(requiredNonEmptyString(options.sourceRoot, "sourceRoot"));
|
|
15
|
+
const dependencyLinkChecks = options.automationConfig.setup.dependencyLinks.map((link, index) => {
|
|
16
|
+
const name = `dependencyLink:${index}`;
|
|
17
|
+
try {
|
|
18
|
+
const sourcePath = resolveInsideRoot(sourceRoot, link.source, "source");
|
|
19
|
+
resolveInsideRoot(sourceRoot, link.target, "target");
|
|
20
|
+
if (!fs.existsSync(sourcePath)) {
|
|
21
|
+
if (link.required) {
|
|
22
|
+
return {
|
|
23
|
+
name,
|
|
24
|
+
status: "failed",
|
|
25
|
+
message: `Required dependency link source does not exist: ${sourcePath}`,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
return {
|
|
29
|
+
name,
|
|
30
|
+
status: "passed",
|
|
31
|
+
message: `Optional dependency link source is absent and will be skipped: ${sourcePath}`,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
return {
|
|
35
|
+
name,
|
|
36
|
+
status: "passed",
|
|
37
|
+
message: `Dependency link ${link.source} -> ${link.target} is safe to materialize`,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
catch (error) {
|
|
41
|
+
return {
|
|
42
|
+
name,
|
|
43
|
+
status: "failed",
|
|
44
|
+
message: errorMessage(error),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
const pluginProjectionChecks = (options.pluginDependencyProjections ?? []).map((projection) => preflightPluginDependencyProjection({
|
|
49
|
+
projection,
|
|
50
|
+
sourceRoot,
|
|
51
|
+
}));
|
|
52
|
+
return [...dependencyLinkChecks, ...pluginProjectionChecks];
|
|
53
|
+
}
|
|
54
|
+
export function materializeNexusAutomationWorktreeSetup(options) {
|
|
55
|
+
const sourceRoot = path.resolve(requiredNonEmptyString(options.sourceRoot, "sourceRoot"));
|
|
56
|
+
const worktreePath = path.resolve(requiredNonEmptyString(options.worktreePath, "worktreePath"));
|
|
57
|
+
if (options.worktreesRoot) {
|
|
58
|
+
assertWorktreePathInsideRoot(options.worktreesRoot, worktreePath);
|
|
59
|
+
}
|
|
60
|
+
const gitRunner = options.gitRunner ?? defaultGitRunner;
|
|
61
|
+
const platform = options.platform ?? process.platform;
|
|
62
|
+
const links = options.automationConfig.setup.dependencyLinks.map((link) => materializeDependencyLink({
|
|
63
|
+
link,
|
|
64
|
+
sourceRoot,
|
|
65
|
+
worktreePath,
|
|
66
|
+
gitRunner,
|
|
67
|
+
platform,
|
|
68
|
+
}));
|
|
69
|
+
const dependencyProjections = (options.pluginDependencyProjections ?? []).map((projection) => materializePluginDependencyProjection({
|
|
70
|
+
projection,
|
|
71
|
+
sourceRoot,
|
|
72
|
+
worktreePath,
|
|
73
|
+
gitRunner,
|
|
74
|
+
platform,
|
|
75
|
+
}));
|
|
76
|
+
const skillProjections = options.context
|
|
77
|
+
? materializeWorkerSkillProjections({
|
|
78
|
+
projectRoot: options.context.project.root,
|
|
79
|
+
worktreePath,
|
|
80
|
+
skillsConfig: options.skillsConfig,
|
|
81
|
+
gitRunner,
|
|
82
|
+
platform,
|
|
83
|
+
})
|
|
84
|
+
: [];
|
|
85
|
+
const context = options.context
|
|
86
|
+
? materializeWorkerContext({
|
|
87
|
+
context: options.context,
|
|
88
|
+
automationConfig: options.automationConfig,
|
|
89
|
+
sourceRoot,
|
|
90
|
+
worktreesRoot: options.worktreesRoot,
|
|
91
|
+
worktreePath,
|
|
92
|
+
gitRunner,
|
|
93
|
+
skillProjections,
|
|
94
|
+
dependencyProjections,
|
|
95
|
+
})
|
|
96
|
+
: undefined;
|
|
97
|
+
return {
|
|
98
|
+
links,
|
|
99
|
+
dependencyProjections,
|
|
100
|
+
skillProjections,
|
|
101
|
+
...(context ? { context } : {}),
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
function materializeWorkerContext(options) {
|
|
105
|
+
const projectRoot = path.resolve(requiredNonEmptyString(options.context.project.root, "context.project.root"));
|
|
106
|
+
const ownership = options.context.ownership;
|
|
107
|
+
const ownershipSourceRoot = path.resolve(requiredNonEmptyString(ownership.sourceRoot, "context.ownership.sourceRoot"));
|
|
108
|
+
const ownershipWorktreesRoot = path.resolve(requiredNonEmptyString(ownership.worktreesRoot, "context.ownership.worktreesRoot"));
|
|
109
|
+
const ownershipWorktreePath = path.resolve(requiredNonEmptyString(ownership.worktreePath, "context.ownership.worktreePath"));
|
|
110
|
+
if (ownershipSourceRoot !== options.sourceRoot) {
|
|
111
|
+
throw new NexusAutomationWorktreeSetupError(`context.ownership.sourceRoot must match sourceRoot: ${ownershipSourceRoot}`);
|
|
112
|
+
}
|
|
113
|
+
if (options.worktreesRoot) {
|
|
114
|
+
const setupWorktreesRoot = path.resolve(options.worktreesRoot);
|
|
115
|
+
if (ownershipWorktreesRoot !== setupWorktreesRoot) {
|
|
116
|
+
throw new NexusAutomationWorktreeSetupError(`context.ownership.worktreesRoot must match worktreesRoot: ${ownershipWorktreesRoot}`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
if (ownershipWorktreePath !== options.worktreePath) {
|
|
120
|
+
throw new NexusAutomationWorktreeSetupError(`context.ownership.worktreePath must match worktreePath: ${ownershipWorktreePath}`);
|
|
121
|
+
}
|
|
122
|
+
const result = materializeNexusWorkerContextBundle({
|
|
123
|
+
projectRoot,
|
|
124
|
+
projectId: options.context.project.id ?? null,
|
|
125
|
+
projectName: options.context.project.name ?? null,
|
|
126
|
+
componentId: ownership.componentId,
|
|
127
|
+
sourceRoot: ownershipSourceRoot,
|
|
128
|
+
worktreesRoot: ownershipWorktreesRoot,
|
|
129
|
+
worktreePath: ownershipWorktreePath,
|
|
130
|
+
branchName: ownership.branchName,
|
|
131
|
+
baseRef: ownership.baseRef,
|
|
132
|
+
workItem: ownership.workItem,
|
|
133
|
+
targetStatePath: options.context.targetStatePath ?? options.automationConfig.target.statePath,
|
|
134
|
+
skills: workerContextSkillsFromProjections(projectRoot, options.skillProjections),
|
|
135
|
+
dependencyProjections: options.dependencyProjections,
|
|
136
|
+
pluginFragments: options.context.pluginFragments,
|
|
137
|
+
});
|
|
138
|
+
addGitInfoExclude({
|
|
139
|
+
worktreePath: options.worktreePath,
|
|
140
|
+
targetPath: result.contextDirectoryPath,
|
|
141
|
+
isDirectory: true,
|
|
142
|
+
gitRunner: options.gitRunner,
|
|
143
|
+
});
|
|
144
|
+
return result;
|
|
145
|
+
}
|
|
146
|
+
function materializeWorkerSkillProjections(options) {
|
|
147
|
+
const enabledTargets = (options.skillsConfig?.agentTargets ?? []).filter((target) => target.enabled !== false);
|
|
148
|
+
if (enabledTargets.length === 0) {
|
|
149
|
+
return [];
|
|
150
|
+
}
|
|
151
|
+
const projectRoot = path.resolve(requiredNonEmptyString(options.projectRoot, "projectRoot"));
|
|
152
|
+
const worktreePath = path.resolve(requiredNonEmptyString(options.worktreePath, "worktreePath"));
|
|
153
|
+
const projectManagedSkillsRoot = path.join(projectRoot, nexusSkillSupportDirectoryName, nexusSkillsDirectoryName);
|
|
154
|
+
if (!fs.existsSync(projectManagedSkillsRoot)) {
|
|
155
|
+
throw new NexusAutomationWorktreeSetupError(`Project-managed skills root does not exist: ${projectManagedSkillsRoot}`);
|
|
156
|
+
}
|
|
157
|
+
const installedSkills = readProjectManagedSkills(projectManagedSkillsRoot);
|
|
158
|
+
const selectedSkillIds = selectedProjectSkillIds(installedSkills.map((skill) => skill.id), options.skillsConfig);
|
|
159
|
+
const installedById = new Map(installedSkills.map((skill) => [skill.id, skill]));
|
|
160
|
+
return enabledTargets.map((target) => {
|
|
161
|
+
const skillsDirectory = resolveWorkerAgentSkillsDirectory(worktreePath, target);
|
|
162
|
+
const sourceControl = target.sourceControl ?? options.skillsConfig?.sourceControl ?? "support";
|
|
163
|
+
const skills = selectedSkillIds
|
|
164
|
+
.map((skillId) => installedById.get(skillId))
|
|
165
|
+
.filter((skill) => Boolean(skill))
|
|
166
|
+
.filter((skill) => skill.manifest.supportedAgents.includes(target.agent))
|
|
167
|
+
.map((skill) => materializeProjectedWorkerSkill({
|
|
168
|
+
skill,
|
|
169
|
+
skillsDirectory,
|
|
170
|
+
platform: options.platform,
|
|
171
|
+
}));
|
|
172
|
+
if (sourceControl === "support" && skills.length > 0) {
|
|
173
|
+
addGitInfoExclude({
|
|
174
|
+
worktreePath,
|
|
175
|
+
targetPath: skillsDirectory,
|
|
176
|
+
isDirectory: true,
|
|
177
|
+
gitRunner: options.gitRunner,
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
return {
|
|
181
|
+
agent: target.agent,
|
|
182
|
+
projectManagedSkillsRoot,
|
|
183
|
+
skillsDirectory,
|
|
184
|
+
sourceControl,
|
|
185
|
+
skills,
|
|
186
|
+
};
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
function workerContextSkillsFromProjections(projectRoot, skillProjections) {
|
|
190
|
+
const projectManagedRoot = skillProjections[0]?.projectManagedSkillsRoot ??
|
|
191
|
+
path.join(projectRoot, nexusSkillSupportDirectoryName, nexusSkillsDirectoryName);
|
|
192
|
+
return {
|
|
193
|
+
projectManagedRoot,
|
|
194
|
+
agentNativeProjections: skillProjections.map((projection) => ({
|
|
195
|
+
agent: projection.agent,
|
|
196
|
+
skillsDirectory: projection.skillsDirectory,
|
|
197
|
+
sourceControl: projection.sourceControl,
|
|
198
|
+
skills: projection.skills.map((skill) => ({
|
|
199
|
+
id: skill.id,
|
|
200
|
+
sourceSkillRoot: skill.sourceSkillRoot,
|
|
201
|
+
projectedSkillRoot: skill.projectedSkillRoot,
|
|
202
|
+
skillPath: skill.skillPath,
|
|
203
|
+
})),
|
|
204
|
+
})),
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
function readProjectManagedSkills(projectManagedSkillsRoot) {
|
|
208
|
+
return fs
|
|
209
|
+
.readdirSync(projectManagedSkillsRoot, { withFileTypes: true })
|
|
210
|
+
.filter((entry) => entry.isDirectory())
|
|
211
|
+
.map((entry) => readProjectManagedSkill(path.join(projectManagedSkillsRoot, entry.name)))
|
|
212
|
+
.sort((left, right) => left.id.localeCompare(right.id));
|
|
213
|
+
}
|
|
214
|
+
function readProjectManagedSkill(skillRoot) {
|
|
215
|
+
const manifestPath = path.join(skillRoot, nexusSkillManifestFileName);
|
|
216
|
+
if (!fs.existsSync(manifestPath)) {
|
|
217
|
+
throw new NexusAutomationWorktreeSetupError(`Project-managed skill manifest is missing: ${manifestPath}`);
|
|
218
|
+
}
|
|
219
|
+
const parsed = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
|
|
220
|
+
if (!isNexusSkillManifest(parsed)) {
|
|
221
|
+
throw new NexusAutomationWorktreeSetupError(`Project-managed skill manifest is invalid: ${manifestPath}`);
|
|
222
|
+
}
|
|
223
|
+
return {
|
|
224
|
+
id: parsed.id,
|
|
225
|
+
manifest: parsed,
|
|
226
|
+
skillRoot,
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
function selectedProjectSkillIds(installedSkillIds, skillsConfig) {
|
|
230
|
+
const selected = skillsConfig?.defaultCorePack === false ? new Set() : new Set(installedSkillIds);
|
|
231
|
+
const order = skillsConfig?.defaultCorePack === false ? [] : [...installedSkillIds].sort();
|
|
232
|
+
for (const item of skillsConfig?.items ?? []) {
|
|
233
|
+
if (item.enabled === false) {
|
|
234
|
+
selected.delete(item.id);
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
if (!selected.has(item.id)) {
|
|
238
|
+
order.push(item.id);
|
|
239
|
+
}
|
|
240
|
+
selected.add(item.id);
|
|
241
|
+
}
|
|
242
|
+
return order.filter((skillId) => selected.has(skillId));
|
|
243
|
+
}
|
|
244
|
+
function materializeProjectedWorkerSkill(options) {
|
|
245
|
+
const projectedSkillRoot = path.join(options.skillsDirectory, options.skill.manifest.id);
|
|
246
|
+
const skillPath = options.skill.manifest.materialization === "reference"
|
|
247
|
+
? null
|
|
248
|
+
: path.join(projectedSkillRoot, nexusSkillMarkdownFileName);
|
|
249
|
+
const before = inspectProjectedWorkerSkill({
|
|
250
|
+
skill: options.skill,
|
|
251
|
+
projectedSkillRoot,
|
|
252
|
+
});
|
|
253
|
+
if (before.status !== "present") {
|
|
254
|
+
refreshProjectedWorkerSkill({
|
|
255
|
+
sourceSkillRoot: options.skill.skillRoot,
|
|
256
|
+
projectedSkillRoot,
|
|
257
|
+
materialization: options.skill.manifest.materialization,
|
|
258
|
+
platform: options.platform,
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
const after = inspectProjectedWorkerSkill({
|
|
262
|
+
skill: options.skill,
|
|
263
|
+
projectedSkillRoot,
|
|
264
|
+
});
|
|
265
|
+
return {
|
|
266
|
+
id: options.skill.manifest.id,
|
|
267
|
+
name: options.skill.manifest.name,
|
|
268
|
+
version: options.skill.manifest.version,
|
|
269
|
+
materialization: options.skill.manifest.materialization,
|
|
270
|
+
sourceSkillRoot: options.skill.skillRoot,
|
|
271
|
+
projectedSkillRoot,
|
|
272
|
+
skillPath,
|
|
273
|
+
beforeStatus: before.status,
|
|
274
|
+
afterStatus: after.status,
|
|
275
|
+
refreshed: before.status !== "present",
|
|
276
|
+
reasons: before.reasons,
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
function inspectProjectedWorkerSkill(options) {
|
|
280
|
+
if (!fs.existsSync(options.projectedSkillRoot)) {
|
|
281
|
+
return {
|
|
282
|
+
status: "missing",
|
|
283
|
+
reasons: ["projected skill is missing"],
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
const reasons = [];
|
|
287
|
+
if (options.skill.manifest.materialization === "symlink") {
|
|
288
|
+
const stat = fs.lstatSync(options.projectedSkillRoot);
|
|
289
|
+
if (!stat.isSymbolicLink()) {
|
|
290
|
+
reasons.push("projected skill root is not a symlink");
|
|
291
|
+
}
|
|
292
|
+
else {
|
|
293
|
+
const actual = fs.realpathSync(options.projectedSkillRoot);
|
|
294
|
+
const expected = fs.realpathSync(options.skill.skillRoot);
|
|
295
|
+
if (actual !== expected) {
|
|
296
|
+
reasons.push("projected skill symlink points at a different source");
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
else if (options.skill.manifest.materialization === "copy") {
|
|
301
|
+
const sourceFiles = relativeSkillFiles(options.skill.skillRoot, false);
|
|
302
|
+
const targetFiles = relativeSkillFiles(options.projectedSkillRoot, true);
|
|
303
|
+
const expectedFiles = new Set(sourceFiles);
|
|
304
|
+
for (const filePath of sourceFiles) {
|
|
305
|
+
const sourcePath = path.join(options.skill.skillRoot, filePath);
|
|
306
|
+
const targetPath = path.join(options.projectedSkillRoot, filePath);
|
|
307
|
+
if (!fs.existsSync(targetPath)) {
|
|
308
|
+
reasons.push(`projected skill file is missing: ${filePath}`);
|
|
309
|
+
continue;
|
|
310
|
+
}
|
|
311
|
+
if (fs.readFileSync(targetPath, "utf8") !== fs.readFileSync(sourcePath, "utf8")) {
|
|
312
|
+
reasons.push(`projected skill file differs: ${filePath}`);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
for (const filePath of targetFiles) {
|
|
316
|
+
if (!expectedFiles.has(filePath)) {
|
|
317
|
+
reasons.push(`projected skill has an unexpected file: ${filePath}`);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
return {
|
|
322
|
+
status: reasons.length > 0 ? "stale" : "present",
|
|
323
|
+
reasons,
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
function refreshProjectedWorkerSkill(options) {
|
|
327
|
+
fs.rmSync(options.projectedSkillRoot, { recursive: true, force: true });
|
|
328
|
+
fs.mkdirSync(path.dirname(options.projectedSkillRoot), { recursive: true });
|
|
329
|
+
if (options.materialization === "symlink") {
|
|
330
|
+
fs.symlinkSync(options.sourceSkillRoot, options.projectedSkillRoot, options.platform === "win32" ? "junction" : "dir");
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
if (options.materialization === "reference") {
|
|
334
|
+
fs.mkdirSync(options.projectedSkillRoot, { recursive: true });
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
copySkillProjectionFiles(options.sourceSkillRoot, options.projectedSkillRoot);
|
|
338
|
+
}
|
|
339
|
+
function copySkillProjectionFiles(sourceRoot, targetRoot) {
|
|
340
|
+
for (const entry of fs.readdirSync(sourceRoot, { withFileTypes: true })) {
|
|
341
|
+
if (entry.name === nexusSkillManifestFileName) {
|
|
342
|
+
continue;
|
|
343
|
+
}
|
|
344
|
+
const sourcePath = path.join(sourceRoot, entry.name);
|
|
345
|
+
const targetPath = path.join(targetRoot, entry.name);
|
|
346
|
+
if (entry.isDirectory()) {
|
|
347
|
+
copySkillProjectionFiles(sourcePath, targetPath);
|
|
348
|
+
continue;
|
|
349
|
+
}
|
|
350
|
+
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
|
|
351
|
+
fs.copyFileSync(sourcePath, targetPath);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
function relativeSkillFiles(root, includeManifest) {
|
|
355
|
+
if (!fs.existsSync(root)) {
|
|
356
|
+
return [];
|
|
357
|
+
}
|
|
358
|
+
const files = [];
|
|
359
|
+
collectRelativeSkillFiles(root, root, includeManifest, files);
|
|
360
|
+
return files.sort();
|
|
361
|
+
}
|
|
362
|
+
function collectRelativeSkillFiles(root, current, includeManifest, files) {
|
|
363
|
+
for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
|
|
364
|
+
const entryPath = path.join(current, entry.name);
|
|
365
|
+
const relative = path.relative(root, entryPath).split(path.sep).join("/");
|
|
366
|
+
if (entry.isDirectory()) {
|
|
367
|
+
collectRelativeSkillFiles(root, entryPath, includeManifest, files);
|
|
368
|
+
continue;
|
|
369
|
+
}
|
|
370
|
+
if (!includeManifest && relative === nexusSkillManifestFileName) {
|
|
371
|
+
continue;
|
|
372
|
+
}
|
|
373
|
+
files.push(relative);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
function resolveWorkerAgentSkillsDirectory(worktreePath, target) {
|
|
377
|
+
const directory = target.directory ?? defaultWorkerAgentSkillsDirectory(target.agent);
|
|
378
|
+
if (!directory) {
|
|
379
|
+
throw new NexusAutomationWorktreeSetupError(`Agent skill target ${target.agent} must define directory`);
|
|
380
|
+
}
|
|
381
|
+
return resolveInsideRoot(worktreePath, directory, "skills agent target directory");
|
|
382
|
+
}
|
|
383
|
+
function defaultWorkerAgentSkillsDirectory(agent) {
|
|
384
|
+
if (agent === "codex") {
|
|
385
|
+
return path.join(".agents", "skills");
|
|
386
|
+
}
|
|
387
|
+
if (agent === "claude") {
|
|
388
|
+
return path.join(".claude", "skills");
|
|
389
|
+
}
|
|
390
|
+
return null;
|
|
391
|
+
}
|
|
392
|
+
function isNexusSkillManifest(value) {
|
|
393
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
394
|
+
return false;
|
|
395
|
+
}
|
|
396
|
+
const record = value;
|
|
397
|
+
return (typeof record.id === "string" &&
|
|
398
|
+
typeof record.name === "string" &&
|
|
399
|
+
typeof record.description === "string" &&
|
|
400
|
+
typeof record.version === "string" &&
|
|
401
|
+
typeof record.license === "string" &&
|
|
402
|
+
record.source !== null &&
|
|
403
|
+
typeof record.source === "object" &&
|
|
404
|
+
!Array.isArray(record.source) &&
|
|
405
|
+
Array.isArray(record.supportedAgents) &&
|
|
406
|
+
record.supportedAgents.every((agent) => typeof agent === "string") &&
|
|
407
|
+
(record.materialization === "copy" ||
|
|
408
|
+
record.materialization === "symlink" ||
|
|
409
|
+
record.materialization === "reference") &&
|
|
410
|
+
(record.sourceControl === "support" || record.sourceControl === "source"));
|
|
411
|
+
}
|
|
412
|
+
function preflightPluginDependencyProjection(options) {
|
|
413
|
+
const name = `pluginDependencyProjection:${requiredNonEmptyString(options.projection.id, "pluginDependencyProjection.id")}`;
|
|
414
|
+
try {
|
|
415
|
+
const sourcePath = resolveInsideRoot(options.sourceRoot, options.projection.source, "plugin dependency projection source");
|
|
416
|
+
resolveInsideRoot(options.sourceRoot, options.projection.target, "plugin dependency projection target");
|
|
417
|
+
normalizeDependencyProjectionSourceControl(options.projection.sourceControl, "plugin dependency projection sourceControl");
|
|
418
|
+
normalizePluginDependencyProjectionSourceMetadata(options.projection.sourceMetadata);
|
|
419
|
+
if (!fs.existsSync(sourcePath)) {
|
|
420
|
+
if (options.projection.required) {
|
|
421
|
+
return {
|
|
422
|
+
name,
|
|
423
|
+
status: "failed",
|
|
424
|
+
message: `Required plugin dependency projection source does not exist: ${sourcePath}`,
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
return {
|
|
428
|
+
name,
|
|
429
|
+
status: "passed",
|
|
430
|
+
message: `Optional plugin dependency projection source is absent and will be skipped: ${sourcePath}`,
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
return {
|
|
434
|
+
name,
|
|
435
|
+
status: "passed",
|
|
436
|
+
message: `Plugin dependency projection ${options.projection.source} -> ${options.projection.target} is safe to materialize`,
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
catch (error) {
|
|
440
|
+
return {
|
|
441
|
+
name,
|
|
442
|
+
status: "failed",
|
|
443
|
+
message: errorMessage(error),
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
function materializePluginDependencyProjection(options) {
|
|
448
|
+
const projection = normalizePluginDependencyProjection(options.projection);
|
|
449
|
+
const sourcePath = resolveInsideRoot(options.sourceRoot, projection.source, "plugin dependency projection source");
|
|
450
|
+
const targetPath = resolveInsideRoot(options.worktreePath, projection.target, "plugin dependency projection target");
|
|
451
|
+
if (!fs.existsSync(sourcePath)) {
|
|
452
|
+
if (projection.required) {
|
|
453
|
+
throw new NexusAutomationWorktreeSetupError(`Required plugin dependency projection source does not exist: ${sourcePath}`);
|
|
454
|
+
}
|
|
455
|
+
return dependencyProjectionResult({
|
|
456
|
+
projection,
|
|
457
|
+
sourcePath,
|
|
458
|
+
targetPath,
|
|
459
|
+
status: "skipped",
|
|
460
|
+
message: `Optional plugin dependency projection source is absent: ${sourcePath}`,
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
const sourceStats = fs.statSync(sourcePath);
|
|
464
|
+
if (fs.existsSync(targetPath)) {
|
|
465
|
+
if (projection.sourceControl === "support") {
|
|
466
|
+
addGitInfoExclude({
|
|
467
|
+
worktreePath: options.worktreePath,
|
|
468
|
+
targetPath,
|
|
469
|
+
isDirectory: sourceStats.isDirectory(),
|
|
470
|
+
gitRunner: options.gitRunner,
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
return dependencyProjectionResult({
|
|
474
|
+
projection,
|
|
475
|
+
sourcePath,
|
|
476
|
+
targetPath,
|
|
477
|
+
status: "present",
|
|
478
|
+
message: `Plugin dependency projection target already exists: ${targetPath}`,
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
|
|
482
|
+
fs.symlinkSync(sourcePath, targetPath, symlinkType(sourceStats, options.platform));
|
|
483
|
+
if (projection.sourceControl === "support") {
|
|
484
|
+
addGitInfoExclude({
|
|
485
|
+
worktreePath: options.worktreePath,
|
|
486
|
+
targetPath,
|
|
487
|
+
isDirectory: sourceStats.isDirectory(),
|
|
488
|
+
gitRunner: options.gitRunner,
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
return dependencyProjectionResult({
|
|
492
|
+
projection,
|
|
493
|
+
sourcePath,
|
|
494
|
+
targetPath,
|
|
495
|
+
status: "linked",
|
|
496
|
+
message: `Linked plugin dependency projection ${sourcePath} -> ${targetPath}`,
|
|
497
|
+
});
|
|
498
|
+
}
|
|
499
|
+
function normalizePluginDependencyProjection(projection) {
|
|
500
|
+
return {
|
|
501
|
+
id: requiredNonEmptyString(projection.id, "plugin dependency projection id"),
|
|
502
|
+
source: requiredNonEmptyString(projection.source, "plugin dependency projection source"),
|
|
503
|
+
target: requiredNonEmptyString(projection.target, "plugin dependency projection target"),
|
|
504
|
+
required: projection.required === true,
|
|
505
|
+
sourceControl: normalizeDependencyProjectionSourceControl(projection.sourceControl, "plugin dependency projection sourceControl"),
|
|
506
|
+
reason: optionalNullableString(projection.reason, "plugin dependency projection reason") ?? null,
|
|
507
|
+
sourceMetadata: normalizePluginDependencyProjectionSourceMetadata(projection.sourceMetadata),
|
|
508
|
+
};
|
|
509
|
+
}
|
|
510
|
+
function normalizeDependencyProjectionSourceControl(value, name) {
|
|
511
|
+
if (value === "support" || value === "source") {
|
|
512
|
+
return value;
|
|
513
|
+
}
|
|
514
|
+
throw new NexusAutomationWorktreeSetupError(`${name} must be support or source`);
|
|
515
|
+
}
|
|
516
|
+
function normalizePluginDependencyProjectionSourceMetadata(sourceMetadata) {
|
|
517
|
+
return {
|
|
518
|
+
pluginId: requiredNonEmptyString(sourceMetadata.pluginId, "plugin dependency projection sourceMetadata.pluginId"),
|
|
519
|
+
pluginName: optionalNullableString(sourceMetadata.pluginName, "plugin dependency projection sourceMetadata.pluginName") ?? null,
|
|
520
|
+
version: optionalNullableString(sourceMetadata.version, "plugin dependency projection sourceMetadata.version") ?? null,
|
|
521
|
+
capabilityId: requiredNonEmptyString(sourceMetadata.capabilityId, "plugin dependency projection sourceMetadata.capabilityId"),
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
function dependencyProjectionResult(options) {
|
|
525
|
+
return {
|
|
526
|
+
id: options.projection.id,
|
|
527
|
+
source: options.projection.source,
|
|
528
|
+
target: options.projection.target,
|
|
529
|
+
sourcePath: options.sourcePath,
|
|
530
|
+
targetPath: options.targetPath,
|
|
531
|
+
required: options.projection.required,
|
|
532
|
+
sourceControl: options.projection.sourceControl,
|
|
533
|
+
reason: options.projection.reason,
|
|
534
|
+
status: options.status,
|
|
535
|
+
message: options.message,
|
|
536
|
+
sourceMetadata: options.projection.sourceMetadata,
|
|
537
|
+
};
|
|
538
|
+
}
|
|
539
|
+
function materializeDependencyLink(options) {
|
|
540
|
+
const sourcePath = resolveInsideRoot(options.sourceRoot, options.link.source, "source");
|
|
541
|
+
const targetPath = resolveInsideRoot(options.worktreePath, options.link.target, "target");
|
|
542
|
+
if (!fs.existsSync(sourcePath)) {
|
|
543
|
+
if (options.link.required) {
|
|
544
|
+
throw new NexusAutomationWorktreeSetupError(`Required dependency link source does not exist: ${sourcePath}`);
|
|
545
|
+
}
|
|
546
|
+
return linkResult({
|
|
547
|
+
link: options.link,
|
|
548
|
+
sourcePath,
|
|
549
|
+
targetPath,
|
|
550
|
+
status: "skipped",
|
|
551
|
+
message: `Optional dependency link source is absent: ${sourcePath}`,
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
if (fs.existsSync(targetPath)) {
|
|
555
|
+
return linkResult({
|
|
556
|
+
link: options.link,
|
|
557
|
+
sourcePath,
|
|
558
|
+
targetPath,
|
|
559
|
+
status: "present",
|
|
560
|
+
message: `Dependency link target already exists: ${targetPath}`,
|
|
561
|
+
});
|
|
562
|
+
}
|
|
563
|
+
const sourceStats = fs.statSync(sourcePath);
|
|
564
|
+
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
|
|
565
|
+
fs.symlinkSync(sourcePath, targetPath, symlinkType(sourceStats, options.platform));
|
|
566
|
+
addGitInfoExclude({
|
|
567
|
+
worktreePath: options.worktreePath,
|
|
568
|
+
targetPath,
|
|
569
|
+
isDirectory: sourceStats.isDirectory(),
|
|
570
|
+
gitRunner: options.gitRunner,
|
|
571
|
+
});
|
|
572
|
+
return linkResult({
|
|
573
|
+
link: options.link,
|
|
574
|
+
sourcePath,
|
|
575
|
+
targetPath,
|
|
576
|
+
status: "linked",
|
|
577
|
+
message: `Linked dependency ${sourcePath} -> ${targetPath}`,
|
|
578
|
+
});
|
|
579
|
+
}
|
|
580
|
+
function addGitInfoExclude(options) {
|
|
581
|
+
const result = options.gitRunner(["rev-parse", "--git-path", "info/exclude"], options.worktreePath);
|
|
582
|
+
if (result.exitCode !== 0) {
|
|
583
|
+
throw new NexusAutomationWorktreeSetupError(`git rev-parse --git-path info/exclude failed: ${result.stderr.trim() || result.stdout.trim()}`);
|
|
584
|
+
}
|
|
585
|
+
const rawPath = result.stdout.trim();
|
|
586
|
+
if (!rawPath) {
|
|
587
|
+
throw new NexusAutomationWorktreeSetupError("git rev-parse --git-path info/exclude returned an empty path");
|
|
588
|
+
}
|
|
589
|
+
const excludePath = path.isAbsolute(rawPath)
|
|
590
|
+
? rawPath
|
|
591
|
+
: path.resolve(options.worktreePath, rawPath);
|
|
592
|
+
const entry = gitExcludeEntry(options.worktreePath, options.targetPath, options.isDirectory);
|
|
593
|
+
fs.mkdirSync(path.dirname(excludePath), { recursive: true });
|
|
594
|
+
const existing = fs.existsSync(excludePath)
|
|
595
|
+
? fs.readFileSync(excludePath, "utf8").split(/\r?\n/u)
|
|
596
|
+
: [];
|
|
597
|
+
if (!existing.includes(entry)) {
|
|
598
|
+
fs.appendFileSync(excludePath, `${entry}\n`, "utf8");
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
function gitExcludeEntry(worktreePath, targetPath, isDirectory) {
|
|
602
|
+
const relative = path.relative(path.resolve(worktreePath), path.resolve(targetPath));
|
|
603
|
+
const normalized = relative.split(path.sep).join("/");
|
|
604
|
+
return isDirectory && !normalized.endsWith("/") ? `${normalized}/` : normalized;
|
|
605
|
+
}
|
|
606
|
+
function symlinkType(sourceStats, platform) {
|
|
607
|
+
if (!sourceStats.isDirectory()) {
|
|
608
|
+
return "file";
|
|
609
|
+
}
|
|
610
|
+
return platform === "win32" ? "junction" : "dir";
|
|
611
|
+
}
|
|
612
|
+
function linkResult(options) {
|
|
613
|
+
return {
|
|
614
|
+
source: options.link.source,
|
|
615
|
+
target: options.link.target,
|
|
616
|
+
sourcePath: options.sourcePath,
|
|
617
|
+
targetPath: options.targetPath,
|
|
618
|
+
required: options.link.required,
|
|
619
|
+
status: options.status,
|
|
620
|
+
message: options.message,
|
|
621
|
+
};
|
|
622
|
+
}
|
|
623
|
+
function resolveInsideRoot(root, value, name) {
|
|
624
|
+
const trimmed = requiredNonEmptyString(value, name);
|
|
625
|
+
if (path.isAbsolute(trimmed)) {
|
|
626
|
+
throw new NexusAutomationWorktreeSetupError(`${name} must be relative: ${trimmed}`);
|
|
627
|
+
}
|
|
628
|
+
const rootPath = path.resolve(root);
|
|
629
|
+
const resolved = path.resolve(rootPath, trimmed);
|
|
630
|
+
const relative = path.relative(rootPath, resolved);
|
|
631
|
+
if (!relative || relative.startsWith("..") || path.isAbsolute(relative)) {
|
|
632
|
+
throw new NexusAutomationWorktreeSetupError(`${name} must resolve inside ${rootPath}: ${trimmed}`);
|
|
633
|
+
}
|
|
634
|
+
return resolved;
|
|
635
|
+
}
|
|
636
|
+
function assertWorktreePathInsideRoot(worktreesRoot, worktreePath) {
|
|
637
|
+
const rootPath = path.resolve(requiredNonEmptyString(worktreesRoot, "worktreesRoot"));
|
|
638
|
+
const resolvedWorktreePath = path.resolve(worktreePath);
|
|
639
|
+
const relative = path.relative(rootPath, resolvedWorktreePath);
|
|
640
|
+
if (!relative || relative.startsWith("..") || path.isAbsolute(relative)) {
|
|
641
|
+
throw new NexusAutomationWorktreeSetupError(`worktreePath must resolve inside worktreesRoot: ${resolvedWorktreePath}`);
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
function optionalNullableString(value, name) {
|
|
645
|
+
if (value === undefined) {
|
|
646
|
+
return undefined;
|
|
647
|
+
}
|
|
648
|
+
if (value === null) {
|
|
649
|
+
return null;
|
|
650
|
+
}
|
|
651
|
+
return requiredNonEmptyString(value, name);
|
|
652
|
+
}
|
|
653
|
+
function requiredNonEmptyString(value, name) {
|
|
654
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
655
|
+
throw new NexusAutomationWorktreeSetupError(`${name} must be a non-empty string`);
|
|
656
|
+
}
|
|
657
|
+
return value.trim();
|
|
658
|
+
}
|
|
659
|
+
function errorMessage(error) {
|
|
660
|
+
return error instanceof Error ? error.message : String(error);
|
|
661
|
+
}
|