@tailor-platform/erp-kit 0.1.0 → 0.1.1

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 (61) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/LICENSE +21 -0
  3. package/dist/cli.js +64 -44
  4. package/package.json +4 -2
  5. package/skills/1-module-docs/SKILL.md +4 -0
  6. package/{rules/module-development → skills/1-module-docs/references}/structure.md +2 -7
  7. package/skills/2-module-feature-breakdown/SKILL.md +6 -0
  8. package/{rules/module-development → skills/2-module-feature-breakdown/references}/commands.md +0 -6
  9. package/{rules/module-development → skills/2-module-feature-breakdown/references}/models.md +0 -5
  10. package/skills/2-module-feature-breakdown/references/structure.md +22 -0
  11. package/skills/3-module-doc-review/SKILL.md +6 -0
  12. package/skills/3-module-doc-review/references/commands.md +54 -0
  13. package/skills/3-module-doc-review/references/models.md +29 -0
  14. package/{rules/module-development → skills/3-module-doc-review/references}/testing.md +0 -6
  15. package/skills/4-module-tdd-implementation/SKILL.md +24 -6
  16. package/skills/4-module-tdd-implementation/references/commands.md +45 -0
  17. package/{rules/sdk-best-practices → skills/4-module-tdd-implementation/references}/db-relations.md +0 -5
  18. package/{rules/module-development → skills/4-module-tdd-implementation/references}/errors.md +0 -5
  19. package/{rules/module-development → skills/4-module-tdd-implementation/references}/exports.md +0 -5
  20. package/skills/4-module-tdd-implementation/references/models.md +30 -0
  21. package/skills/4-module-tdd-implementation/references/structure.md +22 -0
  22. package/skills/4-module-tdd-implementation/references/testing.md +37 -0
  23. package/skills/5-module-implementation-review/SKILL.md +8 -0
  24. package/skills/5-module-implementation-review/references/commands.md +45 -0
  25. package/skills/5-module-implementation-review/references/errors.md +7 -0
  26. package/skills/5-module-implementation-review/references/exports.md +8 -0
  27. package/skills/5-module-implementation-review/references/models.md +30 -0
  28. package/skills/5-module-implementation-review/references/testing.md +29 -0
  29. package/skills/app-compose-1-requirement-analysis/SKILL.md +4 -0
  30. package/{rules/app-compose → skills/app-compose-1-requirement-analysis/references}/structure.md +0 -5
  31. package/skills/app-compose-2-requirements-breakdown/SKILL.md +7 -0
  32. package/{rules/app-compose/frontend → skills/app-compose-2-requirements-breakdown/references}/screen-detailview.md +0 -6
  33. package/{rules/app-compose/frontend → skills/app-compose-2-requirements-breakdown/references}/screen-form.md +0 -6
  34. package/{rules/app-compose/frontend → skills/app-compose-2-requirements-breakdown/references}/screen-listview.md +0 -6
  35. package/skills/app-compose-2-requirements-breakdown/references/structure.md +27 -0
  36. package/skills/app-compose-3-doc-review/SKILL.md +4 -0
  37. package/skills/app-compose-3-doc-review/references/structure.md +27 -0
  38. package/skills/app-compose-4-design-mock/SKILL.md +8 -0
  39. package/{rules/app-compose/frontend → skills/app-compose-4-design-mock/references}/component.md +0 -5
  40. package/skills/app-compose-4-design-mock/references/screen-detailview.md +106 -0
  41. package/skills/app-compose-4-design-mock/references/screen-form.md +139 -0
  42. package/skills/app-compose-4-design-mock/references/screen-listview.md +153 -0
  43. package/skills/app-compose-4-design-mock/references/structure.md +27 -0
  44. package/skills/app-compose-5-design-mock-review/SKILL.md +7 -0
  45. package/skills/app-compose-5-design-mock-review/references/component.md +50 -0
  46. package/skills/app-compose-5-design-mock-review/references/screen-detailview.md +106 -0
  47. package/skills/app-compose-5-design-mock-review/references/screen-form.md +139 -0
  48. package/skills/app-compose-5-design-mock-review/references/screen-listview.md +153 -0
  49. package/skills/app-compose-6-implementation-spec/SKILL.md +5 -0
  50. package/{rules/app-compose/backend → skills/app-compose-6-implementation-spec/references}/auth.md +0 -6
  51. package/skills/app-compose-6-implementation-spec/references/structure.md +27 -0
  52. package/src/cli.ts +2 -2
  53. package/src/commands/init.test.ts +30 -19
  54. package/src/commands/init.ts +76 -43
  55. package/rules/app-compose/frontend/auth.md +0 -55
  56. package/rules/app-compose/frontend/page.md +0 -86
  57. package/rules/module-development/cross-module-type-injection.md +0 -28
  58. package/rules/module-development/dependency-modules.md +0 -24
  59. package/rules/module-development/executors.md +0 -67
  60. package/rules/module-development/sync-vs-async-operations.md +0 -83
  61. package/rules/sdk-best-practices/sdk-docs.md +0 -14
