create-projx 1.6.2 → 1.6.4
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 +18 -6
- package/dist/{baseline-KTCFW2FK.js → baseline-PZM4KJJW.js} +2 -6
- package/dist/{chunk-LTIJPVRZ.js → chunk-6YRBHJ2V.js} +151 -38
- package/dist/{chunk-D33FXCNT.js → chunk-XQ7FE4U3.js} +343 -153
- package/dist/index.js +749 -205
- package/dist/{utils-VY5BBJBQ.js → utils-AVKSTHIF.js} +1 -1
- package/package.json +1 -1
- package/src/templates/README.md.ejs +1 -1
- package/src/templates/ci.yml.ejs +63 -63
- package/src/templates/docker-compose.dev.yml.ejs +44 -29
- package/src/templates/docker-compose.yml.ejs +27 -25
- package/src/templates/pre-commit.ejs +52 -52
- package/src/templates/setup.sh.ejs +16 -16
package/dist/index.js
CHANGED
|
@@ -9,10 +9,11 @@ import {
|
|
|
9
9
|
matchesSkip,
|
|
10
10
|
saveBaselineRef,
|
|
11
11
|
writeTemplateToDir
|
|
12
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-XQ7FE4U3.js";
|
|
13
13
|
import {
|
|
14
14
|
COMPONENTS,
|
|
15
15
|
COMPONENT_MARKER,
|
|
16
|
+
DEFAULT_ROOT_SKIP_PATTERNS,
|
|
16
17
|
EXCLUDE,
|
|
17
18
|
PACKAGE_MANAGERS,
|
|
18
19
|
cleanupRepo,
|
|
@@ -33,7 +34,7 @@ import {
|
|
|
33
34
|
toTitle,
|
|
34
35
|
writeComponentMarker,
|
|
35
36
|
writeProjxConfig
|
|
36
|
-
} from "./chunk-
|
|
37
|
+
} from "./chunk-6YRBHJ2V.js";
|
|
37
38
|
|
|
38
39
|
// src/index.ts
|
|
39
40
|
import { existsSync as existsSync11 } from "fs";
|
|
@@ -76,7 +77,9 @@ async function runPrompts(nameArg) {
|
|
|
76
77
|
if (components.length === 0) {
|
|
77
78
|
p.log.warn("No components selected. Creating an empty project.");
|
|
78
79
|
}
|
|
79
|
-
const hasJs = components.some(
|
|
80
|
+
const hasJs = components.some(
|
|
81
|
+
(c) => ["fastify", "frontend", "e2e"].includes(c)
|
|
82
|
+
);
|
|
80
83
|
let packageManager = "npm";
|
|
81
84
|
if (hasJs) {
|
|
82
85
|
const pm = await p.select({
|
|
@@ -101,11 +104,18 @@ async function scaffold(opts, dest, localRepo) {
|
|
|
101
104
|
const paths = Object.fromEntries(
|
|
102
105
|
opts.components.map((c) => [c, c])
|
|
103
106
|
);
|
|
104
|
-
const vars = {
|
|
107
|
+
const vars = {
|
|
108
|
+
projectName: name,
|
|
109
|
+
components: opts.components,
|
|
110
|
+
paths,
|
|
111
|
+
pm: pmCommands(pm)
|
|
112
|
+
};
|
|
105
113
|
const isLocal = !!localRepo;
|
|
106
114
|
await mkdir(dest, { recursive: true });
|
|
107
115
|
const dlSpinner = p2.spinner();
|
|
108
|
-
dlSpinner.start(
|
|
116
|
+
dlSpinner.start(
|
|
117
|
+
isLocal ? "Using local templates" : "Downloading latest templates"
|
|
118
|
+
);
|
|
109
119
|
const repoDir = await downloadRepo(localRepo).catch((err) => {
|
|
110
120
|
dlSpinner.stop("Failed.");
|
|
111
121
|
p2.log.error(String(err));
|
|
@@ -113,7 +123,9 @@ async function scaffold(opts, dest, localRepo) {
|
|
|
113
123
|
});
|
|
114
124
|
dlSpinner.stop(isLocal ? "Local templates loaded." : "Templates downloaded.");
|
|
115
125
|
try {
|
|
116
|
-
const pkg = JSON.parse(
|
|
126
|
+
const pkg = JSON.parse(
|
|
127
|
+
await readFile(join(repoDir, "cli/package.json"), "utf-8")
|
|
128
|
+
);
|
|
117
129
|
const version = pkg.version;
|
|
118
130
|
p2.log.info(`Scaffolding project in ${dest}`);
|
|
119
131
|
if (opts.git) {
|
|
@@ -121,7 +133,17 @@ async function scaffold(opts, dest, localRepo) {
|
|
|
121
133
|
}
|
|
122
134
|
const spinner7 = p2.spinner();
|
|
123
135
|
spinner7.start("Scaffolding project");
|
|
124
|
-
await applyTemplate(
|
|
136
|
+
await applyTemplate(
|
|
137
|
+
dest,
|
|
138
|
+
repoDir,
|
|
139
|
+
opts.components,
|
|
140
|
+
paths,
|
|
141
|
+
vars,
|
|
142
|
+
version,
|
|
143
|
+
void 0,
|
|
144
|
+
void 0,
|
|
145
|
+
true
|
|
146
|
+
);
|
|
125
147
|
spinner7.stop("Scaffold complete.");
|
|
126
148
|
if (opts.install) {
|
|
127
149
|
await installDeps(dest, opts.components, pm);
|
|
@@ -139,12 +161,14 @@ async function scaffold(opts, dest, localRepo) {
|
|
|
139
161
|
} finally {
|
|
140
162
|
await cleanupRepo(repoDir, isLocal);
|
|
141
163
|
}
|
|
142
|
-
p2.outro(
|
|
164
|
+
p2.outro(
|
|
165
|
+
`Done! Next steps:
|
|
143
166
|
|
|
144
167
|
cd ${name}
|
|
145
|
-
./setup.sh
|
|
168
|
+
./scripts/setup.sh
|
|
146
169
|
|
|
147
|
-
Like projx? Star it: https://github.com/ukanhaupa/projx`
|
|
170
|
+
Like projx? Star it: https://github.com/ukanhaupa/projx`
|
|
171
|
+
);
|
|
148
172
|
}
|
|
149
173
|
async function installDeps(dest, components, pm) {
|
|
150
174
|
const cmds = pmCommands(pm);
|
|
@@ -168,7 +192,9 @@ async function installDeps(dest, components, pm) {
|
|
|
168
192
|
exec(cmds.install, join(dest, "fastify"));
|
|
169
193
|
spinner7.stop("Fastify dependencies installed.");
|
|
170
194
|
} else {
|
|
171
|
-
p2.log.warn(
|
|
195
|
+
p2.log.warn(
|
|
196
|
+
`${pm} not found \u2014 run 'cd fastify && ${cmds.install}' manually.`
|
|
197
|
+
);
|
|
172
198
|
}
|
|
173
199
|
break;
|
|
174
200
|
case "frontend":
|
|
@@ -177,7 +203,9 @@ async function installDeps(dest, components, pm) {
|
|
|
177
203
|
exec(cmds.install, join(dest, "frontend"));
|
|
178
204
|
spinner7.stop("Frontend dependencies installed.");
|
|
179
205
|
} else {
|
|
180
|
-
p2.log.warn(
|
|
206
|
+
p2.log.warn(
|
|
207
|
+
`${pm} not found \u2014 run 'cd frontend && ${cmds.install}' manually.`
|
|
208
|
+
);
|
|
181
209
|
}
|
|
182
210
|
break;
|
|
183
211
|
case "e2e":
|
|
@@ -186,7 +214,9 @@ async function installDeps(dest, components, pm) {
|
|
|
186
214
|
exec(cmds.install, join(dest, "e2e"));
|
|
187
215
|
spinner7.stop("E2E dependencies installed.");
|
|
188
216
|
} else {
|
|
189
|
-
p2.log.warn(
|
|
217
|
+
p2.log.warn(
|
|
218
|
+
`${pm} not found \u2014 run 'cd e2e && ${cmds.install}' manually.`
|
|
219
|
+
);
|
|
190
220
|
}
|
|
191
221
|
break;
|
|
192
222
|
case "mobile":
|
|
@@ -195,7 +225,9 @@ async function installDeps(dest, components, pm) {
|
|
|
195
225
|
exec("flutter pub get", join(dest, "mobile"));
|
|
196
226
|
spinner7.stop("Flutter dependencies installed.");
|
|
197
227
|
} else {
|
|
198
|
-
p2.log.warn(
|
|
228
|
+
p2.log.warn(
|
|
229
|
+
"Flutter not found \u2014 run 'cd mobile && flutter pub get' manually."
|
|
230
|
+
);
|
|
199
231
|
}
|
|
200
232
|
break;
|
|
201
233
|
case "infra":
|
|
@@ -237,17 +269,33 @@ async function update(cwd, localRepo) {
|
|
|
237
269
|
} catch {
|
|
238
270
|
}
|
|
239
271
|
const raw = await readProjxConfig(cwd);
|
|
240
|
-
const {
|
|
272
|
+
const {
|
|
273
|
+
components,
|
|
274
|
+
paths: componentPaths,
|
|
275
|
+
instances
|
|
276
|
+
} = await discoverComponentsFromMarkers(cwd);
|
|
277
|
+
const extraInstances = instances.filter(
|
|
278
|
+
(i) => componentPaths[i.type] !== i.path
|
|
279
|
+
);
|
|
241
280
|
const pendingConflicts = findFilesWithConflictMarkers(cwd);
|
|
242
281
|
if (pendingConflicts.length > 0) {
|
|
243
|
-
p3.log.warn(
|
|
282
|
+
p3.log.warn(
|
|
283
|
+
`Found ${pendingConflicts.length} file(s) with unresolved conflict markers from a prior update:`
|
|
284
|
+
);
|
|
244
285
|
for (const f of pendingConflicts) p3.log.info(` ${f}`);
|
|
245
286
|
p3.log.info("");
|
|
246
287
|
const resumeVersion = String(raw.version ?? "unknown");
|
|
247
|
-
const handled = await promptSkipLearning(
|
|
288
|
+
const handled = await promptSkipLearning(
|
|
289
|
+
cwd,
|
|
290
|
+
componentPaths,
|
|
291
|
+
resumeVersion,
|
|
292
|
+
pendingConflicts
|
|
293
|
+
);
|
|
248
294
|
if (!handled) {
|
|
249
295
|
p3.log.info("");
|
|
250
|
-
p3.log.info(
|
|
296
|
+
p3.log.info(
|
|
297
|
+
"Resolve manually with `git diff` then `git add` / `git checkout --`,"
|
|
298
|
+
);
|
|
251
299
|
p3.log.info("or re-run `npx create-projx update` to resume the prompt.");
|
|
252
300
|
}
|
|
253
301
|
return;
|
|
@@ -261,7 +309,9 @@ async function update(cwd, localRepo) {
|
|
|
261
309
|
process.exit(1);
|
|
262
310
|
}
|
|
263
311
|
if (Object.keys(raw).length > 0) {
|
|
264
|
-
p3.log.info(
|
|
312
|
+
p3.log.info(
|
|
313
|
+
`Found .projx (v${raw.version ?? "unknown"}, components: ${components.join(", ")})`
|
|
314
|
+
);
|
|
265
315
|
} else {
|
|
266
316
|
p3.log.warn("No .projx file found. Detected components from markers.");
|
|
267
317
|
p3.log.info(`Detected: ${components.join(", ")}`);
|
|
@@ -279,7 +329,9 @@ async function update(cwd, localRepo) {
|
|
|
279
329
|
}
|
|
280
330
|
}
|
|
281
331
|
const dlSpinner = p3.spinner();
|
|
282
|
-
dlSpinner.start(
|
|
332
|
+
dlSpinner.start(
|
|
333
|
+
isLocal ? "Using local templates" : "Downloading latest templates"
|
|
334
|
+
);
|
|
283
335
|
const repoDir = await downloadRepo(localRepo).catch((err) => {
|
|
284
336
|
dlSpinner.stop("Failed.");
|
|
285
337
|
p3.log.error(String(err));
|
|
@@ -287,43 +339,88 @@ async function update(cwd, localRepo) {
|
|
|
287
339
|
});
|
|
288
340
|
dlSpinner.stop(isLocal ? "Local templates loaded." : "Templates downloaded.");
|
|
289
341
|
try {
|
|
290
|
-
const pkg = JSON.parse(
|
|
342
|
+
const pkg = JSON.parse(
|
|
343
|
+
await readFile2(join2(repoDir, "cli/package.json"), "utf-8")
|
|
344
|
+
);
|
|
291
345
|
const version = pkg.version;
|
|
292
346
|
const name = detectProjectName(cwd, components, componentPaths);
|
|
293
347
|
const recordedPm = raw.packageManager;
|
|
294
348
|
const detectedPm = detectPackageManagerFromComponents(cwd, componentPaths);
|
|
295
349
|
const pm = detectedPm ?? recordedPm ?? "npm";
|
|
296
350
|
if (detectedPm && recordedPm && detectedPm !== recordedPm) {
|
|
297
|
-
p3.log.warn(
|
|
351
|
+
p3.log.warn(
|
|
352
|
+
`packageManager mismatch: .projx says "${recordedPm}" but lockfile is "${detectedPm}". Using "${detectedPm}".`
|
|
353
|
+
);
|
|
298
354
|
await writeProjxConfig(cwd, { ...raw, packageManager: detectedPm });
|
|
299
355
|
} else if (detectedPm && !recordedPm) {
|
|
300
356
|
await writeProjxConfig(cwd, { ...raw, packageManager: detectedPm });
|
|
301
357
|
}
|
|
302
|
-
const nameOverrides = await detectPackageNameOverrides(
|
|
303
|
-
|
|
358
|
+
const nameOverrides = await detectPackageNameOverrides(
|
|
359
|
+
cwd,
|
|
360
|
+
components,
|
|
361
|
+
componentPaths
|
|
362
|
+
);
|
|
363
|
+
const vars = {
|
|
364
|
+
projectName: name,
|
|
365
|
+
components,
|
|
366
|
+
paths: componentPaths,
|
|
367
|
+
instances,
|
|
368
|
+
pm: pmCommands(pm),
|
|
369
|
+
nameOverrides
|
|
370
|
+
};
|
|
304
371
|
const spinner7 = p3.spinner();
|
|
305
372
|
spinner7.start("Applying template update");
|
|
306
373
|
const rootSkip = Array.isArray(raw.skip) ? raw.skip : [];
|
|
307
374
|
const isLegacyMigration = !raw.defaultsApplied;
|
|
308
375
|
if (isLegacyMigration) {
|
|
309
|
-
p3.log.info(
|
|
376
|
+
p3.log.info(
|
|
377
|
+
"Legacy project detected \u2014 applying default skip patterns for user-owned files."
|
|
378
|
+
);
|
|
310
379
|
}
|
|
311
|
-
const result = await applyTemplate(
|
|
380
|
+
const result = await applyTemplate(
|
|
381
|
+
cwd,
|
|
382
|
+
repoDir,
|
|
383
|
+
components,
|
|
384
|
+
componentPaths,
|
|
385
|
+
vars,
|
|
386
|
+
version,
|
|
387
|
+
componentSkips,
|
|
388
|
+
rootSkip,
|
|
389
|
+
isLegacyMigration,
|
|
390
|
+
extraInstances
|
|
391
|
+
);
|
|
312
392
|
spinner7.stop("Template applied.");
|
|
313
|
-
const pinnedUpdates = await findPinnedFilesWithUpdates(
|
|
393
|
+
const pinnedUpdates = await findPinnedFilesWithUpdates(
|
|
394
|
+
cwd,
|
|
395
|
+
repoDir,
|
|
396
|
+
components,
|
|
397
|
+
componentPaths,
|
|
398
|
+
vars,
|
|
399
|
+
version,
|
|
400
|
+
componentSkips,
|
|
401
|
+
rootSkip
|
|
402
|
+
);
|
|
314
403
|
if (pinnedUpdates.length > 0) {
|
|
315
404
|
p3.log.info("");
|
|
316
|
-
p3.log.info(
|
|
405
|
+
p3.log.info(
|
|
406
|
+
`${pinnedUpdates.length} pinned file(s) have template updates available:`
|
|
407
|
+
);
|
|
317
408
|
for (const f of pinnedUpdates) p3.log.info(` ${f}`);
|
|
318
|
-
p3.log.info(
|
|
409
|
+
p3.log.info(
|
|
410
|
+
"Run `npx create-projx unpin <file> && npx create-projx update` to opt in."
|
|
411
|
+
);
|
|
319
412
|
}
|
|
320
413
|
if (result.status === "merged") {
|
|
321
414
|
saveBaselineRef(cwd);
|
|
322
|
-
p3.log.success(
|
|
415
|
+
p3.log.success(
|
|
416
|
+
`${result.mergedFiles?.length ?? 0} file(s) merged cleanly.`
|
|
417
|
+
);
|
|
323
418
|
p3.outro(`Updated to template v${version}.`);
|
|
324
419
|
} else if (result.status === "conflicts") {
|
|
325
420
|
if (result.mergedFiles && result.mergedFiles.length > 0) {
|
|
326
|
-
p3.log.success(
|
|
421
|
+
p3.log.success(
|
|
422
|
+
`${result.mergedFiles.length} file(s) merged cleanly and staged.`
|
|
423
|
+
);
|
|
327
424
|
}
|
|
328
425
|
const conflictCount = result.conflictedFiles?.length ?? 0;
|
|
329
426
|
if (conflictCount > 0) {
|
|
@@ -332,13 +429,20 @@ async function update(cwd, localRepo) {
|
|
|
332
429
|
p3.log.info(` ${f}`);
|
|
333
430
|
}
|
|
334
431
|
}
|
|
335
|
-
const handled = await promptSkipLearning(
|
|
432
|
+
const handled = await promptSkipLearning(
|
|
433
|
+
cwd,
|
|
434
|
+
componentPaths,
|
|
435
|
+
version,
|
|
436
|
+
result.conflictedFiles ?? []
|
|
437
|
+
);
|
|
336
438
|
if (!handled) {
|
|
337
439
|
p3.log.info("");
|
|
338
440
|
p3.log.info("Review: git diff");
|
|
339
441
|
p3.log.info("Keep: git add <file>");
|
|
340
442
|
p3.log.info("Discard: git checkout -- <file>");
|
|
341
|
-
p3.log.info(
|
|
443
|
+
p3.log.info(
|
|
444
|
+
`Commit: git add . && git commit -m "projx: update to v${version}"`
|
|
445
|
+
);
|
|
342
446
|
p3.outro(`Template v${version} applied. Review with git diff.`);
|
|
343
447
|
}
|
|
344
448
|
} else {
|
|
@@ -371,7 +475,7 @@ function hasUncommittedChanges(cwd) {
|
|
|
371
475
|
async function findPinnedFilesWithUpdates(cwd, repoDir, components, componentPaths, vars, version, componentSkips, rootSkip) {
|
|
372
476
|
const { mkdir: mkdir5, rm: rm2, readFile: readFile7 } = await import("fs/promises");
|
|
373
477
|
const { tmpdir: tmpdir2 } = await import("os");
|
|
374
|
-
const { writeTemplateToDir: writeTemplateToDir2 } = await import("./baseline-
|
|
478
|
+
const { writeTemplateToDir: writeTemplateToDir2 } = await import("./baseline-PZM4KJJW.js");
|
|
375
479
|
const config = await readProjxConfig(cwd);
|
|
376
480
|
const rootPinned = Array.isArray(config.skip) ? config.skip : [];
|
|
377
481
|
const componentPinned = [];
|
|
@@ -388,11 +492,19 @@ async function findPinnedFilesWithUpdates(cwd, repoDir, components, componentPat
|
|
|
388
492
|
void componentSkips;
|
|
389
493
|
void rootSkip;
|
|
390
494
|
try {
|
|
391
|
-
await writeTemplateToDir2(
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
495
|
+
await writeTemplateToDir2(
|
|
496
|
+
tmpTemplate,
|
|
497
|
+
repoDir,
|
|
498
|
+
components,
|
|
499
|
+
componentPaths,
|
|
500
|
+
vars,
|
|
501
|
+
version,
|
|
502
|
+
{
|
|
503
|
+
componentSkips: {},
|
|
504
|
+
rootSkip: [],
|
|
505
|
+
realCwd: tmpTemplate
|
|
506
|
+
}
|
|
507
|
+
);
|
|
396
508
|
const updates = [];
|
|
397
509
|
for (const file of rootPinned) {
|
|
398
510
|
const tmplPath = join2(tmpTemplate, file);
|
|
@@ -439,20 +551,33 @@ async function promptSkipLearning(cwd, componentPaths, version, conflictedFiles)
|
|
|
439
551
|
});
|
|
440
552
|
if (changedFiles.length === 0) return false;
|
|
441
553
|
if (!process.stdin.isTTY) {
|
|
442
|
-
p3.log.info(
|
|
443
|
-
|
|
554
|
+
p3.log.info(
|
|
555
|
+
"Non-interactive: skipping prompt. Resolve conflicts manually with `git diff` then `git add`."
|
|
556
|
+
);
|
|
557
|
+
p3.log.info(
|
|
558
|
+
"Re-run `npx create-projx update` later to interactively decide which files to keep."
|
|
559
|
+
);
|
|
444
560
|
return false;
|
|
445
561
|
}
|
|
446
|
-
const statusOutput = execSync("git status --porcelain", {
|
|
562
|
+
const statusOutput = execSync("git status --porcelain", {
|
|
563
|
+
cwd,
|
|
564
|
+
stdio: "pipe"
|
|
565
|
+
}).toString().trim();
|
|
447
566
|
const entries = statusOutput.split("\n").filter(Boolean).map((line) => ({
|
|
448
567
|
status: line.slice(0, 2).trim(),
|
|
449
568
|
file: line.slice(3).trim()
|
|
450
569
|
}));
|
|
451
570
|
p3.log.warn(`${changedFiles.length} file(s) have conflicts to resolve.`);
|
|
452
|
-
p3.log.info(
|
|
571
|
+
p3.log.info(
|
|
572
|
+
"Each file is currently in your working tree with conflict markers."
|
|
573
|
+
);
|
|
453
574
|
p3.log.info("");
|
|
454
|
-
p3.log.info(
|
|
455
|
-
|
|
575
|
+
p3.log.info(
|
|
576
|
+
"CHECKED = keep your version, resolve markers manually, commit when ready"
|
|
577
|
+
);
|
|
578
|
+
p3.log.info(
|
|
579
|
+
"UNCHECKED = discard template's changes AND skip this file on future updates"
|
|
580
|
+
);
|
|
456
581
|
p3.log.info("");
|
|
457
582
|
const selected = await p3.multiselect({
|
|
458
583
|
message: "Which files do you want to KEEP?",
|
|
@@ -484,10 +609,10 @@ async function promptSkipLearning(cwd, componentPaths, version, conflictedFiles)
|
|
|
484
609
|
);
|
|
485
610
|
}
|
|
486
611
|
if (kept.size > 0) {
|
|
487
|
-
p3.log.info(`${kept.size} file(s) kept with conflict markers \u2014 resolve and commit:`);
|
|
488
612
|
p3.log.info(
|
|
489
|
-
|
|
613
|
+
`${kept.size} file(s) kept with conflict markers \u2014 resolve and commit:`
|
|
490
614
|
);
|
|
615
|
+
p3.log.info(` git add . && git commit -m "projx: update to v${version}"`);
|
|
491
616
|
p3.outro(`Template v${version} applied.`);
|
|
492
617
|
} else {
|
|
493
618
|
p3.outro("All template changes discarded. Skip list updated.");
|
|
@@ -536,15 +661,38 @@ import { copyFileSync as copyFileSync2, existsSync as existsSync3 } from "fs";
|
|
|
536
661
|
import { readFile as readFile3 } from "fs/promises";
|
|
537
662
|
import { join as join3 } from "path";
|
|
538
663
|
import * as p4 from "@clack/prompts";
|
|
539
|
-
async function add(cwd, newComponents, localRepo, skipInstall = false) {
|
|
664
|
+
async function add(cwd, newComponents, localRepo, skipInstall = false, customName) {
|
|
540
665
|
p4.intro("projx add");
|
|
541
666
|
const isLocal = !!localRepo;
|
|
542
667
|
if (!existsSync3(join3(cwd, ".projx"))) {
|
|
543
|
-
p4.log.error(
|
|
668
|
+
p4.log.error(
|
|
669
|
+
"No .projx file found. Run 'npx create-projx <name>' to create a project first."
|
|
670
|
+
);
|
|
544
671
|
process.exit(1);
|
|
545
672
|
}
|
|
546
673
|
const config = await readProjxConfig(cwd);
|
|
547
674
|
const { components: existing } = await discoverComponentsFromMarkers(cwd);
|
|
675
|
+
if (customName) {
|
|
676
|
+
if (newComponents.length !== 1) {
|
|
677
|
+
throw new Error(
|
|
678
|
+
"--name can only be used when adding a single component type."
|
|
679
|
+
);
|
|
680
|
+
}
|
|
681
|
+
const targetDir = join3(cwd, customName);
|
|
682
|
+
if (existsSync3(targetDir)) {
|
|
683
|
+
throw new Error(`Directory '${customName}' already exists.`);
|
|
684
|
+
}
|
|
685
|
+
return await addInstance(
|
|
686
|
+
cwd,
|
|
687
|
+
newComponents[0],
|
|
688
|
+
customName,
|
|
689
|
+
config,
|
|
690
|
+
existing,
|
|
691
|
+
localRepo,
|
|
692
|
+
skipInstall,
|
|
693
|
+
isLocal
|
|
694
|
+
);
|
|
695
|
+
}
|
|
548
696
|
const alreadyExists = newComponents.filter((c) => existing.includes(c));
|
|
549
697
|
if (alreadyExists.length > 0) {
|
|
550
698
|
p4.log.warn(`Already present: ${alreadyExists.join(", ")}. Skipping those.`);
|
|
@@ -556,7 +704,9 @@ async function add(cwd, newComponents, localRepo, skipInstall = false) {
|
|
|
556
704
|
}
|
|
557
705
|
p4.log.info(`Adding: ${toAdd.join(", ")}`);
|
|
558
706
|
const dlSpinner = p4.spinner();
|
|
559
|
-
dlSpinner.start(
|
|
707
|
+
dlSpinner.start(
|
|
708
|
+
isLocal ? "Using local templates" : "Downloading latest templates"
|
|
709
|
+
);
|
|
560
710
|
const repoDir = await downloadRepo(localRepo).catch((err) => {
|
|
561
711
|
dlSpinner.stop("Failed.");
|
|
562
712
|
p4.log.error(String(err));
|
|
@@ -568,17 +718,42 @@ async function add(cwd, newComponents, localRepo, skipInstall = false) {
|
|
|
568
718
|
const existingPaths = await discoverComponentPaths(cwd, existing);
|
|
569
719
|
const paths = { ...existingPaths };
|
|
570
720
|
for (const c of toAdd) paths[c] = c;
|
|
721
|
+
const { instances: existingInstances } = await discoverComponentsFromMarkers(cwd);
|
|
722
|
+
const instances = [
|
|
723
|
+
...existingInstances,
|
|
724
|
+
...toAdd.map((c) => ({ type: c, path: c }))
|
|
725
|
+
];
|
|
571
726
|
const pm = config.packageManager ?? "npm";
|
|
572
727
|
const name = detectProjectName(cwd, existing, paths);
|
|
573
|
-
const vars = {
|
|
574
|
-
|
|
728
|
+
const vars = {
|
|
729
|
+
projectName: name,
|
|
730
|
+
components: allComponents,
|
|
731
|
+
paths,
|
|
732
|
+
instances,
|
|
733
|
+
pm: pmCommands(pm)
|
|
734
|
+
};
|
|
735
|
+
const pkg = JSON.parse(
|
|
736
|
+
await readFile3(join3(repoDir, "cli/package.json"), "utf-8")
|
|
737
|
+
);
|
|
575
738
|
const version = pkg.version;
|
|
576
739
|
const spinner7 = p4.spinner();
|
|
577
740
|
spinner7.start("Adding components");
|
|
578
|
-
await writeTemplateToDir(
|
|
741
|
+
await writeTemplateToDir(
|
|
742
|
+
cwd,
|
|
743
|
+
repoDir,
|
|
744
|
+
allComponents,
|
|
745
|
+
paths,
|
|
746
|
+
vars,
|
|
747
|
+
version,
|
|
748
|
+
{ realCwd: cwd }
|
|
749
|
+
);
|
|
579
750
|
spinner7.stop("Components added.");
|
|
580
751
|
if (!skipInstall) {
|
|
581
|
-
await installDeps2(
|
|
752
|
+
await installDeps2(
|
|
753
|
+
cwd,
|
|
754
|
+
toAdd.map((c) => ({ type: c, path: c })),
|
|
755
|
+
pm
|
|
756
|
+
);
|
|
582
757
|
}
|
|
583
758
|
for (const component of toAdd) {
|
|
584
759
|
const example = join3(cwd, component, ".env.example");
|
|
@@ -590,70 +765,177 @@ async function add(cwd, newComponents, localRepo, skipInstall = false) {
|
|
|
590
765
|
}
|
|
591
766
|
}
|
|
592
767
|
}
|
|
593
|
-
p4.outro(
|
|
768
|
+
p4.outro(
|
|
769
|
+
`Added ${toAdd.join(", ")}.
|
|
594
770
|
|
|
595
|
-
Like projx? Star it: https://github.com/ukanhaupa/projx`
|
|
771
|
+
Like projx? Star it: https://github.com/ukanhaupa/projx`
|
|
772
|
+
);
|
|
773
|
+
} finally {
|
|
774
|
+
await cleanupRepo(repoDir, isLocal);
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
async function addInstance(cwd, type, customName, config, existing, localRepo, skipInstall, isLocal) {
|
|
778
|
+
p4.log.info(`Adding ${type} instance at ${customName}/`);
|
|
779
|
+
const dlSpinner = p4.spinner();
|
|
780
|
+
dlSpinner.start(
|
|
781
|
+
isLocal ? "Using local templates" : "Downloading latest templates"
|
|
782
|
+
);
|
|
783
|
+
const repoDir = await downloadRepo(localRepo).catch((err) => {
|
|
784
|
+
dlSpinner.stop("Failed.");
|
|
785
|
+
throw err;
|
|
786
|
+
});
|
|
787
|
+
dlSpinner.stop(isLocal ? "Local templates loaded." : "Templates downloaded.");
|
|
788
|
+
try {
|
|
789
|
+
const existingPaths = await discoverComponentPaths(cwd, existing);
|
|
790
|
+
const paths = { ...existingPaths };
|
|
791
|
+
const { instances: existingInstances } = await discoverComponentsFromMarkers(cwd);
|
|
792
|
+
const newInstance = { type, path: customName };
|
|
793
|
+
const instances = [...existingInstances, newInstance];
|
|
794
|
+
const pm = config.packageManager ?? "npm";
|
|
795
|
+
const name = detectProjectName(cwd, existing, existingPaths);
|
|
796
|
+
const vars = {
|
|
797
|
+
projectName: name,
|
|
798
|
+
components: existing,
|
|
799
|
+
paths,
|
|
800
|
+
instances,
|
|
801
|
+
pm: pmCommands(pm)
|
|
802
|
+
};
|
|
803
|
+
const pkg = JSON.parse(
|
|
804
|
+
await readFile3(join3(repoDir, "cli/package.json"), "utf-8")
|
|
805
|
+
);
|
|
806
|
+
const version = pkg.version;
|
|
807
|
+
const INSTANCE_AWARE_ROOT = /* @__PURE__ */ new Set([
|
|
808
|
+
".github/workflows/ci.yml",
|
|
809
|
+
".githooks/pre-commit",
|
|
810
|
+
"scripts/setup.sh",
|
|
811
|
+
"docker-compose.yml",
|
|
812
|
+
"docker-compose.dev.yml"
|
|
813
|
+
]);
|
|
814
|
+
const rawSkip = Array.isArray(config.skip) ? config.skip : [];
|
|
815
|
+
const rootSkip = rawSkip.filter((p11) => !INSTANCE_AWARE_ROOT.has(p11));
|
|
816
|
+
const componentSkips = {};
|
|
817
|
+
for (const inst of existingInstances) {
|
|
818
|
+
const m = await readComponentMarker(join3(cwd, inst.path));
|
|
819
|
+
if (m?.skip && m.skip.length > 0) componentSkips[inst.type] = m.skip;
|
|
820
|
+
}
|
|
821
|
+
const spinner7 = p4.spinner();
|
|
822
|
+
spinner7.start(`Scaffolding ${customName}/`);
|
|
823
|
+
const result = await applyTemplate(
|
|
824
|
+
cwd,
|
|
825
|
+
repoDir,
|
|
826
|
+
existing,
|
|
827
|
+
paths,
|
|
828
|
+
vars,
|
|
829
|
+
version,
|
|
830
|
+
componentSkips,
|
|
831
|
+
rootSkip,
|
|
832
|
+
false,
|
|
833
|
+
[newInstance],
|
|
834
|
+
[newInstance]
|
|
835
|
+
);
|
|
836
|
+
spinner7.stop(`Scaffolded ${customName}/.`);
|
|
837
|
+
if (result.status === "merged") {
|
|
838
|
+
p4.log.success(
|
|
839
|
+
`${result.mergedFiles?.length ?? 0} root file(s) merged cleanly.`
|
|
840
|
+
);
|
|
841
|
+
} else if (result.status === "conflicts") {
|
|
842
|
+
const conflictCount = result.conflictedFiles?.length ?? 0;
|
|
843
|
+
if (conflictCount > 0) {
|
|
844
|
+
p4.log.warn(`${conflictCount} root file(s) need manual review:`);
|
|
845
|
+
for (const f of result.conflictedFiles) p4.log.info(` ${f}`);
|
|
846
|
+
p4.log.info("Review: git diff");
|
|
847
|
+
p4.log.info("Keep: git add <file>");
|
|
848
|
+
p4.log.info("Discard: git checkout -- <file>");
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
if (!skipInstall) {
|
|
852
|
+
await installDeps2(cwd, [{ type, path: customName }], pm);
|
|
853
|
+
}
|
|
854
|
+
const example = join3(cwd, customName, ".env.example");
|
|
855
|
+
const env = join3(cwd, customName, ".env");
|
|
856
|
+
if (existsSync3(example) && !existsSync3(env)) {
|
|
857
|
+
try {
|
|
858
|
+
copyFileSync2(example, env);
|
|
859
|
+
} catch {
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
p4.outro(`Added ${type} instance at ${customName}/.`);
|
|
596
863
|
} finally {
|
|
597
864
|
await cleanupRepo(repoDir, isLocal);
|
|
598
865
|
}
|
|
599
866
|
}
|
|
600
|
-
async function installDeps2(dest,
|
|
867
|
+
async function installDeps2(dest, instances, pm) {
|
|
601
868
|
const cmds = pmCommands(pm);
|
|
602
869
|
const pmBin = pm === "bun" ? "bun" : pm;
|
|
603
|
-
for (const
|
|
870
|
+
for (const { type, path } of instances) {
|
|
871
|
+
const dir = join3(dest, path);
|
|
604
872
|
const spinner7 = p4.spinner();
|
|
605
873
|
try {
|
|
606
|
-
switch (
|
|
874
|
+
switch (type) {
|
|
607
875
|
case "fastapi":
|
|
608
876
|
if (hasCommand("uv")) {
|
|
609
|
-
spinner7.start(
|
|
610
|
-
exec("uv sync --all-extras",
|
|
611
|
-
spinner7.stop(
|
|
877
|
+
spinner7.start(`Installing FastAPI dependencies (${path}/)`);
|
|
878
|
+
exec("uv sync --all-extras", dir);
|
|
879
|
+
spinner7.stop(`FastAPI dependencies installed (${path}/).`);
|
|
612
880
|
} else {
|
|
613
|
-
p4.log.warn(
|
|
881
|
+
p4.log.warn(`uv not found \u2014 run 'cd ${path} && uv sync' manually.`);
|
|
614
882
|
}
|
|
615
883
|
break;
|
|
616
884
|
case "fastify":
|
|
617
885
|
if (hasCommand(pmBin)) {
|
|
618
|
-
spinner7.start(
|
|
619
|
-
|
|
620
|
-
|
|
886
|
+
spinner7.start(
|
|
887
|
+
`Installing Fastify dependencies (${path}/, ${cmds.install})`
|
|
888
|
+
);
|
|
889
|
+
exec(cmds.install, dir);
|
|
890
|
+
spinner7.stop(`Fastify dependencies installed (${path}/).`);
|
|
621
891
|
} else {
|
|
622
|
-
p4.log.warn(
|
|
892
|
+
p4.log.warn(
|
|
893
|
+
`${pm} not found \u2014 run 'cd ${path} && ${cmds.install}' manually.`
|
|
894
|
+
);
|
|
623
895
|
}
|
|
624
896
|
break;
|
|
625
897
|
case "frontend":
|
|
626
898
|
if (hasCommand(pmBin)) {
|
|
627
|
-
spinner7.start(
|
|
628
|
-
|
|
629
|
-
|
|
899
|
+
spinner7.start(
|
|
900
|
+
`Installing Frontend dependencies (${path}/, ${cmds.install})`
|
|
901
|
+
);
|
|
902
|
+
exec(cmds.install, dir);
|
|
903
|
+
spinner7.stop(`Frontend dependencies installed (${path}/).`);
|
|
630
904
|
} else {
|
|
631
|
-
p4.log.warn(
|
|
905
|
+
p4.log.warn(
|
|
906
|
+
`${pm} not found \u2014 run 'cd ${path} && ${cmds.install}' manually.`
|
|
907
|
+
);
|
|
632
908
|
}
|
|
633
909
|
break;
|
|
634
910
|
case "e2e":
|
|
635
911
|
if (hasCommand(pmBin)) {
|
|
636
|
-
spinner7.start(
|
|
637
|
-
|
|
638
|
-
|
|
912
|
+
spinner7.start(
|
|
913
|
+
`Installing E2E dependencies (${path}/, ${cmds.install})`
|
|
914
|
+
);
|
|
915
|
+
exec(cmds.install, dir);
|
|
916
|
+
spinner7.stop(`E2E dependencies installed (${path}/).`);
|
|
639
917
|
} else {
|
|
640
|
-
p4.log.warn(
|
|
918
|
+
p4.log.warn(
|
|
919
|
+
`${pm} not found \u2014 run 'cd ${path} && ${cmds.install}' manually.`
|
|
920
|
+
);
|
|
641
921
|
}
|
|
642
922
|
break;
|
|
643
923
|
case "mobile":
|
|
644
924
|
if (hasCommand("flutter")) {
|
|
645
|
-
spinner7.start(
|
|
646
|
-
exec("flutter pub get",
|
|
647
|
-
spinner7.stop(
|
|
925
|
+
spinner7.start(`Installing Flutter dependencies (${path}/)`);
|
|
926
|
+
exec("flutter pub get", dir);
|
|
927
|
+
spinner7.stop(`Flutter dependencies installed (${path}/).`);
|
|
648
928
|
} else {
|
|
649
|
-
p4.log.warn(
|
|
929
|
+
p4.log.warn(
|
|
930
|
+
`Flutter not found \u2014 run 'cd ${path} && flutter pub get' manually.`
|
|
931
|
+
);
|
|
650
932
|
}
|
|
651
933
|
break;
|
|
652
934
|
case "infra":
|
|
653
935
|
break;
|
|
654
936
|
}
|
|
655
937
|
} catch {
|
|
656
|
-
spinner7.stop(`Failed to install ${
|
|
938
|
+
spinner7.stop(`Failed to install ${type} dependencies (${path}/).`);
|
|
657
939
|
}
|
|
658
940
|
}
|
|
659
941
|
}
|
|
@@ -672,7 +954,9 @@ import { join as join4 } from "path";
|
|
|
672
954
|
async function detectComponents(cwd) {
|
|
673
955
|
const results = [];
|
|
674
956
|
const entries = await readdir(cwd, { withFileTypes: true });
|
|
675
|
-
const dirs = entries.filter(
|
|
957
|
+
const dirs = entries.filter(
|
|
958
|
+
(e) => e.isDirectory() && !e.name.startsWith(".") && !EXCLUDE.has(e.name)
|
|
959
|
+
).map((e) => e.name);
|
|
676
960
|
for (const dir of dirs) {
|
|
677
961
|
const full = join4(cwd, dir);
|
|
678
962
|
const detections = await scanDirectory(full, dir);
|
|
@@ -754,11 +1038,15 @@ async function init(cwd, localRepo) {
|
|
|
754
1038
|
p5.intro("projx init");
|
|
755
1039
|
const isLocal = !!localRepo;
|
|
756
1040
|
if (existsSync5(join5(cwd, ".projx"))) {
|
|
757
|
-
p5.log.error(
|
|
1041
|
+
p5.log.error(
|
|
1042
|
+
"This project is already initialized. Use 'npx create-projx update' or 'npx create-projx add' instead."
|
|
1043
|
+
);
|
|
758
1044
|
process.exit(1);
|
|
759
1045
|
}
|
|
760
1046
|
if (!isGitRepo2(cwd)) {
|
|
761
|
-
p5.log.error(
|
|
1047
|
+
p5.log.error(
|
|
1048
|
+
`projx init requires a git repo. Run 'git init && git add -A && git commit -m "initial"' first.`
|
|
1049
|
+
);
|
|
762
1050
|
process.exit(1);
|
|
763
1051
|
}
|
|
764
1052
|
if (hasUncommittedChanges2(cwd)) {
|
|
@@ -771,12 +1059,14 @@ async function init(cwd, localRepo) {
|
|
|
771
1059
|
spinner7.stop(
|
|
772
1060
|
detected.length > 0 ? `Found ${detected.length} component(s).` : "No components detected."
|
|
773
1061
|
);
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
1062
|
+
if (detected.length === 0) {
|
|
1063
|
+
await writeBareProjx(cwd, localRepo, isLocal, detectPackageManager(cwd));
|
|
1064
|
+
p5.outro(
|
|
1065
|
+
"Initialized empty .projx. Add components with 'npx create-projx add <component>'."
|
|
1066
|
+
);
|
|
1067
|
+
return;
|
|
779
1068
|
}
|
|
1069
|
+
const confirmed = await confirmDetections(detected);
|
|
780
1070
|
if (confirmed.length === 0) {
|
|
781
1071
|
p5.log.warn("No components selected. Nothing to do.");
|
|
782
1072
|
process.exit(0);
|
|
@@ -785,7 +1075,9 @@ async function init(cwd, localRepo) {
|
|
|
785
1075
|
const paths = Object.fromEntries(
|
|
786
1076
|
confirmed.map((c) => [c.component, c.directory])
|
|
787
1077
|
);
|
|
788
|
-
const hasJs = components.some(
|
|
1078
|
+
const hasJs = components.some(
|
|
1079
|
+
(c) => ["fastify", "frontend", "e2e"].includes(c)
|
|
1080
|
+
);
|
|
789
1081
|
let pm = "npm";
|
|
790
1082
|
if (hasJs) {
|
|
791
1083
|
const detected2 = detectPackageManager(cwd);
|
|
@@ -803,9 +1095,16 @@ async function init(cwd, localRepo) {
|
|
|
803
1095
|
}
|
|
804
1096
|
}
|
|
805
1097
|
const projectName = toKebab(cwd.split("/").pop());
|
|
806
|
-
const vars = {
|
|
1098
|
+
const vars = {
|
|
1099
|
+
projectName,
|
|
1100
|
+
components,
|
|
1101
|
+
paths,
|
|
1102
|
+
pm: pmCommands(pm)
|
|
1103
|
+
};
|
|
807
1104
|
const dlSpinner = p5.spinner();
|
|
808
|
-
dlSpinner.start(
|
|
1105
|
+
dlSpinner.start(
|
|
1106
|
+
isLocal ? "Using local templates" : "Downloading latest templates"
|
|
1107
|
+
);
|
|
809
1108
|
const repoDir = await downloadRepo(localRepo).catch((err) => {
|
|
810
1109
|
dlSpinner.stop("Failed.");
|
|
811
1110
|
p5.log.error(String(err));
|
|
@@ -813,11 +1112,23 @@ async function init(cwd, localRepo) {
|
|
|
813
1112
|
});
|
|
814
1113
|
dlSpinner.stop(isLocal ? "Local templates loaded." : "Templates downloaded.");
|
|
815
1114
|
try {
|
|
816
|
-
const pkg = JSON.parse(
|
|
1115
|
+
const pkg = JSON.parse(
|
|
1116
|
+
await readFile4(join5(repoDir, "cli/package.json"), "utf-8")
|
|
1117
|
+
);
|
|
817
1118
|
const version = pkg.version;
|
|
818
1119
|
const applySpinner = p5.spinner();
|
|
819
1120
|
applySpinner.start("Applying template");
|
|
820
|
-
const result = await applyTemplate(
|
|
1121
|
+
const result = await applyTemplate(
|
|
1122
|
+
cwd,
|
|
1123
|
+
repoDir,
|
|
1124
|
+
components,
|
|
1125
|
+
paths,
|
|
1126
|
+
vars,
|
|
1127
|
+
version,
|
|
1128
|
+
void 0,
|
|
1129
|
+
void 0,
|
|
1130
|
+
true
|
|
1131
|
+
);
|
|
821
1132
|
applySpinner.stop("Template applied.");
|
|
822
1133
|
if (existsSync5(join5(cwd, ".githooks"))) {
|
|
823
1134
|
try {
|
|
@@ -829,24 +1140,62 @@ async function init(cwd, localRepo) {
|
|
|
829
1140
|
saveBaselineRef(cwd);
|
|
830
1141
|
}
|
|
831
1142
|
if (result.status === "conflicts") {
|
|
832
|
-
p5.log.warn(
|
|
1143
|
+
p5.log.warn(
|
|
1144
|
+
"Some template files differ from your code. Changes written directly."
|
|
1145
|
+
);
|
|
833
1146
|
p5.log.info("Review changes:");
|
|
834
1147
|
p5.log.info(" git diff");
|
|
835
1148
|
p5.log.info("");
|
|
836
1149
|
p5.log.info("Keep a change: git add <file>");
|
|
837
1150
|
p5.log.info("Discard a change: git checkout -- <file>");
|
|
838
|
-
p5.log.info(
|
|
1151
|
+
p5.log.info(
|
|
1152
|
+
'Commit when ready: git add . && git commit -m "projx: init"'
|
|
1153
|
+
);
|
|
839
1154
|
p5.log.info("");
|
|
840
1155
|
p5.log.info("To skip files on future updates, add to .projx-component:");
|
|
841
1156
|
p5.log.info(' { "skip": ["src/**", "tests/**"] }');
|
|
842
|
-
p5.outro(
|
|
1157
|
+
p5.outro(
|
|
1158
|
+
"Template applied. Review with git diff.\n\n Like projx? Star it: https://github.com/ukanhaupa/projx"
|
|
1159
|
+
);
|
|
843
1160
|
} else {
|
|
844
|
-
p5.outro(
|
|
1161
|
+
p5.outro(
|
|
1162
|
+
"Project initialized.\n\n Like projx? Star it: https://github.com/ukanhaupa/projx"
|
|
1163
|
+
);
|
|
845
1164
|
}
|
|
846
1165
|
} finally {
|
|
847
1166
|
await cleanupRepo(repoDir, isLocal);
|
|
848
1167
|
}
|
|
849
1168
|
}
|
|
1169
|
+
async function writeBareProjx(cwd, localRepo, isLocal, pm) {
|
|
1170
|
+
const dlSpinner = p5.spinner();
|
|
1171
|
+
dlSpinner.start(
|
|
1172
|
+
isLocal ? "Using local templates" : "Downloading latest templates"
|
|
1173
|
+
);
|
|
1174
|
+
const repoDir = await downloadRepo(localRepo).catch((err) => {
|
|
1175
|
+
dlSpinner.stop("Failed.");
|
|
1176
|
+
p5.log.error(String(err));
|
|
1177
|
+
process.exit(1);
|
|
1178
|
+
});
|
|
1179
|
+
dlSpinner.stop(isLocal ? "Local templates loaded." : "Templates downloaded.");
|
|
1180
|
+
try {
|
|
1181
|
+
const pkg = JSON.parse(
|
|
1182
|
+
await readFile4(join5(repoDir, "cli/package.json"), "utf-8")
|
|
1183
|
+
);
|
|
1184
|
+
const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
1185
|
+
const config = {
|
|
1186
|
+
version: pkg.version,
|
|
1187
|
+
createdAt: today,
|
|
1188
|
+
updatedAt: today,
|
|
1189
|
+
skip: [...DEFAULT_ROOT_SKIP_PATTERNS],
|
|
1190
|
+
defaultsApplied: true
|
|
1191
|
+
};
|
|
1192
|
+
if (pm) config.packageManager = pm;
|
|
1193
|
+
await writeProjxConfig(cwd, config);
|
|
1194
|
+
saveBaselineRef(cwd);
|
|
1195
|
+
} finally {
|
|
1196
|
+
await cleanupRepo(repoDir, isLocal);
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
850
1199
|
async function confirmDetections(detected) {
|
|
851
1200
|
const confirmed = [];
|
|
852
1201
|
for (const d of detected) {
|
|
@@ -861,33 +1210,6 @@ async function confirmDetections(detected) {
|
|
|
861
1210
|
}
|
|
862
1211
|
return confirmed;
|
|
863
1212
|
}
|
|
864
|
-
async function manualSelect(cwd) {
|
|
865
|
-
const selected = await p5.multiselect({
|
|
866
|
-
message: "No components detected. Select manually:",
|
|
867
|
-
options: COMPONENTS.map((c) => ({
|
|
868
|
-
value: c,
|
|
869
|
-
label: LABELS[c].label,
|
|
870
|
-
hint: LABELS[c].hint
|
|
871
|
-
})),
|
|
872
|
-
required: false
|
|
873
|
-
});
|
|
874
|
-
if (p5.isCancel(selected)) process.exit(0);
|
|
875
|
-
const result = [];
|
|
876
|
-
for (const component of selected) {
|
|
877
|
-
const dir = await p5.text({
|
|
878
|
-
message: `Directory for ${LABELS[component].label}?`,
|
|
879
|
-
placeholder: component,
|
|
880
|
-
defaultValue: component
|
|
881
|
-
});
|
|
882
|
-
if (p5.isCancel(dir)) process.exit(0);
|
|
883
|
-
if (!existsSync5(join5(cwd, dir))) {
|
|
884
|
-
p5.log.warn(`${dir}/ does not exist \u2014 skipping.`);
|
|
885
|
-
continue;
|
|
886
|
-
}
|
|
887
|
-
result.push({ component, directory: dir });
|
|
888
|
-
}
|
|
889
|
-
return result;
|
|
890
|
-
}
|
|
891
1213
|
function isGitRepo2(cwd) {
|
|
892
1214
|
try {
|
|
893
1215
|
execSync2("git rev-parse --is-inside-work-tree", { cwd, stdio: "pipe" });
|
|
@@ -940,7 +1262,10 @@ async function pin(cwd, patterns) {
|
|
|
940
1262
|
p6.log.warn(`Cannot pin ${pattern} \u2014 config files are managed by projx.`);
|
|
941
1263
|
continue;
|
|
942
1264
|
}
|
|
943
|
-
const { scope, component, relative } = classifyPattern(
|
|
1265
|
+
const { scope, component, relative } = classifyPattern(
|
|
1266
|
+
pattern,
|
|
1267
|
+
componentPaths
|
|
1268
|
+
);
|
|
944
1269
|
if (scope === "component" && component) {
|
|
945
1270
|
if (!componentAdds[component]) componentAdds[component] = [];
|
|
946
1271
|
componentAdds[component].push(relative);
|
|
@@ -989,7 +1314,10 @@ async function unpin(cwd, patterns) {
|
|
|
989
1314
|
const rootRemoves = [];
|
|
990
1315
|
const componentRemoves = {};
|
|
991
1316
|
for (const pattern of patterns) {
|
|
992
|
-
const { scope, component, relative } = classifyPattern(
|
|
1317
|
+
const { scope, component, relative } = classifyPattern(
|
|
1318
|
+
pattern,
|
|
1319
|
+
componentPaths
|
|
1320
|
+
);
|
|
993
1321
|
if (scope === "component" && component) {
|
|
994
1322
|
if (!componentRemoves[component]) componentRemoves[component] = [];
|
|
995
1323
|
componentRemoves[component].push(relative);
|
|
@@ -1089,7 +1417,11 @@ async function checkConfig(cwd) {
|
|
|
1089
1417
|
});
|
|
1090
1418
|
return { results };
|
|
1091
1419
|
}
|
|
1092
|
-
results.push({
|
|
1420
|
+
results.push({
|
|
1421
|
+
name: ".projx exists",
|
|
1422
|
+
status: "pass",
|
|
1423
|
+
message: `v${rootConfig.version ?? "unknown"}`
|
|
1424
|
+
});
|
|
1093
1425
|
if (!rootConfig.version) {
|
|
1094
1426
|
results.push({
|
|
1095
1427
|
name: ".projx fields",
|
|
@@ -1110,7 +1442,11 @@ async function checkComponents(cwd, components, componentPaths) {
|
|
|
1110
1442
|
});
|
|
1111
1443
|
return results;
|
|
1112
1444
|
}
|
|
1113
|
-
results.push({
|
|
1445
|
+
results.push({
|
|
1446
|
+
name: "components",
|
|
1447
|
+
status: "pass",
|
|
1448
|
+
message: `${components.length} discovered from markers`
|
|
1449
|
+
});
|
|
1114
1450
|
for (const component of components) {
|
|
1115
1451
|
const dir = componentPaths[component];
|
|
1116
1452
|
const fullDir = join7(cwd, dir);
|
|
@@ -1133,7 +1469,11 @@ async function checkComponents(cwd, components, componentPaths) {
|
|
|
1133
1469
|
continue;
|
|
1134
1470
|
}
|
|
1135
1471
|
const label = dir !== component ? `${dir}/ (${component})` : `${component}/`;
|
|
1136
|
-
results.push({
|
|
1472
|
+
results.push({
|
|
1473
|
+
name: `${component} marker`,
|
|
1474
|
+
status: "pass",
|
|
1475
|
+
message: label
|
|
1476
|
+
});
|
|
1137
1477
|
}
|
|
1138
1478
|
return results;
|
|
1139
1479
|
}
|
|
@@ -1143,18 +1483,36 @@ function checkGit(cwd, fix) {
|
|
|
1143
1483
|
execSync3("git rev-parse --is-inside-work-tree", { cwd, stdio: "pipe" });
|
|
1144
1484
|
results.push({ name: "git repo", status: "pass", message: "OK" });
|
|
1145
1485
|
} catch {
|
|
1146
|
-
results.push({
|
|
1486
|
+
results.push({
|
|
1487
|
+
name: "git repo",
|
|
1488
|
+
status: "fail",
|
|
1489
|
+
message: "Not a git repository."
|
|
1490
|
+
});
|
|
1147
1491
|
return results;
|
|
1148
1492
|
}
|
|
1149
1493
|
try {
|
|
1150
|
-
const ref = execSync3(`git rev-parse --verify ${BASELINE_REF}`, {
|
|
1151
|
-
|
|
1494
|
+
const ref = execSync3(`git rev-parse --verify ${BASELINE_REF}`, {
|
|
1495
|
+
cwd,
|
|
1496
|
+
stdio: "pipe"
|
|
1497
|
+
}).toString().trim();
|
|
1498
|
+
results.push({
|
|
1499
|
+
name: "baseline ref",
|
|
1500
|
+
status: "pass",
|
|
1501
|
+
message: ref.slice(0, 8)
|
|
1502
|
+
});
|
|
1152
1503
|
} catch {
|
|
1153
1504
|
if (fix) {
|
|
1154
1505
|
saveBaselineRef(cwd);
|
|
1155
1506
|
try {
|
|
1156
|
-
execSync3(`git rev-parse --verify ${BASELINE_REF}`, {
|
|
1157
|
-
|
|
1507
|
+
execSync3(`git rev-parse --verify ${BASELINE_REF}`, {
|
|
1508
|
+
cwd,
|
|
1509
|
+
stdio: "pipe"
|
|
1510
|
+
});
|
|
1511
|
+
results.push({
|
|
1512
|
+
name: "baseline ref",
|
|
1513
|
+
status: "pass",
|
|
1514
|
+
message: "Created from git history."
|
|
1515
|
+
});
|
|
1158
1516
|
} catch {
|
|
1159
1517
|
results.push({
|
|
1160
1518
|
name: "baseline ref",
|
|
@@ -1173,12 +1531,19 @@ function checkGit(cwd, fix) {
|
|
|
1173
1531
|
}
|
|
1174
1532
|
}
|
|
1175
1533
|
try {
|
|
1176
|
-
const worktrees = execSync3("git worktree list --porcelain", {
|
|
1534
|
+
const worktrees = execSync3("git worktree list --porcelain", {
|
|
1535
|
+
cwd,
|
|
1536
|
+
stdio: "pipe"
|
|
1537
|
+
}).toString();
|
|
1177
1538
|
const stale = worktrees.split("\n").filter((l) => l.includes("projx-wt-") || l.includes("projx/tmp-"));
|
|
1178
1539
|
if (stale.length > 0) {
|
|
1179
1540
|
if (fix) {
|
|
1180
1541
|
execSync3("git worktree prune", { cwd, stdio: "pipe" });
|
|
1181
|
-
results.push({
|
|
1542
|
+
results.push({
|
|
1543
|
+
name: "worktrees",
|
|
1544
|
+
status: "pass",
|
|
1545
|
+
message: "Pruned stale worktrees."
|
|
1546
|
+
});
|
|
1182
1547
|
} else {
|
|
1183
1548
|
results.push({
|
|
1184
1549
|
name: "worktrees",
|
|
@@ -1198,7 +1563,11 @@ function checkGit(cwd, fix) {
|
|
|
1198
1563
|
const status = execSync3("git status --porcelain", { cwd, stdio: "pipe" }).toString().trim();
|
|
1199
1564
|
if (status) {
|
|
1200
1565
|
const count = status.split("\n").length;
|
|
1201
|
-
results.push({
|
|
1566
|
+
results.push({
|
|
1567
|
+
name: "working tree",
|
|
1568
|
+
status: "warn",
|
|
1569
|
+
message: `${count} uncommitted change(s).`
|
|
1570
|
+
});
|
|
1202
1571
|
} else {
|
|
1203
1572
|
results.push({ name: "working tree", status: "pass", message: "Clean" });
|
|
1204
1573
|
}
|
|
@@ -1236,7 +1605,11 @@ async function checkSkipPatterns(cwd, rootConfig, components, componentPaths) {
|
|
|
1236
1605
|
}
|
|
1237
1606
|
}
|
|
1238
1607
|
if (results.length === 0 && (rootSkip.length > 0 || components.length > 0)) {
|
|
1239
|
-
results.push({
|
|
1608
|
+
results.push({
|
|
1609
|
+
name: "skip patterns",
|
|
1610
|
+
status: "pass",
|
|
1611
|
+
message: "All patterns match files."
|
|
1612
|
+
});
|
|
1240
1613
|
}
|
|
1241
1614
|
return results;
|
|
1242
1615
|
}
|
|
@@ -1275,7 +1648,9 @@ async function doctor(cwd, fix = false) {
|
|
|
1275
1648
|
const { components, paths: componentPaths } = await discoverComponentsFromMarkers(cwd);
|
|
1276
1649
|
allResults.push(...await checkComponents(cwd, components, componentPaths));
|
|
1277
1650
|
allResults.push(...checkGit(cwd, fix));
|
|
1278
|
-
allResults.push(
|
|
1651
|
+
allResults.push(
|
|
1652
|
+
...await checkSkipPatterns(cwd, rootConfig, components, componentPaths)
|
|
1653
|
+
);
|
|
1279
1654
|
printReport(allResults);
|
|
1280
1655
|
const passed = allResults.filter((r) => r.status === "pass").length;
|
|
1281
1656
|
const warns = allResults.filter((r) => r.status === "warn").length;
|
|
@@ -1341,7 +1716,9 @@ async function diff(cwd, localRepo) {
|
|
|
1341
1716
|
}
|
|
1342
1717
|
const rootSkip = Array.isArray(raw.skip) ? raw.skip : [];
|
|
1343
1718
|
const dlSpinner = p8.spinner();
|
|
1344
|
-
dlSpinner.start(
|
|
1719
|
+
dlSpinner.start(
|
|
1720
|
+
isLocal ? "Using local templates" : "Downloading latest templates"
|
|
1721
|
+
);
|
|
1345
1722
|
const repoDir = await downloadRepo(localRepo).catch((err) => {
|
|
1346
1723
|
dlSpinner.stop("Failed.");
|
|
1347
1724
|
p8.log.error(String(err));
|
|
@@ -1349,20 +1726,35 @@ async function diff(cwd, localRepo) {
|
|
|
1349
1726
|
});
|
|
1350
1727
|
dlSpinner.stop(isLocal ? "Local templates loaded." : "Templates downloaded.");
|
|
1351
1728
|
try {
|
|
1352
|
-
const pkg = JSON.parse(
|
|
1729
|
+
const pkg = JSON.parse(
|
|
1730
|
+
await readFile5(join8(repoDir, "cli/package.json"), "utf-8")
|
|
1731
|
+
);
|
|
1353
1732
|
const version = pkg.version;
|
|
1354
1733
|
p8.log.info(`Current: v${raw.version ?? "unknown"} \u2192 Template: v${version}`);
|
|
1355
1734
|
const name = detectProjectName(cwd, components, componentPaths);
|
|
1356
|
-
const vars = {
|
|
1735
|
+
const vars = {
|
|
1736
|
+
projectName: name,
|
|
1737
|
+
components,
|
|
1738
|
+
paths: componentPaths,
|
|
1739
|
+
pm: pmCommands(raw.packageManager ?? "npm")
|
|
1740
|
+
};
|
|
1357
1741
|
const spinner7 = p8.spinner();
|
|
1358
1742
|
spinner7.start("Analyzing changes");
|
|
1359
1743
|
const tmpTemplate = join8(tmpdir(), `projx-diff-${Date.now()}`);
|
|
1360
1744
|
await mkdir2(tmpTemplate, { recursive: true });
|
|
1361
|
-
await writeTemplateToDir(
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1745
|
+
await writeTemplateToDir(
|
|
1746
|
+
tmpTemplate,
|
|
1747
|
+
repoDir,
|
|
1748
|
+
components,
|
|
1749
|
+
componentPaths,
|
|
1750
|
+
vars,
|
|
1751
|
+
version,
|
|
1752
|
+
{
|
|
1753
|
+
componentSkips,
|
|
1754
|
+
rootSkip,
|
|
1755
|
+
realCwd: cwd
|
|
1756
|
+
}
|
|
1757
|
+
);
|
|
1366
1758
|
const baselineRef = getBaselineRef(cwd);
|
|
1367
1759
|
const templateFiles = await collectAllFiles(tmpTemplate, tmpTemplate);
|
|
1368
1760
|
const analyses = [];
|
|
@@ -1409,12 +1801,12 @@ async function diff(cwd, localRepo) {
|
|
|
1409
1801
|
await rm(tmpTemplate, { recursive: true, force: true });
|
|
1410
1802
|
spinner7.stop("Analysis complete.");
|
|
1411
1803
|
const groups = {
|
|
1412
|
-
|
|
1804
|
+
new: [],
|
|
1413
1805
|
"clean-update": [],
|
|
1414
1806
|
"needs-merge": [],
|
|
1415
1807
|
"user-only": [],
|
|
1416
|
-
|
|
1417
|
-
|
|
1808
|
+
unchanged: [],
|
|
1809
|
+
skipped: []
|
|
1418
1810
|
};
|
|
1419
1811
|
for (const a of analyses) {
|
|
1420
1812
|
groups[a.status].push(a);
|
|
@@ -1424,15 +1816,21 @@ async function diff(cwd, localRepo) {
|
|
|
1424
1816
|
for (const a of groups["new"]) p8.log.info(` + ${a.file}`);
|
|
1425
1817
|
}
|
|
1426
1818
|
if (groups["clean-update"].length > 0) {
|
|
1427
|
-
p8.log.success(
|
|
1819
|
+
p8.log.success(
|
|
1820
|
+
`Clean updates \u2014 auto-merged (${groups["clean-update"].length}):`
|
|
1821
|
+
);
|
|
1428
1822
|
for (const a of groups["clean-update"]) p8.log.info(` ~ ${a.file}`);
|
|
1429
1823
|
}
|
|
1430
1824
|
if (groups["needs-merge"].length > 0) {
|
|
1431
|
-
p8.log.warn(
|
|
1825
|
+
p8.log.warn(
|
|
1826
|
+
`Needs merge \u2014 both sides changed (${groups["needs-merge"].length}):`
|
|
1827
|
+
);
|
|
1432
1828
|
for (const a of groups["needs-merge"]) p8.log.info(` ! ${a.file}`);
|
|
1433
1829
|
}
|
|
1434
1830
|
if (groups["user-only"].length > 0) {
|
|
1435
|
-
p8.log.info(
|
|
1831
|
+
p8.log.info(
|
|
1832
|
+
`User-modified only \u2014 no template change (${groups["user-only"].length}):`
|
|
1833
|
+
);
|
|
1436
1834
|
for (const a of groups["user-only"]) p8.log.info(` = ${a.file}`);
|
|
1437
1835
|
}
|
|
1438
1836
|
if (groups["skipped"].length > 0) {
|
|
@@ -1459,12 +1857,21 @@ import { existsSync as existsSync9 } from "fs";
|
|
|
1459
1857
|
import { readFile as readFile6, writeFile, mkdir as mkdir3 } from "fs/promises";
|
|
1460
1858
|
import { join as join9 } from "path";
|
|
1461
1859
|
import * as p9 from "@clack/prompts";
|
|
1462
|
-
var FIELD_TYPES = [
|
|
1860
|
+
var FIELD_TYPES = [
|
|
1861
|
+
"string",
|
|
1862
|
+
"number",
|
|
1863
|
+
"boolean",
|
|
1864
|
+
"date",
|
|
1865
|
+
"datetime",
|
|
1866
|
+
"text",
|
|
1867
|
+
"json"
|
|
1868
|
+
];
|
|
1463
1869
|
function toPascal(s) {
|
|
1464
1870
|
return s.split(/[-_\s]+/).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join("");
|
|
1465
1871
|
}
|
|
1466
1872
|
function pluralize(s) {
|
|
1467
|
-
if (s.endsWith("s") || s.endsWith("x") || s.endsWith("z") || s.endsWith("sh") || s.endsWith("ch"))
|
|
1873
|
+
if (s.endsWith("s") || s.endsWith("x") || s.endsWith("z") || s.endsWith("sh") || s.endsWith("ch"))
|
|
1874
|
+
return s + "es";
|
|
1468
1875
|
if (s.endsWith("y") && !/[aeiou]y$/i.test(s)) return s.slice(0, -1) + "ies";
|
|
1469
1876
|
return s + "s";
|
|
1470
1877
|
}
|
|
@@ -1527,7 +1934,9 @@ async function promptEntityConfig(name) {
|
|
|
1527
1934
|
p9.log.warn("No fields defined. Adding a default 'name' field.");
|
|
1528
1935
|
fields.push({ name: "name", type: "string", required: true });
|
|
1529
1936
|
}
|
|
1530
|
-
const stringFields = fields.filter(
|
|
1937
|
+
const stringFields = fields.filter(
|
|
1938
|
+
(f) => f.type === "string" || f.type === "text"
|
|
1939
|
+
);
|
|
1531
1940
|
let searchableFields = [];
|
|
1532
1941
|
if (stringFields.length > 0) {
|
|
1533
1942
|
const selected = await p9.multiselect({
|
|
@@ -1632,7 +2041,9 @@ function generateFastAPIModel(config) {
|
|
|
1632
2041
|
lines.push("");
|
|
1633
2042
|
for (const field of config.fields) {
|
|
1634
2043
|
const nullable = field.required ? "nullable=False" : "nullable=True";
|
|
1635
|
-
lines.push(
|
|
2044
|
+
lines.push(
|
|
2045
|
+
` ${field.name} = Column(${sqlalchemyType(field.type)}, ${nullable})`
|
|
2046
|
+
);
|
|
1636
2047
|
}
|
|
1637
2048
|
lines.push("");
|
|
1638
2049
|
return lines.join("\n");
|
|
@@ -1726,7 +2137,10 @@ function generateFastifySchemas(config) {
|
|
|
1726
2137
|
}
|
|
1727
2138
|
lines.push(` created_at: Type.String({ format: 'date-time' }),`);
|
|
1728
2139
|
lines.push(` updated_at: Type.String({ format: 'date-time' }),`);
|
|
1729
|
-
if (config.softDelete)
|
|
2140
|
+
if (config.softDelete)
|
|
2141
|
+
lines.push(
|
|
2142
|
+
` deleted_at: Type.Union([Type.String({ format: 'date-time' }), Type.Null()]),`
|
|
2143
|
+
);
|
|
1730
2144
|
lines.push(`});`);
|
|
1731
2145
|
lines.push("");
|
|
1732
2146
|
lines.push(`export type ${className} = Static<typeof ${className}Schema>;`);
|
|
@@ -1741,7 +2155,9 @@ function generateFastifySchemas(config) {
|
|
|
1741
2155
|
}
|
|
1742
2156
|
lines.push(`});`);
|
|
1743
2157
|
lines.push("");
|
|
1744
|
-
lines.push(
|
|
2158
|
+
lines.push(
|
|
2159
|
+
`export type Create${className} = Static<typeof Create${className}Schema>;`
|
|
2160
|
+
);
|
|
1745
2161
|
lines.push("");
|
|
1746
2162
|
lines.push(`export const Update${className}Schema = Type.Object({`);
|
|
1747
2163
|
for (const f of config.fields) {
|
|
@@ -1749,29 +2165,50 @@ function generateFastifySchemas(config) {
|
|
|
1749
2165
|
}
|
|
1750
2166
|
lines.push(`});`);
|
|
1751
2167
|
lines.push("");
|
|
1752
|
-
lines.push(
|
|
2168
|
+
lines.push(
|
|
2169
|
+
`export type Update${className} = Static<typeof Update${className}Schema>;`
|
|
2170
|
+
);
|
|
1753
2171
|
lines.push("");
|
|
1754
2172
|
return lines.join("\n");
|
|
1755
2173
|
}
|
|
1756
2174
|
function generateFastifyIndex(config) {
|
|
1757
2175
|
const className = toPascal(config.name);
|
|
1758
2176
|
const camelConfig = className.charAt(0).toLowerCase() + className.slice(1) + "Config";
|
|
1759
|
-
const allColumns = [
|
|
2177
|
+
const allColumns = [
|
|
2178
|
+
"id",
|
|
2179
|
+
...config.fields.map((f) => f.name),
|
|
2180
|
+
"created_at",
|
|
2181
|
+
"updated_at"
|
|
2182
|
+
];
|
|
1760
2183
|
if (config.softDelete) allColumns.push("deleted_at");
|
|
1761
2184
|
const lines = [];
|
|
1762
|
-
lines.push(
|
|
1763
|
-
|
|
2185
|
+
lines.push(
|
|
2186
|
+
`import { EntityRegistry, type EntityConfig, type FieldMeta } from '../_base/index.js';`
|
|
2187
|
+
);
|
|
2188
|
+
lines.push(
|
|
2189
|
+
`import { ${className}Schema, Create${className}Schema, Update${className}Schema } from './schemas.js';`
|
|
2190
|
+
);
|
|
1764
2191
|
lines.push("");
|
|
1765
2192
|
lines.push(`const fields: FieldMeta[] = [`);
|
|
1766
|
-
lines.push(
|
|
2193
|
+
lines.push(
|
|
2194
|
+
` { key: 'id', label: 'Id', type: 'str', nullable: false, is_auto: true, is_primary_key: true, filterable: true, has_foreign_key: false, field_type: 'text' },`
|
|
2195
|
+
);
|
|
1767
2196
|
for (const f of config.fields) {
|
|
1768
2197
|
const meta = fieldMetaType(f.type);
|
|
1769
|
-
lines.push(
|
|
2198
|
+
lines.push(
|
|
2199
|
+
` { 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}' },`
|
|
2200
|
+
);
|
|
1770
2201
|
}
|
|
1771
|
-
lines.push(
|
|
1772
|
-
|
|
2202
|
+
lines.push(
|
|
2203
|
+
` { 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' },`
|
|
2204
|
+
);
|
|
2205
|
+
lines.push(
|
|
2206
|
+
` { 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' },`
|
|
2207
|
+
);
|
|
1773
2208
|
if (config.softDelete) {
|
|
1774
|
-
lines.push(
|
|
2209
|
+
lines.push(
|
|
2210
|
+
` { 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' },`
|
|
2211
|
+
);
|
|
1775
2212
|
}
|
|
1776
2213
|
lines.push(`];`);
|
|
1777
2214
|
lines.push("");
|
|
@@ -1787,7 +2224,9 @@ function generateFastifyIndex(config) {
|
|
|
1787
2224
|
lines.push(` bulkOperations: ${config.bulkOperations},`);
|
|
1788
2225
|
lines.push(` columnNames: [${allColumns.map((c) => `'${c}'`).join(", ")}],`);
|
|
1789
2226
|
if (config.searchableFields.length > 0) {
|
|
1790
|
-
lines.push(
|
|
2227
|
+
lines.push(
|
|
2228
|
+
` searchableFields: [${config.searchableFields.map((f) => `'${f}'`).join(", ")}],`
|
|
2229
|
+
);
|
|
1791
2230
|
} else {
|
|
1792
2231
|
lines.push(` searchableFields: [],`);
|
|
1793
2232
|
}
|
|
@@ -1898,7 +2337,8 @@ function dartFromJson(fieldName, type, required) {
|
|
|
1898
2337
|
const key = `json['${fieldName}']`;
|
|
1899
2338
|
const isDate = type === "date" || type === "datetime";
|
|
1900
2339
|
if (isDate && required) return `DateTime.parse(${key} as String)`;
|
|
1901
|
-
if (isDate && !required)
|
|
2340
|
+
if (isDate && !required)
|
|
2341
|
+
return `${key} != null ? DateTime.parse(${key} as String) : null`;
|
|
1902
2342
|
if (type === "json" && !required) return `${key} as Map<String, dynamic>?`;
|
|
1903
2343
|
if (type === "json") return `${key} as Map<String, dynamic>`;
|
|
1904
2344
|
const dartT = (() => {
|
|
@@ -1918,14 +2358,22 @@ function dartFromJson(fieldName, type, required) {
|
|
|
1918
2358
|
}
|
|
1919
2359
|
function dartToJson(fieldName, camelName, type, required) {
|
|
1920
2360
|
const isDate = type === "date" || type === "datetime";
|
|
1921
|
-
if (isDate && required)
|
|
1922
|
-
|
|
2361
|
+
if (isDate && required)
|
|
2362
|
+
return `'${fieldName}': ${camelName}.toIso8601String()`;
|
|
2363
|
+
if (isDate && !required)
|
|
2364
|
+
return `'${fieldName}': ${camelName}?.toIso8601String()`;
|
|
1923
2365
|
return `'${fieldName}': ${camelName}`;
|
|
1924
2366
|
}
|
|
1925
2367
|
function generateDartModel(config) {
|
|
1926
2368
|
const className = toPascal(config.name);
|
|
1927
2369
|
const allFields = [
|
|
1928
|
-
{
|
|
2370
|
+
{
|
|
2371
|
+
snake: "id",
|
|
2372
|
+
camel: "id",
|
|
2373
|
+
type: "String",
|
|
2374
|
+
required: true,
|
|
2375
|
+
fieldType: "string"
|
|
2376
|
+
},
|
|
1929
2377
|
...config.fields.map((f) => ({
|
|
1930
2378
|
snake: f.name,
|
|
1931
2379
|
camel: toCamel(f.name),
|
|
@@ -1935,11 +2383,29 @@ function generateDartModel(config) {
|
|
|
1935
2383
|
}))
|
|
1936
2384
|
];
|
|
1937
2385
|
if (config.softDelete) {
|
|
1938
|
-
allFields.push({
|
|
2386
|
+
allFields.push({
|
|
2387
|
+
snake: "deleted_at",
|
|
2388
|
+
camel: "deletedAt",
|
|
2389
|
+
type: "DateTime?",
|
|
2390
|
+
required: false,
|
|
2391
|
+
fieldType: "datetime"
|
|
2392
|
+
});
|
|
1939
2393
|
}
|
|
1940
2394
|
allFields.push(
|
|
1941
|
-
{
|
|
1942
|
-
|
|
2395
|
+
{
|
|
2396
|
+
snake: "created_at",
|
|
2397
|
+
camel: "createdAt",
|
|
2398
|
+
type: "DateTime",
|
|
2399
|
+
required: true,
|
|
2400
|
+
fieldType: "datetime"
|
|
2401
|
+
},
|
|
2402
|
+
{
|
|
2403
|
+
snake: "updated_at",
|
|
2404
|
+
camel: "updatedAt",
|
|
2405
|
+
type: "DateTime",
|
|
2406
|
+
required: true,
|
|
2407
|
+
fieldType: "datetime"
|
|
2408
|
+
}
|
|
1943
2409
|
);
|
|
1944
2410
|
const lines = [];
|
|
1945
2411
|
lines.push(`class ${className} {`);
|
|
@@ -1960,7 +2426,9 @@ function generateDartModel(config) {
|
|
|
1960
2426
|
lines.push(` factory ${className}.fromJson(Map<String, dynamic> json) {`);
|
|
1961
2427
|
lines.push(` return ${className}(`);
|
|
1962
2428
|
for (const f of allFields) {
|
|
1963
|
-
lines.push(
|
|
2429
|
+
lines.push(
|
|
2430
|
+
` ${f.camel}: ${dartFromJson(f.snake, f.fieldType, f.required)},`
|
|
2431
|
+
);
|
|
1964
2432
|
}
|
|
1965
2433
|
lines.push(` );`);
|
|
1966
2434
|
lines.push(` }`);
|
|
@@ -1968,7 +2436,9 @@ function generateDartModel(config) {
|
|
|
1968
2436
|
lines.push(` Map<String, dynamic> toJson() {`);
|
|
1969
2437
|
lines.push(` return {`);
|
|
1970
2438
|
for (const f of allFields) {
|
|
1971
|
-
lines.push(
|
|
2439
|
+
lines.push(
|
|
2440
|
+
` ${dartToJson(f.snake, f.camel, f.fieldType, f.required)},`
|
|
2441
|
+
);
|
|
1972
2442
|
}
|
|
1973
2443
|
lines.push(` };`);
|
|
1974
2444
|
lines.push(` }`);
|
|
@@ -2070,11 +2540,15 @@ function generateFastapiTest(config) {
|
|
|
2070
2540
|
}
|
|
2071
2541
|
lines.push(` }`);
|
|
2072
2542
|
const updateField = config.fields[0];
|
|
2073
|
-
lines.push(
|
|
2543
|
+
lines.push(
|
|
2544
|
+
` update_payload = {"${updateField.name}": ${pyHttpLiteral(updateField.type, "update")}}`
|
|
2545
|
+
);
|
|
2074
2546
|
lines.push(` invalid_payload: dict = {}`);
|
|
2075
2547
|
lines.push(` filter_field = "${filterField.name}"`);
|
|
2076
2548
|
lines.push(` filter_value = ${pyHttpLiteral(filterField.type, "create")}`);
|
|
2077
|
-
lines.push(
|
|
2549
|
+
lines.push(
|
|
2550
|
+
` other_filter_value = ${pyHttpLiteral(filterField.type, "alt")}`
|
|
2551
|
+
);
|
|
2078
2552
|
lines.push("");
|
|
2079
2553
|
lines.push(` def make_model(self, index: int, **overrides):`);
|
|
2080
2554
|
lines.push(` data = {`);
|
|
@@ -2092,7 +2566,9 @@ function generateFastifyTest(config) {
|
|
|
2092
2566
|
const basePath = `/api/v1${config.apiPrefix}`;
|
|
2093
2567
|
const updateField = config.fields[0];
|
|
2094
2568
|
const lines = [];
|
|
2095
|
-
lines.push(
|
|
2569
|
+
lines.push(
|
|
2570
|
+
`import { describeCrudEntity } from '../helpers/crud-test-base.js';`
|
|
2571
|
+
);
|
|
2096
2572
|
lines.push("");
|
|
2097
2573
|
lines.push(`describeCrudEntity({`);
|
|
2098
2574
|
lines.push(` entityName: '${className}',`);
|
|
@@ -2104,7 +2580,9 @@ function generateFastifyTest(config) {
|
|
|
2104
2580
|
}
|
|
2105
2581
|
lines.push(` },`);
|
|
2106
2582
|
lines.push(` updatePayload: {`);
|
|
2107
|
-
lines.push(
|
|
2583
|
+
lines.push(
|
|
2584
|
+
` ${updateField.name}: ${tsLiteral(updateField.type, "update")},`
|
|
2585
|
+
);
|
|
2108
2586
|
lines.push(` },`);
|
|
2109
2587
|
lines.push(`});`);
|
|
2110
2588
|
lines.push("");
|
|
@@ -2150,7 +2628,12 @@ async function gen(cwd, entityName, fieldsFlag, backendFlag) {
|
|
|
2150
2628
|
p9.log.error("No backend component found. Need fastapi or fastify.");
|
|
2151
2629
|
process.exit(1);
|
|
2152
2630
|
}
|
|
2153
|
-
const targetBackend = await resolvePrimaryBackend(
|
|
2631
|
+
const targetBackend = await resolvePrimaryBackend(
|
|
2632
|
+
cwd,
|
|
2633
|
+
hasFastapi,
|
|
2634
|
+
hasFastify,
|
|
2635
|
+
backendFlag
|
|
2636
|
+
);
|
|
2154
2637
|
const genFastapi = targetBackend === "fastapi" && hasFastapi;
|
|
2155
2638
|
const genFastify = targetBackend === "fastify" && hasFastify;
|
|
2156
2639
|
let config;
|
|
@@ -2177,11 +2660,19 @@ async function gen(cwd, entityName, fieldsFlag, backendFlag) {
|
|
|
2177
2660
|
const dir = componentPaths.fastapi;
|
|
2178
2661
|
const entityDir = join9(cwd, dir, "src/entities", toSnake(config.name));
|
|
2179
2662
|
if (existsSync9(entityDir)) {
|
|
2180
|
-
p9.log.warn(
|
|
2663
|
+
p9.log.warn(
|
|
2664
|
+
`${dir}/src/entities/${toSnake(config.name)}/ already exists. Skipping FastAPI.`
|
|
2665
|
+
);
|
|
2181
2666
|
} else {
|
|
2182
2667
|
await mkdir3(entityDir, { recursive: true });
|
|
2183
|
-
await writeFile(
|
|
2184
|
-
|
|
2668
|
+
await writeFile(
|
|
2669
|
+
join9(entityDir, "_model.py"),
|
|
2670
|
+
generateFastAPIModel(config)
|
|
2671
|
+
);
|
|
2672
|
+
await writeFile(
|
|
2673
|
+
join9(entityDir, "__init__.py"),
|
|
2674
|
+
"from ._model import *\n"
|
|
2675
|
+
);
|
|
2185
2676
|
generated.push(`${dir}/src/entities/${toSnake(config.name)}/_model.py`);
|
|
2186
2677
|
generated.push(`${dir}/src/entities/${toSnake(config.name)}/__init__.py`);
|
|
2187
2678
|
const testsDir = join9(cwd, dir, "tests");
|
|
@@ -2196,11 +2687,19 @@ async function gen(cwd, entityName, fieldsFlag, backendFlag) {
|
|
|
2196
2687
|
const dir = componentPaths.fastify;
|
|
2197
2688
|
const moduleDir = join9(cwd, dir, "src/modules", toKebab(config.name));
|
|
2198
2689
|
if (existsSync9(moduleDir)) {
|
|
2199
|
-
p9.log.warn(
|
|
2690
|
+
p9.log.warn(
|
|
2691
|
+
`${dir}/src/modules/${toKebab(config.name)}/ already exists. Skipping Fastify.`
|
|
2692
|
+
);
|
|
2200
2693
|
} else {
|
|
2201
2694
|
await mkdir3(moduleDir, { recursive: true });
|
|
2202
|
-
await writeFile(
|
|
2203
|
-
|
|
2695
|
+
await writeFile(
|
|
2696
|
+
join9(moduleDir, "schemas.ts"),
|
|
2697
|
+
generateFastifySchemas(config)
|
|
2698
|
+
);
|
|
2699
|
+
await writeFile(
|
|
2700
|
+
join9(moduleDir, "index.ts"),
|
|
2701
|
+
generateFastifyIndex(config)
|
|
2702
|
+
);
|
|
2204
2703
|
generated.push(`${dir}/src/modules/${toKebab(config.name)}/schemas.ts`);
|
|
2205
2704
|
generated.push(`${dir}/src/modules/${toKebab(config.name)}/index.ts`);
|
|
2206
2705
|
const appPath = join9(cwd, dir, "src/app.ts");
|
|
@@ -2225,12 +2724,18 @@ async function gen(cwd, entityName, fieldsFlag, backendFlag) {
|
|
|
2225
2724
|
const modelName = `model ${toPascal(config.name)}`;
|
|
2226
2725
|
if (!prismaContent.includes(modelName)) {
|
|
2227
2726
|
const prismaModel = generatePrismaModel(config);
|
|
2228
|
-
await writeFile(
|
|
2727
|
+
await writeFile(
|
|
2728
|
+
prismaPath,
|
|
2729
|
+
prismaContent.trimEnd() + "\n\n" + prismaModel + "\n"
|
|
2730
|
+
);
|
|
2229
2731
|
generated.push(`${dir}/prisma/schema.prisma (model added)`);
|
|
2230
2732
|
}
|
|
2231
2733
|
}
|
|
2232
2734
|
const testsModulesDir = join9(cwd, dir, "tests/modules");
|
|
2233
|
-
const fastifyTestFile = join9(
|
|
2735
|
+
const fastifyTestFile = join9(
|
|
2736
|
+
testsModulesDir,
|
|
2737
|
+
`${toKebab(config.name)}.test.ts`
|
|
2738
|
+
);
|
|
2234
2739
|
if (existsSync9(testsModulesDir) && !existsSync9(fastifyTestFile)) {
|
|
2235
2740
|
await writeFile(fastifyTestFile, generateFastifyTest(config));
|
|
2236
2741
|
generated.push(`${dir}/tests/modules/${toKebab(config.name)}.test.ts`);
|
|
@@ -2243,7 +2748,9 @@ async function gen(cwd, entityName, fieldsFlag, backendFlag) {
|
|
|
2243
2748
|
const fileName = toKebab(config.name) + ".ts";
|
|
2244
2749
|
const filePath = join9(typesDir, fileName);
|
|
2245
2750
|
if (existsSync9(filePath)) {
|
|
2246
|
-
p9.log.warn(
|
|
2751
|
+
p9.log.warn(
|
|
2752
|
+
`${dir}/src/types/${fileName} already exists. Skipping frontend types.`
|
|
2753
|
+
);
|
|
2247
2754
|
} else {
|
|
2248
2755
|
await mkdir3(typesDir, { recursive: true });
|
|
2249
2756
|
await writeFile(filePath, generateFrontendInterface(config));
|
|
@@ -2253,7 +2760,10 @@ async function gen(cwd, entityName, fieldsFlag, backendFlag) {
|
|
|
2253
2760
|
if (existsSync9(barrelPath)) {
|
|
2254
2761
|
const content = await readFile6(barrelPath, "utf-8");
|
|
2255
2762
|
if (!content.includes(exportLine)) {
|
|
2256
|
-
await writeFile(
|
|
2763
|
+
await writeFile(
|
|
2764
|
+
barrelPath,
|
|
2765
|
+
content.trimEnd() + "\n" + exportLine + "\n"
|
|
2766
|
+
);
|
|
2257
2767
|
}
|
|
2258
2768
|
} else {
|
|
2259
2769
|
await writeFile(barrelPath, exportLine + "\n");
|
|
@@ -2266,7 +2776,9 @@ async function gen(cwd, entityName, fieldsFlag, backendFlag) {
|
|
|
2266
2776
|
const entityDir = join9(cwd, dir, "lib/entities", toSnake(config.name));
|
|
2267
2777
|
const modelPath = join9(entityDir, "model.dart");
|
|
2268
2778
|
if (existsSync9(modelPath)) {
|
|
2269
|
-
p9.log.warn(
|
|
2779
|
+
p9.log.warn(
|
|
2780
|
+
`${dir}/lib/entities/${toSnake(config.name)}/model.dart already exists. Skipping mobile model.`
|
|
2781
|
+
);
|
|
2270
2782
|
} else {
|
|
2271
2783
|
await mkdir3(entityDir, { recursive: true });
|
|
2272
2784
|
await writeFile(modelPath, generateDartModel(config));
|
|
@@ -2286,19 +2798,27 @@ async function gen(cwd, entityName, fieldsFlag, backendFlag) {
|
|
|
2286
2798
|
if (genFastapi) {
|
|
2287
2799
|
p9.log.info("");
|
|
2288
2800
|
p9.log.info("FastAPI next steps:");
|
|
2289
|
-
p9.log.info(
|
|
2801
|
+
p9.log.info(
|
|
2802
|
+
` alembic revision --autogenerate -m "add ${config.tableName}"`
|
|
2803
|
+
);
|
|
2290
2804
|
p9.log.info(" alembic upgrade head");
|
|
2291
2805
|
}
|
|
2292
2806
|
if (genFastify) {
|
|
2293
2807
|
p9.log.info("");
|
|
2294
2808
|
p9.log.info("Fastify next steps:");
|
|
2295
|
-
p9.log.info(
|
|
2809
|
+
p9.log.info(
|
|
2810
|
+
` ${pm.prismaExec} migrate dev --name add_${toSnake(config.name)}`
|
|
2811
|
+
);
|
|
2296
2812
|
}
|
|
2297
2813
|
if (hasFrontend) {
|
|
2298
2814
|
p9.log.info("");
|
|
2299
2815
|
p9.log.info("Frontend usage:");
|
|
2300
|
-
p9.log.info(
|
|
2301
|
-
|
|
2816
|
+
p9.log.info(
|
|
2817
|
+
` import type { ${className} } from '../types/${toKebab(config.name)}';`
|
|
2818
|
+
);
|
|
2819
|
+
p9.log.info(
|
|
2820
|
+
` const { data } = await api.list<${className}>('${config.apiPrefix}');`
|
|
2821
|
+
);
|
|
2302
2822
|
}
|
|
2303
2823
|
if (hasMobile) {
|
|
2304
2824
|
p9.log.info("");
|
|
@@ -2645,9 +3165,7 @@ function parseArgs() {
|
|
|
2645
3165
|
if (arg === "--components") {
|
|
2646
3166
|
const val = args[++i];
|
|
2647
3167
|
if (val) {
|
|
2648
|
-
options.components = val.split(",").filter(
|
|
2649
|
-
(c) => COMPONENTS.includes(c)
|
|
2650
|
-
);
|
|
3168
|
+
options.components = val.split(",").filter((c) => COMPONENTS.includes(c));
|
|
2651
3169
|
}
|
|
2652
3170
|
continue;
|
|
2653
3171
|
}
|
|
@@ -2697,6 +3215,11 @@ function parseArgs() {
|
|
|
2697
3215
|
if (val) extraArgs.push(`--fields=${val}`);
|
|
2698
3216
|
continue;
|
|
2699
3217
|
}
|
|
3218
|
+
if (arg === "--name") {
|
|
3219
|
+
const val = args[++i];
|
|
3220
|
+
if (val) extraArgs.push(`--name=${val}`);
|
|
3221
|
+
continue;
|
|
3222
|
+
}
|
|
2700
3223
|
if (!arg.startsWith("-")) {
|
|
2701
3224
|
if (command === "add" || command === "pin" || command === "unpin" || command === "gen") {
|
|
2702
3225
|
extraArgs.push(arg);
|
|
@@ -2713,6 +3236,7 @@ function printHelp() {
|
|
|
2713
3236
|
projx <name> [options] Create a new project
|
|
2714
3237
|
projx init Adopt existing project into projx
|
|
2715
3238
|
projx add <components...> Add components to existing project
|
|
3239
|
+
projx add <type> --name <dir> Add another instance of <type> at <dir>
|
|
2716
3240
|
projx update Update scaffolding to latest
|
|
2717
3241
|
projx diff Preview what update would change
|
|
2718
3242
|
projx pin <patterns...> Skip files on future updates
|
|
@@ -2735,6 +3259,7 @@ function printHelp() {
|
|
|
2735
3259
|
npx create-projx my-app --components fastapi,frontend,e2e
|
|
2736
3260
|
npx create-projx my-app -y
|
|
2737
3261
|
npx create-projx add frontend mobile
|
|
3262
|
+
npx create-projx add fastify --name email-ingestor
|
|
2738
3263
|
npx create-projx@latest update
|
|
2739
3264
|
npx create-projx diff
|
|
2740
3265
|
npx create-projx pin backend/pyproject.toml
|
|
@@ -2758,10 +3283,25 @@ async function main() {
|
|
|
2758
3283
|
(c) => COMPONENTS.includes(c)
|
|
2759
3284
|
);
|
|
2760
3285
|
if (components.length === 0) {
|
|
2761
|
-
console.error(
|
|
3286
|
+
console.error(
|
|
3287
|
+
`Error: specify components to add. Available: ${COMPONENTS.join(", ")}`
|
|
3288
|
+
);
|
|
2762
3289
|
process.exit(1);
|
|
2763
3290
|
}
|
|
2764
|
-
|
|
3291
|
+
const customName = extraArgs.find((a) => a.startsWith("--name="))?.slice("--name=".length);
|
|
3292
|
+
if (customName && components.length > 1) {
|
|
3293
|
+
console.error(
|
|
3294
|
+
"Error: --name can only be used when adding a single component type."
|
|
3295
|
+
);
|
|
3296
|
+
process.exit(2);
|
|
3297
|
+
}
|
|
3298
|
+
await add(
|
|
3299
|
+
process.cwd(),
|
|
3300
|
+
components,
|
|
3301
|
+
localRepo,
|
|
3302
|
+
options.install === false,
|
|
3303
|
+
customName
|
|
3304
|
+
);
|
|
2765
3305
|
return;
|
|
2766
3306
|
}
|
|
2767
3307
|
if (command === "pin") {
|
|
@@ -2774,7 +3314,9 @@ async function main() {
|
|
|
2774
3314
|
}
|
|
2775
3315
|
if (command === "unpin") {
|
|
2776
3316
|
if (extraArgs.length === 0) {
|
|
2777
|
-
console.error(
|
|
3317
|
+
console.error(
|
|
3318
|
+
"Error: specify patterns to unpin. Usage: projx unpin <patterns...>"
|
|
3319
|
+
);
|
|
2778
3320
|
process.exit(1);
|
|
2779
3321
|
}
|
|
2780
3322
|
await unpin(process.cwd(), extraArgs);
|
|
@@ -2797,7 +3339,9 @@ async function main() {
|
|
|
2797
3339
|
if (command === "gen") {
|
|
2798
3340
|
const subcommand = extraArgs[0];
|
|
2799
3341
|
if (subcommand !== "entity" || !extraArgs[1]) {
|
|
2800
|
-
console.error(
|
|
3342
|
+
console.error(
|
|
3343
|
+
'Usage: projx gen entity <name> [--fields "name:string,amount:number"]'
|
|
3344
|
+
);
|
|
2801
3345
|
process.exit(1);
|
|
2802
3346
|
}
|
|
2803
3347
|
const entityName = extraArgs[1];
|