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
37 changes: 16 additions & 21 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,16 @@ bkper group update "Cash" -b abc123 --hidden true
bkper group delete "Cash" -b abc123
```

### Book setup guidance (important)

When setting up a full Book or starter chart of accounts, prefer a hierarchical group structure by default.

Create top-level groups first, then child groups with `--parent`, then accounts with `--groups`.

Verify the resulting group hierarchy and account memberships before reporting success.

Avoid using the same name for a Group and an Account in the same Book.

<details>
<summary>Command reference</summary>

Expand Down Expand Up @@ -448,7 +458,7 @@ CSV is significantly more token-efficient than JSON for tabular data, and for wi

### Batch Operations & Piping

Write commands (`account create`, `group create`, `transaction create`) accept JSON data piped via stdin for batch operations. The `transaction update` command also accepts stdin for batch updates. The input format follows the [Bkper API Types](https://raw.githubusercontent.com/bkper/bkper-api-types/refs/heads/master/index.d.ts) exactly -- a single JSON object or an array of objects.
Write commands (`account create`, `transaction create`) accept JSON data piped via stdin for batch operations. The `transaction update` command also accepts stdin for batch updates. The input format follows the [Bkper API Types](https://raw.githubusercontent.com/bkper/bkper-api-types/refs/heads/master/index.d.ts) exactly -- a single JSON object or an array of objects.

```bash
# Create transactions
Expand All @@ -465,15 +475,13 @@ echo '[{
echo '[{"name":"Cash","type":"ASSET"},{"name":"Revenue","type":"INCOMING"}]' | \
bkper account create -b abc123

# Create groups
echo '[{"name":"Fixed Costs","hidden":true}]' | \
bkper group create -b abc123

# Pipe from a script
python export_bank.py | bkper transaction create -b abc123
```

The input follows the exact `bkper.Transaction`, `bkper.Account`, or `bkper.Group` type from the [Bkper API Types](https://raw.githubusercontent.com/bkper/bkper-api-types/refs/heads/master/index.d.ts). Custom properties go inside the `properties` object.
The input follows the exact `bkper.Transaction` or `bkper.Account` type from the [Bkper API Types](https://raw.githubusercontent.com/bkper/bkper-api-types/refs/heads/master/index.d.ts). Custom properties go inside the `properties` object.

Groups are created explicitly with `bkper group create --name` and optional `--parent` so hierarchy stays deterministic during setup.

The `--property` CLI flag can override or delete properties from the stdin payload:

Expand All @@ -491,21 +499,17 @@ bkper account create -b abc123 < accounts.json

**Piping between commands:**

All JSON output is designed to be piped directly as stdin to other commands. The output of any list or batch create command can feed directly into a create or update command:
For resources that support stdin creation, JSON output can be piped directly into create or update commands:

```bash
# Copy all accounts from one book to another
bkper account list -b $BOOK_A --format json | bkper account create -b $BOOK_B

# Copy all groups from one book to another
bkper group list -b $BOOK_A --format json | bkper group create -b $BOOK_B

# Copy transactions matching a query
bkper transaction list -b $BOOK_A -q "after:2025-01-01" --format json | \
bkper transaction create -b $BOOK_B

# Clone a full chart of accounts: groups, then accounts, then transactions
bkper group list -b $SOURCE --format json | bkper group create -b $DEST
# Clone accounts, then transactions
bkper account list -b $SOURCE --format json | bkper account create -b $DEST
bkper transaction list -b $SOURCE -q "after:2025-01-01" --format json | \
bkper transaction create -b $DEST
Expand Down Expand Up @@ -555,15 +559,6 @@ Only the fields below are meaningful when creating or updating resources via std
| `groups` | `[{"name":"..."}, ...]` | Groups to assign by name or id |
| `properties` | `{"key": "value", ...}` | Custom key/value properties |

**Group** (`bkper.Group`)

| Field | Type | Notes |
| ------------ | ---------------------------------- | -------------------------------- |
| `name` | `string` | Group name (required) |
| `hidden` | `boolean` | Hide from transactions main menu |
| `parent` | `{"name":"..."}` or `{"id":"..."}` | Parent group for nesting |
| `properties` | `{"key": "value", ...}` | Custom key/value properties |

</details>

---
Expand Down
47 changes: 0 additions & 47 deletions src/commands/groups/batch-create.ts

This file was deleted.

1 change: 0 additions & 1 deletion src/commands/groups/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,3 @@ export { getGroup } from './get.js';
export { createGroup, CreateGroupOptions } from './create.js';
export { updateGroup, UpdateGroupOptions } from './update.js';
export { deleteGroup } from './delete.js';
export { batchCreateGroups } from './batch-create.js';
47 changes: 14 additions & 33 deletions src/commands/groups/register.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,7 @@ import { withAction } from '../action.js';
import { collectProperty } from '../cli-helpers.js';
import { renderListResult, renderItem } from '../../render/index.js';
import { validateRequiredOptions, throwIfErrors } from '../../utils/validation.js';
import { parseStdinItems } from '../../input/index.js';
import {
listGroupsFormatted,
getGroup,
createGroup,
updateGroup,
deleteGroup,
batchCreateGroups,
} from './index.js';
import { listGroupsFormatted, getGroup, createGroup, updateGroup, deleteGroup } from './index.js';

