Skip to content

Commit 93e3967

Browse files
authored
feat(cli): work without package.json and copy rule guides into .agents/code-style (#70)
* feat(cli): work without package.json and copy rule guides into .agents/code-style The CLI no longer requires package.json, so init, update, and doctor work in Gradle, pubspec, and Cargo-only projects. - copy the rule guides into .agents/code-style/ together with NOTICE.md and LICENSE-Apache-2.0.txt - link those copies from the AGENTS.md block for all projects - keep reading legacy node_modules/@pleaseai/code-style/rules/ links and migrate them in update - make doctor check that the linked guides exist - update both READMEs, rules.md, and the en/ko CLI docs Refs #65 * test(cli): mirror guides.ts and cover legacy and multi-language runs Move the guide sync tests into test/guides.test.ts, add e2e runs for update on a project carrying the v0.1.1 block and init on a pubspec.yaml + Cargo.toml project, and move the doctor --lang test out of the no-package.json group. Refs #65 * fix(cli): refuse symlinked guide paths and check linked guides in doctor - syncGuides validates .agents, .agents/code-style and every guide it writes or removes before changing anything, and stops on a symlink - doctor checks the guide paths the AGENTS.md block actually links, legacy node_modules links included - doctor skips the package-tool config files without a package.json - AGENTS.md pointers reuse guidePath - docs and usage say update refreshes the block and its guides Refs #65 * fix(cli): check guide paths before init installs and unlink removed guide symlinks - init checks the guide paths for symlinks before installing packages whenever the AGENTS.md block is selected, so a symlinked .agents stops it before anything is written - removing a deselected guide only unlinks it, so the symlink check now covers the written paths only, and dangling links are removed too Refs #65
1 parent b4d3274 commit 93e3967

14 files changed

Lines changed: 509 additions & 95 deletions

File tree

‎apps/docs/src/content/docs/en/cli.mdx‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ Inspired by [ultracite](https://github.com/haydenbleasel/ultracite) and NaverPay
1010

1111
## Quick Start
1212

13-
Run from the root of any project that has a `package.json`:
13+
Run from the root of your project. A `package.json` is optional: without one (Gradle, pubspec or Cargo-only projects) `init` offers only the AGENTS.md block and its guides, and `update` and `doctor` work as usual:
1414

1515
<CodeGroup titles={["bun", "pnpm", "npm"]}>
1616

@@ -48,7 +48,7 @@ pleaseai-code-style init --yes # non-interactive, accept defaults
4848

4949
### `update`
5050

51-
Refresh **only** the `AGENTS.md` rules block without touching packages or other config files. Use this after bumping `@pleaseai/code-style` to pull in the latest rules.
51+
Refresh the `AGENTS.md` rules block and the guides in `.agents/code-style/` without touching packages or other config files. Use this after bumping `@pleaseai/code-style` to pull in the latest rules.
5252

5353
```bash
5454
pleaseai-code-style update
@@ -122,9 +122,9 @@ These rules are managed by `@pleaseai/code-style`. Run
122122
My own content stays here untouched.
123123
```
124124

125-
The CLI **only** owns the content between the two marker comments — everything else in `AGENTS.md` is preserved verbatim. Running `update` twice in a row is a no-op; the block never duplicates.
125+
In `AGENTS.md`, the CLI **only** owns the content between the two marker comments — everything else is preserved verbatim. Running `update` twice in a row is a no-op; the block never duplicates.
126126

127-
The full canonical rules are shipped per language for AI assistants that prefer to read the long version: start with `node_modules/@pleaseai/code-style/rules/general.md` (every language), then the language guide listed in `node_modules/@pleaseai/code-style/rules.md` (for example, `node_modules/@pleaseai/code-style/rules/typescript.md` for TypeScript and JavaScript).
127+
The full canonical rules are copied into your project per language for AI assistants that prefer to read the long version, so they resolve without `node_modules`: start with `.agents/code-style/general.md` (every language), then the guide of your language (for example, `.agents/code-style/typescript.md` for TypeScript and JavaScript). `NOTICE.md` and `LICENSE-Apache-2.0.txt` in the same directory carry the attribution for the adapted guides. Commit `.agents/code-style/`: `init` and `update` rewrite it, overwrite its files and remove the guide of a language that is no longer selected (other files there are never touched). `doctor` checks that every guide the block links exists.
128128

129129
## Options
130130

‎apps/docs/src/content/docs/ko/cli.mdx‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ sidebar:
1010

1111
## 빠른 시작
1212

13-
`package.json`이 있는 프로젝트 루트에서 실행하세요:
13+
프로젝트 루트에서 실행하세요. `package.json`은 필수가 아닙니다. 없는 프로젝트(Gradle, pubspec, Cargo 전용)에서는 `init`이 AGENTS.md 블록과 가이드만 제공하며, `update`와 `doctor`는 그대로 동작합니다:
1414

1515
<CodeGroup titles={["bun", "pnpm", "npm"]}>
1616

@@ -48,7 +48,7 @@ pleaseai-code-style init --yes # 비대화형, 기본값 수락
4848

4949
### `update`
5050

51-
패키지나 다른 설정 파일을 건드리지 않고 `AGENTS.md` 규칙 블록 **만** 새로 고칩니다. `@pleaseai/code-style`을 업그레이드한 뒤 최신 규칙을 적용할 때 사용하세요.
51+
패키지나 다른 설정 파일을 건드리지 않고 `AGENTS.md` 규칙 블록과 `.agents/code-style/`의 가이드를 새로 고칩니다. `@pleaseai/code-style`을 업그레이드한 뒤 최신 규칙을 적용할 때 사용하세요.
5252

5353
```bash
5454
pleaseai-code-style update
@@ -122,9 +122,9 @@ CLI는 `AGENTS.md`에 마커로 구분된 블록을 작성하므로, 주변에
122122
내 콘텐츠는 그대로 유지됩니다.
123123
```
124124

125-
CLI는 두 마커 주석 사이의 내용 **만** 관리하며 — `AGENTS.md`의 나머지 내용은 그대로 보존됩니다. `update`를 연속으로 두 번 실행해도 no-op이며, 블록이 중복되지 않습니다.
125+
`AGENTS.md`에서 CLI는 두 마커 주석 사이의 내용 **만** 관리하며 — 나머지 내용은 그대로 보존됩니다. `update`를 연속으로 두 번 실행해도 no-op이며, 블록이 중복되지 않습니다.
126126

127-
전체 원본 규칙은 AI 어시스턴트가 긴 버전을 읽기 원할 때 참고할 수 있도록 언어별로 나뉘어 함께 배포됩니다. `node_modules/@pleaseai/code-style/rules/general.md`(모든 언어)를 먼저 읽고, `node_modules/@pleaseai/code-style/rules.md`에 나열된 언어별 가이드를 읽으세요(예: TypeScript·JavaScript는 `node_modules/@pleaseai/code-style/rules/typescript.md`).
127+
전체 원본 규칙은 AI 어시스턴트가 긴 버전을 읽기 원할 때 참고할 수 있도록 언어별로 나뉘어 프로젝트에 복사되므로 `node_modules` 없이도 읽을 수 있습니다. `.agents/code-style/general.md`(모든 언어)를 먼저 읽고, 사용하는 언어의 가이드를 읽으세요(예: TypeScript·JavaScript는 `.agents/code-style/typescript.md`). 같은 디렉터리의 `NOTICE.md`와 `LICENSE-Apache-2.0.txt`에 각색한 가이드의 출처 표기가 있습니다. `.agents/code-style/`은 커밋하세요. `init`과 `update`가 이 디렉터리를 다시 쓰며 파일을 덮어쓰고, 더 이상 선택하지 않은 언어의 가이드는 삭제합니다(디렉터리의 다른 파일은 건드리지 않습니다). `doctor`는 블록이 가리키는 가이드가 모두 있는지 확인합니다.
128128

129129
## 옵션
130130

‎packages/cli/README.ko.md‎

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,13 @@ eslint/prettier/editorconfig/ast-grep 패키지를 설치하고, AI 코딩 어
1212

1313
## 사용법
1414

15-
`package.json`이 있는 프로젝트 루트에서 실행하세요:
15+
프로젝트 루트에서 실행하세요. `package.json`은 필수가 아닙니다. 없는 프로젝트(Gradle,
16+
pubspec, Cargo 전용)에서는 `init`이 AGENTS.md 블록과 가이드만 제공합니다:
1617

1718
```bash
1819
bunx @pleaseai/code-style # `init`과 동일
1920
bunx @pleaseai/code-style init # 대화형 설정
20-
bunx @pleaseai/code-style update # AGENTS.md 규칙 블록만 다시 적용
21+
bunx @pleaseai/code-style update # AGENTS.md 규칙 블록과 가이드 다시 적용
2122
bunx @pleaseai/code-style doctor # 현재 프로젝트 상태 확인
2223
bunx @pleaseai/code-style check # 파일 이름과 테스트 경로 검사 (ADR-0022)
2324
```
@@ -200,7 +201,7 @@ bunx @pleaseai/code-style check --strict # 발견 사항이 하나라도 있
200201

201202
## `AGENTS.md` 블록
202203

203-
CLI는 아래 마커 사이의 내용만 관리합니다 — `AGENTS.md`의 나머지 내용은
204+
`AGENTS.md`에서 CLI는 아래 마커 사이의 내용만 관리합니다 — 나머지 내용은
204205
사용자의 것이며 그대로 보존됩니다:
205206

206207
```md
@@ -210,10 +211,14 @@ CLI는 아래 마커 사이의 내용만 관리합니다 — `AGENTS.md`의 나
210211
```
211212

212213
`@pleaseai/code-style`을 업그레이드한 뒤에는 `pleaseai-code-style update`를
213-
다시 실행하면 이 블록만 새로고침할 수 있습니다. 전체 규칙은 참고용으로
214-
언어별로 나뉘어 함께 배포됩니다. `node_modules/@pleaseai/code-style/rules/general.md`(모든 언어)를
215-
먼저 읽고, `node_modules/@pleaseai/code-style/rules.md`에 나열된 언어별 가이드를 읽으세요
216-
(예: TypeScript·JavaScript는 `node_modules/@pleaseai/code-style/rules/typescript.md`).
214+
다시 실행하면 이 블록과 블록이 가리키는 가이드를 새로고침할 수 있습니다. 전체 규칙은 언어별로 나뉘어 프로젝트에
215+
복사되므로 `node_modules` 없이도 어떤 프로젝트에서나 읽을 수 있습니다.
216+
`.agents/code-style/general.md`(모든 언어)를 먼저 읽고, 사용하는 언어의 가이드를 읽으세요
217+
(예: TypeScript·JavaScript는 `.agents/code-style/typescript.md`). 각색한 가이드의 출처 표기는
218+
`NOTICE.md`와 `LICENSE-Apache-2.0.txt`에 있습니다. `.agents/code-style/`은 커밋하세요.
219+
`init`과 `update`가 이 디렉터리를 다시 쓰며 파일을 덮어쓰고, 더 이상 선택하지 않은 언어의
220+
가이드는 삭제합니다(디렉터리의 다른 파일은 건드리지 않습니다). `doctor`는 블록이 가리키는
221+
가이드가 모두 있는지 확인합니다.
217222

218223
## 옵션
219224

‎packages/cli/README.md‎

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,13 @@ NaverPay's [`@naverpay/code-style-cli`](https://github.com/NaverPayDev/code-styl
1212

1313
## Usage
1414

15-
From the root of any project that has a `package.json`:
15+
From the root of your project. A `package.json` is optional: without one (Gradle,
16+
pubspec or Cargo-only projects) `init` offers only the AGENTS.md block and its guides:
1617

1718
```bash
1819
bunx @pleaseai/code-style # same as `init`
1920
bunx @pleaseai/code-style init # interactive setup
20-
bunx @pleaseai/code-style update # re-apply the AGENTS.md rules block only
21+
bunx @pleaseai/code-style update # re-apply the AGENTS.md rules block and its guides
2122
bunx @pleaseai/code-style doctor # check current project status
2223
bunx @pleaseai/code-style check # check file names and test paths (ADR-0022)
2324
```
@@ -382,8 +383,8 @@ does not catch lost tests.
382383

383384
## `AGENTS.md` block
384385

385-
The CLI owns only the content between these markers — everything else in
386-
`AGENTS.md` is your content and is preserved verbatim:
386+
In `AGENTS.md`, the CLI owns only the content between these markers — everything
387+
else is your content and is preserved verbatim:
387388

388389
```md
389390
<!-- pleaseai-code-style:start -->
@@ -392,12 +393,15 @@ The CLI owns only the content between these markers — everything else in
392393
```
393394

394395
Re-run `pleaseai-code-style update` any time you upgrade `@pleaseai/code-style`
395-
to refresh just this block. The full rules are shipped per language for
396-
reference: start with `node_modules/@pleaseai/code-style/rules/general.md`
397-
(every language), then the language guide listed in
398-
`node_modules/@pleaseai/code-style/rules.md` (for example
399-
`node_modules/@pleaseai/code-style/rules/typescript.md` for TypeScript and
400-
JavaScript).
396+
to refresh this block and the guides it links. The full rules are copied into your project, per
397+
language, so they resolve in every project (no `node_modules` needed): start
398+
with `.agents/code-style/general.md` (every language), then the guide of your
399+
language (for example `.agents/code-style/typescript.md` for TypeScript and
400+
JavaScript), with `NOTICE.md` and `LICENSE-Apache-2.0.txt` for the attribution
401+
of the adapted guides. Commit `.agents/code-style/`; `init` and `update`
402+
rewrite it, overwrite the files and remove the guide of a language that is no
403+
longer selected (other files in the directory are never touched). `doctor`
404+
checks that every guide the block links exists.
401405

402406
## Options
403407

‎packages/cli/rules.md‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ apply to every language, then the guide for the language you are writing:
1515
- [`rules/html-css.md`](./rules/html-css.md) — HTML and CSS
1616

1717
`pleaseai-code-style init` and `update` point your `AGENTS.md` block at the
18-
general guide plus the guide of each selected language. Languages are detected
18+
general guide plus the guide of each selected language, and copy those guides
19+
(with `NOTICE.md` and `LICENSE-Apache-2.0.txt`) into `.agents/code-style/` in
20+
your project, so the links work without `node_modules`. Commit that directory;
21+
`update` rewrites it. Languages are detected
1922
from marker files in the project root, or set with `--lang <list>`; `update`
2023
keeps the languages the existing block links unless `--lang` is given.

‎packages/cli/src/appliers.ts‎

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,9 @@ import {
1010
upsertMarkerBlock,
1111
writePackageJson,
1212
} from './fs-utils.js'
13+
import { guidePath, syncGuides } from './guides.js'
1314
import { t } from './i18n.js'
14-
import { detectLanguages, languagesFromAgentsBlock, RULES_PREFIX } from './languages.js'
15+
import { detectLanguages, languagesFromAgentsBlock } from './languages.js'
1516

1617
async function confirmOverwrite(file: string, auto: boolean): Promise<boolean> {
1718
if (auto) {
@@ -182,7 +183,7 @@ function wantsCodeOrganization(ctx: ToolContext): boolean {
182183
}
183184

184185
function agentsBody(ctx: ToolContext, languages: Language[]): string {
185-
const guides = ['general', ...languages].map(id => `\`${RULES_PREFIX}/${id}.md\``)
186+
const guides = ['general', ...languages].map(id => `\`${guidePath(id)}\``)
186187
const pointer = guides.length > 1
187188
? `${guides.slice(0, -1).join(', ')} and ${guides.at(-1)}`
188189
: guides[0]
@@ -200,6 +201,10 @@ export async function applyAgentsMd(ctx: ToolContext): Promise<ToolApplyResult>
200201
const languages = ctx.languages ?? languagesFromAgentsBlock(existing) ?? detectLanguages(ctx.cwd)
201202
const next = upsertMarkerBlock(existing, agentsBody(ctx, languages))
202203

204+
const guides = syncGuides(ctx.cwd, languages)
205+
result.created.push(...guides.created)
206+
result.updated.push(...guides.updated)
207+
203208
if (existing == null) {
204209
writeFileSync(target, next)
205210
result.created.push(label)

‎packages/cli/src/commands.ts‎

Lines changed: 44 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -10,22 +10,21 @@ import {
1010
detectPackageManager,
1111
installCommand,
1212
isPackageInstalled,
13+
readFileOrNull,
1314
readPackageJson,
1415
} from './fs-utils.js'
16+
import { assertGuidesWritable } from './guides.js'
1517
import { t } from './i18n.js'
16-
import { detectLanguages, LANGUAGES } from './languages.js'
18+
import { detectLanguages, guidePathsFromAgentsBlock, LANGUAGES } from './languages.js'
1719

1820
export interface CommandOptions {
1921
cwd: string
2022
autoAccept: boolean
2123
languages?: Language[]
2224
}
2325

24-
function assertProjectRoot(cwd: string): void {
25-
if (!existsSync(resolve(cwd, 'package.json'))) {
26-
log.error(t('noPackageJson'))
27-
process.exit(1)
28-
}
26+
function hasPackageJson(cwd: string): boolean {
27+
return existsSync(resolve(cwd, 'package.json'))
2928
}
3029

3130
function mergeResults(results: ToolApplyResult[]): ToolApplyResult {
@@ -53,27 +52,30 @@ function reportResults(result: ToolApplyResult): void {
5352
// --- init -------------------------------------------------------------------
5453

5554
export async function runInit(opts: CommandOptions): Promise<void> {
56-
assertProjectRoot(opts.cwd)
5755
intro(t('appTitle'))
5856

59-
const pm = detectPackageManager(opts.cwd)
60-
log.info(t('detectedPm')(pm))
61-
62-
const pkg = readPackageJson(opts.cwd)
57+
const withPackages = hasPackageJson(opts.cwd)
58+
const pm = withPackages ? detectPackageManager(opts.cwd) : undefined
59+
const pkg = withPackages ? readPackageJson(opts.cwd) : undefined
60+
if (pm != null) {
61+
log.info(t('detectedPm')(pm))
62+
}
6363

64-
const options = TOOLS.map((tool) => {
65-
const installed = tool.packages.length > 0
64+
const available = withPackages ? TOOLS : TOOLS.filter(tool => tool.packages.length === 0)
65+
const options = available.map((tool) => {
66+
const installed = pkg != null
67+
&& tool.packages.length > 0
6668
&& tool.packages.some(p => isPackageInstalled(pkg, p))
6769
return {
6870
value: tool.id,
6971
label: installed ? `${tool.label} ${t('alreadyInstalled')}` : tool.label,
7072
}
7173
})
7274

73-
const initialValues = opts.autoAccept ? TOOLS.map(tool => tool.id) : undefined
75+
const initialValues = opts.autoAccept ? available.map(tool => tool.id) : undefined
7476

7577
const selectedIdsRaw = opts.autoAccept
76-
? TOOLS.map(tool => tool.id)
78+
? available.map(tool => tool.id)
7779
: await multiselect({
7880
message: t('selectTools'),
7981
options,
@@ -94,7 +96,7 @@ export async function runInit(opts: CommandOptions): Promise<void> {
9496
}
9597

9698
const selectedTools: Tool[] = selectedIds
97-
.map(id => TOOLS.find(tool => tool.id === id))
99+
.map(id => available.find(tool => tool.id === id))
98100
.filter((tool): tool is Tool => tool != null)
99101

100102
let languages = opts.languages
@@ -117,13 +119,17 @@ export async function runInit(opts: CommandOptions): Promise<void> {
117119
languages = chosen
118120
}
119121
}
122+
if (selectedTools.some(tool => tool.id === 'agents-md')) {
123+
// Fail before installing anything rather than leave a half-configured project.
124+
assertGuidesWritable(opts.cwd, languages ?? [])
125+
}
120126

121127
// 1. Install packages (de-duped) for selected tools.
122128
const packagesToInstall = Array.from(
123129
new Set(selectedTools.flatMap(tool => tool.packages)),
124130
)
125131

126-
if (packagesToInstall.length > 0) {
132+
if (pm != null && packagesToInstall.length > 0) {
127133
const [cmd, args] = installCommand(pm, packagesToInstall)
128134
const spin = spinner()
129135
spin.start(t('installing'))
@@ -152,7 +158,6 @@ export async function runInit(opts: CommandOptions): Promise<void> {
152158
// --- update -----------------------------------------------------------------
153159

154160
export async function runUpdate(opts: CommandOptions): Promise<void> {
155-
assertProjectRoot(opts.cwd)
156161
intro(t('appTitle'))
157162
const result = await applyAgentsMd({ cwd: opts.cwd, autoAccept: true, selectedToolIds: [], languages: opts.languages })
158163
reportResults(result)
@@ -163,21 +168,21 @@ export async function runUpdate(opts: CommandOptions): Promise<void> {
163168
// --- doctor -----------------------------------------------------------------
164169

165170
export function runDoctor(opts: CommandOptions): void {
166-
assertProjectRoot(opts.cwd)
167-
const pkg = readPackageJson(opts.cwd)
168-
const pm = detectPackageManager(opts.cwd)
169-
170171
log.info(`${t('doctorHeading')}:`)
171-
log.info(` ${t('detectedPm')(pm)}`)
172172

173-
// Package-based tools only — file-only tools (agents-md) are reported below.
174-
for (const tool of TOOLS) {
175-
if (tool.packages.length === 0) {
176-
continue
173+
const packageBased = hasPackageJson(opts.cwd)
174+
if (packageBased) {
175+
const pkg = readPackageJson(opts.cwd)
176+
log.info(` ${t('detectedPm')(detectPackageManager(opts.cwd))}`)
177+
178+
for (const tool of TOOLS) {
179+
if (tool.packages.length === 0) {
180+
continue
181+
}
182+
const installed = tool.packages.every(p => isPackageInstalled(pkg, p))
183+
const suffix = installed ? t('doctorOk') : t('doctorMissing')
184+
log.info(` ${tool.label} — ${suffix}`)
177185
}
178-
const installed = tool.packages.every(p => isPackageInstalled(pkg, p))
179-
const suffix = installed ? t('doctorOk') : t('doctorMissing')
180-
log.info(` ${tool.label} — ${suffix}`)
181186
}
182187

183188
if (opts.languages != null) {
@@ -188,15 +193,17 @@ export function runDoctor(opts: CommandOptions): void {
188193
log.info(` ${languages.length > 0 ? t('doctorLanguages')(languages.join(', ')) : t('doctorNoLanguages')}`)
189194
}
190195

191-
// File presence checks.
192-
const files = [
193-
'eslint.config.mjs',
194-
'.editorconfig',
195-
'AGENTS.md',
196-
'sgconfig.yml',
197-
]
196+
// File presence checks. Without a package.json, init offers only the AGENTS.md block.
197+
const files = packageBased
198+
? ['eslint.config.mjs', '.editorconfig', 'AGENTS.md', 'sgconfig.yml']
199+
: ['AGENTS.md']
198200
for (const f of files) {
199201
const exists = existsSync(resolve(opts.cwd, f))
200202
log.info(` ${f} — ${exists ? t('doctorOk') : t('doctorMissing')}`)
201203
}
204+
205+
for (const guide of guidePathsFromAgentsBlock(readFileOrNull(resolve(opts.cwd, 'AGENTS.md'))) ?? []) {
206+
const exists = existsSync(resolve(opts.cwd, guide))
207+
log.info(` ${guide} — ${exists ? t('doctorOk') : t('doctorMissing')}`)
208+
}
202209
}

0 commit comments

Comments
 (0)