lucentracode 0.1.0 → 0.2.0

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
@@ -32,9 +32,13 @@ lucentracode logout
32
32
 
33
33
  ## Install notes
34
34
 
35
- This package contains no binary. On install it pulls the one
36
- `lucentracode-<os>-<arch>` package that matches your machine, so you download one
37
- platform's worth of bytes rather than all of them.
35
+ This package contains no binary. On install npm pulls only the
36
+ `lucentracode-<os>-<arch>` packages matching your machine, not all twelve.
37
+
38
+ On arm64 that is one package. On x64 it is two or more: the `-baseline` build
39
+ exists for CPUs without AVX2, and package.json cannot express "requires AVX2" --
40
+ only `os` and `cpu`, which both x64 variants declare identically. npm therefore
41
+ fetches both and the postinstall picks the right one at runtime.
38
42
 
39
43
  If you install with `--ignore-scripts` (or with pnpm, which defaults to that),
40
44
  run the postinstall by hand:
@@ -0,0 +1,187 @@
1
+ #!/usr/bin/env node
2
+ "use strict"
3
+
4
+ // Shipped as `bin/lucentracode.js` inside the published `lucentracode` package, and it
5
+ // is the package's only `bin` entry.
6
+ //
7
+ // WHY A LAUNCHER INSTEAD OF A POSTINSTALL
8
+ //
9
+ // The first release used the conventional trick: ship a placeholder at the `bin` path
10
+ // and have a postinstall script overwrite it with the real platform binary. That is
11
+ // broken on current npm and cannot be fixed from the package side.
12
+ //
13
+ // npm made install scripts opt-in (npm 11.10) and off by default (npm 12), so
14
+ // `postinstall` simply does not run. The package then presents a placeholder as its
15
+ // executable. Ours began `#!/bin/sh`, so npm generated a Windows shim that invoked
16
+ // `/bin/sh` and users got:
17
+ //
18
+ // The term '/bin/sh.exe' is not recognized...
19
+ //
20
+ // And there is no recovery path for a global install: the warning npm prints tells the
21
+ // user to run `npm approve-scripts`, which refuses global installs with EGLOBAL
22
+ // (npm/cli#9463).
23
+ //
24
+ // So resolution moved from install time to run time. This file is plain CommonJS with a
25
+ // node shebang, which means npm generates an ordinary `node`-invoking shim on every
26
+ // platform, and the package declares no install scripts at all -- nothing to approve,
27
+ // nothing to block, no warning.
28
+ //
29
+ // The cost is one Node process in front of the binary. That is the price of an install
30
+ // that works, and it is what esbuild and friends settled on for the same reason.
31
+
32
+ const { spawnSync } = require("child_process")
33
+ const fs = require("fs")
34
+ const os = require("os")
35
+ const path = require("path")
36
+
37
+ const PRODUCT = "lucentracode"
38
+
39
+ const platformMap = { darwin: "darwin", linux: "linux", win32: "windows" }
40
+ const archMap = { x64: "x64", arm64: "arm64" }
41
+
42
+ const platform = platformMap[os.platform()] || os.platform()
43
+ const arch = archMap[os.arch()] || os.arch()
44
+ const base = `${PRODUCT}-${platform}-${arch}`
45
+ const binaryName = platform === "windows" ? `${PRODUCT}.exe` : PRODUCT
46
+
47
+ // Bun's default x64 build assumes AVX2. On a CPU without it the binary dies with an
48
+ // illegal instruction and no explanation, so pre-~2013 hardware needs `-baseline`.
49
+ //
50
+ // This detection is why the choice cannot be left to npm: `package.json` has no field
51
+ // meaning "requires AVX2" -- only `os` and `cpu`, which both x64 variants declare
52
+ // identically. npm therefore installs both and the decision lands here.
53
+ function supportsAvx2() {
54
+ if (arch !== "x64") return false
55
+
56
+ try {
57
+ if (platform === "linux") {
58
+ return /(^|\s)avx2(\s|$)/i.test(fs.readFileSync("/proc/cpuinfo", "utf8"))
59
+ }
60
+
61
+ if (platform === "darwin") {
62
+ const r = spawnSync("sysctl", ["-n", "hw.optional.avx2_0"], { encoding: "utf8", timeout: 1500 })
63
+ return r.status === 0 && (r.stdout || "").trim() === "1"
64
+ }
65
+
66
+ if (platform === "windows") {
67
+ // PF_AVX2_INSTRUCTIONS_AVAILABLE == 40
68
+ const command =
69
+ '(Add-Type -MemberDefinition "[DllImport(""kernel32.dll"")] public static extern bool IsProcessorFeaturePresent(int ProcessorFeature);" -Name K32 -Namespace W32 -PassThru)::IsProcessorFeaturePresent(40)'
70
+ for (const exe of ["powershell.exe", "pwsh.exe"]) {
71
+ const r = spawnSync(exe, ["-NoProfile", "-NonInteractive", "-Command", command], {
72
+ encoding: "utf8",
73
+ timeout: 3000,
74
+ windowsHide: true,
75
+ })
76
+ if (r.status !== 0) continue
77
+ const out = (r.stdout || "").trim().toLowerCase()
78
+ if (out === "true") return true
79
+ if (out === "false") return false
80
+ }
81
+ }
82
+ } catch {
83
+ // Detection is best-effort; a failure falls through to the safe answer below.
84
+ }
85
+
86
+ // Unknown means "assume the older CPU": a baseline binary runs everywhere, while an
87
+ // AVX2 binary on a pre-AVX2 chip crashes with no usable message.
88
+ return false
89
+ }
90
+
91
+ // Alpine and other musl distros need a musl-linked build; a glibc one fails at load
92
+ // with an error naming neither libc nor the product.
93
+ function isMusl() {
94
+ if (platform !== "linux") return false
95
+ try {
96
+ if (fs.existsSync("/etc/alpine-release")) return true
97
+ } catch {
98
+ /* probing may be blocked; fall through */
99
+ }
100
+ try {
101
+ const r = spawnSync("ldd", ["--version"], { encoding: "utf8" })
102
+ return `${r.stdout || ""}${r.stderr || ""}`.toLowerCase().includes("musl")
103
+ } catch {
104
+ return false
105
+ }
106
+ }
107
+
108
+ // Ordered best-first. Detection can be wrong -- a container may hide /proc/cpuinfo,
109
+ // `ldd` may be absent -- so a bad guess falls through to the next candidate instead of
110
+ // failing outright.
111
+ function candidates() {
112
+ const baseline = arch === "x64" && !supportsAvx2()
113
+
114
+ if (platform === "linux") {
115
+ if (isMusl()) {
116
+ if (arch === "x64") {
117
+ return baseline
118
+ ? [`${base}-baseline-musl`, `${base}-musl`, `${base}-baseline`, base]
119
+ : [`${base}-musl`, `${base}-baseline-musl`, base, `${base}-baseline`]
120
+ }
121
+ return [`${base}-musl`, base]
122
+ }
123
+ if (arch === "x64") {
124
+ return baseline
125
+ ? [`${base}-baseline`, base, `${base}-baseline-musl`, `${base}-musl`]
126
+ : [base, `${base}-baseline`, `${base}-musl`, `${base}-baseline-musl`]
127
+ }
128
+ return [base, `${base}-musl`]
129
+ }
130
+
131
+ if (arch === "x64") return baseline ? [`${base}-baseline`, base] : [base, `${base}-baseline`]
132
+ return [base]
133
+ }
134
+
135
+ function resolveBinary() {
136
+ const tried = []
137
+ for (const name of candidates()) {
138
+ try {
139
+ const pkgJson = require.resolve(`${name}/package.json`)
140
+ const candidate = path.join(path.dirname(pkgJson), "bin", binaryName)
141
+ if (fs.existsSync(candidate)) return candidate
142
+ tried.push(`${name} (installed, but ${binaryName} missing)`)
143
+ } catch {
144
+ tried.push(`${name} (not installed)`)
145
+ }
146
+ }
147
+ return { error: tried }
148
+ }
149
+
150
+ const resolved = resolveBinary()
151
+
152
+ if (typeof resolved !== "string") {
153
+ const lines = [
154
+ `${PRODUCT}: no binary for your platform (${os.platform()} ${os.arch()}).`,
155
+ "",
156
+ "Looked for:",
157
+ ...resolved.error.map((t) => ` - ${t}`),
158
+ "",
159
+ "This usually means the install was partial. Try:",
160
+ ` npm install -g ${PRODUCT} --force`,
161
+ "",
162
+ `If it persists, report it with your OS and CPU at`,
163
+ ` https://github.com/aryanbains/lucencode-cli/issues`,
164
+ ]
165
+ process.stderr.write(lines.join("\n") + "\n")
166
+ process.exit(1)
167
+ }
168
+
169
+ // `stdio: "inherit"` hands the real terminal to the child, which the TUI requires: it
170
+ // needs a genuine TTY for raw mode, resize events and colour. It also means the
171
+ // terminal delivers Ctrl+C to the foreground process group, so the child sees the
172
+ // signal directly rather than depending on the parent to relay it.
173
+ const result = spawnSync(resolved, process.argv.slice(2), { stdio: "inherit", windowsHide: false })
174
+
175
+ if (result.error) {
176
+ process.stderr.write(`${PRODUCT}: could not start ${resolved}\n${result.error.message}\n`)
177
+ process.exit(1)
178
+ }
179
+
180
+ // Report a signal death as a signal death (128+n), the way a shell does, so wrappers
181
+ // and CI can tell "interrupted" from "exited non-zero".
182
+ if (result.signal) {
183
+ const signals = { SIGINT: 2, SIGQUIT: 3, SIGKILL: 9, SIGTERM: 15, SIGHUP: 1 }
184
+ process.exit(128 + (signals[result.signal] || 0))
185
+ }
186
+
187
+ process.exit(result.status === null ? 1 : result.status)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lucentracode",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "AI coding agent for the terminal, powered by the LucentraCode gateway.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -21,10 +21,10 @@
21
21
  "lucentracode"
