• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

acossta / captan / 17112697511

20 Aug 2025 11:22PM UTC coverage: 80.559% (-0.2%) from 80.745%
17112697511

Pull #20

github

web-flow
Merge 87ba59d63 into 5cd2bf1bf
Pull Request #20: fix: Use dynamic version from package.json

510 of 580 branches covered (87.93%)

Branch coverage included in aggregate %.

0 of 4 new or added lines in 1 file covered. (0.0%)

2142 of 2712 relevant lines covered (78.98%)

2936.55 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.29
/src/cli.ts
1
#!/usr/bin/env node
2✔
2
import { Command } from 'commander';
×
3
import { LOGO, NAME, TAGLINE } from './branding.js';
×
4
import * as handlers from './cli-handlers.js';
×
5
import { exists } from './store.js';
×
NEW
6
import { createRequire } from 'module';
×
7

NEW
8
const require = createRequire(import.meta.url);
×
NEW
9
const packageJson = require('../package.json');
×
10

11
const program = new Command();
×
12

13
program
×
14
  .name('captan')
×
15
  .description(`${NAME} — ${TAGLINE}`)
×
NEW
16
  .version(packageJson.version)
×
17
  .showHelpAfterError('(use --help for usage)')
×
18
  .addHelpText('before', LOGO + '\n');
×
19

20
// Init command
21
program
×
22
  .command('init')
×
23
  .description('Initialize a new captable.json')
×
24
  .option('-n, --name <name>', 'company name')
×
25
  .option('-t, --type <type>', 'entity type: c-corp, s-corp, or llc')
×
26
  .option('-s, --state <state>', 'state of incorporation (e.g., DE)')
×
27
  .option('-c, --currency <currency>', 'currency code (e.g., USD)')
×
28
  .option('-a, --authorized <qty>', 'authorized shares/units')
×
29
  .option('--par <value>', 'par value per share (corps only)')
×
30
  .option('--pool <qty>', 'option pool size (absolute number)')
×
31
  .option('--pool-pct <pct>', 'option pool as % of fully diluted')
×
32
  .option('-f, --founder <founder...>', 'founder(s) in format "Name:shares" or "Name:email:shares"')
×
33
  .option(
×
34
    '-d, --date <date>',
×
35
    'incorporation date (YYYY-MM-DD)',
×
36
    new Date().toISOString().slice(0, 10)
×
37
  )
×
38
  .option('-w, --wizard', 'run interactive setup wizard')
×
39
  .action(async (opts) => {
×
40
    const result = await handlers.handleInit(opts);
×
41
    if (!result.success) {
×
42
      console.error(result.message);
×
43
      process.exit(1);
×
44
    }
×
45
    console.log(result.message);
×
46
  });
×
47

48
// Enlist command with subcommands
49
const enlistCmd = program.command('enlist').description('Manage stakeholders');
×
50

51
// Enlist stakeholder subcommand - Add a stakeholder
52
enlistCmd
×
53
  .command('stakeholder')
×
54
  .alias('sh')
×
55
  .description('Add a stakeholder (person or entity)')
×
56
  .requiredOption('-n, --name <name>', 'stakeholder name')
×
57
  .option('-e, --email <email>', 'email address')
×
58
  .option('--entity', 'mark as entity (not individual)')
×
59
  .action((opts) => {
×
60
    const result = handlers.handleStakeholder(opts);
×
61
    if (!result.success) {
×
62
      console.error(result.message);
×
63
      process.exit(1);
×
64
    }
×
65
    console.log(result.message);
×
66
  });
×
67

68
// Standalone stakeholder command for backwards compatibility (alias)
69
program
×
70
  .command('stakeholder')
×
71
  .alias('sh')
×
72
  .description('Add a stakeholder (alias for enlist stakeholder)')
×
73
  .requiredOption('-n, --name <name>', 'stakeholder name')
×
74
  .option('-e, --email <email>', 'email address')
×
75
  .option('--entity', 'mark as entity (not individual)')
