@techstream/quark-create-app 1.9.0 → 1.10.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 +2 -2
- package/package.json +1 -1
- package/src/index.js +376 -143
- package/templates/base-project/.cursor/rules/quark.mdc +172 -0
- package/templates/base-project/.github/copilot-instructions.md +55 -0
- package/templates/base-project/CLAUDE.md +273 -0
- package/templates/base-project/README.md +72 -30
- package/templates/base-project/apps/web/next.config.js +5 -1
- package/templates/base-project/apps/web/package.json +3 -3
- package/templates/base-project/apps/web/public/quark.svg +46 -0
- package/templates/base-project/apps/web/railway.json +2 -2
- package/templates/base-project/apps/web/src/app/_components/HealthIndicator.js +85 -0
- package/templates/base-project/apps/web/src/app/_components/HomeThemeToggle.js +63 -0
- package/templates/base-project/apps/web/src/app/_components/QuarkAnimation.js +168 -0
- package/templates/base-project/apps/web/src/app/api/health/route.js +28 -17
- package/templates/base-project/apps/web/src/app/favicon.ico +0 -0
- package/templates/base-project/apps/web/src/app/global-error.js +53 -0
- package/templates/base-project/apps/web/src/app/globals.css +121 -15
- package/templates/base-project/apps/web/src/app/icon.svg +46 -0
- package/templates/base-project/apps/web/src/app/layout.js +1 -0
- package/templates/base-project/apps/web/src/app/not-found.js +35 -0
- package/templates/base-project/apps/web/src/app/page.js +38 -5
- package/templates/base-project/apps/web/src/lib/theme.js +23 -0
- package/templates/base-project/apps/web/src/proxy.js +10 -2
- package/templates/base-project/package.json +2 -0
- package/templates/base-project/packages/db/src/client.js +6 -1
- package/templates/base-project/packages/db/src/index.js +1 -0
- package/templates/base-project/packages/db/src/ping.js +66 -0
- package/templates/base-project/scripts/doctor.js +261 -0
- package/templates/base-project/turbo.json +2 -1
- package/templates/config/package.json +1 -0
- package/templates/jobs/package.json +2 -1
- package/templates/ui/README.md +67 -0
- package/templates/ui/package.json +1 -0
- package/templates/ui/src/badge.js +32 -0
- package/templates/ui/src/badge.test.js +42 -0
- package/templates/ui/src/button.js +64 -15
- package/templates/ui/src/button.test.js +34 -5
- package/templates/ui/src/card.js +58 -0
- package/templates/ui/src/card.test.js +59 -0
- package/templates/ui/src/checkbox.js +35 -0
- package/templates/ui/src/checkbox.test.js +35 -0
- package/templates/ui/src/dialog.js +139 -0
- package/templates/ui/src/dialog.test.js +15 -0
- package/templates/ui/src/index.js +16 -0
- package/templates/ui/src/input.js +15 -0
- package/templates/ui/src/input.test.js +27 -0
- package/templates/ui/src/label.js +14 -0
- package/templates/ui/src/label.test.js +22 -0
- package/templates/ui/src/select.js +42 -0
- package/templates/ui/src/select.test.js +27 -0
- package/templates/ui/src/skeleton.js +14 -0
- package/templates/ui/src/skeleton.test.js +22 -0
- package/templates/ui/src/table.js +75 -0
- package/templates/ui/src/table.test.js +69 -0
- package/templates/ui/src/textarea.js +15 -0
- package/templates/ui/src/textarea.test.js +27 -0
- package/templates/ui/src/theme-constants.js +24 -0
- package/templates/ui/src/theme.js +132 -0
- package/templates/ui/src/toast.js +229 -0
- package/templates/ui/src/toast.test.js +23 -0
- package/templates/{base-project/apps/worker → worker}/package.json +2 -2
- package/templates/{base-project/apps/worker → worker}/src/index.js +38 -23
- package/templates/{base-project/apps/worker → worker}/src/index.test.js +19 -20
- package/templates/base-project/apps/web/public/file.svg +0 -1
- package/templates/base-project/apps/web/public/globe.svg +0 -1
- package/templates/base-project/apps/web/public/next.svg +0 -1
- package/templates/base-project/apps/web/public/vercel.svg +0 -1
- package/templates/base-project/apps/web/public/window.svg +0 -1
- /package/templates/{base-project/apps/worker → worker}/README.md +0 -0
- /package/templates/{base-project/apps/worker → worker}/railway.json +0 -0
- /package/templates/{base-project/apps/worker → worker}/src/handlers/email.js +0 -0
- /package/templates/{base-project/apps/worker → worker}/src/handlers/files.js +0 -0
- /package/templates/{base-project/apps/worker → worker}/src/handlers/index.js +0 -0
package/src/index.js
CHANGED
|
@@ -190,13 +190,42 @@ function replaceDepsScope(deps, scope, selectedPackages) {
|
|
|
190
190
|
}
|
|
191
191
|
}
|
|
192
192
|
|
|
193
|
+
/**
|
|
194
|
+
* Remove unselected optional feature entries from next.config.js transpilePackages.
|
|
195
|
+
* Must run AFTER replaceImportsInSourceFiles so package names are already scoped.
|
|
196
|
+
*/
|
|
197
|
+
async function patchNextConfig(webDir, scope, selectedFeatures) {
|
|
198
|
+
const configPath = path.join(webDir, "next.config.js");
|
|
199
|
+
if (!(await fs.pathExists(configPath))) return;
|
|
200
|
+
|
|
201
|
+
let content = await fs.readFile(configPath, "utf-8");
|
|
202
|
+
|
|
203
|
+
const optionalEntries = {
|
|
204
|
+
ui: `@${scope}/ui`,
|
|
205
|
+
jobs: `@${scope}/jobs`,
|
|
206
|
+
admin: `@${scope}/admin`,
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
for (const [feature, pkg] of Object.entries(optionalEntries)) {
|
|
210
|
+
if (!selectedFeatures.includes(feature)) {
|
|
211
|
+
// Remove the line containing this package from transpilePackages
|
|
212
|
+
content = content.replace(
|
|
213
|
+
new RegExp(`[ \\t]*"${pkg.replace("/", "\\/")}",?\\n`, "g"),
|
|
214
|
+
"",
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
await fs.writeFile(configPath, content);
|
|
220
|
+
}
|
|
221
|
+
|
|
193
222
|
/**
|
|
194
223
|
* Replace @techstream/quark-* import paths in all .js source files
|
|
195
224
|
* for workspace packages (db, jobs, ui, config) with @scope/* equivalents.
|
|
196
225
|
* Registry packages (@techstream/quark-core) are left untouched.
|
|
197
226
|
*/
|
|
198
227
|
async function replaceImportsInSourceFiles(dir, scope) {
|
|
199
|
-
const workspacePackages = ["db", "jobs", "ui", "config"];
|
|
228
|
+
const workspacePackages = ["db", "jobs", "ui", "config", "admin"];
|
|
200
229
|
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
201
230
|
|
|
202
231
|
for (const entry of entries) {
|
|
@@ -261,8 +290,10 @@ program
|
|
|
261
290
|
)
|
|
262
291
|
.option(
|
|
263
292
|
"--features <features>",
|
|
264
|
-
"Comma-separated list of optional features to include (ui,jobs)",
|
|
293
|
+
"Comma-separated list of optional features to include (ui,jobs,admin)",
|
|
265
294
|
)
|
|
295
|
+
.option("--skip-install", "Skip pnpm install and Prisma generate steps")
|
|
296
|
+
.option("--skip-docker", "Skip Docker orphan-volume cleanup")
|
|
266
297
|
.action(async (projectName, options) => {
|
|
267
298
|
console.log(
|
|
268
299
|
chalk.blue.bold(
|
|
@@ -280,48 +311,49 @@ program
|
|
|
280
311
|
// These persist even if the project directory is manually deleted, causing
|
|
281
312
|
// authentication failures when the new project generates different credentials.
|
|
282
313
|
// We also need to stop any running containers that reference these volumes.
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
"
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
`name=${volumePrefix}`,
|
|
290
|
-
"--format",
|
|
291
|
-
"{{.Name}}",
|
|
292
|
-
]);
|
|
293
|
-
const orphanedVolumes = stdout
|
|
294
|
-
.split("\n")
|
|
295
|
-
.filter((v) => v.startsWith(volumePrefix));
|
|
296
|
-
if (orphanedVolumes.length > 0) {
|
|
297
|
-
// Stop and remove any containers using these volumes first
|
|
298
|
-
const { stdout: containerOut } = await execa("docker", [
|
|
299
|
-
"ps",
|
|
300
|
-
"-a",
|
|
314
|
+
if (!options.skipDocker)
|
|
315
|
+
try {
|
|
316
|
+
const volumePrefix = `${projectName}_`;
|
|
317
|
+
const { stdout } = await execa("docker", [
|
|
318
|
+
"volume",
|
|
319
|
+
"ls",
|
|
301
320
|
"--filter",
|
|
302
|
-
`name=${
|
|
321
|
+
`name=${volumePrefix}`,
|
|
303
322
|
"--format",
|
|
304
|
-
"{{.
|
|
323
|
+
"{{.Name}}",
|
|
305
324
|
]);
|
|
306
|
-
const
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
325
|
+
const orphanedVolumes = stdout
|
|
326
|
+
.split("\n")
|
|
327
|
+
.filter((v) => v.startsWith(volumePrefix));
|
|
328
|
+
if (orphanedVolumes.length > 0) {
|
|
329
|
+
// Stop and remove any containers using these volumes first
|
|
330
|
+
const { stdout: containerOut } = await execa("docker", [
|
|
331
|
+
"ps",
|
|
332
|
+
"-a",
|
|
333
|
+
"--filter",
|
|
334
|
+
`name=${projectName}`,
|
|
335
|
+
"--format",
|
|
336
|
+
"{{.ID}}",
|
|
337
|
+
]);
|
|
338
|
+
const containers = containerOut.split("\n").filter(Boolean);
|
|
339
|
+
if (containers.length > 0) {
|
|
340
|
+
await execa("docker", ["rm", "-f", ...containers]);
|
|
341
|
+
}
|
|
342
|
+
// Remove the Docker network if it exists
|
|
343
|
+
try {
|
|
344
|
+
await execa("docker", ["network", "rm", `${projectName}_default`]);
|
|
345
|
+
} catch {
|
|
346
|
+
// Network may not exist — fine
|
|
347
|
+
}
|
|
348
|
+
// Now remove the orphaned volumes
|
|
349
|
+
for (const vol of orphanedVolumes) {
|
|
350
|
+
await execa("docker", ["volume", "rm", "-f", vol]);
|
|
351
|
+
}
|
|
352
|
+
console.log(chalk.green(" ✓ Cleaned up orphaned Docker volumes"));
|
|
319
353
|
}
|
|
320
|
-
|
|
354
|
+
} catch {
|
|
355
|
+
// Docker not available — fine
|
|
321
356
|
}
|
|
322
|
-
} catch {
|
|
323
|
-
// Docker not available — fine
|
|
324
|
-
}
|
|
325
357
|
|
|
326
358
|
// Check if directory already exists
|
|
327
359
|
if (await fs.pathExists(targetDir)) {
|
|
@@ -403,7 +435,7 @@ program
|
|
|
403
435
|
if (!options.prompts && options.features) {
|
|
404
436
|
// Parse features from CLI flag
|
|
405
437
|
console.log(chalk.cyan("\n 🎯 Configuring optional features..."));
|
|
406
|
-
const validFeatures = ["ui", "jobs"];
|
|
438
|
+
const validFeatures = ["ui", "jobs", "admin"];
|
|
407
439
|
features = options.features
|
|
408
440
|
.split(",")
|
|
409
441
|
.map((f) => f.trim())
|
|
@@ -419,6 +451,11 @@ program
|
|
|
419
451
|
);
|
|
420
452
|
}
|
|
421
453
|
|
|
454
|
+
// Enforce admin dependencies: admin requires ui
|
|
455
|
+
if (features.includes("admin") && !features.includes("ui")) {
|
|
456
|
+
features.push("ui");
|
|
457
|
+
}
|
|
458
|
+
|
|
422
459
|
console.log(
|
|
423
460
|
chalk.green(
|
|
424
461
|
` Selected features: ${features.join(", ") || "none"} (non-interactive mode)`,
|
|
@@ -427,7 +464,7 @@ program
|
|
|
427
464
|
} else if (!options.prompts) {
|
|
428
465
|
// Use defaults when --no-prompts is set without --features
|
|
429
466
|
console.log(chalk.cyan("\n 🎯 Configuring optional features..."));
|
|
430
|
-
features = ["ui", "jobs"]; // Default to
|
|
467
|
+
features = ["ui", "jobs"]; // Default to ui + jobs (not admin)
|
|
431
468
|
console.log(
|
|
432
469
|
chalk.green(
|
|
433
470
|
` Using default features: ${features.join(", ")} (non-interactive mode)`,
|
|
@@ -449,10 +486,15 @@ program
|
|
|
449
486
|
selected: true,
|
|
450
487
|
},
|
|
451
488
|
{
|
|
452
|
-
title: "
|
|
489
|
+
title: "Background Jobs (packages/jobs + apps/worker)",
|
|
453
490
|
value: "jobs",
|
|
454
491
|
selected: true,
|
|
455
492
|
},
|
|
493
|
+
{
|
|
494
|
+
title: "Admin Dashboard (packages/admin) [requires: ui]",
|
|
495
|
+
value: "admin",
|
|
496
|
+
selected: false,
|
|
497
|
+
},
|
|
456
498
|
],
|
|
457
499
|
},
|
|
458
500
|
]);
|
|
@@ -465,6 +507,14 @@ program
|
|
|
465
507
|
await fs.remove(targetDir);
|
|
466
508
|
process.exit(0);
|
|
467
509
|
}
|
|
510
|
+
|
|
511
|
+
// Enforce admin dependencies: admin requires ui
|
|
512
|
+
if (features.includes("admin") && !features.includes("ui")) {
|
|
513
|
+
features.push("ui");
|
|
514
|
+
console.log(
|
|
515
|
+
chalk.yellow(" ℹ Admin requires UI — automatically included."),
|
|
516
|
+
);
|
|
517
|
+
}
|
|
468
518
|
}
|
|
469
519
|
|
|
470
520
|
// Step 6: Copy selected optional packages
|
|
@@ -472,6 +522,17 @@ program
|
|
|
472
522
|
console.log(chalk.cyan("\n 📋 Setting up optional packages..."));
|
|
473
523
|
|
|
474
524
|
for (const feature of features) {
|
|
525
|
+
// admin is a package template — skip if no template exists yet
|
|
526
|
+
const templatePath = path.join(templatesDir, feature);
|
|
527
|
+
if (!(await fs.pathExists(templatePath))) {
|
|
528
|
+
console.log(
|
|
529
|
+
chalk.yellow(
|
|
530
|
+
` ⚠ ${feature} (template not yet available — skipped)`,
|
|
531
|
+
),
|
|
532
|
+
);
|
|
533
|
+
continue;
|
|
534
|
+
}
|
|
535
|
+
|
|
475
536
|
const packageDir = path.join(targetDir, "packages", feature);
|
|
476
537
|
await fs.ensureDir(packageDir);
|
|
477
538
|
await copyTemplate(feature, packageDir);
|
|
@@ -482,6 +543,14 @@ program
|
|
|
482
543
|
|
|
483
544
|
console.log(chalk.green(` ✓ ${feature}`));
|
|
484
545
|
}
|
|
546
|
+
|
|
547
|
+
// If jobs selected, also scaffold apps/worker from its template
|
|
548
|
+
if (features.includes("jobs")) {
|
|
549
|
+
const workerDir = path.join(targetDir, "apps", "worker");
|
|
550
|
+
await fs.ensureDir(workerDir);
|
|
551
|
+
await copyTemplate("worker", workerDir);
|
|
552
|
+
console.log(chalk.green(` ✓ worker (paired with jobs)`));
|
|
553
|
+
}
|
|
485
554
|
}
|
|
486
555
|
|
|
487
556
|
// Step 7: Update all package.json dependencies to use correct scope
|
|
@@ -490,7 +559,10 @@ program
|
|
|
490
559
|
// Collect all package.json files that need scope replacement (apps + packages)
|
|
491
560
|
const allPkgPaths = [
|
|
492
561
|
path.join(targetDir, "apps", "web", "package.json"),
|
|
493
|
-
|
|
562
|
+
// Worker is only present when jobs is selected
|
|
563
|
+
...(features.includes("jobs")
|
|
564
|
+
? [path.join(targetDir, "apps", "worker", "package.json")]
|
|
565
|
+
: []),
|
|
494
566
|
// Also update cross-dependencies in scaffolded packages (e.g. db → config)
|
|
495
567
|
...["db", ...features].map((pkg) =>
|
|
496
568
|
path.join(targetDir, "packages", pkg, "package.json"),
|
|
@@ -529,6 +601,45 @@ program
|
|
|
529
601
|
await replaceImportsInSourceFiles(targetDir, scope);
|
|
530
602
|
console.log(chalk.green(` ✓ Import paths updated`));
|
|
531
603
|
|
|
604
|
+
// Step 7c: Patch next.config.js to remove unselected feature transpilePackages entries
|
|
605
|
+
await patchNextConfig(
|
|
606
|
+
path.join(targetDir, "apps", "web"),
|
|
607
|
+
scope,
|
|
608
|
+
features,
|
|
609
|
+
);
|
|
610
|
+
console.log(chalk.green(` ✓ next.config.js patched`));
|
|
611
|
+
|
|
612
|
+
// Step 7d: Strip jobs-related code from register route when jobs not selected
|
|
613
|
+
if (!features.includes("jobs")) {
|
|
614
|
+
const registerPath = path.join(
|
|
615
|
+
targetDir,
|
|
616
|
+
"apps",
|
|
617
|
+
"web",
|
|
618
|
+
"src",
|
|
619
|
+
"app",
|
|
620
|
+
"api",
|
|
621
|
+
"auth",
|
|
622
|
+
"register",
|
|
623
|
+
"route.js",
|
|
624
|
+
);
|
|
625
|
+
if (await fs.pathExists(registerPath)) {
|
|
626
|
+
let content = await fs.readFile(registerPath, "utf-8");
|
|
627
|
+
// Remove the jobs import line
|
|
628
|
+
content = content.replace(
|
|
629
|
+
/import\s*\{[^}]*\}\s*from\s*["']@[^/]+\/jobs["'];?\n/,
|
|
630
|
+
"",
|
|
631
|
+
);
|
|
632
|
+
// Remove createQueue from the core import if present
|
|
633
|
+
content = content.replace(/\tcreateQueue,\n/, "");
|
|
634
|
+
// Remove the @quark:start:jobs ... @quark:end:jobs block (inclusive)
|
|
635
|
+
content = content.replace(
|
|
636
|
+
/[ \t]*\/\/ @quark:start:jobs[\s\S]*?\/\/ @quark:end:jobs\n?/,
|
|
637
|
+
"",
|
|
638
|
+
);
|
|
639
|
+
await fs.writeFile(registerPath, content);
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
|
|
532
643
|
// Step 8: Create .env.example file
|
|
533
644
|
console.log(chalk.cyan("\n 📋 Creating environment configuration..."));
|
|
534
645
|
const envExampleTemplate = `# ⚠️ IMPORTANT: Copy this file to .env and fill in the values for your environment.
|
|
@@ -750,6 +861,8 @@ STORAGE_PROVIDER=local
|
|
|
750
861
|
scaffoldedDate: new Date().toISOString(),
|
|
751
862
|
requiredPackages: ["db", "config"],
|
|
752
863
|
packages: features,
|
|
864
|
+
// Track that worker is paired with jobs (not independently selectable)
|
|
865
|
+
hasWorker: features.includes("jobs"),
|
|
753
866
|
};
|
|
754
867
|
await fs.writeFile(
|
|
755
868
|
path.join(targetDir, ".quark-link.json"),
|
|
@@ -757,8 +870,24 @@ STORAGE_PROVIDER=local
|
|
|
757
870
|
);
|
|
758
871
|
console.log(chalk.green(` ✓ .quark-link.json`));
|
|
759
872
|
|
|
760
|
-
// Step 10b: Generate
|
|
761
|
-
console.log(chalk.cyan("\n 🤖 Generating
|
|
873
|
+
// Step 10b + 10c: Generate all AI coding tool context files
|
|
874
|
+
console.log(chalk.cyan("\n 🤖 Generating AI context files..."));
|
|
875
|
+
|
|
876
|
+
// Build shared variable map (used by SKILL.md, CLAUDE.md, .cursor/rules, copilot-instructions)
|
|
877
|
+
const scaffoldDate = new Date().toISOString().split("T")[0];
|
|
878
|
+
const optionalLines = features
|
|
879
|
+
.map((f) => {
|
|
880
|
+
const labels = {
|
|
881
|
+
ui: "Shared UI components",
|
|
882
|
+
jobs: "Job queue definitions",
|
|
883
|
+
admin: "Auto-generated admin dashboard",
|
|
884
|
+
};
|
|
885
|
+
return `│ ├── ${f}/ # ${labels[f] || f}`;
|
|
886
|
+
})
|
|
887
|
+
.join("\n");
|
|
888
|
+
const optionalBlock = optionalLines ? `${optionalLines}\n` : "";
|
|
889
|
+
|
|
890
|
+
// Step 10b: Substitute variables in project-context SKILL.md
|
|
762
891
|
const skillPath = path.join(
|
|
763
892
|
targetDir,
|
|
764
893
|
".github",
|
|
@@ -768,34 +897,38 @@ STORAGE_PROVIDER=local
|
|
|
768
897
|
);
|
|
769
898
|
if (await fs.pathExists(skillPath)) {
|
|
770
899
|
let skillContent = await fs.readFile(skillPath, "utf-8");
|
|
771
|
-
|
|
772
|
-
// Build optional packages section
|
|
773
|
-
const optionalLines = features
|
|
774
|
-
.map((f) => {
|
|
775
|
-
const labels = {
|
|
776
|
-
ui: "Shared UI components",
|
|
777
|
-
jobs: "Job queue definitions",
|
|
778
|
-
};
|
|
779
|
-
return `│ ├── ${f}/ # ${labels[f] || f}`;
|
|
780
|
-
})
|
|
781
|
-
.join("\n");
|
|
782
|
-
const optionalBlock = optionalLines ? `${optionalLines}\n` : "";
|
|
783
|
-
|
|
784
900
|
skillContent = skillContent
|
|
785
901
|
.replace(/__QUARK_SCOPE__/g, scope)
|
|
786
902
|
.replace(/__QUARK_PROJECT_NAME__/g, projectName)
|
|
787
|
-
.replace(
|
|
788
|
-
/__QUARK_SCAFFOLD_DATE__/g,
|
|
789
|
-
new Date().toISOString().split("T")[0],
|
|
790
|
-
)
|
|
903
|
+
.replace(/__QUARK_SCAFFOLD_DATE__/g, scaffoldDate)
|
|
791
904
|
.replace(/__QUARK_OPTIONAL_PACKAGES__/g, optionalBlock);
|
|
792
|
-
|
|
793
905
|
await fs.writeFile(skillPath, skillContent);
|
|
794
906
|
console.log(
|
|
795
907
|
chalk.green(` ✓ .github/skills/project-context/SKILL.md`),
|
|
796
908
|
);
|
|
797
909
|
}
|
|
798
910
|
|
|
911
|
+
// Step 10c: Substitute variables in all AI coding tool context files
|
|
912
|
+
const aiContextFiles = [
|
|
913
|
+
"README.md",
|
|
914
|
+
"CLAUDE.md",
|
|
915
|
+
".cursor/rules/quark.mdc",
|
|
916
|
+
".github/copilot-instructions.md",
|
|
917
|
+
];
|
|
918
|
+
for (const relPath of aiContextFiles) {
|
|
919
|
+
const filePath = path.join(targetDir, relPath);
|
|
920
|
+
if (await fs.pathExists(filePath)) {
|
|
921
|
+
let content = await fs.readFile(filePath, "utf-8");
|
|
922
|
+
content = content
|
|
923
|
+
.replace(/__QUARK_SCOPE__/g, scope)
|
|
924
|
+
.replace(/__QUARK_PROJECT_NAME__/g, projectName)
|
|
925
|
+
.replace(/__QUARK_SCAFFOLD_DATE__/g, scaffoldDate)
|
|
926
|
+
.replace(/__QUARK_OPTIONAL_PACKAGES__/g, optionalBlock);
|
|
927
|
+
await fs.writeFile(filePath, content);
|
|
928
|
+
console.log(chalk.green(` ✓ ${relPath}`));
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
|
|
799
932
|
// Step 11: Initialize git repository
|
|
800
933
|
console.log(chalk.cyan("\n 📝 Initializing git repository..."));
|
|
801
934
|
const gitInitialized = await initializeGit(targetDir);
|
|
@@ -804,41 +937,45 @@ STORAGE_PROVIDER=local
|
|
|
804
937
|
}
|
|
805
938
|
|
|
806
939
|
// Step 12: Run pnpm install
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
940
|
+
if (!options.skipInstall) {
|
|
941
|
+
console.log(chalk.cyan("\n 📦 Installing dependencies..."));
|
|
942
|
+
try {
|
|
943
|
+
await execa("pnpm", ["install"], {
|
|
944
|
+
cwd: targetDir,
|
|
945
|
+
stdio: "inherit",
|
|
946
|
+
});
|
|
947
|
+
console.log(chalk.green(`\n ✓ Dependencies installed`));
|
|
948
|
+
} catch (installError) {
|
|
949
|
+
console.warn(
|
|
950
|
+
chalk.yellow(
|
|
951
|
+
`\n ⚠️ pnpm install failed: ${installError.message}`,
|
|
952
|
+
),
|
|
953
|
+
);
|
|
954
|
+
console.warn(
|
|
955
|
+
chalk.yellow(
|
|
956
|
+
` Run 'pnpm install' manually after resolving the issue.`,
|
|
957
|
+
),
|
|
958
|
+
);
|
|
959
|
+
}
|
|
824
960
|
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
961
|
+
// Step 13: Generate Prisma client
|
|
962
|
+
console.log(chalk.cyan("\n 🗄️ Generating Prisma client..."));
|
|
963
|
+
try {
|
|
964
|
+
await execa("pnpm", ["--filter", "db", "db:generate"], {
|
|
965
|
+
cwd: targetDir,
|
|
966
|
+
stdio: "inherit",
|
|
967
|
+
});
|
|
968
|
+
console.log(chalk.green(` ✓ Prisma client generated`));
|
|
969
|
+
} catch (generateError) {
|
|
970
|
+
console.warn(
|
|
971
|
+
chalk.yellow(
|
|
972
|
+
`\n ⚠️ Prisma generate failed: ${generateError.message}`,
|
|
973
|
+
),
|
|
974
|
+
);
|
|
975
|
+
console.warn(
|
|
976
|
+
chalk.yellow(` Run 'pnpm --filter db db:generate' manually.`),
|
|
977
|
+
);
|
|
978
|
+
}
|
|
842
979
|
}
|
|
843
980
|
|
|
844
981
|
// Success message
|
|
@@ -849,7 +986,7 @@ STORAGE_PROVIDER=local
|
|
|
849
986
|
);
|
|
850
987
|
|
|
851
988
|
console.log(chalk.white(`📂 Project location: ${targetDir}\n`));
|
|
852
|
-
console.log(chalk.cyan("
|
|
989
|
+
console.log(chalk.cyan("Start here:"));
|
|
853
990
|
console.log(chalk.white(` 1. cd ${projectName}`));
|
|
854
991
|
console.log(chalk.white(` 2. docker compose up -d`));
|
|
855
992
|
console.log(chalk.white(` 3. pnpm db:migrate`));
|
|
@@ -861,20 +998,20 @@ STORAGE_PROVIDER=local
|
|
|
861
998
|
),
|
|
862
999
|
);
|
|
863
1000
|
|
|
864
|
-
console.log(chalk.cyan("
|
|
1001
|
+
console.log(chalk.cyan("Build with AI:"));
|
|
865
1002
|
console.log(
|
|
866
|
-
chalk.white(
|
|
867
|
-
` • Update Quark core with: pnpm update @techstream/quark-core`,
|
|
868
|
-
),
|
|
1003
|
+
chalk.white(` Open CLAUDE.md in your AI tool, then tell it:`),
|
|
869
1004
|
);
|
|
870
1005
|
console.log(
|
|
871
|
-
chalk.white(
|
|
1006
|
+
chalk.white(
|
|
1007
|
+
` "I'm building ${projectName} — [what it does]. Review CLAUDE.md and let's start."\n`,
|
|
1008
|
+
),
|
|
872
1009
|
);
|
|
873
1010
|
|
|
874
|
-
console.log(chalk.cyan("Learn more:"));
|
|
875
|
-
console.log(chalk.white(` 📖 Docs: https://github.com/Bobnoddle/quark`));
|
|
876
1011
|
console.log(
|
|
877
|
-
chalk.
|
|
1012
|
+
chalk.dim(
|
|
1013
|
+
` 📖 github.com/Bobnoddle/quark • Updates: npx @techstream/quark-create-app update\n`,
|
|
1014
|
+
),
|
|
878
1015
|
);
|
|
879
1016
|
} catch (error) {
|
|
880
1017
|
console.error(chalk.red(`\n✗ Error creating project: ${error.message}`));
|
|
@@ -889,13 +1026,55 @@ STORAGE_PROVIDER=local
|
|
|
889
1026
|
}
|
|
890
1027
|
});
|
|
891
1028
|
|
|
1029
|
+
/**
|
|
1030
|
+
* Read the installed version of a package from node_modules.
|
|
1031
|
+
* Returns null if the package is not installed.
|
|
1032
|
+
* @param {string} cwd - Project root
|
|
1033
|
+
* @param {string} packageName - e.g. "@techstream/quark-core"
|
|
1034
|
+
* @returns {Promise<string|null>}
|
|
1035
|
+
*/
|
|
1036
|
+
async function getInstalledVersion(cwd, packageName) {
|
|
1037
|
+
try {
|
|
1038
|
+
const pkgPath = path.join(
|
|
1039
|
+
cwd,
|
|
1040
|
+
"node_modules",
|
|
1041
|
+
...packageName.split("/"),
|
|
1042
|
+
"package.json",
|
|
1043
|
+
);
|
|
1044
|
+
const { version } = await fs.readJSON(pkgPath);
|
|
1045
|
+
return version ?? null;
|
|
1046
|
+
} catch {
|
|
1047
|
+
return null;
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
/**
|
|
1052
|
+
* Fetch the latest published version of a package from the npm registry.
|
|
1053
|
+
* Returns null on network failure so the caller can degrade gracefully.
|
|
1054
|
+
* @param {string} packageName
|
|
1055
|
+
* @returns {Promise<string|null>}
|
|
1056
|
+
*/
|
|
1057
|
+
async function getLatestNpmVersion(packageName) {
|
|
1058
|
+
try {
|
|
1059
|
+
const { stdout } = await execa("npm", [
|
|
1060
|
+
"view",
|
|
1061
|
+
packageName,
|
|
1062
|
+
"version",
|
|
1063
|
+
"--json",
|
|
1064
|
+
]);
|
|
1065
|
+
return JSON.parse(stdout);
|
|
1066
|
+
} catch {
|
|
1067
|
+
return null;
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
|
|
892
1071
|
/**
|
|
893
1072
|
* quark-update command
|
|
894
1073
|
* Updates Quark core infrastructure in a scaffolded project
|
|
895
1074
|
*/
|
|
896
1075
|
program
|
|
897
1076
|
.command("update")
|
|
898
|
-
.description("Update Quark
|
|
1077
|
+
.description("Update Quark packages in the current project")
|
|
899
1078
|
.option("--check", "Check for updates without applying")
|
|
900
1079
|
.option("--force", "Skip safety checks")
|
|
901
1080
|
.action(async (options) => {
|
|
@@ -911,23 +1090,60 @@ program
|
|
|
911
1090
|
}
|
|
912
1091
|
|
|
913
1092
|
const quarkLink = await fs.readJSON(quarkLinkPath);
|
|
914
|
-
|
|
915
|
-
|
|
1093
|
+
|
|
1094
|
+
// The packages this command manages
|
|
1095
|
+
const MANAGED_PACKAGES = [
|
|
1096
|
+
"@techstream/quark-core",
|
|
1097
|
+
"@techstream/quark-create-app",
|
|
1098
|
+
];
|
|
1099
|
+
|
|
1100
|
+
// Snapshot installed versions before any update
|
|
1101
|
+
const before = {};
|
|
1102
|
+
for (const name of MANAGED_PACKAGES) {
|
|
1103
|
+
before[name] = await getInstalledVersion(process.cwd(), name);
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
console.log(chalk.cyan(`Scaffolded: ${quarkLink.scaffoldedDate}`));
|
|
1107
|
+
console.log(
|
|
1108
|
+
chalk.cyan(
|
|
1109
|
+
`Installed core: ${before["@techstream/quark-core"] ?? quarkLink.quarkVersion ?? "unknown"}\n`,
|
|
1110
|
+
),
|
|
1111
|
+
);
|
|
916
1112
|
|
|
917
1113
|
if (options.check) {
|
|
918
|
-
|
|
919
|
-
console.log(
|
|
920
|
-
|
|
921
|
-
)
|
|
1114
|
+
// Query npm for latest versions and report the delta
|
|
1115
|
+
console.log(chalk.yellow("Checking npm registry for updates...\n"));
|
|
1116
|
+
let anyUpdates = false;
|
|
1117
|
+
for (const name of MANAGED_PACKAGES) {
|
|
1118
|
+
const installed = before[name];
|
|
1119
|
+
const latest = await getLatestNpmVersion(name);
|
|
1120
|
+
if (!latest) {
|
|
1121
|
+
console.log(chalk.dim(` ${name}: registry unreachable`));
|
|
1122
|
+
continue;
|
|
1123
|
+
}
|
|
1124
|
+
if (!installed || installed === latest) {
|
|
1125
|
+
console.log(chalk.green(` ✓ ${name} ${latest} — up to date`));
|
|
1126
|
+
} else {
|
|
1127
|
+
console.log(
|
|
1128
|
+
chalk.yellow(` ↑ ${name}: ${installed} → ${chalk.bold(latest)}`),
|
|
1129
|
+
);
|
|
1130
|
+
anyUpdates = true;
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
if (anyUpdates) {
|
|
1134
|
+
console.log(chalk.white("\n Run without --check to apply updates.\n"));
|
|
1135
|
+
} else {
|
|
1136
|
+
console.log(chalk.dim("\n Nothing to update.\n"));
|
|
1137
|
+
}
|
|
922
1138
|
return;
|
|
923
1139
|
}
|
|
924
1140
|
|
|
925
1141
|
try {
|
|
926
|
-
//
|
|
1142
|
+
// Guard: check for both unstaged and staged changes
|
|
927
1143
|
if (!options.force) {
|
|
928
|
-
console.log(chalk.yellow("⚠️ Checking for uncommitted changes..."));
|
|
929
1144
|
try {
|
|
930
|
-
await execa("git", ["diff", "--exit-code"], {
|
|
1145
|
+
await execa("git", ["diff", "--exit-code"], { cwd: process.cwd() });
|
|
1146
|
+
await execa("git", ["diff", "--cached", "--exit-code"], {
|
|
931
1147
|
cwd: process.cwd(),
|
|
932
1148
|
});
|
|
933
1149
|
} catch {
|
|
@@ -943,43 +1159,60 @@ program
|
|
|
943
1159
|
}
|
|
944
1160
|
}
|
|
945
1161
|
|
|
946
|
-
// Run pnpm update
|
|
947
|
-
console.log(chalk.cyan("
|
|
948
|
-
await execa("pnpm", ["update",
|
|
1162
|
+
// Run pnpm update for all managed packages
|
|
1163
|
+
console.log(chalk.cyan("📦 Updating Quark packages...\n"));
|
|
1164
|
+
await execa("pnpm", ["update", ...MANAGED_PACKAGES], {
|
|
949
1165
|
cwd: process.cwd(),
|
|
950
1166
|
stdio: "inherit",
|
|
951
1167
|
});
|
|
952
1168
|
|
|
953
|
-
//
|
|
954
|
-
|
|
1169
|
+
// Snapshot installed versions after update
|
|
1170
|
+
const after = {};
|
|
1171
|
+
for (const name of MANAGED_PACKAGES) {
|
|
1172
|
+
after[name] = await getInstalledVersion(process.cwd(), name);
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
// Report the delta
|
|
1176
|
+
console.log(chalk.cyan("\n📋 Update summary:\n"));
|
|
1177
|
+
for (const name of MANAGED_PACKAGES) {
|
|
1178
|
+
const was = before[name];
|
|
1179
|
+
const now = after[name];
|
|
1180
|
+
if (!now) continue;
|
|
1181
|
+
if (was && was !== now) {
|
|
1182
|
+
console.log(chalk.green(` ✓ ${name}: ${was} → ${chalk.bold(now)}`));
|
|
1183
|
+
} else {
|
|
1184
|
+
console.log(chalk.dim(` · ${name}: ${now} (already current)`));
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
// Persist the new core version — keep previous on failure, never write garbage
|
|
1189
|
+
const newCoreVersion =
|
|
1190
|
+
after["@techstream/quark-core"] ?? quarkLink.quarkVersion;
|
|
1191
|
+
quarkLink.quarkVersion = newCoreVersion;
|
|
1192
|
+
quarkLink.updatedDate = new Date().toISOString();
|
|
1193
|
+
await fs.writeFile(quarkLinkPath, JSON.stringify(quarkLink, null, 2));
|
|
1194
|
+
|
|
1195
|
+
// Run lint to surface any API breakage from the update
|
|
1196
|
+
console.log(chalk.cyan("\n🔍 Running lint to check for breakage...\n"));
|
|
955
1197
|
try {
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
1198
|
+
await execa("pnpm", ["lint"], {
|
|
1199
|
+
cwd: process.cwd(),
|
|
1200
|
+
stdio: "inherit",
|
|
1201
|
+
});
|
|
1202
|
+
console.log(chalk.green("\n ✓ Lint passed\n"));
|
|
1203
|
+
} catch {
|
|
1204
|
+
console.log(
|
|
1205
|
+
chalk.yellow(
|
|
1206
|
+
"\n ⚠️ Lint reported issues — review before committing.\n",
|
|
963
1207
|
),
|
|
964
1208
|
);
|
|
965
|
-
|
|
966
|
-
} catch {}
|
|
967
|
-
quarkLink.quarkVersion = updatedVersion;
|
|
968
|
-
quarkLink.updatedDate = new Date().toISOString();
|
|
969
|
-
await fs.writeFile(quarkLinkPath, JSON.stringify(quarkLink, null, 2));
|
|
1209
|
+
}
|
|
970
1210
|
|
|
971
|
-
console.log(
|
|
972
|
-
chalk.green("\n✅ Quark core infrastructure updated successfully!\n"),
|
|
973
|
-
);
|
|
974
|
-
console.log(
|
|
975
|
-
chalk.cyan("Note: This updates @techstream/quark-core only.\n"),
|
|
976
|
-
);
|
|
1211
|
+
console.log(chalk.green("✅ Quark updated successfully!\n"));
|
|
977
1212
|
console.log(chalk.cyan("Next steps:"));
|
|
978
|
-
console.log(chalk.white(` 1. pnpm
|
|
979
|
-
console.log(chalk.white(` 2. pnpm lint`));
|
|
980
|
-
console.log(chalk.white(` 3. pnpm test`));
|
|
1213
|
+
console.log(chalk.white(` 1. pnpm test`));
|
|
981
1214
|
console.log(
|
|
982
|
-
chalk.white(`
|
|
1215
|
+
chalk.white(` 2. git add . && git commit -m "chore: update Quark"\n`),
|
|
983
1216
|
);
|
|
984
1217
|
} catch (error) {
|
|
985
1218
|
console.error(chalk.red(`\n✗ Update failed: ${error.message}\n`));
|