code-workspace-zhuiyi 0.1.0-beta.2 → 0.1.0-beta.3

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/README.md CHANGED
@@ -37,7 +37,7 @@ Use `--tools claude`, `--tools codex`, or `--tools none` to override the default
37
37
 
38
38
  Initialization writes only Workspace-owned state and integrations:
39
39
 
40
- - `.code-workspace/config.yaml` and `.code-workspace/state.json`
40
+ - `.code-workspace/config.yaml`, `.code-workspace/config-projects.yaml`, and `.code-workspace/state.json`
41
41
  - `USER_GUIDE.md`
42
42
  - `CLAUDE.md` and/or `AGENTS.md`
43
43
  - Workspace-specific commands and skills whose names start with `code-workspace-` or use the `/code-workspace` namespace
@@ -50,20 +50,20 @@ It does not create `openspec/`, install native `/opsx` commands, or install nati
50
50
  `init` can install integrations from the versioned `extensions/` repository shipped inside this npm package. Select extension names interactively, or pass a comma-separated name list non-interactively. The dedicated install command accepts one or more names; without names it opens the built-in extension multiselect, where ESC exits without changes:
51
51
 
52
52
  ```bash
53
- code-w init . --extensions openspec-workspace --yes
53
+ code-w init . --extensions zhuiyi-jira-mcp --yes
54
54
  code-w init . --extensions none --yes
55
55
  code-w extension install
56
- code-w extension install openspec-workspace --yes
57
- code-w extension uninstall openspec-workspace --yes
56
+ code-w extension install zhuiyi-jira-mcp --yes
57
+ code-w extension uninstall zhuiyi-jira-mcp --yes
58
58
  ```
59
59
 
60
60
  The Workspace operation lock shared by init, extension install, and extension uninstall is configured in the Code Workspace project's `.env` (not in the target Workspace). `CODE_WORKSPACE_INIT_LOCK_UPDATE_MS` defaults to `5000`, and `CODE_WORKSPACE_INIT_LOCK_STALE_MS` defaults to `30000`; process environment variables take precedence. See `.env.example` for the project configuration names.
61
61
 
62
- Users select names, not versions; `openspec-workspace@1.0.0` is intentionally rejected. Code Workspace resolves the highest extension SemVer implemented against a Host-supported Extension Spec before confirmation. A new non-interactive Workspace installs no extensions unless `--extensions` is provided. Re-initializing an existing Workspace defaults to its installed extensions and upgrades them when a newer supported built-in version exists. `none` skips extension work and does not uninstall existing artifacts.
62
+ Users select names, not versions; `zhuiyi-jira-mcp@0.1.0` is intentionally rejected. Code Workspace resolves the highest extension SemVer implemented against a Host-supported Extension Spec before confirmation. A new non-interactive Workspace installs no extensions unless `--extensions` is provided. Re-initializing an existing Workspace defaults to its installed extensions and upgrades them when a newer supported built-in version exists. `none` skips extension work and does not uninstall existing artifacts.
63
63
 
64
64
  `extension install` does not rerun core Workspace initialization. In JSON, non-TTY, or `--yes` mode, at least one extension name is required. Multiple names are installed in order with one confirmation boundary and independent transactions; any failure makes the install command fail while later extensions still run.
65
65
 
66
- The bundled `openspec-workspace` extension installs a namespaced `code-workspace-openspec-propose` skill for the selected Agent tools. It does not create an `openspec/` directory or install native OpenSpec commands.
66
+ The bundled `zhuiyi-jira-mcp` extension configures the Jira MCP service for the selected Agent tools. It does not create an `openspec/` directory or install native OpenSpec commands.
67
67
 
68
68
  Extension entries run in separate Node processes and generate files in temporary staging directories. The host rejects undeclared, missing, symbolic-link, non-file, path-escaping, conflicting, and checksum-mismatched artifacts before transactionally installing them. Per-Workspace state is stored in `.code-workspace/ext-manifest.json`. A failed extension is reported as a warning and does not roll back successful core initialization or stop later extensions; a failed upgrade restores and retains the previous installed version.
69
69
 
@@ -93,6 +93,30 @@ code-workspace project add --projects-file projects.json --yes --json
93
93
 
94
94
  The registry stores each project's name, real location, registered branch, type, and context. The registered branch is the Code Workspace expected state; the actual branch is observed from the selected Git worktree. Workspace never guesses a path from a conversation or automatically decides which branch is authoritative.
95
95
 
96
+ Project registration is always stored in the separate `.code-workspace/config-projects.yaml` file. The main configuration contains only its fixed local reference:
97
+
98
+ ```yaml
99
+ # .code-workspace/config.yaml
100
+ projects:
101
+ ref: config-projects.yaml
102
+ ```
103
+
104
+ The referenced file uses this format:
105
+
106
+ ```yaml
107
+ # .code-workspace/config-projects.yaml
108
+ schemaVersion: 1
109
+ projects:
110
+ - name: payments
111
+ location: /absolute/path/to/payments
112
+ branch: main
113
+ type: backend
114
+ context: |-
115
+ Service ownership and navigation context.
116
+ ```
117
+
118
+ `projects.ref` is resolved relative to `config.yaml` and must be exactly `config-projects.yaml`. URLs, globs, absolute paths, path traversal, and inline `projects` arrays are not supported. All project commands keep their existing CLI semantics; they read and write the referenced project file. The `.code-workspace/` directory is ignored by default, so Git history for this local registry requires an explicit repository policy.
119
+
96
120
  ## Daily commands
97
121
 
98
122
  ```bash
@@ -117,11 +141,18 @@ The two reconciliation directions are deliberately separate:
117
141
 
118
142
  Both commands detect plan drift and verify postconditions. `project branch update-latest` is the separate, opt-in path for projects with `updateLatest: true`; it only fetches the configured upstream and fast-forwards a clean matching branch. Code Workspace never creates or downloads a branch and never performs stash, reset, rebase, non-fast-forward merge, production-code edits, or conflict resolution.
119
143
 
120
- Users may manually set the optional project policy in `.code-workspace/config.yaml`:
144
+ Users may manually set the optional project policy in `.code-workspace/config-projects.yaml`:
121
145
 
122
146
  ```yaml
147
+ # .code-workspace/config-projects.yaml
148
+ schemaVersion: 1
123
149
  projects:
124
150
  - name: payments
151
+ location: /absolute/path/to/payments
152
+ branch: main
153
+ type: backend
154
+ context: |-
155
+ Service ownership and navigation context.
125
156
  updateLatest: true
