feat(plugin-cli): add profile policy command#1925
Conversation
🦋 Changeset detectedLatest commit: 5c3d9d2 The changes in this PR will be included in the next version bump. This PR includes changesets to release 5 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
Scope checkThis PR changes 922 lines across 6 files. Large PRs are harder to review and more likely to be closed without review. If this scope is intentional, no action needed. A maintainer will review it. If not, please consider splitting this into smaller PRs. See CONTRIBUTING.md for contribution guidelines. |
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ✅ Deployment successful! View logs |
emdash-playground | 5c3d9d2 | Jul 10 2026, 05:14 PM |
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ✅ Deployment successful! View logs |
emdash-demo-cache | 5c3d9d2 | Jul 10 2026, 05:12 PM |
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ✅ Deployment successful! View logs |
emdash-demo-do | 5c3d9d2 | Jul 10 2026, 05:12 PM |
There was a problem hiding this comment.
This PR adds a sensible, narrowly-scoped piece of the delegated-release work: a policy set command that edits the release-policy fields inside an existing package profile's com.emdashcms.experimental.package.profileExtension extension. It mirrors the architecture of the existing update-package command (API split from CLI wrapper, dry-run by default, atproto CID-based CAS, mock-PDS tests) and uses the right lexicon layer.
I checked the diff, the new command/API files, the sibling update-package and publish commands, the registry lexicons, the publishing client, and the new tests. The implementation is mostly careful about validation, preserving unknown profile/extension fields, and stale-record handling. The test suite covers the main success, idempotency, validation, and CAS paths.
I found two real issues in the JSON-mode contract that should be fixed before this ships, plus one style/consistency note:
- Unstable JSON diff envelope:
formatPolicyJsonResultreturnsresult.diffswithundefinedbefore/aftervalues, soJSON.stringifyomits those keys. Diff objects therefore have an unpredictable shape. The siblingupdate-packagecommand normalisesundefinedtonullso every diff has both keys. - JSON errors drop
ProfilePolicyError.detail:expectedCidforSTALE_RECORD, validation issues forLEXICON_VALIDATION_FAILED, etc. are lost because the catch block only extractscodeandmessage.update-packagepreservesdetailin its JSON error envelope. CliErrorshape diverges frompublish.ts/update-package.ts: it lacks theexitCodeparameter, so every failure exits1and the constructor signature is inconsistent across the package's commands.
Otherwise this looks good to merge once #1 and #2 are addressed.
| profile: result.profileUri, | ||
| written: result.written, | ||
| applied, | ||
| diffs: result.diffs, |
There was a problem hiding this comment.
[needs fixing] The JSON success envelope returns diffs: result.diffs unchanged. When a field was previously unset, before is undefined, which JSON.stringify omits, so diff objects have an unstable shape (some entries have before, others do not). The PR advertises stable JSON output; the sibling update-package command avoids this by normalising undefined to null for both sides.
| diffs: result.diffs, | |
| diffs: result.diffs.map((d) => ({ | |
| field: d.field, | |
| before: d.before ?? null, | |
| after: d.after ?? null, | |
| })), |
| : "INTERNAL_ERROR"; | ||
| const message = error instanceof Error ? error.message : String(error); | ||
| consola.error(message); | ||
| if (json) process.stdout.write(`${JSON.stringify(formatPolicyJsonError(code, message))}\n`); |
There was a problem hiding this comment.
[needs fixing] The top-level catch block calls formatPolicyJsonError(code, message) and discards the detail object carried by ProfilePolicyError — e.g., expectedCid for STALE_RECORD and validation results for LEXICON_VALIDATION_FAILED. update-package preserves detail in its JSON error envelope, and callers scripting this command will want that context.
Capture the detail at the call site:
| if (json) process.stdout.write(`${JSON.stringify(formatPolicyJsonError(code, message))}\n`); | |
| const detail = error instanceof ProfilePolicyError ? error.detail : undefined; | |
| const message = error instanceof Error ? error.message : String(error); | |
| consola.error(message); | |
| if (json) process.stdout.write(`${JSON.stringify(formatPolicyJsonError(code, message, detail))}\n`); |
And update the formatter at line 172 to accept it:
| if (json) process.stdout.write(`${JSON.stringify(formatPolicyJsonError(code, message))}\n`); | |
| export function formatPolicyJsonError(code: string, message: string, detail?: Record<string, unknown>) { | |
| const body: Record<string, unknown> = { code, message }; | |
| if (detail !== undefined) body.detail = detail; | |
| return { error: body }; | |
| } |
| return typeof value === "string" ? [value] : undefined; | ||
| } | ||
|
|
||
| class CliError extends Error { |
There was a problem hiding this comment.
[suggestion] CliError here is new CliError(message, code), but the equivalent classes in publish.ts and update-package.ts are new CliError(message, exitCode, code). That lets those commands return 2 for usage/flag errors and 1 for runtime failures. Aligning the constructor (and passing 2 for INVALID_POLICY_FLAGS, 1 for auth/publisher/manifest errors) would keep the CLI's error handling consistent and make scripts more reliable.
What does this PR do?
Adds
emdash-plugin policy setfor interactively editing an existing package's signed release policy using the publisher's local OAuth session.It supports provenance policy, confirmation mode, approver replacement/clearing, canonical HTTPS repository anchors, dry-run, JSON, and CID CAS while preserving unrelated profile and extension data.
Related to #1908 and #1918.
Type of change
Checklist
pnpm typecheckpassespnpm lintpassespnpm testpasses (or targeted tests for my change)pnpm formathas been runmessages.pochanges except in translation PRs — a workflow extracts catalogs on merge tomain.AI-generated code disclosure
Screenshots / test output
pnpm buildpasses with existing warningsgit diff --checkpassTry this PR
Open a fresh playground →
A full working EmDash site, deployed from this branch. Each visit gets its own session-scoped sandbox: no login needed and no shared state. Try the admin, edit content, hit the public site.
Tracks
feat/delegated-release-service-05-profile-policy. Updated automatically when the playground redeploys.