create-rubrica 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/README.md +86 -0
- package/dist/index.js +1584 -0
- package/package.json +51 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1584 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { intro as intro3 } from "@clack/prompts";
|
|
5
|
+
|
|
6
|
+
// src/commands/create.ts
|
|
7
|
+
import { outro, text as text2, select as select2, multiselect, confirm } from "@clack/prompts";
|
|
8
|
+
import * as nodeFsPromises9 from "fs/promises";
|
|
9
|
+
|
|
10
|
+
// src/prompts/identityPrompt.ts
|
|
11
|
+
import { text, select, isCancel, cancel } from "@clack/prompts";
|
|
12
|
+
function assertNotCancelled(value) {
|
|
13
|
+
if (isCancel(value)) {
|
|
14
|
+
cancel("Opera\xE7\xE3o cancelada.");
|
|
15
|
+
throw new Error("Prompt cancelado pelo usu\xE1rio.");
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
async function identityPrompt(defaults) {
|
|
19
|
+
const siteName = await text({
|
|
20
|
+
message: "Nome do site (aparece no t\xEDtulo do browser e SEO)",
|
|
21
|
+
initialValue: defaults?.siteName ?? ""
|
|
22
|
+
});
|
|
23
|
+
assertNotCancelled(siteName);
|
|
24
|
+
const siteUrl = await text({
|
|
25
|
+
message: "URL do site (sem barra no final)",
|
|
26
|
+
initialValue: defaults?.siteUrl ?? "",
|
|
27
|
+
validate: (v) => {
|
|
28
|
+
if (!v.startsWith("http://") && !v.startsWith("https://")) {
|
|
29
|
+
return "A URL deve come\xE7ar com http:// ou https://";
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
assertNotCancelled(siteUrl);
|
|
34
|
+
const siteDescription = await text({
|
|
35
|
+
message: "Descri\xE7\xE3o curta (SEO e Open Graph)",
|
|
36
|
+
initialValue: defaults?.siteDescription ?? ""
|
|
37
|
+
});
|
|
38
|
+
assertNotCancelled(siteDescription);
|
|
39
|
+
const authorName = await text({
|
|
40
|
+
message: "Seu nome (para RSS e meta author)",
|
|
41
|
+
initialValue: defaults?.authorName ?? ""
|
|
42
|
+
});
|
|
43
|
+
assertNotCancelled(authorName);
|
|
44
|
+
const authorEmail = await text({
|
|
45
|
+
message: "Seu email (para RSS \u2014 n\xE3o aparece publicamente)",
|
|
46
|
+
initialValue: defaults?.authorEmail ?? "",
|
|
47
|
+
validate: (v) => {
|
|
48
|
+
if (!v.includes("@") || !v.includes(".")) {
|
|
49
|
+
return "Informe um email v\xE1lido (ex: contato@meusite.com)";
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
assertNotCancelled(authorEmail);
|
|
54
|
+
const twitterHandle = await text({
|
|
55
|
+
message: "Twitter/X handle (sem @, Enter para pular)",
|
|
56
|
+
initialValue: defaults?.twitterHandle ?? ""
|
|
57
|
+
});
|
|
58
|
+
assertNotCancelled(twitterHandle);
|
|
59
|
+
const lang = await select({
|
|
60
|
+
message: "Idioma padr\xE3o",
|
|
61
|
+
initialValue: defaults?.lang ?? "pt-BR",
|
|
62
|
+
options: [
|
|
63
|
+
{ value: "pt-BR", label: "Portugu\xEAs (pt-BR)" },
|
|
64
|
+
{ value: "en-US", label: "Ingl\xEAs (en-US)" }
|
|
65
|
+
]
|
|
66
|
+
});
|
|
67
|
+
assertNotCancelled(lang);
|
|
68
|
+
return {
|
|
69
|
+
siteName,
|
|
70
|
+
siteUrl,
|
|
71
|
+
siteDescription,
|
|
72
|
+
authorName,
|
|
73
|
+
authorEmail,
|
|
74
|
+
twitterHandle: twitterHandle ?? "",
|
|
75
|
+
lang
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// src/utils/download.ts
|
|
80
|
+
import { writeFile, mkdtemp, rm, mkdir } from "fs/promises";
|
|
81
|
+
import { execFile } from "child_process";
|
|
82
|
+
import { tmpdir } from "os";
|
|
83
|
+
import { join } from "path";
|
|
84
|
+
import { promisify } from "util";
|
|
85
|
+
var execFileAsync = promisify(execFile);
|
|
86
|
+
var GITHUB_REPO = "MierzwaMatheus/rubrica";
|
|
87
|
+
async function getLatestVersion() {
|
|
88
|
+
const url = `https://api.github.com/repos/${GITHUB_REPO}/releases/latest`;
|
|
89
|
+
let response;
|
|
90
|
+
try {
|
|
91
|
+
response = await fetch(url, {
|
|
92
|
+
headers: { "User-Agent": "create-rubrica-cli" }
|
|
93
|
+
});
|
|
94
|
+
} catch {
|
|
95
|
+
throw new Error(
|
|
96
|
+
"Erro de rede ao consultar o GitHub. Verifique sua conex\xE3o e tente novamente."
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
if (!response.ok) {
|
|
100
|
+
throw new Error(
|
|
101
|
+
`N\xE3o foi poss\xEDvel obter a vers\xE3o mais recente do Rubrica (HTTP ${response.status}). Verifique sua conex\xE3o.`
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
const data = await response.json();
|
|
105
|
+
if (!data.tag_name) {
|
|
106
|
+
throw new Error(
|
|
107
|
+
"Release sem tarball encontrada no GitHub. Tente novamente mais tarde."
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
return data.tag_name;
|
|
111
|
+
}
|
|
112
|
+
async function downloadRelease(targetDir, version) {
|
|
113
|
+
const tarballUrl = `https://api.github.com/repos/${GITHUB_REPO}/tarball/${version}`;
|
|
114
|
+
let response;
|
|
115
|
+
try {
|
|
116
|
+
response = await fetch(tarballUrl, {
|
|
117
|
+
headers: { "User-Agent": "create-rubrica-cli" },
|
|
118
|
+
redirect: "follow"
|
|
119
|
+
});
|
|
120
|
+
} catch {
|
|
121
|
+
throw new Error(
|
|
122
|
+
"Erro de rede ao baixar o Rubrica. Verifique sua conex\xE3o e tente novamente."
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
if (!response.ok) {
|
|
126
|
+
throw new Error(
|
|
127
|
+
`N\xE3o foi poss\xEDvel baixar o Rubrica (HTTP ${response.status}).`
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
if (!response.body) {
|
|
131
|
+
throw new Error("Release sem tarball encontrada. Tente novamente mais tarde.");
|
|
132
|
+
}
|
|
133
|
+
const tmpDir = await mkdtemp(join(tmpdir(), "rubrica-download-"));
|
|
134
|
+
const tmpFile = join(tmpDir, "rubrica.tar.gz");
|
|
135
|
+
try {
|
|
136
|
+
const buffer = await response.arrayBuffer();
|
|
137
|
+
await writeFile(tmpFile, Buffer.from(buffer));
|
|
138
|
+
await mkdir(targetDir, { recursive: true });
|
|
139
|
+
await execFileAsync("tar", ["-xzf", tmpFile, "--strip-components=1", "-C", targetDir]);
|
|
140
|
+
} finally {
|
|
141
|
+
await rm(tmpDir, { recursive: true, force: true });
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// src/transforms/applyLayout.ts
|
|
146
|
+
import * as nodeFsPromises from "fs/promises";
|
|
147
|
+
import * as path from "path";
|
|
148
|
+
var LAYOUT_FILES = {
|
|
149
|
+
sidebar: {
|
|
150
|
+
copy: ["Layout.tsx", "Sidebar.tsx"],
|
|
151
|
+
remove: ["Navbar.tsx", "Footer.tsx"]
|
|
152
|
+
},
|
|
153
|
+
topbar: {
|
|
154
|
+
copy: ["Layout.tsx", "Navbar.tsx"],
|
|
155
|
+
remove: ["Sidebar.tsx", "Footer.tsx"]
|
|
156
|
+
},
|
|
157
|
+
centered: {
|
|
158
|
+
copy: ["Layout.tsx", "Footer.tsx"],
|
|
159
|
+
remove: ["Sidebar.tsx", "Navbar.tsx"]
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
var defaultFs = {
|
|
163
|
+
...nodeFsPromises,
|
|
164
|
+
exists: async (p) => {
|
|
165
|
+
try {
|
|
166
|
+
await nodeFsPromises.access(p);
|
|
167
|
+
return true;
|
|
168
|
+
} catch {
|
|
169
|
+
return false;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
async function applyLayout(layout, options, fsModule = defaultFs) {
|
|
174
|
+
const spec = LAYOUT_FILES[layout];
|
|
175
|
+
if (!spec) {
|
|
176
|
+
throw new Error(`applyLayout: layout "${layout}" n\xE3o reconhecido.`);
|
|
177
|
+
}
|
|
178
|
+
const { projectDir, templatesDir } = options;
|
|
179
|
+
const srcDir = path.join(templatesDir, "layouts", layout);
|
|
180
|
+
const destDir = path.join(projectDir, "src", "components");
|
|
181
|
+
await fsModule.mkdir(destDir, { recursive: true });
|
|
182
|
+
for (const file of spec.copy) {
|
|
183
|
+
await fsModule.copyFile(path.join(srcDir, file), path.join(destDir, file));
|
|
184
|
+
}
|
|
185
|
+
for (const file of spec.remove) {
|
|
186
|
+
const filePath = path.join(destDir, file);
|
|
187
|
+
if (await fsModule.exists(filePath)) {
|
|
188
|
+
await fsModule.unlink(filePath);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// src/transforms/applyTheme.ts
|
|
194
|
+
import * as nodeFsPromises2 from "fs/promises";
|
|
195
|
+
|
|
196
|
+
// src/utils/hexToHsl.ts
|
|
197
|
+
function hexToHsl(hex) {
|
|
198
|
+
const clean = hex.startsWith("#") ? hex.slice(1) : hex;
|
|
199
|
+
if (!/^[0-9a-fA-F]{6}$/.test(clean)) {
|
|
200
|
+
throw new Error(`hexToHsl: valor inv\xE1lido "${hex}". Esperado hex de 6 d\xEDgitos (ex: #ff0000).`);
|
|
201
|
+
}
|
|
202
|
+
const r = parseInt(clean.slice(0, 2), 16) / 255;
|
|
203
|
+
const g = parseInt(clean.slice(2, 4), 16) / 255;
|
|
204
|
+
const b = parseInt(clean.slice(4, 6), 16) / 255;
|
|
205
|
+
const max = Math.max(r, g, b);
|
|
206
|
+
const min = Math.min(r, g, b);
|
|
207
|
+
const delta = max - min;
|
|
208
|
+
const l = (max + min) / 2;
|
|
209
|
+
let s = 0;
|
|
210
|
+
if (delta !== 0) {
|
|
211
|
+
s = delta / (1 - Math.abs(2 * l - 1));
|
|
212
|
+
}
|
|
213
|
+
let h = 0;
|
|
214
|
+
if (delta !== 0) {
|
|
215
|
+
if (max === r) h = (g - b) / delta % 6;
|
|
216
|
+
else if (max === g) h = (b - r) / delta + 2;
|
|
217
|
+
else h = (r - g) / delta + 4;
|
|
218
|
+
h = h * 60;
|
|
219
|
+
if (h < 0) h += 360;
|
|
220
|
+
}
|
|
221
|
+
return {
|
|
222
|
+
h: Math.round(h),
|
|
223
|
+
s: Math.round(s * 100),
|
|
224
|
+
l: Math.round(l * 100)
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// src/transforms/applyTheme.ts
|
|
229
|
+
var PRESETS = {
|
|
230
|
+
forest: {
|
|
231
|
+
root: ` --background: 60 20% 97%;
|
|
232
|
+
--foreground: 120 8% 12%;
|
|
233
|
+
--card: 60 20% 99%;
|
|
234
|
+
--card-foreground: 120 8% 12%;
|
|
235
|
+
--popover: 60 20% 99%;
|
|
236
|
+
--popover-foreground: 120 8% 12%;
|
|
237
|
+
--primary: 150 40% 35%;
|
|
238
|
+
--primary-foreground: 0 0% 100%;
|
|
239
|
+
--secondary: 80 15% 90%;
|
|
240
|
+
--secondary-foreground: 120 8% 12%;
|
|
241
|
+
--muted: 80 15% 90%;
|
|
242
|
+
--muted-foreground: 120 5% 45%;
|
|
243
|
+
--accent: 80 15% 90%;
|
|
244
|
+
--accent-foreground: 120 8% 12%;
|
|
245
|
+
--destructive: 0 84% 60%;
|
|
246
|
+
--destructive-foreground: 0 0% 100%;
|
|
247
|
+
--border: 80 15% 84%;
|
|
248
|
+
--input: 80 15% 84%;
|
|
249
|
+
--ring: 150 40% 35%;
|
|
250
|
+
--radius: 0.5rem;
|
|
251
|
+
--sidebar: 60 20% 95%;
|
|
252
|
+
--sidebar-foreground: 120 5% 45%;
|
|
253
|
+
--sidebar-primary: 150 40% 35%;
|
|
254
|
+
--sidebar-primary-foreground: 0 0% 100%;
|
|
255
|
+
--sidebar-accent: 80 15% 90%;
|
|
256
|
+
--sidebar-accent-foreground: 120 8% 12%;
|
|
257
|
+
--sidebar-border: 80 15% 84%;
|
|
258
|
+
--sidebar-ring: 150 40% 35%;
|
|
259
|
+
--neon-purple: 150 40% 35%;
|
|
260
|
+
--neon-lime: 80 50% 40%;`,
|
|
261
|
+
dark: ` --background: 30 15% 8%;
|
|
262
|
+
--foreground: 60 15% 88%;
|
|
263
|
+
--card: 30 12% 12%;
|
|
264
|
+
--card-foreground: 60 15% 88%;
|
|
265
|
+
--popover: 30 12% 12%;
|
|
266
|
+
--popover-foreground: 60 15% 88%;
|
|
267
|
+
--primary: 150 40% 35%;
|
|
268
|
+
--primary-foreground: 0 0% 100%;
|
|
269
|
+
--secondary: 30 10% 18%;
|
|
270
|
+
--secondary-foreground: 60 15% 88%;
|
|
271
|
+
--muted: 30 10% 18%;
|
|
272
|
+
--muted-foreground: 60 8% 55%;
|
|
273
|
+
--accent: 30 10% 18%;
|
|
274
|
+
--accent-foreground: 60 15% 88%;
|
|
275
|
+
--destructive: 0 62% 50%;
|
|
276
|
+
--destructive-foreground: 0 0% 100%;
|
|
277
|
+
--border: 30 10% 18%;
|
|
278
|
+
--input: 30 10% 18%;
|
|
279
|
+
--ring: 150 40% 35%;
|
|
280
|
+
--sidebar: 30 15% 6%;
|
|
281
|
+
--sidebar-foreground: 60 8% 55%;
|
|
282
|
+
--sidebar-primary: 150 40% 35%;
|
|
283
|
+
--sidebar-primary-foreground: 0 0% 100%;
|
|
284
|
+
--sidebar-accent: 30 10% 18%;
|
|
285
|
+
--sidebar-accent-foreground: 60 15% 88%;
|
|
286
|
+
--sidebar-border: 30 10% 18%;
|
|
287
|
+
--sidebar-ring: 150 40% 35%;`
|
|
288
|
+
},
|
|
289
|
+
editorial: {
|
|
290
|
+
root: ` --background: 38 30% 96%;
|
|
291
|
+
--foreground: 24 10% 10%;
|
|
292
|
+
--card: 38 30% 99%;
|
|
293
|
+
--card-foreground: 24 10% 10%;
|
|
294
|
+
--popover: 38 30% 99%;
|
|
295
|
+
--popover-foreground: 24 10% 10%;
|
|
296
|
+
--primary: 35 90% 50%;
|
|
297
|
+
--primary-foreground: 0 0% 100%;
|
|
298
|
+
--secondary: 38 25% 90%;
|
|
299
|
+
--secondary-foreground: 24 10% 10%;
|
|
300
|
+
--muted: 38 25% 90%;
|
|
301
|
+
--muted-foreground: 24 8% 45%;
|
|
302
|
+
--accent: 38 25% 90%;
|
|
303
|
+
--accent-foreground: 24 10% 10%;
|
|
304
|
+
--destructive: 0 84% 60%;
|
|
305
|
+
--destructive-foreground: 0 0% 100%;
|
|
306
|
+
--border: 35 20% 85%;
|
|
307
|
+
--input: 35 20% 85%;
|
|
308
|
+
--ring: 35 90% 50%;
|
|
309
|
+
--radius: 0.375rem;
|
|
310
|
+
--sidebar: 38 30% 94%;
|
|
311
|
+
--sidebar-foreground: 24 8% 45%;
|
|
312
|
+
--sidebar-primary: 35 90% 50%;
|
|
313
|
+
--sidebar-primary-foreground: 0 0% 100%;
|
|
314
|
+
--sidebar-accent: 38 25% 90%;
|
|
315
|
+
--sidebar-accent-foreground: 24 10% 10%;
|
|
316
|
+
--sidebar-border: 35 20% 85%;
|
|
317
|
+
--sidebar-ring: 35 90% 50%;
|
|
318
|
+
--neon-purple: 35 90% 50%;
|
|
319
|
+
--neon-lime: 45 93% 47%;`,
|
|
320
|
+
dark: ` --background: 25 20% 8%;
|
|
321
|
+
--foreground: 38 20% 90%;
|
|
322
|
+
--card: 25 18% 12%;
|
|
323
|
+
--card-foreground: 38 20% 90%;
|
|
324
|
+
--popover: 25 18% 12%;
|
|
325
|
+
--popover-foreground: 38 20% 90%;
|
|
326
|
+
--primary: 35 90% 50%;
|
|
327
|
+
--primary-foreground: 0 0% 100%;
|
|
328
|
+
--secondary: 25 15% 18%;
|
|
329
|
+
--secondary-foreground: 38 20% 90%;
|
|
330
|
+
--muted: 25 15% 18%;
|
|
331
|
+
--muted-foreground: 35 10% 55%;
|
|
332
|
+
--accent: 25 15% 18%;
|
|
333
|
+
--accent-foreground: 38 20% 90%;
|
|
334
|
+
--destructive: 0 62% 50%;
|
|
335
|
+
--destructive-foreground: 0 0% 100%;
|
|
336
|
+
--border: 25 15% 18%;
|
|
337
|
+
--input: 25 15% 18%;
|
|
338
|
+
--ring: 35 90% 50%;
|
|
339
|
+
--sidebar: 25 20% 6%;
|
|
340
|
+
--sidebar-foreground: 35 10% 55%;
|
|
341
|
+
--sidebar-primary: 35 90% 50%;
|
|
342
|
+
--sidebar-primary-foreground: 0 0% 100%;
|
|
343
|
+
--sidebar-accent: 25 15% 18%;
|
|
344
|
+
--sidebar-accent-foreground: 38 20% 90%;
|
|
345
|
+
--sidebar-border: 25 15% 18%;
|
|
346
|
+
--sidebar-ring: 35 90% 50%;`
|
|
347
|
+
},
|
|
348
|
+
minimal: {
|
|
349
|
+
root: ` --background: 0 0% 98%;
|
|
350
|
+
--foreground: 222 47% 11%;
|
|
351
|
+
--card: 0 0% 100%;
|
|
352
|
+
--card-foreground: 222 47% 11%;
|
|
353
|
+
--popover: 0 0% 100%;
|
|
354
|
+
--popover-foreground: 222 47% 11%;
|
|
355
|
+
--primary: 220 90% 56%;
|
|
356
|
+
--primary-foreground: 0 0% 100%;
|
|
357
|
+
--secondary: 210 40% 96%;
|
|
358
|
+
--secondary-foreground: 222 47% 11%;
|
|
359
|
+
--muted: 210 40% 96%;
|
|
360
|
+
--muted-foreground: 215 16% 47%;
|
|
361
|
+
--accent: 210 40% 96%;
|
|
362
|
+
--accent-foreground: 222 47% 11%;
|
|
363
|
+
--destructive: 0 84% 60%;
|
|
364
|
+
--destructive-foreground: 0 0% 100%;
|
|
365
|
+
--border: 214 32% 91%;
|
|
366
|
+
--input: 214 32% 91%;
|
|
367
|
+
--ring: 220 90% 56%;
|
|
368
|
+
--radius: 0.5rem;
|
|
369
|
+
--sidebar: 210 40% 98%;
|
|
370
|
+
--sidebar-foreground: 215 16% 47%;
|
|
371
|
+
--sidebar-primary: 220 90% 56%;
|
|
372
|
+
--sidebar-primary-foreground: 0 0% 100%;
|
|
373
|
+
--sidebar-accent: 210 40% 96%;
|
|
374
|
+
--sidebar-accent-foreground: 222 47% 11%;
|
|
375
|
+
--sidebar-border: 214 32% 91%;
|
|
376
|
+
--sidebar-ring: 220 90% 56%;
|
|
377
|
+
--neon-purple: 220 90% 56%;
|
|
378
|
+
--neon-lime: 142 76% 36%;`,
|
|
379
|
+
dark: ` --background: 220 14% 10%;
|
|
380
|
+
--foreground: 210 40% 96%;
|
|
381
|
+
--card: 222 47% 11%;
|
|
382
|
+
--card-foreground: 210 40% 96%;
|
|
383
|
+
--popover: 222 47% 11%;
|
|
384
|
+
--popover-foreground: 210 40% 96%;
|
|
385
|
+
--primary: 220 90% 56%;
|
|
386
|
+
--primary-foreground: 0 0% 100%;
|
|
387
|
+
--secondary: 217 33% 17%;
|
|
388
|
+
--secondary-foreground: 210 40% 96%;
|
|
389
|
+
--muted: 217 33% 17%;
|
|
390
|
+
--muted-foreground: 215 20% 65%;
|
|
391
|
+
--accent: 217 33% 17%;
|
|
392
|
+
--accent-foreground: 210 40% 96%;
|
|
393
|
+
--destructive: 0 62% 50%;
|
|
394
|
+
--destructive-foreground: 0 0% 100%;
|
|
395
|
+
--border: 217 33% 17%;
|
|
396
|
+
--input: 217 33% 17%;
|
|
397
|
+
--ring: 220 90% 56%;
|
|
398
|
+
--sidebar: 222 47% 8%;
|
|
399
|
+
--sidebar-foreground: 215 20% 65%;
|
|
400
|
+
--sidebar-primary: 220 90% 56%;
|
|
401
|
+
--sidebar-primary-foreground: 0 0% 100%;
|
|
402
|
+
--sidebar-accent: 217 33% 17%;
|
|
403
|
+
--sidebar-accent-foreground: 210 40% 96%;
|
|
404
|
+
--sidebar-border: 217 33% 17%;
|
|
405
|
+
--sidebar-ring: 220 90% 56%;`
|
|
406
|
+
},
|
|
407
|
+
cyberpunk: {
|
|
408
|
+
root: ` --background: 0 0% 14.5%;
|
|
409
|
+
--foreground: 0 0% 90%;
|
|
410
|
+
--card: 0 0% 4%;
|
|
411
|
+
--card-foreground: 0 0% 90%;
|
|
412
|
+
--popover: 0 0% 4%;
|
|
413
|
+
--popover-foreground: 0 0% 90%;
|
|
414
|
+
--primary: 263 90% 66%;
|
|
415
|
+
--primary-foreground: 0 0% 100%;
|
|
416
|
+
--secondary: 215 28% 17%;
|
|
417
|
+
--secondary-foreground: 0 0% 90%;
|
|
418
|
+
--muted: 0 0% 9%;
|
|
419
|
+
--muted-foreground: 0 0% 64%;
|
|
420
|
+
--accent: 0 0% 9%;
|
|
421
|
+
--accent-foreground: 0 0% 90%;
|
|
422
|
+
--destructive: 0 84% 60%;
|
|
423
|
+
--destructive-foreground: 0 0% 100%;
|
|
424
|
+
--border: 0 0% 15%;
|
|
425
|
+
--input: 0 0% 15%;
|
|
426
|
+
--ring: 263 90% 66%;
|
|
427
|
+
--radius: 0.5rem;
|
|
428
|
+
--sidebar: 0 0% 0%;
|
|
429
|
+
--sidebar-foreground: 0 0% 64%;
|
|
430
|
+
--sidebar-primary: 263 90% 66%;
|
|
431
|
+
--sidebar-primary-foreground: 0 0% 100%;
|
|
432
|
+
--sidebar-accent: 0 0% 9%;
|
|
433
|
+
--sidebar-accent-foreground: 0 0% 90%;
|
|
434
|
+
--sidebar-border: 0 0% 15%;
|
|
435
|
+
--sidebar-ring: 263 90% 66%;
|
|
436
|
+
--neon-purple: 271 91% 65%;
|
|
437
|
+
--neon-lime: 82 85% 67%;`,
|
|
438
|
+
dark: ` --background: 0 0% 8%;
|
|
439
|
+
--foreground: 0 0% 90%;
|
|
440
|
+
--card: 0 0% 4%;
|
|
441
|
+
--card-foreground: 0 0% 90%;
|
|
442
|
+
--popover: 0 0% 4%;
|
|
443
|
+
--popover-foreground: 0 0% 90%;
|
|
444
|
+
--primary: 263 90% 66%;
|
|
445
|
+
--primary-foreground: 0 0% 100%;
|
|
446
|
+
--secondary: 215 28% 17%;
|
|
447
|
+
--secondary-foreground: 0 0% 90%;
|
|
448
|
+
--muted: 0 0% 9%;
|
|
449
|
+
--muted-foreground: 0 0% 64%;
|
|
450
|
+
--accent: 0 0% 9%;
|
|
451
|
+
--accent-foreground: 0 0% 90%;
|
|
452
|
+
--destructive: 0 84% 60%;
|
|
453
|
+
--destructive-foreground: 0 0% 100%;
|
|
454
|
+
--border: 0 0% 15%;
|
|
455
|
+
--input: 0 0% 15%;
|
|
456
|
+
--ring: 263 90% 66%;
|
|
457
|
+
--sidebar: 0 0% 4%;
|
|
458
|
+
--sidebar-foreground: 0 0% 64%;
|
|
459
|
+
--sidebar-primary: 263 90% 66%;
|
|
460
|
+
--sidebar-primary-foreground: 0 0% 100%;
|
|
461
|
+
--sidebar-accent: 0 0% 9%;
|
|
462
|
+
--sidebar-accent-foreground: 0 0% 90%;
|
|
463
|
+
--sidebar-border: 0 0% 15%;
|
|
464
|
+
--sidebar-ring: 263 90% 66%;`
|
|
465
|
+
}
|
|
466
|
+
};
|
|
467
|
+
function buildCustomThemeVars(accentColor) {
|
|
468
|
+
const { h, s, l } = hexToHsl(accentColor);
|
|
469
|
+
const hsl = `${h} ${s}% ${l}%`;
|
|
470
|
+
const vars = ` --primary: ${hsl};
|
|
471
|
+
--ring: ${hsl};
|
|
472
|
+
--sidebar-primary: ${hsl};
|
|
473
|
+
--sidebar-ring: ${hsl};`;
|
|
474
|
+
return { root: vars, dark: vars };
|
|
475
|
+
}
|
|
476
|
+
function replaceBlock(css, selector, newVars) {
|
|
477
|
+
const re = new RegExp(`(${selector.replace(".", "\\.")} \\{)[^}]*(\\})`, "s");
|
|
478
|
+
return css.replace(re, `$1
|
|
479
|
+
${newVars}
|
|
480
|
+
$2`);
|
|
481
|
+
}
|
|
482
|
+
async function applyTheme(options, cssPath, fsModule = nodeFsPromises2) {
|
|
483
|
+
let vars;
|
|
484
|
+
if (options.preset) {
|
|
485
|
+
const theme = PRESETS[options.preset];
|
|
486
|
+
if (!theme) {
|
|
487
|
+
throw new Error(`applyTheme: preset "${options.preset}" n\xE3o encontrado.`);
|
|
488
|
+
}
|
|
489
|
+
vars = theme;
|
|
490
|
+
} else if (options.accentColor) {
|
|
491
|
+
vars = buildCustomThemeVars(options.accentColor);
|
|
492
|
+
} else {
|
|
493
|
+
throw new Error("applyTheme: forne\xE7a preset ou accentColor.");
|
|
494
|
+
}
|
|
495
|
+
const css = await fsModule.readFile(cssPath, "utf-8");
|
|
496
|
+
let updated = replaceBlock(css, ":root", vars.root);
|
|
497
|
+
updated = replaceBlock(updated, ".dark", vars.dark);
|
|
498
|
+
await fsModule.writeFile(cssPath, updated);
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// src/transforms/applyFont.ts
|
|
502
|
+
import * as nodeFsPromises3 from "fs/promises";
|
|
503
|
+
function fontToGoogleSlug(font) {
|
|
504
|
+
return font.replace(/ /g, "+");
|
|
505
|
+
}
|
|
506
|
+
function buildFontLink(font) {
|
|
507
|
+
const slug = fontToGoogleSlug(font);
|
|
508
|
+
return `<link
|
|
509
|
+
href="https://fonts.googleapis.com/css2?family=${slug}:wght@300;400;500;600;700&display=swap"
|
|
510
|
+
rel="stylesheet"
|
|
511
|
+
/>`;
|
|
512
|
+
}
|
|
513
|
+
async function applyFont(options, paths, fsModule = nodeFsPromises3) {
|
|
514
|
+
const { fontSans, fontMono, radius } = options;
|
|
515
|
+
let css = await fsModule.readFile(paths.css, "utf-8");
|
|
516
|
+
css = css.replace(
|
|
517
|
+
/(--font-sans:\s*")[^"]*(")/,
|
|
518
|
+
`$1${fontSans}$2`
|
|
519
|
+
);
|
|
520
|
+
css = css.replace(
|
|
521
|
+
/(--font-mono:\s*")[^"]*(")/,
|
|
522
|
+
`$1${fontMono}$2`
|
|
523
|
+
);
|
|
524
|
+
css = css.replace(
|
|
525
|
+
/(--radius:\s*)[^;]+(;)/,
|
|
526
|
+
`$1${radius}$2`
|
|
527
|
+
);
|
|
528
|
+
await fsModule.writeFile(paths.css, css);
|
|
529
|
+
let html = await fsModule.readFile(paths.html, "utf-8");
|
|
530
|
+
const newLink = buildFontLink(fontSans);
|
|
531
|
+
html = html.replace(
|
|
532
|
+
/<link\s[^>]*fonts\.googleapis\.com\/css2[^>]*(?:\s*\/>|>[^<]*<\/link>)/s,
|
|
533
|
+
newLink
|
|
534
|
+
);
|
|
535
|
+
await fsModule.writeFile(paths.html, html);
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
// src/transforms/applyPlugins.ts
|
|
539
|
+
import * as nodeFsPromises4 from "fs/promises";
|
|
540
|
+
async function applyPlugins(plugins, registryPath, fsModule = nodeFsPromises4) {
|
|
541
|
+
let content = await fsModule.readFile(registryPath, "utf-8");
|
|
542
|
+
for (const [id, enabled] of Object.entries(plugins)) {
|
|
543
|
+
if (!content.includes(`id: '${id}'`)) {
|
|
544
|
+
throw new Error(`applyPlugins: plugin id "${id}" n\xE3o encontrado em "${registryPath}".`);
|
|
545
|
+
}
|
|
546
|
+
content = content.replace(
|
|
547
|
+
new RegExp(`(id: '${id}'[\\s\\S]*?defaultEnabled: )(?:true|false)`, ""),
|
|
548
|
+
`$1${enabled}`
|
|
549
|
+
);
|
|
550
|
+
}
|
|
551
|
+
await fsModule.writeFile(registryPath, content);
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// src/transforms/applyIndexHtml.ts
|
|
555
|
+
import * as nodeFsPromises5 from "fs/promises";
|
|
556
|
+
function fontToGoogleSlug2(font) {
|
|
557
|
+
return font.replace(/ /g, "+");
|
|
558
|
+
}
|
|
559
|
+
async function applyIndexHtml(data, htmlPath, fsModule = nodeFsPromises5) {
|
|
560
|
+
let html = await fsModule.readFile(htmlPath, "utf-8");
|
|
561
|
+
const slug = fontToGoogleSlug2(data.fontFamily);
|
|
562
|
+
html = html.replace(
|
|
563
|
+
/<link\s[^>]*fonts\.googleapis\.com\/css2[^>]*(?:\s*\/>|>[^<]*<\/link>)/s,
|
|
564
|
+
`<link
|
|
565
|
+
href="https://fonts.googleapis.com/css2?family=${slug}:wght@300;400;500;600;700&display=swap"
|
|
566
|
+
rel="stylesheet"
|
|
567
|
+
/>`
|
|
568
|
+
);
|
|
569
|
+
html = html.replace(
|
|
570
|
+
/(<meta\s+name="theme-color"\s+content=")[^"]*(")/,
|
|
571
|
+
`$1${data.themeColor}$2`
|
|
572
|
+
);
|
|
573
|
+
html = html.replace(
|
|
574
|
+
/(<meta\s+property="og:title"\s+content=")[^"]*(")/,
|
|
575
|
+
`$1${data.siteName}$2`
|
|
576
|
+
);
|
|
577
|
+
html = html.replace(
|
|
578
|
+
/(<meta\s+property="twitter:title"\s+content=")[^"]*(")/,
|
|
579
|
+
`$1${data.siteName}$2`
|
|
580
|
+
);
|
|
581
|
+
html = html.replace(
|
|
582
|
+
/(<meta\s+property="og:url"\s+content=")[^"]*(")/,
|
|
583
|
+
`$1${data.siteUrl}$2`
|
|
584
|
+
);
|
|
585
|
+
html = html.replace(
|
|
586
|
+
/(<meta\s+property="twitter:url"\s+content=")[^"]*(")/,
|
|
587
|
+
`$1${data.siteUrl}$2`
|
|
588
|
+
);
|
|
589
|
+
const handle = data.twitterHandle.startsWith("@") ? data.twitterHandle : `@${data.twitterHandle}`;
|
|
590
|
+
html = html.replace(
|
|
591
|
+
/(<meta\s+property="twitter:creator"\s+content=")[^"]*(")/,
|
|
592
|
+
`$1${handle}$2`
|
|
593
|
+
);
|
|
594
|
+
html = html.replace(
|
|
595
|
+
/(<meta\s+name="author"\s+content=")[^"]*(")/,
|
|
596
|
+
`$1${data.authorName}$2`
|
|
597
|
+
);
|
|
598
|
+
await fsModule.writeFile(htmlPath, html);
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
// src/transforms/applyRubricalConfig.ts
|
|
602
|
+
import * as nodeFsPromises6 from "fs/promises";
|
|
603
|
+
import * as path2 from "path";
|
|
604
|
+
function generateContent(config) {
|
|
605
|
+
return `export const rubricalConfig = {
|
|
606
|
+
siteName: ${JSON.stringify(config.siteName)},
|
|
607
|
+
siteUrl: ${JSON.stringify(config.siteUrl)},
|
|
608
|
+
siteDescription: ${JSON.stringify(config.siteDescription)},
|
|
609
|
+
authorName: ${JSON.stringify(config.authorName)},
|
|
610
|
+
authorEmail: ${JSON.stringify(config.authorEmail)},
|
|
611
|
+
twitterHandle: ${JSON.stringify(config.twitterHandle)},
|
|
612
|
+
lang: ${JSON.stringify(config.lang)},
|
|
613
|
+
seoHomeTitle: ${JSON.stringify(config.seoHomeTitle)},
|
|
614
|
+
seoHomeDescription: ${JSON.stringify(config.seoHomeDescription)},
|
|
615
|
+
rssTitle: ${JSON.stringify(config.rssTitle)},
|
|
616
|
+
rssDescription: ${JSON.stringify(config.rssDescription)},
|
|
617
|
+
ogImageUrl: ${JSON.stringify(config.ogImageUrl)},
|
|
618
|
+
accentColor: ${JSON.stringify(config.accentColor)},
|
|
619
|
+
fontSans: ${JSON.stringify(config.fontSans)},
|
|
620
|
+
fontMono: ${JSON.stringify(config.fontMono)},
|
|
621
|
+
radius: ${JSON.stringify(config.radius)},
|
|
622
|
+
};
|
|
623
|
+
`;
|
|
624
|
+
}
|
|
625
|
+
async function applyRubricalConfig(config, targetDir, fsModule = nodeFsPromises6) {
|
|
626
|
+
const filePath = path2.join(targetDir, "rubrica.config.ts");
|
|
627
|
+
await fsModule.mkdir(targetDir, { recursive: true });
|
|
628
|
+
await fsModule.writeFile(filePath, generateContent(config));
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
// src/state/writeState.ts
|
|
632
|
+
import * as nodeFsPromises8 from "fs/promises";
|
|
633
|
+
import * as path4 from "path";
|
|
634
|
+
|
|
635
|
+
// src/state/readState.ts
|
|
636
|
+
import * as nodeFsPromises7 from "fs/promises";
|
|
637
|
+
import * as path3 from "path";
|
|
638
|
+
var DEFAULT_STATE = {
|
|
639
|
+
version: "0.0.0",
|
|
640
|
+
layout: "sidebar",
|
|
641
|
+
theme: "cyberpunk",
|
|
642
|
+
accentColor: null,
|
|
643
|
+
fontSans: "Inter",
|
|
644
|
+
fontMono: "JetBrains Mono",
|
|
645
|
+
radius: "0.5rem",
|
|
646
|
+
plugins: {}
|
|
647
|
+
};
|
|
648
|
+
async function readState(projectDir, fsModule = nodeFsPromises7) {
|
|
649
|
+
const filePath = path3.join(projectDir, "rubrica.json");
|
|
650
|
+
let raw;
|
|
651
|
+
try {
|
|
652
|
+
raw = await fsModule.readFile(filePath, "utf-8");
|
|
653
|
+
} catch {
|
|
654
|
+
const state = { ...DEFAULT_STATE };
|
|
655
|
+
await fsModule.mkdir(projectDir, { recursive: true });
|
|
656
|
+
await fsModule.writeFile(filePath, JSON.stringify(state, null, 2));
|
|
657
|
+
return state;
|
|
658
|
+
}
|
|
659
|
+
const parsed = JSON.parse(raw);
|
|
660
|
+
if (!("version" in parsed) || parsed.version === void 0) {
|
|
661
|
+
throw new Error(
|
|
662
|
+
`readState: campo obrigat\xF3rio "version" ausente em "${filePath}".`
|
|
663
|
+
);
|
|
664
|
+
}
|
|
665
|
+
if (!("layout" in parsed) || parsed.layout === void 0) {
|
|
666
|
+
throw new Error(
|
|
667
|
+
`readState: campo obrigat\xF3rio "layout" ausente em "${filePath}".`
|
|
668
|
+
);
|
|
669
|
+
}
|
|
670
|
+
return parsed;
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
// src/state/writeState.ts
|
|
674
|
+
async function writeState(projectDir, partial, fsModule = nodeFsPromises8) {
|
|
675
|
+
const filePath = path4.join(projectDir, "rubrica.json");
|
|
676
|
+
let current;
|
|
677
|
+
try {
|
|
678
|
+
current = await readState(projectDir, fsModule);
|
|
679
|
+
} catch {
|
|
680
|
+
current = {};
|
|
681
|
+
}
|
|
682
|
+
const merged = { ...current, ...partial };
|
|
683
|
+
await fsModule.mkdir(projectDir, { recursive: true });
|
|
684
|
+
await fsModule.writeFile(filePath, JSON.stringify(merged, null, 2));
|
|
685
|
+
return merged;
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
// src/commands/create.ts
|
|
689
|
+
var ALL_PLUGINS = [
|
|
690
|
+
"contact-wizard",
|
|
691
|
+
"proposals",
|
|
692
|
+
"payments",
|
|
693
|
+
"blog",
|
|
694
|
+
"portfolio",
|
|
695
|
+
"resume",
|
|
696
|
+
"about",
|
|
697
|
+
"ai-resumes",
|
|
698
|
+
"audit-log",
|
|
699
|
+
"media-manager",
|
|
700
|
+
"i18n",
|
|
701
|
+
"playground",
|
|
702
|
+
"testimonials",
|
|
703
|
+
"testimonials-intake"
|
|
704
|
+
];
|
|
705
|
+
function validateProjectName(name) {
|
|
706
|
+
if (/\s/.test(name)) {
|
|
707
|
+
throw new Error(
|
|
708
|
+
`Nome inv\xE1lido: "${name}" cont\xE9m espa\xE7os. Use h\xEDfens ou underscores (ex: meu-portfolio).`
|
|
709
|
+
);
|
|
710
|
+
}
|
|
711
|
+
if (/[^a-zA-Z0-9\-_]/.test(name)) {
|
|
712
|
+
throw new Error(
|
|
713
|
+
`Nome inv\xE1lido: "${name}" cont\xE9m caracteres especiais. Use apenas letras, n\xFAmeros, h\xEDfens e underscores.`
|
|
714
|
+
);
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
async function removeDir(dirPath, fs) {
|
|
718
|
+
if (await fs.exists(dirPath)) {
|
|
719
|
+
await fs.rm(dirPath, { recursive: true, force: true });
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
async function runCreate(projectName, deps) {
|
|
723
|
+
validateProjectName(projectName);
|
|
724
|
+
const fs = deps.fs ?? {
|
|
725
|
+
...nodeFsPromises9,
|
|
726
|
+
exists: async (p) => {
|
|
727
|
+
try {
|
|
728
|
+
await nodeFsPromises9.access(p);
|
|
729
|
+
return true;
|
|
730
|
+
} catch {
|
|
731
|
+
return false;
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
};
|
|
735
|
+
const download = deps.download ?? downloadRelease;
|
|
736
|
+
const getVersion = deps.getLatestVersion ?? getLatestVersion;
|
|
737
|
+
const applyLayoutFn = deps.applyLayout ?? applyLayout;
|
|
738
|
+
const applyThemeFn = deps.applyTheme ?? applyTheme;
|
|
739
|
+
const applyFontFn = deps.applyFont ?? applyFont;
|
|
740
|
+
const applyPluginsFn = deps.applyPlugins ?? applyPlugins;
|
|
741
|
+
const applyIndexHtmlFn = deps.applyIndexHtml ?? applyIndexHtml;
|
|
742
|
+
const applyRubricalConfigFn = deps.applyRubricalConfig ?? applyRubricalConfig;
|
|
743
|
+
const writeStateFn = deps.writeState ?? writeState;
|
|
744
|
+
const execFn = deps.exec ?? (async (cmd, cwd) => {
|
|
745
|
+
const { execSync } = await import("child_process");
|
|
746
|
+
execSync(cmd, { cwd, stdio: "inherit" });
|
|
747
|
+
});
|
|
748
|
+
const doIdentityPrompt = deps.identityPrompt ?? identityPrompt;
|
|
749
|
+
const projectDir = `${deps.projectsDir}/${projectName}`;
|
|
750
|
+
const version = await getVersion();
|
|
751
|
+
await download(projectDir, version);
|
|
752
|
+
const layout = await select2({
|
|
753
|
+
message: "Layout",
|
|
754
|
+
options: [
|
|
755
|
+
{ value: "sidebar", label: "Sidebar \u2014 navega\xE7\xE3o lateral com perfil completo" },
|
|
756
|
+
{ value: "topbar", label: "Topbar \u2014 navbar horizontal fixa no topo" },
|
|
757
|
+
{ value: "centered", label: "Centered \u2014 sem nav persistente, foco no conte\xFAdo" }
|
|
758
|
+
]
|
|
759
|
+
});
|
|
760
|
+
await applyLayoutFn(
|
|
761
|
+
layout,
|
|
762
|
+
{ projectDir, templatesDir: `${projectDir}/templates` },
|
|
763
|
+
fs
|
|
764
|
+
);
|
|
765
|
+
const themeChoice = await select2({
|
|
766
|
+
message: "Tema visual",
|
|
767
|
+
options: [
|
|
768
|
+
{ value: "cyberpunk", label: "Cyberpunk \u2014 neon purple + lime, atmosfera dark" },
|
|
769
|
+
{ value: "minimal", label: "Minimal \u2014 clean, azul neutro, m\xE1xima legibilidade" },
|
|
770
|
+
{ value: "editorial", label: "Editorial \u2014 creme, \xE2mbar, tipografia serifada" },
|
|
771
|
+
{ value: "forest", label: "Forest \u2014 verde musgo, off-white, org\xE2nico" },
|
|
772
|
+
{ value: "custom", label: "Personalizado..." }
|
|
773
|
+
]
|
|
774
|
+
});
|
|
775
|
+
let accentColor;
|
|
776
|
+
if (themeChoice === "custom") {
|
|
777
|
+
const hexInput = await text2({
|
|
778
|
+
message: "Cor de destaque (hex, ex: #0065fe)",
|
|
779
|
+
validate: (v) => {
|
|
780
|
+
if (!/^#[0-9a-fA-F]{6}$/.test(v)) return "Informe um hex v\xE1lido (ex: #0065fe)";
|
|
781
|
+
}
|
|
782
|
+
});
|
|
783
|
+
accentColor = hexInput;
|
|
784
|
+
}
|
|
785
|
+
const themeOptions = themeChoice === "custom" ? { accentColor } : { preset: themeChoice };
|
|
786
|
+
await applyThemeFn(
|
|
787
|
+
themeOptions,
|
|
788
|
+
`${projectDir}/src/index.css`,
|
|
789
|
+
fs
|
|
790
|
+
);
|
|
791
|
+
const fontSans = await select2({
|
|
792
|
+
message: "Fonte principal",
|
|
793
|
+
options: [
|
|
794
|
+
{ value: "Inter", label: "Inter \u2014 neutra, leg\xEDvel, padr\xE3o de produtos digitais" },
|
|
795
|
+
{ value: "Chakra Petch", label: "Chakra Petch \u2014 geom\xE9trica, tech, futurista" },
|
|
796
|
+
{ value: "Playfair Display", label: "Playfair Display \u2014 elegante, editorial" },
|
|
797
|
+
{ value: "Space Grotesk", label: "Space Grotesk \u2014 moderna, startup" },
|
|
798
|
+
{ value: "DM Sans", label: "DM Sans \u2014 limpa, amig\xE1vel, vers\xE1til" }
|
|
799
|
+
]
|
|
800
|
+
});
|
|
801
|
+
const fontMono = await select2({
|
|
802
|
+
message: "Fonte mono",
|
|
803
|
+
options: [
|
|
804
|
+
{ value: "JetBrains Mono", label: "JetBrains Mono \u2014 dev-friendly, clara" },
|
|
805
|
+
{ value: "Fira Code", label: "Fira Code \u2014 cl\xE1ssico, com ligatures" },
|
|
806
|
+
{ value: "Space Mono", label: "Space Mono \u2014 retro digital" },
|
|
807
|
+
{ value: "IBM Plex Mono", label: "IBM Plex Mono \u2014 corporativo-tech" }
|
|
808
|
+
]
|
|
809
|
+
});
|
|
810
|
+
const radius = await select2({
|
|
811
|
+
message: "Border radius",
|
|
812
|
+
options: [
|
|
813
|
+
{ value: "0rem", label: "Nenhum \u2014 bordas retas, minimalismo severo" },
|
|
814
|
+
{ value: "0.375rem", label: "Suave \u2014 sutil arredondamento" },
|
|
815
|
+
{ value: "0.5rem", label: "M\xE9dio \u2014 padr\xE3o shadcn/ui" },
|
|
816
|
+
{ value: "0.75rem", label: "Arredondado \u2014 amig\xE1vel, moderno" },
|
|
817
|
+
{ value: "1rem", label: "Pill \u2014 muito arredondado, jovial" }
|
|
818
|
+
]
|
|
819
|
+
});
|
|
820
|
+
await applyFontFn(
|
|
821
|
+
{ fontSans, fontMono, radius },
|
|
822
|
+
{ css: `${projectDir}/src/index.css`, html: `${projectDir}/index.html` },
|
|
823
|
+
fs
|
|
824
|
+
);
|
|
825
|
+
const selectedPlugins = await multiselect({
|
|
826
|
+
message: "Plugins ativos",
|
|
827
|
+
options: [
|
|
828
|
+
{ value: "contact-wizard", label: "Contact Wizard \u2014 formul\xE1rio de contato multi-etapas", hint: "recomendado" },
|
|
829
|
+
{ value: "proposals", label: "Propostas \u2014 propostas comerciais com assinatura eletr\xF4nica", hint: "recomendado" },
|
|
830
|
+
{ value: "payments", label: "Pagamentos \u2014 integra\xE7\xE3o com Stripe/Asaas" },
|
|
831
|
+
{ value: "blog", label: "Blog \u2014 listagem e leitura de posts", hint: "recomendado" },
|
|
832
|
+
{ value: "portfolio", label: "Portfolio \u2014 galeria de projetos", hint: "recomendado" },
|
|
833
|
+
{ value: "resume", label: "Curr\xEDculo \u2014 curr\xEDculo interativo com download de PDF", hint: "recomendado" },
|
|
834
|
+
{ value: "about", label: "Sobre \u2014 p\xE1gina sobre mim", hint: "recomendado" },
|
|
835
|
+
{ value: "ai-resumes", label: "CV com IA \u2014 gera\xE7\xE3o de curr\xEDculo customizado por IA" },
|
|
836
|
+
{ value: "audit-log", label: "Audit Log \u2014 log de auditoria do painel admin", hint: "recomendado" },
|
|
837
|
+
{ value: "media-manager", label: "Media Manager \u2014 gerenciador de arquivos e imagens" },
|
|
838
|
+
{ value: "i18n", label: "Tradu\xE7\xE3o IA \u2014 internacionaliza\xE7\xE3o autom\xE1tica de conte\xFAdo" },
|
|
839
|
+
{ value: "playground", label: "Playground \u2014 ambiente de testes interativo" },
|
|
840
|
+
{ value: "testimonials", label: "Depoimentos \u2014 se\xE7\xE3o de testimoniais de clientes", hint: "recomendado" },
|
|
841
|
+
{ value: "testimonials-intake", label: "Coleta de Depoimentos \u2014 formul\xE1rio p\xFAblico para coleta", hint: "recomendado" }
|
|
842
|
+
],
|
|
843
|
+
required: false
|
|
844
|
+
});
|
|
845
|
+
const pluginsRecord = Object.fromEntries(
|
|
846
|
+
ALL_PLUGINS.map((id) => [id, selectedPlugins.includes(id)])
|
|
847
|
+
);
|
|
848
|
+
await applyPluginsFn(
|
|
849
|
+
pluginsRecord,
|
|
850
|
+
`${projectDir}/convex/pluginRegistry.ts`,
|
|
851
|
+
fs
|
|
852
|
+
);
|
|
853
|
+
const identity = await doIdentityPrompt();
|
|
854
|
+
await applyIndexHtmlFn(
|
|
855
|
+
{
|
|
856
|
+
fontFamily: fontSans,
|
|
857
|
+
themeColor: accentColor ?? "#6d28d9",
|
|
858
|
+
siteName: identity.siteName,
|
|
859
|
+
siteUrl: identity.siteUrl,
|
|
860
|
+
twitterHandle: identity.twitterHandle,
|
|
861
|
+
authorName: identity.authorName
|
|
862
|
+
},
|
|
863
|
+
`${projectDir}/index.html`,
|
|
864
|
+
fs
|
|
865
|
+
);
|
|
866
|
+
await applyRubricalConfigFn(
|
|
867
|
+
{
|
|
868
|
+
siteName: identity.siteName,
|
|
869
|
+
siteUrl: identity.siteUrl,
|
|
870
|
+
siteDescription: identity.siteDescription,
|
|
871
|
+
authorName: identity.authorName,
|
|
872
|
+
authorEmail: identity.authorEmail,
|
|
873
|
+
twitterHandle: identity.twitterHandle,
|
|
874
|
+
lang: identity.lang,
|
|
875
|
+
seoHomeTitle: identity.siteName,
|
|
876
|
+
seoHomeDescription: identity.siteDescription,
|
|
877
|
+
rssTitle: identity.siteName,
|
|
878
|
+
rssDescription: identity.siteDescription,
|
|
879
|
+
ogImageUrl: `${identity.siteUrl}/og.png`,
|
|
880
|
+
accentColor: accentColor ?? "#6d28d9",
|
|
881
|
+
fontSans,
|
|
882
|
+
fontMono
|
|
883
|
+
},
|
|
884
|
+
projectDir,
|
|
885
|
+
fs
|
|
886
|
+
);
|
|
887
|
+
const pkgPath = `${projectDir}/package.json`;
|
|
888
|
+
const pkgRaw = await fs.readFile(pkgPath, "utf-8");
|
|
889
|
+
const pkg = JSON.parse(pkgRaw);
|
|
890
|
+
pkg.name = projectName;
|
|
891
|
+
await fs.writeFile(pkgPath, JSON.stringify(pkg, null, 2));
|
|
892
|
+
await writeStateFn(
|
|
893
|
+
projectDir,
|
|
894
|
+
{
|
|
895
|
+
version: "0.1.0",
|
|
896
|
+
layout,
|
|
897
|
+
theme: themeChoice === "custom" ? "custom" : themeChoice,
|
|
898
|
+
accentColor: accentColor ?? null,
|
|
899
|
+
fontSans,
|
|
900
|
+
fontMono,
|
|
901
|
+
radius,
|
|
902
|
+
plugins: pluginsRecord
|
|
903
|
+
},
|
|
904
|
+
fs
|
|
905
|
+
);
|
|
906
|
+
const doGitInit = await confirm({ message: "Inicializar reposit\xF3rio git?" });
|
|
907
|
+
const packageManager = await select2({
|
|
908
|
+
message: "Gerenciador de pacotes",
|
|
909
|
+
options: [
|
|
910
|
+
{ value: "pnpm", label: "pnpm (recomendado)" },
|
|
911
|
+
{ value: "npm", label: "npm" },
|
|
912
|
+
{ value: "none", label: "N\xE3o agora" }
|
|
913
|
+
]
|
|
914
|
+
});
|
|
915
|
+
if (doGitInit) {
|
|
916
|
+
await execFn("git init", projectDir);
|
|
917
|
+
}
|
|
918
|
+
if (packageManager !== "none") {
|
|
919
|
+
await execFn(`${packageManager} install`, projectDir);
|
|
920
|
+
}
|
|
921
|
+
await removeDir(`${projectDir}/templates`, fs);
|
|
922
|
+
await removeDir(`${projectDir}/cli`, fs);
|
|
923
|
+
const installStep = packageManager === "none" ? "pnpm install\n " : "";
|
|
924
|
+
outro(
|
|
925
|
+
`Projeto criado com sucesso!
|
|
926
|
+
|
|
927
|
+
cd ${projectName}
|
|
928
|
+
|
|
929
|
+
Pr\xF3ximos passos:
|
|
930
|
+
|
|
931
|
+
1. Suba o backend:
|
|
932
|
+
npx convex dev
|
|
933
|
+
(deixe rodando em um terminal)
|
|
934
|
+
|
|
935
|
+
2. Configure o ambiente (em outro terminal, com Convex ativo):
|
|
936
|
+
rubrica setup
|
|
937
|
+
|
|
938
|
+
3. Inicie o frontend:
|
|
939
|
+
${installStep}pnpm dev
|
|
940
|
+
|
|
941
|
+
Boa sorte! \u{1F680}`
|
|
942
|
+
);
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
// src/commands/config.ts
|
|
946
|
+
import { outro as outro2, select as select3, multiselect as multiselect2, confirm as confirm2, isCancel as isCancel3, cancel as cancel3 } from "@clack/prompts";
|
|
947
|
+
import * as nodeFsPromises11 from "fs/promises";
|
|
948
|
+
import * as path6 from "path";
|
|
949
|
+
|
|
950
|
+
// src/utils/detectProject.ts
|
|
951
|
+
import * as nodeFsPromises10 from "fs/promises";
|
|
952
|
+
import * as path5 from "path";
|
|
953
|
+
async function detectProject(startDir, fsModule = nodeFsPromises10) {
|
|
954
|
+
let current = startDir;
|
|
955
|
+
while (true) {
|
|
956
|
+
const candidate = path5.join(current, "rubrica.json");
|
|
957
|
+
try {
|
|
958
|
+
await fsModule.access(candidate);
|
|
959
|
+
return candidate;
|
|
960
|
+
} catch {
|
|
961
|
+
const parent = path5.dirname(current);
|
|
962
|
+
if (parent === current) {
|
|
963
|
+
throw new Error(
|
|
964
|
+
`detectProject: nenhum rubrica.json encontrado em "${startDir}" nem em nenhum diret\xF3rio ancestral.`
|
|
965
|
+
);
|
|
966
|
+
}
|
|
967
|
+
current = parent;
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
// src/commands/config.ts
|
|
973
|
+
var ALL_PLUGINS2 = [
|
|
974
|
+
"blog",
|
|
975
|
+
"portfolio",
|
|
976
|
+
"resume",
|
|
977
|
+
"about",
|
|
978
|
+
"proposals",
|
|
979
|
+
"payments",
|
|
980
|
+
"ai-resumes",
|
|
981
|
+
"i18n",
|
|
982
|
+
"playground",
|
|
983
|
+
"testimonials",
|
|
984
|
+
"testimonials-intake",
|
|
985
|
+
"contact-wizard",
|
|
986
|
+
"audit-log"
|
|
987
|
+
];
|
|
988
|
+
async function runConfig(deps = {}) {
|
|
989
|
+
const cwd = deps.cwd ?? process.cwd();
|
|
990
|
+
const fs = deps.fs ?? nodeFsPromises11;
|
|
991
|
+
const detectProjectFn = deps.detectProject ?? detectProject;
|
|
992
|
+
const readStateFn = deps.readState ?? readState;
|
|
993
|
+
const writeStateFn = deps.writeState ?? writeState;
|
|
994
|
+
const doIdentityPrompt = deps.identityPrompt ?? identityPrompt;
|
|
995
|
+
const applyRubricalConfigFn = deps.applyRubricalConfig ?? applyRubricalConfig;
|
|
996
|
+
const applyThemeFn = deps.applyTheme ?? applyTheme;
|
|
997
|
+
const applyFontFn = deps.applyFont ?? applyFont;
|
|
998
|
+
const applyIndexHtmlFn = deps.applyIndexHtml ?? applyIndexHtml;
|
|
999
|
+
const applyLayoutFn = deps.applyLayout ?? applyLayout;
|
|
1000
|
+
const applyPluginsFn = deps.applyPlugins ?? applyPlugins;
|
|
1001
|
+
const rubricaJsonPath = await detectProjectFn(cwd, fs);
|
|
1002
|
+
const projectDir = path6.dirname(rubricaJsonPath);
|
|
1003
|
+
const currentState = await readStateFn(projectDir, fs);
|
|
1004
|
+
const stateUpdate = {};
|
|
1005
|
+
const sections = await multiselect2({
|
|
1006
|
+
message: "O que deseja reconfigurar?",
|
|
1007
|
+
options: [
|
|
1008
|
+
{ value: "identity", label: "Identidade (nome, URL, SEO, RSS)" },
|
|
1009
|
+
{ value: "appearance", label: "Apar\xEAncia (tema, cores, fonte, radius)" },
|
|
1010
|
+
{ value: "layout", label: "Layout (sidebar / topbar / centered)" },
|
|
1011
|
+
{ value: "plugins", label: "Plugins" }
|
|
1012
|
+
],
|
|
1013
|
+
required: false
|
|
1014
|
+
});
|
|
1015
|
+
if (isCancel3(sections)) {
|
|
1016
|
+
cancel3("Opera\xE7\xE3o cancelada.");
|
|
1017
|
+
return;
|
|
1018
|
+
}
|
|
1019
|
+
const selected = sections;
|
|
1020
|
+
if (selected.includes("identity")) {
|
|
1021
|
+
const identityData = await doIdentityPrompt();
|
|
1022
|
+
await applyRubricalConfigFn(
|
|
1023
|
+
{
|
|
1024
|
+
siteName: identityData.siteName,
|
|
1025
|
+
siteUrl: identityData.siteUrl,
|
|
1026
|
+
siteDescription: identityData.siteDescription,
|
|
1027
|
+
authorName: identityData.authorName,
|
|
1028
|
+
authorEmail: identityData.authorEmail,
|
|
1029
|
+
twitterHandle: identityData.twitterHandle,
|
|
1030
|
+
lang: identityData.lang,
|
|
1031
|
+
seoHomeTitle: "",
|
|
1032
|
+
seoHomeDescription: "",
|
|
1033
|
+
rssTitle: "",
|
|
1034
|
+
rssDescription: "",
|
|
1035
|
+
ogImageUrl: "",
|
|
1036
|
+
accentColor: currentState.accentColor ?? "",
|
|
1037
|
+
fontSans: currentState.fontSans,
|
|
1038
|
+
fontMono: currentState.fontMono,
|
|
1039
|
+
radius: currentState.radius
|
|
1040
|
+
},
|
|
1041
|
+
projectDir,
|
|
1042
|
+
fs
|
|
1043
|
+
);
|
|
1044
|
+
}
|
|
1045
|
+
if (selected.includes("appearance")) {
|
|
1046
|
+
const theme = await select3({
|
|
1047
|
+
message: "Tema visual",
|
|
1048
|
+
initialValue: currentState.theme,
|
|
1049
|
+
options: [
|
|
1050
|
+
{ value: "cyberpunk", label: "Cyberpunk \u2014 neon purple + lime" },
|
|
1051
|
+
{ value: "minimal", label: "Minimal \u2014 clean, azul neutro" },
|
|
1052
|
+
{ value: "editorial", label: "Editorial \u2014 creme, \xE2mbar, serifado" },
|
|
1053
|
+
{ value: "forest", label: "Forest \u2014 verde musgo, off-white" }
|
|
1054
|
+
]
|
|
1055
|
+
});
|
|
1056
|
+
const fontSans = await select3({
|
|
1057
|
+
message: "Fonte principal",
|
|
1058
|
+
initialValue: currentState.fontSans,
|
|
1059
|
+
options: [
|
|
1060
|
+
{ value: "Inter", label: "Inter \u2014 neutra, padr\xE3o de produtos digitais" },
|
|
1061
|
+
{ value: "Chakra Petch", label: "Chakra Petch \u2014 geom\xE9trica, tech" },
|
|
1062
|
+
{ value: "Playfair Display", label: "Playfair Display \u2014 elegante, editorial" },
|
|
1063
|
+
{ value: "Space Grotesk", label: "Space Grotesk \u2014 moderna, startup" },
|
|
1064
|
+
{ value: "DM Sans", label: "DM Sans \u2014 limpa, amig\xE1vel" }
|
|
1065
|
+
]
|
|
1066
|
+
});
|
|
1067
|
+
const fontMono = await select3({
|
|
1068
|
+
message: "Fonte mono",
|
|
1069
|
+
initialValue: currentState.fontMono,
|
|
1070
|
+
options: [
|
|
1071
|
+
{ value: "JetBrains Mono", label: "JetBrains Mono" },
|
|
1072
|
+
{ value: "Fira Code", label: "Fira Code" },
|
|
1073
|
+
{ value: "Space Mono", label: "Space Mono" },
|
|
1074
|
+
{ value: "IBM Plex Mono", label: "IBM Plex Mono" }
|
|
1075
|
+
]
|
|
1076
|
+
});
|
|
1077
|
+
const radius = await select3({
|
|
1078
|
+
message: "Border radius",
|
|
1079
|
+
initialValue: currentState.radius,
|
|
1080
|
+
options: [
|
|
1081
|
+
{ value: "0rem", label: "Nenhum \u2014 0rem" },
|
|
1082
|
+
{ value: "0.375rem", label: "Suave \u2014 0.375rem" },
|
|
1083
|
+
{ value: "0.5rem", label: "M\xE9dio \u2014 0.5rem" },
|
|
1084
|
+
{ value: "0.75rem", label: "Arredondado \u2014 0.75rem" },
|
|
1085
|
+
{ value: "1rem", label: "Pill \u2014 1rem" }
|
|
1086
|
+
]
|
|
1087
|
+
});
|
|
1088
|
+
const cssPath = path6.join(projectDir, "src", "index.css");
|
|
1089
|
+
const htmlPath = path6.join(projectDir, "index.html");
|
|
1090
|
+
await applyThemeFn(
|
|
1091
|
+
{ preset: theme },
|
|
1092
|
+
cssPath,
|
|
1093
|
+
fs
|
|
1094
|
+
);
|
|
1095
|
+
await applyFontFn(
|
|
1096
|
+
{ fontSans, fontMono, radius },
|
|
1097
|
+
{ css: cssPath, html: htmlPath },
|
|
1098
|
+
fs
|
|
1099
|
+
);
|
|
1100
|
+
await applyIndexHtmlFn(
|
|
1101
|
+
{ fontFamily: fontSans, themeColor: "", siteName: "", siteUrl: "", twitterHandle: "", authorName: "" },
|
|
1102
|
+
htmlPath,
|
|
1103
|
+
fs
|
|
1104
|
+
);
|
|
1105
|
+
stateUpdate.theme = theme;
|
|
1106
|
+
stateUpdate.fontSans = fontSans;
|
|
1107
|
+
stateUpdate.fontMono = fontMono;
|
|
1108
|
+
stateUpdate.radius = radius;
|
|
1109
|
+
stateUpdate.accentColor = null;
|
|
1110
|
+
}
|
|
1111
|
+
if (selected.includes("layout")) {
|
|
1112
|
+
const newLayout = await select3({
|
|
1113
|
+
message: "Layout",
|
|
1114
|
+
initialValue: currentState.layout,
|
|
1115
|
+
options: [
|
|
1116
|
+
{ value: "sidebar", label: "Sidebar \u2014 navega\xE7\xE3o lateral com perfil completo" },
|
|
1117
|
+
{ value: "topbar", label: "Topbar \u2014 navbar horizontal fixa no topo" },
|
|
1118
|
+
{ value: "centered", label: "Centered \u2014 sem nav persistente, foco no conte\xFAdo" }
|
|
1119
|
+
]
|
|
1120
|
+
});
|
|
1121
|
+
const confirmed = await confirm2({
|
|
1122
|
+
message: "\u26A0 Isso sobrescreve Layout.tsx e arquivos de navega\xE7\xE3o. Customiza\xE7\xF5es manuais ser\xE3o perdidas. Continuar?"
|
|
1123
|
+
});
|
|
1124
|
+
if (!isCancel3(confirmed) && confirmed === true) {
|
|
1125
|
+
await applyLayoutFn(
|
|
1126
|
+
newLayout,
|
|
1127
|
+
{ projectDir, templatesDir: path6.join(projectDir, "templates") },
|
|
1128
|
+
fs
|
|
1129
|
+
);
|
|
1130
|
+
stateUpdate.layout = newLayout;
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
if (selected.includes("plugins")) {
|
|
1134
|
+
const activePlugins = Object.entries(currentState.plugins).filter(([, v]) => v).map(([k]) => k);
|
|
1135
|
+
const selectedPlugins = await multiselect2({
|
|
1136
|
+
message: "Plugins ativos",
|
|
1137
|
+
initialValues: activePlugins,
|
|
1138
|
+
options: ALL_PLUGINS2.map((p) => ({ value: p, label: p })),
|
|
1139
|
+
required: false
|
|
1140
|
+
});
|
|
1141
|
+
if (!isCancel3(selectedPlugins)) {
|
|
1142
|
+
const pluginsMap = {};
|
|
1143
|
+
for (const p of ALL_PLUGINS2) {
|
|
1144
|
+
pluginsMap[p] = selectedPlugins.includes(p);
|
|
1145
|
+
}
|
|
1146
|
+
await applyPluginsFn(
|
|
1147
|
+
pluginsMap,
|
|
1148
|
+
path6.join(projectDir, "convex", "pluginRegistry.ts"),
|
|
1149
|
+
fs
|
|
1150
|
+
);
|
|
1151
|
+
stateUpdate.plugins = pluginsMap;
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
if (Object.keys(stateUpdate).length > 0) {
|
|
1155
|
+
await writeStateFn(projectDir, stateUpdate, fs);
|
|
1156
|
+
}
|
|
1157
|
+
outro2("Configura\xE7\xE3o atualizada com sucesso!");
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
// src/commands/update.ts
|
|
1161
|
+
import { intro as intro2, outro as outro3, confirm as confirm3, isCancel as isCancel4, cancel as cancel4, log as log2 } from "@clack/prompts";
|
|
1162
|
+
import * as nodeFsPromises12 from "fs/promises";
|
|
1163
|
+
import * as path7 from "path";
|
|
1164
|
+
|
|
1165
|
+
// src/utils/checkRequiredEnv.ts
|
|
1166
|
+
function parseEnvKeys(content) {
|
|
1167
|
+
const keys = /* @__PURE__ */ new Set();
|
|
1168
|
+
for (const line of content.split("\n")) {
|
|
1169
|
+
const trimmed = line.trim();
|
|
1170
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
1171
|
+
const eqIndex = trimmed.indexOf("=");
|
|
1172
|
+
if (eqIndex > 0) {
|
|
1173
|
+
keys.add(trimmed.slice(0, eqIndex).trim());
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
return keys;
|
|
1177
|
+
}
|
|
1178
|
+
async function readEnvKeys(filePath, fs) {
|
|
1179
|
+
try {
|
|
1180
|
+
const content = await fs.readFile(filePath, "utf-8");
|
|
1181
|
+
return parseEnvKeys(content);
|
|
1182
|
+
} catch {
|
|
1183
|
+
return /* @__PURE__ */ new Set();
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
async function checkRequiredEnv(projectDir, fs) {
|
|
1187
|
+
let required = [];
|
|
1188
|
+
try {
|
|
1189
|
+
const raw = await fs.readFile(`${projectDir}/required-env.json`, "utf-8");
|
|
1190
|
+
required = JSON.parse(raw);
|
|
1191
|
+
} catch {
|
|
1192
|
+
return [];
|
|
1193
|
+
}
|
|
1194
|
+
if (!required.length) return [];
|
|
1195
|
+
const envKeys = await readEnvKeys(`${projectDir}/.env`, fs);
|
|
1196
|
+
const envLocalKeys = await readEnvKeys(`${projectDir}/.env.local`, fs);
|
|
1197
|
+
const allKeys = /* @__PURE__ */ new Set([...envKeys, ...envLocalKeys]);
|
|
1198
|
+
return required.filter((v) => !allKeys.has(v.name));
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
// src/commands/update.ts
|
|
1202
|
+
function parseMajor(version) {
|
|
1203
|
+
const clean = version.replace(/^v/, "");
|
|
1204
|
+
const major = parseInt(clean.split(".")[0] ?? "0", 10);
|
|
1205
|
+
return isNaN(major) ? 0 : major;
|
|
1206
|
+
}
|
|
1207
|
+
function normalizeVersion(version) {
|
|
1208
|
+
return version.replace(/^v/, "");
|
|
1209
|
+
}
|
|
1210
|
+
async function runUpdate(deps = {}) {
|
|
1211
|
+
const cwd = deps.cwd ?? process.cwd();
|
|
1212
|
+
const fs = deps.fs ?? {
|
|
1213
|
+
...nodeFsPromises12,
|
|
1214
|
+
exists: async (p) => {
|
|
1215
|
+
try {
|
|
1216
|
+
await nodeFsPromises12.access(p);
|
|
1217
|
+
return true;
|
|
1218
|
+
} catch {
|
|
1219
|
+
return false;
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
};
|
|
1223
|
+
const detectProjectFn = deps.detectProject ?? detectProject;
|
|
1224
|
+
const readStateFn = deps.readState ?? readState;
|
|
1225
|
+
const writeStateFn = deps.writeState ?? writeState;
|
|
1226
|
+
const getLatestVersionFn = deps.getLatestVersion ?? getLatestVersion;
|
|
1227
|
+
const downloadReleaseFn = deps.downloadRelease ?? downloadRelease;
|
|
1228
|
+
const applyLayoutFn = deps.applyLayout ?? applyLayout;
|
|
1229
|
+
const applyThemeFn = deps.applyTheme ?? applyTheme;
|
|
1230
|
+
const applyFontFn = deps.applyFont ?? applyFont;
|
|
1231
|
+
const applyPluginsFn = deps.applyPlugins ?? applyPlugins;
|
|
1232
|
+
const applyIndexHtmlFn = deps.applyIndexHtml ?? applyIndexHtml;
|
|
1233
|
+
const applyRubricalConfigFn = deps.applyRubricalConfig ?? applyRubricalConfig;
|
|
1234
|
+
const checkRequiredEnvFn = deps.checkRequiredEnv ?? checkRequiredEnv;
|
|
1235
|
+
const execFn = deps.exec ?? (async (cmd, cwd2) => {
|
|
1236
|
+
const { execSync } = await import("child_process");
|
|
1237
|
+
execSync(cmd, { cwd: cwd2, stdio: "inherit" });
|
|
1238
|
+
});
|
|
1239
|
+
intro2("Rubrica Update");
|
|
1240
|
+
const rubricaJsonPath = await detectProjectFn(cwd, fs);
|
|
1241
|
+
const projectDir = path7.dirname(rubricaJsonPath);
|
|
1242
|
+
const state = await readStateFn(projectDir, fs);
|
|
1243
|
+
const localVersion = normalizeVersion(state.version);
|
|
1244
|
+
const remoteVersion = normalizeVersion(await getLatestVersionFn());
|
|
1245
|
+
if (localVersion === remoteVersion) {
|
|
1246
|
+
outro3(`J\xE1 est\xE1 na vers\xE3o mais recente (${localVersion}). Nenhuma atualiza\xE7\xE3o necess\xE1ria.`);
|
|
1247
|
+
return;
|
|
1248
|
+
}
|
|
1249
|
+
const localMajor = parseMajor(localVersion);
|
|
1250
|
+
const remoteMajor = parseMajor(remoteVersion);
|
|
1251
|
+
if (remoteMajor > localMajor) {
|
|
1252
|
+
log2.warn(
|
|
1253
|
+
`Atualiza\xE7\xE3o major detectada: ${localVersion} \u2192 ${remoteVersion}
|
|
1254
|
+
Atualiza\xE7\xF5es major podem conter mudan\xE7as destrutivas no schema do Convex.
|
|
1255
|
+
Consulte o guia de migra\xE7\xE3o antes de prosseguir.`
|
|
1256
|
+
);
|
|
1257
|
+
const majorConfirmed = await confirm3({
|
|
1258
|
+
message: `Confirmar atualiza\xE7\xE3o major (${localVersion} \u2192 ${remoteVersion})? Pode haver mudan\xE7as destrutivas.`
|
|
1259
|
+
});
|
|
1260
|
+
if (isCancel4(majorConfirmed) || !majorConfirmed) {
|
|
1261
|
+
cancel4("Atualiza\xE7\xE3o cancelada.");
|
|
1262
|
+
return;
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
if (remoteMajor === localMajor) {
|
|
1266
|
+
const proceed = await confirm3({
|
|
1267
|
+
message: `Atualizar de ${localVersion} para ${remoteVersion}?`
|
|
1268
|
+
});
|
|
1269
|
+
if (isCancel4(proceed) || !proceed) {
|
|
1270
|
+
cancel4("Atualiza\xE7\xE3o cancelada.");
|
|
1271
|
+
return;
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
await downloadReleaseFn(projectDir, remoteVersion);
|
|
1275
|
+
const templatesDir = path7.join(projectDir, "templates");
|
|
1276
|
+
await applyLayoutFn(
|
|
1277
|
+
state.layout,
|
|
1278
|
+
{ projectDir, templatesDir },
|
|
1279
|
+
fs
|
|
1280
|
+
);
|
|
1281
|
+
const cssPath = path7.join(projectDir, "src", "index.css");
|
|
1282
|
+
const themeOpts = state.theme && state.theme !== "custom" ? { preset: state.theme } : { accentColor: state.accentColor ?? "#6366f1" };
|
|
1283
|
+
await applyThemeFn(themeOpts, cssPath, fs);
|
|
1284
|
+
const htmlPath = path7.join(projectDir, "index.html");
|
|
1285
|
+
await applyFontFn(
|
|
1286
|
+
{ fontSans: state.fontSans, fontMono: state.fontMono, radius: state.radius },
|
|
1287
|
+
{ css: cssPath, html: htmlPath },
|
|
1288
|
+
fs
|
|
1289
|
+
);
|
|
1290
|
+
const registryPath = path7.join(projectDir, "convex", "pluginRegistry.ts");
|
|
1291
|
+
await applyPluginsFn(
|
|
1292
|
+
state.plugins,
|
|
1293
|
+
registryPath,
|
|
1294
|
+
fs
|
|
1295
|
+
);
|
|
1296
|
+
const identity = {
|
|
1297
|
+
siteName: "",
|
|
1298
|
+
siteUrl: "",
|
|
1299
|
+
siteDescription: "",
|
|
1300
|
+
authorName: "",
|
|
1301
|
+
authorEmail: "",
|
|
1302
|
+
twitterHandle: "",
|
|
1303
|
+
lang: "pt-BR",
|
|
1304
|
+
seoHomeTitle: "",
|
|
1305
|
+
seoHomeDescription: "",
|
|
1306
|
+
rssTitle: "",
|
|
1307
|
+
rssDescription: "",
|
|
1308
|
+
ogImageUrl: "",
|
|
1309
|
+
accentColor: state.accentColor ?? "#6366f1",
|
|
1310
|
+
fontSans: state.fontSans,
|
|
1311
|
+
fontMono: state.fontMono,
|
|
1312
|
+
radius: state.radius
|
|
1313
|
+
};
|
|
1314
|
+
await applyIndexHtmlFn(
|
|
1315
|
+
{
|
|
1316
|
+
fontFamily: identity.fontSans,
|
|
1317
|
+
themeColor: identity.accentColor,
|
|
1318
|
+
siteName: identity.siteName,
|
|
1319
|
+
siteUrl: identity.siteUrl,
|
|
1320
|
+
twitterHandle: identity.twitterHandle,
|
|
1321
|
+
authorName: identity.authorName
|
|
1322
|
+
},
|
|
1323
|
+
htmlPath,
|
|
1324
|
+
fs
|
|
1325
|
+
);
|
|
1326
|
+
await applyRubricalConfigFn(
|
|
1327
|
+
identity,
|
|
1328
|
+
projectDir,
|
|
1329
|
+
fs
|
|
1330
|
+
);
|
|
1331
|
+
await writeStateFn(projectDir, { version: remoteVersion }, fs);
|
|
1332
|
+
const missingVars = await checkRequiredEnvFn(projectDir, fs);
|
|
1333
|
+
if (missingVars.length > 0) {
|
|
1334
|
+
const varList = missingVars.map((v) => ` \u2022 ${v.name} \u2014 ${v.description}`).join("\n");
|
|
1335
|
+
log2.warn(
|
|
1336
|
+
`Vari\xE1veis de ambiente faltantes no Convex Dashboard:
|
|
1337
|
+
${varList}
|
|
1338
|
+
Configure-as em: https://dashboard.convex.dev`
|
|
1339
|
+
);
|
|
1340
|
+
}
|
|
1341
|
+
outro3(`Rubrica atualizado para ${remoteVersion} com sucesso!`);
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
// src/commands/setup.ts
|
|
1345
|
+
import { outro as outro4, text as text3, password, spinner, isCancel as isCancel5, cancel as cancel5 } from "@clack/prompts";
|
|
1346
|
+
import * as nodeFsPromises13 from "fs/promises";
|
|
1347
|
+
import * as path8 from "path";
|
|
1348
|
+
import * as nodeCrypto from "crypto";
|
|
1349
|
+
import { execFileSync as defaultExecFileSync } from "child_process";
|
|
1350
|
+
|
|
1351
|
+
// src/utils/generateJwtKeys.ts
|
|
1352
|
+
import { generateKeyPair, exportPKCS8, exportJWK } from "jose";
|
|
1353
|
+
async function generateJwtKeys() {
|
|
1354
|
+
const { privateKey, publicKey } = await generateKeyPair("RS256", { extractable: true });
|
|
1355
|
+
const JWT_PRIVATE_KEY = await exportPKCS8(privateKey);
|
|
1356
|
+
const jwk = await exportJWK(publicKey);
|
|
1357
|
+
const JWKS = JSON.stringify({ keys: [{ use: "sig", ...jwk }] });
|
|
1358
|
+
return { JWT_PRIVATE_KEY, JWKS };
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
// src/commands/setup.ts
|
|
1362
|
+
function parseEnvFile(content) {
|
|
1363
|
+
const result = {};
|
|
1364
|
+
for (const line of content.split("\n")) {
|
|
1365
|
+
const trimmed = line.trim();
|
|
1366
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
1367
|
+
const eqIdx = trimmed.indexOf("=");
|
|
1368
|
+
if (eqIdx === -1) continue;
|
|
1369
|
+
const key = trimmed.slice(0, eqIdx).trim();
|
|
1370
|
+
const value = trimmed.slice(eqIdx + 1).trim();
|
|
1371
|
+
result[key] = value;
|
|
1372
|
+
}
|
|
1373
|
+
return result;
|
|
1374
|
+
}
|
|
1375
|
+
async function runSetup(deps = {}) {
|
|
1376
|
+
const cwd = deps.cwd ?? process.cwd();
|
|
1377
|
+
const fs = deps.fs ?? {
|
|
1378
|
+
access: (p) => nodeFsPromises13.access(p).then(() => void 0),
|
|
1379
|
+
readFile: (p, enc) => nodeFsPromises13.readFile(p, enc),
|
|
1380
|
+
writeFile: (p, data) => nodeFsPromises13.writeFile(p, data).then(() => void 0),
|
|
1381
|
+
mkdir: (p, opts) => nodeFsPromises13.mkdir(p, opts).then(() => void 0)
|
|
1382
|
+
};
|
|
1383
|
+
const detectProject2 = deps.detectProject ?? detectProject;
|
|
1384
|
+
const generateJwtKeys2 = deps.generateJwtKeys ?? generateJwtKeys;
|
|
1385
|
+
const readState2 = deps.readState ?? readState;
|
|
1386
|
+
const randomBytes2 = deps.randomBytes ?? ((size) => nodeCrypto.randomBytes(size));
|
|
1387
|
+
const execFileSync = deps.execFileSync ?? ((file, args, opts) => defaultExecFileSync(file, args, opts));
|
|
1388
|
+
try {
|
|
1389
|
+
await detectProject2(cwd);
|
|
1390
|
+
} catch (err) {
|
|
1391
|
+
cancel5(err.message);
|
|
1392
|
+
return;
|
|
1393
|
+
}
|
|
1394
|
+
const envPath = path8.join(cwd, ".env.local");
|
|
1395
|
+
let envVars = {};
|
|
1396
|
+
try {
|
|
1397
|
+
const envContent = await fs.readFile(envPath, "utf-8");
|
|
1398
|
+
envVars = parseEnvFile(envContent);
|
|
1399
|
+
} catch {
|
|
1400
|
+
cancel5(
|
|
1401
|
+
"Arquivo .env.local n\xE3o encontrado. Rode npx convex dev primeiro para cri\xE1-lo."
|
|
1402
|
+
);
|
|
1403
|
+
return;
|
|
1404
|
+
}
|
|
1405
|
+
if (!envVars["VITE_CONVEX_URL"]) {
|
|
1406
|
+
cancel5(
|
|
1407
|
+
"VITE_CONVEX_URL n\xE3o encontrado no .env.local. Rode npx convex dev primeiro."
|
|
1408
|
+
);
|
|
1409
|
+
return;
|
|
1410
|
+
}
|
|
1411
|
+
if (!envVars["VITE_CONVEX_SITE_URL"]) {
|
|
1412
|
+
cancel5(
|
|
1413
|
+
"VITE_CONVEX_SITE_URL n\xE3o encontrado no .env.local. Rode npx convex dev primeiro."
|
|
1414
|
+
);
|
|
1415
|
+
return;
|
|
1416
|
+
}
|
|
1417
|
+
const siteUrlDefault = envVars["VITE_CONVEX_SITE_URL"];
|
|
1418
|
+
const s = spinner();
|
|
1419
|
+
s.start("Gerando chaves JWT (RS256)...");
|
|
1420
|
+
const { JWT_PRIVATE_KEY, JWKS } = await generateJwtKeys2();
|
|
1421
|
+
s.stop("Chaves JWT geradas.");
|
|
1422
|
+
execFileSync("npx", ["convex", "env", "set", "--", "JWT_PRIVATE_KEY", JWT_PRIVATE_KEY], {
|
|
1423
|
+
stdio: "pipe"
|
|
1424
|
+
});
|
|
1425
|
+
execFileSync("npx", ["convex", "env", "set", "--", "JWKS", JWKS], {
|
|
1426
|
+
stdio: "pipe"
|
|
1427
|
+
});
|
|
1428
|
+
const siteUrl = await text3({
|
|
1429
|
+
message: "URL p\xFAblica do site (SITE_URL no Convex):",
|
|
1430
|
+
initialValue: siteUrlDefault,
|
|
1431
|
+
validate: (v) => {
|
|
1432
|
+
if (!v || !v.startsWith("http://") && !v.startsWith("https://"))
|
|
1433
|
+
return "URL deve come\xE7ar com http:// ou https://";
|
|
1434
|
+
}
|
|
1435
|
+
});
|
|
1436
|
+
if (isCancel5(siteUrl)) {
|
|
1437
|
+
cancel5("Setup cancelado.");
|
|
1438
|
+
return;
|
|
1439
|
+
}
|
|
1440
|
+
execFileSync("npx", ["convex", "env", "set", "--", "SITE_URL", siteUrl], {
|
|
1441
|
+
stdio: "pipe"
|
|
1442
|
+
});
|
|
1443
|
+
const state = await readState2(cwd, fs);
|
|
1444
|
+
const plugins = state.plugins;
|
|
1445
|
+
if (plugins["contact-wizard"] || plugins["testimonials-intake"]) {
|
|
1446
|
+
const telegramToken = await text3({
|
|
1447
|
+
message: "TELEGRAM_BOT_TOKEN (Enter para pular):",
|
|
1448
|
+
initialValue: ""
|
|
1449
|
+
});
|
|
1450
|
+
if (!isCancel5(telegramToken) && telegramToken.trim()) {
|
|
1451
|
+
execFileSync("npx", ["convex", "env", "set", "--", "TELEGRAM_BOT_TOKEN", telegramToken], { stdio: "pipe" });
|
|
1452
|
+
}
|
|
1453
|
+
const telegramChatId = await text3({
|
|
1454
|
+
message: "TELEGRAM_ADMIN_CHAT_ID (Enter para pular):",
|
|
1455
|
+
initialValue: ""
|
|
1456
|
+
});
|
|
1457
|
+
if (!isCancel5(telegramChatId) && telegramChatId.trim()) {
|
|
1458
|
+
execFileSync("npx", ["convex", "env", "set", "--", "TELEGRAM_ADMIN_CHAT_ID", telegramChatId], { stdio: "pipe" });
|
|
1459
|
+
}
|
|
1460
|
+
}
|
|
1461
|
+
if (plugins["playground"]) {
|
|
1462
|
+
const pepper = randomBytes2(32).toString("hex");
|
|
1463
|
+
execFileSync("npx", ["convex", "env", "set", "--", "PLAYGROUND_KEY_PEPPER", pepper], { stdio: "pipe" });
|
|
1464
|
+
}
|
|
1465
|
+
if (plugins["ai-resumes"] || plugins["i18n"] || plugins["playground"]) {
|
|
1466
|
+
const openrouterKey = await text3({
|
|
1467
|
+
message: "OPENROUTER_API_KEY (Enter para pular):",
|
|
1468
|
+
initialValue: ""
|
|
1469
|
+
});
|
|
1470
|
+
if (!isCancel5(openrouterKey) && openrouterKey.trim()) {
|
|
1471
|
+
execFileSync("npx", ["convex", "env", "set", "--", "OPENROUTER_API_KEY", openrouterKey], { stdio: "pipe" });
|
|
1472
|
+
}
|
|
1473
|
+
}
|
|
1474
|
+
if (plugins["payments"]) {
|
|
1475
|
+
const stripeSecret = await text3({
|
|
1476
|
+
message: "STRIPE_WEBHOOK_SECRET (Enter para pular):",
|
|
1477
|
+
initialValue: ""
|
|
1478
|
+
});
|
|
1479
|
+
if (!isCancel5(stripeSecret) && stripeSecret.trim()) {
|
|
1480
|
+
execFileSync("npx", ["convex", "env", "set", "--", "STRIPE_WEBHOOK_SECRET", stripeSecret], { stdio: "pipe" });
|
|
1481
|
+
}
|
|
1482
|
+
const asaasToken = await text3({
|
|
1483
|
+
message: "ASAAS_WEBHOOK_TOKEN (Enter para pular):",
|
|
1484
|
+
initialValue: ""
|
|
1485
|
+
});
|
|
1486
|
+
if (!isCancel5(asaasToken) && asaasToken.trim()) {
|
|
1487
|
+
execFileSync("npx", ["convex", "env", "set", "--", "ASAAS_WEBHOOK_TOKEN", asaasToken], { stdio: "pipe" });
|
|
1488
|
+
}
|
|
1489
|
+
}
|
|
1490
|
+
const adminEmail = await text3({
|
|
1491
|
+
message: "Email do usu\xE1rio root:",
|
|
1492
|
+
validate: (v) => {
|
|
1493
|
+
if (!v.includes("@") || !v.includes("."))
|
|
1494
|
+
return "Email inv\xE1lido \u2014 deve conter @ e pelo menos um ponto";
|
|
1495
|
+
}
|
|
1496
|
+
});
|
|
1497
|
+
if (isCancel5(adminEmail)) {
|
|
1498
|
+
cancel5("Setup cancelado.");
|
|
1499
|
+
return;
|
|
1500
|
+
}
|
|
1501
|
+
const adminPassword = await password({
|
|
1502
|
+
message: "Senha do usu\xE1rio root (m\xEDnimo 12 caracteres):",
|
|
1503
|
+
validate: (v) => {
|
|
1504
|
+
if (v.length < 12) return "Senha deve ter pelo menos 12 caracteres";
|
|
1505
|
+
}
|
|
1506
|
+
});
|
|
1507
|
+
if (isCancel5(adminPassword)) {
|
|
1508
|
+
cancel5("Setup cancelado.");
|
|
1509
|
+
return;
|
|
1510
|
+
}
|
|
1511
|
+
const adminPasswordConfirm = await password({
|
|
1512
|
+
message: "Confirme a senha:",
|
|
1513
|
+
validate: (v) => {
|
|
1514
|
+
if (v !== adminPassword) return "As senhas n\xE3o coincidem";
|
|
1515
|
+
}
|
|
1516
|
+
});
|
|
1517
|
+
if (isCancel5(adminPasswordConfirm)) {
|
|
1518
|
+
cancel5("Setup cancelado.");
|
|
1519
|
+
return;
|
|
1520
|
+
}
|
|
1521
|
+
try {
|
|
1522
|
+
execFileSync(
|
|
1523
|
+
"npx",
|
|
1524
|
+
["convex", "run", "seed:setupAdmin", JSON.stringify({ email: adminEmail, password: adminPassword })],
|
|
1525
|
+
{ stdio: "pipe" }
|
|
1526
|
+
);
|
|
1527
|
+
} catch (err) {
|
|
1528
|
+
const e = err;
|
|
1529
|
+
const output = e.stdout?.toString() ?? e.stderr?.toString() ?? e.message ?? "";
|
|
1530
|
+
if (output.includes("Root user already exists")) {
|
|
1531
|
+
cancel5("Admin j\xE1 configurado. Para redefinir, acesse o Convex Dashboard.");
|
|
1532
|
+
} else {
|
|
1533
|
+
cancel5(`Erro ao criar usu\xE1rio root: ${output || String(err)}`);
|
|
1534
|
+
}
|
|
1535
|
+
return;
|
|
1536
|
+
}
|
|
1537
|
+
const vercelHookUrl = await text3({
|
|
1538
|
+
message: "VERCEL_DEPLOY_HOOK_URL (Enter para pular):",
|
|
1539
|
+
initialValue: ""
|
|
1540
|
+
});
|
|
1541
|
+
if (!isCancel5(vercelHookUrl) && vercelHookUrl.trim()) {
|
|
1542
|
+
execFileSync("npx", ["convex", "env", "set", "--", "VERCEL_DEPLOY_HOOK_URL", vercelHookUrl], { stdio: "pipe" });
|
|
1543
|
+
}
|
|
1544
|
+
const vercelWebhookSecret = await text3({
|
|
1545
|
+
message: "VERCEL_WEBHOOK_SECRET (Enter para pular):",
|
|
1546
|
+
initialValue: ""
|
|
1547
|
+
});
|
|
1548
|
+
if (!isCancel5(vercelWebhookSecret) && vercelWebhookSecret.trim()) {
|
|
1549
|
+
execFileSync("npx", ["convex", "env", "set", "--", "VERCEL_WEBHOOK_SECRET", vercelWebhookSecret], { stdio: "pipe" });
|
|
1550
|
+
}
|
|
1551
|
+
outro4(
|
|
1552
|
+
`Setup conclu\xEDdo!
|
|
1553
|
+
|
|
1554
|
+
Admin criado: ${adminEmail}
|
|
1555
|
+
Acesse: /login
|
|
1556
|
+
|
|
1557
|
+
Pr\xF3ximo passo: pnpm dev`
|
|
1558
|
+
);
|
|
1559
|
+
}
|
|
1560
|
+
|
|
1561
|
+
// src/index.ts
|
|
1562
|
+
async function runCli(argv, cwd) {
|
|
1563
|
+
const [, , command, projectName] = argv;
|
|
1564
|
+
intro3("Rubrica CLI");
|
|
1565
|
+
if (command === "create") {
|
|
1566
|
+
if (!projectName) {
|
|
1567
|
+
console.error("Uso: rubrica create <nome-do-projeto>");
|
|
1568
|
+
process.exit(1);
|
|
1569
|
+
}
|
|
1570
|
+
await runCreate(projectName, { projectsDir: cwd });
|
|
1571
|
+
} else if (command === "config") {
|
|
1572
|
+
await runConfig({ cwd });
|
|
1573
|
+
} else if (command === "update") {
|
|
1574
|
+
await runUpdate({ cwd });
|
|
1575
|
+
} else if (command === "setup") {
|
|
1576
|
+
await runSetup({ cwd });
|
|
1577
|
+
} else {
|
|
1578
|
+
console.log("Comandos dispon\xEDveis: create <nome-do-projeto> | config | update | setup");
|
|
1579
|
+
}
|
|
1580
|
+
}
|
|
1581
|
+
await runCli(process.argv, process.cwd());
|
|
1582
|
+
export {
|
|
1583
|
+
runCli
|
|
1584
|
+
};
|