amorphous-cli 0.1.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.
- package/index.mjs +14 -0
- package/lib/api.mjs +67 -0
- package/lib/commands.mjs +736 -0
- package/lib/config.mjs +92 -0
- package/lib/files.mjs +50 -0
- package/lib/help.mjs +150 -0
- package/lib/login.mjs +153 -0
- package/lib/mailpit.mjs +99 -0
- package/lib/session.mjs +36 -0
- package/lib/tls.mjs +25 -0
- package/mcp.mjs +132 -0
- package/package.json +30 -0
package/lib/commands.mjs
ADDED
|
@@ -0,0 +1,736 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, existsSync } from 'node:fs'
|
|
2
|
+
import { apiFetch } from './api.mjs'
|
|
3
|
+
import { abstractsOrigin, appOrigin, projectMetaPath } from './config.mjs'
|
|
4
|
+
import { recordsFromLocal, readProjectFiles, writeProjectFiles } from './files.mjs'
|
|
5
|
+
import { ALIASES, printHelp, wantsHelp } from './help.mjs'
|
|
6
|
+
import { loginWithMagicLink } from './login.mjs'
|
|
7
|
+
|
|
8
|
+
const BOOL_FLAGS = new Set([
|
|
9
|
+
'json',
|
|
10
|
+
'help',
|
|
11
|
+
'none',
|
|
12
|
+
'live',
|
|
13
|
+
'apply',
|
|
14
|
+
'starred',
|
|
15
|
+
'archived',
|
|
16
|
+
'owned',
|
|
17
|
+
'member',
|
|
18
|
+
'open',
|
|
19
|
+
'closed',
|
|
20
|
+
])
|
|
21
|
+
|
|
22
|
+
function parseFlags(args) {
|
|
23
|
+
const flags = {}
|
|
24
|
+
const positional = []
|
|
25
|
+
for (let i = 0; i < args.length; i++) {
|
|
26
|
+
const a = args[i]
|
|
27
|
+
if (a === '-h' || a === '--help') {
|
|
28
|
+
flags.help = true
|
|
29
|
+
continue
|
|
30
|
+
}
|
|
31
|
+
if (a.startsWith('--')) {
|
|
32
|
+
const key = a.slice(2)
|
|
33
|
+
if (BOOL_FLAGS.has(key)) {
|
|
34
|
+
flags[key] = true
|
|
35
|
+
continue
|
|
36
|
+
}
|
|
37
|
+
const next = args[i + 1]
|
|
38
|
+
if (next && !next.startsWith('-')) {
|
|
39
|
+
flags[key] = next
|
|
40
|
+
i++
|
|
41
|
+
} else {
|
|
42
|
+
flags[key] = true
|
|
43
|
+
}
|
|
44
|
+
} else {
|
|
45
|
+
positional.push(a)
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return { flags, positional }
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function jsonMode(flags) {
|
|
52
|
+
return Boolean(flags.json)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function printJson(data) {
|
|
56
|
+
console.log(JSON.stringify(data, null, 2))
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function readMeta(cwd) {
|
|
60
|
+
const path = projectMetaPath(cwd)
|
|
61
|
+
if (!existsSync(path)) return null
|
|
62
|
+
return JSON.parse(readFileSync(path, 'utf8'))
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function writeMeta(cwd, abstractId) {
|
|
66
|
+
writeFileSync(projectMetaPath(cwd), JSON.stringify({ abstractId }, null, 2) + '\n')
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function requireMeta(cwd) {
|
|
70
|
+
const meta = readMeta(cwd)
|
|
71
|
+
if (!meta?.abstractId) {
|
|
72
|
+
throw new Error('No .amorphous.json here. Run amorphous create first.')
|
|
73
|
+
}
|
|
74
|
+
return meta.abstractId
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function resolveAbstractId(cwd, flags, positional) {
|
|
78
|
+
if (flags.abstract) return flags.abstract
|
|
79
|
+
if (positional[0]) {
|
|
80
|
+
const token = positional[0]
|
|
81
|
+
if (/^[0-9a-f-]{36}$/i.test(token)) return token
|
|
82
|
+
const data = await apiFetch('/api/abstracts?view=all')
|
|
83
|
+
const hit = (data.abstracts || []).find(
|
|
84
|
+
(a) => a.name.toLowerCase() === token.toLowerCase(),
|
|
85
|
+
)
|
|
86
|
+
if (hit) return hit.id
|
|
87
|
+
throw new Error(`No abstract named “${token}”`)
|
|
88
|
+
}
|
|
89
|
+
try {
|
|
90
|
+
return requireMeta(cwd)
|
|
91
|
+
} catch {
|
|
92
|
+
throw new Error('Pass an abstract or run this from its directory.')
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async function loadFolders() {
|
|
97
|
+
const meta = await apiFetch('/api/organise/meta')
|
|
98
|
+
return meta.all_collections || meta.collections || []
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async function resolveFolder(token) {
|
|
102
|
+
if (!token) return null
|
|
103
|
+
const folders = await loadFolders()
|
|
104
|
+
const want = String(token)
|
|
105
|
+
const hit = folders.find(
|
|
106
|
+
(c) =>
|
|
107
|
+
c.id === want ||
|
|
108
|
+
c.path === want ||
|
|
109
|
+
c.name.toLowerCase() === want.toLowerCase(),
|
|
110
|
+
)
|
|
111
|
+
if (!hit) throw new Error(`No folder named “${token}”`)
|
|
112
|
+
return hit
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function folderToken(flags) {
|
|
116
|
+
return flags.folder || flags.collection
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function parseAccess(raw) {
|
|
120
|
+
const v = String(raw || 'view').trim().toLowerCase()
|
|
121
|
+
if (['view', 'user', 'can-view', 'can view'].includes(v)) return 'user'
|
|
122
|
+
if (['use', 'can-use', 'can use'].includes(v)) return 'use'
|
|
123
|
+
if (['edit', 'editor', 'can-edit', 'can edit'].includes(v)) return 'editor'
|
|
124
|
+
throw new Error('Access must be view, use, or edit.')
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function accessLabel(role) {
|
|
128
|
+
if (role === 'owner') return 'Owner'
|
|
129
|
+
if (role === 'editor' || role === 'admin') return 'Can edit'
|
|
130
|
+
if (role === 'use') return 'Can use'
|
|
131
|
+
return 'Can view'
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function listRow(row) {
|
|
135
|
+
const tags = (row.tags || []).join(', ')
|
|
136
|
+
return [
|
|
137
|
+
row.name,
|
|
138
|
+
row.id,
|
|
139
|
+
accessLabel(row.role),
|
|
140
|
+
row.starred ? 'starred' : null,
|
|
141
|
+
row.archived ? 'archived' : null,
|
|
142
|
+
row.collection_path ? `in ${row.collection_path}` : null,
|
|
143
|
+
tags ? `#${tags.replace(/,\s*/g, ' #')}` : null,
|
|
144
|
+
row.live_open ? 'open' : 'closed',
|
|
145
|
+
]
|
|
146
|
+
.filter(Boolean)
|
|
147
|
+
.join(' · ')
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function publicAbstract(row) {
|
|
151
|
+
return {
|
|
152
|
+
id: row.id,
|
|
153
|
+
name: row.name,
|
|
154
|
+
role: row.role,
|
|
155
|
+
starred: row.starred,
|
|
156
|
+
archived: row.archived,
|
|
157
|
+
tags: row.tags || [],
|
|
158
|
+
folder: row.collection_path || null,
|
|
159
|
+
folder_id: row.collection_id || null,
|
|
160
|
+
open: Boolean(row.live_open),
|
|
161
|
+
updated_at: row.updated_at,
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
async function cmdLogin(rest) {
|
|
166
|
+
const { flags, positional } = parseFlags(rest)
|
|
167
|
+
if (flags.help) return printHelp('login')
|
|
168
|
+
const email = positional[0]
|
|
169
|
+
if (!email) throw new Error('Pass an email.')
|
|
170
|
+
await loginWithMagicLink(email.trim())
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
async function cmdCreate(cwd, rest) {
|
|
174
|
+
const { flags } = parseFlags(rest)
|
|
175
|
+
if (flags.help) return printHelp('create')
|
|
176
|
+
const data = await apiFetch('/api/abstracts', { method: 'POST' })
|
|
177
|
+
const id = data.id
|
|
178
|
+
writeMeta(cwd, id)
|
|
179
|
+
const name = typeof flags.name === 'string' ? flags.name.trim() : ''
|
|
180
|
+
if (name) {
|
|
181
|
+
await apiFetch(`/api/abstracts/${id}`, {
|
|
182
|
+
method: 'PATCH',
|
|
183
|
+
body: JSON.stringify({ name }),
|
|
184
|
+
})
|
|
185
|
+
}
|
|
186
|
+
const folderName = folderToken(flags)
|
|
187
|
+
if (folderName) {
|
|
188
|
+
const folder = await resolveFolder(folderName)
|
|
189
|
+
await apiFetch(`/api/abstracts/${id}/filing`, {
|
|
190
|
+
method: 'PATCH',
|
|
191
|
+
body: JSON.stringify({ collection_id: folder.id }),
|
|
192
|
+
})
|
|
193
|
+
}
|
|
194
|
+
if (jsonMode(flags)) printJson({ id, name: name || 'Untitled' })
|
|
195
|
+
else console.log(id)
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function takeAccess(flags, positional) {
|
|
199
|
+
if (flags.access || flags.role) {
|
|
200
|
+
return { access: flags.access || flags.role, rest: positional }
|
|
201
|
+
}
|
|
202
|
+
if (positional[0] && /^(view|use|edit|user|editor)$/i.test(positional[0])) {
|
|
203
|
+
return { access: positional[0], rest: positional.slice(1) }
|
|
204
|
+
}
|
|
205
|
+
return { access: 'view', rest: positional }
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
async function resolvePerson(abstractId, who) {
|
|
209
|
+
if (/^[0-9a-f-]{36}$/i.test(who) && !/@/.test(who)) return { userId: who }
|
|
210
|
+
const detail = await apiFetch(`/api/abstracts/${abstractId}`)
|
|
211
|
+
const needle = who.includes('@') ? who.split('@')[0].toLowerCase() : who.toLowerCase()
|
|
212
|
+
const hit = (detail.members || []).find((m) => {
|
|
213
|
+
const name = (m.name || '').toLowerCase()
|
|
214
|
+
return m.user_id === who || name === needle || name === who.toLowerCase()
|
|
215
|
+
})
|
|
216
|
+
if (hit) return { userId: hit.user_id }
|
|
217
|
+
if (who.includes('@')) return { email: who }
|
|
218
|
+
throw new Error('No person matches that.')
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
async function cmdInvite(cwd, rest) {
|
|
222
|
+
const { flags, positional } = parseFlags(rest)
|
|
223
|
+
if (flags.help) return printHelp('invite')
|
|
224
|
+
const email = flags.email || positional[0]
|
|
225
|
+
if (!email) throw new Error('Pass an email.')
|
|
226
|
+
const afterEmail = flags.email ? positional : positional.slice(1)
|
|
227
|
+
const { access, rest: absArgs } = takeAccess(flags, afterEmail)
|
|
228
|
+
const id = await resolveAbstractId(cwd, flags, absArgs)
|
|
229
|
+
const role = parseAccess(access)
|
|
230
|
+
await apiFetch(`/api/abstracts/${id}/grant`, {
|
|
231
|
+
method: 'POST',
|
|
232
|
+
body: JSON.stringify({ email, role }),
|
|
233
|
+
})
|
|
234
|
+
console.log('Invited.')
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
async function cmdRemove(cwd, rest) {
|
|
238
|
+
const { flags, positional } = parseFlags(rest)
|
|
239
|
+
if (flags.help) return printHelp('remove')
|
|
240
|
+
const who = flags.email || positional[0]
|
|
241
|
+
if (!who) throw new Error('Pass an email or id.')
|
|
242
|
+
const id = await resolveAbstractId(cwd, flags, positional.slice(flags.email ? 0 : 1))
|
|
243
|
+
const body = await resolvePerson(id, who)
|
|
244
|
+
await apiFetch(`/api/abstracts/${id}/revoke`, {
|
|
245
|
+
method: 'POST',
|
|
246
|
+
body: JSON.stringify(body),
|
|
247
|
+
})
|
|
248
|
+
console.log('Removed.')
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
async function cmdPeople(cwd, rest) {
|
|
252
|
+
const { flags, positional } = parseFlags(rest)
|
|
253
|
+
if (flags.help) return printHelp('people')
|
|
254
|
+
const id = await resolveAbstractId(cwd, flags, positional)
|
|
255
|
+
const detail = await apiFetch(`/api/abstracts/${id}`)
|
|
256
|
+
const people = (detail.members || []).map((m) => ({
|
|
257
|
+
id: m.user_id,
|
|
258
|
+
name: m.name,
|
|
259
|
+
access: accessLabel(m.role),
|
|
260
|
+
role: m.role,
|
|
261
|
+
}))
|
|
262
|
+
if (jsonMode(flags)) {
|
|
263
|
+
printJson({ people })
|
|
264
|
+
return
|
|
265
|
+
}
|
|
266
|
+
if (!people.length) {
|
|
267
|
+
console.log('No people yet.')
|
|
268
|
+
return
|
|
269
|
+
}
|
|
270
|
+
for (const p of people) {
|
|
271
|
+
console.log(`${p.name} · ${p.access} · ${p.id}`)
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
async function cmdPull(cwd, rest) {
|
|
276
|
+
const { flags } = parseFlags(rest)
|
|
277
|
+
if (flags.help) return printHelp('pull')
|
|
278
|
+
const id = requireMeta(cwd)
|
|
279
|
+
const detail = await apiFetch(`/api/abstracts/${id}`)
|
|
280
|
+
const records = await apiFetch(`/api/abstracts/${id}/records`)
|
|
281
|
+
const rows = records.records || []
|
|
282
|
+
const shared = rows.filter((r) => r.visibility !== 'private')
|
|
283
|
+
const priv = rows.filter((r) => r.visibility === 'private')
|
|
284
|
+
writeProjectFiles(cwd, {
|
|
285
|
+
html: detail.abstract.draft_html,
|
|
286
|
+
css: detail.abstract.draft_css,
|
|
287
|
+
js: detail.abstract.draft_js,
|
|
288
|
+
shared,
|
|
289
|
+
private: priv,
|
|
290
|
+
})
|
|
291
|
+
console.log('Pulled.')
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
async function cmdPush(cwd, rest) {
|
|
295
|
+
const { flags } = parseFlags(rest)
|
|
296
|
+
if (flags.help) return printHelp('push')
|
|
297
|
+
const id = requireMeta(cwd)
|
|
298
|
+
const localRecords = recordsFromLocal(cwd)
|
|
299
|
+
const { refused } = await apiFetch(`/api/abstracts/${id}/records`, {
|
|
300
|
+
method: 'POST',
|
|
301
|
+
body: JSON.stringify({ records: localRecords }),
|
|
302
|
+
})
|
|
303
|
+
if (refused?.length) {
|
|
304
|
+
console.error('Skipped (already live):', refused.join(', '))
|
|
305
|
+
}
|
|
306
|
+
const { html, css, js } = readProjectFiles(cwd)
|
|
307
|
+
await apiFetch(`/api/abstracts/${id}/push`, {
|
|
308
|
+
method: 'POST',
|
|
309
|
+
body: JSON.stringify({ html, css, js }),
|
|
310
|
+
})
|
|
311
|
+
console.log('Pushed.')
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function urlsFor(id) {
|
|
315
|
+
return {
|
|
316
|
+
open: `${appOrigin().replace(/\/$/, '')}/a/${id}`,
|
|
317
|
+
preview: `${abstractsOrigin().replace(/\/$/, '')}/${id}/draft`,
|
|
318
|
+
live: `${abstractsOrigin().replace(/\/$/, '')}/${id}`,
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
async function cmdOpen(cwd, rest) {
|
|
323
|
+
const { flags, positional } = parseFlags(rest)
|
|
324
|
+
if (flags.help) return printHelp('open')
|
|
325
|
+
const id = await resolveAbstractId(cwd, flags, positional)
|
|
326
|
+
const urls = urlsFor(id)
|
|
327
|
+
if (jsonMode(flags)) printJson(urls)
|
|
328
|
+
else console.log(urls.open)
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
async function cmdPreview(cwd, rest) {
|
|
332
|
+
const { flags, positional } = parseFlags(rest)
|
|
333
|
+
if (flags.help) return printHelp('preview')
|
|
334
|
+
const id = await resolveAbstractId(cwd, flags, positional)
|
|
335
|
+
const urls = urlsFor(id)
|
|
336
|
+
if (jsonMode(flags)) printJson({ preview: urls.preview })
|
|
337
|
+
else console.log(urls.preview)
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
async function cmdPublish(cwd, rest) {
|
|
341
|
+
const { flags, positional } = parseFlags(rest)
|
|
342
|
+
if (flags.help) return printHelp('publish')
|
|
343
|
+
const id = await resolveAbstractId(cwd, flags, positional)
|
|
344
|
+
const live = Boolean(flags.live)
|
|
345
|
+
const version = await apiFetch(`/api/abstracts/${id}/publish`, {
|
|
346
|
+
method: 'POST',
|
|
347
|
+
body: JSON.stringify({ live }),
|
|
348
|
+
})
|
|
349
|
+
if (jsonMode(flags)) printJson({ id: version.id, label: version.label, live })
|
|
350
|
+
else console.log(live ? 'It’s live.' : 'Published.')
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
async function cmdLive(cwd, rest) {
|
|
354
|
+
const { flags, positional } = parseFlags(rest)
|
|
355
|
+
if (flags.help) return printHelp('live')
|
|
356
|
+
const id = await resolveAbstractId(cwd, flags, positional)
|
|
357
|
+
const detail = await apiFetch(`/api/abstracts/${id}`)
|
|
358
|
+
const versionId =
|
|
359
|
+
flags.version ||
|
|
360
|
+
detail.abstract.live_version_id ||
|
|
361
|
+
detail.versions?.[detail.versions.length - 1]?.id
|
|
362
|
+
if (!versionId) throw new Error('Nothing published yet.')
|
|
363
|
+
await apiFetch(`/api/abstracts/${id}/live`, {
|
|
364
|
+
method: 'POST',
|
|
365
|
+
body: JSON.stringify({ versionId }),
|
|
366
|
+
})
|
|
367
|
+
console.log('This is the live version.')
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
async function cmdVisibility(cwd, rest) {
|
|
371
|
+
const { flags, positional } = parseFlags(rest)
|
|
372
|
+
if (flags.help) return printHelp('visibility')
|
|
373
|
+
const mode = String(positional[0] || '').toLowerCase()
|
|
374
|
+
if (mode !== 'anyone' && mode !== 'members') {
|
|
375
|
+
throw new Error('Pass anyone or members.')
|
|
376
|
+
}
|
|
377
|
+
const id = await resolveAbstractId(cwd, flags, positional.slice(1))
|
|
378
|
+
const detail = await apiFetch(`/api/abstracts/${id}`)
|
|
379
|
+
const versionId =
|
|
380
|
+
flags.version ||
|
|
381
|
+
detail.abstract.live_version_id ||
|
|
382
|
+
detail.versions?.[detail.versions.length - 1]?.id
|
|
383
|
+
if (!versionId) throw new Error('Nothing published yet.')
|
|
384
|
+
const open = mode === 'anyone'
|
|
385
|
+
await apiFetch(`/api/abstracts/${id}/open`, {
|
|
386
|
+
method: 'POST',
|
|
387
|
+
body: JSON.stringify({ versionId, open }),
|
|
388
|
+
})
|
|
389
|
+
console.log(
|
|
390
|
+
open ? 'Anyone can view this version.' : 'Only people you invite can view it.',
|
|
391
|
+
)
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
async function cmdList(rest) {
|
|
395
|
+
const { flags } = parseFlags(rest)
|
|
396
|
+
if (flags.help) return printHelp('list')
|
|
397
|
+
const params = new URLSearchParams()
|
|
398
|
+
const view = flags.view ? String(flags.view) : 'all'
|
|
399
|
+
params.set('view', view)
|
|
400
|
+
const folder = folderToken(flags)
|
|
401
|
+
if (folder) params.set('collection', String(folder))
|
|
402
|
+
if (flags.tag) params.set('tag', String(flags.tag))
|
|
403
|
+
if (flags.search) params.set('search', String(flags.search))
|
|
404
|
+
if (flags.space) params.set('space', String(flags.space))
|
|
405
|
+
for (const key of ['starred', 'archived', 'owned', 'member', 'open', 'closed']) {
|
|
406
|
+
if (flags[key]) params.set(key, 'true')
|
|
407
|
+
}
|
|
408
|
+
const data = await apiFetch(`/api/abstracts?${params}`)
|
|
409
|
+
const rows = data.abstracts || []
|
|
410
|
+
if (jsonMode(flags)) {
|
|
411
|
+
printJson({ abstracts: rows.map(publicAbstract) })
|
|
412
|
+
return
|
|
413
|
+
}
|
|
414
|
+
if (!rows.length) {
|
|
415
|
+
console.log('Nothing here.')
|
|
416
|
+
return
|
|
417
|
+
}
|
|
418
|
+
for (const row of rows) console.log(listRow(row))
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
async function cmdStar(cwd, rest, starred) {
|
|
422
|
+
const { flags, positional } = parseFlags(rest)
|
|
423
|
+
if (flags.help) return printHelp(starred ? 'star' : 'unstar')
|
|
424
|
+
const id = await resolveAbstractId(cwd, flags, positional)
|
|
425
|
+
await apiFetch(`/api/abstracts/${id}/filing`, {
|
|
426
|
+
method: 'PATCH',
|
|
427
|
+
body: JSON.stringify({ starred }),
|
|
428
|
+
})
|
|
429
|
+
console.log(starred ? 'Starred.' : 'Unstarred.')
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
async function cmdTag(cwd, rest, add) {
|
|
433
|
+
const { flags, positional } = parseFlags(rest)
|
|
434
|
+
if (flags.help) return printHelp(add ? 'tag' : 'untag')
|
|
435
|
+
const tag = flags.tag || positional[1]
|
|
436
|
+
if (!tag || typeof tag !== 'string') throw new Error('Pass --tag <name>.')
|
|
437
|
+
const id = await resolveAbstractId(cwd, flags, positional)
|
|
438
|
+
const data = await apiFetch('/api/abstracts?view=all')
|
|
439
|
+
const row = (data.abstracts || []).find((a) => a.id === id)
|
|
440
|
+
const tags = new Set(row?.tags || [])
|
|
441
|
+
if (add) tags.add(tag.trim().toLowerCase())
|
|
442
|
+
else tags.delete(tag.trim().toLowerCase())
|
|
443
|
+
await apiFetch(`/api/abstracts/${id}/filing`, {
|
|
444
|
+
method: 'PATCH',
|
|
445
|
+
body: JSON.stringify({ tags: [...tags] }),
|
|
446
|
+
})
|
|
447
|
+
console.log(add ? 'Tagged.' : 'Untagged.')
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
async function cmdMove(cwd, rest) {
|
|
451
|
+
const { flags, positional } = parseFlags(rest)
|
|
452
|
+
if (flags.help) return printHelp('move')
|
|
453
|
+
const id = await resolveAbstractId(cwd, flags, positional)
|
|
454
|
+
let collection_id = null
|
|
455
|
+
const token = folderToken(flags)
|
|
456
|
+
if (flags.none) {
|
|
457
|
+
collection_id = null
|
|
458
|
+
} else if (token) {
|
|
459
|
+
collection_id = (await resolveFolder(token)).id
|
|
460
|
+
} else {
|
|
461
|
+
throw new Error('Pass --folder <name> or --none.')
|
|
462
|
+
}
|
|
463
|
+
await apiFetch(`/api/abstracts/${id}/filing`, {
|
|
464
|
+
method: 'PATCH',
|
|
465
|
+
body: JSON.stringify({ collection_id }),
|
|
466
|
+
})
|
|
467
|
+
console.log(collection_id ? 'Moved to folder.' : 'No folder.')
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
async function cmdArchive(cwd, rest, archived) {
|
|
471
|
+
const { flags, positional } = parseFlags(rest)
|
|
472
|
+
if (flags.help) return printHelp(archived ? 'archive' : 'restore')
|
|
473
|
+
const id = await resolveAbstractId(cwd, flags, positional)
|
|
474
|
+
await apiFetch(`/api/abstracts/${id}/filing`, {
|
|
475
|
+
method: 'PATCH',
|
|
476
|
+
body: JSON.stringify({ archived }),
|
|
477
|
+
})
|
|
478
|
+
console.log(archived ? 'Archived.' : 'Restored.')
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
async function cmdSuggestFolder(cwd, rest) {
|
|
482
|
+
const { flags, positional } = parseFlags(rest)
|
|
483
|
+
if (flags.help) return printHelp('suggest-folder')
|
|
484
|
+
const id = await resolveAbstractId(cwd, flags, positional)
|
|
485
|
+
const data = await apiFetch(`/api/abstracts/${id}/organise`, {
|
|
486
|
+
method: 'POST',
|
|
487
|
+
body: JSON.stringify({ apply: Boolean(flags.apply) }),
|
|
488
|
+
})
|
|
489
|
+
const folders = await loadFolders()
|
|
490
|
+
const folderId = data.suggestion?.collection_id ?? null
|
|
491
|
+
const folder = folders.find((f) => f.id === folderId)
|
|
492
|
+
const payload = {
|
|
493
|
+
folder: folder?.path ?? null,
|
|
494
|
+
folder_id: folderId,
|
|
495
|
+
reason: data.suggestion?.collection_reason ?? null,
|
|
496
|
+
tags: data.suggestion?.tags ?? [],
|
|
497
|
+
applied: Boolean(data.applied),
|
|
498
|
+
}
|
|
499
|
+
if (jsonMode(flags)) {
|
|
500
|
+
printJson(payload)
|
|
501
|
+
return
|
|
502
|
+
}
|
|
503
|
+
if (flags.apply && folder) {
|
|
504
|
+
console.log('Moved to that folder.')
|
|
505
|
+
return
|
|
506
|
+
}
|
|
507
|
+
if (!folder) {
|
|
508
|
+
console.log('No folder to suggest.')
|
|
509
|
+
return
|
|
510
|
+
}
|
|
511
|
+
console.log(`Folder: ${folder.path}`)
|
|
512
|
+
if (payload.reason) console.log(payload.reason)
|
|
513
|
+
if (payload.tags.length) console.log(`Tags: ${payload.tags.join(', ')}`)
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
async function cmdRename(cwd, rest) {
|
|
517
|
+
const { flags, positional } = parseFlags(rest)
|
|
518
|
+
if (flags.help) return printHelp('rename')
|
|
519
|
+
const name = typeof flags.name === 'string' ? flags.name.trim() : ''
|
|
520
|
+
if (!name) throw new Error('Pass --name <name>.')
|
|
521
|
+
const id = await resolveAbstractId(cwd, flags, positional)
|
|
522
|
+
await apiFetch(`/api/abstracts/${id}`, {
|
|
523
|
+
method: 'PATCH',
|
|
524
|
+
body: JSON.stringify({ name }),
|
|
525
|
+
})
|
|
526
|
+
console.log('Renamed.')
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
async function cmdFolderNew(rest) {
|
|
530
|
+
const { flags, positional } = parseFlags(rest)
|
|
531
|
+
const name = flags.name || positional[0]
|
|
532
|
+
if (!name) throw new Error('Pass --name <name>.')
|
|
533
|
+
const data = await apiFetch('/api/organise/collections', {
|
|
534
|
+
method: 'POST',
|
|
535
|
+
body: JSON.stringify({
|
|
536
|
+
name,
|
|
537
|
+
parent_id: flags.parent && flags.parent !== 'none' ? flags.parent : null,
|
|
538
|
+
}),
|
|
539
|
+
})
|
|
540
|
+
if (jsonMode(flags)) printJson({ id: data.id, name: data.name })
|
|
541
|
+
else console.log(data.id, data.name)
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
async function cmdFolderList(rest) {
|
|
545
|
+
const { flags } = parseFlags(rest)
|
|
546
|
+
const folders = await loadFolders()
|
|
547
|
+
if (jsonMode(flags)) {
|
|
548
|
+
printJson({
|
|
549
|
+
folders: folders.map((f) => ({
|
|
550
|
+
id: f.id,
|
|
551
|
+
name: f.name,
|
|
552
|
+
path: f.path,
|
|
553
|
+
parent_id: f.parent_id,
|
|
554
|
+
abstracts: f.abstract_count,
|
|
555
|
+
})),
|
|
556
|
+
})
|
|
557
|
+
return
|
|
558
|
+
}
|
|
559
|
+
if (!folders.length) {
|
|
560
|
+
console.log('No folders yet.')
|
|
561
|
+
return
|
|
562
|
+
}
|
|
563
|
+
for (const f of folders) {
|
|
564
|
+
console.log(`${f.path} · ${f.id}`)
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
async function cmdFolderMove(rest) {
|
|
569
|
+
const { flags, positional } = parseFlags(rest)
|
|
570
|
+
const token = flags.id || positional[0]
|
|
571
|
+
if (!token) throw new Error('Pass a folder.')
|
|
572
|
+
if (flags.parent === undefined) {
|
|
573
|
+
throw new Error('Pass --parent <id|none>.')
|
|
574
|
+
}
|
|
575
|
+
const folder = await resolveFolder(token)
|
|
576
|
+
const parent_id =
|
|
577
|
+
!flags.parent || flags.parent === 'none' || flags.parent === 'null'
|
|
578
|
+
? null
|
|
579
|
+
: (await resolveFolder(flags.parent)).id
|
|
580
|
+
await apiFetch(`/api/organise/collections/${folder.id}/move`, {
|
|
581
|
+
method: 'POST',
|
|
582
|
+
body: JSON.stringify({ parent_id }),
|
|
583
|
+
})
|
|
584
|
+
console.log('Moved folder.')
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
async function cmdFolderInvite(rest) {
|
|
588
|
+
const { flags, positional } = parseFlags(rest)
|
|
589
|
+
const token = positional[0]
|
|
590
|
+
const email = flags.email || positional[1]
|
|
591
|
+
if (!token || !email) throw new Error('Pass a folder and --email <email>.')
|
|
592
|
+
const folder = await resolveFolder(token)
|
|
593
|
+
await apiFetch(`/api/organise/collections/${folder.id}/invite`, {
|
|
594
|
+
method: 'POST',
|
|
595
|
+
body: JSON.stringify({ email }),
|
|
596
|
+
})
|
|
597
|
+
console.log('Invited.')
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
async function cmdFolderRemove(rest) {
|
|
601
|
+
const { flags, positional } = parseFlags(rest)
|
|
602
|
+
const token = positional[0]
|
|
603
|
+
const who = flags.email || positional[1]
|
|
604
|
+
if (!token || !who) throw new Error('Pass a folder and an email or id.')
|
|
605
|
+
const folder = await resolveFolder(token)
|
|
606
|
+
const body = /@/.test(who) ? { email: who } : { userId: who }
|
|
607
|
+
await apiFetch(`/api/organise/collections/${folder.id}/revoke`, {
|
|
608
|
+
method: 'POST',
|
|
609
|
+
body: JSON.stringify(body),
|
|
610
|
+
})
|
|
611
|
+
console.log('Removed.')
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
async function cmdFolderPeople(rest) {
|
|
615
|
+
const { flags, positional } = parseFlags(rest)
|
|
616
|
+
const token = positional[0]
|
|
617
|
+
if (!token) throw new Error('Pass a folder.')
|
|
618
|
+
const folder = await resolveFolder(token)
|
|
619
|
+
const share = await apiFetch(`/api/organise/collections/${folder.id}`)
|
|
620
|
+
const people = (share.people || []).map((m) => ({
|
|
621
|
+
id: m.user_id,
|
|
622
|
+
name: m.name,
|
|
623
|
+
access: accessLabel(m.role),
|
|
624
|
+
role: m.role,
|
|
625
|
+
}))
|
|
626
|
+
if (jsonMode(flags)) {
|
|
627
|
+
printJson({ folder: folder.path, people })
|
|
628
|
+
return
|
|
629
|
+
}
|
|
630
|
+
if (!people.length) {
|
|
631
|
+
console.log('No people yet.')
|
|
632
|
+
return
|
|
633
|
+
}
|
|
634
|
+
for (const p of people) {
|
|
635
|
+
console.log(`${p.name} · ${p.access} · ${p.id}`)
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
async function cmdFolder(rest) {
|
|
640
|
+
const { flags } = parseFlags(rest)
|
|
641
|
+
if (flags.help || !rest[0]) return printHelp('folder')
|
|
642
|
+
const [sub, ...tail] = rest
|
|
643
|
+
if (sub === 'new') return cmdFolderNew(tail)
|
|
644
|
+
if (sub === 'list') return cmdFolderList(tail)
|
|
645
|
+
if (sub === 'move') return cmdFolderMove(tail)
|
|
646
|
+
if (sub === 'invite') return cmdFolderInvite(tail)
|
|
647
|
+
if (sub === 'remove') return cmdFolderRemove(tail)
|
|
648
|
+
if (sub === 'people') return cmdFolderPeople(tail)
|
|
649
|
+
throw new Error('folder needs new, list, move, invite, remove, or people.')
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
async function cmdSpaceNew(rest) {
|
|
653
|
+
const { flags } = parseFlags(rest)
|
|
654
|
+
const name = flags.name || rest.find((a) => !a.startsWith('--'))
|
|
655
|
+
if (!name) throw new Error('Use folder new instead.')
|
|
656
|
+
const data = await apiFetch('/api/spaces', {
|
|
657
|
+
method: 'POST',
|
|
658
|
+
body: JSON.stringify({ name }),
|
|
659
|
+
})
|
|
660
|
+
console.log(data.id, data.name)
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
async function cmdSpaceMember(rest) {
|
|
664
|
+
const { flags, positional } = parseFlags(rest)
|
|
665
|
+
const spaceId = flags.space || positional[0]
|
|
666
|
+
const email = flags.email || positional[1]
|
|
667
|
+
if (!spaceId || !email) throw new Error('Use folder invite instead.')
|
|
668
|
+
await apiFetch(`/api/spaces/${spaceId}/members`, {
|
|
669
|
+
method: 'POST',
|
|
670
|
+
body: JSON.stringify({ email, role: flags.role || 'member' }),
|
|
671
|
+
})
|
|
672
|
+
console.log('Invited.')
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
export async function runCommand(argv, cwd = process.cwd()) {
|
|
676
|
+
if (!argv[0] || argv[0] === 'help' || argv[0] === '--help' || argv[0] === '-h') {
|
|
677
|
+
printHelp()
|
|
678
|
+
return
|
|
679
|
+
}
|
|
680
|
+
const [raw, ...rest] = argv
|
|
681
|
+
const cmd = ALIASES[raw] || raw
|
|
682
|
+
switch (cmd) {
|
|
683
|
+
case 'login':
|
|
684
|
+
return cmdLogin(rest)
|
|
685
|
+
case 'create':
|
|
686
|
+
return cmdCreate(cwd, rest)
|
|
687
|
+
case 'invite':
|
|
688
|
+
return cmdInvite(cwd, rest)
|
|
689
|
+
case 'remove':
|
|
690
|
+
return cmdRemove(cwd, rest)
|
|
691
|
+
case 'people':
|
|
692
|
+
return cmdPeople(cwd, rest)
|
|
693
|
+
case 'pull':
|
|
694
|
+
return cmdPull(cwd, rest)
|
|
695
|
+
case 'push':
|
|
696
|
+
return cmdPush(cwd, rest)
|
|
697
|
+
case 'open':
|
|
698
|
+
return cmdOpen(cwd, rest)
|
|
699
|
+
case 'preview':
|
|
700
|
+
return cmdPreview(cwd, rest)
|
|
701
|
+
case 'publish':
|
|
702
|
+
return cmdPublish(cwd, rest)
|
|
703
|
+
case 'live':
|
|
704
|
+
return cmdLive(cwd, rest)
|
|
705
|
+
case 'visibility':
|
|
706
|
+
return cmdVisibility(cwd, rest)
|
|
707
|
+
case 'list':
|
|
708
|
+
return cmdList(rest)
|
|
709
|
+
case 'star':
|
|
710
|
+
return cmdStar(cwd, rest, true)
|
|
711
|
+
case 'unstar':
|
|
712
|
+
return cmdStar(cwd, rest, false)
|
|
713
|
+
case 'tag':
|
|
714
|
+
return cmdTag(cwd, rest, true)
|
|
715
|
+
case 'untag':
|
|
716
|
+
return cmdTag(cwd, rest, false)
|
|
717
|
+
case 'move':
|
|
718
|
+
return cmdMove(cwd, rest)
|
|
719
|
+
case 'archive':
|
|
720
|
+
return cmdArchive(cwd, rest, true)
|
|
721
|
+
case 'restore':
|
|
722
|
+
return cmdArchive(cwd, rest, false)
|
|
723
|
+
case 'suggest-folder':
|
|
724
|
+
return cmdSuggestFolder(cwd, rest)
|
|
725
|
+
case 'folder':
|
|
726
|
+
return cmdFolder(rest)
|
|
727
|
+
case 'rename':
|
|
728
|
+
return cmdRename(cwd, rest)
|
|
729
|
+
case 'space':
|
|
730
|
+
if (rest[0] === 'new') return cmdSpaceNew(rest.slice(1))
|
|
731
|
+
if (rest[0] === 'member') return cmdSpaceMember(rest.slice(1))
|
|
732
|
+
throw new Error('Use folder instead.')
|
|
733
|
+
default:
|
|
734
|
+
throw new Error(`Unknown command “${raw}”. Run amorphous --help.`)
|
|
735
|
+
}
|
|
736
|
+
}
|