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.
@@ -0,0 +1,238 @@
1
+ import { select, input, password, confirm } from "@inquirer/prompts";
2
+ import ora from "ora";
3
+ import * as path from "node:path";
4
+ import * as fs from "node:fs/promises";
5
+ import { fileURLToPath } from "node:url";
6
+ import { BokioClient } from "integrations-bokio";
7
+ import { sanitizeOrgName } from "sync";
8
+ import { initGitRepo, commitAll } from "../lib/git";
9
+ import { createEnvFile } from "../lib/env";
10
+ import { syncJournalEntries, syncChartOfAccounts } from "../sync/bokio-sync";
11
+ const __filename = fileURLToPath(import.meta.url);
12
+ const __dirname = path.dirname(__filename);
13
+ async function createAgentsFile(targetDir, options) {
14
+ // Read template
15
+ const templatePath = path.join(__dirname, "../templates/AGENTS.md");
16
+ let template = await fs.readFile(templatePath, "utf-8");
17
+ // Replace placeholders
18
+ template = template.replace(/\{\{COMPANY_NAME\}\}/g, options.companyName);
19
+ template = template.replace(/\{\{PROVIDER\}\}/g, options.provider);
20
+ template = template.replace(/\{\{PROVIDER_LOWER\}\}/g, options.provider.toLowerCase());
21
+ // Write AGENTS.md
22
+ const agentsPath = path.join(targetDir, "AGENTS.md");
23
+ await fs.writeFile(agentsPath, template, "utf-8");
24
+ // Create CLAUDE.md symlink pointing to AGENTS.md
25
+ const claudePath = path.join(targetDir, "CLAUDE.md");
26
+ await fs.symlink("AGENTS.md", claudePath);
27
+ }
28
+ /**
29
+ * Validate UUID format
30
+ */
31
+ function isValidUUID(value) {
32
+ return /^[0-9a-f-]{36}$/i.test(value.trim());
33
+ }
34
+ /**
35
+ * Validate folder name format
36
+ */
37
+ function isValidFolderName(value) {
38
+ return /^[a-z0-9-]+$/.test(value.trim());
39
+ }
40
+ export async function createBookkeepingRepo(name, options = {}) {
41
+ const { yes: acceptDefaults = false } = options;
42
+ console.log("\n Create a new bookkeeping repository\n");
43
+ // 1. Determine provider (CLI arg or prompt)
44
+ let provider = options.provider;
45
+ if (!provider) {
46
+ provider = await select({
47
+ message: "Select your accounting provider:",
48
+ choices: [
49
+ { name: "Bokio", value: "bokio" },
50
+ { name: "Fortnox (coming soon)", value: "fortnox", disabled: true },
51
+ ],
52
+ });
53
+ }
54
+ if (provider !== "bokio") {
55
+ console.log("Only Bokio is supported at this time.");
56
+ process.exit(1);
57
+ }
58
+ // 2. Get token from env var (required for Bokio)
59
+ const envToken = process.env.BOKIO_TOKEN;
60
+ let token;
61
+ if (envToken) {
62
+ token = envToken;
63
+ console.log(" Using BOKIO_TOKEN from environment");
64
+ }
65
+ else {
66
+ // Interactive mode - prompt for token
67
+ console.log("\n To connect to Bokio, you need:");
68
+ console.log(" - API Token: Bokio app > Settings > Integrations > Create Integration");
69
+ console.log(" - Company ID: From your Bokio URL (app.bokio.se/{companyId}/...)\n");
70
+ console.log(" Tip: Set BOKIO_TOKEN env var to avoid entering token each time\n");
71
+ token = await password({
72
+ message: "Enter your Bokio API token:",
73
+ mask: "*",
74
+ });
75
+ }
76
+ // 3. Get company ID (CLI arg or prompt)
77
+ let companyId = options.companyId;
78
+ if (!companyId) {
79
+ companyId = await input({
80
+ message: "Enter your Bokio Company ID:",
81
+ validate: (value) => {
82
+ if (!value.trim())
83
+ return "Company ID is required";
84
+ if (!isValidUUID(value)) {
85
+ return "Company ID should be a UUID (e.g., 12345678-1234-1234-1234-123456789abc)";
86
+ }
87
+ return true;
88
+ },
89
+ });
90
+ }
91
+ else if (!isValidUUID(companyId)) {
92
+ console.error("Error: Company ID should be a UUID (e.g., 12345678-1234-1234-1234-123456789abc)");
93
+ process.exit(1);
94
+ }
95
+ // 4. Validate credentials
96
+ const spinner = ora("Validating credentials...").start();
97
+ const client = new BokioClient({ token, companyId: companyId.trim() });
98
+ let companyName;
99
+ try {
100
+ const companyInfo = await client.getCompanyInformation();
101
+ companyName = companyInfo.name;
102
+ spinner.succeed(`Connected to: ${companyName}`);
103
+ }
104
+ catch (error) {
105
+ spinner.fail("Failed to connect to Bokio");
106
+ console.error(error instanceof Error ? error.message : "Unknown error");
107
+ process.exit(1);
108
+ }
109
+ // 5. Determine folder name (CLI arg, default, or prompt)
110
+ const suggestedName = `kvitton-${sanitizeOrgName(companyName)}`;
111
+ let folderName;
112
+ if (name) {
113
+ // CLI argument provided
114
+ if (!isValidFolderName(name)) {
115
+ console.error("Error: Folder name should only contain lowercase letters, numbers, and hyphens");
116
+ process.exit(1);
117
+ }
118
+ folderName = name;
119
+ }
120
+ else if (acceptDefaults) {
121
+ // --yes flag: use suggested name
122
+ folderName = suggestedName;
123
+ console.log(` Using folder name: ${folderName}`);
124
+ }
125
+ else {
126
+ // Interactive prompt
127
+ folderName = await input({
128
+ message: "Repository folder name:",
129
+ default: suggestedName,
130
+ validate: (value) => {
131
+ if (!value.trim())
132
+ return "Folder name is required";
133
+ if (!isValidFolderName(value)) {
134
+ return "Folder name should only contain lowercase letters, numbers, and hyphens";
135
+ }
136
+ return true;
137
+ },
138
+ });
139
+ }
140
+ const targetDir = path.resolve(process.cwd(), folderName.trim());
141
+ // Check if directory exists
142
+ try {
143
+ await fs.access(targetDir);
144
+ if (acceptDefaults) {
145
+ // With --yes, abort if directory exists
146
+ console.error(`Error: Directory ${folderName} already exists`);
147
+ process.exit(1);
148
+ }
149
+ const overwrite = await confirm({
150
+ message: `Directory ${folderName} already exists. Continue anyway?`,
151
+ default: false,
152
+ });
153
+ if (!overwrite) {
154
+ console.log("Aborted.");
155
+ process.exit(0);
156
+ }
157
+ }
158
+ catch {
159
+ // Directory doesn't exist, good
160
+ }
161
+ // 6. Create folder and initialize git
162
+ const gitSpinner = ora("Initializing repository...").start();
163
+ await fs.mkdir(targetDir, { recursive: true });
164
+ await initGitRepo(targetDir);
165
+ gitSpinner.succeed("Repository initialized");
166
+ // 7. Create .env file
167
+ await createEnvFile(targetDir, {
168
+ PROVIDER: "bokio",
169
+ BOKIO_TOKEN: token,
170
+ BOKIO_COMPANY_ID: companyId.trim(),
171
+ });
172
+ console.log(" Created .env with credentials");
173
+ // 8. Create AGENTS.md and CLAUDE.md symlink
174
+ await createAgentsFile(targetDir, {
175
+ companyName,
176
+ provider: "Bokio",
177
+ });
178
+ console.log(" Created AGENTS.md and CLAUDE.md symlink");
179
+ // 9. Sync chart of accounts
180
+ const accountsSpinner = ora("Syncing chart of accounts...").start();
181
+ const { accountsCount } = await syncChartOfAccounts(client, targetDir);
182
+ accountsSpinner.succeed(`Synced ${accountsCount} accounts to accounts.yaml`);
183
+ // 10. Sync journal entries (CLI flag, default, or prompt)
184
+ let shouldSync;
185
+ if (options.sync !== undefined) {
186
+ // Explicit --sync or --no-sync flag
187
+ shouldSync = options.sync;
188
+ }
189
+ else if (acceptDefaults) {
190
+ // --yes flag: default to sync
191
+ shouldSync = true;
192
+ }
193
+ else {
194
+ // Interactive prompt
195
+ shouldSync = await confirm({
196
+ message: "Sync existing journal entries from Bokio?",
197
+ default: true,
198
+ });
199
+ }
200
+ let entriesCount = 0;
201
+ let fiscalYearsCount = 0;
202
+ if (shouldSync) {
203
+ const shouldDownloadFiles = options.downloadFiles !== false;
204
+ const result = await syncJournalEntries(client, targetDir, { downloadFiles: shouldDownloadFiles }, (progress) => {
205
+ process.stdout.write(`\r Syncing: ${progress.current}/${progress.total} entries`);
206
+ });
207
+ entriesCount = result.entriesCount;
208
+ fiscalYearsCount = result.fiscalYearsCount;
209
+ if (result.entriesWithFilesDownloaded > 0) {
210
+ console.log(`\n Downloaded files for ${result.entriesWithFilesDownloaded}/${result.entriesCount} entries`);
211
+ }
212
+ console.log(" Sync complete!");
213
+ }
214
+ // 11. Final commit
215
+ const commitSpinner = ora("Creating initial commit...").start();
216
+ await commitAll(targetDir, "Initial sync from Bokio");
217
+ commitSpinner.succeed("Initial commit created");
218
+ // 12. Success message
219
+ console.log(`
220
+ Success! Created ${folderName} at ${targetDir}
221
+
222
+ Summary:
223
+ - ${accountsCount} accounts
224
+ - ${fiscalYearsCount} fiscal year(s)
225
+ - ${entriesCount} journal entries
226
+
227
+ Next steps:
228
+ cd ${folderName}
229
+
230
+ Your bookkeeping data is stored as YAML files:
231
+ journal-entries/FY-2024/A-001-2024-01-15-invoice/entry.yaml
232
+
233
+ Commands:
234
+ kvitton sync-journal Sync journal entries from Bokio
235
+ kvitton sync-inbox Download inbox files from Bokio
236
+ kvitton company-info Display company information
237
+ `);
238
+ }