Skip to content

feat: restore/record/export provisioning commands (the "planned CLI") - #2

Merged
Shinrai merged 20 commits into
nextfrom
feat/provisioning-cli
Jul 19, 2026
Merged

feat: restore/record/export provisioning commands (the "planned CLI")#2
Shinrai merged 20 commits into
nextfrom
feat/provisioning-cli

Conversation

@Shinrai

@Shinrai Shinrai commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

🚀 What's Changed

💥 Breaking Changes

No breaking changes

✨ Features

🐛 Bug Fixes

📦 Dependencies

No dependency updates

🔧 Other Changes

👥 Contributors

Shinrai added 4 commits July 13, 2026 05:59
Provision embedded children on a fresh clone without ever committing a
child URL. New `embedded` engine namespace resolves each gitlink's clone
URL strictest-source-first — local-config registry, manifest (--from),
--base, then origin convention — and SHA-verifies every clone so a wrong
convention guess fails closed (the clone is removed, never a pre-existing
dir) instead of planting the wrong code.

- embedded/gitlinks.mjs   enumerate mode-160000 gitlinks from HEAD
- embedded/registry.mjs   embedded.<path>.url/.branch in the parent's
                          LOCAL config (never committed)
- embedded/manifest.mjs   JSON transfer-file read/build/serialize
- embedded/resolve.mjs    the 4-layer resolution precedence
- embedded/restore.mjs    clone + SHA-verify + detach-checkout engine
- embedded/record.mjs     record present children into the registry

CLI leaves:
- restore [paths...] [--from] [--base] [--skip] [--dry-run]
    outcomes: restored|already-present|unresolved|pinned-mismatch|skipped;
    exits non-zero if any child is unresolved or pinned-mismatch
- record [paths...]
- export [-o <file>] [--scan]  (appends -o path to .git/info/exclude)
A fresh clone of a parent materializes each gitlink as an empty
directory, so `link` refusing any existing path made it unusable to fill
one in. It now clones into a missing OR empty target and refuses only a
non-empty directory. After staging, it records the child's URL + branch
into the parent's local registry (same as `record`), so a later
restore/export already knows the child.
Covers restore-by-convention (clone + pin checkout + registry write +
day-2 already-present), obscured-child unresolved -> link -> present,
pinned-mismatch (clone removed, non-zero exit), record/export round-trip
through a manifest on a second machine, --skip partial restore, and the
link empty-dir fix. Real git repos in tmp with hermetic config.

Suite: 22 passing (15 pre-existing + 7 new).
README gains a "Restoring embedded children (machine-B bootstrap)"
section — clone parent -> `git embedded restore` -> done — plus the
resolution precedence, SHA-verification, obscured-child handling, and the
export/record manifest workflow with a loud "never commit the manifest"
warning. design.md's coverage matrix now marks initial child clone as
covered by `git embedded restore`, and a new "Provisioning" section
documents the security rationale (convention is a resolution default,
never a disclosure; SHA-verification; obscured children resolvable only
via the config/manifest layers).

Full suite: 22 passing (15 pre-existing + 7 new).

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds the “planned CLI” provisioning workflow to @cldmv/git-embedded, allowing a fresh clone of a parent repo to reconstruct embedded children (without committing child URLs) via local per-clone registry, optional manifest transfer, explicit base URL, and a convention-derived fallback.

Changes:

  • Introduces restore/resolve engines to clone missing gitlink children, SHA-verify pins, detach-checkout pinned commits, and persist resolved URLs to local config.
  • Adds local-config registry + manifest (read/build/serialize) + record engine, and wires new CLI commands: restore, record, export.
  • Updates link to accept empty gitlink directories and record URL/branch into the local registry; updates docs and adds provisioning tests.

Reviewed changes

