New:Socket for Asana Is Now Available.Learn more
Get Started

greenly

Package Overview
Dependencies
Maintainers
1
Versions
10
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

greenly - npm Package Compare versions

Comparing version
1.0.1
to
1.1.0
+322
-13
dist/cli.js
#!/usr/bin/env node
import { existsSync } from "node:fs";
import { resolve } from "node:path";
import { loadConfig } from "c12";
import { execSync } from "node:child_process";
import { cancel, confirm, isCancel } from "@clack/prompts";
import { existsSync, readFileSync, writeFileSync } from "node:fs";
import { basename, join, resolve } from "node:path";
import { pathToFileURL } from "node:url";
import { createJiti } from "jiti";
import { exec, execSync } from "node:child_process";
import { promisify } from "node:util";
import { cancel, confirm, intro, isCancel, log, multiselect, outro, select, spinner, text } from "@clack/prompts";
//#region package.json
var version = "1.0.1";
var version = "1.1.0";
//#endregion

@@ -43,4 +45,3 @@ //#region src/lib/args.ts

//#endregion
//#region src/lib/config.ts
const CONFIG_BASENAME = "greenly.config";
//#region src/lib/constants.ts
const CONFIG_EXTENSIONS = [

@@ -55,2 +56,5 @@ "ts",

];
//#endregion
//#region src/lib/config.ts
const CONFIG_BASENAME = "greenly.config";
var ConfigNotFoundError = class extends Error {

@@ -81,6 +85,3 @@ cwd;

if (!configFile) throw new ConfigNotFoundError(cwd);
const { config } = await loadConfig({
cwd,
configFile
});
const config = await createJiti(pathToFileURL(resolve(cwd, "greenly.config")).href).import(configFile, { default: true });
if (!config || typeof config !== "object") throw new ConfigInvalidError(configFile, "config must export an object");

@@ -98,2 +99,304 @@ if (!Array.isArray(config.checks) || config.checks.length === 0) throw new ConfigInvalidError(configFile, `"checks" must be a non-empty array`);

//#endregion
//#region src/lib/init.ts
const execAsync = promisify(exec);
function isRecord(value) {
return typeof value === "object" && value !== null;
}
function scriptCommand(ctx, candidates) {
const found = candidates.find((s) => ctx.scripts.has(s));
return found ? `${ctx.run} ${found}` : null;
}
const CHECK_PRESETS = [
{
value: "typescript",
label: "TypeScript (tsc)",
build: (c) => ({
name: "TypeScript",
command: scriptCommand(c, ["typecheck", "type-check"]) ?? `${c.exec} tsc --noEmit${c.isNext ? " --incremental false" : ""}`
})
},
{
value: "oxfmt",
label: "Oxfmt",
build: (c) => ({
name: "Oxfmt",
command: scriptCommand(c, ["fmt:check", "format:check"]) ?? `${c.exec} oxfmt --check`,
onFail: scriptCommand(c, ["fmt", "format"]) ?? `${c.exec} oxfmt`
})
},
{
value: "prettier",
label: "Prettier",
build: (c) => ({
name: "Prettier",
command: scriptCommand(c, ["fmt:check", "format:check"]) ?? `${c.exec} prettier --check .`,
onFail: scriptCommand(c, ["fmt", "format"]) ?? `${c.exec} prettier --write .`
})
},
{
value: "oxlint",
label: "Oxlint",
build: (c) => ({
name: "Oxlint",
command: scriptCommand(c, ["lint"]) ?? `${c.exec} oxlint`
})
},
{
value: "eslint",
label: "ESLint",
build: (c) => ({
name: "ESLint",
command: scriptCommand(c, ["lint"]) ?? `${c.exec} eslint .`
})
},
{
value: "vitest",
label: "Tests (Vitest)",
build: (c) => ({
name: "Tests",
command: scriptCommand(c, ["test"]) ?? `${c.exec} vitest run`
})
},
{
value: "build",
label: "Build",
build: (c) => ({
name: "Build",
command: scriptCommand(c, ["build"]) ?? `${c.run} build`
})
}
];
function pmContext(pm) {
switch (pm) {
case "pnpm": return {
exec: "pnpm",
run: "pnpm"
};
case "yarn": return {
exec: "yarn",
run: "yarn"
};
case "bun": return {
exec: "bunx",
run: "bun run"
};
default: return {
exec: "npx",
run: "npm run"
};
}
}
function installCommand(pm) {
switch (pm) {
case "pnpm": return "pnpm add -D greenly@latest";
case "yarn": return "yarn add -D greenly@latest";
case "bun": return "bun add -d greenly@latest";
default: return "npm install -D greenly@latest";
}
}
function detectPackageManager(userAgent, lockfiles) {
const ua = userAgent ?? "";
if (ua.startsWith("pnpm")) return "pnpm";
if (ua.startsWith("yarn")) return "yarn";
if (ua.startsWith("bun")) return "bun";
if (ua.startsWith("npm")) return "npm";
if (lockfiles.includes("pnpm-lock.yaml")) return "pnpm";
if (lockfiles.includes("yarn.lock")) return "yarn";
if (lockfiles.includes("bun.lockb") || lockfiles.includes("bun.lock")) return "bun";
return "npm";
}
function buildChecks(selected, pm, opts = {}) {
const ctx = {
...pmContext(pm),
isNext: opts.isNext ?? false,
scripts: opts.scripts ?? new Set()
};
return CHECK_PRESETS.filter((p) => selected.includes(p.value)).map((p) => p.build(ctx));
}
function isNextProject(pkg, hasNextConfig) {
if (hasNextConfig) return true;
const hasNextDep = (field) => {
const deps = pkg?.[field];
return isRecord(deps) && "next" in deps;
};
return hasNextDep("dependencies") || hasNextDep("devDependencies");
}
function packageScripts(pkg) {
const scripts = pkg?.scripts;
return isRecord(scripts) ? new Set(Object.keys(scripts)) : new Set();
}
function installedDependencies(pkg) {
const names = new Set();
for (const field of ["dependencies", "devDependencies"]) {
const deps = pkg?.[field];
if (isRecord(deps)) for (const key of Object.keys(deps)) names.add(key);
}
return names;
}
function availablePresets(installed) {
const hidden = new Set();
const prefer = (a, b) => {
if (installed.has(a) && !installed.has(b)) hidden.add(b);
if (installed.has(b) && !installed.has(a)) hidden.add(a);
};
prefer("oxlint", "eslint");
prefer("oxfmt", "prettier");
return CHECK_PRESETS.filter((p) => !hidden.has(p.value));
}
function configFileName(ext) {
return `greenly.config.${ext}`;
}
function renderCheck(check) {
const parts = [`name: ${JSON.stringify(check.name)}`, `command: ${JSON.stringify(check.command)}`];
if (check.onFail) parts.push(`onFail: ${JSON.stringify(check.onFail)}`);
return `{ ${parts.join(", ")} }`;
}
function isEsm(ext, isModule) {
if (ext === "ts" || ext === "mts" || ext === "mjs") return true;
if (ext === "cts" || ext === "cjs") return false;
return isModule;
}
function renderConfig(opts) {
const { name, checks, ext, isModule } = opts;
if (ext === "json") return `${JSON.stringify({
name,
checks
}, null, 2)}\n`;
const body = checks.map((c) => ` ${renderCheck(c)},`).join("\n");
const object = `{\n name: ${JSON.stringify(name)},\n checks: [\n${body}\n ],\n}`;
if (isEsm(ext, isModule)) return `import { defineConfig } from "greenly";\n\nexport default defineConfig(${object});\n`;
return `const { defineConfig } = require("greenly");\n\nmodule.exports = defineConfig(${object});\n`;
}
function withCheckScript(pkg, scriptName) {
const scripts = isRecord(pkg.scripts) ? pkg.scripts : {};
return {
...pkg,
scripts: {
...scripts,
[scriptName]: "greenly"
}
};
}
function readPackageJson(path) {
if (!existsSync(path)) return null;
try {
const parsed = JSON.parse(readFileSync(path, "utf8"));
if (isRecord(parsed)) return parsed;
} catch {}
return null;
}
function detectLockfiles(cwd) {
return [
"pnpm-lock.yaml",
"yarn.lock",
"package-lock.json",
"bun.lockb",
"bun.lock"
].filter((f) => existsSync(join(cwd, f)));
}
function hasNextConfigFile(cwd) {
return [
"js",
"mjs",
"cjs",
"ts",
"mts",
"cts"
].some((e) => existsSync(join(cwd, `next.config.${e}`)));
}
function ensure(value) {
if (isCancel(value)) {
cancel("init cancelled.");
process.exit(0);
}
return value;
}
async function runInit(cwd = process.cwd()) {
intro(colors.inverse(" greenly ") + colors.dim(" init "));
const pkgPath = join(cwd, "package.json");
const pkg = readPackageJson(pkgPath);
const defaultName = typeof pkg?.name === "string" ? pkg.name : basename(cwd);
const pm = detectPackageManager(process.env.npm_config_user_agent, detectLockfiles(cwd));
const name = ensure(await text({
message: "Project name (shown in the banner)",
initialValue: defaultName,
validate: (value) => value?.trim() ? void 0 : "Please enter a project name"
}));
const ext = ensure(await select({
message: "Config file format",
options: CONFIG_EXTENSIONS.map((e) => ({
value: e,
label: configFileName(e)
})),
initialValue: "ts"
}));
const scriptName = ensure(await text({
message: "Script name to add to package.json",
initialValue: "check",
validate: (value) => value?.trim() ? void 0 : "Please enter a script name"
}));
const presets = availablePresets(installedDependencies(pkg));
const selected = ensure(await multiselect({
message: "Select the checks to include",
options: presets.map((p) => ({
value: p.value,
label: p.label
})),
required: true
}));
const doInstall = ensure(await confirm({
message: `Install greenly now with ${pm}?`,
initialValue: true
}));
const fileName = configFileName(ext);
const filePath = join(cwd, fileName);
if (existsSync(filePath)) {
if (!ensure(await confirm({
message: `${fileName} already exists. Overwrite it?`,
initialValue: false
}))) {
cancel("Kept the existing config. Nothing changed.");
process.exit(0);
}
}
const isNext = isNextProject(pkg, hasNextConfigFile(cwd));
if (isNext && selected.includes("typescript")) log.info(colors.dim("Detected Next.js, using tsc --incremental false"));
const content = renderConfig({
name,
checks: buildChecks(selected, pm, {
isNext,
scripts: packageScripts(pkg)
}),
ext,
isModule: pkg?.type === "module"
});
writeFileSync(filePath, content);
log.success(`Created ${colors.bold(fileName)}`);
if (pkg) {
const current = (isRecord(pkg.scripts) ? pkg.scripts : {})[scriptName];
let write = true;
if (typeof current === "string" && current !== "greenly") write = ensure(await confirm({
message: `Script "${scriptName}" already runs "${current}". Overwrite with "greenly"?`,
initialValue: false
}));
if (write) {
writeFileSync(pkgPath, `${JSON.stringify(withCheckScript(pkg, scriptName), null, 2)}\n`);
log.success(`Added ${colors.bold(`"${scriptName}": "greenly"`)} to package.json`);
}
} else log.warn("No package.json found, skipped adding the script.");
if (doInstall) {
const s = spinner();
s.start(`Installing greenly with ${pm}`);
try {
await execAsync(installCommand(pm), { cwd });
s.stop("Installed greenly");
} catch {
s.stop("Could not install greenly automatically");
log.warn(`Run "${installCommand(pm)}" yourself.`);
}
} else log.info(`Skipped install. Run "${installCommand(pm)}" when ready.`);
const runCmd = `${pmContext(pm).run} ${scriptName}`;
outro(`Done. Run ${colors.bold(runCmd)} to run your checks.`);
}
//#endregion
//#region src/lib/runner.ts

@@ -288,2 +591,3 @@ const MIN_WIDTH = 60;

greenly [options]
greenly init Scaffold a greenly.config file interactively

@@ -311,3 +615,8 @@ ${colors.bold("Options")}

async function main() {
const parsed = parseArgs(process.argv.slice(2));
const argv = process.argv.slice(2);
if (argv[0] === "init") {
await runInit();
return;
}
const parsed = parseArgs(argv);
if (parsed.help) {

@@ -314,0 +623,0 @@ console.log(HELP);

+2
-2
{
"name": "greenly",
"version": "1.0.1",
"version": "1.1.0",
"description": "Config-driven project check runner. Define your lint/format/typecheck/test steps in greenly.config.ts and run them with one command.",

@@ -78,3 +78,3 @@ "keywords": [

"@clack/prompts": "1.7.0",
"c12": "3.3.4"
"jiti": "2.7.0"
},

@@ -81,0 +81,0 @@ "devDependencies": {

# greenly
[![npm version](https://img.shields.io/npm/v/greenly?color=3fb950&label=npm)](https://www.npmjs.com/package/greenly)
[![npm downloads](https://img.shields.io/npm/dm/greenly?color=3fb950)](https://www.npmjs.com/package/greenly)
[![unpacked size](https://img.shields.io/npm/unpacked-size/greenly?color=3fb950)](https://www.npmjs.com/package/greenly)
[![Socket Badge](https://badge.socket.dev/npm/package/greenly/1.0.1)](https://socket.dev/npm/package/greenly)
[![PR Checks](https://github.com/yusifaliyevpro/greenly/actions/workflows/pr-checks.yml/badge.svg)](https://github.com/yusifaliyevpro/greenly/actions/workflows/pr-checks.yml)
[![license](https://img.shields.io/npm/l/greenly?color=3fb950)](https://github.com/yusifaliyevpro/greenly/blob/main/LICENSE)
> Config-driven project check runner. Define your lint / format / typecheck / test steps once in `greenly.config.ts` and run them with a single command.

@@ -7,4 +14,19 @@

## Quick start
The fastest way to get going is the interactive scaffolder. It asks for a project
name, config format, script name, and which checks to include, then writes the config,
adds a `"check": "greenly"` script, and installs greenly. It adapts to your project:
it reuses matching `package.json` scripts (e.g. `fmt:check`, `lint`) when they exist,
only offers the lint/format tools you already use, and detects Next.js:
```bash
pnpx greenly init
# or: npx greenly init
```
## Install
Or set it up manually:
```bash

@@ -14,4 +36,2 @@ pnpm add -D greenly

## Quick start
Create a `greenly.config.ts` at your project root:

@@ -109,10 +129,12 @@

## CLI flags
## CLI
| Flag | Description |
| ---------------------- | ------------------------------------------------------------------------ |
| `-y`, `--yes`, `--fix` | Auto-run every `onFail` fixer without prompting (great for CI / agents). |
| `--no-fix` | Run all checks, never prompt or fix, just report. |
| `-v`, `--version` | Print the version. |
| `-h`, `--help` | Show help. |
| Command / Flag | Description |
| ---------------------- | ---------------------------------------------------------------------------------- |
| `greenly` | Run the checks from `greenly.config.*`. |
| `greenly init` | Scaffold a config interactively (format, script name, checks) and install greenly. |
| `-y`, `--yes`, `--fix` | Auto-run every `onFail` fixer without prompting (great for CI / agents). |
| `--no-fix` | Run all checks, never prompt or fix, just report. |
| `-v`, `--version` | Print the version. |
| `-h`, `--help` | Show help. |

@@ -119,0 +141,0 @@ 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.