Skip to content

Commit d96bed1

Browse files
authored
feat(cli): add devw pull command (v0.4) (#42)
* feat(cli): add devw pull command (v0.4) Implement rule distribution via GitHub. Users can download official rules from the dev-workflows registry with devw pull <category>/<rule>. - Add pull command with --list, --force, --dry-run, --no-compile flags - Add Markdown to YAML converter for rule files - Add GitHub API helpers (raw content + contents API) - Add cache with 1h TTL for registry listing - Extend types with source field on Rule and pulled[] on ProjectConfig - Add pulled files check to doctor command - Add source indicator (pulled/manual) to list command - Create 6 official rules in rules/ directory (P0 + P1) - Add tests for converter, github, cache, and pull integration * chore(cli): move rules to content/rules and fix branch reference - Move rules/ directory to content/rules/ for consistency with content/blocks/ - Change BRANCH from 'feat/pull-command' to 'main' in github.ts - Update RAW_BASE and API_BASE URLs to point to content/rules/ - Update GitHub blob URL in generateYamlOutput - Add 69 edge-case tests across 6 new test files: converter.edge, github.edge, cache.edge, pull.edge, parser.edge, doctor.edge - Export validateInput, generateYamlOutput, updateConfig from pull.ts * docs: update WATCH_SPEC status to complete and add PULL_SPEC for v0.4.
1 parent c7a9b4f commit d96bed1

30 files changed

Lines changed: 2531 additions & 5 deletions

CLAUDE.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,8 @@ pnpm dev # dev mode
3131
- `docs/internal/CLI_SPEC_v0.2.md` → v0.2 specification (COMPLETE)
3232
- `docs/internal/CLI_SPEC_v0.2.1.md` → v0.2.1 UX polish specification (COMPLETE)
3333
- `docs/internal/DOCS_SPEC.md` → Mintlify documentation spec (COMPLETE)
34-
- `docs/internal/WATCH_SPEC.md` → v0.3 watch mode specification (ACTIVE — implement this)
34+
- `docs/internal/WATCH_SPEC.md` → v0.3 watch mode specification (COMPLETE)
35+
- `docs/internal/PULL_SPEC.md` → v0.4 Pull rules specification (IMPLEMENT THIS)
3536
- `docs/internal/DECISIONS.md` → accepted decisions (source of truth if conflict)
3637
- `docs/internal/` is gitignored — internal specs not published
3738

content/rules/README.md

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
# Official Rules
2+
3+
Rules for AI coding agents, distributed via `devw pull`.
4+
5+
## Available Rules
6+
7+
| Rule | Category | Description | Command |
8+
|------|----------|-------------|---------|
9+
| `typescript/strict` | TypeScript | Strict TypeScript conventions | `devw pull typescript/strict` |
10+
| `javascript/react` | JavaScript | React conventions and best practices | `devw pull javascript/react` |
11+
| `javascript/nextjs` | JavaScript | Next.js App Router patterns and RSC | `devw pull javascript/nextjs` |
12+
| `css/tailwind` | CSS | Utility-first Tailwind conventions | `devw pull css/tailwind` |
13+
| `testing/vitest` | Testing | Vitest testing patterns | `devw pull testing/vitest` |
14+
| `security/supabase-rls` | Security | Supabase RLS enforcement | `devw pull security/supabase-rls` |
15+
16+
## Usage
17+
18+
```bash
19+
# List all available rules
20+
devw pull --list
21+
22+
# Pull a specific rule
23+
devw pull typescript/strict
24+
25+
# Preview without writing
26+
devw pull typescript/strict --dry-run
27+
28+
# Force overwrite
29+
devw pull typescript/strict --force
30+
```
31+
32+
## Rule Format
33+
34+
Each rule file uses YAML frontmatter and Markdown bullets:
35+
36+
```markdown
37+
---
38+
name: rule-name
39+
description: "Short description"
40+
version: "0.1.0"
41+
scope: conventions
42+
tags: [tag1, tag2]
43+
---
44+
45+
## Section
46+
47+
- Rule text as a bullet.
48+
Continuation indented.
49+
```

content/rules/css/tailwind.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
---
2+
name: tailwind
3+
description: "Utility-first Tailwind CSS conventions and design tokens"
4+
version: "0.1.0"
5+
scope: conventions
6+
tags: [tailwind, css, styling]
7+
---
8+
9+
## Utilities
10+
11+
- Use Tailwind utility classes for all styling. Do not write
12+
custom CSS unless absolutely necessary (e.g. complex
13+
animations or third-party overrides).
14+
15+
- Avoid `@apply` in CSS files. Extract reusable patterns into
16+
React components instead of creating CSS abstractions.
17+
18+
- Keep className strings readable. Break long class lists across
19+
multiple lines and group related utilities together.
20+
21+
## Design Tokens
22+
23+
- Use Tailwind's design tokens (spacing, colors, typography)
24+
from the theme config. Avoid arbitrary values like
25+
`w-[137px]`; prefer the closest token.
26+
27+
- Extend the theme in `tailwind.config` for project-specific
28+
tokens. Do not hardcode colors or spacing outside the config.

content/rules/javascript/nextjs.md

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
---
2+
name: nextjs
3+
description: "Next.js App Router patterns and React Server Components"
4+
version: "0.1.0"
5+
scope: architecture
6+
tags: [nextjs, react, app-router, rsc]
7+
---
8+
9+
## Server Components
10+
11+
- Minimize `"use client"` directives. Default to Server Components.
12+
Only add `"use client"` when the component needs browser APIs,
13+
event handlers, or React hooks that require client state.
14+
15+
- Fetch data in Server Components or server actions, not in
16+
client components with `useEffect`. Use React Suspense for
17+
loading states.
18+
19+
- Keep Server Components free of side effects. Data fetching
20+
and rendering only; mutations belong in server actions.
21+
22+
## Routing
23+
24+
- Follow the App Router file conventions: `page.tsx`, `layout.tsx`,
25+
`loading.tsx`, `error.tsx`, `not-found.tsx`. Do not create custom
26+
routing abstractions.
27+
28+
- Use route groups `(group)` to organize routes without affecting
29+
the URL structure. Use parallel routes and intercepting routes
30+
when needed.
31+
32+
## Server Actions
33+
34+
- Prefer server actions for form submissions and data mutations.
35+
Define them with `"use server"` in a separate file or at the
36+
top of an async function.
37+
38+
- Validate all inputs in server actions. Never trust data coming
39+
from the client even in server-side code.

content/rules/javascript/react.md

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
---
2+
name: react
3+
description: "React conventions and best practices for AI coding agents"
4+
version: "0.1.0"
5+
scope: conventions
6+
tags: [react, frontend, components, hooks]
7+
---
8+
9+
## Components
10+
11+
- Always use named exports. Never use default exports.
12+
This applies to all files: components, utilities, hooks, and types.
13+
14+
- Use PascalCase for component names and their files
15+
(`UserProfile.tsx`). Use camelCase for hook files prefixed
16+
with `use` (`useAuth.ts`).
17+
18+
- Prefer composition over prop drilling. Use children,
19+
render props, or context for shared behavior rather than
20+
deeply nested prop chains.
21+
22+
- Colocate related files: component, hook, utils, and types
23+
in the same feature folder.
24+
25+
## Hooks
26+
27+
- Follow the Rules of Hooks: only call hooks at the top level,
28+
never inside conditions or loops. Custom hooks must start
29+
with `use`.
30+
31+
- Extract complex logic into custom hooks. A component should
32+
primarily handle rendering; business logic belongs in hooks.
33+
34+
- Use `useMemo` and `useCallback` only when there is a measured
35+
performance problem. Premature memoization adds complexity.
36+
37+
## Styling
38+
39+
- Avoid inline styles. Use CSS modules, Tailwind classes,
40+
or styled-components for styling.
41+
42+
- Keep className logic simple. Extract complex conditional
43+
classes into a helper or use a utility like `clsx`.
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
---
2+
name: supabase-rls
3+
description: "Supabase Row-Level Security enforcement and auth patterns"
4+
version: "0.1.0"
5+
scope: security
6+
tags: [supabase, rls, security, database]
7+
---
8+
9+
## RLS Policies
10+
11+
- Every new table must have RLS policies before merging.
12+
Enable RLS with `ALTER TABLE ... ENABLE ROW LEVEL SECURITY`
13+
and create at least one policy per operation (SELECT, INSERT,
14+
UPDATE, DELETE) as needed.
15+
16+
- Always use `auth.uid()` in RLS policies to scope data to
17+
the authenticated user. Never rely on client-provided
18+
user IDs in queries.
19+
20+
- Test RLS policies in isolation. Write SQL tests that verify
21+
access is denied for unauthorized users before merging.
22+
23+
## Auth Keys
24+
25+
- Never expose the `service_role` key to the client.
26+
Use the anon key in browser code and the `service_role` key
27+
only in server-side or admin contexts.
28+
29+
- Store Supabase keys in environment variables. Never hardcode
30+
keys in source files or commit them to version control.

content/rules/testing/vitest.md

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
---
2+
name: vitest
3+
description: "Vitest testing patterns and best practices"
4+
version: "0.1.0"
5+
scope: testing
6+
tags: [vitest, testing, unit-tests]
7+
---
8+
9+
## Test Structure
10+
11+
- Use descriptive test names that explain the expected behavior.
12+
Follow the pattern: `should [expected behavior] when [condition]`.
13+
14+
- Structure tests with Arrange-Act-Assert (AAA) pattern.
15+
Separate setup, execution, and verification into distinct
16+
sections for readability.
17+
18+
- Group related tests with `describe` blocks. One `describe`
19+
per function or feature under test.
20+
21+
## Assertions
22+
23+
- Test behavior, not implementation details. Avoid asserting
24+
on internal state, private methods, or specific function calls
25+
unless testing integration points.
26+
27+
- Prefer specific assertions (`toEqual`, `toContain`, `toThrow`)
28+
over generic ones (`toBeTruthy`). Specific assertions give
29+
better failure messages.
30+
31+
## Mocking
32+
33+
- Only mock at system boundaries: network requests, databases,
34+
file system, and third-party services. Do not mock internal
35+
modules or utility functions.
36+
37+
- Use `vi.fn()` for function spies and `vi.mock()` for module
38+
mocks. Restore all mocks after each test with `afterEach`.

content/rules/typescript/strict.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
---
2+
name: strict
3+
description: "Strict TypeScript conventions for professional codebases"
4+
version: "0.1.0"
5+
scope: conventions
6+
tags: [typescript, strict, types]
7+
---
8+
9+
## Type Safety
10+
11+
- Never use `any`. Use `unknown` when the type is truly unknown,
12+
then narrow with type guards.
13+
14+
- Always declare explicit return types on exported functions.
15+
Inferred types are fine for internal/private functions.
16+
17+
- Never use non-null assertion (`!`). Handle null/undefined explicitly
18+
with optional chaining, nullish coalescing, or type guards.
19+
20+
## Types and Enums
21+
22+
- Prefer union types over enums.
23+
Use `as const` objects when you need runtime values.
24+
25+
- Prefer `interface` for object shapes that may be extended.
26+
Use `type` for unions, intersections, and mapped types.
27+
28+
- Use `satisfies` to validate object literals against a type
29+
while preserving the narrowest inferred type.
30+
31+
## Generics
32+
33+
- Name generic parameters descriptively when the meaning
34+
is not obvious. Prefer `TItem` over `T` in complex signatures.
35+
36+
- Constrain generic parameters with `extends` to communicate
37+
the expected shape and catch misuse at compile time.

packages/cli/src/bridges/types.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,13 @@ export interface Rule {
66
tags?: string[];
77
enabled: boolean;
88
sourceBlock?: string;
9+
source?: string;
10+
}
11+
12+
export interface PulledEntry {
13+
path: string;
14+
version: string;
15+
pulled_at: string;
916
}
1017

1118
export interface ProjectConfig {
@@ -17,6 +24,7 @@ export interface ProjectConfig {
1724
tools: string[];
1825
mode: 'copy' | 'link';
1926
blocks: string[];
27+
pulled: PulledEntry[];
2028
}
2129

2230
export interface Bridge {

packages/cli/src/commands/doctor.ts

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { cursorBridge } from '../bridges/cursor.js';
99
import { geminiBridge } from '../bridges/gemini.js';
1010
import { windsurfBridge } from '../bridges/windsurf.js';
1111
import { copilotBridge } from '../bridges/copilot.js';
12-
import type { Bridge, ProjectConfig, Rule } from '../bridges/types.js';
12+
import type { Bridge, ProjectConfig, PulledEntry, Rule } from '../bridges/types.js';
1313
import { fileExists } from '../utils/fs.js';
1414
import { isValidScope } from '../core/schema.js';
1515
import * as ui from '../utils/ui.js';
@@ -184,6 +184,32 @@ export async function checkSymlinks(cwd: string, config: ProjectConfig): Promise
184184
return { passed: true, message: 'Symlinks are valid' };
185185
}
186186

187+
export async function checkPulledFilesExist(cwd: string, pulled: PulledEntry[]): Promise<CheckResult> {
188+
if (pulled.length === 0) {
189+
return { passed: true, message: 'Pulled files check skipped (no pulled rules)', skipped: true };
190+
}
191+
192+
const missing: string[] = [];
193+
194+
for (const entry of pulled) {
195+
const slug = entry.path.replace(/\//g, '-');
196+
const fileName = `pulled-${slug}.yml`;
197+
const filePath = join(cwd, '.dwf', 'rules', fileName);
198+
if (!(await fileExists(filePath))) {
199+
missing.push(fileName);
200+
}
201+
}
202+
203+
if (missing.length > 0) {
204+
return {
205+
passed: false,
206+
message: `Missing pulled rule files: ${missing.join(', ')}`,
207+
};
208+
}
209+
210+
return { passed: true, message: `Pulled rule files exist (${String(pulled.length)} entries)` };
211+
}
212+
187213
export async function checkHashSync(cwd: string, rules: Rule[]): Promise<CheckResult> {
188214
const storedHash = await readStoredHash(cwd);
189215
if (storedHash === null) {
@@ -273,7 +299,11 @@ async function runDoctor(): Promise<void> {
273299
const symlinkResult = await checkSymlinks(cwd, config!);
274300
results.push(symlinkResult);
275301

276-
// Check 8: Hash sync (conditional on compiled files existing)
302+
// Check 8: Pulled files exist
303+
const pulledResult = await checkPulledFilesExist(cwd, config!.pulled);
304+
results.push(pulledResult);
305+
306+
// Check 9: Hash sync (conditional on compiled files existing)
277307
const hashResult = await checkHashSync(cwd, rules);
278308
results.push(hashResult);
279309

0 commit comments

Comments
 (0)