| // ============================================================ | |
| // 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 | |
| // ============================================================ | |
| ; | |
| 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.