mirror of
https://github.com/PR0M3TH3AN/TORCH.git
synced 2026-11-06 04:42:24 +00:00
feat: add init and update commands for user-space configuration
Co-authored-by: PR0M3TH3AN <[email protected]>
This commit is contained in:
1 parent
cff3fc17aa
commit
b0381b8c39
4 files changed
+346
-7
No files matched your search
+30
-6
@@ -12,6 +12,7 @@ import { SimplePool } from 'nostr-tools/pool';
|
|||||||
import { useWebSocketImplementation } from 'nostr-tools/relay';
|
import { useWebSocketImplementation } from 'nostr-tools/relay';
|
||||||
import WebSocket from 'ws';
|
import WebSocket from 'ws';
|
||||||
import { loadTorchConfig } from './torch-config.mjs';
|
import { loadTorchConfig } from './torch-config.mjs';
|
||||||
|
import { cmdInit, cmdUpdate } from './ops.mjs';
|
||||||
|
|
||||||
useWebSocketImplementation(WebSocket);
|
useWebSocketImplementation(WebSocket);
|
||||||
|
|
||||||
@@ -28,6 +29,8 @@ const VALID_CADENCES = new Set(['daily', 'weekly']);
|
|||||||
|
|
||||||
// If installed as a package, prompts/roster.json is relative to this file
|
// 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 USER_ROSTER_FILE = path.resolve(process.cwd(), 'torch/roster.json');
|
||||||
|
|
||||||
const FALLBACK_ROSTER = {
|
const FALLBACK_ROSTER = {
|
||||||
daily: [
|
daily: [
|
||||||
'audit-agent',
|
'audit-agent',
|
||||||
@@ -127,8 +130,15 @@ function getQueryTimeoutMs() {
|
|||||||
function loadCanonicalRoster() {
|
function loadCanonicalRoster() {
|
||||||
if (cachedCanonicalRoster) return cachedCanonicalRoster;
|
if (cachedCanonicalRoster) return cachedCanonicalRoster;
|
||||||
|
|
||||||
|
let rosterPath = ROSTER_FILE;
|
||||||
|
|
||||||
|
// Prefer user-managed roster if present
|
||||||
|
if (fs.existsSync(USER_ROSTER_FILE)) {
|
||||||
|
rosterPath = USER_ROSTER_FILE;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const parsed = JSON.parse(fs.readFileSync(ROSTER_FILE, 'utf8'));
|
const parsed = JSON.parse(fs.readFileSync(rosterPath, 'utf8'));
|
||||||
const daily = Array.isArray(parsed.daily) ? parsed.daily.map((item) => String(item).trim()).filter(Boolean) : [];
|
const daily = Array.isArray(parsed.daily) ? parsed.daily.map((item) => String(item).trim()).filter(Boolean) : [];
|
||||||
const weekly = Array.isArray(parsed.weekly)
|
const weekly = Array.isArray(parsed.weekly)
|
||||||
? parsed.weekly.map((item) => String(item).trim()).filter(Boolean)
|
? parsed.weekly.map((item) => String(item).trim()).filter(Boolean)
|
||||||
@@ -139,10 +149,9 @@ function loadCanonicalRoster() {
|
|||||||
return cachedCanonicalRoster;
|
return cachedCanonicalRoster;
|
||||||
}
|
}
|
||||||
|
|
||||||
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: ${rosterPath}`);
|
||||||
} catch (err) {
|
} catch {
|
||||||
// It's okay if roster file is missing when used as a library/CLI without the file present
|
// 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;
|
||||||
@@ -500,7 +509,7 @@ export async function cmdDashboard(port = 4173) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Security check: prevent directory traversal
|
// Security check: prevent directory traversal
|
||||||
const safePath = path.normalize(pathname).replace(/^(\.\.[\/\\])+/, '');
|
const safePath = path.normalize(pathname).replace(new RegExp('^(\\.\\.[\\/\\\\])+'), '');
|
||||||
let filePath = path.join(packageRoot, safePath);
|
let filePath = path.join(packageRoot, safePath);
|
||||||
|
|
||||||
// If directory, try index.html
|
// If directory, try index.html
|
||||||
@@ -545,7 +554,7 @@ export async function cmdDashboard(port = 4173) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function parseArgs(argv) {
|
function parseArgs(argv) {
|
||||||
const args = { command: null, agent: null, cadence: null, dryRun: false, port: 4173 };
|
const args = { command: null, agent: null, cadence: null, dryRun: false, force: false, port: 4173 };
|
||||||
let i = 0;
|
let i = 0;
|
||||||
|
|
||||||
if (argv.length > 0 && !argv[0].startsWith('-')) {
|
if (argv.length > 0 && !argv[0].startsWith('-')) {
|
||||||
@@ -565,6 +574,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 === '--force') {
|
||||||
|
args.force = true;
|
||||||
} else if (arg === '--port') {
|
} else if (arg === '--port') {
|
||||||
args.port = parseInt(argv[++i], 10) || 4173;
|
args.port = parseInt(argv[++i], 10) || 4173;
|
||||||
}
|
}
|
||||||
@@ -581,9 +592,12 @@ Commands:
|
|||||||
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)
|
dashboard [--port <port>] Serve the dashboard (default: 4173)
|
||||||
|
init [--force] Initialize torch/ directory in current project
|
||||||
|
update [--force] Update torch/ configuration (backups, merges)
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
--dry-run Build and sign the event but do not publish
|
--dry-run Build and sign the event but do not publish
|
||||||
|
--force Overwrite existing files (for init) or all files (for update)
|
||||||
|
|
||||||
Environment:
|
Environment:
|
||||||
NOSTR_LOCK_NAMESPACE Namespace prefix for lock tags (default: torch)
|
NOSTR_LOCK_NAMESPACE Namespace prefix for lock tags (default: torch)
|
||||||
@@ -648,6 +662,16 @@ export async function main(argv) {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case 'init': {
|
||||||
|
await cmdInit(args.force);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'update': {
|
||||||
|
await cmdUpdate(args.force);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
default:
|
default:
|
||||||
console.error(`ERROR: Unknown command: ${args.command}`);
|
console.error(`ERROR: Unknown command: ${args.command}`);
|
||||||
usage();
|
usage();
|
||||||
|
|||||||
+192
@@ -0,0 +1,192 @@
|
|||||||
|
import fs from 'node:fs';
|
||||||
|
import path from 'node:path';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
|
||||||
|
const PKG_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
||||||
|
|
||||||
|
// Source paths (in the package)
|
||||||
|
const SRC_PROMPTS_DIR = path.join(PKG_ROOT, 'src', 'prompts');
|
||||||
|
|
||||||
|
// Files to treat as "Static" (always overwrite on update, with transformations)
|
||||||
|
const STATIC_FILES = [
|
||||||
|
'META_PROMPTS.md',
|
||||||
|
'scheduler-flow.md',
|
||||||
|
'daily-scheduler.md',
|
||||||
|
'weekly-scheduler.md',
|
||||||
|
];
|
||||||
|
|
||||||
|
// Directories containing "Evolving" files (copy if missing, preserve if present)
|
||||||
|
const EVOLVING_DIRS = ['daily', 'weekly'];
|
||||||
|
|
||||||
|
function getPaths(userRoot) {
|
||||||
|
const torchDir = path.join(userRoot, 'torch');
|
||||||
|
return {
|
||||||
|
root: userRoot,
|
||||||
|
torchDir,
|
||||||
|
promptsDir: path.join(torchDir, 'prompts'),
|
||||||
|
roster: path.join(torchDir, 'roster.json'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureDir(dir) {
|
||||||
|
if (!fs.existsSync(dir)) {
|
||||||
|
fs.mkdirSync(dir, { recursive: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function transformContent(content) {
|
||||||
|
// Replace source paths with user paths
|
||||||
|
return content
|
||||||
|
.replace(/src\/prompts\/daily\//g, 'torch/prompts/daily/')
|
||||||
|
.replace(/src\/prompts\/weekly\//g, 'torch/prompts/weekly/')
|
||||||
|
.replace(/src\/prompts\/roster\.json/g, 'torch/roster.json')
|
||||||
|
.replace(/src\/prompts\/scheduler-flow\.md/g, 'torch/scheduler-flow.md');
|
||||||
|
}
|
||||||
|
|
||||||
|
function copyFile(src, dest, transform = false, overwrite = true) {
|
||||||
|
if (fs.existsSync(dest) && !overwrite) {
|
||||||
|
return false; // Skipped
|
||||||
|
}
|
||||||
|
|
||||||
|
const content = fs.readFileSync(src, 'utf8');
|
||||||
|
const finalContent = transform ? transformContent(content) : content;
|
||||||
|
fs.writeFileSync(dest, finalContent, 'utf8');
|
||||||
|
return true; // Copied/Overwritten
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function cmdInit(force = false, cwd = process.cwd()) {
|
||||||
|
const paths = getPaths(cwd);
|
||||||
|
console.log(`Initializing torch configuration in ${paths.torchDir}...`);
|
||||||
|
|
||||||
|
if (fs.existsSync(paths.torchDir) && !force) {
|
||||||
|
throw new Error(`${paths.torchDir} already exists. Use --force to overwrite.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
ensureDir(paths.torchDir);
|
||||||
|
ensureDir(paths.promptsDir);
|
||||||
|
|
||||||
|
// 1. Copy Roster (Evolving, but initially copied)
|
||||||
|
const srcRoster = path.join(SRC_PROMPTS_DIR, 'roster.json');
|
||||||
|
if (fs.existsSync(srcRoster)) {
|
||||||
|
copyFile(srcRoster, paths.roster, false, true);
|
||||||
|
console.log(`Created ${path.relative(paths.root, paths.roster)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Copy Static Files (Transformed)
|
||||||
|
for (const file of STATIC_FILES) {
|
||||||
|
const src = path.join(SRC_PROMPTS_DIR, file);
|
||||||
|
const dest = path.join(paths.torchDir, file);
|
||||||
|
if (fs.existsSync(src)) {
|
||||||
|
copyFile(src, dest, true, true);
|
||||||
|
console.log(`Created ${path.relative(paths.root, dest)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Copy Prompts (Evolving)
|
||||||
|
for (const dir of EVOLVING_DIRS) {
|
||||||
|
const srcDir = path.join(SRC_PROMPTS_DIR, dir);
|
||||||
|
const destDir = path.join(paths.promptsDir, dir);
|
||||||
|
ensureDir(destDir);
|
||||||
|
|
||||||
|
if (fs.existsSync(srcDir)) {
|
||||||
|
const files = fs.readdirSync(srcDir);
|
||||||
|
for (const file of files) {
|
||||||
|
const srcFile = path.join(srcDir, file);
|
||||||
|
const destFile = path.join(destDir, file);
|
||||||
|
copyFile(srcFile, destFile, false, true);
|
||||||
|
}
|
||||||
|
console.log(`Created ${files.length} files in ${path.relative(paths.root, destDir)}/`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('\nInitialization complete.');
|
||||||
|
console.log('You can now customize the files in torch/ and torch/prompts/.');
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function cmdUpdate(force = false, cwd = process.cwd()) {
|
||||||
|
const paths = getPaths(cwd);
|
||||||
|
console.log(`Updating torch configuration in ${paths.torchDir}...`);
|
||||||
|
|
||||||
|
if (!fs.existsSync(paths.torchDir)) {
|
||||||
|
throw new Error(`${paths.torchDir} not found. Run 'torch-lock init' first.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. Backup
|
||||||
|
const backupName = `backup_${new Date().toISOString().replace(/[:.]/g, '-')}`;
|
||||||
|
const backupRoot = path.join(paths.torchDir, '_backups');
|
||||||
|
const thisBackupDir = path.join(backupRoot, backupName);
|
||||||
|
|
||||||
|
ensureDir(thisBackupDir);
|
||||||
|
console.log(`Creating backup at ${path.relative(paths.root, thisBackupDir)}...`);
|
||||||
|
|
||||||
|
const entries = fs.readdirSync(paths.torchDir);
|
||||||
|
for (const entry of entries) {
|
||||||
|
// Skip the backups directory itself to avoid recursion
|
||||||
|
if (entry === '_backups') continue;
|
||||||
|
|
||||||
|
const srcPath = path.join(paths.torchDir, entry);
|
||||||
|
const destPath = path.join(thisBackupDir, entry);
|
||||||
|
fs.cpSync(srcPath, destPath, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Update Static Files (Always Overwrite)
|
||||||
|
console.log('Updating static files...');
|
||||||
|
for (const file of STATIC_FILES) {
|
||||||
|
const src = path.join(SRC_PROMPTS_DIR, file);
|
||||||
|
const dest = path.join(paths.torchDir, file);
|
||||||
|
if (fs.existsSync(src)) {
|
||||||
|
copyFile(src, dest, true, true);
|
||||||
|
console.log(` Updated ${file}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Update Roster (Preserve unless force)
|
||||||
|
const srcRoster = path.join(SRC_PROMPTS_DIR, 'roster.json');
|
||||||
|
if (fs.existsSync(srcRoster)) {
|
||||||
|
if (force) {
|
||||||
|
copyFile(srcRoster, paths.roster, false, true);
|
||||||
|
console.log(' Overwrote roster.json (forced)');
|
||||||
|
} else {
|
||||||
|
console.log(' Skipped roster.json (preserved)');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Update Prompts (Copy missing, preserve existing unless force)
|
||||||
|
console.log('Updating prompts...');
|
||||||
|
for (const dir of EVOLVING_DIRS) {
|
||||||
|
const srcDir = path.join(SRC_PROMPTS_DIR, dir);
|
||||||
|
const destDir = path.join(paths.promptsDir, dir);
|
||||||
|
ensureDir(destDir);
|
||||||
|
|
||||||
|
if (fs.existsSync(srcDir)) {
|
||||||
|
const files = fs.readdirSync(srcDir);
|
||||||
|
let added = 0;
|
||||||
|
let updated = 0;
|
||||||
|
let skipped = 0;
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
const srcFile = path.join(srcDir, file);
|
||||||
|
const destFile = path.join(destDir, file);
|
||||||
|
|
||||||
|
// If force is true, we overwrite (updated++).
|
||||||
|
// If force is false and file missing, we write (added++).
|
||||||
|
// If force is false and file exists, we skip (skipped++).
|
||||||
|
|
||||||
|
if (force) {
|
||||||
|
copyFile(srcFile, destFile, false, true);
|
||||||
|
updated++;
|
||||||
|
} else {
|
||||||
|
if (!fs.existsSync(destFile)) {
|
||||||
|
copyFile(srcFile, destFile, false, true);
|
||||||
|
added++;
|
||||||
|
} else {
|
||||||
|
skipped++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.log(` ${dir}/: ${added} added, ${updated} updated, ${skipped} preserved`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('\nUpdate complete.');
|
||||||
|
}
|
||||||
@@ -37,7 +37,7 @@ export function loadTorchConfig() {
|
|||||||
try {
|
try {
|
||||||
raw = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
raw = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
throw new Error(`Failed to parse ${configPath}: ${err.message}`);
|
throw new Error(`Failed to parse ${configPath}: ${err.message}`, { cause: err });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,123 @@
|
|||||||
|
import { test, after } from 'node:test';
|
||||||
|
import assert from 'node:assert';
|
||||||
|
import fs from 'node:fs';
|
||||||
|
import path from 'node:path';
|
||||||
|
import { execSync } from 'node:child_process';
|
||||||
|
import { cmdInit, cmdUpdate } from '../src/ops.mjs';
|
||||||
|
|
||||||
|
const tempBase = fs.mkdtempSync(path.join(process.cwd(), 'test-ops-'));
|
||||||
|
|
||||||
|
after(() => {
|
||||||
|
fs.rmSync(tempBase, { recursive: true, force: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('cmdInit creates directory structure and files', async () => {
|
||||||
|
const projectRoot = path.join(tempBase, 'project1');
|
||||||
|
fs.mkdirSync(projectRoot, { recursive: true });
|
||||||
|
|
||||||
|
await cmdInit(false, projectRoot);
|
||||||
|
|
||||||
|
const torchDir = path.join(projectRoot, 'torch');
|
||||||
|
assert.ok(fs.existsSync(torchDir), 'torch dir created');
|
||||||
|
assert.ok(fs.existsSync(path.join(torchDir, 'prompts', 'daily')), 'daily prompts created');
|
||||||
|
assert.ok(fs.existsSync(path.join(torchDir, 'roster.json')), 'roster created');
|
||||||
|
assert.ok(fs.existsSync(path.join(torchDir, 'META_PROMPTS.md')), 'META_PROMPTS.md created');
|
||||||
|
|
||||||
|
// Check content transformation
|
||||||
|
const metaPrompts = fs.readFileSync(path.join(torchDir, 'META_PROMPTS.md'), 'utf8');
|
||||||
|
assert.ok(metaPrompts.includes('torch/prompts/daily/'), 'paths replaced in META_PROMPTS');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('cmdInit fails if directory exists without force', async () => {
|
||||||
|
const projectRoot = path.join(tempBase, 'project2');
|
||||||
|
fs.mkdirSync(path.join(projectRoot, 'torch'), { recursive: true });
|
||||||
|
|
||||||
|
await assert.rejects(
|
||||||
|
async () => await cmdInit(false, projectRoot),
|
||||||
|
/already exists/,
|
||||||
|
'Should fail if torch dir exists'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('cmdUpdate preserves modified prompt', async () => {
|
||||||
|
const projectRoot = path.join(tempBase, 'project3');
|
||||||
|
fs.mkdirSync(projectRoot, { recursive: true });
|
||||||
|
|
||||||
|
await cmdInit(false, projectRoot);
|
||||||
|
|
||||||
|
const dailyDir = path.join(projectRoot, 'torch', 'prompts', 'daily');
|
||||||
|
const files = fs.readdirSync(dailyDir);
|
||||||
|
assert.ok(files.length > 0, 'Should have daily prompts');
|
||||||
|
|
||||||
|
const targetFile = path.join(dailyDir, files[0]);
|
||||||
|
fs.writeFileSync(targetFile, 'MODIFIED CONTENT', 'utf8');
|
||||||
|
|
||||||
|
await cmdUpdate(false, projectRoot);
|
||||||
|
|
||||||
|
const content = fs.readFileSync(targetFile, 'utf8');
|
||||||
|
assert.strictEqual(content, 'MODIFIED CONTENT', 'Prompt should be preserved');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('cmdUpdate overwrites prompt with force', async () => {
|
||||||
|
const projectRoot = path.join(tempBase, 'project4');
|
||||||
|
fs.mkdirSync(projectRoot, { recursive: true });
|
||||||
|
|
||||||
|
await cmdInit(false, projectRoot);
|
||||||
|
|
||||||
|
const dailyDir = path.join(projectRoot, 'torch', 'prompts', 'daily');
|
||||||
|
const files = fs.readdirSync(dailyDir);
|
||||||
|
const targetFile = path.join(dailyDir, files[0]);
|
||||||
|
|
||||||
|
fs.writeFileSync(targetFile, 'MODIFIED CONTENT', 'utf8');
|
||||||
|
|
||||||
|
await cmdUpdate(true, projectRoot); // Force update
|
||||||
|
|
||||||
|
const content = fs.readFileSync(targetFile, 'utf8');
|
||||||
|
assert.notStrictEqual(content, 'MODIFIED CONTENT', 'Prompt should be overwritten');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('cmdUpdate creates backup', async () => {
|
||||||
|
const projectRoot = path.join(tempBase, 'project5');
|
||||||
|
fs.mkdirSync(projectRoot, { recursive: true });
|
||||||
|
|
||||||
|
await cmdInit(false, projectRoot);
|
||||||
|
|
||||||
|
await cmdUpdate(false, projectRoot);
|
||||||
|
|
||||||
|
const torchDir = path.join(projectRoot, 'torch');
|
||||||
|
const backupRoot = path.join(torchDir, '_backups');
|
||||||
|
assert.ok(fs.existsSync(backupRoot), '_backups directory created');
|
||||||
|
|
||||||
|
const entries = fs.readdirSync(backupRoot);
|
||||||
|
assert.ok(entries.length > 0, 'Should have at least one backup');
|
||||||
|
const backupName = entries[0];
|
||||||
|
assert.match(backupName, /^backup_/, 'Backup directory name starts with backup_');
|
||||||
|
|
||||||
|
const backupContent = fs.readdirSync(path.join(backupRoot, backupName));
|
||||||
|
assert.ok(backupContent.includes('roster.json'), 'Backup contains roster.json');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('torch-lock check respects local roster', async () => {
|
||||||
|
const projectRoot = path.join(tempBase, 'project_roster');
|
||||||
|
fs.mkdirSync(projectRoot, { recursive: true });
|
||||||
|
|
||||||
|
// Init
|
||||||
|
await cmdInit(false, projectRoot);
|
||||||
|
|
||||||
|
// Modify roster
|
||||||
|
const rosterPath = path.join(projectRoot, 'torch', 'roster.json');
|
||||||
|
const roster = JSON.parse(fs.readFileSync(rosterPath, 'utf8'));
|
||||||
|
roster.daily.push('custom-agent-007');
|
||||||
|
fs.writeFileSync(rosterPath, JSON.stringify(roster, null, 2));
|
||||||
|
|
||||||
|
// Run CLI
|
||||||
|
const binPath = path.resolve(process.cwd(), 'bin/torch-lock.mjs');
|
||||||
|
|
||||||
|
const output = execSync(`${process.execPath} ${binPath} check --cadence daily`, {
|
||||||
|
cwd: projectRoot,
|
||||||
|
encoding: 'utf8'
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = JSON.parse(output);
|
||||||
|
assert.ok(result.available.includes('custom-agent-007'), 'Custom agent found in roster');
|
||||||
|
});
|
||||||
Reference in new issue
Block a user