diff --git a/.sdd/ui-bundle-upload/plan.md b/.sdd/ui-bundle-upload/plan.md index 97b755a..746e1a2 100644 --- a/.sdd/ui-bundle-upload/plan.md +++ b/.sdd/ui-bundle-upload/plan.md @@ -50,14 +50,14 @@ None of the remaining non-goals (301/303/304/305) require a dedicated implementa **Exit criteria:** `upload.ts` compiles (even with a stubbed/unimplemented body), `UiBundleUploadResult` type exists and type-checks, message file loads without `Messages.loadMessages` throwing, and `@salesforce/source-deploy-retrieve` resolves as an installed dependency (`node -e "require.resolve('@salesforce/source-deploy-retrieve')"` exits 0). -| Step | File | Action | Spec ref | -| ---- | ------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------- | -| 1.0 | `package.json` (+ `yarn.lock`) | Add `@salesforce/source-deploy-retrieve` to the `dependencies` block — confirmed **absent** today (deps are `@inquirer/select`, `@oclif/core`, `@salesforce/core`, `@salesforce/kit`, `@salesforce/sf-plugins-core`, `@salesforce/ui-bundle`, `chokidar`, `form-data`, `http-proxy`, `micromatch`, `open`, `path-to-regexp`). Use the same `^`-caret range convention as the sibling `@salesforce/*` deps (exact minor version resolved at install time). Run `yarn install` to update `yarn.lock`. This is the **one intended exception** to the §5.2 zero-diff-on-existing rule (spec §2.2 item 8, §2.4, §5.2). Falsifiable: `node -e "require.resolve('@salesforce/source-deploy-retrieve')"` exits 0 and `git diff package.json` shows exactly one added dependency line plus lockfile churn, nothing else in the file. | spec §2.4 (REQ-302) | -| 1.1 | `src/config/types.ts` | Append `UiBundleUploadResult` type directly below existing `UiBundleDevResult` export (confirm exact current line span with a fresh read before editing, since line numbers age). Shape: `{ jobId: string; status: 'Queued' \| 'InProgress' \| 'Succeeded' \| 'Failed'; message?: string }`. The existing `UiBundleDevResult` export must show zero diff — this row is also covered by the spec §5.2 Non-Regression Checklist. | spec §2.3 AC4 (REQ-113) | -| 1.2 | `messages/ui-bundle.upload.md` | Create with `# summary`, `# description`, `# flags.zip-file.summary/.description`, `# flags.bundle-dir.summary/.description`, `# flags.use-salesforce-pages.summary/.description`, `# examples`, plus `# error.*` keys for the 3 CLI-side error names. Model structure on `messages/ui-bundle.dev.md`'s key list (confirmed 40+ keys) but only include upload-relevant keys — do not copy `dev`'s `info.*`/`warning.*` runtime keys verbatim, they're dev-server-specific. **Must also define message keys for ALL customer-facing output the command emits** (spec §6.3): the empty-bundle-dir error message (currently inlined at `compressDirectory`'s empty-dir check, line 50 in upload.ts), the compression-failure error message (line 57), and the `Failed`-status human failure block — the "Upload failed" line + its `Job ID:`/`Message:` labels (lines 144-145). Note: the message file already carries unused `# error.*` keys (`error.upload-failed`, `error.auth-failed`, `error.network-failed`, `error.validation-failed`) that the code doesn't currently reference — the guideline's intent is that authored output routes through such keys rather than duplicating strings inline. Flag names are settled (`--zip-file`/`-z`, `--bundle-dir`/`-d`, `--use-salesforce-pages` no short char) — no gating on this step. | spec §2.4, §3.2, §6.3 | -| 1.3 | `src/commands/ui-bundle/upload.ts` | Create command class `UiBundleUpload extends SfCommand`. Declare `public static readonly state = 'preview';` so the command ships developer-preview — `sf-plugins-core` emits the runtime warning on every invocation and oclif prints `This command is in preview.` in `--help` (spec §2.4). Apache-2.0 header (copy verbatim from `src/index.ts`'s 15-line block — confirmed exact text in scouting). Messages boilerplate mirroring `dev.ts:29-30` exactly, with bundle key `'ui-bundle.upload'`. Static `flags` object: `'zip-file': Flags.file({ summary: ..., description: ..., char: 'z', exists: true, exactlyOne: ['zip-file', 'bundle-dir'] })` — **no longer `required: true`**; `'bundle-dir': Flags.directory({ summary: ..., description: ..., char: 'd', exists: true, exactlyOne: ['zip-file', 'bundle-dir'] })`; `'use-salesforce-pages': Flags.boolean({ summary: ..., description: ..., required: true })` (no `char` — no short flag; always invoked as the full `--use-salesforce-pages`, avoiding `-p` collision with `dev`'s `--port`); `'target-org': Flags.requiredOrg()`. Leave `run()` body as a stub (`throw new Error('not implemented')` or similar) for now — implementation is Phase 2. Flag names, `exactlyOne` relationship, required-boolean shape, and preview state are all settled design — no gating on this step. | spec §2.4 | -| 1.4 | `src/commands/ui-bundle/upload.ts` + `messages/ui-bundle.upload.md` | Add the `--bundle-name` flag declaration to `upload.ts`'s static `flags` object: `'bundle-name': Flags.string({ summary: messages.getMessage('flags.bundle-name.summary'), description: messages.getMessage('flags.bundle-name.description') })` (no `char`, optional). Add the two corresponding message keys to `messages/ui-bundle.upload.md`: `# flags.bundle-name.summary` ("Name to associate with the uploaded UI Bundle.") and `# flags.bundle-name.description` ("A human-readable name for the UI Bundle. If not specified, defaults to the base name of --bundle-dir or --zip-file, with any .zip extension removed."). | spec §2.4 (REQ-116) | -| 1.5 | `src/commands/ui-bundle/upload.ts` | Add the `--api-version` flag declaration to the static `flags` object: `'api-version': Flags.orgApiVersion()` (no options, no summary/description override — the factory supplies its own). Add the module-level constant above the command class: `const MINIMUM_SUPPORTED_API_VERSION = 67;`. | spec §2.4 (REQ-117) | +| Step | File | Action | Spec ref | +| ---- | ------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------- | +| 1.0 | `package.json` (+ `yarn.lock`) | Add `@salesforce/source-deploy-retrieve` to the `dependencies` block — confirmed **absent** today (deps are `@inquirer/select`, `@oclif/core`, `@salesforce/core`, `@salesforce/kit`, `@salesforce/sf-plugins-core`, `@salesforce/ui-bundle`, `chokidar`, `form-data`, `http-proxy`, `micromatch`, `open`, `path-to-regexp`). Use the same `^`-caret range convention as the sibling `@salesforce/*` deps (exact minor version resolved at install time). Run `yarn install` to update `yarn.lock`. This is the **one intended exception** to the §5.2 zero-diff-on-existing rule (spec §2.2 item 8, §2.4, §5.2). Falsifiable: `node -e "require.resolve('@salesforce/source-deploy-retrieve')"` exits 0 and `git diff package.json` shows exactly one added dependency line plus lockfile churn, nothing else in the file. | spec §2.4 (REQ-302) | +| 1.1 | `src/config/types.ts` | Append `UiBundleUploadResult` type directly below existing `UiBundleDevResult` export (confirm exact current line span with a fresh read before editing, since line numbers age). Shape: `{ jobId: string; status: 'Queued' \| 'InProgress' \| 'Succeeded' \| 'Failed'; message?: string }`. The existing `UiBundleDevResult` export must show zero diff — this row is also covered by the spec §5.2 Non-Regression Checklist. | spec §2.3 AC4 (REQ-113) | +| 1.2 | `messages/ui-bundle.upload.md` | Create with `# summary`, `# description`, `# flags.zip-file.summary/.description`, `# flags.bundle-dir.summary/.description`, `# flags.use-salesforce-pages.summary/.description`, `# examples`, plus `# error.*` keys for the 3 CLI-side error names. Model structure on `messages/ui-bundle.dev.md`'s key list (confirmed 40+ keys) but only include upload-relevant keys — do not copy `dev`'s `info.*`/`warning.*` runtime keys verbatim, they're dev-server-specific. **Must also define message keys for ALL customer-facing output the command emits** (spec §6.3): the empty-bundle-dir error message (currently inlined at `compressDirectory`'s empty-dir check, line 50 in upload.ts), the compression-failure error message (line 57), and the `Failed`-status human failure block — the "Upload failed" line + its `Job ID:`/`Message:` labels (lines 144-145). Note: the message file already carries unused `# error.*` keys (`error.upload-failed`, `error.auth-failed`, `error.network-failed`, `error.validation-failed`) that the code doesn't currently reference — the guideline's intent is that authored output routes through such keys rather than duplicating strings inline. Flag names are settled (`--zip-file`/`-z`, `--bundle-dir`/`-d`, `--use-salesforce-pages` no short char) — no gating on this step. | spec §2.4, §3.2, §6.3 | +| 1.3 | `src/commands/ui-bundle/upload.ts` | Create command class `UiBundleUpload extends SfCommand`. Declare `public static readonly state = 'preview';` so the command ships developer-preview — `sf-plugins-core` emits the runtime warning on every invocation and oclif prints `This command is in preview.` in `--help` (spec §2.4). Declare `public static readonly hidden = true;` directly below `state` so the command is also omitted from `--help`'s command listing, from topic help, and from `oclif readme`-generated `COMMANDS.md`, while remaining directly invocable, still rendering its own `--help` page when named explicitly, and still present in `command-snapshot.json` (spec §2.4 Command visibility, AC11). Apache-2.0 header (copy verbatim from `src/index.ts`'s 15-line block — confirmed exact text in scouting). Messages boilerplate mirroring `dev.ts:29-30` exactly, with bundle key `'ui-bundle.upload'`. Static `flags` object: `'zip-file': Flags.file({ summary: ..., description: ..., char: 'z', exists: true, exactlyOne: ['zip-file', 'bundle-dir'] })` — **no longer `required: true`**; `'bundle-dir': Flags.directory({ summary: ..., description: ..., char: 'd', exists: true, exactlyOne: ['zip-file', 'bundle-dir'] })`; `'use-salesforce-pages': Flags.boolean({ summary: ..., description: ..., required: true })` (no `char` — no short flag; always invoked as the full `--use-salesforce-pages`, avoiding `-p` collision with `dev`'s `--port`); `'target-org': Flags.requiredOrg()`. Leave `run()` body as a stub (`throw new Error('not implemented')` or similar) for now — implementation is Phase 2. Flag names, `exactlyOne` relationship, required-boolean shape, preview state, and hidden visibility are all settled design — no gating on this step. | spec §2.4 | +| 1.4 | `src/commands/ui-bundle/upload.ts` + `messages/ui-bundle.upload.md` | Add the `--bundle-name` flag declaration to `upload.ts`'s static `flags` object: `'bundle-name': Flags.string({ summary: messages.getMessage('flags.bundle-name.summary'), description: messages.getMessage('flags.bundle-name.description') })` (no `char`, optional). Add the two corresponding message keys to `messages/ui-bundle.upload.md`: `# flags.bundle-name.summary` ("Name to associate with the uploaded UI Bundle.") and `# flags.bundle-name.description` ("A human-readable name for the UI Bundle. If not specified, defaults to the base name of --bundle-dir or --zip-file, with any .zip extension removed."). | spec §2.4 (REQ-116) | +| 1.5 | `src/commands/ui-bundle/upload.ts` | Add the `--api-version` flag declaration to the static `flags` object: `'api-version': Flags.orgApiVersion()` (no options, no summary/description override — the factory supplies its own). Add the module-level constant above the command class: `const MINIMUM_SUPPORTED_API_VERSION = 67;`. | spec §2.4 (REQ-117) | **Note on flag pattern fidelity:** the scout found `dev.ts`'s `open` flag uses `default: false` instead of `required`, and `target-org` has no inline `summary`/`description`/`char` at all. Do **not** copy those two deviations into `upload.ts` — `--use-salesforce-pages` is `required: true` per REQ-104, and the two bundle-source flags (`zip-file`, `bundle-dir`) are governed by their `exactlyOne` group (REQ-102/102b) rather than a standalone `required: true`, so the `default: false` deviation doesn't apply here; `target-org` correctly stays a bare `Flags.requiredOrg()` call with no local wiring, matching `dev.ts:73`. Char assignments: `zip-file` → `z`, `bundle-dir` → `d` (both free — `dev` uses `b/n/o/p/u`, so no collision, confirmed spec §2.4), and `--use-salesforce-pages` has no `char` at all, so `upload` never binds anything to `-p`. @@ -92,14 +92,14 @@ None of the remaining non-goals (301/303/304/305) require a dedicated implementa **Entry criteria:** Phase 2 exit criteria met; `run()` is feature-complete and manually verified against at least one success and one failure case. -**Exit criteria:** `schemas/ui__bundle-upload.json` validates the shape from 2.4; `command-snapshot.json` has 2 array elements; `README.md`/`COMMANDS.md` both mention `upload` without disturbing `dev`'s sections. +**Exit criteria:** `schemas/ui__bundle-upload.json` validates the shape from 2.4; `command-snapshot.json` has 2 array elements (`hidden` doesn't filter the snapshot); `README.md`'s hand-written subsection mentions `upload` without disturbing `dev`'s section; `COMMANDS.md`, tool-generated via `oclif readme`, shows **zero new `upload` content** — `hidden = true` suppresses it from that generator's output — and `dev`'s existing section is untouched. -| Step | Action | Spec ref | -| ---- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------- | -| 3.1 | Create `schemas/ui__bundle-upload.json` — draft-07, `$ref` → `definitions.UiBundleUploadResult`, properties `jobId`/`status`/`message`, `required: ["jobId", "status"]` (message is optional per the type), `additionalProperties: false`. Mirror `schemas/ui__bundle-dev.json`'s exact structural pattern (confirmed 23-line reference file). Flag naming convention is settled — no gating on this step. | spec §2.3 AC4 + §5.2 | -| 3.2 | Regenerate `command-snapshot.json` by running `node --loader ts-node/esm --no-warnings=ExperimentalWarning "./bin/dev.js" snapshot:generate` (from `@oclif/plugin-command-snapshot`, the same package that backs the `test:deprecation-policy` wireit step's `snapshot:compare` command — confirmed via `package.json`'s script definitions and the plugin's own `commands/snapshot/generate.js`/`compare.js` source). Default output path is `./command-snapshot.json`. **Do not** rely on `yarn test` to regenerate the file: `test:deprecation-policy` only runs `snapshot:compare`, which is read-only — it logs a diff and sets a non-zero exit code on drift, it never calls `write()`. `snapshot:generate` is the correct, existing-pattern command, directly analogous to the `schema:generate`/`schema:compare` pair backing `test:json-schema`. **Then run `npx prettier --write command-snapshot.json`** — the generator emits 4-space indentation with no trailing newline, which diverges from the repo's 2-space prettier config; skipping this leaves an unnecessary formatting diff (a real prior gotcha). After running, diff the file: confirm the existing `ui-bundle:dev` element is byte-for-byte identical (`flagChars: ["b","n","o","p","u"]`, `flags: [...]` — confirmed exact array in scouting) and a **second** element for `ui-bundle:upload` was appended (not the first replaced). Expected new element: `command: "ui-bundle:upload"`, `flagChars` includes both `z` (from `--zip-file`) and `d` (from `--bundle-dir`) plus the base-flag char `o` (from `--target-org`, inherited the same way `dev`'s `o` is) — so `flagChars: ["d","o","z"]`; `flags` includes `use-salesforce-pages`, `bundle-dir`, `flags-dir`, `json`, `target-org`, `zip-file`. `--use-salesforce-pages` has no `char`, so `p` must **not** appear in `upload`'s `flagChars`. Verify against the actual generated output rather than hand-writing this file. Snapshot also reflects the preview `state` on the command. | spec §5.2 Non-Regression Checklist | -| 3.3 | Append `### sf ui-bundle upload` subsection to `README.md`, inserted at the end of the existing `sf ui-bundle dev` subsection and before the `` marker (re-confirm exact line numbers with a fresh read — they age). The generated block must reflect the current contract (spec §2.4): the `This command is in preview.` banner, the `(-z \| -d )` exactly-one usage group, both `-z, --zip-file` and `-d, --bundle-dir` flags, and `--use-salesforce-pages`. Do not touch the Features/Quick Start/Documentation prose or the existing `dev` subsection. | spec §5.2 (README non-regression + REQ-209) | -| 3.4 | Regenerate `COMMANDS.md` via `oclif readme` (the `version` script, per scouting) so a new TOC line + `## \`sf ui-bundle upload\``section is inserted between the existing``/``markers (re-confirm exact marker line numbers with a fresh read). The generated`upload`section must carry the preview banner, the`(-z \| -d)`exactly-one usage, both bundle-source flags, and`--use-salesforce-pages`. Do not hand-edit. | spec §5.2 (REQ-212) | +| Step | Action | Spec ref | +| ---- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------- | +| 3.1 | Create `schemas/ui__bundle-upload.json` — draft-07, `$ref` → `definitions.UiBundleUploadResult`, properties `jobId`/`status`/`message`, `required: ["jobId", "status"]` (message is optional per the type), `additionalProperties: false`. Mirror `schemas/ui__bundle-dev.json`'s exact structural pattern (confirmed 23-line reference file). Flag naming convention is settled — no gating on this step. | spec §2.3 AC4 + §5.2 | +| 3.2 | Regenerate `command-snapshot.json` by running `node --loader ts-node/esm --no-warnings=ExperimentalWarning "./bin/dev.js" snapshot:generate` (from `@oclif/plugin-command-snapshot`, the same package that backs the `test:deprecation-policy` wireit step's `snapshot:compare` command — confirmed via `package.json`'s script definitions and the plugin's own `commands/snapshot/generate.js`/`compare.js` source). Default output path is `./command-snapshot.json`. **Do not** rely on `yarn test` to regenerate the file: `test:deprecation-policy` only runs `snapshot:compare`, which is read-only — it logs a diff and sets a non-zero exit code on drift, it never calls `write()`. `snapshot:generate` is the correct, existing-pattern command, directly analogous to the `schema:generate`/`schema:compare` pair backing `test:json-schema`. **Then run `npx prettier --write command-snapshot.json`** — the generator emits 4-space indentation with no trailing newline, which diverges from the repo's 2-space prettier config; skipping this leaves an unnecessary formatting diff (a real prior gotcha). After running, diff the file: confirm the existing `ui-bundle:dev` element is byte-for-byte identical (`flagChars: ["b","n","o","p","u"]`, `flags: [...]` — confirmed exact array in scouting) and a **second** element for `ui-bundle:upload` was appended (not the first replaced). Expected new element: `command: "ui-bundle:upload"`, `flagChars` includes both `z` (from `--zip-file`) and `d` (from `--bundle-dir`) plus the base-flag char `o` (from `--target-org`, inherited the same way `dev`'s `o` is) — so `flagChars: ["d","o","z"]`; `flags` includes `use-salesforce-pages`, `bundle-dir`, `flags-dir`, `json`, `target-org`, `zip-file`. `--use-salesforce-pages` has no `char`, so `p` must **not** appear in `upload`'s `flagChars`. Verify against the actual generated output rather than hand-writing this file. Snapshot also reflects the preview `state` on the command. `hidden = true` has no effect on this file — `@oclif/plugin-command-snapshot`'s `SnapshotCommand#commands` doesn't filter on `hidden`, so the `ui-bundle:upload` element is expected to appear regardless (spec §2.4 Command visibility). | spec §5.2 Non-Regression Checklist | +| 3.3 | Append `### sf ui-bundle upload` subsection to `README.md`, inserted at the end of the existing `sf ui-bundle dev` subsection and before the `` marker (re-confirm exact line numbers with a fresh read — they age). The generated block must reflect the current contract (spec §2.4): the `This command is in preview.` banner, the `(-z \| -d )` exactly-one usage group, both `-z, --zip-file` and `-d, --bundle-dir` flags, and `--use-salesforce-pages`. Do not touch the Features/Quick Start/Documentation prose or the existing `dev` subsection. This subsection is hand-maintained prose, not `oclif readme` output, so `hidden = true` does **not** suppress it automatically -- a deliberate exception, kept as documentation for maintainers even though the command is hidden from CLI help surfaces (spec §2.4 Command visibility). | spec §5.2 (README non-regression + REQ-209) | +| 3.4 | Regenerate `COMMANDS.md` via `oclif readme` (the `version` script, per scouting). Because `upload.ts` now declares `hidden = true`, the generator's command-listing filter (`!c.hidden`) must NOT emit a TOC line or a `## `sf ui-bundle upload``section for it -- confirm the regenerated`COMMANDS.md` shows zero new content (`git diff COMMANDS.md`is empty) rather than a new`upload` section. This reverses the pre-`hidden`expectation (a new section would previously have been expected here); do not hand-edit`COMMANDS.md` to force a section back in. | spec §5.2 (REQ-212), §2.4 (AC11 118e) | **Non-regression checkpoint 2** — run here, immediately after 3.2 (the snapshot regen), since this is the single highest-risk step for silently mutating the existing `dev` array element. @@ -111,11 +111,11 @@ None of the remaining non-goals (301/303/304/305) require a dedicated implementa **Exit criteria:** `upload.test.ts` green; `upload.nut.ts` Tier 1 green unconditionally, Tier 2 green when `TESTKIT_AUTH_URL` is set (or throws the mandated error when unset — not silently skipped). -| Step | Action | Spec ref | -| ---- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------- | -| 4.1 | Write `test/commands/ui-bundle/upload.test.ts` — Mocha + Chai + Sinon + `TestContext` from `@salesforce/core/testSetup`, `.js`-suffixed relative imports (ESM/NodeNext style), matching `dev.test.ts`'s exact import block (confirmed lines 17-20). Cover: **neither `--zip-file` nor `--bundle-dir`** → `FailedFlagValidationError` (`Exactly one of the following must be provided: --zip-file, --bundle-dir`), no network call (REQ-102); **both `--zip-file` and `--bundle-dir`** → `FailedFlagValidationError` (`--zip-file cannot also be provided when using --bundle-dir`, or the symmetric message depending on parse order — assert on the stable prefix, not the exact wording), no network call (REQ-102b); missing `--use-salesforce-pages` → `FailedFlagValidationError` (`Missing required flag use-salesforce-pages`) (REQ-104); missing `--target-org` with no default → `NoDefaultEnvError`, asserted the same way `dev.nut.ts:58` does it (`expect(result.jsonOutput?.name).to.equal('NoDefaultEnvError')` — note it's the _string_ `'NoDefaultEnvError'`, not an imported constant) but as a **unit** test equivalent, not a NUT (REQ-105); non-existent `--zip-file` path (`Flags.file` validation error); non-existent / not-a-directory `--bundle-dir` path (`Flags.directory` validation error); **`--bundle-dir` compression path** — a `--bundle-dir` pointing at a real directory fixture (Step 4.2) is compressed via `@salesforce/source-deploy-retrieve` and the multipart `bundle` part is a zip identical in shape to the `--zip-file` path (REQ-302); **`--bundle-dir` dotfile/dot-directory exclusion** — a `--bundle-dir` fixture containing dotfiles (e.g. `.env`, `assets/.hidden`) and/or a dot-directory (e.g. `.git/` with files inside) → the compressed zip's entry list excludes all dotfile/dot-directory entries, while sibling non-dotfile files in the same directory are still included (REQ-114); **`--bundle-dir` symlink resolution** — a `--bundle-dir` fixture containing a symlinked file and a symlinked directory (created via `symlinkSync`, skipped via `this.skip()` if creation is rejected in the current environment, e.g. an unprivileged Windows runner) → the compressed zip's entry list includes the symlinked file's target content at the symlink's path and the symlinked directory's nested contents, resolved rather than skipped (REQ-115); **`--zip-file` path** — file sent as-is, no re-compression pass; `Queued` response in both human and `--json` modes (the only response shape the merged Core contract documents — the `202` representation is fixed at `{ jobId, status: "Queued" }`, spec §2.5); `Failed` response in both modes (defensive-path coverage per AC2 / spec §2.6 / §3.1 case 5 / §3.2 — mocked here since it's not a server behavior the real endpoint is expected to produce, only a shape the code must handle if it ever occurs), asserting the Failed-block output (the "Upload failed" line and `Job ID:`/`Message:` labels) renders correctly from the message file; each of the 3 CLI-side `SfError` names (the _actually_-synchronous failure path, per spec §3.2); the developer-preview warning is emitted (`state = 'preview'`) and not suppressed under `--json`'s result payload. **Assert `upload.ts` inlines no customer-facing output literal** — all such output resolves via `messages.getMessage()` per spec §6.3 (the empty-dir error, compression-failure error, and Failed-block output all come from the message file): `grep 'The bundle source directory is empty' src/commands/ui-bundle/upload.ts` → zero matches, `grep 'Failed to compress the bundle source directory' src/commands/ui-bundle/upload.ts` → zero matches, `grep 'Upload failed' src/commands/ui-bundle/upload.ts` → zero matches (the `Failed` block lives in the message file). The `--bundle-dir` compression cases consume the directory fixtures from Step 4.2 (the plain fixture, the dotfile/dot-directory fixture, and the symlink fixture); the connection is mocked for all cases. | spec §5.1, §6.3, AC8 (REQ-115) | -| 4.2 | Add the test-fixture helpers — pick names that don't collide with the confirmed existing exports in `devServerUtils.ts` (`SUITE_TIMEOUT`, `SPAWN_TIMEOUT`, `SPAWN_FAIL_TIMEOUT`, `UiBundleDevHandle`, `spawnUiBundleDev`, `occupyPort`, `startTestHttpServer`, `startViteProxyServer`, `closeServer`) or `uiBundleProjectUtils.ts` (`uiBundlePath`, `authOrgViaUrl`, `createProject`, `createUiBundle`, `createProjectWithUiBundle`, `createProjectWithMultipleUiBundles`, `createEmptyUiBundlesDir`, `createUiBundleDirWithoutMeta`, `writeManifest`, `createProjectWithDevServer`, plus the unexported module-local `createDevServerScript`). Fixtures needed: `createZipFixture` (a pre-built zip for the `--zip-file` path — the spec's suggested name, confirmed collision-free) **and** a `createBundleDirFixture`-style helper that materializes a real uncompressed source directory for the `--bundle-dir` compression path (Step 4.1's compression case and Step 4.3's Tier 2 `--bundle-dir` run both consume it), plus a variant/extension of the directory-fixture helper that also includes dotfiles (e.g. `.env`, `assets/.hidden`) and a dot-directory (e.g. `.git/` with nested files) for the Step 4.1 dotfile-exclusion test case (REQ-114), plus a further variant that creates a symlinked file and a symlinked directory (each pointing at a real target outside the fixture directory) via `symlinkSync` for the Step 4.1 symlink-resolution test case (REQ-115) — this helper must tolerate environments that reject symlink creation (e.g. an unprivileged Windows runner without Developer Mode returns `EPERM`/`ENOSYS`) by signaling the caller to skip the test rather than failing the whole suite — confirm all chosen names are collision-free before use. **Decide each fixture's storage location as part of writing these helpers** (folded in from a spec gap the scout flagged): `_cleanup.nut.ts` sweeps `test_session_*` directories under `process.cwd()` after all NUTs finish, so if a fixture is written **inside** a `TestSession`-managed `test_session_*` dir, cleanup is automatic and no teardown code is needed; if it must live in a separate out-of-session temp/fixtures folder, `_cleanup.nut.ts` will **not** catch it and `upload.nut.ts` (Step 4.3) needs its own teardown. Prefer routing through the existing `TestSession` temp dir — zero new cleanup code — and only add manual teardown if that's not viable. | spec §5.2 Non-Regression Checklist (REQ-208) | -| 4.3 | Write `test/commands/ui-bundle/upload.nut.ts` — Tier 1 (`describe('ui-bundle upload NUTs — Tier 1 (no auth)', ...)`, no guard, `TestSession.create({ devhubAuthStrategy: 'NONE' })` only, flag-parse-only assertions — including neither/both of `--zip-file`/`--bundle-dir` (exactly-one) and missing `--use-salesforce-pages`; note even flag-parse-only NUTs plausibly need a real file/dir path since `Flags.file`/`Flags.directory` check existence, so Tier 1 also consumes the Step 4.2 fixtures) + Tier 2 (guard: `if (!process.env.TESTKIT_AUTH_URL) throw new Error(...)` — must throw, not skip, per the existing contract at `dev.nut.ts:76-85`/`devPort.nut.ts:53-58`/`devWithUrl.nut.ts:61-65`; assert the real-org `POST` returns a `Queued` job id for **both** the `--zip-file` and the `--bundle-dir` (auto-compressed) sources). Consumes the fixture helpers from Step 4.2 — write 4.2 first. | spec §5.2 | +| Step | Action | Spec ref | +| ---- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------- | +| 4.1 | Write `test/commands/ui-bundle/upload.test.ts` — Mocha + Chai + Sinon + `TestContext` from `@salesforce/core/testSetup`, `.js`-suffixed relative imports (ESM/NodeNext style), matching `dev.test.ts`'s exact import block (confirmed lines 17-20). Cover: **neither `--zip-file` nor `--bundle-dir`** → `FailedFlagValidationError` (`Exactly one of the following must be provided: --zip-file, --bundle-dir`), no network call (REQ-102); **both `--zip-file` and `--bundle-dir`** → `FailedFlagValidationError` (`--zip-file cannot also be provided when using --bundle-dir`, or the symmetric message depending on parse order — assert on the stable prefix, not the exact wording), no network call (REQ-102b); missing `--use-salesforce-pages` → `FailedFlagValidationError` (`Missing required flag use-salesforce-pages`) (REQ-104); missing `--target-org` with no default → `NoDefaultEnvError`, asserted the same way `dev.nut.ts:58` does it (`expect(result.jsonOutput?.name).to.equal('NoDefaultEnvError')` — note it's the _string_ `'NoDefaultEnvError'`, not an imported constant) but as a **unit** test equivalent, not a NUT (REQ-105); non-existent `--zip-file` path (`Flags.file` validation error); non-existent / not-a-directory `--bundle-dir` path (`Flags.directory` validation error); **`--bundle-dir` compression path** — a `--bundle-dir` pointing at a real directory fixture (Step 4.2) is compressed via `@salesforce/source-deploy-retrieve` and the multipart `bundle` part is a zip identical in shape to the `--zip-file` path (REQ-302); **`--bundle-dir` dotfile/dot-directory exclusion** — a `--bundle-dir` fixture containing dotfiles (e.g. `.env`, `assets/.hidden`) and/or a dot-directory (e.g. `.git/` with files inside) → the compressed zip's entry list excludes all dotfile/dot-directory entries, while sibling non-dotfile files in the same directory are still included (REQ-114); **`--bundle-dir` symlink resolution** — a `--bundle-dir` fixture containing a symlinked file and a symlinked directory (created via `symlinkSync`, skipped via `this.skip()` if creation is rejected in the current environment, e.g. an unprivileged Windows runner) → the compressed zip's entry list includes the symlinked file's target content at the symlink's path and the symlinked directory's nested contents, resolved rather than skipped (REQ-115); **`--zip-file` path** — file sent as-is, no re-compression pass; `Queued` response in both human and `--json` modes (the only response shape the merged Core contract documents — the `202` representation is fixed at `{ jobId, status: "Queued" }`, spec §2.5); `Failed` response in both modes (defensive-path coverage per AC2 / spec §2.6 / §3.1 case 5 / §3.2 — mocked here since it's not a server behavior the real endpoint is expected to produce, only a shape the code must handle if it ever occurs), asserting the Failed-block output (the "Upload failed" line and `Job ID:`/`Message:` labels) renders correctly from the message file; each of the 3 CLI-side `SfError` names (the _actually_-synchronous failure path, per spec §3.2); the developer-preview warning is emitted (`state = 'preview'`) and not suppressed under `--json`'s result payload; the command class's static `state`/`hidden` properties are asserted directly (`UiBundleUpload.state === 'preview'`, `UiBundleUpload.hidden === true`, spec §2.4 Command visibility / AC11). **Assert `upload.ts` inlines no customer-facing output literal** — all such output resolves via `messages.getMessage()` per spec §6.3 (the empty-dir error, compression-failure error, and Failed-block output all come from the message file): `grep 'The bundle source directory is empty' src/commands/ui-bundle/upload.ts` → zero matches, `grep 'Failed to compress the bundle source directory' src/commands/ui-bundle/upload.ts` → zero matches, `grep 'Upload failed' src/commands/ui-bundle/upload.ts` → zero matches (the `Failed` block lives in the message file). The `--bundle-dir` compression cases consume the directory fixtures from Step 4.2 (the plain fixture, the dotfile/dot-directory fixture, and the symlink fixture); the connection is mocked for all cases. | spec §5.1, §6.3, AC8 (REQ-115) | +| 4.2 | Add the test-fixture helpers — pick names that don't collide with the confirmed existing exports in `devServerUtils.ts` (`SUITE_TIMEOUT`, `SPAWN_TIMEOUT`, `SPAWN_FAIL_TIMEOUT`, `UiBundleDevHandle`, `spawnUiBundleDev`, `occupyPort`, `startTestHttpServer`, `startViteProxyServer`, `closeServer`) or `uiBundleProjectUtils.ts` (`uiBundlePath`, `authOrgViaUrl`, `createProject`, `createUiBundle`, `createProjectWithUiBundle`, `createProjectWithMultipleUiBundles`, `createEmptyUiBundlesDir`, `createUiBundleDirWithoutMeta`, `writeManifest`, `createProjectWithDevServer`, plus the unexported module-local `createDevServerScript`). Fixtures needed: `createZipFixture` (a pre-built zip for the `--zip-file` path — the spec's suggested name, confirmed collision-free) **and** a `createBundleDirFixture`-style helper that materializes a real uncompressed source directory for the `--bundle-dir` compression path (Step 4.1's compression case and Step 4.3's Tier 2 `--bundle-dir` run both consume it), plus a variant/extension of the directory-fixture helper that also includes dotfiles (e.g. `.env`, `assets/.hidden`) and a dot-directory (e.g. `.git/` with nested files) for the Step 4.1 dotfile-exclusion test case (REQ-114), plus a further variant that creates a symlinked file and a symlinked directory (each pointing at a real target outside the fixture directory) via `symlinkSync` for the Step 4.1 symlink-resolution test case (REQ-115) — this helper must tolerate environments that reject symlink creation (e.g. an unprivileged Windows runner without Developer Mode returns `EPERM`/`ENOSYS`) by signaling the caller to skip the test rather than failing the whole suite — confirm all chosen names are collision-free before use. **Decide each fixture's storage location as part of writing these helpers** (folded in from a spec gap the scout flagged): `_cleanup.nut.ts` sweeps `test_session_*` directories under `process.cwd()` after all NUTs finish, so if a fixture is written **inside** a `TestSession`-managed `test_session_*` dir, cleanup is automatic and no teardown code is needed; if it must live in a separate out-of-session temp/fixtures folder, `_cleanup.nut.ts` will **not** catch it and `upload.nut.ts` (Step 4.3) needs its own teardown. Prefer routing through the existing `TestSession` temp dir — zero new cleanup code — and only add manual teardown if that's not viable. | spec §5.2 Non-Regression Checklist (REQ-208) | +| 4.3 | Write `test/commands/ui-bundle/upload.nut.ts` — Tier 1 (`describe('ui-bundle upload NUTs — Tier 1 (no auth)', ...)`, no guard, `TestSession.create({ devhubAuthStrategy: 'NONE' })` only, flag-parse-only assertions — including neither/both of `--zip-file`/`--bundle-dir` (exactly-one) and missing `--use-salesforce-pages`; note even flag-parse-only NUTs plausibly need a real file/dir path since `Flags.file`/`Flags.directory` check existence, so Tier 1 also consumes the Step 4.2 fixtures) + Tier 2 (guard: `if (!process.env.TESTKIT_AUTH_URL) throw new Error(...)` — must throw, not skip, per the existing contract at `dev.nut.ts:76-85`/`devPort.nut.ts:53-58`/`devWithUrl.nut.ts:61-65`; assert the real-org `POST` returns a `Queued` job id for **both** the `--zip-file` and the `--bundle-dir` (auto-compressed) sources). Consumes the fixture helpers from Step 4.2 — write 4.2 first. | spec §5.2 | **Non-regression checkpoint 4** — run here, before moving to Phase 5, since Phase 4 is the first point new test files exist alongside the existing `dev` suites in the same `mocha` glob patterns (`test/**/*.test.ts`, `**/*.nut.ts`) — confirm the new files don't change how the existing suites are discovered or ordered. @@ -181,7 +181,7 @@ Spec §5.2's Non-Regression Checklist (zero-diff on `dev`) is checked at 5 point 1. **End of Phase 2** (after 2.8, before Phase 3 starts) — `git diff src/commands/ui-bundle/dev.ts messages/ui-bundle.dev.md src/config/types.ts` — confirms none of the core-logic implementation work in `upload.ts` accidentally touched `dev.ts` or its message file (easy mistake if copy-pasting patterns), and that the `types.ts` edit from Phase 1.1 is still purely additive. 2. **Immediately after Phase 3 Step 3.2** (snapshot regen) — `git diff command-snapshot.json` — highest-risk single step for corrupting the existing `ui-bundle:dev` array element; diff before proceeding to README/COMMANDS.md work, not after. -3. **Immediately after Phase 3 Step 3.4** (README/COMMANDS.md regen), before Phase 4 begins — `git diff README.md COMMANDS.md` — confirms the hand-edit insertion (3.3) and the `oclif readme` regeneration (3.4) are both purely additive, catching a bad line-number insertion or a clobbered `dev` section before it's buried under Phase 4's test-writing work. +3. **Immediately after Phase 3 Step 3.4** (README/COMMANDS.md regen), before Phase 4 begins — `git diff README.md COMMANDS.md` — confirms the hand-edit insertion (3.3) is purely additive to `README.md` and that `dev`'s existing `COMMANDS.md` section is untouched. Note the expected shape differs by file since `hidden = true`: `README.md` should show new `upload` content (hand-maintained, not suppressed by `hidden`); `COMMANDS.md` should show **zero diff** at all (the `oclif readme` regeneration in 3.4 correctly omits `upload` because it's hidden) — a nonzero `COMMANDS.md` diff here means the generator emitted a section it shouldn't have. 4. **End of Phase 4** (after 4.2/4.3 land) — run `yarn test:only` filtered to just `dev.test.ts` (or run the full unit suite and isolate `dev`'s results) and confirm identical pass/fail vs. the pre-change baseline recorded before Phase 1 started — catch a regression introduced by new shared-helper changes (4.2) before it's buried under Phase 5's full-suite run. 5. **Phase 5 Step 5.5** (final gate) — the complete file-by-file diff across every item in the spec §5.2 Non-Regression Checklist, plus its test-parity requirement via 5.3/5.4, plus the §2 Non-Goals Compliance Checklist verification. @@ -206,6 +206,6 @@ Spec §5.2's Non-Regression Checklist (zero-diff on `dev`) is checked at 5 point - `upload.test.ts` and `upload.nut.ts` (both tiers) green — including the exactly-one (neither/both) cases and the `--bundle-dir` compression path; Tier 2 confirmed to throw (not skip) when `TESTKIT_AUTH_URL` is unset. - All 5 non-regression checkpoints in §6 show zero diff / identical pass-fail at their respective phase boundaries (the sole allowed `package.json` diff aside), with the final Phase 5.5 diff being the authoritative last check. - `yarn fix-license`, `yarn lint`, `yarn build`, `yarn test`, `yarn test:nuts` all pass locally, mirroring `pre-commit`/`pre-push`/CI (`linux-unit-tests`/`windows-unit-tests`/`nuts` jobs) exactly — no hook bypass (`--no-verify`) used to get a commit through. -- `COMMANDS.md` and `command-snapshot.json` show only tool-generated diffs (new `upload` content, including the preview `state`, the `--bundle-dir` flag, and the `d` flagChar), zero hand-edits to generated regions; `command-snapshot.json` re-run through `npx prettier --write` so its formatting matches the repo's 2-space config. +- `command-snapshot.json` shows only a tool-generated diff (new `upload` element, including the preview `state`, the `--bundle-dir` flag, and the `d` flagChar — `hidden` doesn't filter the snapshot), zero hand-edits; re-run through `npx prettier --write` so its formatting matches the repo's 2-space config. `COMMANDS.md`, regenerated the same way, shows **zero diff** — `hidden = true` suppresses `upload` from `oclif readme`'s output entirely, so no new TOC line or section is expected there. - No inlined customer-facing output literals in `upload.ts` — all authored output resolves via `messages.getMessage()` from `messages/ui-bundle.upload.md` per spec §6.3, verified via the Phase 4 Step 4.1 grep checks (empty-dir/compression-failure/Failed-block literals all moved). - Open Question 1 is resolved (§1), and the AC6 transport question is resolved (§5 Risk Callouts row 3) — multipart `bundle` is the locked design per PR #118209. The remaining genuinely-open item to note in the PR description at Phase 5 Step 5.6 is: (1) `--use-salesforce-pages` has no server-side field (spec §2.4); and two external/upstream caveats: (a) PR #118209 is open/not-merged, so the lock is authoritative-intent, not yet live on the branch; (b) the server doesn't yet persist the bundle bytes (FFX_BLOB byte-write is a TODO). The exact SDR zip API (§5 Risk row 6) is resolved at implementation time; pre-signed-URL overlap is no longer relevant (§5 Risk Callouts row 5). diff --git a/.sdd/ui-bundle-upload/spec.md b/.sdd/ui-bundle-upload/spec.md index 7c980c6..5dce1c4 100644 --- a/.sdd/ui-bundle-upload/spec.md +++ b/.sdd/ui-bundle-upload/spec.md @@ -12,6 +12,8 @@ **Command state:** the command ships in developer-preview state — `public static readonly state = 'preview';` on the command class. `sf-plugins-core` therefore emits a runtime warning on every invocation and oclif prints a preview banner in `--help` (§2.4). +**Command visibility:** the command also declares `public static readonly hidden = true;`. This suppresses it from `--help`'s command listing and from topic help, and — since `hidden` is generated-tooling-aware — from `oclif readme`'s auto-generated `COMMANDS.md`. It remains directly invocable (`sf ui-bundle upload ...` still runs normally), still renders its own `--help` page when named directly, and still appears in `command-snapshot.json`, since the snapshot generator does not filter on `hidden` (§2.4). `README.md`'s `### sf ui-bundle upload` subsection is hand-maintained prose, not `oclif readme` output, so `hidden` has no automatic effect there — see §2.4's Command visibility note for how that asymmetry is handled. + **What it enables:** - Standard (non-admin) users can persist a React UI Bundle without the Metadata API, which requires admin-only `ModifyMetadata`/`ModifyAllData` at the framework level. @@ -47,6 +49,7 @@ 6. When `--bundle-dir` is supplied, compress the directory to a zip via `@salesforce/source-deploy-retrieve` before the `POST`; when `--zip-file` is supplied, send the file as-is (REQ-302). 7. When `--bundle-dir` is compressed, dotfiles and dot-directories (any path segment starting with `.` — e.g. `.env`, `.DS_Store`, `.git/`) are excluded from the resulting zip; `--zip-file` is unaffected (REQ-114). 8. Ship the command in developer-preview state (`state = 'preview'`) so both `--help` and runtime surface the preview warning. + 8a. Ship the command hidden (`hidden = true`) so it is omitted from `--help`'s command listing and from tool-generated `COMMANDS.md`, while remaining fully invocable, still rendering its own `--help` page when named directly, and remaining present in `command-snapshot.json`. `README.md`'s hand-written `upload` subsection is a deliberate exception — see §2.4. 9. The change is additive to the plugin's command surface — new `UiBundleUploadResult` type (REQ-113, 205), generated artifacts (`command-snapshot.json`, `COMMANDS.md` — REQ-202, 212), `README.md` section (REQ-209), and test fixtures (REQ-208) are all new or appended, with no existing `dev` command source modified. The one deliberate exception is `package.json`, which gains `@salesforce/source-deploy-retrieve` as a new runtime dependency (§2.4) — so the framing is additive-to-plugin plus one dependency addition, not strictly "nothing existing modified." 10. When `--bundle-dir` is compressed, symlinked files and symlinked directories are resolved to their target (followed, not skipped) during the recursive directory walk, so they appear in the resulting zip like any other file or directory; `--zip-file` is unaffected (REQ-115). 11. Provide an optional `--bundle-name` flag that maps to the `requestedName` field in the Connect API's `deployRequest` JSON part; when omitted, default to the base name of `--bundle-dir` or `--zip-file`, with any `.zip` extension stripped (case-insensitive), falling back to the unstripped filename if stripping leaves an empty string (REQ-116). @@ -110,10 +113,26 @@ - [ ] **117c.** `--api-version` omitted entirely (the flag's own default resolution kicks in, potentially resolving from the target-org's config, or to `undefined`) → the connection still resolves to some effective API version (org-config default or auto-negotiated), and that resolved value is checked against the floor unconditionally, the same as an explicit flag value. - [ ] **117d.** The resolved `flags['api-version']` value (which may be `undefined`) is passed into `flags['target-org'].getConnection(flags['api-version'])`; the floor check then reads `orgConnection.getApiVersion()` — the connection's own resolved value — rather than re-checking the raw flag input, regardless of whether the version was explicit or defaulted. +**AC11 — Command visibility** + +- [ ] **118a.** `sf ui-bundle --help` and `sf help ui-bundle` → the command listing omits `upload` (`hidden = true` suppresses it from topic/command listings), while `dev` still appears. +- [ ] **118b.** `sf ui-bundle upload --help`, invoked directly by name → still renders the full help page documented in §2.4's `--help` block below (`hidden` doesn't block direct invocation of help for a named command). +- [ ] **118c.** `sf ui-bundle upload -z --use-salesforce-pages -o ` → runs identically to a non-hidden command (`hidden` has no runtime/behavioral effect on `run()`); the developer-preview warning (AC per line 460 below) still emits. +- [ ] **118d.** `command-snapshot.json` → still contains the `ui-bundle:upload` element (§3.2); the snapshot generator does not filter on `hidden`. +- [ ] **118e.** `COMMANDS.md`, regenerated via `oclif readme` → contains no `upload` TOC entry and no `## \`sf ui-bundle upload\``section (the generator filters`!c.hidden`); `dev`'s entry is unaffected. + ### 2.4 CLI Command Contract **Command state:** `public static readonly state = 'preview';` on the command class. This marks the command as developer-preview, so oclif prints `This command is in preview.` in `--help` output and `sf-plugins-core` emits the runtime warning `⚠ This command is currently in developer preview. Developer preview commands will likely change before shipping, use at your own risk. Don't use developer preview commands in your scripts.` on every invocation. +**Command visibility:** `public static readonly hidden = true;` on the command class, alongside `state`. Effects, traced through oclif's own filtering (confirmed against `@oclif/core`/`oclif` source at implementation time): + +- `sf ui-bundle --help` and `sf help` (topic/command listings) omit `upload` — `@oclif/core`'s `Help` class filters `commands.filter((c) => this.opts.all || !c.hidden)`. +- `oclif readme` (the `version` script backing `COMMANDS.md` generation, plan Phase 3 Step 3.4) also filters `!c.hidden` when building its command list — so a hidden command's section and TOC entry are **not emitted** into `COMMANDS.md` on regeneration. +- `sf ui-bundle upload --help`, invoked directly against the specific command, still renders that command's own help page — `hidden` suppresses it from listings, not from being described when named explicitly. +- The command remains fully invocable (`sf ui-bundle upload -z ... --use-salesforce-pages -o ...` behaves identically to a non-hidden command) and still appears in `command-snapshot.json` — `@oclif/plugin-command-snapshot`'s `SnapshotCommand#commands` only filters dev-plugin commands and self-referencing aliases, not `hidden`. +- `README.md`'s `### sf ui-bundle upload` subsection (plan Phase 3 Step 3.3) is hand-maintained prose, not tool-generated, so `hidden` has no automatic effect on it — see §5.2's revised Non-Regression Checklist guidance below for how this asymmetry is handled. + | Flag | Char | Type | Required | Notes | | ------------------------ | ---- | ----------------------------------- | --------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `--zip-file` | `-z` | `Flags.file({ exists: true })` | exactly-one (with `--bundle-dir`) | Pre-built zip source, sent as-is. No client-side zip-content validation (REQ-112). Declares `exactlyOne: ['zip-file', 'bundle-dir']`; no longer a standalone `required: true` flag. | @@ -131,7 +150,7 @@ Global `--json` / `--flags-dir` inherited from `SfCommand`. -**`--help`:** +**`--help`** (only rendered when the command is named directly — `sf ui-bundle --help`/`sf help ui-bundle` omit `upload` entirely per AC11 118a, since `hidden` suppresses listing, not direct lookup): ``` This command is in preview. @@ -447,6 +466,7 @@ Status values (`Queued`/`InProgress`/`Succeeded`/`Failed`) match the server-side - [ ] `Failed` response (defensive) → human failure block and `--json` shape (§2.6). - [ ] Each CLI-side `SfError` name asserted: `UiBundleUploadValidationError` / `UiBundleUploadNetworkError` / `UiBundleUploadAuthError` / `UiBundleUploadApiVersionError`. - [ ] Preview-state warning emitted (`state = 'preview'`) — not suppressed under `--json`'s result payload. +- [ ] `upload` is omitted from `sf ui-bundle --help`'s command listing (`hidden = true`, AC11 118a); `sf ui-bundle upload --help`, invoked directly, still renders its full help page (AC11 118b). - [ ] No customer-facing output literal is inlined in `upload.ts` — all such output resolves via `messages.getMessage()` per §6.3. - [ ] Lint, build, and license-header checks clean on all new `.ts` files. @@ -457,7 +477,8 @@ Tiered like `dev.nut.ts` — Tier 1 (`dev.nut.ts:33-71`, no-auth flag-parse chec - [ ] Tier 1: flag-parse / validation cases run without auth — including neither/both of `--zip-file`/`--bundle-dir` (exactly-one) and missing `--use-salesforce-pages`. - [ ] Tier 2: real-org `POST` path returns and reports a `Queued` job id, for both the `--zip-file` and `--bundle-dir` (auto-compressed) sources. - [ ] Tier 2 confirmed to throw (not silently skip) when `TESTKIT_AUTH_URL` is unset. -- [ ] `command-snapshot.json` / `COMMANDS.md` show only tool-generated diffs — zero hand-edits. +- [ ] `command-snapshot.json` shows only a tool-generated diff (new `ui-bundle:upload` element, `hidden` has no effect on the snapshot) — zero hand-edits. +- [ ] `COMMANDS.md`, regenerated via `oclif readme`, shows **no new `upload` content** — no TOC entry, no `## \`sf ui-bundle upload\``section — since`hidden = true`suppresses it from the generator's command list (AC11 118e); confirm via`git diff COMMANDS.md` showing zero diff after regeneration. **Non-Regression Checklist** — adding `upload` must not touch the existing `dev` command. **Zero diff required** on: diff --git a/src/commands/ui-bundle/upload.ts b/src/commands/ui-bundle/upload.ts index e62bb81..94beed3 100644 --- a/src/commands/ui-bundle/upload.ts +++ b/src/commands/ui-bundle/upload.ts @@ -63,6 +63,7 @@ async function compressDirectory(dir: string): Promise { export default class UiBundleUpload extends SfCommand { public static readonly state = 'preview'; + public static readonly hidden = true; public static readonly summary = messages.getMessage('summary'); public static readonly description = messages.getMessage('description'); public static readonly examples = messages.getMessages('examples'); diff --git a/test/commands/ui-bundle/upload.test.ts b/test/commands/ui-bundle/upload.test.ts index afa7574..6b25338 100644 --- a/test/commands/ui-bundle/upload.test.ts +++ b/test/commands/ui-bundle/upload.test.ts @@ -145,6 +145,13 @@ describe('ui-bundle:upload command unit tests', () => { $$.restore(); }); + describe('command metadata', () => { + it('is hidden and marked preview', () => { + expect(UiBundleUpload.state).to.equal('preview'); + expect(UiBundleUpload.hidden).to.equal(true); + }); + }); + /* ------------------------------------------------------------------ * * Flag-validation-only cases — fail during this.parse(), before any * * org resolution or network interaction. No connection stubbing. *