uniweb 0.12.26 → 0.12.28
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 +6 -6
- package/partials/agents.md +3 -0
- package/src/backend/client.js +339 -0
- package/src/commands/clone.js +18 -32
- package/src/commands/deploy.js +218 -1783
- package/src/commands/handoff.js +9 -246
- package/src/commands/invite.js +10 -318
- package/src/commands/org.js +6 -8
- package/src/commands/publish.js +128 -1153
- package/src/commands/pull.js +22 -36
- package/src/commands/push.js +43 -101
- package/src/commands/register.js +184 -39
- package/src/commands/runtime.js +141 -0
- package/src/commands/template.js +13 -221
- package/src/framework-index.json +18 -7
- package/src/index.js +74 -100
- package/src/utils/asset-upload.js +162 -0
- package/src/utils/code-upload.js +245 -0
- package/src/utils/config.js +11 -44
- package/src/utils/registry-auth.js +35 -1
- package/src/utils/registry-orgs.js +141 -73
- package/src/utils/runtime-upload.js +163 -0
- package/src/commands/login.js +0 -230
- package/src/utils/auth.js +0 -212
- package/src/utils/registry.js +0 -466
package/src/commands/handoff.js
CHANGED
|
@@ -1,254 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* handoff — RESERVED (not available on the new backend yet).
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* uniweb handoff <email> # Register site + transfer to client
|
|
9
|
-
* uniweb handoff <email> --site <id> # Specify site ID (default: auto-generated)
|
|
10
|
-
* uniweb handoff <email> --web # Show web-based handoff instructions
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
import { existsSync } from 'node:fs'
|
|
14
|
-
import { readFile } from 'node:fs/promises'
|
|
15
|
-
import { resolve, join } from 'node:path'
|
|
16
|
-
import { execSync } from 'node:child_process'
|
|
17
|
-
import { randomUUID } from 'node:crypto'
|
|
18
|
-
|
|
19
|
-
import { RemoteRegistry } from '../utils/registry.js'
|
|
20
|
-
import { ensureAuth } from '../utils/auth.js'
|
|
21
|
-
import { findWorkspaceRoot, findFoundations, classifyPackage, promptSelect } from '../utils/workspace.js'
|
|
22
|
-
import { isNonInteractive, getCliPrefix } from '../utils/interactive.js'
|
|
23
|
-
|
|
24
|
-
const colors = {
|
|
25
|
-
reset: '\x1b[0m',
|
|
26
|
-
bright: '\x1b[1m',
|
|
27
|
-
dim: '\x1b[2m',
|
|
28
|
-
cyan: '\x1b[36m',
|
|
29
|
-
green: '\x1b[32m',
|
|
30
|
-
yellow: '\x1b[33m',
|
|
31
|
-
red: '\x1b[31m',
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
function success(message) {
|
|
35
|
-
console.log(`${colors.green}✓${colors.reset} ${message}`)
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function error(message) {
|
|
39
|
-
console.error(`${colors.red}✗${colors.reset} ${message}`)
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function info(message) {
|
|
43
|
-
console.log(`${colors.cyan}→${colors.reset} ${message}`)
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* Parse a flag value from args.
|
|
48
|
-
* @param {string[]} args
|
|
49
|
-
* @param {string} flag - e.g. '--site'
|
|
50
|
-
* @returns {string|null}
|
|
4
|
+
* The "create a site and hand it off to a client" flow ran against the legacy
|
|
5
|
+
* platform (the PHP backend + Cloudflare Worker) the CLI no longer talks to. A
|
|
6
|
+
* new-backend CLI equivalent isn't built; the command name is kept reserved so a
|
|
7
|
+
* future rebuild can claim it without a breaking change.
|
|
51
8
|
*/
|
|
52
|
-
function parseFlag(args, flag) {
|
|
53
|
-
const idx = args.indexOf(flag)
|
|
54
|
-
if (idx === -1 || !args[idx + 1]) return null
|
|
55
|
-
return args[idx + 1]
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* Resolve the foundation directory (same logic as invite.js).
|
|
60
|
-
*/
|
|
61
|
-
async function resolveFoundationDir(args) {
|
|
62
|
-
const cwd = process.cwd()
|
|
63
|
-
const prefix = getCliPrefix()
|
|
64
|
-
|
|
65
|
-
const type = await classifyPackage(cwd)
|
|
66
|
-
if (type === 'foundation') return cwd
|
|
67
|
-
|
|
68
|
-
const workspaceRoot = findWorkspaceRoot(cwd)
|
|
69
|
-
if (workspaceRoot) {
|
|
70
|
-
const foundations = await findFoundations(workspaceRoot)
|
|
71
|
-
|
|
72
|
-
if (foundations.length === 1) {
|
|
73
|
-
return resolve(workspaceRoot, foundations[0])
|
|
74
|
-
}
|
|
75
9
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
for (const f of foundations) {
|
|
81
|
-
console.log(` ${colors.cyan}cd ${f} && ${prefix} handoff ...${colors.reset}`)
|
|
82
|
-
}
|
|
83
|
-
process.exit(1)
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
const choice = await promptSelect('Which foundation?', foundations)
|
|
87
|
-
if (!choice) {
|
|
88
|
-
console.log('\nCancelled.')
|
|
89
|
-
process.exit(0)
|
|
90
|
-
}
|
|
91
|
-
return resolve(workspaceRoot, choice)
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
error('No foundation found in this workspace.')
|
|
96
|
-
console.log('')
|
|
97
|
-
console.log(` ${colors.dim}\`handoff\` creates a site record for your foundation and transfers${colors.reset}`)
|
|
98
|
-
console.log(` ${colors.dim}ownership to a client — they get a licensed, ready-to-use project.${colors.reset}`)
|
|
99
|
-
console.log('')
|
|
100
|
-
console.log(` ${colors.dim}Run this command from a foundation directory or workspace root.${colors.reset}`)
|
|
10
|
+
export async function handoff() {
|
|
11
|
+
console.error("\x1b[31m✗\x1b[0m `uniweb handoff` isn't available on the new backend yet.")
|
|
12
|
+
console.error(' The legacy site-handoff flow was retired with the PHP backend.')
|
|
13
|
+
console.error(' Manage client sites from the Uniweb app for now.')
|
|
101
14
|
process.exit(1)
|
|
102
15
|
}
|
|
103
16
|
|
|
104
|
-
/**
|
|
105
|
-
* Read foundation name and version from dist/meta/schema.json, auto-building if needed.
|
|
106
|
-
*/
|
|
107
|
-
async function readSchema(foundationDir) {
|
|
108
|
-
const distDir = join(foundationDir, 'dist')
|
|
109
|
-
const foundationJs = join(distDir, 'foundation.js')
|
|
110
|
-
const schemaJson = join(distDir, 'meta', 'schema.json')
|
|
111
|
-
|
|
112
|
-
if (!existsSync(foundationJs) || !existsSync(schemaJson)) {
|
|
113
|
-
console.log(`${colors.yellow}⚠${colors.reset} No build found. Building foundation...`)
|
|
114
|
-
console.log('')
|
|
115
|
-
execSync('npx uniweb build --target foundation', {
|
|
116
|
-
cwd: foundationDir,
|
|
117
|
-
stdio: 'inherit',
|
|
118
|
-
})
|
|
119
|
-
console.log('')
|
|
120
|
-
|
|
121
|
-
if (!existsSync(foundationJs) || !existsSync(schemaJson)) {
|
|
122
|
-
error('Build did not produce dist/foundation.js and dist/meta/schema.json')
|
|
123
|
-
process.exit(1)
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
try {
|
|
128
|
-
const schema = JSON.parse(await readFile(schemaJson, 'utf8'))
|
|
129
|
-
const name = schema._self?.name
|
|
130
|
-
const version = schema._self?.version
|
|
131
|
-
|
|
132
|
-
if (!name || !version) {
|
|
133
|
-
error('dist/meta/schema.json missing _self.name or _self.version')
|
|
134
|
-
process.exit(1)
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
return { name, version }
|
|
138
|
-
} catch (err) {
|
|
139
|
-
error(`Failed to read dist/meta/schema.json: ${err.message}`)
|
|
140
|
-
process.exit(1)
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
/**
|
|
145
|
-
* Create a RemoteRegistry instance with auth.
|
|
146
|
-
*/
|
|
147
|
-
async function createRegistry(args) {
|
|
148
|
-
const token = await ensureAuth({ command: 'Handing off', args })
|
|
149
|
-
|
|
150
|
-
const registryUrl = parseFlag(args, '--registry')
|
|
151
|
-
const url = registryUrl || process.env.UNIWEB_REGISTRY_URL || 'http://localhost:4001'
|
|
152
|
-
|
|
153
|
-
return new RemoteRegistry(url, token)
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
/**
|
|
157
|
-
* Handle --web flag: show web-based handoff guidance.
|
|
158
|
-
*/
|
|
159
|
-
function showWebHandoff(email, name) {
|
|
160
|
-
console.log('')
|
|
161
|
-
info(`Web-based handoff`)
|
|
162
|
-
console.log('')
|
|
163
|
-
console.log(` 1. Create a site on ${colors.cyan}uniweb.app${colors.reset} using ${colors.bright}${name}${colors.reset}`)
|
|
164
|
-
console.log(` 2. Add pages and content`)
|
|
165
|
-
console.log(` 3. Transfer ownership to ${colors.bright}${email}${colors.reset}:`)
|
|
166
|
-
console.log(` ${colors.dim}Settings → Transfer site${colors.reset}`)
|
|
167
|
-
console.log('')
|
|
168
|
-
console.log(` ${colors.dim}The license stays with the site — the client can edit${colors.reset}`)
|
|
169
|
-
console.log(` ${colors.dim}and publish immediately.${colors.reset}`)
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
/**
|
|
173
|
-
* Main handoff command handler.
|
|
174
|
-
*/
|
|
175
|
-
export async function handoff(args = []) {
|
|
176
|
-
const prefix = getCliPrefix()
|
|
177
|
-
|
|
178
|
-
// Extract email (first positional arg with @)
|
|
179
|
-
const email = args.find(a => !a.startsWith('--') && a.includes('@'))
|
|
180
|
-
if (!email) {
|
|
181
|
-
error('Email is required.')
|
|
182
|
-
console.log('')
|
|
183
|
-
console.log(` ${colors.dim}Usage: ${prefix} handoff <email> [--site <id>] [--web]${colors.reset}`)
|
|
184
|
-
console.log('')
|
|
185
|
-
console.log(` ${colors.dim}Creates a site record for your foundation and transfers ownership${colors.reset}`)
|
|
186
|
-
console.log(` ${colors.dim}to the client. They get a licensed project, ready to use.${colors.reset}`)
|
|
187
|
-
process.exit(1)
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
const foundationDir = await resolveFoundationDir(args)
|
|
191
|
-
const { name, version } = await readSchema(foundationDir)
|
|
192
|
-
|
|
193
|
-
// --web: guidance-only, no API call
|
|
194
|
-
if (args.includes('--web')) {
|
|
195
|
-
showWebHandoff(email, name)
|
|
196
|
-
return
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
const registry = await createRegistry(args)
|
|
200
|
-
|
|
201
|
-
// Derive site ID
|
|
202
|
-
const siteIdFlag = parseFlag(args, '--site')
|
|
203
|
-
const siteId = siteIdFlag || `${name}-${randomUUID().slice(0, 6)}`
|
|
204
|
-
|
|
205
|
-
info(`Creating site ${colors.bright}${siteId}${colors.reset} with ${name} v${version}...`)
|
|
206
|
-
|
|
207
|
-
// 1. Create site record
|
|
208
|
-
let siteResult
|
|
209
|
-
try {
|
|
210
|
-
siteResult = await registry.createSite(siteId, { foundation: { name } })
|
|
211
|
-
} catch (err) {
|
|
212
|
-
if (err.statusCode === 409) {
|
|
213
|
-
error(`Site "${siteId}" already exists.`)
|
|
214
|
-
console.log(` ${colors.dim}Use --site <id> to specify a different site identifier.${colors.reset}`)
|
|
215
|
-
process.exit(1)
|
|
216
|
-
}
|
|
217
|
-
if (err.statusCode === 404) {
|
|
218
|
-
error(err.message)
|
|
219
|
-
console.log(` ${colors.dim}Make sure your foundation is published: ${prefix} publish${colors.reset}`)
|
|
220
|
-
process.exit(1)
|
|
221
|
-
}
|
|
222
|
-
error(err.message)
|
|
223
|
-
process.exit(1)
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
// 2. Transfer ownership to client
|
|
227
|
-
info(`Transferring to ${colors.bright}${email}${colors.reset}...`)
|
|
228
|
-
|
|
229
|
-
try {
|
|
230
|
-
await registry.transferSiteOwnership(siteId, email)
|
|
231
|
-
} catch (err) {
|
|
232
|
-
error(`Site created but transfer failed: ${err.message}`)
|
|
233
|
-
console.log(` ${colors.dim}Site "${siteId}" is registered. Transfer manually:${colors.reset}`)
|
|
234
|
-
console.log(` ${colors.dim} PATCH /api/sites/${siteId}/owner { "newOwner": "${email}" }${colors.reset}`)
|
|
235
|
-
process.exit(1)
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
// 3. Show result
|
|
239
|
-
console.log('')
|
|
240
|
-
success(`Site created and transferred`)
|
|
241
|
-
console.log('')
|
|
242
|
-
console.log(` ${colors.dim}Site:${colors.reset} ${colors.bright}${siteId}${colors.reset}`)
|
|
243
|
-
console.log(` ${colors.dim}Foundation:${colors.reset} ${name} v${version.split('.')[0]}`)
|
|
244
|
-
console.log(` ${colors.dim}Owner:${colors.reset} ${email}`)
|
|
245
|
-
console.log(` ${colors.dim}License:${colors.reset} ${siteResult.license ? `${colors.green}✓${colors.reset} granted` : `${colors.yellow}⚠${colors.reset} not granted`}`)
|
|
246
|
-
console.log('')
|
|
247
|
-
console.log(` ${colors.bright}Next steps:${colors.reset}`)
|
|
248
|
-
console.log(` 1. Add ${colors.cyan}id: ${siteId}${colors.reset} to your site.yml`)
|
|
249
|
-
console.log(` 2. Share the site files with ${colors.bright}${email}${colors.reset}`)
|
|
250
|
-
console.log(` ${colors.dim}(git repo, zip, shared drive — any method works)${colors.reset}`)
|
|
251
|
-
console.log(` 3. Client opens the project in Uniweb Studio`)
|
|
252
|
-
}
|
|
253
|
-
|
|
254
17
|
export default handoff
|
package/src/commands/invite.js
CHANGED
|
@@ -1,326 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* invite — RESERVED (not available on the new backend yet).
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* uniweb invite <email> --expires 60 # 60-day expiry
|
|
10
|
-
* uniweb invite --list # List invites for your foundation
|
|
11
|
-
* uniweb invite --revoke <inviteId> # Revoke an invite
|
|
12
|
-
* uniweb invite --resend <inviteId> # Resend an invite
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
import { existsSync } from 'node:fs'
|
|
16
|
-
import { readFile } from 'node:fs/promises'
|
|
17
|
-
import { resolve, join } from 'node:path'
|
|
18
|
-
import { execSync } from 'node:child_process'
|
|
19
|
-
|
|
20
|
-
import { RemoteRegistry } from '../utils/registry.js'
|
|
21
|
-
import { ensureAuth } from '../utils/auth.js'
|
|
22
|
-
import { findWorkspaceRoot, findFoundations, classifyPackage, promptSelect } from '../utils/workspace.js'
|
|
23
|
-
import { isNonInteractive, getCliPrefix } from '../utils/interactive.js'
|
|
24
|
-
|
|
25
|
-
const colors = {
|
|
26
|
-
reset: '\x1b[0m',
|
|
27
|
-
bright: '\x1b[1m',
|
|
28
|
-
dim: '\x1b[2m',
|
|
29
|
-
cyan: '\x1b[36m',
|
|
30
|
-
green: '\x1b[32m',
|
|
31
|
-
yellow: '\x1b[33m',
|
|
32
|
-
red: '\x1b[31m',
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function success(message) {
|
|
36
|
-
console.log(`${colors.green}✓${colors.reset} ${message}`)
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function error(message) {
|
|
40
|
-
console.error(`${colors.red}✗${colors.reset} ${message}`)
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function info(message) {
|
|
44
|
-
console.log(`${colors.cyan}→${colors.reset} ${message}`)
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* Parse a flag value from args.
|
|
49
|
-
* @param {string[]} args
|
|
50
|
-
* @param {string} flag - e.g. '--uses'
|
|
51
|
-
* @returns {string|null}
|
|
52
|
-
*/
|
|
53
|
-
function parseFlag(args, flag) {
|
|
54
|
-
const idx = args.indexOf(flag)
|
|
55
|
-
if (idx === -1 || !args[idx + 1]) return null
|
|
56
|
-
return args[idx + 1]
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* Resolve the foundation directory (same logic as publish.js).
|
|
4
|
+
* The foundation client-invite flow ran against the legacy platform (the PHP
|
|
5
|
+
* backend + Cloudflare Worker) the CLI no longer talks to. Client onboarding is
|
|
6
|
+
* handled in the Uniweb app for now; a new-backend CLI equivalent isn't built.
|
|
7
|
+
* The command name is kept reserved so a future rebuild can claim it without a
|
|
8
|
+
* breaking change.
|
|
61
9
|
*/
|
|
62
|
-
async function resolveFoundationDir(args) {
|
|
63
|
-
const cwd = process.cwd()
|
|
64
|
-
const prefix = getCliPrefix()
|
|
65
|
-
|
|
66
|
-
const type = await classifyPackage(cwd)
|
|
67
|
-
if (type === 'foundation') return cwd
|
|
68
|
-
|
|
69
|
-
const workspaceRoot = findWorkspaceRoot(cwd)
|
|
70
|
-
if (workspaceRoot) {
|
|
71
|
-
const foundations = await findFoundations(workspaceRoot)
|
|
72
10
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
if (foundations.length > 1) {
|
|
78
|
-
if (isNonInteractive(args)) {
|
|
79
|
-
error('Multiple foundations found. Specify which one.')
|
|
80
|
-
console.log('')
|
|
81
|
-
for (const f of foundations) {
|
|
82
|
-
console.log(` ${colors.cyan}cd ${f} && ${prefix} invite ...${colors.reset}`)
|
|
83
|
-
}
|
|
84
|
-
process.exit(1)
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
const choice = await promptSelect('Which foundation?', foundations)
|
|
88
|
-
if (!choice) {
|
|
89
|
-
console.log('\nCancelled.')
|
|
90
|
-
process.exit(0)
|
|
91
|
-
}
|
|
92
|
-
return resolve(workspaceRoot, choice)
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
error('No foundation found in this workspace.')
|
|
97
|
-
console.log('')
|
|
98
|
-
console.log(` ${colors.dim}\`invite\` lets you authorize a client to create sites with your${colors.reset}`)
|
|
99
|
-
console.log(` ${colors.dim}foundation — they get their own site, set up automatically.${colors.reset}`)
|
|
100
|
-
console.log('')
|
|
101
|
-
console.log(` ${colors.dim}Run this command from a foundation directory or workspace root.${colors.reset}`)
|
|
11
|
+
export async function invite() {
|
|
12
|
+
console.error("\x1b[31m✗\x1b[0m `uniweb invite` isn't available on the new backend yet.")
|
|
13
|
+
console.error(' The legacy client-invite flow was retired with the PHP backend.')
|
|
14
|
+
console.error(' Invite clients to your foundation from the Uniweb app for now.')
|
|
102
15
|
process.exit(1)
|
|
103
16
|
}
|
|
104
17
|
|
|
105
|
-
/**
|
|
106
|
-
* Read foundation name and version from dist/meta/schema.json, auto-building if needed.
|
|
107
|
-
*/
|
|
108
|
-
async function readSchema(foundationDir) {
|
|
109
|
-
const distDir = join(foundationDir, 'dist')
|
|
110
|
-
const foundationJs = join(distDir, 'foundation.js')
|
|
111
|
-
const schemaJson = join(distDir, 'meta', 'schema.json')
|
|
112
|
-
|
|
113
|
-
if (!existsSync(foundationJs) || !existsSync(schemaJson)) {
|
|
114
|
-
console.log(`${colors.yellow}⚠${colors.reset} No build found. Building foundation...`)
|
|
115
|
-
console.log('')
|
|
116
|
-
execSync('npx uniweb build --target foundation', {
|
|
117
|
-
cwd: foundationDir,
|
|
118
|
-
stdio: 'inherit',
|
|
119
|
-
})
|
|
120
|
-
console.log('')
|
|
121
|
-
|
|
122
|
-
if (!existsSync(foundationJs) || !existsSync(schemaJson)) {
|
|
123
|
-
error('Build did not produce dist/foundation.js and dist/meta/schema.json')
|
|
124
|
-
process.exit(1)
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
try {
|
|
129
|
-
const schema = JSON.parse(await readFile(schemaJson, 'utf8'))
|
|
130
|
-
const name = schema._self?.name
|
|
131
|
-
const version = schema._self?.version
|
|
132
|
-
|
|
133
|
-
if (!name || !version) {
|
|
134
|
-
error('dist/meta/schema.json missing _self.name or _self.version')
|
|
135
|
-
process.exit(1)
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
return { name, version, role: schema._self?.role }
|
|
139
|
-
} catch (err) {
|
|
140
|
-
error(`Failed to read dist/meta/schema.json: ${err.message}`)
|
|
141
|
-
process.exit(1)
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
/**
|
|
146
|
-
* Create a RemoteRegistry instance with auth.
|
|
147
|
-
*/
|
|
148
|
-
async function createRegistry(args) {
|
|
149
|
-
const token = await ensureAuth({ command: 'Creating invite', args })
|
|
150
|
-
|
|
151
|
-
const registryUrl = parseFlag(args, '--registry')
|
|
152
|
-
const url = registryUrl || process.env.UNIWEB_REGISTRY_URL || 'http://localhost:4001'
|
|
153
|
-
|
|
154
|
-
return new RemoteRegistry(url, token)
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
/**
|
|
158
|
-
* Handle --list flag.
|
|
159
|
-
*/
|
|
160
|
-
async function handleList(args) {
|
|
161
|
-
const foundationDir = await resolveFoundationDir(args)
|
|
162
|
-
const { name } = await readSchema(foundationDir)
|
|
163
|
-
const registry = await createRegistry(args)
|
|
164
|
-
|
|
165
|
-
info(`Listing invites for ${colors.bright}${name}${colors.reset}...`)
|
|
166
|
-
|
|
167
|
-
try {
|
|
168
|
-
const invites = await registry.listInvites(name)
|
|
169
|
-
|
|
170
|
-
if (invites.length === 0) {
|
|
171
|
-
console.log(`\n No invites found for ${name}.`)
|
|
172
|
-
return
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
console.log('')
|
|
176
|
-
for (const inv of invites) {
|
|
177
|
-
const statusColor = inv.status === 'active' ? colors.green
|
|
178
|
-
: inv.status === 'revoked' ? colors.red
|
|
179
|
-
: colors.yellow
|
|
180
|
-
console.log(` ${statusColor}${inv.status}${colors.reset} ${inv.email} v${inv.majorVersion} ${inv.usedCount}/${inv.maxUses} uses ${colors.dim}${inv.inviteId}${colors.reset}`)
|
|
181
|
-
}
|
|
182
|
-
console.log('')
|
|
183
|
-
} catch (err) {
|
|
184
|
-
error(err.message)
|
|
185
|
-
process.exit(1)
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
/**
|
|
190
|
-
* Handle --revoke flag.
|
|
191
|
-
*/
|
|
192
|
-
async function handleRevoke(args, inviteId) {
|
|
193
|
-
const foundationDir = await resolveFoundationDir(args)
|
|
194
|
-
const { name } = await readSchema(foundationDir)
|
|
195
|
-
const registry = await createRegistry(args)
|
|
196
|
-
|
|
197
|
-
info(`Revoking invite ${colors.dim}${inviteId}${colors.reset}...`)
|
|
198
|
-
|
|
199
|
-
try {
|
|
200
|
-
const result = await registry.revokeInvite(name, inviteId)
|
|
201
|
-
console.log('')
|
|
202
|
-
success(`Revoked invite for ${colors.bright}${result.email}${colors.reset}`)
|
|
203
|
-
} catch (err) {
|
|
204
|
-
error(err.message)
|
|
205
|
-
process.exit(1)
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
/**
|
|
210
|
-
* Handle --resend flag.
|
|
211
|
-
*/
|
|
212
|
-
async function handleResend(args, inviteId) {
|
|
213
|
-
const foundationDir = await resolveFoundationDir(args)
|
|
214
|
-
const { name } = await readSchema(foundationDir)
|
|
215
|
-
const registry = await createRegistry(args)
|
|
216
|
-
|
|
217
|
-
info(`Resending invite ${colors.dim}${inviteId}${colors.reset}...`)
|
|
218
|
-
|
|
219
|
-
try {
|
|
220
|
-
const result = await registry.resendInvite(name, inviteId)
|
|
221
|
-
console.log('')
|
|
222
|
-
success(`Resent invite to ${colors.bright}${result.email}${colors.reset}`)
|
|
223
|
-
} catch (err) {
|
|
224
|
-
error(err.message)
|
|
225
|
-
process.exit(1)
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
/**
|
|
230
|
-
* Handle create invite (default action).
|
|
231
|
-
*/
|
|
232
|
-
async function handleCreate(args, email) {
|
|
233
|
-
const foundationDir = await resolveFoundationDir(args)
|
|
234
|
-
const { name, version, role } = await readSchema(foundationDir)
|
|
235
|
-
const registry = await createRegistry(args)
|
|
236
|
-
|
|
237
|
-
// Parse options
|
|
238
|
-
const versionFlag = parseFlag(args, '--version')
|
|
239
|
-
const majorVersion = versionFlag
|
|
240
|
-
? parseInt(versionFlag, 10)
|
|
241
|
-
: parseInt(version.split('.')[0], 10)
|
|
242
|
-
|
|
243
|
-
const usesFlag = parseFlag(args, '--uses')
|
|
244
|
-
const maxUses = usesFlag ? parseInt(usesFlag, 10) : 1
|
|
245
|
-
|
|
246
|
-
const expiresFlag = parseFlag(args, '--expires')
|
|
247
|
-
const expiresInDays = expiresFlag ? parseInt(expiresFlag, 10) : 30
|
|
248
|
-
|
|
249
|
-
if (isNaN(majorVersion)) {
|
|
250
|
-
error('Could not determine major version. Use --version to specify.')
|
|
251
|
-
process.exit(1)
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
info(`Creating invite for ${colors.bright}${email}${colors.reset} → ${name} v${majorVersion}...`)
|
|
255
|
-
|
|
256
|
-
try {
|
|
257
|
-
const invite = await registry.createInvite(name, {
|
|
258
|
-
email,
|
|
259
|
-
majorVersion,
|
|
260
|
-
maxUses,
|
|
261
|
-
expiresInDays,
|
|
262
|
-
})
|
|
263
|
-
|
|
264
|
-
const isExtension = role === 'extension'
|
|
265
|
-
|
|
266
|
-
console.log('')
|
|
267
|
-
success(`Invite created`)
|
|
268
|
-
console.log('')
|
|
269
|
-
console.log(` ${colors.dim}ID:${colors.reset} ${invite.inviteId}`)
|
|
270
|
-
console.log(` ${colors.dim}To:${colors.reset} ${invite.email}`)
|
|
271
|
-
console.log(` ${colors.dim}For:${colors.reset} ${name} v${invite.majorVersion}${isExtension ? ' (extension)' : ''}`)
|
|
272
|
-
console.log(` ${colors.dim}Uses:${colors.reset} ${invite.maxUses}`)
|
|
273
|
-
console.log(` ${colors.dim}Expires:${colors.reset} ${new Date(invite.expiresAt).toLocaleDateString()}`)
|
|
274
|
-
console.log(` ${colors.dim}Link:${colors.reset} ${colors.cyan}${registry.apiUrl}/invite/${invite.inviteId}${colors.reset}`)
|
|
275
|
-
console.log('')
|
|
276
|
-
if (isExtension) {
|
|
277
|
-
console.log(` ${colors.dim}When ${invite.email} adds ${name} to their site${colors.reset}`)
|
|
278
|
-
console.log(` ${colors.dim}on uniweb.app or Studio, it will be authorized automatically.${colors.reset}`)
|
|
279
|
-
} else {
|
|
280
|
-
console.log(` ${colors.dim}When ${invite.email} creates a site with ${name}${colors.reset}`)
|
|
281
|
-
console.log(` ${colors.dim}on uniweb.app or Studio, it will be authorized automatically.${colors.reset}`)
|
|
282
|
-
}
|
|
283
|
-
} catch (err) {
|
|
284
|
-
error(err.message)
|
|
285
|
-
process.exit(1)
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
/**
|
|
290
|
-
* Main invite command handler.
|
|
291
|
-
*/
|
|
292
|
-
export async function invite(args = []) {
|
|
293
|
-
// Dispatch based on flags
|
|
294
|
-
if (args.includes('--list')) {
|
|
295
|
-
await handleList(args)
|
|
296
|
-
return
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
const revokeId = parseFlag(args, '--revoke')
|
|
300
|
-
if (revokeId) {
|
|
301
|
-
await handleRevoke(args, revokeId)
|
|
302
|
-
return
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
const resendId = parseFlag(args, '--resend')
|
|
306
|
-
if (resendId) {
|
|
307
|
-
await handleResend(args, resendId)
|
|
308
|
-
return
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
// Default: create invite — first positional arg is the email
|
|
312
|
-
const email = args.find(a => !a.startsWith('--') && a.includes('@'))
|
|
313
|
-
if (!email) {
|
|
314
|
-
error('Email is required.')
|
|
315
|
-
console.log('')
|
|
316
|
-
console.log(` ${colors.dim}Usage: ${getCliPrefix()} invite <email> [--uses N] [--expires N]${colors.reset}`)
|
|
317
|
-
console.log(` ${colors.dim} ${getCliPrefix()} invite --list${colors.reset}`)
|
|
318
|
-
console.log(` ${colors.dim} ${getCliPrefix()} invite --revoke <id>${colors.reset}`)
|
|
319
|
-
console.log(` ${colors.dim} ${getCliPrefix()} invite --resend <id>${colors.reset}`)
|
|
320
|
-
process.exit(1)
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
await handleCreate(args, email)
|
|
324
|
-
}
|
|
325
|
-
|
|
326
18
|
export default invite
|
package/src/commands/org.js
CHANGED
|
@@ -8,9 +8,8 @@
|
|
|
8
8
|
* the legacy platform (publish/deploy); this talks to the registry backend.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import { listOrgs, createOrg, validateHandle, bareHandle } from '../utils/registry-orgs.js'
|
|
11
|
+
import { BackendClient } from '../backend/client.js'
|
|
12
|
+
import { validateHandle, bareHandle } from '../utils/registry-orgs.js'
|
|
14
13
|
|
|
15
14
|
const colors = { reset: '\x1b[0m', bright: '\x1b[1m', dim: '\x1b[2m', red: '\x1b[31m', green: '\x1b[32m' }
|
|
16
15
|
const error = (m) => console.error(`${colors.red}✗${colors.reset} ${m}`)
|
|
@@ -18,11 +17,10 @@ const success = (m) => console.log(`${colors.green}✓${colors.reset} ${m}`)
|
|
|
18
17
|
|
|
19
18
|
export async function org(args = []) {
|
|
20
19
|
const sub = args[0]
|
|
21
|
-
const apiBase = getRegistryApiBaseUrl()
|
|
22
20
|
|
|
23
21
|
if (sub === 'list') {
|
|
24
|
-
const
|
|
25
|
-
const orgs = await
|
|
22
|
+
const client = new BackendClient({ args, command: 'Listing orgs' })
|
|
23
|
+
const { orgs } = await client.fetchOrgs()
|
|
26
24
|
if (!orgs.length) {
|
|
27
25
|
console.log('You have no orgs yet. Create one: uniweb org create <handle>')
|
|
28
26
|
return { exitCode: 0 }
|
|
@@ -45,9 +43,9 @@ export async function org(args = []) {
|
|
|
45
43
|
error(invalid)
|
|
46
44
|
return { exitCode: 2 }
|
|
47
45
|
}
|
|
48
|
-
const
|
|
46
|
+
const client = new BackendClient({ args, command: 'Creating an org' })
|
|
49
47
|
try {
|
|
50
|
-
const org = await createOrg(
|
|
48
|
+
const org = await client.createOrg(handle)
|
|
51
49
|
success(`Created ${colors.bright}@${org.handle}${colors.reset} — you're a member${org.is_primary ? ' (primary)' : ''}.`)
|
|
52
50
|
console.log(`${colors.dim}Publish under it: uniweb register --scope @${org.handle}${colors.reset}`)
|
|
53
51
|
return { exitCode: 0 }
|