export function registerGroupCommands(program: Command): void {
const groupCommand = program.command('group').description('Manage Groups');
Expand Down Expand Up @@ -50,30 +42,19 @@ export function registerGroupCommands(program: Command): void {
.option('-p, --property <key=value>', 'Set a property (repeatable)', collectProperty)
.action(options =>
withAction('creating group', async format => {
const stdinData = !process.stdin.isTTY ? await parseStdinItems() : null;

if (stdinData && stdinData.items.length > 0) {
throwIfErrors(
validateRequiredOptions(options, [{ name: 'book', flag: '--book' }])
);
await batchCreateGroups(options.book, stdinData.items, options.property);
} else if (stdinData && stdinData.items.length === 0) {
console.log(JSON.stringify([], null, 2));
} else {
throwIfErrors(
validateRequiredOptions(options, [
{ name: 'book', flag: '--book' },
{ name: 'name', flag: '--name' },
])
);
const group = await createGroup(options.book, {
name: options.name,
parent: options.parent,
hidden: options.hidden,
property: options.property,
});
renderItem(group.json(), format);
}
throwIfErrors(
validateRequiredOptions(options, [
{ name: 'book', flag: '--book' },
{ name: 'name', flag: '--name' },
])
);
const group = await createGroup(options.book, {
name: options.name,
parent: options.parent,
hidden: options.hidden,
property: options.property,
});
renderItem(group.json(), format);
})()
);

Expand Down
56 changes: 56 additions & 0 deletions src/docs-compliance/rules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,36 @@ export function evaluateReadmeCompliance(content: string): ComplianceResult {
});
}

if (!content.includes('### Book setup guidance (important)')) {
errors.push({
code: 'missing-book-setup-guidance-title',
message: 'Missing `Book setup guidance (important)` section.',
});
}

if (
!content.includes(
'Create top-level groups first, then child groups with `--parent`, then accounts with `--groups`.'
)
) {
errors.push({
code: 'missing-book-setup-order-guidance',
message:
'Missing guidance to create top-level groups first, then child groups, then accounts.',
});
}

if (
!content.includes(
'Verify the resulting group hierarchy and account memberships before reporting success.'
)
) {
errors.push({
code: 'missing-book-setup-verification-guidance',
message: 'Missing guidance to verify hierarchy and account memberships before success.',
});
}

if (!content.includes('LLM-first output guidance (important):')) {
errors.push({
code: 'missing-llm-guidance-title',
Expand Down Expand Up @@ -111,5 +141,31 @@ export function evaluateReadmeCompliance(content: string): ComplianceResult {
});
}

if (
content.includes(
'Write commands (`account create`, `group create`, `transaction create`) accept JSON data piped via stdin'
)
) {
errors.push({
code: 'group-create-stdin-documented',
message: 'README should not document stdin batch creation for `group create`.',
});
}

const groupPipePattern = /bkper\s+group\s+list\b.*\|\s*bkper\s+group\s+create\b/;
if (groupPipePattern.test(content)) {
errors.push({
code: 'group-create-pipe-documented',
message: 'README should not document piping group JSON into `group create`.',
});
}

if (content.includes('**Group** (`bkper.Group`)')) {
errors.push({
code: 'group-stdin-fields-documented',
message: 'README should not document stdin writable fields for `bkper.Group`.',
});
}

return {errors};
}
28 changes: 7 additions & 21 deletions test/integration/groups/group-stdin.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,41 +32,27 @@ describe('CLI - group stdin', function () {
});

describe('JSON stdin', function () {
it('should create groups from JSON array', async function () {
it('should reject group creation from JSON stdin', async function () {
const jsonInput = JSON.stringify([
{ name: 'Stdin Group A' },
{ name: 'Stdin Group B' },
]);

const result = await runBkperWithStdin(['group', 'create', '-b', bookId], jsonInput);

expect(result.exitCode).to.equal(0);
const parsed = JSON.parse(result.stdout);
expect(parsed).to.be.an('array').with.length(2);
expect(parsed[0].name).to.equal('Stdin Group A');
expect(parsed[1].name).to.equal('Stdin Group B');
});

it('should create a single group from JSON object', async function () {
const jsonInput = JSON.stringify({ name: 'Stdin Group C' });

const result = await runBkperWithStdin(['group', 'create', '-b', bookId], jsonInput);

expect(result.exitCode).to.equal(0);
const parsed = JSON.parse(result.stdout);
expect(parsed).to.be.an('array').with.length(1);
expect(parsed[0].name).to.equal('Stdin Group C');
expect(result.exitCode).to.not.equal(0);
expect(result.stderr).to.include('Missing required option: --name');
});
});

describe('verification', function () {
it('should list all created groups', async function () {
it('should not create groups from stdin input', async function () {
const result = await runBkperJson<bkper.Group[]>(['group', 'list', '-b', bookId]);

const names = result.map(g => g.name);
expect(names).to.include('Stdin Group A');
expect(names).to.include('Stdin Group B');
expect(names).to.include('Stdin Group C');
expect(names).to.not.include('Stdin Group A');
expect(names).to.not.include('Stdin Group B');
expect(names).to.not.include('Stdin Group C');
});
});
});
2 changes: 1 addition & 1 deletion test/integration/helpers/api-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ export function runBkper(
);

// Close stdin immediately so CLI commands that check for piped input
// (account create, group create, transaction create) don't hang
// (account create and transaction create) don't hang
// waiting for data that will never come.
if (child.stdin) {
child.stdin.end();
Expand Down
Loading
Loading