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,34 @@
1
+ const { WorkspaceError } = require("./errors");
2
+
3
+ const SUPPORTED_TOOLS = Object.freeze(["claude", "codex"]);
4
+
5
+ function normalizeTools(value, label = "tools") {
6
+ let tools;
7
+ if (Array.isArray(value)) tools = value;
8
+ else if (value === "none") tools = [];
9
+ else if (value === "all") tools = [...SUPPORTED_TOOLS];
10
+ else tools = String(value || "").split(",").map((entry) => entry.trim()).filter(Boolean);
11
+ const normalized = [...new Set(tools)];
12
+ const unsupported = normalized.filter((tool) => !SUPPORTED_TOOLS.includes(tool));
13
+ if (unsupported.length > 0) {
14
+ throw new WorkspaceError("CLI_INVALID_TOOLS", `Unsupported AI tool${unsupported.length === 1 ? "" : "s"}: ${unsupported.join(", ")}`, {
15
+ actual: unsupported,
16
+ supported: SUPPORTED_TOOLS,
17
+ source: label,
18
+ });
19
+ }
20
+ return normalized;
21
+ }
22
+
23
+ function resolveWorkspaceTools(options = {}) {
24
+ if (options.explicit !== undefined) {
25
+ return { tools: normalizeTools(options.explicit, "cli"), source: "cli" };
26
+ }
27
+ const persisted = options.state?.tools;
28
+ if (persisted !== undefined) {
29
+ return { tools: normalizeTools(persisted, "workspace-state"), source: "workspace-state" };
30
+ }
31
+ return { tools: normalizeTools(options.manifestTools || SUPPORTED_TOOLS, "manifest"), source: "manifest" };
32
+ }
33
+
34
+ module.exports = { SUPPORTED_TOOLS, normalizeTools, resolveWorkspaceTools };
@@ -0,0 +1,132 @@
1
+ const fs = require("node:fs");
2
+ const path = require("node:path");
3
+
4
+ const { atomicWrite } = require("./fs");
5
+
6
+ const RETAINED_EFFECT_STATUSES = new Set(["applied", "possibly-applied"]);
7
+
8
+ function normalizeRetainedEffect(effect) {
9
+ if (!effect || typeof effect !== "object" || !effect.kind) {
10
+ throw new Error("Retained effects require a kind");
11
+ }
12
+ const status = effect.status || (effect.verified === true ? "applied" : "possibly-applied");
13
+ if (!RETAINED_EFFECT_STATUSES.has(status)) {
14
+ throw new Error(`Invalid retained effect status: ${status}`);
15
+ }
16
+ return { ...effect, status };
17
+ }
18
+
19
+ function attachRetainedEffects(error, effects) {
20
+ const target = error instanceof Error ? error : new Error(String(error));
21
+ const additions = (effects || []).filter(Boolean).map(normalizeRetainedEffect);
22
+ if (additions.length === 0) return target;
23
+ const previousEffects = target.details?.effects || {};
24
+ target.details = {
25
+ ...(target.details || {}),
26
+ effects: {
27
+ ...previousEffects,
28
+ retained: [
29
+ ...(previousEffects.retained || []).map(normalizeRetainedEffect),
30
+ ...additions,
31
+ ],
32
+ },
33
+ };
34
+ return target;
35
+ }
36
+
37
+ function uniqueFiles(files) {
38
+ return [...new Set(files.map((file) => path.resolve(file)))];
39
+ }
40
+
41
+ function missingParents(file) {
42
+ const missing = [];
43
+ let directory = path.dirname(file);
44
+ while (!fs.existsSync(directory)) {
45
+ missing.push(directory);
46
+ const parent = path.dirname(directory);
47
+ if (parent === directory) break;
48
+ directory = parent;
49
+ }
50
+ return missing;
51
+ }
52
+
53
+ function createFileTransaction(files) {
54
+ const snapshots = uniqueFiles(files).map((file) => ({
55
+ file,
56
+ existed: fs.existsSync(file),
57
+ content: fs.existsSync(file) ? fs.readFileSync(file) : null,
58
+ missingParents: missingParents(file),
59
+ }));
60
+ const externalEffects = [];
61
+ let finished = false;
62
+
63
+ return {
64
+ recordExternalEffect(effect) {
65
+ if (effect?.verified !== true) {
66
+ throw new Error(`Cannot record an unverified external effect: ${effect?.kind || "unknown"}`);
67
+ }
68
+ externalEffects.push(normalizeRetainedEffect({ ...effect, status: "applied" }));
69
+ },
70
+ commit() {
71
+ finished = true;
72
+ },
73
+ rollback(error) {
74
+ if (finished) return;
75
+ const rollbackErrors = [];
76
+ const restored = [];
77
+ const removed = [];
78
+ for (const snapshot of snapshots.slice().reverse()) {
79
+ try {
80
+ if (snapshot.existed) {
81
+ atomicWrite(snapshot.file, snapshot.content);
82
+ restored.push(snapshot.file);
83
+ } else if (fs.existsSync(snapshot.file)) {
84
+ fs.unlinkSync(snapshot.file);
85
+ removed.push(snapshot.file);
86
+ }
87
+ } catch (rollbackError) {
88
+ rollbackErrors.push(`${snapshot.file}: ${rollbackError.message}`);
89
+ }
90
+ }
91
+ const directories = snapshots.flatMap((snapshot) => snapshot.missingParents).sort((left, right) => right.length - left.length);
92
+ for (const directory of [...new Set(directories)]) {
93
+ try {
94
+ if (fs.existsSync(directory) && fs.readdirSync(directory).length === 0) fs.rmdirSync(directory);
95
+ } catch (rollbackError) {
96
+ rollbackErrors.push(`${directory}: ${rollbackError.message}`);
97
+ }
98
+ }
99
+ finished = true;
100
+ if (error) {
101
+ const previousEffects = error.details?.effects || {};
102
+ const retained = [
103
+ ...(previousEffects.retained || []).map(normalizeRetainedEffect),
104
+ ...externalEffects.map((effect) => ({ ...effect, retained: true })),
105
+ ];
106
+ error.details = {
107
+ ...(error.details || {}),
108
+ workspaceRolledBack: rollbackErrors.length === 0,
109
+ ...(externalEffects.length > 0 ? { externalEffects } : {}),
110
+ ...(rollbackErrors.length > 0 ? { rollbackErrors } : {}),
111
+ effects: {
112
+ restored: [...new Set([...(previousEffects.restored || []), ...restored])],
113
+ removed: [...new Set([...(previousEffects.removed || []), ...removed])],
114
+ retained,
115
+ rollbackFailures: [...(previousEffects.rollbackFailures || []), ...rollbackErrors],
116
+ },
117
+ };
118
+ }
119
+ },
120
+ get externalEffects() {
121
+ return externalEffects.map((effect) => ({ ...effect }));
122
+ },
123
+ };
124
+ }
125
+
126
+ module.exports = {
127
+ attachRetainedEffects,
128
+ createFileTransaction,
129
+ missingParents,
130
+ normalizeRetainedEffect,
131
+ uniqueFiles,
132
+ };
@@ -0,0 +1,186 @@
1
+ const fs = require("node:fs");
2
+ const path = require("node:path");
3
+
4
+ const { add, result } = require("./diagnostics");
5
+ const { CURRENT_CONFIG_VERSION } = require("./config");
6
+ const { inspectGitWorktree } = require("./project");
7
+
8
+ function isSameOrDescendant(parent, candidate) {
9
+ const relative = path.relative(parent, candidate);
10
+ return relative === "" || (!relative.startsWith(`..${path.sep}`) && relative !== ".." && !path.isAbsolute(relative));
11
+ }
12
+
13
+ function diagnosticProjects(...values) {
14
+ return [...new Set(values.flat().filter((value) => typeof value === "string" && value))];
15
+ }
16
+
17
+ function addExistingDiagnostic(output, diagnostic) {
18
+ output.diagnostics.push(diagnostic);
19
+ if (diagnostic.severity === "error") output.errors.push(diagnostic.message);
20
+ if (diagnostic.severity === "warning") output.warnings.push(diagnostic.message);
21
+ }
22
+
23
+ function validateSchemaAndRegistry(output, config) {
24
+ if (![1, CURRENT_CONFIG_VERSION].includes(config.schemaVersion)) {
25
+ add(output, "error", "UNSUPPORTED_SCHEMA_VERSION", `Unsupported schemaVersion: ${config.schemaVersion}`);
26
+ }
27
+ if (!Array.isArray(config.projects)) {
28
+ add(output, "error", "PROJECTS_NOT_ARRAY", "projects must be an array.");
29
+ return false;
30
+ }
31
+ return true;
32
+ }
33
+
34
+ function validateProjectFields(output, project, label) {
35
+ if (!project || typeof project !== "object" || Array.isArray(project)) {
36
+ add(output, "error", "PROJECT_NOT_OBJECT", `Project ${label} must be a mapping.`);
37
+ return false;
38
+ }
39
+ for (const field of ["name", "location", "branch", "type", "context"]) {
40
+ if (typeof project[field] !== "string" || !project[field].trim()) {
41
+ add(output, "error", "PROJECT_FIELD_REQUIRED", `Project ${label} must contain a non-empty ${field}.`, { projectName: project.name || null, field });
42
+ }
43
+ }
44
+ if (project.updateLatest !== undefined && typeof project.updateLatest !== "boolean") {
45
+ add(output, "error", "PROJECT_UPDATE_LATEST_INVALID", `Project ${label} updateLatest must be a boolean when present.`, {
46
+ projectName: project.name || null,
47
+ field: "updateLatest",
48
+ actual: project.updateLatest,
49
+ });
50
+ }
51
+ if (typeof project.location !== "string" || !path.isAbsolute(project.location)) {
52
+ add(output, "error", "PROJECT_LOCATION_NOT_ABSOLUTE", `Project ${label} location must be absolute: ${project.location}`, { projectName: project.name || null });
53
+ return false;
54
+ }
55
+ return true;
56
+ }
57
+
58
+ function inspectValidatedProject(output, rootRealPath, project, label, inspect) {
59
+ try {
60
+ const actual = inspect(project.location);
61
+ if (isSameOrDescendant(actual.realPath, rootRealPath) || isSameOrDescendant(rootRealPath, actual.realPath)) {
62
+ add(output, "error", "PROJECT_OVERLAPS_WORKSPACE", `Project ${label} must not overlap the workspace root: ${actual.realPath}`, { projectName: project.name || null });
63
+ }
64
+ if (project.branch && project.branch !== actual.branch) {
65
+ add(output, "error", "PROJECT_BRANCH_MISMATCH", `Project ${label} registered branch ${project.branch} does not match actual branch ${actual.branch}.`, {
66
+ projectName: project.name || null,
67
+ registeredBranch: project.branch,
68
+ actualBranch: actual.branch,
69
+ location: actual.realPath,
70
+ });
71
+ }
72
+ return actual;
73
+ } catch (error) {
74
+ add(output, "error", "PROJECT_INSPECTION_FAILED", `Project ${label} cannot be inspected: ${error.message}`, {
75
+ projectName: project.name || null,
76
+ location: project.location,
77
+ cause: error.code || error.name,
78
+ });
79
+ return null;
80
+ }
81
+ }
82
+
83
+ function validateProjects(root, config) {
84
+ const output = result();
85
+ if (!validateSchemaAndRegistry(output, config)) return output;
86
+ if (config.projects.length === 0) {
87
+ add(output, "warning", "NO_PROJECTS", "No local projects are configured.");
88
+ return output;
89
+ }
90
+
91
+ const rootRealPath = fs.realpathSync(root);
92
+ const names = new Map();
93
+ const inspected = [];
94
+
95
+ config.projects.forEach((project, index) => {
96
+ const label = project?.name || `#${index + 1}`;
97
+ if (!validateProjectFields(output, project, label)) return;
98
+ if (project.name) {
99
+ const previous = names.get(project.name);
100
+ if (previous) add(output, "error", "DUPLICATE_PROJECT_NAME", `Duplicate project name: ${project.name}`, {
101
+ projectName: project.name,
102
+ projects: diagnosticProjects(previous, project.name),
103
+ });
104
+ else names.set(project.name, project.name);
105
+ }
106
+ const actual = inspectValidatedProject(output, rootRealPath, project, label, inspectGitWorktree);
107
+ if (actual) {
108
+ inspected.push({ name: label, projectName: project.name || null, realPath: actual.realPath });
109
+ }
110
+ });
111
+
112
+ for (let left = 0; left < inspected.length; left += 1) {
113
+ for (let right = left + 1; right < inspected.length; right += 1) {
114
+ const a = inspected[left];
115
+ const b = inspected[right];
116
+ if (a.realPath === b.realPath) {
117
+ add(output, "error", "DUPLICATE_PROJECT_PATH", `Projects ${a.name} and ${b.name} resolve to the same directory: ${a.realPath}`, {
118
+ projects: diagnosticProjects(a.projectName, b.projectName),
119
+ });
120
+ } else if (isSameOrDescendant(a.realPath, b.realPath) || isSameOrDescendant(b.realPath, a.realPath)) {
121
+ add(output, "error", "NESTED_PROJECT_PATH", `Projects ${a.name} and ${b.name} must not be nested.`, {
122
+ projects: diagnosticProjects(a.projectName, b.projectName),
123
+ });
124
+ }
125
+ }
126
+ }
127
+ return output;
128
+ }
129
+
130
+ function validateProject(root, config, name, options = {}) {
131
+ const output = result();
132
+ if (!validateSchemaAndRegistry(output, config)) return output;
133
+ const matches = config.projects
134
+ .map((project, index) => ({ project, index }))
135
+ .filter(({ project }) => project && typeof project === "object" && !Array.isArray(project) && project.name === name);
136
+ if (matches.length === 0) {
137
+ add(output, "error", "PROJECT_NOT_FOUND", `Unknown local project: ${name}`, { projectName: name });
138
+ return output;
139
+ }
140
+ const { project, index } = matches[0];
141
+ const label = project.name || `#${index + 1}`;
142
+ if (!validateProjectFields(output, project, label)) return output;
143
+ if (matches.length > 1) {
144
+ add(output, "error", "DUPLICATE_PROJECT_NAME", `Duplicate project name: ${name}`, {
145
+ projectName: name,
146
+ projects: [name],
147
+ });
148
+ }
149
+
150
+ const rootRealPath = fs.realpathSync(root);
151
+ inspectValidatedProject(output, rootRealPath, project, label, options.inspectGitWorktree || inspectGitWorktree);
152
+ const targetPath = path.resolve(project.location);
153
+ config.projects.forEach((other, otherIndex) => {
154
+ if (otherIndex === index || !other || typeof other !== "object" || Array.isArray(other)) return;
155
+ if (typeof other.location !== "string" || !path.isAbsolute(other.location)) return;
156
+ const otherPath = path.resolve(other.location);
157
+ if (targetPath === otherPath) {
158
+ add(output, "error", "DUPLICATE_PROJECT_PATH", `Projects ${label} and ${other.name || `#${otherIndex + 1}`} use the same registered directory: ${targetPath}`, {
159
+ projectName: name,
160
+ projects: diagnosticProjects(name, other.name),
161
+ });
162
+ } else if (isSameOrDescendant(targetPath, otherPath) || isSameOrDescendant(otherPath, targetPath)) {
163
+ add(output, "error", "NESTED_PROJECT_PATH", `Projects ${label} and ${other.name || `#${otherIndex + 1}`} must not be nested.`, {
164
+ projectName: name,
165
+ projects: diagnosticProjects(name, other.name),
166
+ });
167
+ }
168
+ });
169
+ return output;
170
+ }
171
+
172
+ function validateProjectSelection(root, config, names, options = {}) {
173
+ const output = result();
174
+ for (const name of names) {
175
+ const selected = validateProject(root, config, name, options);
176
+ for (const diagnostic of selected.diagnostics) addExistingDiagnostic(output, diagnostic);
177
+ }
178
+ return output;
179
+ }
180
+
181
+ module.exports = {
182
+ isSameOrDescendant,
183
+ validateProject,
184
+ validateProjectSelection,
185
+ validateProjects,
186
+ };
@@ -0,0 +1,11 @@
1
+ const { interpolate } = require("./interpolate");
2
+ const { DEFAULT_LANGUAGE_CODE, LANGUAGE_CODES, LANGUAGE_OPTIONS, LOCALES, getLocale } = require("./registry");
3
+
4
+ module.exports = {
5
+ DEFAULT_LANGUAGE_CODE,
6
+ LANGUAGE_CODES,
7
+ LANGUAGE_OPTIONS,
8
+ LOCALES,
9
+ getLocale,
10
+ interpolate,
11
+ };
@@ -0,0 +1,7 @@
1
+ function interpolate(template, values = {}) {
2
+ return String(template).replace(/\{([A-Za-z][A-Za-z0-9]*)\}/g, (match, key) => (
3
+ Object.hasOwn(values, key) ? String(values[key]) : match
4
+ ));
5
+ }
6
+
7
+ module.exports = { interpolate };
@@ -0,0 +1,10 @@
1
+ module.exports = {
2
+ code: "en-US",
3
+ label: "English",
4
+ projectContext: {
5
+ responsibility: "Responsibility",
6
+ technologyStack: "Technology stack",
7
+ codeLocations: "Code locations",
8
+ projectBoundary: "Project boundary",
9
+ },
10
+ };
@@ -0,0 +1,11 @@
1
+ module.exports = {
2
+ code: "zh-CN",
3
+ label: "中文(简体)",
4
+ default: true,
5
+ projectContext: {
6
+ responsibility: "职责",
7
+ technologyStack: "技术栈",
8
+ codeLocations: "代码定位",
9
+ projectBoundary: "项目边界",
10
+ },
11
+ };
@@ -0,0 +1,42 @@
1
+ const fs = require("node:fs");
2
+ const path = require("node:path");
3
+
4
+ const LOCALES_DIRECTORY = path.join(__dirname, "locales");
5
+ const LOCALES = Object.freeze(
6
+ fs.readdirSync(LOCALES_DIRECTORY)
7
+ .filter((file) => file.endsWith(".js"))
8
+ .sort()
9
+ .map((file) => require(path.join(LOCALES_DIRECTORY, file)))
10
+ .sort((left, right) => left.code.localeCompare(right.code))
11
+ );
12
+ const LOCALES_BY_CODE = new Map(LOCALES.map((locale) => [locale.code, locale]));
13
+ const LANGUAGE_CODES = Object.freeze(LOCALES.map((locale) => locale.code));
14
+ const LANGUAGE_OPTIONS = Object.freeze(LOCALES.map((locale) => Object.freeze({
15
+ value: locale.code,
16
+ label: locale.label,
17
+ })));
18
+
19
+ if (LOCALES_BY_CODE.size !== LOCALES.length) throw new Error("Duplicate locale code in i18n registry");
20
+ if (LOCALES.length === 0) throw new Error("At least one locale must be registered");
21
+ for (const locale of LOCALES) {
22
+ if (!locale?.code || !/^[A-Za-z0-9-]+$/.test(locale.code) || !locale.label || !locale.projectContext) {
23
+ throw new Error(`Invalid locale definition: ${locale?.code || "<missing>"}`);
24
+ }
25
+ }
26
+ const defaults = LOCALES.filter((locale) => locale.default === true);
27
+ if (defaults.length !== 1) throw new Error("Exactly one locale must be the default");
28
+ const DEFAULT_LANGUAGE_CODE = defaults[0].code;
29
+
30
+ function getLocale(code) {
31
+ const locale = LOCALES_BY_CODE.get(String(code || "").trim());
32
+ if (!locale) throw new Error(`Unsupported locale: ${code || "<missing>"}`);
33
+ return locale;
34
+ }
35
+
36
+ module.exports = {
37
+ DEFAULT_LANGUAGE_CODE,
38
+ LANGUAGE_CODES,
39
+ LANGUAGE_OPTIONS,
40
+ LOCALES,
41
+ getLocale,
42
+ };
package/src/index.js ADDED
@@ -0,0 +1,25 @@
1
+ const config = require("./core/config");
2
+ const doctor = require("./core/doctor");
3
+ const init = require("./core/init");
4
+ const initializer = require("./core/initializer");
5
+ const i18n = require("./i18n");
6
+ const language = require("./core/language");
7
+ const managedFiles = require("./core/managed-files");
8
+ const monitor = require("./monitor");
9
+ const monitorPage = require("./monitor/page");
10
+ const project = require("./core/project");
11
+ const validation = require("./core/validation");
12
+
13
+ module.exports = {
14
+ ...config,
15
+ ...doctor,
16
+ ...init,
17
+ ...initializer,
18
+ ...i18n,
19
+ ...language,
20
+ ...managedFiles,
21
+ ...monitor,
22
+ ...monitorPage,
23
+ ...project,
24
+ ...validation,
25
+ };
@@ -0,0 +1,12 @@
1
+ function createInitPlan({ root, workspace, tools, monitor, language, extensions = [] }) {
2
+ return {
3
+ root,
4
+ workspace,
5
+ tools: tools.slice(),
6
+ monitor: { ...monitor },
7
+ language,
8
+ extensions: extensions.slice(),
9
+ };
10
+ }
11
+
12
+ module.exports = { createInitPlan };
package/src/init/ui.js ADDED
@@ -0,0 +1,61 @@
1
+ async function createInteractiveUi(options = {}) {
2
+ const clack = await import("@clack/prompts");
3
+ const input = options.input || process.stdin;
4
+ const output = options.output || process.stdout;
5
+ const common = { input, output };
6
+ const cancelMessage = options.cancelMessage || "Initialization cancelled. No changes were made.";
7
+ const cancelCode = options.cancelCode || "INIT_CANCELLED";
8
+
9
+ function unwrap(value) {
10
+ if (!clack.isCancel(value)) return value;
11
+ clack.cancel(cancelMessage, common);
12
+ const error = new Error(cancelMessage);
13
+ error.code = cancelCode;
14
+ throw error;
15
+ }
16
+
17
+ return {
18
+ intro(message = "Code Workspace setup") {
19
+ clack.intro(message, common);
20
+ },
21
+ note(title, lines) {
22
+ clack.note(lines.join("\n"), title, common);
23
+ },
24
+ async text(label, initial) {
25
+ return unwrap(await clack.text({
26
+ ...common,
27
+ message: label,
28
+ placeholder: initial,
29
+ defaultValue: initial,
30
+ }));
31
+ },
32
+ async select(label, choices, initial = 0) {
33
+ return unwrap(await clack.select({
34
+ ...common,
35
+ message: label,
36
+ options: choices,
37
+ initialValue: choices[initial]?.value,
38
+ }));
39
+ },
40
+ async multiselect(label, choices, initialValues = []) {
41
+ return unwrap(await clack.multiselect({
42
+ ...common,
43
+ message: label,
44
+ options: choices,
45
+ initialValues,
46
+ required: false,
47
+ }));
48
+ },
49
+ async confirm(label, initial = true) {
50
+ return unwrap(await clack.confirm({ ...common, message: label, initialValue: initial }));
51
+ },
52
+ cancel(message) {
53
+ clack.cancel(message, common);
54
+ },
55
+ close(message) {
56
+ clack.outro(message, common);
57
+ },
58
+ };
59
+ }
60
+
61
+ module.exports = { createInteractiveUi };
@@ -0,0 +1,97 @@
1
+ const fs = require("node:fs");
2
+ const path = require("node:path");
3
+ const { randomUUID } = require("node:crypto");
4
+
5
+ const { DEFAULT_MONITOR_URL, DEFAULT_WORKSPACE_NAME, configPath, loadConfig } = require("../core/config");
6
+ const { DEFAULT_WORKSPACE_LANGUAGE, SUPPORTED_LANGUAGES, resolveWorkspaceLanguage } = require("../core/language");
7
+ const { createInitPlan } = require("./plan");
8
+ const { createInteractiveUi } = require("./ui");
9
+ const { formatPermissionPlan, planPermissionChanges } = require("../core/permissions");
10
+ const { resolveExtensionPlans } = require("../core/extensions");
11
+
12
+ async function collectInitPlan(root, manifest, options = {}) {
13
+ const ui = options.ui || await createInteractiveUi(options);
14
+ ui.intro();
15
+ ui.note("Environment", [
16
+ `Node.js ${options.nodeVersion || process.versions.node}`,
17
+ `Target ${root}`,
18
+ ]);
19
+
20
+ const requestedLanguage = options.language;
21
+ const existingLanguage = resolveWorkspaceLanguage(root, {
22
+ language: requestedLanguage,
23
+ defaultLanguage: DEFAULT_WORKSPACE_LANGUAGE,
24
+ allowLegacy: true,
25
+ });
26
+ const existing = fs.existsSync(configPath(root)) ? loadConfig(root, { defaultLanguage: existingLanguage }) : null;
27
+ const name = existing?.workspace?.name || options.workspaceName || await ui.text("Workspace name", DEFAULT_WORKSPACE_NAME);
28
+ const languageChoices = SUPPORTED_LANGUAGES.map((entry) => ({
29
+ value: entry.value,
30
+ label: `${entry.label} · ${entry.value}`,
31
+ }));
32
+ const language = requestedLanguage || await ui.select(
33
+ "Workspace language",
34
+ languageChoices,
35
+ Math.max(0, languageChoices.findIndex((entry) => entry.value === existingLanguage))
36
+ );
37
+ const toolChoices = [
38
+ { value: "claude", label: "Claude Code" },
39
+ { value: "codex", label: "Codex" },
40
+ ];
41
+ const tools = options.tools || await ui.multiselect(
42
+ "Agent tools (select any)",
43
+ toolChoices,
44
+ options.initialTools || ["claude", "codex"]
45
+ );
46
+ const extensionCatalog = options.extensionCatalog || [];
47
+ const supportedExtensions = extensionCatalog.filter((entry) => entry.latestSupported);
48
+ const extensionNames = options.extensions !== undefined
49
+ ? options.extensions
50
+ : supportedExtensions.length > 0
51
+ ? await ui.multiselect(
52
+ "Extensions (experimental, select any)",
53
+ supportedExtensions.map((entry) => ({
54
+ value: entry.id,
55
+ label: `${entry.name} · latest supported: ${entry.latestSupported.version} · Extension Spec ${entry.latestSupported.extensionSpecVersion}`,
56
+ })),
57
+ options.initialExtensions || []
58
+ )
59
+ : [];
60
+ const extensions = resolveExtensionPlans(extensionCatalog, extensionNames, {
61
+ tools,
62
+ state: options.extensionState,
63
+ });
64
+ let monitorEnabled = options.monitor !== undefined ? options.monitor : existing?.monitor?.enable;
65
+ if (tools.includes("codex") && monitorEnabled === undefined) monitorEnabled = await ui.confirm("Enable Codex Agent monitor?", true);
66
+ const monitor = {
67
+ enable: tools.includes("codex") && monitorEnabled === true,
68
+ url: options.monitorUrl || existing?.monitor?.url || DEFAULT_MONITOR_URL,
69
+ };
70
+ const workspace = existing?.workspace || { name, uuid: randomUUID() };
71
+ const plan = createInitPlan({ root, workspace, tools, monitor, language, extensions });
72
+ ui.note("Ready to initialize", [
73
+ `Workspace ${workspace.name}`,
74
+ `Language ${language}`,
75
+ `Tools ${tools.length ? tools.join(", ") : "none"}`,
76
+ `Monitor ${monitor.enable ? monitor.url : "disabled"}`,
77
+ `Extensions ${extensions.length ? extensions.map((entry) => `${entry.id}@${entry.version} [Spec ${entry.extensionSpecVersion}] (${entry.manifestSha256})`).join(", ") : "none"}`,
78
+ ]);
79
+ if (existing?.projects?.length) {
80
+ const permissionPlan = planPermissionChanges({
81
+ root,
82
+ tools,
83
+ grants: existing.projects.map((project) => project.location),
84
+ });
85
+ ui.note("Agent authorization", formatPermissionPlan(permissionPlan).split("\n"));
86
+ }
87
+ if (!await ui.confirm("Continue?", true)) {
88
+ (ui.cancel || ui.close)("Initialization cancelled. No changes were made.");
89
+ const error = new Error("Initialization cancelled. No changes were made.");
90
+ error.code = "INIT_CANCELLED";
91
+ throw error;
92
+ }
93
+ ui.close("Plan confirmed. Starting initialization…");
94
+ return plan;
95
+ }
96
+
97
+ module.exports = { collectInitPlan };
@@ -0,0 +1,33 @@
1
+ const fs = require("node:fs");
2
+ const path = require("node:path");
3
+
4
+ const LOCALES_DIRECTORY = path.join(__dirname, "locales");
5
+ const MONITOR_LOCALES = Object.freeze(
6
+ fs.readdirSync(LOCALES_DIRECTORY)
7
+ .filter((file) => file.endsWith(".js"))
8
+ .sort()
9
+ .map((file) => require(path.join(LOCALES_DIRECTORY, file)))
10
+ .sort((left, right) => left.code.localeCompare(right.code))
11
+ );
12
+ const LOCALES_BY_CODE = new Map(MONITOR_LOCALES.map((locale) => [locale.code, locale]));
13
+
14
+ if (MONITOR_LOCALES.length === 0) throw new Error("At least one monitor locale must be registered");
15
+ if (LOCALES_BY_CODE.size !== MONITOR_LOCALES.length) throw new Error("Duplicate monitor locale code");
16
+ for (const locale of MONITOR_LOCALES) {
17
+ if (!locale?.code || !locale.label || !locale.messages) throw new Error(`Invalid monitor locale: ${locale?.code || "<missing>"}`);
18
+ }
19
+ const defaults = MONITOR_LOCALES.filter((locale) => locale.default === true);
20
+ if (defaults.length !== 1) throw new Error("Exactly one monitor locale must be the default");
21
+
22
+ const DEFAULT_MONITOR_LANGUAGE = defaults[0].code;
23
+ const MONITOR_MESSAGES = Object.freeze(Object.fromEntries(
24
+ MONITOR_LOCALES.map((locale) => [locale.code, locale.messages])
25
+ ));
26
+
27
+ function getMonitorLocale(code) {
28
+ const locale = LOCALES_BY_CODE.get(String(code || "").trim());
29
+ if (!locale) throw new Error(`Unsupported monitor locale: ${code || "<missing>"}`);
30
+ return locale;
31
+ }
32
+
33
+ module.exports = { DEFAULT_MONITOR_LANGUAGE, MONITOR_LOCALES, MONITOR_MESSAGES, getMonitorLocale };