@erclx/canon 5.1.0 → 5.2.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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "canon",
3
3
  "description": "Automated governance, versioning, and discovery tools for Claude Code.",
4
- "version": "5.1.0",
4
+ "version": "5.2.0",
5
5
  "author": {
6
6
  "name": "Eric Le",
7
7
  "url": "https://github.com/erclx"
@@ -141,3 +141,9 @@ The report carries three states and never changes an exit code. `behind` names
141
141
  reason the registry could not be reached. Branch on the `skew.state` field in
142
142
  the JSON record rather than on the exit, since an offline machine has to read as
143
143
  unmeasured rather than as a failing check.
144
+
145
+ `canon upgrade --json` carries one further state, `pending`, that the skew report
146
+ does not. It means the checkout at the working directory holds a release npm does
147
+ not serve yet, so there is nothing to install. The record names that version in
148
+ `checkout` beside `latest`, exits 0, and its `message` starts `CLI stays at`. A
149
+ directory outside any canon checkout never reads `pending`.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@erclx/canon",
3
3
  "type": "module",
4
- "version": "5.1.0",
4
+ "version": "5.2.0",
5
5
  "description": "Infrastructure and quality tooling for developer workflows",
6
6
  "license": "MIT",
7
7
  "bin": {
@@ -43,6 +43,6 @@ export function matchInstall(
43
43
 
44
44
  export function updatedMessage(before: string, after: string): string {
45
45
  return after === before
46
- ? `Reinstalled ${after}, unchanged.`
47
- : `Updated ${before} to ${after}.`
46
+ ? `Plugin reinstalled ${after}, unchanged.`
47
+ : `Plugin updated ${before} to ${after}.`
48
48
  }
@@ -1,6 +1,6 @@
1
1
  import type { Command } from 'commander'
2
2
  import { execa } from 'execa'
3
- import { PROJECT_ROOT } from '@/project-root'
3
+ import { findCheckoutMismatch, PROJECT_ROOT } from '@/project-root'
4
4
  import {
5
5
  frameError,
6
6
  intro,
@@ -10,6 +10,7 @@ import {
10
10
  outro,
11
11
  select,
12
12
  } from '@/ui'
13
+ import { compareVersions, parseVersion } from '@/version/compare'
13
14
  import { readInstalled, UNKNOWN_LABEL } from '@/version/installed'
14
15
  import { detectManager, installCommand, type Manager } from '@/version/manager'
15
16
  import {
@@ -30,7 +31,9 @@ interface UpgradeRecord {
30
31
  readonly before: string
31
32
  readonly after?: string
32
33
  readonly latest?: string
33
- readonly state: 'upgraded' | 'current' | 'cancelled' | 'refused'
34
+ /** The version of the checkout at cwd, present only on `pending`. */
35
+ readonly checkout?: string
36
+ readonly state: 'upgraded' | 'current' | 'pending' | 'cancelled' | 'refused'
34
37
  readonly reason?: string
35
38
  /**
36
39
  * One rendered line for a caller that reports the outcome without parsing
@@ -117,7 +120,24 @@ async function runUpgrade(opts: UpgradeOptions): Promise<number> {
117
120
  logStep('Published')
118
121
  logInfo(describeSkew(skew))
119
122
 
123
+ const checkout = readCheckoutVersion()
124
+ const latest = latestOf(skew)
125
+
120
126
  if (skew.state === 'current') {
127
+ const note = pendingNote(latest, checkout)
128
+ if (note !== undefined && checkout !== undefined) {
129
+ logWarn(note)
130
+ outro()
131
+ emit(opts, {
132
+ ...base(before, manager, command, skew),
133
+ after: before,
134
+ checkout,
135
+ state: 'pending',
136
+ message: `CLI stays at ${before}. ${note}`,
137
+ })
138
+ return 0
139
+ }
140
+
121
141
  outro()
122
142
  emit(opts, {
123
143
  ...base(before, manager, command, skew),
@@ -128,7 +148,7 @@ async function runUpgrade(opts: UpgradeOptions): Promise<number> {
128
148
  return 0
129
149
  }
130
150
 
131
- return await applyUpgrade(opts, before, skew, manager, command)
151
+ return await applyUpgrade(opts, before, skew, manager, command, checkout)
132
152
  }
133
153
 
134
154
  /**
@@ -147,6 +167,7 @@ async function applyUpgrade(
147
167
  skew: SkewReport,
148
168
  manager: Manager,
149
169
  command: readonly string[],
170
+ checkout: string | undefined,
150
171
  ): Promise<number> {
151
172
  const proceed = await select({
152
173
  message: `Run \`${command.join(' ')}\`?`,
@@ -199,15 +220,50 @@ async function applyUpgrade(
199
220
  ...base(before, manager, command, skew),
200
221
  after,
201
222
  state: 'upgraded',
202
- message: upgradedMessage(before, after),
223
+ message: [upgradedMessage(before, after), pendingNote(after, checkout)]
224
+ .filter((part) => part !== undefined)
225
+ .join(' '),
203
226
  })
204
227
  return 0
205
228
  }
206
229
 
230
+ /**
231
+ * The version of the checkout the caller stands in, read off cwd rather than
232
+ * off `PROJECT_ROOT`, which is the installed package a bare `canon` resolves
233
+ * to. A target project sits in no canon checkout, so this reads nothing there
234
+ * and the verb behaves as it did.
235
+ */
236
+ function readCheckoutVersion(): string | undefined {
237
+ const dir = findCheckoutMismatch(process.cwd())
238
+ return dir === undefined ? undefined : readInstalled(dir).version
239
+ }
240
+
241
+ /**
242
+ * The clause a caller appends when the checkout carries a release npm does not
243
+ * serve yet. A pull lands the release commit before the publish job finishes,
244
+ * so for a few minutes the checkout leads the registry, and a release that
245
+ * never publishes keeps it leading. The wording names the served version
246
+ * rather than promising a publish for that reason. `undefined` covers every
247
+ * case with nothing to say, including a version that does not parse.
248
+ */
249
+ export function pendingNote(
250
+ served: string | undefined,
251
+ checkout: string | undefined,
252
+ ): string | undefined {
253
+ if (served === undefined || checkout === undefined) return undefined
254
+
255
+ const local = parseVersion(checkout)
256
+ const published = parseVersion(served)
257
+ if (local === undefined || published === undefined) return undefined
258
+ if (compareVersions(local, published) <= 0) return undefined
259
+
260
+ return `The checkout is at ${checkout} and npm serves ${served}.`
261
+ }
262
+
207
263
  export function upgradedMessage(before: string, after: string): string {
208
264
  return after === before
209
- ? `Reinstalled ${after}, unchanged.`
210
- : `Upgraded ${before} to ${after}.`
265
+ ? `CLI reinstalled ${after}, unchanged.`
266
+ : `CLI upgraded ${before} to ${after}.`
211
267
  }
212
268
 
213
269
  function base(