greenly 0.0.1 → 1.0.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 +104 -15
- package/dist/cli.js +331 -0
- package/dist/index.cjs +25 -0
- package/dist/index.d.cts +78 -0
- package/dist/index.d.mts +78 -0
- package/dist/index.mjs +24 -0
- package/package.json +47 -4
- package/.vscode/extensions.json +0 -3
- package/.vscode/settings.json +0 -4
- package/oxfmt.config.ts +0 -10
- package/oxlint.config.ts +0 -19
- package/tsconfig.json +0 -16
- package/tsdown.config.ts +0 -3
package/README.md
CHANGED
|
@@ -1,31 +1,120 @@
|
|
|
1
1
|
# greenly
|
|
2
2
|
|
|
3
|
-
>
|
|
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.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
config file, run them with a single command before you ship.
|
|
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.
|
|
7
6
|
|
|
8
|
-
##
|
|
7
|
+
## Install
|
|
9
8
|
|
|
10
|
-
|
|
9
|
+
```bash
|
|
10
|
+
pnpm add -D greenly
|
|
11
|
+
```
|
|
11
12
|
|
|
12
|
-
|
|
13
|
-
will get you a placeholder. Star or watch the repo to know when `1.0.0` lands.
|
|
13
|
+
## Quick start
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
Create a `greenly.config.ts` at your project root:
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
- Clean, interactive output for pass/fail steps
|
|
17
|
+
```ts
|
|
18
|
+
import { defineConfig } from "greenly";
|
|
20
19
|
|
|
21
|
-
|
|
20
|
+
export default defineConfig({
|
|
21
|
+
name: "MyProject",
|
|
22
|
+
checks: [
|
|
23
|
+
{ name: "TypeScript", command: "pnpm tsc --noEmit" },
|
|
24
|
+
{ name: "Format", command: "pnpm oxfmt --check", onFail: "pnpm oxfmt" },
|
|
25
|
+
{ name: "Lint", command: "pnpm oxlint" },
|
|
26
|
+
{ name: "Build", command: "pnpm build", optional: true },
|
|
27
|
+
],
|
|
28
|
+
});
|
|
29
|
+
```
|
|
22
30
|
|
|
23
|
-
|
|
31
|
+
Run it:
|
|
24
32
|
|
|
25
33
|
```bash
|
|
26
|
-
|
|
34
|
+
pnpm greenly
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Or wire it into your scripts so `pnpm check` works too:
|
|
38
|
+
|
|
39
|
+
```json
|
|
40
|
+
{
|
|
41
|
+
"scripts": {
|
|
42
|
+
"check": "greenly"
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## How it works
|
|
48
|
+
|
|
49
|
+
- Checks run **in order**. Each `command` runs in your shell with its output streamed live, so failures show up immediately.
|
|
50
|
+
- If a check fails and declares an `onFail`, greenly asks **Yes/No** whether to run the fixer, then continues.
|
|
51
|
+
- Checks marked `optional: true` warn on failure but never fail the overall run.
|
|
52
|
+
- greenly exits with code `1` if any non-optional check is still failing, otherwise `0`.
|
|
53
|
+
|
|
54
|
+
## Config reference
|
|
55
|
+
|
|
56
|
+
| Field | Type | Description |
|
|
57
|
+
| ------------------- | ------------------------------------------ | ------------------------------------------------------------------------- |
|
|
58
|
+
| `name` | `string?` | Project name shown in the banner. |
|
|
59
|
+
| `checks` | `Check[]` | Ordered list of checks. |
|
|
60
|
+
| `checks[].name` | `string` | Label shown while running and in the summary. |
|
|
61
|
+
| `checks[].command` | `string \| () => void \| Promise<void>` | Shell command (e.g. `"pnpm tsc --noEmit"`), or a function run in-process. |
|
|
62
|
+
| `checks[].onFail` | `string \| (ctx) => void \| Promise<void>` | Fixer run (after a Yes/No prompt) when the check fails. |
|
|
63
|
+
| `checks[].optional` | `boolean?` | When `true`, a failure warns instead of failing the run. |
|
|
64
|
+
|
|
65
|
+
`command` can be a function instead of a shell string. It may be async, and it must **throw** (or reject) to mark the check as failed. When it throws, greenly prints only the error's `message` (and its `cause` if present) - not a stack trace - so make the message descriptive:
|
|
66
|
+
|
|
67
|
+
```ts
|
|
68
|
+
export default defineConfig({
|
|
69
|
+
checks: [
|
|
70
|
+
{
|
|
71
|
+
name: "Env vars",
|
|
72
|
+
command: () => {
|
|
73
|
+
if (!process.env.API_KEY) throw new Error("API_KEY is not set");
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
],
|
|
77
|
+
});
|
|
27
78
|
```
|
|
28
79
|
|
|
80
|
+
`onFail` can also be a function, useful for custom fix logic:
|
|
81
|
+
|
|
82
|
+
```ts
|
|
83
|
+
export default defineConfig({
|
|
84
|
+
checks: [
|
|
85
|
+
{
|
|
86
|
+
name: "Generated files",
|
|
87
|
+
command: "pnpm verify:generated",
|
|
88
|
+
onFail: async ({ check }) => {
|
|
89
|
+
await regenerate();
|
|
90
|
+
console.log(`Regenerated files for ${check.name}`);
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
],
|
|
94
|
+
});
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### Config file formats
|
|
98
|
+
|
|
99
|
+
Any of these are auto-discovered (first match wins, in this order):
|
|
100
|
+
|
|
101
|
+
```
|
|
102
|
+
greenly.config.ts greenly.config.mts greenly.config.cts
|
|
103
|
+
greenly.config.js greenly.config.mjs greenly.config.cjs
|
|
104
|
+
greenly.config.json
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## CLI flags
|
|
108
|
+
|
|
109
|
+
| Flag | Description |
|
|
110
|
+
| ---------------------- | ------------------------------------------------------------------------ |
|
|
111
|
+
| `-y`, `--yes`, `--fix` | Auto-run every `onFail` fixer without prompting (great for CI / agents). |
|
|
112
|
+
| `--no-fix` | Run all checks, never prompt or fix, just report. |
|
|
113
|
+
| `-v`, `--version` | Print the version. |
|
|
114
|
+
| `-h`, `--help` | Show help. |
|
|
115
|
+
|
|
116
|
+
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.
|
|
117
|
+
|
|
29
118
|
## License
|
|
30
119
|
|
|
31
|
-
MIT
|
|
120
|
+
MIT © [Yusif Aliyev](https://yusifaliyevpro.com)
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,331 @@
|
|
|
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";
|
|
7
|
+
//#region package.json
|
|
8
|
+
var version = "1.0.1";
|
|
9
|
+
//#endregion
|
|
10
|
+
//#region src/lib/args.ts
|
|
11
|
+
function parseArgs(argv) {
|
|
12
|
+
const has = (...flags) => argv.some((a) => flags.includes(a));
|
|
13
|
+
const noFix = has("--no-fix");
|
|
14
|
+
return {
|
|
15
|
+
help: has("-h", "--help"),
|
|
16
|
+
version: has("-v", "--version"),
|
|
17
|
+
noFix,
|
|
18
|
+
autoFix: !noFix && has("-y", "--yes", "--fix")
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
function resolveMode(parsed, isTTY) {
|
|
22
|
+
return {
|
|
23
|
+
autoFix: parsed.autoFix,
|
|
24
|
+
interactive: !parsed.noFix && !parsed.autoFix && isTTY
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
//#endregion
|
|
28
|
+
//#region src/lib/colors.ts
|
|
29
|
+
const enabled = !process.env.NO_COLOR && (process.env.FORCE_COLOR === "1" || (process.stdout?.isTTY ?? false));
|
|
30
|
+
function wrap(open, close) {
|
|
31
|
+
return (text) => enabled ? `\x1b[${open}m${text}\x1b[${close}m` : text;
|
|
32
|
+
}
|
|
33
|
+
const colors = {
|
|
34
|
+
bold: wrap(1, 22),
|
|
35
|
+
dim: wrap(2, 22),
|
|
36
|
+
inverse: wrap(7, 27),
|
|
37
|
+
red: wrap(31, 39),
|
|
38
|
+
green: wrap(32, 39),
|
|
39
|
+
yellow: wrap(33, 39),
|
|
40
|
+
cyan: wrap(36, 39)
|
|
41
|
+
};
|
|
42
|
+
//#endregion
|
|
43
|
+
//#region src/lib/config.ts
|
|
44
|
+
const CONFIG_BASENAME = "greenly.config";
|
|
45
|
+
const CONFIG_EXTENSIONS = [
|
|
46
|
+
"ts",
|
|
47
|
+
"mts",
|
|
48
|
+
"cts",
|
|
49
|
+
"js",
|
|
50
|
+
"mjs",
|
|
51
|
+
"cjs",
|
|
52
|
+
"json"
|
|
53
|
+
];
|
|
54
|
+
var ConfigNotFoundError = class extends Error {
|
|
55
|
+
cwd;
|
|
56
|
+
constructor(cwd) {
|
|
57
|
+
super(`No ${CONFIG_BASENAME} file found in ${cwd}.\nCreate one, e.g. ${CONFIG_BASENAME}.ts:\n\n import { defineConfig } from "greenly";\n\n export default defineConfig({\n name: "MyProject",\n checks: [{ name: "TypeScript", command: "pnpm tsc --noEmit" }],\n });`);
|
|
58
|
+
this.cwd = cwd;
|
|
59
|
+
this.name = "ConfigNotFoundError";
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
var ConfigInvalidError = class extends Error {
|
|
63
|
+
configFile;
|
|
64
|
+
constructor(configFile, detail) {
|
|
65
|
+
super(`Invalid config in ${configFile}: ${detail}`);
|
|
66
|
+
this.configFile = configFile;
|
|
67
|
+
this.name = "ConfigInvalidError";
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
function findConfigFile(cwd = process.cwd()) {
|
|
71
|
+
for (const ext of CONFIG_EXTENSIONS) {
|
|
72
|
+
const candidate = resolve(cwd, `${CONFIG_BASENAME}.${ext}`);
|
|
73
|
+
if (existsSync(candidate)) return candidate;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
async function loadGreenlyConfig(cwd = process.cwd()) {
|
|
77
|
+
const configFile = findConfigFile(cwd);
|
|
78
|
+
if (!configFile) throw new ConfigNotFoundError(cwd);
|
|
79
|
+
const { config } = await loadConfig({
|
|
80
|
+
cwd,
|
|
81
|
+
configFile
|
|
82
|
+
});
|
|
83
|
+
if (!config || typeof config !== "object") throw new ConfigInvalidError(configFile, "config must export an object");
|
|
84
|
+
if (!Array.isArray(config.checks) || config.checks.length === 0) throw new ConfigInvalidError(configFile, `"checks" must be a non-empty array`);
|
|
85
|
+
for (const [i, check] of config.checks.entries()) {
|
|
86
|
+
const validCommand = typeof check?.command === "string" || typeof check?.command === "function";
|
|
87
|
+
if (!check || typeof check.name !== "string" || !validCommand) throw new ConfigInvalidError(configFile, `checks[${i}] must have a string "name" and a string or function "command"`);
|
|
88
|
+
}
|
|
89
|
+
return {
|
|
90
|
+
config,
|
|
91
|
+
configFile
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
//#endregion
|
|
95
|
+
//#region src/lib/runner.ts
|
|
96
|
+
const MIN_WIDTH = 60;
|
|
97
|
+
function bannerWidth(name) {
|
|
98
|
+
const base = Math.min(process.stdout.columns ?? MIN_WIDTH, MIN_WIDTH);
|
|
99
|
+
return Math.max(base, name.length + 6);
|
|
100
|
+
}
|
|
101
|
+
function center(text, width) {
|
|
102
|
+
const total = Math.max(0, width - text.length);
|
|
103
|
+
const left = Math.floor(total / 2);
|
|
104
|
+
return " ".repeat(left) + text + " ".repeat(total - left);
|
|
105
|
+
}
|
|
106
|
+
async function runCommand(command) {
|
|
107
|
+
if (typeof command === "function") try {
|
|
108
|
+
await command();
|
|
109
|
+
return {
|
|
110
|
+
ok: true,
|
|
111
|
+
stderr: ""
|
|
112
|
+
};
|
|
113
|
+
} catch (error) {
|
|
114
|
+
return {
|
|
115
|
+
ok: false,
|
|
116
|
+
stderr: colors.red(formatThrown(error))
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
try {
|
|
120
|
+
execSync(command, {
|
|
121
|
+
stdio: [
|
|
122
|
+
"inherit",
|
|
123
|
+
"inherit",
|
|
124
|
+
"pipe"
|
|
125
|
+
],
|
|
126
|
+
encoding: "utf8"
|
|
127
|
+
});
|
|
128
|
+
return {
|
|
129
|
+
ok: true,
|
|
130
|
+
stderr: ""
|
|
131
|
+
};
|
|
132
|
+
} catch (error) {
|
|
133
|
+
let stderr = "";
|
|
134
|
+
if (error && typeof error === "object" && "stderr" in error) {
|
|
135
|
+
const raw = error.stderr;
|
|
136
|
+
if (typeof raw === "string") stderr = raw;
|
|
137
|
+
else if (Buffer.isBuffer(raw)) stderr = raw.toString("utf8");
|
|
138
|
+
}
|
|
139
|
+
return {
|
|
140
|
+
ok: false,
|
|
141
|
+
stderr
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
function describeUnknown(value) {
|
|
146
|
+
if (value instanceof Error) return value.message || value.name;
|
|
147
|
+
if (typeof value === "string") return value;
|
|
148
|
+
if (typeof value === "object" && value !== null) try {
|
|
149
|
+
return JSON.stringify(value);
|
|
150
|
+
} catch {
|
|
151
|
+
return "[object]";
|
|
152
|
+
}
|
|
153
|
+
return String(value);
|
|
154
|
+
}
|
|
155
|
+
function formatThrown(error) {
|
|
156
|
+
if (error instanceof Error) {
|
|
157
|
+
let message = error.message || error.name;
|
|
158
|
+
if (error.cause !== void 0) message += `\nCause: ${describeUnknown(error.cause)}`;
|
|
159
|
+
return message;
|
|
160
|
+
}
|
|
161
|
+
return describeUnknown(error);
|
|
162
|
+
}
|
|
163
|
+
function commandLine(command) {
|
|
164
|
+
if (typeof command === "string") return `$ ${command}`;
|
|
165
|
+
return command.name ? `→ ${command.name}()` : "→ (function)";
|
|
166
|
+
}
|
|
167
|
+
async function runFix(check, error) {
|
|
168
|
+
try {
|
|
169
|
+
if (typeof check.onFail === "string") execSync(check.onFail, { stdio: "inherit" });
|
|
170
|
+
else if (typeof check.onFail === "function") await check.onFail({
|
|
171
|
+
check,
|
|
172
|
+
error
|
|
173
|
+
});
|
|
174
|
+
return true;
|
|
175
|
+
} catch {
|
|
176
|
+
return false;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
function fixLabel(check) {
|
|
180
|
+
return typeof check.onFail === "string" ? `"${check.onFail}"` : "the fix function";
|
|
181
|
+
}
|
|
182
|
+
async function runChecks(config, options = {}) {
|
|
183
|
+
const { autoFix = false, interactive = true } = options;
|
|
184
|
+
const name = config.name ?? "greenly";
|
|
185
|
+
const width = bannerWidth(name);
|
|
186
|
+
const rule = (char) => char.repeat(width);
|
|
187
|
+
console.log();
|
|
188
|
+
console.log(colors.cyan(colors.bold(rule("═"))));
|
|
189
|
+
console.log(colors.cyan(colors.bold(center(name, width))));
|
|
190
|
+
console.log(colors.cyan(colors.bold(rule("═"))));
|
|
191
|
+
console.log();
|
|
192
|
+
const results = [];
|
|
193
|
+
for (const check of config.checks) {
|
|
194
|
+
console.log(colors.bold(colors.yellow(`▶ ${check.name}`)));
|
|
195
|
+
console.log(` ${colors.cyan(commandLine(check.command))}\n`);
|
|
196
|
+
const { ok, stderr } = await runCommand(check.command);
|
|
197
|
+
if (ok) {
|
|
198
|
+
console.log(`\n${colors.green(`✔ PASSED: ${check.name}`)}\n`);
|
|
199
|
+
results.push({
|
|
200
|
+
name: check.name,
|
|
201
|
+
status: "passed"
|
|
202
|
+
});
|
|
203
|
+
console.log(colors.dim(rule("─")) + "\n");
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
if (stderr) process.stderr.write(stderr.endsWith("\n") ? stderr : `${stderr}\n`);
|
|
207
|
+
console.log(`\n${colors.red(`✖ FAILED: ${check.name}`)}`);
|
|
208
|
+
if (check.onFail === void 0) {
|
|
209
|
+
if (check.optional) console.log(colors.yellow(` ${check.name} is optional, continuing.`));
|
|
210
|
+
results.push({
|
|
211
|
+
name: check.name,
|
|
212
|
+
status: check.optional ? "warned" : "failed"
|
|
213
|
+
});
|
|
214
|
+
console.log("\n" + colors.dim(rule("─")) + "\n");
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
let shouldFix = autoFix;
|
|
218
|
+
if (!autoFix && interactive) {
|
|
219
|
+
const answer = await confirm({
|
|
220
|
+
message: `Run ${fixLabel(check)} to fix ${colors.bold(check.name)}?`,
|
|
221
|
+
initialValue: true
|
|
222
|
+
});
|
|
223
|
+
if (isCancel(answer)) {
|
|
224
|
+
cancel("Aborted.");
|
|
225
|
+
return {
|
|
226
|
+
results,
|
|
227
|
+
failed: results.filter((r) => r.status === "failed").length,
|
|
228
|
+
exitCode: 1
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
shouldFix = answer;
|
|
232
|
+
}
|
|
233
|
+
if (!shouldFix) {
|
|
234
|
+
if (!interactive && !autoFix) console.log(colors.dim(` Fixer available, re-run with --yes to auto-fix.`));
|
|
235
|
+
else console.log(colors.yellow(` Skipped fix.`));
|
|
236
|
+
results.push({
|
|
237
|
+
name: check.name,
|
|
238
|
+
status: check.optional ? "warned" : "failed"
|
|
239
|
+
});
|
|
240
|
+
console.log("\n" + colors.dim(rule("─")) + "\n");
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
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`))) {
|
|
246
|
+
console.log(`\n${colors.green(`✔ Auto-fixed: ${check.name}`)}\n`);
|
|
247
|
+
results.push({
|
|
248
|
+
name: check.name,
|
|
249
|
+
status: "fixed"
|
|
250
|
+
});
|
|
251
|
+
} else {
|
|
252
|
+
console.log(`\n${colors.red(` Auto-fix failed for ${check.name}, please fix manually.`)}\n`);
|
|
253
|
+
results.push({
|
|
254
|
+
name: check.name,
|
|
255
|
+
status: check.optional ? "warned" : "failed"
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
console.log(colors.dim(rule("─")) + "\n");
|
|
259
|
+
}
|
|
260
|
+
const passed = results.filter((r) => r.status === "passed" || r.status === "fixed").length;
|
|
261
|
+
const warned = results.filter((r) => r.status === "warned").length;
|
|
262
|
+
const failedResults = results.filter((r) => r.status === "failed");
|
|
263
|
+
const failed = failedResults.length;
|
|
264
|
+
console.log(colors.cyan(colors.bold(rule("═"))));
|
|
265
|
+
const summary = colors.green(`${passed} passed`) + (warned > 0 ? colors.dim(", ") + colors.yellow(`${warned} warned`) : "") + colors.dim(", ") + (failed > 0 ? colors.red(`${failed} failed`) : colors.dim("0 failed"));
|
|
266
|
+
console.log(colors.bold(` Results: ${summary}`));
|
|
267
|
+
if (failed > 0) {
|
|
268
|
+
console.log(`\n${colors.red(colors.bold("Failed checks:"))}`);
|
|
269
|
+
for (const r of failedResults) console.log(colors.red(` • ${r.name}`));
|
|
270
|
+
console.log(`\n${colors.red(colors.bold("⚠ Fix the issues above before continuing."))}\n`);
|
|
271
|
+
} else console.log(`\n${colors.green(colors.bold("✔ All checks passed!"))}\n`);
|
|
272
|
+
return {
|
|
273
|
+
results,
|
|
274
|
+
failed,
|
|
275
|
+
exitCode: failed > 0 ? 1 : 0
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
//#endregion
|
|
279
|
+
//#region src/cli.ts
|
|
280
|
+
const HELP = `${colors.bold("greenly")} - config-driven project check runner
|
|
281
|
+
|
|
282
|
+
${colors.bold("Usage")}
|
|
283
|
+
greenly [options]
|
|
284
|
+
|
|
285
|
+
${colors.bold("Options")}
|
|
286
|
+
-y, --yes, --fix Auto-run every onFail fixer without prompting (CI / agents)
|
|
287
|
+
--no-fix Run all checks, never prompt or fix, just report
|
|
288
|
+
-v, --version Print version
|
|
289
|
+
-h, --help Show this help
|
|
290
|
+
|
|
291
|
+
${colors.bold("Config")}
|
|
292
|
+
Add a greenly.config.{ts,js,mts,mjs,cts,cjs,json} file:
|
|
293
|
+
|
|
294
|
+
import { defineConfig } from "greenly";
|
|
295
|
+
|
|
296
|
+
export default defineConfig({
|
|
297
|
+
name: "MyProject",
|
|
298
|
+
checks: [
|
|
299
|
+
{ name: "TypeScript", command: "pnpm tsc --noEmit" },
|
|
300
|
+
{ name: "Format", command: "pnpm oxfmt --check", onFail: "pnpm oxfmt" },
|
|
301
|
+
{ name: "Lint", command: "pnpm oxlint" },
|
|
302
|
+
],
|
|
303
|
+
});
|
|
304
|
+
`;
|
|
305
|
+
async function main() {
|
|
306
|
+
const parsed = parseArgs(process.argv.slice(2));
|
|
307
|
+
if (parsed.help) {
|
|
308
|
+
console.log(HELP);
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
if (parsed.version) {
|
|
312
|
+
console.log(version);
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
const mode = resolveMode(parsed, process.stdout.isTTY ?? false);
|
|
316
|
+
try {
|
|
317
|
+
const { config } = await loadGreenlyConfig();
|
|
318
|
+
const { exitCode } = await runChecks(config, mode);
|
|
319
|
+
process.exitCode = exitCode;
|
|
320
|
+
} catch (error) {
|
|
321
|
+
if (error instanceof ConfigNotFoundError || error instanceof ConfigInvalidError) {
|
|
322
|
+
console.error(colors.red(error.message));
|
|
323
|
+
process.exitCode = 1;
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
throw error;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
await main();
|
|
330
|
+
//#endregion
|
|
331
|
+
export {};
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
//#region src/lib/define-config.ts
|
|
3
|
+
/**
|
|
4
|
+
* Define a Greenly config with full type-checking and editor autocomplete.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* ```ts
|
|
8
|
+
* // greenly.config.ts
|
|
9
|
+
* import { defineConfig } from "greenly";
|
|
10
|
+
*
|
|
11
|
+
* export default defineConfig({
|
|
12
|
+
* name: "MyProject",
|
|
13
|
+
* checks: [
|
|
14
|
+
* { name: "TypeScript", command: "pnpm tsc --noEmit" },
|
|
15
|
+
* { name: "Format", command: "pnpm oxfmt --check", onFail: "pnpm oxfmt" },
|
|
16
|
+
* { name: "Lint", command: "pnpm oxlint" },
|
|
17
|
+
* ],
|
|
18
|
+
* });
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
function defineConfig(config) {
|
|
22
|
+
return config;
|
|
23
|
+
}
|
|
24
|
+
//#endregion
|
|
25
|
+
exports.defineConfig = defineConfig;
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
//#region src/lib/types.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Context passed to an `onFail` function when a check fails.
|
|
4
|
+
*/
|
|
5
|
+
interface OnFailContext {
|
|
6
|
+
/** The check that failed. */
|
|
7
|
+
check: GreenlyCheck;
|
|
8
|
+
/** The error thrown while running the check's command. */
|
|
9
|
+
error: unknown;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* A function run to fix a failing check. Invoked after the user confirms
|
|
13
|
+
* (or automatically with `--yes`/`--fix`). Throw to signal the fix failed.
|
|
14
|
+
*/
|
|
15
|
+
type OnFailFn = (ctx: OnFailContext) => void | Promise<void>;
|
|
16
|
+
/**
|
|
17
|
+
* A function run in-process as a check, instead of a shell command. May be
|
|
18
|
+
* async. Throw (or reject) to mark the check as failed; return to pass. On
|
|
19
|
+
* failure only the error's `message` (and `cause`) is shown, not a stack trace.
|
|
20
|
+
*/
|
|
21
|
+
type CommandFn = () => void | Promise<void>;
|
|
22
|
+
/**
|
|
23
|
+
* A single check to run, in order.
|
|
24
|
+
*/
|
|
25
|
+
interface GreenlyCheck {
|
|
26
|
+
/** Label shown while running and in the final summary, e.g. "TypeScript". */
|
|
27
|
+
name: string;
|
|
28
|
+
/**
|
|
29
|
+
* The check to run: a shell command string (e.g. `"pnpm tsc --noEmit"`), or
|
|
30
|
+
* a function run in-process instead. A function may be async, and must throw
|
|
31
|
+
* (or reject) to fail the check.
|
|
32
|
+
*/
|
|
33
|
+
command: string | CommandFn;
|
|
34
|
+
/**
|
|
35
|
+
* Command string, or a function, run to fix the check when it fails.
|
|
36
|
+
* The user is asked Yes/No first (skipped with `--yes`/`--fix`, or when
|
|
37
|
+
* running non-interactively). A string is executed as a shell command.
|
|
38
|
+
*/
|
|
39
|
+
onFail?: string | OnFailFn;
|
|
40
|
+
/**
|
|
41
|
+
* When `true`, a failure warns but does not fail the overall run
|
|
42
|
+
* (no non-zero exit code).
|
|
43
|
+
*/
|
|
44
|
+
optional?: boolean;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Greenly configuration. Author it with {@link defineConfig} in a
|
|
48
|
+
* `greenly.config.{ts,js,mts,mjs,cts,cjs,json}` file.
|
|
49
|
+
*/
|
|
50
|
+
interface GreenlyConfig {
|
|
51
|
+
/** Project name shown in the banner. Defaults to the package name / cwd. */
|
|
52
|
+
name?: string;
|
|
53
|
+
/** Ordered list of checks to run. */
|
|
54
|
+
checks: GreenlyCheck[];
|
|
55
|
+
}
|
|
56
|
+
//#endregion
|
|
57
|
+
//#region src/lib/define-config.d.ts
|
|
58
|
+
/**
|
|
59
|
+
* Define a Greenly config with full type-checking and editor autocomplete.
|
|
60
|
+
*
|
|
61
|
+
* @example
|
|
62
|
+
* ```ts
|
|
63
|
+
* // greenly.config.ts
|
|
64
|
+
* import { defineConfig } from "greenly";
|
|
65
|
+
*
|
|
66
|
+
* export default defineConfig({
|
|
67
|
+
* name: "MyProject",
|
|
68
|
+
* checks: [
|
|
69
|
+
* { name: "TypeScript", command: "pnpm tsc --noEmit" },
|
|
70
|
+
* { name: "Format", command: "pnpm oxfmt --check", onFail: "pnpm oxfmt" },
|
|
71
|
+
* { name: "Lint", command: "pnpm oxlint" },
|
|
72
|
+
* ],
|
|
73
|
+
* });
|
|
74
|
+
* ```
|
|
75
|
+
*/
|
|
76
|
+
declare function defineConfig(config: GreenlyConfig): GreenlyConfig;
|
|
77
|
+
//#endregion
|
|
78
|
+
export { type CommandFn, type GreenlyCheck, type GreenlyConfig, type OnFailContext, type OnFailFn, defineConfig };
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
//#region src/lib/types.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Context passed to an `onFail` function when a check fails.
|
|
4
|
+
*/
|
|
5
|
+
interface OnFailContext {
|
|
6
|
+
/** The check that failed. */
|
|
7
|
+
check: GreenlyCheck;
|
|
8
|
+
/** The error thrown while running the check's command. */
|
|
9
|
+
error: unknown;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* A function run to fix a failing check. Invoked after the user confirms
|
|
13
|
+
* (or automatically with `--yes`/`--fix`). Throw to signal the fix failed.
|
|
14
|
+
*/
|
|
15
|
+
type OnFailFn = (ctx: OnFailContext) => void | Promise<void>;
|
|
16
|
+
/**
|
|
17
|
+
* A function run in-process as a check, instead of a shell command. May be
|
|
18
|
+
* async. Throw (or reject) to mark the check as failed; return to pass. On
|
|
19
|
+
* failure only the error's `message` (and `cause`) is shown, not a stack trace.
|
|
20
|
+
*/
|
|
21
|
+
type CommandFn = () => void | Promise<void>;
|
|
22
|
+
/**
|
|
23
|
+
* A single check to run, in order.
|
|
24
|
+
*/
|
|
25
|
+
interface GreenlyCheck {
|
|
26
|
+
/** Label shown while running and in the final summary, e.g. "TypeScript". */
|
|
27
|
+
name: string;
|
|
28
|
+
/**
|
|
29
|
+
* The check to run: a shell command string (e.g. `"pnpm tsc --noEmit"`), or
|
|
30
|
+
* a function run in-process instead. A function may be async, and must throw
|
|
31
|
+
* (or reject) to fail the check.
|
|
32
|
+
*/
|
|
33
|
+
command: string | CommandFn;
|
|
34
|
+
/**
|
|
35
|
+
* Command string, or a function, run to fix the check when it fails.
|
|
36
|
+
* The user is asked Yes/No first (skipped with `--yes`/`--fix`, or when
|
|
37
|
+
* running non-interactively). A string is executed as a shell command.
|
|
38
|
+
*/
|
|
39
|
+
onFail?: string | OnFailFn;
|
|
40
|
+
/**
|
|
41
|
+
* When `true`, a failure warns but does not fail the overall run
|
|
42
|
+
* (no non-zero exit code).
|
|
43
|
+
*/
|
|
44
|
+
optional?: boolean;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Greenly configuration. Author it with {@link defineConfig} in a
|
|
48
|
+
* `greenly.config.{ts,js,mts,mjs,cts,cjs,json}` file.
|
|
49
|
+
*/
|
|
50
|
+
interface GreenlyConfig {
|
|
51
|
+
/** Project name shown in the banner. Defaults to the package name / cwd. */
|
|
52
|
+
name?: string;
|
|
53
|
+
/** Ordered list of checks to run. */
|
|
54
|
+
checks: GreenlyCheck[];
|
|
55
|
+
}
|
|
56
|
+
//#endregion
|
|
57
|
+
//#region src/lib/define-config.d.ts
|
|
58
|
+
/**
|
|
59
|
+
* Define a Greenly config with full type-checking and editor autocomplete.
|
|
60
|
+
*
|
|
61
|
+
* @example
|
|
62
|
+
* ```ts
|
|
63
|
+
* // greenly.config.ts
|
|
64
|
+
* import { defineConfig } from "greenly";
|
|
65
|
+
*
|
|
66
|
+
* export default defineConfig({
|
|
67
|
+
* name: "MyProject",
|
|
68
|
+
* checks: [
|
|
69
|
+
* { name: "TypeScript", command: "pnpm tsc --noEmit" },
|
|
70
|
+
* { name: "Format", command: "pnpm oxfmt --check", onFail: "pnpm oxfmt" },
|
|
71
|
+
* { name: "Lint", command: "pnpm oxlint" },
|
|
72
|
+
* ],
|
|
73
|
+
* });
|
|
74
|
+
* ```
|
|
75
|
+
*/
|
|
76
|
+
declare function defineConfig(config: GreenlyConfig): GreenlyConfig;
|
|
77
|
+
//#endregion
|
|
78
|
+
export { type CommandFn, type GreenlyCheck, type GreenlyConfig, type OnFailContext, type OnFailFn, defineConfig };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
//#region src/lib/define-config.ts
|
|
2
|
+
/**
|
|
3
|
+
* Define a Greenly config with full type-checking and editor autocomplete.
|
|
4
|
+
*
|
|
5
|
+
* @example
|
|
6
|
+
* ```ts
|
|
7
|
+
* // greenly.config.ts
|
|
8
|
+
* import { defineConfig } from "greenly";
|
|
9
|
+
*
|
|
10
|
+
* export default defineConfig({
|
|
11
|
+
* name: "MyProject",
|
|
12
|
+
* checks: [
|
|
13
|
+
* { name: "TypeScript", command: "pnpm tsc --noEmit" },
|
|
14
|
+
* { name: "Format", command: "pnpm oxfmt --check", onFail: "pnpm oxfmt" },
|
|
15
|
+
* { name: "Lint", command: "pnpm oxlint" },
|
|
16
|
+
* ],
|
|
17
|
+
* });
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
function defineConfig(config) {
|
|
21
|
+
return config;
|
|
22
|
+
}
|
|
23
|
+
//#endregion
|
|
24
|
+
export { defineConfig };
|
package/package.json
CHANGED
|
@@ -1,6 +1,21 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "greenly",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "1.0.1",
|
|
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
|
+
"keywords": [
|
|
6
|
+
"check",
|
|
7
|
+
"checks",
|
|
8
|
+
"ci",
|
|
9
|
+
"cli",
|
|
10
|
+
"format",
|
|
11
|
+
"lint",
|
|
12
|
+
"pr-checks",
|
|
13
|
+
"pre-commit",
|
|
14
|
+
"pre-push",
|
|
15
|
+
"task-runner",
|
|
16
|
+
"typecheck"
|
|
17
|
+
],
|
|
18
|
+
"homepage": "https://github.com/yusifaliyevpro/greenly#readme",
|
|
4
19
|
"bugs": {
|
|
5
20
|
"url": "https://github.com/yusifaliyevpro/greenly/issues"
|
|
6
21
|
},
|
|
@@ -24,7 +39,26 @@
|
|
|
24
39
|
"url": "https://kofe.al/@yusifaliyevpro"
|
|
25
40
|
}
|
|
26
41
|
],
|
|
42
|
+
"bin": {
|
|
43
|
+
"greenly": "dist/cli.js"
|
|
44
|
+
},
|
|
45
|
+
"files": [
|
|
46
|
+
"dist"
|
|
47
|
+
],
|
|
27
48
|
"type": "module",
|
|
49
|
+
"sideEffects": false,
|
|
50
|
+
"exports": {
|
|
51
|
+
".": {
|
|
52
|
+
"import": {
|
|
53
|
+
"types": "./dist/index.d.mts",
|
|
54
|
+
"default": "./dist/index.mjs"
|
|
55
|
+
},
|
|
56
|
+
"require": {
|
|
57
|
+
"types": "./dist/index.d.cts",
|
|
58
|
+
"default": "./dist/index.cjs"
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
},
|
|
28
62
|
"publishConfig": {
|
|
29
63
|
"access": "public"
|
|
30
64
|
},
|
|
@@ -33,15 +67,24 @@
|
|
|
33
67
|
"fmt": "oxfmt",
|
|
34
68
|
"fmt:check": "oxfmt --check",
|
|
35
69
|
"lint": "oxlint",
|
|
36
|
-
"lint:fix": "oxlint --fix"
|
|
70
|
+
"lint:fix": "oxlint --fix",
|
|
71
|
+
"check": "node dist/cli.js",
|
|
72
|
+
"test": "vitest run",
|
|
73
|
+
"test:watch": "vitest",
|
|
74
|
+
"start": "node dist/cli.js"
|
|
75
|
+
},
|
|
76
|
+
"dependencies": {
|
|
77
|
+
"@clack/prompts": "1.7.0",
|
|
78
|
+
"c12": "3.3.4"
|
|
37
79
|
},
|
|
38
80
|
"devDependencies": {
|
|
39
81
|
"@types/node": "24.13.3",
|
|
40
82
|
"oxfmt": "0.61.0",
|
|
41
83
|
"oxlint": "1.76.0",
|
|
42
|
-
"oxlint-tsgolint": "
|
|
84
|
+
"oxlint-tsgolint": "7.0.2001",
|
|
43
85
|
"tsdown": "0.22.14",
|
|
44
|
-
"typescript": "7.0.2"
|
|
86
|
+
"typescript": "7.0.2",
|
|
87
|
+
"vitest": "4.1.10"
|
|
45
88
|
},
|
|
46
89
|
"engines": {
|
|
47
90
|
"node": ">=18"
|
package/.vscode/extensions.json
DELETED
package/.vscode/settings.json
DELETED
package/oxfmt.config.ts
DELETED
package/oxlint.config.ts
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import { defineConfig } from "oxlint";
|
|
2
|
-
|
|
3
|
-
export default defineConfig({
|
|
4
|
-
plugins: ["typescript", "unicorn", "import"],
|
|
5
|
-
categories: {
|
|
6
|
-
suspicious: "warn",
|
|
7
|
-
},
|
|
8
|
-
options: {
|
|
9
|
-
typeAware: true,
|
|
10
|
-
typeCheck: true,
|
|
11
|
-
},
|
|
12
|
-
ignorePatterns: ["dist"],
|
|
13
|
-
rules: {
|
|
14
|
-
eqeqeq: "warn",
|
|
15
|
-
"no-throw-literal": "warn",
|
|
16
|
-
"unicorn/prefer-node-protocol": "warn",
|
|
17
|
-
"typescript/consistent-type-imports": "warn",
|
|
18
|
-
},
|
|
19
|
-
});
|
package/tsconfig.json
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"compilerOptions": {
|
|
3
|
-
"target": "es2023",
|
|
4
|
-
"module": "ESNext",
|
|
5
|
-
"moduleResolution": "bundler",
|
|
6
|
-
"resolveJsonModule": true,
|
|
7
|
-
"strict": true,
|
|
8
|
-
"noUnusedLocals": true,
|
|
9
|
-
"esModuleInterop": true,
|
|
10
|
-
"skipLibCheck": true,
|
|
11
|
-
"noEmit": true,
|
|
12
|
-
"types": ["node"]
|
|
13
|
-
},
|
|
14
|
-
"include": ["src/**/*"],
|
|
15
|
-
"exclude": ["node_modules", "dist"]
|
|
16
|
-
}
|
package/tsdown.config.ts
DELETED