×
76
  .action((opts) => {
×
77
    const result = handlers.handleStakeholder(opts);
×
78
    if (!result.success) {
×
79
      console.error(result.message);
×
80
      process.exit(1);
×
81
    }
×
82
    console.log(result.message);
×
83
  });
×
84

85
// Security add command
86
program
×
87
  .command('security:add')
×
88
  .description('Add a security class')
×
89
  .requiredOption('-k, --kind <kind>', 'security type: common, preferred, or pool')
×
90
  .requiredOption('-l, --label <label>', 'display label')
×
91
  .requiredOption('-a, --authorized <qty>', 'authorized shares/units')
×
92
  .option('-p, --par <value>', 'par value per share')
×
93
  .action((opts) => {
×
94
    const result = handlers.handleSecurityAdd(opts.kind, opts.label, opts.authorized, opts.par);
×
95
    if (!result.success) {
×
96
      console.error(result.message);
×
97
      process.exit(1);
×
98
    }
×
99
    console.log(result.message);
×
100
  });
×
101

102
// Issue command
103
program
×
104
  .command('issue')
×
105
  .description('Issue shares')
×
106
  .requiredOption('--holder <id>', 'stakeholder ID')
×
107
  .option('--security <id>', 'security class ID (defaults to common)')
×
108
  .requiredOption('-q, --qty <amount>', 'number of shares')
×
109
  .option('--pps <amount>', 'price per share')
×
110
  .option('-d, --date <date>', 'issuance date (YYYY-MM-DD)', new Date().toISOString().slice(0, 10))
×
111
  .action((opts) => {
×
112
    // Map to handler's expected params
113
    const mappedOpts = {
×
114
      stakeholder: opts.holder,
×
115
      securityClass: opts.security,
×
116
      qty: opts.qty,
×
117
      price: opts.pps,
×
118
      date: opts.date,
×
119
    };
×
120
    const result = handlers.handleIssue(mappedOpts);
×
121
    if (!result.success) {
×
122
      console.error(result.message);
×
123
      process.exit(1);
×
124
    }
×
125
    console.log(result.message);
×
126
  });
×
127

128
// Grant command
129
program
×
130
  .command('grant')
×
131
  .description('Grant options')
×
132
  .requiredOption('--holder <id>', 'stakeholder ID')
×
133
  .option('-p, --pool <id>', 'option pool ID (defaults to first pool)')
×
134
  .requiredOption('-q, --qty <amount>', 'number of options')
×
135
  .requiredOption('-e, --exercise <price>', 'exercise price per share')
×
136
  .option('-d, --date <date>', 'grant date (YYYY-MM-DD)', new Date().toISOString().slice(0, 10))
×
137
  .option('--months <months>', 'total vesting period in months')
×
138
  .option('--cliff <months>', 'cliff period in months')
×
139
  .option('--vest-start <date>', 'vesting start date (defaults to grant date)')
×
140
  .option('--no-vesting', 'grant without vesting schedule')
×
141
  .action((opts) => {
×
142
    // Map to handler's expected params
143
    const mappedOpts = {
×
144
      stakeholder: opts.holder,
×
145
      pool: opts.pool,
×
146
      qty: opts.qty,
×
147
      exercise: opts.exercise,
×
148
      date: opts.date,
×
149
      vestMonths: opts.noVesting ? undefined : opts.months,
×
150
      vestCliff: opts.noVesting ? undefined : opts.cliff,
×
151
      vestStart: opts.noVesting ? undefined : opts.vestStart,
×
152
    };
×
153
    const result = handlers.handleGrant(mappedOpts);
×
154
    if (!result.success) {
×
155
      console.error(result.message);
×
156
      process.exit(1);
×
157
    }
×
158
    console.log(result.message);
×
159
  });
×
160

161
// SAFE command
162
program
×
163
  .command('safe')
×
164
  .description('Add a SAFE')
×
165
  .requiredOption('--holder <id>', 'stakeholder ID')
×
166
  .requiredOption('-a, --amount <amount>', 'investment amount')
×
167
  .option('-c, --cap <amount>', 'valuation cap')
×
168
  .option('--discount <pct>', 'discount percentage (e.g., 20 for 20%)')