22
22
  ],
23
23
  "bin": {
24
- "lucentracode": "./bin/lucentracode.exe"
24
+ "lucentracode": "./bin/lucentracode.js"
25
25
  },
26
- "scripts": {
27
- "postinstall": "node ./postinstall.mjs"
26
+ "engines": {
27
+ "node": ">=18"
28
28
  },
29
29
  "os": [
30
30
  "darwin",
@@ -36,17 +36,17 @@
36
36
  "x64"
37
37
  ],
38
38
  "optionalDependencies": {
39
- "lucentracode-windows-x64-baseline": "0.1.0",
40
- "lucentracode-windows-x64": "0.1.0",
41
- "lucentracode-windows-arm64": "0.1.0",
42
- "lucentracode-linux-x64-musl": "0.1.0",
43
- "lucentracode-linux-x64-baseline-musl": "0.1.0",
44
- "lucentracode-linux-x64-baseline": "0.1.0",
45
- "lucentracode-linux-x64": "0.1.0",
46
- "lucentracode-linux-arm64-musl": "0.1.0",
47
- "lucentracode-linux-arm64": "0.1.0",
48
- "lucentracode-darwin-x64-baseline": "0.1.0",
49
- "lucentracode-darwin-x64": "0.1.0",
50
- "lucentracode-darwin-arm64": "0.1.0"
39
+ "lucentracode-windows-x64-baseline": "0.2.0",
40
+ "lucentracode-windows-x64": "0.2.0",
41
+ "lucentracode-windows-arm64": "0.2.0",
42
+ "lucentracode-linux-x64-musl": "0.2.0",
43
+ "lucentracode-linux-x64-baseline-musl": "0.2.0",
44
+ "lucentracode-linux-x64-baseline": "0.2.0",
45
+ "lucentracode-linux-x64": "0.2.0",
46
+ "lucentracode-linux-arm64-musl": "0.2.0",
47
+ "lucentracode-linux-arm64": "0.2.0",
48
+ "lucentracode-darwin-x64-baseline": "0.2.0",
49
+ "lucentracode-darwin-x64": "0.2.0",
50
+ "lucentracode-darwin-arm64": "0.2.0"
51
51
  }
52
52
  }