126
157
  ```
127
158
 
package/README.zh-CN.md CHANGED
@@ -37,7 +37,7 @@ code-workspace init . \
37
37
 
38
38
  初始化只写入 Workspace 自有状态和集成:
39
39
 
40
- - `.code-workspace/config.yaml` 与 `.code-workspace/state.json`
40
+ - `.code-workspace/config.yaml`、`.code-workspace/config-projects.yaml` 与 `.code-workspace/state.json`
41
41
  - `USER_GUIDE.md`
42
42
  - `CLAUDE.md` 和/或 `AGENTS.md`
43
43
  - 名称以 `code-workspace-` 开头或使用 `/code-workspace` 命名空间的 Workspace 专用命令与 Skill
@@ -50,20 +50,20 @@ code-workspace init . \
50
50
  `init` 可以从 npm 包内随附的版本化 `extensions/` 仓库安装集成。交互模式按扩展名多选;非交互模式传入逗号分隔的扩展名。独立安装命令接受一个或多个扩展名;不传名称时打开内置扩展多选,按 ESC 可无修改退出:
51
51
 
52
52
  ```bash
53
- code-w init . --extensions openspec-workspace --yes
53
+ code-w init . --extensions zhuiyi-jira-mcp --yes
54
54
  code-w init . --extensions none --yes
55
55
  code-w extension install
56
- code-w extension install openspec-workspace --yes
57
- code-w extension uninstall openspec-workspace --yes
56
+ code-w extension install zhuiyi-jira-mcp --yes
57
+ code-w extension uninstall zhuiyi-jira-mcp --yes
58
58
  ```
59
59
 
60
60
  `init`、扩展安装和扩展卸载共享的 Workspace 操作锁配置在 Code Workspace 项目自身的 `.env` 中(不在目标 Workspace 中)。`CODE_WORKSPACE_INIT_LOCK_UPDATE_MS` 默认值为 `5000`,`CODE_WORKSPACE_INIT_LOCK_STALE_MS` 默认值为 `30000`;进程环境变量优先于 `.env`。配置项名称见 `.env.example`。
61
61
 
62
- 用户只选择扩展名,不能选择版本;`openspec-workspace@1.0.0` 会被明确拒绝。Code Workspace 在确认前,从 Host 明确支持的 Extension Spec 实现中解析最高扩展 SemVer。新 Workspace 非交互初始化时,未传 `--extensions` 就不安装扩展;已有 Workspace 重新初始化时,默认选择已安装扩展,并在存在更高受支持内置版本时升级。`none` 只跳过本次扩展初始化,不会卸载已有制品。
62
+ 用户只选择扩展名,不能选择版本;`zhuiyi-jira-mcp@0.1.0` 会被明确拒绝。Code Workspace 在确认前,从 Host 明确支持的 Extension Spec 实现中解析最高扩展 SemVer。新 Workspace 非交互初始化时,未传 `--extensions` 就不安装扩展;已有 Workspace 重新初始化时,默认选择已安装扩展,并在存在更高受支持内置版本时升级。`none` 只跳过本次扩展初始化,不会卸载已有制品。
63
63
 
64
64
  `extension install` 不会重新执行 Workspace 核心初始化。在 JSON、非 TTY 或 `--yes` 模式下,必须至少提供一个扩展名。多个名称按顺序安装,只确认一次且各自使用独立事务;任一扩展失败会使安装命令失败,但后续扩展仍会继续执行。
65
65
 
66
- 内置 `openspec-workspace` 扩展会为选中的 Agent 工具安装命名空间为 `code-workspace-openspec-propose` 的 Skill;它不会创建 `openspec/` 目录,也不会安装 OpenSpec 原生命令。
66
+ 当前随包提供的内置扩展是 `zhuiyi-jira-mcp`,用于为选中的 Agent 工具配置 Jira MCP 服务;它不会创建 `openspec/` 目录,也不会安装 OpenSpec 原生命令。
67
67
 
68
68
  扩展入口在独立 Node 进程中运行,只向临时 staging 目录生成文件。Host 会在事务安装前拒绝未声明、缺失、符号链接、非文件、路径逃逸、目标冲突和 hash 不匹配的制品。Workspace 状态存放在 `.code-workspace/ext-manifest.json`。扩展失败以 warning 报告,不回滚已成功的核心初始化,也不阻止后续扩展;升级失败会恢复并保留旧的已安装版本。
69
69
 
@@ -93,6 +93,30 @@ code-workspace project add --projects-file projects.json --yes --json
93
93
 
94
94
  注册表保存项目名称、真实路径、注册分支、类型和上下文。注册分支是 Code Workspace 的期望状态,实际分支是从选中 Git worktree 观测到的状态。Workspace 不会根据对话猜测路径,也不会自动判断哪一侧分支更权威。
95
95
 
96
+ 项目注册配置始终独立保存于 `.code-workspace/config-projects.yaml`。主配置只保存固定的本地引用:
97
+
98
+ ```yaml
99
+ # .code-workspace/config.yaml
100
+ projects:
101
+ ref: config-projects.yaml
102
+ ```
103
+
104
+ 引用文件使用以下格式:
105
+
106
+ ```yaml
107
+ # .code-workspace/config-projects.yaml
108
+ schemaVersion: 1
109
+ projects:
110
+ - name: payments
111
+ location: /absolute/path/to/payments
112
+ branch: main
113
+ type: backend
114
+ context: |-
115
+ 服务职责和代码导航上下文。
116
+ ```
117
+
118
+ `projects.ref` 相对于 `config.yaml` 解析,并且必须是 `config-projects.yaml`。不支持 URL、glob、绝对路径、路径逃逸或内联 `projects` 数组。所有项目 CLI 的语义保持不变,只是改为读取和写入引用文件。`.code-workspace/` 默认被忽略;如需 Git 历史,需要显式制定仓库策略。
119
+
96
120
  ## 日常命令
97
121
 
98
122
  ```bash
@@ -117,11 +141,18 @@ code-workspace doctor --json
117
141
 
118
142
  两条命令都会检查计划漂移并验证后置条件。`project branch update-latest` 是独立的显式配置路径:仅当项目 `updateLatest: true` 时,才对干净且分支一致的 worktree fetch upstream 并 fast-forward。Code Workspace 不会创建或下载分支,也不会执行 stash、reset、rebase、非 fast-forward merge、生产代码编辑或冲突处理。
119
143
 
120
- 用户可以手动在 `.code-workspace/config.yaml` 中设置项目策略:
144
+ 用户可以手动在 `.code-workspace/config-projects.yaml` 中设置项目策略:
121
145
 
122
146
  ```yaml
147
+ # .code-workspace/config-projects.yaml
148
+ schemaVersion: 1
123
149
  projects:
124
150
  - name: payments
151
+ location: /absolute/path/to/payments
152
+ branch: main
153
+ type: backend
154
+ context: |-
155
+ 服务职责和代码导航上下文。
125
156
  updateLatest: true
126
157
  ```
