@git.zone/cli 4.0.0 → 6.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/.smartconfig.json +0 -1
  2. package/assets/templates/ci_default/.gitea/workflows/default_tags.yaml +0 -21
  3. package/assets/templates/ci_default_gitlab/.gitlab-ci.yml +0 -13
  4. package/assets/templates/ci_default_private/.gitea/workflows/default_tags.yaml +0 -21
  5. package/assets/templates/ci_default_private_gitlab/.gitlab-ci.yml +0 -13
  6. package/dist_ts/00_commitinfo_data.js +1 -1
  7. package/dist_ts/helpers.climode.js +31 -2
  8. package/dist_ts/helpers.workflow.d.ts +10 -2
  9. package/dist_ts/helpers.workflow.js +102 -19
  10. package/dist_ts/mod_commit/mod.helpers.d.ts +3 -3
  11. package/dist_ts/mod_commit/mod.helpers.js +10 -10
  12. package/dist_ts/mod_config/index.js +22 -23
  13. package/dist_ts/mod_release/classes.releasejournal.d.ts +78 -0
  14. package/dist_ts/mod_release/classes.releasejournal.js +511 -0
  15. package/dist_ts/mod_release/helpers.npmartifact.d.ts +32 -0
  16. package/dist_ts/mod_release/helpers.npmartifact.js +358 -0
  17. package/dist_ts/mod_release/helpers.releasebranch.d.ts +47 -0
  18. package/dist_ts/mod_release/helpers.releasebranch.js +627 -0
  19. package/dist_ts/mod_release/helpers.releasepublication.d.ts +24 -0
  20. package/dist_ts/mod_release/helpers.releasepublication.js +293 -0
  21. package/dist_ts/mod_release/index.d.ts +1 -1
  22. package/dist_ts/mod_release/index.js +539 -209
  23. package/package.json +1 -1
  24. package/readme.hints.md +47 -1
  25. package/readme.md +81 -38
  26. package/ts/00_commitinfo_data.ts +1 -1
  27. package/ts/helpers.climode.ts +34 -1
  28. package/ts/helpers.workflow.ts +155 -19
  29. package/ts/mod_commit/mod.helpers.ts +12 -8
  30. package/ts/mod_config/index.ts +21 -22
  31. package/ts/mod_release/classes.releasejournal.ts +740 -0
  32. package/ts/mod_release/helpers.npmartifact.ts +553 -0
  33. package/ts/mod_release/helpers.releasebranch.ts +1344 -0
  34. package/ts/mod_release/helpers.releasepublication.ts +641 -0
  35. package/ts/mod_release/index.ts +906 -262
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@git.zone/cli",
3
3
  "private": false,
4
- "version": "4.0.0",
4
+ "version": "6.0.0",
5
5
  "description": "A comprehensive CLI tool for enhancing and managing local development workflows with gitzone utilities, focusing on project setup, version control, code formatting, and template management.",
6
6
  "main": "dist_ts/index.js",
7
7
  "typings": "dist_ts/index.d.ts",
package/readme.hints.md CHANGED
@@ -104,11 +104,57 @@ The commit module no longer bumps versions, creates tags, or publishes packages.
104
104
  - Release publishing is target-based under `@git.zone/cli.release.targets`.
105
105
  - NPM registries only live under `@git.zone/cli.release.targets.npm.registries`.
106
106
 
107
+ ### Exact artifact release journal v1
108
+
109
+ Every release that reaches final Git/npm publication atomically installs
110
+ canonical state below the Git common directory at
111
+ `gitzone/releases/v1/v<version>/`. npm releases contain exactly one
112
+ `package.tgz`; the journal binds that file's package identity, byte length,
113
+ SHA-1, SHA-256, SHA-512 integrity, release commit, annotated tag object, hashed
114
+ Git destination, registry list, and target attempts. The state is deliberately
115
+ outside the worktree so build cleanliness checks do not conflict with durable
116
+ recovery.
117
+
118
+ Journal writes are fsynced same-directory replacements under the shared
119
+ atomic-mkdir lock and require the caller's exact prior revision. Transactions
120
+ may update target attempts and completion state but cannot alter release,
121
+ artifact, destination, or target identity. A malformed, noncanonical, future,
122
+ or manually reformatted journal fails closed.
123
+
124
+ Selected Git publication requires a canonical remote name and validates its
125
+ single resolved push URL before remote contact, including during planning.
126
+ Embedded HTTP(S) credentials, passwords, parameters, and unsupported protocols
127
+ fail closed. Git publication then probes the exact branch, annotated tag
128
+ object, and peeled tag commit before and after an atomic leased push. npm publication always uses the
129
+ stored tarball and verifies each public registry anonymously with redirects
130
+ disabled. Verification covers metadata identity, integrity and shasum, bounded
131
+ downloaded bytes, SHA-256, and the `latest` dist-tag.
132
+
133
+ `gitzone release inspect [version]` is read-only. `gitzone release resume
134
+ <version>` rejects fresh-release overrides and never rebuilds or repacks. It
135
+ also rejects changed journaled configuration. A target left in `publishing`
136
+ state first reconciles exact remote
137
+ state. If the remote still looks absent, recovery requires the exact recorded
138
+ attempt ID and independent proof that its process stopped; no timeout or PID
139
+ guess reclaims publication authority.
140
+
141
+ Schema v1 recovery begins only after atomic journal installation. Local
142
+ version, changelog, commit, tag, build, and pack failures before that point, as
143
+ well as the pre-release `--merge` integration push, require explicit operator
144
+ reconciliation; they are not represented as publication attempts.
145
+
146
+ Schema v1 supports Git and public npm only and requires resolved pnpm 11.21.0.
147
+ Private npm cannot be anonymously verified. Docker releases remain blocked
148
+ until tSDocker returns structured immutable digest qualification and promotion
149
+ results. Legacy tag-triggered `npmci npm publish` jobs must be removed because
150
+ they bypass journal ownership.
151
+
107
152
  **Benefits:**
