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
8 changes: 8 additions & 0 deletions .agents/skills/pgpm/references/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -165,8 +165,16 @@ pgpm init --template pnpm/module -w

# Use custom template repository
pgpm init --repo https://github.com/org/templates.git --template my-template

# Refresh a stale cached template repository
pgpm init --refresh
```

Non-interactive init requires every question to be answered by flags; see
[starter-kits.md](starter-kits.md)'s non-interactive flag table for
`--name --fullName --email --username --repoName --license`, plus module
`--moduleName --packageIdentifier --moduleDesc --access`.

### Change Management

**pgpm add** — Add a new database change
Expand Down
27 changes: 24 additions & 3 deletions .agents/skills/pgpm/references/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ Returns:
| `db.query(sql, params?)` | Execute SQL query |
| `db.beforeEach()` | Start savepoint (call in beforeEach) |
| `db.afterEach()` | Rollback to savepoint (call in afterEach) |
| `db.setContext(key, value)` | Set session context variable |
| `db.setContext(context)` | Set session context variables |
| `db.getPool()` | Get underlying pg Pool |

## Seeding Data
Expand Down Expand Up @@ -161,13 +161,34 @@ For RLS (Row Level Security) testing:

```typescript
test('user can only see own data', async () => {
await db.setContext('user_id', 'user-123');
db.setContext({ role: 'authenticated', 'jwt.claims.user_id': 'user-1' });

const result = await db.query('SELECT * FROM user_data');
// Only returns rows where user_id = 'user-123'
// Only returns rows where user_id = 'user-1'
});
```

### RLS testing

`db` from `getConnections()` connects as `app_user`, so grant access to the
schema and table before testing whether RLS policies allow a row:

```sql
GRANT USAGE ON SCHEMA app_public TO authenticated;
GRANT SELECT, INSERT ON app_public.posts TO authenticated;
GRANT USAGE ON SEQUENCE app_public.posts_id_seq TO authenticated;
```

Then set the role and claims on the same client before querying:

```typescript
db.setContext({ role: 'authenticated', 'jwt.claims.user_id': 'user-1' });
```

The `pg` and `db` clients are separate connections with separate savepoints.
A row inserted through `db` is invisible to `pg` in the same test; perform
superuser-visibility assertions through the same client that inserted the row.

### Multiple Connections

```typescript
Expand Down
31 changes: 31 additions & 0 deletions pgpm/cli/__tests__/init.boilerplate.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import os from 'os';
import path from 'path';

import {
isScaffoldableInPlace,
persistBoilerplateSource,
readBoilerplateSource,
resolveInitTemplateRepo,
Expand Down Expand Up @@ -97,3 +98,33 @@ describe('persist/read boilerplate source', () => {
expect(readBoilerplateSource(dir)).toBeUndefined();
});
});

describe('isScaffoldableInPlace', () => {
let dir: string;

beforeEach(() => {
dir = fs.mkdtempSync(path.join(os.tmpdir(), 'pgpm-in-place-'));
});

afterEach(() => {
fs.rmSync(dir, { recursive: true, force: true });
});

it('accepts an empty directory', () => {
expect(isScaffoldableInPlace(dir)).toBe(true);
});

it('accepts a directory containing only .git', () => {
fs.mkdirSync(path.join(dir, '.git'));
expect(isScaffoldableInPlace(dir)).toBe(true);
});

it('rejects a directory containing README.md', () => {
fs.writeFileSync(path.join(dir, 'README.md'), '# project\n');
expect(isScaffoldableInPlace(dir)).toBe(false);
});

it('rejects a missing directory', () => {
expect(isScaffoldableInPlace(path.join(dir, 'missing'))).toBe(false);
});
});
29 changes: 28 additions & 1 deletion pgpm/cli/__tests__/init.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ process.env.PGPM_SKIP_UPDATE_CHECK = 'true';
process.env.PGPM_SKIP_SKILL_INSTALL = 'true';

import { PgpmPackage, TEMPLATE_REPOS } from '@pgpmjs/core';
import { existsSync, readFileSync } from 'fs';
import { existsSync, mkdirSync, readFileSync } from 'fs';
import { sync as glob } from 'glob';
import { Inquirerer, ParsedArgs } from 'inquirerer';
import * as path from 'path';
Expand Down Expand Up @@ -93,6 +93,33 @@ describe('cmds:init', () => {
);
});

it('scaffolds a workspace in place when cwd is an empty named directory', async () => {
const workspaceDir = path.join(fixture.tempDir, 'foo');
mkdirSync(workspaceDir);
const { mockInput, mockOutput } = environment;
const prompter = new Inquirerer({
input: mockInput,
output: mockOutput,
noTty: true
});

await commands(withInitDefaults({
_: ['init', 'workspace'],
cwd: workspaceDir,
name: 'foo',
workspace: true
}), prompter, {
noTty: true,
input: mockInput,
output: mockOutput,
version: '1.0.0',
minimistOpts: {}
});

expect(existsSync(path.join(workspaceDir, 'pgpm.json'))).toBe(true);
expect(existsSync(path.join(workspaceDir, 'foo'))).toBe(false);
});

it('initializes module', async () => {
const workspaceDir = path.join(fixture.tempDir, 'my-workspace');
const moduleDir = path.join(workspaceDir, 'packages', 'my-module');
Expand Down
18 changes: 18 additions & 0 deletions pgpm/cli/__tests__/tty.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { detectNoTtyFromProcess, isNoTtyRequested } from '../src/utils/tty';

describe('tty detection', () => {
const originalIsTTY = process.stdin.isTTY;

afterEach(() => {
Object.defineProperty(process.stdin, 'isTTY', {
configurable: true,
value: originalIsTTY,
});
});

it('treats a non-terminal stdin as non-interactive', () => {
Object.defineProperty(process.stdin, 'isTTY', { configurable: true, value: false });
expect(isNoTtyRequested({})).toBe(true);
expect(detectNoTtyFromProcess(['node', 'pgpm'])).toBe(true);
});
});
9 changes: 9 additions & 0 deletions pgpm/cli/src/commands/init/boilerplate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,15 @@ export interface BoilerplateSource {
dir?: string;
}

export function isScaffoldableInPlace(dir: string): boolean {
try {
return fs.statSync(dir).isDirectory() &&
fs.readdirSync(dir).filter((entry) => entry !== '.git').length === 0;
} catch {
return false;
}
}

/**
* Resolve the template repo for an `init` invocation from its flags.
*
Expand Down
Loading
Loading