Big News: Socket raises $60M Series C at a $1B valuation to secure software supply chains for AI-driven development.Announcement
Sign In

@tekir/cli

Package Overview
Dependencies
Maintainers
1
Versions
7
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@tekir/cli - npm Package Compare versions

Comparing version
0.1.1
to
0.1.2
+157
-102
bin/tekir.mjs

@@ -1,9 +0,20 @@

#!/usr/bin/env node
// tekir CLI bin. Runs under both Node and Bun: `serve` and any
// provider-registered command import the user's entry in-process via
// `await import(pathToFileURL(...))`, so they work on whichever runtime
// is invoking the bin. `build` requires `Bun.build`; on Node we re-exec
// the same script under `bun` (or print an actionable error if Bun is
// not on PATH).
#!/usr/bin/env bun
// tekir CLI bin. Shipped with a Bun shebang so the bin shim
// (npm/bun-generated, including Bun's `tekir.bunx` hint on Windows)
// routes through the Bun runtime by default. Bun is required because
// `tekir build` runs `Bun.build` directly, and because Bun is
// position-strict on its own `--env-file` flag — `--env-file=...`
// tokens after the script path pass through to argv where this bin
// can filter them, while Node's runtime greedily parses the same flag
// everywhere in argv and hard-errors on a missing file before any user
// code runs.
//
// The bin file itself is plain Node-compatible ESM, so a Node-only user
// without Bun on PATH can still invoke it directly:
// `node node_modules/@tekir/cli/bin/tekir.mjs --envfile=path serve`
// On that path the `--envfile` (no hyphen) alias bypasses Node's CLI
// parser; `--env-file` (hyphenated) is intercepted by Node's own
// runtime flag and is therefore Bun-only in practice. `tekir build`
// always requires Bun no matter how the bin is invoked.
//
// Entry resolution order (single rule, applies to every command):

@@ -19,10 +30,21 @@ // 1. `--entry <path>` flag, anywhere in argv.

//
// `--env-file <path>` (multi) is forwarded to the underlying runtime as
// its native `--env-file` flag, not parsed by tekir. The bin filters out
// missing files (with a warning) before forwarding, so monorepo dev
// scripts that chain optional per-package `.env` files work even when
// some of them don't exist yet. For non-watch in-process commands the
// bin re-execs itself once under the runtime to apply the flags; for
// `serve --dev` the existing `--watch` spawn already covers the same
// surface in a single hop.
// Env files load in two ways, both feeding the same manual dotenv loader:
// 1. `tekir.envFiles` (string array) in the cwd's `package.json` —
// recommended for monorepo dev scripts that need many `.env` files
// because the paths never enter the bin's argv.
// 2. `--env-file <path>` (multi) on the command line — convenient for
// one-off invocations.
//
// We deliberately do NOT defer to the runtime's native `--env-file`
// flag. Node 20.6+'s `--env-file` greedily parses the flag everywhere in
// argv (including after the script path) AND hard-errors on missing
// files — the combination breaks the moment a single optional `.env` in
// a monorepo dev script is absent, even when this bin would otherwise
// filter it out, because Node bails during its own argv parsing phase
// before the bin even runs. That failure mode is also why the
// recommended path for many `.env` files is `tekir.envFiles` in
// `package.json`: paths kept out of argv never trigger Node's parser.
// Bun is position-strict and silently skips missing files, but a tekir
// bin running under either runtime needs to behave the same way, so we
// own the loader.
import { existsSync, readFileSync } from 'node:fs'

@@ -36,6 +58,14 @@ import { spawn, spawnSync } from 'node:child_process'

