@ingram-tech/nk-dev 0.2.3 → 0.2.5

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
@@ -15,7 +15,7 @@ vitest, jsdom, jest-dom, knip) as hard dependencies — so one install gives you
15
15
  the whole stack instead of re-listing each tool per site.
16
16
 
17
17
  > **Runtime vs dev-time.** nk-dev is the *dev-time* bundle. Runtime features
18
- > (`@ingram-tech/email`, `nk-db`, `nk-auth`, …) stay separate packages that
18
+ > (`@ingram-tech/nk-email`, `nk-db`, `nk-auth`, …) stay separate packages that
19
19
  > peer-depend on `next`/`react`. See the dev-toolchain carve-out in
20
20
  > [`philosophy.md`](https://github.com/ingram-technologies/nextkit/blob/main/docs/philosophy.md).
21
21
 
@@ -8,24 +8,50 @@
8
8
  *
9
9
  * Behavior:
10
10
  * - Only staged files are touched (never the whole tree).
11
+ * - Partially staged files (staged + unstaged hunks in the same file) are
12
+ * SKIPPED: `oxfmt --write` rewrites the working tree and the re-`git add`
13
+ * would silently sweep the unstaged hunks into the commit.
11
14
  * - Only extensions oxfmt understands are passed to it.
12
15
  * - oxfmt auto-discovers the repo's `.oxfmtrc.json` for house style.
13
16
  * - Files are re-staged after formatting so the commit includes the result.
14
17
  * - Formatting only — no lint gate on commit (lint runs in CI).
15
18
  */
16
19
  import { execFileSync } from "node:child_process";
20
+ import { existsSync } from "node:fs";
17
21
 
18
22
  const FORMATTABLE = /\.(jsx?|mjs|cjs|tsx?|mts|cts|json|jsonc|css|graphql|gql)$/;
19
23
 
20
- const git = (args) => execFileSync("git", args, { encoding: "utf8" }).trim();
24
+ const git = (args) => execFileSync("git", args, { encoding: "utf8" });
21
25
 
22
- const stagedFiles = git(["diff", "--cached", "--name-only", "--diff-filter=ACMRTUXB"])
23
- .split("\n")
24
- .filter(Boolean);
26
+ // -z: NUL-separated, unquoted names. The default output octal-escapes any
27
+ // non-ASCII filename ("\303\251 test.ts"), which then matches no real path and
28
+ // silently never gets formatted or re-staged.
29
+ const gitPathList = (args) =>
30
+ git([...args, "-z"])
31
+ .split("\0")
32
+ .filter(Boolean);
33
+
34
+ const stagedFiles = gitPathList([
35
+ "diff",
36
+ "--cached",
37
+ "--name-only",
38
+ "--diff-filter=ACMRTUXB",
39
+ ]);
25
40
 
26
41
  if (stagedFiles.length === 0) process.exit(0);
27
42
 
28
- const toFormat = stagedFiles.filter((f) => FORMATTABLE.test(f));
43
+ // Files that also carry unstaged edits: formatting + re-adding them would
44
+ // commit hunks the developer deliberately left out (git add -p).
45
+ const partiallyStaged = new Set(gitPathList(["diff", "--name-only"]));
46
+
47
+ const formattable = stagedFiles.filter((f) => FORMATTABLE.test(f));
48
+ const toFormat = formattable.filter((f) => !partiallyStaged.has(f));
49
+ const skipped = formattable.filter((f) => partiallyStaged.has(f));
50
+ if (skipped.length > 0) {
51
+ console.warn(
52
+ `[nextkit] not formatting partially staged file(s) (unstaged hunks would be committed): ${skipped.join(", ")}`,
53
+ );
54
+ }
29
55
  if (toFormat.length === 0) process.exit(0);
30
56
 
31
57
  try {
@@ -40,12 +66,5 @@ try {
40
66
  }
41
67
 
42
68
  // Re-stage only the files that were already staged and still exist.
43
- const existing = toFormat.filter((f) => {
44
- try {
45
- execFileSync("test", ["-e", f]);
46
- return true;
47
- } catch {
48
- return false;
49
- }
50
- });
69
+ const existing = toFormat.filter((f) => existsSync(f));
51
70
  if (existing.length > 0) git(["add", "--", ...existing]);
package/bin/nk.js CHANGED
@@ -13,8 +13,8 @@ Commands:
13
13
  init Scaffold this project to use nextkit: writes the oxlint /
14
14
  oxfmt / TypeScript / Vitest config, the format-on-commit
15
15
  hook, and the agent-guide import. Skips files that exist.
16
- dev Start the Next dev server. Boots local PGlite first when
17
- @ingram-tech/nk-db is installed (no Docker); else plain dev.
16
+ dev Start the Next dev server (Turbopack). Boots local PGlite
17
+ first when @ingram-tech/nk-db is installed (no Docker).
18
18
  format [--check] Format code with oxfmt and SQL with Prettier. --check
19
19
  verifies without writing (for CI).
20
20
  lint Lint with oxlint.
package/guide.md CHANGED
@@ -9,7 +9,7 @@ package. Stay a thin, standard Next.js app (bun · oxlint + oxfmt · strict TS).
9
9
  - **Any form that emails or stores a submission MUST use `@ingram-tech/bot-protection`**
10
10
  (server: `verifyHuman` → silently drop bots; client: honeypot + signed token).
11
11
  Never ship a form without it.
12
- - **Send email only via `@ingram-tech/email`** — never add another mail client.
12
+ - **Send email only via `@ingram-tech/nk-email`** — never add another mail client.
13
13
  - **Never trust an external request body's shape — validate it with Zod, never
14
14
  `as`-cast it.** Every `/api` route and webhook handler takes untrusted input;
15
15
  an `as` cast is a lie the type-checker can't catch at runtime.
@@ -57,9 +57,12 @@ the UI/page tree, and never expose internal plumbing under `/api/`.
57
57
  nanoids. UUIDv7 is time-ordered, so it keeps index locality instead of
58
58
  fragmenting the B-tree on random inserts, and one uniform id format spans every
59
59
  table. On Postgres ≥18 the column default is native `uuidv7()`
60
- (`uuid("id").primaryKey().default(sql\`uuidv7()\`)`); set Better Auth
61
- `advanced.database.generateId: false` so the DB not Better Auth's JS nanoid
62
- mints ids. Ids that cross a **public contract** are skinned to `prefix_base58`
60
+ (`uuid("id").primaryKey().default(sql\`uuidv7()\`)`) and Better Auth gets
61
+ `advanced.database.generateId: false` so the DB mints ids; below 18 and in
62
+ the nk-auth README's canonical example pass
63
+ `advanced.database.generateId: uuidGenerateId` (JS-minted UUIDv7 from
64
+ `@ingram-tech/nk-auth`) instead. Either way, never Better Auth's default JS
65
+ nanoid. Ids that cross a **public contract** are skinned to `prefix_base58`
63
66
  via `@ingram-tech/nk-db/id` (`createIdRegistry`) — never expose a raw UUID.
64
67
  External ids you don't mint (Stripe `cus_`, OAuth) stay `text`.
65
68
  - **Migrations don't auto-apply on deploy.** Code ships ahead of the prod schema
@@ -71,11 +74,13 @@ the UI/page tree, and never expose internal plumbing under `/api/`.
71
74
 
72
75
  ## What nextkit provides (reach for these)
73
76
 
74
- - `@ingram-tech/email` — Cloudflare email: `sendEmail`, `fromAddress`
75
- - `@ingram-tech/nk-auth` — Better Auth foundation: presets you spread into your own `betterAuth()` (mounts at `/auth` via `authBasePath`; org / JWT / passkey / pool / client helpers)
76
- - `@ingram-tech/nk-db` — Postgres data layer: `createPool` (one TLS-aware pool) + `createQueries` (raw SQL) + `createDb` (Drizzle), plus a PGlite dev/test harness at `@ingram-tech/nk-db/pglite`
77
+ - `@ingram-tech/nk-email` — Cloudflare email: `sendEmail`, `fromAddress`
78
+ - `@ingram-tech/nk-auth` — Better Auth foundation: presets you spread into your own `betterAuth()` (mounts at `/auth` via `authBasePath`; org / JWT / passkey / pool / client helpers). Don't hand-roll session reads or auth middleware — bind `createAuthHelpers` (`getUser` / `requireUser` / `redirectIfAuthenticated`, from `@ingram-tech/nk-auth/server`) and gate routes with the loop-safe `createAuthMiddleware`
79
+ - `@ingram-tech/nk-db` — Postgres data layer: `createPool` (one TLS-aware pool) + `createQueries` (raw SQL) + `createDb` (Drizzle), the PGlite dev/test harness at `@ingram-tech/nk-db/pglite`, the prefixed-id codec at `@ingram-tech/nk-db/id`, and the drift-aware migration runner at `@ingram-tech/nk-db/migrate`
80
+ - `@ingram-tech/nk-api` — the standard HTTP API seam (Hono + `@hono/zod-openapi`): one `{ error, details? }` envelope, `createApiApp` / `createRouter`, auth + multi-tenant resource-scope middleware, pagination helpers, and an emitted OpenAPI/Swagger doc. Reach for it instead of hand-rolling route handlers
81
+ - `@ingram-tech/nk-billing` — Stripe primitives: subscriptions, a Stripe-side wallet, and an optional Postgres credit ledger behind the `/credits` subpath. Prices resolve at runtime by Stripe `lookup_key` — **never hardcode a price id**, so test and live share one code path
77
82
  - `@ingram-tech/bot-protection` — invisible form protection (honeypot + timing + Vercel BotID)
78
- - `@ingram-tech/newsletter` — Supabase newsletter: subscribe / send, 1-click unsubscribe
83
+ - `@ingram-tech/nk-i18n` — type-safe, English-as-key i18n: the English source text *is* the key (no `en.json`), ICU MessageFormat, colocated JSON catalogs; routing is left to the site
79
84
  - `@ingram-tech/nk-dev` — the whole dev toolchain in one devDependency: the `nk` command (`nk dev` boots local PGlite via `@ingram-tech/nk-db` if installed, then Next; plus `nk format` / `lint` / `knip` / `check` / `type-check` / `build`), the shared oxlint + oxfmt / TypeScript / Vitest config, knip, the oxfmt format-on-commit hook, and this guide. `nk check` runs every fast checker (oxlint, oxfmt, SQL, knip) in one gate. `nk init` scaffolds a site to use it all.
80
85
 
81
86
  For detail on any package, read its README in `node_modules/@ingram-tech/<pkg>/`.
package/lib/dev.js CHANGED
@@ -3,9 +3,11 @@ import { createRequire } from "node:module";
3
3
  import { resolve } from "node:path";
4
4
 
5
5
  /**
6
- * Whether the site has `@ingram-tech/nk-db` installedi.e. its `nk-pglite-dev`
7
- * bin is available. Resolved from the site's own `node_modules`, so `nk` only
8
- * orchestrates a tool the site already provides (the carve-out).
6
+ * Whether `@ingram-tech/nk-db` is resolvable from the site the signal that
7
+ * its `nk-pglite-dev` bin is available. Note this is package *resolvability*,
8
+ * not a direct-dependency check: a transitively hoisted nk-db also flips PGlite
9
+ * mode on (harmless — the bin still runs `next dev`; it just also boots a local
10
+ * Postgres the site may not use).
9
11
  */
10
12
  function hasPgliteDev() {
11
13
  try {
@@ -26,8 +28,8 @@ function hasPgliteDev() {
26
28
  * (static/marketing sites with no database). PGlite logic lives in nk-db, not
27
29
  * here — `nk` only orchestrates.
28
30
  *
29
- * `nk dev` does not boot local Supabase; the fleet has moved off it. The few
30
- * Supabase-Postgres holdouts start it themselves until they migrate.
31
+ * `nk dev` boots no external database service when nk-db is installed it runs
32
+ * local PGlite (Postgres-in-WASM), and a site with no database just runs Next.
31
33
  */
32
34
  export function dev(extraArgs = []) {
33
35
  const command = hasPgliteDev()
@@ -38,5 +40,6 @@ export function dev(extraArgs = []) {
38
40
  }
39
41
  // spawnSync inherits stdio and blocks until exit, so Ctrl-C reaches the child.
40
42
  const res = spawnSync("bunx", command, { stdio: "inherit" });
41
- process.exit(res.status ?? 0);
43
+ // Signal-killed (status null) is a failure, not a clean exit.
44
+ process.exit(res.status ?? (res.signal ? 1 : 0));
42
45
  }
package/lib/format.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { spawnSync } from "node:child_process";
2
- import { readdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { existsSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
3
3
  import { createRequire } from "node:module";
4
4
  import { join, relative } from "node:path";
5
5
  import { FORMATTER } from "./formatter.js";
@@ -8,14 +8,20 @@ import { run } from "./run.js";
8
8
  const require = createRequire(import.meta.url);
9
9
 
10
10
  // House SQL defaults, used only when the site has no Prettier config of its own
11
- // (matches the house tab style + Supabase's Postgres dialect).
12
- const SQL_DEFAULTS = { useTabs: true, language: "postgresql" };
11
+ // (matches the house tabs/4/88 style + the PostgreSQL dialect; without the
12
+ // explicit widths Prettier falls back to 80/2).
13
+ const SQL_DEFAULTS = {
14
+ useTabs: true,
15
+ tabWidth: 4,
16
+ printWidth: 88,
17
+ language: "postgresql",
18
+ };
13
19
 
14
20
  /**
15
21
  * `nk format` / `nk format --check`.
16
22
  *
17
23
  * Code (JS/TS/JSON/CSS) goes through oxfmt; SQL goes through Prettier, which
18
- * oxfmt can't format. Prettier + prettier-plugin-sql are bundled with nk-cli,
24
+ * oxfmt can't format. Prettier + prettier-plugin-sql are bundled with nk-dev,
19
25
  * so they never appear in any app's dependencies — the "no Prettier for code"
20
26
  * rule still holds, it's just the one file type oxfmt lacks.
21
27
  */
@@ -24,13 +30,18 @@ export async function format({ check }) {
24
30
  const code = run(op[0], op[1]);
25
31
  if (code !== 0) process.exitCode = code;
26
32
 
27
- await formatSql({ check });
33
+ if (await formatSql({ check })) process.exitCode = 1;
28
34
  }
29
35
 
30
- /** Format (or, with `check`, verify) every tracked `.sql` file via Prettier. */
36
+ /**
37
+ * Format (or, with `check`, verify) every tracked `.sql` file via Prettier.
38
+ * Returns true when a check found unformatted files — the caller owns the exit
39
+ * code (inferring failure from the `process.exitCode` global misattributes any
40
+ * earlier failure to SQL).
41
+ */
31
42
  export async function formatSql({ check }) {
32
43
  const files = sqlFiles();
33
- if (files.length === 0) return;
44
+ if (files.length === 0) return false;
34
45
 
35
46
  const prettier = require("prettier");
36
47
  const pluginPath = require.resolve("prettier-plugin-sql");
@@ -38,6 +49,9 @@ export async function formatSql({ check }) {
38
49
  let unformatted = 0;
39
50
  let written = 0;
40
51
  for (const file of files) {
52
+ // git ls-files lists tracked files deleted from the worktree without
53
+ // `git rm`; reading one would throw an unhandled ENOENT.
54
+ if (!existsSync(file)) continue;
41
55
  const source = readFileSync(file, "utf8");
42
56
  // The site's own .prettierrc / package.json "prettier" wins over our
43
57
  // defaults; we always inject the bundled SQL plugin + parser.
@@ -72,24 +86,25 @@ export async function formatSql({ check }) {
72
86
  console.error(
73
87
  `nk: ${unformatted} SQL file(s) need formatting — run \`nk format\`.`,
74
88
  );
75
- process.exitCode = 1;
76
- } else if (!check && written > 0) {
89
+ return true;
90
+ }
91
+ if (!check && written > 0) {
77
92
  console.log(`nk: formatted ${written} SQL file(s).`);
78
93
  }
94
+ return false;
79
95
  }
80
96
 
81
97
  /** Tracked + untracked-not-ignored `.sql` files; falls back to an fs walk. */
82
98
  function sqlFiles() {
99
+ // -z: NUL-separated, unquoted — the default output octal-escapes non-ASCII
100
+ // filenames, which then match no real path.
83
101
  const res = spawnSync(
84
102
  "git",
85
- ["ls-files", "--cached", "--others", "--exclude-standard", "*.sql"],
103
+ ["ls-files", "-z", "--cached", "--others", "--exclude-standard", "*.sql"],
86
104
  { encoding: "utf8" },
87
105
  );
88
106
  if (res.status === 0) {
89
- return res.stdout
90
- .split("\n")
91
- .map((s) => s.trim())
92
- .filter(Boolean);
107
+ return res.stdout.split("\0").filter(Boolean);
93
108
  }
94
109
  return walkSql(process.cwd());
95
110
  }
@@ -19,8 +19,7 @@ export async function check() {
19
19
  // hide another. oxc splits lint (oxlint) and format (oxfmt), so we run both.
20
20
  const lintFailed = run(FORMATTER.lint[0], FORMATTER.lint[1]) !== 0;
21
21
  const fmtFailed = run(FORMATTER.checkFormat[0], FORMATTER.checkFormat[1]) !== 0;
22
- await formatSql({ check: true });
23
- const sqlFailed = Boolean(process.exitCode);
22
+ const sqlFailed = await formatSql({ check: true });
24
23
  // knip (unused deps/exports/files). Opt-in: only when the repo has a knip
25
24
  // config — knip has no shareable config, so absence means "not adopted".
26
25
  const knipFailed = hasKnipConfig() ? runKnip() !== 0 : false;
package/lib/run.js CHANGED
@@ -12,29 +12,9 @@ export function run(tool, args = [], opts = {}) {
12
12
  }
13
13
  throw res.error;
14
14
  }
15
- return res.status ?? 0;
16
- }
17
-
18
- /**
19
- * Run a tool capturing its stdout (stderr still streams to the terminal).
20
- * Exits the process if the tool fails — callers depend on the captured output.
21
- */
22
- export function capture(tool, args = [], opts = {}) {
23
- const res = spawnSync("bunx", [tool, ...args], {
24
- encoding: "utf8",
25
- stdio: ["inherit", "pipe", "inherit"],
26
- ...opts,
27
- });
28
- if (res.error) {
29
- if (res.error.code === "ENOENT") {
30
- fail("could not run `bunx` — is bun installed and on PATH?");
31
- }
32
- throw res.error;
33
- }
34
- if (res.status !== 0) {
35
- fail(`\`${tool} ${args.join(" ")}\` exited with ${res.status}`);
36
- }
37
- return res.stdout ?? "";
15
+ // A signal-killed child (OOM, SIGSEGV) has status null — that's a failure,
16
+ // not a pass; `?? 0` would let a crashed linter through the CI gate.
17
+ return res.status ?? (res.signal ? 1 : 0);
38
18
  }
39
19
 
40
20
  /** Print an `nk:`-prefixed error and exit non-zero. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ingram-tech/nk-dev",
3
- "version": "0.2.3",
3
+ "version": "0.2.5",
4
4
  "description": "The nextkit dev toolchain in one package: the `nk` CLI plus shared oxlint/oxfmt, TypeScript, and Vitest config, the format-on-commit hook, and the AI agent guide. `nk init` scaffolds a site to use it.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -45,13 +45,13 @@
45
45
  "dependencies": {
46
46
  "@testing-library/jest-dom": "^6.9.1",
47
47
  "jsdom": "^29.1.1",
48
- "knip": "^6.17.1",
49
- "oxfmt": "^0.55.0",
50
- "oxlint": "^1.70.0",
51
- "prettier": "^3.8.3",
48
+ "knip": "^6.23.0",
49
+ "oxfmt": "^0.56.0",
50
+ "oxlint": "^1.71.0",
51
+ "prettier": "^3.9.3",
52
52
  "prettier-plugin-sql": "^0.20.0",
53
53
  "typescript": "^6.0.3",
54
- "vitest": "^4.1.6"
54
+ "vitest": "^4.1.9"
55
55
  },
56
56
  "engines": {
57
57
  "node": ">=20"
package/tier-b.json CHANGED
@@ -5,10 +5,6 @@
5
5
  "error",
6
6
  {
7
7
  "paths": [
8
- {
9
- "name": "@supabase/supabase-js",
10
- "message": "Golden-path data access uses @ingram-tech/nk-db (direct pg + Drizzle), not supabase-js."
11
- },
12
8
  {
13
9
  "name": "pg",
14
10
  "importNames": ["Pool", "Client"],