uniweb 0.12.4 → 0.12.6
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 +5 -5
- package/src/commands/deploy.js +50 -6
- package/src/commands/publish.js +220 -15
- package/src/framework-index.json +2 -2
- package/src/index.js +13 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "uniweb",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.6",
|
|
4
4
|
"description": "Create structured Vite + React sites with content/code separation",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -41,14 +41,14 @@
|
|
|
41
41
|
"js-yaml": "^4.1.0",
|
|
42
42
|
"prompts": "^2.4.2",
|
|
43
43
|
"tar": "^7.0.0",
|
|
44
|
-
"@uniweb/core": "0.7.9",
|
|
45
44
|
"@uniweb/kit": "0.9.9",
|
|
46
|
-
"@uniweb/runtime": "0.8.10"
|
|
45
|
+
"@uniweb/runtime": "0.8.10",
|
|
46
|
+
"@uniweb/core": "0.7.9"
|
|
47
47
|
},
|
|
48
48
|
"peerDependencies": {
|
|
49
49
|
"@uniweb/build": "0.13.3",
|
|
50
|
-
"@uniweb/
|
|
51
|
-
"@uniweb/
|
|
50
|
+
"@uniweb/semantic-parser": "1.1.16",
|
|
51
|
+
"@uniweb/content-reader": "1.1.9"
|
|
52
52
|
},
|
|
53
53
|
"peerDependenciesMeta": {
|
|
54
54
|
"@uniweb/build": {
|
package/src/commands/deploy.js
CHANGED
|
@@ -166,13 +166,36 @@ const say = {
|
|
|
166
166
|
dim: (m) => console.log(` ${c.dim}${m}${c.reset}`),
|
|
167
167
|
}
|
|
168
168
|
|
|
169
|
+
/**
|
|
170
|
+
* Read the git state for `dir`, scoped to that directory's history and
|
|
171
|
+
* working tree — NOT the whole repo's HEAD.
|
|
172
|
+
*
|
|
173
|
+
* `gitSha` : last commit that touched `dir` (`git log -1 -- .`).
|
|
174
|
+
* `gitDirty`: uncommitted changes inside `dir` only (`git status -- .`).
|
|
175
|
+
*
|
|
176
|
+
* Why scope it. In a multi-package monorepo, `git rev-parse HEAD` is
|
|
177
|
+
* the same value for every directory — the repo's current HEAD. That
|
|
178
|
+
* meant editing a SITE then deploying triggered the foundation's
|
|
179
|
+
* staleness check (its receipt's recorded sha didn't match the new
|
|
180
|
+
* repo HEAD), even though the foundation source was unchanged. The
|
|
181
|
+
* receipt's `publishedFromGitSha` field is per-foundation by design;
|
|
182
|
+
* the comparison side has to be too.
|
|
183
|
+
*
|
|
184
|
+
* If the path is outside a git repo, or has no commits touching it
|
|
185
|
+
* yet, the function returns `{ gitSha: null, gitDirty: false }` —
|
|
186
|
+
* same fallback shape as before.
|
|
187
|
+
*/
|
|
169
188
|
function readGitState(dir) {
|
|
170
189
|
try {
|
|
171
|
-
|
|
190
|
+
// `git log -1 --format=%H -- .` returns the SHA of the last
|
|
191
|
+
// commit that touched the cwd path. If no such commit exists
|
|
192
|
+
// yet (path was never committed), output is empty — caller
|
|
193
|
+
// treats null as "no published-from-sha to compare against."
|
|
194
|
+
const sha = execSync('git log -1 --format=%H -- .', {
|
|
172
195
|
cwd: dir,
|
|
173
196
|
stdio: ['ignore', 'pipe', 'ignore'],
|
|
174
197
|
}).toString().trim()
|
|
175
|
-
const status = execSync('git status --porcelain', {
|
|
198
|
+
const status = execSync('git status --porcelain -- .', {
|
|
176
199
|
cwd: dir,
|
|
177
200
|
stdio: ['ignore', 'pipe', 'ignore'],
|
|
178
201
|
}).toString()
|
|
@@ -231,10 +254,31 @@ async function inspectLocalFoundationReceipt(localPath, { dirtyAsStale, registry
|
|
|
231
254
|
return { stale: true, reason: 'foundation directory is not in a git repo or has no commits', receipt }
|
|
232
255
|
}
|
|
233
256
|
if (receipt.publishedFromGitSha && receipt.publishedFromGitSha !== gitSha) {
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
257
|
+
// Receipt's recorded sha differs from the foundation's per-directory
|
|
258
|
+
// last-touched commit. Normally that's "real" staleness — somebody
|
|
259
|
+
// committed changes to src/ that haven't been republished.
|
|
260
|
+
//
|
|
261
|
+
// Exception: when the publish was made FROM A DIRTY tree, the
|
|
262
|
+
// recorded sha is a checkpoint, not an identity. The published
|
|
263
|
+
// artifact reflects the committed state at that sha PLUS the
|
|
264
|
+
// uncommitted changes that were on disk when publish ran. After
|
|
265
|
+
// those changes get committed (a normal post-deploy housekeeping
|
|
266
|
+
// step — e.g., committing the auto-derived `uniweb.id`), the
|
|
267
|
+
// per-foundation sha moves forward, but the artifact upstream
|
|
268
|
+
// hasn't materially changed. Don't fire staleness on the sha
|
|
269
|
+
// alone in that case; let the dirty-tree check below do its job
|
|
270
|
+
// if the tree IS still dirty, and otherwise treat as fresh.
|
|
271
|
+
//
|
|
272
|
+
// Edge: if the user committed real source changes ON TOP of the
|
|
273
|
+
// auto-derive in the same commit, we won't detect that as stale
|
|
274
|
+
// here — the next publish would 409 against the registry though,
|
|
275
|
+
// surfacing the issue with a clear "bump the version" message.
|
|
276
|
+
if (!receipt.publishedFromGitDirty) {
|
|
277
|
+
return {
|
|
278
|
+
stale: true,
|
|
279
|
+
reason: `foundation has new commits since last publish (${receipt.publishedFromGitSha.slice(0, 7)} → ${gitSha.slice(0, 7)})`,
|
|
280
|
+
receipt,
|
|
281
|
+
}
|
|
238
282
|
}
|
|
239
283
|
}
|
|
240
284
|
if (gitDirty && dirtyAsStale) {
|
package/src/commands/publish.js
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
import { existsSync } from 'node:fs'
|
|
15
|
-
import { readFile, writeFile } from 'node:fs/promises'
|
|
15
|
+
import { readFile, writeFile, mkdir } from 'node:fs/promises'
|
|
16
16
|
import { resolve, join } from 'node:path'
|
|
17
17
|
import { execSync } from 'node:child_process'
|
|
18
18
|
|
|
@@ -188,13 +188,129 @@ export async function publish(args = []) {
|
|
|
188
188
|
process.exit(1)
|
|
189
189
|
}
|
|
190
190
|
|
|
191
|
-
// 2. Auto-build if dist/ is missing
|
|
191
|
+
// 2. Auto-build if dist/ is missing OR stale.
|
|
192
|
+
//
|
|
193
|
+
// "Stale" means the schema fingerprint baked into
|
|
194
|
+
// `dist/meta/schema.json::_self.version` doesn't match the user's
|
|
195
|
+
// current `package.json::version`. That happens when the user bumps
|
|
196
|
+
// the version and runs `uniweb publish` without rebuilding — the
|
|
197
|
+
// artifact in dist/ encodes the OLD version, but the publish
|
|
198
|
+
// intends the NEW one. Without rebuilding we'd ship inconsistent
|
|
199
|
+
// bytes (schema says one version, registry record says another).
|
|
192
200
|
const distDir = join(foundationDir, 'dist')
|
|
193
201
|
const foundationJs = join(distDir, 'foundation.js')
|
|
194
202
|
const schemaJson = join(distDir, 'meta', 'schema.json')
|
|
195
203
|
|
|
196
|
-
|
|
197
|
-
|
|
204
|
+
// Pre-read package.json so we can compare its version against the
|
|
205
|
+
// schema before deciding whether to rebuild.
|
|
206
|
+
const pkgPath = join(foundationDir, 'package.json')
|
|
207
|
+
let earlyPkg
|
|
208
|
+
try {
|
|
209
|
+
earlyPkg = JSON.parse(await readFile(pkgPath, 'utf8'))
|
|
210
|
+
} catch (err) {
|
|
211
|
+
error(`Failed to read package.json: ${err.message}`)
|
|
212
|
+
process.exit(1)
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
let needsBuild = !existsSync(foundationJs) || !existsSync(schemaJson)
|
|
216
|
+
let buildReason = needsBuild ? 'no dist/ found' : null
|
|
217
|
+
|
|
218
|
+
if (!needsBuild) {
|
|
219
|
+
try {
|
|
220
|
+
const peekSchema = JSON.parse(await readFile(schemaJson, 'utf8'))
|
|
221
|
+
if (peekSchema?._self?.version && earlyPkg.version && peekSchema._self.version !== earlyPkg.version) {
|
|
222
|
+
needsBuild = true
|
|
223
|
+
buildReason = `package.json::version (${earlyPkg.version}) differs from dist/meta/schema.json::_self.version (${peekSchema._self.version})`
|
|
224
|
+
}
|
|
225
|
+
} catch {
|
|
226
|
+
// Malformed schema → treat as stale.
|
|
227
|
+
needsBuild = true
|
|
228
|
+
buildReason = 'dist/meta/schema.json could not be parsed'
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// 2b. Pre-flight registry check — runs BEFORE the build so we don't
|
|
233
|
+
// burn vite cycles on a foundation we already know we can't (or
|
|
234
|
+
// don't need to) publish.
|
|
235
|
+
//
|
|
236
|
+
// Two outcomes short-circuit the build:
|
|
237
|
+
//
|
|
238
|
+
// a. The registry already has `<canonicalName>@<version>`
|
|
239
|
+
// published from the CURRENT git sha (per-foundation last
|
|
240
|
+
// commit). The artifact upstream is correct; refresh the
|
|
241
|
+
// local receipt and exit. (Same outcome as the post-build
|
|
242
|
+
// duplicate check, just earlier — saves a build.)
|
|
243
|
+
//
|
|
244
|
+
// b. The registry has the version published from a DIFFERENT
|
|
245
|
+
// sha. The user has unpublished changes against an already-
|
|
246
|
+
// published version → "bump the version" error before any
|
|
247
|
+
// build work. Was the eval skill's pp-03 row.
|
|
248
|
+
//
|
|
249
|
+
// If the pre-flight can't determine the canonical name from
|
|
250
|
+
// pkg.json + flags + auth alone (e.g., needs a TTY prompt for
|
|
251
|
+
// the foundation id), it falls through silently to the existing
|
|
252
|
+
// post-build path. No-build-saved is still the existing behavior.
|
|
253
|
+
if (!isLocal) {
|
|
254
|
+
const preflightName = quickResolveCanonicalName(earlyPkg, { namespaceFlag, nameFlag })
|
|
255
|
+
const preflightVersion = earlyPkg.version
|
|
256
|
+
if (preflightName && preflightVersion) {
|
|
257
|
+
try {
|
|
258
|
+
const auth = await readAuth()
|
|
259
|
+
if (auth?.token) {
|
|
260
|
+
const claims = decodeJwtPayload(auth.token)
|
|
261
|
+
const memberUuid = claims?.memberUuid
|
|
262
|
+
// Empty-scope publishes are server-rewritten to ~<memberUuid>/<id>.
|
|
263
|
+
// Mirror that here so getVersionEntry queries the canonical key.
|
|
264
|
+
const lookupName = preflightName.startsWith('@') || preflightName.startsWith('~')
|
|
265
|
+
? preflightName
|
|
266
|
+
: memberUuid ? `~${memberUuid}/${preflightName}` : null
|
|
267
|
+
if (lookupName) {
|
|
268
|
+
const registryUrlPre = registryUrl || getRegistryUrl()
|
|
269
|
+
const registryPre = new RemoteRegistry(registryUrlPre, auth.token)
|
|
270
|
+
const existing = await registryPre.getVersionEntry(lookupName, preflightVersion)
|
|
271
|
+
if (existing) {
|
|
272
|
+
const { gitSha } = readGitState(foundationDir)
|
|
273
|
+
if (gitSha && existing.publishedFromGitSha === gitSha) {
|
|
274
|
+
// Match → refresh receipt, exit clean. NO BUILD.
|
|
275
|
+
const refreshed = receiptFromRegistryEntry({
|
|
276
|
+
existingEntry: existing,
|
|
277
|
+
registry: registryPre,
|
|
278
|
+
name: lookupName,
|
|
279
|
+
version: preflightVersion,
|
|
280
|
+
isLocal: false,
|
|
281
|
+
isPropagateDefault: isPropagate,
|
|
282
|
+
})
|
|
283
|
+
if (refreshed) {
|
|
284
|
+
await mkdir(distDir, { recursive: true })
|
|
285
|
+
await writeFile(join(distDir, 'publish.json'), JSON.stringify(refreshed, null, 2) + '\n')
|
|
286
|
+
console.log('')
|
|
287
|
+
success(`${colors.bright}${lookupName}@${preflightVersion}${colors.reset} already published from ${gitSha.slice(0, 7)} — receipt refreshed.`)
|
|
288
|
+
return
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
// Sha mismatch (or no provenance recorded for the existing
|
|
292
|
+
// entry, which shouldn't happen for new publishes after
|
|
293
|
+
// the receipt-as-cache work shipped). Clean error before
|
|
294
|
+
// any build work.
|
|
295
|
+
console.log('')
|
|
296
|
+
error(`Foundation source has changed since the last publish, but ${colors.bright}${lookupName}@${preflightVersion}${colors.reset} is already published.`)
|
|
297
|
+
console.log('')
|
|
298
|
+
console.log(` Bump ${colors.cyan}package.json::version${colors.reset} to publish an update:`)
|
|
299
|
+
console.log(` ${colors.dim}"version": "${bumpPatch(preflightVersion)}"${colors.reset}`)
|
|
300
|
+
process.exit(1)
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
} catch {
|
|
305
|
+
// Network down, malformed auth, etc. — fall through to the
|
|
306
|
+
// existing post-build flow. No-build-saved is still the same
|
|
307
|
+
// behavior the user got before this pre-flight existed.
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
if (needsBuild) {
|
|
313
|
+
console.log(`${colors.yellow}⚠${colors.reset} ${buildReason}. Building foundation...`)
|
|
198
314
|
console.log('')
|
|
199
315
|
execSync('npx uniweb build --target foundation', {
|
|
200
316
|
cwd: foundationDir,
|
|
@@ -208,7 +324,13 @@ export async function publish(args = []) {
|
|
|
208
324
|
}
|
|
209
325
|
}
|
|
210
326
|
|
|
211
|
-
// 3. Read name
|
|
327
|
+
// 3. Read name + version from the (now-fresh) schema + package.json.
|
|
328
|
+
//
|
|
329
|
+
// `_self.name` is the build-RESOLVED form — applies `uniweb.id`,
|
|
330
|
+
// scope resolution, etc., that are easier to read off the build
|
|
331
|
+
// output than to redo here. `version` is sourced from package.json
|
|
332
|
+
// directly; the version-skew check above already ensured the
|
|
333
|
+
// schema and package.json agree.
|
|
212
334
|
let schema
|
|
213
335
|
try {
|
|
214
336
|
schema = JSON.parse(await readFile(schemaJson, 'utf8'))
|
|
@@ -218,11 +340,12 @@ export async function publish(args = []) {
|
|
|
218
340
|
}
|
|
219
341
|
|
|
220
342
|
const rawName = schema._self?.name
|
|
221
|
-
const version =
|
|
343
|
+
const version = earlyPkg.version
|
|
222
344
|
|
|
223
345
|
if (!rawName || !version) {
|
|
224
|
-
error('
|
|
225
|
-
console.log(`${colors.dim} Ensure your package.json has "name" and "version" fields
|
|
346
|
+
error('Foundation missing name or version')
|
|
347
|
+
console.log(`${colors.dim} Ensure your package.json has "name" and "version" fields,${colors.reset}`)
|
|
348
|
+
console.log(`${colors.dim} and that the build has produced dist/meta/schema.json with _self.name.${colors.reset}`)
|
|
226
349
|
process.exit(1)
|
|
227
350
|
}
|
|
228
351
|
|
|
@@ -267,8 +390,9 @@ export async function publish(args = []) {
|
|
|
267
390
|
// affects only the registry identity, never the workspace. Most users
|
|
268
391
|
// benefit from leaving `package.json::name` as the scaffold default
|
|
269
392
|
// (`src`) and putting the published-as id in `uniweb.id`.
|
|
270
|
-
|
|
271
|
-
|
|
393
|
+
// pkgPath was declared earlier (during the rebuild-stale-dist check).
|
|
394
|
+
// Reuse the already-loaded `earlyPkg` rather than re-reading from disk.
|
|
395
|
+
const pkg = earlyPkg
|
|
272
396
|
const uniwebNamespace = pkg.uniweb?.namespace
|
|
273
397
|
const uniwebId = pkg.uniweb?.id
|
|
274
398
|
const orgScopeMatch = (pkg.name || '').match(/^@([a-z0-9_-]+)\/([a-z0-9_-]+)$/)
|
|
@@ -541,6 +665,20 @@ export async function publish(args = []) {
|
|
|
541
665
|
isPropagateDefault: isPropagate,
|
|
542
666
|
})
|
|
543
667
|
if (refreshedReceipt) {
|
|
668
|
+
// Persist uniweb.id BEFORE the early return when an auto-derive
|
|
669
|
+
// or prompt-resolved id was set in this run. Without this, the
|
|
670
|
+
// next run wouldn't know the id and would have to re-derive
|
|
671
|
+
// from scratch — which means the pre-flight registry check at
|
|
672
|
+
// the top of publish() can't fire either (it relies on a
|
|
673
|
+
// resolvable id from pkg.json alone). Persisting here closes
|
|
674
|
+
// that loop so future deploys hit the pre-flight bail and skip
|
|
675
|
+
// the build entirely.
|
|
676
|
+
if (writeBackId) {
|
|
677
|
+
pkg.uniweb = pkg.uniweb || {}
|
|
678
|
+
pkg.uniweb.id = foundationName
|
|
679
|
+
await writeFile(pkgPath, JSON.stringify(pkg, null, 2) + '\n')
|
|
680
|
+
info(`Wrote ${colors.cyan}uniweb.id: "${foundationName}"${colors.reset} to ${colors.dim}package.json${colors.reset}`)
|
|
681
|
+
}
|
|
544
682
|
await writeFile(join(distDir, 'publish.json'), JSON.stringify(refreshedReceipt, null, 2) + '\n')
|
|
545
683
|
console.log('')
|
|
546
684
|
success(`${colors.bright}${lookupName}@${version}${colors.reset} already published from ${gitSha.slice(0, 7)} — receipt refreshed.`)
|
|
@@ -548,10 +686,10 @@ export async function publish(args = []) {
|
|
|
548
686
|
}
|
|
549
687
|
}
|
|
550
688
|
console.log('')
|
|
551
|
-
error(
|
|
689
|
+
error(`Foundation source has changed since the last publish, but ${colors.bright}${name}@${version}${colors.reset} is already published.`)
|
|
552
690
|
console.log('')
|
|
553
|
-
console.log(` Bump
|
|
554
|
-
console.log(` ${colors.dim}
|
|
691
|
+
console.log(` Bump ${colors.cyan}package.json::version${colors.reset} to publish an update:`)
|
|
692
|
+
console.log(` ${colors.dim}"version": "${bumpPatch(version)}"${colors.reset}`)
|
|
555
693
|
process.exit(1)
|
|
556
694
|
}
|
|
557
695
|
|
|
@@ -650,6 +788,65 @@ export async function publish(args = []) {
|
|
|
650
788
|
* @param {string} version - e.g. "1.0.0"
|
|
651
789
|
* @returns {string} - e.g. "1.0.1"
|
|
652
790
|
*/
|
|
791
|
+
/**
|
|
792
|
+
* Quickly compute the canonical foundation name from `package.json` +
|
|
793
|
+
* CLI flags alone, without prompting and without reading the build's
|
|
794
|
+
* `dist/meta/schema.json`. Used by the pre-flight registry check so we
|
|
795
|
+
* can short-circuit the build when the registry already has this
|
|
796
|
+
* version published.
|
|
797
|
+
*
|
|
798
|
+
* Returns null when resolution would need a prompt or auto-derive
|
|
799
|
+
* (caller falls through to the existing post-build resolution path,
|
|
800
|
+
* which handles those cases). The returned string is one of:
|
|
801
|
+
* - `@<scope>/<id>` (org scope, full canonical form)
|
|
802
|
+
* - `~<handle>/<id>` (personal alias scope)
|
|
803
|
+
* - `<id>` (bare; caller may prepend `~<memberUuid>/`
|
|
804
|
+
* from the JWT for the actual lookup)
|
|
805
|
+
*
|
|
806
|
+
* The full resolution at line 313+ is the canonical implementation;
|
|
807
|
+
* this helper is a strict subset that mirrors the high-confidence
|
|
808
|
+
* paths only. If they diverge, the helper is the one that should
|
|
809
|
+
* stay conservative (return null on uncertainty).
|
|
810
|
+
*/
|
|
811
|
+
function quickResolveCanonicalName(pkg, { namespaceFlag, nameFlag } = {}) {
|
|
812
|
+
if (!pkg) return null
|
|
813
|
+
const orgScopeMatch = (pkg.name || '').match(/^@([a-z0-9_-]+)\/([a-z0-9_-]+)$/)
|
|
814
|
+
const personalScopeMatch = (pkg.name || '').match(/^~([a-z0-9_-]+)\/([a-z0-9_-]+)$/)
|
|
815
|
+
const uniwebNamespace = pkg.uniweb?.namespace
|
|
816
|
+
const uniwebId = pkg.uniweb?.id
|
|
817
|
+
|
|
818
|
+
// Scope precedence mirrors the full resolution.
|
|
819
|
+
let scopeSigil = null
|
|
820
|
+
let scopeName = null
|
|
821
|
+
if (namespaceFlag) {
|
|
822
|
+
scopeSigil = '@'
|
|
823
|
+
scopeName = namespaceFlag
|
|
824
|
+
} else if (orgScopeMatch) {
|
|
825
|
+
scopeSigil = '@'
|
|
826
|
+
scopeName = orgScopeMatch[1]
|
|
827
|
+
} else if (personalScopeMatch) {
|
|
828
|
+
scopeSigil = '~'
|
|
829
|
+
scopeName = personalScopeMatch[1]
|
|
830
|
+
} else if (uniwebNamespace) {
|
|
831
|
+
scopeSigil = '@'
|
|
832
|
+
scopeName = uniwebNamespace
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
// Id precedence mirrors the full resolution but stops at "no-prompt"
|
|
836
|
+
// sources. Auto-derive and TTY prompts both happen post-build so the
|
|
837
|
+
// user sees suggestions in context; the pre-flight only fires when
|
|
838
|
+
// the id is already determined.
|
|
839
|
+
let id = null
|
|
840
|
+
if (nameFlag) id = nameFlag
|
|
841
|
+
else if (orgScopeMatch) id = orgScopeMatch[2]
|
|
842
|
+
else if (personalScopeMatch) id = personalScopeMatch[2]
|
|
843
|
+
else if (uniwebId) id = uniwebId
|
|
844
|
+
else return null
|
|
845
|
+
|
|
846
|
+
if (scopeSigil) return `${scopeSigil}${scopeName}/${id}`
|
|
847
|
+
return id
|
|
848
|
+
}
|
|
849
|
+
|
|
653
850
|
function bumpPatch(version) {
|
|
654
851
|
const parts = version.split('.')
|
|
655
852
|
if (parts.length !== 3) return version
|
|
@@ -799,13 +996,21 @@ async function buildIdSuggestions({ foundationDir, workspaceRoot, pkg }) {
|
|
|
799
996
|
return out
|
|
800
997
|
}
|
|
801
998
|
|
|
999
|
+
/**
|
|
1000
|
+
* Per-directory git state. Mirrors `deploy.js::readGitState` exactly —
|
|
1001
|
+
* scopes the sha + dirty check to `dir` rather than reading the whole
|
|
1002
|
+
* repo's HEAD. Receipts compare against this; if publish records the
|
|
1003
|
+
* repo HEAD but deploy compares against the foundation's last commit,
|
|
1004
|
+
* the receipt-as-cache no-op-refresh path drifts. Both sides must read
|
|
1005
|
+
* the same shape.
|
|
1006
|
+
*/
|
|
802
1007
|
function readGitState(dir) {
|
|
803
1008
|
try {
|
|
804
|
-
const sha = execSync('git
|
|
1009
|
+
const sha = execSync('git log -1 --format=%H -- .', {
|
|
805
1010
|
cwd: dir,
|
|
806
1011
|
stdio: ['ignore', 'pipe', 'ignore'],
|
|
807
1012
|
}).toString().trim()
|
|
808
|
-
const status = execSync('git status --porcelain', {
|
|
1013
|
+
const status = execSync('git status --porcelain -- .', {
|
|
809
1014
|
cwd: dir,
|
|
810
1015
|
stdio: ['ignore', 'pipe', 'ignore'],
|
|
811
1016
|
}).toString()
|
package/src/framework-index.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
|
-
"generatedAt": "2026-04-
|
|
3
|
+
"generatedAt": "2026-04-30T02:16:23.561Z",
|
|
4
4
|
"packages": {
|
|
5
5
|
"@uniweb/build": {
|
|
6
6
|
"version": "0.13.3",
|
|
@@ -92,7 +92,7 @@
|
|
|
92
92
|
"deps": []
|
|
93
93
|
},
|
|
94
94
|
"@uniweb/unipress": {
|
|
95
|
-
"version": "0.4.
|
|
95
|
+
"version": "0.4.3",
|
|
96
96
|
"path": "framework/unipress",
|
|
97
97
|
"deps": [
|
|
98
98
|
"@uniweb/build",
|
package/src/index.js
CHANGED
|
@@ -440,9 +440,20 @@ async function main() {
|
|
|
440
440
|
return
|
|
441
441
|
}
|
|
442
442
|
|
|
443
|
-
// Global install launcher: delegate project-bound commands to local CLI
|
|
443
|
+
// Global install launcher: delegate project-bound commands to local CLI.
|
|
444
|
+
//
|
|
445
|
+
// Escape hatch: `UNIWEB_DISABLE_LOCAL_DELEGATION=1` forces the in-process
|
|
446
|
+
// CLI to handle the command itself, even when a project-local copy of
|
|
447
|
+
// `uniweb` is installed. This exists for the workspace-ergonomics eval
|
|
448
|
+
// harness — when it points the eval at `node $WORKSPACE_ROOT/.../index.js`
|
|
449
|
+
// it expects to exercise the workspace source, not whatever `uniweb`
|
|
450
|
+
// version is symlinked under the test fixture's `node_modules`. Without
|
|
451
|
+
// the escape, evals silently test the published npm version and any
|
|
452
|
+
// unpublished workspace fixes are invisible. See
|
|
453
|
+
// `kb/framework/build/workspace-ergonomics-runbook.md` (`--cli=workspace`).
|
|
444
454
|
const global = isGlobalInstall()
|
|
445
|
-
|
|
455
|
+
const skipDelegation = process.env.UNIWEB_DISABLE_LOCAL_DELEGATION === '1'
|
|
456
|
+
if (global && !skipDelegation && command && !STANDALONE_COMMANDS.has(command)) {
|
|
446
457
|
const localCli = findLocalCli()
|
|
447
458
|
if (localCli) {
|
|
448
459
|
await delegateToLocal(localCli)
|