Repository navigation
feat: add shell completion for nemoclaw CLI #160
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 4 commits
c5edc91
6728e40
3bafc6e
28d1f66
e92b117
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,172 @@ | ||
| // SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. | ||
| // SPDX-License-Identifier: Apache-2.0 | ||
| // | ||
| // Sandbox backup and restore functionality | ||
|
|
||
| const fs = require("fs"); | ||
| const path = require("path"); | ||
| const os = require("os"); | ||
| const { execFileSync } = require("child_process"); | ||
| const registry = require("./registry"); | ||
|
|
||
| const BACKUP_DIR = path.join(process.env.HOME || "/tmp", ".nemoclaw", "backups"); | ||
|
|
||
| function ensureBackupDir() { | ||
| fs.mkdirSync(BACKUP_DIR, { recursive: true, mode: 0o700 }); | ||
| } | ||
|
|
||
| function listBackups() { | ||
| ensureBackupDir(); | ||
| const files = fs.readdirSync(BACKUP_DIR).filter(f => f.endsWith(".json")); | ||
| const backups = []; | ||
|
|
||
| for (const file of files) { | ||
| try { | ||
| const content = JSON.parse(fs.readFileSync(path.join(BACKUP_DIR, file), "utf-8")); | ||
| backups.push({ | ||
| name: content.metadata.name, | ||
| createdAt: content.metadata.createdAt, | ||
| path: path.join(BACKUP_DIR, file), | ||
| size: fs.statSync(path.join(BACKUP_DIR, file)).size, | ||
| }); | ||
| } catch {} | ||
| } | ||
|
|
||
| return backups.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt)); | ||
| } | ||
|
|
||
| function exportSandbox(sandboxName, outputPath) { | ||
| const sandbox = registry.getSandbox(sandboxName); | ||
| if (!sandbox) { | ||
| console.error(` Sandbox not found: ${sandboxName}`); | ||
| return null; | ||
| } | ||
|
|
||
| ensureBackupDir(); | ||
|
|
||
| const timestamp = new Date().toISOString().replace(/[:.]/g, "-"); | ||
| const defaultName = `${sandboxName}-${timestamp}.json`; | ||
| const backupPath = outputPath || path.join(BACKUP_DIR, defaultName); | ||
|
|
||
| let policyContent = ""; | ||
| try { | ||
| policyContent = execFileSync("openshell", ["policy", "get", sandboxName], { | ||
| encoding: "utf-8", | ||
| timeout: 10000, | ||
| stdio: ["pipe", "pipe", "pipe"] | ||
| }); | ||
| } catch { | ||
| policyContent = ""; | ||
| } | ||
|
|
||
| const backup = { | ||
| version: "1.0", | ||
| metadata: { | ||
| name: sandboxName, | ||
| createdAt: new Date().toISOString(), | ||
| nemoclawVersion: require("../../package.json").version, | ||
| }, | ||
| sandbox: { | ||
| name: sandbox.name, | ||
| model: sandbox.model, | ||
| provider: sandbox.provider, | ||
| gpuEnabled: sandbox.gpuEnabled, | ||
| policies: sandbox.policies || [], | ||
| }, | ||
| policy: policyContent, | ||
| }; | ||
|
|
||
| const dir = path.dirname(backupPath); | ||
| if (dir !== BACKUP_DIR) { | ||
| fs.mkdirSync(dir, { recursive: true, mode: 0o700 }); | ||
| } | ||
|
|
||
| fs.writeFileSync(backupPath, JSON.stringify(backup, null, 2), { mode: 0o600 }); | ||
| console.log(` Exported sandbox '${sandboxName}' to: ${backupPath}`); | ||
|
|
||
| return backupPath; | ||
| } | ||
|
|
||
| function importSandbox(backupPath, newName) { | ||
| if (!fs.existsSync(backupPath)) { | ||
| console.error(` Backup file not found: ${backupPath}`); | ||
| return false; | ||
| } | ||
|
|
||
| let backup; | ||
| try { | ||
| backup = JSON.parse(fs.readFileSync(backupPath, "utf-8")); | ||
| } catch { | ||
| console.error(` Invalid backup file: ${backupPath}`); | ||
| return false; | ||
| } | ||
|
|
||
| if (!backup.version || !backup.sandbox) { | ||
| console.error(" Invalid backup format"); | ||
| return false; | ||
| } | ||
|
|
||
| const sandboxName = newName || backup.sandbox.name; | ||
|
|
||
| console.log(` Creating sandbox '${sandboxName}' from backup...`); | ||
|
|
||
| try { | ||
| execFileSync("openshell", ["sandbox", "exists", sandboxName], { | ||
| encoding: "utf-8", | ||
| stdio: ["pipe", "pipe", "pipe"] | ||
| }); | ||
| console.error(` Sandbox '${sandboxName}' already exists. Use a different name or delete it first.`); | ||
| return false; | ||
| } catch {} | ||
|
|
||
| console.log(` Note: This only imports the registry config.`); | ||
| console.log(` You need to manually recreate the sandbox and apply policies.`); | ||
|
|
||
| registry.registerSandbox({ | ||
| name: sandboxName, | ||
| model: backup.sandbox.model, | ||
| provider: backup.sandbox.provider, | ||
| gpuEnabled: backup.sandbox.gpuEnabled, | ||
| policies: backup.sandbox.policies || [], | ||
| }); | ||
|
Comment on lines
+128
to
+150
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Validate imported sandbox names before persisting them. Line 128 accepts 🤖 Prompt for AI Agents |
||
|
|
||
| if (backup.policy) { | ||
| const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-restore-")); | ||
| const policyPath = path.join(tmpDir, "policy.yaml"); | ||
| fs.writeFileSync(policyPath, backup.policy, { mode: 0o600 }); | ||
| try { | ||
| execFileSync("openshell", ["policy", "set", "--policy", policyPath, "--wait", sandboxName], { | ||
| encoding: "utf-8", | ||
| timeout: 30000, | ||
| stdio: ["pipe", "pipe", "pipe"] | ||
| }); | ||
| console.log(` Restored policy for '${sandboxName}'`); | ||
| } catch (err) { | ||
| console.warn(` Warning: Could not restore policy: ${err.message}`); | ||
| } finally { | ||
| fs.unlinkSync(policyPath); | ||
| fs.rmdirSync(tmpDir); | ||
| } | ||
| } | ||
|
|
||
| console.log(` Imported sandbox '${sandboxName}' from backup`); | ||
| return true; | ||
| } | ||
|
|
||
| function deleteBackup(backupPath) { | ||
| if (!fs.existsSync(backupPath)) { | ||
| console.error(` Backup not found: ${backupPath}`); | ||
| return false; | ||
| } | ||
| fs.unlinkSync(backupPath); | ||
| console.log(` Deleted backup: ${backupPath}`); | ||
| return true; | ||
| } | ||
|
|
||
| module.exports = { | ||
| BACKUP_DIR, | ||
| listBackups, | ||
| exportSandbox, | ||
| importSandbox, | ||
| deleteBackup, | ||
| }; | ||
Uh oh!
There was an error while loading. Please reload this page.