create-harness-vibe-coding 0.8.12 → 0.8.13

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-CN.md CHANGED
@@ -191,8 +191,20 @@ node Harness/scripts/validate-harness.mjs
191
191
 
192
192
  # bootstrap 完成后或发布前
193
193
  node Harness/scripts/validate-harness.mjs --strict
194
+
195
+ # 发布更新版本后
196
+ npm run check:mirrors
194
197
  ```
195
198
 
199
+ ## 发布门禁
200
+
201
+ 铁律:每次 Harness 更新发版都必须同时保持两个更新通道可用:
202
+
203
+ - Canonical:npm `create-harness-vibe-coding@latest` 和 `https://github.com/LiWeny16/create-harness-vibe-coding`
204
+ - 低版本兼容镜像:`https://github.com/zingspark/create-harness-vibe-coding`
205
+
206
+ 低版本用户的 updater 可能已经硬编码到旧的 `zingspark` 仓库。发布不能只更新新仓库;必须等 legacy mirror 拥有同一份 commit、tag 和生成后的 template manifest,才算发版完成。新安装生成的 `Harness/.harness-version` 仍然记录 canonical `LiWeny16` source;`zingspark` 只作为旧用户兼容镜像保留。
207
+
196
208
  ## 适配范围与体积
197
209
 
198
210
  - 支持 Claude Code、Codex 和 OpenCode 的共享 Harness 工作流。
package/README.md CHANGED
@@ -189,8 +189,20 @@ node Harness/scripts/validate-harness.mjs
189
189
 
190
190
  # Generated project after bootstrap or before release
191
191
  node Harness/scripts/validate-harness.mjs --strict
192
+
193
+ # After publishing an update release
194
+ npm run check:mirrors
192
195
  ```
193
196
 
197
+ ## Release gate
198
+
199
+ Iron rule: every update release must keep both update channels live:
200
+
201
+ - Canonical: npm `create-harness-vibe-coding@latest` and `https://github.com/LiWeny16/create-harness-vibe-coding`
202
+ - Legacy compatibility mirror: `https://github.com/zingspark/create-harness-vibe-coding`
203
+
204
+ Low-version installs can have updater scripts hardcoded to the legacy mirror. Do not mark a release complete until the legacy mirror exposes the same commit, tag, and generated template manifest as canonical. Generated installs still record the canonical `LiWeny16` source; the `zingspark` repo is kept for backward compatibility.
205
+
194
206
  ## Footprint
195
207
 
196
208
  | | |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-harness-vibe-coding",
3
- "version": "0.8.12",
3
+ "version": "0.8.13",
4
4
  "description": "Scaffold a 0-1 product harness for AI-assisted research, PRD, planning, architecture, build, test, and feedback loops",
5
5
  "type": "module",
