@aihu/compiler 0.5.0 → 0.5.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.
package/README.md CHANGED
@@ -21,7 +21,7 @@ npm install @aihu/compiler
21
21
  bun add @aihu/compiler
22
22
  ```
23
23
 
24
- <sub><i>Auto-generated against `@aihu/compiler@0.5.0`.</i></sub>
24
+ <sub><i>Auto-generated against `@aihu/compiler@0.5.1`.</i></sub>
25
25
 
26
26
  <!-- END_AUTOGEN: install -->
27
27
 
@@ -32,12 +32,12 @@ bun add @aihu/compiler
32
32
 
33
33
  | | |
34
34
  |---|---|
35
- | **Version** | `0.5.0` |
35
+ | **Version** | `0.5.1` |
36
36
  | **Tier** | D — Compiler — Single-File Component (.aihu) → Web Component |
37
- | **Published files** | 5 entries |
37
+ | **Published files** | 4 entries |
38
38
  | **License** | MIT |
39
39
 
40
- <sub><i>Auto-generated against `@aihu/compiler@0.5.0`.</i></sub>
40
+ <sub><i>Auto-generated against `@aihu/compiler@0.5.1`.</i></sub>
41
41
 
42
42
  <!-- END_AUTOGEN: stats -->
43
43
 
@@ -50,7 +50,7 @@ bun add @aihu/compiler
50
50
  |---|---|---|
51
51
  | `.` | `./dist/index.js` | `—` |
52
52
 
53
- <sub><i>Auto-generated against `@aihu/compiler@0.5.0`.</i></sub>
53
+ <sub><i>Auto-generated against `@aihu/compiler@0.5.1`.</i></sub>
54
54
 
55
55
  <!-- END_AUTOGEN: exports -->
56
56
 
@@ -62,9 +62,9 @@ bun add @aihu/compiler
62
62
  **Peer dependencies:**
63
63
 
64
64
  - `vite` — `>=5.0.0`
65
- - `@aihu/css-engine` — `>=0.2.0`
65
+ - `@aihu/css-engine` — `>=0.2.2`
66
66
 
67
- <sub><i>Auto-generated against `@aihu/compiler@0.5.0`.</i></sub>
67
+ <sub><i>Auto-generated against `@aihu/compiler@0.5.1`.</i></sub>
68
68
 
69
69
  <!-- END_AUTOGEN: deps -->
70
70
 
@@ -78,7 +78,7 @@ bun add @aihu/compiler
78
78
  - [Macro Vocabulary spec](../../docs/superpowers/specs/2026-05-02-spec-macro-vocabulary.md)
79
79
  - [Aihu framework root](../../README.md)
80
80
 
81
- <sub><i>Auto-generated against `@aihu/compiler@0.5.0`.</i></sub>
81
+ <sub><i>Auto-generated against `@aihu/compiler@0.5.1`.</i></sub>
82
82
 
83
83
  <!-- END_AUTOGEN: see-also -->
84
84
 
@@ -89,6 +89,6 @@ bun add @aihu/compiler
89
89
 
90
90
  MIT — see [LICENSE](../../LICENSE).
91
91
 
92
- <sub><i>Auto-generated against `@aihu/compiler@0.5.0`.</i></sub>
92
+ <sub><i>Auto-generated against `@aihu/compiler@0.5.1`.</i></sub>
93
93
 
94
94
  <!-- END_AUTOGEN: license -->
package/js/postinstall.ts CHANGED
@@ -4,9 +4,10 @@
4
4
  * Resolution order (first match wins):
5
5
  *
6
6
  * 1. SCRIBE_SKIP_POSTINSTALL=1 → no-op, exit 0.
7
- * 2. Binary already present at → no-op, exit 0.
8
- * bin/aihu-compile<ext> OR
9
- * target/release/aihu-compile<ext>
7
+ * 2. Binary already present at → arch-validate it; if compatible no-op
8
+ * bin/aihu-compile<ext> OR exit 0, if incompatible (e.g. a Linux
9
+ * target/release/aihu-compile<ext> ELF that leaked into the tarball on a
10
+ * macOS host) delete it and fall through.
10
11
  * 3. SCRIBE_COMPILE_BIN=<path> → copy that path → bin/, exit 0.
11
12
  * 4. GitHub Releases download → bin/aihu-compile<ext>, verify SHA256,
12
13
  * exit 0. (arch-4 §4.3 — sidecar
@@ -42,10 +43,13 @@ import { spawnSync } from 'node:child_process'
42
43
  import { createHash } from 'node:crypto'
43
44
  import {
44
45
  chmodSync,
46
+ closeSync,
45
47
  copyFileSync,
46
48
  existsSync,
47
49
  mkdirSync,
50
+ openSync,
48
51
  readFileSync,
52
+ readSync,
49
53
  unlinkSync,
50
54
  writeFileSync,
51
55
  } from 'node:fs'
@@ -194,6 +198,106 @@ async function verifySha256(
194
198
  return { ok: true }
195
199
  }
196
200
 
201
+ /**
202
+ * Inspect a binary's file-format magic bytes and (where cheaply available) its
203
+ * architecture field. The point is to catch a wrong-arch binary sitting on disk
204
+ * BEFORE we hand it to spawn() and ENOEXEC the user (see PR description: a
205
+ * Linux x86-64 ELF can ship inside the tarball when the publisher's machine
206
+ * left one in bin/, and arch-blind idempotency then traps it).
207
+ *
208
+ * Returns `null` if the file can't be read; format `'unknown'` for headers we
209
+ * don't recognise (e.g. shell scripts, FAT/universal Mach-O — those callers
210
+ * conservatively treat as compatible).
211
+ */
212
+ function inspectBinary(
213
+ path: string,
214
+ ): { format: 'elf' | 'macho' | 'macho-fat' | 'pe' | 'unknown'; arch: string | null } | null {
215
+ let fd: number | null = null
216
+ try {
217
+ fd = openSync(path, 'r')
218
+ const buf = Buffer.alloc(20)
219
+ const bytesRead = readSync(fd, buf, 0, 20, 0)
220
+ if (bytesRead < 20) return null
221
+
222
+ // ELF: 0x7F 'E' 'L' 'F'
223
+ if (buf[0] === 0x7f && buf[1] === 0x45 && buf[2] === 0x4c && buf[3] === 0x46) {
224
+ // e_machine at offset 18 (u16, endianness per EI_DATA at offset 5).
225
+ const littleEndian = buf[5] === 1
226
+ const machine = littleEndian ? buf.readUInt16LE(18) : buf.readUInt16BE(18)
227
+ const arch =
228
+ machine === 0x3e ? 'x64' : machine === 0xb7 ? 'arm64' : machine === 0x03 ? 'ia32' : null
229
+ return { format: 'elf', arch }
230
+ }
231
+
232
+ const magic = buf.readUInt32LE(0)
233
+ // Mach-O 64-bit LE-on-disk magic. cputype at offset 4 (u32 LE).
234
+ if (magic === 0xfeedfacf) {
235
+ const cputype = buf.readUInt32LE(4)
236
+ const arch =
237
+ cputype === 0x01000007 ? 'x64' : cputype === 0x0100000c ? 'arm64' : null
238
+ return { format: 'macho', arch }
239
+ }
240
+ // Mach-O FAT/universal (multi-arch). On-disk bytes are big-endian per spec;
241
+ // a little-endian read yields 0xBEBAFECA.
242
+ if (magic === 0xbebafeca || magic === 0xcafebabe) {
243
+ return { format: 'macho-fat', arch: null }
244
+ }
245
+
246
+ // PE (Windows): 'MZ' at offset 0. Skip detailed machine parse — Windows
247
+ // arch mismatches are rare and not the bug we're fixing here.
248
+ if (buf[0] === 0x4d && buf[1] === 0x5a) {
249
+ return { format: 'pe', arch: null }
250
+ }
251
+
252
+ return { format: 'unknown', arch: null }
253
+ } catch {
254
+ return null
255
+ } finally {
256
+ if (fd !== null) {
257
+ try {
258
+ closeSync(fd)
259
+ } catch {
260
+ /* swallow */
261
+ }
262
+ }
263
+ }
264
+ }
265
+
266
+ function expectedFormatFor(platform: NodeJS.Platform): 'elf' | 'macho' | 'pe' | null {
267
+ if (platform === 'darwin') return 'macho'
268
+ if (platform === 'win32') return 'pe'
269
+ if (platform === 'linux') return 'elf'
270
+ return null
271
+ }
272
+
273
+ /**
274
+ * Decide whether an on-disk binary is safe to keep for the current host.
275
+ * Returns `null` when compatible; otherwise a short reason string suitable for
276
+ * a warn-level log. `'unknown'` format is treated as compatible (avoid breaking
277
+ * exotic but legitimate setups — e.g. a shell wrapper a dev placed here).
278
+ */
279
+ function incompatibilityReason(
280
+ path: string,
281
+ platform: NodeJS.Platform,
282
+ arch: string,
283
+ ): string | null {
284
+ const probe = inspectBinary(path)
285
+ if (!probe) return null
286
+ if (probe.format === 'unknown') return null
287
+ // FAT/universal Mach-O ships multiple slices; trust it on darwin, reject elsewhere.
288
+ if (probe.format === 'macho-fat') {
289
+ return platform === 'darwin' ? null : `universal Mach-O on ${platform}`
290
+ }
291
+ const expected = expectedFormatFor(platform)
292
+ if (expected !== null && probe.format !== expected) {
293
+ return `${probe.format} binary on ${platform} (expected ${expected})`
294
+ }
295
+ if (probe.arch !== null && probe.arch !== arch) {
296
+ return `${probe.arch} binary on ${platform}/${arch}`
297
+ }
298
+ return null
299
+ }
300
+
197
301
  function tryLocalBuild(pkgDir: string): boolean {
198
302
  // Check for cargo first — quick probe without spawning a build.
199
303
  const probe = spawnSync('cargo', ['--version'], {
@@ -247,15 +351,33 @@ async function main(): Promise<void> {
247
351
  mkdirSync(binDir, { recursive: true })
248
352
  }
249
353
 
250
- // Idempotency: nothing to do if a binary is already in place at either
251
- // the released-asset path (bin/) or the local-build path (target/release).
354
+ // Idempotency: nothing to do if a usable binary is already in place at
355
+ // either the released-asset path (bin/) or the local-build path
356
+ // (target/release). "Usable" means the magic bytes match the host
357
+ // platform/arch — without that arch probe a wrong-arch binary sitting in
358
+ // the tarball (e.g. a Linux ELF that leaked from the publisher's machine)
359
+ // short-circuits the download path and ENOEXECs the user at spawn time.
252
360
  if (existsSync(binPath)) {
253
- info(`bin already present at ${binPath}, skipping.`)
254
- return
361
+ const reason = incompatibilityReason(binPath, platform, arch)
362
+ if (reason === null) {
363
+ info(`bin already present at ${binPath}, skipping.`)
364
+ return
365
+ }
366
+ warn(`existing ${binPath} is incompatible (${reason}); removing and re-acquiring.`)
367
+ try {
368
+ unlinkSync(binPath)
369
+ } catch (err) {
370
+ const detail = err instanceof Error ? err.message : String(err)
371
+ warn(`could not remove incompatible binary at ${binPath}: ${detail}. Continuing — download will overwrite.`)
372
+ }
255
373
  }
256
374
  if (existsSync(targetReleaseBin)) {
257
- info(`local cargo build already present at ${targetReleaseBin}, skipping.`)
258
- return
375
+ const reason = incompatibilityReason(targetReleaseBin, platform, arch)
376
+ if (reason === null) {
377
+ info(`local cargo build already present at ${targetReleaseBin}, skipping.`)
378
+ return
379
+ }
380
+ warn(`existing ${targetReleaseBin} is incompatible (${reason}); ignoring and acquiring a fresh binary.`)
259
381
  }
260
382
 
261
383
  // Local dev override — copy a locally built binary instead of downloading.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aihu/compiler",
3
- "version": "0.5.0",
3
+ "version": "0.5.1",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -18,7 +18,6 @@
18
18
  "files": [
19
19
  "dist",
20
20
  "js/postinstall.ts",
21
- "bin",
22
21
  "README.md",
23
22
  "LICENSE"
24
23
  ],
@@ -32,7 +31,7 @@
32
31
  },
33
32
  "peerDependencies": {
34
33
  "vite": ">=5.0.0",
35
- "@aihu/css-engine": ">=0.2.0"
34
+ "@aihu/css-engine": ">=0.2.2"
36
35
  },
37
36
  "peerDependenciesMeta": {
38
37
  "vite": {
@@ -43,7 +42,7 @@
43
42
  }
44
43
  },
45
44
  "devDependencies": {
46
- "@aihu/css-engine": "0.2.0"
45
+ "@aihu/css-engine": "0.2.2"
47
46
  },
48
47
  "description": "Single File Component (.aihu) compiler — Rust binary + JS glue.",
49
48
  "repository": {