create-dwy 0.6.2 → 0.6.4

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/bin/index.js CHANGED
@@ -27,7 +27,7 @@ program
27
27
 
28
28
  program
29
29
  .command('sync-claude-md')
30
- .description('单独同步 CLAUDE.md 到当前目录的 .claude/')
30
+ .description('单独同步 CLAUDE.md 到全局 ~/.claude/')
31
31
  .action(syncProjectClaudeMd)
32
32
 
33
33
  program.parse()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-dwy",
3
- "version": "0.6.2",
3
+ "version": "0.6.4",
4
4
  "description": "项目脚手架与 Claude Code 配置同步工具",
5
5
  "type": "module",
6
6
  "bin": {
package/src/sync.js CHANGED
@@ -205,6 +205,22 @@ export async function syncClaude() {
205
205
  process.exit(1)
206
206
  }
207
207
 
208
+ // 如果当前目录没有 .claude,先确认是否在此创建
209
+ if (!await fs.pathExists(projectTargetDir)) {
210
+ const { shouldCreate } = await inquirer.prompt([
211
+ {
212
+ type: 'confirm',
213
+ name: 'shouldCreate',
214
+ message: `当前目录 ${chalk.yellow(process.cwd())} 未找到 .claude 目录,是否在此创建?`,
215
+ default: false,
216
+ },
217
+ ])
218
+ if (!shouldCreate) {
219
+ console.log(chalk.yellow('\n已取消同步。'))
220
+ return
221
+ }
222
+ }
223
+
208
224
  // 扫描可用资源
209
225
  const skills = await scanSkills(sourceDir)
210
226
  const rules = await scanRules(sourceDir)
@@ -244,11 +260,11 @@ export async function syncClaude() {
244
260
  }
245
261
 
246
262
  /**
247
- * 单独同步 CLAUDE.md 到当前项目 .claude/
263
+ * 单独同步 CLAUDE.md 到全局 ~/.claude/
248
264
  */
249
265
  export async function syncProjectClaudeMd() {
250
266
  const sourceDir = await resolveSourceDir()
251
- const targetDir = path.join(process.cwd(), '.claude')
267
+ const targetDir = path.join(os.homedir(), '.claude')
252
268
 
253
269
  if (!await fs.pathExists(sourceDir)) {
254
270
  console.error(chalk.red('Error: claude-global templates not found in repo'))
@@ -6,11 +6,11 @@
6
6
  - **服务地址**:必须输出完整的 URL(如 `http://localhost:8000/api/users`),禁止省略协议、端口或路径
7
7
  - **数据库连接串**:必须输出完整连接字符串(如 `postgresql+asyncpg://user:pass@localhost:5432/mydb`),禁止只写主机名或库名
8
8
 
9
- ## Agent 团队执行
9
+ ## 团队执行任务
10
10
 
11
- 所有任务默认启用 agent team 模式执行:任务可拆分时优先并行派遣 Agent,具体派遣策略、并行度、worktree 隔离、review 闭环等由 Claude 自行判断,不在此处硬编码规则。
11
+ 任务执行优先使用 agent team 模式执行。
12
12
 
13
- ## Behavioral Guidelines — 减少常见 LLM 编码错误的行为指南
13
+ ## 减少常见 LLM 编码错误的行为指南 (Behavioral Guidelines)
14
14
 
15
15
  与项目特定指令合并使用。
16
16
 
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: dwy-ekit
3
- description: "@dwydev/ekit 前端工具库速查(v0.7.0)。触发条件:HTTP 请求、dwyeapi 响应契约对接、localStorage/Cookie、表单校验、日期/时区、剪贴板、查询字符串、文件下载、PII 脱敏、Vue composable 工具。"
3
+ description: "【强制速查】只要涉及以下任何主题,必须调用本 skill,无论用户是否说ekit:HTTP 请求 / axios 封装 / token 刷新 / dwyeapi 响应契约对接 / localStorage / Cookie / useStorage / useCookie / 日期格式化 / 时区处理 / dayjs / 表单校验 / zod schema / vee-validate / PII 脱敏 / 剪贴板 / useClipboard / 文件下载 / Blob / 查询字符串 / qs / Vue composables(debounce / throttle / click-outside / event-listener / window-size / media-query / intersection-observer / resize-observer / useFormPersist)。本 skill 是 @dwydev/ekit(v0.7.0)的唯一权威来源,禁止绕过 ekit 直接手写底层 API。"
4
4
  ---
5
5
 
6
6
  # @dwydev/ekit 工具库速查(v0.7.0)
@@ -29,11 +29,11 @@ import {
29
29
  ekit 对外类型 **不允许** 出现底层库类名。下面这些导入 **会失败** —— 因为 ekit 不再 re-export:
30
30
 
31
31
  ```ts
32
- // ❌ 错误:ekit 不导出底层类型
32
+ // 错误:ekit 不导出底层类型
33
33
  import type { AxiosInstance, IStringifyOptions, CookieAttributes } from '@dwydev/ekit'
34
34
  import { dayjs } from '@dwydev/ekit'
35
35
 
36
- // ✅ 正确:用 ekit 自有契约
36
+ // 正确:用 ekit 自有契约
37
37
  import type { HttpClient, StringifyOptions, CookieOptions } from '@dwydev/ekit'
38
38
  import { now, formatTimestamp } from '@dwydev/ekit' // 不需要 dayjs 实例
39
39
  ```
@@ -49,26 +49,295 @@ import { now, formatTimestamp } from '@dwydev/ekit' // 不需要 dayjs 实例
49
49
  | `document.addEventListener('click', ...)` | `useEventListener()` |
50
50
  | 手写 axios 实例 | `createRequest()` |
51
51
 
52
- ## 查阅源码
52
+ ## 模块速查
53
53
 
54
- 每个模块源码在 `/Users/chances/WebstormProjects/dwy-shared/frontend/ekit/src/{module}/`,函数都很短可以直接读。
54
+ ### request — HTTP 客户端 + 4 内置插件 + dwyeapi 契约
55
55
 
56
- ## 模块索引
56
+ ```ts
57
+ import {
58
+ createRequest, tokenPlugin, headerPlugin, unwrapPlugin, refreshTokenPlugin,
59
+ SUCCESS_CODE, isApiBusinessError, extractValidationErrors,
60
+ } from '@dwydev/ekit'
61
+ import type {
62
+ HttpClient, HttpConfig, HttpResponse, HttpError, HttpPlugin,
63
+ CreateRequestOptions, ApiResponse, PageData,
64
+ } from '@dwydev/ekit'
65
+ ```
66
+
67
+ **createRequest**
68
+
69
+ ```ts
70
+ const http: HttpClient = createRequest({
71
+ baseURL: '/api',
72
+ timeout: 30000,
73
+ headers: { 'X-Client': 'web' },
74
+ plugins: [tokenPlugin({ getToken: () => localStorage.getItem('token') }), unwrapPlugin()],
75
+ })
76
+
77
+ const res = await http.get<User>('/users/1') // res: HttpResponse<User>
78
+ const list = await http.get<PageData<User>>('/users')
79
+ list.data.items // User[]
80
+ list.data.total // number
81
+ list.data.page // number
82
+ list.data.page_size // number
83
+ ```
84
+
85
+ **内置插件**
86
+
87
+ | 插件 | 参数 | 说明 |
88
+ |------|------|------|
89
+ | `tokenPlugin` | `{ getToken: () => string | null }` | 注入 `Authorization: Bearer <token>` |
90
+ | `headerPlugin` | `{ name, getValue }` | 动态自定义 header |
91
+ | `unwrapPlugin` | 无 | 解包 dwyeapi `{ code, message, data, timestamp }`,业务失败时 reject |
92
+ | `refreshTokenPlugin` | `{ getRefreshToken, refreshFn, onRefreshFail, retry, isLoginUrl? }` | 401 自动刷新并重放;`retry: (cfg) => http.request(cfg)` 必填 |
93
+
94
+ **响应契约 & 错误捕获**
95
+
96
+ ```ts
97
+ // unwrap 后 data 是纯业务数据(单体)或 PageData<T>(分页)
98
+ const res = await http.get<User>('/users/1') // res.data: User
99
+
100
+ // 捕获业务错误
101
+ try {
102
+ await http.post('/orders', body)
103
+ } catch (err) {
104
+ if (!isApiBusinessError(err)) { message.error('网络异常'); return }
105
+ switch (err.businessCode) {
106
+ case 'INSUFFICIENT_BALANCE': showRecharge(); break
107
+ case 'VALIDATION_ERROR': form.setErrors(extractValidationErrors(err)); break
108
+ default: message.error(err.message)
109
+ }
110
+ }
111
+ ```
112
+
113
+ `extractValidationErrors` 剥离 `body. / query. / path.` 前缀,返回 `{ field: message }` 扁平对象,与 vee-validate 的 `form.setErrors` 直接兼容。
114
+
115
+ ### storage — localStorage
116
+
117
+ ```ts
118
+ import { useStorage, storage } from '@dwydev/ekit'
119
+ ```
120
+
121
+ | API | 签名 / 说明 |
122
+ |-----|------------|
123
+ | `useStorage(key, defaultValue, storage?, opts?)` | 再导出 @vueuse/core;跨标签页同步、SSR-safe、自动 JSON 序列化 |
124
+ | `storage.get<T>(key, defaultValue?)` | 返回 `T | undefined`;JSON 解析失败按字符串返回 |
125
+ | `storage.set(key, value)` | 自动 `JSON.stringify` |
126
+ | `storage.remove(key)` | 删除 |
127
+ | `storage.clear()` | 清空全部(慎用) |
128
+
129
+ ```ts
130
+ const token = useStorage<string>('token', '')
131
+ const config = useStorage('config', { theme: 'light' }, localStorage, { mergeDefaults: true })
132
+ ```
133
+
134
+ ### cookie — Cookie 读写
135
+
136
+ ```ts
137
+ import { useCookie, cookie } from '@dwydev/ekit'
138
+ import type { CookieOptions } from '@dwydev/ekit'
139
+ ```
140
+
141
+ ```ts
142
+ interface CookieOptions {
143
+ expires?: number | Date // 天数或 Date;不传 = 会话级
144
+ path?: string // 默认 '/'
145
+ domain?: string
146
+ secure?: boolean
147
+ sameSite?: 'strict' | 'lax' | 'none'
148
+ }
149
+
150
+ cookie.get<User>('user')
151
+ cookie.set('user', { id: 1 }, { expires: 7, secure: true })
152
+ cookie.remove('user', { path: '/' })
153
+
154
+ const lang = useCookie('lang', 'zh-CN')
155
+ lang.value = 'en-US' // 自动写入
156
+ lang.value = undefined // 自动删除
157
+ ```
158
+
159
+ ### date — 日期 / 时间 / 时区
160
+
161
+ ```ts
162
+ import {
163
+ now, formatTimestamp, formatInTimezone,
164
+ formatDate, formatDateTime, formatTime, formatRelativeTime, formatBy,
165
+ } from '@dwydev/ekit'
166
+ ```
167
+
168
+ | 函数 | 签名 | 输出示例 |
169
+ |------|------|---------|
170
+ | `now()` | `(): number` | `1745318445000`(UTC 毫秒戳) |
171
+ | `formatTimestamp(ts, fmt?)` | `(number, string?) => string` | `'2026-04-22 18:40:45'`(默认) |
172
+ | `formatInTimezone(tz, ts?, fmt?)` | `(string, number?, string?) => string` | `formatInTimezone('America/New_York', ts)` |
173
+ | `formatDate(input)` | `(string | number | Date) => string` | `'2026-04-22'` |
174
+ | `formatDateTime(input)` | 同上 | `'2026-04-22 18:40:45'` |
175
+ | `formatTime(input)` | 同上 | `'18:40'` |
176
+ | `formatRelativeTime(input)` | 同上 | `'刚刚' / '5 分钟前' / '3 小时前' / '2 天前'`;> 30 天退化为 `YYYY-MM-DD` |
177
+ | `formatBy(input, template)` | 同上 | `formatBy(ts, 'YYYY年MM月DD日')` -> `'2026年04月22日'` |
178
+
179
+ ### qs — 查询字符串
180
+
181
+ ```ts
182
+ import { stringify, parse } from '@dwydev/ekit'
183
+ import type { StringifyOptions, ParseOptions } from '@dwydev/ekit'
184
+ ```
185
+
186
+ ```ts
187
+ stringify({ ids: [1, 2], name: 'a' }) // 'ids=1&ids=2&name=a'
188
+ stringify({ ids: [1, 2] }, { arrayFormat: 'brackets' }) // 'ids[]=1&ids[]=2'
189
+ stringify({ a: 1 }, { addQueryPrefix: true }) // '?a=1'
190
+
191
+ parse('?a=1&b=2') // { a: '1', b: '2' }(默认 ignoreQueryPrefix: true)
192
+ parse('a.b=1', { allowDots: true }) // { a: { b: '1' } }
193
+ ```
194
+
195
+ ### validators — 表单校验
196
+
197
+ ```ts
198
+ import {
199
+ isPhone, isEmail, isIdCard, isUrl, isRequired, minLength, maxLength,
200
+ phoneSchema, emailSchema, idCardSchema, urlSchema,
201
+ requiredSchema, minLengthSchema, maxLengthSchema,
202
+ } from '@dwydev/ekit'
203
+ ```
204
+
205
+ **布尔函数**
206
+
207
+ | 函数 | 规则 |
208
+ |------|------|
209
+ | `isPhone(value)` | 中国手机号 1[3-9] 开头 11 位 |
210
+ | `isEmail(value)` | 基础邮箱格式 |
211
+ | `isIdCard(value)` | 18 位身份证(末位允许 X/x) |
212
+ | `isUrl(value)` | 合法 URL |
213
+ | `isRequired(value)` | 非空(0 / false 视为有效) |
214
+ | `minLength(value, n)` / `maxLength(value, n)` | 长度校验 |
215
+
216
+ **zod schema(配合 vee-validate)**
57
217
 
58
- 按需读取对应 references 文件,避免一次性加载全部 API 文档。
218
+ ```ts
219
+ import { useForm } from 'vee-validate'
220
+ import { toTypedSchema } from '@vee-validate/zod'
221
+ import { phoneSchema, emailSchema } from '@dwydev/ekit'
222
+
223
+ const schema = toTypedSchema(z.object({ phone: phoneSchema, email: emailSchema }))
224
+ const { defineField, errors } = useForm({ validationSchema: schema })
225
+ ```
226
+
227
+ > `minLengthSchema(n)` / `maxLengthSchema(n)` 是工厂函数,调用后返回 schema。
228
+
229
+ ### masking — PII 脱敏
230
+
231
+ ```ts
232
+ import {
233
+ maskPhone, maskEmail, maskIdCard, maskBankCard,
234
+ maskName, maskAddress, maskIp, maskLicensePlate, maskText,
235
+ } from '@dwydev/ekit'
236
+ ```
237
+
238
+ 纯函数,无外部依赖。**输入不符合格式时原样返回,不抛异常**。
239
+
240
+ | 函数 | 示例输出 |
241
+ |------|---------|
242
+ | | |
243
+ | | |
244
+ | | |
245
+ | | |
246
+ | / | / |
247
+ | | |
248
+ | | |
249
+ | | |
250
+ | | |
251
+
252
+ ### copy — 剪贴板
253
+
254
+ ```ts
255
+ import { copyText, useClipboard } from '@dwydev/ekit'
256
+ ```
257
+
258
+ ```ts
259
+ await copyText('hello')
260
+
261
+ const { text, copy, copied, isSupported } = useClipboard({ source: '', copiedDuring: 1500 })
262
+ copy('hello')
263
+ // copied: ComputedRef<boolean>,1500ms 后自动复位
264
+ // isSupported: ComputedRef<boolean>
265
+ ```
59
266
 
60
- | 模块 | 用途 | 详见 |
61
- |---|---|---|
62
- | `request` | HTTP 客户端(防腐层 + 4 个内置插件 + dwyeapi 响应契约 + 业务错误捕获) | `references/request.md` |
63
- | `storage` | localStorage 封装(@vueuse/core 的 useStorage + 静态 storage 对象) | `references/data.md` § storage |
64
- | `cookie` | Cookie 读写(js-cookie 薄封装 + useCookie) | `references/data.md` § cookie |
65
- | `date` | 日期 / 时间 / 时区格式化(基于 dayjs,不暴露实例) | `references/data.md` § date |
66
- | `qs` | 查询字符串序列化 / 解析(基于 qs) | `references/data.md` § qs |
67
- | `validators` | 表单校验(布尔函数 + zod schema 配合 vee-validate) | `references/form.md` § validators |
68
- | `masking` | PII 数据脱敏(手机/邮箱/身份证/银行卡/姓名/地址等) | `references/form.md` § masking |
69
- | `copy` | 剪贴板(copyText + @vueuse/core 的 useClipboard) | `references/ui.md` § copy |
70
- | `file` | 文件下载 / Blob / 文件大小格式化 | `references/ui.md` § file |
71
- | `hooks` | Vue Composables(全部再导出 @vueuse/core) | `references/ui.md` § hooks |
267
+ ### file — 文件下载 / Blob / 文件大小
268
+
269
+ ```ts
270
+ import { downloadFile, saveBlob, formatFileSize } from '@dwydev/ekit'
271
+ import type { DownloadOptions, FileRequester } from '@dwydev/ekit'
272
+ ```
273
+
274
+ ```ts
275
+ await downloadFile('/api/export.xlsx') // GET,自动解析文件名
276
+ await downloadFile('/api/export', {
277
+ filename: '报表.xlsx', method: 'POST', data: { dateRange },
278
+ })
279
+
280
+ // 自定义 requester(axios 实例天然满足 FileRequester)
281
+ await downloadFile('/api/export', { requester: myHttpClient })
282
+
283
+ saveBlob(blob, 'data.json')
284
+ formatFileSize(1234567) // '1.18 MB'
285
+ formatFileSize(1234567, 0) // '1 MB'
286
+ ```
287
+
288
+ ### hooks — Vue Composables
289
+
290
+ ```ts
291
+ import {
292
+ useDebounce, useClickOutside, useEventListener, useThrottle,
293
+ useWindowSize, useMediaQuery, useIntersectionObserver, useResizeObserver,
294
+ useFormPersist, DEFAULT_SENSITIVE_FIELDS,
295
+ } from '@dwydev/ekit'
296
+ ```
297
+
298
+ | Composable | 说明 |
299
+ |-----------|------|
300
+ | `useDebounce(value, delay=300)` | 防抖 ref;返回 `Readonly<Ref<T>>` |
301
+ | `useClickOutside(target, handler)` | 点击元素外部触发 |
302
+ | `useEventListener(target, event, handler, options?)` | 自动绑/解绑 |
303
+ | `useThrottle(fn, ms)` | 节流函数 |
304
+ | `useWindowSize()` | `{ width, height }` |
305
+ | `useMediaQuery(query)` | `useMediaQuery('(max-width: 768px)')` |
306
+ | `useIntersectionObserver(target, cb)` | 监听元素出现/消失 |
307
+ | `useResizeObserver(target, cb)` | 监听元素尺寸变化 |
308
+ | `useFormPersist(key, initialValue, options?)` | 表单刷新自动回填;默认排除敏感字段 |
309
+
310
+ **useFormPersist**
311
+
312
+ ```ts
313
+ const { form, reset, clear } = useFormPersist('register-form', {
314
+ email: '', code: '', remark: '',
315
+ })
316
+
317
+ // 选项
318
+ interface UseFormPersistOptions<T> {
319
+ exclude?: (keyof T)[] // 追加敏感字段
320
+ storage?: 'session' | 'local' // 默认 'session'
321
+ disableDefaultExclude?: boolean // 默认 false(开启内置排除)
322
+ }
323
+
324
+ // 提交成功后清空残留
325
+ await api.register(form.value)
326
+ reset()
327
+ ```
328
+
329
+ 内置 `DEFAULT_SENSITIVE_FIELDS` 包含 password / confirmPassword / oldPassword / newPassword / pwd / code / captcha / verifyCode / verificationCode / smsCode / emailCode / phoneCode / otp / token / accessToken / refreshToken / signSecret / secret / apiKey 共 19 项,不区分大小写。
330
+
331
+ ## 何时读取 references
332
+
333
+ 本 SKILL.md 正文已包含 10 个模块的核心 API、签名和典型用法,满足 90% 编码场景。`references/` 目录保存边缘细节,仅在需要时读取:
334
+
335
+ | 文件 | 内容 |
336
+ |------|------|
337
+ | `references/request.md` | `HttpPlugin` 完整接口、`HttpError` 字段详情、refreshTokenPlugin 完整参数、`CommonBusinessCode` 枚举、非标准响应兜底行为 |
338
+ | `references/data.md` | `StringifyOptions` / `ParseOptions` 完整字段、`CookieOptions` 完整字段、useStorage 全部选项 |
339
+ | `references/form.md` | validators 底层正则规则、masking 边界处理说明 |
340
+ | `references/ui.md` | `FileRequester` 完整契约、`DownloadOptions` 全部字段、旧版迁移陷阱 |
72
341
 
73
342
  ## v0.6.0 迁移要点
74
343