@agentxm/workspace-state 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/README.md +11 -0
- package/dist/src/index.d.ts +72 -0
- package/dist/src/index.js +88 -0
- package/dist/src/knowledge/discovery-config.d.ts +12 -0
- package/dist/src/knowledge/discovery-config.js +9 -0
- package/dist/src/lockfile/accepted-registry-version.d.ts +12 -0
- package/dist/src/lockfile/accepted-registry-version.js +13 -0
- package/dist/src/lockfile/entry-fields.d.ts +189 -0
- package/dist/src/lockfile/entry-fields.js +89 -0
- package/dist/src/lockfile/errors.d.ts +48 -0
- package/dist/src/lockfile/errors.js +27 -0
- package/dist/src/lockfile/index.d.ts +18 -0
- package/dist/src/lockfile/index.js +16 -0
- package/dist/src/lockfile/lockfile.d.ts +2255 -0
- package/dist/src/lockfile/lockfile.js +235 -0
- package/dist/src/lockfile/resolved-version.d.ts +4 -0
- package/dist/src/lockfile/resolved-version.js +7 -0
- package/dist/src/lockfile/schema.d.ts +3062 -0
- package/dist/src/lockfile/schema.js +406 -0
- package/dist/src/schema/index.d.ts +3 -0
- package/dist/src/schema/index.js +2 -0
- package/dist/src/schema/read-and-validate-json-file.d.ts +20 -0
- package/dist/src/schema/read-and-validate-json-file.js +85 -0
- package/dist/src/schema/types.d.ts +26 -0
- package/dist/src/schema/types.js +2 -0
- package/dist/src/settings/errors.d.ts +22 -0
- package/dist/src/settings/errors.js +15 -0
- package/dist/src/settings/format-preserving-json.d.ts +40 -0
- package/dist/src/settings/format-preserving-json.js +80 -0
- package/dist/src/settings/index.d.ts +15 -0
- package/dist/src/settings/index.js +14 -0
- package/dist/src/settings/schema.d.ts +687 -0
- package/dist/src/settings/schema.js +977 -0
- package/dist/src/settings/settings.d.ts +38 -0
- package/dist/src/settings/settings.js +213 -0
- package/dist/src/testing.d.ts +17 -0
- package/dist/src/testing.js +17 -0
- package/dist/src/utils/atomic-write.d.ts +67 -0
- package/dist/src/utils/atomic-write.js +71 -0
- package/dist/src/utils/environment.d.ts +16 -0
- package/dist/src/utils/environment.js +19 -0
- package/dist/src/utils/path-safety.d.ts +25 -0
- package/dist/src/utils/path-safety.js +34 -0
- package/dist/src/utils/resolve-parent-symlinks.d.ts +16 -0
- package/dist/src/utils/resolve-parent-symlinks.js +36 -0
- package/dist/src/workspace/accepted-canonical-ref.d.ts +58 -0
- package/dist/src/workspace/accepted-canonical-ref.js +229 -0
- package/dist/src/workspace/artifact-change.d.ts +14 -0
- package/dist/src/workspace/artifact-change.js +18 -0
- package/dist/src/workspace/canonical-observation.d.ts +39 -0
- package/dist/src/workspace/canonical-observation.js +307 -0
- package/dist/src/workspace/configured-agent-outcome.d.ts +22 -0
- package/dist/src/workspace/configured-agent-outcome.js +33 -0
- package/dist/src/workspace/configured-agent-outcomes-provider.d.ts +50 -0
- package/dist/src/workspace/configured-agent-outcomes-provider.js +26 -0
- package/dist/src/workspace/configured-agent-outcomes.d.ts +48 -0
- package/dist/src/workspace/configured-agent-outcomes.js +113 -0
- package/dist/src/workspace/configured-entry-resolution/types.d.ts +32 -0
- package/dist/src/workspace/configured-entry-resolution/types.js +2 -0
- package/dist/src/workspace/configured-entry-resolution/workspace-ref.d.ts +32 -0
- package/dist/src/workspace/configured-entry-resolution/workspace-ref.js +176 -0
- package/dist/src/workspace/constants.d.ts +6 -0
- package/dist/src/workspace/constants.js +6 -0
- package/dist/src/workspace/create-symlink.d.ts +26 -0
- package/dist/src/workspace/create-symlink.js +87 -0
- package/dist/src/workspace/desired-pack-lock.d.ts +18 -0
- package/dist/src/workspace/desired-pack-lock.js +132 -0
- package/dist/src/workspace/desired-state-enabled.d.ts +18 -0
- package/dist/src/workspace/desired-state-enabled.js +16 -0
- package/dist/src/workspace/desired-state-graph.d.ts +114 -0
- package/dist/src/workspace/desired-state-graph.js +384 -0
- package/dist/src/workspace/desired-state-problem-text.d.ts +6 -0
- package/dist/src/workspace/desired-state-problem-text.js +27 -0
- package/dist/src/workspace/errors.d.ts +184 -0
- package/dist/src/workspace/errors.js +79 -0
- package/dist/src/workspace/extension-name.d.ts +28 -0
- package/dist/src/workspace/extension-name.js +66 -0
- package/dist/src/workspace/extension-paths.d.ts +82 -0
- package/dist/src/workspace/extension-paths.js +172 -0
- package/dist/src/workspace/footprint-recorder.d.ts +30 -0
- package/dist/src/workspace/footprint-recorder.js +32 -0
- package/dist/src/workspace/layout.d.ts +43 -0
- package/dist/src/workspace/layout.js +164 -0
- package/dist/src/workspace/lock-entry-to-ref.d.ts +40 -0
- package/dist/src/workspace/lock-entry-to-ref.js +433 -0
- package/dist/src/workspace/lock-entry-to-source-params.d.ts +26 -0
- package/dist/src/workspace/lock-entry-to-source-params.js +81 -0
- package/dist/src/workspace/locked-entries.d.ts +30 -0
- package/dist/src/workspace/locked-entries.js +34 -0
- package/dist/src/workspace/materialized-file-target.d.ts +14 -0
- package/dist/src/workspace/materialized-file-target.js +14 -0
- package/dist/src/workspace/materialized-tree.d.ts +23 -0
- package/dist/src/workspace/materialized-tree.js +90 -0
- package/dist/src/workspace/mcp-entry-semantics.d.ts +31 -0
- package/dist/src/workspace/mcp-entry-semantics.js +60 -0
- package/dist/src/workspace/observed-installed.d.ts +9 -0
- package/dist/src/workspace/observed-installed.js +9 -0
- package/dist/src/workspace/pack-manifest-content-identity.d.ts +12 -0
- package/dist/src/workspace/pack-manifest-content-identity.js +20 -0
- package/dist/src/workspace/pack-paths.d.ts +24 -0
- package/dist/src/workspace/pack-paths.js +16 -0
- package/dist/src/workspace/package-hash.d.ts +25 -0
- package/dist/src/workspace/package-hash.js +45 -0
- package/dist/src/workspace/paths.d.ts +24 -0
- package/dist/src/workspace/paths.js +52 -0
- package/dist/src/workspace/read-model/__fixtures__/builder.d.ts +199 -0
- package/dist/src/workspace/read-model/__fixtures__/builder.js +588 -0
- package/dist/src/workspace/read-model/__fixtures__/decoders.d.ts +15 -0
- package/dist/src/workspace/read-model/__fixtures__/decoders.js +15 -0
- package/dist/src/workspace/read-model/__fixtures__/occurrences.d.ts +72 -0
- package/dist/src/workspace/read-model/__fixtures__/occurrences.js +134 -0
- package/dist/src/workspace/read-model/__fixtures__/scenario-harness.d.ts +99 -0
- package/dist/src/workspace/read-model/__fixtures__/scenario-harness.js +142 -0
- package/dist/src/workspace/read-model/__fixtures__/test-layer.d.ts +60 -0
- package/dist/src/workspace/read-model/__fixtures__/test-layer.js +57 -0
- package/dist/src/workspace/read-model/agent-presence.d.ts +32 -0
- package/dist/src/workspace/read-model/agent-presence.js +21 -0
- package/dist/src/workspace/read-model/agent-root-resolver.d.ts +38 -0
- package/dist/src/workspace/read-model/agent-root-resolver.js +35 -0
- package/dist/src/workspace/read-model/agents/index.d.ts +38 -0
- package/dist/src/workspace/read-model/agents/index.js +45 -0
- package/dist/src/workspace/read-model/agents/shared.d.ts +27 -0
- package/dist/src/workspace/read-model/agents/shared.js +63 -0
- package/dist/src/workspace/read-model/agents/types.d.ts +133 -0
- package/dist/src/workspace/read-model/agents/types.js +51 -0
- package/dist/src/workspace/read-model/diagnostics.d.ts +18 -0
- package/dist/src/workspace/read-model/diagnostics.js +14 -0
- package/dist/src/workspace/read-model/discovery/index.d.ts +5 -0
- package/dist/src/workspace/read-model/discovery/index.js +5 -0
- package/dist/src/workspace/read-model/discovery/plugin-manifests.d.ts +16 -0
- package/dist/src/workspace/read-model/discovery/plugin-manifests.js +168 -0
- package/dist/src/workspace/read-model/discovery/skills.d.ts +58 -0
- package/dist/src/workspace/read-model/discovery/skills.js +263 -0
- package/dist/src/workspace/read-model/discovery/subagents.d.ts +37 -0
- package/dist/src/workspace/read-model/discovery/subagents.js +48 -0
- package/dist/src/workspace/read-model/errors.d.ts +93 -0
- package/dist/src/workspace/read-model/errors.js +30 -0
- package/dist/src/workspace/read-model/extensions/actual-helpers.d.ts +4 -0
- package/dist/src/workspace/read-model/extensions/actual-helpers.js +4 -0
- package/dist/src/workspace/read-model/extensions/hook.d.ts +104 -0
- package/dist/src/workspace/read-model/extensions/hook.js +119 -0
- package/dist/src/workspace/read-model/extensions/index.d.ts +19 -0
- package/dist/src/workspace/read-model/extensions/index.js +43 -0
- package/dist/src/workspace/read-model/extensions/indexByName.d.ts +31 -0
- package/dist/src/workspace/read-model/extensions/indexByName.js +30 -0
- package/dist/src/workspace/read-model/extensions/inventory.d.ts +99 -0
- package/dist/src/workspace/read-model/extensions/inventory.js +111 -0
- package/dist/src/workspace/read-model/extensions/knowledge.d.ts +101 -0
- package/dist/src/workspace/read-model/extensions/knowledge.js +121 -0
- package/dist/src/workspace/read-model/extensions/mcp-server.d.ts +106 -0
- package/dist/src/workspace/read-model/extensions/mcp-server.js +122 -0
- package/dist/src/workspace/read-model/extensions/pack.d.ts +104 -0
- package/dist/src/workspace/read-model/extensions/pack.js +123 -0
- package/dist/src/workspace/read-model/extensions/package-root.d.ts +39 -0
- package/dist/src/workspace/read-model/extensions/package-root.js +48 -0
- package/dist/src/workspace/read-model/extensions/projection.d.ts +137 -0
- package/dist/src/workspace/read-model/extensions/projection.js +180 -0
- package/dist/src/workspace/read-model/extensions/rule.d.ts +104 -0
- package/dist/src/workspace/read-model/extensions/rule.js +113 -0
- package/dist/src/workspace/read-model/extensions/skill.d.ts +170 -0
- package/dist/src/workspace/read-model/extensions/skill.js +148 -0
- package/dist/src/workspace/read-model/extensions/subagent.d.ts +97 -0
- package/dist/src/workspace/read-model/extensions/subagent.js +120 -0
- package/dist/src/workspace/read-model/scanners/agent-dir.d.ts +49 -0
- package/dist/src/workspace/read-model/scanners/agent-dir.js +180 -0
- package/dist/src/workspace/read-model/scanners/agent-root.d.ts +73 -0
- package/dist/src/workspace/read-model/scanners/agent-root.js +148 -0
- package/dist/src/workspace/read-model/scanners/agent-settings.d.ts +43 -0
- package/dist/src/workspace/read-model/scanners/agent-settings.js +54 -0
- package/dist/src/workspace/read-model/scanners/canonical-extensions.d.ts +48 -0
- package/dist/src/workspace/read-model/scanners/canonical-extensions.js +237 -0
- package/dist/src/workspace/read-model/scanners/fs-helpers.d.ts +63 -0
- package/dist/src/workspace/read-model/scanners/fs-helpers.js +106 -0
- package/dist/src/workspace/read-model/scanners/index.d.ts +15 -0
- package/dist/src/workspace/read-model/scanners/index.js +34 -0
- package/dist/src/workspace/read-model/scanners/mcp-config.d.ts +47 -0
- package/dist/src/workspace/read-model/scanners/mcp-config.js +212 -0
- package/dist/src/workspace/read-model/scanners/types.d.ts +170 -0
- package/dist/src/workspace/read-model/scanners/types.js +86 -0
- package/dist/src/workspace/read-model/service.d.ts +103 -0
- package/dist/src/workspace/read-model/service.js +391 -0
- package/dist/src/workspace/read-model/state.d.ts +35 -0
- package/dist/src/workspace/read-model/state.js +97 -0
- package/dist/src/workspace/read-model/types.d.ts +75 -0
- package/dist/src/workspace/read-model/types.js +26 -0
- package/dist/src/workspace/read-model-record-readers.d.ts +34 -0
- package/dist/src/workspace/read-model-record-readers.js +467 -0
- package/dist/src/workspace/read-model-record-rows.d.ts +45 -0
- package/dist/src/workspace/read-model-record-rows.js +33 -0
- package/dist/src/workspace/read-model-record-types.d.ts +45 -0
- package/dist/src/workspace/read-model-record-types.js +13 -0
- package/dist/src/workspace/remove-if-exists.d.ts +13 -0
- package/dist/src/workspace/remove-if-exists.js +26 -0
- package/dist/src/workspace/rendered-files.d.ts +53 -0
- package/dist/src/workspace/rendered-files.js +50 -0
- package/dist/src/workspace/scope-refusal.d.ts +26 -0
- package/dist/src/workspace/scope-refusal.js +29 -0
- package/dist/src/workspace/service-interface.d.ts +479 -0
- package/dist/src/workspace/service-interface.js +27 -0
- package/dist/src/workspace/service.d.ts +1665 -0
- package/dist/src/workspace/service.js +1263 -0
- package/dist/src/workspace/setup-scope-support.d.ts +27 -0
- package/dist/src/workspace/setup-scope-support.js +299 -0
- package/dist/src/workspace/skill-paths.d.ts +33 -0
- package/dist/src/workspace/skill-paths.js +14 -0
- package/dist/src/workspace/source-metadata.d.ts +7 -0
- package/dist/src/workspace/source-metadata.js +10 -0
- package/dist/src/workspace/source-to-lock-entry.d.ts +32 -0
- package/dist/src/workspace/source-to-lock-entry.js +68 -0
- package/dist/src/workspace/test-stubs.d.ts +144 -0
- package/dist/src/workspace/test-stubs.js +412 -0
- package/dist/src/workspace/transaction.d.ts +218 -0
- package/dist/src/workspace/transaction.js +177 -0
- package/package.json +57 -0
|
@@ -0,0 +1,1263 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WorkspaceMutations mutation facade implementation.
|
|
3
|
+
*
|
|
4
|
+
* This is the sole public gateway for all settings and lockfile read/write
|
|
5
|
+
* operations. It reads through `WorkspaceReadModel`, writes settings directly,
|
|
6
|
+
* and commits lockfile snapshots through the lockfile module so cross-process
|
|
7
|
+
* updates are serialized and merged per entry. It serializes in-process
|
|
8
|
+
* mutations with one service-owned semaphore and transaction admission with a
|
|
9
|
+
* distinct service-owned semaphore so nested mutation calls cannot deadlock.
|
|
10
|
+
* No other service should perform settings or lockfile I/O in production; the per-service semaphores in
|
|
11
|
+
* `settings/service.ts` and `lockfile/service.ts` have been removed.
|
|
12
|
+
*
|
|
13
|
+
* Supporting logic is split into focused modules:
|
|
14
|
+
* - `source-metadata.ts` — source metadata derivation helpers
|
|
15
|
+
* - `initialization.ts` — workspace initialization (agent detection, settings creation)
|
|
16
|
+
*
|
|
17
|
+
* @experimental This API is unstable and may change without notice.
|
|
18
|
+
* @packageDocumentation
|
|
19
|
+
*/
|
|
20
|
+
import * as FileSystem from "effect/FileSystem";
|
|
21
|
+
import * as Path from "effect/Path";
|
|
22
|
+
import * as Option from "effect/Option";
|
|
23
|
+
import * as Schema from "effect/Schema";
|
|
24
|
+
import * as Semaphore from "effect/Semaphore";
|
|
25
|
+
import { LOCKFILE_VERSION, commitLockfileSnapshotUpdateAtPath, } from "../lockfile/index.js";
|
|
26
|
+
import { computeSkillPathsForLayout } from "./skill-paths.js";
|
|
27
|
+
import { computePackPathsForLayout } from "./pack-paths.js";
|
|
28
|
+
import { sanitizeName } from "./extension-name.js";
|
|
29
|
+
import { ConfigurableAgentIdSchema, decodeExtensionNameSync, formatFqn, parseSourceQualifiedRegistrySourcePatternParts, } from "@agentxm/extension-model/unstable/extensions";
|
|
30
|
+
import { DesiredPackGraphIncomplete, InvalidAgentId, LockedSkillMissing, SettingsEntryMissing, WorkspaceNotInitialized, } from "./errors.js";
|
|
31
|
+
import { LockfileValidationError } from "../lockfile/errors.js";
|
|
32
|
+
import { createDefaultSettings, writeSettingsAtPath, } from "../settings/index.js";
|
|
33
|
+
import { DEFAULT_MINIMUM_RELEASE_AGE } from "@agentxm/registry-protocol/unstable/registry/release-age-policy";
|
|
34
|
+
import {} from "@agentxm/extension-model/unstable/extensions/release-age";
|
|
35
|
+
import { printSourceParams } from "@agentxm/extension-model/unstable/sources/printer";
|
|
36
|
+
import { lockEntryToSourceParams } from "./lock-entry-to-source-params.js";
|
|
37
|
+
import { makeAbsolutePath } from "@agentxm/extension-model/unstable/path-types";
|
|
38
|
+
import { resolveKnowledgeDiscoveryConfig } from "../knowledge/discovery-config.js";
|
|
39
|
+
import { getProjectRuntimeDir, resolveUserHome } from "./paths.js";
|
|
40
|
+
import { resolveProjectWorkspaceLayout, resolveProjectWorkspaceStatePaths, resolveUserWorkspaceLayout, } from "./layout.js";
|
|
41
|
+
import * as DateTime from "effect/DateTime";
|
|
42
|
+
import * as Effect from "effect/Effect";
|
|
43
|
+
import * as Layer from "effect/Layer";
|
|
44
|
+
import { AgentRootResolverLive } from "./read-model/agent-root-resolver.js";
|
|
45
|
+
import { makeWorkspaceReadModel, WorkspaceReadModelConfig, } from "./read-model/service.js";
|
|
46
|
+
import {} from "./service-interface.js";
|
|
47
|
+
import { makeReadModelRecordReaders } from "./read-model-record-readers.js";
|
|
48
|
+
import { buildDesiredStateGraph } from "./desired-state-graph.js";
|
|
49
|
+
import { validateDesiredPackLock } from "./desired-pack-lock.js";
|
|
50
|
+
const createEmptyLockfile = () => ({
|
|
51
|
+
lockfileVersion: LOCKFILE_VERSION,
|
|
52
|
+
skills: {},
|
|
53
|
+
});
|
|
54
|
+
const normalizeForStableCompare = (value) => {
|
|
55
|
+
if (DateTime.isDateTime(value))
|
|
56
|
+
return DateTime.formatIso(value);
|
|
57
|
+
if (Array.isArray(value))
|
|
58
|
+
return value.map(normalizeForStableCompare);
|
|
59
|
+
if (typeof value === "object" && value !== null) {
|
|
60
|
+
const normalized = {};
|
|
61
|
+
for (const [key, entryValue] of Object.entries(value).sort(([left], [right]) => left.localeCompare(right))) {
|
|
62
|
+
normalized[key] = normalizeForStableCompare(entryValue);
|
|
63
|
+
}
|
|
64
|
+
return normalized;
|
|
65
|
+
}
|
|
66
|
+
return value;
|
|
67
|
+
};
|
|
68
|
+
const stableCompare = (left, right) => JSON.stringify(normalizeForStableCompare(left)) ===
|
|
69
|
+
JSON.stringify(normalizeForStableCompare(right));
|
|
70
|
+
const lockEntrySemanticallyEqual = (current, next) => current !== undefined && stableCompare(current, next);
|
|
71
|
+
const skillLockEntrySemanticallyEqual = (current, next) => lockEntrySemanticallyEqual(current, next);
|
|
72
|
+
const preserveAcceptedResolutionOnNoop = (current, next) => (lockEntrySemanticallyEqual(current, next) && current !== undefined ? current : next);
|
|
73
|
+
const subagentLockEntrySemanticallyEqual = (current, next) => lockEntrySemanticallyEqual(current, next);
|
|
74
|
+
const requireInitializedWorkspace = (settingsPath, settings) => settings.pipe(Effect.flatMap(Option.match({
|
|
75
|
+
onNone: () => Effect.fail(new WorkspaceNotInitialized({ settingsPath })),
|
|
76
|
+
onSome: () => Effect.void,
|
|
77
|
+
})));
|
|
78
|
+
/**
|
|
79
|
+
* Create the workspace mutations service from an existing workspace on disk.
|
|
80
|
+
*
|
|
81
|
+
* The workspace must already be initialized. Missing or invalid settings fail
|
|
82
|
+
* fast with a typed `WorkspaceMutationsError`. The two operations-side
|
|
83
|
+
* capabilities — the transaction runner and transition acquirer — are
|
|
84
|
+
* injected through `makeCapabilities`; the composition seam in
|
|
85
|
+
* `./operations/load-workspace.ts` supplies the live implementation.
|
|
86
|
+
*/
|
|
87
|
+
export const makeWorkspaceMutations = (options, makeCapabilities) => Effect.gen(function* () {
|
|
88
|
+
const userHome = yield* resolveUserHome();
|
|
89
|
+
const initialUserLayout = yield* resolveUserWorkspaceLayout(userHome);
|
|
90
|
+
const userRuntimeDir = initialUserLayout.runtimeDir;
|
|
91
|
+
const localRuntimeDir = yield* getProjectRuntimeDir(options.projectRoot);
|
|
92
|
+
const workspaceDir = options.scope === "user" ? userRuntimeDir : localRuntimeDir;
|
|
93
|
+
// Capture FileSystem and Path for use in closures
|
|
94
|
+
const fs = yield* FileSystem.FileSystem;
|
|
95
|
+
const path = yield* Path.Path;
|
|
96
|
+
const semaphore = yield* Semaphore.make(1);
|
|
97
|
+
const initialProjectState = resolveProjectWorkspaceStatePaths(path, options.projectRoot);
|
|
98
|
+
const settingsPath = options.scope === "user" ? initialUserLayout.settingsPath : initialProjectState.settingsPath;
|
|
99
|
+
const lockPath = options.scope === "user" ? initialUserLayout.lockPath : initialProjectState.lockPath;
|
|
100
|
+
const baseDir = options.scope === "user" ? userHome : options.projectRoot;
|
|
101
|
+
const fsLayer = Layer.mergeAll(Layer.succeed(FileSystem.FileSystem, fs), Layer.succeed(Path.Path, path));
|
|
102
|
+
const contextEnv = Layer.mergeAll(fsLayer, Layer.succeed(WorkspaceReadModelConfig, {
|
|
103
|
+
projectRoot: options.projectRoot,
|
|
104
|
+
userHome,
|
|
105
|
+
allowedRoot: makeAbsolutePath(path, "/"),
|
|
106
|
+
}), AgentRootResolverLive.pipe(Layer.provide(fsLayer)));
|
|
107
|
+
const scopeForDir = (dir, sharedScope = options.scope) => dir === userRuntimeDir && dir === localRuntimeDir
|
|
108
|
+
? sharedScope
|
|
109
|
+
: dir === userRuntimeDir
|
|
110
|
+
? "user"
|
|
111
|
+
: "project";
|
|
112
|
+
const readSettingsCell = (dir, sharedScope) => makeWorkspaceReadModel(scopeForDir(dir, sharedScope)).pipe(Effect.flatMap((readModel) => readModel.state.settings), Effect.provide(contextEnv));
|
|
113
|
+
const readLockfileCell = (dir, sharedScope) => makeWorkspaceReadModel(scopeForDir(dir, sharedScope)).pipe(Effect.flatMap((readModel) => readModel.state.lockfile), Effect.map(Option.getOrElse(createEmptyLockfile)), Effect.provide(contextEnv));
|
|
114
|
+
if (options.allowUninitialized !== true) {
|
|
115
|
+
yield* requireInitializedWorkspace(settingsPath, readSettingsCell(workspaceDir));
|
|
116
|
+
}
|
|
117
|
+
const projectSettings = yield* readSettingsCell(localRuntimeDir, "project").pipe(Effect.map(Option.getOrElse(() => createDefaultSettings())));
|
|
118
|
+
const userSettings = yield* readSettingsCell(userRuntimeDir, "user").pipe(Effect.map(Option.getOrElse(() => createDefaultSettings())));
|
|
119
|
+
const projectLayout = yield* resolveProjectWorkspaceLayout(options.projectRoot, projectSettings);
|
|
120
|
+
const userLayout = yield* resolveUserWorkspaceLayout(userHome, userSettings);
|
|
121
|
+
const layout = options.scope === "project" ? projectLayout : userLayout;
|
|
122
|
+
// Built-in sources: parameterized via options, falling back to git forges only
|
|
123
|
+
const builtInSources = options.builtInSources ?? [
|
|
124
|
+
{ name: "github", type: "github", url: new URL("https://github.com") },
|
|
125
|
+
{ name: "gitlab", type: "gitlab", url: new URL("https://gitlab.com") },
|
|
126
|
+
{ name: "bitbucket", type: "bitbucket", url: new URL("https://bitbucket.org") },
|
|
127
|
+
];
|
|
128
|
+
// Mutable cache for merged sources (invalidated by addConfiguredSource)
|
|
129
|
+
let cachedSources = null;
|
|
130
|
+
const withMutex = semaphore.withPermits(1);
|
|
131
|
+
/**
|
|
132
|
+
* Read settings from a directory, returning default settings if not found.
|
|
133
|
+
*/
|
|
134
|
+
const readSettingsSafe = (dir, sharedScope) => readSettingsCell(dir, sharedScope).pipe(Effect.map(Option.getOrElse(() => createDefaultSettings())));
|
|
135
|
+
/**
|
|
136
|
+
* Read lockfile from a directory, returning empty lockfile if not found.
|
|
137
|
+
*/
|
|
138
|
+
const readLockfileSafe = (dir) => readLockfileCell(dir);
|
|
139
|
+
const writeScopedSettings = (settings) => writeSettingsAtPath(settingsPath, settings);
|
|
140
|
+
const commitWorkspaceState = (base, next) => commitLockfileSnapshotUpdateAtPath(lockPath, base, next).pipe(Effect.provide(fsLayer));
|
|
141
|
+
const { runTransaction, acquireTransition } = yield* makeCapabilities({
|
|
142
|
+
workspaceDir,
|
|
143
|
+
settingsPath,
|
|
144
|
+
lockPath,
|
|
145
|
+
});
|
|
146
|
+
/**
|
|
147
|
+
* Look up `key` in `record`, failing with `onMissing`'s error when absent.
|
|
148
|
+
*/
|
|
149
|
+
const getEntryOrFail = (record, key, onMissing) => key in record && record[key] !== undefined
|
|
150
|
+
? Effect.succeed(record[key])
|
|
151
|
+
: Effect.fail(onMissing());
|
|
152
|
+
/**
|
|
153
|
+
* Probe lockfile state without mutating disk.
|
|
154
|
+
*/
|
|
155
|
+
const getLockfileState = () => Effect.gen(function* () {
|
|
156
|
+
const exists = yield* fs
|
|
157
|
+
.exists(lockPath)
|
|
158
|
+
.pipe(Effect.mapError((cause) => new LockfileValidationError({ path: lockPath, step: "probe", cause })));
|
|
159
|
+
if (!exists) {
|
|
160
|
+
return "missing";
|
|
161
|
+
}
|
|
162
|
+
// An unreadable or corrupt lockfile is actionable workspace state,
|
|
163
|
+
// not a violated invariant.
|
|
164
|
+
return yield* readLockfileSafe(workspaceDir).pipe(Effect.as("ok"), Effect.catchTag(["LockfileIoError", "LockfileParseError", "LockfileDecodeError"], () => Effect.succeed("invalid")));
|
|
165
|
+
}).pipe(Effect.withSpan("WorkspaceMutations.getLockfileState"));
|
|
166
|
+
const readScopedContext = (f) => makeWorkspaceReadModel(scopeForDir(workspaceDir)).pipe(Effect.flatMap(f), Effect.provide(contextEnv));
|
|
167
|
+
const readDesiredStateGraph = (graphOptions) => Effect.gen(function* () {
|
|
168
|
+
const settings = yield* readSettingsSafe(workspaceDir);
|
|
169
|
+
const graph = yield* buildDesiredStateGraph({
|
|
170
|
+
baseDir,
|
|
171
|
+
settings,
|
|
172
|
+
layout,
|
|
173
|
+
...(graphOptions?.prospectivePacks === undefined
|
|
174
|
+
? {}
|
|
175
|
+
: { prospectivePacks: graphOptions.prospectivePacks }),
|
|
176
|
+
}).pipe(Effect.provideService(FileSystem.FileSystem, fs), Effect.provideService(Path.Path, path));
|
|
177
|
+
const lockfile = yield* readLockfileSafe(workspaceDir);
|
|
178
|
+
return yield* validateDesiredPackLock({ graph, lockfile, layout }).pipe(Effect.provideService(FileSystem.FileSystem, fs), Effect.provideService(Path.Path, path));
|
|
179
|
+
});
|
|
180
|
+
const readModelRecordReaders = makeReadModelRecordReaders({
|
|
181
|
+
baseDir,
|
|
182
|
+
path,
|
|
183
|
+
readScopedContext,
|
|
184
|
+
getDesiredStateGraph: readDesiredStateGraph,
|
|
185
|
+
});
|
|
186
|
+
const records = {
|
|
187
|
+
getInventory: readModelRecordReaders.getInventory,
|
|
188
|
+
getExtensionInventory: readModelRecordReaders.getExtensionInventory,
|
|
189
|
+
rows: readModelRecordReaders.getReadModelRecordRows,
|
|
190
|
+
};
|
|
191
|
+
/**
|
|
192
|
+
* Resolve the immutable registry name for a skill's directory.
|
|
193
|
+
*
|
|
194
|
+
* Registry skill directories are tied to the registry name, not the
|
|
195
|
+
* user-facing alias. This helper tries the lockfile first, then falls
|
|
196
|
+
* back to parsing the settings source string, and finally returns the
|
|
197
|
+
* passed-in name (correct for fresh installs).
|
|
198
|
+
*/
|
|
199
|
+
const resolveRegistryDirName = (name) => Effect.gen(function* () {
|
|
200
|
+
// Try lockfile
|
|
201
|
+
const lockEntry = yield* readLockfileSafe(workspaceDir).pipe(Effect.map((lf) => Option.fromUndefinedOr(lf.skills[name])));
|
|
202
|
+
if (Option.isSome(lockEntry) && lockEntry.value.type === "registry") {
|
|
203
|
+
return lockEntry.value.name;
|
|
204
|
+
}
|
|
205
|
+
// Fallback to settings source string
|
|
206
|
+
const settings = yield* readSettingsSafe(workspaceDir);
|
|
207
|
+
const skills = settings.skills ?? {};
|
|
208
|
+
const entry = skills[name];
|
|
209
|
+
if (entry !== undefined) {
|
|
210
|
+
const parsed = parseSourceQualifiedRegistrySourcePatternParts(entry.source);
|
|
211
|
+
if (parsed?.name !== undefined) {
|
|
212
|
+
return parsed.name;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
// Final fallback: passed-in name (correct for fresh installs)
|
|
216
|
+
return name;
|
|
217
|
+
});
|
|
218
|
+
/**
|
|
219
|
+
* Three-layer merge: project sources -> user-scope sources -> built-in sources.
|
|
220
|
+
* Name-based deduplication: earlier layers win.
|
|
221
|
+
*/
|
|
222
|
+
const getConfiguredSources = () => Effect.gen(function* () {
|
|
223
|
+
if (cachedSources !== null)
|
|
224
|
+
return cachedSources;
|
|
225
|
+
const projectSettings = yield* readSettingsSafe(localRuntimeDir, "project");
|
|
226
|
+
const globalSettings = yield* readSettingsSafe(userRuntimeDir, "user");
|
|
227
|
+
const projectSources = projectSettings.sources ?? [];
|
|
228
|
+
const globalSources = globalSettings.sources ?? [];
|
|
229
|
+
const projectNames = new Set(projectSources.map((s) => s.name));
|
|
230
|
+
const filteredGlobal = globalSources.filter((s) => !projectNames.has(s.name));
|
|
231
|
+
const projectGlobalNames = new Set([...projectNames, ...filteredGlobal.map((s) => s.name)]);
|
|
232
|
+
const merged = [
|
|
233
|
+
...projectSources,
|
|
234
|
+
...filteredGlobal,
|
|
235
|
+
...builtInSources.filter((s) => !projectGlobalNames.has(s.name)),
|
|
236
|
+
];
|
|
237
|
+
cachedSources = merged;
|
|
238
|
+
return merged;
|
|
239
|
+
}).pipe(Effect.withSpan("WorkspaceMutations.getConfiguredSources"));
|
|
240
|
+
return {
|
|
241
|
+
scope: options.scope,
|
|
242
|
+
path: workspaceDir,
|
|
243
|
+
baseDir,
|
|
244
|
+
layout,
|
|
245
|
+
runTransaction,
|
|
246
|
+
acquireTransition,
|
|
247
|
+
getLockfileState,
|
|
248
|
+
getDesiredStateGraph: (graphOptions) => readDesiredStateGraph(graphOptions).pipe(Effect.withSpan("WorkspaceMutations.getDesiredStateGraph")),
|
|
249
|
+
getConfiguredSources,
|
|
250
|
+
getConfiguredSourceByName: (name) => getConfiguredSources().pipe(Effect.map((sources) => Option.fromUndefinedOr(sources.find((s) => s.name === name)))),
|
|
251
|
+
getRegistrySourceHosts: () => getConfiguredSources().pipe(Effect.map((sources) => {
|
|
252
|
+
const registrySources = sources.filter((s) => s.type === "registry");
|
|
253
|
+
return registrySources;
|
|
254
|
+
})),
|
|
255
|
+
records,
|
|
256
|
+
getConfiguredOwner: () => Effect.gen(function* () {
|
|
257
|
+
const projectSettings = yield* readSettingsSafe(localRuntimeDir, "project");
|
|
258
|
+
if (projectSettings.owner)
|
|
259
|
+
return Option.some(projectSettings.owner);
|
|
260
|
+
const globalSettings = yield* readSettingsSafe(userRuntimeDir, "user");
|
|
261
|
+
if (globalSettings.owner)
|
|
262
|
+
return Option.some(globalSettings.owner);
|
|
263
|
+
return Option.none();
|
|
264
|
+
}),
|
|
265
|
+
getPublishDefaultVisibility: () => readSettingsSafe(workspaceDir).pipe(Effect.map((settings) => Option.fromUndefinedOr(settings.publish?.defaultVisibility))),
|
|
266
|
+
getMinimumReleaseAge: Effect.fn("WorkspaceMutations.getMinimumReleaseAge")(function* () {
|
|
267
|
+
const scopedSettings = yield* readSettingsSafe(workspaceDir);
|
|
268
|
+
if (scopedSettings.minimumReleaseAge !== undefined) {
|
|
269
|
+
return scopedSettings.minimumReleaseAge;
|
|
270
|
+
}
|
|
271
|
+
if (options.scope === "project") {
|
|
272
|
+
const globalSettings = yield* readSettingsSafe(userRuntimeDir, "user");
|
|
273
|
+
if (globalSettings.minimumReleaseAge !== undefined) {
|
|
274
|
+
return globalSettings.minimumReleaseAge;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
return DEFAULT_MINIMUM_RELEASE_AGE;
|
|
278
|
+
}),
|
|
279
|
+
getMinimumReleaseAgeExclude: Effect.fn("WorkspaceMutations.getMinimumReleaseAgeExclude")(function* () {
|
|
280
|
+
const scopedSettings = yield* readSettingsSafe(workspaceDir);
|
|
281
|
+
if (scopedSettings.minimumReleaseAgeExclude !== undefined) {
|
|
282
|
+
return scopedSettings.minimumReleaseAgeExclude.map((pattern) => ({ pattern, scope: options.scope }));
|
|
283
|
+
}
|
|
284
|
+
if (options.scope === "project") {
|
|
285
|
+
const globalSettings = yield* readSettingsSafe(userRuntimeDir, "user");
|
|
286
|
+
if (globalSettings.minimumReleaseAgeExclude !== undefined) {
|
|
287
|
+
return globalSettings.minimumReleaseAgeExclude.map((pattern) => ({ pattern, scope: "user" }));
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
return [];
|
|
291
|
+
}),
|
|
292
|
+
addConfiguredSource: (source) => withMutex(Effect.gen(function* () {
|
|
293
|
+
const current = yield* readSettingsSafe(workspaceDir);
|
|
294
|
+
const currentSources = current.sources ?? [];
|
|
295
|
+
const updatedSettings = { ...current, sources: [...currentSources, source] };
|
|
296
|
+
yield* writeScopedSettings(updatedSettings).pipe(Effect.provide(fsLayer));
|
|
297
|
+
cachedSources = null; // invalidate cache
|
|
298
|
+
})).pipe(Effect.withSpan("WorkspaceMutations.addConfiguredSource")),
|
|
299
|
+
getConfiguredSkillEntries: () => readSettingsSafe(workspaceDir).pipe(Effect.map((s) => s.skills ?? {}), Effect.withSpan("WorkspaceMutations.getConfiguredSkillEntries")),
|
|
300
|
+
getConfiguredAgents: () => readSettingsSafe(workspaceDir).pipe(Effect.map((s) => s.agents ?? []), Effect.withSpan("WorkspaceMutations.getConfiguredAgents")),
|
|
301
|
+
getInstructionsConfig: () => readSettingsSafe(workspaceDir).pipe(Effect.map((s) => Option.fromUndefinedOr(s.instructionFiles)), Effect.withSpan("WorkspaceMutations.getInstructionsConfig")),
|
|
302
|
+
setInstructionsConfig: (config) => withMutex(Effect.gen(function* () {
|
|
303
|
+
const current = yield* readSettingsSafe(workspaceDir);
|
|
304
|
+
const updatedSettings = {
|
|
305
|
+
...current,
|
|
306
|
+
instructionFiles: config,
|
|
307
|
+
};
|
|
308
|
+
yield* writeScopedSettings(updatedSettings).pipe(Effect.provide(fsLayer));
|
|
309
|
+
})).pipe(Effect.withSpan("WorkspaceMutations.setInstructionsConfig")),
|
|
310
|
+
getConfiguredMcpServerEntries: () => readSettingsSafe(workspaceDir).pipe(Effect.map((s) => s.mcpServers ?? {}), Effect.withSpan("WorkspaceMutations.getConfiguredMcpServerEntries")),
|
|
311
|
+
getConfiguredRuleEntries: () => readSettingsSafe(workspaceDir).pipe(Effect.map((s) => s.rules ?? {}), Effect.withSpan("WorkspaceMutations.getConfiguredRuleEntries")),
|
|
312
|
+
getLockedRules: () => readLockfileSafe(workspaceDir).pipe(Effect.map((lf) => lf.rules ?? {})),
|
|
313
|
+
getLockedRuleEntry: (name) => readLockfileSafe(workspaceDir).pipe(Effect.map((lf) => Option.fromUndefinedOr((lf.rules ?? {})[name]))),
|
|
314
|
+
setRule: ({ name, lockEntry, versionRange }) => withMutex(Effect.gen(function* () {
|
|
315
|
+
const source = lockEntry.type === "registry"
|
|
316
|
+
? (() => {
|
|
317
|
+
const fqn = formatFqn({
|
|
318
|
+
owner: lockEntry.owner,
|
|
319
|
+
type: "rule",
|
|
320
|
+
name: decodeExtensionNameSync(name),
|
|
321
|
+
});
|
|
322
|
+
const locator = Option.isSome(versionRange)
|
|
323
|
+
? `${fqn}@${versionRange.value}`
|
|
324
|
+
: fqn;
|
|
325
|
+
return `${lockEntry.sourceName}:${locator}`;
|
|
326
|
+
})()
|
|
327
|
+
: printSourceParams(lockEntryToSourceParams(lockEntry));
|
|
328
|
+
const currentSettings = yield* readSettingsSafe(workspaceDir);
|
|
329
|
+
const currentRules = currentSettings.rules ?? {};
|
|
330
|
+
yield* writeScopedSettings({
|
|
331
|
+
...currentSettings,
|
|
332
|
+
rules: {
|
|
333
|
+
...currentRules,
|
|
334
|
+
[name]: { source, enabled: true },
|
|
335
|
+
},
|
|
336
|
+
}).pipe(Effect.provide(fsLayer));
|
|
337
|
+
const currentLockfile = yield* readLockfileSafe(workspaceDir);
|
|
338
|
+
const currentLockedRules = currentLockfile.rules ?? {};
|
|
339
|
+
const previous = currentLockedRules[name];
|
|
340
|
+
const updatedLockfile = {
|
|
341
|
+
...currentLockfile,
|
|
342
|
+
rules: {
|
|
343
|
+
...currentLockedRules,
|
|
344
|
+
[name]: preserveAcceptedResolutionOnNoop(previous, lockEntry),
|
|
345
|
+
},
|
|
346
|
+
};
|
|
347
|
+
yield* commitWorkspaceState(currentLockfile, updatedLockfile);
|
|
348
|
+
})).pipe(Effect.withSpan("WorkspaceMutations.setRule")),
|
|
349
|
+
setRuleLock: ({ name, lockEntry }) => withMutex(Effect.gen(function* () {
|
|
350
|
+
const currentLockfile = yield* readLockfileSafe(workspaceDir);
|
|
351
|
+
const currentLockedRules = currentLockfile.rules ?? {};
|
|
352
|
+
const previous = currentLockedRules[name];
|
|
353
|
+
const updatedLockfile = {
|
|
354
|
+
...currentLockfile,
|
|
355
|
+
rules: {
|
|
356
|
+
...currentLockedRules,
|
|
357
|
+
[name]: preserveAcceptedResolutionOnNoop(previous, lockEntry),
|
|
358
|
+
},
|
|
359
|
+
};
|
|
360
|
+
yield* commitWorkspaceState(currentLockfile, updatedLockfile);
|
|
361
|
+
})).pipe(Effect.withSpan("WorkspaceMutations.setRuleLock")),
|
|
362
|
+
removeRule: (name) => withMutex(Effect.gen(function* () {
|
|
363
|
+
const currentSettings = yield* readSettingsSafe(workspaceDir);
|
|
364
|
+
const currentRules = currentSettings.rules ?? {};
|
|
365
|
+
const remainingSettings = name in currentRules
|
|
366
|
+
? (() => {
|
|
367
|
+
const { [name]: _, ...remainingRules } = currentRules;
|
|
368
|
+
void _;
|
|
369
|
+
return { ...currentSettings, rules: remainingRules };
|
|
370
|
+
})()
|
|
371
|
+
: currentSettings;
|
|
372
|
+
const currentLockfile = yield* readLockfileSafe(workspaceDir);
|
|
373
|
+
const currentLockedRules = currentLockfile.rules ?? {};
|
|
374
|
+
const remainingLockfile = name in currentLockedRules
|
|
375
|
+
? (() => {
|
|
376
|
+
const { [name]: _, ...remainingRules } = currentLockedRules;
|
|
377
|
+
void _;
|
|
378
|
+
return { ...currentLockfile, rules: remainingRules };
|
|
379
|
+
})()
|
|
380
|
+
: currentLockfile;
|
|
381
|
+
yield* writeScopedSettings(remainingSettings).pipe(Effect.provide(fsLayer));
|
|
382
|
+
yield* commitLockfileSnapshotUpdateAtPath(lockPath, currentLockfile, remainingLockfile).pipe(Effect.provide(fsLayer));
|
|
383
|
+
})).pipe(Effect.withSpan("WorkspaceMutations.removeRule")),
|
|
384
|
+
removeRuleSettings: (name) => withMutex(Effect.gen(function* () {
|
|
385
|
+
const currentSettings = yield* readSettingsSafe(workspaceDir);
|
|
386
|
+
const currentRules = currentSettings.rules ?? {};
|
|
387
|
+
if (!(name in currentRules))
|
|
388
|
+
return;
|
|
389
|
+
const { [name]: _, ...remainingRules } = currentRules;
|
|
390
|
+
void _;
|
|
391
|
+
yield* writeScopedSettings({
|
|
392
|
+
...currentSettings,
|
|
393
|
+
rules: remainingRules,
|
|
394
|
+
}).pipe(Effect.provide(fsLayer));
|
|
395
|
+
})).pipe(Effect.withSpan("WorkspaceMutations.removeRuleSettings")),
|
|
396
|
+
removeRuleLock: (name) => withMutex(Effect.gen(function* () {
|
|
397
|
+
const currentLockfile = yield* readLockfileSafe(workspaceDir);
|
|
398
|
+
const currentLockedRules = currentLockfile.rules ?? {};
|
|
399
|
+
if (!(name in currentLockedRules))
|
|
400
|
+
return;
|
|
401
|
+
const { [name]: _, ...remainingRules } = currentLockedRules;
|
|
402
|
+
void _;
|
|
403
|
+
const updatedLockfile = {
|
|
404
|
+
...currentLockfile,
|
|
405
|
+
rules: remainingRules,
|
|
406
|
+
};
|
|
407
|
+
yield* commitLockfileSnapshotUpdateAtPath(lockPath, currentLockfile, updatedLockfile).pipe(Effect.provide(fsLayer));
|
|
408
|
+
})).pipe(Effect.withSpan("WorkspaceMutations.removeRuleLock")),
|
|
409
|
+
updateRuleEntry: (name, updater) => withMutex(Effect.gen(function* () {
|
|
410
|
+
const currentSettings = yield* readSettingsSafe(workspaceDir);
|
|
411
|
+
const currentRules = currentSettings.rules ?? {};
|
|
412
|
+
const existingEntry = currentRules[name];
|
|
413
|
+
if (existingEntry === undefined)
|
|
414
|
+
return;
|
|
415
|
+
yield* writeScopedSettings({
|
|
416
|
+
...currentSettings,
|
|
417
|
+
rules: { ...currentRules, [name]: updater(existingEntry) },
|
|
418
|
+
}).pipe(Effect.provide(fsLayer));
|
|
419
|
+
})).pipe(Effect.withSpan("WorkspaceMutations.updateRuleEntry")),
|
|
420
|
+
setRuleEntry: (name, entry) => withMutex(Effect.gen(function* () {
|
|
421
|
+
const currentSettings = yield* readSettingsSafe(workspaceDir);
|
|
422
|
+
const currentRules = currentSettings.rules ?? {};
|
|
423
|
+
yield* writeScopedSettings({
|
|
424
|
+
...currentSettings,
|
|
425
|
+
rules: { ...currentRules, [name]: entry },
|
|
426
|
+
}).pipe(Effect.provide(fsLayer));
|
|
427
|
+
})).pipe(Effect.withSpan("WorkspaceMutations.setRuleEntry")),
|
|
428
|
+
getConfiguredHookEntries: () => readSettingsSafe(workspaceDir).pipe(Effect.map((s) => s.hooks ?? {}), Effect.withSpan("WorkspaceMutations.getConfiguredHookEntries")),
|
|
429
|
+
getLockedHooks: () => readLockfileSafe(workspaceDir).pipe(Effect.map((lf) => lf.hooks ?? {})),
|
|
430
|
+
getLockedHookEntry: (name) => readLockfileSafe(workspaceDir).pipe(Effect.map((lf) => Option.fromUndefinedOr((lf.hooks ?? {})[name]))),
|
|
431
|
+
setHook: ({ name, lockEntry, versionRange }) => withMutex(Effect.gen(function* () {
|
|
432
|
+
const source = lockEntry.type === "registry"
|
|
433
|
+
? (() => {
|
|
434
|
+
const fqn = formatFqn({
|
|
435
|
+
owner: lockEntry.owner,
|
|
436
|
+
type: "hook",
|
|
437
|
+
name: decodeExtensionNameSync(name),
|
|
438
|
+
});
|
|
439
|
+
const locator = Option.isSome(versionRange)
|
|
440
|
+
? `${fqn}@${versionRange.value}`
|
|
441
|
+
: fqn;
|
|
442
|
+
return `${lockEntry.sourceName}:${locator}`;
|
|
443
|
+
})()
|
|
444
|
+
: printSourceParams(lockEntryToSourceParams(lockEntry));
|
|
445
|
+
const currentSettings = yield* readSettingsSafe(workspaceDir);
|
|
446
|
+
const currentHooks = currentSettings.hooks ?? {};
|
|
447
|
+
yield* writeScopedSettings({
|
|
448
|
+
...currentSettings,
|
|
449
|
+
hooks: {
|
|
450
|
+
...currentHooks,
|
|
451
|
+
[name]: { source, enabled: true },
|
|
452
|
+
},
|
|
453
|
+
}).pipe(Effect.provide(fsLayer));
|
|
454
|
+
const currentLockfile = yield* readLockfileSafe(workspaceDir);
|
|
455
|
+
const currentLockedHooks = currentLockfile.hooks ?? {};
|
|
456
|
+
const previous = currentLockedHooks[name];
|
|
457
|
+
const updatedLockfile = {
|
|
458
|
+
...currentLockfile,
|
|
459
|
+
hooks: {
|
|
460
|
+
...currentLockedHooks,
|
|
461
|
+
[name]: preserveAcceptedResolutionOnNoop(previous, lockEntry),
|
|
462
|
+
},
|
|
463
|
+
};
|
|
464
|
+
yield* commitWorkspaceState(currentLockfile, updatedLockfile);
|
|
465
|
+
})).pipe(Effect.withSpan("WorkspaceMutations.setHook")),
|
|
466
|
+
setHookLock: ({ name, lockEntry }) => withMutex(Effect.gen(function* () {
|
|
467
|
+
const currentLockfile = yield* readLockfileSafe(workspaceDir);
|
|
468
|
+
const currentLockedHooks = currentLockfile.hooks ?? {};
|
|
469
|
+
const previous = currentLockedHooks[name];
|
|
470
|
+
const updatedLockfile = {
|
|
471
|
+
...currentLockfile,
|
|
472
|
+
hooks: {
|
|
473
|
+
...currentLockedHooks,
|
|
474
|
+
[name]: preserveAcceptedResolutionOnNoop(previous, lockEntry),
|
|
475
|
+
},
|
|
476
|
+
};
|
|
477
|
+
yield* commitWorkspaceState(currentLockfile, updatedLockfile);
|
|
478
|
+
})).pipe(Effect.withSpan("WorkspaceMutations.setHookLock")),
|
|
479
|
+
removeHook: (name) => withMutex(Effect.gen(function* () {
|
|
480
|
+
const currentSettings = yield* readSettingsSafe(workspaceDir);
|
|
481
|
+
const currentHooks = currentSettings.hooks ?? {};
|
|
482
|
+
const remainingSettings = name in currentHooks
|
|
483
|
+
? (() => {
|
|
484
|
+
const { [name]: _, ...remainingHooks } = currentHooks;
|
|
485
|
+
void _;
|
|
486
|
+
return { ...currentSettings, hooks: remainingHooks };
|
|
487
|
+
})()
|
|
488
|
+
: currentSettings;
|
|
489
|
+
const currentLockfile = yield* readLockfileSafe(workspaceDir);
|
|
490
|
+
const currentLockedHooks = currentLockfile.hooks ?? {};
|
|
491
|
+
const remainingLockfile = name in currentLockedHooks
|
|
492
|
+
? (() => {
|
|
493
|
+
const { [name]: _, ...remainingHooks } = currentLockedHooks;
|
|
494
|
+
void _;
|
|
495
|
+
return { ...currentLockfile, hooks: remainingHooks };
|
|
496
|
+
})()
|
|
497
|
+
: currentLockfile;
|
|
498
|
+
yield* writeScopedSettings(remainingSettings).pipe(Effect.provide(fsLayer));
|
|
499
|
+
yield* commitLockfileSnapshotUpdateAtPath(lockPath, currentLockfile, remainingLockfile).pipe(Effect.provide(fsLayer));
|
|
500
|
+
})).pipe(Effect.withSpan("WorkspaceMutations.removeHook")),
|
|
501
|
+
removeHookSettings: (name) => withMutex(Effect.gen(function* () {
|
|
502
|
+
const currentSettings = yield* readSettingsSafe(workspaceDir);
|
|
503
|
+
const currentHooks = currentSettings.hooks ?? {};
|
|
504
|
+
if (!(name in currentHooks))
|
|
505
|
+
return;
|
|
506
|
+
const { [name]: _, ...remainingHooks } = currentHooks;
|
|
507
|
+
void _;
|
|
508
|
+
yield* writeScopedSettings({
|
|
509
|
+
...currentSettings,
|
|
510
|
+
hooks: remainingHooks,
|
|
511
|
+
}).pipe(Effect.provide(fsLayer));
|
|
512
|
+
})).pipe(Effect.withSpan("WorkspaceMutations.removeHookSettings")),
|
|
513
|
+
removeHookLock: (name) => withMutex(Effect.gen(function* () {
|
|
514
|
+
const currentLockfile = yield* readLockfileSafe(workspaceDir);
|
|
515
|
+
const currentLockedHooks = currentLockfile.hooks ?? {};
|
|
516
|
+
if (!(name in currentLockedHooks))
|
|
517
|
+
return;
|
|
518
|
+
const { [name]: _, ...remainingHooks } = currentLockedHooks;
|
|
519
|
+
void _;
|
|
520
|
+
const updatedLockfile = {
|
|
521
|
+
...currentLockfile,
|
|
522
|
+
hooks: remainingHooks,
|
|
523
|
+
};
|
|
524
|
+
yield* commitLockfileSnapshotUpdateAtPath(lockPath, currentLockfile, updatedLockfile).pipe(Effect.provide(fsLayer));
|
|
525
|
+
})).pipe(Effect.withSpan("WorkspaceMutations.removeHookLock")),
|
|
526
|
+
updateHookEntry: (name, updater) => withMutex(Effect.gen(function* () {
|
|
527
|
+
const currentSettings = yield* readSettingsSafe(workspaceDir);
|
|
528
|
+
const currentHooks = currentSettings.hooks ?? {};
|
|
529
|
+
const existingEntry = currentHooks[name];
|
|
530
|
+
if (existingEntry === undefined)
|
|
531
|
+
return;
|
|
532
|
+
yield* writeScopedSettings({
|
|
533
|
+
...currentSettings,
|
|
534
|
+
hooks: { ...currentHooks, [name]: updater(existingEntry) },
|
|
535
|
+
}).pipe(Effect.provide(fsLayer));
|
|
536
|
+
})).pipe(Effect.withSpan("WorkspaceMutations.updateHookEntry")),
|
|
537
|
+
setHookEntry: (name, entry) => withMutex(Effect.gen(function* () {
|
|
538
|
+
const currentSettings = yield* readSettingsSafe(workspaceDir);
|
|
539
|
+
const currentHooks = currentSettings.hooks ?? {};
|
|
540
|
+
yield* writeScopedSettings({
|
|
541
|
+
...currentSettings,
|
|
542
|
+
hooks: { ...currentHooks, [name]: entry },
|
|
543
|
+
}).pipe(Effect.provide(fsLayer));
|
|
544
|
+
})).pipe(Effect.withSpan("WorkspaceMutations.setHookEntry")),
|
|
545
|
+
getConfiguredKnowledgeEntries: () => readSettingsSafe(workspaceDir).pipe(Effect.map((settings) => settings.knowledge ?? {}), Effect.withSpan("WorkspaceMutations.getConfiguredKnowledgeEntries")),
|
|
546
|
+
getKnowledgeDiscoveryConfig: () => readSettingsSafe(workspaceDir).pipe(Effect.map((settings) => resolveKnowledgeDiscoveryConfig({
|
|
547
|
+
...(settings.knowledgeConfig?.instructions === false ? { instructions: false } : {}),
|
|
548
|
+
})), Effect.withSpan("WorkspaceMutations.getKnowledgeDiscoveryConfig")),
|
|
549
|
+
getLockedKnowledge: () => readLockfileSafe(workspaceDir).pipe(Effect.map((lockfile) => lockfile.knowledge ?? {})),
|
|
550
|
+
getLockedKnowledgeEntry: (name) => readLockfileSafe(workspaceDir).pipe(Effect.map((lockfile) => Option.fromUndefinedOr((lockfile.knowledge ?? {})[name]))),
|
|
551
|
+
setKnowledge: ({ name, lockEntry, versionRange }) => withMutex(Effect.gen(function* () {
|
|
552
|
+
const source = lockEntry.type === "registry"
|
|
553
|
+
? (() => {
|
|
554
|
+
const fqn = formatFqn({
|
|
555
|
+
owner: lockEntry.owner,
|
|
556
|
+
type: "knowledge",
|
|
557
|
+
name: decodeExtensionNameSync(name),
|
|
558
|
+
});
|
|
559
|
+
const locator = Option.isSome(versionRange)
|
|
560
|
+
? `${fqn}@${versionRange.value}`
|
|
561
|
+
: fqn;
|
|
562
|
+
return `${lockEntry.sourceName}:${locator}`;
|
|
563
|
+
})()
|
|
564
|
+
: printSourceParams(lockEntryToSourceParams(lockEntry));
|
|
565
|
+
const currentSettings = yield* readSettingsSafe(workspaceDir);
|
|
566
|
+
const currentKnowledge = currentSettings.knowledge ?? {};
|
|
567
|
+
const currentEntry = currentKnowledge[name];
|
|
568
|
+
yield* writeScopedSettings({
|
|
569
|
+
...currentSettings,
|
|
570
|
+
knowledge: {
|
|
571
|
+
...currentKnowledge,
|
|
572
|
+
[name]: {
|
|
573
|
+
source,
|
|
574
|
+
enabled: true,
|
|
575
|
+
...(currentEntry?.instructionEntry === undefined
|
|
576
|
+
? {}
|
|
577
|
+
: { instructionEntry: currentEntry.instructionEntry }),
|
|
578
|
+
},
|
|
579
|
+
},
|
|
580
|
+
}).pipe(Effect.provide(fsLayer));
|
|
581
|
+
const currentLockfile = yield* readLockfileSafe(workspaceDir);
|
|
582
|
+
const currentLocked = currentLockfile.knowledge ?? {};
|
|
583
|
+
const previous = currentLocked[name];
|
|
584
|
+
yield* commitWorkspaceState(currentLockfile, {
|
|
585
|
+
...currentLockfile,
|
|
586
|
+
knowledge: {
|
|
587
|
+
...currentLocked,
|
|
588
|
+
[name]: preserveAcceptedResolutionOnNoop(previous, lockEntry),
|
|
589
|
+
},
|
|
590
|
+
});
|
|
591
|
+
})).pipe(Effect.withSpan("WorkspaceMutations.setKnowledge")),
|
|
592
|
+
setKnowledgeLock: ({ name, lockEntry }) => withMutex(Effect.gen(function* () {
|
|
593
|
+
const currentLockfile = yield* readLockfileSafe(workspaceDir);
|
|
594
|
+
const currentLocked = currentLockfile.knowledge ?? {};
|
|
595
|
+
const previous = currentLocked[name];
|
|
596
|
+
yield* commitWorkspaceState(currentLockfile, {
|
|
597
|
+
...currentLockfile,
|
|
598
|
+
knowledge: {
|
|
599
|
+
...currentLocked,
|
|
600
|
+
[name]: preserveAcceptedResolutionOnNoop(previous, lockEntry),
|
|
601
|
+
},
|
|
602
|
+
});
|
|
603
|
+
})).pipe(Effect.withSpan("WorkspaceMutations.setKnowledgeLock")),
|
|
604
|
+
removeKnowledge: (name) => withMutex(Effect.gen(function* () {
|
|
605
|
+
const currentSettings = yield* readSettingsSafe(workspaceDir);
|
|
606
|
+
const configured = currentSettings.knowledge ?? {};
|
|
607
|
+
const nextSettings = name in configured
|
|
608
|
+
? (() => {
|
|
609
|
+
const { [name]: removed, ...remaining } = configured;
|
|
610
|
+
void removed;
|
|
611
|
+
return { ...currentSettings, knowledge: remaining };
|
|
612
|
+
})()
|
|
613
|
+
: currentSettings;
|
|
614
|
+
const currentLockfile = yield* readLockfileSafe(workspaceDir);
|
|
615
|
+
const locked = currentLockfile.knowledge ?? {};
|
|
616
|
+
const nextLockfile = name in locked
|
|
617
|
+
? (() => {
|
|
618
|
+
const { [name]: removed, ...remaining } = locked;
|
|
619
|
+
void removed;
|
|
620
|
+
return { ...currentLockfile, knowledge: remaining };
|
|
621
|
+
})()
|
|
622
|
+
: currentLockfile;
|
|
623
|
+
yield* writeScopedSettings(nextSettings).pipe(Effect.provide(fsLayer));
|
|
624
|
+
yield* commitLockfileSnapshotUpdateAtPath(lockPath, currentLockfile, nextLockfile).pipe(Effect.provide(fsLayer));
|
|
625
|
+
})).pipe(Effect.withSpan("WorkspaceMutations.removeKnowledge")),
|
|
626
|
+
removeKnowledgeSettings: (name) => withMutex(Effect.gen(function* () {
|
|
627
|
+
const currentSettings = yield* readSettingsSafe(workspaceDir);
|
|
628
|
+
const configured = currentSettings.knowledge ?? {};
|
|
629
|
+
if (!(name in configured))
|
|
630
|
+
return;
|
|
631
|
+
const { [name]: removed, ...remaining } = configured;
|
|
632
|
+
void removed;
|
|
633
|
+
yield* writeScopedSettings({
|
|
634
|
+
...currentSettings,
|
|
635
|
+
knowledge: remaining,
|
|
636
|
+
}).pipe(Effect.provide(fsLayer));
|
|
637
|
+
})).pipe(Effect.withSpan("WorkspaceMutations.removeKnowledgeSettings")),
|
|
638
|
+
removeKnowledgeLock: (name) => withMutex(Effect.gen(function* () {
|
|
639
|
+
const currentLockfile = yield* readLockfileSafe(workspaceDir);
|
|
640
|
+
const locked = currentLockfile.knowledge ?? {};
|
|
641
|
+
if (!(name in locked))
|
|
642
|
+
return;
|
|
643
|
+
const { [name]: removed, ...remaining } = locked;
|
|
644
|
+
void removed;
|
|
645
|
+
yield* commitLockfileSnapshotUpdateAtPath(lockPath, currentLockfile, {
|
|
646
|
+
...currentLockfile,
|
|
647
|
+
knowledge: remaining,
|
|
648
|
+
}).pipe(Effect.provide(fsLayer));
|
|
649
|
+
})).pipe(Effect.withSpan("WorkspaceMutations.removeKnowledgeLock")),
|
|
650
|
+
updateKnowledgeEntry: (name, updater) => withMutex(Effect.gen(function* () {
|
|
651
|
+
const currentSettings = yield* readSettingsSafe(workspaceDir);
|
|
652
|
+
const configured = currentSettings.knowledge ?? {};
|
|
653
|
+
const existing = configured[name];
|
|
654
|
+
if (existing === undefined)
|
|
655
|
+
return;
|
|
656
|
+
yield* writeScopedSettings({
|
|
657
|
+
...currentSettings,
|
|
658
|
+
knowledge: { ...configured, [name]: updater(existing) },
|
|
659
|
+
}).pipe(Effect.provide(fsLayer));
|
|
660
|
+
})).pipe(Effect.withSpan("WorkspaceMutations.updateKnowledgeEntry")),
|
|
661
|
+
setKnowledgeEntry: (name, entry) => withMutex(Effect.gen(function* () {
|
|
662
|
+
const currentSettings = yield* readSettingsSafe(workspaceDir);
|
|
663
|
+
const configured = currentSettings.knowledge ?? {};
|
|
664
|
+
yield* writeScopedSettings({
|
|
665
|
+
...currentSettings,
|
|
666
|
+
knowledge: { ...configured, [name]: entry },
|
|
667
|
+
}).pipe(Effect.provide(fsLayer));
|
|
668
|
+
})).pipe(Effect.withSpan("WorkspaceMutations.setKnowledgeEntry")),
|
|
669
|
+
getLockedSkills: () => readLockfileSafe(workspaceDir).pipe(Effect.map((lf) => lf.skills)),
|
|
670
|
+
getLockedSkill: (name) => readLockfileSafe(workspaceDir).pipe(Effect.map((lf) => Option.fromUndefinedOr(lf.skills[name]))),
|
|
671
|
+
getSkillDir: (name, source) => Effect.gen(function* () {
|
|
672
|
+
if (source !== undefined) {
|
|
673
|
+
const dirName = source.refType === "registry" ? yield* resolveRegistryDirName(name) : name;
|
|
674
|
+
return computeSkillPathsForLayout(path.join, layout, source, sanitizeName(dirName));
|
|
675
|
+
}
|
|
676
|
+
const lockEntry = yield* readLockfileSafe(workspaceDir).pipe(Effect.map((lf) => Option.fromUndefinedOr(lf.skills[name])));
|
|
677
|
+
if (Option.isNone(lockEntry)) {
|
|
678
|
+
return yield* new LockedSkillMissing({ name });
|
|
679
|
+
}
|
|
680
|
+
const entry = lockEntry.value;
|
|
681
|
+
const entrySource = (() => {
|
|
682
|
+
switch (entry.type) {
|
|
683
|
+
case "registry":
|
|
684
|
+
return {
|
|
685
|
+
refType: "registry",
|
|
686
|
+
owner: entry.owner,
|
|
687
|
+
source: {
|
|
688
|
+
type: "registry",
|
|
689
|
+
name: entry.sourceName,
|
|
690
|
+
location: entry.endpoint,
|
|
691
|
+
owner: Option.some(entry.owner),
|
|
692
|
+
},
|
|
693
|
+
};
|
|
694
|
+
case "local":
|
|
695
|
+
return {
|
|
696
|
+
refType: "local",
|
|
697
|
+
source: { type: "local", path: entry.path },
|
|
698
|
+
sourcePath: entry.path,
|
|
699
|
+
};
|
|
700
|
+
case "github":
|
|
701
|
+
case "gitlab":
|
|
702
|
+
case "bitbucket":
|
|
703
|
+
return {
|
|
704
|
+
refType: "git-hosted",
|
|
705
|
+
source: {
|
|
706
|
+
type: entry.type,
|
|
707
|
+
name: entry.sourceName,
|
|
708
|
+
url: entry.endpoint,
|
|
709
|
+
owner: entry.owner,
|
|
710
|
+
repo: entry.repo,
|
|
711
|
+
ref: Option.fromUndefinedOr(entry.ref),
|
|
712
|
+
subPath: Option.fromUndefinedOr(entry.path),
|
|
713
|
+
},
|
|
714
|
+
...(entry.path === undefined ? {} : { sourcePath: entry.path }),
|
|
715
|
+
};
|
|
716
|
+
case "azurerepos":
|
|
717
|
+
return {
|
|
718
|
+
refType: "git-hosted",
|
|
719
|
+
source: {
|
|
720
|
+
type: "azurerepos",
|
|
721
|
+
name: entry.sourceName,
|
|
722
|
+
url: entry.endpoint,
|
|
723
|
+
organization: entry.organization,
|
|
724
|
+
project: entry.project,
|
|
725
|
+
repo: entry.repo,
|
|
726
|
+
ref: Option.fromUndefinedOr(entry.ref),
|
|
727
|
+
subPath: Option.fromUndefinedOr(entry.path),
|
|
728
|
+
},
|
|
729
|
+
...(entry.path === undefined ? {} : { sourcePath: entry.path }),
|
|
730
|
+
};
|
|
731
|
+
case "git":
|
|
732
|
+
return {
|
|
733
|
+
refType: "git-hosted",
|
|
734
|
+
source: {
|
|
735
|
+
type: "git",
|
|
736
|
+
url: new URL(entry.url),
|
|
737
|
+
ref: Option.fromUndefinedOr(entry.ref),
|
|
738
|
+
},
|
|
739
|
+
...(entry.path === undefined ? {} : { sourcePath: entry.path }),
|
|
740
|
+
};
|
|
741
|
+
}
|
|
742
|
+
})();
|
|
743
|
+
const dirName = entry.type === "registry" ? entry.name : entry.packageName;
|
|
744
|
+
return computeSkillPathsForLayout(path.join, layout, entrySource, sanitizeName(dirName));
|
|
745
|
+
}),
|
|
746
|
+
setSkill: ({ name, lockEntry, versionRange }) => withMutex(Effect.gen(function* () {
|
|
747
|
+
// Update settings — thread version constraint through so it survives the round-trip
|
|
748
|
+
const sourceInput = lockEntryToSourceParams(lockEntry);
|
|
749
|
+
const source = lockEntry.type === "registry"
|
|
750
|
+
? (() => {
|
|
751
|
+
const fqn = formatFqn({
|
|
752
|
+
owner: lockEntry.owner,
|
|
753
|
+
type: "skill",
|
|
754
|
+
name: decodeExtensionNameSync(name),
|
|
755
|
+
});
|
|
756
|
+
const locator = Option.isSome(versionRange)
|
|
757
|
+
? `${fqn}@${versionRange.value}`
|
|
758
|
+
: fqn;
|
|
759
|
+
return `${lockEntry.sourceName}:${locator}`;
|
|
760
|
+
})()
|
|
761
|
+
: printSourceParams(sourceInput);
|
|
762
|
+
const currentSettings = yield* readSettingsSafe(workspaceDir);
|
|
763
|
+
const currentSkills = currentSettings.skills ?? {};
|
|
764
|
+
const nextSkillEntry = { source, enabled: true };
|
|
765
|
+
const updatedSettings = {
|
|
766
|
+
...currentSettings,
|
|
767
|
+
skills: { ...currentSkills, [name]: nextSkillEntry },
|
|
768
|
+
};
|
|
769
|
+
const currentLockfile = yield* readLockfileSafe(workspaceDir);
|
|
770
|
+
const currentLockEntry = currentLockfile.skills[name];
|
|
771
|
+
const settingsChanged = !stableCompare(currentSkills[name], nextSkillEntry);
|
|
772
|
+
const lockChanged = !skillLockEntrySemanticallyEqual(currentLockEntry, lockEntry);
|
|
773
|
+
if (settingsChanged) {
|
|
774
|
+
yield* writeScopedSettings(updatedSettings).pipe(Effect.provide(fsLayer));
|
|
775
|
+
}
|
|
776
|
+
if (!lockChanged)
|
|
777
|
+
return;
|
|
778
|
+
const updatedLockfile = {
|
|
779
|
+
...currentLockfile,
|
|
780
|
+
skills: {
|
|
781
|
+
...currentLockfile.skills,
|
|
782
|
+
[name]: preserveAcceptedResolutionOnNoop(currentLockEntry, lockEntry),
|
|
783
|
+
},
|
|
784
|
+
};
|
|
785
|
+
yield* commitWorkspaceState(currentLockfile, updatedLockfile);
|
|
786
|
+
})).pipe(Effect.withSpan("WorkspaceMutations.setSkill")),
|
|
787
|
+
setSkillLock: ({ name, lockEntry }) => withMutex(Effect.gen(function* () {
|
|
788
|
+
// Update lockfile only (skip settings) — used for pack dependencies
|
|
789
|
+
const currentLockfile = yield* readLockfileSafe(workspaceDir);
|
|
790
|
+
const currentLockEntry = currentLockfile.skills[name];
|
|
791
|
+
if (skillLockEntrySemanticallyEqual(currentLockEntry, lockEntry)) {
|
|
792
|
+
return;
|
|
793
|
+
}
|
|
794
|
+
const updatedLockfile = {
|
|
795
|
+
...currentLockfile,
|
|
796
|
+
skills: {
|
|
797
|
+
...currentLockfile.skills,
|
|
798
|
+
[name]: preserveAcceptedResolutionOnNoop(currentLockEntry, lockEntry),
|
|
799
|
+
},
|
|
800
|
+
};
|
|
801
|
+
yield* commitWorkspaceState(currentLockfile, updatedLockfile);
|
|
802
|
+
})).pipe(Effect.withSpan("WorkspaceMutations.setSkillLock")),
|
|
803
|
+
removeSkill: (name) => withMutex(Effect.gen(function* () {
|
|
804
|
+
// Update settings
|
|
805
|
+
const currentSettings = yield* readSettingsSafe(workspaceDir);
|
|
806
|
+
const currentSkills = currentSettings.skills ?? {};
|
|
807
|
+
const hasSettingsEntry = name in currentSkills;
|
|
808
|
+
if (hasSettingsEntry) {
|
|
809
|
+
const { [name]: _, ...remainingSkills } = currentSkills;
|
|
810
|
+
void _;
|
|
811
|
+
const updatedSettings = { ...currentSettings, skills: remainingSkills };
|
|
812
|
+
yield* writeScopedSettings(updatedSettings).pipe(Effect.provide(fsLayer));
|
|
813
|
+
}
|
|
814
|
+
// Update lockfile
|
|
815
|
+
const currentLockfile = yield* readLockfileSafe(workspaceDir);
|
|
816
|
+
const hasLockfileEntry = name in currentLockfile.skills;
|
|
817
|
+
if (hasLockfileEntry) {
|
|
818
|
+
const { [name]: __, ...remainingLockSkills } = currentLockfile.skills;
|
|
819
|
+
void __;
|
|
820
|
+
const updatedLockfile = { ...currentLockfile, skills: remainingLockSkills };
|
|
821
|
+
yield* commitLockfileSnapshotUpdateAtPath(lockPath, currentLockfile, updatedLockfile).pipe(Effect.provide(fsLayer));
|
|
822
|
+
}
|
|
823
|
+
})).pipe(Effect.withSpan("WorkspaceMutations.removeSkill")),
|
|
824
|
+
removeSkillFromSettings: (name) => withMutex(Effect.gen(function* () {
|
|
825
|
+
const currentSettings = yield* readSettingsSafe(workspaceDir);
|
|
826
|
+
const currentSkills = currentSettings.skills ?? {};
|
|
827
|
+
if (!(name in currentSkills))
|
|
828
|
+
return; // no-op
|
|
829
|
+
const { [name]: _, ...remainingSkills } = currentSkills;
|
|
830
|
+
void _;
|
|
831
|
+
const updatedSettings = { ...currentSettings, skills: remainingSkills };
|
|
832
|
+
yield* writeScopedSettings(updatedSettings).pipe(Effect.provide(fsLayer));
|
|
833
|
+
})),
|
|
834
|
+
updateSkillEntry: (name, updater) => withMutex(Effect.gen(function* () {
|
|
835
|
+
const currentSettings = yield* readSettingsSafe(workspaceDir);
|
|
836
|
+
const currentSkills = currentSettings.skills ?? {};
|
|
837
|
+
const currentEntry = yield* getEntryOrFail(currentSkills, name, () => new SettingsEntryMissing({ entryType: "skill", name }));
|
|
838
|
+
const updated = updater(currentEntry);
|
|
839
|
+
const updatedSettings = {
|
|
840
|
+
...currentSettings,
|
|
841
|
+
skills: { ...currentSkills, [name]: updated },
|
|
842
|
+
};
|
|
843
|
+
yield* writeScopedSettings(updatedSettings).pipe(Effect.provide(fsLayer));
|
|
844
|
+
})),
|
|
845
|
+
setSkillEntry: (name, entry) => withMutex(Effect.gen(function* () {
|
|
846
|
+
const currentSettings = yield* readSettingsSafe(workspaceDir);
|
|
847
|
+
const currentSkills = currentSettings.skills ?? {};
|
|
848
|
+
const updatedSettings = {
|
|
849
|
+
...currentSettings,
|
|
850
|
+
skills: { ...currentSkills, [name]: entry },
|
|
851
|
+
};
|
|
852
|
+
yield* writeScopedSettings(updatedSettings).pipe(Effect.provide(fsLayer));
|
|
853
|
+
})),
|
|
854
|
+
addConfiguredAgent: (agentId) => withMutex(Effect.gen(function* () {
|
|
855
|
+
const validId = yield* Schema.decodeUnknownEffect(ConfigurableAgentIdSchema)(agentId).pipe(Effect.mapError((cause) => new InvalidAgentId({ agentId, cause })));
|
|
856
|
+
const current = yield* readSettingsSafe(workspaceDir);
|
|
857
|
+
const agents = current.agents ?? [];
|
|
858
|
+
if (agents.includes(validId))
|
|
859
|
+
return;
|
|
860
|
+
const updatedSettings = { ...current, agents: [...agents, validId] };
|
|
861
|
+
yield* writeScopedSettings(updatedSettings).pipe(Effect.provide(fsLayer));
|
|
862
|
+
})),
|
|
863
|
+
removeConfiguredAgent: (agentId) => withMutex(Effect.gen(function* () {
|
|
864
|
+
const validId = yield* Schema.decodeUnknownEffect(ConfigurableAgentIdSchema)(agentId).pipe(Effect.mapError((cause) => new InvalidAgentId({ agentId, cause })));
|
|
865
|
+
const current = yield* readSettingsSafe(workspaceDir);
|
|
866
|
+
const agents = current.agents ?? [];
|
|
867
|
+
if (!agents.includes(validId))
|
|
868
|
+
return;
|
|
869
|
+
const updatedSettings = {
|
|
870
|
+
...current,
|
|
871
|
+
agents: agents.filter((configured) => configured !== validId),
|
|
872
|
+
};
|
|
873
|
+
yield* writeScopedSettings(updatedSettings).pipe(Effect.provide(fsLayer));
|
|
874
|
+
})),
|
|
875
|
+
getConfiguredPackEntries: () => readSettingsSafe(workspaceDir).pipe(Effect.map((s) => s.packs ?? {}), Effect.withSpan("WorkspaceMutations.getConfiguredPackEntries")),
|
|
876
|
+
getLockedPacks: () => readLockfileSafe(workspaceDir).pipe(Effect.map((lf) => lf.packs ?? {})),
|
|
877
|
+
getLockedPack: (name) => readLockfileSafe(workspaceDir).pipe(Effect.map((lf) => Option.fromUndefinedOr((lf.packs ?? {})[name]))),
|
|
878
|
+
setPack: (args) => withMutex(Effect.gen(function* () {
|
|
879
|
+
const { versionRange, ...lockEntry } = args;
|
|
880
|
+
const name = lockEntry.name;
|
|
881
|
+
// Update settings — thread versionRange through so it's preserved
|
|
882
|
+
const fqn = formatFqn({
|
|
883
|
+
owner: args.owner,
|
|
884
|
+
type: "pack",
|
|
885
|
+
name: decodeExtensionNameSync(name),
|
|
886
|
+
});
|
|
887
|
+
const locator = Option.isSome(versionRange) ? `${fqn}@${versionRange.value}` : fqn;
|
|
888
|
+
const source = `${lockEntry.sourceName}:${locator}`;
|
|
889
|
+
const currentSettings = yield* readSettingsSafe(workspaceDir);
|
|
890
|
+
const currentPacks = currentSettings.packs ?? {};
|
|
891
|
+
const enabled = currentPacks[name]?.enabled ?? true;
|
|
892
|
+
const updatedSettings = {
|
|
893
|
+
...currentSettings,
|
|
894
|
+
packs: { ...currentPacks, [name]: { source, enabled } },
|
|
895
|
+
};
|
|
896
|
+
yield* writeScopedSettings(updatedSettings).pipe(Effect.provide(fsLayer));
|
|
897
|
+
// Update lockfile
|
|
898
|
+
const currentLockfile = yield* readLockfileSafe(workspaceDir);
|
|
899
|
+
const currentLockedPacks = currentLockfile.packs ?? {};
|
|
900
|
+
const updatedLockfile = {
|
|
901
|
+
...currentLockfile,
|
|
902
|
+
packs: {
|
|
903
|
+
...currentLockedPacks,
|
|
904
|
+
[name]: preserveAcceptedResolutionOnNoop(currentLockedPacks[name], lockEntry),
|
|
905
|
+
},
|
|
906
|
+
};
|
|
907
|
+
yield* commitWorkspaceState(currentLockfile, updatedLockfile);
|
|
908
|
+
})).pipe(Effect.withSpan("WorkspaceMutations.setPack")),
|
|
909
|
+
setPackLock: (args) => withMutex(Effect.gen(function* () {
|
|
910
|
+
const { versionRange: _, ...lockEntry } = args;
|
|
911
|
+
void _;
|
|
912
|
+
const currentLockfile = yield* readLockfileSafe(workspaceDir);
|
|
913
|
+
const currentLockedPacks = currentLockfile.packs ?? {};
|
|
914
|
+
const updatedLockfile = {
|
|
915
|
+
...currentLockfile,
|
|
916
|
+
packs: {
|
|
917
|
+
...currentLockedPacks,
|
|
918
|
+
[lockEntry.name]: preserveAcceptedResolutionOnNoop(currentLockedPacks[lockEntry.name], lockEntry),
|
|
919
|
+
},
|
|
920
|
+
};
|
|
921
|
+
yield* commitWorkspaceState(currentLockfile, updatedLockfile);
|
|
922
|
+
})).pipe(Effect.withSpan("WorkspaceMutations.setPackLock")),
|
|
923
|
+
setPackEntry: (name, entry) => withMutex(Effect.gen(function* () {
|
|
924
|
+
const currentSettings = yield* readSettingsSafe(workspaceDir);
|
|
925
|
+
const currentPacks = currentSettings.packs ?? {};
|
|
926
|
+
const updatedSettings = {
|
|
927
|
+
...currentSettings,
|
|
928
|
+
packs: { ...currentPacks, [name]: entry },
|
|
929
|
+
};
|
|
930
|
+
yield* writeScopedSettings(updatedSettings).pipe(Effect.provide(fsLayer));
|
|
931
|
+
})).pipe(Effect.withSpan("WorkspaceMutations.setPackEntry")),
|
|
932
|
+
removePack: (name) => withMutex(Effect.gen(function* () {
|
|
933
|
+
// Update settings
|
|
934
|
+
const currentSettings = yield* readSettingsSafe(workspaceDir);
|
|
935
|
+
const currentPacks = currentSettings.packs ?? {};
|
|
936
|
+
if (!(name in currentPacks))
|
|
937
|
+
return; // no-op
|
|
938
|
+
const { [name]: _, ...remainingPacks } = currentPacks;
|
|
939
|
+
void _;
|
|
940
|
+
const updatedSettings = { ...currentSettings, packs: remainingPacks };
|
|
941
|
+
yield* writeScopedSettings(updatedSettings).pipe(Effect.provide(fsLayer));
|
|
942
|
+
// Update lockfile
|
|
943
|
+
const currentLockfile = yield* readLockfileSafe(workspaceDir);
|
|
944
|
+
const currentLockedPacks = currentLockfile.packs ?? {};
|
|
945
|
+
if (name in currentLockedPacks) {
|
|
946
|
+
const { [name]: __, ...remainingLockedPacks } = currentLockedPacks;
|
|
947
|
+
void __;
|
|
948
|
+
const updatedLockfile = { ...currentLockfile, packs: remainingLockedPacks };
|
|
949
|
+
yield* commitLockfileSnapshotUpdateAtPath(lockPath, currentLockfile, updatedLockfile).pipe(Effect.provide(fsLayer));
|
|
950
|
+
}
|
|
951
|
+
})).pipe(Effect.withSpan("WorkspaceMutations.removePack")),
|
|
952
|
+
getPackDir: (name, owner, sourceName) => Effect.succeed(computePackPathsForLayout(path.join, layout, sourceName, owner, name)),
|
|
953
|
+
// -----------------------------------------------------------------------
|
|
954
|
+
// Subagent methods
|
|
955
|
+
// -----------------------------------------------------------------------
|
|
956
|
+
getLockedSubagents: () => readLockfileSafe(workspaceDir).pipe(Effect.map((lf) => lf.subagents ?? {})),
|
|
957
|
+
getLockedSubagent: (name) => readLockfileSafe(workspaceDir).pipe(Effect.map((lf) => Option.fromUndefinedOr((lf.subagents ?? {})[name]))),
|
|
958
|
+
getConfiguredSubagentEntries: () => readSettingsSafe(workspaceDir).pipe(Effect.map((s) => s.subagents ?? {}), Effect.withSpan("WorkspaceMutations.getConfiguredSubagentEntries")),
|
|
959
|
+
setSubagent: ({ name, lockEntry, versionRange }) => withMutex(Effect.gen(function* () {
|
|
960
|
+
// Update settings
|
|
961
|
+
const source = lockEntry.type === "registry"
|
|
962
|
+
? (() => {
|
|
963
|
+
const fqn = formatFqn({
|
|
964
|
+
owner: lockEntry.owner,
|
|
965
|
+
type: "subagent",
|
|
966
|
+
name: decodeExtensionNameSync(name),
|
|
967
|
+
});
|
|
968
|
+
const locator = Option.isSome(versionRange)
|
|
969
|
+
? `${fqn}@${versionRange.value}`
|
|
970
|
+
: fqn;
|
|
971
|
+
return `${lockEntry.sourceName}:${locator}`;
|
|
972
|
+
})()
|
|
973
|
+
: printSourceParams(lockEntryToSourceParams(lockEntry));
|
|
974
|
+
const currentSettings = yield* readSettingsSafe(workspaceDir);
|
|
975
|
+
const currentSubagents = currentSettings.subagents ?? {};
|
|
976
|
+
const nextSubagentEntry = { source, enabled: true };
|
|
977
|
+
const updatedSettings = {
|
|
978
|
+
...currentSettings,
|
|
979
|
+
subagents: { ...currentSubagents, [name]: nextSubagentEntry },
|
|
980
|
+
};
|
|
981
|
+
const currentLockfile = yield* readLockfileSafe(workspaceDir);
|
|
982
|
+
const currentLockedSubagents = currentLockfile.subagents ?? {};
|
|
983
|
+
const currentLockEntry = currentLockedSubagents[name];
|
|
984
|
+
const settingsChanged = !stableCompare(currentSubagents[name], nextSubagentEntry);
|
|
985
|
+
const lockChanged = !subagentLockEntrySemanticallyEqual(currentLockEntry, lockEntry);
|
|
986
|
+
if (settingsChanged) {
|
|
987
|
+
yield* writeScopedSettings(updatedSettings).pipe(Effect.provide(fsLayer));
|
|
988
|
+
}
|
|
989
|
+
if (!lockChanged)
|
|
990
|
+
return;
|
|
991
|
+
const updatedLockfile = {
|
|
992
|
+
...currentLockfile,
|
|
993
|
+
subagents: {
|
|
994
|
+
...currentLockedSubagents,
|
|
995
|
+
[name]: preserveAcceptedResolutionOnNoop(currentLockEntry, lockEntry),
|
|
996
|
+
},
|
|
997
|
+
};
|
|
998
|
+
yield* commitWorkspaceState(currentLockfile, updatedLockfile);
|
|
999
|
+
})).pipe(Effect.withSpan("WorkspaceMutations.setSubagent")),
|
|
1000
|
+
setSubagentLock: ({ name, lockEntry }) => withMutex(Effect.gen(function* () {
|
|
1001
|
+
// Update lockfile only (skip settings) — used for pack dependencies
|
|
1002
|
+
const currentLockfile = yield* readLockfileSafe(workspaceDir);
|
|
1003
|
+
const currentLockedSubagents = currentLockfile.subagents ?? {};
|
|
1004
|
+
if (subagentLockEntrySemanticallyEqual(currentLockedSubagents[name], lockEntry)) {
|
|
1005
|
+
return;
|
|
1006
|
+
}
|
|
1007
|
+
const updatedLockfile = {
|
|
1008
|
+
...currentLockfile,
|
|
1009
|
+
subagents: {
|
|
1010
|
+
...currentLockedSubagents,
|
|
1011
|
+
[name]: preserveAcceptedResolutionOnNoop(currentLockedSubagents[name], lockEntry),
|
|
1012
|
+
},
|
|
1013
|
+
};
|
|
1014
|
+
yield* commitWorkspaceState(currentLockfile, updatedLockfile);
|
|
1015
|
+
})),
|
|
1016
|
+
removeSubagent: (name) => withMutex(Effect.gen(function* () {
|
|
1017
|
+
// Update settings
|
|
1018
|
+
const currentSettings = yield* readSettingsSafe(workspaceDir);
|
|
1019
|
+
const currentSubagents = currentSettings.subagents ?? {};
|
|
1020
|
+
const hasSettingsEntry = name in currentSubagents;
|
|
1021
|
+
if (hasSettingsEntry) {
|
|
1022
|
+
const { [name]: _, ...remainingSubagents } = currentSubagents;
|
|
1023
|
+
void _;
|
|
1024
|
+
const updatedSettings = { ...currentSettings, subagents: remainingSubagents };
|
|
1025
|
+
yield* writeScopedSettings(updatedSettings).pipe(Effect.provide(fsLayer));
|
|
1026
|
+
}
|
|
1027
|
+
// Update lockfile
|
|
1028
|
+
const currentLockfile = yield* readLockfileSafe(workspaceDir);
|
|
1029
|
+
const currentLockedSubagents = currentLockfile.subagents ?? {};
|
|
1030
|
+
if (name in currentLockedSubagents) {
|
|
1031
|
+
const { [name]: __, ...remainingLockedSubagents } = currentLockedSubagents;
|
|
1032
|
+
void __;
|
|
1033
|
+
const updatedLockfile = {
|
|
1034
|
+
...currentLockfile,
|
|
1035
|
+
subagents: remainingLockedSubagents,
|
|
1036
|
+
};
|
|
1037
|
+
yield* commitLockfileSnapshotUpdateAtPath(lockPath, currentLockfile, updatedLockfile).pipe(Effect.provide(fsLayer));
|
|
1038
|
+
}
|
|
1039
|
+
})).pipe(Effect.withSpan("WorkspaceMutations.removeSubagent")),
|
|
1040
|
+
updateSubagentEntry: (name, updater) => withMutex(Effect.gen(function* () {
|
|
1041
|
+
const currentSettings = yield* readSettingsSafe(workspaceDir);
|
|
1042
|
+
const currentSubagents = currentSettings.subagents ?? {};
|
|
1043
|
+
const existingEntry = currentSubagents[name];
|
|
1044
|
+
if (existingEntry === undefined)
|
|
1045
|
+
return;
|
|
1046
|
+
const updated = updater(existingEntry);
|
|
1047
|
+
const updatedSettings = {
|
|
1048
|
+
...currentSettings,
|
|
1049
|
+
subagents: { ...currentSubagents, [name]: updated },
|
|
1050
|
+
};
|
|
1051
|
+
yield* writeScopedSettings(updatedSettings).pipe(Effect.provide(fsLayer));
|
|
1052
|
+
})).pipe(Effect.withSpan("WorkspaceMutations.updateSubagentEntry")),
|
|
1053
|
+
setSubagentEntry: (name, entry) => withMutex(Effect.gen(function* () {
|
|
1054
|
+
const currentSettings = yield* readSettingsSafe(workspaceDir);
|
|
1055
|
+
const currentSubagents = currentSettings.subagents ?? {};
|
|
1056
|
+
const updatedSettings = {
|
|
1057
|
+
...currentSettings,
|
|
1058
|
+
subagents: { ...currentSubagents, [name]: entry },
|
|
1059
|
+
};
|
|
1060
|
+
yield* writeScopedSettings(updatedSettings).pipe(Effect.provide(fsLayer));
|
|
1061
|
+
})).pipe(Effect.withSpan("WorkspaceMutations.setSubagentEntry")),
|
|
1062
|
+
removeSubagentSettings: (name) => withMutex(Effect.gen(function* () {
|
|
1063
|
+
const currentSettings = yield* readSettingsSafe(workspaceDir);
|
|
1064
|
+
const currentSubagents = currentSettings.subagents ?? {};
|
|
1065
|
+
if (!(name in currentSubagents))
|
|
1066
|
+
return; // no-op
|
|
1067
|
+
const { [name]: _, ...remainingSubagents } = currentSubagents;
|
|
1068
|
+
void _;
|
|
1069
|
+
const updatedSettings = { ...currentSettings, subagents: remainingSubagents };
|
|
1070
|
+
yield* writeScopedSettings(updatedSettings).pipe(Effect.provide(fsLayer));
|
|
1071
|
+
})),
|
|
1072
|
+
removeSubagentLock: (name) => withMutex(Effect.gen(function* () {
|
|
1073
|
+
const currentLockfile = yield* readLockfileSafe(workspaceDir);
|
|
1074
|
+
const currentLockedSubagents = currentLockfile.subagents ?? {};
|
|
1075
|
+
if (!(name in currentLockedSubagents))
|
|
1076
|
+
return;
|
|
1077
|
+
const { [name]: _, ...remaining } = currentLockedSubagents;
|
|
1078
|
+
void _;
|
|
1079
|
+
const updatedLockfile = { ...currentLockfile, subagents: remaining };
|
|
1080
|
+
yield* commitLockfileSnapshotUpdateAtPath(lockPath, currentLockfile, updatedLockfile).pipe(Effect.provide(fsLayer));
|
|
1081
|
+
})).pipe(Effect.withSpan("WorkspaceMutations.removeSubagentLock")),
|
|
1082
|
+
// -----------------------------------------------------------------------
|
|
1083
|
+
// MCP Server methods
|
|
1084
|
+
// -----------------------------------------------------------------------
|
|
1085
|
+
getLockedMcpServers: () => readLockfileSafe(workspaceDir).pipe(Effect.map((lf) => lf.mcpServers ?? {})),
|
|
1086
|
+
getLockedMcpServer: (name) => readLockfileSafe(workspaceDir).pipe(Effect.map((lf) => Option.fromUndefinedOr((lf.mcpServers ?? {})[name]))),
|
|
1087
|
+
setMcpServer: ({ name, lockEntry, versionRange, env, enabled, agents }) => withMutex(Effect.gen(function* () {
|
|
1088
|
+
// Update settings (uses "mcpServers" key)
|
|
1089
|
+
const currentSettings = yield* readSettingsSafe(workspaceDir);
|
|
1090
|
+
const currentMcpServers = currentSettings.mcpServers ?? {};
|
|
1091
|
+
const currentEnabled = currentMcpServers[name]?.enabled ?? true;
|
|
1092
|
+
const currentEnv = currentMcpServers[name]?.env ?? {};
|
|
1093
|
+
const currentAgents = currentMcpServers[name]?.agents;
|
|
1094
|
+
const settingsEntry = {
|
|
1095
|
+
kind: "sourced",
|
|
1096
|
+
source: lockEntry.type === "registry"
|
|
1097
|
+
? (() => {
|
|
1098
|
+
const fqn = formatFqn({
|
|
1099
|
+
owner: lockEntry.owner,
|
|
1100
|
+
type: "mcp-server",
|
|
1101
|
+
name: decodeExtensionNameSync(name),
|
|
1102
|
+
});
|
|
1103
|
+
const locator = Option.isSome(versionRange)
|
|
1104
|
+
? `${fqn}@${versionRange.value}`
|
|
1105
|
+
: fqn;
|
|
1106
|
+
return `${lockEntry.sourceName}:${locator}`;
|
|
1107
|
+
})()
|
|
1108
|
+
: printSourceParams(lockEntryToSourceParams(lockEntry)),
|
|
1109
|
+
enabled: enabled ?? currentEnabled,
|
|
1110
|
+
env: env ?? currentEnv,
|
|
1111
|
+
...((agents ?? currentAgents) === undefined
|
|
1112
|
+
? {}
|
|
1113
|
+
: { agents: agents ?? currentAgents }),
|
|
1114
|
+
};
|
|
1115
|
+
const updatedSettings = {
|
|
1116
|
+
...currentSettings,
|
|
1117
|
+
mcpServers: {
|
|
1118
|
+
...currentMcpServers,
|
|
1119
|
+
[name]: settingsEntry,
|
|
1120
|
+
},
|
|
1121
|
+
};
|
|
1122
|
+
yield* writeScopedSettings(updatedSettings).pipe(Effect.provide(fsLayer));
|
|
1123
|
+
// Update lockfile (uses "mcpServers" key)
|
|
1124
|
+
const currentLockfile = yield* readLockfileSafe(workspaceDir);
|
|
1125
|
+
const currentLockedMcpServers = currentLockfile.mcpServers ?? {};
|
|
1126
|
+
const updatedLockfile = {
|
|
1127
|
+
...currentLockfile,
|
|
1128
|
+
mcpServers: {
|
|
1129
|
+
...currentLockedMcpServers,
|
|
1130
|
+
[name]: preserveAcceptedResolutionOnNoop(currentLockedMcpServers[name], lockEntry),
|
|
1131
|
+
},
|
|
1132
|
+
};
|
|
1133
|
+
yield* commitWorkspaceState(currentLockfile, updatedLockfile);
|
|
1134
|
+
})).pipe(Effect.withSpan("WorkspaceMutations.setMcpServer")),
|
|
1135
|
+
setMcpServerLock: ({ name, lockEntry }) => withMutex(Effect.gen(function* () {
|
|
1136
|
+
// Update lockfile only (skip settings) — used for pack dependencies
|
|
1137
|
+
const currentLockfile = yield* readLockfileSafe(workspaceDir);
|
|
1138
|
+
const currentLockedMcpServers = currentLockfile.mcpServers ?? {};
|
|
1139
|
+
const updatedLockfile = {
|
|
1140
|
+
...currentLockfile,
|
|
1141
|
+
mcpServers: {
|
|
1142
|
+
...currentLockedMcpServers,
|
|
1143
|
+
[name]: preserveAcceptedResolutionOnNoop(currentLockedMcpServers[name], lockEntry),
|
|
1144
|
+
},
|
|
1145
|
+
};
|
|
1146
|
+
yield* commitWorkspaceState(currentLockfile, updatedLockfile);
|
|
1147
|
+
})),
|
|
1148
|
+
updateMcpServerEntry: (name, updater) => withMutex(Effect.gen(function* () {
|
|
1149
|
+
const currentSettings = yield* readSettingsSafe(workspaceDir);
|
|
1150
|
+
const currentMcpServers = currentSettings.mcpServers ?? {};
|
|
1151
|
+
const currentEntry = yield* getEntryOrFail(currentMcpServers, name, () => new SettingsEntryMissing({ entryType: "mcp-server", name }));
|
|
1152
|
+
const updated = updater(currentEntry);
|
|
1153
|
+
const updatedSettings = {
|
|
1154
|
+
...currentSettings,
|
|
1155
|
+
mcpServers: { ...currentMcpServers, [name]: updated },
|
|
1156
|
+
};
|
|
1157
|
+
yield* writeScopedSettings(updatedSettings).pipe(Effect.provide(fsLayer));
|
|
1158
|
+
})),
|
|
1159
|
+
setMcpServerEntry: (name, entry) => withMutex(Effect.gen(function* () {
|
|
1160
|
+
const currentSettings = yield* readSettingsSafe(workspaceDir);
|
|
1161
|
+
const currentMcpServers = currentSettings.mcpServers ?? {};
|
|
1162
|
+
const updatedSettings = {
|
|
1163
|
+
...currentSettings,
|
|
1164
|
+
mcpServers: { ...currentMcpServers, [name]: entry },
|
|
1165
|
+
};
|
|
1166
|
+
yield* writeScopedSettings(updatedSettings).pipe(Effect.provide(fsLayer));
|
|
1167
|
+
})),
|
|
1168
|
+
removeMcpServer: (name) => withMutex(Effect.gen(function* () {
|
|
1169
|
+
// Update settings (uses "mcpServers" key)
|
|
1170
|
+
const currentSettings = yield* readSettingsSafe(workspaceDir);
|
|
1171
|
+
const currentMcpServers = currentSettings.mcpServers ?? {};
|
|
1172
|
+
const hasSettingsEntry = name in currentMcpServers;
|
|
1173
|
+
if (hasSettingsEntry) {
|
|
1174
|
+
const { [name]: _, ...remainingMcpServers } = currentMcpServers;
|
|
1175
|
+
void _;
|
|
1176
|
+
const updatedSettings = {
|
|
1177
|
+
...currentSettings,
|
|
1178
|
+
mcpServers: remainingMcpServers,
|
|
1179
|
+
};
|
|
1180
|
+
yield* writeScopedSettings(updatedSettings).pipe(Effect.provide(fsLayer));
|
|
1181
|
+
}
|
|
1182
|
+
// Update lockfile (uses "mcpServers" key)
|
|
1183
|
+
const currentLockfile = yield* readLockfileSafe(workspaceDir);
|
|
1184
|
+
const currentLockedMcpServers = currentLockfile.mcpServers ?? {};
|
|
1185
|
+
if (name in currentLockedMcpServers) {
|
|
1186
|
+
const { [name]: __, ...remainingLockedMcpServers } = currentLockedMcpServers;
|
|
1187
|
+
void __;
|
|
1188
|
+
const updatedLockfile = {
|
|
1189
|
+
...currentLockfile,
|
|
1190
|
+
mcpServers: remainingLockedMcpServers,
|
|
1191
|
+
};
|
|
1192
|
+
yield* commitLockfileSnapshotUpdateAtPath(lockPath, currentLockfile, updatedLockfile).pipe(Effect.provide(fsLayer));
|
|
1193
|
+
}
|
|
1194
|
+
})).pipe(Effect.withSpan("WorkspaceMutations.removeMcpServer")),
|
|
1195
|
+
// -----------------------------------------------------------------------
|
|
1196
|
+
// Granular removal methods (settings-only or lockfile-only)
|
|
1197
|
+
// -----------------------------------------------------------------------
|
|
1198
|
+
removeSkillLock: (name) => withMutex(Effect.gen(function* () {
|
|
1199
|
+
const currentLockfile = yield* readLockfileSafe(workspaceDir);
|
|
1200
|
+
if (!(name in currentLockfile.skills))
|
|
1201
|
+
return;
|
|
1202
|
+
const { [name]: _, ...remainingSkills } = currentLockfile.skills;
|
|
1203
|
+
void _;
|
|
1204
|
+
const updatedLockfile = { ...currentLockfile, skills: remainingSkills };
|
|
1205
|
+
yield* commitLockfileSnapshotUpdateAtPath(lockPath, currentLockfile, updatedLockfile).pipe(Effect.provide(fsLayer));
|
|
1206
|
+
})).pipe(Effect.withSpan("WorkspaceMutations.removeSkillLock")),
|
|
1207
|
+
removeMcpServerSettings: (name) => withMutex(Effect.gen(function* () {
|
|
1208
|
+
const currentSettings = yield* readSettingsSafe(workspaceDir);
|
|
1209
|
+
const currentMcpServers = currentSettings.mcpServers ?? {};
|
|
1210
|
+
if (!(name in currentMcpServers))
|
|
1211
|
+
return;
|
|
1212
|
+
const { [name]: _, ...remainingMcpServers } = currentMcpServers;
|
|
1213
|
+
void _;
|
|
1214
|
+
const updatedSettings = { ...currentSettings, mcpServers: remainingMcpServers };
|
|
1215
|
+
yield* writeScopedSettings(updatedSettings).pipe(Effect.provide(fsLayer));
|
|
1216
|
+
})),
|
|
1217
|
+
removeMcpServerLock: (name) => withMutex(Effect.gen(function* () {
|
|
1218
|
+
const currentLockfile = yield* readLockfileSafe(workspaceDir);
|
|
1219
|
+
const currentLockedMcpServers = currentLockfile.mcpServers ?? {};
|
|
1220
|
+
if (!(name in currentLockedMcpServers))
|
|
1221
|
+
return;
|
|
1222
|
+
const { [name]: _, ...remainingMcpServers } = currentLockedMcpServers;
|
|
1223
|
+
void _;
|
|
1224
|
+
const updatedLockfile = { ...currentLockfile, mcpServers: remainingMcpServers };
|
|
1225
|
+
yield* commitLockfileSnapshotUpdateAtPath(lockPath, currentLockfile, updatedLockfile).pipe(Effect.provide(fsLayer));
|
|
1226
|
+
})),
|
|
1227
|
+
removePackSettings: (name) => withMutex(Effect.gen(function* () {
|
|
1228
|
+
const currentSettings = yield* readSettingsSafe(workspaceDir);
|
|
1229
|
+
const currentPacks = currentSettings.packs ?? {};
|
|
1230
|
+
if (!(name in currentPacks))
|
|
1231
|
+
return;
|
|
1232
|
+
const { [name]: _, ...remainingPacks } = currentPacks;
|
|
1233
|
+
void _;
|
|
1234
|
+
const updatedSettings = { ...currentSettings, packs: remainingPacks };
|
|
1235
|
+
yield* writeScopedSettings(updatedSettings).pipe(Effect.provide(fsLayer));
|
|
1236
|
+
})),
|
|
1237
|
+
removePackLock: (name) => withMutex(Effect.gen(function* () {
|
|
1238
|
+
const currentLockfile = yield* readLockfileSafe(workspaceDir);
|
|
1239
|
+
const currentLockedPacks = currentLockfile.packs ?? {};
|
|
1240
|
+
if (!(name in currentLockedPacks))
|
|
1241
|
+
return;
|
|
1242
|
+
const { [name]: _, ...remainingPacks } = currentLockedPacks;
|
|
1243
|
+
void _;
|
|
1244
|
+
const updatedLockfile = { ...currentLockfile, packs: remainingPacks };
|
|
1245
|
+
yield* commitLockfileSnapshotUpdateAtPath(lockPath, currentLockfile, updatedLockfile).pipe(Effect.provide(fsLayer));
|
|
1246
|
+
})),
|
|
1247
|
+
// -----------------------------------------------------------------------
|
|
1248
|
+
// Pack dependency queries
|
|
1249
|
+
// -----------------------------------------------------------------------
|
|
1250
|
+
isExtensionRequiredByInstalledPack: (target) => Effect.gen(function* () {
|
|
1251
|
+
if (target.type === "pack")
|
|
1252
|
+
return false;
|
|
1253
|
+
const graph = yield* readDesiredStateGraph();
|
|
1254
|
+
if (!graph.complete) {
|
|
1255
|
+
return yield* new DesiredPackGraphIncomplete();
|
|
1256
|
+
}
|
|
1257
|
+
return graph.nodes.some((node) => node.type === target.type &&
|
|
1258
|
+
node.name === target.name &&
|
|
1259
|
+
node.origins.some((origin) => origin.type === "pack"));
|
|
1260
|
+
}),
|
|
1261
|
+
};
|
|
1262
|
+
});
|
|
1263
|
+
//# sourceMappingURL=service.js.map
|