greenly 1.1.0 → 1.1.2
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 +72 -16
- package/dist/cli.js +185 -81
- package/dist/index.d.cts +6 -6
- package/dist/index.d.mts +6 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
|
|
1
|
+
<h1>
|
|
2
|
+
<img src="https://raw.githubusercontent.com/yusifaliyevpro/greenly/main/assets/greenly.svg" alt="" height="34" align="center" />
|
|
3
|
+
greenly
|
|
4
|
+
</h1>
|
|
2
5
|
|
|
3
6
|
[](https://www.npmjs.com/package/greenly)
|
|
4
7
|
[](https://www.npmjs.com/package/greenly)
|
|
@@ -7,17 +10,24 @@
|
|
|
7
10
|
[](https://github.com/yusifaliyevpro/greenly/actions/workflows/pr-checks.yml)
|
|
8
11
|
[](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
|
-
|
|
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
|
-
|
|
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,46 @@ 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
|
+
|
|
115
|
+
## Examples in the wild
|
|
116
|
+
|
|
117
|
+
Real `greenly.config.ts` files that mix the usual checks with project-specific ones:
|
|
118
|
+
|
|
119
|
+
- [**vscode-icons**](https://github.com/yusifaliyevpro/vscode-icons/blob/main/greenly.config.ts) - adds custom "Sorted Icons" (auto-fixed), "Icon Integrity", and "Version Check" steps, so the icon set and version bump are validated by the same command as everything else instead of a separate script nobody remembers to run.
|
|
120
|
+
- [**countries**](https://github.com/yusifaliyevpro/countries/blob/main/greenly.config.ts) - alongside typecheck / format / lint / test / build, a custom "Sorted data" check keeps the country dataset in order and auto-sorts it on failure, so a misordered entry can never slip into a PR.
|
|
121
|
+
- [**Azerbaijan GitHub Community / blog**](https://github.com/azerbaijan-git-community/blog/blob/main/greenly.config.ts) - a custom "Validate Posts" check runs on every contribution, so a malformed blog post is caught automatically instead of during review.
|
|
122
|
+
- [**Azerbaijan GitHub Community / showcase**](https://github.com/azerbaijan-git-community/showcase/blob/main/greenly.config.ts) - a custom "Validate projects" check verifies each submitted showcase entry, so contributor PRs are checked the same way locally and in CI.
|
|
123
|
+
|
|
74
124
|
## Config reference
|
|
75
125
|
|
|
76
126
|
| Field | Type | Description |
|
|
@@ -126,17 +176,23 @@ greenly.config.json
|
|
|
126
176
|
|
|
127
177
|
## CLI
|
|
128
178
|
|
|
129
|
-
| Command / Flag | Description
|
|
130
|
-
| ---------------------- |
|
|
131
|
-
| `greenly` | Run the checks from `greenly.config.*`.
|
|
132
|
-
| `greenly init` |
|
|
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.
|
|
179
|
+
| Command / Flag | Description |
|
|
180
|
+
| ---------------------- | ------------------------------------------------------------------------ |
|
|
181
|
+
| `greenly` | Run the checks from `greenly.config.*`. |
|
|
182
|
+
| `greenly init` | Set up a config by answering a few questions, then install greenly. |
|
|
183
|
+
| `-y`, `--yes`, `--fix` | Auto-run every `onFail` fixer without prompting (great for CI / agents). |
|
|
184
|
+
| `--no-fix` | Run all checks, never prompt or fix, just report. |
|
|
185
|
+
| `-v`, `--version` | Print the version. |
|
|
186
|
+
| `-h`, `--help` | Show help. |
|
|
137
187
|
|
|
138
188
|
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
189
|
|
|
190
|
+
## Demo
|
|
191
|
+
|
|
192
|
+
Here is `pnpm check` running greenly on a real project ([**vscode-icons**](https://github.com/yusifaliyevpro/vscode-icons/blob/main/greenly.config.ts)):
|
|
193
|
+
|
|
194
|
+

|
|
195
|
+
|
|
140
196
|
## License
|
|
141
197
|
|
|
142
198
|
MIT © [Yusif Aliyev](https://yusifaliyevpro.com)
|
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
|
|
10
|
+
var name = "greenly";
|
|
11
|
+
var version = "1.1.2";
|
|
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.
|
|
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.
|
|
120
|
-
onFail: scriptCommand(c, ["fmt", "format"]) ?? `${c.
|
|
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.
|
|
129
|
-
onFail: scriptCommand(c, ["fmt", "format"]) ?? `${c.
|
|
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.
|
|
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.
|
|
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.
|
|
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: "
|
|
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
|
-
|
|
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
|
|
228
|
-
if (
|
|
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
|
|
233
|
-
const
|
|
234
|
-
|
|
235
|
-
|
|
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
|
-
|
|
239
|
-
|
|
240
|
-
return
|
|
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
|
|
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
|
|
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
|
-
|
|
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(`
|
|
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
|
-
|
|
547
|
-
|
|
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
|
|
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
|
-
|
|
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