What’s New
Release notes for acp2md, newest first. Conversion behaviour comes from the shared Climakers ADF engine, so most output changes apply to acs2md as well.
v1.0.12
Restore a page from a backup
page restore puts one page back into Confluence from a page get file (--file, with --attachments-dir) or from one page of an acs2md space backup (--backup-dir and --page-id). A page still current in the space gets its backed-up version restored, keeping its ID and history, or the backed-up body written as a new version; a trashed, archived or deleted page is created anew under its backed-up parent. Missing attachments, labels and content properties are added, and pictures in an ADF body are pointed at the restored files. A property the page has with another value is kept, since apps store state there; --overwrite-properties replaces it. The restored version’s message names the backup’s date, never a local path.
The plan is shown and confirmed first (--yes in scripts, --dry-run to only look). This is the first acp2md command that writes to Confluence, and it never deletes, trashes, archives or moves anything. page check compares a backed-up page with Confluence first: its state, version and body, and the attachments and labels it no longer has. See Restore Pages.
page get saves attachments
--attachments-dir <dir> downloads the page’s attachments into <dir>/<page-id>/ and describes them: inside the page JSON for atlas_doc_format, in <dir>/<page-id>.attachments.json for storage. Available on by-id, by-title and by-url, so a page can be backed up and restored with its files. See Saving attachments.
Every request has a deadline
The new http.client.request_timeout (default 10m, 0 disables it) bounds one request from sending it to reading the whole response, including its retries and Retry-After waits. Before, a connection that stalled hung the run until Ctrl-C. Existing config files do not have the key and get the default; doctor shows the value in a new HTTP request timeout row. The “Timed out waiting for the Confluence API” hint now names this setting instead of http.client.max_timeout, which caps the retry backoff and does not end a slow request. See Every request has a deadline.
Fixes
- Commands no longer hang when stdin is left open. Every command that takes a page ID, title or URL read standard input to the end whenever it was not a terminal, even when the value was on the command line. From a CI runner, an ssh session or a tool that keeps stdin open, the command waited forever. Stdin is now read only when the argument is missing, so
echo 123456 | acp2md page convert by-idstill works. - Re-activating with a renewed key works. A renewed subscription keeps its key, but
license activate --license-key <the same key>refused it with “license has expired” once the expiry had been seen locally. The command now asks the store and refreshes the installed license when the store reports the key active again. --forcedoes what its help promised with a key.license activate --license-key <new-key> --forcereplaces an active license, releasing its seat first; before, the flag only applied to license files. See Runningactivateagain.- The command trees are regenerated from the binaries. The acp2md reference had fallen behind and was missing the flags of
page convert,page countandpage properties; thetree --shortlisting in this manual now showspage checkandpage restoretoo.
Under the hood
Writes go through a separate HTTP client that honours Retry-After on 429 only and never retries blindly, so a page create whose answer was lost is looked up instead of sent twice. The sync state and attachment downloads are published through a temporary file and a rename, so a download that fails part way leaves nothing behind. Built with Go 1.27.3.
v1.0.10
Knowing when there is something to install
acp2md doctor reports the newest release available to you in a new Latest version row, alongside the command that installs it; the check never fails the diagnosis and says “could not check” when offline. After an ordinary command, acp2md also asks the update service at most once a day, waits at most two seconds, and prints a single line to stderr when a newer release exists. The notice never appears in CI, under sudo, when stderr is not a terminal, or after update, license, doctor and config. Turn it off with acp2md config set update.notice false. See The daily new-version notice.
acp2md update also keeps working when the store’s daily download limit is reached — each file may be downloaded ten times a day — and beyond that fetches the same release from Climakers’ own release storage, once a day per installation.
Activation is steadier on Linux
License files are encrypted against a machine identifier, and on Linux that identifier now survives kernel and microcode updates and VM resizes. Licenses activated before this release keep working: a subscription license moves to the new identifier the first time it is opened, and a signed offline license keeps the identifier it was issued for. Nothing needs re-activating. macOS is unaffected.
Fixes
acp2md updatesees a new release right away. Just after a release,updatecould report the previous version for several minutes because the update service answered from a cached copy. Installing, andupdate --check --force, now ask the service directly, so a release is offered within a minute of being published; a plainupdate --checkmay take up to two minutes.
v1.0.9
Fixes
license activate --license-key <file>says which flag takes a file. Passing a license file to--license-keyanswered “invalid license key format: expected UUID v4”; it now explains that a license file is activated with--license-fileand prints the exact command.
v1.0.8
Nothing you use changes: the update engine moved to a shared Climakers module, which ajat now uses too, and update behaves exactly as before.
v1.0.7
Offline licenses update too
A local license issued by licensegen with a signing key carries a signature the update service verifies on its own, so offline customers get acp2md update like everyone else; their downloads come from Climakers’ release storage rather than the store. Local licenses issued before signing existed are refused with “this local license is not signed” and a hint to activate a reissued file. See Your subscription and older versions.
v1.0.6
Updates from inside the tool
acp2md update installs the newest release your license includes: it checks Climakers for a newer release, downloads it, verifies its SHA-256 checksum and replaces the running binary. On macOS it installs the signed, notarized package and runs as sudo acp2md update; on Linux it replaces the binary in place and never uses sudo. --check reports without changing anything, --version v1.1.0 installs a specific release (which is also how you go back), --yes skips the prompt and is required in CI, and --format json is for scripts. After a subscription ends, releases published before the end date stay installable. The command needs no Confluence credentials and does not count toward the license’s command limit. See Updating acp2md.
--format json output changed in two ways
JSON output is now produced by Go’s encoding/json/v2. Two visible differences, both deliberate:
- an empty list or map prints as
[]/{}instead ofnull, so a page with no labels is"labels": []rather than"labels": null; <,>and&in strings are no longer escaped, so a title likeQ&A <draft>reads as written.
Key order, indentation and the trailing newline are unchanged, and sync state files remain readable by earlier versions. Scripts that test for null should test for an empty list instead.
Platform notes
- macOS 13 (Ventura) or later is required. The Go toolchain this release is built with dropped support for earlier macOS releases.
SSL_CERT_FILEandSSL_CERT_DIRare honoured on macOS. When either is set, certificate roots are loaded from those paths instead of the platform store — the behaviour Linux always had. Corporate-proxy setups that set these variables for other tools will see them apply here too;GODEBUG=x509sslcertoverrideplatform=0restores the previous behaviour.
v1.0.5
This release changed default Markdown output for pages containing panels, and
for anyone using --include-metadata. Both actions below are no-ops if you
never wrote the corresponding setting.
Action: check image.max_size_bytes
The default dropped to 2 MB, but a configuration written before this release almost certainly pins the old 10485760 — permissive enough to inline a page background and produce a file no editor will lay out. Configuration beats defaults, so it keeps winning:
acp2md config get image.max_size_bytes # 10485760 means the old value is pinned
acp2md config set image.max_size_bytes 2097152acs2md reads its own configuration, so run the same pair there if you use it.
Action: --resolve-users and --link-mentions now default on
Front matter carries display names and mentions link to people profiles. Where an export is published outside the organisation, --resolve-users=false keeps opaque account IDs instead.
Retry-After is honoured
When Confluence answers a throttled request with 429 or 503 and names a delay, the client now waits exactly that long instead of falling back to a locally guessed schedule. The server knows how long its window is; the client does not.
A delay longer than 60 seconds is surfaced rather than slept, so a command never looks hung, and waiting is interruptible.
The retry backoff defaults changed, and upgrading does not reach an existing
install. They were raised from 10ms/100ms to 500ms/30s, but a config
file materialises every default when it is created and configuration beats
defaults. An older install keeps a 70 ms total budget across three attempts,
which cannot outlast a real throttling window. Run acp2md doctor to check,
then see Retries and rate limiting.
Images can be written as sidecar files
--image-sidecars saves each image to attachments/<page-id>/ beside the Markdown and links it relatively, instead of embedding a base64 data URI. The export stays self-contained either way — what changes is line length.
On one real page: 1,440,451 B and a 244,311-character line as base64, against 12,100 B and 379 characters as sidecars. Across a space it also deduplicates — an image reused on ten pages is stored once.
--obsidian turns this on, because base64 was the last thing in that preset that did not render in Obsidian.
Obsidian output
A full preset — callouts, heading anchors Obsidian resolves, and image sidecars — plus --wikilinks, --inline-tags, --panel-style and --media-dimensions. See Obsidian Vaults.
Panels render as GitHub alerts
> [!NOTE], > [!TIP], > [!WARNING], > [!CAUTION] replace the previous emoji-labelled blockquote. Alerts render on GitHub and Obsidian. Panel types with no alert equivalent keep the emoji form. --panel-style=emoji restores the old output.
Front matter is proper YAML
--include-metadata now emits deterministic, correctly quoted YAML that Obsidian reads as Properties and Jekyll, Hugo and Pandoc parse:
| Before | Now |
|---|---|
Title, AuthorID, CreatedAt, ID, LastOwnerID, OwnerID, ParentID, ParentType, Position, SpaceID, Status, SubType, VersionAuthorID, VersionCreatedAt, VersionMessage, VersionMinorEdit, VersionNumber | title, tags, author, created, updated, source, space, status, version, id, parent_id, owner_id |
Dates are YYYY-MM-DD, tags comes from the page’s Confluence labels, source is the browser URL, and empty fields are omitted. The dropped fields were Confluence bookkeeping no Markdown consumer used.
Labels are fetched automatically
With front matter enabled, tags is populated without passing --include-labels. On page convert this rides along on the existing page request at no extra cost.
Heading links resolve in the renderer you export for
The table of contents previously emitted GitHub-style slug anchors regardless of target. --panel-style=obsidian (and therefore --obsidian) now emits anchors Obsidian resolves.
Every command reports how long it took
⏱️ Converted in 559msThe figure covers the whole command, API calls and image downloads included. It is printed to stderr, never stdout, so acp2md page convert by-id 123 > page.md and pipelines stay clean.
A second Ctrl+C force-quits
The first interrupt cancels cleanly and lets in-flight work unwind. Previously any further Ctrl+C was swallowed and a run that looked stuck could not be stopped.
Fixes
--debugproduced no output at all. It set the log level but left the destination at its default, which discards everything. It now logs to stderr unless--log-filesays otherwise.- A broken
log.level,log.formator unwritablelog.fileblocked every command — including theconfigcommands that repair it. Those now fall back to working settings for the run and name the key to change. - A global flag placed before an exempt command made it demand a licence or validate the configuration.
acp2md --log-file x.log doctortreated--log-fileas the command name, sodoctor— the command you run to diagnose licensing — refused to start on an unlicensed machine. - An invalid
http.client.proxyis now an error rather than a warning-and-continue. Silently sending traffic direct when asked to route it is, on a corporate network, worth stopping for.
See also
- Convert Pages — the full flag reference
- Restore Pages —
page checkandpage restore - Updating acp2md — installing a release from inside the tool
- Configuration — retries, timeouts, proxy and credentials