uniweb 0.24.0 → 0.25.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "uniweb",
3
- "version": "0.24.0",
3
+ "version": "0.25.0",
4
4
  "description": "Create structured Vite + React sites with content/code separation",
5
5
  "type": "module",
6
6
  "bin": {
@@ -113,13 +113,18 @@ function flagValue(args, name) {
113
113
  return null
114
114
  }
115
115
 
116
- export async function push(args = []) {
116
+ export async function push(args = [], deps = {}) {
117
117
  // An unrecognized flag is invisible to a literal scan, so it silently keeps the
118
118
  // default — including for --backend, where the default can be production.
119
- const bad = checkFlags('push', args)
120
- if (bad) {
121
- error(bad.message)
122
- return { exitCode: 2 }
119
+ //
120
+ // `sync` validates the UNION of its two halves' flags and forwards raw argv, so a
121
+ // legal `uniweb sync --no-git` would be rejected here. It passes `skipFlagCheck`.
122
+ if (!deps.skipFlagCheck) {
123
+ const bad = checkFlags('push', args)
124
+ if (bad) {
125
+ error(bad.message)
126
+ return { exitCode: 2 }
127
+ }
123
128
  }
124
129
  const dryRun = args.includes('--dry-run')
125
130
  const output = flagValue(args, '-o') || flagValue(args, '--output')
@@ -46,6 +46,7 @@ import {
46
46
  headProvenance
47
47
  } from '../utils/git.js'
48
48
  import { probeUnpushed } from '../backend/site-sync.js'
49
+ import { checkFlags } from '../utils/flag-guard.js'
49
50
 
50
51
  const c = {
51
52
  reset: '\x1b[0m',
@@ -98,6 +99,21 @@ function siteContentUuid(siteDir) {
98
99
  * network or a real backend).
99
100
  */
100
101
  export async function refresh(args = [], deps = {}) {
102
+ // An unrecognized flag is invisible to a literal scan, so it silently keeps the
103
+ // default — and for `--backend` the default can be production. This verb never
104
+ // sent data anywhere, which is why it went unguarded; but it DELEGATES to `pull`,
105
+ // and a mistyped `--backend` is not forwarded, so the pull runs against whatever
106
+ // the origin ladder resolves instead of the backend the user named.
107
+ //
108
+ // `sync` validates the UNION of both halves and forwards raw argv, so a legal
109
+ // `uniweb sync --force` would be rejected here. It passes `skipFlagCheck`.
110
+ if (!deps.skipFlagCheck) {
111
+ const bad = checkFlags('refresh', args)
112
+ if (bad) {
113
+ say.err(bad.message)
114
+ return { exitCode: 2 }
115
+ }
116
+ }
101
117
  const skipGit = args.includes('--no-git')
102
118
  const skipBackend = args.includes('--no-backend')
103
119
  const resolveSite = deps.resolveSiteDir || resolveSiteDir
@@ -38,6 +38,8 @@
38
38
  * uniweb sync --backend <url> override the backend origin
39
39
  */
40
40
 
41
+ import { checkFlags } from '../utils/flag-guard.js'
42
+
41
43
  const c = {
42
44
  reset: '\x1b[0m',
43
45
  dim: '\x1b[2m',
@@ -55,6 +57,18 @@ const say = {
55
57
  * @param {object} [deps] - injectable seams for testing: `refresh`, `push`.
56
58
  */
57
59
  export async function sync(args = [], deps = {}) {
60
+ // THE ONE flag check for the whole composite, against the union of both halves'
61
+ // accepted sets (`VERBS.sync`, derived in flag-guard.js). It has to happen here
62
+ // and only here: each half's set is a strict subset, so `push` would reject
63
+ // `--no-git` and `refresh` would reject `--force` — both legal on `sync`. Hence
64
+ // `skipFlagCheck` below; the halves are not being trusted, they are being told
65
+ // the caller already validated a superset.
66
+ const bad = checkFlags('sync', args)
67
+ if (bad) {
68
+ say.err(bad.message)
69
+ return { exitCode: 2 }
70
+ }
71
+
58
72
  const refresh = deps.refresh || (await import('./refresh.js')).refresh
59
73
  const push = deps.push || (await import('./push.js')).push
60
74
 
@@ -63,7 +77,7 @@ export async function sync(args = [], deps = {}) {
63
77
  // to send — the same word meaning opposite things on the two halves.
64
78
  const refreshArgs = args.filter((a) => a !== '--force')
65
79
 
66
- const r = await refresh(refreshArgs)
80
+ const r = await refresh(refreshArgs, { skipFlagCheck: true })
67
81
  if (r?.exitCode) {
68
82
  // refresh already explained itself — conflicts, or a git failure. Adding a
69
83
  // second summary here would just bury it.
@@ -73,7 +87,7 @@ export async function sync(args = [], deps = {}) {
73
87
 
74
88
  console.log('')
75
89
  say.info('Pushing your changes…')
76
- const p = await push(args)
90
+ const p = await push(args, { skipFlagCheck: true })
77
91
  return { exitCode: p?.exitCode ?? 0 }
78
92
  }
79
93
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
- "generatedAt": "2026-08-16T05:48:07.516Z",
3
+ "generatedAt": "2026-08-16T13:24:21.340Z",
4
4
  "packages": {
5
5
  "@uniweb/build": {
6
6
  "version": "0.24.0",
@@ -111,7 +111,7 @@
111
111
  "deps": []
112
112
  },
113
113
  "@uniweb/unipress": {
114
- "version": "0.8.9",
114
+ "version": "0.8.10",
115
115
  "path": "framework/unipress",
116
116
  "deps": [
117
117
  "@uniweb/build",
package/src/index.js CHANGED
@@ -1544,6 +1544,58 @@ ${colors.bright}Options:${colors.reset}
1544
1544
  In non-interactive mode (CI / no TTY), pass \`--token <bearer>\`, or set
1545
1545
  \`UNIWEB_USERNAME\` + \`UNIWEB_PASSWORD\`, or set \`UNIWEB_TOKEN\` (used per-command,
1546
1546
  not stored). Run \`uniweb logout\` to clear the stored session.
1547
+ `,
1548
+ refresh: `
1549
+ ${colors.cyan}${colors.bright}uniweb refresh${colors.reset} ${colors.dim}— Catch up with teammates AND app authors${colors.reset}
1550
+
1551
+ ${colors.bright}Usage:${colors.reset}
1552
+ uniweb refresh [options]
1553
+
1554
+ A developer on a synced site has TWO external sources: the git remote (teammates'
1555
+ commits) and the backend (content authors' edits in the app). Run only \`git pull\`
1556
+ and you miss every app edit; run only \`uniweb pull\` and you miss your teammates.
1557
+ This does both — git first, then \`pull --merge\`.
1558
+
1559
+ ${colors.bright}READ-ONLY.${colors.reset} It never pushes and cannot ship anything, so it is safe to run
1560
+ reflexively. Exits non-zero if a merge leaves conflicts.
1561
+
1562
+ ${colors.bright}Options:${colors.reset}
1563
+ --no-git Skip the git remote; backend only
1564
+ --no-backend Skip the backend; git only
1565
+ --backend <url> Override the backend origin
1566
+ --token <bearer> Read with this bearer; skips \`uniweb login\`
1567
+
1568
+ \`--force\` and \`--merge\` are NOT accepted here: this verb builds the delegated
1569
+ pull's arguments itself, and to pull \`--force\` means "discard my local work".
1570
+ Use \`uniweb sync\` when you also mean to share.
1571
+ `,
1572
+ sync: `
1573
+ ${colors.cyan}${colors.bright}uniweb sync${colors.reset} ${colors.dim}— Catch up, then share${colors.reset}
1574
+
1575
+ ${colors.bright}Usage:${colors.reset}
1576
+ uniweb sync [options]
1577
+
1578
+ \`uniweb refresh\` followed by \`uniweb push\`, and nothing more. It adds no
1579
+ guarantees of its own — refresh stops on conflicts, push refuses when the backend
1580
+ has moved under you, and sync inherits both. \`uniweb refresh && uniweb push\`
1581
+ behaves identically.
1582
+
1583
+ ${colors.bright}It does NOT publish.${colors.reset} Sync brings your copy and the backend DRAFT into
1584
+ agreement; going live stays a separate act (\`uniweb publish\`). So the worst case
1585
+ of an unintended sync is work-in-progress visible to authors, not shipped to
1586
+ visitors.
1587
+
1588
+ ${colors.bright}Options:${colors.reset}
1589
+ Accepts every option of both halves — see \`uniweb refresh --help\` and the push
1590
+ options in \`uniweb --help\`. The common ones:
1591
+
1592
+ --no-git Skip the git remote half of the refresh
1593
+ --force Overwrite upstream changes (reaches the PUSH half only)
1594
+ --backend <url> Override the backend origin
1595
+ --token <bearer> Auth bearer; skips \`uniweb login\`
1596
+
1597
+ \`--force\` means "overwrite upstream" to push but "discard my local work" to pull,
1598
+ so it is deliberately kept away from the refresh half.
1547
1599
  `,
1548
1600
  invite: `
1549
1601
  ${colors.cyan}${colors.bright}uniweb invite${colors.reset} ${colors.dim}— (reserved; not available on the new backend yet)${colors.reset}
@@ -85,9 +85,44 @@ const VERBS = {
85
85
  '--backend', '--json', '--registry', '--remote', '--token', '--dry-run',
86
86
  '--force', '--no-verify', '--no-validate', '--yes', '--org', '--as-org',
87
87
  ...VIA_DEPLOY
88
+ ],
89
+ /**
90
+ * `refresh` = `git pull`, then a DELEGATED `pull --merge`.
91
+ *
92
+ * It forwards exactly three flags to that pull — `--backend` / `--registry` /
93
+ * `--token`, via its own `collectPassthrough` — and constructs the rest of the
94
+ * argv itself. So pull's own flags (`--merge`, `--force`, `--no-delete`,
95
+ * `--no-prune`, `--content-only`, …) are NOT reachable from a `refresh` argv and
96
+ * are deliberately absent here. ⚠️ `--force` especially: `refresh` is read-only by
97
+ * design, and forwarding it would ask pull to DISCARD local work.
98
+ *
99
+ * The trailing group is inert on this verb but reachable through the
100
+ * `resolveSiteDir` / `probeUnpushed` imports — listed rather than filtered, per
101
+ * the VIA_DEPLOY note above, and required by `flag-guard-coverage.test.js`.
102
+ */
103
+ refresh: [
104
+ '--backend', '--no-backend', '--no-git', '--registry', '--token',
105
+ '--as-org', '--org', '--dry-run', '--no-validate', '--yes', ...VIA_DEPLOY
88
106
  ]
89
107
  }
90
108
 
109
+ /**
110
+ * `sync` is `refresh` + `push`, forwarding RAW argv to both halves — so its
111
+ * accepted set is exactly the UNION of theirs. Derived, never hand-written.
112
+ *
113
+ * Hand-writing it would rot the moment either half gained a flag, and it would rot
114
+ * in the expensive direction: `uniweb sync --no-git` and `uniweb sync --force` are
115
+ * both documented in `sync.js`, and a union that forgot either would reject a
116
+ * working command.
117
+ *
118
+ * ⭐ The union is also WHY `sync` checks and the two halves skip (`skipFlagCheck`,
119
+ * see `sync.js`). Each half's set is a strict subset of this one, so letting them
120
+ * re-check would have `push` reject `--no-git` and `refresh` reject `--force` —
121
+ * both legal on `sync`. Validating once, against the union, at the only layer that
122
+ * knows both halves are in play, is what makes the composite safe.
123
+ */
124
+ VERBS.sync = [...new Set([...VERBS.refresh, ...VERBS.push])]
125
+
91
126
  /**
92
127
  * The accepted set per verb: its own flags, plus the login-method flags every
93
128
  * backend verb can reach, plus the globals.