lintlane 3.0.1 → 4.0.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.
package/bin/cli.mjs CHANGED
@@ -4,13 +4,17 @@ import { chmod, mkdir, readFile, writeFile } from "node:fs/promises";
4
4
  import { dirname, join } from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
6
  import { parseArgs } from "node:util";
7
+ import { resolveChecks } from "./checks.mjs";
7
8
  import { binPath, resolveBiome, runInherit, runProcess } from "./proc.mjs";
8
9
  import {
10
+ COMMITIZEN_PATH,
11
+ classifyHook,
9
12
  detectPackageManager,
10
13
  detectPreset,
11
- hasUnsafeRestaging,
14
+ hasCommitlintConfig,
15
+ hasLintStagedConfig,
12
16
  installCommand,
13
- isLegacyPreCommit,
17
+ LINT_STAGED_CONFIG,
14
18
  PRESET_PATHS,
15
19
  renderWorkflow,
16
20
  runnerCommand,
@@ -36,11 +40,14 @@ Commands:
36
40
  check [paths] Run biome check (defaults to .)
37
41
  check:fix [paths] Run biome check --write (defaults to .)
38
42
  ci [paths] Run biome ci --reporter=github (defaults to .)
39
- commit [options] Guided commit (alias: c): asks step by step, then commits as a
40
- Conventional Commit. Flags: --dry-run, --retry, --no-retry,
41
- --write-message-to-file <f>, -l <n>; git flags go after --
42
- check-commit Validate commit messages (--message, --commit-msg-file,
43
- --rev-range, -d, --allow-abort, --allowed-prefixes, -l)
43
+ run [checks] Run the project's checks (Biome, typecheck, build, tests) in
44
+ order, the same list CI and the dashboard use. Flags:
45
+ --keep-going (don't stop at the first failure), --list
46
+ doctor [--json] Diagnose the setup: tools, hooks, configs, CI. Read-only
47
+ commit [options] Guided Conventional Commit (alias: c), run by Commitizen;
48
+ git flags go after --
49
+ check-commit Validate commit messages with commitlint (--message,
50
+ --commit-msg-file, --rev-range, -d)
44
51
  ui [options] Open the local dashboard: run checks, review errors and
45
52
  proposed fixes as a diff, apply fixes per file
46
53
 
@@ -53,6 +60,7 @@ init options:
53
60
  --no-hooks Skip husky + pre-commit hook setup
54
61
  --no-commit-lint Skip the commit-message hooks (commit-msg, prepare-commit-msg, pre-push)
55
62
  --no-ci Skip the GitHub Actions workflow
63
+ --dry-run Show everything init would do; change nothing
56
64
  --skip-install Print install commands instead of running them
57
65
  -h, --help Show this help
58
66
 
@@ -102,6 +110,10 @@ const { values, positionals } = parseArgs({
102
110
  "no-ci": { type: "boolean", default: false },
103
111
  "no-commit-lint": { type: "boolean", default: false },
104
112
  "skip-install": { type: "boolean", default: false },
113
+ "dry-run": { type: "boolean", default: false },
114
+ json: { type: "boolean", default: false },
115
+ "keep-going": { type: "boolean", default: false },
116
+ list: { type: "boolean", default: false },
105
117
  port: { type: "string" },
106
118
  "no-open": { type: "boolean", default: false },
107
119
  help: { type: "boolean", short: "h", default: false },
@@ -140,6 +152,71 @@ if (command === "check-commit") {
140
152
  process.exit(await main(commandArgs, cwd));
141
153
  }
142
154
 
155
+ if (command === "doctor") {
156
+ const { formatReport, runDoctor } = await import("./doctor.mjs");
157
+ const report = await runDoctor({ cwd, kitDir: KIT_DIR });
158
+ console.log(values.json ? JSON.stringify(report, null, 2) : formatReport(report));
159
+ process.exit(report.ok ? 0 : 1);
160
+ }
161
+
162
+ if (command === "hook") {
163
+ const { runHook } = await import("./hook.mjs");
164
+ const at = rawArgs.indexOf("hook");
165
+ const [name, ...hookArgs] = rawArgs.slice(at + 1);
166
+ process.exit(await runHook(name, hookArgs, cwd));
167
+ }
168
+
169
+ if (command === "run") {
170
+ const biome = resolveBiome(cwd);
171
+ if (!biome) fail(`Biome isn't installed in this project. Run: ${RUNNER} ${KIT_NAME} init`);
172
+ const pkg = existsSync(join(cwd, "package.json"))
173
+ ? await readJson(join(cwd, "package.json"))
174
+ : {};
175
+ const { steps, problems } = resolveChecks({
176
+ cwd,
177
+ biome,
178
+ pmName: detectPackageManager(cwd, pkg).name,
179
+ pkg,
180
+ });
181
+ for (const problem of problems) warn(problem);
182
+ if (values.list) {
183
+ for (const step of steps) {
184
+ console.log(`${step.id.padEnd(10)} ${step.defaultOn ? "on " : "off"} ${step.display}`);
185
+ }
186
+ process.exit(0);
187
+ }
188
+ const wanted = positionals.slice(1);
189
+ const unknown = wanted.filter((id) => !steps.some((step) => step.id === id));
190
+ if (unknown.length)
191
+ fail(`Unknown check: ${unknown.join(", ")}. Available: ${steps.map((st) => st.id).join(", ")}`);
192
+ const chosen = wanted.length
193
+ ? steps.filter((step) => wanted.includes(step.id))
194
+ : steps.filter((step) => step.defaultOn);
195
+ if (chosen.length === 0) fail("No checks to run.");
196
+ const { runCiSteps } = await import("./checks.mjs");
197
+ const results = [];
198
+ const byId = new Map(steps.map((step) => [step.id, step]));
199
+ await runCiSteps({
200
+ cwd,
201
+ steps: chosen,
202
+ stopOnFail: !values["keep-going"],
203
+ send: (event) => {
204
+ if (event.type === "start")
205
+ console.log(`▶ ${byId.get(event.id).label} (${byId.get(event.id).display})`);
206
+ else if (event.type === "log") process.stdout.write(event.text);
207
+ else if (event.type === "end") {
208
+ results.push(event);
209
+ console.log(
210
+ `${event.code === 0 ? "✔" : "✘"} ${byId.get(event.id).label} (${(event.ms / 1000).toFixed(1)}s)`,
211
+ );
212
+ } else if (event.type === "skip") console.log(`- skipped ${byId.get(event.id).label}`);
213
+ },
214
+ });
215
+ process.exit(
216
+ results.every((result) => result.code === 0) && results.length === chosen.length ? 0 : 1,
217
+ );
218
+ }
219
+
143
220
  if (command === "ui") {
144
221
  const biome = resolveBiome(cwd);
145
222
  if (!biome) {
@@ -166,7 +243,7 @@ if (command === "ui") {
166
243
 
167
244
  if (command !== "init") {
168
245
  fail(
169
- `Unknown command "${command}". Usage: ${RUNNER} ${KIT_NAME} <init|check|check:fix|ci|commit|check-commit|ui> [options]`,
246
+ `Unknown command "${command}". Usage: ${RUNNER} ${KIT_NAME} <init|check|check:fix|ci|run|doctor|commit|check-commit|ui> [options]`,
170
247
  );
171
248
  }
172
249
 
@@ -196,16 +273,38 @@ if (preset === undefined) {
196
273
  }
197
274
  }
198
275
 
276
+ const dryRun = values["dry-run"];
277
+ if (dryRun) console.log("Dry run: nothing will be changed.\n");
278
+
279
+ // Every change goes through act(): in a dry run it only says what it would do.
280
+ async function act(message, fn) {
281
+ if (dryRun) {
282
+ console.log(`+ would ${message}`);
283
+ return;
284
+ }
285
+ await fn();
286
+ ok(message[0].toUpperCase() + message.slice(1));
287
+ }
288
+
199
289
  // LINTLANE_INSTALL_SPEC lets you install lintlane from somewhere other than the
200
290
  // registry (a tarball from `npm pack`, a local folder) when testing a build.
201
291
  const kitSpec = process.env.LINTLANE_INSTALL_SPEC || `${KIT_NAME}@${KIT_VERSION}`;
202
- const devPackages = ["@biomejs/biome", kitSpec];
203
- if (!values["no-hooks"]) devPackages.push("husky");
204
- const installArgv = installCommand(pmName, devPackages);
205
-
206
- if (values["skip-install"]) {
207
- warn(`Skipped install. Run: ${installArgv.join(" ")}`);
292
+ const declared = { ...pkg.dependencies, ...pkg.devDependencies };
293
+ // Only install what's missing, so an existing Biome or husky version is never bumped.
294
+ const devPackages = [];
295
+ if (!("@biomejs/biome" in declared)) devPackages.push("@biomejs/biome");
296
+ if (process.env.LINTLANE_INSTALL_SPEC || declared[KIT_NAME] !== KIT_VERSION)
297
+ devPackages.push(kitSpec);
298
+ if (!values["no-hooks"] && !("husky" in declared)) devPackages.push("husky");
299
+
300
+ if (devPackages.length === 0) {
301
+ ok("Dependencies already installed");
302
+ } else if (values["skip-install"] || dryRun) {
303
+ const argv = installCommand(pmName, devPackages).join(" ");
304
+ if (dryRun) console.log(`+ would run: ${argv}`);
305
+ else warn(`Skipped install. Run: ${argv}`);
208
306
  } else {
307
+ const installArgv = installCommand(pmName, devPackages);
209
308
  const install = await runProcess(installArgv[0], installArgv.slice(1), { cwd });
210
309
  if (install.code !== 0) {
211
310
  const reason =
@@ -228,7 +327,7 @@ const biomeJsoncPath = join(cwd, "biome.jsonc");
228
327
  const extendsSpecifier = `${KIT_NAME}/${PRESET_PATHS[preset]}`;
229
328
 
230
329
  if (existsSync(biomeJsonPath) || existsSync(biomeJsoncPath)) {
231
- warn(`biome.json already exists — add this to its "extends": "${extendsSpecifier}"`);
330
+ warn(`biome.json already exists — make sure its "extends" includes "${extendsSpecifier}"`);
232
331
  } else {
233
332
  const config = {
234
333
  $schema: "./node_modules/@biomejs/biome/configuration_schema.json",
@@ -237,45 +336,86 @@ if (existsSync(biomeJsonPath) || existsSync(biomeJsoncPath)) {
237
336
  if (preset === "next") {
238
337
  config.files = { includes: ["**", "!**/.next", "!**/out", "!**/next-env.d.ts"] };
239
338
  }
240
- await writeFile(biomeJsonPath, `${JSON.stringify(config, null, 2)}\n`);
241
- ok(`Wrote biome.json (extends ${extendsSpecifier})`);
339
+ await act(`write biome.json (extends ${extendsSpecifier})`, () =>
340
+ writeFile(biomeJsonPath, `${JSON.stringify(config, null, 2)}\n`),
341
+ );
242
342
  }
243
343
 
344
+ // package.json: scripts, lint-staged, Commitizen. Only ever adds what's missing.
244
345
  pkg.scripts ??= {};
245
- let pkgChanged = false;
346
+ const pkgEdits = [];
347
+ const addScript = (name, value) => {
348
+ if (!pkg.scripts[name]) {
349
+ pkgEdits.push(() => {
350
+ pkg.scripts[name] = value;
351
+ });
352
+ return `add script "${name}"`;
353
+ }
354
+ warn(`Script "${name}" already exists — left untouched`);
355
+ return null;
356
+ };
357
+ const pending = [
358
+ addScript("check", "biome check ."),
359
+ addScript("check:fix", "biome check --write ."),
360
+ ];
246
361
 
247
- if (!pkg.scripts.check) {
248
- pkg.scripts.check = "biome check .";
249
- pkgChanged = true;
250
- ok('Added script "check"');
251
- } else {
252
- warn('Script "check" already exists — left untouched');
253
- }
362
+ if (!values["no-hooks"]) {
363
+ if (!pkg.scripts.prepare) pending.push(addScript("prepare", "husky"));
364
+ else if (!pkg.scripts.prepare.includes("husky")) {
365
+ pkgEdits.push(() => {
366
+ pkg.scripts.prepare = `${pkg.scripts.prepare} && husky`;
367
+ });
368
+ pending.push('append husky to the existing "prepare" script');
369
+ } else {
370
+ warn('Script "prepare" already runs husky — left untouched');
371
+ }
254
372
 
255
- if (!pkg.scripts["check:fix"]) {
256
- pkg.scripts["check:fix"] = "biome check --write .";
257
- pkgChanged = true;
258
- ok('Added script "check:fix"');
259
- } else {
260
- warn('Script "check:fix" already exists — left untouched');
373
+ if (hasLintStagedConfig(cwd, pkg)) {
374
+ warn("lint-staged is already configured — left untouched");
375
+ } else {
376
+ pkgEdits.push(() => {
377
+ pkg["lint-staged"] = LINT_STAGED_CONFIG;
378
+ });
379
+ pending.push('add "lint-staged" config (Biome on staged files)');
380
+ }
261
381
  }
262
382
 
263
- if (!values["no-hooks"]) {
264
- if (!pkg.scripts.prepare) {
265
- pkg.scripts.prepare = "husky";
266
- pkgChanged = true;
267
- ok('Added script "prepare"');
268
- } else if (!pkg.scripts.prepare.includes("husky")) {
269
- pkg.scripts.prepare = `${pkg.scripts.prepare} && husky`;
270
- pkgChanged = true;
271
- ok('Appended husky to existing "prepare" script');
383
+ if (!values["no-commit-lint"]) {
384
+ if (pkg.config?.commitizen?.path) {
385
+ warn("Commitizen is already configured — left untouched");
272
386
  } else {
273
- warn('Script "prepare" already runs husky — left untouched');
387
+ pkgEdits.push(() => {
388
+ pkg.config = {
389
+ ...pkg.config,
390
+ commitizen: { ...pkg.config?.commitizen, path: COMMITIZEN_PATH },
391
+ };
392
+ });
393
+ pending.push('add "config.commitizen" (guided commits)');
274
394
  }
275
395
  }
276
396
 
277
- if (pkgChanged) {
397
+ for (const message of pending.filter(Boolean)) {
398
+ if (dryRun) console.log(`+ would ${message} in package.json`);
399
+ }
400
+ if (pkgEdits.length > 0 && !dryRun) {
401
+ for (const edit of pkgEdits) edit();
278
402
  await writeFile(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`);
403
+ for (const message of pending.filter(Boolean))
404
+ ok(`${message[0].toUpperCase()}${message.slice(1)}`);
405
+ }
406
+
407
+ // commitlint config
408
+ if (!values["no-commit-lint"]) {
409
+ if (hasCommitlintConfig(cwd, pkg)) {
410
+ warn("commitlint is already configured — left untouched");
411
+ } else {
412
+ await act("write commitlint.config.mjs", () =>
413
+ writeFile(
414
+ join(cwd, "commitlint.config.mjs"),
415
+ 'export { default } from "lintlane/commitlint";\n',
416
+ ),
417
+ );
418
+ }
279
419
  }
280
420
 
281
421
  if (!values["no-hooks"]) {
@@ -291,30 +431,33 @@ if (!values["no-hooks"]) {
291
431
  for (const hook of hooks) {
292
432
  const hookPath = join(huskyDir, hook);
293
433
  const template = await readFile(join(KIT_DIR, "templates", hook), "utf8");
294
- if (existsSync(hookPath)) {
295
- const existing = await readFile(hookPath, "utf8");
296
- if (hook === "pre-commit" && isLegacyPreCommit(existing)) {
297
- // The pre-2.6 hook re-staged files with `git update-index --again`, which also
298
- // stages unstaged hunks of partially staged files. It's ours and untouched, so replace it.
299
- await writeFile(hookPath, template);
300
- await chmod(hookPath, 0o755);
301
- ok("Replaced .husky/pre-commit (the old one could stage changes you hadn't staged)");
302
- wroteHook = true;
303
- } else if (hook === "pre-commit" && hasUnsafeRestaging(existing)) {
304
- warn(
305
- ".husky/pre-commit runs `git update-index --again`, which can stage changes you didn't stage. " +
306
- "Replace its contents with node_modules/lintlane/templates/pre-commit (or delete it and re-run init).",
307
- );
308
- } else {
309
- warn(`.husky/${hook} already exists — left untouched`);
310
- }
434
+ const writeHook = async () => {
435
+ await mkdir(huskyDir, { recursive: true });
436
+ await writeFile(hookPath, template);
437
+ await chmod(hookPath, 0o755);
438
+ wroteHook = true;
439
+ };
440
+ if (!existsSync(hookPath)) {
441
+ await act(`write .husky/${hook}`, writeHook);
311
442
  continue;
312
443
  }
313
- await mkdir(huskyDir, { recursive: true });
314
- await writeFile(hookPath, template);
315
- await chmod(hookPath, 0o755);
316
- ok(`Wrote .husky/${hook}`);
317
- wroteHook = true;
444
+ const state = classifyHook(hook, await readFile(hookPath, "utf8"), template);
445
+ if (state === "current") {
446
+ ok(`.husky/${hook} is up to date`);
447
+ } else if (state === "legacy") {
448
+ // Untouched hook from an older lintlane; the files it called are gone.
449
+ await act(`upgrade .husky/${hook} (the old one is from an earlier lintlane)`, writeHook);
450
+ } else if (state === "stale" || state === "unsafe") {
451
+ warn(
452
+ `.husky/${hook} was edited and still calls files lintlane no longer ships` +
453
+ (state === "unsafe"
454
+ ? " (and `git update-index --again` can stage changes you didn't stage)"
455
+ : "") +
456
+ `. Replace its contents with node_modules/lintlane/templates/${hook}.`,
457
+ );
458
+ } else {
459
+ warn(`.husky/${hook} already exists — left untouched`);
460
+ }
318
461
  }
319
462
 
320
463
  if (wroteHook) {
@@ -330,6 +473,8 @@ if (!values["no-hooks"]) {
330
473
  warn(`husky binary not found at ${husky} — run it manually once installed`);
331
474
  }
332
475
  }
476
+ } else if (dryRun) {
477
+ console.log("+ would run husky to point git at .husky");
333
478
  }
334
479
  }
335
480
  }
@@ -338,13 +483,25 @@ if (!values["no-ci"]) {
338
483
  const workflowsDir = join(cwd, ".github", "workflows");
339
484
  const ciPath = join(workflowsDir, "biome.yml");
340
485
  if (existsSync(ciPath)) {
341
- warn(".github/workflows/biome.yml already exists — left untouched");
486
+ warn(
487
+ ".github/workflows/biome.yml already exists — left untouched (run `lintlane doctor` to compare it with your checks)",
488
+ );
342
489
  } else {
343
490
  const withCommitCheck = !values["no-commit-lint"] && !values["no-hooks"];
344
- await mkdir(workflowsDir, { recursive: true });
345
- await writeFile(ciPath, renderWorkflow({ pmName, kitName: KIT_NAME, withCommitCheck }));
346
- ok("Wrote .github/workflows/biome.yml");
491
+ const biomeArgv = resolveBiome(cwd) ?? ["biome"];
492
+ const { steps: checks } = resolveChecks({ cwd, biome: biomeArgv, pmName, pkg });
493
+ await act("write .github/workflows/biome.yml", async () => {
494
+ await mkdir(workflowsDir, { recursive: true });
495
+ await writeFile(
496
+ ciPath,
497
+ renderWorkflow({ pmName, kitName: KIT_NAME, withCommitCheck, checks }),
498
+ );
499
+ });
347
500
  }
348
501
  }
349
502
 
350
- console.log(`\nRun: ${pmName} run check:fix`);
503
+ console.log(
504
+ dryRun
505
+ ? "\nDry run finished. Run again without --dry-run to apply."
506
+ : `\nRun: ${pmName} run check:fix (and \`${RUNNER} ${KIT_NAME} doctor\` to verify the setup)`,
507
+ );
@@ -1,148 +1,62 @@
1
- // `lintlane commit` — guided Conventional Commit (Commitizen's `cz commit`).
1
+ // `lintlane commit` — guided Conventional Commit, run by Commitizen.
2
2
  //
3
- // lintlane commit [options] [-- <git commit args>]
3
+ // lintlane commit [commitizen options] [-- <git commit args>]
4
4
  //
5
- // --dry-run show the message, don't commit
6
- // --write-message-to-file <file> write the message to a file
7
- // --retry / --no-retry reuse the last message after a failed commit / force a new prompt
8
- // -l, --message-length-limit <n> max header length (0 = no limit)
9
- // -- <args> everything after -- goes to `git commit`
10
- // (e.g. `-- -a -S` to stage all and sign, `-- -n` to skip hooks)
11
- import { spawnSync } from "node:child_process";
12
- import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
13
- import { dirname, resolve } from "node:path";
14
- import { parseArgs } from "node:util";
15
- import { loadCommitConfig } from "./commit-config.mjs";
16
- import { promptCommit } from "./commit-prompt.mjs";
5
+ // Commitizen's own flags work as-is: --dry-run, --retry, -a, -S, ... and
6
+ // everything after -- goes to `git commit` (e.g. `-- -n` to skip hooks).
7
+ import { existsSync, readFileSync } from "node:fs";
8
+ import { join } from "node:path";
9
+ import { resolveTool, runInherit } from "./proc.mjs";
17
10
 
18
11
  export const COMMIT_HELP = `Usage: lintlane commit [options] [-- <git commit args>]
19
12
 
20
- Asks a few questions and creates a Conventional Commit. Alias: lintlane c
13
+ Asks a few questions and creates a Conventional Commit (Commitizen). Alias: lintlane c
21
14
 
22
- Options:
23
- --dry-run Show the message without committing
24
- --write-message-to-file <file> Write the message to a file (combine with --dry-run)
25
- --retry Reuse the last message after a failed commit
26
- --no-retry Always ask, even if retryAfterFailure is set
27
- -l, --message-length-limit <n> Maximum header length (0 = no limit)
15
+ Options (passed to Commitizen):
16
+ --dry-run Show what would happen without committing
17
+ --retry Reuse the last commit message after a failed commit
18
+ -a, --all Stage all tracked changes first
19
+ -S, --signoff Sign off the commit
28
20
 
29
21
  Anything after -- is passed to git commit:
30
- lintlane commit -- -a -S stage all tracked changes and sign the commit
31
- lintlane commit -- -n skip git hooks
32
- lintlane commit -- -s add a Signed-off-by line
22
+ lintlane commit -- -S sign the commit
23
+ lintlane commit -- -n skip git hooks
24
+
25
+ Needs "config.commitizen.path" in package.json; \`lintlane init\` adds it.
33
26
  `;
34
27
 
35
28
  const err = (text) => process.stderr.write(`${text}\n`);
36
29
 
37
- function git(args, cwd, extra = {}) {
38
- return spawnSync("git", args, { cwd, encoding: "utf8", ...extra });
30
+ function hasAdapter(cwd) {
31
+ try {
32
+ const pkg = JSON.parse(readFileSync(join(cwd, "package.json"), "utf8"));
33
+ return Boolean(pkg?.config?.commitizen?.path) || existsSync(join(cwd, ".czrc"));
34
+ } catch {
35
+ return false;
36
+ }
39
37
  }
40
38
 
41
39
  export async function runCommit({ argv, gitArgs = [], cwd = process.cwd() }) {
42
- let values;
43
- try {
44
- ({ values } = parseArgs({
45
- args: argv,
46
- options: {
47
- "dry-run": { type: "boolean" },
48
- "write-message-to-file": { type: "string" },
49
- retry: { type: "boolean" },
50
- "no-retry": { type: "boolean" },
51
- "message-length-limit": { type: "string", short: "l" },
52
- help: { type: "boolean", short: "h" },
53
- },
54
- }));
55
- } catch (error) {
56
- err(`✘ ${error.message}\n\n${COMMIT_HELP}`);
57
- return 2;
58
- }
59
- if (values.help) {
40
+ if (argv.includes("-h") || argv.includes("--help")) {
60
41
  process.stdout.write(COMMIT_HELP);
61
42
  return 0;
62
43
  }
63
- if (values.retry && values["no-retry"]) {
64
- err("✘ Use either --retry or --no-retry, not both.");
65
- return 2;
66
- }
67
-
68
- const inRepo = git(["rev-parse", "--git-dir"], cwd);
69
- if (inRepo.status !== 0) {
70
- err("✘ Not inside a git repository.");
44
+ const cz = resolveTool("commitizen", "git-cz");
45
+ if (!cz) {
46
+ err("✘ Commitizen isn't installed. Reinstall lintlane with your package manager.");
71
47
  return 1;
72
48
  }
73
-
74
- const config = loadCommitConfig(cwd);
75
- const options = {
76
- ...config,
77
- messageLengthLimit: values["message-length-limit"] ?? config.messageLengthLimit,
78
- };
79
- const dryRun = Boolean(values["dry-run"]);
80
-
81
- // `-- -a` makes git stage tracked changes itself, so "nothing staged" is fine then.
82
- const stagesItself = gitArgs.some((a) => a === "-a" || a === "--all");
83
- if (!dryRun && !stagesItself && git(["diff", "--cached", "--quiet"], cwd).status === 0) {
49
+ if (!hasAdapter(cwd)) {
84
50
  err(
85
- "✘ Nothing is staged. Stage your changes first (git add <files>), or use: lintlane commit -- -a",
51
+ '✘ Commitizen has no adapter configured. Run `lintlane init`, or add to package.json:\n "config": { "commitizen": { "path": "lintlane/presets/commitizen.cjs" } }',
86
52
  );
87
53
  return 1;
88
54
  }
89
-
90
- const savedPath = resolve(
91
- cwd,
92
- git(["rev-parse", "--git-path", "lintlane-commit-msg"], cwd).stdout.trim(),
93
- );
94
- const saved = existsSync(savedPath) ? readFileSync(savedPath, "utf8") : null;
95
-
96
- let message;
97
- const autoRetry = Boolean(config.retryAfterFailure) && !values["no-retry"] && saved !== null;
98
- if (values.retry || autoRetry) {
99
- if (saved === null) {
100
- err("✘ There's no previous commit message to retry.");
101
- return 1;
102
- }
103
- message = saved;
104
- err("↻ Reusing your last commit message:\n");
105
- err(message.replace(/^/gm, " "));
106
- err("");
107
- } else {
108
- try {
109
- message = await promptCommit({ options });
110
- } catch {
111
- err("\n✘ Commit cancelled.");
112
- return 1;
113
- }
114
- if (message === null) {
115
- err("\n✘ Commit cancelled.");
116
- return 1;
117
- }
118
- }
119
-
120
- if (values["write-message-to-file"]) {
121
- const target = resolve(cwd, values["write-message-to-file"]);
122
- mkdirSync(dirname(target), { recursive: true });
123
- writeFileSync(target, `${message}\n`);
124
- }
125
- if (dryRun) {
126
- process.stdout.write(`${message}\n`);
127
- return 0;
128
- }
129
-
130
- // Keep the message until the commit actually succeeds, so a failing
131
- // pre-commit hook doesn't throw away your answers.
132
- mkdirSync(dirname(savedPath), { recursive: true });
133
- writeFileSync(savedPath, message);
134
-
135
- const result = spawnSync("git", ["commit", "-F", "-", ...gitArgs], {
136
- cwd,
137
- input: message,
138
- stdio: ["pipe", "inherit", "inherit"],
139
- });
140
- if (result.status === 0) {
141
- rmSync(savedPath, { force: true });
142
- return 0;
143
- }
144
- err(
145
- "\n✘ git commit failed. Your message was saved — fix the problem, then run: lintlane commit --retry",
55
+ return runInherit(
56
+ cz[0],
57
+ [...cz.slice(1), ...argv, ...(gitArgs.length ? ["--", ...gitArgs] : [])],
58
+ {
59
+ cwd,
60
+ },
146
61
  );
147
- return result.status ?? 1;
148
62
  }