108
153
 
109
154
  - Commit is safer and has no publishing side effects.
110
155
  - Multiple source commits can accumulate into one release via `Pending`.
111
- - Per-artifact release results can distinguish published, already-published, skipped, and failed targets.
156
+ - Durable target states distinguish pending, publishing, verified, failed,
157
+ conflicting, and skipped publication while retaining exact attempt evidence.
112
158
 
113
159
  ### Auto-Accept Flag for Commits
114
160
 
package/readme.md CHANGED
@@ -133,25 +133,31 @@ Commit flags:
133
133
 
134
134
  ## Release Workflow
135
135
 
136
- `gitzone release` performs the release core once, then publishes to configured targets.
136
+ `gitzone release` releases from `main`. Running it on another branch fails before release metadata or ref mutation unless the bare `--merge` flag is explicitly supplied.
137
+
138
+ `--merge` is intentionally strict: the source and existing `main` worktree must be clean, the source must be linearly rebased onto current remote `main`, and the Git target must push both the branch and tag. GitZone pins the single configured push URL, compare-and-swap fast-forwards local `main`, updates its verified worktree, and lease-pushes that exact source commit before creating release metadata. It rejects diverged or merged history, local replacement refs or grafts, stale worktrees, incomplete shallow history, changed destinations, and remote races. If the pre-release push fails, GitZone restores local `main` only when doing so cannot overwrite concurrent work. This plumbing-level integration intentionally does not run merge hooks or write `ORIG_HEAD`.
139
+
140
+ `gitzone release` performs the release core once, then publishes to configured targets. An active Git target must succeed and be verified before npm publication can start.
137
141
 
138
142
  The release core is not configurable plumbing. It always follows the same professional release transaction:
139
143
 
140
- 1. Run configured preflight checks.
141
- 2. Read `changelog.md` `## Pending` entries.
142
- 3. Infer or accept a semver bump.
143
- 4. Update version files and baked commit info.
144
- 5. Move pending changelog entries into the new version section.
145
- 6. Create the local release commit.
146
- 7. Create the local release tag.
144
+ 1. Verify the release branch, clean state, and `main` worktree ownership.
145
+ 2. Read `changelog.md` `## Pending` entries and infer or accept a semver bump.
146
+ 3. Run configured tests.
147
+ 4. With `--merge`, fast-forward and lease-push `main` before release metadata is created.
148
+ 5. Update version files and baked commit info.
149
+ 6. Move pending changelog entries into the new version section.
150
+ 7. Create the local release commit and tag on `main`.
151
+ 8. Run the configured release build, require a clean tree, and revalidate the release checkout.
152
+ 9. When npm is selected, pack it once into an exact tarball. Atomically install a durable journal for Git-only and npm releases before final Git release-ref or npm publication.
147
153
 
148
154
  Targets decide what happens after that:
149
155
 
150
156
  | Target | What it does |
151
157
  | --- | --- |
152
- | `git` | Pushes the release commit and tags, often triggering remote CI release builds |
153
- | `npm` | Publishes the package to configured npm registries |
154
- | `docker` | Delegates container builds and pushes to `tsdocker` |
158
+ | `git` | Atomically pushes the exact `main` release commit and new tag, often triggering remote CI release builds |
159
+ | `npm` | Publishes the same journaled tarball to every configured registry and verifies its bytes anonymously |
160
+ | `docker` | Fails closed in journal schema v1 until tSDocker provides structured digest-qualified publication results |
155
161
 
