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
package/src/core/fs.js ADDED
@@ -0,0 +1,32 @@
1
+ const crypto = require("node:crypto");
2
+ const fs = require("node:fs");
3
+ const path = require("node:path");
4
+
5
+ function atomicWrite(file, content) {
6
+ fs.mkdirSync(path.dirname(file), { recursive: true });
7
+ const temporary = path.join(
8
+ path.dirname(file),
9
+ `.${path.basename(file)}.${process.pid}.${Date.now()}.tmp`
10
+ );
11
+ fs.writeFileSync(temporary, content);
12
+ fs.renameSync(temporary, file);
13
+ }
14
+
15
+ function sha256(content) {
16
+ return crypto.createHash("sha256").update(content).digest("hex");
17
+ }
18
+
19
+ function readTextIfExists(file) {
20
+ return fs.existsSync(file) ? fs.readFileSync(file, "utf8") : null;
21
+ }
22
+
23
+ function removeFileIfExists(file) {
24
+ if (fs.existsSync(file)) fs.unlinkSync(file);
25
+ }
26
+
27
+ module.exports = {
28
+ atomicWrite,
29
+ readTextIfExists,
30
+ removeFileIfExists,
31
+ sha256,
32
+ };
@@ -0,0 +1,30 @@
1
+ const { parentPort, workerData } = require("node:worker_threads");
2
+
3
+ const properLockfile = require("proper-lockfile");
4
+
5
+ (async () => {
6
+ let release;
7
+ try {
8
+ release = await properLockfile.lock(workerData.target, {
9
+ realpath: false,
10
+ retries: false,
11
+ update: workerData.update,
12
+ stale: workerData.stale,
13
+ });
14
+ parentPort.postMessage({ type: "ready" });
15
+ parentPort.on("message", async (message) => {
16
+ if (message?.type !== "release") return;
17
+ try {
18
+ await release();
19
+ parentPort.postMessage({ type: "released" });
20
+ } catch (error) {
21
+ parentPort.postMessage({ type: "release-error", code: error.code, message: error.message });
22
+ } finally {
23
+ process.exit(0);
24
+ }
25
+ });
26
+ } catch (error) {
27
+ parentPort.postMessage({ type: "error", code: error.code, message: error.message });
28
+ process.exit(1);
29
+ }
30
+ })();
@@ -0,0 +1,159 @@
1
+ const crypto = require("node:crypto");
2
+ const fs = require("node:fs");
3
+ const os = require("node:os");
4
+ const path = require("node:path");
5
+ const { Worker } = require("node:worker_threads");
6
+
7
+ const { WorkspaceError } = require("./errors");
8
+
9
+ const PACKAGE_ROOT = path.resolve(__dirname, "..", "..");
10
+ const LOCK_DIRECTORY = path.join(os.tmpdir(), "code-workspace-init-locks");
11
+ const UPDATE_ENV = "CODE_WORKSPACE_INIT_LOCK_UPDATE_MS";
12
+ const STALE_ENV = "CODE_WORKSPACE_INIT_LOCK_STALE_MS";
13
+ const DEFAULT_UPDATE_MS = 5000;
14
+ const DEFAULT_STALE_MS = 30000;
15
+
16
+ let projectEnvLoaded = false;
17
+
18
+ function loadProjectEnv() {
19
+ if (projectEnvLoaded) return;
20
+ projectEnvLoaded = true;
21
+ if (typeof process.loadEnvFile !== "function") return;
22
+ const envFile = path.join(PACKAGE_ROOT, ".env");
23
+ try {
24
+ process.loadEnvFile(envFile);
25
+ } catch (error) {
26
+ if (error.code !== "ENOENT") {
27
+ throw new WorkspaceError("INIT_LOCK_CONFIG_INVALID", `Cannot load Code Workspace .env: ${error.message}`, { file: envFile });
28
+ }
29
+ }
30
+ }
31
+
32
+ function parseMilliseconds(value, fallback, name) {
33
+ if (value === undefined || value === "") return fallback;
34
+ const text = String(value).trim();
35
+ if (!/^\d+$/.test(text)) {
36
+ throw new WorkspaceError("INIT_LOCK_CONFIG_INVALID", `${name} must be a positive integer in milliseconds`, { variable: name, value });
37
+ }
38
+ const parsed = Number(text);
39
+ if (!Number.isSafeInteger(parsed) || parsed < 1) {
40
+ throw new WorkspaceError("INIT_LOCK_CONFIG_INVALID", `${name} must be a positive integer in milliseconds`, { variable: name, value });
41
+ }
42
+ return parsed;
43
+ }
44
+
45
+ function readInitLockConfig(options = {}) {
46
+ if (!options.env) loadProjectEnv();
47
+ const env = options.env || process.env;
48
+ const update = parseMilliseconds(env[UPDATE_ENV], DEFAULT_UPDATE_MS, UPDATE_ENV);
49
+ const stale = parseMilliseconds(env[STALE_ENV], DEFAULT_STALE_MS, STALE_ENV);
50
+ if (stale <= update * 2) {
51
+ throw new WorkspaceError("INIT_LOCK_CONFIG_INVALID", `${STALE_ENV} must be greater than twice ${UPDATE_ENV}`, {
52
+ update,
53
+ stale,
54
+ updateVariable: UPDATE_ENV,
55
+ staleVariable: STALE_ENV,
56
+ });
57
+ }
58
+ return Object.freeze({ update, stale });
59
+ }
60
+
61
+ function canonicalWorkspacePath(root) {
62
+ const resolved = path.resolve(root);
63
+ try {
64
+ return fs.realpathSync.native(resolved);
65
+ } catch {
66
+ return resolved;
67
+ }
68
+ }
69
+
70
+ function workspaceLockTarget(root) {
71
+ const key = crypto.createHash("sha256").update(canonicalWorkspacePath(root)).digest("hex");
72
+ fs.mkdirSync(LOCK_DIRECTORY, { recursive: true });
73
+ return path.join(LOCK_DIRECTORY, key);
74
+ }
75
+
76
+ async function acquireInitLock(root, options = {}) {
77
+ const config = options.config || readInitLockConfig(options);
78
+ const target = options.target || workspaceLockTarget(root);
79
+ const worker = new Worker(path.join(__dirname, "init-lock-holder.js"), {
80
+ workerData: { target, update: config.update, stale: config.stale },
81
+ });
82
+ return new Promise((resolve, reject) => {
83
+ let settled = false;
84
+ let releasing = false;
85
+ let releaseResolve;
86
+ let releaseReject;
87
+ const fail = (error) => {
88
+ if (settled) return;
89
+ settled = true;
90
+ reject(error);
91
+ };
92
+ worker.on("message", (message) => {
93
+ if (message.type === "ready") {
94
+ if (settled) return;
95
+ settled = true;
96
+ resolve(() => new Promise((releaseDone, releaseFailed) => {
97
+ if (releasing) {
98
+ releaseFailed(new WorkspaceError("INIT_LOCK_RELEASE_FAILED", "The init lock is already being released."));
99
+ return;
100
+ }
101
+ releasing = true;
102
+ releaseResolve = releaseDone;
103
+ releaseReject = releaseFailed;
104
+ worker.postMessage({ type: "release" });
105
+ }));
106
+ } else if (message.type === "released") {
107
+ releaseResolve?.();
108
+ releaseResolve = null;
109
+ releaseReject = null;
110
+ } else if (message.type === "release-error") {
111
+ releaseReject?.(new WorkspaceError("INIT_LOCK_RELEASE_FAILED", `Could not release the init lock: ${message.message}`, { cause: message.code }));
112
+ releaseResolve = null;
113
+ releaseReject = null;
114
+ } else if (message.type === "error") {
115
+ const error = message.code === "ELOCKED"
116
+ ? new WorkspaceError("INIT_ALREADY_RUNNING", "Another init operation is already running for this Workspace.", {
117
+ workspace: canonicalWorkspacePath(root),
118
+ remediation: "Wait for the existing init operation to finish, then retry.",
119
+ })
120
+ : new WorkspaceError("INIT_LOCK_ACQUIRE_FAILED", `Could not acquire the init lock: ${message.message}`, {
121
+ workspace: canonicalWorkspacePath(root),
122
+ cause: message.code,
123
+ });
124
+ fail(error);
125
+ }
126
+ });
127
+ worker.on("error", (error) => {
128
+ if (releasing) {
129
+ releaseReject?.(new WorkspaceError("INIT_LOCK_RELEASE_FAILED", `Could not release the init lock: ${error.message}`, { cause: error.code || error.name }));
130
+ releaseResolve = null;
131
+ releaseReject = null;
132
+ } else {
133
+ fail(new WorkspaceError("INIT_LOCK_ACQUIRE_FAILED", `Could not acquire the init lock: ${error.message}`, {
134
+ workspace: canonicalWorkspacePath(root),
135
+ cause: error.code || error.name,
136
+ }));
137
+ }
138
+ });
139
+ worker.on("exit", (code) => {
140
+ if (!releasing && !settled) {
141
+ fail(new WorkspaceError("INIT_LOCK_ACQUIRE_FAILED", `The init lock worker exited before acquiring the lock (status ${code}).`, {
142
+ workspace: canonicalWorkspacePath(root),
143
+ exitCode: code,
144
+ }));
145
+ }
146
+ });
147
+ });
148
+ }
149
+
150
+ module.exports = {
151
+ DEFAULT_STALE_MS,
152
+ DEFAULT_UPDATE_MS,
153
+ STALE_ENV,
154
+ UPDATE_ENV,
155
+ acquireInitLock,
156
+ canonicalWorkspacePath,
157
+ readInitLockConfig,
158
+ workspaceLockTarget,
159
+ };
@@ -0,0 +1,160 @@
1
+ const fs = require("node:fs");
2
+ const path = require("node:path");
3
+ const { spawnSync } = require("node:child_process");
4
+
5
+ const packageJson = require("../../package.json");
6
+ const { WorkspaceError } = require("./errors");
7
+ const { sha256 } = require("./fs");
8
+ const { loadState, saveState } = require("./config");
9
+ const { MANIFEST_FILE, loadManagedManifest } = require("./managed-files");
10
+ const { attachRetainedEffects } = require("./transaction");
11
+
12
+ const PACKAGE_ROOT = path.resolve(__dirname, "..", "..");
13
+
14
+ function parseVersion(value) {
15
+ const match = String(value || "").trim().match(/^v?(\d+)\.(\d+)\.(\d+)(?:[-+].*)?$/);
16
+ if (!match) throw new Error(`Invalid semantic version: ${value}`);
17
+ return match.slice(1).map(Number);
18
+ }
19
+
20
+ function compareVersions(left, right) {
21
+ const a = parseVersion(left);
22
+ const b = parseVersion(right);
23
+ for (let index = 0; index < 3; index += 1) {
24
+ if (a[index] < b[index]) return -1;
25
+ if (a[index] > b[index]) return 1;
26
+ }
27
+ return 0;
28
+ }
29
+
30
+ function minimumFromRange(range) {
31
+ const match = String(range || "").trim().match(/^>=\s*(v?\d+\.\d+\.\d+)$/);
32
+ if (!match) throw new Error(`Unsupported version range: ${range}`);
33
+ return match[1];
34
+ }
35
+
36
+ function runCommand(command, args, options = {}) {
37
+ const result = spawnSync(command, args, {
38
+ cwd: options.cwd,
39
+ env: options.env || process.env,
40
+ encoding: "utf8",
41
+ stdio: options.capture ? ["ignore", "pipe", "pipe"] : "inherit",
42
+ timeout: options.timeout,
43
+ });
44
+ if (result.error && !options.allowFailure) throw result.error;
45
+ if (result.status !== 0 && !options.allowFailure) {
46
+ const detail = String(result.stderr || result.stdout || "").trim().split(/\r?\n/)[0];
47
+ throw new Error(`Command failed (${result.status ?? "spawn"}): ${command} ${args.join(" ")}${detail ? ` — ${detail}` : ""}`);
48
+ }
49
+ return result;
50
+ }
51
+
52
+ function loadInitManifest(file = MANIFEST_FILE) {
53
+ const manifest = loadManagedManifest(file);
54
+ minimumFromRange(manifest.requirements?.node);
55
+ return manifest;
56
+ }
57
+
58
+ function installWorkspaceDependencies(root, options = {}) {
59
+ const packageFile = path.join(root, "package.json");
60
+ if (!fs.existsSync(packageFile)) return { action: "skip", reason: "target has no package.json" };
61
+ let targetPackage;
62
+ try {
63
+ targetPackage = JSON.parse(fs.readFileSync(packageFile, "utf8"));
64
+ } catch (error) {
65
+ throw new Error(`Failed to parse target package.json: ${error.message}`);
66
+ }
67
+ if (targetPackage.name !== packageJson.name) {
68
+ return { action: "skip", reason: `target package is ${targetPackage.name || "unnamed"}` };
69
+ }
70
+ const scope = ["node_modules", "package-lock.json"];
71
+ const before = Object.fromEntries(scope.map((target) => [target, fs.existsSync(path.join(root, target))]));
72
+ const retainedEffect = (missing = []) => {
73
+ const after = Object.fromEntries(scope.map((target) => [target, fs.existsSync(path.join(root, target))]));
74
+ const created = scope.filter((target) => !before[target] && after[target]);
75
+ return {
76
+ kind: "retained-local-effect",
77
+ status: created.length > 0 ? "applied" : "possibly-applied",
78
+ name: "workspace dependencies",
79
+ command: "npm install",
80
+ targets: scope,
81
+ scope: { enumeration: "dependency-roots-only", complete: false },
82
+ evidence: { before, after, ...(missing.length > 0 ? { missingPackages: missing } : {}) },
83
+ remediation: "Run npm install again and verify the declared dependencies before continuing.",
84
+ };
85
+ };
86
+ let installCompleted = false;
87
+ try {
88
+ (options.run || runCommand)("npm", ["install"], { cwd: root });
89
+ installCompleted = true;
90
+ } catch (cause) {
91
+ const error = cause.code ? cause : new WorkspaceError("WORKSPACE_DEPENDENCY_INSTALL_FAILED", cause.message, { cause: cause.name });
92
+ throw attachRetainedEffects(error, [retainedEffect()]);
93
+ }
94
+ const dependencies = {
95
+ ...(targetPackage.dependencies || {}),
96
+ ...(targetPackage.devDependencies || {}),
97
+ };
98
+ const missing = Object.keys(dependencies).filter((name) => !fs.existsSync(path.join(root, "node_modules", name, "package.json")));
99
+ if (missing.length > 0) {
100
+ const error = new WorkspaceError(
101
+ "WORKSPACE_DEPENDENCY_VERIFICATION_FAILED",
102
+ `Workspace dependency verification failed; missing installed packages: ${missing.join(", ")}`,
103
+ { missing }
104
+ );
105
+ throw attachRetainedEffects(error, [retainedEffect(missing)]);
106
+ }
107
+ return {
108
+ action: "install",
109
+ verified: installCompleted,
110
+ retainedPaths: ["node_modules", ...(fs.existsSync(path.join(root, "package-lock.json")) ? ["package-lock.json"] : [])],
111
+ };
112
+ }
113
+
114
+ function commitInitializationState(root, manifest, options = {}) {
115
+ const current = loadState(root) || {};
116
+ const state = {
117
+ ...current,
118
+ schemaVersion: 2,
119
+ status: "healthy",
120
+ appliedReleaseVersion: manifest.releaseVersion,
121
+ appliedManifestSha256: sha256(fs.readFileSync(options.manifestFile || MANIFEST_FILE)),
122
+ tools: options.tools || manifest.tools,
123
+ };
124
+ delete state.workspaceLanguage;
125
+ saveState(root, state);
126
+ return state;
127
+ }
128
+
129
+ function commitUpdateState(root, manifest, options = {}) {
130
+ const current = loadState(root);
131
+ if (!current) {
132
+ throw new WorkspaceError(
133
+ "UPDATE_STATE_MISSING",
134
+ "Local initialization state is missing; update can only commit an existing workspace state.",
135
+ { remediation: "Re-run code-w init . --yes before updating this workspace." }
136
+ );
137
+ }
138
+ const state = {
139
+ ...current,
140
+ appliedReleaseVersion: manifest.releaseVersion,
141
+ appliedManifestSha256: sha256(fs.readFileSync(options.manifestFile || MANIFEST_FILE)),
142
+ ...(options.tools !== undefined ? { tools: options.tools } : {}),
143
+ };
144
+ if (options.removeLegacyWorkspaceLanguage === true) delete state.workspaceLanguage;
145
+ saveState(root, state);
146
+ return state;
147
+ }
148
+
149
+ module.exports = {
150
+ MANIFEST_FILE,
151
+ PACKAGE_ROOT,
152
+ compareVersions,
153
+ commitInitializationState,
154
+ commitUpdateState,
155
+ installWorkspaceDependencies,
156
+ loadInitManifest,
157
+ minimumFromRange,
158
+ parseVersion,
159
+ runCommand,
160
+ };
@@ -0,0 +1,233 @@
1
+ const fs = require("node:fs");
2
+ const path = require("node:path");
3
+ const { randomUUID } = require("node:crypto");
4
+ const readline = require("node:readline/promises");
5
+
6
+ const { cleanupObsoleteAssets, OBSOLETE_ASSETS } = require("./assets");
7
+ const {
8
+ DEFAULT_MONITOR_URL,
9
+ DEFAULT_WORKSPACE_NAME,
10
+ configPath,
11
+ ensureLocalIgnore,
12
+ loadConfig,
13
+ normalizeConfig,
14
+ saveConfig,
15
+ statePath,
16
+ } = require("./config");
17
+ const { doctorWorkspace } = require("./doctor");
18
+ const {
19
+ MANIFEST_FILE,
20
+ commitInitializationState,
21
+ compareVersions,
22
+ installWorkspaceDependencies,
23
+ loadInitManifest,
24
+ minimumFromRange,
25
+ runCommand,
26
+ } = require("./init");
27
+ const { applyPermissionPlan, permissionTargets, planPermissionChanges } = require("./permissions");
28
+ const { installManagedFiles } = require("./managed-files");
29
+ const { DEFAULT_WORKSPACE_LANGUAGE, workspaceGuide } = require("./language");
30
+ const { planWorkspaceMaintenance } = require("./migration");
31
+ const { createFileTransaction } = require("./transaction");
32
+ const { validateProjects } = require("./validation");
33
+
34
+ const INITIALIZATION_STAGE_IDS = Object.freeze({
35
+ "Configure workspace identity": "configure-workspace",
36
+ "Validate manifest": "validate-manifest",
37
+ "Install workspace dependencies": "install-dependencies",
38
+ "Remove obsolete managed files": "cleanup-obsolete",
39
+ "Install managed files": "install-managed-files",
40
+ "Prepare local workspace configuration": "save-config",
41
+ "Apply Agent workspace permissions": "apply-permissions",
42
+ "Run strict workspace doctor": "strict-doctor",
43
+ "Commit local initialization state": "commit-state",
44
+ "Verify local initialization state": "final-verify",
45
+ });
46
+
47
+ async function collectWorkspaceSetup(root, options = {}) {
48
+ const language = options.language || DEFAULT_WORKSPACE_LANGUAGE;
49
+ const file = configPath(root);
50
+ if (fs.existsSync(file)) {
51
+ const current = loadConfig(root, { defaultLanguage: language });
52
+ return normalizeConfig({
53
+ ...current,
54
+ workspace: current.workspace || {
55
+ name: options.workspaceName || DEFAULT_WORKSPACE_NAME,
56
+ uuid: randomUUID(),
57
+ },
58
+ monitor: {
59
+ ...current.monitor,
60
+ ...(options.monitor !== undefined ? { enable: options.monitor === true } : {}),
61
+ ...(options.monitorUrl ? { url: options.monitorUrl } : {}),
62
+ },
63
+ });
64
+ }
65
+
66
+ const interactive = options.interactive ?? (process.stdin.isTTY && process.stdout.isTTY);
67
+ let name = options.workspaceName;
68
+ let monitorEnabled = options.monitor;
69
+ if (interactive && (!name || (monitorEnabled === undefined && options.tools.includes("codex")))) {
70
+ const prompt = readline.createInterface({ input: options.input || process.stdin, output: options.output || process.stdout });
71
+ try {
72
+ if (!name) name = (await prompt.question(`Workspace name [${DEFAULT_WORKSPACE_NAME}]: `)).trim() || DEFAULT_WORKSPACE_NAME;
73
+ if (monitorEnabled === undefined && options.tools.includes("codex")) {
74
+ monitorEnabled = /^y(?:es)?$/i.test((await prompt.question("Enable Codex Agent monitoring hooks? [y/N] ")).trim());
75
+ }
76
+ } finally {
77
+ prompt.close();
78
+ }
79
+ }
80
+ return normalizeConfig({
81
+ schemaVersion: 2,
82
+ workspace: { name: name || DEFAULT_WORKSPACE_NAME, uuid: options.workspaceUuid || randomUUID(), language },
83
+ monitor: {
84
+ enable: options.tools.includes("codex") && monitorEnabled !== false,
85
+ url: options.monitorUrl || DEFAULT_MONITOR_URL,
86
+ },
87
+ projects: [],
88
+ });
89
+ }
90
+
91
+ async function initializeWorkspaceStages(rootInput, options = {}) {
92
+ const root = path.resolve(rootInput || ".");
93
+ const migration = options.migration || planWorkspaceMaintenance(root, {
94
+ language: options.language,
95
+ defaultLanguage: DEFAULT_WORKSPACE_LANGUAGE,
96
+ allowLegacy: true,
97
+ });
98
+ const language = migration.language.value;
99
+ fs.mkdirSync(root, { recursive: true });
100
+ const run = options.run || runCommand;
101
+ options = { ...options, language };
102
+ const stages = [];
103
+ const stage = async (name, action) => {
104
+ options.onStage?.(name);
105
+ const value = await action();
106
+ options.injectFailure?.(INITIALIZATION_STAGE_IDS[name] || name, value);
107
+ stages.push({ name, result: value });
108
+ return value;
109
+ };
110
+
111
+ const workspaceConfig = await stage("Configure workspace identity", () => collectWorkspaceSetup(root, options));
112
+ const capabilities = workspaceConfig.monitor.enable ? ["monitor"] : [];
113
+
114
+ const manifest = await stage("Validate manifest", () => loadInitManifest(options.manifestFile || MANIFEST_FILE));
115
+ const minimumNode = minimumFromRange(manifest.requirements.node);
116
+ const nodeVersion = options.nodeVersion || process.versions.node;
117
+ if (compareVersions(nodeVersion, minimumNode) < 0) {
118
+ throw new Error(`Node ${minimumNode} or newer is required; found ${nodeVersion}`);
119
+ }
120
+
121
+ const dependencies = await stage("Install workspace dependencies", () =>
122
+ installWorkspaceDependencies(root, { run })
123
+ );
124
+ if (dependencies.action === "install") {
125
+ options.transaction?.recordExternalEffect({
126
+ kind: "retained-local-effect",
127
+ name: "workspace dependencies",
128
+ targets: dependencies.retainedPaths,
129
+ verified: dependencies.verified === true,
130
+ });
131
+ }
132
+
133
+ const obsoleteFiles = await stage("Remove obsolete managed files", () =>
134
+ cleanupObsoleteAssets(root, options.tools, { force: options.force === true })
135
+ );
136
+
137
+ const managedFiles = await stage("Install managed files", () =>
138
+ installManagedFiles(root, manifest, options.tools, {
139
+ force: options.force === true,
140
+ capabilities,
141
+ variables: { WORKSPACE_LANGUAGE: language, WORKSPACE_USER_GUIDE: workspaceGuide(language) },
142
+ })
143
+ );
144
+
145
+ const localConfig = await stage("Prepare local workspace configuration", () => {
146
+ const file = configPath(root);
147
+ const action = fs.existsSync(file) ? "skip" : "write";
148
+ saveConfig(root, workspaceConfig);
149
+ return { action, file: path.relative(root, file), gitignore: ensureLocalIgnore(root) };
150
+ });
151
+
152
+ const config = loadConfig(root);
153
+ const projects = validateProjects(root, config);
154
+ if (projects.errors.length > 0) {
155
+ throw new Error(`Local project verification failed: ${projects.errors.join("; ")}`);
156
+ }
157
+ const permissions = await stage("Apply Agent workspace permissions", () => {
158
+ const permissionPlan = planPermissionChanges({
159
+ root,
160
+ tools: options.tools,
161
+ grants: config.projects.map((project) => project.location),
162
+ });
163
+ return applyPermissionPlan(permissionPlan, { transaction: options.transaction });
164
+ });
165
+
166
+ await stage("Run strict workspace doctor", () => {
167
+ const result = doctorWorkspace(root, manifest, {
168
+ allowIncompleteState: true,
169
+ run,
170
+ tools: options.tools,
171
+ capabilities,
172
+ });
173
+ if (result.errors.length > 0) throw new Error(`Workspace doctor failed: ${result.errors.join("; ")}`);
174
+ return { warnings: result.warnings, projects: config.projects.length };
175
+ });
176
+
177
+ const state = await stage("Commit local initialization state", () =>
178
+ commitInitializationState(root, manifest, {
179
+ manifestFile: options.manifestFile || MANIFEST_FILE,
180
+ tools: options.tools,
181
+ language,
182
+ })
183
+ );
184
+
185
+ const verification = await stage("Verify local initialization state", () => {
186
+ const result = doctorWorkspace(root, manifest, { run, tools: options.tools, capabilities });
187
+ if (result.errors.length > 0) throw new Error(`Final workspace verification failed: ${result.errors.join("; ")}`);
188
+ return { warnings: result.warnings, projects: config.projects.length };
189
+ });
190
+
191
+ return {
192
+ root,
193
+ manifest,
194
+ nodeVersion,
195
+ language,
196
+ migration,
197
+ dependencies,
198
+ obsoleteFiles,
199
+ managedFiles,
200
+ localConfig,
201
+ workspaceConfig: config,
202
+ permissions,
203
+ state,
204
+ verification,
205
+ initPlan: options.initPlan || null,
206
+ stages,
207
+ };
208
+ }
209
+
210
+ async function initializeWorkspace(rootInput, options = {}) {
211
+ const root = path.resolve(rootInput || ".");
212
+ const manifest = loadInitManifest(options.manifestFile || MANIFEST_FILE);
213
+ const files = [
214
+ statePath(root),
215
+ configPath(root),
216
+ path.join(root, ".gitignore"),
217
+ path.join(root, ".codex", "config.toml"),
218
+ ...permissionTargets(root, options.tools),
219
+ ...manifest.managedFiles.map((entry) => path.join(root, entry.target)),
220
+ ...OBSOLETE_ASSETS.map((target) => path.join(root, target)),
221
+ ];
222
+ const transaction = createFileTransaction(files);
223
+ try {
224
+ const result = await initializeWorkspaceStages(root, { ...options, transaction });
225
+ transaction.commit();
226
+ return result;
227
+ } catch (error) {
228
+ transaction.rollback(error);
229
+ throw error;
230
+ }
231
+ }
232
+
233
+ module.exports = { INITIALIZATION_STAGE_IDS, collectWorkspaceSetup, initializeWorkspace, initializeWorkspaceStages };