pacioli 1.0.1

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/src/cli.ts ADDED
@@ -0,0 +1,111 @@
1
+ #!/usr/bin/env bun
2
+
3
+ /**
4
+ * Pacioli CLI
5
+ * Main entry point for command routing
6
+ */
7
+
8
+ import { join, dirname } from "path";
9
+ import { fileURLToPath } from "url";
10
+ import { initCommand } from "./commands/init";
11
+ import { generateCommand } from "./commands/generate";
12
+
13
+ const __filename = fileURLToPath(import.meta.url);
14
+ const __dirname = dirname(__filename);
15
+
16
+ /**
17
+ * Get version from package.json
18
+ */
19
+ function getVersion(): string {
20
+ try {
21
+ const packageRoot = join(__dirname, "..");
22
+ const packageJsonPath = join(packageRoot, "package.json");
23
+ const packageJson = require(packageJsonPath);
24
+ return packageJson.version || "1.0.0";
25
+ } catch {
26
+ return "1.0.0";
27
+ }
28
+ }
29
+
30
+ /**
31
+ * Print main help message
32
+ */
33
+ function printHelp() {
34
+ console.log(`
35
+ Pacioli - Financial Document Generator for Freelancers
36
+ Named after Luca Pacioli, father of accounting (1494)
37
+
38
+ Usage:
39
+ pacioli <command> [options]
40
+
41
+ Commands:
42
+ init Initialize a new project with templates and examples
43
+ generate Generate a financial document (invoice, quotation, receipt)
44
+ help Show this help message
45
+ version Show version number
46
+
47
+ Examples:
48
+ # Initialize new project
49
+ mkdir my-invoices && cd my-invoices
50
+ pacioli init
51
+
52
+ # Generate documents
53
+ pacioli generate invoice examples/invoice.json --customer customers/acme-corp.json
54
+ pacioli generate quotation examples/quotation.json --customer customers/demo.json
55
+ pacioli generate receipt examples/receipt.json --customer customers/test.json
56
+
57
+ For command-specific help:
58
+ pacioli generate --help
59
+
60
+ Learn more: https://github.com/peerasak-u/pacioli
61
+ `);
62
+ }
63
+
64
+ /**
65
+ * Main CLI router
66
+ */
67
+ async function main() {
68
+ const args = process.argv.slice(2);
69
+
70
+ // No arguments - show help
71
+ if (args.length === 0) {
72
+ printHelp();
73
+ process.exit(0);
74
+ }
75
+
76
+ const command = args[0];
77
+
78
+ // Route commands
79
+ switch (command) {
80
+ case "init":
81
+ await initCommand(args.slice(1));
82
+ break;
83
+
84
+ case "generate":
85
+ await generateCommand(args.slice(1));
86
+ break;
87
+
88
+ case "help":
89
+ case "--help":
90
+ case "-h":
91
+ printHelp();
92
+ break;
93
+
94
+ case "version":
95
+ case "--version":
96
+ case "-v":
97
+ console.log(`v${getVersion()}`);
98
+ break;
99
+
100
+ default:
101
+ console.error(`Unknown command: ${command}`);
102
+ console.error('Run "pacioli help" for usage information');
103
+ process.exit(1);
104
+ }
105
+ }
106
+
107
+ // Run CLI
108
+ main().catch((error) => {
109
+ console.error("Fatal error:", error instanceof Error ? error.message : error);
110
+ process.exit(1);
111
+ });
@@ -0,0 +1,286 @@
1
+ /**
2
+ * Pacioli Generate Command
3
+ * Generates financial documents (invoice, quotation, receipt) as PDFs
4
+ */
5
+
6
+ import { join, dirname } from "path";
7
+ import { fileURLToPath } from "url";
8
+ import { fileExists, readJSON, getOutputPath } from "../utils";
9
+ import {
10
+ validateInvoice,
11
+ validateQuotation,
12
+ validateReceipt,
13
+ validateFreelancerConfig,
14
+ validateCustomer,
15
+ type DocumentData,
16
+ type FreelancerConfig,
17
+ type Customer,
18
+ } from "../validator";
19
+ import { generatePDF } from "../generator";
20
+ import {
21
+ getNextDocumentNumber,
22
+ incrementDocumentCounter,
23
+ } from "../metadata";
24
+
25
+ const __filename = fileURLToPath(import.meta.url);
26
+ const __dirname = dirname(__filename);
27
+
28
+ const VALID_TYPES = ["invoice", "quotation", "receipt"] as const;
29
+ type DocumentType = (typeof VALID_TYPES)[number];
30
+
31
+ /**
32
+ * Get the package root directory
33
+ */
34
+ function getPackageRoot(): string {
35
+ return join(__dirname, "../..");
36
+ }
37
+
38
+ /**
39
+ * Print generate command help
40
+ */
41
+ function printGenerateHelp() {
42
+ console.log(`
43
+ Generate a financial document as PDF
44
+
45
+ Usage:
46
+ pacioli generate <type> <input-json> --customer <customer-json> [options]
47
+
48
+ Arguments:
49
+ <type> Document type: invoice, quotation, or receipt
50
+ <input-json> Path to document data JSON file (relative to current directory)
51
+
52
+ Required Options:
53
+ --customer <path> Path to customer JSON file (required)
54
+
55
+ Optional Settings:
56
+ --output <path> Custom output PDF path (default: output/{type}-{number}.pdf)
57
+ --profile <path> Path to freelancer profile (default: config/freelancer.json)
58
+ --help Show this help message
59
+
60
+ Examples:
61
+ # Generate invoice with auto-numbering
62
+ pacioli generate invoice examples/invoice-auto.json --customer customers/acme-corp.json
63
+
64
+ # Generate quotation with custom output
65
+ pacioli generate quotation examples/quotation.json --customer customers/demo.json --output custom/quote.pdf
66
+
67
+ # Generate receipt with specific profile
68
+ pacioli generate receipt examples/receipt.json --customer customers/test.json --profile config/freelancer-alt.json
69
+
70
+ Document Types:
71
+ invoice - Bill for completed work (includes due date, payment terms)
72
+ quotation - Price estimate before work begins (includes validity period)
73
+ receipt - Payment confirmation (includes payment date, method, reference)
74
+
75
+ Auto-numbering:
76
+ Set "documentNumber": "auto" in your JSON file to use sequential numbering.
77
+ Format: PREFIX-YYYYMM-NUMBER (e.g., INV-202410-001)
78
+ Counters automatically reset each month.
79
+ `);
80
+ }
81
+
82
+ /**
83
+ * Parse generate command arguments
84
+ */
85
+ function parseGenerateArgs(args: string[]) {
86
+ const options: {
87
+ type?: DocumentType;
88
+ inputPath?: string;
89
+ customerPath?: string;
90
+ outputPath?: string;
91
+ configPath: string;
92
+ help: boolean;
93
+ } = {
94
+ configPath: "config/freelancer.json",
95
+ help: false,
96
+ };
97
+
98
+ // Check for help flag
99
+ if (args.includes("--help") || args.includes("-h")) {
100
+ options.help = true;
101
+ return options;
102
+ }
103
+
104
+ // Get positional arguments
105
+ const positionalArgs = args.filter((arg) => !arg.startsWith("--"));
106
+
107
+ if (positionalArgs.length >= 2) {
108
+ const type = positionalArgs[0];
109
+ if (VALID_TYPES.includes(type as DocumentType)) {
110
+ options.type = type as DocumentType;
111
+ }
112
+ options.inputPath = positionalArgs[1];
113
+ }
114
+
115
+ // Parse options
116
+ for (let i = 0; i < args.length; i++) {
117
+ const arg = args[i];
118
+ const nextArg = args[i + 1];
119
+
120
+ if (arg === "--customer" && nextArg) {
121
+ options.customerPath = nextArg;
122
+ i++;
123
+ } else if (arg === "--output" && nextArg) {
124
+ options.outputPath = nextArg;
125
+ i++;
126
+ } else if (arg === "--profile" && nextArg) {
127
+ options.configPath = nextArg;
128
+ i++;
129
+ }
130
+ }
131
+
132
+ return options;
133
+ }
134
+
135
+ /**
136
+ * Resolve paths relative to current working directory
137
+ */
138
+ function resolvePaths(options: ReturnType<typeof parseGenerateArgs>) {
139
+ const cwd = process.cwd();
140
+
141
+ return {
142
+ inputPath: options.inputPath ? join(cwd, options.inputPath) : undefined,
143
+ customerPath: options.customerPath ? join(cwd, options.customerPath) : undefined,
144
+ outputPath: options.outputPath ? join(cwd, options.outputPath) : undefined,
145
+ configPath: join(cwd, options.configPath),
146
+ };
147
+ }
148
+
149
+ /**
150
+ * Generate command handler
151
+ */
152
+ export async function generateCommand(args: string[]) {
153
+ const options = parseGenerateArgs(args);
154
+
155
+ if (options.help) {
156
+ printGenerateHelp();
157
+ return;
158
+ }
159
+
160
+ // Validate arguments
161
+ if (!options.type) {
162
+ console.error("❌ Error: Invalid or missing document type");
163
+ console.error(`Valid types: ${VALID_TYPES.join(", ")}`);
164
+ console.error('\nRun "pacioli generate --help" for usage information');
165
+ process.exit(1);
166
+ }
167
+
168
+ if (!options.inputPath) {
169
+ console.error("❌ Error: Input JSON file path is required");
170
+ console.error('\nRun "pacioli generate --help" for usage information');
171
+ process.exit(1);
172
+ }
173
+
174
+ if (!options.customerPath) {
175
+ console.error("❌ Error: Customer JSON file path is required (use --customer)");
176
+ console.error('\nRun "pacioli generate --help" for usage information');
177
+ process.exit(1);
178
+ }
179
+
180
+ // Resolve all paths relative to current directory
181
+ const paths = resolvePaths(options);
182
+
183
+ // Check if input file exists
184
+ if (!(await fileExists(paths.inputPath!))) {
185
+ console.error(`❌ Error: Input file not found: ${options.inputPath}`);
186
+ console.error(` Looking in: ${paths.inputPath}`);
187
+ process.exit(1);
188
+ }
189
+
190
+ // Check if customer file exists
191
+ if (!(await fileExists(paths.customerPath!))) {
192
+ console.error(`❌ Error: Customer file not found: ${options.customerPath}`);
193
+ console.error(` Looking in: ${paths.customerPath}`);
194
+ process.exit(1);
195
+ }
196
+
197
+ // Check if profile file exists
198
+ if (!(await fileExists(paths.configPath))) {
199
+ console.error(`❌ Error: Profile file not found: ${options.configPath}`);
200
+ console.error(` Looking in: ${paths.configPath}`);
201
+ console.error("\n💡 Tip: Did you forget to run 'pacioli init'?");
202
+ console.error(" Or copy config/freelancer.example.json to config/freelancer.json");
203
+ process.exit(1);
204
+ }
205
+
206
+ try {
207
+ console.log(`\n📄 Generating ${options.type}...`);
208
+
209
+ // Load freelancer profile
210
+ console.log(`📋 Loading profile from ${options.configPath}...`);
211
+ const config = await readJSON<FreelancerConfig>(paths.configPath);
212
+
213
+ // Validate profile
214
+ const configValidation = validateFreelancerConfig(config);
215
+ if (!configValidation.valid) {
216
+ console.error("❌ Error: Invalid freelancer profile:");
217
+ configValidation.errors.forEach((err) => console.error(` - ${err}`));
218
+ process.exit(1);
219
+ }
220
+
221
+ // Load customer data
222
+ console.log(`👤 Loading customer from ${options.customerPath}...`);
223
+ const customer = await readJSON<Customer>(paths.customerPath!);
224
+
225
+ // Validate customer
226
+ const customerValidation = validateCustomer(customer);
227
+ if (!customerValidation.valid) {
228
+ console.error("❌ Error: Invalid customer data:");
229
+ customerValidation.errors.forEach((err) => console.error(` - ${err}`));
230
+ process.exit(1);
231
+ }
232
+
233
+ // Load document data
234
+ console.log(`📋 Loading data from ${options.inputPath}...`);
235
+ const data = await readJSON<DocumentData>(paths.inputPath!);
236
+
237
+ // Validate document data based on type
238
+ let validation;
239
+ switch (options.type) {
240
+ case "invoice":
241
+ validation = validateInvoice(data);
242
+ break;
243
+ case "quotation":
244
+ validation = validateQuotation(data);
245
+ break;
246
+ case "receipt":
247
+ validation = validateReceipt(data);
248
+ break;
249
+ }
250
+
251
+ if (!validation.valid) {
252
+ console.error(`❌ Error: Invalid ${options.type} data:`);
253
+ validation.errors.forEach((err) => console.error(` - ${err}`));
254
+ process.exit(1);
255
+ }
256
+
257
+ // Handle auto-numbering
258
+ let resolvedDocumentNumber = data.documentNumber;
259
+ if (data.documentNumber === "auto") {
260
+ console.log(`🔢 Generating next document number...`);
261
+ resolvedDocumentNumber = await getNextDocumentNumber(options.type);
262
+ data.documentNumber = resolvedDocumentNumber;
263
+ console.log(` ✓ Document number: ${resolvedDocumentNumber}`);
264
+ }
265
+
266
+ // Determine output path
267
+ const outputPath = getOutputPath(
268
+ options.type,
269
+ resolvedDocumentNumber,
270
+ paths.outputPath
271
+ );
272
+
273
+ // Generate PDF
274
+ console.log(`🔨 Generating PDF...`);
275
+ await generatePDF(options.type, data, customer, config, outputPath);
276
+
277
+ // Update metadata counter after successful generation
278
+ await incrementDocumentCounter(options.type, resolvedDocumentNumber);
279
+
280
+ console.log(`\n✅ Success! PDF saved to: ${outputPath}`);
281
+ } catch (error) {
282
+ console.error("\n❌ Generation failed:");
283
+ console.error(error instanceof Error ? error.message : error);
284
+ process.exit(1);
285
+ }
286
+ }