Wave/3 full interactive mode#4
Conversation
Refactor CLI to wire up interactive handlers and improve user experience. - devlink.ts: register --version alias, show version string, and delegate to interactive handlers when commands are invoked with no or minimal args (publish, add, installations, update, update-all, restore, remove, retreat). Keeps previous non-interactive flows when args provided. - interactive.ts: export numerous interactive handlers (handlePublish, handleAdd, handleInstallations, handleUpdate, handleUpdateAll, handleRetreat, handleRestore, handleRemove, handleStore) and enhance their behavior (package name parsing, lockfile reading, multiselect/version prompts, spinners, better messages). Improve startInteractive UI (figlet header, author line, added menu entries such as Update All, Retreat, Restore, Remove). - publish.ts: clean up unused imports/whitespace. These changes enable a richer interactive mode and smoother defaults when users run commands without arguments, while preserving existing scripted behavior when arguments are provided.
Bump package version to 0.0.3 and add changelog entries for the release. Remove .npmignore and add a files whitelist to package.json so only dist, README.md, CHANGELOG.md and package.json are published. Improve publish watch behavior by defaulting options.changed=true, adding a 200ms debounce to avoid repeated/infinite publishes, expanding ignored patterns (dist/build/out/lib/target/vendor/.next/.nuxt/.output and lock files), and ignoring devlink.lock/devlink.sig. Ensure the debounce timer is cleared on watcher cleanup.
There was a problem hiding this comment.
Pull request overview
This PR bumps Devlink to v0.0.3 and substantially expands the interactive CLI experience, while also improving publish watch-mode behavior and adjusting npm publish packaging.
Changes:
- Refactors CLI command routing to support guided interactive flows for
publish,add,update,remove,retreat,restore, andupdate-all. - Improves
publish:watchstability via additional ignored paths and debounced republish triggering. - Updates distribution/versioning metadata (version bump,
filesallowlist, changelog updates, remove.npmignore).
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| src/publish.ts | Adds debounce + expanded ignore list in watch mode; defaults changed behavior for watch. |
| src/interactive.ts | Adds/exports interactive handlers for main workflows; improves add/update/remove flows and welcome screen. |
| src/devlink.ts | Routes several commands into interactive handlers when invoked without args; adds yargs --version handling. |
| package.json | Bumps version to 0.0.3 and switches to explicit npm files allowlist. |
| CHANGELOG.md | Documents v0.0.3 features/fixes. |
| .npmignore | Removed in favor of package.json#files. |
Comments suppressed due to low confidence (1)
src/devlink.ts:307
devlink remove --retreatwith no package args currently falls intohandleRemove()and will perform a normal remove flow, ignoring--retreat. If--retreatis supported on this command, route tohandleRetreat()(or otherwise preserve the flag’s semantics) when no packages are specified.
handler: async (argv) => {
const packages = argv._.slice(1) as string[];
if (packages.length === 0 && !argv.all) {
await handleRemove();
return;
}
await removePackages(packages, {
retreat: !!argv.retreat,
workingDir: process.cwd(),
all: !!argv.all,
});
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // Default to changed check in watch mode to skip redundant publishes | ||
| if (options.changed === undefined) { | ||
| options.changed = true; | ||
| } |
There was a problem hiding this comment.
This assigns to the options parameter (options.changed = true), which violates the repo’s Biome rule style.noParameterAssign (and also mutates the caller’s object). Create a derived options object for watch-mode defaults instead (e.g., compute changed = options.changed ?? true and pass { ...options, changed } to publishPackage).
| .boolean(['push', 'watch'].concat(publishFlags)); | ||
| }, | ||
| handler: async (argv) => { | ||
| if (argv._.length <= 1) { | ||
| await handlePublish(); | ||
| return; | ||
| } |
There was a problem hiding this comment.
When devlink publish is run with flags but no positional args (e.g. devlink publish --watch), this branch always invokes the interactive handler and ignores the provided flags. Consider only falling back to handlePublish() when there are no extra CLI tokens after the command (or when an explicit --interactive flag is set), and otherwise keep the existing non-interactive behavior.
| .boolean(['push', 'watch'].concat(publishFlags)); | |
| }, | |
| handler: async (argv) => { | |
| if (argv._.length <= 1) { | |
| await handlePublish(); | |
| return; | |
| } | |
| .boolean(['push', 'watch', 'interactive'].concat(publishFlags)); | |
| }, | |
| handler: async (argv) => { | |
| // Determine if there are any CLI tokens after the "publish" command. | |
| // process.argv layout: [node, script, 'publish', ...tokensAfterCommand] | |
| const hasExtraCliTokensAfterCommand = process.argv.length > 3; | |
| if (argv.interactive || !hasExtraCliTokensAfterCommand) { | |
| await handlePublish(); | |
| return; | |
| } |
| command: 'update-all', | ||
| describe: 'Update all devlinked packages to latest version', | ||
| handler: async () => { | ||
| await updateAllPackages(process.cwd()); | ||
| if (process.argv.length <= 3) { | ||
| await handleUpdateAll(); | ||
| } else { | ||
| await updateAllPackages(process.cwd()); | ||
| } | ||
| }, |
There was a problem hiding this comment.
Using process.argv.length to decide between interactive vs non-interactive makes behavior depend on unrelated global flags (e.g. devlink update-all --quiet will skip the interactive flow). Prefer deciding based on the parsed argv for this command (e.g. argv._.length) or an explicit --interactive/--no-interactive option.
| await updatePackages(argv._.slice(1) as string[], { | ||
| const packages = argv._.slice(1) as string[]; | ||
| if (packages.length === 0) { | ||
| await handleUpdate(); |
There was a problem hiding this comment.
This changes devlink update with no package args from non-interactive behavior (updating/syncing all devlinked packages) to an interactive prompt, which can break automation/CI scripts. If interactive is intended, consider gating it behind TTY detection or an explicit --interactive flag, and keep the current non-interactive default when stdin/stdout isn’t a TTY.
| await handleUpdate(); | |
| if (process.stdout.isTTY && process.stdin.isTTY) { | |
| await handleUpdate(); | |
| } else { | |
| await updateAllPackages(process.cwd()); | |
| } |
| const watcher = chokidar.watch(workingDir, { | ||
| ignored: [ | ||
| '**/node_modules/**', | ||
| '**/.git/**', | ||
| '**/dist/**', | ||
| '**/build/**', | ||
| '**/out/**', | ||
| '**/lib/**', | ||
| '**/target/**', | ||
| '**/vendor/**', | ||
| '**/.next/**', | ||
| '**/.nuxt/**', | ||
| '**/.output/**', | ||
| '**/package-lock.json', | ||
| '**/pnpm-lock.yaml', | ||
| '**/yarn.lock', | ||
| join(workingDir, 'package.json'), | ||
| join(workingDir, 'devlink.lock'), | ||
| join(workingDir, 'devlink.sig'), | ||
| ], | ||
| persistent: true, | ||
| ignoreInitial: true, | ||
| }); | ||
|
|
||
| watcher.on('all', async (event, path) => { | ||
| console.log(`File ${path} ${event}, republishing...`); | ||
| await runPublish(); | ||
| if (debounceTimeout) clearTimeout(debounceTimeout); | ||
| debounceTimeout = setTimeout(async () => { | ||
| console.log(`File ${path} ${event}, republishing...`); | ||
| await runPublish(); | ||
| }, 200); |
There was a problem hiding this comment.
The new debounce behavior and expanded ignore patterns in watch mode aren’t covered by tests. There’s already a watch-mode test; consider extending it to assert that multiple rapid changes result in a single republish (debounce) and that changes to ignored paths don’t trigger republish.
| if (packageName) { | ||
| const parsed = parsePackageName(packageName); | ||
| selectedPackage = parsed.name; | ||
| selectedVersion = parsed.version; | ||
| } | ||
|
|
||
| if (!selectedPackage) { | ||
| const selected = await select({ | ||
| message: 'Select a package to add from store:', | ||
| options: packageNames.map((name) => ({ value: name, label: name })), | ||
| }); | ||
|
|
||
| if (isCancel(selected)) return; | ||
| selectedPackage = selected as string; | ||
| } | ||
|
|
||
| const pkgData = store.packages[selectedPackage]; | ||
| if (!pkgData) { | ||
| note(pc.red(`Package ${selectedPackage} not found in store.`), 'Error'); | ||
| return; | ||
| } | ||
|
|
||
| const pkgData = store.packages[selectedPackage as string]; | ||
| const versions = Object.keys(pkgData.versions).sort((a, b) => { | ||
| const timeA = new Date(pkgData.versions[a].publishedAt).getTime(); | ||
| const timeB = new Date(pkgData.versions[b].publishedAt).getTime(); | ||
| return timeB - timeA; | ||
| }); | ||
|
|
||
| const selectedVersion = await select({ | ||
| message: `Select version for ${pc.cyan(selectedPackage as string)}:`, | ||
| options: versions.map((v) => ({ value: v, label: v })), | ||
| }); | ||
| if (!selectedVersion) { | ||
| const version = await select({ | ||
| message: `Select version for ${pc.cyan(selectedPackage)}:`, | ||
| options: versions.map((v) => ({ value: v, label: v })), | ||
| }); | ||
|
|
||
| if (isCancel(selectedVersion)) return; | ||
| if (isCancel(version)) return; | ||
| selectedVersion = version as string; | ||
| } |
There was a problem hiding this comment.
When handleAdd() is invoked with a packageName that includes a version (e.g. via devlink add pkg@1.2.3), selectedVersion skips the version picker but is never validated against pkgData.versions. If the version doesn’t exist in the store, addPackages() will just warn/skip internally, but this flow will still print a success message. Validate that the requested version exists (or fall back to prompting) before calling addPackages.
Detect interactive TTY sessions and add a --interactive option; introduce shouldRunInteractive to auto-launch interactive handlers when appropriate across publish/add/install/update/restore/remove/retreat commands. Validate explicit package versions in handleAdd to surface errors when a provided version is missing. Refactor publishPackageWatch to default to changed=true in watch mode, use a function-based ignored matcher with normalized relative paths and regex patterns, ensure debounce timers are cleared on close, and pass normalized watch options into publish. Update tests to use an isolated store location, adjust timing, and add tests for debounce behavior and ignored-directory handling.
This pull request introduces Devlink version 0.0.3, focusing on a major upgrade to the interactive CLI experience, improved workflows for all commands, and several bug fixes and enhancements. The changes include a comprehensive refactor of the interactive command handlers, improved package management flows, a premium welcome screen, and fixes for versioning and publish issues. The
.npmignoreis also removed in favor of explicitfilesconfiguration inpackage.json.Interactive CLI and Workflow Improvements
add,update,remove,restore,retreat, andupdate-all, providing project-aware package selection and multi-select where appropriate. (src/interactive.tsF5bb7c26L41R185, F5bb7c26L90R433, [1] [2] [3] [4] [5] [6] [7] [8]src/interactive.tssrc/interactive.tsL280-R438)update-all,retreat,restore, and improved navigation for installations and package store browsing. (src/interactive.tssrc/interactive.tsR454-L307)Bug Fixes and Quality of Life Enhancements
publish:watchmode by improving file ignoring and debouncing logic. (CHANGELOG.mdCHANGELOG.mdR3-R15)devlink --version. (CHANGELOG.md[1]package.json[2] [3]addcommand, including parsing package names and handling versions interactively. (src/interactive.tsF5bb7c26L87R99, src/interactive.tsL99-R141)Package Management and Distribution
update-allcommand to sync all devlinked packages to their latest versions, available both as a CLI and interactive command. (src/interactive.ts[1] [2].npmignorefile was removed, and afilesarray was added topackage.jsonto explicitly control published content. (.npmignore[1]package.json[2]Documentation
CHANGELOG.mdto reflect all new features, fixes, and improvements in version 0.0.3. (CHANGELOG.mdCHANGELOG.mdR3-R15)