6
6
  "bin": {
@@ -18,6 +18,7 @@
18
18
  "scripts": {
19
19
  "start": "node src/index.js",
20
20
  "build:version": "node scripts/build-version.mjs",
21
+ "check:mirrors": "node scripts/check-update-mirrors.mjs",
21
22
  "prepush": "node scripts/pre-push-check.mjs",
22
23
  "test": "node --test tests/*.test.js",
23
24
  "test:e2e": "node tests/e2e-wf-scripts.test.mjs",
package/src/index.js CHANGED
@@ -9,6 +9,7 @@ import { generate, getOptionalCatalog } from './generator.js';
9
9
 
10
10
  const UPDATE_SUCCESS_STATUSES = new Set(['up-to-date', 'update-available', 'partial-update']);
11
11
  const UPDATE_FAILURE_STATUSES = new Set(['error', 'offline', 'template-remote', 'downgrade-refused']);
12
+ const CANONICAL_UPDATE_SOURCE_BASE = 'https://raw.githubusercontent.com/LiWeny16/create-harness-vibe-coding/main/templates/common/';
12
13
 
13
14
  // ── CLI flags ──────────────────────────────────────────────
14
15
  const raw = process.argv.slice(2);
@@ -515,6 +516,7 @@ function runUpdateSwitch(scan, { json }) {
515
516
  console.log(pc.yellow('Existing Harness detected. Switching to wf-update check.'));
516
517
  console.log(pc.dim(`Directory ${scan.resolvedDir}`));
517
518
  console.log(pc.dim('Command node Harness/scripts/wf-update-check.mjs'));
519
+ console.log(pc.dim(`Source ${CANONICAL_UPDATE_SOURCE_BASE}`));
518
520
  console.log('');
519
521
 
520
522
  if (!updateResult.success && updateResult.error) {
@@ -555,12 +557,14 @@ function createUpdateSwitchResult(scan, { json }) {
555
557
  updateCommand: json
556
558
  ? 'node Harness/scripts/wf-update-check.mjs --json'
557
559
  : 'node Harness/scripts/wf-update-check.mjs',
560
+ updateSourceBase: CANONICAL_UPDATE_SOURCE_BASE,
558
561
  next: [
559
562
  {
560
563
  action: 'update',
561
564
  command: json
562
565
  ? 'node Harness/scripts/wf-update-check.mjs --json'
563
566
  : 'node Harness/scripts/wf-update-check.mjs',
567
+ env: { WF_SOURCE_BASE: CANONICAL_UPDATE_SOURCE_BASE },
564
568
  reason: 'Harness already exists, so updates must use the installed Harness update flow.',
565
569
  },
566
570
  ],
@@ -589,6 +593,10 @@ function createUpdateSwitchResult(scan, { json }) {
589
593
  const result = spawnSync(process.execPath, args, {
590
594
  cwd: scan.resolvedDir,
591
595
  encoding: 'utf8',
596
+ env: {
597
+ ...process.env,
598
+ WF_SOURCE_BASE: CANONICAL_UPDATE_SOURCE_BASE,
599
+ },
592
600
  });
593
601
  const stdout = result.stdout || '';
594
602
  const stderr = result.stderr || '';
@@ -4,7 +4,7 @@ Run the Harness update checker script. Do not invoke a skill or start WF mode.
4
4
 
5
5
  ## Flow
6
6
 
7
- 1. Run `node Harness/scripts/wf-update-check.mjs --json` and use the `agent` block as the action plan.
7
+ 1. Run `node Harness/scripts/wf-update-check.mjs --json` and use the `agent` block as the action plan. Current updaters try npm `create-harness-vibe-coding@latest` first, then the canonical GitHub source `LiWeny16/create-harness-vibe-coding`, then the legacy compatibility mirror `zingspark/create-harness-vibe-coding`.
8
8
  2. Preserve all PRESERVE files. Never overwrite user task, memory, research, README, package, or architecture files.
9
9
  3. If `agent.safeApplyCommand` is present, run it to apply SAFE/NEW files first.
10
10
  4. For conflicts, compare local with `templateHint` or `remoteUrl`, decide merge/keep-local/overwrite, and record via `--accept-local`, `--accept-merged`, or `--accept-template`.
@@ -22,3 +22,9 @@ npx create-harness-vibe-coding@latest <project-name> . -y --on-conflict skip
22
22
  ```
23
23
 
24
24
  This creates missing Harness files while preserving CLAUDE.md, README.md, tasks, memory, research, and all user data. After recovery, re-run the update check.
25
+
26
+ If an old updater reports only `0.8.10`, run the latest installer command above or re-run the checker with:
27
+
28
+ ```
29
+ node Harness/scripts/wf-update-check.mjs --json --source-base https://raw.githubusercontent.com/LiWeny16/create-harness-vibe-coding/main/templates/common/
30
+ ```
@@ -23,7 +23,10 @@ This skill is a Codex compatibility shim plus script-flow reference. Claude Code
23
23
  ## Flow
24
24
 
25
25
  1. Run `node Harness/scripts/wf-update-check.mjs --json` first and use the
26
- `agent` block as the action plan.
26
+ `agent` block as the action plan. Current updaters try npm
27
+ `create-harness-vibe-coding@latest` first, then the canonical GitHub source
28
+ `LiWeny16/create-harness-vibe-coding`, then the legacy compatibility mirror
29
+ `zingspark/create-harness-vibe-coding`.
27
30
  2. Preserve all PRESERVE files. Never overwrite user task, memory, research,
28
31
  README, package, or architecture files.
29
32
  3. If `agent.safeApplyCommand` is present, run it to apply SAFE, NEW, and
@@ -50,6 +53,13 @@ npx create-harness-vibe-coding@latest <project-name> . -y --on-conflict skip
50
53
 
51
54
  The `--on-conflict skip` policy preserves all existing user files (CLAUDE.md, README.md, tasks, memory, research, architecture) and only creates missing Harness infrastructure files. After recovery, re-run the update check.
52
55
 
56
+ If an old updater reports only `0.8.10`, run the latest installer command above
57
+ or re-run the checker with:
58
+
59
+ ```
60
+ node Harness/scripts/wf-update-check.mjs --json --source-base https://raw.githubusercontent.com/LiWeny16/create-harness-vibe-coding/main/templates/common/
61
+ ```
62
+
53
63
  ## Return
54
64
 
55
65
  Report version, SAFE/NEW updates, conflicts and decisions, preserved files,
@@ -1,9 +1,9 @@
1
1
  {
2
- "generator": "0.8.12",
3
- "generated": "2026-07-22T08:15:49.785Z",
2
+ "generator": "0.8.13",
3
+ "generated": "2026-07-22T09:32:47.044Z",
4
4
  "options": [],
5
5
  "autoCheck": true,
6
- "source": "https://raw.githubusercontent.com/zingspark/create-harness-vibe-coding/main/templates/common/",
6
+ "source": "https://raw.githubusercontent.com/LiWeny16/create-harness-vibe-coding/main/templates/common/",
7
7
  "checksums": {
8
8
  ".agents/skills/subagent-orchestrator/SKILL.md": "sha256-8d9e8c0db4647bcf168a5f99c8b2ea97d154bfca30e78ffd1cc5a98ce185bb1d",
9
9
  ".agents/skills/tdd/SKILL.md": "sha256-1e2ec0b54a5cf5b071800b0f2e5672a128a42471e16f70f00bb4c0a07a3617cb",
@@ -14,7 +14,7 @@
14
14
  ".agents/skills/wf-readme/SKILL.md": "sha256-4d34ee3b6b305fa2581f5d8cb31e9152759f3df8bf8fa08efbb43782f128bdcd",
15
15
  ".agents/skills/wf-remove/SKILL.md": "sha256-21ebef1a1fa8645d094c8ceeca78b33f036b6c42cda6db8be425d38d9e91af75",
16
16
  ".agents/skills/wf-review/SKILL.md": "sha256-ab1122cbc9612ba734f98736e5479d7777206293d011d5471aaa34d1da35dae6",
17
- ".agents/skills/wf-update/SKILL.md": "sha256-6fa16fd5a526969837b9fd5fcae6aed55eec5ccd755e0a9cddcb7065878f61d8",
17
+ ".agents/skills/wf-update/SKILL.md": "sha256-a0942270455063911bfc603fe6e5fad3e0247e845fc8095e5b4a54fc883e1fba",
18
18
  ".agents/skills/wf/SKILL.md": "sha256-53225645b593f4cf060781590f9b3025cf17315baad4e2acce850cc4cd972f97",
19
19
  ".claude/agents/architect-manager.md": "sha256-d98c3a2d044d5216d9bcde217b4359b1c33f5be152416e242807652ddcd2868b",
20
20
  ".claude/agents/architect.md": "sha256-61cea40c8c3e01bd53f3ad642ef734850fdb931497e7ee20552ea7a8b4e7b70c",
@@ -36,7 +36,7 @@
36
36
  ".claude/agents/test-writer.md": "sha256-724866d627f193caaf060ec6af97b1d54c2df2d5c5f937e78fc3c53c7f798b16",
37
37
  ".claude/agents/verifier.md": "sha256-d501a505b97e679daa89ee1d910ee185edc42ba57d58203e962b1c5a844c0872",
38
38
  ".claude/commands/wf-help.md": "sha256-aaaa6834c6cb61d2ecbeb41ad8fcf77de10ac1638b2009a3822284c01cf41fac",
39
- ".claude/commands/wf-update.md": "sha256-1988e82eb60311b8208367e3c8e22e6222ae05f4b0f645fa9a0e426790705c12",
39
+ ".claude/commands/wf-update.md": "sha256-5a92a2b32952b6dac6b9ef318c63bc049d68c9dbf2623370a116c4fe602392dd",
40
40
  ".claude/rules/ecc/common.md": "sha256-e5f046d02ec9f5d326088f608c63758632d9a5f6c67688b20e065b6380884992",
41
41
  ".claude/settings.json": "sha256-99e8552584b8e71e3e11b4b760c3bfbf8c1b18dadda7ec3f6e3c46f88d9ea513",
42
42
  ".claude/skills/subagent-orchestrator/SKILL.md": "sha256-8d9e8c0db4647bcf168a5f99c8b2ea97d154bfca30e78ffd1cc5a98ce185bb1d",
@@ -48,7 +48,7 @@
48
48
  ".claude/skills/wf-readme/SKILL.md": "sha256-4d34ee3b6b305fa2581f5d8cb31e9152759f3df8bf8fa08efbb43782f128bdcd",
49
49
  ".claude/skills/wf-remove/SKILL.md": "sha256-21ebef1a1fa8645d094c8ceeca78b33f036b6c42cda6db8be425d38d9e91af75",
50
50
  ".claude/skills/wf-review/SKILL.md": "sha256-ab1122cbc9612ba734f98736e5479d7777206293d011d5471aaa34d1da35dae6",
51
- ".claude/skills/wf-update/SKILL.md": "sha256-6fa16fd5a526969837b9fd5fcae6aed55eec5ccd755e0a9cddcb7065878f61d8",
51
+ ".claude/skills/wf-update/SKILL.md": "sha256-a0942270455063911bfc603fe6e5fad3e0247e845fc8095e5b4a54fc883e1fba",
52
52
  ".claude/skills/wf/SKILL.md": "sha256-53225645b593f4cf060781590f9b3025cf17315baad4e2acce850cc4cd972f97",
53
53
  ".codex/config.toml": "sha256-e0af046982c6dbc535c52b19610d16ce9aba08a17dd29f11b74cc8dad8371145",
54
54
  ".codex/hooks.json": "sha256-3200c42c2da434c07cd6ab1ce86f8845ab46a1ed9a25b65117b2b2b42056c725",
@@ -79,7 +79,7 @@
79
79
  ".opencode/commands/wf-readme.md": "sha256-276e80255bd38f51e17b158650d182401304bf06388407d259f47850306556e8",
80
80
  ".opencode/commands/wf-remove.md": "sha256-fe9823152c4f74e66b151e352de7167436d1bc47376c2a688d190391931d300a",
81
81
  ".opencode/commands/wf-review.md": "sha256-4bd59f66bc96c2773868e63c37cbacf39dd521555e997c0b60d50538ef2d5dbd",
82
- ".opencode/commands/wf-update.md": "sha256-1988e82eb60311b8208367e3c8e22e6222ae05f4b0f645fa9a0e426790705c12",
82
+ ".opencode/commands/wf-update.md": "sha256-5a92a2b32952b6dac6b9ef318c63bc049d68c9dbf2623370a116c4fe602392dd",
83
83
  ".opencode/commands/wf.md": "sha256-d86cf20a78c37757f76aed7e252475fb82e7597cd55c6a4c916441b3b75fe9af",
84
84
  ".opencode/plugins/harness-wf-status.mjs": "sha256-ee0987cec5530c34784c619a803562d7085ae26fe5d199fde7ce42c2bed4724e",
85
85
  "AGENTS.md": "sha256-fad176931742a5080e073d1ddba99a3eabb67e8c42bebb132df99a51ca6a184d",
@@ -109,11 +109,11 @@
109
109
  "Harness/lifecycle.md": "sha256-a2d025e52f3b4412d8439083aca0d2cf7a4458ad0e30de4fc6465f990379190f",
110
110
  "Harness/research/README.md": "sha256-956c154df01cb9376972eb8b88bb3be0fe4e2dda802a72457c0126e3476fa755",
111
111
  "Harness/scripts/archive-tasks.mjs": "sha256-07bba20c2f4b3baae737b8b9a951125a48e37dd482a1f1ca122b9cf88e90e956",
112
- "Harness/scripts/scan-clean.mjs": "sha256-1278900563545d67d1d4057d66a03a95f5e9aede0d33ac5d6d0837a72a38d4b0",
112
+ "Harness/scripts/scan-clean.mjs": "sha256-2b414738317cfe41432926b72691edb55ce313ff2427fd1312c1947bf687da61",
113
113
  "Harness/scripts/validate-harness.mjs": "sha256-a6174f918453943387df8015664f81599ea74a179bec351d22187ec0a19a1e7c",
114
114
  "Harness/scripts/wf-auto-update-prompt.mjs": "sha256-14210304dbd8e648b8939fc4c5d3664f943052153e689264c16b05b7b81bd39e",
115
115
  "Harness/scripts/wf-remove.mjs": "sha256-576bc92325bf63d4142eb6028d095f12abf9d845a1a178dfe247b813794476a9",
116
- "Harness/scripts/wf-update-check.mjs": "sha256-8d883930676126f4308ecdfedd3fccb7c62e170c8086723ed2c10d44e580a7e1",
116
+ "Harness/scripts/wf-update-check.mjs": "sha256-40044032d6b4a3307e6dc5ea978d18c010936342d41f3324b3a2b92abe6646c4",
117
117
  "Harness/subagents.md": "sha256-f2e81c21792b4d70c64c9bd22e9548d783d945dddb3504b4937e04ca1d04320d",
118
118
  "Harness/templates/ACCEPTANCE.template.md": "sha256-394a6c08b155b1d121f1d51da774f7988bdb891224c6d5f2ca2c131e8c95ffb3",
119
119
  "Harness/templates/API_CONTRACT.template.md": "sha256-e11ec9866576624982f35e77359e3f4ef3aea3e3f6b5895f6cc5419d29699fc5",
@@ -4,7 +4,7 @@ Run the Harness update checker script. Do not invoke a skill or start WF mode.
4
4
 
5
5
  ## Flow
6
6
 
7
- 1. Run `node Harness/scripts/wf-update-check.mjs --json` and use the `agent` block as the action plan.
7
+ 1. Run `node Harness/scripts/wf-update-check.mjs --json` and use the `agent` block as the action plan. Current updaters try npm `create-harness-vibe-coding@latest` first, then the canonical GitHub source `LiWeny16/create-harness-vibe-coding`, then the legacy compatibility mirror `zingspark/create-harness-vibe-coding`.
8
8
  2. Preserve all PRESERVE files. Never overwrite user task, memory, research, README, package, or architecture files.
9
9
  3. If `agent.safeApplyCommand` is present, run it to apply SAFE/NEW files first.
10
10
  4. For conflicts, compare local with `templateHint` or `remoteUrl`, decide merge/keep-local/overwrite, and record via `--accept-local`, `--accept-merged`, or `--accept-template`.
@@ -22,3 +22,9 @@ npx create-harness-vibe-coding@latest <project-name> . -y --on-conflict skip
22
22
  ```
23
23
 
24
24
  This creates missing Harness files while preserving CLAUDE.md, README.md, tasks, memory, research, and all user data. After recovery, re-run the update check.
25
+
26
+ If an old updater reports only `0.8.10`, run the latest installer command above or re-run the checker with:
27
+
28
+ ```
29
+ node Harness/scripts/wf-update-check.mjs --json --source-base https://raw.githubusercontent.com/LiWeny16/create-harness-vibe-coding/main/templates/common/
30
+ ```
@@ -19,11 +19,16 @@ import { readFileSync, writeFileSync, existsSync, lstatSync, readdirSync, unlink
19
19
  import { resolve, dirname, sep, join } from 'path';
20
20
  import { fileURLToPath } from 'url';
21
21
  import { createInterface } from 'readline';
22
+ import { gunzipSync } from 'zlib';
22
23
 
23
24
  const __dirname = dirname(fileURLToPath(import.meta.url));
24
25
  const ROOT = process.env.WF_ROOT ? resolve(process.env.WF_ROOT) : resolve(__dirname, '..', '..');
25
26
  const VERSION_FILE = resolve(ROOT, 'Harness', '.harness-version');
27
+ const NPM_PACKAGE = 'create-harness-vibe-coding';
28
+ const NPM_REGISTRY = (process.env.NPM_CONFIG_REGISTRY || 'https://registry.npmjs.org').replace(/\/+$/, '');
29
+ const TEMPLATE_SUBPATH = 'templates/common/';
26
30
  const DEFAULT_SOURCE_BASE = 'https://raw.githubusercontent.com/LiWeny16/create-harness-vibe-coding/main/templates/common/';
31
+ const LEGACY_SOURCE_BASE = 'https://raw.githubusercontent.com/zingspark/create-harness-vibe-coding/main/templates/common/';
27
32
 
28
33
  // ── Classification constants ────────────────────────────────────────
29
34
 
@@ -151,6 +156,73 @@ async function fetchRemote(url, timeoutMs = 30000) {
151
156
  }
152
157
  }
153
158
 
159
+ async function fetchBytes(url, timeoutMs = 30000) {
160
+ const controller = new AbortController();
161
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
162
+ try {
163
+ const res = await fetch(url, { signal: controller.signal, headers: { 'User-Agent': 'harness-scan-clean' } });
164
+ if (!res.ok) throw new Error(`HTTP ${res.status}: ${url}`);
165
+ return Buffer.from(await res.arrayBuffer());
166
+ } finally {
167
+ clearTimeout(timer);
168
+ }
169
+ }
170
+
171
+ function tarString(header, start, length) {
172
+ const slice = header.subarray(start, start + length);
173
+ const zero = slice.indexOf(0);
174
+ return Buffer.from(zero === -1 ? slice : slice.subarray(0, zero)).toString('utf8').trim();
175
+ }
176
+
177
+ function parseTarFiles(buffer) {
178
+ const files = new Map();
179
+ for (let offset = 0; offset + 512 <= buffer.length;) {
180
+ const header = buffer.subarray(offset, offset + 512);
181
+ if (header.every(byte => byte === 0)) break;
182
+
183
+ const name = tarString(header, 0, 100);
184
+ const prefix = tarString(header, 345, 155);
185
+ const size = Number.parseInt(tarString(header, 124, 12) || '0', 8) || 0;
186
+ const type = String.fromCharCode(header[156] || 0);
187
+ const fullName = prefix ? `${prefix}/${name}` : name;
188
+ const dataStart = offset + 512;
189
+
190
+ if ((type === '\0' || type === '0' || type === '') && fullName) {
191
+ files.set(fullName, buffer.subarray(dataStart, dataStart + size));
192
+ }
193
+
194
+ offset = dataStart + Math.ceil(size / 512) * 512;
195
+ }
196
+ return files;
197
+ }
198
+
199
+ async function fetchNpmLatestManifest() {
200
+ const meta = JSON.parse(await fetchRemote(`${NPM_REGISTRY}/${NPM_PACKAGE}/latest`, 15000));
201
+ const tarball = meta?.dist?.tarball;
202
+ if (!tarball) throw new Error(`npm metadata missing dist.tarball for ${NPM_PACKAGE}@latest`);
203
+ const files = parseTarFiles(gunzipSync(await fetchBytes(tarball, 30000)));
204
+ const manifest = files.get(`package/${TEMPLATE_SUBPATH}.harness-version`);
205
+ if (!manifest) throw new Error('npm package missing templates/common/.harness-version');
206
+ return Buffer.from(manifest).toString('utf8');
207
+ }
208
+
209
+ async function fetchRemoteManifest(explicitSource) {
210
+ if (explicitSource) return fetchRemote(normalizeSourceBase(explicitSource) + '.harness-version');
211
+ try {
212
+ return await fetchNpmLatestManifest();
213
+ } catch (npmError) {
214
+ try {
215
+ return await fetchRemote(DEFAULT_SOURCE_BASE + '.harness-version');
216
+ } catch (canonicalError) {
217
+ try {
218
+ return await fetchRemote(LEGACY_SOURCE_BASE + '.harness-version');
219
+ } catch (legacyError) {
220
+ throw new Error(`npm latest: ${npmError.message}; canonical GitHub: ${canonicalError.message}; legacy mirror: ${legacyError.message}`);
221
+ }
222
+ }
223
+ }
224
+ }
225
+
154
226
  // ── Orphan detection ─────────────────────────────────────────────────
155
227
 
156
228
  /**
@@ -285,7 +357,7 @@ async function main() {
285
357
  const clean = args.includes('--clean');
286
358
  const yes = args.includes('--yes');
287
359
  const jsonOut = args.includes('--json');
288
- const sourceBase = normalizeSourceBase(readFlagValue(args, '--source-base') || process.env.WF_SOURCE_BASE || DEFAULT_SOURCE_BASE);
360
+ const explicitSource = readFlagValue(args, '--source-base') || process.env.WF_SOURCE_BASE;
289
361
 
290
362
  // 1. Read local state
291
363
  if (!existsSync(VERSION_FILE)) {
@@ -314,7 +386,7 @@ async function main() {
314
386
  // 2. Fetch remote version file
315
387
  let remoteVersion;
316
388
  try {
317
- const raw = await fetchRemote(sourceBase + '.harness-version');
389
+ const raw = await fetchRemoteManifest(explicitSource);
318
390
  if (isTemplate(raw)) {
319
391
  if (jsonOut) {
320
392
  console.log(JSON.stringify({ status: 'error', message: 'Remote .harness-version is a template (contains {{placeholders}}). Cannot determine dead files.' }));
@@ -328,9 +400,9 @@ async function main() {
328
400
  remoteVersion = JSON.parse(raw);
329
401
  } catch (e) {
330
402
  if (jsonOut) {
331
- console.log(JSON.stringify({ status: 'error', message: 'Cannot reach GitHub or invalid remote JSON: ' + e.message }));
403
+ console.log(JSON.stringify({ status: 'error', message: 'Cannot reach npm, canonical GitHub, or legacy mirror update sources: ' + e.message }));
332
404
  } else {
333
- console.error('ERROR: Cannot reach GitHub or invalid remote JSON. Offline?');
405
+ console.error('ERROR: Cannot reach npm, canonical GitHub, or legacy mirror update sources. Offline?');
334
406
  console.error(e.message);
335
407
  }
336
408
  process.exit(1);
@@ -19,15 +19,22 @@ import { readFileSync, writeFileSync, existsSync, mkdirSync, lstatSync } from 'f
19
19
  import { createHash } from 'crypto';
20
20
  import { resolve, dirname, sep } from 'path';
21
21
  import { fileURLToPath } from 'url';
22
+ import { gunzipSync } from 'zlib';
22
23
 
23
24
  const __dirname = dirname(fileURLToPath(import.meta.url));
24
25
  const ROOT = process.env.WF_ROOT ? resolve(process.env.WF_ROOT) : resolve(__dirname, '..', '..');
25
26
  const VERSION_FILE = resolve(ROOT, 'Harness', '.harness-version');
27
+ const NPM_PACKAGE = 'create-harness-vibe-coding';
28
+ const NPM_REGISTRY = (process.env.NPM_CONFIG_REGISTRY || 'https://registry.npmjs.org').replace(/\/+$/, '');
26
29
  const GITHUB_REPO = 'LiWeny16/create-harness-vibe-coding';
30
+ const LEGACY_GITHUB_REPO = 'zingspark/create-harness-vibe-coding';
27
31
  const GITHUB_LATEST_STABLE = `https://api.github.com/repos/${GITHUB_REPO}/releases/latest`;
28
32
  const RAW_GITHUB = `https://raw.githubusercontent.com/${GITHUB_REPO}`;
33
+ const LEGACY_RAW_GITHUB = `https://raw.githubusercontent.com/${LEGACY_GITHUB_REPO}`;
29
34
  const TEMPLATE_SUBPATH = 'templates/common/';
30
35
  const DEFAULT_SOURCE_BASE = `${RAW_GITHUB}/main/${TEMPLATE_SUBPATH}`;
36
+ const LEGACY_SOURCE_BASE = `${LEGACY_RAW_GITHUB}/main/${TEMPLATE_SUBPATH}`;
37
+ const npmPackageCache = new Map();
31
38
 
32
39
  // Tier classification
33
40
 
@@ -185,6 +192,47 @@ async function fetchRemote(url, timeoutMs = 30000) {
185
192
  }
186
193
  }
187
194
 
195
+ async function fetchBytes(url, timeoutMs = 30000) {
196
+ const controller = new AbortController();
197
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
198
+ try {
199
+ const res = await fetch(url, { signal: controller.signal, headers: { 'User-Agent': 'harness-wf-update-check' } });
200
+ if (!res.ok) throw new Error(`HTTP ${res.status}: ${url}`);
201
+ return Buffer.from(await res.arrayBuffer());
202
+ } finally {
203
+ clearTimeout(timer);
204
+ }
205
+ }
206
+
207
+ function tarString(header, start, length) {
208
+ const slice = header.subarray(start, start + length);
209
+ const zero = slice.indexOf(0);
210
+ return Buffer.from(zero === -1 ? slice : slice.subarray(0, zero)).toString('utf8').trim();
211
+ }
212
+
213
+ function parseTarFiles(buffer) {
214
+ const files = new Map();
215
+ for (let offset = 0; offset + 512 <= buffer.length;) {
216
+ const header = buffer.subarray(offset, offset + 512);
217
+ if (header.every(byte => byte === 0)) break;
218
+
219
+ const name = tarString(header, 0, 100);
220
+ const prefix = tarString(header, 345, 155);
221
+ const sizeText = tarString(header, 124, 12);
222
+ const size = Number.parseInt(sizeText || '0', 8) || 0;
223
+ const type = String.fromCharCode(header[156] || 0);
224
+ const fullName = prefix ? `${prefix}/${name}` : name;
225
+ const dataStart = offset + 512;
226
+
227
+ if ((type === '\0' || type === '0' || type === '') && fullName) {
228
+ files.set(fullName, buffer.subarray(dataStart, dataStart + size));
229
+ }
230
+
231
+ offset = dataStart + Math.ceil(size / 512) * 512;
232
+ }
233
+ return files;
234
+ }
235
+
188
236
  // Main
189
237
 
190
238
  function isPrerelease(v) {
@@ -192,7 +240,74 @@ function isPrerelease(v) {
192
240
  return /-[0-9A-Za-z.-]+/.test(v.replace(/^[^0-9]*/, ''));
193
241
  }
194
242
 
195
- async function resolveStableSourceBase() {
243
+ function parseSemver(v) {
244
+ if (!v || typeof v !== 'string') return [0, 0, 0];
245
+ return v.replace(/^[^0-9]*/, '').split('.').slice(0, 3).map(n => Number(n) || 0);
246
+ }
247
+
248
+ function cmpSemver(a, b) {
249
+ const va = parseSemver(a), vb = parseSemver(b);
250
+ for (let i = 0; i < 3; i++) {
251
+ if ((va[i] || 0) > (vb[i] || 0)) return 1;
252
+ if ((va[i] || 0) < (vb[i] || 0)) return -1;
253
+ }
254
+ return 0;
255
+ }
256
+
257
+ async function loadNpmPackage(version = 'latest') {
258
+ const cacheKey = `${NPM_REGISTRY}:${version}`;
259
+ if (npmPackageCache.has(cacheKey)) return npmPackageCache.get(cacheKey);
260
+
261
+ const metaUrl = `${NPM_REGISTRY}/${NPM_PACKAGE}/${version}`;
262
+ const meta = JSON.parse(await fetchRemote(metaUrl, 15000));
263
+ const packageVersion = meta.version;
264
+ const tarball = meta?.dist?.tarball;
265
+ if (!packageVersion || !tarball) throw new Error(`npm metadata missing dist.tarball for ${NPM_PACKAGE}@${version}`);
266
+
267
+ const archive = gunzipSync(await fetchBytes(tarball, 30000));
268
+ const files = parseTarFiles(archive);
269
+ const loaded = { version: packageVersion, tarball, files };
270
+ npmPackageCache.set(cacheKey, loaded);
271
+ npmPackageCache.set(`${NPM_REGISTRY}:${packageVersion}`, loaded);
272
+ return loaded;
273
+ }
274
+
275
+ function readNpmFile(pkg, templateRelPath) {
276
+ const key = `package/${TEMPLATE_SUBPATH}${templateRelPath}`;
277
+ const content = pkg.files.get(key);
278
+ if (!content) throw new Error(`npm package missing ${key}`);
279
+ return Buffer.from(content).toString('utf8');
280
+ }
281
+
282
+ async function createNpmSource(version = 'latest') {
283
+ const pkg = await loadNpmPackage(version);
284
+ const raw = readNpmFile(pkg, '.harness-version');
285
+ if (isTemplate(raw)) throw new Error('npm .harness-version has not been generated yet');
286
+ const manifest = JSON.parse(raw);
287
+ return {
288
+ kind: 'npm',
289
+ stable: true,
290
+ label: `npm:${NPM_PACKAGE}@${pkg.version}/${TEMPLATE_SUBPATH}`,
291
+ version: pkg.version,
292
+ manifest,
293
+ package: pkg,
294
+ };
295
+ }
296
+
297
+ async function createUrlSource(sourceBase, { stable = false } = {}) {
298
+ const base = normalizeSourceBase(sourceBase);
299
+ const raw = await fetchRemote(base + '.harness-version');
300
+ if (isTemplate(raw)) throw new Error('remote .harness-version has not been generated yet');
301
+ return {
302
+ kind: 'url',
303
+ stable,
304
+ label: base,
305
+ base,
306
+ manifest: JSON.parse(raw),
307
+ };
308
+ }
309
+
310
+ async function resolveGithubReleaseSource() {
196
311
  try {
197
312
  const controller = new AbortController();
198
313
  const timer = setTimeout(() => controller.abort(), 15000);
@@ -209,26 +324,45 @@ async function resolveStableSourceBase() {
209
324
  const data = JSON.parse(await res.text());
210
325
  const tag = data && data.tag_name;
211
326
  if (!tag || data.prerelease) return null;
212
- const source = `${RAW_GITHUB}/${tag}/${TEMPLATE_SUBPATH}`;
213
- const versionController = new AbortController();
214
- const versionTimer = setTimeout(() => versionController.abort(), 15000);
215
- try {
216
- const versionRes = await fetch(source + '.harness-version', {
217
- signal: versionController.signal,
218
- headers: { 'User-Agent': 'harness-wf-update-check' },
219
- });
220
- if (!versionRes.ok) return null;
221
- const raw = await versionRes.text();
222
- if (isTemplate(raw)) return null;
223
- } finally {
224
- clearTimeout(versionTimer);
225
- }
226
- return source;
327
+ return await createUrlSource(`${RAW_GITHUB}/${tag}/${TEMPLATE_SUBPATH}`, { stable: true });
227
328
  } catch {
228
329
  return null;
229
330
  }
230
331
  }
231
332
 
333
+ async function resolveUpdateSource(explicitSource) {
334
+ if (explicitSource) {
335
+ return createUrlSource(explicitSource, { stable: false });
336
+ }
337
+
338
+ const errors = [];
339
+ try {
340
+ return await createNpmSource('latest');
341
+ } catch (e) {
342
+ errors.push(`npm latest: ${e.message}`);
343
+ }
344
+
345
+ const releaseSource = await resolveGithubReleaseSource();
346
+ if (releaseSource) return releaseSource;
347
+ errors.push('GitHub latest release: unavailable');
348
+
349
+ try {
350
+ return await createUrlSource(DEFAULT_SOURCE_BASE, { stable: false });
351
+ } catch (e) {
352
+ errors.push(`GitHub canonical main: ${e.message}`);
353
+ }
354
+
355
+ try {
356
+ return await createUrlSource(LEGACY_SOURCE_BASE, { stable: false });
357
+ } catch (e) {
358
+ errors.push(`GitHub legacy mirror: ${e.message}`);
359
+ }
360
+
361
+ const err = new Error('Cannot reach npm, canonical GitHub, or legacy mirror update sources.');
362
+ err.sourceErrors = errors;
363
+ throw err;
364
+ }
365
+
232
366
  async function main() {
233
367
  const args = process.argv.slice(2);
234
368
  const apply = args.includes('--apply');
@@ -240,14 +374,16 @@ async function main() {
240
374
  const acceptTemplate = readRepeatedFlagValues(args, '--accept-template');
241
375
  const ignoreVersion = args.includes('--ignore-version') || args.includes('--force-check');
242
376
  const explicitSource = readFlagValue(args, '--source-base') || process.env.WF_SOURCE_BASE;
243
- let sourceBase = normalizeSourceBase(explicitSource || DEFAULT_SOURCE_BASE);
244
- let stableTagResolved = false;
245
- if (!explicitSource) {
246
- const resolved = await resolveStableSourceBase();
247
- if (resolved) {
248
- sourceBase = normalizeSourceBase(resolved);
249
- stableTagResolved = true;
250
- }
377
+ let source;
378
+ let sourceBase;
379
+ let sourceStable = false;
380
+ let sourceErrors = [];
381
+ try {
382
+ source = await resolveUpdateSource(explicitSource);
383
+ sourceBase = source.label;
384
+ sourceStable = Boolean(source.stable);
385
+ } catch (e) {
386
+ sourceErrors = e.sourceErrors || [e.message];
251
387
  }
252
388
 
253
389
  // 1. Read local state
@@ -302,44 +438,23 @@ async function main() {
302
438
 
303
439
  const localChecksums = localVersion.checksums || {};
304
440
 
305
- // 2. Fetch remote version file
306
- let remoteVersion;
307
- try {
308
- const raw = await fetchRemote(sourceBase + '.harness-version');
309
- if (isTemplate(raw)) {
310
- if (jsonOut) {
311
- console.log(JSON.stringify({ status: 'template-remote', message: 'Remote .harness-version has not been generated yet.' }));
312
- } else {
313
- console.log('WARN: Remote .harness-version is a template (contains {{placeholders}}).');
314
- console.log(' The generate step has not been run on the remote repo. No update possible.');
315
- console.log(' This is expected during development; the update mechanism works once the remote is live.');
316
- }
317
- process.exitCode = 1;
318
- return;
319
- }
320
- remoteVersion = JSON.parse(raw);
321
- } catch (e) {
441
+ // 2. Read remote version metadata from the selected update source.
442
+ let remoteVersion = source?.manifest;
443
+ if (!remoteVersion) {
322
444
  if (jsonOut) {
323
- console.log(JSON.stringify({ status: 'offline', message: 'Cannot reach GitHub.' }));
445
+ console.log(JSON.stringify({
446
+ status: 'offline',
447
+ message: 'Cannot reach npm, canonical GitHub, or legacy mirror update sources.',
448
+ sourceErrors,
449
+ }));
324
450
  } else {
325
- console.error('ERROR: Cannot reach GitHub or invalid JSON. Offline?');
326
- console.error(e.message);
451
+ console.error('ERROR: Cannot reach npm, canonical GitHub, or legacy mirror update sources. Offline?');
452
+ for (const err of sourceErrors) console.error(` ${err}`);
327
453
  }
328
454
  process.exitCode = 1;
329
455
  return;
330
456
  }
331
457
 
332
- // Compare versions and prevent downgrades from explicit custom sources.
333
- function parseSemver(v) {
334
- if (!v || typeof v !== 'string') return [0, 0, 0];
335
- return v.replace(/^[^0-9]*/, '').split('.').slice(0, 3).map(n => Number(n) || 0);
336
- }
337
- function cmpSemver(a, b) {
338
- const va = parseSemver(a), vb = parseSemver(b);
339
- for (let i = 0; i < 3; i++) { if ((va[i]||0) > (vb[i]||0)) return 1; if ((va[i]||0) < (vb[i]||0)) return -1; }
340
- return 0;
341
- }
342
-
343
458
  const localGen = localVersion.generator || '0.0.0';
344
459
  const remoteGen = remoteVersion.generator || '0.0.0';
345
460
 
@@ -355,7 +470,7 @@ async function main() {
355
470
  const versionCmp = cmpSemver(remoteGen, localGen);
356
471
 
357
472
  if (!ignoreVersion && versionCmp <= 0) {
358
- const reportDowngrade = !stableTagResolved;
473
+ const reportDowngrade = !sourceStable;
359
474
  if (jsonOut) {
360
475
  console.log(JSON.stringify({
361
476
  status: (versionCmp < 0 && reportDowngrade) ? 'downgrade-refused' : 'up-to-date',
@@ -385,13 +500,23 @@ async function main() {
385
500
  return remoteSources[file] || file;
386
501
  }
387
502
 
503
+ async function fetchSourceFile(templateRelPath) {
504
+ if (source.kind === 'npm') return readNpmFile(source.package, templateRelPath);
505
+ return fetchRemote(source.base + templateRelPath);
506
+ }
507
+
508
+ function sourceUrl(templateRelPath) {
509
+ if (source.kind === 'npm') return `${source.label}${templateRelPath}`;
510
+ return source.base + templateRelPath;
511
+ }
512
+
388
513
  function withRemoteMeta(entry) {
389
514
  if (!entry || !entry.file || !remoteChecksums[entry.file]) return entry;
390
515
  const templateHint = remotePath(entry.file);
391
516
  return {
392
517
  ...entry,
393
518
  templateHint,
394
- remoteUrl: sourceBase + templateHint,
519
+ remoteUrl: sourceUrl(templateHint),
395
520
  };
396
521
  }
397
522
 
@@ -459,7 +584,7 @@ async function main() {
459
584
  const dest = safePath(file);
460
585
  if (!dest) throw new Error(`Traversal rejected: ${file}`);
461
586
  if (existsSync(dest) && lstatSync(dest).isSymbolicLink()) throw new Error(`Symlink rejected: ${file}`);
462
- const content = await fetchRemote(sourceBase + remotePath(file));
587
+ const content = await fetchSourceFile(remotePath(file));
463
588
  const normalized = content.replace(/\r\n/g, '\n');
464
589
  const fetchedHash = sha256(normalized);
465
590
  if (fetchedHash !== remoteHash) throw new Error(`Hash mismatch: ${file}`);
@@ -678,56 +803,62 @@ async function main() {
678
803
  console.log(`Applying SAFE/NEW files only; ${plan.conflict.length} conflicts will remain for AI/user merge.`);
679
804
  }
680
805
 
681
- const lexists = existsSync;
682
- let applied = 0;
806
+ const preparedWrites = [];
683
807
  let failed = 0;
684
808
 
685
- for (const u of plan.updated) {
809
+ async function prepareWrite(entry, { mustNotExist = false } = {}) {
686
810
  try {
687
- const dest = safePath(u.file);
688
- if (!dest) { console.error(` x Traversal rejected: ${u.file}`); failed++; continue; }
811
+ const dest = safePath(entry.file);
812
+ if (!dest) { console.error(` x Traversal rejected: ${entry.file}`); failed++; return; }
689
813
  // Symlink rejection: do not follow symlinks.
690
- if (lexists(dest)) {
691
- try { if (lstatSync(dest).isSymbolicLink()) { console.error(` x Symlink rejected: ${u.file}`); failed++; continue; } } catch (_) {}
814
+ if (existsSync(dest)) {
815
+ try { if (lstatSync(dest).isSymbolicLink()) { console.error(` x Symlink rejected: ${entry.file}`); failed++; return; } } catch (_) {}
816
+ if (mustNotExist) {
817
+ console.error(` x File created since plan: ${entry.file} - treating as CONFLICT`);
818
+ failed++; return;
819
+ }
692
820
  }
693
- const content = await fetchRemote(sourceBase + remotePath(u.file));
821
+ const content = await fetchSourceFile(remotePath(entry.file));
694
822
  const normalized = content.replace(/\r\n/g, '\n');
695
823
  const fetchedHash = sha256(normalized);
696
- if (fetchedHash !== u.remoteHash) {
697
- console.error(` x Hash mismatch: ${u.file}`);
698
- failed++; continue;
824
+ if (fetchedHash !== entry.remoteHash) {
825
+ console.error(` x Hash mismatch: ${entry.file}`);
826
+ failed++; return;
699
827
  }
700
- mkdirSync(dirname(dest), { recursive: true });
701
- writeFileSync(dest, normalized, 'utf-8');
702
- applied++;
828
+ preparedWrites.push({ file: entry.file, dest, content: normalized, mustNotExist });
703
829
  } catch (e) {
704
- console.error(` x Failed: ${u.file} - ${e.message}`);
830
+ console.error(` x Failed: ${entry.file} - ${e.message}`);
705
831
  failed++;
706
832
  }
707
833
  }
708
834
 
835
+ for (const u of plan.updated) {
836
+ await prepareWrite(u);
837
+ }
838
+
709
839
  for (const c of plan.created) {
710
- try {
711
- const dest = safePath(c.file);
712
- if (!dest) { console.error(` x Traversal rejected: ${c.file}`); failed++; continue; }
713
- // TOCTOU: recheck file didn't appear since planning
714
- if (lexists(dest)) {
715
- try { if (lstatSync(dest).isSymbolicLink()) { console.error(` x Symlink rejected: ${c.file}`); failed++; continue; } } catch (_) {}
716
- console.error(` x File created since plan: ${c.file} - treating as CONFLICT`);
717
- failed++; continue;
840
+ await prepareWrite(c, { mustNotExist: true });
841
+ }
842
+
843
+ if (failed === 0) {
844
+ for (const prepared of preparedWrites) {
845
+ if (prepared.mustNotExist && existsSync(prepared.dest)) {
846
+ console.error(` x File created since plan: ${prepared.file} - treating as CONFLICT`);
847
+ failed++;
718
848
  }
719
- const content = await fetchRemote(sourceBase + remotePath(c.file));
720
- const normalized = content.replace(/\r\n/g, '\n');
721
- const fetchedHash = sha256(normalized);
722
- if (fetchedHash !== c.remoteHash) {
723
- console.error(` x Hash mismatch: ${c.file}`);
724
- failed++; continue;
849
+ }
850
+ }
851
+
852
+ let applied = 0;
853
+ if (failed === 0) {
854
+ try {
855
+ for (const prepared of preparedWrites) {
856
+ mkdirSync(dirname(prepared.dest), { recursive: true });
857
+ writeFileSync(prepared.dest, prepared.content, 'utf-8');
858
+ applied++;
725
859
  }
726
- mkdirSync(dirname(dest), { recursive: true });
727
- writeFileSync(dest, normalized, 'utf-8');
728
- applied++;
729
860
  } catch (e) {
730
- console.error(` x Failed: ${c.file} - ${e.message}`);
861
+ console.error(` x Failed while writing prepared files - ${e.message}`);
731
862
  failed++;
732
863
  }
733
864
  }