@famgia/omnify-typescript 0.0.28 → 0.0.29
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.
- package/ai-guides/antdesign-guide.md +401 -0
- package/ai-guides/typescript-guide.md +254 -0
- package/package.json +4 -3
- package/scripts/postinstall.js +21 -346
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
# Omnify + Ant Design Integration Guide
|
|
2
|
+
|
|
3
|
+
This guide shows how to use Omnify-generated validation rules with Ant Design Forms.
|
|
4
|
+
|
|
5
|
+
## Generated Files
|
|
6
|
+
|
|
7
|
+
Omnify generates validation rules in `rules/` directory:
|
|
8
|
+
- `rules/{Model}.rules.ts` - Validation rules for each model
|
|
9
|
+
|
|
10
|
+
## File Structure
|
|
11
|
+
|
|
12
|
+
```typescript
|
|
13
|
+
// rules/User.rules.ts
|
|
14
|
+
|
|
15
|
+
// Display name for model (multi-locale)
|
|
16
|
+
export const UserDisplayName: LocaleMap = {
|
|
17
|
+
ja: 'ユーザー',
|
|
18
|
+
en: 'User',
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
// Display names for properties (multi-locale)
|
|
22
|
+
export const UserPropertyDisplayNames: Record<string, LocaleMap> = {
|
|
23
|
+
name: { ja: '名前', en: 'Name' },
|
|
24
|
+
email: { ja: 'メールアドレス', en: 'Email' },
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
// Validation rules with multi-locale messages
|
|
28
|
+
export const UserRules: Record<string, ValidationRule[]> = {
|
|
29
|
+
name: [
|
|
30
|
+
{ required: true, message: { ja: '名前は必須です', en: 'Name is required' } },
|
|
31
|
+
{ max: 100, message: { ja: '名前は100文字以内で入力してください', en: 'Name must be at most 100 characters' } },
|
|
32
|
+
],
|
|
33
|
+
email: [
|
|
34
|
+
{ required: true, message: { ja: 'メールアドレスは必須です', en: 'Email is required' } },
|
|
35
|
+
{ type: 'email', message: { ja: 'メールアドレスの形式が正しくありません', en: 'Email is not a valid email address' } },
|
|
36
|
+
],
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
// Helper functions
|
|
40
|
+
export function getUserRules(locale: string): Record<string, Rule[]>;
|
|
41
|
+
export function getUserDisplayName(locale: string): string;
|
|
42
|
+
export function getUserPropertyDisplayName(property: string, locale: string): string;
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Basic Usage
|
|
46
|
+
|
|
47
|
+
```tsx
|
|
48
|
+
import { Form, Input, Button } from 'antd';
|
|
49
|
+
import {
|
|
50
|
+
getUserRules,
|
|
51
|
+
getUserDisplayName,
|
|
52
|
+
getUserPropertyDisplayName
|
|
53
|
+
} from '@/types/model/rules/User.rules';
|
|
54
|
+
|
|
55
|
+
interface UserFormProps {
|
|
56
|
+
locale?: string;
|
|
57
|
+
onSubmit: (values: User) => void;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function UserForm({ locale = 'ja', onSubmit }: UserFormProps) {
|
|
61
|
+
const [form] = Form.useForm();
|
|
62
|
+
const rules = getUserRules(locale);
|
|
63
|
+
|
|
64
|
+
return (
|
|
65
|
+
<Form
|
|
66
|
+
form={form}
|
|
67
|
+
layout="vertical"
|
|
68
|
+
onFinish={onSubmit}
|
|
69
|
+
>
|
|
70
|
+
<Form.Item
|
|
71
|
+
name="name"
|
|
72
|
+
label={getUserPropertyDisplayName('name', locale)}
|
|
73
|
+
rules={rules.name}
|
|
74
|
+
>
|
|
75
|
+
<Input placeholder={getUserPropertyDisplayName('name', locale)} />
|
|
76
|
+
</Form.Item>
|
|
77
|
+
|
|
78
|
+
<Form.Item
|
|
79
|
+
name="email"
|
|
80
|
+
label={getUserPropertyDisplayName('email', locale)}
|
|
81
|
+
rules={rules.email}
|
|
82
|
+
>
|
|
83
|
+
<Input type="email" />
|
|
84
|
+
</Form.Item>
|
|
85
|
+
|
|
86
|
+
<Form.Item>
|
|
87
|
+
<Button type="primary" htmlType="submit">
|
|
88
|
+
{locale === 'ja' ? '送信' : 'Submit'}
|
|
89
|
+
</Button>
|
|
90
|
+
</Form.Item>
|
|
91
|
+
</Form>
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## With Edit Mode (Initial Values)
|
|
97
|
+
|
|
98
|
+
```tsx
|
|
99
|
+
import { Form, Input, Button, Spin } from 'antd';
|
|
100
|
+
import { getUserRules, getUserPropertyDisplayName } from '@/types/model/rules/User.rules';
|
|
101
|
+
import { User } from '@/types/model';
|
|
102
|
+
|
|
103
|
+
interface UserEditFormProps {
|
|
104
|
+
user: User;
|
|
105
|
+
locale?: string;
|
|
106
|
+
onSubmit: (values: Partial<User>) => void;
|
|
107
|
+
loading?: boolean;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function UserEditForm({ user, locale = 'ja', onSubmit, loading }: UserEditFormProps) {
|
|
111
|
+
const [form] = Form.useForm();
|
|
112
|
+
const rules = getUserRules(locale);
|
|
113
|
+
|
|
114
|
+
// Set initial values when user data changes
|
|
115
|
+
React.useEffect(() => {
|
|
116
|
+
form.setFieldsValue(user);
|
|
117
|
+
}, [user, form]);
|
|
118
|
+
|
|
119
|
+
return (
|
|
120
|
+
<Spin spinning={loading}>
|
|
121
|
+
<Form
|
|
122
|
+
form={form}
|
|
123
|
+
layout="vertical"
|
|
124
|
+
initialValues={user}
|
|
125
|
+
onFinish={onSubmit}
|
|
126
|
+
>
|
|
127
|
+
<Form.Item
|
|
128
|
+
name="name"
|
|
129
|
+
label={getUserPropertyDisplayName('name', locale)}
|
|
130
|
+
rules={rules.name}
|
|
131
|
+
>
|
|
132
|
+
<Input />
|
|
133
|
+
</Form.Item>
|
|
134
|
+
|
|
135
|
+
<Form.Item
|
|
136
|
+
name="email"
|
|
137
|
+
label={getUserPropertyDisplayName('email', locale)}
|
|
138
|
+
rules={rules.email}
|
|
139
|
+
>
|
|
140
|
+
<Input type="email" />
|
|
141
|
+
</Form.Item>
|
|
142
|
+
|
|
143
|
+
<Form.Item>
|
|
144
|
+
<Button type="primary" htmlType="submit" loading={loading}>
|
|
145
|
+
{locale === 'ja' ? '更新' : 'Update'}
|
|
146
|
+
</Button>
|
|
147
|
+
</Form.Item>
|
|
148
|
+
</Form>
|
|
149
|
+
</Spin>
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
## Dynamic Locale Switching
|
|
155
|
+
|
|
156
|
+
```tsx
|
|
157
|
+
import { Form, Input, Select } from 'antd';
|
|
158
|
+
import { useState, useMemo } from 'react';
|
|
159
|
+
import { getUserRules, getUserPropertyDisplayName, getUserDisplayName } from '@/types/model/rules/User.rules';
|
|
160
|
+
|
|
161
|
+
export function UserFormWithLocale() {
|
|
162
|
+
const [locale, setLocale] = useState('ja');
|
|
163
|
+
const [form] = Form.useForm();
|
|
164
|
+
|
|
165
|
+
// Memoize rules to avoid recalculation
|
|
166
|
+
const rules = useMemo(() => getUserRules(locale), [locale]);
|
|
167
|
+
|
|
168
|
+
return (
|
|
169
|
+
<>
|
|
170
|
+
<Select value={locale} onChange={setLocale} style={{ marginBottom: 16 }}>
|
|
171
|
+
<Select.Option value="ja">日本語</Select.Option>
|
|
172
|
+
<Select.Option value="en">English</Select.Option>
|
|
173
|
+
<Select.Option value="vi">Tiếng Việt</Select.Option>
|
|
174
|
+
</Select>
|
|
175
|
+
|
|
176
|
+
<h2>{getUserDisplayName(locale)}</h2>
|
|
177
|
+
|
|
178
|
+
<Form form={form} layout="vertical">
|
|
179
|
+
<Form.Item
|
|
180
|
+
name="name"
|
|
181
|
+
label={getUserPropertyDisplayName('name', locale)}
|
|
182
|
+
rules={rules.name}
|
|
183
|
+
>
|
|
184
|
+
<Input />
|
|
185
|
+
</Form.Item>
|
|
186
|
+
|
|
187
|
+
<Form.Item
|
|
188
|
+
name="email"
|
|
189
|
+
label={getUserPropertyDisplayName('email', locale)}
|
|
190
|
+
rules={rules.email}
|
|
191
|
+
>
|
|
192
|
+
<Input type="email" />
|
|
193
|
+
</Form.Item>
|
|
194
|
+
</Form>
|
|
195
|
+
</>
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
## With Table Columns
|
|
201
|
+
|
|
202
|
+
```tsx
|
|
203
|
+
import { Table, TableColumnsType } from 'antd';
|
|
204
|
+
import { getUserPropertyDisplayName } from '@/types/model/rules/User.rules';
|
|
205
|
+
import { User } from '@/types/model';
|
|
206
|
+
|
|
207
|
+
interface UserTableProps {
|
|
208
|
+
users: User[];
|
|
209
|
+
locale?: string;
|
|
210
|
+
loading?: boolean;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export function UserTable({ users, locale = 'ja', loading }: UserTableProps) {
|
|
214
|
+
const columns: TableColumnsType<User> = [
|
|
215
|
+
{
|
|
216
|
+
title: getUserPropertyDisplayName('name', locale),
|
|
217
|
+
dataIndex: 'name',
|
|
218
|
+
key: 'name',
|
|
219
|
+
},
|
|
220
|
+
{
|
|
221
|
+
title: getUserPropertyDisplayName('email', locale),
|
|
222
|
+
dataIndex: 'email',
|
|
223
|
+
key: 'email',
|
|
224
|
+
},
|
|
225
|
+
];
|
|
226
|
+
|
|
227
|
+
return (
|
|
228
|
+
<Table
|
|
229
|
+
columns={columns}
|
|
230
|
+
dataSource={users}
|
|
231
|
+
rowKey="id"
|
|
232
|
+
loading={loading}
|
|
233
|
+
/>
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
## Custom Form Hook
|
|
239
|
+
|
|
240
|
+
```tsx
|
|
241
|
+
import { Form, FormInstance } from 'antd';
|
|
242
|
+
import { useMemo } from 'react';
|
|
243
|
+
|
|
244
|
+
// Generic hook for any model's rules
|
|
245
|
+
export function useModelForm<T>(
|
|
246
|
+
getRules: (locale: string) => Record<string, any[]>,
|
|
247
|
+
locale: string = 'ja'
|
|
248
|
+
): {
|
|
249
|
+
form: FormInstance<T>;
|
|
250
|
+
rules: Record<string, any[]>;
|
|
251
|
+
} {
|
|
252
|
+
const [form] = Form.useForm<T>();
|
|
253
|
+
const rules = useMemo(() => getRules(locale), [getRules, locale]);
|
|
254
|
+
|
|
255
|
+
return { form, rules };
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Usage
|
|
259
|
+
import { getUserRules } from '@/types/model/rules/User.rules';
|
|
260
|
+
|
|
261
|
+
function MyComponent() {
|
|
262
|
+
const { form, rules } = useModelForm(getUserRules, 'ja');
|
|
263
|
+
|
|
264
|
+
return (
|
|
265
|
+
<Form form={form}>
|
|
266
|
+
<Form.Item name="name" rules={rules.name}>
|
|
267
|
+
<Input />
|
|
268
|
+
</Form.Item>
|
|
269
|
+
</Form>
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
## Modal Form
|
|
275
|
+
|
|
276
|
+
```tsx
|
|
277
|
+
import { Modal, Form, Input, message } from 'antd';
|
|
278
|
+
import { getUserRules, getUserDisplayName, getUserPropertyDisplayName } from '@/types/model/rules/User.rules';
|
|
279
|
+
import { User } from '@/types/model';
|
|
280
|
+
|
|
281
|
+
interface UserModalFormProps {
|
|
282
|
+
open: boolean;
|
|
283
|
+
onClose: () => void;
|
|
284
|
+
onSubmit: (values: Partial<User>) => Promise<void>;
|
|
285
|
+
initialValues?: Partial<User>;
|
|
286
|
+
locale?: string;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
export function UserModalForm({
|
|
290
|
+
open,
|
|
291
|
+
onClose,
|
|
292
|
+
onSubmit,
|
|
293
|
+
initialValues,
|
|
294
|
+
locale = 'ja'
|
|
295
|
+
}: UserModalFormProps) {
|
|
296
|
+
const [form] = Form.useForm();
|
|
297
|
+
const [loading, setLoading] = useState(false);
|
|
298
|
+
const rules = getUserRules(locale);
|
|
299
|
+
|
|
300
|
+
const handleSubmit = async () => {
|
|
301
|
+
try {
|
|
302
|
+
const values = await form.validateFields();
|
|
303
|
+
setLoading(true);
|
|
304
|
+
await onSubmit(values);
|
|
305
|
+
message.success(locale === 'ja' ? '保存しました' : 'Saved successfully');
|
|
306
|
+
form.resetFields();
|
|
307
|
+
onClose();
|
|
308
|
+
} catch (error) {
|
|
309
|
+
if (error instanceof Error) {
|
|
310
|
+
message.error(error.message);
|
|
311
|
+
}
|
|
312
|
+
} finally {
|
|
313
|
+
setLoading(false);
|
|
314
|
+
}
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
return (
|
|
318
|
+
<Modal
|
|
319
|
+
title={getUserDisplayName(locale)}
|
|
320
|
+
open={open}
|
|
321
|
+
onCancel={onClose}
|
|
322
|
+
onOk={handleSubmit}
|
|
323
|
+
confirmLoading={loading}
|
|
324
|
+
okText={locale === 'ja' ? '保存' : 'Save'}
|
|
325
|
+
cancelText={locale === 'ja' ? 'キャンセル' : 'Cancel'}
|
|
326
|
+
>
|
|
327
|
+
<Form
|
|
328
|
+
form={form}
|
|
329
|
+
layout="vertical"
|
|
330
|
+
initialValues={initialValues}
|
|
331
|
+
>
|
|
332
|
+
<Form.Item
|
|
333
|
+
name="name"
|
|
334
|
+
label={getUserPropertyDisplayName('name', locale)}
|
|
335
|
+
rules={rules.name}
|
|
336
|
+
>
|
|
337
|
+
<Input />
|
|
338
|
+
</Form.Item>
|
|
339
|
+
|
|
340
|
+
<Form.Item
|
|
341
|
+
name="email"
|
|
342
|
+
label={getUserPropertyDisplayName('email', locale)}
|
|
343
|
+
rules={rules.email}
|
|
344
|
+
>
|
|
345
|
+
<Input type="email" />
|
|
346
|
+
</Form.Item>
|
|
347
|
+
</Form>
|
|
348
|
+
</Modal>
|
|
349
|
+
);
|
|
350
|
+
}
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
## Validation Rule Types
|
|
354
|
+
|
|
355
|
+
Omnify generates these rule types based on schema:
|
|
356
|
+
|
|
357
|
+
| Schema | Generated Rule |
|
|
358
|
+
|--------|---------------|
|
|
359
|
+
| `required: true` | `{ required: true, message: {...} }` |
|
|
360
|
+
| `type: Email` | `{ type: 'email', message: {...} }` |
|
|
361
|
+
| `maxLength: N` | `{ max: N, message: {...} }` |
|
|
362
|
+
| `minLength: N` | `{ min: N, message: {...} }` |
|
|
363
|
+
| `max: N` (numeric) | `{ max: N, type: 'number', message: {...} }` |
|
|
364
|
+
| `min: N` (numeric) | `{ min: N, type: 'number', message: {...} }` |
|
|
365
|
+
| `pattern: regex` | `{ pattern: /regex/, message: {...} }` |
|
|
366
|
+
|
|
367
|
+
## Built-in Validation Messages
|
|
368
|
+
|
|
369
|
+
Omnify includes templates for 5 languages:
|
|
370
|
+
- Japanese (ja)
|
|
371
|
+
- English (en)
|
|
372
|
+
- Vietnamese (vi)
|
|
373
|
+
- Korean (ko)
|
|
374
|
+
- Chinese (zh)
|
|
375
|
+
|
|
376
|
+
Custom templates can be configured in `omnify.config.ts`:
|
|
377
|
+
|
|
378
|
+
```typescript
|
|
379
|
+
export default defineConfig({
|
|
380
|
+
output: {
|
|
381
|
+
typescript: {
|
|
382
|
+
validationTemplates: {
|
|
383
|
+
required: {
|
|
384
|
+
ja: '${displayName}を入力してください',
|
|
385
|
+
en: '${displayName} is required',
|
|
386
|
+
vi: '${displayName} là bắt buộc',
|
|
387
|
+
},
|
|
388
|
+
maxLength: {
|
|
389
|
+
ja: '${displayName}は${max}文字以内です',
|
|
390
|
+
en: '${displayName} must be at most ${max} characters',
|
|
391
|
+
},
|
|
392
|
+
},
|
|
393
|
+
},
|
|
394
|
+
},
|
|
395
|
+
});
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
Available placeholders:
|
|
399
|
+
- `${displayName}` - Property display name
|
|
400
|
+
- `${min}` - Minimum value/length
|
|
401
|
+
- `${max}` - Maximum value/length
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
# Omnify TypeScript Generator Guide
|
|
2
|
+
|
|
3
|
+
This guide covers TypeScript-specific features and generated code patterns for Omnify.
|
|
4
|
+
|
|
5
|
+
## Generated Files
|
|
6
|
+
|
|
7
|
+
When you run `npx omnify generate`, the following TypeScript files are generated:
|
|
8
|
+
|
|
9
|
+
- `base/*.ts` - Base model interfaces
|
|
10
|
+
- `enum/*.ts` - Enum types with multi-locale labels
|
|
11
|
+
- `rules/*.ts` - Ant Design compatible validation rules
|
|
12
|
+
|
|
13
|
+
## Type Generation
|
|
14
|
+
|
|
15
|
+
### Object Schema → Interface
|
|
16
|
+
|
|
17
|
+
```yaml
|
|
18
|
+
# yaml-language-server: $schema=./node_modules/.omnify/combined-schema.json
|
|
19
|
+
name: User
|
|
20
|
+
properties:
|
|
21
|
+
id:
|
|
22
|
+
type: BigInt
|
|
23
|
+
required: true
|
|
24
|
+
name:
|
|
25
|
+
type: String
|
|
26
|
+
required: true
|
|
27
|
+
maxLength: 255
|
|
28
|
+
email:
|
|
29
|
+
type: String
|
|
30
|
+
required: true
|
|
31
|
+
unique: true
|
|
32
|
+
profile:
|
|
33
|
+
type: Json
|
|
34
|
+
createdAt:
|
|
35
|
+
type: DateTime
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Generated:
|
|
39
|
+
```typescript
|
|
40
|
+
export interface User {
|
|
41
|
+
id: number;
|
|
42
|
+
name: string;
|
|
43
|
+
email: string;
|
|
44
|
+
profile: Record<string, unknown> | null;
|
|
45
|
+
createdAt: Date | null;
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Type Mapping
|
|
50
|
+
|
|
51
|
+
| Schema Type | TypeScript Type |
|
|
52
|
+
|-------------|-----------------|
|
|
53
|
+
| `String` | `string` |
|
|
54
|
+
| `LongText` | `string` |
|
|
55
|
+
| `Int` | `number` |
|
|
56
|
+
| `BigInt` | `number` |
|
|
57
|
+
| `Float` | `number` |
|
|
58
|
+
| `Boolean` | `boolean` |
|
|
59
|
+
| `Date` | `Date` |
|
|
60
|
+
| `DateTime` | `Date` |
|
|
61
|
+
| `Json` | `Record<string, unknown>` |
|
|
62
|
+
| `EnumRef` | Generated enum type |
|
|
63
|
+
| `Association` | Related model type / array |
|
|
64
|
+
|
|
65
|
+
## Enum Generation (Multi-locale)
|
|
66
|
+
|
|
67
|
+
```yaml
|
|
68
|
+
# schemas/PostStatus.yaml
|
|
69
|
+
name: PostStatus
|
|
70
|
+
kind: enum
|
|
71
|
+
displayName:
|
|
72
|
+
ja: 投稿ステータス
|
|
73
|
+
en: Post Status
|
|
74
|
+
values:
|
|
75
|
+
draft:
|
|
76
|
+
ja: 下書き
|
|
77
|
+
en: Draft
|
|
78
|
+
published:
|
|
79
|
+
ja: 公開済み
|
|
80
|
+
en: Published
|
|
81
|
+
archived:
|
|
82
|
+
ja: アーカイブ
|
|
83
|
+
en: Archived
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Generated:
|
|
87
|
+
```typescript
|
|
88
|
+
export const PostStatus = {
|
|
89
|
+
draft: 'draft',
|
|
90
|
+
published: 'published',
|
|
91
|
+
archived: 'archived',
|
|
92
|
+
} as const;
|
|
93
|
+
|
|
94
|
+
export type PostStatus = typeof PostStatus[keyof typeof PostStatus];
|
|
95
|
+
|
|
96
|
+
// Multi-locale labels
|
|
97
|
+
export const PostStatusLabels: Record<PostStatus, Record<string, string>> = {
|
|
98
|
+
draft: { ja: '下書き', en: 'Draft' },
|
|
99
|
+
published: { ja: '公開済み', en: 'Published' },
|
|
100
|
+
archived: { ja: 'アーカイブ', en: 'Archived' },
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
// Get label for specific locale
|
|
104
|
+
export function getPostStatusLabel(value: PostStatus, locale: string = 'en'): string {
|
|
105
|
+
return PostStatusLabels[value]?.[locale] ?? PostStatusLabels[value]?.['en'] ?? value;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Helper functions
|
|
109
|
+
export const PostStatusValues = Object.values(PostStatus);
|
|
110
|
+
export function isPostStatus(value: unknown): value is PostStatus {
|
|
111
|
+
return PostStatusValues.includes(value as PostStatus);
|
|
112
|
+
}
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## Validation Rules (Ant Design)
|
|
116
|
+
|
|
117
|
+
Omnify generates Ant Design compatible validation rules in `rules/` directory.
|
|
118
|
+
|
|
119
|
+
**See detailed guide:** `.claude/omnify/antdesign-guide.md`
|
|
120
|
+
|
|
121
|
+
Quick example:
|
|
122
|
+
```tsx
|
|
123
|
+
import { Form, Input } from 'antd';
|
|
124
|
+
import { getUserRules, getUserPropertyDisplayName } from './types/model/rules/User.rules';
|
|
125
|
+
|
|
126
|
+
function UserForm({ locale = 'ja' }) {
|
|
127
|
+
const rules = getUserRules(locale);
|
|
128
|
+
return (
|
|
129
|
+
<Form>
|
|
130
|
+
<Form.Item name="name" label={getUserPropertyDisplayName('name', locale)} rules={rules.name}>
|
|
131
|
+
<Input />
|
|
132
|
+
</Form.Item>
|
|
133
|
+
</Form>
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## Association Types
|
|
139
|
+
|
|
140
|
+
### ManyToOne
|
|
141
|
+
```yaml
|
|
142
|
+
author:
|
|
143
|
+
type: Association
|
|
144
|
+
relation: ManyToOne
|
|
145
|
+
target: User
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Generated:
|
|
149
|
+
```typescript
|
|
150
|
+
export interface Post {
|
|
151
|
+
authorId: number;
|
|
152
|
+
author?: User; // Optional: loaded relation
|
|
153
|
+
}
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### OneToMany
|
|
157
|
+
```yaml
|
|
158
|
+
posts:
|
|
159
|
+
type: Association
|
|
160
|
+
relation: OneToMany
|
|
161
|
+
target: Post
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
Generated:
|
|
165
|
+
```typescript
|
|
166
|
+
export interface User {
|
|
167
|
+
posts?: Post[]; // Optional: loaded relation array
|
|
168
|
+
}
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
### ManyToMany
|
|
172
|
+
```yaml
|
|
173
|
+
tags:
|
|
174
|
+
type: Association
|
|
175
|
+
relation: ManyToMany
|
|
176
|
+
target: Tag
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
Generated:
|
|
180
|
+
```typescript
|
|
181
|
+
export interface Post {
|
|
182
|
+
tags?: Tag[]; // Optional: loaded relation array
|
|
183
|
+
}
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
## Nullable Fields
|
|
187
|
+
|
|
188
|
+
Fields without `required: true` are nullable:
|
|
189
|
+
|
|
190
|
+
```yaml
|
|
191
|
+
description:
|
|
192
|
+
type: LongText # No required: true
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
Generated:
|
|
196
|
+
```typescript
|
|
197
|
+
description: string | null;
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
## Using Generated Types
|
|
201
|
+
|
|
202
|
+
```typescript
|
|
203
|
+
import { User, Post, PostStatus, isPostStatus } from './types/omnify-types';
|
|
204
|
+
|
|
205
|
+
// Type-safe object creation
|
|
206
|
+
const user: User = {
|
|
207
|
+
id: 1,
|
|
208
|
+
name: 'John',
|
|
209
|
+
email: 'john@example.com',
|
|
210
|
+
profile: null,
|
|
211
|
+
createdAt: new Date(),
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
// Enum usage
|
|
215
|
+
const status: PostStatus = PostStatus.draft;
|
|
216
|
+
|
|
217
|
+
// Type guard
|
|
218
|
+
function handleStatus(value: unknown) {
|
|
219
|
+
if (isPostStatus(value)) {
|
|
220
|
+
console.log('Valid status:', value);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
## Commands
|
|
226
|
+
|
|
227
|
+
```bash
|
|
228
|
+
# Generate TypeScript types
|
|
229
|
+
npx omnify generate
|
|
230
|
+
|
|
231
|
+
# Force regenerate all files
|
|
232
|
+
npx omnify generate --force
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
## Configuration
|
|
236
|
+
|
|
237
|
+
```typescript
|
|
238
|
+
// omnify.config.ts
|
|
239
|
+
import { defineConfig } from '@famgia/omnify';
|
|
240
|
+
|
|
241
|
+
export default defineConfig({
|
|
242
|
+
schemasDir: './schemas',
|
|
243
|
+
output: {
|
|
244
|
+
typescript: {
|
|
245
|
+
path: './src/types/model',
|
|
246
|
+
generateRules: true, // Generate Ant Design validation rules
|
|
247
|
+
},
|
|
248
|
+
},
|
|
249
|
+
locale: {
|
|
250
|
+
locales: ['ja', 'en'],
|
|
251
|
+
defaultLocale: 'ja',
|
|
252
|
+
},
|
|
253
|
+
});
|
|
254
|
+
```
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@famgia/omnify-typescript",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.29",
|
|
4
4
|
"description": "TypeScript type definitions generator for Omnify schemas",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.cjs",
|
|
@@ -30,7 +30,8 @@
|
|
|
30
30
|
},
|
|
31
31
|
"files": [
|
|
32
32
|
"dist",
|
|
33
|
-
"scripts"
|
|
33
|
+
"scripts",
|
|
34
|
+
"ai-guides"
|
|
34
35
|
],
|
|
35
36
|
"keywords": [
|
|
36
37
|
"omnify",
|
|
@@ -47,7 +48,7 @@
|
|
|
47
48
|
"directory": "packages/typescript-generator"
|
|
48
49
|
},
|
|
49
50
|
"dependencies": {
|
|
50
|
-
"@famgia/omnify-types": "0.0.
|
|
51
|
+
"@famgia/omnify-types": "0.0.37"
|
|
51
52
|
},
|
|
52
53
|
"devDependencies": {
|
|
53
54
|
"tsup": "^8.5.1",
|
package/scripts/postinstall.js
CHANGED
|
@@ -2,348 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
import fs from 'fs';
|
|
4
4
|
import path from 'path';
|
|
5
|
+
import { fileURLToPath } from 'url';
|
|
5
6
|
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
This guide covers TypeScript-specific features and generated code patterns for Omnify.
|
|
9
|
-
|
|
10
|
-
## Generated Files
|
|
11
|
-
|
|
12
|
-
When you run \`npx omnify generate\`, the following TypeScript files are generated:
|
|
13
|
-
|
|
14
|
-
- \`base/*.ts\` - Base model interfaces
|
|
15
|
-
- \`enum/*.ts\` - Enum types with multi-locale labels
|
|
16
|
-
- \`rules/*.ts\` - Ant Design compatible validation rules
|
|
17
|
-
|
|
18
|
-
## Type Generation
|
|
19
|
-
|
|
20
|
-
### Object Schema → Interface
|
|
21
|
-
|
|
22
|
-
\`\`\`yaml
|
|
23
|
-
# yaml-language-server: $schema=./node_modules/.omnify/combined-schema.json
|
|
24
|
-
name: User
|
|
25
|
-
properties:
|
|
26
|
-
id:
|
|
27
|
-
type: BigInt
|
|
28
|
-
required: true
|
|
29
|
-
name:
|
|
30
|
-
type: String
|
|
31
|
-
required: true
|
|
32
|
-
maxLength: 255
|
|
33
|
-
email:
|
|
34
|
-
type: String
|
|
35
|
-
required: true
|
|
36
|
-
unique: true
|
|
37
|
-
profile:
|
|
38
|
-
type: Json
|
|
39
|
-
createdAt:
|
|
40
|
-
type: DateTime
|
|
41
|
-
\`\`\`
|
|
42
|
-
|
|
43
|
-
Generated:
|
|
44
|
-
\`\`\`typescript
|
|
45
|
-
export interface User {
|
|
46
|
-
id: number;
|
|
47
|
-
name: string;
|
|
48
|
-
email: string;
|
|
49
|
-
profile: Record<string, unknown> | null;
|
|
50
|
-
createdAt: Date | null;
|
|
51
|
-
}
|
|
52
|
-
\`\`\`
|
|
53
|
-
|
|
54
|
-
## Type Mapping
|
|
55
|
-
|
|
56
|
-
| Schema Type | TypeScript Type |
|
|
57
|
-
|-------------|-----------------|
|
|
58
|
-
| \`String\` | \`string\` |
|
|
59
|
-
| \`LongText\` | \`string\` |
|
|
60
|
-
| \`Int\` | \`number\` |
|
|
61
|
-
| \`BigInt\` | \`number\` |
|
|
62
|
-
| \`Float\` | \`number\` |
|
|
63
|
-
| \`Boolean\` | \`boolean\` |
|
|
64
|
-
| \`Date\` | \`Date\` |
|
|
65
|
-
| \`DateTime\` | \`Date\` |
|
|
66
|
-
| \`Json\` | \`Record<string, unknown>\` |
|
|
67
|
-
| \`EnumRef\` | Generated enum type |
|
|
68
|
-
| \`Association\` | Related model type / array |
|
|
69
|
-
|
|
70
|
-
## Enum Generation (Multi-locale)
|
|
71
|
-
|
|
72
|
-
\`\`\`yaml
|
|
73
|
-
# schemas/PostStatus.yaml
|
|
74
|
-
name: PostStatus
|
|
75
|
-
kind: enum
|
|
76
|
-
displayName:
|
|
77
|
-
ja: 投稿ステータス
|
|
78
|
-
en: Post Status
|
|
79
|
-
values:
|
|
80
|
-
draft:
|
|
81
|
-
ja: 下書き
|
|
82
|
-
en: Draft
|
|
83
|
-
published:
|
|
84
|
-
ja: 公開済み
|
|
85
|
-
en: Published
|
|
86
|
-
archived:
|
|
87
|
-
ja: アーカイブ
|
|
88
|
-
en: Archived
|
|
89
|
-
\`\`\`
|
|
90
|
-
|
|
91
|
-
Generated:
|
|
92
|
-
\`\`\`typescript
|
|
93
|
-
export const PostStatus = {
|
|
94
|
-
draft: 'draft',
|
|
95
|
-
published: 'published',
|
|
96
|
-
archived: 'archived',
|
|
97
|
-
} as const;
|
|
98
|
-
|
|
99
|
-
export type PostStatus = typeof PostStatus[keyof typeof PostStatus];
|
|
100
|
-
|
|
101
|
-
// Multi-locale labels
|
|
102
|
-
export const PostStatusLabels: Record<PostStatus, Record<string, string>> = {
|
|
103
|
-
draft: { ja: '下書き', en: 'Draft' },
|
|
104
|
-
published: { ja: '公開済み', en: 'Published' },
|
|
105
|
-
archived: { ja: 'アーカイブ', en: 'Archived' },
|
|
106
|
-
};
|
|
107
|
-
|
|
108
|
-
// Get label for specific locale
|
|
109
|
-
export function getPostStatusLabel(value: PostStatus, locale: string = 'en'): string {
|
|
110
|
-
return PostStatusLabels[value]?.[locale] ?? PostStatusLabels[value]?.['en'] ?? value;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
// Helper functions
|
|
114
|
-
export const PostStatusValues = Object.values(PostStatus);
|
|
115
|
-
export function isPostStatus(value: unknown): value is PostStatus {
|
|
116
|
-
return PostStatusValues.includes(value as PostStatus);
|
|
117
|
-
}
|
|
118
|
-
\`\`\`
|
|
119
|
-
|
|
120
|
-
## Validation Rules (Ant Design)
|
|
121
|
-
|
|
122
|
-
Omnify generates Ant Design compatible validation rules with multi-locale messages.
|
|
123
|
-
|
|
124
|
-
\`\`\`yaml
|
|
125
|
-
# schemas/User.yaml
|
|
126
|
-
name: User
|
|
127
|
-
displayName:
|
|
128
|
-
ja: ユーザー
|
|
129
|
-
en: User
|
|
130
|
-
properties:
|
|
131
|
-
name:
|
|
132
|
-
type: String
|
|
133
|
-
displayName:
|
|
134
|
-
ja: 名前
|
|
135
|
-
en: Name
|
|
136
|
-
required: true
|
|
137
|
-
maxLength: 100
|
|
138
|
-
email:
|
|
139
|
-
type: String
|
|
140
|
-
displayName:
|
|
141
|
-
ja: メールアドレス
|
|
142
|
-
en: Email
|
|
143
|
-
required: true
|
|
144
|
-
\`\`\`
|
|
145
|
-
|
|
146
|
-
Generated (\`rules/User.rules.ts\`):
|
|
147
|
-
\`\`\`typescript
|
|
148
|
-
export const UserDisplayName: LocaleMap = {
|
|
149
|
-
ja: 'ユーザー',
|
|
150
|
-
en: 'User',
|
|
151
|
-
};
|
|
152
|
-
|
|
153
|
-
export const UserPropertyDisplayNames: Record<string, LocaleMap> = {
|
|
154
|
-
name: { ja: '名前', en: 'Name' },
|
|
155
|
-
email: { ja: 'メールアドレス', en: 'Email' },
|
|
156
|
-
};
|
|
157
|
-
|
|
158
|
-
export const UserRules: Record<string, ValidationRule[]> = {
|
|
159
|
-
name: [
|
|
160
|
-
{ required: true, message: { ja: '名前は必須です', en: 'Name is required' } },
|
|
161
|
-
{ max: 100, message: { ja: '名前は100文字以内で入力してください', en: 'Name must be at most 100 characters' } },
|
|
162
|
-
],
|
|
163
|
-
email: [
|
|
164
|
-
{ required: true, message: { ja: 'メールアドレスは必須です', en: 'Email is required' } },
|
|
165
|
-
],
|
|
166
|
-
};
|
|
167
|
-
|
|
168
|
-
// Get rules for specific locale (for Ant Design Form)
|
|
169
|
-
export function getUserRules(locale: string): Record<string, Array<{ required?: boolean; max?: number; message: string }>> { ... }
|
|
170
|
-
|
|
171
|
-
// Get display names
|
|
172
|
-
export function getUserDisplayName(locale: string): string { ... }
|
|
173
|
-
export function getUserPropertyDisplayName(property: string, locale: string): string { ... }
|
|
174
|
-
\`\`\`
|
|
175
|
-
|
|
176
|
-
### Using in Ant Design Form
|
|
177
|
-
|
|
178
|
-
\`\`\`tsx
|
|
179
|
-
import { Form, Input } from 'antd';
|
|
180
|
-
import { getUserRules, getUserPropertyDisplayName } from './types/model/rules/User.rules';
|
|
181
|
-
|
|
182
|
-
function UserForm({ locale = 'ja' }) {
|
|
183
|
-
const rules = getUserRules(locale);
|
|
184
|
-
|
|
185
|
-
return (
|
|
186
|
-
<Form>
|
|
187
|
-
<Form.Item
|
|
188
|
-
name="name"
|
|
189
|
-
label={getUserPropertyDisplayName('name', locale)}
|
|
190
|
-
rules={rules.name}
|
|
191
|
-
>
|
|
192
|
-
<Input />
|
|
193
|
-
</Form.Item>
|
|
194
|
-
<Form.Item
|
|
195
|
-
name="email"
|
|
196
|
-
label={getUserPropertyDisplayName('email', locale)}
|
|
197
|
-
rules={rules.email}
|
|
198
|
-
>
|
|
199
|
-
<Input />
|
|
200
|
-
</Form.Item>
|
|
201
|
-
</Form>
|
|
202
|
-
);
|
|
203
|
-
}
|
|
204
|
-
\`\`\`
|
|
205
|
-
|
|
206
|
-
### Built-in Validation Templates
|
|
207
|
-
|
|
208
|
-
Omnify includes built-in validation message templates for 5 languages:
|
|
209
|
-
- Japanese (ja), English (en), Vietnamese (vi), Korean (ko), Chinese (zh)
|
|
210
|
-
|
|
211
|
-
You can customize templates in \`omnify.config.ts\`:
|
|
212
|
-
\`\`\`typescript
|
|
213
|
-
export default defineConfig({
|
|
214
|
-
output: {
|
|
215
|
-
typescript: {
|
|
216
|
-
validationTemplates: {
|
|
217
|
-
required: {
|
|
218
|
-
ja: '\${displayName}を入力してください',
|
|
219
|
-
en: '\${displayName} is required',
|
|
220
|
-
},
|
|
221
|
-
maxLength: {
|
|
222
|
-
ja: '\${displayName}は\${max}文字以内です',
|
|
223
|
-
en: '\${displayName} must be \${max} characters or less',
|
|
224
|
-
},
|
|
225
|
-
},
|
|
226
|
-
},
|
|
227
|
-
},
|
|
228
|
-
});
|
|
229
|
-
\`\`\`
|
|
230
|
-
|
|
231
|
-
## Association Types
|
|
232
|
-
|
|
233
|
-
### ManyToOne
|
|
234
|
-
\`\`\`yaml
|
|
235
|
-
author:
|
|
236
|
-
type: Association
|
|
237
|
-
relation: ManyToOne
|
|
238
|
-
target: User
|
|
239
|
-
\`\`\`
|
|
240
|
-
|
|
241
|
-
Generated:
|
|
242
|
-
\`\`\`typescript
|
|
243
|
-
export interface Post {
|
|
244
|
-
authorId: number;
|
|
245
|
-
author?: User; // Optional: loaded relation
|
|
246
|
-
}
|
|
247
|
-
\`\`\`
|
|
248
|
-
|
|
249
|
-
### OneToMany
|
|
250
|
-
\`\`\`yaml
|
|
251
|
-
posts:
|
|
252
|
-
type: Association
|
|
253
|
-
relation: OneToMany
|
|
254
|
-
target: Post
|
|
255
|
-
\`\`\`
|
|
256
|
-
|
|
257
|
-
Generated:
|
|
258
|
-
\`\`\`typescript
|
|
259
|
-
export interface User {
|
|
260
|
-
posts?: Post[]; // Optional: loaded relation array
|
|
261
|
-
}
|
|
262
|
-
\`\`\`
|
|
263
|
-
|
|
264
|
-
### ManyToMany
|
|
265
|
-
\`\`\`yaml
|
|
266
|
-
tags:
|
|
267
|
-
type: Association
|
|
268
|
-
relation: ManyToMany
|
|
269
|
-
target: Tag
|
|
270
|
-
\`\`\`
|
|
271
|
-
|
|
272
|
-
Generated:
|
|
273
|
-
\`\`\`typescript
|
|
274
|
-
export interface Post {
|
|
275
|
-
tags?: Tag[]; // Optional: loaded relation array
|
|
276
|
-
}
|
|
277
|
-
\`\`\`
|
|
278
|
-
|
|
279
|
-
## Nullable Fields
|
|
280
|
-
|
|
281
|
-
Fields without \`required: true\` are nullable:
|
|
282
|
-
|
|
283
|
-
\`\`\`yaml
|
|
284
|
-
description:
|
|
285
|
-
type: LongText # No required: true
|
|
286
|
-
\`\`\`
|
|
287
|
-
|
|
288
|
-
Generated:
|
|
289
|
-
\`\`\`typescript
|
|
290
|
-
description: string | null;
|
|
291
|
-
\`\`\`
|
|
292
|
-
|
|
293
|
-
## Using Generated Types
|
|
294
|
-
|
|
295
|
-
\`\`\`typescript
|
|
296
|
-
import { User, Post, PostStatus, isPostStatus } from './types/omnify-types';
|
|
297
|
-
|
|
298
|
-
// Type-safe object creation
|
|
299
|
-
const user: User = {
|
|
300
|
-
id: 1,
|
|
301
|
-
name: 'John',
|
|
302
|
-
email: 'john@example.com',
|
|
303
|
-
profile: null,
|
|
304
|
-
createdAt: new Date(),
|
|
305
|
-
};
|
|
306
|
-
|
|
307
|
-
// Enum usage
|
|
308
|
-
const status: PostStatus = PostStatus.draft;
|
|
309
|
-
|
|
310
|
-
// Type guard
|
|
311
|
-
function handleStatus(value: unknown) {
|
|
312
|
-
if (isPostStatus(value)) {
|
|
313
|
-
console.log('Valid status:', value);
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
\`\`\`
|
|
317
|
-
|
|
318
|
-
## Commands
|
|
319
|
-
|
|
320
|
-
\`\`\`bash
|
|
321
|
-
# Generate TypeScript types
|
|
322
|
-
npx omnify generate --typescript
|
|
323
|
-
|
|
324
|
-
# Generate to specific output
|
|
325
|
-
npx omnify generate --typescript --output ./src/types
|
|
326
|
-
|
|
327
|
-
# Watch for changes
|
|
328
|
-
npx omnify watch --typescript
|
|
329
|
-
\`\`\`
|
|
330
|
-
|
|
331
|
-
## Configuration
|
|
332
|
-
|
|
333
|
-
\`\`\`javascript
|
|
334
|
-
// omnify.config.js
|
|
335
|
-
export default {
|
|
336
|
-
schemasDir: './schemas',
|
|
337
|
-
typescript: {
|
|
338
|
-
outputDir: './types',
|
|
339
|
-
outputFile: 'omnify-types.ts',
|
|
340
|
-
enumsFile: 'enums.ts',
|
|
341
|
-
generateHelpers: true, // Generate enum helpers
|
|
342
|
-
strictNullChecks: true // Use | null for optional
|
|
343
|
-
}
|
|
344
|
-
};
|
|
345
|
-
\`\`\`
|
|
346
|
-
`;
|
|
7
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
8
|
+
const __dirname = path.dirname(__filename);
|
|
347
9
|
|
|
348
10
|
function findProjectRoot() {
|
|
349
11
|
// npm/pnpm set INIT_CWD to the directory where the install was run
|
|
@@ -359,17 +21,30 @@ function findProjectRoot() {
|
|
|
359
21
|
return null;
|
|
360
22
|
}
|
|
361
23
|
|
|
362
|
-
function
|
|
24
|
+
function copyAiGuidesToProject(projectRoot) {
|
|
363
25
|
const omnifyDir = path.join(projectRoot, '.claude', 'omnify');
|
|
26
|
+
const aiGuidesDir = path.join(__dirname, '..', 'ai-guides');
|
|
364
27
|
|
|
365
28
|
try {
|
|
29
|
+
// Create target directory
|
|
366
30
|
if (!fs.existsSync(omnifyDir)) {
|
|
367
31
|
fs.mkdirSync(omnifyDir, { recursive: true });
|
|
368
32
|
}
|
|
369
33
|
|
|
370
|
-
|
|
371
|
-
fs.
|
|
372
|
-
|
|
34
|
+
// Copy all files from ai-guides directory
|
|
35
|
+
if (fs.existsSync(aiGuidesDir)) {
|
|
36
|
+
const files = fs.readdirSync(aiGuidesDir);
|
|
37
|
+
for (const file of files) {
|
|
38
|
+
const srcPath = path.join(aiGuidesDir, file);
|
|
39
|
+
const destPath = path.join(omnifyDir, file);
|
|
40
|
+
|
|
41
|
+
if (fs.statSync(srcPath).isFile()) {
|
|
42
|
+
fs.copyFileSync(srcPath, destPath);
|
|
43
|
+
console.log(` Created .claude/omnify/${file}`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
373
48
|
return true;
|
|
374
49
|
} catch {
|
|
375
50
|
return false;
|
|
@@ -389,7 +64,7 @@ function main() {
|
|
|
389
64
|
|
|
390
65
|
const projectRoot = findProjectRoot();
|
|
391
66
|
if (projectRoot) {
|
|
392
|
-
|
|
67
|
+
copyAiGuidesToProject(projectRoot);
|
|
393
68
|
}
|
|
394
69
|
}
|
|
395
70
|
|