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 +70 -7
- package/dist/cli.js +433 -20
- package/dist/index.d.cts +6 -6
- package/dist/index.d.mts +6 -6
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,17 +1,47 @@
|
|
|
1
|
-
|
|
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
|
+
[](https://www.npmjs.com/package/greenly)
|
|
7
|
+
[](https://www.npmjs.com/package/greenly)
|
|
8
|
+
[](https://www.npmjs.com/package/greenly)
|
|
9
|
+
[](https://socket.dev/npm/package/greenly)
|
|
10
|
+
[](https://github.com/yusifaliyevpro/greenly/actions/workflows/pr-checks.yml)
|
|
11
|
+
[](https://github.com/yusifaliyevpro/greenly/blob/main/LICENSE)
|
|
4
12
|
|
|
5
|
-
|
|
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
|
|
168
|
+
## CLI
|
|
108
169
|
|
|
109
|
-
| Flag
|
|
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 {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
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
|
|
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/
|
|
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
|
|
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
|
-
|
|
244
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
"
|
|
78
|
+
"jiti": "2.7.0"
|
|
79
79
|
},
|
|
80
80
|
"devDependencies": {
|
|
81
81
|
"@types/node": "24.13.3",
|