uniweb 0.64.0 → 0.65.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/package.json +5 -5
- package/partials/agents.md +26 -17
- package/src/backend/client.js +61 -51
- package/src/backend/site-sync.js +17 -229
- package/src/backend/workspace.js +199 -0
- package/src/commands/clone.js +19 -29
- package/src/commands/publish.js +22 -39
- package/src/commands/pull.js +13 -14
- package/src/commands/push.js +21 -40
- package/src/commands/status.js +7 -12
- package/src/framework-index.json +3 -3
- package/src/index.js +19 -14
- package/src/utils/flag-guard.js +7 -5
- package/src/utils/registry-auth.js +75 -16
- package/src/utils/registry-orgs.js +2 -2
package/src/index.js
CHANGED
|
@@ -1456,15 +1456,10 @@ ${colors.bright}Options:${colors.reset}
|
|
|
1456
1456
|
bind to, and the app cannot open such a site.
|
|
1457
1457
|
--no-save Do not record this deploy in deploy.yml
|
|
1458
1458
|
--no-validate Skip the content-conformance check (it only warns)
|
|
1459
|
-
--org @org
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
are asked once. Later, it must match the workspace the backend
|
|
1464
|
-
works on the site from, or publish stops.
|
|
1465
|
-
--personal Create the site under your personal account, deliberately.
|
|
1466
|
-
Only needed on a first publish, and only to answer the owner
|
|
1467
|
-
question without a prompt (CI, agents, scripts).
|
|
1459
|
+
--org @org Work in @org for this publish, instead of the workspace
|
|
1460
|
+
--personal chosen at \`uniweb login\` (or your personal workspace). A
|
|
1461
|
+
site this publish creates is created — owned — there, and a
|
|
1462
|
+
site the backend keeps in another workspace stops it.
|
|
1468
1463
|
`,
|
|
1469
1464
|
create: `
|
|
1470
1465
|
${colors.cyan}${colors.bright}uniweb create${colors.reset} ${colors.dim}— Create a new project${colors.reset}
|
|
@@ -1793,16 +1788,26 @@ does nothing; add --password, --browser, --token-paste or --token to log in agai
|
|
|
1793
1788
|
Without --backend: https://uniweb.app (or \$UNIWEB_REGISTER_URL). No command talks
|
|
1794
1789
|
to a backend you are not logged in to — run one before logging in and it asks first.
|
|
1795
1790
|
|
|
1791
|
+
${colors.bright}The workspace you work in.${colors.reset} A login works in ONE workspace — your
|
|
1792
|
+
personal one, or an organization's — and every push, pull and publish works in it:
|
|
1793
|
+
a site it creates is created there, and a site kept in another workspace is refused.
|
|
1794
|
+
With no organization it is your personal workspace; with organizations you are asked,
|
|
1795
|
+
or name it. Already logged in, \`uniweb login --org @other\` switches without logging
|
|
1796
|
+
in again.
|
|
1797
|
+
|
|
1796
1798
|
${colors.bright}Options:${colors.reset}
|
|
1797
1799
|
--backend <url> The backend to log in to
|
|
1800
|
+
--org @org Work in @org (an organization you belong to)
|
|
1801
|
+
--personal Work in your personal workspace
|
|
1798
1802
|
--token <bearer> Seed + verify a session from a bearer token (non-interactive)
|
|
1799
1803
|
--browser Force the browser method
|
|
1800
1804
|
--password Force the username/password method
|
|
1801
1805
|
--token-paste Force the paste-a-token prompt
|
|
1802
1806
|
|
|
1803
|
-
In non-interactive mode (
|
|
1804
|
-
\`UNIWEB_USERNAME\` + \`UNIWEB_PASSWORD\`, or set
|
|
1805
|
-
not stored)
|
|
1807
|
+
In non-interactive mode (no TTY — an agent, a script), pass \`--token <bearer>\` with
|
|
1808
|
+
\`--org @org\` or \`--personal\`, or set \`UNIWEB_USERNAME\` + \`UNIWEB_PASSWORD\`, or set
|
|
1809
|
+
\`UNIWEB_TOKEN\` (per process, not stored) with \`UNIWEB_WORKSPACE=@org\` or \`personal\`.
|
|
1810
|
+
\`uniweb logout\` logs you out.
|
|
1806
1811
|
`,
|
|
1807
1812
|
refresh: `
|
|
1808
1813
|
${colors.cyan}${colors.bright}uniweb refresh${colors.reset} ${colors.dim}— Catch up with teammates AND app authors${colors.reset}
|
|
@@ -2082,8 +2087,8 @@ ${colors.bright}Publish Options:${colors.reset}
|
|
|
2082
2087
|
--dry-run Resolve everything; release/sync/POST nothing
|
|
2083
2088
|
--yes Skip confirmations (CI); never block on a prompt
|
|
2084
2089
|
--no-release Ship content against the already-released code; release nothing
|
|
2085
|
-
--org @org
|
|
2086
|
-
--personal
|
|
2090
|
+
--org @org Work in @org for this publish, not your login's workspace
|
|
2091
|
+
--personal Work in your personal workspace for this publish
|
|
2087
2092
|
--no-save Do not record this deploy in deploy.yml
|
|
2088
2093
|
--no-validate Skip the content-conformance check (it only warns)
|
|
2089
2094
|
|
package/src/utils/flag-guard.js
CHANGED
|
@@ -79,15 +79,17 @@ const VERBS = {
|
|
|
79
79
|
'--no-assets',
|
|
80
80
|
'--no-records', '--no-delete', '--no-prune',
|
|
81
81
|
// via backend/site-sync.js (the owner resolver) and utils/conformance.js
|
|
82
|
-
'--yes', '--org', '--no-validate', ...VIA_DEPLOY
|
|
82
|
+
'--yes', '--org', '--personal', '--no-validate', ...VIA_DEPLOY
|
|
83
83
|
],
|
|
84
84
|
clone: [
|
|
85
85
|
'--content-only', '--no-assets', '--no-records', '--path',
|
|
86
|
-
'--project', '--org'
|
|
86
|
+
'--project', '--org', '--personal'
|
|
87
87
|
],
|
|
88
88
|
register: [
|
|
89
89
|
'--dry-run', '--json', '--output', '-o',
|
|
90
|
-
'--schema-only', '--scope', '--org'
|
|
90
|
+
'--schema-only', '--scope', '--org',
|
|
91
|
+
// reach the login a register may trigger, which chooses the workspace it works in
|
|
92
|
+
'--personal'
|
|
91
93
|
],
|
|
92
94
|
/**
|
|
93
95
|
* `forget` = remove one backend's records (`--backend <url>`), or everything a
|
|
@@ -98,7 +100,7 @@ const VERBS = {
|
|
|
98
100
|
forget: ['--backend', '--all'],
|
|
99
101
|
status: [
|
|
100
102
|
'--json', '--remote', '--dry-run',
|
|
101
|
-
'--force', '--no-verify', '--no-validate', '--yes', '--org',
|
|
103
|
+
'--force', '--no-verify', '--no-validate', '--yes', '--org', '--personal',
|
|
102
104
|
// inert here, reachable through the bring-along module status imports for
|
|
103
105
|
// `resolveLocalFoundation` — listed per the over-approximation note above
|
|
104
106
|
'--no-release', ...VIA_DEPLOY
|
|
@@ -118,7 +120,7 @@ const VERBS = {
|
|
|
118
120
|
*/
|
|
119
121
|
refresh: [
|
|
120
122
|
'--no-backend', '--no-git',
|
|
121
|
-
'--org', '--dry-run', '--no-validate', '--yes', ...VIA_DEPLOY
|
|
123
|
+
'--org', '--personal', '--dry-run', '--no-validate', '--yes', ...VIA_DEPLOY
|
|
122
124
|
]
|
|
123
125
|
}
|
|
124
126
|
|
|
@@ -49,6 +49,7 @@ import { createServer } from 'node:http'
|
|
|
49
49
|
import { randomBytes } from 'node:crypto'
|
|
50
50
|
|
|
51
51
|
import { DEFAULT_BACKEND_ORIGIN } from './config.js'
|
|
52
|
+
import { readOrgFlag } from './args.js'
|
|
52
53
|
import { normalizeSessionFile, sessionFilePath, loggedInOriginOf } from './session-file.js'
|
|
53
54
|
|
|
54
55
|
const LOGIN_PATH = '/dev/auth/login'
|
|
@@ -641,6 +642,36 @@ async function loginViaBrowser({ apiBase }) {
|
|
|
641
642
|
return record
|
|
642
643
|
}
|
|
643
644
|
|
|
645
|
+
/**
|
|
646
|
+
* Choose the WORKSPACE this login works in, and store it with the session.
|
|
647
|
+
*
|
|
648
|
+
* ⭐ A login works in one workspace *[Diego, 2026-09-23: "at login, it can be mandatory to
|
|
649
|
+
* specify which workspace"]* — `--org @acme` / `--personal`, else your personal workspace
|
|
650
|
+
* when you belong to no organization, else a pick at a terminal. Without one (no
|
|
651
|
+
* terminal, organizations, no flag) the login stays and this says how to choose; every
|
|
652
|
+
* command refuses until one is chosen (`backend/workspace.js`).
|
|
653
|
+
*
|
|
654
|
+
* @param {object} record - the session just stored (`token`, `origin`, …)
|
|
655
|
+
* @param {string[]} args
|
|
656
|
+
* @returns {Promise<{ record: object, refused?: string }>}
|
|
657
|
+
*/
|
|
658
|
+
async function settleWorkspace(record, args) {
|
|
659
|
+
const { chooseWorkspace } = await import('../backend/workspace.js')
|
|
660
|
+
const pick = await chooseWorkspace({ apiBase: record.origin, token: record.token, args })
|
|
661
|
+
if (pick.refused) return { record, refused: pick.reason }
|
|
662
|
+
if (pick.note) console.error(`\x1b[2m${pick.note}\x1b[0m`)
|
|
663
|
+
const next = { ...record, workspace: pick.choice }
|
|
664
|
+
await writeRegistryAuth(next)
|
|
665
|
+
return { record: next }
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
/** ` · working in @acme` — the tail of a login line. */
|
|
669
|
+
async function workspaceTail(record) {
|
|
670
|
+
if (!record?.workspace) return ''
|
|
671
|
+
const { describeWorkspace } = await import('../backend/workspace.js')
|
|
672
|
+
return `, working in \x1b[1m${describeWorkspace(record.workspace)}\x1b[0m`
|
|
673
|
+
}
|
|
674
|
+
|
|
644
675
|
/**
|
|
645
676
|
* `uniweb login` against the new backend — a multi-method picker:
|
|
646
677
|
* browser/social (default, once available) · username+password · paste a token.
|
|
@@ -675,19 +706,37 @@ export async function runRegistryLogin({ apiBase, args = [] } = {}) {
|
|
|
675
706
|
existing.handle ||
|
|
676
707
|
(existing.uuid ? `account ${existing.uuid}` : '')
|
|
677
708
|
if (!forced) {
|
|
678
|
-
// Already logged in to this backend: nothing to
|
|
679
|
-
// method flag. (Also where a file from before sessions were single, holding
|
|
680
|
-
// backend among others, becomes single: the rewrite keeps only this session.)
|
|
709
|
+
// Already logged in to this backend: nothing to re-authenticate — logging in again is
|
|
710
|
+
// behind a method flag. (Also where a file from before sessions were single, holding
|
|
711
|
+
// this backend among others, becomes single: the rewrite keeps only this session.)
|
|
712
|
+
let session = { ...existing, origin: key }
|
|
681
713
|
if (current !== key || Object.keys(file.sessions).length > 1) {
|
|
682
|
-
await writeRegistryAuth(
|
|
714
|
+
await writeRegistryAuth(session)
|
|
715
|
+
}
|
|
716
|
+
// ⭐ SWITCHING WORKSPACE NEEDS NO NEW LOGIN: `--org @x` / `--personal` on a valid
|
|
717
|
+
// session just changes the workspace it works in. One with none chosen yet (a
|
|
718
|
+
// session from before workspaces) is asked now.
|
|
719
|
+
const wants = readOrgFlag(args) || args.includes('--personal')
|
|
720
|
+
if (wants || !session.workspace) {
|
|
721
|
+
const settled = await settleWorkspace(session, args)
|
|
722
|
+
if (settled.refused) {
|
|
723
|
+
console.error(`\x1b[32m✓\x1b[0m Logged in to ${key}${who ? ` as \x1b[1m${who}\x1b[0m` : ''}.`)
|
|
724
|
+
console.error(`\x1b[31m✗\x1b[0m ${settled.refused}`)
|
|
725
|
+
process.exit(2)
|
|
726
|
+
}
|
|
727
|
+
session = settled.record
|
|
728
|
+
if (wants) {
|
|
729
|
+
console.error(`\x1b[32m✓\x1b[0m Now working in ${(await workspaceTail(session)).replace(/^, working in /, '')} (${key}).`)
|
|
730
|
+
return session
|
|
731
|
+
}
|
|
683
732
|
}
|
|
684
733
|
console.error(
|
|
685
|
-
`\x1b[32m✓\x1b[0m Already logged in to ${key}${who ? ` as \x1b[1m${who}\x1b[0m` : ''}.`
|
|
734
|
+
`\x1b[32m✓\x1b[0m Already logged in to ${key}${who ? ` as \x1b[1m${who}\x1b[0m` : ''}${await workspaceTail(session)}.`
|
|
686
735
|
)
|
|
687
736
|
console.error(
|
|
688
|
-
'\x1b[
|
|
737
|
+
'\x1b[2mSwitch workspace: --org @acme or --personal. To log in again, name a method: --password, --browser, --token-paste or --token <bearer>.\x1b[0m'
|
|
689
738
|
)
|
|
690
|
-
return
|
|
739
|
+
return session
|
|
691
740
|
}
|
|
692
741
|
console.error(
|
|
693
742
|
`Already logged in${who ? ` as \x1b[1m${who}\x1b[0m` : ''} (${key}) — logging in again replaces that session.\n`
|
|
@@ -720,10 +769,7 @@ export async function runRegistryLogin({ apiBase, args = [] } = {}) {
|
|
|
720
769
|
if (account?.username) record.username = account.username
|
|
721
770
|
if (account?.handle) record.handle = account.handle
|
|
722
771
|
await writeRegistryAuth(record)
|
|
723
|
-
|
|
724
|
-
`\x1b[32m✓\x1b[0m Logged in${account?.username ? ` as \x1b[1m${account.username}\x1b[0m` : ''}${apiBase ? ` (${apiBase})` : ''}`
|
|
725
|
-
)
|
|
726
|
-
return record
|
|
772
|
+
return finishLogin(record, apiBase, args)
|
|
727
773
|
}
|
|
728
774
|
|
|
729
775
|
let method = args.includes('--browser')
|
|
@@ -787,10 +833,23 @@ export async function runRegistryLogin({ apiBase, args = [] } = {}) {
|
|
|
787
833
|
process.exit(1)
|
|
788
834
|
}
|
|
789
835
|
|
|
790
|
-
if (record?.token)
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
836
|
+
if (!record?.token) return record
|
|
837
|
+
return finishLogin({ ...record, origin: record.origin || normOrigin(apiBase) }, apiBase, args)
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
/**
|
|
841
|
+
* A login succeeded and its session is stored: choose its workspace, and say where the
|
|
842
|
+
* login works. Without a workspace (no terminal, organizations, no flag) the session
|
|
843
|
+
* stays, the reason is printed, and the login exits 2 — every command would refuse.
|
|
844
|
+
*/
|
|
845
|
+
async function finishLogin(record, apiBase, args) {
|
|
846
|
+
const settled = await settleWorkspace(record, args)
|
|
847
|
+
const who = settled.record.username ? ` as \x1b[1m${settled.record.username}\x1b[0m` : ''
|
|
848
|
+
if (settled.refused) {
|
|
849
|
+
console.error(`\x1b[32m✓\x1b[0m Logged in${who} (${apiBase}).`)
|
|
850
|
+
console.error(`\x1b[31m✗\x1b[0m ${settled.refused}`)
|
|
851
|
+
process.exit(2)
|
|
794
852
|
}
|
|
795
|
-
|
|
853
|
+
console.error(`\x1b[32m✓\x1b[0m Logged in${who} (${apiBase})${await workspaceTail(settled.record)}.`)
|
|
854
|
+
return settled.record
|
|
796
855
|
}
|
|
@@ -99,8 +99,8 @@ export function validateHandle(handle) {
|
|
|
99
99
|
* personal org already exists.
|
|
100
100
|
* @returns {Promise<{account_handle: string|null, orgs: Array<{handle: string, is_primary: boolean}>}>}
|
|
101
101
|
*/
|
|
102
|
-
export async function fetchOrgs({ apiBase, token }) {
|
|
103
|
-
const res = await
|
|
102
|
+
export async function fetchOrgs({ apiBase, token, fetchImpl = globalThis.fetch }) {
|
|
103
|
+
const res = await fetchImpl(`${apiBase.replace(/\/$/, '')}${ORGS_PATH}`, {
|
|
104
104
|
headers: { Authorization: `Bearer ${token}` }
|
|
105
105
|
})
|
|
106
106
|
if (!res.ok)
|