nikcli-ai 1.210.0 → 1.211.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.
Files changed (3) hide show
  1. package/bin/nikcli +113 -17
  2. package/package.json +13 -13
  3. package/postinstall.mjs +178 -106
package/bin/nikcli CHANGED
@@ -36,28 +36,111 @@ const archMap = {
36
36
  arm: "arm",
37
37
  }
38
38
 
39
- let platform = platformMap[os.platform()]
40
- if (!platform) {
41
- platform = os.platform()
42
- }
43
- let arch = archMap[os.arch()]
44
- if (!arch) {
45
- arch = os.arch()
46
- }
39
+ const platform = platformMap[os.platform()] || os.platform()
40
+ const arch = archMap[os.arch()] || os.arch()
47
41
  const base = "nikcli-ai-" + platform + "-" + arch
48
42
  const binary = platform === "windows" ? "nikcli.exe" : "nikcli"
49
43
 
44
+ // Fast path: postinstall drops the native binary right next to this wrapper.
45
+ const materialized = path.join(scriptDir, platform === "windows" ? "nikcli.exe" : "nikcli-bin")
46
+ if (fs.existsSync(materialized)) {
47
+ run(materialized)
48
+ }
49
+
50
+ function supportsAvx2() {
51
+ if (arch !== "x64") return false
52
+ if (platform === "linux") {
53
+ try {
54
+ return /(^|\s)avx2(\s|$)/i.test(fs.readFileSync("/proc/cpuinfo", "utf8"))
55
+ } catch (e) {
56
+ return false
57
+ }
58
+ }
59
+ if (platform === "darwin") {
60
+ try {
61
+ const result = childProcess.spawnSync("sysctl", ["-n", "hw.optional.avx2_0"], {
62
+ encoding: "utf8",
63
+ timeout: 1500,
64
+ })
65
+ return result.status === 0 && (result.stdout || "").trim() === "1"
66
+ } catch (e) {
67
+ return false
68
+ }
69
+ }
70
+ if (platform === "windows") {
71
+ // PF_AVX2_INSTRUCTIONS_AVAILABLE == 40
72
+ const command =
73
+ '(Add-Type -MemberDefinition "[DllImport(""kernel32.dll"")] public static extern bool IsProcessorFeaturePresent(int ProcessorFeature);" -Name Kernel32 -Namespace Win32 -PassThru)::IsProcessorFeaturePresent(40)'
74
+ const executables = ["powershell.exe", "pwsh.exe", "pwsh", "powershell"]
75
+ for (let i = 0; i < executables.length; i++) {
76
+ try {
77
+ const result = childProcess.spawnSync(executables[i], ["-NoProfile", "-NonInteractive", "-Command", command], {
78
+ encoding: "utf8",
79
+ timeout: 3000,
80
+ windowsHide: true,
81
+ })
82
+ if (result.status !== 0) continue
83
+ const output = (result.stdout || "").trim().toLowerCase()
84
+ if (output === "true" || output === "1") return true
85
+ if (output === "false" || output === "0") return false
86
+ } catch (e) {
87
+ continue
88
+ }
89
+ }
90
+ }
91
+ return false
92
+ }
93
+
94
+ function isMusl() {
95
+ if (platform !== "linux") return false
96
+ try {
97
+ if (fs.existsSync("/etc/alpine-release")) return true
98
+ } catch (e) {
99
+ // ignore
100
+ }
101
+ try {
102
+ const result = childProcess.spawnSync("ldd", ["--version"], { encoding: "utf8" })
103
+ return ((result.stdout || "") + (result.stderr || "")).toLowerCase().indexOf("musl") !== -1
104
+ } catch (e) {
105
+ return false
106
+ }
107
+ }
108
+
109
+ // Ordered best → worst. A prefix match is not enough: on x64 it would happily
110
+ // pick a `-musl` or `-baseline` build depending on readdir order, and a glibc
111
+ // binary on musl (or an AVX2 binary on a CPU without it) does not merely run
112
+ // slower — it fails to start.
113
+ function packageNames() {
114
+ const baseline = arch === "x64" && !supportsAvx2()
115
+ if (platform === "linux") {
116
+ if (isMusl()) {
117
+ if (arch === "x64") {
118
+ return baseline
119
+ ? [base + "-baseline-musl", base + "-musl", base + "-baseline", base]
120
+ : [base + "-musl", base + "-baseline-musl", base, base + "-baseline"]
121
+ }
122
+ return [base + "-musl", base]
123
+ }
124
+ if (arch === "x64") {
125
+ return baseline
126
+ ? [base + "-baseline", base, base + "-baseline-musl", base + "-musl"]
127
+ : [base, base + "-baseline", base + "-musl", base + "-baseline-musl"]
128
+ }
129
+ return [base, base + "-musl"]
130
+ }
131
+ if (arch === "x64") return baseline ? [base + "-baseline", base] : [base, base + "-baseline"]
132
+ return [base]
133
+ }
134
+
135
+ const candidates = packageNames()
136
+
50
137
  function findBinary(startDir) {
51
138
  let current = startDir
52
139
  for (;;) {
53
140
  const modules = path.join(current, "node_modules")
54
141
  if (fs.existsSync(modules)) {
55
- const entries = fs.readdirSync(modules)
56
- for (const entry of entries) {
57
- if (!entry.startsWith(base)) {
58
- continue
59
- }
60
- const candidate = path.join(modules, entry, "bin", binary)
142
+ for (let i = 0; i < candidates.length; i++) {
143
+ const candidate = path.join(modules, candidates[i], "bin", binary)
61
144
  if (fs.existsSync(candidate)) {
62
145
  return candidate
63
146
  }
@@ -71,12 +154,25 @@ function findBinary(startDir) {
71
154
  }
72
155
  }
73
156
 
74
- const resolved = findBinary(scriptDir)
157
+ // Covers layouts the upward walk misses (pnpm's virtual store, yarn PnP).
158
+ function resolveBinary() {
159
+ for (let i = 0; i < candidates.length; i++) {
160
+ try {
161
+ const manifest = require.resolve(candidates[i] + "/package.json")
162
+ const resolved = path.join(path.dirname(manifest), "bin", binary)
163
+ if (fs.existsSync(resolved)) return resolved
164
+ } catch (e) {
165
+ continue
166
+ }
167
+ }
168
+ }
169
+
170
+ const resolved = findBinary(scriptDir) || resolveBinary()
75
171
  if (!resolved) {
76
172
  console.error(
77
173
  'It seems that your package manager failed to install the right version of the nikcli CLI for your platform. You can try manually installing the "' +
78
- base +
79
- '" package',
174
+ candidates[0] +
175
+ '" package, or reinstall with the standalone installer: https://nikcli.store/install',
80
176
  )
81
177
  process.exit(1)
82
178
  }
package/package.json CHANGED
@@ -6,19 +6,19 @@
6
6
  "scripts": {
7
7
  "postinstall": "bun ./postinstall.mjs || node ./postinstall.mjs"
8
8
  },
9
- "version": "1.210.0",
9
+ "version": "1.211.0",
10
10
  "optionalDependencies": {
11
- "nikcli-ai-linux-arm64": "1.210.0",
12
- "nikcli-ai-linux-x64": "1.210.0",
13
- "nikcli-ai-linux-x64-baseline": "1.210.0",
14
- "nikcli-ai-linux-arm64-musl": "1.210.0",
15
- "nikcli-ai-linux-x64-musl": "1.210.0",
16
- "nikcli-ai-linux-x64-baseline-musl": "1.210.0",
17
- "nikcli-ai-darwin-arm64": "1.210.0",
18
- "nikcli-ai-darwin-x64": "1.210.0",
19
- "nikcli-ai-darwin-x64-baseline": "1.210.0",
20
- "nikcli-ai-windows-arm64": "1.210.0",
21
- "nikcli-ai-windows-x64": "1.210.0",
22
- "nikcli-ai-windows-x64-baseline": "1.210.0"
11
+ "nikcli-ai-linux-arm64": "1.211.0",
12
+ "nikcli-ai-linux-x64": "1.211.0",
13
+ "nikcli-ai-linux-x64-baseline": "1.211.0",
14
+ "nikcli-ai-linux-arm64-musl": "1.211.0",
15
+ "nikcli-ai-linux-x64-musl": "1.211.0",
16
+ "nikcli-ai-linux-x64-baseline-musl": "1.211.0",
17
+ "nikcli-ai-darwin-arm64": "1.211.0",
18
+ "nikcli-ai-darwin-x64": "1.211.0",
19
+ "nikcli-ai-darwin-x64-baseline": "1.211.0",
20
+ "nikcli-ai-windows-arm64": "1.211.0",
21
+ "nikcli-ai-windows-x64": "1.211.0",
22
+ "nikcli-ai-windows-x64-baseline": "1.211.0"
23
23
  }
24
24
  }
package/postinstall.mjs CHANGED
@@ -1,144 +1,216 @@
1
1
  #!/usr/bin/env node
2
2
 
3
+ // Materializes the correct per-platform nikcli binary next to the `bin/nikcli`
4
+ // wrapper so the wrapper can exec it directly.
5
+ //
6
+ // This is best-effort by design: the wrapper resolves the binary at runtime too,
7
+ // so a failure here (pnpm without postinstall, --ignore-scripts, yarn PnP, npm's
8
+ // optionalDependencies bug) must never fail the install. We exit 0 and let the
9
+ // wrapper try again later.
10
+
3
11
  import fs from "fs"
4
- import path from "path"
5
12
  import os from "os"
6
- import { execSync } from "child_process"
7
- import { fileURLToPath } from "url"
13
+ import path from "path"
14
+ import childProcess from "child_process"
8
15
  import { createRequire } from "module"
16
+ import { fileURLToPath } from "url"
9
17
 
10
18
  const __dirname = path.dirname(fileURLToPath(import.meta.url))
11
19
  const require = createRequire(import.meta.url)
12
20
 
13
- function detectPlatformAndArch() {
14
- let platform
15
- switch (os.platform()) {
16
- case "darwin":
17
- platform = "darwin"
18
- break
19
- case "linux":
20
- platform = "linux"
21
- break
22
- case "win32":
23
- platform = "windows"
24
- break
25
- default:
26
- platform = os.platform()
27
- break
28
- }
21
+ // Published layout puts this file at the package root; in the repo it lives in
22
+ // `script/`. Resolve the package root from whichever one actually has a manifest.
23
+ const packageRoot = fs.existsSync(path.join(__dirname, "package.json")) ? __dirname : path.join(__dirname, "..")
24
+ const packageJson = JSON.parse(fs.readFileSync(path.join(packageRoot, "package.json"), "utf8"))
29
25
 
30
- let arch
31
- switch (os.arch()) {
32
- case "x64":
33
- arch = "x64"
34
- break
35
- case "arm64":
36
- arch = "arm64"
37
- break
38
- default:
39
- arch = os.arch()
40
- break
41
- }
26
+ const platformMap = { darwin: "darwin", linux: "linux", win32: "windows" }
27
+ const archMap = { x64: "x64", arm64: "arm64", arm: "arm" }
42
28
 
43
- return { platform, arch }
44
- }
29
+ const platform = platformMap[os.platform()] ?? os.platform()
30
+ const arch = archMap[os.arch()] ?? os.arch()
31
+ const base = `nikcli-ai-${platform}-${arch}`
32
+ // Bun's `--compile` appends `.exe` on Windows targets, so that is the name inside
33
+ // the per-platform package on Windows and the bare name everywhere else.
34
+ const sourceBinary = platform === "windows" ? "nikcli.exe" : "nikcli"
35
+ // Never overwrite `bin/nikcli` — that file IS the wrapper this package declares
36
+ // in its `bin` field. We drop the native binary beside it instead.
37
+ const targetBinary = path.join(packageRoot, "bin", platform === "windows" ? "nikcli.exe" : "nikcli-bin")
45
38
 
46
- function detectMusl() {
47
- if (os.platform() !== "linux") return false
48
- try {
49
- const ldd = execSync("ldd --version 2>&1", { encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] })
50
- if (ldd.toLowerCase().includes("musl")) return true
51
- } catch {}
52
- return fs.existsSync("/etc/alpine-release")
53
- }
39
+ function supportsAvx2() {
40
+ if (arch !== "x64") return false
54
41
 
55
- function detectBaseline() {
56
- if (os.platform() === "linux") {
42
+ if (platform === "linux") {
57
43
  try {
58
- const cpuinfo = fs.readFileSync("/proc/cpuinfo", "utf8")
59
- return !cpuinfo.includes("avx2")
60
- } catch {}
61
- return false
44
+ return /(^|\s)avx2(\s|$)/i.test(fs.readFileSync("/proc/cpuinfo", "utf8"))
45
+ } catch {
46
+ return false
47
+ }
62
48
  }
63
- if (os.platform() === "darwin") {
49
+
50
+ if (platform === "darwin") {
64
51
  try {
65
- const avx2 = execSync("sysctl -n hw.optional.avx2_0 2>/dev/null || echo 0", { encoding: "utf8" }).trim()
66
- return avx2 !== "1"
67
- } catch {}
52
+ const result = childProcess.spawnSync("sysctl", ["-n", "hw.optional.avx2_0"], {
53
+ encoding: "utf8",
54
+ timeout: 1500,
55
+ })
56
+ if (result.status !== 0) return false
57
+ return (result.stdout || "").trim() === "1"
58
+ } catch {
59
+ return false
60
+ }
61
+ }
62
+
63
+ if (platform === "windows") {
64
+ // PF_AVX2_INSTRUCTIONS_AVAILABLE == 40
65
+ const command =
66
+ '(Add-Type -MemberDefinition "[DllImport(""kernel32.dll"")] public static extern bool IsProcessorFeaturePresent(int ProcessorFeature);" -Name Kernel32 -Namespace Win32 -PassThru)::IsProcessorFeaturePresent(40)'
67
+
68
+ for (const executable of ["powershell.exe", "pwsh.exe", "pwsh", "powershell"]) {
69
+ try {
70
+ const result = childProcess.spawnSync(executable, ["-NoProfile", "-NonInteractive", "-Command", command], {
71
+ encoding: "utf8",
72
+ timeout: 3000,
73
+ windowsHide: true,
74
+ })
75
+ if (result.status !== 0) continue
76
+ const output = (result.stdout || "").trim().toLowerCase()
77
+ if (output === "true" || output === "1") return true
78
+ if (output === "false" || output === "0") return false
79
+ } catch {
80
+ continue
81
+ }
82
+ }
68
83
  }
84
+
69
85
  return false
70
86
  }
71
87
 
72
- function tryResolvePackage(packageName, binaryName) {
88
+ function isMusl() {
89
+ if (platform !== "linux") return false
90
+
91
+ try {
92
+ if (fs.existsSync("/etc/alpine-release")) return true
93
+ } catch {
94
+ // Ignore filesystem probes blocked by the host.
95
+ }
96
+
73
97
  try {
74
- const packageJsonPath = require.resolve(`${packageName}/package.json`)
75
- const packageDir = path.dirname(packageJsonPath)
76
- const binaryPath = path.join(packageDir, "bin", binaryName)
77
- if (!fs.existsSync(binaryPath)) return null
78
- return binaryPath
98
+ const result = childProcess.spawnSync("ldd", ["--version"], { encoding: "utf8" })
99
+ return `${result.stdout || ""}${result.stderr || ""}`.toLowerCase().includes("musl")
79
100
  } catch {
80
- return null
101
+ return false
81
102
  }
82
103
  }
83
104
 
84
- function findBinary() {
85
- const { platform, arch } = detectPlatformAndArch()
86
- const binaryName = platform === "windows" ? "nikcli.exe" : "nikcli"
87
- const isMusl = platform === "linux" ? detectMusl() : false
88
- const isBaseline = arch === "x64" ? detectBaseline() : false
89
-
90
- // Try in order: most specific → least specific
91
- const candidates = []
92
-
93
- if (isBaseline && isMusl) candidates.push(`nikcli-ai-${platform}-${arch}-baseline-musl`)
94
- if (isBaseline && !isMusl) candidates.push(`nikcli-ai-${platform}-${arch}-baseline`)
95
- if (!isBaseline && isMusl) candidates.push(`nikcli-ai-${platform}-${arch}-musl`)
96
- candidates.push(`nikcli-ai-${platform}-${arch}`)
97
- // Final fallbacks (skip musl/baseline restriction)
98
- if (isMusl) candidates.push(`nikcli-ai-${platform}-${arch}`)
99
- if (isBaseline) candidates.push(`nikcli-ai-${platform}-${arch}`)
100
-
101
- for (const name of candidates) {
102
- const found = tryResolvePackage(name, binaryName)
103
- if (found) {
104
- console.log(`nikcli: resolved binary from ${name}`)
105
- return { binaryPath: found, binaryName }
105
+ // Ordered best → worst. A baseline binary runs on an AVX2 machine (just slower),
106
+ // so it stays in the list as a fallback; the reverse is a SIGILL crash, which is
107
+ // why the baseline probe must be right on every platform.
108
+ function packageNames() {
109
+ const baseline = arch === "x64" && !supportsAvx2()
110
+
111
+ if (platform === "linux") {
112
+ if (isMusl()) {
113
+ if (arch === "x64")
114
+ return baseline
115
+ ? [`${base}-baseline-musl`, `${base}-musl`, `${base}-baseline`, base]
116
+ : [`${base}-musl`, `${base}-baseline-musl`, base, `${base}-baseline`]
117
+ return [`${base}-musl`, base]
106
118
  }
119
+
120
+ if (arch === "x64")
121
+ return baseline
122
+ ? [`${base}-baseline`, base, `${base}-baseline-musl`, `${base}-musl`]
123
+ : [base, `${base}-baseline`, `${base}-musl`, `${base}-baseline-musl`]
124
+ return [base, `${base}-musl`]
107
125
  }
108
126
 
109
- throw new Error(
110
- `Could not find nikcli binary for ${platform}-${arch}${isMusl ? " (musl)" : ""}${isBaseline ? " (baseline)" : ""}.\n` +
111
- `Tried: ${candidates.join(", ")}\n` +
112
- `Install manually: https://nikcli.store/install`,
113
- )
127
+ if (arch === "x64") return baseline ? [`${base}-baseline`, base] : [base, `${base}-baseline`]
128
+ return [base]
129
+ }
130
+
131
+ function resolveBinary(name) {
132
+ const packageJsonPath = require.resolve(`${name}/package.json`)
133
+ const binaryPath = path.join(path.dirname(packageJsonPath), "bin", sourceBinary)
134
+ if (!fs.existsSync(binaryPath)) throw new Error(`Binary not found at ${binaryPath}`)
135
+ return binaryPath
136
+ }
137
+
138
+ function copyBinary(source, target) {
139
+ if (!fs.existsSync(source)) throw new Error(`Binary not found at ${source}`)
140
+ fs.mkdirSync(path.dirname(target), { recursive: true })
141
+ if (fs.existsSync(target)) fs.unlinkSync(target)
142
+ try {
143
+ // Hardlink when we can: no second copy of a ~100MB binary on disk.
144
+ fs.linkSync(source, target)
145
+ } catch {
146
+ fs.copyFileSync(source, target)
147
+ }
148
+ fs.chmodSync(target, 0o755)
114
149
  }
115
150
 
116
- function prepareBinDirectory(binaryName) {
117
- const binDir = path.join(__dirname, "..", "bin")
118
- const targetPath = path.join(binDir, binaryName)
119
- if (!fs.existsSync(binDir)) fs.mkdirSync(binDir, { recursive: true })
120
- if (fs.existsSync(targetPath)) fs.unlinkSync(targetPath)
121
- return { binDir, targetPath }
151
+ // npm has a long-standing bug where optionalDependencies are skipped when a
152
+ // lockfile was produced on another platform. Fetching the one package we need
153
+ // into a temp prefix recovers from it without touching the user's tree.
154
+ function installPackage(name) {
155
+ const version = packageJson.optionalDependencies?.[name]
156
+ if (!version) return false
157
+
158
+ const temp = fs.mkdtempSync(path.join(os.tmpdir(), "nikcli-install-"))
159
+ try {
160
+ const result = childProcess.spawnSync(
161
+ "npm",
162
+ ["install", "--ignore-scripts", "--no-save", "--loglevel=error", "--prefix", temp, `${name}@${version}`],
163
+ // npm is npm.cmd on Windows, so it needs a shell to be spawnable.
164
+ // Output stays quiet: we print our own diagnostic if every candidate fails.
165
+ { stdio: "ignore", windowsHide: true, shell: process.platform === "win32" },
166
+ )
167
+ if (result.status !== 0) return false
168
+ copyBinary(path.join(temp, "node_modules", name, "bin", sourceBinary), targetBinary)
169
+ return true
170
+ } catch {
171
+ return false
172
+ } finally {
173
+ fs.rmSync(temp, { recursive: true, force: true })
174
+ }
122
175
  }
123
176
 
124
- function symlinkBinary(sourcePath, binaryName) {
125
- const { targetPath } = prepareBinDirectory(binaryName)
126
- fs.symlinkSync(sourcePath, targetPath)
127
- if (!fs.existsSync(targetPath)) throw new Error(`Failed to symlink binary to ${targetPath}`)
128
- console.log(`nikcli: ${targetPath} -> ${sourcePath}`)
177
+ function verifyBinary() {
178
+ const result = childProcess.spawnSync(targetBinary, ["--version"], {
179
+ encoding: "utf8",
180
+ stdio: "ignore",
181
+ windowsHide: true,
182
+ timeout: 30000,
183
+ })
184
+ return result.status === 0
129
185
  }
130
186
 
131
- async function main() {
132
- if (os.platform() === "win32") {
133
- console.log("nikcli: Windows detected, skipping postinstall symlink")
134
- return
187
+ function main() {
188
+ const names = packageNames()
189
+ for (const name of names) {
190
+ try {
191
+ copyBinary(resolveBinary(name), targetBinary)
192
+ if (verifyBinary()) {
193
+ console.log(`nikcli: using ${name}`)
194
+ return
195
+ }
196
+ } catch {
197
+ if (installPackage(name) && verifyBinary()) {
198
+ console.log(`nikcli: using ${name} (fetched separately)`)
199
+ return
200
+ }
201
+ }
135
202
  }
136
203
 
137
- const { binaryPath, binaryName } = findBinary()
138
- symlinkBinary(binaryPath, binaryName)
204
+ throw new Error(
205
+ `could not resolve a native binary for ${platform}-${arch}. Tried: ${names.join(", ")}.\n` +
206
+ `The 'nikcli' command will try again at runtime. To fix it now, install one of those packages manually,\n` +
207
+ `or use the standalone installer: https://nikcli.store/install`,
208
+ )
139
209
  }
140
210
 
141
- main().catch((error) => {
142
- console.error("nikcli postinstall failed:", error.message)
143
- process.exit(1)
144
- })
211
+ try {
212
+ main()
213
+ } catch (error) {
214
+ // Non-fatal on purpose: the wrapper resolves the binary at runtime as well.
215
+ console.warn(`nikcli postinstall: ${error.message}`)
216
+ }