uniweb 1.21.1 → 1.21.2
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 +4 -4
- package/partials/agents.md +1 -1
- package/src/commands/status.js +330 -20
- package/src/framework-index.json +2 -2
- package/src/index.js +5 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "uniweb",
|
|
3
|
-
"version": "1.21.
|
|
3
|
+
"version": "1.21.2",
|
|
4
4
|
"description": "Create structured Vite + React sites with content/code separation",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -47,9 +47,9 @@
|
|
|
47
47
|
"@uniweb/semantic-parser": "~1.13.0"
|
|
48
48
|
},
|
|
49
49
|
"peerDependencies": {
|
|
50
|
-
"@uniweb/build": "~1.10.
|
|
51
|
-
"@uniweb/
|
|
52
|
-
"@uniweb/
|
|
50
|
+
"@uniweb/build": "~1.10.4",
|
|
51
|
+
"@uniweb/content-reader": "~1.5.7",
|
|
52
|
+
"@uniweb/semantic-parser": "~1.13.0"
|
|
53
53
|
},
|
|
54
54
|
"peerDependenciesMeta": {
|
|
55
55
|
"@uniweb/build": {
|
package/partials/agents.md
CHANGED
|
@@ -192,7 +192,7 @@ The rest of this guide explains how Uniweb works. This part is what to do *first
|
|
|
192
192
|
|
|
193
193
|
### 1. Orient — what kind of workspace is this?
|
|
194
194
|
|
|
195
|
-
**Run `uniweb status` first.** It says
|
|
195
|
+
**Run `uniweb status` first.** It says where the site sits and what it is made of — the workspace's sites and foundations, the foundation this site is built with and which other sites a change to it reaches, pages it takes from outside its folder (another repository, sometimes), the languages it is written in — then whether it changed in the app since this copy last synced, whether git or Uniweb has something newer, and what to run about each, before you edit something someone else just changed. Run at the root of a workspace with several sites, it says what each one is. The rest of this step is how to read the same wiring by hand.
|
|
196
196
|
|
|
197
197
|
**There is no single layout. Read `pnpm-workspace.yaml` first** — its `packages:` globs name the shape in one glance:
|
|
198
198
|
|
package/src/commands/status.js
CHANGED
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
|
|
44
44
|
import { execFileSync } from 'node:child_process'
|
|
45
45
|
import { existsSync, readFileSync } from 'node:fs'
|
|
46
|
-
import { join, relative, resolve } from 'node:path'
|
|
46
|
+
import { basename, join, relative, resolve } from 'node:path'
|
|
47
47
|
import yaml from 'js-yaml'
|
|
48
48
|
|
|
49
49
|
import {
|
|
@@ -790,20 +790,241 @@ async function doctorCheck(siteDir) {
|
|
|
790
790
|
}
|
|
791
791
|
}
|
|
792
792
|
|
|
793
|
-
/**
|
|
794
|
-
|
|
795
|
-
|
|
793
|
+
/**
|
|
794
|
+
* `dev.server`: the site's dev server, confirmed by asking it — and the command that starts it
|
|
795
|
+
* for this site. In a workspace with several, a bare `uniweb dev` starts the first one found
|
|
796
|
+
* (`commands/dev.js`), which may not be this one.
|
|
797
|
+
*/
|
|
798
|
+
async function devCheck(siteDir, { offline, sites = [] }) {
|
|
799
|
+
const root = findWorkspaceRoot(siteDir) || siteDir
|
|
800
|
+
const mine = sites.find((s) => resolve(root, s.path) === resolve(siteDir))
|
|
801
|
+
const first = sites[0]
|
|
802
|
+
const command = sites.length > 1 && mine && first !== mine ? `npx uniweb dev ${mine.name}` : 'npx uniweb dev'
|
|
803
|
+
const note = command === 'npx uniweb dev' ? '' : ` (a bare \`uniweb dev\` starts ${first.name})`
|
|
804
|
+
if (offline) return { id: 'dev.server', state: 'skipped', summary: `The dev server was not asked (--offline); \`${command}\` starts it${note}.`, detail: { command } }
|
|
796
805
|
try {
|
|
797
806
|
const { runningDevServer } = await import('@uniweb/build/site')
|
|
798
807
|
const running = await runningDevServer(siteDir, { timeout: 1000 })
|
|
799
808
|
return running
|
|
800
|
-
? { id: 'dev.server', state: 'ok', summary: `The dev server is running at ${running.url}.`, detail: { url: running.url } }
|
|
801
|
-
: { id: 'dev.server', state: 'skipped', summary:
|
|
809
|
+
? { id: 'dev.server', state: 'ok', summary: `The dev server is running at ${running.url}.`, detail: { url: running.url, command } }
|
|
810
|
+
: { id: 'dev.server', state: 'skipped', summary: `The dev server is not running — \`${command}\` starts it${note}.`, detail: { command } }
|
|
811
|
+
} catch (err) {
|
|
812
|
+
return { id: 'dev.server', state: 'unknown', summary: `Could not ask the dev server: ${err.message}`, detail: { command } }
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
// ─── the project ────────────────────────────────────────────────────────────
|
|
817
|
+
|
|
818
|
+
/**
|
|
819
|
+
* The workspace, as its wiring says: the sites and foundations its globs hold, by the build's
|
|
820
|
+
* classifier (`utils/discover.js`), and the globs themselves.
|
|
821
|
+
*
|
|
822
|
+
* ⛔ NO LAYOUT NAME. "Single", "segregated" and the rest are conventions — the globs and the
|
|
823
|
+
* wiring are the truth — and a name guessed from folders is wrong where a workspace mixes them.
|
|
824
|
+
*/
|
|
825
|
+
async function workspaceFacts(root) {
|
|
826
|
+
const [{ discoverSites, discoverFoundations }, { readWorkspaceConfig }, { isExtensionPackage }] = await Promise.all([
|
|
827
|
+
import('../utils/discover.js'),
|
|
828
|
+
import('../utils/config.js'),
|
|
829
|
+
import('@uniweb/build')
|
|
830
|
+
])
|
|
831
|
+
const [sites, packages, config] = await Promise.all([
|
|
832
|
+
discoverSites(root),
|
|
833
|
+
discoverFoundations(root),
|
|
834
|
+
readWorkspaceConfig(root).catch(() => ({ packages: [] }))
|
|
835
|
+
])
|
|
836
|
+
const extension = (f) => {
|
|
837
|
+
try {
|
|
838
|
+
return isExtensionPackage(join(root, f.path))
|
|
839
|
+
} catch {
|
|
840
|
+
return false
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
return {
|
|
844
|
+
root,
|
|
845
|
+
patterns: Array.isArray(config.packages) ? config.packages : [],
|
|
846
|
+
sites,
|
|
847
|
+
foundations: packages.filter((f) => !extension(f)),
|
|
848
|
+
extensions: packages.filter(extension)
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
function workspaceLine(ws) {
|
|
853
|
+
const { sites, foundations, extensions, patterns } = ws
|
|
854
|
+
if (sites.length === 1 && foundations.length === 1 && !extensions.length) {
|
|
855
|
+
return `One site and one foundation in this workspace: ${sites[0].path}/ and ${foundations[0].path}/.`
|
|
856
|
+
}
|
|
857
|
+
const parts = [count(sites.length, 'site'), count(foundations.length, 'foundation')]
|
|
858
|
+
if (extensions.length) parts.push(count(extensions.length, 'extension'))
|
|
859
|
+
const from = patterns.length ? `, from ${list(patterns)}` : ''
|
|
860
|
+
return `${parts.slice(0, -1).join(', ')} and ${parts.at(-1)} in this workspace${from}.`
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
/**
|
|
864
|
+
* A site's foundation, as the build finds it — the one resolver (`detectFoundationType`), so
|
|
865
|
+
* what status names is what the build would build. A catalog ref, which a build refuses, is
|
|
866
|
+
* named as one.
|
|
867
|
+
*/
|
|
868
|
+
async function foundationOf(siteDir, siteYml, root) {
|
|
869
|
+
const declared = siteYml?.foundation
|
|
870
|
+
if (!declared) return { kind: 'none' }
|
|
871
|
+
const name = typeof declared === 'string' ? declared : declared.ref || declared.name || declared.url || null
|
|
872
|
+
const [{ detectFoundationType }, { parseCatalogRef }] = await Promise.all([import('@uniweb/build'), import('@uniweb/build/uwx')])
|
|
873
|
+
if (parseCatalogRef(name)) return { kind: 'catalog', name, ref: name }
|
|
874
|
+
try {
|
|
875
|
+
const found = detectFoundationType(declared, siteDir)
|
|
876
|
+
if (found.type === 'local') return { kind: 'local', name: found.name, path: relative(root, found.path) || '.', abs: resolve(found.path) }
|
|
877
|
+
return { kind: 'url', name, url: found.url }
|
|
802
878
|
} catch (err) {
|
|
803
|
-
return {
|
|
879
|
+
return { kind: 'unresolved', name, error: String(err?.message || err).split('\n')[0] }
|
|
804
880
|
}
|
|
805
881
|
}
|
|
806
882
|
|
|
883
|
+
/** A site's extensions, each where it is found: a folder here, the catalog, or a URL. */
|
|
884
|
+
async function extensionsOf(siteDir, siteYml, root) {
|
|
885
|
+
const declared = Array.isArray(siteYml?.extensions) ? siteYml.extensions : []
|
|
886
|
+
if (!declared.length) return []
|
|
887
|
+
const [{ findSiteExtension, extensionNameOf }, { parseCatalogRef }] = await Promise.all([import('@uniweb/build'), import('@uniweb/build/uwx')])
|
|
888
|
+
return declared.map((entry) => {
|
|
889
|
+
const value = entry && typeof entry === 'object' ? entry.url || entry.ref || entry.name : entry
|
|
890
|
+
if (typeof value !== 'string' || !value) return { value: String(value), kind: 'unresolved' }
|
|
891
|
+
if (parseCatalogRef(value)) return { value, kind: 'catalog' }
|
|
892
|
+
if (/^https?:\/\//.test(value)) return { value, kind: 'url' }
|
|
893
|
+
// A site-relative URL names an extension by its first segment; a bare name is that name.
|
|
894
|
+
const name = value.startsWith('/') ? extensionNameOf(value, siteYml.base)?.name : value
|
|
895
|
+
const dir = name ? findSiteExtension(name, siteDir) : null
|
|
896
|
+
return dir ? { value, kind: 'local', name, path: relative(root, dir) || '.' } : { value, kind: 'unresolved', name: name ?? null }
|
|
897
|
+
})
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
/**
|
|
901
|
+
* `project.*`: where this site sits — the workspace's sites and foundations, the foundation it
|
|
902
|
+
* is built with and what else uses it, its extensions, pages it takes from outside its folder,
|
|
903
|
+
* and the languages it is written in. All from files: no request.
|
|
904
|
+
*/
|
|
905
|
+
async function projectChecks(siteDir, siteYml) {
|
|
906
|
+
const inWorkspace = Boolean(findWorkspaceRoot(siteDir))
|
|
907
|
+
const root = findWorkspaceRoot(siteDir) || siteDir
|
|
908
|
+
const ws = await workspaceFacts(root)
|
|
909
|
+
const checks = []
|
|
910
|
+
checks.push({
|
|
911
|
+
id: 'project.workspace',
|
|
912
|
+
state: 'ok',
|
|
913
|
+
summary: inWorkspace ? workspaceLine(ws) : 'This site is on its own, in no workspace.',
|
|
914
|
+
detail: { patterns: ws.patterns, sites: ws.sites, foundations: ws.foundations, extensions: ws.extensions }
|
|
915
|
+
})
|
|
916
|
+
|
|
917
|
+
// ⭐ The foundation, and what a change to it reaches: every site that resolves to the same one.
|
|
918
|
+
const fnd = await foundationOf(siteDir, siteYml, root)
|
|
919
|
+
const here = resolve(siteDir)
|
|
920
|
+
const sharedWith = []
|
|
921
|
+
if (fnd.kind === 'local') {
|
|
922
|
+
for (const s of ws.sites) {
|
|
923
|
+
const dir = resolve(root, s.path)
|
|
924
|
+
if (dir === here) continue
|
|
925
|
+
const other = await foundationOf(dir, readSiteYml(dir), root)
|
|
926
|
+
if (other.kind === 'local' && other.abs === fnd.abs) sharedWith.push(s.path)
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
const extensions = await extensionsOf(siteDir, siteYml, root)
|
|
930
|
+
let summary
|
|
931
|
+
let state = 'ok'
|
|
932
|
+
if (fnd.kind === 'local') {
|
|
933
|
+
summary =
|
|
934
|
+
`Foundation ${fnd.name}, in ${fnd.path}/ — its source is here` +
|
|
935
|
+
(sharedWith.length
|
|
936
|
+
? `, and ${sharedWith.length === 1 ? 'another site uses' : `${sharedWith.length} other sites use`} it too: ${sharedWith.join(', ')} — a change to a section type changes ${sharedWith.length === 1 ? 'it' : 'them'} as well.`
|
|
937
|
+
: ws.sites.length > 1 ? '; no other site here uses it.' : '.')
|
|
938
|
+
} else if (fnd.kind === 'catalog') {
|
|
939
|
+
summary = `Foundation ${fnd.ref}, from the catalog — its source is not in this workspace; work within the section types it offers.`
|
|
940
|
+
} else if (fnd.kind === 'url') {
|
|
941
|
+
summary = `Foundation loaded from ${fnd.url} — its source is not in this workspace; work within the section types it offers.`
|
|
942
|
+
} else if (fnd.kind === 'unresolved') {
|
|
943
|
+
state = 'attention'
|
|
944
|
+
summary = `site.yml names the foundation \`${fnd.name}\`, which does not resolve: ${fnd.error}`
|
|
945
|
+
} else {
|
|
946
|
+
state = 'attention'
|
|
947
|
+
summary = 'site.yml names no foundation.'
|
|
948
|
+
}
|
|
949
|
+
const extensionLines = extensions.map((e) =>
|
|
950
|
+
e.kind === 'local'
|
|
951
|
+
? `Extension ${e.name}, in ${e.path}/ — its source is here.`
|
|
952
|
+
: e.kind === 'catalog'
|
|
953
|
+
? `Extension ${e.value}, from the catalog — its source is not in this workspace.`
|
|
954
|
+
: e.kind === 'url'
|
|
955
|
+
? `Extension loaded from ${e.value} — its source is not in this workspace.`
|
|
956
|
+
: `Extension \`${e.value}\` names ${e.name ? `\`${e.name}\`, which no folder here holds` : 'nothing this site can find'}.`
|
|
957
|
+
)
|
|
958
|
+
if (extensions.some((e) => e.kind === 'unresolved')) state = 'attention'
|
|
959
|
+
checks.push({
|
|
960
|
+
id: 'project.foundation',
|
|
961
|
+
state,
|
|
962
|
+
summary,
|
|
963
|
+
lines: extensionLines,
|
|
964
|
+
detail: {
|
|
965
|
+
kind: fnd.kind,
|
|
966
|
+
name: fnd.name ?? null,
|
|
967
|
+
path: fnd.path ?? null,
|
|
968
|
+
url: fnd.url ?? null,
|
|
969
|
+
editable: fnd.kind === 'local',
|
|
970
|
+
sharedWith,
|
|
971
|
+
extensions: extensions.map(({ value, kind, name, path }) => ({ value, kind, name: name ?? null, path: path ?? null }))
|
|
972
|
+
}
|
|
973
|
+
})
|
|
974
|
+
|
|
975
|
+
// ⭐ Pages from outside the site's folder (`paths:` mounts, by the build's own rule): a pull
|
|
976
|
+
// never writes them (`uwx/git-authored.js`), and one in its own repository is edited there.
|
|
977
|
+
const { mountEntriesOf } = await import('@uniweb/build/site')
|
|
978
|
+
const siteRepo = repoOf(siteDir)?.top ?? null
|
|
979
|
+
const mounts = mountEntriesOf(siteYml?.paths).map(([segment, dir]) => {
|
|
980
|
+
const abs = resolve(siteDir, String(dir))
|
|
981
|
+
const exists = existsSync(abs)
|
|
982
|
+
const top = exists ? repoOf(abs)?.top ?? null : null
|
|
983
|
+
const repository = !exists ? null : !top ? 'none' : top === siteRepo ? 'same' : 'other'
|
|
984
|
+
return { segment, dir: String(dir), exists, repository }
|
|
985
|
+
})
|
|
986
|
+
const mountLine = (m) =>
|
|
987
|
+
!m.exists
|
|
988
|
+
? `pages/${m.segment} names ${m.dir}, which does not exist.`
|
|
989
|
+
: m.repository === 'other'
|
|
990
|
+
? `pages/${m.segment} comes from ${m.dir}, its own git repository — edits there are commits in it.`
|
|
991
|
+
: `pages/${m.segment} comes from ${m.dir}, outside the site's folder.`
|
|
992
|
+
checks.push(
|
|
993
|
+
mounts.length
|
|
994
|
+
? {
|
|
995
|
+
id: 'project.mounts',
|
|
996
|
+
state: mounts.some((m) => !m.exists) ? 'attention' : 'ok',
|
|
997
|
+
summary: mounts.length === 1 ? mountLine(mounts[0]) : `${count(mounts.length, 'page folder')} come from outside the site's folder.`,
|
|
998
|
+
lines: [
|
|
999
|
+
...(mounts.length === 1 ? [] : mounts.map(mountLine)),
|
|
1000
|
+
'An edit made in the app to those pages does not come back on a pull, and the next push replaces it with the files.'
|
|
1001
|
+
],
|
|
1002
|
+
detail: { mounts }
|
|
1003
|
+
}
|
|
1004
|
+
: { id: 'project.mounts', state: 'skipped', summary: "All the site's pages are in its own folder.", quiet: true, detail: { mounts: [] } }
|
|
1005
|
+
)
|
|
1006
|
+
|
|
1007
|
+
// The languages, by core's own rules: the one the files are written in, and the rest.
|
|
1008
|
+
const { normalizeLanguageList, resolveDefaultLocale, isWildcardLanguages } = await import('@uniweb/core/locale-config')
|
|
1009
|
+
const source = resolveDefaultLocale(siteYml || {})
|
|
1010
|
+
const wildcard = isWildcardLanguages(siteYml?.languages)
|
|
1011
|
+
const translations = normalizeLanguageList(siteYml?.languages).filter((code) => code !== source && code !== '*')
|
|
1012
|
+
checks.push(
|
|
1013
|
+
translations.length || wildcard
|
|
1014
|
+
? {
|
|
1015
|
+
id: 'project.languages',
|
|
1016
|
+
state: 'ok',
|
|
1017
|
+
summary:
|
|
1018
|
+
`Written in ${source}, and translated into ${wildcard ? 'every language in locales/' : translations.join(', ')} — ` +
|
|
1019
|
+
`a text you change in ${source} needs translating again.`,
|
|
1020
|
+
lines: ['After a change: build, then `npx uniweb i18n sync`, and `npx uniweb i18n status` lists what is left.'],
|
|
1021
|
+
detail: { source, translations, wildcard }
|
|
1022
|
+
}
|
|
1023
|
+
: { id: 'project.languages', state: 'ok', summary: `One language: ${source}.`, detail: { source, translations: [], wildcard: false } }
|
|
1024
|
+
)
|
|
1025
|
+
return { checks, ws }
|
|
1026
|
+
}
|
|
1027
|
+
|
|
807
1028
|
// ─── where the site is hosted ───────────────────────────────────────────────
|
|
808
1029
|
|
|
809
1030
|
/** A host adapter's name as people know it (`S3 + CloudFront`), or its id. */
|
|
@@ -1012,6 +1233,7 @@ function summaryLine({ name, synced, live, targets, origin }) {
|
|
|
1012
1233
|
// ─── the command ────────────────────────────────────────────────────────────
|
|
1013
1234
|
|
|
1014
1235
|
const GROUPS = [
|
|
1236
|
+
['project', 'Project'],
|
|
1015
1237
|
['git', 'Git'],
|
|
1016
1238
|
['app', 'App'],
|
|
1017
1239
|
['hosting', 'Hosting'],
|
|
@@ -1072,6 +1294,97 @@ function render({ summary, backend, checks, next }) {
|
|
|
1072
1294
|
return out.join('\n')
|
|
1073
1295
|
}
|
|
1074
1296
|
|
|
1297
|
+
/**
|
|
1298
|
+
* At a workspace with several sites: what each one is — synced where, deployed where, built
|
|
1299
|
+
* with which foundation, with pages from where, in how many languages — from its files alone,
|
|
1300
|
+
* and the way to one site's checks. Nothing is asked of a backend: what changed in the app is
|
|
1301
|
+
* a site's question.
|
|
1302
|
+
*/
|
|
1303
|
+
async function workspaceOverview(root, { json }) {
|
|
1304
|
+
const ws = await workspaceFacts(root)
|
|
1305
|
+
const { mountEntriesOf } = await import('@uniweb/build/site')
|
|
1306
|
+
const { normalizeLanguageList } = await import('@uniweb/core/locale-config')
|
|
1307
|
+
const sites = []
|
|
1308
|
+
for (const s of ws.sites) {
|
|
1309
|
+
const dir = resolve(root, s.path)
|
|
1310
|
+
const yml = readSiteYml(dir)
|
|
1311
|
+
const synced = listSyncedBackends(dir).filter((o) => readBackendState(dir, o).site?.uuid)
|
|
1312
|
+
const { targets } = await deployChecks(dir)
|
|
1313
|
+
const fnd = await foundationOf(dir, yml, root)
|
|
1314
|
+
const siteRepo = repoOf(dir)?.top ?? null
|
|
1315
|
+
const mounts = mountEntriesOf(yml.paths).map(([segment, d]) => {
|
|
1316
|
+
const abs = resolve(dir, String(d))
|
|
1317
|
+
const top = existsSync(abs) ? repoOf(abs)?.top ?? null : null
|
|
1318
|
+
return { segment, dir: String(d), repository: !existsSync(abs) ? null : !top ? 'none' : top === siteRepo ? 'same' : 'other' }
|
|
1319
|
+
})
|
|
1320
|
+
const languages = normalizeLanguageList(yml.languages)
|
|
1321
|
+
const parts = [synced.length ? `synced with ${synced.join(' and ')}` : 'not synced']
|
|
1322
|
+
if (targets.length) parts.push(`deployed to ${targets.map((t) => t.title).join(' and ')}`)
|
|
1323
|
+
parts.push(
|
|
1324
|
+
fnd.kind === 'local'
|
|
1325
|
+
? `foundation ${fnd.name}`
|
|
1326
|
+
: fnd.kind === 'catalog' || fnd.kind === 'url'
|
|
1327
|
+
? `foundation ${fnd.name}, not in this workspace`
|
|
1328
|
+
: `foundation ${fnd.name ? `\`${fnd.name}\`, unresolved` : 'not named'}`
|
|
1329
|
+
)
|
|
1330
|
+
for (const m of mounts) parts.push(`pages/${m.segment} from ${m.dir}${m.repository === 'other' ? ', its own repository' : ''}`)
|
|
1331
|
+
if (languages.length > 1) parts.push(`${languages.length} languages`)
|
|
1332
|
+
sites.push({
|
|
1333
|
+
path: s.path,
|
|
1334
|
+
name: s.name,
|
|
1335
|
+
summary: `${s.path} — ${parts.join(' · ')}`,
|
|
1336
|
+
synced,
|
|
1337
|
+
deploys: targets.map(({ name, host, title, default: isDefault, ci }) => ({ name, host, title, default: isDefault, ci })),
|
|
1338
|
+
foundation: { kind: fnd.kind, name: fnd.name ?? null, path: fnd.path ?? null },
|
|
1339
|
+
mounts,
|
|
1340
|
+
languages
|
|
1341
|
+
})
|
|
1342
|
+
}
|
|
1343
|
+
// The repository the workspace is in, as far as this machine knows — no remote is asked.
|
|
1344
|
+
const repo = repoOf(root)
|
|
1345
|
+
let git = null
|
|
1346
|
+
if (repo?.head) {
|
|
1347
|
+
let uncommitted = 0
|
|
1348
|
+
try {
|
|
1349
|
+
uncommitted = execFileSync('git', ['status', '--porcelain'], { cwd: root, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] })
|
|
1350
|
+
.split('\n')
|
|
1351
|
+
.filter(Boolean).length
|
|
1352
|
+
} catch {
|
|
1353
|
+
uncommitted = 0
|
|
1354
|
+
}
|
|
1355
|
+
git = { uncommitted, ...(aheadBehind(root) || {}), upstream: upstreamOf(root)?.tracking ?? null }
|
|
1356
|
+
}
|
|
1357
|
+
const summary = `${basename(root)} — ${workspaceLine(ws).replace(/\.$/, '')}`
|
|
1358
|
+
const first = ws.sites[0]
|
|
1359
|
+
if (json) {
|
|
1360
|
+
console.log(
|
|
1361
|
+
JSON.stringify({
|
|
1362
|
+
schema: STATUS_SCHEMA,
|
|
1363
|
+
uniweb: getCliVersion(),
|
|
1364
|
+
scope: 'workspace',
|
|
1365
|
+
summary,
|
|
1366
|
+
workspace: { patterns: ws.patterns, sites: ws.sites, foundations: ws.foundations, extensions: ws.extensions },
|
|
1367
|
+
sites,
|
|
1368
|
+
git,
|
|
1369
|
+
checks: [],
|
|
1370
|
+
next: [{ command: null, why: "run `npx uniweb status` in a site's folder for its checks — what changed in the app, in git, and where it is hosted", ask: false }]
|
|
1371
|
+
})
|
|
1372
|
+
)
|
|
1373
|
+
return { exitCode: 0 }
|
|
1374
|
+
}
|
|
1375
|
+
const out = ['', `${c.bold}${summary}${c.reset}`, '']
|
|
1376
|
+
for (const s of sites) out.push(` ${s.summary}`)
|
|
1377
|
+
if (git) {
|
|
1378
|
+
const said = [git.uncommitted ? `${count(git.uncommitted, 'file')} not committed` : 'all committed']
|
|
1379
|
+
if (git.upstream) said.push(git.ahead || git.behind ? `${git.ahead} ahead of, ${git.behind} behind ${git.upstream} as of the last fetch` : `level with ${git.upstream} as of the last fetch`)
|
|
1380
|
+
out.push('', `${c.dim}Git: ${said.join(' · ')}${c.reset}`)
|
|
1381
|
+
}
|
|
1382
|
+
out.push('', `A bare \`uniweb dev\` starts ${first.name}. For one site's checks — what changed in the app, in git, and where it is hosted — run status in its folder, for example:`)
|
|
1383
|
+
out.push(` ${c.cyan}cd ${first.path} && npx uniweb status${c.reset}`, '')
|
|
1384
|
+
console.log(out.join('\n'))
|
|
1385
|
+
return { exitCode: 0 }
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1075
1388
|
export async function status(args = []) {
|
|
1076
1389
|
// See utils/flag-guard.js — an unrecognized flag is invisible to a literal scan.
|
|
1077
1390
|
// Straight to stderr: this is a usage error, so it must not be mistaken for the
|
|
@@ -1086,15 +1399,11 @@ export async function status(args = []) {
|
|
|
1086
1399
|
|
|
1087
1400
|
const found = await findSite(process.cwd())
|
|
1088
1401
|
if (!found.siteDir) {
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
for (const s of sites) console.error(` ${c.cyan}cd ${s} && npx uniweb status${c.reset}`)
|
|
1095
|
-
} else {
|
|
1096
|
-
console.error(`${c.red}✗${c.reset} No Uniweb site here. Run status from a site, or a project with one site.`)
|
|
1097
|
-
}
|
|
1402
|
+
// ⭐ A WORKSPACE OF SEVERAL SITES IS ANSWERED, not refused: what each one is, from its
|
|
1403
|
+
// files, and the way to one site's checks.
|
|
1404
|
+
if (found.several?.length) return workspaceOverview(found.root, { json })
|
|
1405
|
+
if (json) console.log(JSON.stringify({ schema: STATUS_SCHEMA, scope: null, error: 'no-site' }))
|
|
1406
|
+
else console.error(`${c.red}✗${c.reset} No Uniweb site here. Run status from a site, or a project with one site.`)
|
|
1098
1407
|
return { exitCode: 2 }
|
|
1099
1408
|
}
|
|
1100
1409
|
const siteDir = found.siteDir
|
|
@@ -1105,21 +1414,21 @@ export async function status(args = []) {
|
|
|
1105
1414
|
// helpers say on the way goes to stderr.
|
|
1106
1415
|
const saved = { log: console.log, info: console.info, warn: console.warn }
|
|
1107
1416
|
if (json) console.log = console.info = console.warn = (...a) => console.error(...a)
|
|
1108
|
-
let git, app, version, health, dev, deploys, services
|
|
1417
|
+
let project, git, app, version, health, dev, deploys, services
|
|
1109
1418
|
try {
|
|
1110
|
-
git = await gitChecks(siteDir, { offline, origin })
|
|
1419
|
+
;[project, git] = await Promise.all([projectChecks(siteDir, siteYml), gitChecks(siteDir, { offline, origin })])
|
|
1111
1420
|
;[app, version, health, dev, deploys] = await Promise.all([
|
|
1112
1421
|
appChecks(siteDir, siteYml, { offline, args, git }),
|
|
1113
1422
|
versionCheck(siteDir, { offline }),
|
|
1114
1423
|
doctorCheck(siteDir),
|
|
1115
|
-
devCheck(siteDir, { offline }),
|
|
1424
|
+
devCheck(siteDir, { offline, sites: project.ws.sites }),
|
|
1116
1425
|
deployChecks(siteDir)
|
|
1117
1426
|
])
|
|
1118
1427
|
services = await servicesCheck({ siteDir, siteYml, site: app.site })
|
|
1119
1428
|
} finally {
|
|
1120
1429
|
Object.assign(console, saved)
|
|
1121
1430
|
}
|
|
1122
|
-
const checks = [...git.checks, ...app.checks, ...deploys.checks, ...services.checks, ...version.checks, ...health.checks, dev]
|
|
1431
|
+
const checks = [...project.checks, ...git.checks, ...app.checks, ...deploys.checks, ...services.checks, ...version.checks, ...health.checks, dev]
|
|
1123
1432
|
const next = planNext([...git.proposals, ...app.proposals, ...deploys.proposals, ...services.proposals, ...version.proposals, ...health.proposals])
|
|
1124
1433
|
|
|
1125
1434
|
const root = findWorkspaceRoot(siteDir) || siteDir
|
|
@@ -1139,6 +1448,7 @@ export async function status(args = []) {
|
|
|
1139
1448
|
JSON.stringify({
|
|
1140
1449
|
schema: STATUS_SCHEMA,
|
|
1141
1450
|
uniweb: getCliVersion(),
|
|
1451
|
+
scope: 'site',
|
|
1142
1452
|
summary,
|
|
1143
1453
|
site,
|
|
1144
1454
|
backend: app.backend.origin,
|
package/src/framework-index.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
|
-
"generatedAt": "2026-10-26T17:
|
|
3
|
+
"generatedAt": "2026-10-26T17:53:53.949Z",
|
|
4
4
|
"packages": {
|
|
5
5
|
"@uniweb/api": {
|
|
6
6
|
"version": "1.6.4"
|
|
7
7
|
},
|
|
8
8
|
"@uniweb/build": {
|
|
9
|
-
"version": "1.10.
|
|
9
|
+
"version": "1.10.4"
|
|
10
10
|
},
|
|
11
11
|
"@uniweb/content-reader": {
|
|
12
12
|
"version": "1.5.7"
|
package/src/index.js
CHANGED
|
@@ -1781,6 +1781,9 @@ Run it when you begin, when you come back to the project, before you push, publi
|
|
|
1781
1781
|
or deploy, and after a command refuses. It opens with what the site is — synced
|
|
1782
1782
|
where, live where, deployed where — and checks:
|
|
1783
1783
|
|
|
1784
|
+
project the workspace's sites and foundations, the foundation this site is built
|
|
1785
|
+
with and the other sites it reaches, pages from outside the site's folder,
|
|
1786
|
+
the languages
|
|
1784
1787
|
git conflicts, a merge waiting, the site's files not committed, the git remote
|
|
1785
1788
|
app the session, what changed on the backend since this copy last synced —
|
|
1786
1789
|
the files a pull would change — what is not pushed, who has the site
|
|
@@ -1791,7 +1794,8 @@ where, live where, deployed where — and checks:
|
|
|
1791
1794
|
health doctor's findings
|
|
1792
1795
|
dev the dev server
|
|
1793
1796
|
|
|
1794
|
-
and ends with the steps to take, in the order to take them.
|
|
1797
|
+
and ends with the steps to take, in the order to take them. At the root of a
|
|
1798
|
+
workspace with several sites, it says what each one is. A step marked
|
|
1795
1799
|
"ask the person first" is theirs to decide — going live, for one.
|
|
1796
1800
|
|
|
1797
1801
|
It changes nothing and asks nothing: with no session it says how to sign in.
|