127
158
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "schemaVersion": 2,
3
- "releaseVersion": "0.1.0-beta.2",
3
+ "releaseVersion": "0.1.0-beta.3",
4
4
  "requirements": {
5
5
  "node": ">=20.19.0"
6
6
  },
@@ -5,10 +5,12 @@
5
5
  ],
6
6
  "env": {
7
7
  "JIRA_AUTH_TYPE": "cookie",
8
+ "JIRA_COOKIE": "",
8
9
  "JIRA_API_VERSION": "latest",
9
10
  "JIRA_ALLOWED_HOSTS": "jira.in.wezhuiyi.com",
10
11
  "JIRA_REQUIREMENT_FIELDS": "{\"acceptanceCriteria\":\"customfield_10042\",\"storyPoints\":\"customfield_10016\"}",
11
12
  "JIRA_ATTACHMENT_DIR": ".jira-attachments",
12
- "JIRA_ATTACHMENT_MAX_SIZE": "20MB"
13
+ "JIRA_ATTACHMENT_MAX_SIZE": "20MB",
14
+ "JIRA_ATTACHMENT_ALLOWED_TYPES": "pdf,png,jpg,jpeg,gif,webp,txt,md,csv,json,xml,doc,docx,xls,xlsx,ppt,pptx,html"
13
15
  }
14
16
  }
@@ -4,8 +4,10 @@ args = [".code-workspace/extensions/zhuiyi-jira-mcp/0.1.0/dist/index.js"]
4
4
 
5
5
  [mcp_servers.zhuiyi-jira.env]
6
6
  JIRA_AUTH_TYPE = "cookie"
7
+ JIRA_COOKIE = ""
7
8
  JIRA_API_VERSION = "latest"
8
9
  JIRA_ALLOWED_HOSTS = "jira.in.wezhuiyi.com"
9
10
  JIRA_REQUIREMENT_FIELDS = '{"acceptanceCriteria":"customfield_10042","storyPoints":"customfield_10016"}'
10
11
  JIRA_ATTACHMENT_DIR = ".jira-attachments"
11
12
  JIRA_ATTACHMENT_MAX_SIZE = "20MB"