156
162
  ```bash
157
163
  # Preview the resolved release plan
@@ -160,13 +166,13 @@ gitzone release --plan
160
166
  # Release to configured targets
161
167
  gitzone release
162
168
 
169
+ # From a clean feature branch already rebased onto current main
170
+ gitzone release --merge
171
+
163
172
  # Release only to npm
164
173
  gitzone release --target npm
165
174
 
166
- # Release only to git and Docker
167
- gitzone release --target git,docker
168
-
169
- # Skip package/container publishing and keep only git target
175
+ # Remove npm and Docker from the resolved targets; Git remains only if configured
170
176
  gitzone release --no-publish
171
177
 
172
178
  # Override inferred semver level
@@ -179,15 +185,58 @@ Release flags:
179
185
  | --- | --- |
180
186
  | `-y`, `--yes` | Run without interactive confirmation |
181
187
  | `-t`, `--test` | Enable preflight tests |
182
- | `-b`, `--build` | Enable preflight build |
183
- | `-p`, `--push` | Enable the `git` target |
184
- | `--target <csv>` | Use only selected targets, e.g. `git,npm` |
185
- | `--npm` | Enable the `npm` target |
186
- | `--docker` | Enable the `docker` target |
187
- | `--no-publish` | Keep release core and `git` target only |
188
- | `--no-build` | Disable preflight build for this run |
188
+ | `-b`, `--build` | Enable the release build after local release metadata is created |
189
+ | `-p`, `--push` | Explicitly select the `git` target; combine with other target flags as needed |
190
+ | `--target <csv>` | Replace configured targets with a non-empty comma-separated subset of `git`, `npm`, and `docker`; empty or unknown elements fail |
191
+ | `--npm` | Explicitly select the `npm` target; combine with other target flags as needed |
192
+ | `--docker` | Explicitly select `docker`; planning is supported, but a mutating schema v1 release fails before integration or release metadata |
193
+ | `--no-publish` | Remove npm and Docker from the resolved target set without implicitly enabling Git |
194
+ | `--no-build` | Disable the post-metadata release build for this run |
195
+ | `--merge` | Fast-forward and lease-push a cleanly rebased feature branch into `main`, then release from `main` |
189
196
  | `--major`, `--minor`, `--patch` | Override inferred semver level |
190
- | `--plan` | Show resolved workflow only |
197
+ | `--plan` | Show the resolved workflow without fetching or mutating refs, files, the index, or worktrees |
198
+ | `inspect [version]` | Read one or all durable release journals; add `--json` for machine-readable output |
199
+ | `resume <version>` | Resume only journaled Git/npm publication; fresh-release overrides, including negated forms, are rejected, and `--json` remains inspect-only |
200
+ | `--recover-attempt <id>` | Recover the exact 32-character lowercase hexadecimal `attempt.id` shown by `inspect --json`, after independently proving its publisher stopped |
201
+
202
+ ### Exact npm artifacts and release journals
203
+
204
+ Journal schema v1 supports Git and public npm publication. Fresh and resumed npm publication requires the resolved `pnpm` binary to be exactly the version pinned by this package (`11.21.0`). Registries must be unique canonical credential-free HTTP(S) URLs, and `release.targets.npm.accessLevel` must be `public` so verification does not depend on publisher credentials.
205
+
206
+ A selected Git target requires a canonical remote name and exactly one resolved push URL. Before any remote contact, including during `--plan`, GitZone rejects HTTP(S) usernames or passwords, embedded passwords, query strings, fragments, and unsupported protocols. Canonical local paths and SSH destinations may retain their required SSH username.
207
+
208
+ Every release that reaches final Git/npm publication stores `journal.json` under the repository's Git common directory. npm releases additionally run `pnpm pack` once and store the resulting `package.tgz` beside it:
209
+
210
+ ```text
211
+ <git-common-dir>/gitzone/releases/v1/v<version>/package.tgz
212
+ <git-common-dir>/gitzone/releases/v1/v<version>/journal.json
213
+ ```
214
+
215
+ The canonical journal binds the release commit, annotated tag object, hashed Git destination, npm registries, per-target attempt states, and, when npm is selected, the package identity, tarball size, SHA-1, SHA-256, and SHA-512 integrity. Journal updates use revision compare-and-swap under an interprocess lock. Release identity and destinations cannot change after journal creation. Inspect and resume reject malformed, future, noncanonical, duplicate-key, or merely reformatted journal JSON rather than repairing or normalizing it.
216
+
217
+ Packing must leave the release tree clean. Each registry receives that exact stored tarball through `pnpm publish <tarball> --ignore-scripts`, so publish lifecycle scripts do not run. Success is recorded only after anonymous no-redirect probes verify version metadata, SHA-1 and integrity fields, the downloaded tarball bytes, and the `latest` dist-tag. A matching pre-existing version follows `release.targets.npm.alreadyPublished`; conflicting bytes always stop the release, while transient responses and dist-tag propagation remain retryable.
218
+
219
+ Independent tag-triggered npm publishers are incompatible with this transaction. GitZone rejects a release while `.gitea/workflows/default_tags.yaml` or `.gitlab-ci.yml` still contains the legacy `npmci npm publish` command. Apply the v6 Gitea workflow template or remove the legacy GitLab job and commit that change first.
220
+
221
+ Inspect and resume without regenerating release identity:
222
+
223
+ ```bash
224
+ # List journals or inspect one exact version
225
+ gitzone release inspect
226
+ gitzone release inspect 6.0.0 --json
227
+
228
+ # Reconcile remote state, then continue only unfinished targets
229
+ gitzone release resume 6.0.0 -y
230
+
231
+ # Only after proving the recorded publisher process has stopped
232
+ gitzone release resume 6.0.0 -y --recover-attempt="$ATTEMPT_ID"
233
+ ```
234
+
235
+ Resume requires the local `main` commit, annotated release tag, and each journaled destination setting to remain exact. The Git remote must still match when Git is journaled; npm registry, access, and already-published settings must still match when npm is journaled. Non-journaled target configuration is ignored, so releases created with a target subset remain resumable. Fresh-release overrides such as targets, integration, build, test, and version flags are rejected even in negated forms such as `--no-git`, `--no-publish`, and `--no-build`; JSON output is available only through `inspect --json`. Resume probes remote state before acting. Exact remote state is accepted without republishing; positive byte, metadata, or ref conflicts fail closed, while absent, pending, inconclusive, and dist-tag propagation results do not overwrite a previously verified state. A target left in `publishing` state is never reclaimed from an absent remote result unless the exact recorded 32-character lowercase hexadecimal `attempt.id` from `git.attempt` or an npm registry attempt is supplied.
236
+
237
+ Schema v1 resumes only final Git release-ref and npm publication after the journal is atomically installed. It does not recover local version, changelog, commit, tag, build, or pack failures that occur before installation, nor the pre-release `--merge` integration push. Those failures stop before the final release commit/tag or npm artifact is published and require explicit operator reconciliation.
238
+
239
+ Docker target configuration remains available for planning and future journal schemas, but a mutating v6 release with an active Docker target stops before release integration or metadata changes. This restriction remains until tSDocker owns structured immutable digest qualification and promotion.
191
240
 
192
241
  ## Standard Changelog
193
242
 
@@ -209,7 +258,7 @@ The standard buckets are `Breaking Changes`, `Features`, `Fixes`, `Documentation
209
258
 
