@nurix/etna 0.20.30 → 0.20.31

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.
@@ -1,5 +1,5 @@
1
1
  {
2
- "fingerprint": "687486a21483",
2
+ "fingerprint": "1ced34191650",
3
3
  "harnesses": {
4
4
  "code": {
5
5
  "plugin": "code",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "harness": "code",
3
3
  "name": "code",
4
- "version": "3.20.1",
4
+ "version": "3.20.2",
5
5
  "files": [
6
6
  ".prettierignore",
7
7
  ".prettierrc.json",
@@ -29,5 +29,5 @@
29
29
  "origin": "https://cdn.nustack.io",
30
30
  "track": "latest"
31
31
  },
32
- "digest": "f52d24285ab6"
32
+ "digest": "535a7aa34dfe"
33
33
  }
@@ -206,7 +206,7 @@ function removeEmptyParents(projectDir, rel) {
206
206
  }
207
207
  }
208
208
 
209
- // Writes the manifest's mounted files that differ on disk, then deletes owned names the manifest omits.
209
+ // Fetches the manifest's mounted files that differ on disk, writes them deepest path first, then deletes owned names the manifest omits.
210
210
  async function mountFromManifest({ projectDir, manifest, origin, cacheDir, fetchImpl, timeoutMs, priorOwned }) {
211
211
  const all = mountedEntries(manifest);
212
212
  const entries = admittedEntries(projectDir, all);
@@ -218,14 +218,28 @@ async function mountFromManifest({ projectDir, manifest, origin, cacheDir, fetch
218
218
  const dest = join(projectDir, entry.path);
219
219
  return !(existsSync(dest) && sha256Of(readFileSync(dest)) === entry.sha256);
220
220
  });
221
+ // Booby trap: every live session in the checkout runs these hooks while the mount rewrites them.
222
+ // Writing as each fetch lands let `hooks/compliance.mjs` arrive seconds before a new `hooks/lib/`
223
+ // module it imports, and every hook call in that window died with ERR_MODULE_NOT_FOUND (3.20.1
224
+ // rollout, 2026-10-05). Fetch everything, then write locally, imports (deeper paths) before importers.
225
+ const fetched = [];
221
226
  await eachLimited(stale, async (entry) => {
222
227
  try {
223
- writeAtomic(join(projectDir, entry.path), await readObject({ origin, entry, cacheDir, fetchImpl, timeoutMs }));
224
- written.push(entry.path);
228
+ fetched.push({ entry, bytes: await readObject({ origin, entry, cacheDir, fetchImpl, timeoutMs }) });
225
229
  } catch {
226
230
  failed.push(entry.path);
227
231
  }
228
232
  });
233
+ const depth = (path) => path.split('/').length;
234
+ fetched.sort((a, b) => depth(b.entry.path) - depth(a.entry.path) || a.entry.path.localeCompare(b.entry.path));
235
+ for (const { entry, bytes } of fetched) {
236
+ try {
237
+ writeAtomic(join(projectDir, entry.path), bytes);
238
+ written.push(entry.path);
239
+ } catch {
240
+ failed.push(entry.path);
241
+ }
242
+ }
229
243
  for (const rel of prunableFiles(projectDir, unionOwned(manifest.owned, priorOwned))) {
230
244
  if (listed.has(rel)) continue;
231
245
  rmSync(join(projectDir, rel), { force: true });
@@ -204,7 +204,7 @@ function removeEmptyParents(projectDir, rel) {
204
204
  }
205
205
  }
206
206
 
207
- // Writes the manifest's mounted files that differ on disk, then deletes owned names the manifest omits.
207
+ // Fetches the manifest's mounted files that differ on disk, writes them deepest path first, then deletes owned names the manifest omits.
208
208
  async function mountFromManifest({ projectDir, manifest, origin, cacheDir, fetchImpl, timeoutMs, priorOwned }) {
209
209
  const all = mountedEntries(manifest);
210
210
  const entries = admittedEntries(projectDir, all);
@@ -216,14 +216,28 @@ async function mountFromManifest({ projectDir, manifest, origin, cacheDir, fetch
216
216
  const dest = join(projectDir, entry.path);
217
217
  return !(existsSync(dest) && sha256Of(readFileSync(dest)) === entry.sha256);
218
218
  });
219
+ // Booby trap: every live session in the checkout runs these hooks while the mount rewrites them.
220
+ // Writing as each fetch lands let `hooks/compliance.mjs` arrive seconds before a new `hooks/lib/`
221
+ // module it imports, and every hook call in that window died with ERR_MODULE_NOT_FOUND (3.20.1
222
+ // rollout, 2026-10-05). Fetch everything, then write locally, imports (deeper paths) before importers.
223
+ const fetched = [];
219
224
  await eachLimited(stale, async (entry) => {
220
225
  try {
221
- writeAtomic(join(projectDir, entry.path), await readObject({ origin, entry, cacheDir, fetchImpl, timeoutMs }));
222
- written.push(entry.path);
226
+ fetched.push({ entry, bytes: await readObject({ origin, entry, cacheDir, fetchImpl, timeoutMs }) });
223
227
  } catch {
224
228
  failed.push(entry.path);
225
229
  }
226
230
  });
231
+ const depth = (path) => path.split('/').length;
232
+ fetched.sort((a, b) => depth(b.entry.path) - depth(a.entry.path) || a.entry.path.localeCompare(b.entry.path));
233
+ for (const { entry, bytes } of fetched) {
234
+ try {
235
+ writeAtomic(join(projectDir, entry.path), bytes);
236
+ written.push(entry.path);
237
+ } catch {
238
+ failed.push(entry.path);
239
+ }
240
+ }
227
241
  for (const rel of prunableFiles(projectDir, unionOwned(manifest.owned, priorOwned))) {
228
242
  if (listed.has(rel)) continue;
229
243
  rmSync(join(projectDir, rel), { force: true });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nurix/etna",
3
- "version": "0.20.30",
3
+ "version": "0.20.31",
4
4
  "description": "Bootstrap a project with a Nurix harness — writes the committed contract (CLAUDE.md, AGENTS.md, .claude/harness.json, settings.json, the mount hook) and mounts the harness itself, gitignored, from a bundled payload; sessions keep it current from the CDN.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -46,5 +46,5 @@
46
46
  "agent",
47
47
  "plugin"
48
48
  ],
49
- "etnaFingerprint": "687486a21483"
49
+ "etnaFingerprint": "1ced34191650"
50
50
  }