×
169
  .option('--post-money', 'use post-money SAFE calculation')
×
170
  .option(
×
171
    '-d, --date <date>',
×
172
    'investment date (YYYY-MM-DD)',
×
173
    new Date().toISOString().slice(0, 10)
×
174
  )
×
175
  .option('-n, --note <note>', 'optional note')
×
176
  .action((opts) => {
×
177
    // Map to handler's expected params
178
    const mappedOpts = {
×
179
      stakeholder: opts.holder,
×
180
      amount: opts.amount,
×
181
      cap: opts.cap,
×
182
      discount: opts.discount,
×
183
      postMoney: opts.postMoney,
×
184
      date: opts.date,
×
185
      note: opts.note,
×
186
    };
×
187
    const result = handlers.handleSAFE(mappedOpts);
×
188
    if (!result.success) {
×
189
      console.error(result.message);
×
190
      process.exit(1);
×
191
    }
×
192
    console.log(result.message);
×
193
  });
×
194

195
// SAFEs list command
196
program
×
197
  .command('safes')
×
198
  .description('List all SAFEs with details')
×
199
  .action(() => {
×
200
    const result = handlers.handleSafes();
×
201
    if (!result.success) {
×
202
      console.error(result.message);
×
203
      process.exit(1);
×
204
    }
×
205
    console.log(result.message);
×
206
  });
×
207

208
// Convert command
209
program
×
210
  .command('convert')
×
211
  .description('Convert all SAFEs at a given price')
×
212
  .option('--pre-money <amount>', 'pre-money valuation')
×
213
  .option('--new-money <amount>', 'new money raised')
×
214
  .option('--pps <price>', 'price per share for conversion')
×
215
  .option('--price <price>', 'price per share (alias for --pps)')
×
216
  .option(
×
217
    '-d, --date <date>',
×
218
    'conversion date (YYYY-MM-DD)',
×
219
    new Date().toISOString().slice(0, 10)
×
220
  )
×
221
  .option('--post-money', 'use post-money calculation')
×
222
  .option('--dry-run', 'preview conversion without executing')
×
223
  .action((opts) => {
×
224
    // Use pps or price, whichever is provided
225
    const price = opts.pps || opts.price;
×
226
    if (!price && !opts.preMoney) {
×
227
      console.error('Error: Must provide either --pps/--price or --pre-money');
×
228
      process.exit(1);
×
229
    }
×
230
    const mappedOpts = {
×
231
      price: price,
×
232
      preMoney: opts.preMoney,
×
233
      newMoney: opts.newMoney,
×
234
      date: opts.date,
×
235
      postMoney: opts.postMoney,
×
236
      dryRun: opts.dryRun,
×
237
    };
×
238
    const result = handlers.handleConvert(mappedOpts);
×
239
    if (!result.success) {
×
240
      console.error(result.message);
×
241
      process.exit(1);
×
242
    }
×
243
    console.log(result.message);
×
244
  });
×
245

246
// Chart command
247
program
×
248
  .command('chart')
×
249
  .description('Display cap table chart')
×
250
  .option('-d, --date <date>', 'as-of date (YYYY-MM-DD)')
×
251
  .option('-f, --format <format>', 'output format')
×
252
  .action((opts) => {
×
253
    const result = handlers.handleChart(opts);
×
254
    if (!result.success) {
×
255
      console.error(result.message);
×
256
      process.exit(1);
×
257
    }
×
258
    console.log(result.message);
×
259
  });
×
260

261
// Export command
262
program
×
263
  .command('export')
×
264
  .description('Export cap table data')
×
265
  .argument('<format>', 'export format: json, csv, or summary')
×
266
  .option('--no-options', 'exclude option grants from CSV export')
×
267
  .action((format, opts) => {
×
268
    const result = handlers.handleExport(format, opts);
×
269
    if (!result.success) {
×
270
      console.error(result.message);
×
271
      process.exit(1);
×
272
    }
×
273
    console.log(result.message);
×
274
  });
×
275

276
// Report command
277
program
×
278
  .command('report')
×
279
  .description('Generate detailed reports')
