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
27 changes: 0 additions & 27 deletions .npmignore

This file was deleted.

13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,18 @@
# Devlink changelog

## 0.0.3

### Minor Changes

- Implement full interactive mode with guided workflows for all commands.
- Add premium welcome screen with "Graceful" font and "by MayR Labs" subtitle.
- Fix infinite publish loop in `publish:watch` mode with improved file ignoring and debouncing.
- Fixed versioning issue where `devlink --version` reported an incorrect version.
- Add new `update-all` command to sync all devlinked packages to their latest versions.
- Improved store browsing and version/flag selection for the `add` command.
- Integrated multi-select for cleaning installations.
- Guided `retreat`, `restore`, and `remove` commands with project-aware package selection.

## 0.0.2 (2026-04-02)

### Major Enhancements
Expand Down
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@mayrlabs/devlink",
"version": "0.0.2",
"version": "0.0.3",
"description": "Work with npm/pnpm/yarn packages locally like a boss.",
"private": false,
"author": {
Expand Down Expand Up @@ -65,5 +65,6 @@
},
"publishConfig": {
"access": "public"
}
},
"files": ["dist", "README.md", "CHANGELOG.md", "package.json"]
}
114 changes: 97 additions & 17 deletions src/devlink.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,17 @@ import {
values,
} from './index.js';
import { cleanInstallations, showInstallations } from './installations.js';
import { startInteractive } from './interactive.js';
import {
handleAdd,
handleInstallations,
handlePublish,
handleRemove,
handleRestore,
handleRetreat,
handleUpdate,
handleUpdateAll,
startInteractive,
} from './interactive.js';
import { publishPackageWatch } from './publish.js';
import type { PublishPackageOptions } from './publish.js';
import { readRcConfig } from './rc.js';
Expand Down Expand Up @@ -89,12 +99,25 @@ const commands = [
'help',
];

const isTTY = process.stdin.isTTY && process.stdout.isTTY;

const shouldRunInteractive = (argv: any, positionalArgsCount = 0) => {
if (argv.interactive !== undefined) return !!argv.interactive;
return isTTY && argv._.length <= positionalArgsCount;
};

if (process.argv.length <= 2) {
await startInteractive();
} else {
/* tslint:disable-next-line */
const argv = await yargs(process.argv.slice(2))
.usage(`${cliCommand} [command] [options] [package1 [package2...]]`)
.version(getVersionMessage())
.alias('v', 'version')
.option('interactive', {
type: 'boolean',
describe: 'Run in interactive mode',
})
.coerce('store-folder', (folder: string) => {
if (!devlinkGlobal.devlinkStoreMainDir) {
devlinkGlobal.devlinkStoreMainDir = resolve(folder);
Expand All @@ -119,12 +142,17 @@ if (process.argv.length <= 2) {
.boolean(['push', 'watch'].concat(publishFlags));
},
handler: async (argv) => {
const options = getPublishOptions(argv);
if (argv.watch) {
await publishPackageWatch(options);
} else {
await publishPackage(options);
await publishPackageWatch(getPublishOptions(argv));
return;
}
Comment on lines 142 to +148
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
.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;
}

Copilot uses AI. Check for mistakes.

if (shouldRunInteractive(argv, 1)) {
await handlePublish();
return;
}

await publishPackage(getPublishOptions(argv));
},
})
.command({
Expand All @@ -150,6 +178,10 @@ if (process.argv.length <= 2) {
builder: (y) => y.boolean(['dry']),
handler: async (argv) => {
const action = argv._[1];
if (shouldRunInteractive(argv, 1)) {
await handleInstallations();
return;
}
const packages = argv._.slice(2) as string[];
switch (action) {
case 'show':
Expand Down Expand Up @@ -179,7 +211,31 @@ if (process.argv.length <= 2) {
.help(true);
},
handler: async (argv) => {
await addPackages(argv._.slice(1) as string[], {
const packages = argv._.slice(1) as string[];
const hasFlags =
argv.dev ||
argv.link ||
argv.restore ||
argv.pure ||
argv.workspace ||
argv.update ||
argv.upgrade;

if (shouldRunInteractive(argv, 1) && !hasFlags) {
await handleAdd();
return;
}

if (
packages.length === 1 &&
!hasFlags &&
shouldRunInteractive(argv, 2)
) {
await handleAdd(packages[0]);
return;
}

await addPackages(packages, {
dev: !!argv.dev,
linkDep: !!argv.link,
restore: !!argv.restore,
Expand All @@ -200,7 +256,12 @@ if (process.argv.length <= 2) {
.help(true);
},
handler: async (argv) => {
await updatePackages(argv._.slice(1) as string[], {
const packages = argv._.slice(1) as string[];
if (packages.length === 0 && shouldRunInteractive(argv, 1)) {
await handleUpdate();
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
await handleUpdate();
if (process.stdout.isTTY && process.stdin.isTTY) {
await handleUpdate();
} else {
await updateAllPackages(process.cwd());
}

Copilot uses AI. Check for mistakes.
return;
}
await updatePackages(packages, {
update: !!(argv.update || argv.upgrade),
restore: !!argv.restore,
workingDir: process.cwd(),
Expand All @@ -210,8 +271,9 @@ if (process.argv.length <= 2) {
.command({
command: 'update-all',
describe: 'Update all devlinked packages to latest version',
handler: async () => {
await updateAllPackages(process.cwd());
handler: async (argv) => {
if (shouldRunInteractive(argv, 1)) await handleUpdateAll();
else await updateAllPackages(process.cwd());
},
Comment on lines 272 to 277
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
})
.command({
Expand All @@ -224,7 +286,12 @@ if (process.argv.length <= 2) {
.help(true);
},
handler: async (argv) => {
await updatePackages(argv._.slice(1) as string[], {
const packages = argv._.slice(1) as string[];
if (packages.length === 0 && shouldRunInteractive(argv, 1)) {
await handleRestore();
return;
}
await updatePackages(packages, {
update: !!(argv.update || argv.upgrade),
restore: true,
workingDir: process.cwd(),
Expand All @@ -241,7 +308,16 @@ if (process.argv.length <= 2) {
.help(true);
},
handler: async (argv) => {
await removePackages(argv._.slice(1) as string[], {
const packages = argv._.slice(1) as string[];
if (
packages.length === 0 &&
!argv.all &&
shouldRunInteractive(argv, 1)
) {
await handleRemove();
return;
}
await removePackages(packages, {
retreat: !!argv.retreat,
workingDir: process.cwd(),
all: !!argv.all,
Expand All @@ -254,7 +330,16 @@ if (process.argv.length <= 2) {
'Remove packages from project, but leave in lock file (to be restored later)',
builder: (y) => y.boolean(['all']).help(true),
handler: async (argv) => {
await removePackages(argv._.slice(1) as string[], {
const packages = argv._.slice(1) as string[];
if (
packages.length === 0 &&
!argv.all &&
shouldRunInteractive(argv, 1)
) {
await handleRetreat();
return;
}
await removePackages(packages, {
all: !!argv.all,
retreat: true,
workingDir: process.cwd(),
Expand Down Expand Up @@ -314,11 +399,6 @@ if (process.argv.length <= 2) {
handler: (argv) => {
const inputCommand = argv._[0] as string;
if (!inputCommand) {
if (argv.version) {
console.log(getVersionMessage());
} else {
console.log('Use `devlink --help` to see available commands.');
}
return;
}

Expand Down
Loading
Loading