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 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
- await loginWithMagicLink(email.trim())
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
- Sign in with a magic link sent to that email.`,
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 { createServer } from 'node:http'
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 loginViaCallbackServer(supabase, email, redirectTo) {
58
- const callbackPort = new URL(redirectTo).port || '8765'
59
- return new Promise((resolve, reject) => {
60
- const server = createServer(async (req, res) => {
61
- try {
62
- const url = new URL(req.url || '/', redirectTo)
63
- if (url.pathname === '/callback' && url.searchParams.get('code')) {
64
- const code = url.searchParams.get('code')
65
- const { data, error } = await supabase.auth.exchangeCodeForSession(code)
66
- if (error) throw error
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
- server.on('error', reject)
120
- setTimeout(() => {
121
- server.close()
122
- reject(new Error('Login timed out after 5 minutes'))
123
- }, 5 * 60 * 1000)
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
- let tokens
136
- if (await mailpitReachable()) {
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(`Magic link sent to ${email}. Reading Mailpit (${mailpitBase()})…`)
143
- tokens = await loginViaMailpit(supabase, email, redirectTo, sinceMs)
144
- } else {
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
- saveSession({
149
- access_token: tokens.access_token,
150
- refresh_token: tokens.refresh_token,
151
- })
152
- console.log('Logged in.')
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "amorphous-cli",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Command line for Amorphous",
5
5
  "type": "module",
6
6
  "bin": {