13
+ JIRA_ATTACHMENT_ALLOWED_TYPES = "pdf,png,jpg,jpeg,gif,webp,txt,md,csv,json,xml,doc,docx,xls,xlsx,ppt,pptx,html"
@@ -0,0 +1,2 @@
1
+ # Zhuiyi Jira MCP attachments
2
+ /.jira-attachments/
@@ -30,6 +30,9 @@ async function main() {
30
30
  await prepareRelease(release, runtime);
31
31
  outputs.push({ id: "runtime", source: "runtime" });
32
32
 
33
+ copyOutput(outputRoot, "artifacts/gitignore", "gitignore");
34
+ outputs.push({ id: "gitignore", source: "gitignore" });
35
+
33
36
  if (context.tools.includes("codex")) {
34
37
  copyOutput(outputRoot, "artifacts/codex/config.toml", "codex-config.toml");
35
38
  outputs.push({ id: "codex-config", source: "codex-config.toml" });
@@ -6,7 +6,7 @@
6
6
  "name": "Zhuiyi Jira MCP",
7
7
  "version": "0.1.0",
8
8
  "entry": "init.js",
9
- "entrySha256": "af7f4a71835402235b3d763418ee1b4135da0125a85aff1153bfaa0f6c83a022",
9
+ "entrySha256": "53a814ffcbc5669a024f921c5c30e74385d334b749913b0212bdcc2e48a2e0bd",
10
10
  "timeoutMs": 120000,
11
11
  "capabilities": {
12
12
  "networkHosts": ["gitee.com", "raw.giteeusercontent.com"]
@@ -18,6 +18,13 @@
18
18
  "ownership": "exclusive",
19
19
  "target": ".code-workspace/extensions/zhuiyi-jira-mcp/0.1.0"
20
20
  },
21
+ {
22
+ "id": "gitignore",
23
+ "kind": "text-block",
24
+ "ownership": "shared",
25
+ "target": ".gitignore",
26
+ "format": "text"
27
+ },
21
28
  {
22
29
  "id": "codex-config",
23
30
  "kind": "text-block",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "code-workspace-zhuiyi",
3
- "version": "0.1.0-beta.2",
3
+ "version": "0.1.0-beta.3",
4
4
  "description": "Local multi-project registry and safety layer for AI coding workspaces",
5
5
  "author": "icebearx-ai",
6
6
  "license": "MIT",
@@ -95,7 +95,7 @@ async function executeExtensionInstall(invocation) {
95
95
  let requested = invocation.args.length > 0 ? normalizeExtensionNames(invocation.args) : null;
96
96
  if (requested === null && !interactive) {
97
97
  throw new WorkspaceError("EXTENSION_SELECTION_REQUIRED", "Extension installation requires one or more extension names outside interactive mode.", {
98
- remediation: "Pass one or more names, for example: code-w extension install openspec-workspace --yes",
98
+ remediation: "Pass one or more extension names, for example: code-w extension install <extension-name> --yes",
99
99
  });
100
100
  }
101
101
 
@@ -1,4 +1,4 @@
1
- const { configPath, loadConfigProjection, updateProjectBranch } = require("../../core/config");
1
+ const { configPath, loadConfigProjection, projectConfigPath, updateProjectBranch } = require("../../core/config");
2
2
  const { WorkspaceError } = require("../../core/errors");
3
3
  const {
4
4
  assertRegisteredBranchSwitchAvailable,
@@ -54,7 +54,7 @@ function applyAcceptActual(root, plan, options = {}) {
54
54
  const currentConfig = load(root, ["projects"]);
55
55
  assertAcceptPlanCurrent(plan, inspect(currentConfig, plan.project.name, options));
56
56
 
57
- const transaction = createFileTransaction([configPath(root)]);
57
+ const transaction = createFileTransaction([configPath(root), projectConfigPath(root)]);
58
58
  try {
59
59
  (options.updateProjectBranch || updateProjectBranch)(root, {
60
60
  name: plan.project.name,
@@ -5,6 +5,7 @@ const {
5
5
  configPath,
6
6
  loadConfig,
7
7
  loadState,
8
+ projectConfigPath,
8
9
  saveConfig,
9
10
  statePath,
10
11
  } = require("../../core/config");
@@ -82,6 +83,7 @@ function updateWorkspace(root, options = {}) {
82
83
  }
83
84
  const transaction = createFileTransaction([
84
85
  configPath(root),
86
+ projectConfigPath(root),
85
87
  statePath(root),
86
88
  ...managedPlan.plans.map((plan) => plan.target),
87
89
  ...obsoletePlan.map((plan) => path.join(root, plan.target)),
@@ -8,6 +8,8 @@ const { LANGUAGE_CODES } = require("../i18n");
8
8
 
9
9
  const LOCAL_DIRECTORY = ".code-workspace";
10
10
  const CONFIG_FILE = "config.yaml";
11
+ const PROJECT_CONFIG_FILE = "config-projects.yaml";
12
+ const PROJECT_CONFIG_VERSION = 1;
11
13
  const STATE_FILE = "state.json";
12
14
  const DEFAULT_WORKSPACE_NAME = "code-workspace";
13
15
  const DEFAULT_MONITOR_URL = "http://127.0.0.1:3211";
@@ -29,6 +31,10 @@ function configPath(root) {
29
31
  return path.join(root, LOCAL_DIRECTORY, CONFIG_FILE);
30
32
  }
31
33
 
34
+ function projectConfigPath(root) {
35
+ return path.join(root, LOCAL_DIRECTORY, PROJECT_CONFIG_FILE);
36
+ }
37
+
32
38
  function statePath(root) {
33
39
  return path.join(root, LOCAL_DIRECTORY, STATE_FILE);
34
40
  }
@@ -122,6 +128,101 @@ function normalizeProjects(value) {
122
128
  });
123
129
  }
124
130
 
131
+ function normalizeProjectReference(value, options = {}) {
132
+ const file = options.file || null;
133
+ if (value == null || (typeof value === "object" && !Array.isArray(value) && !Object.prototype.hasOwnProperty.call(value, "ref"))) {
134
+ throw new WorkspaceError("PROJECT_CONFIG_REFERENCE_MISSING", "projects.ref must reference config-projects.yaml; no project configuration reference was provided", {
135
+ file,
136
+ remediation: "Set projects.ref to config-projects.yaml and move project records into .code-workspace/config-projects.yaml.",
137
+ });
138
+ }
139
+ if (typeof value !== "object" || Array.isArray(value)) {
140
+ throw new WorkspaceError("PROJECT_CONFIG_INLINE_UNSUPPORTED", "projects must reference config-projects.yaml; inline project arrays are not supported", {
141
+ file,
142
+ remediation: "Set projects.ref to config-projects.yaml and move project records into .code-workspace/config-projects.yaml.",
143
+ });
144
+ }
145
+ const ref = String(value.ref || "").trim();
146
+ if (ref !== PROJECT_CONFIG_FILE) {
147
+ throw new WorkspaceError("PROJECT_CONFIG_REFERENCE_INVALID", `projects.ref must be ${PROJECT_CONFIG_FILE}`, {
148
+ file,
149
+ ref: ref || null,
150
+ expected: PROJECT_CONFIG_FILE,
151
+ remediation: `Set projects.ref to ${PROJECT_CONFIG_FILE}.`,
152
+ });
153
+ }
154
+ return { ref: PROJECT_CONFIG_FILE };
155
+ }
156
+
157
+ function readProjectConfigDocument(root) {
158
+ const file = projectConfigPath(root);
159
+ let stat;
160
+ try {
161
+ stat = fs.lstatSync(file);
162
+ } catch (error) {
163
+ if (error.code === "ENOENT") {
164
+ throw new WorkspaceError("PROJECT_CONFIG_FILE_MISSING", `Missing project configuration: ${file}`, {
165
+ file,
166
+ remediation: "Create .code-workspace/config-projects.yaml with schemaVersion: 1 and projects: [].",
167
+ });
168
+ }
169
+ throw new WorkspaceError("PROJECT_CONFIG_FILE_INVALID", `Cannot inspect project configuration ${file}: ${error.message}`, {
170
+ file,
171
+ cause: error.code || error.name,
172
+ });
173
+ }
174
+ if (stat.isSymbolicLink()) {
175
+ throw new WorkspaceError("PROJECT_CONFIG_FILE_INVALID", `Project configuration must be a regular file, not a symbolic link: ${file}`, {
176
+ file,
177
+ kind: "symbolic-link",
178
+ remediation: "Replace the symbolic link with a regular .code-workspace/config-projects.yaml file.",
179
+ });
180
+ }
181
+ if (!stat.isFile()) {
182
+ throw new WorkspaceError("PROJECT_CONFIG_FILE_INVALID", `Project configuration must be a regular file: ${file}`, {
183
+ file,
184
+ kind: stat.isDirectory() ? "directory" : "special-file",
185
+ remediation: "Create .code-workspace/config-projects.yaml as a regular file.",
186
+ });
187
+ }
188
+ let value;
189
+ try {
190
+ value = yaml.load(fs.readFileSync(file, "utf8"));
191
+ } catch (error) {
192
+ if (error instanceof WorkspaceError) throw error;
193
+ if (error.name === "YAMLException") {
194
+ throw new WorkspaceError("PROJECT_CONFIG_FILE_PARSE_FAILED", `Cannot parse project configuration ${file}: ${error.message}`, {
195
+ file,
196
+ });
197
+ }
198
+ throw new WorkspaceError("PROJECT_CONFIG_FILE_INVALID", `Cannot read project configuration ${file}: ${error.message}`, {
199
+ file,
200
+ cause: error.code || error.name,
201
+ });
202
+ }
203
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
204
+ throw new WorkspaceError("PROJECT_CONFIG_FILE_INVALID", `Project configuration must be a YAML object: ${file}`, { file });
205
+ }
206
+ if (value.schemaVersion !== PROJECT_CONFIG_VERSION) {
207
+ throw new WorkspaceError("PROJECT_CONFIG_SCHEMA_UNSUPPORTED", `Project configuration schemaVersion must be ${PROJECT_CONFIG_VERSION}: ${file}`, {
208
+ file,
209
+ version: value.schemaVersion ?? null,
210
+ supported: PROJECT_CONFIG_VERSION,
211
+ });
212
+ }
213
+ if (!Array.isArray(value.projects)) {
214
+ throw new WorkspaceError("PROJECT_CONFIG_FILE_INVALID", `Project configuration projects must be an array: ${file}`, { file });
215
+ }
216
+ return {
217
+ file,
218
+ schemaVersion: PROJECT_CONFIG_VERSION,
219
+ value: {
220
+ ...value,
221
+ projects: normalizeProjects(value.projects),
222
+ },
223
+ };
224
+ }
225
+
125
226
  function configVersion(value) {
126
227
  const version = value?.schemaVersion ?? 0;
127
228
  if (!Number.isInteger(version) || version < MINIMUM_READABLE_CONFIG_VERSION) {
@@ -182,23 +283,31 @@ function normalizeConfig(value, options = {}) {
182
283
  const source = value && typeof value === "object" && !Array.isArray(value) ? value : {};
183
284
  const migration = planConfigMigration(source);
184
285
  const config = migration.value;
286
+ const projects = options.projects !== undefined ? normalizeProjects(options.projects) : normalizeProjects(config.projects);
185
287
  return {
186
288
  ...config,
187
289
  schemaVersion: configVersion(config),
188
290
  workspace: normalizeWorkspace(config.workspace, options),
189
291
  monitor: normalizeMonitor(config.monitor),
190
- projects: normalizeProjects(config.projects),
292
+ projects,
191
293
  };
192
294
  }
193
295
 
194
296
  function loadConfig(root, options = {}) {
195
- return normalizeConfig(readConfigDocument(root).value, options);
297
+ const document = readConfigDocument(root);
298
+ normalizeProjectReference(document.value.projects, { file: document.file });
299
+ const projects = readProjectConfigDocument(root);
300
+ return normalizeConfig(document.value, {
301
+ ...options,
302
+ projects: projects.value.projects,
303
+ projectConfigFile: projects.file,
304
+ });
196
305
  }
197
306
 
198
307
  function loadConfigProjection(root, domains, options = {}) {
199
308
  const document = readConfigDocument(root);
200
309
  const requested = new Set(domains || []);
201
- if (requested.has("complete")) return normalizeConfig(document.value, options);
310
+ if (requested.has("complete")) return loadConfig(root, options);
202
311
  const projected = { schemaVersion: CURRENT_CONFIG_VERSION, sourceSchemaVersion: document.schemaVersion };
203
312
  const attachFile = (error) => {
204
313
  error.details = { ...(error.details || {}), file: document.file };
@@ -230,9 +339,12 @@ function loadConfigProjection(root, domains, options = {}) {
230
339
  }
231
340
  if (requested.has("projects")) {
232
341
  try {
233
- projected.projects = normalizeProjects(document.value.projects);
342
+ normalizeProjectReference(document.value.projects, { file: document.file });
343
+ const projects = readProjectConfigDocument(root);
344
+ projected.projects = projects.value.projects;
234
345
  } catch (error) {
235
- attachFile(error);
346
+ error.details = { ...(error.details || {}), configFile: document.file };
347
+ throw error;
236
348
  }
237
349
  }
238
350
  return projected;
@@ -262,7 +374,7 @@ function inspectConfigDomains(root, options = {}) {
262
374
  try {
263
375
  return { valid: true, value: project(), diagnostics: [] };
264
376
  } catch (error) {
265
- error.details = { ...(error.details || {}), file: document.file };
377
+ error.details = { ...(error.details || {}), configFile: document.file };
266
378
  return { valid: false, value: null, diagnostics: [error] };
267
379
  }
268
380
  };
@@ -273,7 +385,11 @@ function inspectConfigDomains(root, options = {}) {
273
385
  identity: inspect(() => normalizeWorkspaceIdentity(document.value.workspace)),
274
386
  language: inspect(() => normalizeWorkspaceLanguage(document.value.workspace?.language, options)),
275
387
  monitor: inspect(() => normalizeMonitor(document.value.monitor)),
276
- projects: inspect(() => normalizeProjects(document.value.projects)),
388
+ projects: inspect(() => {
389
+ normalizeProjectReference(document.value.projects, { file: document.file });
390
+ const projectDocument = readProjectConfigDocument(root);
391
+ return projectDocument.value.projects;
392
+ }),
277
393
  };
278
394
  }
279
395
 
@@ -282,19 +398,33 @@ function renderConfigDocument(value) {
282
398
  }
283
399
 
284
400
  function renderConfig(config) {
285
- return renderConfigDocument(normalizeConfig(config));
401
+ const normalized = normalizeConfig(config);
402
+ const { projectReference: _projectReference, ...withoutProjectReference } = normalized;
403
+ return renderConfigDocument({
404
+ ...withoutProjectReference,
405
+ projects: { ref: PROJECT_CONFIG_FILE },
406
+ });
407
+ }
408
+
409
+ function renderProjectConfig(projects) {
410
+ return renderConfigDocument({ schemaVersion: PROJECT_CONFIG_VERSION, projects: normalizeProjects(projects) });
286
411
  }
287
412
 
288
413
  function saveConfig(root, config) {
289
- atomicWrite(configPath(root), renderConfig(config));
414
+ const normalized = normalizeConfig(config);
415
+ saveProjectConfig(root, normalized.projects);
416
+ atomicWrite(configPath(root), renderConfig(normalized));
417
+ }
418
+
419
+ function saveProjectConfig(root, projects) {
420
+ atomicWrite(projectConfigPath(root), renderProjectConfig(projects));
290
421
  }
291
422
 
292
423
  function updateProjectBranch(root, update, options = {}) {
293
424
  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
- }
425
+ normalizeProjectReference(document.value.projects, { file: document.file });
426
+ const projectDocument = (options.readProjectConfigDocument || readProjectConfigDocument)(root);
427
+ const sourceProjects = projectDocument.value.projects;
298
428
  const sourceProject = sourceProjects.find((project) => project?.name === update.name);
299
429
  if (!sourceProject || sourceProject.branch !== update.before.registeredBranch) {
300
430
  throw new WorkspaceError(
@@ -312,10 +442,10 @@ function updateProjectBranch(root, update, options = {}) {
312
442
  }
313
443
  const project = { ...sourceProject, branch: update.after.registeredBranch };
314
444
  const nextDocument = {
315
- ...document.value,
445
+ ...projectDocument.value,
316
446
  projects: sourceProjects.map((entry) => entry?.name === update.name ? project : entry),
317
447
  };
318
- (options.atomicWrite || atomicWrite)(configPath(root), renderConfigDocument(nextDocument));
448
+ (options.atomicWrite || atomicWrite)(projectConfigPath(root), renderProjectConfig(nextDocument.projects));
319
449
  return project;
320
450
  }
321
451
 
@@ -345,6 +475,8 @@ function ensureLocalIgnore(root) {
345
475
 
346
476
  module.exports = {
347
477
  CONFIG_FILE,
478
+ PROJECT_CONFIG_FILE,
479
+ PROJECT_CONFIG_VERSION,
348
480
  CURRENT_CONFIG_VERSION,
349
481
  DEFAULT_MONITOR_URL,
350
482
  DEFAULT_WORKSPACE_NAME,
@@ -363,6 +495,7 @@ module.exports = {
363
495
  normalizeConfig,
364
496
  normalizeMonitor,
365
497
  normalizeProjects,
498
+ normalizeProjectReference,
366
499
  normalizeWorkspace,
367
500
  normalizeWorkspaceIdentity,
368
501
  normalizeWorkspaceLanguage,
@@ -370,8 +503,12 @@ module.exports = {
370
503
  readConfigDocument,
371
504
  renderConfig,
372
505
  renderConfigDocument,
506
+ renderProjectConfig,
373
507
  requireWorkspaceRoot,
374
508
  saveConfig,
509
+ saveProjectConfig,
510
+ readProjectConfigDocument,
511
+ projectConfigPath,
375
512
  saveState,
376
513
  statePath,
377
514
  updateProjectBranch,
@@ -152,7 +152,6 @@ function coreWholeFileTargets() {
152
152
  `${LOCAL_DIRECTORY}/${CONFIG_FILE}`,
153
153
  `${LOCAL_DIRECTORY}/${STATE_FILE}`,
154
154
  `${LOCAL_DIRECTORY}/${EXTENSION_STATE_FILE}`,
155
- ".gitignore",
156
155
  ...manifest.managedFiles.map((entry) => entry.target),
157
156
  ...OBSOLETE_ASSETS,
158
157
  ]);
@@ -162,6 +161,7 @@ function coreManagedTargets() {
162
161
  const root = path.parse(PACKAGE_ROOT).root;
163
162
  return new Set([
164
163
  ...coreWholeFileTargets(),
164
+ ".gitignore",
165
165
  ...permissionTargets(root, ["claude", "codex"]).map((target) => path.relative(root, target).split(path.sep).join("/")),
166
166
  ]);
167
167
  }
@@ -8,6 +8,7 @@ const {
8
8
  DEFAULT_MONITOR_URL,
9
9
  DEFAULT_WORKSPACE_NAME,
10
10
  configPath,
11
+ projectConfigPath,
11
12
  ensureLocalIgnore,
12
13
  loadConfig,
13
14
  normalizeConfig,
@@ -213,6 +214,7 @@ async function initializeWorkspace(rootInput, options = {}) {
213
214
  const files = [
214
215
  statePath(root),
215
216
  configPath(root),
217
+ projectConfigPath(root),
216
218
  path.join(root, ".gitignore"),
217
219
  path.join(root, ".codex", "config.toml"),
218
220
  ...permissionTargets(root, options.tools),
@@ -1,4 +1,4 @@
1
- const { configPath, loadConfigProjection, loadState, saveConfig } = require("./config");
1
+ const { configPath, loadConfigProjection, loadState, projectConfigPath, saveProjectConfig } = require("./config");
2
2
  const { WorkspaceError } = require("./errors");
3
3
  const { loadInitManifest } = require("./init");
4
4
  const { applyPermissionPlan, planPermissionChanges } = require("./permissions");
@@ -30,9 +30,9 @@ function applyProjectConfiguration(root, nextConfig, permissionPlan, options = {
30
30
  options = permissionPlan || options;
31
31
  permissionPlan = fallbackPermissionPlan(root, nextConfig, options);
32
32
  }
33
- const transaction = createFileTransaction([configPath(root), ...permissionPlan.targets]);
33
+ const transaction = createFileTransaction([configPath(root), projectConfigPath(root), ...permissionPlan.targets]);
34
34
  try {
35
- (options.saveConfig || saveConfig)(root, nextConfig);
35
+ (options.saveProjectConfig || saveProjectConfig)(root, nextConfig.projects);
36
36
  options.injectFailure?.("after-config-save");
37
37
  const persisted = (options.loadConfigProjection || loadConfigProjection)(root, ["projects"]);
38
38
  if (JSON.stringify(persisted.projects) !== JSON.stringify(nextConfig.projects)) {
@@ -1,64 +0,0 @@
1
- # Built-in Extension Contract
2
-
3
- Code Workspace extensions are trusted, versioned packages shipped under `extensions/<id>/<semver>/`. The extension process is fault-isolated, but it is not a security sandbox: bundled extension code runs with the current user's operating-system permissions.
4
-
5
- Each version contains `manifest.json`, `init.js`, and any private templates, metadata, or helper code it needs. The current Host explicitly supports Extension Spec v1, defined in English at `spec/extension/v1/specification.en-US.md` and in Chinese at `spec/extension/v1/specification.zh-CN.md`, with these component schemas:
6
-
7
- - `schemas/extension-manifest-v3.json`
8
- - `schemas/extension-init-context-v1.json`
9
- - `schemas/extension-init-result-v1.json`
10
-
11
- The static manifest declares its `extensionSpecVersion`, identity, entry hash and timeout, declarative network hosts, and maximum output scope. The Host executes only explicitly supported specification versions; the Code Workspace product version is not an extension compatibility key. Code Workspace freezes the specification version, manifest, entry, and complete extension-version directory digest before confirmation, then verifies them again before execution.
12
-
13
- The Host starts the entry with independent temporary paths:
14
-
15
- ```text
16
- node init.js --context <context-file> --output <staging-directory> --result <result-file>
17
- ```
18
-
19
- The context contains the specification version, extension identity, Workspace display metadata, and selected tools. It does not expose the real Workspace path. The result echoes the same specification version and contains only the extension identity and `{ id, source }` entries. It must return exactly every manifest output applicable to the selected tools; it cannot redefine target, kind, ownership, selector, or digest.
20
-
21
- The Host recursively validates staging, rejects undeclared content, path escapes, symbolic links, and special files, and independently computes installed file and directory digests. Extensions never write the real Workspace or installed state directly and never provide uninstall code.
22
-
23
- ## Generic output kinds
24
-
25
- - `file`: one extension-owned Workspace-relative regular file.
26
- - `directory`: one extension-owned Workspace-relative directory tree containing only regular files and directories.
27
- - `text-block`: a Host-marked fragment in a shared text file. `format: "toml"` validates both the fragment and the complete composed document.
28
- - `json-member`: one JSON value owned at a declared JSON Pointer in a shared object document.
29
-
30
- Exclusive targets cannot overlap core-managed paths or another extension's targets. Shared text blocks can coexist in the same text target. JSON member ownership rejects equal or parent/child selectors. The Host preserves user, core, and other extension content during install, upgrade, rollback, and uninstall.
31
-
32
- Download protocols, archive formats, package managers, Jira, MCP, and individual Agent products are not public output kinds. An extension may use those details privately to prepare a candidate file or directory in staging. Declarative `networkHosts` are shown in the plan for review; they are not operating-system-level egress enforcement.
33
-
34
- The installed manifest is the only installation fact. New records contain the installed-record version, Extension Spec version, extension version, frozen package and manifest digests, generic output ownership, Host-computed digests, and shared contribution data. Idempotency verifies both this state and the real Workspace. Upgrade handles retained, added, replaced, and removed outputs in one per-extension recoverable transaction. Uninstall reads only installed state, so it still works when the bundled package or execution specification is absent. Unknown local changes stop upgrade or uninstall; there is no force mode.
35
-
36
- Previously published installed protocol-v1/v2 records for files, Codex configuration blocks, and Codex Hooks remain readable and uninstallable. They are compatibility state, not new Extension Spec v1 output kinds.
37
-
38
- `init`, `extension install`, and `extension uninstall` share one non-blocking per-Workspace operation lock. A multi-extension install uses one confirmation boundary and an independent transaction per extension; a failure does not stop later extensions, but the overall command reports failure.
39
-
40
- ## Zhuiyi Jira MCP
41
-
42
- Install it with:
43
-
44
- ```bash
45
- code-w extension install zhuiyi-jira-mcp --yes
46
- ```
47
-
48
- The Jira extension privately downloads the pinned Gitee release, verifies its fixed SHA-256, safely extracts it, and validates the npm package name, version, and `dist/index.js` entry inside staging. The archive already contains `dist` and runtime dependencies; initialization never runs `npm install`, `npm ci`, a build, or an archive script.
49
-
50
- After validation, the Host installs only generic outputs:
51
-
52
- ```text
53
- .code-workspace/extensions/zhuiyi-jira-mcp/0.1.0/ # directory
54
- .codex/config.toml # text-block when Codex is selected
55
- .mcp.json#/mcpServers/zhuiyi-jira # json-member when Claude is selected
56
- ```
57
-
58
- The generated configuration contains non-secret defaults only. It never persists a Jira cookie, token, email, or password; provide credentials in the environment that launches the Agent, for example:
59
-
60
- ```bash
61
- export JIRA_COOKIE='JSESSIONID=...; atlassian.xsrf.token=...'
62
- ```
63
-
64
- Downloaded attachments default to `.jira-attachments/` in the Workspace. They are runtime user data, not installation artifacts, and are retained when the extension is upgraded or uninstalled.
@@ -1,64 +0,0 @@
1
- # 内置扩展契约
2
-
3
- Code Workspace 扩展是随发布包提供、位于 `extensions/<id>/<semver>/` 下的可信版本化软件包。扩展进程具有故障隔离,但不是安全沙箱:内置扩展代码仍以当前用户的操作系统权限运行。
4
-
5
- 每个版本包含 `manifest.json`、`init.js`,以及扩展需要的私有模板、元数据或辅助代码。当前 Host 明确支持 Extension Spec v1;中文规范位于 `spec/extension/v1/specification.zh-CN.md`,英文规范位于 `spec/extension/v1/specification.en-US.md`,其组件 schema 为:
6
-
7
- - `schemas/extension-manifest-v3.json`
8
- - `schemas/extension-init-context-v1.json`
9
- - `schemas/extension-init-result-v1.json`
10
-
11
- 静态 manifest 通过 `extensionSpecVersion` 声明扩展实现的开发规范,并声明扩展身份、入口摘要和超时、声明性网络 host,以及最大输出范围。Host 只执行其明确支持的规范版本;Code Workspace 产品版本不参与扩展兼容判断。Code Workspace 在确认前冻结规范版本、manifest、入口和完整扩展版本目录摘要,并在执行前重新验证。
12
-
13
- Host 使用相互独立的临时路径启动入口:
14
-
15
- ```text
16
- node init.js --context <context-file> --output <staging-directory> --result <result-file>
17
- ```
18
-
19
- context 只包含规范版本、扩展身份、Workspace 显示元数据和所选工具,不提供真实 Workspace 路径。result 回显同一规范版本,并只包含扩展身份和 `{ id, source }`。它必须完整返回本次工具选择所适用的全部 manifest 输出,不能重新声明 target、kind、ownership、selector 或摘要。
20
-
21
- Host 递归验证 staging,拒绝未声明内容、路径逃逸、符号链接和特殊文件,并独立计算已安装文件或目录摘要。扩展不直接写入真实 Workspace 或 installed 状态,也不提供卸载脚本。
22
-
23
- ## 通用输出类型
24
-
25
- - `file`:扩展独占的一个 Workspace 相对普通文件。
26
- - `directory`:扩展独占的一个 Workspace 相对目录树,其中只能包含普通文件和目录。
27
- - `text-block`:Host 使用稳定标记管理的共享文本片段;`format: "toml"` 会同时验证片段和合成后的完整文档。
28
- - `json-member`:扩展在共享 JSON 对象文档的声明 JSON Pointer 位置拥有一个值。
29
-
30
- 独占目标不能与核心管理路径或其他扩展目标重叠。多个共享文本块可以共存于同一文本文件;JSON 成员所有权会拒绝相同 selector 及父子 selector。安装、升级、回滚和卸载均保留用户、核心及其他扩展的内容。
31
-
32
- 下载协议、归档格式、包管理器、Jira、MCP 和具体 Agent 产品都不是公共输出类型。扩展可以在私有实现中使用这些知识,在 staging 中准备候选文件或目录。声明的 `networkHosts` 会展示在计划中供用户确认,但不代表操作系统级网络出口强制隔离。
33
-
34
- installed manifest 是唯一安装事实。新记录包含 installed record 版本、Extension Spec 版本、扩展版本、冻结的扩展包和 manifest 摘要、通用输出所有权、Host 计算的摘要及共享 contribution 数据。幂等判断同时验证状态和真实 Workspace。升级在单扩展可恢复事务中处理保留、新增、替换和移除的输出。卸载只读取 installed 状态,因此扩展包或执行规范已经不存在时仍可工作。发现未知本地修改时,升级或卸载会停止;基础版不提供强制模式。
35
-
36
- 已经发布的 installed protocol v1/v2 文件、Codex 配置块和 Codex Hooks 记录仍可读取和卸载;它们只属于兼容状态,不再是 Extension Spec v1 的新输出类型。
37
-
38
- `init`、`extension install` 和 `extension uninstall` 共用同一个非阻塞 Workspace 操作锁。多扩展安装只确认一次,每个扩展使用独立事务;单个失败不会阻止后续扩展,但整体命令会报告失败。
39
-
40
- ## Zhuiyi Jira MCP
41
-
42
- 安装:
43
-
44
- ```bash
45
- code-w extension install zhuiyi-jira-mcp --yes
46
- ```
47
-
48
- Jira 扩展在私有实现中下载固定的 Gitee 发布包,校验固定 SHA-256,安全解压,并在 staging 中验证 npm 包名称、版本和 `dist/index.js` 入口。归档已经包含 `dist` 和运行依赖;初始化不会执行 `npm install`、`npm ci`、构建或归档内脚本。
49
-
50
- 验证完成后,Host 只安装通用输出:
51
-
52
- ```text
53
- .code-workspace/extensions/zhuiyi-jira-mcp/0.1.0/ # directory
54
- .codex/config.toml # 选择 Codex 时的 text-block
55
- .mcp.json#/mcpServers/zhuiyi-jira # 选择 Claude 时的 json-member
56
- ```
57
-
58
- 生成配置只包含非敏感默认值,不会持久化 Jira Cookie、Token、邮箱或密码。用户需要在启动 Agent 的环境中提供凭证,例如:
59
-
60
- ```bash
61
- export JIRA_COOKIE='JSESSIONID=...; atlassian.xsrf.token=...'
62
- ```
63
-
64
- 下载附件默认保存在 Workspace 的 `.jira-attachments/`。它是运行期用户数据,不是安装制品,扩展升级或卸载时都会保留。
@@ -1,16 +0,0 @@
1
- ---
2
- name: code-workspace-openspec-propose
3
- description: Create an OpenSpec change for a selected Code Workspace project after resolving and verifying that project's registered scope.
4
- ---
5
-
6
- # Code Workspace OpenSpec Propose
7
-
8
- Use this skill only when the user explicitly asks to create an OpenSpec proposal for a project registered in Code Workspace.
9
-
10
- 1. Resolve exactly one target project with `code-w project show "<project-name>" --json` and retain its `location`, `branch`, `type`, and `context` as the working boundary.
11
- 2. Verify that target with `code-w project verify "<project-name>" --json`. If branch reconciliation is required, stop and use the Code Workspace branch-resolution workflow before creating a change.
12
- 3. Change the working directory to the verified project location. Do not create the OpenSpec change in the Workspace registry directory.
13
- 4. Follow the installed OpenSpec propose workflow in that project. Derive a kebab-case change name, create the change, and generate every artifact required for apply.
14
- 5. Report the project name, change name, artifact paths, validation status, and whether the change is ready to implement.
15
-
16
- Do not modify `.code-workspace/`, do not broaden the request to other registered projects, and do not implement or archive the change unless the user asks.
@@ -1,16 +0,0 @@
1
- ---
2
- name: code-workspace-openspec-propose
3
- description: Create an OpenSpec change for a selected Code Workspace project after resolving and verifying that project's registered scope.
4
- ---
5
-
6
- # Code Workspace OpenSpec Propose
7
-
8
- Use this skill only when the user explicitly asks to create an OpenSpec proposal for a project registered in Code Workspace.
9
-
10
- 1. Resolve exactly one target project with `code-w project show "<project-name>" --json` and retain its `location`, `branch`, `type`, and `context` as the working boundary.
11
- 2. Verify that target with `code-w project verify "<project-name>" --json`. If branch reconciliation is required, stop and use the Code Workspace branch-resolution workflow before creating a change.
12
- 3. Change the working directory to the verified project location. Do not create the OpenSpec change in the Workspace registry directory.
13
- 4. Follow the installed OpenSpec propose workflow in that project. Derive a kebab-case change name, create the change, and generate every artifact required for apply.
14
- 5. Report the project name, change name, artifact paths, validation status, and whether the change is ready to implement.
15
-
16
- Do not modify `.code-workspace/`, do not broaden the request to other registered projects, and do not implement or archive the change unless the user asks.
@@ -1,54 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- const fs = require("node:fs");
4
- const path = require("node:path");
5
-
6
- function fail(message) {
7
- process.stderr.write(`${message}\n`);
8
- process.exitCode = 1;
9
- }
10
-
11
- function option(name) {
12
- const index = process.argv.indexOf(name);
13
- return index >= 0 ? process.argv[index + 1] : null;
14
- }
15
-
16
- try {
17
- const contextFile = option("--context");
18
- const outputRoot = option("--output");
19
- const resultFile = option("--result");
20
- if (!contextFile || !outputRoot || !resultFile) throw new Error("Usage: init.js --context <file> --output <directory> --result <file>");
21
- const context = JSON.parse(fs.readFileSync(contextFile, "utf8"));
22
- if (context.schemaVersion !== 1 || context.extensionSpecVersion !== 1 || context.extension?.id !== "openspec-workspace" || !Array.isArray(context.tools)) {
23
- throw new Error("Invalid extension context");
24
- }
25
- const definitions = {
26
- codex: {
27
- id: "codex-propose-skill",
28
- source: path.join(__dirname, "artifacts", "codex", "SKILL.md"),
29
- target: ".codex/skills/code-workspace-openspec-propose/SKILL.md",
30
- },
31
- claude: {
32
- id: "claude-propose-skill",
33
- source: path.join(__dirname, "artifacts", "claude", "SKILL.md"),
34
- target: ".claude/skills/code-workspace-openspec-propose/SKILL.md",
35
- },
36
- };
37
- const outputs = [];
38
- for (const tool of context.tools) {
39
- const definition = definitions[tool];
40
- if (!definition) continue;
41
- const target = path.join(outputRoot, ...definition.target.split("/"));
42
- fs.mkdirSync(path.dirname(target), { recursive: true });
43
- fs.copyFileSync(definition.source, target);
44
- outputs.push({ id: definition.id, source: definition.target });
45
- }
46
- fs.writeFileSync(resultFile, `${JSON.stringify({
47
- schemaVersion: 1,
48
- extensionSpecVersion: 1,
49
- extension: { id: "openspec-workspace", version: "1.0.0" },
50
- outputs,
51
- }, null, 2)}\n`, { mode: 0o600 });
52
- } catch (error) {
53
- fail(error.message);
54
- }
@@ -1,27 +0,0 @@
1
- {
2
- "schemaVersion": 3,
3
- "extensionSpecVersion": 1,
4
- "experimental": true,
5
- "id": "openspec-workspace",
6
- "name": "OpenSpec Workspace",
7
- "version": "1.0.0",
8
- "entry": "init.js",
9
- "entrySha256": "5d95dc6261ef686f81a61cc09f0cf1d87bbb5014512be8665df2cc9f9b9dae24",
10
- "timeoutMs": 60000,
11
- "outputs": [
12
- {
13
- "id": "codex-propose-skill",
14
- "kind": "file",
15
- "ownership": "exclusive",
16
- "target": ".codex/skills/code-workspace-openspec-propose/SKILL.md",
17
- "tools": ["codex"]
18
- },
19
- {
20
- "id": "claude-propose-skill",
21
- "kind": "file",
22
- "ownership": "exclusive",
23
- "target": ".claude/skills/code-workspace-openspec-propose/SKILL.md",
24
- "tools": ["claude"]
25
- }
26
- ]
27
- }