Embedded project-generation API (3.0.0)#17
Conversation
Packkit can now run inside another Node application as a
project-generation engine. A host resolves a preset, generates the
project entirely in memory, layers its own deployment files on, and
writes to disk when ready — no prompts, installs, git, network, or
command execution anywhere in the path.
New public API (create-packkit, /embedded, /writer):
- createProject(input) — preset + overrides → GeneratedProject in memory
- extendProject(project, ext) — immutable; add files/package.json fields
with a collision policy (default error)
- writeGeneratedProject({project, destination}) — the only disk touch;
re-validates every path, refuses traversal, runs nothing else
- exportProjectDefinition / createProjectFromDefinition — serialize and
reproduce a project; schema- and version-checked
- calculateProjectDigest — stable over config + file contents
- deriveDeploymentContract — provider-neutral build/runtime description
Supporting work:
- normalizeConfig now optionally records every coercion it makes, so a
host learns what Packkit changed (STORYBOOK_DISABLED_FOR_TARGET, etc.)
instead of it happening silently. Backward compatible — callers that
pass no collector get identical behavior.
- generate() delegates to a new assemble() that tracks per-file and
per-fragment provenance, powering file- and package.json-conflict
diagnostics without changing generated output.
- Path validation module rejects absolute, `..`-escaping, null-byte,
Windows-reserved, and case-insensitive-colliding paths, applied both
when a project is built and again at the writer boundary.
- TypeScript declarations for the whole public surface; package now ships
a `types` entry and types/ directory.
- Root export is now src/index.js (core + embedded); ./core stays the
browser-safe core-only entry. Additive for existing consumers.
Tests: 23 new (unit + filesystem) covering resolution, diagnostics,
immutability, collisions, path traversal, determinism, definition
round-trip, and the writer. Integration harness gains --embedded, wired
into CI for ts-lib, cli, react-app, and node-service — each generated
through the API, extended, written, then installed/built/tested/linted,
with the deployment contract and digest verified. Packed-tarball checks
confirm every export resolves and the CLI still works from an install.
All 49 existing tests unchanged; CLI, web configurator, MCP, and presets
behavior preserved.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Both merge blockers and every "fix before merge" / "strongly preferred"
item from the review.
Blockers:
- Writer now follows real inodes and rejects a symlink anywhere between
the destination and target (and a symlinked destination itself), so a
`dest/link -> /outside` can no longer redirect a write past the lexical
containment check. Covers the final path component too, closing the
overwrite-through-symlink hole.
- Definition replay records whether each extension file was an add or a
replace. On replay under a newer Packkit, an original "add" that now
collides with a generated file is surfaced as an error-severity
diagnostic instead of silently overwriting — the stored copy still
reproduces, but the drift is visible. (SCHEMA_VERSION -> 2.)
Also fixed:
- Dependency conflicts key by section+name, so differing ranges across
dependencies vs peerDependencies aren't reported as a conflict.
- exists()/lstat helpers only treat ENOENT as "absent"; permission and
I/O errors surface instead of being read as a missing file.
- Extension file contents validated as strings up front.
- Host package.json overrides now emit EXTENSION_PACKAGE_FIELD_OVERRIDE /
EXTENSION_DEPENDENCY_VERSION_OVERRIDE — the host still wins, visibly.
- Writer preflights all collisions under the error policy and writes
nothing if any exist (no partial output).
- Single assembly pass: generate() delegates to generateTracked(), which
both the public result and the embedded provenance read from.
- createProjectFromDefinition validates definition structure, rejects
prototype-pollution keys, and caps file count / total size for
untrusted stores.
- deepMerge skips __proto__/constructor/prototype defensively.
- Deployment contract no longer marks PORT required (it has a default).
- Internal extension state moved to a WeakMap, so GeneratedProject no
longer carries a `_extensions` field.
- Recursive protected-field flattening (array key paths, dot-safe).
- PackkitValidationError.code and PackkitWriteError.{path,destination,cause}.
12 new tests cover symlink traversal, collision preflight, replay drift,
override diagnostics, content validation, per-section dep conflicts,
prototype-pollution defense, definition limits, and state encapsulation.
84 tests pass; embedded integration (incl. a service responding on
/health) green; CLI, web, MCP unchanged.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Thanks — thorough review. Everything in Fix before merge and Strongly preferred is addressed in the latest commit. Rundown: Blockers
Also fixed
Follow-ups also done
12 new tests (84 total, all green). Embedded integration covers ts-lib/cli/react-app/node-service — each generated through the API, written, then installed/built/tested/linted, with the service confirmed responding on |
Replay re-derived each extension file's mode against the current base, so an original `add` became `replace` after one export/import cycle and the drift warning silently vanished. createProjectFromDefinition now carries the stored modes through extendProject (internal fileModes) so the original intent survives repeated round-trips. Regression tests cover both add and replace. Also: count definition size in UTF-8 bytes (Buffer.byteLength), not JS string length, so the "bytes" limit and the value agree for multibyte content. Roadmapped the two non-blocking review follow-ups: documenting the writer's TOCTOU threat model (safe against hostile file maps and existing symlinks, not against a concurrent process mutating the destination), and a driftPolicy: 'report' | 'error' option so treating a replay collision as fatal is opt-in. 85 tests pass; embedded integration green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Fixed the mode-preservation issue — good catch, that would have quietly eroded the drift signal.
Also folded in the byte-counting fix — definition size now uses The two remaining follow-ups are non-blocking and I've added them to the roadmap so they don't get lost:
85 tests pass, CI green. |
… 3.0 approval The approval review made explicit that 3.0 turns the embedded API into Packkit's public programmatic surface, and that the core/embedded/consumer layers should stay distinct by rule. Recorded six architecture principles (worth an ARCHITECTURE.md) and a post-3.0 item to rebase the CLI onto the embedded API so there is one orchestration layer with every surface sharing the same diagnostics, collision handling, and path safety. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds a stable, typed, side-effect-free API so a Node application can use Packkit as a project-generation engine. Generate in memory, layer on your own deployment files, write to disk when ready — no prompts, installs, git, network, or command execution anywhere in the path.
This is the
3.0.0feature. It's additive; nothing existing changes behavior.The flow (acceptance criteria, verified)
The written project then installs, builds, tests, and lints green — confirmed for
ts-lib,cli,react-app, andnode-service.What's in it
createProject(input)→GeneratedProjectextendProject(project, ext)— collision policy, defaulterrorwriteGeneratedProject({project, destination})exportProjectDefinition/createProjectFromDefinitioncalculateProjectDigestderiveDeploymentContractDesign decisions worth a look
Normalization stops being silent.
normalizeConfignow optionally records every coercion — Storybook disabled for a non-component-library, a size budget dropped for an unpublished package — as aDiagnosticwithpreviousValue/resolvedValue. This is one of the architecture-review P1 items, landed here because the embedded API needed it. Fully backward compatible: callers that pass no collector get byte-identical behavior, which is why all 49 existing tests are untouched.One generation path, with provenance. Rather than fork generation,
generate()now delegates to a newassemble()that tracks which feature produced each file and fragment.generate()'s output is unchanged; the embedded layer reads the provenance to report file- andpackage.json-field conflicts.Path safety is enforced twice — when a project is assembled and again at the writer boundary — so a project that reached the writer from an untrusted source still can't escape the destination. Rejects absolute,
.., null-byte, Windows-reserved, and case-insensitive-colliding paths. An invalid path fails the whole write before anything lands.Root export moved to
src/index.js(core + embedded) soimport { createProject } from 'create-packkit'works, per the brief's primary example../corestays the browser-safe core-only entry, and the web bundle builds fromsrc/coredirectly, so browser compatibility is unaffected. Additive for existing consumers.Types
Full
.d.tsfor the public surface (types/), atypesentry inpackage.json, and per-exporttypes. Packed-tarball tests confirm every export resolves from a clean install, the declarations ship, and the CLI still works.Tests
23 new (unit + filesystem): resolution, diagnostics, unknown/invalid options, extension immutability, collisions, path traversal, determinism, definition round-trip, deployment contracts, and the writer (nested paths, Unicode/space filenames, collision policies, traversal rejection, no-install/no-git). Integration harness gains
--embedded, wired into CI for four presets. 72 tests pass locally.Explicitly out of scope (by the brief)
No cloud provisioning, repo creation, auth, infra, or provider SDKs. The deployment contract is provider-neutral — no AWS/Vercel/Netlify/Cloudflare/GitHub fields. The core generation path stays free of network calls.
Docs: docs/EMBEDDING.md.
🤖 Generated with Claude Code