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 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
- npm install -g taste-lint
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
- taste-lint scan .
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.3";
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$1 = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : {};
3066
+ const object$2 = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : {};
3067
3067
  const json = (u) => {
3068
3068
  try {
3069
- return object$1(JSON.parse(u.text));
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$1(value.bin)).filter((v) => typeof v === "string"));
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$1(value.scripts).build) throw new UnresolvedError(`Build output not available: ${entry}. Build or inspect the packed artifact.`);
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$1(value.bin)).filter((v) => typeof v === "string");
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$1(json(u).exports));
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$1(value[key])))]);
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$1(entry).path;
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);