uniweb 1.0.0 → 1.0.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/LICENSE +201 -0
- package/README.md +532 -3
- package/package.json +60 -11
- package/partials/adding-pages.hbs +77 -0
- package/partials/agents.md +3000 -0
- package/partials/ai-assistance.hbs +28 -0
- package/partials/components-docs.hbs +13 -0
- package/partials/config-reference.hbs +66 -0
- package/partials/deployment.hbs +13 -0
- package/partials/learn-more.hbs +6 -0
- package/partials/quick-start.hbs +8 -0
- package/partials/search-docs.hbs +110 -0
- package/src/backend/asset-download.js +222 -0
- package/src/backend/client.js +986 -0
- package/src/backend/foundation-bring-along.js +692 -0
- package/src/backend/payment-handoff.js +362 -0
- package/src/backend/service-request.js +210 -0
- package/src/backend/site-media.js +255 -0
- package/src/backend/site-preview.js +77 -0
- package/src/backend/site-sync.js +2515 -0
- package/src/backend/workspace.js +234 -0
- package/src/commands/add.js +1900 -0
- package/src/commands/build.js +1214 -0
- package/src/commands/clone.js +466 -0
- package/src/commands/content.js +202 -0
- package/src/commands/deploy.js +537 -0
- package/src/commands/dev.js +202 -0
- package/src/commands/docs.js +530 -0
- package/src/commands/doctor.js +1982 -0
- package/src/commands/export.js +116 -0
- package/src/commands/families.js +82 -0
- package/src/commands/forget.js +197 -0
- package/src/commands/handoff.js +21 -0
- package/src/commands/i18n.js +1707 -0
- package/src/commands/inspect.js +375 -0
- package/src/commands/invite.js +24 -0
- package/src/commands/org.js +96 -0
- package/src/commands/publish.js +1087 -0
- package/src/commands/pull.js +1257 -0
- package/src/commands/push.js +663 -0
- package/src/commands/refresh.js +212 -0
- package/src/commands/register.js +1092 -0
- package/src/commands/rename.js +647 -0
- package/src/commands/site.js +358 -0
- package/src/commands/snapshot.js +461 -0
- package/src/commands/starter.js +206 -0
- package/src/commands/status.js +302 -0
- package/src/commands/sync.js +93 -0
- package/src/commands/template.js +107 -0
- package/src/commands/update.js +887 -0
- package/src/commands/validate.js +376 -0
- package/src/framework-index.json +248 -0
- package/src/index.js +2241 -0
- package/src/templates/fetchers/github.js +113 -0
- package/src/templates/fetchers/npm.js +85 -0
- package/src/templates/fetchers/release.js +92 -0
- package/src/templates/index.js +190 -0
- package/src/templates/processor.js +333 -0
- package/src/templates/resolver.js +232 -0
- package/src/templates/validator.js +155 -0
- package/src/utils/agents-stamp.js +110 -0
- package/src/utils/args.js +126 -0
- package/src/utils/asset-upload.js +270 -0
- package/src/utils/bytes.js +30 -0
- package/src/utils/code-upload.js +394 -0
- package/src/utils/config.js +417 -0
- package/src/utils/conformance.js +274 -0
- package/src/utils/dep-survey.js +176 -0
- package/src/utils/destination-prompt.js +146 -0
- package/src/utils/discover.js +75 -0
- package/src/utils/env.js +22 -0
- package/src/utils/exit.js +30 -0
- package/src/utils/fetch-retry.js +109 -0
- package/src/utils/flag-guard.js +246 -0
- package/src/utils/foundation-name.js +179 -0
- package/src/utils/git.js +342 -0
- package/src/utils/host-prompt.js +51 -0
- package/src/utils/install-integrity.js +210 -0
- package/src/utils/interactive.js +118 -0
- package/src/utils/json-file.js +72 -0
- package/src/utils/names.js +91 -0
- package/src/utils/placement.js +151 -0
- package/src/utils/pm.js +165 -0
- package/src/utils/preview.js +20 -0
- package/src/utils/pull-written.js +132 -0
- package/src/utils/records-guard.js +88 -0
- package/src/utils/registry-auth.js +994 -0
- package/src/utils/registry-orgs.js +290 -0
- package/src/utils/scaffold.js +483 -0
- package/src/utils/schemaless-report.js +30 -0
- package/src/utils/semver-precedence.js +80 -0
- package/src/utils/session-file.js +95 -0
- package/src/utils/site-content-refs.js +23 -0
- package/src/utils/site-copies.js +123 -0
- package/src/utils/site-data-upload.js +302 -0
- package/src/utils/site-identity.js +137 -0
- package/src/utils/snapshot-settings.js +112 -0
- package/src/utils/update-check.js +302 -0
- package/src/utils/uwx-read.js +157 -0
- package/src/utils/workspace.js +223 -0
- package/src/versions.js +467 -0
- package/starter/foundation/main.js +50 -0
- package/starter/foundation/sections/Section/index.jsx +95 -0
- package/starter/foundation/sections/Section/meta.js +44 -0
- package/starter/site/pages/about/1-about.md.hbs +13 -0
- package/starter/site/pages/about/page.yml +3 -0
- package/starter/site/pages/home/1-welcome.md.hbs +14 -0
- package/starter/site/pages/home/page.yml +3 -0
- package/templates/foundation/_gitignore +5 -0
- package/templates/foundation/components/.gitkeep +0 -0
- package/templates/foundation/main.js.hbs +33 -0
- package/templates/foundation/package.json.hbs +51 -0
- package/templates/foundation/sections/.gitkeep +0 -0
- package/templates/foundation/styles.css +6 -0
- package/templates/foundation/vite.config.js +3 -0
- package/templates/site/_gitignore +12 -0
- package/templates/site/entry.js +7 -0
- package/templates/site/index.html.hbs +13 -0
- package/templates/site/package.json.hbs +27 -0
- package/templates/site/public/favicon.svg +7 -0
- package/templates/site/site.yml.hbs +91 -0
- package/templates/site/theme.yml +99 -0
- package/templates/site/vite.config.js +7 -0
- package/templates/workspace/AGENTS.md.hbs +2 -0
- package/templates/workspace/README.md.hbs +87 -0
- package/templates/workspace/_gitignore +29 -0
- package/templates/workspace/_vscode/settings.json +6 -0
- package/templates/workspace/package.json.hbs +20 -0
- package/templates/workspace/pnpm-workspace.yaml.hbs +26 -0
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The record of what a machine wrote into a site project — pull's own output.
|
|
3
|
+
*
|
|
4
|
+
* ⭐ And, since 2026-09-23, what a push delivered: the files of the units the backend
|
|
5
|
+
* now holds as they were sent (`recordPushedUnits`, site-sync.js). Both are "the last
|
|
6
|
+
* version this file synced at", which is what `pull --merge` needs as its ancestor —
|
|
7
|
+
* found again in the repo's history by this hash (`findCommitted`, utils/git.js).
|
|
8
|
+
*
|
|
9
|
+
* ## What it is for
|
|
10
|
+
*
|
|
11
|
+
* `uniweb pull` refuses to run when there are uncommitted changes under the files
|
|
12
|
+
* it rewrites, because it reconciles the working tree to the backend and would
|
|
13
|
+
* overwrite them. That guard needs one distinction to be useful: **a file the
|
|
14
|
+
* user edited** versus **a file a previous pull wrote and nobody has touched
|
|
15
|
+
* since**. Without it the guard cries wolf — pull rewrites the tree, so the next
|
|
16
|
+
* pull sees its own output as uncommitted work and refuses, listing files the
|
|
17
|
+
* user never touched. A guard that fires on nothing teaches people to reach for
|
|
18
|
+
* `--force`, which is the destructive option.
|
|
19
|
+
*
|
|
20
|
+
* So each write is recorded with a content hash, and a dirty path whose hash
|
|
21
|
+
* still matches is not user work.
|
|
22
|
+
*
|
|
23
|
+
* ## Why it lives here rather than in `pull.js`
|
|
24
|
+
*
|
|
25
|
+
* `uniweb clone` has the same claim to make and cannot make it from there.
|
|
26
|
+
* `pull.js` statically imports `@uniweb/build`, which resolves from the
|
|
27
|
+
* *project's* `node_modules` — and `clone` runs before a project exists (see
|
|
28
|
+
* `utils/uwx-read.js` for the same constraint on the `.uwx` reader).
|
|
29
|
+
*
|
|
30
|
+
* Clone scaffolds `site.yml` and `theme.yml` and then delegates to `pull`, so
|
|
31
|
+
* without this the delegated pull sees clone's own scaffolding as uncommitted
|
|
32
|
+
* user work and refuses — on a project created seconds earlier, where there is no
|
|
33
|
+
* user work to protect. Clone records what it wrote; the guard then exempts those
|
|
34
|
+
* files **for the right reason** rather than being overridden.
|
|
35
|
+
*
|
|
36
|
+
* Nothing here imports `@uniweb/build`, and nothing here may start to.
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
import { readFileSync, writeFileSync, mkdirSync } from 'node:fs'
|
|
40
|
+
import { createHash } from 'node:crypto'
|
|
41
|
+
import { join, dirname, relative } from 'node:path'
|
|
42
|
+
|
|
43
|
+
/** Where the record lives — gitignored, beside the other per-site caches. */
|
|
44
|
+
export function writtenCachePath(siteDir) {
|
|
45
|
+
return join(siteDir, '.uniweb', 'pull-written.json')
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* @param {string} siteDir
|
|
50
|
+
* @returns {{files: Record<string,string>, deleted: string[]}}
|
|
51
|
+
*/
|
|
52
|
+
export function readWritten(siteDir) {
|
|
53
|
+
try {
|
|
54
|
+
const o = JSON.parse(readFileSync(writtenCachePath(siteDir), 'utf8'))
|
|
55
|
+
return {
|
|
56
|
+
files: o && typeof o.files === 'object' ? o.files : {},
|
|
57
|
+
deleted: Array.isArray(o?.deleted) ? o.deleted : []
|
|
58
|
+
}
|
|
59
|
+
} catch {
|
|
60
|
+
return { files: {}, deleted: [] }
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Record files as machine-written, by absolute path.
|
|
66
|
+
*
|
|
67
|
+
* MERGE, don't replace. A conditional pull that 304s writes nothing, and a
|
|
68
|
+
* partial pull writes only some lanes — in both cases the previous record is
|
|
69
|
+
* still "the last thing written there". Replacing would forget those paths and
|
|
70
|
+
* the next pull would see them as the user's work again, which is the false alarm
|
|
71
|
+
* this cache exists to prevent. A stale entry for a file that no longer exists is
|
|
72
|
+
* harmless: the hash read fails and it counts as a local change.
|
|
73
|
+
*
|
|
74
|
+
* @param {string} siteDir
|
|
75
|
+
* @param {string[]} absPaths - files just written
|
|
76
|
+
* @param {string[]} [deletedAbs] - files just pruned
|
|
77
|
+
*/
|
|
78
|
+
export function recordWritten(siteDir, absPaths, deletedAbs = []) {
|
|
79
|
+
const prior = readWritten(siteDir)
|
|
80
|
+
const files = prior.files
|
|
81
|
+
for (const abs of absPaths) {
|
|
82
|
+
try {
|
|
83
|
+
files[relative(siteDir, abs)] = createHash('sha256')
|
|
84
|
+
.update(readFileSync(abs))
|
|
85
|
+
.digest('hex')
|
|
86
|
+
} catch {
|
|
87
|
+
/* deleted or unreadable — nothing to remember */
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
// Pull PRUNES too, and a deletion is a dirty path git reports just like an edit.
|
|
91
|
+
// Without recording them, pull's own pruning reads as the user having deleted
|
|
92
|
+
// files — the same false alarm as its writes, arriving by the other door.
|
|
93
|
+
const deleted = [
|
|
94
|
+
...new Set([...prior.deleted, ...deletedAbs.map((a) => relative(siteDir, a))])
|
|
95
|
+
]
|
|
96
|
+
try {
|
|
97
|
+
mkdirSync(dirname(writtenCachePath(siteDir)), { recursive: true })
|
|
98
|
+
writeFileSync(
|
|
99
|
+
writtenCachePath(siteDir),
|
|
100
|
+
JSON.stringify({ version: 1, files, deleted }, null, 2) + '\n'
|
|
101
|
+
)
|
|
102
|
+
} catch {
|
|
103
|
+
/* best-effort: losing it only costs a spurious refusal */
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Is this dirty path just machine output nobody has touched?
|
|
109
|
+
*
|
|
110
|
+
* The hash comparison is what keeps the guard honest: a scaffolded or pulled file
|
|
111
|
+
* the user then EDITED no longer matches, so it counts as their work and the
|
|
112
|
+
* refusal stands.
|
|
113
|
+
*
|
|
114
|
+
* @param {string} siteDir
|
|
115
|
+
* @param {string} relPath
|
|
116
|
+
* @param {{files: Record<string,string>, deleted: string[]}} written
|
|
117
|
+
* @returns {boolean}
|
|
118
|
+
*/
|
|
119
|
+
export function isPullOutput(siteDir, relPath, written) {
|
|
120
|
+
let exists = true
|
|
121
|
+
let hash = null
|
|
122
|
+
try {
|
|
123
|
+
hash = createHash('sha256')
|
|
124
|
+
.update(readFileSync(join(siteDir, relPath)))
|
|
125
|
+
.digest('hex')
|
|
126
|
+
} catch {
|
|
127
|
+
exists = false
|
|
128
|
+
}
|
|
129
|
+
// Absent because pull pruned it — not because the user deleted it.
|
|
130
|
+
if (!exists) return written.deleted.includes(relPath)
|
|
131
|
+
return written.files[relPath] === hash
|
|
132
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// ⛔ THE ONE PLACE AN ORDINARY ACT IS DESTRUCTIVE.
|
|
2
|
+
//
|
|
3
|
+
// A push sends the site's records folder whole, and it REPLACES the backend's. A site
|
|
4
|
+
// with no records directory sends none and leaves the backend's alone; a records
|
|
5
|
+
// directory that holds no records sends an EMPTY folder, and the backend removes what
|
|
6
|
+
// is there. The asymmetry is well-shaped — the safe state is the ABSENCE of the
|
|
7
|
+
// directory, so a site whose records live only on the backend is never emptied by a
|
|
8
|
+
// push of its pages — and it leaves exactly one sharp edge: a directory emptied by
|
|
9
|
+
// accident, or kept with only a placeholder (`records/.gitkeep`), and pushed.
|
|
10
|
+
//
|
|
11
|
+
// ⭐ THE CLI DOES THE ASKING. Never make "empty" mean "missing" to dodge this: that
|
|
12
|
+
// would delete a capability to avoid writing a prompt.
|
|
13
|
+
//
|
|
14
|
+
// ⛔ Until 2026-09-21 both states were `records.yml`'s — missing and empty — because
|
|
15
|
+
// that file listed the records. The directory holds them now (`@uniweb/build`'s
|
|
16
|
+
// `sendsFolder` decides what a push sends, and this asks about exactly its empty case).
|
|
17
|
+
//
|
|
18
|
+
// The count comes from the placement identity a previous push banked — what WE
|
|
19
|
+
// last saw the folder hold. It needs no network call, and it is the right source:
|
|
20
|
+
// a site that has never pushed has nothing to lose and is never asked.
|
|
21
|
+
|
|
22
|
+
import { readEntityPool } from '@uniweb/build/uwx'
|
|
23
|
+
import { readFolderItemUuids } from '../backend/site-sync.js'
|
|
24
|
+
import { confirm, isNonInteractive, getCliPrefix } from './interactive.js'
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Record placements in a banked placement map.
|
|
28
|
+
*
|
|
29
|
+
* ⭐ A placement banked since 2026-09-21 is keyed by the record it references —
|
|
30
|
+
* `@<record uuid>`, one per record — so where those exist they ARE the count. A map
|
|
31
|
+
* banked before holds `name` chains only: a branch's path is a prefix of every path
|
|
32
|
+
* beneath it, so anything that is a prefix of another key is a folder rather than a
|
|
33
|
+
* record. Counting raw keys would report a two-record site inside one folder as
|
|
34
|
+
* three things to lose — or, with both kinds of key, each record twice.
|
|
35
|
+
*/
|
|
36
|
+
export function countPlacedRecords(pathToUuid) {
|
|
37
|
+
const keys = Object.keys(pathToUuid || {})
|
|
38
|
+
const byRecord = keys.filter((k) => k.startsWith('@'))
|
|
39
|
+
if (byRecord.length) return byRecord.length
|
|
40
|
+
return keys.filter((p) => !keys.some((q) => q !== p && q.startsWith(`${p}/`))).length
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Stop an empty records directory from silently emptying the backend's folder.
|
|
45
|
+
*
|
|
46
|
+
* @param {object} params
|
|
47
|
+
* @param {string} params.siteDir
|
|
48
|
+
* @param {string} params.backend - whose placements to count; they are minted per backend
|
|
49
|
+
* @param {string[]} params.args - the verb's argv, for --yes / non-interactive
|
|
50
|
+
* @param {(m: string) => void} params.warn - the CALLER's reporter. Each verb owns
|
|
51
|
+
* its own output style; a second copy here would drift from all of them.
|
|
52
|
+
* @param {(m: string) => void} params.note
|
|
53
|
+
* @returns {Promise<{ ok: boolean, count: number }>} `ok: false` means abort
|
|
54
|
+
*/
|
|
55
|
+
export async function guardEmptyRecords({ siteDir, backend, args = [], warn, note }) {
|
|
56
|
+
const pool = await readEntityPool(siteDir)
|
|
57
|
+
// Only a directory that is there and holds nothing at all — no records, and no
|
|
58
|
+
// files that fail to be records — sends the empty folder.
|
|
59
|
+
if (!pool.exists || pool.entities.length > 0 || pool.errors.length > 0) return { ok: true, count: 0 }
|
|
60
|
+
|
|
61
|
+
const count = countPlacedRecords(readFolderItemUuids(siteDir, backend))
|
|
62
|
+
// Nothing banked ⇒ nothing this push can remove. A first push of an empty
|
|
63
|
+
// folder is a legitimate (if odd) thing to do, and asking about it would train
|
|
64
|
+
// people to type y.
|
|
65
|
+
if (count === 0) return { ok: true, count: 0 }
|
|
66
|
+
|
|
67
|
+
warn(
|
|
68
|
+
`${pool.dir}/ holds no records, and this push would REMOVE ${count} record${count === 1 ? '' : 's'} ` +
|
|
69
|
+
`from the site's folder on the backend.`
|
|
70
|
+
)
|
|
71
|
+
note(
|
|
72
|
+
`An empty ${pool.dir}/ means "the folder holds nothing" — it is not the same as having no ` +
|
|
73
|
+
`${pool.dir}/ at all, which leaves the backend's folder alone. If you did not mean to remove ` +
|
|
74
|
+
`them, put the files back (or pull) before pushing.`
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
// ⚠️ `--yes` ONLY. `-y` is not a flag this CLI has anywhere, and adding one here
|
|
78
|
+
// would have been caught by `flag-guard-coverage.test.js` — which it was.
|
|
79
|
+
if (args.includes('--yes')) return { ok: true, count }
|
|
80
|
+
if (isNonInteractive(args)) {
|
|
81
|
+
warn(`Refusing to remove ${count} record${count === 1 ? '' : 's'} without confirmation.`)
|
|
82
|
+
note(`Re-run with --yes if that is what you want: ${getCliPrefix()} push --yes`)
|
|
83
|
+
return { ok: false, count }
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const yes = await confirm(`Remove ${count} record${count === 1 ? '' : 's'} from the site's folder?`, false)
|
|
87
|
+
return { ok: yes, count }
|
|
88
|
+
}
|