diff --git a/.gitignore b/.gitignore index db062f6..33ccbb8 100644 --- a/.gitignore +++ b/.gitignore @@ -40,6 +40,9 @@ coverage/ .eslintcache .turbo/ +# Tsup bundled configs +*.bundled_*.mjs + # Lefthook lefthook-local.yml @@ -48,3 +51,4 @@ lefthook-local.yml # Test reports **/reports/ +../../temp/ diff --git a/.promptx/pouch.json b/.promptx/pouch.json new file mode 100644 index 0000000..22caeb8 --- /dev/null +++ b/.promptx/pouch.json @@ -0,0 +1,245 @@ +{ + "currentState": "initial", + "stateHistory": [ + { + "from": "initial", + "command": "action", + "timestamp": "2025-10-11T09:34:06.196Z", + "args": ["sean"] + }, + { + "from": "initial", + "command": "discover", + "timestamp": "2025-10-11T10:55:40.594Z", + "args": [] + }, + { + "from": "initial", + "command": "action", + "timestamp": "2025-10-11T10:55:44.612Z", + "args": ["sean"] + }, + { + "from": "initial", + "command": "action", + "timestamp": "2025-10-11T13:07:00.161Z", + "args": ["sean"] + }, + { + "from": "initial", + "command": "project", + "timestamp": "2025-10-11T13:07:20.826Z", + "args": [ + { + "workingDirectory": "/Users/sean/Deepractice/projects/NodeSpec", + "ideType": "claude" + } + ] + }, + { + "from": "initial", + "command": "recall", + "timestamp": "2025-10-11T13:13:29.153Z", + "args": [ + { + "role": "sean", + "query": "矛盾论 决策", + "mode": "focused" + } + ] + }, + { + "from": "initial", + "command": "remember", + "timestamp": "2025-10-11T13:21:59.642Z", + "args": [ + { + "role": "sean", + "engrams": [ + { + "content": "NodeSpec eslint-config 包实现了 defaultSeverity 支持,规则可以在 .rule.md 中声明默认严格度(error/warn/off),未声明则默认 error。no-interface-prefix 规则改为 warn,因为接口重命名是重构操作不适合 autoFix。测试步骤自动使用规则的 defaultSeverity。", + "schema": "NodeSpec - eslint-config - defaultSeverity - warn - autoFix", + "strength": 0.8, + "type": "ATOMIC" + }, + { + "content": "no-interface-prefix 规则的检测逻辑经过优化,避免误判正常 I 开头单词(Image、Icon)和缩写词(IO、IPC、IPv4)。判断条件:I + 大写 + 小写字母(ILogger),排除 I + 小写(Image)和版本号模式(IPv4)。正则 /^[A-Z][a-z]?[0-9]/ 排除缩写+版本号。", + "schema": "no-interface-prefix - 边界情况 - 正则表达式 - IPv4 - Image", + "strength": 0.7, + "type": "PATTERN" + }, + { + "content": "ESLint 规则测试失败的根因是缺少 @typescript-eslint/parser 配置。TypeScript AST 节点(TSInterfaceDeclaration)需要 TypeScript parser 才能解析,测试步骤中添加 languageOptions.parser: tsParser 解决。", + "schema": "ESLint - TypeScript - parser - TSInterfaceDeclaration - 测试", + "strength": 0.7, + "type": "LINK" + } + ] + } + ] + }, + { + "from": "initial", + "command": "recall", + "timestamp": "2025-10-11T13:22:21.698Z", + "args": [ + { + "role": "sean", + "query": "Java 规范 标准", + "mode": "balanced" + } + ] + }, + { + "from": "initial", + "command": "remember", + "timestamp": "2025-10-11T13:40:24.436Z", + "args": [ + { + "role": "sean", + "engrams": [ + { + "content": "ESLint 规则设计采用 Wrapper 模式:复用 @typescript-eslint 的检测逻辑,但用自己的 .rule.md 提供 AI-friendly 文档。优势是不重复造轮子,同时保持统一的错误信息格式和 defaultSeverity 控制。wrapRule 函数封装外部规则,加载自定义文档,统一错误信息体验。", + "schema": "ESLint - Wrapper模式 - AI-friendly - rule.md - 复用", + "strength": 0.9, + "type": "PATTERN" + }, + { + "content": "AI-first 代码规范的核心原则:单一标准无备选方案(deterministic rules)、利用 AI 预训练知识(TypeScript 生态最大公约数)、短提示词即可生效(ESLint 自动检查)。遵循 AI 肌肉记忆:PascalCase for types、camelCase for variables、kebab-case for files、无匈牙利前缀。目标是让 AI 一句话 'Follow @deepracticex/eslint-config' 就能理解所有规范。", + "schema": "AI-first - 代码规范 - 单一标准 - 预训练知识 - ESLint - 肌肉记忆", + "strength": 0.9, + "type": "ATOMIC" + }, + { + "content": "BDD 开发流程:先写 feature 测试场景,再实现规则(Feature First 原则)。Phase 1 核心规则包含 naming(no-type-prefix、class-pascal-case、variable-camel-case)、file-naming(kebab-case-files、file-suffix-convention)、structure(no-core-in-api、no-implementation-in-types)、imports(use-tilde-alias、no-relative-parent)。规则优先级基于影响:error 破坏架构、warn 风格不一致、off 极致规范。", + "schema": "BDD - Feature First - Phase 1 - 规则分类 - 优先级 - defaultSeverity", + "strength": 0.8, + "type": "PATTERN" + } + ] + } + ] + }, + { + "from": "initial", + "command": "action", + "timestamp": "2025-10-12T01:50:32.289Z", + "args": ["sean"] + }, + { + "from": "initial", + "command": "recall", + "timestamp": "2025-10-12T01:53:14.080Z", + "args": [ + { + "role": "sean", + "query": "矛盾论 主要矛盾 决策", + "mode": "focused" + } + ] + }, + { + "from": "initial", + "command": "remember", + "timestamp": "2025-10-12T01:57:38.537Z", + "args": [ + { + "role": "sean", + "engrams": [ + { + "content": "决定将 eslint-config 的 AI-friendly 特性拆分为独立项目 DeepLint。主要矛盾是通用性 vs AI友好性,通过产品拆分实现矛盾转化。DeepLint 专注于 AI 可理解的代码规范系统,每个规则包含可执行逻辑和 .rule.md 文档。NodeSpec 的 eslint-config 将使用 DeepLint 提供的规则,形成产品驱动生态的模式。", + "schema": "DeepLint - NodeSpec - AI友好 - 矛盾转化 - 产品拆分", + "strength": 0.9, + "type": "PATTERN" + }, + { + "content": "Issue #19 的根本问题:rules 被 preset 的 import 语句打包进 dist/index.js,导致 import.meta.url 指向错误位置,无法找到对应的 .rule.md 文件。虽然 tsup 配置了分离规则文件,但 src/api/presets/common.ts 中的 import ~/rules 触发了打包。", + "schema": "Issue19 - import.meta.url - 打包问题 - preset导入", + "strength": 0.8, + "type": "LINK" + } + ] + } + ] + }, + { + "from": "initial", + "command": "recall", + "timestamp": "2025-10-12T01:59:59.363Z", + "args": [ + { + "role": "sean", + "query": "品牌命名 产品定位 Deepractice", + "mode": "balanced" + } + ] + }, + { + "from": "initial", + "command": "remember", + "timestamp": "2025-10-12T02:03:25.146Z", + "args": [ + { + "role": "sean", + "engrams": [ + { + "content": "项目命名讨论:DeepLint vs PracticeLint vs Deeplinter。最终选择 Deeplinter。命名考虑因素:品牌关联度、独特性、社区接受度、产品定位(AI可学习的linting规则系统)。Deeplinter 结合了 Deep(AI/智能)+ linter(检查工具),er后缀给人工具感和专业感。", + "schema": "Deeplinter - 命名 - 品牌策略 - AI-linting", + "strength": 0.8, + "type": "ATOMIC" + } + ] + } + ] + }, + { + "from": "initial", + "command": "remember", + "timestamp": "2025-10-12T02:06:21.257Z", + "args": [ + { + "role": "sean", + "engrams": [ + { + "content": "使用 NodeSpec CLI 成功创建了 Deeplinter monorepo 项目。命令:node NodeSpec/apps/cli/dist/cli.js infra monorepo create Deeplinter --skip-git --skip-install。创建的结构包含:src/(core, domain), packages/, apps/, services/ 目录,完整的 pnpm workspace 配置,turbo.json, lefthook, commitlint 等标准配置。这验证了 NodeSpec CLI 的完整性和可用性。", + "schema": "NodeSpec-CLI - Deeplinter - monorepo创建 - 项目结构", + "strength": 0.9, + "type": "PATTERN" + }, + { + "content": "Deeplinter 定位为 AI-learnable linting framework,口号 'Linting rules that AI can learn'。核心特性:每个规则包含可执行 JS + AI 可读的 .rule.md 文档。解决 Issue #19 的根本方案是将规则系统独立为 Deeplinter 项目,NodeSpec 的 eslint-config 将作为 Deeplinter 的第一个用户。这是矛盾转化的实践:通用性 vs AI友好性 → 产品拆分。", + "schema": "Deeplinter - 产品定位 - AI-learnable - 矛盾转化 - Issue19", + "strength": 0.9, + "type": "LINK" + } + ] + } + ] + }, + { + "from": "initial", + "command": "remember", + "timestamp": "2025-10-12T02:10:11.392Z", + "args": [ + { + "role": "sean", + "engrams": [ + { + "content": "完成 Deeplinter 项目初始化。使用 NodeSpec CLI 创建 monorepo,手动创建 @deeplinter/framework 包(因为 CLI mirror 文件缺失)。Framework 包含 api/core/types 三层架构,实现了 createRule() 和 RuleRegistry 核心 API。已初始化 git 仓库并提交首个 commit。项目地址:/Users/sean/Deepractice/projects/Deeplinter", + "schema": "Deeplinter - 项目初始化 - framework包 - git仓库", + "strength": 0.9, + "type": "PATTERN" + } + ] + } + ] + }, + { + "from": "initial", + "command": "action", + "timestamp": "2025-10-12T02:15:41.860Z", + "args": ["sean"] + } + ], + "lastUpdated": "2025-10-12T02:15:41.870Z" +} diff --git a/apps/cli/cucumber.cjs b/apps/cli/cucumber.cjs deleted file mode 100644 index 52f243b..0000000 --- a/apps/cli/cucumber.cjs +++ /dev/null @@ -1,18 +0,0 @@ -/** - * Cucumber configuration for @deepracticex/nodespec-cli - */ - -const { createConfig } = require("@deepracticex/cucumber-config"); - -const config = createConfig({ - paths: ["features/**/*.feature"], - import: ["tests/e2e/**/*.steps.ts", "tests/e2e/support/**/*.ts"], - timeout: 60000, // 60 seconds for steps that run pnpm install -}); - -// Add tag expression to skip @skip scenarios -config.default.tags = "not @skip"; -if (config.dev) config.dev.tags = "not @skip"; -if (config.ci) config.ci.tags = "not @skip"; - -module.exports = config; diff --git a/apps/cli/features/infra/app/add.feature b/apps/cli/features/infra/app/add.feature index fcecd07..8cf5122 100644 --- a/apps/cli/features/infra/app/add.feature +++ b/apps/cli/features/infra/app/add.feature @@ -43,8 +43,9 @@ Feature: Add Application to Monorepo Given I am in the monorepo root When I run "nodespec infra app add my-cli" Then the command should succeed - And "apps/my-cli/package.json" should contain "\"build\": \"tsup\"" - And "apps/my-cli/package.json" should contain "\"dev\"" + And "apps/my-cli/package.json" should contain "\"build\":" + And "apps/my-cli/package.json" should contain "tsup" + And "apps/my-cli/package.json" should contain "\"dev\":" And "apps/my-cli/package.json" should contain "\"typecheck\": \"tsc --noEmit\"" Scenario: Generated application is buildable and executable diff --git a/apps/cli/package.json b/apps/cli/package.json index 8221d68..94f9878 100644 --- a/apps/cli/package.json +++ b/apps/cli/package.json @@ -16,16 +16,16 @@ } }, "scripts": { - "build": "NODE_OPTIONS='--import tsx' tsup", - "dev": "NODE_OPTIONS='--import tsx' tsup --watch", + "build": "tsup && tsx scripts/copy-mirror.ts", + "dev": "tsup --watch", "clean": "rimraf dist", "typecheck": "tsc --noEmit", - "test": "pnpm build && vitest run && cucumber-tsx", - "test:unit": "vitest run", - "test:e2e": "pnpm build && cucumber-tsx" + "test": "pnpm build && vitest run", + "test:dev": "vitest" }, "dependencies": { "@deepracticex/nodespec-core": "workspace:*", + "@deepracticex/nodespec-mirror": "workspace:*", "commander": "^12.1.0", "chalk": "^5.4.1", "ora": "^8.1.1", @@ -33,10 +33,8 @@ "execa": "^9.5.2" }, "devDependencies": { - "@deepracticex/tsup-config": "workspace:*", - "@deepracticex/typescript-config": "workspace:*", - "@deepracticex/vitest-config": "workspace:*", - "@deepracticex/cucumber-config": "workspace:*" + "@deepracticex/config-preset": "workspace:*", + "@deepracticex/testing-utils": "workspace:*" }, "files": [ "dist", diff --git a/apps/cli/scripts/copy-mirror.ts b/apps/cli/scripts/copy-mirror.ts new file mode 100644 index 0000000..1cb5fdf --- /dev/null +++ b/apps/cli/scripts/copy-mirror.ts @@ -0,0 +1,41 @@ +#!/usr/bin/env tsx + +/** + * Copy mirror from nodespec-mirror package to CLI dist + */ + +import path from "node:path"; +import fs from "fs-extra"; +import { fileURLToPath } from "node:url"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +async function copyMirror() { + console.log("🪞 Copying NodeSpec mirror to CLI dist..."); + + const mirrorSource = path.resolve( + __dirname, + "../../../src/mirror/dist/mirror", + ); + const mirrorDest = path.resolve(__dirname, "../dist/mirror"); + + // Check if mirror source exists + if (!(await fs.pathExists(mirrorSource))) { + console.error( + "❌ Mirror source not found. Please run 'pnpm --filter @deepracticex/nodespec-mirror build' first.", + ); + process.exit(1); + } + + // Copy mirror + await fs.copy(mirrorSource, mirrorDest); + + console.log(`✅ Mirror copied to dist/mirror/`); +} + +// Run +copyMirror().catch((error) => { + console.error("❌ Failed to copy mirror:", error); + process.exit(1); +}); diff --git a/apps/cli/src/commands/infra/monorepo/init.ts b/apps/cli/src/commands/infra/monorepo/init.ts index 6a700f5..56ddc79 100644 --- a/apps/cli/src/commands/infra/monorepo/init.ts +++ b/apps/cli/src/commands/infra/monorepo/init.ts @@ -3,7 +3,7 @@ import fs from "fs-extra"; import chalk from "chalk"; import ora from "ora"; import { execa } from "execa"; -import { ProjectGenerator } from "@deepracticex/nodespec-core"; +import { MonorepoGenerator } from "@deepracticex/nodespec-core"; interface InitOptions { name?: string; @@ -30,9 +30,9 @@ export async function initAction(options: InitOptions): Promise { console.log(chalk.blue(`Initializing NodeSpec monorepo: ${projectName}`)); console.log(chalk.gray(`Target directory: ${targetDir}\n`)); - // Generate project structure using ProjectGenerator + // Generate project structure using MonorepoGenerator spinner.start("Generating project structure"); - const generator = new ProjectGenerator(); + const generator = new MonorepoGenerator(); await generator.generate(targetDir, { name: projectName }); spinner.succeed("Project structure generated"); diff --git a/apps/cli/tests/e2e/steps/common.steps.ts b/apps/cli/tests/e2e/steps/common.steps.ts index 4e4cb54..45403d9 100644 --- a/apps/cli/tests/e2e/steps/common.steps.ts +++ b/apps/cli/tests/e2e/steps/common.steps.ts @@ -1,13 +1,13 @@ /** * Common step definitions for infra E2E tests */ -import { Given, When, Then } from "@cucumber/cucumber"; +import { Given, When, Then } from "@deepracticex/testing-utils"; import { expect } from "chai"; import { execa } from "execa"; import fs from "fs-extra"; import path from "node:path"; import os from "node:os"; -import type { InfraWorld } from "../support/world"; +import type { InfraWorld } from "../support/world.js"; // Background steps Given("I am in a temporary test directory", async function (this: InfraWorld) { @@ -112,10 +112,10 @@ When("I run {string}", async function (this: InfraWorld, command: string) { this.exitCode = this.lastResult.exitCode; if (this.lastResult.stdout) { - this.stdout.push(this.lastResult.stdout); + this.stdout.push(String(this.lastResult.stdout)); } if (this.lastResult.stderr) { - this.stderr.push(this.lastResult.stderr); + this.stderr.push(String(this.lastResult.stderr)); } // Debug logging @@ -158,10 +158,10 @@ When( this.exitCode = this.lastResult.exitCode; if (this.lastResult.stdout) { - this.stdout.push(this.lastResult.stdout); + this.stdout.push(String(this.lastResult.stdout)); } if (this.lastResult.stderr) { - this.stderr.push(this.lastResult.stderr); + this.stderr.push(String(this.lastResult.stderr)); } } catch (error) { this.lastError = error as Error; diff --git a/apps/cli/tests/e2e/steps/config.steps.ts b/apps/cli/tests/e2e/steps/config.steps.ts index 43699a2..300f753 100644 --- a/apps/cli/tests/e2e/steps/config.steps.ts +++ b/apps/cli/tests/e2e/steps/config.steps.ts @@ -1,11 +1,11 @@ /** * Step definitions for configuration initialization scenarios */ -import { Given, Then } from "@cucumber/cucumber"; +import { Given, Then, DataTable } from "@deepracticex/testing-utils"; import { expect } from "chai"; import fs from "fs-extra"; import path from "node:path"; -import type { InfraWorld } from "../support/world"; +import type { InfraWorld } from "../support/world.js"; // Given steps for config scenarios @@ -32,8 +32,8 @@ Given( Then( "the following config files should exist:", - async function (this: InfraWorld, dataTable: { rawTable: string[][] }) { - const rows = dataTable.rawTable.slice(1); // Skip header row + async function (this: InfraWorld, dataTable: DataTable) { + const rows = dataTable.raw().slice(1); // Skip header row for (const row of rows) { const [fileName, tool] = row; @@ -107,16 +107,12 @@ Then( Then( "{string} should contain:", - async function ( - this: InfraWorld, - fileName: string, - dataTable: { rawTable: string[][] }, - ) { + async function (this: InfraWorld, fileName: string, dataTable: DataTable) { const filePath = path.join(this.testDir!, fileName); const content = await fs.readFile(filePath, "utf-8"); const json = JSON.parse(content); - const rows = dataTable.rawTable.slice(1); // Skip header row + const rows = dataTable.raw().slice(1); // Skip header row for (const row of rows) { const [setting, value] = row; @@ -155,8 +151,8 @@ Then( Given( "the following config files exist:", - async function (this: InfraWorld, dataTable: { rawTable: string[][] }) { - const rows = dataTable.rawTable.slice(1); // Skip header row + async function (this: InfraWorld, dataTable: DataTable) { + const rows = dataTable.raw().slice(1); // Skip header row for (const row of rows) { const [fileName] = row; @@ -395,9 +391,9 @@ Given( Then( "I should see configuration files listed:", - function (this: InfraWorld, dataTable: { rawTable: string[][] }) { + function (this: InfraWorld, dataTable: DataTable) { const allOutput = [...this.stdout, ...this.stderr].join("\n"); - const rows = dataTable.rawTable.slice(1); // Skip header row + const rows = dataTable.raw().slice(1); // Skip header row for (const row of rows) { const [file, tool, status] = row; @@ -450,9 +446,9 @@ Then( Then( "I should see missing configs:", - function (this: InfraWorld, dataTable: { rawTable: string[][] }) { + function (this: InfraWorld, dataTable: DataTable) { const allOutput = [...this.stdout, ...this.stderr].join("\n"); - const rows = dataTable.rawTable.slice(1); // Skip header row + const rows = dataTable.raw().slice(1); // Skip header row for (const row of rows) { const [file, tool, status] = row; @@ -477,9 +473,9 @@ Then( Then( "I should see conflict details:", - function (this: InfraWorld, dataTable: { rawTable: string[][] }) { + function (this: InfraWorld, dataTable: DataTable) { const allOutput = [...this.stdout, ...this.stderr].join("\n"); - const rows = dataTable.rawTable.slice(1); // Skip header row + const rows = dataTable.raw().slice(1); // Skip header row for (const row of rows) { const [setting, eslintValue, prettierValue] = row; diff --git a/apps/cli/tests/e2e/steps/infra.steps.ts b/apps/cli/tests/e2e/steps/infra.steps.ts index a6e8e49..4b71f93 100644 --- a/apps/cli/tests/e2e/steps/infra.steps.ts +++ b/apps/cli/tests/e2e/steps/infra.steps.ts @@ -1,18 +1,21 @@ /** * Infrastructure-specific step definitions for init and create scenarios */ -import { Given, When, Then } from "@cucumber/cucumber"; +import { When, Then, DataTable } from "@deepracticex/testing-utils"; import { expect } from "chai"; import { execa } from "execa"; import fs from "fs-extra"; import path from "node:path"; -import type { InfraWorld } from "../support/world"; +import type { InfraWorld } from "../support/world.js"; // File existence assertions Then( "the following files should exist:", - async function (this: InfraWorld, dataTable: { rawTable: string[][] }) { - const files = dataTable.rawTable.slice(1).map((row) => row[0]!); + async function (this: InfraWorld, dataTable: DataTable) { + const files = dataTable + .raw() + .slice(1) + .map((row) => row[0]!); for (const file of files) { const filePath = path.join(this.testDir!, file); @@ -24,12 +27,11 @@ Then( Then( "the following files should exist in {string}:", - async function ( - this: InfraWorld, - directory: string, - dataTable: { rawTable: string[][] }, - ) { - const files = dataTable.rawTable.slice(1).map((row) => row[0]!); + async function (this: InfraWorld, directory: string, dataTable: DataTable) { + const files = dataTable + .raw() + .slice(1) + .map((row) => row[0]!); for (const file of files) { const filePath = path.join(this.testDir!, directory, file); @@ -41,8 +43,11 @@ Then( Then( "the following directories should exist:", - async function (this: InfraWorld, dataTable: { rawTable: string[][] }) { - const directories = dataTable.rawTable.slice(1).map((row) => row[0]!); + async function (this: InfraWorld, dataTable: DataTable) { + const directories = dataTable + .raw() + .slice(1) + .map((row) => row[0]!); for (const dir of directories) { const dirPath = path.join(this.testDir!, dir); diff --git a/apps/cli/tests/e2e/steps/list.steps.ts b/apps/cli/tests/e2e/steps/list.steps.ts index 8ad63eb..97856ab 100644 --- a/apps/cli/tests/e2e/steps/list.steps.ts +++ b/apps/cli/tests/e2e/steps/list.steps.ts @@ -1,11 +1,11 @@ /** * Step definitions for package and app list scenarios */ -import { Given, Then } from "@cucumber/cucumber"; +import { Given, Then, DataTable } from "@deepracticex/testing-utils"; import { expect } from "chai"; import fs from "fs-extra"; import path from "node:path"; -import type { InfraWorld } from "../support/world"; +import type { InfraWorld } from "../support/world.js"; // Given steps for list scenarios @@ -98,9 +98,9 @@ Given( Then( "I should see output containing:", - function (this: InfraWorld, dataTable: { rawTable: string[][] }) { + function (this: InfraWorld, dataTable: DataTable) { const allOutput = [...this.stdout, ...this.stderr].join("\n"); - const rows = dataTable.rawTable.slice(1); // Skip header row + const rows = dataTable.raw().slice(1); // Skip header row for (const row of rows) { const [item, version] = row; @@ -117,18 +117,14 @@ Then( Then( "I should see package {string} with details:", - function ( - this: InfraWorld, - packageName: string, - dataTable: { rawTable: string[][] }, - ) { + function (this: InfraWorld, packageName: string, dataTable: DataTable) { const allOutput = [...this.stdout, ...this.stderr].join("\n"); expect(allOutput).to.include( packageName, `Output should contain package ${packageName}`, ); - const rows = dataTable.rawTable.slice(1); // Skip header row + const rows = dataTable.raw().slice(1); // Skip header row for (const row of rows) { const [field, value] = row; expect(allOutput).to.include( @@ -141,18 +137,14 @@ Then( Then( "I should see app {string} with details:", - function ( - this: InfraWorld, - appName: string, - dataTable: { rawTable: string[][] }, - ) { + function (this: InfraWorld, appName: string, dataTable: DataTable) { const allOutput = [...this.stdout, ...this.stderr].join("\n"); expect(allOutput).to.include( appName, `Output should contain app ${appName}`, ); - const rows = dataTable.rawTable.slice(1); // Skip header row + const rows = dataTable.raw().slice(1); // Skip header row for (const row of rows) { const [field, value] = row; expect(allOutput).to.include( @@ -309,18 +301,14 @@ Given( Then( "I should see service {string} with details:", - function ( - this: InfraWorld, - serviceName: string, - dataTable: { rawTable: string[][] }, - ) { + function (this: InfraWorld, serviceName: string, dataTable: DataTable) { const allOutput = [...this.stdout, ...this.stderr].join("\n"); expect(allOutput).to.include( serviceName, `Output should contain service ${serviceName}`, ); - const rows = dataTable.rawTable.slice(1); // Skip header row + const rows = dataTable.raw().slice(1); // Skip header row for (const row of rows) { const [field, value] = row; expect(allOutput).to.include( diff --git a/apps/cli/tests/e2e/steps/removal.steps.ts b/apps/cli/tests/e2e/steps/removal.steps.ts index 1965bd2..aeaf162 100644 --- a/apps/cli/tests/e2e/steps/removal.steps.ts +++ b/apps/cli/tests/e2e/steps/removal.steps.ts @@ -1,11 +1,11 @@ /** * Step definitions for package and app removal scenarios */ -import { Given, When, Then } from "@cucumber/cucumber"; +import { Given, Then } from "@deepracticex/testing-utils"; import { expect } from "chai"; import fs from "fs-extra"; import path from "node:path"; -import type { InfraWorld } from "../support/world"; +import type { InfraWorld } from "../support/world.js"; // Given steps for removal scenarios @@ -145,7 +145,7 @@ Then( async function (this: InfraWorld, packageName: string) { // Extract directory name from scoped package names const dirName = packageName.startsWith("@") - ? packageName.split("/")[1] + ? (packageName.split("/")[1] ?? packageName) : packageName; // Check packages/ directory diff --git a/apps/cli/tests/e2e/steps/service.steps.ts b/apps/cli/tests/e2e/steps/service.steps.ts index 7c3f588..0fcf9d3 100644 --- a/apps/cli/tests/e2e/steps/service.steps.ts +++ b/apps/cli/tests/e2e/steps/service.steps.ts @@ -1,11 +1,11 @@ /** * Service-specific step definitions for backend service management */ -import { Given, Then } from "@cucumber/cucumber"; +import { Given, Then } from "@deepracticex/testing-utils"; import { expect } from "chai"; import fs from "fs-extra"; import path from "node:path"; -import type { InfraWorld } from "../support/world"; +import type { InfraWorld } from "../support/world.js"; // Given steps for service creation and setup diff --git a/apps/cli/tests/e2e/steps/validation.steps.ts b/apps/cli/tests/e2e/steps/validation.steps.ts index 862a3c2..b07771a 100644 --- a/apps/cli/tests/e2e/steps/validation.steps.ts +++ b/apps/cli/tests/e2e/steps/validation.steps.ts @@ -1,11 +1,11 @@ /** * Step definitions for validation scenarios (package, app, and monorepo) */ -import { Given, Then } from "@cucumber/cucumber"; +import { Given, Then, DataTable } from "@deepracticex/testing-utils"; import { expect } from "chai"; import fs from "fs-extra"; import path from "node:path"; -import type { InfraWorld } from "../support/world"; +import type { InfraWorld } from "../support/world.js"; // Given steps for validation scenarios @@ -107,7 +107,7 @@ Given( async function ( this: InfraWorld, tsconfigPath: string, - extendsValue: string, + _extendsValue: string, ) { const fullPath = path.join(this.testDir!, tsconfigPath); const tsconfig = await fs.readJson(fullPath); @@ -349,9 +349,9 @@ Given("the monorepo has validation errors", async function (this: InfraWorld) { Then( "I should see all error messages:", - function (this: InfraWorld, dataTable: { rawTable: string[][] }) { + function (this: InfraWorld, dataTable: DataTable) { const allOutput = [...this.stdout, ...this.stderr].join("\n"); - const rows = dataTable.rawTable.slice(1); // Skip header row + const rows = dataTable.raw().slice(1); // Skip header row for (const row of rows) { const errorMessage = row[0]!; @@ -365,9 +365,9 @@ Then( Then( "I should see validation summary:", - function (this: InfraWorld, dataTable: { rawTable: string[][] }) { + function (this: InfraWorld, dataTable: DataTable) { const allOutput = [...this.stdout, ...this.stderr].join("\n"); - const rows = dataTable.rawTable.slice(1); // Skip header row + const rows = dataTable.raw().slice(1); // Skip header row for (const row of rows) { const [status, count] = row; @@ -398,9 +398,9 @@ Then( Then( "I should see validation summary showing:", - function (this: InfraWorld, dataTable: { rawTable: string[][] }) { + function (this: InfraWorld, dataTable: DataTable) { const allOutput = [...this.stdout, ...this.stderr].join("\n"); - const rows = dataTable.rawTable.slice(1); // Skip header row + const rows = dataTable.raw().slice(1); // Skip header row for (const row of rows) { const [component, status, errors] = row; diff --git a/apps/cli/tests/e2e/steps/workspace.steps.ts b/apps/cli/tests/e2e/steps/workspace.steps.ts index f3067d4..72b1904 100644 --- a/apps/cli/tests/e2e/steps/workspace.steps.ts +++ b/apps/cli/tests/e2e/steps/workspace.steps.ts @@ -1,12 +1,12 @@ /** * Workspace-specific step definitions for package and app management */ -import { Given, Then } from "@cucumber/cucumber"; +import { Given, Then, DataTable } from "@deepracticex/testing-utils"; import { expect } from "chai"; import fs from "fs-extra"; import path from "node:path"; import os from "node:os"; -import type { InfraWorld } from "../support/world"; +import type { InfraWorld } from "../support/world.js"; // Given steps for workspace context @@ -372,9 +372,9 @@ Then( Then( "I should see workspace summary:", - function (this: InfraWorld, dataTable: { rawTable: string[][] }) { + function (this: InfraWorld, dataTable: DataTable) { const allOutput = [...this.stdout, ...this.stderr].join("\n"); - const rows = dataTable.rawTable.slice(1); // Skip header row + const rows = dataTable.raw().slice(1); // Skip header row for (const row of rows) { const [type, count] = row; @@ -389,9 +389,9 @@ Then( Then( "I should see workspace directories:", - function (this: InfraWorld, dataTable: { rawTable: string[][] }) { + function (this: InfraWorld, dataTable: DataTable) { const allOutput = [...this.stdout, ...this.stderr].join("\n"); - const rows = dataTable.rawTable.slice(1); // Skip header row + const rows = dataTable.raw().slice(1); // Skip header row for (const row of rows) { const [directory, count] = row; @@ -409,9 +409,9 @@ Then( Then( "I should see configuration summary:", - function (this: InfraWorld, dataTable: { rawTable: string[][] }) { + function (this: InfraWorld, dataTable: DataTable) { const allOutput = [...this.stdout, ...this.stderr].join("\n"); - const rows = dataTable.rawTable.slice(1); // Skip header row + const rows = dataTable.raw().slice(1); // Skip header row for (const row of rows) { const [tool, status] = row; @@ -428,9 +428,9 @@ Then( Then( "I should see detailed configuration:", - function (this: InfraWorld, dataTable: { rawTable: string[][] }) { + function (this: InfraWorld, dataTable: DataTable) { const allOutput = [...this.stdout, ...this.stderr].join("\n"); - const rows = dataTable.rawTable.slice(1); // Skip header row + const rows = dataTable.raw().slice(1); // Skip header row for (const row of rows) { const [tool, version, configFile] = row; diff --git a/apps/cli/tests/e2e/support/hooks.ts b/apps/cli/tests/e2e/support/hooks.ts index 29c111e..a238a1f 100644 --- a/apps/cli/tests/e2e/support/hooks.ts +++ b/apps/cli/tests/e2e/support/hooks.ts @@ -1,9 +1,17 @@ /** - * Cucumber hooks for test setup and cleanup + * Vitest-Cucumber hooks for test setup and cleanup */ -import { Before, After, BeforeAll, AfterAll } from "@cucumber/cucumber"; -import fs from "fs-extra"; -import type { InfraWorld } from "./world"; +import { + Before, + After, + BeforeAll, + AfterAll, + setWorldConstructor, +} from "@deepracticex/testing-utils"; +import { createWorld } from "./world.js"; + +// Register World factory +setWorldConstructor(createWorld); // Global setup BeforeAll(async function () { @@ -16,38 +24,13 @@ AfterAll(async function () { }); // Before each scenario -Before(async function (this: InfraWorld) { - // Reset state - this.stdout = []; - this.stderr = []; - this.lastCommand = undefined; - this.lastResult = undefined; - this.lastError = undefined; - this.exitCode = undefined; +Before(async function () { + // Context is now shared between Background and Scenario (plugin 1.1.0+) + // No need to initialize here }); // After each scenario -After(async function (this: InfraWorld) { - // Restore original working directory - if (this.originalCwd) { - process.chdir(this.originalCwd); - } - - // Clean up test directory - if (this.testDir) { - try { - await fs.remove(this.testDir); - } catch (error) { - console.error( - `Failed to clean up test directory: ${this.testDir}`, - error, - ); - } - } - - // Restore any captured console methods - const restoreConsole = this.get("restoreConsole") as (() => void) | undefined; - if (restoreConsole) { - restoreConsole(); - } +After(async function () { + // Cleanup happens automatically through World factory + // Additional cleanup if needed can be added here }); diff --git a/apps/cli/tests/e2e/support/world.ts b/apps/cli/tests/e2e/support/world.ts index d98d103..a7b6689 100644 --- a/apps/cli/tests/e2e/support/world.ts +++ b/apps/cli/tests/e2e/support/world.ts @@ -1,10 +1,9 @@ /** - * Cucumber World - shared context for infra E2E tests + * World context for CLI E2E tests */ -import { setWorldConstructor, World, IWorldOptions } from "@cucumber/cucumber"; import type { Result } from "execa"; -export interface InfraWorld extends World { +export interface InfraWorld { // Working directory context testDir?: string; originalCwd?: string; @@ -22,34 +21,42 @@ export interface InfraWorld extends World { // Test state expectedMissingPackages?: string[]; + // Context storage + context: Map; + // Helper methods set(key: string, value: unknown): void; get(key: string): unknown; + clear(): void; } -class CustomWorld extends World implements InfraWorld { - testDir?: string; - originalCwd?: string; - lastCommand?: string; - lastResult?: Result; - lastError?: Error; - exitCode?: number; - stdout: string[] = []; - stderr: string[] = []; - - private context: Map = new Map(); - - constructor(options: IWorldOptions) { - super(options); - } - - set(key: string, value: unknown): void { - this.context.set(key, value); - } - - get(key: string): unknown { - return this.context.get(key); - } +export function createWorld(): InfraWorld { + const context = new Map(); + + return { + stdout: [], + stderr: [], + context, + + set(key: string, value: unknown) { + this.context.set(key, value); + }, + + get(key: string) { + return this.context.get(key); + }, + + clear() { + this.context.clear(); + this.testDir = undefined; + this.originalCwd = undefined; + this.lastCommand = undefined; + this.lastResult = undefined; + this.lastError = undefined; + this.exitCode = undefined; + this.stdout = []; + this.stderr = []; + this.expectedMissingPackages = undefined; + }, + }; } - -setWorldConstructor(CustomWorld); diff --git a/apps/cli/tsconfig.json b/apps/cli/tsconfig.json index 3b7676b..8701313 100644 --- a/apps/cli/tsconfig.json +++ b/apps/cli/tsconfig.json @@ -1,9 +1,11 @@ { - "extends": "@deepracticex/typescript-config/base.json", + "extends": "@deepracticex/config-preset/typescript-base.json", "compilerOptions": { "outDir": "./dist", - "rootDir": "./src" + "paths": { + "~/*": ["./src/*"] + } }, - "include": ["src/**/*"], + "include": ["src/**/*", "tests/**/*"], "exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.spec.ts"] } diff --git a/apps/cli/tsup.config.ts b/apps/cli/tsup.config.ts index dbea97a..c5e4933 100644 --- a/apps/cli/tsup.config.ts +++ b/apps/cli/tsup.config.ts @@ -1,6 +1,6 @@ -import { createConfig } from "@deepracticex/tsup-config"; +import { tsup } from "@deepracticex/config-preset/tsup"; -export default createConfig({ +export default tsup.createConfig({ entry: ["src/cli.ts", "src/index.ts"], format: ["esm"], dts: true, diff --git a/apps/cli/vitest.config.ts b/apps/cli/vitest.config.ts index c82b582..3e00c24 100644 --- a/apps/cli/vitest.config.ts +++ b/apps/cli/vitest.config.ts @@ -1,3 +1,17 @@ -import { baseConfig } from "@deepracticex/vitest-config/base"; +import path from "node:path"; +import { defineConfig, mergeConfig } from "vitest/config"; +import { vitest } from "@deepracticex/config-preset/vitest"; -export default baseConfig; +export default mergeConfig( + vitest.withCucumber({ + steps: "tests/e2e/steps", + verbose: true, + }), + defineConfig({ + resolve: { + alias: { + "~": path.resolve(__dirname, "./src"), + }, + }, + }), +); diff --git a/apps/example-cli/README.md b/apps/example-cli/README.md new file mode 100644 index 0000000..41400bd --- /dev/null +++ b/apps/example-cli/README.md @@ -0,0 +1,111 @@ +# @deepracticex/example-cli + +**Deepractice CLI Development Standards Example** + +This application serves as the standard example for all Deepractice CLI applications. It demonstrates best practices for CLI app development with TypeScript. + +## Features + +- Commander.js for CLI command parsing +- Chalk for colorful terminal output +- TypeScript for type safety +- Shared tsup build configuration +- Shared TypeScript configuration + +## Quick Start + +### Installation + +```bash +# Install dependencies (from monorepo root) +pnpm install + +# Build the app +pnpm build +``` + +### Usage + +```bash +# Run the CLI +node dist/cli.js + +# Or via npm link (for development) +pnpm link --global +example-cli greet "Alice" +example-cli hello +``` + +### Development + +```bash +# Watch mode with hot reload +pnpm dev + +# Type checking +pnpm typecheck + +# Clean build artifacts +pnpm clean +``` + +## Directory Structure + +``` +apps/example-cli/ +├── src/ +│ ├── index.ts # Main entry point +│ └── cli.ts # CLI entry point +├── dist/ # Build output +├── tsconfig.json # TypeScript config +├── tsup.config.ts # Build config +└── package.json # Package manifest +``` + +## Commands + +### greet + +Greet someone by name. + +```bash +example-cli greet [name] +``` + +### hello + +Display a hello message. + +```bash +example-cli hello +``` + +## Development Guidelines + +### Adding Commands + +1. Define command in `src/cli.ts` using Commander.js +2. Implement logic in `src/index.ts` or separate modules +3. Build and test + +### Using Shared Configurations + +This app uses shared configurations from the monorepo: + +- **TypeScript**: `@deepracticex/typescript-config` +- **Tsup**: `@deepracticex/tsup-config` + +These are managed as workspace dependencies and provide consistent build settings across all apps. + +## Best Practices + +- Use Commander.js for command-line parsing +- Use Chalk for colorful output +- Separate CLI logic from business logic +- Export reusable functions from index.ts +- Use TypeScript for type safety +- Follow semantic versioning + +--- + +_Last updated: 2025-10-10_ diff --git a/apps/example-cli/package.json b/apps/example-cli/package.json new file mode 100644 index 0000000..6b55607 --- /dev/null +++ b/apps/example-cli/package.json @@ -0,0 +1,45 @@ +{ + "name": "@deepracticex/example-cli", + "version": "0.0.1", + "description": "Example CLI application demonstrating Deepractice CLI development standards", + "type": "module", + "bin": { + "example-cli": "./dist/cli.js" + }, + "main": "./dist/index.js", + "module": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + } + }, + "scripts": { + "build": "tsup", + "dev": "tsup --watch", + "clean": "rimraf dist", + "typecheck": "tsc --noEmit", + "start": "node dist/cli.js" + }, + "dependencies": { + "commander": "^12.1.0", + "chalk": "^5.4.1" + }, + "devDependencies": { + "@deepracticex/config-preset": "workspace:*" + }, + "files": [ + "dist", + "package.json", + "README.md" + ], + "keywords": [ + "cli", + "app", + "example", + "deepractice" + ], + "author": "Deepractice", + "license": "MIT" +} diff --git a/apps/example-cli/src/cli.ts b/apps/example-cli/src/cli.ts new file mode 100644 index 0000000..bab535f --- /dev/null +++ b/apps/example-cli/src/cli.ts @@ -0,0 +1,33 @@ +#!/usr/bin/env node + +/** + * example-cli + */ + +import { Command } from "commander"; +import chalk from "chalk"; +import { greet } from "./index.js"; + +const program = new Command(); + +program + .name("example-cli") + .description("Example CLI application") + .version("0.0.1"); + +program + .command("greet") + .description("Greet someone") + .argument("[name]", "Name to greet", "World") + .action((name: string) => { + console.log(chalk.green(greet(name))); + }); + +program + .command("hello") + .description("Say hello") + .action(() => { + console.log(chalk.blue("Hello from example-cli!")); + }); + +program.parse(); diff --git a/apps/example-cli/src/index.ts b/apps/example-cli/src/index.ts new file mode 100644 index 0000000..ef77c7c --- /dev/null +++ b/apps/example-cli/src/index.ts @@ -0,0 +1,13 @@ +/** + * example-cli + * + * Example CLI application demonstrating Deepractice CLI development standards + */ + +export function greet(name: string): string { + return `Hello, ${name}!`; +} + +export function main(): void { + console.log(greet("World")); +} diff --git a/apps/example-cli/tsconfig.json b/apps/example-cli/tsconfig.json new file mode 100644 index 0000000..8701313 --- /dev/null +++ b/apps/example-cli/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "@deepracticex/config-preset/typescript-base.json", + "compilerOptions": { + "outDir": "./dist", + "paths": { + "~/*": ["./src/*"] + } + }, + "include": ["src/**/*", "tests/**/*"], + "exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.spec.ts"] +} diff --git a/apps/example-cli/tsup.config.ts b/apps/example-cli/tsup.config.ts new file mode 100644 index 0000000..2e6ef18 --- /dev/null +++ b/apps/example-cli/tsup.config.ts @@ -0,0 +1,9 @@ +import { tsup } from "@deepracticex/config-preset/tsup"; + +export default tsup.createConfig({ + entry: ["src/index.ts", "src/cli.ts"], + format: ["esm"], + dts: true, + clean: true, + shims: true, +}); diff --git a/commitlint.config.js b/commitlint.config.js index 66b5356..3ec4308 100644 --- a/commitlint.config.js +++ b/commitlint.config.js @@ -1,3 +1,3 @@ -export default { - extends: ["@deepracticex/commitlint-config"], -}; +import { commitlint } from "@deepracticex/config-preset"; + +export default commitlint.base; diff --git a/configs/commitlint/README.md b/configs/commitlint/README.md deleted file mode 100644 index f50337e..0000000 --- a/configs/commitlint/README.md +++ /dev/null @@ -1,146 +0,0 @@ -# @deepracticex/commitlint-config - -Shared commitlint configuration for Deepractice projects following [Conventional Commits](https://www.conventionalcommits.org/). - -## Installation - -```bash -pnpm add -D @deepracticex/commitlint-config @commitlint/cli -``` - -## Usage - -Create `commitlint.config.js` in your project root: - -```javascript -export default { - extends: ["@deepracticex/commitlint-config"], -}; -``` - -## Commit Message Format - -``` -(): - - - -