diff --git a/.agents/skills/dev-utils.zip b/.agents/skills/dev-utils.zip new file mode 100644 index 0000000..0a8ef98 Binary files /dev/null and b/.agents/skills/dev-utils.zip differ diff --git a/.agents/skills/dev-utils/SKILL.md b/.agents/skills/dev-utils/SKILL.md new file mode 100644 index 0000000..395281c --- /dev/null +++ b/.agents/skills/dev-utils/SKILL.md @@ -0,0 +1,57 @@ +--- +name: dev-utils +description: "Developer utilities monorepo — inquirerer (interactive CLI prompts), yanse (terminal colors), appStash (state persistence), inflekt (string inflection), strfy-js (JSON stringify), clean-ansi, and 25+ other packages. Use when building CLIs with inquirerer, working with terminal colors, string inflection, or any package in the dev-utils repo." +metadata: + author: constructive-io + version: "2.0.0" +--- + +# dev-utils + +Developer utilities monorepo with 30+ packages for CLI tooling, parsing, and general-purpose TypeScript utilities. + +## When to Apply + +Use this skill when: +- Building interactive CLIs with `inquirerer` +- Working with terminal colors (`yanse`) +- Using string inflection (`inflekt`) +- Working with any package in the `constructive-io/dev-utils` repo + +**For pnpm workspace management, publishing, and monorepo configuration**, see the `constructive-pnpm` skill. + +## inquirerer CLI Framework + +Build interactive CLI tools with prompts, appStash state persistence, and yanse terminal colors. + +See [inquirerer-cli.md](./references/inquirerer-cli.md) for the CLI framework guide. + +## README Formatting + +Consistent documentation formatting conventions for Constructive projects. + +See [readme-formatting.md](./references/readme-formatting.md) for formatting rules. + +## Reference Guide + +### CLI + +| Reference | Topic | Consult When | +|-----------|-------|--------------| +| [inquirerer-cli.md](./references/inquirerer-cli.md) | inquirerer CLI framework | Building interactive CLI tools | +| [inquirerer-cli-building.md](./references/inquirerer-cli-building.md) | CLI building patterns | Command structure, argument parsing | +| [inquirerer-appstash.md](./references/inquirerer-appstash.md) | appStash state management | Persisting CLI state between runs | +| [inquirerer-yanse.md](./references/inquirerer-yanse.md) | yanse terminal colors | Colored output, styling | +| [inquirerer-anti-patterns.md](./references/inquirerer-anti-patterns.md) | Anti-patterns to avoid | Common mistakes in CLI building | + +### Documentation + +| Reference | Topic | Consult When | +|-----------|-------|--------------| +| [readme-formatting.md](./references/readme-formatting.md) | README conventions | Formatting standards, structure | + +## Cross-References + +- `constructive-pnpm` — PNPM workspace management, publishing, monorepo configuration +- `pgpm` — Uses pnpm workspaces for module management +- `constructive-starter-kits` — Boilerplate templates use these tools diff --git a/.agents/skills/dev-utils/references/inquirerer-anti-patterns.md b/.agents/skills/dev-utils/references/inquirerer-anti-patterns.md new file mode 100644 index 0000000..293d4bc --- /dev/null +++ b/.agents/skills/dev-utils/references/inquirerer-anti-patterns.md @@ -0,0 +1,58 @@ +--- +name: inquirerer-anti-patterns +description: Anti-patterns for CLI development. Do NOT use commander, inquirer.js, yargs, or other CLI libraries in Constructive projects. Use inquirerer instead. Triggers on "commander", "inquirer.js", "yargs", "CLI library", or when reviewing CLI code. +compatibility: inquirerer, Node.js 18+, TypeScript +metadata: + author: constructive-io + version: "1.0.0" + type: anti-pattern +--- + +# CLI Anti-Patterns: Avoid These Libraries + +This skill defines what NOT to do when building CLI tools in Constructive projects. All CLI development should use `inquirerer` instead of other CLI libraries. + +## When to Apply + +Apply this skill when: +- Reviewing code that imports commander, inquirer.js, yargs, or similar +- Someone asks about using a CLI library other than inquirerer +- Creating a new CLI tool and considering which library to use + +## Forbidden Libraries + +Do NOT use these libraries in Constructive projects: + +| Library | Reason to Avoid | +|---------|-----------------| +| `commander` | Separate argument parsing, no integrated prompts | +| `inquirer` / `inquirer.js` | Outdated, not TypeScript-first, different API | +| `yargs` | Complex API, no integrated prompts | +| `prompts` | Limited features, no resolver system | +| `enquirer` | Different API, no Constructive integration | +| `vorpal` | Unmaintained, complex | +| `oclif` | Heavyweight framework, overkill for most uses | +| `meow` | Minimal, no prompt support | +| `arg` | Argument parsing only | +| `minimist` (directly) | Use inquirerer's `parseArgv` wrapper instead | +| `ora` | Use inquirerer's `createSpinner` instead | +| `cli-progress` | Use inquirerer's `createProgress` instead | + +## Why inquirerer is the Standard + +inquirerer is the standard CLI library for all Constructive monorepos because it provides a unified approach across all our projects: + +1. **Consistency**: All Constructive CLIs have the same look, feel, and behavior +2. **TypeScript-first**: Full type safety for questions and answers +3. **Integrated**: Single library for argument parsing, prompts, and UI components +4. **Dynamic defaults**: Built-in resolvers for git config, npm, dates, workspace info +5. **CI/CD ready**: Non-interactive mode works without code changes +6. **Maintained**: Actively developed as part of Constructive tooling + +By standardizing on inquirerer, developers can move between Constructive projects and immediately understand how CLI tools work without learning different libraries. + +## References + +- Use instead: `inquirerer` - https://www.npmjs.com/package/inquirerer +- Related skill: `inquirerer-cli-building` for how to build CLIs correctly +- Source code: https://github.com/constructive-io/dev-utils/tree/main/packages/inquirerer diff --git a/.agents/skills/dev-utils/references/inquirerer-appstash.md b/.agents/skills/dev-utils/references/inquirerer-appstash.md new file mode 100644 index 0000000..a82f9d9 --- /dev/null +++ b/.agents/skills/dev-utils/references/inquirerer-appstash.md @@ -0,0 +1,358 @@ +--- +name: appstash-cli +description: Use appstash for CLI application directory management. Apply when building CLI tools that need config storage, caching, logging, or update checking. +compatibility: Node.js 18+, TypeScript +metadata: + author: constructive-io + version: "1.0.0" +--- + +# appstash CLI Directory Management + +Use `appstash` for simple, clean application directory resolution in CLI tools. It provides consistent paths for config, cache, data, logs, and temp directories with graceful fallback handling. + +## When to Apply + +Use this skill when: +- Building a CLI tool that needs to store configuration +- Implementing persistent caching for CLI operations +- Managing API keys or auth tokens for CLI tools +- Adding logging to CLI applications +- Storing temporary files during CLI operations + +## Installation + +```bash +npm install appstash +# or +pnpm add appstash +``` + +## Core Concepts + +### Directory Structure + +appstash creates a clean directory structure under the user's home directory: + +``` +~/./ + ├── config/ # Configuration files (settings, auth profiles) + ├── cache/ # Cached data (repos, API responses) + ├── data/ # Application data (databases, state) + └── logs/ # Log files + +/tmp// # Temporary files (ephemeral) +``` + +### Fallback Chain + +appstash never throws errors. If the home directory is unavailable: +1. Falls back to XDG directories (`~/.config/`, `~/.cache/`, etc.) +2. Falls back to system temp (`/tmp//`) + +## Basic Usage + +```typescript +import { appstash, resolve } from 'appstash'; + +// Get directories for your CLI tool +const dirs = appstash('mycli', { ensure: true }); + +console.log(dirs.config); // ~/.mycli/config +console.log(dirs.cache); // ~/.mycli/cache +console.log(dirs.data); // ~/.mycli/data +console.log(dirs.logs); // ~/.mycli/logs +console.log(dirs.tmp); // /tmp/mycli +``` + +## Common Patterns + +### Storing Auth Profiles + +Store multiple API endpoints and tokens for CLI tools: + +```typescript +import { appstash, resolve } from 'appstash'; +import * as fs from 'fs'; + +interface AuthProfile { + endpoint: string; + token?: string; +} + +interface CliConfig { + current?: string; + profiles: Record; +} + +const dirs = appstash('mycli', { ensure: true }); +const configFile = resolve(dirs, 'config', 'auth.json'); + +function loadConfig(): CliConfig { + if (fs.existsSync(configFile)) { + return JSON.parse(fs.readFileSync(configFile, 'utf8')); + } + return { profiles: {} }; +} + +function saveConfig(config: CliConfig): void { + fs.writeFileSync(configFile, JSON.stringify(config, null, 2)); +} + +function addProfile(name: string, endpoint: string, token?: string): void { + const config = loadConfig(); + config.profiles[name] = { endpoint, token }; + if (!config.current) { + config.current = name; + } + saveConfig(config); +} + +function useProfile(name: string): void { + const config = loadConfig(); + if (!config.profiles[name]) { + throw new Error(`Profile "${name}" not found`); + } + config.current = name; + saveConfig(config); +} + +function getActiveProfile(): AuthProfile | null { + const config = loadConfig(); + if (config.current && config.profiles[config.current]) { + return config.profiles[config.current]; + } + return null; +} +``` + +### Caching API Responses + +```typescript +import { appstash, resolve } from 'appstash'; +import * as fs from 'fs'; +import * as path from 'path'; + +const dirs = appstash('mycli', { ensure: true }); + +interface CacheEntry { + data: T; + timestamp: number; +} + +function getCached(key: string, ttlMs: number): T | null { + const cachePath = resolve(dirs, 'cache', `${key}.json`); + + if (!fs.existsSync(cachePath)) { + return null; + } + + const entry: CacheEntry = JSON.parse(fs.readFileSync(cachePath, 'utf8')); + const age = Date.now() - entry.timestamp; + + if (age > ttlMs) { + fs.unlinkSync(cachePath); + return null; + } + + return entry.data; +} + +function setCache(key: string, data: T): void { + const cachePath = resolve(dirs, 'cache', `${key}.json`); + fs.mkdirSync(path.dirname(cachePath), { recursive: true }); + + const entry: CacheEntry = { + data, + timestamp: Date.now(), + }; + + fs.writeFileSync(cachePath, JSON.stringify(entry)); +} +``` + +### Logging + +```typescript +import { appstash, resolve } from 'appstash'; +import * as fs from 'fs'; + +const dirs = appstash('mycli', { ensure: true }); +const logFile = resolve(dirs, 'logs', 'cli.log'); + +function log(level: 'info' | 'warn' | 'error', message: string): void { + const timestamp = new Date().toISOString(); + const line = `[${timestamp}] [${level.toUpperCase()}] ${message}\n`; + fs.appendFileSync(logFile, line); +} +``` + +### Update Checking with @inquirerer/utils + +Combine appstash with `@inquirerer/utils` for update checking: + +```typescript +import { appstash, resolve } from 'appstash'; +import { checkForUpdates } from '@inquirerer/utils'; +import * as fs from 'fs'; + +const dirs = appstash('mycli', { ensure: true }); +const updateCacheFile = resolve(dirs, 'cache', 'update-check.json'); + +async function checkUpdates(pkgName: string, pkgVersion: string): Promise { + // Check if we've checked recently (within 24 hours) + if (fs.existsSync(updateCacheFile)) { + const cache = JSON.parse(fs.readFileSync(updateCacheFile, 'utf8')); + const age = Date.now() - cache.timestamp; + if (age < 24 * 60 * 60 * 1000) { + return; // Skip check + } + } + + const result = await checkForUpdates({ + pkgName, + pkgVersion, + toolName: 'mycli', + }); + + // Cache the check timestamp + fs.writeFileSync(updateCacheFile, JSON.stringify({ timestamp: Date.now() })); + + if (result.hasUpdate && result.message) { + console.warn(result.message); + console.warn('Run `npm update -g mycli` to upgrade.'); + } +} +``` + +## Integration with inquirerer CLI + +When building CLIs with `inquirerer`, use appstash for all persistent storage: + +```typescript +import { CLI, CLIOptions, Inquirerer, ParsedArgs } from 'inquirerer'; +import { appstash, resolve } from 'appstash'; + +const dirs = appstash('mycli', { ensure: true }); + +// Auth command using appstash +const authCommand = async (argv: Partial, prompter: Inquirerer) => { + const configFile = resolve(dirs, 'config', 'auth.json'); + + // ... implement auth management +}; + +// Main CLI setup +const commands = async (argv: Partial, prompter: Inquirerer, options: CLIOptions) => { + // ... command routing +}; + +const app = new CLI(commands, { + minimistOpts: { + alias: { v: 'version', h: 'help' } + } +}); + +app.run(); +``` + +## Environment Variable Override + +Always allow environment variables to override stored config: + +```typescript +import { appstash, resolve } from 'appstash'; + +function getEndpoint(): string { + // Environment variable takes precedence + if (process.env.MYCLI_ENDPOINT) { + return process.env.MYCLI_ENDPOINT; + } + + // Fall back to stored profile + const profile = getActiveProfile(); + return profile?.endpoint || 'http://localhost:3000'; +} + +function getAuthToken(): string | undefined { + // Environment variable takes precedence + if (process.env.MYCLI_TOKEN) { + return process.env.MYCLI_TOKEN; + } + + // Fall back to stored profile + const profile = getActiveProfile(); + return profile?.token; +} +``` + +## Testing + +Isolate tests using custom `baseDir`: + +```typescript +import { appstash } from 'appstash'; +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; + +describe('CLI config', () => { + let testDirs: ReturnType; + + beforeEach(() => { + testDirs = appstash('mycli', { + baseDir: fs.mkdtempSync(path.join(os.tmpdir(), 'test-')), + ensure: true + }); + }); + + afterEach(() => { + fs.rmSync(testDirs.root, { recursive: true, force: true }); + }); + + it('should store config', () => { + // Test with isolated directories + }); +}); +``` + +## API Reference + +### `appstash(tool, options?)` + +Get application directories for a tool. + +**Parameters:** +- `tool` (string): Tool name (e.g., 'pgpm', 'mycli') +- `options.baseDir` (string): Custom base directory (default: `os.homedir()`) +- `options.ensure` (boolean): Create directories if missing (default: `false`) +- `options.useXdgFallback` (boolean): Use XDG fallback if home fails (default: `true`) +- `options.tmpRoot` (string): Root for temp directory (default: `os.tmpdir()`) + +**Returns:** `AppStashResult` with `root`, `config`, `cache`, `data`, `logs`, `tmp` paths + +### `resolve(dirs, kind, ...parts)` + +Resolve a path within a specific directory. + +**Parameters:** +- `dirs`: Result from `appstash()` +- `kind`: 'config' | 'cache' | 'data' | 'logs' | 'tmp' +- `parts`: Path segments to join + +**Returns:** Resolved path string + +### `ensure(dirs)` + +Create directories if they don't exist. + +**Parameters:** +- `dirs`: Result from `appstash()` + +**Returns:** `{ created: string[], usedFallback: boolean }` + +## References + +- Package: https://www.npmjs.com/package/appstash +- Source: https://github.com/constructive-io/dev-utils/tree/main/packages/appstash +- Related: `inquirerer` for CLI building, `@inquirerer/utils` for update checking diff --git a/.agents/skills/dev-utils/references/inquirerer-cli-building.md b/.agents/skills/dev-utils/references/inquirerer-cli-building.md new file mode 100644 index 0000000..2384553 --- /dev/null +++ b/.agents/skills/dev-utils/references/inquirerer-cli-building.md @@ -0,0 +1,539 @@ +--- +name: inquirerer-cli-building +description: Build interactive CLI tools with inquirerer. Use when asked to "create a CLI", "build a command-line tool", "add prompts", "create interactive prompts", or when building any CLI application in a Constructive project. +compatibility: inquirerer, Node.js 18+, TypeScript +metadata: + author: constructive-io + version: "1.0.0" +--- + +# Building CLI Tools with inquirerer + +A comprehensive guide to building interactive command-line interfaces using inquirerer, the TypeScript-first CLI library used across Constructive projects. + +## When to Apply + +Use this skill when: +- Creating a new CLI application +- Adding interactive prompts to an existing tool +- Building project scaffolding or setup wizards +- Creating configuration builders +- Implementing any command-line interface in a Constructive project + +## Installation + +```bash +pnpm add inquirerer +``` + +## Quick Start + +```typescript +import { Inquirerer } from 'inquirerer'; + +const prompter = new Inquirerer(); + +const answers = await prompter.prompt({}, [ + { + type: 'text', + name: 'projectName', + message: 'What is your project name?', + required: true + }, + { + type: 'confirm', + name: 'useTypeScript', + message: 'Use TypeScript?', + default: true + } +]); + +console.log(answers); +prompter.close(); +``` + +## Question Types + +inquirerer supports six question types: + +### Text Question + +Collect string input: + +```typescript +{ + type: 'text', + name: 'username', + message: 'Enter your username', + required: true, + pattern: '^[a-z0-9_]+$', // Regex validation + default: 'user' +} +``` + +### Number Question + +Collect numeric input: + +```typescript +{ + type: 'number', + name: 'port', + message: 'Server port?', + default: 3000, + validate: (port) => port > 0 && port < 65536 +} +``` + +### Confirm Question + +Yes/no questions: + +```typescript +{ + type: 'confirm', + name: 'proceed', + message: 'Continue with installation?', + default: true +} +``` + +### List Question + +Select one option (no search): + +```typescript +{ + type: 'list', + name: 'license', + message: 'Choose a license', + options: ['MIT', 'Apache-2.0', 'GPL-3.0'], + default: 'MIT', + maxDisplayLines: 5 +} +``` + +### Autocomplete Question + +Select with fuzzy search: + +```typescript +{ + type: 'autocomplete', + name: 'framework', + message: 'Choose a framework', + options: [ + { name: 'React', value: 'react' }, + { name: 'Vue.js', value: 'vue' }, + { name: 'Angular', value: 'angular' } + ], + allowCustomOptions: true, + maxDisplayLines: 8 +} +``` + +### Checkbox Question + +Multi-select with search: + +```typescript +{ + type: 'checkbox', + name: 'features', + message: 'Select features', + options: ['Auth', 'Database', 'API', 'Testing'], + default: ['Auth', 'API'], + returnFullResults: false, // Only return selected items + required: true +} +``` + +## Question Properties + +All questions support these base properties: + +| Property | Type | Description | +|----------|------|-------------| +| `name` | string | Property name in result object | +| `type` | string | Question type | +| `message` | string | Prompt message to display | +| `default` | any | Default value | +| `required` | boolean | Whether input is required | +| `validate` | function | Custom validation function | +| `sanitize` | function | Transform input before storing | +| `pattern` | string | Regex pattern for validation | +| `when` | function | Conditional display | +| `dependsOn` | string[] | Question dependencies | +| `_` | boolean | Mark as positional argument | +| `alias` | string/string[] | Short flag aliases | +| `defaultFrom` | string | Dynamic default from resolver | +| `setFrom` | string | Auto-set value from resolver | + +## Validation + +### Pattern Validation + +```typescript +{ + type: 'text', + name: 'email', + message: 'Enter email', + pattern: '^[^@]+@[^@]+\\.[^@]+$' +} +``` + +### Custom Validation + +```typescript +{ + type: 'text', + name: 'password', + message: 'Enter password', + validate: (input) => { + if (input.length < 8) { + return { success: false, reason: 'Must be at least 8 characters' }; + } + return { success: true }; + } +} +``` + +### Sanitization + +```typescript +{ + type: 'text', + name: 'tags', + message: 'Enter tags (comma-separated)', + sanitize: (input) => input.split(',').map(t => t.trim()) +} +``` + +## Conditional Questions + +Show questions based on previous answers: + +```typescript +const questions = [ + { + type: 'confirm', + name: 'useDatabase', + message: 'Need a database?', + default: false + }, + { + type: 'list', + name: 'database', + message: 'Which database?', + options: ['PostgreSQL', 'MySQL', 'SQLite'], + when: (answers) => answers.useDatabase === true + } +]; +``` + +## Question Dependencies + +Ensure questions appear in correct order: + +```typescript +[ + { + type: 'checkbox', + name: 'services', + message: 'Select services', + options: ['Auth', 'Storage', 'Functions'] + }, + { + type: 'text', + name: 'authProvider', + message: 'Auth provider?', + dependsOn: ['services'], + when: (answers) => answers.services?.includes('Auth') + } +] +``` + +## Positional Arguments + +Allow values without flags using `_: true`: + +```typescript +const questions = [ + { _: true, name: 'source', type: 'text', message: 'Source file' }, + { _: true, name: 'dest', type: 'text', message: 'Destination' } +]; + +// Users can run: mycli input.txt output.txt +// Instead of: mycli --source input.txt --dest output.txt +``` + +## Aliases + +Define short flags: + +```typescript +{ + name: 'workspace', + type: 'confirm', + alias: 'w', // or ['w', 'ws'] for multiple + message: 'Create workspace?' +} + +// Users can run: mycli -w +// Instead of: mycli --workspace +``` + +## Dynamic Defaults with Resolvers + +Auto-populate defaults from git, npm, or custom sources: + +```typescript +const questions = [ + { + type: 'text', + name: 'author', + message: 'Author name?', + defaultFrom: 'git.user.name' // Auto-fills from git config + }, + { + type: 'text', + name: 'email', + message: 'Email?', + defaultFrom: 'git.user.email' + }, + { + type: 'text', + name: 'year', + message: 'Copyright year?', + defaultFrom: 'date.year' + } +]; +``` + +### Built-in Resolvers + +| Resolver | Description | +|----------|-------------| +| `git.user.name` | Git global user name | +| `git.user.email` | Git global user email | +| `npm.whoami` | Logged in npm user | +| `date.year` | Current year | +| `date.month` | Current month | +| `date.day` | Current day | +| `date.iso` | ISO date (YYYY-MM-DD) | +| `workspace.name` | Package name from nearest package.json | +| `workspace.license` | License from package.json | +| `workspace.author` | Author from package.json | + +### Custom Resolvers + +```typescript +import { registerDefaultResolver } from 'inquirerer'; + +registerDefaultResolver('cwd.name', () => { + return process.cwd().split('/').pop(); +}); + +// Use in questions +{ + type: 'text', + name: 'projectName', + defaultFrom: 'cwd.name' +} +``` + +### setFrom vs defaultFrom + +- `defaultFrom`: Sets as default, user can override +- `setFrom`: Auto-sets value, skips prompt entirely + +```typescript +{ + type: 'text', + name: 'createdAt', + setFrom: 'date.iso' // Auto-set, no prompt shown +} +``` + +## CLI Class + +For complete CLI applications with argument parsing: + +```typescript +import { CLI, CommandHandler, CLIOptions } from 'inquirerer'; + +const handler: CommandHandler = async (argv, prompter, options) => { + const answers = await prompter.prompt(argv, [ + { type: 'text', name: 'name', message: 'Name?', required: true } + ]); + console.log('Hello,', answers.name); +}; + +const options: Partial = { + version: 'myapp@1.0.0', + minimistOpts: { + alias: { v: 'version', h: 'help' } + } +}; + +const cli = new CLI(handler, options); +await cli.run(); +``` + +## CLI Utilities + +inquirerer provides utilities for building CLIs: + +```typescript +import { + parseArgv, // Parse command-line arguments + extractFirst, // Extract subcommand + getPackageVersion, // Get version from package.json + cliExitWithError // Exit with error message +} from 'inquirerer'; + +const argv = parseArgv(process.argv); +const { first: command, newArgv } = extractFirst(argv); + +switch (command) { + case 'init': + await handleInit(newArgv); + break; + case 'build': + await handleBuild(newArgv); + break; + default: + console.log('Unknown command'); +} +``` + +## UI Components + +### Spinner + +```typescript +import { createSpinner } from 'inquirerer'; + +const spinner = createSpinner('Loading...'); +spinner.start(); +await doWork(); +spinner.succeed('Done!'); +// Or: spinner.fail('Failed'), spinner.warn('Warning') +``` + +### Progress Bar + +```typescript +import { createProgress } from 'inquirerer'; + +const progress = createProgress('Installing'); +progress.start(); +for (let i = 0; i < items.length; i++) { + await processItem(items[i]); + progress.update((i + 1) / items.length); +} +progress.complete('Installed'); +``` + +### Streaming Text + +```typescript +import { createStream } from 'inquirerer'; + +const stream = createStream({ showCursor: true }); +stream.start(); +for await (const token of llmResponse) { + stream.append(token); +} +stream.done(); +``` + +## Non-Interactive Mode + +For CI/CD environments: + +```typescript +const prompter = new Inquirerer({ + noTty: true, // Disable interactive mode + useDefaults: true // Use defaults without prompting +}); +``` + +## Complete Example + +```typescript +import { Inquirerer, Question, parseArgv } from 'inquirerer'; + +interface ProjectConfig { + name: string; + description: string; + typescript: boolean; + features: string[]; +} + +const argv = parseArgv(process.argv); +const prompter = new Inquirerer(); + +const questions: Question[] = [ + { + _: true, + type: 'text', + name: 'name', + message: 'Project name', + required: true, + pattern: '^[a-z0-9-]+$', + defaultFrom: 'cwd.name' + }, + { + type: 'text', + name: 'description', + message: 'Description', + default: 'My awesome project' + }, + { + type: 'confirm', + name: 'typescript', + alias: 'ts', + message: 'Use TypeScript?', + default: true + }, + { + type: 'checkbox', + name: 'features', + message: 'Select features', + options: ['ESLint', 'Prettier', 'Jest', 'Husky'], + default: ['ESLint', 'Prettier'] + } +]; + +const config = await prompter.prompt(argv, questions); +console.log('Creating project:', config); +prompter.close(); +``` + +Run interactively or with CLI args: + +```bash +# Interactive +node setup.js + +# With args +node setup.js my-project --ts --features ESLint,Jest +``` + +## Best Practices + +1. **Always close the prompter** when done: `prompter.close()` +2. **Use TypeScript interfaces** for type-safe answers +3. **Provide defaults** for better UX +4. **Use `defaultFrom`** for dynamic defaults from git/npm +5. **Support non-interactive mode** for CI/CD +6. **Use positional arguments** for common inputs +7. **Add aliases** for frequently used flags +8. **Validate early** with patterns and custom validators + +## References + +- npm package: https://www.npmjs.com/package/inquirerer +- Related skill: `inquirerer-anti-patterns` for what NOT to do +- Related skill: `pnpm-workspace` for monorepo setup diff --git a/.agents/skills/dev-utils/references/inquirerer-cli.md b/.agents/skills/dev-utils/references/inquirerer-cli.md new file mode 100644 index 0000000..8789053 --- /dev/null +++ b/.agents/skills/dev-utils/references/inquirerer-cli.md @@ -0,0 +1,194 @@ +--- +name: inquirerer-cli +description: Build interactive CLI tools with inquirerer, appstash, and yanse. Use when asked to "create a CLI", "build a command-line tool", "add prompts", "create interactive prompts", "store CLI config", "add terminal colors", or when building any CLI application in a Constructive project. Also triggers on "commander", "inquirer.js", "yargs" to redirect to inquirerer. +compatibility: inquirerer, appstash, yanse, Node.js 18+, TypeScript +metadata: + author: constructive-io + version: "2.0.0" +--- + +# inquirerer CLI Development + +Build interactive command-line interfaces using Constructive's CLI toolkit: **inquirerer** for prompts and argument parsing, **appstash** for persistent storage, and **yanse** for terminal colors. + +## When to Apply + +Use this skill when: +- **Building CLIs:** Creating interactive prompts, argument parsing, subcommands +- **Storing config:** Auth profiles, caching, logging, temp files +- **Terminal output:** Colors, spinners, progress bars, streaming text +- **Reviewing CLI code:** Redirecting from forbidden libraries to inquirerer + +## Quick Start + +```bash +pnpm add inquirerer appstash yanse +``` + +```typescript +import { Inquirerer } from 'inquirerer'; + +const prompter = new Inquirerer(); + +const answers = await prompter.prompt({}, [ + { + type: 'text', + name: 'projectName', + message: 'What is your project name?', + required: true + }, + { + type: 'confirm', + name: 'useTypeScript', + message: 'Use TypeScript?', + default: true + } +]); + +console.log(answers); +prompter.close(); +``` + +## Critical: Forbidden Libraries + +Do NOT use these libraries in Constructive projects: + +| Library | Use Instead | +|---------|-------------| +| `commander` | `inquirerer` CLI class | +| `inquirer` / `inquirer.js` | `inquirerer` | +| `yargs` | `inquirerer` parseArgv | +| `prompts` / `enquirer` | `inquirerer` | +| `chalk` | `yanse` | +| `ora` | `inquirerer` createSpinner | +| `cli-progress` | `inquirerer` createProgress | +| `minimist` (directly) | `inquirerer` parseArgv | + +## Question Types + +| Type | Description | +|------|-------------| +| `text` | String input with pattern validation | +| `number` | Numeric input with custom validation | +| `confirm` | Yes/no boolean | +| `list` | Select one option (no search) | +| `autocomplete` | Select with fuzzy search | +| `checkbox` | Multi-select with search | + +## CLI Application Pattern + +```typescript +import { CLI, CommandHandler, CLIOptions } from 'inquirerer'; + +const handler: CommandHandler = async (argv, prompter, options) => { + const answers = await prompter.prompt(argv, [ + { type: 'text', name: 'name', message: 'Name?', required: true } + ]); + console.log('Hello,', answers.name); +}; + +const cli = new CLI(handler, { + version: 'myapp@1.0.0', + minimistOpts: { alias: { v: 'version', h: 'help' } } +}); + +await cli.run(); +``` + +## Terminal Colors with yanse + +```typescript +// Use yanse instead of chalk (same API, works with CJS + ESM) +import chalk from 'yanse'; + +console.log(chalk.green('Success!')); +console.log(chalk.red.bold('Error!')); +``` + +## Persistent Storage with appstash + +```typescript +import { appstash, resolve } from 'appstash'; + +const dirs = appstash('mycli', { ensure: true }); +// dirs.config → ~/.mycli/config +// dirs.cache → ~/.mycli/cache +// dirs.data → ~/.mycli/data +// dirs.logs → ~/.mycli/logs +// dirs.tmp → /tmp/mycli + +const configFile = resolve(dirs, 'config', 'auth.json'); +``` + +## UI Components + +```typescript +import { createSpinner, createProgress, createStream } from 'inquirerer'; + +// Spinner +const spinner = createSpinner('Loading...'); +spinner.start(); +await doWork(); +spinner.succeed('Done!'); + +// Progress bar +const progress = createProgress('Installing'); +progress.start(); +progress.update(0.5); +progress.complete('Installed'); + +// Streaming text (for LLM output) +const stream = createStream({ showCursor: true }); +stream.start(); +stream.append(token); +stream.done(); +``` + +## Dynamic Defaults + +```typescript +{ + type: 'text', + name: 'author', + message: 'Author?', + defaultFrom: 'git.user.name' // Auto-fills from git config +} +``` + +Built-in resolvers: `git.user.name`, `git.user.email`, `npm.whoami`, `date.year`, `date.iso`, `workspace.name`, `workspace.license`, `workspace.author`. + +## Non-Interactive Mode (CI/CD) + +```typescript +const prompter = new Inquirerer({ + noTty: true, + useDefaults: true +}); +``` + +## Best Practices + +1. **Always close the prompter** when done: `prompter.close()` +2. **Use TypeScript interfaces** for type-safe answers +3. **Support non-interactive mode** for CI/CD +4. **Use `defaultFrom`** for dynamic defaults from git/npm +5. **Use appstash** for all persistent CLI storage +6. **Use yanse** instead of chalk for terminal colors +7. **Environment variables override** stored config + +## Reference Guide + +Consult these reference files for detailed documentation on specific topics: + +| Reference | Topic | Consult When | +|-----------|-------|--------------| +| [references/cli-building.md](references/cli-building.md) | Building CLIs with inquirerer | Question types, validation, conditional questions, positional args, aliases, resolvers, CLI class | +| [references/anti-patterns.md](references/anti-patterns.md) | Forbidden CLI libraries | Reviewing code that uses commander/inquirer.js/yargs, choosing a CLI library | +| [references/appstash.md](references/appstash.md) | CLI directory management | Auth profiles, caching, logging, update checking, environment overrides, testing | +| [references/yanse.md](references/yanse.md) | Terminal color styling | Replacing chalk imports, color API reference | + +## Cross-References + +Related skills (separate from this skill): +- `constructive-pnpm` — Monorepo setup for CLI packages +- `pgpm` — pgpm CLI is built with inquirerer diff --git a/.agents/skills/dev-utils/references/inquirerer-yanse.md b/.agents/skills/dev-utils/references/inquirerer-yanse.md new file mode 100644 index 0000000..1f78460 --- /dev/null +++ b/.agents/skills/dev-utils/references/inquirerer-yanse.md @@ -0,0 +1,43 @@ +--- +name: yanse-terminal-colors +description: Use yanse for terminal color styling instead of chalk. Use when adding colors to CLI output or terminal logs in Constructive projects. +--- + +Use `yanse` for terminal color styling instead of `chalk`. + +## When to Apply + +Use this skill when adding colors to CLI output or terminal logs. + +## Overview + +`yanse` is a chalk-compatible terminal color library with zero dependencies. It exists because chalk v5+ is ESM-only, which causes issues in CommonJS projects. + +The API is identical to chalk - if you know chalk, you know yanse. + +## Anti-Pattern + +```typescript +// Do not use chalk +import chalk from 'chalk'; +``` + +## Pattern + +```typescript +// Use yanse instead +import chalk from 'yanse'; +``` + +That's it. Same API, just a different import. + +## Why yanse? + +- Zero dependencies (chalk has dependencies) +- Supports both ESM and CommonJS (chalk v5+ is ESM-only) +- Identical API to chalk - no learning curve + +## References + +- [yanse on npm](https://www.npmjs.com/package/yanse) +- [chalk documentation](https://github.com/chalk/chalk) (API is the same) diff --git a/.agents/skills/dev-utils/references/readme-formatting.md b/.agents/skills/dev-utils/references/readme-formatting.md new file mode 100644 index 0000000..89e5186 --- /dev/null +++ b/.agents/skills/dev-utils/references/readme-formatting.md @@ -0,0 +1,264 @@ +--- +name: readme-formatting +description: Format README files with Constructive branding including header logos and badges. Use when creating new packages, publishing modules, or when asked to "add header image", "add badges", "format README", or "standardize README". +compatibility: npm, pnpm, pgpm, any package type +metadata: + author: constructive-io + version: "1.0.0" +--- + +# README Formatting (Constructive Standard) + +Format README files with consistent Constructive branding including centered header logos and appropriate badges for different package types. + +## When to Apply + +Use this skill when: +- Creating a new npm, pnpm, or pgpm package +- Publishing a module to npm or pgpm registry +- Asked to add header images or badges to a README +- Standardizing README formatting across packages +- A README is missing the Constructive logo header + +## Header Logo + +All Constructive packages should include a centered logo at the top of the README, immediately after the package title. + +### Standard Header (Most Packages) + +```markdown +# package-name + +