@@ -0,0 +1,27 @@
1
+ # Application Directory Structure
2
+
3
+ ```
4
+ {app_name}/
5
+ ├── backend/
6
+ │ ├── src/
7
+ │ │ ├── modules.ts # Declaring module usage
8
+ │ │ ├── modules/
9
+ │ │ │ └── {module-name}/ # Module-specific directory
10
+ │ │ │ ├── resolvers/ # API Definition to expose graphql apis
11
+ │ │ │ └── executors/ # PubSub Automation (one file per declaration)
12
+ │ │ └── generated/ # Auto-generated code (do not edit)
13
+ │ └── tailor.config.ts # tailor application config
14
+
15
+ └── frontend/
16
+ └── src/
17
+ ├── pages/ # File-based routing (auto-discovered by Vite plugin)
18
+ │ └── {page-path}/
19
+ │ ├── page.tsx
20
+ │ └── {page-path}/
21
+ │ ├── components/
22
+ │ └── page.tsx
23
+ ├── components/
24
+ │ └── ui/ # Generic UI components
25
+ ├── graphql/ # gql.tada settings
26
+ └── providers/ # react providers
27
+ ```
@@ -281,3 +281,10 @@ Look for `iconFontName` property in sidebar menu items. Icons use Lucide icon fo
281
281
  ## Next Step
282
282
 
283
283
  After completing design review, use `/app-compose-6-implementation-spec` to create Tier 4 documentation (resolvers).
