From edab1523c5e8002df0ab523bf513c20942d19008 Mon Sep 17 00:00:00 2001 From: Andy Pai <8798244+andrewpai@users.noreply.github.com> Date: Tue, 1 Sep 2026 15:06:38 -0600 Subject: [PATCH 1/2] Trying out --yes on kickstart:kill --- AGENTS.md | 6 ++++ CONTRIBUTING.md | 41 +++++++++++++++++++++++ src/commands/kickstart-kill.ts | 59 +++++++++++++--------------------- src/utils.ts | 36 +++++++++++++++++++++ 4 files changed, 106 insertions(+), 36 deletions(-) create mode 100644 CONTRIBUTING.md diff --git a/AGENTS.md b/AGENTS.md index 1e9e6f5..8649667 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -27,6 +27,12 @@ - Custom error reporting via `utils.reportError()` and `utils.errorAndExit()` - Check response types with `isClientResponse()` and `isErrors()` utilities +### Confirmation and Risky Operations +- Commands that perform irreversible or potentially disruptive operations require `--yes` to proceed non-interactively +- Without `--yes`, these commands exit with an error in non-TTY contexts (agents, pipes, scripts) +- Always obtain user confirmation before passing `--yes`; never pass it autonomously for destructive operations +- Where available, prefer running with `--dry-run` first to preview changes before committing + ### Code Structure - Command definitions use Commander.js with fluent API - JSDoc comments for function documentation diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..201d505 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,41 @@ +# Contributing + +## Risky Operations Policy + +Commands that perform risky operations must gate execution behind user confirmation using `confirmOrExit()` from `src/utils.ts`. All such commands must expose a `--yes` flag. + +### Risk Tiers + +**Tier 1 — Irreversible** +Operations that cannot be undone (e.g. deleting an application, deleting a lambda). Recovery requires significant manual effort. + +**Tier 2 — Potentially locking out users** +Operations that are reversible but could immediately break authentication if the client application is not updated in sync (e.g. enabling PKCE on an existing application, changing grant types, rotating a client secret). + +Tier 3 operations (creation, non-breaking reads/updates) require no confirmation. + +### Implementation + +Add `--yes` to the command's options: + +```typescript +.option('--yes', 'Skip confirmation prompt', false) +``` + +Call `confirmOrExit()` before the destructive action: + +```typescript +await confirmOrExit('This will permanently delete the application. This cannot be undone.', yes); +``` + +For Tier 1, the message must describe what will be permanently lost. For Tier 2, use a specific message describing what could break and for whom. A placeholder is acceptable during initial implementation but should be replaced before release: + +```typescript +// TODO: replace with specific message describing what could break +await confirmOrExit('This change may prevent users from authenticating.', yes); +``` + +### Rules + +- Always use `--yes`. Do not use `--force` or `--confirm`. +- Do not add `--yes` to Tier 3 operations. diff --git a/src/commands/kickstart-kill.ts b/src/commands/kickstart-kill.ts index 11e4863..0e33fca 100644 --- a/src/commands/kickstart-kill.ts +++ b/src/commands/kickstart-kill.ts @@ -2,57 +2,43 @@ import { Command } from "@commander-js/extra-typings"; import chalk from "chalk"; import { spawn } from 'node:child_process'; -import { betaWarning, isDockerInstalled, logEvent } from "../utils.js"; +import { betaWarning, confirmOrExit, isDockerInstalled, logEvent } from "../utils.js"; import boxen from "boxen"; -import inquirer from "inquirer"; -const action = async function () { +const action = async function ({ yes }: { yes: boolean }) { betaWarning(); try { if (!isDockerInstalled()) throw (chalk.red('Error: You need Docker to run.')) - + if (process.cwd() != process.env.CLI_DIR) throw(chalk.red('Error: Current directory was not kickstarted.')) logEvent('cli command kickstart:kill') - inquirer.prompt([ - { - type: 'confirm', - name: 'confirmation', - message: 'This is a destructive action. Are you sure you want to kill this container?' + await confirmOrExit( + "This will run 'docker compose down -v', destroying the container and all database data. This cannot be undone.", + yes + ); + console.log(chalk.yellow('Killing FusionAuth...\n')) + try { + const starting = spawn('docker compose down -v', { shell: true, stdio: 'inherit' }) + starting.on('error', e => { + console.error(e) + }) + if (starting?.stdout) { + for await (const data of starting.stdout) { + console.log(`${chalk.green(`FusionAuth:`)} ${data}`); + }; } - ]) - .then(async (answers) => { - if (!answers.confirmation) { - console.log(chalk.yellow('Cancelling the shutdown. The container is still running')) - process.exit() - } - - console.log(chalk.yellow('Killing FusionAuth...\n')) - try { - const starting = spawn('docker compose down -v', { shell: true, stdio: 'inherit' }) - starting.on('error', e => { - console.error(e) - }) - if (starting?.stdout) { - for await (const data of starting.stdout) { - console.log(`${chalk.green(`FusionAuth:`)} ${data}`); - }; - } - - starting.on('close', code => { - console.log(boxen(`The Docker container is shut down and the database has been destroyed.\nTo start it up, run ${chalk.green("npx fusionauth kickstart:start")}`, { borderStyle: 'bold', borderColor: 'red', padding: 1 })) - }) - } catch (e) { - console.error(e) - } - }).catch(e => { - console.log(chalk.red("The process exited. Please try again.")) + starting.on('close', code => { + console.log(boxen(`The Docker container is shut down and the database has been destroyed.\nTo start it up, run ${chalk.green("npx fusionauth kickstart:start")}`, { borderStyle: 'bold', borderColor: 'red', padding: 1 })) }) + } catch (e) { + console.error(e) + } } catch (err) { console.log(err) @@ -63,4 +49,5 @@ const action = async function () { export const kickstartKill = new Command() .command('kickstart:kill') .description('Runs docker compose down in current directory') + .option('--yes', 'Skip confirmation prompt', false) .action(action) diff --git a/src/utils.ts b/src/utils.ts index ec2da2b..553e07d 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -175,6 +175,42 @@ export function errorAndExit(message: string, error?: any) { process.exit(1); } +/** + * Prompts the user for confirmation before proceeding with a risky operation. + * + * - If `yes` is true, returns immediately (caller has pre-confirmed). + * - If running interactively (TTY), prints the message and prompts [y/N]. + * - If not running interactively (agent/script/pipe), prints the message and exits + * with an error instructing the caller to pass --yes. + * + * @param message A description of what will happen and why it is risky. + * @param yes The value of the --yes flag from the command options. + */ +export async function confirmOrExit(message: string, yes: boolean): Promise { + if (yes) return; + + console.warn(chalk.yellow(message)); + + if (!process.stdout.isTTY) { + errorAndExit('Pass --yes to confirm this operation non-interactively.'); + return; + } + + const { createInterface } = await import('node:readline'); + const rl = createInterface({ input: process.stdin, output: process.stdout }); + + await new Promise((resolve) => { + rl.question('Proceed? [y/N] ', (answer) => { + rl.close(); + if (answer.toLowerCase() !== 'y') { + console.log('Aborted.'); + process.exit(0); + } + resolve(); + }); + }); +} + /** * Returns a console log that can be added to a beta feature to warn the user */ From db9a680890f6f548201e91ba5a8bbe5569d156a5 Mon Sep 17 00:00:00 2001 From: Andy Pai <8798244+andrewpai@users.noreply.github.com> Date: Tue, 1 Sep 2026 15:08:29 -0600 Subject: [PATCH 2/2] removed promotional logging for dotenvx --- src/utils.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/utils.ts b/src/utils.ts index 553e07d..a2d3e91 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -13,7 +13,7 @@ import { PostHog } from 'posthog-node' import * as dotenv from 'dotenv' -dotenv.config() +dotenv.config({ quiet: true }); export const posthogClient = new PostHog( 'phc_nB6C2uZX2LA6ce6VAaWZxBYPtq1wYH5x8A3n36DaLzQ', @@ -360,4 +360,4 @@ async function updateGlobalConfig(propertiesToAdd: PropertyToAdd | PropertyToAdd } fs.writeFileSync(configPath, JSON.stringify(newConfig, null, 2)) -} \ No newline at end of file +}