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.1.0
to
1.1.1
+185
-81
dist/cli.js

@@ -10,3 +10,4 @@ #!/usr/bin/env node

//#region package.json
var version = "1.1.0";
var name = "greenly";
var version = "1.1.1";
//#endregion

@@ -97,2 +98,87 @@ //#region src/lib/args.ts

//#endregion
//#region src/lib/utils.ts
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 detectLockfiles(cwd) {
return [
"pnpm-lock.yaml",
"yarn.lock",
"package-lock.json",
"bun.lockb",
"bun.lock"
].filter((f) => existsSync(join(cwd, f)));
}
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";
}
//#endregion
//#region src/lib/version.ts
const parse = (v) => {
const [core = "", pre = ""] = v.trim().replace(/^[v^~>=< ]+/, "").split("-", 2);
return {
nums: core.split(".").map((n) => Number.parseInt(n, 10) || 0),
pre
};
};
function compareVersions(a, b) {
const pa = parse(a);
const pb = parse(b);
for (let i = 0; i < 3; i++) {
const diff = (pa.nums[i] ?? 0) - (pb.nums[i] ?? 0);
if (diff !== 0) return diff > 0 ? 1 : -1;
}
if (pa.pre === pb.pre) return 0;
if (pa.pre === "") return 1;
if (pb.pre === "") return -1;
return pa.pre > pb.pre ? 1 : -1;
}
function isNewer(latest, current) {
return compareVersions(latest, current) > 0;
}
async function fetchLatestVersion(pkg, timeoutMs = 3e3) {
try {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), timeoutMs);
try {
const res = await fetch(`https://registry.npmjs.org/${encodeURIComponent(pkg)}/latest`, {
signal: controller.signal,
headers: { accept: "application/vnd.npm.install-v1+json" }
});
if (!res.ok) return null;
const data = await res.json();
if (data && typeof data === "object" && "version" in data) {
const version = data.version;
return typeof version === "string" ? version : null;
}
return null;
} finally {
clearTimeout(timer);
}
} catch {
return null;
}
}
async function checkForUpdate(pkg, current) {
const latest = await fetchLatestVersion(pkg);
if (latest && isNewer(latest, current)) return {
current,
latest
};
return null;
}
//#endregion
//#region src/lib/init.ts

@@ -107,2 +193,3 @@ const execAsync = promisify(exec);

}
const dep = (name) => (deps) => deps.has(name);
const CHECK_PRESETS = [

@@ -112,5 +199,6 @@ {

label: "TypeScript (tsc)",
detect: dep("typescript"),
build: (c) => ({
name: "TypeScript",
command: scriptCommand(c, ["typecheck", "type-check"]) ?? `${c.exec} tsc --noEmit${c.isNext ? " --incremental false" : ""}`
command: scriptCommand(c, ["typecheck", "type-check"]) ?? `${c.run} tsc --noEmit${dep("next")(c.deps) ? " --incremental false" : ""}`
})

@@ -121,6 +209,7 @@ },

label: "Oxfmt",
detect: dep("oxfmt"),
build: (c) => ({
name: "Oxfmt",
command: scriptCommand(c, ["fmt:check", "format:check"]) ?? `${c.exec} oxfmt --check`,
onFail: scriptCommand(c, ["fmt", "format"]) ?? `${c.exec} oxfmt`
command: scriptCommand(c, ["fmt:check", "format:check"]) ?? `${c.run} oxfmt --check`,
onFail: scriptCommand(c, ["fmt", "format"]) ?? `${c.run} oxfmt`
})

@@ -131,6 +220,7 @@ },

label: "Prettier",
detect: dep("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 .`
command: scriptCommand(c, ["fmt:check", "format:check"]) ?? `${c.run} prettier --check .`,
onFail: scriptCommand(c, ["fmt", "format"]) ?? `${c.run} prettier --write .`
})

@@ -141,5 +231,6 @@ },

