greenly 1.1.0 → 1.1.1

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/README.md CHANGED
@@ -1,4 +1,7 @@
1
- # greenly
1
+ <h1>
2
+ <img src="https://raw.githubusercontent.com/yusifaliyevpro/greenly/main/assets/greenly.svg" alt="" height="34" align="top" />
3
+ greenly
4
+ </h1>
2
5
 
3
6
  [![npm version](https://img.shields.io/npm/v/greenly?color=3fb950&label=npm)](https://www.npmjs.com/package/greenly)
4
7
  [![npm downloads](https://img.shields.io/npm/dm/greenly?color=3fb950)](https://www.npmjs.com/package/greenly)
@@ -7,17 +10,24 @@
7
10
  [![PR Checks](https://github.com/yusifaliyevpro/greenly/actions/workflows/pr-checks.yml/badge.svg)](https://github.com/yusifaliyevpro/greenly/actions/workflows/pr-checks.yml)
8
11
  [![license](https://img.shields.io/npm/l/greenly?color=3fb950)](https://github.com/yusifaliyevpro/greenly/blob/main/LICENSE)
9
12
 
10
- > Config-driven project check runner. Define your lint / format / typecheck / test steps once in `greenly.config.ts` and run them with a single command.
13
+ > Config-driven project check runner. Define your lint / format / typecheck / test / custom steps once in `greenly.config.ts` and run them with a single command.
11
14
 
12
- Stop copy-pasting a `pr-checks` script into every repo. Describe your checks in a typed config, and `greenly` runs them in order, streams their output, and offers to auto-fix the ones that can be fixed.
15
+ ## Why greenly
16
+
17
+ **greenly runs all your lint, format, typecheck, test, build, and custom checks from one config file with a single command.**
18
+
19
+ Instead of pushing and waiting for CI to catch a formatting slip or a type error, run
20
+ `pnpm greenly` before you open a PR. It puts your CI checks and local checks in the same
21
+ place, so if everything is green locally, it is green in CI. Checks run in order with
22
+ their output streamed live, and greenly offers to auto-fix the ones that have a fixer.
23
+
24
+ It works where your tools do. In an interactive terminal greenly prompts before running a
25
+ fixer; in an **agent terminal or CI (non-TTY)** it skips prompts and just reports pass or
26
+ fail, so it never hangs and an agent can run `greenly` directly.
13
27
 
14
28
  ## Quick start
15
29
 
16
- The fastest way to get going is the interactive scaffolder. It asks for a project
17
- name, config format, script name, and which checks to include, then writes the config,
18
- adds a `"check": "greenly"` script, and installs greenly. It adapts to your project:
19
- it reuses matching `package.json` scripts (e.g. `fmt:check`, `lint`) when they exist,
20
- only offers the lint/format tools you already use, and detects Next.js:
30
+ Run this and greenly sets everything up for you, based on the tools your project already uses:
21
31
 
22
32
  ```bash
23
33
  pnpx greenly init
@@ -71,6 +81,37 @@ Or wire it into your scripts so `pnpm check` works too:
71
81
  - Checks marked `optional: true` warn on failure but never fail the overall run.
72
82
  - greenly exits with code `1` if any non-optional check is still failing, otherwise `0`.
73
83
 
84
+ ## Use it in CI
85
+
86
+ The same command you run locally is the command CI runs. Replace your separate check
87
+ steps with one:
88
+
89
+ ```diff
90
+ - run: pnpm install --frozen-lockfile
91
+
92
+ - - name: TypeScript
93
+ - run: pnpm tsc --noEmit
94
+ -
95
+ - - name: Oxfmt
96
+ - run: pnpm fmt:check
97
+ -
98
+ - - name: Oxlint
99
+ - run: pnpm lint
100
+ -
101
+ - - name: Tests
102
+ - run: pnpm test
103
+ -
104
+ - - name: Build
105
+ - run: pnpm build
106
+ -
107
+ - - name: Version
108
+ - run: node --no-warnings scripts/version-check.ts
109
+ -
110
+ + # pnpm greenly
111
+ + - name: Run checks
112
+ + run: pnpm check
113
+ ```
114
+
74
115
  ## Config reference
75
116
 
76
117
  | Field | Type | Description |
@@ -126,14 +167,14 @@ greenly.config.json
126
167
 
127
168
  ## CLI
128
169
 
129
- | Command / Flag | Description |
130
- | ---------------------- | ---------------------------------------------------------------------------------- |
131
- | `greenly` | Run the checks from `greenly.config.*`. |
132
- | `greenly init` | Scaffold a config interactively (format, script name, checks) and install greenly. |
133
- | `-y`, `--yes`, `--fix` | Auto-run every `onFail` fixer without prompting (great for CI / agents). |
134
- | `--no-fix` | Run all checks, never prompt or fix, just report. |
135
- | `-v`, `--version` | Print the version. |
136
- | `-h`, `--help` | Show help. |
170
+ | Command / Flag | Description |
171
+ | ---------------------- | ------------------------------------------------------------------------ |
172
+ | `greenly` | Run the checks from `greenly.config.*`. |
173
+ | `greenly init` | Set up a config by answering a few questions, then install greenly. |
174
+ | `-y`, `--yes`, `--fix` | Auto-run every `onFail` fixer without prompting (great for CI / agents). |
175
+ | `--no-fix` | Run all checks, never prompt or fix, just report. |
176
+ | `-v`, `--version` | Print the version. |
177
+ | `-h`, `--help` | Show help. |
137
178
 
138
179
  When stdout is **not a TTY** (CI, piped output), greenly is non-interactive by default, so it never prompts and nothing hangs. Use `--yes` there to auto-apply fixes.
139
180
 
package/dist/cli.js CHANGED
@@ -7,7 +7,8 @@ import { exec, execSync } from "node:child_process";
7
7
  import { promisify } from "node:util";
8
8
  import { cancel, confirm, intro, isCancel, log, multiselect, outro, select, spinner, text } from "@clack/prompts";
9
9
  //#region package.json
10
- var version = "1.1.0";
10
+ var name = "greenly";
11
+ var version = "1.1.1";
11
12
  //#endregion
12
13
  //#region src/lib/args.ts
13
14
  function parseArgs(argv) {
@@ -93,6 +94,91 @@ async function loadGreenlyConfig(cwd = process.cwd()) {
93
94
  };
94
95
  }
95
96
  //#endregion
97
+ //#region src/lib/utils.ts
98
+ function installCommand(pm) {
99
+ switch (pm) {
100
+ case "pnpm": return "pnpm add -D greenly@latest";
101
+ case "yarn": return "yarn add -D greenly@latest";
102
+ case "bun": return "bun add -d greenly@latest";
103
+ default: return "npm install -D greenly@latest";
104
+ }
105
+ }
106
+ function detectLockfiles(cwd) {
107
+ return [
108
+ "pnpm-lock.yaml",
109
+ "yarn.lock",
110
+ "package-lock.json",
111
+ "bun.lockb",
112
+ "bun.lock"
113
+ ].filter((f) => existsSync(join(cwd, f)));
114
+ }
115
+ function detectPackageManager(userAgent, lockfiles) {
116
+ const ua = userAgent ?? "";
117
+ if (ua.startsWith("pnpm")) return "pnpm";
118
+ if (ua.startsWith("yarn")) return "yarn";
119
+ if (ua.startsWith("bun")) return "bun";
120
+ if (ua.startsWith("npm")) return "npm";
121
+ if (lockfiles.includes("pnpm-lock.yaml")) return "pnpm";
122
+ if (lockfiles.includes("yarn.lock")) return "yarn";
123
+ if (lockfiles.includes("bun.lockb") || lockfiles.includes("bun.lock")) return "bun";
124
+ return "npm";
125
+ }
126
+ //#endregion
127
+ //#region src/lib/version.ts
128
+ const parse = (v) => {
129
+ const [core = "", pre = ""] = v.trim().replace(/^[v^~>=< ]+/, "").split("-", 2);
130
+ return {
131
+ nums: core.split(".").map((n) => Number.parseInt(n, 10) || 0),
132
+ pre
133
+ };
134
+ };
135
+ function compareVersions(a, b) {
136
+ const pa = parse(a);
137
+ const pb = parse(b);
138
+ for (let i = 0; i < 3; i++) {
139
+ const diff = (pa.nums[i] ?? 0) - (pb.nums[i] ?? 0);
140
+ if (diff !== 0) return diff > 0 ? 1 : -1;
141
+ }
142
+ if (pa.pre === pb.pre) return 0;
143
+ if (pa.pre === "") return 1;
144
+ if (pb.pre === "") return -1;
145
+ return pa.pre > pb.pre ? 1 : -1;
146
+ }
147
+ function isNewer(latest, current) {
148
+ return compareVersions(latest, current) > 0;
149
+ }
150
+ async function fetchLatestVersion(pkg, timeoutMs = 3e3) {
151
+ try {
152
+ const controller = new AbortController();
153
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
154
+ try {
155
+ const res = await fetch(`https://registry.npmjs.org/${encodeURIComponent(pkg)}/latest`, {
156
+ signal: controller.signal,
157
+ headers: { accept: "application/vnd.npm.install-v1+json" }
158
+ });
159
+ if (!res.ok) return null;
160
+ const data = await res.json();
161
+ if (data && typeof data === "object" && "version" in data) {
162
+ const version = data.version;
163
+ return typeof version === "string" ? version : null;
164
+ }
165
+ return null;
166
+ } finally {
167
+ clearTimeout(timer);
168
+ }
169
+ } catch {
170
+ return null;
171
+ }
172
+ }
173
+ async function checkForUpdate(pkg, current) {
174
+ const latest = await fetchLatestVersion(pkg);
175
+ if (latest && isNewer(latest, current)) return {
176
+ current,
177
+ latest
178
+ };
179
+ return null;
180
+ }
181
+ //#endregion
96
182
  //#region src/lib/init.ts
97
183
  const execAsync = promisify(exec);
98
184
  function isRecord(value) {
@@ -102,55 +188,71 @@ function scriptCommand(ctx, candidates) {
102
188
  const found = candidates.find((s) => ctx.scripts.has(s));
103
189
  return found ? `${ctx.run} ${found}` : null;
104
190
  }
191
+ const dep = (name) => (deps) => deps.has(name);
105
192
  const CHECK_PRESETS = [
106
193
  {
107
194
  value: "typescript",
108
195
  label: "TypeScript (tsc)",
196
+ detect: dep("typescript"),
109
197
  build: (c) => ({
110
198
  name: "TypeScript",
111
- command: scriptCommand(c, ["typecheck", "type-check"]) ?? `${c.exec} tsc --noEmit${c.isNext ? " --incremental false" : ""}`
199
+ command: scriptCommand(c, ["typecheck", "type-check"]) ?? `${c.run} tsc --noEmit${dep("next")(c.deps) ? " --incremental false" : ""}`
112
200
  })
113
201
  },
114
202
  {
115
203
  value: "oxfmt",
116
204
  label: "Oxfmt",
205
+ detect: dep("oxfmt"),
117
206
  build: (c) => ({
118
207
  name: "Oxfmt",
119
- command: scriptCommand(c, ["fmt:check", "format:check"]) ?? `${c.exec} oxfmt --check`,
120
- onFail: scriptCommand(c, ["fmt", "format"]) ?? `${c.exec} oxfmt`
208
+ command: scriptCommand(c, ["fmt:check", "format:check"]) ?? `${c.run} oxfmt --check`,
209
+ onFail: scriptCommand(c, ["fmt", "format"]) ?? `${c.run} oxfmt`
121
210
  })
122
211
  },
123
212
  {
124
213
  value: "prettier",
125
214
  label: "Prettier",
215
+ detect: dep("prettier"),
126
216
  build: (c) => ({
127
217
  name: "Prettier",
128
- command: scriptCommand(c, ["fmt:check", "format:check"]) ?? `${c.exec} prettier --check .`,
129
- onFail: scriptCommand(c, ["fmt", "format"]) ?? `${c.exec} prettier --write .`
218
+ command: scriptCommand(c, ["fmt:check", "format:check"]) ?? `${c.run} prettier --check .`,
219
+ onFail: scriptCommand(c, ["fmt", "format"]) ?? `${c.run} prettier --write .`
130
220
  })
131
221
  },
132
222
  {
133
223
  value: "oxlint",
134
224
  label: "Oxlint",
225
+ detect: dep("oxlint"),
135
226
  build: (c) => ({
136
227
  name: "Oxlint",
137
- command: scriptCommand(c, ["lint"]) ?? `${c.exec} oxlint`
228
+ command: scriptCommand(c, ["lint"]) ?? `${c.run} oxlint`
138
229
  })
139
230
  },
140
231
  {
141
232
  value: "eslint",
142
233
  label: "ESLint",
234
+ detect: dep("eslint"),
143
235
  build: (c) => ({
144
236
  name: "ESLint",
145
- command: scriptCommand(c, ["lint"]) ?? `${c.exec} eslint .`
237
+ command: scriptCommand(c, ["lint"]) ?? `${c.run} eslint .`
146
238
  })
147
239
  },
148
240
  {
149
241
  value: "vitest",
150
242
  label: "Tests (Vitest)",
243
+ detect: dep("vitest"),
151
244
  build: (c) => ({
152
245
  name: "Tests",
153
- command: scriptCommand(c, ["test"]) ?? `${c.exec} vitest run`
246
+ command: scriptCommand(c, ["test"]) ?? `${c.run} vitest run`
247
+ })
248
+ },
249
+ {
250
+ value: "expo-doctor",
251
+ label: "Expo Doctor",
252
+ detect: dep("expo"),
253
+ build: (c) => ({
254
+ name: "Expo Doctor",
255
+ command: scriptCommand(c, ["doctor"]) ?? `${c.exec} expo-doctor`
154
256
  })
155
257
  },
156
258
  {
@@ -160,16 +262,25 @@ const CHECK_PRESETS = [
160
262
  name: "Build",
161
263
  command: scriptCommand(c, ["build"]) ?? `${c.run} build`
162
264
  })
265
+ },
266
+ {
267
+ value: "react-doctor",
268
+ label: "React Doctor",
269
+ detect: dep("react"),
270
+ build: (c) => ({
271
+ name: "React Doctor",
272
+ command: `${dep("react-doctor")(c.deps) ? c.run : c.exec} react-doctor --verbose`
273
+ })
163
274
  }
164
275
  ];
165
276
  function pmContext(pm) {
166
277
  switch (pm) {
167
278
  case "pnpm": return {
168
- exec: "pnpm",
279
+ exec: "pnpx",
169
280
  run: "pnpm"
170
281
  };
171
282
  case "yarn": return {
172
- exec: "yarn",
283
+ exec: "yarn dlx",
173
284
  run: "yarn"
174
285
  };
175
286
  case "bun": return {
@@ -182,62 +293,53 @@ function pmContext(pm) {
182
293
  };
183
294
  }
184
295
  }
185
- function installCommand(pm) {
186
- switch (pm) {
187
- case "pnpm": return "pnpm add -D greenly@latest";
188
- case "yarn": return "yarn add -D greenly@latest";
189
- case "bun": return "bun add -d greenly@latest";
190
- default: return "npm install -D greenly@latest";
191
- }
192
- }
193
- function detectPackageManager(userAgent, lockfiles) {
194
- const ua = userAgent ?? "";
195
- if (ua.startsWith("pnpm")) return "pnpm";
196
- if (ua.startsWith("yarn")) return "yarn";
197
- if (ua.startsWith("bun")) return "bun";
198
- if (ua.startsWith("npm")) return "npm";
199
- if (lockfiles.includes("pnpm-lock.yaml")) return "pnpm";
200
- if (lockfiles.includes("yarn.lock")) return "yarn";
201
- if (lockfiles.includes("bun.lockb") || lockfiles.includes("bun.lock")) return "bun";
202
- return "npm";
203
- }
204
296
  function buildChecks(selected, pm, opts = {}) {
205
297
  const ctx = {
206
298
  ...pmContext(pm),
207
- isNext: opts.isNext ?? false,
299
+ deps: opts.deps ?? new Set(),
208
300
  scripts: opts.scripts ?? new Set()
209
301
  };
210
302
  return CHECK_PRESETS.filter((p) => selected.includes(p.value)).map((p) => p.build(ctx));
211
303
  }
212
- function isNextProject(pkg, hasNextConfig) {
213
- if (hasNextConfig) return true;
214
- const hasNextDep = (field) => {
215
- const deps = pkg?.[field];
216
- return isRecord(deps) && "next" in deps;
217
- };
218
- return hasNextDep("dependencies") || hasNextDep("devDependencies");
219
- }
220
304
  function packageScripts(pkg) {
221
305
  const scripts = pkg?.scripts;
222
306
  return isRecord(scripts) ? new Set(Object.keys(scripts)) : new Set();
223
307
  }
308
+ function depRecord(pkg, field) {
309
+ const deps = pkg?.[field];
310
+ return isRecord(deps) ? deps : null;
311
+ }
224
312
  function installedDependencies(pkg) {
225
313
  const names = new Set();
226
314
  for (const field of ["dependencies", "devDependencies"]) {
227
- const deps = pkg?.[field];
228
- if (isRecord(deps)) for (const key of Object.keys(deps)) names.add(key);
315
+ const deps = depRecord(pkg, field);
316
+ if (deps) for (const key of Object.keys(deps)) names.add(key);
229
317
  }
230
318
  return names;
231
319
  }
232
- function availablePresets(installed) {
233
- const hidden = new Set();
234
- const prefer = (a, b) => {
235
- if (installed.has(a) && !installed.has(b)) hidden.add(b);
236
- if (installed.has(b) && !installed.has(a)) hidden.add(a);
320
+ function greenlyLocation(pkg) {
321
+ const inField = (field) => {
322
+ const deps = depRecord(pkg, field);
323
+ return deps !== null && "greenly" in deps;
237
324
  };
238
- prefer("oxlint", "eslint");
239
- prefer("oxfmt", "prettier");
240
- return CHECK_PRESETS.filter((p) => !hidden.has(p.value));
325
+ if (inField("devDependencies")) return "dev";
326
+ if (inField("dependencies")) return "prod";
327
+ return "none";
328
+ }
329
+ function declaredGreenlyVersion(pkg) {
330
+ for (const field of ["devDependencies", "dependencies"]) {
331
+ const version = depRecord(pkg, field)?.greenly;
332
+ if (typeof version === "string") return version;
333
+ }
334
+ return null;
335
+ }
336
+ function shouldOfferInstall(opts) {
337
+ const { location, declaredVersion, latestVersion } = opts;
338
+ if (location === "dev" && declaredVersion && latestVersion && !isNewer(latestVersion, declaredVersion)) return false;
339
+ return true;
340
+ }
341
+ function availablePresets(installed) {
342
+ return CHECK_PRESETS.filter((p) => !p.detect || p.detect(installed));
241
343
  }
242
344
  function configFileName(ext) {
243
345
  return `greenly.config.${ext}`;
@@ -281,25 +383,6 @@ function readPackageJson(path) {
281
383
  } catch {}
282
384
  return null;
283
385
  }
284
- function detectLockfiles(cwd) {
285
- return [
286
- "pnpm-lock.yaml",
287
- "yarn.lock",
288
- "package-lock.json",
289
- "bun.lockb",
290
- "bun.lock"
291
- ].filter((f) => existsSync(join(cwd, f)));
292
- }
293
- function hasNextConfigFile(cwd) {
294
- return [
295
- "js",
296
- "mjs",
297
- "cjs",
298
- "ts",
299
- "mts",
300
- "cts"
301
- ].some((e) => existsSync(join(cwd, `next.config.${e}`)));
302
- }
303
386
  function ensure(value) {
304
387
  if (isCancel(value)) {
305
388
  cancel("init cancelled.");
@@ -313,6 +396,7 @@ async function runInit(cwd = process.cwd()) {
313
396
  const pkg = readPackageJson(pkgPath);
314
397
  const defaultName = typeof pkg?.name === "string" ? pkg.name : basename(cwd);
315
398
  const pm = detectPackageManager(process.env.npm_config_user_agent, detectLockfiles(cwd));
399
+ const latestPromise = fetchLatestVersion("greenly");
316
400
  const name = ensure(await text({
317
401
  message: "Project name (shown in the banner)",
318
402
  initialValue: defaultName,
@@ -331,7 +415,8 @@ async function runInit(cwd = process.cwd()) {
331
415
  initialValue: "check",
332
416
  validate: (value) => value?.trim() ? void 0 : "Please enter a script name"
333
417
  }));
334
- const presets = availablePresets(installedDependencies(pkg));
418
+ const installed = installedDependencies(pkg);
419
+ const presets = availablePresets(installed);
335
420
  const selected = ensure(await multiselect({
336
421
  message: "Select the checks to include",
337
422
  options: presets.map((p) => ({
@@ -340,7 +425,14 @@ async function runInit(cwd = process.cwd()) {
340
425
  })),
341
426
  required: true
342
427
  }));
343
- const doInstall = ensure(await confirm({
428
+ const declaredVersion = declaredGreenlyVersion(pkg);
429
+ const latestVersion = await latestPromise;
430
+ const alreadyLatest = !shouldOfferInstall({
431
+ location: greenlyLocation(pkg),
432
+ declaredVersion,
433
+ latestVersion
434
+ });
435
+ const doInstall = alreadyLatest ? false : ensure(await confirm({
344
436
  message: `Install greenly now with ${pm}?`,
345
437
  initialValue: true
346
438
  }));
@@ -355,12 +447,10 @@ async function runInit(cwd = process.cwd()) {
355
447
  process.exit(0);
356
448
  }
357
449
  }
358
- const isNext = isNextProject(pkg, hasNextConfigFile(cwd));
359
- if (isNext && selected.includes("typescript")) log.info(colors.dim("Detected Next.js, using tsc --incremental false"));
360
450
  const content = renderConfig({
361
451
  name,
362
452
  checks: buildChecks(selected, pm, {
363
- isNext,
453
+ deps: installed,
364
454
  scripts: packageScripts(pkg)
365
455
  }),
366
456
  ext,
@@ -390,7 +480,8 @@ async function runInit(cwd = process.cwd()) {
390
480
  s.stop("Could not install greenly automatically");
391
481
  log.warn(`Run "${installCommand(pm)}" yourself.`);
392
482
  }
393
- } else log.info(`Skipped install. Run "${installCommand(pm)}" when ready.`);
483
+ } else if (alreadyLatest) log.info(`greenly ${colors.bold(declaredVersion ?? "")} already a devDependency at latest, skipped install.`);
484
+ else log.info(`Skipped install. Run "${installCommand(pm)}" when ready.`);
394
485
  const runCmd = `${pmContext(pm).run} ${scriptName}`;
395
486
  outro(`Done. Run ${colors.bold(runCmd)} to run your checks.`);
396
487
  }
@@ -416,7 +507,8 @@ async function runCommand(command) {
416
507
  } catch (error) {
417
508
  return {
418
509
  ok: false,
419
- stderr: colors.red(formatThrown(error))
510
+ stderr: colors.red(formatThrown(error)),
511
+ error
420
512
  };
421
513
  }
422
514
  try {
@@ -441,7 +533,8 @@ async function runCommand(command) {
441
533
  }
442
534
  return {
443
535
  ok: false,
444
- stderr
536
+ stderr,
537
+ error
445
538
  };
446
539
  }
447
540
  }
@@ -482,6 +575,9 @@ async function runFix(check, error) {
482
575
  function fixLabel(check) {
483
576
  return typeof check.onFail === "string" ? `"${check.onFail}"` : "the fix function";
484
577
  }
578
+ function fixCommand(check) {
579
+ return typeof check.onFail === "string" ? check.onFail : "fix function";
580
+ }
485
581
  async function runChecks(config, options = {}) {
486
582
  const { autoFix = false, interactive = true } = options;
487
583
  const name = config.name ?? "greenly";
@@ -496,7 +592,7 @@ async function runChecks(config, options = {}) {
496
592
  for (const check of config.checks) {
497
593
  console.log(colors.bold(colors.yellow(`▶ ${check.name}`)));
498
594
  console.log(` ${colors.cyan(commandLine(check.command))}\n`);
499
- const { ok, stderr } = await runCommand(check.command);
595
+ const { ok, stderr, error } = await runCommand(check.command);
500
596
  if (ok) {
501
597
  console.log(`\n${colors.green(`✔ PASSED: ${check.name}`)}\n`);
502
598
  results.push({
@@ -543,9 +639,8 @@ async function runChecks(config, options = {}) {
543
639
  console.log("\n" + colors.dim(rule("─")) + "\n");
544
640
  continue;
545
641
  }
546
- const fixDisplay = typeof check.onFail === "string" ? check.onFail : "fix function";
547
- console.log(`\n ${colors.cyan(`$ ${fixDisplay}`)}\n`);
548
- if (await runFix(check, new Error(`${check.name} failed`))) {
642
+ console.log(`\n ${colors.cyan(`$ ${fixCommand(check)}`)}\n`);
643
+ if (await runFix(check, error)) {
549
644
  console.log(`\n${colors.green(`✔ Auto-fixed: ${check.name}`)}\n`);
550
645
  results.push({
551
646
  name: check.name,
@@ -606,6 +701,11 @@ ${colors.bold("Config")}
606
701
  ],
607
702
  });
608
703
  `;
704
+ function printUpdateNotice(info) {
705
+ const pm = detectPackageManager(process.env.npm_config_user_agent, detectLockfiles(process.cwd()));
706
+ console.log(colors.yellow(`Update available: greenly ${colors.dim(info.current)} -> ${colors.bold(info.latest)}`));
707
+ console.log(colors.dim(`Run ${colors.bold(installCommand(pm))} to update.`) + "\n");
708
+ }
609
709
  async function main() {
610
710
  const argv = process.argv.slice(2);
611
711
  if (argv[0] === "init") {
@@ -621,11 +721,15 @@ async function main() {
621
721
  console.log(version);
622
722
  return;
623
723
  }
624
- const mode = resolveMode(parsed, process.stdout.isTTY ?? false);
724
+ const isTTY = process.stdout.isTTY ?? false;
725
+ const mode = resolveMode(parsed, isTTY);
726
+ const updateCheck = isTTY ? checkForUpdate(name, version) : null;
625
727
  try {
626
728
  const { config } = await loadGreenlyConfig();
627
729
  const { exitCode } = await runChecks(config, mode);
628
730
  process.exitCode = exitCode;
731
+ const update = updateCheck ? await updateCheck : null;
732
+ if (update) printUpdateNotice(update);
629
733
  } catch (error) {
630
734
  if (error instanceof ConfigNotFoundError || error instanceof ConfigInvalidError) {
631
735
  console.error(colors.red(error.message));
package/dist/index.d.cts CHANGED
@@ -2,12 +2,12 @@
2
2
  /**
3
3
  * Context passed to an `onFail` function when a check fails.
4
4
  */
5
- interface OnFailContext {
5
+ type OnFailContext = {
6
6
  /** The check that failed. */
7
7
  check: GreenlyCheck;
8
8
  /** The error thrown while running the check's command. */
9
9
  error: unknown;
10
- }
10
+ };
11
11
  /**
12
12
  * A function run to fix a failing check. Invoked after the user confirms
13
13
  * (or automatically with `--yes`/`--fix`). Throw to signal the fix failed.
@@ -22,7 +22,7 @@ type CommandFn = () => void | Promise<void>;
22
22
  /**
23
23
  * A single check to run, in order.
24
24
  */
25
- interface GreenlyCheck {
25
+ type GreenlyCheck = {
26
26
  /** Label shown while running and in the final summary, e.g. "TypeScript". */
27
27
  name: string;
28
28
  /**
@@ -42,17 +42,17 @@ interface GreenlyCheck {
42
42
  * (no non-zero exit code).
43
43
  */
44
44
  optional?: boolean;
45
- }
45
+ };
46
46
  /**
47
47
  * Greenly configuration. Author it with {@link defineConfig} in a
48
48
  * `greenly.config.{ts,js,mts,mjs,cts,cjs,json}` file.
49
49
  */
50
- interface GreenlyConfig {
50
+ type GreenlyConfig = {
51
51
  /** Project name shown in the banner. Defaults to the package name / cwd. */
52
52
  name?: string;
53
53
  /** Ordered list of checks to run. */
54
54
  checks: GreenlyCheck[];
55
- }
55
+ };
56
56
  //#endregion
57
57
  //#region src/lib/define-config.d.ts
58
58
  /**
package/dist/index.d.mts CHANGED
@@ -2,12 +2,12 @@
2
2
  /**
3
3
  * Context passed to an `onFail` function when a check fails.
4
4
  */
5
- interface OnFailContext {
5
+ type OnFailContext = {
6
6
  /** The check that failed. */
7
7
  check: GreenlyCheck;
8
8
  /** The error thrown while running the check's command. */
9
9
  error: unknown;
10
- }
10
+ };
11
11
  /**
12
12
  * A function run to fix a failing check. Invoked after the user confirms
13
13
  * (or automatically with `--yes`/`--fix`). Throw to signal the fix failed.
@@ -22,7 +22,7 @@ type CommandFn = () => void | Promise<void>;
22
22
  /**
23
23
  * A single check to run, in order.
24
24
  */
25
- interface GreenlyCheck {
25
+ type GreenlyCheck = {
26
26
  /** Label shown while running and in the final summary, e.g. "TypeScript". */
27
27
  name: string;
28
28
  /**
@@ -42,17 +42,17 @@ interface GreenlyCheck {
42
42
  * (no non-zero exit code).
43
43
  */
44
44
  optional?: boolean;
45
- }
45
+ };
46
46
  /**
47
47
  * Greenly configuration. Author it with {@link defineConfig} in a
48
48
  * `greenly.config.{ts,js,mts,mjs,cts,cjs,json}` file.
49
49
  */
50
- interface GreenlyConfig {
50
+ type GreenlyConfig = {
51
51
  /** Project name shown in the banner. Defaults to the package name / cwd. */
52
52
  name?: string;
53
53
  /** Ordered list of checks to run. */
54
54
  checks: GreenlyCheck[];
55
- }
55
+ };
56
56
  //#endregion
57
57
  //#region src/lib/define-config.d.ts
58
58
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "greenly",
3
- "version": "1.1.0",
3
+ "version": "1.1.1",
4
4
  "description": "Config-driven project check runner. Define your lint/format/typecheck/test steps in greenly.config.ts and run them with one command.",
5
5
  "keywords": [
6
6
  "check",