pm4ai 0.0.73

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.

Potentially problematic release.


This version of pm4ai might be problematic. Click here for more details.

Files changed (51) hide show
  1. package/README.md +34 -0
  2. package/dist/audit-oQQfgtxr.mjs +287 -0
  3. package/dist/cleanup-M-ALxTqh.mjs +35 -0
  4. package/dist/cli.d.mts +1 -0
  5. package/dist/cli.mjs +77 -0
  6. package/dist/dashboard-DVRNZGun.mjs +39 -0
  7. package/dist/discover-d8ENQC1K.mjs +172 -0
  8. package/dist/fix-BcMN_cuG.mjs +260 -0
  9. package/dist/fix-DvtItv_V.mjs +2 -0
  10. package/dist/guide-BS7-RqpH.d.mts +4 -0
  11. package/dist/guide-CifUmtQN.mjs +59 -0
  12. package/dist/guide.d.mts +2 -0
  13. package/dist/guide.mjs +2 -0
  14. package/dist/ignores-BBl55eUM.mjs +37 -0
  15. package/dist/index.d.mts +83 -0
  16. package/dist/index.mjs +11 -0
  17. package/dist/init-C-073mRX.mjs +120 -0
  18. package/dist/list-QdJPgkEO.mjs +31 -0
  19. package/dist/package-NpIViQjo.mjs +4 -0
  20. package/dist/schemas-Dsbtf6P2.mjs +51 -0
  21. package/dist/schemas.d.mts +48 -0
  22. package/dist/schemas.mjs +2 -0
  23. package/dist/setup-BPuE4oWT.mjs +164 -0
  24. package/dist/status-ByiuW1iF.mjs +2 -0
  25. package/dist/status-CzCNkG58.mjs +1775 -0
  26. package/dist/sync-DN1rgN3P.mjs +732 -0
  27. package/dist/templates/cli/package.json +30 -0
  28. package/dist/templates/cli/src/cli.ts +16 -0
  29. package/dist/templates/cli/src/index.ts +2 -0
  30. package/dist/templates/cli/src/tui.tsx +57 -0
  31. package/dist/templates/cli/tsdown.config.ts +9 -0
  32. package/dist/templates/docs/content/docs/index.mdx +6 -0
  33. package/dist/templates/docs/package.json +20 -0
  34. package/dist/templates/docs/source.config.ts +16 -0
  35. package/dist/templates/docs/src/app/(home)/page.tsx +11 -0
  36. package/dist/templates/lib/package.json +22 -0
  37. package/dist/templates/lib/src/index.ts +2 -0
  38. package/dist/templates/lib/tsdown.config.ts +9 -0
  39. package/dist/templates/root-package.txt +38 -0
  40. package/dist/templates/web/package.json +17 -0
  41. package/dist/templates/web/src/app/page.tsx +6 -0
  42. package/dist/templates/web/src/app/providers.tsx +15 -0
  43. package/dist/utils-CpkOMuQN.mjs +221 -0
  44. package/dist/watch-D4OSFClu.mjs +566 -0
  45. package/dist/watch-emitter-uTmZ3oQd.mjs +177 -0
  46. package/dist/watch-state-DIMHiLr5.d.mts +118 -0
  47. package/dist/watch-state-wF-NfC-k.mjs +274 -0
  48. package/dist/watch-state.d.mts +2 -0
  49. package/dist/watch-state.mjs +2 -0
  50. package/dist/watch-types-BzSNCGb2.mjs +22 -0
  51. package/package.json +65 -0
