amorphous-cli 0.1.0 → 0.1.2
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/lib/commands.mjs +2 -1
- package/lib/help.mjs +3 -2
- package/lib/login.mjs +64 -83
- package/package.json +1 -1
package/lib/commands.mjs
CHANGED
|
@@ -167,7 +167,8 @@ async function cmdLogin(rest) {
|
|
|
167
167
|
if (flags.help) return printHelp('login')
|
|
168
168
|
const email = positional[0]
|
|
169
169
|
if (!email) throw new Error('Pass an email.')
|
|
170
|
-
|
|
170
|
+
const code = typeof flags.code === 'string' ? flags.code : undefined
|
|
171
|
+
await loginWithMagicLink(email.trim(), { code })
|
|
171
172
|
}
|
|
172
173
|
|
|
173
174
|
async function cmdCreate(cwd, rest) {
|
package/lib/help.mjs
CHANGED
|
@@ -30,9 +30,10 @@ Run amorphous <command> --help for flags. Pass --json where you need machine out
|
|
|
30
30
|
`
|
|
31
31
|
|
|
32
32
|
export const COMMAND_HELP = {
|
|
33
|
-
login: `login <email>
|
|
33
|
+
login: `login <email> [--code <code>]
|
|
34
34
|
|
|
35
|
-
|
|
35
|
+
Send a sign-in code to that email, then type it.
|
|
36
|
+
--code skips the prompt. An agent reads the code from the inbox and passes it here.`,
|
|
36
37
|
|
|
37
38
|
list: `list [--view recents|all|starred|archive] [--folder <name>] [--tag <name>] [--search <q>] [--json]
|
|
38
39
|
|
package/lib/login.mjs
CHANGED
|
@@ -1,10 +1,19 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createInterface } from 'node:readline'
|
|
2
2
|
import { createServer as createNetServer } from 'node:net'
|
|
3
3
|
import { createClient } from '@supabase/supabase-js'
|
|
4
4
|
import { loadDotEnv, supabaseAnonKey, supabaseUrl } from './config.mjs'
|
|
5
5
|
import { mailpitBase, sessionFromVerifyUrl, waitForMagicLinkVerifyUrl } from './mailpit.mjs'
|
|
6
6
|
import { saveSession } from './session.mjs'
|
|
7
7
|
|
|
8
|
+
function isLocalSupabase() {
|
|
9
|
+
try {
|
|
10
|
+
const host = new URL(supabaseUrl()).hostname
|
|
11
|
+
return host === '127.0.0.1' || host === 'localhost'
|
|
12
|
+
} catch {
|
|
13
|
+
return false
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
8
17
|
async function freePort(preferred) {
|
|
9
18
|
if (preferred) {
|
|
10
19
|
const ok = await new Promise((resolve) => {
|
|
@@ -29,6 +38,7 @@ async function freePort(preferred) {
|
|
|
29
38
|
|
|
30
39
|
async function mailpitReachable() {
|
|
31
40
|
if (process.env.AMORPHOUS_AGENT_LOGIN === '0') return false
|
|
41
|
+
if (!isLocalSupabase()) return false
|
|
32
42
|
try {
|
|
33
43
|
const res = await fetch(`${mailpitBase()}/api/v1/messages`, {
|
|
34
44
|
signal: AbortSignal.timeout(1500),
|
|
@@ -54,100 +64,71 @@ async function loginViaMailpit(supabase, email, redirectTo, sinceMs) {
|
|
|
54
64
|
}
|
|
55
65
|
}
|
|
56
66
|
|
|
57
|
-
async function
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
res.writeHead(200, { 'Content-Type': 'text/html' })
|
|
68
|
-
res.end('<p>Signed in. You can close this tab and return to the terminal.</p>')
|
|
69
|
-
server.close()
|
|
70
|
-
resolve(data.session)
|
|
71
|
-
return
|
|
72
|
-
}
|
|
73
|
-
if (url.pathname === '/callback') {
|
|
74
|
-
res.writeHead(200, { 'Content-Type': 'text/html' })
|
|
75
|
-
res.end(`<!DOCTYPE html><html><body><p>Completing sign-in…</p><script>
|
|
76
|
-
const h = new URLSearchParams(location.hash.slice(1));
|
|
77
|
-
const access_token = h.get('access_token');
|
|
78
|
-
const refresh_token = h.get('refresh_token');
|
|
79
|
-
if (access_token) {
|
|
80
|
-
fetch('/token', { method:'POST', headers:{'Content-Type':'application/json'},
|
|
81
|
-
body: JSON.stringify({ access_token, refresh_token }) })
|
|
82
|
-
.then(() => { document.body.innerHTML = '<p>Signed in. Close this tab.</p>'; });
|
|
83
|
-
}
|
|
84
|
-
</script></body></html>`)
|
|
85
|
-
return
|
|
86
|
-
}
|
|
87
|
-
if (url.pathname === '/token' && req.method === 'POST') {
|
|
88
|
-
let body = ''
|
|
89
|
-
for await (const chunk of req) body += chunk
|
|
90
|
-
const parsed = JSON.parse(body)
|
|
91
|
-
res.writeHead(204)
|
|
92
|
-
res.end()
|
|
93
|
-
server.close()
|
|
94
|
-
resolve({
|
|
95
|
-
access_token: parsed.access_token,
|
|
96
|
-
refresh_token: parsed.refresh_token,
|
|
97
|
-
})
|
|
98
|
-
return
|
|
99
|
-
}
|
|
100
|
-
res.writeHead(404)
|
|
101
|
-
res.end()
|
|
102
|
-
} catch (e) {
|
|
103
|
-
reject(e)
|
|
104
|
-
}
|
|
105
|
-
})
|
|
106
|
-
server.listen(callbackPort, '127.0.0.1', async () => {
|
|
107
|
-
const { error } = await supabase.auth.signInWithOtp({
|
|
108
|
-
email,
|
|
109
|
-
options: { emailRedirectTo: redirectTo },
|
|
110
|
-
})
|
|
111
|
-
if (error) {
|
|
112
|
-
server.close()
|
|
113
|
-
reject(error)
|
|
114
|
-
return
|
|
115
|
-
}
|
|
116
|
-
console.error(`Magic link sent to ${email}. Open it (Mailpit ${mailpitBase()}).`)
|
|
117
|
-
console.error(`Waiting for callback on ${redirectTo} …`)
|
|
67
|
+
async function readCode(email) {
|
|
68
|
+
if (!process.stdin.isTTY) {
|
|
69
|
+
throw new Error(
|
|
70
|
+
`Code sent to ${email}. Read it from that inbox, then run: amorphous login ${email} --code <code>`,
|
|
71
|
+
)
|
|
72
|
+
}
|
|
73
|
+
const rl = createInterface({ input: process.stdin, output: process.stderr })
|
|
74
|
+
try {
|
|
75
|
+
const answer = await new Promise((resolve) => {
|
|
76
|
+
rl.question('Code: ', resolve)
|
|
118
77
|
})
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
78
|
+
return String(answer || '').replace(/\s/g, '')
|
|
79
|
+
} finally {
|
|
80
|
+
rl.close()
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function finish(tokens) {
|
|
85
|
+
if (!tokens?.access_token || !tokens?.refresh_token) {
|
|
86
|
+
throw new Error('Sign-in did not return a session.')
|
|
87
|
+
}
|
|
88
|
+
saveSession({
|
|
89
|
+
access_token: tokens.access_token,
|
|
90
|
+
refresh_token: tokens.refresh_token,
|
|
91
|
+
})
|
|
92
|
+
console.log('Logged in.')
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async function verifyEmailCode(supabase, email, code) {
|
|
96
|
+
const token = String(code || '').replace(/\s/g, '')
|
|
97
|
+
if (!/^\d{6,10}$/.test(token)) throw new Error('Enter the code from the email.')
|
|
98
|
+
const { data, error } = await supabase.auth.verifyOtp({
|
|
99
|
+
email,
|
|
100
|
+
token,
|
|
101
|
+
type: 'email',
|
|
124
102
|
})
|
|
103
|
+
if (error) throw error
|
|
104
|
+
return data.session
|
|
125
105
|
}
|
|
126
106
|
|
|
127
|
-
export async function loginWithMagicLink(email) {
|
|
107
|
+
export async function loginWithMagicLink(email, { code } = {}) {
|
|
128
108
|
loadDotEnv()
|
|
129
|
-
const preferred = Number(process.env.AMORPHOUS_CLI_CALLBACK_PORT || 8765)
|
|
130
|
-
const port = await freePort(preferred)
|
|
131
|
-
const redirectTo = `http://127.0.0.1:${port}/callback`
|
|
132
109
|
const supabase = createClient(supabaseUrl(), supabaseAnonKey())
|
|
133
|
-
const sinceMs = Date.now()
|
|
134
110
|
|
|
135
|
-
|
|
136
|
-
|
|
111
|
+
if (!code && (await mailpitReachable())) {
|
|
112
|
+
const preferred = Number(process.env.AMORPHOUS_CLI_CALLBACK_PORT || 8765)
|
|
113
|
+
const port = await freePort(preferred)
|
|
114
|
+
const redirectTo = `http://127.0.0.1:${port}/callback`
|
|
115
|
+
const sinceMs = Date.now()
|
|
137
116
|
const { error } = await supabase.auth.signInWithOtp({
|
|
138
117
|
email,
|
|
139
118
|
options: { emailRedirectTo: redirectTo },
|
|
140
119
|
})
|
|
141
120
|
if (error) throw error
|
|
142
|
-
console.error(`
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
tokens = await loginViaCallbackServer(supabase, email, redirectTo)
|
|
121
|
+
console.error(`Code sent to ${email}. Reading the local mail catcher…`)
|
|
122
|
+
finish(await loginViaMailpit(supabase, email, redirectTo, sinceMs))
|
|
123
|
+
return
|
|
146
124
|
}
|
|
147
125
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
126
|
+
if (!code) {
|
|
127
|
+
const { error } = await supabase.auth.signInWithOtp({ email })
|
|
128
|
+
if (error) throw error
|
|
129
|
+
console.error(`Code sent to ${email}.`)
|
|
130
|
+
code = await readCode(email)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
finish(await verifyEmailCode(supabase, email, code))
|
|
153
134
|
}
|