code-workspace-zhuiyi 0.1.0-beta.1

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.
Files changed (98) hide show
  1. package/.env.example +3 -0
  2. package/LICENSE +21 -0
  3. package/README.md +168 -0
  4. package/README.zh-CN.md +168 -0
  5. package/artifacts/manifest.json +183 -0
  6. package/artifacts/templates/USER_GUIDE.template.md +1 -0
  7. package/artifacts/templates/agents/WORKSPACE_GUARD.md.template +73 -0
  8. package/artifacts/templates/agents/skills/code-workspace-resolve-branch/SKILL.md +88 -0
  9. package/artifacts/templates/agents/skills/code-workspace-resolve-branch/evals/evals.json +45 -0
  10. package/artifacts/templates/claude/commands/code-workspace/add-projects.md +23 -0
  11. package/artifacts/templates/codex/hooks.json +86 -0
  12. package/artifacts/templates/codex/skills/code-workspace-add-projects/SKILL.md +28 -0
  13. package/artifacts/templates/user-guide/en-US.md +97 -0
  14. package/artifacts/templates/user-guide/zh-CN.md +97 -0
  15. package/assets/i18n-icon.svg +1 -0
  16. package/assets/logo-vector.svg +30 -0
  17. package/assets/request_tip.mp3 +0 -0
  18. package/assets/session_finish.mp3 +0 -0
  19. package/bin/code-workspace.js +11 -0
  20. package/docs/extension-architecture.zh-CN.md +429 -0
  21. package/docs/extensions.md +64 -0
  22. package/docs/extensions.zh-CN.md +64 -0
  23. package/extensions/openspec-workspace/1.0.0/artifacts/claude/SKILL.md +16 -0
  24. package/extensions/openspec-workspace/1.0.0/artifacts/codex/SKILL.md +16 -0
  25. package/extensions/openspec-workspace/1.0.0/init.js +54 -0
  26. package/extensions/openspec-workspace/1.0.0/manifest.json +27 -0
  27. package/extensions/zhuiyi-jira-mcp/0.1.0/artifacts/claude/server.json +14 -0
  28. package/extensions/zhuiyi-jira-mcp/0.1.0/artifacts/codex/config.toml +11 -0
  29. package/extensions/zhuiyi-jira-mcp/0.1.0/init.js +53 -0
  30. package/extensions/zhuiyi-jira-mcp/0.1.0/lib/archive.js +196 -0
  31. package/extensions/zhuiyi-jira-mcp/0.1.0/manifest.json +38 -0
  32. package/extensions/zhuiyi-jira-mcp/0.1.0/release.json +15 -0
  33. package/package.json +58 -0
  34. package/schemas/extension-init-context-v1.json +39 -0
  35. package/schemas/extension-init-result-v1.json +42 -0
  36. package/schemas/extension-manifest-v1.json +55 -0
  37. package/schemas/extension-manifest-v2.json +84 -0
  38. package/schemas/extension-manifest-v3.json +84 -0
  39. package/spec/extension/v1/specification.en-US.md +171 -0
  40. package/spec/extension/v1/specification.zh-CN.md +170 -0
  41. package/src/cli/commands/completion.js +203 -0
  42. package/src/cli/commands/extension.js +193 -0
  43. package/src/cli/commands/help.js +36 -0
  44. package/src/cli/commands/init.js +200 -0
  45. package/src/cli/commands/monitor.js +62 -0
  46. package/src/cli/commands/permissions.js +33 -0
  47. package/src/cli/commands/project-branch-update.js +107 -0
  48. package/src/cli/commands/project-branch.js +433 -0
  49. package/src/cli/commands/project.js +259 -0
  50. package/src/cli/commands/update.js +146 -0
  51. package/src/cli/commands/workspace.js +24 -0
  52. package/src/cli/confirmation.js +22 -0
  53. package/src/cli/parser.js +108 -0
  54. package/src/cli/registry.js +108 -0
  55. package/src/cli/renderer.js +20 -0
  56. package/src/cli/result.js +118 -0
  57. package/src/cli.js +76 -0
  58. package/src/core/assets.js +83 -0
  59. package/src/core/config.js +378 -0
  60. package/src/core/diagnostics.js +20 -0
  61. package/src/core/directory-digest.js +34 -0
  62. package/src/core/doctor.js +115 -0
  63. package/src/core/errors.js +10 -0
  64. package/src/core/extension-artifacts-legacy.js +180 -0
  65. package/src/core/extension-artifacts.js +310 -0
  66. package/src/core/extensions.js +1216 -0
  67. package/src/core/fs.js +32 -0
  68. package/src/core/init-lock-holder.js +30 -0
  69. package/src/core/init-lock.js +159 -0
  70. package/src/core/init.js +160 -0
  71. package/src/core/initializer.js +233 -0
  72. package/src/core/language.js +102 -0
  73. package/src/core/managed-files.js +334 -0
  74. package/src/core/migration.js +68 -0
  75. package/src/core/permissions/claude.js +92 -0
  76. package/src/core/permissions/codex.js +126 -0
  77. package/src/core/permissions/common.js +44 -0
  78. package/src/core/permissions/index.js +163 -0
  79. package/src/core/project-branch-update.js +424 -0
  80. package/src/core/project-configuration.js +55 -0
  81. package/src/core/project.js +674 -0
  82. package/src/core/tools.js +34 -0
  83. package/src/core/transaction.js +132 -0
  84. package/src/core/validation.js +186 -0
  85. package/src/i18n/index.js +11 -0
  86. package/src/i18n/interpolate.js +7 -0
  87. package/src/i18n/locales/en-US.js +10 -0
  88. package/src/i18n/locales/zh-CN.js +11 -0
  89. package/src/i18n/registry.js +42 -0
  90. package/src/index.js +25 -0
  91. package/src/init/plan.js +12 -0
  92. package/src/init/ui.js +61 -0
  93. package/src/init/wizard.js +97 -0
  94. package/src/monitor/i18n/index.js +33 -0
  95. package/src/monitor/i18n/locales/en-US.js +85 -0
  96. package/src/monitor/i18n/locales/zh-CN.js +79 -0
  97. package/src/monitor/index.js +344 -0
  98. package/src/monitor/page.js +118 -0
