create-projx 1.6.1 → 1.6.3
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 +17 -3
- package/dist/{baseline-5XAJJ457.js → baseline-RXPDDEDD.js} +2 -6
- package/dist/{chunk-FTHX7ILT.js → chunk-LYPPFXGK.js} +140 -34
- package/dist/{chunk-TNI4XBVS.js → chunk-OBYYB6PR.js} +293 -150
- package/dist/index.js +708 -172
- package/dist/{utils-OOY5OZDX.js → utils-BXHJP6HF.js} +3 -1
- package/package.json +8 -9
- package/src/templates/README.md.ejs +1 -1
- package/src/templates/ci.yml.ejs +83 -66
- package/src/templates/pre-commit.ejs +53 -52
- package/src/templates/setup.sh.ejs +16 -16
package/dist/index.js
CHANGED
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
matchesSkip,
|
|
10
10
|
saveBaselineRef,
|
|
11
11
|
writeTemplateToDir
|
|
12
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-OBYYB6PR.js";
|
|
13
13
|
import {
|
|
14
14
|
COMPONENTS,
|
|
15
15
|
COMPONENT_MARKER,
|
|
@@ -33,7 +33,7 @@ import {
|
|
|
33
33
|
toTitle,
|
|
34
34
|
writeComponentMarker,
|
|
35
35
|
writeProjxConfig
|
|
36
|
-
} from "./chunk-
|
|
36
|
+
} from "./chunk-LYPPFXGK.js";
|
|
37
37
|
|
|
38
38
|
// src/index.ts
|
|
39
39
|
import { existsSync as existsSync11 } from "fs";
|
|
@@ -76,7 +76,9 @@ async function runPrompts(nameArg) {
|
|
|
76
76
|
if (components.length === 0) {
|
|
77
77
|
p.log.warn("No components selected. Creating an empty project.");
|
|
78
78
|
}
|
|
79
|
-
const hasJs = components.some(
|
|
79
|
+
const hasJs = components.some(
|
|
80
|
+
(c) => ["fastify", "frontend", "e2e"].includes(c)
|
|
81
|
+
);
|
|
80
82
|
let packageManager = "npm";
|
|
81
83
|
if (hasJs) {
|
|
82
84
|
const pm = await p.select({
|
|
@@ -101,11 +103,18 @@ async function scaffold(opts, dest, localRepo) {
|
|
|
101
103
|
const paths = Object.fromEntries(
|
|
102
104
|
opts.components.map((c) => [c, c])
|
|
103
105
|
);
|
|
104
|
-
const vars = {
|
|
106
|
+
const vars = {
|
|
107
|
+
projectName: name,
|
|
108
|
+
components: opts.components,
|
|
109
|
+
paths,
|
|
110
|
+
pm: pmCommands(pm)
|
|
111
|
+
};
|
|
105
112
|
const isLocal = !!localRepo;
|
|
106
113
|
await mkdir(dest, { recursive: true });
|
|
107
114
|
const dlSpinner = p2.spinner();
|
|
108
|
-
dlSpinner.start(
|
|
115
|
+
dlSpinner.start(
|
|
116
|
+
isLocal ? "Using local templates" : "Downloading latest templates"
|
|
117
|
+
);
|
|
109
118
|
const repoDir = await downloadRepo(localRepo).catch((err) => {
|
|
110
119
|
dlSpinner.stop("Failed.");
|
|
111
120
|
p2.log.error(String(err));
|
|
@@ -113,7 +122,9 @@ async function scaffold(opts, dest, localRepo) {
|
|
|
113
122
|
});
|
|
114
123
|
dlSpinner.stop(isLocal ? "Local templates loaded." : "Templates downloaded.");
|
|
115
124
|
try {
|
|
116
|
-
const pkg = JSON.parse(
|
|
125
|
+
const pkg = JSON.parse(
|
|
126
|
+
await readFile(join(repoDir, "cli/package.json"), "utf-8")
|
|
127
|
+
);
|
|
117
128
|
const version = pkg.version;
|
|
118
129
|
p2.log.info(`Scaffolding project in ${dest}`);
|
|
119
130
|
if (opts.git) {
|
|
@@ -121,7 +132,17 @@ async function scaffold(opts, dest, localRepo) {
|
|
|
121
132
|
}
|
|
122
133
|
const spinner7 = p2.spinner();
|
|
123
134
|
spinner7.start("Scaffolding project");
|
|
124
|
-
await applyTemplate(
|
|
135
|
+
await applyTemplate(
|
|
136
|
+
dest,
|
|
137
|
+
repoDir,
|
|
138
|
+
opts.components,
|
|
139
|
+
paths,
|
|
140
|
+
vars,
|
|
141
|
+
version,
|
|
142
|
+
void 0,
|
|
143
|
+
void 0,
|
|
144
|
+
true
|
|
145
|
+
);
|
|
125
146
|
spinner7.stop("Scaffold complete.");
|
|
126
147
|
if (opts.install) {
|
|
127
148
|
await installDeps(dest, opts.components, pm);
|
|
@@ -139,12 +160,14 @@ async function scaffold(opts, dest, localRepo) {
|
|
|
139
160
|
} finally {
|
|
140
161
|
await cleanupRepo(repoDir, isLocal);
|
|
141
162
|
}
|
|
142
|
-
p2.outro(
|
|
163
|
+
p2.outro(
|
|
164
|
+
`Done! Next steps:
|
|
143
165
|
|
|
144
166
|
cd ${name}
|
|
145
167
|
./setup.sh
|
|
146
168
|
|
|
147
|
-
Like projx? Star it: https://github.com/ukanhaupa/projx`
|
|
169
|
+
Like projx? Star it: https://github.com/ukanhaupa/projx`
|
|
170
|
+
);
|
|
148
171
|
}
|
|
149
172
|
async function installDeps(dest, components, pm) {
|
|
150
173
|
const cmds = pmCommands(pm);
|
|
@@ -168,7 +191,9 @@ async function installDeps(dest, components, pm) {
|
|
|
168
191
|
exec(cmds.install, join(dest, "fastify"));
|
|
169
192
|
spinner7.stop("Fastify dependencies installed.");
|
|
170
193
|
} else {
|
|
171
|
-
p2.log.warn(
|
|
194
|
+
p2.log.warn(
|
|
195
|
+
`${pm} not found \u2014 run 'cd fastify && ${cmds.install}' manually.`
|
|
196
|
+
);
|
|
172
197
|
}
|
|
173
198
|
break;
|
|
174
199
|
case "frontend":
|
|
@@ -177,7 +202,9 @@ async function installDeps(dest, components, pm) {
|
|
|
177
202
|
exec(cmds.install, join(dest, "frontend"));
|
|
178
203
|
spinner7.stop("Frontend dependencies installed.");
|
|
179
204
|
} else {
|
|
180
|
-
p2.log.warn(
|
|
205
|
+
p2.log.warn(
|
|
206
|
+
`${pm} not found \u2014 run 'cd frontend && ${cmds.install}' manually.`
|
|
207
|
+
);
|
|
181
208
|
}
|
|
182
209
|
break;
|
|
183
210
|
case "e2e":
|
|
@@ -186,7 +213,9 @@ async function installDeps(dest, components, pm) {
|
|
|
186
213
|
exec(cmds.install, join(dest, "e2e"));
|
|
187
214
|
spinner7.stop("E2E dependencies installed.");
|
|
188
215
|
} else {
|
|
189
|
-
p2.log.warn(
|
|
216
|
+
p2.log.warn(
|
|
217
|
+
`${pm} not found \u2014 run 'cd e2e && ${cmds.install}' manually.`
|
|
218
|
+
);
|
|
190
219
|
}
|
|
191
220
|
break;
|
|
192
221
|
case "mobile":
|
|
@@ -195,7 +224,9 @@ async function installDeps(dest, components, pm) {
|
|
|
195
224
|
exec("flutter pub get", join(dest, "mobile"));
|
|
196
225
|
spinner7.stop("Flutter dependencies installed.");
|
|
197
226
|
} else {
|
|
198
|
-
p2.log.warn(
|
|
227
|
+
p2.log.warn(
|
|
228
|
+
"Flutter not found \u2014 run 'cd mobile && flutter pub get' manually."
|
|
229
|
+
);
|
|
199
230
|
}
|
|
200
231
|
break;
|
|
201
232
|
case "infra":
|
|
@@ -237,17 +268,33 @@ async function update(cwd, localRepo) {
|
|
|
237
268
|
} catch {
|
|
238
269
|
}
|
|
239
270
|
const raw = await readProjxConfig(cwd);
|
|
240
|
-
const {
|
|
271
|
+
const {
|
|
272
|
+
components,
|
|
273
|
+
paths: componentPaths,
|
|
274
|
+
instances
|
|
275
|
+
} = await discoverComponentsFromMarkers(cwd);
|
|
276
|
+
const extraInstances = instances.filter(
|
|
277
|
+
(i) => componentPaths[i.type] !== i.path
|
|
278
|
+
);
|
|
241
279
|
const pendingConflicts = findFilesWithConflictMarkers(cwd);
|
|
242
280
|
if (pendingConflicts.length > 0) {
|
|
243
|
-
p3.log.warn(
|
|
281
|
+
p3.log.warn(
|
|
282
|
+
`Found ${pendingConflicts.length} file(s) with unresolved conflict markers from a prior update:`
|
|
283
|
+
);
|
|
244
284
|
for (const f of pendingConflicts) p3.log.info(` ${f}`);
|
|
245
285
|
p3.log.info("");
|
|
246
286
|
const resumeVersion = String(raw.version ?? "unknown");
|
|
247
|
-
const handled = await promptSkipLearning(
|
|
287
|
+
const handled = await promptSkipLearning(
|
|
288
|
+
cwd,
|
|
289
|
+
componentPaths,
|
|
290
|
+
resumeVersion,
|
|
291
|
+
pendingConflicts
|
|
292
|
+
);
|
|
248
293
|
if (!handled) {
|
|
249
294
|
p3.log.info("");
|
|
250
|
-
p3.log.info(
|
|
295
|
+
p3.log.info(
|
|
296
|
+
"Resolve manually with `git diff` then `git add` / `git checkout --`,"
|
|
297
|
+
);
|
|
251
298
|
p3.log.info("or re-run `npx create-projx update` to resume the prompt.");
|
|
252
299
|
}
|
|
253
300
|
return;
|
|
@@ -261,7 +308,9 @@ async function update(cwd, localRepo) {
|
|
|
261
308
|
process.exit(1);
|
|
262
309
|
}
|
|
263
310
|
if (Object.keys(raw).length > 0) {
|
|
264
|
-
p3.log.info(
|
|
311
|
+
p3.log.info(
|
|
312
|
+
`Found .projx (v${raw.version ?? "unknown"}, components: ${components.join(", ")})`
|
|
313
|
+
);
|
|
265
314
|
} else {
|
|
266
315
|
p3.log.warn("No .projx file found. Detected components from markers.");
|
|
267
316
|
p3.log.info(`Detected: ${components.join(", ")}`);
|
|
@@ -279,7 +328,9 @@ async function update(cwd, localRepo) {
|
|
|
279
328
|
}
|
|
280
329
|
}
|
|
281
330
|
const dlSpinner = p3.spinner();
|
|
282
|
-
dlSpinner.start(
|
|
331
|
+
dlSpinner.start(
|
|
332
|
+
isLocal ? "Using local templates" : "Downloading latest templates"
|
|
333
|
+
);
|
|
283
334
|
const repoDir = await downloadRepo(localRepo).catch((err) => {
|
|
284
335
|
dlSpinner.stop("Failed.");
|
|
285
336
|
p3.log.error(String(err));
|
|
@@ -287,43 +338,88 @@ async function update(cwd, localRepo) {
|
|
|
287
338
|
});
|
|
288
339
|
dlSpinner.stop(isLocal ? "Local templates loaded." : "Templates downloaded.");
|
|
289
340
|
try {
|
|
290
|
-
const pkg = JSON.parse(
|
|
341
|
+
const pkg = JSON.parse(
|
|
342
|
+
await readFile2(join2(repoDir, "cli/package.json"), "utf-8")
|
|
343
|
+
);
|
|
291
344
|
const version = pkg.version;
|
|
292
345
|
const name = detectProjectName(cwd, components, componentPaths);
|
|
293
346
|
const recordedPm = raw.packageManager;
|
|
294
347
|
const detectedPm = detectPackageManagerFromComponents(cwd, componentPaths);
|
|
295
348
|
const pm = detectedPm ?? recordedPm ?? "npm";
|
|
296
349
|
if (detectedPm && recordedPm && detectedPm !== recordedPm) {
|
|
297
|
-
p3.log.warn(
|
|
350
|
+
p3.log.warn(
|
|
351
|
+
`packageManager mismatch: .projx says "${recordedPm}" but lockfile is "${detectedPm}". Using "${detectedPm}".`
|
|
352
|
+
);
|
|
298
353
|
await writeProjxConfig(cwd, { ...raw, packageManager: detectedPm });
|
|
299
354
|
} else if (detectedPm && !recordedPm) {
|
|
300
355
|
await writeProjxConfig(cwd, { ...raw, packageManager: detectedPm });
|
|
301
356
|
}
|
|
302
|
-
const nameOverrides = await detectPackageNameOverrides(
|
|
303
|
-
|
|
357
|
+
const nameOverrides = await detectPackageNameOverrides(
|
|
358
|
+
cwd,
|
|
359
|
+
components,
|
|
360
|
+
componentPaths
|
|
361
|
+
);
|
|
362
|
+
const vars = {
|
|
363
|
+
projectName: name,
|
|
364
|
+
components,
|
|
365
|
+
paths: componentPaths,
|
|
366
|
+
instances,
|
|
367
|
+
pm: pmCommands(pm),
|
|
368
|
+
nameOverrides
|
|
369
|
+
};
|
|
304
370
|
const spinner7 = p3.spinner();
|
|
305
371
|
spinner7.start("Applying template update");
|
|
306
372
|
const rootSkip = Array.isArray(raw.skip) ? raw.skip : [];
|
|
307
373
|
const isLegacyMigration = !raw.defaultsApplied;
|
|
308
374
|
if (isLegacyMigration) {
|
|
309
|
-
p3.log.info(
|
|
375
|
+
p3.log.info(
|
|
376
|
+
"Legacy project detected \u2014 applying default skip patterns for user-owned files."
|
|
377
|
+
);
|
|
310
378
|
}
|
|
311
|
-
const result = await applyTemplate(
|
|
379
|
+
const result = await applyTemplate(
|
|
380
|
+
cwd,
|
|
381
|
+
repoDir,
|
|
382
|
+
components,
|
|
383
|
+
componentPaths,
|
|
384
|
+
vars,
|
|
385
|
+
version,
|
|
386
|
+
componentSkips,
|
|
387
|
+
rootSkip,
|
|
388
|
+
isLegacyMigration,
|
|
389
|
+
extraInstances
|
|
390
|
+
);
|
|
312
391
|
spinner7.stop("Template applied.");
|
|
313
|
-
const pinnedUpdates = await findPinnedFilesWithUpdates(
|
|
392
|
+
const pinnedUpdates = await findPinnedFilesWithUpdates(
|
|
393
|
+
cwd,
|
|
394
|
+
repoDir,
|
|
395
|
+
components,
|
|
396
|
+
componentPaths,
|
|
397
|
+
vars,
|
|
398
|
+
version,
|
|
399
|
+
componentSkips,
|
|
400
|
+
rootSkip
|
|
401
|
+
);
|
|
314
402
|
if (pinnedUpdates.length > 0) {
|
|
315
403
|
p3.log.info("");
|
|
316
|
-
p3.log.info(
|
|
404
|
+
p3.log.info(
|
|
405
|
+
`${pinnedUpdates.length} pinned file(s) have template updates available:`
|
|
406
|
+
);
|
|
317
407
|
for (const f of pinnedUpdates) p3.log.info(` ${f}`);
|
|
318
|
-
p3.log.info(
|
|
408
|
+
p3.log.info(
|
|
409
|
+
"Run `npx create-projx unpin <file> && npx create-projx update` to opt in."
|
|
410
|
+
);
|
|
319
411
|
}
|
|
320
412
|
if (result.status === "merged") {
|
|
321
413
|
saveBaselineRef(cwd);
|
|
322
|
-
p3.log.success(
|
|
414
|
+
p3.log.success(
|
|
415
|
+
`${result.mergedFiles?.length ?? 0} file(s) merged cleanly.`
|
|
416
|
+
);
|
|
323
417
|
p3.outro(`Updated to template v${version}.`);
|
|
324
418
|
} else if (result.status === "conflicts") {
|
|
325
419
|
if (result.mergedFiles && result.mergedFiles.length > 0) {
|
|
326
|
-
p3.log.success(
|
|
420
|
+
p3.log.success(
|
|
421
|
+
`${result.mergedFiles.length} file(s) merged cleanly and staged.`
|
|
422
|
+
);
|
|
327
423
|
}
|
|
328
424
|
const conflictCount = result.conflictedFiles?.length ?? 0;
|
|
329
425
|
if (conflictCount > 0) {
|
|
@@ -332,13 +428,20 @@ async function update(cwd, localRepo) {
|
|
|
332
428
|
p3.log.info(` ${f}`);
|
|
333
429
|
}
|
|
334
430
|
}
|
|
335
|
-
const handled = await promptSkipLearning(
|
|
431
|
+
const handled = await promptSkipLearning(
|
|
432
|
+
cwd,
|
|
433
|
+
componentPaths,
|
|
434
|
+
version,
|
|
435
|
+
result.conflictedFiles ?? []
|
|
436
|
+
);
|
|
336
437
|
if (!handled) {
|
|
337
438
|
p3.log.info("");
|
|
338
439
|
p3.log.info("Review: git diff");
|
|
339
440
|
p3.log.info("Keep: git add <file>");
|
|
340
441
|
p3.log.info("Discard: git checkout -- <file>");
|
|
341
|
-
p3.log.info(
|
|
442
|
+
p3.log.info(
|
|
443
|
+
`Commit: git add . && git commit -m "projx: update to v${version}"`
|
|
444
|
+
);
|
|
342
445
|
p3.outro(`Template v${version} applied. Review with git diff.`);
|
|
343
446
|
}
|
|
344
447
|
} else {
|
|
@@ -371,7 +474,7 @@ function hasUncommittedChanges(cwd) {
|
|
|
371
474
|
async function findPinnedFilesWithUpdates(cwd, repoDir, components, componentPaths, vars, version, componentSkips, rootSkip) {
|
|
372
475
|
const { mkdir: mkdir5, rm: rm2, readFile: readFile7 } = await import("fs/promises");
|
|
373
476
|
const { tmpdir: tmpdir2 } = await import("os");
|
|
374
|
-
const { writeTemplateToDir: writeTemplateToDir2 } = await import("./baseline-
|
|
477
|
+
const { writeTemplateToDir: writeTemplateToDir2 } = await import("./baseline-RXPDDEDD.js");
|
|
375
478
|
const config = await readProjxConfig(cwd);
|
|
376
479
|
const rootPinned = Array.isArray(config.skip) ? config.skip : [];
|
|
377
480
|
const componentPinned = [];
|
|
@@ -388,11 +491,19 @@ async function findPinnedFilesWithUpdates(cwd, repoDir, components, componentPat
|
|
|
388
491
|
void componentSkips;
|
|
389
492
|
void rootSkip;
|
|
390
493
|
try {
|
|
391
|
-
await writeTemplateToDir2(
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
494
|
+
await writeTemplateToDir2(
|
|
495
|
+
tmpTemplate,
|
|
496
|
+
repoDir,
|
|
497
|
+
components,
|
|
498
|
+
componentPaths,
|
|
499
|
+
vars,
|
|
500
|
+
version,
|
|
501
|
+
{
|
|
502
|
+
componentSkips: {},
|
|
503
|
+
rootSkip: [],
|
|
504
|
+
realCwd: tmpTemplate
|
|
505
|
+
}
|
|
506
|
+
);
|
|
396
507
|
const updates = [];
|
|
397
508
|
for (const file of rootPinned) {
|
|
398
509
|
const tmplPath = join2(tmpTemplate, file);
|
|
@@ -439,20 +550,33 @@ async function promptSkipLearning(cwd, componentPaths, version, conflictedFiles)
|
|
|
439
550
|
});
|
|
440
551
|
if (changedFiles.length === 0) return false;
|
|
441
552
|
if (!process.stdin.isTTY) {
|
|
442
|
-
p3.log.info(
|
|
443
|
-
|
|
553
|
+
p3.log.info(
|
|
554
|
+
"Non-interactive: skipping prompt. Resolve conflicts manually with `git diff` then `git add`."
|
|
555
|
+
);
|
|
556
|
+
p3.log.info(
|
|
557
|
+
"Re-run `npx create-projx update` later to interactively decide which files to keep."
|
|
558
|
+
);
|
|
444
559
|
return false;
|
|
445
560
|
}
|
|
446
|
-
const statusOutput = execSync("git status --porcelain", {
|
|
561
|
+
const statusOutput = execSync("git status --porcelain", {
|
|
562
|
+
cwd,
|
|
563
|
+
stdio: "pipe"
|
|
564
|
+
}).toString().trim();
|
|
447
565
|
const entries = statusOutput.split("\n").filter(Boolean).map((line) => ({
|
|
448
566
|
status: line.slice(0, 2).trim(),
|
|
449
567
|
file: line.slice(3).trim()
|
|
450
568
|
}));
|
|
451
569
|
p3.log.warn(`${changedFiles.length} file(s) have conflicts to resolve.`);
|
|
452
|
-
p3.log.info(
|
|
570
|
+
p3.log.info(
|
|
571
|
+
"Each file is currently in your working tree with conflict markers."
|
|
572
|
+
);
|
|
453
573
|
p3.log.info("");
|
|
454
|
-
p3.log.info(
|
|
455
|
-
|
|
574
|
+
p3.log.info(
|
|
575
|
+
"CHECKED = keep your version, resolve markers manually, commit when ready"
|
|
576
|
+
);
|
|
577
|
+
p3.log.info(
|
|
578
|
+
"UNCHECKED = discard template's changes AND skip this file on future updates"
|
|
579
|
+
);
|
|
456
580
|
p3.log.info("");
|
|
457
581
|
const selected = await p3.multiselect({
|
|
458
582
|
message: "Which files do you want to KEEP?",
|
|
@@ -484,10 +608,10 @@ async function promptSkipLearning(cwd, componentPaths, version, conflictedFiles)
|
|
|
484
608
|
);
|
|
485
609
|
}
|
|
486
610
|
if (kept.size > 0) {
|
|
487
|
-
p3.log.info(`${kept.size} file(s) kept with conflict markers \u2014 resolve and commit:`);
|
|
488
611
|
p3.log.info(
|
|
489
|
-
|
|
612
|
+
`${kept.size} file(s) kept with conflict markers \u2014 resolve and commit:`
|
|
490
613
|
);
|
|
614
|
+
p3.log.info(` git add . && git commit -m "projx: update to v${version}"`);
|
|
491
615
|
p3.outro(`Template v${version} applied.`);
|
|
492
616
|
} else {
|
|
493
617
|
p3.outro("All template changes discarded. Skip list updated.");
|
|
@@ -536,15 +660,38 @@ import { copyFileSync as copyFileSync2, existsSync as existsSync3 } from "fs";
|
|
|
536
660
|
import { readFile as readFile3 } from "fs/promises";
|
|
537
661
|
import { join as join3 } from "path";
|
|
538
662
|
import * as p4 from "@clack/prompts";
|
|
539
|
-
async function add(cwd, newComponents, localRepo, skipInstall = false) {
|
|
663
|
+
async function add(cwd, newComponents, localRepo, skipInstall = false, customName) {
|
|
540
664
|
p4.intro("projx add");
|
|
541
665
|
const isLocal = !!localRepo;
|
|
542
666
|
if (!existsSync3(join3(cwd, ".projx"))) {
|
|
543
|
-
p4.log.error(
|
|
667
|
+
p4.log.error(
|
|
668
|
+
"No .projx file found. Run 'npx create-projx <name>' to create a project first."
|
|
669
|
+
);
|
|
544
670
|
process.exit(1);
|
|
545
671
|
}
|
|
546
672
|
const config = await readProjxConfig(cwd);
|
|
547
673
|
const { components: existing } = await discoverComponentsFromMarkers(cwd);
|
|
674
|
+
if (customName) {
|
|
675
|
+
if (newComponents.length !== 1) {
|
|
676
|
+
throw new Error(
|
|
677
|
+
"--name can only be used when adding a single component type."
|
|
678
|
+
);
|
|
679
|
+
}
|
|
680
|
+
const targetDir = join3(cwd, customName);
|
|
681
|
+
if (existsSync3(targetDir)) {
|
|
682
|
+
throw new Error(`Directory '${customName}' already exists.`);
|
|
683
|
+
}
|
|
684
|
+
return await addInstance(
|
|
685
|
+
cwd,
|
|
686
|
+
newComponents[0],
|
|
687
|
+
customName,
|
|
688
|
+
config,
|
|
689
|
+
existing,
|
|
690
|
+
localRepo,
|
|
691
|
+
skipInstall,
|
|
692
|
+
isLocal
|
|
693
|
+
);
|
|
694
|
+
}
|
|
548
695
|
const alreadyExists = newComponents.filter((c) => existing.includes(c));
|
|
549
696
|
if (alreadyExists.length > 0) {
|
|
550
697
|
p4.log.warn(`Already present: ${alreadyExists.join(", ")}. Skipping those.`);
|
|
@@ -556,7 +703,9 @@ async function add(cwd, newComponents, localRepo, skipInstall = false) {
|
|
|
556
703
|
}
|
|
557
704
|
p4.log.info(`Adding: ${toAdd.join(", ")}`);
|
|
558
705
|
const dlSpinner = p4.spinner();
|
|
559
|
-
dlSpinner.start(
|
|
706
|
+
dlSpinner.start(
|
|
707
|
+
isLocal ? "Using local templates" : "Downloading latest templates"
|
|
708
|
+
);
|
|
560
709
|
const repoDir = await downloadRepo(localRepo).catch((err) => {
|
|
561
710
|
dlSpinner.stop("Failed.");
|
|
562
711
|
p4.log.error(String(err));
|
|
@@ -568,17 +717,42 @@ async function add(cwd, newComponents, localRepo, skipInstall = false) {
|
|
|
568
717
|
const existingPaths = await discoverComponentPaths(cwd, existing);
|
|
569
718
|
const paths = { ...existingPaths };
|
|
570
719
|
for (const c of toAdd) paths[c] = c;
|
|
720
|
+
const { instances: existingInstances } = await discoverComponentsFromMarkers(cwd);
|
|
721
|
+
const instances = [
|
|
722
|
+
...existingInstances,
|
|
723
|
+
...toAdd.map((c) => ({ type: c, path: c }))
|
|
724
|
+
];
|
|
571
725
|
const pm = config.packageManager ?? "npm";
|
|
572
726
|
const name = detectProjectName(cwd, existing, paths);
|
|
573
|
-
const vars = {
|
|
574
|
-
|
|
727
|
+
const vars = {
|
|
728
|
+
projectName: name,
|
|
729
|
+
components: allComponents,
|
|
730
|
+
paths,
|
|
731
|
+
instances,
|
|
732
|
+
pm: pmCommands(pm)
|
|
733
|
+
};
|
|
734
|
+
const pkg = JSON.parse(
|
|
735
|
+
await readFile3(join3(repoDir, "cli/package.json"), "utf-8")
|
|
736
|
+
);
|
|
575
737
|
const version = pkg.version;
|
|
576
738
|
const spinner7 = p4.spinner();
|
|
577
739
|
spinner7.start("Adding components");
|
|
578
|
-
await writeTemplateToDir(
|
|
740
|
+
await writeTemplateToDir(
|
|
741
|
+
cwd,
|
|
742
|
+
repoDir,
|
|
743
|
+
allComponents,
|
|
744
|
+
paths,
|
|
745
|
+
vars,
|
|
746
|
+
version,
|
|
747
|
+
{ realCwd: cwd }
|
|
748
|
+
);
|
|
579
749
|
spinner7.stop("Components added.");
|
|
580
750
|
if (!skipInstall) {
|
|
581
|
-
await installDeps2(
|
|
751
|
+
await installDeps2(
|
|
752
|
+
cwd,
|
|
753
|
+
toAdd.map((c) => ({ type: c, path: c })),
|
|
754
|
+
pm
|
|
755
|
+
);
|
|
582
756
|
}
|
|
583
757
|
for (const component of toAdd) {
|
|
584
758
|
const example = join3(cwd, component, ".env.example");
|
|
@@ -590,70 +764,175 @@ async function add(cwd, newComponents, localRepo, skipInstall = false) {
|
|
|
590
764
|
}
|
|
591
765
|
}
|
|
592
766
|
}
|
|
593
|
-
p4.outro(
|
|
767
|
+
p4.outro(
|
|
768
|
+
`Added ${toAdd.join(", ")}.
|
|
594
769
|
|
|
595
|
-
Like projx? Star it: https://github.com/ukanhaupa/projx`
|
|
770
|
+
Like projx? Star it: https://github.com/ukanhaupa/projx`
|
|
771
|
+
);
|
|
596
772
|
} finally {
|
|
597
773
|
await cleanupRepo(repoDir, isLocal);
|
|
598
774
|
}
|
|
599
775
|
}
|
|
600
|
-
async function
|
|
776
|
+
async function addInstance(cwd, type, customName, config, existing, localRepo, skipInstall, isLocal) {
|
|
777
|
+
p4.log.info(`Adding ${type} instance at ${customName}/`);
|
|
778
|
+
const dlSpinner = p4.spinner();
|
|
779
|
+
dlSpinner.start(
|
|
780
|
+
isLocal ? "Using local templates" : "Downloading latest templates"
|
|
781
|
+
);
|
|
782
|
+
const repoDir = await downloadRepo(localRepo).catch((err) => {
|
|
783
|
+
dlSpinner.stop("Failed.");
|
|
784
|
+
throw err;
|
|
785
|
+
});
|
|
786
|
+
dlSpinner.stop(isLocal ? "Local templates loaded." : "Templates downloaded.");
|
|
787
|
+
try {
|
|
788
|
+
const existingPaths = await discoverComponentPaths(cwd, existing);
|
|
789
|
+
const paths = { ...existingPaths };
|
|
790
|
+
const { instances: existingInstances } = await discoverComponentsFromMarkers(cwd);
|
|
791
|
+
const newInstance = { type, path: customName };
|
|
792
|
+
const instances = [...existingInstances, newInstance];
|
|
793
|
+
const pm = config.packageManager ?? "npm";
|
|
794
|
+
const name = detectProjectName(cwd, existing, existingPaths);
|
|
795
|
+
const vars = {
|
|
796
|
+
projectName: name,
|
|
797
|
+
components: existing,
|
|
798
|
+
paths,
|
|
799
|
+
instances,
|
|
800
|
+
pm: pmCommands(pm)
|
|
801
|
+
};
|
|
802
|
+
const pkg = JSON.parse(
|
|
803
|
+
await readFile3(join3(repoDir, "cli/package.json"), "utf-8")
|
|
804
|
+
);
|
|
805
|
+
const version = pkg.version;
|
|
806
|
+
const INSTANCE_AWARE_ROOT = /* @__PURE__ */ new Set([
|
|
807
|
+
".github/workflows/ci.yml",
|
|
808
|
+
".githooks/pre-commit",
|
|
809
|
+
"setup.sh"
|
|
810
|
+
]);
|
|
811
|
+
const rawSkip = Array.isArray(config.skip) ? config.skip : [];
|
|
812
|
+
const rootSkip = rawSkip.filter((p11) => !INSTANCE_AWARE_ROOT.has(p11));
|
|
813
|
+
const componentSkips = {};
|
|
814
|
+
for (const inst of existingInstances) {
|
|
815
|
+
const m = await readComponentMarker(join3(cwd, inst.path));
|
|
816
|
+
if (m?.skip && m.skip.length > 0) componentSkips[inst.type] = m.skip;
|
|
817
|
+
}
|
|
818
|
+
const spinner7 = p4.spinner();
|
|
819
|
+
spinner7.start(`Scaffolding ${customName}/`);
|
|
820
|
+
const result = await applyTemplate(
|
|
821
|
+
cwd,
|
|
822
|
+
repoDir,
|
|
823
|
+
existing,
|
|
824
|
+
paths,
|
|
825
|
+
vars,
|
|
826
|
+
version,
|
|
827
|
+
componentSkips,
|
|
828
|
+
rootSkip,
|
|
829
|
+
false,
|
|
830
|
+
[newInstance],
|
|
831
|
+
[newInstance]
|
|
832
|
+
);
|
|
833
|
+
spinner7.stop(`Scaffolded ${customName}/.`);
|
|
834
|
+
if (result.status === "merged") {
|
|
835
|
+
p4.log.success(
|
|
836
|
+
`${result.mergedFiles?.length ?? 0} root file(s) merged cleanly.`
|
|
837
|
+
);
|
|
838
|
+
} else if (result.status === "conflicts") {
|
|
839
|
+
const conflictCount = result.conflictedFiles?.length ?? 0;
|
|
840
|
+
if (conflictCount > 0) {
|
|
841
|
+
p4.log.warn(`${conflictCount} root file(s) need manual review:`);
|
|
842
|
+
for (const f of result.conflictedFiles) p4.log.info(` ${f}`);
|
|
843
|
+
p4.log.info("Review: git diff");
|
|
844
|
+
p4.log.info("Keep: git add <file>");
|
|
845
|
+
p4.log.info("Discard: git checkout -- <file>");
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
if (!skipInstall) {
|
|
849
|
+
await installDeps2(cwd, [{ type, path: customName }], pm);
|
|
850
|
+
}
|
|
851
|
+
const example = join3(cwd, customName, ".env.example");
|
|
852
|
+
const env = join3(cwd, customName, ".env");
|
|
853
|
+
if (existsSync3(example) && !existsSync3(env)) {
|
|
854
|
+
try {
|
|
855
|
+
copyFileSync2(example, env);
|
|
856
|
+
} catch {
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
p4.outro(`Added ${type} instance at ${customName}/.`);
|
|
860
|
+
} finally {
|
|
861
|
+
await cleanupRepo(repoDir, isLocal);
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
async function installDeps2(dest, instances, pm) {
|
|
601
865
|
const cmds = pmCommands(pm);
|
|
602
866
|
const pmBin = pm === "bun" ? "bun" : pm;
|
|
603
|
-
for (const
|
|
867
|
+
for (const { type, path } of instances) {
|
|
868
|
+
const dir = join3(dest, path);
|
|
604
869
|
const spinner7 = p4.spinner();
|
|
605
870
|
try {
|
|
606
|
-
switch (
|
|
871
|
+
switch (type) {
|
|
607
872
|
case "fastapi":
|
|
608
873
|
if (hasCommand("uv")) {
|
|
609
|
-
spinner7.start(
|
|
610
|
-
exec("uv sync --all-extras",
|
|
611
|
-
spinner7.stop(
|
|
874
|
+
spinner7.start(`Installing FastAPI dependencies (${path}/)`);
|
|
875
|
+
exec("uv sync --all-extras", dir);
|
|
876
|
+
spinner7.stop(`FastAPI dependencies installed (${path}/).`);
|
|
612
877
|
} else {
|
|
613
|
-
p4.log.warn(
|
|
878
|
+
p4.log.warn(`uv not found \u2014 run 'cd ${path} && uv sync' manually.`);
|
|
614
879
|
}
|
|
615
880
|
break;
|
|
616
881
|
case "fastify":
|
|
617
882
|
if (hasCommand(pmBin)) {
|
|
618
|
-
spinner7.start(
|
|
619
|
-
|
|
620
|
-
|
|
883
|
+
spinner7.start(
|
|
884
|
+
`Installing Fastify dependencies (${path}/, ${cmds.install})`
|
|
885
|
+
);
|
|
886
|
+
exec(cmds.install, dir);
|
|
887
|
+
spinner7.stop(`Fastify dependencies installed (${path}/).`);
|
|
621
888
|
} else {
|
|
622
|
-
p4.log.warn(
|
|
889
|
+
p4.log.warn(
|
|
890
|
+
`${pm} not found \u2014 run 'cd ${path} && ${cmds.install}' manually.`
|
|
891
|
+
);
|
|
623
892
|
}
|
|
624
893
|
break;
|
|
625
894
|
case "frontend":
|
|
626
895
|
if (hasCommand(pmBin)) {
|
|
627
|
-
spinner7.start(
|
|
628
|
-
|
|
629
|
-
|
|
896
|
+
spinner7.start(
|
|
897
|
+
`Installing Frontend dependencies (${path}/, ${cmds.install})`
|
|
898
|
+
);
|
|
899
|
+
exec(cmds.install, dir);
|
|
900
|
+
spinner7.stop(`Frontend dependencies installed (${path}/).`);
|
|
630
901
|
} else {
|
|
631
|
-
p4.log.warn(
|
|
902
|
+
p4.log.warn(
|
|
903
|
+
`${pm} not found \u2014 run 'cd ${path} && ${cmds.install}' manually.`
|
|
904
|
+
);
|
|
632
905
|
}
|
|
633
906
|
break;
|
|
634
907
|
case "e2e":
|
|
635
908
|
if (hasCommand(pmBin)) {
|
|
636
|
-
spinner7.start(
|
|
637
|
-
|
|
638
|
-
|
|
909
|
+
spinner7.start(
|
|
910
|
+
`Installing E2E dependencies (${path}/, ${cmds.install})`
|
|
911
|
+
);
|
|
912
|
+
exec(cmds.install, dir);
|
|
913
|
+
spinner7.stop(`E2E dependencies installed (${path}/).`);
|
|
639
914
|
} else {
|
|
640
|
-
p4.log.warn(
|
|
915
|
+
p4.log.warn(
|
|
916
|
+
`${pm} not found \u2014 run 'cd ${path} && ${cmds.install}' manually.`
|
|
917
|
+
);
|
|
641
918
|
}
|
|
642
919
|
break;
|
|
643
920
|
case "mobile":
|
|
644
921
|
if (hasCommand("flutter")) {
|
|
645
|
-
spinner7.start(
|
|
646
|
-
exec("flutter pub get",
|
|
647
|
-
spinner7.stop(
|
|
922
|
+
spinner7.start(`Installing Flutter dependencies (${path}/)`);
|
|
923
|
+
exec("flutter pub get", dir);
|
|
924
|
+
spinner7.stop(`Flutter dependencies installed (${path}/).`);
|
|
648
925
|
} else {
|
|
649
|
-
p4.log.warn(
|
|
926
|
+
p4.log.warn(
|
|
927
|
+
`Flutter not found \u2014 run 'cd ${path} && flutter pub get' manually.`
|
|
928
|
+
);
|
|
650
929
|
}
|
|
651
930
|
break;
|
|
652
931
|
case "infra":
|
|
653
932
|
break;
|
|
654
933
|
}
|
|
655
934
|
} catch {
|
|
656
|
-
spinner7.stop(`Failed to install ${
|
|
935
|
+
spinner7.stop(`Failed to install ${type} dependencies (${path}/).`);
|
|
657
936
|
}
|
|
658
937
|
}
|
|
659
938
|
}
|
|
@@ -672,7 +951,9 @@ import { join as join4 } from "path";
|
|
|
672
951
|
async function detectComponents(cwd) {
|
|
673
952
|
const results = [];
|
|
674
953
|
const entries = await readdir(cwd, { withFileTypes: true });
|
|
675
|
-
const dirs = entries.filter(
|
|
954
|
+
const dirs = entries.filter(
|
|
955
|
+
(e) => e.isDirectory() && !e.name.startsWith(".") && !EXCLUDE.has(e.name)
|
|
956
|
+
).map((e) => e.name);
|
|
676
957
|
for (const dir of dirs) {
|
|
677
958
|
const full = join4(cwd, dir);
|
|
678
959
|
const detections = await scanDirectory(full, dir);
|
|
@@ -754,11 +1035,15 @@ async function init(cwd, localRepo) {
|
|
|
754
1035
|
p5.intro("projx init");
|
|
755
1036
|
const isLocal = !!localRepo;
|
|
756
1037
|
if (existsSync5(join5(cwd, ".projx"))) {
|
|
757
|
-
p5.log.error(
|
|
1038
|
+
p5.log.error(
|
|
1039
|
+
"This project is already initialized. Use 'npx create-projx update' or 'npx create-projx add' instead."
|
|
1040
|
+
);
|
|
758
1041
|
process.exit(1);
|
|
759
1042
|
}
|
|
760
1043
|
if (!isGitRepo2(cwd)) {
|
|
761
|
-
p5.log.error(
|
|
1044
|
+
p5.log.error(
|
|
1045
|
+
`projx init requires a git repo. Run 'git init && git add -A && git commit -m "initial"' first.`
|
|
1046
|
+
);
|
|
762
1047
|
process.exit(1);
|
|
763
1048
|
}
|
|
764
1049
|
if (hasUncommittedChanges2(cwd)) {
|
|
@@ -785,7 +1070,9 @@ async function init(cwd, localRepo) {
|
|
|
785
1070
|
const paths = Object.fromEntries(
|
|
786
1071
|
confirmed.map((c) => [c.component, c.directory])
|
|
787
1072
|
);
|
|
788
|
-
const hasJs = components.some(
|
|
1073
|
+
const hasJs = components.some(
|
|
1074
|
+
(c) => ["fastify", "frontend", "e2e"].includes(c)
|
|
1075
|
+
);
|
|
789
1076
|
let pm = "npm";
|
|
790
1077
|
if (hasJs) {
|
|
791
1078
|
const detected2 = detectPackageManager(cwd);
|
|
@@ -803,9 +1090,16 @@ async function init(cwd, localRepo) {
|
|
|
803
1090
|
}
|
|
804
1091
|
}
|
|
805
1092
|
const projectName = toKebab(cwd.split("/").pop());
|
|
806
|
-
const vars = {
|
|
1093
|
+
const vars = {
|
|
1094
|
+
projectName,
|
|
1095
|
+
components,
|
|
1096
|
+
paths,
|
|
1097
|
+
pm: pmCommands(pm)
|
|
1098
|
+
};
|
|
807
1099
|
const dlSpinner = p5.spinner();
|
|
808
|
-
dlSpinner.start(
|
|
1100
|
+
dlSpinner.start(
|
|
1101
|
+
isLocal ? "Using local templates" : "Downloading latest templates"
|
|
1102
|
+
);
|
|
809
1103
|
const repoDir = await downloadRepo(localRepo).catch((err) => {
|
|
810
1104
|
dlSpinner.stop("Failed.");
|
|
811
1105
|
p5.log.error(String(err));
|
|
@@ -813,11 +1107,23 @@ async function init(cwd, localRepo) {
|
|
|
813
1107
|
});
|
|
814
1108
|
dlSpinner.stop(isLocal ? "Local templates loaded." : "Templates downloaded.");
|
|
815
1109
|
try {
|
|
816
|
-
const pkg = JSON.parse(
|
|
1110
|
+
const pkg = JSON.parse(
|
|
1111
|
+
await readFile4(join5(repoDir, "cli/package.json"), "utf-8")
|
|
1112
|
+
);
|
|
817
1113
|
const version = pkg.version;
|
|
818
1114
|
const applySpinner = p5.spinner();
|
|
819
1115
|
applySpinner.start("Applying template");
|
|
820
|
-
const result = await applyTemplate(
|
|
1116
|
+
const result = await applyTemplate(
|
|
1117
|
+
cwd,
|
|
1118
|
+
repoDir,
|
|
1119
|
+
components,
|
|
1120
|
+
paths,
|
|
1121
|
+
vars,
|
|
1122
|
+
version,
|
|
1123
|
+
void 0,
|
|
1124
|
+
void 0,
|
|
1125
|
+
true
|
|
1126
|
+
);
|
|
821
1127
|
applySpinner.stop("Template applied.");
|
|
822
1128
|
if (existsSync5(join5(cwd, ".githooks"))) {
|
|
823
1129
|
try {
|
|
@@ -829,19 +1135,27 @@ async function init(cwd, localRepo) {
|
|
|
829
1135
|
saveBaselineRef(cwd);
|
|
830
1136
|
}
|
|
831
1137
|
if (result.status === "conflicts") {
|
|
832
|
-
p5.log.warn(
|
|
1138
|
+
p5.log.warn(
|
|
1139
|
+
"Some template files differ from your code. Changes written directly."
|
|
1140
|
+
);
|
|
833
1141
|
p5.log.info("Review changes:");
|
|
834
1142
|
p5.log.info(" git diff");
|
|
835
1143
|
p5.log.info("");
|
|
836
1144
|
p5.log.info("Keep a change: git add <file>");
|
|
837
1145
|
p5.log.info("Discard a change: git checkout -- <file>");
|
|
838
|
-
p5.log.info(
|
|
1146
|
+
p5.log.info(
|
|
1147
|
+
'Commit when ready: git add . && git commit -m "projx: init"'
|
|
1148
|
+
);
|
|
839
1149
|
p5.log.info("");
|
|
840
1150
|
p5.log.info("To skip files on future updates, add to .projx-component:");
|
|
841
1151
|
p5.log.info(' { "skip": ["src/**", "tests/**"] }');
|
|
842
|
-
p5.outro(
|
|
1152
|
+
p5.outro(
|
|
1153
|
+
"Template applied. Review with git diff.\n\n Like projx? Star it: https://github.com/ukanhaupa/projx"
|
|
1154
|
+
);
|
|
843
1155
|
} else {
|
|
844
|
-
p5.outro(
|
|
1156
|
+
p5.outro(
|
|
1157
|
+
"Project initialized.\n\n Like projx? Star it: https://github.com/ukanhaupa/projx"
|
|
1158
|
+
);
|
|
845
1159
|
}
|
|
846
1160
|
} finally {
|
|
847
1161
|
await cleanupRepo(repoDir, isLocal);
|
|
@@ -940,7 +1254,10 @@ async function pin(cwd, patterns) {
|
|
|
940
1254
|
p6.log.warn(`Cannot pin ${pattern} \u2014 config files are managed by projx.`);
|
|
941
1255
|
continue;
|
|
942
1256
|
}
|
|
943
|
-
const { scope, component, relative } = classifyPattern(
|
|
1257
|
+
const { scope, component, relative } = classifyPattern(
|
|
1258
|
+
pattern,
|
|
1259
|
+
componentPaths
|
|
1260
|
+
);
|
|
944
1261
|
if (scope === "component" && component) {
|
|
945
1262
|
if (!componentAdds[component]) componentAdds[component] = [];
|
|
946
1263
|
componentAdds[component].push(relative);
|
|
@@ -989,7 +1306,10 @@ async function unpin(cwd, patterns) {
|
|
|
989
1306
|
const rootRemoves = [];
|
|
990
1307
|
const componentRemoves = {};
|
|
991
1308
|
for (const pattern of patterns) {
|
|
992
|
-
const { scope, component, relative } = classifyPattern(
|
|
1309
|
+
const { scope, component, relative } = classifyPattern(
|
|
1310
|
+
pattern,
|
|
1311
|
+
componentPaths
|
|
1312
|
+
);
|
|
993
1313
|
if (scope === "component" && component) {
|
|
994
1314
|
if (!componentRemoves[component]) componentRemoves[component] = [];
|
|
995
1315
|
componentRemoves[component].push(relative);
|
|
@@ -1089,7 +1409,11 @@ async function checkConfig(cwd) {
|
|
|
1089
1409
|
});
|
|
1090
1410
|
return { results };
|
|
1091
1411
|
}
|
|
1092
|
-
results.push({
|
|
1412
|
+
results.push({
|
|
1413
|
+
name: ".projx exists",
|
|
1414
|
+
status: "pass",
|
|
1415
|
+
message: `v${rootConfig.version ?? "unknown"}`
|
|
1416
|
+
});
|
|
1093
1417
|
if (!rootConfig.version) {
|
|
1094
1418
|
results.push({
|
|
1095
1419
|
name: ".projx fields",
|
|
@@ -1110,7 +1434,11 @@ async function checkComponents(cwd, components, componentPaths) {
|
|
|
1110
1434
|
});
|
|
1111
1435
|
return results;
|
|
1112
1436
|
}
|
|
1113
|
-
results.push({
|
|
1437
|
+
results.push({
|
|
1438
|
+
name: "components",
|
|
1439
|
+
status: "pass",
|
|
1440
|
+
message: `${components.length} discovered from markers`
|
|
1441
|
+
});
|
|
1114
1442
|
for (const component of components) {
|
|
1115
1443
|
const dir = componentPaths[component];
|
|
1116
1444
|
const fullDir = join7(cwd, dir);
|
|
@@ -1133,7 +1461,11 @@ async function checkComponents(cwd, components, componentPaths) {
|
|
|
1133
1461
|
continue;
|
|
1134
1462
|
}
|
|
1135
1463
|
const label = dir !== component ? `${dir}/ (${component})` : `${component}/`;
|
|
1136
|
-
results.push({
|
|
1464
|
+
results.push({
|
|
1465
|
+
name: `${component} marker`,
|
|
1466
|
+
status: "pass",
|
|
1467
|
+
message: label
|
|
1468
|
+
});
|
|
1137
1469
|
}
|
|
1138
1470
|
return results;
|
|
1139
1471
|
}
|
|
@@ -1143,18 +1475,36 @@ function checkGit(cwd, fix) {
|
|
|
1143
1475
|
execSync3("git rev-parse --is-inside-work-tree", { cwd, stdio: "pipe" });
|
|
1144
1476
|
results.push({ name: "git repo", status: "pass", message: "OK" });
|
|
1145
1477
|
} catch {
|
|
1146
|
-
results.push({
|
|
1478
|
+
results.push({
|
|
1479
|
+
name: "git repo",
|
|
1480
|
+
status: "fail",
|
|
1481
|
+
message: "Not a git repository."
|
|
1482
|
+
});
|
|
1147
1483
|
return results;
|
|
1148
1484
|
}
|
|
1149
1485
|
try {
|
|
1150
|
-
const ref = execSync3(`git rev-parse --verify ${BASELINE_REF}`, {
|
|
1151
|
-
|
|
1486
|
+
const ref = execSync3(`git rev-parse --verify ${BASELINE_REF}`, {
|
|
1487
|
+
cwd,
|
|
1488
|
+
stdio: "pipe"
|
|
1489
|
+
}).toString().trim();
|
|
1490
|
+
results.push({
|
|
1491
|
+
name: "baseline ref",
|
|
1492
|
+
status: "pass",
|
|
1493
|
+
message: ref.slice(0, 8)
|
|
1494
|
+
});
|
|
1152
1495
|
} catch {
|
|
1153
1496
|
if (fix) {
|
|
1154
1497
|
saveBaselineRef(cwd);
|
|
1155
1498
|
try {
|
|
1156
|
-
execSync3(`git rev-parse --verify ${BASELINE_REF}`, {
|
|
1157
|
-
|
|
1499
|
+
execSync3(`git rev-parse --verify ${BASELINE_REF}`, {
|
|
1500
|
+
cwd,
|
|
1501
|
+
stdio: "pipe"
|
|
1502
|
+
});
|
|
1503
|
+
results.push({
|
|
1504
|
+
name: "baseline ref",
|
|
1505
|
+
status: "pass",
|
|
1506
|
+
message: "Created from git history."
|
|
1507
|
+
});
|
|
1158
1508
|
} catch {
|
|
1159
1509
|
results.push({
|
|
1160
1510
|
name: "baseline ref",
|
|
@@ -1173,12 +1523,19 @@ function checkGit(cwd, fix) {
|
|
|
1173
1523
|
}
|
|
1174
1524
|
}
|
|
1175
1525
|
try {
|
|
1176
|
-
const worktrees = execSync3("git worktree list --porcelain", {
|
|
1526
|
+
const worktrees = execSync3("git worktree list --porcelain", {
|
|
1527
|
+
cwd,
|
|
1528
|
+
stdio: "pipe"
|
|
1529
|
+
}).toString();
|
|
1177
1530
|
const stale = worktrees.split("\n").filter((l) => l.includes("projx-wt-") || l.includes("projx/tmp-"));
|
|
1178
1531
|
if (stale.length > 0) {
|
|
1179
1532
|
if (fix) {
|
|
1180
1533
|
execSync3("git worktree prune", { cwd, stdio: "pipe" });
|
|
1181
|
-
results.push({
|
|
1534
|
+
results.push({
|
|
1535
|
+
name: "worktrees",
|
|
1536
|
+
status: "pass",
|
|
1537
|
+
message: "Pruned stale worktrees."
|
|
1538
|
+
});
|
|
1182
1539
|
} else {
|
|
1183
1540
|
results.push({
|
|
1184
1541
|
name: "worktrees",
|
|
@@ -1198,7 +1555,11 @@ function checkGit(cwd, fix) {
|
|
|
1198
1555
|
const status = execSync3("git status --porcelain", { cwd, stdio: "pipe" }).toString().trim();
|
|
1199
1556
|
if (status) {
|
|
1200
1557
|
const count = status.split("\n").length;
|
|
1201
|
-
results.push({
|
|
1558
|
+
results.push({
|
|
1559
|
+
name: "working tree",
|
|
1560
|
+
status: "warn",
|
|
1561
|
+
message: `${count} uncommitted change(s).`
|
|
1562
|
+
});
|
|
1202
1563
|
} else {
|
|
1203
1564
|
results.push({ name: "working tree", status: "pass", message: "Clean" });
|
|
1204
1565
|
}
|
|
@@ -1236,7 +1597,11 @@ async function checkSkipPatterns(cwd, rootConfig, components, componentPaths) {
|
|
|
1236
1597
|
}
|
|
1237
1598
|
}
|
|
1238
1599
|
if (results.length === 0 && (rootSkip.length > 0 || components.length > 0)) {
|
|
1239
|
-
results.push({
|
|
1600
|
+
results.push({
|
|
1601
|
+
name: "skip patterns",
|
|
1602
|
+
status: "pass",
|
|
1603
|
+
message: "All patterns match files."
|
|
1604
|
+
});
|
|
1240
1605
|
}
|
|
1241
1606
|
return results;
|
|
1242
1607
|
}
|
|
@@ -1275,7 +1640,9 @@ async function doctor(cwd, fix = false) {
|
|
|
1275
1640
|
const { components, paths: componentPaths } = await discoverComponentsFromMarkers(cwd);
|
|
1276
1641
|
allResults.push(...await checkComponents(cwd, components, componentPaths));
|
|
1277
1642
|
allResults.push(...checkGit(cwd, fix));
|
|
1278
|
-
allResults.push(
|
|
1643
|
+
allResults.push(
|
|
1644
|
+
...await checkSkipPatterns(cwd, rootConfig, components, componentPaths)
|
|
1645
|
+
);
|
|
1279
1646
|
printReport(allResults);
|
|
1280
1647
|
const passed = allResults.filter((r) => r.status === "pass").length;
|
|
1281
1648
|
const warns = allResults.filter((r) => r.status === "warn").length;
|
|
@@ -1341,7 +1708,9 @@ async function diff(cwd, localRepo) {
|
|
|
1341
1708
|
}
|
|
1342
1709
|
const rootSkip = Array.isArray(raw.skip) ? raw.skip : [];
|
|
1343
1710
|
const dlSpinner = p8.spinner();
|
|
1344
|
-
dlSpinner.start(
|
|
1711
|
+
dlSpinner.start(
|
|
1712
|
+
isLocal ? "Using local templates" : "Downloading latest templates"
|
|
1713
|
+
);
|
|
1345
1714
|
const repoDir = await downloadRepo(localRepo).catch((err) => {
|
|
1346
1715
|
dlSpinner.stop("Failed.");
|
|
1347
1716
|
p8.log.error(String(err));
|
|
@@ -1349,20 +1718,35 @@ async function diff(cwd, localRepo) {
|
|
|
1349
1718
|
});
|
|
1350
1719
|
dlSpinner.stop(isLocal ? "Local templates loaded." : "Templates downloaded.");
|
|
1351
1720
|
try {
|
|
1352
|
-
const pkg = JSON.parse(
|
|
1721
|
+
const pkg = JSON.parse(
|
|
1722
|
+
await readFile5(join8(repoDir, "cli/package.json"), "utf-8")
|
|
1723
|
+
);
|
|
1353
1724
|
const version = pkg.version;
|
|
1354
1725
|
p8.log.info(`Current: v${raw.version ?? "unknown"} \u2192 Template: v${version}`);
|
|
1355
1726
|
const name = detectProjectName(cwd, components, componentPaths);
|
|
1356
|
-
const vars = {
|
|
1727
|
+
const vars = {
|
|
1728
|
+
projectName: name,
|
|
1729
|
+
components,
|
|
1730
|
+
paths: componentPaths,
|
|
1731
|
+
pm: pmCommands(raw.packageManager ?? "npm")
|
|
1732
|
+
};
|
|
1357
1733
|
const spinner7 = p8.spinner();
|
|
1358
1734
|
spinner7.start("Analyzing changes");
|
|
1359
1735
|
const tmpTemplate = join8(tmpdir(), `projx-diff-${Date.now()}`);
|
|
1360
1736
|
await mkdir2(tmpTemplate, { recursive: true });
|
|
1361
|
-
await writeTemplateToDir(
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1737
|
+
await writeTemplateToDir(
|
|
1738
|
+
tmpTemplate,
|
|
1739
|
+
repoDir,
|
|
1740
|
+
components,
|
|
1741
|
+
componentPaths,
|
|
1742
|
+
vars,
|
|
1743
|
+
version,
|
|
1744
|
+
{
|
|
1745
|
+
componentSkips,
|
|
1746
|
+
rootSkip,
|
|
1747
|
+
realCwd: cwd
|
|
1748
|
+
}
|
|
1749
|
+
);
|
|
1366
1750
|
const baselineRef = getBaselineRef(cwd);
|
|
1367
1751
|
const templateFiles = await collectAllFiles(tmpTemplate, tmpTemplate);
|
|
1368
1752
|
const analyses = [];
|
|
@@ -1409,12 +1793,12 @@ async function diff(cwd, localRepo) {
|
|
|
1409
1793
|
await rm(tmpTemplate, { recursive: true, force: true });
|
|
1410
1794
|
spinner7.stop("Analysis complete.");
|
|
1411
1795
|
const groups = {
|
|
1412
|
-
|
|
1796
|
+
new: [],
|
|
1413
1797
|
"clean-update": [],
|
|
1414
1798
|
"needs-merge": [],
|
|
1415
1799
|
"user-only": [],
|
|
1416
|
-
|
|
1417
|
-
|
|
1800
|
+
unchanged: [],
|
|
1801
|
+
skipped: []
|
|
1418
1802
|
};
|
|
1419
1803
|
for (const a of analyses) {
|
|
1420
1804
|
groups[a.status].push(a);
|
|
@@ -1424,15 +1808,21 @@ async function diff(cwd, localRepo) {
|
|
|
1424
1808
|
for (const a of groups["new"]) p8.log.info(` + ${a.file}`);
|
|
1425
1809
|
}
|
|
1426
1810
|
if (groups["clean-update"].length > 0) {
|
|
1427
|
-
p8.log.success(
|
|
1811
|
+
p8.log.success(
|
|
1812
|
+
`Clean updates \u2014 auto-merged (${groups["clean-update"].length}):`
|
|
1813
|
+
);
|
|
1428
1814
|
for (const a of groups["clean-update"]) p8.log.info(` ~ ${a.file}`);
|
|
1429
1815
|
}
|
|
1430
1816
|
if (groups["needs-merge"].length > 0) {
|
|
1431
|
-
p8.log.warn(
|
|
1817
|
+
p8.log.warn(
|
|
1818
|
+
`Needs merge \u2014 both sides changed (${groups["needs-merge"].length}):`
|
|
1819
|
+
);
|
|
1432
1820
|
for (const a of groups["needs-merge"]) p8.log.info(` ! ${a.file}`);
|
|
1433
1821
|
}
|
|
1434
1822
|
if (groups["user-only"].length > 0) {
|
|
1435
|
-
p8.log.info(
|
|
1823
|
+
p8.log.info(
|
|
1824
|
+
`User-modified only \u2014 no template change (${groups["user-only"].length}):`
|
|
1825
|
+
);
|
|
1436
1826
|
for (const a of groups["user-only"]) p8.log.info(` = ${a.file}`);
|
|
1437
1827
|
}
|
|
1438
1828
|
if (groups["skipped"].length > 0) {
|
|
@@ -1459,12 +1849,21 @@ import { existsSync as existsSync9 } from "fs";
|
|
|
1459
1849
|
import { readFile as readFile6, writeFile, mkdir as mkdir3 } from "fs/promises";
|
|
1460
1850
|
import { join as join9 } from "path";
|
|
1461
1851
|
import * as p9 from "@clack/prompts";
|
|
1462
|
-
var FIELD_TYPES = [
|
|
1852
|
+
var FIELD_TYPES = [
|
|
1853
|
+
"string",
|
|
1854
|
+
"number",
|
|
1855
|
+
"boolean",
|
|
1856
|
+
"date",
|
|
1857
|
+
"datetime",
|
|
1858
|
+
"text",
|
|
1859
|
+
"json"
|
|
1860
|
+
];
|
|
1463
1861
|
function toPascal(s) {
|
|
1464
1862
|
return s.split(/[-_\s]+/).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join("");
|
|
1465
1863
|
}
|
|
1466
1864
|
function pluralize(s) {
|
|
1467
|
-
if (s.endsWith("s") || s.endsWith("x") || s.endsWith("z") || s.endsWith("sh") || s.endsWith("ch"))
|
|
1865
|
+
if (s.endsWith("s") || s.endsWith("x") || s.endsWith("z") || s.endsWith("sh") || s.endsWith("ch"))
|
|
1866
|
+
return s + "es";
|
|
1468
1867
|
if (s.endsWith("y") && !/[aeiou]y$/i.test(s)) return s.slice(0, -1) + "ies";
|
|
1469
1868
|
return s + "s";
|
|
1470
1869
|
}
|
|
@@ -1527,7 +1926,9 @@ async function promptEntityConfig(name) {
|
|
|
1527
1926
|
p9.log.warn("No fields defined. Adding a default 'name' field.");
|
|
1528
1927
|
fields.push({ name: "name", type: "string", required: true });
|
|
1529
1928
|
}
|
|
1530
|
-
const stringFields = fields.filter(
|
|
1929
|
+
const stringFields = fields.filter(
|
|
1930
|
+
(f) => f.type === "string" || f.type === "text"
|
|
1931
|
+
);
|
|
1531
1932
|
let searchableFields = [];
|
|
1532
1933
|
if (stringFields.length > 0) {
|
|
1533
1934
|
const selected = await p9.multiselect({
|
|
@@ -1632,7 +2033,9 @@ function generateFastAPIModel(config) {
|
|
|
1632
2033
|
lines.push("");
|
|
1633
2034
|
for (const field of config.fields) {
|
|
1634
2035
|
const nullable = field.required ? "nullable=False" : "nullable=True";
|
|
1635
|
-
lines.push(
|
|
2036
|
+
lines.push(
|
|
2037
|
+
` ${field.name} = Column(${sqlalchemyType(field.type)}, ${nullable})`
|
|
2038
|
+
);
|
|
1636
2039
|
}
|
|
1637
2040
|
lines.push("");
|
|
1638
2041
|
return lines.join("\n");
|
|
@@ -1726,7 +2129,10 @@ function generateFastifySchemas(config) {
|
|
|
1726
2129
|
}
|
|
1727
2130
|
lines.push(` created_at: Type.String({ format: 'date-time' }),`);
|
|
1728
2131
|
lines.push(` updated_at: Type.String({ format: 'date-time' }),`);
|
|
1729
|
-
if (config.softDelete)
|
|
2132
|
+
if (config.softDelete)
|
|
2133
|
+
lines.push(
|
|
2134
|
+
` deleted_at: Type.Union([Type.String({ format: 'date-time' }), Type.Null()]),`
|
|
2135
|
+
);
|
|
1730
2136
|
lines.push(`});`);
|
|
1731
2137
|
lines.push("");
|
|
1732
2138
|
lines.push(`export type ${className} = Static<typeof ${className}Schema>;`);
|
|
@@ -1741,7 +2147,9 @@ function generateFastifySchemas(config) {
|
|
|
1741
2147
|
}
|
|
1742
2148
|
lines.push(`});`);
|
|
1743
2149
|
lines.push("");
|
|
1744
|
-
lines.push(
|
|
2150
|
+
lines.push(
|
|
2151
|
+
`export type Create${className} = Static<typeof Create${className}Schema>;`
|
|
2152
|
+
);
|
|
1745
2153
|
lines.push("");
|
|
1746
2154
|
lines.push(`export const Update${className}Schema = Type.Object({`);
|
|
1747
2155
|
for (const f of config.fields) {
|
|
@@ -1749,29 +2157,50 @@ function generateFastifySchemas(config) {
|
|
|
1749
2157
|
}
|
|
1750
2158
|
lines.push(`});`);
|
|
1751
2159
|
lines.push("");
|
|
1752
|
-
lines.push(
|
|
2160
|
+
lines.push(
|
|
2161
|
+
`export type Update${className} = Static<typeof Update${className}Schema>;`
|
|
2162
|
+
);
|
|
1753
2163
|
lines.push("");
|
|
1754
2164
|
return lines.join("\n");
|
|
1755
2165
|
}
|
|
1756
2166
|
function generateFastifyIndex(config) {
|
|
1757
2167
|
const className = toPascal(config.name);
|
|
1758
2168
|
const camelConfig = className.charAt(0).toLowerCase() + className.slice(1) + "Config";
|
|
1759
|
-
const allColumns = [
|
|
2169
|
+
const allColumns = [
|
|
2170
|
+
"id",
|
|
2171
|
+
...config.fields.map((f) => f.name),
|
|
2172
|
+
"created_at",
|
|
2173
|
+
"updated_at"
|
|
2174
|
+
];
|
|
1760
2175
|
if (config.softDelete) allColumns.push("deleted_at");
|
|
1761
2176
|
const lines = [];
|
|
1762
|
-
lines.push(
|
|
1763
|
-
|
|
2177
|
+
lines.push(
|
|
2178
|
+
`import { EntityRegistry, type EntityConfig, type FieldMeta } from '../_base/index.js';`
|
|
2179
|
+
);
|
|
2180
|
+
lines.push(
|
|
2181
|
+
`import { ${className}Schema, Create${className}Schema, Update${className}Schema } from './schemas.js';`
|
|
2182
|
+
);
|
|
1764
2183
|
lines.push("");
|
|
1765
2184
|
lines.push(`const fields: FieldMeta[] = [`);
|
|
1766
|
-
lines.push(
|
|
2185
|
+
lines.push(
|
|
2186
|
+
` { key: 'id', label: 'Id', type: 'str', nullable: false, is_auto: true, is_primary_key: true, filterable: true, has_foreign_key: false, field_type: 'text' },`
|
|
2187
|
+
);
|
|
1767
2188
|
for (const f of config.fields) {
|
|
1768
2189
|
const meta = fieldMetaType(f.type);
|
|
1769
|
-
lines.push(
|
|
2190
|
+
lines.push(
|
|
2191
|
+
` { key: '${f.name}', label: '${toTitle(f.name)}', type: '${meta.type}', nullable: ${!f.required}, is_auto: false, is_primary_key: false, filterable: true, has_foreign_key: false, field_type: '${meta.fieldType}' },`
|
|
2192
|
+
);
|
|
1770
2193
|
}
|
|
1771
|
-
lines.push(
|
|
1772
|
-
|
|
2194
|
+
lines.push(
|
|
2195
|
+
` { key: 'created_at', label: 'Created At', type: 'datetime', nullable: false, is_auto: true, is_primary_key: false, filterable: true, has_foreign_key: false, field_type: 'datetime' },`
|
|
2196
|
+
);
|
|
2197
|
+
lines.push(
|
|
2198
|
+
` { key: 'updated_at', label: 'Updated At', type: 'datetime', nullable: false, is_auto: true, is_primary_key: false, filterable: true, has_foreign_key: false, field_type: 'datetime' },`
|
|
2199
|
+
);
|
|
1773
2200
|
if (config.softDelete) {
|
|
1774
|
-
lines.push(
|
|
2201
|
+
lines.push(
|
|
2202
|
+
` { key: 'deleted_at', label: 'Deleted At', type: 'datetime', nullable: true, is_auto: true, is_primary_key: false, filterable: true, has_foreign_key: false, field_type: 'datetime' },`
|
|
2203
|
+
);
|
|
1775
2204
|
}
|
|
1776
2205
|
lines.push(`];`);
|
|
1777
2206
|
lines.push("");
|
|
@@ -1787,7 +2216,9 @@ function generateFastifyIndex(config) {
|
|
|
1787
2216
|
lines.push(` bulkOperations: ${config.bulkOperations},`);
|
|
1788
2217
|
lines.push(` columnNames: [${allColumns.map((c) => `'${c}'`).join(", ")}],`);
|
|
1789
2218
|
if (config.searchableFields.length > 0) {
|
|
1790
|
-
lines.push(
|
|
2219
|
+
lines.push(
|
|
2220
|
+
` searchableFields: [${config.searchableFields.map((f) => `'${f}'`).join(", ")}],`
|
|
2221
|
+
);
|
|
1791
2222
|
} else {
|
|
1792
2223
|
lines.push(` searchableFields: [],`);
|
|
1793
2224
|
}
|
|
@@ -1898,7 +2329,8 @@ function dartFromJson(fieldName, type, required) {
|
|
|
1898
2329
|
const key = `json['${fieldName}']`;
|
|
1899
2330
|
const isDate = type === "date" || type === "datetime";
|
|
1900
2331
|
if (isDate && required) return `DateTime.parse(${key} as String)`;
|
|
1901
|
-
if (isDate && !required)
|
|
2332
|
+
if (isDate && !required)
|
|
2333
|
+
return `${key} != null ? DateTime.parse(${key} as String) : null`;
|
|
1902
2334
|
if (type === "json" && !required) return `${key} as Map<String, dynamic>?`;
|
|
1903
2335
|
if (type === "json") return `${key} as Map<String, dynamic>`;
|
|
1904
2336
|
const dartT = (() => {
|
|
@@ -1918,14 +2350,22 @@ function dartFromJson(fieldName, type, required) {
|
|
|
1918
2350
|
}
|
|
1919
2351
|
function dartToJson(fieldName, camelName, type, required) {
|
|
1920
2352
|
const isDate = type === "date" || type === "datetime";
|
|
1921
|
-
if (isDate && required)
|
|
1922
|
-
|
|
2353
|
+
if (isDate && required)
|
|
2354
|
+
return `'${fieldName}': ${camelName}.toIso8601String()`;
|
|
2355
|
+
if (isDate && !required)
|
|
2356
|
+
return `'${fieldName}': ${camelName}?.toIso8601String()`;
|
|
1923
2357
|
return `'${fieldName}': ${camelName}`;
|
|
1924
2358
|
}
|
|
1925
2359
|
function generateDartModel(config) {
|
|
1926
2360
|
const className = toPascal(config.name);
|
|
1927
2361
|
const allFields = [
|
|
1928
|
-
{
|
|
2362
|
+
{
|
|
2363
|
+
snake: "id",
|
|
2364
|
+
camel: "id",
|
|
2365
|
+
type: "String",
|
|
2366
|
+
required: true,
|
|
2367
|
+
fieldType: "string"
|
|
2368
|
+
},
|
|
1929
2369
|
...config.fields.map((f) => ({
|
|
1930
2370
|
snake: f.name,
|
|
1931
2371
|
camel: toCamel(f.name),
|
|
@@ -1935,11 +2375,29 @@ function generateDartModel(config) {
|
|
|
1935
2375
|
}))
|
|
1936
2376
|
];
|
|
1937
2377
|
if (config.softDelete) {
|
|
1938
|
-
allFields.push({
|
|
2378
|
+
allFields.push({
|
|
2379
|
+
snake: "deleted_at",
|
|
2380
|
+
camel: "deletedAt",
|
|
2381
|
+
type: "DateTime?",
|
|
2382
|
+
required: false,
|
|
2383
|
+
fieldType: "datetime"
|
|
2384
|
+
});
|
|
1939
2385
|
}
|
|
1940
2386
|
allFields.push(
|
|
1941
|
-
{
|
|
1942
|
-
|
|
2387
|
+
{
|
|
2388
|
+
snake: "created_at",
|
|
2389
|
+
camel: "createdAt",
|
|
2390
|
+
type: "DateTime",
|
|
2391
|
+
required: true,
|
|
2392
|
+
fieldType: "datetime"
|
|
2393
|
+
},
|
|
2394
|
+
{
|
|
2395
|
+
snake: "updated_at",
|
|
2396
|
+
camel: "updatedAt",
|
|
2397
|
+
type: "DateTime",
|
|
2398
|
+
required: true,
|
|
2399
|
+
fieldType: "datetime"
|
|
2400
|
+
}
|
|
1943
2401
|
);
|
|
1944
2402
|
const lines = [];
|
|
1945
2403
|
lines.push(`class ${className} {`);
|
|
@@ -1960,7 +2418,9 @@ function generateDartModel(config) {
|
|
|
1960
2418
|
lines.push(` factory ${className}.fromJson(Map<String, dynamic> json) {`);
|
|
1961
2419
|
lines.push(` return ${className}(`);
|
|
1962
2420
|
for (const f of allFields) {
|
|
1963
|
-
lines.push(
|
|
2421
|
+
lines.push(
|
|
2422
|
+
` ${f.camel}: ${dartFromJson(f.snake, f.fieldType, f.required)},`
|
|
2423
|
+
);
|
|
1964
2424
|
}
|
|
1965
2425
|
lines.push(` );`);
|
|
1966
2426
|
lines.push(` }`);
|
|
@@ -1968,7 +2428,9 @@ function generateDartModel(config) {
|
|
|
1968
2428
|
lines.push(` Map<String, dynamic> toJson() {`);
|
|
1969
2429
|
lines.push(` return {`);
|
|
1970
2430
|
for (const f of allFields) {
|
|
1971
|
-
lines.push(
|
|
2431
|
+
lines.push(
|
|
2432
|
+
` ${dartToJson(f.snake, f.camel, f.fieldType, f.required)},`
|
|
2433
|
+
);
|
|
1972
2434
|
}
|
|
1973
2435
|
lines.push(` };`);
|
|
1974
2436
|
lines.push(` }`);
|
|
@@ -2070,11 +2532,15 @@ function generateFastapiTest(config) {
|
|
|
2070
2532
|
}
|
|
2071
2533
|
lines.push(` }`);
|
|
2072
2534
|
const updateField = config.fields[0];
|
|
2073
|
-
lines.push(
|
|
2535
|
+
lines.push(
|
|
2536
|
+
` update_payload = {"${updateField.name}": ${pyHttpLiteral(updateField.type, "update")}}`
|
|
2537
|
+
);
|
|
2074
2538
|
lines.push(` invalid_payload: dict = {}`);
|
|
2075
2539
|
lines.push(` filter_field = "${filterField.name}"`);
|
|
2076
2540
|
lines.push(` filter_value = ${pyHttpLiteral(filterField.type, "create")}`);
|
|
2077
|
-
lines.push(
|
|
2541
|
+
lines.push(
|
|
2542
|
+
` other_filter_value = ${pyHttpLiteral(filterField.type, "alt")}`
|
|
2543
|
+
);
|
|
2078
2544
|
lines.push("");
|
|
2079
2545
|
lines.push(` def make_model(self, index: int, **overrides):`);
|
|
2080
2546
|
lines.push(` data = {`);
|
|
@@ -2092,7 +2558,9 @@ function generateFastifyTest(config) {
|
|
|
2092
2558
|
const basePath = `/api/v1${config.apiPrefix}`;
|
|
2093
2559
|
const updateField = config.fields[0];
|
|
2094
2560
|
const lines = [];
|
|
2095
|
-
lines.push(
|
|
2561
|
+
lines.push(
|
|
2562
|
+
`import { describeCrudEntity } from '../helpers/crud-test-base.js';`
|
|
2563
|
+
);
|
|
2096
2564
|
lines.push("");
|
|
2097
2565
|
lines.push(`describeCrudEntity({`);
|
|
2098
2566
|
lines.push(` entityName: '${className}',`);
|
|
@@ -2104,7 +2572,9 @@ function generateFastifyTest(config) {
|
|
|
2104
2572
|
}
|
|
2105
2573
|
lines.push(` },`);
|
|
2106
2574
|
lines.push(` updatePayload: {`);
|
|
2107
|
-
lines.push(
|
|
2575
|
+
lines.push(
|
|
2576
|
+
` ${updateField.name}: ${tsLiteral(updateField.type, "update")},`
|
|
2577
|
+
);
|
|
2108
2578
|
lines.push(` },`);
|
|
2109
2579
|
lines.push(`});`);
|
|
2110
2580
|
lines.push("");
|
|
@@ -2150,7 +2620,12 @@ async function gen(cwd, entityName, fieldsFlag, backendFlag) {
|
|
|
2150
2620
|
p9.log.error("No backend component found. Need fastapi or fastify.");
|
|
2151
2621
|
process.exit(1);
|
|
2152
2622
|
}
|
|
2153
|
-
const targetBackend = await resolvePrimaryBackend(
|
|
2623
|
+
const targetBackend = await resolvePrimaryBackend(
|
|
2624
|
+
cwd,
|
|
2625
|
+
hasFastapi,
|
|
2626
|
+
hasFastify,
|
|
2627
|
+
backendFlag
|
|
2628
|
+
);
|
|
2154
2629
|
const genFastapi = targetBackend === "fastapi" && hasFastapi;
|
|
2155
2630
|
const genFastify = targetBackend === "fastify" && hasFastify;
|
|
2156
2631
|
let config;
|
|
@@ -2177,11 +2652,19 @@ async function gen(cwd, entityName, fieldsFlag, backendFlag) {
|
|
|
2177
2652
|
const dir = componentPaths.fastapi;
|
|
2178
2653
|
const entityDir = join9(cwd, dir, "src/entities", toSnake(config.name));
|
|
2179
2654
|
if (existsSync9(entityDir)) {
|
|
2180
|
-
p9.log.warn(
|
|
2655
|
+
p9.log.warn(
|
|
2656
|
+
`${dir}/src/entities/${toSnake(config.name)}/ already exists. Skipping FastAPI.`
|
|
2657
|
+
);
|
|
2181
2658
|
} else {
|
|
2182
2659
|
await mkdir3(entityDir, { recursive: true });
|
|
2183
|
-
await writeFile(
|
|
2184
|
-
|
|
2660
|
+
await writeFile(
|
|
2661
|
+
join9(entityDir, "_model.py"),
|
|
2662
|
+
generateFastAPIModel(config)
|
|
2663
|
+
);
|
|
2664
|
+
await writeFile(
|
|
2665
|
+
join9(entityDir, "__init__.py"),
|
|
2666
|
+
"from ._model import *\n"
|
|
2667
|
+
);
|
|
2185
2668
|
generated.push(`${dir}/src/entities/${toSnake(config.name)}/_model.py`);
|
|
2186
2669
|
generated.push(`${dir}/src/entities/${toSnake(config.name)}/__init__.py`);
|
|
2187
2670
|
const testsDir = join9(cwd, dir, "tests");
|
|
@@ -2196,11 +2679,19 @@ async function gen(cwd, entityName, fieldsFlag, backendFlag) {
|
|
|
2196
2679
|
const dir = componentPaths.fastify;
|
|
2197
2680
|
const moduleDir = join9(cwd, dir, "src/modules", toKebab(config.name));
|
|
2198
2681
|
if (existsSync9(moduleDir)) {
|
|
2199
|
-
p9.log.warn(
|
|
2682
|
+
p9.log.warn(
|
|
2683
|
+
`${dir}/src/modules/${toKebab(config.name)}/ already exists. Skipping Fastify.`
|
|
2684
|
+
);
|
|
2200
2685
|
} else {
|
|
2201
2686
|
await mkdir3(moduleDir, { recursive: true });
|
|
2202
|
-
await writeFile(
|
|
2203
|
-
|
|
2687
|
+
await writeFile(
|
|
2688
|
+
join9(moduleDir, "schemas.ts"),
|
|
2689
|
+
generateFastifySchemas(config)
|
|
2690
|
+
);
|
|
2691
|
+
await writeFile(
|
|
2692
|
+
join9(moduleDir, "index.ts"),
|
|
2693
|
+
generateFastifyIndex(config)
|
|
2694
|
+
);
|
|
2204
2695
|
generated.push(`${dir}/src/modules/${toKebab(config.name)}/schemas.ts`);
|
|
2205
2696
|
generated.push(`${dir}/src/modules/${toKebab(config.name)}/index.ts`);
|
|
2206
2697
|
const appPath = join9(cwd, dir, "src/app.ts");
|
|
@@ -2225,12 +2716,18 @@ async function gen(cwd, entityName, fieldsFlag, backendFlag) {
|
|
|
2225
2716
|
const modelName = `model ${toPascal(config.name)}`;
|
|
2226
2717
|
if (!prismaContent.includes(modelName)) {
|
|
2227
2718
|
const prismaModel = generatePrismaModel(config);
|
|
2228
|
-
await writeFile(
|
|
2719
|
+
await writeFile(
|
|
2720
|
+
prismaPath,
|
|
2721
|
+
prismaContent.trimEnd() + "\n\n" + prismaModel + "\n"
|
|
2722
|
+
);
|
|
2229
2723
|
generated.push(`${dir}/prisma/schema.prisma (model added)`);
|
|
2230
2724
|
}
|
|
2231
2725
|
}
|
|
2232
2726
|
const testsModulesDir = join9(cwd, dir, "tests/modules");
|
|
2233
|
-
const fastifyTestFile = join9(
|
|
2727
|
+
const fastifyTestFile = join9(
|
|
2728
|
+
testsModulesDir,
|
|
2729
|
+
`${toKebab(config.name)}.test.ts`
|
|
2730
|
+
);
|
|
2234
2731
|
if (existsSync9(testsModulesDir) && !existsSync9(fastifyTestFile)) {
|
|
2235
2732
|
await writeFile(fastifyTestFile, generateFastifyTest(config));
|
|
2236
2733
|
generated.push(`${dir}/tests/modules/${toKebab(config.name)}.test.ts`);
|
|
@@ -2243,7 +2740,9 @@ async function gen(cwd, entityName, fieldsFlag, backendFlag) {
|
|
|
2243
2740
|
const fileName = toKebab(config.name) + ".ts";
|
|
2244
2741
|
const filePath = join9(typesDir, fileName);
|
|
2245
2742
|
if (existsSync9(filePath)) {
|
|
2246
|
-
p9.log.warn(
|
|
2743
|
+
p9.log.warn(
|
|
2744
|
+
`${dir}/src/types/${fileName} already exists. Skipping frontend types.`
|
|
2745
|
+
);
|
|
2247
2746
|
} else {
|
|
2248
2747
|
await mkdir3(typesDir, { recursive: true });
|
|
2249
2748
|
await writeFile(filePath, generateFrontendInterface(config));
|
|
@@ -2253,7 +2752,10 @@ async function gen(cwd, entityName, fieldsFlag, backendFlag) {
|
|
|
2253
2752
|
if (existsSync9(barrelPath)) {
|
|
2254
2753
|
const content = await readFile6(barrelPath, "utf-8");
|
|
2255
2754
|
if (!content.includes(exportLine)) {
|
|
2256
|
-
await writeFile(
|
|
2755
|
+
await writeFile(
|
|
2756
|
+
barrelPath,
|
|
2757
|
+
content.trimEnd() + "\n" + exportLine + "\n"
|
|
2758
|
+
);
|
|
2257
2759
|
}
|
|
2258
2760
|
} else {
|
|
2259
2761
|
await writeFile(barrelPath, exportLine + "\n");
|
|
@@ -2266,7 +2768,9 @@ async function gen(cwd, entityName, fieldsFlag, backendFlag) {
|
|
|
2266
2768
|
const entityDir = join9(cwd, dir, "lib/entities", toSnake(config.name));
|
|
2267
2769
|
const modelPath = join9(entityDir, "model.dart");
|
|
2268
2770
|
if (existsSync9(modelPath)) {
|
|
2269
|
-
p9.log.warn(
|
|
2771
|
+
p9.log.warn(
|
|
2772
|
+
`${dir}/lib/entities/${toSnake(config.name)}/model.dart already exists. Skipping mobile model.`
|
|
2773
|
+
);
|
|
2270
2774
|
} else {
|
|
2271
2775
|
await mkdir3(entityDir, { recursive: true });
|
|
2272
2776
|
await writeFile(modelPath, generateDartModel(config));
|
|
@@ -2286,19 +2790,27 @@ async function gen(cwd, entityName, fieldsFlag, backendFlag) {
|
|
|
2286
2790
|
if (genFastapi) {
|
|
2287
2791
|
p9.log.info("");
|
|
2288
2792
|
p9.log.info("FastAPI next steps:");
|
|
2289
|
-
p9.log.info(
|
|
2793
|
+
p9.log.info(
|
|
2794
|
+
` alembic revision --autogenerate -m "add ${config.tableName}"`
|
|
2795
|
+
);
|
|
2290
2796
|
p9.log.info(" alembic upgrade head");
|
|
2291
2797
|
}
|
|
2292
2798
|
if (genFastify) {
|
|
2293
2799
|
p9.log.info("");
|
|
2294
2800
|
p9.log.info("Fastify next steps:");
|
|
2295
|
-
p9.log.info(
|
|
2801
|
+
p9.log.info(
|
|
2802
|
+
` ${pm.prismaExec} migrate dev --name add_${toSnake(config.name)}`
|
|
2803
|
+
);
|
|
2296
2804
|
}
|
|
2297
2805
|
if (hasFrontend) {
|
|
2298
2806
|
p9.log.info("");
|
|
2299
2807
|
p9.log.info("Frontend usage:");
|
|
2300
|
-
p9.log.info(
|
|
2301
|
-
|
|
2808
|
+
p9.log.info(
|
|
2809
|
+
` import type { ${className} } from '../types/${toKebab(config.name)}';`
|
|
2810
|
+
);
|
|
2811
|
+
p9.log.info(
|
|
2812
|
+
` const { data } = await api.list<${className}>('${config.apiPrefix}');`
|
|
2813
|
+
);
|
|
2302
2814
|
}
|
|
2303
2815
|
if (hasMobile) {
|
|
2304
2816
|
p9.log.info("");
|
|
@@ -2645,9 +3157,7 @@ function parseArgs() {
|
|
|
2645
3157
|
if (arg === "--components") {
|
|
2646
3158
|
const val = args[++i];
|
|
2647
3159
|
if (val) {
|
|
2648
|
-
options.components = val.split(",").filter(
|
|
2649
|
-
(c) => COMPONENTS.includes(c)
|
|
2650
|
-
);
|
|
3160
|
+
options.components = val.split(",").filter((c) => COMPONENTS.includes(c));
|
|
2651
3161
|
}
|
|
2652
3162
|
continue;
|
|
2653
3163
|
}
|
|
@@ -2697,6 +3207,11 @@ function parseArgs() {
|
|
|
2697
3207
|
if (val) extraArgs.push(`--fields=${val}`);
|
|
2698
3208
|
continue;
|
|
2699
3209
|
}
|
|
3210
|
+
if (arg === "--name") {
|
|
3211
|
+
const val = args[++i];
|
|
3212
|
+
if (val) extraArgs.push(`--name=${val}`);
|
|
3213
|
+
continue;
|
|
3214
|
+
}
|
|
2700
3215
|
if (!arg.startsWith("-")) {
|
|
2701
3216
|
if (command === "add" || command === "pin" || command === "unpin" || command === "gen") {
|
|
2702
3217
|
extraArgs.push(arg);
|
|
@@ -2713,6 +3228,7 @@ function printHelp() {
|
|
|
2713
3228
|
projx <name> [options] Create a new project
|
|
2714
3229
|
projx init Adopt existing project into projx
|
|
2715
3230
|
projx add <components...> Add components to existing project
|
|
3231
|
+
projx add <type> --name <dir> Add another instance of <type> at <dir>
|
|
2716
3232
|
projx update Update scaffolding to latest
|
|
2717
3233
|
projx diff Preview what update would change
|
|
2718
3234
|
projx pin <patterns...> Skip files on future updates
|
|
@@ -2735,6 +3251,7 @@ function printHelp() {
|
|
|
2735
3251
|
npx create-projx my-app --components fastapi,frontend,e2e
|
|
2736
3252
|
npx create-projx my-app -y
|
|
2737
3253
|
npx create-projx add frontend mobile
|
|
3254
|
+
npx create-projx add fastify --name email-ingestor
|
|
2738
3255
|
npx create-projx@latest update
|
|
2739
3256
|
npx create-projx diff
|
|
2740
3257
|
npx create-projx pin backend/pyproject.toml
|
|
@@ -2758,10 +3275,25 @@ async function main() {
|
|
|
2758
3275
|
(c) => COMPONENTS.includes(c)
|
|
2759
3276
|
);
|
|
2760
3277
|
if (components.length === 0) {
|
|
2761
|
-
console.error(
|
|
3278
|
+
console.error(
|
|
3279
|
+
`Error: specify components to add. Available: ${COMPONENTS.join(", ")}`
|
|
3280
|
+
);
|
|
2762
3281
|
process.exit(1);
|
|
2763
3282
|
}
|
|
2764
|
-
|
|
3283
|
+
const customName = extraArgs.find((a) => a.startsWith("--name="))?.slice("--name=".length);
|
|
3284
|
+
if (customName && components.length > 1) {
|
|
3285
|
+
console.error(
|
|
3286
|
+
"Error: --name can only be used when adding a single component type."
|
|
3287
|
+
);
|
|
3288
|
+
process.exit(2);
|
|
3289
|
+
}
|
|
3290
|
+
await add(
|
|
3291
|
+
process.cwd(),
|
|
3292
|
+
components,
|
|
3293
|
+
localRepo,
|
|
3294
|
+
options.install === false,
|
|
3295
|
+
customName
|
|
3296
|
+
);
|
|
2765
3297
|
return;
|
|
2766
3298
|
}
|
|
2767
3299
|
if (command === "pin") {
|
|
@@ -2774,7 +3306,9 @@ async function main() {
|
|
|
2774
3306
|
}
|
|
2775
3307
|
if (command === "unpin") {
|
|
2776
3308
|
if (extraArgs.length === 0) {
|
|
2777
|
-
console.error(
|
|
3309
|
+
console.error(
|
|
3310
|
+
"Error: specify patterns to unpin. Usage: projx unpin <patterns...>"
|
|
3311
|
+
);
|
|
2778
3312
|
process.exit(1);
|
|
2779
3313
|
}
|
|
2780
3314
|
await unpin(process.cwd(), extraArgs);
|
|
@@ -2797,7 +3331,9 @@ async function main() {
|
|
|
2797
3331
|
if (command === "gen") {
|
|
2798
3332
|
const subcommand = extraArgs[0];
|
|
2799
3333
|
if (subcommand !== "entity" || !extraArgs[1]) {
|
|
2800
|
-
console.error(
|
|
3334
|
+
console.error(
|
|
3335
|
+
'Usage: projx gen entity <name> [--fields "name:string,amount:number"]'
|
|
3336
|
+
);
|
|
2801
3337
|
process.exit(1);
|
|
2802
3338
|
}
|
|
2803
3339
|
const entityName = extraArgs[1];
|