Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 50 additions & 19 deletions bin/lib/onboard.js
Original file line number Diff line number Diff line change
Expand Up @@ -177,9 +177,14 @@ const REMOTE_PROVIDER_CONFIG = {
// Non-interactive mode: set by --non-interactive flag or env var.
// When active, all prompts use env var overrides or sensible defaults.
let NON_INTERACTIVE = false;
let RECREATE_SANDBOX = false;

function isNonInteractive() {
return NON_INTERACTIVE;
return NON_INTERACTIVE || process.env.NEMOCLAW_NON_INTERACTIVE === "1";
}

function isRecreateSandbox() {
return RECREATE_SANDBOX || process.env.NEMOCLAW_RECREATE_SANDBOX === "1";
}

function note(message) {
Expand Down Expand Up @@ -2003,34 +2008,59 @@ async function createSandbox(
hasMessagingTokens &&
messagingTokenDefs.some(({ name, token }) => token && !providerExistsInGateway(name));

if (existingSandboxState === "ready" && process.env.NEMOCLAW_RECREATE_SANDBOX !== "1") {
if (needsProviderMigration) {
console.log(` Sandbox '${sandboxName}' exists but messaging providers are not attached.`);
console.log(" Recreating to ensure credentials flow through the provider pipeline.");
} else {
// Upsert messaging providers even on reuse so credential changes take
// effect without requiring a full sandbox recreation. Only the
// --provider attachment flags need to be on the create path.
upsertMessagingProviders(messagingTokenDefs);
ensureDashboardForward(sandboxName, chatUiUrl);
if (isNonInteractive()) {
if (!isRecreateSandbox() && !needsProviderMigration) {
if (isNonInteractive()) {
if (existingSandboxState === "ready") {
// Upsert messaging providers even on reuse so credential changes take
// effect without requiring a full sandbox recreation.
upsertMessagingProviders(messagingTokenDefs);
note(` [non-interactive] Sandbox '${sandboxName}' exists and is ready — reusing it`);
} else {
console.log(` Sandbox '${sandboxName}' already exists and is ready.`);
console.log(" Reusing existing sandbox.");
console.log(" Set NEMOCLAW_RECREATE_SANDBOX=1 to recreate it instead.");
note(" Pass --recreate-sandbox or set NEMOCLAW_RECREATE_SANDBOX=1 to force recreation.");
ensureDashboardForward(sandboxName, chatUiUrl);
return sandboxName;
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
console.error(` Sandbox '${sandboxName}' already exists but is not ready.`);
console.error(" Pass --recreate-sandbox or set NEMOCLAW_RECREATE_SANDBOX=1 to overwrite.");
process.exit(1);
}

if (existingSandboxState === "ready") {
console.log(` Sandbox '${sandboxName}' already exists.`);
console.log(" Choosing 'n' will delete the existing sandbox and create a new one.");
const answer = await promptOrDefault(" Reuse existing sandbox? [Y/n]: ", null, "y");
const normalizedAnswer = answer.trim().toLowerCase();
if (normalizedAnswer !== "n" && normalizedAnswer !== "no") {
upsertMessagingProviders(messagingTokenDefs);
ensureDashboardForward(sandboxName, chatUiUrl);
return sandboxName;
}
} else {
console.log(` Sandbox '${sandboxName}' exists but is not ready.`);
console.log(" Selecting 'n' will abort onboarding.");
const answer = await promptOrDefault(
" Delete it and create a new one? [Y/n]: ",
null,
"y",
);
const normalizedAnswer = answer.trim().toLowerCase();
if (normalizedAnswer === "n" || normalizedAnswer === "no") {
console.log(" Aborting onboarding.");
process.exit(1);
}
return sandboxName;
}
}

if (existingSandboxState === "ready" && needsProviderMigration) {
note(` Sandbox '${sandboxName}' exists — recreating to attach messaging providers.`);
if (needsProviderMigration) {
console.log(` Sandbox '${sandboxName}' exists but messaging providers are not attached.`);
console.log(" Recreating to ensure credentials flow through the provider pipeline.");
} else if (existingSandboxState === "ready") {
note(` Sandbox '${sandboxName}' exists and is ready — recreating by explicit request.`);
} else {
note(` Sandbox '${sandboxName}' exists but is not ready — recreating it.`);
}

note(` Deleting and recreating sandbox '${sandboxName}'...`);

// Destroy old sandbox
runOpenshell(["sandbox", "delete", sandboxName], { ignoreError: true });
registry.removeSandbox(sandboxName);
Expand Down Expand Up @@ -3775,6 +3805,7 @@ function skippedStepMessage(stepName, detail, reason = "resume") {
// eslint-disable-next-line complexity
async function onboard(opts = {}) {
NON_INTERACTIVE = opts.nonInteractive || process.env.NEMOCLAW_NON_INTERACTIVE === "1";
RECREATE_SANDBOX = opts.recreateSandbox || process.env.NEMOCLAW_RECREATE_SANDBOX === "1";
delete process.env.OPENSHELL_GATEWAY;
const resume = opts.resume === true;
// In non-interactive mode also accept the env var so CI pipelines can set it.
Expand Down
20 changes: 16 additions & 4 deletions bin/nemoclaw.js
Original file line number Diff line number Diff line change
Expand Up @@ -765,27 +765,39 @@ async function onboard(args) {
if (!fromDockerfile || fromDockerfile.startsWith("--")) {
console.error(" --from requires a path to a Dockerfile");
console.error(
` Usage: nemoclaw onboard [--non-interactive] [--resume] [--from <Dockerfile>] [${NOTICE_ACCEPT_FLAG}]`,
` Usage: nemoclaw onboard [--non-interactive] [--resume] [--recreate-sandbox] [--from <Dockerfile>] [${NOTICE_ACCEPT_FLAG}]`,
);
process.exit(1);
}
args = [...args.slice(0, fromIdx), ...args.slice(fromIdx + 2)];
}

const allowedArgs = new Set(["--non-interactive", "--resume", NOTICE_ACCEPT_FLAG]);
const allowedArgs = new Set([
"--non-interactive",
"--resume",
"--recreate-sandbox",
NOTICE_ACCEPT_FLAG,
]);
const unknownArgs = args.filter((arg) => !allowedArgs.has(arg));
if (unknownArgs.length > 0) {
console.error(` Unknown onboard option(s): ${unknownArgs.join(", ")}`);
console.error(
` Usage: nemoclaw onboard [--non-interactive] [--resume] [--from <Dockerfile>] [${NOTICE_ACCEPT_FLAG}]`,
` Usage: nemoclaw onboard [--non-interactive] [--resume] [--recreate-sandbox] [--from <Dockerfile>] [${NOTICE_ACCEPT_FLAG}]`,
);
process.exit(1);
}
const nonInteractive = args.includes("--non-interactive");
const resume = args.includes("--resume");
const recreateSandbox = args.includes("--recreate-sandbox");
const acceptThirdPartySoftware =
args.includes(NOTICE_ACCEPT_FLAG) || String(process.env[NOTICE_ACCEPT_ENV] || "") === "1";
await runOnboard({ nonInteractive, resume, fromDockerfile, acceptThirdPartySoftware });
await runOnboard({
nonInteractive,
resume,
recreateSandbox,
fromDockerfile,
acceptThirdPartySoftware,
});
}

async function setup(args = []) {
Expand Down
Loading
Loading