uniweb 0.13.17 → 0.14.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/README.md +1 -1
- package/package.json +7 -7
- package/partials/agents.md +51 -0
- package/src/backend/client.js +173 -40
- package/src/backend/data-bundle.js +15 -3
- package/src/backend/foundation-bring-along.js +76 -21
- package/src/backend/payment-handoff.js +28 -9
- package/src/backend/site-media.js +147 -19
- package/src/backend/site-sync.js +362 -40
- package/src/commands/add.js +575 -273
- package/src/commands/build.js +160 -57
- package/src/commands/clone.js +79 -26
- package/src/commands/content.js +11 -7
- package/src/commands/deploy.js +80 -32
- package/src/commands/dev.js +39 -17
- package/src/commands/docs.js +44 -16
- package/src/commands/doctor.js +338 -82
- package/src/commands/export.js +15 -7
- package/src/commands/handoff.js +6 -2
- package/src/commands/i18n.js +248 -99
- package/src/commands/inspect.js +48 -19
- package/src/commands/invite.js +9 -3
- package/src/commands/org.js +19 -5
- package/src/commands/publish.js +229 -60
- package/src/commands/pull.js +157 -48
- package/src/commands/push.js +144 -42
- package/src/commands/refresh.js +37 -10
- package/src/commands/register.js +235 -64
- package/src/commands/rename.js +126 -46
- package/src/commands/runtime.js +74 -24
- package/src/commands/status.js +48 -15
- package/src/commands/sync.js +7 -2
- package/src/commands/template.js +12 -4
- package/src/commands/update.js +263 -87
- package/src/commands/validate.js +115 -32
- package/src/framework-index.json +15 -13
- package/src/index.js +298 -145
- package/src/templates/fetchers/github.js +7 -7
- package/src/templates/fetchers/npm.js +3 -3
- package/src/templates/fetchers/release.js +21 -18
- package/src/templates/index.js +34 -12
- package/src/templates/processor.js +44 -12
- package/src/templates/resolver.js +42 -15
- package/src/templates/validator.js +14 -7
- package/src/utils/asset-upload.js +90 -22
- package/src/utils/code-upload.js +62 -37
- package/src/utils/config.js +31 -10
- package/src/utils/dep-survey.js +33 -7
- package/src/utils/destination-prompt.js +27 -17
- package/src/utils/git.js +66 -22
- package/src/utils/host-prompt.js +4 -3
- package/src/utils/install-integrity.js +8 -4
- package/src/utils/interactive.js +3 -3
- package/src/utils/json-file.js +6 -2
- package/src/utils/names.js +15 -6
- package/src/utils/placement.js +16 -8
- package/src/utils/registry-auth.js +185 -57
- package/src/utils/registry-orgs.js +142 -53
- package/src/utils/runtime-upload.js +52 -14
- package/src/utils/scaffold.js +55 -14
- package/src/utils/site-content-refs.js +3 -1
- package/src/utils/update-check.js +43 -17
- package/src/utils/workspace.js +13 -7
- package/src/versions.js +18 -7
- package/templates/site/_gitignore +5 -0
package/src/commands/push.js
CHANGED
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
import { writeFileSync } from 'node:fs'
|
|
56
56
|
import { resolve } from 'node:path'
|
|
57
57
|
import { emitSyncPackages } from '@uniweb/build/uwx'
|
|
58
|
-
import { uploadSiteMedia,
|
|
58
|
+
import { uploadSiteMedia, describeAssetRefusal } from '../backend/site-media.js'
|
|
59
59
|
import { BackendClient } from '../backend/client.js'
|
|
60
60
|
import { resolveSiteDir, resolveSiteBackend } from './deploy.js'
|
|
61
61
|
import {
|
|
@@ -65,16 +65,26 @@ import {
|
|
|
65
65
|
readItemBaseVersions,
|
|
66
66
|
readItemUuids,
|
|
67
67
|
ensureItemUuids,
|
|
68
|
-
|
|
68
|
+
ensureSiteExists,
|
|
69
|
+
clearRemoteSyncStateIfUnbound,
|
|
70
|
+
pushSyncPackages
|
|
69
71
|
} from '../backend/site-sync.js'
|
|
70
72
|
|
|
71
73
|
// Re-exported for downstream importers (pull.js, push.test.js) that read these
|
|
72
74
|
// helpers from this module — their canonical home is now ../backend/site-sync.js.
|
|
73
|
-
export {
|
|
75
|
+
export {
|
|
76
|
+
extractMintedSiteUuid,
|
|
77
|
+
makeModelResolver
|
|
78
|
+
} from '../backend/site-sync.js'
|
|
74
79
|
|
|
75
80
|
const colors = {
|
|
76
|
-
reset: '\x1b[0m',
|
|
77
|
-
|
|
81
|
+
reset: '\x1b[0m',
|
|
82
|
+
bright: '\x1b[1m',
|
|
83
|
+
dim: '\x1b[2m',
|
|
84
|
+
red: '\x1b[31m',
|
|
85
|
+
green: '\x1b[32m',
|
|
86
|
+
yellow: '\x1b[33m',
|
|
87
|
+
blue: '\x1b[36m'
|
|
78
88
|
}
|
|
79
89
|
const log = console.log
|
|
80
90
|
const success = (m) => log(`${colors.green}✓${colors.reset} ${m}`)
|
|
@@ -86,7 +96,8 @@ function flagValue(args, name) {
|
|
|
86
96
|
const eq = args.find((a) => a.startsWith(`${name}=`))
|
|
87
97
|
if (eq) return eq.slice(name.length + 1)
|
|
88
98
|
const i = args.indexOf(name)
|
|
89
|
-
if (i !== -1 && args[i + 1] && !args[i + 1].startsWith('-'))
|
|
99
|
+
if (i !== -1 && args[i + 1] && !args[i + 1].startsWith('-'))
|
|
100
|
+
return args[i + 1]
|
|
90
101
|
return null
|
|
91
102
|
}
|
|
92
103
|
|
|
@@ -116,7 +127,7 @@ export async function push(args = []) {
|
|
|
116
127
|
siteBackend,
|
|
117
128
|
token: tokenFlag,
|
|
118
129
|
args,
|
|
119
|
-
command: 'Syncing'
|
|
130
|
+
command: 'Syncing'
|
|
120
131
|
})
|
|
121
132
|
|
|
122
133
|
// Build BOTH directional packages (the producer side). Each carries its own
|
|
@@ -140,18 +151,51 @@ export async function push(args = []) {
|
|
|
140
151
|
// push exists to avoid. `publish` uploaded and rewrote; push dropped
|
|
141
152
|
// `localAssets` on the floor.
|
|
142
153
|
//
|
|
143
|
-
//
|
|
144
|
-
//
|
|
154
|
+
// The upload runs before `ensureItemUuids`, and the reason once recorded here —
|
|
155
|
+
// "BEFORE `ensureItemUuids`, which mints uuids on the backend, so a refusal
|
|
156
|
+
// leaves nothing minted" — is FALSE. `ensureItemUuids` mints nothing: it reads a
|
|
157
|
+
// local cache, and if that is empty it reads `site.yml::$uuid` and returns
|
|
158
|
+
// immediately when there is none (the first-push case). Its only backend call is
|
|
159
|
+
// a GET (`pullSiteContent`); its only write is a local file. So this ordering is
|
|
160
|
+
// not protecting what that comment claimed, and is under review — an upload is a
|
|
161
|
+
// durable, metered write, which makes uploading first the thing that leaves bytes
|
|
162
|
+
// behind when a later step fails.
|
|
145
163
|
//
|
|
146
164
|
// The probe emit runs WITHOUT `priorHashes`, so it surfaces every local ref
|
|
147
165
|
// rather than only the changed ones. That is not waste — the lane is
|
|
148
166
|
// content-addressed with a `present` skip-list, so unchanged bytes are a no-op
|
|
149
167
|
// PUT. It is also what makes the storage rule fall out of the mechanism instead
|
|
150
|
-
// of needing a special case: a content-only push
|
|
151
|
-
//
|
|
152
|
-
// quota check can refuse it. Only a push that genuinely ADDS bytes can be
|
|
168
|
+
// of needing a special case: a content-only push adds no new stored content, so
|
|
169
|
+
// a quota has nothing to refuse. Only a push that genuinely ADDS bytes can be
|
|
153
170
|
// blocked.
|
|
154
171
|
//
|
|
172
|
+
// Two claims live here and only ONE is a guarantee, per the backend's asset
|
|
173
|
+
// accounting model:
|
|
174
|
+
// - "moves zero bytes" is the COMMON CASE, not a property. `present` is an
|
|
175
|
+
// optimization: a failed presence probe degrades to all-absent, so unchanged
|
|
176
|
+
// bytes may be re-PUT. It is never a false positive, so skipping stays safe.
|
|
177
|
+
// - "is never refused for storage" IS a property, because metering is
|
|
178
|
+
// IDEMPOTENT per (workspace, asset) and recorded at PLAN time. Presenting an
|
|
179
|
+
// asset already on this workspace's books charges nothing, so a push that
|
|
180
|
+
// changes only content meters nothing and cannot be refused.
|
|
181
|
+
//
|
|
182
|
+
// ⚠️ "unchanged bytes skip the transfer" is TRUE. "…and are therefore FREE" is
|
|
183
|
+
// NOT — do not write it into a message, a summary, or a comment. Every upload is
|
|
184
|
+
// chargeable; deduplication is the backend's storage optimization, not the user's
|
|
185
|
+
// discount, so an asset another workspace already uploaded comes back
|
|
186
|
+
// `present: true`, never PUTs, and is STILL charged to this one. What is free is
|
|
187
|
+
// copying URLs around (duplicating a site, a snapshot, a backup).
|
|
188
|
+
//
|
|
189
|
+
// Consequently `needed_bytes` = "assets not yet on this workspace's books", which
|
|
190
|
+
// can be NON-ZERO with zero transfers — so a push can print no `↑` line at all and
|
|
191
|
+
// still be refused. A refusal message must talk about assets new to the workspace,
|
|
192
|
+
// never "the files being uploaded", and must not size itself from what moved.
|
|
193
|
+
//
|
|
194
|
+
// Two framings were proposed and WITHDRAWN in-channel; do not reintroduce either:
|
|
195
|
+
// "charged on distinct stored content", and "the quota fails open on a
|
|
196
|
+
// presence-probe failure" (retired — `needed_bytes` reads the ledger, not the
|
|
197
|
+
// probe, so the property no longer depends on that error path behaving).
|
|
198
|
+
//
|
|
155
199
|
// It also has to be this emit that carries `assetRewrite` below: the push cache
|
|
156
200
|
// stores hashes of the REWRITTEN content, so the emit compared against it must
|
|
157
201
|
// rewrite too, or every entity reads as changed forever.
|
|
@@ -161,7 +205,7 @@ export async function push(args = []) {
|
|
|
161
205
|
try {
|
|
162
206
|
const probe = await emitSyncPackages(siteDir, {
|
|
163
207
|
...(foundationDir ? { foundationDir } : {}),
|
|
164
|
-
resolveModel: makeModelResolver({ client, offline: false })
|
|
208
|
+
resolveModel: makeModelResolver({ client, offline: false })
|
|
165
209
|
})
|
|
166
210
|
mediaRefs = probe.localAssets || []
|
|
167
211
|
} catch (err) {
|
|
@@ -169,53 +213,89 @@ export async function push(args = []) {
|
|
|
169
213
|
return { exitCode: 2 }
|
|
170
214
|
}
|
|
171
215
|
if (mediaRefs.length) {
|
|
216
|
+
// The site has to exist before its bytes do — an upload with no owning
|
|
217
|
+
// entity is charged and cannot be freed, because freeing means deleting the
|
|
218
|
+
// owner. A no-op once `$uuid` is set, so only a never-synced site pays.
|
|
219
|
+
const dropped = clearRemoteSyncStateIfUnbound(siteDir)
|
|
220
|
+
if (dropped.length) {
|
|
221
|
+
note(
|
|
222
|
+
`Cleared stale sync state from a previous site (${dropped.join(', ')}).`
|
|
223
|
+
)
|
|
224
|
+
}
|
|
225
|
+
const site = await ensureSiteExists({ client, siteDir, asOrg, note })
|
|
226
|
+
if (!site.uuid) {
|
|
227
|
+
error(`Could not create the site on the backend: ${site.reason}`)
|
|
228
|
+
note('Nothing was uploaded and nothing was charged.')
|
|
229
|
+
return { exitCode: 1 }
|
|
230
|
+
}
|
|
172
231
|
info('Uploading media…')
|
|
173
232
|
try {
|
|
174
|
-
const { map, failed } = await uploadSiteMedia(
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
233
|
+
const { map, failed } = await uploadSiteMedia(
|
|
234
|
+
client,
|
|
235
|
+
siteDir,
|
|
236
|
+
mediaRefs,
|
|
237
|
+
{
|
|
238
|
+
siteUuid: site.uuid,
|
|
239
|
+
onProgress: (m) => note(` ${m}`),
|
|
240
|
+
warn: (m) => note(`! ${m}`)
|
|
241
|
+
}
|
|
242
|
+
)
|
|
178
243
|
// Bytes that did not land must not be pushed around: the content would go
|
|
179
244
|
// up still naming the local path, so the teammate sees the broken image
|
|
180
245
|
// this whole change exists to prevent, and the only trace is a warning. A
|
|
181
246
|
// missing FILE is a different thing — already broken before us, warned by
|
|
182
247
|
// the uploader, and not worth blocking a push over.
|
|
183
248
|
if (failed.length) {
|
|
184
|
-
error(
|
|
249
|
+
error(
|
|
250
|
+
`${failed.length} asset(s) failed to upload — nothing was pushed.`
|
|
251
|
+
)
|
|
185
252
|
for (const f of failed) note(` ${f.path} (HTTP ${f.status})`)
|
|
186
253
|
return { exitCode: 1 }
|
|
187
254
|
}
|
|
188
255
|
if (Object.keys(map).length) assetRewrite = map
|
|
189
|
-
note(
|
|
256
|
+
note(
|
|
257
|
+
`${Object.keys(map).length}/${mediaRefs.length} media ref(s) → serve URL`
|
|
258
|
+
)
|
|
190
259
|
} catch (err) {
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
260
|
+
// Typed plan refusals get their own account. Note the storage one must not
|
|
261
|
+
// be phrased from what moved — see describeAssetRefusal's rule 1; a push can
|
|
262
|
+
// print no `↑` line at all and still be refused.
|
|
263
|
+
const refusal = describeAssetRefusal(err)
|
|
264
|
+
if (refusal) {
|
|
265
|
+
error(refusal.headline)
|
|
266
|
+
for (const line of refusal.notes) note(line)
|
|
267
|
+
} else {
|
|
268
|
+
error(`Media upload failed: ${err.message}`)
|
|
197
269
|
}
|
|
198
|
-
error(`Media upload failed: ${err.message}`)
|
|
199
270
|
return { exitCode: 1 }
|
|
200
271
|
}
|
|
201
272
|
}
|
|
202
273
|
}
|
|
203
274
|
|
|
204
|
-
const itemUuids =
|
|
205
|
-
|
|
206
|
-
|
|
275
|
+
const itemUuids =
|
|
276
|
+
output || dryRun
|
|
277
|
+
? readItemUuids(siteDir)
|
|
278
|
+
: await ensureItemUuids({ client, siteDir, note })
|
|
207
279
|
let pkg
|
|
208
280
|
try {
|
|
209
281
|
pkg = await emitSyncPackages(siteDir, {
|
|
210
282
|
...(foundationDir ? { foundationDir } : {}),
|
|
211
|
-
resolveModel: makeModelResolver({
|
|
283
|
+
resolveModel: makeModelResolver({
|
|
284
|
+
client,
|
|
285
|
+
offline: Boolean(output) || dryRun
|
|
286
|
+
}),
|
|
212
287
|
priorHashes,
|
|
213
288
|
sendAll,
|
|
214
289
|
itemUuids,
|
|
215
290
|
// Both grains are dropped together by --force: one flag, one meaning,
|
|
216
291
|
// no partial-force mode.
|
|
217
|
-
...(force
|
|
218
|
-
|
|
292
|
+
...(force
|
|
293
|
+
? {}
|
|
294
|
+
: {
|
|
295
|
+
baseVersions: readBaseVersions(siteDir),
|
|
296
|
+
itemBaseVersions: readItemBaseVersions(siteDir)
|
|
297
|
+
}),
|
|
298
|
+
...(assetRewrite ? { assetRewrite } : {})
|
|
219
299
|
})
|
|
220
300
|
} catch (err) {
|
|
221
301
|
error(`Could not build the sync package: ${err.message}`)
|
|
@@ -225,36 +305,53 @@ export async function push(args = []) {
|
|
|
225
305
|
log('')
|
|
226
306
|
for (const w of warnings) note(`! ${w}`)
|
|
227
307
|
|
|
228
|
-
const totalEntities =
|
|
308
|
+
const totalEntities =
|
|
309
|
+
(siteContent?.entityCount || 0) + (collections?.entityCount || 0)
|
|
229
310
|
|
|
230
311
|
// Nothing changed since the last push — the backend is already up to date.
|
|
231
312
|
if (totalEntities === 0) {
|
|
232
|
-
success(
|
|
313
|
+
success(
|
|
314
|
+
`Nothing to push — ${skipped} entit${skipped === 1 ? 'y' : 'ies'} unchanged since the last push.`
|
|
315
|
+
)
|
|
233
316
|
return { exitCode: 0 }
|
|
234
317
|
}
|
|
235
|
-
if (siteContent)
|
|
318
|
+
if (siteContent)
|
|
319
|
+
info(
|
|
320
|
+
`${colors.bright}site-content${colors.reset} → ${siteContent.models.join(', ')}`
|
|
321
|
+
)
|
|
236
322
|
if (collections) {
|
|
237
323
|
const n = collections.entityCount
|
|
238
|
-
info(
|
|
324
|
+
info(
|
|
325
|
+
`${colors.bright}collections${colors.reset} (${n} entit${n === 1 ? 'y' : 'ies'}) → ${collections.models.join(', ')}`
|
|
326
|
+
)
|
|
239
327
|
}
|
|
240
328
|
if (skipped) note(`${skipped} unchanged, skipped`)
|
|
241
329
|
|
|
242
330
|
// Preview paths — no submit, no auth. Two lanes → up to two files / two routes.
|
|
243
331
|
if (output) {
|
|
244
332
|
const base = output.replace(/\.uwx$/, '')
|
|
245
|
-
if (siteContent)
|
|
246
|
-
|
|
247
|
-
|
|
333
|
+
if (siteContent)
|
|
334
|
+
writeFileSync(resolve(`${base}.site-content.uwx`), siteContent.buffer)
|
|
335
|
+
if (collections)
|
|
336
|
+
writeFileSync(resolve(`${base}.collections.uwx`), collections.buffer)
|
|
337
|
+
const lanes = [
|
|
338
|
+
siteContent && 'site-content',
|
|
339
|
+
collections && 'collections'
|
|
340
|
+
].filter(Boolean)
|
|
248
341
|
success(`Wrote ${lanes.join(' + ')} .uwx — not submitted`)
|
|
249
342
|
return { exitCode: 0 }
|
|
250
343
|
}
|
|
251
344
|
if (dryRun) {
|
|
252
345
|
if (siteContent) {
|
|
253
346
|
const verb = siteContentUuid ? 'update' : 'create'
|
|
254
|
-
info(
|
|
347
|
+
info(
|
|
348
|
+
`Dry run — would ${verb} content at ${colors.dim}${client.origin}${colors.reset}`
|
|
349
|
+
)
|
|
255
350
|
}
|
|
256
351
|
if (collections) {
|
|
257
|
-
info(
|
|
352
|
+
info(
|
|
353
|
+
`Dry run — would push the folder at ${colors.dim}${client.origin}${colors.reset}`
|
|
354
|
+
)
|
|
258
355
|
}
|
|
259
356
|
return { exitCode: 0 }
|
|
260
357
|
}
|
|
@@ -266,7 +363,12 @@ export async function push(args = []) {
|
|
|
266
363
|
siteDir,
|
|
267
364
|
pkg,
|
|
268
365
|
asOrg,
|
|
269
|
-
report: {
|
|
366
|
+
report: {
|
|
367
|
+
info,
|
|
368
|
+
note,
|
|
369
|
+
error,
|
|
370
|
+
dim: (s) => `${colors.dim}${s}${colors.reset}`
|
|
371
|
+
}
|
|
270
372
|
})
|
|
271
373
|
if (result.exitCode !== 0) return { exitCode: result.exitCode }
|
|
272
374
|
success(
|
package/src/commands/refresh.js
CHANGED
|
@@ -39,19 +39,29 @@ import { join } from 'node:path'
|
|
|
39
39
|
import yaml from 'js-yaml'
|
|
40
40
|
|
|
41
41
|
import { resolveSiteDir } from './deploy.js'
|
|
42
|
-
import {
|
|
42
|
+
import {
|
|
43
|
+
isGitRepo,
|
|
44
|
+
hasRemote,
|
|
45
|
+
pullRemote,
|
|
46
|
+
headProvenance
|
|
47
|
+
} from '../utils/git.js'
|
|
43
48
|
import { probeUnpushed } from '../backend/site-sync.js'
|
|
44
49
|
|
|
45
50
|
const c = {
|
|
46
|
-
reset: '\x1b[0m',
|
|
47
|
-
|
|
51
|
+
reset: '\x1b[0m',
|
|
52
|
+
bold: '\x1b[1m',
|
|
53
|
+
dim: '\x1b[2m',
|
|
54
|
+
cyan: '\x1b[36m',
|
|
55
|
+
green: '\x1b[32m',
|
|
56
|
+
yellow: '\x1b[33m',
|
|
57
|
+
red: '\x1b[31m'
|
|
48
58
|
}
|
|
49
59
|
const say = {
|
|
50
60
|
ok: (m) => console.log(`${c.green}✓${c.reset} ${m}`),
|
|
51
61
|
info: (m) => console.log(`${c.cyan}→${c.reset} ${m}`),
|
|
52
62
|
warn: (m) => console.log(`${c.yellow}⚠${c.reset} ${m}`),
|
|
53
63
|
err: (m) => console.error(`${c.red}✗${c.reset} ${m}`),
|
|
54
|
-
dim: (m) => console.log(` ${c.dim}${m}${c.reset}`)
|
|
64
|
+
dim: (m) => console.log(` ${c.dim}${m}${c.reset}`)
|
|
55
65
|
}
|
|
56
66
|
|
|
57
67
|
// Forward a flag AND its value to the delegated verb. `--backend http://x` is two
|
|
@@ -65,7 +75,8 @@ function collectPassthrough(args, names) {
|
|
|
65
75
|
const name = names.find((n) => a === n || a.startsWith(`${n}=`))
|
|
66
76
|
if (!name) continue
|
|
67
77
|
out.push(a)
|
|
68
|
-
if (a === name && args[i + 1] && !args[i + 1].startsWith('-'))
|
|
78
|
+
if (a === name && args[i + 1] && !args[i + 1].startsWith('-'))
|
|
79
|
+
out.push(args[++i])
|
|
69
80
|
}
|
|
70
81
|
return out
|
|
71
82
|
}
|
|
@@ -119,7 +130,11 @@ export async function refresh(args = [], deps = {}) {
|
|
|
119
130
|
// order to address them in.
|
|
120
131
|
return { exitCode: 1 }
|
|
121
132
|
}
|
|
122
|
-
say.dim(
|
|
133
|
+
say.dim(
|
|
134
|
+
r.changed
|
|
135
|
+
? 'Took new commits from the remote.'
|
|
136
|
+
: 'Already up to date with the remote.'
|
|
137
|
+
)
|
|
123
138
|
consulted.push('git')
|
|
124
139
|
}
|
|
125
140
|
|
|
@@ -133,7 +148,11 @@ export async function refresh(args = [], deps = {}) {
|
|
|
133
148
|
const pull = deps.pull || (await import('./pull.js')).pull
|
|
134
149
|
// `--merge` rather than a plain pull: an author editing a different part of the
|
|
135
150
|
// same section is not a conflict, and should not be presented as one.
|
|
136
|
-
const passthrough = collectPassthrough(args, [
|
|
151
|
+
const passthrough = collectPassthrough(args, [
|
|
152
|
+
'--backend',
|
|
153
|
+
'--registry',
|
|
154
|
+
'--token'
|
|
155
|
+
])
|
|
137
156
|
const res = await pull(['--merge', ...passthrough])
|
|
138
157
|
conflicts = res?.merge?.conflicted?.length ?? 0
|
|
139
158
|
if (res?.exitCode && !conflicts) {
|
|
@@ -151,14 +170,20 @@ export async function refresh(args = [], deps = {}) {
|
|
|
151
170
|
for (const s of skipped) say.dim(`Skipped: ${s}`)
|
|
152
171
|
|
|
153
172
|
const git = headProvenance(siteDir)
|
|
154
|
-
if (git)
|
|
173
|
+
if (git)
|
|
174
|
+
say.dim(
|
|
175
|
+
`Commit : ${git.sha.slice(0, 8)}${git.dirty ? ' (working tree has changes)' : ''}`
|
|
176
|
+
)
|
|
155
177
|
|
|
156
178
|
// What is still yours to send. The point of a milestone check is knowing both
|
|
157
179
|
// directions, not just that you took what was waiting.
|
|
158
180
|
if (!skipBackend && siteContentUuid(siteDir)) {
|
|
159
181
|
try {
|
|
160
182
|
const probe = await probeUnpushed(siteDir)
|
|
161
|
-
if (probe.changed)
|
|
183
|
+
if (probe.changed)
|
|
184
|
+
say.dim(
|
|
185
|
+
`Unpushed: ${probe.changed} entit${probe.changed === 1 ? 'y' : 'ies'} changed locally`
|
|
186
|
+
)
|
|
162
187
|
} catch {
|
|
163
188
|
/* the probe is a courtesy; never fail a refresh over it */
|
|
164
189
|
}
|
|
@@ -166,7 +191,9 @@ export async function refresh(args = [], deps = {}) {
|
|
|
166
191
|
|
|
167
192
|
console.log('')
|
|
168
193
|
if (conflicts) {
|
|
169
|
-
say.err(
|
|
194
|
+
say.err(
|
|
195
|
+
`${conflicts} file(s) need you to resolve conflicts before pushing.`
|
|
196
|
+
)
|
|
170
197
|
return { exitCode: 1 }
|
|
171
198
|
}
|
|
172
199
|
say.ok('Up to date.')
|