@saluzi/saluzi-edu 0.2.7 → 0.2.9

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@saluzi/saluzi-edu",
3
- "version": "0.2.7",
3
+ "version": "0.2.9",
4
4
  "description": "Saluzi CLI - interactive AI coding assistant in the terminal",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -29,7 +29,6 @@
29
29
  "files": [
30
30
  "dist",
31
31
  "scripts/acp-link.mjs",
32
- "scripts/postinstall.cjs",
33
32
  "scripts/run-parallel.mjs",
34
33
  "packages/remote-control-server/src",
35
34
  "packages/remote-control-server/web/dist",
@@ -58,7 +57,6 @@
58
57
  "check:bundle": "bun run scripts/check-bundle-integrity.ts",
59
58
  "check:unused": "knip-bun",
60
59
  "health": "bun run scripts/health-check.ts",
61
- "postinstall": "node scripts/postinstall.cjs",
62
60
  "docs:dev": "npx mintlify dev",
63
61
  "typecheck": "tsc --noEmit",
64
62
  "precheck": "bun run typecheck && bun run check:fix && bun test --isolate",
@@ -91,7 +89,7 @@
91
89
  "@anthropic-ai/sandbox-runtime": "^0.0.44",
92
90
  "@anthropic-ai/sdk": "^0.81.0",
93
91
  "@anthropic-ai/vertex-sdk": "^0.16.0",
94
- "@saluzi/sa-ink": "^0.1.10",
92
+ "@saluzi/sa-ink": "^0.1.11",
95
93
  "@aws-sdk/client-bedrock": "^3.1037.0",
96
94
  "@aws-sdk/client-bedrock-runtime": "^3.1037.0",
97
95
  "@aws-sdk/client-sts": "^3.1037.0",
@@ -1,580 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * Postinstall script — runs automatically after `bun install` or `npm install`.
4
- *
5
- * Downloads ripgrep binary (idempotent, skips if exists).
6
- * Works in dev mode (src/ exists), published mode (dist/ exists), with bun or node.
7
- *
8
- * Usage:
9
- * node scripts/postinstall.js
10
- * node scripts/postinstall.js --force
11
- * bun run scripts/postinstall.js
12
- */
13
-
14
- const {
15
- existsSync,
16
- mkdirSync,
17
- readFileSync,
18
- renameSync,
19
- rmSync,
20
- statSync,
21
- writeFileSync,
22
- chmodSync,
23
- } = require('fs')
24
- const { spawnSync } = require('child_process')
25
- const { setDefaultResultOrder } = require('node:dns')
26
- const path = require('path')
27
- const os = require('os')
28
-
29
- // Prefer IPv4 first — Bun on Windows sometimes fails GitHub over broken IPv6 paths.
30
- try {
31
- setDefaultResultOrder('ipv4first')
32
- } catch {
33
- /* ignore */
34
- }
35
-
36
- // --- Config ---
37
-
38
- const RG_VERSION = '15.0.1'
39
- const DEFAULT_RELEASE_BASE = `https://github.com/microsoft/ripgrep-prebuilt/releases/download/v${RG_VERSION}`
40
- const MIRROR_RELEASE_BASE = `https://ghproxy.net/https://github.com/microsoft/ripgrep-prebuilt/releases/download/v${RG_VERSION}`
41
- const RELEASE_BASE = (
42
- process.env.RIPGREP_DOWNLOAD_BASE ?? DEFAULT_RELEASE_BASE
43
- ).replace(/\/$/, '')
44
-
45
- const scriptDir = path.dirname(__filename)
46
- const projectRoot = path.resolve(scriptDir, '..')
47
-
48
- // --- Platform mapping ---
49
-
50
- function getPlatformMapping() {
51
- const arch = process.arch
52
- const platform = process.platform
53
-
54
- if (platform === 'darwin') {
55
- if (arch === 'arm64')
56
- return { target: 'aarch64-apple-darwin', ext: 'tar.gz' }
57
- if (arch === 'x64') return { target: 'x86_64-apple-darwin', ext: 'tar.gz' }
58
- throw new Error(`Unsupported macOS arch: ${arch}`)
59
- }
60
-
61
- if (platform === 'win32') {
62
- if (arch === 'x64') return { target: 'x86_64-pc-windows-msvc', ext: 'zip' }
63
- if (arch === 'arm64')
64
- return { target: 'aarch64-pc-windows-msvc', ext: 'zip' }
65
- throw new Error(`Unsupported Windows arch: ${arch}`)
66
- }
67
-
68
- if (platform === 'linux') {
69
- const isMusl = detectMusl()
70
- if (arch === 'x64') {
71
- return { target: 'x86_64-unknown-linux-musl', ext: 'tar.gz' }
72
- }
73
- if (arch === 'arm64') {
74
- return isMusl
75
- ? { target: 'aarch64-unknown-linux-musl', ext: 'tar.gz' }
76
- : { target: 'aarch64-unknown-linux-gnu', ext: 'tar.gz' }
77
- }
78
- throw new Error(`Unsupported Linux arch: ${arch}`)
79
- }
80
-
81
- throw new Error(`Unsupported platform: ${platform}`)
82
- }
83
-
84
- function detectMusl() {
85
- const muslArch = process.arch === 'x64' ? 'x86_64' : 'aarch64'
86
- try {
87
- statSync(`/lib/libc.musl-${muslArch}.so.1`)
88
- return true
89
- } catch {
90
- return false
91
- }
92
- }
93
-
94
- // --- Paths ---
95
-
96
- function getVendorDir() {
97
- if (existsSync(path.join(projectRoot, 'src'))) {
98
- return path.resolve(projectRoot, 'src', 'utils', 'vendor', 'ripgrep')
99
- }
100
- return path.resolve(projectRoot, 'dist', 'vendor', 'ripgrep')
101
- }
102
-
103
- function getBinaryPath() {
104
- const dir = getVendorDir()
105
- const subdir = `${process.arch}-${process.platform}`
106
- const binary = process.platform === 'win32' ? 'rg.exe' : 'rg'
107
- return path.resolve(dir, subdir, binary)
108
- }
109
-
110
- // --- Download helpers ---
111
-
112
- function proxyEnvSet() {
113
- const v = s => (s ?? '').trim()
114
- return !!(
115
- v(process.env.HTTPS_PROXY) ||
116
- v(process.env.HTTP_PROXY) ||
117
- v(process.env.ALL_PROXY) ||
118
- v(process.env.https_proxy) ||
119
- v(process.env.http_proxy)
120
- )
121
- }
122
-
123
- function tryPowerShellDownload(url, dest) {
124
- const u = url.replace(/'/g, "''")
125
- const d = dest.replace(/'/g, "''")
126
- const cmd = `Invoke-WebRequest -Uri '${u}' -OutFile '${d}' -UseBasicParsing`
127
- const result = spawnSync(
128
- 'powershell.exe',
129
- [
130
- '-NoProfile',
131
- '-NonInteractive',
132
- '-ExecutionPolicy',
133
- 'Bypass',
134
- '-Command',
135
- cmd,
136
- ],
137
- { stdio: 'pipe', windowsHide: true },
138
- )
139
- return result.status === 0 && existsSync(dest) && statSync(dest).size > 0
140
- }
141
-
142
- function tryCurlDownload(url, dest) {
143
- const curl = process.platform === 'win32' ? 'curl.exe' : 'curl'
144
- const result = spawnSync(curl, ['-fsSL', '-L', '--fail', '-o', dest, url], {
145
- stdio: 'pipe',
146
- windowsHide: true,
147
- })
148
- return result.status === 0 && existsSync(dest) && statSync(dest).size > 0
149
- }
150
-
151
- async function fetchRelease(url) {
152
- // Prefer undici when available (installed as optionalDep) — its
153
- // EnvHttpProxyAgent honors HTTPS_PROXY/HTTP_PROXY/NO_PROXY, which Node's
154
- // built-in fetch does NOT. On published installs where undici may be absent
155
- // (--omit=optional, pnpm strict), fall back gracefully.
156
- let undici
157
- try {
158
- undici = require('undici')
159
- } catch {
160
- // undici unavailable — fall through to global fetch + curl/PowerShell
161
- }
162
- if (proxyEnvSet() && undici) {
163
- try {
164
- return await undici.fetch(url, {
165
- redirect: 'follow',
166
- dispatcher: new undici.EnvHttpProxyAgent(),
167
- })
168
- } catch (e) {
169
- // undici fetch failed — fall through to downloadUrlToBufferWithFallback
170
- // which tries curl/PowerShell (those honor system proxy config).
171
- const msg = e instanceof Error ? e.message : String(e)
172
- console.warn(
173
- `[postinstall] undici fetch failed (${msg}); trying curl/PowerShell fallback`,
174
- )
175
- }
176
- }
177
- // Node 18+ has global fetch, Bun has it too. NOTE: global fetch does NOT
178
- // honor HTTPS_PROXY — proxy users without undici must rely on the
179
- // curl/PowerShell fallback in downloadUrlToBufferWithFallback, which DOES
180
- // honor system proxy config (curl on Linux/macOS, winhttp on Windows).
181
- return await fetch(url, { redirect: 'follow' })
182
- }
183
-
184
- async function downloadUrlToBuffer(url) {
185
- const response = await fetchRelease(url)
186
- if (!response.ok) {
187
- throw new Error(
188
- `Download failed: ${response.status} ${response.statusText}`,
189
- )
190
- }
191
- return Buffer.from(await response.arrayBuffer())
192
- }
193
-
194
- async function downloadUrlToBufferWithFallback(url) {
195
- let firstError
196
- try {
197
- return await downloadUrlToBuffer(url)
198
- } catch (e) {
199
- firstError = e
200
- }
201
-
202
- const tmpRoot = path.join(
203
- os.tmpdir(),
204
- `ripgrep-dl-${process.pid}-${Date.now()}`,
205
- )
206
- const tmpFile = path.join(tmpRoot, 'archive')
207
- mkdirSync(tmpRoot, { recursive: true })
208
- try {
209
- if (process.platform === 'win32' && tryPowerShellDownload(url, tmpFile)) {
210
- return readFileSync(tmpFile)
211
- }
212
- if (tryCurlDownload(url, tmpFile)) {
213
- return readFileSync(tmpFile)
214
- }
215
- } finally {
216
- rmSync(tmpRoot, { recursive: true, force: true })
217
- }
218
-
219
- throw firstError
220
- }
221
-
222
- // --- Extract ---
223
-
224
- function findZipEntryKey(files, want) {
225
- return Object.keys(files).find(k => {
226
- const norm = k.replace(/\\/g, '/')
227
- return norm === want || norm.endsWith(`/${want}`)
228
- })
229
- }
230
-
231
- async function extractZip(buffer, binaryPath, extractedBinary) {
232
- const binaryDir = path.dirname(binaryPath)
233
- // Try fflate first (bundled dep)
234
- let fflateError
235
- try {
236
- const { unzipSync } = require('fflate')
237
- const unzipped = unzipSync(new Uint8Array(buffer))
238
- const key = findZipEntryKey(unzipped, extractedBinary)
239
- if (!key) {
240
- throw new Error(`Binary ${extractedBinary} not found in zip`)
241
- }
242
- writeFileSync(binaryPath, Buffer.from(unzipped[key]))
243
- return
244
- } catch (e) {
245
- fflateError = e
246
- }
247
-
248
- // Fallback: PowerShell Expand-Archive or unzip CLI
249
- const tmpDir = path.join(binaryDir, '.tmp-download')
250
- rmSync(tmpDir, { recursive: true, force: true })
251
- mkdirSync(tmpDir, { recursive: true })
252
- try {
253
- const assetName = `archive.zip`
254
- const archivePath = path.join(tmpDir, assetName)
255
- writeFileSync(archivePath, buffer)
256
-
257
- let extracted = false
258
- if (process.platform === 'win32') {
259
- const psCmd = `Expand-Archive -Path '${archivePath.replace(/'/g, "''")}' -DestinationPath '${tmpDir.replace(/'/g, "''")}' -Force`
260
- const psResult = spawnSync(
261
- 'powershell.exe',
262
- [
263
- '-NoProfile',
264
- '-NonInteractive',
265
- '-ExecutionPolicy',
266
- 'Bypass',
267
- '-Command',
268
- psCmd,
269
- ],
270
- { stdio: 'pipe', windowsHide: true },
271
- )
272
- if (psResult.status === 0) {
273
- extracted = true
274
- }
275
- }
276
-
277
- if (!extracted) {
278
- const result = spawnSync('unzip', ['-o', archivePath, '-d', tmpDir], {
279
- stdio: 'pipe',
280
- })
281
- if (result.status !== 0) {
282
- const unzipErr = result.stderr?.toString().trim() || 'command not found'
283
- const fflateMsg =
284
- fflateError instanceof Error
285
- ? fflateError.message
286
- : String(fflateError)
287
- throw new Error(
288
- `zip extraction failed (fflate: ${fflateMsg}; unzip: ${unzipErr})`,
289
- )
290
- }
291
- }
292
-
293
- const srcBinary = path.join(tmpDir, extractedBinary)
294
- if (!existsSync(srcBinary)) {
295
- throw new Error(`Binary not found at expected path: ${srcBinary}`)
296
- }
297
- renameSync(srcBinary, binaryPath)
298
- } finally {
299
- rmSync(tmpDir, { recursive: true, force: true })
300
- }
301
- }
302
-
303
- async function extractTarGz(buffer, binaryPath, extractedBinary, assetName) {
304
- const binaryDir = path.dirname(binaryPath)
305
- const tmpDir = path.join(binaryDir, '.tmp-download')
306
- rmSync(tmpDir, { recursive: true, force: true })
307
- mkdirSync(tmpDir, { recursive: true })
308
- try {
309
- const archivePath = path.join(tmpDir, assetName)
310
- writeFileSync(archivePath, buffer)
311
- const result = spawnSync('tar', ['xzf', archivePath, '-C', tmpDir], {
312
- stdio: 'pipe',
313
- })
314
- if (result.status !== 0) {
315
- throw new Error(`tar extract failed: ${result.stderr?.toString()}`)
316
- }
317
- const srcBinary = path.join(tmpDir, extractedBinary)
318
- if (!existsSync(srcBinary)) {
319
- throw new Error(`Binary not found at expected path: ${srcBinary}`)
320
- }
321
- renameSync(srcBinary, binaryPath)
322
- } finally {
323
- rmSync(tmpDir, { recursive: true, force: true })
324
- }
325
- }
326
-
327
- // --- Main ---
328
-
329
- async function downloadAndExtract() {
330
- const { target, ext } = getPlatformMapping()
331
- const assetName = `ripgrep-v${RG_VERSION}-${target}.${ext}`
332
-
333
- const binaryPath = getBinaryPath()
334
- const binaryDir = path.dirname(binaryPath)
335
-
336
- const force = process.argv.includes('--force')
337
- if (!force && existsSync(binaryPath)) {
338
- const stat = statSync(binaryPath)
339
- if (stat.size > 0) {
340
- console.log(`[ripgrep] Binary already exists at ${binaryPath}, skipping.`)
341
- return
342
- }
343
- }
344
-
345
- console.log(`[ripgrep] Downloading v${RG_VERSION} for ${target}...`)
346
-
347
- const extractedBinary = process.platform === 'win32' ? 'rg.exe' : 'rg'
348
-
349
- const mirrors = [RELEASE_BASE]
350
- if (RELEASE_BASE === DEFAULT_RELEASE_BASE.replace(/\/$/, '')) {
351
- mirrors.push(MIRROR_RELEASE_BASE.replace(/\/$/, ''))
352
- }
353
-
354
- let buffer
355
- let lastError
356
- for (const base of mirrors) {
357
- const url = `${base}/${assetName}`
358
- try {
359
- console.log(`[ripgrep] Trying ${url}`)
360
- buffer = await downloadUrlToBufferWithFallback(url)
361
- break
362
- } catch (e) {
363
- console.warn(
364
- `[ripgrep] Download from ${base} failed: ${e instanceof Error ? e.message : e}`,
365
- )
366
- lastError = e
367
- }
368
- }
369
- if (!buffer) {
370
- throw lastError
371
- }
372
-
373
- try {
374
- console.log(`[ripgrep] Downloaded ${Math.round(buffer.length / 1024)} KB`)
375
-
376
- mkdirSync(binaryDir, { recursive: true })
377
-
378
- if (ext === 'tar.gz') {
379
- await extractTarGz(buffer, binaryPath, extractedBinary, assetName)
380
- } else {
381
- await extractZip(buffer, binaryPath, extractedBinary)
382
- }
383
-
384
- if (process.platform !== 'win32') {
385
- chmodSync(binaryPath, 0o755)
386
- }
387
-
388
- console.log(`[ripgrep] Installed to ${binaryPath}`)
389
- } catch (e) {
390
- const msg = e instanceof Error ? e.message : String(e)
391
- const hint =
392
- 'Check network or set HTTPS_PROXY. If GitHub is blocked, set RIPGREP_DOWNLOAD_BASE to a mirror (see script header).'
393
- throw new Error(`${msg} ${hint}`)
394
- }
395
- }
396
-
397
- async function main() {
398
- await downloadAndExtract()
399
- await setupCodegraph()
400
- }
401
-
402
- // --- Codegraph dist/ setup ---
403
- // The workspace package packages/codegraph/ is a stub (only package.json).
404
- // The published @saluzi/codegraph on npm has a complete dist/ with bun:sqlite
405
- // compatibility. This step downloads it so require('@saluzi/codegraph') works.
406
-
407
- const CODEGRAPH_PKG = '@saluzi/codegraph'
408
- const CODEGRAPH_METADATA_FILE = '.dist-metadata.json'
409
-
410
- function getCodegraphVersion() {
411
- // Read from root package.json dependencies, strip semver range prefix (^~>=)
412
- try {
413
- const pkgPath = path.join(projectRoot, 'package.json')
414
- const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'))
415
- const version = pkg.dependencies?.[CODEGRAPH_PKG]
416
- if (version) {
417
- return version.replace(/^[\^~>=<]+/, '')
418
- }
419
- } catch {
420
- // fallback
421
- }
422
- return '0.1.0'
423
- }
424
-
425
- function getCodegraphDistIndex() {
426
- return path.join(projectRoot, 'packages', 'codegraph', 'dist', 'index.js')
427
- }
428
-
429
- function getCodegraphMetadataPath() {
430
- return path.join(
431
- projectRoot,
432
- 'packages',
433
- 'codegraph',
434
- CODEGRAPH_METADATA_FILE,
435
- )
436
- }
437
-
438
- function readCodegraphMetadata() {
439
- try {
440
- const metaPath = getCodegraphMetadataPath()
441
- if (existsSync(metaPath)) {
442
- return JSON.parse(readFileSync(metaPath, 'utf8'))
443
- }
444
- } catch {
445
- // corrupted or unreadable
446
- }
447
- return null
448
- }
449
-
450
- function writeCodegraphMetadata(metadata) {
451
- const metaPath = getCodegraphMetadataPath()
452
- writeFileSync(metaPath, JSON.stringify(metadata, null, 2) + '\n')
453
- }
454
-
455
- async function setupCodegraph() {
456
- const distIndex = getCodegraphDistIndex()
457
- const force = process.argv.includes('--force')
458
- const requiredVersion = getCodegraphVersion()
459
-
460
- // Check if dist/ exists
461
- if (!force && existsSync(distIndex)) {
462
- // Read metadata to check version
463
- const metadata = readCodegraphMetadata()
464
-
465
- // If no metadata, assume it's a local build — don't overwrite
466
- if (!metadata) {
467
- console.log(
468
- `[codegraph] dist/ exists without metadata (local build?), skipping.`,
469
- )
470
- return
471
- }
472
-
473
- // If metadata indicates local build, don't overwrite
474
- if (metadata.source === 'local') {
475
- console.log(`[codegraph] dist/ is a local build, skipping.`)
476
- return
477
- }
478
-
479
- // If version matches, skip
480
- if (metadata.version === requiredVersion) {
481
- console.log(`[codegraph] dist/ already at v${requiredVersion}, skipping.`)
482
- return
483
- }
484
-
485
- // Version mismatch — need to update
486
- console.log(
487
- `[codegraph] dist/ version mismatch: installed ${metadata.version}, required ${requiredVersion}`,
488
- )
489
- }
490
-
491
- console.log(
492
- `[codegraph] Fetching ${CODEGRAPH_PKG}@${requiredVersion} from npm registry...`,
493
- )
494
-
495
- // 1. Resolve tarball URL from npm registry
496
- const registryUrl = `https://registry.npmjs.org/${CODEGRAPH_PKG}/${requiredVersion}`
497
- let tarballUrl
498
- try {
499
- const resp = await fetchRelease(registryUrl)
500
- if (!resp.ok) {
501
- throw new Error(`${resp.status} ${resp.statusText}`)
502
- }
503
- const meta = await resp.json()
504
- tarballUrl = meta.dist?.tarball
505
- if (!tarballUrl) {
506
- throw new Error('no tarball URL in registry response')
507
- }
508
- } catch (e) {
509
- const msg = e instanceof Error ? e.message : String(e)
510
- console.warn(
511
- `[codegraph] Failed to resolve tarball URL: ${msg} (non-fatal, codegraph graph features will be unavailable)`,
512
- )
513
- return
514
- }
515
-
516
- // 2. Download tarball
517
- let buffer
518
- try {
519
- buffer = await downloadUrlToBufferWithFallback(tarballUrl)
520
- } catch (e) {
521
- const msg = e instanceof Error ? e.message : String(e)
522
- console.warn(
523
- `[codegraph] Download failed: ${msg} (non-fatal, codegraph graph features will be unavailable)`,
524
- )
525
- return
526
- }
527
-
528
- // 3. Extract package/dist/ → packages/codegraph/dist/
529
- const tmpDir = path.join(os.tmpdir(), `codegraph-setup-${process.pid}`)
530
- mkdirSync(tmpDir, { recursive: true })
531
- try {
532
- const tgzPath = path.join(tmpDir, 'package.tgz')
533
- writeFileSync(tgzPath, buffer)
534
- const result = spawnSync('tar', ['xzf', tgzPath, '-C', tmpDir], {
535
- stdio: 'pipe',
536
- })
537
- if (result.status !== 0) {
538
- throw new Error(
539
- `tar extract failed: ${result.stderr?.toString() || 'unknown error'}`,
540
- )
541
- }
542
-
543
- const extractedDist = path.join(tmpDir, 'package', 'dist')
544
- if (!existsSync(extractedDist)) {
545
- throw new Error('extracted package has no dist/ directory')
546
- }
547
-
548
- const targetDist = path.join(projectRoot, 'packages', 'codegraph', 'dist')
549
- rmSync(targetDist, { recursive: true, force: true })
550
- renameSync(extractedDist, targetDist)
551
-
552
- // Write metadata to track installed version
553
- writeCodegraphMetadata({
554
- version: requiredVersion,
555
- source: 'npm',
556
- installedAt: new Date().toISOString(),
557
- })
558
-
559
- console.log(
560
- `[codegraph] Installed dist/ to ${targetDist} (${CODEGRAPH_PKG}@${requiredVersion})`,
561
- )
562
- } catch (e) {
563
- const msg = e instanceof Error ? e.message : String(e)
564
- console.warn(
565
- `[codegraph] Setup failed: ${msg} (non-fatal, codegraph graph features will be unavailable)`,
566
- )
567
- } finally {
568
- rmSync(tmpDir, { recursive: true, force: true })
569
- }
570
- }
571
-
572
- main().catch(error => {
573
- const msg = error instanceof Error ? error.message : String(error)
574
- console.error(`[postinstall] ripgrep download failed (non-fatal): ${msg}`)
575
- console.error(
576
- `[postinstall] You can install ripgrep manually: https://github.com/BurntSushi/ripgrep#installation`,
577
- )
578
- // Never exit with error code — postinstall must not break install
579
- process.exit(0)
580
- })