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.
- package/.env.example +3 -0
- package/LICENSE +21 -0
- package/README.md +168 -0
- package/README.zh-CN.md +168 -0
- package/artifacts/manifest.json +183 -0
- package/artifacts/templates/USER_GUIDE.template.md +1 -0
- package/artifacts/templates/agents/WORKSPACE_GUARD.md.template +73 -0
- package/artifacts/templates/agents/skills/code-workspace-resolve-branch/SKILL.md +88 -0
- package/artifacts/templates/agents/skills/code-workspace-resolve-branch/evals/evals.json +45 -0
- package/artifacts/templates/claude/commands/code-workspace/add-projects.md +23 -0
- package/artifacts/templates/codex/hooks.json +86 -0
- package/artifacts/templates/codex/skills/code-workspace-add-projects/SKILL.md +28 -0
- package/artifacts/templates/user-guide/en-US.md +97 -0
- package/artifacts/templates/user-guide/zh-CN.md +97 -0
- package/assets/i18n-icon.svg +1 -0
- package/assets/logo-vector.svg +30 -0
- package/assets/request_tip.mp3 +0 -0
- package/assets/session_finish.mp3 +0 -0
- package/bin/code-workspace.js +11 -0
- package/docs/extension-architecture.zh-CN.md +429 -0
- package/docs/extensions.md +64 -0
- package/docs/extensions.zh-CN.md +64 -0
- package/extensions/openspec-workspace/1.0.0/artifacts/claude/SKILL.md +16 -0
- package/extensions/openspec-workspace/1.0.0/artifacts/codex/SKILL.md +16 -0
- package/extensions/openspec-workspace/1.0.0/init.js +54 -0
- package/extensions/openspec-workspace/1.0.0/manifest.json +27 -0
- package/extensions/zhuiyi-jira-mcp/0.1.0/artifacts/claude/server.json +14 -0
- package/extensions/zhuiyi-jira-mcp/0.1.0/artifacts/codex/config.toml +11 -0
- package/extensions/zhuiyi-jira-mcp/0.1.0/init.js +53 -0
- package/extensions/zhuiyi-jira-mcp/0.1.0/lib/archive.js +196 -0
- package/extensions/zhuiyi-jira-mcp/0.1.0/manifest.json +38 -0
- package/extensions/zhuiyi-jira-mcp/0.1.0/release.json +15 -0
- package/package.json +58 -0
- package/schemas/extension-init-context-v1.json +39 -0
- package/schemas/extension-init-result-v1.json +42 -0
- package/schemas/extension-manifest-v1.json +55 -0
- package/schemas/extension-manifest-v2.json +84 -0
- package/schemas/extension-manifest-v3.json +84 -0
- package/spec/extension/v1/specification.en-US.md +171 -0
- package/spec/extension/v1/specification.zh-CN.md +170 -0
- package/src/cli/commands/completion.js +203 -0
- package/src/cli/commands/extension.js +193 -0
- package/src/cli/commands/help.js +36 -0
- package/src/cli/commands/init.js +200 -0
- package/src/cli/commands/monitor.js +62 -0
- package/src/cli/commands/permissions.js +33 -0
- package/src/cli/commands/project-branch-update.js +107 -0
- package/src/cli/commands/project-branch.js +433 -0
- package/src/cli/commands/project.js +259 -0
- package/src/cli/commands/update.js +146 -0
- package/src/cli/commands/workspace.js +24 -0
- package/src/cli/confirmation.js +22 -0
- package/src/cli/parser.js +108 -0
- package/src/cli/registry.js +108 -0
- package/src/cli/renderer.js +20 -0
- package/src/cli/result.js +118 -0
- package/src/cli.js +76 -0
- package/src/core/assets.js +83 -0
- package/src/core/config.js +378 -0
- package/src/core/diagnostics.js +20 -0
- package/src/core/directory-digest.js +34 -0
- package/src/core/doctor.js +115 -0
- package/src/core/errors.js +10 -0
- package/src/core/extension-artifacts-legacy.js +180 -0
- package/src/core/extension-artifacts.js +310 -0
- package/src/core/extensions.js +1216 -0
- package/src/core/fs.js +32 -0
- package/src/core/init-lock-holder.js +30 -0
- package/src/core/init-lock.js +159 -0
- package/src/core/init.js +160 -0
- package/src/core/initializer.js +233 -0
- package/src/core/language.js +102 -0
- package/src/core/managed-files.js +334 -0
- package/src/core/migration.js +68 -0
- package/src/core/permissions/claude.js +92 -0
- package/src/core/permissions/codex.js +126 -0
- package/src/core/permissions/common.js +44 -0
- package/src/core/permissions/index.js +163 -0
- package/src/core/project-branch-update.js +424 -0
- package/src/core/project-configuration.js +55 -0
- package/src/core/project.js +674 -0
- package/src/core/tools.js +34 -0
- package/src/core/transaction.js +132 -0
- package/src/core/validation.js +186 -0
- package/src/i18n/index.js +11 -0
- package/src/i18n/interpolate.js +7 -0
- package/src/i18n/locales/en-US.js +10 -0
- package/src/i18n/locales/zh-CN.js +11 -0
- package/src/i18n/registry.js +42 -0
- package/src/index.js +25 -0
- package/src/init/plan.js +12 -0
- package/src/init/ui.js +61 -0
- package/src/init/wizard.js +97 -0
- package/src/monitor/i18n/index.js +33 -0
- package/src/monitor/i18n/locales/en-US.js +85 -0
- package/src/monitor/i18n/locales/zh-CN.js +79 -0
- package/src/monitor/index.js +344 -0
- package/src/monitor/page.js +118 -0
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
const fs = require("node:fs");
|
|
2
|
+
const path = require("node:path");
|
|
3
|
+
const { loadState, saveState } = require("./config");
|
|
4
|
+
const { WorkspaceError } = require("./errors");
|
|
5
|
+
const { sha256 } = require("./fs");
|
|
6
|
+
|
|
7
|
+
const OBSOLETE_ASSETS = [
|
|
8
|
+
"AGENT.md",
|
|
9
|
+
".claude/commands/opsxw/add-projects.md",
|
|
10
|
+
".claude/commands/opsxw/explore.md",
|
|
11
|
+
".claude/commands/opsxw/propose.md",
|
|
12
|
+
".claude/commands/opsxw/apply.md",
|
|
13
|
+
".claude/commands/opsxw/sync-specs.md",
|
|
14
|
+
".claude/commands/opsxw/archive.md",
|
|
15
|
+
"USER_GUIDE.zh-CN.md",
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
const FORGOTTEN_ASSETS = [];
|
|
19
|
+
|
|
20
|
+
function selectedObsoleteAssets(tools) {
|
|
21
|
+
void tools;
|
|
22
|
+
return OBSOLETE_ASSETS.slice();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function removeEmptyParents(root, file) {
|
|
26
|
+
const stop = path.resolve(root);
|
|
27
|
+
let directory = path.dirname(file);
|
|
28
|
+
while (directory !== stop && directory.startsWith(`${stop}${path.sep}`)) {
|
|
29
|
+
if (fs.readdirSync(directory).length > 0) break;
|
|
30
|
+
fs.rmdirSync(directory);
|
|
31
|
+
directory = path.dirname(directory);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function cleanupObsoleteAssets(root, tools, options = {}) {
|
|
36
|
+
const state = loadState(root) || { schemaVersion: 2, managedFiles: {} };
|
|
37
|
+
const managedFiles = { ...(state.managedFiles || {}) };
|
|
38
|
+
const results = [];
|
|
39
|
+
for (const item of planObsoleteAssets(root, tools, options)) {
|
|
40
|
+
const targetRelative = item.target;
|
|
41
|
+
const target = path.join(root, targetRelative);
|
|
42
|
+
if (fs.existsSync(target)) {
|
|
43
|
+
fs.unlinkSync(target);
|
|
44
|
+
removeEmptyParents(root, target);
|
|
45
|
+
results.push({ target: targetRelative, action: "remove" });
|
|
46
|
+
}
|
|
47
|
+
delete managedFiles[targetRelative];
|
|
48
|
+
}
|
|
49
|
+
for (const targetRelative of FORGOTTEN_ASSETS) {
|
|
50
|
+
if (!managedFiles[targetRelative]) continue;
|
|
51
|
+
delete managedFiles[targetRelative];
|
|
52
|
+
results.push({ target: targetRelative, action: "forget" });
|
|
53
|
+
}
|
|
54
|
+
saveState(root, { ...state, schemaVersion: 2, managedFiles });
|
|
55
|
+
return results;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function planObsoleteAssets(root, tools, options = {}) {
|
|
59
|
+
const state = loadState(root) || { schemaVersion: 2, managedFiles: {} };
|
|
60
|
+
const results = [];
|
|
61
|
+
for (const targetRelative of selectedObsoleteAssets(tools)) {
|
|
62
|
+
const previous = state.managedFiles?.[targetRelative];
|
|
63
|
+
if (!previous) continue;
|
|
64
|
+
const target = path.join(root, targetRelative);
|
|
65
|
+
if (fs.existsSync(target)) {
|
|
66
|
+
const expected = previous.installedSha256 || previous.sha256;
|
|
67
|
+
const actual = sha256(fs.readFileSync(target));
|
|
68
|
+
if (!options.force && expected && expected !== actual) {
|
|
69
|
+
throw new WorkspaceError("OBSOLETE_MANAGED_FILE_UNKNOWN", `Obsolete managed file contains unknown changes: ${targetRelative}`, { target: targetRelative });
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
results.push({ target: targetRelative, action: fs.existsSync(target) ? "remove" : "forget" });
|
|
73
|
+
}
|
|
74
|
+
return results;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
module.exports = {
|
|
78
|
+
FORGOTTEN_ASSETS,
|
|
79
|
+
OBSOLETE_ASSETS,
|
|
80
|
+
cleanupObsoleteAssets,
|
|
81
|
+
planObsoleteAssets,
|
|
82
|
+
selectedObsoleteAssets,
|
|
83
|
+
};
|
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
const fs = require("node:fs");
|
|
2
|
+
const path = require("node:path");
|
|
3
|
+
const yaml = require("js-yaml");
|
|
4
|
+
|
|
5
|
+
const { atomicWrite } = require("./fs");
|
|
6
|
+
const { WorkspaceError } = require("./errors");
|
|
7
|
+
const { LANGUAGE_CODES } = require("../i18n");
|
|
8
|
+
|
|
9
|
+
const LOCAL_DIRECTORY = ".code-workspace";
|
|
10
|
+
const CONFIG_FILE = "config.yaml";
|
|
11
|
+
const STATE_FILE = "state.json";
|
|
12
|
+
const DEFAULT_WORKSPACE_NAME = "code-workspace";
|
|
13
|
+
const DEFAULT_MONITOR_URL = "http://127.0.0.1:3211";
|
|
14
|
+
const CURRENT_CONFIG_VERSION = 2;
|
|
15
|
+
const MINIMUM_READABLE_CONFIG_VERSION = 0;
|
|
16
|
+
const CONFIG_RENDER_OPTIONS = Object.freeze({
|
|
17
|
+
lineWidth: -1,
|
|
18
|
+
noRefs: true,
|
|
19
|
+
sortKeys: false,
|
|
20
|
+
styles: { "!!str": "literal" },
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
const CONFIG_MIGRATIONS = new Map([
|
|
24
|
+
[0, (value) => ({ ...value, schemaVersion: 1 })],
|
|
25
|
+
[1, (value) => ({ ...value, schemaVersion: 2 })],
|
|
26
|
+
]);
|
|
27
|
+
|
|
28
|
+
function configPath(root) {
|
|
29
|
+
return path.join(root, LOCAL_DIRECTORY, CONFIG_FILE);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function statePath(root) {
|
|
33
|
+
return path.join(root, LOCAL_DIRECTORY, STATE_FILE);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function findWorkspaceRoot(start = process.cwd()) {
|
|
37
|
+
let current = path.resolve(start);
|
|
38
|
+
while (true) {
|
|
39
|
+
if (fs.existsSync(configPath(current))) return current;
|
|
40
|
+
const parent = path.dirname(current);
|
|
41
|
+
if (parent === current) return null;
|
|
42
|
+
current = parent;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function requireWorkspaceRoot(start = process.cwd()) {
|
|
47
|
+
const root = findWorkspaceRoot(start);
|
|
48
|
+
if (!root) {
|
|
49
|
+
throw new WorkspaceError(
|
|
50
|
+
"WORKSPACE_NOT_FOUND",
|
|
51
|
+
"No local Code Workspace found. Run `code-workspace init` first.",
|
|
52
|
+
{ start: path.resolve(start) }
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
return root;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function normalizeWorkspaceIdentity(value) {
|
|
59
|
+
if (value == null) return null;
|
|
60
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new WorkspaceError("WORKSPACE_IDENTITY_INVALID", "workspace must be an object");
|
|
61
|
+
const name = String(value.name || "").trim();
|
|
62
|
+
const uuid = String(value.uuid || "").trim();
|
|
63
|
+
if (!name || [...name].length > 64 || /[\u0000-\u001f\u007f]/.test(name)) {
|
|
64
|
+
throw new WorkspaceError("WORKSPACE_NAME_INVALID", "workspace.name must contain 1-64 characters without control characters");
|
|
65
|
+
}
|
|
66
|
+
if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(uuid)) {
|
|
67
|
+
throw new WorkspaceError("WORKSPACE_UUID_INVALID", "workspace.uuid must be a UUID");
|
|
68
|
+
}
|
|
69
|
+
return { ...value, name, uuid };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function normalizeWorkspaceLanguage(value, options = {}) {
|
|
73
|
+
const language = String(value || options.defaultLanguage || "").trim();
|
|
74
|
+
if (!LANGUAGE_CODES.includes(language)) {
|
|
75
|
+
const code = language ? "WORKSPACE_LANGUAGE_INVALID" : "WORKSPACE_LANGUAGE_MISSING";
|
|
76
|
+
throw new WorkspaceError(code, `workspace.language must be one of: ${LANGUAGE_CODES.join(", ")}`, {
|
|
77
|
+
actual: language || null,
|
|
78
|
+
supported: LANGUAGE_CODES,
|
|
79
|
+
remediation: `code-w update --language ${LANGUAGE_CODES[0]}`,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
return language;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function normalizeWorkspace(value, options = {}) {
|
|
86
|
+
const identity = normalizeWorkspaceIdentity(value);
|
|
87
|
+
if (!identity) return null;
|
|
88
|
+
return { ...identity, language: normalizeWorkspaceLanguage(value.language, options) };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function normalizeMonitor(value) {
|
|
92
|
+
const monitor = value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
93
|
+
const enable = monitor.enable === true;
|
|
94
|
+
const rawUrl = String(monitor.url || DEFAULT_MONITOR_URL);
|
|
95
|
+
let parsed;
|
|
96
|
+
try {
|
|
97
|
+
parsed = new URL(rawUrl);
|
|
98
|
+
} catch {
|
|
99
|
+
throw new WorkspaceError("MONITOR_CONFIG_INVALID", "monitor.url must be an absolute URL", { actual: rawUrl });
|
|
100
|
+
}
|
|
101
|
+
if (parsed.protocol !== "http:" || !["127.0.0.1", "localhost", "[::1]"].includes(parsed.hostname)) {
|
|
102
|
+
throw new WorkspaceError("MONITOR_CONFIG_INVALID", "monitor.url must use HTTP and a loopback host", { actual: rawUrl });
|
|
103
|
+
}
|
|
104
|
+
if (parsed.username || parsed.password || parsed.search || parsed.hash || !["", "/"].includes(parsed.pathname)) {
|
|
105
|
+
throw new WorkspaceError("MONITOR_CONFIG_INVALID", "monitor.url must not contain credentials, path, query, or fragment", { actual: rawUrl });
|
|
106
|
+
}
|
|
107
|
+
if (!parsed.port) parsed.port = "3211";
|
|
108
|
+
const port = Number(parsed.port);
|
|
109
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
110
|
+
throw new WorkspaceError("MONITOR_CONFIG_INVALID", "monitor.url port must be between 1 and 65535", { actual: rawUrl });
|
|
111
|
+
}
|
|
112
|
+
return { ...monitor, enable, url: parsed.toString().replace(/\/$/, "") };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function normalizeProjects(value) {
|
|
116
|
+
if (value == null) return [];
|
|
117
|
+
if (!Array.isArray(value)) throw new WorkspaceError("PROJECT_REGISTRY_INVALID", "projects must be an array");
|
|
118
|
+
return value.map((project) => {
|
|
119
|
+
if (!project || typeof project !== "object" || Array.isArray(project)) return project;
|
|
120
|
+
const { specPrefix: _legacySpecPrefix, ...normalized } = project;
|
|
121
|
+
return normalized;
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function configVersion(value) {
|
|
126
|
+
const version = value?.schemaVersion ?? 0;
|
|
127
|
+
if (!Number.isInteger(version) || version < MINIMUM_READABLE_CONFIG_VERSION) {
|
|
128
|
+
throw new WorkspaceError("CONFIG_SCHEMA_VERSION_INVALID", `Invalid workspace config schemaVersion: ${version}`, { version });
|
|
129
|
+
}
|
|
130
|
+
if (version > CURRENT_CONFIG_VERSION) {
|
|
131
|
+
throw new WorkspaceError("CONFIG_SCHEMA_VERSION_UNSUPPORTED", `Workspace config schemaVersion ${version} is newer than supported version ${CURRENT_CONFIG_VERSION}`, {
|
|
132
|
+
version,
|
|
133
|
+
supported: CURRENT_CONFIG_VERSION,
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
return version;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function planConfigMigration(value) {
|
|
140
|
+
const fromVersion = configVersion(value);
|
|
141
|
+
let version = fromVersion;
|
|
142
|
+
let migrated = { ...value };
|
|
143
|
+
const steps = [];
|
|
144
|
+
while (version < CURRENT_CONFIG_VERSION) {
|
|
145
|
+
const migrate = CONFIG_MIGRATIONS.get(version);
|
|
146
|
+
if (!migrate) {
|
|
147
|
+
throw new WorkspaceError("CONFIG_MIGRATION_UNAVAILABLE", `No workspace config migration is registered for schemaVersion ${version}`, {
|
|
148
|
+
version,
|
|
149
|
+
targetVersion: CURRENT_CONFIG_VERSION,
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
const next = version + 1;
|
|
153
|
+
migrated = migrate(migrated);
|
|
154
|
+
steps.push({ fromVersion: version, toVersion: next });
|
|
155
|
+
version = next;
|
|
156
|
+
}
|
|
157
|
+
return {
|
|
158
|
+
fromVersion,
|
|
159
|
+
toVersion: CURRENT_CONFIG_VERSION,
|
|
160
|
+
steps,
|
|
161
|
+
changed: steps.length > 0,
|
|
162
|
+
value: migrated,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function readConfigDocument(root) {
|
|
167
|
+
const file = configPath(root);
|
|
168
|
+
if (!fs.existsSync(file)) throw new WorkspaceError("LOCAL_CONFIG_MISSING", `Missing local workspace config: ${file}`, { file });
|
|
169
|
+
let value;
|
|
170
|
+
try {
|
|
171
|
+
value = yaml.load(fs.readFileSync(file, "utf8"));
|
|
172
|
+
} catch (error) {
|
|
173
|
+
throw new WorkspaceError("LOCAL_CONFIG_PARSE_FAILED", `Cannot parse local workspace config ${file}: ${error.message}`, { file });
|
|
174
|
+
}
|
|
175
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
176
|
+
throw new WorkspaceError("LOCAL_CONFIG_INVALID", `Local workspace config must be a YAML object: ${file}`, { file });
|
|
177
|
+
}
|
|
178
|
+
return { file, value, schemaVersion: configVersion(value) };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function normalizeConfig(value, options = {}) {
|
|
182
|
+
const source = value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
183
|
+
const migration = planConfigMigration(source);
|
|
184
|
+
const config = migration.value;
|
|
185
|
+
return {
|
|
186
|
+
...config,
|
|
187
|
+
schemaVersion: configVersion(config),
|
|
188
|
+
workspace: normalizeWorkspace(config.workspace, options),
|
|
189
|
+
monitor: normalizeMonitor(config.monitor),
|
|
190
|
+
projects: normalizeProjects(config.projects),
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function loadConfig(root, options = {}) {
|
|
195
|
+
return normalizeConfig(readConfigDocument(root).value, options);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function loadConfigProjection(root, domains, options = {}) {
|
|
199
|
+
const document = readConfigDocument(root);
|
|
200
|
+
const requested = new Set(domains || []);
|
|
201
|
+
if (requested.has("complete")) return normalizeConfig(document.value, options);
|
|
202
|
+
const projected = { schemaVersion: CURRENT_CONFIG_VERSION, sourceSchemaVersion: document.schemaVersion };
|
|
203
|
+
const attachFile = (error) => {
|
|
204
|
+
error.details = { ...(error.details || {}), file: document.file };
|
|
205
|
+
throw error;
|
|
206
|
+
};
|
|
207
|
+
if (requested.has("identity")) {
|
|
208
|
+
try {
|
|
209
|
+
projected.workspace = normalizeWorkspaceIdentity(document.value.workspace);
|
|
210
|
+
} catch (error) {
|
|
211
|
+
attachFile(error);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
if (requested.has("language")) {
|
|
215
|
+
try {
|
|
216
|
+
projected.workspace = {
|
|
217
|
+
...(projected.workspace || {}),
|
|
218
|
+
language: normalizeWorkspaceLanguage(document.value.workspace?.language, options),
|
|
219
|
+
};
|
|
220
|
+
} catch (error) {
|
|
221
|
+
attachFile(error);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
if (requested.has("monitor")) {
|
|
225
|
+
try {
|
|
226
|
+
projected.monitor = normalizeMonitor(document.value.monitor);
|
|
227
|
+
} catch (error) {
|
|
228
|
+
attachFile(error);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
if (requested.has("projects")) {
|
|
232
|
+
try {
|
|
233
|
+
projected.projects = normalizeProjects(document.value.projects);
|
|
234
|
+
} catch (error) {
|
|
235
|
+
attachFile(error);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
return projected;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function loadConfigMigrationPlan(root) {
|
|
242
|
+
const document = readConfigDocument(root);
|
|
243
|
+
return { ...planConfigMigration(document.value), file: document.file };
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function inspectConfigDomains(root, options = {}) {
|
|
247
|
+
let document;
|
|
248
|
+
try {
|
|
249
|
+
document = readConfigDocument(root);
|
|
250
|
+
} catch (error) {
|
|
251
|
+
return {
|
|
252
|
+
file: error.details?.file || configPath(root),
|
|
253
|
+
schemaVersion: null,
|
|
254
|
+
document: { valid: false, value: null, diagnostics: [error] },
|
|
255
|
+
identity: { valid: false, value: null, diagnostics: [] },
|
|
256
|
+
language: { valid: false, value: null, diagnostics: [] },
|
|
257
|
+
monitor: { valid: false, value: null, diagnostics: [] },
|
|
258
|
+
projects: { valid: false, value: null, diagnostics: [] },
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
const inspect = (project) => {
|
|
262
|
+
try {
|
|
263
|
+
return { valid: true, value: project(), diagnostics: [] };
|
|
264
|
+
} catch (error) {
|
|
265
|
+
error.details = { ...(error.details || {}), file: document.file };
|
|
266
|
+
return { valid: false, value: null, diagnostics: [error] };
|
|
267
|
+
}
|
|
268
|
+
};
|
|
269
|
+
return {
|
|
270
|
+
file: document.file,
|
|
271
|
+
schemaVersion: document.schemaVersion,
|
|
272
|
+
document: { valid: true, value: document.value, diagnostics: [] },
|
|
273
|
+
identity: inspect(() => normalizeWorkspaceIdentity(document.value.workspace)),
|
|
274
|
+
language: inspect(() => normalizeWorkspaceLanguage(document.value.workspace?.language, options)),
|
|
275
|
+
monitor: inspect(() => normalizeMonitor(document.value.monitor)),
|
|
276
|
+
projects: inspect(() => normalizeProjects(document.value.projects)),
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function renderConfigDocument(value) {
|
|
281
|
+
return yaml.dump(value, CONFIG_RENDER_OPTIONS);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function renderConfig(config) {
|
|
285
|
+
return renderConfigDocument(normalizeConfig(config));
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function saveConfig(root, config) {
|
|
289
|
+
atomicWrite(configPath(root), renderConfig(config));
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function updateProjectBranch(root, update, options = {}) {
|
|
293
|
+
const document = (options.readConfigDocument || readConfigDocument)(root);
|
|
294
|
+
const sourceProjects = document.value.projects;
|
|
295
|
+
if (!Array.isArray(sourceProjects)) {
|
|
296
|
+
throw new WorkspaceError("PROJECT_REGISTRY_INVALID", "projects must be an array", { file: document.file });
|
|
297
|
+
}
|
|
298
|
+
const sourceProject = sourceProjects.find((project) => project?.name === update.name);
|
|
299
|
+
if (!sourceProject || sourceProject.branch !== update.before.registeredBranch) {
|
|
300
|
+
throw new WorkspaceError(
|
|
301
|
+
"PROJECT_BRANCH_ACCEPT_CONFLICT",
|
|
302
|
+
`Registered branch changed while preparing to accept the actual branch for ${update.name}.`,
|
|
303
|
+
{
|
|
304
|
+
project: update.name,
|
|
305
|
+
expectedState: update.before,
|
|
306
|
+
observedState: {
|
|
307
|
+
registeredBranch: sourceProject?.branch || null,
|
|
308
|
+
actualBranch: update.before.actualBranch,
|
|
309
|
+
},
|
|
310
|
+
}
|
|
311
|
+
);
|
|
312
|
+
}
|
|
313
|
+
const project = { ...sourceProject, branch: update.after.registeredBranch };
|
|
314
|
+
const nextDocument = {
|
|
315
|
+
...document.value,
|
|
316
|
+
projects: sourceProjects.map((entry) => entry?.name === update.name ? project : entry),
|
|
317
|
+
};
|
|
318
|
+
(options.atomicWrite || atomicWrite)(configPath(root), renderConfigDocument(nextDocument));
|
|
319
|
+
return project;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function loadState(root) {
|
|
323
|
+
const file = statePath(root);
|
|
324
|
+
if (!fs.existsSync(file)) return null;
|
|
325
|
+
return JSON.parse(fs.readFileSync(file, "utf8"));
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function saveState(root, state) {
|
|
329
|
+
atomicWrite(statePath(root), `${JSON.stringify(state, null, 2)}\n`);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function ensureLocalIgnore(root) {
|
|
333
|
+
const file = path.join(root, ".gitignore");
|
|
334
|
+
const current = fs.existsSync(file) ? fs.readFileSync(file, "utf8") : "";
|
|
335
|
+
const lines = current.split(/\r?\n/);
|
|
336
|
+
const ignored = lines.some((line) => {
|
|
337
|
+
const value = line.trim();
|
|
338
|
+
return value === LOCAL_DIRECTORY || value === `${LOCAL_DIRECTORY}/` || value === `/${LOCAL_DIRECTORY}/`;
|
|
339
|
+
});
|
|
340
|
+
if (ignored) return "skip";
|
|
341
|
+
const prefix = current && !current.endsWith("\n") ? "\n" : "";
|
|
342
|
+
atomicWrite(file, `${current}${prefix}\n# Code Workspace local configuration\n/${LOCAL_DIRECTORY}/\n`);
|
|
343
|
+
return "write";
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
module.exports = {
|
|
347
|
+
CONFIG_FILE,
|
|
348
|
+
CURRENT_CONFIG_VERSION,
|
|
349
|
+
DEFAULT_MONITOR_URL,
|
|
350
|
+
DEFAULT_WORKSPACE_NAME,
|
|
351
|
+
LOCAL_DIRECTORY,
|
|
352
|
+
MINIMUM_READABLE_CONFIG_VERSION,
|
|
353
|
+
CONFIG_MIGRATIONS,
|
|
354
|
+
STATE_FILE,
|
|
355
|
+
configPath,
|
|
356
|
+
ensureLocalIgnore,
|
|
357
|
+
findWorkspaceRoot,
|
|
358
|
+
loadConfig,
|
|
359
|
+
inspectConfigDomains,
|
|
360
|
+
loadConfigMigrationPlan,
|
|
361
|
+
loadConfigProjection,
|
|
362
|
+
loadState,
|
|
363
|
+
normalizeConfig,
|
|
364
|
+
normalizeMonitor,
|
|
365
|
+
normalizeProjects,
|
|
366
|
+
normalizeWorkspace,
|
|
367
|
+
normalizeWorkspaceIdentity,
|
|
368
|
+
normalizeWorkspaceLanguage,
|
|
369
|
+
planConfigMigration,
|
|
370
|
+
readConfigDocument,
|
|
371
|
+
renderConfig,
|
|
372
|
+
renderConfigDocument,
|
|
373
|
+
requireWorkspaceRoot,
|
|
374
|
+
saveConfig,
|
|
375
|
+
saveState,
|
|
376
|
+
statePath,
|
|
377
|
+
updateProjectBranch,
|
|
378
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
function result() {
|
|
2
|
+
return { errors: [], warnings: [], diagnostics: [] };
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
function add(target, severity, code, message, details = {}) {
|
|
6
|
+
target.diagnostics.push({ code, severity, message, ...details });
|
|
7
|
+
if (severity === "error") target.errors.push(message);
|
|
8
|
+
if (severity === "warning") target.warnings.push(message);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function payload(target, extra = {}) {
|
|
12
|
+
return {
|
|
13
|
+
schemaVersion: 1,
|
|
14
|
+
ok: target.errors.length === 0,
|
|
15
|
+
diagnostics: target.diagnostics,
|
|
16
|
+
...extra,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
module.exports = { add, payload, result };
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
const crypto = require("node:crypto");
|
|
2
|
+
const fs = require("node:fs");
|
|
3
|
+
const path = require("node:path");
|
|
4
|
+
|
|
5
|
+
const { WorkspaceError } = require("./errors");
|
|
6
|
+
|
|
7
|
+
function directoryError(code, message, details = {}) {
|
|
8
|
+
return new WorkspaceError(code, message, details);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function directoryEntries(root, directory = root) {
|
|
12
|
+
const entries = [];
|
|
13
|
+
for (const entry of fs.readdirSync(directory, { withFileTypes: true }).sort((left, right) => left.name.localeCompare(right.name))) {
|
|
14
|
+
const file = path.join(directory, entry.name);
|
|
15
|
+
const relative = path.relative(root, file).split(path.sep).join("/");
|
|
16
|
+
const stat = fs.lstatSync(file);
|
|
17
|
+
if (stat.isSymbolicLink()) throw directoryError("EXTENSION_OUTPUT_SYMLINK", `Extension directory contains a symbolic link: ${relative}`, { path: relative });
|
|
18
|
+
if (stat.isDirectory()) entries.push(...directoryEntries(root, file));
|
|
19
|
+
else if (stat.isFile()) entries.push(`${relative}\0${stat.mode & 0o777}\0${crypto.createHash("sha256").update(fs.readFileSync(file)).digest("hex")}`);
|
|
20
|
+
else throw directoryError("EXTENSION_OUTPUT_INVALID", `Extension directory contains a special file: ${relative}`, { path: relative });
|
|
21
|
+
}
|
|
22
|
+
return entries;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function directoryDigest(root) {
|
|
26
|
+
if (!fs.existsSync(root)) throw directoryError("EXTENSION_ARTIFACT_MISSING", `Extension directory is missing: ${root}`, { path: root });
|
|
27
|
+
const stat = fs.lstatSync(root);
|
|
28
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()) {
|
|
29
|
+
throw directoryError("EXTENSION_ARTIFACT_TARGET_INVALID", `Extension directory target is not a regular directory: ${root}`, { path: root });
|
|
30
|
+
}
|
|
31
|
+
return crypto.createHash("sha256").update(directoryEntries(root).join("\n")).digest("hex");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
module.exports = { directoryDigest, directoryEntries };
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
const { CURRENT_CONFIG_VERSION, inspectConfigDomains, loadState } = require("./config");
|
|
2
|
+
const { add, result } = require("./diagnostics");
|
|
3
|
+
const { inspectManagedFiles } = require("./managed-files");
|
|
4
|
+
const { DEFAULT_WORKSPACE_LANGUAGE, workspaceGuide } = require("./language");
|
|
5
|
+
const { inspectProjectPermissions } = require("./permissions");
|
|
6
|
+
const { validateProjects } = require("./validation");
|
|
7
|
+
const { resolveWorkspaceTools } = require("./tools");
|
|
8
|
+
|
|
9
|
+
function merge(target, source) {
|
|
10
|
+
target.errors.push(...source.errors);
|
|
11
|
+
target.warnings.push(...source.warnings);
|
|
12
|
+
target.diagnostics.push(...source.diagnostics);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function doctorWorkspace(root, manifest, options = {}) {
|
|
16
|
+
const output = result();
|
|
17
|
+
const inspection = inspectConfigDomains(root);
|
|
18
|
+
const config = {
|
|
19
|
+
schemaVersion: CURRENT_CONFIG_VERSION,
|
|
20
|
+
workspace: inspection.identity.value
|
|
21
|
+
? { ...inspection.identity.value, ...(inspection.language.valid ? { language: inspection.language.value } : {}) }
|
|
22
|
+
: null,
|
|
23
|
+
monitor: inspection.monitor.value,
|
|
24
|
+
projects: inspection.projects.value || [],
|
|
25
|
+
};
|
|
26
|
+
const domainErrors = [inspection.document, inspection.identity, inspection.language, inspection.monitor, inspection.projects]
|
|
27
|
+
.flatMap((domain) => domain.diagnostics || []);
|
|
28
|
+
for (const error of domainErrors) {
|
|
29
|
+
add(output, "error", error.code || "LOCAL_CONFIG_INVALID", error.message, error.details || {});
|
|
30
|
+
}
|
|
31
|
+
if (inspection.projects.valid) {
|
|
32
|
+
merge(output, validateProjects(root, config));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (inspection.identity.valid && !config.workspace) {
|
|
36
|
+
add(output, "error", "WORKSPACE_IDENTITY_MISSING", "Local workspace name and UUID are missing. Re-run `code-w init .` to generate them.");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const toolSelection = options.toolSelection || resolveWorkspaceTools({
|
|
40
|
+
explicit: options.tools,
|
|
41
|
+
state: loadState(root),
|
|
42
|
+
manifestTools: manifest.tools,
|
|
43
|
+
});
|
|
44
|
+
const tools = toolSelection.tools;
|
|
45
|
+
const capabilities = options.capabilities || (inspection.monitor.valid && config.monitor?.enable ? ["monitor"] : []);
|
|
46
|
+
if (inspection.monitor.valid && config.monitor?.enable && !tools.includes("codex")) {
|
|
47
|
+
add(output, "error", "MONITOR_CODEX_REQUIRED", "Agent monitoring is enabled, but Codex is not one of the selected tools.");
|
|
48
|
+
}
|
|
49
|
+
const language = inspection.language.valid ? config.workspace?.language : null;
|
|
50
|
+
try {
|
|
51
|
+
if (!inspection.language.valid) {
|
|
52
|
+
add(output, "warning", "MANAGED_FILE_CHECK_SKIPPED", "Managed file content verification was skipped because workspace.language is invalid.", {
|
|
53
|
+
prerequisite: "workspace.language",
|
|
54
|
+
});
|
|
55
|
+
} else {
|
|
56
|
+
const managed = inspectManagedFiles(root, manifest, tools, capabilities, {
|
|
57
|
+
WORKSPACE_LANGUAGE: language || DEFAULT_WORKSPACE_LANGUAGE,
|
|
58
|
+
WORKSPACE_USER_GUIDE: workspaceGuide(language || DEFAULT_WORKSPACE_LANGUAGE),
|
|
59
|
+
});
|
|
60
|
+
for (const file of [...managed.managedOld, ...managed.replaceable]) {
|
|
61
|
+
add(output, "error", "MANAGED_FILE_OUTDATED", `Managed file is not at the desired version: ${file}`);
|
|
62
|
+
}
|
|
63
|
+
for (const file of managed.missing) {
|
|
64
|
+
add(output, "error", "MANAGED_FILE_MISSING", `Managed file target is missing: ${file}`);
|
|
65
|
+
}
|
|
66
|
+
for (const file of managed.unknown) {
|
|
67
|
+
add(output, "error", "MANAGED_FILE_UNKNOWN", `Managed file contains unknown changes: ${file}`);
|
|
68
|
+
}
|
|
69
|
+
output.managedFileInspection = managed;
|
|
70
|
+
}
|
|
71
|
+
} catch (error) {
|
|
72
|
+
add(output, "error", "MANAGED_FILE_MANIFEST_INVALID", error.message);
|
|
73
|
+
}
|
|
74
|
+
if (inspection.projects.valid && config.projects.length > 0) {
|
|
75
|
+
try {
|
|
76
|
+
output.permissionInspection = inspectProjectPermissions({ root, tools, projects: config.projects });
|
|
77
|
+
for (const tool of output.permissionInspection) {
|
|
78
|
+
for (const directory of tool.missing) {
|
|
79
|
+
add(output, "error", "WORKSPACE_PERMISSION_MISSING", `Registered project directory is not authorized for ${tool.tool}: ${directory}`, {
|
|
80
|
+
tool: tool.tool,
|
|
81
|
+
directory,
|
|
82
|
+
file: tool.target,
|
|
83
|
+
remediation: "Review and run code-w permissions apply --yes.",
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
} catch (error) {
|
|
88
|
+
add(output, "error", error.code || "WORKSPACE_PERMISSION_INSPECTION_FAILED", error.message, error.details || {});
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const state = loadState(root);
|
|
93
|
+
if (!options.allowIncompleteState) {
|
|
94
|
+
if (!state || state.status !== "healthy") add(output, "error", "INIT_STATE_UNHEALTHY", "Local initialization state is missing or unhealthy.");
|
|
95
|
+
else {
|
|
96
|
+
if (state.appliedReleaseVersion !== manifest.releaseVersion) {
|
|
97
|
+
add(output, "error", "INIT_RELEASE_OUTDATED", `Initialized release ${state.appliedReleaseVersion || "missing"} does not match package ${manifest.releaseVersion}.`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
...output,
|
|
104
|
+
config,
|
|
105
|
+
tools,
|
|
106
|
+
toolSelection,
|
|
107
|
+
capabilities,
|
|
108
|
+
language,
|
|
109
|
+
configInspection: inspection,
|
|
110
|
+
managedFiles: output.managedFileInspection?.files || [],
|
|
111
|
+
permissions: output.permissionInspection || [],
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
module.exports = { doctorWorkspace, merge };
|