+ +

+``` + +### Root Repository Header + +For root-level README files (monorepo roots), use the filled logo with smaller height: + +```markdown +# Repository Name + +

+ +

+``` + +## Badges + +Badges appear in a centered paragraph below the logo. Include badges based on package type and visibility. + +### Badge Types + +| Badge | When to Use | Example | +|-------|-------------|---------| +| CI Status | Public repos with GitHub Actions | Shows build status | +| License | All public packages | MIT, Apache-2.0, etc. | +| npm Version | npm/pnpm packages published to registry | Shows current version | +| Downloads | Optional, for popular packages | Shows download count | + +### Badge Templates + +**CI Status Badge:** +```html + + + +``` + +**MIT License Badge:** +```html + + + +``` + +**npm Version Badge (from package.json in repo):** +```html + + + +``` + +**npm Version Badge (from npm registry):** +```html + + + +``` + +**Downloads Badge (optional):** +```html + + + +``` + +### Complete Badge Section + +```markdown +

+ + + + + + + + + +

+``` + +## Package Type Guidelines + +### npm/pnpm Packages (Published to npm) + +Include: Logo + CI badge + License badge + Version badge + +```markdown +# @scope/package-name + +

+ +

+ +

+ + + + + + + + + +

+ +Package description here. +``` + +### pgpm Packages (PostgreSQL Modules) + +Include: Logo + CI badge + License badge (no npm version badge) + +```markdown +# @pgpm/module-name + +

