+76
-27
@@ -51,3 +51,3 @@ #!/usr/bin/env bun | ||
| import { spawn, spawnSync } from 'node:child_process' | ||
| import { isAbsolute, join, resolve } from 'node:path' | ||
| import { isAbsolute, join, resolve, relative, sep } from 'node:path' | ||
| import { pathToFileURL } from 'node:url' | ||
@@ -57,3 +57,29 @@ | ||
| // Marker passed to a re-exec'd child so it doesn't re-load env files the | ||
| // parent already applied (the child inherits process.env, so a second load | ||
| // would re-emit "not found" warnings and re-apply the same values). | ||
| const ENV_LOADED_MARKER = 'TEKIR_ENV_LOADED' | ||
| /** | ||
| * Resolve an entry path against cwd and confine it under cwd. The entry is | ||
| * `import()`-executed, and it can come from `--entry` or `package.json` | ||
| * (`tekir.entry`), so a value pointing outside the project (e.g. | ||
| * `../../evil.ts`) must be rejected rather than run. | ||
| * | ||
| * @param {string} entry - The raw entry path. | ||
| * @returns {string} The absolute, confined entry path. | ||
| */ | ||
| function resolveEntryPath(entry) { | ||
| const cwd = process.cwd() | ||
| const abs = isAbsolute(entry) ? entry : resolve(cwd, entry) | ||
| const rel = relative(cwd, abs) | ||
| if (rel === '' || rel.startsWith('..' + sep) || rel === '..' || isAbsolute(rel)) { | ||
| console.error(`[tekir] Refusing to load entry outside the project directory: ${entry}`) | ||
| console.error(` The entry must live under ${cwd}.`) | ||
| process.exit(1) | ||
| } | ||
| return abs | ||
| } | ||
| /** | ||
| * Pull `--entry`, `--envfile`, and `--env-file` (multi for the env | ||
@@ -157,8 +183,12 @@ * variants) out of argv. Everything else stays in `rest` in original | ||
| let value = line.slice(eq + 1).trim() | ||
| if (value.length >= 2) { | ||
| const f = value[0] | ||
| const l = value[value.length - 1] | ||
| if ((f === '"' && l === '"') || (f === "'" && l === "'")) { | ||
| value = value.slice(1, -1) | ||
| } | ||
| if (value.length >= 2 && ((value[0] === '"' && value[value.length - 1] === '"') || | ||
| (value[0] === "'" && value[value.length - 1] === "'"))) { | ||
| // Quoted value: take the content verbatim (a `#` inside quotes is part | ||
| // of the value, not a comment). | ||
| value = value.slice(1, -1) | ||
| } else { | ||
| // Unquoted value: strip a trailing inline `# comment` (must be preceded | ||
| // by whitespace so a `#` inside a token like `pa#ss` stays intact). | ||
| const hashIdx = value.search(/\s#/) | ||
| if (hashIdx !== -1) value = value.slice(0, hashIdx).trimEnd() | ||
| } | ||
@@ -251,3 +281,3 @@ if (!shellKeys.has(key)) process.env[key] = value | ||
| async function runEntry(entry, command, args) { | ||
| const absEntry = isAbsolute(entry) ? entry : resolve(process.cwd(), entry) | ||
| const absEntry = resolveEntryPath(entry) | ||
| if (!existsSync(absEntry)) { | ||
@@ -308,5 +338,9 @@ console.error(`[tekir] Entry file not found: ${absEntry}`) | ||
| const allEnvFiles = [...jsonEnvFiles, ...cliEnvFiles] | ||
| if (allEnvFiles.length > 0) { | ||
| // Skip loading when a parent process already did it (re-exec under Bun for | ||
| // `tekir build`). The child inherits process.env, so re-loading would only | ||
| // re-emit "not found" warnings and re-apply identical values. | ||
| if (allEnvFiles.length > 0 && !process.env[ENV_LOADED_MARKER]) { | ||
| const shellKeys = new Set(Object.keys(process.env)) | ||
| for (const f of allEnvFiles) loadEnvFile(f, shellKeys) | ||
| process.env[ENV_LOADED_MARKER] = '1' | ||
| } | ||
@@ -349,3 +383,3 @@ | ||
| const rest = argv.slice(1) | ||
| const absEntry = isAbsolute(entry) ? entry : resolve(process.cwd(), entry) | ||
| const absEntry = resolveEntryPath(entry) | ||
| let result = null | ||
@@ -374,20 +408,33 @@ try { | ||
| const buildEntryPath = join(dirname(absEntry), `.tekir-build-entry-${process.pid}-${Date.now()}.ts`) | ||
| writeFileSync(buildEntryPath, result.source) | ||
| // The in-app build dispatcher calls `process.exit(0)` once | ||
| // `Bun.build` resolves, which short-circuits any `try/finally` | ||
| // unlink we'd schedule below. `process.on('exit')` runs | ||
| // synchronously at the very end of the exit sequence and is | ||
| // allowed to do filesystem work, so the temp file goes away on | ||
| // every successful build. The handler also covers a non-zero exit | ||
| // (build failure) since `process.exit(N)` fires the same event. | ||
| process.on('exit', () => { try { unlinkSync(buildEntryPath) } catch {} }) | ||
| // `process.argv[1]` MUST stay on the original entry so the in-app | ||
| // build dispatcher bundles the real file. We import the temp file | ||
| // only to fire the `tekir({...})` call. | ||
| process.argv = [process.argv[0], absEntry, command, ...rest] | ||
| let wrote = false | ||
| try { | ||
| await import(pathToFileURL(buildEntryPath).href) | ||
| } finally { | ||
| try { unlinkSync(buildEntryPath) } catch {} | ||
| writeFileSync(buildEntryPath, result.source) | ||
| wrote = true | ||
| } catch (err) { | ||
| // A read-only source directory (CI, read-only mount) can't host the | ||
| // temp build entry; fall back to a plain full-entry import instead of | ||
| // crashing mid-build with a raw filesystem error. | ||
| console.warn(`[tekir] Could not write temp build entry (${err.message}); building from the full entry instead.`) | ||
| } | ||
| if (wrote) { | ||
| // The in-app build dispatcher calls `process.exit(0)` once | ||
| // `Bun.build` resolves, which short-circuits any `try/finally` | ||
| // unlink we'd schedule below. `process.on('exit')` runs | ||
| // synchronously at the very end of the exit sequence and is | ||
| // allowed to do filesystem work, so the temp file goes away on | ||
| // every successful build. The handler also covers a non-zero exit | ||
| // (build failure) since `process.exit(N)` fires the same event. | ||
| process.on('exit', () => { try { unlinkSync(buildEntryPath) } catch {} }) | ||
| // `process.argv[1]` MUST stay on the original entry so the in-app | ||
| // build dispatcher bundles the real file. We import the temp file | ||
| // only to fire the `tekir({...})` call. | ||
| process.argv = [process.argv[0], absEntry, command, ...rest] | ||
| try { | ||
| await import(pathToFileURL(buildEntryPath).href) | ||
| } finally { | ||
| try { unlinkSync(buildEntryPath) } catch {} | ||
| } | ||
| } else { | ||
| await runEntry(entry, command, rest) | ||
| } | ||
| } else { | ||
@@ -410,3 +457,5 @@ await runEntry(entry, command, rest) | ||
| const runner = isBun ? 'bun' : 'npx' | ||
| const runnerArgs = isBun ? ['test', ...argv.slice(1)] : ['vitest', 'run', ...argv.slice(1)] | ||
| // `--no-install` so a missing local vitest fails fast instead of npx | ||
| // silently fetching it from the registry (unexpected network / wrong pkg). | ||
| const runnerArgs = isBun ? ['test', ...argv.slice(1)] : ['--no-install', 'vitest', 'run', ...argv.slice(1)] | ||
| const proc = spawn(runner, runnerArgs, { | ||
@@ -413,0 +462,0 @@ stdio: 'inherit', |
+2
-2
| { | ||
| "name": "@tekir/cli", | ||
| "version": "0.1.5", | ||
| "version": "0.1.6", | ||
| "description": "tekir command-line tool: serve, build, generate-key, and provider-registered commands.", | ||
@@ -33,3 +33,3 @@ "author": "dev@tekir.io", | ||
| "dependencies": { | ||
| "@tekir/core": "^0.1.29", | ||
| "@tekir/core": "^0.1.34", | ||
| "oxc-parser": ">=0.30.0" | ||
@@ -36,0 +36,0 @@ }, |
AI-detected potential code anomaly
Supply chain riskAI has identified unusual behaviors that may pose a security risk.
Found 1 instance in 1 package
25081
10.48%463
11.3%9
12.5%Updated