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

acossta / captan / 17080736335

19 Aug 2025 08:14PM UTC coverage: 77.606% (-0.5%) from 78.148%
17080736335

Pull #13

github

web-flow
Merge f10b3aef7 into a4cd928d6
Pull Request #13: feat: Enhanced JSON Schema Validation with Business Rules

546 of 624 branches covered (87.5%)

Branch coverage included in aggregate %.

299 of 404 new or added lines in 4 files covered. (74.01%)

2015 of 2676 relevant lines covered (75.3%)

2973.22 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';
×
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) => {
×
36
    const result = await handlers.handleInit(opts);
×
37
    if (!result.success) {
×
38
      console.error(result.message);
×
39
      process.exit(1);
×
40
    }
×
41
    console.log(result.message);
×
42
  });
×
43

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

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

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

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

98
// Issue command
99
program
×
100
  .command('issue')
×
101
  .description('Issue shares')
×
102
  .requiredOption('--holder <id>', 'stakeholder ID')
×
103
  .option('--security <id>', 'security class ID (defaults to common)')
×
104
  .requiredOption('-q, --qty <amount>', 'number of shares')
×
105
  .option('--pps <amount>', 'price per share')
×
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
109
    const mappedOpts = {
×
110
      stakeholder: opts.holder,
×
111
      securityClass: opts.security,
×
112
      qty: opts.qty,
×
113
      price: opts.pps,
×
114
      date: opts.date,
×
115
    };
×
116
    const result = handlers.handleIssue(mappedOpts);
×
117
    if (!result.success) {
×
118
      console.error(result.message);
×
119
      process.exit(1);
×
120
    }
×
121
    console.log(result.message);
×
122
  });
×
123

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

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

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

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

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

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

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

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

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

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

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

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

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

381
program.commands.forEach((cmd) => {
×
382
  if (commandsThatNeedCaptable.includes(cmd.name())) {
×
383
    cmd.hook('preAction', ensureCaptableExists);
×
384
  }
×
385
});
×
386

387
// Parse and execute
388
program.parseAsync(process.argv).catch((error) => {
×
389
  console.error(`❌ ${error.message}`);
×
390
  process.exit(1);
×
391
});
×
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