captan
Version:
Captan — Command your ownership. A tiny, hackable CLI cap table tool.
289 lines • 11.1 kB
JavaScript
/**
* Security Class Resource Handlers
*
* Handles all security class-related commands:
* - add: Create a new security class
* - list: List all security classes
* - show: Show security class details
* - update: Update security class information
* - delete: Remove a security class
*/
import * as helpers from '../services/helpers.js';
import { load, save } from '../store.js';
export function handleSecurityAdd(opts) {
try {
const captable = load('captable.json');
const kind = opts.kind.toUpperCase();
if (!['COMMON', 'PREFERRED', 'OPTION_POOL'].includes(kind)) {
return {
success: false,
message: '❌ Invalid security kind. Must be COMMON, PREFERRED, or OPTION_POOL.',
};
}
const authorized = parseInt(opts.authorized || '10000000', 10);
const par = opts.par ? parseFloat(opts.par) : undefined;
// Validate authorized shares
if (!Number.isFinite(authorized) || authorized <= 0) {
return {
success: false,
message: '❌ Invalid authorized shares. Please provide a positive integer.',
};
}
// Validate par value if provided
if (par !== undefined && (!Number.isFinite(par) || par < 0)) {
return {
success: false,
message: '❌ Invalid par value. Please provide a non-negative number.',
};
}
const security = helpers.createSecurityClass(kind, opts.label, authorized, par);
captable.securityClasses.push(security);
helpers.logAction(captable, {
action: 'SECURITY_ADD',
entity: 'security',
entityId: security.id,
details: `Added ${kind} security class: ${opts.label} (${authorized.toLocaleString('en-US')} authorized)`,
});
save(captable, 'captable.json');
return {
success: true,
message: `✅ Added security class "${opts.label}" (${security.id})`,
data: security,
};
}
catch (error) {
const msg = error instanceof Error ? error.message : String(error);
return {
success: false,
message: `❌ Error: ${msg}`,
};
}
}
export function handleSecurityList(opts) {
try {
const captable = load('captable.json');
if (opts.format === 'json') {
return {
success: true,
message: JSON.stringify(captable.securityClasses, null, 2),
data: captable.securityClasses,
};
}
// Table format
if (captable.securityClasses.length === 0) {
return {
success: true,
message: 'No security classes found.',
};
}
let output = '🏦 Security Classes\n\n';
output += 'ID Type Label Authorized Issued\n';
output += '─'.repeat(85) + '\n';
for (const sc of captable.securityClasses) {
const issued = helpers.getIssuedShares(captable, sc.id);
const id = sc.id.padEnd(14);
const type = sc.kind.padEnd(12);
const label = sc.label.substring(0, 22).padEnd(22);
const auth = sc.authorized.toLocaleString('en-US').padStart(14);
const iss = issued.toLocaleString('en-US').padStart(14);
output += `${id} ${type} ${label} ${auth} ${iss}\n`;
}
return {
success: true,
message: output,
data: captable.securityClasses,
};
}
catch (error) {
const msg = error instanceof Error ? error.message : String(error);
return {
success: false,
message: `❌ Error: ${msg}`,
};
}
}
export function handleSecurityShow(id, _opts) {
try {
if (!id) {
return {
success: false,
message: '❌ Please provide a security class ID',
};
}
const captable = load('captable.json');
const security = captable.securityClasses.find((sc) => sc.id === id);
if (!security) {
return {
success: false,
message: `❌ Security class not found: ${id}`,
};
}
const issued = helpers.getIssuedShares(captable, security.id);
const available = security.authorized - issued;
const utilization = security.authorized > 0 ? ((issued / security.authorized) * 100).toFixed(1) : '0.0';
let output = `\n🏦 Security Class Details\n\n`;
output += `Label: ${security.label}\n`;
output += `ID: ${security.id}\n`;
output += `Type: ${security.kind}\n`;
output += `Authorized: ${security.authorized.toLocaleString('en-US')}\n`;
output += `Issued: ${issued.toLocaleString('en-US')}\n`;
output += `Available: ${available.toLocaleString('en-US')}\n`;
output += `Utilization: ${utilization}%\n`;
if (security.parValue !== undefined) {
output += `Par Value: $${security.parValue}\n`;
}
// Show issuances
const issuances = captable.issuances.filter((i) => i.securityClassId === security.id);
if (issuances.length > 0) {
output += `\n📊 Issuances:\n`;
issuances.forEach((iss) => {
const holder = captable.stakeholders.find((sh) => sh.id === iss.stakeholderId);
output += ` • ${iss.qty.toLocaleString('en-US')} shares to ${holder?.name || 'Unknown'}`;
if (iss.pps) {
// Fixed: pricePerShare -> pps
output += ` at $${iss.pps}/share`;
}
output += `\n`;
});
}
return {
success: true,
message: output,
data: { security, issued, available },
};
}
catch (error) {
const msg = error instanceof Error ? error.message : String(error);
return {
success: false,
message: `❌ Error: ${msg}`,
};
}
}
export function handleSecurityUpdate(id, opts) {
try {
if (!id) {
return {
success: false,
message: '❌ Please provide a security class ID',
};
}
const captable = load('captable.json');
const security = captable.securityClasses.find((sc) => sc.id === id);
if (!security) {
return {
success: false,
message: `❌ Security class not found: ${id}`,
};
}
const updates = [];
if (opts.authorized !== undefined) {
const newAuthorized = parseInt(opts.authorized, 10);
// Validate authorized shares
if (!Number.isFinite(newAuthorized) || newAuthorized <= 0) {
return {
success: false,
message: '❌ Invalid authorized shares. Please provide a positive integer.',
};
}
const issued = helpers.getIssuedShares(captable, security.id);
if (newAuthorized < issued) {
return {
success: false,
message: `❌ Cannot set authorized (${newAuthorized.toLocaleString('en-US')}) below issued (${issued.toLocaleString('en-US')})`,
};
}
security.authorized = newAuthorized;
updates.push(`authorized to ${newAuthorized.toLocaleString('en-US')}`);
}
if (opts.label) {
security.label = opts.label;
updates.push(`label to "${opts.label}"`);
}
if (updates.length === 0) {
return {
success: false,
message: '❌ No updates provided. Use --authorized or --label to update.',
};
}
helpers.logAction(captable, {
action: 'SECURITY_UPDATE',
entity: 'security',
entityId: security.id,
details: `Updated ${updates.join(' and ')}`,
});
save(captable, 'captable.json');
return {
success: true,
message: `✅ Updated security class "${security.label}" (${security.id})`,
data: security,
};
}
catch (error) {
const msg = error instanceof Error ? error.message : String(error);
return {
success: false,
message: `❌ Error: ${msg}`,
};
}
}
export function handleSecurityDelete(id, opts) {
try {
if (!id) {
return {
success: false,
message: '❌ Please provide a security class ID',
};
}
const captable = load('captable.json');
const index = captable.securityClasses.findIndex((sc) => sc.id === id);
if (index === -1) {
return {
success: false,
message: `❌ Security class not found: ${id}`,
};
}
const security = captable.securityClasses[index];
const issued = helpers.getIssuedShares(captable, security.id);
if (issued > 0 && !opts.force) {
return {
success: false,
message: `❌ Security class has ${issued.toLocaleString('en-US')} issued shares. Use --force to delete anyway.`,
};
}
captable.securityClasses.splice(index, 1);
// If forced, also remove all related issuances
if (opts.force) {
captable.issuances = captable.issuances.filter((i) => i.securityClassId !== security.id);
// For option pools, also remove grants
// Note: optionPoolId doesn't exist in the model, so we can't filter by it
// Instead, when deleting an option pool, we should remove all grants if it's the only pool
if (security.kind === 'OPTION_POOL') {
const remainingPools = captable.securityClasses.filter((sc) => sc.kind === 'OPTION_POOL');
if (remainingPools.length === 0) {
// No more pools, remove all grants
captable.optionGrants = [];
}
}
}
helpers.logAction(captable, {
action: 'SECURITY_DELETE',
entity: 'security',
entityId: security.id,
details: `Deleted security class: ${security.label}${opts.force ? ' (forced, removed all issuances)' : ''}`,
});
save(captable, 'captable.json');
return {
success: true,
message: `✅ Deleted security class "${security.label}" (${security.id})`,
};
}
catch (error) {
const msg = error instanceof Error ? error.message : String(error);
return {
success: false,
message: `❌ Error: ${msg}`,
};
}
}
//# sourceMappingURL=security.handlers.js.map