diff --git a/package.json b/package.json index eb5d989..6d1a09c 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "type": "module", "version": "0.1.0", "private": true, - "packageManager": "pnpm@10.18.0", + "packageManager": "pnpm@10.18.1", "description": "A monorepo template for developing Vue libraries", "author": "Gleb Stepanov ", "license": "MIT", diff --git a/packages/cli/bin/create-app.mjs b/packages/cli/bin/create-app.mjs new file mode 100644 index 0000000..80a5aa3 --- /dev/null +++ b/packages/cli/bin/create-app.mjs @@ -0,0 +1,14 @@ +#!/usr/bin/env node + +import { dirname, resolve } from 'node:path' +import process from 'node:process' +import { fileURLToPath, pathToFileURL } from 'node:url' + +const __filename = fileURLToPath(import.meta.url) +const __dirname = dirname(__filename) + +const cliPath = resolve(__dirname, '../dist/index.js') +import(pathToFileURL(cliPath).href).catch((err) => { + console.error('Failed to load CLI', err) + process.exit(1) +}) diff --git a/packages/cli/package.json b/packages/cli/package.json new file mode 100644 index 0000000..b747ed2 --- /dev/null +++ b/packages/cli/package.json @@ -0,0 +1,40 @@ +{ + "name": "@glstep/vue-ts-app-lib", + "type": "module", + "version": "0.1.0", + "description": "CLI tool to scaffold Vue/TS library projects", + "author": "Gleb Stepanov ", + "license": "MIT", + "bin": { + "create-app": "./bin/create-app.mjs" + }, + "files": [ + "bin", + "dist", + "src", + "templates" + ], + "scripts": { + "build": "tsup src/index.ts --format esm --dts", + "dev": "tsup src/index.ts --format esm --watch", + "typecheck": "tsc --noEmit", + "test": "vitest", + "test-run": "vitest run", + "test-unit": "vitest run src", + "test-e2e": "vitest run tests/e2e" + }, + "dependencies": { + "execa": "^9.6.0", + "fast-glob": "^3.3.3", + "ora": "^9.0.0", + "picocolors": "^1.1.1", + "prompts": "^2.4.2" + }, + "devDependencies": { + "@types/node": "^24.6.2", + "@types/prompts": "^2.4.9", + "tsup": "^8.5.0", + "typescript": "^5.9.0", + "vitest": "^3.2.4" + } +} diff --git a/packages/cli/src/commands/__tests__/create.spec.ts b/packages/cli/src/commands/__tests__/create.spec.ts new file mode 100644 index 0000000..e69de29 diff --git a/packages/cli/src/commands/create.ts b/packages/cli/src/commands/create.ts new file mode 100644 index 0000000..a4e7ed1 --- /dev/null +++ b/packages/cli/src/commands/create.ts @@ -0,0 +1,59 @@ +import { resolve } from 'node:path' +import process from 'node:process' +import ora from 'ora' +import pc from 'picocolors' +import { promptProjectOptions } from '../prompts/project' +import { scaffoldProject } from '../templates/index' +import { ensureDir, isDirEmpty } from '../utils/fs' +import { installDependencies } from '../utils/install' + +export async function createCommand(targetDir: string): Promise { + const root = resolve(process.cwd(), targetDir) + + // Check if directory is empty + const isEmpty = await isDirEmpty(root) + if (!isEmpty) { + console.error( + pc.red(`✖ Directory ${pc.bold(targetDir)} is not empty`), + ) + process.exit(1) + } + + // Ensure directory exists + await ensureDir(root) + + // Get user configuration + const config = await promptProjectOptions(targetDir) + if (!config) { + process.exit(0) + } + + // Scaffold project + const spinner = ora('Creating project structure...').start() + + try { + await scaffoldProject(root, config) + spinner.succeed('Project structure created') + } + catch (err) { + spinner.fail('Failed to create project structure') + console.error(err) + process.exit(1) + } + + // Install dependencies + if (config.installDeps) { + await installDependencies(root) + } + + // Success message + console.log(`\n${pc.green('✔')} Project created successfully!\n`) + console.log(pc.bold('Next steps:')) + console.log(` ${pc.cyan('cd')} ${config.projectName}`) + if (!config.installDeps) { + console.log(` ${pc.cyan('pnpm install')}`) + } + console.log(` ${pc.cyan('pnpm dev')}\n`) + + console.log(pc.dim('Happy coding! 🚀\n')) +} diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts new file mode 100644 index 0000000..d6b5264 --- /dev/null +++ b/packages/cli/src/index.ts @@ -0,0 +1,15 @@ +#!/usr/bin/env node +import process from 'node:process' +import { createCommand } from './commands/create' + +async function main() { + const args = process.argv.slice(2) + const targetDir = args[0] || '.' + + await createCommand(targetDir) +} + +main().catch((err) => { + console.error(err) + process.exit(1) +}) diff --git a/packages/cli/src/prompts/__tests__/project.spec.ts b/packages/cli/src/prompts/__tests__/project.spec.ts new file mode 100644 index 0000000..e69de29 diff --git a/packages/cli/src/prompts/project.ts b/packages/cli/src/prompts/project.ts new file mode 100644 index 0000000..2e5af4f --- /dev/null +++ b/packages/cli/src/prompts/project.ts @@ -0,0 +1,105 @@ +import process from 'node:process' +import pc from 'picocolors' +import prompts from 'prompts' + +export interface ProjectOptions { + projectName: string + packageName: string + scope: string + includeLib: boolean + includeLibTs: boolean + includePlayground: boolean + includeDocs: boolean + installDeps: boolean +} + +export async function promptProjectOptions(targetDir: string): Promise { + console.log(`\n${pc.bold(pc.cyan('Create a Vue/TS Library project'))}\n`) + + const response = await prompts( + [ + { + type: 'text', + name: 'projectName', + message: 'Project name: ', + initial: targetDir === '.' ? 'my-lib' : targetDir, + validate: (value: string) => value.trim() ? true : 'Project name cannot be empty', + }, + { + type: 'text', + name: 'scope', + message: 'npm scope (optional, without @): ', + initial: '', + format: (value: string) => value.trim(), + }, + { + type: 'text', + name: 'packageName', + message: 'Package name (npm package name): ', + initial: 'lib', + validate: (value: string) => value.trim() ? true : 'Package name cannot be empty', + }, + { + type: 'multiselect', + name: 'packages', + message: 'Select packages to include: ', + choices: [ + { + title: 'Vue 3 library', + value: 'lib', + selected: false, + }, + { + title: 'TypeScript library', + value: 'libTs', + selected: false, + }, + { + title: `Playground (Vue 3 + Vite) ${pc.bold('(recommended)')}`, + value: 'playground', + selected: true, + }, + { + title: 'Documentation (VitePress)', + value: 'docs', + selected: false, + }, + ], + min: 1, + hint: '- Space to select. Return to submit', + instructions: false, + }, + { + type: 'confirm', + name: 'installDeps', + message: 'Install dependencies after the project is created?', + initial: true, + }, + ], + { + onCancel: () => { + console.log(`${pc.red('\n✖')} Operation cancelled.`) + process.exit(0) + }, + }, + ) + + if (!response.projectName) { + return null + } + + const packages: string[] = response.packages ?? [] + + const options: ProjectOptions = { + projectName: response.projectName, + packageName: response.packageName, + scope: response.scope, + includeLib: packages.includes('lib'), + includeLibTs: packages.includes('libTs'), + includePlayground: packages.includes('playground'), + includeDocs: packages.includes('docs'), + installDeps: response.installDeps, + } + + return options +} diff --git a/packages/cli/src/templates/__tests__/index.spec.ts b/packages/cli/src/templates/__tests__/index.spec.ts new file mode 100644 index 0000000..e69de29 diff --git a/packages/cli/src/templates/index.ts b/packages/cli/src/templates/index.ts new file mode 100644 index 0000000..e09bd34 --- /dev/null +++ b/packages/cli/src/templates/index.ts @@ -0,0 +1,244 @@ +import type { ProjectOptions } from '../prompts/project' +import { writeFile } from 'node:fs/promises' +import { dirname, join } from 'node:path' +import { fileURLToPath } from 'node:url' +import { copyDir, readJson, writeJson } from '../utils/fs' +import { replaceInFiles } from '../utils/replace' + +const __dirname = dirname(fileURLToPath(import.meta.url)) + +const packagesDir = join(__dirname, '../../..') + +export async function scaffoldProject(targetDir: string, config: ProjectOptions): Promise { + await createBaseFiles(targetDir, config) +} + +// TODO: adjust content of package.json, programmatically +async function createBaseFiles(targetDir: string, config: ProjectOptions): Promise { + const rootPkg = { + name: config.projectName, + type: 'module', + version: '0.1.0', + private: true, + packageManager: 'pnpm@10.18.1', + description: `${config.projectName} - A Vue/TS library`, + author: 'Your name ', + license: 'MIT', + engines: { + node: '>=24.7.0', + pnpm: '>=10.16.1', + }, + scripts: { + 'dev': 'pnpm -F playground dev', + 'test': 'pnpm --if-present -r run test', + 'test-ci': 'pnpm --if-present -r run test-ci', + 'docs': 'pnpm -F docs run dev', + 'docs-build': 'pnpm -F docs run build', + 'lint': 'eslint .', + 'lint-fix': 'eslint . --fix', + 'build': buildScripts(config), + }, + devDependencies: { + '@antfu/eslint-config': '^5.4.1', + '@tsconfig/node24': '^24.0.1', + '@types/node': '24.6.2', + '@vitejs/plugin-vue': '^6.0.1', + '@vue/compiler-dom': '^3.5.22', + '@vue/test-utils': '^2.4.6', + '@vue/tsconfig': '^0.8.1', + 'eslint': '^9.36.0', + 'eslint-plugin-format': '^1.0.1', + 'jsdom': '^27.0.0', + 'typescript': '^5.9.0', + 'vite': '^7.1.0', + 'vitest': '^3.2.4', + 'vue': '^3.5.0', + 'vue-tsc': '^3.0.0', + }, + } + + await writeJson(join(targetDir, 'package.json'), rootPkg) + + const configFiles = [ + 'tsconfig.json', + 'tsconfig.config.json', + 'eslint.config.js', + ] + + for (const file of configFiles) { + const sourcePath = join(packagesDir, file) + const destPath = join(targetDir, file) + await copyDir(sourcePath, destPath) + } + + const gitignore = `# Dependencies +node_modules/ +.pnpm-store/ + +# Build outputs +dist/ +*.tsbuildinfo + +# Environment +.env +.env.local + +# IDE +.vscode/ +.idea/ +*.swp +*.swo + +# OS +.DS_Store +Thumbs.db + +# Logs +*.log +npm-debug.log* +pnpm-debug.log* +` + + await writeFile(join(targetDir, '.gitignore'), gitignore) + + const readme = `# ${config.projectName} +A Vue/TypeScript library created with @glstep/app + +## Development + +\`\`\`bash +# Install dependencies +pnpm install + +# Start playground +pnpm dev + +# Run tests +pnpm test + +# Build +pnpm build +\`\`\` + +## Packages + +${config.includeLib ? '- **lib**: Vue component library\n' : ''}${config.includeLibTs ? '- **lib-ts**: TypeScript utility library\n' : ''}${config.includePlayground ? '- **playground**: Development playground\n' : ''}${config.includeDocs ? '- **docs**: Documentation site\n' : ''} +` + + await writeFile(join(targetDir, 'README.md'), readme) +} + +function buildScripts(config: ProjectOptions): string { + const scripts: string[] = [] + + if (config.includeLib) + scripts.push('pnpm -F @SCOPE@/lib run build') + if (config.includeLibTs) + scripts.push('pnpm -F @SCOPE@/lib-ts run build') + if (config.includePlayground) + scripts.push('pnpm -F playground run build') + if (config.includeDocs) + scripts.push('pnpm -F docs run build') + + return scripts.join(' && ') +} + +async function copyPackage(packageName: string, targetDir: string): Promise { + const sourceDir = join(packagesDir, 'packages', packageName) + const destDir = join(targetDir, 'packages', packageName) + + await copyDir(sourceDir, destDir) +} + +async function createWorkspaceFile(targetDir: string): Promise { + const content = `packages: + - 'packages/*' + ` + + await writeFile(join(targetDir, 'pnpm-workspace.yaml'), content) +} + +async function replacePlaceholders(targetDir: string, config: ProjectOptions): Promise { + const fullScope = config.scope ? `@${config.scope}` : '' + const fullLibName = fullScope ? `${fullScope}/${config.packageName}` : config.packageName + + const replacements = [ + { from: /@glstep\/lib-ts/g, to: fullScope ? `${fullScope}/lib-ts` : 'lib-ts' }, + { from: /@glstep\/lib/g, to: fullLibName }, + { from: /@glstep/g, to: fullScope }, + { from: /vue-lib-monorepo-template/g, to: config.projectName }, + { from: /Gleb Stepanov /g, to: 'Your Name ' }, + { from: '@SCOPE@', to: fullScope || config.projectName }, + ] + + await replaceInFiles(targetDir, replacements) + + await updatePackageJsonFiles(targetDir, config, fullScope, fullLibName) +} + +async function updatePackageJsonFiles( + targetDir: string, + config: ProjectOptions, + fullScope: string, + fullLibName: string, +): Promise { + const updates = [] + + if (config.includeLib) { + updates.push({ + path: join(targetDir, 'packages', 'lib', 'package.json'), + name: fullLibName, + }) + } + + if (config.includeLibTs) { + updates.push({ + path: join(targetDir, 'packages', 'lib-ts', 'package.json'), + name: fullScope ? `${fullScope}/lib-ts` : 'lib-ts', + }) + } + + if (config.includePlayground) { + updates.push({ + path: join(targetDir, 'packages', 'playground', 'package.json'), + name: 'playground', + }) + } + + if (config.includeDocs) { + updates.push({ + path: join(targetDir, 'packages', 'docs', 'package.json'), + name: 'docs', + }) + } + + for (const { path, name } of updates) { + try { + const pkg = await readJson(path) + pkg.name = name + + // Update dependencies + if (pkg.dependencies) { + const newDeps: Record = {} + for (const [key, value] of Object.entries(pkg.dependencies)) { + if (key === '@glstep/lib') { + newDeps[fullLibName] = value as string + } + else if (key.startsWith('@glstep/')) { + const pkgName = key.split('/')[1] + newDeps[fullScope ? `${fullScope}/${pkgName}` : pkgName] = value as string + } + else { + newDeps[key] = value as string + } + } + pkg.dependencies = newDeps + } + + await writeJson(path, pkg) + } + catch (err) { + console.warn(`Warning: Could not update package.json at ${path}: ${(err as Error).message}`) + } + } +} diff --git a/packages/cli/src/utils/__tests__/fs.spec.ts b/packages/cli/src/utils/__tests__/fs.spec.ts new file mode 100644 index 0000000..e81bc83 --- /dev/null +++ b/packages/cli/src/utils/__tests__/fs.spec.ts @@ -0,0 +1,195 @@ +import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { afterEach, beforeEach, describe, expect, it } from 'vitest' +import { copyDir, ensureDir, isDirEmpty, readJson, writeJson } from '../fs' + +describe('fs utilities', () => { + let tempDir: string + + beforeEach(() => { + tempDir = mkdtempSync(join(tmpdir(), 'fs-utils-test')) + }) + + afterEach(() => { + if (existsSync(tempDir)) { + rmSync(tempDir, { recursive: true, force: true }) + } + }) + + describe('ensureDir', () => { + it('should create new dir if does not exist', async () => { + const newDir = join(tempDir, 'new-dir') + expect(existsSync(newDir)).toBe(false) + await ensureDir(newDir) + expect(existsSync(newDir)).toBe(true) + }) + + it('should not fail, if dir already exists', async () => { + const existingDir = join(tempDir, 'existing-dir') + await ensureDir(existingDir) + await expect(ensureDir(existingDir)).resolves.toBeUndefined() + expect(existsSync(existingDir)).toBe(true) + }) + + it('should create nested directories', async () => { + const nestedDir = join(tempDir, 'level1', 'level2', 'level3') + await ensureDir(nestedDir) + expect(existsSync(nestedDir)).toBe(true) + }) + }) + + describe('isDirEmpty', () => { + it('should return true for non existent dir', async () => { + const nonExistentDir = join(tempDir, 'non-existent') + expect(existsSync(nonExistentDir)).toBe(false) + const result = await isDirEmpty(nonExistentDir) + expect(result).toBe(true) + }) + + it('should return true for empty dir', async () => { + const emptyDir = join(tempDir, 'empty-dir') + await ensureDir(emptyDir) + const result = await isDirEmpty(emptyDir) + expect(result).toBe(true) + }) + + it('should return false for non empty dir', async () => { + const nonEmptyDir = join(tempDir, 'non-empty-dir') + await ensureDir(nonEmptyDir) + writeFileSync(join(nonEmptyDir, 'file.txt'), 'Hello, World!', { encoding: 'utf8', flag: 'wx' }) + const result = await isDirEmpty(nonEmptyDir) + expect(result).toBe(false) + }) + + it('should return false for dir with sub-dir', async () => { + const dirWithSubDir = join(tempDir, 'dir-with-sub-dir') + const subDir = join(dirWithSubDir, 'sub-dir') + await ensureDir(subDir) + const result = await isDirEmpty(dirWithSubDir) + expect(result).toBe(false) + }) + + it('should return false for dir with hidden files', async () => { + const dirWithHiddenFile = join(tempDir, 'dir-with-hidden-file') + await ensureDir(dirWithHiddenFile) + writeFileSync(join(dirWithHiddenFile, '.hiddenfile'), 'This is a hidden file', { encoding: 'utf8', flag: 'wx' }) + const result = await isDirEmpty(dirWithHiddenFile) + expect(result).toBe(false) + }) + + it('should return false for dir with multiple items', async () => { + const dirWithMultipleItems = join(tempDir, 'dir-with-multiple-items') + await ensureDir(dirWithMultipleItems) + writeFileSync(join(dirWithMultipleItems, 'file1.txt'), 'File 1', { encoding: 'utf8', flag: 'wx' }) + writeFileSync(join(dirWithMultipleItems, 'file2.txt'), 'File 2', { encoding: 'utf8', flag: 'wx' }) + await ensureDir(join(dirWithMultipleItems, 'sub-dir')) + const result = await isDirEmpty(dirWithMultipleItems) + expect(result).toBe(false) + }) + }) + + describe('copyDir', () => { + it('should copy directory with all files', async () => { + const srcDir = join(tempDir, 'src') + const destDir = join(tempDir, 'dest') + + await ensureDir(srcDir) + writeFileSync(join(srcDir, 'file1.txt'), 'File 1', { encoding: 'utf8', flag: 'wx' }) + writeFileSync(join(srcDir, 'file2.txt'), 'File 2', { encoding: 'utf8', flag: 'wx' }) + + await copyDir(srcDir, destDir) + + expect(existsSync(destDir)).toBe(true) + expect(existsSync(join(destDir, 'file1.txt'))).toBe(true) + expect(existsSync(join(destDir, 'file2.txt'))).toBe(true) + expect(readFileSync(join(destDir, 'file1.txt'), 'utf8')).toBe('File 1') + expect(readFileSync(join(destDir, 'file2.txt'), 'utf8')).toBe('File 2') + }) + + it('should copy dir with nested structure', async () => { + const srcDir = join(tempDir, 'src-nested') + await ensureDir(join(srcDir, 'level1', 'level2')) + writeFileSync(join(srcDir, 'level1', 'level2', 'file.txt'), 'Nested File', { encoding: 'utf8', flag: 'wx' }) + writeFileSync(join(srcDir, 'level1', 'file.txt'), 'Another nested File', { encoding: 'utf8', flag: 'wx' }) + writeFileSync(join(srcDir, 'file.txt'), 'Root File', { encoding: 'utf8', flag: 'wx' }) + + const destDir = join(tempDir, 'dest-nested') + await copyDir(srcDir, destDir) + + expect(existsSync(join(destDir, 'file.txt'))).toBe(true) + expect(readFileSync(join(destDir, 'file.txt'), 'utf8')).toBe('Root File') + + expect(existsSync(join(destDir, 'level1', 'file.txt'))).toBe(true) + expect(readFileSync(join(destDir, 'level1', 'file.txt'), 'utf8')).toBe('Another nested File') + + expect(existsSync(join(destDir, 'level1', 'level2', 'file.txt'))).toBe(true) + expect(readFileSync(join(destDir, 'level1', 'level2', 'file.txt'), 'utf8')).toBe('Nested File') + }) + }) + + describe('readJson', async () => { + it('should read and parse JSON file', async () => { + const jsonFile = join(tempDir, 'data.json') + const jsonData = { + 'name': 'Test', + 'value': 42, + 'nested': { a: 1, b: [1, 2, 3] }, + 'array': [10, 20, 30], + 'arrayObject': [{ x: 1 }, { y: 2 }], + 'bool': true, + 'nullValue': null, + 'string-test': 'value', + } + writeFileSync(jsonFile, JSON.stringify(jsonData, null, 2), { encoding: 'utf8', flag: 'wx' }) + + const result = await readJson(jsonFile) + expect(result).toEqual(jsonData) + expect(result.name).toEqual(jsonData.name) + expect(result['string-test']).toEqual(jsonData['string-test']) + }) + + it('should throw error for invalid JSON', async () => { + const invalidJsonFile = join(tempDir, 'invalid.json') + writeFileSync(invalidJsonFile, '{ invalid json ', { encoding: 'utf8', flag: 'wx' }) + + const result = readJson(invalidJsonFile) + await expect(result).rejects.toThrow(SyntaxError) + }) + }) + + describe('writeJson', async () => { + it('should write JSON object to file', async () => { + const jsonFile = join(tempDir, 'output.json') + const jsonData = { + title: 'Output Test', + count: 100, + items: ['a', 'b', 'c'], + details: { key: 'value' }, + isActive: false, + nullField: null, + } + + await writeJson(jsonFile, jsonData) + + expect(existsSync(jsonFile)).toBe(true) + const fileContent = readFileSync(jsonFile, { encoding: 'utf8' }) + const parsedContent = JSON.parse(fileContent) + expect(parsedContent).toEqual(jsonData) + expect(fileContent.endsWith('\n')).toBe(true) + }) + + it('should overwrite existing file', async () => { + const jsonFile = join(tempDir, 'overwrite.json') + const initialData = { initial: true } + writeFileSync(jsonFile, JSON.stringify(initialData), { encoding: 'utf8', flag: 'wx' }) + + const newData = { updated: true, number: 123 } + await writeJson(jsonFile, newData) + + const fileContent = readFileSync(jsonFile, { encoding: 'utf8' }) + const parsedContent = JSON.parse(fileContent) + expect(parsedContent).toEqual(newData) + }) + }) +}) diff --git a/packages/cli/src/utils/__tests__/install.spec.ts b/packages/cli/src/utils/__tests__/install.spec.ts new file mode 100644 index 0000000..02047a4 --- /dev/null +++ b/packages/cli/src/utils/__tests__/install.spec.ts @@ -0,0 +1,281 @@ +import { existsSync, mkdtempSync, rmSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' + +// Declare mocks at module level (before any tests) +const execMock = vi.fn() +const spinnerMock = { start: vi.fn().mockReturnThis(), succeed: vi.fn(), fail: vi.fn() } + +vi.mock('node:child_process', () => ({ execSync: execMock })) +vi.mock('ora', () => ({ default: vi.fn(() => spinnerMock) })) +vi.mock('picocolors', () => ({ default: { yellow: (s: string) => s } })) + +describe('install utilities', () => { + let tempDir: string + + beforeEach(() => { + tempDir = mkdtempSync(join(tmpdir(), 'install-utils-test')) + vi.clearAllMocks() + }) + + afterEach(() => { + if (existsSync(tempDir)) { + rmSync(tempDir, { recursive: true, force: true }) + } + }) + + describe('successful installation', () => { + it('should execute pnpm install', async () => { + const { installDependencies } = await import('../install') + + await installDependencies(tempDir) + + expect(execMock).toHaveBeenCalledWith('pnpm install', { + cwd: tempDir, + stdio: 'ignore', + }) + }) + + it('should run command in specified directory', async () => { + const { installDependencies } = await import('../install') + + await installDependencies(tempDir) + + expect(execMock).toHaveBeenCalledWith('pnpm install', expect.objectContaining({ + cwd: tempDir, + })) + }) + + it('should show spinner and message during installation', async () => { + const { installDependencies } = await import('../install') + + await installDependencies(tempDir) + + expect(spinnerMock.start).toHaveBeenCalled() + }) + + it('should show success message on completion', async () => { + const { installDependencies } = await import('../install') + + await installDependencies(tempDir) + + expect(spinnerMock.succeed).toHaveBeenCalledWith('Dependencies installed successfully.') + }) + + it('should resolve without errors', async () => { + const { installDependencies } = await import('../install') + + await expect(installDependencies(tempDir)).resolves.toBeUndefined() + }) + + it('should create spinner with correct message', async () => { + const oraMock = vi.mocked(await import('ora')).default + + const { installDependencies } = await import('../install') + await installDependencies(tempDir) + + expect(oraMock).toHaveBeenCalledWith('Installing dependencies...') + }) + }) + + describe('failed installation', () => { + it('should handle failed pnpm install command', async () => { + execMock.mockImplementationOnce(() => { + throw new Error('pnpm command failed') + }) + + const exitSpy = vi.spyOn(process, 'exit').mockImplementation((code?: number | string | null | undefined) => { + throw new Error(`process.exit: ${code}`) + }) + const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {}) + + const { installDependencies } = await import('../install') + + await expect(installDependencies(tempDir)).rejects.toThrow('process.exit: 0') + + expect(execMock).toHaveBeenCalled() + expect(logSpy).toHaveBeenCalled() + expect(exitSpy).toHaveBeenCalledWith(0) + + exitSpy.mockRestore() + logSpy.mockRestore() + }) + + it('should show failure message', async () => { + execMock.mockImplementationOnce(() => { + throw new Error('pnpm failed') + }) + + vi.spyOn(process, 'exit').mockImplementation((code?: number | string | null | undefined) => { + throw new Error(`exit: ${code}`) + }) + vi.spyOn(console, 'log').mockImplementation(() => {}) + + const { installDependencies } = await import('../install') + + await expect(installDependencies(tempDir)).rejects.toThrow() + + expect(spinnerMock.fail).toHaveBeenCalledWith('Failed to install dependencies.') + }) + + it('should prompt user to run command manually', async () => { + execMock.mockImplementationOnce(() => { + throw new Error('fail') + }) + + vi.spyOn(process, 'exit').mockImplementation((code?: number | string | null | undefined) => { + throw new Error(`exit: ${code}`) + }) + const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {}) + + const { installDependencies } = await import('../install') + + await expect(installDependencies(tempDir)).rejects.toThrow() + + expect(logSpy).toHaveBeenCalled() + const logCall = logSpy.mock.calls[0][0] + expect(logCall).toContain('pnpm install') + expect(logCall).toContain('manually') + }) + + it('should exit process after failure with code 0', async () => { + execMock.mockImplementationOnce(() => { + throw new Error('fail') + }) + + const exitSpy = vi.spyOn(process, 'exit').mockImplementation((code?: number | string | null | undefined) => { + throw new Error(`exit: ${code}`) + }) + vi.spyOn(console, 'log').mockImplementation(() => {}) + + const { installDependencies } = await import('../install') + + await expect(installDependencies(tempDir)).rejects.toThrow('exit: 0') + + expect(exitSpy).toHaveBeenCalledWith(0) + }) + }) + + describe('command execution', () => { + it('should run with stdio: ignore', async () => { + const { installDependencies } = await import('../install') + + await installDependencies(tempDir) + + expect(execMock).toHaveBeenCalledWith('pnpm install', expect.objectContaining({ + stdio: 'ignore', + })) + }) + + it('should pass correct parameters to execSync', async () => { + const { installDependencies } = await import('../install') + + await installDependencies(tempDir) + + expect(execMock).toHaveBeenCalledWith('pnpm install', { + cwd: tempDir, + stdio: 'ignore', + }) + }) + }) + + describe('spinner behavior', () => { + it('should start spinner before installation', async () => { + const { installDependencies } = await import('../install') + + await installDependencies(tempDir) + + expect(spinnerMock.start).toHaveBeenCalled() + const startCallOrder = spinnerMock.start.mock.invocationCallOrder[0] + const execCallOrder = execMock.mock.invocationCallOrder[0] + expect(startCallOrder).toBeLessThan(execCallOrder) + }) + + it('should stop spinner after installation', async () => { + const { installDependencies } = await import('../install') + + await installDependencies(tempDir) + + expect(spinnerMock.succeed).toHaveBeenCalled() + expect(spinnerMock.fail).not.toHaveBeenCalled() + }) + + it('should stop spinner on error', async () => { + execMock.mockImplementationOnce(() => { + throw new Error('fail') + }) + + vi.spyOn(process, 'exit').mockImplementation((code?: number | string | null | undefined) => { + throw new Error(`exit: ${code}`) + }) + vi.spyOn(console, 'log').mockImplementation(() => {}) + + const { installDependencies } = await import('../install') + + await expect(installDependencies(tempDir)).rejects.toThrow() + + expect(spinnerMock.fail).toHaveBeenCalled() + expect(spinnerMock.succeed).not.toHaveBeenCalled() + }) + }) + + describe('edge cases', () => { + it('should handle missing pnpm', async () => { + execMock.mockImplementationOnce(() => { + const error = new Error('Command not found: pnpm') as NodeJS.ErrnoException + error.code = 'ENOENT' + throw error + }) + + vi.spyOn(process, 'exit').mockImplementation((code?: number | string | null | undefined) => { + throw new Error(`exit: ${code}`) + }) + vi.spyOn(console, 'log').mockImplementation(() => {}) + + const { installDependencies } = await import('../install') + + await expect(installDependencies(tempDir)).rejects.toThrow('exit: 0') + + expect(spinnerMock.fail).toHaveBeenCalled() + }) + + it('should handle permission error', async () => { + execMock.mockImplementationOnce(() => { + const error = new Error('Permission denied') as NodeJS.ErrnoException + error.code = 'EACCES' + throw error + }) + + vi.spyOn(process, 'exit').mockImplementation((code?: number | string | null | undefined) => { + throw new Error(`exit: ${code}`) + }) + vi.spyOn(console, 'log').mockImplementation(() => {}) + + const { installDependencies } = await import('../install') + + await expect(installDependencies(tempDir)).rejects.toThrow('exit: 0') + + expect(spinnerMock.fail).toHaveBeenCalled() + }) + + it('should handle network issues', async () => { + execMock.mockImplementationOnce(() => { + const error = new Error('Network timeout') as NodeJS.ErrnoException + error.code = 'ETIMEDOUT' + throw error + }) + + vi.spyOn(process, 'exit').mockImplementation((code?: number | string | null | undefined) => { + throw new Error(`exit: ${code}`) + }) + vi.spyOn(console, 'log').mockImplementation(() => {}) + + const { installDependencies } = await import('../install') + + await expect(installDependencies(tempDir)).rejects.toThrow('exit: 0') + + expect(spinnerMock.fail).toHaveBeenCalled() + }) + }) +}) diff --git a/packages/cli/src/utils/__tests__/replace.spec.ts b/packages/cli/src/utils/__tests__/replace.spec.ts new file mode 100644 index 0000000..f7656f4 --- /dev/null +++ b/packages/cli/src/utils/__tests__/replace.spec.ts @@ -0,0 +1,275 @@ +import { Buffer } from 'node:buffer' +import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { afterEach, beforeEach, describe, expect, it } from 'vitest' +import { replaceInFiles } from '../replace' + +describe('replace utilities', () => { + let tempDir: string + + beforeEach(() => { + tempDir = mkdtempSync(join(tmpdir(), 'replace-utils-test')) + }) + + afterEach(() => { + if (existsSync(tempDir)) { + rmSync(tempDir, { recursive: true, force: true }) + } + }) + + describe('string replacement', () => { + it('should replace string in text files', async () => { + const filePath = join(tempDir, 'test.txt') + const contentBefore = 'Hello World' + writeFileSync(filePath, contentBefore, 'utf-8') + await replaceInFiles(tempDir, [{ from: 'World', to: 'Vitest' }]) + const contentAfter = readFileSync(filePath, 'utf-8') + expect(contentAfter).toBe('Hello Vitest') + }) + + it('should replace multiple occurrences of a string', async () => { + const filePath = join(tempDir, 'test.txt') + const contentBefore = 'foo bar foo baz foo' + writeFileSync(filePath, contentBefore, 'utf-8') + await replaceInFiles(tempDir, [{ from: 'foo', to: 'qux' }]) + const contentAfter = readFileSync(filePath, 'utf-8') + expect(contentAfter).toBe('qux bar qux baz qux') + }) + + it('should replace strings in multiple files', async () => { + const contDir = join(tempDir, 'subdir') + mkdirSync(contDir) + const filePath1 = join(contDir, 'file1.txt') + const filePath2 = join(contDir, 'file2.txt') + const contentBefore1 = 'apple banana' + const contentBefore2 = 'banana cherry' + writeFileSync(filePath1, contentBefore1, 'utf-8') + writeFileSync(filePath2, contentBefore2, 'utf-8') + await replaceInFiles(tempDir, [{ from: 'banana', to: 'orange' }]) + const contentAfter1 = readFileSync(filePath1, 'utf-8') + const contentAfter2 = readFileSync(filePath2, 'utf-8') + expect(contentAfter1).toBe('apple orange') + expect(contentAfter2).toBe('orange cherry') + }) + + it('should not modify files if string not found', async () => { + const filePath = join(tempDir, 'test.txt') + const contentBefore = 'No matching string here.' + writeFileSync(filePath, contentBefore, 'utf-8') + await replaceInFiles(tempDir, [{ from: 'nonexistent', to: 'replacement' }]) + const contentAfter = readFileSync(filePath, 'utf-8') + expect(contentAfter).toBe(contentBefore) + }) + + it('should handle case-sensitive replacements', async () => { + const filePath = join(tempDir, 'test.txt') + const contentBefore = 'Case Sensitive CASE sensitive case SENSITIVE' + writeFileSync(filePath, contentBefore, 'utf-8') + await replaceInFiles(tempDir, [{ from: 'case', to: 'word' }, { from: 'SENSITIVE', to: 'SOFT' }]) + const contentAfter = readFileSync(filePath, 'utf-8') + expect(contentAfter).toBe('Case Sensitive CASE sensitive word SOFT') + }) + }) + + describe('regex replacement', () => { + it('should replace pattern in text files', async () => { + const filePath = join(tempDir, 'test.txt') + const contentBefore = 'The quick brown fox jumps over the lazy dog.' + writeFileSync(filePath, contentBefore, 'utf-8') + await replaceInFiles(tempDir, [{ from: /\bthe\b/gi, to: 'a' }]) + const contentAfter = readFileSync(filePath, 'utf-8') + expect(contentAfter).toBe('a quick brown fox jumps over a lazy dog.') + }) + + it('should replace multiple occurrences of a pattern', async () => { + const filePath = join(tempDir, 'test.txt') + const contentBefore = 'foo1 bar2 foo3 baz4 foo5' + writeFileSync(filePath, contentBefore, 'utf-8') + await replaceInFiles(tempDir, [{ from: /foo\d/g, to: 'qux' }]) + const contentAfter = readFileSync(filePath, 'utf-8') + expect(contentAfter).toBe('qux bar2 qux baz4 qux') + }) + + it('should replace patterns in multiple files', async () => { + const contDir = join(tempDir, 'subdir') + mkdirSync(contDir) + const filePath1 = join(contDir, 'file1.txt') + const filePath2 = join(contDir, 'file2.txt') + const contentBefore1 = 'item123 item4560' + const contentBefore2 = 'item789 item0120' + writeFileSync(filePath1, contentBefore1, 'utf-8') + writeFileSync(filePath2, contentBefore2, 'utf-8') + await replaceInFiles(tempDir, [{ from: /item\d{3}/g, to: 'product' }]) + const contentAfter1 = readFileSync(filePath1, 'utf-8') + const contentAfter2 = readFileSync(filePath2, 'utf-8') + expect(contentAfter1).toBe('product product0') + expect(contentAfter2).toBe('product product0') + }) + }) + + describe('file type handling', () => { + it('should process files with supported text extensions', async () => { + const filePath = join(tempDir, 'test.md') + const contentBefore = 'Markdown file with foo.' + writeFileSync(filePath, contentBefore, 'utf-8') + await replaceInFiles(tempDir, [{ from: 'foo', to: 'bar' }]) + const contentAfter = readFileSync(filePath, 'utf-8') + expect(contentAfter).toBe('Markdown file with bar.') + }) + + it('should skip binary files', async () => { + const filePath = join(tempDir, 'image.png') + const binaryContent = Buffer.from([0x89, 0x50, 0x4E, 0x47]) + writeFileSync(filePath, binaryContent) + await replaceInFiles(tempDir, [{ from: 'foo', to: 'bar' }]) + const contentAfter = readFileSync(filePath) + expect(contentAfter).toEqual(binaryContent) + }) + + it('should skip unsupported file types', async () => { + const filePath = join(tempDir, 'archive.zip') + const binaryContent = Buffer.from([0x50, 0x4B, 0x03, 0x04]) + writeFileSync(filePath, binaryContent) + await replaceInFiles(tempDir, [{ from: 'foo', to: 'bar' }]) + const contentAfter = readFileSync(filePath) + expect(contentAfter).toEqual(binaryContent) + }) + + it('should handle mixed file types in a directory', async () => { + const contDir = join(tempDir, 'mixed') + mkdirSync(contDir) + const textFilePath = join(contDir, 'file.txt') + const binaryFilePath = join(contDir, 'file.bin') + const mdFilePath = join(contDir, 'file.md') + const tsFilePath = join(contDir, 'file.ts') + writeFileSync(textFilePath, 'foo in text file', 'utf-8') + writeFileSync(binaryFilePath, Buffer.from([0x00, 0x01, 0x02, 0x03])) + writeFileSync(mdFilePath, 'foo in markdown file', 'utf-8') + writeFileSync(tsFilePath, 'const foo = 42;', 'utf-8') + await replaceInFiles(tempDir, [{ from: 'foo', to: 'bar' }]) + const textContentAfter = readFileSync(textFilePath, 'utf-8') + const binaryContentAfter = readFileSync(binaryFilePath) + const mdContentAfter = readFileSync(mdFilePath, 'utf-8') + const tsContentAfter = readFileSync(tsFilePath, 'utf-8') + expect(textContentAfter).toBe('bar in text file') + expect(binaryContentAfter).toEqual(Buffer.from([0x00, 0x01, 0x02, 0x03])) + expect(mdContentAfter).toBe('bar in markdown file') + expect(tsContentAfter).toBe('const bar = 42;') + }) + + it('should process package.json regardless of extension check', async () => { + const filePath = join(tempDir, 'package.json') + const contentBefore = '{"name": "foo-package", "version": "1.0.0"}' + writeFileSync(filePath, contentBefore, 'utf-8') + await replaceInFiles(tempDir, [{ from: 'foo', to: 'bar' }]) + const contentAfter = readFileSync(filePath, 'utf-8') + expect(contentAfter).toBe('{"name": "bar-package", "version": "1.0.0"}') + }) + }) + + describe('directory handling', () => { + it('should ignore node_modules and dist directories', async () => { + const nodeModulesDir = join(tempDir, 'node_modules') + const distDir = join(tempDir, 'dist') + mkdirSync(nodeModulesDir) + mkdirSync(distDir) + const filePath1 = join(nodeModulesDir, 'file.txt') + const filePath2 = join(distDir, 'file.txt') + writeFileSync(filePath1, 'foo in node_modules', 'utf-8') + writeFileSync(filePath2, 'foo in dist', 'utf-8') + await replaceInFiles(tempDir, [{ from: 'foo', to: 'bar' }]) + const contentAfter1 = readFileSync(filePath1, 'utf-8') + const contentAfter2 = readFileSync(filePath2, 'utf-8') + expect(contentAfter1).toBe('foo in node_modules') + expect(contentAfter2).toBe('foo in dist') + }) + + it('should ignore .git directories', async () => { + const gitDir = join(tempDir, '.git') + mkdirSync(gitDir) + const filePath = join(gitDir, 'config') + writeFileSync(filePath, 'foo in .git config', 'utf-8') + await replaceInFiles(tempDir, [{ from: 'foo', to: 'bar' }]) + const contentAfter = readFileSync(filePath, 'utf-8') + expect(contentAfter).toBe('foo in .git config') + }) + + it('should handle nested directories', async () => { + const nestedDir = join(tempDir, 'a/b/c/d/e') + mkdirSync(nestedDir, { recursive: true }) + const filePath = join(nestedDir, 'test.txt') + const contentBefore = 'Nested foo here' + writeFileSync(filePath, contentBefore, 'utf-8') + await replaceInFiles(tempDir, [{ from: 'foo', to: 'bar' }]) + const contentAfter = readFileSync(filePath, 'utf-8') + expect(contentAfter).toBe('Nested bar here') + }) + }) + + describe('edge cases', () => { + it('should handle empty files', async () => { + const filePath = join(tempDir, 'empty.txt') + writeFileSync(filePath, '', 'utf-8') + await replaceInFiles(tempDir, [{ from: 'foo', to: 'bar' }]) + const contentAfter = readFileSync(filePath, 'utf-8') + expect(contentAfter).toBe('') + }) + + it('should handle files with special characters', async () => { + const filePath = join(tempDir, 'special.txt') + const contentBefore = 'Special chars: !@#$%^&*()_+[]{}|;:\'",.<>?/`~' + writeFileSync(filePath, contentBefore, 'utf-8') + await replaceInFiles(tempDir, [{ from: '@#$', to: '###' }]) + const contentAfter = readFileSync(filePath, 'utf-8') + expect(contentAfter).toBe('Special chars: !###%^&*()_+[]{}|;:\'",.<>?/`~') + }) + + it('should handle very large files efficiently', async () => { + const filePath = join(tempDir, 'large.txt') + const largeContent = 'foo '.repeat(10000) // ~40KB + writeFileSync(filePath, largeContent, 'utf-8') + await replaceInFiles(tempDir, [{ from: 'foo', to: 'bar' }]) + const contentAfter = readFileSync(filePath, 'utf-8') + expect(contentAfter).toBe('bar '.repeat(10000)) + }) + + it('should handle files with no extensions', async () => { + const filePath = join(tempDir, 'README') + const contentBefore = 'This is a README file with foo.' + writeFileSync(filePath, contentBefore, 'utf-8') + await replaceInFiles(tempDir, [{ from: 'foo', to: 'bar' }]) + const contentAfter = readFileSync(filePath, 'utf-8') + expect(contentAfter).not.toBe('This is a README file with bar.') + }) + }) + + describe('content integrity', () => { + it('should preserve file encoding', async () => { + const filePath = join(tempDir, 'utf8.txt') + const contentBefore = 'Café Münsterländer' + writeFileSync(filePath, contentBefore, 'utf-8') + await replaceInFiles(tempDir, [{ from: 'Münsterländer', to: 'Rheinländer' }]) + const contentAfter = readFileSync(filePath, 'utf-8') + expect(contentAfter).toBe('Café Rheinländer') + }) + + it('should preserve line endings', async () => { + const filePath = join(tempDir, 'lineendings.txt') + const contentBefore = 'Line one.\r\nLine two.\r\nLine three.' + writeFileSync(filePath, contentBefore, 'utf-8') + await replaceInFiles(tempDir, [{ from: 'Line', to: 'Sentence' }]) + const contentAfter = readFileSync(filePath, 'utf-8') + expect(contentAfter).toBe('Sentence one.\r\nSentence two.\r\nSentence three.') + }) + + it('should preserve whitespace, when no replacement occurs', async () => { + const filePath = join(tempDir, 'whitespace.txt') + const contentBefore = ' Leading and trailing whitespace \n\tTabbed line' + writeFileSync(filePath, contentBefore, 'utf-8') + await replaceInFiles(tempDir, [{ from: 'nonexistent', to: 'replacement' }]) + const contentAfter = readFileSync(filePath, 'utf-8') + expect(contentAfter).toBe(contentBefore) + }) + }) +}) diff --git a/packages/cli/src/utils/fs.ts b/packages/cli/src/utils/fs.ts new file mode 100644 index 0000000..2e203ef --- /dev/null +++ b/packages/cli/src/utils/fs.ts @@ -0,0 +1,29 @@ +import { existsSync } from 'node:fs' +import { cp, mkdir, readdir, readFile, writeFile } from 'node:fs/promises' + +export async function ensureDir(dir: string): Promise { + if (!existsSync(dir)) { + await mkdir(dir, { recursive: true }) + } +} + +export async function isDirEmpty(dir: string): Promise { + if (!existsSync(dir)) { + return true + } + const files = await readdir(dir) + return files.length === 0 +} + +export async function copyDir(src: string, dest: string): Promise { + await cp(src, dest, { recursive: true, force: true }) +} + +export async function readJson(path: string): Promise { + const content = await readFile(path, 'utf-8') + return JSON.parse(content) +} + +export async function writeJson(path: string, data: any): Promise { + await writeFile(path, `${JSON.stringify(data, null, 2)}\n`, 'utf-8') +} diff --git a/packages/cli/src/utils/install.ts b/packages/cli/src/utils/install.ts new file mode 100644 index 0000000..ecd6eee --- /dev/null +++ b/packages/cli/src/utils/install.ts @@ -0,0 +1,21 @@ +import { execSync } from 'node:child_process' +import process from 'node:process' +import ora from 'ora' +import pc from 'picocolors' + +export async function installDependencies(cwd: string): Promise { + const spinner = ora('Installing dependencies...').start() + + try { + execSync('pnpm install', { + cwd, + stdio: 'ignore', + }) + spinner.succeed('Dependencies installed successfully.') + } + catch { + spinner.fail('Failed to install dependencies.') + console.log(`\n${pc.yellow('Please run "pnpm install" manually.\n')}`) + process.exit(0) + } +} diff --git a/packages/cli/src/utils/replace.ts b/packages/cli/src/utils/replace.ts new file mode 100644 index 0000000..14177cf --- /dev/null +++ b/packages/cli/src/utils/replace.ts @@ -0,0 +1,70 @@ +import { readFile, writeFile } from 'node:fs/promises' +import { join } from 'node:path' +import fg from 'fast-glob' + +export interface Replacement { + from: string | RegExp + to: string +} + +export async function replaceInFiles(dir: string, replacements: Replacement[]): Promise { + const files = await fg('**/*', { + cwd: dir, + ignore: ['node_modules/**', '**/dist/**', '.git/**'], + dot: true, + onlyFiles: true, + }) + + const textExtensions = [ + '.js', + '.mjs', + '.cjs', + '.ts', + '.tsx', + '.jsx', + '.vue', + '.json', + '.html', + '.css', + '.scss', + '.md', + '.yml', + '.yaml', + '.txt', + ] + + for (const file of files) { + const filePath = join(dir, file) + const ext = file.substring(file.lastIndexOf('.')) + + if (!textExtensions.includes(ext) && !file.endsWith('package.json')) { + continue + } + + try { + let content = await readFile(filePath, 'utf-8') + let modified = false + + for (const { from, to } of replacements) { + const oldContent = content + if (typeof from === 'string') { + content = content.split(from).join(to) + } + else { + content = content.replace(from, to) + } + + if (content !== oldContent) { + modified = true + } + } + + if (modified) { + await writeFile(filePath, content, 'utf-8') + } + } + catch (err) { + console.error(`Error processing file ${filePath}:`, err) + } + } +} diff --git a/packages/cli/tsconfig.json b/packages/cli/tsconfig.json new file mode 100644 index 0000000..965d5e0 --- /dev/null +++ b/packages/cli/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "ES2022", + "rootDir": "src", + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": false, + "strict": true, + "declaration": true, + "declarationMap": true, + "emitDeclarationOnly": false, + "outDir": "dist", + "forceConsistentCasingInFileNames": true + }, + "include": ["src/**/*", "vitest.config.ts"], + "exclude": ["node_modules", "dist", "**/*.spec.ts"] +} diff --git a/packages/cli/tsup.config.ts b/packages/cli/tsup.config.ts new file mode 100644 index 0000000..3e6b3bf --- /dev/null +++ b/packages/cli/tsup.config.ts @@ -0,0 +1,14 @@ +import { defineConfig } from 'tsup' + +export default defineConfig({ + entry: ['./src/index.ts'], + format: ['esm'], + outDir: 'dist', + clean: true, + dts: { + resolve: true, + compilerOptions: { + composite: false, + }, + }, +}) diff --git a/packages/cli/vitest.config.ts b/packages/cli/vitest.config.ts new file mode 100644 index 0000000..3235eef --- /dev/null +++ b/packages/cli/vitest.config.ts @@ -0,0 +1,16 @@ +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + globals: true, + environment: 'node', + include: ['src/**/*.spec.ts', 'tests/**/*.spec.ts'], + coverage: { + provider: 'v8', + reporter: ['text', 'html', 'lcov'], + include: ['src/**/*.ts'], + exclude: ['src/**/*.spec.ts', 'tests/**/*.ts', 'node_modules/**', 'dist/**'], + }, + testTimeout: 15000, + }, +}) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ff38d96..dd27dff 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -60,6 +60,40 @@ importers: specifier: ^3.0.0 version: 3.1.0(typescript@5.9.3) + packages/cli: + dependencies: + execa: + specifier: ^9.6.0 + version: 9.6.0 + fast-glob: + specifier: ^3.3.3 + version: 3.3.3 + ora: + specifier: ^9.0.0 + version: 9.0.0 + picocolors: + specifier: ^1.1.1 + version: 1.1.1 + prompts: + specifier: ^2.4.2 + version: 2.4.2 + devDependencies: + '@types/node': + specifier: ^24.6.2 + version: 24.6.2 + '@types/prompts': + specifier: ^2.4.9 + version: 2.4.9 + tsup: + specifier: ^8.5.0 + version: 8.5.0(jiti@1.21.7)(postcss@8.5.6)(typescript@5.9.3)(yaml@2.8.1) + typescript: + specifier: ^5.9.0 + version: 5.9.3 + vitest: + specifier: ^3.2.4 + version: 3.2.4(@types/debug@4.1.12)(@types/node@24.6.2)(jiti@1.21.7)(jsdom@27.0.0(postcss@8.5.6))(yaml@2.8.1) + packages/docs: dependencies: '@glstep/lib': @@ -1028,9 +1062,16 @@ packages: cpu: [x64] os: [win32] + '@sec-ant/readable-stream@0.4.1': + resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==} + '@sinclair/typebox@0.27.8': resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} + '@sindresorhus/merge-streams@4.0.0': + resolution: {integrity: sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==} + engines: {node: '>=18'} + '@stylistic/eslint-plugin@5.4.0': resolution: {integrity: sha512-UG8hdElzuBDzIbjG1QDwnYH0MQ73YLXDFHgZzB4Zh/YJfnw8XNsloVtytqzx0I2Qky9THSdpTmi8Vjn/pf/Lew==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -1081,6 +1122,9 @@ packages: '@types/node@24.6.2': resolution: {integrity: sha512-d2L25Y4j+W3ZlNAeMKcy7yDsK425ibcAOO2t7aPTz6gNMH0z2GThtwENCDc0d/Pw9wgyRqE5Px1wkV7naz8ang==} + '@types/prompts@2.4.9': + resolution: {integrity: sha512-qTxFi6Buiu8+50/+3DGIWLHM6QuWsEKugJnnP6iv2Mc4ncxE4A/OJkjuVOA+5X0X1S/nq5VJRa8Lu+nwcvbrKA==} + '@types/unist@3.0.3': resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} @@ -1469,6 +1513,12 @@ packages: peerDependencies: esbuild: '>=0.17' + bundle-require@5.1.0: + resolution: {integrity: sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + peerDependencies: + esbuild: '>=0.18' + cac@6.7.14: resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} engines: {node: '>=8'} @@ -1499,6 +1549,10 @@ packages: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} + chalk@5.6.2: + resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==} + engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + change-case@5.4.4: resolution: {integrity: sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w==} @@ -1516,6 +1570,10 @@ packages: resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} engines: {node: '>= 8.10.0'} + chokidar@4.0.3: + resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} + engines: {node: '>= 14.16.0'} + ci-info@4.3.1: resolution: {integrity: sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA==} engines: {node: '>=8'} @@ -1528,6 +1586,10 @@ packages: resolution: {integrity: sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==} engines: {node: '>=18'} + cli-spinners@3.3.0: + resolution: {integrity: sha512-/+40ljC3ONVnYIttjMWrlL51nItDAbBrq2upN8BPyvGU/2n5Oxw3tbNwORCaNuNqLJnxGqOfjUuhsv7l5Q4IsQ==} + engines: {node: '>=18.20'} + cli-truncate@5.1.0: resolution: {integrity: sha512-7JDGG+4Zp0CsknDCedl0DYdaeOhc46QNpXi3NLQblkZpXXgA6LncLDUUyvrjSvZeF3VRQa+KiMGomazQrC1V8g==} engines: {node: '>=20'} @@ -1570,6 +1632,10 @@ packages: config-chain@1.1.13: resolution: {integrity: sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==} + consola@3.4.2: + resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==} + engines: {node: ^14.18.0 || >=16.10.0} + core-js-compat@3.45.1: resolution: {integrity: sha512-tqTt5T4PzsMIZ430XGviK4vzYSoeNJ6CXODi6c/voxOT6IZqBht5/EKaSNnYiEjjRYxjVz7DQIsOsY0XNi8PIA==} @@ -1942,6 +2008,10 @@ packages: resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} engines: {node: '>=10'} + execa@9.6.0: + resolution: {integrity: sha512-jpWzZ1ZhwUmeWRhS7Qv3mhpOhLfwI+uAX4e5fOcXqwMR7EcJ0pj2kV1CVzHVMX/LphnKWD3LObjZCoJ71lKpHw==} + engines: {node: ^18.19.0 || >=20.5.0} + expect-type@1.2.2: resolution: {integrity: sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==} engines: {node: '>=12.0.0'} @@ -1980,6 +2050,10 @@ packages: picomatch: optional: true + figures@6.1.0: + resolution: {integrity: sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==} + engines: {node: '>=18'} + file-entry-cache@8.0.0: resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} engines: {node: '>=16.0.0'} @@ -1996,6 +2070,9 @@ packages: resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} engines: {node: '>=10'} + fix-dts-default-cjs-exports@1.0.1: + resolution: {integrity: sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg==} + flat-cache@4.0.1: resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} engines: {node: '>=16'} @@ -2036,6 +2113,10 @@ packages: resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} engines: {node: '>=10'} + get-stream@9.0.1: + resolution: {integrity: sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==} + engines: {node: '>=18'} + get-tsconfig@4.10.1: resolution: {integrity: sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==} @@ -2103,6 +2184,10 @@ packages: resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} engines: {node: '>=10.17.0'} + human-signals@8.0.1: + resolution: {integrity: sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ==} + engines: {node: '>=18.18.0'} + iconv-lite@0.6.3: resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} engines: {node: '>=0.10.0'} @@ -2158,10 +2243,18 @@ packages: resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} engines: {node: '>=0.10.0'} + is-interactive@2.0.0: + resolution: {integrity: sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==} + engines: {node: '>=12'} + is-number@7.0.0: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} + is-plain-obj@4.1.0: + resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} + engines: {node: '>=12'} + is-potential-custom-element-name@1.0.1: resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} @@ -2169,6 +2262,14 @@ packages: resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} engines: {node: '>=8'} + is-stream@4.0.1: + resolution: {integrity: sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==} + engines: {node: '>=18'} + + is-unicode-supported@2.1.0: + resolution: {integrity: sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==} + engines: {node: '>=18'} + isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} @@ -2249,6 +2350,10 @@ packages: keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + kleur@3.0.3: + resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} + engines: {node: '>=6'} + levn@0.4.1: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} @@ -2298,6 +2403,10 @@ packages: lodash@4.17.21: resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + log-symbols@7.0.1: + resolution: {integrity: sha512-ja1E3yCr9i/0hmBVaM0bfwDjnGy8I/s6PP4DFp+yP+a+mrHO4Rm7DtmnqROTUkHIkqffC84YY7AeqX6oFk0WFg==} + engines: {node: '>=18'} + log-update@6.1.0: resolution: {integrity: sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==} engines: {node: '>=18'} @@ -2538,6 +2647,10 @@ packages: resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} engines: {node: '>=8'} + npm-run-path@6.0.0: + resolution: {integrity: sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==} + engines: {node: '>=18'} + nth-check@2.1.1: resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} @@ -2564,6 +2677,10 @@ packages: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} + ora@9.0.0: + resolution: {integrity: sha512-m0pg2zscbYgWbqRR6ABga5c3sZdEon7bSgjnlXC64kxtxLOyjRcbbUkLj7HFyy/FTD+P2xdBWu8snGhYI0jc4A==} + engines: {node: '>=20'} + p-limit@3.1.0: resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} engines: {node: '>=10'} @@ -2593,6 +2710,10 @@ packages: parse-imports-exports@0.2.4: resolution: {integrity: sha512-4s6vd6dx1AotCx/RCI2m7t7GCh5bDRUtGNvRfHSP2wbBQdMi67pPe7mtzmgwcaQ8VKK/6IB7Glfyu3qdZJPybQ==} + parse-ms@4.0.0: + resolution: {integrity: sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==} + engines: {node: '>=18'} + parse-statements@1.0.11: resolution: {integrity: sha512-HlsyYdMBnbPQ9Jr/VgJ1YF4scnldvJpJxCVx6KgqPL4dxppsWrJHCIIxQXMJrqGnsRkNPATbeMJ8Yxu7JMsYcA==} @@ -2610,6 +2731,10 @@ packages: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} engines: {node: '>=8'} + path-key@4.0.0: + resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==} + engines: {node: '>=12'} + path-parse@1.0.7: resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} @@ -2750,6 +2875,14 @@ packages: resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + pretty-ms@9.3.0: + resolution: {integrity: sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ==} + engines: {node: '>=18'} + + prompts@2.4.2: + resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} + engines: {node: '>= 6'} + proto-list@1.2.4: resolution: {integrity: sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==} @@ -2773,6 +2906,10 @@ packages: resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} engines: {node: '>=8.10.0'} + readdirp@4.1.2: + resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} + engines: {node: '>= 14.18.0'} + refa@0.12.1: resolution: {integrity: sha512-J8rn6v4DBb2nnFqkqwy6/NnTYMcgLA+sLr0iIO41qpv0n+ngb7ksag2tMRl0inb1bbO/esUwzW1vbJi7K0sI0g==} engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} @@ -2914,6 +3051,10 @@ packages: std-env@3.9.0: resolution: {integrity: sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==} + stdin-discarder@0.2.2: + resolution: {integrity: sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==} + engines: {node: '>=18'} + string-argv@0.3.2: resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==} engines: {node: '>=0.6.19'} @@ -2946,6 +3087,10 @@ packages: resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==} engines: {node: '>=6'} + strip-final-newline@4.0.0: + resolution: {integrity: sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==} + engines: {node: '>=18'} + strip-indent@4.1.0: resolution: {integrity: sha512-OA95x+JPmL7kc7zCu+e+TeYxEiaIyndRx0OrBcK2QPPH09oAndr2ALvymxWA+Lx1PYYvFUm4O63pRkdJAaW96w==} engines: {node: '>=12'} @@ -3099,6 +3244,25 @@ packages: typescript: optional: true + tsup@8.5.0: + resolution: {integrity: sha512-VmBp77lWNQq6PfuMqCHD3xWl22vEoWsKajkF8t+yMBawlUS8JzEI+vOVMeuNZIuMML8qXRizFKi9oD5glKQVcQ==} + engines: {node: '>=18'} + hasBin: true + peerDependencies: + '@microsoft/api-extractor': ^7.36.0 + '@swc/core': ^1 + postcss: ^8.4.12 + typescript: '>=4.5.0' + peerDependenciesMeta: + '@microsoft/api-extractor': + optional: true + '@swc/core': + optional: true + postcss: + optional: true + typescript: + optional: true + type-check@0.4.0: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} @@ -3122,6 +3286,10 @@ packages: undici-types@7.13.0: resolution: {integrity: sha512-Ov2Rr9Sx+fRgagJ5AX0qvItZG/JKKoBRAVITs1zk7IqZGTJUwgUr7qoYBpWwakpWilTZFM98rG/AFRocu10iIQ==} + unicorn-magic@0.3.0: + resolution: {integrity: sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==} + engines: {node: '>=18'} + unist-util-is@6.0.0: resolution: {integrity: sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==} @@ -3434,6 +3602,10 @@ packages: resolution: {integrity: sha512-AyeEbWOu/TAXdxlV9wmGcR0+yh2j3vYPGOECcIj2S7MkrLyC7ne+oye2BKTItt0ii2PHk4cDy+95+LshzbXnGg==} engines: {node: '>=12.20'} + yoctocolors@2.1.2: + resolution: {integrity: sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==} + engines: {node: '>=18'} + zwitch@2.0.4: resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} @@ -4136,8 +4308,12 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.52.4': optional: true + '@sec-ant/readable-stream@0.4.1': {} + '@sinclair/typebox@0.27.8': {} + '@sindresorhus/merge-streams@4.0.0': {} + '@stylistic/eslint-plugin@5.4.0(eslint@9.37.0(jiti@1.21.7))': dependencies: '@eslint-community/eslint-utils': 4.9.0(eslint@9.37.0(jiti@1.21.7)) @@ -4189,6 +4365,11 @@ snapshots: dependencies: undici-types: 7.13.0 + '@types/prompts@2.4.9': + dependencies: + '@types/node': 24.6.2 + kleur: 3.0.3 + '@types/unist@3.0.3': {} '@types/web-bluetooth@0.0.20': {} @@ -4627,6 +4808,11 @@ snapshots: esbuild: 0.17.19 load-tsconfig: 0.2.5 + bundle-require@5.1.0(esbuild@0.25.10): + dependencies: + esbuild: 0.25.10 + load-tsconfig: 0.2.5 + cac@6.7.14: {} callsites@3.1.0: {} @@ -4660,6 +4846,8 @@ snapshots: ansi-styles: 4.3.0 supports-color: 7.2.0 + chalk@5.6.2: {} + change-case@5.4.4: {} character-entities@2.0.2: {} @@ -4682,6 +4870,10 @@ snapshots: optionalDependencies: fsevents: 2.3.3 + chokidar@4.0.3: + dependencies: + readdirp: 4.1.2 + ci-info@4.3.1: {} clean-regexp@1.0.0: @@ -4692,6 +4884,8 @@ snapshots: dependencies: restore-cursor: 5.1.0 + cli-spinners@3.3.0: {} + cli-truncate@5.1.0: dependencies: slice-ansi: 7.1.2 @@ -4724,6 +4918,8 @@ snapshots: ini: 1.3.8 proto-list: 1.2.4 + consola@3.4.2: {} + core-js-compat@3.45.1: dependencies: browserslist: 4.26.3 @@ -5226,6 +5422,21 @@ snapshots: signal-exit: 3.0.7 strip-final-newline: 2.0.0 + execa@9.6.0: + dependencies: + '@sindresorhus/merge-streams': 4.0.0 + cross-spawn: 7.0.6 + figures: 6.1.0 + get-stream: 9.0.1 + human-signals: 8.0.1 + is-plain-obj: 4.1.0 + is-stream: 4.0.1 + npm-run-path: 6.0.0 + pretty-ms: 9.3.0 + signal-exit: 4.1.0 + strip-final-newline: 4.0.0 + yoctocolors: 2.1.2 + expect-type@1.2.2: {} exsolve@1.0.7: {} @@ -5258,6 +5469,10 @@ snapshots: optionalDependencies: picomatch: 4.0.3 + figures@6.1.0: + dependencies: + is-unicode-supported: 2.1.0 + file-entry-cache@8.0.0: dependencies: flat-cache: 4.0.1 @@ -5273,6 +5488,12 @@ snapshots: locate-path: 6.0.0 path-exists: 4.0.0 + fix-dts-default-cjs-exports@1.0.1: + dependencies: + magic-string: 0.30.19 + mlly: 1.8.0 + rollup: 4.52.4 + flat-cache@4.0.1: dependencies: flatted: 3.3.3 @@ -5304,6 +5525,11 @@ snapshots: get-stream@6.0.1: {} + get-stream@9.0.1: + dependencies: + '@sec-ant/readable-stream': 0.4.1 + is-stream: 4.0.1 + get-tsconfig@4.10.1: dependencies: resolve-pkg-maps: 1.0.0 @@ -5374,6 +5600,8 @@ snapshots: human-signals@2.1.0: {} + human-signals@8.0.1: {} + iconv-lite@0.6.3: dependencies: safer-buffer: 2.1.2 @@ -5417,12 +5645,20 @@ snapshots: dependencies: is-extglob: 2.1.1 + is-interactive@2.0.0: {} + is-number@7.0.0: {} + is-plain-obj@4.1.0: {} + is-potential-custom-element-name@1.0.1: {} is-stream@2.0.1: {} + is-stream@4.0.1: {} + + is-unicode-supported@2.1.0: {} + isexe@2.0.0: {} jackspeak@3.4.3: @@ -5508,6 +5744,8 @@ snapshots: dependencies: json-buffer: 3.0.1 + kleur@3.0.3: {} + levn@0.4.1: dependencies: prelude-ls: 1.2.1 @@ -5558,6 +5796,11 @@ snapshots: lodash@4.17.21: {} + log-symbols@7.0.1: + dependencies: + is-unicode-supported: 2.1.0 + yoctocolors: 2.1.2 + log-update@6.1.0: dependencies: ansi-escapes: 7.1.1 @@ -5967,6 +6210,11 @@ snapshots: dependencies: path-key: 3.1.1 + npm-run-path@6.0.0: + dependencies: + path-key: 4.0.0 + unicorn-magic: 0.3.0 + nth-check@2.1.1: dependencies: boolbase: 1.0.0 @@ -5996,6 +6244,18 @@ snapshots: type-check: 0.4.0 word-wrap: 1.2.5 + ora@9.0.0: + dependencies: + chalk: 5.6.2 + cli-cursor: 5.0.0 + cli-spinners: 3.3.0 + is-interactive: 2.0.0 + is-unicode-supported: 2.1.0 + log-symbols: 7.0.1 + stdin-discarder: 0.2.2 + string-width: 8.1.0 + strip-ansi: 7.1.2 + p-limit@3.1.0: dependencies: yocto-queue: 0.1.0 @@ -6022,6 +6282,8 @@ snapshots: dependencies: parse-statements: 1.0.11 + parse-ms@4.0.0: {} + parse-statements@1.0.11: {} parse5@7.3.0: @@ -6034,6 +6296,8 @@ snapshots: path-key@3.1.1: {} + path-key@4.0.0: {} + path-parse@1.0.7: {} path-scurry@1.11.1: @@ -6142,6 +6406,15 @@ snapshots: ansi-styles: 5.2.0 react-is: 18.3.1 + pretty-ms@9.3.0: + dependencies: + parse-ms: 4.0.0 + + prompts@2.4.2: + dependencies: + kleur: 3.0.3 + sisteransi: 1.0.5 + proto-list@1.2.4: {} punycode@2.3.1: {} @@ -6160,6 +6433,8 @@ snapshots: dependencies: picomatch: 2.3.1 + readdirp@4.1.2: {} + refa@0.12.1: dependencies: '@eslint-community/regexpp': 4.12.1 @@ -6301,6 +6576,8 @@ snapshots: std-env@3.9.0: {} + stdin-discarder@0.2.2: {} + string-argv@0.3.2: {} string-width@4.2.3: @@ -6336,6 +6613,8 @@ snapshots: strip-final-newline@2.0.0: {} + strip-final-newline@4.0.0: {} + strip-indent@4.1.0: {} strip-json-comments@3.1.1: {} @@ -6500,6 +6779,34 @@ snapshots: - supports-color - ts-node + tsup@8.5.0(jiti@1.21.7)(postcss@8.5.6)(typescript@5.9.3)(yaml@2.8.1): + dependencies: + bundle-require: 5.1.0(esbuild@0.25.10) + cac: 6.7.14 + chokidar: 4.0.3 + consola: 3.4.2 + debug: 4.4.3 + esbuild: 0.25.10 + fix-dts-default-cjs-exports: 1.0.1 + joycon: 3.1.1 + picocolors: 1.1.1 + postcss-load-config: 6.0.1(jiti@1.21.7)(postcss@8.5.6)(yaml@2.8.1) + resolve-from: 5.0.0 + rollup: 4.52.4 + source-map: 0.8.0-beta.0 + sucrase: 3.35.0 + tinyexec: 0.3.2 + tinyglobby: 0.2.15 + tree-kill: 1.2.2 + optionalDependencies: + postcss: 8.5.6 + typescript: 5.9.3 + transitivePeerDependencies: + - jiti + - supports-color + - tsx + - yaml + type-check@0.4.0: dependencies: prelude-ls: 1.2.1 @@ -6514,6 +6821,8 @@ snapshots: undici-types@7.13.0: {} + unicorn-magic@0.3.0: {} + unist-util-is@6.0.0: dependencies: '@types/unist': 3.0.3 @@ -6846,4 +7155,6 @@ snapshots: yocto-queue@1.2.1: {} + yoctocolors@2.1.2: {} + zwitch@2.0.4: {} diff --git a/tsconfig.json b/tsconfig.json index 8dae9b0..a25f723 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -35,6 +35,7 @@ { "path": "./packages/playground/tsconfig.app.json" } + // CLI ], "files": [] }