Skip to content
Open
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: 9 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,15 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]


### Added

- New `apm export-patch` command exports local edits to APM-managed files
as unified diffs against the packages that deployed them, one
`git apply`-ready `.patch` file per package with the locked base recorded
in the header. Verbatim-deployed files only; transformed deployments are
listed as skipped with the reason. (by @edenfunf; closes #2118) (#2162)

### Fixed

- Fresh checkouts with declared consumer targets no longer remain
Expand Down
69 changes: 69 additions & 0 deletions docs/src/content/docs/reference/cli/export-patch.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
---
title: apm export-patch
description: Export local edits to APM-managed files as patches against their source packages.
sidebar:
order: 14
---

## Synopsis

```bash
apm export-patch [OPTIONS]
```

## Description

`apm export-patch` turns local edits to APM-managed files into unified diffs against the packages that deployed them, so the edits can be contributed back upstream.

It replays the locked install into a scratch tree (the same machinery as the [`apm audit`](../audit/) drift check) and, for every managed file that was modified locally, maps the change back to the package source file that produced it. Each package with exportable edits gets one `.patch` file, ready for `git apply` in a clone of the package repository. When two packages would sanitize to the same patch filename, the later one gets a short digest suffix so nothing is overwritten.

Every patch file records its base in a leading comment block: the package key, its source, and the exact snapshot the diff applies to (`commit <sha>` for git dependencies, `version <x>` for registry packages). Apply the patch from the package repository root, checked out at that base, then open your upstream pull request from the result:

```bash
git -C path/to/package-clone checkout <base-commit>
git -C path/to/package-clone apply path/to/apm-patches/<package>.patch
```

Only verbatim-deployed files can be exported: a local edit maps cleanly back to its source only when the deployment copied that source byte-for-byte. Everything else is listed as skipped with the reason instead of producing a patch that would not apply:

- Deployments that transform their content (frontmatter rewrites for rule directories, compiled `AGENTS.md` output, aggregated `copilot-instructions.md` sections, files with resolved links).
- Source files that are not normalization-clean (CRLF line endings, a UTF-8 BOM, or a build-id header): the deployed copy matches them only after normalization, so an exported diff could not be applied to the raw file.
- Conflicting edits: when one source file is deployed to several targets and the copies were edited differently, the conflict is reported instead of exporting either version. Identical edits across copies export as a single diff.
- Local deletions of managed files are never exported, since deleting a deployed copy does not imply the file should be removed from the package.

Findings that belong to local path dependencies or to the project's own `.apm/` content are also skipped: their source already lives on disk, so the edit belongs there directly. A lockfile containing only local dependencies short-circuits before the replay.

The replay is cache-only and read-only: it needs a lockfile and a populated `apm_modules/` (run `apm install` first) and never mutates the project tree. Patch files are the only output, and `--out` may not point inside `apm_modules/`.

## Options

| Flag | Default | Description |
|---|---|---|
| `--out`, `-o DIR` | `apm-patches` | Directory to write per-package `.patch` files into. Created if missing; only created when there is something to export. Must not point inside `apm_modules/`. |
| `--dry-run` | off | List what would be exported (and what gets skipped, with reasons) without writing patch files. |
| `--verbose`, `-v` | off | Show replay progress and per-file skip details. |

## Exit codes

| Code | Meaning |
|---|---|
| 0 | Success, including "nothing to export" (skipped-only runs exit 0 with warnings). |
| 1 | Replay or export failed (missing or unparsable lockfile, cold cache, invalid `--out`). |

## Examples

```bash
# Export all local edits as per-package patches under ./apm-patches/
apm export-patch

# Preview what would be exported without writing anything
apm export-patch --dry-run

# Write patches somewhere else
apm export-patch -o /tmp/spec-patches
```

## See also

- [`apm audit`](../audit/) -- detect the drift this command exports.
- [Drift and secure by default](../../../consumer/drift-and-secure-by-default/) -- why local edits to managed files are overwritten on the next install.
1 change: 1 addition & 0 deletions packages/apm-guide/.apm/skills/apm-usage/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
| `apm targets` | Show resolved deployment targets for the current project (Click group; reads filesystem signals; works with or without `apm.yml`) | `--all` also include the `agent-skills` meta-target (only meaningful with `--json`), `--json` machine-readable output. No provenance line is printed (the table is the provenance). |
| `apm uninstall PKGS...` | Remove packages (accepts `owner/repo` or `name@marketplace`) | `--dry-run`, `-g` global |
| `apm prune` | Remove orphaned packages | `--dry-run` |
| `apm export-patch` | Export local edits to APM-managed files as `git apply`-ready patches against their source packages (one `.patch` per package, locked base in the header; verbatim-deployed files only -- transformed deployments are listed as skipped with the reason) | `-o/--out DIR` output dir (default `apm-patches`; must not point inside `apm_modules/`), `--dry-run` preview, `-v` verbose |
| `apm deps list` | List installed packages | `-g` global, `--all` both scopes, `--insecure` |
| `apm deps tree` | Show dependency tree | -- |
| `apm deps why PKG` | Explain why a package is installed (walks lockfile bottom-up to direct deps; analogue of `npm why` / `yarn why`) | `-g` global, `--json` |
Expand Down
2 changes: 2 additions & 0 deletions src/apm_cli/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
from apm_cli.commands.deps import deps
from apm_cli.commands.doctor import doctor
from apm_cli.commands.experimental import experimental
from apm_cli.commands.export_patch import export_patch
from apm_cli.commands.find import find as find_cmd
from apm_cli.commands.init import init
from apm_cli.commands.install import install
Expand Down Expand Up @@ -177,6 +178,7 @@ def cli(ctx, verbose: bool) -> None:
cli.add_command(lock)
cli.add_command(uninstall)
cli.add_command(prune)
cli.add_command(export_patch, name="export-patch")
cli.add_command(update)
cli.add_command(self_update)
cli.add_command(plugin_cmd, name="plugin")
Expand Down
Loading