@@ -0,0 +1,424 @@
1
+ const { spawnSync } = require("node:child_process");
2
+ const fs = require("node:fs");
3
+ const path = require("node:path");
4
+
5
+ const { WorkspaceError } = require("./errors");
6
+ const { attachRetainedEffects } = require("./transaction");
7
+ const { findRegisteredProject, gitWorktreeClean, inspectGitWorktree } = require("./project");
8
+ const { loadConfigProjection } = require("./config");
9
+
10
+ const DEFAULT_GIT_TIMEOUT = 30000;
11
+
12
+ function canonicalLocation(location) {
13
+ try {
14
+ return fs.realpathSync(location);
15
+ } catch {
16
+ return path.resolve(location);
17
+ }
18
+ }
19
+
20
+ function executeGit(location, args, options = {}) {
21
+ const result = spawnSync("git", ["-C", location, ...args], {
22
+ encoding: "utf8",
23
+ timeout: options.timeout || DEFAULT_GIT_TIMEOUT,
24
+ });
25
+ if (result.error) {
26
+ throw new WorkspaceError("GIT_COMMAND_FAILED", result.error.message, {
27
+ location,
28
+ args,
29
+ cause: result.error.code || result.error.name,
30
+ });
31
+ }
32
+ return result;
33
+ }
34
+
35
+ function runGit(location, args, options = {}) {
36
+ const result = executeGit(location, args, options);
37
+ if (result.status !== 0) {
38
+ const detail = String(result.stderr || result.stdout || "").trim().split(/\r?\n/)[0];
39
+ throw new WorkspaceError("GIT_COMMAND_FAILED", detail || `git ${args.join(" ")} failed`, {
40
+ location,
41
+ args,
42
+ exitCode: result.status,
43
+ });
44
+ }
45
+ return String(result.stdout || "").trim();
46
+ }
47
+
48
+ function projectUpdateError(code, message, project, details = {}) {
49
+ return new WorkspaceError(code, message, {
50
+ project: project.name,
51
+ location: project.location,
52
+ registeredBranch: project.branch,
53
+ ...details,
54
+ });
55
+ }
56
+
57
+ function updateProjectPolicy(project) {
58
+ if (project.updateLatest === undefined) return false;
59
+ if (typeof project.updateLatest !== "boolean") {
60
+ throw projectUpdateError(
61
+ "PROJECT_UPDATE_LATEST_INVALID",
62
+ `Project ${project.name} updateLatest must be a boolean when present.`,
63
+ project,
64
+ { field: "updateLatest", actual: project.updateLatest }
65
+ );
66
+ }
67
+ return project.updateLatest;
68
+ }
69
+
70
+ function gitHead(location, ref = "HEAD", options = {}) {
71
+ return runGit(location, ["rev-parse", ref], options);
72
+ }
73
+
74
+ function gitUpstream(location, branch, options = {}) {
75
+ const upstreamResult = executeGit(location, ["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{upstream}"], options);
76
+ const upstreamError = String(upstreamResult.stderr || upstreamResult.stdout || "");
77
+ if (upstreamResult.status === 1 || /no upstream|upstream/i.test(upstreamError)) {
78
+ throw new WorkspaceError("PROJECT_BRANCH_UPSTREAM_MISSING", `Project branch ${branch} has no configured upstream.`, {
79
+ location,
80
+ branch,
81
+ remediation: "Configure an upstream branch manually, then retry.",
82
+ });
83
+ }
84
+ if (upstreamResult.status !== 0) {
85
+ const detail = String(upstreamResult.stderr || upstreamResult.stdout || "").trim();
86
+ throw new WorkspaceError("GIT_COMMAND_FAILED", detail || "git rev-parse upstream failed", {
87
+ location,
88
+ branch,
89
+ args: ["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{upstream}"],
90
+ exitCode: upstreamResult.status,
91
+ });
92
+ }
93
+ const upstream = String(upstreamResult.stdout || "").trim();
94
+ let remote;
95
+ let merge;
96
+ try {
97
+ remote = runGit(location, ["config", "--get", `branch.${branch}.remote`], options);
98
+ merge = runGit(location, ["config", "--get", `branch.${branch}.merge`], options);
99
+ } catch (error) {
100
+ throw new WorkspaceError("PROJECT_BRANCH_UPSTREAM_MISSING", `Project branch ${branch} has no configured upstream.`, {
101
+ location,
102
+ branch,
103
+ upstream,
104
+ cause: error.code || error.name,
105
+ remediation: "Configure an upstream branch manually, then retry.",
106
+ });
107
+ }
108
+ if (!remote || !merge) {
109
+ throw new WorkspaceError("PROJECT_BRANCH_UPSTREAM_MISSING", `Project branch ${branch} has no configured upstream.`, {
110
+ location,
111
+ branch,
112
+ upstream: upstream || null,
113
+ remediation: "Configure an upstream branch manually, then retry.",
114
+ });
115
+ }
116
+ return { name: upstream, remote, merge };
117
+ }
118
+
119
+ function inspectLatestBranchState(project, options = {}) {
120
+ const inspect = options.inspectGitWorktree || inspectGitWorktree;
121
+ const clean = options.gitWorktreeClean || gitWorktreeClean;
122
+ try {
123
+ const actual = inspect(project.location);
124
+ const worktreeClean = clean(project.location);
125
+ const head = (options.gitHead || gitHead)(project.location, "HEAD", options);
126
+ let upstream = null;
127
+ try {
128
+ upstream = (options.gitUpstream || gitUpstream)(project.location, actual.branch, options);
129
+ } catch (error) {
130
+ if (error.code !== "PROJECT_BRANCH_UPSTREAM_MISSING") throw error;
131
+ upstream = null;
132
+ }
133
+ return {
134
+ project: {
135
+ name: project.name,
136
+ location: actual.realPath || project.location,
137
+ },
138
+ registeredBranch: project.branch,
139
+ actualBranch: actual.branch,
140
+ matches: project.branch === actual.branch,
141
+ worktreeClean,
142
+ head,
143
+ upstream,
144
+ };
145
+ } catch (error) {
146
+ if (error.code === "PROJECT_UPDATE_LATEST_INVALID" || error.code === "PROJECT_BRANCH_UPSTREAM_MISSING") throw error;
147
+ throw projectUpdateError(
148
+ "PROJECT_BRANCH_UPDATE_INSPECTION_FAILED",
149
+ `Cannot inspect latest-branch state for project ${project.name}: ${error.message}`,
150
+ project,
151
+ {
152
+ actualBranch: error.details?.actualBranch || null,
153
+ cause: error.code || error.name,
154
+ remediation: "Inspect the selected Git worktree and retry the command.",
155
+ }
156
+ );
157
+ }
158
+ }
159
+
160
+ function sameUpdatePlan(expected, observed) {
161
+ return expected.registeredBranch === observed.registeredBranch
162
+ && expected.actualBranch === observed.actualBranch
163
+ && expected.worktreeClean === observed.worktreeClean
164
+ && expected.head === observed.head
165
+ && expected.upstream?.name === observed.upstream?.name;
166
+ }
167
+
168
+ function staleUpdatePlanError(project, expected, observed) {
169
+ return projectUpdateError(
170
+ "PROJECT_BRANCH_UPDATE_PLAN_STALE",
171
+ `Project ${project.name} changed while preparing its latest-branch update.`,
172
+ project,
173
+ {
174
+ expectedState: expected,
175
+ observedState: observed,
176
+ remediation: "Reinspect the selected project and retry the latest-branch update.",
177
+ }
178
+ );
179
+ }
180
+
181
+ function assertUpdatePreconditions(project, state) {
182
+ if (!state.matches) {
183
+ throw projectUpdateError(
184
+ "PROJECT_BRANCH_MISMATCH",
185
+ `Project ${project.name} registered branch ${state.registeredBranch} does not match actual branch ${state.actualBranch}.`,
186
+ project,
187
+ {
188
+ actualBranch: state.actualBranch,
189
+ remediation: "Resolve the branch mismatch first, then retry the latest-branch update.",
190
+ }
191
+ );
192
+ }
193
+ if (!state.worktreeClean) {
194
+ throw projectUpdateError(
195
+ "PROJECT_WORKTREE_DIRTY",
196
+ `Project ${project.name} has uncommitted changes; its latest branch cannot be updated automatically.`,
197
+ project,
198
+ {
199
+ actualBranch: state.actualBranch,
200
+ remediation: "Commit, discard, or otherwise resolve the selected worktree changes manually, then retry.",
201
+ }
202
+ );
203
+ }
204
+ if (!state.upstream) {
205
+ throw projectUpdateError(
206
+ "PROJECT_BRANCH_UPSTREAM_MISSING",
207
+ `Project branch ${state.actualBranch} has no configured upstream.`,
208
+ project,
209
+ {
210
+ actualBranch: state.actualBranch,
211
+ remediation: "Configure an upstream branch manually, then retry.",
212
+ }
213
+ );
214
+ }
215
+ }
216
+
217
+ function planLatestBranchUpdate(config, name, options = {}) {
218
+ const project = (options.findRegisteredProject || findRegisteredProject)(config, name);
219
+ const enabled = updateProjectPolicy(project);
220
+ if (!enabled) {
221
+ return {
222
+ action: "skip",
223
+ reason: "disabled",
224
+ project: { name: project.name, location: project.location },
225
+ updateLatest: false,
226
+ registeredBranch: project.branch,
227
+ };
228
+ }
229
+ const state = (options.inspectLatestBranchState || inspectLatestBranchState)(project, options);
230
+ assertUpdatePreconditions(project, state);
231
+ const targetHead = state.upstream
232
+ ? (options.gitHead || gitHead)(project.location, state.upstream.name, options)
233
+ : null;
234
+ return {
235
+ action: "update",
236
+ reason: null,
237
+ project: state.project,
238
+ updateLatest: true,
239
+ registeredBranch: state.registeredBranch,
240
+ actualBranch: state.actualBranch,
241
+ worktreeClean: state.worktreeClean,
242
+ beforeHead: state.head,
243
+ targetHead,
244
+ upstream: state.upstream.name,
245
+ upstreamRemote: state.upstream.remote,
246
+ upstreamMerge: state.upstream.merge,
247
+ };
248
+ }
249
+
250
+ function fetchUpstream(location, upstream, options = {}) {
251
+ try {
252
+ (options.runGit || runGit)(location, ["fetch", "--no-tags", upstream.remote, upstream.merge], options);
253
+ } catch (error) {
254
+ throw new WorkspaceError(
255
+ "PROJECT_BRANCH_FETCH_FAILED",
256
+ `Could not fetch upstream ${upstream.name}: ${error.message}`,
257
+ {
258
+ location,
259
+ upstream: upstream.name,
260
+ remote: upstream.remote,
261
+ merge: upstream.merge,
262
+ cause: error.code || error.name,
263
+ remediation: "Check network access and Git credentials, then retry.",
264
+ }
265
+ );
266
+ }
267
+ }
268
+
269
+ function assertFastForward(location, beforeHead, targetHead, project, options = {}) {
270
+ const result = (options.executeGit || executeGit)(location, ["merge-base", "--is-ancestor", beforeHead, targetHead], options);
271
+ if (result.status === 0) return;
272
+ if (result.status === 1) {
273
+ throw projectUpdateError(
274
+ "PROJECT_BRANCH_NOT_FAST_FORWARD",
275
+ `Project ${project.name} cannot fast-forward from ${beforeHead.slice(0, 12)} to ${targetHead.slice(0, 12)}.`,
276
+ project,
277
+ {
278
+ actualBranch: project.branch,
279
+ beforeHead,
280
+ targetHead,
281
+ remediation: "Resolve the local/remote divergence manually, then retry.",
282
+ }
283
+ );
284
+ }
285
+ const detail = String(result.stderr || result.stdout || "").trim();
286
+ throw new WorkspaceError("GIT_COMMAND_FAILED", detail || "git merge-base failed", {
287
+ location,
288
+ args: ["merge-base", "--is-ancestor", beforeHead, targetHead],
289
+ exitCode: result.status,
290
+ });
291
+ }
292
+
293
+ function applyLatestBranchUpdate(root, plan, options = {}) {
294
+ const project = {
295
+ name: plan.project.name,
296
+ location: plan.project.location,
297
+ branch: plan.registeredBranch,
298
+ updateLatest: plan.updateLatest,
299
+ };
300
+ if (plan.action === "skip" && plan.reason === "disabled") return plan;
301
+
302
+ const load = options.loadConfigProjection || loadConfigProjection;
303
+ const currentConfig = load(root, ["projects"]);
304
+ let currentProject;
305
+ try {
306
+ currentProject = (options.findRegisteredProject || findRegisteredProject)(currentConfig, project.name);
307
+ } catch (error) {
308
+ if (error.code === "PROJECT_NOT_FOUND") throw staleUpdatePlanError(project, plan, { configChanged: true, cause: error.code });
309
+ throw error;
310
+ }
311
+ if (
312
+ updateProjectPolicy(currentProject) !== true
313
+ || currentProject.branch !== plan.registeredBranch
314
+ || typeof currentProject.location !== "string"
315
+ || canonicalLocation(currentProject.location) !== canonicalLocation(plan.project.location)
316
+ ) {
317
+ throw staleUpdatePlanError(project, plan, { configChanged: true });
318
+ }
319
+
320
+ const observe = () => (options.inspectLatestBranchState || inspectLatestBranchState)(project, options);
321
+ const before = observe();
322
+ assertUpdatePreconditions(project, before);
323
+ if (before.head !== plan.beforeHead || before.upstream?.name !== plan.upstream) {
324
+ throw staleUpdatePlanError(project, plan, before);
325
+ }
326
+ fetchUpstream(project.location, before.upstream, options);
327
+ const afterFetch = observe();
328
+ const targetHead = (options.gitHead || gitHead)(project.location, before.upstream.name, options);
329
+ if (!sameUpdatePlan(before, afterFetch)) throw staleUpdatePlanError(project, before, afterFetch);
330
+ assertFastForward(project.location, before.head, targetHead, project, options);
331
+ if (targetHead === before.head) {
332
+ return {
333
+ ...plan,
334
+ action: "skip",
335
+ reason: "already-latest",
336
+ targetHead,
337
+ afterHead: before.head,
338
+ fetched: true,
339
+ fastForwarded: false,
340
+ };
341
+ }
342
+
343
+ let mergeAttempted = false;
344
+ try {
345
+ options.injectFailure?.("before-merge", { before, targetHead });
346
+ mergeAttempted = true;
347
+ (options.runGit || runGit)(project.location, ["merge", "--ff-only", targetHead], options);
348
+ options.injectFailure?.("after-merge", { before, targetHead });
349
+ const after = observe();
350
+ if (after.actualBranch !== project.branch || !after.worktreeClean || after.head !== targetHead) {
351
+ throw projectUpdateError(
352
+ "PROJECT_BRANCH_UPDATE_VERIFY_FAILED",
353
+ `Project ${project.name} did not reach the fetched upstream HEAD in a clean state.`,
354
+ project,
355
+ {
356
+ actualBranch: after.actualBranch,
357
+ beforeHead: before.head,
358
+ targetHead,
359
+ afterHead: after.head,
360
+ observedState: after,
361
+ remediation: "Inspect the current project HEAD and restore the intended state manually before retrying.",
362
+ }
363
+ );
364
+ }
365
+ options.injectFailure?.("after-verify", after);
366
+ return {
367
+ ...plan,
368
+ action: "update",
369
+ targetHead,
370
+ afterHead: after.head,
371
+ fetched: true,
372
+ fastForwarded: true,
373
+ worktreeClean: after.worktreeClean,
374
+ };
375
+ } catch (error) {
376
+ const after = (() => {
377
+ try { return observe(); } catch { return null; }
378
+ })();
379
+ const changed = mergeAttempted && after && after.head !== before.head;
380
+ const failure = error.code === "PROJECT_BRANCH_UPDATE_VERIFY_FAILED"
381
+ ? error
382
+ : projectUpdateError(
383
+ "PROJECT_BRANCH_UPDATE_FAILED",
384
+ `Could not fast-forward project ${project.name}: ${error.message}`,
385
+ project,
386
+ {
387
+ beforeHead: before.head,
388
+ targetHead,
389
+ afterHead: after?.head || null,
390
+ actualBranch: after?.actualBranch || before.actualBranch,
391
+ cause: error.code || error.name,
392
+ remediation: "Inspect the selected worktree and resolve the Git error manually before retrying.",
393
+ }
394
+ );
395
+ if (changed) {
396
+ throw attachRetainedEffects(failure, [{
397
+ kind: "git-fast-forward",
398
+ status: "possibly-applied",
399
+ retained: true,
400
+ project: project.name,
401
+ location: project.location,
402
+ beforeHead: before.head,
403
+ targetHead,
404
+ observedState: after,
405
+ remediation: failure.details.remediation,
406
+ }]);
407
+ }
408
+ throw failure;
409
+ }
410
+ }
411
+
412
+ module.exports = {
413
+ DEFAULT_GIT_TIMEOUT,
414
+ applyLatestBranchUpdate,
415
+ assertFastForward,
416
+ assertUpdatePreconditions,
417
+ fetchUpstream,
418
+ gitHead,
419
+ gitUpstream,
420
+ inspectLatestBranchState,
421
+ planLatestBranchUpdate,
422
+ runGit,
423
+ updateProjectPolicy,
424
+ };
@@ -0,0 +1,55 @@
1
+ const { configPath, loadConfigProjection, loadState, saveConfig } = require("./config");
2
+ const { WorkspaceError } = require("./errors");
3
+ const { loadInitManifest } = require("./init");
4
+ const { applyPermissionPlan, planPermissionChanges } = require("./permissions");
5
+ const { resolveWorkspaceTools } = require("./tools");
6
+ const { createFileTransaction } = require("./transaction");
7
+
8
+ function projectPermissionTools(root, options = {}) {
9
+ return resolveWorkspaceTools({
10
+ explicit: options.tools,
11
+ state: loadState(root),
12
+ manifestTools: loadInitManifest().tools,
13
+ });
14
+ }
15
+
16
+ function fallbackPermissionPlan(root, nextConfig, options = {}) {
17
+ const previous = (options.loadConfigProjection || loadConfigProjection)(root, ["projects"]);
18
+ const previousLocations = new Set(previous.projects.map((project) => project.location));
19
+ const nextLocations = new Set(nextConfig.projects.map((project) => project.location));
20
+ return planPermissionChanges({
21
+ root,
22
+ tools: projectPermissionTools(root, options).tools,
23
+ grants: [...nextLocations].filter((location) => !previousLocations.has(location)),
24
+ revokes: [...previousLocations].filter((location) => !nextLocations.has(location)),
25
+ }, options);
26
+ }
27
+
28
+ function applyProjectConfiguration(root, nextConfig, permissionPlan, options = {}) {
29
+ if (!permissionPlan?.plans) {
30
+ options = permissionPlan || options;
31
+ permissionPlan = fallbackPermissionPlan(root, nextConfig, options);
32
+ }
33
+ const transaction = createFileTransaction([configPath(root), ...permissionPlan.targets]);
34
+ try {
35
+ (options.saveConfig || saveConfig)(root, nextConfig);
36
+ options.injectFailure?.("after-config-save");
37
+ const persisted = (options.loadConfigProjection || loadConfigProjection)(root, ["projects"]);
38
+ if (JSON.stringify(persisted.projects) !== JSON.stringify(nextConfig.projects)) {
39
+ throw new WorkspaceError("PROJECT_CONFIGURATION_VERIFY_FAILED", "Persisted project configuration does not match the confirmed plan.");
40
+ }
41
+ const permissions = (options.applyPermissionPlan || applyPermissionPlan)(permissionPlan, { ...options, transaction });
42
+ options.injectFailure?.("after-permissions-sync");
43
+ transaction.commit();
44
+ return permissions;
45
+ } catch (error) {
46
+ transaction.rollback(error);
47
+ throw new WorkspaceError(
48
+ "PROJECT_CONFIGURATION_UPDATE_FAILED",
49
+ `Project configuration update rolled back: ${error.message}`,
50
+ { ...(error.details || {}), cause: error.code || error.name }
51
+ );
52
+ }
53
+ }
54
+
55
+ module.exports = { applyProjectConfiguration, projectPermissionTools };