captan
Version:
Captan — Command your ownership. A tiny, hackable CLI cap table tool.
199 lines • 6.03 kB
JavaScript
/**
* Identifier Resolution System
*
* Provides utilities to resolve stakeholder identifiers that can be either:
* - A prefixed ID (e.g., "sh_alice")
* - An email address (e.g., "alice@example.com")
*
* This allows users to reference stakeholders using whichever identifier
* is more convenient for their workflow.
*/
import { load } from './store.js';
/**
* Determines if a string is likely an email address
*/
export function isEmail(identifier) {
// Basic email pattern check
const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailPattern.test(identifier);
}
/**
* Determines if a string is a prefixed ID
*/
export function isPrefixedId(identifier) {
// Check for pattern: prefix_identifier
const idPattern = /^[a-z]+_[a-zA-Z0-9-]+$/;
return idPattern.test(identifier);
}
/**
* Resolves a stakeholder identifier (ID or email) to a Stakeholder object
*/
export function resolveStakeholder(identifier) {
if (!identifier) {
return {
success: false,
error: 'No identifier provided',
};
}
// Trim input to avoid whitespace issues
const input = identifier.trim();
try {
const captable = load('captable.json');
// Determine lookup method based on identifier format
let stakeholder;
if (isEmail(input)) {
// Lookup by email (case-insensitive)
const lowerInput = input.toLowerCase();
stakeholder = captable.stakeholders.find((sh) => sh.email?.toLowerCase() === lowerInput);
if (!stakeholder) {
return {
success: false,
error: `No stakeholder found with email: ${input}`,
};
}
}
else if (isPrefixedId(input)) {
// Lookup by ID (case-sensitive)
stakeholder = captable.stakeholders.find((sh) => sh.id === input);
if (!stakeholder) {
return {
success: false,
error: `No stakeholder found with ID: ${input}`,
};
}
}
else {
// Try both methods as fallback (case-insensitive for email)
const lowerInput = input.toLowerCase();
stakeholder = captable.stakeholders.find((sh) => sh.id === input || sh.email?.toLowerCase() === lowerInput);
if (!stakeholder) {
return {
success: false,
error: `No stakeholder found with identifier: ${input}`,
};
}
}
return {
success: true,
stakeholder,
};
}
catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : 'Failed to load captable',
};
}
}
/**
* Resolves multiple stakeholder identifiers
*/
export function resolveStakeholders(identifiers) {
const stakeholders = [];
const errors = [];
for (const identifier of identifiers) {
const result = resolveStakeholder(identifier);
if (result.success && result.stakeholder) {
stakeholders.push(result.stakeholder);
}
else {
errors.push(result.error || `Failed to resolve: ${identifier}`);
}
}
return {
success: errors.length === 0,
stakeholders,
errors,
};
}
/**
* Gets a display name for a stakeholder identifier
* This is useful for error messages and confirmations
*/
export function getIdentifierDisplay(identifier) {
if (isEmail(identifier)) {
return `email '${identifier}'`;
}
else if (isPrefixedId(identifier)) {
return `ID '${identifier}'`;
}
else {
return `'${identifier}'`;
}
}
/**
* Validates that an identifier can be used for stakeholder lookup
*/
export function validateIdentifier(identifier) {
if (!identifier || identifier.trim() === '') {
return {
valid: false,
error: 'Identifier cannot be empty',
};
}
if (isEmail(identifier)) {
return {
valid: true,
type: 'email',
};
}
if (isPrefixedId(identifier)) {
return {
valid: true,
type: 'id',
};
}
// Could still be valid, just not a standard format
return {
valid: true,
type: undefined,
};
}
/**
* Suggests similar stakeholders when resolution fails
* Useful for providing helpful error messages
*/
export function suggestSimilarStakeholders(identifier, limit = 3) {
try {
const captable = load('captable.json');
const lowerIdentifier = identifier.toLowerCase();
// Score each stakeholder based on similarity
const scored = captable.stakeholders.map((sh) => {
let score = 0;
// Check ID similarity
if (sh.id.toLowerCase().includes(lowerIdentifier)) {
score += 2;
}
// Check email similarity
if (sh.email && sh.email.toLowerCase().includes(lowerIdentifier)) {
score += 2;
}
// Check name similarity
if (sh.name.toLowerCase().includes(lowerIdentifier)) {
score += 1;
}
return { stakeholder: sh, score };
});
// Sort by score and return top matches
return scored
.filter((s) => s.score > 0)
.sort((a, b) => b.score - a.score)
.slice(0, limit)
.map((s) => s.stakeholder);
}
catch {
return [];
}
}
/**
* Formats a stakeholder for display with both ID and email
*/
export function formatStakeholderReference(stakeholder) {
if (stakeholder.email) {
return `${stakeholder.name} (${stakeholder.id}, ${stakeholder.email})`;
}
else {
return `${stakeholder.name} (${stakeholder.id})`;
}
}
//# sourceMappingURL=identifier-resolver.js.map