Copilot reviewed 13 out of 14 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
tests/embedded-provisioning.test.mjs New Vitest coverage for restore/link/record/export provisioning flows.
src/api/embedded/restore.mjs New restore engine: resolve URL, clone, SHA-verify, detach-checkout, and record URL.
src/api/embedded/resolve.mjs New layered URL resolver (local-config → manifest → base → convention).
src/api/embedded/registry.mjs New per-clone local-config registry read/write/scan helpers.
src/api/embedded/record.mjs New engine to record present child origins/branches into the registry.
src/api/embedded/manifest.mjs New manifest transfer format helpers (read/build/serialize).
src/api/embedded/gitlinks.mjs New gitlink enumerator (git ls-tree -r HEAD mode 160000).
src/api/cli/restore.mjs New git-embedded restore command wrapper/reporting.
src/api/cli/record.mjs New git-embedded record command wrapper/reporting.
src/api/cli/export.mjs New git-embedded export command wrapper + .git/info/exclude courtesy.
src/api/cli/link.mjs link accepts empty gitlink dirs and now records into the local registry.
README.md Documents restore/record/export behavior, resolution layers, and security model.
docs/design.md Updates coverage matrix + adds provisioning design section and command reference.
.gitignore Ignores tmp/.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/api/embedded/restore.mjs
Comment thread src/api/cli/link.mjs Outdated
Comment thread src/api/embedded/manifest.mjs Outdated
Address the three Copilot review findings on #2:
- restore: refuse any pre-existing target that is not an empty directory (a file or a non-empty dir is user data — never clone into it, never remove it; previously a failed clone could wipe pre-existing contents via removeClone)
- link: isNonEmpty treats a file (ENOTDIR) or unreadable dir as blocking instead of acceptable; only ENOENT passes
- manifest.read: gate version === 1 with a clear error instead of accepting unknown formats silently
Adds 4 guard tests (existing tests untouched); suite 26 passing.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 13 out of 14 changed files in this pull request and generated 3 comments.

Comment thread src/api/embedded/resolve.mjs
Comment thread src/api/embedded/restore.mjs Outdated
Comment thread src/api/cli/link.mjs Outdated
Shinrai added 2 commits July 13, 2026 19:32
Address the second Copilot review round on #2:
- restore/link: lstat-based target checks — a symlink at a gitlink path (even to an empty dir) is refused instead of cloned through (writing outside the repo); broken symlinks are caught too (existsSync missed them)
- link: guard folded into blocksClone(target) called unconditionally, so a dangling link no longer slips past the existsSync pre-check
- conventionUrl: scp-style origins with the repo at the path root (git@host:parent.git) now derive the sibling after the last ':' — matching the documented scp support
Adds 4 tests (scp-root dry-run, symlink, broken symlink, link symlink refusal); existing tests untouched; suite 30 passing.
Windows can't create symlinks without Developer Mode/elevation, so the three cases that CREATE links to prove the guards skip where creation is denied (probe at suite setup); creations get an explicit 'dir' type for Windows correctness when they do run. The guards themselves are lstat-based, need no symlink rights, and stay exercised on POSIX CI. Suite 30 passing.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 13 out of 14 changed files in this pull request and generated 2 comments.

Comment thread src/api/cli/restore.mjs Outdated
Comment thread docs/design.md Outdated
Review round 3: the summary line counted failures inside 'unchanged' (results.length - restored) — each outcome now lands in exactly one bucket (restored / unchanged=already-present / skipped when present / failed), verified live: '0 restored, 0 unchanged, 1 skipped, 0 failed.' design.md's provisioning section said 'three optional layers' while implementing four sources — now lists local config → manifest → explicit --base → convention, matching README and the resolver.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 13 out of 14 changed files in this pull request and generated 5 comments.

Comment thread src/api/embedded/manifest.mjs Outdated
Comment thread src/api/embedded/manifest.mjs Outdated
Comment thread src/api/embedded/resolve.mjs Outdated
Comment thread README.md Outdated
Comment thread tests/embedded-provisioning.test.mjs
Shinrai added 2 commits July 13, 2026 21:04
- manifest.read rejects an array children value (typeof [] === 'object' slipped through)
- manifest.build uses a null-prototype children map so a '__proto__' child path is a plain key, never a prototype mutation
- resolve's manifest layer reads own properties only (Object.hasOwn — also correct for null-proto maps)
- README: link's refusal wording now matches the implementation (non-empty dir, file, symlink, unreadable — not 'only a non-empty directory')
- test symlink probe cleans its temp dir in finally (no leak when the probe fails, e.g. Windows without Developer Mode)
Two tests added; suite 32 passing.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 13 out of 14 changed files in this pull request and generated 3 comments.

Comment thread src/api/embedded/restore.mjs
Comment thread src/api/cli/link.mjs Outdated
Comment thread src/api/cli/link.mjs
…iew round 5)

- restore/link/registry: every git call that receives a resolved URL or user path now passes `--` first, so a value starting with '-' (e.g. --upload-pack=...) is a repo argument, never an option — injection test proves the payload is not executed
- link: normalizes the target to the repo-root-relative slash-normalized gitlink path before recording ('./tests' and 'tests/' record as 'tests', matching restore/gitlinks/export), and refuses a target outside the worktree with exit 2
Three tests added; suite 35 passing.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 13 out of 14 changed files in this pull request and generated 1 comment.

