From 3b758cac8382b82dbe6d21004969b26a14f8c6c7 Mon Sep 17 00:00:00 2001 From: Minsu Lee Date: Fri, 23 Oct 2026 19:25:54 +0900 Subject: [PATCH 1/3] feat(cli): detect project languages and select rule guides with --lang Detect languages from root markers: package.json/tsconfig.json, pubspec.yaml, Cargo.toml, build.gradle(.kts)/pom.xml, go.mod, and pyproject.toml. --lang overrides detection; an unknown value exits 1 and lists the accepted languages. init pre-selects the detected languages. update keeps the languages the existing AGENTS.md block already links. The block links the general guide plus each selected guide, and shows the TS bullets only for typescript. doctor reports the detected languages. Refs pleaseai/code-style#65 --- apps/docs/src/content/docs/en/cli.mdx | 7 ++- apps/docs/src/content/docs/ko/cli.mdx | 7 ++- packages/cli/README.ko.md | 21 +++++-- packages/cli/README.md | 22 +++++-- packages/cli/rules.md | 6 +- packages/cli/src/appliers.ts | 34 ++++++---- packages/cli/src/commands.ts | 31 +++++++++- packages/cli/src/fs-utils.ts | 9 +++ packages/cli/src/i18n.ts | 34 +++++----- packages/cli/src/index.ts | 16 +++-- packages/cli/src/languages.ts | 66 ++++++++++++++++++++ packages/cli/src/tools.ts | 3 + packages/cli/test/appliers.test.ts | 34 ++++++++++ packages/cli/test/i18n.test.ts | 8 +-- packages/cli/test/index.test.ts | 33 ++++++++-- packages/cli/test/languages.test.ts | 89 +++++++++++++++++++++++++++ 16 files changed, 366 insertions(+), 54 deletions(-) create mode 100644 packages/cli/src/languages.ts create mode 100644 packages/cli/test/languages.test.ts diff --git a/apps/docs/src/content/docs/en/cli.mdx b/apps/docs/src/content/docs/en/cli.mdx index ed20c08..74bf18d 100644 --- a/apps/docs/src/content/docs/en/cli.mdx +++ b/apps/docs/src/content/docs/en/cli.mdx @@ -131,10 +131,15 @@ The full canonical rules are shipped per language for AI assistants that prefer | Flag | Description | | --- | --- | | `--yes`, `-y` | Accept defaults and overwrite existing files without prompting | +| `--lang ` | Languages for the AGENTS.md rules block, comma-separated (defaults to detected; see below) | | `--locale ` | Force the CLI locale (defaults to `$LC_ALL` / `$LANG`) | | `--help`, `-h` | Print help | | `--version`, `-v` | Print the CLI version | +## Languages + +`--lang ` selects the programming languages the AGENTS.md rules block links to, for example `--lang typescript,dart`. Accepted values: `typescript`, `dart`, `rust`, `java`, `go`, `python`, `ruby`, `csharp`, `html-css`. Without it, `init` detects languages from marker files in the project root: `package.json` or `tsconfig.json` (typescript), `pubspec.yaml` (dart), `Cargo.toml` (rust), `build.gradle`, `build.gradle.kts` or `pom.xml` (java), `go.mod` (go), `pyproject.toml` (python). Interactive `init` lets you adjust the detected set. `update` keeps the languages the existing block links unless `--lang` is given. An unknown `--lang` value exits with an error (exit code 1) listing the accepted languages. `doctor` reports the detected languages. + ## Localisation The CLI auto-detects your locale from `LC_ALL`, `LANG`, or `LC_MESSAGES` and currently ships Korean and English messages. Override with `--locale ko` or `--locale en`: @@ -143,7 +148,7 @@ The CLI auto-detects your locale from `LC_ALL`, `LANG`, or `LC_MESSAGES` and cur pleaseai-code-style --locale ko init ``` -`--lang ko` and `--lang en` still set the locale for now, with a deprecation warning. When both flags are given, `--locale` wins. Any other `--lang` value, or a `--locale` value other than `ko` or `en`, exits with an error (exit code 1). A future release uses `--lang` to select programming languages. +`--lang ko` and `--lang en` are the deprecated locale alias: they still set the locale for now, with a deprecation warning, and select no languages. When both `--lang ko|en` and `--locale` are given, `--locale` wins. A `--locale` value other than `ko` or `en` also exits with an error (exit code 1). ## CI Usage diff --git a/apps/docs/src/content/docs/ko/cli.mdx b/apps/docs/src/content/docs/ko/cli.mdx index 224e67c..85bd5f8 100644 --- a/apps/docs/src/content/docs/ko/cli.mdx +++ b/apps/docs/src/content/docs/ko/cli.mdx @@ -131,10 +131,15 @@ CLI는 두 마커 주석 사이의 내용 **만** 관리하며 — `AGENTS.md` | 플래그 | 설명 | | --- | --- | | `--yes`, `-y` | 기본값을 수락하고 기존 파일을 확인 없이 덮어씀 | +| `--lang ` | AGENTS.md 룰 블록에 넣을 언어, 쉼표로 구분 (기본값은 자동 감지, 아래 참고) | | `--locale ` | CLI 언어 강제 지정 (기본값은 `$LC_ALL` / `$LANG`) | | `--help`, `-h` | 도움말 출력 | | `--version`, `-v` | CLI 버전 출력 | +## 언어 + +`--lang `는 AGENTS.md 룰 블록이 가리킬 프로그래밍 언어를 고릅니다. 예: `--lang typescript,dart`. 지원 값은 `typescript`, `dart`, `rust`, `java`, `go`, `python`, `ruby`, `csharp`, `html-css`입니다. 옵션이 없으면 `init`이 프로젝트 루트의 마커 파일로 언어를 감지합니다. `package.json` 또는 `tsconfig.json`(typescript), `pubspec.yaml`(dart), `Cargo.toml`(rust), `build.gradle`, `build.gradle.kts`, `pom.xml`(java), `go.mod`(go), `pyproject.toml`(python)이 해당합니다. 대화형 `init`은 감지된 목록을 조정할 수 있게 합니다. `update`는 `--lang`을 주지 않으면 기존 블록이 가리키는 언어를 유지합니다. 알 수 없는 `--lang` 값은 지원하는 언어 목록과 함께 오류로 종료합니다(종료 코드 1). `doctor`는 감지된 언어를 보여줍니다. + ## 현지화 CLI는 `LC_ALL`, `LANG`, `LC_MESSAGES`에서 locale을 자동 감지하며, 현재 한국어와 영어 메시지를 제공합니다. `--locale ko` 또는 `--locale en`으로 강제 지정할 수 있습니다: @@ -143,7 +148,7 @@ CLI는 `LC_ALL`, `LANG`, `LC_MESSAGES`에서 locale을 자동 감지하며, 현 pleaseai-code-style --locale ko init ``` -`--lang ko`와 `--lang en`도 당분간은 로케일을 지정하지만 사용 중단 경고를 출력합니다. 두 플래그를 함께 주면 `--locale`이 우선합니다. 그 밖의 `--lang` 값이나 지원하지 않는 `--locale` 값은 무시되지 않고 오류로 종료합니다(종료 코드 1). 이후 릴리스에서는 `--lang`이 프로그래밍 언어 선택에 쓰입니다. +`--lang ko`와 `--lang en`은 사용 중단된 로케일 별칭입니다. 당분간은 로케일을 지정하며 사용 중단 경고를 출력하고, 언어는 선택하지 않습니다. `--lang ko|en`과 `--locale`을 함께 주면 `--locale`이 우선합니다. 지원하지 않는 `--locale` 값도 오류로 종료합니다(종료 코드 1). ## CI에서의 사용 diff --git a/packages/cli/README.ko.md b/packages/cli/README.ko.md index edcf83b..c865ce2 100644 --- a/packages/cli/README.ko.md +++ b/packages/cli/README.ko.md @@ -220,19 +220,32 @@ CLI는 아래 마커 사이의 내용만 관리합니다 — `AGENTS.md`의 나 | 플래그 | 설명 | | --- | --- | | `--yes`, `-y` | 기본값을 수락하고 기존 파일을 확인 없이 덮어씀 | +| `--lang ` | AGENTS.md 룰 블록에 넣을 언어, 쉼표로 구분 (기본값은 자동 감지, 아래 참고) | | `--locale ` | CLI 언어 강제 지정 (기본값은 `$LANG`) | | `--help`, `-h` | 도움말 출력 | | `--version`, `-v` | 버전 출력 | +## 언어 + +`--lang `는 AGENTS.md 룰 블록이 가리킬 프로그래밍 언어를 고릅니다. 예: +`--lang typescript,dart`. 지원 값은 `typescript`, `dart`, `rust`, `java`, `go`, +`python`, `ruby`, `csharp`, `html-css`입니다. 옵션이 없으면 `init`이 프로젝트 루트의 +마커 파일로 언어를 감지합니다. `package.json` 또는 `tsconfig.json`(typescript), +`pubspec.yaml`(dart), `Cargo.toml`(rust), `build.gradle`, `build.gradle.kts`, +`pom.xml`(java), `go.mod`(go), `pyproject.toml`(python)이 해당합니다. 대화형 `init`은 +감지된 목록을 조정할 수 있게 합니다. `update`는 `--lang`을 주지 않으면 기존 블록이 +가리키는 언어를 유지합니다. 알 수 없는 `--lang` 값은 지원하는 언어 목록과 함께 오류로 +종료합니다(종료 코드 1). `doctor`는 감지된 언어를 보여줍니다. + ## 현지화 CLI는 `LC_ALL` / `LANG` / `LC_MESSAGES`에서 locale을 자동 감지하며, 현재 한국어와 영어 메시지를 제공합니다. `--locale ko` 또는 `--locale en`으로 강제 지정할 수 있습니다. -`--lang ko`와 `--lang en`도 당분간은 로케일을 지정하지만 사용 중단 경고를 출력합니다. -두 플래그를 함께 주면 `--locale`이 우선합니다. 그 밖의 `--lang` 값이나 지원하지 않는 -`--locale` 값은 무시되지 않고 오류로 종료합니다(종료 코드 1). 이후 릴리스에서는 -`--lang`이 프로그래밍 언어 선택에 쓰입니다. +`--lang ko`와 `--lang en`은 사용 중단된 로케일 별칭입니다. 당분간은 로케일을 지정하며 +사용 중단 경고를 출력하고, 언어는 선택하지 않습니다. `--lang ko|en`과 `--locale`을 +함께 주면 `--locale`이 우선합니다. 지원하지 않는 `--locale` 값도 오류로 +종료합니다(종료 코드 1). ## 라이선스 diff --git a/packages/cli/README.md b/packages/cli/README.md index 6c8862b..9b64917 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -404,20 +404,34 @@ JavaScript). | Flag | Description | | --- | --- | | `--yes`, `-y` | Accept defaults, overwrite existing files without prompting | +| `--lang ` | Languages for the AGENTS.md rules block, comma-separated (defaults to detected; see below) | | `--locale ` | Force the CLI locale (defaults to `$LANG`) | | `--help`, `-h` | Print help | | `--version`, `-v` | Print version | +## Languages + +`--lang ` selects the programming languages the AGENTS.md rules block links +to, for example `--lang typescript,dart`. Accepted values: `typescript`, `dart`, +`rust`, `java`, `go`, `python`, `ruby`, `csharp`, `html-css`. Without it, `init` +detects languages from marker files in the project root: `package.json` or +`tsconfig.json` (typescript), `pubspec.yaml` (dart), `Cargo.toml` (rust), +`build.gradle`, `build.gradle.kts` or `pom.xml` (java), `go.mod` (go), +`pyproject.toml` (python). Interactive `init` lets you adjust the detected set. +`update` keeps the languages the existing block links unless `--lang` is given. +An unknown `--lang` value exits with an error (exit code 1) listing the accepted +languages. `doctor` reports the detected languages. + ## Localisation The CLI auto-detects your locale from `LC_ALL` / `LANG` / `LC_MESSAGES` and currently ships Korean and English messages. Override with `--locale ko` or `--locale en`. -`--lang ko` and `--lang en` still set the locale for now, with a deprecation -warning. When both flags are given, `--locale` wins. Any other `--lang` value, or -a `--locale` value other than `ko` or `en`, exits with an error (exit code 1). A -future release uses `--lang` to select programming languages. +`--lang ko` and `--lang en` are the deprecated locale alias: they still set the +locale for now, with a deprecation warning, and select no languages. When both +`--lang ko|en` and `--locale` are given, `--locale` wins. A `--locale` value other +than `ko` or `en` also exits with an error (exit code 1). ## License diff --git a/packages/cli/rules.md b/packages/cli/rules.md index 56b75d4..535b141 100644 --- a/packages/cli/rules.md +++ b/packages/cli/rules.md @@ -14,5 +14,7 @@ apply to every language, then the guide for the language you are writing: - [`rules/csharp.md`](./rules/csharp.md) — C# - [`rules/html-css.md`](./rules/html-css.md) — HTML and CSS -`pleaseai-code-style update` points your `AGENTS.md` block at the general and -TypeScript guides; the other guides are read from this index for now. +`pleaseai-code-style init` and `update` point your `AGENTS.md` block at the +general guide plus the guide of each selected language. Languages are detected +from marker files in the project root, or set with `--lang `; `update` +keeps the languages the existing block links unless `--lang` is given. diff --git a/packages/cli/src/appliers.ts b/packages/cli/src/appliers.ts index 5e43c3e..e6d86c0 100644 --- a/packages/cli/src/appliers.ts +++ b/packages/cli/src/appliers.ts @@ -1,3 +1,4 @@ +import type { Language } from './languages.js' import type { Tool, ToolApplyResult, ToolContext } from './tools.js' import { copyFileSync, existsSync, writeFileSync } from 'node:fs' import { dirname, resolve as joinPath } from 'node:path' @@ -10,6 +11,7 @@ import { writePackageJson, } from './fs-utils.js' import { t } from './i18n.js' +import { detectLanguages, languagesFromAgentsBlock, RULES_PREFIX } from './languages.js' async function confirmOverwrite(file: string, auto: boolean): Promise { if (auto) { @@ -136,25 +138,25 @@ async function applyEditorConfig(ctx: ToolContext): Promise { // --- AGENTS.md -------------------------------------------------------------- -const AGENTS_BODY = `# Passion Factory Code Style +const AGENTS_INTRO = `# Passion Factory Code Style These rules are managed by \`@pleaseai/code-style\`. Run \`pleaseai-code-style update\` to refresh this block. Do not edit between the marker comments — your changes will be overwritten. -- Formatter: \`@pleaseai/eslint-config\` (wraps \`@antfu/eslint-config\`) +` + +const AGENTS_TYPESCRIPT = `- Formatter: \`@pleaseai/eslint-config\` (wraps \`@antfu/eslint-config\`) - No semicolons, single quotes, 2-space indent, trailing commas, LF line endings - ESM only — never emit \`require\`/\`module.exports\` - TypeScript: \`strict: true\`, prefer \`type\` over \`interface\`, no implicit \`any\` - Prefer named exports, early returns, \`async\`/\`await\`, optional chaining -- File size target: ≤ 500 lines +` + +const AGENTS_GENERAL = `- File size target: ≤ 500 lines - Tests live under the package's \`test/\` (or \`tests/\`) root, mirroring the source path (\`src/foo/bar.ts\` → \`test/foo/bar.test.ts\`); no \`import.meta.vitest\` in-source tests -- Conventional Commits for all commit messages - -For the full rules (what an AI coding assistant needs to know before writing code), -read \`node_modules/@pleaseai/code-style/rules/general.md\` and -\`node_modules/@pleaseai/code-style/rules/typescript.md\`.` +` const AGENTS_CODE_ORGANIZATION = `- Code organization (named exports, file names, error files, test paths, shared helpers): \`node_modules/@pleaseai/ast-grep-config/README.md\`; check with \`ast-grep scan\` and \`please-style check\` @@ -179,10 +181,15 @@ function wantsCodeOrganization(ctx: ToolContext): boolean { } } -function agentsBody(ctx: ToolContext): string { - return wantsCodeOrganization(ctx) - ? AGENTS_BODY.replace('- Conventional', `${AGENTS_CODE_ORGANIZATION}- Conventional`) - : AGENTS_BODY +function agentsBody(ctx: ToolContext, languages: Language[]): string { + const guides = ['general', ...languages].map(id => `\`${RULES_PREFIX}/${id}.md\``) + const pointer = guides.length > 1 + ? `${guides.slice(0, -1).join(', ')} and ${guides.at(-1)}` + : guides[0] + return `${AGENTS_INTRO}${languages.includes('typescript') ? AGENTS_TYPESCRIPT : ''}${AGENTS_GENERAL}${wantsCodeOrganization(ctx) ? AGENTS_CODE_ORGANIZATION : ''}- Conventional Commits for all commit messages + +For the full rules (what an AI coding assistant needs to know before writing code), +read ${pointer}.` } export async function applyAgentsMd(ctx: ToolContext): Promise { @@ -190,7 +197,8 @@ export async function applyAgentsMd(ctx: ToolContext): Promise const target = joinPath(ctx.cwd, 'AGENTS.md') const label = 'AGENTS.md' const existing = readFileOrNull(target) - const next = upsertMarkerBlock(existing, agentsBody(ctx)) + const languages = ctx.languages ?? languagesFromAgentsBlock(existing) ?? detectLanguages(ctx.cwd) + const next = upsertMarkerBlock(existing, agentsBody(ctx, languages)) if (existing == null) { writeFileSync(target, next) diff --git a/packages/cli/src/commands.ts b/packages/cli/src/commands.ts index 706de10..55cf25e 100644 --- a/packages/cli/src/commands.ts +++ b/packages/cli/src/commands.ts @@ -1,3 +1,4 @@ +import type { Language } from './languages.js' import type { Tool, ToolApplyResult, ToolContext } from './tools.js' import { spawnSync } from 'node:child_process' import { existsSync } from 'node:fs' @@ -12,10 +13,12 @@ import { readPackageJson, } from './fs-utils.js' import { t } from './i18n.js' +import { detectLanguages, LANGUAGES } from './languages.js' export interface CommandOptions { cwd: string autoAccept: boolean + languages?: Language[] } function assertProjectRoot(cwd: string): void { @@ -94,6 +97,27 @@ export async function runInit(opts: CommandOptions): Promise { .map(id => TOOLS.find(tool => tool.id === id)) .filter((tool): tool is Tool => tool != null) + let languages = opts.languages + if (languages === undefined && selectedTools.some(tool => tool.id === 'agents-md')) { + const detected = detectLanguages(opts.cwd) + if (opts.autoAccept) { + languages = detected + } + else { + const chosen = await multiselect({ + message: t('selectLanguages'), + options: LANGUAGES.map(value => ({ value, label: value })), + initialValues: detected, + required: false, + }) + if (isCancel(chosen)) { + cancel(t('cancelled')) + process.exit(0) + } + languages = chosen + } + } + // 1. Install packages (de-duped) for selected tools. const packagesToInstall = Array.from( new Set(selectedTools.flatMap(tool => tool.packages)), @@ -115,7 +139,7 @@ export async function runInit(opts: CommandOptions): Promise { } // 2. Apply each tool's side-effects. - const ctx: ToolContext = { cwd: opts.cwd, autoAccept: opts.autoAccept, selectedToolIds: selectedTools.map(tool => tool.id) } + const ctx: ToolContext = { cwd: opts.cwd, autoAccept: opts.autoAccept, selectedToolIds: selectedTools.map(tool => tool.id), languages } const results: ToolApplyResult[] = [] for (const tool of selectedTools) { results.push(await tool.apply(ctx)) @@ -130,7 +154,7 @@ export async function runInit(opts: CommandOptions): Promise { export async function runUpdate(opts: CommandOptions): Promise { assertProjectRoot(opts.cwd) intro(t('appTitle')) - const result = await applyAgentsMd({ cwd: opts.cwd, autoAccept: true, selectedToolIds: [] }) + const result = await applyAgentsMd({ cwd: opts.cwd, autoAccept: true, selectedToolIds: [], languages: opts.languages }) reportResults(result) log.success(t('agentsBlockUpdated')) outro(t('done')) @@ -156,6 +180,9 @@ export function runDoctor(opts: CommandOptions): void { log.info(` ${tool.label} — ${suffix}`) } + const languages = detectLanguages(opts.cwd) + log.info(` ${languages.length > 0 ? t('doctorLanguages')(languages.join(', ')) : t('doctorNoLanguages')}`) + // File presence checks. const files = [ 'eslint.config.mjs', diff --git a/packages/cli/src/fs-utils.ts b/packages/cli/src/fs-utils.ts index b023e75..eda5208 100644 --- a/packages/cli/src/fs-utils.ts +++ b/packages/cli/src/fs-utils.ts @@ -70,6 +70,15 @@ export function buildMarkerBlock(body: string): string { return `${BEGIN_MARKER}\n${body.trim()}\n${END_MARKER}` } +export function readMarkerBlock(content: string): string | undefined { + const begin = content.indexOf(BEGIN_MARKER) + const end = content.indexOf(END_MARKER) + if (begin === -1 || end === -1 || end < begin) { + return undefined + } + return content.slice(begin + BEGIN_MARKER.length, end) +} + export function upsertMarkerBlock(existing: string | null, body: string): string { const block = buildMarkerBlock(body) if (existing == null) { diff --git a/packages/cli/src/i18n.ts b/packages/cli/src/i18n.ts index e08e083..a15fadd 100644 --- a/packages/cli/src/i18n.ts +++ b/packages/cli/src/i18n.ts @@ -1,4 +1,5 @@ import process from 'node:process' +import { LANGUAGES } from './languages.js' export type Locale = 'en' | 'ko' @@ -29,7 +30,11 @@ interface Messages { optionBeforeCheck: (option: string) => string deprecatedLangFlag: (locale: Locale) => string ignoredLangFlag: (locale: Locale) => string - invalidLocaleFlag: (flag: LocaleFlag, value: string) => string + invalidLocaleFlag: (value: string) => string + invalidLangFlag: (value: string) => string + selectLanguages: string + doctorLanguages: (languages: string) => string + doctorNoLanguages: string usage: string checkUsage: string } @@ -62,9 +67,11 @@ const messages: Record = { optionBeforeCheck: option => `${option} is a check option; put it after \`check\` (please-style check ${option})`, deprecatedLangFlag: locale => `--lang ${locale} is deprecated; use --locale ${locale}. A future release will use --lang to select programming languages.`, ignoredLangFlag: locale => `--lang ${locale} is deprecated and ignored because --locale is set; remove it. A future release will use --lang to select programming languages.`, - invalidLocaleFlag: (flag, value) => flag === '--locale' - ? `Unknown --locale value: "${value}". Use ko or en.` - : `Unknown --lang value: "${value}". To set the CLI locale, use --locale .`, + invalidLocaleFlag: value => `Unknown --locale value: "${value}". Use ko or en.`, + invalidLangFlag: value => `Unknown --lang value: "${value}". Use a comma-separated list of: ${LANGUAGES.join(', ')}. To set the CLI locale, use --locale .`, + selectLanguages: 'Select the languages for the AGENTS.md rules block', + doctorLanguages: languages => `Detected languages: ${languages}`, + doctorNoLanguages: 'Detected languages: none', usage: `Usage: pleaseai-code-style [options] @@ -76,6 +83,7 @@ Commands: Options: --yes, -y Accept defaults, overwrite existing files + --lang Languages for the rules block, comma-separated (default: detected) --locale Force CLI locale --help, -h Show this message --version, -v Print version`, @@ -123,9 +131,11 @@ Exit codes: 0 ok or warnings only, 1 findings with --strict, 2 usage or config e optionBeforeCheck: option => `${option}는 check 옵션입니다. \`check\` 뒤에 쓰세요 (please-style check ${option})`, deprecatedLangFlag: locale => `--lang ${locale} 옵션은 더 이상 권장하지 않습니다. 대신 --locale ${locale} 옵션을 쓰세요. 이후 릴리스에서 --lang은 프로그래밍 언어 선택에 쓰입니다.`, ignoredLangFlag: locale => `--lang ${locale} 옵션은 더 이상 권장하지 않으며, --locale 옵션이 있어 무시합니다. 이 옵션을 지우세요. 이후 릴리스에서 --lang은 프로그래밍 언어 선택에 쓰입니다.`, - invalidLocaleFlag: (flag, value) => flag === '--locale' - ? `알 수 없는 --locale 값: "${value}". ko 또는 en을 쓰세요.` - : `알 수 없는 --lang 값: "${value}". CLI 로케일은 --locale 으로 지정하세요.`, + invalidLocaleFlag: value => `알 수 없는 --locale 값: "${value}". ko 또는 en을 쓰세요.`, + invalidLangFlag: value => `알 수 없는 --lang 값: "${value}". 쉼표로 구분한 목록으로 지정하세요: ${LANGUAGES.join(', ')}. CLI 로케일은 --locale 으로 지정하세요.`, + selectLanguages: 'AGENTS.md 룰 블록에 넣을 언어를 선택하세요', + doctorLanguages: languages => `감지된 언어: ${languages}`, + doctorNoLanguages: '감지된 언어: 없음', usage: `사용법: pleaseai-code-style [options] @@ -137,6 +147,7 @@ Exit codes: 0 ok or warnings only, 1 findings with --strict, 2 usage or config e 옵션: --yes, -y 기본값 수락, 기존 파일 덮어쓰기 + --lang 룰 블록에 넣을 언어, 쉼표로 구분 (기본: 자동 감지) --locale CLI 로케일 강제 지정 --help, -h 이 메시지 표시 --version, -v 버전 출력`, @@ -167,8 +178,6 @@ export function detectLocale(override?: string): Locale { return env.toLowerCase().startsWith('ko') ? 'ko' : 'en' } -export type LocaleFlag = '--locale' | '--lang' - type FlagValue = string | boolean | undefined export interface LangWarning { @@ -178,7 +187,7 @@ export interface LangWarning { export type LocaleFlags = | { kind: 'ok', locale: Locale, langWarning?: LangWarning } - | { kind: 'invalid', locale: Locale, flag: LocaleFlag, value: string } + | { kind: 'invalid', locale: Locale, value: string } function asLocale(value: FlagValue): Locale | undefined { return value === 'ko' || value === 'en' ? value : undefined @@ -193,10 +202,7 @@ export function resolveLocaleFlags(flags: { locale?: FlagValue, lang?: FlagValue const fromLang = asLocale(flags.lang) const locale = fromLocale ?? fromLang ?? detectLocale() if (flags.locale !== undefined && fromLocale === undefined) { - return { kind: 'invalid', locale, flag: '--locale', value: flagText(flags.locale) } - } - if (flags.lang !== undefined && fromLang === undefined) { - return { kind: 'invalid', locale, flag: '--lang', value: flagText(flags.lang) } + return { kind: 'invalid', locale, value: flagText(flags.locale) } } if (fromLang === undefined) { return { kind: 'ok', locale } diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index fafd3c7..92190b6 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -6,6 +6,7 @@ import { parseArgs } from 'node:util' import { runCheck } from './check/run-check.js' import { runDoctor, runInit, runUpdate } from './commands.js' import { resolveLocaleFlags, setLocale, t } from './i18n.js' +import { parseLangFlag } from './languages.js' const GLOBAL_OPTIONS = { yes: { type: 'boolean', short: 'y' }, @@ -27,7 +28,7 @@ const { values: allValues, positionals, tokens } = parseArgs({ const localeFlags = resolveLocaleFlags({ locale: allValues.locale, lang: allValues.lang }) setLocale(localeFlags.locale) if (localeFlags.kind === 'invalid') { - process.stderr.write(`${t('invalidLocaleFlag')(localeFlags.flag, localeFlags.value)}\n`) + process.stderr.write(`${t('invalidLocaleFlag')(localeFlags.value)}\n`) process.exit(1) } else if (localeFlags.langWarning !== undefined) { @@ -35,6 +36,13 @@ else if (localeFlags.langWarning !== undefined) { process.stderr.write(`${t(kind === 'deprecated' ? 'deprecatedLangFlag' : 'ignoredLangFlag')(locale)}\n`) } +const langFlag = parseLangFlag(allValues.lang) +if (langFlag.kind === 'invalid') { + process.stderr.write(`${t('invalidLangFlag')(langFlag.value)}\n`) + process.exit(1) +} +const languages = langFlag.kind === 'languages' ? langFlag.languages : undefined + const command = positionals[0] ?? 'init' // `check` parses its own flags strictly, so only what precedes it is global // (`please-style check -h` shows the check usage, not the global one). @@ -74,13 +82,13 @@ const cwd = process.cwd() try { switch (command) { case 'init': - await runInit({ cwd, autoAccept }) + await runInit({ cwd, autoAccept, languages }) break case 'update': - await runUpdate({ cwd, autoAccept }) + await runUpdate({ cwd, autoAccept, languages }) break case 'doctor': - runDoctor({ cwd, autoAccept }) + runDoctor({ cwd, autoAccept, languages }) break case 'check': // exitCode, not exit(): a large --json report must flush before exiting. diff --git a/packages/cli/src/languages.ts b/packages/cli/src/languages.ts new file mode 100644 index 0000000..9c01c62 --- /dev/null +++ b/packages/cli/src/languages.ts @@ -0,0 +1,66 @@ +import { existsSync } from 'node:fs' +import { resolve } from 'node:path' +import { readMarkerBlock } from './fs-utils.js' + +export const LANGUAGES = ['typescript', 'dart', 'rust', 'java', 'go', 'python', 'ruby', 'csharp', 'html-css'] as const + +export type Language = typeof LANGUAGES[number] + +export const RULES_PREFIX = 'node_modules/@pleaseai/code-style/rules' + +const MARKERS: Array<{ language: Language, files: string[] }> = [ + { language: 'typescript', files: ['package.json', 'tsconfig.json'] }, + { language: 'dart', files: ['pubspec.yaml'] }, + { language: 'rust', files: ['Cargo.toml'] }, + { language: 'java', files: ['build.gradle', 'build.gradle.kts', 'pom.xml'] }, + { language: 'go', files: ['go.mod'] }, + { language: 'python', files: ['pyproject.toml'] }, +] + +function isLanguage(value: string): value is Language { + return (LANGUAGES as readonly string[]).includes(value) +} + +function inOrder(languages: Iterable): Language[] { + const set = new Set(languages) + return LANGUAGES.filter(language => set.has(language)) +} + +export function detectLanguages(cwd: string): Language[] { + return inOrder(MARKERS + .filter(({ files }) => files.some(f => existsSync(resolve(cwd, f)))) + .map(({ language }) => language)) +} + +export type LangFlag + = | { kind: 'none' } + | { kind: 'languages', languages: Language[] } + | { kind: 'invalid', value: string } + +const LEGACY_LOCALE_ALIASES: ReadonlyArray = ['ko', 'en'] + +export function parseLangFlag(value: string | boolean | undefined): LangFlag { + if (value === undefined || LEGACY_LOCALE_ALIASES.includes(value)) { + return { kind: 'none' } + } + if (typeof value !== 'string') { + return { kind: 'invalid', value: '' } + } + const items = value.split(',').map(item => item.trim()).filter(item => item !== '') + if (items.length === 0 || !items.every(isLanguage)) { + return { kind: 'invalid', value } + } + return { kind: 'languages', languages: inOrder(items) } +} + +function isLanguageAwareBlock(block: string | undefined): block is string { + return block !== undefined && block.includes(`${RULES_PREFIX}/general.md`) +} + +export function languagesFromAgentsBlock(content: string | null): Language[] | undefined { + const block = content == null ? undefined : readMarkerBlock(content) + if (!isLanguageAwareBlock(block)) { + return undefined + } + return LANGUAGES.filter(language => block.includes(`${RULES_PREFIX}/${language}.md`)) +} diff --git a/packages/cli/src/tools.ts b/packages/cli/src/tools.ts index ddf3fa4..73a7383 100644 --- a/packages/cli/src/tools.ts +++ b/packages/cli/src/tools.ts @@ -1,3 +1,5 @@ +import type { Language } from './languages.js' + /** * Registry of tools this CLI knows how to install + configure. * @@ -14,6 +16,7 @@ export interface ToolContext { autoAccept: boolean /** Tools chosen in this run; unset when the caller has no selection (the code-organization pointer is then kept). */ selectedToolIds?: ToolId[] + languages?: Language[] } export interface ToolApplyResult { diff --git a/packages/cli/test/appliers.test.ts b/packages/cli/test/appliers.test.ts index ee27a36..3a9699c 100644 --- a/packages/cli/test/appliers.test.ts +++ b/packages/cli/test/appliers.test.ts @@ -4,6 +4,7 @@ import { join } from 'node:path' import { log } from '@clack/prompts' import { afterEach, beforeEach, describe, expect, spyOn, test } from 'bun:test' import { applyAgentsMd, applyAstGrep, findTool } from '../src/appliers.js' +import { LANGUAGES } from '../src/languages.js' let dir: string @@ -145,6 +146,10 @@ describe('editorconfig tool', () => { // --------------------------------------------------------------------------- describe('AGENTS.md rules block', () => { + beforeEach(() => { + writeFileSync(join(dir, 'package.json'), '{}') + }) + test('puts tests under the test root instead of next to the source', async () => { await applyAgentsMd({ cwd: dir, autoAccept: true }) const body = readFileSync(join(dir, 'AGENTS.md'), 'utf-8') @@ -194,4 +199,33 @@ describe('AGENTS.md rules block', () => { await applyAgentsMd({ cwd: dir, autoAccept: true, selectedToolIds: [] }) expect(readFileSync(join(dir, 'AGENTS.md'), 'utf-8')).toContain('please-style check') }) + + test('links the selected languages and drops the TypeScript bullets when typescript is not selected', async () => { + await applyAgentsMd({ cwd: dir, autoAccept: true, languages: ['dart', 'rust'] }) + const body = readFileSync(join(dir, 'AGENTS.md'), 'utf-8') + const linked = [...body.matchAll(/node_modules\/@pleaseai\/code-style\/([\w./-]+\.md)/g)].map(m => m[1]) + expect(linked).toEqual(['rules/general.md', 'rules/dart.md', 'rules/rust.md']) + expect(body).not.toContain('@pleaseai/eslint-config') + expect(body).not.toContain('ESM only') + expect(body).toContain('- File size target') + expect(body).toContain('- Conventional Commits') + }) + + test('keeps the languages of the existing block when none are given, even if detection differs', async () => { + await applyAgentsMd({ cwd: dir, autoAccept: true, languages: ['dart'] }) + await applyAgentsMd({ cwd: dir, autoAccept: true }) + const body = readFileSync(join(dir, 'AGENTS.md'), 'utf-8') + expect(body).toContain('rules/dart.md') + expect(body).not.toContain('rules/typescript.md') + }) + + test('ships a guide for every language, covered by the package files', () => { + const pkgRoot = join(import.meta.dir, '..') + const { files } = JSON.parse(readFileSync(join(pkgRoot, 'package.json'), 'utf-8')) as { files: string[] } + for (const id of LANGUAGES) { + const path = `rules/${id}.md` + expect(existsSync(join(pkgRoot, path))).toBe(true) + expect(files.some(entry => path.startsWith(`${entry}/`))).toBe(true) + } + }) }) diff --git a/packages/cli/test/i18n.test.ts b/packages/cli/test/i18n.test.ts index 88157b1..5290fe2 100644 --- a/packages/cli/test/i18n.test.ts +++ b/packages/cli/test/i18n.test.ts @@ -70,13 +70,13 @@ describe('resolveLocaleFlags', () => { expect(resolveLocaleFlags({ locale: 'en', lang: 'ko' })).toEqual({ kind: 'ok', locale: 'en', langWarning: { kind: 'ignored', locale: 'ko' } }) }) - test('an unknown --lang value is invalid instead of ignored', () => { - expect(resolveLocaleFlags({ lang: 'dart' })).toMatchObject({ kind: 'invalid', flag: '--lang', value: 'dart' }) - expect(resolveLocaleFlags({ lang: true })).toMatchObject({ kind: 'invalid', flag: '--lang', value: '' }) + test('a --lang language list does not affect the locale', () => { + expect(resolveLocaleFlags({ lang: 'dart,rust', locale: 'ko' })).toEqual({ kind: 'ok', locale: 'ko' }) + expect(resolveLocaleFlags({ lang: true, locale: 'en' })).toEqual({ kind: 'ok', locale: 'en' }) }) test('an unknown --locale value is invalid', () => { - expect(resolveLocaleFlags({ locale: 'fr' })).toMatchObject({ kind: 'invalid', flag: '--locale', value: 'fr' }) + expect(resolveLocaleFlags({ locale: 'fr' })).toMatchObject({ kind: 'invalid', value: 'fr' }) }) }) diff --git a/packages/cli/test/index.test.ts b/packages/cli/test/index.test.ts index 016f076..4ce173e 100644 --- a/packages/cli/test/index.test.ts +++ b/packages/cli/test/index.test.ts @@ -1,13 +1,16 @@ import { spawnSync } from 'node:child_process' +import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' import process from 'node:process' import { fileURLToPath } from 'node:url' import { describe, expect, test } from 'bun:test' const CLI = fileURLToPath(new URL('../src/index.ts', import.meta.url)) -function run(args: string[]): { status: number | null, stdout: string, stderr: string } { +function run(args: string[], cwd?: string): { status: number | null, stdout: string, stderr: string } { const env = { ...process.env, LC_ALL: 'en_US.UTF-8', LANG: 'en_US.UTF-8', LC_MESSAGES: 'en_US.UTF-8' } - const res = spawnSync('bun', [CLI, ...args], { encoding: 'utf-8', env }) + const res = spawnSync('bun', [CLI, ...args], { encoding: 'utf-8', env, cwd, stdio: ['ignore', 'pipe', 'pipe'] }) return { status: res.status, stdout: res.stdout, stderr: res.stderr } } @@ -32,11 +35,31 @@ describe('locale flags', () => { expect(res.stdout).toContain('Usage:') expect(res.stderr).toContain('--lang ko is deprecated and ignored because --locale is set; remove it.') }) +}) - test('an unknown --lang value exits non-zero instead of being ignored', () => { - const res = run(['--lang', 'dart', '--help']) +describe('--lang languages', () => { + test('an unknown value exits non-zero and lists the accepted languages', () => { + const res = run(['--lang', 'foo', '--help']) expect(res.status).toBe(1) expect(res.stdout).toBe('') - expect(res.stderr).toContain('Unknown --lang value: "dart"') + expect(res.stderr).toContain('Unknown --lang value: "foo"') + expect(res.stderr).toContain('typescript') + expect(res.stderr).toContain('html-css') + expect(res.stderr).toContain('--locale ') + }) + + test('update --lang dart links the dart guide instead of the TypeScript one', () => { + const dir = mkdtempSync(join(tmpdir(), 'pleaseai-cli-lang-')) + try { + writeFileSync(join(dir, 'package.json'), '{}') + const res = run(['update', '--lang', 'dart'], dir) + expect(res.status).toBe(0) + const body = readFileSync(join(dir, 'AGENTS.md'), 'utf-8') + expect(body).toContain('rules/dart.md') + expect(body).not.toContain('rules/typescript.md') + } + finally { + rmSync(dir, { recursive: true, force: true }) + } }) }) diff --git a/packages/cli/test/languages.test.ts b/packages/cli/test/languages.test.ts new file mode 100644 index 0000000..bf56096 --- /dev/null +++ b/packages/cli/test/languages.test.ts @@ -0,0 +1,89 @@ +import { mkdtempSync, rmSync, writeFileSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { afterEach, beforeEach, describe, expect, test } from 'bun:test' +import { detectLanguages, languagesFromAgentsBlock, parseLangFlag } from '../src/languages.js' + +let dir: string + +beforeEach(() => { + dir = mkdtempSync(join(tmpdir(), 'pleaseai-lang-test-')) +}) + +afterEach(() => { + rmSync(dir, { recursive: true, force: true }) +}) + +function touch(...files: string[]): void { + for (const f of files) { + writeFileSync(join(dir, f), '') + } +} + +describe('detectLanguages', () => { + test.each([ + ['package.json', 'typescript'], + ['tsconfig.json', 'typescript'], + ['build.gradle', 'java'], + ['build.gradle.kts', 'java'], + ['pom.xml', 'java'], + ['pubspec.yaml', 'dart'], + ['Cargo.toml', 'rust'], + ['go.mod', 'go'], + ['pyproject.toml', 'python'], + ])('%s marks %s', (marker, language) => { + touch(marker) + expect(detectLanguages(dir)).toEqual([language]) + }) + + test('reports every language with a root marker, in guide order', () => { + touch('pubspec.yaml', 'package.json') + expect(detectLanguages(dir)).toEqual(['typescript', 'dart']) + }) + + test('returns nothing without a marker', () => { + expect(detectLanguages(dir)).toEqual([]) + }) +}) + +describe('parseLangFlag', () => { + test('an unset flag and the locale aliases select no languages', () => { + expect(parseLangFlag(undefined)).toEqual({ kind: 'none' }) + expect(parseLangFlag('ko')).toEqual({ kind: 'none' }) + expect(parseLangFlag('en')).toEqual({ kind: 'none' }) + }) + + test('a list is trimmed, deduped, and ordered as the guides', () => { + expect(parseLangFlag('dart')).toEqual({ kind: 'languages', languages: ['dart'] }) + expect(parseLangFlag(' rust, typescript,,rust ')).toEqual({ kind: 'languages', languages: ['typescript', 'rust'] }) + }) + + test('an unknown item, an empty list, or a missing value is invalid', () => { + expect(parseLangFlag('dart,foo')).toEqual({ kind: 'invalid', value: 'dart,foo' }) + expect(parseLangFlag(',')).toEqual({ kind: 'invalid', value: ',' }) + expect(parseLangFlag(true)).toEqual({ kind: 'invalid', value: '' }) + }) + + test('ko or en inside a list is invalid', () => { + expect(parseLangFlag('ko,dart')).toEqual({ kind: 'invalid', value: 'ko,dart' }) + }) +}) + +describe('languagesFromAgentsBlock', () => { + const block = (body: string): string => `\n${body}\n\n` + const guide = (id: string): string => `node_modules/@pleaseai/code-style/rules/${id}.md` + + test('is undefined without a block', () => { + expect(languagesFromAgentsBlock(null)).toBeUndefined() + expect(languagesFromAgentsBlock(`see ${guide('general')} and ${guide('dart')}`)).toBeUndefined() + }) + + test('is undefined for a legacy block that does not link the general guide', () => { + expect(languagesFromAgentsBlock(block(`read ${guide('typescript')}`))).toBeUndefined() + }) + + test('lists the languages the block links', () => { + expect(languagesFromAgentsBlock(block(`read ${guide('general')} and ${guide('dart')}`))).toEqual(['dart']) + expect(languagesFromAgentsBlock(block(`read ${guide('general')}`))).toEqual([]) + }) +}) From 402dd57eb76d5eeb6faa14249d7b51415a60506a Mon Sep 17 00:00:00 2001 From: Minsu Lee Date: Fri, 23 Oct 2026 22:46:36 +0900 Subject: [PATCH 2/3] chore(cli): apply AI code review suggestions Move the TypeScript test-layout bullet out of the general AGENTS.md block, and cover the v0.1.1 block falling back to language detection. Refs #65 --- packages/cli/src/appliers.ts | 4 ++-- packages/cli/test/appliers.test.ts | 1 + packages/cli/test/languages.test.ts | 5 +++++ 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/cli/src/appliers.ts b/packages/cli/src/appliers.ts index e6d86c0..8aea6db 100644 --- a/packages/cli/src/appliers.ts +++ b/packages/cli/src/appliers.ts @@ -151,11 +151,11 @@ const AGENTS_TYPESCRIPT = `- Formatter: \`@pleaseai/eslint-config\` (wraps \`@an - ESM only — never emit \`require\`/\`module.exports\` - TypeScript: \`strict: true\`, prefer \`type\` over \`interface\`, no implicit \`any\` - Prefer named exports, early returns, \`async\`/\`await\`, optional chaining +- Tests live under the package's \`test/\` (or \`tests/\`) root, mirroring the source path + (\`src/foo/bar.ts\` → \`test/foo/bar.test.ts\`); no \`import.meta.vitest\` in-source tests ` const AGENTS_GENERAL = `- File size target: ≤ 500 lines -- Tests live under the package's \`test/\` (or \`tests/\`) root, mirroring the source path - (\`src/foo/bar.ts\` → \`test/foo/bar.test.ts\`); no \`import.meta.vitest\` in-source tests ` const AGENTS_CODE_ORGANIZATION = `- Code organization (named exports, file names, error files, test paths, shared helpers): diff --git a/packages/cli/test/appliers.test.ts b/packages/cli/test/appliers.test.ts index 3a9699c..bb9c6bb 100644 --- a/packages/cli/test/appliers.test.ts +++ b/packages/cli/test/appliers.test.ts @@ -207,6 +207,7 @@ describe('AGENTS.md rules block', () => { expect(linked).toEqual(['rules/general.md', 'rules/dart.md', 'rules/rust.md']) expect(body).not.toContain('@pleaseai/eslint-config') expect(body).not.toContain('ESM only') + expect(body).not.toContain('import.meta.vitest') expect(body).toContain('- File size target') expect(body).toContain('- Conventional Commits') }) diff --git a/packages/cli/test/languages.test.ts b/packages/cli/test/languages.test.ts index bf56096..b1a1de7 100644 --- a/packages/cli/test/languages.test.ts +++ b/packages/cli/test/languages.test.ts @@ -82,6 +82,11 @@ describe('languagesFromAgentsBlock', () => { expect(languagesFromAgentsBlock(block(`read ${guide('typescript')}`))).toBeUndefined() }) + test('is undefined for the v0.1.1 block, so update falls back to detection', () => { + const released = block('For the full rules (what an AI coding assistant needs to know before writing code),\nread `node_modules/@pleaseai/code-style/rules.md`.') + expect(languagesFromAgentsBlock(released)).toBeUndefined() + }) + test('lists the languages the block links', () => { expect(languagesFromAgentsBlock(block(`read ${guide('general')} and ${guide('dart')}`))).toEqual(['dart']) expect(languagesFromAgentsBlock(block(`read ${guide('general')}`))).toEqual([]) From a3aa56bf9e4a6ecb6ad9c1bdb4c63929e5808eae Mon Sep 17 00:00:00 2001 From: Minsu Lee Date: Fri, 23 Oct 2026 22:50:24 +0900 Subject: [PATCH 3/3] chore(cli): apply AI code review suggestions doctor reports --lang languages as selected rather than detected, the --lang ko|en warnings stop promising a future --lang, and the docs describe the update default. Refs #65 --- apps/docs/src/content/docs/en/cli.mdx | 2 +- apps/docs/src/content/docs/ko/cli.mdx | 2 +- packages/cli/README.ko.md | 2 +- packages/cli/README.md | 2 +- packages/cli/src/commands.ts | 9 +++++++-- packages/cli/src/i18n.ts | 11 +++++++---- packages/cli/test/index.test.ts | 14 ++++++++++++++ 7 files changed, 32 insertions(+), 10 deletions(-) diff --git a/apps/docs/src/content/docs/en/cli.mdx b/apps/docs/src/content/docs/en/cli.mdx index 74bf18d..e73e3b0 100644 --- a/apps/docs/src/content/docs/en/cli.mdx +++ b/apps/docs/src/content/docs/en/cli.mdx @@ -131,7 +131,7 @@ The full canonical rules are shipped per language for AI assistants that prefer | Flag | Description | | --- | --- | | `--yes`, `-y` | Accept defaults and overwrite existing files without prompting | -| `--lang ` | Languages for the AGENTS.md rules block, comma-separated (defaults to detected; see below) | +| `--lang ` | Languages for the AGENTS.md rules block, comma-separated (`init`/`doctor`: detected; `update`: the languages already in the block; see below) | | `--locale ` | Force the CLI locale (defaults to `$LC_ALL` / `$LANG`) | | `--help`, `-h` | Print help | | `--version`, `-v` | Print the CLI version | diff --git a/apps/docs/src/content/docs/ko/cli.mdx b/apps/docs/src/content/docs/ko/cli.mdx index 85bd5f8..9ac0dc1 100644 --- a/apps/docs/src/content/docs/ko/cli.mdx +++ b/apps/docs/src/content/docs/ko/cli.mdx @@ -131,7 +131,7 @@ CLI는 두 마커 주석 사이의 내용 **만** 관리하며 — `AGENTS.md` | 플래그 | 설명 | | --- | --- | | `--yes`, `-y` | 기본값을 수락하고 기존 파일을 확인 없이 덮어씀 | -| `--lang ` | AGENTS.md 룰 블록에 넣을 언어, 쉼표로 구분 (기본값은 자동 감지, 아래 참고) | +| `--lang ` | AGENTS.md 룰 블록에 넣을 언어, 쉼표로 구분 (`init`/`doctor`: 자동 감지, `update`: 블록에 이미 있는 언어, 아래 참고) | | `--locale ` | CLI 언어 강제 지정 (기본값은 `$LC_ALL` / `$LANG`) | | `--help`, `-h` | 도움말 출력 | | `--version`, `-v` | CLI 버전 출력 | diff --git a/packages/cli/README.ko.md b/packages/cli/README.ko.md index c865ce2..1d7e729 100644 --- a/packages/cli/README.ko.md +++ b/packages/cli/README.ko.md @@ -220,7 +220,7 @@ CLI는 아래 마커 사이의 내용만 관리합니다 — `AGENTS.md`의 나 | 플래그 | 설명 | | --- | --- | | `--yes`, `-y` | 기본값을 수락하고 기존 파일을 확인 없이 덮어씀 | -| `--lang ` | AGENTS.md 룰 블록에 넣을 언어, 쉼표로 구분 (기본값은 자동 감지, 아래 참고) | +| `--lang ` | AGENTS.md 룰 블록에 넣을 언어, 쉼표로 구분 (`init`/`doctor`: 자동 감지, `update`: 블록에 이미 있는 언어, 아래 참고) | | `--locale ` | CLI 언어 강제 지정 (기본값은 `$LANG`) | | `--help`, `-h` | 도움말 출력 | | `--version`, `-v` | 버전 출력 | diff --git a/packages/cli/README.md b/packages/cli/README.md index 9b64917..f11b5f9 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -404,7 +404,7 @@ JavaScript). | Flag | Description | | --- | --- | | `--yes`, `-y` | Accept defaults, overwrite existing files without prompting | -| `--lang ` | Languages for the AGENTS.md rules block, comma-separated (defaults to detected; see below) | +| `--lang ` | Languages for the AGENTS.md rules block, comma-separated (`init`/`doctor`: detected; `update`: the languages already in the block; see below) | | `--locale ` | Force the CLI locale (defaults to `$LANG`) | | `--help`, `-h` | Print help | | `--version`, `-v` | Print version | diff --git a/packages/cli/src/commands.ts b/packages/cli/src/commands.ts index 55cf25e..2115fad 100644 --- a/packages/cli/src/commands.ts +++ b/packages/cli/src/commands.ts @@ -180,8 +180,13 @@ export function runDoctor(opts: CommandOptions): void { log.info(` ${tool.label} — ${suffix}`) } - const languages = detectLanguages(opts.cwd) - log.info(` ${languages.length > 0 ? t('doctorLanguages')(languages.join(', ')) : t('doctorNoLanguages')}`) + if (opts.languages != null) { + log.info(` ${t('doctorSelectedLanguages')(opts.languages.join(', '))}`) + } + else { + const languages = detectLanguages(opts.cwd) + log.info(` ${languages.length > 0 ? t('doctorLanguages')(languages.join(', ')) : t('doctorNoLanguages')}`) + } // File presence checks. const files = [ diff --git a/packages/cli/src/i18n.ts b/packages/cli/src/i18n.ts index a15fadd..732172c 100644 --- a/packages/cli/src/i18n.ts +++ b/packages/cli/src/i18n.ts @@ -35,6 +35,7 @@ interface Messages { selectLanguages: string doctorLanguages: (languages: string) => string doctorNoLanguages: string + doctorSelectedLanguages: (languages: string) => string usage: string checkUsage: string } @@ -65,13 +66,14 @@ const messages: Record = { unknownCommand: cmd => `Unknown command: ${cmd}`, unexpectedArguments: args => `Unexpected arguments: ${args}`, optionBeforeCheck: option => `${option} is a check option; put it after \`check\` (please-style check ${option})`, - deprecatedLangFlag: locale => `--lang ${locale} is deprecated; use --locale ${locale}. A future release will use --lang to select programming languages.`, - ignoredLangFlag: locale => `--lang ${locale} is deprecated and ignored because --locale is set; remove it. A future release will use --lang to select programming languages.`, + deprecatedLangFlag: locale => `--lang ${locale} is deprecated; use --locale ${locale}. Use --lang to select programming languages.`, + ignoredLangFlag: locale => `--lang ${locale} is deprecated and ignored because --locale is set; remove it. Use --lang to select programming languages.`, invalidLocaleFlag: value => `Unknown --locale value: "${value}". Use ko or en.`, invalidLangFlag: value => `Unknown --lang value: "${value}". Use a comma-separated list of: ${LANGUAGES.join(', ')}. To set the CLI locale, use --locale .`, selectLanguages: 'Select the languages for the AGENTS.md rules block', doctorLanguages: languages => `Detected languages: ${languages}`, doctorNoLanguages: 'Detected languages: none', + doctorSelectedLanguages: languages => `Selected languages (--lang): ${languages}`, usage: `Usage: pleaseai-code-style [options] @@ -129,13 +131,14 @@ Exit codes: 0 ok or warnings only, 1 findings with --strict, 2 usage or config e unknownCommand: cmd => `알 수 없는 명령어: ${cmd}`, unexpectedArguments: args => `예상하지 못한 인자: ${args}`, optionBeforeCheck: option => `${option}는 check 옵션입니다. \`check\` 뒤에 쓰세요 (please-style check ${option})`, - deprecatedLangFlag: locale => `--lang ${locale} 옵션은 더 이상 권장하지 않습니다. 대신 --locale ${locale} 옵션을 쓰세요. 이후 릴리스에서 --lang은 프로그래밍 언어 선택에 쓰입니다.`, - ignoredLangFlag: locale => `--lang ${locale} 옵션은 더 이상 권장하지 않으며, --locale 옵션이 있어 무시합니다. 이 옵션을 지우세요. 이후 릴리스에서 --lang은 프로그래밍 언어 선택에 쓰입니다.`, + deprecatedLangFlag: locale => `--lang ${locale} 옵션은 더 이상 권장하지 않습니다. 대신 --locale ${locale} 옵션을 쓰세요. 프로그래밍 언어는 --lang으로 고르세요.`, + ignoredLangFlag: locale => `--lang ${locale} 옵션은 더 이상 권장하지 않으며, --locale 옵션이 있어 무시합니다. 이 옵션을 지우세요. 프로그래밍 언어는 --lang으로 고르세요.`, invalidLocaleFlag: value => `알 수 없는 --locale 값: "${value}". ko 또는 en을 쓰세요.`, invalidLangFlag: value => `알 수 없는 --lang 값: "${value}". 쉼표로 구분한 목록으로 지정하세요: ${LANGUAGES.join(', ')}. CLI 로케일은 --locale 으로 지정하세요.`, selectLanguages: 'AGENTS.md 룰 블록에 넣을 언어를 선택하세요', doctorLanguages: languages => `감지된 언어: ${languages}`, doctorNoLanguages: '감지된 언어: 없음', + doctorSelectedLanguages: languages => `선택한 언어(--lang): ${languages}`, usage: `사용법: pleaseai-code-style [options] diff --git a/packages/cli/test/index.test.ts b/packages/cli/test/index.test.ts index 4ce173e..5ea330e 100644 --- a/packages/cli/test/index.test.ts +++ b/packages/cli/test/index.test.ts @@ -62,4 +62,18 @@ describe('--lang languages', () => { rmSync(dir, { recursive: true, force: true }) } }) + + test('doctor --lang reports the given languages instead of the detected ones', () => { + const dir = mkdtempSync(join(tmpdir(), 'pleaseai-cli-lang-')) + try { + writeFileSync(join(dir, 'package.json'), '{}') + const res = run(['doctor', '--lang', 'dart'], dir) + expect(res.status).toBe(0) + expect(res.stdout).toContain('Selected languages (--lang): dart') + expect(res.stdout).not.toContain('typescript') + } + finally { + rmSync(dir, { recursive: true, force: true }) + } + }) })