Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,16 @@ on:
branches: [main]
pull_request:
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: '24'
cache: npm
- run: npm ci
- run: npm run lint
test:
runs-on: ubuntu-latest
strategy:
Expand Down
2 changes: 1 addition & 1 deletion ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ _Second external review, this one of the codebase rather than the experience. Ra
**The theme worth acting on: Packkit doesn't eat its own cooking.** It generates `publint` + are-the-types-wrong checks, strict TypeScript, and lint gates for other people's packages, then ships without any of them. `npm run check:pkg` is something Packkit writes for you, not something it runs on itself — and its own package would fail it.

- [ ] **Type declarations for the public API** — `exports` exposes `.`, `./core`, `./cli`, `./scaffold`, with **no `types`**. Programmatic consumers and the MCP server get nothing. JSDoc + `tsc --declaration` is enough; a rewrite isn't needed. Types wanted for `PackkitConfig`, the resolved config, `GenerateResult`, the file map, preset names, and scaffold results. **v3 requirement.**
- [ ] **Self-enforcement** — root scripts are `start, test, check:deps, integration, build:web, update:node, gen:reference, sync:mcp`. No lint, no format check, no typecheck. Add a `check` script running all of them, plus `tsc --allowJs --checkJs --noEmit` to catch API drift even while the source stays JS.
- [x] ~~**Self-enforcement**~~**Shipped (3.1).** Packkit now lints its own source with the stack it generates for users (eslint flat config + `@eslint/js`): `npm run lint`, a `check` script (lint + test), and a CI lint job. It immediately earned its keep — caught a real duplicate `minify` key in the CLI arg parser plus several dead vars/imports, all fixed. Full `tsc --checkJs` was assessed and deferred: 95 errors, nearly all the incremental-object-literal pattern (noise), not worth annotating around right now — though it did surface the dead `reactEntry` reference removed in the CLI-on-embedded PR.
- [ ] **Validate generated paths at the writer** — `writeProject` joins each relative path onto the target and writes it. Fine while every feature is trusted first-party code; a security boundary the moment third-party features, community recipes, or MCP-supplied data can contribute paths. Reject absolute paths, `..` escapes, and post-normalization collisions **in the writer**, not per feature — before extensibility ships, not after.
- [ ] **Collision diagnostics** — `deepMerge` ends in `return source`, so when two features set the same `scripts.build` or `exports` key the last one silently wins. Additive fields (dependencies) should keep merging; `scripts`, `exports`, `bin`, `files` should report which features collided. Same for two features emitting the same file path.
- [ ] **Structured subprocess errors** — `run()` returns a boolean and discards stdout/stderr when quiet, so "install failed" can't distinguish a missing executable from a network error, an unconfigured git identity, or a rejected push. Return `{ ok, command, exitCode, stdout, stderr, category }`; the CLI can still print something friendly, while MCP callers get something actionable.
Expand Down
7 changes: 2 additions & 5 deletions docs/packkit-core.js
Original file line number Diff line number Diff line change
Expand Up @@ -1303,7 +1303,6 @@ import { app } from './app.js';`;
}
const api = runner === "jest" ? `` : `import { describe, it, expect } from 'vitest';
`;
const expectApi = runner === "jest" ? "" : "";
return [
api + `import { greet } from '${imp}';`,
``,
Expand Down Expand Up @@ -1471,7 +1470,7 @@ var githooks_default = {
pkg.scripts.prepare = "husky";
pkg.devDependencies.husky = "^9.1.0";
} else if (cfg.gitHooks === "lefthook") {
files["lefthook.yml"] = lefthookYml(cfg, staged);
files["lefthook.yml"] = lefthookYml(cfg);
pkg.scripts.prepare = "lefthook install || true";
pkg.devDependencies.lefthook = "^2.0.0";
}
Expand All @@ -1482,7 +1481,7 @@ var githooks_default = {
return { files, pkg };
}
};
function lefthookYml(cfg, staged) {
function lefthookYml(cfg) {
const cmd = cfg.lint === "biome" ? "biome check --write --no-errors-on-unmatched {staged_files}" : cfg.lint === "none" ? "npm test" : "prettier --write {staged_files}";
return [
"pre-commit:",
Expand Down Expand Up @@ -2498,7 +2497,6 @@ function buildMonorepo(cfg) {
test: exampleTest2(`import { shout } from './index.js';`, `expect(shout('world')).toBe('HELLO, WORLD!')`),
deps: { [core]: wsProto }
});
const install = pm === "npm" ? "npm install" : `${pm} install`;
return {
config: cfg,
files,
Expand All @@ -2517,7 +2515,6 @@ function buildFullstack(cfg) {
const scope = cfg.name.replace(/^@/, "").split("/")[0];
const shared = `@${scope}/shared`;
const wsProto = pm === "pnpm" ? "workspace:*" : "*";
const run2 = (s) => pm === "npm" ? `npm run ${s}` : `${pm} ${s}`;
for (const feat of [community_default, agents_default, gitfiles_default]) {
if (feat.active(cfg)) Object.assign(files, feat.apply(cfg).files);
}
Expand Down
36 changes: 36 additions & 0 deletions eslint.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// Packkit lints its own source with the same stack it generates for users
// (eslint flat config + @eslint/js recommended). Kept deliberately lean: this
// is here to catch real mistakes — undefined names, unreachable code, unused
// values — not to enforce a style (formatting isn't wired up yet).

import js from '@eslint/js';
import globals from 'globals';

export default [
{
ignores: ['docs/packkit-core.js', 'node_modules/**', 'coverage/**'],
},
js.configs.recommended,
{
files: ['**/*.js', '**/*.mjs'],
languageOptions: {
ecmaVersion: 2023,
sourceType: 'module',
globals: { ...globals.node },
},
rules: {
// Flag unused values, but let intentionally-ignored args (prefixed _) pass.
'no-unused-vars': ['error', { argsIgnorePattern: '^_', varsIgnorePattern: '^_' }],
'no-undef': 'error',
'no-constant-condition': ['error', { checkLoops: false }],
},
},
{
// The web configurator runs in the browser, so it has DOM globals, not Node.
// JSZip is loaded from a <script> tag in index.html (the zip download).
files: ['docs/**/*.js'],
languageOptions: {
globals: { ...globals.browser, JSZip: 'readonly' },
},
},
];
Loading