greenly 0.0.1 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,31 +1,120 @@
1
1
  # greenly
2
2
 
3
- > 🚧 Coming soon
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
- A config-driven pre-ship checks runner. Define your preflight steps once in a
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
- ## Status
7
+ ## Install
9
8
 
10
- **This package is under active development and not ready for use yet.**
9
+ ```bash
10
+ pnpm add -D greenly
11
+ ```
11
12
 
12
- The name is reserved while the first release is being built. Installing it now
13
- will get you a placeholder. Star or watch the repo to know when `1.0.0` lands.
13
+ ## Quick start
14
14
 
15
- ## What it will do
15
+ Create a `greenly.config.ts` at your project root:
16
16
 
17
- - Define your check steps in an `greenly.config.ts`
18
- - Run them all with one command (e.g. `pnpm check`)
19
- - Clean, interactive output for pass/fail steps
17
+ ```ts
18
+ import { defineConfig } from "greenly";
20
19
 
21
- ## Install
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
- Not yet recommended. Once the first release is out:
31
+ Run it:
24
32
 
25
33
  ```bash
26
- npm install -D greenly
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,326 @@
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.0";
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
+ process.exit(1);
226
+ }
227
+ shouldFix = answer;
228
+ }
229
+ if (!shouldFix) {
230
+ if (!interactive && !autoFix) console.log(colors.dim(` Fixer available, re-run with --yes to auto-fix.`));
231
+ else console.log(colors.yellow(` Skipped fix.`));
232
+ results.push({
233
+ name: check.name,
234
+ status: check.optional ? "warned" : "failed"
235
+ });
236
+ console.log("\n" + colors.dim(rule("─")) + "\n");
237
+ continue;
238
+ }
239
+ const fixDisplay = typeof check.onFail === "string" ? check.onFail : "fix function";
240
+ console.log(`\n ${colors.cyan(`$ ${fixDisplay}`)}\n`);
241
+ if (await runFix(check, new Error(`${check.name} failed`))) {
242
+ console.log(`\n${colors.green(`✔ Auto-fixed: ${check.name}`)}\n`);
243
+ results.push({
244
+ name: check.name,
245
+ status: "fixed"
246
+ });
247
+ } else {
248
+ console.log(`\n${colors.red(` Auto-fix failed for ${check.name}, please fix manually.`)}\n`);
249
+ results.push({
250
+ name: check.name,
251
+ status: check.optional ? "warned" : "failed"
252
+ });
253
+ }
254
+ console.log(colors.dim(rule("─")) + "\n");
255
+ }
256
+ const passed = results.filter((r) => r.status === "passed" || r.status === "fixed").length;
257
+ const warned = results.filter((r) => r.status === "warned").length;
258
+ const failedResults = results.filter((r) => r.status === "failed");
259
+ const failed = failedResults.length;
260
+ console.log(colors.cyan(colors.bold(rule("═"))));
261
+ 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"));
262
+ console.log(colors.bold(` Results: ${summary}`));
263
+ if (failed > 0) {
264
+ console.log(`\n${colors.red(colors.bold("Failed checks:"))}`);
265
+ for (const r of failedResults) console.log(colors.red(` • ${r.name}`));
266
+ console.log(`\n${colors.red(colors.bold("⚠ Fix the issues above before continuing."))}\n`);
267
+ } else console.log(`\n${colors.green(colors.bold("✔ All checks passed!"))}\n`);
268
+ return {
269
+ results,
270
+ failed,
271
+ exitCode: failed > 0 ? 1 : 0
272
+ };
273
+ }
274
+ //#endregion
275
+ //#region src/cli.ts
276
+ const HELP = `${colors.bold("greenly")} - config-driven project check runner
277
+
278
+ ${colors.bold("Usage")}
279
+ greenly [options]
280
+
281
+ ${colors.bold("Options")}
282
+ -y, --yes, --fix Auto-run every onFail fixer without prompting (CI / agents)
283
+ --no-fix Run all checks, never prompt or fix, just report
284
+ -v, --version Print version
285
+ -h, --help Show this help
286
+
287
+ ${colors.bold("Config")}
288
+ Add a greenly.config.{ts,js,mts,mjs,cts,cjs,json} file:
289
+
290
+ import { defineConfig } from "greenly";
291
+
292
+ export default defineConfig({
293
+ name: "MyProject",
294
+ checks: [
295
+ { name: "TypeScript", command: "pnpm tsc --noEmit" },
296
+ { name: "Format", command: "pnpm oxfmt --check", onFail: "pnpm oxfmt" },
297
+ { name: "Lint", command: "pnpm oxlint" },
298
+ ],
299
+ });
300
+ `;
301
+ async function main() {
302
+ const parsed = parseArgs(process.argv.slice(2));
303
+ if (parsed.help) {
304
+ console.log(HELP);
305
+ return;
306
+ }
307
+ if (parsed.version) {
308
+ console.log(version);
309
+ return;
310
+ }
311
+ const mode = resolveMode(parsed, process.stdout.isTTY ?? false);
312
+ try {
313
+ const { config } = await loadGreenlyConfig();
314
+ const { exitCode } = await runChecks(config, mode);
315
+ process.exit(exitCode);
316
+ } catch (error) {
317
+ if (error instanceof ConfigNotFoundError || error instanceof ConfigInvalidError) {
318
+ console.error(colors.red(error.message));
319
+ process.exit(1);
320
+ }
321
+ throw error;
322
+ }
323
+ }
324
+ await main();
325
+ //#endregion
326
+ 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;
@@ -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 };
@@ -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": "0.0.1",
3
+ "version": "1.0.0",
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,7 +67,15 @@
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",
@@ -41,7 +83,8 @@
41
83
  "oxlint": "1.76.0",
42
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"
@@ -1,3 +0,0 @@
1
- {
2
- "recommendations": ["oxc.oxc-vscode"]
3
- }
@@ -1,4 +0,0 @@
1
- {
2
- "editor.defaultFormatter": "oxc.oxc-vscode",
3
- "editor.formatOnSave": true
4
- }
package/oxfmt.config.ts DELETED
@@ -1,10 +0,0 @@
1
- import { defineConfig } from "oxfmt";
2
-
3
- export default defineConfig({
4
- printWidth: 120,
5
- singleQuote: false,
6
- insertFinalNewline: true,
7
- sortImports: {
8
- newlinesBetween: false,
9
- },
10
- });
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
@@ -1,3 +0,0 @@
1
- import { defineConfig } from "tsdown";
2
-
3
- export default defineConfig({});