Merge pull request #346 from PR0M3TH3AN/claude/add-torch-removal-command-keFx5

Add cmdRemove command to completely uninstall TORCH from projects
This commit is contained in:
thePR0M3TH3AN authored and GitHub committed 2026-02-19 21:09:54 -05:00
commit 158026bdb4
7 files changed
+584 -3

No files matched your search

+15
View File
@@ -58,6 +58,9 @@ npx --no-install torch-lock init
# Update TORCH configuration and scripts
npx --no-install torch-lock update
# Remove TORCH completely from this project
npx --no-install torch-lock remove
# Run the Dashboard
npx --no-install torch-lock dashboard
```
@@ -113,6 +116,18 @@ By default, the dashboard binds to `127.0.0.1`. To allow external access, you ca
The dashboard supports Basic Authentication. You can enable it by setting the `TORCH_DASHBOARD_AUTH` environment variable or the `dashboard.auth` field in `torch-config.json` with the format `username:password`.
## Removing TORCH
To completely remove TORCH from a project:
```bash
npx torch-lock remove
```
This removes the `torch/` directory, configuration files, runtime artifacts, host package.json scripts, and the npm package. Use `--force` to skip the confirmation prompt.
For manual removal steps and a full list of what gets cleaned up, see [docs/removal.md](docs/removal.md).
## Environment variables
You can override configuration using environment variables:
+151
View File
@@ -0,0 +1,151 @@
# Removing TORCH from a Project
This guide covers how to completely remove TORCH from a project repository that installed it via the standard installation process (`npm install <torch-tarball> && npx torch-lock init`).
## Quick Removal (Recommended)
Run the built-in removal command from your project root:
```bash
npx torch-lock remove
```
This will:
1. Detect your TORCH installation (whether installed to `torch/` or to the project root)
2. Show a list of all artifacts that will be removed
3. Ask for confirmation before deleting anything
4. Remove all TORCH files, directories, configuration, and the npm package
To skip the confirmation prompt:
```bash
npx torch-lock remove --force
```
## What Gets Removed
The `remove` command cleans up everything that `torch-lock init` created, plus any runtime artifacts:
| Artifact | Description |
|---|---|
| `torch/` | The main TORCH install directory (contains src, bin, dashboard, scripts, prompts, etc.) |
| `torch-config.json` | TORCH configuration file at the project root |
| `.torch/` | Hidden directory for prompt history tracking |
| `.scheduler-memory/` | Runtime memory store used by the memory subsystem |
| `task-logs/` | Scheduler run logs (daily/weekly cycle logs, relay health history) |
| `src/proposals/` | Governance proposals directory |
| `torch:*` scripts in `package.json` | Convenience scripts injected into the host project's package.json |
| `node_modules/torch-lock/` | The installed npm package |
## Manual Removal
If you prefer to remove TORCH manually, or if the `remove` command is unavailable (e.g., the package was already uninstalled), follow these steps from your project root:
### 1. Remove the TORCH directory
```bash
rm -rf torch/
```
### 2. Remove TORCH configuration
```bash
rm -f torch-config.json
```
### 3. Remove hidden directories
```bash
rm -rf .torch/
rm -rf .scheduler-memory/
```
### 4. Remove runtime logs
```bash
rm -rf task-logs/
```
### 5. Remove governance proposals (if empty/unused)
```bash
# Only remove if this directory was created by TORCH and contains no user content
rm -rf src/proposals/
# Remove src/ too if it's now empty
rmdir src/ 2>/dev/null
```
### 6. Clean up package.json scripts
Remove these keys from the `"scripts"` section of your `package.json`:
```json
{
"torch:dashboard": "...",
"torch:check": "...",
"torch:lock": "...",
"torch:health": "...",
"torch:memory:list": "...",
"torch:memory:inspect": "..."
}
```
### 7. Uninstall the npm package
```bash
npm uninstall torch-lock
```
### 8. Clean up environment variables (if set)
If you configured TORCH environment variables in your shell profile, CI, or `.env` files, remove them:
- `NOSTR_LOCK_NAMESPACE`
- `NOSTR_LOCK_HASHTAG`
- `NOSTR_LOCK_RELAYS`
- `NOSTR_LOCK_TTL`
- `NOSTR_LOCK_QUERY_TIMEOUT_MS`
- `NOSTR_LOCK_DAILY_ROSTER`
- `NOSTR_LOCK_WEEKLY_ROSTER`
- `TORCH_CONFIG_PATH`
- `TORCH_DASHBOARD_AUTH`
- `TORCH_MEMORY_ENABLED`
- `TORCH_MEMORY_INGEST_ENABLED`
- `TORCH_MEMORY_RETRIEVAL_ENABLED`
- `TORCH_MEMORY_PRUNE_ENABLED`
- `AGENT_PLATFORM`
### 9. Optional: Remove generated reports
If you ran lock reliability reports, you may also want to remove:
```bash
rm -rf artifacts/lock-reliability/
```
## Verifying Complete Removal
After removal, verify no TORCH artifacts remain:
```bash
# Should return no results
ls torch/ 2>/dev/null
ls .torch/ 2>/dev/null
ls .scheduler-memory/ 2>/dev/null
ls torch-config.json 2>/dev/null
# Should not contain torch-lock
npm ls torch-lock 2>/dev/null
# Should not contain torch:* scripts
grep '"torch:' package.json
```
## Re-installing TORCH
If you want to re-install TORCH later:
```bash
npm install https://github.com/PR0M3TH3AN/TORCH/archive/refs/heads/main.tar.gz && npx torch-lock init
```
+2 -1
View File
@@ -68,7 +68,8 @@
"test:ci-resilience": "node --test test/lock-acquisition-resilience.integration.test.mjs",
"test:extended-main": "npm run test:integration:e2e && npm run test:ci-resilience",
"torch:memory:list": "node bin/torch-lock.mjs list-memories",
"torch:memory:inspect": "node bin/torch-lock.mjs inspect-memory"
"torch:memory:inspect": "node bin/torch-lock.mjs inspect-memory",
"torch:remove": "node bin/torch-lock.mjs remove"
},
"dependencies": {
"nostr-tools": "2.23.1",
+14
View File
@@ -23,6 +23,19 @@ export const VALID_CADENCES = new Set(['daily', 'weekly']);
export const KIND_APP_DATA = 30078;
export const MS_PER_SECOND = 1000;
/**
* Script keys injected into the host package.json during `torch-lock init`.
* Used by both init (to add) and remove (to clean up).
*/
export const TORCH_HOST_SCRIPTS = [
'torch:dashboard',
'torch:check',
'torch:lock',
'torch:health',
'torch:memory:list',
'torch:memory:inspect',
];
export const USAGE_TEXT = `Usage: torch-lock <command> [options]
Commands:
@@ -34,6 +47,7 @@ Commands:
dashboard [--port <port>] [--host <host>] Serve the dashboard (default: ${DEFAULT_DASHBOARD_PORT})
init [--force] Initialize torch/ directory in current project
update [--force] Update torch/ configuration (backups, merges)
remove [--force] Remove all TORCH files and configuration from project
list-memories [--agent <id>] [--type <type>] [--tags <a,b>] [--pinned <true|false>] [--full]
inspect-memory --id <memoryId>
+6 -1
View File
@@ -20,7 +20,7 @@ import {
USAGE_TEXT,
MS_PER_SECOND,
} from './constants.mjs';
import { cmdInit, cmdUpdate } from './ops.mjs';
import { cmdInit, cmdUpdate, cmdRemove } from './ops.mjs';
import { parseArgs } from './cli-parser.mjs';
import { getRoster as _getRoster } from './roster.mjs';
import { queryLocks as _queryLocks, publishLock as _publishLock, parseLockEvent } from './lock-ops.mjs';
@@ -637,6 +637,11 @@ export async function main(argv) {
break;
}
case 'remove': {
await cmdRemove(args.force);
break;
}
case 'list-memories': {
const result = await listMemories({
agent_id: args.agent,
+252 -1
View File
@@ -4,7 +4,7 @@ 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';
import { DEFAULT_RELAYS, TORCH_HOST_SCRIPTS } from './constants.mjs';
import { ensureDir } from './utils.mjs';
const PKG_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
@@ -401,6 +401,257 @@ function injectScriptsIntoHost(hostRoot, installDirName) {
}
}
/**
* Detects the TORCH install directory within a project.
*
* Strategy:
* 1. If a 'torch/' directory exists with a roster.json or bin/, it's the install dir.
* 2. If the current directory's package.json has name "torch-lock", installed to '.'.
* 3. Returns null if TORCH is not detected.
*
* @param {string} cwd - The project root directory
* @returns {string|null} - The install directory name ('torch', '.', etc.) or null
*/
function detectTorchInstallDir(cwd) {
// Check for 'torch/' subdirectory with TORCH markers
const torchSubdir = path.join(cwd, 'torch');
if (fs.existsSync(torchSubdir)) {
const hasRoster = fs.existsSync(path.join(torchSubdir, 'roster.json'));
const hasBin = fs.existsSync(path.join(torchSubdir, 'bin'));
if (hasRoster || hasBin) {
return 'torch';
}
}
// Check if installed to current directory
const pkgPath = path.join(cwd, 'package.json');
if (fs.existsSync(pkgPath)) {
try {
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
if (pkg.name === 'torch-lock') {
return '.';
}
} catch (_e) {
// Ignore parse errors
}
}
return null;
}
/**
* Removes a directory or file if it exists, logging the action.
*
* @param {string} target - Absolute path to remove
* @param {string} label - Human-readable label for log output
* @param {Function} log - Logging function
* @returns {boolean} - True if something was removed
*/
function removeIfExists(target, label, log) {
if (!fs.existsSync(target)) return false;
const stat = fs.statSync(target);
if (stat.isDirectory()) {
fs.rmSync(target, { recursive: true, force: true });
} else {
fs.unlinkSync(target);
}
log(` Removed ${label}`);
return true;
}
/**
* Removes TORCH torch:* scripts from the host package.json.
*
* @param {string} hostRoot - Project root containing package.json
* @param {Function} log - Logging function
* @returns {boolean} - True if any scripts were removed
*/
function removeHostScripts(hostRoot, log) {
const hostPkgPath = path.join(hostRoot, 'package.json');
if (!fs.existsSync(hostPkgPath)) return false;
try {
const raw = fs.readFileSync(hostPkgPath, 'utf8');
const pkg = JSON.parse(raw);
if (!pkg.scripts) return false;
let modified = false;
for (const key of TORCH_HOST_SCRIPTS) {
if (key in pkg.scripts) {
delete pkg.scripts[key];
log(` Removed script: "${key}"`);
modified = true;
}
}
if (modified) {
fs.writeFileSync(hostPkgPath, JSON.stringify(pkg, null, 2) + '\n', 'utf8');
log(' Updated package.json.');
}
return modified;
} catch (e) {
log(` Warning: Could not update package.json: ${e.message}`);
return false;
}
}
/**
* Completely removes TORCH from a project that installed it via
* `npm install <torch-tarball> && npx torch-lock init`.
*
* What it removes:
* 1. The torch/ install directory (or equivalent)
* 2. torch-config.json at the project root
* 3. .torch/ hidden directory (prompt history)
* 4. .scheduler-memory/ directory (runtime memory store)
* 5. task-logs/ directory (scheduler run logs)
* 6. src/proposals/ directory (governance proposals)
* 7. torch:* scripts from the host package.json
* 8. The torch-lock npm package (node_modules/torch-lock)
*
* @param {boolean} force - If true, skip the confirmation prompt
* @param {string} cwd - Working directory (project root)
* @param {Object|null} mockAnswers - For testing: { confirm: true/false }
* @returns {Promise<void>}
*/
export async function cmdRemove(force = false, cwd = process.cwd(), mockAnswers = null) {
const log = console.log;
const installDir = detectTorchInstallDir(cwd);
if (!installDir) {
log('No TORCH installation detected in this directory.');
log('Looked for:');
log(' - A torch/ subdirectory containing roster.json or bin/');
log(' - A package.json with name "torch-lock" (root install)');
return;
}
const isRootInstall = installDir === '.';
const torchDir = path.resolve(cwd, installDir);
// Build a manifest of what will be removed
const targets = [];
if (!isRootInstall && fs.existsSync(torchDir)) {
targets.push({ path: torchDir, label: `${installDir}/ (TORCH install directory)` });
}
const configPath = path.join(cwd, 'torch-config.json');
if (fs.existsSync(configPath)) {
targets.push({ path: configPath, label: 'torch-config.json' });
}
const dotTorch = path.join(cwd, '.torch');
if (fs.existsSync(dotTorch)) {
targets.push({ path: dotTorch, label: '.torch/ (prompt history)' });
}
const schedulerMemory = path.join(cwd, '.scheduler-memory');
if (fs.existsSync(schedulerMemory)) {
targets.push({ path: schedulerMemory, label: '.scheduler-memory/ (memory store)' });
}
const taskLogs = path.join(cwd, 'task-logs');
if (fs.existsSync(taskLogs)) {
targets.push({ path: taskLogs, label: 'task-logs/ (scheduler logs)' });
}
const proposals = path.join(cwd, 'src', 'proposals');
if (fs.existsSync(proposals)) {
targets.push({ path: proposals, label: 'src/proposals/ (governance proposals)' });
}
// Check for host scripts
const hostPkgPath = path.join(cwd, 'package.json');
let hasHostScripts = false;
if (fs.existsSync(hostPkgPath)) {
try {
const pkg = JSON.parse(fs.readFileSync(hostPkgPath, 'utf8'));
if (pkg.scripts) {
hasHostScripts = TORCH_HOST_SCRIPTS.some(key => key in pkg.scripts);
}
} catch (_e) { /* ignore */ }
}
if (targets.length === 0 && !hasHostScripts) {
log('Nothing to remove — no TORCH artifacts found.');
return;
}
// Show what will be removed
log('\nThe following TORCH artifacts will be removed:\n');
for (const t of targets) {
log(` - ${t.label}`);
}
if (hasHostScripts) {
log(' - torch:* scripts from package.json');
}
log(' - torch-lock npm package (via npm uninstall)');
log('');
// Confirm unless --force
if (!force) {
let confirmed;
if (mockAnswers) {
confirmed = mockAnswers.confirm;
} else {
const rl = readline.createInterface({ input, output });
try {
const answer = await rl.question('Proceed with removal? (yes/no): ');
confirmed = answer.trim().toLowerCase() === 'yes';
} finally {
rl.close();
}
}
if (!confirmed) {
log('Removal cancelled.');
return;
}
}
log('\nRemoving TORCH...\n');
// 1. Remove directories and files
for (const t of targets) {
removeIfExists(t.path, t.label, log);
}
// 2. Clean up empty src/ directory if proposals was the only thing in it
const srcDir = path.join(cwd, 'src');
if (fs.existsSync(srcDir)) {
try {
const entries = fs.readdirSync(srcDir);
if (entries.length === 0) {
fs.rmdirSync(srcDir);
log(' Removed empty src/ directory');
}
} catch (_e) { /* leave it if not empty or permission error */ }
}
// 3. Remove host package.json scripts
if (hasHostScripts) {
removeHostScripts(cwd, log);
}
// 4. Uninstall the npm package
log('\n Running npm uninstall torch-lock...');
try {
const { execSync } = await import('node:child_process');
execSync('npm uninstall torch-lock', { cwd, stdio: 'pipe' });
log(' Uninstalled torch-lock package.');
} catch (e) {
log(` Warning: npm uninstall torch-lock failed: ${e.message}`);
log(' You may need to run "npm uninstall torch-lock" manually.');
}
log('\nTORCH has been completely removed from this project.');
log('If you used TORCH environment variables (NOSTR_LOCK_*, TORCH_*),');
log('remember to remove them from your shell profile or CI configuration.');
}
export function cmdUpdate(force = false, cwd = process.cwd()) {
// Update logic needs to know WHERE torch is installed.
// We can look for torch directory? Or assume 'torch'?
+144
View File
@@ -0,0 +1,144 @@
import { test, after } from 'node:test';
import assert from 'node:assert';
import fs from 'node:fs';
import path from 'node:path';
import { cmdInit, cmdRemove } from '../src/ops.mjs';
const MOCK_CONFIG = {
installDir: 'torch',
namespace: 'test-namespace',
relays: ['wss://relay.damus.io']
};
const tempBase = fs.mkdtempSync(path.join(process.cwd(), 'test-ops-remove-'));
after(() => {
fs.rmSync(tempBase, { recursive: true, force: true });
});
test('cmdRemove reports nothing when no TORCH installation exists', async () => {
const projectRoot = path.join(tempBase, 'empty-project');
fs.mkdirSync(projectRoot, { recursive: true });
// Create a basic package.json so it looks like a real project
fs.writeFileSync(path.join(projectRoot, 'package.json'), JSON.stringify({ name: 'my-app' }));
// Should complete without error — just reports nothing found
await cmdRemove(true, projectRoot);
// Project should be untouched
assert.ok(fs.existsSync(path.join(projectRoot, 'package.json')), 'package.json still exists');
});
test('cmdRemove with --force removes torch/ directory', async () => {
const projectRoot = path.join(tempBase, 'project-force');
fs.mkdirSync(projectRoot, { recursive: true });
await cmdInit(false, projectRoot, MOCK_CONFIG);
// Verify init created the expected artifacts
assert.ok(fs.existsSync(path.join(projectRoot, 'torch')), 'torch/ exists before remove');
assert.ok(fs.existsSync(path.join(projectRoot, 'torch-config.json')), 'torch-config.json exists before remove');
assert.ok(fs.existsSync(path.join(projectRoot, '.torch')), '.torch/ exists before remove');
await cmdRemove(true, projectRoot);
// All TORCH artifacts should be gone
assert.ok(!fs.existsSync(path.join(projectRoot, 'torch')), 'torch/ removed');
assert.ok(!fs.existsSync(path.join(projectRoot, 'torch-config.json')), 'torch-config.json removed');
assert.ok(!fs.existsSync(path.join(projectRoot, '.torch')), '.torch/ removed');
});
test('cmdRemove removes runtime artifacts (task-logs, .scheduler-memory)', async () => {
const projectRoot = path.join(tempBase, 'project-runtime');
fs.mkdirSync(projectRoot, { recursive: true });
await cmdInit(false, projectRoot, MOCK_CONFIG);
// Simulate runtime artifacts that the scheduler would create
fs.mkdirSync(path.join(projectRoot, 'task-logs', 'daily'), { recursive: true });
fs.writeFileSync(path.join(projectRoot, 'task-logs', 'daily', 'run.md'), '# log');
fs.mkdirSync(path.join(projectRoot, '.scheduler-memory'), { recursive: true });
fs.writeFileSync(path.join(projectRoot, '.scheduler-memory', 'memory-store.json'), '{}');
await cmdRemove(true, projectRoot);
assert.ok(!fs.existsSync(path.join(projectRoot, 'task-logs')), 'task-logs/ removed');
assert.ok(!fs.existsSync(path.join(projectRoot, '.scheduler-memory')), '.scheduler-memory/ removed');
});
test('cmdRemove removes src/proposals/ and cleans empty src/', async () => {
const projectRoot = path.join(tempBase, 'project-proposals');
fs.mkdirSync(projectRoot, { recursive: true });
await cmdInit(false, projectRoot, MOCK_CONFIG);
// src/proposals/ is created by cmdInit
assert.ok(fs.existsSync(path.join(projectRoot, 'src', 'proposals')), 'src/proposals/ exists before remove');
await cmdRemove(true, projectRoot);
assert.ok(!fs.existsSync(path.join(projectRoot, 'src', 'proposals')), 'src/proposals/ removed');
// src/ should also be removed since it's now empty
assert.ok(!fs.existsSync(path.join(projectRoot, 'src')), 'empty src/ removed');
});
test('cmdRemove cleans torch:* scripts from host package.json', async () => {
const projectRoot = path.join(tempBase, 'project-scripts');
fs.mkdirSync(projectRoot, { recursive: true });
// Create a host package.json with existing user scripts
fs.writeFileSync(path.join(projectRoot, 'package.json'), JSON.stringify({
name: 'my-app',
scripts: {
test: 'jest',
build: 'webpack'
}
}, null, 2));
await cmdInit(false, projectRoot, MOCK_CONFIG);
// Verify init injected torch scripts
const pkgBefore = JSON.parse(fs.readFileSync(path.join(projectRoot, 'package.json'), 'utf8'));
assert.ok('torch:dashboard' in pkgBefore.scripts, 'torch:dashboard injected');
assert.ok('torch:check' in pkgBefore.scripts, 'torch:check injected');
await cmdRemove(true, projectRoot);
// User scripts should remain, torch scripts should be gone
const pkgAfter = JSON.parse(fs.readFileSync(path.join(projectRoot, 'package.json'), 'utf8'));
assert.strictEqual(pkgAfter.scripts.test, 'jest', 'user test script preserved');
assert.strictEqual(pkgAfter.scripts.build, 'webpack', 'user build script preserved');
assert.ok(!('torch:dashboard' in pkgAfter.scripts), 'torch:dashboard removed');
assert.ok(!('torch:check' in pkgAfter.scripts), 'torch:check removed');
assert.ok(!('torch:lock' in pkgAfter.scripts), 'torch:lock removed');
assert.ok(!('torch:health' in pkgAfter.scripts), 'torch:health removed');
assert.ok(!('torch:memory:list' in pkgAfter.scripts), 'torch:memory:list removed');
assert.ok(!('torch:memory:inspect' in pkgAfter.scripts), 'torch:memory:inspect removed');
});
test('cmdRemove cancels when user declines confirmation', async () => {
const projectRoot = path.join(tempBase, 'project-cancel');
fs.mkdirSync(projectRoot, { recursive: true });
await cmdInit(false, projectRoot, MOCK_CONFIG);
// Decline the confirmation
await cmdRemove(false, projectRoot, { confirm: false });
// Everything should still exist
assert.ok(fs.existsSync(path.join(projectRoot, 'torch')), 'torch/ still exists after cancel');
assert.ok(fs.existsSync(path.join(projectRoot, 'torch-config.json')), 'config still exists after cancel');
});
test('cmdRemove confirmed via mockAnswers removes artifacts', async () => {
const projectRoot = path.join(tempBase, 'project-confirm');
fs.mkdirSync(projectRoot, { recursive: true });
await cmdInit(false, projectRoot, MOCK_CONFIG);
// Confirm the removal
await cmdRemove(false, projectRoot, { confirm: true });
assert.ok(!fs.existsSync(path.join(projectRoot, 'torch')), 'torch/ removed after confirm');
assert.ok(!fs.existsSync(path.join(projectRoot, 'torch-config.json')), 'config removed after confirm');
});