210
259
  ## Configuration
211
260
 
212
- CLI workflow config lives under `@git.zone/cli` in `.smartconfig.json`. Docker build and registry behavior lives under `@git.zone/tsdocker` and is used by the Docker release target.
261
+ CLI workflow config lives under `@git.zone/cli` in `.smartconfig.json`. Dormant Docker build and registry settings remain under `@git.zone/tsdocker` for the later journal schema that will restore Docker publication.
213
262
 
214
263
  ```json
215
264
  {
@@ -223,7 +272,6 @@ CLI workflow config lives under `@git.zone/cli` in `.smartconfig.json`. Docker b
223
272
  "release": {
224
273
  "confirmation": "prompt",
225
274
  "preflight": {
226
- "requireCleanTree": true,
227
275
  "test": false,
228
276
  "build": true
229
277
  },
@@ -241,7 +289,7 @@ CLI workflow config lives under `@git.zone/cli` in `.smartconfig.json`. Docker b
241
289
  "alreadyPublished": "success"
242
290
  },
243
291
  "docker": {
244
- "enabled": true,
292
+ "enabled": false,
245
293
  "engine": "tsdocker",
246
294
  "registry": "registry.gitlab.com",
247
295
  "buildRegistries": ["registry.gitlab.com"],
@@ -276,23 +324,18 @@ values should be hosts without `http://` or `https://`:
276
324
  @git.zone/tsdocker.registries
277
325
  ```
278
326
 
279
- The Docker release target runs the project's pinned `@git.zone/tsdocker` through
280
- `pnpm exec`; install it in each Docker-producing project:
327
+ Docker release configuration uses the project's pinned `@git.zone/tsdocker`
328
+ through `pnpm exec`. Install it in each Docker-producing project, but keep the
329
+ target disabled while release journal schema v1 is active:
281
330
 
282
331
  ```bash
283
332
  pnpm add --save-dev @git.zone/tsdocker
