@agentxm/workspace-configuration 0.28.4-bootstrap.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +110 -0
- package/dist/src/errors.d.ts +30 -0
- package/dist/src/errors.js +29 -0
- package/dist/src/index.d.ts +18 -0
- package/dist/src/index.js +17 -0
- package/dist/src/initialization-interaction.d.ts +112 -0
- package/dist/src/initialization-interaction.js +62 -0
- package/dist/src/initialization.d.ts +199 -0
- package/dist/src/initialization.js +671 -0
- package/dist/src/inline-mcp.d.ts +29 -0
- package/dist/src/inline-mcp.js +132 -0
- package/dist/src/instruction-reconciliation.d.ts +57 -0
- package/dist/src/instruction-reconciliation.js +93 -0
- package/dist/src/mcp-import-preflight.d.ts +44 -0
- package/dist/src/mcp-import-preflight.js +277 -0
- package/dist/src/mcp-import.d.ts +40 -0
- package/dist/src/mcp-import.js +276 -0
- package/dist/src/membership.d.ts +29 -0
- package/dist/src/membership.js +162 -0
- package/dist/src/testing.d.ts +9 -0
- package/dist/src/testing.js +9 -0
- package/package.json +58 -0
|
@@ -0,0 +1,671 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WorkspaceMutations initialization logic.
|
|
3
|
+
*
|
|
4
|
+
* Handles initial setup of project and user-scope workspaces: agent detection,
|
|
5
|
+
* interactive agent selection, and settings/lockfile creation.
|
|
6
|
+
*
|
|
7
|
+
* @internal
|
|
8
|
+
*/
|
|
9
|
+
import * as FileSystem from "effect/FileSystem";
|
|
10
|
+
import * as Path from "effect/Path";
|
|
11
|
+
import * as Array from "effect/Array";
|
|
12
|
+
import * as Effect from "effect/Effect";
|
|
13
|
+
import * as Layer from "effect/Layer";
|
|
14
|
+
import * as Option from "effect/Option";
|
|
15
|
+
import { CONFIGURABLE_AGENTS_BY_ID } from "@agentxm/extension-model/unstable/agent-capabilities/catalog";
|
|
16
|
+
import { detectAgentScopeResults } from "@agentxm/agent-integration";
|
|
17
|
+
import { AGENTS } from "@agentxm/extension-model/unstable/agents/registry";
|
|
18
|
+
import { isConfigurableAgentId, } from "@agentxm/extension-model/unstable/agents/types";
|
|
19
|
+
import { WorkspaceConfigurationFailed } from "./errors.js";
|
|
20
|
+
import { isGitManaged } from "@agentxm/extension-sources";
|
|
21
|
+
import { LOCKFILE_NAME } from "@agentxm/extension-model/unstable/workspace-files";
|
|
22
|
+
import { LOCKFILE_VERSION, writeLockfileAtPath } from "@agentxm/workspace-state";
|
|
23
|
+
import { createDefaultSettings, writeSettingsAtPath, } from "@agentxm/workspace-state";
|
|
24
|
+
import { makeAbsolutePath } from "@agentxm/extension-model/unstable/path-types";
|
|
25
|
+
import { AgentRootResolverLive } from "@agentxm/workspace-state";
|
|
26
|
+
import { makeWorkspaceReadModel, WorkspaceReadModelConfig } from "@agentxm/workspace-state";
|
|
27
|
+
import { WorkspaceInitializationInteraction, } from "./initialization-interaction.js";
|
|
28
|
+
import { locateWorkspace, resolveUserHome } from "@agentxm/workspace-state";
|
|
29
|
+
import { setupScopeSupport } from "@agentxm/workspace-state";
|
|
30
|
+
import { protectWorkspacePath } from "@agentxm/workspace-state";
|
|
31
|
+
import { LOCK_FILENAME } from "@agentxm/workspace-state";
|
|
32
|
+
import { SETTINGS_FILENAME } from "@agentxm/extension-model/unstable/workspace-files";
|
|
33
|
+
import { resolveInstructionTarget, syncInstructions } from "@agentxm/extension-workspace";
|
|
34
|
+
const SELECT_AGENTS_PROMPT_MISSING = new WorkspaceConfigurationFailed({
|
|
35
|
+
category: "usage",
|
|
36
|
+
detail: "Interactive prompt required: Select agents to configure",
|
|
37
|
+
suggestions: [{ description: "Provide WorkspaceInitializationInteraction in the runtime." }],
|
|
38
|
+
});
|
|
39
|
+
const DEFAULT_INSTRUCTIONS_FILE = "AGENTS.md";
|
|
40
|
+
const DEFAULT_INSTRUCTIONS_GITIGNORE = true;
|
|
41
|
+
const POPULAR_AGENT_IDS = [
|
|
42
|
+
"claude-code",
|
|
43
|
+
"codex",
|
|
44
|
+
"cursor",
|
|
45
|
+
"github-copilot-cli",
|
|
46
|
+
"opencode",
|
|
47
|
+
];
|
|
48
|
+
const INSTRUCTION_SOURCE_CANDIDATES = [
|
|
49
|
+
DEFAULT_INSTRUCTIONS_FILE,
|
|
50
|
+
"CLAUDE.md",
|
|
51
|
+
"GEMINI.md",
|
|
52
|
+
"QWEN.md",
|
|
53
|
+
"replit.md",
|
|
54
|
+
".cursorrules",
|
|
55
|
+
];
|
|
56
|
+
const isKnownAgentId = (id) => Object.hasOwn(AGENTS, id);
|
|
57
|
+
const isKnownConfigurableAgentId = (id) => isKnownAgentId(id) && isConfigurableAgentId(id);
|
|
58
|
+
const isAutoSelectableAgent = (agent) => agent.id === "universal" || CONFIGURABLE_AGENTS_BY_ID[agent.id].lifecycle.state !== "retired";
|
|
59
|
+
const allAgentDescriptors = (preferredIds) => {
|
|
60
|
+
const preferred = preferredIds.flatMap((id) => isKnownConfigurableAgentId(id) ? [AGENTS[id]] : []);
|
|
61
|
+
const preferredSet = new Set(preferred.map((agent) => agent.id));
|
|
62
|
+
const remaining = Object.values(AGENTS).filter((agent) => isConfigurableAgentId(agent.id) && !preferredSet.has(agent.id));
|
|
63
|
+
return [...preferred, ...remaining];
|
|
64
|
+
};
|
|
65
|
+
const setupAgentCandidates = (args) => {
|
|
66
|
+
const selectedIds = new Set(args.selectedAgents.map((agent) => agent.id));
|
|
67
|
+
const suggestedIds = new Set(args.suggestedIds);
|
|
68
|
+
const detectionsById = new Map(args.detections.map((detection) => [detection.agent.id, detection]));
|
|
69
|
+
const relevantIds = [
|
|
70
|
+
...args.selectedAgents.map((agent) => agent.id),
|
|
71
|
+
...args.detections.map((detection) => detection.agent.id),
|
|
72
|
+
...args.suggestedIds,
|
|
73
|
+
];
|
|
74
|
+
return [...new Set(relevantIds)].flatMap((id) => {
|
|
75
|
+
if (!isKnownConfigurableAgentId(id))
|
|
76
|
+
return [];
|
|
77
|
+
const agent = AGENTS[id];
|
|
78
|
+
const detection = detectionsById.get(id);
|
|
79
|
+
const projectDetected = detection?.project ?? false;
|
|
80
|
+
const userDetected = detection?.user ?? false;
|
|
81
|
+
const retired = !isAutoSelectableAgent(agent);
|
|
82
|
+
const selected = selectedIds.has(id);
|
|
83
|
+
const selectionReason = selected
|
|
84
|
+
? args.explicit
|
|
85
|
+
? "explicit"
|
|
86
|
+
: args.scope === "project" && projectDetected
|
|
87
|
+
? "project-detected"
|
|
88
|
+
: args.scope === "user" && userDetected
|
|
89
|
+
? "user-detected"
|
|
90
|
+
: suggestedIds.has(id)
|
|
91
|
+
? "catalog-suggestion"
|
|
92
|
+
: undefined
|
|
93
|
+
: undefined;
|
|
94
|
+
return [
|
|
95
|
+
{
|
|
96
|
+
id,
|
|
97
|
+
name: agent.name,
|
|
98
|
+
projectDetected,
|
|
99
|
+
userDetected,
|
|
100
|
+
state: retired
|
|
101
|
+
? "retired"
|
|
102
|
+
: selected
|
|
103
|
+
? "selected"
|
|
104
|
+
: suggestedIds.has(id)
|
|
105
|
+
? "suggested"
|
|
106
|
+
: "available",
|
|
107
|
+
...(selectionReason === undefined ? {} : { selectionReason }),
|
|
108
|
+
},
|
|
109
|
+
];
|
|
110
|
+
});
|
|
111
|
+
};
|
|
112
|
+
const DEFAULT_SETUP_SKILLS = {
|
|
113
|
+
axm: {
|
|
114
|
+
source: "workspace",
|
|
115
|
+
enabled: true,
|
|
116
|
+
origin: "bundled",
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
const instructionValueFromSettings = (settings) => settings.instructionFiles;
|
|
120
|
+
const currentInstructionFileName = (settings) => {
|
|
121
|
+
const value = instructionValueFromSettings(settings);
|
|
122
|
+
if (value === undefined || value === false)
|
|
123
|
+
return DEFAULT_INSTRUCTIONS_FILE;
|
|
124
|
+
return value.fileName ?? DEFAULT_INSTRUCTIONS_FILE;
|
|
125
|
+
};
|
|
126
|
+
const currentInstructionSyncEnabled = (settings) => {
|
|
127
|
+
const value = instructionValueFromSettings(settings);
|
|
128
|
+
return value !== false;
|
|
129
|
+
};
|
|
130
|
+
const readFileOption = (filePath) => Effect.gen(function* () {
|
|
131
|
+
const fs = yield* FileSystem.FileSystem;
|
|
132
|
+
return yield* fs.readFileString(filePath).pipe(Effect.option);
|
|
133
|
+
});
|
|
134
|
+
const fileExists = (filePath) => Effect.gen(function* () {
|
|
135
|
+
const fs = yield* FileSystem.FileSystem;
|
|
136
|
+
return yield* fs.exists(filePath).pipe(Effect.catch(() => Effect.succeed(false)));
|
|
137
|
+
});
|
|
138
|
+
const WORKSPACE_TRANSIENT_GITIGNORE_LINES = ["/.axm/", "*.axm-staging/", "*.axm-backup/"];
|
|
139
|
+
const newlineFor = (content) => content.includes("\r\n") ? "\r\n" : content.includes("\r") ? "\r" : "\n";
|
|
140
|
+
const ensureWorkspaceTransientIgnores = (workspaceRoot) => Effect.gen(function* () {
|
|
141
|
+
const fs = yield* FileSystem.FileSystem;
|
|
142
|
+
const path = yield* Path.Path;
|
|
143
|
+
if (!(yield* isGitManaged(workspaceRoot)))
|
|
144
|
+
return;
|
|
145
|
+
const filePath = path.join(workspaceRoot, ".gitignore");
|
|
146
|
+
const exists = yield* fileExists(filePath);
|
|
147
|
+
const current = exists
|
|
148
|
+
? yield* fs.readFileString(filePath).pipe(Effect.mapError((cause) => new WorkspaceConfigurationFailed({
|
|
149
|
+
category: "internal",
|
|
150
|
+
detail: `Failed to read AXM workspace ignore file: ${filePath}`,
|
|
151
|
+
cause,
|
|
152
|
+
})))
|
|
153
|
+
: "";
|
|
154
|
+
const newline = newlineFor(current);
|
|
155
|
+
const existing = new Set(current.split(/\r\n|\r|\n/u));
|
|
156
|
+
const missing = WORKSPACE_TRANSIENT_GITIGNORE_LINES.filter((line) => !existing.has(line));
|
|
157
|
+
if (missing.length === 0)
|
|
158
|
+
return;
|
|
159
|
+
const hasTrailingNewline = current.endsWith("\n") || current.endsWith("\r");
|
|
160
|
+
const prefix = current.length === 0 || hasTrailingNewline ? current : `${current}${newline}`;
|
|
161
|
+
yield* protectWorkspacePath(filePath);
|
|
162
|
+
yield* fs.writeFileString(filePath, `${prefix}${missing.join(newline)}${newline}`).pipe(Effect.mapError((cause) => new WorkspaceConfigurationFailed({
|
|
163
|
+
category: "internal",
|
|
164
|
+
detail: `Failed to write AXM workspace ignore file: ${filePath}`,
|
|
165
|
+
cause,
|
|
166
|
+
})));
|
|
167
|
+
});
|
|
168
|
+
const lineCount = (content) => {
|
|
169
|
+
if (content.length === 0)
|
|
170
|
+
return 0;
|
|
171
|
+
return content.split(/\r\n|\r|\n/).length;
|
|
172
|
+
};
|
|
173
|
+
const instructionSourceChoices = (workspaceRoot, defaultFileName) => Effect.gen(function* () {
|
|
174
|
+
const path = yield* Path.Path;
|
|
175
|
+
const names = [
|
|
176
|
+
defaultFileName,
|
|
177
|
+
...INSTRUCTION_SOURCE_CANDIDATES.filter((candidate) => candidate !== defaultFileName),
|
|
178
|
+
];
|
|
179
|
+
const uniqueNames = [...new Set(names)];
|
|
180
|
+
return yield* Effect.forEach(uniqueNames, (fileName) => Effect.gen(function* () {
|
|
181
|
+
const content = yield* readFileOption(path.join(workspaceRoot, fileName));
|
|
182
|
+
return {
|
|
183
|
+
fileName,
|
|
184
|
+
exists: Option.isSome(content),
|
|
185
|
+
lines: Option.match(content, { onNone: () => 0, onSome: lineCount }),
|
|
186
|
+
content,
|
|
187
|
+
};
|
|
188
|
+
}), { concurrency: "unbounded" });
|
|
189
|
+
});
|
|
190
|
+
const richestExistingInstructionFile = (choices) => {
|
|
191
|
+
const existing = choices.filter((choice) => Option.isSome(choice.content));
|
|
192
|
+
if (existing.length === 0)
|
|
193
|
+
return Option.none();
|
|
194
|
+
const ranked = [...existing].sort((a, b) => b.lines - a.lines);
|
|
195
|
+
const first = ranked[0];
|
|
196
|
+
return first === undefined ? Option.none() : Option.some(first);
|
|
197
|
+
};
|
|
198
|
+
const sourceContentForApply = (args) => {
|
|
199
|
+
const selected = args.choices.find((choice) => choice.fileName === args.selectedFileName);
|
|
200
|
+
if (selected !== undefined && Option.isSome(selected.content))
|
|
201
|
+
return Option.none();
|
|
202
|
+
const richest = richestExistingInstructionFile(args.choices);
|
|
203
|
+
return Option.match(richest, {
|
|
204
|
+
onNone: () => Option.some(""),
|
|
205
|
+
onSome: (choice) => choice.content,
|
|
206
|
+
});
|
|
207
|
+
};
|
|
208
|
+
const writeSourceFileIfMissing = (args) => Effect.gen(function* () {
|
|
209
|
+
if (Option.isNone(args.content))
|
|
210
|
+
return Option.none();
|
|
211
|
+
const path = yield* Path.Path;
|
|
212
|
+
const fs = yield* FileSystem.FileSystem;
|
|
213
|
+
const filePath = path.join(args.workspaceRoot, args.fileName);
|
|
214
|
+
const exists = yield* fileExists(filePath);
|
|
215
|
+
if (exists)
|
|
216
|
+
return Option.none();
|
|
217
|
+
if (!args.dryRun) {
|
|
218
|
+
yield* protectWorkspacePath(filePath);
|
|
219
|
+
yield* fs.makeDirectory(path.dirname(filePath), { recursive: true }).pipe(Effect.mapError((error) => new WorkspaceConfigurationFailed({
|
|
220
|
+
category: "internal",
|
|
221
|
+
detail: `Failed to create instruction source directory: ${path.dirname(filePath)}`,
|
|
222
|
+
cause: error,
|
|
223
|
+
})));
|
|
224
|
+
yield* fs.writeFileString(filePath, args.content.value).pipe(Effect.mapError((error) => new WorkspaceConfigurationFailed({
|
|
225
|
+
category: "internal",
|
|
226
|
+
detail: `Failed to write instruction source file: ${filePath}`,
|
|
227
|
+
cause: error,
|
|
228
|
+
})));
|
|
229
|
+
}
|
|
230
|
+
return Option.some(filePath);
|
|
231
|
+
});
|
|
232
|
+
const instructionMechanismLabel = (mechanism) => {
|
|
233
|
+
switch (mechanism) {
|
|
234
|
+
case "native":
|
|
235
|
+
return "in sync (native)";
|
|
236
|
+
case "symlink":
|
|
237
|
+
return "write ← symlink";
|
|
238
|
+
case "copy":
|
|
239
|
+
return "write ← copy";
|
|
240
|
+
case "adapter":
|
|
241
|
+
case "none":
|
|
242
|
+
return "unsupported";
|
|
243
|
+
}
|
|
244
|
+
};
|
|
245
|
+
const instructionPlanRows = (args) => {
|
|
246
|
+
const rows = [
|
|
247
|
+
{
|
|
248
|
+
target: args.sourceFileName,
|
|
249
|
+
action: args.sourceWillBeCreated ? "create" : "in sync",
|
|
250
|
+
detail: Option.match(args.sourceSeed, {
|
|
251
|
+
onNone: () => "source",
|
|
252
|
+
onSome: (choice) => `seeded from ${choice.fileName}`,
|
|
253
|
+
}),
|
|
254
|
+
},
|
|
255
|
+
...args.selectedAgents.map((agent) => {
|
|
256
|
+
const resolution = resolveInstructionTarget({
|
|
257
|
+
instructions: agent.instructions,
|
|
258
|
+
sourceFileName: args.sourceFileName,
|
|
259
|
+
symlinkSupported: true,
|
|
260
|
+
});
|
|
261
|
+
if (resolution.action === "skip") {
|
|
262
|
+
return {
|
|
263
|
+
target: agent.name,
|
|
264
|
+
action: "skip",
|
|
265
|
+
detail: "no instruction convention",
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
return {
|
|
269
|
+
target: resolution.relativeTarget,
|
|
270
|
+
action: instructionMechanismLabel(resolution.mechanism),
|
|
271
|
+
detail: agent.name,
|
|
272
|
+
};
|
|
273
|
+
}),
|
|
274
|
+
];
|
|
275
|
+
return rows;
|
|
276
|
+
};
|
|
277
|
+
const selectSetupAgents = (args) => Effect.gen(function* () {
|
|
278
|
+
const nonInteractive = args.options.nonInteractive === true;
|
|
279
|
+
const interaction = yield* Effect.serviceOption(WorkspaceInitializationInteraction);
|
|
280
|
+
const requested = args.options.agents;
|
|
281
|
+
if (requested !== undefined && requested.length > 0) {
|
|
282
|
+
const unrecognized = requested.filter((id) => !isKnownConfigurableAgentId(id));
|
|
283
|
+
if (unrecognized.length > 0) {
|
|
284
|
+
const label = unrecognized.length === 1 ? "agent" : "agents";
|
|
285
|
+
return yield* new WorkspaceConfigurationFailed({
|
|
286
|
+
category: "validation",
|
|
287
|
+
detail: `Unrecognized setup ${label}: ${unrecognized.join(", ")}`,
|
|
288
|
+
suggestions: [{ description: "Show available setup agents.", cmd: "axm setup --help" }],
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
const detections = yield* detectAgentScopeResults(args.workspaceRoot).pipe(Effect.mapError((error) => new WorkspaceConfigurationFailed({
|
|
293
|
+
category: "internal",
|
|
294
|
+
detail: `Failed to detect agents: ${error.message}`,
|
|
295
|
+
cause: error,
|
|
296
|
+
})));
|
|
297
|
+
const detectedAgents = detections.map((detection) => detection.agent);
|
|
298
|
+
const autoSelectableAgents = detectedAgents.filter(isAutoSelectableAgent);
|
|
299
|
+
const retiredDetectedAgents = detectedAgents.filter((agent) => !isAutoSelectableAgent(agent));
|
|
300
|
+
const detectedIds = Array.map(autoSelectableAgents, (agent) => agent.id);
|
|
301
|
+
const projectDetectedIds = detections.flatMap(({ agent, project }) => project && isAutoSelectableAgent(agent) ? [agent.id] : []);
|
|
302
|
+
const userDetectedIds = detections.flatMap(({ agent, user }) => user && isAutoSelectableAgent(agent) ? [agent.id] : []);
|
|
303
|
+
if (requested !== undefined && requested.length > 0) {
|
|
304
|
+
const selected = requested.flatMap((id) => isKnownConfigurableAgentId(id) ? [AGENTS[id]] : []);
|
|
305
|
+
return {
|
|
306
|
+
selectedAgents: selected,
|
|
307
|
+
candidates: setupAgentCandidates({
|
|
308
|
+
detections,
|
|
309
|
+
selectedAgents: selected,
|
|
310
|
+
suggestedIds: [],
|
|
311
|
+
explicit: true,
|
|
312
|
+
scope: args.options.scope,
|
|
313
|
+
}),
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
if (Option.isSome(interaction)) {
|
|
317
|
+
yield* interaction.value.presentAgentScan({
|
|
318
|
+
detectedCount: detectedAgents.length,
|
|
319
|
+
retiredAgents: retiredDetectedAgents.map((agent) => ({
|
|
320
|
+
id: agent.id,
|
|
321
|
+
name: agent.name,
|
|
322
|
+
})),
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
const configuredIds = args.existingSettings.agents ?? [];
|
|
326
|
+
const strongDetectedIds = args.options.scope === "project" ? projectDetectedIds : userDetectedIds;
|
|
327
|
+
const suggestedIds = strongDetectedIds.length === 0 && configuredIds.length === 0 ? [...POPULAR_AGENT_IDS] : [];
|
|
328
|
+
const preferredIds = [...configuredIds, ...strongDetectedIds, ...detectedIds, ...suggestedIds];
|
|
329
|
+
const defaultIds = [...new Set([...configuredIds, ...strongDetectedIds, ...suggestedIds])];
|
|
330
|
+
if (nonInteractive || args.options.yes === true || args.options.preview === true) {
|
|
331
|
+
const selectedAgents = defaultIds.flatMap((id) => isKnownConfigurableAgentId(id) ? [AGENTS[id]] : []);
|
|
332
|
+
return {
|
|
333
|
+
selectedAgents,
|
|
334
|
+
candidates: setupAgentCandidates({
|
|
335
|
+
detections,
|
|
336
|
+
selectedAgents,
|
|
337
|
+
suggestedIds,
|
|
338
|
+
explicit: false,
|
|
339
|
+
scope: args.options.scope,
|
|
340
|
+
}),
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
const selectedIds = Option.isSome(interaction)
|
|
344
|
+
? yield* interaction.value.selectAgents({
|
|
345
|
+
allAgents: allAgentDescriptors(preferredIds),
|
|
346
|
+
detectedIds,
|
|
347
|
+
projectDetectedIds,
|
|
348
|
+
userDetectedIds,
|
|
349
|
+
suggestedIds,
|
|
350
|
+
configuredIds,
|
|
351
|
+
})
|
|
352
|
+
: yield* SELECT_AGENTS_PROMPT_MISSING;
|
|
353
|
+
const selectedAgents = selectedIds.flatMap((id) => isKnownConfigurableAgentId(id) ? [AGENTS[id]] : []);
|
|
354
|
+
return {
|
|
355
|
+
selectedAgents,
|
|
356
|
+
candidates: setupAgentCandidates({
|
|
357
|
+
detections,
|
|
358
|
+
selectedAgents,
|
|
359
|
+
suggestedIds,
|
|
360
|
+
explicit: false,
|
|
361
|
+
scope: args.options.scope,
|
|
362
|
+
}),
|
|
363
|
+
};
|
|
364
|
+
});
|
|
365
|
+
const resolveInstructionSetup = (args) => Effect.gen(function* () {
|
|
366
|
+
const nonInteractive = args.options.nonInteractive === true;
|
|
367
|
+
const interaction = yield* Effect.serviceOption(WorkspaceInitializationInteraction);
|
|
368
|
+
const defaultSyncEnabled = currentInstructionSyncEnabled(args.existingSettings);
|
|
369
|
+
const syncEnabled = nonInteractive || args.options.yes === true
|
|
370
|
+
? true
|
|
371
|
+
: Option.isSome(interaction)
|
|
372
|
+
? yield* interaction.value.confirmInstructionSync({ enabled: defaultSyncEnabled })
|
|
373
|
+
: defaultSyncEnabled;
|
|
374
|
+
const defaultFileName = currentInstructionFileName(args.existingSettings);
|
|
375
|
+
const choices = yield* instructionSourceChoices(args.workspaceRoot, defaultFileName);
|
|
376
|
+
const selectedFileName = syncEnabled && !nonInteractive && args.options.yes !== true && Option.isSome(interaction)
|
|
377
|
+
? yield* interaction.value.selectInstructionSource({
|
|
378
|
+
defaultFileName,
|
|
379
|
+
choices: choices.map(({ fileName, exists, lines }) => ({ fileName, exists, lines })),
|
|
380
|
+
})
|
|
381
|
+
: defaultFileName;
|
|
382
|
+
return {
|
|
383
|
+
enabled: syncEnabled,
|
|
384
|
+
fileName: selectedFileName.trim().length > 0 ? selectedFileName.trim() : defaultFileName,
|
|
385
|
+
choices,
|
|
386
|
+
};
|
|
387
|
+
});
|
|
388
|
+
const applyProjectSetup = (args) => Effect.gen(function* () {
|
|
389
|
+
if (args.dryRun)
|
|
390
|
+
return;
|
|
391
|
+
const path = yield* Path.Path;
|
|
392
|
+
const settingsPath = path.join(args.workspaceRoot, SETTINGS_FILENAME);
|
|
393
|
+
const lockfilePath = path.join(args.workspaceRoot, LOCK_FILENAME);
|
|
394
|
+
yield* writeSettingsAtPath(settingsPath, args.settings);
|
|
395
|
+
const lockfileExists = yield* fileExists(lockfilePath);
|
|
396
|
+
if (!lockfileExists) {
|
|
397
|
+
yield* protectWorkspacePath(lockfilePath);
|
|
398
|
+
yield* writeLockfileAtPath(lockfilePath, {
|
|
399
|
+
lockfileVersion: LOCKFILE_VERSION,
|
|
400
|
+
skills: {},
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
yield* ensureWorkspaceTransientIgnores(args.workspaceRoot);
|
|
404
|
+
if (!args.syncInstructions)
|
|
405
|
+
return;
|
|
406
|
+
yield* writeSourceFileIfMissing({
|
|
407
|
+
workspaceRoot: args.workspaceRoot,
|
|
408
|
+
fileName: args.sourceFileName,
|
|
409
|
+
content: args.sourceContent,
|
|
410
|
+
dryRun: args.dryRun,
|
|
411
|
+
});
|
|
412
|
+
yield* syncInstructions({
|
|
413
|
+
workspaceRoot: args.workspaceRoot,
|
|
414
|
+
scope: "project",
|
|
415
|
+
configuredAgents: args.settings.agents ?? [],
|
|
416
|
+
config: {
|
|
417
|
+
fileName: args.sourceFileName,
|
|
418
|
+
gitignoreAliases: DEFAULT_INSTRUCTIONS_GITIGNORE,
|
|
419
|
+
},
|
|
420
|
+
dryRun: args.dryRun,
|
|
421
|
+
});
|
|
422
|
+
});
|
|
423
|
+
const readSettingsFromReadModel = (scope, projectRoot, userHome) => Effect.gen(function* () {
|
|
424
|
+
const fs = yield* FileSystem.FileSystem;
|
|
425
|
+
const path = yield* Path.Path;
|
|
426
|
+
const platformLayer = Layer.mergeAll(Layer.succeed(FileSystem.FileSystem, fs), Layer.succeed(Path.Path, path));
|
|
427
|
+
const env = Layer.mergeAll(platformLayer, Layer.succeed(WorkspaceReadModelConfig, {
|
|
428
|
+
projectRoot: makeAbsolutePath(path, projectRoot),
|
|
429
|
+
userHome: makeAbsolutePath(path, userHome),
|
|
430
|
+
allowedRoot: makeAbsolutePath(path, "/"),
|
|
431
|
+
}), AgentRootResolverLive.pipe(Layer.provide(platformLayer)));
|
|
432
|
+
return yield* makeWorkspaceReadModel(scope).pipe(Effect.flatMap((readModel) => readModel.state.settings), Effect.provide(env), Effect.mapError((error) => new WorkspaceConfigurationFailed({
|
|
433
|
+
category: "validation",
|
|
434
|
+
detail: "Workspace settings could not be read",
|
|
435
|
+
cause: error,
|
|
436
|
+
})));
|
|
437
|
+
});
|
|
438
|
+
/**
|
|
439
|
+
* Initialize project workspace by detecting and selecting agents.
|
|
440
|
+
*
|
|
441
|
+
* @param localDir - Path to local .axm directory
|
|
442
|
+
* @param options - WorkspaceMutations options
|
|
443
|
+
* @returns Effect yielding selected agent IDs
|
|
444
|
+
*/
|
|
445
|
+
const configureProjectWorkspace = (args) => Effect.gen(function* () {
|
|
446
|
+
const path = yield* Path.Path;
|
|
447
|
+
const workspaceRoot = path.dirname(args.localDir);
|
|
448
|
+
const nonInteractive = args.options.nonInteractive === true;
|
|
449
|
+
const selection = yield* selectSetupAgents({
|
|
450
|
+
options: args.options,
|
|
451
|
+
existingSettings: args.existingSettings,
|
|
452
|
+
workspaceRoot,
|
|
453
|
+
});
|
|
454
|
+
const selectedAgents = selection.selectedAgents;
|
|
455
|
+
const instructionSetup = yield* resolveInstructionSetup({
|
|
456
|
+
options: args.options,
|
|
457
|
+
existingSettings: args.existingSettings,
|
|
458
|
+
workspaceRoot,
|
|
459
|
+
});
|
|
460
|
+
const agentIds = selectedAgents.flatMap((agent) => isConfigurableAgentId(agent.id) ? [agent.id] : []);
|
|
461
|
+
const settings = {
|
|
462
|
+
...args.existingSettings,
|
|
463
|
+
agents: agentIds,
|
|
464
|
+
skills: args.existingSettings.skills ?? DEFAULT_SETUP_SKILLS,
|
|
465
|
+
instructionFiles: instructionSetup.enabled
|
|
466
|
+
? {
|
|
467
|
+
fileName: instructionSetup.fileName,
|
|
468
|
+
gitignoreAliases: DEFAULT_INSTRUCTIONS_GITIGNORE,
|
|
469
|
+
}
|
|
470
|
+
: false,
|
|
471
|
+
};
|
|
472
|
+
const sourceContent = sourceContentForApply({
|
|
473
|
+
selectedFileName: instructionSetup.fileName,
|
|
474
|
+
choices: instructionSetup.choices,
|
|
475
|
+
});
|
|
476
|
+
const sourceSeed = Option.isSome(sourceContent)
|
|
477
|
+
? richestExistingInstructionFile(instructionSetup.choices)
|
|
478
|
+
: Option.none();
|
|
479
|
+
const sourceWillBeCreated = Option.isSome(sourceContent);
|
|
480
|
+
const gitManaged = yield* isGitManaged(workspaceRoot);
|
|
481
|
+
const planRows = instructionSetup.enabled
|
|
482
|
+
? instructionPlanRows({
|
|
483
|
+
selectedAgents,
|
|
484
|
+
sourceFileName: instructionSetup.fileName,
|
|
485
|
+
sourceWillBeCreated,
|
|
486
|
+
sourceSeed,
|
|
487
|
+
})
|
|
488
|
+
: [
|
|
489
|
+
{
|
|
490
|
+
target: "instructionFiles",
|
|
491
|
+
action: "skip",
|
|
492
|
+
detail: "instructions disabled",
|
|
493
|
+
},
|
|
494
|
+
];
|
|
495
|
+
const interaction = yield* Effect.serviceOption(WorkspaceInitializationInteraction);
|
|
496
|
+
if (Option.isSome(interaction) && (!nonInteractive || args.options.preview === true)) {
|
|
497
|
+
yield* interaction.value.presentSetupPlan([
|
|
498
|
+
{
|
|
499
|
+
target: SETTINGS_FILENAME,
|
|
500
|
+
action: "create",
|
|
501
|
+
detail: `agents: ${agentIds.join(", ")}`,
|
|
502
|
+
},
|
|
503
|
+
...(gitManaged
|
|
504
|
+
? [
|
|
505
|
+
{
|
|
506
|
+
target: ".gitignore",
|
|
507
|
+
action: "update",
|
|
508
|
+
detail: "AXM runtime and package transaction artifacts",
|
|
509
|
+
},
|
|
510
|
+
]
|
|
511
|
+
: []),
|
|
512
|
+
...planRows,
|
|
513
|
+
]);
|
|
514
|
+
if (args.options.preview !== true) {
|
|
515
|
+
yield* interaction.value.presentScopeSupport(args.options.scope, setupScopeSupport(agentIds, args.options.scope));
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
const confirmed = args.options.preview === true ||
|
|
519
|
+
args.options.yes === true ||
|
|
520
|
+
nonInteractive ||
|
|
521
|
+
Option.isNone(interaction)
|
|
522
|
+
? true
|
|
523
|
+
: yield* interaction.value.confirmSetupPlan();
|
|
524
|
+
if (!confirmed) {
|
|
525
|
+
return {
|
|
526
|
+
settings: args.existingSettings,
|
|
527
|
+
agentCandidates: selection.candidates,
|
|
528
|
+
confirmed: false,
|
|
529
|
+
};
|
|
530
|
+
}
|
|
531
|
+
yield* applyProjectSetup({
|
|
532
|
+
localDir: args.localDir,
|
|
533
|
+
workspaceRoot,
|
|
534
|
+
settings,
|
|
535
|
+
sourceFileName: instructionSetup.fileName,
|
|
536
|
+
sourceContent,
|
|
537
|
+
syncInstructions: instructionSetup.enabled,
|
|
538
|
+
dryRun: args.options.preview ?? false,
|
|
539
|
+
});
|
|
540
|
+
return { settings, agentCandidates: selection.candidates, confirmed: true };
|
|
541
|
+
});
|
|
542
|
+
export const initializeProjectWorkspace = (localDir, options) => configureProjectWorkspace({
|
|
543
|
+
localDir,
|
|
544
|
+
options,
|
|
545
|
+
existingSettings: createDefaultSettings(),
|
|
546
|
+
});
|
|
547
|
+
const initializeUserWorkspace = (workspaceRoot, options) => Effect.gen(function* () {
|
|
548
|
+
const path = yield* Path.Path;
|
|
549
|
+
const selection = yield* selectSetupAgents({
|
|
550
|
+
options,
|
|
551
|
+
existingSettings: createDefaultSettings(),
|
|
552
|
+
workspaceRoot: options.projectRoot,
|
|
553
|
+
});
|
|
554
|
+
const selectedAgents = selection.selectedAgents;
|
|
555
|
+
const agentIds = selectedAgents.flatMap((agent) => isConfigurableAgentId(agent.id) ? [agent.id] : []);
|
|
556
|
+
const settings = {
|
|
557
|
+
agents: agentIds,
|
|
558
|
+
skills: DEFAULT_SETUP_SKILLS,
|
|
559
|
+
};
|
|
560
|
+
const nonInteractive = options.nonInteractive === true;
|
|
561
|
+
const interaction = yield* Effect.serviceOption(WorkspaceInitializationInteraction);
|
|
562
|
+
if (Option.isSome(interaction) && (!nonInteractive || options.preview === true)) {
|
|
563
|
+
yield* interaction.value.presentSetupPlan([
|
|
564
|
+
{
|
|
565
|
+
target: SETTINGS_FILENAME,
|
|
566
|
+
action: "create",
|
|
567
|
+
detail: `agents: ${agentIds.join(", ")}`,
|
|
568
|
+
},
|
|
569
|
+
{
|
|
570
|
+
target: LOCKFILE_NAME,
|
|
571
|
+
action: "create",
|
|
572
|
+
detail: "accepted resolution",
|
|
573
|
+
},
|
|
574
|
+
]);
|
|
575
|
+
if (options.preview !== true) {
|
|
576
|
+
yield* interaction.value.presentScopeSupport(options.scope, setupScopeSupport(agentIds, options.scope));
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
const confirmed = options.preview === true ||
|
|
580
|
+
options.yes === true ||
|
|
581
|
+
nonInteractive ||
|
|
582
|
+
Option.isNone(interaction)
|
|
583
|
+
? true
|
|
584
|
+
: yield* interaction.value.confirmSetupPlan();
|
|
585
|
+
if (!confirmed) {
|
|
586
|
+
return {
|
|
587
|
+
settings: createDefaultSettings(),
|
|
588
|
+
agentCandidates: selection.candidates,
|
|
589
|
+
confirmed: false,
|
|
590
|
+
};
|
|
591
|
+
}
|
|
592
|
+
if (options.preview !== true) {
|
|
593
|
+
const settingsPath = path.join(workspaceRoot, SETTINGS_FILENAME);
|
|
594
|
+
const lockPath = path.join(workspaceRoot, LOCKFILE_NAME);
|
|
595
|
+
yield* protectWorkspacePath(lockPath);
|
|
596
|
+
yield* writeSettingsAtPath(settingsPath, settings);
|
|
597
|
+
yield* writeLockfileAtPath(lockPath, { lockfileVersion: LOCKFILE_VERSION, skills: {} });
|
|
598
|
+
}
|
|
599
|
+
return { settings, agentCandidates: selection.candidates, confirmed: true };
|
|
600
|
+
});
|
|
601
|
+
const workspaceInitializationState = (settings, initialized, wouldInitialize, agentCandidates = [], cancelled = false) => ({
|
|
602
|
+
settings,
|
|
603
|
+
initialized,
|
|
604
|
+
wouldInitialize,
|
|
605
|
+
cancelled,
|
|
606
|
+
agentCandidates,
|
|
607
|
+
});
|
|
608
|
+
/**
|
|
609
|
+
* Ensure the user workspace has axm.json and axm-lock.yaml.
|
|
610
|
+
*
|
|
611
|
+
* Creates missing files with empty defaults.
|
|
612
|
+
*
|
|
613
|
+
* @param workspaceRoot - Path to the user workspace root
|
|
614
|
+
*/
|
|
615
|
+
export const ensureUserWorkspaceInitialized = (workspaceRoot, options) => Effect.gen(function* () {
|
|
616
|
+
const fs = yield* FileSystem.FileSystem;
|
|
617
|
+
const path = yield* Path.Path;
|
|
618
|
+
const settingsPath = path.join(workspaceRoot, SETTINGS_FILENAME);
|
|
619
|
+
const settingsExists = yield* fs.exists(settingsPath).pipe(Effect.mapError((error) => new WorkspaceConfigurationFailed({
|
|
620
|
+
category: "validation",
|
|
621
|
+
detail: `Failed to check if settings file exists: ${settingsPath}`,
|
|
622
|
+
cause: error,
|
|
623
|
+
})));
|
|
624
|
+
if (!settingsExists) {
|
|
625
|
+
const initialization = yield* initializeUserWorkspace(workspaceRoot, options);
|
|
626
|
+
if (!initialization.confirmed) {
|
|
627
|
+
return workspaceInitializationState(initialization.settings, false, false, initialization.agentCandidates, true);
|
|
628
|
+
}
|
|
629
|
+
return workspaceInitializationState(initialization.settings, options.preview !== true, options.preview === true, initialization.agentCandidates);
|
|
630
|
+
}
|
|
631
|
+
const userHome = yield* resolveUserHome();
|
|
632
|
+
const settings = yield* readSettingsFromReadModel("user", options.projectRoot, userHome).pipe(Effect.map(Option.getOrElse(() => createDefaultSettings())));
|
|
633
|
+
return workspaceInitializationState(settings, false, false);
|
|
634
|
+
});
|
|
635
|
+
/**
|
|
636
|
+
* Ensure project workspace is initialized, returning local settings.
|
|
637
|
+
*
|
|
638
|
+
* Reads existing local settings or runs the initialization flow when missing.
|
|
639
|
+
*
|
|
640
|
+
* @param localDir - Path to local .axm directory
|
|
641
|
+
* @param options - WorkspaceMutations options
|
|
642
|
+
* @returns Effect yielding local Settings
|
|
643
|
+
*/
|
|
644
|
+
export const ensureProjectWorkspaceInitialized = (localDir, options) => Effect.gen(function* () {
|
|
645
|
+
const path = yield* Path.Path;
|
|
646
|
+
const userHome = yield* resolveUserHome();
|
|
647
|
+
const localSettingsResult = yield* readSettingsFromReadModel("project", path.dirname(localDir), userHome).pipe(Effect.map(Option.match({
|
|
648
|
+
onNone: () => ({ found: false, settings: createDefaultSettings() }),
|
|
649
|
+
onSome: (s) => ({ found: true, settings: s }),
|
|
650
|
+
})));
|
|
651
|
+
if (!localSettingsResult.found) {
|
|
652
|
+
// Initialize project workspace and return the settings it wrote
|
|
653
|
+
const initialization = yield* initializeProjectWorkspace(localDir, options);
|
|
654
|
+
if (!initialization.confirmed) {
|
|
655
|
+
return workspaceInitializationState(initialization.settings, false, false, initialization.agentCandidates, true);
|
|
656
|
+
}
|
|
657
|
+
return workspaceInitializationState(initialization.settings, options.preview !== true, options.preview === true, initialization.agentCandidates);
|
|
658
|
+
}
|
|
659
|
+
return workspaceInitializationState(localSettingsResult.settings, false, false);
|
|
660
|
+
});
|
|
661
|
+
export const bootstrapWorkspace = (options) => Effect.gen(function* () {
|
|
662
|
+
const location = yield* locateWorkspace(options.scope, options.projectRoot);
|
|
663
|
+
const workspaceDir = location.path;
|
|
664
|
+
if (options.scope === "user") {
|
|
665
|
+
const result = yield* ensureUserWorkspaceInitialized(location.workspaceRoot, options);
|
|
666
|
+
return { ...result, location };
|
|
667
|
+
}
|
|
668
|
+
const result = yield* ensureProjectWorkspaceInitialized(workspaceDir, options);
|
|
669
|
+
return { ...result, location };
|
|
670
|
+
});
|
|
671
|
+
//# sourceMappingURL=initialization.js.map
|