mirror of
https://github.com/PR0M3TH3AN/TORCH.git
synced 2026-10-27 07:51:52 +00:00
Improve torch init experience with interactive prompts and script injection
- Modified `init` to be interactive, asking for install directory, namespace, and relays. - Added automatic injection of convenience scripts (`torch:check`, `torch:lock`, etc.) into the host `package.json`. - Increased default network timeouts (30s query, 15s publish) to reduce failure rate. - Added visual feedback (logs) during relay connection and querying. - Refactored `src/ops.mjs` to support flexible installation paths. - Updated `src/torch-config.mjs` to support config resolution from parent directories (e.g., when running via `npm run --prefix`). Co-authored-by: PR0M3TH3AN <[email protected]>
This commit is contained in:
1 parent
4bbbc1dc0f
commit
487081f319
5 files changed
+215
-56
No files matched your search
+2
-2
@@ -7,8 +7,8 @@ export const DEFAULT_RELAYS = [
|
||||
];
|
||||
export const DEFAULT_TTL = 7200;
|
||||
export const DEFAULT_NAMESPACE = 'torch';
|
||||
export const DEFAULT_QUERY_TIMEOUT_MS = 15_000;
|
||||
export const DEFAULT_PUBLISH_TIMEOUT_MS = 10_000;
|
||||
export const DEFAULT_QUERY_TIMEOUT_MS = 30_000;
|
||||
export const DEFAULT_PUBLISH_TIMEOUT_MS = 15_000;
|
||||
export const DEFAULT_MIN_SUCCESSFUL_PUBLISHES = 1;
|
||||
export const DEFAULT_MIN_ACTIVE_RELAY_POOL = 1;
|
||||
export const VALID_CADENCES = new Set(['daily', 'weekly']);
|
||||
|
||||
@@ -315,6 +315,10 @@ export async function queryLocks(relays, cadence, dateStr, namespace, deps = {})
|
||||
|
||||
const runQuery = async (relaySet, phase) => {
|
||||
const { prioritized } = prioritizeRelays(relaySet, healthConfig);
|
||||
if (prioritized.length > 0) {
|
||||
errorLogger(`[${phase}] Querying ${prioritized.length} relays (${relayListLabel(prioritized)})...`);
|
||||
}
|
||||
|
||||
const startedAtMs = Date.now();
|
||||
try {
|
||||
const events = await withTimeout(
|
||||
@@ -433,6 +437,8 @@ export async function publishLock(relays, event, deps = {}) {
|
||||
const { prioritized } = prioritizeRelays(phaseRelays, healthConfig);
|
||||
if (!prioritized.length) return;
|
||||
|
||||
console.error(`[${phaseName}] Publishing to ${prioritized.length} relays (${prioritized.join(', ')})...`);
|
||||
|
||||
const startedAtMs = Date.now();
|
||||
for (const relay of prioritized) {
|
||||
attempted.add(relay);
|
||||
|
||||
+197
-51
@@ -2,6 +2,9 @@ import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import crypto from 'node:crypto';
|
||||
import readline from 'node:readline/promises';
|
||||
import { stdin as input, stdout as output } from 'node:process';
|
||||
import { DEFAULT_RELAYS } from './constants.mjs';
|
||||
|
||||
const PKG_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
||||
|
||||
@@ -23,10 +26,10 @@ const EVOLVING_DIRS = ['daily', 'weekly'];
|
||||
const APP_DIRS = ['src', 'bin', 'dashboard', 'landing', 'assets', 'scripts'];
|
||||
const APP_FILES = ['package.json', 'build.mjs', 'README.md', 'torch-config.example.json'];
|
||||
|
||||
function getPaths(userRoot) {
|
||||
const torchDir = path.join(userRoot, 'torch');
|
||||
function getPaths(root, installDirName) {
|
||||
const torchDir = path.resolve(root, installDirName);
|
||||
return {
|
||||
root: userRoot,
|
||||
root,
|
||||
torchDir,
|
||||
promptsDir: path.join(torchDir, 'prompts'),
|
||||
roster: path.join(torchDir, 'roster.json'),
|
||||
@@ -45,33 +48,96 @@ function copyDir(src, dest) {
|
||||
}
|
||||
}
|
||||
|
||||
function transformContent(content) {
|
||||
function transformContent(content, installDirName) {
|
||||
// Replace source paths with user paths
|
||||
// We assume the user is running from root, so 'src/prompts/daily/' becomes 'torch/prompts/daily/'
|
||||
// If installDirName is different, we should use that.
|
||||
// Note: These replacements are somewhat fragile regexes.
|
||||
// We'll replace 'src/' with 'installDirName/' essentially?
|
||||
// The original code replaced 'src/prompts/' with 'torch/prompts/'.
|
||||
|
||||
// If installDirName is '.', we want 'prompts/daily/'.
|
||||
// If installDirName is 'torch', we want 'torch/prompts/daily/'.
|
||||
|
||||
const prefix = installDirName === '.' ? '' : `${installDirName}/`;
|
||||
|
||||
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');
|
||||
.replace(/src\/prompts\/daily\//g, `${prefix}prompts/daily/`)
|
||||
.replace(/src\/prompts\/weekly\//g, `${prefix}prompts/weekly/`)
|
||||
.replace(/src\/prompts\/roster\.json/g, `${prefix}roster.json`)
|
||||
.replace(/src\/prompts\/scheduler-flow\.md/g, `${prefix}scheduler-flow.md`);
|
||||
}
|
||||
|
||||
function copyFile(src, dest, transform = false, overwrite = true) {
|
||||
function copyFile(src, dest, transform = false, overwrite = true, installDirName = 'torch') {
|
||||
if (fs.existsSync(dest) && !overwrite) {
|
||||
return false; // Skipped
|
||||
}
|
||||
if (!fs.existsSync(src)) return false;
|
||||
|
||||
const content = fs.readFileSync(src, 'utf8');
|
||||
const finalContent = transform ? transformContent(content) : content;
|
||||
const finalContent = transform ? transformContent(content, installDirName) : content;
|
||||
fs.writeFileSync(dest, finalContent, 'utf8');
|
||||
return true; // Copied/Overwritten
|
||||
}
|
||||
|
||||
export function cmdInit(force = false, cwd = process.cwd()) {
|
||||
const paths = getPaths(cwd);
|
||||
console.log(`Initializing torch configuration in ${paths.torchDir}...`);
|
||||
async function interactiveInit(cwd) {
|
||||
const rl = readline.createInterface({ input, output });
|
||||
const currentDirName = path.basename(cwd);
|
||||
|
||||
console.log('\n🔥 TORCH Initialization 🔥\n');
|
||||
|
||||
try {
|
||||
// 1. Install Directory
|
||||
let defaultDir = 'torch';
|
||||
if (currentDirName === 'torch') {
|
||||
defaultDir = '.';
|
||||
}
|
||||
|
||||
const dirAnswer = await rl.question(`Install directory (default: ${defaultDir}): `);
|
||||
const installDir = dirAnswer.trim() || defaultDir;
|
||||
|
||||
// 2. Namespace
|
||||
const randomSuffix = crypto.randomBytes(4).toString('hex');
|
||||
const defaultNamespace = `torch-${randomSuffix}`;
|
||||
const namespaceAnswer = await rl.question(`Nostr Namespace (default: ${defaultNamespace}): `);
|
||||
const namespace = namespaceAnswer.trim() || defaultNamespace;
|
||||
|
||||
// 3. Relays
|
||||
console.log(`\nDefault Relays:\n ${DEFAULT_RELAYS.join('\n ')}`);
|
||||
const relaysAnswer = await rl.question('Enter relays (comma-separated) or press Enter to use defaults: ');
|
||||
let relays = DEFAULT_RELAYS;
|
||||
if (relaysAnswer.trim()) {
|
||||
relays = relaysAnswer.split(',').map(r => r.trim()).filter(Boolean);
|
||||
}
|
||||
|
||||
return { installDir, namespace, relays };
|
||||
} finally {
|
||||
rl.close();
|
||||
}
|
||||
}
|
||||
|
||||
export async function cmdInit(force = false, cwd = process.cwd(), mockAnswers = null) {
|
||||
// Use interactive mode to get configuration unless mock answers are provided
|
||||
let config;
|
||||
if (mockAnswers) {
|
||||
config = mockAnswers;
|
||||
} else {
|
||||
config = await interactiveInit(cwd);
|
||||
}
|
||||
const { installDir, namespace, relays } = config;
|
||||
|
||||
const paths = getPaths(cwd, installDir);
|
||||
console.log(`\nInitializing torch in ${paths.torchDir}...`);
|
||||
|
||||
// Check if directory exists and is not empty (unless force is used or we are installing to .)
|
||||
if (fs.existsSync(paths.torchDir) && !force) {
|
||||
throw new Error(`${paths.torchDir} already exists. Use --force to overwrite.`);
|
||||
const entries = fs.readdirSync(paths.torchDir);
|
||||
if (entries.length > 0 && installDir !== '.') {
|
||||
// If installing to current dir, we might expect some files, but warn anyway?
|
||||
// Simpler: just throw if strictly not empty and not forced.
|
||||
// But usually we don't want to fail if just re-running init.
|
||||
console.warn(`Warning: ${paths.torchDir} already exists.`);
|
||||
}
|
||||
}
|
||||
|
||||
ensureDir(paths.torchDir);
|
||||
@@ -82,6 +148,7 @@ export function cmdInit(force = false, cwd = process.cwd()) {
|
||||
for (const dir of APP_DIRS) {
|
||||
const src = path.join(PKG_ROOT, dir);
|
||||
const dest = path.join(paths.torchDir, dir);
|
||||
// If installing to '.', src/bin maps to ./bin, which is fine.
|
||||
if (fs.existsSync(src)) {
|
||||
copyDir(src, dest);
|
||||
console.log(` Copied ${dir}/`);
|
||||
@@ -94,29 +161,41 @@ export function cmdInit(force = false, cwd = process.cwd()) {
|
||||
const src = path.join(PKG_ROOT, file);
|
||||
const dest = path.join(paths.torchDir, file);
|
||||
if (fs.existsSync(src)) {
|
||||
// Careful not to overwrite critical files if they exist in root?
|
||||
// e.g. package.json.
|
||||
// If installing to '.', we definitely don't want to overwrite package.json unless it's the torch one?
|
||||
// But torch is "vendored", so it has its own package.json.
|
||||
// If installing to '.', we are overwriting the USER's package.json?
|
||||
// That is dangerous.
|
||||
|
||||
if (installDir === '.' && file === 'package.json' && fs.existsSync(dest)) {
|
||||
console.warn(' Skipping package.json to avoid overwriting host package.json (installing to root).');
|
||||
continue;
|
||||
}
|
||||
|
||||
fs.copyFileSync(src, dest);
|
||||
console.log(` Copied ${file}`);
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Copy Roster (Evolving, but initially copied)
|
||||
// 3. Copy Roster
|
||||
const srcRoster = path.join(SRC_PROMPTS_DIR, 'roster.json');
|
||||
if (fs.existsSync(srcRoster)) {
|
||||
copyFile(srcRoster, paths.roster, false, true);
|
||||
copyFile(srcRoster, paths.roster, false, true, installDir);
|
||||
console.log(`Created ${path.relative(paths.root, paths.roster)}`);
|
||||
}
|
||||
|
||||
// 4. Copy Static Files (Transformed) - Prompts root files
|
||||
// 4. Copy 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);
|
||||
copyFile(src, dest, true, true, installDir);
|
||||
console.log(`Created ${path.relative(paths.root, dest)}`);
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Copy Prompts (Evolving)
|
||||
// 5. Copy Prompts
|
||||
for (const dir of EVOLVING_DIRS) {
|
||||
const srcDir = path.join(SRC_PROMPTS_DIR, dir);
|
||||
const destDir = path.join(paths.promptsDir, dir);
|
||||
@@ -127,50 +206,113 @@ export function cmdInit(force = false, cwd = process.cwd()) {
|
||||
for (const file of files) {
|
||||
const srcFile = path.join(srcDir, file);
|
||||
const destFile = path.join(destDir, file);
|
||||
copyFile(srcFile, destFile, false, true);
|
||||
copyFile(srcFile, destFile, false, true, installDir);
|
||||
}
|
||||
console.log(`Created ${files.length} files in ${path.relative(paths.root, destDir)}/`);
|
||||
}
|
||||
}
|
||||
|
||||
// 6. Create torch-config.json logic
|
||||
// 6. Create/Update torch-config.json
|
||||
const configPath = path.join(paths.root, 'torch-config.json');
|
||||
|
||||
if (!fs.existsSync(configPath)) {
|
||||
// Generate new
|
||||
try {
|
||||
let configData = {};
|
||||
|
||||
// Try to load existing or example
|
||||
if (fs.existsSync(configPath)) {
|
||||
try {
|
||||
configData = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
||||
console.log(`Updating existing ${path.relative(cwd, configPath)}...`);
|
||||
} catch (e) {
|
||||
console.warn(`Could not parse existing config: ${e.message}`);
|
||||
}
|
||||
} else {
|
||||
const exampleConfigPath = path.join(PKG_ROOT, 'torch-config.example.json');
|
||||
if (fs.existsSync(exampleConfigPath)) {
|
||||
const exampleConfig = JSON.parse(fs.readFileSync(exampleConfigPath, 'utf8'));
|
||||
|
||||
// Generate random namespace
|
||||
const randomSuffix = crypto.randomBytes(4).toString('hex');
|
||||
const newNamespace = `torch-${randomSuffix}`;
|
||||
|
||||
if (exampleConfig.nostrLock) {
|
||||
exampleConfig.nostrLock.namespace = newNamespace;
|
||||
} else {
|
||||
exampleConfig.nostrLock = { namespace: newNamespace };
|
||||
}
|
||||
|
||||
fs.writeFileSync(configPath, JSON.stringify(exampleConfig, null, 2), 'utf8');
|
||||
console.log(`Created ${path.relative(cwd, configPath)} with namespace "${newNamespace}"`);
|
||||
} else {
|
||||
console.warn(`Warning: Could not find ${exampleConfigPath} to generate torch-config.json`);
|
||||
configData = JSON.parse(fs.readFileSync(exampleConfigPath, 'utf8'));
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`Failed to create torch-config.json: ${err.message}`);
|
||||
}
|
||||
} else {
|
||||
console.log(`Skipped ${path.relative(cwd, configPath)} (exists)`);
|
||||
}
|
||||
|
||||
// Apply user choices
|
||||
if (!configData.nostrLock) configData.nostrLock = {};
|
||||
configData.nostrLock.namespace = namespace;
|
||||
configData.nostrLock.relays = relays;
|
||||
|
||||
fs.writeFileSync(configPath, JSON.stringify(configData, null, 2), 'utf8');
|
||||
console.log(`Saved configuration to ${path.relative(cwd, configPath)}`);
|
||||
|
||||
// 7. Inject Scripts into Host Package.json
|
||||
// If we are NOT installing to '.', the host package.json is in paths.root
|
||||
if (installDir !== '.') {
|
||||
injectScriptsIntoHost(paths.root, installDir);
|
||||
}
|
||||
|
||||
console.log('\nInitialization complete.');
|
||||
console.log('You can now customize the files in torch/ and torch/prompts/.');
|
||||
console.log('You can now customize the files in ' + path.relative(cwd, paths.torchDir) + '/');
|
||||
}
|
||||
|
||||
function injectScriptsIntoHost(hostRoot, installDirName) {
|
||||
const hostPkgPath = path.join(hostRoot, 'package.json');
|
||||
if (!fs.existsSync(hostPkgPath)) {
|
||||
console.warn('No package.json found in host root. Skipping script injection.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const pkg = JSON.parse(fs.readFileSync(hostPkgPath, 'utf8'));
|
||||
if (!pkg.scripts) pkg.scripts = {};
|
||||
|
||||
const scriptsToAdd = {
|
||||
'torch:dashboard': `npm run --prefix ${installDirName} dashboard:serve`,
|
||||
'torch:check': `npm run --prefix ${installDirName} lock:check:daily`, // Default to daily check
|
||||
'torch:lock': `npm run --prefix ${installDirName} lock:lock`,
|
||||
'torch:health': `npm run --prefix ${installDirName} lock:health`,
|
||||
};
|
||||
|
||||
let modified = false;
|
||||
for (const [key, cmd] of Object.entries(scriptsToAdd)) {
|
||||
if (!pkg.scripts[key]) {
|
||||
pkg.scripts[key] = cmd;
|
||||
console.log(` Added script: "${key}"`);
|
||||
modified = true;
|
||||
} else {
|
||||
console.log(` Script "${key}" already exists, skipping.`);
|
||||
}
|
||||
}
|
||||
|
||||
if (modified) {
|
||||
fs.writeFileSync(hostPkgPath, JSON.stringify(pkg, null, 2), 'utf8');
|
||||
console.log('Updated package.json with convenience scripts.');
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
console.error(`Failed to inject scripts: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
export function cmdUpdate(force = false, cwd = process.cwd()) {
|
||||
const paths = getPaths(cwd);
|
||||
// Update logic needs to know WHERE torch is installed.
|
||||
// We can look for torch directory? Or assume 'torch'?
|
||||
// For now, let's look for 'torch' directory first, then fallback to '.' if we detect torch files?
|
||||
// Or just default to 'torch' and let user move files if they changed it?
|
||||
// Realistically, 'update' should probably take an argument for the dir, or we just default to 'torch'.
|
||||
|
||||
// If the user installed to 'custom-dir', cmdUpdate will fail unless we auto-detect.
|
||||
// Auto-detection strategy: check if 'torch' exists. If not, check if 'package.json' has 'torch-lock' name?
|
||||
|
||||
let installDirName = 'torch';
|
||||
if (!fs.existsSync(path.join(cwd, 'torch')) && fs.existsSync(path.join(cwd, 'package.json'))) {
|
||||
// Check if current dir is the torch dir
|
||||
try {
|
||||
const pkg = JSON.parse(fs.readFileSync(path.join(cwd, 'package.json'), 'utf8'));
|
||||
if (pkg.name === 'torch-lock') {
|
||||
installDirName = '.';
|
||||
}
|
||||
} catch (e) {
|
||||
// Ignore error if package.json is missing or invalid
|
||||
}
|
||||
}
|
||||
|
||||
const paths = getPaths(cwd, installDirName);
|
||||
console.log(`Updating torch configuration in ${paths.torchDir}...`);
|
||||
|
||||
if (!fs.existsSync(paths.torchDir)) {
|
||||
@@ -212,6 +354,10 @@ export function cmdUpdate(force = false, cwd = process.cwd()) {
|
||||
const src = path.join(PKG_ROOT, file);
|
||||
const dest = path.join(paths.torchDir, file);
|
||||
if (fs.existsSync(src)) {
|
||||
if (installDirName === '.' && file === 'package.json') {
|
||||
console.log(' Skipping package.json update (installed in root).');
|
||||
continue;
|
||||
}
|
||||
fs.copyFileSync(src, dest);
|
||||
console.log(` Updated ${file}`);
|
||||
}
|
||||
@@ -223,7 +369,7 @@ export function cmdUpdate(force = false, cwd = process.cwd()) {
|
||||
const src = path.join(SRC_PROMPTS_DIR, file);
|
||||
const dest = path.join(paths.torchDir, file);
|
||||
if (fs.existsSync(src)) {
|
||||
copyFile(src, dest, true, true);
|
||||
copyFile(src, dest, true, true, installDirName);
|
||||
console.log(` Updated ${file}`);
|
||||
}
|
||||
}
|
||||
@@ -232,7 +378,7 @@ export function cmdUpdate(force = false, cwd = process.cwd()) {
|
||||
const srcRoster = path.join(SRC_PROMPTS_DIR, 'roster.json');
|
||||
if (fs.existsSync(srcRoster)) {
|
||||
if (force) {
|
||||
copyFile(srcRoster, paths.roster, false, true);
|
||||
copyFile(srcRoster, paths.roster, false, true, installDirName);
|
||||
console.log(' Overwrote roster.json (forced)');
|
||||
} else {
|
||||
console.log(' Skipped roster.json (preserved)');
|
||||
@@ -257,11 +403,11 @@ export function cmdUpdate(force = false, cwd = process.cwd()) {
|
||||
const destFile = path.join(destDir, file);
|
||||
|
||||
if (force) {
|
||||
copyFile(srcFile, destFile, false, true);
|
||||
copyFile(srcFile, destFile, false, true, installDirName);
|
||||
updated++;
|
||||
} else {
|
||||
if (!fs.existsSync(destFile)) {
|
||||
copyFile(srcFile, destFile, false, true);
|
||||
copyFile(srcFile, destFile, false, true, installDirName);
|
||||
added++;
|
||||
} else {
|
||||
skipped++;
|
||||
|
||||
@@ -72,7 +72,14 @@ function parseRoster(value) {
|
||||
export function getTorchConfigPath() {
|
||||
const explicitPath = (process.env.TORCH_CONFIG_PATH || '').trim();
|
||||
if (explicitPath) return path.resolve(process.cwd(), explicitPath);
|
||||
return path.resolve(process.cwd(), DEFAULT_CONFIG_PATH);
|
||||
|
||||
const localPath = path.resolve(process.cwd(), DEFAULT_CONFIG_PATH);
|
||||
if (fs.existsSync(localPath)) return localPath;
|
||||
|
||||
const parentPath = path.resolve(process.cwd(), '..', DEFAULT_CONFIG_PATH);
|
||||
if (fs.existsSync(parentPath)) return parentPath;
|
||||
|
||||
return localPath;
|
||||
}
|
||||
|
||||
export function parseTorchConfig(raw, configPath = null) {
|
||||
|
||||
@@ -7,8 +7,8 @@
|
||||
"wss://relay.primal.net"
|
||||
],
|
||||
"ttlSeconds": 7200,
|
||||
"queryTimeoutMs": 15000,
|
||||
"publishTimeoutMs": 8000,
|
||||
"queryTimeoutMs": 30000,
|
||||
"publishTimeoutMs": 15000,
|
||||
"minSuccessfulRelayPublishes": 2,
|
||||
"minActiveRelayPool": 2,
|
||||
"relayFallbacks": [
|
||||
|
||||
Reference in new issue
Block a user