284
333
  ```
285
334
 
286
- Set `release.targets.docker.registry` to select one destination already declared
287
- by the tSDocker config when a release must not publish to every configured
288
- registry.
289
- Set `release.targets.docker.buildRegistries` when the build should authenticate
290
- only the configured registries used by private base images; an empty list keeps
291
- the compatibility default of authenticating every configured registry. Bounded
292
- build authentication requires project-local `@git.zone/tsdocker` 3.3.0 or newer.
293
- Set `release.targets.docker.test` to `true` to require every selected image test
294
- to pass before destination publication. This requires project-local
295
- `@git.zone/tsdocker` 3.4.0 or newer.
335
+ The dormant `release.targets.docker.registry`, `buildRegistries`, and `test`
336
+ settings describe the intended tSDocker destination, bounded base-image
337
+ authentication, and pre-publication image tests. They do not execute while
338
+ journal schema v1 is active.
296
339
 
297
340
  Useful config commands:
298
341
 
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@git.zone/cli',
6
- version: '4.0.0',
6
+ version: '6.0.0',
7
7
  description: 'A comprehensive CLI tool for enhancing and managing local development workflows with gitzone utilities, focusing on project setup, version control, code formatting, and template management.'
8
8
  }
@@ -24,6 +24,29 @@ interface ICliConfigSettings {
24
24
 
25
25
  type TArgSource = Record<string, any> & { _?: string[] };
26
26
 
27
+ const booleanLongFlags = new Set([
28
+ "agent",
29
+ "allow-breaking",
30
+ "allow-empty",
31
+ "build",
32
+ "check-updates",
33
+ "docker",
34
+ "help",
35
+ "interactive",
36
+ "json",
37
+ "major",
38
+ "merge",
39
+ "minor",
40
+ "npm",
41
+ "patch",
42
+ "plain",
43
+ "plan",
44
+ "push",
45
+ "quiet",
46
+ "test",
47
+ "yes",
48
+ ]);
49
+
27
50
  const camelCase = (value: string): string => {
28
51
  return value.replace(/-([a-z])/g, (_match, group: string) =>
29
52
  group.toUpperCase(),
@@ -55,13 +78,23 @@ const parseRawArgv = (argv: string[]): TArgSource => {
55
78
 
56
79
  if (currentArg.startsWith("--")) {
57
80
  const withoutPrefix = currentArg.slice(2);
58
- const [rawKey, inlineValue] = withoutPrefix.split("=", 2);
81
+ const equalsIndex = withoutPrefix.indexOf("=");
82
+ const rawKey =
83
+ equalsIndex === -1 ? withoutPrefix : withoutPrefix.slice(0, equalsIndex);
84
+ const inlineValue =
85
+ equalsIndex === -1 ? undefined : withoutPrefix.slice(equalsIndex + 1);
59
86
  if (inlineValue !== undefined) {
60
87
  parsedArgv[rawKey] = inlineValue;
61
88
  parsedArgv[camelCase(rawKey)] = inlineValue;
62
89
  continue;
63
90
  }
64
91
 
92
+ if (booleanLongFlags.has(rawKey)) {
93
+ parsedArgv[rawKey] = true;
94
+ parsedArgv[camelCase(rawKey)] = true;
95
+ continue;
96
+ }
97
+
65
98
  const nextArg = argv[i + 1];
66
99
  if (nextArg && !nextArg.startsWith("-")) {
67
100
  parsedArgv[rawKey] = nextArg;
@@ -70,7 +70,6 @@ export interface IReleaseWorkflowConfig {
70
70
  source?: "pendingChangelog" | "manual";
71
71
  };
72
72
  preflight?: {
73
- requireCleanTree?: boolean;
74
73
  test?: boolean;
75
74
  build?: boolean;
76
75
  testCommand?: string;
@@ -101,7 +100,6 @@ export interface IResolvedReleaseWorkflow {
101
100
  confirmation: TConfirmationMode;
102
101
  plan: string[];
103
102
  targets: TReleaseTarget[];
104
- requireCleanTree: boolean;
105
103
  runTests: boolean;
106
104
  runBuild: boolean;
107
105
  testCommand: string;
@@ -129,6 +127,13 @@ export interface IResolvedReleaseWorkflow {
129
127
  dockerNoBuild: boolean;
130
128
  }
131
129
 
130
+ export interface IResolvedReleaseResumeConfiguration {
131
+ gitRemote: string;
132
+ npmRegistries: string[];
133
+ npmAccessLevel: "public" | "private";
134
+ npmAlreadyPublished: "success" | "error";
135
+ }
136
+
132
137
  interface ICliWorkflowConfig {
133
138
  commit?: ICommitWorkflowConfig;
134
139
  release?: IReleaseWorkflowConfig;
@@ -202,6 +207,22 @@ export const resolveDockerTestSetting = (valueArg: unknown): boolean => {
202
207
  return valueArg;
203
208
  };
204
209
 
210
+ const resolveGitPushSetting = (
211
+ valueArg: unknown,
212
+ settingArg: "pushBranch" | "pushTags",
213
+ gitEnabledArg: boolean,
214
+ ): boolean => {
215
+ if (!gitEnabledArg) {
216
+ return valueArg === undefined ? true : valueArg === true;
217
+ }
218
+ if (valueArg !== undefined && valueArg !== true) {
219
+ throw new Error(
220
+ `release.targets.git.${settingArg} must be true when the Git target is enabled`,
221
+ );
222
+ }
223
+ return true;
224
+ };
225
+
205
226
  const isStringRecord = (valueArg: unknown): valueArg is Record<string, unknown> => {
206
227
  return typeof valueArg === "object" && valueArg !== null && !Array.isArray(valueArg);
207
228
  };
@@ -325,12 +346,31 @@ const normalizeCommitSteps = (rawSteps: TCommitStep[]): TCommitStep[] => {
325
346
 
326
347
  const getTargetOverride = (argvArg: any): TReleaseTarget[] | undefined => {
327
348
  const validTargets: TReleaseTarget[] = ["git", "npm", "docker"];
328
- const rawTargets = argvArg.target || argvArg.targets;
329
- if (typeof rawTargets === "string") {
330
- return rawTargets
349
+ const targetKey = ["target", "targets"].find((keyArg) =>
350
+ Object.prototype.hasOwnProperty.call(argvArg, keyArg),
351
+ );
352
+ if (targetKey) {
353
+ const rawTargets = argvArg[targetKey];
354
+ if (typeof rawTargets !== "string" || !rawTargets.trim()) {
355
+ throw new Error(
356
+ "--target requires a non-empty comma-separated list of release targets.",
357
+ );
358
+ }
359
+ const requestedTargets = rawTargets
331
360
  .split(",")
332
- .map((target) => target.trim())
333
- .filter((target): target is TReleaseTarget => validTargets.includes(target as TReleaseTarget));
361
+ .map((target) => target.trim());
362
+ if (requestedTargets.some((target) => !target)) {
363
+ throw new Error(
364
+ "--target requires a non-empty comma-separated list of release targets.",
365
+ );
366
+ }
367
+ const invalidTargets = requestedTargets.filter(
368
+ (target) => !validTargets.includes(target as TReleaseTarget),
369
+ );
370
+ if (invalidTargets.length > 0) {
371
+ throw new Error(`Unknown release target: ${invalidTargets.join(", ")}`);
372
+ }
373
+ return requestedTargets as TReleaseTarget[];
334
374
  }
335
375
 
336
376
  const targets: TReleaseTarget[] = [];
@@ -340,17 +380,23 @@ const getTargetOverride = (argvArg: any): TReleaseTarget[] | undefined => {
340
380
  return targets.length > 0 ? targets : undefined;
341
381
  };
342
382
 
383
+ const releaseTargetOrder: TReleaseTarget[] = ["git", "npm", "docker"];
384
+
343
385
  const buildReleasePlan = (options: {
344
- requireCleanTree: boolean;
386
+ mergeRequested: boolean;
345
387
  runTests: boolean;
346
388
  runBuild: boolean;
347
389
  targets: TReleaseTarget[];
348
390
  }): string[] => {
349
- const plan: string[] = [];
350
- if (options.requireCleanTree) plan.push("preflight.cleanTree");
391
+ const plan: string[] = ["preflight.branch", "preflight.cleanTree"];
351
392
  if (options.runTests) plan.push("preflight.test");
393
+ if (options.mergeRequested) {
394
+ plan.push("integration.main", "integration.pushMain");
395
+ }
352
396
  plan.push("core.version", "core.changelog", "core.commit", "core.tag");
353
397
  if (options.runBuild) plan.push("core.build");
398
+ if (options.targets.includes("npm")) plan.push("core.packNpmArtifact");
399
+ plan.push("core.writeReleaseJournal");
354
400
  for (const target of options.targets) {
355
401
  plan.push(`target.${target}`);
356
402
  }
@@ -417,10 +463,23 @@ export const resolveReleaseWorkflow = async (argvArg: any): Promise<IResolvedRel
417
463
  const gitConfig = targetConfig.git || {};
418
464
  const npmConfig = targetConfig.npm || {};
419
465
  const dockerConfig = targetConfig.docker || {};
420
- const npmRegistries = (npmConfig.registries || []).map(normalizeRegistryUrl);
421
- const npmEnabled = npmConfig.enabled ?? npmRegistries.length > 0;
466
+ const rawNpmRegistries = npmConfig.registries;
467
+ const npmRegistryConfigurationPresent = Array.isArray(rawNpmRegistries)
468
+ ? rawNpmRegistries.length > 0
469
+ : rawNpmRegistries !== undefined;
470
+ const npmEnabled = npmConfig.enabled ?? npmRegistryConfigurationPresent;
422
471
  const gitEnabled = gitConfig.enabled ?? true;
423
472
  const dockerEnabled = dockerConfig.enabled ?? false;
473
+ const pushBranch = resolveGitPushSetting(
474
+ gitConfig.pushBranch,
475
+ "pushBranch",
476
+ gitEnabled,
477
+ );
478
+ const pushTags = resolveGitPushSetting(
479
+ gitConfig.pushTags,
480
+ "pushTags",
481
+ gitEnabled,
482
+ );
424
483
 
425
484
  let confirmation = normalizeConfirmation(releaseConfig.confirmation, "prompt");
426
485
  if (argvArg.plan) {
@@ -429,14 +488,12 @@ export const resolveReleaseWorkflow = async (argvArg: any): Promise<IResolvedRel
429
488
  confirmation = "auto";
430
489
  }
431
490
 
432
- let requireCleanTree = releaseConfig.preflight?.requireCleanTree ?? true;
433
491
  let runTests = releaseConfig.preflight?.test ?? false;
434
492
  let runBuild = releaseConfig.preflight?.build ?? true;
435
493
  if (argvArg.t || argvArg.test) runTests = true;
436
494
  if (argvArg.b || argvArg.build) runBuild = true;
437
495
  if (isDisabled(argvArg, "test")) runTests = false;
438
496
  if (isDisabled(argvArg, "build")) runBuild = false;
439
- if (isDisabled(argvArg, "preflight")) requireCleanTree = false;
440
497
 
441
498
  const configuredTargets: TReleaseTarget[] = [];
442
499
  if (gitEnabled) configuredTargets.push("git");
@@ -449,13 +506,32 @@ export const resolveReleaseWorkflow = async (argvArg: any): Promise<IResolvedRel
449
506
  if (isDisabled(argvArg, "publish")) {
450
507
  targets = targets.filter((target) => target === "git");
451
508
  }
452
- targets = unique(targets);
509
+ const selectedTargets = unique(targets);
510
+ targets = releaseTargetOrder.filter((target) =>
511
+ selectedTargets.includes(target),
512
+ );
513
+ let npmRegistries: string[] = [];
514
+ if (targets.includes("npm")) {
515
+ if (
516
+ rawNpmRegistries !== undefined &&
517
+ (!Array.isArray(rawNpmRegistries) ||
518
+ rawNpmRegistries.some((registryArg) => typeof registryArg !== "string"))
519
+ ) {
520
+ throw new Error("release.targets.npm.registries must be an array of strings");
521
+ }
522
+ npmRegistries = (rawNpmRegistries || []).map(normalizeRegistryUrl);
523
+ if (new Set(npmRegistries).size !== npmRegistries.length) {
524
+ throw new Error(
525
+ "release.targets.npm.registries must be unique after normalization",
526
+ );
527
+ }
528
+ }
529
+ const mergeRequested = argvArg.merge === true;
453
530
 
454
531
  return {
455
532
  confirmation,
456
- plan: buildReleasePlan({ requireCleanTree, runTests, runBuild, targets }),
533
+ plan: buildReleasePlan({ mergeRequested, runTests, runBuild, targets }),
457
534
  targets,
458
- requireCleanTree,
459
535
  runTests,
460
536
  runBuild,
461
537
  testCommand: releaseConfig.preflight?.testCommand || "pnpm test",
@@ -465,8 +541,8 @@ export const resolveReleaseWorkflow = async (argvArg: any): Promise<IResolvedRel
465
541
  changelogVersionHeading: "## {{date}} - {{version}}",
466
542
  gitEnabled,
467
543
  gitRemote: gitConfig.remote || "origin",
468
- pushBranch: gitConfig.pushBranch ?? true,
469
- pushTags: gitConfig.pushTags ?? true,
544
+ pushBranch,
545
+ pushTags,
470
546
  npmEnabled,
471
547
  npmRegistries,
472
548
  npmAccessLevel: npmConfig.accessLevel || "public",
@@ -492,3 +568,63 @@ export const resolveReleaseWorkflow = async (argvArg: any): Promise<IResolvedRel
492
568
  dockerNoBuild: dockerConfig.noBuild ?? false,
493
569
  };
494
570
  };
571
+
572
+ export const resolveReleaseResumeConfiguration = async (optionsArg: {
573
+ git: boolean;
574
+ npm: boolean;
575
+ }): Promise<IResolvedReleaseResumeConfiguration> => {
576
+ const cliConfig = await readCliWorkflowConfig();
577
+ const releaseConfig = cliConfig.release || {};
578
+ const targetConfig = releaseConfig.targets || {};
579
+ const gitConfig = targetConfig.git || {};
580
+ const npmConfig = targetConfig.npm || {};
581
+
582
+ const gitRemote = optionsArg.git ? gitConfig.remote || "origin" : "origin";
583
+ if (typeof gitRemote !== "string" || !gitRemote.trim()) {
584
+ throw new Error("release.targets.git.remote must be a non-empty string");
585
+ }
586
+
587
+ let npmRegistries: string[] = [];
588
+ let npmAccessLevel: "public" | "private" = "public";
589
+ let npmAlreadyPublished: "success" | "error" = "success";
590
+ if (optionsArg.npm) {
591
+ if (
592
+ npmConfig.registries !== undefined &&
593
+ (!Array.isArray(npmConfig.registries) ||
594
+ npmConfig.registries.some((registryArg) => typeof registryArg !== "string"))
595
+ ) {
596
+ throw new Error("release.targets.npm.registries must be an array of strings");
597
+ }
598
+ npmRegistries = (npmConfig.registries || []).map(normalizeRegistryUrl);
599
+ if (new Set(npmRegistries).size !== npmRegistries.length) {
600
+ throw new Error(
601
+ "release.targets.npm.registries must be unique after normalization",
602
+ );
603
+ }
604
+ if (
605
+ npmConfig.accessLevel !== undefined &&
606
+ npmConfig.accessLevel !== "public" &&
607
+ npmConfig.accessLevel !== "private"
608
+ ) {
609
+ throw new Error("release.targets.npm.accessLevel must be public or private");
610
+ }
611
+ if (
612
+ npmConfig.alreadyPublished !== undefined &&
613
+ npmConfig.alreadyPublished !== "success" &&
614
+ npmConfig.alreadyPublished !== "error"
615
+ ) {
616
+ throw new Error(
617
+ "release.targets.npm.alreadyPublished must be success or error",
618
+ );
619
+ }
620
+ npmAccessLevel = npmConfig.accessLevel || "public";
621
+ npmAlreadyPublished = npmConfig.alreadyPublished || "success";
622
+ }
623
+
624
+ return {
625
+ gitRemote,
626
+ npmRegistries,
627
+ npmAccessLevel,
628
+ npmAlreadyPublished,
629
+ };
630
+ };
@@ -37,9 +37,9 @@ export async function detectCurrentBranch(): Promise<string> {
37
37
  * Detects the project type based on presence of package.json and/or deno.json
38
38
  * @returns The project type
39
39
  */
40
- export async function detectProjectType(): Promise<ProjectType> {
41
- const packageJsonPath = plugins.path.join(paths.cwd, 'package.json');
42
- const denoJsonPath = plugins.path.join(paths.cwd, 'deno.json');
40
+ export async function detectProjectType(cwdArg = paths.cwd): Promise<ProjectType> {
41
+ const packageJsonPath = plugins.path.join(cwdArg, 'package.json');
42
+ const denoJsonPath = plugins.path.join(cwdArg, 'deno.json');
43
43
 
44
44
  const hasPackageJson = await plugins.smartfs.file(packageJsonPath).exists();
45
45
  const hasDenoJson = await plugins.smartfs.file(denoJsonPath).exists();
@@ -96,9 +96,12 @@ export function calculateNewVersion(currentVersion: string, versionType: Version
96
96
  * @param projectType The project type to determine which file to read
97
97
  * @returns The current version string
98
98
  */
99
- export async function readCurrentVersion(projectType: ProjectType): Promise<string> {
99
+ export async function readCurrentVersion(
100
+ projectType: ProjectType,
101
+ cwdArg = paths.cwd,
102
+ ): Promise<string> {
100
103
  if (projectType === 'npm' || projectType === 'both') {
101
- const packageJsonPath = plugins.path.join(paths.cwd, 'package.json');
104
+ const packageJsonPath = plugins.path.join(cwdArg, 'package.json');
102
105
  const content = (await plugins.smartfs
103
106
  .file(packageJsonPath)
104
107
  .encoding('utf8')
@@ -110,7 +113,7 @@ export async function readCurrentVersion(projectType: ProjectType): Promise<stri
110
113
  }
111
114
  return packageJson.version;
112
115
  } else {
113
- const denoJsonPath = plugins.path.join(paths.cwd, 'deno.json');
116
+ const denoJsonPath = plugins.path.join(cwdArg, 'deno.json');
114
117
  const content = (await plugins.smartfs
115
118
  .file(denoJsonPath)
116
119
  .encoding('utf8')
@@ -148,10 +151,11 @@ export async function updateVersionFile(filePath: string, newVersion: string): P
148
151
  export async function updateProjectVersionFiles(
149
152
  projectType: ProjectType,
150
153
  newVersion: string,
154
+ cwdArg = paths.cwd,
151
155
  ): Promise<string[]> {
152
156
  const filesToUpdate: string[] = [];
153
- const packageJsonPath = plugins.path.join(paths.cwd, 'package.json');
154
- const denoJsonPath = plugins.path.join(paths.cwd, 'deno.json');
157
+ const packageJsonPath = plugins.path.join(cwdArg, 'package.json');
158
+ const denoJsonPath = plugins.path.join(cwdArg, 'deno.json');
155
159
 
156
160
  if (projectType === 'npm' || projectType === 'both') {
157
161
  await updateVersionFile(packageJsonPath, newVersion);