@cencivic/runx 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 cencivic
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,147 @@
1
+ # @cencivic/runx
2
+
3
+ [![npm version](https://img.shields.io/npm/v/@cencivic/runx.svg)](https://www.npmjs.com/package/@cencivic/runx)
4
+ [![npm downloads](https://img.shields.io/npm/dm/@cencivic/runx.svg)](https://www.npmjs.com/package/@cencivic/runx)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
6
+
7
+ Run TypeScript scripts with inline dependencies.
8
+
9
+ Inspired by [uv](https://github.com/astral-sh/uv)'s script runner for Python.
10
+
11
+ ## Table of Contents
12
+
13
+ - [Why runx?](#why-runx)
14
+ - [Installation](#installation)
15
+ - [Usage](#usage)
16
+ - [CLI](#cli)
17
+ - [Contributing](#contributing)
18
+ - [Acknowledgments](#acknowledgments)
19
+ - [License](#license)
20
+
21
+ ## Why runx?
22
+
23
+ ### AI-friendly scripting without side effects
24
+
25
+ AI coding assistants (Claude, Copilot, Cursor, etc.) are great at generating one-off utility scripts — data migration, file processing, API testing, quick prototyping, and more. But these scripts often need external packages, and that's where things get messy:
26
+
27
+ - Adding dependencies to the project's `package.json` just for a throwaway script
28
+ - Lock file (`package-lock.json`, `bun.lock`) diffs polluting your git history
29
+ - Risk of version conflicts with your actual project dependencies
30
+ - Forgetting to clean up after the script is no longer needed
31
+
32
+ **runx solves this.** Each script declares its own dependencies inline, installed into an isolated cache — completely independent from your project.
33
+
34
+ ```typescript
35
+ #!/usr/bin/env runx
36
+ /**
37
+ * @runx {
38
+ * "dependencies": {
39
+ * "csv-parse": "^5.5.0",
40
+ * "chalk": "^5.3.0"
41
+ * }
42
+ * }
43
+ */
44
+
45
+ import { parse } from 'csv-parse/sync';
46
+ import chalk from 'chalk';
47
+
48
+ // AI-generated one-off migration script
49
+ // Zero impact on your project's package.json or lock file
50
+ const data = parse(await Bun.file('data.csv').text(), { columns: true });
51
+ console.log(chalk.green(`Processed ${data.length} rows`));
52
+ ```
53
+
54
+ Just ask your AI assistant to *"write a runx script that does X"* — you get a single, self-contained `.ts` file that runs anywhere without touching your project configuration. When you're done, simply delete the file. No cleanup needed.
55
+
56
+ ## Installation
57
+
58
+ ```bash
59
+ npm install -g @cencivic/runx
60
+ ```
61
+
62
+ Requires [bun](https://bun.sh) to be installed.
63
+
64
+ ## Usage
65
+
66
+ Create a TypeScript file with dependencies declared in a `@runx` JSDoc block:
67
+
68
+ ```typescript
69
+ #!/usr/bin/env runx
70
+ /**
71
+ * @runx {
72
+ * "dependencies": {
73
+ * "zod": "^3.22.0",
74
+ * "chalk": "^5.3.0"
75
+ * }
76
+ * }
77
+ */
78
+
79
+ import { z } from 'zod';
80
+ import chalk from 'chalk';
81
+
82
+ const UserSchema = z.object({
83
+ name: z.string(),
84
+ age: z.number(),
85
+ });
86
+
87
+ const user = UserSchema.parse({ name: 'Alice', age: 30 });
88
+ console.log(chalk.green(`Hello, ${user.name}!`));
89
+ ```
90
+
91
+ Run it:
92
+
93
+ ```bash
94
+ runx script.ts
95
+ ```
96
+
97
+ On first run, dependencies are installed and cached. Subsequent runs use the cache.
98
+
99
+ ### Metadata Fields
100
+
101
+ The `@runx` block accepts a JSON object with the following fields:
102
+
103
+ | Field | Type | Description |
104
+ |---|---|---|
105
+ | `dependencies` | `Record<string, string>` | Package.json-style dependencies (required) |
106
+ | `env` | `Record<string, string>` | Environment variables to set |
107
+ | `engines` | `{ bun?: string; node?: string }` | Runtime version requirements |
108
+ | `args` | `string[]` | Default arguments for the script |
109
+
110
+ Full example with all fields:
111
+
112
+ ```typescript
113
+ /**
114
+ * @runx {
115
+ * "dependencies": {
116
+ * "chalk": "^5.0.0",
117
+ * "zod": "~3.22.0",
118
+ * "@types/node": "20.0.0"
119
+ * },
120
+ * "env": { "NODE_ENV": "production", "DEBUG": "true" },
121
+ * "engines": { "bun": ">=1.0", "node": ">=18" },
122
+ * "args": ["--verbose"]
123
+ * }
124
+ */
125
+ ```
126
+
127
+ ## CLI
128
+
129
+ ```bash
130
+ runx <script.ts> [args...] # Run a script
131
+ runx --clean # Clear all cached environments
132
+ runx --version # Show version
133
+ runx --help # Show help
134
+ ```
135
+
136
+ ## Contributing
137
+
138
+ Contributions are welcome! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.
139
+
140
+ ## Acknowledgments
141
+
142
+ - [uv](https://github.com/astral-sh/uv) - The inspiration for this project. uv's inline script dependencies for Python showed how powerful this pattern can be.
143
+ - [bun](https://bun.sh) - Used for fast dependency installation and script execution.
144
+
145
+ ## License
146
+
147
+ MIT
package/bin/runx.js ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import '../dist/cli.js';
package/dist/cli.d.ts ADDED
@@ -0,0 +1 @@
1
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,92 @@
1
+ import { execFileSync } from 'node:child_process';
2
+ import { existsSync, readFileSync } from 'node:fs';
3
+ import { dirname, join } from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { parseScriptMetadata } from './parser.js';
6
+ import { cleanCache, resolveEnvironment } from './resolver.js';
7
+ import { runScript } from './runner.js';
8
+ import { checkForUpdate, formatUpdateMessage } from './update-check.js';
9
+ const __dirname = dirname(fileURLToPath(import.meta.url));
10
+ const { version: VERSION } = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf-8'));
11
+ const HELP = `
12
+ runx - uv-like script runner for TypeScript
13
+
14
+ Usage:
15
+ runx <script.ts> [args...] Run a TypeScript script
16
+ runx --clean Clean all cached environments
17
+ runx --version Show version
18
+ runx --help Show this help
19
+
20
+ Example script:
21
+ /**
22
+ * @runx {
23
+ * "dependencies": {
24
+ * "chalk": "^5.0.0",
25
+ * "zod": "~3.22.0"
26
+ * }
27
+ * }
28
+ */
29
+ import chalk from 'chalk';
30
+ console.log(chalk.green('Hello!'));
31
+
32
+ Dependency versions (same as package.json):
33
+ "5.3.0" Exact version
34
+ "^5.0.0" Compatible with 5.x.x (minor/patch updates)
35
+ "~5.3.0" Approximately 5.3.x (patch updates only)
36
+ ">=1.0.0" Version range
37
+ "latest" Latest version
38
+ "*" Any version
39
+ `.trim();
40
+ function ensureBun() {
41
+ try {
42
+ execFileSync('bun', ['--version'], { stdio: 'ignore' });
43
+ }
44
+ catch {
45
+ console.error('Error: bun is required but not found.');
46
+ console.error('Install it: https://bun.sh');
47
+ process.exit(1);
48
+ }
49
+ }
50
+ async function main() {
51
+ const args = process.argv.slice(2);
52
+ if (args.length === 0 || args[0] === '--help' || args[0] === '-h') {
53
+ console.log(HELP);
54
+ process.exit(0);
55
+ }
56
+ if (args[0] === '--version' || args[0] === '-v') {
57
+ console.log(VERSION);
58
+ process.exit(0);
59
+ }
60
+ if (args[0] === '--clean') {
61
+ await cleanCache();
62
+ process.exit(0);
63
+ }
64
+ ensureBun();
65
+ const scriptPath = args[0];
66
+ const scriptArgs = args.slice(1);
67
+ if (!existsSync(scriptPath)) {
68
+ console.error(`Error: File not found: ${scriptPath}`);
69
+ process.exit(1);
70
+ }
71
+ // Start update check in background (non-blocking)
72
+ const updatePromise = checkForUpdate(VERSION);
73
+ try {
74
+ // Parse script metadata
75
+ const metadata = await parseScriptMetadata(scriptPath);
76
+ // Resolve environment (install deps if needed)
77
+ const nodeModulesPath = await resolveEnvironment(metadata, scriptPath);
78
+ // Run the script
79
+ const exitCode = await runScript(scriptPath, nodeModulesPath, scriptArgs);
80
+ // Show update notification after script finishes
81
+ const latestVersion = await updatePromise;
82
+ if (latestVersion) {
83
+ console.error(formatUpdateMessage(VERSION, latestVersion));
84
+ }
85
+ process.exit(exitCode);
86
+ }
87
+ catch (err) {
88
+ console.error('Error:', err instanceof Error ? err.message : err);
89
+ process.exit(1);
90
+ }
91
+ }
92
+ main();
@@ -0,0 +1,16 @@
1
+ import type { ScriptMetadata } from './types.js';
2
+ /**
3
+ * Parse script metadata from @runx JSDoc tag.
4
+ *
5
+ * Expected format:
6
+ * /**
7
+ * * @runx {
8
+ * * "dependencies": {
9
+ * * "zod": "^3.22.0",
10
+ * * "chalk": "^5.3.0"
11
+ * * }
12
+ * * }
13
+ * *\/
14
+ */
15
+ export declare function parseScriptMetadata(scriptPath: string): Promise<ScriptMetadata>;
16
+ export declare function parseMetadataFromContent(content: string): ScriptMetadata;
package/dist/parser.js ADDED
@@ -0,0 +1,77 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { parse } from 'comment-parser';
3
+ /**
4
+ * Parse script metadata from @runx JSDoc tag.
5
+ *
6
+ * Expected format:
7
+ * /**
8
+ * * @runx {
9
+ * * "dependencies": {
10
+ * * "zod": "^3.22.0",
11
+ * * "chalk": "^5.3.0"
12
+ * * }
13
+ * * }
14
+ * *\/
15
+ */
16
+ export async function parseScriptMetadata(scriptPath) {
17
+ const content = await readFile(scriptPath, 'utf-8');
18
+ return parseMetadataFromContent(content);
19
+ }
20
+ export function parseMetadataFromContent(content) {
21
+ const parsed = parse(content);
22
+ for (const block of parsed) {
23
+ for (const tag of block.tags) {
24
+ if (tag.tag === 'runx') {
25
+ const jsonStr = extractJsonFromTag(tag);
26
+ if (jsonStr) {
27
+ try {
28
+ const metadata = JSON.parse(jsonStr);
29
+ return {
30
+ dependencies: metadata.dependencies ?? {},
31
+ };
32
+ }
33
+ catch {
34
+ throw new Error(`Invalid JSON in @runx tag: ${jsonStr}`);
35
+ }
36
+ }
37
+ }
38
+ }
39
+ }
40
+ return { dependencies: {} };
41
+ }
42
+ function extractJsonFromTag(tag) {
43
+ // comment-parser puts { ... } content in the 'type' token
44
+ // For multiline JSON, we need to reconstruct from all source lines
45
+ const typeParts = [];
46
+ for (const sourceLine of tag.source) {
47
+ const typeToken = sourceLine.tokens.type;
48
+ if (typeToken) {
49
+ typeParts.push(typeToken);
50
+ }
51
+ }
52
+ const fullContent = typeParts.join('\n').trim();
53
+ // Find the JSON object boundaries
54
+ const startIndex = fullContent.indexOf('{');
55
+ if (startIndex === -1) {
56
+ return null;
57
+ }
58
+ // Find matching closing brace
59
+ let braceCount = 0;
60
+ let endIndex = -1;
61
+ for (let i = startIndex; i < fullContent.length; i++) {
62
+ if (fullContent[i] === '{') {
63
+ braceCount++;
64
+ }
65
+ else if (fullContent[i] === '}') {
66
+ braceCount--;
67
+ if (braceCount === 0) {
68
+ endIndex = i;
69
+ break;
70
+ }
71
+ }
72
+ }
73
+ if (endIndex === -1) {
74
+ return null;
75
+ }
76
+ return fullContent.slice(startIndex, endIndex + 1);
77
+ }
@@ -0,0 +1,22 @@
1
+ import type { ScriptMetadata } from './types.js';
2
+ /**
3
+ * Generate cache key from dependencies.
4
+ */
5
+ export declare function generateCacheKey(metadata: ScriptMetadata): string;
6
+ /**
7
+ * Get cache directory path for given metadata and script path.
8
+ */
9
+ export declare function getCacheDir(metadata: ScriptMetadata, scriptPath: string): string;
10
+ /**
11
+ * Check if cache exists for given metadata.
12
+ */
13
+ export declare function cacheExists(metadata: ScriptMetadata, scriptPath: string): boolean;
14
+ /**
15
+ * Resolve environment for script metadata.
16
+ * Creates cache if it doesn't exist.
17
+ */
18
+ export declare function resolveEnvironment(metadata: ScriptMetadata, scriptPath: string): Promise<string>;
19
+ /**
20
+ * Clean all cached environments.
21
+ */
22
+ export declare function cleanCache(): Promise<void>;
@@ -0,0 +1,121 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { createHash } from 'node:crypto';
3
+ import { existsSync } from 'node:fs';
4
+ import { mkdir, readdir, rm, writeFile } from 'node:fs/promises';
5
+ import { homedir } from 'node:os';
6
+ import { basename, join } from 'node:path';
7
+ /**
8
+ * Get platform-appropriate cache base directory.
9
+ */
10
+ function getCacheBase() {
11
+ if (process.platform === 'win32') {
12
+ const localAppData = process.env.LOCALAPPDATA;
13
+ if (localAppData) {
14
+ return join(localAppData, 'runx', 'envs');
15
+ }
16
+ return join(homedir(), 'AppData', 'Local', 'runx', 'envs');
17
+ }
18
+ // macOS & Linux: XDG_CACHE_HOME or ~/.cache
19
+ const xdgCacheHome = process.env.XDG_CACHE_HOME;
20
+ if (xdgCacheHome) {
21
+ return join(xdgCacheHome, 'runx', 'envs');
22
+ }
23
+ return join(homedir(), '.cache', 'runx', 'envs');
24
+ }
25
+ /**
26
+ * Generate cache key from dependencies.
27
+ */
28
+ export function generateCacheKey(metadata) {
29
+ const sortedEntries = Object.entries(metadata.dependencies).sort(([a], [b]) => a.localeCompare(b));
30
+ const content = JSON.stringify(sortedEntries);
31
+ return createHash('sha256').update(content).digest('hex').slice(0, 16);
32
+ }
33
+ /**
34
+ * Extract script name (without extension) from script path.
35
+ */
36
+ function getScriptName(scriptPath) {
37
+ const base = basename(scriptPath);
38
+ const dotIndex = base.lastIndexOf('.');
39
+ return dotIndex > 0 ? base.slice(0, dotIndex) : base;
40
+ }
41
+ /**
42
+ * Get cache directory path for given metadata and script path.
43
+ */
44
+ export function getCacheDir(metadata, scriptPath) {
45
+ const key = generateCacheKey(metadata);
46
+ const scriptName = getScriptName(scriptPath);
47
+ return join(getCacheBase(), `${scriptName}-${key}`);
48
+ }
49
+ /**
50
+ * Check if cache exists for given metadata.
51
+ */
52
+ export function cacheExists(metadata, scriptPath) {
53
+ const cacheDir = getCacheDir(metadata, scriptPath);
54
+ return existsSync(join(cacheDir, 'node_modules'));
55
+ }
56
+ /**
57
+ * Create package.json content for dependencies.
58
+ */
59
+ function createPackageJson(metadata) {
60
+ return JSON.stringify({
61
+ name: 'runx-env',
62
+ version: '0.0.0',
63
+ private: true,
64
+ dependencies: metadata.dependencies,
65
+ }, null, 2);
66
+ }
67
+ /**
68
+ * Run bun install in the cache directory.
69
+ */
70
+ async function runBunInstall(cacheDir) {
71
+ return new Promise((resolve, reject) => {
72
+ const proc = spawn('bun', ['install'], {
73
+ cwd: cacheDir,
74
+ stdio: 'inherit',
75
+ });
76
+ proc.on('close', (code) => {
77
+ if (code === 0) {
78
+ resolve();
79
+ }
80
+ else {
81
+ reject(new Error(`bun install failed with code ${code}`));
82
+ }
83
+ });
84
+ proc.on('error', reject);
85
+ });
86
+ }
87
+ /**
88
+ * Resolve environment for script metadata.
89
+ * Creates cache if it doesn't exist.
90
+ */
91
+ export async function resolveEnvironment(metadata, scriptPath) {
92
+ if (Object.keys(metadata.dependencies).length === 0) {
93
+ return '';
94
+ }
95
+ const cacheDir = getCacheDir(metadata, scriptPath);
96
+ if (cacheExists(metadata, scriptPath)) {
97
+ return join(cacheDir, 'node_modules');
98
+ }
99
+ // Create cache directory
100
+ await mkdir(cacheDir, { recursive: true });
101
+ // Write package.json
102
+ const packageJson = createPackageJson(metadata);
103
+ await writeFile(join(cacheDir, 'package.json'), packageJson);
104
+ // Run bun install
105
+ console.error(`Installing dependencies...`);
106
+ await runBunInstall(cacheDir);
107
+ return join(cacheDir, 'node_modules');
108
+ }
109
+ /**
110
+ * Clean all cached environments.
111
+ */
112
+ export async function cleanCache() {
113
+ const cacheBase = getCacheBase();
114
+ if (!existsSync(cacheBase)) {
115
+ console.log('Cache is already empty.');
116
+ return;
117
+ }
118
+ const entries = await readdir(cacheBase);
119
+ await rm(cacheBase, { recursive: true, force: true });
120
+ console.log(`Cleaned ${entries.length} cached environment(s).`);
121
+ }
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Run a TypeScript script with bun.
3
+ */
4
+ export declare function runScript(scriptPath: string, nodeModulesPath: string, args: string[]): Promise<number>;
package/dist/runner.js ADDED
@@ -0,0 +1,29 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { resolve } from 'node:path';
3
+ /**
4
+ * Run a TypeScript script with bun.
5
+ */
6
+ export async function runScript(scriptPath, nodeModulesPath, args) {
7
+ const absoluteScriptPath = resolve(scriptPath);
8
+ const env = { ...process.env };
9
+ if (nodeModulesPath) {
10
+ // Set NODE_PATH to include the cached node_modules
11
+ const existingNodePath = env.NODE_PATH || '';
12
+ const separator = process.platform === 'win32' ? ';' : ':';
13
+ env.NODE_PATH = existingNodePath
14
+ ? `${nodeModulesPath}${separator}${existingNodePath}`
15
+ : nodeModulesPath;
16
+ }
17
+ return new Promise((resolve, reject) => {
18
+ const proc = spawn('bun', ['run', absoluteScriptPath, ...args], {
19
+ stdio: 'inherit',
20
+ env,
21
+ });
22
+ proc.on('close', (code) => {
23
+ resolve(code ?? 0);
24
+ });
25
+ proc.on('error', (err) => {
26
+ reject(err);
27
+ });
28
+ });
29
+ }
@@ -0,0 +1,3 @@
1
+ export interface ScriptMetadata {
2
+ dependencies: Record<string, string>;
3
+ }
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Check npm registry for a newer version of runx.
3
+ * Caches result for 24 hours. Returns silently on any error.
4
+ */
5
+ export declare function checkForUpdate(currentVersion: string): Promise<string>;
6
+ /**
7
+ * Format update notification message.
8
+ */
9
+ export declare function formatUpdateMessage(currentVersion: string, latestVersion: string): string;
@@ -0,0 +1,101 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { mkdir, readFile, writeFile } from 'node:fs/promises';
3
+ import { homedir } from 'node:os';
4
+ import { dirname, join } from 'node:path';
5
+ const REGISTRY_URL = 'https://registry.npmjs.org/@cencivic/runx/latest';
6
+ const CHECK_INTERVAL = 86_400_000; // 24 hours
7
+ /**
8
+ * Get path for the update check cache file.
9
+ */
10
+ function getCacheFile() {
11
+ if (process.platform === 'win32') {
12
+ const localAppData = process.env.LOCALAPPDATA;
13
+ if (localAppData) {
14
+ return join(localAppData, 'runx', 'update-check.json');
15
+ }
16
+ return join(homedir(), 'AppData', 'Local', 'runx', 'update-check.json');
17
+ }
18
+ const xdgCacheHome = process.env.XDG_CACHE_HOME;
19
+ if (xdgCacheHome) {
20
+ return join(xdgCacheHome, 'runx', 'update-check.json');
21
+ }
22
+ return join(homedir(), '.cache', 'runx', 'update-check.json');
23
+ }
24
+ /**
25
+ * Read cached update check result.
26
+ */
27
+ async function readCache() {
28
+ try {
29
+ const cacheFile = getCacheFile();
30
+ if (!existsSync(cacheFile))
31
+ return null;
32
+ const data = await readFile(cacheFile, 'utf-8');
33
+ return JSON.parse(data);
34
+ }
35
+ catch {
36
+ return null;
37
+ }
38
+ }
39
+ /**
40
+ * Write update check result to cache.
41
+ */
42
+ async function writeCache(result) {
43
+ const cacheFile = getCacheFile();
44
+ await mkdir(dirname(cacheFile), { recursive: true });
45
+ await writeFile(cacheFile, JSON.stringify(result));
46
+ }
47
+ /**
48
+ * Compare two semver version strings.
49
+ * Returns true if `latest` is newer than `current`.
50
+ */
51
+ function isNewer(current, latest) {
52
+ const a = current.split('.').map(Number);
53
+ const b = latest.split('.').map(Number);
54
+ for (let i = 0; i < 3; i++) {
55
+ if ((b[i] ?? 0) > (a[i] ?? 0))
56
+ return true;
57
+ if ((b[i] ?? 0) < (a[i] ?? 0))
58
+ return false;
59
+ }
60
+ return false;
61
+ }
62
+ /**
63
+ * Check npm registry for a newer version of runx.
64
+ * Caches result for 24 hours. Returns silently on any error.
65
+ */
66
+ export async function checkForUpdate(currentVersion) {
67
+ // Skip in CI
68
+ if (process.env.CI)
69
+ return '';
70
+ try {
71
+ const cached = await readCache();
72
+ const now = Date.now();
73
+ // Use cache if within 24h
74
+ if (cached && now - cached.time < CHECK_INTERVAL) {
75
+ return isNewer(currentVersion, cached.latest) ? cached.latest : '';
76
+ }
77
+ // Fetch with 1.5s timeout
78
+ const response = await Promise.race([
79
+ fetch(REGISTRY_URL),
80
+ new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), 1500)),
81
+ ]);
82
+ const data = (await response.json());
83
+ const latest = data.version;
84
+ await writeCache({ latest, time: now });
85
+ return isNewer(currentVersion, latest) ? latest : '';
86
+ }
87
+ catch {
88
+ return '';
89
+ }
90
+ }
91
+ /**
92
+ * Format update notification message.
93
+ */
94
+ export function formatUpdateMessage(currentVersion, latestVersion) {
95
+ return [
96
+ '',
97
+ ` Update available: ${currentVersion} → \x1b[32m${latestVersion}\x1b[0m`,
98
+ ` Run \x1b[36mbun install -g @cencivic/runx\x1b[0m to update`,
99
+ '',
100
+ ].join('\n');
101
+ }
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "@cencivic/runx",
3
+ "version": "0.1.0",
4
+ "description": "uv-like script runner for TypeScript",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "bin": {
8
+ "runx": "./bin/runx.js"
9
+ },
10
+ "files": [
11
+ "bin",
12
+ "dist"
13
+ ],
14
+ "scripts": {
15
+ "build": "tsc",
16
+ "test": "bun test",
17
+ "lint": "biome check --fix .",
18
+ "format": "biome format --write .",
19
+ "check": "biome check .",
20
+ "prepublishOnly": "npm run build"
21
+ },
22
+ "engines": {
23
+ "node": ">=18"
24
+ },
25
+ "devDependencies": {
26
+ "@biomejs/biome": "^2.3.13",
27
+ "@types/node": "^20.0.0",
28
+ "bun-types": "^1.3.8",
29
+ "typescript": "^5.0.0",
30
+ "@types/bun": "latest"
31
+ },
32
+ "dependencies": {
33
+ "comment-parser": "^1.4.5"
34
+ }
35
+ }