create-kvitton 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/create.js +238 -0
- package/dist/index.js +1283 -0
- package/dist/lib/env.js +8 -0
- package/dist/lib/git.js +32 -0
- package/dist/sync/bokio-sync.js +142 -0
- package/dist/templates/AGENTS.md +95 -0
- package/package.json +36 -0
package/dist/lib/env.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import * as fs from "node:fs/promises";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
export async function createEnvFile(repoPath, variables) {
|
|
4
|
+
const lines = Object.entries(variables)
|
|
5
|
+
.map(([key, value]) => `${key}=${value}`)
|
|
6
|
+
.join("\n");
|
|
7
|
+
await fs.writeFile(path.join(repoPath, ".env"), `${lines}\n`);
|
|
8
|
+
}
|
package/dist/lib/git.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import simpleGit from "simple-git";
|
|
2
|
+
import * as fs from "node:fs/promises";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
const GITIGNORE = `# Environment files with secrets
|
|
5
|
+
.env
|
|
6
|
+
.env.*
|
|
7
|
+
|
|
8
|
+
# CLI cache
|
|
9
|
+
.kvitton/
|
|
10
|
+
|
|
11
|
+
# OS files
|
|
12
|
+
.DS_Store
|
|
13
|
+
Thumbs.db
|
|
14
|
+
|
|
15
|
+
# Editor files
|
|
16
|
+
.vscode/
|
|
17
|
+
.idea/
|
|
18
|
+
*.swp
|
|
19
|
+
`;
|
|
20
|
+
export async function initGitRepo(repoPath) {
|
|
21
|
+
const git = simpleGit(repoPath);
|
|
22
|
+
await git.init();
|
|
23
|
+
await fs.writeFile(path.join(repoPath, ".gitignore"), GITIGNORE);
|
|
24
|
+
}
|
|
25
|
+
export async function commitAll(repoPath, message) {
|
|
26
|
+
const git = simpleGit(repoPath);
|
|
27
|
+
await git.add(".");
|
|
28
|
+
const status = await git.status();
|
|
29
|
+
if (status.files.length > 0) {
|
|
30
|
+
await git.commit(message);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { FilesystemStorageService, mapBokioEntryToJournalEntry, journalEntryPath, journalEntryDirFromPath, toYaml, fiscalYearDirName, downloadFilesForEntry, } from "sync";
|
|
2
|
+
/**
|
|
3
|
+
* Create a FileDownloader adapter for BokioClient
|
|
4
|
+
*/
|
|
5
|
+
function createBokioDownloader(client) {
|
|
6
|
+
return {
|
|
7
|
+
async getFilesForEntry(journalEntryId) {
|
|
8
|
+
// Bokio API requires query format: journalEntryId==UUID
|
|
9
|
+
const response = await client.getUploads({
|
|
10
|
+
query: `journalEntryId==${journalEntryId}`,
|
|
11
|
+
});
|
|
12
|
+
return response.data.map((upload) => ({
|
|
13
|
+
id: upload.id,
|
|
14
|
+
contentType: upload.contentType,
|
|
15
|
+
description: upload.description ?? undefined,
|
|
16
|
+
}));
|
|
17
|
+
},
|
|
18
|
+
async downloadFile(id) {
|
|
19
|
+
return client.downloadFile(id);
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
export async function syncJournalEntries(client, repoPath, options, onProgress) {
|
|
24
|
+
const storage = new FilesystemStorageService(repoPath);
|
|
25
|
+
// 1. Fetch fiscal years
|
|
26
|
+
const fiscalYearsResponse = await client.getFiscalYears();
|
|
27
|
+
const fiscalYears = fiscalYearsResponse.data;
|
|
28
|
+
// 2. Fetch total count first for progress display
|
|
29
|
+
const firstPage = await client.getJournalEntries({ page: 1, pageSize: 1 });
|
|
30
|
+
const totalEntries = firstPage.pagination.totalItems;
|
|
31
|
+
onProgress({ current: 0, total: totalEntries, message: "Starting sync..." });
|
|
32
|
+
if (totalEntries === 0) {
|
|
33
|
+
// Write fiscal years even if no entries
|
|
34
|
+
await writeFiscalYearsMetadata(storage, fiscalYears);
|
|
35
|
+
return {
|
|
36
|
+
entriesCount: 0,
|
|
37
|
+
fiscalYearsCount: fiscalYears.length,
|
|
38
|
+
entriesWithFilesDownloaded: 0,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
// 3. Fetch all entries with pagination
|
|
42
|
+
const allEntries = [];
|
|
43
|
+
let page = 1;
|
|
44
|
+
const pageSize = 100;
|
|
45
|
+
while (true) {
|
|
46
|
+
const response = await client.getJournalEntries({ page, pageSize });
|
|
47
|
+
allEntries.push(...response.data);
|
|
48
|
+
onProgress({ current: allEntries.length, total: totalEntries });
|
|
49
|
+
if (!response.pagination.hasNextPage)
|
|
50
|
+
break;
|
|
51
|
+
page++;
|
|
52
|
+
}
|
|
53
|
+
// 4. Write each entry and download files
|
|
54
|
+
let entriesWithFilesDownloaded = 0;
|
|
55
|
+
const downloader = createBokioDownloader(client);
|
|
56
|
+
for (const entry of allEntries) {
|
|
57
|
+
const fiscalYear = findFiscalYear(entry.date, fiscalYears);
|
|
58
|
+
if (!fiscalYear)
|
|
59
|
+
continue;
|
|
60
|
+
const fyYear = parseInt(fiscalYear.startDate.slice(0, 4), 10);
|
|
61
|
+
const entryDir = await writeJournalEntry(storage, fyYear, entry);
|
|
62
|
+
// Download files for this entry if enabled
|
|
63
|
+
if (options.downloadFiles !== false && entryDir) {
|
|
64
|
+
const filesDownloaded = await downloadFilesForEntry({
|
|
65
|
+
storage,
|
|
66
|
+
repoPath,
|
|
67
|
+
entryDir,
|
|
68
|
+
journalEntryId: entry.id,
|
|
69
|
+
downloader,
|
|
70
|
+
sourceIntegration: "bokio",
|
|
71
|
+
});
|
|
72
|
+
if (filesDownloaded > 0) {
|
|
73
|
+
entriesWithFilesDownloaded++;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
// 5. Write fiscal year metadata
|
|
78
|
+
await writeFiscalYearsMetadata(storage, fiscalYears);
|
|
79
|
+
return {
|
|
80
|
+
entriesCount: allEntries.length,
|
|
81
|
+
fiscalYearsCount: fiscalYears.length,
|
|
82
|
+
entriesWithFilesDownloaded,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
function findFiscalYear(date, fiscalYears) {
|
|
86
|
+
return fiscalYears.find((fy) => date >= fy.startDate && date <= fy.endDate);
|
|
87
|
+
}
|
|
88
|
+
async function writeJournalEntry(storage, fyYear, entry) {
|
|
89
|
+
// Transform Bokio entry to unified format
|
|
90
|
+
const journalEntry = mapBokioEntryToJournalEntry({
|
|
91
|
+
id: entry.id,
|
|
92
|
+
journalEntryNumber: entry.journalEntryNumber,
|
|
93
|
+
date: entry.date,
|
|
94
|
+
title: entry.title,
|
|
95
|
+
items: entry.items.map((item) => ({
|
|
96
|
+
account: item.account,
|
|
97
|
+
debit: item.debit,
|
|
98
|
+
credit: item.credit,
|
|
99
|
+
})),
|
|
100
|
+
});
|
|
101
|
+
// Generate path
|
|
102
|
+
const entryPath = journalEntryPath(fyYear, journalEntry.series ?? null, journalEntry.entryNumber, journalEntry.entryDate, journalEntry.description);
|
|
103
|
+
// Write YAML
|
|
104
|
+
const yamlContent = toYaml(journalEntry);
|
|
105
|
+
await storage.writeFile(entryPath, yamlContent);
|
|
106
|
+
// Return directory path for file downloads
|
|
107
|
+
return journalEntryDirFromPath(entryPath);
|
|
108
|
+
}
|
|
109
|
+
async function writeFiscalYearsMetadata(storage, fiscalYears) {
|
|
110
|
+
for (const fy of fiscalYears) {
|
|
111
|
+
const fyDir = fiscalYearDirName({ start_date: fy.startDate });
|
|
112
|
+
const metadataPath = `journal-entries/${fyDir}/_fiscal-year.yaml`;
|
|
113
|
+
const metadata = {
|
|
114
|
+
id: fy.id,
|
|
115
|
+
startDate: fy.startDate,
|
|
116
|
+
endDate: fy.endDate,
|
|
117
|
+
status: fy.status,
|
|
118
|
+
};
|
|
119
|
+
const yamlContent = toYaml(metadata);
|
|
120
|
+
await storage.writeFile(metadataPath, yamlContent);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Sync chart of accounts from Bokio to accounts.yaml
|
|
125
|
+
*/
|
|
126
|
+
export async function syncChartOfAccounts(client, repoPath) {
|
|
127
|
+
const storage = new FilesystemStorageService(repoPath);
|
|
128
|
+
// Fetch chart of accounts
|
|
129
|
+
const bokioAccounts = await client.getChartOfAccounts();
|
|
130
|
+
// Sort by account number and transform to expected format
|
|
131
|
+
const accounts = [...bokioAccounts]
|
|
132
|
+
.sort((a, b) => a.account - b.account)
|
|
133
|
+
.map((account) => ({
|
|
134
|
+
code: account.account.toString(),
|
|
135
|
+
name: account.name,
|
|
136
|
+
description: account.name,
|
|
137
|
+
}));
|
|
138
|
+
// Write accounts.yaml
|
|
139
|
+
const yamlContent = toYaml({ accounts });
|
|
140
|
+
await storage.writeFile("accounts.yaml", yamlContent);
|
|
141
|
+
return { accountsCount: accounts.length };
|
|
142
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# AGENTS.md
|
|
2
|
+
|
|
3
|
+
This is an accounting data repository for {{COMPANY_NAME}} (Swedish company), storing financial records, journal entries, and documents. It integrates with {{PROVIDER}} (Swedish accounting software) for document management.
|
|
4
|
+
|
|
5
|
+
## Repository Structure
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
/
|
|
9
|
+
├── accounts.yaml # Swedish chart of accounts (account codes 1000-9999)
|
|
10
|
+
├── journal-entries/ # Main accounting data organized by fiscal year
|
|
11
|
+
│ └── FY-YYYY/ # Fiscal year folders (FY-2014 through FY-2026)
|
|
12
|
+
│ ├── _fiscal-year.yaml
|
|
13
|
+
│ └── [SERIES]-[NUM]-[DATE]-[DESC]/
|
|
14
|
+
│ ├── entry.yaml
|
|
15
|
+
│ └── *.pdf # Supporting documents
|
|
16
|
+
│
|
|
17
|
+
├── inbox/ # Raw incoming documents (no entry yet)
|
|
18
|
+
│ └── [DATE]-[NAME]/
|
|
19
|
+
│ ├── document.yaml
|
|
20
|
+
│ └── *.pdf
|
|
21
|
+
└── drafts/ # Documents with entries, ready to post
|
|
22
|
+
└── [DATE] - [NAME]/ # e.g., "2025-12-24 - anthropic"
|
|
23
|
+
├── document.yaml
|
|
24
|
+
├── entry.yaml
|
|
25
|
+
└── *.pdf
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## CLI Commands
|
|
29
|
+
|
|
30
|
+
Use the `kvitton` CLI to interact with this repository:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
kvitton sync-journal # Sync journal entries from accounting provider
|
|
34
|
+
kvitton sync-inbox # Download inbox files from accounting provider
|
|
35
|
+
kvitton company-info # Display company information
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Entities
|
|
39
|
+
|
|
40
|
+
### Journal Entry (entry.yaml)
|
|
41
|
+
|
|
42
|
+
```yaml
|
|
43
|
+
series: A # A=Admin, B=Customer invoices, C=Customer payments,
|
|
44
|
+
# D=Supplier invoices, E=Supplier payments, K=Salary
|
|
45
|
+
entryNumber: 1
|
|
46
|
+
entryDate: 2024-01-03
|
|
47
|
+
description: Transaction description
|
|
48
|
+
status: POSTED # or DRAFT
|
|
49
|
+
currency: SEK
|
|
50
|
+
lines:
|
|
51
|
+
- account: "1930" # Account code from accounts.yaml
|
|
52
|
+
debit: 1000.00 # or credit:
|
|
53
|
+
memo: Account description
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### Document Metadata (document.yaml)
|
|
57
|
+
|
|
58
|
+
```yaml
|
|
59
|
+
kind: RECEIPT # RECEIPT, INVOICE, BANK_FILE, etc.
|
|
60
|
+
status: DRAFT
|
|
61
|
+
fileName: document.pdf
|
|
62
|
+
sourceIntegration: {{PROVIDER_LOWER}}
|
|
63
|
+
sourceId: UUID
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Fiscal Year (_fiscal-year.yaml)
|
|
67
|
+
|
|
68
|
+
```yaml
|
|
69
|
+
name: FY 2024
|
|
70
|
+
startDate: 2024-01-01
|
|
71
|
+
endDate: 2024-12-31
|
|
72
|
+
status: OPEN
|
|
73
|
+
externalId: "12" # Provider reference
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Account Codes
|
|
77
|
+
|
|
78
|
+
Swedish BAS account codes are organized by ranges:
|
|
79
|
+
|
|
80
|
+
- **1xxx** - Assets (Tillgångar)
|
|
81
|
+
- **2xxx** - Liabilities & Equity (Skulder och eget kapital)
|
|
82
|
+
- **3xxx** - Revenue (Intäkter)
|
|
83
|
+
- **4xxx** - Cost of goods sold (Inköp)
|
|
84
|
+
- **5xxx-6xxx** - Operating expenses (Kostnader)
|
|
85
|
+
- **7xxx** - Personnel costs (Personalkostnader)
|
|
86
|
+
- **8xxx** - Financial items (Finansiella poster)
|
|
87
|
+
- **9xxx** - Year-end allocations (Bokslutsdispositioner)
|
|
88
|
+
|
|
89
|
+
Common accounts:
|
|
90
|
+
- `1930` - Business bank account
|
|
91
|
+
- `2440` - Supplier payables
|
|
92
|
+
- `2610` - Outgoing VAT 25%
|
|
93
|
+
- `2640` - Incoming VAT
|
|
94
|
+
- `4000` - Cost of goods sold
|
|
95
|
+
- `6570` - Bank charges
|
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "create-kvitton",
|
|
3
|
+
"version": "0.4.0",
|
|
4
|
+
"description": "Create a new kvitton bookkeeping repository",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"create-kvitton": "./dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"files": ["dist"],
|
|
10
|
+
"scripts": {
|
|
11
|
+
"build": "bun scripts/build.ts",
|
|
12
|
+
"dev": "tsc --watch",
|
|
13
|
+
"type-check": "tsc --noEmit"
|
|
14
|
+
},
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"@inquirer/prompts": "^7.0.0",
|
|
17
|
+
"@supabase/supabase-js": "^2.49.1",
|
|
18
|
+
"commander": "^12.1.0",
|
|
19
|
+
"ora": "^8.1.1",
|
|
20
|
+
"simple-git": "^3.27.0",
|
|
21
|
+
"yaml": "^2.8.2",
|
|
22
|
+
"zod": "^3.24.1"
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"@types/bun": "latest",
|
|
26
|
+
"typescript": "^5.0.0",
|
|
27
|
+
"integrations-bokio": "workspace:*",
|
|
28
|
+
"sync": "workspace:*"
|
|
29
|
+
},
|
|
30
|
+
"engines": {
|
|
31
|
+
"node": ">=18"
|
|
32
|
+
},
|
|
33
|
+
"publishConfig": {
|
|
34
|
+
"access": "public"
|
|
35
|
+
}
|
|
36
|
+
}
|