@@ -0,0 +1,732 @@
1
+ import { C as DEFAULT_FILES, E as EXPECTED, L as TSDOWN_BASE, N as READONLY_UI, P as REQUIRED_ROOT_DEVDEPS, S as DEFAULT_DEP_VERSION, T as DEFAULT_SCRIPTS, V as filterTurboWorkspaceWarning, c as gitCleanRe, d as isSkippedPath, g as resolveManagedFiles, l as isExtended, m as readPkg, n as collectWorkspacePackages, o as getGhRepo, p as readJson, s as getTsconfigTypes, t as buildPkgDepMap, v as writeJson, w as DEFAULT_LICENSE, y as CLAUDE_MD } from "./utils-CpkOMuQN.mjs";
2
+ import { i as DEP_FIELDS, n as isPublishedPkg, r as ALL_DEP_FIELDS } from "./audit-oQQfgtxr.mjs";
3
+ import { $, file, write } from "bun";
4
+ import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, readlinkSync, rmSync, writeFileSync } from "node:fs";
5
+ import { dirname, join } from "node:path";
6
+ //#region src/infer.ts
7
+ const parseFrontmatter = (content) => {
8
+ if (!content.startsWith("---")) return {};
9
+ const endIdx = content.indexOf("---", 3);
10
+ if (endIdx === -1) return {};
11
+ const fm = {};
12
+ for (const line of content.slice(3, endIdx).trim().split("\n")) {
13
+ const colon = line.indexOf(":");
14
+ if (colon > 0) fm[line.slice(0, colon).trim()] = line.slice(colon + 1).trim();
15
+ }
16
+ return fm;
17
+ };
18
+ const getRulesDir = () => {
19
+ return [join(import.meta.dir, "..", "..", "..", "apps", "web", "content", "rules"), join(import.meta.dir, "..", "apps", "web", "content", "rules")].find((c) => existsSync(c));
20
+ };
21
+ const getAllDeps = async (projectPath) => {
22
+ const deps = /* @__PURE__ */ new Set();
23
+ const entries = await collectWorkspacePackages(projectPath);
24
+ for (const { pkg } of entries) for (const field of ALL_DEP_FIELDS) {
25
+ const d = pkg[field];
26
+ if (d) for (const name of Object.keys(d)) deps.add(name);
27
+ }
28
+ return deps;
29
+ };
30
+ const inferRules = async (projectPath, rulesDir) => {
31
+ const dir = rulesDir ?? getRulesDir();
32
+ if (!(dir && existsSync(dir))) return [];
33
+ const deps = await getAllDeps(projectPath);
34
+ const rules = [];
35
+ const mdxFiles = readdirSync(dir).filter((f) => f.endsWith(".mdx"));
36
+ const indexFile = mdxFiles.find((f) => f === "index.mdx");
37
+ const sorted = [...indexFile ? [indexFile] : [], ...mdxFiles.filter((f) => f !== "index.mdx").toSorted()];
38
+ for (const mdxFile of sorted) {
39
+ const { infer } = parseFrontmatter(readFileSync(join(dir, mdxFile), "utf8"));
40
+ if (infer && (infer === "always" || deps.has(infer))) rules.push(mdxFile.replace(".mdx", ""));
41
+ }
42
+ return rules;
43
+ };
44
+ //#endregion
45
+ //#region src/sync.ts
46
+ /** biome-ignore-all lint/performance/noDelete: must delete pkg keys */
47
+ /** biome-ignore-all lint/nursery/noContinue: loop control flow */
48
+ const sortKeys = (obj) => Object.fromEntries(Object.entries(obj).toSorted(([a], [b]) => a.localeCompare(b)));
49
+ const stripFrontmatter = (content) => {
50
+ if (!content.startsWith("---")) return content;
51
+ const endIdx = content.indexOf("---", 3);
52
+ if (endIdx === -1) return content;
53
+ return content.slice(endIdx + 3).trim();
54
+ };
55
+ const syncConfigs = async (selfPath, projectPath) => {
56
+ const managed = await resolveManagedFiles(projectPath);
57
+ return (await Promise.all(managed.map(async ({ extendable, path: name }) => {
58
+ const src = file(join(selfPath, name));
59
+ const dst = file(join(projectPath, name));
60
+ if (!await src.exists()) return;
61
+ const srcContent = await src.text();
62
+ const dstContent = await dst.exists() ? await dst.text() : "";
63
+ if (extendable && isExtended(srcContent, dstContent)) return;
64
+ if (srcContent !== dstContent) {
65
+ await write(dst, srcContent);
66
+ return {
67
+ detail: `${name} updated`,
68
+ type: "synced"
69
+ };
70
+ }
71
+ }))).filter((r) => r !== void 0);
72
+ };
73
+ const canonicaliseViaLintmax = async (rawContent, relName, projectPath) => {
74
+ const lintmaxBin = join(projectPath, "node_modules", ".bin", "lintmax");
75
+ if (!existsSync(lintmaxBin)) return rawContent;
76
+ const cacheDir = join(projectPath, "node_modules", ".cache", "canon");
77
+ if (!existsSync(cacheDir)) mkdirSync(cacheDir, { recursive: true });
78
+ const tmpRel = join("node_modules", ".cache", "canon", relName.replaceAll("/", "_"));
79
+ const tmpPath = join(projectPath, tmpRel);
80
+ await write(file(tmpPath), rawContent);
81
+ await $`${lintmaxBin} fix ${tmpRel}`.cwd(projectPath).quiet().nothrow();
82
+ const formatted = await file(tmpPath).text();
83
+ try {
84
+ rmSync(tmpPath);
85
+ } catch {}
86
+ return formatted;
87
+ };
88
+ const syncClaudeMd = async (selfPath, projectPath) => {
89
+ const issues = [];
90
+ const rulesDir = join(selfPath, "apps", "docs", "content", "rules");
91
+ if (!existsSync(rulesDir)) {
92
+ issues.push({
93
+ detail: "rules directory not found in pm4ai repo",
94
+ type: "error"
95
+ });
96
+ return issues;
97
+ }
98
+ const inferred = await inferRules(projectPath, rulesDir);
99
+ const generated = await canonicaliseViaLintmax(`${(await Promise.all(inferred.map(async (rule) => file(join(rulesDir, `${rule}.mdx`)).text()))).map((c) => stripFrontmatter(c)).join("\n\n---\n\n")}\n`, CLAUDE_MD, projectPath);
100
+ const claudeFile = file(join(projectPath, CLAUDE_MD));
101
+ if (generated !== (await claudeFile.exists() ? await claudeFile.text() : "")) {
102
+ await write(claudeFile, generated);
103
+ issues.push({
104
+ detail: `${CLAUDE_MD} updated`,
105
+ type: "synced"
106
+ });
107
+ }
108
+ return issues;
109
+ };
110
+ const isFixScriptCanonical = (script, fallback) => {
111
+ const value = script ?? "";
112
+ return value.endsWith(fallback) || value.endsWith("cli.js fix") || value.endsWith("cli.mjs fix");
113
+ };
114
+ const syncRootScripts = (scripts, issues) => {
115
+ let changed = false;
116
+ for (const [name, value] of Object.entries(DEFAULT_SCRIPTS)) if (name === "postinstall") {
117
+ if (!scripts.postinstall?.includes("sherif")) {
118
+ scripts.postinstall = scripts.postinstall ? `${scripts.postinstall} && sherif` : DEFAULT_SCRIPTS.postinstall;
119
+ changed = true;
120
+ issues.push({
121
+ detail: "added sherif to postinstall",
122
+ type: "synced"
123
+ });
124
+ }
125
+ } else if (name === "fix" ? !isFixScriptCanonical(scripts.fix, value) : scripts[name] !== value) {
126
+ const action = scripts[name] ? "updated" : "added";
127
+ scripts[name] = value;
128
+ changed = true;
129
+ issues.push({
130
+ detail: `${action} ${name} script`,
131
+ type: "synced"
132
+ });
133
+ }
134
+ for (const [name, cmd] of Object.entries(scripts)) {
135
+ if (name.startsWith("dev") || !cmd.includes("turbo") || !cmd.includes("--output-logs=errors-only") || cmd.includes("WARNING|Could not resolve workspaces|missing field|Turborepo will still function")) continue;
136
+ scripts[name] = filterTurboWorkspaceWarning(cmd);
137
+ changed = true;
138
+ issues.push({
139
+ detail: `updated ${name} script to filter turbo workspace warning`,
140
+ type: "synced"
141
+ });
142
+ }
143
+ return changed;
144
+ };
145
+ const syncRootDevDeps = (pkg, devDeps, issues) => {
146
+ let changed = false;
147
+ const allDeps = {
148
+ ...pkg.dependencies,
149
+ ...devDeps
150
+ };
151
+ for (const dep of REQUIRED_ROOT_DEVDEPS) if (!allDeps[dep]) {
152
+ devDeps[dep] = DEFAULT_DEP_VERSION;
153
+ changed = true;
154
+ issues.push({
155
+ detail: `added ${dep} to devDependencies`,
156
+ type: "synced"
157
+ });
158
+ }
159
+ return changed;
160
+ };
161
+ const syncPackageJson = async (projectPath, selfPath) => {
162
+ const issues = [];
163
+ const pkgPath = join(projectPath, "package.json");
164
+ const pkg = await readPkg(pkgPath);
165
+ if (!pkg) return issues;
166
+ const wasPrivate = Boolean(pkg.private);
167
+ if (!pkg.private) {
168
+ pkg.private = true;
169
+ issues.push({
170
+ detail: "set root package.json to private",
171
+ type: "synced"
172
+ });
173
+ }
174
+ const scripts = pkg.scripts ?? {};
175
+ pkg.scripts = scripts;
176
+ const hadHooks = Boolean(pkg["simple-git-hooks"]);
177
+ if (!pkg["simple-git-hooks"]) {
178
+ pkg["simple-git-hooks"] = { "pre-commit": EXPECTED.preCommit };
179
+ issues.push({
180
+ detail: "added simple-git-hooks",
181
+ type: "synced"
182
+ });
183
+ }
184
+ let changed = syncRootScripts(scripts, issues) || !wasPrivate || !hadHooks;
185
+ const devDeps = pkg.devDependencies ?? {};
186
+ pkg.devDependencies = devDeps;
187
+ changed = syncRootDevDeps(pkg, devDeps, issues) || changed;
188
+ const allRootDeps = {
189
+ ...pkg.dependencies,
190
+ ...devDeps
191
+ };
192
+ const allRootDepNames = Object.keys(allRootDeps);
193
+ const required = new Set(REQUIRED_ROOT_DEVDEPS);
194
+ const depPkgs = await Promise.all(allRootDepNames.map(async (depName) => {
195
+ const depPkg = await readPkg(join(projectPath, "node_modules", depName, "package.json"));
196
+ return depPkg ? {
197
+ depName,
198
+ transitive: depPkg.dependencies ?? {}
199
+ } : void 0;
200
+ }));
201
+ for (const dep of depPkgs) {
202
+ if (!dep) continue;
203
+ for (const other of allRootDepNames) if (other !== dep.depName && dep.transitive[other] && devDeps[other] && !required.has(other)) {
204
+ delete devDeps[other];
205
+ changed = true;
206
+ issues.push({
207
+ detail: `removed redundant "${other}" (provided by ${dep.depName})`,
208
+ type: "synced"
209
+ });
210
+ }
211
+ }
212
+ if (!pkg.packageManager) {
213
+ const bunVersion = (await $`bun --version`.quiet().nothrow()).stdout.toString().trim();
214
+ if (bunVersion) {
215
+ pkg.packageManager = `bun@${bunVersion}`;
216
+ changed = true;
217
+ issues.push({
218
+ detail: `set packageManager to bun@${bunVersion}`,
219
+ type: "synced"
220
+ });
221
+ }
222
+ }
223
+ const trusted = pkg.trustedDependencies ?? [];
224
+ let requiredTrusted = [];
225
+ if (selfPath) {
226
+ const selfPkgPath = join(selfPath, "package.json");
227
+ requiredTrusted = JSON.parse(await file(selfPkgPath).text()).trustedDependencies ?? [];
228
+ }
229
+ const missingTrusted = requiredTrusted.filter((d) => !trusted.includes(d));
230
+ if (missingTrusted.length > 0) {
231
+ pkg.trustedDependencies = [...trusted, ...missingTrusted].toSorted();
232
+ changed = true;
233
+ issues.push({
234
+ detail: `added ${missingTrusted.join(", ")} to trustedDependencies`,
235
+ type: "synced"
236
+ });
237
+ }
238
+ if (!scripts.action?.startsWith("sh up.sh")) {
239
+ scripts.action = scripts.action ? `sh up.sh && ${scripts.action}` : "sh up.sh";
240
+ changed = true;
241
+ issues.push({
242
+ detail: "action must start with \"sh up.sh\"",
243
+ type: "synced"
244
+ });
245
+ }
246
+ if (changed) {
247
+ pkg.devDependencies = sortKeys(pkg.devDependencies ?? {});
248
+ await writeJson(pkgPath, pkg);
249
+ }
250
+ return issues;
251
+ };
252
+ const syncTsconfig = async (projectPath) => {
253
+ const issues = [];
254
+ const tsconfigPath = join(projectPath, "tsconfig.json");
255
+ const tsconfig = await readJson(tsconfigPath);
256
+ if (!tsconfig) return issues;
257
+ let changed = false;
258
+ if ("include" in tsconfig) {
259
+ delete tsconfig.include;
260
+ changed = true;
261
+ issues.push({
262
+ detail: "removed \"include\" from root tsconfig.json",
263
+ type: "synced"
264
+ });
265
+ }
266
+ if (tsconfig.extends !== EXPECTED.tsconfigExtends) {
267
+ tsconfig.extends = EXPECTED.tsconfigExtends;
268
+ changed = true;
269
+ issues.push({
270
+ detail: `set tsconfig extends to "${EXPECTED.tsconfigExtends}"`,
271
+ type: "synced"
272
+ });
273
+ }
274
+ if (!getTsconfigTypes(tsconfig)?.includes("bun-types")) {
275
+ const co = tsconfig.compilerOptions ?? {};
276
+ co.types = ["bun-types"];
277
+ tsconfig.compilerOptions = co;
278
+ changed = true;
279
+ issues.push({
280
+ detail: "added \"bun-types\" to tsconfig compilerOptions.types",
281
+ type: "synced"
282
+ });
283
+ }
284
+ if (changed) await writeJson(tsconfigPath, tsconfig);
285
+ return issues;
286
+ };
287
+ const distPrefixRe = /^\.?\/?dist\//u;
288
+ const dotSlashRe = /^\.\//u;
289
+ const mjsExtRe = /\.mjs$/u;
290
+ const monorepoRootRe = /\/(?:packages|tool|lib)\/[^/]+$/u;
291
+ const resolveExportSource = (key, importPath, pkgDir) => {
292
+ if (importPath.endsWith(".json")) return;
293
+ const srcBase = key === "." ? "src/index" : `src/${key.replace(dotSlashRe, "")}`;
294
+ for (const ext of [".ts", ".tsx"]) if (existsSync(join(pkgDir, `${srcBase}${ext}`))) return `${srcBase}${ext}`;
295
+ };
296
+ const inferTsdownConfig = (pkg, pkgDir) => {
297
+ const entry = [];
298
+ const copy = [];
299
+ const { exports } = pkg;
300
+ if (exports) for (const [key, val] of Object.entries(exports)) {
301
+ const importPath = typeof val === "string" ? val : val.import;
302
+ if (!importPath) continue;
303
+ if (importPath.endsWith(".css")) {
304
+ const srcCss = importPath.replace(distPrefixRe, "src/");
305
+ if (existsSync(join(pkgDir, srcCss))) copy.push(srcCss);
306
+ } else {
307
+ const src = resolveExportSource(key, importPath, pkgDir);
308
+ if (src) entry.push(src);
309
+ }
310
+ }
311
+ if (pkg.bin) {
312
+ const bins = typeof pkg.bin === "string" ? { default: pkg.bin } : pkg.bin;
313
+ for (const binPath of Object.values(bins)) {
314
+ const srcPath = binPath.replace(distPrefixRe, "src/").replace(mjsExtRe, ".ts");
315
+ if (existsSync(join(pkgDir, srcPath)) && !entry.includes(srcPath)) entry.push(srcPath);
316
+ }
317
+ }
318
+ if (entry.length === 0 && !pkg.bin) {
319
+ if (existsSync(join(pkgDir, "src/index.ts"))) entry.push("src/index.ts");
320
+ else if (existsSync(join(pkgDir, "src/index.tsx"))) entry.push("src/index.tsx");
321
+ }
322
+ if (entry.length === 0) return;
323
+ const config = { entry };
324
+ if (copy.length > 0) config.copy = copy;
325
+ return config;
326
+ };
327
+ const serializeTsdownConfig = (config) => {
328
+ const fields = [
329
+ ` clean: ${TSDOWN_BASE.clean},`,
330
+ ` deps: { neverBundle: [${TSDOWN_BASE.deps.neverBundle.map((d) => `'${d}'`).join(", ")}] },`,
331
+ ` dts: ${TSDOWN_BASE.dts},`
332
+ ];
333
+ if (config.copy) fields.push(` copy: [${config.copy.map((c) => `'${c}'`).join(", ")}],`);
334
+ fields.push(` entry: [${config.entry.map((e) => `'${e}'`).join(", ")}],`);
335
+ fields.push(` format: '${TSDOWN_BASE.format}',`);
336
+ fields.push(` outDir: '${TSDOWN_BASE.outDir}'`);
337
+ return `import { defineConfig } from 'tsdown'\n\nexport default defineConfig({\n${fields.join("\n")}\n})\n`;
338
+ };
339
+ const syncReadmeSymlink = ({ issues, monorepoRoot, pkgDir, rel }) => {
340
+ const readmeSrc = join(monorepoRoot, "README.md");
341
+ const readmeDst = join(pkgDir, "README.md");
342
+ if (!existsSync(readmeSrc)) return false;
343
+ const srcContent = readFileSync(readmeSrc, "utf8");
344
+ if (existsSync(readmeDst)) {
345
+ let isSymlink;
346
+ try {
347
+ readlinkSync(readmeDst);
348
+ isSymlink = true;
349
+ } catch {
350
+ isSymlink = false;
351
+ }
352
+ if (!isSymlink && readFileSync(readmeDst, "utf8") === srcContent) return false;
353
+ if (isSymlink) rmSync(readmeDst);
354
+ }
355
+ writeFileSync(readmeDst, srcContent);
356
+ issues.push({
357
+ detail: `${rel} synced README.md`,
358
+ type: "synced"
359
+ });
360
+ return true;
361
+ };
362
+ const fixPublishedPkg = ({ issues, pkg, pkgPath, rel, repo }) => {
363
+ let changed = false;
364
+ if (pkg.type !== "module") {
365
+ pkg.type = "module";
366
+ changed = true;
367
+ issues.push({
368
+ detail: `${rel} set "type": "module"`,
369
+ type: "synced"
370
+ });
371
+ }
372
+ if (!pkg.files) {
373
+ pkg.files = DEFAULT_FILES;
374
+ changed = true;
375
+ issues.push({
376
+ detail: `${rel} set "files": ${JSON.stringify(DEFAULT_FILES)}`,
377
+ type: "synced"
378
+ });
379
+ }
380
+ if (!pkg.license) {
381
+ pkg.license = DEFAULT_LICENSE;
382
+ changed = true;
383
+ issues.push({
384
+ detail: `${rel} set "license": "${DEFAULT_LICENSE}"`,
385
+ type: "synced"
386
+ });
387
+ }
388
+ if (!pkg.repository && repo) {
389
+ pkg.repository = {
390
+ directory: dirname(rel),
391
+ type: "git",
392
+ url: `https://github.com/${repo}`
393
+ };
394
+ changed = true;
395
+ issues.push({
396
+ detail: `${rel} added repository`,
397
+ type: "synced"
398
+ });
399
+ }
400
+ const pubScripts = pkg.scripts ?? {};
401
+ if (pubScripts.build && pubScripts.build !== "tsdown") {
402
+ pubScripts.build = "tsdown";
403
+ pkg.scripts = pubScripts;
404
+ changed = true;
405
+ issues.push({
406
+ detail: `${rel} set build to "tsdown"`,
407
+ type: "synced"
408
+ });
409
+ }
410
+ const pkgDir = dirname(pkgPath);
411
+ const tsdownConfigPath = join(pkgDir, "tsdown.config.ts");
412
+ const tsdownConfig = inferTsdownConfig(pkg, pkgDir);
413
+ if (tsdownConfig) {
414
+ const existingContent = existsSync(tsdownConfigPath) ? readFileSync(tsdownConfigPath, "utf8") : "";
415
+ if (existingContent.includes("dts: false") || existingContent.includes("onSuccess")) {} else {
416
+ const generatedContent = serializeTsdownConfig(tsdownConfig);
417
+ if (existingContent !== generatedContent) {
418
+ writeFileSync(tsdownConfigPath, generatedContent);
419
+ issues.push({
420
+ detail: `${rel} generated tsdown.config.ts`,
421
+ type: "synced"
422
+ });
423
+ }
424
+ }
425
+ }
426
+ syncReadmeSymlink({
427
+ issues,
428
+ monorepoRoot: pkgDir.replace(monorepoRootRe, ""),
429
+ pkgDir,
430
+ rel
431
+ });
432
+ const expectedPostpublish = "bun ../../tools/prune-versions.ts";
433
+ if (pubScripts.postpublish !== expectedPostpublish) {
434
+ pubScripts.postpublish = expectedPostpublish;
435
+ delete pubScripts["cleanup-old-versions"];
436
+ pkg.scripts = pubScripts;
437
+ changed = true;
438
+ issues.push({
439
+ detail: `${rel} set postpublish to prune-versions script`,
440
+ type: "synced"
441
+ });
442
+ }
443
+ if (pubScripts.build && pubScripts.prepublishOnly !== "bun run build") {
444
+ pubScripts.prepublishOnly = "bun run build";
445
+ pkg.scripts = pubScripts;
446
+ changed = true;
447
+ issues.push({
448
+ detail: `${rel} set prepublishOnly to always build before publish`,
449
+ type: "synced"
450
+ });
451
+ }
452
+ return changed;
453
+ };
454
+ const fixGitClean = (pkg, rel, issues) => {
455
+ let changed = false;
456
+ const scripts = pkg.scripts ?? {};
457
+ for (const [name, cmd] of Object.entries(scripts)) {
458
+ gitCleanRe.lastIndex = 0;
459
+ if (gitCleanRe.test(cmd)) {
460
+ gitCleanRe.lastIndex = 0;
461
+ scripts[name] = cmd.replace(gitCleanRe, "rm -rf ");
462
+ changed = true;
463
+ issues.push({
464
+ detail: `${rel} "${name}" replaced git clean with rm -rf`,
465
+ type: "synced"
466
+ });
467
+ }
468
+ }
469
+ return changed;
470
+ };
471
+ const hoistDevDeps = ({ issues, rel, rootDevDeps, subDevDeps }) => {
472
+ const external = Object.entries(subDevDeps).filter(([, v]) => !v.startsWith("workspace:"));
473
+ if (external.length === 0) return {
474
+ hoisted: false,
475
+ remaining: Object.keys(subDevDeps).length === 0 ? void 0 : subDevDeps
476
+ };
477
+ let hoisted = false;
478
+ for (const [name, version] of external) if (!rootDevDeps[name]) {
479
+ rootDevDeps[name] = version;
480
+ hoisted = true;
481
+ issues.push({
482
+ detail: `hoisted ${name} from ${rel} to root`,
483
+ type: "synced"
484
+ });
485
+ }
486
+ const ws = Object.fromEntries(Object.entries(subDevDeps).filter(([, v]) => v.startsWith("workspace:")));
487
+ return {
488
+ hoisted,
489
+ remaining: Object.keys(ws).length === 0 ? void 0 : ws
490
+ };
491
+ };
492
+ const fixSubEntry = ({ entry, issues, projectPath, repo }) => {
493
+ const rel = entry.path.replace(`${projectPath}/`, "");
494
+ if (isSkippedPath(rel)) return false;
495
+ let changed = false;
496
+ if (rel.startsWith("apps/") && !entry.pkg.private) {
497
+ entry.pkg.private = true;
498
+ changed = true;
499
+ issues.push({
500
+ detail: `${rel} set to private`,
501
+ type: "synced"
502
+ });
503
+ }
504
+ if (isPublishedPkg(entry.pkg)) changed = fixPublishedPkg({
505
+ issues,
506
+ pkg: entry.pkg,
507
+ pkgPath: entry.path,
508
+ rel,
509
+ repo
510
+ }) || changed;
511
+ if (entry.pkg.scripts?.clean) {
512
+ delete entry.pkg.scripts.clean;
513
+ if (Object.keys(entry.pkg.scripts).length === 0) delete entry.pkg.scripts;
514
+ changed = true;
515
+ issues.push({
516
+ detail: `${rel} removed redundant "clean" script`,
517
+ type: "synced"
518
+ });
519
+ }
520
+ return fixGitClean(entry.pkg, rel, issues) || changed;
521
+ };
522
+ const hoistSubEntry = ({ entry, issues, projectPath, rootDevDeps }) => {
523
+ const rel = entry.path.replace(`${projectPath}/`, "");
524
+ const originalDevDeps = entry.pkg.devDependencies;
525
+ const { hoisted, remaining } = hoistDevDeps({
526
+ issues,
527
+ rel,
528
+ rootDevDeps,
529
+ subDevDeps: originalDevDeps ?? {}
530
+ });
531
+ const changed = remaining !== originalDevDeps && !(remaining === void 0 && !originalDevDeps);
532
+ if (changed) entry.pkg.devDependencies = remaining;
533
+ return {
534
+ changed,
535
+ hoisted,
536
+ pkg: entry.pkg,
537
+ pkgPath: entry.path
538
+ };
539
+ };
540
+ const syncSubPackages = async (_selfPath, projectPath) => {
541
+ const issues = [];
542
+ const entries = await collectWorkspacePackages(projectPath);
543
+ const rootPkgPath = join(projectPath, "package.json");
544
+ const repo = await getGhRepo(projectPath);
545
+ const subEntries = entries.filter((e) => e.path !== rootPkgPath);
546
+ const writes = [];
547
+ for (const entry of subEntries) if (fixSubEntry({
548
+ entry,
549
+ issues,
550
+ projectPath,
551
+ repo
552
+ })) writes.push(writeJson(entry.path, entry.pkg));
553
+ const pkgDepsByName = buildPkgDepMap(entries);
554
+ for (const entry of subEntries) {
555
+ const rel = entry.path.replace(`${projectPath}/`, "");
556
+ if (!(isSkippedPath(rel) || rel.startsWith("apps/"))) {
557
+ const wsDeps = [...Object.entries(entry.pkg.dependencies ?? {}), ...Object.entries(entry.pkg.devDependencies ?? {})].filter(([, v]) => v.startsWith("workspace:")).map(([n]) => n);
558
+ const providedByWs = /* @__PURE__ */ new Set();
559
+ for (const ws of wsDeps) for (const d of pkgDepsByName.get(ws) ?? []) providedByWs.add(d);
560
+ let dedupChanged = false;
561
+ for (const field of DEP_FIELDS) {
562
+ const deps = entry.pkg[field];
563
+ if (!deps) continue;
564
+ for (const [name, version] of Object.entries(deps)) if (!version.startsWith("workspace:") && providedByWs.has(name)) {
565
+ delete deps[name];
566
+ dedupChanged = true;
567
+ issues.push({
568
+ detail: `${rel} removed duplicate "${name}" (provided by workspace dep)`,
569
+ type: "synced"
570
+ });
571
+ }
572
+ if (Object.keys(deps).length === 0) delete entry.pkg[field];
573
+ }
574
+ if (dedupChanged) writes.push(writeJson(entry.path, entry.pkg));
575
+ }
576
+ }
577
+ await Promise.all(writes);
578
+ const rootPkg = await readPkg(rootPkgPath);
579
+ if (!rootPkg) return issues;
580
+ let rootChanged = false;
581
+ const rootDevDeps = rootPkg.devDependencies ?? {};
582
+ const subWrites = [];
583
+ const nonSkipped = subEntries.filter((e) => !isSkippedPath(e.path.replace(`${projectPath}/`, "")));
584
+ for (const entry of nonSkipped) {
585
+ const result = hoistSubEntry({
586
+ entry,
587
+ issues,
588
+ projectPath,
589
+ rootDevDeps
590
+ });
591
+ if (result.hoisted) rootChanged = true;
592
+ if (result.changed) subWrites.push(writeJson(result.pkgPath, result.pkg));
593
+ }
594
+ await Promise.all(subWrites);
595
+ if (rootChanged) {
596
+ rootPkg.devDependencies = sortKeys(rootDevDeps);
597
+ await writeJson(rootPkgPath, rootPkg);
598
+ }
599
+ return issues;
600
+ };
601
+ const syncUi = (cnsyncPath, projectPath) => {
602
+ const issues = [];
603
+ const src = join(cnsyncPath, READONLY_UI);
604
+ const dst = join(projectPath, READONLY_UI);
605
+ if (!existsSync(src)) {
606
+ issues.push({
607
+ detail: `${READONLY_UI} not found in cnsync repo`,
608
+ type: "error"
609
+ });
610
+ return issues;
611
+ }
612
+ if (projectPath === cnsyncPath) return issues;
613
+ cpSync(src, dst, { recursive: true });
614
+ for (const ext of [
615
+ "mjs",
616
+ "ts",
617
+ "js"
618
+ ]) {
619
+ const p = join(dst, `postcss.config.${ext}`);
620
+ if (existsSync(p)) rmSync(p);
621
+ }
622
+ issues.push({
623
+ detail: `${READONLY_UI} updated`,
624
+ type: "synced"
625
+ });
626
+ return issues;
627
+ };
628
+ const NEXT_BUILD_RE = /(?<!bunx --bun )\bnext build\b/gu;
629
+ const FUMADOCS_MDX_RE = /(?<!bunx --bun )\bfumadocs-mdx\b/gu;
630
+ const patchFumadocsScript = (script) => script.replace(NEXT_BUILD_RE, "bunx --bun next build").replace(FUMADOCS_MDX_RE, "bunx --bun fumadocs-mdx");
631
+ const syncFumadocsBuild = async (projectPath) => {
632
+ const fumadocsApps = (await collectWorkspacePackages(projectPath)).filter((e) => {
633
+ const allDeps = {
634
+ ...e.pkg.dependencies,
635
+ ...e.pkg.devDependencies
636
+ };
637
+ return Boolean(allDeps["fumadocs-ui"]);
638
+ });
639
+ const issues = [];
640
+ for (const app of fumadocsApps) {
641
+ const scripts = app.pkg.scripts ?? {};
642
+ let changed = false;
643
+ const next = { ...scripts };
644
+ for (const [k, v] of Object.entries(scripts)) {
645
+ const patched = patchFumadocsScript(v);
646
+ if (patched !== v) {
647
+ next[k] = patched;
648
+ changed = true;
649
+ }
650
+ }
651
+ if (changed) {
652
+ writeFileSync(app.path, `${JSON.stringify({
653
+ ...app.pkg,
654
+ scripts: next
655
+ }, null, 2)}\n`);
656
+ const rel = app.path.replace(`${projectPath}/`, "");
657
+ issues.push({
658
+ detail: `${rel} prefixed fumadocs/next build with bunx --bun`,
659
+ type: "synced"
660
+ });
661
+ }
662
+ }
663
+ return issues;
664
+ };
665
+ const getProjectGithubUrl = async (projectPath) => {
666
+ const r = await $`gh repo view --json url -q .url`.cwd(projectPath).quiet().nothrow();
667
+ if (r.exitCode !== 0) return;
668
+ return r.stdout.toString().trim() || void 0;
669
+ };
670
+ const ALIAS_TRIM_RE = /\/\*$/u;
671
+ const ALIAS_DOT_SLASH_RE = /^\.\//u;
672
+ const resolveLibBase = async (appDir) => {
673
+ const aliasTarget = (((await readJson(join(appDir, "tsconfig.json")))?.compilerOptions)?.paths)?.["@/*"]?.[0];
674
+ if (!aliasTarget) return "src";
675
+ return aliasTarget.replace(ALIAS_TRIM_RE, "").replace(ALIAS_DOT_SLASH_RE, "") || ".";
676
+ };
677
+ const layoutSharedRel = (libBase) => libBase === "." ? "lib/layout.shared.tsx" : `${libBase}/lib/layout.shared.tsx`;
678
+ const layoutSharedTemplate = (title, githubUrl) => `import type { BaseLayoutProps } from 'fumadocs-ui/layouts/shared'
679
+ export const baseOptions = (): BaseLayoutProps => ({
680
+ githubUrl: '${githubUrl}',
681
+ nav: {
682
+ title: '${title}'
683
+ }
684
+ })
685
+ `;
686
+ const SCOPED_PREFIX_RE = /^@[^/]+\//u;
687
+ const GITHUB_URL_KEY_RE = /\bgithubUrl\s*:/u;
688
+ const BASEOPTIONS_OPEN_RE = /(?<open>\(\)[^=]*=>\s*\(\{\s*\n)/u;
689
+ const stripScopedPrefix = (name) => name.replace(SCOPED_PREFIX_RE, "");
690
+ const patchSharedFile = ({ githubUrl, projectPath, sharedPath, title }) => {
691
+ const relShared = sharedPath.replace(`${projectPath}/`, "");
692
+ if (!existsSync(sharedPath)) {
693
+ mkdirSync(dirname(sharedPath), { recursive: true });
694
+ writeFileSync(sharedPath, layoutSharedTemplate(title, githubUrl));
695
+ return {
696
+ detail: `${relShared} created with baseOptions + githubUrl`,
697
+ type: "synced"
698
+ };
699
+ }
700
+ const content = readFileSync(sharedPath, "utf8");
701
+ if (GITHUB_URL_KEY_RE.test(content)) return;
702
+ const next = content.replace(BASEOPTIONS_OPEN_RE, `$<open> githubUrl: '${githubUrl}',\n`);
703
+ if (next === content) return;
704
+ writeFileSync(sharedPath, next);
705
+ return {
706
+ detail: `${relShared} added githubUrl`,
707
+ type: "synced"
708
+ };
709
+ };
710
+ const syncFumadocsGithubUrl = async (projectPath) => {
711
+ const fumadocsApps = (await collectWorkspacePackages(projectPath)).filter((e) => {
712
+ const allDeps = {
713
+ ...e.pkg.dependencies,
714
+ ...e.pkg.devDependencies
715
+ };
716
+ return Boolean(allDeps["fumadocs-ui"]);
717
+ });
718
+ if (fumadocsApps.length === 0) return [];
719
+ const githubUrl = await getProjectGithubUrl(projectPath);
720
+ if (!githubUrl) return [];
721
+ return (await Promise.all(fumadocsApps.map(async (app) => {
722
+ const appDir = dirname(app.path);
723
+ return patchSharedFile({
724
+ githubUrl,
725
+ projectPath,
726
+ sharedPath: join(appDir, layoutSharedRel(await resolveLibBase(appDir))),
727
+ title: stripScopedPrefix(app.pkg.name ?? "docs")
728
+ });
729
+ }))).filter((r) => r !== void 0);
730
+ };
731
+ //#endregion
732
+ export { syncPackageJson as a, syncUi as c, syncFumadocsGithubUrl as i, inferRules as l, syncConfigs as n, syncSubPackages as o, syncFumadocsBuild as r, syncTsconfig as s, syncClaudeMd as t };