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
24 changes: 24 additions & 0 deletions .agents/skills/pgpm/references/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,30 @@ Non-interactive init requires every question to be answered by flags; see
`--name --fullName --email --username --repoName --license`, plus module
`--moduleName --packageIdentifier --moduleDesc --access`.

### Workspace Inspection

**pgpm ls** — List the pgpm modules in the current workspace

```bash
# Human-readable listing
pgpm ls

# Names or workspace-relative paths, one per line
pgpm ls --names
pgpm ls --paths

# JSON output for scripts and CI
pgpm ls --json
pgpm ls --paths --json
```

For CI package matrices, use the workspace-relative paths directly:

```yaml
- id: list
run: echo "packages=$(pnpm exec pgpm ls --paths --json)" >> "$GITHUB_OUTPUT"
```

### Change Management

**pgpm add** — Add a new database change
Expand Down
177 changes: 177 additions & 0 deletions pgpm/cli/__tests__/ls.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
import fs from 'fs';
import path from 'path';

import { TestFixture } from '../test-utils';

describe('pgpm ls', () => {
let fixture: TestFixture;

beforeEach(() => {
fixture = new TestFixture();
});

afterEach(() => {
fixture.cleanup();
});

const createWorkspace = (modules: Array<{
name: string;
version: string;
requires?: string[];
}> = []) => {
const workspaceDir = path.join(fixture.tempDir, 'workspace');
fs.mkdirSync(path.join(workspaceDir, 'packages'), { recursive: true });
fs.writeFileSync(
path.join(workspaceDir, 'pgpm.json'),
JSON.stringify({ packages: ['packages/*'] }, null, 2)
);

for (const module of modules) {
const moduleDir = path.join(workspaceDir, 'packages', module.name);
fs.mkdirSync(moduleDir, { recursive: true });
fs.writeFileSync(
path.join(moduleDir, 'pgpm.plan'),
'%syntax-version=1.0.0\n'
);
fs.writeFileSync(
path.join(moduleDir, `${module.name}.control`),
[
`# ${module.name} extension`,
`default_version = '${module.version}'`,
module.requires && module.requires.length > 0
? `requires = '${module.requires.join(',')}'`
: ''
].filter(Boolean).join('\n') + '\n'
);
}

return workspaceDir;
};

const runLs = async (argv: Record<string, unknown>) => {
const output: string[] = [];
const write = jest.spyOn(process.stdout, 'write').mockImplementation((chunk: any) => {
output.push(String(chunk));
return true;
});
try {
await fixture.runCmd(argv);
} finally {
write.mockRestore();
}
return output.join('');
};

it('prints module objects as JSON sorted by name', async () => {
const workspaceDir = createWorkspace([
{ name: 'b', version: '2.0.0', requires: ['plpgsql'] },
{ name: 'a', version: '1.0.0' }
]);

const output = await runLs({
_: ['ls'],
cwd: workspaceDir,
json: true
});

expect(output).toBe(`[
{
"name": "a",
"version": "1.0.0",
"path": "packages/a",
"requires": []
},
{
"name": "b",
"version": "2.0.0",
"path": "packages/b",
"requires": [
"plpgsql"
]
}
]
`);
});

it('prints sorted names as a single-line JSON array', async () => {
const workspaceDir = createWorkspace([
{ name: 'b', version: '2.0.0' },
{ name: 'a', version: '1.0.0' }
]);

const output = await runLs({
_: ['ls'],
cwd: workspaceDir,
names: true,
json: true
});

expect(output).toBe('["a","b"]');
});

it('prints workspace-relative paths as a single-line JSON array', async () => {
const workspaceDir = createWorkspace([
{ name: 'b', version: '2.0.0' },
{ name: 'a', version: '1.0.0' }
]);

const output = await runLs({
_: ['ls'],
cwd: workspaceDir,
paths: true,
json: true
});

expect(output).toBe('["packages/a","packages/b"]');
});

it('prints human-readable module listings', async () => {
const workspaceDir = createWorkspace([
{ name: 'b', version: '2.0.0' },
{ name: 'a', version: '1.0.0' }
]);

const output = await runLs({
_: ['ls'],
cwd: workspaceDir
});

expect(output).toContain('a');
expect(output).toContain('b');
expect(output).toContain('1.0.0');
expect(output).toContain('packages/a');
});

it('prints an empty JSON array for an empty workspace', async () => {
const workspaceDir = createWorkspace();

const output = await runLs({
_: ['ls'],
cwd: workspaceDir,
names: true,
json: true
});

expect(output).toBe('[]');
});

it('fails outside a workspace', async () => {
const exit = jest.spyOn(process, 'exit').mockImplementation((code?: string | number) => {
throw new Error(`process.exit(${code})`);
});
const error = jest.spyOn(console, 'error').mockImplementation(() => undefined);

await expect(fixture.runCmd({
_: ['ls'],
cwd: fixture.tempDir,
json: true
})).rejects.toThrow('process.exit(1)');

expect(error).toHaveBeenCalledWith(
expect.stringContaining('Not inside a pgpm workspace')
);

error.mockRestore();
exit.mockRestore();
});
});
5 changes: 5 additions & 0 deletions pgpm/cli/src/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import _import from './commands/import';
import init from './commands/init';
import install from './commands/install';
import kill from './commands/kill';
import ls from './commands/ls';
import materialize from './commands/materialize';
import migrate from './commands/migrate';
import _package from './commands/package';
Expand Down Expand Up @@ -62,6 +63,8 @@ const ENGINE_EXEMPT_COMMANDS = new Set([
'import',
'init',
'install',
'ls',
'list',
'package',
'materialize',
'plan',
Expand Down Expand Up @@ -111,6 +114,8 @@ export const createPgpmCommandMap = (skipPgTeardown: boolean = false): Record<st
tag: pgt(tag),
kill: pgt(kill),
install: pgt(install),
ls,
list: ls,
migrate: pgt(migrate),
materialize,
analyze: pgt(analyze),
Expand Down
105 changes: 105 additions & 0 deletions pgpm/cli/src/commands/ls.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import { PgpmPackage } from '@pgpmjs/core';
import { cliExitWithError, CLIOptions, Inquirerer, ParsedArgs } from 'inquirerer';
import path from 'path';

const lsUsageText = `
List Command:

pgpm ls [OPTIONS]

List the pgpm modules in the current workspace.

Options:
--help, -h Show this help message
--json Print JSON output
--names Print module names only
--paths Print workspace-relative module paths only
--cwd <dir> Working directory (default: current directory)

Examples:
pgpm ls
pgpm ls --names
pgpm ls --paths --json
`;

interface ModuleListing {
name: string;
version: string;
path: string;
requires: string[];
}

export default async (
argv: Partial<ParsedArgs>,
_prompter: Inquirerer,
_options: CLIOptions
) => {
if (argv.help || argv.h) {
console.log(lsUsageText);
process.exit(0);
}

const names = Boolean(argv.names);
const paths = Boolean(argv.paths);
const json = Boolean(argv.json);

if (names && paths) {
await cliExitWithError('--names and --paths cannot be used together.');
}

const cwd = path.resolve((argv.cwd as string) || process.cwd());
const workspace = new PgpmPackage(cwd);
const workspacePath = workspace.getWorkspacePath();
if (!workspacePath) {
await cliExitWithError(
`Not inside a pgpm workspace: ${cwd}. Pass --cwd <workspace-directory>.`
);
}

const entries: ModuleListing[] = Object.entries(workspace.getModuleMap())
.sort(([left], [right]) => left.localeCompare(right))
.map(([name, module]) => ({
name,
version: module.version || 'unknown',
path: module.path,
requires: [...module.requires]
}));

if (json) {
if (names) {
process.stdout.write(JSON.stringify(entries.map(entry => entry.name)));
return;
}
if (paths) {
process.stdout.write(JSON.stringify(entries.map(entry => entry.path)));
return;
}
process.stdout.write(`${JSON.stringify(entries, null, 2)}\n`);
return;
}

if (names) {
process.stdout.write(entries.map(entry => entry.name).join('\n'));
if (entries.length > 0) process.stdout.write('\n');
return;
}

if (paths) {
process.stdout.write(entries.map(entry => entry.path).join('\n'));
if (entries.length > 0) process.stdout.write('\n');
return;
}

if (entries.length === 0) {
process.stdout.write('No modules found.\n');
return;
}

const nameWidth = Math.max(...entries.map(entry => entry.name.length));
const versionWidth = Math.max(...entries.map(entry => entry.version.length));
process.stdout.write(entries
.map(entry =>
`${entry.name.padEnd(nameWidth)} ${entry.version.padEnd(versionWidth)} ${entry.path}`
)
.join('\n') + '\n');
};
1 change: 1 addition & 0 deletions pgpm/cli/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export { default as _export } from './commands/export';
export { default as extension } from './commands/extension';
export { default as install } from './commands/install';
export { default as kill } from './commands/kill';
export { default as ls } from './commands/ls';
export { default as migrate } from './commands/migrate';
export { default as _package } from './commands/package';
export { default as plan } from './commands/plan';
Expand Down
Loading