diff --git a/README.md b/README.md
index c71686c..2a8ddd9 100644
--- a/README.md
+++ b/README.md
@@ -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.
+
Command reference
@@ -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
@@ -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:
@@ -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
@@ -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 |
-
---
diff --git a/src/commands/groups/batch-create.ts b/src/commands/groups/batch-create.ts
deleted file mode 100644
index 524cec3..0000000
--- a/src/commands/groups/batch-create.ts
+++ /dev/null
@@ -1,47 +0,0 @@
-import { getBkperInstance } from '../../bkper-factory.js';
-import { Group } from 'bkper-js';
-import { parsePropertyFlag } from '../../utils/properties.js';
-
-/**
- * Creates multiple groups from stdin items using the batch API.
- * Outputs a flat JSON array of all created groups.
- *
- * Stdin items must follow the bkper.Group format exactly.
- *
- * @param bookId - Target book ID
- * @param items - Parsed stdin items as bkper.Group payloads
- * @param propertyOverrides - CLI --property flags that override stdin properties
- */
-export async function batchCreateGroups(
- bookId: string,
- items: Record[],
- propertyOverrides?: string[]
-): Promise {
- const bkper = getBkperInstance();
- const book = await bkper.getBook(bookId);
-
- const groups: Group[] = [];
-
- for (const item of items) {
- const group = new Group(book, item as bkper.Group);
-
- // CLI --property flags override stdin properties
- if (propertyOverrides) {
- for (const raw of propertyOverrides) {
- const [key, value] = parsePropertyFlag(raw);
- if (value === '') {
- group.deleteProperty(key);
- } else {
- group.setProperty(key, value);
- }
- }
- }
-
- groups.push(group);
- }
-
- const results = await book.batchCreateGroups(groups);
- const allResults = results.map(result => result.json());
-
- console.log(JSON.stringify(allResults, null, 2));
-}
diff --git a/src/commands/groups/index.ts b/src/commands/groups/index.ts
index ca9a1eb..3ba5c68 100644
--- a/src/commands/groups/index.ts
+++ b/src/commands/groups/index.ts
@@ -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';
diff --git a/src/commands/groups/register.ts b/src/commands/groups/register.ts
index 5b50837..20da87b 100644
--- a/src/commands/groups/register.ts
+++ b/src/commands/groups/register.ts
@@ -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');
@@ -50,30 +42,19 @@ export function registerGroupCommands(program: Command): void {
.option('-p, --property ', '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);
})()
);
diff --git a/src/docs-compliance/rules.ts b/src/docs-compliance/rules.ts
index 574455b..5db6d5f 100644
--- a/src/docs-compliance/rules.ts
+++ b/src/docs-compliance/rules.ts
@@ -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',
@@ -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};
}
diff --git a/test/integration/groups/group-stdin.test.ts b/test/integration/groups/group-stdin.test.ts
index 53d2b1e..25e1534 100644
--- a/test/integration/groups/group-stdin.test.ts
+++ b/test/integration/groups/group-stdin.test.ts
@@ -32,7 +32,7 @@ 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' },
@@ -40,33 +40,19 @@ describe('CLI - group stdin', function () {
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(['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');
});
});
});
diff --git a/test/integration/helpers/api-helpers.ts b/test/integration/helpers/api-helpers.ts
index c6c40f3..ac78f36 100644
--- a/test/integration/helpers/api-helpers.ts
+++ b/test/integration/helpers/api-helpers.ts
@@ -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();
diff --git a/test/integration/pipelines/pipeline.test.ts b/test/integration/pipelines/pipeline.test.ts
index 4df0abd..d16069b 100644
--- a/test/integration/pipelines/pipeline.test.ts
+++ b/test/integration/pipelines/pipeline.test.ts
@@ -35,101 +35,37 @@ describe('CLI - pipeline (end-to-end)', function () {
});
// ----------------------------------------------------------------
- // Groups: round-trip piping
+ // Groups: stdin is not supported for creation
// ----------------------------------------------------------------
- describe('groups round-trip', function () {
- it('should pipe group list output as group create input', async function () {
- // Seed groups in book A
- const seedResult = await runBkperWithStdin(
- ['group', 'create', '-b', bookA],
- JSON.stringify([{ name: 'Pipe Group Alpha' }, { name: 'Pipe Group Beta' }])
- );
- expect(seedResult.exitCode).to.equal(0);
+ describe('groups creation', function () {
+ it('should reject piping group JSON into group create', async function () {
+ await runBkper([
+ 'group',
+ 'create',
+ '-b',
+ bookA,
+ '--name',
+ 'Pipe Group Alpha',
+ ]);
+ await runBkper([
+ 'group',
+ 'create',
+ '-b',
+ bookA,
+ '--name',
+ 'Pipe Group Beta',
+ ]);
- // List groups from book A as JSON
const listResult = await runBkper(['--format', 'json', 'group', 'list', '-b', bookA]);
expect(listResult.exitCode).to.equal(0);
- const listedGroups = JSON.parse(listResult.stdout);
- expect(listedGroups).to.be.an('array');
- expect(listedGroups.length).to.be.greaterThanOrEqual(2);
- // Pipe list output into book B create
const pipeResult = await runBkperWithStdin(
['group', 'create', '-b', bookB],
listResult.stdout
);
- expect(pipeResult.exitCode).to.equal(0);
-
- // Verify groups exist in book B
- const verifyResult = await runBkperJson(['group', 'list', '-b', bookB]);
- const names = verifyResult.map(g => g.name);
- expect(names).to.include('Pipe Group Alpha');
- expect(names).to.include('Pipe Group Beta');
- });
-
- it('should pipe group batch create output as group create input', async function () {
- // Create groups in book A via batch
- const createResult = await runBkperWithStdin(
- ['group', 'create', '-b', bookA],
- JSON.stringify([{ name: 'Batch Pipe Group X' }])
- );
- expect(createResult.exitCode).to.equal(0);
-
- // Parse batch output (flat JSON array) and pipe to book B
- const parsed = JSON.parse(createResult.stdout);
- expect(parsed).to.be.an('array').with.length(1);
-
- const pipeResult = await runBkperWithStdin(
- ['group', 'create', '-b', bookB],
- createResult.stdout
- );
- expect(pipeResult.exitCode).to.equal(0);
-
- // Verify
- const verifyResult = await runBkperJson(['group', 'list', '-b', bookB]);
- const names = verifyResult.map(g => g.name);
- expect(names).to.include('Batch Pipe Group X');
- });
-
- it('should pipe single group get output as group create input', async function () {
- // Get a group from book A
- const groups = await runBkperJson(['group', 'list', '-b', bookA]);
- const targetGroup = groups.find(g => g.name === 'Pipe Group Alpha');
- expect(targetGroup).to.exist;
-
- const getResult = await runBkper([
- '--format',
- 'json',
- 'group',
- 'get',
- targetGroup!.id!,
- '-b',
- bookA,
- ]);
- expect(getResult.exitCode).to.equal(0);
-
- // Pipe single group JSON into create (single object -> one-item batch)
- // Create in a fresh book to avoid name collision
- const bookC = await createTestBook(uniqueTestName('test-pipe-grp-get'));
- try {
- const pipeResult = await runBkperWithStdin(
- ['group', 'create', '-b', bookC],
- getResult.stdout
- );
- expect(pipeResult.exitCode).to.equal(0);
-
- const verifyResult = await runBkperJson([
- 'group',
- 'list',
- '-b',
- bookC,
- ]);
- const names = verifyResult.map(g => g.name);
- expect(names).to.include('Pipe Group Alpha');
- } finally {
- await deleteTestBook(bookC);
- }
+ expect(pipeResult.exitCode).to.not.equal(0);
+ expect(pipeResult.stderr).to.include('Missing required option: --name');
});
});
@@ -493,7 +429,7 @@ describe('CLI - pipeline (end-to-end)', function () {
// ----------------------------------------------------------------
describe('stdin { items: [...] } wrapper', function () {
- it('should accept { items: [...] } wrapper for group create', async function () {
+ it('should reject { items: [...] } wrapper for group create', async function () {
const bookC = await createTestBook(uniqueTestName('test-pipe-wrap-grp'));
try {
const wrappedInput = JSON.stringify({
@@ -504,16 +440,8 @@ describe('CLI - pipeline (end-to-end)', function () {
['group', 'create', '-b', bookC],
wrappedInput
);
- expect(result.exitCode).to.equal(0);
-
- const verifyResult = await runBkperJson([
- 'group',
- 'list',
- '-b',
- bookC,
- ]);
- const names = verifyResult.map(g => g.name);
- expect(names).to.include('Wrapped Group');
+ expect(result.exitCode).to.not.equal(0);
+ expect(result.stderr).to.include('Missing required option: --name');
} finally {
await deleteTestBook(bookC);
}
@@ -599,11 +527,10 @@ describe('CLI - pipeline (end-to-end)', function () {
expect(parsed).to.be.an('array').with.length(0);
});
- it('should handle empty array stdin gracefully for groups', async function () {
+ it('should reject empty array stdin for groups', async function () {
const result = await runBkperWithStdin(['group', 'create', '-b', bookA], '[]');
- expect(result.exitCode).to.equal(0);
- const parsed = JSON.parse(result.stdout);
- expect(parsed).to.be.an('array').with.length(0);
+ expect(result.exitCode).to.not.equal(0);
+ expect(result.stderr).to.include('Missing required option: --name');
});
it('should handle empty array stdin gracefully for transactions', async function () {
@@ -613,14 +540,13 @@ describe('CLI - pipeline (end-to-end)', function () {
expect(parsed).to.be.an('array').with.length(0);
});
- it('should handle { items: [] } wrapper with empty items', async function () {
+ it('should reject { items: [] } wrapper for groups', async function () {
const result = await runBkperWithStdin(
['group', 'create', '-b', bookA],
JSON.stringify({ items: [] })
);
- expect(result.exitCode).to.equal(0);
- const parsed = JSON.parse(result.stdout);
- expect(parsed).to.be.an('array').with.length(0);
+ expect(result.exitCode).to.not.equal(0);
+ expect(result.stderr).to.include('Missing required option: --name');
});
});
@@ -629,36 +555,50 @@ describe('CLI - pipeline (end-to-end)', function () {
// ----------------------------------------------------------------
describe('cross-resource workflow', function () {
- it('should pipe groups, accounts, and transactions across books', async function () {
+ it('should copy groups explicitly, then pipe accounts and transactions across books', async function () {
const sourceBook = await createTestBook(uniqueTestName('test-pipe-src'));
const destBook = await createTestBook(uniqueTestName('test-pipe-dest'));
try {
- // Step 1: Create groups in source
- const grpCreate = await runBkperWithStdin(
- ['group', 'create', '-b', sourceBook],
- JSON.stringify([{ name: 'XFlow Assets' }, { name: 'XFlow Income' }])
- );
- expect(grpCreate.exitCode).to.equal(0);
-
- // Step 2: Pipe groups from source to dest
- const grpList = await runBkper([
- '--format',
- 'json',
+ // Step 1: Create matching groups explicitly in both books
+ const sourceAssets = await runBkper([
'group',
- 'list',
+ 'create',
'-b',
sourceBook,
+ '--name',
+ 'XFlow Assets',
]);
- expect(grpList.exitCode).to.equal(0);
-
- const grpPipe = await runBkperWithStdin(
- ['group', 'create', '-b', destBook],
- grpList.stdout
- );
- expect(grpPipe.exitCode).to.equal(0);
+ expect(sourceAssets.exitCode).to.equal(0);
+ const sourceIncome = await runBkper([
+ 'group',
+ 'create',
+ '-b',
+ sourceBook,
+ '--name',
+ 'XFlow Income',
+ ]);
+ expect(sourceIncome.exitCode).to.equal(0);
+ const destAssets = await runBkper([
+ 'group',
+ 'create',
+ '-b',
+ destBook,
+ '--name',
+ 'XFlow Assets',
+ ]);
+ expect(destAssets.exitCode).to.equal(0);
+ const destIncome = await runBkper([
+ 'group',
+ 'create',
+ '-b',
+ destBook,
+ '--name',
+ 'XFlow Income',
+ ]);
+ expect(destIncome.exitCode).to.equal(0);
- // Step 3: Create accounts in source
+ // Step 2: Create accounts in source
const acctCreate = await runBkperWithStdin(
['account', 'create', '-b', sourceBook],
JSON.stringify([
@@ -668,7 +608,7 @@ describe('CLI - pipeline (end-to-end)', function () {
);
expect(acctCreate.exitCode).to.equal(0);
- // Step 4: Pipe accounts from source to dest
+ // Step 3: Pipe accounts from source to dest
const acctList = await runBkper([
'--format',
'json',
@@ -685,7 +625,7 @@ describe('CLI - pipeline (end-to-end)', function () {
);
expect(acctPipe.exitCode).to.equal(0);
- // Step 5: Create transactions in source
+ // Step 4: Create transactions in source
const txCreate = await runBkperWithStdin(
['transaction', 'create', '-b', sourceBook],
JSON.stringify([
@@ -707,7 +647,7 @@ describe('CLI - pipeline (end-to-end)', function () {
);
expect(txCreate.exitCode).to.equal(0);
- // Step 6: Pipe transactions from source to dest
+ // Step 5: Pipe transactions from source to dest
const txList = await runBkper([
'--format',
'json',
@@ -726,7 +666,7 @@ describe('CLI - pipeline (end-to-end)', function () {
);
expect(txPipe.exitCode).to.equal(0);
- // Step 7: Verify everything in dest
+ // Step 6: Verify everything in dest
const destGroups = await runBkperJson([
'group',
'list',
@@ -819,16 +759,15 @@ describe('CLI - pipeline (end-to-end)', function () {
}
});
- it('should output batch group create as a flat JSON array', async function () {
+ it('should reject batch group create via stdin', async function () {
const bookC = await createTestBook(uniqueTestName('test-fmt-grp'));
try {
const result = await runBkperWithStdin(
['group', 'create', '-b', bookC],
JSON.stringify([{ name: 'Fmt Group' }])
);
- expect(result.exitCode).to.equal(0);
- const parsed = JSON.parse(result.stdout);
- expect(parsed).to.be.an('array');
+ expect(result.exitCode).to.not.equal(0);
+ expect(result.stderr).to.include('Missing required option: --name');
} finally {
await deleteTestBook(bookC);
}
diff --git a/test/integration/transactions/transaction-commands.test.ts b/test/integration/transactions/transaction-commands.test.ts
index 4ff4748..bcb0168 100644
--- a/test/integration/transactions/transaction-commands.test.ts
+++ b/test/integration/transactions/transaction-commands.test.ts
@@ -471,30 +471,48 @@ describe('CLI - transaction commands', function () {
expect(result.exitCode).to.not.equal(0);
});
- it('should fail when missing required --date option', async function () {
+ it('should create a draft transaction when --date is missing', async function () {
const result = await runBkper([
+ '--format',
+ 'json',
'transaction',
'create',
'-b',
bookId,
'--amount',
'100',
+ '--description',
+ 'Draft without date',
]);
- expect(result.exitCode).to.not.equal(0);
+ expect(result.exitCode).to.equal(0);
+ const created = JSON.parse(result.stdout) as bkper.Transaction;
+ expect(created).to.be.an('object');
+ expect(created.id).to.be.a('string');
+ expect(created.description).to.equal('Draft without date');
+ expect(created.posted).to.not.equal(true);
});
- it('should fail when missing required --amount option', async function () {
+ it('should create a draft transaction when --amount is missing', async function () {
const result = await runBkper([
+ '--format',
+ 'json',
'transaction',
'create',
'-b',
bookId,
'--date',
'2025-01-01',
+ '--description',
+ 'Draft without amount',
]);
- expect(result.exitCode).to.not.equal(0);
+ expect(result.exitCode).to.equal(0);
+ const created = JSON.parse(result.stdout) as bkper.Transaction;
+ expect(created).to.be.an('object');
+ expect(created.id).to.be.a('string');
+ expect(created.description).to.equal('Draft without amount');
+ expect(created.posted).to.not.equal(true);
});
it('should fail when missing required --query option for list', async function () {
diff --git a/test/unit/commands/groups/batch-create.test.ts b/test/unit/commands/groups/batch-create.test.ts
deleted file mode 100644
index 8b4d962..0000000
--- a/test/unit/commands/groups/batch-create.test.ts
+++ /dev/null
@@ -1,103 +0,0 @@
-import { expect, setupTestEnvironment } from '../../helpers/test-setup.js';
-import { setMockBkper } from '../../helpers/mock-factory.js';
-
-// Import after mock setup
-const { batchCreateGroups } = await import('../../../../src/commands/groups/batch-create.js');
-
-describe('CLI - group batch-create Command', function () {
- let mockBook: any;
- let batchCalls: any[][];
- let consoleOutput: string[];
-
- beforeEach(function () {
- setupTestEnvironment();
- batchCalls = [];
- consoleOutput = [];
-
- const originalLog = console.log;
- console.log = (...args: any[]) => {
- consoleOutput.push(args.join(' '));
- };
-
- mockBook = {
- getGroup: async (name: string) => ({
- getId: () => `${name}-id`,
- getName: () => name,
- }),
- batchCreateGroups: async (groups: any[]) => {
- batchCalls.push(groups);
- return groups.map((g: any, idx: number) => ({
- json: () => ({ id: `grp-${idx}`, name: g.getName() }),
- }));
- },
- };
-
- setMockBkper({
- setConfig: () => {},
- getBook: async () => mockBook,
- });
-
- afterEach(() => {
- console.log = originalLog;
- });
- });
-
- it('should create a single group', async function () {
- await batchCreateGroups('book-123', [{ name: 'Assets' }]);
-
- expect(batchCalls).to.have.length(1);
- expect(batchCalls[0]).to.have.length(1);
- expect(batchCalls[0][0].getName()).to.equal('Assets');
- });
-
- it('should output flat JSON array for created groups', async function () {
- await batchCreateGroups('book-123', [{ name: 'Assets' }, { name: 'Liabilities' }]);
-
- expect(consoleOutput).to.have.length(1);
- const parsed = JSON.parse(consoleOutput[0]);
- expect(parsed).to.be.an('array').with.length(2);
- expect(parsed[0]).to.have.property('name');
- expect(parsed[1]).to.have.property('name');
- });
-
- it('should send all items in a single batch call', async function () {
- const items = Array.from({ length: 150 }, (_, i) => ({ name: `Group-${i}` }));
- await batchCreateGroups('book-123', items);
-
- expect(batchCalls).to.have.length(1);
- expect(batchCalls[0]).to.have.length(150);
- });
-
- it('should set properties from stdin payload', async function () {
- await batchCreateGroups('book-123', [{ name: 'Test', properties: { color: 'red' } }]);
-
- expect(batchCalls).to.have.length(1);
- const group = batchCalls[0][0];
- expect(group.getProperty('color')).to.equal('red');
- });
-
- it('should apply property overrides from CLI flags', async function () {
- await batchCreateGroups('book-123', [{ name: 'Test' }], ['region=EU']);
-
- expect(batchCalls).to.have.length(1);
- const group = batchCalls[0][0];
- expect(group.getProperty('region')).to.equal('EU');
- });
-
- it('should set hidden status when provided as boolean', async function () {
- await batchCreateGroups('book-123', [{ name: 'Internal', hidden: true }]);
-
- expect(batchCalls).to.have.length(1);
- const group = batchCalls[0][0];
- expect(group.isHidden()).to.be.true;
- });
-
- it('should reject hidden as string (must be boolean)', async function () {
- await batchCreateGroups('book-123', [{ name: 'Internal', hidden: 'true' as any }]);
-
- expect(batchCalls).to.have.length(1);
- const group = batchCalls[0][0];
- // String 'true' is truthy but not strictly boolean true
- expect(group.isHidden()).to.equal('true');
- });
-});
diff --git a/test/unit/docs-compliance/rules.test.ts b/test/unit/docs-compliance/rules.test.ts
index 1cf6bdf..8334e01 100644
--- a/test/unit/docs-compliance/rules.test.ts
+++ b/test/unit/docs-compliance/rules.test.ts
@@ -5,6 +5,10 @@ import { evaluateReadmeCompliance } from '../../../src/docs-compliance/rules.js'
describe('docs-compliance rules', function () {
it('should pass when README content follows required rules', function () {
const readme = `
+### Book setup guidance (important)
+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.
+
### Query semantics (transactions and balances)
- \`on:2025\` → full year
- \`after:\` is **inclusive** and \`before:\` is **exclusive**.
@@ -69,10 +73,41 @@ bkper balance list -b abc123 -q "on:2025-12-31"
const result = evaluateReadmeCompliance(readme);
const codes = result.errors.map(e => e.code);
+ expect(codes).to.include('missing-book-setup-guidance-title');
+ expect(codes).to.include('missing-book-setup-order-guidance');
+ expect(codes).to.include('missing-book-setup-verification-guidance');
expect(codes).to.include('missing-llm-guidance-title');
expect(codes).to.include('missing-csv-guidance');
expect(codes).to.include('missing-json-guidance');
expect(codes).to.include('missing-query-semantics-section');
expect(codes).to.include('missing-after-before-semantics');
});
+
+ it('should report when README documents group stdin batch creation', function () {
+ const readme = `
+### Book setup guidance (important)
+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.
+
+### Query semantics (transactions and balances)
+- \`after:\` is **inclusive** and \`before:\` is **exclusive**.
+
+**LLM-first output guidance (important):**
+- **LLM consumption of lists/reports** → CSV
+- **Programmatic processing / pipelines** → JSON
+
+Write commands (\`account create\`, \`group create\`, \`transaction create\`) accept JSON data piped via stdin.
+\`\`\`bash
+bkper group list -b $BOOK_A --format json | bkper group create -b $BOOK_B
+\`\`\`
+
+**Group** (\`bkper.Group\`)
+`;
+
+ const result = evaluateReadmeCompliance(readme);
+ const codes = result.errors.map(e => e.code);
+ expect(codes).to.include('group-create-stdin-documented');
+ expect(codes).to.include('group-create-pipe-documented');
+ expect(codes).to.include('group-stdin-fields-documented');
+ });
});