create-projx 1.5.5 → 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 -1185
- 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,447 +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
|
-
async function writeManagedProjx(cwd, version, vars, components) {
|
|
505
|
-
const projxPath = join3(cwd, ".projx");
|
|
506
|
-
let existing = {};
|
|
507
|
-
if (existsSync2(projxPath)) {
|
|
508
|
-
try {
|
|
509
|
-
existing = JSON.parse(await readFile3(projxPath, "utf-8"));
|
|
510
|
-
} catch (err) {
|
|
511
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
512
|
-
console.warn(`projx: existing .projx is unreadable (${msg}); writing fresh.`);
|
|
513
|
-
}
|
|
514
|
-
}
|
|
515
|
-
const merged = {
|
|
516
|
-
...existing,
|
|
517
|
-
version
|
|
518
|
-
};
|
|
519
|
-
if (components !== void 0) {
|
|
520
|
-
merged.components = components;
|
|
521
|
-
}
|
|
522
|
-
if (typeof merged.createdAt !== "string") {
|
|
523
|
-
merged.createdAt = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
524
|
-
}
|
|
525
|
-
const pmObj = vars.pm;
|
|
526
|
-
if (pmObj?.name && !merged.packageManager) {
|
|
527
|
-
merged.packageManager = pmObj.name;
|
|
528
|
-
}
|
|
529
|
-
await writeFile2(projxPath, JSON.stringify(merged, null, 2) + "\n");
|
|
530
|
-
}
|
|
531
|
-
function matchesSkip(filePath, patterns) {
|
|
532
|
-
for (const pattern of patterns) {
|
|
533
|
-
if (pattern === "**") return true;
|
|
534
|
-
if (pattern.endsWith("/**")) {
|
|
535
|
-
const prefix = pattern.slice(0, -3);
|
|
536
|
-
if (filePath.startsWith(prefix + "/") || filePath === prefix) return true;
|
|
537
|
-
}
|
|
538
|
-
if (pattern.startsWith("**/")) {
|
|
539
|
-
const suffix = pattern.slice(3);
|
|
540
|
-
if (suffix.startsWith("*.")) {
|
|
541
|
-
const ext = suffix.slice(1);
|
|
542
|
-
if (filePath.endsWith(ext)) return true;
|
|
543
|
-
} else if (filePath.endsWith(suffix) || filePath.includes("/" + suffix)) {
|
|
544
|
-
return true;
|
|
545
|
-
}
|
|
546
|
-
}
|
|
547
|
-
if (pattern.startsWith("*.")) {
|
|
548
|
-
const ext = pattern.slice(1);
|
|
549
|
-
if (filePath.endsWith(ext)) return true;
|
|
550
|
-
}
|
|
551
|
-
if (filePath === pattern) return true;
|
|
552
|
-
}
|
|
553
|
-
return false;
|
|
554
|
-
}
|
|
555
|
-
function saveBaselineRef(cwd) {
|
|
556
|
-
try {
|
|
557
|
-
const head = execSync2("git rev-parse HEAD", { cwd, stdio: "pipe" }).toString().trim();
|
|
558
|
-
execSync2(`git update-ref ${BASELINE_REF} ${head}`, { cwd, stdio: "pipe" });
|
|
559
|
-
} catch {
|
|
560
|
-
}
|
|
561
|
-
}
|
|
562
|
-
function getBaselineRef(cwd) {
|
|
563
|
-
try {
|
|
564
|
-
return execSync2(`git rev-parse --verify ${BASELINE_REF}`, { cwd, stdio: "pipe" }).toString().trim();
|
|
565
|
-
} catch {
|
|
566
|
-
}
|
|
567
|
-
try {
|
|
568
|
-
const sha = execSync2("git log -1 --format=%H -- .projx", { cwd, stdio: "pipe" }).toString().trim();
|
|
569
|
-
if (sha) return sha;
|
|
570
|
-
} catch {
|
|
571
|
-
}
|
|
572
|
-
return null;
|
|
573
|
-
}
|
|
574
|
-
function getFileAtRef(cwd, ref, filePath) {
|
|
575
|
-
try {
|
|
576
|
-
return execSync2(`git show ${ref}:"${filePath}"`, { cwd, stdio: "pipe" }).toString();
|
|
577
|
-
} catch {
|
|
578
|
-
return null;
|
|
579
|
-
}
|
|
580
|
-
}
|
|
581
|
-
function mergeFileThreeWay(oursPath, baseContent, theirsContent) {
|
|
582
|
-
const id = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
583
|
-
const baseTmp = join3(tmpdir2(), `projx-base-${id}`);
|
|
584
|
-
const theirsTmp = join3(tmpdir2(), `projx-theirs-${id}`);
|
|
585
|
-
try {
|
|
586
|
-
writeFileSync(baseTmp, baseContent);
|
|
587
|
-
writeFileSync(theirsTmp, theirsContent);
|
|
588
|
-
execSync2(
|
|
589
|
-
`git merge-file -L "your changes" -L "previous projx baseline" -L "new projx template" "${oursPath}" "${baseTmp}" "${theirsTmp}"`,
|
|
590
|
-
{ stdio: "pipe" }
|
|
591
|
-
);
|
|
592
|
-
return true;
|
|
593
|
-
} catch {
|
|
594
|
-
return false;
|
|
595
|
-
} finally {
|
|
596
|
-
try {
|
|
597
|
-
unlinkSync(baseTmp);
|
|
598
|
-
} catch {
|
|
599
|
-
}
|
|
600
|
-
try {
|
|
601
|
-
unlinkSync(theirsTmp);
|
|
602
|
-
} catch {
|
|
603
|
-
}
|
|
604
|
-
}
|
|
605
|
-
}
|
|
606
|
-
async function collectAllFiles(dir, base) {
|
|
607
|
-
const { readdir: readdir4 } = await import("fs/promises");
|
|
608
|
-
const results = [];
|
|
609
|
-
const walk = async (current) => {
|
|
610
|
-
const entries = await readdir4(current, { withFileTypes: true });
|
|
611
|
-
for (const entry of entries) {
|
|
612
|
-
const full = join3(current, entry.name);
|
|
613
|
-
if (entry.isDirectory()) {
|
|
614
|
-
await walk(full);
|
|
615
|
-
} else {
|
|
616
|
-
results.push(full.slice(base.length + 1));
|
|
617
|
-
}
|
|
618
|
-
}
|
|
619
|
-
};
|
|
620
|
-
await walk(dir);
|
|
621
|
-
return results;
|
|
622
|
-
}
|
|
623
|
-
async function tryThreeWayMerge(cwd, templateDir, baselineRef) {
|
|
624
|
-
const templateFiles = await collectAllFiles(templateDir, templateDir);
|
|
625
|
-
const merged = [];
|
|
626
|
-
const conflicted = [];
|
|
627
|
-
for (const file of templateFiles) {
|
|
628
|
-
if (file === ".projx") continue;
|
|
629
|
-
const oursPath = join3(cwd, file);
|
|
630
|
-
if (!existsSync2(oursPath)) continue;
|
|
631
|
-
const baseContent = getFileAtRef(cwd, baselineRef, file);
|
|
632
|
-
if (baseContent === null) continue;
|
|
633
|
-
let theirsContent;
|
|
634
|
-
try {
|
|
635
|
-
theirsContent = await readFile3(join3(templateDir, file), "utf-8");
|
|
636
|
-
} catch {
|
|
637
|
-
continue;
|
|
638
|
-
}
|
|
639
|
-
const oursContent = await readFile3(oursPath, "utf-8");
|
|
640
|
-
if (theirsContent === baseContent) continue;
|
|
641
|
-
if (oursContent === baseContent) {
|
|
642
|
-
await writeFile2(oursPath, theirsContent);
|
|
643
|
-
merged.push(file);
|
|
644
|
-
continue;
|
|
645
|
-
}
|
|
646
|
-
if (oursContent === theirsContent) continue;
|
|
647
|
-
const clean = mergeFileThreeWay(oursPath, baseContent, theirsContent);
|
|
648
|
-
if (clean) {
|
|
649
|
-
merged.push(file);
|
|
650
|
-
} else {
|
|
651
|
-
conflicted.push(file);
|
|
652
|
-
}
|
|
653
|
-
}
|
|
654
|
-
return { merged, conflicted };
|
|
655
|
-
}
|
|
656
|
-
function createOrphanWorktree(cwd) {
|
|
657
|
-
const id = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
658
|
-
const branch = `projx/tmp-${id}`;
|
|
659
|
-
const worktree = join3(tmpdir2(), `projx-wt-${id}`);
|
|
660
|
-
try {
|
|
661
|
-
execSync2("git worktree prune", { cwd, stdio: "pipe" });
|
|
662
|
-
} catch {
|
|
663
|
-
}
|
|
664
|
-
execSync2(`git worktree add --orphan -b ${branch} "${worktree}"`, {
|
|
665
|
-
cwd,
|
|
666
|
-
stdio: "pipe"
|
|
667
|
-
});
|
|
668
|
-
return { worktree, branch };
|
|
669
|
-
}
|
|
670
|
-
function cleanupWorktree(cwd, worktree, branch) {
|
|
671
|
-
try {
|
|
672
|
-
execSync2(`git worktree remove "${worktree}" --force`, { cwd, stdio: "pipe" });
|
|
673
|
-
} catch {
|
|
674
|
-
try {
|
|
675
|
-
rm2(worktree, { recursive: true, force: true });
|
|
676
|
-
execSync2("git worktree prune", { cwd, stdio: "pipe" });
|
|
677
|
-
} catch {
|
|
678
|
-
}
|
|
679
|
-
}
|
|
680
|
-
try {
|
|
681
|
-
execSync2(`git branch -D ${branch}`, { cwd, stdio: "pipe" });
|
|
682
|
-
} catch {
|
|
683
|
-
}
|
|
684
|
-
}
|
|
685
|
-
async function removeSkippedFiles(dir, skipPatterns) {
|
|
686
|
-
if (skipPatterns.length === 0) return;
|
|
687
|
-
const { readdir: readdir4, unlink: unlink2 } = await import("fs/promises");
|
|
688
|
-
const walk = async (current, base) => {
|
|
689
|
-
const entries = await readdir4(current, { withFileTypes: true });
|
|
690
|
-
for (const entry of entries) {
|
|
691
|
-
const full = join3(current, entry.name);
|
|
692
|
-
const rel = full.slice(base.length + 1);
|
|
693
|
-
if (entry.isDirectory()) {
|
|
694
|
-
await walk(full, base);
|
|
695
|
-
} else if (entry.name !== ".projx-component" && matchesSkip(rel, skipPatterns)) {
|
|
696
|
-
await unlink2(full);
|
|
697
|
-
}
|
|
698
|
-
}
|
|
699
|
-
};
|
|
700
|
-
await walk(dir, dir);
|
|
701
|
-
}
|
|
702
|
-
async function writeTemplateToDir(dest, repoDir, components, componentPaths, vars, version, origin, componentSkips, rootSkip) {
|
|
703
|
-
const name = vars.projectName;
|
|
704
|
-
const nameSnake = toSnake(name);
|
|
705
|
-
for (const component of components) {
|
|
706
|
-
const targetDir = componentPaths[component];
|
|
707
|
-
const skipPatterns = componentSkips?.[component] ?? [];
|
|
708
|
-
const tmpDir = join3(dest, "__cptmp__");
|
|
709
|
-
await copyComponent(repoDir, component, tmpDir);
|
|
710
|
-
const srcDir = join3(tmpDir, component);
|
|
711
|
-
if (skipPatterns.length > 0) {
|
|
712
|
-
await removeSkippedFiles(srcDir, skipPatterns);
|
|
713
|
-
}
|
|
714
|
-
const outDir = join3(dest, targetDir);
|
|
715
|
-
await mkdir2(outDir, { recursive: true });
|
|
716
|
-
const { cp: cp2 } = await import("fs/promises");
|
|
717
|
-
if (existsSync2(srcDir)) {
|
|
718
|
-
await cp2(srcDir, outDir, { recursive: true, force: true });
|
|
719
|
-
}
|
|
720
|
-
await rm2(tmpDir, { recursive: true, force: true });
|
|
721
|
-
await writeComponentMarker(join3(dest, targetDir), component, origin, skipPatterns.length > 0 ? skipPatterns : void 0);
|
|
722
|
-
}
|
|
723
|
-
await substituteNames(dest, components, componentPaths, name, nameSnake);
|
|
724
|
-
const hasBackend = components.includes("fastapi") || components.includes("fastify");
|
|
725
|
-
const skip = rootSkip ?? [];
|
|
726
|
-
const shouldWrite = (file) => !matchesSkip(file, skip);
|
|
727
|
-
if (hasBackend || components.includes("frontend")) {
|
|
728
|
-
if (shouldWrite("docker-compose.yml"))
|
|
729
|
-
await writeFile2(join3(dest, "docker-compose.yml"), await generateDockerCompose(vars));
|
|
730
|
-
if (shouldWrite("docker-compose.dev.yml"))
|
|
731
|
-
await writeFile2(join3(dest, "docker-compose.dev.yml"), await generateDockerComposeDev(vars));
|
|
732
|
-
}
|
|
733
|
-
if (shouldWrite("README.md"))
|
|
734
|
-
await writeFile2(join3(dest, "README.md"), await generateReadme(vars));
|
|
735
|
-
if (shouldWrite(".githooks/pre-commit")) {
|
|
736
|
-
await mkdir2(join3(dest, ".githooks"), { recursive: true });
|
|
737
|
-
await writeFile2(join3(dest, ".githooks/pre-commit"), await generatePreCommit(vars));
|
|
738
|
-
await chmod(join3(dest, ".githooks/pre-commit"), 493);
|
|
739
|
-
}
|
|
740
|
-
if (shouldWrite(".github/workflows/ci.yml")) {
|
|
741
|
-
await mkdir2(join3(dest, ".github/workflows"), { recursive: true });
|
|
742
|
-
await writeFile2(join3(dest, ".github/workflows/ci.yml"), await generateCiYml(vars));
|
|
743
|
-
}
|
|
744
|
-
if (shouldWrite("setup.sh")) {
|
|
745
|
-
await writeFile2(join3(dest, "setup.sh"), await generateSetupSh(vars));
|
|
746
|
-
await chmod(join3(dest, "setup.sh"), 493);
|
|
747
|
-
}
|
|
748
|
-
await copyStaticFiles(repoDir, dest);
|
|
749
|
-
if (shouldWrite(".vscode/settings.json")) {
|
|
750
|
-
await mkdir2(join3(dest, ".vscode"), { recursive: true });
|
|
751
|
-
await writeFile2(join3(dest, ".vscode/settings.json"), generateVscodeSettings(vars));
|
|
752
|
-
}
|
|
753
|
-
await writeManagedProjx(dest, version, vars, components);
|
|
754
|
-
}
|
|
755
|
-
async function substituteNames(dest, components, paths, name, nameSnake) {
|
|
756
|
-
if (components.includes("fastapi")) {
|
|
757
|
-
await replaceInFile(join3(dest, `${paths.fastapi}/pyproject.toml`), "projx-fastapi", `${name}-fastapi`);
|
|
758
|
-
}
|
|
759
|
-
if (components.includes("fastify")) {
|
|
760
|
-
await replaceInFile(join3(dest, `${paths.fastify}/package.json`), "projx-fastify", `${name}-fastify`);
|
|
761
|
-
}
|
|
762
|
-
if (components.includes("frontend")) {
|
|
763
|
-
await replaceInFile(join3(dest, `${paths.frontend}/package.json`), "projx-frontend", `${name}-frontend`);
|
|
764
|
-
}
|
|
765
|
-
if (components.includes("e2e")) {
|
|
766
|
-
await replaceInFile(join3(dest, `${paths.e2e}/package.json`), "projx-e2e", `${name}-e2e`);
|
|
767
|
-
}
|
|
768
|
-
if (components.includes("mobile")) {
|
|
769
|
-
await replaceInFile(join3(dest, `${paths.mobile}/pubspec.yaml`), "projx_mobile", `${nameSnake}_mobile`);
|
|
770
|
-
await replaceInDir(join3(dest, `${paths.mobile}`), "package:projx_mobile/", `package:${nameSnake}_mobile/`, ".dart");
|
|
771
|
-
}
|
|
772
|
-
}
|
|
773
|
-
async function applyTemplate(cwd, repoDir, components, componentPaths, vars, version, origin = "scaffold", componentSkips, rootSkip) {
|
|
774
|
-
const hasHead = (() => {
|
|
775
|
-
try {
|
|
776
|
-
execSync2("git rev-parse HEAD", { cwd, stdio: "pipe" });
|
|
777
|
-
return true;
|
|
778
|
-
} catch {
|
|
779
|
-
return false;
|
|
780
|
-
}
|
|
781
|
-
})();
|
|
782
|
-
if (!hasHead) {
|
|
783
|
-
await writeTemplateToDir(cwd, repoDir, components, componentPaths, vars, version, origin, componentSkips, rootSkip);
|
|
784
|
-
return { status: "clean" };
|
|
785
|
-
}
|
|
786
|
-
const { worktree, branch } = createOrphanWorktree(cwd);
|
|
787
|
-
try {
|
|
788
|
-
await writeTemplateToDir(worktree, repoDir, components, componentPaths, vars, version, origin, componentSkips, rootSkip);
|
|
789
|
-
execSync2("git add -A", { cwd: worktree, stdio: "pipe" });
|
|
790
|
-
const diff2 = execSync2("git diff --cached --stat", { cwd: worktree, stdio: "pipe" }).toString().trim();
|
|
791
|
-
if (!diff2) {
|
|
792
|
-
cleanupWorktree(cwd, worktree, branch);
|
|
793
|
-
return { status: "clean" };
|
|
794
|
-
}
|
|
795
|
-
execSync2(
|
|
796
|
-
`git commit --no-verify -m "projx: template v${version} [${components.join(", ")}]"`,
|
|
797
|
-
{ cwd: worktree, stdio: "pipe" }
|
|
798
|
-
);
|
|
799
|
-
try {
|
|
800
|
-
execSync2(`git worktree remove "${worktree}" --force`, { cwd, stdio: "pipe" });
|
|
801
|
-
} catch {
|
|
802
|
-
try {
|
|
803
|
-
await rm2(worktree, { recursive: true, force: true });
|
|
804
|
-
execSync2("git worktree prune", { cwd, stdio: "pipe" });
|
|
805
|
-
} catch {
|
|
806
|
-
}
|
|
807
|
-
}
|
|
808
|
-
let mergeClean = false;
|
|
809
|
-
try {
|
|
810
|
-
execSync2(
|
|
811
|
-
`git merge ${branch} --allow-unrelated-histories -m "projx: update to template v${version}"`,
|
|
812
|
-
{ cwd, stdio: "pipe" }
|
|
813
|
-
);
|
|
814
|
-
mergeClean = true;
|
|
815
|
-
} catch {
|
|
816
|
-
try {
|
|
817
|
-
execSync2("git merge --abort", { cwd, stdio: "pipe" });
|
|
818
|
-
} catch {
|
|
819
|
-
}
|
|
820
|
-
}
|
|
821
|
-
try {
|
|
822
|
-
execSync2(`git branch -D ${branch}`, { cwd, stdio: "pipe" });
|
|
823
|
-
} catch {
|
|
824
|
-
}
|
|
825
|
-
if (mergeClean) {
|
|
826
|
-
saveBaselineRef(cwd);
|
|
827
|
-
return { status: "clean" };
|
|
828
|
-
}
|
|
829
|
-
const baselineRef = getBaselineRef(cwd);
|
|
830
|
-
if (baselineRef) {
|
|
831
|
-
const tmpTemplate = join3(tmpdir2(), `projx-tpl-${Date.now()}`);
|
|
832
|
-
await mkdir2(tmpTemplate, { recursive: true });
|
|
833
|
-
await writeTemplateToDir(tmpTemplate, repoDir, components, componentPaths, vars, version, origin, componentSkips, rootSkip);
|
|
834
|
-
const result = await tryThreeWayMerge(cwd, tmpTemplate, baselineRef);
|
|
835
|
-
await rm2(tmpTemplate, { recursive: true, force: true });
|
|
836
|
-
if (result.conflicted.length === 0) {
|
|
837
|
-
await writeManagedProjx(cwd, version, vars);
|
|
838
|
-
execSync2("git add -A", { cwd, stdio: "pipe" });
|
|
839
|
-
const staged = execSync2("git diff --cached --stat", { cwd, stdio: "pipe" }).toString().trim();
|
|
840
|
-
if (staged) {
|
|
841
|
-
execSync2(
|
|
842
|
-
`git commit --no-verify -m "projx: update to template v${version} (3-way merge)"`,
|
|
843
|
-
{ cwd, stdio: "pipe" }
|
|
844
|
-
);
|
|
845
|
-
}
|
|
846
|
-
saveBaselineRef(cwd);
|
|
847
|
-
return result.merged.length > 0 ? { status: "merged", mergedFiles: result.merged } : { status: "clean" };
|
|
848
|
-
}
|
|
849
|
-
await writeManagedProjx(cwd, version, vars);
|
|
850
|
-
for (const f of result.merged) {
|
|
851
|
-
try {
|
|
852
|
-
execSync2(`git add "${f}"`, { cwd, stdio: "pipe" });
|
|
853
|
-
} catch {
|
|
854
|
-
}
|
|
855
|
-
}
|
|
856
|
-
execSync2("git add .projx", { cwd, stdio: "pipe" });
|
|
857
|
-
return {
|
|
858
|
-
status: "conflicts",
|
|
859
|
-
mergedFiles: result.merged,
|
|
860
|
-
conflictedFiles: result.conflicted
|
|
861
|
-
};
|
|
862
|
-
}
|
|
863
|
-
await writeTemplateToDir(cwd, repoDir, components, componentPaths, vars, version, origin, componentSkips, rootSkip);
|
|
864
|
-
return { status: "conflicts" };
|
|
865
|
-
} catch (err) {
|
|
866
|
-
cleanupWorktree(cwd, worktree, branch);
|
|
867
|
-
throw err;
|
|
868
|
-
}
|
|
869
|
-
}
|
|
870
|
-
|
|
871
|
-
// src/scaffold.ts
|
|
872
98
|
async function scaffold(opts, dest, localRepo) {
|
|
873
99
|
const name = toKebab(opts.name);
|
|
874
100
|
const pm = opts.packageManager ?? "npm";
|
|
@@ -877,7 +103,7 @@ async function scaffold(opts, dest, localRepo) {
|
|
|
877
103
|
);
|
|
878
104
|
const vars = { projectName: name, components: opts.components, paths, pm: pmCommands(pm) };
|
|
879
105
|
const isLocal = !!localRepo;
|
|
880
|
-
await
|
|
106
|
+
await mkdir(dest, { recursive: true });
|
|
881
107
|
const dlSpinner = p2.spinner();
|
|
882
108
|
dlSpinner.start(isLocal ? "Using local templates" : "Downloading latest templates");
|
|
883
109
|
const repoDir = await downloadRepo(localRepo).catch((err) => {
|
|
@@ -887,16 +113,15 @@ async function scaffold(opts, dest, localRepo) {
|
|
|
887
113
|
});
|
|
888
114
|
dlSpinner.stop(isLocal ? "Local templates loaded." : "Templates downloaded.");
|
|
889
115
|
try {
|
|
890
|
-
const pkg = JSON.parse(await
|
|
116
|
+
const pkg = JSON.parse(await readFile(join(repoDir, "cli/package.json"), "utf-8"));
|
|
891
117
|
const version = pkg.version;
|
|
892
118
|
p2.log.info(`Scaffolding project in ${dest}`);
|
|
893
119
|
if (opts.git) {
|
|
894
120
|
exec("git init", dest);
|
|
895
|
-
exec("git config core.hooksPath .githooks", dest);
|
|
896
121
|
}
|
|
897
122
|
const spinner7 = p2.spinner();
|
|
898
123
|
spinner7.start("Scaffolding project");
|
|
899
|
-
await applyTemplate(dest, repoDir, opts.components, paths, vars, version);
|
|
124
|
+
await applyTemplate(dest, repoDir, opts.components, paths, vars, version, void 0, void 0, true);
|
|
900
125
|
spinner7.stop("Scaffold complete.");
|
|
901
126
|
if (opts.install) {
|
|
902
127
|
await installDeps(dest, opts.components, pm);
|
|
@@ -905,7 +130,8 @@ async function scaffold(opts, dest, localRepo) {
|
|
|
905
130
|
if (opts.git) {
|
|
906
131
|
try {
|
|
907
132
|
exec("git add -A", dest);
|
|
908
|
-
exec('git commit
|
|
133
|
+
exec('git commit -m "Initial scaffold from projx"', dest);
|
|
134
|
+
exec("git config core.hooksPath .githooks", dest);
|
|
909
135
|
saveBaselineRef(dest);
|
|
910
136
|
} catch {
|
|
911
137
|
}
|
|
@@ -930,7 +156,7 @@ async function installDeps(dest, components, pm) {
|
|
|
930
156
|
case "fastapi":
|
|
931
157
|
if (hasCommand("uv")) {
|
|
932
158
|
spinner7.start("Installing FastAPI dependencies (uv sync)");
|
|
933
|
-
exec("uv sync --all-extras",
|
|
159
|
+
exec("uv sync --all-extras", join(dest, "fastapi"));
|
|
934
160
|
spinner7.stop("FastAPI dependencies installed.");
|
|
935
161
|
} else {
|
|
936
162
|
p2.log.warn("uv not found \u2014 run 'cd fastapi && uv sync' manually.");
|
|
@@ -939,7 +165,7 @@ async function installDeps(dest, components, pm) {
|
|
|
939
165
|
case "fastify":
|
|
940
166
|
if (hasCommand(pmBin)) {
|
|
941
167
|
spinner7.start(`Installing Fastify dependencies (${cmds.install})`);
|
|
942
|
-
exec(cmds.install,
|
|
168
|
+
exec(cmds.install, join(dest, "fastify"));
|
|
943
169
|
spinner7.stop("Fastify dependencies installed.");
|
|
944
170
|
} else {
|
|
945
171
|
p2.log.warn(`${pm} not found \u2014 run 'cd fastify && ${cmds.install}' manually.`);
|
|
@@ -948,7 +174,7 @@ async function installDeps(dest, components, pm) {
|
|
|
948
174
|
case "frontend":
|
|
949
175
|
if (hasCommand(pmBin)) {
|
|
950
176
|
spinner7.start(`Installing Frontend dependencies (${cmds.install})`);
|
|
951
|
-
exec(cmds.install,
|
|
177
|
+
exec(cmds.install, join(dest, "frontend"));
|
|
952
178
|
spinner7.stop("Frontend dependencies installed.");
|
|
953
179
|
} else {
|
|
954
180
|
p2.log.warn(`${pm} not found \u2014 run 'cd frontend && ${cmds.install}' manually.`);
|
|
@@ -957,7 +183,7 @@ async function installDeps(dest, components, pm) {
|
|
|
957
183
|
case "e2e":
|
|
958
184
|
if (hasCommand(pmBin)) {
|
|
959
185
|
spinner7.start(`Installing E2E dependencies (${cmds.install})`);
|
|
960
|
-
exec(cmds.install,
|
|
186
|
+
exec(cmds.install, join(dest, "e2e"));
|
|
961
187
|
spinner7.stop("E2E dependencies installed.");
|
|
962
188
|
} else {
|
|
963
189
|
p2.log.warn(`${pm} not found \u2014 run 'cd e2e && ${cmds.install}' manually.`);
|
|
@@ -966,7 +192,7 @@ async function installDeps(dest, components, pm) {
|
|
|
966
192
|
case "mobile":
|
|
967
193
|
if (hasCommand("flutter")) {
|
|
968
194
|
spinner7.start("Installing Flutter dependencies");
|
|
969
|
-
exec("flutter pub get",
|
|
195
|
+
exec("flutter pub get", join(dest, "mobile"));
|
|
970
196
|
spinner7.stop("Flutter dependencies installed.");
|
|
971
197
|
} else {
|
|
972
198
|
p2.log.warn("Flutter not found \u2014 run 'cd mobile && flutter pub get' manually.");
|
|
@@ -982,9 +208,9 @@ async function installDeps(dest, components, pm) {
|
|
|
982
208
|
}
|
|
983
209
|
function copyEnvExamples(dest, components) {
|
|
984
210
|
for (const component of components) {
|
|
985
|
-
const example =
|
|
986
|
-
const env =
|
|
987
|
-
if (
|
|
211
|
+
const example = join(dest, component, ".env.example");
|
|
212
|
+
const env = join(dest, component, ".env");
|
|
213
|
+
if (existsSync(example) && !existsSync(env)) {
|
|
988
214
|
try {
|
|
989
215
|
copyFileSync(example, env);
|
|
990
216
|
} catch {
|
|
@@ -994,10 +220,10 @@ function copyEnvExamples(dest, components) {
|
|
|
994
220
|
}
|
|
995
221
|
|
|
996
222
|
// src/update.ts
|
|
997
|
-
import { existsSync as
|
|
998
|
-
import { readFile as
|
|
999
|
-
import { execSync
|
|
1000
|
-
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";
|
|
1001
227
|
import * as p3 from "@clack/prompts";
|
|
1002
228
|
async function update(cwd, localRepo) {
|
|
1003
229
|
p3.intro("projx update");
|
|
@@ -1007,39 +233,47 @@ async function update(cwd, localRepo) {
|
|
|
1007
233
|
process.exit(1);
|
|
1008
234
|
}
|
|
1009
235
|
try {
|
|
1010
|
-
|
|
236
|
+
execSync("git worktree prune", { cwd, stdio: "pipe" });
|
|
1011
237
|
} catch {
|
|
1012
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
|
+
}
|
|
1013
255
|
if (hasUncommittedChanges(cwd)) {
|
|
1014
256
|
p3.log.error("You have uncommitted changes. Commit or stash them first.");
|
|
1015
257
|
process.exit(1);
|
|
1016
258
|
}
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
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(", ")})`);
|
|
1024
265
|
} else {
|
|
1025
|
-
p3.log.warn("No .projx file found.
|
|
1026
|
-
|
|
1027
|
-
if (discovered.length === 0) {
|
|
1028
|
-
p3.log.error("No projx components found. Run 'projx init' first.");
|
|
1029
|
-
process.exit(1);
|
|
1030
|
-
}
|
|
1031
|
-
config = { version: "0.0.0", components: discovered, createdAt: "unknown" };
|
|
1032
|
-
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(", ")}`);
|
|
1033
268
|
}
|
|
1034
|
-
const
|
|
1035
|
-
for (const c of config.components) {
|
|
269
|
+
for (const c of components) {
|
|
1036
270
|
const dir = componentPaths[c];
|
|
1037
271
|
p3.log.info(dir !== c ? `${c} \u2192 ${dir}/` : `${c}/`);
|
|
1038
272
|
}
|
|
1039
273
|
const componentSkips = {};
|
|
1040
|
-
for (const component of
|
|
274
|
+
for (const component of components) {
|
|
1041
275
|
const dir = componentPaths[component];
|
|
1042
|
-
const marker = await readComponentMarker(
|
|
276
|
+
const marker = await readComponentMarker(join2(cwd, dir));
|
|
1043
277
|
if (marker?.skip && marker.skip.length > 0) {
|
|
1044
278
|
componentSkips[component] = marker.skip;
|
|
1045
279
|
}
|
|
@@ -1053,17 +287,36 @@ async function update(cwd, localRepo) {
|
|
|
1053
287
|
});
|
|
1054
288
|
dlSpinner.stop(isLocal ? "Local templates loaded." : "Templates downloaded.");
|
|
1055
289
|
try {
|
|
1056
|
-
const pkg = JSON.parse(await
|
|
290
|
+
const pkg = JSON.parse(await readFile2(join2(repoDir, "cli/package.json"), "utf-8"));
|
|
1057
291
|
const version = pkg.version;
|
|
1058
|
-
const name = detectProjectName(cwd,
|
|
1059
|
-
const
|
|
1060
|
-
const
|
|
1061
|
-
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 };
|
|
1062
304
|
const spinner7 = p3.spinner();
|
|
1063
305
|
spinner7.start("Applying template update");
|
|
1064
|
-
const rootSkip =
|
|
1065
|
-
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);
|
|
1066
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
|
+
}
|
|
1067
320
|
if (result.status === "merged") {
|
|
1068
321
|
saveBaselineRef(cwd);
|
|
1069
322
|
p3.log.success(`${result.mergedFiles?.length ?? 0} file(s) merged cleanly.`);
|
|
@@ -1079,7 +332,7 @@ async function update(cwd, localRepo) {
|
|
|
1079
332
|
p3.log.info(` ${f}`);
|
|
1080
333
|
}
|
|
1081
334
|
}
|
|
1082
|
-
const handled = await promptSkipLearning(cwd, componentPaths, version);
|
|
335
|
+
const handled = await promptSkipLearning(cwd, componentPaths, version, result.conflictedFiles ?? []);
|
|
1083
336
|
if (!handled) {
|
|
1084
337
|
p3.log.info("");
|
|
1085
338
|
p3.log.info("Review: git diff");
|
|
@@ -1101,7 +354,7 @@ async function update(cwd, localRepo) {
|
|
|
1101
354
|
}
|
|
1102
355
|
function isGitRepo(cwd) {
|
|
1103
356
|
try {
|
|
1104
|
-
|
|
357
|
+
execSync("git rev-parse --is-inside-work-tree", { cwd, stdio: "pipe" });
|
|
1105
358
|
return true;
|
|
1106
359
|
} catch {
|
|
1107
360
|
return false;
|
|
@@ -1109,33 +362,108 @@ function isGitRepo(cwd) {
|
|
|
1109
362
|
}
|
|
1110
363
|
function hasUncommittedChanges(cwd) {
|
|
1111
364
|
try {
|
|
1112
|
-
const status =
|
|
365
|
+
const status = execSync("git status --porcelain", { cwd, stdio: "pipe" }).toString().trim();
|
|
1113
366
|
return status.length > 0;
|
|
1114
367
|
} catch {
|
|
1115
368
|
return false;
|
|
1116
369
|
}
|
|
1117
370
|
}
|
|
1118
|
-
async function
|
|
1119
|
-
|
|
1120
|
-
const
|
|
1121
|
-
|
|
1122
|
-
const
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
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) => {
|
|
1127
436
|
const base = f.split("/").pop();
|
|
1128
437
|
if (base === ".projx" || base === COMPONENT_MARKER) return false;
|
|
1129
438
|
return true;
|
|
1130
439
|
});
|
|
1131
440
|
if (changedFiles.length === 0) return false;
|
|
1132
|
-
|
|
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("");
|
|
1133
457
|
const selected = await p3.multiselect({
|
|
1134
|
-
message: "
|
|
458
|
+
message: "Which files do you want to KEEP?",
|
|
1135
459
|
options: changedFiles.map((f) => ({ value: f, label: f })),
|
|
1136
460
|
required: false
|
|
1137
461
|
});
|
|
1138
|
-
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
|
+
}
|
|
1139
467
|
const kept = new Set(selected);
|
|
1140
468
|
const discarded = changedFiles.filter((f) => !kept.has(f));
|
|
1141
469
|
if (discarded.length > 0) {
|
|
@@ -1143,9 +471,9 @@ async function promptSkipLearning(cwd, componentPaths, version) {
|
|
|
1143
471
|
const entry = entries.find((e) => e.file === file);
|
|
1144
472
|
try {
|
|
1145
473
|
if (entry?.status === "??") {
|
|
1146
|
-
await unlink(
|
|
474
|
+
await unlink(join2(cwd, file));
|
|
1147
475
|
} else {
|
|
1148
|
-
|
|
476
|
+
execSync(`git checkout -- "${file}"`, { cwd, stdio: "pipe" });
|
|
1149
477
|
}
|
|
1150
478
|
} catch {
|
|
1151
479
|
}
|
|
@@ -1156,7 +484,7 @@ async function promptSkipLearning(cwd, componentPaths, version) {
|
|
|
1156
484
|
);
|
|
1157
485
|
}
|
|
1158
486
|
if (kept.size > 0) {
|
|
1159
|
-
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:`);
|
|
1160
488
|
p3.log.info(
|
|
1161
489
|
` git add . && git commit -m "projx: update to v${version}"`
|
|
1162
490
|
);
|
|
@@ -1190,42 +518,33 @@ async function learnSkips(cwd, files, componentPaths) {
|
|
|
1190
518
|
}
|
|
1191
519
|
for (const [component, additions] of Object.entries(componentSkipAdds)) {
|
|
1192
520
|
const dir = componentPaths[component];
|
|
1193
|
-
const
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
data.skip = [.../* @__PURE__ */ new Set([...existing, ...additions])];
|
|
1198
|
-
await writeFile3(markerPath, JSON.stringify(data, null, 2) + "\n");
|
|
1199
|
-
} catch {
|
|
1200
|
-
}
|
|
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 });
|
|
1201
525
|
}
|
|
1202
526
|
if (rootSkipAdds.length > 0) {
|
|
1203
|
-
const
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
data.skip = [.../* @__PURE__ */ new Set([...existing, ...rootSkipAdds])];
|
|
1208
|
-
await writeFile3(configPath, JSON.stringify(data, null, 2) + "\n");
|
|
1209
|
-
} catch {
|
|
1210
|
-
}
|
|
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 });
|
|
1211
531
|
}
|
|
1212
532
|
}
|
|
1213
533
|
|
|
1214
534
|
// src/add.ts
|
|
1215
|
-
import { copyFileSync as copyFileSync2, existsSync as
|
|
1216
|
-
import { readFile as
|
|
1217
|
-
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";
|
|
1218
538
|
import * as p4 from "@clack/prompts";
|
|
1219
539
|
async function add(cwd, newComponents, localRepo, skipInstall = false) {
|
|
1220
540
|
p4.intro("projx add");
|
|
1221
541
|
const isLocal = !!localRepo;
|
|
1222
|
-
|
|
1223
|
-
if (!existsSync5(configPath)) {
|
|
542
|
+
if (!existsSync3(join3(cwd, ".projx"))) {
|
|
1224
543
|
p4.log.error("No .projx file found. Run 'npx create-projx <name>' to create a project first.");
|
|
1225
544
|
process.exit(1);
|
|
1226
545
|
}
|
|
1227
|
-
const config =
|
|
1228
|
-
const existing =
|
|
546
|
+
const config = await readProjxConfig(cwd);
|
|
547
|
+
const { components: existing } = await discoverComponentsFromMarkers(cwd);
|
|
1229
548
|
const alreadyExists = newComponents.filter((c) => existing.includes(c));
|
|
1230
549
|
if (alreadyExists.length > 0) {
|
|
1231
550
|
p4.log.warn(`Already present: ${alreadyExists.join(", ")}. Skipping those.`);
|
|
@@ -1252,19 +571,19 @@ async function add(cwd, newComponents, localRepo, skipInstall = false) {
|
|
|
1252
571
|
const pm = config.packageManager ?? "npm";
|
|
1253
572
|
const name = detectProjectName(cwd, existing, paths);
|
|
1254
573
|
const vars = { projectName: name, components: allComponents, paths, pm: pmCommands(pm) };
|
|
1255
|
-
const pkg = JSON.parse(await
|
|
574
|
+
const pkg = JSON.parse(await readFile3(join3(repoDir, "cli/package.json"), "utf-8"));
|
|
1256
575
|
const version = pkg.version;
|
|
1257
576
|
const spinner7 = p4.spinner();
|
|
1258
577
|
spinner7.start("Adding components");
|
|
1259
|
-
await writeTemplateToDir(cwd, repoDir, allComponents, paths, vars, version,
|
|
578
|
+
await writeTemplateToDir(cwd, repoDir, allComponents, paths, vars, version, { realCwd: cwd });
|
|
1260
579
|
spinner7.stop("Components added.");
|
|
1261
580
|
if (!skipInstall) {
|
|
1262
581
|
await installDeps2(cwd, toAdd, pm);
|
|
1263
582
|
}
|
|
1264
583
|
for (const component of toAdd) {
|
|
1265
|
-
const example =
|
|
1266
|
-
const env =
|
|
1267
|
-
if (
|
|
584
|
+
const example = join3(cwd, component, ".env.example");
|
|
585
|
+
const env = join3(cwd, component, ".env");
|
|
586
|
+
if (existsSync3(example) && !existsSync3(env)) {
|
|
1268
587
|
try {
|
|
1269
588
|
copyFileSync2(example, env);
|
|
1270
589
|
} catch {
|
|
@@ -1288,7 +607,7 @@ async function installDeps2(dest, components, pm) {
|
|
|
1288
607
|
case "fastapi":
|
|
1289
608
|
if (hasCommand("uv")) {
|
|
1290
609
|
spinner7.start("Installing FastAPI dependencies");
|
|
1291
|
-
exec("uv sync --all-extras",
|
|
610
|
+
exec("uv sync --all-extras", join3(dest, "fastapi"));
|
|
1292
611
|
spinner7.stop("FastAPI dependencies installed.");
|
|
1293
612
|
} else {
|
|
1294
613
|
p4.log.warn("uv not found \u2014 run 'cd fastapi && uv sync' manually.");
|
|
@@ -1297,7 +616,7 @@ async function installDeps2(dest, components, pm) {
|
|
|
1297
616
|
case "fastify":
|
|
1298
617
|
if (hasCommand(pmBin)) {
|
|
1299
618
|
spinner7.start(`Installing Fastify dependencies (${cmds.install})`);
|
|
1300
|
-
exec(cmds.install,
|
|
619
|
+
exec(cmds.install, join3(dest, "fastify"));
|
|
1301
620
|
spinner7.stop("Fastify dependencies installed.");
|
|
1302
621
|
} else {
|
|
1303
622
|
p4.log.warn(`${pm} not found \u2014 run 'cd fastify && ${cmds.install}' manually.`);
|
|
@@ -1306,7 +625,7 @@ async function installDeps2(dest, components, pm) {
|
|
|
1306
625
|
case "frontend":
|
|
1307
626
|
if (hasCommand(pmBin)) {
|
|
1308
627
|
spinner7.start(`Installing Frontend dependencies (${cmds.install})`);
|
|
1309
|
-
exec(cmds.install,
|
|
628
|
+
exec(cmds.install, join3(dest, "frontend"));
|
|
1310
629
|
spinner7.stop("Frontend dependencies installed.");
|
|
1311
630
|
} else {
|
|
1312
631
|
p4.log.warn(`${pm} not found \u2014 run 'cd frontend && ${cmds.install}' manually.`);
|
|
@@ -1315,7 +634,7 @@ async function installDeps2(dest, components, pm) {
|
|
|
1315
634
|
case "e2e":
|
|
1316
635
|
if (hasCommand(pmBin)) {
|
|
1317
636
|
spinner7.start(`Installing E2E dependencies (${cmds.install})`);
|
|
1318
|
-
exec(cmds.install,
|
|
637
|
+
exec(cmds.install, join3(dest, "e2e"));
|
|
1319
638
|
spinner7.stop("E2E dependencies installed.");
|
|
1320
639
|
} else {
|
|
1321
640
|
p4.log.warn(`${pm} not found \u2014 run 'cd e2e && ${cmds.install}' manually.`);
|
|
@@ -1324,7 +643,7 @@ async function installDeps2(dest, components, pm) {
|
|
|
1324
643
|
case "mobile":
|
|
1325
644
|
if (hasCommand("flutter")) {
|
|
1326
645
|
spinner7.start("Installing Flutter dependencies");
|
|
1327
|
-
exec("flutter pub get",
|
|
646
|
+
exec("flutter pub get", join3(dest, "mobile"));
|
|
1328
647
|
spinner7.stop("Flutter dependencies installed.");
|
|
1329
648
|
} else {
|
|
1330
649
|
p4.log.warn("Flutter not found \u2014 run 'cd mobile && flutter pub get' manually.");
|
|
@@ -1340,22 +659,22 @@ async function installDeps2(dest, components, pm) {
|
|
|
1340
659
|
}
|
|
1341
660
|
|
|
1342
661
|
// src/init.ts
|
|
1343
|
-
import { existsSync as
|
|
1344
|
-
import { readFile as
|
|
1345
|
-
import { execSync as
|
|
1346
|
-
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";
|
|
1347
666
|
import * as p5 from "@clack/prompts";
|
|
1348
667
|
|
|
1349
668
|
// src/detect.ts
|
|
1350
|
-
import { existsSync as
|
|
1351
|
-
import { readdir
|
|
1352
|
-
import { join as
|
|
669
|
+
import { existsSync as existsSync4 } from "fs";
|
|
670
|
+
import { readdir } from "fs/promises";
|
|
671
|
+
import { join as join4 } from "path";
|
|
1353
672
|
async function detectComponents(cwd) {
|
|
1354
673
|
const results = [];
|
|
1355
|
-
const entries = await
|
|
674
|
+
const entries = await readdir(cwd, { withFileTypes: true });
|
|
1356
675
|
const dirs = entries.filter((e) => e.isDirectory() && !e.name.startsWith(".") && !EXCLUDE.has(e.name)).map((e) => e.name);
|
|
1357
676
|
for (const dir of dirs) {
|
|
1358
|
-
const full =
|
|
677
|
+
const full = join4(cwd, dir);
|
|
1359
678
|
const detections = await scanDirectory(full, dir);
|
|
1360
679
|
results.push(...detections);
|
|
1361
680
|
}
|
|
@@ -1363,7 +682,7 @@ async function detectComponents(cwd) {
|
|
|
1363
682
|
}
|
|
1364
683
|
async function scanDirectory(dir, relPath) {
|
|
1365
684
|
const results = [];
|
|
1366
|
-
const pyproject = await readFileOrNull(
|
|
685
|
+
const pyproject = await readFileOrNull(join4(dir, "pyproject.toml"));
|
|
1367
686
|
if (pyproject && /fastapi/i.test(pyproject)) {
|
|
1368
687
|
results.push({
|
|
1369
688
|
component: "fastapi",
|
|
@@ -1400,7 +719,7 @@ async function scanDirectory(dir, relPath) {
|
|
|
1400
719
|
});
|
|
1401
720
|
}
|
|
1402
721
|
}
|
|
1403
|
-
const pubspec = await readFileOrNull(
|
|
722
|
+
const pubspec = await readFileOrNull(join4(dir, "pubspec.yaml"));
|
|
1404
723
|
if (pubspec && /flutter:/i.test(pubspec)) {
|
|
1405
724
|
results.push({
|
|
1406
725
|
component: "mobile",
|
|
@@ -1409,7 +728,7 @@ async function scanDirectory(dir, relPath) {
|
|
|
1409
728
|
evidence: "pubspec.yaml has flutter dependency"
|
|
1410
729
|
});
|
|
1411
730
|
}
|
|
1412
|
-
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"));
|
|
1413
732
|
if (hasTf) {
|
|
1414
733
|
results.push({
|
|
1415
734
|
component: "infra",
|
|
@@ -1421,7 +740,7 @@ async function scanDirectory(dir, relPath) {
|
|
|
1421
740
|
return results;
|
|
1422
741
|
}
|
|
1423
742
|
async function readPkg(dir) {
|
|
1424
|
-
const content = await readFileOrNull(
|
|
743
|
+
const content = await readFileOrNull(join4(dir, "package.json"));
|
|
1425
744
|
if (!content) return null;
|
|
1426
745
|
try {
|
|
1427
746
|
return JSON.parse(content);
|
|
@@ -1434,7 +753,7 @@ async function readPkg(dir) {
|
|
|
1434
753
|
async function init(cwd, localRepo) {
|
|
1435
754
|
p5.intro("projx init");
|
|
1436
755
|
const isLocal = !!localRepo;
|
|
1437
|
-
if (
|
|
756
|
+
if (existsSync5(join5(cwd, ".projx"))) {
|
|
1438
757
|
p5.log.error("This project is already initialized. Use 'npx create-projx update' or 'npx create-projx add' instead.");
|
|
1439
758
|
process.exit(1);
|
|
1440
759
|
}
|
|
@@ -1494,15 +813,15 @@ async function init(cwd, localRepo) {
|
|
|
1494
813
|
});
|
|
1495
814
|
dlSpinner.stop(isLocal ? "Local templates loaded." : "Templates downloaded.");
|
|
1496
815
|
try {
|
|
1497
|
-
const pkg = JSON.parse(await
|
|
816
|
+
const pkg = JSON.parse(await readFile4(join5(repoDir, "cli/package.json"), "utf-8"));
|
|
1498
817
|
const version = pkg.version;
|
|
1499
818
|
const applySpinner = p5.spinner();
|
|
1500
819
|
applySpinner.start("Applying template");
|
|
1501
|
-
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);
|
|
1502
821
|
applySpinner.stop("Template applied.");
|
|
1503
|
-
if (
|
|
822
|
+
if (existsSync5(join5(cwd, ".githooks"))) {
|
|
1504
823
|
try {
|
|
1505
|
-
|
|
824
|
+
execSync2("git config core.hooksPath .githooks", { cwd, stdio: "pipe" });
|
|
1506
825
|
} catch {
|
|
1507
826
|
}
|
|
1508
827
|
}
|
|
@@ -1561,7 +880,7 @@ async function manualSelect(cwd) {
|
|
|
1561
880
|
defaultValue: component
|
|
1562
881
|
});
|
|
1563
882
|
if (p5.isCancel(dir)) process.exit(0);
|
|
1564
|
-
if (!
|
|
883
|
+
if (!existsSync5(join5(cwd, dir))) {
|
|
1565
884
|
p5.log.warn(`${dir}/ does not exist \u2014 skipping.`);
|
|
1566
885
|
continue;
|
|
1567
886
|
}
|
|
@@ -1571,7 +890,7 @@ async function manualSelect(cwd) {
|
|
|
1571
890
|
}
|
|
1572
891
|
function isGitRepo2(cwd) {
|
|
1573
892
|
try {
|
|
1574
|
-
|
|
893
|
+
execSync2("git rev-parse --is-inside-work-tree", { cwd, stdio: "pipe" });
|
|
1575
894
|
return true;
|
|
1576
895
|
} catch {
|
|
1577
896
|
return false;
|
|
@@ -1579,7 +898,7 @@ function isGitRepo2(cwd) {
|
|
|
1579
898
|
}
|
|
1580
899
|
function hasUncommittedChanges2(cwd) {
|
|
1581
900
|
try {
|
|
1582
|
-
const status =
|
|
901
|
+
const status = execSync2("git status --porcelain", { cwd, stdio: "pipe" }).toString().trim();
|
|
1583
902
|
return status.length > 0;
|
|
1584
903
|
} catch {
|
|
1585
904
|
return false;
|
|
@@ -1587,9 +906,8 @@ function hasUncommittedChanges2(cwd) {
|
|
|
1587
906
|
}
|
|
1588
907
|
|
|
1589
908
|
// src/pin.ts
|
|
1590
|
-
import { existsSync as
|
|
1591
|
-
import {
|
|
1592
|
-
import { join as join9 } from "path";
|
|
909
|
+
import { existsSync as existsSync6 } from "fs";
|
|
910
|
+
import { join as join6 } from "path";
|
|
1593
911
|
import * as p6 from "@clack/prompts";
|
|
1594
912
|
function classifyPattern(pattern, componentPaths) {
|
|
1595
913
|
const dirToComponent = {};
|
|
@@ -1609,12 +927,11 @@ function classifyPattern(pattern, componentPaths) {
|
|
|
1609
927
|
}
|
|
1610
928
|
async function pin(cwd, patterns) {
|
|
1611
929
|
p6.intro("projx pin");
|
|
1612
|
-
|
|
1613
|
-
if (!existsSync8(configPath)) {
|
|
930
|
+
if (!existsSync6(join6(cwd, ".projx"))) {
|
|
1614
931
|
p6.log.error("No .projx file found. Run 'npx create-projx init' first.");
|
|
1615
932
|
process.exit(1);
|
|
1616
933
|
}
|
|
1617
|
-
const config =
|
|
934
|
+
const config = await readProjxConfig(cwd);
|
|
1618
935
|
const componentPaths = (await discoverComponentsFromMarkers(cwd)).paths;
|
|
1619
936
|
const rootAdds = [];
|
|
1620
937
|
const componentAdds = {};
|
|
@@ -1633,30 +950,27 @@ async function pin(cwd, patterns) {
|
|
|
1633
950
|
}
|
|
1634
951
|
for (const [component, additions] of Object.entries(componentAdds)) {
|
|
1635
952
|
const dir = componentPaths[component];
|
|
1636
|
-
const
|
|
1637
|
-
|
|
1638
|
-
const data = JSON.parse(await readFile8(markerPath, "utf-8"));
|
|
1639
|
-
const existing = data.skip ?? [];
|
|
1640
|
-
const merged = [.../* @__PURE__ */ new Set([...existing, ...additions])];
|
|
1641
|
-
const added = merged.length - existing.length;
|
|
1642
|
-
if (added > 0) {
|
|
1643
|
-
data.skip = merged;
|
|
1644
|
-
await writeFile4(markerPath, JSON.stringify(data, null, 2) + "\n");
|
|
1645
|
-
p6.log.success(`${component}: pinned ${additions.join(", ")}`);
|
|
1646
|
-
} else {
|
|
1647
|
-
p6.log.info(`${component}: already pinned.`);
|
|
1648
|
-
}
|
|
1649
|
-
} catch {
|
|
953
|
+
const marker = await readComponentMarker(join6(cwd, dir));
|
|
954
|
+
if (!marker) {
|
|
1650
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.`);
|
|
1651
966
|
}
|
|
1652
967
|
}
|
|
1653
968
|
if (rootAdds.length > 0) {
|
|
1654
|
-
const existing = config.skip
|
|
969
|
+
const existing = Array.isArray(config.skip) ? config.skip : [];
|
|
1655
970
|
const merged = [.../* @__PURE__ */ new Set([...existing, ...rootAdds])];
|
|
1656
971
|
const added = merged.length - existing.length;
|
|
1657
972
|
if (added > 0) {
|
|
1658
|
-
config
|
|
1659
|
-
await writeFile4(configPath, JSON.stringify(config, null, 2) + "\n");
|
|
973
|
+
await writeProjxConfig(cwd, { ...config, skip: merged });
|
|
1660
974
|
p6.log.success(`root: pinned ${rootAdds.join(", ")}`);
|
|
1661
975
|
} else {
|
|
1662
976
|
p6.log.info("root: already pinned.");
|
|
@@ -1666,12 +980,11 @@ async function pin(cwd, patterns) {
|
|
|
1666
980
|
}
|
|
1667
981
|
async function unpin(cwd, patterns) {
|
|
1668
982
|
p6.intro("projx unpin");
|
|
1669
|
-
|
|
1670
|
-
if (!existsSync8(configPath)) {
|
|
983
|
+
if (!existsSync6(join6(cwd, ".projx"))) {
|
|
1671
984
|
p6.log.error("No .projx file found. Run 'npx create-projx init' first.");
|
|
1672
985
|
process.exit(1);
|
|
1673
986
|
}
|
|
1674
|
-
const config =
|
|
987
|
+
const config = await readProjxConfig(cwd);
|
|
1675
988
|
const componentPaths = (await discoverComponentsFromMarkers(cwd)).paths;
|
|
1676
989
|
const rootRemoves = [];
|
|
1677
990
|
const componentRemoves = {};
|
|
@@ -1686,38 +999,27 @@ async function unpin(cwd, patterns) {
|
|
|
1686
999
|
}
|
|
1687
1000
|
for (const [component, removals] of Object.entries(componentRemoves)) {
|
|
1688
1001
|
const dir = componentPaths[component];
|
|
1689
|
-
const
|
|
1690
|
-
|
|
1691
|
-
const data = JSON.parse(await readFile8(markerPath, "utf-8"));
|
|
1692
|
-
const existing = data.skip ?? [];
|
|
1693
|
-
const filtered = existing.filter((s) => !removals.includes(s));
|
|
1694
|
-
const removed = existing.length - filtered.length;
|
|
1695
|
-
if (removed > 0) {
|
|
1696
|
-
if (filtered.length > 0) {
|
|
1697
|
-
data.skip = filtered;
|
|
1698
|
-
} else {
|
|
1699
|
-
delete data.skip;
|
|
1700
|
-
}
|
|
1701
|
-
await writeFile4(markerPath, JSON.stringify(data, null, 2) + "\n");
|
|
1702
|
-
p6.log.success(`${component}: unpinned ${removals.join(", ")}`);
|
|
1703
|
-
} else {
|
|
1704
|
-
p6.log.info(`${component}: not found in skip list.`);
|
|
1705
|
-
}
|
|
1706
|
-
} catch {
|
|
1002
|
+
const marker = await readComponentMarker(join6(cwd, dir));
|
|
1003
|
+
if (!marker) {
|
|
1707
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.`);
|
|
1708
1015
|
}
|
|
1709
1016
|
}
|
|
1710
1017
|
if (rootRemoves.length > 0) {
|
|
1711
|
-
const existing = config.skip
|
|
1018
|
+
const existing = Array.isArray(config.skip) ? config.skip : [];
|
|
1712
1019
|
const filtered = existing.filter((s) => !rootRemoves.includes(s));
|
|
1713
1020
|
const removed = existing.length - filtered.length;
|
|
1714
1021
|
if (removed > 0) {
|
|
1715
|
-
|
|
1716
|
-
config.skip = filtered;
|
|
1717
|
-
} else {
|
|
1718
|
-
delete config.skip;
|
|
1719
|
-
}
|
|
1720
|
-
await writeFile4(configPath, JSON.stringify(config, null, 2) + "\n");
|
|
1022
|
+
await writeProjxConfig(cwd, { ...config, skip: filtered });
|
|
1721
1023
|
p6.log.success(`root: unpinned ${rootRemoves.join(", ")}`);
|
|
1722
1024
|
} else {
|
|
1723
1025
|
p6.log.info("root: not found in skip list.");
|
|
@@ -1727,24 +1029,24 @@ async function unpin(cwd, patterns) {
|
|
|
1727
1029
|
}
|
|
1728
1030
|
async function listPins(cwd) {
|
|
1729
1031
|
p6.intro("projx pin --list");
|
|
1730
|
-
|
|
1731
|
-
if (!existsSync8(configPath)) {
|
|
1032
|
+
if (!existsSync6(join6(cwd, ".projx"))) {
|
|
1732
1033
|
p6.log.error("No .projx file found. Run 'npx create-projx init' first.");
|
|
1733
1034
|
process.exit(1);
|
|
1734
1035
|
}
|
|
1735
|
-
const config =
|
|
1036
|
+
const config = await readProjxConfig(cwd);
|
|
1736
1037
|
const { components: discovered, paths: componentPaths } = await discoverComponentsFromMarkers(cwd);
|
|
1737
1038
|
let hasAny = false;
|
|
1738
|
-
|
|
1039
|
+
const rootSkip = Array.isArray(config.skip) ? config.skip : [];
|
|
1040
|
+
if (rootSkip.length > 0) {
|
|
1739
1041
|
hasAny = true;
|
|
1740
1042
|
p6.log.info("root:");
|
|
1741
|
-
for (const s of
|
|
1043
|
+
for (const s of rootSkip) {
|
|
1742
1044
|
p6.log.info(` ${s}`);
|
|
1743
1045
|
}
|
|
1744
1046
|
}
|
|
1745
1047
|
for (const component of discovered) {
|
|
1746
1048
|
const dir = componentPaths[component];
|
|
1747
|
-
const marker = await readComponentMarker(
|
|
1049
|
+
const marker = await readComponentMarker(join6(cwd, dir));
|
|
1748
1050
|
if (marker?.skip && marker.skip.length > 0) {
|
|
1749
1051
|
hasAny = true;
|
|
1750
1052
|
const label = dir !== component ? `${component} (${dir}/)` : `${component}`;
|
|
@@ -1761,15 +1063,15 @@ async function listPins(cwd) {
|
|
|
1761
1063
|
}
|
|
1762
1064
|
|
|
1763
1065
|
// src/doctor.ts
|
|
1764
|
-
import { existsSync as
|
|
1765
|
-
import {
|
|
1766
|
-
import { execSync as
|
|
1767
|
-
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";
|
|
1768
1070
|
import * as p7 from "@clack/prompts";
|
|
1769
1071
|
async function checkConfig(cwd) {
|
|
1770
1072
|
const results = [];
|
|
1771
|
-
const configPath =
|
|
1772
|
-
if (!
|
|
1073
|
+
const configPath = join7(cwd, ".projx");
|
|
1074
|
+
if (!existsSync7(configPath)) {
|
|
1773
1075
|
results.push({
|
|
1774
1076
|
name: ".projx exists",
|
|
1775
1077
|
status: "fail",
|
|
@@ -1778,44 +1080,41 @@ async function checkConfig(cwd) {
|
|
|
1778
1080
|
});
|
|
1779
1081
|
return { results };
|
|
1780
1082
|
}
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
config = JSON.parse(await readFile9(configPath, "utf-8"));
|
|
1784
|
-
} catch {
|
|
1083
|
+
const rootConfig = await readProjxConfig(cwd);
|
|
1084
|
+
if (Object.keys(rootConfig).length === 0) {
|
|
1785
1085
|
results.push({
|
|
1786
1086
|
name: ".projx valid JSON",
|
|
1787
1087
|
status: "fail",
|
|
1788
|
-
message: ".projx contains invalid JSON."
|
|
1088
|
+
message: ".projx contains invalid JSON or is empty."
|
|
1789
1089
|
});
|
|
1790
1090
|
return { results };
|
|
1791
1091
|
}
|
|
1792
|
-
results.push({ name: ".projx exists", status: "pass", message: `v${
|
|
1793
|
-
if (!
|
|
1092
|
+
results.push({ name: ".projx exists", status: "pass", message: `v${rootConfig.version ?? "unknown"}` });
|
|
1093
|
+
if (!rootConfig.version) {
|
|
1794
1094
|
results.push({
|
|
1795
1095
|
name: ".projx fields",
|
|
1796
|
-
status: "fail",
|
|
1797
|
-
message: "Missing required fields (version, components)."
|
|
1798
|
-
});
|
|
1799
|
-
return { results };
|
|
1800
|
-
}
|
|
1801
|
-
const invalid = config.components.filter((c) => !COMPONENTS.includes(c));
|
|
1802
|
-
if (invalid.length > 0) {
|
|
1803
|
-
results.push({
|
|
1804
|
-
name: "component names",
|
|
1805
1096
|
status: "warn",
|
|
1806
|
-
message:
|
|
1097
|
+
message: "Missing version field."
|
|
1807
1098
|
});
|
|
1808
|
-
} else {
|
|
1809
|
-
results.push({ name: "component names", status: "pass", message: `${config.components.length} valid` });
|
|
1810
1099
|
}
|
|
1811
|
-
return { results,
|
|
1100
|
+
return { results, rootConfig };
|
|
1812
1101
|
}
|
|
1813
|
-
async function checkComponents(cwd,
|
|
1102
|
+
async function checkComponents(cwd, components, componentPaths) {
|
|
1814
1103
|
const results = [];
|
|
1815
|
-
|
|
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) {
|
|
1816
1115
|
const dir = componentPaths[component];
|
|
1817
|
-
const fullDir =
|
|
1818
|
-
if (!
|
|
1116
|
+
const fullDir = join7(cwd, dir);
|
|
1117
|
+
if (!existsSync7(fullDir)) {
|
|
1819
1118
|
results.push({
|
|
1820
1119
|
name: `${component} directory`,
|
|
1821
1120
|
status: "fail",
|
|
@@ -1833,53 +1132,28 @@ async function checkComponents(cwd, config, componentPaths) {
|
|
|
1833
1132
|
});
|
|
1834
1133
|
continue;
|
|
1835
1134
|
}
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
name: `${component} marker`,
|
|
1839
|
-
status: "warn",
|
|
1840
|
-
message: `Marker in ${dir}/ does not list "${component}".`
|
|
1841
|
-
});
|
|
1842
|
-
} else {
|
|
1843
|
-
const label = dir !== component ? `${dir}/ (${component})` : `${component}/`;
|
|
1844
|
-
results.push({ name: `${component} marker`, status: "pass", message: label });
|
|
1845
|
-
}
|
|
1846
|
-
}
|
|
1847
|
-
try {
|
|
1848
|
-
const entries = await readdir3(cwd, { withFileTypes: true });
|
|
1849
|
-
for (const entry of entries) {
|
|
1850
|
-
if (!entry.isDirectory() || entry.name.startsWith(".")) continue;
|
|
1851
|
-
const markerPath = join10(cwd, entry.name, COMPONENT_MARKER);
|
|
1852
|
-
if (!existsSync9(markerPath)) continue;
|
|
1853
|
-
const isKnown = Object.values(componentPaths).includes(entry.name);
|
|
1854
|
-
if (!isKnown) {
|
|
1855
|
-
results.push({
|
|
1856
|
-
name: `orphan marker`,
|
|
1857
|
-
status: "warn",
|
|
1858
|
-
message: `${entry.name}/ has a ${COMPONENT_MARKER} but is not in .projx components.`
|
|
1859
|
-
});
|
|
1860
|
-
}
|
|
1861
|
-
}
|
|
1862
|
-
} catch {
|
|
1135
|
+
const label = dir !== component ? `${dir}/ (${component})` : `${component}/`;
|
|
1136
|
+
results.push({ name: `${component} marker`, status: "pass", message: label });
|
|
1863
1137
|
}
|
|
1864
1138
|
return results;
|
|
1865
1139
|
}
|
|
1866
1140
|
function checkGit(cwd, fix) {
|
|
1867
1141
|
const results = [];
|
|
1868
1142
|
try {
|
|
1869
|
-
|
|
1143
|
+
execSync3("git rev-parse --is-inside-work-tree", { cwd, stdio: "pipe" });
|
|
1870
1144
|
results.push({ name: "git repo", status: "pass", message: "OK" });
|
|
1871
1145
|
} catch {
|
|
1872
1146
|
results.push({ name: "git repo", status: "fail", message: "Not a git repository." });
|
|
1873
1147
|
return results;
|
|
1874
1148
|
}
|
|
1875
1149
|
try {
|
|
1876
|
-
const ref =
|
|
1150
|
+
const ref = execSync3(`git rev-parse --verify ${BASELINE_REF}`, { cwd, stdio: "pipe" }).toString().trim();
|
|
1877
1151
|
results.push({ name: "baseline ref", status: "pass", message: ref.slice(0, 8) });
|
|
1878
1152
|
} catch {
|
|
1879
1153
|
if (fix) {
|
|
1880
1154
|
saveBaselineRef(cwd);
|
|
1881
1155
|
try {
|
|
1882
|
-
|
|
1156
|
+
execSync3(`git rev-parse --verify ${BASELINE_REF}`, { cwd, stdio: "pipe" });
|
|
1883
1157
|
results.push({ name: "baseline ref", status: "pass", message: "Created from git history." });
|
|
1884
1158
|
} catch {
|
|
1885
1159
|
results.push({
|
|
@@ -1899,11 +1173,11 @@ function checkGit(cwd, fix) {
|
|
|
1899
1173
|
}
|
|
1900
1174
|
}
|
|
1901
1175
|
try {
|
|
1902
|
-
const worktrees =
|
|
1176
|
+
const worktrees = execSync3("git worktree list --porcelain", { cwd, stdio: "pipe" }).toString();
|
|
1903
1177
|
const stale = worktrees.split("\n").filter((l) => l.includes("projx-wt-") || l.includes("projx/tmp-"));
|
|
1904
1178
|
if (stale.length > 0) {
|
|
1905
1179
|
if (fix) {
|
|
1906
|
-
|
|
1180
|
+
execSync3("git worktree prune", { cwd, stdio: "pipe" });
|
|
1907
1181
|
results.push({ name: "worktrees", status: "pass", message: "Pruned stale worktrees." });
|
|
1908
1182
|
} else {
|
|
1909
1183
|
results.push({
|
|
@@ -1921,7 +1195,7 @@ function checkGit(cwd, fix) {
|
|
|
1921
1195
|
results.push({ name: "worktrees", status: "pass", message: "OK" });
|
|
1922
1196
|
}
|
|
1923
1197
|
try {
|
|
1924
|
-
const status =
|
|
1198
|
+
const status = execSync3("git status --porcelain", { cwd, stdio: "pipe" }).toString().trim();
|
|
1925
1199
|
if (status) {
|
|
1926
1200
|
const count = status.split("\n").length;
|
|
1927
1201
|
results.push({ name: "working tree", status: "warn", message: `${count} uncommitted change(s).` });
|
|
@@ -1932,26 +1206,25 @@ function checkGit(cwd, fix) {
|
|
|
1932
1206
|
}
|
|
1933
1207
|
return results;
|
|
1934
1208
|
}
|
|
1935
|
-
async function checkSkipPatterns(cwd,
|
|
1209
|
+
async function checkSkipPatterns(cwd, rootConfig, components, componentPaths) {
|
|
1936
1210
|
const results = [];
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
}
|
|
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
|
+
});
|
|
1947
1220
|
}
|
|
1948
1221
|
}
|
|
1949
|
-
for (const component of
|
|
1222
|
+
for (const component of components) {
|
|
1950
1223
|
const dir = componentPaths[component];
|
|
1951
|
-
const marker = await readComponentMarker(
|
|
1224
|
+
const marker = await readComponentMarker(join7(cwd, dir));
|
|
1952
1225
|
if (marker?.skip && marker.skip.length > 0) {
|
|
1953
1226
|
for (const pattern of marker.skip) {
|
|
1954
|
-
const matches = await patternMatchesAnything(
|
|
1227
|
+
const matches = await patternMatchesAnything(join7(cwd, dir), pattern);
|
|
1955
1228
|
if (!matches) {
|
|
1956
1229
|
results.push({
|
|
1957
1230
|
name: `${component} skip`,
|
|
@@ -1962,23 +1235,23 @@ async function checkSkipPatterns(cwd, config, componentPaths) {
|
|
|
1962
1235
|
}
|
|
1963
1236
|
}
|
|
1964
1237
|
}
|
|
1965
|
-
if (results.length === 0 && (
|
|
1238
|
+
if (results.length === 0 && (rootSkip.length > 0 || components.length > 0)) {
|
|
1966
1239
|
results.push({ name: "skip patterns", status: "pass", message: "All patterns match files." });
|
|
1967
1240
|
}
|
|
1968
1241
|
return results;
|
|
1969
1242
|
}
|
|
1970
1243
|
async function patternMatchesAnything(dir, pattern) {
|
|
1971
1244
|
if (pattern === "**") return true;
|
|
1972
|
-
if (!
|
|
1245
|
+
if (!existsSync7(dir)) return false;
|
|
1973
1246
|
const walk = async (current, base) => {
|
|
1974
1247
|
let entries;
|
|
1975
1248
|
try {
|
|
1976
|
-
entries = await
|
|
1249
|
+
entries = await readdir2(current, { withFileTypes: true });
|
|
1977
1250
|
} catch {
|
|
1978
1251
|
return false;
|
|
1979
1252
|
}
|
|
1980
1253
|
for (const entry of entries) {
|
|
1981
|
-
const full =
|
|
1254
|
+
const full = join7(current, entry.name);
|
|
1982
1255
|
const rel = full.slice(base.length + 1);
|
|
1983
1256
|
if (entry.isDirectory()) {
|
|
1984
1257
|
if (await walk(full, base)) return true;
|
|
@@ -1993,17 +1266,16 @@ async function patternMatchesAnything(dir, pattern) {
|
|
|
1993
1266
|
async function doctor(cwd, fix = false) {
|
|
1994
1267
|
p7.intro("projx doctor");
|
|
1995
1268
|
const allResults = [];
|
|
1996
|
-
const { results: configResults,
|
|
1269
|
+
const { results: configResults, rootConfig } = await checkConfig(cwd);
|
|
1997
1270
|
allResults.push(...configResults);
|
|
1998
|
-
if (!
|
|
1271
|
+
if (!rootConfig) {
|
|
1999
1272
|
printReport(allResults);
|
|
2000
1273
|
process.exit(1);
|
|
2001
1274
|
}
|
|
2002
|
-
const { components
|
|
2003
|
-
|
|
2004
|
-
allResults.push(...await checkComponents(cwd, resolvedConfig, componentPaths));
|
|
1275
|
+
const { components, paths: componentPaths } = await discoverComponentsFromMarkers(cwd);
|
|
1276
|
+
allResults.push(...await checkComponents(cwd, components, componentPaths));
|
|
2005
1277
|
allResults.push(...checkGit(cwd, fix));
|
|
2006
|
-
allResults.push(...await checkSkipPatterns(cwd,
|
|
1278
|
+
allResults.push(...await checkSkipPatterns(cwd, rootConfig, components, componentPaths));
|
|
2007
1279
|
printReport(allResults);
|
|
2008
1280
|
const passed = allResults.filter((r) => r.status === "pass").length;
|
|
2009
1281
|
const warns = allResults.filter((r) => r.status === "warn").length;
|
|
@@ -2027,10 +1299,10 @@ function printReport(results) {
|
|
|
2027
1299
|
}
|
|
2028
1300
|
|
|
2029
1301
|
// src/diff.ts
|
|
2030
|
-
import { existsSync as
|
|
2031
|
-
import { readFile as
|
|
2032
|
-
import { join as
|
|
2033
|
-
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";
|
|
2034
1306
|
import * as p8 from "@clack/prompts";
|
|
2035
1307
|
function isSkipped(file, componentPaths, componentSkips, rootSkip) {
|
|
2036
1308
|
for (const [component, dir] of Object.entries(componentPaths)) {
|
|
@@ -2053,23 +1325,21 @@ function fileComponent(file, componentPaths) {
|
|
|
2053
1325
|
async function diff(cwd, localRepo) {
|
|
2054
1326
|
p8.intro("projx diff");
|
|
2055
1327
|
const isLocal = !!localRepo;
|
|
2056
|
-
|
|
2057
|
-
if (!existsSync10(configPath)) {
|
|
1328
|
+
if (!existsSync8(join8(cwd, ".projx"))) {
|
|
2058
1329
|
p8.log.error("No .projx file found. Run 'npx create-projx init' first.");
|
|
2059
1330
|
process.exit(1);
|
|
2060
1331
|
}
|
|
2061
|
-
const raw =
|
|
2062
|
-
const { components
|
|
2063
|
-
const config = { ...raw, components: discovered.length > 0 ? discovered : raw.components };
|
|
1332
|
+
const raw = await readProjxConfig(cwd);
|
|
1333
|
+
const { components, paths: componentPaths } = await discoverComponentsFromMarkers(cwd);
|
|
2064
1334
|
const componentSkips = {};
|
|
2065
|
-
for (const component of
|
|
1335
|
+
for (const component of components) {
|
|
2066
1336
|
const dir = componentPaths[component];
|
|
2067
|
-
const marker = await readComponentMarker(
|
|
1337
|
+
const marker = await readComponentMarker(join8(cwd, dir));
|
|
2068
1338
|
if (marker?.skip && marker.skip.length > 0) {
|
|
2069
1339
|
componentSkips[component] = marker.skip;
|
|
2070
1340
|
}
|
|
2071
1341
|
}
|
|
2072
|
-
const rootSkip =
|
|
1342
|
+
const rootSkip = Array.isArray(raw.skip) ? raw.skip : [];
|
|
2073
1343
|
const dlSpinner = p8.spinner();
|
|
2074
1344
|
dlSpinner.start(isLocal ? "Using local templates" : "Downloading latest templates");
|
|
2075
1345
|
const repoDir = await downloadRepo(localRepo).catch((err) => {
|
|
@@ -2079,16 +1349,20 @@ async function diff(cwd, localRepo) {
|
|
|
2079
1349
|
});
|
|
2080
1350
|
dlSpinner.stop(isLocal ? "Local templates loaded." : "Templates downloaded.");
|
|
2081
1351
|
try {
|
|
2082
|
-
const pkg = JSON.parse(await
|
|
1352
|
+
const pkg = JSON.parse(await readFile5(join8(repoDir, "cli/package.json"), "utf-8"));
|
|
2083
1353
|
const version = pkg.version;
|
|
2084
|
-
p8.log.info(`Current: v${
|
|
2085
|
-
const name = detectProjectName(cwd,
|
|
2086
|
-
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") };
|
|
2087
1357
|
const spinner7 = p8.spinner();
|
|
2088
1358
|
spinner7.start("Analyzing changes");
|
|
2089
|
-
const tmpTemplate =
|
|
2090
|
-
await
|
|
2091
|
-
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
|
+
});
|
|
2092
1366
|
const baselineRef = getBaselineRef(cwd);
|
|
2093
1367
|
const templateFiles = await collectAllFiles(tmpTemplate, tmpTemplate);
|
|
2094
1368
|
const analyses = [];
|
|
@@ -2098,16 +1372,16 @@ async function diff(cwd, localRepo) {
|
|
|
2098
1372
|
analyses.push({ file, status: "skipped", component });
|
|
2099
1373
|
continue;
|
|
2100
1374
|
}
|
|
2101
|
-
const oursPath =
|
|
2102
|
-
if (!
|
|
1375
|
+
const oursPath = join8(cwd, file);
|
|
1376
|
+
if (!existsSync8(oursPath)) {
|
|
2103
1377
|
analyses.push({ file, status: "new", component });
|
|
2104
1378
|
continue;
|
|
2105
1379
|
}
|
|
2106
1380
|
let oursContent;
|
|
2107
1381
|
let theirsContent;
|
|
2108
1382
|
try {
|
|
2109
|
-
oursContent = await
|
|
2110
|
-
theirsContent = await
|
|
1383
|
+
oursContent = await readFile5(oursPath, "utf-8");
|
|
1384
|
+
theirsContent = await readFile5(join8(tmpTemplate, file), "utf-8");
|
|
2111
1385
|
} catch {
|
|
2112
1386
|
continue;
|
|
2113
1387
|
}
|
|
@@ -2132,7 +1406,7 @@ async function diff(cwd, localRepo) {
|
|
|
2132
1406
|
analyses.push({ file, status: "needs-merge", component });
|
|
2133
1407
|
}
|
|
2134
1408
|
}
|
|
2135
|
-
await
|
|
1409
|
+
await rm(tmpTemplate, { recursive: true, force: true });
|
|
2136
1410
|
spinner7.stop("Analysis complete.");
|
|
2137
1411
|
const groups = {
|
|
2138
1412
|
"new": [],
|
|
@@ -2181,9 +1455,9 @@ async function diff(cwd, localRepo) {
|
|
|
2181
1455
|
}
|
|
2182
1456
|
|
|
2183
1457
|
// src/gen.ts
|
|
2184
|
-
import { existsSync as
|
|
2185
|
-
import { readFile as
|
|
2186
|
-
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";
|
|
2187
1461
|
import * as p9 from "@clack/prompts";
|
|
2188
1462
|
var FIELD_TYPES = ["string", "number", "boolean", "date", "datetime", "text", "json"];
|
|
2189
1463
|
function toPascal(s) {
|
|
@@ -2840,15 +2114,9 @@ async function resolvePrimaryBackend(cwd, hasFastapi, hasFastify, backendFlag) {
|
|
|
2840
2114
|
if (backendFlag) return backendFlag;
|
|
2841
2115
|
if (hasFastapi && !hasFastify) return "fastapi";
|
|
2842
2116
|
if (hasFastify && !hasFastapi) return "fastify";
|
|
2843
|
-
const
|
|
2844
|
-
if (
|
|
2845
|
-
|
|
2846
|
-
const data = JSON.parse(await readFile11(configPath, "utf-8"));
|
|
2847
|
-
if (data.primaryBackend === "fastapi" || data.primaryBackend === "fastify") {
|
|
2848
|
-
return data.primaryBackend;
|
|
2849
|
-
}
|
|
2850
|
-
} catch {
|
|
2851
|
-
}
|
|
2117
|
+
const config = await readProjxConfig(cwd);
|
|
2118
|
+
if (config.primaryBackend === "fastapi" || config.primaryBackend === "fastify") {
|
|
2119
|
+
return config.primaryBackend;
|
|
2852
2120
|
}
|
|
2853
2121
|
if (!process.stdin.isTTY) return "fastify";
|
|
2854
2122
|
const choice = await p9.select({
|
|
@@ -2860,23 +2128,17 @@ async function resolvePrimaryBackend(cwd, hasFastapi, hasFastify, backendFlag) {
|
|
|
2860
2128
|
initialValue: "fastify"
|
|
2861
2129
|
});
|
|
2862
2130
|
if (p9.isCancel(choice)) process.exit(0);
|
|
2863
|
-
|
|
2864
|
-
|
|
2865
|
-
data.primaryBackend = choice;
|
|
2866
|
-
await writeFile5(configPath, JSON.stringify(data, null, 2) + "\n");
|
|
2867
|
-
p9.log.success(`Saved primaryBackend: ${choice} to .projx`);
|
|
2868
|
-
} catch {
|
|
2869
|
-
}
|
|
2131
|
+
await writeProjxConfig(cwd, { ...config, primaryBackend: choice });
|
|
2132
|
+
p9.log.success(`Saved primaryBackend: ${choice} to .projx`);
|
|
2870
2133
|
return choice;
|
|
2871
2134
|
}
|
|
2872
2135
|
async function gen(cwd, entityName, fieldsFlag, backendFlag) {
|
|
2873
2136
|
p9.intro(`projx gen entity ${entityName}`);
|
|
2874
|
-
|
|
2875
|
-
if (!existsSync11(configPath)) {
|
|
2137
|
+
if (!existsSync9(join9(cwd, ".projx"))) {
|
|
2876
2138
|
p9.log.error("No .projx file found. Run 'npx create-projx init' first.");
|
|
2877
2139
|
process.exit(1);
|
|
2878
2140
|
}
|
|
2879
|
-
const projxData =
|
|
2141
|
+
const projxData = await readProjxConfig(cwd);
|
|
2880
2142
|
const pmName = projxData.packageManager ?? "npm";
|
|
2881
2143
|
const pm = pmCommands(pmName);
|
|
2882
2144
|
const { components: discovered, paths: componentPaths } = await discoverComponentsFromMarkers(cwd);
|
|
@@ -2913,37 +2175,37 @@ async function gen(cwd, entityName, fieldsFlag, backendFlag) {
|
|
|
2913
2175
|
const generated = [];
|
|
2914
2176
|
if (genFastapi) {
|
|
2915
2177
|
const dir = componentPaths.fastapi;
|
|
2916
|
-
const entityDir =
|
|
2917
|
-
if (
|
|
2178
|
+
const entityDir = join9(cwd, dir, "src/entities", toSnake(config.name));
|
|
2179
|
+
if (existsSync9(entityDir)) {
|
|
2918
2180
|
p9.log.warn(`${dir}/src/entities/${toSnake(config.name)}/ already exists. Skipping FastAPI.`);
|
|
2919
2181
|
} else {
|
|
2920
|
-
await
|
|
2921
|
-
await
|
|
2922
|
-
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");
|
|
2923
2185
|
generated.push(`${dir}/src/entities/${toSnake(config.name)}/_model.py`);
|
|
2924
2186
|
generated.push(`${dir}/src/entities/${toSnake(config.name)}/__init__.py`);
|
|
2925
|
-
const testsDir =
|
|
2926
|
-
const testFile =
|
|
2927
|
-
if (
|
|
2928
|
-
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));
|
|
2929
2191
|
generated.push(`${dir}/tests/test_${toSnake(config.name)}_entity.py`);
|
|
2930
2192
|
}
|
|
2931
2193
|
}
|
|
2932
2194
|
}
|
|
2933
2195
|
if (genFastify) {
|
|
2934
2196
|
const dir = componentPaths.fastify;
|
|
2935
|
-
const moduleDir =
|
|
2936
|
-
if (
|
|
2197
|
+
const moduleDir = join9(cwd, dir, "src/modules", toKebab(config.name));
|
|
2198
|
+
if (existsSync9(moduleDir)) {
|
|
2937
2199
|
p9.log.warn(`${dir}/src/modules/${toKebab(config.name)}/ already exists. Skipping Fastify.`);
|
|
2938
2200
|
} else {
|
|
2939
|
-
await
|
|
2940
|
-
await
|
|
2941
|
-
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));
|
|
2942
2204
|
generated.push(`${dir}/src/modules/${toKebab(config.name)}/schemas.ts`);
|
|
2943
2205
|
generated.push(`${dir}/src/modules/${toKebab(config.name)}/index.ts`);
|
|
2944
|
-
const appPath =
|
|
2945
|
-
if (
|
|
2946
|
-
const appContent = await
|
|
2206
|
+
const appPath = join9(cwd, dir, "src/app.ts");
|
|
2207
|
+
if (existsSync9(appPath)) {
|
|
2208
|
+
const appContent = await readFile6(appPath, "utf-8");
|
|
2947
2209
|
const importLine = `import './modules/${toKebab(config.name)}/index.js';`;
|
|
2948
2210
|
if (!appContent.includes(importLine)) {
|
|
2949
2211
|
const updated = appContent.replace(
|
|
@@ -2952,62 +2214,62 @@ async function gen(cwd, entityName, fieldsFlag, backendFlag) {
|
|
|
2952
2214
|
`
|
|
2953
2215
|
);
|
|
2954
2216
|
if (updated !== appContent) {
|
|
2955
|
-
await
|
|
2217
|
+
await writeFile(appPath, updated);
|
|
2956
2218
|
generated.push(`${dir}/src/app.ts (import added)`);
|
|
2957
2219
|
}
|
|
2958
2220
|
}
|
|
2959
2221
|
}
|
|
2960
|
-
const prismaPath =
|
|
2961
|
-
if (
|
|
2962
|
-
const prismaContent = await
|
|
2222
|
+
const prismaPath = join9(cwd, dir, "prisma/schema.prisma");
|
|
2223
|
+
if (existsSync9(prismaPath)) {
|
|
2224
|
+
const prismaContent = await readFile6(prismaPath, "utf-8");
|
|
2963
2225
|
const modelName = `model ${toPascal(config.name)}`;
|
|
2964
2226
|
if (!prismaContent.includes(modelName)) {
|
|
2965
2227
|
const prismaModel = generatePrismaModel(config);
|
|
2966
|
-
await
|
|
2228
|
+
await writeFile(prismaPath, prismaContent.trimEnd() + "\n\n" + prismaModel + "\n");
|
|
2967
2229
|
generated.push(`${dir}/prisma/schema.prisma (model added)`);
|
|
2968
2230
|
}
|
|
2969
2231
|
}
|
|
2970
|
-
const testsModulesDir =
|
|
2971
|
-
const fastifyTestFile =
|
|
2972
|
-
if (
|
|
2973
|
-
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));
|
|
2974
2236
|
generated.push(`${dir}/tests/modules/${toKebab(config.name)}.test.ts`);
|
|
2975
2237
|
}
|
|
2976
2238
|
}
|
|
2977
2239
|
}
|
|
2978
2240
|
if (hasFrontend) {
|
|
2979
2241
|
const dir = componentPaths.frontend;
|
|
2980
|
-
const typesDir =
|
|
2242
|
+
const typesDir = join9(cwd, dir, "src/types");
|
|
2981
2243
|
const fileName = toKebab(config.name) + ".ts";
|
|
2982
|
-
const filePath =
|
|
2983
|
-
if (
|
|
2244
|
+
const filePath = join9(typesDir, fileName);
|
|
2245
|
+
if (existsSync9(filePath)) {
|
|
2984
2246
|
p9.log.warn(`${dir}/src/types/${fileName} already exists. Skipping frontend types.`);
|
|
2985
2247
|
} else {
|
|
2986
|
-
await
|
|
2987
|
-
await
|
|
2248
|
+
await mkdir3(typesDir, { recursive: true });
|
|
2249
|
+
await writeFile(filePath, generateFrontendInterface(config));
|
|
2988
2250
|
generated.push(`${dir}/src/types/${fileName}`);
|
|
2989
|
-
const barrelPath =
|
|
2251
|
+
const barrelPath = join9(typesDir, "index.ts");
|
|
2990
2252
|
const exportLine = `export * from './${toKebab(config.name)}';`;
|
|
2991
|
-
if (
|
|
2992
|
-
const content = await
|
|
2253
|
+
if (existsSync9(barrelPath)) {
|
|
2254
|
+
const content = await readFile6(barrelPath, "utf-8");
|
|
2993
2255
|
if (!content.includes(exportLine)) {
|
|
2994
|
-
await
|
|
2256
|
+
await writeFile(barrelPath, content.trimEnd() + "\n" + exportLine + "\n");
|
|
2995
2257
|
}
|
|
2996
2258
|
} else {
|
|
2997
|
-
await
|
|
2259
|
+
await writeFile(barrelPath, exportLine + "\n");
|
|
2998
2260
|
}
|
|
2999
2261
|
generated.push(`${dir}/src/types/index.ts`);
|
|
3000
2262
|
}
|
|
3001
2263
|
}
|
|
3002
2264
|
if (hasMobile) {
|
|
3003
2265
|
const dir = componentPaths.mobile;
|
|
3004
|
-
const entityDir =
|
|
3005
|
-
const modelPath =
|
|
3006
|
-
if (
|
|
2266
|
+
const entityDir = join9(cwd, dir, "lib/entities", toSnake(config.name));
|
|
2267
|
+
const modelPath = join9(entityDir, "model.dart");
|
|
2268
|
+
if (existsSync9(modelPath)) {
|
|
3007
2269
|
p9.log.warn(`${dir}/lib/entities/${toSnake(config.name)}/model.dart already exists. Skipping mobile model.`);
|
|
3008
2270
|
} else {
|
|
3009
|
-
await
|
|
3010
|
-
await
|
|
2271
|
+
await mkdir3(entityDir, { recursive: true });
|
|
2272
|
+
await writeFile(modelPath, generateDartModel(config));
|
|
3011
2273
|
generated.push(`${dir}/lib/entities/${toSnake(config.name)}/model.dart`);
|
|
3012
2274
|
}
|
|
3013
2275
|
}
|
|
@@ -3047,9 +2309,9 @@ async function gen(cwd, entityName, fieldsFlag, backendFlag) {
|
|
|
3047
2309
|
}
|
|
3048
2310
|
|
|
3049
2311
|
// src/sync.ts
|
|
3050
|
-
import { existsSync as
|
|
3051
|
-
import { writeFile as
|
|
3052
|
-
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";
|
|
3053
2315
|
import * as p10 from "@clack/prompts";
|
|
3054
2316
|
function toPascal2(s) {
|
|
3055
2317
|
return s.replace(/(?:^|[_\-\s])([a-zA-Z])/g, (_, c) => c.toUpperCase());
|
|
@@ -3220,8 +2482,8 @@ function generateDartModel2(entity) {
|
|
|
3220
2482
|
}
|
|
3221
2483
|
async function sync(cwd, url) {
|
|
3222
2484
|
p10.intro("projx sync");
|
|
3223
|
-
const configPath =
|
|
3224
|
-
if (!
|
|
2485
|
+
const configPath = join10(cwd, ".projx");
|
|
2486
|
+
if (!existsSync10(configPath)) {
|
|
3225
2487
|
p10.log.error("No .projx file found. Run 'npx create-projx init' first.");
|
|
3226
2488
|
process.exit(1);
|
|
3227
2489
|
}
|
|
@@ -3253,18 +2515,18 @@ async function sync(cwd, url) {
|
|
|
3253
2515
|
const generated = [];
|
|
3254
2516
|
if (hasFrontend) {
|
|
3255
2517
|
const dir = componentPaths.frontend;
|
|
3256
|
-
const typesDir =
|
|
3257
|
-
await
|
|
2518
|
+
const typesDir = join10(cwd, dir, "src/types");
|
|
2519
|
+
await mkdir4(typesDir, { recursive: true });
|
|
3258
2520
|
const barrelExports = [];
|
|
3259
2521
|
for (const entity of meta.entities) {
|
|
3260
2522
|
const fileName = toKebab(toSnake(entity.name)) + ".ts";
|
|
3261
|
-
const filePath =
|
|
3262
|
-
await
|
|
2523
|
+
const filePath = join10(typesDir, fileName);
|
|
2524
|
+
await writeFile2(filePath, generateTsInterface(entity));
|
|
3263
2525
|
generated.push(`${dir}/src/types/${fileName}`);
|
|
3264
2526
|
barrelExports.push(`export * from './${toKebab(toSnake(entity.name))}';`);
|
|
3265
2527
|
}
|
|
3266
|
-
await
|
|
3267
|
-
|
|
2528
|
+
await writeFile2(
|
|
2529
|
+
join10(typesDir, "index.ts"),
|
|
3268
2530
|
barrelExports.join("\n") + "\n"
|
|
3269
2531
|
);
|
|
3270
2532
|
generated.push(`${dir}/src/types/index.ts`);
|
|
@@ -3272,10 +2534,10 @@ async function sync(cwd, url) {
|
|
|
3272
2534
|
if (hasMobile) {
|
|
3273
2535
|
const dir = componentPaths.mobile;
|
|
3274
2536
|
for (const entity of meta.entities) {
|
|
3275
|
-
const entityDir =
|
|
3276
|
-
await
|
|
3277
|
-
const modelPath =
|
|
3278
|
-
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));
|
|
3279
2541
|
generated.push(`${dir}/lib/entities/${toSnake(entity.name)}/model.dart`);
|
|
3280
2542
|
}
|
|
3281
2543
|
}
|
|
@@ -3298,10 +2560,10 @@ async function sync(cwd, url) {
|
|
|
3298
2560
|
function detectMetaUrl(cwd) {
|
|
3299
2561
|
const envFiles = [".env", ".env.dev", ".env.local"];
|
|
3300
2562
|
for (const envFile of envFiles) {
|
|
3301
|
-
const envPath =
|
|
3302
|
-
if (
|
|
2563
|
+
const envPath = join10(cwd, envFile);
|
|
2564
|
+
if (existsSync10(envPath)) {
|
|
3303
2565
|
try {
|
|
3304
|
-
const content =
|
|
2566
|
+
const content = readFileSync(envPath, "utf-8");
|
|
3305
2567
|
const match = content.match(/VITE_API_URL\s*=\s*(.+)/);
|
|
3306
2568
|
if (match) {
|
|
3307
2569
|
const base = match[1].trim().replace(/["']/g, "");
|
|
@@ -3317,10 +2579,10 @@ function detectMetaUrl(cwd) {
|
|
|
3317
2579
|
"frontend/.env.dev"
|
|
3318
2580
|
];
|
|
3319
2581
|
for (const envFile of frontendEnvFiles) {
|
|
3320
|
-
const envPath =
|
|
3321
|
-
if (
|
|
2582
|
+
const envPath = join10(cwd, envFile);
|
|
2583
|
+
if (existsSync10(envPath)) {
|
|
3322
2584
|
try {
|
|
3323
|
-
const content =
|
|
2585
|
+
const content = readFileSync(envPath, "utf-8");
|
|
3324
2586
|
const match = content.match(/VITE_API_URL\s*=\s*(.+)/);
|
|
3325
2587
|
if (match) {
|
|
3326
2588
|
const base = match[1].trim().replace(/["']/g, "");
|
|
@@ -3390,7 +2652,7 @@ function parseArgs() {
|
|
|
3390
2652
|
continue;
|
|
3391
2653
|
}
|
|
3392
2654
|
if (arg === "--local") {
|
|
3393
|
-
localRepo =
|
|
2655
|
+
localRepo = resolve(args[++i] || ".");
|
|
3394
2656
|
continue;
|
|
3395
2657
|
}
|
|
3396
2658
|
if (arg === "--no-git") {
|
|
@@ -3562,8 +2824,8 @@ async function main() {
|
|
|
3562
2824
|
opts.git = options.git ?? opts.git;
|
|
3563
2825
|
opts.install = options.install ?? opts.install;
|
|
3564
2826
|
}
|
|
3565
|
-
const dest =
|
|
3566
|
-
if (
|
|
2827
|
+
const dest = resolve(process.cwd(), opts.name);
|
|
2828
|
+
if (existsSync11(dest)) {
|
|
3567
2829
|
console.error(`Error: ${dest} already exists.`);
|
|
3568
2830
|
process.exit(1);
|
|
3569
2831
|
}
|