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,102 @@
1
+ const fs = require("node:fs");
2
+ const path = require("node:path");
3
+
4
+ const { configPath, loadConfigProjection, loadState, statePath } = require("./config");
5
+ const { WorkspaceError } = require("./errors");
6
+ const { DEFAULT_LANGUAGE_CODE, LANGUAGE_OPTIONS, getLocale } = require("../i18n");
7
+
8
+ const DEFAULT_WORKSPACE_LANGUAGE = DEFAULT_LANGUAGE_CODE;
9
+ const PACKAGE_ROOT = path.resolve(__dirname, "..", "..");
10
+ const SUPPORTED_LANGUAGES = LANGUAGE_OPTIONS;
11
+ const SUPPORTED_LANGUAGE_VALUES = new Set(SUPPORTED_LANGUAGES.map((entry) => entry.value));
12
+
13
+ function normalizeLanguage(value) {
14
+ const language = String(value || "").trim();
15
+ if (!SUPPORTED_LANGUAGE_VALUES.has(language)) {
16
+ throw new WorkspaceError("WORKSPACE_LANGUAGE_INVALID", `Unsupported workspace language: ${language || "<missing>"}; choose ${SUPPORTED_LANGUAGES.map((entry) => entry.value).join(", ")}`, {
17
+ actual: language || null,
18
+ supported: SUPPORTED_LANGUAGES.map((entry) => entry.value),
19
+ });
20
+ }
21
+ return language;
22
+ }
23
+
24
+ function stateLanguage(root) {
25
+ const value = loadState(root)?.workspaceLanguage;
26
+ if (!value) return null;
27
+ return normalizeLanguage(value);
28
+ }
29
+
30
+ function legacyWorkspaceLanguageSelection(root) {
31
+ const previous = stateLanguage(root);
32
+ if (!previous) return null;
33
+ return {
34
+ language: previous,
35
+ source: "legacy-state",
36
+ legacy: true,
37
+ evidence: { state: { file: statePath(root), language: previous } },
38
+ };
39
+ }
40
+
41
+ function legacyWorkspaceLanguage(root) {
42
+ return legacyWorkspaceLanguageSelection(root)?.language || null;
43
+ }
44
+
45
+ function resolveWorkspaceLanguageSelection(root, options = {}) {
46
+ if (options.language !== undefined) {
47
+ return { language: normalizeLanguage(options.language), source: "cli", legacy: false };
48
+ }
49
+ const localConfig = configPath(root);
50
+ if (fs.existsSync(localConfig)) {
51
+ try {
52
+ const configured = loadConfigProjection(root, ["language"]);
53
+ if (configured.workspace?.language) {
54
+ return {
55
+ language: normalizeLanguage(configured.workspace.language),
56
+ source: "workspace-config",
57
+ legacy: false,
58
+ evidence: { file: localConfig },
59
+ };
60
+ }
61
+ } catch (error) {
62
+ if (!options.allowLegacy || error.code !== "WORKSPACE_LANGUAGE_MISSING") throw error;
63
+ }
64
+ }
65
+ if (options.allowLegacy) {
66
+ const previous = legacyWorkspaceLanguageSelection(root);
67
+ if (previous) return previous;
68
+ }
69
+ if (options.defaultLanguage === false) {
70
+ throw new WorkspaceError("WORKSPACE_LANGUAGE_MISSING", "Workspace language is not configured.", {
71
+ remediation: `Re-run code-w init . --language <${SUPPORTED_LANGUAGES.map((entry) => entry.value).join("|")}>.`,
72
+ });
73
+ }
74
+ return {
75
+ language: normalizeLanguage(options.defaultLanguage || DEFAULT_WORKSPACE_LANGUAGE),
76
+ source: "default",
77
+ legacy: false,
78
+ };
79
+ }
80
+
81
+ function resolveWorkspaceLanguage(root, options = {}) {
82
+ return resolveWorkspaceLanguageSelection(root, options).language;
83
+ }
84
+
85
+ function workspaceGuide(language) {
86
+ const locale = getLocale(normalizeLanguage(language));
87
+ const file = path.join(PACKAGE_ROOT, "artifacts", "templates", "user-guide", `${locale.code}.md`);
88
+ if (!fs.existsSync(file)) throw new WorkspaceError("WORKSPACE_GUIDE_MISSING", `Missing workspace guide template for locale ${locale.code}: ${file}`, { file, language: locale.code });
89
+ return fs.readFileSync(file, "utf8").trimEnd();
90
+ }
91
+
92
+ module.exports = {
93
+ DEFAULT_WORKSPACE_LANGUAGE,
94
+ SUPPORTED_LANGUAGES,
95
+ normalizeLanguage,
96
+ legacyWorkspaceLanguage,
97
+ legacyWorkspaceLanguageSelection,
98
+ resolveWorkspaceLanguage,
99
+ resolveWorkspaceLanguageSelection,
100
+ stateLanguage,
101
+ workspaceGuide,
102
+ };
@@ -0,0 +1,334 @@
1
+ const fs = require("node:fs");
2
+ const path = require("node:path");
3
+
4
+ const packageJson = require("../../package.json");
5
+ const { loadState, saveState } = require("./config");
6
+ const { WorkspaceError } = require("./errors");
7
+ const { atomicWrite, sha256 } = require("./fs");
8
+ const { DEFAULT_WORKSPACE_LANGUAGE, workspaceGuide } = require("./language");
9
+ const { CODEX_HOOKS_TARGET, composeHookContent } = require("./extension-artifacts");
10
+
11
+ const PACKAGE_ROOT = path.resolve(__dirname, "..", "..");
12
+ const ARTIFACTS_ROOT = path.join(PACKAGE_ROOT, "artifacts");
13
+ const MANIFEST_FILE = path.join(ARTIFACTS_ROOT, "manifest.json");
14
+
15
+ function canonicalize(value) {
16
+ if (Array.isArray(value)) return value.map(canonicalize);
17
+ if (!value || typeof value !== "object") return value;
18
+ return Object.fromEntries(
19
+ Object.keys(value).sort().map((key) => [key, canonicalize(value[key])])
20
+ );
21
+ }
22
+
23
+ function definitionSha256(entry) {
24
+ return sha256(JSON.stringify(canonicalize(entry)));
25
+ }
26
+
27
+ function renderManagedContent(entry, variables = {}) {
28
+ let content = fs.readFileSync(entry.sourceFile, "utf8");
29
+ const values = {
30
+ WORKSPACE_LANGUAGE: DEFAULT_WORKSPACE_LANGUAGE,
31
+ WORKSPACE_USER_GUIDE: workspaceGuide(DEFAULT_WORKSPACE_LANGUAGE),
32
+ ...variables,
33
+ ...(entry.render?.values || {}),
34
+ };
35
+ const required = entry.render?.variables || [];
36
+ for (const name of required) {
37
+ if (!/^[A-Z][A-Z0-9_]*$/.test(name)) throw new Error(`Invalid managed template variable: ${name}`);
38
+ if (values[name] === undefined) throw new Error(`Missing managed template variable ${name} for ${entry.target}`);
39
+ const token = `{{${name}}}`;
40
+ if (!content.includes(token)) throw new Error(`Managed template ${entry.target} does not contain ${token}`);
41
+ content = content.split(token).join(String(values[name]));
42
+ }
43
+ const unresolved = content.match(/{{[A-Z][A-Z0-9_]*}}/);
44
+ if (unresolved) throw new Error(`Unresolved managed template variable ${unresolved[0]} in ${entry.target}`);
45
+ return Buffer.from(content);
46
+ }
47
+
48
+ function extensionHookState(root, provided) {
49
+ if (provided !== undefined) return provided;
50
+ const file = path.join(root, ".code-workspace", "ext-manifest.json");
51
+ if (!fs.existsSync(file)) return { schemaVersion: 1, experimental: true, extensions: {} };
52
+ try {
53
+ const state = JSON.parse(fs.readFileSync(file, "utf8"));
54
+ return state?.schemaVersion === 1 && state.experimental === true && state.extensions && typeof state.extensions === "object" ? state : null;
55
+ } catch {
56
+ return null;
57
+ }
58
+ }
59
+
60
+ function hasExtensionHooks(state) {
61
+ if (!state) return false;
62
+ return Object.values(state.extensions || {}).some((value) => (value.installed?.artifacts || []).some((artifact) => artifact.kind === "codex-hooks"));
63
+ }
64
+
65
+ function desiredManagedContent(root, entry, variables, extensionState, includeCore = true) {
66
+ if (entry.target !== CODEX_HOOKS_TARGET) return renderManagedContent(entry, variables);
67
+ const target = path.join(root, entry.target);
68
+ if (extensionState === null && fs.existsSync(target)) return fs.readFileSync(target);
69
+ const base = includeCore ? renderManagedContent(entry, variables) : Buffer.from('{\n "hooks": {}\n}\n');
70
+ return composeHookContent(base, extensionState || { extensions: {} });
71
+ }
72
+
73
+ function resolveArtifact(relativePath) {
74
+ const resolved = path.resolve(ARTIFACTS_ROOT, relativePath);
75
+ if (resolved !== ARTIFACTS_ROOT && !resolved.startsWith(`${ARTIFACTS_ROOT}${path.sep}`)) {
76
+ throw new Error(`Artifact escapes artifacts directory: ${relativePath}`);
77
+ }
78
+ return resolved;
79
+ }
80
+
81
+ function verifySource(reference, label) {
82
+ if (!reference?.source || !/^[a-f0-9]{64}$/.test(reference.sha256 || "")) {
83
+ throw new Error(`Invalid ${label} source definition`);
84
+ }
85
+ const file = resolveArtifact(reference.source);
86
+ if (!fs.existsSync(file)) throw new Error(`Missing ${label} source: ${reference.source}`);
87
+ const actual = sha256(fs.readFileSync(file));
88
+ if (actual !== reference.sha256) throw new Error(`${label} source checksum mismatch: ${reference.source}`);
89
+ return file;
90
+ }
91
+
92
+ function loadManagedManifest(file = MANIFEST_FILE) {
93
+ if (!fs.existsSync(file)) throw new Error(`Missing managed files manifest: ${file}`);
94
+ const manifest = JSON.parse(fs.readFileSync(file, "utf8"));
95
+ if (manifest.schemaVersion !== 2) throw new Error(`Unsupported managed files manifest schema: ${manifest.schemaVersion}`);
96
+ if (manifest.releaseVersion !== packageJson.version) {
97
+ throw new Error(`Manifest release ${manifest.releaseVersion} does not match package ${packageJson.version}`);
98
+ }
99
+ if (!Array.isArray(manifest.tools) || manifest.tools.length === 0 || manifest.tools.some((tool) => !["claude", "codex"].includes(tool))) {
100
+ throw new Error("Managed files manifest contains invalid default tools");
101
+ }
102
+ if (!Array.isArray(manifest.managedFiles) || manifest.managedFiles.length === 0) {
103
+ throw new Error("Managed files manifest contains no managed files");
104
+ }
105
+
106
+ const sourceIds = new Set();
107
+ for (const source of manifest.sources || []) {
108
+ if (!source.id || sourceIds.has(source.id) || source.kind !== "asset" || !source.path || !/^[a-f0-9]{64}$/.test(source.sha256 || "")) {
109
+ throw new Error(`Invalid or duplicate artifact source: ${source.id || "<missing>"}`);
110
+ }
111
+ sourceIds.add(source.id);
112
+ const sourceFile = resolveArtifact(source.path);
113
+ if (!fs.existsSync(sourceFile)) throw new Error(`Missing artifact source: ${source.path}`);
114
+ if (sha256(fs.readFileSync(sourceFile)) !== source.sha256) {
115
+ throw new Error(`Artifact source checksum mismatch: ${source.path}`);
116
+ }
117
+ }
118
+
119
+ const ids = new Set();
120
+ const targets = new Set();
121
+ for (const entry of manifest.managedFiles || []) {
122
+ if (!entry.id || ids.has(entry.id)) throw new Error(`Duplicate or missing managed file id: ${entry.id || "<missing>"}`);
123
+ const normalizedTarget = entry.target && path.posix.normalize(entry.target);
124
+ if (
125
+ !entry.target ||
126
+ normalizedTarget !== entry.target ||
127
+ normalizedTarget === ".." ||
128
+ normalizedTarget.startsWith("../") ||
129
+ normalizedTarget.startsWith("/") ||
130
+ normalizedTarget.includes("\\") ||
131
+ targets.has(normalizedTarget)
132
+ ) {
133
+ throw new Error(`Duplicate or invalid managed target: ${entry.target || "<missing>"}`);
134
+ }
135
+ ids.add(entry.id);
136
+ targets.add(normalizedTarget);
137
+ Object.defineProperty(entry, "sourceFile", {
138
+ value: verifySource(entry.desired, `managed file ${entry.id}`),
139
+ enumerable: false,
140
+ });
141
+ for (const accepted of entry.replaceable || []) {
142
+ if (!/^[a-f0-9]{64}$/.test(accepted.sha256 || "")) {
143
+ throw new Error(`Invalid replaceable fingerprint for managed file ${entry.id}`);
144
+ }
145
+ }
146
+ if (entry.provenance?.sourceId && !sourceIds.has(entry.provenance.sourceId)) {
147
+ throw new Error(`Unknown provenance source ${entry.provenance.sourceId} for managed file ${entry.id}`);
148
+ }
149
+ for (const tool of entry.tools || []) {
150
+ if (!["claude", "codex"].includes(tool)) throw new Error(`Unsupported managed file tool: ${tool}`);
151
+ }
152
+ for (const capability of entry.capabilities || []) {
153
+ if (capability !== "monitor") throw new Error(`Unsupported managed file capability: ${capability}`);
154
+ }
155
+ if (entry.render) {
156
+ if (!Array.isArray(entry.render.variables) || entry.render.variables.length === 0) {
157
+ throw new Error(`Invalid managed file render definition: ${entry.id}`);
158
+ }
159
+ const renderValues = entry.render.values || {};
160
+ if (!renderValues || typeof renderValues !== "object" || Array.isArray(renderValues)) {
161
+ throw new Error(`Invalid managed file render values: ${entry.id}`);
162
+ }
163
+ const declared = new Set();
164
+ for (const variable of entry.render.variables) {
165
+ if (!/^[A-Z][A-Z0-9_]*$/.test(variable)) throw new Error(`Invalid managed template variable: ${variable}`);
166
+ if (declared.has(variable)) throw new Error(`Duplicate managed template variable: ${variable}`);
167
+ declared.add(variable);
168
+ }
169
+ for (const [name, value] of Object.entries(renderValues)) {
170
+ if (!/^[A-Z][A-Z0-9_]*$/.test(name) || !declared.has(name) || typeof value !== "string") {
171
+ throw new Error(`Invalid managed file render value ${name}: ${entry.id}`);
172
+ }
173
+ }
174
+ }
175
+ }
176
+ return manifest;
177
+ }
178
+
179
+ function selectedManagedFiles(manifest, tools, capabilities = []) {
180
+ const selected = new Set(tools || []);
181
+ const enabledCapabilities = new Set(capabilities || []);
182
+ return manifest.managedFiles.filter((entry) =>
183
+ (!entry.tools?.length || entry.tools.some((tool) => selected.has(tool))) &&
184
+ (!entry.capabilities?.length || entry.capabilities.every((capability) => enabledCapabilities.has(capability)))
185
+ );
186
+ }
187
+
188
+ function previousInstalledSha(state, entry) {
189
+ const previous = state?.managedFiles?.[entry.target];
190
+ return previous?.installedSha256 || previous?.sha256 || null;
191
+ }
192
+
193
+ function classifyManagedFile(root, entry, state, variables = {}, extensionState, includeCore = true) {
194
+ const target = path.join(root, entry.target);
195
+ const desiredSha256 = sha256(desiredManagedContent(root, entry, variables, extensionState, includeCore));
196
+ if (!fs.existsSync(target)) return { state: "missing", target, sha256: null, desiredSha256 };
197
+ const actualSha256 = sha256(fs.readFileSync(target));
198
+ if (actualSha256 === desiredSha256) return { state: "current", target, sha256: actualSha256, desiredSha256 };
199
+ if (actualSha256 === previousInstalledSha(state, entry)) return { state: "managed-old", target, sha256: actualSha256, desiredSha256 };
200
+ if ((entry.replaceable || []).some((accepted) => accepted.sha256 === actualSha256)) {
201
+ return { state: "replaceable", target, sha256: actualSha256, desiredSha256 };
202
+ }
203
+ return { state: "unknown", target, sha256: actualSha256, desiredSha256 };
204
+ }
205
+
206
+ function inspectManagedFiles(root, manifest, tools, capabilities = [], variables = {}, options = {}) {
207
+ const state = loadState(root);
208
+ const hooksState = extensionHookState(root, options.extensionState);
209
+ const coreSelected = new Set(selectedManagedFiles(manifest, tools, capabilities).map((entry) => entry.id));
210
+ const selected = new Set(coreSelected);
211
+ const hookEntry = manifest.managedFiles.find((entry) => entry.target === CODEX_HOOKS_TARGET);
212
+ if (hookEntry && (hasExtensionHooks(hooksState) || (hooksState === null && fs.existsSync(path.join(root, CODEX_HOOKS_TARGET))))) selected.add(hookEntry.id);
213
+ const output = { current: [], managedOld: [], replaceable: [], missing: [], unknown: [], files: [] };
214
+ for (const entry of manifest.managedFiles.filter((entry) => selected.has(entry.id))) {
215
+ const classified = classifyManagedFile(root, entry, state, variables, hooksState, coreSelected.has(entry.id));
216
+ const key = classified.state === "managed-old" ? "managedOld" : classified.state;
217
+ output[key].push(entry.target);
218
+ output.files.push({
219
+ id: entry.id,
220
+ target: entry.target,
221
+ state: classified.state,
222
+ sha256: classified.sha256,
223
+ desiredSha256: classified.desiredSha256,
224
+ provenance: entry.provenance,
225
+ });
226
+ }
227
+ return output;
228
+ }
229
+
230
+ function planManagedFiles(root, manifest, tools, options = {}) {
231
+ const state = loadState(root) || { schemaVersion: 2, managedFiles: {} };
232
+ const plans = [];
233
+ const hooksState = extensionHookState(root, options.extensionState);
234
+ const coreSelected = new Set(selectedManagedFiles(manifest, tools, options.capabilities).map((entry) => entry.id));
235
+ const selected = new Set(coreSelected);
236
+ const hookEntry = manifest.managedFiles.find((entry) => entry.target === CODEX_HOOKS_TARGET);
237
+ if (hookEntry && (hasExtensionHooks(hooksState) || (hooksState === null && fs.existsSync(path.join(root, CODEX_HOOKS_TARGET))))) selected.add(hookEntry.id);
238
+ for (const entry of manifest.managedFiles) {
239
+ if (!selected.has(entry.id)) {
240
+ const previousSha = previousInstalledSha(state, entry);
241
+ if (!previousSha) continue;
242
+ const target = path.join(root, entry.target);
243
+ const exists = fs.existsSync(target);
244
+ const actualSha = exists ? sha256(fs.readFileSync(target)) : null;
245
+ if (exists && actualSha !== previousSha && !options.force) {
246
+ throw new WorkspaceError("MANAGED_FILE_UNKNOWN", `Managed file contains unknown changes: ${entry.target}`, { target: entry.target });
247
+ }
248
+ plans.push({
249
+ entry,
250
+ target,
251
+ action: exists ? "remove" : "forget",
252
+ previous: exists ? fs.readFileSync(target) : null,
253
+ reason: "not-selected",
254
+ desiredSha256: null,
255
+ });
256
+ continue;
257
+ }
258
+ const classified = classifyManagedFile(root, entry, state, options.variables, hooksState, coreSelected.has(entry.id));
259
+ if (classified.state === "current") {
260
+ plans.push({ entry, target: classified.target, action: "skip", previous: null, reason: "current", desiredSha256: classified.desiredSha256 });
261
+ continue;
262
+ }
263
+ if (classified.state === "missing" && !entry.allowMissing) {
264
+ throw new WorkspaceError("MANAGED_FILE_MISSING", `Managed file target is missing: ${entry.target}`, { target: entry.target });
265
+ }
266
+ if (classified.state === "unknown" && !options.force) {
267
+ throw new WorkspaceError("MANAGED_FILE_UNKNOWN", `Managed file contains unknown changes: ${entry.target}`, { target: entry.target });
268
+ }
269
+ plans.push({
270
+ entry,
271
+ target: classified.target,
272
+ action: "write",
273
+ reason: classified.state,
274
+ previous: fs.existsSync(classified.target) ? fs.readFileSync(classified.target) : null,
275
+ content: desiredManagedContent(root, entry, options.variables, hooksState, coreSelected.has(entry.id)),
276
+ desiredSha256: classified.desiredSha256,
277
+ });
278
+ }
279
+ return { plans, state };
280
+ }
281
+
282
+ function installManagedFiles(root, manifest, tools, options = {}) {
283
+ const { plans, state } = planManagedFiles(root, manifest, tools, options);
284
+ const written = [];
285
+ try {
286
+ for (const plan of plans) {
287
+ if (plan.action === "write") atomicWrite(plan.target, plan.content);
288
+ else if (plan.action === "remove" && fs.existsSync(plan.target)) fs.unlinkSync(plan.target);
289
+ else continue;
290
+ written.push(plan);
291
+ }
292
+ const nextManaged = { ...(state.managedFiles || {}) };
293
+ for (const plan of plans) {
294
+ if (["remove", "forget"].includes(plan.action)) delete nextManaged[plan.entry.target];
295
+ else {
296
+ nextManaged[plan.entry.target] = {
297
+ artifactId: plan.entry.id,
298
+ definitionSha256: definitionSha256(plan.entry),
299
+ installedSha256: plan.desiredSha256,
300
+ };
301
+ }
302
+ }
303
+ saveState(root, { ...state, schemaVersion: 2, managedFiles: nextManaged });
304
+ } catch (error) {
305
+ for (const plan of written.reverse()) {
306
+ if (plan.action === "remove") atomicWrite(plan.target, plan.previous);
307
+ else if (plan.previous === null) {
308
+ if (fs.existsSync(plan.target)) fs.unlinkSync(plan.target);
309
+ } else atomicWrite(plan.target, plan.previous);
310
+ }
311
+ throw error;
312
+ }
313
+ return plans.map((plan) => ({
314
+ id: plan.entry.id,
315
+ target: plan.entry.target,
316
+ action: plan.action,
317
+ reason: plan.reason,
318
+ }));
319
+ }
320
+
321
+ module.exports = {
322
+ ARTIFACTS_ROOT,
323
+ MANIFEST_FILE,
324
+ canonicalize,
325
+ classifyManagedFile,
326
+ definitionSha256,
327
+ inspectManagedFiles,
328
+ installManagedFiles,
329
+ loadManagedManifest,
330
+ planManagedFiles,
331
+ renderManagedContent,
332
+ resolveArtifact,
333
+ selectedManagedFiles,
334
+ };
@@ -0,0 +1,68 @@
1
+ const fs = require("node:fs");
2
+
3
+ const {
4
+ CURRENT_CONFIG_VERSION,
5
+ configPath,
6
+ loadState,
7
+ planConfigMigration,
8
+ readConfigDocument,
9
+ statePath,
10
+ } = require("./config");
11
+ const { resolveWorkspaceLanguageSelection } = require("./language");
12
+
13
+ function planWorkspaceMaintenance(root, options = {}) {
14
+ const localConfig = configPath(root);
15
+ const hasConfig = fs.existsSync(localConfig);
16
+ const document = hasConfig ? readConfigDocument(root) : null;
17
+ const schema = document ? planConfigMigration(document.value) : {
18
+ fromVersion: null,
19
+ toVersion: CURRENT_CONFIG_VERSION,
20
+ steps: [],
21
+ changed: false,
22
+ value: null,
23
+ };
24
+ const language = resolveWorkspaceLanguageSelection(root, options);
25
+ const configuredLanguage = document?.value?.workspace?.language || null;
26
+ const languageChanged = configuredLanguage !== language.language;
27
+ const state = loadState(root);
28
+ const removeLegacyStateLanguage = Boolean(state && Object.prototype.hasOwnProperty.call(state, "workspaceLanguage"));
29
+ const steps = [
30
+ ...schema.steps.map((step) => ({ id: `config-schema-${step.fromVersion}-to-${step.toVersion}`, kind: "config-schema", ...step })),
31
+ ...(languageChanged ? [{
32
+ id: configuredLanguage ? "change-workspace-language" : "set-workspace-language",
33
+ kind: "workspace-language",
34
+ from: configuredLanguage,
35
+ to: language.language,
36
+ source: language.source,
37
+ }] : []),
38
+ ...(removeLegacyStateLanguage ? [{
39
+ id: "remove-legacy-state-language",
40
+ kind: "state-cleanup",
41
+ target: "workspaceLanguage",
42
+ }] : []),
43
+ ];
44
+ const writeTargets = [
45
+ ...(schema.changed || languageChanged ? [localConfig] : []),
46
+ ...(removeLegacyStateLanguage ? [statePath(root)] : []),
47
+ ];
48
+ return {
49
+ schema: {
50
+ fromVersion: schema.fromVersion,
51
+ toVersion: schema.toVersion,
52
+ changed: schema.changed,
53
+ steps: schema.steps,
54
+ },
55
+ language: {
56
+ value: language.language,
57
+ source: language.source,
58
+ legacy: language.legacy === true,
59
+ changed: languageChanged,
60
+ ...(language.evidence ? { evidence: language.evidence } : {}),
61
+ },
62
+ steps,
63
+ writeTargets: [...new Set(writeTargets)],
64
+ changed: steps.length > 0,
65
+ };
66
+ }
67
+
68
+ module.exports = { planWorkspaceMaintenance };
@@ -0,0 +1,92 @@
1
+ const fs = require("node:fs");
2
+ const path = require("node:path");
3
+
4
+ const { WorkspaceError } = require("../errors");
5
+ const { atomicWrite } = require("../fs");
6
+ const { assertFingerprint, planDirectoryMutation, readTarget } = require("./common");
7
+
8
+ function parseSettings(target) {
9
+ if (!target.exists || !target.content.trim()) return {};
10
+ let value;
11
+ try {
12
+ value = JSON.parse(target.content);
13
+ } catch (error) {
14
+ throw new WorkspaceError("CLAUDE_PERMISSION_CONFIG_PARSE_FAILED", `Cannot parse Claude settings ${target.file}: ${error.message}`, { file: target.file });
15
+ }
16
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
17
+ throw new WorkspaceError("CLAUDE_PERMISSION_CONFIG_INVALID", `Claude settings must be a JSON object: ${target.file}`, { file: target.file });
18
+ }
19
+ if (value.permissions != null && (!value.permissions || typeof value.permissions !== "object" || Array.isArray(value.permissions))) {
20
+ throw new WorkspaceError("CLAUDE_PERMISSION_CONFIG_INVALID", `Claude permissions must be a JSON object: ${target.file}`, { file: target.file, field: "permissions" });
21
+ }
22
+ const directories = value.permissions?.additionalDirectories;
23
+ if (directories != null && (!Array.isArray(directories) || directories.some((entry) => typeof entry !== "string"))) {
24
+ throw new WorkspaceError("CLAUDE_PERMISSION_CONFIG_INVALID", `Claude permissions.additionalDirectories must be an array of strings: ${target.file}`, {
25
+ file: target.file,
26
+ field: "permissions.additionalDirectories",
27
+ });
28
+ }
29
+ return value;
30
+ }
31
+
32
+ function renderSettings(settings) {
33
+ return `${JSON.stringify(settings, null, 2)}\n`;
34
+ }
35
+
36
+ const claudePermissionAdapter = {
37
+ id: "claude",
38
+ targets(root) {
39
+ return [path.join(root, ".claude", "settings.local.json")];
40
+ },
41
+ inspect(root) {
42
+ const target = readTarget(this.targets(root)[0]);
43
+ const settings = parseSettings(target);
44
+ return {
45
+ ...target,
46
+ settings,
47
+ directories: settings.permissions?.additionalDirectories || [],
48
+ };
49
+ },
50
+ plan({ current, grants, revokes }) {
51
+ const mutation = planDirectoryMutation(current.directories, grants, revokes);
52
+ const settings = structuredClone(current.settings);
53
+ if (mutation.action === "write") {
54
+ settings.permissions = { ...(settings.permissions || {}), additionalDirectories: mutation.desired };
55
+ }
56
+ return {
57
+ tool: this.id,
58
+ target: current.file,
59
+ targets: [current.file],
60
+ fingerprint: current.fingerprint,
61
+ ...mutation,
62
+ desiredContent: mutation.action === "write" ? renderSettings(settings) : current.content,
63
+ };
64
+ },
65
+ assertCurrent(plan) {
66
+ assertFingerprint(plan);
67
+ },
68
+ apply(plan, options = {}) {
69
+ (options.atomicWrite || atomicWrite)(plan.target, plan.desiredContent);
70
+ },
71
+ verify(plan) {
72
+ let actual;
73
+ try {
74
+ actual = fs.readFileSync(plan.target, "utf8");
75
+ } catch (error) {
76
+ throw new WorkspaceError("WORKSPACE_PERMISSION_VERIFY_FAILED", `Cannot verify Claude directory authorization: ${error.message}`, {
77
+ tool: this.id,
78
+ file: plan.target,
79
+ remediation: "Inspect .claude/settings.local.json and re-run code-w permissions apply.",
80
+ });
81
+ }
82
+ if (actual !== plan.desiredContent) {
83
+ throw new WorkspaceError("WORKSPACE_PERMISSION_VERIFY_FAILED", "Claude directory authorization does not match the confirmed plan.", {
84
+ tool: this.id,
85
+ file: plan.target,
86
+ remediation: "Inspect .claude/settings.local.json and re-run code-w permissions apply.",
87
+ });
88
+ }
89
+ },
90
+ };
91
+
92
+ module.exports = { claudePermissionAdapter, parseSettings, renderSettings };