uniweb 0.64.1 → 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.
@@ -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 do — logging in again is behind a
679
- // method flag. (Also where a file from before sessions were single, holding this
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({ ...existing, origin: key })
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[2mTo log in again, name a method: --password, --browser, --token-paste or --token <bearer>.\x1b[0m'
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 { ...existing, origin: key }
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
- console.error(
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
- console.error(
792
- `\x1b[32m✓\x1b[0m Logged in${record.username ? ` as \x1b[1m${record.username}\x1b[0m` : ''}${apiBase ? ` (${apiBase})` : ''}`
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
- return record
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 fetch(`${apiBase.replace(/\/$/, '')}${ORGS_PATH}`, {
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)