@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.
@@ -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