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

acossta / captan / 17073157919

19 Aug 2025 02:43PM UTC coverage: 77.955% (+15.4%) from 62.54%
17073157919

push

github

web-flow
Merge pull request #10 from acossta/improve-coverage

feat: add --dry-run option for SAFE conversion preview

427 of 492 branches covered (86.79%)

Branch coverage included in aggregate %.

618 of 903 new or added lines in 3 files covered. (68.44%)

14 existing lines in 1 file now uncovered.

1769 of 2325 relevant lines covered (76.09%)

3389.81 hits per line

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

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

7
const program = new Command();
×
8

9
program
×
10
  .name('captan')
×
11
  .description(`${NAME} — ${TAGLINE}`)
×
12
  .version('0.1.0')
×
13
  .showHelpAfterError('(use --help for usage)')
×
14
  .addHelpText('before', LOGO + '\n');
×
15

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

44
// Enlist command with subcommands
NEW
45
const enlistCmd = program.command('enlist').description('Manage stakeholders');
×
46

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

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

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

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

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

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

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

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

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

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

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

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

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

327
// Helper function to check if captable exists for commands that need it
NEW
328
function ensureCaptableExists() {
×
NEW
329
  if (!exists('captable.json')) {
×
NEW
330
    console.error('❌ No captable.json found. Run "captan init" first.');
×
NEW
331
    process.exit(1);
×
NEW
332
  }
×
NEW
333
}
×
334

335
// Add pre-action hook for commands that need existing captable
NEW
336
const commandsThatNeedCaptable = [
×
NEW
337
  'stakeholder',
×
NEW
338
  'security:add',
×
NEW
339
  'issue',
×
NEW
340
  'grant',
×
NEW
341
  'safe',
×
NEW
342
  'safes',
×
NEW
343
  'convert',
×
NEW
344
  'chart',
×
NEW
345
  'export',
×
NEW
346
  'report',
×
NEW
347
  'log',
×
NEW
348
  'list',
×
NEW
349
  'enlist',
×
NEW
350
];
×
351

NEW
352
program.commands.forEach((cmd) => {
×
NEW
353
  if (commandsThatNeedCaptable.includes(cmd.name())) {
×
NEW
354
    cmd.hook('preAction', ensureCaptableExists);
×
NEW
355
  }
×
NEW
356
});
×
357

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

© 2026 Coveralls, Inc