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
2 changes: 2 additions & 0 deletions apps/cli/CHANGELOG.MD
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

## Added

- CLI can now also be invoked as `packmind` (alias for `packmind-cli`)
- `packmind` symlink automatically created on install and self-update
- `whoami` command now displays an update notice when a newer CLI version is available on GitHub
- `lint` command now supports negative globs in standard scopes, starting with `!`
- `playbook submit` now checks for duplicate artifact names before submitting creation proposals. If an artifact with the same name (case-insensitive) already exists in the target space, the submission is rejected with a clear error message.
Expand Down
3 changes: 2 additions & 1 deletion apps/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
"description": "A command-line interface for Packmind linting and code quality checks",
"private": false,
"bin": {
"packmind-cli": "./main.cjs"
"packmind-cli": "./main.cjs",
"packmind": "./main.cjs"
},
"main": "./main.cjs",
"files": [
Expand Down
16 changes: 16 additions & 0 deletions apps/cli/scripts/install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,22 @@ install_binary() {
esac

success "Installed to: $target_path"

# Create forward-compatible symlink: packmind -> packmind-cli
case "$PLATFORM" in
windows-*)
alias_name="packmind.exe"
;;
*)
alias_name="packmind"
;;
esac
alias_path="${INSTALL_DIR}/${alias_name}"
if ln -sf "$target_name" "$alias_path" 2>/dev/null; then
info "Created symlink: $alias_path -> $target_name"
else
warn "Could not create symlink: $alias_path -> $target_name (non-critical)"
fi
}

# Auto-login if credentials provided
Expand Down
80 changes: 80 additions & 0 deletions apps/cli/src/infra/commands/updateHandler.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
fetchLatestVersionFromGitHub,
isLocalNpmPackage,
isHomebrewInstall,
createForwardCompatSymlink,
} from './updateHandler';
import * as consoleLogger from '../utils/consoleLogger';
import * as fs from 'fs';
Expand All @@ -20,6 +21,8 @@ jest.mock('../utils/consoleLogger', () => ({
jest.mock('fs', () => ({
...jest.requireActual('fs'),
realpathSync: jest.fn((p: string) => p),
symlinkSync: jest.fn(),
unlinkSync: jest.fn(),
}));

const mockConsoleLogger = consoleLogger as jest.Mocked<typeof consoleLogger>;
Expand Down Expand Up @@ -448,6 +451,83 @@ describe('updateHandler', () => {
});
});

describe('createForwardCompatSymlink', () => {
beforeEach(() => {
(fs.unlinkSync as jest.Mock).mockReset();
(fs.symlinkSync as jest.Mock).mockReset();
});

describe('when running as packmind-cli', () => {
beforeEach(() => {
createForwardCompatSymlink('/usr/local/bin/packmind-cli', 'linux');
});

it('creates packmind symlink', () => {
expect(fs.symlinkSync).toHaveBeenCalledWith(
'packmind-cli',
'/usr/local/bin/packmind',
);
});

it('removes existing symlink before creating new one', () => {
expect(fs.unlinkSync).toHaveBeenCalledWith('/usr/local/bin/packmind');
});
});

describe('when running as packmind', () => {
beforeEach(() => {
createForwardCompatSymlink('/usr/local/bin/packmind', 'linux');
});

it('does not create any symlink', () => {
expect(fs.symlinkSync).not.toHaveBeenCalled();
});

it('does not remove any file', () => {
expect(fs.unlinkSync).not.toHaveBeenCalled();
});
});
Comment thread
greptile-apps[bot] marked this conversation as resolved.

describe('when running on Windows', () => {
it('uses .exe extension', () => {
createForwardCompatSymlink('C:/bin/packmind-cli.exe', 'win32');

expect(fs.symlinkSync).toHaveBeenCalledWith(
'packmind-cli.exe',
'C:/bin/packmind.exe',
);
});
});

describe('when running with an unknown binary name', () => {
it('does nothing', () => {
createForwardCompatSymlink('/usr/local/bin/custom-cli', 'linux');

expect(fs.symlinkSync).not.toHaveBeenCalled();
});
});

describe('when unlink throws (symlink does not exist)', () => {
beforeEach(() => {
(fs.unlinkSync as jest.Mock).mockImplementation(() => {
throw new Error('ENOENT');
});
});

it('does not fail', () => {
expect(() =>
createForwardCompatSymlink('/usr/local/bin/packmind-cli', 'linux'),
).not.toThrow();
});

it('still creates the symlink', () => {
createForwardCompatSymlink('/usr/local/bin/packmind-cli', 'linux');

expect(fs.symlinkSync).toHaveBeenCalled();
});
});
});

describe('updateHandler - executable mode', () => {
it('fetches from GitHub releases API', async () => {
deps.isExecutableMode = true;
Expand Down
37 changes: 37 additions & 0 deletions apps/cli/src/infra/commands/updateHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
unlinkSync,
statSync,
realpathSync,
symlinkSync,
} from 'fs';
import { pipeline } from 'stream/promises';
import { Readable } from 'stream';
Expand Down Expand Up @@ -141,6 +142,39 @@ async function downloadExecutable(
);
}

export function createForwardCompatSymlink(
currentPath: string,
platform: string,
): void {
const dir = path.dirname(currentPath);
const ext = platform === 'win32' ? '.exe' : '';
const primaryName = `packmind-cli${ext}`;
const aliasName = `packmind${ext}`;
const currentBasename = path.basename(currentPath);

if (currentBasename !== primaryName) {
return;
}

const targetName = primaryName;
const symlinkName = aliasName;

const symlinkPath = path.join(dir, symlinkName);
try {
unlinkSync(symlinkPath);
} catch {
// May not exist
}
Comment thread
greptile-apps[bot] marked this conversation as resolved.
try {
symlinkSync(targetName, symlinkPath);
logInfoConsole(
`Created forward-compatible symlink: ${symlinkPath} -> ${targetName}`,
);
} catch {
// Non-critical: symlink creation may fail (e.g., Windows without admin)
}
}

function updateViaNpm(version: string): void {
logInfoConsole(`Updating via npm to version ${version}...`);
execSync(`npm install -g ${NPM_PACKAGE}@${version}`, {
Expand All @@ -166,6 +200,9 @@ async function updateViaExecutableReplace(
if (deps.platform !== 'win32') {
chmodSync(currentPath, 0o755);
}

// Create forward-compatible symlink (mirrors install.sh behavior)
createForwardCompatSymlink(currentPath, deps.platform);
} catch (error) {
// Clean up temp file on failure
try {
Expand Down
1 change: 1 addition & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.