uniweb 1.20.2 → 1.20.4
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 +145 -113
- package/package.json +5 -5
- package/partials/agents.md +3 -3
- package/partials/start.md +109 -0
- package/src/backend/payment-handoff.js +25 -2
- package/src/backend/workspace.js +38 -0
- package/src/commands/clone.js +2 -0
- package/src/commands/publish.js +8 -5
- package/src/commands/push.js +7 -2
- package/src/commands/start.js +60 -0
- package/src/commands/update.js +1 -23
- package/src/framework-index.json +2 -2
- package/src/index.js +46 -0
- package/src/utils/registry-auth.js +15 -5
- package/src/utils/scaffold.js +4 -0
- package/src/utils/workspace.js +26 -0
- package/starter/foundation/sections/Section/index.jsx +6 -6
- package/starter/site/pages/home/1-welcome.md.hbs +1 -1
- package/templates/workspace/README.md.hbs +132 -50
- package/partials/adding-pages.hbs +0 -77
- package/partials/ai-assistance.hbs +0 -28
- package/partials/components-docs.hbs +0 -13
- package/partials/config-reference.hbs +0 -66
- package/partials/deployment.hbs +0 -13
- package/partials/learn-more.hbs +0 -6
- package/partials/quick-start.hbs +0 -8
- package/partials/search-docs.hbs +0 -110
|
@@ -244,6 +244,28 @@ async function openDoor({ url, say, open }) {
|
|
|
244
244
|
return { opened }
|
|
245
245
|
}
|
|
246
246
|
|
|
247
|
+
/**
|
|
248
|
+
* Where a charge that was not asked is confirmed — at a terminal with `--yes`, or with no one at
|
|
249
|
+
* the terminal at all.
|
|
250
|
+
*
|
|
251
|
+
* ⛔ **UNATTENDED, NEVER "AT A TERMINAL".** The run with no one at the terminal is often an AI agent
|
|
252
|
+
* working for someone who does not use one, and it passes this line on: the person confirms in
|
|
253
|
+
* their browser, at the door the refusal printed, or in the app when there is none. *(Until
|
|
254
|
+
* 2026-10-26 every case said "run `uniweb publish` at a terminal".)* Either way a person confirms
|
|
255
|
+
* it, which is why the line says that `--yes` cannot.
|
|
256
|
+
*
|
|
257
|
+
* @param {{ interactive: boolean, door: boolean }} o - someone is at the terminal; a door was printed
|
|
258
|
+
* @returns {string}
|
|
259
|
+
*/
|
|
260
|
+
function confirmChargeLine({ interactive, door }) {
|
|
261
|
+
if (interactive) {
|
|
262
|
+
return 'This change charges the site\'s card, so only you can confirm it: run `uniweb publish` without `--yes`.'
|
|
263
|
+
}
|
|
264
|
+
return door
|
|
265
|
+
? 'This change charges the site\'s card, so a person confirms it, in a browser at the address above. `--yes` never confirms a charge.'
|
|
266
|
+
: 'This change charges the site\'s card, so a person confirms it, in the app. `--yes` never confirms a charge.'
|
|
267
|
+
}
|
|
268
|
+
|
|
247
269
|
const CONFIRM = 'Confirm and publish'
|
|
248
270
|
const CONTINUE = 'Continue and publish'
|
|
249
271
|
const OPEN = 'Open it in the app'
|
|
@@ -275,7 +297,8 @@ const MAX_UNATTENDED = 3
|
|
|
275
297
|
* "allow it for reductions only, keep charges interactive".]*
|
|
276
298
|
*
|
|
277
299
|
* With no one at the terminal and nothing to answer it, there is no prompt: the refusal is reported as
|
|
278
|
-
* before, its door printed, and the publish fails
|
|
300
|
+
* before, its door printed, and the publish fails — and a charge says where a person confirms it, in a
|
|
301
|
+
* browser (`confirmChargeLine`). A refusal with no `confirm` behaves as it always has.
|
|
279
302
|
*
|
|
280
303
|
* @param {object} o
|
|
281
304
|
* @param {ReturnType<typeof readPaymentRefusal>} o.verdict - the first refusal
|
|
@@ -312,7 +335,7 @@ export async function settleRefusal({ verdict, args = [], say, republish, intera
|
|
|
312
335
|
} else if (!offer || !interactive || (yes && !reduction)) {
|
|
313
336
|
await reportPaymentRefusal({ verdict: current, args, say, open, interactive: interactive && !yes })
|
|
314
337
|
if (offer && !reduction) {
|
|
315
|
-
say.dim(
|
|
338
|
+
say.dim(confirmChargeLine({ interactive, door: current.kind === 'settle' }))
|
|
316
339
|
} else if (reduction) {
|
|
317
340
|
say.dim('This change only lowers what the site pays: `uniweb publish --yes` continues it without a prompt.')
|
|
318
341
|
}
|
package/src/backend/workspace.js
CHANGED
|
@@ -139,6 +139,44 @@ export const SOURCE_LABEL = {
|
|
|
139
139
|
offline: 'not resolved in a preview'
|
|
140
140
|
}
|
|
141
141
|
|
|
142
|
+
/**
|
|
143
|
+
* What a preview (`--dry-run`) says about the workspace: the one the command would work in,
|
|
144
|
+
* and a warning when the project's record says its site was created in another.
|
|
145
|
+
*
|
|
146
|
+
* ⭐ A preview resolves the workspace offline (`resolveWorkspace`): a flag, `UNIWEB_WORKSPACE`
|
|
147
|
+
* or the login's choice. With none of those, which one the command settles on takes the
|
|
148
|
+
* backend, so the preview says what the command does instead of guessing.
|
|
149
|
+
*
|
|
150
|
+
* ⚠️ The record says where the site was CREATED (`readSiteWorkspace`), not where the backend
|
|
151
|
+
* keeps it now. The backend decides, and a site kept outside the workspace stops the command
|
|
152
|
+
* (`409 wrong_workspace`), so a difference is worded as a warning about the record.
|
|
153
|
+
*
|
|
154
|
+
* @param {{ workspace: string|null, source: string }} ws - `resolveWorkspace`'s answer
|
|
155
|
+
* @param {string|null} createdIn - `readSiteWorkspace` for the project's site on this backend;
|
|
156
|
+
* null when it has none, or no workspace is recorded for it
|
|
157
|
+
* @param {string} verb - `publish`, `push`
|
|
158
|
+
* @returns {{ workspace: string, warning: string|null }}
|
|
159
|
+
*/
|
|
160
|
+
export function previewWorkspace(ws, createdIn, verb) {
|
|
161
|
+
if (ws.source === 'offline') {
|
|
162
|
+
return {
|
|
163
|
+
workspace:
|
|
164
|
+
`none chosen at login — the ${verb} works in your personal workspace if you belong to no ` +
|
|
165
|
+
'organization or project, and otherwise stops and says how to choose one',
|
|
166
|
+
warning: null
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
const differs =
|
|
170
|
+
typeof createdIn === 'string' && createdIn.toLowerCase() !== String(ws.workspace ?? '').toLowerCase()
|
|
171
|
+
return {
|
|
172
|
+
workspace: `${describeWorkspace(ws.workspace)} (${SOURCE_LABEL[ws.source]})`,
|
|
173
|
+
warning: differs
|
|
174
|
+
? `sync.json records this site as created in ${describeWorkspace(createdIn)}. If the backend ` +
|
|
175
|
+
`still keeps it there, it stops this ${verb}: add --org ${createdIn} to work in it.`
|
|
176
|
+
: null
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
142
180
|
/** How to choose, for a command on `origin` — its login names the backend when it must (`loginCommand`). */
|
|
143
181
|
const choose = (origin) =>
|
|
144
182
|
[
|
package/src/commands/clone.js
CHANGED
|
@@ -421,6 +421,8 @@ export async function clone(args = [], deps = {}) {
|
|
|
421
421
|
{
|
|
422
422
|
projectName: workspaceName,
|
|
423
423
|
workspaceGlobs: ['site'],
|
|
424
|
+
packages: [{ path: 'site', site: true }],
|
|
425
|
+
cloned: true,
|
|
424
426
|
scripts: { dev: 'uniweb dev', build: 'uniweb build' }
|
|
425
427
|
},
|
|
426
428
|
{ onProgress }
|
package/src/commands/publish.js
CHANGED
|
@@ -111,11 +111,12 @@ import {
|
|
|
111
111
|
commitWriteBacks,
|
|
112
112
|
reportWriteBacks,
|
|
113
113
|
dropSiteBoundValues,
|
|
114
|
-
pushSyncPackages
|
|
114
|
+
pushSyncPackages,
|
|
115
|
+
readSiteWorkspace
|
|
115
116
|
} from '../backend/site-sync.js'
|
|
116
117
|
import { repoOf } from '../utils/sync-point.js'
|
|
117
118
|
import { commitBeforeSending } from '../backend/commit-before-sending.js'
|
|
118
|
-
import { resolveWorkspace, describeWorkspace, SOURCE_LABEL } from '../backend/workspace.js'
|
|
119
|
+
import { resolveWorkspace, describeWorkspace, previewWorkspace, SOURCE_LABEL } from '../backend/workspace.js'
|
|
119
120
|
import { uploadSiteMedia, describeAssetRefusal } from '../backend/site-media.js'
|
|
120
121
|
import {
|
|
121
122
|
updateBackendMap,
|
|
@@ -479,13 +480,15 @@ export async function publish(args = []) {
|
|
|
479
480
|
}
|
|
480
481
|
|
|
481
482
|
if (dryRun) {
|
|
483
|
+
const siteUuid = readBackendState(siteDir, client.origin).site?.uuid
|
|
484
|
+
const preview = previewWorkspace(ws, siteUuid ? readSiteWorkspace(siteDir, client.origin) : null, 'publish')
|
|
482
485
|
say.info('Dry run — would bring the foundation along, sync, and go live:')
|
|
483
486
|
say.dim(`Backend : ${client.origin}`)
|
|
484
|
-
say.dim(
|
|
485
|
-
|
|
486
|
-
)
|
|
487
|
+
say.dim(`Workspace : ${preview.workspace}`)
|
|
488
|
+
say.dim(`site_uuid : ${siteUuid || '(none — the site is created before anything uploads)'}`)
|
|
487
489
|
const langs = languagesFromSiteYml(siteYml)
|
|
488
490
|
if (langs) say.dim(`Languages : ${langs.join(', ')}`)
|
|
491
|
+
if (preview.warning) say.warn(preview.warning)
|
|
489
492
|
await bringFoundationAlong({
|
|
490
493
|
client,
|
|
491
494
|
siteDir,
|
package/src/commands/push.js
CHANGED
|
@@ -103,10 +103,11 @@ import {
|
|
|
103
103
|
commitWriteBacks,
|
|
104
104
|
reportWriteBacks,
|
|
105
105
|
holdUnchangedPush,
|
|
106
|
-
pushSyncPackages
|
|
106
|
+
pushSyncPackages,
|
|
107
|
+
readSiteWorkspace
|
|
107
108
|
} from '../backend/site-sync.js'
|
|
108
109
|
import { commitBeforeSending } from '../backend/commit-before-sending.js'
|
|
109
|
-
import { resolveWorkspace, describeWorkspace, SOURCE_LABEL } from '../backend/workspace.js'
|
|
110
|
+
import { resolveWorkspace, describeWorkspace, previewWorkspace, SOURCE_LABEL } from '../backend/workspace.js'
|
|
110
111
|
|
|
111
112
|
// Re-exported for downstream importers (pull.js, push.test.js) that read these
|
|
112
113
|
// helpers from this module — their canonical home is now ../backend/site-sync.js.
|
|
@@ -685,6 +686,10 @@ export async function push(args = [], deps = {}) {
|
|
|
685
686
|
`Dry run — would push the folder at ${colors.dim}${client.origin}${colors.reset}`
|
|
686
687
|
)
|
|
687
688
|
}
|
|
689
|
+
const siteUuid = readBackendState(siteDir, client.origin).site?.uuid
|
|
690
|
+
const preview = previewWorkspace(ws, siteUuid ? readSiteWorkspace(siteDir, client.origin) : null, 'push')
|
|
691
|
+
note(`Workspace: ${preview.workspace}`)
|
|
692
|
+
if (preview.warning) warn(preview.warning)
|
|
688
693
|
return { exitCode: 0 }
|
|
689
694
|
}
|
|
690
695
|
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `uniweb start` — the guide an AI agent reads first.
|
|
3
|
+
*
|
|
4
|
+
* ⭐ FOR AN AGENT WORKING FOR SOMEONE WHO DOES NOT USE A TERMINAL. The person asks their
|
|
5
|
+
* agent for a website; the agent runs `npx uniweb@latest start` and follows what it
|
|
6
|
+
* prints: create a project, read its AGENTS.md, run it, sign the person in, and bring the
|
|
7
|
+
* site into the Uniweb app. The person's part is their browser — signing in, and paying
|
|
8
|
+
* when the site goes live — so nothing here asks them to run a command.
|
|
9
|
+
*
|
|
10
|
+
* ⛔ READ-ONLY, AND SAFE ANYWHERE. It asks nothing, writes nothing and makes no request,
|
|
11
|
+
* so an agent can run it in any folder, as often as it likes. That is also why
|
|
12
|
+
* `index.js` answers it before the update check, which can write a cache and fetch.
|
|
13
|
+
*
|
|
14
|
+
* ⭐ THE TEXT SHIPS WITH THIS CLI (`partials/start.md`), so it describes the version that
|
|
15
|
+
* prints it. Inside a Uniweb project (`findUniwebWorkspace`) it starts from that project
|
|
16
|
+
* instead of creating one, and names that project's own package manager.
|
|
17
|
+
*
|
|
18
|
+
* It is answered by the CLI that was invoked, never delegated to a project's own copy
|
|
19
|
+
* (`STANDALONE_COMMANDS`): an older project CLI does not have it.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { existsSync, readFileSync } from 'node:fs'
|
|
23
|
+
import { dirname, join } from 'node:path'
|
|
24
|
+
import { fileURLToPath } from 'node:url'
|
|
25
|
+
import Handlebars from 'handlebars'
|
|
26
|
+
import { findUniwebWorkspace, findWorkspaceRoot } from '../utils/workspace.js'
|
|
27
|
+
import { detectWorkspacePm, installCmd } from '../utils/pm.js'
|
|
28
|
+
|
|
29
|
+
const PARTIAL = join(dirname(fileURLToPath(import.meta.url)), '..', '..', 'partials', 'start.md')
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* The guide, as Markdown, for a command run in `cwd`.
|
|
33
|
+
*
|
|
34
|
+
* Outside a project, `otherProject` names a workspace of another kind that holds `cwd`:
|
|
35
|
+
* `create` refuses to make a project inside one, so the guide says to create it elsewhere.
|
|
36
|
+
*
|
|
37
|
+
* @param {{ cwd?: string }} [o]
|
|
38
|
+
* @returns {string}
|
|
39
|
+
*/
|
|
40
|
+
export function startGuide({ cwd = process.cwd() } = {}) {
|
|
41
|
+
const root = findUniwebWorkspace(cwd)
|
|
42
|
+
const template = Handlebars.compile(readFileSync(PARTIAL, 'utf8'), { noEscape: true })
|
|
43
|
+
return template({
|
|
44
|
+
project: !!root,
|
|
45
|
+
root,
|
|
46
|
+
// A project `create` makes has one; one already here may not.
|
|
47
|
+
agents: root ? existsSync(join(root, 'AGENTS.md')) : true,
|
|
48
|
+
otherProject: root ? null : findWorkspaceRoot(cwd),
|
|
49
|
+
install: installCmd(root ? detectWorkspacePm(root) : 'npm')
|
|
50
|
+
})
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* @param {string[]} [args] - none are read
|
|
55
|
+
* @returns {Promise<{ exitCode: number }>}
|
|
56
|
+
*/
|
|
57
|
+
export async function start(args = []) {
|
|
58
|
+
console.log(startGuide())
|
|
59
|
+
return { exitCode: 0 }
|
|
60
|
+
}
|
package/src/commands/update.js
CHANGED
|
@@ -44,7 +44,7 @@ import { join } from 'node:path'
|
|
|
44
44
|
import { spawn } from 'node:child_process'
|
|
45
45
|
import prompts from 'prompts'
|
|
46
46
|
|
|
47
|
-
import {
|
|
47
|
+
import { findUniwebWorkspace } from '../utils/workspace.js'
|
|
48
48
|
import {
|
|
49
49
|
readAgentsVersion,
|
|
50
50
|
refreshAgentsContent,
|
|
@@ -108,28 +108,6 @@ function isNpxInvocation() {
|
|
|
108
108
|
return p.includes('/_npx/')
|
|
109
109
|
}
|
|
110
110
|
|
|
111
|
-
/**
|
|
112
|
-
* Find a *Uniweb* workspace root from cwd. Stricter than findWorkspaceRoot
|
|
113
|
-
* — also requires that the workspace's root package.json declares uniweb
|
|
114
|
-
* as a dep or devDep. Otherwise we'd write AGENTS.md and edit package.json
|
|
115
|
-
* files in unrelated monorepos.
|
|
116
|
-
*/
|
|
117
|
-
function findUniwebWorkspace(cwd) {
|
|
118
|
-
const workspaceDir = findWorkspaceRoot(cwd)
|
|
119
|
-
if (!workspaceDir) return null
|
|
120
|
-
const pkgPath = join(workspaceDir, 'package.json')
|
|
121
|
-
if (!existsSync(pkgPath)) return null
|
|
122
|
-
try {
|
|
123
|
-
const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'))
|
|
124
|
-
const hasUniwebDep = !!(
|
|
125
|
-
pkg.devDependencies?.uniweb || pkg.dependencies?.uniweb
|
|
126
|
-
)
|
|
127
|
-
return hasUniwebDep ? workspaceDir : null
|
|
128
|
-
} catch {
|
|
129
|
-
return null
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
|
|
133
111
|
/**
|
|
134
112
|
* Trailing advisory: a newer CLI exists, and this run could not have used it.
|
|
135
113
|
*
|
package/src/framework-index.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
|
-
"generatedAt": "2026-10-
|
|
3
|
+
"generatedAt": "2026-10-26T02:00:48.151Z",
|
|
4
4
|
"packages": {
|
|
5
5
|
"@uniweb/api": {
|
|
6
6
|
"version": "1.6.4"
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
"version": "1.0.6"
|
|
25
25
|
},
|
|
26
26
|
"@uniweb/kit": {
|
|
27
|
-
"version": "1.6.
|
|
27
|
+
"version": "1.6.5"
|
|
28
28
|
},
|
|
29
29
|
"@uniweb/loom": {
|
|
30
30
|
"version": "1.1.2"
|
package/src/index.js
CHANGED
|
@@ -152,6 +152,8 @@ function getCliVersion() {
|
|
|
152
152
|
// against *this* CLI's version matrix — when run from a (newer) global
|
|
153
153
|
// install that's the whole point; delegating to the project-local copy
|
|
154
154
|
// would align the project to the version it already has, i.e. a no-op.
|
|
155
|
+
// `start` is here because a project's older CLI may not have it, and its
|
|
156
|
+
// guide describes the CLI that prints it.
|
|
155
157
|
const STANDALONE_COMMANDS = new Set([
|
|
156
158
|
'create',
|
|
157
159
|
'clone',
|
|
@@ -161,6 +163,7 @@ const STANDALONE_COMMANDS = new Set([
|
|
|
161
163
|
'-v',
|
|
162
164
|
'login',
|
|
163
165
|
'logout',
|
|
166
|
+
'start',
|
|
164
167
|
'update'
|
|
165
168
|
])
|
|
166
169
|
|
|
@@ -347,6 +350,10 @@ async function createFromPackageTemplates(
|
|
|
347
350
|
{
|
|
348
351
|
projectName,
|
|
349
352
|
workspaceGlobs: ['site', 'src'],
|
|
353
|
+
packages: [
|
|
354
|
+
{ path: 'src', foundation: true },
|
|
355
|
+
{ path: 'site', site: true }
|
|
356
|
+
],
|
|
350
357
|
scripts: {
|
|
351
358
|
dev: 'uniweb dev',
|
|
352
359
|
build: 'uniweb build',
|
|
@@ -413,6 +420,7 @@ async function createBlankWorkspace(projectDir, projectName, options = {}) {
|
|
|
413
420
|
{
|
|
414
421
|
projectName,
|
|
415
422
|
workspaceGlobs: [],
|
|
423
|
+
packages: [],
|
|
416
424
|
scripts: {
|
|
417
425
|
build: 'uniweb build'
|
|
418
426
|
}
|
|
@@ -480,6 +488,8 @@ async function createFromContentTemplate(
|
|
|
480
488
|
{
|
|
481
489
|
projectName,
|
|
482
490
|
workspaceGlobs,
|
|
491
|
+
// `type` is foundation, extension or site: the README's table reads it as a flag.
|
|
492
|
+
packages: placed.map((p) => ({ path: p.relativePath, [p.type]: true })),
|
|
483
493
|
scripts
|
|
484
494
|
},
|
|
485
495
|
{ onProgress, onWarning }
|
|
@@ -696,6 +706,19 @@ async function main() {
|
|
|
696
706
|
// Commands that need @uniweb/build will get a helpful error via importProjectCommand().
|
|
697
707
|
}
|
|
698
708
|
|
|
709
|
+
// `start` — the guide an AI agent reads first (commands/start.js). Answered before the
|
|
710
|
+
// update check: it asks nothing, writes nothing and makes no request, and the check can
|
|
711
|
+
// write a cache and fetch.
|
|
712
|
+
if (command === 'start') {
|
|
713
|
+
if (args.slice(1).some((a) => a === '--help' || a === '-h')) {
|
|
714
|
+
printCommandHelp('start')
|
|
715
|
+
return
|
|
716
|
+
}
|
|
717
|
+
const { start } = await import('./commands/start.js')
|
|
718
|
+
await start(args.slice(1))
|
|
719
|
+
return
|
|
720
|
+
}
|
|
721
|
+
|
|
699
722
|
// Update check for global installs.
|
|
700
723
|
//
|
|
701
724
|
// - `create` / `clone` STOP when a newer release exists, before anything is
|
|
@@ -1132,6 +1155,9 @@ async function main() {
|
|
|
1132
1155
|
if (nonInteractive && !projectName) {
|
|
1133
1156
|
error(`Missing project name.\n`)
|
|
1134
1157
|
log(`Usage: ${prefix} create <project-name> [--template <name>] [--blank]`)
|
|
1158
|
+
log(
|
|
1159
|
+
`\nAn AI agent building a site for someone: run ${colors.cyan}npx uniweb@latest start${colors.reset} first — it says how, step by step.`
|
|
1160
|
+
)
|
|
1135
1161
|
await exitWhenDrained(1)
|
|
1136
1162
|
}
|
|
1137
1163
|
|
|
@@ -1389,6 +1415,10 @@ async function main() {
|
|
|
1389
1415
|
` ${colors.cyan}${runCmd(recPm, 'dev')}${colors.reset} ${colors.dim}# start the dev server${colors.reset}`
|
|
1390
1416
|
)
|
|
1391
1417
|
}
|
|
1418
|
+
log('')
|
|
1419
|
+
log(
|
|
1420
|
+
` ${colors.bright}AGENTS.md${colors.reset} is the project's manual, for you and for AI agents: its Part 0 says where to begin.`
|
|
1421
|
+
)
|
|
1392
1422
|
if (recPm !== 'pnpm') {
|
|
1393
1423
|
log('')
|
|
1394
1424
|
log(
|
|
@@ -1529,6 +1559,21 @@ ${colors.bright}Examples:${colors.reset}
|
|
|
1529
1559
|
uniweb create . # Scaffold into current dir
|
|
1530
1560
|
uniweb create . --template docs # In place + a content template
|
|
1531
1561
|
uniweb create . --name=my-app # In place, explicit slug
|
|
1562
|
+
`,
|
|
1563
|
+
start: `
|
|
1564
|
+
${colors.cyan}${colors.bright}uniweb start${colors.reset} ${colors.dim}— How an AI agent begins a Uniweb site${colors.reset}
|
|
1565
|
+
|
|
1566
|
+
${colors.bright}Usage:${colors.reset}
|
|
1567
|
+
npx uniweb@latest start Print the guide, in Markdown
|
|
1568
|
+
|
|
1569
|
+
A short guide for an AI agent building a website for someone: create a
|
|
1570
|
+
project, read its AGENTS.md, run it, sign the person in, and bring the site
|
|
1571
|
+
into the Uniweb app. The person only uses their browser, to sign in and to
|
|
1572
|
+
pay when the site goes live; the guide has the agent run every command.
|
|
1573
|
+
|
|
1574
|
+
Inside a Uniweb project it starts from that project instead of creating one.
|
|
1575
|
+
It asks nothing, writes nothing and makes no request, so it is safe to run
|
|
1576
|
+
anywhere. To run a site's dev server, use \`uniweb dev\`.
|
|
1532
1577
|
`,
|
|
1533
1578
|
dev: `
|
|
1534
1579
|
${colors.cyan}${colors.bright}uniweb dev${colors.reset} ${colors.dim}— Start a dev server for a site${colors.reset}
|
|
@@ -2075,6 +2120,7 @@ ${colors.bright}Usage:${colors.reset}
|
|
|
2075
2120
|
uniweb <command> [options]
|
|
2076
2121
|
|
|
2077
2122
|
${colors.bright}Commands:${colors.reset}
|
|
2123
|
+
start How an AI agent begins a site (a guide; changes nothing)
|
|
2078
2124
|
create [name] Create a new project
|
|
2079
2125
|
clone <site-uuid> Clone a backend site into a local file project
|
|
2080
2126
|
add <type> [name] Add a foundation, site, or extension to a project
|
|
@@ -311,12 +311,15 @@ export async function ensureRegistryAuth({
|
|
|
311
311
|
`\x1b[31m✗\x1b[0m ${reason} ${command} requires a Uniweb account, and the CLI is non-interactive (CI / no TTY / --non-interactive).`
|
|
312
312
|
)
|
|
313
313
|
console.error(' Options:')
|
|
314
|
-
|
|
314
|
+
// ⛔ Not "run `login` interactively": with no one at the terminal this is often an AI
|
|
315
|
+
// agent working for someone who does not use one, and it passes the line on. A browser
|
|
316
|
+
// sign-in asks nothing here. *(It said "interactively" until 2026-10-26.)*
|
|
315
317
|
console.error(
|
|
316
|
-
` •
|
|
318
|
+
` • Run \`${prefix} login --browser\` first — someone signs in, in a browser on this machine — then re-run.`
|
|
317
319
|
)
|
|
320
|
+
console.error(` • Set UNIWEB_TOKEN to a bearer token.`)
|
|
318
321
|
console.error(
|
|
319
|
-
` •
|
|
322
|
+
` • Set UNIWEB_USERNAME + UNIWEB_PASSWORD to log in non-interactively.`
|
|
320
323
|
)
|
|
321
324
|
process.exit(1)
|
|
322
325
|
}
|
|
@@ -515,7 +518,9 @@ export async function awaitBrowserCallback({
|
|
|
515
518
|
res.end(
|
|
516
519
|
`<!doctype html><html><body style="font-family:system-ui,sans-serif;text-align:center;padding:60px">` +
|
|
517
520
|
`<h2 style="color:${failed ? '#dc2626' : '#16a34a'}">${failed ? errTitle : okTitle}</h2>` +
|
|
518
|
-
|
|
521
|
+
// ⛔ Not "return to your terminal": the person signing in may have none — an AI
|
|
522
|
+
// agent ran the login for them. *(It said so until 2026-10-26.)*
|
|
523
|
+
`<p>You can close this tab.</p></body></html>`
|
|
519
524
|
)
|
|
520
525
|
cleanup()
|
|
521
526
|
resolve(failed ? { error: verdict.error } : { value: verdict.value })
|
|
@@ -817,11 +822,16 @@ export async function runRegistryLogin({ apiBase, args = [] } = {}) {
|
|
|
817
822
|
} else {
|
|
818
823
|
// Only what works without a terminal: `--password` and `--token-paste` prompt. The
|
|
819
824
|
// command keeps the backend named, or re-run as printed it would go to the default.
|
|
825
|
+
// ⭐ `--browser` first: it asks nothing here, and it is the way for an AI agent working
|
|
826
|
+
// for someone who signs in in their browser. *(It was missing until 2026-10-26.)*
|
|
820
827
|
const server = args.some((a) => a === '--server' || a.startsWith('--server='))
|
|
821
828
|
? ` --server ${key}`
|
|
822
829
|
: ''
|
|
823
830
|
console.error(
|
|
824
|
-
'\x1b[31m✗\x1b[0m Without a terminal, a login needs a way to sign in that asks nothing:'
|
|
831
|
+
'\x1b[31m✗\x1b[0m Without a terminal, a login needs a way to sign in that asks nothing here:'
|
|
832
|
+
)
|
|
833
|
+
console.error(
|
|
834
|
+
` ${getCliPrefix()} login${server} --browser (someone signs in, in a browser on this machine)`
|
|
825
835
|
)
|
|
826
836
|
console.error(
|
|
827
837
|
` ${getCliPrefix()} login${server} --token <bearer> (with --org @org or --personal)`
|
package/src/utils/scaffold.js
CHANGED
|
@@ -31,6 +31,10 @@ const STARTER_DIR = join(__dirname, '..', '..', 'starter')
|
|
|
31
31
|
* @param {string} context.projectName - Workspace/project name
|
|
32
32
|
* @param {string[]} context.workspaceGlobs - Workspace glob patterns
|
|
33
33
|
* @param {Object} context.scripts - Root package.json scripts
|
|
34
|
+
* @param {Array<{ path: string, foundation?: boolean, extension?: boolean, site?: boolean }>} context.packages -
|
|
35
|
+
* The packages the project starts with, each by its folder and kind, for the README's
|
|
36
|
+
* table. `[]` for a blank workspace, which the README tells how to grow.
|
|
37
|
+
* @param {boolean} [context.cloned] - The project is a `uniweb clone` of a site in the app
|
|
34
38
|
* @param {Object} [options] - Processing options
|
|
35
39
|
*/
|
|
36
40
|
export async function scaffoldWorkspace(targetDir, context, options = {}) {
|
package/src/utils/workspace.js
CHANGED
|
@@ -60,6 +60,32 @@ export function findWorkspaceRoot(startDir = process.cwd()) {
|
|
|
60
60
|
return null
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
+
/**
|
|
64
|
+
* Find a *Uniweb* workspace root from cwd. Stricter than findWorkspaceRoot
|
|
65
|
+
* — also requires that the workspace's root package.json declares uniweb
|
|
66
|
+
* as a dep or devDep. Otherwise `update` would write AGENTS.md and edit
|
|
67
|
+
* package.json files in unrelated monorepos, and `start` would take one for
|
|
68
|
+
* a Uniweb project.
|
|
69
|
+
*
|
|
70
|
+
* @param {string} [cwd]
|
|
71
|
+
* @returns {string|null} - Workspace root path or null
|
|
72
|
+
*/
|
|
73
|
+
export function findUniwebWorkspace(cwd = process.cwd()) {
|
|
74
|
+
const workspaceDir = findWorkspaceRoot(cwd)
|
|
75
|
+
if (!workspaceDir) return null
|
|
76
|
+
const pkgPath = join(workspaceDir, 'package.json')
|
|
77
|
+
if (!existsSync(pkgPath)) return null
|
|
78
|
+
try {
|
|
79
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'))
|
|
80
|
+
const hasUniwebDep = !!(
|
|
81
|
+
pkg.devDependencies?.uniweb || pkg.dependencies?.uniweb
|
|
82
|
+
)
|
|
83
|
+
return hasUniwebDep ? workspaceDir : null
|
|
84
|
+
} catch {
|
|
85
|
+
return null
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
63
89
|
/**
|
|
64
90
|
* Get workspace packages from pnpm-workspace.yaml or package.json workspaces —
|
|
65
91
|
* read as pnpm reads them (`workspacePackages`, ./workspace-packages.js).
|
|
@@ -5,14 +5,14 @@ import { H1, H2, P, Link, cn } from '@uniweb/kit'
|
|
|
5
5
|
*
|
|
6
6
|
* A versatile content section that handles headings, text, and links.
|
|
7
7
|
* Uses semantic tokens so it adapts to any theme context automatically.
|
|
8
|
+
*
|
|
9
|
+
* No fallbacks needed: the runtime hands every section the full content shape
|
|
10
|
+
* (empty strings and arrays where the markdown has nothing), and fills each
|
|
11
|
+
* param from its `default` in meta.js.
|
|
8
12
|
*/
|
|
9
13
|
export default function Section({ content, params }) {
|
|
10
|
-
const { title, pretitle, subtitle, paragraphs
|
|
11
|
-
|
|
12
|
-
const {
|
|
13
|
-
align = 'center',
|
|
14
|
-
width = 'default',
|
|
15
|
-
} = params || {}
|
|
14
|
+
const { title, pretitle, subtitle, paragraphs, links, images } = content
|
|
15
|
+
const { align, width } = params
|
|
16
16
|
|
|
17
17
|
const alignments = {
|
|
18
18
|
left: 'text-left',
|