create-projx 1.5.4 → 1.5.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/baseline-72Z7TC2E.js +27 -0
- package/dist/chunk-FTHX7ILT.js +424 -0
- package/dist/chunk-G74HYIE4.js +629 -0
- package/dist/index.js +447 -1168
- package/dist/utils-OOY5OZDX.js +70 -0
- package/package.json +1 -1
- package/src/templates/README.md.ejs +10 -10
- package/src/templates/ci.yml.ejs +24 -24
- package/src/templates/docker-compose.dev.yml.ejs +2 -6
- package/src/templates/pre-commit.ejs +34 -34
- package/src/templates/setup.sh.ejs +6 -6
package/dist/index.js
CHANGED
|
@@ -1,380 +1,43 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
BASELINE_REF,
|
|
4
|
+
applyTemplate,
|
|
5
|
+
collectAllFiles,
|
|
6
|
+
detectPackageNameOverrides,
|
|
7
|
+
getBaselineRef,
|
|
8
|
+
getFileAtRef,
|
|
9
|
+
matchesSkip,
|
|
10
|
+
saveBaselineRef,
|
|
11
|
+
writeTemplateToDir
|
|
12
|
+
} from "./chunk-G74HYIE4.js";
|
|
13
|
+
import {
|
|
14
|
+
COMPONENTS,
|
|
15
|
+
COMPONENT_MARKER,
|
|
16
|
+
EXCLUDE,
|
|
17
|
+
PACKAGE_MANAGERS,
|
|
18
|
+
cleanupRepo,
|
|
19
|
+
detectPackageManager,
|
|
20
|
+
detectPackageManagerFromComponents,
|
|
21
|
+
detectProjectName,
|
|
22
|
+
discoverComponentPaths,
|
|
23
|
+
discoverComponentsFromMarkers,
|
|
24
|
+
downloadRepo,
|
|
25
|
+
exec,
|
|
26
|
+
hasCommand,
|
|
27
|
+
pmCommands,
|
|
28
|
+
readComponentMarker,
|
|
29
|
+
readFileOrNull,
|
|
30
|
+
readProjxConfig,
|
|
31
|
+
toKebab,
|
|
32
|
+
toSnake,
|
|
33
|
+
toTitle,
|
|
34
|
+
writeComponentMarker,
|
|
35
|
+
writeProjxConfig
|
|
36
|
+
} from "./chunk-FTHX7ILT.js";
|
|
2
37
|
|
|
3
38
|
// src/index.ts
|
|
4
|
-
import { existsSync as
|
|
5
|
-
import { resolve
|
|
6
|
-
|
|
7
|
-
// src/utils.ts
|
|
8
|
-
import { execSync } from "child_process";
|
|
9
|
-
import { existsSync, readFileSync } from "fs";
|
|
10
|
-
import { cp, mkdir, readdir, readFile, rm, writeFile } from "fs/promises";
|
|
11
|
-
import { join, resolve } from "path";
|
|
12
|
-
import { tmpdir } from "os";
|
|
13
|
-
import { fileURLToPath } from "url";
|
|
14
|
-
var REPO = "ukanhaupa/projx";
|
|
15
|
-
var REPO_URL = `https://github.com/${REPO}`;
|
|
16
|
-
var COMPONENTS = [
|
|
17
|
-
"fastapi",
|
|
18
|
-
"fastify",
|
|
19
|
-
"frontend",
|
|
20
|
-
"mobile",
|
|
21
|
-
"e2e",
|
|
22
|
-
"infra"
|
|
23
|
-
];
|
|
24
|
-
var PACKAGE_MANAGERS = ["npm", "pnpm", "yarn", "bun"];
|
|
25
|
-
function pmCommands(pm) {
|
|
26
|
-
switch (pm) {
|
|
27
|
-
case "npm":
|
|
28
|
-
return { name: "npm", install: "npm install", ci: "npm ci", run: "npm run", exec: "npx", dlx: "npx", lockfile: "package-lock.json", prismaExec: "npx prisma", runDev: "npm run dev" };
|
|
29
|
-
case "pnpm":
|
|
30
|
-
return { name: "pnpm", install: "pnpm install", ci: "pnpm install --frozen-lockfile", run: "pnpm", exec: "pnpm exec", dlx: "pnpm dlx", lockfile: "pnpm-lock.yaml", prismaExec: "pnpm prisma", runDev: "pnpm dev" };
|
|
31
|
-
case "yarn":
|
|
32
|
-
return { name: "yarn", install: "yarn", ci: "yarn --frozen-lockfile", run: "yarn", exec: "yarn", dlx: "yarn dlx", lockfile: "yarn.lock", prismaExec: "yarn prisma", runDev: "yarn dev" };
|
|
33
|
-
case "bun":
|
|
34
|
-
return { name: "bun", install: "bun install", ci: "bun install --frozen-lockfile", run: "bun run", exec: "bunx", dlx: "bunx", lockfile: "bun.lockb", prismaExec: "bunx prisma", runDev: "bun run dev" };
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
function detectPackageManager(cwd) {
|
|
38
|
-
if (existsSync(join(cwd, "bun.lockb"))) return "bun";
|
|
39
|
-
if (existsSync(join(cwd, "pnpm-lock.yaml"))) return "pnpm";
|
|
40
|
-
if (existsSync(join(cwd, "yarn.lock"))) return "yarn";
|
|
41
|
-
if (existsSync(join(cwd, "package-lock.json"))) return "npm";
|
|
42
|
-
return null;
|
|
43
|
-
}
|
|
44
|
-
function toKebab(s) {
|
|
45
|
-
return s.replace(/([a-z])([A-Z])/g, "$1-$2").replace(/[\s_]+/g, "-").toLowerCase();
|
|
46
|
-
}
|
|
47
|
-
function toSnake(s) {
|
|
48
|
-
return toKebab(s).replace(/-/g, "_");
|
|
49
|
-
}
|
|
50
|
-
function toTitle(s) {
|
|
51
|
-
return s.split(/[-_\s]+/).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
|
|
52
|
-
}
|
|
53
|
-
function hasCommand(cmd) {
|
|
54
|
-
try {
|
|
55
|
-
execSync(`command -v ${cmd}`, { stdio: "ignore" });
|
|
56
|
-
return true;
|
|
57
|
-
} catch {
|
|
58
|
-
return false;
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
function exec(cmd, cwd) {
|
|
62
|
-
execSync(cmd, { cwd, stdio: "pipe" });
|
|
63
|
-
}
|
|
64
|
-
function sharedTemplateDir() {
|
|
65
|
-
const thisFile = fileURLToPath(import.meta.url);
|
|
66
|
-
return join(thisFile, "../../src/templates");
|
|
67
|
-
}
|
|
68
|
-
async function downloadRepo(localPath) {
|
|
69
|
-
if (localPath) {
|
|
70
|
-
return localPath;
|
|
71
|
-
}
|
|
72
|
-
const dest = join(tmpdir(), `projx-${Date.now()}`);
|
|
73
|
-
await mkdir(dest, { recursive: true });
|
|
74
|
-
if (hasCommand("git")) {
|
|
75
|
-
execSync(
|
|
76
|
-
`git clone --depth 1 ${REPO_URL}.git "${dest}/repo"`,
|
|
77
|
-
{ stdio: "pipe" }
|
|
78
|
-
);
|
|
79
|
-
return join(dest, "repo");
|
|
80
|
-
}
|
|
81
|
-
const tarUrl = `${REPO_URL}/archive/refs/heads/main.tar.gz`;
|
|
82
|
-
execSync(
|
|
83
|
-
`curl -sL "${tarUrl}" | tar xz -C "${dest}"`,
|
|
84
|
-
{ stdio: "pipe" }
|
|
85
|
-
);
|
|
86
|
-
const entries = await readdir(dest);
|
|
87
|
-
const extracted = entries.find((e) => e.startsWith("projx-"));
|
|
88
|
-
if (!extracted) throw new Error("Failed to extract repo archive.");
|
|
89
|
-
return join(dest, extracted);
|
|
90
|
-
}
|
|
91
|
-
async function cleanupRepo(repoDir, isLocal) {
|
|
92
|
-
if (isLocal) return;
|
|
93
|
-
const parent = resolve(repoDir, "..");
|
|
94
|
-
if (parent.startsWith(tmpdir())) {
|
|
95
|
-
await rm(parent, { recursive: true, force: true });
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
var EXCLUDE = /* @__PURE__ */ new Set([
|
|
99
|
-
"node_modules",
|
|
100
|
-
"dist",
|
|
101
|
-
"build",
|
|
102
|
-
"coverage",
|
|
103
|
-
"__pycache__",
|
|
104
|
-
".dart_tool",
|
|
105
|
-
".flutter-plugins",
|
|
106
|
-
".flutter-plugins-dependencies",
|
|
107
|
-
".venv",
|
|
108
|
-
".pytest_cache",
|
|
109
|
-
".ruff_cache",
|
|
110
|
-
".mypy_cache",
|
|
111
|
-
"playwright-report",
|
|
112
|
-
"test-results",
|
|
113
|
-
".terraform",
|
|
114
|
-
"cli"
|
|
115
|
-
]);
|
|
116
|
-
var EXCLUDE_FILES = /* @__PURE__ */ new Set([
|
|
117
|
-
"uv.lock",
|
|
118
|
-
"pnpm-lock.yaml",
|
|
119
|
-
"package-lock.json",
|
|
120
|
-
"pubspec.lock",
|
|
121
|
-
".env",
|
|
122
|
-
".env.dev",
|
|
123
|
-
".env.staging",
|
|
124
|
-
".env.prod",
|
|
125
|
-
"dev.tfplan",
|
|
126
|
-
".coverage"
|
|
127
|
-
]);
|
|
128
|
-
async function copyComponent(repoDir, component, dest) {
|
|
129
|
-
const src = join(repoDir, component);
|
|
130
|
-
const out = join(dest, component);
|
|
131
|
-
const files = [];
|
|
132
|
-
await cp(src, out, {
|
|
133
|
-
recursive: true,
|
|
134
|
-
filter: (source) => {
|
|
135
|
-
const base = source.split("/").pop();
|
|
136
|
-
if (EXCLUDE.has(base)) return false;
|
|
137
|
-
if (EXCLUDE_FILES.has(base)) return false;
|
|
138
|
-
if (base.endsWith(".pyc")) return false;
|
|
139
|
-
return true;
|
|
140
|
-
}
|
|
141
|
-
});
|
|
142
|
-
await collectFiles(out, out, files);
|
|
143
|
-
return files;
|
|
144
|
-
}
|
|
145
|
-
async function copyStaticFiles(repoDir, dest) {
|
|
146
|
-
const manifest = [];
|
|
147
|
-
const tpl = repoDir;
|
|
148
|
-
const statics = [".editorconfig"];
|
|
149
|
-
for (const file of statics) {
|
|
150
|
-
const src = join(tpl, file);
|
|
151
|
-
if (existsSync(src)) {
|
|
152
|
-
await cp(src, join(dest, file));
|
|
153
|
-
manifest.push(file);
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
const extensionsJson = join(tpl, ".vscode/extensions.json");
|
|
157
|
-
if (existsSync(extensionsJson)) {
|
|
158
|
-
await mkdir(join(dest, ".vscode"), { recursive: true });
|
|
159
|
-
await cp(extensionsJson, join(dest, ".vscode/extensions.json"));
|
|
160
|
-
manifest.push(".vscode/extensions.json");
|
|
161
|
-
}
|
|
162
|
-
const scripts = join(tpl, "scripts");
|
|
163
|
-
if (existsSync(scripts)) {
|
|
164
|
-
await cp(scripts, join(dest, "scripts"), { recursive: true });
|
|
165
|
-
manifest.push("scripts/setup-ssl.sh");
|
|
166
|
-
}
|
|
167
|
-
return manifest;
|
|
168
|
-
}
|
|
169
|
-
async function collectFiles(dir, root, files) {
|
|
170
|
-
const entries = await readdir(dir, { withFileTypes: true });
|
|
171
|
-
for (const entry of entries) {
|
|
172
|
-
const full = join(dir, entry.name);
|
|
173
|
-
if (entry.isDirectory()) {
|
|
174
|
-
await collectFiles(full, root, files);
|
|
175
|
-
} else {
|
|
176
|
-
files.push(full.slice(root.length + 1));
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
async function replaceInFile(filePath, find, replace) {
|
|
181
|
-
if (!existsSync(filePath)) return;
|
|
182
|
-
const content = await readFile(filePath, "utf-8");
|
|
183
|
-
if (!content.includes(find)) return;
|
|
184
|
-
await writeFile(filePath, content.replaceAll(find, replace));
|
|
185
|
-
}
|
|
186
|
-
async function replaceInDir(dir, find, replace, ext) {
|
|
187
|
-
if (!existsSync(dir)) return;
|
|
188
|
-
const entries = await readdir(dir, { withFileTypes: true });
|
|
189
|
-
for (const entry of entries) {
|
|
190
|
-
const full = join(dir, entry.name);
|
|
191
|
-
if (entry.isDirectory()) {
|
|
192
|
-
await replaceInDir(full, find, replace, ext);
|
|
193
|
-
} else if (entry.name.endsWith(ext)) {
|
|
194
|
-
await replaceInFile(full, find, replace);
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
var COMPONENT_MARKER = ".projx-component";
|
|
199
|
-
async function readFileOrNull(path) {
|
|
200
|
-
try {
|
|
201
|
-
return await readFile(path, "utf-8");
|
|
202
|
-
} catch {
|
|
203
|
-
return null;
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
async function readComponentMarker(dir) {
|
|
207
|
-
const raw = await readFileOrNull(join(dir, COMPONENT_MARKER));
|
|
208
|
-
if (!raw) return null;
|
|
209
|
-
try {
|
|
210
|
-
const data = JSON.parse(raw);
|
|
211
|
-
return {
|
|
212
|
-
components: data.components ?? (data.component ? [data.component] : []),
|
|
213
|
-
origin: data.origin,
|
|
214
|
-
skip: data.skip
|
|
215
|
-
};
|
|
216
|
-
} catch {
|
|
217
|
-
return null;
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
async function writeComponentMarker(dir, component, origin = "scaffold", skip) {
|
|
221
|
-
const markerPath = join(dir, COMPONENT_MARKER);
|
|
222
|
-
let components = [component];
|
|
223
|
-
let existingOrigin = origin;
|
|
224
|
-
let existingSkip = skip;
|
|
225
|
-
const existing = await readFileOrNull(markerPath);
|
|
226
|
-
if (existing) {
|
|
227
|
-
try {
|
|
228
|
-
const data = JSON.parse(existing);
|
|
229
|
-
const prev = data.components ?? (data.component ? [data.component] : []);
|
|
230
|
-
existingOrigin = origin ?? data.origin ?? "scaffold";
|
|
231
|
-
existingSkip = skip ?? data.skip;
|
|
232
|
-
if (!prev.includes(component)) {
|
|
233
|
-
components = [...prev, component];
|
|
234
|
-
} else {
|
|
235
|
-
components = prev;
|
|
236
|
-
}
|
|
237
|
-
} catch {
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
const marker = { components, origin: existingOrigin };
|
|
241
|
-
if (existingSkip && existingSkip.length > 0) marker.skip = existingSkip;
|
|
242
|
-
await writeFile(markerPath, JSON.stringify(marker, null, 2) + "\n");
|
|
243
|
-
}
|
|
244
|
-
async function discoverComponentPaths(cwd, components) {
|
|
245
|
-
const paths = {};
|
|
246
|
-
const scan = async (dir) => {
|
|
247
|
-
const entries = await readdir(dir, { withFileTypes: true });
|
|
248
|
-
for (const entry of entries) {
|
|
249
|
-
if (!entry.isDirectory()) continue;
|
|
250
|
-
if (EXCLUDE.has(entry.name)) continue;
|
|
251
|
-
if (entry.name.startsWith(".")) continue;
|
|
252
|
-
const full = join(dir, entry.name);
|
|
253
|
-
const marker = join(full, COMPONENT_MARKER);
|
|
254
|
-
if (existsSync(marker)) {
|
|
255
|
-
try {
|
|
256
|
-
const data = JSON.parse(await readFile(marker, "utf-8"));
|
|
257
|
-
const markerComponents = data.components ?? (data.component ? [data.component] : []);
|
|
258
|
-
for (const mc of markerComponents) {
|
|
259
|
-
if (components.includes(mc)) {
|
|
260
|
-
paths[mc] = entry.name;
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
} catch {
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
};
|
|
268
|
-
await scan(cwd);
|
|
269
|
-
for (const c of components) {
|
|
270
|
-
if (!paths[c]) paths[c] = c;
|
|
271
|
-
}
|
|
272
|
-
return paths;
|
|
273
|
-
}
|
|
274
|
-
async function discoverComponentsFromMarkers(cwd) {
|
|
275
|
-
const components = [];
|
|
276
|
-
const paths = {};
|
|
277
|
-
const entries = await readdir(cwd, { withFileTypes: true });
|
|
278
|
-
for (const entry of entries) {
|
|
279
|
-
if (!entry.isDirectory()) continue;
|
|
280
|
-
if (EXCLUDE.has(entry.name)) continue;
|
|
281
|
-
if (entry.name.startsWith(".")) continue;
|
|
282
|
-
const full = join(cwd, entry.name);
|
|
283
|
-
const marker = join(full, COMPONENT_MARKER);
|
|
284
|
-
if (existsSync(marker)) {
|
|
285
|
-
try {
|
|
286
|
-
const data = JSON.parse(await readFile(marker, "utf-8"));
|
|
287
|
-
const markerComponents = data.components ?? (data.component ? [data.component] : []);
|
|
288
|
-
for (const mc of markerComponents) {
|
|
289
|
-
if (COMPONENTS.includes(mc) && !components.includes(mc)) {
|
|
290
|
-
components.push(mc);
|
|
291
|
-
paths[mc] = entry.name;
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
} catch {
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
for (const c of components) {
|
|
299
|
-
if (!paths[c]) paths[c] = c;
|
|
300
|
-
}
|
|
301
|
-
return { components, paths };
|
|
302
|
-
}
|
|
303
|
-
function render(template, vars) {
|
|
304
|
-
const components = vars.components;
|
|
305
|
-
const projectName = vars.projectName;
|
|
306
|
-
const lines = template.split("\n");
|
|
307
|
-
const output = [];
|
|
308
|
-
const stack = [];
|
|
309
|
-
for (const line of lines) {
|
|
310
|
-
const ifMatch = line.match(/^<%\s*if\s*\((.+?)\)\s*\{?\s*%>$/);
|
|
311
|
-
if (ifMatch) {
|
|
312
|
-
const pmName = vars.pm?.name ?? "npm";
|
|
313
|
-
const fn = new Function("components", "projectName", "pm", `return ${ifMatch[1]}`);
|
|
314
|
-
const result = fn(components, projectName, pmName);
|
|
315
|
-
stack.push({ active: result, matched: result });
|
|
316
|
-
continue;
|
|
317
|
-
}
|
|
318
|
-
const elseIfMatch = line.match(/^<%\s*\}\s*else\s+if\s*\((.+?)\)\s*\{?\s*%>$/);
|
|
319
|
-
if (elseIfMatch) {
|
|
320
|
-
if (stack.length > 0) {
|
|
321
|
-
const top = stack[stack.length - 1];
|
|
322
|
-
if (top.matched) {
|
|
323
|
-
top.active = false;
|
|
324
|
-
} else {
|
|
325
|
-
const pmN = vars.pm?.name ?? "npm";
|
|
326
|
-
const fn = new Function("components", "projectName", "pm", `return ${elseIfMatch[1]}`);
|
|
327
|
-
const result = fn(components, projectName, pmN);
|
|
328
|
-
top.active = result;
|
|
329
|
-
if (result) top.matched = true;
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
continue;
|
|
333
|
-
}
|
|
334
|
-
if (/^<%\s*\}\s*else\s*\{?\s*%>$/.test(line)) {
|
|
335
|
-
if (stack.length > 0) {
|
|
336
|
-
const top = stack[stack.length - 1];
|
|
337
|
-
top.active = !top.matched;
|
|
338
|
-
}
|
|
339
|
-
continue;
|
|
340
|
-
}
|
|
341
|
-
if (/^<%\s*\}?\s*%>$/.test(line)) {
|
|
342
|
-
stack.pop();
|
|
343
|
-
continue;
|
|
344
|
-
}
|
|
345
|
-
if (stack.length > 0 && stack.some((v) => !v.active)) continue;
|
|
346
|
-
const replaced = line.replace(
|
|
347
|
-
/<%=\s*([\w.]+)\s*%>/g,
|
|
348
|
-
(_, expr) => {
|
|
349
|
-
const parts = expr.split(".");
|
|
350
|
-
let val = vars;
|
|
351
|
-
for (const p11 of parts) {
|
|
352
|
-
val = val?.[p11];
|
|
353
|
-
}
|
|
354
|
-
return String(val ?? "");
|
|
355
|
-
}
|
|
356
|
-
);
|
|
357
|
-
output.push(replaced);
|
|
358
|
-
}
|
|
359
|
-
return output.join("\n").replace(/\n{3,}/g, "\n\n");
|
|
360
|
-
}
|
|
361
|
-
function detectProjectName(cwd, components, componentPaths) {
|
|
362
|
-
for (const component of components) {
|
|
363
|
-
const dir = componentPaths[component] ?? component;
|
|
364
|
-
const pkgPath = join(cwd, dir, "package.json");
|
|
365
|
-
if (existsSync(pkgPath)) {
|
|
366
|
-
try {
|
|
367
|
-
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
368
|
-
const n = pkg.name;
|
|
369
|
-
if (n && n.includes("-")) {
|
|
370
|
-
return n.substring(0, n.lastIndexOf("-"));
|
|
371
|
-
}
|
|
372
|
-
} catch {
|
|
373
|
-
}
|
|
374
|
-
}
|
|
375
|
-
}
|
|
376
|
-
return toKebab(cwd.split("/").pop());
|
|
377
|
-
}
|
|
39
|
+
import { existsSync as existsSync11 } from "fs";
|
|
40
|
+
import { resolve } from "path";
|
|
378
41
|
|
|
379
42
|
// src/prompts.ts
|
|
380
43
|
import * as p from "@clack/prompts";
|
|
@@ -428,430 +91,10 @@ async function runPrompts(nameArg) {
|
|
|
428
91
|
}
|
|
429
92
|
|
|
430
93
|
// src/scaffold.ts
|
|
431
|
-
import { copyFileSync, existsSync
|
|
432
|
-
import { mkdir
|
|
433
|
-
import { join
|
|
94
|
+
import { copyFileSync, existsSync } from "fs";
|
|
95
|
+
import { mkdir, readFile } from "fs/promises";
|
|
96
|
+
import { join } from "path";
|
|
434
97
|
import * as p2 from "@clack/prompts";
|
|
435
|
-
|
|
436
|
-
// src/baseline.ts
|
|
437
|
-
import { existsSync as existsSync2, writeFileSync, unlinkSync } from "fs";
|
|
438
|
-
import { chmod, mkdir as mkdir2, writeFile as writeFile2, rm as rm2, readFile as readFile3 } from "fs/promises";
|
|
439
|
-
import { execSync as execSync2 } from "child_process";
|
|
440
|
-
import { join as join3 } from "path";
|
|
441
|
-
import { tmpdir as tmpdir2 } from "os";
|
|
442
|
-
|
|
443
|
-
// src/generators/index.ts
|
|
444
|
-
import { readFile as readFile2 } from "fs/promises";
|
|
445
|
-
import { join as join2 } from "path";
|
|
446
|
-
async function renderShared(filename, vars) {
|
|
447
|
-
const tpl = await readFile2(
|
|
448
|
-
join2(sharedTemplateDir(), filename),
|
|
449
|
-
"utf-8"
|
|
450
|
-
);
|
|
451
|
-
return render(tpl, vars);
|
|
452
|
-
}
|
|
453
|
-
async function generateDockerCompose(vars) {
|
|
454
|
-
return renderShared("docker-compose.yml.ejs", vars);
|
|
455
|
-
}
|
|
456
|
-
async function generateDockerComposeDev(vars) {
|
|
457
|
-
return renderShared("docker-compose.dev.yml.ejs", vars);
|
|
458
|
-
}
|
|
459
|
-
async function generatePreCommit(vars) {
|
|
460
|
-
return renderShared("pre-commit.ejs", vars);
|
|
461
|
-
}
|
|
462
|
-
async function generateSetupSh(vars) {
|
|
463
|
-
return renderShared("setup.sh.ejs", vars);
|
|
464
|
-
}
|
|
465
|
-
async function generateCiYml(vars) {
|
|
466
|
-
return renderShared("ci.yml.ejs", vars);
|
|
467
|
-
}
|
|
468
|
-
async function generateReadme(vars) {
|
|
469
|
-
return renderShared("README.md.ejs", vars);
|
|
470
|
-
}
|
|
471
|
-
function generateVscodeSettings(vars) {
|
|
472
|
-
const settings = {};
|
|
473
|
-
if (vars.components.includes("fastapi")) {
|
|
474
|
-
settings["[python]"] = {
|
|
475
|
-
"editor.defaultFormatter": "charliermarsh.ruff",
|
|
476
|
-
"editor.codeActionsOnSave": { "source.fixAll.ruff": "explicit" }
|
|
477
|
-
};
|
|
478
|
-
}
|
|
479
|
-
settings["[typescript]"] = { "editor.defaultFormatter": "esbenp.prettier-vscode" };
|
|
480
|
-
settings["[typescriptreact]"] = { "editor.defaultFormatter": "esbenp.prettier-vscode" };
|
|
481
|
-
settings["[javascript]"] = { "editor.defaultFormatter": "esbenp.prettier-vscode" };
|
|
482
|
-
settings["[json]"] = { "editor.defaultFormatter": "esbenp.prettier-vscode" };
|
|
483
|
-
settings["[css]"] = { "editor.defaultFormatter": "esbenp.prettier-vscode" };
|
|
484
|
-
settings["[yaml]"] = { "editor.defaultFormatter": "esbenp.prettier-vscode" };
|
|
485
|
-
settings["editor.formatOnSave"] = true;
|
|
486
|
-
settings["editor.codeActionsOnSave"] = { "source.fixAll.eslint": "explicit" };
|
|
487
|
-
settings["eslint.useFlatConfig"] = true;
|
|
488
|
-
const prettierComponent = ["frontend", "fastify", "e2e"].find(
|
|
489
|
-
(c) => vars.components.includes(c)
|
|
490
|
-
);
|
|
491
|
-
if (prettierComponent) {
|
|
492
|
-
settings["prettier.configPath"] = `${vars.paths[prettierComponent]}/.prettierrc`;
|
|
493
|
-
}
|
|
494
|
-
if (vars.components.includes("fastapi")) {
|
|
495
|
-
settings["ruff.lineLength"] = 120;
|
|
496
|
-
settings["python.analysis.extraPaths"] = [`${vars.paths.fastapi}/src`];
|
|
497
|
-
settings["python.analysis.importFormat"] = "absolute";
|
|
498
|
-
}
|
|
499
|
-
return JSON.stringify(settings, null, 2) + "\n";
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
// src/baseline.ts
|
|
503
|
-
var BASELINE_REF = "refs/projx/baseline";
|
|
504
|
-
function matchesSkip(filePath, patterns) {
|
|
505
|
-
for (const pattern of patterns) {
|
|
506
|
-
if (pattern === "**") return true;
|
|
507
|
-
if (pattern.endsWith("/**")) {
|
|
508
|
-
const prefix = pattern.slice(0, -3);
|
|
509
|
-
if (filePath.startsWith(prefix + "/") || filePath === prefix) return true;
|
|
510
|
-
}
|
|
511
|
-
if (pattern.startsWith("**/")) {
|
|
512
|
-
const suffix = pattern.slice(3);
|
|
513
|
-
if (suffix.startsWith("*.")) {
|
|
514
|
-
const ext = suffix.slice(1);
|
|
515
|
-
if (filePath.endsWith(ext)) return true;
|
|
516
|
-
} else if (filePath.endsWith(suffix) || filePath.includes("/" + suffix)) {
|
|
517
|
-
return true;
|
|
518
|
-
}
|
|
519
|
-
}
|
|
520
|
-
if (pattern.startsWith("*.")) {
|
|
521
|
-
const ext = pattern.slice(1);
|
|
522
|
-
if (filePath.endsWith(ext)) return true;
|
|
523
|
-
}
|
|
524
|
-
if (filePath === pattern) return true;
|
|
525
|
-
}
|
|
526
|
-
return false;
|
|
527
|
-
}
|
|
528
|
-
function saveBaselineRef(cwd) {
|
|
529
|
-
try {
|
|
530
|
-
const head = execSync2("git rev-parse HEAD", { cwd, stdio: "pipe" }).toString().trim();
|
|
531
|
-
execSync2(`git update-ref ${BASELINE_REF} ${head}`, { cwd, stdio: "pipe" });
|
|
532
|
-
} catch {
|
|
533
|
-
}
|
|
534
|
-
}
|
|
535
|
-
function getBaselineRef(cwd) {
|
|
536
|
-
try {
|
|
537
|
-
return execSync2(`git rev-parse --verify ${BASELINE_REF}`, { cwd, stdio: "pipe" }).toString().trim();
|
|
538
|
-
} catch {
|
|
539
|
-
}
|
|
540
|
-
try {
|
|
541
|
-
const sha = execSync2("git log -1 --format=%H -- .projx", { cwd, stdio: "pipe" }).toString().trim();
|
|
542
|
-
if (sha) return sha;
|
|
543
|
-
} catch {
|
|
544
|
-
}
|
|
545
|
-
return null;
|
|
546
|
-
}
|
|
547
|
-
function getFileAtRef(cwd, ref, filePath) {
|
|
548
|
-
try {
|
|
549
|
-
return execSync2(`git show ${ref}:"${filePath}"`, { cwd, stdio: "pipe" }).toString();
|
|
550
|
-
} catch {
|
|
551
|
-
return null;
|
|
552
|
-
}
|
|
553
|
-
}
|
|
554
|
-
function mergeFileThreeWay(oursPath, baseContent, theirsContent) {
|
|
555
|
-
const id = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
556
|
-
const baseTmp = join3(tmpdir2(), `projx-base-${id}`);
|
|
557
|
-
const theirsTmp = join3(tmpdir2(), `projx-theirs-${id}`);
|
|
558
|
-
try {
|
|
559
|
-
writeFileSync(baseTmp, baseContent);
|
|
560
|
-
writeFileSync(theirsTmp, theirsContent);
|
|
561
|
-
execSync2(`git merge-file "${oursPath}" "${baseTmp}" "${theirsTmp}"`, { stdio: "pipe" });
|
|
562
|
-
return true;
|
|
563
|
-
} catch {
|
|
564
|
-
return false;
|
|
565
|
-
} finally {
|
|
566
|
-
try {
|
|
567
|
-
unlinkSync(baseTmp);
|
|
568
|
-
} catch {
|
|
569
|
-
}
|
|
570
|
-
try {
|
|
571
|
-
unlinkSync(theirsTmp);
|
|
572
|
-
} catch {
|
|
573
|
-
}
|
|
574
|
-
}
|
|
575
|
-
}
|
|
576
|
-
async function collectAllFiles(dir, base) {
|
|
577
|
-
const { readdir: readdir4 } = await import("fs/promises");
|
|
578
|
-
const results = [];
|
|
579
|
-
const walk = async (current) => {
|
|
580
|
-
const entries = await readdir4(current, { withFileTypes: true });
|
|
581
|
-
for (const entry of entries) {
|
|
582
|
-
const full = join3(current, entry.name);
|
|
583
|
-
if (entry.isDirectory()) {
|
|
584
|
-
await walk(full);
|
|
585
|
-
} else {
|
|
586
|
-
results.push(full.slice(base.length + 1));
|
|
587
|
-
}
|
|
588
|
-
}
|
|
589
|
-
};
|
|
590
|
-
await walk(dir);
|
|
591
|
-
return results;
|
|
592
|
-
}
|
|
593
|
-
async function tryThreeWayMerge(cwd, templateDir, baselineRef) {
|
|
594
|
-
const templateFiles = await collectAllFiles(templateDir, templateDir);
|
|
595
|
-
const merged = [];
|
|
596
|
-
const conflicted = [];
|
|
597
|
-
for (const file of templateFiles) {
|
|
598
|
-
const oursPath = join3(cwd, file);
|
|
599
|
-
if (!existsSync2(oursPath)) continue;
|
|
600
|
-
const baseContent = getFileAtRef(cwd, baselineRef, file);
|
|
601
|
-
if (baseContent === null) continue;
|
|
602
|
-
let theirsContent;
|
|
603
|
-
try {
|
|
604
|
-
theirsContent = await readFile3(join3(templateDir, file), "utf-8");
|
|
605
|
-
} catch {
|
|
606
|
-
continue;
|
|
607
|
-
}
|
|
608
|
-
const oursContent = await readFile3(oursPath, "utf-8");
|
|
609
|
-
if (oursContent === baseContent) continue;
|
|
610
|
-
if (theirsContent === baseContent) continue;
|
|
611
|
-
const clean = mergeFileThreeWay(oursPath, baseContent, theirsContent);
|
|
612
|
-
if (clean) {
|
|
613
|
-
merged.push(file);
|
|
614
|
-
} else {
|
|
615
|
-
conflicted.push(file);
|
|
616
|
-
}
|
|
617
|
-
}
|
|
618
|
-
return { merged, conflicted };
|
|
619
|
-
}
|
|
620
|
-
function createOrphanWorktree(cwd) {
|
|
621
|
-
const id = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
622
|
-
const branch = `projx/tmp-${id}`;
|
|
623
|
-
const worktree = join3(tmpdir2(), `projx-wt-${id}`);
|
|
624
|
-
try {
|
|
625
|
-
execSync2("git worktree prune", { cwd, stdio: "pipe" });
|
|
626
|
-
} catch {
|
|
627
|
-
}
|
|
628
|
-
execSync2(`git worktree add --orphan -b ${branch} "${worktree}"`, {
|
|
629
|
-
cwd,
|
|
630
|
-
stdio: "pipe"
|
|
631
|
-
});
|
|
632
|
-
return { worktree, branch };
|
|
633
|
-
}
|
|
634
|
-
function cleanupWorktree(cwd, worktree, branch) {
|
|
635
|
-
try {
|
|
636
|
-
execSync2(`git worktree remove "${worktree}" --force`, { cwd, stdio: "pipe" });
|
|
637
|
-
} catch {
|
|
638
|
-
try {
|
|
639
|
-
rm2(worktree, { recursive: true, force: true });
|
|
640
|
-
execSync2("git worktree prune", { cwd, stdio: "pipe" });
|
|
641
|
-
} catch {
|
|
642
|
-
}
|
|
643
|
-
}
|
|
644
|
-
try {
|
|
645
|
-
execSync2(`git branch -D ${branch}`, { cwd, stdio: "pipe" });
|
|
646
|
-
} catch {
|
|
647
|
-
}
|
|
648
|
-
}
|
|
649
|
-
async function removeSkippedFiles(dir, skipPatterns) {
|
|
650
|
-
if (skipPatterns.length === 0) return;
|
|
651
|
-
const { readdir: readdir4, unlink: unlink2 } = await import("fs/promises");
|
|
652
|
-
const walk = async (current, base) => {
|
|
653
|
-
const entries = await readdir4(current, { withFileTypes: true });
|
|
654
|
-
for (const entry of entries) {
|
|
655
|
-
const full = join3(current, entry.name);
|
|
656
|
-
const rel = full.slice(base.length + 1);
|
|
657
|
-
if (entry.isDirectory()) {
|
|
658
|
-
await walk(full, base);
|
|
659
|
-
} else if (entry.name !== ".projx-component" && matchesSkip(rel, skipPatterns)) {
|
|
660
|
-
await unlink2(full);
|
|
661
|
-
}
|
|
662
|
-
}
|
|
663
|
-
};
|
|
664
|
-
await walk(dir, dir);
|
|
665
|
-
}
|
|
666
|
-
async function writeTemplateToDir(dest, repoDir, components, componentPaths, vars, version, origin, componentSkips, rootSkip) {
|
|
667
|
-
const name = vars.projectName;
|
|
668
|
-
const nameSnake = toSnake(name);
|
|
669
|
-
for (const component of components) {
|
|
670
|
-
const targetDir = componentPaths[component];
|
|
671
|
-
const skipPatterns = componentSkips?.[component] ?? [];
|
|
672
|
-
const tmpDir = join3(dest, "__cptmp__");
|
|
673
|
-
await copyComponent(repoDir, component, tmpDir);
|
|
674
|
-
const srcDir = join3(tmpDir, component);
|
|
675
|
-
if (skipPatterns.length > 0) {
|
|
676
|
-
await removeSkippedFiles(srcDir, skipPatterns);
|
|
677
|
-
}
|
|
678
|
-
const outDir = join3(dest, targetDir);
|
|
679
|
-
await mkdir2(outDir, { recursive: true });
|
|
680
|
-
const { cp: cp2 } = await import("fs/promises");
|
|
681
|
-
if (existsSync2(srcDir)) {
|
|
682
|
-
await cp2(srcDir, outDir, { recursive: true, force: true });
|
|
683
|
-
}
|
|
684
|
-
await rm2(tmpDir, { recursive: true, force: true });
|
|
685
|
-
await writeComponentMarker(join3(dest, targetDir), component, origin, skipPatterns.length > 0 ? skipPatterns : void 0);
|
|
686
|
-
}
|
|
687
|
-
await substituteNames(dest, components, componentPaths, name, nameSnake);
|
|
688
|
-
const hasBackend = components.includes("fastapi") || components.includes("fastify");
|
|
689
|
-
const skip = rootSkip ?? [];
|
|
690
|
-
const shouldWrite = (file) => !matchesSkip(file, skip);
|
|
691
|
-
if (hasBackend || components.includes("frontend")) {
|
|
692
|
-
if (shouldWrite("docker-compose.yml"))
|
|
693
|
-
await writeFile2(join3(dest, "docker-compose.yml"), await generateDockerCompose(vars));
|
|
694
|
-
if (shouldWrite("docker-compose.dev.yml"))
|
|
695
|
-
await writeFile2(join3(dest, "docker-compose.dev.yml"), await generateDockerComposeDev(vars));
|
|
696
|
-
}
|
|
697
|
-
if (shouldWrite("README.md"))
|
|
698
|
-
await writeFile2(join3(dest, "README.md"), await generateReadme(vars));
|
|
699
|
-
if (shouldWrite(".githooks/pre-commit")) {
|
|
700
|
-
await mkdir2(join3(dest, ".githooks"), { recursive: true });
|
|
701
|
-
await writeFile2(join3(dest, ".githooks/pre-commit"), await generatePreCommit(vars));
|
|
702
|
-
await chmod(join3(dest, ".githooks/pre-commit"), 493);
|
|
703
|
-
}
|
|
704
|
-
if (shouldWrite(".github/workflows/ci.yml")) {
|
|
705
|
-
await mkdir2(join3(dest, ".github/workflows"), { recursive: true });
|
|
706
|
-
await writeFile2(join3(dest, ".github/workflows/ci.yml"), await generateCiYml(vars));
|
|
707
|
-
}
|
|
708
|
-
if (shouldWrite("setup.sh")) {
|
|
709
|
-
await writeFile2(join3(dest, "setup.sh"), await generateSetupSh(vars));
|
|
710
|
-
await chmod(join3(dest, "setup.sh"), 493);
|
|
711
|
-
}
|
|
712
|
-
await copyStaticFiles(repoDir, dest);
|
|
713
|
-
if (shouldWrite(".vscode/settings.json")) {
|
|
714
|
-
await mkdir2(join3(dest, ".vscode"), { recursive: true });
|
|
715
|
-
await writeFile2(join3(dest, ".vscode/settings.json"), generateVscodeSettings(vars));
|
|
716
|
-
}
|
|
717
|
-
const projxConfig = {
|
|
718
|
-
version,
|
|
719
|
-
components,
|
|
720
|
-
createdAt: (/* @__PURE__ */ new Date()).toISOString().split("T")[0]
|
|
721
|
-
};
|
|
722
|
-
const pmObj = vars.pm;
|
|
723
|
-
if (pmObj?.name) projxConfig.packageManager = pmObj.name;
|
|
724
|
-
await writeFile2(join3(dest, ".projx"), JSON.stringify(projxConfig, null, 2) + "\n");
|
|
725
|
-
}
|
|
726
|
-
async function substituteNames(dest, components, paths, name, nameSnake) {
|
|
727
|
-
if (components.includes("fastapi")) {
|
|
728
|
-
await replaceInFile(join3(dest, `${paths.fastapi}/pyproject.toml`), "projx-fastapi", `${name}-fastapi`);
|
|
729
|
-
}
|
|
730
|
-
if (components.includes("fastify")) {
|
|
731
|
-
await replaceInFile(join3(dest, `${paths.fastify}/package.json`), "projx-fastify", `${name}-fastify`);
|
|
732
|
-
}
|
|
733
|
-
if (components.includes("frontend")) {
|
|
734
|
-
await replaceInFile(join3(dest, `${paths.frontend}/package.json`), "projx-frontend", `${name}-frontend`);
|
|
735
|
-
}
|
|
736
|
-
if (components.includes("e2e")) {
|
|
737
|
-
await replaceInFile(join3(dest, `${paths.e2e}/package.json`), "projx-e2e", `${name}-e2e`);
|
|
738
|
-
}
|
|
739
|
-
if (components.includes("mobile")) {
|
|
740
|
-
await replaceInFile(join3(dest, `${paths.mobile}/pubspec.yaml`), "projx_mobile", `${nameSnake}_mobile`);
|
|
741
|
-
await replaceInDir(join3(dest, `${paths.mobile}`), "package:projx_mobile/", `package:${nameSnake}_mobile/`, ".dart");
|
|
742
|
-
}
|
|
743
|
-
}
|
|
744
|
-
async function applyTemplate(cwd, repoDir, components, componentPaths, vars, version, origin = "scaffold", componentSkips, rootSkip) {
|
|
745
|
-
const hasHead = (() => {
|
|
746
|
-
try {
|
|
747
|
-
execSync2("git rev-parse HEAD", { cwd, stdio: "pipe" });
|
|
748
|
-
return true;
|
|
749
|
-
} catch {
|
|
750
|
-
return false;
|
|
751
|
-
}
|
|
752
|
-
})();
|
|
753
|
-
if (!hasHead) {
|
|
754
|
-
await writeTemplateToDir(cwd, repoDir, components, componentPaths, vars, version, origin, componentSkips, rootSkip);
|
|
755
|
-
return { status: "clean" };
|
|
756
|
-
}
|
|
757
|
-
const { worktree, branch } = createOrphanWorktree(cwd);
|
|
758
|
-
try {
|
|
759
|
-
await writeTemplateToDir(worktree, repoDir, components, componentPaths, vars, version, origin, componentSkips, rootSkip);
|
|
760
|
-
execSync2("git add -A", { cwd: worktree, stdio: "pipe" });
|
|
761
|
-
const diff2 = execSync2("git diff --cached --stat", { cwd: worktree, stdio: "pipe" }).toString().trim();
|
|
762
|
-
if (!diff2) {
|
|
763
|
-
cleanupWorktree(cwd, worktree, branch);
|
|
764
|
-
return { status: "clean" };
|
|
765
|
-
}
|
|
766
|
-
execSync2(
|
|
767
|
-
`git commit --no-verify -m "projx: template v${version} [${components.join(", ")}]"`,
|
|
768
|
-
{ cwd: worktree, stdio: "pipe" }
|
|
769
|
-
);
|
|
770
|
-
try {
|
|
771
|
-
execSync2(`git worktree remove "${worktree}" --force`, { cwd, stdio: "pipe" });
|
|
772
|
-
} catch {
|
|
773
|
-
try {
|
|
774
|
-
await rm2(worktree, { recursive: true, force: true });
|
|
775
|
-
execSync2("git worktree prune", { cwd, stdio: "pipe" });
|
|
776
|
-
} catch {
|
|
777
|
-
}
|
|
778
|
-
}
|
|
779
|
-
let mergeClean = false;
|
|
780
|
-
try {
|
|
781
|
-
execSync2(
|
|
782
|
-
`git merge ${branch} --allow-unrelated-histories -m "projx: update to template v${version}"`,
|
|
783
|
-
{ cwd, stdio: "pipe" }
|
|
784
|
-
);
|
|
785
|
-
mergeClean = true;
|
|
786
|
-
} catch {
|
|
787
|
-
try {
|
|
788
|
-
execSync2("git merge --abort", { cwd, stdio: "pipe" });
|
|
789
|
-
} catch {
|
|
790
|
-
}
|
|
791
|
-
}
|
|
792
|
-
try {
|
|
793
|
-
execSync2(`git branch -D ${branch}`, { cwd, stdio: "pipe" });
|
|
794
|
-
} catch {
|
|
795
|
-
}
|
|
796
|
-
if (mergeClean) {
|
|
797
|
-
saveBaselineRef(cwd);
|
|
798
|
-
return { status: "clean" };
|
|
799
|
-
}
|
|
800
|
-
const baselineRef = getBaselineRef(cwd);
|
|
801
|
-
if (baselineRef) {
|
|
802
|
-
const tmpTemplate = join3(tmpdir2(), `projx-tpl-${Date.now()}`);
|
|
803
|
-
await mkdir2(tmpTemplate, { recursive: true });
|
|
804
|
-
await writeTemplateToDir(tmpTemplate, repoDir, components, componentPaths, vars, version, origin, componentSkips, rootSkip);
|
|
805
|
-
const result = await tryThreeWayMerge(cwd, tmpTemplate, baselineRef);
|
|
806
|
-
await rm2(tmpTemplate, { recursive: true, force: true });
|
|
807
|
-
const projxConfig = {
|
|
808
|
-
version,
|
|
809
|
-
components,
|
|
810
|
-
createdAt: (/* @__PURE__ */ new Date()).toISOString().split("T")[0]
|
|
811
|
-
};
|
|
812
|
-
const pmObj = vars.pm;
|
|
813
|
-
if (pmObj?.name) projxConfig.packageManager = pmObj.name;
|
|
814
|
-
await writeFile2(join3(cwd, ".projx"), JSON.stringify(projxConfig, null, 2) + "\n");
|
|
815
|
-
if (result.conflicted.length === 0) {
|
|
816
|
-
execSync2("git add -A", { cwd, stdio: "pipe" });
|
|
817
|
-
const staged = execSync2("git diff --cached --stat", { cwd, stdio: "pipe" }).toString().trim();
|
|
818
|
-
if (staged) {
|
|
819
|
-
execSync2(
|
|
820
|
-
`git commit --no-verify -m "projx: update to template v${version} (3-way merge)"`,
|
|
821
|
-
{ cwd, stdio: "pipe" }
|
|
822
|
-
);
|
|
823
|
-
}
|
|
824
|
-
saveBaselineRef(cwd);
|
|
825
|
-
return result.merged.length > 0 ? { status: "merged", mergedFiles: result.merged } : { status: "clean" };
|
|
826
|
-
}
|
|
827
|
-
for (const f of result.conflicted) {
|
|
828
|
-
try {
|
|
829
|
-
execSync2(`git checkout -- "${f}"`, { cwd, stdio: "pipe" });
|
|
830
|
-
} catch {
|
|
831
|
-
}
|
|
832
|
-
}
|
|
833
|
-
for (const f of result.merged) {
|
|
834
|
-
try {
|
|
835
|
-
execSync2(`git add "${f}"`, { cwd, stdio: "pipe" });
|
|
836
|
-
} catch {
|
|
837
|
-
}
|
|
838
|
-
}
|
|
839
|
-
execSync2("git add .projx", { cwd, stdio: "pipe" });
|
|
840
|
-
return {
|
|
841
|
-
status: "conflicts",
|
|
842
|
-
mergedFiles: result.merged,
|
|
843
|
-
conflictedFiles: result.conflicted
|
|
844
|
-
};
|
|
845
|
-
}
|
|
846
|
-
await writeTemplateToDir(cwd, repoDir, components, componentPaths, vars, version, origin, componentSkips, rootSkip);
|
|
847
|
-
return { status: "conflicts" };
|
|
848
|
-
} catch (err) {
|
|
849
|
-
cleanupWorktree(cwd, worktree, branch);
|
|
850
|
-
throw err;
|
|
851
|
-
}
|
|
852
|
-
}
|
|
853
|
-
|
|
854
|
-
// src/scaffold.ts
|
|
855
98
|
async function scaffold(opts, dest, localRepo) {
|
|
856
99
|
const name = toKebab(opts.name);
|
|
857
100
|
const pm = opts.packageManager ?? "npm";
|
|
@@ -860,7 +103,7 @@ async function scaffold(opts, dest, localRepo) {
|
|
|
860
103
|
);
|
|
861
104
|
const vars = { projectName: name, components: opts.components, paths, pm: pmCommands(pm) };
|
|
862
105
|
const isLocal = !!localRepo;
|
|
863
|
-
await
|
|
106
|
+
await mkdir(dest, { recursive: true });
|
|
864
107
|
const dlSpinner = p2.spinner();
|
|
865
108
|
dlSpinner.start(isLocal ? "Using local templates" : "Downloading latest templates");
|
|
866
109
|
const repoDir = await downloadRepo(localRepo).catch((err) => {
|
|
@@ -870,16 +113,15 @@ async function scaffold(opts, dest, localRepo) {
|
|
|
870
113
|
});
|
|
871
114
|
dlSpinner.stop(isLocal ? "Local templates loaded." : "Templates downloaded.");
|
|
872
115
|
try {
|
|
873
|
-
const pkg = JSON.parse(await
|
|
116
|
+
const pkg = JSON.parse(await readFile(join(repoDir, "cli/package.json"), "utf-8"));
|
|
874
117
|
const version = pkg.version;
|
|
875
118
|
p2.log.info(`Scaffolding project in ${dest}`);
|
|
876
119
|
if (opts.git) {
|
|
877
120
|
exec("git init", dest);
|
|
878
|
-
exec("git config core.hooksPath .githooks", dest);
|
|
879
121
|
}
|
|
880
122
|
const spinner7 = p2.spinner();
|
|
881
123
|
spinner7.start("Scaffolding project");
|
|
882
|
-
await applyTemplate(dest, repoDir, opts.components, paths, vars, version);
|
|
124
|
+
await applyTemplate(dest, repoDir, opts.components, paths, vars, version, void 0, void 0, true);
|
|
883
125
|
spinner7.stop("Scaffold complete.");
|
|
884
126
|
if (opts.install) {
|
|
885
127
|
await installDeps(dest, opts.components, pm);
|
|
@@ -888,7 +130,8 @@ async function scaffold(opts, dest, localRepo) {
|
|
|
888
130
|
if (opts.git) {
|
|
889
131
|
try {
|
|
890
132
|
exec("git add -A", dest);
|
|
891
|
-
exec('git commit
|
|
133
|
+
exec('git commit -m "Initial scaffold from projx"', dest);
|
|
134
|
+
exec("git config core.hooksPath .githooks", dest);
|
|
892
135
|
saveBaselineRef(dest);
|
|
893
136
|
} catch {
|
|
894
137
|
}
|
|
@@ -913,7 +156,7 @@ async function installDeps(dest, components, pm) {
|
|
|
913
156
|
case "fastapi":
|
|
914
157
|
if (hasCommand("uv")) {
|
|
915
158
|
spinner7.start("Installing FastAPI dependencies (uv sync)");
|
|
916
|
-
exec("uv sync --all-extras",
|
|
159
|
+
exec("uv sync --all-extras", join(dest, "fastapi"));
|
|
917
160
|
spinner7.stop("FastAPI dependencies installed.");
|
|
918
161
|
} else {
|
|
919
162
|
p2.log.warn("uv not found \u2014 run 'cd fastapi && uv sync' manually.");
|
|
@@ -922,7 +165,7 @@ async function installDeps(dest, components, pm) {
|
|
|
922
165
|
case "fastify":
|
|
923
166
|
if (hasCommand(pmBin)) {
|
|
924
167
|
spinner7.start(`Installing Fastify dependencies (${cmds.install})`);
|
|
925
|
-
exec(cmds.install,
|
|
168
|
+
exec(cmds.install, join(dest, "fastify"));
|
|
926
169
|
spinner7.stop("Fastify dependencies installed.");
|
|
927
170
|
} else {
|
|
928
171
|
p2.log.warn(`${pm} not found \u2014 run 'cd fastify && ${cmds.install}' manually.`);
|
|
@@ -931,7 +174,7 @@ async function installDeps(dest, components, pm) {
|
|
|
931
174
|
case "frontend":
|
|
932
175
|
if (hasCommand(pmBin)) {
|
|
933
176
|
spinner7.start(`Installing Frontend dependencies (${cmds.install})`);
|
|
934
|
-
exec(cmds.install,
|
|
177
|
+
exec(cmds.install, join(dest, "frontend"));
|
|
935
178
|
spinner7.stop("Frontend dependencies installed.");
|
|
936
179
|
} else {
|
|
937
180
|
p2.log.warn(`${pm} not found \u2014 run 'cd frontend && ${cmds.install}' manually.`);
|
|
@@ -940,7 +183,7 @@ async function installDeps(dest, components, pm) {
|
|
|
940
183
|
case "e2e":
|
|
941
184
|
if (hasCommand(pmBin)) {
|
|
942
185
|
spinner7.start(`Installing E2E dependencies (${cmds.install})`);
|
|
943
|
-
exec(cmds.install,
|
|
186
|
+
exec(cmds.install, join(dest, "e2e"));
|
|
944
187
|
spinner7.stop("E2E dependencies installed.");
|
|
945
188
|
} else {
|
|
946
189
|
p2.log.warn(`${pm} not found \u2014 run 'cd e2e && ${cmds.install}' manually.`);
|
|
@@ -949,7 +192,7 @@ async function installDeps(dest, components, pm) {
|
|
|
949
192
|
case "mobile":
|
|
950
193
|
if (hasCommand("flutter")) {
|
|
951
194
|
spinner7.start("Installing Flutter dependencies");
|
|
952
|
-
exec("flutter pub get",
|
|
195
|
+
exec("flutter pub get", join(dest, "mobile"));
|
|
953
196
|
spinner7.stop("Flutter dependencies installed.");
|
|
954
197
|
} else {
|
|
955
198
|
p2.log.warn("Flutter not found \u2014 run 'cd mobile && flutter pub get' manually.");
|
|
@@ -965,9 +208,9 @@ async function installDeps(dest, components, pm) {
|
|
|
965
208
|
}
|
|
966
209
|
function copyEnvExamples(dest, components) {
|
|
967
210
|
for (const component of components) {
|
|
968
|
-
const example =
|
|
969
|
-
const env =
|
|
970
|
-
if (
|
|
211
|
+
const example = join(dest, component, ".env.example");
|
|
212
|
+
const env = join(dest, component, ".env");
|
|
213
|
+
if (existsSync(example) && !existsSync(env)) {
|
|
971
214
|
try {
|
|
972
215
|
copyFileSync(example, env);
|
|
973
216
|
} catch {
|
|
@@ -977,10 +220,10 @@ function copyEnvExamples(dest, components) {
|
|
|
977
220
|
}
|
|
978
221
|
|
|
979
222
|
// src/update.ts
|
|
980
|
-
import { existsSync as
|
|
981
|
-
import { readFile as
|
|
982
|
-
import { execSync
|
|
983
|
-
import { join as
|
|
223
|
+
import { existsSync as existsSync2 } from "fs";
|
|
224
|
+
import { readFile as readFile2, unlink } from "fs/promises";
|
|
225
|
+
import { execSync } from "child_process";
|
|
226
|
+
import { join as join2 } from "path";
|
|
984
227
|
import * as p3 from "@clack/prompts";
|
|
985
228
|
async function update(cwd, localRepo) {
|
|
986
229
|
p3.intro("projx update");
|
|
@@ -990,39 +233,47 @@ async function update(cwd, localRepo) {
|
|
|
990
233
|
process.exit(1);
|
|
991
234
|
}
|
|
992
235
|
try {
|
|
993
|
-
|
|
236
|
+
execSync("git worktree prune", { cwd, stdio: "pipe" });
|
|
994
237
|
} catch {
|
|
995
238
|
}
|
|
239
|
+
const raw = await readProjxConfig(cwd);
|
|
240
|
+
const { components, paths: componentPaths } = await discoverComponentsFromMarkers(cwd);
|
|
241
|
+
const pendingConflicts = findFilesWithConflictMarkers(cwd);
|
|
242
|
+
if (pendingConflicts.length > 0) {
|
|
243
|
+
p3.log.warn(`Found ${pendingConflicts.length} file(s) with unresolved conflict markers from a prior update:`);
|
|
244
|
+
for (const f of pendingConflicts) p3.log.info(` ${f}`);
|
|
245
|
+
p3.log.info("");
|
|
246
|
+
const resumeVersion = String(raw.version ?? "unknown");
|
|
247
|
+
const handled = await promptSkipLearning(cwd, componentPaths, resumeVersion, pendingConflicts);
|
|
248
|
+
if (!handled) {
|
|
249
|
+
p3.log.info("");
|
|
250
|
+
p3.log.info("Resolve manually with `git diff` then `git add` / `git checkout --`,");
|
|
251
|
+
p3.log.info("or re-run `npx create-projx update` to resume the prompt.");
|
|
252
|
+
}
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
996
255
|
if (hasUncommittedChanges(cwd)) {
|
|
997
256
|
p3.log.error("You have uncommitted changes. Commit or stash them first.");
|
|
998
257
|
process.exit(1);
|
|
999
258
|
}
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
p3.log.info(`Found .projx (v${config.version}, components: ${config.components.join(", ")})`);
|
|
259
|
+
if (components.length === 0) {
|
|
260
|
+
p3.log.error("No projx components found. Run 'projx init' first.");
|
|
261
|
+
process.exit(1);
|
|
262
|
+
}
|
|
263
|
+
if (Object.keys(raw).length > 0) {
|
|
264
|
+
p3.log.info(`Found .projx (v${raw.version ?? "unknown"}, components: ${components.join(", ")})`);
|
|
1007
265
|
} else {
|
|
1008
|
-
p3.log.warn("No .projx file found.
|
|
1009
|
-
|
|
1010
|
-
if (discovered.length === 0) {
|
|
1011
|
-
p3.log.error("No projx components found. Run 'projx init' first.");
|
|
1012
|
-
process.exit(1);
|
|
1013
|
-
}
|
|
1014
|
-
config = { version: "0.0.0", components: discovered, createdAt: "unknown" };
|
|
1015
|
-
p3.log.info(`Detected: ${discovered.join(", ")}`);
|
|
266
|
+
p3.log.warn("No .projx file found. Detected components from markers.");
|
|
267
|
+
p3.log.info(`Detected: ${components.join(", ")}`);
|
|
1016
268
|
}
|
|
1017
|
-
const
|
|
1018
|
-
for (const c of config.components) {
|
|
269
|
+
for (const c of components) {
|
|
1019
270
|
const dir = componentPaths[c];
|
|
1020
271
|
p3.log.info(dir !== c ? `${c} \u2192 ${dir}/` : `${c}/`);
|
|
1021
272
|
}
|
|
1022
273
|
const componentSkips = {};
|
|
1023
|
-
for (const component of
|
|
274
|
+
for (const component of components) {
|
|
1024
275
|
const dir = componentPaths[component];
|
|
1025
|
-
const marker = await readComponentMarker(
|
|
276
|
+
const marker = await readComponentMarker(join2(cwd, dir));
|
|
1026
277
|
if (marker?.skip && marker.skip.length > 0) {
|
|
1027
278
|
componentSkips[component] = marker.skip;
|
|
1028
279
|
}
|
|
@@ -1036,17 +287,36 @@ async function update(cwd, localRepo) {
|
|
|
1036
287
|
});
|
|
1037
288
|
dlSpinner.stop(isLocal ? "Local templates loaded." : "Templates downloaded.");
|
|
1038
289
|
try {
|
|
1039
|
-
const pkg = JSON.parse(await
|
|
290
|
+
const pkg = JSON.parse(await readFile2(join2(repoDir, "cli/package.json"), "utf-8"));
|
|
1040
291
|
const version = pkg.version;
|
|
1041
|
-
const name = detectProjectName(cwd,
|
|
1042
|
-
const
|
|
1043
|
-
const
|
|
1044
|
-
const
|
|
292
|
+
const name = detectProjectName(cwd, components, componentPaths);
|
|
293
|
+
const recordedPm = raw.packageManager;
|
|
294
|
+
const detectedPm = detectPackageManagerFromComponents(cwd, componentPaths);
|
|
295
|
+
const pm = detectedPm ?? recordedPm ?? "npm";
|
|
296
|
+
if (detectedPm && recordedPm && detectedPm !== recordedPm) {
|
|
297
|
+
p3.log.warn(`packageManager mismatch: .projx says "${recordedPm}" but lockfile is "${detectedPm}". Using "${detectedPm}".`);
|
|
298
|
+
await writeProjxConfig(cwd, { ...raw, packageManager: detectedPm });
|
|
299
|
+
} else if (detectedPm && !recordedPm) {
|
|
300
|
+
await writeProjxConfig(cwd, { ...raw, packageManager: detectedPm });
|
|
301
|
+
}
|
|
302
|
+
const nameOverrides = await detectPackageNameOverrides(cwd, components, componentPaths);
|
|
303
|
+
const vars = { projectName: name, components, paths: componentPaths, pm: pmCommands(pm), nameOverrides };
|
|
1045
304
|
const spinner7 = p3.spinner();
|
|
1046
305
|
spinner7.start("Applying template update");
|
|
1047
|
-
const rootSkip =
|
|
1048
|
-
const
|
|
306
|
+
const rootSkip = Array.isArray(raw.skip) ? raw.skip : [];
|
|
307
|
+
const isLegacyMigration = !raw.defaultsApplied;
|
|
308
|
+
if (isLegacyMigration) {
|
|
309
|
+
p3.log.info("Legacy project detected \u2014 applying default skip patterns for user-owned files.");
|
|
310
|
+
}
|
|
311
|
+
const result = await applyTemplate(cwd, repoDir, components, componentPaths, vars, version, componentSkips, rootSkip, isLegacyMigration);
|
|
1049
312
|
spinner7.stop("Template applied.");
|
|
313
|
+
const pinnedUpdates = await findPinnedFilesWithUpdates(cwd, repoDir, components, componentPaths, vars, version, componentSkips, rootSkip);
|
|
314
|
+
if (pinnedUpdates.length > 0) {
|
|
315
|
+
p3.log.info("");
|
|
316
|
+
p3.log.info(`${pinnedUpdates.length} pinned file(s) have template updates available:`);
|
|
317
|
+
for (const f of pinnedUpdates) p3.log.info(` ${f}`);
|
|
318
|
+
p3.log.info("Run `npx create-projx unpin <file> && npx create-projx update` to opt in.");
|
|
319
|
+
}
|
|
1050
320
|
if (result.status === "merged") {
|
|
1051
321
|
saveBaselineRef(cwd);
|
|
1052
322
|
p3.log.success(`${result.mergedFiles?.length ?? 0} file(s) merged cleanly.`);
|
|
@@ -1062,7 +332,7 @@ async function update(cwd, localRepo) {
|
|
|
1062
332
|
p3.log.info(` ${f}`);
|
|
1063
333
|
}
|
|
1064
334
|
}
|
|
1065
|
-
const handled = await promptSkipLearning(cwd, componentPaths, version);
|
|
335
|
+
const handled = await promptSkipLearning(cwd, componentPaths, version, result.conflictedFiles ?? []);
|
|
1066
336
|
if (!handled) {
|
|
1067
337
|
p3.log.info("");
|
|
1068
338
|
p3.log.info("Review: git diff");
|
|
@@ -1084,7 +354,7 @@ async function update(cwd, localRepo) {
|
|
|
1084
354
|
}
|
|
1085
355
|
function isGitRepo(cwd) {
|
|
1086
356
|
try {
|
|
1087
|
-
|
|
357
|
+
execSync("git rev-parse --is-inside-work-tree", { cwd, stdio: "pipe" });
|
|
1088
358
|
return true;
|
|
1089
359
|
} catch {
|
|
1090
360
|
return false;
|
|
@@ -1092,33 +362,108 @@ function isGitRepo(cwd) {
|
|
|
1092
362
|
}
|
|
1093
363
|
function hasUncommittedChanges(cwd) {
|
|
1094
364
|
try {
|
|
1095
|
-
const status =
|
|
365
|
+
const status = execSync("git status --porcelain", { cwd, stdio: "pipe" }).toString().trim();
|
|
1096
366
|
return status.length > 0;
|
|
1097
367
|
} catch {
|
|
1098
368
|
return false;
|
|
1099
369
|
}
|
|
1100
370
|
}
|
|
1101
|
-
async function
|
|
1102
|
-
|
|
1103
|
-
const
|
|
1104
|
-
|
|
1105
|
-
const
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
371
|
+
async function findPinnedFilesWithUpdates(cwd, repoDir, components, componentPaths, vars, version, componentSkips, rootSkip) {
|
|
372
|
+
const { mkdir: mkdir5, rm: rm2, readFile: readFile7 } = await import("fs/promises");
|
|
373
|
+
const { tmpdir: tmpdir2 } = await import("os");
|
|
374
|
+
const { writeTemplateToDir: writeTemplateToDir2 } = await import("./baseline-72Z7TC2E.js");
|
|
375
|
+
const config = await readProjxConfig(cwd);
|
|
376
|
+
const rootPinned = Array.isArray(config.skip) ? config.skip : [];
|
|
377
|
+
const componentPinned = [];
|
|
378
|
+
for (const component of components) {
|
|
379
|
+
const dir = componentPaths[component];
|
|
380
|
+
const marker = await readComponentMarker(join2(cwd, dir));
|
|
381
|
+
if (marker?.skip && marker.skip.length > 0) {
|
|
382
|
+
componentPinned.push({ component, dir, patterns: marker.skip });
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
if (rootPinned.length === 0 && componentPinned.length === 0) return [];
|
|
386
|
+
const tmpTemplate = join2(tmpdir2(), `projx-pinned-${Date.now()}`);
|
|
387
|
+
await mkdir5(tmpTemplate, { recursive: true });
|
|
388
|
+
void componentSkips;
|
|
389
|
+
void rootSkip;
|
|
390
|
+
try {
|
|
391
|
+
await writeTemplateToDir2(tmpTemplate, repoDir, components, componentPaths, vars, version, {
|
|
392
|
+
componentSkips: {},
|
|
393
|
+
rootSkip: [],
|
|
394
|
+
realCwd: tmpTemplate
|
|
395
|
+
});
|
|
396
|
+
const updates = [];
|
|
397
|
+
for (const file of rootPinned) {
|
|
398
|
+
const tmplPath = join2(tmpTemplate, file);
|
|
399
|
+
const userPath = join2(cwd, file);
|
|
400
|
+
if (!existsSync2(tmplPath) || !existsSync2(userPath)) continue;
|
|
401
|
+
const tmplContent = await readFile7(tmplPath, "utf-8");
|
|
402
|
+
const userContent = await readFile7(userPath, "utf-8");
|
|
403
|
+
if (tmplContent !== userContent) updates.push(file);
|
|
404
|
+
}
|
|
405
|
+
for (const { dir, patterns } of componentPinned) {
|
|
406
|
+
for (const pattern of patterns) {
|
|
407
|
+
if (pattern.includes("*")) continue;
|
|
408
|
+
const rel = `${dir}/${pattern}`;
|
|
409
|
+
const tmplPath = join2(tmpTemplate, rel);
|
|
410
|
+
const userPath = join2(cwd, rel);
|
|
411
|
+
if (!existsSync2(tmplPath) || !existsSync2(userPath)) continue;
|
|
412
|
+
const tmplContent = await readFile7(tmplPath, "utf-8");
|
|
413
|
+
const userContent = await readFile7(userPath, "utf-8");
|
|
414
|
+
if (tmplContent !== userContent) updates.push(rel);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
return updates;
|
|
418
|
+
} finally {
|
|
419
|
+
await rm2(tmpTemplate, { recursive: true, force: true });
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
function findFilesWithConflictMarkers(cwd) {
|
|
423
|
+
try {
|
|
424
|
+
const out = execSync(
|
|
425
|
+
`git -c core.quotepath=off grep -lE '^<<<<<<< (your changes|HEAD)'`,
|
|
426
|
+
{ cwd, stdio: "pipe" }
|
|
427
|
+
).toString().trim();
|
|
428
|
+
if (!out) return [];
|
|
429
|
+
return out.split("\n").filter(Boolean);
|
|
430
|
+
} catch {
|
|
431
|
+
return [];
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
async function promptSkipLearning(cwd, componentPaths, version, conflictedFiles) {
|
|
435
|
+
const changedFiles = conflictedFiles.filter((f) => {
|
|
1110
436
|
const base = f.split("/").pop();
|
|
1111
437
|
if (base === ".projx" || base === COMPONENT_MARKER) return false;
|
|
1112
438
|
return true;
|
|
1113
439
|
});
|
|
1114
440
|
if (changedFiles.length === 0) return false;
|
|
1115
|
-
|
|
441
|
+
if (!process.stdin.isTTY) {
|
|
442
|
+
p3.log.info("Non-interactive: skipping prompt. Resolve conflicts manually with `git diff` then `git add`.");
|
|
443
|
+
p3.log.info("Re-run `npx create-projx update` later to interactively decide which files to keep.");
|
|
444
|
+
return false;
|
|
445
|
+
}
|
|
446
|
+
const statusOutput = execSync("git status --porcelain", { cwd, stdio: "pipe" }).toString().trim();
|
|
447
|
+
const entries = statusOutput.split("\n").filter(Boolean).map((line) => ({
|
|
448
|
+
status: line.slice(0, 2).trim(),
|
|
449
|
+
file: line.slice(3).trim()
|
|
450
|
+
}));
|
|
451
|
+
p3.log.warn(`${changedFiles.length} file(s) have conflicts to resolve.`);
|
|
452
|
+
p3.log.info("Each file is currently in your working tree with conflict markers.");
|
|
453
|
+
p3.log.info("");
|
|
454
|
+
p3.log.info("CHECKED = keep your version, resolve markers manually, commit when ready");
|
|
455
|
+
p3.log.info("UNCHECKED = discard template's changes AND skip this file on future updates");
|
|
456
|
+
p3.log.info("");
|
|
1116
457
|
const selected = await p3.multiselect({
|
|
1117
|
-
message: "
|
|
458
|
+
message: "Which files do you want to KEEP?",
|
|
1118
459
|
options: changedFiles.map((f) => ({ value: f, label: f })),
|
|
1119
460
|
required: false
|
|
1120
461
|
});
|
|
1121
|
-
if (p3.isCancel(selected))
|
|
462
|
+
if (p3.isCancel(selected)) {
|
|
463
|
+
p3.log.warn("Cancelled. Conflict markers remain in the working tree.");
|
|
464
|
+
p3.log.info("Re-run `npx create-projx update` later to resume the prompt.");
|
|
465
|
+
return false;
|
|
466
|
+
}
|
|
1122
467
|
const kept = new Set(selected);
|
|
1123
468
|
const discarded = changedFiles.filter((f) => !kept.has(f));
|
|
1124
469
|
if (discarded.length > 0) {
|
|
@@ -1126,9 +471,9 @@ async function promptSkipLearning(cwd, componentPaths, version) {
|
|
|
1126
471
|
const entry = entries.find((e) => e.file === file);
|
|
1127
472
|
try {
|
|
1128
473
|
if (entry?.status === "??") {
|
|
1129
|
-
await unlink(
|
|
474
|
+
await unlink(join2(cwd, file));
|
|
1130
475
|
} else {
|
|
1131
|
-
|
|
476
|
+
execSync(`git checkout -- "${file}"`, { cwd, stdio: "pipe" });
|
|
1132
477
|
}
|
|
1133
478
|
} catch {
|
|
1134
479
|
}
|
|
@@ -1139,7 +484,7 @@ async function promptSkipLearning(cwd, componentPaths, version) {
|
|
|
1139
484
|
);
|
|
1140
485
|
}
|
|
1141
486
|
if (kept.size > 0) {
|
|
1142
|
-
p3.log.info(`${kept.size} file(s) kept \u2014
|
|
487
|
+
p3.log.info(`${kept.size} file(s) kept with conflict markers \u2014 resolve and commit:`);
|
|
1143
488
|
p3.log.info(
|
|
1144
489
|
` git add . && git commit -m "projx: update to v${version}"`
|
|
1145
490
|
);
|
|
@@ -1173,42 +518,33 @@ async function learnSkips(cwd, files, componentPaths) {
|
|
|
1173
518
|
}
|
|
1174
519
|
for (const [component, additions] of Object.entries(componentSkipAdds)) {
|
|
1175
520
|
const dir = componentPaths[component];
|
|
1176
|
-
const
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
data.skip = [.../* @__PURE__ */ new Set([...existing, ...additions])];
|
|
1181
|
-
await writeFile3(markerPath, JSON.stringify(data, null, 2) + "\n");
|
|
1182
|
-
} catch {
|
|
1183
|
-
}
|
|
521
|
+
const marker = await readComponentMarker(join2(cwd, dir));
|
|
522
|
+
if (!marker) continue;
|
|
523
|
+
const merged = [.../* @__PURE__ */ new Set([...marker.skip, ...additions])];
|
|
524
|
+
await writeComponentMarker(join2(cwd, dir), { ...marker, skip: merged });
|
|
1184
525
|
}
|
|
1185
526
|
if (rootSkipAdds.length > 0) {
|
|
1186
|
-
const
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
data.skip = [.../* @__PURE__ */ new Set([...existing, ...rootSkipAdds])];
|
|
1191
|
-
await writeFile3(configPath, JSON.stringify(data, null, 2) + "\n");
|
|
1192
|
-
} catch {
|
|
1193
|
-
}
|
|
527
|
+
const config = await readProjxConfig(cwd);
|
|
528
|
+
const existing = Array.isArray(config.skip) ? config.skip : [];
|
|
529
|
+
const merged = [.../* @__PURE__ */ new Set([...existing, ...rootSkipAdds])];
|
|
530
|
+
await writeProjxConfig(cwd, { ...config, skip: merged });
|
|
1194
531
|
}
|
|
1195
532
|
}
|
|
1196
533
|
|
|
1197
534
|
// src/add.ts
|
|
1198
|
-
import { copyFileSync as copyFileSync2, existsSync as
|
|
1199
|
-
import { readFile as
|
|
1200
|
-
import { join as
|
|
535
|
+
import { copyFileSync as copyFileSync2, existsSync as existsSync3 } from "fs";
|
|
536
|
+
import { readFile as readFile3 } from "fs/promises";
|
|
537
|
+
import { join as join3 } from "path";
|
|
1201
538
|
import * as p4 from "@clack/prompts";
|
|
1202
539
|
async function add(cwd, newComponents, localRepo, skipInstall = false) {
|
|
1203
540
|
p4.intro("projx add");
|
|
1204
541
|
const isLocal = !!localRepo;
|
|
1205
|
-
|
|
1206
|
-
if (!existsSync5(configPath)) {
|
|
542
|
+
if (!existsSync3(join3(cwd, ".projx"))) {
|
|
1207
543
|
p4.log.error("No .projx file found. Run 'npx create-projx <name>' to create a project first.");
|
|
1208
544
|
process.exit(1);
|
|
1209
545
|
}
|
|
1210
|
-
const config =
|
|
1211
|
-
const existing =
|
|
546
|
+
const config = await readProjxConfig(cwd);
|
|
547
|
+
const { components: existing } = await discoverComponentsFromMarkers(cwd);
|
|
1212
548
|
const alreadyExists = newComponents.filter((c) => existing.includes(c));
|
|
1213
549
|
if (alreadyExists.length > 0) {
|
|
1214
550
|
p4.log.warn(`Already present: ${alreadyExists.join(", ")}. Skipping those.`);
|
|
@@ -1235,19 +571,19 @@ async function add(cwd, newComponents, localRepo, skipInstall = false) {
|
|
|
1235
571
|
const pm = config.packageManager ?? "npm";
|
|
1236
572
|
const name = detectProjectName(cwd, existing, paths);
|
|
1237
573
|
const vars = { projectName: name, components: allComponents, paths, pm: pmCommands(pm) };
|
|
1238
|
-
const pkg = JSON.parse(await
|
|
574
|
+
const pkg = JSON.parse(await readFile3(join3(repoDir, "cli/package.json"), "utf-8"));
|
|
1239
575
|
const version = pkg.version;
|
|
1240
576
|
const spinner7 = p4.spinner();
|
|
1241
577
|
spinner7.start("Adding components");
|
|
1242
|
-
await writeTemplateToDir(cwd, repoDir, allComponents, paths, vars, version,
|
|
578
|
+
await writeTemplateToDir(cwd, repoDir, allComponents, paths, vars, version, { realCwd: cwd });
|
|
1243
579
|
spinner7.stop("Components added.");
|
|
1244
580
|
if (!skipInstall) {
|
|
1245
581
|
await installDeps2(cwd, toAdd, pm);
|
|
1246
582
|
}
|
|
1247
583
|
for (const component of toAdd) {
|
|
1248
|
-
const example =
|
|
1249
|
-
const env =
|
|
1250
|
-
if (
|
|
584
|
+
const example = join3(cwd, component, ".env.example");
|
|
585
|
+
const env = join3(cwd, component, ".env");
|
|
586
|
+
if (existsSync3(example) && !existsSync3(env)) {
|
|
1251
587
|
try {
|
|
1252
588
|
copyFileSync2(example, env);
|
|
1253
589
|
} catch {
|
|
@@ -1271,7 +607,7 @@ async function installDeps2(dest, components, pm) {
|
|
|
1271
607
|
case "fastapi":
|
|
1272
608
|
if (hasCommand("uv")) {
|
|
1273
609
|
spinner7.start("Installing FastAPI dependencies");
|
|
1274
|
-
exec("uv sync --all-extras",
|
|
610
|
+
exec("uv sync --all-extras", join3(dest, "fastapi"));
|
|
1275
611
|
spinner7.stop("FastAPI dependencies installed.");
|
|
1276
612
|
} else {
|
|
1277
613
|
p4.log.warn("uv not found \u2014 run 'cd fastapi && uv sync' manually.");
|
|
@@ -1280,7 +616,7 @@ async function installDeps2(dest, components, pm) {
|
|
|
1280
616
|
case "fastify":
|
|
1281
617
|
if (hasCommand(pmBin)) {
|
|
1282
618
|
spinner7.start(`Installing Fastify dependencies (${cmds.install})`);
|
|
1283
|
-
exec(cmds.install,
|
|
619
|
+
exec(cmds.install, join3(dest, "fastify"));
|
|
1284
620
|
spinner7.stop("Fastify dependencies installed.");
|
|
1285
621
|
} else {
|
|
1286
622
|
p4.log.warn(`${pm} not found \u2014 run 'cd fastify && ${cmds.install}' manually.`);
|
|
@@ -1289,7 +625,7 @@ async function installDeps2(dest, components, pm) {
|
|
|
1289
625
|
case "frontend":
|
|
1290
626
|
if (hasCommand(pmBin)) {
|
|
1291
627
|
spinner7.start(`Installing Frontend dependencies (${cmds.install})`);
|
|
1292
|
-
exec(cmds.install,
|
|
628
|
+
exec(cmds.install, join3(dest, "frontend"));
|
|
1293
629
|
spinner7.stop("Frontend dependencies installed.");
|
|
1294
630
|
} else {
|
|
1295
631
|
p4.log.warn(`${pm} not found \u2014 run 'cd frontend && ${cmds.install}' manually.`);
|
|
@@ -1298,7 +634,7 @@ async function installDeps2(dest, components, pm) {
|
|
|
1298
634
|
case "e2e":
|
|
1299
635
|
if (hasCommand(pmBin)) {
|
|
1300
636
|
spinner7.start(`Installing E2E dependencies (${cmds.install})`);
|
|
1301
|
-
exec(cmds.install,
|
|
637
|
+
exec(cmds.install, join3(dest, "e2e"));
|
|
1302
638
|
spinner7.stop("E2E dependencies installed.");
|
|
1303
639
|
} else {
|
|
1304
640
|
p4.log.warn(`${pm} not found \u2014 run 'cd e2e && ${cmds.install}' manually.`);
|
|
@@ -1307,7 +643,7 @@ async function installDeps2(dest, components, pm) {
|
|
|
1307
643
|
case "mobile":
|
|
1308
644
|
if (hasCommand("flutter")) {
|
|
1309
645
|
spinner7.start("Installing Flutter dependencies");
|
|
1310
|
-
exec("flutter pub get",
|
|
646
|
+
exec("flutter pub get", join3(dest, "mobile"));
|
|
1311
647
|
spinner7.stop("Flutter dependencies installed.");
|
|
1312
648
|
} else {
|
|
1313
649
|
p4.log.warn("Flutter not found \u2014 run 'cd mobile && flutter pub get' manually.");
|
|
@@ -1323,22 +659,22 @@ async function installDeps2(dest, components, pm) {
|
|
|
1323
659
|
}
|
|
1324
660
|
|
|
1325
661
|
// src/init.ts
|
|
1326
|
-
import { existsSync as
|
|
1327
|
-
import { readFile as
|
|
1328
|
-
import { execSync as
|
|
1329
|
-
import { join as
|
|
662
|
+
import { existsSync as existsSync5 } from "fs";
|
|
663
|
+
import { readFile as readFile4 } from "fs/promises";
|
|
664
|
+
import { execSync as execSync2 } from "child_process";
|
|
665
|
+
import { join as join5 } from "path";
|
|
1330
666
|
import * as p5 from "@clack/prompts";
|
|
1331
667
|
|
|
1332
668
|
// src/detect.ts
|
|
1333
|
-
import { existsSync as
|
|
1334
|
-
import { readdir
|
|
1335
|
-
import { join as
|
|
669
|
+
import { existsSync as existsSync4 } from "fs";
|
|
670
|
+
import { readdir } from "fs/promises";
|
|
671
|
+
import { join as join4 } from "path";
|
|
1336
672
|
async function detectComponents(cwd) {
|
|
1337
673
|
const results = [];
|
|
1338
|
-
const entries = await
|
|
674
|
+
const entries = await readdir(cwd, { withFileTypes: true });
|
|
1339
675
|
const dirs = entries.filter((e) => e.isDirectory() && !e.name.startsWith(".") && !EXCLUDE.has(e.name)).map((e) => e.name);
|
|
1340
676
|
for (const dir of dirs) {
|
|
1341
|
-
const full =
|
|
677
|
+
const full = join4(cwd, dir);
|
|
1342
678
|
const detections = await scanDirectory(full, dir);
|
|
1343
679
|
results.push(...detections);
|
|
1344
680
|
}
|
|
@@ -1346,7 +682,7 @@ async function detectComponents(cwd) {
|
|
|
1346
682
|
}
|
|
1347
683
|
async function scanDirectory(dir, relPath) {
|
|
1348
684
|
const results = [];
|
|
1349
|
-
const pyproject = await readFileOrNull(
|
|
685
|
+
const pyproject = await readFileOrNull(join4(dir, "pyproject.toml"));
|
|
1350
686
|
if (pyproject && /fastapi/i.test(pyproject)) {
|
|
1351
687
|
results.push({
|
|
1352
688
|
component: "fastapi",
|
|
@@ -1383,7 +719,7 @@ async function scanDirectory(dir, relPath) {
|
|
|
1383
719
|
});
|
|
1384
720
|
}
|
|
1385
721
|
}
|
|
1386
|
-
const pubspec = await readFileOrNull(
|
|
722
|
+
const pubspec = await readFileOrNull(join4(dir, "pubspec.yaml"));
|
|
1387
723
|
if (pubspec && /flutter:/i.test(pubspec)) {
|
|
1388
724
|
results.push({
|
|
1389
725
|
component: "mobile",
|
|
@@ -1392,7 +728,7 @@ async function scanDirectory(dir, relPath) {
|
|
|
1392
728
|
evidence: "pubspec.yaml has flutter dependency"
|
|
1393
729
|
});
|
|
1394
730
|
}
|
|
1395
|
-
const hasTf =
|
|
731
|
+
const hasTf = existsSync4(join4(dir, "main.tf")) || existsSync4(join4(dir, "variables.tf")) || existsSync4(join4(dir, "stack/main.tf")) || existsSync4(join4(dir, "versions.tf"));
|
|
1396
732
|
if (hasTf) {
|
|
1397
733
|
results.push({
|
|
1398
734
|
component: "infra",
|
|
@@ -1404,7 +740,7 @@ async function scanDirectory(dir, relPath) {
|
|
|
1404
740
|
return results;
|
|
1405
741
|
}
|
|
1406
742
|
async function readPkg(dir) {
|
|
1407
|
-
const content = await readFileOrNull(
|
|
743
|
+
const content = await readFileOrNull(join4(dir, "package.json"));
|
|
1408
744
|
if (!content) return null;
|
|
1409
745
|
try {
|
|
1410
746
|
return JSON.parse(content);
|
|
@@ -1417,7 +753,7 @@ async function readPkg(dir) {
|
|
|
1417
753
|
async function init(cwd, localRepo) {
|
|
1418
754
|
p5.intro("projx init");
|
|
1419
755
|
const isLocal = !!localRepo;
|
|
1420
|
-
if (
|
|
756
|
+
if (existsSync5(join5(cwd, ".projx"))) {
|
|
1421
757
|
p5.log.error("This project is already initialized. Use 'npx create-projx update' or 'npx create-projx add' instead.");
|
|
1422
758
|
process.exit(1);
|
|
1423
759
|
}
|
|
@@ -1477,15 +813,15 @@ async function init(cwd, localRepo) {
|
|
|
1477
813
|
});
|
|
1478
814
|
dlSpinner.stop(isLocal ? "Local templates loaded." : "Templates downloaded.");
|
|
1479
815
|
try {
|
|
1480
|
-
const pkg = JSON.parse(await
|
|
816
|
+
const pkg = JSON.parse(await readFile4(join5(repoDir, "cli/package.json"), "utf-8"));
|
|
1481
817
|
const version = pkg.version;
|
|
1482
818
|
const applySpinner = p5.spinner();
|
|
1483
819
|
applySpinner.start("Applying template");
|
|
1484
|
-
const result = await applyTemplate(cwd, repoDir, components, paths, vars, version,
|
|
820
|
+
const result = await applyTemplate(cwd, repoDir, components, paths, vars, version, void 0, void 0, true);
|
|
1485
821
|
applySpinner.stop("Template applied.");
|
|
1486
|
-
if (
|
|
822
|
+
if (existsSync5(join5(cwd, ".githooks"))) {
|
|
1487
823
|
try {
|
|
1488
|
-
|
|
824
|
+
execSync2("git config core.hooksPath .githooks", { cwd, stdio: "pipe" });
|
|
1489
825
|
} catch {
|
|
1490
826
|
}
|
|
1491
827
|
}
|
|
@@ -1544,7 +880,7 @@ async function manualSelect(cwd) {
|
|
|
1544
880
|
defaultValue: component
|
|
1545
881
|
});
|
|
1546
882
|
if (p5.isCancel(dir)) process.exit(0);
|
|
1547
|
-
if (!
|
|
883
|
+
if (!existsSync5(join5(cwd, dir))) {
|
|
1548
884
|
p5.log.warn(`${dir}/ does not exist \u2014 skipping.`);
|
|
1549
885
|
continue;
|
|
1550
886
|
}
|
|
@@ -1554,7 +890,7 @@ async function manualSelect(cwd) {
|
|
|
1554
890
|
}
|
|
1555
891
|
function isGitRepo2(cwd) {
|
|
1556
892
|
try {
|
|
1557
|
-
|
|
893
|
+
execSync2("git rev-parse --is-inside-work-tree", { cwd, stdio: "pipe" });
|
|
1558
894
|
return true;
|
|
1559
895
|
} catch {
|
|
1560
896
|
return false;
|
|
@@ -1562,7 +898,7 @@ function isGitRepo2(cwd) {
|
|
|
1562
898
|
}
|
|
1563
899
|
function hasUncommittedChanges2(cwd) {
|
|
1564
900
|
try {
|
|
1565
|
-
const status =
|
|
901
|
+
const status = execSync2("git status --porcelain", { cwd, stdio: "pipe" }).toString().trim();
|
|
1566
902
|
return status.length > 0;
|
|
1567
903
|
} catch {
|
|
1568
904
|
return false;
|
|
@@ -1570,9 +906,8 @@ function hasUncommittedChanges2(cwd) {
|
|
|
1570
906
|
}
|
|
1571
907
|
|
|
1572
908
|
// src/pin.ts
|
|
1573
|
-
import { existsSync as
|
|
1574
|
-
import {
|
|
1575
|
-
import { join as join9 } from "path";
|
|
909
|
+
import { existsSync as existsSync6 } from "fs";
|
|
910
|
+
import { join as join6 } from "path";
|
|
1576
911
|
import * as p6 from "@clack/prompts";
|
|
1577
912
|
function classifyPattern(pattern, componentPaths) {
|
|
1578
913
|
const dirToComponent = {};
|
|
@@ -1592,12 +927,11 @@ function classifyPattern(pattern, componentPaths) {
|
|
|
1592
927
|
}
|
|
1593
928
|
async function pin(cwd, patterns) {
|
|
1594
929
|
p6.intro("projx pin");
|
|
1595
|
-
|
|
1596
|
-
if (!existsSync8(configPath)) {
|
|
930
|
+
if (!existsSync6(join6(cwd, ".projx"))) {
|
|
1597
931
|
p6.log.error("No .projx file found. Run 'npx create-projx init' first.");
|
|
1598
932
|
process.exit(1);
|
|
1599
933
|
}
|
|
1600
|
-
const config =
|
|
934
|
+
const config = await readProjxConfig(cwd);
|
|
1601
935
|
const componentPaths = (await discoverComponentsFromMarkers(cwd)).paths;
|
|
1602
936
|
const rootAdds = [];
|
|
1603
937
|
const componentAdds = {};
|
|
@@ -1616,30 +950,27 @@ async function pin(cwd, patterns) {
|
|
|
1616
950
|
}
|
|
1617
951
|
for (const [component, additions] of Object.entries(componentAdds)) {
|
|
1618
952
|
const dir = componentPaths[component];
|
|
1619
|
-
const
|
|
1620
|
-
|
|
1621
|
-
const data = JSON.parse(await readFile8(markerPath, "utf-8"));
|
|
1622
|
-
const existing = data.skip ?? [];
|
|
1623
|
-
const merged = [.../* @__PURE__ */ new Set([...existing, ...additions])];
|
|
1624
|
-
const added = merged.length - existing.length;
|
|
1625
|
-
if (added > 0) {
|
|
1626
|
-
data.skip = merged;
|
|
1627
|
-
await writeFile4(markerPath, JSON.stringify(data, null, 2) + "\n");
|
|
1628
|
-
p6.log.success(`${component}: pinned ${additions.join(", ")}`);
|
|
1629
|
-
} else {
|
|
1630
|
-
p6.log.info(`${component}: already pinned.`);
|
|
1631
|
-
}
|
|
1632
|
-
} catch {
|
|
953
|
+
const marker = await readComponentMarker(join6(cwd, dir));
|
|
954
|
+
if (!marker) {
|
|
1633
955
|
p6.log.error(`Could not read marker for ${component}.`);
|
|
956
|
+
continue;
|
|
957
|
+
}
|
|
958
|
+
const merged = [.../* @__PURE__ */ new Set([...marker.skip, ...additions])];
|
|
959
|
+
const added = merged.length - marker.skip.length;
|
|
960
|
+
if (added > 0) {
|
|
961
|
+
const next = { ...marker, skip: merged };
|
|
962
|
+
await writeComponentMarker(join6(cwd, dir), next);
|
|
963
|
+
p6.log.success(`${component}: pinned ${additions.join(", ")}`);
|
|
964
|
+
} else {
|
|
965
|
+
p6.log.info(`${component}: already pinned.`);
|
|
1634
966
|
}
|
|
1635
967
|
}
|
|
1636
968
|
if (rootAdds.length > 0) {
|
|
1637
|
-
const existing = config.skip
|
|
969
|
+
const existing = Array.isArray(config.skip) ? config.skip : [];
|
|
1638
970
|
const merged = [.../* @__PURE__ */ new Set([...existing, ...rootAdds])];
|
|
1639
971
|
const added = merged.length - existing.length;
|
|
1640
972
|
if (added > 0) {
|
|
1641
|
-
config
|
|
1642
|
-
await writeFile4(configPath, JSON.stringify(config, null, 2) + "\n");
|
|
973
|
+
await writeProjxConfig(cwd, { ...config, skip: merged });
|
|
1643
974
|
p6.log.success(`root: pinned ${rootAdds.join(", ")}`);
|
|
1644
975
|
} else {
|
|
1645
976
|
p6.log.info("root: already pinned.");
|
|
@@ -1649,12 +980,11 @@ async function pin(cwd, patterns) {
|
|
|
1649
980
|
}
|
|
1650
981
|
async function unpin(cwd, patterns) {
|
|
1651
982
|
p6.intro("projx unpin");
|
|
1652
|
-
|
|
1653
|
-
if (!existsSync8(configPath)) {
|
|
983
|
+
if (!existsSync6(join6(cwd, ".projx"))) {
|
|
1654
984
|
p6.log.error("No .projx file found. Run 'npx create-projx init' first.");
|
|
1655
985
|
process.exit(1);
|
|
1656
986
|
}
|
|
1657
|
-
const config =
|
|
987
|
+
const config = await readProjxConfig(cwd);
|
|
1658
988
|
const componentPaths = (await discoverComponentsFromMarkers(cwd)).paths;
|
|
1659
989
|
const rootRemoves = [];
|
|
1660
990
|
const componentRemoves = {};
|
|
@@ -1669,38 +999,27 @@ async function unpin(cwd, patterns) {
|
|
|
1669
999
|
}
|
|
1670
1000
|
for (const [component, removals] of Object.entries(componentRemoves)) {
|
|
1671
1001
|
const dir = componentPaths[component];
|
|
1672
|
-
const
|
|
1673
|
-
|
|
1674
|
-
const data = JSON.parse(await readFile8(markerPath, "utf-8"));
|
|
1675
|
-
const existing = data.skip ?? [];
|
|
1676
|
-
const filtered = existing.filter((s) => !removals.includes(s));
|
|
1677
|
-
const removed = existing.length - filtered.length;
|
|
1678
|
-
if (removed > 0) {
|
|
1679
|
-
if (filtered.length > 0) {
|
|
1680
|
-
data.skip = filtered;
|
|
1681
|
-
} else {
|
|
1682
|
-
delete data.skip;
|
|
1683
|
-
}
|
|
1684
|
-
await writeFile4(markerPath, JSON.stringify(data, null, 2) + "\n");
|
|
1685
|
-
p6.log.success(`${component}: unpinned ${removals.join(", ")}`);
|
|
1686
|
-
} else {
|
|
1687
|
-
p6.log.info(`${component}: not found in skip list.`);
|
|
1688
|
-
}
|
|
1689
|
-
} catch {
|
|
1002
|
+
const marker = await readComponentMarker(join6(cwd, dir));
|
|
1003
|
+
if (!marker) {
|
|
1690
1004
|
p6.log.error(`Could not read marker for ${component}.`);
|
|
1005
|
+
continue;
|
|
1006
|
+
}
|
|
1007
|
+
const filtered = marker.skip.filter((s) => !removals.includes(s));
|
|
1008
|
+
const removed = marker.skip.length - filtered.length;
|
|
1009
|
+
if (removed > 0) {
|
|
1010
|
+
const next = { ...marker, skip: filtered };
|
|
1011
|
+
await writeComponentMarker(join6(cwd, dir), next);
|
|
1012
|
+
p6.log.success(`${component}: unpinned ${removals.join(", ")}`);
|
|
1013
|
+
} else {
|
|
1014
|
+
p6.log.info(`${component}: not found in skip list.`);
|
|
1691
1015
|
}
|
|
1692
1016
|
}
|
|
1693
1017
|
if (rootRemoves.length > 0) {
|
|
1694
|
-
const existing = config.skip
|
|
1018
|
+
const existing = Array.isArray(config.skip) ? config.skip : [];
|
|
1695
1019
|
const filtered = existing.filter((s) => !rootRemoves.includes(s));
|
|
1696
1020
|
const removed = existing.length - filtered.length;
|
|
1697
1021
|
if (removed > 0) {
|
|
1698
|
-
|
|
1699
|
-
config.skip = filtered;
|
|
1700
|
-
} else {
|
|
1701
|
-
delete config.skip;
|
|
1702
|
-
}
|
|
1703
|
-
await writeFile4(configPath, JSON.stringify(config, null, 2) + "\n");
|
|
1022
|
+
await writeProjxConfig(cwd, { ...config, skip: filtered });
|
|
1704
1023
|
p6.log.success(`root: unpinned ${rootRemoves.join(", ")}`);
|
|
1705
1024
|
} else {
|
|
1706
1025
|
p6.log.info("root: not found in skip list.");
|
|
@@ -1710,24 +1029,24 @@ async function unpin(cwd, patterns) {
|
|
|
1710
1029
|
}
|
|
1711
1030
|
async function listPins(cwd) {
|
|
1712
1031
|
p6.intro("projx pin --list");
|
|
1713
|
-
|
|
1714
|
-
if (!existsSync8(configPath)) {
|
|
1032
|
+
if (!existsSync6(join6(cwd, ".projx"))) {
|
|
1715
1033
|
p6.log.error("No .projx file found. Run 'npx create-projx init' first.");
|
|
1716
1034
|
process.exit(1);
|
|
1717
1035
|
}
|
|
1718
|
-
const config =
|
|
1036
|
+
const config = await readProjxConfig(cwd);
|
|
1719
1037
|
const { components: discovered, paths: componentPaths } = await discoverComponentsFromMarkers(cwd);
|
|
1720
1038
|
let hasAny = false;
|
|
1721
|
-
|
|
1039
|
+
const rootSkip = Array.isArray(config.skip) ? config.skip : [];
|
|
1040
|
+
if (rootSkip.length > 0) {
|
|
1722
1041
|
hasAny = true;
|
|
1723
1042
|
p6.log.info("root:");
|
|
1724
|
-
for (const s of
|
|
1043
|
+
for (const s of rootSkip) {
|
|
1725
1044
|
p6.log.info(` ${s}`);
|
|
1726
1045
|
}
|
|
1727
1046
|
}
|
|
1728
1047
|
for (const component of discovered) {
|
|
1729
1048
|
const dir = componentPaths[component];
|
|
1730
|
-
const marker = await readComponentMarker(
|
|
1049
|
+
const marker = await readComponentMarker(join6(cwd, dir));
|
|
1731
1050
|
if (marker?.skip && marker.skip.length > 0) {
|
|
1732
1051
|
hasAny = true;
|
|
1733
1052
|
const label = dir !== component ? `${component} (${dir}/)` : `${component}`;
|
|
@@ -1744,15 +1063,15 @@ async function listPins(cwd) {
|
|
|
1744
1063
|
}
|
|
1745
1064
|
|
|
1746
1065
|
// src/doctor.ts
|
|
1747
|
-
import { existsSync as
|
|
1748
|
-
import {
|
|
1749
|
-
import { execSync as
|
|
1750
|
-
import { join as
|
|
1066
|
+
import { existsSync as existsSync7 } from "fs";
|
|
1067
|
+
import { readdir as readdir2 } from "fs/promises";
|
|
1068
|
+
import { execSync as execSync3 } from "child_process";
|
|
1069
|
+
import { join as join7 } from "path";
|
|
1751
1070
|
import * as p7 from "@clack/prompts";
|
|
1752
1071
|
async function checkConfig(cwd) {
|
|
1753
1072
|
const results = [];
|
|
1754
|
-
const configPath =
|
|
1755
|
-
if (!
|
|
1073
|
+
const configPath = join7(cwd, ".projx");
|
|
1074
|
+
if (!existsSync7(configPath)) {
|
|
1756
1075
|
results.push({
|
|
1757
1076
|
name: ".projx exists",
|
|
1758
1077
|
status: "fail",
|
|
@@ -1761,44 +1080,41 @@ async function checkConfig(cwd) {
|
|
|
1761
1080
|
});
|
|
1762
1081
|
return { results };
|
|
1763
1082
|
}
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
config = JSON.parse(await readFile9(configPath, "utf-8"));
|
|
1767
|
-
} catch {
|
|
1083
|
+
const rootConfig = await readProjxConfig(cwd);
|
|
1084
|
+
if (Object.keys(rootConfig).length === 0) {
|
|
1768
1085
|
results.push({
|
|
1769
1086
|
name: ".projx valid JSON",
|
|
1770
1087
|
status: "fail",
|
|
1771
|
-
message: ".projx contains invalid JSON."
|
|
1088
|
+
message: ".projx contains invalid JSON or is empty."
|
|
1772
1089
|
});
|
|
1773
1090
|
return { results };
|
|
1774
1091
|
}
|
|
1775
|
-
results.push({ name: ".projx exists", status: "pass", message: `v${
|
|
1776
|
-
if (!
|
|
1092
|
+
results.push({ name: ".projx exists", status: "pass", message: `v${rootConfig.version ?? "unknown"}` });
|
|
1093
|
+
if (!rootConfig.version) {
|
|
1777
1094
|
results.push({
|
|
1778
1095
|
name: ".projx fields",
|
|
1779
|
-
status: "fail",
|
|
1780
|
-
message: "Missing required fields (version, components)."
|
|
1781
|
-
});
|
|
1782
|
-
return { results };
|
|
1783
|
-
}
|
|
1784
|
-
const invalid = config.components.filter((c) => !COMPONENTS.includes(c));
|
|
1785
|
-
if (invalid.length > 0) {
|
|
1786
|
-
results.push({
|
|
1787
|
-
name: "component names",
|
|
1788
1096
|
status: "warn",
|
|
1789
|
-
message:
|
|
1097
|
+
message: "Missing version field."
|
|
1790
1098
|
});
|
|
1791
|
-
} else {
|
|
1792
|
-
results.push({ name: "component names", status: "pass", message: `${config.components.length} valid` });
|
|
1793
1099
|
}
|
|
1794
|
-
return { results,
|
|
1100
|
+
return { results, rootConfig };
|
|
1795
1101
|
}
|
|
1796
|
-
async function checkComponents(cwd,
|
|
1102
|
+
async function checkComponents(cwd, components, componentPaths) {
|
|
1797
1103
|
const results = [];
|
|
1798
|
-
|
|
1104
|
+
if (components.length === 0) {
|
|
1105
|
+
results.push({
|
|
1106
|
+
name: "components",
|
|
1107
|
+
status: "fail",
|
|
1108
|
+
message: `No ${COMPONENT_MARKER} files found in any directory.`,
|
|
1109
|
+
fix: "Run 'npx create-projx init' to detect and mark components."
|
|
1110
|
+
});
|
|
1111
|
+
return results;
|
|
1112
|
+
}
|
|
1113
|
+
results.push({ name: "components", status: "pass", message: `${components.length} discovered from markers` });
|
|
1114
|
+
for (const component of components) {
|
|
1799
1115
|
const dir = componentPaths[component];
|
|
1800
|
-
const fullDir =
|
|
1801
|
-
if (!
|
|
1116
|
+
const fullDir = join7(cwd, dir);
|
|
1117
|
+
if (!existsSync7(fullDir)) {
|
|
1802
1118
|
results.push({
|
|
1803
1119
|
name: `${component} directory`,
|
|
1804
1120
|
status: "fail",
|
|
@@ -1816,53 +1132,28 @@ async function checkComponents(cwd, config, componentPaths) {
|
|
|
1816
1132
|
});
|
|
1817
1133
|
continue;
|
|
1818
1134
|
}
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
name: `${component} marker`,
|
|
1822
|
-
status: "warn",
|
|
1823
|
-
message: `Marker in ${dir}/ does not list "${component}".`
|
|
1824
|
-
});
|
|
1825
|
-
} else {
|
|
1826
|
-
const label = dir !== component ? `${dir}/ (${component})` : `${component}/`;
|
|
1827
|
-
results.push({ name: `${component} marker`, status: "pass", message: label });
|
|
1828
|
-
}
|
|
1829
|
-
}
|
|
1830
|
-
try {
|
|
1831
|
-
const entries = await readdir3(cwd, { withFileTypes: true });
|
|
1832
|
-
for (const entry of entries) {
|
|
1833
|
-
if (!entry.isDirectory() || entry.name.startsWith(".")) continue;
|
|
1834
|
-
const markerPath = join10(cwd, entry.name, COMPONENT_MARKER);
|
|
1835
|
-
if (!existsSync9(markerPath)) continue;
|
|
1836
|
-
const isKnown = Object.values(componentPaths).includes(entry.name);
|
|
1837
|
-
if (!isKnown) {
|
|
1838
|
-
results.push({
|
|
1839
|
-
name: `orphan marker`,
|
|
1840
|
-
status: "warn",
|
|
1841
|
-
message: `${entry.name}/ has a ${COMPONENT_MARKER} but is not in .projx components.`
|
|
1842
|
-
});
|
|
1843
|
-
}
|
|
1844
|
-
}
|
|
1845
|
-
} catch {
|
|
1135
|
+
const label = dir !== component ? `${dir}/ (${component})` : `${component}/`;
|
|
1136
|
+
results.push({ name: `${component} marker`, status: "pass", message: label });
|
|
1846
1137
|
}
|
|
1847
1138
|
return results;
|
|
1848
1139
|
}
|
|
1849
1140
|
function checkGit(cwd, fix) {
|
|
1850
1141
|
const results = [];
|
|
1851
1142
|
try {
|
|
1852
|
-
|
|
1143
|
+
execSync3("git rev-parse --is-inside-work-tree", { cwd, stdio: "pipe" });
|
|
1853
1144
|
results.push({ name: "git repo", status: "pass", message: "OK" });
|
|
1854
1145
|
} catch {
|
|
1855
1146
|
results.push({ name: "git repo", status: "fail", message: "Not a git repository." });
|
|
1856
1147
|
return results;
|
|
1857
1148
|
}
|
|
1858
1149
|
try {
|
|
1859
|
-
const ref =
|
|
1150
|
+
const ref = execSync3(`git rev-parse --verify ${BASELINE_REF}`, { cwd, stdio: "pipe" }).toString().trim();
|
|
1860
1151
|
results.push({ name: "baseline ref", status: "pass", message: ref.slice(0, 8) });
|
|
1861
1152
|
} catch {
|
|
1862
1153
|
if (fix) {
|
|
1863
1154
|
saveBaselineRef(cwd);
|
|
1864
1155
|
try {
|
|
1865
|
-
|
|
1156
|
+
execSync3(`git rev-parse --verify ${BASELINE_REF}`, { cwd, stdio: "pipe" });
|
|
1866
1157
|
results.push({ name: "baseline ref", status: "pass", message: "Created from git history." });
|
|
1867
1158
|
} catch {
|
|
1868
1159
|
results.push({
|
|
@@ -1882,11 +1173,11 @@ function checkGit(cwd, fix) {
|
|
|
1882
1173
|
}
|
|
1883
1174
|
}
|
|
1884
1175
|
try {
|
|
1885
|
-
const worktrees =
|
|
1176
|
+
const worktrees = execSync3("git worktree list --porcelain", { cwd, stdio: "pipe" }).toString();
|
|
1886
1177
|
const stale = worktrees.split("\n").filter((l) => l.includes("projx-wt-") || l.includes("projx/tmp-"));
|
|
1887
1178
|
if (stale.length > 0) {
|
|
1888
1179
|
if (fix) {
|
|
1889
|
-
|
|
1180
|
+
execSync3("git worktree prune", { cwd, stdio: "pipe" });
|
|
1890
1181
|
results.push({ name: "worktrees", status: "pass", message: "Pruned stale worktrees." });
|
|
1891
1182
|
} else {
|
|
1892
1183
|
results.push({
|
|
@@ -1904,7 +1195,7 @@ function checkGit(cwd, fix) {
|
|
|
1904
1195
|
results.push({ name: "worktrees", status: "pass", message: "OK" });
|
|
1905
1196
|
}
|
|
1906
1197
|
try {
|
|
1907
|
-
const status =
|
|
1198
|
+
const status = execSync3("git status --porcelain", { cwd, stdio: "pipe" }).toString().trim();
|
|
1908
1199
|
if (status) {
|
|
1909
1200
|
const count = status.split("\n").length;
|
|
1910
1201
|
results.push({ name: "working tree", status: "warn", message: `${count} uncommitted change(s).` });
|
|
@@ -1915,26 +1206,25 @@ function checkGit(cwd, fix) {
|
|
|
1915
1206
|
}
|
|
1916
1207
|
return results;
|
|
1917
1208
|
}
|
|
1918
|
-
async function checkSkipPatterns(cwd,
|
|
1209
|
+
async function checkSkipPatterns(cwd, rootConfig, components, componentPaths) {
|
|
1919
1210
|
const results = [];
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
}
|
|
1211
|
+
const rootSkip = Array.isArray(rootConfig.skip) ? rootConfig.skip : [];
|
|
1212
|
+
for (const pattern of rootSkip) {
|
|
1213
|
+
const matches = await patternMatchesAnything(cwd, pattern);
|
|
1214
|
+
if (!matches) {
|
|
1215
|
+
results.push({
|
|
1216
|
+
name: "root skip",
|
|
1217
|
+
status: "warn",
|
|
1218
|
+
message: `"${pattern}" matches no files \u2014 stale?`
|
|
1219
|
+
});
|
|
1930
1220
|
}
|
|
1931
1221
|
}
|
|
1932
|
-
for (const component of
|
|
1222
|
+
for (const component of components) {
|
|
1933
1223
|
const dir = componentPaths[component];
|
|
1934
|
-
const marker = await readComponentMarker(
|
|
1224
|
+
const marker = await readComponentMarker(join7(cwd, dir));
|
|
1935
1225
|
if (marker?.skip && marker.skip.length > 0) {
|
|
1936
1226
|
for (const pattern of marker.skip) {
|
|
1937
|
-
const matches = await patternMatchesAnything(
|
|
1227
|
+
const matches = await patternMatchesAnything(join7(cwd, dir), pattern);
|
|
1938
1228
|
if (!matches) {
|
|
1939
1229
|
results.push({
|
|
1940
1230
|
name: `${component} skip`,
|
|
@@ -1945,23 +1235,23 @@ async function checkSkipPatterns(cwd, config, componentPaths) {
|
|
|
1945
1235
|
}
|
|
1946
1236
|
}
|
|
1947
1237
|
}
|
|
1948
|
-
if (results.length === 0 && (
|
|
1238
|
+
if (results.length === 0 && (rootSkip.length > 0 || components.length > 0)) {
|
|
1949
1239
|
results.push({ name: "skip patterns", status: "pass", message: "All patterns match files." });
|
|
1950
1240
|
}
|
|
1951
1241
|
return results;
|
|
1952
1242
|
}
|
|
1953
1243
|
async function patternMatchesAnything(dir, pattern) {
|
|
1954
1244
|
if (pattern === "**") return true;
|
|
1955
|
-
if (!
|
|
1245
|
+
if (!existsSync7(dir)) return false;
|
|
1956
1246
|
const walk = async (current, base) => {
|
|
1957
1247
|
let entries;
|
|
1958
1248
|
try {
|
|
1959
|
-
entries = await
|
|
1249
|
+
entries = await readdir2(current, { withFileTypes: true });
|
|
1960
1250
|
} catch {
|
|
1961
1251
|
return false;
|
|
1962
1252
|
}
|
|
1963
1253
|
for (const entry of entries) {
|
|
1964
|
-
const full =
|
|
1254
|
+
const full = join7(current, entry.name);
|
|
1965
1255
|
const rel = full.slice(base.length + 1);
|
|
1966
1256
|
if (entry.isDirectory()) {
|
|
1967
1257
|
if (await walk(full, base)) return true;
|
|
@@ -1976,17 +1266,16 @@ async function patternMatchesAnything(dir, pattern) {
|
|
|
1976
1266
|
async function doctor(cwd, fix = false) {
|
|
1977
1267
|
p7.intro("projx doctor");
|
|
1978
1268
|
const allResults = [];
|
|
1979
|
-
const { results: configResults,
|
|
1269
|
+
const { results: configResults, rootConfig } = await checkConfig(cwd);
|
|
1980
1270
|
allResults.push(...configResults);
|
|
1981
|
-
if (!
|
|
1271
|
+
if (!rootConfig) {
|
|
1982
1272
|
printReport(allResults);
|
|
1983
1273
|
process.exit(1);
|
|
1984
1274
|
}
|
|
1985
|
-
const { components
|
|
1986
|
-
|
|
1987
|
-
allResults.push(...await checkComponents(cwd, resolvedConfig, componentPaths));
|
|
1275
|
+
const { components, paths: componentPaths } = await discoverComponentsFromMarkers(cwd);
|
|
1276
|
+
allResults.push(...await checkComponents(cwd, components, componentPaths));
|
|
1988
1277
|
allResults.push(...checkGit(cwd, fix));
|
|
1989
|
-
allResults.push(...await checkSkipPatterns(cwd,
|
|
1278
|
+
allResults.push(...await checkSkipPatterns(cwd, rootConfig, components, componentPaths));
|
|
1990
1279
|
printReport(allResults);
|
|
1991
1280
|
const passed = allResults.filter((r) => r.status === "pass").length;
|
|
1992
1281
|
const warns = allResults.filter((r) => r.status === "warn").length;
|
|
@@ -2010,10 +1299,10 @@ function printReport(results) {
|
|
|
2010
1299
|
}
|
|
2011
1300
|
|
|
2012
1301
|
// src/diff.ts
|
|
2013
|
-
import { existsSync as
|
|
2014
|
-
import { readFile as
|
|
2015
|
-
import { join as
|
|
2016
|
-
import { tmpdir
|
|
1302
|
+
import { existsSync as existsSync8 } from "fs";
|
|
1303
|
+
import { readFile as readFile5, mkdir as mkdir2, rm } from "fs/promises";
|
|
1304
|
+
import { join as join8 } from "path";
|
|
1305
|
+
import { tmpdir } from "os";
|
|
2017
1306
|
import * as p8 from "@clack/prompts";
|
|
2018
1307
|
function isSkipped(file, componentPaths, componentSkips, rootSkip) {
|
|
2019
1308
|
for (const [component, dir] of Object.entries(componentPaths)) {
|
|
@@ -2036,23 +1325,21 @@ function fileComponent(file, componentPaths) {
|
|
|
2036
1325
|
async function diff(cwd, localRepo) {
|
|
2037
1326
|
p8.intro("projx diff");
|
|
2038
1327
|
const isLocal = !!localRepo;
|
|
2039
|
-
|
|
2040
|
-
if (!existsSync10(configPath)) {
|
|
1328
|
+
if (!existsSync8(join8(cwd, ".projx"))) {
|
|
2041
1329
|
p8.log.error("No .projx file found. Run 'npx create-projx init' first.");
|
|
2042
1330
|
process.exit(1);
|
|
2043
1331
|
}
|
|
2044
|
-
const raw =
|
|
2045
|
-
const { components
|
|
2046
|
-
const config = { ...raw, components: discovered.length > 0 ? discovered : raw.components };
|
|
1332
|
+
const raw = await readProjxConfig(cwd);
|
|
1333
|
+
const { components, paths: componentPaths } = await discoverComponentsFromMarkers(cwd);
|
|
2047
1334
|
const componentSkips = {};
|
|
2048
|
-
for (const component of
|
|
1335
|
+
for (const component of components) {
|
|
2049
1336
|
const dir = componentPaths[component];
|
|
2050
|
-
const marker = await readComponentMarker(
|
|
1337
|
+
const marker = await readComponentMarker(join8(cwd, dir));
|
|
2051
1338
|
if (marker?.skip && marker.skip.length > 0) {
|
|
2052
1339
|
componentSkips[component] = marker.skip;
|
|
2053
1340
|
}
|
|
2054
1341
|
}
|
|
2055
|
-
const rootSkip =
|
|
1342
|
+
const rootSkip = Array.isArray(raw.skip) ? raw.skip : [];
|
|
2056
1343
|
const dlSpinner = p8.spinner();
|
|
2057
1344
|
dlSpinner.start(isLocal ? "Using local templates" : "Downloading latest templates");
|
|
2058
1345
|
const repoDir = await downloadRepo(localRepo).catch((err) => {
|
|
@@ -2062,16 +1349,20 @@ async function diff(cwd, localRepo) {
|
|
|
2062
1349
|
});
|
|
2063
1350
|
dlSpinner.stop(isLocal ? "Local templates loaded." : "Templates downloaded.");
|
|
2064
1351
|
try {
|
|
2065
|
-
const pkg = JSON.parse(await
|
|
1352
|
+
const pkg = JSON.parse(await readFile5(join8(repoDir, "cli/package.json"), "utf-8"));
|
|
2066
1353
|
const version = pkg.version;
|
|
2067
|
-
p8.log.info(`Current: v${
|
|
2068
|
-
const name = detectProjectName(cwd,
|
|
2069
|
-
const vars = { projectName: name, components
|
|
1354
|
+
p8.log.info(`Current: v${raw.version ?? "unknown"} \u2192 Template: v${version}`);
|
|
1355
|
+
const name = detectProjectName(cwd, components, componentPaths);
|
|
1356
|
+
const vars = { projectName: name, components, paths: componentPaths, pm: pmCommands(raw.packageManager ?? "npm") };
|
|
2070
1357
|
const spinner7 = p8.spinner();
|
|
2071
1358
|
spinner7.start("Analyzing changes");
|
|
2072
|
-
const tmpTemplate =
|
|
2073
|
-
await
|
|
2074
|
-
await writeTemplateToDir(tmpTemplate, repoDir,
|
|
1359
|
+
const tmpTemplate = join8(tmpdir(), `projx-diff-${Date.now()}`);
|
|
1360
|
+
await mkdir2(tmpTemplate, { recursive: true });
|
|
1361
|
+
await writeTemplateToDir(tmpTemplate, repoDir, components, componentPaths, vars, version, {
|
|
1362
|
+
componentSkips,
|
|
1363
|
+
rootSkip,
|
|
1364
|
+
realCwd: cwd
|
|
1365
|
+
});
|
|
2075
1366
|
const baselineRef = getBaselineRef(cwd);
|
|
2076
1367
|
const templateFiles = await collectAllFiles(tmpTemplate, tmpTemplate);
|
|
2077
1368
|
const analyses = [];
|
|
@@ -2081,16 +1372,16 @@ async function diff(cwd, localRepo) {
|
|
|
2081
1372
|
analyses.push({ file, status: "skipped", component });
|
|
2082
1373
|
continue;
|
|
2083
1374
|
}
|
|
2084
|
-
const oursPath =
|
|
2085
|
-
if (!
|
|
1375
|
+
const oursPath = join8(cwd, file);
|
|
1376
|
+
if (!existsSync8(oursPath)) {
|
|
2086
1377
|
analyses.push({ file, status: "new", component });
|
|
2087
1378
|
continue;
|
|
2088
1379
|
}
|
|
2089
1380
|
let oursContent;
|
|
2090
1381
|
let theirsContent;
|
|
2091
1382
|
try {
|
|
2092
|
-
oursContent = await
|
|
2093
|
-
theirsContent = await
|
|
1383
|
+
oursContent = await readFile5(oursPath, "utf-8");
|
|
1384
|
+
theirsContent = await readFile5(join8(tmpTemplate, file), "utf-8");
|
|
2094
1385
|
} catch {
|
|
2095
1386
|
continue;
|
|
2096
1387
|
}
|
|
@@ -2115,7 +1406,7 @@ async function diff(cwd, localRepo) {
|
|
|
2115
1406
|
analyses.push({ file, status: "needs-merge", component });
|
|
2116
1407
|
}
|
|
2117
1408
|
}
|
|
2118
|
-
await
|
|
1409
|
+
await rm(tmpTemplate, { recursive: true, force: true });
|
|
2119
1410
|
spinner7.stop("Analysis complete.");
|
|
2120
1411
|
const groups = {
|
|
2121
1412
|
"new": [],
|
|
@@ -2164,9 +1455,9 @@ async function diff(cwd, localRepo) {
|
|
|
2164
1455
|
}
|
|
2165
1456
|
|
|
2166
1457
|
// src/gen.ts
|
|
2167
|
-
import { existsSync as
|
|
2168
|
-
import { readFile as
|
|
2169
|
-
import { join as
|
|
1458
|
+
import { existsSync as existsSync9 } from "fs";
|
|
1459
|
+
import { readFile as readFile6, writeFile, mkdir as mkdir3 } from "fs/promises";
|
|
1460
|
+
import { join as join9 } from "path";
|
|
2170
1461
|
import * as p9 from "@clack/prompts";
|
|
2171
1462
|
var FIELD_TYPES = ["string", "number", "boolean", "date", "datetime", "text", "json"];
|
|
2172
1463
|
function toPascal(s) {
|
|
@@ -2823,15 +2114,9 @@ async function resolvePrimaryBackend(cwd, hasFastapi, hasFastify, backendFlag) {
|
|
|
2823
2114
|
if (backendFlag) return backendFlag;
|
|
2824
2115
|
if (hasFastapi && !hasFastify) return "fastapi";
|
|
2825
2116
|
if (hasFastify && !hasFastapi) return "fastify";
|
|
2826
|
-
const
|
|
2827
|
-
if (
|
|
2828
|
-
|
|
2829
|
-
const data = JSON.parse(await readFile11(configPath, "utf-8"));
|
|
2830
|
-
if (data.primaryBackend === "fastapi" || data.primaryBackend === "fastify") {
|
|
2831
|
-
return data.primaryBackend;
|
|
2832
|
-
}
|
|
2833
|
-
} catch {
|
|
2834
|
-
}
|
|
2117
|
+
const config = await readProjxConfig(cwd);
|
|
2118
|
+
if (config.primaryBackend === "fastapi" || config.primaryBackend === "fastify") {
|
|
2119
|
+
return config.primaryBackend;
|
|
2835
2120
|
}
|
|
2836
2121
|
if (!process.stdin.isTTY) return "fastify";
|
|
2837
2122
|
const choice = await p9.select({
|
|
@@ -2843,23 +2128,17 @@ async function resolvePrimaryBackend(cwd, hasFastapi, hasFastify, backendFlag) {
|
|
|
2843
2128
|
initialValue: "fastify"
|
|
2844
2129
|
});
|
|
2845
2130
|
if (p9.isCancel(choice)) process.exit(0);
|
|
2846
|
-
|
|
2847
|
-
|
|
2848
|
-
data.primaryBackend = choice;
|
|
2849
|
-
await writeFile5(configPath, JSON.stringify(data, null, 2) + "\n");
|
|
2850
|
-
p9.log.success(`Saved primaryBackend: ${choice} to .projx`);
|
|
2851
|
-
} catch {
|
|
2852
|
-
}
|
|
2131
|
+
await writeProjxConfig(cwd, { ...config, primaryBackend: choice });
|
|
2132
|
+
p9.log.success(`Saved primaryBackend: ${choice} to .projx`);
|
|
2853
2133
|
return choice;
|
|
2854
2134
|
}
|
|
2855
2135
|
async function gen(cwd, entityName, fieldsFlag, backendFlag) {
|
|
2856
2136
|
p9.intro(`projx gen entity ${entityName}`);
|
|
2857
|
-
|
|
2858
|
-
if (!existsSync11(configPath)) {
|
|
2137
|
+
if (!existsSync9(join9(cwd, ".projx"))) {
|
|
2859
2138
|
p9.log.error("No .projx file found. Run 'npx create-projx init' first.");
|
|
2860
2139
|
process.exit(1);
|
|
2861
2140
|
}
|
|
2862
|
-
const projxData =
|
|
2141
|
+
const projxData = await readProjxConfig(cwd);
|
|
2863
2142
|
const pmName = projxData.packageManager ?? "npm";
|
|
2864
2143
|
const pm = pmCommands(pmName);
|
|
2865
2144
|
const { components: discovered, paths: componentPaths } = await discoverComponentsFromMarkers(cwd);
|
|
@@ -2896,37 +2175,37 @@ async function gen(cwd, entityName, fieldsFlag, backendFlag) {
|
|
|
2896
2175
|
const generated = [];
|
|
2897
2176
|
if (genFastapi) {
|
|
2898
2177
|
const dir = componentPaths.fastapi;
|
|
2899
|
-
const entityDir =
|
|
2900
|
-
if (
|
|
2178
|
+
const entityDir = join9(cwd, dir, "src/entities", toSnake(config.name));
|
|
2179
|
+
if (existsSync9(entityDir)) {
|
|
2901
2180
|
p9.log.warn(`${dir}/src/entities/${toSnake(config.name)}/ already exists. Skipping FastAPI.`);
|
|
2902
2181
|
} else {
|
|
2903
|
-
await
|
|
2904
|
-
await
|
|
2905
|
-
await
|
|
2182
|
+
await mkdir3(entityDir, { recursive: true });
|
|
2183
|
+
await writeFile(join9(entityDir, "_model.py"), generateFastAPIModel(config));
|
|
2184
|
+
await writeFile(join9(entityDir, "__init__.py"), "from ._model import *\n");
|
|
2906
2185
|
generated.push(`${dir}/src/entities/${toSnake(config.name)}/_model.py`);
|
|
2907
2186
|
generated.push(`${dir}/src/entities/${toSnake(config.name)}/__init__.py`);
|
|
2908
|
-
const testsDir =
|
|
2909
|
-
const testFile =
|
|
2910
|
-
if (
|
|
2911
|
-
await
|
|
2187
|
+
const testsDir = join9(cwd, dir, "tests");
|
|
2188
|
+
const testFile = join9(testsDir, `test_${toSnake(config.name)}_entity.py`);
|
|
2189
|
+
if (existsSync9(testsDir) && !existsSync9(testFile)) {
|
|
2190
|
+
await writeFile(testFile, generateFastapiTest(config));
|
|
2912
2191
|
generated.push(`${dir}/tests/test_${toSnake(config.name)}_entity.py`);
|
|
2913
2192
|
}
|
|
2914
2193
|
}
|
|
2915
2194
|
}
|
|
2916
2195
|
if (genFastify) {
|
|
2917
2196
|
const dir = componentPaths.fastify;
|
|
2918
|
-
const moduleDir =
|
|
2919
|
-
if (
|
|
2197
|
+
const moduleDir = join9(cwd, dir, "src/modules", toKebab(config.name));
|
|
2198
|
+
if (existsSync9(moduleDir)) {
|
|
2920
2199
|
p9.log.warn(`${dir}/src/modules/${toKebab(config.name)}/ already exists. Skipping Fastify.`);
|
|
2921
2200
|
} else {
|
|
2922
|
-
await
|
|
2923
|
-
await
|
|
2924
|
-
await
|
|
2201
|
+
await mkdir3(moduleDir, { recursive: true });
|
|
2202
|
+
await writeFile(join9(moduleDir, "schemas.ts"), generateFastifySchemas(config));
|
|
2203
|
+
await writeFile(join9(moduleDir, "index.ts"), generateFastifyIndex(config));
|
|
2925
2204
|
generated.push(`${dir}/src/modules/${toKebab(config.name)}/schemas.ts`);
|
|
2926
2205
|
generated.push(`${dir}/src/modules/${toKebab(config.name)}/index.ts`);
|
|
2927
|
-
const appPath =
|
|
2928
|
-
if (
|
|
2929
|
-
const appContent = await
|
|
2206
|
+
const appPath = join9(cwd, dir, "src/app.ts");
|
|
2207
|
+
if (existsSync9(appPath)) {
|
|
2208
|
+
const appContent = await readFile6(appPath, "utf-8");
|
|
2930
2209
|
const importLine = `import './modules/${toKebab(config.name)}/index.js';`;
|
|
2931
2210
|
if (!appContent.includes(importLine)) {
|
|
2932
2211
|
const updated = appContent.replace(
|
|
@@ -2935,62 +2214,62 @@ async function gen(cwd, entityName, fieldsFlag, backendFlag) {
|
|
|
2935
2214
|
`
|
|
2936
2215
|
);
|
|
2937
2216
|
if (updated !== appContent) {
|
|
2938
|
-
await
|
|
2217
|
+
await writeFile(appPath, updated);
|
|
2939
2218
|
generated.push(`${dir}/src/app.ts (import added)`);
|
|
2940
2219
|
}
|
|
2941
2220
|
}
|
|
2942
2221
|
}
|
|
2943
|
-
const prismaPath =
|
|
2944
|
-
if (
|
|
2945
|
-
const prismaContent = await
|
|
2222
|
+
const prismaPath = join9(cwd, dir, "prisma/schema.prisma");
|
|
2223
|
+
if (existsSync9(prismaPath)) {
|
|
2224
|
+
const prismaContent = await readFile6(prismaPath, "utf-8");
|
|
2946
2225
|
const modelName = `model ${toPascal(config.name)}`;
|
|
2947
2226
|
if (!prismaContent.includes(modelName)) {
|
|
2948
2227
|
const prismaModel = generatePrismaModel(config);
|
|
2949
|
-
await
|
|
2228
|
+
await writeFile(prismaPath, prismaContent.trimEnd() + "\n\n" + prismaModel + "\n");
|
|
2950
2229
|
generated.push(`${dir}/prisma/schema.prisma (model added)`);
|
|
2951
2230
|
}
|
|
2952
2231
|
}
|
|
2953
|
-
const testsModulesDir =
|
|
2954
|
-
const fastifyTestFile =
|
|
2955
|
-
if (
|
|
2956
|
-
await
|
|
2232
|
+
const testsModulesDir = join9(cwd, dir, "tests/modules");
|
|
2233
|
+
const fastifyTestFile = join9(testsModulesDir, `${toKebab(config.name)}.test.ts`);
|
|
2234
|
+
if (existsSync9(testsModulesDir) && !existsSync9(fastifyTestFile)) {
|
|
2235
|
+
await writeFile(fastifyTestFile, generateFastifyTest(config));
|
|
2957
2236
|
generated.push(`${dir}/tests/modules/${toKebab(config.name)}.test.ts`);
|
|
2958
2237
|
}
|
|
2959
2238
|
}
|
|
2960
2239
|
}
|
|
2961
2240
|
if (hasFrontend) {
|
|
2962
2241
|
const dir = componentPaths.frontend;
|
|
2963
|
-
const typesDir =
|
|
2242
|
+
const typesDir = join9(cwd, dir, "src/types");
|
|
2964
2243
|
const fileName = toKebab(config.name) + ".ts";
|
|
2965
|
-
const filePath =
|
|
2966
|
-
if (
|
|
2244
|
+
const filePath = join9(typesDir, fileName);
|
|
2245
|
+
if (existsSync9(filePath)) {
|
|
2967
2246
|
p9.log.warn(`${dir}/src/types/${fileName} already exists. Skipping frontend types.`);
|
|
2968
2247
|
} else {
|
|
2969
|
-
await
|
|
2970
|
-
await
|
|
2248
|
+
await mkdir3(typesDir, { recursive: true });
|
|
2249
|
+
await writeFile(filePath, generateFrontendInterface(config));
|
|
2971
2250
|
generated.push(`${dir}/src/types/${fileName}`);
|
|
2972
|
-
const barrelPath =
|
|
2251
|
+
const barrelPath = join9(typesDir, "index.ts");
|
|
2973
2252
|
const exportLine = `export * from './${toKebab(config.name)}';`;
|
|
2974
|
-
if (
|
|
2975
|
-
const content = await
|
|
2253
|
+
if (existsSync9(barrelPath)) {
|
|
2254
|
+
const content = await readFile6(barrelPath, "utf-8");
|
|
2976
2255
|
if (!content.includes(exportLine)) {
|
|
2977
|
-
await
|
|
2256
|
+
await writeFile(barrelPath, content.trimEnd() + "\n" + exportLine + "\n");
|
|
2978
2257
|
}
|
|
2979
2258
|
} else {
|
|
2980
|
-
await
|
|
2259
|
+
await writeFile(barrelPath, exportLine + "\n");
|
|
2981
2260
|
}
|
|
2982
2261
|
generated.push(`${dir}/src/types/index.ts`);
|
|
2983
2262
|
}
|
|
2984
2263
|
}
|
|
2985
2264
|
if (hasMobile) {
|
|
2986
2265
|
const dir = componentPaths.mobile;
|
|
2987
|
-
const entityDir =
|
|
2988
|
-
const modelPath =
|
|
2989
|
-
if (
|
|
2266
|
+
const entityDir = join9(cwd, dir, "lib/entities", toSnake(config.name));
|
|
2267
|
+
const modelPath = join9(entityDir, "model.dart");
|
|
2268
|
+
if (existsSync9(modelPath)) {
|
|
2990
2269
|
p9.log.warn(`${dir}/lib/entities/${toSnake(config.name)}/model.dart already exists. Skipping mobile model.`);
|
|
2991
2270
|
} else {
|
|
2992
|
-
await
|
|
2993
|
-
await
|
|
2271
|
+
await mkdir3(entityDir, { recursive: true });
|
|
2272
|
+
await writeFile(modelPath, generateDartModel(config));
|
|
2994
2273
|
generated.push(`${dir}/lib/entities/${toSnake(config.name)}/model.dart`);
|
|
2995
2274
|
}
|
|
2996
2275
|
}
|
|
@@ -3030,9 +2309,9 @@ async function gen(cwd, entityName, fieldsFlag, backendFlag) {
|
|
|
3030
2309
|
}
|
|
3031
2310
|
|
|
3032
2311
|
// src/sync.ts
|
|
3033
|
-
import { existsSync as
|
|
3034
|
-
import { writeFile as
|
|
3035
|
-
import { join as
|
|
2312
|
+
import { existsSync as existsSync10, readFileSync } from "fs";
|
|
2313
|
+
import { writeFile as writeFile2, mkdir as mkdir4 } from "fs/promises";
|
|
2314
|
+
import { join as join10 } from "path";
|
|
3036
2315
|
import * as p10 from "@clack/prompts";
|
|
3037
2316
|
function toPascal2(s) {
|
|
3038
2317
|
return s.replace(/(?:^|[_\-\s])([a-zA-Z])/g, (_, c) => c.toUpperCase());
|
|
@@ -3203,8 +2482,8 @@ function generateDartModel2(entity) {
|
|
|
3203
2482
|
}
|
|
3204
2483
|
async function sync(cwd, url) {
|
|
3205
2484
|
p10.intro("projx sync");
|
|
3206
|
-
const configPath =
|
|
3207
|
-
if (!
|
|
2485
|
+
const configPath = join10(cwd, ".projx");
|
|
2486
|
+
if (!existsSync10(configPath)) {
|
|
3208
2487
|
p10.log.error("No .projx file found. Run 'npx create-projx init' first.");
|
|
3209
2488
|
process.exit(1);
|
|
3210
2489
|
}
|
|
@@ -3236,18 +2515,18 @@ async function sync(cwd, url) {
|
|
|
3236
2515
|
const generated = [];
|
|
3237
2516
|
if (hasFrontend) {
|
|
3238
2517
|
const dir = componentPaths.frontend;
|
|
3239
|
-
const typesDir =
|
|
3240
|
-
await
|
|
2518
|
+
const typesDir = join10(cwd, dir, "src/types");
|
|
2519
|
+
await mkdir4(typesDir, { recursive: true });
|
|
3241
2520
|
const barrelExports = [];
|
|
3242
2521
|
for (const entity of meta.entities) {
|
|
3243
2522
|
const fileName = toKebab(toSnake(entity.name)) + ".ts";
|
|
3244
|
-
const filePath =
|
|
3245
|
-
await
|
|
2523
|
+
const filePath = join10(typesDir, fileName);
|
|
2524
|
+
await writeFile2(filePath, generateTsInterface(entity));
|
|
3246
2525
|
generated.push(`${dir}/src/types/${fileName}`);
|
|
3247
2526
|
barrelExports.push(`export * from './${toKebab(toSnake(entity.name))}';`);
|
|
3248
2527
|
}
|
|
3249
|
-
await
|
|
3250
|
-
|
|
2528
|
+
await writeFile2(
|
|
2529
|
+
join10(typesDir, "index.ts"),
|
|
3251
2530
|
barrelExports.join("\n") + "\n"
|
|
3252
2531
|
);
|
|
3253
2532
|
generated.push(`${dir}/src/types/index.ts`);
|
|
@@ -3255,10 +2534,10 @@ async function sync(cwd, url) {
|
|
|
3255
2534
|
if (hasMobile) {
|
|
3256
2535
|
const dir = componentPaths.mobile;
|
|
3257
2536
|
for (const entity of meta.entities) {
|
|
3258
|
-
const entityDir =
|
|
3259
|
-
await
|
|
3260
|
-
const modelPath =
|
|
3261
|
-
await
|
|
2537
|
+
const entityDir = join10(cwd, dir, "lib/entities", toSnake(entity.name));
|
|
2538
|
+
await mkdir4(entityDir, { recursive: true });
|
|
2539
|
+
const modelPath = join10(entityDir, "model.dart");
|
|
2540
|
+
await writeFile2(modelPath, generateDartModel2(entity));
|
|
3262
2541
|
generated.push(`${dir}/lib/entities/${toSnake(entity.name)}/model.dart`);
|
|
3263
2542
|
}
|
|
3264
2543
|
}
|
|
@@ -3281,10 +2560,10 @@ async function sync(cwd, url) {
|
|
|
3281
2560
|
function detectMetaUrl(cwd) {
|
|
3282
2561
|
const envFiles = [".env", ".env.dev", ".env.local"];
|
|
3283
2562
|
for (const envFile of envFiles) {
|
|
3284
|
-
const envPath =
|
|
3285
|
-
if (
|
|
2563
|
+
const envPath = join10(cwd, envFile);
|
|
2564
|
+
if (existsSync10(envPath)) {
|
|
3286
2565
|
try {
|
|
3287
|
-
const content =
|
|
2566
|
+
const content = readFileSync(envPath, "utf-8");
|
|
3288
2567
|
const match = content.match(/VITE_API_URL\s*=\s*(.+)/);
|
|
3289
2568
|
if (match) {
|
|
3290
2569
|
const base = match[1].trim().replace(/["']/g, "");
|
|
@@ -3300,10 +2579,10 @@ function detectMetaUrl(cwd) {
|
|
|
3300
2579
|
"frontend/.env.dev"
|
|
3301
2580
|
];
|
|
3302
2581
|
for (const envFile of frontendEnvFiles) {
|
|
3303
|
-
const envPath =
|
|
3304
|
-
if (
|
|
2582
|
+
const envPath = join10(cwd, envFile);
|
|
2583
|
+
if (existsSync10(envPath)) {
|
|
3305
2584
|
try {
|
|
3306
|
-
const content =
|
|
2585
|
+
const content = readFileSync(envPath, "utf-8");
|
|
3307
2586
|
const match = content.match(/VITE_API_URL\s*=\s*(.+)/);
|
|
3308
2587
|
if (match) {
|
|
3309
2588
|
const base = match[1].trim().replace(/["']/g, "");
|
|
@@ -3373,7 +2652,7 @@ function parseArgs() {
|
|
|
3373
2652
|
continue;
|
|
3374
2653
|
}
|
|
3375
2654
|
if (arg === "--local") {
|
|
3376
|
-
localRepo =
|
|
2655
|
+
localRepo = resolve(args[++i] || ".");
|
|
3377
2656
|
continue;
|
|
3378
2657
|
}
|
|
3379
2658
|
if (arg === "--no-git") {
|
|
@@ -3545,8 +2824,8 @@ async function main() {
|
|
|
3545
2824
|
opts.git = options.git ?? opts.git;
|
|
3546
2825
|
opts.install = options.install ?? opts.install;
|
|
3547
2826
|
}
|
|
3548
|
-
const dest =
|
|
3549
|
-
if (
|
|
2827
|
+
const dest = resolve(process.cwd(), opts.name);
|
|
2828
|
+
if (existsSync11(dest)) {
|
|
3550
2829
|
console.error(`Error: ${dest} already exists.`);
|
|
3551
2830
|
process.exit(1);
|
|
3552
2831
|
}
|