label: "Oxlint",
detect: dep("oxlint"),
build: (c) => ({
name: "Oxlint",
command: scriptCommand(c, ["lint"]) ?? `${c.exec} oxlint`
command: scriptCommand(c, ["lint"]) ?? `${c.run} oxlint`
})

@@ -150,5 +241,6 @@ },

label: "ESLint",
detect: dep("eslint"),
build: (c) => ({
name: "ESLint",
command: scriptCommand(c, ["lint"]) ?? `${c.exec} eslint .`
command: scriptCommand(c, ["lint"]) ?? `${c.run} eslint .`
})

@@ -159,8 +251,18 @@ },

label: "Tests (Vitest)",
detect: dep("vitest"),
build: (c) => ({
name: "Tests",
command: scriptCommand(c, ["test"]) ?? `${c.exec} vitest run`
command: scriptCommand(c, ["test"]) ?? `${c.run} vitest run`
})
},
{
value: "expo-doctor",
label: "Expo Doctor",
detect: dep("expo"),
build: (c) => ({
name: "Expo Doctor",
command: scriptCommand(c, ["doctor"]) ?? `${c.exec} expo-doctor`
})
},
{
value: "build",

@@ -172,2 +274,11 @@ label: "Build",

})
},
{
value: "react-doctor",
label: "React Doctor",
detect: dep("react"),
build: (c) => ({
name: "React Doctor",
command: `${dep("react-doctor")(c.deps) ? c.run : c.exec} react-doctor --verbose`
})
}

@@ -178,7 +289,7 @@ ];

case "pnpm": return {
exec: "pnpm",
exec: "pnpx",
run: "pnpm"
};
case "yarn": return {
exec: "yarn",
exec: "yarn dlx",
run: "yarn"

@@ -196,25 +307,6 @@ };

}
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,
deps: opts.deps ?? new Set(),
scripts: opts.scripts ?? new Set()

@@ -224,10 +316,2 @@ };

}
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) {

@@ -237,20 +321,38 @@ const scripts = pkg?.scripts;

}
function depRecord(pkg, field) {
const deps = pkg?.[field];
return isRecord(deps) ? deps : null;
}
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);
const deps = depRecord(pkg, field);
if (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);
function greenlyLocation(pkg) {
const inField = (field) => {
const deps = depRecord(pkg, field);
return deps !== null && "greenly" in deps;
};
prefer("oxlint", "eslint");
prefer("oxfmt", "prettier");
return CHECK_PRESETS.filter((p) => !hidden.has(p.value));
if (inField("devDependencies")) return "dev";
if (inField("dependencies")) return "prod";
return "none";
}
function declaredGreenlyVersion(pkg) {
for (const field of ["devDependencies", "dependencies"]) {
const version = depRecord(pkg, field)?.greenly;
if (typeof version === "string") return version;
}
return null;
}
function shouldOfferInstall(opts) {
const { location, declaredVersion, latestVersion } = opts;
if (location === "dev" && declaredVersion && latestVersion && !isNewer(latestVersion, declaredVersion)) return false;
return true;
}
function availablePresets(installed) {
return CHECK_PRESETS.filter((p) => !p.detect || p.detect(installed));
}
function configFileName(ext) {

@@ -298,21 +400,2 @@ return `greenly.config.${ext}`;

}
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) {

@@ -331,2 +414,3 @@ if (isCancel(value)) {

const pm = detectPackageManager(process.env.npm_config_user_agent, detectLockfiles(cwd));
const latestPromise = fetchLatestVersion("greenly");
const name = ensure(await text({

@@ -350,3 +434,4 @@ message: "Project name (shown in the banner)",

}));
const presets = availablePresets(installedDependencies(pkg));
const installed = installedDependencies(pkg);
const presets = availablePresets(installed);
const selected = ensure(await multiselect({

@@ -360,3 +445,10 @@ message: "Select the checks to include",

}));
const doInstall = ensure(await confirm({
const declaredVersion = declaredGreenlyVersion(pkg);
const latestVersion = await latestPromise;
const alreadyLatest = !shouldOfferInstall({
location: greenlyLocation(pkg),
declaredVersion,
latestVersion
});
const doInstall = alreadyLatest ? false : ensure(await confirm({
message: `Install greenly now with ${pm}?`,

@@ -376,8 +468,6 @@ initialValue: true

}
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,
deps: installed,
scripts: packageScripts(pkg)

@@ -412,3 +502,4 @@ }),

}
} else log.info(`Skipped install. Run "${installCommand(pm)}" when ready.`);
} else if (alreadyLatest) log.info(`greenly ${colors.bold(declaredVersion ?? "")} already a devDependency at latest, skipped install.`);
else log.info(`Skipped install. Run "${installCommand(pm)}" when ready.`);
const runCmd = `${pmContext(pm).run} ${scriptName}`;

@@ -439,3 +530,4 @@ outro(`Done. Run ${colors.bold(runCmd)} to run your checks.`);

ok: false,
stderr: colors.red(formatThrown(error))
stderr: colors.red(formatThrown(error)),
error
};

@@ -465,3 +557,4 @@ }