Comment thread src/api/cli/restore.mjs

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 16 out of 17 changed files in this pull request and generated 1 comment.

Comment thread src/api/embedded/sync.mjs
sync ran git commands with -C on a gitlink path checked only via existsSync, so a symlinked child pointing outside the parent worktree would be fetched/checked-out out there — the same 'cd out of the worktree' risk restore and link already refuse. sync now lstats the path and reports a symlink as sync-failed (non-zero exit) before touching it, even on an unfiltered run. Test added.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 16 out of 17 changed files in this pull request and generated 4 comments.

Comment thread src/api/embedded/restore.mjs
Comment thread src/api/embedded/restore.mjs
Comment thread src/api/embedded/sync.mjs
Comment thread src/api/embedded/sync.mjs
…iew round 9)

- restore/sync: a non-ENOENT lstat error (EACCES/ENOTDIR on an existing path) is now refused (restore -> unresolved, sync -> sync-failed) instead of assumed 'missing' — which could otherwise clone into, and removeClone against, a pre-existing path we can't stat.
- restore: a failed verification 'git fetch' is surfaced in the pinned-mismatch note (auth/network failure is not the same as a wrong-repo convention guess).
- sync: a failed fallback 'git fetch' is reported as sync-failed with stderr instead of pin-unavailable. Test added (broken child origin -> sync-failed).

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 16 out of 17 changed files in this pull request and generated 2 comments.

Comment thread src/api/embedded/sync.mjs Outdated
Comment thread src/api/embedded/branch.mjs
… attach (review round 10)

- sync: a non-zero 'git branch --show-current' is now sync-failed (with stderr) instead of read as detached HEAD (branch=null), which could mask a real repo/permission error.
- branch.attach: pass '--' before the branch name in 'git branch --set-upstream-to', so a registry/manifest branch name starting with '-' is never parsed as an option (matches the '--' hardening clone/add already have).

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 16 out of 17 changed files in this pull request and generated 2 comments.

Comment thread src/api/embedded/restore.mjs Outdated
Comment thread src/api/embedded/sync.mjs Outdated
… divergence (review round 11)

- restore: run `git clone` with cwd = the parent repo root so a RELATIVE
  registry/manifest url (e.g. "../sibling.git") resolves deterministically
  against the repo rather than the Node process CWD. The destination is
  absolute, so only a relative source url is affected. Adds a relative-url
  test (fails without the anchor: the clone resolves against the runner's
  cwd and the child comes back unresolved).
- sync: `merge-base --is-ancestor` exit 1 (HEAD not an ancestor — your
  work) and 128 (corrupt repo / missing objects) were both treated as
  "ahead" and left alone with a zero exit, masking a real git error. Now
  only exit 1 is "ahead"; anything else surfaces as sync-failed, matching
  the rev-parse / status / fetch / branch failure handling already in sync.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 16 out of 17 changed files in this pull request and generated 2 comments.

Comment thread src/api/embedded/sync.mjs
Comment thread src/api/embedded/restore.mjs
…(review round 12)

restore and sync reported a failed `git checkout --detach <pin>` with only
the truncated sha, dropping git's stderr/exit code — leaving a sync-failed /
pinned-mismatch undiagnosable. Both notes now append `checkout.stderr`
(falling back to the exit code), matching the stderr-in-note pattern the
clone / rev-parse / status / fetch failures already use. Message-only
enrichment; the checkout-failure branch is defensive (the pin is
cat-file-verified present immediately above), so it stays guarded rather
than unit-triggered.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 16 out of 17 changed files in this pull request and generated no new comments.

Brings the released 1.1.0 line into the branch: the guard-modes +
pre-push hook work (#4), the version bump, and the feature-pr workflow.

Only docs/design.md conflicted. Resolved by keeping this branch's
provisioning-aware wording — the `git reset --hard` gap now points at
`git embedded sync`, and the "Initial child clone" row points at
`git embedded restore` — and splicing in next's two genuinely-new pieces:
the `### pre-push` design subsection and the guard-modes-aware `git commit`
row of the coverage matrix. Full suite green (78 tests).
@cldmv-bot cldmv-bot Bot added ! feature → next v4 flow: contributor PR targeting the next integration branch type: feature Implements new functionality — a PR or issue that adds a feature labels Jul 19, 2026
@Shinrai
Shinrai merged commit 3f618e7 into next Jul 19, 2026
19 checks passed
@cldmv-bot
cldmv-bot Bot deleted the feat/provisioning-cli branch July 19, 2026 02:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

! feature → next v4 flow: contributor PR targeting the next integration branch type: feature Implements new functionality — a PR or issue that adds a feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants