Skip to content

Commit a20d99a

Browse files
committed
feat(cli): add @pleaseai/code-style CLI package
Ship a ultracite-style setup CLI that wires PleaseAI's shared code style into any project with one command (bunx @pleaseai/code-style). Inspired by @naverpay/code-style-cli and Hayden Bleasel's ultracite. What it does: - Auto-detects the user's package manager (bun → pnpm → yarn → npm) - Presents a @clack/prompts checkbox UI of PleaseAI style tools, marking already-installed ones - Installs the selected packages as devDependencies - Writes eslint.config.mjs, sets package.json#prettier, copies .editorconfig - Manages an idempotent marker block in AGENTS.md so AI coding assistants see PleaseAI style rules without being trapped by a full file rewrite Subcommands: - init (default): interactive setup - update: refresh only the AGENTS.md rules block - doctor: report current project config status Extras: - ko/en i18n with LC_ALL/LANG detection and --lang override - --yes/-y non-interactive mode for CI - rules.md shipped in the tarball as the canonical style reference - 35 unit tests covering PM detection, marker-block idempotency, package.json I/O and locale handling - Registered with release-please so the CLI versions independently The bin is exposed under both `pleaseai-code-style` and `please-style`.
1 parent a2a827a commit a20d99a

16 files changed

Lines changed: 1274 additions & 19 deletions

.release-please-manifest.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
{
22
"packages/eslint-config": "0.0.3",
3-
"packages/perttier-config": "0.0.1"
3+
"packages/perttier-config": "0.0.1",
4+
"packages/cli": "0.0.1"
45
}

bun.lock

Lines changed: 73 additions & 18 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/cli/README.md

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
# @pleaseai/code-style
2+
3+
CLI that wires PleaseAI's shared code style into any project — installs the
4+
eslint/prettier/editorconfig packages and manages the `AGENTS.md` rules block
5+
so AI coding assistants know how to write code that passes lint on the first
6+
try.
7+
8+
Inspired by [ultracite](https://github.com/haydenbleasel/ultracite) and
9+
NaverPay's [`@naverpay/code-style-cli`](https://github.com/NaverPayDev/code-style).
10+
11+
## Usage
12+
13+
From the root of any project that has a `package.json`:
14+
15+
```bash
16+
bunx @pleaseai/code-style # same as `init`
17+
bunx @pleaseai/code-style init # interactive setup
18+
bunx @pleaseai/code-style update # re-apply the AGENTS.md rules block only
19+
bunx @pleaseai/code-style doctor # check current project status
20+
```
21+
22+
Also works with `npx`, `pnpm dlx`, and `yarn dlx`.
23+
24+
## What it does
25+
26+
The `init` command:
27+
28+
1. Detects your package manager (bun → pnpm → yarn → npm, based on lockfile).
29+
2. Shows a checkbox UI listing PleaseAI code-style tools; already-installed
30+
ones are labelled `(installed)` / `(설치됨)`.
31+
3. Installs the packages you selected as dev dependencies.
32+
4. Writes / updates the corresponding config files.
33+
34+
### Supported tools
35+
36+
| Tool | npm package(s) | Config file |
37+
| --- | --- | --- |
38+
| eslint-config | `@pleaseai/eslint-config`, `eslint` | `eslint.config.mjs` |
39+
| prettier-config | `@pleaseai/prettier-config`, `prettier` | `package.json#prettier` |
40+
| editorconfig | `@pleaseai/editorconfig` | `.editorconfig` (copied from `node_modules`) |
41+
| agents-md || `AGENTS.md` (marker-managed block) |
42+
43+
## `AGENTS.md` block
44+
45+
The CLI owns only the content between these markers — everything else in
46+
`AGENTS.md` is your content and is preserved verbatim:
47+
48+
```md
49+
<!-- pleaseai-code-style:start -->
50+
...managed content...
51+
<!-- pleaseai-code-style:end -->
52+
```
53+
54+
Re-run `pleaseai-code-style update` any time you upgrade `@pleaseai/code-style`
55+
to refresh just this block. The full rules list is shipped as
56+
`node_modules/@pleaseai/code-style/rules.md` for reference.
57+
58+
## Options
59+
60+
| Flag | Description |
61+
| --- | --- |
62+
| `--yes`, `-y` | Accept defaults, overwrite existing files without prompting |
63+
| `--lang <ko\|en>` | Force the CLI locale (defaults to `$LANG`) |
64+
| `--help`, `-h` | Print help |
65+
| `--version`, `-v` | Print version |
66+
67+
## Localisation
68+
69+
The CLI auto-detects your locale from `LC_ALL` / `LANG` / `LC_MESSAGES` and
70+
currently ships Korean and English messages. Override with `--lang ko` or
71+
`--lang en`.
72+
73+
## License
74+
75+
MIT

packages/cli/package.json

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
{
2+
"name": "@pleaseai/code-style",
3+
"type": "module",
4+
"version": "0.0.1",
5+
"description": "CLI to set up PleaseAI code style (eslint, prettier, editorconfig, AGENTS.md) in any project",
6+
"author": "PleaseAI",
7+
"license": "MIT",
8+
"homepage": "https://github.com/pleaseai/code-style#readme",
9+
"repository": {
10+
"type": "git",
11+
"url": "https://github.com/pleaseai/code-style.git",
12+
"directory": "packages/cli"
13+
},
14+
"keywords": [
15+
"cli",
16+
"code-style",
17+
"eslint",
18+
"prettier",
19+
"editorconfig",
20+
"agents",
21+
"pleaseai"
22+
],
23+
"exports": {
24+
".": {
25+
"types": "./dist/index.d.mts",
26+
"import": "./dist/index.mjs"
27+
}
28+
},
29+
"main": "./dist/index.mjs",
30+
"types": "./dist/index.d.mts",
31+
"bin": {
32+
"pleaseai-code-style": "./dist/index.mjs",
33+
"please-style": "./dist/index.mjs"
34+
},
35+
"files": [
36+
"dist",
37+
"rules.md"
38+
],
39+
"engines": {
40+
"node": ">=22.0.0"
41+
},
42+
"scripts": {
43+
"build": "tsdown",
44+
"test": "bun test"
45+
},
46+
"dependencies": {
47+
"@clack/prompts": "^0.11.0"
48+
},
49+
"devDependencies": {
50+
"@types/bun": "^1.3.12",
51+
"@types/node": "^22.10.0",
52+
"tsdown": "^0.21.5",
53+
"typescript": "^6.0.2"
54+
}
55+
}

packages/cli/rules.md

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
# PleaseAI Code Style Rules
2+
3+
Canonical rules for `@pleaseai/code-style`. The `@pleaseai/eslint-config` package
4+
(based on `@antfu/eslint-config`) is the single source of truth — this document
5+
summarises the parts that an AI coding assistant needs to know _before_ writing
6+
code, so generated output passes lint on the first try.
7+
8+
## Formatting
9+
10+
- **No semicolons** at the end of statements.
11+
- **Single quotes** for strings; template literals are fine when interpolating.
12+
- **2-space indentation**, never tabs.
13+
- **Trailing commas** in multi-line arrays, objects, and parameter lists.
14+
- **LF** line endings.
15+
- Soft line length ~100, hard limit 120.
16+
17+
## Modules
18+
19+
- **ESM only** (`"type": "module"`). Never emit `require`/`module.exports`.
20+
- Use `import` / `export` statements.
21+
- Source extensions: `.ts`, `.tsx`, `.mjs`, `.mts`.
22+
- Import ordering (auto-fixable): node builtins → external → internal aliases →
23+
parent → sibling → index, with a blank line between groups and alphabetised
24+
within each group.
25+
- Prefer **named exports**. Reserve default exports for framework-required
26+
entry points (e.g. a Next.js page, a Vite plugin).
27+
28+
## TypeScript
29+
30+
- `target: ES2022`, `moduleResolution: bundler`.
31+
- `strict: true` — no implicit `any`, no unchecked indexed access hacks.
32+
- Prefer `type` aliases over `interface` unless `extends`/declaration merging
33+
is actually needed.
34+
- Use `const` by default; `let` only when reassignment is required; never `var`.
35+
- Avoid `any`; use `unknown` and narrow, or define a precise type.
36+
- Add explicit return types on exported functions and public class methods.
37+
- No unused imports or variables (auto-removed by `eslint --fix`).
38+
39+
## Code Quality
40+
41+
- Prefer **early returns** over deeply nested conditionals.
42+
- Use **optional chaining** (`?.`) and **nullish coalescing** (`??`).
43+
- Prefer `async`/`await` over raw `.then()` chains.
44+
- No `console.log` in production code paths — use a logger or remove before
45+
commit. `console.warn`/`console.error` are acceptable for CLI tooling.
46+
- Throw `Error` instances (or subclasses), never bare strings or objects.
47+
48+
## File Organisation
49+
50+
- One primary export per file when practical.
51+
- Target file size ≤ 500 lines; split when a file grows past that.
52+
- Colocate tests as `*.test.ts` next to the source they cover.
53+
- Avoid barrel re-exports (`index.ts` that just re-exports everything) for
54+
internal modules — they defeat tree-shaking.
55+
56+
## JSON & `package.json`
57+
58+
- `package.json` keys are sorted by `eslint-plugin-package-json`.
59+
- Declare `engines.node` (>= 22 for PleaseAI projects).
60+
- Libraries must ship an explicit `exports` map; no bare `main`-only fields.
61+
- `"type": "module"` is required.
62+
63+
## Commit Messages
64+
65+
- Conventional Commits: `feat:`, `fix:`, `docs:`, `refactor:`, `test:`,
66+
`chore:`, `build:`, `ci:`, `perf:`, `style:`, `revert:`.
67+
- Subject ≤ 72 chars, imperative mood, no trailing period.
68+
- Body explains **why**, not **what** — the diff already shows the what.

packages/cli/src/appliers.ts

Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
import type { Tool, ToolApplyResult, ToolContext } from './tools.js'
2+
import { copyFileSync, existsSync, writeFileSync } from 'node:fs'
3+
import { resolve as joinPath } from 'node:path'
4+
import { confirm, isCancel } from '@clack/prompts'
5+
import {
6+
readFileOrNull,
7+
readPackageJson,
8+
upsertMarkerBlock,
9+
writePackageJson,
10+
} from './fs-utils.js'
11+
import { t } from './i18n.js'
12+
13+
async function confirmOverwrite(file: string, auto: boolean): Promise<boolean> {
14+
if (auto) {
15+
return true
16+
}
17+
const answer = await confirm({
18+
message: t('overwritePrompt')(file),
19+
initialValue: false,
20+
})
21+
if (isCancel(answer)) {
22+
return false
23+
}
24+
return answer === true
25+
}
26+
27+
// --- eslint-config ----------------------------------------------------------
28+
29+
const ESLINT_CONFIG_TEMPLATE = `import pleaseai from '@pleaseai/eslint-config'
30+
31+
export default pleaseai()
32+
`
33+
34+
async function applyEslintConfig(ctx: ToolContext): Promise<ToolApplyResult> {
35+
const result: ToolApplyResult = { created: [], updated: [], skipped: [] }
36+
const target = joinPath(ctx.cwd, 'eslint.config.mjs')
37+
const label = 'eslint.config.mjs'
38+
39+
if (existsSync(target)) {
40+
const ok = await confirmOverwrite(label, ctx.autoAccept)
41+
if (!ok) {
42+
result.skipped.push(label)
43+
return result
44+
}
45+
writeFileSync(target, ESLINT_CONFIG_TEMPLATE)
46+
result.updated.push(label)
47+
return result
48+
}
49+
50+
writeFileSync(target, ESLINT_CONFIG_TEMPLATE)
51+
result.created.push(label)
52+
return result
53+
}
54+
55+
// --- prettier-config --------------------------------------------------------
56+
57+
async function applyPrettierConfig(ctx: ToolContext): Promise<ToolApplyResult> {
58+
const result: ToolApplyResult = { created: [], updated: [], skipped: [] }
59+
const pkg = readPackageJson(ctx.cwd)
60+
const label = 'package.json#prettier'
61+
62+
if (pkg.prettier === '@pleaseai/prettier-config') {
63+
// Already configured — nothing to do.
64+
return result
65+
}
66+
67+
if (pkg.prettier != null) {
68+
const ok = await confirmOverwrite(label, ctx.autoAccept)
69+
if (!ok) {
70+
result.skipped.push(label)
71+
return result
72+
}
73+
}
74+
75+
pkg.prettier = '@pleaseai/prettier-config'
76+
writePackageJson(pkg, ctx.cwd)
77+
result.updated.push(label)
78+
return result
79+
}
80+
81+
// --- editorconfig -----------------------------------------------------------
82+
83+
async function applyEditorConfig(ctx: ToolContext): Promise<ToolApplyResult> {
84+
const result: ToolApplyResult = { created: [], updated: [], skipped: [] }
85+
const target = joinPath(ctx.cwd, '.editorconfig')
86+
const source = joinPath(ctx.cwd, 'node_modules', '@pleaseai', 'editorconfig', '.editorconfig')
87+
const label = '.editorconfig'
88+
89+
if (!existsSync(source)) {
90+
// Package not installed yet (e.g. install step failed). Skip silently.
91+
result.skipped.push(label)
92+
return result
93+
}
94+
95+
if (existsSync(target)) {
96+
const ok = await confirmOverwrite(label, ctx.autoAccept)
97+
if (!ok) {
98+
result.skipped.push(label)
99+
return result
100+
}
101+
copyFileSync(source, target)
102+
result.updated.push(label)
103+
return result
104+
}
105+
106+
copyFileSync(source, target)
107+
result.created.push(label)
108+
return result
109+
}
110+
111+
// --- AGENTS.md --------------------------------------------------------------
112+
113+
const AGENTS_BODY = `# PleaseAI Code Style
114+
115+
These rules are managed by \`@pleaseai/code-style\`. Run \`pleaseai-code-style update\`
116+
to refresh this block. Do not edit between the marker comments — your changes
117+
will be overwritten.
118+
119+
- Formatter: \`@pleaseai/eslint-config\` (wraps \`@antfu/eslint-config\`)
120+
- No semicolons, single quotes, 2-space indent, trailing commas, LF line endings
121+
- ESM only — never emit \`require\`/\`module.exports\`
122+
- TypeScript: \`strict: true\`, prefer \`type\` over \`interface\`, no implicit \`any\`
123+
- Prefer named exports, early returns, \`async\`/\`await\`, optional chaining
124+
- File size target: ≤ 500 lines; colocate \`*.test.ts\` with the source
125+
- Conventional Commits for all commit messages
126+
127+
For the full rules (what an AI coding assistant needs to know before writing code),
128+
read \`node_modules/@pleaseai/code-style/rules.md\`.`
129+
130+
export async function applyAgentsMd(ctx: ToolContext): Promise<ToolApplyResult> {
131+
const result: ToolApplyResult = { created: [], updated: [], skipped: [] }
132+
const target = joinPath(ctx.cwd, 'AGENTS.md')
133+
const label = 'AGENTS.md'
134+
const existing = readFileOrNull(target)
135+
const next = upsertMarkerBlock(existing, AGENTS_BODY)
136+
137+
if (existing == null) {
138+
writeFileSync(target, next)
139+
result.created.push(label)
140+
return result
141+
}
142+
143+
if (existing === next) {
144+
// No-op — already up to date.
145+
return result
146+
}
147+
148+
writeFileSync(target, next)
149+
result.updated.push(label)
150+
return result
151+
}
152+
153+
// --- Registry ---------------------------------------------------------------
154+
155+
export const TOOLS: Tool[] = [
156+
{
157+
id: 'eslint-config',
158+
label: '@pleaseai/eslint-config (eslint.config.mjs)',
159+
packages: ['@pleaseai/eslint-config', 'eslint'],
160+
apply: applyEslintConfig,
161+
},
162+
{
163+
id: 'prettier-config',
164+
label: '@pleaseai/prettier-config (package.json#prettier)',
165+
packages: ['@pleaseai/prettier-config', 'prettier'],
166+
apply: applyPrettierConfig,
167+
},
168+
{
169+
id: 'editorconfig',
170+
label: '@pleaseai/editorconfig (.editorconfig)',
171+
packages: ['@pleaseai/editorconfig'],
172+
apply: applyEditorConfig,
173+
},
174+
{
175+
id: 'agents-md',
176+
label: 'AGENTS.md (AI coding rules block)',
177+
packages: [],
178+
apply: applyAgentsMd,
179+
},
180+
]
181+
182+
export function findTool(id: string): Tool | undefined {
183+
return TOOLS.find(t => t.id === id)
184+
}

0 commit comments

Comments
 (0)