284
+
285
+ ## References
286
+
287
+ - [ListView screen](references/screen-listview.md)
288
+ - [Form screen](references/screen-form.md)
289
+ - [DetailView screen](references/screen-detailview.md)
290
+ - [Page components](references/component.md)
@@ -0,0 +1,50 @@
1
+ # Page Components
2
+
3
+ Page-specific components are placed in a `components/` directory, separated from page.tsx.
4
+
5
+ ```
6
+ {page-name}/
7
+ ├── components/
8
+ │ └── *.tsx
9
+ └── page.tsx
10
+ ```
11
+
12
+ ## Fragment Collocation
13
+
14
+ Components define and export their own GraphQL Fragment for the data they display. The parent page imports the Fragment and includes it in the query.
15
+
16
+ Use `graphql`, `FragmentOf`, and `readFragment` from `@/graphql`.
17
+
18
+ ```tsx
19
+ // components/user-card.tsx
20
+ import { graphql, type FragmentOf, readFragment } from "@/graphql";
21
+
22
+ export const UserCardFragment = graphql(`
23
+ fragment UserCard on User {
24
+ id
25
+ name
26
+ email
27
+ }
28
+ `);
29
+
30
+ export const UserCard = ({ user }: { user: FragmentOf<typeof UserCardFragment> }) => {
31
+ const data = readFragment(UserCardFragment, user);
32
+ return <div>{data.name}</div>;
33
+ };
34
+ ```
35
+
36
+ ```tsx
37
+ // page.tsx
38
+ import { UserCard, UserCardFragment } from "./components/user-card";
39
+
40
+ const UserQuery = graphql(
41
+ `
42
+ query User($id: ID!) {
43
+ user(id: $id) {
44
+ ...UserCard
45
+ }
46
+ }
47
+ `,
48
+ [UserCardFragment],
49
+ );
50
+ ```
@@ -0,0 +1,106 @@
1
+ # DetailView Screen Implementation
2
+
3
+ Implementation pattern for screens with `Screen Type: DetailView`.
4
+ Assumes `page.md` and `component.md` rules.
5
+
6
+ ## File Structure
7
+
8
+ ```
9
+ {screen-path}/[id]/
10
+ ├── components/
11
+ │ ├── {screen-name}-detail.tsx # Main content (left column)
12
+ │ └── {screen-name}-actions.tsx # Action sidebar (right column)
13
+ ├── edit/
14
+ │ ├── components/
15
+ │ │ └── edit-{screen-name}-form.tsx
16
+ │ └── page.tsx
17
+ └── page.tsx
18
+ ```
19
+
20
+ ## Layout
21
+
22
+ - Two-column layout: main content on the left, actions on the right.
23
+
24
+ ```tsx
25
+ const ResourcePage = () => {
26
+ const { id } = useParams();
27
+ const [{ data, error, fetching }, reexecuteQuery] = useQuery({
28
+ query: ResourceQuery,
29
+ variables: { id: id! },
30
+ });
31
+
32
+ if (fetching) return <Loading />;
33
+ if (error || !data?.resource) return <ErrorFallback ... />;
34
+
35
+ return (
36
+ <Layout columns={2} title="Resource Detail">
37
+ <Layout.Column>
38
+ <ResourceDetail resource={data.resource} />
39
+ </Layout.Column>
40
+ <Layout.Column>
41
+ <ResourceActions resource={data.resource} />
42
+ </Layout.Column>
43
+ </Layout>
44
+ );
45
+ };
46
+ ```
47
+
48
+ ## Left Column: Detail Component
49
+
50
+ Stack `DescriptionCard` and related tables vertically with `space-y-6`.
51
+
52
+ - `DescriptionCard` (`@tailor-platform/app-shell`): renders key-value fields declaratively.
53
+ - Complex content (tables, timelines): wrap in `<div className="rounded-lg border bg-card p-6">`.
54
+
55
+ ### DescriptionCard
56
+
57
+ ```tsx
58
+ <DescriptionCard
59
+ data={resource}
60
+ title="Overview"
61
+ columns={3}
62
+ fields={[
63
+ { key: "name", label: "Name", meta: { copyable: true } },
64
+ {
65
+ key: "status",
66
+ label: "Status",
67
+ type: "badge",
68
+ meta: { badgeVariantMap: { ACTIVE: "success", PENDING: "warning" } },
69
+ },
70
+ { type: "divider" },
71
+ {
72
+ key: "createdAt",
73
+ label: "Created At",
74
+ type: "date",
75
+ meta: { dateFormat: "medium" },
76
+ },
77
+ ]}
78
+ />
79
+ ```
80
+
81
+ Field types: `"text"` (default), `"badge"`, `"money"`, `"date"`, `"link"`, `"address"`, `"reference"`, `"divider"`
82
+
83
+ ## Right Column: Actions Component
84
+
85
+ Wrap in a `Card` component. Use `Button variant="ghost"` for each action item.
86
+
87
+ ```tsx
88
+ <Card>
89
+ <CardHeader>
90
+ <CardTitle>Actions</CardTitle>
91
+ </CardHeader>
92
+ <CardContent className="space-y-2">
93
+ <Button variant="ghost" className="w-full justify-start gap-2" asChild>
94
+ <Link to="edit">✎ Edit</Link>
95
+ </Button>
96
+ <Button variant="ghost" className="w-full justify-start gap-2" onClick={handler}>
97
+ ✓ Approve
98
+ </Button>
99
+ </CardContent>
100
+ </Card>
101
+ ```
102
+
103
+ - Navigation: `<Button variant="ghost" asChild><Link to="...">`
104
+ - Mutation: `<Button variant="ghost" onClick={handler}>` with custom resolvers (see `backend/resolvers.md`)
105
+ - Conditional: show/hide based on status
106
+ - Multiple cards: stack with `<div className="space-y-6">`
@@ -0,0 +1,139 @@
1
+ # Form Screen Implementation
2
+
3
+ Implementation pattern for screens with `Screen Type: Form`.
4
+ Assumes `page.md` and `component.md` rules.
5
+
6
+ ## File Structure
7
+
8
+ ```
9
+ {screen-path}/
10
+ ├── components/
11
+ │ └── {screen-name}-form.tsx # Form component with validation
12
+ └── page.tsx
13
+ ```
14
+
15
+ ## Page Component (page.tsx)
16
+
17
+ Form pages delegate mutation logic to the form component.
18
+
19
+ ```tsx
20
+ const ScreenNamePage = () => (
21
+ <Layout columns={1} title="Screen Title">
22
+ <Layout.Column>
23
+ <ScreenNameForm />
24
+ </Layout.Column>
25
+ </Layout>
26
+ );
27
+ ```
28
+
29
+ For edit forms that need existing data, co-locate data fetching in the page component:
30
+
31
+ ```tsx
32
+ const EditPage = () => {
33
+ const { id } = useParams();
34
+ const [{ data, error, fetching }] = useQuery({
35
+ query: ResourceQuery,
36
+ variables: { id: id! },
37
+ });
38
+
39
+ if (fetching) return <Loading />;
40
+ if (error || !data?.resource) return <ErrorFallback ... />;
41
+
42
+ return (
43
+ <Layout columns={1} title="Edit Resource">
44
+ <Layout.Column>
45
+ <EditResourceForm resource={data.resource} />
46
+ </Layout.Column>
47
+ </Layout>
48
+ );
49
+ };
50
+ ```
51
+
52
+ ## Form Component (components/{screen-name}-form.tsx)
53
+
54
+ ### Technology Stack
55
+
56
+ - `react-hook-form` — form state management
57
+ - `zod` + `@hookform/resolvers/zod` — validation
58
+ - `useMutation` (urql) — GraphQL mutation
59
+ - `useNavigate` (@tailor-platform/app-shell) — post-submit navigation
60
+
61
+ ### Pattern
62
+
63
+ ```tsx
64
+ const formSchema = z.object({
65
+ title: z.string().min(1, "Title is required"),
66
+ description: z.string().optional(),
67
+ });
68
+
69
+ type FormValues = z.infer<typeof formSchema>;
70
+
71
+ export const ScreenNameForm = () => {
72
+ const navigate = useNavigate();
73
+ const [, createResource] = useMutation(CreateMutation);
74
+
75
+ const form = useForm<FormValues>({
76
+ resolver: zodResolver(formSchema),
77
+ defaultValues: { title: "", description: "" },
78
+ });
79
+
80
+ const onSubmit = (values: FormValues) => {
81
+ void createResource({ input: values }).then((result) => {
82
+ if (!result.error) {
83
+ void navigate("..");
84
+ }
85
+ });
86
+ };
87
+
88
+ return (
89
+ <Form {...form}>
90
+ <form onSubmit={(e) => void form.handleSubmit(onSubmit)(e)} className="max-w-md space-y-4">
91
+ <FormField
92
+ control={form.control}
93
+ name="title"
94
+ render={({ field }) => (
95
+ <FormItem>
96
+ <FormLabel>Title</FormLabel>
97
+ <FormControl>
98
+ <Input placeholder="Enter title" {...field} />
99
+ </FormControl>
100
+ <FormMessage />
101
+ </FormItem>
102
+ )}
103
+ />
104
+ <div className="flex gap-2">
105
+ <Button type="submit">Create</Button>
106
+ <Button type="button" variant="outline" onClick={() => void navigate("..")}>
107
+ Cancel
108
+ </Button>
109
+ </div>
110
+ </form>
111
+ </Form>
112
+ );
113
+ };
114
+ ```
115
+
116
+ ## Field Type Mapping
117
+
118
+ | Field Type | Component | Zod Schema |
119
+ | ---------- | ------------------------------ | ------------------------------- |
120
+ | Text | `<Input />` | `z.string()` |
121
+ | Textarea | `<textarea className="..." />` | `z.string()` |
122
+ | Dropdown | `<Select />` | `z.string()` or `z.enum([...])` |
123
+ | Date | `<Input type="date" />` | `z.string()` (ISO format) |
124
+ | Number | `<Input type="number" />` | `z.coerce.number()` |
125
+ | Email | `<Input type="email" />` | `z.string().email()` |
126
+ | Checkbox | `<Checkbox />` | `z.boolean()` |
127
+ | Radio | `<RadioGroup />` | `z.enum([...])` |
128
+
129
+ ## Validation Mapping
130
+
131
+ - **Required: Yes** → `.min(1, "Field is required")` (string) / `.positive()` (number)
132
+ - **Required: No** → `.optional()`
133
+
134
+ ## Key Points
135
+
136
+ - Set `defaultValues` for all fields (empty string, false, etc.)
137
+ - Navigate to `".."` after successful mutation
138
+ - Cancel button must use `type="button"` to prevent form submit
139
+ - For edit forms, accept fragment data as props and pre-fill `defaultValues`
@@ -0,0 +1,153 @@
1
+ # ListView Screen Implementation
2
+
3
+ Implementation pattern for screens with `Screen Type: ListView`.
4
+ Assumes `page.md` and `component.md` rules.
5
+
6
+ ## File Structure
7
+
8
+ ```
9
+ {screen-path}/
10
+ ├── components/
11
+ │ └── {screen-name}-table.tsx # Table component with fragments
12
+ └── page.tsx
13
+ ```
14
+
15
+ ## Page Component (page.tsx)
16
+
17
+ Data fetching and `Layout` must be co-located in the same page component.
18
+ Do NOT split into an inner Content component — `Layout` requires `Layout.Column` as direct children.
19
+
20
+ ```tsx
21
+ const ResourcesQuery = graphql(
22
+ `
23
+ query Resources {
24
+ resources {
25
+ ...ResourceTable
26
+ }
27
+ }
28
+ `,
29
+ [ResourceTableFragment],
30
+ );
31
+
32
+ const ResourcesPage = () => {
33
+ const [{ data, error, fetching }, reexecuteQuery] = useQuery({
34
+ query: ResourcesQuery,
35
+ });
36
+
37
+ if (fetching) return <Loading />;
38
+
39
+ if (error || !data) {
40
+ return (
41
+ <ErrorFallback
42
+ title="Failed to load resources"
43
+ message="An error occurred while fetching the list."
44
+ onReset={() => reexecuteQuery({ requestPolicy: "network-only" })}
45
+ />
46
+ );
47
+ }
48
+
49
+ return (
50
+ <Layout
51
+ columns={1}
52
+ title="Resources"
53
+ actions={[
54
+ <Button key="create" asChild>
55
+ <Link to="create">Create</Link>
56
+ </Button>,
57
+ ]}
58
+ >
59
+ <Layout.Column>
60
+ <ResourceTable data={data.resources} />
61
+ </Layout.Column>
62
+ </Layout>
63
+ );
64
+ };
65
+ ```
66
+
67
+ ## Table Component (components/{screen-name}-table.tsx)
68
+
69
+ ### Fragment Collocation
70
+
71
+ Define a row fragment and a table fragment wrapping the Connection type.
72
+
73
+ ```tsx
74
+ const ResourceRowFragment = graphql(`
75
+ fragment ResourceRow on Resource {
76
+ id
77
+ title
78
+ status
79
+ createdAt
80
+ }
81
+ `);
82
+
83
+ export const ResourceTableFragment = graphql(
84
+ `
85
+ fragment ResourceTable on ResourceConnection {
86
+ edges {
87
+ node {
88
+ ...ResourceRow
89
+ }
90
+ }
91
+ }
92
+ `,
93
+ [ResourceRowFragment],
94
+ );
95
+ ```
96
+
97
+ ### Row Component
98
+
99
+ ```tsx
100
+ const ResourceRow = ({ resource: resourceFragment }: ResourceRowProps) => {
101
+ const resource = readFragment(ResourceRowFragment, resourceFragment);
102
+ return (
103
+ <TableRow>
104
+ <TableCell>{resource.title}</TableCell>
105
+ <TableCell>
106
+ <Badge variant={resource.status === "ACTIVE" ? "default" : "secondary"}>
107
+ {resource.status}
108
+ </Badge>
109
+ </TableCell>
110
+ <TableCell>
111
+ <Button variant="ghost" size="sm" asChild>
112
+ <Link to={resource.id}>View</Link>
113
+ </Button>
114
+ </TableCell>
115
+ </TableRow>
116
+ );
117
+ };
118
+ ```
119
+
120
+ ### Empty State
121
+
122
+ ```tsx
123
+ if (connection.edges.length === 0) {
124
+ return (
125
+ <EmptyState
126
+ title="No resources"
127
+ message="Get started by creating a new resource."
128
+ action={
129
+ <Button asChild>
130
+ <Link to="create">Create</Link>
131
+ </Button>
132
+ }
133
+ />
134
+ );
135
+ }
136
+ ```
137
+
138
+ ## Column Property Mapping
139
+
140
+ | Property | Implementation |
141
+ | --------------- | ----------------------------------------------------------- |
142
+ | Hideable: Yes | Column visibility state to toggle show/hide |
143
+ | Sortable: Yes | Sort icon on `<TableHead>` + onClick to toggle query vars |
144
+ | Filterable: Yes | Filter UI above table (`<Select />`) |
145
+ | Searchable: Yes | Search input above table (`<Input placeholder="Search" />`) |
146
+
147
+ ## Key Points
148
+
149
+ - Use `<Badge />` for status columns
150
+ - Format dates with `toLocaleDateString()`
151
+ - Use `<EmptyState />` with Create action for empty lists
152
+ - Add a View button per row to navigate to the detail page
153
+ - Iterate data using Connection type `edges.node` pattern
@@ -120,3 +120,8 @@ After Tier 4, the application specification is complete:
120
120
  - **Tier 2**: Actors and business workflows
