@techstream/quark-create-app 1.2.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 +38 -0
- package/package.json +34 -0
- package/src/index.js +611 -0
- package/templates/base-project/README.md +35 -0
- package/templates/base-project/apps/web/next.config.js +6 -0
- package/templates/base-project/apps/web/package.json +32 -0
- package/templates/base-project/apps/web/postcss.config.mjs +7 -0
- package/templates/base-project/apps/web/public/file.svg +1 -0
- package/templates/base-project/apps/web/public/globe.svg +1 -0
- package/templates/base-project/apps/web/public/next.svg +1 -0
- package/templates/base-project/apps/web/public/vercel.svg +1 -0
- package/templates/base-project/apps/web/public/window.svg +1 -0
- package/templates/base-project/apps/web/src/app/api/auth/[...nextauth]/route.js +4 -0
- package/templates/base-project/apps/web/src/app/api/auth/register/route.js +39 -0
- package/templates/base-project/apps/web/src/app/api/csrf/route.js +42 -0
- package/templates/base-project/apps/web/src/app/api/error-handler.js +21 -0
- package/templates/base-project/apps/web/src/app/api/health/route.js +78 -0
- package/templates/base-project/apps/web/src/app/api/posts/[id]/route.js +61 -0
- package/templates/base-project/apps/web/src/app/api/posts/route.js +34 -0
- package/templates/base-project/apps/web/src/app/api/users/[id]/route.js +54 -0
- package/templates/base-project/apps/web/src/app/api/users/route.js +36 -0
- package/templates/base-project/apps/web/src/app/favicon.ico +0 -0
- package/templates/base-project/apps/web/src/app/globals.css +26 -0
- package/templates/base-project/apps/web/src/app/layout.js +12 -0
- package/templates/base-project/apps/web/src/app/page.js +10 -0
- package/templates/base-project/apps/web/src/app/page.test.js +11 -0
- package/templates/base-project/apps/web/src/lib/auth-middleware.js +14 -0
- package/templates/base-project/apps/web/src/lib/auth.js +102 -0
- package/templates/base-project/apps/web/src/middleware.js +265 -0
- package/templates/base-project/apps/worker/package.json +28 -0
- package/templates/base-project/apps/worker/src/index.js +154 -0
- package/templates/base-project/apps/worker/src/index.test.js +19 -0
- package/templates/base-project/docker-compose.yml +40 -0
- package/templates/base-project/package.json +26 -0
- package/templates/base-project/packages/db/package.json +29 -0
- package/templates/base-project/packages/db/prisma/migrations/20260202061128_initial/migration.sql +176 -0
- package/templates/base-project/packages/db/prisma/migrations/migration_lock.toml +3 -0
- package/templates/base-project/packages/db/prisma/schema.prisma +147 -0
- package/templates/base-project/packages/db/prisma.config.ts +25 -0
- package/templates/base-project/packages/db/scripts/seed.js +47 -0
- package/templates/base-project/packages/db/src/client.js +52 -0
- package/templates/base-project/packages/db/src/generated/prisma/browser.ts +53 -0
- package/templates/base-project/packages/db/src/generated/prisma/client.ts +82 -0
- package/templates/base-project/packages/db/src/generated/prisma/commonInputTypes.ts +649 -0
- package/templates/base-project/packages/db/src/generated/prisma/enums.ts +19 -0
- package/templates/base-project/packages/db/src/generated/prisma/internal/class.ts +305 -0
- package/templates/base-project/packages/db/src/generated/prisma/internal/prismaNamespace.ts +1428 -0
- package/templates/base-project/packages/db/src/generated/prisma/internal/prismaNamespaceBrowser.ts +217 -0
- package/templates/base-project/packages/db/src/generated/prisma/models/Account.ts +2098 -0
- package/templates/base-project/packages/db/src/generated/prisma/models/AuditLog.ts +1805 -0
- package/templates/base-project/packages/db/src/generated/prisma/models/Job.ts +1737 -0
- package/templates/base-project/packages/db/src/generated/prisma/models/Post.ts +1762 -0
- package/templates/base-project/packages/db/src/generated/prisma/models/Session.ts +1738 -0
- package/templates/base-project/packages/db/src/generated/prisma/models/User.ts +2298 -0
- package/templates/base-project/packages/db/src/generated/prisma/models/VerificationToken.ts +1450 -0
- package/templates/base-project/packages/db/src/generated/prisma/models.ts +18 -0
- package/templates/base-project/packages/db/src/index.js +3 -0
- package/templates/base-project/packages/db/src/queries.js +267 -0
- package/templates/base-project/packages/db/src/queries.test.js +79 -0
- package/templates/base-project/packages/db/src/schemas.js +31 -0
- package/templates/base-project/pnpm-workspace.yaml +7 -0
- package/templates/base-project/turbo.json +25 -0
- package/templates/config/package.json +8 -0
- package/templates/config/src/index.js +21 -0
- package/templates/jobs/package.json +8 -0
- package/templates/jobs/src/definitions.js +9 -0
- package/templates/jobs/src/handlers.js +20 -0
- package/templates/jobs/src/index.js +2 -0
- package/templates/ui/package.json +11 -0
- package/templates/ui/src/button.js +19 -0
- package/templates/ui/src/card.js +14 -0
- package/templates/ui/src/index.js +3 -0
- package/templates/ui/src/input.js +11 -0
package/README.md
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# @techstream/quark-create-app CLI
|
|
2
|
+
|
|
3
|
+
Scaffold a new Quark project with sensible defaults for full-stack JavaScript development.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npx @techstream/quark-create-app@latest my-awesome-app
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
The CLI scaffolds a complete project structure with:
|
|
12
|
+
- **Next.js** web application
|
|
13
|
+
- **Prisma** database schema and migrations
|
|
14
|
+
- **BullMQ** job queues
|
|
15
|
+
- **Docker Compose** setup (PostgreSQL, Redis, Mailhog)
|
|
16
|
+
- **JavaScript** monorepo with `pnpm` workspaces
|
|
17
|
+
|
|
18
|
+
## Quick Setup
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
cd my-awesome-app
|
|
22
|
+
docker compose up -d
|
|
23
|
+
pnpm db:generate
|
|
24
|
+
pnpm db:migrate
|
|
25
|
+
pnpm dev
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Common Tasks
|
|
29
|
+
|
|
30
|
+
- **Update Quark packages**: `quark-update` or `pnpm update @techstream/quark-*`
|
|
31
|
+
- **Check for updates**: `quark-update --check`
|
|
32
|
+
- **Configure environment**: Edit `.env` file (see `.env.example`)
|
|
33
|
+
|
|
34
|
+
## Support
|
|
35
|
+
|
|
36
|
+
For issues, questions, and discussions:
|
|
37
|
+
- š [Issue Tracker](https://github.com/Bobnoddle/quark/issues)
|
|
38
|
+
- š¬ [Discussions](https://github.com/Bobnoddle/quark/discussions)
|
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@techstream/quark-create-app",
|
|
3
|
+
"version": "1.2.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"bin": {
|
|
6
|
+
"quark-create-app": "src/index.js",
|
|
7
|
+
"create-quark-app": "src/index.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"src",
|
|
11
|
+
"templates",
|
|
12
|
+
"README.md"
|
|
13
|
+
],
|
|
14
|
+
"scripts": {
|
|
15
|
+
"test": "node test-cli.js",
|
|
16
|
+
"test:e2e": "node test-e2e.js",
|
|
17
|
+
"test:integration": "node test-integration.js",
|
|
18
|
+
"test:all": "node test-all.js"
|
|
19
|
+
},
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"chalk": "^5.6.2",
|
|
22
|
+
"commander": "^12.1.0",
|
|
23
|
+
"execa": "^9.6.1",
|
|
24
|
+
"fs-extra": "^11.3.3",
|
|
25
|
+
"prompts": "^2.4.2"
|
|
26
|
+
},
|
|
27
|
+
"publishConfig": {
|
|
28
|
+
"registry": "https://registry.npmjs.org",
|
|
29
|
+
"access": "public"
|
|
30
|
+
},
|
|
31
|
+
"engines": {
|
|
32
|
+
"node": ">=24"
|
|
33
|
+
}
|
|
34
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,611 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import crypto from "node:crypto";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import chalk from "chalk";
|
|
6
|
+
import { Command } from "commander";
|
|
7
|
+
import { execa } from "execa";
|
|
8
|
+
import fs from "fs-extra";
|
|
9
|
+
import prompts from "prompts";
|
|
10
|
+
|
|
11
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
12
|
+
const templatesDir = path.join(__dirname, "../templates");
|
|
13
|
+
const pkg = await fs.readJSON(path.join(__dirname, "../package.json"));
|
|
14
|
+
|
|
15
|
+
const program = new Command();
|
|
16
|
+
|
|
17
|
+
program
|
|
18
|
+
.name("quark-create-app")
|
|
19
|
+
.description("Scaffold a new project from the Quark monorepo")
|
|
20
|
+
.version(pkg.version);
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Generate a cryptographically secure random string
|
|
24
|
+
* @param {number} length - Length of the random string (default: 32)
|
|
25
|
+
* @returns {string} Base64 encoded random string
|
|
26
|
+
*/
|
|
27
|
+
function generateSecureSecret(length = 32) {
|
|
28
|
+
return crypto
|
|
29
|
+
.randomBytes(length)
|
|
30
|
+
.toString("base64")
|
|
31
|
+
.replace(/[/+=]/g, "")
|
|
32
|
+
.substring(0, length);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Generate a secure random password
|
|
37
|
+
* @param {number} length - Length of the password (default: 24)
|
|
38
|
+
* @returns {string} Alphanumeric password
|
|
39
|
+
*/
|
|
40
|
+
function generateSecurePassword(length = 24) {
|
|
41
|
+
const chars = "ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz23456789";
|
|
42
|
+
const bytes = crypto.randomBytes(length);
|
|
43
|
+
let result = "";
|
|
44
|
+
for (let i = 0; i < length; i++) {
|
|
45
|
+
result += chars[bytes[i] % chars.length];
|
|
46
|
+
}
|
|
47
|
+
return result;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Copy a template directory to the target location, with variable substitution
|
|
52
|
+
*/
|
|
53
|
+
async function copyTemplate(templateName, targetDir, variables = {}) {
|
|
54
|
+
const templatePath = path.join(templatesDir, templateName);
|
|
55
|
+
|
|
56
|
+
if (!(await fs.pathExists(templatePath))) {
|
|
57
|
+
throw new Error(`Template not found: ${templateName}`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Copy the template
|
|
61
|
+
await fs.copy(templatePath, targetDir);
|
|
62
|
+
|
|
63
|
+
// Replace variables in package.json files
|
|
64
|
+
if (Object.keys(variables).length > 0) {
|
|
65
|
+
const packageJsonPath = path.join(targetDir, "package.json");
|
|
66
|
+
if (await fs.pathExists(packageJsonPath)) {
|
|
67
|
+
let content = await fs.readFile(packageJsonPath, "utf-8");
|
|
68
|
+
|
|
69
|
+
for (const [key, value] of Object.entries(variables)) {
|
|
70
|
+
const pattern = new RegExp(key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "g");
|
|
71
|
+
content = content.replace(pattern, value);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
await fs.writeFile(packageJsonPath, content);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Initialize a git repository and create an initial commit
|
|
81
|
+
*/
|
|
82
|
+
async function initializeGit(projectDir) {
|
|
83
|
+
try {
|
|
84
|
+
// Initialize git repo
|
|
85
|
+
await execa("git", ["init"], { cwd: projectDir });
|
|
86
|
+
|
|
87
|
+
// Add all files
|
|
88
|
+
await execa("git", ["add", "."], { cwd: projectDir });
|
|
89
|
+
|
|
90
|
+
// Create initial commit
|
|
91
|
+
await execa(
|
|
92
|
+
"git",
|
|
93
|
+
["commit", "-m", "Initial commit: Quark project scaffold"],
|
|
94
|
+
{
|
|
95
|
+
cwd: projectDir,
|
|
96
|
+
},
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
return true;
|
|
100
|
+
} catch (error) {
|
|
101
|
+
console.warn(
|
|
102
|
+
chalk.yellow(`ā ļø Git initialization failed: ${error.message}`),
|
|
103
|
+
);
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Update package.json name with scope
|
|
110
|
+
*/
|
|
111
|
+
async function updatePackageJsonName(filePath, scope) {
|
|
112
|
+
const content = await fs.readFile(filePath, "utf-8");
|
|
113
|
+
const packageJson = JSON.parse(content);
|
|
114
|
+
|
|
115
|
+
// Extract package name (e.g., "@myquark/ui" -> "ui")
|
|
116
|
+
const parts = packageJson.name.split("/");
|
|
117
|
+
const packageName = parts[parts.length - 1];
|
|
118
|
+
|
|
119
|
+
packageJson.name = `@${scope}/${packageName}`;
|
|
120
|
+
|
|
121
|
+
await fs.writeFile(filePath, `${JSON.stringify(packageJson, null, 2)}\n`);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Replace @techstream/quark-* workspace deps with @scope/* for local packages.
|
|
126
|
+
* Also removes deps for packages that were not selected.
|
|
127
|
+
*/
|
|
128
|
+
function replaceDepsScope(deps, scope, selectedPackages) {
|
|
129
|
+
if (!deps) return;
|
|
130
|
+
for (const [key, value] of Object.entries(deps)) {
|
|
131
|
+
if (key.startsWith("@techstream/quark-") && value === "workspace:*") {
|
|
132
|
+
const packageName = key.replace("@techstream/quark-", "");
|
|
133
|
+
delete deps[key];
|
|
134
|
+
// Only keep the dep if the package was selected (or is "db" which is always required)
|
|
135
|
+
if (packageName === "db" || selectedPackages.includes(packageName)) {
|
|
136
|
+
deps[`@${scope}/${packageName}`] = value;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Replace @techstream/quark-* import paths in all .js source files
|
|
144
|
+
* for workspace packages (db, jobs, ui, config) with @scope/* equivalents.
|
|
145
|
+
* Registry packages (@techstream/quark-core) are left untouched.
|
|
146
|
+
*/
|
|
147
|
+
async function replaceImportsInSourceFiles(dir, scope) {
|
|
148
|
+
const workspacePackages = ["db", "jobs", "ui", "config"];
|
|
149
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
150
|
+
|
|
151
|
+
for (const entry of entries) {
|
|
152
|
+
const fullPath = path.join(dir, entry.name);
|
|
153
|
+
|
|
154
|
+
if (entry.isDirectory() && entry.name !== "node_modules" && entry.name !== ".next") {
|
|
155
|
+
await replaceImportsInSourceFiles(fullPath, scope);
|
|
156
|
+
} else if (entry.isFile() && /\.(js|ts|jsx|tsx|mjs)$/.test(entry.name)) {
|
|
157
|
+
let content = await fs.readFile(fullPath, "utf-8");
|
|
158
|
+
let changed = false;
|
|
159
|
+
|
|
160
|
+
for (const pkg of workspacePackages) {
|
|
161
|
+
const pattern = new RegExp(`@techstream/quark-${pkg}`, "g");
|
|
162
|
+
if (pattern.test(content)) {
|
|
163
|
+
content = content.replace(pattern, `@${scope}/${pkg}`);
|
|
164
|
+
changed = true;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (changed) {
|
|
169
|
+
await fs.writeFile(fullPath, content);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Validate project name to prevent path traversal and ensure safe directory creation
|
|
177
|
+
*/
|
|
178
|
+
function validateProjectName(name) {
|
|
179
|
+
if (!name || typeof name !== "string") {
|
|
180
|
+
throw new Error("Project name is required");
|
|
181
|
+
}
|
|
182
|
+
// Only allow safe characters: alphanumeric, hyphens, underscores, dots
|
|
183
|
+
if (!/^[a-zA-Z0-9._-]+$/.test(name)) {
|
|
184
|
+
throw new Error(
|
|
185
|
+
"Project name may only contain letters, numbers, hyphens, underscores, and dots",
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
// Block path traversal patterns
|
|
189
|
+
if (name.startsWith(".") || name.includes("..")) {
|
|
190
|
+
throw new Error("Project name must not start with '.' or contain '..'");
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const resolved = path.resolve(process.cwd(), name);
|
|
194
|
+
if (!resolved.startsWith(process.cwd())) {
|
|
195
|
+
throw new Error("Project name must not escape the current directory");
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return resolved;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
program
|
|
202
|
+
.argument("<project-name>", "Name of the project to create")
|
|
203
|
+
.action(async (projectName) => {
|
|
204
|
+
console.log(
|
|
205
|
+
chalk.blue.bold(`\n\uD83D\uDE80 Creating your new Quark project: ${projectName}\n`),
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
const targetDir = validateProjectName(projectName);
|
|
209
|
+
const scope = projectName.toLowerCase().replace(/[^a-z0-9-]/g, "");
|
|
210
|
+
|
|
211
|
+
// Check if directory already exists
|
|
212
|
+
if (await fs.pathExists(targetDir)) {
|
|
213
|
+
console.error(chalk.red(`ā Directory already exists: ${targetDir}`));
|
|
214
|
+
process.exit(1);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
try {
|
|
218
|
+
// Create the base directory
|
|
219
|
+
await fs.ensureDir(targetDir);
|
|
220
|
+
|
|
221
|
+
// Step 1: Copy base project template
|
|
222
|
+
console.log(chalk.cyan(" š¦ Scaffolding base project structure..."));
|
|
223
|
+
await copyTemplate("base-project", targetDir, {
|
|
224
|
+
"@myquark": `@${scope}`,
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
// Step 2: Create apps directory
|
|
228
|
+
await fs.ensureDir(path.join(targetDir, "apps"));
|
|
229
|
+
|
|
230
|
+
// Step 3: Create packages directory
|
|
231
|
+
await fs.ensureDir(path.join(targetDir, "packages"));
|
|
232
|
+
|
|
233
|
+
// Step 4: Copy required packages (always included)
|
|
234
|
+
console.log(chalk.cyan("\n š¦ Setting up required packages..."));
|
|
235
|
+
|
|
236
|
+
// Database package is always required (already copied from base-project)
|
|
237
|
+
// Just update its package.json name
|
|
238
|
+
const dbPackageDir = path.join(targetDir, "packages", "db");
|
|
239
|
+
const dbPackageJsonPath = path.join(dbPackageDir, "package.json");
|
|
240
|
+
const dbPackageJson = await fs.readJSON(dbPackageJsonPath);
|
|
241
|
+
dbPackageJson.name = `@${scope}/db`;
|
|
242
|
+
await fs.writeFile(
|
|
243
|
+
dbPackageJsonPath,
|
|
244
|
+
`${JSON.stringify(dbPackageJson, null, 2)}\n`,
|
|
245
|
+
);
|
|
246
|
+
console.log(chalk.green(` ā db (required)`));
|
|
247
|
+
|
|
248
|
+
// Step 5: Ask which optional features to eject
|
|
249
|
+
console.log(chalk.cyan("\n šÆ Configuring optional features..."));
|
|
250
|
+
const response = await prompts([
|
|
251
|
+
{
|
|
252
|
+
type: "multiselect",
|
|
253
|
+
name: "features",
|
|
254
|
+
message: "Which optional packages would you like to include?",
|
|
255
|
+
choices: [
|
|
256
|
+
{
|
|
257
|
+
title: "UI Components (packages/ui)",
|
|
258
|
+
value: "ui",
|
|
259
|
+
selected: true,
|
|
260
|
+
},
|
|
261
|
+
{
|
|
262
|
+
title: "Job Definitions (packages/jobs)",
|
|
263
|
+
value: "jobs",
|
|
264
|
+
selected: true,
|
|
265
|
+
},
|
|
266
|
+
{
|
|
267
|
+
title: "Configuration (packages/config)",
|
|
268
|
+
value: "config",
|
|
269
|
+
selected: false,
|
|
270
|
+
},
|
|
271
|
+
],
|
|
272
|
+
},
|
|
273
|
+
]);
|
|
274
|
+
|
|
275
|
+
const { features } = response;
|
|
276
|
+
|
|
277
|
+
// Handle prompt cancellation (Ctrl+C)
|
|
278
|
+
if (!features) {
|
|
279
|
+
console.log(chalk.yellow("\n\u26A0\uFE0F Setup cancelled."));
|
|
280
|
+
await fs.remove(targetDir);
|
|
281
|
+
process.exit(0);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Step 6: Copy selected optional packages
|
|
285
|
+
if (features.length > 0) {
|
|
286
|
+
console.log(chalk.cyan("\n š Setting up optional packages..."));
|
|
287
|
+
|
|
288
|
+
for (const feature of features) {
|
|
289
|
+
const packageDir = path.join(targetDir, "packages", feature);
|
|
290
|
+
await fs.ensureDir(packageDir);
|
|
291
|
+
await copyTemplate(feature, packageDir);
|
|
292
|
+
|
|
293
|
+
// Update package.json with proper scope
|
|
294
|
+
const packageJsonPath = path.join(packageDir, "package.json");
|
|
295
|
+
await updatePackageJsonName(packageJsonPath, scope);
|
|
296
|
+
|
|
297
|
+
console.log(chalk.green(` ā ${feature}`));
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Step 7: Update app dependencies to use correct scope
|
|
302
|
+
console.log(chalk.cyan("\n š§ Updating app dependencies..."));
|
|
303
|
+
|
|
304
|
+
// Update app package.json files to use correct scope
|
|
305
|
+
const appPaths = [
|
|
306
|
+
path.join(targetDir, "apps", "web", "package.json"),
|
|
307
|
+
path.join(targetDir, "apps", "worker", "package.json"),
|
|
308
|
+
];
|
|
309
|
+
|
|
310
|
+
for (const appPkgPath of appPaths) {
|
|
311
|
+
if (await fs.pathExists(appPkgPath)) {
|
|
312
|
+
const appPkg = await fs.readJSON(appPkgPath);
|
|
313
|
+
replaceDepsScope(appPkg.dependencies, scope, features);
|
|
314
|
+
replaceDepsScope(appPkg.devDependencies, scope, features);
|
|
315
|
+
await fs.writeFile(
|
|
316
|
+
appPkgPath,
|
|
317
|
+
`${JSON.stringify(appPkg, null, 2)}\n`,
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
console.log(chalk.green(` ā App dependencies updated`));
|
|
323
|
+
|
|
324
|
+
// Step 7b: Replace workspace package imports in source files
|
|
325
|
+
console.log(chalk.cyan("\n š Updating import paths..."));
|
|
326
|
+
await replaceImportsInSourceFiles(targetDir, scope);
|
|
327
|
+
console.log(chalk.green(` ā Import paths updated`));
|
|
328
|
+
|
|
329
|
+
// Step 8: Create .env.example file
|
|
330
|
+
console.log(chalk.cyan("\n š Creating environment configuration..."));
|
|
331
|
+
const envExampleTemplate = `# --- Database Configuration ---
|
|
332
|
+
# These map to the service names in docker-compose.yml
|
|
333
|
+
# ā ļø SECURITY WARNING: Change these default passwords in production!
|
|
334
|
+
# Generate strong passwords with: openssl rand -base64 32
|
|
335
|
+
POSTGRES_HOST=localhost
|
|
336
|
+
POSTGRES_PORT=5432
|
|
337
|
+
POSTGRES_USER=quark_user
|
|
338
|
+
POSTGRES_PASSWORD=CHANGE_ME_TO_STRONG_PASSWORD
|
|
339
|
+
POSTGRES_DB=${scope}_dev
|
|
340
|
+
# Optional: Set DATABASE_URL to override the dynamic construction above
|
|
341
|
+
# DATABASE_URL="postgresql://quark_user:CHANGE_ME_TO_STRONG_PASSWORD@localhost:5432/${scope}_dev?schema=public"
|
|
342
|
+
|
|
343
|
+
# --- Redis Configuration ---
|
|
344
|
+
REDIS_HOST=localhost
|
|
345
|
+
REDIS_PORT=6379
|
|
346
|
+
# Optional: Set REDIS_URL to override the dynamic construction above
|
|
347
|
+
# REDIS_URL="redis://localhost:6379"
|
|
348
|
+
|
|
349
|
+
# --- Mailhog Configuration ---
|
|
350
|
+
MAILHOG_HOST=localhost
|
|
351
|
+
MAILHOG_SMTP_PORT=1025
|
|
352
|
+
MAILHOG_UI_PORT=8025
|
|
353
|
+
# Optional: Set MAILHOG_SMTP_URL to override the dynamic construction above
|
|
354
|
+
# MAILHOG_SMTP_URL="smtp://localhost:1025"
|
|
355
|
+
|
|
356
|
+
# --- Application URL ---
|
|
357
|
+
# The canonical URL of your application.
|
|
358
|
+
# NEXTAUTH_URL, CORS origins, and other URL-dependent settings are derived from this.
|
|
359
|
+
# Development: http://localhost:3000
|
|
360
|
+
# Production: https://yourdomain.com
|
|
361
|
+
APP_URL=http://localhost:3000
|
|
362
|
+
|
|
363
|
+
# --- NextAuth Configuration ---
|
|
364
|
+
# ā ļø CRITICAL: Generate a secure secret with: openssl rand -base64 32
|
|
365
|
+
# This secret is used to encrypt JWT tokens and session data
|
|
366
|
+
NEXTAUTH_SECRET=CHANGE_ME_TO_STRONG_SECRET
|
|
367
|
+
|
|
368
|
+
# --- OAuth Providers (Optional) ---
|
|
369
|
+
# GitHub OAuth - Get credentials at: https://github.com/settings/developers
|
|
370
|
+
# GITHUB_ID=your_github_client_id
|
|
371
|
+
# GITHUB_SECRET=your_github_client_secret
|
|
372
|
+
|
|
373
|
+
# Google OAuth - Get credentials at: https://console.cloud.google.com/apis/credentials
|
|
374
|
+
# GOOGLE_CLIENT_ID=your_google_client_id
|
|
375
|
+
# GOOGLE_CLIENT_SECRET=your_google_client_secret
|
|
376
|
+
|
|
377
|
+
# --- Web App Configuration ---
|
|
378
|
+
WEB_PORT=3000
|
|
379
|
+
|
|
380
|
+
# --- Worker Configuration ---
|
|
381
|
+
WORKER_CONCURRENCY=5
|
|
382
|
+
`;
|
|
383
|
+
await fs.writeFile(
|
|
384
|
+
path.join(targetDir, ".env.example"),
|
|
385
|
+
envExampleTemplate,
|
|
386
|
+
);
|
|
387
|
+
console.log(chalk.green(` ā .env.example`));
|
|
388
|
+
|
|
389
|
+
// Step 9: Generate .env with secure defaults
|
|
390
|
+
console.log(chalk.cyan("\n š Generating secure environment file..."));
|
|
391
|
+
|
|
392
|
+
// Generate secure random values
|
|
393
|
+
const dbPassword = generateSecurePassword(24);
|
|
394
|
+
const nextAuthSecret = generateSecureSecret(32);
|
|
395
|
+
|
|
396
|
+
// Create .env with auto-generated secure values
|
|
397
|
+
const envContent = `# --- Database Configuration ---
|
|
398
|
+
POSTGRES_HOST=localhost
|
|
399
|
+
POSTGRES_PORT=5432
|
|
400
|
+
POSTGRES_USER=quark_user
|
|
401
|
+
POSTGRES_PASSWORD=${dbPassword}
|
|
402
|
+
POSTGRES_DB=${scope}_dev
|
|
403
|
+
|
|
404
|
+
# --- Redis Configuration ---
|
|
405
|
+
REDIS_HOST=localhost
|
|
406
|
+
REDIS_PORT=6379
|
|
407
|
+
|
|
408
|
+
# --- Mailhog Configuration ---
|
|
409
|
+
MAILHOG_HOST=localhost
|
|
410
|
+
MAILHOG_SMTP_PORT=1025
|
|
411
|
+
MAILHOG_UI_PORT=8025
|
|
412
|
+
|
|
413
|
+
# --- NextAuth Configuration ---
|
|
414
|
+
NEXTAUTH_SECRET=${nextAuthSecret}
|
|
415
|
+
|
|
416
|
+
# --- Application URL ---
|
|
417
|
+
APP_URL=http://localhost:3000
|
|
418
|
+
|
|
419
|
+
# --- Web App Configuration ---
|
|
420
|
+
WEB_PORT=3000
|
|
421
|
+
|
|
422
|
+
# --- Worker Configuration ---
|
|
423
|
+
WORKER_CONCURRENCY=5
|
|
424
|
+
`;
|
|
425
|
+
await fs.writeFile(path.join(targetDir, ".env"), envContent);
|
|
426
|
+
console.log(
|
|
427
|
+
chalk.green(` ā .env (with auto-generated secure secrets)`),
|
|
428
|
+
);
|
|
429
|
+
|
|
430
|
+
// Step 11: Create .quark-link.json to track Quark version
|
|
431
|
+
const quarkLinkJson = {
|
|
432
|
+
quarkVersion: process.env.QUARK_VERSION || "latest",
|
|
433
|
+
quarkSourcePath: process.env.QUARK_SOURCE_PATH || "../../quark",
|
|
434
|
+
scaffoldedDate: new Date().toISOString(),
|
|
435
|
+
requiredPackages: ["db"],
|
|
436
|
+
packages: features,
|
|
437
|
+
};
|
|
438
|
+
await fs.writeFile(
|
|
439
|
+
path.join(targetDir, ".quark-link.json"),
|
|
440
|
+
JSON.stringify(quarkLinkJson, null, 2),
|
|
441
|
+
);
|
|
442
|
+
console.log(chalk.green(` ā .quark-link.json`));
|
|
443
|
+
|
|
444
|
+
// Step 11: Initialize git repository
|
|
445
|
+
console.log(chalk.cyan("\n š Initializing git repository..."));
|
|
446
|
+
const gitInitialized = await initializeGit(targetDir);
|
|
447
|
+
if (gitInitialized) {
|
|
448
|
+
console.log(chalk.green(` ā Git initialized with initial commit`));
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// Step 12: Run pnpm install
|
|
452
|
+
console.log(chalk.cyan("\n š¦ Installing dependencies..."));
|
|
453
|
+
try {
|
|
454
|
+
await execa("pnpm", ["install"], {
|
|
455
|
+
cwd: targetDir,
|
|
456
|
+
stdio: "inherit",
|
|
457
|
+
});
|
|
458
|
+
console.log(chalk.green(`\n ā Dependencies installed`));
|
|
459
|
+
} catch (installError) {
|
|
460
|
+
console.warn(
|
|
461
|
+
chalk.yellow(
|
|
462
|
+
`\n ā ļø pnpm install failed: ${installError.message}`,
|
|
463
|
+
),
|
|
464
|
+
);
|
|
465
|
+
console.warn(
|
|
466
|
+
chalk.yellow(
|
|
467
|
+
` Run 'pnpm install' manually after resolving the issue.`,
|
|
468
|
+
),
|
|
469
|
+
);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// Success message
|
|
473
|
+
console.log(
|
|
474
|
+
chalk.green.bold(
|
|
475
|
+
`\nā
Project "${projectName}" created successfully!\n`,
|
|
476
|
+
),
|
|
477
|
+
);
|
|
478
|
+
|
|
479
|
+
console.log(chalk.white(`š Project location: ${targetDir}\n`));
|
|
480
|
+
console.log(chalk.cyan("Next steps:"));
|
|
481
|
+
console.log(chalk.white(` 1. cd ${projectName}`));
|
|
482
|
+
console.log(chalk.white(` 2. docker compose up -d`));
|
|
483
|
+
console.log(chalk.white(` 3. pnpm dev\n`));
|
|
484
|
+
|
|
485
|
+
console.log(chalk.cyan("Important:"));
|
|
486
|
+
console.log(
|
|
487
|
+
chalk.white(
|
|
488
|
+
` ⢠Update Quark core with: pnpm update @techstream/quark-core`,
|
|
489
|
+
),
|
|
490
|
+
);
|
|
491
|
+
console.log(
|
|
492
|
+
chalk.white(` ⢠Use 'quark-update' to upgrade Quark packages\n`),
|
|
493
|
+
);
|
|
494
|
+
|
|
495
|
+
console.log(chalk.cyan("Learn more:"));
|
|
496
|
+
console.log(chalk.white(` š Docs: https://github.com/Bobnoddle/quark`));
|
|
497
|
+
console.log(
|
|
498
|
+
chalk.white(` š¬ Issues: https://github.com/Bobnoddle/quark/issues\n`),
|
|
499
|
+
);
|
|
500
|
+
} catch (error) {
|
|
501
|
+
console.error(chalk.red(`\nā Error creating project: ${error.message}`));
|
|
502
|
+
console.error(chalk.dim(error.stack));
|
|
503
|
+
|
|
504
|
+
// Clean up on error
|
|
505
|
+
if (await fs.pathExists(targetDir)) {
|
|
506
|
+
await fs.remove(targetDir);
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
process.exit(1);
|
|
510
|
+
}
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
/**
|
|
514
|
+
* quark-update command
|
|
515
|
+
* Updates Quark core infrastructure in a scaffolded project
|
|
516
|
+
*/
|
|
517
|
+
program
|
|
518
|
+
.command("update")
|
|
519
|
+
.description("Update Quark core infrastructure in the current project")
|
|
520
|
+
.option("--check", "Check for updates without applying")
|
|
521
|
+
.option("--force", "Skip safety checks")
|
|
522
|
+
.action(async (options) => {
|
|
523
|
+
console.log(chalk.blue.bold(`\nš Quark Package Update\n`));
|
|
524
|
+
|
|
525
|
+
// Check if .quark-link.json exists
|
|
526
|
+
const quarkLinkPath = path.join(process.cwd(), ".quark-link.json");
|
|
527
|
+
if (!(await fs.pathExists(quarkLinkPath))) {
|
|
528
|
+
console.error(
|
|
529
|
+
chalk.red("ā .quark-link.json not found. Are you in a Quark project?"),
|
|
530
|
+
);
|
|
531
|
+
process.exit(1);
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
const quarkLink = await fs.readJSON(quarkLinkPath);
|
|
535
|
+
console.log(chalk.cyan(`Current Quark version: ${quarkLink.quarkVersion}`));
|
|
536
|
+
console.log(chalk.cyan(`Scaffolded: ${quarkLink.scaffoldedDate}\n`));
|
|
537
|
+
|
|
538
|
+
if (options.check) {
|
|
539
|
+
console.log(chalk.yellow("Checking for updates..."));
|
|
540
|
+
console.log(
|
|
541
|
+
chalk.white("Run 'pnpm update @techstream/quark-*' to apply updates."),
|
|
542
|
+
);
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
try {
|
|
547
|
+
// Warn if git has uncommitted changes
|
|
548
|
+
if (!options.force) {
|
|
549
|
+
console.log(chalk.yellow("ā ļø Checking for uncommitted changes..."));
|
|
550
|
+
try {
|
|
551
|
+
await execa("git", ["diff", "--exit-code"], {
|
|
552
|
+
cwd: process.cwd(),
|
|
553
|
+
});
|
|
554
|
+
} catch {
|
|
555
|
+
console.log(
|
|
556
|
+
chalk.yellow(
|
|
557
|
+
"ā ļø You have uncommitted changes. Commit or stash them first.",
|
|
558
|
+
),
|
|
559
|
+
);
|
|
560
|
+
console.log(
|
|
561
|
+
chalk.white("Use --force to skip this check (not recommended).\n"),
|
|
562
|
+
);
|
|
563
|
+
process.exit(1);
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
// Run pnpm update
|
|
568
|
+
console.log(chalk.cyan("\nš¦ Updating Quark core infrastructure...\n"));
|
|
569
|
+
await execa("pnpm", ["update", "@techstream/quark-core"], {
|
|
570
|
+
cwd: process.cwd(),
|
|
571
|
+
stdio: "inherit",
|
|
572
|
+
});
|
|
573
|
+
|
|
574
|
+
// Update .quark-link.json
|
|
575
|
+
let updatedVersion = "updated";
|
|
576
|
+
try {
|
|
577
|
+
const corePkg = await fs.readJSON(
|
|
578
|
+
path.join(
|
|
579
|
+
process.cwd(),
|
|
580
|
+
"node_modules",
|
|
581
|
+
"@techstream",
|
|
582
|
+
"quark-core",
|
|
583
|
+
"package.json",
|
|
584
|
+
),
|
|
585
|
+
);
|
|
586
|
+
updatedVersion = corePkg.version;
|
|
587
|
+
} catch {}
|
|
588
|
+
quarkLink.quarkVersion = updatedVersion;
|
|
589
|
+
quarkLink.updatedDate = new Date().toISOString();
|
|
590
|
+
await fs.writeFile(quarkLinkPath, JSON.stringify(quarkLink, null, 2));
|
|
591
|
+
|
|
592
|
+
console.log(
|
|
593
|
+
chalk.green("\nā
Quark core infrastructure updated successfully!\n"),
|
|
594
|
+
);
|
|
595
|
+
console.log(
|
|
596
|
+
chalk.cyan("Note: This updates @techstream/quark-core only.\n"),
|
|
597
|
+
);
|
|
598
|
+
console.log(chalk.cyan("Next steps:"));
|
|
599
|
+
console.log(chalk.white(` 1. pnpm install (if prompted)`));
|
|
600
|
+
console.log(chalk.white(` 2. pnpm lint`));
|
|
601
|
+
console.log(chalk.white(` 3. pnpm test`));
|
|
602
|
+
console.log(
|
|
603
|
+
chalk.white(` 4. git add . && git commit -m "chore: update Quark"\n`),
|
|
604
|
+
);
|
|
605
|
+
} catch (error) {
|
|
606
|
+
console.error(chalk.red(`\nā Update failed: ${error.message}\n`));
|
|
607
|
+
process.exit(1);
|
|
608
|
+
}
|
|
609
|
+
});
|
|
610
|
+
|
|
611
|
+
program.parse();
|