uniweb 0.8.21 → 0.8.23
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/src/commands/login.js +161 -18
- package/src/commands/publish.js +57 -3
- package/src/utils/auth.js +7 -49
- package/src/utils/config.js +59 -1
- package/src/utils/registry.js +6 -5
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "uniweb",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.23",
|
|
4
4
|
"description": "Create structured Vite + React sites with content/code separation",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -41,13 +41,13 @@
|
|
|
41
41
|
"js-yaml": "^4.1.0",
|
|
42
42
|
"prompts": "^2.4.2",
|
|
43
43
|
"tar": "^7.0.0",
|
|
44
|
-
"@uniweb/core": "0.5.
|
|
45
|
-
"@uniweb/kit": "0.7.
|
|
46
|
-
"@uniweb/runtime": "0.6.
|
|
44
|
+
"@uniweb/core": "0.5.15",
|
|
45
|
+
"@uniweb/kit": "0.7.16",
|
|
46
|
+
"@uniweb/runtime": "0.6.18"
|
|
47
47
|
},
|
|
48
48
|
"peerDependencies": {
|
|
49
|
-
"@uniweb/build": "0.8.20",
|
|
50
49
|
"@uniweb/content-reader": "1.1.4",
|
|
50
|
+
"@uniweb/build": "0.8.22",
|
|
51
51
|
"@uniweb/semantic-parser": "1.1.7"
|
|
52
52
|
},
|
|
53
53
|
"peerDependenciesMeta": {
|
package/src/commands/login.js
CHANGED
|
@@ -3,15 +3,21 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Authenticates with the Uniweb platform. Stores credentials at ~/.uniweb/auth.json.
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
6
|
+
* Flow:
|
|
7
|
+
* 1. Start a temporary HTTP server on a random port
|
|
8
|
+
* 2. Open the browser to {backend}/cli-auth.php?action=login&callback=http://localhost:{port}/callback
|
|
9
|
+
* 3. PHP authenticates the user, signs a JWT, redirects to the callback
|
|
10
|
+
* 4. CLI receives the token and stores it at ~/.uniweb/auth.json
|
|
11
|
+
* 5. Falls back to token-paste if browser fails
|
|
8
12
|
*
|
|
9
13
|
* Usage:
|
|
10
14
|
* uniweb login
|
|
15
|
+
* uniweb login --token-paste # Skip browser, use token paste
|
|
11
16
|
*/
|
|
12
17
|
|
|
13
|
-
import
|
|
18
|
+
import { createServer } from 'node:http'
|
|
14
19
|
import { writeAuth, readAuth, isExpired } from '../utils/auth.js'
|
|
20
|
+
import { getBackendUrl } from '../utils/config.js'
|
|
15
21
|
|
|
16
22
|
// Colors for terminal output
|
|
17
23
|
const colors = {
|
|
@@ -33,21 +39,119 @@ function error(message) {
|
|
|
33
39
|
}
|
|
34
40
|
|
|
35
41
|
/**
|
|
36
|
-
*
|
|
42
|
+
* Try to open a URL in the default browser.
|
|
43
|
+
* @param {string} url
|
|
44
|
+
* @returns {Promise<boolean>} Whether the browser was opened
|
|
37
45
|
*/
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
46
|
+
async function openBrowser(url) {
|
|
47
|
+
try {
|
|
48
|
+
const { exec } = await import('node:child_process')
|
|
49
|
+
const cmd = process.platform === 'darwin'
|
|
50
|
+
? `open "${url}"`
|
|
51
|
+
: process.platform === 'win32'
|
|
52
|
+
? `start "" "${url}"`
|
|
53
|
+
: `xdg-open "${url}"`
|
|
54
|
+
|
|
55
|
+
return new Promise((resolve) => {
|
|
56
|
+
exec(cmd, (err) => resolve(!err))
|
|
57
|
+
})
|
|
58
|
+
} catch {
|
|
59
|
+
return false
|
|
45
60
|
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Browser-based login flow.
|
|
65
|
+
*
|
|
66
|
+
* Starts a temp HTTP server, opens the browser to the PHP login page,
|
|
67
|
+
* waits for the callback with the JWT token.
|
|
68
|
+
*
|
|
69
|
+
* @param {string} backendUrl - PHP backend URL
|
|
70
|
+
* @param {number} [timeoutMs=120000] - Timeout in ms
|
|
71
|
+
* @returns {Promise<{ token: string, email: string } | null>}
|
|
72
|
+
*/
|
|
73
|
+
function browserLogin(backendUrl, timeoutMs = 120000) {
|
|
74
|
+
return new Promise((resolve) => {
|
|
75
|
+
const server = createServer((req, res) => {
|
|
76
|
+
const url = new URL(req.url, `http://localhost`)
|
|
77
|
+
if (url.pathname !== '/callback') {
|
|
78
|
+
res.writeHead(404)
|
|
79
|
+
res.end('Not found')
|
|
80
|
+
return
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const token = url.searchParams.get('token')
|
|
84
|
+
const email = url.searchParams.get('email')
|
|
85
|
+
|
|
86
|
+
if (!token) {
|
|
87
|
+
res.writeHead(400, { 'Content-Type': 'text/html' })
|
|
88
|
+
res.end('<h2>Login failed</h2><p>No token received. Please try again.</p>')
|
|
89
|
+
cleanup()
|
|
90
|
+
resolve(null)
|
|
91
|
+
return
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
res.writeHead(200, { 'Content-Type': 'text/html' })
|
|
95
|
+
res.end(`
|
|
96
|
+
<html>
|
|
97
|
+
<body style="font-family: system-ui, sans-serif; text-align: center; padding: 60px;">
|
|
98
|
+
<h2 style="color: #16a34a;">Login successful!</h2>
|
|
99
|
+
<p>You can close this window and return to your terminal.</p>
|
|
100
|
+
</body>
|
|
101
|
+
</html>
|
|
102
|
+
`)
|
|
103
|
+
cleanup()
|
|
104
|
+
resolve({ token, email: email || '' })
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
let timeout
|
|
108
|
+
|
|
109
|
+
function cleanup() {
|
|
110
|
+
clearTimeout(timeout)
|
|
111
|
+
server.close()
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Listen on a random port
|
|
115
|
+
server.listen(0, '127.0.0.1', async () => {
|
|
116
|
+
const port = server.address().port
|
|
117
|
+
const callbackUrl = `http://localhost:${port}/callback`
|
|
118
|
+
const loginUrl = `${backendUrl}/cli-auth.php?action=login&callback=${encodeURIComponent(callbackUrl)}`
|
|
119
|
+
|
|
120
|
+
console.log(`${colors.cyan}→${colors.reset} Opening browser for login...`)
|
|
121
|
+
console.log(` ${colors.dim}${loginUrl}${colors.reset}`)
|
|
122
|
+
console.log('')
|
|
123
|
+
|
|
124
|
+
const opened = await openBrowser(loginUrl)
|
|
125
|
+
if (!opened) {
|
|
126
|
+
console.log(`${colors.yellow}⚠${colors.reset} Could not open browser.`)
|
|
127
|
+
console.log(` Open this URL manually: ${colors.cyan}${loginUrl}${colors.reset}`)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
console.log(`${colors.dim}Waiting for login... (${timeoutMs / 1000}s timeout)${colors.reset}`)
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
// Timeout
|
|
134
|
+
timeout = setTimeout(() => {
|
|
135
|
+
server.close()
|
|
136
|
+
resolve(null)
|
|
137
|
+
}, timeoutMs)
|
|
138
|
+
|
|
139
|
+
server.on('error', () => {
|
|
140
|
+
resolve(null)
|
|
141
|
+
})
|
|
142
|
+
})
|
|
143
|
+
}
|
|
46
144
|
|
|
47
|
-
|
|
145
|
+
/**
|
|
146
|
+
* Token-paste login flow (fallback).
|
|
147
|
+
* @returns {Promise<{ token: string, email: string } | null>}
|
|
148
|
+
*/
|
|
149
|
+
async function tokenPasteLogin() {
|
|
150
|
+
const prompts = (await import('prompts')).default
|
|
151
|
+
|
|
152
|
+
console.log('Paste your token from the Uniweb login page.')
|
|
48
153
|
console.log('')
|
|
49
154
|
|
|
50
|
-
// Phase 1: token-paste flow
|
|
51
155
|
const response = await prompts([
|
|
52
156
|
{
|
|
53
157
|
type: 'text',
|
|
@@ -58,7 +162,7 @@ export async function login(args = []) {
|
|
|
58
162
|
{
|
|
59
163
|
type: 'password',
|
|
60
164
|
name: 'token',
|
|
61
|
-
message: 'Token
|
|
165
|
+
message: 'Token:',
|
|
62
166
|
validate: (v) => (v ? true : 'Token is required'),
|
|
63
167
|
},
|
|
64
168
|
], {
|
|
@@ -69,19 +173,58 @@ export async function login(args = []) {
|
|
|
69
173
|
})
|
|
70
174
|
|
|
71
175
|
if (!response.email || !response.token) {
|
|
176
|
+
return null
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return { token: response.token, email: response.email }
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Main login command handler
|
|
184
|
+
*/
|
|
185
|
+
export async function login(args = []) {
|
|
186
|
+
const forceTokenPaste = args.includes('--token-paste')
|
|
187
|
+
|
|
188
|
+
// Check if already logged in
|
|
189
|
+
const existing = await readAuth()
|
|
190
|
+
if (existing && !isExpired(existing)) {
|
|
191
|
+
console.log(`Already logged in as ${colors.bright}${existing.email}${colors.reset}`)
|
|
192
|
+
console.log(`${colors.dim}Continuing will replace the existing session.${colors.reset}`)
|
|
193
|
+
console.log('')
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const backendUrl = getBackendUrl()
|
|
197
|
+
let result = null
|
|
198
|
+
|
|
199
|
+
if (!forceTokenPaste) {
|
|
200
|
+
// Try browser-based login
|
|
201
|
+
result = await browserLogin(backendUrl)
|
|
202
|
+
|
|
203
|
+
if (!result) {
|
|
204
|
+
console.log('')
|
|
205
|
+
console.log(`${colors.yellow}⚠${colors.reset} Browser login timed out or failed.`)
|
|
206
|
+
console.log(` Falling back to token paste...`)
|
|
207
|
+
console.log('')
|
|
208
|
+
result = await tokenPasteLogin()
|
|
209
|
+
}
|
|
210
|
+
} else {
|
|
211
|
+
result = await tokenPasteLogin()
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (!result) {
|
|
72
215
|
error('Login cancelled.')
|
|
73
216
|
process.exit(1)
|
|
74
217
|
}
|
|
75
218
|
|
|
76
|
-
// Store credentials
|
|
219
|
+
// Store credentials (JWT has 30-day expiry)
|
|
77
220
|
await writeAuth({
|
|
78
|
-
token:
|
|
79
|
-
email:
|
|
221
|
+
token: result.token,
|
|
222
|
+
email: result.email,
|
|
80
223
|
expiresAt: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString(),
|
|
81
224
|
})
|
|
82
225
|
|
|
83
226
|
console.log('')
|
|
84
|
-
success(`Logged in as ${colors.bright}${
|
|
227
|
+
success(`Logged in as ${colors.bright}${result.email}${colors.reset}`)
|
|
85
228
|
}
|
|
86
229
|
|
|
87
230
|
export default login
|
package/src/commands/publish.js
CHANGED
|
@@ -18,6 +18,7 @@ import { execSync } from 'node:child_process'
|
|
|
18
18
|
|
|
19
19
|
import { createLocalRegistry, RemoteRegistry } from '../utils/registry.js'
|
|
20
20
|
import { ensureAuth, readAuth } from '../utils/auth.js'
|
|
21
|
+
import { getRegistryUrl } from '../utils/config.js'
|
|
21
22
|
import { findWorkspaceRoot, findFoundations, findSites, classifyPackage, promptSelect } from '../utils/workspace.js'
|
|
22
23
|
import { isNonInteractive, getCliPrefix } from '../utils/interactive.js'
|
|
23
24
|
|
|
@@ -116,6 +117,17 @@ function parseRegistryUrl(args) {
|
|
|
116
117
|
return args[idx + 1]
|
|
117
118
|
}
|
|
118
119
|
|
|
120
|
+
/**
|
|
121
|
+
* Parse --namespace <handle> from args.
|
|
122
|
+
* @param {string[]} args
|
|
123
|
+
* @returns {string|null}
|
|
124
|
+
*/
|
|
125
|
+
function parseNamespace(args) {
|
|
126
|
+
const idx = args.indexOf('--namespace')
|
|
127
|
+
if (idx === -1 || !args[idx + 1]) return null
|
|
128
|
+
return args[idx + 1]
|
|
129
|
+
}
|
|
130
|
+
|
|
119
131
|
/**
|
|
120
132
|
* Parse --edit-access <policy> from args.
|
|
121
133
|
* @param {string[]} args
|
|
@@ -140,6 +152,7 @@ export async function publish(args = []) {
|
|
|
140
152
|
const isDryRun = args.includes('--dry-run')
|
|
141
153
|
const registryUrl = parseRegistryUrl(args)
|
|
142
154
|
const editAccess = parseEditAccess(args)
|
|
155
|
+
const namespaceFlag = parseNamespace(args)
|
|
143
156
|
|
|
144
157
|
// 1. Resolve foundation directory
|
|
145
158
|
const foundationDir = await resolveFoundationDir(args)
|
|
@@ -179,15 +192,56 @@ export async function publish(args = []) {
|
|
|
179
192
|
process.exit(1)
|
|
180
193
|
}
|
|
181
194
|
|
|
182
|
-
const
|
|
195
|
+
const rawName = schema._self?.name
|
|
183
196
|
const version = schema._self?.version
|
|
184
197
|
|
|
185
|
-
if (!
|
|
198
|
+
if (!rawName || !version) {
|
|
186
199
|
error('dist/meta/schema.json missing _self.name or _self.version')
|
|
187
200
|
console.log(`${colors.dim} Ensure your package.json has "name" and "version" fields.${colors.reset}`)
|
|
188
201
|
process.exit(1)
|
|
189
202
|
}
|
|
190
203
|
|
|
204
|
+
// 3b. Resolve namespace (priority: --namespace flag > package.json uniweb.namespace > scoped name)
|
|
205
|
+
const pkg = JSON.parse(await readFile(join(foundationDir, 'package.json'), 'utf8'))
|
|
206
|
+
const uniwebNamespace = pkg.uniweb?.namespace
|
|
207
|
+
const scopedMatch = rawName.match(/^@([a-z0-9_-]+)\//)
|
|
208
|
+
const namespace = namespaceFlag || uniwebNamespace || scopedMatch?.[1]
|
|
209
|
+
|
|
210
|
+
if (!namespace) {
|
|
211
|
+
error('Namespace is required for publishing.')
|
|
212
|
+
console.log('')
|
|
213
|
+
console.log(` ${colors.dim}Use one of:${colors.reset}`)
|
|
214
|
+
console.log(` ${colors.cyan}uniweb publish --namespace <org-handle>${colors.reset}`)
|
|
215
|
+
console.log(` ${colors.dim}Add ${colors.reset}"uniweb": { "namespace": "<org-handle>" }${colors.dim} to package.json${colors.reset}`)
|
|
216
|
+
console.log(` ${colors.dim}Or use a scoped name: ${colors.reset}"name": "@org/foundation"${colors.dim} in package.json${colors.reset}`)
|
|
217
|
+
process.exit(1)
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Construct scoped name: @namespace/foundationName
|
|
221
|
+
const foundationName = scopedMatch ? rawName.slice(scopedMatch[0].length) : rawName
|
|
222
|
+
const name = `@${namespace}/${foundationName}`
|
|
223
|
+
|
|
224
|
+
// 3c. Advisory namespace check (Worker enforces — this is for early UX feedback)
|
|
225
|
+
if (!isLocal) {
|
|
226
|
+
const auth = await readAuth()
|
|
227
|
+
if (auth?.token) {
|
|
228
|
+
try {
|
|
229
|
+
const payload = JSON.parse(atob(auth.token.split('.')[1]))
|
|
230
|
+
if (payload.namespaces && !payload.namespaces.includes(namespace)) {
|
|
231
|
+
error(`You don't have publish access to namespace "${colors.bright}@${namespace}${colors.reset}"`)
|
|
232
|
+
if (payload.namespaces.length > 0) {
|
|
233
|
+
console.log(` ${colors.dim}Your namespaces: ${payload.namespaces.map(n => '@' + n).join(', ')}${colors.reset}`)
|
|
234
|
+
} else {
|
|
235
|
+
console.log(` ${colors.dim}You don't belong to any organizations. Ask an admin to add you.${colors.reset}`)
|
|
236
|
+
}
|
|
237
|
+
process.exit(1)
|
|
238
|
+
}
|
|
239
|
+
} catch {
|
|
240
|
+
// JWT decode failed — let the Worker validate
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
191
245
|
// 4. Create registry (local or remote)
|
|
192
246
|
const isRemote = !isLocal
|
|
193
247
|
let registry
|
|
@@ -198,7 +252,7 @@ export async function publish(args = []) {
|
|
|
198
252
|
// Remote publish — ensure authenticated (inline login if needed)
|
|
199
253
|
const token = await ensureAuth({ command: 'Publishing' })
|
|
200
254
|
|
|
201
|
-
const url = registryUrl ||
|
|
255
|
+
const url = registryUrl || getRegistryUrl()
|
|
202
256
|
registry = new RemoteRegistry(url, token)
|
|
203
257
|
}
|
|
204
258
|
|
package/src/utils/auth.js
CHANGED
|
@@ -88,63 +88,21 @@ export async function ensureAuth({ command = 'This command' } = {}) {
|
|
|
88
88
|
return auth.token
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
-
// Need to log in
|
|
92
|
-
const prompts = (await import('prompts')).default
|
|
93
|
-
|
|
91
|
+
// Need to log in — delegate to the login command
|
|
94
92
|
if (auth && isExpired(auth)) {
|
|
95
93
|
console.log(`\x1b[33mSession expired.\x1b[0m ${command} requires a Uniweb account.\n`)
|
|
96
94
|
} else {
|
|
97
95
|
console.log(`${command} requires a Uniweb account.\n`)
|
|
98
96
|
}
|
|
99
97
|
|
|
100
|
-
const {
|
|
101
|
-
|
|
102
|
-
name: 'action',
|
|
103
|
-
message: 'What would you like to do?',
|
|
104
|
-
choices: [
|
|
105
|
-
{ title: 'Log in (paste token from uniweb.app/cli-login)', value: 'login' },
|
|
106
|
-
{ title: 'Cancel', value: 'cancel' },
|
|
107
|
-
],
|
|
108
|
-
}, {
|
|
109
|
-
onCancel: () => {
|
|
110
|
-
process.exit(0)
|
|
111
|
-
},
|
|
112
|
-
})
|
|
113
|
-
|
|
114
|
-
if (action !== 'login') {
|
|
115
|
-
process.exit(0)
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
const response = await prompts([
|
|
119
|
-
{
|
|
120
|
-
type: 'text',
|
|
121
|
-
name: 'email',
|
|
122
|
-
message: 'Email:',
|
|
123
|
-
validate: (v) => (v && v.includes('@') ? true : 'Enter a valid email'),
|
|
124
|
-
},
|
|
125
|
-
{
|
|
126
|
-
type: 'password',
|
|
127
|
-
name: 'token',
|
|
128
|
-
message: 'Token:',
|
|
129
|
-
validate: (v) => (v ? true : 'Token is required'),
|
|
130
|
-
},
|
|
131
|
-
], {
|
|
132
|
-
onCancel: () => {
|
|
133
|
-
process.exit(0)
|
|
134
|
-
},
|
|
135
|
-
})
|
|
98
|
+
const { login } = await import('../commands/login.js')
|
|
99
|
+
await login([])
|
|
136
100
|
|
|
137
|
-
|
|
101
|
+
// Re-read auth after login
|
|
102
|
+
const newAuth = await readAuth()
|
|
103
|
+
if (!newAuth?.token) {
|
|
138
104
|
process.exit(1)
|
|
139
105
|
}
|
|
140
106
|
|
|
141
|
-
|
|
142
|
-
token: response.token,
|
|
143
|
-
email: response.email,
|
|
144
|
-
expiresAt: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString(),
|
|
145
|
-
})
|
|
146
|
-
|
|
147
|
-
console.log(`\n\x1b[32m✓\x1b[0m Logged in as \x1b[1m${response.email}\x1b[0m\n`)
|
|
148
|
-
|
|
149
|
-
return response.token
|
|
107
|
+
return newAuth.token
|
|
150
108
|
}
|
package/src/utils/config.js
CHANGED
|
@@ -5,12 +5,70 @@
|
|
|
5
5
|
* Used by both `create` and `add` commands.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { existsSync } from 'node:fs'
|
|
8
|
+
import { existsSync, readFileSync } from 'node:fs'
|
|
9
9
|
import { readFile, writeFile } from 'node:fs/promises'
|
|
10
10
|
import { join } from 'node:path'
|
|
11
|
+
import { homedir } from 'node:os'
|
|
11
12
|
import yaml from 'js-yaml'
|
|
12
13
|
import { filterCmd } from './pm.js'
|
|
13
14
|
|
|
15
|
+
// ── Platform URLs ──────────────────────────────────────────────
|
|
16
|
+
|
|
17
|
+
// Production defaults — regular users get these out of the box
|
|
18
|
+
const PRODUCTION_BACKEND_URL = 'https://uniweb.app'
|
|
19
|
+
const PRODUCTION_REGISTRY_URL = 'https://site-router.uniweb-edge.workers.dev'
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Read ~/.uniweb/config.json for persistent URL overrides.
|
|
23
|
+
* Platform developers use this to point CLI to local servers.
|
|
24
|
+
*
|
|
25
|
+
* Example ~/.uniweb/config.json:
|
|
26
|
+
* { "backendUrl": "http://127.0.0.1:8002", "registryUrl": "http://localhost:4001" }
|
|
27
|
+
*
|
|
28
|
+
* @returns {{ backendUrl?: string, registryUrl?: string }}
|
|
29
|
+
*/
|
|
30
|
+
let _cliConfig = undefined
|
|
31
|
+
function readCliConfig() {
|
|
32
|
+
if (_cliConfig !== undefined) return _cliConfig
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
const configPath = join(homedir(), '.uniweb', 'config.json')
|
|
36
|
+
if (existsSync(configPath)) {
|
|
37
|
+
_cliConfig = JSON.parse(readFileSync(configPath, 'utf8'))
|
|
38
|
+
return _cliConfig
|
|
39
|
+
}
|
|
40
|
+
} catch {
|
|
41
|
+
// ignore
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
_cliConfig = {}
|
|
45
|
+
return _cliConfig
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Get the PHP backend URL.
|
|
50
|
+
*
|
|
51
|
+
* Priority: env var > ~/.uniweb/config.json > production default
|
|
52
|
+
* @returns {string}
|
|
53
|
+
*/
|
|
54
|
+
export function getBackendUrl() {
|
|
55
|
+
return process.env.UNIWEB_BACKEND_URL
|
|
56
|
+
|| readCliConfig().backendUrl
|
|
57
|
+
|| PRODUCTION_BACKEND_URL
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Get the registry API URL (Cloudflare Worker or local unicloud).
|
|
62
|
+
*
|
|
63
|
+
* Priority: env var > ~/.uniweb/config.json > production default
|
|
64
|
+
* @returns {string}
|
|
65
|
+
*/
|
|
66
|
+
export function getRegistryUrl() {
|
|
67
|
+
return process.env.UNIWEB_REGISTRY_URL
|
|
68
|
+
|| readCliConfig().registryUrl
|
|
69
|
+
|| PRODUCTION_REGISTRY_URL
|
|
70
|
+
}
|
|
71
|
+
|
|
14
72
|
/**
|
|
15
73
|
* Read workspace package globs.
|
|
16
74
|
* Tries pnpm-workspace.yaml first, falls back to package.json workspaces.
|
package/src/utils/registry.js
CHANGED
|
@@ -42,7 +42,8 @@ export function getRegistryDir(startDir = process.cwd()) {
|
|
|
42
42
|
* @returns {string}
|
|
43
43
|
*/
|
|
44
44
|
function sanitizeName(name) {
|
|
45
|
-
|
|
45
|
+
// Strip leading @ for directory structure: @org/name → org/name
|
|
46
|
+
return name.startsWith('@') ? name.slice(1) : name
|
|
46
47
|
}
|
|
47
48
|
|
|
48
49
|
/**
|
|
@@ -256,7 +257,7 @@ export class RemoteRegistry {
|
|
|
256
257
|
* @returns {Promise<Object>}
|
|
257
258
|
*/
|
|
258
259
|
async createInvite(foundationName, payload) {
|
|
259
|
-
const res = await fetch(`${this.apiUrl}/api/foundations/${foundationName}/invites`, {
|
|
260
|
+
const res = await fetch(`${this.apiUrl}/api/foundations/${encodeURIComponent(foundationName)}/invites`, {
|
|
260
261
|
method: 'POST',
|
|
261
262
|
headers: this._authHeaders(),
|
|
262
263
|
body: JSON.stringify(payload),
|
|
@@ -274,7 +275,7 @@ export class RemoteRegistry {
|
|
|
274
275
|
* @returns {Promise<Array>}
|
|
275
276
|
*/
|
|
276
277
|
async listInvites(foundationName) {
|
|
277
|
-
const res = await fetch(`${this.apiUrl}/api/foundations/${foundationName}/invites`, {
|
|
278
|
+
const res = await fetch(`${this.apiUrl}/api/foundations/${encodeURIComponent(foundationName)}/invites`, {
|
|
278
279
|
headers: this._authHeaders(),
|
|
279
280
|
})
|
|
280
281
|
const body = await res.json()
|
|
@@ -291,7 +292,7 @@ export class RemoteRegistry {
|
|
|
291
292
|
* @returns {Promise<Object>}
|
|
292
293
|
*/
|
|
293
294
|
async revokeInvite(foundationName, inviteId) {
|
|
294
|
-
const res = await fetch(`${this.apiUrl}/api/foundations/${foundationName}/invites/${inviteId}`, {
|
|
295
|
+
const res = await fetch(`${this.apiUrl}/api/foundations/${encodeURIComponent(foundationName)}/invites/${inviteId}`, {
|
|
295
296
|
method: 'DELETE',
|
|
296
297
|
headers: this._authHeaders(),
|
|
297
298
|
})
|
|
@@ -309,7 +310,7 @@ export class RemoteRegistry {
|
|
|
309
310
|
* @returns {Promise<Object>}
|
|
310
311
|
*/
|
|
311
312
|
async resendInvite(foundationName, inviteId) {
|
|
312
|
-
const res = await fetch(`${this.apiUrl}/api/foundations/${foundationName}/invites/${inviteId}/resend`, {
|
|
313
|
+
const res = await fetch(`${this.apiUrl}/api/foundations/${encodeURIComponent(foundationName)}/invites/${inviteId}/resend`, {
|
|
313
314
|
method: 'POST',
|
|
314
315
|
headers: this._authHeaders(),
|
|
315
316
|
})
|