@mriekki/project-forge 4.7.0 → 4.8.1

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.
package/README.md CHANGED
@@ -103,28 +103,17 @@ The stable descriptor at `chatgpt/current.json` resolves an immutable Project Fo
103
103
 
104
104
  `@mriekki/project-forge` is the public distribution for Project Forge engineering assets used by generated projects.
105
105
 
106
- Project Forge assembles that package from approved private platform components using exact pinned source commits and deterministic distribution descriptors. Published packages are self-contained; Builders do not need access to the private implementation repositories to install or update managed assets.
106
+ Project Forge assembles that package from approved private platform components using exact pinned source revisions and deterministic distribution descriptors. Published packages are self-contained; Builders do not need access to the private implementation repositories to install or update managed assets.
107
107
 
108
- Install:
108
+ Generated Projects receive an exact approved package during deterministic generation. Existing generated Projects should normally adopt a newer supported package through:
109
109
 
110
- ```powershell
111
- npx @mriekki/project-forge install
112
- ```
113
-
114
- Update to the newest published package:
115
-
116
- ```powershell
117
- npx @mriekki/project-forge@latest update
110
+ ```text
111
+ /project-forge-update
118
112
  ```
119
113
 
120
- Inspect and verify:
121
-
122
- ```powershell
123
- npx @mriekki/project-forge@latest status
124
- npx @mriekki/project-forge@latest check
125
- ```
114
+ That Project workflow resolves the supported exact package version and preserves the managed-baseline, drift, review, and delivery guardrails. Do not use a floating `@latest` package update as the normal Builder path: npm publication can intentionally occur before the stable public contract is advanced to the new version.
126
115
 
127
- Managed updates verify baselines and refuse unsafe drift or conflicts before modifying Project Forge-managed files.
116
+ The package CLI remains available for exact-version troubleshooting and maintainer verification. Use the exact version recorded by the Project or selected by the Project Forge workflow rather than a floating tag.
128
117
 
129
118
  ## Maintainer system reference
130
119
 
@@ -137,7 +126,7 @@ The system reference deliberately does not duplicate current component SHAs, pac
137
126
 
138
127
  ## Maintainer release process
139
128
 
140
- Project Forge maintainers use a gated release workflow that resolves accepted component commits, verifies the assembled package, creates the package pull request, verifies CI, and stops at the explicit publication gate.
129
+ Project Forge maintainers use a gated release workflow that resolves accepted component revisions, verifies the assembled package, creates the package pull request, verifies CI, and stops at the explicit publication gate.
141
130
 
142
131
  Publication uses npm Trusted Publishing (OIDC), reconstructs the exact package from pinned sources, verifies registry identity and integrity, and smoke-tests the public package from a neutral directory.
143
132
 
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "schemaVersion": 2,
3
3
  "package": "@mriekki/project-forge",
4
- "version": "4.7.0",
4
+ "version": "4.8.1",
5
5
  "defaultProfile": "greenfield-project",
6
6
  "profiles": {
7
7
  "greenfield-project": {
@@ -13,7 +13,7 @@
13
13
  },
14
14
  "components": {
15
15
  "core": {
16
- "revision": "737dab0636252410c022be057d25c36c1dd5f116",
16
+ "revision": "a78c2482f4f559ead8efbab6c25b4b05496db735",
17
17
  "version": "4.0.0",
18
18
  "distribution": "greenfield-workspace",
19
19
  "descriptor": "assets/core/distribution.json",
@@ -21,7 +21,7 @@
21
21
  "canonicalDescriptor": "distributions/greenfield-workspace.json"
22
22
  },
23
23
  "deployment": {
24
- "revision": "f7441081114be94a22d6e1774194dc8bd7a88e18",
24
+ "revision": "aaf5d24c897b3d89b3d1791c79bed4979414ca40",
25
25
  "distribution": "greenfield-project",
26
26
  "descriptor": "assets/deployment/distribution.json",
27
27
  "sourceRoot": "assets/generated/deployment",
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "schemaVersion": 2,
3
3
  "package": "@mriekki/project-forge",
4
- "version": "4.7.0",
4
+ "version": "4.8.1",
5
5
  "components": {
6
6
  "core": {
7
- "revision": "737dab0636252410c022be057d25c36c1dd5f116",
7
+ "revision": "a78c2482f4f559ead8efbab6c25b4b05496db735",
8
8
  "version": "4.0.0",
9
9
  "distribution": "greenfield-workspace",
10
10
  "files": [
@@ -116,12 +116,12 @@
116
116
  {
117
117
  "source": "core/skills/deployment-readiness/SKILL.md",
118
118
  "destination": ".github/skills/deployment-readiness/SKILL.md",
119
- "sha256": "33be75e856b1b16378bb2a456d8418f8ae0b03edee75fb826d8f7ef3824d2f6a"
119
+ "sha256": "30202702cc368c872e62ce731e8ffe84a796aae243b287e3e670c8c8e1cd6531"
120
120
  },
121
121
  {
122
122
  "source": "core/skills/deploy/SKILL.md",
123
123
  "destination": ".github/skills/deploy/SKILL.md",
124
- "sha256": "21b8209bc756ef2981f9e44f19ee80e79c1c5c7d5be4c03c9b528fdadd7b8eca"
124
+ "sha256": "f5d40e549ca958cb51606ced7ca482651d9a436327b6070a9a94171f22401700"
125
125
  },
126
126
  {
127
127
  "source": "core/skills/tear-down/SKILL.md",
@@ -326,7 +326,7 @@
326
326
  ]
327
327
  },
328
328
  "deployment": {
329
- "revision": "f7441081114be94a22d6e1774194dc8bd7a88e18",
329
+ "revision": "aaf5d24c897b3d89b3d1791c79bed4979414ca40",
330
330
  "distribution": "greenfield-project",
331
331
  "files": [
332
332
  {
@@ -357,12 +357,12 @@
357
357
  {
358
358
  "source": "src/project_forge_deployment/app_cli.py",
359
359
  "destination": "tools/project-forge-deployment/project_forge_deployment/app_cli.py",
360
- "sha256": "9bf8eb7337c5b506311bf718de71b9a2c9ac445eb94cb7dfdeb63a09e5f0f899"
360
+ "sha256": "f27704c80b1eff45888bf56745bcf8755f2fad88851147600af7056953757825"
361
361
  },
362
362
  {
363
363
  "source": "src/project_forge_deployment/app_deployment.py",
364
364
  "destination": "tools/project-forge-deployment/project_forge_deployment/app_deployment.py",
365
- "sha256": "e58158e48c9fbd51b51f7af90f2dd178987ad3936e44507523cfc41b96fcd9f9"
365
+ "sha256": "606d6a4793ac8c8a95da4cd4c61792c69f02ad86900be1944d2ed570dd71e73e"
366
366
  },
367
367
  {
368
368
  "source": "src/project_forge_deployment/app_runtime.py",
@@ -502,12 +502,12 @@
502
502
  {
503
503
  "source": "src/project_forge_deployment/registry_access.py",
504
504
  "destination": "tools/project-forge-deployment/project_forge_deployment/registry_access.py",
505
- "sha256": "5815a1921ecf37dc395f9c68b1ebda2b17dd6a44620b2356d1adff5df44c4382"
505
+ "sha256": "1f582f1b5da656579625748b13c779f6d67ed6a6049f0ebb104ce8c3f9185a4b"
506
506
  },
507
507
  {
508
508
  "source": "src/project_forge_deployment/registry_cli.py",
509
509
  "destination": "tools/project-forge-deployment/project_forge_deployment/registry_cli.py",
510
- "sha256": "1936156eb3b6103895795cde3b5f520491e407bf35f311be883a54fbfff635fc"
510
+ "sha256": "2ce977fa570580407c04a3e59d0a524f128af420b7ee20fd9706e6452378b7b3"
511
511
  },
512
512
  {
513
513
  "source": "src/project_forge_deployment/route_registry.py",
@@ -44,7 +44,7 @@ Read only what is needed, including when present:
44
44
  - `PROJECT_FORGE_BASELINE.json`;
45
45
  - `DEPLOYMENT_BASELINE.json`;
46
46
  - project topology and approved deployment/release requirements;
47
- - `/deployment-readiness` evidence, including deterministic application isolation evidence when supported;
47
+ - `/deployment-readiness` evidence, including first-application outcome assessment and deterministic application isolation evidence when supported;
48
48
  - fresh setup-time environment-proof evidence for this workstation;
49
49
  - established application deployment facts.
50
50
 
@@ -62,7 +62,27 @@ Proof does not establish the real application's image/release, persistence/migra
62
62
 
63
63
  A prior readiness isolation fingerprint is review evidence only. It does not authorize deployment and does not replace the deterministic deployment planner's current isolation revalidation.
64
64
 
65
- ## 3. First real deployment asks only unresolved application questions
65
+ For a first real deployment, do not collapse a target/platform `READY` result into application deployability. Confirm that readiness established an actual artifact build/publication/acquisition path when that path is material. If artifact identity or acquisition is still hypothetical, return to the missing prerequisite rather than requesting the application write gate.
66
+
67
+ ## 3. Preserve execution and credential context
68
+
69
+ When installed tooling exposes structured capability evidence, preserve its semantics exactly in the deployment assessment and preview. In particular, retain fields equivalent to:
70
+
71
+ ```text
72
+ executionLocation
73
+ credentialContext
74
+ capabilityProved
75
+ capabilityAttempted
76
+ doesNotProve
77
+ ```
78
+
79
+ A registry manifest check executed on a remote target proves only the capability reported for that target/context. Never describe it as workstation registry access. A workstation registry check proves only the workstation capability it reports. Neither implies Actions registry transport or package-metadata API access.
80
+
81
+ Do not infer provider-specific token scopes or error meaning from generic registry results. Provider-specific package metadata/publication behavior belongs to repository/provider tooling and its own evidence.
82
+
83
+ When a new credential is genuinely required, explain its purpose, execution context, storage location, retrievability, and rotation impact before asking the developer to create it. Credential values remain in the provider/local secret surface and never enter chat.
84
+
85
+ ## 4. First real deployment asks only unresolved application questions
66
86
 
67
87
  The first real `/deploy <environment>` after environment proof may need a small application-specific completion step. Ask only material unknowns not already answered by project evidence, such as:
68
88
 
@@ -79,7 +99,7 @@ When a bounded choice is useful, follow `INTERACTIVE_SESSION_GUIDE.md`: use host
79
99
 
80
100
  Once these facts are durably established, later deployments should be routine rather than repeating onboarding.
81
101
 
82
- ## 4. Placement determines launcher
102
+ ## 5. Placement determines launcher
83
103
 
84
104
  ### Workstation container
85
105
 
@@ -114,7 +134,7 @@ Use only an installed repository/catalog provider adapter. If none exists, stop
114
134
 
115
135
  Report `DEPLOYMENT NOT BOUND YET` and route to `/setup <environment>` or `/deployment-readiness <environment>`.
116
136
 
117
- ## 5. Preserve custom Production classification
137
+ ## 6. Preserve custom Production classification
118
138
 
119
139
  Pass the exact configured environment name to the deterministic launcher.
120
140
 
@@ -126,7 +146,7 @@ When resolved purpose/classification is Production, use the installed tool's exp
126
146
 
127
147
  Inspect installed help/version evidence. If the installed tool cannot preserve Production classification for a custom name, stop rather than downgrading safety.
128
148
 
129
- ## 6. Build the deterministic plan
149
+ ## 7. Build the deterministic plan
130
150
 
131
151
  Use repository-provided tooling to construct/inspect the exact application deployment plan. Supported operations may include:
132
152
 
@@ -149,7 +169,44 @@ The deterministic deployment tool owns the final current-state recheck immediate
149
169
 
150
170
  Do not smuggle infrastructure provisioning, target/provider rebinding, destructive restore, or environment tear-down into the application plan.
151
171
 
152
- ## 7. Preserve exact authorization contracts
172
+ ### Remote runtime + route rollout preview
173
+
174
+ When the installed Deployment Kit exposes a deterministic read-only `app rollout` capability and the selected remote deployment has an intended route, prefer that capability for the final pre-write preview rather than independently narrating runtime and route consequences.
175
+
176
+ A current installed contract may expose a command shape similar to:
177
+
178
+ ```text
179
+ project-forge-deploy app rollout <target>
180
+ --owner-repo <owner/repository>
181
+ --environment <exact environment>
182
+ --app-slug <application slug>
183
+ --container-name <container>
184
+ --runtime-port <port>
185
+ --persistence-mode <none|directory>
186
+ --image <immutable digest>
187
+ --health-path <path>
188
+ --run-as <uid[:gid]>
189
+ [--route-hostname <hostname> ...]
190
+ [--production]
191
+ --json
192
+ ```
193
+
194
+ Inspect installed help/version rather than assuming this example exactly matches every package version.
195
+
196
+ Treat the returned staged rollout as **read-only review evidence**. Preserve:
197
+
198
+ ```text
199
+ runtime plan + isolation evidence
200
+ route plan + route-inventory evidence
201
+ separate stage authorizations
202
+ endpoint state before runtime
203
+ after-runtime / before-route endpoint state
204
+ after-route verification state
205
+ ```
206
+
207
+ If the installed package lacks this composite preview, retain the existing safe runtime and route planners and state their sequence explicitly. Do not invent a combined write command.
208
+
209
+ ## 8. Preserve exact authorization contracts
153
210
 
154
211
  Core consequence labels do not replace stricter installed-tool phrases.
155
212
 
@@ -158,17 +215,20 @@ The standard Deployment Kit may use exact phrases such as:
158
215
  ```text
159
216
  [APP DEPLOY WRITE]
160
217
  [PRODUCTION APP DEPLOY]
218
+ [ROUTE WRITE]
161
219
  ```
162
220
 
163
221
  For Production-class environments also show `[PRODUCTION WRITE]` consequence visibility where Core policy calls for it.
164
222
 
165
- Resolve the exact phrase from the installed tool/version. Do not assume an example phrase when the installed contract differs.
223
+ Resolve the exact phrase from the installed tool/version. For a staged rollout, resolve it separately for each stage. Do not assume an example phrase when the installed contract differs.
224
+
225
+ Authorization applies only to the exact environment, resolved classification, placement/target/provider, artifact, reviewed plan/stage, current isolation evidence and route evidence, and evidence shown. Material changes require renewed review/authorization.
166
226
 
167
- Authorization applies only to the exact environment, resolved classification, placement/target/provider, artifact, reviewed plan, current isolation evidence, and evidence shown. Material changes require renewed review/authorization.
227
+ A runtime authorization never authorizes the route stage. A route authorization never retroactively authorizes a changed runtime artifact. If the rollout contains both stages, preserve the separate gates even though the Builder reviewed one coherent outcome.
168
228
 
169
- ## 8. Preview and execute only the approved contract
229
+ ## 9. Preview and execute only the approved contract
170
230
 
171
- Before mutation, show concise non-secret evidence such as exact environment, purpose, placement, target/provider when applicable, proof freshness, immutable application artifact, persistence/migration, exposure/route, application isolation state/fingerprint when exposed, shared failure-domain note when relevant, planned operations, and exact required authorization phrase.
231
+ Before mutation, show concise non-secret evidence such as exact environment, purpose, placement, target/provider when applicable, proof freshness, immutable application artifact, persistence/migration, exposure/route, application isolation state/fingerprint when exposed, shared failure-domain note when relevant, planned stages, endpoint state at each stage, and the exact required authorization phrase for the next consequence only.
172
232
 
173
233
  After authorization, invoke only the repository-provided deterministic launcher. Never redirect to another environment/host/provider, weaken trust/exposure, force through lock contention, or invent recovery writes.
174
234
 
@@ -176,7 +236,37 @@ The deterministic launcher must revalidate current isolation immediately before
176
236
 
177
237
  Existing per-app/per-environment locks are normal concurrency protection when another workstation is deploying the same environment.
178
238
 
179
- ## 9. Validate live evidence
239
+ ### Runtime succeeded but route is pending
240
+
241
+ A healthy application runtime is a valid intermediate outcome. When a requested route has not yet been registered, say this explicitly before showing the route gate:
242
+
243
+ ```text
244
+ APPLICATION RUNTIME READY
245
+ Expected endpoint: <hostname/URL>
246
+ Endpoint state: NOT ACTIVE — ROUTE PENDING
247
+ Do not test this URL yet; the route write has not been authorized/applied.
248
+ ```
249
+
250
+ Then, if the reviewed route plan is still current, present the route consequence directly. Do not require the developer to invoke `/deploy` or another slash command merely to rediscover the already reviewed route stage.
251
+
252
+ The runtime success does not consume or imply route authorization. If route evidence changed, rebuild/review the route plan first.
253
+
254
+ ## 10. Resume the established outcome without navigation ceremony
255
+
256
+ Within the same conversation, if a bounded engineering/delivery correction or external prerequisite completes and the developer's requested deployment outcome remains unambiguous, automatically resume safe **read-only** reconstruction of readiness/deployment evidence from the repository, GitHub, installed tools, and runtime state.
257
+
258
+ Do not require the developer to type `/deliver`, `/deployment-readiness`, or `/deploy` merely as navigation to rediscover known state after that bounded action completes.
259
+
260
+ This continuation:
261
+
262
+ - never repeats a completed write;
263
+ - never reuses exhausted registry/Production/route/merge/migration/destructive authority;
264
+ - stops when the artifact, plan, target, consequence, or other material evidence changed;
265
+ - does not create a parallel persistent workflow database.
266
+
267
+ If `/deploy` is invoked again after an already validated deployment, first reconstruct current deployment evidence. If the same validated artifact/runtime/route remains current and no new deployment outcome is requested, report the existing validated state rather than proposing or performing a redundant redeployment.
268
+
269
+ ## 11. Validate live evidence
180
270
 
181
271
  After execution, use repository-provided inventory/health/route/smoke/evidence commands when available.
182
272
 
@@ -191,10 +281,32 @@ Artifact: <exact immutable identity>
191
281
  Artifact delivery: <workstation-transfer|remote-registry|local-runtime|provider>
192
282
  Endpoint/Route: READY / LOOPBACK / NOT APPLICABLE
193
283
  Application health: READY
284
+ Workstation/browser verification: PASSED / NOT APPLICABLE / PENDING
194
285
  Smoke: PASSED / NOT REQUIRED
195
286
  Deployment evidence: RECORDED / VERIFIED
196
287
  ```
197
288
 
198
- Do not report success merely because a container/process started when stronger health evidence is required.
289
+ Do not report success merely because a container/process started when stronger health evidence is required. Do not report a route as user-accessible merely because registration returned; perform the repository-supported endpoint/workstation/browser verification when the requested outcome depends on it.
199
290
 
200
291
  A setup-time disposable environment proof is not real application deployment evidence. A successful application deployment never authorizes a later Production write or destructive `/tear-down`.
292
+
293
+ Keep human access confirmation distinct from automated browser/health evidence. The developer saying they can reach the endpoint is useful observed human evidence; it is not automatically formal work-item acceptance.
294
+
295
+ ## 12. Reconcile the durable outcome without asserting acceptance
296
+
297
+ After `DEPLOYMENT VALIDATED`, inspect the repository's configured work-item/operating authority. When an existing current-status or deployment-result section is stale and the observed deployment outcome is material, offer a bounded reconciliation containing only supported non-secret facts such as:
298
+
299
+ ```text
300
+ exact environment
301
+ release/source/artifact identity
302
+ artifact delivery method
303
+ runtime health result
304
+ route/endpoint
305
+ workstation/browser validation
306
+ explicit human access confirmation, when actually stated
307
+ known limitations and evidence boundaries
308
+ ```
309
+
310
+ Preserve historical approval records. Do not silently rewrite product requirements, broaden the milestone, mark formal acceptance, close the work item, or infer acceptance from browser accessibility.
311
+
312
+ If the human authorizes the bounded documentation update, use the repository's normal work-item/delivery workflow for that edit. Documentation reconciliation is not a reason to redeploy and does not create new Production write authority.
@@ -78,7 +78,78 @@ It does not prove final application artifact identity, persistence/migration beh
78
78
 
79
79
  If topology, target binding, host trust, platform state, or another material fact changes, treat the proof as stale and reverify the affected prerequisite.
80
80
 
81
- ## 3. Placement branches
81
+ ## 3. First-application outcome assessment
82
+
83
+ For the first real application deployment to an environment, or whenever the developer's stated goal is a usable endpoint rather than only machine readiness, keep that requested outcome visible and assess the whole material path before declaring the application deploy-ready.
84
+
85
+ Inspect available non-secret repository, package/tool, workstation, target, and GitHub/CI evidence before asking for facts. Do not require a release workflow to fail merely to discover a prerequisite that can be identified read-only.
86
+
87
+ Build a compact prerequisite matrix with rows such as:
88
+
89
+ ```text
90
+ requested outcome / expected endpoint
91
+ workstation architecture
92
+ required artifact/target architecture
93
+ immutable artifact or release identity policy
94
+ artifact destination / publication state when relevant
95
+ credential capability + execution location for each required operation
96
+ workstation artifact acquisition capability
97
+ target delivery method
98
+ current application isolation / footprint
99
+ runtime health contract
100
+ route / exposure mechanism
101
+ final workstation/browser verification
102
+ ```
103
+
104
+ For each material row distinguish:
105
+
106
+ ```text
107
+ evidence source
108
+ execution location / credential context
109
+ PROVEN / UNKNOWN / BLOCKED / NOT APPLICABLE
110
+ missing fact or prerequisite
111
+ exact consequential gate, if any
112
+ effect on the requested endpoint
113
+ ```
114
+
115
+ Architecture is a portability fact, not an operating-system stereotype. For example, an ARM64 workstation targeting a Linux/x86-64 environment must surface the required artifact platform explicitly; do not label that generically as a macOS defect.
116
+
117
+ Do not report `ENVIRONMENT READY TO DEPLOY` for the first real application while its actual artifact build/publication/acquisition path is still hypothetical. Target/platform readiness and application-deployment readiness are different dimensions.
118
+
119
+ Keep these evidence dimensions separate in status language:
120
+
121
+ ```text
122
+ static/review correctness
123
+ live external-contract confidence
124
+ credential capability in the exact execution context
125
+ immutable artifact provenance
126
+ environment/runtime readiness
127
+ route state
128
+ workstation/browser validation
129
+ human access confirmation
130
+ formal work-item acceptance
131
+ ```
132
+
133
+ A green result in one dimension never proves another.
134
+
135
+ ### Credential capability and lifecycle
136
+
137
+ Reason from **operation + execution context**, not from the existence of a generic token or successful access somewhere else. When installed tooling exposes structured registry capability evidence, preserve fields such as execution location, credential context, capability proved, and `doesNotProve` rather than paraphrasing a target result as workstation access.
138
+
139
+ Before asking the developer to create or rotate a credential, explain non-secret lifecycle facts first:
140
+
141
+ ```text
142
+ purpose / capability
143
+ machine, workflow, or process where it is used
144
+ where the credential will be stored
145
+ whether its value can be retrieved again
146
+ rotation/regeneration impact on existing consumers
147
+ value must remain in the local/provider secret surface, never chat
148
+ ```
149
+
150
+ Do not assume registry transport access proves package-metadata API access, that target registry access proves workstation access, or that a workstation login proves an Actions workflow capability. Provider-specific token types, scopes, and error-body strings belong to installed/provider-owned adapters or Project evidence, not generic Core invention.
151
+
152
+ ## 4. Placement branches
82
153
 
83
154
  ### A. Workstation placement
84
155
 
@@ -188,7 +259,7 @@ DEPLOYMENT INTENTIONALLY DEFERRED
188
259
 
189
260
  Do not mark workspace readiness failed. Route to `/setup <environment>` or `/setup remote` if the developer later wants to bind it.
190
261
 
191
- ## 4. Shared failure domains / colocated environments
262
+ ## 5. Shared failure domains / colocated environments
192
263
 
193
264
  Multiple logical environments may share physical failure domains, but they remain independently targetable and independently authorized. If environments share physical failure, state it explicitly.
194
265
 
@@ -196,7 +267,7 @@ A clear application isolation preflight means the current Deployment Kit-managed
196
267
 
197
268
  Colocation does not demonstrate host-level Production isolation. Never infer readiness for one logical environment from another merely because they share hardware.
198
269
 
199
- ## 5. State-driven cadence
270
+ ## 6. State-driven cadence
200
271
 
201
272
  Normal cadence is:
202
273
 
@@ -205,6 +276,7 @@ select exact project-defined environment
205
276
  → read current topology
206
277
  → reuse fresh environment-proof evidence when valid
207
278
  → rediscover placement-relevant local state
279
+ → for first real deployment, assess the complete artifact/credential/delivery/route/endpoint path
208
280
  → verify current application isolation when the installed adapter supports it
209
281
  → verify/repair only missing prerequisites under exact gates
210
282
  → ENVIRONMENT READY TO DEPLOY
@@ -213,7 +285,9 @@ select exact project-defined environment
213
285
 
214
286
  For remote placement, reuse valid existing target access rather than creating duplicate credentials simply because another Project/environment uses the same machine.
215
287
 
216
- ## 6. Read-only status
288
+ When a bounded engineering/delivery correction completes in the same conversation and the developer's deployment outcome is still clear, automatically resume safe read-only readiness reconstruction from current repository/tool evidence. Do not require the developer to type `/deployment-readiness` again merely as navigation. This continuation does not reuse an exhausted write authorization and does not authorize a new registry, Production, route, migration, merge, or other consequential write.
289
+
290
+ ## 7. Read-only status
217
291
 
218
292
  `/deployment-readiness status [environment]` performs no remote writes. Without a specific environment, summarize all known environments from project topology, including Placement and deferred state where evidenced.
219
293
 
@@ -229,7 +303,7 @@ Archive demo deferred NOT APPLICABLE DEFERRED
229
303
 
230
304
  Do not manufacture target/provider facts for deferred environments.
231
305
 
232
- ## 7. Bounded readiness writes
306
+ ## 8. Bounded readiness writes
233
307
 
234
308
  A missing prerequisite is not authorization to change it. Use the repository/tool's exact authorization phrase for key/trust/platform/provider mutations.
235
309
 
@@ -237,21 +311,23 @@ Do not hide the real application deployment inside readiness. Readiness may rely
237
311
 
238
312
  The application isolation preflight itself is read-only and does not authorize later deployment, route, persistence, migration, Production, or destructive writes.
239
313
 
240
- ## 8. Pre-deploy versus post-deploy evidence
314
+ ## 9. Pre-deploy versus post-deploy evidence
241
315
 
242
316
  Readiness asks whether `/deploy <name>` can safely perform the real application deployment now.
243
317
 
244
- Pre-deploy evidence may include topology, adapter availability, binding, local/remote identity and access, target platform, fresh proof evidence, artifact policy, deterministic application isolation/collision evidence, route/port availability, persistence/backup/migration prerequisites, lock/recovery capability, and Production-specific safeguards.
318
+ Pre-deploy evidence may include topology, adapter availability, binding, local/remote identity and access, target platform, fresh proof evidence, artifact policy, actual artifact acquisition/publication capability when material, deterministic application isolation/collision evidence, route/port availability, persistence/backup/migration prerequisites, lock/recovery capability, and Production-specific safeguards.
245
319
 
246
320
  Do not require first-deployment live route, deployed-release identity, application health, or smoke evidence before the application has ever been deployed. Those are post-deploy validation evidence owned by `/deploy`.
247
321
 
248
- ## 9. Production policy
322
+ The intended endpoint may still be known before deployment. When the route does not yet exist, label it explicitly `NOT ACTIVE — ROUTE PENDING`; do not invite the developer to test it as though readiness had made it reachable.
323
+
324
+ ## 10. Production policy
249
325
 
250
326
  Production is logical policy, not hardware count and not the literal name `production`. A named environment such as `prod-east` with purpose `production` must retain Production readiness and later deployment gates.
251
327
 
252
328
  Readiness never authorizes Production application deployment.
253
329
 
254
- ## 10. Final handoff
330
+ ## 11. Final handoff
255
331
 
256
332
  When pre-deploy requirements are satisfied:
257
333
 
@@ -263,10 +339,14 @@ Purpose: <purpose>
263
339
  Placement: <...>
264
340
  Binding: VERIFIED / LOCAL / provider-specific
265
341
  This-workstation environment proof: PROVEN / NOT REQUIRED / NOT AVAILABLE
342
+ Artifact path: PROVEN / NOT APPLICABLE
343
+ Credential contexts: PROVEN / NOT APPLICABLE
266
344
  Application isolation: CLEAR / NOT APPLICABLE
267
345
  Isolation evidence: <evidenceSha256 when exposed by installed tooling>
268
346
  Platform prerequisites: READY
269
347
  Application-specific prerequisites: READY / <remaining items>
348
+ Expected endpoint: <URL/route when known>
349
+ Endpoint state: NOT ACTIVE — ROUTE PENDING / LOOPBACK PENDING / provider-specific
270
350
  Security blockers: NONE
271
351
 
272
352
  POST-DEPLOY VALIDATION PENDING
@@ -12,6 +12,7 @@ from .app_deployment import (
12
12
  apply_app_deploy,
13
13
  apply_app_deploy_from_workstation,
14
14
  plan_app_deploy,
15
+ plan_app_rollout,
15
16
  )
16
17
  from .app_runtime import (
17
18
  APP_DEPLOY_WRITE_PHRASE,
@@ -85,6 +86,29 @@ def _add_isolation_footprint(parser: argparse.ArgumentParser) -> None:
85
86
  )
86
87
 
87
88
 
89
+ def _add_runtime_contract(parser: argparse.ArgumentParser) -> None:
90
+ parser.add_argument("--image", required=True)
91
+ parser.add_argument("--health-path", required=True)
92
+ parser.add_argument("--run-as", required=True)
93
+ parser.add_argument("--data-container-path")
94
+ parser.add_argument(
95
+ "--env",
96
+ action="append",
97
+ default=[],
98
+ help="Non-secret runtime environment value in NAME=value form; repeat as needed",
99
+ )
100
+ parser.add_argument(
101
+ "--image-source",
102
+ choices=("remote-registry", "workstation"),
103
+ default="remote-registry",
104
+ help=(
105
+ "remote-registry preserves the existing target-side pull path; workstation streams an exact local image "
106
+ "over verified SSH so the target does not need registry credentials"
107
+ ),
108
+ )
109
+ parser.add_argument("--json", action="store_true")
110
+
111
+
88
112
  def build_parser() -> argparse.ArgumentParser:
89
113
  parser = argparse.ArgumentParser(prog="project-forge-deploy app")
90
114
  sub = parser.add_subparsers(dest="command", required=True)
@@ -108,31 +132,19 @@ def build_parser() -> argparse.ArgumentParser:
108
132
  )
109
133
  isolation.add_argument("--json", action="store_true")
110
134
 
135
+ rollout = sub.add_parser(
136
+ "rollout",
137
+ help="Preview application runtime and owned route as one read-only staged rollout",
138
+ )
139
+ _add_isolation_footprint(rollout)
140
+ _add_runtime_contract(rollout)
141
+
111
142
  deploy = sub.add_parser(
112
143
  "deploy",
113
144
  help="Deploy one immutable application runtime without modifying routes",
114
145
  )
115
146
  _add_isolation_footprint(deploy)
116
- deploy.add_argument("--image", required=True)
117
- deploy.add_argument("--health-path", required=True)
118
- deploy.add_argument("--run-as", required=True)
119
- deploy.add_argument("--data-container-path")
120
- deploy.add_argument(
121
- "--env",
122
- action="append",
123
- default=[],
124
- help="Non-secret runtime environment value in NAME=value form; repeat as needed",
125
- )
126
- deploy.add_argument(
127
- "--image-source",
128
- choices=("remote-registry", "workstation"),
129
- default="remote-registry",
130
- help=(
131
- "remote-registry preserves the existing target-side pull path; workstation streams an exact local image "
132
- "over verified SSH so the target does not need registry credentials"
133
- ),
134
- )
135
- deploy.add_argument("--json", action="store_true")
147
+ _add_runtime_contract(deploy)
136
148
  deploy.add_argument("--yes", action="store_true")
137
149
  return parser
138
150
 
@@ -175,6 +187,20 @@ def main(argv: list[str] | None = None) -> int:
175
187
  environment_variables=_parse_env(args.env),
176
188
  )
177
189
  assert_app_spec_matches_persistence(args.name, spec)
190
+
191
+ if args.command == "rollout":
192
+ rollout_plan = plan_app_rollout(
193
+ args.name,
194
+ spec,
195
+ production=production_classified,
196
+ database_file=args.database_file,
197
+ route_hostnames=args.route_hostname,
198
+ )
199
+ rollout_plan["imageSource"] = args.image_source
200
+ rollout_plan["targetRegistryCredentialRequired"] = args.image_source == "remote-registry"
201
+ _print(rollout_plan, as_json=args.json)
202
+ return 0
203
+
178
204
  plan = plan_app_deploy(
179
205
  args.name,
180
206
  spec,
@@ -10,15 +10,19 @@ from __future__ import annotations
10
10
 
11
11
  from typing import Any, Iterable
12
12
 
13
+ from .app_runtime import PRODUCTION_APP_DEPLOY_WRITE_PHRASE
13
14
  from .app_runtime import apply_app_deploy as _apply_runtime_deploy
15
+ from .app_runtime import deploy_write_phrase
14
16
  from .app_runtime import plan_app_deploy as _plan_runtime_deploy
15
17
  from .app_runtime_workstation import apply_app_deploy_with_preloaded_image
16
18
  from .environment_isolation import environment_isolation_preflight
17
19
  from .errors import DeploymentKitError
18
20
  from .image_transfer import transfer_image
21
+ from .route_registry import ROUTE_WRITE_PHRASE, plan_route_registration
19
22
 
20
23
 
21
24
  ISOLATION_REVIEW_KEY = "isolationReview"
25
+ ROLLOUT_SCHEMA_VERSION = 1
22
26
 
23
27
 
24
28
  def app_isolation_check(
@@ -139,6 +143,100 @@ def plan_app_deploy(
139
143
  return plan
140
144
 
141
145
 
146
+ def _endpoint_states(route_plan: dict[str, Any] | None) -> dict[str, str]:
147
+ if route_plan is None:
148
+ return {
149
+ "beforeRuntime": "NO_ROUTE_REQUESTED",
150
+ "afterRuntimeBeforeRoute": "NO_ROUTE_REQUESTED",
151
+ "afterRoute": "NO_ROUTE_REQUESTED",
152
+ }
153
+ if route_plan["changeType"] == "create":
154
+ before = "NOT_ACTIVE_ROUTE_ABSENT"
155
+ after_runtime = "NOT_ACTIVE_ROUTE_PENDING"
156
+ else:
157
+ before = "ROUTE_PRESENT_RUNTIME_STATE_NOT_VALIDATED"
158
+ after_runtime = "ROUTE_PRESENT_ROUTE_CHANGE_PENDING"
159
+ return {
160
+ "beforeRuntime": before,
161
+ "afterRuntimeBeforeRoute": after_runtime,
162
+ "afterRoute": "ROUTE_REGISTERED_BROWSER_VERIFICATION_PENDING",
163
+ }
164
+
165
+
166
+ def plan_app_rollout(
167
+ target_name: str,
168
+ spec: dict[str, Any],
169
+ *,
170
+ production: bool = False,
171
+ database_file: str | None = None,
172
+ route_hostnames: Iterable[str] | None = None,
173
+ ) -> dict[str, Any]:
174
+ """Compose read-only runtime and route plans into one staged rollout preview."""
175
+
176
+ hosts = list(route_hostnames or [])
177
+ app_plan = plan_app_deploy(
178
+ target_name,
179
+ spec,
180
+ production=production,
181
+ database_file=database_file,
182
+ route_hostnames=hosts,
183
+ )
184
+ route_plan = None
185
+ if hosts:
186
+ route_plan = plan_route_registration(
187
+ target_name,
188
+ owner_repo=spec["ownerRepo"],
189
+ environment=spec["environment"],
190
+ app_slug=spec["appSlug"],
191
+ upstream_container=spec["containerName"],
192
+ upstream_port=spec["runtimePort"],
193
+ hostnames=hosts,
194
+ )
195
+
196
+ runtime_authorization = (
197
+ PRODUCTION_APP_DEPLOY_WRITE_PHRASE
198
+ if production or spec["environment"] == "production"
199
+ else deploy_write_phrase(app_plan)
200
+ )
201
+ stages = [
202
+ {
203
+ "stage": "runtime",
204
+ "action": "deploy-application-runtime",
205
+ "requiredAuthorization": runtime_authorization,
206
+ "mutated": False,
207
+ }
208
+ ]
209
+ if route_plan is not None:
210
+ stages.append(
211
+ {
212
+ "stage": "route",
213
+ "action": "register-application-route",
214
+ "requiredAuthorization": ROUTE_WRITE_PHRASE,
215
+ "mutated": False,
216
+ }
217
+ )
218
+
219
+ runtime_view = {key: value for key, value in app_plan.items() if key != "spec"}
220
+ return {
221
+ "schemaVersion": ROLLOUT_SCHEMA_VERSION,
222
+ "targetName": target_name,
223
+ "environment": spec["environment"],
224
+ "production": bool(production or spec["environment"] == "production"),
225
+ "image": spec["image"],
226
+ "runtimePlan": runtime_view,
227
+ "routePlan": route_plan,
228
+ "stages": stages,
229
+ "separateAuthorizationsRequired": route_plan is not None,
230
+ "endpointState": _endpoint_states(route_plan),
231
+ "postRouteVerification": (
232
+ ["application-health", "route-http", "workstation-browser"]
233
+ if route_plan is not None
234
+ else ["application-health"]
235
+ ),
236
+ "mutated": False,
237
+ }
238
+
239
+
142
240
  def revalidate_app_isolation(plan: dict[str, Any]) -> dict[str, Any]:
143
241
  """Recheck the reviewed application footprint immediately before mutation."""
144
242
 
@@ -186,9 +284,11 @@ def apply_app_deploy_from_workstation(plan: dict[str, Any]) -> dict[str, Any]:
186
284
 
187
285
  __all__ = [
188
286
  "ISOLATION_REVIEW_KEY",
287
+ "ROLLOUT_SCHEMA_VERSION",
189
288
  "app_isolation_check",
190
289
  "apply_app_deploy",
191
290
  "apply_app_deploy_from_workstation",
192
291
  "plan_app_deploy",
292
+ "plan_app_rollout",
193
293
  "revalidate_app_isolation",
194
294
  ]
@@ -1,7 +1,8 @@
1
- """Read-only exact-image registry access verification for logical targets."""
1
+ """Read-only exact-image registry capability verification by execution context."""
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import os
5
6
  import re
6
7
  import shlex
7
8
  import subprocess
@@ -13,6 +14,17 @@ from .ssh import ssh_args, verify_access
13
14
 
14
15
 
15
16
  _IMAGE_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._/:+-]*@sha256:[0-9a-f]{64}$")
17
+ _REMOTE_DOES_NOT_PROVE = [
18
+ "workstation-registry-access",
19
+ "workstation-image-pull",
20
+ "actions-registry-transport",
21
+ "package-metadata-api-access",
22
+ ]
23
+ _WORKSTATION_DOES_NOT_PROVE = [
24
+ "remote-target-registry-access",
25
+ "actions-registry-transport",
26
+ "package-metadata-api-access",
27
+ ]
16
28
 
17
29
  _REMOTE_SCRIPT = r'''#!/usr/bin/env bash
18
30
  set -Eeuo pipefail
@@ -61,6 +73,14 @@ printf 'ACCESS\t%s\n' "$credential_state"
61
73
  '''
62
74
 
63
75
 
76
+ class RegistryCapabilityError(DeploymentKitError):
77
+ """Fail-closed registry capability error with safe structured evidence."""
78
+
79
+ def __init__(self, message: str, result: dict[str, Any]) -> None:
80
+ super().__init__(message)
81
+ self.result = result
82
+
83
+
64
84
  def normalize_registry_image(value: str) -> str:
65
85
  if not isinstance(value, str) or _IMAGE_RE.fullmatch(value) is None:
66
86
  raise DeploymentKitError(
@@ -83,13 +103,55 @@ def _parse_access(output: str) -> str:
83
103
  return parts[1]
84
104
 
85
105
 
106
+ def _remote_failure_result(
107
+ *,
108
+ name: str,
109
+ image: str,
110
+ address: str,
111
+ returncode: int,
112
+ ) -> dict[str, Any]:
113
+ failure_codes = {
114
+ 42: "docker-cli-unavailable",
115
+ 43: "credential-config-unsafe",
116
+ 44: "manifest-access-failed",
117
+ }
118
+ return {
119
+ "status": "failed",
120
+ "executionLocation": "remote-target",
121
+ "targetName": name,
122
+ "verifiedAddress": address,
123
+ "capabilityAttempted": "registry-manifest-read",
124
+ "image": image,
125
+ "failureCode": failure_codes.get(returncode, "remote-command-failed"),
126
+ "doesNotProve": list(_REMOTE_DOES_NOT_PROVE),
127
+ "mutated": False,
128
+ }
129
+
130
+
131
+ def _workstation_failure_result(*, image: str, failure_code: str) -> dict[str, Any]:
132
+ return {
133
+ "status": "failed",
134
+ "executionLocation": "workstation",
135
+ "credentialContext": "workstation-docker-client",
136
+ "dockerConfigSource": "explicit-environment" if os.environ.get("DOCKER_CONFIG") else "default",
137
+ "capabilityAttempted": "registry-manifest-read",
138
+ "image": image,
139
+ "failureCode": failure_code,
140
+ "doesNotProve": list(_WORKSTATION_DOES_NOT_PROVE),
141
+ "mutated": False,
142
+ }
143
+
144
+
86
145
  def verify_registry_access(name: str, image: str) -> dict[str, Any]:
146
+ """Verify exact-image manifest access from the named remote target."""
147
+
87
148
  exact_image = normalize_registry_image(image)
88
149
  profile = load_profile(name)
89
150
  access = verify_access(profile)
151
+ address = access["address"]
90
152
  command = ssh_args(
91
153
  profile,
92
- access["address"],
154
+ address,
93
155
  command="bash -s -- " + shlex.quote(exact_image),
94
156
  )
95
157
  completed = subprocess.run(
@@ -99,16 +161,84 @@ def verify_registry_access(name: str, image: str) -> dict[str, Any]:
99
161
  capture_output=True,
100
162
  )
101
163
  stdout = completed.stdout.decode("utf-8", errors="replace")
102
- stderr = completed.stderr.decode("utf-8", errors="replace")
103
164
  if completed.returncode != 0:
104
- detail = stderr.strip() or stdout.strip() or f"exit {completed.returncode}"
105
- raise DeploymentKitError(f"Read-only registry access verification failed: {detail}")
165
+ result = _remote_failure_result(
166
+ name=name,
167
+ image=exact_image,
168
+ address=address,
169
+ returncode=completed.returncode,
170
+ )
171
+ raise RegistryCapabilityError(
172
+ f"Remote-target registry capability check failed ({result['failureCode']}).",
173
+ result,
174
+ )
106
175
 
107
176
  credential_state = _parse_access(stdout)
108
177
  return {
178
+ "status": "verified",
179
+ "executionLocation": "remote-target",
109
180
  "targetName": name,
181
+ "verifiedAddress": address,
182
+ "credentialContext": (
183
+ "target-docker-config" if credential_state == "protected" else "target-anonymous"
184
+ ),
185
+ "capabilityProved": "registry-manifest-read",
110
186
  "image": exact_image,
111
187
  "manifestAccess": "verified",
112
188
  "credentialConfig": credential_state,
189
+ "doesNotProve": list(_REMOTE_DOES_NOT_PROVE),
113
190
  "mutated": False,
114
191
  }
192
+
193
+
194
+ def verify_workstation_registry_access(image: str) -> dict[str, Any]:
195
+ """Verify exact-image manifest access from this workstation without pulling."""
196
+
197
+ exact_image = normalize_registry_image(image)
198
+ command = ["docker", "manifest", "inspect", exact_image]
199
+ try:
200
+ completed = subprocess.run(
201
+ command,
202
+ check=False,
203
+ capture_output=True,
204
+ )
205
+ except FileNotFoundError as error:
206
+ result = _workstation_failure_result(
207
+ image=exact_image,
208
+ failure_code="docker-cli-unavailable",
209
+ )
210
+ raise RegistryCapabilityError(
211
+ "Workstation registry capability check failed (docker-cli-unavailable).",
212
+ result,
213
+ ) from error
214
+
215
+ if completed.returncode != 0:
216
+ result = _workstation_failure_result(
217
+ image=exact_image,
218
+ failure_code="manifest-access-failed",
219
+ )
220
+ raise RegistryCapabilityError(
221
+ "Workstation registry capability check failed (manifest-access-failed).",
222
+ result,
223
+ )
224
+
225
+ return {
226
+ "status": "verified",
227
+ "executionLocation": "workstation",
228
+ "credentialContext": "workstation-docker-client",
229
+ "dockerConfigSource": "explicit-environment" if os.environ.get("DOCKER_CONFIG") else "default",
230
+ "capabilityProved": "registry-manifest-read",
231
+ "image": exact_image,
232
+ "manifestAccess": "verified",
233
+ "doesNotProve": list(_WORKSTATION_DOES_NOT_PROVE),
234
+ "mutated": False,
235
+ }
236
+
237
+
238
+ __all__ = [
239
+ "RegistryCapabilityError",
240
+ "build_remote_script",
241
+ "normalize_registry_image",
242
+ "verify_registry_access",
243
+ "verify_workstation_registry_access",
244
+ ]
@@ -1,4 +1,4 @@
1
- """CLI for read-only registry access verification."""
1
+ """CLI for read-only registry capability verification."""
2
2
 
3
3
  from __future__ import annotations
4
4
 
@@ -8,7 +8,11 @@ import sys
8
8
  from typing import Any
9
9
 
10
10
  from .errors import DeploymentKitError
11
- from .registry_access import verify_registry_access
11
+ from .registry_access import (
12
+ RegistryCapabilityError,
13
+ verify_registry_access,
14
+ verify_workstation_registry_access,
15
+ )
12
16
 
13
17
 
14
18
  def _print(value: Any, *, as_json: bool) -> None:
@@ -28,11 +32,24 @@ def build_parser() -> argparse.ArgumentParser:
28
32
 
29
33
  verify = sub.add_parser(
30
34
  "verify",
31
- help="Verify exact-image registry manifest access without pulling or modifying target state",
35
+ help=(
36
+ "Verify exact-image registry manifest access on the named remote target without pulling "
37
+ "or modifying target state"
38
+ ),
32
39
  )
33
- verify.add_argument("name")
40
+ verify.add_argument("name", help="Named remote target profile")
34
41
  verify.add_argument("--image", required=True)
35
42
  verify.add_argument("--json", action="store_true")
43
+
44
+ verify_workstation = sub.add_parser(
45
+ "verify-workstation",
46
+ help=(
47
+ "Verify exact-image registry manifest access from this workstation without pulling "
48
+ "or modifying target state"
49
+ ),
50
+ )
51
+ verify_workstation.add_argument("--image", required=True)
52
+ verify_workstation.add_argument("--json", action="store_true")
36
53
  return parser
37
54
 
38
55
 
@@ -45,6 +62,18 @@ def main(argv: list[str] | None = None) -> int:
45
62
  as_json=args.json,
46
63
  )
47
64
  return 0
65
+ if args.command == "verify-workstation":
66
+ _print(
67
+ verify_workstation_registry_access(args.image),
68
+ as_json=args.json,
69
+ )
70
+ return 0
71
+ except RegistryCapabilityError as error:
72
+ if getattr(args, "json", False):
73
+ _print(error.result, as_json=True)
74
+ else:
75
+ print(f"ERROR: {error}", file=sys.stderr)
76
+ return 2
48
77
  except DeploymentKitError as error:
49
78
  print(f"ERROR: {error}", file=sys.stderr)
50
79
  return 2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mriekki/project-forge",
3
- "version": "4.7.0",
3
+ "version": "4.8.1",
4
4
  "description": "Public consumer distribution and updater for Project Forge engineering assets.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/cli.mjs CHANGED
@@ -44,6 +44,11 @@ function inferConsumer(root) {
44
44
  return path.basename(root);
45
45
  }
46
46
 
47
+ export function componentRevisionForDisplay(component) {
48
+ const revision = component?.revision ?? component?.commit;
49
+ return typeof revision === 'string' && revision ? revision : '<unknown revision>';
50
+ }
51
+
47
52
  function printPlan(plan, bootstrapMigrations = { rows: [], unsafe: [] }) {
48
53
  console.log(`Project Forge profile: ${plan.profileName}`);
49
54
  if (bootstrapMigrations.rows.length) {
@@ -51,7 +56,7 @@ function printPlan(plan, bootstrapMigrations = { rows: [], unsafe: [] }) {
51
56
  for (const row of bootstrapMigrations.rows) console.log(`${row.state.padEnd(30)} ${row.path}`);
52
57
  }
53
58
  for (const component of plan.plans) {
54
- console.log(`\n${component.componentId.toUpperCase()} ${component.component.commit}`);
59
+ console.log(`\n${component.componentId.toUpperCase()} ${componentRevisionForDisplay(component.component)}`);
55
60
  for (const row of component.rows) console.log(`${row.state.padEnd(20)} ${row.path}`);
56
61
  for (const retired of component.retired) console.log(`${retired.state.padEnd(20)} ${retired.path}`);
57
62
  }
@@ -112,7 +117,7 @@ export async function run(argv = process.argv.slice(2), packageData = loadPackag
112
117
  console.log(`Available package: ${packageData.pkg.name}@${packageData.pkg.version}`);
113
118
  console.log(`Profile: ${installed.profile}`);
114
119
  for (const [name, component] of Object.entries(installed.components ?? {})) {
115
- console.log(`${name}: ${component.version ? component.version + ' @ ' : ''}${component.commit}`);
120
+ console.log(`${name}: ${component.version ? component.version + ' @ ' : ''}${componentRevisionForDisplay(component)}`);
116
121
  }
117
122
  return 0;
118
123
  }