📦 deps(thirdparty): update snapshots
This commit is contained in:
@@ -0,0 +1,183 @@
|
||||
---
|
||||
name: stack-audit
|
||||
version: 0.1.0
|
||||
description: Scan codebase for Outfitter Stack adoption candidates. Identifies throw statements, console usage, hardcoded paths, and custom errors. Use when assessing adoption scope or checking readiness.
|
||||
context: fork
|
||||
agent: stacker
|
||||
allowed-tools: Read Grep Glob Bash(rg *) Bash(bun *)
|
||||
---
|
||||
|
||||
# Stack Audit
|
||||
|
||||
Scan a codebase to identify Outfitter Stack adoption candidates and generate an audit report.
|
||||
|
||||
## Quick Start
|
||||
|
||||
**Option 1: Run the scanner** (recommended for large projects)
|
||||
|
||||
```bash
|
||||
bun run plugins/outfitter-stack/skills/stack-audit/scripts/init-audit.ts [project-root]
|
||||
```
|
||||
|
||||
Generates `.outfitter/adopt/` with:
|
||||
- `audit-report.md` - Scan results and scope
|
||||
- `plan/` - Stage-by-stage task files
|
||||
|
||||
**Option 2: Manual scan** (smaller projects)
|
||||
|
||||
Run the audit commands below to understand scope.
|
||||
|
||||
## Audit Commands
|
||||
|
||||
### Critical Issues - Exceptions
|
||||
|
||||
```bash
|
||||
# Count throw statements
|
||||
rg "throw (new |[a-zA-Z])" --type ts -c
|
||||
|
||||
# List throw locations
|
||||
rg "throw (new |[a-zA-Z])" --type ts -n
|
||||
|
||||
# Count try-catch blocks
|
||||
rg "(try \{|catch \()" --type ts -c
|
||||
```
|
||||
|
||||
### Console Usage
|
||||
|
||||
```bash
|
||||
# Count console statements
|
||||
rg "console\.(log|error|warn|debug|info)" --type ts -c
|
||||
|
||||
# List console locations
|
||||
rg "console\.(log|error|warn|debug|info)" --type ts -n
|
||||
```
|
||||
|
||||
### Hardcoded Paths
|
||||
|
||||
```bash
|
||||
# Homedir usage
|
||||
rg "(homedir\(\)|os\.homedir)" --type ts -c
|
||||
|
||||
# Tilde paths
|
||||
rg "~/\." --type ts -c
|
||||
|
||||
# Combined path issues
|
||||
rg "(homedir|~\/\.)" --type ts -n
|
||||
```
|
||||
|
||||
### Custom Error Classes
|
||||
|
||||
```bash
|
||||
# Find custom error classes
|
||||
rg "class \w+Error extends Error" --type ts -n
|
||||
|
||||
# Count usage of custom errors
|
||||
rg "new MyCustomError\(" --type ts -c
|
||||
```
|
||||
|
||||
## Generated Structure
|
||||
|
||||
```
|
||||
.outfitter/adopt/
|
||||
├── audit-report.md # Scan results, scope, recommendations
|
||||
└── plan/
|
||||
├── 00-overview.md # Status dashboard, dependencies
|
||||
├── 01-foundation.md # Dependencies, context, logger
|
||||
├── 02-handlers.md # Handler conversions
|
||||
├── 03-errors.md # Error taxonomy mappings
|
||||
├── 04-paths.md # XDG path migrations
|
||||
├── 05-adapters.md # CLI/MCP transport layers
|
||||
├── 06-documents.md # Documentation updates
|
||||
└── 99-unknowns.md # Items requiring review
|
||||
```
|
||||
|
||||
## Migration Stages
|
||||
|
||||
| Stage | Blocked By | Focus |
|
||||
|-------|------------|-------|
|
||||
| 1. Foundation | - | Install packages, create context/logger |
|
||||
| 2. Handlers | Foundation | Convert throw to Result |
|
||||
| 3. Errors | Handlers | Map to error taxonomy |
|
||||
| 4. Paths | - | XDG paths, securePath |
|
||||
| 5. Adapters | Handlers | CLI/MCP wrappers |
|
||||
| 6. Documents | All | Update docs to reflect patterns |
|
||||
| 99. Unknowns | - | Review anytime |
|
||||
|
||||
## Audit Report Fields
|
||||
|
||||
| Field | Description |
|
||||
|-------|-------------|
|
||||
| Exceptions | `throw` statements to convert to Result |
|
||||
| Try/Catch | Error handling blocks to restructure |
|
||||
| Console | Logging to convert to structured logging |
|
||||
| Paths | Hardcoded paths to convert to XDG |
|
||||
| Error Classes | Custom errors to map to taxonomy |
|
||||
| Handlers | Functions with throws to convert |
|
||||
| Unknowns | Complex patterns requiring review |
|
||||
|
||||
## Error Taxonomy Reference
|
||||
|
||||
When mapping errors, use this reference:
|
||||
|
||||
| Original | Outfitter | Category |
|
||||
|----------|-----------|----------|
|
||||
| `NotFoundError` | `NotFoundError` | `not_found` |
|
||||
| `InvalidInputError` | `ValidationError` | `validation` |
|
||||
| `DuplicateError` | `ConflictError` | `conflict` |
|
||||
| `UnauthorizedError` | `AuthError` | `auth` |
|
||||
| `ForbiddenError` | `PermissionError` | `permission` |
|
||||
| Generic `Error` | `InternalError` | `internal` |
|
||||
|
||||
## Effort Estimation
|
||||
|
||||
| Count | Effort Level |
|
||||
|-------|--------------|
|
||||
| 0 | None |
|
||||
| 1-5 | Low |
|
||||
| 6-15 | Medium |
|
||||
| 16+ | High |
|
||||
|
||||
## Interpreting Results
|
||||
|
||||
### High-Priority Items
|
||||
|
||||
- Functions with 3+ throw statements (complex error handling)
|
||||
- Files with 3+ try-catch blocks (may need restructuring)
|
||||
- Custom error classes with high usage counts
|
||||
|
||||
### Medium-Priority Items
|
||||
|
||||
- Isolated throw statements (simple conversions)
|
||||
- Console logging (straightforward migration)
|
||||
- Hardcoded paths (mechanical replacement)
|
||||
|
||||
### Low-Priority Items
|
||||
|
||||
- Documentation updates (can happen last)
|
||||
- Test file updates (follow handler changes)
|
||||
|
||||
## Next Steps After Audit
|
||||
|
||||
1. Review `audit-report.md` for accuracy
|
||||
2. Adjust priorities in `plan/00-overview.md`
|
||||
3. Begin with Stage 1 (Foundation)
|
||||
4. Load `outfitter-stack:stack-patterns` for conversion guidance
|
||||
5. Load `outfitter-stack:stack-templates` for scaffolding
|
||||
|
||||
## Constraints
|
||||
|
||||
**Always:**
|
||||
- Run audit before planning adoption
|
||||
- Review unknowns for complex patterns
|
||||
- Estimate effort before committing
|
||||
|
||||
**Never:**
|
||||
- Skip the audit phase
|
||||
- Underestimate try-catch complexity
|
||||
- Ignore custom error classes
|
||||
|
||||
## Related Skills
|
||||
|
||||
- `outfitter-stack:stack-patterns` - Target patterns reference
|
||||
- `outfitter-stack:stack-templates` - Component templates
|
||||
- `outfitter-stack:stack-review` - Verify compliance
|
||||
+545
@@ -0,0 +1,545 @@
|
||||
#!/usr/bin/env bun
|
||||
/**
|
||||
* Stack Audit Scanner & Plan Generator
|
||||
*
|
||||
* Scans a codebase for Outfitter Stack adoption candidates and generates
|
||||
* a structured audit report with stage-specific task files.
|
||||
*
|
||||
* Usage:
|
||||
* bun run init-audit.ts [project-root]
|
||||
*/
|
||||
|
||||
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
||||
import { basename, dirname, join } from "node:path";
|
||||
|
||||
// Constants
|
||||
const FUNCTION_PROXIMITY_LINES = 50; // How close a throw must be to a function to be associated
|
||||
|
||||
// Types
|
||||
interface ScanResult {
|
||||
file: string;
|
||||
line: number;
|
||||
content: string;
|
||||
}
|
||||
|
||||
interface HandlerInfo {
|
||||
name: string;
|
||||
file: string;
|
||||
line: number;
|
||||
signature: string;
|
||||
throws: string[];
|
||||
priority: "high" | "medium" | "low";
|
||||
}
|
||||
|
||||
interface ErrorClassInfo {
|
||||
name: string;
|
||||
file: string;
|
||||
line: number;
|
||||
usageCount: number;
|
||||
suggestedMapping: string;
|
||||
}
|
||||
|
||||
interface PathUsage {
|
||||
file: string;
|
||||
line: number;
|
||||
current: string;
|
||||
pattern: "homedir" | "tilde" | "hardcoded";
|
||||
}
|
||||
|
||||
interface Unknown {
|
||||
id: string;
|
||||
title: string;
|
||||
file: string;
|
||||
line: number;
|
||||
priority: "high" | "medium" | "low";
|
||||
category: string;
|
||||
code: string;
|
||||
reason: string;
|
||||
options: string[];
|
||||
}
|
||||
|
||||
interface ScanData {
|
||||
projectName: string;
|
||||
date: string;
|
||||
throws: ScanResult[];
|
||||
tryCatch: ScanResult[];
|
||||
console: ScanResult[];
|
||||
paths: PathUsage[];
|
||||
errorClasses: ErrorClassInfo[];
|
||||
handlers: HandlerInfo[];
|
||||
docs: string[];
|
||||
unknowns: Unknown[];
|
||||
}
|
||||
|
||||
// Scanner functions
|
||||
async function runRg(
|
||||
pattern: string,
|
||||
options: string[] = []
|
||||
): Promise<ScanResult[]> {
|
||||
try {
|
||||
const proc = Bun.spawn(["rg", pattern, "--type", "ts", "-n", ...options], {
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
const output = await new Response(proc.stdout).text();
|
||||
const exitCode = await proc.exited;
|
||||
|
||||
// Exit code 1 means no matches (not an error), 2+ means actual error
|
||||
if (exitCode > 1) {
|
||||
console.error(`Warning: rg failed with exit code ${exitCode}. Is ripgrep installed?`);
|
||||
return [];
|
||||
}
|
||||
|
||||
const results: ScanResult[] = [];
|
||||
|
||||
for (const line of output.split("\n").filter(Boolean)) {
|
||||
const match = line.match(/^(.+?):(\d+):(.*)$/);
|
||||
if (match) {
|
||||
results.push({
|
||||
file: match[1],
|
||||
line: Number.parseInt(match[2], 10),
|
||||
content: match[3].trim(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
} catch (error) {
|
||||
console.error("Warning: Failed to run rg. Is ripgrep installed?", error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async function countMatches(pattern: string): Promise<number> {
|
||||
try {
|
||||
const proc = Bun.spawn(["rg", pattern, "--type", "ts", "-c"], {
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
const output = await new Response(proc.stdout).text();
|
||||
const exitCode = await proc.exited;
|
||||
|
||||
// Exit code 1 means no matches (not an error), 2+ means actual error
|
||||
if (exitCode > 1) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
let total = 0;
|
||||
|
||||
for (const line of output.split("\n").filter(Boolean)) {
|
||||
// rg -c outputs "file:count" for multiple files, or just "count" for single file
|
||||
const colonIndex = line.lastIndexOf(":");
|
||||
if (colonIndex !== -1) {
|
||||
// file:count format
|
||||
const count = Number.parseInt(line.slice(colonIndex + 1), 10);
|
||||
if (!Number.isNaN(count)) total += count;
|
||||
} else {
|
||||
// just count (single file case)
|
||||
const count = Number.parseInt(line, 10);
|
||||
if (!Number.isNaN(count)) total += count;
|
||||
}
|
||||
}
|
||||
|
||||
return total;
|
||||
} catch {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
async function scanThrows(): Promise<ScanResult[]> {
|
||||
return runRg("throw (new |[a-zA-Z])");
|
||||
}
|
||||
|
||||
async function scanTryCatch(): Promise<ScanResult[]> {
|
||||
return runRg("(try \\{|catch \\()");
|
||||
}
|
||||
|
||||
async function scanConsole(): Promise<ScanResult[]> {
|
||||
return runRg("console\\.(log|error|warn|debug|info)");
|
||||
}
|
||||
|
||||
async function scanPaths(): Promise<PathUsage[]> {
|
||||
const homedirResults = await runRg("(homedir\\(\\)|os\\.homedir)");
|
||||
const tildeResults = await runRg("~/\\.");
|
||||
|
||||
const paths: PathUsage[] = [];
|
||||
|
||||
for (const r of homedirResults) {
|
||||
paths.push({
|
||||
file: r.file,
|
||||
line: r.line,
|
||||
current: r.content,
|
||||
pattern: "homedir",
|
||||
});
|
||||
}
|
||||
|
||||
for (const r of tildeResults) {
|
||||
paths.push({
|
||||
file: r.file,
|
||||
line: r.line,
|
||||
current: r.content,
|
||||
pattern: "tilde",
|
||||
});
|
||||
}
|
||||
|
||||
return paths;
|
||||
}
|
||||
|
||||
async function scanErrorClasses(): Promise<ErrorClassInfo[]> {
|
||||
const results = await runRg("class (\\w+Error) extends Error");
|
||||
const classes: ErrorClassInfo[] = [];
|
||||
|
||||
for (const r of results) {
|
||||
const match = r.content.match(/class (\w+Error)/);
|
||||
if (match) {
|
||||
const name = match[1];
|
||||
const usages = await countMatches(`new ${name}\\(`);
|
||||
|
||||
classes.push({
|
||||
name,
|
||||
file: r.file,
|
||||
line: r.line,
|
||||
usageCount: usages,
|
||||
suggestedMapping: suggestErrorMapping(name),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return classes;
|
||||
}
|
||||
|
||||
function suggestErrorMapping(name: string): string {
|
||||
const lower = name.toLowerCase();
|
||||
|
||||
if (lower.includes("notfound") || lower.includes("missing"))
|
||||
return "NotFoundError";
|
||||
if (
|
||||
lower.includes("validation") ||
|
||||
lower.includes("invalid") ||
|
||||
lower.includes("input")
|
||||
)
|
||||
return "ValidationError";
|
||||
if (
|
||||
lower.includes("conflict") ||
|
||||
lower.includes("duplicate") ||
|
||||
lower.includes("exists")
|
||||
)
|
||||
return "ConflictError";
|
||||
if (lower.includes("permission") || lower.includes("forbidden"))
|
||||
return "PermissionError";
|
||||
if (lower.includes("timeout")) return "TimeoutError";
|
||||
if (lower.includes("ratelimit") || lower.includes("rate"))
|
||||
return "RateLimitError";
|
||||
if (lower.includes("network") || lower.includes("connection"))
|
||||
return "NetworkError";
|
||||
if (
|
||||
lower.includes("auth") ||
|
||||
lower.includes("unauthorized") ||
|
||||
lower.includes("unauthenticated")
|
||||
)
|
||||
return "AuthError";
|
||||
if (lower.includes("cancel")) return "CancelledError";
|
||||
|
||||
return "InternalError";
|
||||
}
|
||||
|
||||
async function scanHandlers(throws: ScanResult[]): Promise<HandlerInfo[]> {
|
||||
const handlers: HandlerInfo[] = [];
|
||||
const fileThrows = new Map<string, ScanResult[]>();
|
||||
|
||||
// Group throws by file
|
||||
for (const t of throws) {
|
||||
const existing = fileThrows.get(t.file) || [];
|
||||
existing.push(t);
|
||||
fileThrows.set(t.file, existing);
|
||||
}
|
||||
|
||||
// Find functions containing throws
|
||||
// NOTE: This regex finds common function patterns but may miss:
|
||||
// - Arrow functions without const (e.g., assigned to object properties)
|
||||
// - Class methods
|
||||
// - export default function
|
||||
// These limitations are acceptable for audit purposes; manual review catches edge cases.
|
||||
for (const [file, fileResults] of fileThrows) {
|
||||
const funcResults = await runRg(
|
||||
"(async )?(function |const )\\w+.*=.*async|async \\w+\\(",
|
||||
[file]
|
||||
);
|
||||
|
||||
for (const func of funcResults) {
|
||||
const nameMatch = func.content.match(
|
||||
/(function |const )(\w+)|async (\w+)\(/
|
||||
);
|
||||
if (nameMatch) {
|
||||
const name = nameMatch[2] || nameMatch[3];
|
||||
const nearbyThrows = fileResults.filter(
|
||||
(t) => Math.abs(t.line - func.line) < FUNCTION_PROXIMITY_LINES
|
||||
);
|
||||
|
||||
if (nearbyThrows.length > 0) {
|
||||
handlers.push({
|
||||
name,
|
||||
file: func.file,
|
||||
line: func.line,
|
||||
signature: func.content.slice(0, 80),
|
||||
throws: nearbyThrows.map((t) => t.content),
|
||||
priority:
|
||||
nearbyThrows.length > 3
|
||||
? "high"
|
||||
: nearbyThrows.length > 1
|
||||
? "medium"
|
||||
: "low",
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return handlers;
|
||||
}
|
||||
|
||||
async function scanDocs(): Promise<string[]> {
|
||||
const proc = Bun.spawn(["find", ".", "-name", "*.md", "-type", "f"], {
|
||||
stdout: "pipe",
|
||||
});
|
||||
const output = await new Response(proc.stdout).text();
|
||||
return output
|
||||
.split("\n")
|
||||
.filter(Boolean)
|
||||
.filter((f) => !f.includes("node_modules"));
|
||||
}
|
||||
|
||||
function identifyUnknowns(data: Partial<ScanData>): Unknown[] {
|
||||
const unknowns: Unknown[] = [];
|
||||
let id = 1;
|
||||
|
||||
// Complex try-catch (nested or multi-catch)
|
||||
const tryCatch = data.tryCatch || [];
|
||||
const tryCatchFiles = new Map<string, number>();
|
||||
for (const t of tryCatch) {
|
||||
tryCatchFiles.set(t.file, (tryCatchFiles.get(t.file) || 0) + 1);
|
||||
}
|
||||
|
||||
for (const [file, count] of tryCatchFiles) {
|
||||
if (count > 3) {
|
||||
unknowns.push({
|
||||
id: `U${id++}`,
|
||||
title: `Complex try-catch in ${basename(file)}`,
|
||||
file,
|
||||
line: 0,
|
||||
priority: "medium",
|
||||
category: "complex-pattern",
|
||||
code: `${count} try-catch blocks`,
|
||||
reason: "Multiple try-catch blocks may need manual restructuring",
|
||||
options: [
|
||||
"Convert each to Result-returning helper",
|
||||
"Combine into single Result chain",
|
||||
"Use wrapAsync for third-party calls",
|
||||
],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return unknowns;
|
||||
}
|
||||
|
||||
// Template rendering
|
||||
function render(template: string, data: Record<string, unknown>): string {
|
||||
let result = template;
|
||||
|
||||
// Simple variable replacement
|
||||
result = result.replace(/\{\{(\w+)\}\}/g, (_, key) => {
|
||||
const value = data[key];
|
||||
if (value === undefined) return `{{${key}}}`;
|
||||
return String(value);
|
||||
});
|
||||
|
||||
// Handle {{#each}} blocks
|
||||
result = result.replace(
|
||||
/\{\{#each (\w+)\}\}([\s\S]*?)\{\{\/each\}\}/g,
|
||||
(_, key, content) => {
|
||||
const items = data[key] as unknown[];
|
||||
if (!(items && Array.isArray(items))) return "";
|
||||
return items
|
||||
.map((item) => render(content, item as Record<string, unknown>))
|
||||
.join("");
|
||||
}
|
||||
);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// File generation
|
||||
function generateAuditReport(data: ScanData): string {
|
||||
const templatePath = join(
|
||||
dirname(import.meta.path),
|
||||
"../templates/audit-report.md"
|
||||
);
|
||||
const template = readFileSync(templatePath, "utf-8");
|
||||
|
||||
return render(template, {
|
||||
PROJECT_NAME: data.projectName,
|
||||
DATE: data.date,
|
||||
THROW_COUNT: data.throws.length,
|
||||
THROW_FILES: [...new Set(data.throws.map((t) => t.file))].length,
|
||||
TRY_CATCH_COUNT: data.tryCatch.length,
|
||||
TRY_CATCH_FILES: [...new Set(data.tryCatch.map((t) => t.file))].length,
|
||||
CONSOLE_COUNT: data.console.length,
|
||||
CONSOLE_FILES: [...new Set(data.console.map((t) => t.file))].length,
|
||||
PATH_COUNT: data.paths.length,
|
||||
PATH_FILES: [...new Set(data.paths.map((p) => p.file))].length,
|
||||
ERROR_CLASS_COUNT: data.errorClasses.length,
|
||||
DOC_COUNT: data.docs.length,
|
||||
UNKNOWN_COUNT: data.unknowns.length,
|
||||
HANDLER_COUNT: data.handlers.length,
|
||||
HANDLER_EFFORT: effortLevel(data.handlers.length),
|
||||
ERROR_EFFORT: effortLevel(data.errorClasses.length * 2),
|
||||
PATH_EFFORT: effortLevel(data.paths.length),
|
||||
ADAPTER_COUNT: 0,
|
||||
ADAPTER_EFFORT: "TBD",
|
||||
DOC_EFFORT: effortLevel(data.docs.length),
|
||||
});
|
||||
}
|
||||
|
||||
function effortLevel(count: number): string {
|
||||
if (count === 0) return "None";
|
||||
if (count <= 5) return "Low";
|
||||
if (count <= 15) return "Medium";
|
||||
return "High";
|
||||
}
|
||||
|
||||
function generatePlanFile(stage: string, data: ScanData): string {
|
||||
// Validate stage to prevent path traversal
|
||||
if (!/^[\w-]+\.md$/.test(stage)) {
|
||||
return `# ${stage}\n\nInvalid stage name.`;
|
||||
}
|
||||
|
||||
const templatePath = join(
|
||||
dirname(import.meta.path),
|
||||
`../templates/plan/${stage}`
|
||||
);
|
||||
|
||||
if (!existsSync(templatePath)) {
|
||||
return `# ${stage}\n\nTemplate not found.`;
|
||||
}
|
||||
|
||||
const template = readFileSync(templatePath, "utf-8");
|
||||
|
||||
return render(template, {
|
||||
PROJECT_NAME: data.projectName,
|
||||
DATE: data.date,
|
||||
HANDLER_COUNT: data.handlers.length,
|
||||
ERROR_CLASS_COUNT: data.errorClasses.length,
|
||||
PATH_COUNT: data.paths.length,
|
||||
ADAPTER_COUNT: 0,
|
||||
DOC_COUNT: data.docs.length,
|
||||
UNKNOWN_COUNT: data.unknowns.length,
|
||||
HANDLERS: data.handlers,
|
||||
ERROR_CLASSES: data.errorClasses,
|
||||
PATH_FILES: data.paths,
|
||||
DOC_FILES: data.docs.map((f) => ({
|
||||
file: f,
|
||||
type: "markdown",
|
||||
issues: [],
|
||||
updates: [],
|
||||
})),
|
||||
UNKNOWNS: data.unknowns,
|
||||
CLI_COMMANDS: [],
|
||||
MCP_TOOLS: [],
|
||||
FOUNDATION_NOTES: "",
|
||||
HANDLER_NOTES: "",
|
||||
ERROR_NOTES: "",
|
||||
PATH_NOTES: "",
|
||||
ADAPTER_NOTES: "",
|
||||
DOC_NOTES: "",
|
||||
UNKNOWN_NOTES: "",
|
||||
});
|
||||
}
|
||||
|
||||
// Main
|
||||
async function main() {
|
||||
const projectRoot = process.argv[2] || process.cwd();
|
||||
const projectName = basename(projectRoot);
|
||||
const outputDir = join(projectRoot, ".outfitter", "adopt");
|
||||
|
||||
console.log(`Scanning ${projectName}...`);
|
||||
|
||||
// Run scans
|
||||
const [throws, tryCatch, consoleLog, paths, errorClasses, docs] =
|
||||
await Promise.all([
|
||||
scanThrows(),
|
||||
scanTryCatch(),
|
||||
scanConsole(),
|
||||
scanPaths(),
|
||||
scanErrorClasses(),
|
||||
scanDocs(),
|
||||
]);
|
||||
|
||||
const handlers = await scanHandlers(throws);
|
||||
|
||||
const data: ScanData = {
|
||||
projectName,
|
||||
date: new Date().toISOString().split("T")[0],
|
||||
throws,
|
||||
tryCatch,
|
||||
console: consoleLog,
|
||||
paths,
|
||||
errorClasses,
|
||||
handlers,
|
||||
docs,
|
||||
unknowns: [],
|
||||
};
|
||||
|
||||
data.unknowns = identifyUnknowns(data);
|
||||
|
||||
// Print summary
|
||||
console.log("\nScan Results:");
|
||||
console.log(` Exceptions: ${throws.length}`);
|
||||
console.log(` Try/Catch: ${tryCatch.length}`);
|
||||
console.log(` Console: ${consoleLog.length}`);
|
||||
console.log(` Paths: ${paths.length}`);
|
||||
console.log(` Error Classes: ${errorClasses.length}`);
|
||||
console.log(` Handlers: ${handlers.length}`);
|
||||
console.log(` Docs: ${docs.length}`);
|
||||
console.log(` Unknowns: ${data.unknowns.length}`);
|
||||
|
||||
// Create output directory
|
||||
mkdirSync(join(outputDir, "plan"), { recursive: true });
|
||||
|
||||
// Generate files
|
||||
console.log("\nGenerating audit report...");
|
||||
|
||||
writeFileSync(join(outputDir, "audit-report.md"), generateAuditReport(data));
|
||||
console.log(" Created: audit-report.md");
|
||||
|
||||
const stages = [
|
||||
"00-overview.md",
|
||||
"01-foundation.md",
|
||||
"02-handlers.md",
|
||||
"03-errors.md",
|
||||
"04-paths.md",
|
||||
"05-adapters.md",
|
||||
"06-documents.md",
|
||||
"99-unknowns.md",
|
||||
];
|
||||
|
||||
for (const stage of stages) {
|
||||
const content = generatePlanFile(stage, data);
|
||||
writeFileSync(join(outputDir, "plan", stage), content);
|
||||
console.log(` Created: plan/${stage}`);
|
||||
}
|
||||
|
||||
console.log(`\nAudit report created at: ${outputDir}`);
|
||||
console.log("\nNext steps:");
|
||||
console.log(" 1. Review audit-report.md for scope");
|
||||
console.log(" 2. Adjust priorities in plan/00-overview.md");
|
||||
console.log(" 3. Load outfitter-stack:stack-patterns for conversion guidance");
|
||||
console.log(" 4. Begin adoption with plan/01-foundation.md");
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -0,0 +1,61 @@
|
||||
# Migration Audit Report
|
||||
|
||||
**Project:** {{PROJECT_NAME}}
|
||||
**Date:** {{DATE}}
|
||||
**Generated by:** `@outfitter/migrate init`
|
||||
|
||||
## Summary
|
||||
|
||||
| Category | Count | Files |
|
||||
|----------|-------|-------|
|
||||
| Exceptions (`throw`) | {{THROW_COUNT}} | {{THROW_FILES}} |
|
||||
| Try/Catch blocks | {{TRY_CATCH_COUNT}} | {{TRY_CATCH_FILES}} |
|
||||
| Console logging | {{CONSOLE_COUNT}} | {{CONSOLE_FILES}} |
|
||||
| Hardcoded paths | {{PATH_COUNT}} | {{PATH_FILES}} |
|
||||
| Custom error classes | {{ERROR_CLASS_COUNT}} | — |
|
||||
| Documentation files | {{DOC_COUNT}} | — |
|
||||
| **Unknowns** | {{UNKNOWN_COUNT}} | — |
|
||||
|
||||
## Estimated Effort
|
||||
|
||||
| Stage | Scope | Effort |
|
||||
|-------|-------|--------|
|
||||
| Foundation | Setup | Low |
|
||||
| Handlers | {{HANDLER_COUNT}} functions | {{HANDLER_EFFORT}} |
|
||||
| Errors | {{ERROR_CLASS_COUNT}} classes | {{ERROR_EFFORT}} |
|
||||
| Paths | {{PATH_COUNT}} usages | {{PATH_EFFORT}} |
|
||||
| Adapters | {{ADAPTER_COUNT}} commands/tools | {{ADAPTER_EFFORT}} |
|
||||
| Documents | {{DOC_COUNT}} files | {{DOC_EFFORT}} |
|
||||
| Unknowns | {{UNKNOWN_COUNT}} items | Review required |
|
||||
|
||||
## Dependencies to Add
|
||||
|
||||
```bash
|
||||
bun add @outfitter/contracts @outfitter/logging @outfitter/config
|
||||
```
|
||||
|
||||
Optional:
|
||||
```bash
|
||||
bun add @outfitter/cli # If building CLI
|
||||
bun add @outfitter/mcp # If building MCP server
|
||||
bun add @outfitter/file-ops # If file operations with path security
|
||||
bun add @outfitter/daemon # If building background services
|
||||
```
|
||||
|
||||
## Migration Plan
|
||||
|
||||
See [plan/](./plan/) for stage-by-stage breakdown:
|
||||
|
||||
1. [Foundation](./plan/01-foundation.md) — Dependencies, context, logger
|
||||
2. [Handlers](./plan/02-handlers.md) — Convert to Result-returning handlers
|
||||
3. [Errors](./plan/03-errors.md) — Map to error taxonomy
|
||||
4. [Paths](./plan/04-paths.md) — XDG-compliant paths
|
||||
5. [Adapters](./plan/05-adapters.md) — CLI/MCP transport layers
|
||||
6. [Documents](./plan/06-documents.md) — Update documentation
|
||||
7. [Unknowns](./plan/99-unknowns.md) — Items requiring review
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. Review this report for accuracy
|
||||
2. Adjust priorities in [00-overview.md](./plan/00-overview.md)
|
||||
3. Begin with Stage 1 (Foundation)
|
||||
+87
@@ -0,0 +1,87 @@
|
||||
# Migration Overview
|
||||
|
||||
**Project:** {{PROJECT_NAME}}
|
||||
**Started:** {{DATE}}
|
||||
**Last Updated:** {{DATE}}
|
||||
|
||||
## Status Dashboard
|
||||
|
||||
| Stage | Status | Progress | Blocked By |
|
||||
|-------|--------|----------|------------|
|
||||
| 1. Foundation | ⬜ Not Started | 0/4 | — |
|
||||
| 2. Handlers | ⬜ Not Started | 0/{{HANDLER_COUNT}} | Foundation |
|
||||
| 3. Errors | ⬜ Not Started | 0/{{ERROR_CLASS_COUNT}} | Handlers |
|
||||
| 4. Paths | ⬜ Not Started | 0/{{PATH_COUNT}} | — |
|
||||
| 5. Adapters | ⬜ Not Started | 0/{{ADAPTER_COUNT}} | Handlers |
|
||||
| 6. Documents | ⬜ Not Started | 0/{{DOC_COUNT}} | All |
|
||||
| 99. Unknowns | ⬜ Review | 0/{{UNKNOWN_COUNT}} | — |
|
||||
|
||||
**Status Key:** ⬜ Not Started · 🟡 In Progress · ✅ Complete · 🔴 Blocked · ⏭️ Skipped
|
||||
|
||||
## Stage Dependencies
|
||||
|
||||
```
|
||||
┌─────────────┐
|
||||
│ Foundation │
|
||||
└──────┬──────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────┐ ┌─────────────┐
|
||||
│ Handlers │────▶│ Adapters │
|
||||
└──────┬──────┘ └─────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────┐
|
||||
│ Errors │
|
||||
└─────────────┘
|
||||
|
||||
┌─────────────┐
|
||||
│ Paths │ (independent)
|
||||
└─────────────┘
|
||||
|
||||
┌─────────────┐
|
||||
│ Documents │ (after all stages)
|
||||
└─────────────┘
|
||||
|
||||
┌─────────────┐
|
||||
│ Unknowns │ (review anytime)
|
||||
└─────────────┘
|
||||
```
|
||||
|
||||
## Recommended Order
|
||||
|
||||
1. **Foundation** — Must be first (context, logger)
|
||||
2. **Paths** — Can run parallel with Handlers
|
||||
3. **Handlers** — Core conversion work
|
||||
4. **Errors** — After handlers identify error cases
|
||||
5. **Adapters** — After handlers are converted
|
||||
6. **Unknowns** — Review throughout, resolve before Documents
|
||||
7. **Documents** — Last, after code is stable
|
||||
|
||||
## Progress Log
|
||||
|
||||
| Date | Stage | Work Done | Notes |
|
||||
|------|-------|-----------|-------|
|
||||
| {{DATE}} | — | Generated migration plan | Initial scan |
|
||||
|
||||
## Decisions
|
||||
|
||||
| Decision | Rationale | Date |
|
||||
|----------|-----------|------|
|
||||
|
||||
## Blockers
|
||||
|
||||
| Blocker | Stage | Status | Resolution |
|
||||
|---------|-------|--------|------------|
|
||||
|
||||
## Completion Criteria
|
||||
|
||||
- [ ] All handlers return `Result<T, E>`
|
||||
- [ ] No `throw` statements in application code
|
||||
- [ ] No `console.log` in production code
|
||||
- [ ] All paths use XDG conventions
|
||||
- [ ] All user paths validated with `securePath()`
|
||||
- [ ] CLI uses `output()` and `exitWithError()`
|
||||
- [ ] Documentation reflects new patterns
|
||||
- [ ] All unknowns resolved or documented
|
||||
- [ ] Tests updated and passing
|
||||
+81
@@ -0,0 +1,81 @@
|
||||
# Stage 1: Foundation
|
||||
|
||||
**Status:** ⬜ Not Started
|
||||
**Blocked By:** None
|
||||
**Unlocks:** Handlers, Errors, Adapters
|
||||
|
||||
## Objective
|
||||
|
||||
Install dependencies and create shared infrastructure (context, logger).
|
||||
|
||||
## Tasks
|
||||
|
||||
### 1.1 Install Dependencies
|
||||
|
||||
- [ ] Install core packages
|
||||
```bash
|
||||
bun add @outfitter/contracts @outfitter/logging @outfitter/config
|
||||
```
|
||||
|
||||
- [ ] Install optional packages (as needed)
|
||||
```bash
|
||||
bun add @outfitter/cli # CLI commands
|
||||
bun add @outfitter/mcp # MCP server
|
||||
bun add @outfitter/file-ops # File operations
|
||||
bun add @outfitter/daemon # Background services
|
||||
bun add @outfitter/testing # Test harnesses
|
||||
```
|
||||
|
||||
### 1.2 Create Logger
|
||||
|
||||
- [ ] Create `src/logger.ts`
|
||||
```typescript
|
||||
import { createLogger, createConsoleSink } from "@outfitter/logging";
|
||||
|
||||
export const logger = createLogger({
|
||||
name: "{{PROJECT_NAME}}",
|
||||
level: process.env.LOG_LEVEL || "info",
|
||||
sinks: [createConsoleSink()],
|
||||
redaction: { enabled: true },
|
||||
});
|
||||
```
|
||||
|
||||
### 1.3 Create Context Factory
|
||||
|
||||
- [ ] Create `src/context.ts`
|
||||
```typescript
|
||||
import { createContext } from "@outfitter/contracts";
|
||||
import { logger } from "./logger";
|
||||
|
||||
export const createAppContext = () => createContext({ logger });
|
||||
export type AppContext = ReturnType<typeof createAppContext>;
|
||||
```
|
||||
|
||||
### 1.4 Verify Setup
|
||||
|
||||
- [ ] Create smoke test
|
||||
```typescript
|
||||
import { describe, it, expect } from "bun:test";
|
||||
import { createAppContext } from "../context";
|
||||
|
||||
describe("foundation", () => {
|
||||
it("creates context with logger", () => {
|
||||
const ctx = createAppContext();
|
||||
expect(ctx.logger).toBeDefined();
|
||||
expect(ctx.requestId).toBeDefined();
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
- [ ] Run test: `bun test`
|
||||
|
||||
## Completion Checklist
|
||||
|
||||
- [ ] Core packages installed
|
||||
- [ ] Logger created with redaction enabled
|
||||
- [ ] Context factory created
|
||||
- [ ] Smoke test passing
|
||||
|
||||
## Notes
|
||||
|
||||
{{FOUNDATION_NOTES}}
|
||||
+88
@@ -0,0 +1,88 @@
|
||||
# Stage 2: Handlers
|
||||
|
||||
**Status:** ⬜ Not Started
|
||||
**Blocked By:** Foundation
|
||||
**Unlocks:** Errors, Adapters
|
||||
|
||||
## Objective
|
||||
|
||||
Convert functions with `throw` to handlers returning `Result<T, E>`.
|
||||
|
||||
## Handlers to Convert
|
||||
|
||||
{{#each HANDLERS}}
|
||||
### {{this.name}}
|
||||
|
||||
- **File:** `{{this.file}}:{{this.line}}`
|
||||
- **Current:** `{{this.signature}}`
|
||||
- **Throws:** {{this.throws}}
|
||||
- **Priority:** {{this.priority}}
|
||||
|
||||
#### Conversion
|
||||
|
||||
- [ ] Define input schema (Zod)
|
||||
- [ ] Define output type
|
||||
- [ ] Identify error cases → taxonomy mapping
|
||||
- [ ] Convert to Handler signature
|
||||
- [ ] Replace `throw` with `Result.err()`
|
||||
- [ ] Add `createValidator()` for input
|
||||
- [ ] Update callers to use `isOk()` / `isErr()`
|
||||
- [ ] Add/update tests
|
||||
|
||||
```typescript
|
||||
// Target signature
|
||||
const {{this.name}}: Handler<{{this.inputType}}, {{this.outputType}}, {{this.errorType}}> = async (input, ctx) => {
|
||||
// ...
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
{{/each}}
|
||||
|
||||
## Conversion Pattern
|
||||
|
||||
### Before
|
||||
|
||||
```typescript
|
||||
async function getUser(id: string): Promise<User> {
|
||||
const user = await db.users.findById(id);
|
||||
if (!user) throw new Error(`Not found: ${id}`);
|
||||
return user;
|
||||
}
|
||||
|
||||
try {
|
||||
const user = await getUser("123");
|
||||
} catch (error) {
|
||||
console.error(error.message);
|
||||
}
|
||||
```
|
||||
|
||||
### After
|
||||
|
||||
```typescript
|
||||
import { Result, NotFoundError, type Handler } from "@outfitter/contracts";
|
||||
|
||||
const getUser: Handler<{ id: string }, User, NotFoundError> = async (input, ctx) => {
|
||||
const user = await db.users.findById(input.id);
|
||||
if (!user) return Result.err(new NotFoundError("user", input.id));
|
||||
return Result.ok(user);
|
||||
};
|
||||
|
||||
const result = await getUser({ id: "123" }, ctx);
|
||||
if (result.isErr()) {
|
||||
ctx.logger.error("Failed", { error: result.error });
|
||||
}
|
||||
```
|
||||
|
||||
## Completion Checklist
|
||||
|
||||
- [ ] All handlers return `Result<T, E>`
|
||||
- [ ] No `throw` in handler code
|
||||
- [ ] Input validation with `createValidator()`
|
||||
- [ ] Callers check `isOk()` / `isErr()`
|
||||
- [ ] Tests updated for Result assertions
|
||||
|
||||
## Notes
|
||||
|
||||
{{HANDLER_NOTES}}
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
# Stage 3: Errors
|
||||
|
||||
**Status:** ⬜ Not Started
|
||||
**Blocked By:** Handlers
|
||||
**Unlocks:** Documents
|
||||
|
||||
## Objective
|
||||
|
||||
Replace custom error classes with Outfitter error taxonomy.
|
||||
|
||||
## Error Taxonomy Reference
|
||||
|
||||
| Category | Class | Exit | HTTP | Use For |
|
||||
|----------|-------|------|------|---------|
|
||||
| `validation` | `ValidationError` | 1 | 400 | Invalid input, schema failures |
|
||||
| `not_found` | `NotFoundError` | 2 | 404 | Resource doesn't exist |
|
||||
| `conflict` | `ConflictError` | 3 | 409 | Already exists, version mismatch |
|
||||
| `permission` | `PermissionError` | 4 | 403 | Forbidden action |
|
||||
| `timeout` | `TimeoutError` | 5 | 504 | Operation took too long |
|
||||
| `rate_limit` | `RateLimitError` | 6 | 429 | Too many requests |
|
||||
| `network` | `NetworkError` | 7 | 503 | Connection failures |
|
||||
| `internal` | `InternalError` | 8 | 500 | Unexpected errors, bugs |
|
||||
| `auth` | `AuthError` | 9 | 401 | Authentication required |
|
||||
| `cancelled` | `CancelledError` | 130 | 499 | User interrupted |
|
||||
|
||||
## Error Classes to Migrate
|
||||
|
||||
{{#each ERROR_CLASSES}}
|
||||
### {{this.name}}
|
||||
|
||||
- **File:** `{{this.file}}:{{this.line}}`
|
||||
- **Usages:** {{this.usageCount}}
|
||||
- **Suggested Mapping:** `{{this.suggestedMapping}}`
|
||||
|
||||
#### Migration
|
||||
|
||||
- [ ] Identify all usages of `{{this.name}}`
|
||||
- [ ] Replace with `{{this.suggestedMapping}}`
|
||||
- [ ] Update error metadata/details
|
||||
- [ ] Remove original class definition
|
||||
- [ ] Update tests
|
||||
|
||||
```typescript
|
||||
// Before
|
||||
throw new {{this.name}}({{this.exampleArgs}});
|
||||
|
||||
// After
|
||||
return Result.err(new {{this.suggestedMapping}}({{this.newArgs}}));
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
{{/each}}
|
||||
|
||||
## Unmapped Errors
|
||||
|
||||
Errors that don't fit standard taxonomy:
|
||||
|
||||
{{#each UNMAPPED_ERRORS}}
|
||||
- [ ] `{{this.name}}` — {{this.reason}}
|
||||
{{/each}}
|
||||
|
||||
**Options for unmapped errors:**
|
||||
1. Use `InternalError` with descriptive message
|
||||
2. Create domain-specific error extending `OutfitterError`
|
||||
3. Map to closest category with metadata
|
||||
|
||||
## Completion Checklist
|
||||
|
||||
- [ ] All custom errors mapped to taxonomy
|
||||
- [ ] Original error classes removed
|
||||
- [ ] Error messages include structured metadata
|
||||
- [ ] Exit codes verified correct
|
||||
- [ ] Tests updated
|
||||
|
||||
## Notes
|
||||
|
||||
{{ERROR_NOTES}}
|
||||
+117
@@ -0,0 +1,117 @@
|
||||
# Stage 4: Paths
|
||||
|
||||
**Status:** ⬜ Not Started
|
||||
**Blocked By:** None (can run parallel with Handlers)
|
||||
**Unlocks:** Documents
|
||||
|
||||
## Objective
|
||||
|
||||
Replace hardcoded paths with XDG-compliant paths and add path security.
|
||||
|
||||
## XDG Directory Reference
|
||||
|
||||
| Function | Path | Purpose |
|
||||
|----------|------|---------|
|
||||
| `getConfigDir(name)` | `~/.config/{name}` | Configuration files |
|
||||
| `getCacheDir(name)` | `~/.cache/{name}` | Cache files |
|
||||
| `getDataDir(name)` | `~/.local/share/{name}` | Persistent data |
|
||||
| `getStateDir(name)` | `~/.local/state/{name}` | Runtime state |
|
||||
|
||||
## Files to Migrate
|
||||
|
||||
{{#each PATH_FILES}}
|
||||
### {{this.file}}
|
||||
|
||||
- **Line:** {{this.line}}
|
||||
- **Current:** `{{this.current}}`
|
||||
- **Pattern:** {{this.pattern}}
|
||||
|
||||
#### Migration
|
||||
|
||||
- [ ] Replace with XDG function
|
||||
- [ ] Add `securePath()` if user-provided
|
||||
- [ ] Update tests
|
||||
|
||||
```typescript
|
||||
// Before
|
||||
{{this.beforeCode}}
|
||||
|
||||
// After
|
||||
{{this.afterCode}}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
{{/each}}
|
||||
|
||||
## Path Security
|
||||
|
||||
For user-provided paths, use `securePath()`:
|
||||
|
||||
```typescript
|
||||
import { securePath } from "@outfitter/file-ops";
|
||||
|
||||
const validatePath = (userPath: string, baseDir: string) => {
|
||||
const result = securePath(userPath, { base: baseDir });
|
||||
if (result.isErr()) {
|
||||
return Result.err(new ValidationError("Invalid path", { path: userPath }));
|
||||
}
|
||||
return Result.ok(result.value);
|
||||
};
|
||||
```
|
||||
|
||||
**Security checks:**
|
||||
- Path traversal (`../`)
|
||||
- Symlink following
|
||||
- Base directory escape
|
||||
- Null bytes
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Config File
|
||||
|
||||
```typescript
|
||||
// Before
|
||||
const configPath = path.join(os.homedir(), ".myapp", "config.json");
|
||||
|
||||
// After
|
||||
import { getConfigDir } from "@outfitter/config";
|
||||
const configPath = path.join(getConfigDir("myapp"), "config.json");
|
||||
```
|
||||
|
||||
### Cache Directory
|
||||
|
||||
```typescript
|
||||
// Before
|
||||
const cacheDir = path.join(os.homedir(), ".cache", "myapp");
|
||||
|
||||
// After
|
||||
import { getCacheDir } from "@outfitter/config";
|
||||
const cacheDir = getCacheDir("myapp");
|
||||
```
|
||||
|
||||
### User-Provided Path
|
||||
|
||||
```typescript
|
||||
// Before
|
||||
const filePath = args.file;
|
||||
await fs.readFile(filePath);
|
||||
|
||||
// After
|
||||
import { securePath } from "@outfitter/file-ops";
|
||||
const pathResult = securePath(args.file, { base: process.cwd() });
|
||||
if (pathResult.isErr()) return pathResult;
|
||||
await fs.readFile(pathResult.value);
|
||||
```
|
||||
|
||||
## Completion Checklist
|
||||
|
||||
- [ ] All `os.homedir()` replaced with XDG functions
|
||||
- [ ] All `~/` literals replaced
|
||||
- [ ] User-provided paths validated with `securePath()`
|
||||
- [ ] Tests use `withTempDir()` fixture
|
||||
- [ ] No hardcoded absolute paths
|
||||
|
||||
## Notes
|
||||
|
||||
{{PATH_NOTES}}
|
||||
+166
@@ -0,0 +1,166 @@
|
||||
# Stage 5: Adapters
|
||||
|
||||
**Status:** ⬜ Not Started
|
||||
**Blocked By:** Handlers
|
||||
**Unlocks:** Documents
|
||||
|
||||
## Objective
|
||||
|
||||
Wrap handlers with CLI and/or MCP transport adapters.
|
||||
|
||||
## CLI Commands
|
||||
|
||||
{{#each CLI_COMMANDS}}
|
||||
### {{this.name}}
|
||||
|
||||
- **Handler:** `{{this.handler}}`
|
||||
- **Current File:** `{{this.file}}`
|
||||
|
||||
#### Migration
|
||||
|
||||
- [ ] Create command with Zod schema
|
||||
- [ ] Wrap handler
|
||||
- [ ] Use `output()` for responses
|
||||
- [ ] Use `exitWithError()` for errors
|
||||
- [ ] Add integration test
|
||||
|
||||
```typescript
|
||||
import { command, output, exitWithError } from "@outfitter/cli";
|
||||
import { {{this.handler}} } from "../handlers/{{this.handlerFile}}";
|
||||
import { createAppContext } from "../context";
|
||||
import { z } from "zod";
|
||||
|
||||
const InputSchema = z.object({
|
||||
{{this.inputFields}}
|
||||
});
|
||||
|
||||
export const {{this.name}}Command = command("{{this.commandName}}")
|
||||
.description("{{this.description}}")
|
||||
{{this.options}}
|
||||
.action(async ({ args, flags }) => {
|
||||
const ctx = createAppContext();
|
||||
const result = await {{this.handler}}({ {{this.inputMapping}} }, ctx);
|
||||
|
||||
if (result.isErr()) {
|
||||
exitWithError(result.error);
|
||||
}
|
||||
|
||||
await output(result.value);
|
||||
})
|
||||
.build();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
{{/each}}
|
||||
|
||||
## MCP Tools
|
||||
|
||||
{{#each MCP_TOOLS}}
|
||||
### {{this.name}}
|
||||
|
||||
- **Handler:** `{{this.handler}}`
|
||||
- **Current File:** `{{this.file}}`
|
||||
|
||||
#### Migration
|
||||
|
||||
- [ ] Create tool with Zod schema
|
||||
- [ ] Add `.describe()` to all fields
|
||||
- [ ] Wrap handler
|
||||
- [ ] Register with server
|
||||
- [ ] Add integration test
|
||||
|
||||
```typescript
|
||||
import { defineTool } from "@outfitter/mcp";
|
||||
import { {{this.handler}} } from "../handlers/{{this.handlerFile}}";
|
||||
import { z } from "zod";
|
||||
|
||||
export const {{this.name}}Tool = defineTool({
|
||||
name: "{{this.toolName}}",
|
||||
description: "{{this.description}}",
|
||||
schema: z.object({
|
||||
{{this.schemaFields}}
|
||||
}),
|
||||
handler: async (input, ctx) => {
|
||||
return {{this.handler}}(input, ctx);
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
{{/each}}
|
||||
|
||||
## CLI Patterns
|
||||
|
||||
### Output Modes
|
||||
|
||||
```typescript
|
||||
// Automatic mode detection (TTY vs pipe)
|
||||
await output(data);
|
||||
|
||||
// Force specific mode
|
||||
await output(data, { mode: "json" });
|
||||
await output(data, { mode: "human" });
|
||||
```
|
||||
|
||||
### Error Handling
|
||||
|
||||
```typescript
|
||||
if (result.isErr()) {
|
||||
exitWithError(result.error);
|
||||
// Prints error message
|
||||
// Exits with category-mapped code (1-9, 130)
|
||||
}
|
||||
```
|
||||
|
||||
### Testing CLI
|
||||
|
||||
```typescript
|
||||
import { createCliHarness } from "@outfitter/testing";
|
||||
|
||||
const harness = createCliHarness(myCommand);
|
||||
|
||||
it("handles success", async () => {
|
||||
const result = await harness.run(["--id", "123"]);
|
||||
expect(result.exitCode).toBe(0);
|
||||
expect(result.stdout).toContain("success");
|
||||
});
|
||||
```
|
||||
|
||||
## MCP Patterns
|
||||
|
||||
### Tool Registration
|
||||
|
||||
```typescript
|
||||
import { createMcpServer } from "@outfitter/mcp";
|
||||
|
||||
const server = createMcpServer({ name: "myapp" });
|
||||
server.registerTool(myTool);
|
||||
server.start();
|
||||
```
|
||||
|
||||
### Testing MCP
|
||||
|
||||
```typescript
|
||||
import { createMcpHarness } from "@outfitter/testing";
|
||||
|
||||
const harness = createMcpHarness(server);
|
||||
|
||||
it("handles tool call", async () => {
|
||||
const result = await harness.callTool("my-tool", { id: "123" });
|
||||
expect(result.isOk()).toBe(true);
|
||||
});
|
||||
```
|
||||
|
||||
## Completion Checklist
|
||||
|
||||
- [ ] All CLI commands use `output()` and `exitWithError()`
|
||||
- [ ] All MCP tools have `.describe()` on schema fields
|
||||
- [ ] Handlers wrapped, not inlined
|
||||
- [ ] Integration tests with harnesses
|
||||
- [ ] Error codes verified
|
||||
|
||||
## Notes
|
||||
|
||||
{{ADAPTER_NOTES}}
|
||||
+123
@@ -0,0 +1,123 @@
|
||||
# Stage 6: Documents
|
||||
|
||||
**Status:** ⬜ Not Started
|
||||
**Blocked By:** All other stages
|
||||
**Unlocks:** None (final stage)
|
||||
|
||||
## Objective
|
||||
|
||||
Update documentation to reflect new patterns and APIs.
|
||||
|
||||
## Files to Update
|
||||
|
||||
{{#each DOC_FILES}}
|
||||
### {{this.file}}
|
||||
|
||||
- **Type:** {{this.type}}
|
||||
- **Issues:** {{this.issues}}
|
||||
|
||||
#### Updates Needed
|
||||
|
||||
{{#each this.updates}}
|
||||
- [ ] {{this}}
|
||||
{{/each}}
|
||||
|
||||
---
|
||||
|
||||
{{/each}}
|
||||
|
||||
## README Updates
|
||||
|
||||
- [ ] Update installation instructions (add @outfitter/* packages)
|
||||
- [ ] Update API examples (Result types, not exceptions)
|
||||
- [ ] Update error handling section
|
||||
- [ ] Add migration notes for consumers (if library)
|
||||
|
||||
### Example API Section
|
||||
|
||||
```markdown
|
||||
## Usage
|
||||
|
||||
\`\`\`typescript
|
||||
import { getUser } from "mylib";
|
||||
import { createContext } from "@outfitter/contracts";
|
||||
|
||||
const ctx = createContext();
|
||||
const result = await getUser({ id: "123" }, ctx);
|
||||
|
||||
if (result.isOk()) {
|
||||
console.log(result.value);
|
||||
} else {
|
||||
console.error(result.error.message);
|
||||
}
|
||||
\`\`\`
|
||||
```
|
||||
|
||||
## TSDoc/JSDoc Updates
|
||||
|
||||
Update function documentation to reflect Result return types:
|
||||
|
||||
```typescript
|
||||
/**
|
||||
* Fetches a user by ID.
|
||||
*
|
||||
* @param input - The input containing the user ID
|
||||
* @param ctx - Handler context
|
||||
* @returns Result with User on success, NotFoundError if user doesn't exist
|
||||
*
|
||||
* @example
|
||||
* const result = await getUser({ id: "123" }, ctx);
|
||||
* if (result.isOk()) {
|
||||
* console.log(result.value.name);
|
||||
* }
|
||||
*/
|
||||
```
|
||||
|
||||
## CHANGELOG Entry
|
||||
|
||||
```markdown
|
||||
## [X.Y.Z] - {{DATE}}
|
||||
|
||||
### Changed
|
||||
|
||||
- **BREAKING**: All handlers now return `Result<T, E>` instead of throwing
|
||||
- **BREAKING**: Error types use Outfitter taxonomy
|
||||
- Paths now use XDG conventions
|
||||
|
||||
### Added
|
||||
|
||||
- Structured logging with `@outfitter/logging`
|
||||
- Input validation with Zod schemas
|
||||
|
||||
### Migration
|
||||
|
||||
See [MIGRATION.md](./MIGRATION.md) for upgrade guide.
|
||||
```
|
||||
|
||||
## Migration Guide (if library)
|
||||
|
||||
- [ ] Create `MIGRATION.md` for consumers
|
||||
- [ ] Document breaking changes
|
||||
- [ ] Provide before/after examples
|
||||
- [ ] List error type mappings
|
||||
|
||||
## Inline Comments
|
||||
|
||||
Review and update comments that reference old patterns:
|
||||
|
||||
- [ ] Remove `// throws XError` comments
|
||||
- [ ] Update `@throws` JSDoc tags to `@returns Result`
|
||||
- [ ] Fix examples in code comments
|
||||
|
||||
## Completion Checklist
|
||||
|
||||
- [ ] README reflects new API patterns
|
||||
- [ ] TSDoc/JSDoc updated for all public APIs
|
||||
- [ ] CHANGELOG entry added
|
||||
- [ ] Migration guide created (if library)
|
||||
- [ ] Inline comments reviewed
|
||||
- [ ] Examples compile and work
|
||||
|
||||
## Notes
|
||||
|
||||
{{DOC_NOTES}}
|
||||
+126
@@ -0,0 +1,126 @@
|
||||
# Stage 99: Unknowns
|
||||
|
||||
**Status:** ⬜ Review Required
|
||||
**Blocked By:** None
|
||||
**Unlocks:** None (review throughout migration)
|
||||
|
||||
## Objective
|
||||
|
||||
Track items the scanner couldn't categorize or that need human judgment.
|
||||
|
||||
## Review Priority
|
||||
|
||||
| Priority | Meaning |
|
||||
|----------|---------|
|
||||
| 🔴 High | Blocks other work, needs immediate decision |
|
||||
| 🟡 Medium | Should resolve before Documents stage |
|
||||
| 🟢 Low | Can defer or skip with documentation |
|
||||
|
||||
## Unknowns
|
||||
|
||||
{{#each UNKNOWNS}}
|
||||
### {{this.id}}: {{this.title}}
|
||||
|
||||
- **File:** `{{this.file}}:{{this.line}}`
|
||||
- **Priority:** {{this.priority}}
|
||||
- **Category:** {{this.category}}
|
||||
|
||||
#### Context
|
||||
|
||||
```typescript
|
||||
{{this.code}}
|
||||
```
|
||||
|
||||
#### Why Unknown
|
||||
|
||||
{{this.reason}}
|
||||
|
||||
#### Options
|
||||
|
||||
{{#each this.options}}
|
||||
{{@index}}. {{this}}
|
||||
{{/each}}
|
||||
|
||||
#### Decision
|
||||
|
||||
- [ ] Reviewed
|
||||
- [ ] Decision: _____________
|
||||
- [ ] Implemented
|
||||
|
||||
---
|
||||
|
||||
{{/each}}
|
||||
|
||||
## Common Unknown Categories
|
||||
|
||||
### Third-Party Libraries That Throw
|
||||
|
||||
Libraries that throw exceptions need wrapper decisions:
|
||||
|
||||
```typescript
|
||||
// Option 1: Wrap at call site
|
||||
const result = await wrapAsync(() => thirdPartyLib.doThing());
|
||||
|
||||
// Option 2: Create typed wrapper
|
||||
const safeDoThing = wrapThirdParty(thirdPartyLib.doThing);
|
||||
```
|
||||
|
||||
### Complex Try/Catch Blocks
|
||||
|
||||
Nested or multi-catch blocks that can't be auto-converted:
|
||||
|
||||
```typescript
|
||||
// May need manual restructuring
|
||||
try {
|
||||
await step1();
|
||||
await step2();
|
||||
} catch (e) {
|
||||
if (e instanceof TypeA) { ... }
|
||||
else if (e instanceof TypeB) { ... }
|
||||
else { throw e; }
|
||||
}
|
||||
```
|
||||
|
||||
### Async Patterns
|
||||
|
||||
Unusual async patterns (Promise.race, Promise.allSettled with throws):
|
||||
|
||||
```typescript
|
||||
// May need Result-aware alternatives
|
||||
const results = await Promise.all(items.map(processItem));
|
||||
```
|
||||
|
||||
### Domain-Specific Errors
|
||||
|
||||
Errors that don't map cleanly to taxonomy:
|
||||
|
||||
- Consider if they're really `ValidationError` with metadata
|
||||
- Consider if they're `InternalError` with descriptive message
|
||||
- Consider creating domain error extending `OutfitterError`
|
||||
|
||||
## Resolution Log
|
||||
|
||||
| ID | Decision | Rationale | Date |
|
||||
|----|----------|-----------|------|
|
||||
|
||||
## Stack Feedback
|
||||
|
||||
Issues discovered that should be reported to outfitter-dev/outfitter:
|
||||
|
||||
{{#each STACK_FEEDBACK}}
|
||||
- [ ] {{this.title}} — {{this.type}}
|
||||
{{/each}}
|
||||
|
||||
Use `outfitter-stack:stack-feedback` skill to create GitHub issues.
|
||||
|
||||
## Completion Checklist
|
||||
|
||||
- [ ] All unknowns reviewed
|
||||
- [ ] Decisions documented
|
||||
- [ ] High-priority items resolved
|
||||
- [ ] Stack feedback reported
|
||||
- [ ] Remaining items documented for future
|
||||
|
||||
## Notes
|
||||
|
||||
{{UNKNOWN_NOTES}}
|
||||
Reference in New Issue
Block a user