Skip to content

Commit 54377c8

Browse files
authored
fix(plugin-cli): improve profile release setup UX (#3117)
* fix(plugin-cli): clarify profile release setup * docs: explain plugin profile setup flow * docs: explain CLI installation
1 parent 76b1322 commit 54377c8

11 files changed

Lines changed: 213 additions & 49 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@emdash-cms/plugin-cli": patch
3+
---
4+
5+
Improves package-profile and automated-release setup: missing manifests point to the plugin-directory command, GitHub repository prompts use a detected `origin` remote, setup failures omit stack traces, and a published profile shows both manual and GitHub Actions release commands.

docs/src/content/docs/plugins/creating-plugins/cli.mdx

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,16 @@ import { Steps } from "@astrojs/starlight/components";
99

1010
The CLI uses an [Atmosphere account](/plugins/creating-plugins/publishing/#your-atmosphere-account) as the publisher identity for package profiles and releases.
1111

12+
## Install the CLI
13+
14+
Plugins created with `pnpm dlx @emdash-cms/plugin-cli init` already include the CLI as a pinned development dependency. Add it to an existing plugin before using the other commands:
15+
16+
```sh
17+
pnpm add -D @emdash-cms/plugin-cli
18+
```
19+
20+
The examples use `pnpm exec emdash-plugin` so each command runs the version installed in the plugin. Use `pnpm dlx` for the one-off `init` command, not for repeated build, login, or release commands.
21+
1222
## Commands
1323

1424
The CLI provides the following commands:
@@ -51,7 +61,7 @@ The following example shows the two scripts most plugins add to `package.json`:
5161
Create a new plugin with `init`:
5262

5363
```sh
54-
npx @emdash-cms/plugin-cli init my-plugin
64+
pnpm dlx @emdash-cms/plugin-cli init my-plugin
5565
```
5666

5767
This scaffolds `emdash-plugin.jsonc`, `src/plugin.ts`, `package.json`, `tsconfig.json`, `vitest.config.ts`, a workerd-backed test, a README, `AGENTS.md`, a local `creating-plugins` skill, and package-manager configuration. `.agents/skills` and `.claude/skills` link to the canonical `skills` directory, and `.claude/CLAUDE.md` links to `AGENTS.md`, so Codex and Claude use the same project guidance. The source starts with one route assigned to a `SandboxedPlugin`-typed constant and exported as default. The test invokes that route through EmDash's production sandbox wrapper and host bridge.
@@ -63,7 +73,7 @@ The CLI detects whether npm, pnpm, Yarn, or Bun launched it and generates matchi
6373
Non-interactive setup requires explicit ownership metadata. Use the following form in scripts:
6474

6575
```sh
66-
npx @emdash-cms/plugin-cli init my-plugin --yes \
76+
pnpm dlx @emdash-cms/plugin-cli init my-plugin --yes \
6777
--publisher did:plc:abc123def456 \
6878
--author-name "Jane Doe" \
6979
--security-email security@example.com
@@ -150,7 +160,7 @@ The write uses the current record CID as a precondition. If another process chan
150160

151161
`profile setup` prepares the publisher-owned package profile for automated releases. It creates a missing profile from `emdash-plugin.jsonc`, or adds delegated-release settings to an existing valid profile without replacing its package metadata.
152162

153-
Run the interactive setup from the plugin directory:
163+
Run the interactive setup from the plugin directory. From elsewhere in a monorepo, pass `--dir <plugin-directory>`:
154164

155165
```sh
156166
emdash-plugin profile setup
@@ -159,15 +169,15 @@ emdash-plugin profile setup
159169
| Flag | Default | Description |
160170
| --- | --- | --- |
161171
| `--dir <path>` | Current directory | Plugin source directory. |
162-
| `--repository <url>` | Manifest `repo` | Canonical public GitHub repository URL. Interactive setup asks when neither value is present. |
172+
| `--repository <url>` | Manifest `repo`, then Git `origin` | Canonical public GitHub repository URL. Interactive setup pre-fills a detected GitHub remote or asks when none is available. |
163173
| `--confirmation <mode>` | `escalation-only` | Use `escalation-only` for permission increases or `always` for every release. |
164174
| `--yes`, `-y` | `false` | Accept the default policy without prompting. Required when a non-interactive run would change the profile. |
165175

166-
The command uses the active CLI login to write the profile. It refuses to replace a different signed repository. Run `emdash-plugin switch <did>` when the active account does not match the manifest publisher.
176+
The command uses the active CLI login to write the profile. It refuses to replace a different signed repository. Run `emdash-plugin switch <did>` when the active account does not match the manifest publisher. After publishing the profile, it shows `emdash-plugin publish` for a manual release and `emdash-plugin release setup` for GitHub Actions.
167177

168178
## `release setup`
169179

170-
`release setup` runs package-profile setup, then creates one shared `.github/workflows/emdash-release.yml` at the Git repository root. Nested plugin packages reuse the same workflow.
180+
`release setup` runs package-profile setup from one plugin directory, then creates one shared `.github/workflows/emdash-release.yml` at the Git repository root. Nested plugin packages reuse the same workflow. Run it from a plugin directory or pass `--dir <plugin-directory>`; the repository root does not identify which package profile to prepare.
171181

172182
```sh
173183
emdash-plugin release setup

docs/src/content/docs/plugins/creating-plugins/delegated-releases.mdx

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@ Use [`emdash-plugin publish`](/plugins/creating-plugins/publishing/#publish) for
2121
Prepare the following before starting:
2222

2323
- A public GitHub repository containing a sandboxed EmDash plugin.
24-
- A valid [`emdash-plugin.jsonc`](/plugins/creating-plugins/manifest/) with `slug`, `publisher`, `license`, an author, and a security contact. Set `repo` to the canonical GitHub URL, or enter it during interactive setup.
24+
- `@emdash-cms/plugin-cli` installed as a development dependency. Plugins created with the CLI already include it.
25+
- A valid [`emdash-plugin.jsonc`](/plugins/creating-plugins/manifest/) with `slug`, `publisher`, `license`, an author, and a security contact. Set `repo` to the canonical GitHub URL, or confirm the detected GitHub remote during interactive setup.
2526
- A version in `package.json`, or in `emdash-plugin.jsonc` for a registry-only plugin.
2627
- The Atmosphere account named by `publisher`.
2728
- A browser that supports passkeys. Release approval requires user verification.
@@ -44,14 +45,16 @@ pnpm exec emdash-plugin validate
4445

4546
The CLI stores this local publishing session outside the project. GitHub Actions never receives it.
4647

47-
2. Prepare the package profile and generate the workflow.
48+
2. Prepare the package profile and generate the workflow from the plugin directory.
4849

4950
```sh
5051
pnpm exec emdash-plugin release setup
5152
```
5253

5354
The command reads the package metadata from `emdash-plugin.jsonc`. If the package profile is missing, it offers to create it. If the profile exists without delegated-release settings, it offers to add them while preserving the existing package metadata.
5455

56+
In a monorepo, run the command inside the plugin package or pass `--dir <plugin-directory>`. If the manifest does not contain `repo`, setup detects the GitHub `origin` remote and pre-fills the repository prompt.
57+
5558
Setup asks when a release needs approval:
5659

5760
- **When plugin permissions increase** is the default. A release waits for approval when its declared access expands relative to the latest release.
@@ -65,7 +68,9 @@ pnpm exec emdash-plugin validate
6568
pnpm exec emdash-plugin profile setup
6669
```
6770

68-
In a non-interactive terminal, pass `--yes` to accept the default approval policy. Pass `--repository <https-url>` when the manifest does not contain `repo`, and `--confirmation always` to require approval for every release.
71+
After publishing the package profile, this command shows the manual and GitHub Actions release commands.
72+
73+
In a non-interactive terminal, pass `--yes` to accept the default approval policy. Pass `--repository <https-url>` when neither the manifest nor Git remote supplies the repository, and `--confirmation always` to require approval for every release.
6974

7075
3. Review and commit the generated workflow.
7176

packages/plugin-cli/README.md

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,23 @@ CLI for authoring, building, and publishing EmDash plugins.
77
## Installation
88

99
```sh
10-
npx @emdash-cms/plugin-cli init my-plugin
10+
pnpm dlx @emdash-cms/plugin-cli init my-plugin
1111
```
1212

1313
Interactive setup collects the required publisher, author, and security metadata, detects the invoking package manager, and shows a project summary before writing. The scaffold includes a workerd-backed Vitest host, `AGENTS.md`, a canonical `skills/creating-plugins` skill shared through `.agents/skills` and `.claude/skills` symlinks, a Claude instruction link, package scripts for every validation and publishing command, and pnpm build-script policy when pnpm is selected.
1414

15+
The generated plugin includes `@emdash-cms/plugin-cli` as a pinned development dependency. Add it before using the CLI in an existing plugin:
16+
17+
```sh
18+
pnpm add -D @emdash-cms/plugin-cli
19+
```
20+
21+
Run project commands with `pnpm exec emdash-plugin`. This uses the version pinned by the plugin instead of downloading a potentially different version for each command.
22+
1523
Non-interactive setup requires explicit ownership metadata:
1624

1725
```sh
18-
npx @emdash-cms/plugin-cli init my-plugin --yes \
26+
pnpm dlx @emdash-cms/plugin-cli init my-plugin --yes \
1927
--publisher did:plc:abc123def456 \
2028
--author-name "Jane Doe" \
2129
--security-email security@example.com
@@ -118,16 +126,16 @@ On first publish, pass `--license` and `--security-email` (or `--security-url`)
118126

119127
See [Automated plugin releases](https://docs.emdashcms.com/plugins/creating-plugins/delegated-releases/) for the complete publisher journey, including release-service authorisation, first-run repository approval, passkeys, and troubleshooting.
120128

121-
Run the setup command from a public GitHub repository containing an EmDash plugin:
129+
Run the setup command from the plugin directory, not the monorepo root. Pass `--dir <plugin-directory>` when running it from elsewhere:
122130

123131
```sh
124-
emdash-plugin login <handle-or-did>
125-
emdash-plugin release setup
132+
pnpm exec emdash-plugin login <handle-or-did>
133+
pnpm exec emdash-plugin release setup
126134
```
127135

128136
The command reads the plugin metadata and publisher from `emdash-plugin.jsonc`. If the package profile does not exist, it offers to create it. If the profile predates delegated releases, it offers to add the signed repository and release policy while preserving the existing package metadata. The default policy requires the publisher's [Atmosphere account](https://docs.emdashcms.com/plugins/creating-plugins/publishing/#your-atmosphere-account) to approve releases when plugin permissions increase. Choose the every-release option to require approval each time.
129137

130-
Set `repo` in `emdash-plugin.jsonc`, or enter the canonical GitHub repository URL when prompted. The standalone `emdash-plugin profile setup` command prepares only the package profile.
138+
Set `repo` in `emdash-plugin.jsonc`, or confirm the canonical GitHub repository URL when prompted. If the manifest omits `repo`, setup detects a GitHub `origin` remote and uses it as the prompt default. The standalone `emdash-plugin profile setup` command prepares only the package profile, then shows the commands for publishing a release manually or configuring GitHub Actions.
131139

132140
Both setup commands accept `--repository <url>`, `--confirmation escalation-only|always`, and `--yes`. `release setup` also accepts `--service-url`, `--action-ref`, `--trigger auto|changesets|tags|manual`, and `--force` for the generated workflow. The default hosted service is `https://releases.emdashcms.com`.
133141

packages/plugin-cli/src/build/pipeline.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ export async function resolveSources(
130130
if (!(await fileExists(manifestPath))) {
131131
throw new BuildPipelineError(
132132
"MISSING_MANIFEST",
133-
`No ${MANIFEST_FILENAME} found in ${resolvedDir}. Scaffold one with: emdash-plugin init`,
133+
`No ${MANIFEST_FILENAME} found in ${resolvedDir}. Run this command from a plugin directory, or pass --dir <plugin-directory>.`,
134134
);
135135
}
136136

packages/plugin-cli/src/commands/profile.ts

Lines changed: 59 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import consola from "consola";
88
import pc from "picocolors";
99

1010
import { resolveSources } from "../build/pipeline.js";
11+
import { probeEnvironment } from "../init/environment.js";
1112
import { resolveHandleToDid } from "../manifest/publisher.js";
1213
import { manifestToProfileInput, resolveSections } from "../manifest/translate.js";
1314
import { resumeSession } from "../oauth.js";
@@ -22,34 +23,78 @@ export interface RunProfileSetupOptions {
2223
repository?: string;
2324
confirmation?: string;
2425
yes?: boolean;
26+
nextSteps?: boolean;
2527
}
2628

2729
function cancelled(value: unknown): asserts value is Exclude<typeof value, symbol> {
2830
if (clack.isCancel(value))
2931
throw new PackageProfileSetupError("INVALID_INPUT", "Setup cancelled.");
3032
}
3133

32-
async function repositoryValue(
33-
configured: string | undefined,
34-
interactive: boolean,
34+
interface RepositoryPromptOptions {
35+
message: string;
36+
defaultValue?: string;
37+
placeholder: string;
38+
validate(value: string | undefined): string | undefined;
39+
}
40+
41+
export interface ResolveProfileRepositoryOptions {
42+
configured: string | undefined;
43+
interactive: boolean;
44+
pluginDir: string;
45+
prompt?: (options: RepositoryPromptOptions) => Promise<unknown>;
46+
}
47+
48+
export async function resolveProfileRepository(
49+
options: ResolveProfileRepositoryOptions,
3550
): Promise<string> {
36-
if (configured) return configured;
37-
if (!interactive) {
51+
if (options.configured) return options.configured;
52+
const environment = await probeEnvironment(options.pluginDir);
53+
const detected = environment.repo
54+
? (canonicalGitHubRepository(environment.repo) ?? undefined)
55+
: undefined;
56+
if (!options.interactive) {
57+
if (detected) return detected;
3858
throw new PackageProfileSetupError(
3959
"INVALID_REPOSITORY",
4060
"Add `repo` to emdash-plugin.jsonc or pass --repository with an HTTPS GitHub repository URL.",
4161
);
4262
}
43-
const answer = await clack.text({
44-
message: "GitHub repository URL",
45-
placeholder: "https://github.com/example/gallery",
63+
const prompt = options.prompt ?? ((input) => clack.text(input));
64+
const answer = await prompt({
65+
message: detected
66+
? "GitHub repository URL (press enter to use the detected origin)"
67+
: "GitHub repository URL",
68+
...(detected === undefined ? {} : { defaultValue: detected }),
69+
placeholder: detected ?? "https://github.com/example/gallery",
4670
validate: (value) =>
4771
canonicalGitHubRepository(value ?? "") ? undefined : "Enter an HTTPS GitHub repository URL.",
4872
});
4973
cancelled(answer);
5074
return String(answer);
5175
}
5276

77+
export function printProfileSetupResult(
78+
result: { status: "created" | "ready" | "updated"; profileUri: string },
79+
confirmation: "always" | "escalation-only",
80+
showNextSteps: boolean,
81+
): void {
82+
if (result.status === "ready") {
83+
consola.success(`Package profile is ready: ${pc.dim(result.profileUri)}`);
84+
} else {
85+
consola.success(`Published package profile: ${pc.dim(result.profileUri)}`);
86+
consola.info(
87+
confirmation === "always"
88+
? "Your Atmosphere account must approve every release."
89+
: "Your Atmosphere account must approve releases when plugin permissions increase.",
90+
);
91+
}
92+
if (!showNextSteps) return;
93+
consola.info("Next, publish a release:");
94+
consola.info(` Manually: ${pc.cyan("emdash-plugin publish")}`);
95+
consola.info(` With GitHub Actions: ${pc.cyan("emdash-plugin release setup")}`);
96+
}
97+
5398
async function confirmationValue(
5499
configured: string | undefined,
55100
interactive: boolean,
@@ -111,10 +156,11 @@ async function runProfileSetupInternal(options: RunProfileSetupOptions): Promise
111156
`The active CLI account does not own ${sources.manifest.slug}. Run \`emdash-plugin switch ${publisherDid}\` first.`,
112157
);
113158
}
114-
const repository = await repositoryValue(
115-
options.repository ?? sources.manifest.repo,
159+
const repository = await resolveProfileRepository({
160+
configured: options.repository ?? sources.manifest.repo,
116161
interactive,
117-
);
162+
pluginDir: sources.pluginDir,
163+
});
118164
const confirmation = await confirmationValue(options.confirmation, interactive);
119165
const loaded = await import("../manifest/load.js").then(({ loadManifest }) =>
120166
loadManifest(sources.manifestPath),
@@ -135,7 +181,7 @@ async function runProfileSetupInternal(options: RunProfileSetupOptions): Promise
135181
};
136182
const proposed = await setupPackageProfile(input);
137183
if (proposed.status === "ready") {
138-
consola.success(`Package profile is ready: ${pc.dim(proposed.profileUri)}`);
184+
printProfileSetupResult(proposed, confirmation, options.nextSteps !== false);
139185
return;
140186
}
141187
if (!interactive && options.yes !== true) {
@@ -159,14 +205,7 @@ async function runProfileSetupInternal(options: RunProfileSetupOptions): Promise
159205
}
160206
}
161207
const result = await setupPackageProfile({ ...input, apply: true });
162-
consola.success(
163-
`${result.status === "created" ? "Created" : "Updated"} package profile: ${pc.dim(result.profileUri)}`,
164-
);
165-
consola.info(
166-
confirmation === "always"
167-
? "Your Atmosphere account must approve every release."
168-
: "Your Atmosphere account must approve releases when plugin permissions increase.",
169-
);
208+
printProfileSetupResult(result, confirmation, options.nextSteps !== false);
170209
}
171210

172211
export async function runProfileSetup(options: RunProfileSetupOptions): Promise<void> {

packages/plugin-cli/src/release-setup.ts

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import { resolveSources } from "./build/pipeline.js";
1111
import { runProfileSetup } from "./commands/profile.js";
1212
import { resolveHandleToDid } from "./manifest/publisher.js";
1313
import { installedCliVersion } from "./package-version.js";
14-
import { PackageProfileSetupError } from "./profile/setup.js";
1514
import { findRepositoryRoot } from "./release-prepare.js";
1615

1716
export const DEFAULT_RELEASE_SERVICE_URL = "https://releases.emdashcms.com";
@@ -623,6 +622,7 @@ export const releaseSetupCommand = defineCommand({
623622
repository: args.repository,
624623
confirmation: args.confirmation,
625624
yes: args.yes,
625+
nextSteps: false,
626626
}),
627627
});
628628
for (const warning of result.warnings) consola.warn(warning);
@@ -641,11 +641,8 @@ export const releaseSetupCommand = defineCommand({
641641
);
642642
consola.info("The first run links this repository workflow in the release dashboard.");
643643
} catch (error) {
644-
if (error instanceof ReleaseSetupError || error instanceof PackageProfileSetupError) {
645-
consola.error(error.message);
646-
process.exit(1);
647-
}
648-
throw error;
644+
consola.error(error instanceof Error ? error.message : "Release setup failed.");
645+
process.exit(1);
649646
}
650647
},
651648
});
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { execFile } from "node:child_process";
2+
import { mkdtemp, rm } from "node:fs/promises";
3+
import { tmpdir } from "node:os";
4+
import { join } from "node:path";
5+
import { fileURLToPath } from "node:url";
6+
7+
import { expect, it } from "vitest";
8+
9+
const CLI = fileURLToPath(new URL("../dist/index.mjs", import.meta.url));
10+
11+
function runCli(args: string[], cwd: string) {
12+
return new Promise<{ code: number | string | undefined; output: string }>((resolve) => {
13+
execFile(
14+
process.execPath,
15+
[CLI, ...args],
16+
{ cwd, env: { ...process.env, NO_COLOR: "1" } },
17+
(error, stdout, stderr) => {
18+
resolve({ code: error?.code, output: `${stdout}${stderr}` });
19+
},
20+
);
21+
});
22+
}
23+
24+
it("reports a missing plugin directory without a stack trace", async () => {
25+
const dir = await mkdtemp(join(tmpdir(), "emdash-release-setup-root-"));
26+
try {
27+
const result = await runCli(["release", "setup", "--yes"], dir);
28+
29+
expect(result.code).toBe(1);
30+
expect(result.output).toContain("Run this command from a plugin directory");
31+
expect(result.output).toContain("--dir <plugin-directory>");
32+
expect(result.output).not.toContain("BuildPipelineError");
33+
expect(result.output).not.toMatch(/\n\s+at /);
34+
} finally {
35+
await rm(dir, { recursive: true, force: true });
36+
}
37+
});

0 commit comments

Comments
 (0)