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
|
@@ -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
|
+
}
|