codebyplan 1.13.60 → 1.13.61
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/ci.js +518 -0
- package/dist/cli.js +395 -355
- package/package.json +12 -2
package/dist/ci.js
ADDED
|
@@ -0,0 +1,518 @@
|
|
|
1
|
+
// src/lib/repo-reader.ts
|
|
2
|
+
import * as fsPromises from "node:fs/promises";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
var LocalFsReader = class {
|
|
5
|
+
constructor(rootDir) {
|
|
6
|
+
this.rootDir = rootDir;
|
|
7
|
+
}
|
|
8
|
+
rootDir;
|
|
9
|
+
resolve(p) {
|
|
10
|
+
return path.isAbsolute(p) ? p : path.resolve(this.rootDir, p);
|
|
11
|
+
}
|
|
12
|
+
async list(dir) {
|
|
13
|
+
const abs = this.resolve(dir);
|
|
14
|
+
try {
|
|
15
|
+
const entries = await fsPromises.readdir(abs, { withFileTypes: true });
|
|
16
|
+
return entries.filter((e) => e.isDirectory()).map((e) => e.name);
|
|
17
|
+
} catch {
|
|
18
|
+
return [];
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
async read(filePath) {
|
|
22
|
+
const abs = this.resolve(filePath);
|
|
23
|
+
try {
|
|
24
|
+
return await fsPromises.readFile(abs, "utf-8");
|
|
25
|
+
} catch (err) {
|
|
26
|
+
throw new Error(
|
|
27
|
+
`repo-reader: could not read '${abs}': ${err instanceof Error ? err.message : String(err)}`
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
async exists(filePath) {
|
|
32
|
+
const abs = this.resolve(filePath);
|
|
33
|
+
try {
|
|
34
|
+
await fsPromises.access(abs);
|
|
35
|
+
return true;
|
|
36
|
+
} catch {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
// src/lib/ci-init.ts
|
|
43
|
+
import * as fs2 from "node:fs";
|
|
44
|
+
import * as path2 from "node:path";
|
|
45
|
+
|
|
46
|
+
// src/lib/atomic-write.ts
|
|
47
|
+
import * as fs from "node:fs";
|
|
48
|
+
function writeJsonAtomic(filePath, value) {
|
|
49
|
+
const tmpPath = filePath + ".tmp";
|
|
50
|
+
try {
|
|
51
|
+
fs.writeFileSync(tmpPath, JSON.stringify(value, null, 2) + "\n", "utf-8");
|
|
52
|
+
fs.renameSync(tmpPath, filePath);
|
|
53
|
+
} catch (err) {
|
|
54
|
+
try {
|
|
55
|
+
fs.unlinkSync(tmpPath);
|
|
56
|
+
} catch {
|
|
57
|
+
}
|
|
58
|
+
throw err;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// src/lib/ci-init.ts
|
|
63
|
+
var PLATFORM_COMMAND_MAP = {
|
|
64
|
+
next_js: {
|
|
65
|
+
unit_test: {
|
|
66
|
+
command: "pnpm turbo test",
|
|
67
|
+
scope: "per_app_changed",
|
|
68
|
+
hard_fail: true
|
|
69
|
+
},
|
|
70
|
+
typecheck: {
|
|
71
|
+
command: "pnpm turbo typecheck",
|
|
72
|
+
scope: "per_app_changed",
|
|
73
|
+
hard_fail: true
|
|
74
|
+
},
|
|
75
|
+
build: {
|
|
76
|
+
command: "pnpm turbo build",
|
|
77
|
+
scope: "per_app_changed",
|
|
78
|
+
hard_fail: true
|
|
79
|
+
},
|
|
80
|
+
lint: {
|
|
81
|
+
scope: "per_app_changed",
|
|
82
|
+
hard_fail: true,
|
|
83
|
+
delegates_to: ".codebyplan/eslint.json"
|
|
84
|
+
},
|
|
85
|
+
e2e: {
|
|
86
|
+
scope: "per_app_changed",
|
|
87
|
+
hard_fail: false,
|
|
88
|
+
delegates_to: ".codebyplan/e2e.json"
|
|
89
|
+
},
|
|
90
|
+
audit: {
|
|
91
|
+
command: "pnpm audit --json",
|
|
92
|
+
scope: "full_repo",
|
|
93
|
+
hard_fail: true
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
nestjs: {
|
|
97
|
+
unit_test: {
|
|
98
|
+
command: "pnpm turbo test",
|
|
99
|
+
scope: "per_app_changed",
|
|
100
|
+
hard_fail: true
|
|
101
|
+
},
|
|
102
|
+
typecheck: {
|
|
103
|
+
command: "pnpm turbo typecheck",
|
|
104
|
+
scope: "per_app_changed",
|
|
105
|
+
hard_fail: true
|
|
106
|
+
},
|
|
107
|
+
build: {
|
|
108
|
+
command: "pnpm turbo build",
|
|
109
|
+
scope: "per_app_changed",
|
|
110
|
+
hard_fail: true
|
|
111
|
+
},
|
|
112
|
+
lint: {
|
|
113
|
+
scope: "per_app_changed",
|
|
114
|
+
hard_fail: true,
|
|
115
|
+
delegates_to: ".codebyplan/eslint.json"
|
|
116
|
+
},
|
|
117
|
+
e2e: {
|
|
118
|
+
scope: "per_app_changed",
|
|
119
|
+
hard_fail: false,
|
|
120
|
+
delegates_to: ".codebyplan/e2e.json"
|
|
121
|
+
},
|
|
122
|
+
audit: {
|
|
123
|
+
command: "pnpm audit --json",
|
|
124
|
+
scope: "full_repo",
|
|
125
|
+
hard_fail: true
|
|
126
|
+
}
|
|
127
|
+
},
|
|
128
|
+
tauri: {
|
|
129
|
+
unit_test: {
|
|
130
|
+
command: "pnpm turbo test",
|
|
131
|
+
scope: "per_app_changed",
|
|
132
|
+
hard_fail: true
|
|
133
|
+
},
|
|
134
|
+
typecheck: {
|
|
135
|
+
command: "pnpm turbo typecheck",
|
|
136
|
+
scope: "per_app_changed",
|
|
137
|
+
hard_fail: true
|
|
138
|
+
},
|
|
139
|
+
build: {
|
|
140
|
+
command: "pnpm turbo build",
|
|
141
|
+
scope: "per_app_changed",
|
|
142
|
+
hard_fail: true
|
|
143
|
+
},
|
|
144
|
+
lint: {
|
|
145
|
+
scope: "per_app_changed",
|
|
146
|
+
hard_fail: true,
|
|
147
|
+
delegates_to: ".codebyplan/eslint.json"
|
|
148
|
+
},
|
|
149
|
+
e2e: {
|
|
150
|
+
scope: "per_app_changed",
|
|
151
|
+
hard_fail: false,
|
|
152
|
+
delegates_to: ".codebyplan/e2e.json"
|
|
153
|
+
},
|
|
154
|
+
audit: {
|
|
155
|
+
command: "pnpm audit --json",
|
|
156
|
+
scope: "full_repo",
|
|
157
|
+
hard_fail: true
|
|
158
|
+
}
|
|
159
|
+
},
|
|
160
|
+
vscode: {
|
|
161
|
+
unit_test: {
|
|
162
|
+
command: "pnpm turbo test",
|
|
163
|
+
scope: "per_app_changed",
|
|
164
|
+
hard_fail: true
|
|
165
|
+
},
|
|
166
|
+
typecheck: {
|
|
167
|
+
command: "pnpm turbo typecheck",
|
|
168
|
+
scope: "per_app_changed",
|
|
169
|
+
hard_fail: true
|
|
170
|
+
},
|
|
171
|
+
build: {
|
|
172
|
+
command: "pnpm turbo build",
|
|
173
|
+
scope: "per_app_changed",
|
|
174
|
+
hard_fail: true
|
|
175
|
+
},
|
|
176
|
+
lint: {
|
|
177
|
+
scope: "per_app_changed",
|
|
178
|
+
hard_fail: true,
|
|
179
|
+
delegates_to: ".codebyplan/eslint.json"
|
|
180
|
+
},
|
|
181
|
+
e2e: {
|
|
182
|
+
scope: "per_app_changed",
|
|
183
|
+
hard_fail: false,
|
|
184
|
+
delegates_to: ".codebyplan/e2e.json"
|
|
185
|
+
},
|
|
186
|
+
audit: {
|
|
187
|
+
command: "pnpm audit --json",
|
|
188
|
+
scope: "full_repo",
|
|
189
|
+
hard_fail: true
|
|
190
|
+
}
|
|
191
|
+
},
|
|
192
|
+
expo: {
|
|
193
|
+
unit_test: {
|
|
194
|
+
command: "pnpm turbo test",
|
|
195
|
+
scope: "per_app_changed",
|
|
196
|
+
hard_fail: true
|
|
197
|
+
},
|
|
198
|
+
typecheck: {
|
|
199
|
+
command: "pnpm turbo typecheck",
|
|
200
|
+
scope: "per_app_changed",
|
|
201
|
+
hard_fail: true
|
|
202
|
+
},
|
|
203
|
+
build: {
|
|
204
|
+
command: "pnpm turbo build",
|
|
205
|
+
scope: "per_app_changed",
|
|
206
|
+
hard_fail: true
|
|
207
|
+
},
|
|
208
|
+
lint: {
|
|
209
|
+
scope: "per_app_changed",
|
|
210
|
+
hard_fail: true,
|
|
211
|
+
delegates_to: ".codebyplan/eslint.json"
|
|
212
|
+
},
|
|
213
|
+
e2e: {
|
|
214
|
+
scope: "per_app_changed",
|
|
215
|
+
hard_fail: false,
|
|
216
|
+
delegates_to: ".codebyplan/e2e.json"
|
|
217
|
+
},
|
|
218
|
+
audit: {
|
|
219
|
+
command: "pnpm audit --json",
|
|
220
|
+
scope: "full_repo",
|
|
221
|
+
hard_fail: true
|
|
222
|
+
}
|
|
223
|
+
},
|
|
224
|
+
package: {
|
|
225
|
+
unit_test: {
|
|
226
|
+
command: "pnpm turbo test",
|
|
227
|
+
scope: "per_app_changed",
|
|
228
|
+
hard_fail: true
|
|
229
|
+
},
|
|
230
|
+
typecheck: {
|
|
231
|
+
command: "pnpm turbo typecheck",
|
|
232
|
+
scope: "per_app_changed",
|
|
233
|
+
hard_fail: true
|
|
234
|
+
},
|
|
235
|
+
build: {
|
|
236
|
+
command: "pnpm turbo build",
|
|
237
|
+
scope: "per_app_changed",
|
|
238
|
+
hard_fail: true
|
|
239
|
+
},
|
|
240
|
+
lint: {
|
|
241
|
+
scope: "per_app_changed",
|
|
242
|
+
hard_fail: true,
|
|
243
|
+
delegates_to: ".codebyplan/eslint.json"
|
|
244
|
+
},
|
|
245
|
+
e2e: {
|
|
246
|
+
scope: "per_app_changed",
|
|
247
|
+
hard_fail: false,
|
|
248
|
+
delegates_to: ".codebyplan/e2e.json"
|
|
249
|
+
},
|
|
250
|
+
audit: {
|
|
251
|
+
command: "pnpm audit --json",
|
|
252
|
+
scope: "full_repo",
|
|
253
|
+
hard_fail: true
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
};
|
|
257
|
+
async function tryReadJson(reader, filePath) {
|
|
258
|
+
try {
|
|
259
|
+
const raw = await reader.read(filePath);
|
|
260
|
+
return JSON.parse(raw);
|
|
261
|
+
} catch {
|
|
262
|
+
return null;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
function hasDep(pkg, name) {
|
|
266
|
+
if (!pkg) return false;
|
|
267
|
+
const deps = pkg["dependencies"];
|
|
268
|
+
const devDeps = pkg["devDependencies"];
|
|
269
|
+
return Boolean(deps?.[name] ?? devDeps?.[name]);
|
|
270
|
+
}
|
|
271
|
+
function hasDevDep(pkg, name) {
|
|
272
|
+
if (!pkg) return false;
|
|
273
|
+
const devDeps = pkg["devDependencies"];
|
|
274
|
+
return Boolean(devDeps?.[name]);
|
|
275
|
+
}
|
|
276
|
+
async function detectPlatforms(reader) {
|
|
277
|
+
const detected = /* @__PURE__ */ new Set();
|
|
278
|
+
const dirsToScan = [""];
|
|
279
|
+
const appsChildren = await reader.list("apps");
|
|
280
|
+
for (const name of appsChildren) {
|
|
281
|
+
dirsToScan.push(`apps/${name}`);
|
|
282
|
+
}
|
|
283
|
+
for (const dir of dirsToScan) {
|
|
284
|
+
const pkgPath = dir ? `${dir}/package.json` : "package.json";
|
|
285
|
+
const pkg = await tryReadJson(reader, pkgPath);
|
|
286
|
+
const nextBase = dir ? `${dir}/` : "";
|
|
287
|
+
if (await reader.exists(`${nextBase}next.config.ts`) || await reader.exists(`${nextBase}next.config.js`) || await reader.exists(`${nextBase}next.config.mjs`)) {
|
|
288
|
+
detected.add("next_js");
|
|
289
|
+
}
|
|
290
|
+
if (hasDep(pkg, "@nestjs/core")) {
|
|
291
|
+
detected.add("nestjs");
|
|
292
|
+
}
|
|
293
|
+
if (await reader.exists(`${nextBase}tauri.conf.json`) || await reader.exists(`${nextBase}src-tauri`)) {
|
|
294
|
+
detected.add("tauri");
|
|
295
|
+
}
|
|
296
|
+
if (hasDevDep(pkg, "@types/vscode")) {
|
|
297
|
+
detected.add("vscode");
|
|
298
|
+
}
|
|
299
|
+
const deps = pkg?.["dependencies"];
|
|
300
|
+
if (deps?.["expo"]) {
|
|
301
|
+
detected.add("expo");
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
const packagesChildren = await reader.list("packages");
|
|
305
|
+
if (packagesChildren.length > 0) {
|
|
306
|
+
detected.add("package");
|
|
307
|
+
}
|
|
308
|
+
if (detected.size === 0) {
|
|
309
|
+
detected.add("package");
|
|
310
|
+
}
|
|
311
|
+
return Array.from(detected);
|
|
312
|
+
}
|
|
313
|
+
function buildDefaultCiConfig(platforms) {
|
|
314
|
+
const config = {
|
|
315
|
+
platforms: {},
|
|
316
|
+
delegation: {
|
|
317
|
+
lint: ".codebyplan/eslint.json",
|
|
318
|
+
e2e: ".codebyplan/e2e.json",
|
|
319
|
+
detection: ".claude/docs/architecture/testing-matrix.md"
|
|
320
|
+
},
|
|
321
|
+
workflow: {
|
|
322
|
+
required_check_enforced: false
|
|
323
|
+
}
|
|
324
|
+
};
|
|
325
|
+
for (const platform of platforms) {
|
|
326
|
+
const checks = PLATFORM_COMMAND_MAP[platform] ?? PLATFORM_COMMAND_MAP["package"];
|
|
327
|
+
if (checks) {
|
|
328
|
+
config.platforms[platform] = { ...checks };
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
return config;
|
|
332
|
+
}
|
|
333
|
+
async function runCiInit(opts) {
|
|
334
|
+
const projectDir = path2.resolve(opts?.projectDir ?? process.cwd());
|
|
335
|
+
const dryRun = opts?.dryRun ?? false;
|
|
336
|
+
const force = opts?.force ?? false;
|
|
337
|
+
const ciPath = path2.join(projectDir, ".codebyplan", "ci.json");
|
|
338
|
+
const reader = opts?.reader ?? new LocalFsReader(projectDir);
|
|
339
|
+
const platforms = await detectPlatforms(reader);
|
|
340
|
+
if (dryRun) {
|
|
341
|
+
return { status: "dry_run", path: ciPath, platforms };
|
|
342
|
+
}
|
|
343
|
+
let existing = null;
|
|
344
|
+
if (fs2.existsSync(ciPath)) {
|
|
345
|
+
try {
|
|
346
|
+
existing = JSON.parse(fs2.readFileSync(ciPath, "utf-8"));
|
|
347
|
+
} catch {
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
const newConfig = buildDefaultCiConfig(platforms);
|
|
351
|
+
if (existing !== null && !force) {
|
|
352
|
+
if (!existing.platforms) existing.platforms = {};
|
|
353
|
+
const newPlatforms = platforms.filter((p) => !(p in existing.platforms));
|
|
354
|
+
for (const p of newPlatforms) {
|
|
355
|
+
const checks = newConfig.platforms[p];
|
|
356
|
+
if (checks) {
|
|
357
|
+
existing.platforms[p] = checks;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
let categoriesAdded = false;
|
|
361
|
+
for (const p of platforms) {
|
|
362
|
+
if (newPlatforms.includes(p)) continue;
|
|
363
|
+
const existingChecks = existing.platforms[p];
|
|
364
|
+
const defaultChecks = newConfig.platforms[p];
|
|
365
|
+
if (!existingChecks || !defaultChecks) continue;
|
|
366
|
+
for (const [cat, check] of Object.entries(defaultChecks)) {
|
|
367
|
+
if (!(cat in existingChecks)) {
|
|
368
|
+
existingChecks[cat] = check;
|
|
369
|
+
categoriesAdded = true;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
if (newPlatforms.length === 0 && !categoriesAdded) {
|
|
374
|
+
return { status: "skipped", path: ciPath, platforms };
|
|
375
|
+
}
|
|
376
|
+
writeJsonAtomic(ciPath, existing);
|
|
377
|
+
return {
|
|
378
|
+
status: "written",
|
|
379
|
+
path: ciPath,
|
|
380
|
+
platforms: newPlatforms.length > 0 ? newPlatforms : platforms
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
const codebyplanDir = path2.join(projectDir, ".codebyplan");
|
|
384
|
+
fs2.mkdirSync(codebyplanDir, { recursive: true });
|
|
385
|
+
writeJsonAtomic(ciPath, newConfig);
|
|
386
|
+
return { status: "written", path: ciPath, platforms };
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// src/lib/scaffold-ci-workflow.ts
|
|
390
|
+
import * as fs4 from "node:fs";
|
|
391
|
+
import * as path4 from "node:path";
|
|
392
|
+
|
|
393
|
+
// src/lib/templates-dir.ts
|
|
394
|
+
import * as fs3 from "node:fs";
|
|
395
|
+
import * as path3 from "node:path";
|
|
396
|
+
import { fileURLToPath } from "node:url";
|
|
397
|
+
function resolveTemplatesDir() {
|
|
398
|
+
const here = path3.dirname(fileURLToPath(import.meta.url));
|
|
399
|
+
const candidates = [
|
|
400
|
+
path3.resolve(here, "..", "templates"),
|
|
401
|
+
path3.resolve(here, "..", "..", "templates"),
|
|
402
|
+
path3.resolve(here, "..", "..", "..", "templates")
|
|
403
|
+
];
|
|
404
|
+
for (const c of candidates) {
|
|
405
|
+
if (fs3.existsSync(c) && fs3.statSync(c).isDirectory()) {
|
|
406
|
+
return c;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
throw new Error(
|
|
410
|
+
`codebyplan: could not locate templates/ directory. Probed:
|
|
411
|
+
${candidates.join(
|
|
412
|
+
"\n "
|
|
413
|
+
)}`
|
|
414
|
+
);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// src/lib/scaffold-ci-workflow.ts
|
|
418
|
+
function substituteTokens(template, tokens) {
|
|
419
|
+
let result = template;
|
|
420
|
+
for (const [token, value] of Object.entries(tokens)) {
|
|
421
|
+
result = result.split(`{{${token}}}`).join(value);
|
|
422
|
+
}
|
|
423
|
+
return result;
|
|
424
|
+
}
|
|
425
|
+
async function detectPnpmVersionFromPackageJson(reader) {
|
|
426
|
+
try {
|
|
427
|
+
const raw = await reader.read("package.json");
|
|
428
|
+
const pkg = JSON.parse(raw);
|
|
429
|
+
const pm = pkg.packageManager;
|
|
430
|
+
if (typeof pm === "string" && pm.startsWith("pnpm@")) {
|
|
431
|
+
const version = pm.slice("pnpm@".length).split("+")[0];
|
|
432
|
+
if (version && /^\d/.test(version)) {
|
|
433
|
+
return version;
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
return "10";
|
|
437
|
+
} catch {
|
|
438
|
+
return "10";
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
async function detectStrictEnforcedFromCiJson(reader) {
|
|
442
|
+
try {
|
|
443
|
+
const raw = await reader.read(".codebyplan/ci.json");
|
|
444
|
+
const parsed = JSON.parse(raw);
|
|
445
|
+
return parsed.workflow?.strict_check_enforced === true;
|
|
446
|
+
} catch {
|
|
447
|
+
return false;
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
async function renderCiWorkflowContent(opts) {
|
|
451
|
+
const projectDir = path4.resolve(opts?.projectDir ?? process.cwd());
|
|
452
|
+
const reader = opts?.reader ?? new LocalFsReader(projectDir);
|
|
453
|
+
const pnpmVersion = opts?.pnpmVersion ?? await detectPnpmVersionFromPackageJson(reader);
|
|
454
|
+
const nodeVersion = opts?.nodeVersion ?? "22";
|
|
455
|
+
const strictEnforced = opts?.strictEnforced ?? await detectStrictEnforcedFromCiJson(reader);
|
|
456
|
+
const templatesDir = opts?.templatesDir ?? resolveTemplatesDir();
|
|
457
|
+
const templatePath = path4.join(templatesDir, "github-workflows", "ci.yml");
|
|
458
|
+
if (!fs4.existsSync(templatePath)) {
|
|
459
|
+
throw new Error(
|
|
460
|
+
`scaffold-ci-workflow: template not found at ${templatePath}`
|
|
461
|
+
);
|
|
462
|
+
}
|
|
463
|
+
const rawTemplate = fs4.readFileSync(templatePath, "utf-8");
|
|
464
|
+
return substituteTokens(rawTemplate, {
|
|
465
|
+
PNPM_VERSION: pnpmVersion,
|
|
466
|
+
NODE_VERSION: nodeVersion,
|
|
467
|
+
STRICT_NAME_SUFFIX: strictEnforced ? "" : " (report-only)",
|
|
468
|
+
STRICT_CONTINUE_ON_ERROR_LINE: strictEnforced ? "" : "\n continue-on-error: true"
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
async function runScaffoldCiWorkflow(opts) {
|
|
472
|
+
const dryRun = opts?.dryRun ?? false;
|
|
473
|
+
const force = opts?.force ?? false;
|
|
474
|
+
const projectDir = path4.resolve(opts?.projectDir ?? process.cwd());
|
|
475
|
+
const renderedContent = await renderCiWorkflowContent(opts);
|
|
476
|
+
const targetPath = path4.join(projectDir, ".github", "workflows", "ci.yml");
|
|
477
|
+
if (dryRun) {
|
|
478
|
+
return { status: "dry_run", path: targetPath };
|
|
479
|
+
}
|
|
480
|
+
if (fs4.existsSync(targetPath)) {
|
|
481
|
+
const existingContent = fs4.readFileSync(targetPath, "utf-8");
|
|
482
|
+
if (existingContent === renderedContent) {
|
|
483
|
+
return {
|
|
484
|
+
status: "skipped",
|
|
485
|
+
path: targetPath,
|
|
486
|
+
reason: "already up to date"
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
if (!force) {
|
|
490
|
+
throw new Error(
|
|
491
|
+
`scaffold-ci-workflow: ${targetPath} already exists and differs from the template. Pass --force to overwrite.`
|
|
492
|
+
);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
const targetDir = path4.dirname(targetPath);
|
|
496
|
+
fs4.mkdirSync(targetDir, { recursive: true });
|
|
497
|
+
const tmpPath = targetPath + ".tmp";
|
|
498
|
+
try {
|
|
499
|
+
fs4.writeFileSync(tmpPath, renderedContent, "utf-8");
|
|
500
|
+
fs4.renameSync(tmpPath, targetPath);
|
|
501
|
+
} catch (err) {
|
|
502
|
+
try {
|
|
503
|
+
fs4.unlinkSync(tmpPath);
|
|
504
|
+
} catch {
|
|
505
|
+
}
|
|
506
|
+
throw err;
|
|
507
|
+
}
|
|
508
|
+
return { status: "written", path: targetPath };
|
|
509
|
+
}
|
|
510
|
+
export {
|
|
511
|
+
LocalFsReader,
|
|
512
|
+
PLATFORM_COMMAND_MAP,
|
|
513
|
+
buildDefaultCiConfig,
|
|
514
|
+
detectPlatforms,
|
|
515
|
+
renderCiWorkflowContent,
|
|
516
|
+
runCiInit,
|
|
517
|
+
runScaffoldCiWorkflow
|
|
518
|
+
};
|