@properui/cli 0.1.0
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/LICENSE +21 -0
- package/README.md +112 -0
- package/dist/index.js +1699 -0
- package/package.json +59 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1699 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
|
|
6
|
+
// src/commands/add.ts
|
|
7
|
+
import path6 from "path";
|
|
8
|
+
|
|
9
|
+
// src/config.ts
|
|
10
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync } from "fs";
|
|
11
|
+
import path2 from "path";
|
|
12
|
+
|
|
13
|
+
// src/detect.ts
|
|
14
|
+
import { existsSync, readFileSync, statSync } from "fs";
|
|
15
|
+
import path from "path";
|
|
16
|
+
var FRAMEWORK_LABEL = {
|
|
17
|
+
"next-app": "Next.js (App Router)",
|
|
18
|
+
"next-pages": "Next.js (Pages Router)",
|
|
19
|
+
vite: "Vite",
|
|
20
|
+
remix: "Remix",
|
|
21
|
+
react: "React"
|
|
22
|
+
};
|
|
23
|
+
var isFile = (target) => existsSync(target) && statSync(target).isFile();
|
|
24
|
+
var isDir = (target) => existsSync(target) && statSync(target).isDirectory();
|
|
25
|
+
function parseJsonc(source) {
|
|
26
|
+
const withoutComments = source.replace(/\\"|"(?:\\"|[^"])*"|(\/\/.*$)|(\/\*[\s\S]*?\*\/)/gm, (match, lineComment, blockComment) => lineComment || blockComment ? "" : match).replace(/,(\s*[}\]])/g, "$1");
|
|
27
|
+
try {
|
|
28
|
+
return JSON.parse(withoutComments);
|
|
29
|
+
} catch {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function readJsonc(file) {
|
|
34
|
+
if (!isFile(file)) return null;
|
|
35
|
+
return parseJsonc(readFileSync(file, "utf8"));
|
|
36
|
+
}
|
|
37
|
+
function readPackageJson(cwd) {
|
|
38
|
+
return readJsonc(path.join(cwd, "package.json"));
|
|
39
|
+
}
|
|
40
|
+
function allDependencies(pkg) {
|
|
41
|
+
return { ...pkg?.dependencies, ...pkg?.devDependencies, ...pkg?.peerDependencies };
|
|
42
|
+
}
|
|
43
|
+
function tsConfigChain(cwd) {
|
|
44
|
+
const found = [];
|
|
45
|
+
const seen = /* @__PURE__ */ new Set();
|
|
46
|
+
const visit = (file, depth) => {
|
|
47
|
+
const resolved = path.resolve(file);
|
|
48
|
+
if (depth > 4 || seen.has(resolved)) return;
|
|
49
|
+
seen.add(resolved);
|
|
50
|
+
const config = readJsonc(resolved);
|
|
51
|
+
if (!config) return;
|
|
52
|
+
found.push({ file: resolved, config });
|
|
53
|
+
const dir = path.dirname(resolved);
|
|
54
|
+
if (config.extends && config.extends.startsWith(".")) {
|
|
55
|
+
const target = config.extends.endsWith(".json") ? config.extends : `${config.extends}.json`;
|
|
56
|
+
visit(path.join(dir, target), depth + 1);
|
|
57
|
+
}
|
|
58
|
+
for (const reference of config.references ?? []) {
|
|
59
|
+
const target = path.join(dir, reference.path);
|
|
60
|
+
visit(isDir(target) ? path.join(target, "tsconfig.json") : target, depth + 1);
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
visit(path.join(cwd, "tsconfig.json"), 0);
|
|
64
|
+
visit(path.join(cwd, "tsconfig.app.json"), 0);
|
|
65
|
+
visit(path.join(cwd, "jsconfig.json"), 0);
|
|
66
|
+
return found;
|
|
67
|
+
}
|
|
68
|
+
function detectAlias(cwd, srcDir) {
|
|
69
|
+
const candidates = [];
|
|
70
|
+
for (const { file, config } of tsConfigChain(cwd)) {
|
|
71
|
+
const paths = config.compilerOptions?.paths;
|
|
72
|
+
if (!paths) continue;
|
|
73
|
+
const configDir = path.dirname(file);
|
|
74
|
+
const baseUrl = config.compilerOptions?.baseUrl ?? ".";
|
|
75
|
+
for (const [pattern, targets] of Object.entries(paths)) {
|
|
76
|
+
const target = targets[0];
|
|
77
|
+
if (!pattern.endsWith("/*") || !target || !target.endsWith("/*")) continue;
|
|
78
|
+
candidates.push({
|
|
79
|
+
prefix: `${pattern.slice(0, -1)}`,
|
|
80
|
+
base: path.resolve(configDir, baseUrl, target.slice(0, -2))
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
const preferred = candidates.find((candidate) => candidate.prefix === "@/") ?? candidates[0];
|
|
85
|
+
if (preferred) return { ...preferred, declared: true };
|
|
86
|
+
return { prefix: "@/", base: path.join(cwd, srcDir ? "src" : "."), declared: false };
|
|
87
|
+
}
|
|
88
|
+
function detectFramework(cwd, deps) {
|
|
89
|
+
if (deps["@remix-run/react"] || deps["@react-router/dev"]) return "remix";
|
|
90
|
+
if (deps.next || isFile(path.join(cwd, "next.config.ts")) || isFile(path.join(cwd, "next.config.js")) || isFile(path.join(cwd, "next.config.mjs"))) {
|
|
91
|
+
if (isDir(path.join(cwd, "app")) || isDir(path.join(cwd, "src", "app"))) return "next-app";
|
|
92
|
+
if (isDir(path.join(cwd, "pages")) || isDir(path.join(cwd, "src", "pages"))) return "next-pages";
|
|
93
|
+
return "next-app";
|
|
94
|
+
}
|
|
95
|
+
const viteConfig = ["vite.config.ts", "vite.config.js", "vite.config.mts", "vite.config.mjs"].some((name) => isFile(path.join(cwd, name)));
|
|
96
|
+
if (deps.vite || viteConfig) return "vite";
|
|
97
|
+
return "react";
|
|
98
|
+
}
|
|
99
|
+
function majorVersion(range) {
|
|
100
|
+
if (!range) return null;
|
|
101
|
+
const match = /(\d+)\./.exec(range) ?? /(\d+)/.exec(range);
|
|
102
|
+
return match?.[1] ? Number(match[1]) : null;
|
|
103
|
+
}
|
|
104
|
+
function detectTailwindVersion(cwd, deps) {
|
|
105
|
+
const installed = readJsonc(path.join(cwd, "node_modules", "tailwindcss", "package.json"));
|
|
106
|
+
return majorVersion(installed?.version) ?? majorVersion(deps.tailwindcss);
|
|
107
|
+
}
|
|
108
|
+
var CSS_CANDIDATES = [
|
|
109
|
+
"app/globals.css",
|
|
110
|
+
"src/app/globals.css",
|
|
111
|
+
"src/styles/globals.css",
|
|
112
|
+
"styles/globals.css",
|
|
113
|
+
"src/index.css",
|
|
114
|
+
"src/main.css",
|
|
115
|
+
"src/App.css",
|
|
116
|
+
"src/global.css",
|
|
117
|
+
"app/global.css"
|
|
118
|
+
];
|
|
119
|
+
function detectCssFile(cwd) {
|
|
120
|
+
return CSS_CANDIDATES.find((candidate) => isFile(path.join(cwd, candidate))) ?? null;
|
|
121
|
+
}
|
|
122
|
+
function defaultCssFile(framework, srcDir) {
|
|
123
|
+
if (framework === "next-app") return srcDir ? "src/app/globals.css" : "app/globals.css";
|
|
124
|
+
if (framework === "next-pages") return srcDir ? "src/styles/globals.css" : "styles/globals.css";
|
|
125
|
+
return "src/index.css";
|
|
126
|
+
}
|
|
127
|
+
var LOCKFILES = [
|
|
128
|
+
["pnpm-lock.yaml", "pnpm"],
|
|
129
|
+
["bun.lockb", "bun"],
|
|
130
|
+
["bun.lock", "bun"],
|
|
131
|
+
["yarn.lock", "yarn"],
|
|
132
|
+
["package-lock.json", "npm"]
|
|
133
|
+
];
|
|
134
|
+
function detectPackageManager(cwd) {
|
|
135
|
+
let dir = path.resolve(cwd);
|
|
136
|
+
for (; ; ) {
|
|
137
|
+
for (const [lockfile, manager] of LOCKFILES) {
|
|
138
|
+
if (isFile(path.join(dir, lockfile))) return manager;
|
|
139
|
+
}
|
|
140
|
+
const parent = path.dirname(dir);
|
|
141
|
+
if (parent === dir) return "npm";
|
|
142
|
+
dir = parent;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
function detectProject(cwd, frameworkOverride2) {
|
|
146
|
+
const pkg = readPackageJson(cwd);
|
|
147
|
+
const deps = allDependencies(pkg);
|
|
148
|
+
const srcDir = isDir(path.join(cwd, "src"));
|
|
149
|
+
const alias = detectAlias(cwd, srcDir);
|
|
150
|
+
return {
|
|
151
|
+
cwd,
|
|
152
|
+
framework: frameworkOverride2 ?? detectFramework(cwd, deps),
|
|
153
|
+
typescript: isFile(path.join(cwd, "tsconfig.json")) || Boolean(deps.typescript),
|
|
154
|
+
srcDir,
|
|
155
|
+
aliasPrefix: alias.prefix,
|
|
156
|
+
aliasBase: alias.base,
|
|
157
|
+
aliasDeclared: alias.declared,
|
|
158
|
+
tailwindVersion: detectTailwindVersion(cwd, deps),
|
|
159
|
+
cssFile: detectCssFile(cwd),
|
|
160
|
+
packageManager: detectPackageManager(cwd)
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// src/config.ts
|
|
165
|
+
var CONFIG_FILE = "components.json";
|
|
166
|
+
var CONFIG_SCHEMA_URL = "https://properui.dev/schema.json";
|
|
167
|
+
function configPath(cwd) {
|
|
168
|
+
return path2.join(cwd, CONFIG_FILE);
|
|
169
|
+
}
|
|
170
|
+
function readConfig(cwd) {
|
|
171
|
+
const file = configPath(cwd);
|
|
172
|
+
if (!existsSync2(file)) return null;
|
|
173
|
+
const parsed = parseJsonc(readFileSync2(file, "utf8"));
|
|
174
|
+
if (!parsed?.aliases?.components) return null;
|
|
175
|
+
return parsed;
|
|
176
|
+
}
|
|
177
|
+
function writeConfig(cwd, config) {
|
|
178
|
+
const file = configPath(cwd);
|
|
179
|
+
writeFileSync(file, `${JSON.stringify(config, null, 2)}
|
|
180
|
+
`, "utf8");
|
|
181
|
+
return file;
|
|
182
|
+
}
|
|
183
|
+
function aliasPrefixOf(alias) {
|
|
184
|
+
const slash = alias.indexOf("/");
|
|
185
|
+
return slash === -1 ? `${alias}/` : alias.slice(0, slash + 1);
|
|
186
|
+
}
|
|
187
|
+
function aliasBaseDir(cwd, config) {
|
|
188
|
+
const prefix = aliasPrefixOf(config.aliases.components);
|
|
189
|
+
const detected = detectAlias(cwd, existsSync2(path2.join(cwd, "src")));
|
|
190
|
+
if (detected.declared && detected.prefix === prefix) return detected.base;
|
|
191
|
+
return path2.join(cwd, existsSync2(path2.join(cwd, "src")) ? "src" : ".");
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// src/deps.ts
|
|
195
|
+
import { spawnSync } from "child_process";
|
|
196
|
+
var ASSUMED = /* @__PURE__ */ new Set(["react", "react-dom", "next"]);
|
|
197
|
+
var INSTALL_SPECS = {
|
|
198
|
+
"@properui/icons": "@properui/icons@npm:@untitledui/icons@^0.0.22"
|
|
199
|
+
};
|
|
200
|
+
var installSpec = (name) => INSTALL_SPECS[name] ?? name;
|
|
201
|
+
function missingDependencies(cwd, required) {
|
|
202
|
+
const installed = allDependencies(readPackageJson(cwd));
|
|
203
|
+
return [...new Set(required)].filter((name) => !ASSUMED.has(name) && !installed[name]).sort();
|
|
204
|
+
}
|
|
205
|
+
function installCommand(manager, packages) {
|
|
206
|
+
const verb = manager === "npm" ? "install" : "add";
|
|
207
|
+
return `${manager} ${verb} ${packages.map(installSpec).join(" ")}`;
|
|
208
|
+
}
|
|
209
|
+
function installDependencies(cwd, manager, packages) {
|
|
210
|
+
const verb = manager === "npm" ? "install" : "add";
|
|
211
|
+
const command = installCommand(manager, packages);
|
|
212
|
+
const result = spawnSync(manager, [verb, ...packages.map(installSpec)], { cwd, stdio: "inherit" });
|
|
213
|
+
return { ok: result.status === 0, command };
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// src/files.ts
|
|
217
|
+
import { existsSync as existsSync3, mkdirSync, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
|
|
218
|
+
import path3 from "path";
|
|
219
|
+
var IMPORT_SPECIFIER = /((?:\bfrom\s+)|(?:\bimport\s+)|(?:\bimport\s*\(\s*)|(?:\brequire\s*\(\s*))(["'])([^"']+)\2/g;
|
|
220
|
+
function rewriteImports(content, aliasPrefix) {
|
|
221
|
+
if (aliasPrefix === "@/") return content;
|
|
222
|
+
return content.replace(
|
|
223
|
+
IMPORT_SPECIFIER,
|
|
224
|
+
(match, lead, quote, specifier) => specifier.startsWith("@/") ? `${lead}${quote}${aliasPrefix}${specifier.slice(2)}${quote}` : match
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
function resolveTarget(file, options) {
|
|
228
|
+
const target = file.target.replace(/^\.?\//, "");
|
|
229
|
+
if (options.pathOverride && target.startsWith("components/")) {
|
|
230
|
+
return path3.resolve(options.cwd, options.pathOverride, target.slice("components/".length));
|
|
231
|
+
}
|
|
232
|
+
return path3.resolve(options.aliasBase, target);
|
|
233
|
+
}
|
|
234
|
+
function writeSourceFile(file, content, options) {
|
|
235
|
+
const relative = path3.relative(options.cwd, file) || path3.basename(file);
|
|
236
|
+
const exists = existsSync3(file);
|
|
237
|
+
if (exists) {
|
|
238
|
+
const current = readFileSync3(file, "utf8");
|
|
239
|
+
if (current === content) return { file, relative, status: "unchanged" };
|
|
240
|
+
if (!options.overwrite) return { file, relative, status: "skipped" };
|
|
241
|
+
}
|
|
242
|
+
if (!options.dryRun) {
|
|
243
|
+
mkdirSync(path3.dirname(file), { recursive: true });
|
|
244
|
+
writeFileSync2(file, content, "utf8");
|
|
245
|
+
}
|
|
246
|
+
return { file, relative, status: exists ? "updated" : "created" };
|
|
247
|
+
}
|
|
248
|
+
function targetForLanguage(target, tsx) {
|
|
249
|
+
if (tsx) return target;
|
|
250
|
+
return target.replace(/\.tsx$/, ".jsx").replace(/\.ts$/, ".js");
|
|
251
|
+
}
|
|
252
|
+
function prepareFile(file, config, options) {
|
|
253
|
+
const aliasPrefix = aliasPrefixOf(config.aliases.components);
|
|
254
|
+
const adjusted = { ...file, target: targetForLanguage(file.target, config.tsx) };
|
|
255
|
+
return {
|
|
256
|
+
target: resolveTarget(adjusted, options),
|
|
257
|
+
content: rewriteImports(file.content, aliasPrefix)
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// src/fuzzy.ts
|
|
262
|
+
var normalize = (value) => value.toLowerCase().replace(/[\s\-_/.]+/g, "");
|
|
263
|
+
function fuzzyScore(haystack, query) {
|
|
264
|
+
const target = normalize(haystack);
|
|
265
|
+
const needle = normalize(query);
|
|
266
|
+
if (needle.length === 0) return 0;
|
|
267
|
+
if (target === needle) return 1;
|
|
268
|
+
const exact = target.indexOf(needle);
|
|
269
|
+
if (exact !== -1) return 0.9 - Math.min(exact, 40) / 200;
|
|
270
|
+
let cursor = 0;
|
|
271
|
+
let matched = 0;
|
|
272
|
+
let streak = 0;
|
|
273
|
+
let bestStreak = 0;
|
|
274
|
+
for (const character of needle) {
|
|
275
|
+
const found = target.indexOf(character, cursor);
|
|
276
|
+
if (found === -1) return 0;
|
|
277
|
+
streak = found === cursor ? streak + 1 : 1;
|
|
278
|
+
bestStreak = Math.max(bestStreak, streak);
|
|
279
|
+
cursor = found + 1;
|
|
280
|
+
matched += 1;
|
|
281
|
+
}
|
|
282
|
+
return 0.3 * (matched / needle.length) + 0.3 * (bestStreak / needle.length);
|
|
283
|
+
}
|
|
284
|
+
function editDistance(a, b) {
|
|
285
|
+
const left = normalize(a);
|
|
286
|
+
const right = normalize(b);
|
|
287
|
+
let previous = Array.from({ length: right.length + 1 }, (_, index) => index);
|
|
288
|
+
for (let i = 1; i <= left.length; i += 1) {
|
|
289
|
+
const current = [i];
|
|
290
|
+
for (let j = 1; j <= right.length; j += 1) {
|
|
291
|
+
const substitution = (previous[j - 1] ?? 0) + (left[i - 1] === right[j - 1] ? 0 : 1);
|
|
292
|
+
current[j] = Math.min((current[j - 1] ?? 0) + 1, (previous[j] ?? 0) + 1, substitution);
|
|
293
|
+
}
|
|
294
|
+
previous = current;
|
|
295
|
+
}
|
|
296
|
+
return previous[right.length] ?? Math.max(left.length, right.length);
|
|
297
|
+
}
|
|
298
|
+
function nearestNames(names, query, limit = 5) {
|
|
299
|
+
const tolerance = query.length <= 4 ? 1 : 2;
|
|
300
|
+
return names.map((name) => ({ name, score: fuzzyScore(name, query), distance: editDistance(name, query) })).filter((match) => match.score > 0.25 || match.distance <= tolerance).sort((a, b) => b.score - a.score || a.distance - b.distance || a.name.localeCompare(b.name)).slice(0, limit).map((match) => match.name);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// src/prompt.ts
|
|
304
|
+
import prompts from "prompts";
|
|
305
|
+
var CancelledError = class extends Error {
|
|
306
|
+
constructor() {
|
|
307
|
+
super("Cancelled.");
|
|
308
|
+
}
|
|
309
|
+
};
|
|
310
|
+
var canPrompt = () => Boolean(process.stdin.isTTY) && Boolean(process.stdout.isTTY) && !process.env.CI;
|
|
311
|
+
var onCancel = () => {
|
|
312
|
+
throw new CancelledError();
|
|
313
|
+
};
|
|
314
|
+
async function confirm(message, options) {
|
|
315
|
+
if (options.yes || !canPrompt()) return options.fallback;
|
|
316
|
+
const answer = await prompts({ type: "confirm", name: "value", message, initial: options.fallback }, { onCancel });
|
|
317
|
+
return Boolean(answer.value);
|
|
318
|
+
}
|
|
319
|
+
async function ask(message, options) {
|
|
320
|
+
if (options.yes || !canPrompt()) return options.initial;
|
|
321
|
+
const answer = await prompts({ type: "text", name: "value", message, initial: options.initial }, { onCancel });
|
|
322
|
+
return typeof answer.value === "string" && answer.value.trim() ? answer.value.trim() : options.initial;
|
|
323
|
+
}
|
|
324
|
+
async function askSecret(message) {
|
|
325
|
+
if (!canPrompt()) return "";
|
|
326
|
+
const answer = await prompts({ type: "password", name: "value", message }, { onCancel });
|
|
327
|
+
return typeof answer.value === "string" ? answer.value.trim() : "";
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// src/registry.ts
|
|
331
|
+
import { existsSync as existsSync5, readFileSync as readFileSync5, statSync as statSync2 } from "fs";
|
|
332
|
+
import path5 from "path";
|
|
333
|
+
|
|
334
|
+
// src/auth.ts
|
|
335
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
|
|
336
|
+
import { homedir } from "os";
|
|
337
|
+
import path4 from "path";
|
|
338
|
+
var authDir = () => path4.join(homedir(), ".properui");
|
|
339
|
+
var authFile = () => path4.join(authDir(), "auth.json");
|
|
340
|
+
function readAuth() {
|
|
341
|
+
const file = authFile();
|
|
342
|
+
if (!existsSync4(file)) return null;
|
|
343
|
+
try {
|
|
344
|
+
const parsed = JSON.parse(readFileSync4(file, "utf8"));
|
|
345
|
+
return typeof parsed?.token === "string" && parsed.token.length > 0 ? parsed : null;
|
|
346
|
+
} catch {
|
|
347
|
+
return null;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
var readAuthToken = () => process.env.PROPERUI_TOKEN ?? readAuth()?.token ?? null;
|
|
351
|
+
function writeAuth(auth) {
|
|
352
|
+
mkdirSync2(authDir(), { recursive: true, mode: 448 });
|
|
353
|
+
const file = authFile();
|
|
354
|
+
writeFileSync3(file, `${JSON.stringify({ ...auth, createdAt: auth.createdAt ?? (/* @__PURE__ */ new Date()).toISOString() }, null, 2)}
|
|
355
|
+
`, { mode: 384 });
|
|
356
|
+
return file;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// src/registry.ts
|
|
360
|
+
var DEFAULT_REGISTRY_URL = "https://properui.dev/r";
|
|
361
|
+
var RegistryError = class extends Error {
|
|
362
|
+
};
|
|
363
|
+
var isHttp = (source) => /^https?:\/\//i.test(source);
|
|
364
|
+
function resolveRegistrySource(flag, fromConfig) {
|
|
365
|
+
const source = flag ?? process.env.REGISTRY_URL ?? fromConfig ?? DEFAULT_REGISTRY_URL;
|
|
366
|
+
return isHttp(source) ? source.replace(/\/+$/, "") : path5.resolve(source);
|
|
367
|
+
}
|
|
368
|
+
var Registry = class {
|
|
369
|
+
source;
|
|
370
|
+
remote;
|
|
371
|
+
indexCache = null;
|
|
372
|
+
entryCache = /* @__PURE__ */ new Map();
|
|
373
|
+
constructor(source) {
|
|
374
|
+
this.source = source;
|
|
375
|
+
this.remote = isHttp(source);
|
|
376
|
+
}
|
|
377
|
+
describe() {
|
|
378
|
+
return this.remote ? this.source : `${this.source} (local)`;
|
|
379
|
+
}
|
|
380
|
+
async readJson(file) {
|
|
381
|
+
if (!this.remote) {
|
|
382
|
+
const full = path5.join(this.source, file);
|
|
383
|
+
if (!existsSync5(full) || !statSync2(full).isFile()) return null;
|
|
384
|
+
try {
|
|
385
|
+
return JSON.parse(readFileSync5(full, "utf8"));
|
|
386
|
+
} catch (error) {
|
|
387
|
+
throw new RegistryError(`${full} is not valid JSON: ${error.message}`);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
const url = `${this.source}/${file}`;
|
|
391
|
+
const token = readAuthToken();
|
|
392
|
+
let response;
|
|
393
|
+
try {
|
|
394
|
+
response = await fetch(url, { headers: token ? { authorization: `Bearer ${token}` } : {} });
|
|
395
|
+
} catch (error) {
|
|
396
|
+
throw new RegistryError(`Could not reach ${url} \u2014 ${error.message}`);
|
|
397
|
+
}
|
|
398
|
+
if (response.status === 404) return null;
|
|
399
|
+
if (response.status === 401 || response.status === 403) {
|
|
400
|
+
throw new RegistryError(`${url} requires authentication. Run \`properui login\` first.`);
|
|
401
|
+
}
|
|
402
|
+
if (!response.ok) throw new RegistryError(`${url} responded ${response.status} ${response.statusText}`);
|
|
403
|
+
return await response.json();
|
|
404
|
+
}
|
|
405
|
+
async index() {
|
|
406
|
+
if (this.indexCache) return this.indexCache;
|
|
407
|
+
const index = await this.readJson("index.json");
|
|
408
|
+
if (!index) throw new RegistryError(`No index.json at ${this.source}. Is the registry source correct?`);
|
|
409
|
+
this.indexCache = index.components ?? [];
|
|
410
|
+
return this.indexCache;
|
|
411
|
+
}
|
|
412
|
+
async find(name) {
|
|
413
|
+
return (await this.index()).find((entry) => entry.name === name);
|
|
414
|
+
}
|
|
415
|
+
async item(name) {
|
|
416
|
+
const cached = this.entryCache.get(name);
|
|
417
|
+
if (cached) return cached;
|
|
418
|
+
const entry = await this.readJson(`${name}.json`);
|
|
419
|
+
if (!entry) throw new RegistryError(`Unknown component "${name}". Run \`properui list\` to see what is available.`);
|
|
420
|
+
this.entryCache.set(name, entry);
|
|
421
|
+
return entry;
|
|
422
|
+
}
|
|
423
|
+
/**
|
|
424
|
+
* Resolves `names` plus every `registryDependencies` edge, depth-first, so that a
|
|
425
|
+
* dependency always appears before the entry that needs it.
|
|
426
|
+
*/
|
|
427
|
+
async resolveTree(names) {
|
|
428
|
+
const ordered = [];
|
|
429
|
+
const seen = /* @__PURE__ */ new Set();
|
|
430
|
+
const visit = async (name) => {
|
|
431
|
+
if (seen.has(name)) return;
|
|
432
|
+
seen.add(name);
|
|
433
|
+
const entry = await this.item(name);
|
|
434
|
+
for (const dependency of entry.registryDependencies) await visit(dependency);
|
|
435
|
+
ordered.push(entry);
|
|
436
|
+
};
|
|
437
|
+
for (const name of names) await visit(name);
|
|
438
|
+
return ordered;
|
|
439
|
+
}
|
|
440
|
+
};
|
|
441
|
+
|
|
442
|
+
// src/ui.ts
|
|
443
|
+
import kleur from "kleur";
|
|
444
|
+
import ora from "ora";
|
|
445
|
+
var isInteractive = () => Boolean(process.stdout.isTTY) && !process.env.CI;
|
|
446
|
+
var log = {
|
|
447
|
+
plain: (message = "") => console.log(message),
|
|
448
|
+
info: (message) => console.log(`${kleur.cyan("info")} ${message}`),
|
|
449
|
+
success: (message) => console.log(`${kleur.green("done")} ${message}`),
|
|
450
|
+
warn: (message) => console.log(`${kleur.yellow("warn")} ${message}`),
|
|
451
|
+
error: (message) => console.error(`${kleur.red("error")} ${message}`),
|
|
452
|
+
step: (message) => console.log(`${kleur.dim("\xB7")} ${message}`),
|
|
453
|
+
title: (message) => console.log(`
|
|
454
|
+
${kleur.bold(message)}`)
|
|
455
|
+
};
|
|
456
|
+
function spinner(text) {
|
|
457
|
+
if (!isInteractive()) {
|
|
458
|
+
let current = text;
|
|
459
|
+
return {
|
|
460
|
+
update: (next) => {
|
|
461
|
+
current = next;
|
|
462
|
+
},
|
|
463
|
+
succeed: (message) => log.success(message ?? current),
|
|
464
|
+
fail: (message) => log.error(message ?? current),
|
|
465
|
+
stop: () => void 0
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
const instance = ora({ text, spinner: "dots" }).start();
|
|
469
|
+
return {
|
|
470
|
+
update: (next) => {
|
|
471
|
+
instance.text = next;
|
|
472
|
+
},
|
|
473
|
+
succeed: (message) => {
|
|
474
|
+
instance.succeed(message);
|
|
475
|
+
},
|
|
476
|
+
fail: (message) => {
|
|
477
|
+
instance.fail(message);
|
|
478
|
+
},
|
|
479
|
+
stop: () => {
|
|
480
|
+
instance.stop();
|
|
481
|
+
}
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// src/commands/add.ts
|
|
486
|
+
var STATUS_ORDER = ["created", "updated", "skipped", "unchanged"];
|
|
487
|
+
var statusLabel = (status) => {
|
|
488
|
+
if (status === "created") return kleur.green("added ");
|
|
489
|
+
if (status === "updated") return kleur.yellow("update");
|
|
490
|
+
if (status === "skipped") return kleur.dim("skip ");
|
|
491
|
+
return kleur.dim("same ");
|
|
492
|
+
};
|
|
493
|
+
async function runAdd(names, options) {
|
|
494
|
+
const cwd = path6.resolve(options.cwd ?? process.cwd());
|
|
495
|
+
const config = readConfig(cwd);
|
|
496
|
+
if (!config) {
|
|
497
|
+
log.error(`No ${path6.relative(cwd, configPath(cwd)) || "components.json"} found. Run \`properui init\` first.`);
|
|
498
|
+
process.exitCode = 1;
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
const exampleMode = names[0] === "example";
|
|
502
|
+
const requested = exampleMode ? names.slice(1) : names;
|
|
503
|
+
const registry = new Registry(resolveRegistrySource(options.registry, config.registry));
|
|
504
|
+
if (!options.all && requested.length === 0) {
|
|
505
|
+
log.error(exampleMode ? "Which example? e.g. `properui add example settings-01`" : "Nothing to add. Pass component names or --all.");
|
|
506
|
+
process.exitCode = 1;
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
let targets;
|
|
510
|
+
try {
|
|
511
|
+
const index = await registry.index();
|
|
512
|
+
if (options.all) {
|
|
513
|
+
targets = index.filter((entry) => entry.type === "component").map((entry) => entry.name);
|
|
514
|
+
} else {
|
|
515
|
+
const known = new Set(index.map((entry) => entry.name));
|
|
516
|
+
const unknown = requested.filter((name) => !known.has(name));
|
|
517
|
+
if (unknown.length > 0) {
|
|
518
|
+
for (const name of unknown) {
|
|
519
|
+
const hints = nearestNames([...known], name);
|
|
520
|
+
log.error(`Unknown component "${name}".${hints.length > 0 ? ` Did you mean: ${hints.join(", ")}?` : ""}`);
|
|
521
|
+
}
|
|
522
|
+
process.exitCode = 1;
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
525
|
+
if (exampleMode) {
|
|
526
|
+
const notExamples = requested.filter((name) => index.find((entry) => entry.name === name)?.type !== "example");
|
|
527
|
+
for (const name of notExamples) log.warn(`"${name}" is not a page example \u2014 adding it as a component.`);
|
|
528
|
+
}
|
|
529
|
+
targets = requested;
|
|
530
|
+
}
|
|
531
|
+
} catch (error) {
|
|
532
|
+
log.error(error instanceof RegistryError ? error.message : error.message);
|
|
533
|
+
process.exitCode = 1;
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
536
|
+
const resolveSpinner = spinner(`Resolving ${targets.length} component${targets.length === 1 ? "" : "s"} from ${registry.describe()}`);
|
|
537
|
+
let entries;
|
|
538
|
+
try {
|
|
539
|
+
entries = await registry.resolveTree(targets);
|
|
540
|
+
resolveSpinner.succeed(`Resolved ${entries.length} registry item${entries.length === 1 ? "" : "s"} (including dependencies).`);
|
|
541
|
+
} catch (error) {
|
|
542
|
+
resolveSpinner.fail(error instanceof RegistryError ? error.message : error.message);
|
|
543
|
+
process.exitCode = 1;
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
546
|
+
const aliasBase = aliasBaseDir(cwd, config);
|
|
547
|
+
const resolveOptions = { cwd, aliasBase, pathOverride: options.path };
|
|
548
|
+
const writeOptions = { cwd, overwrite: Boolean(options.overwrite), dryRun: Boolean(options.dryRun) };
|
|
549
|
+
const results = [];
|
|
550
|
+
for (const entry of entries) {
|
|
551
|
+
const writes = entry.files.map((file) => {
|
|
552
|
+
const { target, content } = prepareFile(file, config, resolveOptions);
|
|
553
|
+
return writeSourceFile(target, content, writeOptions);
|
|
554
|
+
});
|
|
555
|
+
results.push({ entry, writes });
|
|
556
|
+
}
|
|
557
|
+
reportWrites(results, config, options);
|
|
558
|
+
const npmDependencies = missingDependencies(
|
|
559
|
+
cwd,
|
|
560
|
+
entries.flatMap((entry) => entry.dependencies)
|
|
561
|
+
);
|
|
562
|
+
if (npmDependencies.length > 0) {
|
|
563
|
+
const manager = detectPackageManager(cwd);
|
|
564
|
+
log.plain();
|
|
565
|
+
log.title("Dependencies");
|
|
566
|
+
for (const dependency of npmDependencies) log.step(`${kleur.cyan("need ")} ${dependency}`);
|
|
567
|
+
if (options.dryRun) {
|
|
568
|
+
log.info(`Dry run \u2014 would run: ${kleur.bold(installCommand(manager, npmDependencies))}`);
|
|
569
|
+
} else {
|
|
570
|
+
const shouldInstall = await confirm(
|
|
571
|
+
`Install ${npmDependencies.length} missing package${npmDependencies.length === 1 ? "" : "s"} with ${manager}?`,
|
|
572
|
+
{
|
|
573
|
+
yes: options.yes,
|
|
574
|
+
fallback: Boolean(options.yes)
|
|
575
|
+
}
|
|
576
|
+
);
|
|
577
|
+
if (shouldInstall) {
|
|
578
|
+
const { ok, command } = installDependencies(cwd, manager, npmDependencies);
|
|
579
|
+
if (ok) log.success(`Installed with \`${command}\`.`);
|
|
580
|
+
else log.error(`\`${command}\` failed \u2014 install the packages above manually.`);
|
|
581
|
+
} else {
|
|
582
|
+
log.info(`Skipped install. Run: ${kleur.bold(installCommand(manager, npmDependencies))}`);
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
function reportWrites(results, config, options) {
|
|
588
|
+
const all = results.flatMap((result) => result.writes);
|
|
589
|
+
const counts = Object.fromEntries(STATUS_ORDER.map((status) => [status, all.filter((write) => write.status === status).length]));
|
|
590
|
+
log.plain();
|
|
591
|
+
log.title(options.dryRun ? "Files (dry run \u2014 nothing was written)" : "Files");
|
|
592
|
+
for (const { entry, writes } of results) {
|
|
593
|
+
const changed = writes.some((write) => write.status === "created" || write.status === "updated");
|
|
594
|
+
log.plain(` ${changed ? kleur.bold(entry.name) : kleur.dim(entry.name)}`);
|
|
595
|
+
for (const write of writes) log.plain(` ${statusLabel(write.status)} ${write.relative}`);
|
|
596
|
+
}
|
|
597
|
+
const aliasPrefix = aliasPrefixOf(config.aliases.components);
|
|
598
|
+
log.plain();
|
|
599
|
+
if (aliasPrefix !== "@/") log.info(`Rewrote \`@/\` imports to \`${aliasPrefix}\`.`);
|
|
600
|
+
if (options.path) log.info(`Component files were placed under \`${options.path}\` \u2014 check the imports if that folder is outside your alias.`);
|
|
601
|
+
if (counts.created + counts.updated === 0) {
|
|
602
|
+
log.success(
|
|
603
|
+
counts.skipped > 0 ? `No changes \u2014 ${counts.skipped} file${counts.skipped === 1 ? "" : "s"} already exist. Pass --overwrite to replace them.` : "No changes \u2014 everything is already up to date."
|
|
604
|
+
);
|
|
605
|
+
return;
|
|
606
|
+
}
|
|
607
|
+
const parts = [`${counts.created} added`, `${counts.updated} updated`, `${counts.skipped} skipped`, `${counts.unchanged} unchanged`];
|
|
608
|
+
log.success(options.dryRun ? `Would apply: ${parts.join(", ")}.` : parts.join(", ") + ".");
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
// src/commands/agent.ts
|
|
612
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync3, readFileSync as readFileSync6, writeFileSync as writeFileSync4 } from "fs";
|
|
613
|
+
import path7 from "path";
|
|
614
|
+
|
|
615
|
+
// src/agent-templates.ts
|
|
616
|
+
var SKILL_MD = "---\nname: properui\ndescription: Use when building or editing UI screens, pages, forms, or components in a project that uses (or could use) Proper UI (@properui/ui) \u2014 before writing any new JSX/TSX markup by hand. Covers checking the registry for an existing component or full-page example, inspecting the project's Proper UI setup, installing with the CLI instead of hand-copying source, and the accessibility/token/RTL rules the installed code must keep. Trigger on \"build a settings page\", \"add a form\", \"make a dashboard\", \"add a button/modal/table\", or any request to create or modify UI in a React/Next.js/Vite project.\nlicense: MIT\n---\n\n# Proper UI\n\nProper UI (`@properui/ui`) is a registry of React Aria + Tailwind v4 components distributed as\nsource, not a runtime package you import blindly. The `properui` CLI copies the files you ask for\ninto the project and rewrites their imports to fit. Follow these steps, in order, every time UI work\ncomes up.\n\n## 1. Inspect the project first\n\nBefore adding or writing anything, run:\n\n```bash\nnpx @properui/cli@latest info --json\n```\n\nThis reports whether the project is already set up (framework, Tailwind version, `components.json`\naliases, theme CSS path, which registry entries are already installed, and the installed\n`@properui/ui` / `properui` versions). Read it before deciding anything else:\n\n- No `components.json` \u2192 run `npx @properui/cli@latest init -y` first. Do not hand-write\n `components.json`, `utils/cx.ts`, the theme token file, or the `ThemeProvider` wiring \u2014 `init`\n generates all of it correctly for the detected framework.\n- `components.json` exists \u2192 note the `aliases.components` value (often `@/components`, sometimes a\n project-specific prefix) and use it for every import you write by hand.\n- Tailwind is not v4 \u2192 `init` will refuse and print an upgrade path. Do not attempt to work around\n this by writing v3-style config.\n\n## 2. Search before creating\n\nNever write a component's markup from memory or invent your own version of something the registry\nalready has. Check first:\n\n```bash\nnpx @properui/cli@latest search \"<what you need>\" # fuzzy match over names, titles, examples\nnpx @properui/cli@latest list --layer base # browse by layer: base, application, marketing\nnpx @properui/cli@latest list --type example # full-page examples specifically\n```\n\nOnly write custom markup when the search genuinely comes up empty. If it does, still build the\ncustom piece out of already-installed primitives and the same semantic tokens (below) rather than\none-off styling.\n\n## 3. Prefer whole examples for whole screens\n\n- Building a recognizable whole screen (a settings page, a pricing page, an onboarding flow, a\n dashboard, an auth page)? Search `list --type example` / `search` for a matching full-page\n example first and install it with `add example <name>`. Adapt copy and data to the request; don't\n rebuild the layout from primitives when an example already covers it.\n- Building or fixing one isolated piece of behavior (a button variant, a single form field, a\n tooltip)? Install the specific primitive(s) with `add <name>` instead of pulling in a whole\n example.\n\n```bash\nnpx @properui/cli@latest add example settings-01\nnpx @properui/cli@latest add button input select\n```\n\n`add` resolves `registryDependencies` recursively (installing a component's own component\ndependencies), rewrites the library's internal `@/` imports to the project's configured alias, and\nreports missing npm packages to install \u2014 it does not silently run installs for you. A second `add`\nof the same name is a no-op unless you pass `--overwrite`; never pass `--overwrite` on top of a file\na human has since edited without checking `diff` first:\n\n```bash\nnpx @properui/cli@latest diff <name> # see local modifications before overwriting\n```\n\n## 4. Never mix component systems\n\nOnce a screen uses Proper UI components, keep using Proper UI components for the rest of that\nscreen \u2014 don't drop in a different UI library's `<Button>` or a hand-rolled equivalent alongside\ninstalled ones. If the project already has another design system in place, ask before introducing\nProper UI into it rather than mixing the two silently.\n\n## 5. Write code that matches the installed conventions\n\nEvery file `add` copies in already follows these rules. Any markup you write by hand \u2014 glue code, a\npage shell, a piece the registry doesn't have \u2014 must follow them too:\n\n- **React Aria props, not DOM props.** `onPress` not `onClick`, `isDisabled` not `disabled`,\n `isSelected` not `checked`. These components wrap React Aria Components; a DOM prop is silently\n ignored.\n- **Semantic tokens only \u2014 never a literal.** `bg-primary`, `text-tertiary`, `border-secondary`,\n `bg-brand-solid`. Never a raw palette class (`bg-purple-600`) and never an arbitrary value\n (`bg-[#7f56d9]`, `p-[13px]`). Typography is tokenised the same way: `text-display-lg`, `text-md`,\n not `text-4xl`. The full token set lives in the project's theme CSS file (path reported by\n `info --json`).\n- **No `dark:` utilities.** A `.dark-mode` class on an ancestor repoints every semantic token, so a\n component written against tokens is already correct in both themes. A `dark:` utility is a bug,\n not a stylistic choice.\n- **Logical properties for anything directional**, so `dir=\"rtl\"` keeps working: `ms-*`/`me-*` not\n `ml-*`/`mr-*`, `ps-*`/`pe-*` not `pl-*`/`pr-*`, `start-*`/`end-*` not `left-*`/`right-*`,\n `text-start` not `text-left`.\n- **Icons as component references.** `<Button iconLeading={ArrowRight}>`, not\n `<Button iconLeading={<ArrowRight />}>` \u2014 the component applies its own sizing and the `data-icon`\n attribute its styles target.\n- **Import from the component's subpath**, e.g. `@properui/ui/components/base/buttons/button`, so\n bundlers keep only what's used \u2014 never a barrel import of the whole library for one component.\n- **Preserve what's already there.** Keyboard interaction, focus order, ARIA attributes, and\n responsive breakpoints on installed components are load-bearing. When adapting a copied file,\n change content and composition, not the underlying interaction or accessibility behavior \u2014 and\n don't remove a responsive class because a screenshot at one width looked fine without it.\n\n## 6. Verify after installing or editing\n\nBefore reporting the work as done, run whatever subset of these the project defines (check\n`package.json` scripts \u2014 names vary by project, but the checks are the same ones the registry's own\nCI runs):\n\n1. Type-check (`tsc --noEmit` or the project's `type-check`/`typecheck` script).\n2. Build (`next build`, `vite build`, or the project's `build` script) \u2014 catches broken imports from\n alias rewriting.\n3. Targeted tests for anything touched, if the project has a test runner configured.\n\nIf a check fails because of something `add` did (a missing dependency it reported but that wasn't\ninstalled, for example), fix that before moving on \u2014 don't report success with a broken build.\n\n## 7. Report what happened\n\nEnd every piece of UI work with a short, concrete summary:\n\n- **Files added** \u2014 which components/examples were installed, and where (respecting `--path` or the\n project's configured alias directory).\n- **Entries reused** \u2014 anything `info --json` or `diff` showed was already installed and left alone.\n- **Checks run** \u2014 which of type-check / build / tests were run, and whether they passed.\n\nThis is what lets a human (or the next session) trust the change without re-deriving it.\n";
|
|
617
|
+
var markerStart = (id) => `<!-- properui:${id}:start -->`;
|
|
618
|
+
var markerEnd = (id) => `<!-- properui:${id}:end -->`;
|
|
619
|
+
function upsertMarkedBlock(existing, id, block) {
|
|
620
|
+
const start = markerStart(id);
|
|
621
|
+
const end = markerEnd(id);
|
|
622
|
+
const section = `${start}
|
|
623
|
+
${block.trim()}
|
|
624
|
+
${end}`;
|
|
625
|
+
const pattern = new RegExp(`${start}[\\s\\S]*?${end}`);
|
|
626
|
+
if (pattern.test(existing)) return existing.replace(pattern, section);
|
|
627
|
+
const separator = existing.trim().length > 0 ? `${existing.replace(/\s+$/, "")}
|
|
628
|
+
|
|
629
|
+
` : "";
|
|
630
|
+
return `${separator}${section}
|
|
631
|
+
`;
|
|
632
|
+
}
|
|
633
|
+
var CLAUDE_MD_BLOCK = `## Proper UI
|
|
634
|
+
|
|
635
|
+
This project has the Proper UI skill installed at \`.claude/skills/properui/SKILL.md\`. Claude
|
|
636
|
+
Code loads it automatically for UI work. Before writing new UI markup by hand: run
|
|
637
|
+
\`npx @properui/cli@latest info --json\`, search the registry with \`npx @properui/cli@latest search\`,
|
|
638
|
+
and prefer \`npx @properui/cli@latest add\` (or \`add example <name>\` for a whole screen) over
|
|
639
|
+
hand-rolling markup the registry already has.`;
|
|
640
|
+
var AGENTS_MD_BLOCK = `## Proper UI
|
|
641
|
+
|
|
642
|
+
This project uses Proper UI (\`@properui/ui\`). Follow \`.agents/skills/properui/SKILL.md\`
|
|
643
|
+
for the full workflow before writing any new UI markup. In short:
|
|
644
|
+
|
|
645
|
+
1. Run \`npx @properui/cli@latest info --json\` to see what is already configured and installed.
|
|
646
|
+
2. Search the registry (\`npx @properui/cli@latest search "<what you need>"\`) before writing markup
|
|
647
|
+
by hand \u2014 prefer \`add example <name>\` for a whole screen, \`add <name>\` for one component.
|
|
648
|
+
3. Keep semantic tokens (\`bg-primary\`, \`text-tertiary\`, ...), React Aria props (\`onPress\`,
|
|
649
|
+
\`isDisabled\`), and logical properties (\`ms-*\`/\`me-*\`, \`start-*\`/\`end-*\`) in anything
|
|
650
|
+
you write yourself \u2014 never a raw palette class, an arbitrary value, or a \`dark:\` utility.
|
|
651
|
+
4. Run type-check and build after installing or editing components before calling the work done.`;
|
|
652
|
+
var CURSOR_RULE_MDC = `---
|
|
653
|
+
alwaysApply: true
|
|
654
|
+
---
|
|
655
|
+
|
|
656
|
+
# Proper UI
|
|
657
|
+
|
|
658
|
+
This project uses Proper UI (\`@properui/ui\`). Before writing any new UI markup:
|
|
659
|
+
|
|
660
|
+
- Run \`npx @properui/cli@latest info --json\` to see the project's Proper UI setup and what is
|
|
661
|
+
already installed.
|
|
662
|
+
- Run \`npx @properui/cli@latest search "<what you need>"\` to check whether a component or
|
|
663
|
+
full-page example already covers it.
|
|
664
|
+
- Install matches with \`npx @properui/cli@latest add <name>\` (or \`add example <name>\` for a whole
|
|
665
|
+
screen) rather than hand-rolling the equivalent markup.
|
|
666
|
+
- Import installed components from \`@/components/...\` (or this project's configured alias in
|
|
667
|
+
\`components.json\`) and follow their existing prop APIs \u2014 don't rename props to "clean them up".
|
|
668
|
+
- Never mix in another component library once a screen uses Proper UI components.
|
|
669
|
+
|
|
670
|
+
When writing or editing component code:
|
|
671
|
+
|
|
672
|
+
- Use React Aria props, not DOM props: \`onPress\` not \`onClick\`, \`isDisabled\` not
|
|
673
|
+
\`disabled\`, \`isSelected\` not \`checked\`.
|
|
674
|
+
- Use semantic tokens only: \`bg-primary\`, \`text-tertiary\`, \`border-secondary\`,
|
|
675
|
+
\`bg-brand-solid\`. Never a raw palette class (\`bg-purple-600\`) or an arbitrary value
|
|
676
|
+
(\`bg-[#7f56d9]\`).
|
|
677
|
+
- Never use \`dark:\` utilities \u2014 a \`.dark-mode\` class on an ancestor repoints every semantic
|
|
678
|
+
token, so a component written against tokens is already correct in both themes.
|
|
679
|
+
- Use logical properties for anything directional: \`ms-*\`/\`me-*\` not \`ml-*\`/\`mr-*\`,
|
|
680
|
+
\`ps-*\`/\`pe-*\` not \`pl-*\`/\`pr-*\`, \`start-*\`/\`end-*\` not \`left-*\`/\`right-*\`,
|
|
681
|
+
\`text-start\` not \`text-left\`.
|
|
682
|
+
- Use tokenised typography \u2014 \`text-display-lg\`, \`text-md\` \u2014 not raw sizes like \`text-4xl\`.
|
|
683
|
+
- Pass icons as component references: \`<Button iconLeading={ArrowRight}>\`, not
|
|
684
|
+
\`<Button iconLeading={<ArrowRight />}>\`.
|
|
685
|
+
- Import from the component's subpath so bundlers keep only what's used, e.g.
|
|
686
|
+
\`@properui/ui/components/base/buttons/button\`.
|
|
687
|
+
- Run type-check and build after installing or editing components before calling the work done.
|
|
688
|
+
`;
|
|
689
|
+
var LOVABLE_SKILL_URL = "https://github.com/properui/properui/blob/main/skills/properui/SKILL.md";
|
|
690
|
+
var LOVABLE_INSTRUCTIONS = `Lovable runs in the browser and does not read files from this
|
|
691
|
+
checkout, so there is nothing to write locally. Instead:
|
|
692
|
+
|
|
693
|
+
1. Open your Lovable project's knowledge / custom instructions panel.
|
|
694
|
+
2. Paste this URL so Lovable can fetch the Skill's raw Markdown:
|
|
695
|
+
${LOVABLE_SKILL_URL}
|
|
696
|
+
3. Or paste the Skill's content directly \u2014 copy it from that URL, or from
|
|
697
|
+
skills/properui/SKILL.md if you have this repo checked out.
|
|
698
|
+
|
|
699
|
+
Once added, prompt Lovable the same way you would Claude Code or Codex: describe the screen and
|
|
700
|
+
mention using the installed Proper UI components. Lovable will then follow the Skill's steps
|
|
701
|
+
(check the registry, install with the CLI or npm, keep semantic tokens and React Aria props)
|
|
702
|
+
instead of generating its own markup from scratch.`;
|
|
703
|
+
|
|
704
|
+
// src/commands/agent.ts
|
|
705
|
+
var CLIENTS = ["claude", "codex", "cursor", "lovable"];
|
|
706
|
+
function resolveClients(client) {
|
|
707
|
+
if (!client || client === "all") return CLIENTS;
|
|
708
|
+
if (CLIENTS.includes(client)) return [client];
|
|
709
|
+
throw new Error(`Unknown --client "${client}". Use one of: ${CLIENTS.join(", ")}, all.`);
|
|
710
|
+
}
|
|
711
|
+
function upsertProjectFile(file, id, block, dryRun) {
|
|
712
|
+
const relative = path7.basename(file);
|
|
713
|
+
const existing = existsSync6(file) ? readFileSync6(file, "utf8") : "";
|
|
714
|
+
const next = upsertMarkedBlock(existing, id, block);
|
|
715
|
+
if (existing === next) return { file, relative, status: "unchanged" };
|
|
716
|
+
if (!dryRun) {
|
|
717
|
+
mkdirSync3(path7.dirname(file), { recursive: true });
|
|
718
|
+
writeFileSync4(file, next, "utf8");
|
|
719
|
+
}
|
|
720
|
+
return { file, relative, status: existing ? "updated" : "created" };
|
|
721
|
+
}
|
|
722
|
+
function statusLabel2(status) {
|
|
723
|
+
if (status === "created") return kleur.green("write");
|
|
724
|
+
if (status === "updated") return kleur.yellow("updat");
|
|
725
|
+
if (status === "skipped") return kleur.dim("skip ");
|
|
726
|
+
return kleur.dim("keep ");
|
|
727
|
+
}
|
|
728
|
+
function installClaude(cwd, writeOptions) {
|
|
729
|
+
const skill = writeSourceFile(path7.join(cwd, ".claude", "skills", "properui", "SKILL.md"), SKILL_MD, writeOptions);
|
|
730
|
+
log.step(`${statusLabel2(skill.status)} ${skill.relative}`);
|
|
731
|
+
const claudeMd = upsertProjectFile(path7.join(cwd, "CLAUDE.md"), "skill", CLAUDE_MD_BLOCK, writeOptions.dryRun);
|
|
732
|
+
log.step(`${statusLabel2(claudeMd.status)} ${claudeMd.relative}${claudeMd.status === "created" ? " (created)" : " (pointer appended)"}`);
|
|
733
|
+
}
|
|
734
|
+
function installCodex(cwd, writeOptions) {
|
|
735
|
+
const skill = writeSourceFile(path7.join(cwd, ".agents", "skills", "properui", "SKILL.md"), SKILL_MD, writeOptions);
|
|
736
|
+
log.step(`${statusLabel2(skill.status)} ${skill.relative}`);
|
|
737
|
+
const agentsMd = upsertProjectFile(path7.join(cwd, "AGENTS.md"), "agents", AGENTS_MD_BLOCK, writeOptions.dryRun);
|
|
738
|
+
log.step(`${statusLabel2(agentsMd.status)} ${agentsMd.relative}${agentsMd.status === "created" ? " (created)" : " (rules block appended)"}`);
|
|
739
|
+
}
|
|
740
|
+
function installCursor(cwd, writeOptions) {
|
|
741
|
+
const rule = writeSourceFile(path7.join(cwd, ".cursor", "rules", "properui.mdc"), CURSOR_RULE_MDC, writeOptions);
|
|
742
|
+
log.step(`${statusLabel2(rule.status)} ${rule.relative}`);
|
|
743
|
+
}
|
|
744
|
+
function installLovable() {
|
|
745
|
+
log.step(`Lovable reads from the web, not this checkout \u2014 nothing is written locally.`);
|
|
746
|
+
log.plain();
|
|
747
|
+
log.info(`Paste this into Lovable's knowledge / custom instructions panel:`);
|
|
748
|
+
log.plain(kleur.dim(` ${LOVABLE_SKILL_URL}`));
|
|
749
|
+
log.plain();
|
|
750
|
+
for (const line of LOVABLE_INSTRUCTIONS.split("\n")) log.plain(line ? ` ${line}` : "");
|
|
751
|
+
}
|
|
752
|
+
async function runAgentInit(options) {
|
|
753
|
+
const cwd = path7.resolve(options.cwd ?? process.cwd());
|
|
754
|
+
const clients = resolveClients(options.client);
|
|
755
|
+
const writeOptions = { cwd, overwrite: Boolean(options.overwrite), dryRun: false };
|
|
756
|
+
log.title(`Installing the Proper UI skill for ${clients.length > 1 ? clients.join(", ") : clients[0]}`);
|
|
757
|
+
for (const client of clients) {
|
|
758
|
+
log.plain();
|
|
759
|
+
log.plain(kleur.bold(client));
|
|
760
|
+
if (client === "claude") installClaude(cwd, writeOptions);
|
|
761
|
+
else if (client === "codex") installCodex(cwd, writeOptions);
|
|
762
|
+
else if (client === "cursor") installCursor(cwd, writeOptions);
|
|
763
|
+
else installLovable();
|
|
764
|
+
}
|
|
765
|
+
log.plain();
|
|
766
|
+
log.success("Done. Re-run any time \u2014 existing files are updated in place, not duplicated.");
|
|
767
|
+
if (!options.overwrite) log.info("Pass --overwrite to replace the Skill file even if you've edited it locally.");
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
// src/commands/diff.ts
|
|
771
|
+
import { existsSync as existsSync7, readFileSync as readFileSync7 } from "fs";
|
|
772
|
+
import path8 from "path";
|
|
773
|
+
|
|
774
|
+
// src/diff.ts
|
|
775
|
+
function diffLines(before, after) {
|
|
776
|
+
const rows = before.length;
|
|
777
|
+
const columns = after.length;
|
|
778
|
+
const table = Array.from({ length: rows + 1 }, () => new Array(columns + 1).fill(0));
|
|
779
|
+
for (let i2 = rows - 1; i2 >= 0; i2 -= 1) {
|
|
780
|
+
for (let j2 = columns - 1; j2 >= 0; j2 -= 1) {
|
|
781
|
+
const current = table[i2] ?? [];
|
|
782
|
+
const next = table[i2 + 1] ?? [];
|
|
783
|
+
current[j2] = before[i2] === after[j2] ? (next[j2 + 1] ?? 0) + 1 : Math.max(next[j2] ?? 0, current[j2 + 1] ?? 0);
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
const result = [];
|
|
787
|
+
let i = 0;
|
|
788
|
+
let j = 0;
|
|
789
|
+
while (i < rows && j < columns) {
|
|
790
|
+
if (before[i] === after[j]) {
|
|
791
|
+
result.push({ op: "equal", text: before[i] ?? "" });
|
|
792
|
+
i += 1;
|
|
793
|
+
j += 1;
|
|
794
|
+
} else if ((table[i + 1]?.[j] ?? 0) >= (table[i]?.[j + 1] ?? 0)) {
|
|
795
|
+
result.push({ op: "remove", text: before[i] ?? "" });
|
|
796
|
+
i += 1;
|
|
797
|
+
} else {
|
|
798
|
+
result.push({ op: "add", text: after[j] ?? "" });
|
|
799
|
+
j += 1;
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
while (i < rows) {
|
|
803
|
+
result.push({ op: "remove", text: before[i] ?? "" });
|
|
804
|
+
i += 1;
|
|
805
|
+
}
|
|
806
|
+
while (j < columns) {
|
|
807
|
+
result.push({ op: "add", text: after[j] ?? "" });
|
|
808
|
+
j += 1;
|
|
809
|
+
}
|
|
810
|
+
return result;
|
|
811
|
+
}
|
|
812
|
+
function hunks(lines, context = 3) {
|
|
813
|
+
const changed = lines.map((line) => line.op !== "equal");
|
|
814
|
+
const keep = lines.map((_, index) => changed.slice(Math.max(0, index - context), index + context + 1).some(Boolean));
|
|
815
|
+
const groups = [];
|
|
816
|
+
let current = [];
|
|
817
|
+
keep.forEach((wanted, index) => {
|
|
818
|
+
const line = lines[index];
|
|
819
|
+
if (wanted && line) {
|
|
820
|
+
current.push(line);
|
|
821
|
+
} else if (current.length > 0) {
|
|
822
|
+
groups.push(current);
|
|
823
|
+
current = [];
|
|
824
|
+
}
|
|
825
|
+
});
|
|
826
|
+
if (current.length > 0) groups.push(current);
|
|
827
|
+
return groups;
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
// src/commands/diff.ts
|
|
831
|
+
async function runDiff(component, options) {
|
|
832
|
+
const cwd = path8.resolve(options.cwd ?? process.cwd());
|
|
833
|
+
const config = readConfig(cwd);
|
|
834
|
+
if (!config) {
|
|
835
|
+
log.error("No components.json found. Run `properui init` first.");
|
|
836
|
+
process.exitCode = 1;
|
|
837
|
+
return;
|
|
838
|
+
}
|
|
839
|
+
const registry = new Registry(resolveRegistrySource(options.registry, config.registry));
|
|
840
|
+
const aliasBase = aliasBaseDir(cwd, config);
|
|
841
|
+
const resolveOptions = { cwd, aliasBase };
|
|
842
|
+
let entries;
|
|
843
|
+
const load = spinner(component ? `Loading ${component}` : "Scanning for components copied into this project");
|
|
844
|
+
try {
|
|
845
|
+
if (component) {
|
|
846
|
+
entries = [await registry.item(component)];
|
|
847
|
+
} else {
|
|
848
|
+
const index = await registry.index();
|
|
849
|
+
entries = [];
|
|
850
|
+
for (const meta of index) {
|
|
851
|
+
if (meta.type === "example") continue;
|
|
852
|
+
const entry = await registry.item(meta.name);
|
|
853
|
+
const present = entry.files.some((file) => existsSync7(prepareFile(file, config, resolveOptions).target));
|
|
854
|
+
if (present) entries.push(entry);
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
load.succeed(`Comparing ${entries.length} component${entries.length === 1 ? "" : "s"} against ${registry.describe()}.`);
|
|
858
|
+
} catch (error) {
|
|
859
|
+
load.fail(error instanceof RegistryError ? error.message : error.message);
|
|
860
|
+
process.exitCode = 1;
|
|
861
|
+
return;
|
|
862
|
+
}
|
|
863
|
+
let modified = 0;
|
|
864
|
+
let missing = 0;
|
|
865
|
+
for (const entry of entries) {
|
|
866
|
+
for (const file of entry.files) {
|
|
867
|
+
const { target, content } = prepareFile(file, config, resolveOptions);
|
|
868
|
+
const relative = path8.relative(cwd, target);
|
|
869
|
+
if (!existsSync7(target)) {
|
|
870
|
+
if (component) {
|
|
871
|
+
missing += 1;
|
|
872
|
+
log.plain(` ${kleur.dim("absent")} ${relative}`);
|
|
873
|
+
}
|
|
874
|
+
continue;
|
|
875
|
+
}
|
|
876
|
+
const local = readFileSync7(target, "utf8");
|
|
877
|
+
if (local === content) continue;
|
|
878
|
+
modified += 1;
|
|
879
|
+
log.title(`${entry.name} \xB7 ${relative}`);
|
|
880
|
+
for (const hunk of hunks(diffLines(local.split("\n"), content.split("\n")))) {
|
|
881
|
+
for (const line of hunk) {
|
|
882
|
+
if (line.op === "equal") log.plain(kleur.dim(` ${line.text}`));
|
|
883
|
+
else if (line.op === "remove") log.plain(kleur.red(` - ${line.text}`));
|
|
884
|
+
else log.plain(kleur.green(` + ${line.text}`));
|
|
885
|
+
}
|
|
886
|
+
log.plain(kleur.dim(" ---"));
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
log.plain();
|
|
891
|
+
if (modified === 0) {
|
|
892
|
+
log.success(missing > 0 ? `No local modifications (${missing} file(s) not installed).` : "No local modifications \u2014 everything matches the registry.");
|
|
893
|
+
return;
|
|
894
|
+
}
|
|
895
|
+
log.warn(`${modified} file${modified === 1 ? "" : "s"} differ from the registry.`);
|
|
896
|
+
log.info(`Take the registry version with: npx @properui/cli add ${component ?? "<component>"} --overwrite`);
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
// src/commands/info.ts
|
|
900
|
+
import { existsSync as existsSync8, readFileSync as readFileSync8 } from "fs";
|
|
901
|
+
import path9 from "path";
|
|
902
|
+
function packageVersions(cwd, name, deps) {
|
|
903
|
+
const pkgPath = path9.join(cwd, "node_modules", ...name.split("/"), "package.json");
|
|
904
|
+
let installed = null;
|
|
905
|
+
if (existsSync8(pkgPath)) {
|
|
906
|
+
try {
|
|
907
|
+
installed = JSON.parse(readFileSync8(pkgPath, "utf8")).version ?? null;
|
|
908
|
+
} catch {
|
|
909
|
+
installed = null;
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
return { installed, declared: deps[name] ?? null };
|
|
913
|
+
}
|
|
914
|
+
async function collectSnapshot(options) {
|
|
915
|
+
const cwd = path9.resolve(options.cwd ?? process.cwd());
|
|
916
|
+
const project = detectProject(cwd);
|
|
917
|
+
const config = readConfig(cwd);
|
|
918
|
+
const deps = allDependencies(readPackageJson(cwd));
|
|
919
|
+
const registrySource = resolveRegistrySource(options.registry, config?.registry);
|
|
920
|
+
const registry = new Registry(registrySource);
|
|
921
|
+
const installed = [];
|
|
922
|
+
let registryReachable = true;
|
|
923
|
+
if (config) {
|
|
924
|
+
const aliasBase = aliasBaseDir(cwd, config);
|
|
925
|
+
const resolveOptions = { cwd, aliasBase };
|
|
926
|
+
try {
|
|
927
|
+
const index = await registry.index();
|
|
928
|
+
for (const meta of index) {
|
|
929
|
+
if (meta.type === "example") continue;
|
|
930
|
+
const entry = await registry.item(meta.name);
|
|
931
|
+
const present = entry.files.some((file) => existsSync8(prepareFile(file, config, resolveOptions).target));
|
|
932
|
+
if (present) installed.push({ name: meta.name, layer: meta.layer, type: meta.type });
|
|
933
|
+
}
|
|
934
|
+
} catch {
|
|
935
|
+
registryReachable = false;
|
|
936
|
+
}
|
|
937
|
+
} else {
|
|
938
|
+
registryReachable = false;
|
|
939
|
+
}
|
|
940
|
+
return {
|
|
941
|
+
cwd,
|
|
942
|
+
framework: project.framework,
|
|
943
|
+
typescript: project.typescript,
|
|
944
|
+
tailwindVersion: project.tailwindVersion,
|
|
945
|
+
packageManager: project.packageManager,
|
|
946
|
+
config: {
|
|
947
|
+
present: Boolean(config),
|
|
948
|
+
file: path9.relative(cwd, configPath(cwd)) || "components.json",
|
|
949
|
+
aliases: config?.aliases ?? null,
|
|
950
|
+
theme: config?.tailwind.theme ?? null,
|
|
951
|
+
css: config?.tailwind.css ?? null,
|
|
952
|
+
registry: config?.registry ?? null
|
|
953
|
+
},
|
|
954
|
+
packages: {
|
|
955
|
+
"@properui/ui": packageVersions(cwd, "@properui/ui", deps),
|
|
956
|
+
properui: packageVersions(cwd, "properui", deps)
|
|
957
|
+
},
|
|
958
|
+
registrySource,
|
|
959
|
+
registryReachable,
|
|
960
|
+
installed: installed.sort((a, b) => a.name.localeCompare(b.name))
|
|
961
|
+
};
|
|
962
|
+
}
|
|
963
|
+
function formatVersions(versions) {
|
|
964
|
+
if (versions.installed) return versions.installed;
|
|
965
|
+
if (versions.declared) return `${versions.declared} (not installed)`;
|
|
966
|
+
return "not found";
|
|
967
|
+
}
|
|
968
|
+
function printHuman(snapshot) {
|
|
969
|
+
log.title("Proper UI project info");
|
|
970
|
+
log.step(`Framework ${snapshot.framework}`);
|
|
971
|
+
log.step(`Language ${snapshot.typescript ? "TypeScript" : "JavaScript"}`);
|
|
972
|
+
log.step(`Tailwind ${snapshot.tailwindVersion ? `v${snapshot.tailwindVersion}` : "not installed"}`);
|
|
973
|
+
log.step(`Package manager ${snapshot.packageManager}`);
|
|
974
|
+
log.step(`@properui/ui ${formatVersions(snapshot.packages["@properui/ui"])}`);
|
|
975
|
+
log.step(`properui (CLI) ${formatVersions(snapshot.packages.properui)}`);
|
|
976
|
+
log.plain();
|
|
977
|
+
if (!snapshot.config.present) {
|
|
978
|
+
log.warn(`No ${snapshot.config.file} found. Run \`npx @properui/cli@latest init\` before installing components.`);
|
|
979
|
+
return;
|
|
980
|
+
}
|
|
981
|
+
log.step(`Config file ${snapshot.config.file}`);
|
|
982
|
+
log.step(`Components alias ${snapshot.config.aliases?.components}`);
|
|
983
|
+
log.step(`Utils alias ${snapshot.config.aliases?.utils}`);
|
|
984
|
+
log.step(`UI alias ${snapshot.config.aliases?.ui}`);
|
|
985
|
+
log.step(`Hooks alias ${snapshot.config.aliases?.hooks}`);
|
|
986
|
+
log.step(`Theme CSS ${snapshot.config.theme}`);
|
|
987
|
+
log.step(`Global CSS ${snapshot.config.css}`);
|
|
988
|
+
log.step(`Registry ${snapshot.registrySource}`);
|
|
989
|
+
log.plain();
|
|
990
|
+
if (!snapshot.registryReachable) {
|
|
991
|
+
log.warn(`Could not reach the registry \u2014 installed entries below may be incomplete.`);
|
|
992
|
+
}
|
|
993
|
+
if (snapshot.installed.length === 0) {
|
|
994
|
+
log.info("No registry entries installed yet.");
|
|
995
|
+
return;
|
|
996
|
+
}
|
|
997
|
+
log.title(`${snapshot.installed.length} installed entr${snapshot.installed.length === 1 ? "y" : "ies"}`);
|
|
998
|
+
const width = Math.max(...snapshot.installed.map((entry) => entry.name.length));
|
|
999
|
+
for (const entry of snapshot.installed) {
|
|
1000
|
+
log.plain(` ${kleur.bold(entry.name.padEnd(width))} ${kleur.dim(entry.layer)}`);
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
async function runInfo(options) {
|
|
1004
|
+
let snapshot;
|
|
1005
|
+
try {
|
|
1006
|
+
snapshot = await collectSnapshot(options);
|
|
1007
|
+
} catch (error) {
|
|
1008
|
+
log.error(error instanceof RegistryError ? error.message : error.message);
|
|
1009
|
+
process.exitCode = 1;
|
|
1010
|
+
return;
|
|
1011
|
+
}
|
|
1012
|
+
if (options.json) {
|
|
1013
|
+
log.plain(JSON.stringify(snapshot, null, 2));
|
|
1014
|
+
return;
|
|
1015
|
+
}
|
|
1016
|
+
printHuman(snapshot);
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
// src/commands/init.ts
|
|
1020
|
+
import { existsSync as existsSync9, mkdirSync as mkdirSync4, readFileSync as readFileSync9, writeFileSync as writeFileSync5 } from "fs";
|
|
1021
|
+
import path10 from "path";
|
|
1022
|
+
|
|
1023
|
+
// src/templates.ts
|
|
1024
|
+
var THEME_CSS_PLACEHOLDER = `/*
|
|
1025
|
+
* Proper UI theme tokens \u2014 PLACEHOLDER.
|
|
1026
|
+
*
|
|
1027
|
+
* The full token set could not be downloaded (the registry was unreachable), so this file
|
|
1028
|
+
* only carries enough tokens to keep Tailwind compiling. Replace it with the real theme:
|
|
1029
|
+
*
|
|
1030
|
+
* npx @properui/cli add styles --overwrite
|
|
1031
|
+
*
|
|
1032
|
+
* Spec: docs/theming.md
|
|
1033
|
+
*/
|
|
1034
|
+
|
|
1035
|
+
@theme {
|
|
1036
|
+
--font-body: var(--font-inter, "Inter"), -apple-system, "Segoe UI", Roboto, Arial, sans-serif;
|
|
1037
|
+
|
|
1038
|
+
--color-brand-600: rgb(127 86 217);
|
|
1039
|
+
--color-brand-700: rgb(105 65 198);
|
|
1040
|
+
|
|
1041
|
+
--color-bg-primary: rgb(255 255 255);
|
|
1042
|
+
--color-bg-secondary: rgb(249 250 251);
|
|
1043
|
+
--color-bg-brand-solid: var(--color-brand-600);
|
|
1044
|
+
|
|
1045
|
+
--color-text-primary: rgb(16 24 40);
|
|
1046
|
+
--color-text-secondary: rgb(71 84 103);
|
|
1047
|
+
|
|
1048
|
+
--color-border-primary: rgb(208 213 221);
|
|
1049
|
+
|
|
1050
|
+
--background-color-primary: var(--color-bg-primary);
|
|
1051
|
+
--background-color-secondary: var(--color-bg-secondary);
|
|
1052
|
+
--background-color-brand-solid: var(--color-bg-brand-solid);
|
|
1053
|
+
|
|
1054
|
+
--text-color-primary: var(--color-text-primary);
|
|
1055
|
+
--text-color-secondary: var(--color-text-secondary);
|
|
1056
|
+
|
|
1057
|
+
--border-color-primary: var(--color-border-primary);
|
|
1058
|
+
--ring-color-primary: var(--color-border-primary);
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
@layer base {
|
|
1062
|
+
.dark-mode {
|
|
1063
|
+
--color-bg-primary: rgb(12 14 18);
|
|
1064
|
+
--color-bg-secondary: rgb(22 26 33);
|
|
1065
|
+
--color-text-primary: rgb(247 247 247);
|
|
1066
|
+
--color-text-secondary: rgb(203 202 204);
|
|
1067
|
+
--color-border-primary: rgb(55 58 67);
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
`;
|
|
1071
|
+
var CX_TS_FALLBACK = `import { extendTailwindMerge } from "tailwind-merge";
|
|
1072
|
+
|
|
1073
|
+
const twMerge = extendTailwindMerge({
|
|
1074
|
+
extend: {
|
|
1075
|
+
theme: {
|
|
1076
|
+
text: ["display-xs", "display-sm", "display-md", "display-lg", "display-xl", "display-2xl"],
|
|
1077
|
+
},
|
|
1078
|
+
},
|
|
1079
|
+
});
|
|
1080
|
+
|
|
1081
|
+
/**
|
|
1082
|
+
* This function is a wrapper around the twMerge function.
|
|
1083
|
+
* It is used to merge the classes inside style objects.
|
|
1084
|
+
*/
|
|
1085
|
+
export const cx = twMerge;
|
|
1086
|
+
|
|
1087
|
+
/**
|
|
1088
|
+
* This function does nothing besides helping us to be able to
|
|
1089
|
+
* sort the classes inside style objects which is not supported
|
|
1090
|
+
* by the Tailwind IntelliSense by default.
|
|
1091
|
+
*/
|
|
1092
|
+
export function sortCx<T extends Record<string, string | number | Record<string, string | number | Record<string, string | number>>>>(classes: T): T {
|
|
1093
|
+
return classes;
|
|
1094
|
+
}
|
|
1095
|
+
`;
|
|
1096
|
+
var THEME_PROVIDER_TSX = `"use client";
|
|
1097
|
+
|
|
1098
|
+
import { createContext, useCallback, useContext, useEffect, useMemo, useState, type ReactNode } from "react";
|
|
1099
|
+
|
|
1100
|
+
/**
|
|
1101
|
+
* Class-based theme provider. Adds \`.light-mode\` / \`.dark-mode\` to <html>, which is what
|
|
1102
|
+
* every semantic token in styles/theme.css keys off.
|
|
1103
|
+
*/
|
|
1104
|
+
export type Theme = "light" | "dark" | "system";
|
|
1105
|
+
|
|
1106
|
+
interface ThemeContextValue {
|
|
1107
|
+
theme: Theme;
|
|
1108
|
+
resolvedTheme: "light" | "dark";
|
|
1109
|
+
setTheme: (theme: Theme) => void;
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
const ThemeContext = createContext<ThemeContextValue | null>(null);
|
|
1113
|
+
|
|
1114
|
+
const STORAGE_KEY = "properui-theme";
|
|
1115
|
+
|
|
1116
|
+
const systemTheme = (): "light" | "dark" =>
|
|
1117
|
+
typeof window !== "undefined" && window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
|
|
1118
|
+
|
|
1119
|
+
const readStoredTheme = (fallback: Theme): Theme => {
|
|
1120
|
+
if (typeof window === "undefined") return fallback;
|
|
1121
|
+
const stored = window.localStorage.getItem(STORAGE_KEY);
|
|
1122
|
+
return stored === "light" || stored === "dark" || stored === "system" ? stored : fallback;
|
|
1123
|
+
};
|
|
1124
|
+
|
|
1125
|
+
export const ThemeProvider = ({ children, defaultTheme = "system" }: { children: ReactNode; defaultTheme?: Theme }) => {
|
|
1126
|
+
const [theme, setThemeState] = useState<Theme>(defaultTheme);
|
|
1127
|
+
const [resolvedTheme, setResolvedTheme] = useState<"light" | "dark">("light");
|
|
1128
|
+
|
|
1129
|
+
useEffect(() => setThemeState(readStoredTheme(defaultTheme)), [defaultTheme]);
|
|
1130
|
+
|
|
1131
|
+
useEffect(() => {
|
|
1132
|
+
const apply = () => {
|
|
1133
|
+
const resolved = theme === "system" ? systemTheme() : theme;
|
|
1134
|
+
setResolvedTheme(resolved);
|
|
1135
|
+
const root = document.documentElement;
|
|
1136
|
+
root.classList.remove("light-mode", "dark-mode");
|
|
1137
|
+
root.classList.add(resolved === "dark" ? "dark-mode" : "light-mode");
|
|
1138
|
+
root.style.colorScheme = resolved;
|
|
1139
|
+
};
|
|
1140
|
+
|
|
1141
|
+
apply();
|
|
1142
|
+
if (theme !== "system") return;
|
|
1143
|
+
|
|
1144
|
+
const media = window.matchMedia("(prefers-color-scheme: dark)");
|
|
1145
|
+
media.addEventListener("change", apply);
|
|
1146
|
+
return () => media.removeEventListener("change", apply);
|
|
1147
|
+
}, [theme]);
|
|
1148
|
+
|
|
1149
|
+
const setTheme = useCallback((next: Theme) => {
|
|
1150
|
+
window.localStorage.setItem(STORAGE_KEY, next);
|
|
1151
|
+
setThemeState(next);
|
|
1152
|
+
}, []);
|
|
1153
|
+
|
|
1154
|
+
const value = useMemo(() => ({ theme, resolvedTheme, setTheme }), [theme, resolvedTheme, setTheme]);
|
|
1155
|
+
|
|
1156
|
+
return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>;
|
|
1157
|
+
};
|
|
1158
|
+
|
|
1159
|
+
export const useTheme = () => {
|
|
1160
|
+
const context = useContext(ThemeContext);
|
|
1161
|
+
if (!context) throw new Error("useTheme must be used inside <ThemeProvider>.");
|
|
1162
|
+
return context;
|
|
1163
|
+
};
|
|
1164
|
+
`;
|
|
1165
|
+
var THEME_PROVIDER_JSX = `"use client";
|
|
1166
|
+
|
|
1167
|
+
import { createContext, useCallback, useContext, useEffect, useMemo, useState } from "react";
|
|
1168
|
+
|
|
1169
|
+
/**
|
|
1170
|
+
* Class-based theme provider. Adds \`.light-mode\` / \`.dark-mode\` to <html>, which is what
|
|
1171
|
+
* every semantic token in styles/theme.css keys off.
|
|
1172
|
+
*/
|
|
1173
|
+
const ThemeContext = createContext(null);
|
|
1174
|
+
|
|
1175
|
+
const STORAGE_KEY = "properui-theme";
|
|
1176
|
+
|
|
1177
|
+
const systemTheme = () => (typeof window !== "undefined" && window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light");
|
|
1178
|
+
|
|
1179
|
+
const readStoredTheme = (fallback) => {
|
|
1180
|
+
if (typeof window === "undefined") return fallback;
|
|
1181
|
+
const stored = window.localStorage.getItem(STORAGE_KEY);
|
|
1182
|
+
return stored === "light" || stored === "dark" || stored === "system" ? stored : fallback;
|
|
1183
|
+
};
|
|
1184
|
+
|
|
1185
|
+
export const ThemeProvider = ({ children, defaultTheme = "system" }) => {
|
|
1186
|
+
const [theme, setThemeState] = useState(defaultTheme);
|
|
1187
|
+
const [resolvedTheme, setResolvedTheme] = useState("light");
|
|
1188
|
+
|
|
1189
|
+
useEffect(() => setThemeState(readStoredTheme(defaultTheme)), [defaultTheme]);
|
|
1190
|
+
|
|
1191
|
+
useEffect(() => {
|
|
1192
|
+
const apply = () => {
|
|
1193
|
+
const resolved = theme === "system" ? systemTheme() : theme;
|
|
1194
|
+
setResolvedTheme(resolved);
|
|
1195
|
+
const root = document.documentElement;
|
|
1196
|
+
root.classList.remove("light-mode", "dark-mode");
|
|
1197
|
+
root.classList.add(resolved === "dark" ? "dark-mode" : "light-mode");
|
|
1198
|
+
root.style.colorScheme = resolved;
|
|
1199
|
+
};
|
|
1200
|
+
|
|
1201
|
+
apply();
|
|
1202
|
+
if (theme !== "system") return;
|
|
1203
|
+
|
|
1204
|
+
const media = window.matchMedia("(prefers-color-scheme: dark)");
|
|
1205
|
+
media.addEventListener("change", apply);
|
|
1206
|
+
return () => media.removeEventListener("change", apply);
|
|
1207
|
+
}, [theme]);
|
|
1208
|
+
|
|
1209
|
+
const setTheme = useCallback((next) => {
|
|
1210
|
+
window.localStorage.setItem(STORAGE_KEY, next);
|
|
1211
|
+
setThemeState(next);
|
|
1212
|
+
}, []);
|
|
1213
|
+
|
|
1214
|
+
const value = useMemo(() => ({ theme, resolvedTheme, setTheme }), [theme, resolvedTheme, setTheme]);
|
|
1215
|
+
|
|
1216
|
+
return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>;
|
|
1217
|
+
};
|
|
1218
|
+
|
|
1219
|
+
export const useTheme = () => {
|
|
1220
|
+
const context = useContext(ThemeContext);
|
|
1221
|
+
if (!context) throw new Error("useTheme must be used inside <ThemeProvider>.");
|
|
1222
|
+
return context;
|
|
1223
|
+
};
|
|
1224
|
+
`;
|
|
1225
|
+
|
|
1226
|
+
// src/commands/init.ts
|
|
1227
|
+
var TAILWIND_V3_MESSAGE = [
|
|
1228
|
+
"Proper UI requires Tailwind CSS v4. This project is on v3.",
|
|
1229
|
+
"",
|
|
1230
|
+
" 1. npx @tailwindcss/upgrade@latest",
|
|
1231
|
+
" 2. Replace tailwind.config.js content with the v4 CSS-first setup:",
|
|
1232
|
+
' @import "tailwindcss";',
|
|
1233
|
+
" 3. Re-run: npx @properui/cli init",
|
|
1234
|
+
"",
|
|
1235
|
+
"Upgrade guide: https://tailwindcss.com/docs/upgrade-guide"
|
|
1236
|
+
].join("\n");
|
|
1237
|
+
function frameworkOverride(options) {
|
|
1238
|
+
if (options.nextjs) return "next-app";
|
|
1239
|
+
if (options.vite) return "vite";
|
|
1240
|
+
return void 0;
|
|
1241
|
+
}
|
|
1242
|
+
function relativeCssPath(from, to) {
|
|
1243
|
+
const relative = path10.relative(path10.dirname(from), to).split(path10.sep).join("/");
|
|
1244
|
+
return relative.startsWith(".") ? relative : `./${relative}`;
|
|
1245
|
+
}
|
|
1246
|
+
function relativeDirPath(from, to) {
|
|
1247
|
+
const relative = path10.relative(from, to).split(path10.sep).join("/");
|
|
1248
|
+
if (relative === "") return ".";
|
|
1249
|
+
return relative.startsWith(".") ? relative : `./${relative}`;
|
|
1250
|
+
}
|
|
1251
|
+
function matchBalancedBrace(source, openIndex) {
|
|
1252
|
+
let depth = 0;
|
|
1253
|
+
for (let i = openIndex; i < source.length; i++) {
|
|
1254
|
+
if (source[i] === "{") depth++;
|
|
1255
|
+
else if (source[i] === "}") {
|
|
1256
|
+
depth--;
|
|
1257
|
+
if (depth === 0) return i;
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
return -1;
|
|
1261
|
+
}
|
|
1262
|
+
function wireViteTsconfigPaths(project, dryRun) {
|
|
1263
|
+
const file = ["tsconfig.app.json", "tsconfig.json", "jsconfig.json"].map((name) => path10.join(project.cwd, name)).find((candidate) => existsSync9(candidate));
|
|
1264
|
+
const target = `${relativeDirPath(path10.dirname(file ?? project.cwd), project.aliasBase)}/*`;
|
|
1265
|
+
const snippet = `"paths": { "${project.aliasPrefix}*": ["${target}"] }`;
|
|
1266
|
+
if (project.aliasDeclared) return { file: file ?? null, status: "already-declared", snippet };
|
|
1267
|
+
if (!file) return { file: null, status: "skipped", snippet };
|
|
1268
|
+
const raw = readFileSync9(file, "utf8");
|
|
1269
|
+
const match = /"compilerOptions"\s*:\s*\{/.exec(raw);
|
|
1270
|
+
if (!match) return { file, status: "unsupported", snippet };
|
|
1271
|
+
const afterBrace = match.index + match[0].length;
|
|
1272
|
+
const indent = /\n([ \t]*)\S/.exec(raw.slice(afterBrace))?.[1] ?? " ";
|
|
1273
|
+
const next = `${raw.slice(0, afterBrace)}
|
|
1274
|
+
${indent}${snippet},${raw.slice(afterBrace)}`;
|
|
1275
|
+
if (!dryRun) writeFileSync5(file, next, "utf8");
|
|
1276
|
+
return { file, status: "written", snippet };
|
|
1277
|
+
}
|
|
1278
|
+
var VITE_CONFIG_NAMES = ["vite.config.ts", "vite.config.mts", "vite.config.js", "vite.config.mjs"];
|
|
1279
|
+
function wireViteConfigAlias(project, dryRun) {
|
|
1280
|
+
const file = VITE_CONFIG_NAMES.map((name) => path10.join(project.cwd, name)).find((candidate) => existsSync9(candidate));
|
|
1281
|
+
const aliasKey = project.aliasPrefix.replace(/\/$/, "");
|
|
1282
|
+
const srcPath = relativeDirPath(project.cwd, project.aliasBase);
|
|
1283
|
+
const raw = file ? readFileSync9(file, "utf8") : "";
|
|
1284
|
+
const isEsm = raw ? /^\s*(?:import\s|export\s+default\b)/m.test(raw) : true;
|
|
1285
|
+
const aliasExpr = isEsm ? `fileURLToPath(new URL("${srcPath}", import.meta.url))` : `path.resolve(__dirname, "${srcPath}")`;
|
|
1286
|
+
const hasUrlImport = /fileURLToPath/.test(raw) && /from\s+["'](?:node:)?url["']/.test(raw);
|
|
1287
|
+
const hasPathImport = /require\(\s*["'](?:node:)?path["']\s*\)/.test(raw) || /from\s+["'](?:node:)?path["']/.test(raw);
|
|
1288
|
+
const needsImport = isEsm ? !hasUrlImport : !hasPathImport;
|
|
1289
|
+
const importLine = isEsm ? 'import { fileURLToPath } from "node:url";' : 'const path = require("node:path");';
|
|
1290
|
+
const snippet = [...needsImport ? [importLine, ""] : [], "resolve: {", ` alias: { "${aliasKey}": ${aliasExpr} },`, "},"].join("\n");
|
|
1291
|
+
if (!file) return { file: null, aliasKey, status: "skipped", snippet };
|
|
1292
|
+
const aliasKeyPattern = aliasKey.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1293
|
+
if (new RegExp(`alias\\s*:\\s*\\{[^]*?["']${aliasKeyPattern}["']\\s*:`, "").test(raw)) {
|
|
1294
|
+
return { file, aliasKey, status: "already-present", snippet };
|
|
1295
|
+
}
|
|
1296
|
+
let next = null;
|
|
1297
|
+
const resolveMatch = /resolve\s*:\s*\{/.exec(raw);
|
|
1298
|
+
if (resolveMatch) {
|
|
1299
|
+
const openIndex = resolveMatch.index + resolveMatch[0].length - 1;
|
|
1300
|
+
const closeIndex = matchBalancedBrace(raw, openIndex);
|
|
1301
|
+
if (closeIndex === -1) return { file, aliasKey, status: "unsupported", snippet };
|
|
1302
|
+
const body = raw.slice(openIndex + 1, closeIndex);
|
|
1303
|
+
const aliasMatch = /alias\s*:\s*(\{|\[)/.exec(body);
|
|
1304
|
+
if (aliasMatch?.[1] === "[") return { file, aliasKey, status: "unsupported", snippet };
|
|
1305
|
+
if (aliasMatch) {
|
|
1306
|
+
const aliasOpenIndex = openIndex + 1 + aliasMatch.index + aliasMatch[0].length;
|
|
1307
|
+
next = `${raw.slice(0, aliasOpenIndex)} "${aliasKey}": ${aliasExpr},${raw.slice(aliasOpenIndex)}`;
|
|
1308
|
+
} else {
|
|
1309
|
+
const indent = /\n([ \t]*)\S/.exec(body)?.[1] ?? " ";
|
|
1310
|
+
next = `${raw.slice(0, openIndex + 1)}
|
|
1311
|
+
${indent}alias: { "${aliasKey}": ${aliasExpr} },${raw.slice(openIndex + 1)}`;
|
|
1312
|
+
}
|
|
1313
|
+
} else {
|
|
1314
|
+
const defineConfigMatch = /(?:defineConfig\s*\(\s*|export\s+default\s*)\{/.exec(raw);
|
|
1315
|
+
if (!defineConfigMatch) return { file, aliasKey, status: "unsupported", snippet };
|
|
1316
|
+
const afterBrace = defineConfigMatch.index + defineConfigMatch[0].length;
|
|
1317
|
+
const indent = /\n([ \t]*)\S/.exec(raw.slice(afterBrace))?.[1] ?? " ";
|
|
1318
|
+
next = `${raw.slice(0, afterBrace)}
|
|
1319
|
+
${indent}resolve: {
|
|
1320
|
+
${indent} alias: { "${aliasKey}": ${aliasExpr} },
|
|
1321
|
+
${indent}},${raw.slice(afterBrace)}`;
|
|
1322
|
+
}
|
|
1323
|
+
if (needsImport) {
|
|
1324
|
+
const importLines = [...next.matchAll(/^import .+;$/gm)];
|
|
1325
|
+
const last = importLines[importLines.length - 1];
|
|
1326
|
+
next = last ? `${next.slice(0, last.index + last[0].length)}
|
|
1327
|
+
${importLine}${next.slice(last.index + last[0].length)}` : `${importLine}
|
|
1328
|
+
${next}`;
|
|
1329
|
+
}
|
|
1330
|
+
if (!dryRun) writeFileSync5(file, next, "utf8");
|
|
1331
|
+
return { file, aliasKey, status: "written", snippet };
|
|
1332
|
+
}
|
|
1333
|
+
function wireStylesheet(cwd, config, dryRun) {
|
|
1334
|
+
const cssFile = path10.resolve(cwd, config.tailwind.css);
|
|
1335
|
+
const existing = existsSync9(cssFile) ? readFileSync9(cssFile, "utf8") : "";
|
|
1336
|
+
const themeImport = `@import "${relativeCssPath(cssFile, path10.resolve(cwd, config.tailwind.theme))}";`;
|
|
1337
|
+
const componentsDir = path10.resolve(cwd, config.tailwind.theme, "..", "..", "components");
|
|
1338
|
+
const sourceLine = `@source "${relativeCssPath(cssFile, componentsDir)}/**/*.{ts,tsx,js,jsx}";`;
|
|
1339
|
+
const wanted = ['@import "tailwindcss";', themeImport, sourceLine];
|
|
1340
|
+
const added = wanted.filter((line) => !existing.includes(line));
|
|
1341
|
+
if (added.length === 0) return { file: cssFile, added };
|
|
1342
|
+
const header = existing.trim().length > 0 ? `${added.join("\n")}
|
|
1343
|
+
|
|
1344
|
+
${existing.replace(/^\uFEFF/, "")}` : `${added.join("\n")}
|
|
1345
|
+
`;
|
|
1346
|
+
if (!dryRun) {
|
|
1347
|
+
mkdirSync4(path10.dirname(cssFile), { recursive: true });
|
|
1348
|
+
writeFileSync5(cssFile, header, "utf8");
|
|
1349
|
+
}
|
|
1350
|
+
return { file: cssFile, added };
|
|
1351
|
+
}
|
|
1352
|
+
function entryCandidates(project) {
|
|
1353
|
+
const roots = project.srcDir ? ["src", "."] : [".", "src"];
|
|
1354
|
+
const names = project.framework === "next-app" ? ["app/layout.tsx", "app/layout.jsx"] : project.framework === "next-pages" ? ["pages/_app.tsx", "pages/_app.jsx"] : ["main.tsx", "main.jsx", "index.tsx", "index.jsx"];
|
|
1355
|
+
return roots.flatMap((root) => names.map((name) => path10.join(project.cwd, root, name)));
|
|
1356
|
+
}
|
|
1357
|
+
function reindent(block, indent) {
|
|
1358
|
+
const lines = block.replace(/\s+$/, "").split("\n");
|
|
1359
|
+
const rest = lines.slice(1).filter((line) => line.trim());
|
|
1360
|
+
const common = rest.length > 0 ? Math.min(...rest.map((line) => (/^\s*/.exec(line)?.[0] ?? "").length)) : 0;
|
|
1361
|
+
return lines.map((line, index) => index === 0 ? `${indent}${line.trim()}` : line.trim() ? `${indent}${line.slice(common)}` : "").join("\n");
|
|
1362
|
+
}
|
|
1363
|
+
function wireThemeProvider(project, config, dryRun) {
|
|
1364
|
+
const entry = entryCandidates(project).find((candidate) => existsSync9(candidate));
|
|
1365
|
+
if (!entry) return null;
|
|
1366
|
+
const source = readFileSync9(entry, "utf8");
|
|
1367
|
+
if (source.includes("ThemeProvider")) return { file: entry, changed: false };
|
|
1368
|
+
const importPath = `${config.aliases.components.replace(/\/components$/, "")}/providers/theme-provider`;
|
|
1369
|
+
const importLine = `import { ThemeProvider } from "${importPath}";`;
|
|
1370
|
+
let next = null;
|
|
1371
|
+
const bodyMatch = /^([ \t]*)(<body[^>]*>)([\s\S]*?)(<\/body>)/m.exec(source);
|
|
1372
|
+
const renderStart = source.indexOf(".render(");
|
|
1373
|
+
if (bodyMatch?.[2] && bodyMatch[3]?.trim() && bodyMatch[4]) {
|
|
1374
|
+
const indent = bodyMatch[1] ?? "";
|
|
1375
|
+
const inner = bodyMatch[3].trim();
|
|
1376
|
+
const wrapped = inner.includes("\n") ? `${indent} <ThemeProvider>
|
|
1377
|
+
${reindent(inner, `${indent} `)}
|
|
1378
|
+
${indent} </ThemeProvider>` : `${indent} <ThemeProvider>${inner}</ThemeProvider>`;
|
|
1379
|
+
next = source.replace(bodyMatch[0], `${indent}${bodyMatch[2]}
|
|
1380
|
+
${wrapped}
|
|
1381
|
+
${indent}${bodyMatch[4]}`);
|
|
1382
|
+
} else if (renderStart !== -1) {
|
|
1383
|
+
const open = renderStart + ".render(".length;
|
|
1384
|
+
const close = source.lastIndexOf(")");
|
|
1385
|
+
const inner = close > open ? source.slice(open, close).trim().replace(/,$/, "") : "";
|
|
1386
|
+
if (inner) {
|
|
1387
|
+
next = `${source.slice(0, open)}
|
|
1388
|
+
<ThemeProvider>
|
|
1389
|
+
${reindent(inner, " ")}
|
|
1390
|
+
</ThemeProvider>,
|
|
1391
|
+
${source.slice(close)}`;
|
|
1392
|
+
}
|
|
1393
|
+
}
|
|
1394
|
+
if (!next) return { file: entry, changed: false };
|
|
1395
|
+
const withImport = /^(["']use client["'];?\s*\n)?/.exec(next);
|
|
1396
|
+
const insertAt = withImport?.[0]?.length ?? 0;
|
|
1397
|
+
next = `${next.slice(0, insertAt)}${importLine}
|
|
1398
|
+
${next.slice(insertAt)}`;
|
|
1399
|
+
if (!dryRun) writeFileSync5(entry, next, "utf8");
|
|
1400
|
+
return { file: entry, changed: true };
|
|
1401
|
+
}
|
|
1402
|
+
async function runInit(options) {
|
|
1403
|
+
const cwd = path10.resolve(options.cwd ?? process.cwd());
|
|
1404
|
+
const project = detectProject(cwd, frameworkOverride(options));
|
|
1405
|
+
log.title("Configuring this project for Proper UI");
|
|
1406
|
+
log.step(`Framework ${FRAMEWORK_LABEL[project.framework]}`);
|
|
1407
|
+
log.step(`Language ${project.typescript ? "TypeScript" : "JavaScript"}`);
|
|
1408
|
+
log.step(`Source folder ${project.srcDir ? "src/" : "project root"}`);
|
|
1409
|
+
log.step(`Import alias ${project.aliasPrefix}${project.aliasDeclared ? "" : kleur.yellow(" (not declared in tsconfig paths)")}`);
|
|
1410
|
+
log.step(`Tailwind ${project.tailwindVersion ? `v${project.tailwindVersion}` : "not installed"}`);
|
|
1411
|
+
log.step(`Package manager ${project.packageManager}`);
|
|
1412
|
+
log.plain();
|
|
1413
|
+
if (project.tailwindVersion === 3) {
|
|
1414
|
+
log.error(TAILWIND_V3_MESSAGE);
|
|
1415
|
+
process.exitCode = 1;
|
|
1416
|
+
return;
|
|
1417
|
+
}
|
|
1418
|
+
if (!project.aliasDeclared) {
|
|
1419
|
+
log.warn(`No \`paths\` mapping found. Add this to tsconfig.json so \`${project.aliasPrefix}\` resolves:`);
|
|
1420
|
+
log.plain(kleur.dim(` "baseUrl": ".", "paths": { "${project.aliasPrefix}*": ["./${project.srcDir ? "src/" : ""}*"] }`));
|
|
1421
|
+
log.plain();
|
|
1422
|
+
}
|
|
1423
|
+
const existingConfig = readConfig(cwd);
|
|
1424
|
+
if (existingConfig && !options.overwrite) {
|
|
1425
|
+
const proceed = await confirm(`${path10.basename(configPath(cwd))} already exists. Overwrite it?`, { yes: options.yes, fallback: false });
|
|
1426
|
+
if (!proceed) {
|
|
1427
|
+
log.info("Keeping the existing components.json; only missing files will be written.");
|
|
1428
|
+
}
|
|
1429
|
+
}
|
|
1430
|
+
const baseRelative = path10.relative(cwd, project.aliasBase).split(path10.sep).join("/");
|
|
1431
|
+
const withBase = (target) => baseRelative && baseRelative !== "." ? `${baseRelative}/${target}` : target;
|
|
1432
|
+
const cssFile = await ask("Where is your global stylesheet?", {
|
|
1433
|
+
yes: options.yes,
|
|
1434
|
+
initial: existingConfig?.tailwind.css ?? project.cssFile ?? defaultCssFile(project.framework, project.srcDir)
|
|
1435
|
+
});
|
|
1436
|
+
const registrySource = resolveRegistrySource(options.registry, existingConfig?.registry);
|
|
1437
|
+
const alias = project.aliasPrefix;
|
|
1438
|
+
const config = {
|
|
1439
|
+
$schema: CONFIG_SCHEMA_URL,
|
|
1440
|
+
style: "default",
|
|
1441
|
+
tsx: project.typescript,
|
|
1442
|
+
tailwind: {
|
|
1443
|
+
css: cssFile,
|
|
1444
|
+
theme: withBase("styles/theme.css"),
|
|
1445
|
+
prefix: ""
|
|
1446
|
+
},
|
|
1447
|
+
aliases: {
|
|
1448
|
+
components: `${alias}components`,
|
|
1449
|
+
utils: `${alias}utils`,
|
|
1450
|
+
ui: `${alias}components/base`,
|
|
1451
|
+
hooks: `${alias}hooks`
|
|
1452
|
+
},
|
|
1453
|
+
registry: registrySource.startsWith("http") ? registrySource : existingConfig?.registry ?? DEFAULT_REGISTRY_URL
|
|
1454
|
+
};
|
|
1455
|
+
if (!existingConfig || options.overwrite) writeConfig(cwd, config);
|
|
1456
|
+
const registry = new Registry(registrySource);
|
|
1457
|
+
const writes = [];
|
|
1458
|
+
const writeOptions = { cwd, overwrite: Boolean(options.overwrite), dryRun: false };
|
|
1459
|
+
const themeSpinner = spinner(`Fetching theme tokens from ${registry.describe()}`);
|
|
1460
|
+
let themeCss = THEME_CSS_PLACEHOLDER;
|
|
1461
|
+
let themeFromRegistry = false;
|
|
1462
|
+
let cxSource = CX_TS_FALLBACK;
|
|
1463
|
+
try {
|
|
1464
|
+
const styles = await registry.item("styles");
|
|
1465
|
+
themeCss = styles.files.find((file) => file.target.endsWith("theme.css"))?.content ?? themeCss;
|
|
1466
|
+
themeFromRegistry = true;
|
|
1467
|
+
cxSource = (await registry.item("cx")).files[0]?.content ?? cxSource;
|
|
1468
|
+
themeSpinner.succeed("Fetched theme tokens and utils/cx from the registry.");
|
|
1469
|
+
} catch (error) {
|
|
1470
|
+
themeSpinner.stop();
|
|
1471
|
+
log.warn(`Registry unavailable (${error.message}).`);
|
|
1472
|
+
log.warn("Wrote placeholder theme tokens \u2014 run `properui add styles --overwrite` once the registry is reachable.");
|
|
1473
|
+
}
|
|
1474
|
+
writes.push(writeSourceFile(path10.resolve(cwd, config.tailwind.theme), themeCss, writeOptions));
|
|
1475
|
+
writes.push(writeSourceFile(path10.resolve(project.aliasBase, config.tsx ? "utils/cx.ts" : "utils/cx.js"), cxSource, writeOptions));
|
|
1476
|
+
writes.push(
|
|
1477
|
+
writeSourceFile(
|
|
1478
|
+
path10.resolve(project.aliasBase, config.tsx ? "providers/theme-provider.tsx" : "providers/theme-provider.jsx"),
|
|
1479
|
+
config.tsx ? THEME_PROVIDER_TSX : THEME_PROVIDER_JSX,
|
|
1480
|
+
writeOptions
|
|
1481
|
+
)
|
|
1482
|
+
);
|
|
1483
|
+
const stylesheet = wireStylesheet(cwd, config, false);
|
|
1484
|
+
const wiring = options.manual ? null : wireThemeProvider(project, config, false);
|
|
1485
|
+
const viteDryRun = Boolean(options.manual);
|
|
1486
|
+
const viteTsconfig = project.framework === "vite" ? wireViteTsconfigPaths(project, viteDryRun) : null;
|
|
1487
|
+
const viteConfigAlias = project.framework === "vite" ? wireViteConfigAlias(project, viteDryRun) : null;
|
|
1488
|
+
log.plain();
|
|
1489
|
+
log.title("Changes");
|
|
1490
|
+
log.step(`${kleur.green("write")} ${path10.relative(cwd, configPath(cwd))}`);
|
|
1491
|
+
for (const result of writes) log.step(`${statusLabel3(result.status)} ${result.relative}`);
|
|
1492
|
+
if (stylesheet.added.length > 0) {
|
|
1493
|
+
log.step(
|
|
1494
|
+
`${kleur.green("write")} ${path10.relative(cwd, stylesheet.file)} (+${stylesheet.added.length} line${stylesheet.added.length === 1 ? "" : "s"})`
|
|
1495
|
+
);
|
|
1496
|
+
for (const line of stylesheet.added) log.plain(kleur.dim(` ${line}`));
|
|
1497
|
+
} else {
|
|
1498
|
+
log.step(`${kleur.dim("keep ")} ${path10.relative(cwd, stylesheet.file)} (already wired)`);
|
|
1499
|
+
}
|
|
1500
|
+
if (wiring?.changed) {
|
|
1501
|
+
log.step(`${kleur.green("write")} ${path10.relative(cwd, wiring.file)} (wrapped in <ThemeProvider>)`);
|
|
1502
|
+
} else {
|
|
1503
|
+
const importPath = `${config.aliases.components.replace(/\/components$/, "")}/providers/theme-provider`;
|
|
1504
|
+
log.plain();
|
|
1505
|
+
log.info(options.manual ? "Manual mode \u2014 wrap your app yourself:" : "Could not wire the provider automatically. Wrap your app root yourself:");
|
|
1506
|
+
log.plain(kleur.dim(` import { ThemeProvider } from "${importPath}";`));
|
|
1507
|
+
log.plain(kleur.dim(" <ThemeProvider>{children}</ThemeProvider>"));
|
|
1508
|
+
}
|
|
1509
|
+
if (viteTsconfig || viteConfigAlias) {
|
|
1510
|
+
log.plain();
|
|
1511
|
+
if (viteTsconfig) logTsconfigAliasResult(cwd, viteTsconfig, Boolean(options.manual));
|
|
1512
|
+
if (viteConfigAlias) logViteConfigAliasResult(cwd, viteConfigAlias, Boolean(options.manual));
|
|
1513
|
+
}
|
|
1514
|
+
log.plain();
|
|
1515
|
+
if (!themeFromRegistry) log.warn("Theme tokens are a placeholder \u2014 see the note above.");
|
|
1516
|
+
const viteAliasFailed = !options.manual && ((viteTsconfig?.status ?? "written") === "unsupported" || (viteConfigAlias?.status ?? "written") === "unsupported");
|
|
1517
|
+
if (viteAliasFailed) {
|
|
1518
|
+
log.error("Could not wire the Vite `@` alias automatically \u2014 add the snippets printed above by hand, then re-run `properui init`.");
|
|
1519
|
+
process.exitCode = 1;
|
|
1520
|
+
return;
|
|
1521
|
+
}
|
|
1522
|
+
log.success("Project configured. Next: npx @properui/cli add button badges");
|
|
1523
|
+
}
|
|
1524
|
+
function statusLabel3(status) {
|
|
1525
|
+
if (status === "created") return kleur.green("write");
|
|
1526
|
+
if (status === "updated") return kleur.yellow("updat");
|
|
1527
|
+
if (status === "skipped") return kleur.dim("skip ");
|
|
1528
|
+
return kleur.dim("keep ");
|
|
1529
|
+
}
|
|
1530
|
+
function logTsconfigAliasResult(cwd, result, manual) {
|
|
1531
|
+
if (result.status === "already-declared") {
|
|
1532
|
+
log.step(`${kleur.dim("keep ")} ${result.file ? path10.relative(cwd, result.file) : "tsconfig.json"} (alias already declared)`);
|
|
1533
|
+
return;
|
|
1534
|
+
}
|
|
1535
|
+
if (result.status === "skipped") {
|
|
1536
|
+
log.warn("No tsconfig.json, tsconfig.app.json or jsconfig.json found \u2014 cannot wire the path alias. Add it yourself:");
|
|
1537
|
+
log.plain(kleur.dim(` ${result.snippet}`));
|
|
1538
|
+
return;
|
|
1539
|
+
}
|
|
1540
|
+
const label = result.file ? path10.relative(cwd, result.file) : "tsconfig.json";
|
|
1541
|
+
if (manual || result.status === "unsupported") {
|
|
1542
|
+
log.info(manual ? `Manual mode \u2014 add this to ${label}'s "compilerOptions":` : `Could not find "compilerOptions" in ${label}. Add this yourself:`);
|
|
1543
|
+
log.plain(kleur.dim(` ${result.snippet}`));
|
|
1544
|
+
return;
|
|
1545
|
+
}
|
|
1546
|
+
log.step(`${kleur.green("write")} ${label} (+ \`paths\` entry)`);
|
|
1547
|
+
}
|
|
1548
|
+
function logViteConfigAliasResult(cwd, result, manual) {
|
|
1549
|
+
if (result.status === "already-present") {
|
|
1550
|
+
log.step(`${kleur.dim("keep ")} ${result.file ? path10.relative(cwd, result.file) : "vite.config.ts"} (resolve.alias already present)`);
|
|
1551
|
+
return;
|
|
1552
|
+
}
|
|
1553
|
+
if (result.status === "skipped") {
|
|
1554
|
+
log.warn("No vite.config.(ts|mts|js|mjs) found. Vite build/tsc will fail on `@/...` imports until you add:");
|
|
1555
|
+
for (const line of result.snippet.split("\n")) log.plain(kleur.dim(` ${line}`));
|
|
1556
|
+
return;
|
|
1557
|
+
}
|
|
1558
|
+
const label = result.file ? path10.relative(cwd, result.file) : "vite.config.ts";
|
|
1559
|
+
if (manual || result.status === "unsupported") {
|
|
1560
|
+
log.info(
|
|
1561
|
+
manual ? `Manual mode \u2014 add this inside ${label}'s defineConfig({ ... }):` : `Could not safely edit ${label} (unrecognised shape). Add this yourself:`
|
|
1562
|
+
);
|
|
1563
|
+
for (const line of result.snippet.split("\n")) log.plain(kleur.dim(` ${line}`));
|
|
1564
|
+
return;
|
|
1565
|
+
}
|
|
1566
|
+
log.step(`${kleur.green("write")} ${label} (+ \`resolve.alias\` for \`${result.aliasKey}\`)`);
|
|
1567
|
+
}
|
|
1568
|
+
|
|
1569
|
+
// src/commands/list.ts
|
|
1570
|
+
import path11 from "path";
|
|
1571
|
+
var truncate = (value, max) => value.length <= max ? value : `${value.slice(0, max - 1).trimEnd()}\u2026`;
|
|
1572
|
+
async function runList(options) {
|
|
1573
|
+
const cwd = path11.resolve(options.cwd ?? process.cwd());
|
|
1574
|
+
const registry = new Registry(resolveRegistrySource(options.registry, readConfig(cwd)?.registry));
|
|
1575
|
+
let entries;
|
|
1576
|
+
try {
|
|
1577
|
+
entries = await registry.index();
|
|
1578
|
+
} catch (error) {
|
|
1579
|
+
log.error(error instanceof RegistryError ? error.message : error.message);
|
|
1580
|
+
process.exitCode = 1;
|
|
1581
|
+
return;
|
|
1582
|
+
}
|
|
1583
|
+
if (options.layer) entries = entries.filter((entry) => entry.layer === options.layer);
|
|
1584
|
+
if (options.type) entries = entries.filter((entry) => entry.type === options.type);
|
|
1585
|
+
if (options.json) {
|
|
1586
|
+
log.plain(JSON.stringify(entries, null, 2));
|
|
1587
|
+
return;
|
|
1588
|
+
}
|
|
1589
|
+
if (entries.length === 0) {
|
|
1590
|
+
log.warn("Nothing matched those filters.");
|
|
1591
|
+
return;
|
|
1592
|
+
}
|
|
1593
|
+
const width = Math.max(...entries.map((entry) => entry.name.length));
|
|
1594
|
+
log.title(`${entries.length} item${entries.length === 1 ? "" : "s"} \xB7 ${registry.describe()}`);
|
|
1595
|
+
for (const entry of entries) {
|
|
1596
|
+
log.plain(` ${kleur.bold(entry.name.padEnd(width))} ${kleur.dim(entry.layer.padEnd(18))} ${truncate(entry.description, 72)}`);
|
|
1597
|
+
}
|
|
1598
|
+
log.plain();
|
|
1599
|
+
log.info("Add one with: npx @properui/cli add <name>");
|
|
1600
|
+
}
|
|
1601
|
+
|
|
1602
|
+
// src/commands/login.ts
|
|
1603
|
+
import path12 from "path";
|
|
1604
|
+
var mask = (token) => `${"\u2022".repeat(Math.max(0, Math.min(token.length, 24) - 4))}${token.slice(-4)}`;
|
|
1605
|
+
async function runLogin(options) {
|
|
1606
|
+
const cwd = path12.resolve(options.cwd ?? process.cwd());
|
|
1607
|
+
const registrySource = resolveRegistrySource(options.registry, readConfig(cwd)?.registry);
|
|
1608
|
+
const existing = readAuth();
|
|
1609
|
+
if (existing && !options.token) log.info(`Existing token for ${existing.registry ?? "the default registry"}: ${mask(existing.token)}`);
|
|
1610
|
+
let token = options.token ?? "";
|
|
1611
|
+
if (!token) {
|
|
1612
|
+
if (!canPrompt() || options.yes) {
|
|
1613
|
+
log.error("No token given. Pass --token <token>, or run `properui login` in an interactive terminal.");
|
|
1614
|
+
process.exitCode = 1;
|
|
1615
|
+
return;
|
|
1616
|
+
}
|
|
1617
|
+
log.plain();
|
|
1618
|
+
log.info(`Create a token at ${kleur.underline(`${registrySource.replace(/\/r$/, "")}/account/tokens`)} and paste it below.`);
|
|
1619
|
+
token = await askSecret("Registry token");
|
|
1620
|
+
}
|
|
1621
|
+
if (!token) {
|
|
1622
|
+
log.error("No token entered.");
|
|
1623
|
+
process.exitCode = 1;
|
|
1624
|
+
return;
|
|
1625
|
+
}
|
|
1626
|
+
const file = writeAuth({ token, registry: registrySource });
|
|
1627
|
+
log.success(`Token saved to ${file} (mode 0600).`);
|
|
1628
|
+
log.info(`Delete ${authFile()} to log out.`);
|
|
1629
|
+
}
|
|
1630
|
+
|
|
1631
|
+
// src/commands/search.ts
|
|
1632
|
+
import path13 from "path";
|
|
1633
|
+
function scoreEntry(entry, query) {
|
|
1634
|
+
return Math.max(
|
|
1635
|
+
fuzzyScore(entry.name, query),
|
|
1636
|
+
0.85 * fuzzyScore(entry.title, query),
|
|
1637
|
+
0.6 * fuzzyScore(entry.description, query),
|
|
1638
|
+
0.7 * Math.max(0, ...entry.examples.map((example) => fuzzyScore(example, query)))
|
|
1639
|
+
);
|
|
1640
|
+
}
|
|
1641
|
+
async function runSearch(query, options) {
|
|
1642
|
+
const cwd = path13.resolve(options.cwd ?? process.cwd());
|
|
1643
|
+
const registry = new Registry(resolveRegistrySource(options.registry, readConfig(cwd)?.registry));
|
|
1644
|
+
let entries;
|
|
1645
|
+
try {
|
|
1646
|
+
entries = await registry.index();
|
|
1647
|
+
} catch (error) {
|
|
1648
|
+
log.error(error instanceof RegistryError ? error.message : error.message);
|
|
1649
|
+
process.exitCode = 1;
|
|
1650
|
+
return;
|
|
1651
|
+
}
|
|
1652
|
+
const limit = Number(options.limit ?? 20);
|
|
1653
|
+
const matches = entries.map((entry) => ({ entry, score: scoreEntry(entry, query) })).filter((match) => match.score > 0.2).sort((a, b) => b.score - a.score || a.entry.name.localeCompare(b.entry.name)).slice(0, Number.isFinite(limit) && limit > 0 ? limit : 20);
|
|
1654
|
+
if (matches.length === 0) {
|
|
1655
|
+
log.warn(`Nothing matched "${query}".`);
|
|
1656
|
+
return;
|
|
1657
|
+
}
|
|
1658
|
+
const width = Math.max(...matches.map((match) => match.entry.name.length));
|
|
1659
|
+
log.title(`${matches.length} match${matches.length === 1 ? "" : "es"} for "${query}"`);
|
|
1660
|
+
for (const { entry } of matches) {
|
|
1661
|
+
const examples = entry.examples.length > 0 ? kleur.dim(` \xB7 ${entry.examples.length} examples`) : "";
|
|
1662
|
+
log.plain(` ${kleur.bold(entry.name.padEnd(width))} ${kleur.dim(entry.layer.padEnd(18))} ${entry.title}${examples}`);
|
|
1663
|
+
}
|
|
1664
|
+
}
|
|
1665
|
+
|
|
1666
|
+
// src/index.ts
|
|
1667
|
+
var REGISTRY_HELP = `registry directory or base URL (default: $REGISTRY_URL or ${DEFAULT_REGISTRY_URL})`;
|
|
1668
|
+
function guard(action) {
|
|
1669
|
+
return async (...args) => {
|
|
1670
|
+
try {
|
|
1671
|
+
await action(...args);
|
|
1672
|
+
} catch (error) {
|
|
1673
|
+
if (error instanceof CancelledError) {
|
|
1674
|
+
log.warn("Cancelled.");
|
|
1675
|
+
process.exitCode = 130;
|
|
1676
|
+
return;
|
|
1677
|
+
}
|
|
1678
|
+
log.error(error instanceof RegistryError ? error.message : error.message ?? String(error));
|
|
1679
|
+
if (process.env.PROPERUI_DEBUG) console.error(error);
|
|
1680
|
+
process.exitCode = 1;
|
|
1681
|
+
}
|
|
1682
|
+
};
|
|
1683
|
+
}
|
|
1684
|
+
var program = new Command();
|
|
1685
|
+
program.name("properui").description("Add Proper UI components to your project").version("0.1.0").option("--cwd <dir>", "run against another directory", process.cwd());
|
|
1686
|
+
program.command("init").description("Configure this project: components.json, theme tokens, cx util and the ThemeProvider").option("--nextjs", "treat this project as Next.js instead of auto-detecting").option("--vite", "treat this project as Vite instead of auto-detecting").option("--manual", "write the files but do not edit the app entry point").option("--overwrite", "replace components.json and any files that already exist").option("--registry <source>", REGISTRY_HELP).option("-y, --yes", "accept every default; never prompt").action(guard(async (options) => runInit({ ...options, cwd: program.opts().cwd })));
|
|
1687
|
+
program.command("add").description("Add components (or `add example <name>` for a full page example) and their dependencies").argument("[components...]", "component names, or `example <name>`").option("--all", "add every component in the registry").option("--overwrite", "replace files that already exist").option("--path <dir>", "put component files in this directory instead of the components alias").option("--dry-run", "print what would change without writing anything").option("--registry <source>", REGISTRY_HELP).option("-y, --yes", "accept every default; never prompt").action(guard(async (components, options) => runAdd(components, { ...options, cwd: program.opts().cwd })));
|
|
1688
|
+
program.command("list").description("List available components with layer and description").option("--layer <layer>", "filter by layer, e.g. base, application, marketing").option("--type <type>", "filter by type: component, example, util, hook, style").option("--json", "print the raw index rows").option("--registry <source>", REGISTRY_HELP).option("-y, --yes", "accept every default; never prompt").action(guard(async (options) => runList({ ...options, cwd: program.opts().cwd })));
|
|
1689
|
+
program.command("search").description("Fuzzy search over component names, descriptions and example names").argument("<query>").option("--limit <n>", "maximum results", "20").option("--registry <source>", REGISTRY_HELP).option("-y, --yes", "accept every default; never prompt").action(guard(async (query, options) => runSearch(query, { ...options, cwd: program.opts().cwd })));
|
|
1690
|
+
program.command("diff").description("Show local modifications against the registry version").argument("[component]", "component to compare; omit to check everything already installed").option("--registry <source>", REGISTRY_HELP).option("-y, --yes", "accept every default; never prompt").action(guard(async (component, options) => runDiff(component, { ...options, cwd: program.opts().cwd })));
|
|
1691
|
+
program.command("agent").description("Manage the portable Proper UI Skill for AI coding tools").addCommand(
|
|
1692
|
+
new Command("init").description("Install the Proper UI Skill for an AI coding tool: claude, codex, cursor, lovable, or all").option("--client <client>", "claude, codex, cursor, lovable, or all", "all").option("--overwrite", "replace the Skill file even if it already exists").option("-y, --yes", "accept every default; never prompt").action(guard(async (options) => runAgentInit({ ...options, cwd: program.opts().cwd })))
|
|
1693
|
+
);
|
|
1694
|
+
program.command("info").description("Show this project's Proper UI setup: framework, Tailwind, aliases and installed entries").option("--json", "print machine-readable JSON instead of a formatted report").option("--registry <source>", REGISTRY_HELP).action(guard(async (options) => runInfo({ ...options, cwd: program.opts().cwd })));
|
|
1695
|
+
program.command("login").description("Store a registry token at ~/.properui/auth.json (private registries only)").option("--token <token>", "use this token instead of prompting").option("--registry <source>", REGISTRY_HELP).option("-y, --yes", "accept every default; never prompt").action(guard(async (options) => runLogin({ ...options, cwd: program.opts().cwd })));
|
|
1696
|
+
program.parseAsync().catch((error) => {
|
|
1697
|
+
log.error(error.message);
|
|
1698
|
+
process.exit(1);
|
|
1699
|
+
});
|