greenly 1.0.1 → 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,17 +1,47 @@
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
- > Config-driven project check runner. Define your lint / format / typecheck / test steps once in `greenly.config.ts` and run them with a single command.
6
+ [![npm version](https://img.shields.io/npm/v/greenly?color=3fb950&label=npm)](https://www.npmjs.com/package/greenly)
7
+ [![npm downloads](https://img.shields.io/npm/dm/greenly?color=3fb950)](https://www.npmjs.com/package/greenly)
8
+ [![unpacked size](https://img.shields.io/npm/unpacked-size/greenly?color=3fb950)](https://www.npmjs.com/package/greenly)
9
+ [![Socket Badge](https://badge.socket.dev/npm/package/greenly/1.0.1)](https://socket.dev/npm/package/greenly)
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)
11
+ [![license](https://img.shields.io/npm/l/greenly?color=3fb950)](https://github.com/yusifaliyevpro/greenly/blob/main/LICENSE)
4
12
 
5
- 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.
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.
14
+
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.
27
+
28
+ ## Quick start
29
+
30
+ Run this and greenly sets everything up for you, based on the tools your project already uses:
31
+
32
+ ```bash
33
+ pnpx greenly init
34
+ # or: npx greenly init
35
+ ```
6
36
 
7
37
  ## Install
8
38
 
39
+ Or set it up manually:
40
+
9
41
  ```bash
10
42
  pnpm add -D greenly
11
43
  ```
12
44
 
13
- ## Quick start
14
-
15
45
  Create a `greenly.config.ts` at your project root:
16
46
 
17
47
  ```ts
@@ -51,6 +81,37 @@ Or wire it into your scripts so `pnpm check` works too:
51
81
  - Checks marked `optional: true` warn on failure but never fail the overall run.
52
82
  - greenly exits with code `1` if any non-optional check is still failing, otherwise `0`.
53
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
+
54
115
  ## Config reference
55
116
 
56
117
  | Field | Type | Description |
@@ -104,10 +165,12 @@ greenly.config.js greenly.config.mjs greenly.config.cjs
104
165
  greenly.config.json
105
166
  ```
106
167
 
107
- ## CLI flags
168
+ ## CLI
108
169
 
109
- | Flag | Description |
170
+ | Command / Flag | Description |
110
171
  | ---------------------- | ------------------------------------------------------------------------ |
172
+ | `greenly` | Run the checks from `greenly.config.*`. |
173
+ | `greenly init` | Set up a config by answering a few questions, then install greenly. |
111
174
  | `-y`, `--yes`, `--fix` | Auto-run every `onFail` fixer without prompting (great for CI / agents). |
112
175
  | `--no-fix` | Run all checks, never prompt or fix, just report. |
113
176
  | `-v`, `--version` | Print the version. |
package/dist/cli.js CHANGED
@@ -1,11 +1,14 @@
1
1
  #!/usr/bin/env node
2
- import { existsSync } from "node:fs";
3
- import { resolve } from "node:path";
4
- import { loadConfig } from "c12";
5
- import { execSync } from "node:child_process";
6
- import { cancel, confirm, isCancel } from "@clack/prompts";
2
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
3
+ import { basename, join, resolve } from "node:path";
4
+ import { pathToFileURL } from "node:url";
5
+ import { createJiti } from "jiti";
6
+ import { exec, execSync } from "node:child_process";
7
+ import { promisify } from "node:util";
8
+ import { cancel, confirm, intro, isCancel, log, multiselect, outro, select, spinner, text } from "@clack/prompts";
7
9
  //#region package.json
8
- var version = "1.0.1";
10
+ var name = "greenly";
11
+ var version = "1.1.1";
9
12
  //#endregion
10
13
  //#region src/lib/args.ts
11
14
  function parseArgs(argv) {
@@ -40,8 +43,7 @@ const colors = {
40
43
  cyan: wrap(36, 39)
41
44
  };
42
45
  //#endregion
43
- //#region src/lib/config.ts
44
- const CONFIG_BASENAME = "greenly.config";
46
+ //#region src/lib/constants.ts
45
47
  const CONFIG_EXTENSIONS = [
46
48
  "ts",
47
49
  "mts",
@@ -51,6 +53,9 @@ const CONFIG_EXTENSIONS = [
51
53
  "cjs",
52
54
  "json"
53
55
  ];
56
+ //#endregion
57
+ //#region src/lib/config.ts
58
+ const CONFIG_BASENAME = "greenly.config";
54
59
  var ConfigNotFoundError = class extends Error {
55
60
  cwd;
56
61
  constructor(cwd) {
@@ -76,10 +81,7 @@ function findConfigFile(cwd = process.cwd()) {
76
81
  async function loadGreenlyConfig(cwd = process.cwd()) {
77
82
  const configFile = findConfigFile(cwd);
78
83
  if (!configFile) throw new ConfigNotFoundError(cwd);
79
- const { config } = await loadConfig({
80
- cwd,
81
- configFile
82
- });
84
+ const config = await createJiti(pathToFileURL(resolve(cwd, "greenly.config")).href).import(configFile, { default: true });
83
85
  if (!config || typeof config !== "object") throw new ConfigInvalidError(configFile, "config must export an object");
84
86
  if (!Array.isArray(config.checks) || config.checks.length === 0) throw new ConfigInvalidError(configFile, `"checks" must be a non-empty array`);
85
87
  for (const [i, check] of config.checks.entries()) {
@@ -92,6 +94,398 @@ async function loadGreenlyConfig(cwd = process.cwd()) {
92
94
  };
93
95
  }
94
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
182
+ //#region src/lib/init.ts
183
+ const execAsync = promisify(exec);
184
+ function isRecord(value) {
185
+ return typeof value === "object" && value !== null;
186
+ }
187
+ function scriptCommand(ctx, candidates) {
188
+ const found = candidates.find((s) => ctx.scripts.has(s));
189
+ return found ? `${ctx.run} ${found}` : null;
190
+ }
191
+ const dep = (name) => (deps) => deps.has(name);
192
+ const CHECK_PRESETS = [
193
+ {
194
+ value: "typescript",
195
+ label: "TypeScript (tsc)",
196
+ detect: dep("typescript"),
197
+ build: (c) => ({
198
+ name: "TypeScript",
199
+ command: scriptCommand(c, ["typecheck", "type-check"]) ?? `${c.run} tsc --noEmit${dep("next")(c.deps) ? " --incremental false" : ""}`
200
+ })
201
+ },
202
+ {
203
+ value: "oxfmt",
204
+ label: "Oxfmt",
205
+ detect: dep("oxfmt"),
206
+ build: (c) => ({
207
+ name: "Oxfmt",
208
+ command: scriptCommand(c, ["fmt:check", "format:check"]) ?? `${c.run} oxfmt --check`,
209
+ onFail: scriptCommand(c, ["fmt", "format"]) ?? `${c.run} oxfmt`
210
+ })
211
+ },
212
+ {
213
+ value: "prettier",
214
+ label: "Prettier",
215
+ detect: dep("prettier"),
216
+ build: (c) => ({
217
+ name: "Prettier",
218
+ command: scriptCommand(c, ["fmt:check", "format:check"]) ?? `${c.run} prettier --check .`,
219
+ onFail: scriptCommand(c, ["fmt", "format"]) ?? `${c.run} prettier --write .`
220
+ })
221
+ },
222
+ {
223
+ value: "oxlint",
224
+ label: "Oxlint",
225
+ detect: dep("oxlint"),
226
+ build: (c) => ({
227
+ name: "Oxlint",
228
+ command: scriptCommand(c, ["lint"]) ?? `${c.run} oxlint`
229
+ })
230
+ },
231
+ {
232
+ value: "eslint",
233
+ label: "ESLint",
234
+ detect: dep("eslint"),
235
+ build: (c) => ({
236
+ name: "ESLint",
237
+ command: scriptCommand(c, ["lint"]) ?? `${c.run} eslint .`
238
+ })
239
+ },
240
+ {
241
+ value: "vitest",
242
+ label: "Tests (Vitest)",
243
+ detect: dep("vitest"),
244
+ build: (c) => ({
245
+ name: "Tests",
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`
256
+ })
257
+ },
258
+ {
259
+ value: "build",
260
+ label: "Build",
261
+ build: (c) => ({
262
+ name: "Build",
263
+ command: scriptCommand(c, ["build"]) ?? `${c.run} build`
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
+ })
274
+ }
275
+ ];
276
+ function pmContext(pm) {
277
+ switch (pm) {
278
+ case "pnpm": return {
279
+ exec: "pnpx",
280
+ run: "pnpm"
281
+ };
282
+ case "yarn": return {
283
+ exec: "yarn dlx",
284
+ run: "yarn"
285
+ };
286
+ case "bun": return {
287
+ exec: "bunx",
288
+ run: "bun run"
289
+ };
290
+ default: return {
291
+ exec: "npx",
292
+ run: "npm run"
293
+ };
294
+ }
295
+ }
296
+ function buildChecks(selected, pm, opts = {}) {
297
+ const ctx = {
298
+ ...pmContext(pm),
299
+ deps: opts.deps ?? new Set(),
300
+ scripts: opts.scripts ?? new Set()
301
+ };
302
+ return CHECK_PRESETS.filter((p) => selected.includes(p.value)).map((p) => p.build(ctx));
303
+ }
304
+ function packageScripts(pkg) {
305
+ const scripts = pkg?.scripts;
306
+ return isRecord(scripts) ? new Set(Object.keys(scripts)) : new Set();
307
+ }
308
+ function depRecord(pkg, field) {
309
+ const deps = pkg?.[field];
310
+ return isRecord(deps) ? deps : null;
311
+ }
312
+ function installedDependencies(pkg) {
313
+ const names = new Set();
314
+ for (const field of ["dependencies", "devDependencies"]) {
315
+ const deps = depRecord(pkg, field);
316
+ if (deps) for (const key of Object.keys(deps)) names.add(key);
317
+ }
318
+ return names;
319
+ }
320
+ function greenlyLocation(pkg) {
321
+ const inField = (field) => {
322
+ const deps = depRecord(pkg, field);
323
+ return deps !== null && "greenly" in deps;
324
+ };
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));
343
+ }
344
+ function configFileName(ext) {
345
+ return `greenly.config.${ext}`;
346
+ }
347
+ function renderCheck(check) {
348
+ const parts = [`name: ${JSON.stringify(check.name)}`, `command: ${JSON.stringify(check.command)}`];
349
+ if (check.onFail) parts.push(`onFail: ${JSON.stringify(check.onFail)}`);
350
+ return `{ ${parts.join(", ")} }`;
351
+ }
352
+ function isEsm(ext, isModule) {
353
+ if (ext === "ts" || ext === "mts" || ext === "mjs") return true;
354
+ if (ext === "cts" || ext === "cjs") return false;
355
+ return isModule;
356
+ }
357
+ function renderConfig(opts) {
358
+ const { name, checks, ext, isModule } = opts;
359
+ if (ext === "json") return `${JSON.stringify({
360
+ name,
361
+ checks
362
+ }, null, 2)}\n`;
363
+ const body = checks.map((c) => ` ${renderCheck(c)},`).join("\n");
364
+ const object = `{\n name: ${JSON.stringify(name)},\n checks: [\n${body}\n ],\n}`;
365
+ if (isEsm(ext, isModule)) return `import { defineConfig } from "greenly";\n\nexport default defineConfig(${object});\n`;
366
+ return `const { defineConfig } = require("greenly");\n\nmodule.exports = defineConfig(${object});\n`;
367
+ }
368
+ function withCheckScript(pkg, scriptName) {
369
+ const scripts = isRecord(pkg.scripts) ? pkg.scripts : {};
370
+ return {
371
+ ...pkg,
372
+ scripts: {
373
+ ...scripts,
374
+ [scriptName]: "greenly"
375
+ }
376
+ };
377
+ }
378
+ function readPackageJson(path) {
379
+ if (!existsSync(path)) return null;
380
+ try {
381
+ const parsed = JSON.parse(readFileSync(path, "utf8"));
382
+ if (isRecord(parsed)) return parsed;
383
+ } catch {}
384
+ return null;
385
+ }
386
+ function ensure(value) {
387
+ if (isCancel(value)) {
388
+ cancel("init cancelled.");
389
+ process.exit(0);
390
+ }
391
+ return value;
392
+ }
393
+ async function runInit(cwd = process.cwd()) {
394
+ intro(colors.inverse(" greenly ") + colors.dim(" init "));
395
+ const pkgPath = join(cwd, "package.json");
396
+ const pkg = readPackageJson(pkgPath);
397
+ const defaultName = typeof pkg?.name === "string" ? pkg.name : basename(cwd);
398
+ const pm = detectPackageManager(process.env.npm_config_user_agent, detectLockfiles(cwd));
399
+ const latestPromise = fetchLatestVersion("greenly");
400
+ const name = ensure(await text({
401
+ message: "Project name (shown in the banner)",
402
+ initialValue: defaultName,
403
+ validate: (value) => value?.trim() ? void 0 : "Please enter a project name"
404
+ }));
405
+ const ext = ensure(await select({
406
+ message: "Config file format",
407
+ options: CONFIG_EXTENSIONS.map((e) => ({
408
+ value: e,
409
+ label: configFileName(e)
410
+ })),
411
+ initialValue: "ts"
412
+ }));
413
+ const scriptName = ensure(await text({
414
+ message: "Script name to add to package.json",
415
+ initialValue: "check",
416
+ validate: (value) => value?.trim() ? void 0 : "Please enter a script name"
417
+ }));
418
+ const installed = installedDependencies(pkg);
419
+ const presets = availablePresets(installed);
420
+ const selected = ensure(await multiselect({
421
+ message: "Select the checks to include",
422
+ options: presets.map((p) => ({
423
+ value: p.value,
424
+ label: p.label
425
+ })),
426
+ required: true
427
+ }));
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({
436
+ message: `Install greenly now with ${pm}?`,
437
+ initialValue: true
438
+ }));
439
+ const fileName = configFileName(ext);
440
+ const filePath = join(cwd, fileName);
441
+ if (existsSync(filePath)) {
442
+ if (!ensure(await confirm({
443
+ message: `${fileName} already exists. Overwrite it?`,
444
+ initialValue: false
445
+ }))) {
446
+ cancel("Kept the existing config. Nothing changed.");
447
+ process.exit(0);
448
+ }
449
+ }
450
+ const content = renderConfig({
451
+ name,
452
+ checks: buildChecks(selected, pm, {
453
+ deps: installed,
454
+ scripts: packageScripts(pkg)
455
+ }),
456
+ ext,
457
+ isModule: pkg?.type === "module"
458
+ });
459
+ writeFileSync(filePath, content);
460
+ log.success(`Created ${colors.bold(fileName)}`);
461
+ if (pkg) {
462
+ const current = (isRecord(pkg.scripts) ? pkg.scripts : {})[scriptName];
463
+ let write = true;
464
+ if (typeof current === "string" && current !== "greenly") write = ensure(await confirm({
465
+ message: `Script "${scriptName}" already runs "${current}". Overwrite with "greenly"?`,
466
+ initialValue: false
467
+ }));
468
+ if (write) {
469
+ writeFileSync(pkgPath, `${JSON.stringify(withCheckScript(pkg, scriptName), null, 2)}\n`);
470
+ log.success(`Added ${colors.bold(`"${scriptName}": "greenly"`)} to package.json`);
471
+ }
472
+ } else log.warn("No package.json found, skipped adding the script.");
473
+ if (doInstall) {
474
+ const s = spinner();
475
+ s.start(`Installing greenly with ${pm}`);
476
+ try {
477
+ await execAsync(installCommand(pm), { cwd });
478
+ s.stop("Installed greenly");
479
+ } catch {
480
+ s.stop("Could not install greenly automatically");
481
+ log.warn(`Run "${installCommand(pm)}" yourself.`);
482
+ }
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.`);
485
+ const runCmd = `${pmContext(pm).run} ${scriptName}`;
486
+ outro(`Done. Run ${colors.bold(runCmd)} to run your checks.`);
487
+ }
488
+ //#endregion
95
489
  //#region src/lib/runner.ts
96
490
  const MIN_WIDTH = 60;
97
491
  function bannerWidth(name) {
@@ -113,7 +507,8 @@ async function runCommand(command) {
113
507
  } catch (error) {
114
508
  return {
115
509
  ok: false,
116
- stderr: colors.red(formatThrown(error))
510
+ stderr: colors.red(formatThrown(error)),
511
+ error
117
512
  };
118
513
  }
119
514
  try {
@@ -138,7 +533,8 @@ async function runCommand(command) {
138
533
  }
139
534
  return {
140
535
  ok: false,
141
- stderr
536
+ stderr,
537
+ error
142
538
  };
143
539
  }
144
540
  }
@@ -179,6 +575,9 @@ async function runFix(check, error) {
179
575
  function fixLabel(check) {
180
576
  return typeof check.onFail === "string" ? `"${check.onFail}"` : "the fix function";
181
577
  }
578
+ function fixCommand(check) {
579
+ return typeof check.onFail === "string" ? check.onFail : "fix function";
580
+ }
182
581
  async function runChecks(config, options = {}) {
183
582
  const { autoFix = false, interactive = true } = options;
184
583
  const name = config.name ?? "greenly";
@@ -193,7 +592,7 @@ async function runChecks(config, options = {}) {
193
592
  for (const check of config.checks) {
194
593
  console.log(colors.bold(colors.yellow(`▶ ${check.name}`)));
195
594
  console.log(` ${colors.cyan(commandLine(check.command))}\n`);
196
- const { ok, stderr } = await runCommand(check.command);
595
+ const { ok, stderr, error } = await runCommand(check.command);
197
596
  if (ok) {
198
597
  console.log(`\n${colors.green(`✔ PASSED: ${check.name}`)}\n`);
199
598
  results.push({
@@ -240,9 +639,8 @@ async function runChecks(config, options = {}) {
240
639
  console.log("\n" + colors.dim(rule("─")) + "\n");
241
640
  continue;
242
641
  }
243
- const fixDisplay = typeof check.onFail === "string" ? check.onFail : "fix function";
244
- console.log(`\n ${colors.cyan(`$ ${fixDisplay}`)}\n`);
245
- if (await runFix(check, new Error(`${check.name} failed`))) {
642
+ console.log(`\n ${colors.cyan(`$ ${fixCommand(check)}`)}\n`);
643
+ if (await runFix(check, error)) {
246
644
  console.log(`\n${colors.green(`✔ Auto-fixed: ${check.name}`)}\n`);
247
645
  results.push({
248
646
  name: check.name,
@@ -281,6 +679,7 @@ const HELP = `${colors.bold("greenly")} - config-driven project check runner
281
679
 
282
680
  ${colors.bold("Usage")}
283
681
  greenly [options]
682
+ greenly init Scaffold a greenly.config file interactively
284
683
 
285
684
  ${colors.bold("Options")}
286
685
  -y, --yes, --fix Auto-run every onFail fixer without prompting (CI / agents)
@@ -302,8 +701,18 @@ ${colors.bold("Config")}
302
701
  ],
303
702
  });
304
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
+ }
305
709
  async function main() {
306
- const parsed = parseArgs(process.argv.slice(2));
710
+ const argv = process.argv.slice(2);
711
+ if (argv[0] === "init") {
712
+ await runInit();
713
+ return;
714
+ }
715
+ const parsed = parseArgs(argv);
307
716
  if (parsed.help) {
308
717
  console.log(HELP);
309
718
  return;
@@ -312,11 +721,15 @@ async function main() {
312
721
  console.log(version);
313
722
  return;
314
723
  }
315
- 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;
316
727
  try {
317
728
  const { config } = await loadGreenlyConfig();
318
729
  const { exitCode } = await runChecks(config, mode);
319
730
  process.exitCode = exitCode;
731
+ const update = updateCheck ? await updateCheck : null;
732
+ if (update) printUpdateNotice(update);
320
733
  } catch (error) {
321
734
  if (error instanceof ConfigNotFoundError || error instanceof ConfigInvalidError) {
322
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.0.1",
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",
@@ -75,7 +75,7 @@
75
75
  },
76
76
  "dependencies": {
77
77
  "@clack/prompts": "1.7.0",
78
- "c12": "3.3.4"
78
+ "jiti": "2.7.0"
79
79
  },
80
80
  "devDependencies": {
81
81
  "@types/node": "24.13.3",