/**
* Pull `--entry <path>` / `--entry=path` and `--env-file <path>` /
* `--env-file=<path>` (multi) out of argv. Everything else stays in
* `rest` in original order so the in-app dispatcher and Bun.build see
* only their own flags.
* Pull `--entry`, `--envfile`, and `--env-file` (multi for the env
* variants) out of argv. Everything else stays in `rest` in original
* order so the in-app dispatcher and Bun.build see only their own flags.
*
* `--envfile` (no hyphen) is the recommended Node-host-safe spelling.
* Node's CLI parser does not recognize the un-hyphenated form and lets
* it pass through to the script's argv even when it appears after the
* script path. The hyphenated `--env-file` is also accepted and routes
* to the same loader, but on Node hosts Node's own `--env-file` parser
* intercepts it first and hard-errors on a missing file before the bin
* even runs — so it works mainly for Bun-direct invocations where the
* runtime is position-strict and missing-tolerant.
*/

@@ -66,6 +96,6 @@ function extractTekirFlags(argv) {

}
if (tok === '--env-file') {
if (tok === '--env-file' || tok === '--envfile') {
const next = argv[i + 1]
if (next === undefined || next.startsWith('--')) {
console.error('[tekir] --env-file requires a value.')
console.error(`[tekir] ${tok} requires a value.`)
process.exit(1)

@@ -77,6 +107,7 @@ }

}
if (tok.startsWith('--env-file=')) {
const v = tok.slice('--env-file='.length)
if (tok.startsWith('--env-file=') || tok.startsWith('--envfile=')) {
const eq = tok.indexOf('=')
const v = tok.slice(eq + 1)
if (!v) {
console.error('[tekir] --env-file requires a value.')
console.error(`[tekir] ${tok.slice(0, eq)} requires a value.`)
process.exit(1)

@@ -93,43 +124,67 @@ }

/**
* Strip `--env-file` flags from argv when we're about to re-spawn ourselves
* under the runtime: the runtime applies them as native flags before
* loading our bin, so the bin must not see them again or it would loop.
* Minimal `.env` loader. Matches the dotenv conventions Bun and Node use
* for their own `--env-file` flag:
* - blank lines and `#` comments skipped
* - optional `export KEY=value` prefix
* - single- or double-quoted values are unwrapped
* - shell-provided env (anything already set when tekir started) wins;
* later files override earlier ones for the same key
*
* Caller passes `shellKeys` (a Set of keys that existed before any file
* load) so the precedence rule is independent of load order.
*
* Missing files are warned and skipped — see the `--env-file` comment at
* the top of this file for why we never delegate this to the runtime.
*/
function stripEnvFileFlags(argv) {
const out = []
for (let i = 0; i < argv.length; i++) {
const t = argv[i]
if (t === '--env-file') { i++; continue }
if (t.startsWith('--env-file=')) continue
out.push(t)
function loadEnvFile(path, shellKeys) {
if (!existsSync(path)) {
console.warn(`[tekir] --env-file '${path}' not found, skipping`)
return
}
return out
let content
try {
content = readFileSync(path, 'utf8')
} catch (err) {
console.warn(`[tekir] Could not read --env-file '${path}': ${err.message}, skipping`)
return
}
for (const rawLine of content.split(/\r?\n/)) {
let line = rawLine.trim()
if (!line || line.startsWith('#')) continue
if (line.startsWith('export ')) line = line.slice('export '.length).trimStart()
const eq = line.indexOf('=')
if (eq === -1) continue
const key = line.slice(0, eq).trim()
if (!key) continue
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 (!shellKeys.has(key)) process.env[key] = value
}
}
/**
* Filter user-supplied env-file paths to those that exist on disk. Both
* Bun and Node 20.6+'s native `--env-file` flag hard-error on a missing
* file, which would break monorepos that chain optional per-package
* `.env` files. Warn for each absent file so typos still surface.
* Read the `tekir` field of the cwd's `package.json` once. Returns an
* object with `entry?: string` and `envFiles?: string[]`, or null when
* no package.json or no `tekir` block. Lookups elsewhere reuse this
* cached value so we don't re-read the file three times in a row.
*/
function existingEnvFiles(paths) {
const out = []
for (const f of paths) {
if (existsSync(f)) {
out.push(f)
} else {
console.warn(`[tekir] --env-file '${f}' not found, skipping`)
}
}
return out
function readTekirConfig() {
try {
const pkg = JSON.parse(readFileSync('package.json', 'utf8'))
const cfg = pkg?.tekir
if (cfg && typeof cfg === 'object') return cfg
} catch {}
return null
}
async function findEntry(explicit) {
async function findEntry(explicit, cfg) {
if (explicit) return explicit
if (cfg?.entry && typeof cfg.entry === 'string') return cfg.entry
try {
const pkg = JSON.parse(readFileSync('package.json', 'utf8'))
if (pkg?.tekir?.entry && typeof pkg.tekir.entry === 'string') return pkg.tekir.entry
} catch {}
for (const candidate of ['index.ts', 'api/index.ts', 'app/index.ts', 'src/index.ts', 'index.js']) {

@@ -160,7 +215,13 @@ if (existsSync(candidate)) return candidate

console.error('')
console.error('Environment files: pass `--env-file <path>` (multi) to load `.env`-style files')
console.error(' via the runtime\'s own `--env-file` flag (Bun, or Node 20.6+). tekir filters')
console.error(' out missing files with a warning before forwarding, so optional per-package')
console.error(' `.env` files in a monorepo do not need to exist for the rest to load.')
console.error('Environment files: declare them in `package.json` (recommended for monorepo')
console.error(' dev scripts) and/or pass `--env-file <path>` flags. The bin loads them itself,')
console.error(' so paths in `package.json` never enter argv where Node\'s native `--env-file`')
console.error(' parser would bail on a missing file:')
console.error('')
console.error(' "tekir": { "envFiles": ["a/.env", "b/.env"] }')
console.error('')
console.error(' Shell-provided env wins; later files override earlier ones; missing files are')
console.error(' warned and skipped. `package.json` paths load first, then any `--env-file`')
console.error(' flags from the command line.')
console.error('')
console.error('Build flags: --target, --format esm|cjs|iife, --minify / --no-minify,')

@@ -207,22 +268,16 @@ console.error(' granular --minify-syntax / --minify-whitespace / --minify-identifiers,')

/**
* Re-spawn this same bin under `runtime` with native `--env-file` flags
* applied first. The runtime parses the env-files into its own
* `process.env` before our bin's second invocation imports the entry, so
* we never have to ship a hand-rolled dotenv parser. Exits the parent
* with the child's exit code.
* `Bun.build` is Bun-only. On Node we re-spawn the same bin under `bun`
* with the original argv preserved; if Bun is not on PATH we print an
* actionable error. The child inherits `process.env`, which the parent
* has already populated from any `--env-file` flags, so env-bearing
* builds work without passing the flag through.
*/
function reexecUnder(runtime, envFilesToForward) {
const childArgv = [
...envFilesToForward.map(f => `--env-file=${f}`),
process.argv[1] ?? '',
...stripEnvFileFlags(rawArgv),
]
const result = spawnSync(runtime, childArgv, { stdio: 'inherit' })
function ensureBunOrReexec() {
if (isBun) return
const result = spawnSync('bun', [process.argv[1] ?? '', ...process.argv.slice(2)], {
stdio: 'inherit',
})
if (result.error && /** @type {NodeJS.ErrnoException} */ (result.error).code === 'ENOENT') {
if (runtime === 'bun') {
console.error('[tekir] `bun` is required for this command but is not on PATH.')
console.error(' Install Bun: https://bun.sh.')
} else {
console.error(`[tekir] '${runtime}' is not on PATH.`)
}
console.error('[tekir] `tekir build` requires Bun (Bun.build is the bundler).')
console.error(' Install Bun: https://bun.sh, or run via `bunx @tekir/cli build`.')
process.exit(1)

@@ -234,11 +289,22 @@ }

const rawArgv = process.argv.slice(2)
const { entry: entryFlag, envFiles, rest: argv } = extractTekirFlags(rawArgv)
const { entry: entryFlag, envFiles: cliEnvFiles, rest: argv } = extractTekirFlags(rawArgv)
const command = argv[0]
const presentEnvFiles = existingEnvFiles(envFiles)
const tekirCfg = readTekirConfig()
// Preload env files into the parent process before any in-process import
// or subprocess spawn. `package.json.tekir.envFiles` loads first (so the
// JSON config sets the baseline), then any `--env-file` flags from the
// command line override / extend on top — matches the "later wins"
// dotenv precedence. Shell-provided env always wins both.
const jsonEnvFiles = Array.isArray(tekirCfg?.envFiles)
? tekirCfg.envFiles.filter(x => typeof x === 'string')
: []
const allEnvFiles = [...jsonEnvFiles, ...cliEnvFiles]
if (allEnvFiles.length > 0) {
const shellKeys = new Set(Object.keys(process.env))
for (const f of allEnvFiles) loadEnvFile(f, shellKeys)
}
if (!command) {
// Bare `tekir`: behaves like `tekir serve`. If env-files were passed,
// re-exec under the current runtime so it loads them first.
if (presentEnvFiles.length > 0) reexecUnder(isBun ? 'bun' : 'node', presentEnvFiles)
const entry = await findEntry(entryFlag)
const entry = await findEntry(entryFlag, tekirCfg)
await runEntry(entry, undefined, [])

@@ -250,11 +316,5 @@ }

if (command === 'build') {
// `Bun.build` is Bun-only. Re-exec under bun if we're on Node, OR if
// the user passed `--env-file` flags (so bun loads them as the build
// process boots and any custom plugin code that reads `process.env`
// sees them).
if (!isBun || presentEnvFiles.length > 0) {
reexecUnder('bun', presentEnvFiles)
}
// We are now guaranteed to be running under Bun with env applied.
const entry = await findEntry(entryFlag)
ensureBunOrReexec()
// From here on we are guaranteed to be running under Bun.
const entry = await findEntry(entryFlag, tekirCfg)
const flags = argv.slice(1)

@@ -266,3 +326,3 @@ const { runBuild } = await import('@tekir/core')

const entry = await findEntry(entryFlag)
const entry = await findEntry(entryFlag, tekirCfg)
const rest = argv.slice(1)

@@ -274,10 +334,9 @@

// has to spawn a subprocess; in-app `tekir()` still receives `serve` as
// the command and starts the server normally inside it. `--env-file`
// flags are forwarded to the runtime alongside `--watch` so editing a
// loaded `.env` re-applies on the next restart.
// the command and starts the server normally inside it. The child
// inherits the parent's `process.env` (already populated from any
// `--env-file` flags), so `--env-file` is not forwarded to the runtime.
if (command === 'serve' && rest.includes('--dev')) {
const watchRest = rest.filter(a => a !== '--dev')
const runner = isBun ? 'bun' : 'node'
const watchFlags = ['--watch', ...presentEnvFiles.map(f => `--env-file=${f}`)]
const proc = spawn(runner, [...watchFlags, entry, 'serve', ...watchRest], {
const proc = spawn(runner, ['--watch', entry, 'serve', ...watchRest], {
stdio: 'inherit',

@@ -288,7 +347,3 @@ env: { ...process.env, NODE_ENV: process.env.NODE_ENV || 'development' },

} else {
// Any other command imports the entry in-process. If env-files were
// passed, re-exec under the runtime first so its native `--env-file`
// flag applies them before our second invocation runs the entry.
if (presentEnvFiles.length > 0) reexecUnder(isBun ? 'bun' : 'node', presentEnvFiles)
await runEntry(entry, command, rest)
}
{
"name": "@tekir/cli",
"version": "0.1.1",
"version": "0.1.2",
"description": "tekir command-line tool: serve, build, generate-key, and provider-registered commands.",

@@ -5,0 +5,0 @@ "author": "dev@tekir.io",