@dennisrongo/skills 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +230 -0
  3. package/bin/claude-skills.js +189 -0
  4. package/lib/install.js +111 -0
  5. package/lib/list.js +63 -0
  6. package/lib/paths.js +39 -0
  7. package/lib/remove.js +52 -0
  8. package/lib/skills.js +121 -0
  9. package/package.json +48 -0
  10. package/skills/_template/SKILL.md +34 -0
  11. package/skills/conventional-commits/SKILL.md +136 -0
  12. package/skills/diagnose/SKILL.md +140 -0
  13. package/skills/dotnet-onion-api/SKILL.md +267 -0
  14. package/skills/dotnet-onion-api/references/anti-patterns.md +155 -0
  15. package/skills/dotnet-onion-api/references/solution-layout.md +113 -0
  16. package/skills/dotnet-onion-api/references/templates/appsettings.md +75 -0
  17. package/skills/dotnet-onion-api/references/templates/base-controller.cs.md +90 -0
  18. package/skills/dotnet-onion-api/references/templates/csproj-files.md +178 -0
  19. package/skills/dotnet-onion-api/references/templates/dbcontext.cs.md +149 -0
  20. package/skills/dotnet-onion-api/references/templates/exception-middleware.cs.md +101 -0
  21. package/skills/dotnet-onion-api/references/templates/feature-slice.md +349 -0
  22. package/skills/dotnet-onion-api/references/templates/program-cs.md +171 -0
  23. package/skills/dotnet-onion-api/references/templates/worker-program.cs.md +166 -0
  24. package/skills/grill-with-docs/SKILL.md +203 -0
  25. package/skills/handoff/SKILL.md +155 -0
  26. package/skills/improve-codebase-architecture/DEEPENING.md +37 -0
  27. package/skills/improve-codebase-architecture/INTERFACE-DESIGN.md +44 -0
  28. package/skills/improve-codebase-architecture/LANGUAGE.md +53 -0
  29. package/skills/improve-codebase-architecture/SKILL.md +121 -0
  30. package/skills/nextjs-app-router/SKILL.md +328 -0
  31. package/skills/nextjs-app-router/references/anti-patterns.md +203 -0
  32. package/skills/nextjs-app-router/references/folder-layout.md +151 -0
  33. package/skills/nextjs-app-router/references/good-patterns.md +212 -0
  34. package/skills/nextjs-app-router/references/templates/api-base.md +62 -0
  35. package/skills/nextjs-app-router/references/templates/api-slice.md +75 -0
  36. package/skills/nextjs-app-router/references/templates/auth-slice.md +95 -0
  37. package/skills/nextjs-app-router/references/templates/ci-and-hooks.md +94 -0
  38. package/skills/nextjs-app-router/references/templates/components-json.md +41 -0
  39. package/skills/nextjs-app-router/references/templates/env-and-utils.md +110 -0
  40. package/skills/nextjs-app-router/references/templates/eslint-prettier.md +82 -0
  41. package/skills/nextjs-app-router/references/templates/feature-slice.md +184 -0
  42. package/skills/nextjs-app-router/references/templates/form-with-zod.md +192 -0
  43. package/skills/nextjs-app-router/references/templates/middleware.md +88 -0
  44. package/skills/nextjs-app-router/references/templates/next-config.md +42 -0
  45. package/skills/nextjs-app-router/references/templates/package.md +99 -0
  46. package/skills/nextjs-app-router/references/templates/providers-and-store.md +79 -0
  47. package/skills/nextjs-app-router/references/templates/root-layout.md +146 -0
  48. package/skills/nextjs-app-router/references/templates/route-group-layouts.md +90 -0
  49. package/skills/nextjs-app-router/references/templates/tailwind-config.md +89 -0
  50. package/skills/nextjs-app-router/references/templates/testing.md +137 -0
  51. package/skills/nextjs-app-router/references/templates/tsconfig.md +40 -0
  52. package/skills/plan-and-build/SKILL.md +214 -0
  53. package/skills/pr-review/SKILL.md +132 -0
  54. package/skills/write-a-skill/SKILL.md +191 -0
