@kardoe/quickback 0.4.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/dist/commands/compile.d.ts +18 -0
- package/dist/commands/compile.d.ts.map +1 -0
- package/dist/commands/compile.js +144 -0
- package/dist/commands/compile.js.map +1 -0
- package/dist/commands/create.d.ts +18 -0
- package/dist/commands/create.d.ts.map +1 -0
- package/dist/commands/create.js +669 -0
- package/dist/commands/create.js.map +1 -0
- package/dist/commands/init.d.ts +6 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +383 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/login.d.ts +8 -0
- package/dist/commands/login.d.ts.map +1 -0
- package/dist/commands/login.js +190 -0
- package/dist/commands/login.js.map +1 -0
- package/dist/commands/logout.d.ts +7 -0
- package/dist/commands/logout.d.ts.map +1 -0
- package/dist/commands/logout.js +19 -0
- package/dist/commands/logout.js.map +1 -0
- package/dist/commands/whoami.d.ts +7 -0
- package/dist/commands/whoami.d.ts.map +1 -0
- package/dist/commands/whoami.js +37 -0
- package/dist/commands/whoami.js.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +140 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/api-client.d.ts +82 -0
- package/dist/lib/api-client.d.ts.map +1 -0
- package/dist/lib/api-client.js +59 -0
- package/dist/lib/api-client.js.map +1 -0
- package/dist/lib/auth.d.ts +44 -0
- package/dist/lib/auth.d.ts.map +1 -0
- package/dist/lib/auth.js +85 -0
- package/dist/lib/auth.js.map +1 -0
- package/dist/lib/compiler-stubs.d.ts +66 -0
- package/dist/lib/compiler-stubs.d.ts.map +1 -0
- package/dist/lib/compiler-stubs.js +75 -0
- package/dist/lib/compiler-stubs.js.map +1 -0
- package/dist/lib/file-loader.d.ts +73 -0
- package/dist/lib/file-loader.d.ts.map +1 -0
- package/dist/lib/file-loader.js +291 -0
- package/dist/lib/file-loader.js.map +1 -0
- package/dist/lib/file-writer.d.ts +33 -0
- package/dist/lib/file-writer.d.ts.map +1 -0
- package/dist/lib/file-writer.js +110 -0
- package/dist/lib/file-writer.js.map +1 -0
- package/dist/lib/helpers.d.ts +39 -0
- package/dist/lib/helpers.d.ts.map +1 -0
- package/dist/lib/helpers.js +299 -0
- package/dist/lib/helpers.js.map +1 -0
- package/dist/lib/shell.d.ts +15 -0
- package/dist/lib/shell.d.ts.map +1 -0
- package/dist/lib/shell.js +32 -0
- package/dist/lib/shell.js.map +1 -0
- package/dist/templates/registry.d.ts +36 -0
- package/dist/templates/registry.d.ts.map +1 -0
- package/dist/templates/registry.js +143 -0
- package/dist/templates/registry.js.map +1 -0
- package/package.json +37 -0
|
@@ -0,0 +1,669 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Create Command
|
|
3
|
+
*
|
|
4
|
+
* One-shot project creation with templates.
|
|
5
|
+
* Usage: quickback create <template> <app-name>
|
|
6
|
+
* Example: quickback create betterauth-d1-cloudflare my-app
|
|
7
|
+
*/
|
|
8
|
+
import { existsSync, mkdirSync, writeFileSync } from "fs";
|
|
9
|
+
import { join, resolve } from "path";
|
|
10
|
+
import { spawnSync } from "child_process";
|
|
11
|
+
import pc from "picocolors";
|
|
12
|
+
import ora from "ora";
|
|
13
|
+
import prompts from "prompts";
|
|
14
|
+
import { getTemplate, listTemplates, formatTemplateList, TEMPLATE_ALIASES, } from "../templates/registry.js";
|
|
15
|
+
import { callCompiler } from "../lib/api-client.js";
|
|
16
|
+
import { loadConfig, loadFeatures, findConfigPath, findFeaturesDir } from "../lib/file-loader.js";
|
|
17
|
+
import { writeFiles } from "../lib/file-writer.js";
|
|
18
|
+
// ============================================================================
|
|
19
|
+
// Main Create Function
|
|
20
|
+
// ============================================================================
|
|
21
|
+
export async function create(args) {
|
|
22
|
+
console.log(pc.bold("\nQuickback Create") + pc.gray(" · One-shot project generator\n"));
|
|
23
|
+
try {
|
|
24
|
+
// Parse arguments
|
|
25
|
+
const options = await parseCreateArgs(args);
|
|
26
|
+
if (options.dryRun) {
|
|
27
|
+
await performDryRun(options);
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
// Resolve template
|
|
31
|
+
const template = getTemplate(options.template);
|
|
32
|
+
if (!template) {
|
|
33
|
+
console.error(pc.red(`Unknown template: ${options.template}\n`));
|
|
34
|
+
console.log(formatTemplateList());
|
|
35
|
+
process.exit(1);
|
|
36
|
+
}
|
|
37
|
+
// Validate target directory
|
|
38
|
+
const targetDir = resolve(process.cwd(), options.appName);
|
|
39
|
+
if (existsSync(targetDir)) {
|
|
40
|
+
console.error(pc.red(`Directory already exists: ${options.appName}`));
|
|
41
|
+
console.log(pc.gray("Choose a different name or delete the existing directory."));
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
44
|
+
console.log(pc.cyan(`Creating ${template.name}...`));
|
|
45
|
+
console.log(pc.gray(`Template: ${options.template}`));
|
|
46
|
+
console.log(pc.gray(`Location: ${targetDir}\n`));
|
|
47
|
+
// Step 1: Create project directory
|
|
48
|
+
const spinner = ora("Creating project structure...").start();
|
|
49
|
+
mkdirSync(targetDir, { recursive: true });
|
|
50
|
+
mkdirSync(join(targetDir, "quickback"), { recursive: true });
|
|
51
|
+
mkdirSync(join(targetDir, "quickback", "definitions", "features", "todos"), { recursive: true });
|
|
52
|
+
spinner.succeed("Project structure created");
|
|
53
|
+
// Step 2: Generate config file
|
|
54
|
+
spinner.start("Generating configuration...");
|
|
55
|
+
generateConfigFile(targetDir, options.appName, template);
|
|
56
|
+
generateTodoFeature(targetDir);
|
|
57
|
+
generateAuthConfig(targetDir);
|
|
58
|
+
spinner.succeed("Configuration generated");
|
|
59
|
+
// Step 3: Setup Cloudflare resources (if applicable)
|
|
60
|
+
if (template.requiresCloudflare && !options.skipCloudflareSetup) {
|
|
61
|
+
await setupCloudflareResources(options.appName, targetDir, options.verbose);
|
|
62
|
+
}
|
|
63
|
+
// Step 4: Compile the project via remote API
|
|
64
|
+
spinner.start("Compiling via Quickback API...");
|
|
65
|
+
const quickbackDir = join(targetDir, "quickback");
|
|
66
|
+
const configPath = findConfigPath(quickbackDir);
|
|
67
|
+
if (!configPath) {
|
|
68
|
+
spinner.fail("Configuration not found");
|
|
69
|
+
throw new Error("Could not find quickback.config.ts");
|
|
70
|
+
}
|
|
71
|
+
const config = await loadConfig(configPath);
|
|
72
|
+
const featuresDir = findFeaturesDir(quickbackDir);
|
|
73
|
+
const features = featuresDir ? await loadFeatures(featuresDir) : [];
|
|
74
|
+
const result = await callCompiler({
|
|
75
|
+
config: {
|
|
76
|
+
name: config.name,
|
|
77
|
+
preset: config.preset,
|
|
78
|
+
template: config.template,
|
|
79
|
+
providers: config.providers,
|
|
80
|
+
build: config.build,
|
|
81
|
+
},
|
|
82
|
+
features: features.map((f) => ({
|
|
83
|
+
name: f.name,
|
|
84
|
+
schema: f.schema,
|
|
85
|
+
resource: f.resource,
|
|
86
|
+
actions: f.actions,
|
|
87
|
+
})),
|
|
88
|
+
});
|
|
89
|
+
// Write output files to project root
|
|
90
|
+
writeFiles(targetDir, result.files);
|
|
91
|
+
spinner.succeed(`Compiled ${result.meta.fileCount} files (v${result.meta.version})`);
|
|
92
|
+
// Step 5: Install dependencies
|
|
93
|
+
if (!options.skipInstall) {
|
|
94
|
+
spinner.start("Installing dependencies...");
|
|
95
|
+
// Prefer bun for bun templates (better peer dep handling)
|
|
96
|
+
const preferBun = template.preset === 'bun' || !template.requiresCloudflare;
|
|
97
|
+
const pm = detectPackageManager(preferBun);
|
|
98
|
+
const installResult = runCommand(pm, ["install"], targetDir, options.verbose);
|
|
99
|
+
if (installResult.success) {
|
|
100
|
+
spinner.succeed(`Dependencies installed with ${pm}`);
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
spinner.warn("Dependency installation had issues");
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
// Step 6: Generate database schemas (drizzle-kit generate)
|
|
107
|
+
spinner.start("Generating database schemas...");
|
|
108
|
+
const schemaResult = runCommand("npx", ["drizzle-kit", "generate"], targetDir, options.verbose);
|
|
109
|
+
if (schemaResult.success) {
|
|
110
|
+
spinner.succeed("Database schemas generated");
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
spinner.warn("Schema generation skipped (install dependencies first)");
|
|
114
|
+
}
|
|
115
|
+
// Step 7: Apply local migrations (for non-Cloudflare)
|
|
116
|
+
if (!template.requiresCloudflare && !options.skipMigrations) {
|
|
117
|
+
// Ensure data directory exists for SQLite
|
|
118
|
+
const dataDir = join(targetDir, "data");
|
|
119
|
+
if (!existsSync(dataDir)) {
|
|
120
|
+
mkdirSync(dataDir, { recursive: true });
|
|
121
|
+
}
|
|
122
|
+
spinner.start("Applying local migrations...");
|
|
123
|
+
const migrateResult = runCommand("npx", ["drizzle-kit", "migrate"], targetDir, options.verbose);
|
|
124
|
+
if (migrateResult.success) {
|
|
125
|
+
spinner.succeed("Local migrations applied");
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
spinner.warn("Migrations skipped");
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
// Success!
|
|
132
|
+
printSuccessMessage(options.appName, template);
|
|
133
|
+
}
|
|
134
|
+
catch (error) {
|
|
135
|
+
console.error(pc.red("\nProject creation failed:"));
|
|
136
|
+
console.error(pc.red(String(error)));
|
|
137
|
+
process.exit(1);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
// ============================================================================
|
|
141
|
+
// Argument Parsing
|
|
142
|
+
// ============================================================================
|
|
143
|
+
async function parseCreateArgs(args) {
|
|
144
|
+
let template;
|
|
145
|
+
let appName;
|
|
146
|
+
let skipCloudflareSetup = false;
|
|
147
|
+
let skipInstall = false;
|
|
148
|
+
let skipMigrations = false;
|
|
149
|
+
let dryRun = false;
|
|
150
|
+
let verbose = false;
|
|
151
|
+
// Parse flags and positional args
|
|
152
|
+
const positional = [];
|
|
153
|
+
for (const arg of args) {
|
|
154
|
+
if (arg === "--skip-cloudflare-setup" || arg === "--skip-cf") {
|
|
155
|
+
skipCloudflareSetup = true;
|
|
156
|
+
}
|
|
157
|
+
else if (arg === "--skip-install") {
|
|
158
|
+
skipInstall = true;
|
|
159
|
+
}
|
|
160
|
+
else if (arg === "--skip-migrations") {
|
|
161
|
+
skipMigrations = true;
|
|
162
|
+
}
|
|
163
|
+
else if (arg === "--dry-run") {
|
|
164
|
+
dryRun = true;
|
|
165
|
+
}
|
|
166
|
+
else if (arg === "--verbose" || arg === "-v") {
|
|
167
|
+
verbose = true;
|
|
168
|
+
}
|
|
169
|
+
else if (arg === "--help" || arg === "-h") {
|
|
170
|
+
printHelp();
|
|
171
|
+
process.exit(0);
|
|
172
|
+
}
|
|
173
|
+
else if (arg === "--list" || arg === "-l") {
|
|
174
|
+
console.log(formatTemplateList());
|
|
175
|
+
process.exit(0);
|
|
176
|
+
}
|
|
177
|
+
else if (!arg.startsWith("-")) {
|
|
178
|
+
positional.push(arg);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
// Extract template and appName from positional args
|
|
182
|
+
if (positional.length >= 2) {
|
|
183
|
+
template = positional[0];
|
|
184
|
+
appName = positional[1];
|
|
185
|
+
}
|
|
186
|
+
else if (positional.length === 1) {
|
|
187
|
+
// Could be template or app name - check if it's a known template
|
|
188
|
+
if (getTemplate(positional[0]) || TEMPLATE_ALIASES[positional[0]]) {
|
|
189
|
+
template = positional[0];
|
|
190
|
+
}
|
|
191
|
+
else {
|
|
192
|
+
appName = positional[0];
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
// Interactive prompts if needed
|
|
196
|
+
if (!template) {
|
|
197
|
+
const templates = listTemplates();
|
|
198
|
+
const response = await prompts({
|
|
199
|
+
type: "select",
|
|
200
|
+
name: "template",
|
|
201
|
+
message: "Choose a template",
|
|
202
|
+
choices: templates.map(t => ({
|
|
203
|
+
title: t.name,
|
|
204
|
+
description: t.description,
|
|
205
|
+
value: Object.entries(TEMPLATE_ALIASES).find(([_, v]) => v === Object.keys(TEMPLATE_ALIASES).find(k => getTemplate(k)?.name === t.name))?.[0] || t.name.toLowerCase().replace(/\s+/g, "-"),
|
|
206
|
+
})),
|
|
207
|
+
});
|
|
208
|
+
template = response.template;
|
|
209
|
+
if (!template) {
|
|
210
|
+
console.log(pc.yellow("\nOperation cancelled."));
|
|
211
|
+
process.exit(0);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
if (!appName) {
|
|
215
|
+
const response = await prompts({
|
|
216
|
+
type: "text",
|
|
217
|
+
name: "appName",
|
|
218
|
+
message: "Project name",
|
|
219
|
+
validate: (v) => {
|
|
220
|
+
if (!v || v.trim().length === 0)
|
|
221
|
+
return "Name is required";
|
|
222
|
+
if (!/^[a-z0-9][a-z0-9-]*[a-z0-9]$|^[a-z0-9]$/.test(v)) {
|
|
223
|
+
return "Use lowercase letters, numbers, and hyphens only";
|
|
224
|
+
}
|
|
225
|
+
return true;
|
|
226
|
+
},
|
|
227
|
+
});
|
|
228
|
+
appName = response.appName;
|
|
229
|
+
if (!appName) {
|
|
230
|
+
console.log(pc.yellow("\nOperation cancelled."));
|
|
231
|
+
process.exit(0);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
return {
|
|
235
|
+
template: template,
|
|
236
|
+
appName: appName,
|
|
237
|
+
skipCloudflareSetup,
|
|
238
|
+
skipInstall,
|
|
239
|
+
skipMigrations,
|
|
240
|
+
dryRun,
|
|
241
|
+
verbose,
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
// ============================================================================
|
|
245
|
+
// File Generation
|
|
246
|
+
// ============================================================================
|
|
247
|
+
function generateConfigFile(targetDir, appName, template) {
|
|
248
|
+
// Generate providers based on template preset
|
|
249
|
+
const providers = getProvidersForPreset(template.preset);
|
|
250
|
+
const configContent = `/**
|
|
251
|
+
* Quickback Configuration
|
|
252
|
+
* Generated by: quickback create ${template.preset} ${appName}
|
|
253
|
+
*
|
|
254
|
+
* Project structure:
|
|
255
|
+
* ${appName}/
|
|
256
|
+
* ├── quickback/ ← definitions live here
|
|
257
|
+
* │ ├── quickback.config.ts
|
|
258
|
+
* │ └── definitions/features/...
|
|
259
|
+
* ├── src/ ← compiled code
|
|
260
|
+
* ├── drizzle/ ← migrations (or supabase/migrations/)
|
|
261
|
+
* └── package.json
|
|
262
|
+
*
|
|
263
|
+
* Run 'quickback compile' to regenerate after editing definitions.
|
|
264
|
+
*/
|
|
265
|
+
|
|
266
|
+
export default {
|
|
267
|
+
name: "${appName}",
|
|
268
|
+
template: "hono",
|
|
269
|
+
providers: ${JSON.stringify(providers, null, 8).replace(/^/gm, ' ').trim()},
|
|
270
|
+
};
|
|
271
|
+
`;
|
|
272
|
+
writeFileSync(join(targetDir, "quickback", "quickback.config.ts"), configContent);
|
|
273
|
+
}
|
|
274
|
+
function getProvidersForPreset(preset) {
|
|
275
|
+
switch (preset) {
|
|
276
|
+
case "bun":
|
|
277
|
+
return {
|
|
278
|
+
runtime: { name: "bun", config: {} },
|
|
279
|
+
database: { name: "bun-sqlite", config: { path: "./data/app.db" } },
|
|
280
|
+
auth: { name: "better-auth", config: {} },
|
|
281
|
+
};
|
|
282
|
+
case "cloudflare":
|
|
283
|
+
return {
|
|
284
|
+
runtime: { name: "cloudflare", config: {} },
|
|
285
|
+
database: { name: "cloudflare-d1", config: { binding: "DB" } },
|
|
286
|
+
auth: { name: "better-auth", config: {} },
|
|
287
|
+
};
|
|
288
|
+
case "turso":
|
|
289
|
+
return {
|
|
290
|
+
runtime: { name: "bun", config: {} },
|
|
291
|
+
database: { name: "libsql", config: {} },
|
|
292
|
+
auth: { name: "better-auth", config: {} },
|
|
293
|
+
};
|
|
294
|
+
default:
|
|
295
|
+
return {
|
|
296
|
+
runtime: { name: "bun", config: {} },
|
|
297
|
+
database: { name: "bun-sqlite", config: {} },
|
|
298
|
+
auth: { name: "better-auth", config: {} },
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
function generateTodoFeature(targetDir) {
|
|
303
|
+
// Schema with all fields required for security pillars
|
|
304
|
+
const schemaContent = `/**
|
|
305
|
+
* Todo Feature Schema
|
|
306
|
+
* Demonstrates all 4 Security Pillars: Firewall, Guards, Safety, Masking
|
|
307
|
+
*/
|
|
308
|
+
import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core";
|
|
309
|
+
|
|
310
|
+
export const todos = sqliteTable("todos", {
|
|
311
|
+
// Primary key
|
|
312
|
+
id: text("id").primaryKey().notNull(),
|
|
313
|
+
|
|
314
|
+
// Business fields
|
|
315
|
+
title: text("title").notNull(),
|
|
316
|
+
description: text("description"),
|
|
317
|
+
completed: integer("completed", { mode: "boolean" }).default(false),
|
|
318
|
+
priority: text("priority").default("medium"), // low, medium, high
|
|
319
|
+
dueDate: integer("due_date", { mode: "timestamp" }),
|
|
320
|
+
|
|
321
|
+
// Ownership (for Firewall)
|
|
322
|
+
organizationId: text("organization_id").notNull(),
|
|
323
|
+
ownerId: text("owner_id").notNull(),
|
|
324
|
+
|
|
325
|
+
// Audit trail (for Safety - immutable fields)
|
|
326
|
+
createdAt: integer("created_at", { mode: "timestamp" }).notNull(),
|
|
327
|
+
createdBy: text("created_by").notNull(),
|
|
328
|
+
modifiedAt: integer("modified_at", { mode: "timestamp" }).notNull(),
|
|
329
|
+
modifiedBy: text("modified_by").notNull(),
|
|
330
|
+
|
|
331
|
+
// Soft delete (for Firewall)
|
|
332
|
+
deletedAt: integer("deleted_at", { mode: "timestamp" }),
|
|
333
|
+
});
|
|
334
|
+
`;
|
|
335
|
+
// Resource definition with all 4 security pillars
|
|
336
|
+
const resourceContent = `/**
|
|
337
|
+
* Todo Resource Definition
|
|
338
|
+
* Demonstrates all 4 Security Pillars
|
|
339
|
+
*/
|
|
340
|
+
export default {
|
|
341
|
+
name: "todos",
|
|
342
|
+
schema: "./schema",
|
|
343
|
+
|
|
344
|
+
// =========================================================================
|
|
345
|
+
// PILLAR 1: FIREWALL
|
|
346
|
+
// Controls data isolation at the query level
|
|
347
|
+
// =========================================================================
|
|
348
|
+
firewall: {
|
|
349
|
+
organization: {
|
|
350
|
+
column: "organizationId",
|
|
351
|
+
source: "ctx.activeOrgId",
|
|
352
|
+
},
|
|
353
|
+
owner: {
|
|
354
|
+
column: "ownerId",
|
|
355
|
+
source: "ctx.userId",
|
|
356
|
+
mode: "optional", // Can filter by owner when needed
|
|
357
|
+
},
|
|
358
|
+
softDelete: {
|
|
359
|
+
column: "deletedAt",
|
|
360
|
+
},
|
|
361
|
+
},
|
|
362
|
+
|
|
363
|
+
// =========================================================================
|
|
364
|
+
// PILLAR 2: GUARDS (via CRUD config)
|
|
365
|
+
// Controls who can perform which operations
|
|
366
|
+
// =========================================================================
|
|
367
|
+
crud: {
|
|
368
|
+
list: {
|
|
369
|
+
guard: { roles: ["member", "admin"] },
|
|
370
|
+
},
|
|
371
|
+
get: {
|
|
372
|
+
guard: { roles: ["member", "admin"] },
|
|
373
|
+
},
|
|
374
|
+
create: {
|
|
375
|
+
guard: { roles: ["member", "admin"] },
|
|
376
|
+
},
|
|
377
|
+
update: {
|
|
378
|
+
guard: { roles: ["member", "admin"], record: { ownerId: { equals: "$ctx.userId" } } },
|
|
379
|
+
},
|
|
380
|
+
delete: {
|
|
381
|
+
guard: { roles: ["admin"] },
|
|
382
|
+
mode: "soft",
|
|
383
|
+
},
|
|
384
|
+
},
|
|
385
|
+
|
|
386
|
+
// =========================================================================
|
|
387
|
+
// PILLAR 3: SAFETY
|
|
388
|
+
// Protects data integrity with field-level validation
|
|
389
|
+
// =========================================================================
|
|
390
|
+
safety: {
|
|
391
|
+
// Fields that can be set on create
|
|
392
|
+
createable: [
|
|
393
|
+
"title",
|
|
394
|
+
"description",
|
|
395
|
+
"completed",
|
|
396
|
+
"priority",
|
|
397
|
+
"dueDate",
|
|
398
|
+
],
|
|
399
|
+
// Fields that can be modified on update
|
|
400
|
+
updatable: [
|
|
401
|
+
"title",
|
|
402
|
+
"description",
|
|
403
|
+
"completed",
|
|
404
|
+
"priority",
|
|
405
|
+
"dueDate",
|
|
406
|
+
],
|
|
407
|
+
// Fields that can never be changed after creation
|
|
408
|
+
immutable: [
|
|
409
|
+
"id",
|
|
410
|
+
"organizationId",
|
|
411
|
+
"ownerId",
|
|
412
|
+
"createdAt",
|
|
413
|
+
"createdBy",
|
|
414
|
+
],
|
|
415
|
+
},
|
|
416
|
+
|
|
417
|
+
// =========================================================================
|
|
418
|
+
// PILLAR 4: MASKING
|
|
419
|
+
// Controls what data is visible based on context
|
|
420
|
+
// =========================================================================
|
|
421
|
+
masking: {
|
|
422
|
+
// Hide description for non-owners (privacy)
|
|
423
|
+
description: {
|
|
424
|
+
type: "redact",
|
|
425
|
+
show: {
|
|
426
|
+
roles: ["admin"],
|
|
427
|
+
or: "owner",
|
|
428
|
+
},
|
|
429
|
+
},
|
|
430
|
+
},
|
|
431
|
+
};
|
|
432
|
+
`;
|
|
433
|
+
// Actions definition for custom business logic
|
|
434
|
+
const actionsContent = `/**
|
|
435
|
+
* Todo Actions Definition
|
|
436
|
+
* Custom business logic with security guards
|
|
437
|
+
*
|
|
438
|
+
* Input schemas use JSON Schema format which gets converted to Zod at compile time.
|
|
439
|
+
*/
|
|
440
|
+
export default {
|
|
441
|
+
name: "todos",
|
|
442
|
+
schema: "./schema",
|
|
443
|
+
|
|
444
|
+
actions: {
|
|
445
|
+
// Mark a todo as complete
|
|
446
|
+
complete: {
|
|
447
|
+
description: "Mark todo as complete",
|
|
448
|
+
input: {
|
|
449
|
+
type: "object",
|
|
450
|
+
properties: {
|
|
451
|
+
completedAt: { type: "string", format: "datetime", optional: true },
|
|
452
|
+
},
|
|
453
|
+
},
|
|
454
|
+
guard: {
|
|
455
|
+
roles: ["owner", "member", "admin"],
|
|
456
|
+
record: { completed: { equals: false } },
|
|
457
|
+
},
|
|
458
|
+
sideEffects: "sync",
|
|
459
|
+
},
|
|
460
|
+
|
|
461
|
+
// Mark a todo as incomplete
|
|
462
|
+
uncomplete: {
|
|
463
|
+
description: "Mark todo as incomplete",
|
|
464
|
+
input: { type: "object", properties: {} },
|
|
465
|
+
guard: {
|
|
466
|
+
roles: ["owner", "member", "admin"],
|
|
467
|
+
record: { completed: { equals: true } },
|
|
468
|
+
},
|
|
469
|
+
sideEffects: "sync",
|
|
470
|
+
},
|
|
471
|
+
},
|
|
472
|
+
};
|
|
473
|
+
`;
|
|
474
|
+
const featureDir = join(targetDir, "quickback", "definitions", "features", "todos");
|
|
475
|
+
writeFileSync(join(featureDir, "schema.ts"), schemaContent);
|
|
476
|
+
writeFileSync(join(featureDir, "resource.ts"), resourceContent);
|
|
477
|
+
writeFileSync(join(featureDir, "actions.ts"), actionsContent);
|
|
478
|
+
}
|
|
479
|
+
function generateAuthConfig(targetDir) {
|
|
480
|
+
const authContent = `/**
|
|
481
|
+
* Better Auth Configuration
|
|
482
|
+
*/
|
|
483
|
+
export default {
|
|
484
|
+
emailAndPassword: {
|
|
485
|
+
enabled: true,
|
|
486
|
+
requireEmailVerification: false,
|
|
487
|
+
},
|
|
488
|
+
plugins: ["anonymous"],
|
|
489
|
+
session: {
|
|
490
|
+
expiresInDays: 7,
|
|
491
|
+
updateAgeInDays: 1,
|
|
492
|
+
},
|
|
493
|
+
database: {
|
|
494
|
+
usePlural: true,
|
|
495
|
+
debugLogs: true,
|
|
496
|
+
},
|
|
497
|
+
};
|
|
498
|
+
`;
|
|
499
|
+
mkdirSync(join(targetDir, "quickback", "definitions", "auth", "better-auth"), { recursive: true });
|
|
500
|
+
writeFileSync(join(targetDir, "quickback", "definitions", "auth", "better-auth", "config.ts"), authContent);
|
|
501
|
+
}
|
|
502
|
+
// ============================================================================
|
|
503
|
+
// Cloudflare Setup
|
|
504
|
+
// ============================================================================
|
|
505
|
+
async function setupCloudflareResources(appName, targetDir, verbose) {
|
|
506
|
+
const spinner = ora("Setting up Cloudflare resources...").start();
|
|
507
|
+
// Check if wrangler is available
|
|
508
|
+
const wranglerCheck = spawnSync("npx", ["wrangler", "--version"], { encoding: "utf-8" });
|
|
509
|
+
if (wranglerCheck.status !== 0) {
|
|
510
|
+
spinner.warn("Wrangler not available - skipping Cloudflare setup");
|
|
511
|
+
console.log(pc.gray(" Install wrangler and run: npx wrangler d1 create " + appName + "-db"));
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
// Check authentication
|
|
515
|
+
const whoami = spawnSync("npx", ["wrangler", "whoami"], { encoding: "utf-8" });
|
|
516
|
+
if (whoami.status !== 0 || whoami.stdout.includes("not authenticated")) {
|
|
517
|
+
spinner.warn("Not authenticated with Cloudflare - skipping resource creation");
|
|
518
|
+
console.log(pc.gray(" Run 'npx wrangler login' first, then create resources manually."));
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
// Create D1 database
|
|
522
|
+
spinner.text = `Creating D1 database: ${appName}-db`;
|
|
523
|
+
const d1Result = spawnSync("npx", ["wrangler", "d1", "create", `${appName}-db`], { encoding: "utf-8", cwd: targetDir });
|
|
524
|
+
if (d1Result.status === 0) {
|
|
525
|
+
// Extract database ID from output
|
|
526
|
+
const dbIdMatch = d1Result.stdout.match(/database_id\s*=\s*"([^"]+)"/);
|
|
527
|
+
if (dbIdMatch) {
|
|
528
|
+
spinner.succeed(`D1 database created: ${appName}-db (${dbIdMatch[1]})`);
|
|
529
|
+
}
|
|
530
|
+
else {
|
|
531
|
+
spinner.succeed(`D1 database created: ${appName}-db`);
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
else if (d1Result.stderr.includes("already exists")) {
|
|
535
|
+
spinner.info(`D1 database already exists: ${appName}-db`);
|
|
536
|
+
}
|
|
537
|
+
else {
|
|
538
|
+
spinner.warn("D1 database creation failed");
|
|
539
|
+
if (verbose) {
|
|
540
|
+
console.log(pc.gray(d1Result.stderr));
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
// ============================================================================
|
|
545
|
+
// Utilities
|
|
546
|
+
// ============================================================================
|
|
547
|
+
function runCommand(cmd, args, cwd, verbose) {
|
|
548
|
+
const result = spawnSync(cmd, args, {
|
|
549
|
+
cwd,
|
|
550
|
+
encoding: "utf-8",
|
|
551
|
+
stdio: verbose ? "inherit" : "pipe",
|
|
552
|
+
});
|
|
553
|
+
return {
|
|
554
|
+
success: result.status === 0,
|
|
555
|
+
stdout: result.stdout || "",
|
|
556
|
+
stderr: result.stderr || "",
|
|
557
|
+
};
|
|
558
|
+
}
|
|
559
|
+
function detectPackageManager(preferBun = false) {
|
|
560
|
+
// Check for existing lockfiles first
|
|
561
|
+
if (existsSync("bun.lockb"))
|
|
562
|
+
return "bun";
|
|
563
|
+
if (existsSync("pnpm-lock.yaml"))
|
|
564
|
+
return "pnpm";
|
|
565
|
+
if (existsSync("yarn.lock"))
|
|
566
|
+
return "yarn";
|
|
567
|
+
// If preferBun and bun is available, use it (better peer dep handling)
|
|
568
|
+
if (preferBun) {
|
|
569
|
+
try {
|
|
570
|
+
const result = spawnSync("bun", ["--version"], { encoding: "utf-8" });
|
|
571
|
+
if (result.status === 0)
|
|
572
|
+
return "bun";
|
|
573
|
+
}
|
|
574
|
+
catch {
|
|
575
|
+
// bun not available, fall through
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
return "npm";
|
|
579
|
+
}
|
|
580
|
+
function performDryRun(options) {
|
|
581
|
+
const template = getTemplate(options.template);
|
|
582
|
+
console.log(pc.bold("Dry Run - No changes will be made\n"));
|
|
583
|
+
console.log(pc.cyan("Configuration:"));
|
|
584
|
+
console.log(` Template: ${options.template}${template ? ` (${template.name})` : ""}`);
|
|
585
|
+
console.log(` App Name: ${options.appName}`);
|
|
586
|
+
console.log(` Target Dir: ${resolve(process.cwd(), options.appName)}`);
|
|
587
|
+
console.log("");
|
|
588
|
+
console.log(pc.cyan("Steps that would be executed:"));
|
|
589
|
+
console.log(" 1. Create project directory structure");
|
|
590
|
+
console.log(" 2. Generate quickback.config.ts");
|
|
591
|
+
console.log(" 3. Generate feature definitions");
|
|
592
|
+
if (template?.requiresCloudflare && !options.skipCloudflareSetup) {
|
|
593
|
+
console.log(" 4. Create Cloudflare D1 database");
|
|
594
|
+
}
|
|
595
|
+
else {
|
|
596
|
+
console.log(" 4. [SKIP] Cloudflare setup");
|
|
597
|
+
}
|
|
598
|
+
console.log(" 5. Compile project (quickback compile)");
|
|
599
|
+
if (!options.skipInstall) {
|
|
600
|
+
console.log(" 6. Install dependencies");
|
|
601
|
+
}
|
|
602
|
+
else {
|
|
603
|
+
console.log(" 6. [SKIP] Install dependencies");
|
|
604
|
+
}
|
|
605
|
+
console.log(" 7. Generate database schemas");
|
|
606
|
+
if (!template?.requiresCloudflare && !options.skipMigrations) {
|
|
607
|
+
console.log(" 8. Apply local migrations");
|
|
608
|
+
}
|
|
609
|
+
else {
|
|
610
|
+
console.log(" 8. [SKIP] Local migrations");
|
|
611
|
+
}
|
|
612
|
+
console.log(pc.green("\nDry run complete. Remove --dry-run to execute."));
|
|
613
|
+
}
|
|
614
|
+
function printSuccessMessage(appName, template) {
|
|
615
|
+
console.log("");
|
|
616
|
+
console.log(pc.green("✔ Project created successfully!"));
|
|
617
|
+
console.log("");
|
|
618
|
+
console.log(pc.bold("Next steps:"));
|
|
619
|
+
console.log(pc.cyan(` cd ${appName}`));
|
|
620
|
+
console.log(pc.cyan(" npm run dev"));
|
|
621
|
+
console.log("");
|
|
622
|
+
if (template.requiresCloudflare) {
|
|
623
|
+
console.log(pc.bold("Deploy to Cloudflare:"));
|
|
624
|
+
console.log(pc.cyan(" npm run deploy"));
|
|
625
|
+
console.log("");
|
|
626
|
+
}
|
|
627
|
+
console.log(pc.bold("Useful commands:"));
|
|
628
|
+
console.log(" npm run dev " + pc.gray("Start development server"));
|
|
629
|
+
console.log(" quickback compile " + pc.gray("Regenerate after editing definitions"));
|
|
630
|
+
console.log(" npx drizzle-kit generate " + pc.gray("Generate new migrations"));
|
|
631
|
+
console.log(" npx drizzle-kit migrate " + pc.gray("Apply database migrations"));
|
|
632
|
+
if (template.requiresCloudflare) {
|
|
633
|
+
console.log(" npm run deploy " + pc.gray("Deploy to Cloudflare Workers"));
|
|
634
|
+
}
|
|
635
|
+
console.log("");
|
|
636
|
+
console.log(pc.bold("Project structure:"));
|
|
637
|
+
console.log(pc.gray(` ${appName}/`));
|
|
638
|
+
console.log(pc.gray(" ├── quickback/ ← edit definitions here"));
|
|
639
|
+
console.log(pc.gray(" ├── src/ ← generated API code"));
|
|
640
|
+
console.log(pc.gray(" └── drizzle/ ← database migrations"));
|
|
641
|
+
console.log("");
|
|
642
|
+
}
|
|
643
|
+
function printHelp() {
|
|
644
|
+
console.log(`
|
|
645
|
+
${pc.bold("quickback create")} - Create a new Quickback project
|
|
646
|
+
|
|
647
|
+
${pc.bold("Usage:")}
|
|
648
|
+
quickback create <template> <app-name>
|
|
649
|
+
quickback create <app-name> ${pc.gray("(interactive template selection)")}
|
|
650
|
+
|
|
651
|
+
${pc.bold("Examples:")}
|
|
652
|
+
quickback create betterauth-d1-cloudflare my-app
|
|
653
|
+
quickback create cloudflare my-app ${pc.gray("(alias)")}
|
|
654
|
+
quickback create bun my-app ${pc.gray("(local development)")}
|
|
655
|
+
|
|
656
|
+
${pc.bold("Options:")}
|
|
657
|
+
--skip-cloudflare-setup Skip automatic Cloudflare resource creation
|
|
658
|
+
--skip-install Skip npm/bun install
|
|
659
|
+
--skip-migrations Skip database migrations
|
|
660
|
+
--dry-run Preview what would be created
|
|
661
|
+
--verbose, -v Show detailed output
|
|
662
|
+
--list, -l List available templates
|
|
663
|
+
--help, -h Show this help
|
|
664
|
+
|
|
665
|
+
${pc.bold("Templates:")}
|
|
666
|
+
${formatTemplateList()}
|
|
667
|
+
`);
|
|
668
|
+
}
|
|
669
|
+
//# sourceMappingURL=create.js.map
|