@aihu/compiler 0.5.0 → 0.5.2

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.2`.</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.2` |
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.2`.</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.2`.</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.3`
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.2`.</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.2`.</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.2`.</i></sub>
93
93
 
94
94
  <!-- END_AUTOGEN: license -->
package/bin/aihu-compile CHANGED
Binary file
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,105 @@ 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 = cputype === 0x01000007 ? 'x64' : cputype === 0x0100000c ? 'arm64' : null
237
+ return { format: 'macho', arch }
238
+ }
239
+ // Mach-O FAT/universal (multi-arch). On-disk bytes are big-endian per spec;
240
+ // a little-endian read yields 0xBEBAFECA.
241
+ if (magic === 0xbebafeca || magic === 0xcafebabe) {
242
+ return { format: 'macho-fat', arch: null }
243
+ }
244
+
245
+ // PE (Windows): 'MZ' at offset 0. Skip detailed machine parse — Windows
246
+ // arch mismatches are rare and not the bug we're fixing here.
247
+ if (buf[0] === 0x4d && buf[1] === 0x5a) {
248
+ return { format: 'pe', arch: null }
249
+ }
250
+
251
+ return { format: 'unknown', arch: null }
252
+ } catch {
253
+ return null
254
+ } finally {
255
+ if (fd !== null) {
256
+ try {
257
+ closeSync(fd)
258
+ } catch {
259
+ /* swallow */
260
+ }
261
+ }
262
+ }
263
+ }
264
+
265
+ function expectedFormatFor(platform: NodeJS.Platform): 'elf' | 'macho' | 'pe' | null {
266
+ if (platform === 'darwin') return 'macho'
267
+ if (platform === 'win32') return 'pe'
268
+ if (platform === 'linux') return 'elf'
269
+ return null
270
+ }
271
+
272
+ /**
273
+ * Decide whether an on-disk binary is safe to keep for the current host.
274
+ * Returns `null` when compatible; otherwise a short reason string suitable for
275
+ * a warn-level log. `'unknown'` format is treated as compatible (avoid breaking
276
+ * exotic but legitimate setups — e.g. a shell wrapper a dev placed here).
277
+ */
278
+ function incompatibilityReason(
279
+ path: string,
280
+ platform: NodeJS.Platform,
281
+ arch: string,
282
+ ): string | null {
283
+ const probe = inspectBinary(path)
284
+ if (!probe) return null
285
+ if (probe.format === 'unknown') return null
286
+ // FAT/universal Mach-O ships multiple slices; trust it on darwin, reject elsewhere.
287
+ if (probe.format === 'macho-fat') {
288
+ return platform === 'darwin' ? null : `universal Mach-O on ${platform}`
289
+ }
290
+ const expected = expectedFormatFor(platform)
291
+ if (expected !== null && probe.format !== expected) {
292
+ return `${probe.format} binary on ${platform} (expected ${expected})`
293
+ }
294
+ if (probe.arch !== null && probe.arch !== arch) {
295
+ return `${probe.arch} binary on ${platform}/${arch}`
296
+ }
297
+ return null
298
+ }
299
+
197
300
  function tryLocalBuild(pkgDir: string): boolean {
198
301
  // Check for cargo first — quick probe without spawning a build.
199
302
  const probe = spawnSync('cargo', ['--version'], {
@@ -247,15 +350,37 @@ async function main(): Promise<void> {
247
350
  mkdirSync(binDir, { recursive: true })
248
351
  }
249
352
 
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).
353
+ // Idempotency: nothing to do if a usable binary is already in place at
354
+ // either the released-asset path (bin/) or the local-build path
355
+ // (target/release). "Usable" means the magic bytes match the host
356
+ // platform/arch — without that arch probe a wrong-arch binary sitting in
357
+ // the tarball (e.g. a Linux ELF that leaked from the publisher's machine)
358
+ // short-circuits the download path and ENOEXECs the user at spawn time.
252
359
  if (existsSync(binPath)) {
253
- info(`bin already present at ${binPath}, skipping.`)
254
- return
360
+ const reason = incompatibilityReason(binPath, platform, arch)
361
+ if (reason === null) {
362
+ info(`bin already present at ${binPath}, skipping.`)
363
+ return
364
+ }
365
+ warn(`existing ${binPath} is incompatible (${reason}); removing and re-acquiring.`)
366
+ try {
367
+ unlinkSync(binPath)
368
+ } catch (err) {
369
+ const detail = err instanceof Error ? err.message : String(err)
370
+ warn(
371
+ `could not remove incompatible binary at ${binPath}: ${detail}. Continuing — download will overwrite.`,
372
+ )
373
+ }
255
374
  }
256
375
  if (existsSync(targetReleaseBin)) {
257
- info(`local cargo build already present at ${targetReleaseBin}, skipping.`)
258
- return
376
+ const reason = incompatibilityReason(targetReleaseBin, platform, arch)
377
+ if (reason === null) {
378
+ info(`local cargo build already present at ${targetReleaseBin}, skipping.`)
379
+ return
380
+ }
381
+ warn(
382
+ `existing ${targetReleaseBin} is incompatible (${reason}); ignoring and acquiring a fresh binary.`,
383
+ )
259
384
  }
260
385
 
261
386
  // 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.2",
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.3"
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.3"
47
46
  },
48
47
  "description": "Single File Component (.aihu) compiler — Rust binary + JS glue.",
49
48
  "repository": {