create-dwy 0.6.3 → 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.3",
3
+ "version": "0.6.4",
4
4
  "description": "项目脚手架与 Claude Code 配置同步工具",
5
5
  "type": "module",
6
6
  "bin": {
package/src/sync.js CHANGED
@@ -260,11 +260,11 @@ export async function syncClaude() {
260
260
  }
261
261
 
262
262
  /**
263
- * 单独同步 CLAUDE.md 到当前项目 .claude/
263
+ * 单独同步 CLAUDE.md 到全局 ~/.claude/
264
264
  */
265
265
  export async function syncProjectClaudeMd() {
266
266
  const sourceDir = await resolveSourceDir()
267
- const targetDir = path.join(process.cwd(), '.claude')
267
+ const targetDir = path.join(os.homedir(), '.claude')
268
268
 
269
269
  if (!await fs.pathExists(sourceDir)) {
270
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
 
@@ -111,3 +111,244 @@ try {
111
111
  ```
112
112
 
113
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)**
217
+
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
+ ```
266
+
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` 全部字段、旧版迁移陷阱 |
341
+
342
+ ## v0.6.0 迁移要点
343
+
344
+ | 旧 | 新 |
345
+ |----|----|
346
+ | `createRequest(): AxiosInstance` | `createRequest(): HttpClient` |
347
+ | `RequestPlugin` | `HttpPlugin` |
348
+ | `import { dayjs } from '@dwydev/ekit'` | 用 `now / formatTimestamp / formatInTimezone / formatDateTime` 等函数 |
349
+ | `CookieAttributes` | `CookieOptions` |
350
+ | `IStringifyOptions / IParseOptions` | `StringifyOptions / ParseOptions` |
351
+ | `DownloadOptions.requestInstance: AxiosInstance` | `DownloadOptions.requester: FileRequester` |
352
+ | 自写 `useDebounce` 返回可写 `Ref` | VueUse `refDebounced` 返回 `Readonly<Ref>` |
353
+ | 自写 `useClipboard` 返回 `{ text, copy, copied, isSupported }`(isSupported 是 boolean) | VueUse 版(isSupported 是 `ComputedRef<boolean>`) |
354
+ | `refreshTokenPlugin` 自动重放 | 必须传 `retry: (cfg) => client.request(cfg)` |