paperlamp 0.0.0-stage → 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 (4) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +12 -2
  3. package/dist/index.js +512 -0
  4. package/package.json +31 -4
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 peach0x33a
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,13 @@
1
- # Temporary Holding Version
1
+ # paperlamp
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ 纸灯 / Paperlamp 的命令行工具:把组件源码复制进你的 Vue 3 + Tailwind CSS v4 项目,源码归你所有、随你修改。
4
+
5
+ 文档与组件一览:<https://peach0x33a.github.io/paperlamp/>
6
+
7
+ ```bash
8
+ npx paperlamp@latest init # 写入主题、cn() 工具与 paperlamp.json,并安装依赖
9
+ npx paperlamp@latest add button card # 复制组件及其依赖的组件
10
+ npx paperlamp@latest list # 列出全部组件
11
+ ```
12
+
13
+ 在文档站的「构建」页调好基色、主题色、字体与圆角后,用 `init --preset <code>` 应用。
package/dist/index.js ADDED
@@ -0,0 +1,512 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { spawnSync } from "node:child_process";
5
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
6
+ import { dirname, join, relative, resolve } from "node:path";
7
+ import process from "node:process";
8
+ // package.json
9
+ var package_default = {
10
+ name: "paperlamp",
11
+ version: "0.1.1",
12
+ description: "纸灯 / Paperlamp CLI — copy Paperlamp Vue components into your project",
13
+ license: "MIT",
14
+ type: "module",
15
+ bin: {
16
+ paperlamp: "dist/index.js"
17
+ },
18
+ files: [
19
+ "dist"
20
+ ],
21
+ engines: {
22
+ node: ">=18.17"
23
+ },
24
+ keywords: [
25
+ "vue",
26
+ "components",
27
+ "tailwindcss",
28
+ "reka-ui",
29
+ "design-system",
30
+ "cli"
31
+ ],
32
+ repository: {
33
+ type: "git",
34
+ url: "git+https://github.com/peach0x33a/paperlamp.git",
35
+ directory: "packages/cli"
36
+ },
37
+ scripts: {
38
+ build: "bun build src/index.ts --target node --outfile dist/index.js"
39
+ },
40
+ homepage: "https://peach0x33a.github.io/paperlamp/"
41
+ };
42
+
43
+ // src/preset.ts
44
+ var RADII = [
45
+ { name: "none", label: "直角", value: "0px" },
46
+ { name: "small", label: "小", value: "4px" },
47
+ { name: "default", label: "默认", value: "8px" },
48
+ { name: "medium", label: "中", value: "10px" },
49
+ { name: "large", label: "大", value: "14px" }
50
+ ];
51
+ var scale = (s) => s.split(" ");
52
+ var [s50, s100, s200, , s400, s500, , , s800, s900, s950] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
53
+ var BASES = [
54
+ { name: "paper", label: "纸白", swatch: "#2C2C2C", scale: null },
55
+ { name: "zinc", label: "锌灰", swatch: "#71717A", scale: scale("#FAFAFA #F4F4F5 #E4E4E7 #D4D4D8 #A1A1AA #71717A #52525B #3F3F46 #27272A #18181B #09090B") },
56
+ { name: "stone", label: "石色", swatch: "#78716C", scale: scale("#FAFAF9 #F5F5F4 #E7E5E4 #D6D3D1 #A8A29E #78716C #57534E #44403C #292524 #1C1917 #0C0A09") },
57
+ { name: "slate", label: "板岩", swatch: "#64748B", scale: scale("#F8FAFC #F1F5F9 #E2E8F0 #CBD5E1 #94A3B8 #64748B #475569 #334155 #1E293B #0F172A #020617") },
58
+ { name: "neutral", label: "中性", swatch: "#737373", scale: scale("#FAFAFA #F5F5F5 #E5E5E5 #D4D4D4 #A3A3A3 #737373 #525252 #404040 #262626 #171717 #0A0A0A") }
59
+ ];
60
+ var THEMES = [
61
+ { name: "ink", label: "墨色", swatch: "#2C2C2C", light: null, dark: null },
62
+ { name: "indigo", label: "靛蓝", swatch: "#4F46E5", light: ["#4F46E5", "#FFFFFF"], dark: ["#818CF8", "#1E1B4B"] },
63
+ { name: "blue", label: "蓝", swatch: "#2563EB", light: ["#2563EB", "#FFFFFF"], dark: ["#60A5FA", "#172554"] },
64
+ { name: "teal", label: "青绿", swatch: "#0D9488", light: ["#0D9488", "#FFFFFF"], dark: ["#2DD4BF", "#042F2E"] },
65
+ { name: "green", label: "绿", swatch: "#16A34A", light: ["#16A34A", "#FFFFFF"], dark: ["#4ADE80", "#052E16"] },
66
+ { name: "amber", label: "琥珀", swatch: "#F59E0B", light: ["#F59E0B", "#451A03"], dark: ["#FBBF24", "#451A03"] },
67
+ { name: "rose", label: "玫红", swatch: "#E11D48", light: ["#E11D48", "#FFFFFF"], dark: ["#FB7185", "#4C0519"] },
68
+ { name: "violet", label: "紫", swatch: "#7C3AED", light: ["#7C3AED", "#FFFFFF"], dark: ["#A78BFA", "#2E1065"] }
69
+ ];
70
+ var FONTS = [
71
+ { name: "wenkai", label: "霞鹜文楷", value: null },
72
+ { name: "sans", label: "黑体", value: `system-ui, -apple-system, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "Noto Sans SC", sans-serif` },
73
+ { name: "song", label: "宋体", value: `"Noto Serif SC", "Source Han Serif SC", "Songti SC", STSong, "SimSun", serif` }
74
+ ];
75
+ var DEFAULT_PRESET = { radius: "default", base: "paper", theme: "ink", font: "wenkai" };
76
+ var VERSION = "a";
77
+ var FIELDS = [
78
+ { key: "radius", options: RADII.map((r) => r.name) },
79
+ { key: "base", options: BASES.map((b) => b.name) },
80
+ { key: "theme", options: THEMES.map((t) => t.name) },
81
+ { key: "font", options: FONTS.map((f) => f.name) }
82
+ ];
83
+ function encodePreset(p) {
84
+ return VERSION + FIELDS.map((f) => Math.max(0, f.options.indexOf(p[f.key])).toString(36)).join("");
85
+ }
86
+ function decodePreset(code) {
87
+ const c = code.trim().replace(/^--preset[=\s]+/, "");
88
+ if (!c.startsWith(VERSION) || !/^[a-z][0-9a-z]*$/.test(c) || c.length > FIELDS.length + 1)
89
+ return null;
90
+ const out = { ...DEFAULT_PRESET };
91
+ for (const [i, f] of FIELDS.entries()) {
92
+ const ch = c[i + 1];
93
+ if (ch === undefined)
94
+ continue;
95
+ const opt = f.options[parseInt(ch, 36)];
96
+ if (!opt)
97
+ return null;
98
+ out[f.key] = opt;
99
+ }
100
+ return out;
101
+ }
102
+ var radiusValue = (name) => RADII.find((r) => r.name === name).value;
103
+ function presetVars(p) {
104
+ const light = {};
105
+ const dark = {};
106
+ if (p.radius !== DEFAULT_PRESET.radius)
107
+ light["--radius"] = radiusValue(p.radius);
108
+ const font = FONTS.find((f) => f.name === p.font)?.value;
109
+ if (font)
110
+ light["--font-serif"] = font;
111
+ const sc = BASES.find((b) => b.name === p.base)?.scale;
112
+ if (sc) {
113
+ const wash = (c, pct) => `color-mix(in oklab, ${c} ${pct}%, transparent)`;
114
+ const darkBg = `color-mix(in oklab, ${sc[s900]}, ${sc[s950]})`;
115
+ Object.assign(light, {
116
+ "--background": sc[s100],
117
+ "--foreground": sc[s800],
118
+ "--card": "#FFFFFF",
119
+ "--card-foreground": sc[s800],
120
+ "--popover": "#FFFFFF",
121
+ "--popover-foreground": sc[s800],
122
+ "--primary": sc[s800],
123
+ "--primary-hover": wash(sc[s800], 90),
124
+ "--primary-foreground": sc[s50],
125
+ "--secondary": wash(sc[s200], 60),
126
+ "--secondary-foreground": sc[s800],
127
+ "--muted": wash(sc[s200], 40),
128
+ "--muted-foreground": sc[s500],
129
+ "--accent": wash(sc[s200], 50),
130
+ "--accent-foreground": sc[s800],
131
+ "--border": sc[s200],
132
+ "--input": sc[s200],
133
+ "--ring": sc[s800],
134
+ "--overlay": wash(sc[s100], 80)
135
+ });
136
+ Object.assign(dark, {
137
+ "--background": darkBg,
138
+ "--foreground": sc[s200],
139
+ "--card": sc[s900],
140
+ "--card-foreground": sc[s200],
141
+ "--popover": sc[s900],
142
+ "--popover-foreground": sc[s200],
143
+ "--primary": sc[s200],
144
+ "--primary-hover": wash(sc[s200], 90),
145
+ "--primary-foreground": sc[s950],
146
+ "--secondary": wash(sc[s800], 60),
147
+ "--secondary-foreground": sc[s200],
148
+ "--muted": wash(sc[s800], 40),
149
+ "--muted-foreground": sc[s400],
150
+ "--accent": wash(sc[s800], 50),
151
+ "--accent-foreground": sc[s200],
152
+ "--border": sc[s800],
153
+ "--input": sc[s800],
154
+ "--ring": sc[s200],
155
+ "--overlay": wash(darkBg, 80)
156
+ });
157
+ }
158
+ const theme = THEMES.find((t) => t.name === p.theme);
159
+ if (theme.light && theme.dark) {
160
+ for (const [vars, [c, on]] of [[light, theme.light], [dark, theme.dark]]) {
161
+ Object.assign(vars, {
162
+ "--primary": c,
163
+ "--primary-hover": `color-mix(in oklab, ${c} 90%, transparent)`,
164
+ "--primary-foreground": on,
165
+ "--ring": c
166
+ });
167
+ }
168
+ }
169
+ return { light, dark };
170
+ }
171
+ function applyPreset(css, p, radius) {
172
+ const { light, dark } = presetVars(p);
173
+ if (radius)
174
+ light["--radius"] = radius;
175
+ const patch = (block, vars) => Object.entries(vars).reduce((b, [k, v]) => b.replace(new RegExp(`(\\n\\s*${k}:)\\s*[^;]+;`), `$1 ${v};`), block);
176
+ return css.replace(/:root\s*\{[^}]*\}/, (b) => patch(b, light)).replace(/\.dark\s*\{[^}]*\}/, (b) => patch(b, dark));
177
+ }
178
+
179
+ // src/index.ts
180
+ var CONFIG_FILE = "paperlamp.json";
181
+ var DEFAULT_REGISTRY = "https://peach0x33a.github.io/paperlamp/r";
182
+ var CATEGORY_LABELS = {
183
+ general: "通用",
184
+ layout: "布局",
185
+ navigation: "导航",
186
+ form: "数据录入",
187
+ data: "数据展示",
188
+ feedback: "反馈",
189
+ other: "其他"
190
+ };
191
+ var tty = process.stdout.isTTY && !process.env.NO_COLOR;
192
+ var paint = (code) => (s) => tty ? `\x1B[${code}m${s}\x1B[0m` : s;
193
+ var dim = paint(2);
194
+ var bold = paint(1);
195
+ var green = paint(32);
196
+ var yellow = paint(33);
197
+ var red = paint(31);
198
+
199
+ class CliError extends Error {
200
+ }
201
+ var fail = (msg) => {
202
+ throw new CliError(msg);
203
+ };
204
+ function parseArgs(argv) {
205
+ const out = { _: [], flags: {} };
206
+ const bool = new Set(["all", "overwrite", "yes", "no-install", "help", "version", "h", "v"]);
207
+ for (let i = 0;i < argv.length; i++) {
208
+ const a = argv[i];
209
+ if (a.startsWith("--")) {
210
+ const [k, v] = a.slice(2).split("=", 2);
211
+ if (v !== undefined)
212
+ out.flags[k] = v;
213
+ else if (bool.has(k) || argv[i + 1] === undefined || argv[i + 1].startsWith("-"))
214
+ out.flags[k] = true;
215
+ else
216
+ out.flags[k] = argv[++i];
217
+ } else if (a.startsWith("-") && a.length === 2) {
218
+ out.flags[a.slice(1)] = true;
219
+ } else {
220
+ out._.push(a);
221
+ }
222
+ }
223
+ return out;
224
+ }
225
+ var str = (v) => typeof v === "string" ? v : undefined;
226
+ function readJson(file) {
227
+ if (!existsSync(file))
228
+ return null;
229
+ try {
230
+ return JSON.parse(readFileSync(file, "utf8"));
231
+ } catch {
232
+ return fail(`${file} 不是合法的 JSON`);
233
+ }
234
+ }
235
+ function write(file, content) {
236
+ mkdirSync(dirname(file), { recursive: true });
237
+ writeFileSync(file, content);
238
+ }
239
+ function packageManager(cwd) {
240
+ if (existsSync(join(cwd, "bun.lock")) || existsSync(join(cwd, "bun.lockb")))
241
+ return { name: "bun", add: ["add"] };
242
+ if (existsSync(join(cwd, "pnpm-lock.yaml")))
243
+ return { name: "pnpm", add: ["add"] };
244
+ if (existsSync(join(cwd, "yarn.lock")))
245
+ return { name: "yarn", add: ["add"] };
246
+ return { name: "npm", add: ["install"] };
247
+ }
248
+ function install(cwd, specs, skip) {
249
+ const pj = readJson(join(cwd, "package.json"));
250
+ const have = new Set(Object.keys({ ...pj?.dependencies, ...pj?.devDependencies }));
251
+ const nameOf = (s) => s.replace(/(?<=.)@[^@]*$/, "");
252
+ const missing = [...new Set(specs)].filter((s) => !have.has(nameOf(s))).sort();
253
+ if (!missing.length)
254
+ return;
255
+ const pm = packageManager(cwd);
256
+ if (skip) {
257
+ console.log(`${yellow("!")} 跳过依赖安装,请手动运行:${pm.name} ${pm.add.join(" ")} ${missing.join(" ")}`);
258
+ return;
259
+ }
260
+ console.log(`${dim("›")} ${pm.name} ${pm.add.join(" ")} ${missing.join(" ")}`);
261
+ const r = spawnSync(pm.name, [...pm.add, ...missing], { cwd, stdio: "inherit", shell: process.platform === "win32" });
262
+ if (r.status !== 0)
263
+ fail(`依赖安装失败(${pm.name} 退出码 ${r.status ?? "?"}),可以手动重试上面的命令`);
264
+ }
265
+ async function fetchItem(base, name) {
266
+ const url = `${base}/${name}.json`;
267
+ let res;
268
+ try {
269
+ res = await fetch(url);
270
+ } catch (e) {
271
+ return fail(`连不上 registry:${url}
272
+ ${e.message}`);
273
+ }
274
+ if (res.status === 404 || res.ok && !isJson(res))
275
+ return fail(`registry 里没有「${name}」。运行 paperlamp list 查看可用组件`);
276
+ if (!res.ok)
277
+ return fail(`读取 ${url} 失败:HTTP ${res.status}`);
278
+ return await res.json();
279
+ }
280
+ var isJson = (res) => (res.headers.get("content-type") ?? "").includes("json");
281
+ async function resolveTree(base, names) {
282
+ const seen = new Map;
283
+ const order = [];
284
+ async function visit(name, path) {
285
+ if (path.includes(name))
286
+ return;
287
+ if (seen.has(name))
288
+ return;
289
+ const item = await fetchItem(base, name);
290
+ seen.set(name, item);
291
+ for (const d of item.registryDependencies)
292
+ await visit(d, [...path, name]);
293
+ order.push(item);
294
+ }
295
+ for (const n of names)
296
+ await visit(n, []);
297
+ return order;
298
+ }
299
+ var normalizeRegistry = (url) => {
300
+ if (!/^https?:\/\//.test(url))
301
+ fail(`registry 地址需要以 http:// 或 https:// 开头:${url}`);
302
+ return url.replace(/\/+$/, "").replace(/\/index\.json$/, "");
303
+ };
304
+ function loadConfig(cwd) {
305
+ const c = readJson(join(cwd, CONFIG_FILE));
306
+ if (!c)
307
+ fail(`当前目录没有 ${CONFIG_FILE}。先运行 paperlamp init`);
308
+ return c;
309
+ }
310
+ function defaultPaths(cwd) {
311
+ const nuxt = ["nuxt.config.ts", "nuxt.config.js", "nuxt.config.mjs"].some((f) => existsSync(join(cwd, f)));
312
+ const base = nuxt && !existsSync(join(cwd, "src")) ? "" : "src/";
313
+ return { ui: `${base}components/ui`, lib: `${base}lib` };
314
+ }
315
+ var CSS_CANDIDATES = [
316
+ "src/style.css",
317
+ "src/styles.css",
318
+ "src/index.css",
319
+ "src/main.css",
320
+ "src/assets/main.css",
321
+ "src/assets/css/main.css",
322
+ "app/assets/css/main.css",
323
+ "assets/css/main.css"
324
+ ];
325
+ function hasAtAlias(cwd) {
326
+ for (const f of ["tsconfig.json", "tsconfig.app.json", "jsconfig.json"]) {
327
+ const p = join(cwd, f);
328
+ if (existsSync(p) && readFileSync(p, "utf8").includes('"@/*"'))
329
+ return true;
330
+ }
331
+ return existsSync(join(cwd, "nuxt.config.ts")) || existsSync(join(cwd, "nuxt.config.js"));
332
+ }
333
+ function presetFrom(flags) {
334
+ const presetFlag = str(flags.preset);
335
+ if (presetFlag) {
336
+ const p = decodePreset(presetFlag);
337
+ if (!p)
338
+ fail(`无法识别的 preset:${presetFlag}`);
339
+ return { preset: p, radius: radiusValue(p.radius), code: encodePreset(p) };
340
+ }
341
+ const r = str(flags.radius);
342
+ if (!r)
343
+ return { preset: DEFAULT_PRESET, radius: radiusValue(DEFAULT_PRESET.radius), code: encodePreset(DEFAULT_PRESET) };
344
+ const named = RADII.find((x) => x.name === r);
345
+ if (named) {
346
+ const p = { ...DEFAULT_PRESET, radius: named.name };
347
+ return { preset: p, radius: named.value, code: encodePreset(p) };
348
+ }
349
+ if (/^\d+(\.\d+)?(px|rem|em)$|^0$/.test(r))
350
+ return { preset: DEFAULT_PRESET, radius: r };
351
+ return fail(`--radius 只接受 ${RADII.map((x) => x.name).join(" / ")},或一个长度值(如 6px、0.5rem)`);
352
+ }
353
+ function ensureImports(file, imports) {
354
+ const wanted = imports.map((i) => `@import "${i}";`);
355
+ const body = existsSync(file) ? readFileSync(file, "utf8") : "";
356
+ const rest = body.split(/\r?\n/).filter((line) => !wanted.includes(line.trim().replace(/'/g, '"'))).join(`
357
+ `).replace(/^\s+/, "");
358
+ write(file, `${wanted.join(`
359
+ `)}
360
+ ${rest ? `
361
+ ${rest}` : ""}`);
362
+ }
363
+ async function init(cwd, flags) {
364
+ if (!existsSync(join(cwd, "package.json")))
365
+ fail("当前目录没有 package.json,请在项目根目录运行");
366
+ const existing = readJson(join(cwd, CONFIG_FILE));
367
+ const registry = normalizeRegistry(str(flags.registry) ?? process.env.PAPERLAMP_REGISTRY ?? existing?.registry ?? DEFAULT_REGISTRY);
368
+ const cssFile = str(flags.css) ?? existing?.css ?? CSS_CANDIDATES.find((f) => existsSync(join(cwd, f))) ?? "src/style.css";
369
+ const createdCss = !existsSync(join(cwd, cssFile));
370
+ const { preset: chosen, radius, code: preset } = presetFrom(flags);
371
+ const config = {
372
+ registry,
373
+ css: cssFile,
374
+ radius,
375
+ ...preset ? { preset } : {},
376
+ paths: existing?.paths ?? defaultPaths(cwd),
377
+ aliases: existing?.aliases ?? { ui: "@/components/ui", lib: "@/lib" }
378
+ };
379
+ const [style, ...rest] = await resolveTree(registry, ["paperlamp"]).then((items) => {
380
+ const s = items.find((i) => i.type === "style");
381
+ if (!s)
382
+ fail("registry 里缺少主题条目 paperlamp");
383
+ return [s, ...items.filter((i) => i !== s)];
384
+ });
385
+ const themeFile = join(dirname(join(cwd, cssFile)), "paperlamp.css");
386
+ write(themeFile, applyPreset(style.files[0].content, chosen, radius));
387
+ let themeImport = relative(dirname(join(cwd, cssFile)), themeFile).replace(/\\/g, "/");
388
+ if (!themeImport.startsWith("."))
389
+ themeImport = `./${themeImport}`;
390
+ ensureImports(join(cwd, cssFile), [...style.cssImports ?? [], themeImport]);
391
+ const written = writeItems(cwd, config, rest, Boolean(flags.overwrite));
392
+ write(join(cwd, CONFIG_FILE), `${JSON.stringify(config, null, 2)}
393
+ `);
394
+ console.log(`${green("✓")} ${"主题".padEnd(14)}${relative(cwd, themeFile)} ${dim(preset ? `preset ${preset}` : `--radius: ${radius}`)}`);
395
+ console.log(`${green("✓")} ${"样式".padEnd(14)}${cssFile}${createdCss ? dim("(新建)") : ""}`);
396
+ for (const w of written)
397
+ console.log(w);
398
+ console.log(`${green("✓")} ${"配置".padEnd(14)}${CONFIG_FILE}`);
399
+ install(cwd, [...style.dependencies, ...rest.flatMap((i) => i.dependencies)], Boolean(flags["no-install"]));
400
+ if (createdCss)
401
+ console.log(`${yellow("!")} 新建了 ${cssFile},记得在入口文件里 import 它`);
402
+ if (!hasAtAlias(cwd))
403
+ console.log(`${yellow("!")} 没找到 "@/*" 路径别名。组件用 @/components/ui 与 @/lib 互相引用,请在 tsconfig 与 vite.config 里把 @ 指向 src`);
404
+ console.log(`
405
+ 现在可以添加组件:${bold("npx paperlamp add button")}`);
406
+ }
407
+ function rewriteImports(content, aliases) {
408
+ return content.replace(/(["'])@\/components\/ui\//g, `$1${aliases.ui}/`).replace(/(["'])@\/lib\//g, `$1${aliases.lib}/`);
409
+ }
410
+ function writeItems(cwd, config, items, overwrite) {
411
+ const lines = [];
412
+ for (const item of items) {
413
+ if (item.type === "style")
414
+ continue;
415
+ let wrote = 0;
416
+ let kept = 0;
417
+ for (const f of item.files) {
418
+ const root = f.target === "lib" ? config.paths.lib : config.paths.ui;
419
+ const dest = join(cwd, root, f.path);
420
+ if (existsSync(dest) && !overwrite) {
421
+ kept++;
422
+ continue;
423
+ }
424
+ write(dest, rewriteImports(f.content, config.aliases));
425
+ wrote++;
426
+ }
427
+ const where = item.type === "lib" ? config.paths.lib : `${config.paths.ui}/${item.name}`;
428
+ if (wrote)
429
+ lines.push(`${green("✓")} ${item.name.padEnd(14)}${where}`);
430
+ else if (kept)
431
+ lines.push(`${dim("·")} ${item.name.padEnd(14)}${dim("已存在,跳过(--overwrite 覆盖)")}`);
432
+ }
433
+ return lines;
434
+ }
435
+ async function add(cwd, names, flags) {
436
+ const config = loadConfig(cwd);
437
+ const registry = normalizeRegistry(str(flags.registry) ?? config.registry);
438
+ if (flags.all) {
439
+ const index = await fetchIndex(registry);
440
+ names = index.items.filter((i) => i.type === "ui").map((i) => i.name);
441
+ }
442
+ if (!names.length)
443
+ fail("要添加哪个组件?例如 paperlamp add button tool-card");
444
+ const items = await resolveTree(registry, names);
445
+ const written = writeItems(cwd, config, items, Boolean(flags.overwrite));
446
+ for (const w of written)
447
+ console.log(w);
448
+ install(cwd, items.flatMap((i) => i.dependencies), Boolean(flags["no-install"]));
449
+ }
450
+ async function fetchIndex(registry) {
451
+ const res = await fetch(`${registry}/index.json`).catch((e) => fail(`连不上 registry:${registry}
452
+ ${e.message}`));
453
+ if (!res.ok || !isJson(res))
454
+ fail(`${registry} 不是纸灯 registry(${registry}/index.json 不存在)`);
455
+ return await res.json();
456
+ }
457
+ async function list(cwd, flags) {
458
+ const registry = normalizeRegistry(str(flags.registry) ?? readJson(join(cwd, CONFIG_FILE))?.registry ?? process.env.PAPERLAMP_REGISTRY ?? DEFAULT_REGISTRY);
459
+ const { items } = await fetchIndex(registry);
460
+ const groups = new Map;
461
+ for (const i of items.filter((x) => x.type === "ui")) {
462
+ const k = i.category ?? "other";
463
+ groups.set(k, [...groups.get(k) ?? [], i]);
464
+ }
465
+ const order = Object.keys(CATEGORY_LABELS);
466
+ const rank = (k) => order.includes(k) ? order.indexOf(k) : order.length;
467
+ for (const [cat, group] of [...groups].sort(([a], [b]) => rank(a) - rank(b))) {
468
+ console.log(bold(CATEGORY_LABELS[cat] ?? "其他"));
469
+ for (const i of group)
470
+ console.log(` ${i.name.padEnd(16)}${dim(i.title)}`);
471
+ }
472
+ }
473
+ var HELP = `${bold("paperlamp")} ${dim(package_default.version)} — 把纸灯组件源码复制进你的 Vue 项目
474
+
475
+ 用法
476
+ paperlamp init 写入主题、cn() 工具与 paperlamp.json
477
+ --registry <url> 组件来源(默认 ${DEFAULT_REGISTRY})
478
+ --preset <code> 使用 /create 构建器生成的 preset
479
+ --radius <name|长度> ${RADII.map((r) => r.name).join(" / ")},或 6px、0.5rem
480
+ --css <file> 主样式文件(默认自动查找,找不到则新建 src/style.css)
481
+ paperlamp add <组件...> 复制组件及其依赖的组件,并安装 npm 依赖
482
+ --all 添加全部组件
483
+ --overwrite 覆盖已存在的文件
484
+ paperlamp list 列出 registry 里的组件
485
+
486
+ 通用选项
487
+ --cwd <dir> 在指定目录运行
488
+ --no-install 只写文件,不安装依赖
489
+ -v, --version 显示版本
490
+ `;
491
+ async function main() {
492
+ const { _: [cmd, ...rest], flags } = parseArgs(process.argv.slice(2));
493
+ const cwd = resolve(str(flags.cwd) ?? process.cwd());
494
+ if (flags.version || flags.v)
495
+ return console.log(package_default.version);
496
+ if (!cmd || flags.help || flags.h || cmd === "help")
497
+ return console.log(HELP);
498
+ if (cmd === "init")
499
+ return init(cwd, flags);
500
+ if (cmd === "add")
501
+ return add(cwd, rest, flags);
502
+ if (cmd === "list" || cmd === "ls")
503
+ return list(cwd, flags);
504
+ fail(`未知命令:${cmd}。运行 paperlamp --help 查看用法`);
505
+ }
506
+ main().catch((e) => {
507
+ if (e instanceof CliError) {
508
+ console.error(`${red("✗")} ${e.message}`);
509
+ process.exit(1);
510
+ }
511
+ throw e;
512
+ });
package/package.json CHANGED
@@ -1,6 +1,33 @@
1
1
  {
2
2
  "name": "paperlamp",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.1",
4
+ "description": "纸灯 / Paperlamp CLI — copy Paperlamp Vue components into your project",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "bin": {
8
+ "paperlamp": "dist/index.js"
9
+ },
10
+ "files": [
11
+ "dist"
12
+ ],
13
+ "engines": {
14
+ "node": ">=18.17"
15
+ },
16
+ "keywords": [
17
+ "vue",
18
+ "components",
19
+ "tailwindcss",
20
+ "reka-ui",
21
+ "design-system",
22
+ "cli"
23
+ ],
24
+ "repository": {
25
+ "type": "git",
26
+ "url": "git+https://github.com/peach0x33a/paperlamp.git",
27
+ "directory": "packages/cli"
28
+ },
29
+ "scripts": {
30
+ "build": "bun build src/index.ts --target node --outfile dist/index.js"
31
+ },
32
+ "homepage": "https://peach0x33a.github.io/paperlamp/"
33
+ }