@bensandee/tooling 0.8.1 → 0.9.0

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.
Files changed (2) hide show
  1. package/dist/bin.mjs +151 -96
  2. package/package.json +2 -2
package/dist/bin.mjs CHANGED
@@ -437,29 +437,31 @@ function writeFile(targetDir, relativePath, content) {
437
437
  function createContext(config, confirmOverwrite) {
438
438
  const archivedFiles = [];
439
439
  const pkgRaw = readFile(config.targetDir, "package.json");
440
- return {
441
- ctx: {
442
- config,
443
- targetDir: config.targetDir,
444
- packageJson: pkgRaw ? parsePackageJson(pkgRaw) : void 0,
445
- exists: (rel) => fileExists(config.targetDir, rel),
446
- read: (rel) => readFile(config.targetDir, rel),
447
- write: (rel, content) => {
448
- if (!rel.startsWith(".tooling-archived/")) {
449
- const existing = readFile(config.targetDir, rel);
450
- if (existing !== void 0 && existing !== content) {
451
- writeFile(config.targetDir, `.tooling-archived/${rel}`, existing);
452
- archivedFiles.push(rel);
453
- }
440
+ const ctx = {
441
+ config,
442
+ targetDir: config.targetDir,
443
+ packageJson: pkgRaw ? parsePackageJson(pkgRaw) : void 0,
444
+ exists: (rel) => fileExists(config.targetDir, rel),
445
+ read: (rel) => readFile(config.targetDir, rel),
446
+ write: (rel, content) => {
447
+ if (!rel.startsWith(".tooling-archived/")) {
448
+ const existing = readFile(config.targetDir, rel);
449
+ if (existing !== void 0 && existing !== content) {
450
+ writeFile(config.targetDir, `.tooling-archived/${rel}`, existing);
451
+ archivedFiles.push(rel);
454
452
  }
455
- writeFile(config.targetDir, rel, content);
456
- },
457
- remove: (rel) => {
458
- const fullPath = path.join(config.targetDir, rel);
459
- if (existsSync(fullPath)) rmSync(fullPath);
460
- },
461
- confirmOverwrite
453
+ }
454
+ writeFile(config.targetDir, rel, content);
455
+ if (rel === "package.json") ctx.packageJson = parsePackageJson(content);
456
+ },
457
+ remove: (rel) => {
458
+ const fullPath = path.join(config.targetDir, rel);
459
+ if (existsSync(fullPath)) rmSync(fullPath);
462
460
  },
461
+ confirmOverwrite
462
+ };
463
+ return {
464
+ ctx,
463
465
  archivedFiles
464
466
  };
465
467
  }
@@ -472,20 +474,22 @@ function createDryRunContext(config) {
472
474
  const pkgRaw = readFile(config.targetDir, "package.json");
473
475
  const pendingWrites = /* @__PURE__ */ new Map();
474
476
  const shadow = /* @__PURE__ */ new Map();
475
- return {
476
- ctx: {
477
- config,
478
- targetDir: config.targetDir,
479
- packageJson: pkgRaw ? parsePackageJson(pkgRaw) : void 0,
480
- exists: (rel) => shadow.has(rel) || fileExists(config.targetDir, rel),
481
- read: (rel) => shadow.get(rel) ?? readFile(config.targetDir, rel),
482
- write: (rel, content) => {
483
- pendingWrites.set(rel, content);
484
- shadow.set(rel, content);
485
- },
486
- remove: () => {},
487
- confirmOverwrite: async () => "overwrite"
477
+ const ctx = {
478
+ config,
479
+ targetDir: config.targetDir,
480
+ packageJson: pkgRaw ? parsePackageJson(pkgRaw) : void 0,
481
+ exists: (rel) => shadow.has(rel) || fileExists(config.targetDir, rel),
482
+ read: (rel) => shadow.get(rel) ?? readFile(config.targetDir, rel),
483
+ write: (rel, content) => {
484
+ pendingWrites.set(rel, content);
485
+ shadow.set(rel, content);
486
+ if (rel === "package.json") ctx.packageJson = parsePackageJson(content);
488
487
  },
488
+ remove: () => {},
489
+ confirmOverwrite: async () => "overwrite"
490
+ };
491
+ return {
492
+ ctx,
489
493
  pendingWrites
490
494
  };
491
495
  }
@@ -562,8 +566,8 @@ function addReleaseDeps(deps, config) {
562
566
  function getAddedDevDepNames(config) {
563
567
  const deps = { ...ROOT_DEV_DEPS };
564
568
  if (config.structure !== "monorepo") Object.assign(deps, PER_PACKAGE_DEV_DEPS);
565
- deps["@bensandee/config"] = "0.7.0";
566
- deps["@bensandee/tooling"] = "0.8.1";
569
+ deps["@bensandee/config"] = "0.7.1";
570
+ deps["@bensandee/tooling"] = "0.9.0";
567
571
  if (config.formatter === "oxfmt") deps["oxfmt"] = "0.35.0";
568
572
  if (config.formatter === "prettier") deps["prettier"] = "3.8.1";
569
573
  addReleaseDeps(deps, config);
@@ -583,8 +587,8 @@ async function generatePackageJson(ctx) {
583
587
  if (ctx.config.releaseStrategy !== "none") allScripts["trigger-release"] = "pnpm exec tooling release:trigger";
584
588
  const devDeps = { ...ROOT_DEV_DEPS };
585
589
  if (!isMonorepo) Object.assign(devDeps, PER_PACKAGE_DEV_DEPS);
586
- devDeps["@bensandee/config"] = isWorkspacePackage(ctx, "@bensandee/config") ? "workspace:*" : "0.7.0";
587
- devDeps["@bensandee/tooling"] = isWorkspacePackage(ctx, "@bensandee/tooling") ? "workspace:*" : "0.8.1";
590
+ devDeps["@bensandee/config"] = isWorkspacePackage(ctx, "@bensandee/config") ? "workspace:*" : "0.7.1";
591
+ devDeps["@bensandee/tooling"] = isWorkspacePackage(ctx, "@bensandee/tooling") ? "workspace:*" : "0.9.0";
588
592
  if (ctx.config.useEslintPlugin) devDeps["@bensandee/eslint-plugin"] = isWorkspacePackage(ctx, "@bensandee/eslint-plugin") ? "workspace:*" : "0.9.0";
589
593
  if (ctx.config.formatter === "oxfmt") devDeps["oxfmt"] = "0.35.0";
590
594
  if (ctx.config.formatter === "prettier") devDeps["prettier"] = "3.8.1";
@@ -2058,15 +2062,107 @@ async function generateLefthook(ctx) {
2058
2062
  const SCHEMA_NPM_PATH = "@bensandee/config/schemas/forgejo-workflow.schema.json";
2059
2063
  const SCHEMA_LOCAL_PATH = ".vscode/forgejo-workflow.schema.json";
2060
2064
  const SETTINGS_PATH = ".vscode/settings.json";
2065
+ const SCHEMA_GLOB = ".forgejo/workflows/*.yml";
2061
2066
  const VscodeSettingsSchema = z.object({ "yaml.schemas": z.record(z.string(), z.unknown()).default({}) }).passthrough();
2067
+ const FullWorkspaceFileSchema = z.object({ settings: z.record(z.string(), z.unknown()).default({}) }).passthrough();
2062
2068
  function readSchemaFromNodeModules(targetDir) {
2063
2069
  const candidate = path.join(targetDir, "node_modules", SCHEMA_NPM_PATH);
2064
2070
  if (!existsSync(candidate)) return void 0;
2065
2071
  return readFileSync(candidate, "utf-8");
2066
2072
  }
2073
+ /** Find a *.code-workspace file in the target directory. */
2074
+ function findWorkspaceFile(targetDir) {
2075
+ try {
2076
+ return readdirSync(targetDir).find((e) => e.endsWith(".code-workspace"));
2077
+ } catch {
2078
+ return;
2079
+ }
2080
+ }
2067
2081
  function serializeSettings(settings) {
2068
2082
  return JSON.stringify(settings, null, 2) + "\n";
2069
2083
  }
2084
+ const YamlSchemasSchema = z.record(z.string(), z.unknown());
2085
+ /** Merge yaml.schemas into a settings object. Returns the result and whether anything changed. */
2086
+ function mergeYamlSchemas(settings) {
2087
+ const parsed = YamlSchemasSchema.safeParse(settings["yaml.schemas"]);
2088
+ const yamlSchemas = parsed.success ? { ...parsed.data } : {};
2089
+ if (SCHEMA_LOCAL_PATH in yamlSchemas) return {
2090
+ merged: settings,
2091
+ changed: false
2092
+ };
2093
+ yamlSchemas[SCHEMA_LOCAL_PATH] = SCHEMA_GLOB;
2094
+ return {
2095
+ merged: {
2096
+ ...settings,
2097
+ "yaml.schemas": yamlSchemas
2098
+ },
2099
+ changed: true
2100
+ };
2101
+ }
2102
+ function writeSchemaToWorkspaceFile(ctx, wsFileName) {
2103
+ const raw = ctx.read(wsFileName);
2104
+ if (!raw) return {
2105
+ filePath: wsFileName,
2106
+ action: "skipped",
2107
+ description: "Could not read workspace file"
2108
+ };
2109
+ const fullParsed = FullWorkspaceFileSchema.safeParse(parse(raw));
2110
+ if (!fullParsed.success) return {
2111
+ filePath: wsFileName,
2112
+ action: "skipped",
2113
+ description: "Could not parse workspace file"
2114
+ };
2115
+ const { merged, changed } = mergeYamlSchemas(fullParsed.data.settings);
2116
+ if (!changed) return {
2117
+ filePath: wsFileName,
2118
+ action: "skipped",
2119
+ description: "Already has Forgejo schema mapping"
2120
+ };
2121
+ const updated = {
2122
+ ...fullParsed.data,
2123
+ settings: merged
2124
+ };
2125
+ ctx.write(wsFileName, JSON.stringify(updated, null, 2) + "\n");
2126
+ return {
2127
+ filePath: wsFileName,
2128
+ action: "updated",
2129
+ description: "Added Forgejo workflow schema mapping to workspace settings"
2130
+ };
2131
+ }
2132
+ function writeSchemaToSettings(ctx) {
2133
+ if (ctx.exists(SETTINGS_PATH)) {
2134
+ const raw = ctx.read(SETTINGS_PATH);
2135
+ if (!raw) return {
2136
+ filePath: SETTINGS_PATH,
2137
+ action: "skipped",
2138
+ description: "Could not read existing settings"
2139
+ };
2140
+ const parsed = VscodeSettingsSchema.safeParse(parse(raw));
2141
+ if (!parsed.success) return {
2142
+ filePath: SETTINGS_PATH,
2143
+ action: "skipped",
2144
+ description: "Could not parse existing settings"
2145
+ };
2146
+ const { merged, changed } = mergeYamlSchemas(parsed.data);
2147
+ if (!changed) return {
2148
+ filePath: SETTINGS_PATH,
2149
+ action: "skipped",
2150
+ description: "Already has Forgejo schema mapping"
2151
+ };
2152
+ ctx.write(SETTINGS_PATH, serializeSettings(merged));
2153
+ return {
2154
+ filePath: SETTINGS_PATH,
2155
+ action: "updated",
2156
+ description: "Added Forgejo workflow schema mapping"
2157
+ };
2158
+ }
2159
+ ctx.write(SETTINGS_PATH, serializeSettings({ "yaml.schemas": { [SCHEMA_LOCAL_PATH]: SCHEMA_GLOB } }));
2160
+ return {
2161
+ filePath: SETTINGS_PATH,
2162
+ action: "created",
2163
+ description: "Generated .vscode/settings.json with Forgejo workflow schema"
2164
+ };
2165
+ }
2070
2166
  async function generateVscodeSettings(ctx) {
2071
2167
  const results = [];
2072
2168
  if (ctx.config.ci !== "forgejo") {
@@ -2100,54 +2196,8 @@ async function generateVscodeSettings(ctx) {
2100
2196
  description: "Copied Forgejo workflow schema from @bensandee/config"
2101
2197
  });
2102
2198
  }
2103
- const schemaGlob = ".forgejo/workflows/*.yml";
2104
- if (ctx.exists(SETTINGS_PATH)) {
2105
- const raw = ctx.read(SETTINGS_PATH);
2106
- if (!raw) {
2107
- results.push({
2108
- filePath: SETTINGS_PATH,
2109
- action: "skipped",
2110
- description: "Could not read existing settings"
2111
- });
2112
- return results;
2113
- }
2114
- const parsed = VscodeSettingsSchema.safeParse(JSON.parse(raw));
2115
- if (!parsed.success) {
2116
- results.push({
2117
- filePath: SETTINGS_PATH,
2118
- action: "skipped",
2119
- description: "Could not parse existing settings"
2120
- });
2121
- return results;
2122
- }
2123
- const existing = parsed.data;
2124
- const yamlSchemas = existing["yaml.schemas"];
2125
- if (SCHEMA_LOCAL_PATH in yamlSchemas) {
2126
- results.push({
2127
- filePath: SETTINGS_PATH,
2128
- action: "skipped",
2129
- description: "Already has Forgejo schema mapping"
2130
- });
2131
- return results;
2132
- }
2133
- yamlSchemas[SCHEMA_LOCAL_PATH] = schemaGlob;
2134
- ctx.write(SETTINGS_PATH, serializeSettings({
2135
- ...existing,
2136
- "yaml.schemas": yamlSchemas
2137
- }));
2138
- results.push({
2139
- filePath: SETTINGS_PATH,
2140
- action: "updated",
2141
- description: "Added Forgejo workflow schema mapping"
2142
- });
2143
- } else {
2144
- ctx.write(SETTINGS_PATH, serializeSettings({ "yaml.schemas": { [SCHEMA_LOCAL_PATH]: schemaGlob } }));
2145
- results.push({
2146
- filePath: SETTINGS_PATH,
2147
- action: "created",
2148
- description: "Generated .vscode/settings.json with Forgejo workflow schema"
2149
- });
2150
- }
2199
+ const wsFile = findWorkspaceFile(ctx.targetDir);
2200
+ results.push(wsFile ? writeSchemaToWorkspaceFile(ctx, wsFile) : writeSchemaToSettings(ctx));
2151
2201
  return results;
2152
2202
  }
2153
2203
  //#endregion
@@ -2355,7 +2405,7 @@ async function runInit(config, options = {}) {
2355
2405
  const promptPath = ".tooling-migrate.md";
2356
2406
  ctx.write(promptPath, prompt);
2357
2407
  p.log.info(`Migration prompt written to ${promptPath}`);
2358
- p.log.info("Paste its contents into Claude Code to finish the migration.");
2408
+ p.log.info("In Claude Code, run: \"Execute the steps in .tooling-migrate.md\"");
2359
2409
  }
2360
2410
  const bensandeeDeps = getAddedDevDepNames(config).filter((name) => name.startsWith("@bensandee/"));
2361
2411
  const hasLockfile = ctx.exists("pnpm-lock.yaml");
@@ -2376,7 +2426,7 @@ async function runInit(config, options = {}) {
2376
2426
  "2. Run: pnpm typecheck",
2377
2427
  "3. Run: pnpm build",
2378
2428
  "4. Run: pnpm test",
2379
- ...options.noPrompt ? [] : ["5. Paste .tooling-migrate.md into Claude Code for cleanup"]
2429
+ ...options.noPrompt ? [] : ["5. In Claude Code, run: \"Execute the steps in .tooling-migrate.md\""]
2380
2430
  ].join("\n"), "Next steps");
2381
2431
  return results;
2382
2432
  }
@@ -2398,9 +2448,12 @@ const updateCommand = defineCommand({
2398
2448
  });
2399
2449
  async function runUpdate(targetDir) {
2400
2450
  const saved = loadToolingConfig(targetDir);
2401
- const detected = buildDefaultConfig(targetDir, {});
2402
- if (!saved) p.log.warn("No .tooling.json found — using detected defaults. Run `tooling repo:init` to save your preferences.");
2403
- return runInit(saved ? mergeWithSavedConfig(detected, saved) : detected, {
2451
+ if (!saved) {
2452
+ p.log.error("No .tooling.json found. Run `tooling repo:init` first to initialize the project.");
2453
+ process.exitCode = 1;
2454
+ return [];
2455
+ }
2456
+ return runInit(mergeWithSavedConfig(buildDefaultConfig(targetDir, {}), saved), {
2404
2457
  noPrompt: true,
2405
2458
  confirmOverwrite: async () => "overwrite"
2406
2459
  });
@@ -2424,9 +2477,11 @@ const checkCommand = defineCommand({
2424
2477
  });
2425
2478
  async function runCheck(targetDir) {
2426
2479
  const saved = loadToolingConfig(targetDir);
2427
- const detected = buildDefaultConfig(targetDir, {});
2428
- if (!saved) p.log.warn("No .tooling.json found — using detected defaults. Run `tooling repo:init` to save your preferences.");
2429
- const { ctx, pendingWrites } = createDryRunContext(saved ? mergeWithSavedConfig(detected, saved) : detected);
2480
+ if (!saved) {
2481
+ p.log.error("No .tooling.json found. Run `tooling repo:init` first to initialize the project.");
2482
+ return 1;
2483
+ }
2484
+ const { ctx, pendingWrites } = createDryRunContext(mergeWithSavedConfig(buildDefaultConfig(targetDir, {}), saved));
2430
2485
  const actionable = (await runGenerators(ctx)).filter((r) => r.action === "created" || r.action === "updated");
2431
2486
  if (actionable.length === 0) {
2432
2487
  p.log.success("Repository is up to date.");
@@ -3152,7 +3207,7 @@ function mergeGitHub(dryRun) {
3152
3207
  const main = defineCommand({
3153
3208
  meta: {
3154
3209
  name: "tooling",
3155
- version: "0.8.1",
3210
+ version: "0.9.0",
3156
3211
  description: "Bootstrap and maintain standardized TypeScript project tooling"
3157
3212
  },
3158
3213
  subCommands: {
@@ -3165,7 +3220,7 @@ const main = defineCommand({
3165
3220
  "release:merge": releaseMergeCommand
3166
3221
  }
3167
3222
  });
3168
- console.log(`@bensandee/tooling v0.8.1`);
3223
+ console.log(`@bensandee/tooling v0.9.0`);
3169
3224
  runMain(main);
3170
3225
  //#endregion
3171
3226
  export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bensandee/tooling",
3
- "version": "0.8.1",
3
+ "version": "0.9.0",
4
4
  "description": "CLI tool to bootstrap and maintain standardized TypeScript project tooling",
5
5
  "bin": {
6
6
  "tooling": "./dist/bin.mjs"
@@ -33,7 +33,7 @@
33
33
  "tsdown": "0.21.0",
34
34
  "typescript": "5.9.3",
35
35
  "vitest": "4.0.18",
36
- "@bensandee/config": "0.7.0"
36
+ "@bensandee/config": "0.7.1"
37
37
  },
38
38
  "scripts": {
39
39
  "build": "tsdown",