+ +

+ +

+ + + + + + +

+ +Module description here. +``` + +### Internal/Private Packages + +Include: Logo only (no badges needed) + +```markdown +# package-name + +

+ +

+ +Package description here. +``` + +### Packages with Custom License + +For packages with "All Rights Reserved" or custom licenses, omit the license badge: + +```markdown +

+ + + +

+``` + +## Complete README Template + +```markdown +# @scope/package-name + +

+ +

+ +

+ + + + + + + + + +

+ +Brief description of what the package does. + +## Installation + +\`\`\`bash +pnpm add @scope/package-name +\`\`\` + +## Usage + +\`\`\`typescript +import { something } from '@scope/package-name'; +\`\`\` + +## API + +Document the main exports and functions. + +## License + +MIT (or appropriate license) +``` + +## Checklist + +When formatting a README, verify: + +- [ ] Package title is at the top (h1) +- [ ] Logo is centered below the title +- [ ] Logo uses correct URL (outline-logo.svg for packages, logo.svg for roots) +- [ ] Logo height is appropriate (250px for packages, 150px for roots) +- [ ] Badges are in a separate centered paragraph below logo +- [ ] Badge links point to correct URLs +- [ ] Version badge uses correct package name (with scope if applicable) +- [ ] License badge matches actual license in package +- [ ] CI badge points to correct workflow file + +## Common Mistakes + +1. **Missing logo**: Always add the header logo +2. **Wrong logo URL**: Use the raw.githubusercontent.com URL, not a relative path +3. **Badges in wrong order**: CI, then License, then Version +4. **Missing badge links**: Each badge image should be wrapped in an anchor tag +5. **Incorrect package name in version badge**: Must match exactly what's published to npm +6. **Using license badge for "All Rights Reserved"**: Only use for open source licenses + +## References + +- Logo assets: https://github.com/constructive-io/constructive/tree/main/assets +- Shields.io badges: https://shields.io/ +- Related skill: `pnpm-publishing` for publishing workflow +- Related skill: `pgpm` (`references/publishing.md`) for pgpm module publishing diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..e808ed4 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,25 @@ +# AGENTS.md + +This file provides guidance to AI agents working with the `constructive-io/dev-utils` monorepo. + +## Available Skills + +| Skill | Description | +|-------|-------------| +| **dev-utils** | Developer utilities — inquirerer (CLI prompts), yanse (terminal colors), appStash (state persistence), inflekt (string inflection), and 25+ other packages | + +## Skill Structure + +``` +.agents/skills/ + dev-utils/ + SKILL.md # Skill definition + references/ # Detailed documentation + inquirerer-cli.md + inquirerer-cli-building.md + inquirerer-appstash.md + inquirerer-yanse.md + inquirerer-anti-patterns.md + readme-formatting.md + dev-utils.zip # Packaged for distribution +```