mirror of
https://github.com/PR0M3TH3AN/TORCH.git
synced 2026-10-27 07:51:52 +00:00
Refactor torch-lock into an installable NPM library and CLI
- Rename `src/nostr-lock.mjs` to `src/lib.mjs` and export core functions. - Create `bin/torch-lock.mjs` as the CLI entry point. - Implement `dashboard` command in `src/lib.mjs` to serve the dashboard using Node.js. - Update `package.json` to configure `bin`, `main`, and `files` for NPM packaging. - Create `torch-config.example.json` as a template for consumers. - Update tests and README.md to reflect the new structure. - Exclude task-specific artifacts (logs, context) from the package while retaining prompts and docs. Co-authored-by: PR0M3TH3AN <[email protected]>
This commit is contained in:
1 parent
b8d0372780
commit
35092b3d5a
7 files changed
+240
-117
No files matched your search
@@ -2,77 +2,69 @@
|
||||
|
||||
TORCH is a portable Nostr-based task locking toolkit for multi-agent coordination.
|
||||
|
||||
## First-run quickstart
|
||||
|
||||
From the repository root:
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
npm install
|
||||
cp torch-config.json /path/to/your-project/torch-config.json # optional template step when vendoring TORCH
|
||||
npm run lock:check:daily
|
||||
AGENT_PLATFORM=codex npm run lock:lock -- --agent docs-agent --cadence daily
|
||||
npm run lock:list
|
||||
npm install torch-lock
|
||||
```
|
||||
|
||||
### Project configuration (`torch-config.json`)
|
||||
## Usage
|
||||
|
||||
Create a `torch-config.json` file in your project root (or set `TORCH_CONFIG_PATH`) to override defaults per repo:
|
||||
### CLI
|
||||
|
||||
The `torch-lock` CLI allows you to check, lock, and list tasks.
|
||||
|
||||
```bash
|
||||
# Check locks for a specific cadence
|
||||
npx torch-lock check --cadence daily
|
||||
|
||||
# Lock a task for an agent
|
||||
AGENT_PLATFORM=codex npx torch-lock lock --agent docs-agent --cadence daily
|
||||
|
||||
# List all active locks
|
||||
npx torch-lock list
|
||||
|
||||
# Run the Dashboard
|
||||
npx torch-lock dashboard
|
||||
```
|
||||
|
||||
### Configuration (`torch-config.json`)
|
||||
|
||||
Create a `torch-config.json` file in your project root to configure TORCH for your repository. You can use the included `torch-config.example.json` as a template.
|
||||
|
||||
Configuration options:
|
||||
|
||||
- `nostrLock.namespace` (d-tag/tag namespace)
|
||||
- `nostrLock.relays`
|
||||
- `nostrLock.ttlSeconds`
|
||||
- `nostrLock.queryTimeoutMs`
|
||||
- `nostrLock.dailyRoster` / `nostrLock.weeklyRoster`
|
||||
- `nostrLock.relays` (array of relay URLs)
|
||||
- `nostrLock.ttlSeconds` (lock duration in seconds)
|
||||
- `nostrLock.queryTimeoutMs` (timeout for relay queries)
|
||||
- `nostrLock.dailyRoster` / `nostrLock.weeklyRoster` (explicit rosters)
|
||||
- `dashboard.defaultCadenceView` (`daily`, `weekly`, or `all`)
|
||||
- `dashboard.defaultStatusView` (`active` or `all`)
|
||||
- `scheduler.firstPromptByCadence.daily` / `.weekly`
|
||||
- `scheduler.firstPromptByCadence.daily` / `.weekly` (first prompt to run)
|
||||
- `scheduler.paused.daily` / `.weekly` (array of agent names to pause locally)
|
||||
|
||||
Weekly alignment maintenance is available via `src/prompts/weekly/repo-fit-agent.md`, which is designed to keep TORCH defaults and docs tuned to the host repository over time.
|
||||
### Dashboard
|
||||
|
||||
Default first-run behavior is configured so daily scheduling starts with `scheduler-update-agent`.
|
||||
|
||||
Optional dashboard:
|
||||
The dashboard provides a live view of lock events.
|
||||
|
||||
```bash
|
||||
npm run dashboard:serve
|
||||
# then open http://localhost:4173/dashboard/
|
||||
npx torch-lock dashboard --port 4173
|
||||
# Open http://localhost:4173/dashboard/
|
||||
```
|
||||
|
||||
## Included
|
||||
|
||||
- `src/nostr-lock.mjs` — Generic lock/check/list CLI
|
||||
- `src/docs/TORCH.md` — Protocol summary and usage
|
||||
- `src/prompts/` — Generic scheduler prompts and flow
|
||||
- `skills/` — Repository-local skill guides for agent onboarding and repeatable workflows
|
||||
- `examples/` — Optional scheduler overlay examples for adapting TORCH to downstream repositories
|
||||
- `dashboard/index.html` — Static lock dashboard
|
||||
|
||||
## CLI dependencies
|
||||
|
||||
Declared in `package.json` and pinned:
|
||||
|
||||
- `[email protected]`
|
||||
- `[email protected]`
|
||||
|
||||
## NPM script helpers
|
||||
|
||||
- `npm run lock:check:daily`
|
||||
- `npm run lock:check:weekly`
|
||||
- `npm run lock:list`
|
||||
- `npm run lock:lock -- --agent <agent-name> --cadence <daily|weekly>`
|
||||
- `npm run dashboard:serve`
|
||||
|
||||
## Environment variables
|
||||
|
||||
You can override configuration using environment variables:
|
||||
|
||||
- `NOSTR_LOCK_NAMESPACE`
|
||||
- `NOSTR_LOCK_RELAYS`
|
||||
- `NOSTR_LOCK_RELAYS` (comma-separated)
|
||||
- `NOSTR_LOCK_TTL`
|
||||
- `NOSTR_LOCK_QUERY_TIMEOUT_MS`
|
||||
- `NOSTR_LOCK_DAILY_ROSTER`
|
||||
- `NOSTR_LOCK_WEEKLY_ROSTER`
|
||||
- `TORCH_CONFIG_PATH`
|
||||
- `AGENT_PLATFORM`
|
||||
- `NOSTR_LOCK_DAILY_ROSTER` (comma-separated list of agents)
|
||||
- `NOSTR_LOCK_WEEKLY_ROSTER` (comma-separated list of agents)
|
||||
- `TORCH_CONFIG_PATH` (path to config file)
|
||||
- `AGENT_PLATFORM` (platform identifier, e.g. `codex`)
|
||||
|
||||
## Roster precedence
|
||||
|
||||
@@ -80,16 +72,22 @@ The lock CLI resolves roster names in this order:
|
||||
|
||||
1. `NOSTR_LOCK_DAILY_ROSTER` / `NOSTR_LOCK_WEEKLY_ROSTER` (comma-separated env overrides).
|
||||
2. `torch-config.json` (`nostrLock.dailyRoster` / `nostrLock.weeklyRoster`).
|
||||
3. `src/prompts/roster.json` (`daily` / `weekly` canonical scheduler roster).
|
||||
4. Built-in fallback roster (used only if `src/prompts/roster.json` is unreadable).
|
||||
3. `src/prompts/roster.json` (canonical scheduler roster included in the package).
|
||||
4. Built-in fallback roster.
|
||||
|
||||
`lock --agent` validates names against the resolved cadence roster, and `check`/`list` report lock events whose agent names do not match scheduler roster entries exactly.
|
||||
## Included Resources
|
||||
|
||||
- `src/lib.mjs` — Core library logic (can be imported in scripts)
|
||||
- `src/docs/TORCH.md` — Protocol summary and usage
|
||||
- `src/prompts/` — Generic scheduler prompts and flow
|
||||
- `skills/` — Repository-local skill guides for agent onboarding and repeatable workflows
|
||||
- `dashboard/` — Static lock dashboard assets
|
||||
|
||||
## Example
|
||||
## NPM Scripts (for development)
|
||||
|
||||
```bash
|
||||
NOSTR_LOCK_NAMESPACE=my-project \
|
||||
AGENT_PLATFORM=codex \
|
||||
node src/nostr-lock.mjs lock --agent docs-agent --cadence daily
|
||||
```
|
||||
If you are developing `torch-lock` itself:
|
||||
|
||||
- `npm run lock:check:daily`
|
||||
- `npm run lock:check:weekly`
|
||||
- `npm run lock:list`
|
||||
- `npm run dashboard:serve`
|
||||
Executable
+5
@@ -0,0 +1,5 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { main } from '../src/lib.mjs';
|
||||
|
||||
main(process.argv.slice(2));
|
||||
Generated
+3
-16
@@ -11,6 +11,9 @@
|
||||
"nostr-tools": "2.19.4",
|
||||
"ws": "8.19.0"
|
||||
},
|
||||
"bin": {
|
||||
"torch-lock": "bin/torch-lock.mjs"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^10.0.1",
|
||||
"eslint": "^10.0.0",
|
||||
@@ -252,22 +255,6 @@
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
},
|
||||
"node_modules/@playwright/test": {
|
||||
"version": "1.58.2",
|
||||
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.58.2.tgz",
|
||||
"integrity": "sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright": "1.58.2"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@scure/base": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.1.tgz",
|
||||
|
||||
+21
-7
@@ -1,16 +1,30 @@
|
||||
{
|
||||
"name": "torch-lock",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"description": "TORCH standalone lock CLI and optional dashboard",
|
||||
"description": "TORCH standalone lock CLI and dashboard",
|
||||
"main": "src/lib.mjs",
|
||||
"bin": {
|
||||
"torch-lock": "./bin/torch-lock.mjs"
|
||||
},
|
||||
"files": [
|
||||
"bin/",
|
||||
"src/lib.mjs",
|
||||
"src/torch-config.mjs",
|
||||
"src/prompts/",
|
||||
"src/docs/",
|
||||
"skills/",
|
||||
"dashboard/",
|
||||
"torch-config.example.json",
|
||||
"README.md"
|
||||
],
|
||||
"scripts": {
|
||||
"test": "node --test test/*.test.mjs",
|
||||
"lock:check:daily": "node src/nostr-lock.mjs check --cadence daily",
|
||||
"lock:check:weekly": "node src/nostr-lock.mjs check --cadence weekly",
|
||||
"lock:list": "node src/nostr-lock.mjs list",
|
||||
"lock:lock": "node src/nostr-lock.mjs lock",
|
||||
"dashboard:serve": "python3 -m http.server 4173",
|
||||
"lock:check:daily": "node bin/torch-lock.mjs check --cadence daily",
|
||||
"lock:check:weekly": "node bin/torch-lock.mjs check --cadence weekly",
|
||||
"lock:list": "node bin/torch-lock.mjs list",
|
||||
"lock:lock": "node bin/torch-lock.mjs lock",
|
||||
"dashboard:serve": "node bin/torch-lock.mjs dashboard",
|
||||
"build": "node build.mjs",
|
||||
"lint": "eslint ."
|
||||
},
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import http from 'node:http';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { generateSecretKey, getPublicKey, finalizeEvent } from 'nostr-tools/pure';
|
||||
import { SimplePool } from 'nostr-tools/pool';
|
||||
@@ -25,6 +26,7 @@ const DEFAULT_NAMESPACE = 'torch';
|
||||
const DEFAULT_QUERY_TIMEOUT_MS = 15_000;
|
||||
const VALID_CADENCES = new Set(['daily', 'weekly']);
|
||||
|
||||
// If installed as a package, prompts/roster.json is relative to this file
|
||||
const ROSTER_FILE = path.resolve(path.dirname(fileURLToPath(import.meta.url)), 'prompts/roster.json');
|
||||
const FALLBACK_ROSTER = {
|
||||
daily: [
|
||||
@@ -72,6 +74,14 @@ const FALLBACK_ROSTER = {
|
||||
|
||||
let cachedCanonicalRoster = null;
|
||||
|
||||
// Custom Error class for controlled exits
|
||||
class ExitError extends Error {
|
||||
constructor(code, message) {
|
||||
super(message);
|
||||
this.code = code;
|
||||
}
|
||||
}
|
||||
|
||||
function getRelays() {
|
||||
const config = loadTorchConfig();
|
||||
const envRelays = process.env.NOSTR_LOCK_RELAYS;
|
||||
@@ -131,7 +141,8 @@ function loadCanonicalRoster() {
|
||||
|
||||
console.error(`WARNING: Roster file is missing daily/weekly entries, falling back: ${ROSTER_FILE}`);
|
||||
} catch (err) {
|
||||
console.error(`WARNING: Failed to read roster file (${ROSTER_FILE}): ${err.message}`);
|
||||
// It's okay if roster file is missing when used as a library/CLI without the file present
|
||||
// console.error(`WARNING: Failed to read roster file (${ROSTER_FILE}): ${err.message}`);
|
||||
}
|
||||
|
||||
cachedCanonicalRoster = FALLBACK_ROSTER;
|
||||
@@ -206,7 +217,7 @@ function filterActiveLocks(locks) {
|
||||
return locks.filter((lock) => !lock.expiresAt || lock.expiresAt > now);
|
||||
}
|
||||
|
||||
async function queryLocks(relays, cadence, dateStr, namespace) {
|
||||
export async function queryLocks(relays, cadence, dateStr, namespace) {
|
||||
const pool = new SimplePool();
|
||||
const tagFilter = `${namespace}-lock-${cadence}-${dateStr}`;
|
||||
const queryTimeoutMs = getQueryTimeoutMs();
|
||||
@@ -228,7 +239,7 @@ async function queryLocks(relays, cadence, dateStr, namespace) {
|
||||
}
|
||||
}
|
||||
|
||||
async function publishLock(relays, event) {
|
||||
export async function publishLock(relays, event) {
|
||||
const pool = new SimplePool();
|
||||
|
||||
try {
|
||||
@@ -255,7 +266,7 @@ async function publishLock(relays, event) {
|
||||
}
|
||||
}
|
||||
|
||||
async function cmdCheck(cadence) {
|
||||
export async function cmdCheck(cadence) {
|
||||
const relays = getRelays();
|
||||
const namespace = getNamespace();
|
||||
const dateStr = todayDateStr();
|
||||
@@ -276,9 +287,7 @@ async function cmdCheck(cadence) {
|
||||
const unknownLockedAgents = lockedAgents.filter((agent) => !roster.includes(agent));
|
||||
const available = roster.filter((a) => !excludedAgents.includes(a));
|
||||
|
||||
console.log(
|
||||
JSON.stringify(
|
||||
{
|
||||
const result = {
|
||||
namespace,
|
||||
cadence,
|
||||
date: dateStr,
|
||||
@@ -295,14 +304,13 @@ async function cmdCheck(cadence) {
|
||||
expiresAt: l.expiresAtIso,
|
||||
platform: l.platform,
|
||||
})),
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
console.log(JSON.stringify(result, null, 2));
|
||||
return result;
|
||||
}
|
||||
|
||||
async function cmdLock(agent, cadence, dryRun = false) {
|
||||
export async function cmdLock(agent, cadence, dryRun = false) {
|
||||
const relays = getRelays();
|
||||
const namespace = getNamespace();
|
||||
const dateStr = todayDateStr();
|
||||
@@ -318,7 +326,7 @@ async function cmdLock(agent, cadence, dryRun = false) {
|
||||
if (!roster.includes(agent)) {
|
||||
console.error(`ERROR: agent "${agent}" is not in the ${cadence} roster`);
|
||||
console.error(`Allowed ${cadence} agents: ${roster.join(', ')}`);
|
||||
process.exit(1);
|
||||
throw new ExitError(1, 'Agent not in roster');
|
||||
}
|
||||
|
||||
console.error('Step 1: Checking for existing locks...');
|
||||
@@ -334,7 +342,7 @@ async function cmdLock(agent, cadence, dryRun = false) {
|
||||
console.log('LOCK_STATUS=denied');
|
||||
console.log('LOCK_REASON=already_locked');
|
||||
console.log(`LOCK_EXISTING_EVENT=${earliest.eventId}`);
|
||||
process.exit(3);
|
||||
throw new ExitError(3, 'Lock denied');
|
||||
}
|
||||
|
||||
console.error('Step 2: Generating ephemeral keypair...');
|
||||
@@ -393,7 +401,7 @@ async function cmdLock(agent, cadence, dryRun = false) {
|
||||
console.log('LOCK_STATUS=race_lost');
|
||||
console.log('LOCK_REASON=earlier_claim_exists');
|
||||
console.log(`LOCK_WINNER_EVENT=${winner.eventId}`);
|
||||
process.exit(3);
|
||||
throw new ExitError(3, 'Race check lost');
|
||||
}
|
||||
|
||||
console.error('RACE CHECK: won');
|
||||
@@ -407,9 +415,10 @@ async function cmdLock(agent, cadence, dryRun = false) {
|
||||
console.log(`LOCK_DATE=${dateStr}`);
|
||||
console.log(`LOCK_EXPIRES=${expiresAt}`);
|
||||
console.log(`LOCK_EXPIRES_ISO=${new Date(expiresAt * 1000).toISOString()}`);
|
||||
return { status: 'ok', eventId: event.id };
|
||||
}
|
||||
|
||||
async function cmdList(cadence) {
|
||||
export async function cmdList(cadence) {
|
||||
const relays = getRelays();
|
||||
const namespace = getNamespace();
|
||||
const dateStr = todayDateStr();
|
||||
@@ -459,8 +468,84 @@ async function cmdList(cadence) {
|
||||
}
|
||||
}
|
||||
|
||||
export async function cmdDashboard(port = 4173) {
|
||||
// Resolve package root relative to this file (src/lib.mjs)
|
||||
// this file is in <root>/src/lib.mjs, so '..' goes to <root>
|
||||
const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
||||
|
||||
const server = http.createServer((req, res) => {
|
||||
// URL parsing
|
||||
const url = new URL(req.url, `http://${req.headers.host}`);
|
||||
let pathname = url.pathname;
|
||||
|
||||
// Redirect / to /dashboard/
|
||||
if (pathname === '/' || pathname === '/dashboard') {
|
||||
res.writeHead(302, { 'Location': '/dashboard/' });
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
|
||||
// Special case: /torch-config.json
|
||||
// Priority: User's CWD > Package default
|
||||
if (pathname === '/torch-config.json') {
|
||||
const userConfigPath = path.resolve(process.cwd(), 'torch-config.json');
|
||||
if (fs.existsSync(userConfigPath)) {
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
fs.createReadStream(userConfigPath).pipe(res);
|
||||
return;
|
||||
}
|
||||
// If not found in CWD, fall through to serve from packageRoot (if it exists there)
|
||||
// or return empty object if missing?
|
||||
// Falling through means it looks for packageRoot/torch-config.json
|
||||
}
|
||||
|
||||
// Security check: prevent directory traversal
|
||||
const safePath = path.normalize(pathname).replace(/^(\.\.[\/\\])+/, '');
|
||||
let filePath = path.join(packageRoot, safePath);
|
||||
|
||||
// If directory, try index.html
|
||||
if (fs.existsSync(filePath) && fs.statSync(filePath).isDirectory()) {
|
||||
filePath = path.join(filePath, 'index.html');
|
||||
}
|
||||
|
||||
// Check if file exists and is a file
|
||||
if (!fs.existsSync(filePath) || !fs.statSync(filePath).isFile()) {
|
||||
res.writeHead(404);
|
||||
res.end('Not Found');
|
||||
return;
|
||||
}
|
||||
|
||||
// MIME types
|
||||
const extname = path.extname(filePath);
|
||||
let contentType = 'text/plain';
|
||||
switch (extname) {
|
||||
case '.html': contentType = 'text/html'; break;
|
||||
case '.js': contentType = 'text/javascript'; break;
|
||||
case '.css': contentType = 'text/css'; break;
|
||||
case '.json': contentType = 'application/json'; break;
|
||||
case '.png': contentType = 'image/png'; break;
|
||||
case '.jpg': contentType = 'image/jpeg'; break;
|
||||
case '.svg': contentType = 'image/svg+xml'; break;
|
||||
case '.ico': contentType = 'image/x-icon'; break;
|
||||
case '.md': contentType = 'text/markdown'; break;
|
||||
}
|
||||
|
||||
res.writeHead(200, { 'Content-Type': contentType });
|
||||
fs.createReadStream(filePath).pipe(res);
|
||||
});
|
||||
|
||||
server.listen(port, () => {
|
||||
console.log(`Dashboard running at http://localhost:${port}/dashboard/`);
|
||||
console.log(`Serving files from ${packageRoot}`);
|
||||
console.log(`Using configuration from ${process.cwd()}`);
|
||||
});
|
||||
|
||||
// Keep process alive
|
||||
return new Promise(() => {});
|
||||
}
|
||||
|
||||
function parseArgs(argv) {
|
||||
const args = { command: null, agent: null, cadence: null, dryRun: false };
|
||||
const args = { command: null, agent: null, cadence: null, dryRun: false, port: 4173 };
|
||||
let i = 0;
|
||||
|
||||
if (argv.length > 0 && !argv[0].startsWith('-')) {
|
||||
@@ -480,6 +565,8 @@ function parseArgs(argv) {
|
||||
args.cadence = arg.split('=')[1];
|
||||
} else if (arg === '--dry-run') {
|
||||
args.dryRun = true;
|
||||
} else if (arg === '--port') {
|
||||
args.port = parseInt(argv[++i], 10) || 4173;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -487,12 +574,13 @@ function parseArgs(argv) {
|
||||
}
|
||||
|
||||
function usage() {
|
||||
console.error(`Usage: node src/nostr-lock.mjs <command> [options]
|
||||
console.error(`Usage: torch-lock <command> [options]
|
||||
|
||||
Commands:
|
||||
check --cadence <daily|weekly> Check locked agents (JSON)
|
||||
lock --agent <name> --cadence <daily|weekly> Claim a lock
|
||||
list [--cadence <daily|weekly>] Print active lock table
|
||||
dashboard [--port <port>] Serve the dashboard (default: 4173)
|
||||
|
||||
Options:
|
||||
--dry-run Build and sign the event but do not publish
|
||||
@@ -514,19 +602,20 @@ Exit codes:
|
||||
3 Lock denied (already locked or race lost)`);
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const args = parseArgs(process.argv.slice(2));
|
||||
export async function main(argv) {
|
||||
try {
|
||||
const args = parseArgs(argv);
|
||||
|
||||
if (!args.command) {
|
||||
usage();
|
||||
process.exit(1);
|
||||
throw new ExitError(1, 'No command specified');
|
||||
}
|
||||
|
||||
switch (args.command) {
|
||||
case 'check': {
|
||||
if (!args.cadence || !VALID_CADENCES.has(args.cadence)) {
|
||||
console.error('ERROR: --cadence <daily|weekly> is required for check');
|
||||
process.exit(1);
|
||||
throw new ExitError(1, 'Missing cadence');
|
||||
}
|
||||
await cmdCheck(args.cadence);
|
||||
break;
|
||||
@@ -535,11 +624,11 @@ async function main() {
|
||||
case 'lock': {
|
||||
if (!args.agent) {
|
||||
console.error('ERROR: --agent <name> is required for lock');
|
||||
process.exit(1);
|
||||
throw new ExitError(1, 'Missing agent');
|
||||
}
|
||||
if (!args.cadence || !VALID_CADENCES.has(args.cadence)) {
|
||||
console.error('ERROR: --cadence <daily|weekly> is required for lock');
|
||||
process.exit(1);
|
||||
throw new ExitError(1, 'Missing cadence');
|
||||
}
|
||||
await cmdLock(args.agent, args.cadence, args.dryRun);
|
||||
break;
|
||||
@@ -548,20 +637,28 @@ async function main() {
|
||||
case 'list': {
|
||||
if (args.cadence && !VALID_CADENCES.has(args.cadence)) {
|
||||
console.error('ERROR: --cadence must be daily or weekly');
|
||||
process.exit(1);
|
||||
throw new ExitError(1, 'Invalid cadence');
|
||||
}
|
||||
await cmdList(args.cadence || null);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'dashboard': {
|
||||
await cmdDashboard(args.port);
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
console.error(`ERROR: Unknown command: ${args.command}`);
|
||||
usage();
|
||||
process.exit(1);
|
||||
throw new ExitError(1, 'Unknown command');
|
||||
}
|
||||
} catch (err) {
|
||||
if (err instanceof ExitError) {
|
||||
process.exit(err.code);
|
||||
} else {
|
||||
console.error(`torch-lock failed: ${err.message}`);
|
||||
process.exit(2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error(`nostr-lock failed: ${err.message}`);
|
||||
process.exit(2);
|
||||
});
|
||||
@@ -5,13 +5,13 @@ import fs from 'node:fs';
|
||||
|
||||
describe('CLI Smoke Test', () => {
|
||||
it('should print usage when no args provided', () => {
|
||||
const result = spawnSync('node', ['src/nostr-lock.mjs']);
|
||||
const result = spawnSync('node', ['bin/torch-lock.mjs']);
|
||||
assert.strictEqual(result.status, 1);
|
||||
assert.match(result.stderr.toString(), /Usage:/);
|
||||
});
|
||||
|
||||
it('should fail when checking without cadence', () => {
|
||||
const result = spawnSync('node', ['src/nostr-lock.mjs', 'check']);
|
||||
const result = spawnSync('node', ['bin/torch-lock.mjs', 'check']);
|
||||
assert.strictEqual(result.status, 1);
|
||||
assert.match(result.stderr.toString(), /--cadence <daily|weekly> is required/);
|
||||
});
|
||||
@@ -23,7 +23,7 @@ describe('CLI Smoke Test', () => {
|
||||
}));
|
||||
|
||||
try {
|
||||
const result = spawnSync('node', ['src/nostr-lock.mjs', 'check', '--cadence', 'daily'], {
|
||||
const result = spawnSync('node', ['bin/torch-lock.mjs', 'check', '--cadence', 'daily'], {
|
||||
env: { ...process.env, TORCH_CONFIG_PATH: configPath }
|
||||
});
|
||||
const output = JSON.parse(result.stdout.toString());
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"nostrLock": {
|
||||
"namespace": "torch-example",
|
||||
"relays": [
|
||||
"wss://relay.damus.io",
|
||||
"wss://nos.lol",
|
||||
"wss://relay.primal.net"
|
||||
],
|
||||
"ttlSeconds": 7200,
|
||||
"queryTimeoutMs": 15000
|
||||
},
|
||||
"dashboard": {
|
||||
"defaultCadenceView": "daily",
|
||||
"defaultStatusView": "active"
|
||||
},
|
||||
"scheduler": {
|
||||
"firstPromptByCadence": {
|
||||
"daily": "scheduler-update-agent",
|
||||
"weekly": "bug-reproducer-agent"
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user