mirror of
https://github.com/PR0M3TH3AN/TORCH.git
synced 2026-11-06 21:01:54 +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
+290
-167
No files matched your search
@@ -2,77 +2,69 @@
|
|||||||
|
|
||||||
TORCH is a portable Nostr-based task locking toolkit for multi-agent coordination.
|
TORCH is a portable Nostr-based task locking toolkit for multi-agent coordination.
|
||||||
|
|
||||||
## First-run quickstart
|
## Installation
|
||||||
|
|
||||||
From the repository root:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm install
|
npm install torch-lock
|
||||||
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
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### 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.namespace` (d-tag/tag namespace)
|
||||||
- `nostrLock.relays`
|
- `nostrLock.relays` (array of relay URLs)
|
||||||
- `nostrLock.ttlSeconds`
|
- `nostrLock.ttlSeconds` (lock duration in seconds)
|
||||||
- `nostrLock.queryTimeoutMs`
|
- `nostrLock.queryTimeoutMs` (timeout for relay queries)
|
||||||
- `nostrLock.dailyRoster` / `nostrLock.weeklyRoster`
|
- `nostrLock.dailyRoster` / `nostrLock.weeklyRoster` (explicit rosters)
|
||||||
- `dashboard.defaultCadenceView` (`daily`, `weekly`, or `all`)
|
- `dashboard.defaultCadenceView` (`daily`, `weekly`, or `all`)
|
||||||
- `dashboard.defaultStatusView` (`active` 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)
|
- `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`.
|
The dashboard provides a live view of lock events.
|
||||||
|
|
||||||
Optional dashboard:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm run dashboard:serve
|
npx torch-lock dashboard --port 4173
|
||||||
# then open http://localhost:4173/dashboard/
|
# 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
|
## Environment variables
|
||||||
|
|
||||||
|
You can override configuration using environment variables:
|
||||||
|
|
||||||
- `NOSTR_LOCK_NAMESPACE`
|
- `NOSTR_LOCK_NAMESPACE`
|
||||||
- `NOSTR_LOCK_RELAYS`
|
- `NOSTR_LOCK_RELAYS` (comma-separated)
|
||||||
- `NOSTR_LOCK_TTL`
|
- `NOSTR_LOCK_TTL`
|
||||||
- `NOSTR_LOCK_QUERY_TIMEOUT_MS`
|
- `NOSTR_LOCK_QUERY_TIMEOUT_MS`
|
||||||
- `NOSTR_LOCK_DAILY_ROSTER`
|
- `NOSTR_LOCK_DAILY_ROSTER` (comma-separated list of agents)
|
||||||
- `NOSTR_LOCK_WEEKLY_ROSTER`
|
- `NOSTR_LOCK_WEEKLY_ROSTER` (comma-separated list of agents)
|
||||||
- `TORCH_CONFIG_PATH`
|
- `TORCH_CONFIG_PATH` (path to config file)
|
||||||
- `AGENT_PLATFORM`
|
- `AGENT_PLATFORM` (platform identifier, e.g. `codex`)
|
||||||
|
|
||||||
## Roster precedence
|
## 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).
|
1. `NOSTR_LOCK_DAILY_ROSTER` / `NOSTR_LOCK_WEEKLY_ROSTER` (comma-separated env overrides).
|
||||||
2. `torch-config.json` (`nostrLock.dailyRoster` / `nostrLock.weeklyRoster`).
|
2. `torch-config.json` (`nostrLock.dailyRoster` / `nostrLock.weeklyRoster`).
|
||||||
3. `src/prompts/roster.json` (`daily` / `weekly` canonical scheduler roster).
|
3. `src/prompts/roster.json` (canonical scheduler roster included in the package).
|
||||||
4. Built-in fallback roster (used only if `src/prompts/roster.json` is unreadable).
|
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
|
If you are developing `torch-lock` itself:
|
||||||
NOSTR_LOCK_NAMESPACE=my-project \
|
|
||||||
AGENT_PLATFORM=codex \
|
- `npm run lock:check:daily`
|
||||||
node src/nostr-lock.mjs lock --agent docs-agent --cadence 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",
|
"nostr-tools": "2.19.4",
|
||||||
"ws": "8.19.0"
|
"ws": "8.19.0"
|
||||||
},
|
},
|
||||||
|
"bin": {
|
||||||
|
"torch-lock": "bin/torch-lock.mjs"
|
||||||
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^10.0.1",
|
"@eslint/js": "^10.0.1",
|
||||||
"eslint": "^10.0.0",
|
"eslint": "^10.0.0",
|
||||||
@@ -252,22 +255,6 @@
|
|||||||
"url": "https://paulmillr.com/funding/"
|
"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": {
|
"node_modules/@scure/base": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.1.tgz",
|
||||||
|
|||||||
+21
-7
@@ -1,16 +1,30 @@
|
|||||||
{
|
{
|
||||||
"name": "torch-lock",
|
"name": "torch-lock",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"private": true,
|
|
||||||
"type": "module",
|
"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": {
|
"scripts": {
|
||||||
"test": "node --test test/*.test.mjs",
|
"test": "node --test test/*.test.mjs",
|
||||||
"lock:check:daily": "node src/nostr-lock.mjs check --cadence daily",
|
"lock:check:daily": "node bin/torch-lock.mjs check --cadence daily",
|
||||||
"lock:check:weekly": "node src/nostr-lock.mjs check --cadence weekly",
|
"lock:check:weekly": "node bin/torch-lock.mjs check --cadence weekly",
|
||||||
"lock:list": "node src/nostr-lock.mjs list",
|
"lock:list": "node bin/torch-lock.mjs list",
|
||||||
"lock:lock": "node src/nostr-lock.mjs lock",
|
"lock:lock": "node bin/torch-lock.mjs lock",
|
||||||
"dashboard:serve": "python3 -m http.server 4173",
|
"dashboard:serve": "node bin/torch-lock.mjs dashboard",
|
||||||
"build": "node build.mjs",
|
"build": "node build.mjs",
|
||||||
"lint": "eslint ."
|
"lint": "eslint ."
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
|
|
||||||
import fs from 'node:fs';
|
import fs from 'node:fs';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
|
import http from 'node:http';
|
||||||
import { fileURLToPath } from 'node:url';
|
import { fileURLToPath } from 'node:url';
|
||||||
import { generateSecretKey, getPublicKey, finalizeEvent } from 'nostr-tools/pure';
|
import { generateSecretKey, getPublicKey, finalizeEvent } from 'nostr-tools/pure';
|
||||||
import { SimplePool } from 'nostr-tools/pool';
|
import { SimplePool } from 'nostr-tools/pool';
|
||||||
@@ -25,6 +26,7 @@ const DEFAULT_NAMESPACE = 'torch';
|
|||||||
const DEFAULT_QUERY_TIMEOUT_MS = 15_000;
|
const DEFAULT_QUERY_TIMEOUT_MS = 15_000;
|
||||||
const VALID_CADENCES = new Set(['daily', 'weekly']);
|
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 ROSTER_FILE = path.resolve(path.dirname(fileURLToPath(import.meta.url)), 'prompts/roster.json');
|
||||||
const FALLBACK_ROSTER = {
|
const FALLBACK_ROSTER = {
|
||||||
daily: [
|
daily: [
|
||||||
@@ -72,6 +74,14 @@ const FALLBACK_ROSTER = {
|
|||||||
|
|
||||||
let cachedCanonicalRoster = null;
|
let cachedCanonicalRoster = null;
|
||||||
|
|
||||||
|
// Custom Error class for controlled exits
|
||||||
|
class ExitError extends Error {
|
||||||
|
constructor(code, message) {
|
||||||
|
super(message);
|
||||||
|
this.code = code;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function getRelays() {
|
function getRelays() {
|
||||||
const config = loadTorchConfig();
|
const config = loadTorchConfig();
|
||||||
const envRelays = process.env.NOSTR_LOCK_RELAYS;
|
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}`);
|
console.error(`WARNING: Roster file is missing daily/weekly entries, falling back: ${ROSTER_FILE}`);
|
||||||
} catch (err) {
|
} 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;
|
cachedCanonicalRoster = FALLBACK_ROSTER;
|
||||||
@@ -206,7 +217,7 @@ function filterActiveLocks(locks) {
|
|||||||
return locks.filter((lock) => !lock.expiresAt || lock.expiresAt > now);
|
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 pool = new SimplePool();
|
||||||
const tagFilter = `${namespace}-lock-${cadence}-${dateStr}`;
|
const tagFilter = `${namespace}-lock-${cadence}-${dateStr}`;
|
||||||
const queryTimeoutMs = getQueryTimeoutMs();
|
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();
|
const pool = new SimplePool();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -255,7 +266,7 @@ async function publishLock(relays, event) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function cmdCheck(cadence) {
|
export async function cmdCheck(cadence) {
|
||||||
const relays = getRelays();
|
const relays = getRelays();
|
||||||
const namespace = getNamespace();
|
const namespace = getNamespace();
|
||||||
const dateStr = todayDateStr();
|
const dateStr = todayDateStr();
|
||||||
@@ -276,33 +287,30 @@ async function cmdCheck(cadence) {
|
|||||||
const unknownLockedAgents = lockedAgents.filter((agent) => !roster.includes(agent));
|
const unknownLockedAgents = lockedAgents.filter((agent) => !roster.includes(agent));
|
||||||
const available = roster.filter((a) => !excludedAgents.includes(a));
|
const available = roster.filter((a) => !excludedAgents.includes(a));
|
||||||
|
|
||||||
console.log(
|
const result = {
|
||||||
JSON.stringify(
|
namespace,
|
||||||
{
|
cadence,
|
||||||
namespace,
|
date: dateStr,
|
||||||
cadence,
|
locked: lockedAgents.sort(),
|
||||||
date: dateStr,
|
paused: pausedAgents.sort(),
|
||||||
locked: lockedAgents.sort(),
|
excluded: excludedAgents.sort(),
|
||||||
paused: pausedAgents.sort(),
|
available: available.sort(),
|
||||||
excluded: excludedAgents.sort(),
|
lockCount: locks.length,
|
||||||
available: available.sort(),
|
unknownLockedAgents: unknownLockedAgents.sort(),
|
||||||
lockCount: locks.length,
|
locks: locks.map((l) => ({
|
||||||
unknownLockedAgents: unknownLockedAgents.sort(),
|
agent: l.agent,
|
||||||
locks: locks.map((l) => ({
|
eventId: l.eventId,
|
||||||
agent: l.agent,
|
createdAt: l.createdAtIso,
|
||||||
eventId: l.eventId,
|
expiresAt: l.expiresAtIso,
|
||||||
createdAt: l.createdAtIso,
|
platform: l.platform,
|
||||||
expiresAt: l.expiresAtIso,
|
})),
|
||||||
platform: l.platform,
|
};
|
||||||
})),
|
|
||||||
},
|
console.log(JSON.stringify(result, null, 2));
|
||||||
null,
|
return result;
|
||||||
2,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function cmdLock(agent, cadence, dryRun = false) {
|
export async function cmdLock(agent, cadence, dryRun = false) {
|
||||||
const relays = getRelays();
|
const relays = getRelays();
|
||||||
const namespace = getNamespace();
|
const namespace = getNamespace();
|
||||||
const dateStr = todayDateStr();
|
const dateStr = todayDateStr();
|
||||||
@@ -318,7 +326,7 @@ async function cmdLock(agent, cadence, dryRun = false) {
|
|||||||
if (!roster.includes(agent)) {
|
if (!roster.includes(agent)) {
|
||||||
console.error(`ERROR: agent "${agent}" is not in the ${cadence} roster`);
|
console.error(`ERROR: agent "${agent}" is not in the ${cadence} roster`);
|
||||||
console.error(`Allowed ${cadence} agents: ${roster.join(', ')}`);
|
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...');
|
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_STATUS=denied');
|
||||||
console.log('LOCK_REASON=already_locked');
|
console.log('LOCK_REASON=already_locked');
|
||||||
console.log(`LOCK_EXISTING_EVENT=${earliest.eventId}`);
|
console.log(`LOCK_EXISTING_EVENT=${earliest.eventId}`);
|
||||||
process.exit(3);
|
throw new ExitError(3, 'Lock denied');
|
||||||
}
|
}
|
||||||
|
|
||||||
console.error('Step 2: Generating ephemeral keypair...');
|
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_STATUS=race_lost');
|
||||||
console.log('LOCK_REASON=earlier_claim_exists');
|
console.log('LOCK_REASON=earlier_claim_exists');
|
||||||
console.log(`LOCK_WINNER_EVENT=${winner.eventId}`);
|
console.log(`LOCK_WINNER_EVENT=${winner.eventId}`);
|
||||||
process.exit(3);
|
throw new ExitError(3, 'Race check lost');
|
||||||
}
|
}
|
||||||
|
|
||||||
console.error('RACE CHECK: won');
|
console.error('RACE CHECK: won');
|
||||||
@@ -407,9 +415,10 @@ async function cmdLock(agent, cadence, dryRun = false) {
|
|||||||
console.log(`LOCK_DATE=${dateStr}`);
|
console.log(`LOCK_DATE=${dateStr}`);
|
||||||
console.log(`LOCK_EXPIRES=${expiresAt}`);
|
console.log(`LOCK_EXPIRES=${expiresAt}`);
|
||||||
console.log(`LOCK_EXPIRES_ISO=${new Date(expiresAt * 1000).toISOString()}`);
|
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 relays = getRelays();
|
||||||
const namespace = getNamespace();
|
const namespace = getNamespace();
|
||||||
const dateStr = todayDateStr();
|
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) {
|
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;
|
let i = 0;
|
||||||
|
|
||||||
if (argv.length > 0 && !argv[0].startsWith('-')) {
|
if (argv.length > 0 && !argv[0].startsWith('-')) {
|
||||||
@@ -480,6 +565,8 @@ function parseArgs(argv) {
|
|||||||
args.cadence = arg.split('=')[1];
|
args.cadence = arg.split('=')[1];
|
||||||
} else if (arg === '--dry-run') {
|
} else if (arg === '--dry-run') {
|
||||||
args.dryRun = true;
|
args.dryRun = true;
|
||||||
|
} else if (arg === '--port') {
|
||||||
|
args.port = parseInt(argv[++i], 10) || 4173;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -487,12 +574,13 @@ function parseArgs(argv) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function usage() {
|
function usage() {
|
||||||
console.error(`Usage: node src/nostr-lock.mjs <command> [options]
|
console.error(`Usage: torch-lock <command> [options]
|
||||||
|
|
||||||
Commands:
|
Commands:
|
||||||
check --cadence <daily|weekly> Check locked agents (JSON)
|
check --cadence <daily|weekly> Check locked agents (JSON)
|
||||||
lock --agent <name> --cadence <daily|weekly> Claim a lock
|
lock --agent <name> --cadence <daily|weekly> Claim a lock
|
||||||
list [--cadence <daily|weekly>] Print active lock table
|
list [--cadence <daily|weekly>] Print active lock table
|
||||||
|
dashboard [--port <port>] Serve the dashboard (default: 4173)
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
--dry-run Build and sign the event but do not publish
|
--dry-run Build and sign the event but do not publish
|
||||||
@@ -514,54 +602,63 @@ Exit codes:
|
|||||||
3 Lock denied (already locked or race lost)`);
|
3 Lock denied (already locked or race lost)`);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function main() {
|
export async function main(argv) {
|
||||||
const args = parseArgs(process.argv.slice(2));
|
try {
|
||||||
|
const args = parseArgs(argv);
|
||||||
|
|
||||||
if (!args.command) {
|
if (!args.command) {
|
||||||
usage();
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
await cmdCheck(args.cadence);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'lock': {
|
|
||||||
if (!args.agent) {
|
|
||||||
console.error('ERROR: --agent <name> is required for lock');
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
if (!args.cadence || !VALID_CADENCES.has(args.cadence)) {
|
|
||||||
console.error('ERROR: --cadence <daily|weekly> is required for lock');
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
await cmdLock(args.agent, args.cadence, args.dryRun);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'list': {
|
|
||||||
if (args.cadence && !VALID_CADENCES.has(args.cadence)) {
|
|
||||||
console.error('ERROR: --cadence must be daily or weekly');
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
await cmdList(args.cadence || null);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
default:
|
|
||||||
console.error(`ERROR: Unknown command: ${args.command}`);
|
|
||||||
usage();
|
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');
|
||||||
|
throw new ExitError(1, 'Missing cadence');
|
||||||
|
}
|
||||||
|
await cmdCheck(args.cadence);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'lock': {
|
||||||
|
if (!args.agent) {
|
||||||
|
console.error('ERROR: --agent <name> is required for lock');
|
||||||
|
throw new ExitError(1, 'Missing agent');
|
||||||
|
}
|
||||||
|
if (!args.cadence || !VALID_CADENCES.has(args.cadence)) {
|
||||||
|
console.error('ERROR: --cadence <daily|weekly> is required for lock');
|
||||||
|
throw new ExitError(1, 'Missing cadence');
|
||||||
|
}
|
||||||
|
await cmdLock(args.agent, args.cadence, args.dryRun);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'list': {
|
||||||
|
if (args.cadence && !VALID_CADENCES.has(args.cadence)) {
|
||||||
|
console.error('ERROR: --cadence must be daily or weekly');
|
||||||
|
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();
|
||||||
|
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', () => {
|
describe('CLI Smoke Test', () => {
|
||||||
it('should print usage when no args provided', () => {
|
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.strictEqual(result.status, 1);
|
||||||
assert.match(result.stderr.toString(), /Usage:/);
|
assert.match(result.stderr.toString(), /Usage:/);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should fail when checking without cadence', () => {
|
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.strictEqual(result.status, 1);
|
||||||
assert.match(result.stderr.toString(), /--cadence <daily|weekly> is required/);
|
assert.match(result.stderr.toString(), /--cadence <daily|weekly> is required/);
|
||||||
});
|
});
|
||||||
@@ -23,7 +23,7 @@ describe('CLI Smoke Test', () => {
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
try {
|
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 }
|
env: { ...process.env, TORCH_CONFIG_PATH: configPath }
|
||||||
});
|
});
|
||||||
const output = JSON.parse(result.stdout.toString());
|
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