tahamajs/IE / Projects /cli /ie-cli.js
tahamajs's picture
download
raw
31.3 kB
#!/usr/bin/env node
// ============================================================
// ie-cli.js – IE Research Platform Developer CLI
// Version: 3.0.0
//
// Commands:
// init Bootstrap a new custom tool project
// login Authenticate and save JWT token
// logout Clear saved credentials
// whoami Show current logged-in user
// deploy Deploy a tool to the server (first publish)
// update Update an already-deployed tool by ID
// delete Remove a deployed tool from the server
// list List all your deployed tools
// status Show live status of a deployed tool
// logs Stream or print recent deploy logs
// validate Validate tool.json and tool.html before deploy
// help Show this help screen
// ============================================================
'use strict';
const fs = require('fs');
const path = require('path');
const os = require('os');
const rl = require('readline');
// ── ANSI colour helpers ──────────────────────────────────────
const C = {
reset: '\x1b[0m',
bold: '\x1b[1m',
dim: '\x1b[2m',
red: '\x1b[31m',
green: '\x1b[32m',
yellow: '\x1b[33m',
blue: '\x1b[34m',
magenta:'\x1b[35m',
cyan: '\x1b[36m',
white: '\x1b[37m',
};
const ok = (s) => `${C.green}${C.bold}✔${C.reset} ${s}`;
const err = (s) => `${C.red}${C.bold}✘${C.reset} ${s}`;
const inf = (s) => `${C.cyan}${C.bold}ℹ${C.reset} ${s}`;
const wrn = (s) => `${C.yellow}${C.bold}⚠${C.reset} ${s}`;
const hdr = (s) => `\n${C.bold}${C.magenta}${s}${C.reset}\n${'─'.repeat(s.length)}`;
// ── Spinner ──────────────────────────────────────────────────
function spinner(label) {
const frames = ['⠋','⠙','⠹','⠸','⠼','⠴','⠦','⠧','⠇','⠏'];
let i = 0;
const iv = setInterval(() => {
process.stdout.write(`\r${C.cyan}${frames[i++ % frames.length]}${C.reset} ${label} `);
}, 80);
return {
stop(successMsg) {
clearInterval(iv);
process.stdout.write('\r\x1b[2K');
if (successMsg) console.log(ok(successMsg));
},
fail(failMsg) {
clearInterval(iv);
process.stdout.write('\r\x1b[2K');
if (failMsg) console.error(err(failMsg));
},
};
}
// ── Prompt helper ────────────────────────────────────────────
function ask(question, hidden = false) {
return new Promise((resolve) => {
const iface = rl.createInterface({ input: process.stdin, output: process.stdout });
if (hidden && process.stdin.isTTY) process.stdin.setRawMode(true);
iface.question(`${C.bold}${question}${C.reset}`, (answer) => {
iface.close();
if (hidden && process.stdin.isTTY) {
process.stdin.setRawMode(false);
process.stdout.write('\n');
}
resolve(answer.trim());
});
});
}
// ── Argument parser ──────────────────────────────────────────
const VALUE_FLAGS = new Set([
'--username', '-u',
'--password', '-p',
'--host',
'--file', '-f',
'--id',
'--limit'
]);
const parsedArgs = (function parseArgs(args) {
const flags = {};
const positional = [];
for (let i = 0; i < args.length; i++) {
const arg = args[i];
if (arg.startsWith('-')) {
if (arg.includes('=')) {
const parts = arg.split('=');
flags[parts[0]] = parts.slice(1).join('=');
} else {
if (VALUE_FLAGS.has(arg)) {
const next = args[i + 1];
if (next !== undefined && !next.startsWith('-')) {
flags[arg] = next;
i++;
} else {
flags[arg] = '';
}
} else {
flags[arg] = true;
}
}
} else {
positional.push(arg);
}
}
return { flags, positional };
})(process.argv.slice(2));
const command = parsedArgs.positional[0];
function flag(long, short) {
if (parsedArgs.flags[long] !== undefined && parsedArgs.flags[long] !== '') return parsedArgs.flags[long];
if (parsedArgs.flags[short] !== undefined && parsedArgs.flags[short] !== '') return parsedArgs.flags[short];
return null;
}
function hasFlag(long, short) {
return parsedArgs.flags[long] !== undefined || parsedArgs.flags[short] !== undefined;
}
// ── Config paths ─────────────────────────────────────────────
const CLI_CONFIG_PATH = path.join(os.homedir(), '.ie-research-cli.json');
const DEFAULT_HOST = 'http://localhost:8080';
function loadCliConfig() {
if (!fs.existsSync(CLI_CONFIG_PATH)) return null;
try { return JSON.parse(fs.readFileSync(CLI_CONFIG_PATH, 'utf8')); }
catch { return null; }
}
function saveCliConfig(data) {
fs.writeFileSync(CLI_CONFIG_PATH, JSON.stringify(data, null, 2), { encoding: 'utf8', mode: 0o600 });
}
// ── HTTP helper ──────────────────────────────────────────────
async function request(method, url, body, token) {
const headers = { 'Content-Type': 'application/json' };
if (token) headers['Authorization'] = `Bearer ${token}`;
const res = await fetch(url, {
method,
headers,
body: body ? JSON.stringify(body) : undefined,
});
const text = await res.text();
let data;
try { data = JSON.parse(text); } catch { data = { message: text }; }
if (!res.ok) throw Object.assign(new Error(data.message || data.error || `HTTP ${res.status}`), { status: res.status, data });
return data;
}
// ════════════════════════════════════════════════════════════
// COMMANDS
// ════════════════════════════════════════════════════════════
// ── help ─────────────────────────────────────────────────────
function showHelp() {
console.log(`
${C.bold}${C.magenta}ie-research-cli${C.reset} ${C.dim}v3.0.0 – IE Research Platform Developer CLI${C.reset}
${C.bold}USAGE${C.reset}
node ie-cli.js <command> [options]
${C.bold}COMMANDS${C.reset}
${C.cyan}init${C.reset} Interactively bootstrap a new custom tool project
Creates tool.json (config) and tool.html (sandbox template)
${C.cyan}login${C.reset} Authenticate with the platform and save your JWT token
--username, -u Your platform username
--password, -p Your platform password
--host Backend base URL (default: ${DEFAULT_HOST})
${C.cyan}logout${C.reset} Clear saved credentials from this machine
${C.cyan}whoami${C.reset} Show which account you are currently logged in as
${C.cyan}validate${C.reset} Check tool.json and tool.html for errors before deploying
--file, -f Path to the HTML file (default: ./tool.html)
${C.cyan}deploy${C.reset} Publish a new custom tool to the server
--file, -f Path to the sandboxed HTML/JS tool file
--public Make the tool publicly visible in the store (default: true)
--private Make the tool private (not visible in the store)
--pro Flag tool as requiring a Pro subscription
${C.cyan}update${C.reset} Update an already-deployed tool
--id Tool ID to update (required)
--file, -f Path to the updated HTML/JS tool file
--public Make the tool public
--private Make the tool private
--pro Make the tool Pro-only (default: false)
${C.cyan}delete${C.reset} Permanently remove a deployed tool from the server
--id Tool ID to delete (required)
${C.cyan}list${C.reset} List all custom tools you have deployed
${C.cyan}status${C.reset} Show live detail about a specific deployed tool
--id Tool ID to inspect (required)
${C.cyan}logs${C.reset} Print recent deployment logs
--id Tool ID (optional – shows all if omitted)
--limit Number of log lines to show (default: 20)
${C.cyan}help${C.reset} Show this help screen
${C.bold}EXAMPLES${C.reset}
# 1. Authenticate
node ie-cli.js login --username alice --password s3cr3t
# 2. Create a new tool project in the current directory
node ie-cli.js init
# 3. Validate before deploying
node ie-cli.js validate --file ./tool.html
# 4. Deploy to the server
node ie-cli.js deploy --file ./tool.html
# 5. Update after editing
node ie-cli.js update --id abc123 --file ./tool.html
# 6. List all your tools
node ie-cli.js list
# 7. Check a specific tool's live status
node ie-cli.js status --id abc123
# 8. Delete a tool
node ie-cli.js delete --id abc123
${C.bold}TOOL PROJECT FILES${C.reset}
tool.json Metadata: name, description, version, category, tags, isPublic, requiresPro
tool.html Sandboxed HTML/JS – rendered inside the platform workspace iframe
${C.dim}Full docs at /api-docs inside the platform.${C.reset}
`);
}
// ── init ─────────────────────────────────────────────────────
async function handleInit() {
console.log(hdr('Initializing new custom tool project'));
const name = await ask('Tool Name (e.g. Citation Counter): ') || 'My Custom Tool';
const description = await ask('Short Description: ') || 'A sandboxed custom tool.';
const author = await ask('Author Name: ') || 'Developer';
const category = await ask('Category [ai|statistics|data-viz|productivity|utility]: ') || 'utility';
const version = await ask('Version (default 1.0.0): ') || '1.0.0';
const isPublic = (await ask('Publish to tool store? [Y/n]: ')).toLowerCase() !== 'n';
const requiresPro = (await ask('Require Pro subscription? [y/N]: ')).toLowerCase() === 'y';
const config = { name, description, author, category, version, type: 'script', tags: [category, 'custom'], isPublic, requiresPro };
const configPath = path.join(process.cwd(), 'tool.json');
fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf8');
console.log(ok(`Created tool.json → ${configPath}`));
const sampleHtml = `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>${name}</title>
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: system-ui, -apple-system, sans-serif;
background: linear-gradient(135deg, #0f172a, #1e293b);
color: #f1f5f9;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 24px;
}
.card {
background: rgba(255,255,255,0.07);
backdrop-filter: blur(20px);
border: 1px solid rgba(255,255,255,0.15);
border-radius: 20px;
padding: 36px 40px;
max-width: 460px;
width: 100%;
text-align: center;
box-shadow: 0 25px 50px -12px rgba(0,0,0,0.5);
}
.icon { font-size: 48px; margin-bottom: 16px; }
h1 { font-size: 22px; font-weight: 700; margin-bottom: 8px; }
p { font-size: 14px; color: #94a3b8; margin-bottom: 24px; line-height: 1.6; }
button {
padding: 10px 28px;
background: linear-gradient(135deg, #14b8a6, #0ea5e9);
color: white;
border: none;
border-radius: 50px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: transform 0.15s, box-shadow 0.15s;
}
button:hover { transform: translateY(-2px); box-shadow: 0 8px 20px rgba(20,184,166,0.35); }
button:active { transform: translateY(0); }
#output { margin-top: 20px; font-size: 13px; color: #4ade80; font-weight: 500; min-height: 20px; }
</style>
</head>
<body>
<div class="card">
<div class="icon">🔧</div>
<h1>${name}</h1>
<p>${description}</p>
<button id="btn">Run Action</button>
<div id="output"></div>
</div>
<script>
document.getElementById('btn').addEventListener('click', () => {
document.getElementById('output').textContent = '✓ Action executed successfully inside the sandbox!';
// Dispatch notification to parent portal frame via secure postMessage broker
if (typeof window.sendNotification === 'function') {
window.sendNotification('Tool Action', '✓ Action executed successfully inside the sandbox!', 'medium');
} else {
window.parent.postMessage({
type: 'SEND_NOTIFICATION',
payload: {
title: 'Tool Action',
body: '✓ Action executed successfully inside the sandbox!',
priority: 'medium'
}
}, '*');
}
});
</script>
</body>
</html>
`;
const htmlPath = path.join(process.cwd(), 'tool.html');
fs.writeFileSync(htmlPath, sampleHtml, 'utf8');
console.log(ok(`Created tool.html → ${htmlPath}`));
console.log(`
${C.bold}${C.green}Project ready!${C.reset} Next steps:
1. Edit ${C.cyan}tool.html${C.reset} with your custom UI and JavaScript logic
2. Edit ${C.cyan}tool.json${C.reset} to update metadata (name, description, category…)
3. Validate before deploying:
${C.dim}node ie-cli.js validate${C.reset}
4. Log in to the platform:
${C.dim}node ie-cli.js login --username <you> --password <pass>${C.reset}
5. Deploy:
${C.dim}node ie-cli.js deploy --file ./tool.html${C.reset}
`);
}
// ── login ────────────────────────────────────────────────────
async function handleLogin() {
console.log(hdr('Login'));
let username = flag('--username', '-u');
let password = flag('--password', '-p');
const host = flag('--host') || DEFAULT_HOST;
if (!username) username = await ask('Username: ');
if (!password) password = await ask('Password: ', true);
if (!username || !password) {
console.error(err('Username and password are required.'));
process.exit(1);
}
const spin = spinner(`Authenticating as ${C.bold}${username}${C.reset}…`);
try {
const data = await request('POST', `${host}/api/auth/login`, { username, password });
const token = data.token || data.accessToken;
if (!token) throw new Error('Server did not return a token.');
saveCliConfig({ token, host, username, loggedInAt: new Date().toISOString() });
spin.stop(`Logged in as ${C.bold}${username}${C.reset} (token saved to ${CLI_CONFIG_PATH})`);
} catch (e) {
spin.fail(`Login failed: ${e.message}`);
process.exit(1);
}
}
// ── logout ───────────────────────────────────────────────────
function handleLogout() {
if (fs.existsSync(CLI_CONFIG_PATH)) {
fs.unlinkSync(CLI_CONFIG_PATH);
console.log(ok('Credentials cleared. You are now logged out.'));
} else {
console.log(wrn('No saved credentials found – already logged out.'));
}
}
// ── whoami ───────────────────────────────────────────────────
async function handleWhoami() {
const cfg = loadCliConfig();
if (!cfg || !cfg.token) {
console.error(err('Not logged in. Run: node ie-cli.js login'));
process.exit(1);
}
const spin = spinner('Fetching account info…');
try {
const data = await request('GET', `${cfg.host}/api/auth/me`, null, cfg.token);
spin.stop('Account details:');
const u = data.user || data;
console.log(` ${C.bold}Username${C.reset} : ${u.username || cfg.username}`);
console.log(` ${C.bold}Email${C.reset} : ${u.email || '—'}`);
console.log(` ${C.bold}Role${C.reset} : ${u.role || '—'}`);
console.log(` ${C.bold}Plan${C.reset} : ${u.subscriptionPlan || 'FREE'}`);
console.log(` ${C.bold}Host${C.reset} : ${cfg.host}`);
console.log(` ${C.bold}Logged in${C.reset} : ${cfg.loggedInAt || '—'}`);
} catch (e) {
spin.fail(`Could not fetch account info: ${e.message}`);
process.exit(1);
}
}
// ── validate ─────────────────────────────────────────────────
function handleValidate() {
console.log(hdr('Validating tool files'));
const filePath = flag('--file', '-f') || './tool.html';
const configPath = path.join(path.dirname(path.resolve(filePath)), 'tool.json');
let pass = true;
// Check HTML file
const absFile = path.resolve(filePath);
if (!fs.existsSync(absFile)) {
console.error(err(`tool.html not found: ${absFile}`));
pass = false;
} else {
const code = fs.readFileSync(absFile, 'utf8');
console.log(ok(`tool.html found (${(code.length / 1024).toFixed(1)} KB)`));
if (code.length > 512 * 1024) console.warn(wrn('tool.html is larger than 512 KB – consider reducing size.'));
if (!code.includes('<html')) console.warn(wrn('tool.html does not contain <html> tag.'));
if (!code.includes('<script')) console.warn(wrn('tool.html has no <script> block – may be static only.'));
}
// Check tool.json
const absConfig = fs.existsSync(configPath) ? configPath : path.join(process.cwd(), 'tool.json');
if (!fs.existsSync(absConfig)) {
console.error(err(`tool.json not found: ${absConfig}`));
pass = false;
} else {
let cfg;
try {
cfg = JSON.parse(fs.readFileSync(absConfig, 'utf8'));
console.log(ok(`tool.json parsed successfully`));
} catch (e) {
console.error(err(`tool.json is invalid JSON: ${e.message}`));
pass = false;
}
if (cfg) {
if (!cfg.name) { console.error(err('tool.json missing required field: name')); pass = false; }
if (!cfg.description) { console.warn(wrn('tool.json missing field: description')); }
if (!cfg.category) { console.warn(wrn('tool.json missing field: category')); }
if (!cfg.version) { console.warn(wrn('tool.json missing field: version')); }
if (cfg.name) console.log(ok(`Tool name: "${cfg.name}" v${cfg.version || '?'}`));
}
}
if (pass) {
console.log(`\n${C.green}${C.bold}All checks passed – ready to deploy!${C.reset}`);
} else {
console.log(`\n${C.red}${C.bold}Validation failed – fix the errors above before deploying.${C.reset}`);
process.exit(1);
}
}
// ── deploy ───────────────────────────────────────────────────
async function handleDeploy() {
console.log(hdr('Deploying tool'));
const cfg = loadCliConfig();
if (!cfg || !cfg.token) { console.error(err('Not logged in. Run: node ie-cli.js login')); process.exit(1); }
const filePath = flag('--file', '-f') || './tool.html';
const absFile = path.resolve(filePath);
if (!fs.existsSync(absFile)) { console.error(err(`File not found: ${absFile}`)); process.exit(1); }
// Locate tool.json
const configPath = [
path.join(path.dirname(absFile), 'tool.json'),
path.join(process.cwd(), 'tool.json'),
].find(fs.existsSync);
if (!configPath) { console.error(err('tool.json not found. Run: node ie-cli.js init')); process.exit(1); }
let meta;
try { meta = JSON.parse(fs.readFileSync(configPath, 'utf8')); }
catch (e) { console.error(err(`Invalid tool.json: ${e.message}`)); process.exit(1); }
const code = fs.readFileSync(absFile, 'utf8');
console.log(inf(`Tool : ${meta.name || 'Unnamed'} v${meta.version || '1.0.0'}`));
console.log(inf(`File : ${absFile} (${(code.length / 1024).toFixed(1)} KB)`));
console.log(inf(`Server : ${cfg.host}`));
console.log(inf(`User : ${cfg.username || '—'}`));
const payload = {
name: meta.name || 'Unnamed Tool',
description: meta.description || '',
version: meta.version || '1.0.0',
category: meta.category || 'utility',
type: meta.type || 'script',
tags: Array.isArray(meta.tags) ? meta.tags.join(',') : (meta.tags || ''),
icon: meta.icon || '🔧',
isPublic: hasFlag('--private') ? false : (hasFlag('--public') ? true : (meta.isPublic ?? true)),
requiresPro: hasFlag('--pro') ? true : (meta.requiresPro ?? false),
license: meta.license || 'MIT',
code,
};
const spin = spinner('Uploading to server…');
try {
const data = await request('POST', `${cfg.host}/api/tools/deploy`, payload, cfg.token);
const id = data.toolId || data.id || data.tool?.id;
const url = data.deploymentUrl || (id ? `${cfg.host}/tools/${id}` : cfg.host);
spin.stop('Deployment successful! 🚀');
console.log(`\n ${C.bold}Tool ID${C.reset} : ${C.cyan}${id}${C.reset}`);
console.log(` ${C.bold}URL${C.reset} : ${C.cyan}${url}${C.reset}`);
console.log(`\n${C.dim}Tip: run ${C.reset}node ie-cli.js status --id ${id}${C.dim} to check live status.${C.reset}\n`);
// Save last deployed ID to config for convenience
saveCliConfig({ ...cfg, lastDeployedId: id });
} catch (e) {
spin.fail(`Deployment failed (${e.status || '?'}): ${e.message}`);
if (e.data) console.error(C.dim + JSON.stringify(e.data, null, 2) + C.reset);
process.exit(1);
}
}
// ── update ───────────────────────────────────────────────────
async function handleUpdate() {
console.log(hdr('Updating deployed tool'));
const cfg = loadCliConfig();
if (!cfg || !cfg.token) { console.error(err('Not logged in.')); process.exit(1); }
const id = flag('--id') || cfg.lastDeployedId;
if (!id) { console.error(err('--id is required. Get it from: node ie-cli.js list')); process.exit(1); }
const filePath = flag('--file', '-f') || './tool.html';
const absFile = path.resolve(filePath);
if (!fs.existsSync(absFile)) { console.error(err(`File not found: ${absFile}`)); process.exit(1); }
const code = fs.readFileSync(absFile, 'utf8');
const configPath = [
path.join(path.dirname(absFile), 'tool.json'),
path.join(process.cwd(), 'tool.json'),
].find(fs.existsSync);
let meta = {};
if (configPath) { try { meta = JSON.parse(fs.readFileSync(configPath, 'utf8')); } catch {} }
let isPublic = meta.isPublic ?? true;
if (hasFlag('--private')) {
isPublic = false;
} else if (hasFlag('--public')) {
isPublic = true;
}
let requiresPro = meta.requiresPro ?? false;
if (hasFlag('--pro')) {
requiresPro = true;
}
const payload = {
...meta,
isPublic,
requiresPro,
code
};
console.log(inf(`Tool ID: ${id}`));
const spin = spinner('Uploading update…');
try {
const data = await request('PUT', `${cfg.host}/api/tools/${id}`, payload, cfg.token);
spin.stop(`Tool ${id} updated successfully`);
console.log(` ${C.bold}Version${C.reset}: ${data.tool?.version || data.version || meta.version || '—'}`);
} catch (e) {
spin.fail(`Update failed: ${e.message}`);
process.exit(1);
}
}
// ── delete ───────────────────────────────────────────────────
async function handleDelete() {
console.log(hdr('Delete deployed tool'));
const cfg = loadCliConfig();
if (!cfg || !cfg.token) { console.error(err('Not logged in.')); process.exit(1); }
const id = flag('--id');
if (!id) { console.error(err('--id is required.')); process.exit(1); }
const confirm = await ask(`${C.red}Are you sure you want to permanently delete tool ${id}? [yes/N]: ${C.reset}`);
if (confirm.toLowerCase() !== 'yes') { console.log(inf('Cancelled.')); return; }
const spin = spinner(`Deleting tool ${id}…`);
try {
await request('DELETE', `${cfg.host}/api/tools/${id}`, null, cfg.token);
spin.stop(`Tool ${id} deleted permanently.`);
} catch (e) {
spin.fail(`Delete failed: ${e.message}`);
process.exit(1);
}
}
function truncate(str, len) {
str = String(str || '');
if (str.length > len) {
return str.slice(0, len - 1) + '…';
}
return str.padEnd(len);
}
// ── list ─────────────────────────────────────────────────────
async function handleList() {
console.log(hdr('Your deployed tools'));
const cfg = loadCliConfig();
if (!cfg || !cfg.token) { console.error(err('Not logged in.')); process.exit(1); }
const spin = spinner('Fetching tool list…');
try {
const data = await request('GET', `${cfg.host}/api/tools?limit=100`, null, cfg.token);
const tools = Array.isArray(data) ? data : (data.tools || data.items || data.results || []);
spin.stop(`Found ${tools.length} tool(s)`);
if (tools.length === 0) {
console.log(inf('No tools deployed yet. Run: node ie-cli.js deploy'));
return;
}
const cols = [8, 30, 12, 10, 8, 10];
const divider = cols.map(c => '─'.repeat(c)).join('─┼─');
const header = [
'ID'.padEnd(cols[0]),
'Name'.padEnd(cols[1]),
'Category'.padEnd(cols[2]),
'Version'.padEnd(cols[3]),
'Public'.padEnd(cols[4]),
'Downloads'.padEnd(cols[5]),
].join(' │ ');
console.log(`\n ${C.bold}${header}${C.reset}`);
console.log(` ${divider}`);
tools.forEach(t => {
const row = [
truncate(t.id, cols[0]),
truncate(t.name, cols[1]),
truncate(t.category, cols[2]),
truncate(t.version, cols[3]),
truncate(t.isPublic ? 'Yes' : 'No', cols[4]),
truncate(t.downloads || 0, cols[5]),
].join(' │ ');
console.log(` ${row}`);
});
console.log();
} catch (e) {
spin.fail(`Could not fetch tools: ${e.message}`);
process.exit(1);
}
}
// ── status ───────────────────────────────────────────────────
async function handleStatus() {
console.log(hdr('Tool status'));
const cfg = loadCliConfig();
if (!cfg || !cfg.token) { console.error(err('Not logged in.')); process.exit(1); }
const id = flag('--id') || cfg.lastDeployedId;
if (!id) { console.error(err('--id is required.')); process.exit(1); }
const spin = spinner(`Fetching status for tool ${id}…`);
try {
const data = await request('GET', `${cfg.host}/api/tools/${id}`, null, cfg.token);
const t = data.tool || data;
spin.stop(`Status for: ${C.bold}${t.name}${C.reset}`);
console.log(`
${C.bold}ID${C.reset} : ${t.id}
${C.bold}Name${C.reset} : ${t.name}
${C.bold}Version${C.reset} : ${t.version || '—'}
${C.bold}Category${C.reset} : ${t.category || '—'}
${C.bold}Author${C.reset} : ${t.authorUsername || '—'}
${C.bold}Public${C.reset} : ${t.isPublic ? C.green + 'Yes' : C.red + 'No'}${C.reset}
${C.bold}Requires Pro${C.reset} : ${t.requiresPro ? C.yellow + 'Yes' : 'No'}${C.reset}
${C.bold}Downloads${C.reset} : ${t.downloads || 0}
${C.bold}Rating${C.reset} : ${t.rating ? t.rating.toFixed(1) + ' / 5' : '—'} (${t.ratingCount || 0} reviews)
${C.bold}Created${C.reset} : ${t.createdAt ? new Date(t.createdAt).toLocaleString() : '—'}
${C.bold}Updated${C.reset} : ${t.updatedAt ? new Date(t.updatedAt).toLocaleString() : '—'}
${C.bold}URL${C.reset} : ${C.cyan}${cfg.host}/tools/${t.id}${C.reset}
`);
} catch (e) {
spin.fail(`Could not fetch status: ${e.message}`);
process.exit(1);
}
}
// ── logs ─────────────────────────────────────────────────────
async function handleLogs() {
console.log(hdr('Deployment logs'));
const cfg = loadCliConfig();
if (!cfg || !cfg.token) { console.error(err('Not logged in.')); process.exit(1); }
const id = flag('--id') || cfg.lastDeployedId;
const limit = parseInt(flag('--limit') || '20', 10);
const url = id
? `${cfg.host}/api/tools/${id}/logs?limit=${limit}`
: `${cfg.host}/api/tools/logs?limit=${limit}`;
const spin = spinner('Fetching logs…');
try {
const data = await request('GET', url, null, cfg.token);
const logs = Array.isArray(data) ? data : (data.logs || data.items || []);
spin.stop(`${logs.length} log entries`);
if (logs.length === 0) { console.log(inf('No log entries found.')); return; }
logs.forEach(l => {
const ts = l.timestamp ? new Date(l.timestamp).toLocaleTimeString() : '?';
const lvl = (l.level || 'INFO').toUpperCase();
const col = lvl === 'ERROR' ? C.red : lvl === 'WARN' ? C.yellow : C.green;
console.log(` ${C.dim}${ts}${C.reset} ${col}${lvl.padEnd(5)}${C.reset} ${l.message || JSON.stringify(l)}`);
});
} catch (e) {
spin.fail(`Could not fetch logs: ${e.message}`);
// Graceful fallback – logs endpoint may not exist
console.log(inf('The server may not support logs yet. Check /api-docs for details.'));
}
}
// ════════════════════════════════════════════════════════════
// MAIN ROUTER
// ════════════════════════════════════════════════════════════
async function main() {
switch (command) {
case 'init': await handleInit(); break;
case 'login': await handleLogin(); break;
case 'logout': handleLogout(); break;
case 'whoami': await handleWhoami(); break;
case 'validate': handleValidate(); break;
case 'deploy': await handleDeploy(); break;
case 'update': await handleUpdate(); break;
case 'delete': await handleDelete(); break;
case 'list': await handleList(); break;
case 'status': await handleStatus(); break;
case 'logs': await handleLogs(); break;
case 'help':
case '-h':
case '--help':
case undefined:
default:
showHelp();
}
}
main().catch(e => {
console.error(err(`Unexpected error: ${e.message}`));
process.exit(1);
});

Xet Storage Details

Size:
31.3 kB
·
Xet hash:
7cbca10bfc1b8c2e68e58985eb84adc63639e19dd1c799fbf70e1e025b8a30fd

Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.