@@ -0,0 +1,94 @@
1
+ # CI workflow + Husky + lint-staged
2
+
3
+ ## `.github/workflows/ci.yml`
4
+
5
+ ```yaml
6
+ name: ci
7
+
8
+ on:
9
+ push:
10
+ branches: [main]
11
+ pull_request:
12
+
13
+ concurrency:
14
+ group: ci-${{ github.workflow }}-${{ github.ref }}
15
+ cancel-in-progress: true
16
+
17
+ jobs:
18
+ build:
19
+ runs-on: ubuntu-latest
20
+ timeout-minutes: 15
21
+ steps:
22
+ - uses: actions/checkout@v4
23
+
24
+ - uses: pnpm/action-setup@v4
25
+ with:
26
+ version: 9
27
+
28
+ - uses: actions/setup-node@v4
29
+ with:
30
+ node-version: 20
31
+ cache: pnpm
32
+
33
+ - run: pnpm install --frozen-lockfile
34
+
35
+ - run: pnpm lint --max-warnings=0
36
+ - run: pnpm typecheck
37
+ - run: pnpm test
38
+ - run: pnpm build
39
+ env:
40
+ NEXT_PUBLIC_API_BASE_URL: http://localhost:5000
41
+
42
+ # Optional separate job for E2E — only run on push to main to save CI minutes.
43
+ e2e:
44
+ if: github.event_name == 'push'
45
+ runs-on: ubuntu-latest
46
+ timeout-minutes: 20
47
+ needs: build
48
+ steps:
49
+ - uses: actions/checkout@v4
50
+ - uses: pnpm/action-setup@v4
51
+ with: { version: 9 }
52
+ - uses: actions/setup-node@v4
53
+ with: { node-version: 20, cache: pnpm }
54
+ - run: pnpm install --frozen-lockfile
55
+ - run: pnpm exec playwright install --with-deps chromium
56
+ - run: pnpm test:e2e
57
+ env:
58
+ NEXT_PUBLIC_API_BASE_URL: http://localhost:5000
59
+ ```
60
+
61
+ **Notes:**
62
+ - `--frozen-lockfile` ensures the lockfile is honored — CI fails if `package.json` and lockfile drift.
63
+ - `--max-warnings=0` on lint means even warnings fail CI. On a fresh scaffold there are none; this catches regressions early.
64
+ - E2E is split into its own job behind `if: github.event_name == 'push'` so PRs stay fast.
65
+
66
+ ## Husky + lint-staged
67
+
68
+ After `pnpm install`, run:
69
+
70
+ ```bash
71
+ pnpm exec husky init
72
+ ```
73
+
74
+ Then write `.husky/pre-commit`:
75
+
76
+ ```sh
77
+ #!/usr/bin/env sh
78
+ . "$(dirname -- "$0")/_/husky.sh"
79
+
80
+ pnpm exec lint-staged
81
+ ```
82
+
83
+ The `lint-staged` config lives in `package.json` (see [`package.md`](package.md)):
84
+
85
+ ```jsonc
86
+ "lint-staged": {
87
+ "*.{ts,tsx}": ["prettier --write", "eslint --fix"],
88
+ "*.{json,md,css,yml,yaml}": ["prettier --write"]
89
+ }
90
+ ```
91
+
92
+ **Notes:**
93
+ - Do not put `tsc --noEmit` in lint-staged — it's per-staged-file and TypeScript is project-wide; let CI handle it.
94
+ - Do not skip hooks (`--no-verify`) as a workflow. If a hook is annoying, fix the underlying issue.
@@ -0,0 +1,41 @@
1
+ # `components.json` template (shadcn config)
2
+
3
+ ```jsonc
4
+ {
5
+ "$schema": "https://ui.shadcn.com/schema.json",
6
+ "style": "default",
7
+ "rsc": true,
8
+ "tsx": true,
9
+ "tailwind": {
10
+ "config": "tailwind.config.ts",
11
+ "css": "src/app/globals.css",
12
+ "baseColor": "neutral",
13
+ "cssVariables": true,
14
+ "prefix": ""
15
+ },
16
+ "aliases": {
17
+ "components": "@/components",
18
+ "utils": "@/lib/utils",
19
+ "ui": "@/components/ui",
20
+ "lib": "@/lib",
21
+ "hooks": "@/lib/hooks"
22
+ }
23
+ }
24
+ ```
25
+
26
+ **Initial components to generate** (only what the scaffolded templates actually import — add more on demand via `pnpm dlx shadcn@latest add <name>`):
27
+
28
+ - `button`
29
+ - `input`
30
+ - `label`
31
+ - `form` (the RHF-aware wrapper)
32
+ - `dialog`
33
+ - `dropdown-menu`
34
+ - `select`
35
+ - `popover`
36
+ - `calendar` (only if forms need a date picker)
37
+ - `toast` + `toaster` + `use-toast`
38
+ - `card`
39
+ - `separator`
40
+
41
+ **Do not** bulk-install every shadcn primitive at scaffold time. Each one is owned code that needs to be reviewed and maintained.
@@ -0,0 +1,110 @@
1
+ # Env validation, `cn()`, and shared utils
2
+
3
+ ## `src/config/env.ts` (runtime-validated env)
4
+
5
+ ```ts
6
+ import { z } from 'zod';
7
+
8
+ const envSchema = z.object({
9
+ NEXT_PUBLIC_API_BASE_URL: z.string().url(),
10
+ // Add more vars here as needed. Keep client-readable ones prefixed with NEXT_PUBLIC_.
11
+ });
12
+
13
+ function loadEnv() {
14
+ const parsed = envSchema.safeParse({
15
+ NEXT_PUBLIC_API_BASE_URL: process.env.NEXT_PUBLIC_API_BASE_URL,
16
+ });
17
+ if (!parsed.success) {
18
+ // Fail fast at module load.
19
+ console.error('Invalid environment configuration:', parsed.error.flatten().fieldErrors);
20
+ throw new Error('Invalid environment configuration. See .env.example for required variables.');
21
+ }
22
+ return parsed.data;
23
+ }
24
+
25
+ export const env = loadEnv();
26
+ ```
27
+
28
+ **Why:** misconfiguration (`NEXT_PUBLIC_API_BASE_URL` undefined, typo'd) blows up at startup with a clear error, not via a confusing 404 on the first network request.
29
+
30
+ ## `.env.example` (committed)
31
+
32
+ ```
33
+ # Backend API base URL. Required.
34
+ NEXT_PUBLIC_API_BASE_URL=http://localhost:5000
35
+
36
+ # Optional: real-time
37
+ # NEXT_PUBLIC_PUSHER_KEY=
38
+ # NEXT_PUBLIC_PUSHER_CLUSTER=
39
+
40
+ # Server-only: JWT verification key (only for Variant B middleware).
41
+ # JWT_VERIFY_KEY=
42
+ ```
43
+
44
+ `.env`, `.env.local`, `.env.*.local` go in `.gitignore`. Only `.env.example` is committed.
45
+
46
+ ## `src/lib/utils.ts`
47
+
48
+ ```ts
49
+ import { clsx, type ClassValue } from 'clsx';
50
+ import { twMerge } from 'tailwind-merge';
51
+
52
+ export function cn(...inputs: ClassValue[]): string {
53
+ return twMerge(clsx(inputs));
54
+ }
55
+ ```
56
+
57
+ That's the entire file. No bag of unrelated helpers — those go in `src/lib/formatters/` or feature-local utility files.
58
+
59
+ ## `src/lib/formatters/date.ts`
60
+
61
+ ```ts
62
+ import { format, formatDistanceToNow, parseISO } from 'date-fns';
63
+
64
+ export function formatDate(value: string | Date, pattern = 'PP'): string {
65
+ const d = typeof value === 'string' ? parseISO(value) : value;
66
+ return format(d, pattern);
67
+ }
68
+
69
+ export function formatRelative(value: string | Date): string {
70
+ const d = typeof value === 'string' ? parseISO(value) : value;
71
+ return formatDistanceToNow(d, { addSuffix: true });
72
+ }
73
+ ```
74
+
75
+ **No `moment`. Anywhere. Ever.** ESLint's `no-restricted-imports` blocks new imports of it.
76
+
77
+ ## `src/components/UnsavedChangesWarning.tsx`
78
+
79
+ ```tsx
80
+ 'use client';
81
+
82
+ import { useEffect } from 'react';
83
+
84
+ interface Props {
85
+ when: boolean;
86
+ message?: string;
87
+ }
88
+
89
+ export function UnsavedChangesWarning({ when, message = 'You have unsaved changes. Leave anyway?' }: Props) {
90
+ useEffect(() => {
91
+ if (!when) return;
92
+
93
+ const onBeforeUnload = (e: BeforeUnloadEvent) => {
94
+ e.preventDefault();
95
+ e.returnValue = message;
96
+ };
97
+
98
+ window.addEventListener('beforeunload', onBeforeUnload);
99
+ return () => window.removeEventListener('beforeunload', onBeforeUnload);
100
+ }, [when, message]);
101
+
102
+ // Next's App Router does not yet expose a stable router-events API for intercepting client-side navigation.
103
+ // The pattern below (overriding history.pushState) is a workaround — review and adjust per the Next version
104
+ // resolved at scaffold time (use context7 to check whether the official `useBeforeUnload` / `unstable_*` API has landed).
105
+
106
+ return null;
107
+ }
108
+ ```
109
+
110
+ **Adjust at scaffold time:** if the Next.js version resolved in Step 1 exposes a stable navigation-intercept hook, use it instead of the `beforeunload`-only fallback. Verify via context7.
@@ -0,0 +1,82 @@
1
+ # ESLint + Prettier templates
2
+
3
+ ## `.eslintrc.json`
4
+
5
+ ```jsonc
6
+ {
7
+ "root": true,
8
+ "extends": [
9
+ "next/core-web-vitals",
10
+ "next/typescript",
11
+ "plugin:@typescript-eslint/recommended",
12
+ "plugin:react-hooks/recommended",
13
+ "plugin:jsx-a11y/recommended",
14
+ "prettier"
15
+ ],
16
+ "plugins": ["@typescript-eslint", "react-hooks", "jsx-a11y"],
17
+ "rules": {
18
+ "no-console": ["warn", { "allow": ["warn", "error"] }],
19
+ "@typescript-eslint/no-explicit-any": "error",
20
+ "@typescript-eslint/no-unused-vars": ["error", { "argsIgnorePattern": "^_", "varsIgnorePattern": "^_" }],
21
+ "@typescript-eslint/consistent-type-imports": ["error", { "prefer": "type-imports" }],
22
+ "react-hooks/exhaustive-deps": "error",
23
+ "prefer-const": "error",
24
+ "eqeqeq": ["error", "always", { "null": "ignore" }],
25
+ "no-restricted-imports": [
26
+ "error",
27
+ {
28
+ "paths": [
29
+ { "name": "moment", "message": "Use date-fns. moment is in maintenance mode." },
30
+ { "name": "styled-components", "message": "This project uses Tailwind. Do not add CSS-in-JS." },
31
+ { "name": "lodash", "message": "Import the specific lodash function: 'lodash/debounce'. Or write a one-line helper." }
32
+ ]
33
+ }
34
+ ]
35
+ },
36
+ "overrides": [
37
+ {
38
+ "files": ["tests/**/*", "**/*.test.{ts,tsx}"],
39
+ "rules": {
40
+ "@typescript-eslint/no-explicit-any": "off",
41
+ "no-console": "off"
42
+ }
43
+ }
44
+ ]
45
+ }
46
+ ```
47
+
48
+ ## `.prettierrc`
49
+
50
+ ```jsonc
51
+ {
52
+ "singleQuote": true,
53
+ "semi": true,
54
+ "trailingComma": "all",
55
+ "printWidth": 100,
56
+ "tabWidth": 2,
57
+ "plugins": ["prettier-plugin-tailwindcss"]
58
+ }
59
+ ```
60
+
61
+ ## `.prettierignore`
62
+
63
+ ```
64
+ .next/
65
+ node_modules/
66
+ out/
67
+ dist/
68
+ build/
69
+ coverage/
70
+ playwright-report/
71
+ test-results/
72
+ *.lock
73
+ *.lockb
74
+ pnpm-lock.yaml
75
+ package-lock.json
76
+ yarn.lock
77
+ ```
78
+
79
+ **Notes:**
80
+ - `prettier-plugin-tailwindcss` sorts Tailwind class names automatically — keep class lists alphabetized without bikeshedding.
81
+ - The `no-restricted-imports` rule is the scaffolder's enforcement of the date library / styling system choices. Removing these rules requires a discussion, not a unilateral edit.
82
+ - Keep ESLint warnings *off* a fresh scaffold; warnings only show up after the project adds something that triggers them. CI runs `--max-warnings=0` so they fail loud.
@@ -0,0 +1,184 @@
1
+ # Feature slice template (Mode 2 — `add-feature`)
2
+
3
+ A feature is a route folder under a route group, with its own private `_components/` and `_hooks/`, a local Zod `schema.ts`, and (usually) one or more RTK Query endpoints injected into the appropriate domain slice.
4
+
5
+ ## Layout for feature `{{feature}}` under `(app)`
6
+
7
+ ```
8
+ src/app/(app)/{{feature}}/
9
+ ├── page.tsx # SERVER component (renders client child if interactive)
10
+ ├── loading.tsx
11
+ ├── error.tsx # optional but recommended
12
+ ├── schema.ts # Zod schemas owned by this feature
13
+ ├── _components/
14
+ │ ├── {{Feature}}Table.tsx # 'use client' if it has interactions
15
+ │ └── {{Feature}}Form.tsx # 'use client'
16
+ └── _hooks/
17
+ └── use{{Feature}}Filters.ts
18
+ ```
19
+
20
+ For a feature with sub-routes (list + detail + new):
21
+
22
+ ```
23
+ src/app/(app)/{{feature}}/
24
+ ├── page.tsx # list
25
+ ├── new/
26
+ │ └── page.tsx # SERVER renders <{{Feature}}Form mode="create" />
27
+ ├── [id]/
28
+ │ ├── page.tsx # SERVER renders <{{Feature}}Form mode="edit" id={id} />
29
+ │ ├── loading.tsx
30
+ │ └── _components/{{Feature}}DetailPanel.tsx
31
+ ├── schema.ts
32
+ ├── _components/
33
+ │ ├── {{Feature}}Table.tsx
34
+ │ └── {{Feature}}Form.tsx
35
+ └── _hooks/
36
+ ```
37
+
38
+ ## `page.tsx` for the list view (SERVER)
39
+
40
+ ```tsx
41
+ import { Suspense } from 'react';
42
+ import { {{Feature}}Table } from './_components/{{Feature}}Table';
43
+
44
+ export const metadata = { title: '{{Feature}}s' };
45
+
46
+ export default function {{Feature}}sPage() {
47
+ return (
48
+ <section className="space-y-4">
49
+ <header className="flex items-center justify-between">
50
+ <h1 className="text-2xl font-semibold">{{Feature}}s</h1>
51
+ </header>
52
+ <Suspense fallback={<div>Loading…</div>}>
53
+ <{{Feature}}Table />
54
+ </Suspense>
55
+ </section>
56
+ );
57
+ }
58
+ ```
59
+
60
+ ## `_components/{{Feature}}Table.tsx` (CLIENT)
61
+
62
+ ```tsx
63
+ 'use client';
64
+
65
+ import { use{{Feature}}sQuery } from '@/redux/api/{{feature}}sApi';
66
+
67
+ export function {{Feature}}sTable() {
68
+ const { data, isLoading, error } = use{{Feature}}sQuery();
69
+
70
+ if (isLoading) return <p className="text-muted-foreground">Loading…</p>;
71
+ if (error) return <p className="text-destructive">Failed to load.</p>;
72
+ if (!data?.length) return <p className="text-muted-foreground">No {{feature}}s yet.</p>;
73
+
74
+ return (
75
+ <table className="w-full text-left">
76
+ <thead>
77
+ <tr className="border-b">
78
+ <th className="py-2">Name</th>
79
+ <th className="py-2">Created</th>
80
+ </tr>
81
+ </thead>
82
+ <tbody>
83
+ {data.map((row) => (
84
+ <tr key={row.id} className="border-b">
85
+ <td className="py-2">{row.name}</td>
86
+ <td className="py-2 text-muted-foreground">{row.createdAt}</td>
87
+ </tr>
88
+ ))}
89
+ </tbody>
90
+ </table>
91
+ );
92
+ }
93
+ ```
94
+
95
+ ## `schema.ts`
96
+
97
+ ```ts
98
+ import { z } from 'zod';
99
+
100
+ export const {{feature}}Schema = z.object({
101
+ name: z.string().min(1).max(120),
102
+ email: z.string().email(),
103
+ });
104
+
105
+ export type {{Feature}}FormValues = z.infer<typeof {{feature}}Schema>;
106
+ ```
107
+
108
+ ## `_components/{{Feature}}Form.tsx` (CLIENT)
109
+
110
+ ```tsx
111
+ 'use client';
112
+
113
+ import { useRouter } from 'next/navigation';
114
+ import { useForm } from 'react-hook-form';
115
+ import { zodResolver } from '@hookform/resolvers/zod';
116
+ import { Form } from '@/components/ui/form';
117
+ import { Button } from '@/components/ui/button';
118
+ import { FormInputField } from '@/components/forms/FormInputField';
119
+ import { UnsavedChangesWarning } from '@/components/UnsavedChangesWarning';
120
+ import { useToast } from '@/components/ui/use-toast';
121
+ import { useCreate{{Feature}}Mutation } from '@/redux/api/{{feature}}sApi';
122
+ import { getDefaultValuesFromSchema, unwrapZodEffects } from '@/lib/zod-utils';
123
+ import { {{feature}}Schema, type {{Feature}}FormValues } from '../schema';
124
+
125
+ export function {{Feature}}Form() {
126
+ const router = useRouter();
127
+ const { toast } = useToast();
128
+ const [create, { isLoading }] = useCreate{{Feature}}Mutation();
129
+
130
+ const form = useForm<{{Feature}}FormValues>({
131
+ resolver: zodResolver({{feature}}Schema),
132
+ defaultValues: getDefaultValuesFromSchema(unwrapZodEffects({{feature}}Schema)) as {{Feature}}FormValues,
133
+ });
134
+
135
+ const onSubmit = async (values: {{Feature}}FormValues) => {
136
+ try {
137
+ const created = await create(values).unwrap();
138
+ toast({ title: '{{Feature}} created.' });
139
+ router.replace(`/app/{{feature}}s/${created.id}`);
140
+ } catch {
141
+ toast({ title: 'Could not save', variant: 'destructive' });
142
+ }
143
+ };
144
+
145
+ return (
146
+ <Form {...form}>
147
+ <UnsavedChangesWarning when={form.formState.isDirty && !form.formState.isSubmitting} />
148
+ <form onSubmit={form.handleSubmit(onSubmit)} className="max-w-lg space-y-4">
149
+ <FormInputField form={form} schema={{feature}}Schema} fieldName="name" label="Name" />
150
+ <FormInputField form={form} schema={{feature}}Schema} fieldName="email" label="Email" type="email" />
151
+ <div className="flex gap-2">
152
+ <Button type="submit" disabled={isLoading}>{isLoading ? 'Saving…' : 'Save'}</Button>
153
+ <Button type="button" variant="ghost" onClick={() => router.back()}>Cancel</Button>
154
+ </div>
155
+ </form>
156
+ </Form>
157
+ );
158
+ }
159
+ ```
160
+
161
+ ## Schema unit test — `tests/unit/{{feature}}.schema.test.ts`
162
+
163
+ ```ts
164
+ import { describe, expect, it } from 'vitest';
165
+ import { {{feature}}Schema } from '@/app/(app)/{{feature}}s/schema';
166
+
167
+ describe('{{feature}}Schema', () => {
168
+ it('accepts a valid payload', () => {
169
+ expect({{feature}}Schema.safeParse({ name: 'Acme', email: 'ops@acme.test' }).success).toBe(true);
170
+ });
171
+
172
+ it('rejects empty name', () => {
173
+ expect({{feature}}Schema.safeParse({ name: '', email: 'ops@acme.test' }).success).toBe(false);
174
+ });
175
+
176
+ it('rejects invalid email', () => {
177
+ expect({{feature}}Schema.safeParse({ name: 'Acme', email: 'nope' }).success).toBe(false);
178
+ });
179
+ });
180
+ ```
181
+
182
+ ## Endpoint additions
183
+
184
+ If the domain slice doesn't exist yet, run Mode 3 first (`add-api-slice`). Then add the feature's query/mutation pair to that slice — see [`api-slice.md`](api-slice.md).