create-fullstack-scaffold 0.4.4 → 0.4.6
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/cli/index.js +127 -513
- package/dist/cli/index.js.map +1 -1
- package/package.json +1 -1
- package/template/drizzle/0003_ambiguous_magdalene.sql +100 -0
- package/template/drizzle/meta/0003_snapshot.json +1837 -0
- package/template/drizzle/meta/_journal.json +7 -0
- package/template/src/server/module-plugin/services/plugin-seed-service.ts +7 -1
package/dist/cli/index.js
CHANGED
|
@@ -1,433 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import { z } from '@hono/zod-openapi';
|
|
5
|
-
import fs, { readdirSync, statSync, existsSync, readFileSync } from 'fs';
|
|
6
|
-
import path2, { join } from 'path';
|
|
7
|
-
import os from 'os';
|
|
2
|
+
import { readFileSync, existsSync, readdirSync, statSync } from 'fs';
|
|
3
|
+
import path, { join } from 'path';
|
|
8
4
|
import { fileURLToPath, pathToFileURL } from 'url';
|
|
9
|
-
import
|
|
5
|
+
import { Command } from 'commander';
|
|
10
6
|
import chalk from 'chalk';
|
|
7
|
+
import { select } from '@inquirer/prompts';
|
|
8
|
+
import fs from 'fs-extra';
|
|
11
9
|
import ora from 'ora';
|
|
12
10
|
|
|
13
|
-
function createRPCClient(baseUrl) {
|
|
14
|
-
return hc(baseUrl);
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
// src/cli/utils/api.ts
|
|
18
|
-
var DEFAULT_BASE_URL = "http://localhost:3010";
|
|
19
|
-
var globalBaseUrl = process.env.BIOMIMIC_API_URL || DEFAULT_BASE_URL;
|
|
20
|
-
function setBaseUrl(url) {
|
|
21
|
-
globalBaseUrl = url;
|
|
22
|
-
}
|
|
23
|
-
function getBaseUrl() {
|
|
24
|
-
return globalBaseUrl;
|
|
25
|
-
}
|
|
26
|
-
function getClient() {
|
|
27
|
-
return createRPCClient(globalBaseUrl);
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
// src/cli/utils/logger.ts
|
|
31
|
-
var Logger = class {
|
|
32
|
-
level = "info";
|
|
33
|
-
constructor(options = {}) {
|
|
34
|
-
if (options.debug || process.env.BIOMIMIC_DEBUG === "true") {
|
|
35
|
-
this.level = "debug";
|
|
36
|
-
} else if (options.verbose || process.env.BIOMIMIC_VERBOSE === "true") {
|
|
37
|
-
this.level = "info";
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
setLevel(level) {
|
|
41
|
-
this.level = level;
|
|
42
|
-
}
|
|
43
|
-
shouldLog(level) {
|
|
44
|
-
const levels = ["silent", "error", "warn", "info", "debug"];
|
|
45
|
-
return levels.indexOf(level) <= levels.indexOf(this.level);
|
|
46
|
-
}
|
|
47
|
-
info(message, ...args) {
|
|
48
|
-
if (this.shouldLog("info")) {
|
|
49
|
-
process.stdout.write(`${message}${args.length ? " " + args.join(" ") : ""}
|
|
50
|
-
`);
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
debug(message, ...args) {
|
|
54
|
-
if (this.shouldLog("debug")) {
|
|
55
|
-
process.stdout.write(`[debug] ${message}${args.length ? " " + args.join(" ") : ""}
|
|
56
|
-
`);
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
warn(message) {
|
|
60
|
-
if (this.shouldLog("warn")) {
|
|
61
|
-
process.stderr.write(`[warn] ${message}
|
|
62
|
-
`);
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
error(message) {
|
|
66
|
-
if (this.shouldLog("error")) {
|
|
67
|
-
process.stderr.write(`[error] ${message}
|
|
68
|
-
`);
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
success(message, ...args) {
|
|
72
|
-
if (this.shouldLog("info")) {
|
|
73
|
-
process.stdout.write(`\u2713 ${message}${args.length ? " " + args.join(" ") : ""}
|
|
74
|
-
`);
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
fail(message) {
|
|
78
|
-
if (this.shouldLog("error")) {
|
|
79
|
-
process.stderr.write(`\u2717 ${message}
|
|
80
|
-
`);
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
};
|
|
84
|
-
var logger = null;
|
|
85
|
-
function createLogger(options = {}) {
|
|
86
|
-
logger = new Logger(options);
|
|
87
|
-
return logger;
|
|
88
|
-
}
|
|
89
|
-
function getLogger() {
|
|
90
|
-
if (!logger) {
|
|
91
|
-
logger = new Logger();
|
|
92
|
-
}
|
|
93
|
-
return logger;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// src/cli/utils/auto-command.ts
|
|
97
|
-
function extractZodInfo(schema) {
|
|
98
|
-
const def = schema._def;
|
|
99
|
-
if (def.typeName === "ZodOptional") {
|
|
100
|
-
const inner = schema._def.innerType;
|
|
101
|
-
const info = extractZodInfo(inner);
|
|
102
|
-
return { ...info, required: false };
|
|
103
|
-
}
|
|
104
|
-
if (def.typeName === "ZodDefault") {
|
|
105
|
-
const innerDef = schema._def;
|
|
106
|
-
const defaultValue = innerDef.defaultValue();
|
|
107
|
-
const info = extractZodInfo(innerDef.innerType);
|
|
108
|
-
return { ...info, required: false, defaultValue };
|
|
109
|
-
}
|
|
110
|
-
if (def.typeName === "ZodEnum") {
|
|
111
|
-
const enumValues = [...schema._def.values];
|
|
112
|
-
return { type: "enum", required: true, enumValues };
|
|
113
|
-
}
|
|
114
|
-
if (def.typeName === "ZodString") return { type: "string", required: true };
|
|
115
|
-
if (def.typeName === "ZodNumber") return { type: "number", required: true };
|
|
116
|
-
if (def.typeName === "ZodBoolean") return { type: "boolean", required: true };
|
|
117
|
-
if (def.typeName === "ZodObject") return { type: "object", required: true };
|
|
118
|
-
return { type: "string", required: true };
|
|
119
|
-
}
|
|
120
|
-
function schemaToOptions(schema) {
|
|
121
|
-
const options = [];
|
|
122
|
-
const shape = schema.shape;
|
|
123
|
-
for (const [key, zodType] of Object.entries(shape)) {
|
|
124
|
-
const info = extractZodInfo(zodType);
|
|
125
|
-
const longFlag = key.replace(/([A-Z])/g, "-$1").toLowerCase();
|
|
126
|
-
let flags = `--${longFlag} <value>`;
|
|
127
|
-
if (info.type === "boolean") {
|
|
128
|
-
flags = `--${longFlag}`;
|
|
129
|
-
}
|
|
130
|
-
let description = String(info.type);
|
|
131
|
-
if (info.enumValues) {
|
|
132
|
-
description = `${info.type} (${info.enumValues.join("|")})`;
|
|
133
|
-
}
|
|
134
|
-
options.push({
|
|
135
|
-
flags,
|
|
136
|
-
description,
|
|
137
|
-
required: info.required,
|
|
138
|
-
defaultValue: info.defaultValue
|
|
139
|
-
});
|
|
140
|
-
}
|
|
141
|
-
return options;
|
|
142
|
-
}
|
|
143
|
-
function pathToApiCall(client, _method, path3) {
|
|
144
|
-
const pathParts = path3.split("/").filter(Boolean);
|
|
145
|
-
let current = client.api;
|
|
146
|
-
for (const part of pathParts) {
|
|
147
|
-
if (part.startsWith("{") && part.endsWith("}")) {
|
|
148
|
-
continue;
|
|
149
|
-
}
|
|
150
|
-
current = current[part] || current[`:${part}`];
|
|
151
|
-
}
|
|
152
|
-
return current;
|
|
153
|
-
}
|
|
154
|
-
function createCommandFromRoute(config) {
|
|
155
|
-
const options = [];
|
|
156
|
-
const arguments_ = [];
|
|
157
|
-
if (config.params) {
|
|
158
|
-
const shape = config.params.shape;
|
|
159
|
-
for (const [key] of Object.entries(shape)) {
|
|
160
|
-
arguments_.push({
|
|
161
|
-
name: key,
|
|
162
|
-
description: `${key} parameter`,
|
|
163
|
-
required: true
|
|
164
|
-
});
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
if (config.body) {
|
|
168
|
-
const bodySchema = config.body;
|
|
169
|
-
if (bodySchema.shape) {
|
|
170
|
-
options.push(...schemaToOptions(bodySchema) || []);
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
if (config.query) {
|
|
174
|
-
options.push(...schemaToOptions(config.query) || []);
|
|
175
|
-
}
|
|
176
|
-
return {
|
|
177
|
-
name: config.command,
|
|
178
|
-
description: config.description,
|
|
179
|
-
options,
|
|
180
|
-
arguments: arguments_,
|
|
181
|
-
action: async (opts, args) => {
|
|
182
|
-
const logger2 = getLogger();
|
|
183
|
-
const client = getClient();
|
|
184
|
-
const param = {};
|
|
185
|
-
if (config.params) {
|
|
186
|
-
const shape = config.params.shape;
|
|
187
|
-
const keys = Object.keys(shape);
|
|
188
|
-
keys.forEach((key, i) => {
|
|
189
|
-
param[key] = args[i] || String(opts[key]);
|
|
190
|
-
});
|
|
191
|
-
}
|
|
192
|
-
const json = {};
|
|
193
|
-
if (config.body && opts) {
|
|
194
|
-
const bodySchema = config.body;
|
|
195
|
-
if (bodySchema.shape) {
|
|
196
|
-
const shape = bodySchema.shape;
|
|
197
|
-
for (const key of Object.keys(shape)) {
|
|
198
|
-
if (opts[key] !== void 0) {
|
|
199
|
-
json[key] = opts[key];
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
const query = {};
|
|
205
|
-
if (config.query && opts) {
|
|
206
|
-
const shape = config.query.shape;
|
|
207
|
-
for (const key of Object.keys(shape)) {
|
|
208
|
-
if (opts[key] !== void 0) {
|
|
209
|
-
query[key] = String(opts[key]);
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
const apiCall = pathToApiCall(client, config.method, config.path);
|
|
214
|
-
const methodCall = apiCall[`$${config.method.charAt(0).toUpperCase() + config.method.slice(1)}`] || apiCall.$get;
|
|
215
|
-
const res = await methodCall.call(apiCall, {
|
|
216
|
-
param: Object.keys(param).length > 0 ? param : void 0,
|
|
217
|
-
query: Object.keys(query).length > 0 ? query : void 0,
|
|
218
|
-
json: Object.keys(json).length > 0 ? json : void 0
|
|
219
|
-
});
|
|
220
|
-
const data = await res.json();
|
|
221
|
-
logger2.info(JSON.stringify(data, null, 2));
|
|
222
|
-
}
|
|
223
|
-
};
|
|
224
|
-
}
|
|
225
|
-
function registerAutoCommand(program2, config) {
|
|
226
|
-
const cmd = createCommandFromRoute(config);
|
|
227
|
-
const command = program2.command(cmd.name).description(cmd.description).action(async (firstArg, opts) => {
|
|
228
|
-
const args = typeof firstArg === "string" ? [firstArg] : [];
|
|
229
|
-
const options = (typeof firstArg === "object" ? firstArg : opts) || {};
|
|
230
|
-
await cmd.action(options, args);
|
|
231
|
-
});
|
|
232
|
-
cmd.options?.forEach((opt) => {
|
|
233
|
-
if (opt.required) {
|
|
234
|
-
command.requiredOption(opt.flags, opt.description, opt.defaultValue);
|
|
235
|
-
} else {
|
|
236
|
-
command.option(opt.flags, opt.description, opt.defaultValue);
|
|
237
|
-
}
|
|
238
|
-
});
|
|
239
|
-
cmd.arguments?.forEach((arg) => {
|
|
240
|
-
command.argument(arg.required ? `<${arg.name}>` : `[${arg.name}]`, arg.description);
|
|
241
|
-
});
|
|
242
|
-
}
|
|
243
|
-
var TodoStatusSchema = z.enum(["pending", "in_progress", "completed"]);
|
|
244
|
-
var todoRoutes = [
|
|
245
|
-
{
|
|
246
|
-
method: "get",
|
|
247
|
-
path: "/todos",
|
|
248
|
-
command: "list",
|
|
249
|
-
description: "List all todos"
|
|
250
|
-
},
|
|
251
|
-
{
|
|
252
|
-
method: "get",
|
|
253
|
-
path: "/todos/{id}",
|
|
254
|
-
command: "get",
|
|
255
|
-
description: "Get a todo by ID",
|
|
256
|
-
params: z.object({ id: z.string() })
|
|
257
|
-
},
|
|
258
|
-
{
|
|
259
|
-
method: "post",
|
|
260
|
-
path: "/todos",
|
|
261
|
-
command: "create",
|
|
262
|
-
description: "Create a new todo",
|
|
263
|
-
body: z.object({
|
|
264
|
-
title: z.string().min(1),
|
|
265
|
-
description: z.string().optional()
|
|
266
|
-
})
|
|
267
|
-
},
|
|
268
|
-
{
|
|
269
|
-
method: "put",
|
|
270
|
-
path: "/todos/{id}",
|
|
271
|
-
command: "update",
|
|
272
|
-
description: "Update a todo",
|
|
273
|
-
params: z.object({ id: z.string() }),
|
|
274
|
-
body: z.object({
|
|
275
|
-
title: z.string().optional(),
|
|
276
|
-
description: z.string().optional(),
|
|
277
|
-
status: TodoStatusSchema.optional()
|
|
278
|
-
})
|
|
279
|
-
},
|
|
280
|
-
{
|
|
281
|
-
method: "delete",
|
|
282
|
-
path: "/todos/{id}",
|
|
283
|
-
command: "delete",
|
|
284
|
-
description: "Delete a todo",
|
|
285
|
-
params: z.object({ id: z.string() })
|
|
286
|
-
}
|
|
287
|
-
];
|
|
288
|
-
function registerTodoCommands(program2) {
|
|
289
|
-
const todo = program2.command("todo").description("Todo management commands");
|
|
290
|
-
for (const route of todoRoutes) {
|
|
291
|
-
registerAutoCommand(todo, route);
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
// src/cli/modules/notification/index.ts
|
|
296
|
-
function registerNotificationCommands(program2) {
|
|
297
|
-
const notification = program2.command("notification").description("Notification management commands");
|
|
298
|
-
notification.command("list").description("List all notifications").option("--unread-only", "Show only unread notifications").option("--limit <number>", "Limit number of results", "20").action(async (options) => {
|
|
299
|
-
const logger2 = getLogger();
|
|
300
|
-
const client = getClient();
|
|
301
|
-
const unreadOnly = Boolean(options.unreadOnly);
|
|
302
|
-
const limit = parseInt(options.limit || "20");
|
|
303
|
-
const res = await client.api.notifications.$get({
|
|
304
|
-
query: { unreadOnly: String(unreadOnly), limit: String(limit) }
|
|
305
|
-
});
|
|
306
|
-
const data = await res.json();
|
|
307
|
-
logger2.info(JSON.stringify(data, null, 2));
|
|
308
|
-
});
|
|
309
|
-
notification.command("create").description("Create a new notification").requiredOption("-t, --title <title>", "Notification title").requiredOption("-m, --message <message>", "Notification message").option("--type <type>", "Notification type (info|warning|success|error)", "info").action(async (options) => {
|
|
310
|
-
const logger2 = getLogger();
|
|
311
|
-
const client = getClient();
|
|
312
|
-
const res = await client.api.notifications.$post({
|
|
313
|
-
json: {
|
|
314
|
-
type: options.type,
|
|
315
|
-
title: options.title,
|
|
316
|
-
message: options.message
|
|
317
|
-
}
|
|
318
|
-
});
|
|
319
|
-
const data = await res.json();
|
|
320
|
-
logger2.success("Notification created");
|
|
321
|
-
logger2.info(JSON.stringify(data, null, 2));
|
|
322
|
-
});
|
|
323
|
-
notification.command("unread-count").description("Get unread notification count").action(async () => {
|
|
324
|
-
const logger2 = getLogger();
|
|
325
|
-
const client = getClient();
|
|
326
|
-
const res = await client.api.notifications["unread-count"].$get();
|
|
327
|
-
const data = await res.json();
|
|
328
|
-
logger2.info(JSON.stringify(data, null, 2));
|
|
329
|
-
});
|
|
330
|
-
notification.command("mark-read").description("Mark a notification as read").argument("<id>", "Notification ID").action(async (id) => {
|
|
331
|
-
const logger2 = getLogger();
|
|
332
|
-
const client = getClient();
|
|
333
|
-
const res = await client.api.notifications[":id"].read.$patch({ param: { id } });
|
|
334
|
-
const data = await res.json();
|
|
335
|
-
logger2.success("Notification marked as read");
|
|
336
|
-
logger2.info(JSON.stringify(data, null, 2));
|
|
337
|
-
});
|
|
338
|
-
notification.command("delete").description("Delete a notification").argument("<id>", "Notification ID").action(async (id) => {
|
|
339
|
-
const logger2 = getLogger();
|
|
340
|
-
const client = getClient();
|
|
341
|
-
const res = await client.api.notifications[":id"].$delete({ param: { id } });
|
|
342
|
-
const data = await res.json();
|
|
343
|
-
logger2.success("Notification deleted");
|
|
344
|
-
logger2.info(JSON.stringify(data, null, 2));
|
|
345
|
-
});
|
|
346
|
-
}
|
|
347
|
-
var CONFIG_DIR = path2.join(os.homedir(), ".biomimic");
|
|
348
|
-
var CONFIG_FILE = path2.join(CONFIG_DIR, "config.json");
|
|
349
|
-
function loadConfig() {
|
|
350
|
-
try {
|
|
351
|
-
if (fs.existsSync(CONFIG_FILE)) {
|
|
352
|
-
const content = fs.readFileSync(CONFIG_FILE, "utf-8");
|
|
353
|
-
return JSON.parse(content);
|
|
354
|
-
}
|
|
355
|
-
} catch {
|
|
356
|
-
}
|
|
357
|
-
return { baseUrl: "http://localhost:3010" };
|
|
358
|
-
}
|
|
359
|
-
function saveConfig(config) {
|
|
360
|
-
if (!fs.existsSync(CONFIG_DIR)) {
|
|
361
|
-
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
362
|
-
}
|
|
363
|
-
fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
|
|
364
|
-
}
|
|
365
|
-
function registerConfigCommands(program2) {
|
|
366
|
-
const config = program2.command("config").description("CLI configuration and service management");
|
|
367
|
-
config.command("get").description("Show current configuration").option("-k, --key <key>", "Get specific config key").action((options) => {
|
|
368
|
-
const logger2 = getLogger();
|
|
369
|
-
const cfg = loadConfig();
|
|
370
|
-
if (options.key) {
|
|
371
|
-
const value = cfg[options.key];
|
|
372
|
-
logger2.info(`${options.key}: ${value ?? "not set"}`);
|
|
373
|
-
} else {
|
|
374
|
-
logger2.info(JSON.stringify(cfg, null, 2));
|
|
375
|
-
}
|
|
376
|
-
});
|
|
377
|
-
config.command("set").description("Set configuration value").option("-u, --url <url>", "Set server URL").action((options) => {
|
|
378
|
-
const logger2 = getLogger();
|
|
379
|
-
const cfg = loadConfig();
|
|
380
|
-
if (options.url) {
|
|
381
|
-
cfg.baseUrl = options.url;
|
|
382
|
-
setBaseUrl(options.url);
|
|
383
|
-
logger2.success(`Server URL set to: ${options.url}`);
|
|
384
|
-
}
|
|
385
|
-
saveConfig(cfg);
|
|
386
|
-
});
|
|
387
|
-
config.command("url").description("Show or set server URL").argument("[url]", "New server URL").action((url) => {
|
|
388
|
-
const logger2 = getLogger();
|
|
389
|
-
if (url) {
|
|
390
|
-
const cfg = loadConfig();
|
|
391
|
-
cfg.baseUrl = url;
|
|
392
|
-
setBaseUrl(url);
|
|
393
|
-
saveConfig(cfg);
|
|
394
|
-
logger2.success(`Server URL set to: ${url}`);
|
|
395
|
-
} else {
|
|
396
|
-
logger2.info(`Current server URL: ${getBaseUrl()}`);
|
|
397
|
-
}
|
|
398
|
-
});
|
|
399
|
-
config.command("status").description("Check server connection status").action(async () => {
|
|
400
|
-
const logger2 = getLogger();
|
|
401
|
-
const client = getClient();
|
|
402
|
-
try {
|
|
403
|
-
const res = await client.health.$get();
|
|
404
|
-
const data = await res.json();
|
|
405
|
-
logger2.success("Server is reachable");
|
|
406
|
-
logger2.info(JSON.stringify(data, null, 2));
|
|
407
|
-
} catch (error) {
|
|
408
|
-
logger2.error(`Server not reachable: ${getBaseUrl()}`);
|
|
409
|
-
logger2.error(String(error));
|
|
410
|
-
}
|
|
411
|
-
});
|
|
412
|
-
config.command("reset").description("Reset configuration to defaults").action(() => {
|
|
413
|
-
const logger2 = getLogger();
|
|
414
|
-
const defaultConfig = { baseUrl: "http://localhost:3010" };
|
|
415
|
-
saveConfig(defaultConfig);
|
|
416
|
-
setBaseUrl(defaultConfig.baseUrl);
|
|
417
|
-
logger2.success("Configuration reset to defaults");
|
|
418
|
-
});
|
|
419
|
-
config.command("path").description("Show config file path").action(() => {
|
|
420
|
-
const logger2 = getLogger();
|
|
421
|
-
logger2.info(`Config file: ${CONFIG_FILE}`);
|
|
422
|
-
});
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
// src/cli/modules/index.ts
|
|
426
|
-
function registerModules(program2) {
|
|
427
|
-
registerTodoCommands(program2);
|
|
428
|
-
registerNotificationCommands(program2);
|
|
429
|
-
registerConfigCommands(program2);
|
|
430
|
-
}
|
|
431
11
|
var tsImportFn;
|
|
432
12
|
async function getTsImport() {
|
|
433
13
|
if (tsImportFn) return tsImportFn;
|
|
@@ -1145,6 +725,11 @@ var SEED_MODULES = [
|
|
|
1145
725
|
module: "tenant",
|
|
1146
726
|
importLine: "",
|
|
1147
727
|
call: "import('../module-tenant/services/tenant-service').then(m => m.seedTenantsIfEmpty())"
|
|
728
|
+
},
|
|
729
|
+
{
|
|
730
|
+
module: "plugin",
|
|
731
|
+
importLine: "",
|
|
732
|
+
call: "import('../module-plugin/services/plugin-seed-service').then(m => m.seedPluginsIfEmpty())"
|
|
1148
733
|
}
|
|
1149
734
|
];
|
|
1150
735
|
var INITIAL_PERMISSIONS = `const initialPermissions = [
|
|
@@ -3426,7 +3011,7 @@ if (typeof window !== 'undefined') {
|
|
|
3426
3011
|
|
|
3427
3012
|
// src/commands/create.ts
|
|
3428
3013
|
var __filename$1 = fileURLToPath(import.meta.url);
|
|
3429
|
-
var __dirname$1 =
|
|
3014
|
+
var __dirname$1 = path.dirname(__filename$1);
|
|
3430
3015
|
var TEMPLATE_PROJECT_NAME = "biomimic-todo-app";
|
|
3431
3016
|
var TEMPLATE_DB_NAME = "biomimic-todo-db";
|
|
3432
3017
|
var ScaffoldError = class extends Error {
|
|
@@ -3468,11 +3053,11 @@ function generateDbName(projectName) {
|
|
|
3468
3053
|
return `${sanitized}-db`;
|
|
3469
3054
|
}
|
|
3470
3055
|
async function updateWranglerToml(targetDir, projectName) {
|
|
3471
|
-
const wranglerPath =
|
|
3472
|
-
if (!await
|
|
3056
|
+
const wranglerPath = path.join(targetDir, "wrangler.toml");
|
|
3057
|
+
if (!await fs.pathExists(wranglerPath)) {
|
|
3473
3058
|
return;
|
|
3474
3059
|
}
|
|
3475
|
-
let content = await
|
|
3060
|
+
let content = await fs.readFile(wranglerPath, "utf-8");
|
|
3476
3061
|
const dbName = generateDbName(projectName);
|
|
3477
3062
|
content = content.replace(
|
|
3478
3063
|
new RegExp(`^name = "${TEMPLATE_PROJECT_NAME}"`, "m"),
|
|
@@ -3486,43 +3071,43 @@ async function updateWranglerToml(targetDir, projectName) {
|
|
|
3486
3071
|
/database_id = "[^"]+"/,
|
|
3487
3072
|
`database_id = "" # TODO: Run 'wrangler d1 create ${dbName}' and paste the ID here`
|
|
3488
3073
|
);
|
|
3489
|
-
await
|
|
3074
|
+
await fs.writeFile(wranglerPath, content);
|
|
3490
3075
|
}
|
|
3491
3076
|
async function updatePackageJson(targetDir, projectName, resolved) {
|
|
3492
|
-
const pkgJsonPath =
|
|
3493
|
-
if (!await
|
|
3077
|
+
const pkgJsonPath = path.join(targetDir, "package.json");
|
|
3078
|
+
if (!await fs.pathExists(pkgJsonPath)) {
|
|
3494
3079
|
return;
|
|
3495
3080
|
}
|
|
3496
|
-
let pkgJson = await
|
|
3081
|
+
let pkgJson = await fs.readJson(pkgJsonPath);
|
|
3497
3082
|
pkgJson = filterPackageJson(pkgJson, resolved);
|
|
3498
3083
|
pkgJson.name = projectName;
|
|
3499
3084
|
if (pkgJson.bin) {
|
|
3500
3085
|
delete pkgJson.bin;
|
|
3501
3086
|
}
|
|
3502
|
-
await
|
|
3087
|
+
await fs.writeJson(pkgJsonPath, pkgJson, { spaces: 2 });
|
|
3503
3088
|
}
|
|
3504
3089
|
async function updatePackageLockJson(targetDir, projectName) {
|
|
3505
|
-
const lockFilePath =
|
|
3506
|
-
if (!await
|
|
3090
|
+
const lockFilePath = path.join(targetDir, "package-lock.json");
|
|
3091
|
+
if (!await fs.pathExists(lockFilePath)) {
|
|
3507
3092
|
return;
|
|
3508
3093
|
}
|
|
3509
|
-
const lockFile = await
|
|
3094
|
+
const lockFile = await fs.readJson(lockFilePath);
|
|
3510
3095
|
if (lockFile.name === TEMPLATE_PROJECT_NAME) {
|
|
3511
3096
|
lockFile.name = projectName;
|
|
3512
3097
|
}
|
|
3513
3098
|
if (lockFile.packages?.[""]?.name === TEMPLATE_PROJECT_NAME) {
|
|
3514
3099
|
lockFile.packages[""].name = projectName;
|
|
3515
3100
|
}
|
|
3516
|
-
await
|
|
3101
|
+
await fs.writeJson(lockFilePath, lockFile, { spaces: 2 });
|
|
3517
3102
|
}
|
|
3518
3103
|
async function updateReadme(targetDir, projectName) {
|
|
3519
|
-
const readmePath =
|
|
3520
|
-
if (!await
|
|
3104
|
+
const readmePath = path.join(targetDir, "README.md");
|
|
3105
|
+
if (!await fs.pathExists(readmePath)) {
|
|
3521
3106
|
return;
|
|
3522
3107
|
}
|
|
3523
|
-
let content = await
|
|
3108
|
+
let content = await fs.readFile(readmePath, "utf-8");
|
|
3524
3109
|
content = content.replace(/^# (.+)$/m, `# ${projectName}`);
|
|
3525
|
-
await
|
|
3110
|
+
await fs.writeFile(readmePath, content);
|
|
3526
3111
|
}
|
|
3527
3112
|
async function createProject(projectNameOrOptions, useCurrentDir = false, preset) {
|
|
3528
3113
|
let projectName;
|
|
@@ -3545,19 +3130,19 @@ async function createProject(projectNameOrOptions, useCurrentDir = false, preset
|
|
|
3545
3130
|
if (!currentDir) {
|
|
3546
3131
|
validateProjectName(projectName);
|
|
3547
3132
|
}
|
|
3548
|
-
const templateDir =
|
|
3133
|
+
const templateDir = path.join(__dirname$1, "../../template");
|
|
3549
3134
|
let targetDir;
|
|
3550
3135
|
if (currentDir) {
|
|
3551
3136
|
targetDir = process.cwd();
|
|
3552
|
-
projectName =
|
|
3137
|
+
projectName = path.basename(targetDir);
|
|
3553
3138
|
} else if (outputDir) {
|
|
3554
|
-
targetDir =
|
|
3555
|
-
if (await
|
|
3139
|
+
targetDir = path.resolve(outputDir);
|
|
3140
|
+
if (await fs.pathExists(targetDir)) {
|
|
3556
3141
|
throw new ScaffoldError(`Directory ${outputDir} already exists`);
|
|
3557
3142
|
}
|
|
3558
3143
|
} else {
|
|
3559
|
-
targetDir =
|
|
3560
|
-
if (await
|
|
3144
|
+
targetDir = path.resolve(process.cwd(), projectName);
|
|
3145
|
+
if (await fs.pathExists(targetDir)) {
|
|
3561
3146
|
throw new ScaffoldError(`Directory ${projectName} already exists`);
|
|
3562
3147
|
}
|
|
3563
3148
|
}
|
|
@@ -3584,16 +3169,16 @@ async function createProject(projectNameOrOptions, useCurrentDir = false, preset
|
|
|
3584
3169
|
for (const file of generatedFiles2) {
|
|
3585
3170
|
console.log(` ${chalk.green("\u2713")} ${file}`);
|
|
3586
3171
|
}
|
|
3587
|
-
const gitignorePath2 =
|
|
3172
|
+
const gitignorePath2 = path.join(templateDir, ".gitignore");
|
|
3588
3173
|
let ignorePatterns2 = [];
|
|
3589
|
-
if (await
|
|
3590
|
-
const gitignoreContent = await
|
|
3174
|
+
if (await fs.pathExists(gitignorePath2)) {
|
|
3175
|
+
const gitignoreContent = await fs.readFile(gitignorePath2, "utf-8");
|
|
3591
3176
|
ignorePatterns2 = parseGitignore(gitignoreContent);
|
|
3592
3177
|
}
|
|
3593
3178
|
ignorePatterns2.push("node_modules", ".wrangler");
|
|
3594
3179
|
const excludePatterns2 = getExcludePatterns(resolved, allManifests);
|
|
3595
3180
|
let templateFileCount = 0;
|
|
3596
|
-
const templateFiles = await
|
|
3181
|
+
const templateFiles = await fs.readdir(templateDir, { recursive: true });
|
|
3597
3182
|
for (const file of templateFiles) {
|
|
3598
3183
|
const relative = String(file);
|
|
3599
3184
|
if (!relative) continue;
|
|
@@ -3628,21 +3213,21 @@ async function createProject(projectNameOrOptions, useCurrentDir = false, preset
|
|
|
3628
3213
|
}
|
|
3629
3214
|
if (!currentDir) {
|
|
3630
3215
|
const dirSpinner = ora("Creating project directory...").start();
|
|
3631
|
-
await
|
|
3216
|
+
await fs.ensureDir(targetDir);
|
|
3632
3217
|
dirSpinner.succeed(chalk.green("Project directory created"));
|
|
3633
3218
|
}
|
|
3634
3219
|
const copySpinner = ora("Copying template files...").start();
|
|
3635
|
-
const gitignorePath =
|
|
3220
|
+
const gitignorePath = path.join(templateDir, ".gitignore");
|
|
3636
3221
|
let ignorePatterns = [];
|
|
3637
|
-
if (await
|
|
3638
|
-
const gitignoreContent = await
|
|
3222
|
+
if (await fs.pathExists(gitignorePath)) {
|
|
3223
|
+
const gitignoreContent = await fs.readFile(gitignorePath, "utf-8");
|
|
3639
3224
|
ignorePatterns = parseGitignore(gitignoreContent);
|
|
3640
3225
|
}
|
|
3641
3226
|
ignorePatterns.push("node_modules", ".wrangler");
|
|
3642
3227
|
const excludePatterns = getExcludePatterns(resolved, allManifests);
|
|
3643
|
-
await
|
|
3228
|
+
await fs.copy(templateDir, targetDir, {
|
|
3644
3229
|
filter: (src) => {
|
|
3645
|
-
const relative =
|
|
3230
|
+
const relative = path.relative(templateDir, src);
|
|
3646
3231
|
if (relative === "") return true;
|
|
3647
3232
|
const negated = ignorePatterns.filter((p) => p.startsWith("!"));
|
|
3648
3233
|
const gitIgnored = ignorePatterns.filter((p) => !p.startsWith("!") && relative.startsWith(p));
|
|
@@ -3664,80 +3249,80 @@ async function createProject(projectNameOrOptions, useCurrentDir = false, preset
|
|
|
3664
3249
|
copySpinner.succeed(chalk.green("Template files copied"));
|
|
3665
3250
|
const genSpinner = ora("Generating module-specific files...").start();
|
|
3666
3251
|
const routeRegistryContent = generateRouteRegistry(resolved);
|
|
3667
|
-
await
|
|
3252
|
+
await fs.writeFile(path.join(targetDir, "src/server/route-registry.ts"), routeRegistryContent);
|
|
3668
3253
|
const dbSchemaContent = generateDbSchemaBarrel(resolved);
|
|
3669
|
-
await
|
|
3254
|
+
await fs.writeFile(path.join(targetDir, "src/server/db/schema/index.ts"), dbSchemaContent);
|
|
3670
3255
|
if (resolved.hasClient) {
|
|
3671
3256
|
const clientNavContent = generateClientNavigation(resolved);
|
|
3672
|
-
await
|
|
3673
|
-
|
|
3257
|
+
await fs.writeFile(
|
|
3258
|
+
path.join(targetDir, "src/client/components/Navigation.tsx"),
|
|
3674
3259
|
clientNavContent
|
|
3675
3260
|
);
|
|
3676
3261
|
const clientAppTestContent = generateClientAppTest(resolved);
|
|
3677
|
-
await
|
|
3678
|
-
await
|
|
3679
|
-
|
|
3262
|
+
await fs.ensureDir(path.join(targetDir, "src/client/components/__tests__"));
|
|
3263
|
+
await fs.writeFile(
|
|
3264
|
+
path.join(targetDir, "src/client/components/__tests__/App.test.tsx"),
|
|
3680
3265
|
clientAppTestContent
|
|
3681
3266
|
);
|
|
3682
3267
|
const clientNavTestContent = generateClientNavigationTest(resolved);
|
|
3683
|
-
await
|
|
3684
|
-
|
|
3268
|
+
await fs.writeFile(
|
|
3269
|
+
path.join(targetDir, "src/client/components/__tests__/Navigation.test.tsx"),
|
|
3685
3270
|
clientNavTestContent
|
|
3686
3271
|
);
|
|
3687
3272
|
const presetUIConfigContent = generatePresetUIConfig(resolved, selectedPreset.id);
|
|
3688
|
-
await
|
|
3689
|
-
|
|
3273
|
+
await fs.writeFile(
|
|
3274
|
+
path.join(targetDir, "src/client/preset-ui-config.ts"),
|
|
3690
3275
|
presetUIConfigContent
|
|
3691
3276
|
);
|
|
3692
3277
|
const clientMainContent = generateClientMain(resolved, selectedPreset.id);
|
|
3693
|
-
await
|
|
3278
|
+
await fs.writeFile(path.join(targetDir, "src/client/main.tsx"), clientMainContent);
|
|
3694
3279
|
}
|
|
3695
3280
|
if (resolved.hasClient && resolved.modules.has("admin")) {
|
|
3696
3281
|
const adminAppContent = generateAdminApp(resolved);
|
|
3697
3282
|
if (adminAppContent) {
|
|
3698
|
-
await
|
|
3699
|
-
await
|
|
3283
|
+
await fs.ensureDir(path.join(targetDir, "src/admin"));
|
|
3284
|
+
await fs.writeFile(path.join(targetDir, "src/admin/App.tsx"), adminAppContent);
|
|
3700
3285
|
}
|
|
3701
3286
|
}
|
|
3702
3287
|
const serverAppContent = generateServerApp(resolved);
|
|
3703
|
-
await
|
|
3288
|
+
await fs.writeFile(path.join(targetDir, "src/server/app.ts"), serverAppContent);
|
|
3704
3289
|
const generatedFiles = getGeneratedFiles(resolved);
|
|
3705
3290
|
if (generatedFiles.includes("src/server/db/init.ts")) {
|
|
3706
3291
|
const dbInitContent = generateDbInit(resolved);
|
|
3707
|
-
await
|
|
3292
|
+
await fs.writeFile(path.join(targetDir, "src/server/db/init.ts"), dbInitContent);
|
|
3708
3293
|
}
|
|
3709
3294
|
const sharedModulesContent = generateSharedModulesIndex(resolved);
|
|
3710
|
-
await
|
|
3295
|
+
await fs.writeFile(path.join(targetDir, "src/shared/modules/index.ts"), sharedModulesContent);
|
|
3711
3296
|
const sharedSchemasContent = generateSharedSchemasIndex(resolved);
|
|
3712
|
-
await
|
|
3297
|
+
await fs.writeFile(path.join(targetDir, "src/shared/schemas/index.ts"), sharedSchemasContent);
|
|
3713
3298
|
const middlewareIndexContent = generateMiddlewareIndex(resolved);
|
|
3714
|
-
await
|
|
3715
|
-
|
|
3299
|
+
await fs.writeFile(
|
|
3300
|
+
path.join(targetDir, "src/server/middleware/index.ts"),
|
|
3716
3301
|
middlewareIndexContent
|
|
3717
3302
|
);
|
|
3718
3303
|
if (generatedFiles.includes("src/server/middleware/auth.ts")) {
|
|
3719
3304
|
const authMiddlewareContent = generateAuthMiddleware(resolved);
|
|
3720
|
-
await
|
|
3721
|
-
|
|
3305
|
+
await fs.writeFile(
|
|
3306
|
+
path.join(targetDir, "src/server/middleware/auth.ts"),
|
|
3722
3307
|
authMiddlewareContent
|
|
3723
3308
|
);
|
|
3724
3309
|
}
|
|
3725
3310
|
if (generatedFiles.includes("src/server/utils/auth.ts")) {
|
|
3726
3311
|
const authUtilsContent = generateAuthUtils(resolved);
|
|
3727
|
-
await
|
|
3312
|
+
await fs.writeFile(path.join(targetDir, "src/server/utils/auth.ts"), authUtilsContent);
|
|
3728
3313
|
}
|
|
3729
3314
|
if (resolved.hasClient) {
|
|
3730
3315
|
const clientComponentsContent = generateClientComponentsIndex(resolved);
|
|
3731
|
-
await
|
|
3732
|
-
|
|
3316
|
+
await fs.writeFile(
|
|
3317
|
+
path.join(targetDir, "src/client/components/index.ts"),
|
|
3733
3318
|
clientComponentsContent
|
|
3734
3319
|
);
|
|
3735
3320
|
}
|
|
3736
3321
|
const cliModulesContent = generateCliModulesIndex(resolved);
|
|
3737
|
-
await
|
|
3322
|
+
await fs.writeFile(path.join(targetDir, "src/cli/modules/index.ts"), cliModulesContent);
|
|
3738
3323
|
if (resolved.hasClient && generatedFiles.includes("vite.config.ts")) {
|
|
3739
3324
|
const viteConfigContent = generateViteConfig(resolved, templateDir);
|
|
3740
|
-
await
|
|
3325
|
+
await fs.writeFile(path.join(targetDir, "vite.config.ts"), viteConfigContent);
|
|
3741
3326
|
}
|
|
3742
3327
|
genSpinner.succeed(chalk.green("Module-specific files generated"));
|
|
3743
3328
|
const pkgSpinner = ora("Configuring package.json...").start();
|
|
@@ -3789,37 +3374,66 @@ async function createProject(projectNameOrOptions, useCurrentDir = false, preset
|
|
|
3789
3374
|
}
|
|
3790
3375
|
}
|
|
3791
3376
|
|
|
3792
|
-
// src/
|
|
3793
|
-
var
|
|
3794
|
-
|
|
3795
|
-
|
|
3796
|
-
|
|
3797
|
-
|
|
3798
|
-
|
|
3799
|
-
|
|
3800
|
-
|
|
3801
|
-
|
|
3802
|
-
|
|
3803
|
-
|
|
3804
|
-
|
|
3805
|
-
|
|
3806
|
-
|
|
3807
|
-
|
|
3808
|
-
|
|
3809
|
-
|
|
3810
|
-
|
|
3811
|
-
|
|
3812
|
-
|
|
3377
|
+
// src/index.ts
|
|
3378
|
+
var __filename2 = fileURLToPath(import.meta.url);
|
|
3379
|
+
var __dirname2 = path.dirname(__filename2);
|
|
3380
|
+
var rootDir = __dirname2.endsWith(path.join("src")) || __dirname2.endsWith(path.join("dist")) ? path.resolve(__dirname2, "..") : path.resolve(__dirname2, "..", "..");
|
|
3381
|
+
var packageJson = JSON.parse(readFileSync(path.join(rootDir, "package.json"), "utf-8"));
|
|
3382
|
+
var program = new Command();
|
|
3383
|
+
program.name("create-fullstack-scaffold").description("Create a new full-stack scaffold app with Todo List example").version(packageJson.version).argument("[project-name]", "Name of your project").option("-c, --current-dir", "Create project in current directory").option(
|
|
3384
|
+
"-p, --preset <preset>",
|
|
3385
|
+
"Template preset to use (fullstack-admin, todo-app, cli-only, minimal)"
|
|
3386
|
+
).option("-o, --output-dir <path>", "Output directory (defaults to project name)").option("--dry-run", "Show what would be generated without creating files").action(
|
|
3387
|
+
async (projectName = "my-fullstack-app", options) => {
|
|
3388
|
+
console.log("");
|
|
3389
|
+
console.log(chalk.cyan.bold(" \u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557"));
|
|
3390
|
+
console.log(chalk.cyan.bold(" \u2551 Create Fullstack Scaffold App \u2551"));
|
|
3391
|
+
console.log(chalk.cyan.bold(" \u2551 React + Hono + Vite + Zustand + TS \u2551"));
|
|
3392
|
+
console.log(chalk.cyan.bold(" \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D"));
|
|
3393
|
+
console.log("");
|
|
3394
|
+
let preset = options.preset;
|
|
3395
|
+
if (!preset && process.stdin.isTTY) {
|
|
3396
|
+
const templateDir = path.join(__dirname2, "../template");
|
|
3397
|
+
const presets = await loadPresets(templateDir);
|
|
3398
|
+
preset = await select({
|
|
3399
|
+
message: "Choose a template preset:",
|
|
3400
|
+
choices: presets.map((p) => ({
|
|
3401
|
+
value: p.id,
|
|
3402
|
+
name: `${p.name} \u2014 ${p.description}`
|
|
3403
|
+
}))
|
|
3404
|
+
});
|
|
3405
|
+
}
|
|
3406
|
+
if (!preset) {
|
|
3407
|
+
preset = "fullstack-admin";
|
|
3408
|
+
}
|
|
3409
|
+
try {
|
|
3410
|
+
await createProject({
|
|
3411
|
+
projectName,
|
|
3412
|
+
currentDir: options.currentDir ?? false,
|
|
3413
|
+
preset,
|
|
3414
|
+
outputDir: options.outputDir,
|
|
3415
|
+
dryRun: options.dryRun ?? false
|
|
3416
|
+
});
|
|
3417
|
+
} catch (error) {
|
|
3418
|
+
if (error instanceof ScaffoldError) {
|
|
3419
|
+
console.error(chalk.red(` \u2716 ${error.message}`));
|
|
3420
|
+
process.exit(1);
|
|
3421
|
+
}
|
|
3422
|
+
throw error;
|
|
3423
|
+
}
|
|
3813
3424
|
}
|
|
3814
|
-
|
|
3815
|
-
program.
|
|
3816
|
-
const
|
|
3817
|
-
|
|
3818
|
-
|
|
3819
|
-
|
|
3425
|
+
);
|
|
3426
|
+
program.command("presets").description("List available template presets").action(async () => {
|
|
3427
|
+
const templateDir = path.join(__dirname2, "../template");
|
|
3428
|
+
const presets = await loadPresets(templateDir);
|
|
3429
|
+
console.log(chalk.cyan("\nAvailable presets:\n"));
|
|
3430
|
+
for (const preset of presets) {
|
|
3431
|
+
console.log(` ${chalk.green(preset.id.padEnd(20))} ${preset.name}`);
|
|
3432
|
+
console.log(` ${" ".repeat(20)} ${preset.description}`);
|
|
3433
|
+
console.log(` ${" ".repeat(20)} Modules: ${preset.modules.join(", ")}`);
|
|
3434
|
+
console.log();
|
|
3820
3435
|
}
|
|
3821
3436
|
});
|
|
3822
|
-
registerModules(program);
|
|
3823
3437
|
program.parse();
|
|
3824
3438
|
//# sourceMappingURL=index.js.map
|
|
3825
3439
|
//# sourceMappingURL=index.js.map
|