×
280
  .argument('<type>', 'report type: stakeholder, security, or summary')
×
281
  .argument('[id]', 'entity ID to report on (not needed for summary)')
×
282
  .action((type, id) => {
×
283
    // Summary doesn't require an ID
284
    if (type === 'summary' && !id) {
×
285
      id = '';
×
286
    } else if (type !== 'summary' && !id) {
×
287
      console.error('Error: ID is required for stakeholder and security reports');
×
288
      process.exit(1);
×
289
    }
×
290
    const result = handlers.handleReport({ type, id });
×
291
    if (!result.success) {
×
292
      console.error(result.message);
×
293
      process.exit(1);
×
294
    }
×
295
    console.log(result.message);
×
296
  });
×
297

298
// Log command
299
program
×
300
  .command('log')
×
301
  .alias('audit')
×
302
  .description('Show audit log')
×
303
  .option('-l, --limit <count>', 'number of entries to show', '20')
×
304
  .option('-a, --action <action>', 'filter by action type')
×
305
  .action((opts) => {
×
306
    const result = handlers.handleLog(opts);
×
307
    if (!result.success) {
×
308
      console.error(result.message);
×
309
      process.exit(1);
×
310
    }
×
311
    console.log(result.message);
×
312
  });
×
313

314
// List command
315
program
×
316
  .command('list')
×
317
  .alias('ls')
×
318
  .description('List entities')
×
319
  .argument('<type>', 'entity type: stakeholders, securities/classes, or safes')
×
320
  .action((type) => {
×
321
    // Map 'securities' to 'classes' for backwards compatibility
322
    const mappedType = type === 'securities' ? 'classes' : type;
×
323
    const result = handlers.handleList({ type: mappedType });
×
324
    if (!result.success) {
×
325
      console.error(result.message);
×
326
      process.exit(1);
×
327
    }
×
328
    console.log(result.message);
×
329
  });
×
330

331
// Validate command
332
program
×
333
  .command('validate')
×
334
  .description('Validate a captable.json file')
×
335
  .option('-f, --file <file>', 'captable file to validate', 'captable.json')
×
336
  .option('-e, --extended', 'perform extended validation with business rules')
×
337
  .action((opts) => {
×
338
    const result = handlers.handleValidate(opts);
×
339
    if (!result.success) {
×
340
      console.error(result.message);
×
341
      process.exit(1);
×
342
    }
×
343
    console.log(result.message);
×
344
  });
×
345

346
// Schema command
347
program
×
348
  .command('schema')
×
349
  .description('Export JSON Schema for captable validation')
×
350
  .option('-o, --output <file>', 'output file', 'captable.schema.json')
×
351
  .action((opts) => {
×
352
    const result = handlers.handleSchema(opts);
×
353
    if (!result.success) {
×
354
      console.error(result.message);
×
355
      process.exit(1);
×
356
    }
×
357
    console.log(result.message);
×
358
  });
×
359

360
// Helper function to check if captable exists for commands that need it
361
function ensureCaptableExists() {
×
362
  if (!exists('captable.json')) {
×
363
    console.error('❌ No captable.json found. Run "captan init" first.');
×
364
    process.exit(1);
×
365
  }
×
366
}
×
367

368
// Add pre-action hook for commands that need existing captable
369
const commandsThatNeedCaptable = [
×
370
  'stakeholder',
×
371
  'security:add',
×
372
  'issue',
×
373
  'grant',
×
374
  'safe',
×
375
  'safes',
×
376
  'convert',
×
377
  'chart',
×
378
  'export',
×
379
  'report',
×
380
  'log',
×
381
  'list',
×
382
  'enlist',
×
383
];
×
384

385
program.commands.forEach((cmd) => {
×
386
  if (commandsThatNeedCaptable.includes(cmd.name())) {
×
387
    cmd.hook('preAction', ensureCaptableExists);
×
388
  }
×
389
});
×
390

391
// Parse and execute
392
program.parseAsync(process.argv).catch((error) => {
×
393
  console.error(`❌ ${error.message}`);
×
394
  process.exit(1);
×
395
});
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc