uniweb 0.64.1 → 0.66.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.
- package/package.json +7 -7
- package/partials/agents.md +19 -19
- package/src/backend/client.js +61 -51
- package/src/backend/foundation-bring-along.js +154 -97
- package/src/backend/site-sync.js +237 -243
- package/src/backend/workspace.js +199 -0
- package/src/commands/clone.js +20 -30
- package/src/commands/deploy.js +9 -7
- package/src/commands/publish.js +37 -43
- package/src/commands/pull.js +232 -81
- package/src/commands/push.js +36 -45
- package/src/commands/status.js +24 -15
- package/src/commands/validate.js +16 -12
- package/src/framework-index.json +4 -4
- package/src/index.js +39 -27
- package/src/utils/conformance.js +68 -10
- package/src/utils/flag-guard.js +12 -9
- package/src/utils/git.js +48 -1
- package/src/utils/pull-written.js +5 -0
- package/src/utils/registry-auth.js +75 -16
- package/src/utils/registry-orgs.js +2 -2
- package/src/utils/semver-precedence.js +23 -0
|
@@ -5,11 +5,29 @@
|
|
|
5
5
|
* A publish must never ship a site pointing at stale or missing foundation code
|
|
6
6
|
* (the footgun: a site goes live referencing a version the catalog doesn't
|
|
7
7
|
* have). So when the site references a LOCAL foundation, the verb fingerprints it
|
|
8
|
-
* and reconciles with the catalog
|
|
8
|
+
* and reconciles with the catalog (§4):
|
|
9
9
|
*
|
|
10
|
-
* | version not yet registered
|
|
11
|
-
* | registered, code unchanged
|
|
12
|
-
* | registered, code CHANGED
|
|
10
|
+
* | version not yet registered | release it |
|
|
11
|
+
* | registered, code unchanged | skip the release (digest match) |
|
|
12
|
+
* | registered, code CHANGED | release it under the next version |
|
|
13
|
+
* | a NEWER version is registered | stop — `--bump` releases above it |
|
|
14
|
+
*
|
|
15
|
+
* ⭐ **Changed code is released by default** *[Diego, 2026-09-24]*. A site whose project
|
|
16
|
+
* holds its own foundation is a content+code bundle — *"a developer is used to code
|
|
17
|
+
* going with the site, so they don't think of bumping the code of a site"* — and the
|
|
18
|
+
* version there decides nothing: the only site reading it is the one being pushed.
|
|
19
|
+
* Since a registered version is immutable, the change goes under the next version
|
|
20
|
+
* above the registered one, written into the package's `package.json` first. ⛔ This
|
|
21
|
+
* reverses the version-bump release gate of 2026-06-23, whose reasoning — a deliberate,
|
|
22
|
+
* npm-style release — fits a foundation other sites use as a product; those sites pin
|
|
23
|
+
* a catalog ref and never reach this code. (A library's own demo site, which names it
|
|
24
|
+
* by path, does — whether such a foundation should declare its releases deliberate is
|
|
25
|
+
* open.)
|
|
26
|
+
*
|
|
27
|
+
* ⛔ **A NEWER registered version still stops it.** It was released from another copy
|
|
28
|
+
* of the project, whose code this one may not have, and releasing above it would make
|
|
29
|
+
* the older code the newest — the code twin of push's content staleness gate. `--bump`
|
|
30
|
+
* releases above it anyway; `--no-release` ships the content bound to it.
|
|
13
31
|
*
|
|
14
32
|
* The freshness signal is the backend-stored, framework-computed digest (§4.1):
|
|
15
33
|
* no local state, multi-machine-safe. When the site references a published
|
|
@@ -44,7 +62,11 @@ import {
|
|
|
44
62
|
} from '@uniweb/build'
|
|
45
63
|
import { computeFoundationDigest } from '../utils/code-upload.js'
|
|
46
64
|
import { isNonInteractive } from '../utils/interactive.js'
|
|
47
|
-
import {
|
|
65
|
+
import { writeJsonPreservingStyle } from '../utils/json-file.js'
|
|
66
|
+
import {
|
|
67
|
+
compareSemverPrecedence,
|
|
68
|
+
nextVersionAbove
|
|
69
|
+
} from '../utils/semver-precedence.js'
|
|
48
70
|
|
|
49
71
|
/**
|
|
50
72
|
* Resolve the site's LOCAL foundation — the one publish should bring along — or
|
|
@@ -160,6 +182,14 @@ function readPkgField(dir, field) {
|
|
|
160
182
|
}
|
|
161
183
|
}
|
|
162
184
|
|
|
185
|
+
// Set the package's version, changing nothing else in the file — its key order,
|
|
186
|
+
// indentation and trailing newline stay as they were, so the diff is one line.
|
|
187
|
+
function writePkgVersion(dir, version) {
|
|
188
|
+
const path = join(dir, 'package.json')
|
|
189
|
+
const src = readFileSync(path, 'utf8')
|
|
190
|
+
writeJsonPreservingStyle(path, { ...JSON.parse(src), version }, src)
|
|
191
|
+
}
|
|
192
|
+
|
|
163
193
|
// What travels to the spawned `uniweb register` / `build` on the command line: only
|
|
164
194
|
// --non-interactive. The BACKEND travels in the child's environment
|
|
165
195
|
// (releaseFoundation), and the SESSION is the shared session file — or UNIWEB_TOKEN,
|
|
@@ -232,7 +262,9 @@ export async function bringFoundationAlong({
|
|
|
232
262
|
* @param {object} o
|
|
233
263
|
* @param {{dir: string, version: string|null}} o.local
|
|
234
264
|
* @param {'foundation'|'extension'} o.kind
|
|
235
|
-
* @returns {Promise<{ released: boolean, proceed: boolean, ref: string|null }>}
|
|
265
|
+
* @returns {Promise<{ released: boolean, proceed: boolean, ref: string|null, bumped?: string }>}
|
|
266
|
+
* `bumped` is the version written into package.json when changed code was released
|
|
267
|
+
* under the next version.
|
|
236
268
|
*/
|
|
237
269
|
async function bringLocalCodeAlong({
|
|
238
270
|
client,
|
|
@@ -271,6 +303,19 @@ async function bringLocalCodeAlong({
|
|
|
271
303
|
// says (see the `!reg` branch).
|
|
272
304
|
const noRelease = args.includes('--no-release')
|
|
273
305
|
|
|
306
|
+
// `--bump`: when the registry holds a version NEWER than the local one, release the
|
|
307
|
+
// local code above it instead of stopping. That is all it does — changed code under
|
|
308
|
+
// the registered version is released without it, and unchanged code never is — so a
|
|
309
|
+
// script can pass it on every run.
|
|
310
|
+
const bump = args.includes('--bump')
|
|
311
|
+
|
|
312
|
+
if (bump && noRelease) {
|
|
313
|
+
say.err(
|
|
314
|
+
'`--bump` releases your local changes and `--no-release` ships without them — pass one of the two.'
|
|
315
|
+
)
|
|
316
|
+
return { released: false, proceed: false, refused: true, ref: null }
|
|
317
|
+
}
|
|
318
|
+
|
|
274
319
|
// The pinned ref to stamp on the pushed site — read at RETURN time (after any
|
|
275
320
|
// release), so it reflects the released version, the scope register derived and
|
|
276
321
|
// the name it asked for. null when no scoped ref can be formed (then the site.yml
|
|
@@ -336,6 +381,36 @@ async function bringLocalCodeAlong({
|
|
|
336
381
|
}
|
|
337
382
|
}
|
|
338
383
|
|
|
384
|
+
// The next version above the registered one — what changed code is released under
|
|
385
|
+
// when the local version is taken. Null when the registered version is not SemVer,
|
|
386
|
+
// and then there is nothing to count up from.
|
|
387
|
+
const bumpTo = nextVersionAbove(reg.latest_version)
|
|
388
|
+
|
|
389
|
+
// Write it into package.json, then release. `register` rebuilds on its own when the
|
|
390
|
+
// built schema's version no longer matches package.json, so the release carries it.
|
|
391
|
+
// `optOut` names `--no-release` for a release nobody asked for by flag.
|
|
392
|
+
//
|
|
393
|
+
// ⚖️ A failed release leaves the new version in the file, and that is the useful
|
|
394
|
+
// state: the next run finds a local version above the registered one and releases
|
|
395
|
+
// it, with nothing to redo.
|
|
396
|
+
const releaseAsNext = async (why, { optOut = false } = {}) => {
|
|
397
|
+
if (!bumpTo) {
|
|
398
|
+
say.err(
|
|
399
|
+
`Cannot release ${label} under a new version: the registered ${reg.latest_version} is not a SemVer version.`
|
|
400
|
+
)
|
|
401
|
+
say.dim(
|
|
402
|
+
`Nothing was sent. Set a version above it in the ${kind}'s package.json, then re-run \`uniweb ${verb}\`; or \`uniweb ${verb} --no-release\` sends content bound to the released ${reg.latest_version}.`
|
|
403
|
+
)
|
|
404
|
+
return { released: false, proceed: false, refused: true, ref: null }
|
|
405
|
+
}
|
|
406
|
+
writePkgVersion(local.dir, bumpTo)
|
|
407
|
+
say.info(`Releasing the ${kind} ${scopedName || kind} as ${bumpTo} — ${why}…`)
|
|
408
|
+
const released = releaseFoundation(local, args, cliBin, say, client?.origin)
|
|
409
|
+
say.dim(`The ${kind}'s package.json now says ${bumpTo} — commit it.`)
|
|
410
|
+
if (optOut) say.dim('To send content without releasing code, pass `--no-release`.')
|
|
411
|
+
return { released, proceed: true, bumped: bumpTo, ref: await pinnedRef() }
|
|
412
|
+
}
|
|
413
|
+
|
|
339
414
|
// Registered — fingerprint the local build and compare. Build first so the
|
|
340
415
|
// digest reflects current source (idempotent: a no-op when already fresh).
|
|
341
416
|
buildFoundation(local, cliBin)
|
|
@@ -348,38 +423,70 @@ async function bringLocalCodeAlong({
|
|
|
348
423
|
return { released: false, proceed: true, ref: await pinnedRef() }
|
|
349
424
|
}
|
|
350
425
|
|
|
351
|
-
// A different version locally
|
|
426
|
+
// A different version locally.
|
|
352
427
|
if (local.version && local.version !== reg.latest_version) {
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
428
|
+
const order = compareSemverPrecedence(local.version, reg.latest_version)
|
|
429
|
+
// Equal in precedence (build metadata apart) is the registered version itself, and
|
|
430
|
+
// is treated as it, below.
|
|
431
|
+
if (order !== 0) {
|
|
432
|
+
if (noRelease) {
|
|
433
|
+
// ⚠️ Bind to the REGISTERED version, not the local one. `pinnedRef()` would
|
|
434
|
+
// return the bumped-but-unreleased `local.version` here — a ref no deployment
|
|
435
|
+
// can serve, which is the very failure this flag must not create.
|
|
436
|
+
say.info(
|
|
437
|
+
`Keeping the released ${kind} ${reg.latest_version} — local ${local.version} not released (\`--no-release\`).`
|
|
438
|
+
)
|
|
439
|
+
return { released: false, proceed: true, ref: registeredRef(reg) }
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// ⛔ OLDER than the registered latest: that version was released from another
|
|
443
|
+
// copy of the project, and this one may not have its code. Releasing above it
|
|
444
|
+
// would make this code the newest and bind the site to it — a teammate's release
|
|
445
|
+
// undone by a push that never saw it. So it stops, and names the ways on.
|
|
446
|
+
//
|
|
447
|
+
// Until 2026-09-24 it was submitted, the registry deciding: it took a version it
|
|
448
|
+
// already held with this very code (a resume), and refused anything else. Only
|
|
449
|
+
// the latest version's digest is readable, and the digest folds in the version,
|
|
450
|
+
// so "this code, under an older version" cannot be told apart from here.
|
|
451
|
+
if (order === -1) {
|
|
452
|
+
if (bump) {
|
|
453
|
+
say.warn(
|
|
454
|
+
`The registry holds ${reg.latest_version}, newer than your ${label} — releasing your code above it (\`--bump\`). If ${reg.latest_version} is someone else's release, its changes are not in yours.`
|
|
455
|
+
)
|
|
456
|
+
return releaseAsNext(`above the registered ${reg.latest_version}`)
|
|
457
|
+
}
|
|
458
|
+
say.err(
|
|
459
|
+
`The registry holds ${kind} ${scopedName || kind} ${reg.latest_version}, newer than your ${local.version} — a release this copy does not have.`
|
|
460
|
+
)
|
|
461
|
+
say.dim(
|
|
462
|
+
`Nothing was sent. Pull the change that released it, then re-run \`uniweb ${verb}\`; or:`
|
|
463
|
+
)
|
|
464
|
+
if (bumpTo) {
|
|
465
|
+
say.dim(` • \`uniweb ${verb} --bump\` — releases your code as ${bumpTo}, above it`)
|
|
466
|
+
}
|
|
467
|
+
say.dim(
|
|
468
|
+
` • \`uniweb ${verb} --no-release\` — sends content bound to the released ${reg.latest_version}`
|
|
469
|
+
)
|
|
470
|
+
return { released: false, proceed: false, refused: true, ref: null }
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// Newer than anything registered — a new version, released as it stands. Not
|
|
474
|
+
// SemVer (`order === null`) — nothing to compare, so the same, and the registry
|
|
475
|
+
// decides; `register` prints its answer if it is a no.
|
|
357
476
|
say.info(
|
|
358
|
-
|
|
477
|
+
order === 1
|
|
478
|
+
? `Releasing the ${kind} ${label} (new version; registered latest is ${reg.latest_version})…`
|
|
479
|
+
: `Releasing the ${kind} ${label} (registered latest is ${reg.latest_version})…`
|
|
359
480
|
)
|
|
360
|
-
return {
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
// does not refuse: it submits, says what will decide, and `register` prints
|
|
366
|
-
// the registry's answer if it is a no.
|
|
367
|
-
const order = compareSemverPrecedence(local.version, reg.latest_version)
|
|
368
|
-
say.info(
|
|
369
|
-
order === 1
|
|
370
|
-
? `Releasing the ${kind} ${label} (new version; registered latest is ${reg.latest_version})…`
|
|
371
|
-
: order === null
|
|
372
|
-
? `Releasing the ${kind} ${label} (registered latest is ${reg.latest_version})…`
|
|
373
|
-
: `Releasing the ${kind} ${label} — not newer than the registered latest ${reg.latest_version}, so the registry takes it only if ${local.version} is already registered with this code…`
|
|
374
|
-
)
|
|
375
|
-
return {
|
|
376
|
-
released: releaseFoundation(local, args, cliBin, say, client?.origin),
|
|
377
|
-
proceed: true,
|
|
378
|
-
ref: await pinnedRef()
|
|
481
|
+
return {
|
|
482
|
+
released: releaseFoundation(local, args, cliBin, say, client?.origin),
|
|
483
|
+
proceed: true,
|
|
484
|
+
ref: await pinnedRef()
|
|
485
|
+
}
|
|
379
486
|
}
|
|
380
487
|
}
|
|
381
488
|
|
|
382
|
-
//
|
|
489
|
+
// The registered version — but the digest differs, or the backend can't confirm it.
|
|
383
490
|
if (!reg.digest) {
|
|
384
491
|
// Degrade: the backend doesn't return the stored digest yet, so we can't
|
|
385
492
|
// be sure the registered version matches local. Offer to re-deliver.
|
|
@@ -393,8 +500,10 @@ async function bringLocalCodeAlong({
|
|
|
393
500
|
`Can't verify the registered ${label} matches your local copy (backend returned no digest).`
|
|
394
501
|
)
|
|
395
502
|
if (skipPrompts || isNonInteractive(args)) {
|
|
503
|
+
// ⚖️ Not released: nothing says the code changed, and releasing on no evidence
|
|
504
|
+
// would mint a version on every push against such a backend.
|
|
396
505
|
say.dim(
|
|
397
|
-
|
|
506
|
+
`Proceeding without re-releasing — to release a change, set a higher version in the ${kind}'s package.json.`
|
|
398
507
|
)
|
|
399
508
|
return { released: false, proceed: true, ref: await pinnedRef() }
|
|
400
509
|
}
|
|
@@ -411,10 +520,8 @@ async function bringLocalCodeAlong({
|
|
|
411
520
|
return { released: false, proceed: true, ref: await pinnedRef() }
|
|
412
521
|
}
|
|
413
522
|
|
|
414
|
-
//
|
|
415
|
-
//
|
|
416
|
-
// the deliberate release gate is a version bump (§3.1).
|
|
417
|
-
// Asked for explicitly: the case this flag exists to name.
|
|
523
|
+
// The code changed and the version did not. A registered version is immutable, so
|
|
524
|
+
// the change is released under the next one.
|
|
418
525
|
if (noRelease) {
|
|
419
526
|
say.info(
|
|
420
527
|
`Keeping the released ${kind} ${reg.latest_version} — your local changes are not released (\`--no-release\`).`
|
|
@@ -422,67 +529,17 @@ async function bringLocalCodeAlong({
|
|
|
422
529
|
return { released: false, proceed: true, ref: registeredRef(reg) }
|
|
423
530
|
}
|
|
424
531
|
|
|
425
|
-
// ⭐
|
|
532
|
+
// ⭐ RELEASED BY DEFAULT — for a person and an agent alike, with no question.
|
|
426
533
|
//
|
|
427
|
-
//
|
|
428
|
-
//
|
|
429
|
-
//
|
|
430
|
-
//
|
|
431
|
-
//
|
|
432
|
-
//
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
// cheap outcome is one more cycle and the expensive one is a silent wrong success
|
|
437
|
-
// it will not look at again. Refusing costs an agent a retry; proceeding costs it
|
|
438
|
-
// a false completion report and a live site nobody notices is stale.
|
|
439
|
-
|
|
440
|
-
// 1. Explicit consent — proceed, but at WARN. `dim` is what we print for things
|
|
441
|
-
// nobody needs to read, and "your changes are not live" is not that.
|
|
442
|
-
if (skipPrompts) {
|
|
443
|
-
say.warn(
|
|
444
|
-
`Local ${label} differs from the registered ${reg.latest_version} and the version wasn't bumped — ` +
|
|
445
|
-
`shipping against the registered code. Your local changes will NOT be live.`
|
|
446
|
-
)
|
|
447
|
-
return { released: false, proceed: true, ref: await pinnedRef() }
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
// 2. Nobody to ask. Refuse, and name both real options as runnable commands so
|
|
451
|
-
// the caller's next step needs no interpretation. `refused` marks this as a
|
|
452
|
-
// hard stop rather than a human's "no", which callers map to different exits.
|
|
453
|
-
if (isNonInteractive(args)) {
|
|
454
|
-
say.err(
|
|
455
|
-
`Local ${kind} ${label} differs from the registered ${reg.latest_version}, and the version wasn't bumped.`
|
|
456
|
-
)
|
|
457
|
-
say.dim(`Nothing was sent. Either release the change, or ship without it:`)
|
|
458
|
-
say.dim(` • bump the ${kind}'s version in package.json, then re-run \`uniweb ${verb}\``)
|
|
459
|
-
// Teach the flag that NAMES this, not `--yes`. Both work, but `--yes` means "do
|
|
460
|
-
// not ask me" and only does this as a side effect — pointing a stuck user at it
|
|
461
|
-
// teaches a blunt instrument for a precise job.
|
|
462
|
-
say.dim(
|
|
463
|
-
` • \`uniweb ${verb} --no-release\` — sends content bound to the released ${reg.latest_version}`
|
|
464
|
-
)
|
|
465
|
-
return { released: false, proceed: false, refused: true, ref: null }
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
// 3. A human is here. State it, then ask.
|
|
469
|
-
say.warn(
|
|
470
|
-
`Your local ${label} differs from the registered version ${reg.latest_version}, but the version wasn't bumped.`
|
|
471
|
-
)
|
|
472
|
-
say.dim(
|
|
473
|
-
`A registered version is immutable — bump the ${kind}'s version to release the change.`
|
|
474
|
-
)
|
|
475
|
-
const proceed = await confirm(
|
|
476
|
-
`Continue with the already-registered ${reg.latest_version} anyway?`,
|
|
477
|
-
false
|
|
478
|
-
)
|
|
479
|
-
if (!proceed) {
|
|
480
|
-
say.info(
|
|
481
|
-
`Aborted — nothing was sent. Bump the ${kind} version, then re-run \`uniweb ${verb}\`.`
|
|
482
|
-
)
|
|
483
|
-
return { released: false, proceed: false, ref: null }
|
|
484
|
-
}
|
|
485
|
-
return { released: false, proceed: true, ref: await pinnedRef() }
|
|
534
|
+
// Until 2026-09-24 this stopped: a terminal was asked, a run without one refused, and
|
|
535
|
+
// `--yes` shipped the REGISTERED code. That guarded against a site bound to code that
|
|
536
|
+
// was not the working tree's — which is exactly what releasing prevents: the site is
|
|
537
|
+
// bound to the code just released, as it is to the content pushed with it. `--yes`
|
|
538
|
+
// has nothing left to consent to here, and a question whose answer is nearly always
|
|
539
|
+
// yes teaches people to stop reading it.
|
|
540
|
+
return releaseAsNext(`its code changed since ${reg.latest_version} was registered`, {
|
|
541
|
+
optOut: true
|
|
542
|
+
})
|
|
486
543
|
}
|
|
487
544
|
|
|
488
545
|
// Build the foundation so its dist/ can be fingerprinted. Idempotent — the
|