taste-lint 0.0.3 → 0.0.4
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 +5 -3
- package/dist/cli.js +129 -9
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -16,10 +16,10 @@ Scan your project with local checks and [Jev by TypeSafe AI](https://docs.typesa
|
|
|
16
16
|
## Install
|
|
17
17
|
|
|
18
18
|
```bash
|
|
19
|
-
|
|
19
|
+
npx taste-lint@latest init
|
|
20
20
|
```
|
|
21
21
|
|
|
22
|
-
Requires Node 24.11 or later.
|
|
22
|
+
Requires Node 24.11 or later. Run from your project directory to install locally and add check scripts.
|
|
23
23
|
|
|
24
24
|
## Quickstart
|
|
25
25
|
|
|
@@ -27,11 +27,13 @@ Create a [Vercel AI Gateway key](https://vercel.com/docs/ai-gateway/authenticati
|
|
|
27
27
|
|
|
28
28
|
```bash
|
|
29
29
|
export AI_GATEWAY_API_KEY="your-vercel-ai-gateway-key"
|
|
30
|
-
|
|
30
|
+
npm run taste
|
|
31
31
|
```
|
|
32
32
|
|
|
33
33
|
No account or config for taste-lint. AI checks send selected text and rule context to Vercel AI Gateway, billed to your account. Answers are cached for repeat runs.
|
|
34
34
|
|
|
35
|
+
Run `npm run check:taste` for local checks without a key. Use your package manager in place of npm. Add `--agent` to init for agent instructions, or `--dry-run` to preview setup.
|
|
36
|
+
|
|
35
37
|
## What it checks
|
|
36
38
|
|
|
37
39
|
- **Product interfaces:** copy, typography, interaction, and motion in JSX, TSX, and CSS.
|
package/dist/cli.js
CHANGED
|
@@ -16,7 +16,7 @@ import { mdxjs } from "micromark-extension-mdxjs";
|
|
|
16
16
|
import { styleText } from "node:util";
|
|
17
17
|
import os from "node:os";
|
|
18
18
|
//#region package.json
|
|
19
|
-
var version = "0.0.
|
|
19
|
+
var version = "0.0.4";
|
|
20
20
|
//#endregion
|
|
21
21
|
//#region src/lib/stats.ts
|
|
22
22
|
const makePRNG = (seed) => {
|
|
@@ -3063,10 +3063,10 @@ const at = (evidence, offset = 0) => ({
|
|
|
3063
3063
|
fired: true,
|
|
3064
3064
|
offset
|
|
3065
3065
|
});
|
|
3066
|
-
const object$
|
|
3066
|
+
const object$2 = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
3067
3067
|
const json = (u) => {
|
|
3068
3068
|
try {
|
|
3069
|
-
return object$
|
|
3069
|
+
return object$2(JSON.parse(u.text));
|
|
3070
3070
|
} catch {
|
|
3071
3071
|
throw new UnresolvedError("JSON is not parseable");
|
|
3072
3072
|
}
|
|
@@ -3101,7 +3101,7 @@ const REPOSITORY_RULES = [
|
|
|
3101
3101
|
"typings"
|
|
3102
3102
|
]) if (typeof value[key] === "string") entries.push(value[key]);
|
|
3103
3103
|
if (typeof value.bin === "string") entries.push(value.bin);
|
|
3104
|
-
else entries.push(...Object.values(object$
|
|
3104
|
+
else entries.push(...Object.values(object$2(value.bin)).filter((v) => typeof v === "string"));
|
|
3105
3105
|
const visit = (v) => {
|
|
3106
3106
|
if (typeof v === "string") entries.push(v);
|
|
3107
3107
|
else if (v && typeof v === "object") for (const child of Object.values(v)) visit(child);
|
|
@@ -3112,7 +3112,7 @@ const REPOSITORY_RULES = [
|
|
|
3112
3112
|
const resolved = u.facts.repository.resolve(u.file, entry);
|
|
3113
3113
|
if (resolved === void 0) return at(`Entry escapes repository: ${entry}`);
|
|
3114
3114
|
if (!u.facts.repository.exists(resolved)) {
|
|
3115
|
-
if (/^(?:\.\/)?(?:dist|build|lib)\//.test(entry) && object$
|
|
3115
|
+
if (/^(?:\.\/)?(?:dist|build|lib)\//.test(entry) && object$2(value.scripts).build) throw new UnresolvedError(`Build output not available: ${entry}. Build or inspect the packed artifact.`);
|
|
3116
3116
|
return at(`Missing package entry: ${entry}`);
|
|
3117
3117
|
}
|
|
3118
3118
|
}
|
|
@@ -3120,7 +3120,7 @@ const REPOSITORY_RULES = [
|
|
|
3120
3120
|
}, "Build and inspect the package, then make bin, exports, and declaration paths match its actual artifacts."),
|
|
3121
3121
|
make$1("dx", "package-bin-shebang", "CLI executable has no Node shebang", PKG, "scaffold-cli/references/scaffold-source.md", (u) => {
|
|
3122
3122
|
const value = json(u);
|
|
3123
|
-
const bins = typeof value.bin === "string" ? [value.bin] : Object.values(object$
|
|
3123
|
+
const bins = typeof value.bin === "string" ? [value.bin] : Object.values(object$2(value.bin)).filter((v) => typeof v === "string");
|
|
3124
3124
|
for (const bin of bins) {
|
|
3125
3125
|
if (!/\.[cm]?js$/.test(bin)) continue;
|
|
3126
3126
|
const resolved = u.facts.repository.resolve(u.file, bin);
|
|
@@ -3132,7 +3132,7 @@ const REPOSITORY_RULES = [
|
|
|
3132
3132
|
return none;
|
|
3133
3133
|
}, "Emit exactly one Node shebang in the published executable."),
|
|
3134
3134
|
make$1("dx", "exports-mixed-keys", "Package exports mixes conditions and subpaths", PKG, "dx-audit/rules/api-stable-contract.md", (u) => {
|
|
3135
|
-
const keys = Object.keys(object$
|
|
3135
|
+
const keys = Object.keys(object$2(json(u).exports));
|
|
3136
3136
|
return keys.some((k) => k.startsWith(".")) && keys.some((k) => !k.startsWith(".")) ? at("Exports mixes subpath keys with condition keys at the same level") : none;
|
|
3137
3137
|
}, "Put conditions inside each exported subpath, or use a conditions-only root export."),
|
|
3138
3138
|
make$1("architecture", "undeclared-import", "Import has no declared package dependency", CODE, GUARD, (u) => {
|
|
@@ -3145,7 +3145,7 @@ const REPOSITORY_RULES = [
|
|
|
3145
3145
|
"devDependencies",
|
|
3146
3146
|
"peerDependencies",
|
|
3147
3147
|
"optionalDependencies"
|
|
3148
|
-
].flatMap((key) => Object.keys(object$
|
|
3148
|
+
].flatMap((key) => Object.keys(object$2(value[key])))]);
|
|
3149
3149
|
for (const item of u.facts.imports) {
|
|
3150
3150
|
if (/^(?:\.|\/|#|~|[a-z]+:)/i.test(item.name) || builtins.has(item.name)) continue;
|
|
3151
3151
|
const name = item.name.startsWith("@") ? item.name.split("/").slice(0, 2).join("/") : item.name.split("/")[0];
|
|
@@ -3171,7 +3171,7 @@ const REPOSITORY_RULES = [
|
|
|
3171
3171
|
make$1("architecture", "missing-tsconfig-reference", "TypeScript project reference points to a missing target", ["**/tsconfig*.json"], GUARD, (u) => {
|
|
3172
3172
|
const value = json(u);
|
|
3173
3173
|
for (const entry of Array.isArray(value.references) ? value.references : []) {
|
|
3174
|
-
const target = object$
|
|
3174
|
+
const target = object$2(entry).path;
|
|
3175
3175
|
if (typeof target !== "string") continue;
|
|
3176
3176
|
const resolved = u.facts.repository.resolve(u.file, target);
|
|
3177
3177
|
if (!resolved) throw new UnresolvedError("Project reference outside repository");
|
|
@@ -4554,6 +4554,125 @@ function registerExtractCommand(program) {
|
|
|
4554
4554
|
});
|
|
4555
4555
|
}
|
|
4556
4556
|
//#endregion
|
|
4557
|
+
//#region src/setup/init.ts
|
|
4558
|
+
const managers = [
|
|
4559
|
+
"npm",
|
|
4560
|
+
"pnpm",
|
|
4561
|
+
"yarn",
|
|
4562
|
+
"bun"
|
|
4563
|
+
];
|
|
4564
|
+
const lockfiles = {
|
|
4565
|
+
bun: ["bun.lock", "bun.lockb"],
|
|
4566
|
+
npm: ["package-lock.json", "npm-shrinkwrap.json"],
|
|
4567
|
+
pnpm: ["pnpm-lock.yaml"],
|
|
4568
|
+
yarn: ["yarn.lock"]
|
|
4569
|
+
};
|
|
4570
|
+
const marker = "<!-- taste-lint -->";
|
|
4571
|
+
const agentText = `${marker}
|
|
4572
|
+
## Taste Lint
|
|
4573
|
+
|
|
4574
|
+
Run the local check with the project's check:taste script after editing copy or UI.
|
|
4575
|
+
For AI checks, preview the taste script with --dry-run, then run it with a user-supplied AI_GATEWAY_API_KEY.
|
|
4576
|
+
Fix act findings, review advisory findings in context, and recheck the edited files.
|
|
4577
|
+
Never invent a key or treat unknown checks as passes.
|
|
4578
|
+
<!-- /taste-lint -->
|
|
4579
|
+
`;
|
|
4580
|
+
function object$1(value) {
|
|
4581
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4582
|
+
}
|
|
4583
|
+
function initProject(options) {
|
|
4584
|
+
const root = path.resolve(options.root);
|
|
4585
|
+
const manifestPath = path.join(root, "package.json");
|
|
4586
|
+
let manifest;
|
|
4587
|
+
try {
|
|
4588
|
+
const parsed = JSON.parse(fs.readFileSync(manifestPath, "utf-8"));
|
|
4589
|
+
if (!object$1(parsed)) throw new Error("Expected an object");
|
|
4590
|
+
manifest = parsed;
|
|
4591
|
+
} catch {
|
|
4592
|
+
throw new InputError("INVALID_PROJECT", "Run init in a project with a valid package.json, or use --root <path>.");
|
|
4593
|
+
}
|
|
4594
|
+
const declared = typeof manifest.packageManager === "string" ? manifest.packageManager.split("@")[0] : void 0;
|
|
4595
|
+
const detected = managers.filter((manager) => lockfiles[manager].some((file) => fs.existsSync(path.join(root, file))));
|
|
4596
|
+
const selected = options.pm ?? declared ?? (detected.length === 1 ? detected[0] : void 0);
|
|
4597
|
+
if (!selected && detected.length > 1) throw new InputError("AMBIGUOUS_PACKAGE_MANAGER", "Multiple lockfiles found. Choose --pm npm, pnpm, yarn, or bun.");
|
|
4598
|
+
const pm = selected ?? "npm";
|
|
4599
|
+
if (!managers.includes(pm)) throw new InputError("INVALID_PACKAGE_MANAGER", "Choose --pm npm, pnpm, yarn, or bun.");
|
|
4600
|
+
for (const field of [
|
|
4601
|
+
"scripts",
|
|
4602
|
+
"dependencies",
|
|
4603
|
+
"devDependencies"
|
|
4604
|
+
]) if (manifest[field] !== void 0 && !object$1(manifest[field])) throw new InputError("INVALID_PROJECT", `package.json ${field} must be an object.`);
|
|
4605
|
+
const dependencies = {
|
|
4606
|
+
...manifest.dependencies,
|
|
4607
|
+
...manifest.devDependencies
|
|
4608
|
+
};
|
|
4609
|
+
const profile = [
|
|
4610
|
+
"react",
|
|
4611
|
+
"next",
|
|
4612
|
+
"vue",
|
|
4613
|
+
"svelte",
|
|
4614
|
+
"astro"
|
|
4615
|
+
].some((name) => name in dependencies) ? "product" : "writing";
|
|
4616
|
+
const scripts = { ...manifest.scripts };
|
|
4617
|
+
const added = [];
|
|
4618
|
+
for (const [name, command] of Object.entries({
|
|
4619
|
+
"check:taste": `taste-lint scan . --profile ${profile} --mechanical-only`,
|
|
4620
|
+
taste: `taste-lint scan . --profile ${profile}`
|
|
4621
|
+
})) if (!(name in scripts)) {
|
|
4622
|
+
scripts[name] = command;
|
|
4623
|
+
added.push(name);
|
|
4624
|
+
}
|
|
4625
|
+
const agentPath = path.join(root, "AGENTS.md");
|
|
4626
|
+
const previousAgent = options.agent && fs.existsSync(agentPath) ? fs.readFileSync(agentPath, "utf-8") : "";
|
|
4627
|
+
const addAgent = Boolean(options.agent && !previousAgent.includes(marker));
|
|
4628
|
+
const needsDependency = !("taste-lint" in dependencies);
|
|
4629
|
+
const installArgs = pm === "npm" ? ["install", "--save-dev"] : ["add", "--dev"];
|
|
4630
|
+
installArgs.push(`taste-lint@^${version}`);
|
|
4631
|
+
const shouldInstall = needsDependency && options.install !== false;
|
|
4632
|
+
const result = {
|
|
4633
|
+
agentAdded: addAgent,
|
|
4634
|
+
dryRun: Boolean(options.dryRun),
|
|
4635
|
+
installCommand: shouldInstall ? [pm, ...installArgs] : null,
|
|
4636
|
+
packageManager: pm,
|
|
4637
|
+
profile,
|
|
4638
|
+
root,
|
|
4639
|
+
scriptsAdded: added
|
|
4640
|
+
};
|
|
4641
|
+
if (options.dryRun) return result;
|
|
4642
|
+
if (shouldInstall) {
|
|
4643
|
+
const installed = spawnSync(pm, installArgs, {
|
|
4644
|
+
cwd: root,
|
|
4645
|
+
shell: false,
|
|
4646
|
+
stdio: [
|
|
4647
|
+
"ignore",
|
|
4648
|
+
"inherit",
|
|
4649
|
+
"inherit"
|
|
4650
|
+
]
|
|
4651
|
+
});
|
|
4652
|
+
if (installed.error || installed.status !== 0) throw new InputError("INSTALL_FAILED", `Installation failed. Run ${pm} ${installArgs.join(" ")} and retry init.`);
|
|
4653
|
+
manifest = JSON.parse(fs.readFileSync(manifestPath, "utf-8"));
|
|
4654
|
+
}
|
|
4655
|
+
if (added.length > 0) {
|
|
4656
|
+
manifest.scripts = scripts;
|
|
4657
|
+
fs.writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`);
|
|
4658
|
+
}
|
|
4659
|
+
if (addAgent) fs.writeFileSync(agentPath, `${previousAgent}${previousAgent ? "\n\n" : ""}${agentText}`);
|
|
4660
|
+
return result;
|
|
4661
|
+
}
|
|
4662
|
+
//#endregion
|
|
4663
|
+
//#region src/commands/init.ts
|
|
4664
|
+
function registerInitCommand(program) {
|
|
4665
|
+
program.command("init").description("Install Taste Lint locally and add project check scripts").option("--root <path>", "Project directory", process.cwd()).option("--pm <name>", "Package manager: npm, pnpm, yarn, or bun").option("--dry-run", "Preview setup without installing or writing files").option("--no-install", "Add scripts without installing the package").option("--agent", "Append Taste Lint guidance to AGENTS.md").action((options) => {
|
|
4666
|
+
const result = initProject(options);
|
|
4667
|
+
process.stdout.write(`${result.dryRun ? "Setup preview" : "Setup complete"}: ${result.packageManager}, ${result.profile} profile\n`);
|
|
4668
|
+
process.stdout.write(`Scripts added: ${result.scriptsAdded.join(", ") || "none (existing scripts preserved)"}\n`);
|
|
4669
|
+
if (result.installCommand) process.stdout.write(`Install: ${result.installCommand.join(" ")}\n`);
|
|
4670
|
+
if (result.agentAdded) process.stdout.write("Agent instructions: AGENTS.md\n");
|
|
4671
|
+
process.stdout.write(`\nRun ${result.packageManager} run check:taste for local checks.\nSet AI_GATEWAY_API_KEY, then run ${result.packageManager} run taste for AI checks.\n`);
|
|
4672
|
+
if (options.install === false) process.stdout.write("Installation skipped; the scripts require taste-lint to be installed.\n");
|
|
4673
|
+
});
|
|
4674
|
+
}
|
|
4675
|
+
//#endregion
|
|
4557
4676
|
//#region src/lib/spawn.ts
|
|
4558
4677
|
function spawnCapture(cmd, argv, options = {}) {
|
|
4559
4678
|
return new Promise((resolve, reject) => {
|
|
@@ -6430,6 +6549,7 @@ Without a key: taste-lint scan . --mechanical-only
|
|
|
6430
6549
|
Get a key: https://vercel.com/docs/ai-gateway/authentication-and-byok/api-keys
|
|
6431
6550
|
`);
|
|
6432
6551
|
registerLintCommand(program);
|
|
6552
|
+
registerInitCommand(program);
|
|
6433
6553
|
registerScanCommand(program);
|
|
6434
6554
|
registerExtractCommand(program);
|
|
6435
6555
|
registerRulesCommand(program);
|