121
121
  - **Tier 3**: User stories and screens
122
122
  - **Tier 4**: Implementation specifications
123
+
124
+ ## References
125
+
126
+ - [Application structure](references/structure.md)
127
+ - [Backend auth](references/auth.md)
@@ -1,9 +1,3 @@
1
- ---
2
- paths:
3
- - "examples/**/backend/tailor.config.ts"
4
- - "examples/**/backend/.env"
5
- ---
6
-
7
1
  # Backend Auth Configuration
8
2
 
9
3
  Use Tailor SDK auth resources as the default pattern for backend authentication setup.
@@ -0,0 +1,27 @@
1
+ # Application Directory Structure
2
+
3
+ ```
4
+ {app_name}/
5
+ ├── backend/
6
+ │ ├── src/
7
+ │ │ ├── modules.ts # Declaring module usage
8
+ │ │ ├── modules/
9
+ │ │ │ └── {module-name}/ # Module-specific directory
10
+ │ │ │ ├── resolvers/ # API Definition to expose graphql apis
11
+ │ │ │ └── executors/ # PubSub Automation (one file per declaration)
12
+ │ │ └── generated/ # Auto-generated code (do not edit)
13
+ │ └── tailor.config.ts # tailor application config
14
+
15
+ └── frontend/
16
+ └── src/
17
+ ├── pages/ # File-based routing (auto-discovered by Vite plugin)
18
+ │ └── {page-path}/
19
+ │ ├── page.tsx
20
+ │ └── {page-path}/
21
+ │ ├── components/
22
+ │ └── page.tsx
23
+ ├── components/
24
+ │ └── ui/ # Generic UI components
25
+ ├── graphql/ # gql.tada settings
26
+ └── providers/ # react providers
27
+ ```
package/src/cli.ts CHANGED
@@ -92,11 +92,11 @@ const scaffoldCommand = defineCommand({
92
92
 
93
93
  const initCommand = defineCommand({
94
94
  name: "init",
95
- description: "Set up consumer repo (skills, rules)",
95
+ description: "Set up consumer repo with framework skills",
96
96
  args: z.object({
97
97
  force: arg(z.boolean().default(false), {
98
98
  alias: "f",
99
- description: "Overwrite existing skills and rules",
99
+ description: "Overwrite existing skills",
100
100
  }),
101
101
  }),
102
102
  run: (args) => {
@@ -1,6 +1,6 @@
1
1
  import fs from "node:fs";
2
- import path from "node:path";
3
2
  import os from "node:os";
3
+ import path from "node:path";
4
4
  import { describe, it, expect, beforeEach, afterEach } from "vitest";
5
5
  import { runInit } from "./init.js";
6
6
 
@@ -21,6 +21,22 @@ describe("runInit", () => {
21
21
  expect(fs.existsSync(skillPath)).toBe(true);
22
22
  });
23
23
 
24
+ it("copies skill references/ directories", () => {
25
+ runInit(tmpDir, false);
26
+ const refPath = path.join(
27
+ tmpDir,
28
+ ".agents",
29
+ "skills",
30
+ "4-module-tdd-implementation",
31
+ "references",
32
+ "models.md",
33
+ );
34
+ expect(fs.existsSync(refPath)).toBe(true);
35
+
36
+ const content = fs.readFileSync(refPath, "utf-8");
37
+ expect(content).toContain("# Database Models");
38
+ });
39
+
24
40
  it("does not overwrite project-specific skills", () => {
25
41
  const customSkillDir = path.join(tmpDir, ".agents", "skills", "my-custom-skill");
26
42
  fs.mkdirSync(customSkillDir, { recursive: true });
@@ -48,30 +64,25 @@ describe("runInit", () => {
48
64
  expect(content).not.toBe("# Customized");
49
65
  });
50
66
 
51
- it("copies framework rules to .agents/rules/", () => {
67
+ it("creates .claude/skills symlink to .agents/skills", () => {
52
68
  runInit(tmpDir, false);
53
- const rulePath = path.join(tmpDir, ".agents", "rules", "module-development", "structure.md");
54
- expect(fs.existsSync(rulePath)).toBe(true);
55
- // Also check nested subdirectory
56
- const nestedRule = path.join(tmpDir, ".agents", "rules", "app-compose", "frontend", "auth.md");
57
- expect(fs.existsSync(nestedRule)).toBe(true);
69
+ const claudeSkills = path.join(tmpDir, ".claude", "skills");
70
+ expect(fs.lstatSync(claudeSkills).isSymbolicLink()).toBe(true);
71
+ expect(fs.readlinkSync(claudeSkills)).toBe(path.join("..", ".agents", "skills"));
58
72
  });
59
73
 
60
- it("does not overwrite existing rules", () => {
61
- const ruleDir = path.join(tmpDir, ".agents", "rules", "module-development");
62
- fs.mkdirSync(ruleDir, { recursive: true });
63
- fs.writeFileSync(path.join(ruleDir, "structure.md"), "# Custom Rule");
74
+ it("skips symlink when .claude/skills is a real directory", () => {
75
+ const claudeSkills = path.join(tmpDir, ".claude", "skills");
76
+ fs.mkdirSync(claudeSkills, { recursive: true });
64
77
  runInit(tmpDir, false);
65
- const content = fs.readFileSync(path.join(ruleDir, "structure.md"), "utf-8");
66
- expect(content).toBe("# Custom Rule");
78
+ expect(fs.lstatSync(claudeSkills).isSymbolicLink()).toBe(false);
67
79
  });
68
80
 
69
- it("overwrites existing rules with --force", () => {
70
- const ruleDir = path.join(tmpDir, ".agents", "rules", "module-development");
71
- fs.mkdirSync(ruleDir, { recursive: true });
72
- fs.writeFileSync(path.join(ruleDir, "structure.md"), "# Custom Rule");
81
+ it("relinks symlink with --force when target differs", () => {
82
+ const claudeSkills = path.join(tmpDir, ".claude", "skills");
83
+ fs.mkdirSync(path.dirname(claudeSkills), { recursive: true });
84
+ fs.symlinkSync("../old-target", claudeSkills);
73
85
  runInit(tmpDir, true);
74
- const content = fs.readFileSync(path.join(ruleDir, "structure.md"), "utf-8");
75
- expect(content).not.toBe("# Custom Rule");
86
+ expect(fs.readlinkSync(claudeSkills)).toBe(path.join("..", ".agents", "skills"));
76
87
  });
77
88
  });