@@ -1,11 +0,0 @@
1
- #!/bin/sh
2
- echo "Error: lucentracode's postinstall script did not run." >&2
3
- echo "" >&2
4
- echo "This happens with --ignore-scripts, and with package managers such as pnpm" >&2
5
- echo "that do not run postinstall scripts by default." >&2
6
- echo "" >&2
7
- echo "Fix it by running:" >&2
8
- echo " cd node_modules/lucentracode && node postinstall.mjs" >&2
9
- echo "" >&2
10
- echo "Or reinstall without --ignore-scripts." >&2
11
- exit 1
package/postinstall.mjs DELETED
@@ -1,224 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- // Installed as `postinstall.mjs` inside the published `lucentracode` package.
4
- //
5
- // LucentraCode fork of script/postinstall.mjs. It is a separate file rather than an
6
- // edit because the upstream one is still used by upstream's own release path, and
7
- // because every identifier in here is a published package name -- a merge that
8
- // silently reverted `lucentracode-linux-x64` to `opencode-linux-x64` would make our
9
- // installer pull a different product's binary. Keeping them apart makes that
10
- // impossible rather than merely unlikely.
11
- //
12
- // What it does: npm has already installed whichever `lucentracode-<platform>-<arch>`
13
- // optional dependency matches this machine. This finds it and hard-links (or
14
- // copies) the real binary over the placeholder shim at bin/lucentracode.exe.
15
-
16
- import childProcess from "child_process"
17
- import fs from "fs"
18
- import os from "os"
19
- import path from "path"
20
- import { createRequire } from "module"
21
- import { fileURLToPath } from "url"
22
-
23
- const __dirname = path.dirname(fileURLToPath(import.meta.url))
24
- const require = createRequire(import.meta.url)
25
- const packageJson = JSON.parse(fs.readFileSync(path.join(__dirname, "package.json"), "utf8"))
26
-
27
- const PRODUCT = "lucentracode"
28
-
29
- const platformMap = {
30
- darwin: "darwin",
31
- linux: "linux",
32
- win32: "windows",
33
- }
34
- const archMap = {
35
- x64: "x64",
36
- arm64: "arm64",
37
- arm: "arm",
38
- }
39
-
40
- const platform = platformMap[os.platform()] ?? os.platform()
41
- const arch = archMap[os.arch()] ?? os.arch()
42
- const base = `${PRODUCT}-${platform}-${arch}`
43
- const sourceBinary = platform === "windows" ? `${PRODUCT}.exe` : PRODUCT
44
- // Always `.exe`, on every platform. The published `bin` entry has to point at a
45
- // path that exists before this script runs, and the same path afterwards; using one
46
- // name everywhere means there is exactly one thing to replace. npm sets the
47
- // executable bit from the bin field regardless of the extension.
48
- const targetBinary = path.join(__dirname, "bin", `${PRODUCT}.exe`)
49
-
50
- // AVX2 detection. Bun's default x64 build assumes AVX2; on a CPU without it the
51
- // binary dies with an illegal instruction and no useful message, so pre-~2013
52
- // hardware needs the `-baseline` variant instead.
53
- function supportsAvx2() {
54
- if (arch !== "x64") return false
55
-
56
- if (platform === "linux") {
57
- try {
58
- return /(^|\s)avx2(\s|$)/i.test(fs.readFileSync("/proc/cpuinfo", "utf8"))
59
- } catch {
60
- return false
61
- }
62
- }
63
-
64
- if (platform === "darwin") {
65
- try {
66
- const result = childProcess.spawnSync("sysctl", ["-n", "hw.optional.avx2_0"], {
67
- encoding: "utf8",
68
- timeout: 1500,
69
- })
70
- if (result.status !== 0) return false
71
- return (result.stdout || "").trim() === "1"
72
- } catch {
73
- return false
74
- }
75
- }
76
-
77
- if (platform === "windows") {
78
- const command =
79
- '(Add-Type -MemberDefinition "[DllImport(""kernel32.dll"")] public static extern bool IsProcessorFeaturePresent(int ProcessorFeature);" -Name Kernel32 -Namespace Win32 -PassThru)::IsProcessorFeaturePresent(40)'
80
-
81
- for (const executable of ["powershell.exe", "pwsh.exe", "pwsh", "powershell"]) {
82
- try {
83
- const result = childProcess.spawnSync(executable, ["-NoProfile", "-NonInteractive", "-Command", command], {
84
- encoding: "utf8",
85
- timeout: 3000,
86
- windowsHide: true,
87
- })
88
- if (result.status !== 0) continue
89
- const output = (result.stdout || "").trim().toLowerCase()
90
- if (output === "true" || output === "1") return true
91
- if (output === "false" || output === "0") return false
92
- } catch {
93
- continue
94
- }
95
- }
96
- }
97
-
98
- return false
99
- }
100
-
101
- // Alpine and other musl distros need a musl-linked binary; a glibc one fails at
102
- // load time with an error that mentions neither libc nor LucentraCode.
103
- function isMusl() {
104
- if (platform !== "linux") return false
105
-
106
- try {
107
- if (fs.existsSync("/etc/alpine-release")) return true
108
- } catch {
109
- // Ignore filesystem probes that are blocked by the host.
110
- }
111
-
112
- try {
113
- const result = childProcess.spawnSync("ldd", ["--version"], { encoding: "utf8" })
114
- return `${result.stdout || ""}${result.stderr || ""}`.toLowerCase().includes("musl")
115
- } catch {
116
- return false
117
- }
118
- }
119
-
120
- // Ordered best-first, with fallbacks. Detection can be wrong -- a container may
121
- // hide /proc/cpuinfo, `ldd` may be absent -- so a wrong guess degrades to trying
122
- // the next candidate instead of failing the install.
123
- function packageNames() {
124
- const baseline = arch === "x64" && !supportsAvx2()
125
-
126
- if (platform === "linux") {
127
- if (isMusl()) {
128
- if (arch === "x64")
129
- return baseline
130
- ? [`${base}-baseline-musl`, `${base}-musl`, `${base}-baseline`, base]
131
- : [`${base}-musl`, `${base}-baseline-musl`, base, `${base}-baseline`]
132
- return [`${base}-musl`, base]
133
- }
134
-
135
- if (arch === "x64")
136
- return baseline
137
- ? [`${base}-baseline`, base, `${base}-baseline-musl`, `${base}-musl`]
138
- : [base, `${base}-baseline`, `${base}-musl`, `${base}-baseline-musl`]
139
- return [base, `${base}-musl`]
140
- }
141
-
142
- if (arch === "x64") return baseline ? [`${base}-baseline`, base] : [base, `${base}-baseline`]
143
- return [base]
144
- }
145
-
146
- function resolveBinary(name) {
147
- const packageJsonPath = require.resolve(`${name}/package.json`)
148
- const binaryPath = path.join(path.dirname(packageJsonPath), "bin", sourceBinary)
149
- if (!fs.existsSync(binaryPath)) throw new Error(`Binary not found at ${binaryPath}`)
150
- return binaryPath
151
- }
152
-
153
- // pnpm and yarn PnP sometimes do not place optional dependencies where
154
- // require.resolve can see them. Rather than fail, fetch the one package we need
155
- // into a temp prefix and take the binary out of it.
156
- function installPackage(name) {
157
- const version = packageJson.optionalDependencies?.[name]
158
- if (!version) return
159
-
160
- const temp = fs.mkdtempSync(path.join(os.tmpdir(), `${PRODUCT}-install-`))
161
- try {
162
- const result = childProcess.spawnSync(
163
- "npm",
164
- ["install", "--ignore-scripts", "--no-save", "--loglevel=error", "--prefix", temp, `${name}@${version}`],
165
- { stdio: "inherit", windowsHide: true },
166
- )
167
- if (result.status !== 0) return
168
- const packageDir = path.join(temp, "node_modules", name)
169
- copyBinary(path.join(packageDir, "bin", sourceBinary), targetBinary)
170
- return true
171
- } finally {
172
- fs.rmSync(temp, { recursive: true, force: true })
173
- }
174
- }
175
-
176
- function copyBinary(source, target) {
177
- if (!fs.existsSync(source)) throw new Error(`Binary not found at ${source}`)
178
- fs.mkdirSync(path.dirname(target), { recursive: true })
179
- if (fs.existsSync(target)) fs.unlinkSync(target)
180
- try {
181
- // A hard link avoids a second ~140 MB on disk. Falls back to a copy when the
182
- // store and the project are on different filesystems.
183
- fs.linkSync(source, target)
184
- } catch {
185
- fs.copyFileSync(source, target)
186
- }
187
- fs.chmodSync(target, 0o755)
188
- }
189
-
190
- // Actually run it. A binary that is present but cannot execute -- wrong libc,
191
- // wrong instruction set, truncated download -- must fall through to the next
192
- // candidate rather than be declared installed.
193
- function verifyBinary() {
194
- const result = childProcess.spawnSync(targetBinary, ["--version"], {
195
- encoding: "utf8",
196
- stdio: "ignore",
197
- windowsHide: true,
198
- })
199
- return result.status === 0
200
- }
201
-
202
- function main() {
203
- for (const name of packageNames()) {
204
- try {
205
- copyBinary(resolveBinary(name), targetBinary)
206
- if (verifyBinary()) return
207
- } catch {
208
- if (installPackage(name) && verifyBinary()) return
209
- }
210
- }
211
-
212
- throw new Error(
213
- `Your package manager did not install a matching LucentraCode binary. Try installing ${packageNames()
214
- .map((name) => JSON.stringify(name))
215
- .join(" or ")} manually, or report this at https://github.com/aryanbains/lucencode-cli/issues with your OS and CPU architecture.`,
216
- )
217
- }
218
-
219
- try {
220
- main()
221
- } catch (error) {
222
- console.error(error.message)
223
- process.exit(1)
224
- }