ok: false,
stderr
stderr,
error
};

@@ -507,2 +600,5 @@ }

}
function fixCommand(check) {
return typeof check.onFail === "string" ? check.onFail : "fix function";
}
async function runChecks(config, options = {}) {

@@ -522,3 +618,3 @@ const { autoFix = false, interactive = true } = options;

console.log(` ${colors.cyan(commandLine(check.command))}\n`);
const { ok, stderr } = await runCommand(check.command);
const { ok, stderr, error } = await runCommand(check.command);
if (ok) {

@@ -570,5 +666,4 @@ console.log(`\n${colors.green(`✔ PASSED: ${check.name}`)}\n`);

}
const fixDisplay = typeof check.onFail === "string" ? check.onFail : "fix function";
console.log(`\n ${colors.cyan(`$ ${fixDisplay}`)}\n`);
if (await runFix(check, new Error(`${check.name} failed`))) {
console.log(`\n ${colors.cyan(`$ ${fixCommand(check)}`)}\n`);
if (await runFix(check, error)) {
console.log(`\n${colors.green(`✔ Auto-fixed: ${check.name}`)}\n`);

@@ -634,2 +729,7 @@ results.push({

`;
function printUpdateNotice(info) {
const pm = detectPackageManager(process.env.npm_config_user_agent, detectLockfiles(process.cwd()));
console.log(colors.yellow(`Update available: greenly ${colors.dim(info.current)} -> ${colors.bold(info.latest)}`));
console.log(colors.dim(`Run ${colors.bold(installCommand(pm))} to update.`) + "\n");
}
async function main() {

@@ -650,3 +750,5 @@ const argv = process.argv.slice(2);

}
const mode = resolveMode(parsed, process.stdout.isTTY ?? false);
const isTTY = process.stdout.isTTY ?? false;
const mode = resolveMode(parsed, isTTY);
const updateCheck = isTTY ? checkForUpdate(name, version) : null;
try {

@@ -656,2 +758,4 @@ const { config } = await loadGreenlyConfig();

process.exitCode = exitCode;
const update = updateCheck ? await updateCheck : null;
if (update) printUpdateNotice(update);
} catch (error) {

@@ -658,0 +762,0 @@ if (error instanceof ConfigNotFoundError || error instanceof ConfigInvalidError) {

@@ -5,3 +5,3 @@ //#region src/lib/types.d.ts

*/
interface OnFailContext {
type OnFailContext = {
/** The check that failed. */

@@ -11,3 +11,3 @@ check: GreenlyCheck;

error: unknown;
}
};
/**

@@ -27,3 +27,3 @@ * A function run to fix a failing check. Invoked after the user confirms

*/
interface GreenlyCheck {
type GreenlyCheck = {
/** Label shown while running and in the final summary, e.g. "TypeScript". */

@@ -48,3 +48,3 @@ name: string;

optional?: boolean;
}
};
/**

@@ -54,3 +54,3 @@ * Greenly configuration. Author it with {@link defineConfig} in a

*/
interface GreenlyConfig {
type GreenlyConfig = {
/** Project name shown in the banner. Defaults to the package name / cwd. */

@@ -60,3 +60,3 @@ name?: string;

checks: GreenlyCheck[];
}
};
//#endregion

@@ -63,0 +63,0 @@ //#region src/lib/define-config.d.ts

@@ -5,3 +5,3 @@ //#region src/lib/types.d.ts

*/
interface OnFailContext {
type OnFailContext = {
/** The check that failed. */

@@ -11,3 +11,3 @@ check: GreenlyCheck;

error: unknown;
}
};
/**

@@ -27,3 +27,3 @@ * A function run to fix a failing check. Invoked after the user confirms

*/
interface GreenlyCheck {
type GreenlyCheck = {
/** Label shown while running and in the final summary, e.g. "TypeScript". */

@@ -48,3 +48,3 @@ name: string;

optional?: boolean;
}
};
/**

@@ -54,3 +54,3 @@ * Greenly configuration. Author it with {@link defineConfig} in a

*/
interface GreenlyConfig {
type GreenlyConfig = {
/** Project name shown in the banner. Defaults to the package name / cwd. */

@@ -60,3 +60,3 @@ name?: string;

checks: GreenlyCheck[];
}
};
//#endregion

@@ -63,0 +63,0 @@ //#region src/lib/define-config.d.ts

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

@@ -5,0 +5,0 @@ "keywords": [

+57
-16

@@ -1,2 +0,5 @@

# greenly
<h1>
<img src="https://raw.githubusercontent.com/yusifaliyevpro/greenly/main/assets/greenly.svg" alt="" height="34" align="top" />
greenly
</h1>

@@ -10,13 +13,20 @@ [![npm version](https://img.shields.io/npm/v/greenly?color=3fb950&label=npm)](https://www.npmjs.com/package/greenly)

> Config-driven project check runner. Define your lint / format / typecheck / test steps once in `greenly.config.ts` and run them with a single command.
> 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.
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.
## Why greenly
**greenly runs all your lint, format, typecheck, test, build, and custom checks from one config file with a single command.**
Instead of pushing and waiting for CI to catch a formatting slip or a type error, run
`pnpm greenly` before you open a PR. It puts your CI checks and local checks in the same
place, so if everything is green locally, it is green in CI. Checks run in order with
their output streamed live, and greenly offers to auto-fix the ones that have a fixer.
It works where your tools do. In an interactive terminal greenly prompts before running a
fixer; in an **agent terminal or CI (non-TTY)** it skips prompts and just reports pass or
fail, so it never hangs and an agent can run `greenly` directly.
## 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:
Run this and greenly sets everything up for you, based on the tools your project already uses:

@@ -75,2 +85,33 @@ ```bash

## Use it in CI
The same command you run locally is the command CI runs. Replace your separate check
steps with one:
```diff
- run: pnpm install --frozen-lockfile
- - name: TypeScript
- run: pnpm tsc --noEmit
-
- - name: Oxfmt
- run: pnpm fmt:check
-
- - name: Oxlint
- run: pnpm lint
-
- - name: Tests
- run: pnpm test
-
- - name: Build
- run: pnpm build
-
- - name: Version
- run: node --no-warnings scripts/version-check.ts
-
+ # pnpm greenly
+ - name: Run checks
+ run: pnpm check
```
## Config reference

@@ -131,10 +172,10 @@

| 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. |
| Command / Flag | Description |
| ---------------------- | ------------------------------------------------------------------------ |
| `greenly` | Run the checks from `greenly.config.*`. |
| `greenly init` | Set up a config by answering a few questions, then 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. |

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