@ossy/cli 1.16.10 → 1.17.3
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/README.md +193 -51
- package/package.json +4 -3
- package/src/{cms → app}/cli.js +13 -7
- package/src/{publish/load-website-config.js → app/load-config.js} +6 -20
- package/src/app/publish.js +206 -0
- package/src/{cms → app}/upload-resource-templates.js +1 -1
- package/src/auth/cli.js +135 -0
- package/src/call/cli.js +311 -0
- package/src/file.js +99 -0
- package/src/index.js +76 -48
- package/src/registry/ecr-push-credentials.js +9 -8
- package/src/state.js +115 -0
- package/src/upload-dir/cli.js +270 -0
- package/src/workspace/cli.js +84 -0
- package/src/publish/cli.js +0 -180
- package/src/publish/resolve-config.js +0 -44
- package/src/publish/resource-templates-after-publish.js +0 -75
- package/src/publish/site-artifacts-after-publish.js +0 -210
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import path from 'path'
|
|
2
|
+
import { existsSync } from 'fs'
|
|
3
|
+
import arg from 'arg'
|
|
4
|
+
import * as Sdk from '@ossy/sdk'
|
|
5
|
+
import { logInfo, logError } from '../log.js'
|
|
6
|
+
import { resolveAppConfigPath } from '../resolve-app-config-path.js'
|
|
7
|
+
import { getAuth } from '../state.js'
|
|
8
|
+
import { walk } from '../upload-dir/cli.js'
|
|
9
|
+
import { prepareFileUpload, uploadFileToUrl } from '../file.js'
|
|
10
|
+
import {
|
|
11
|
+
postResourceTemplates,
|
|
12
|
+
requireAppAuthentication,
|
|
13
|
+
resolveApiBaseUrlForUpload,
|
|
14
|
+
} from './upload-resource-templates.js'
|
|
15
|
+
import { readAppConfig } from './load-config.js'
|
|
16
|
+
|
|
17
|
+
function normaliseLoc (loc) {
|
|
18
|
+
let s = loc.replace(/\/+$/, '')
|
|
19
|
+
if (!s.startsWith('/')) s = '/' + s
|
|
20
|
+
return s
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function publish (options) {
|
|
24
|
+
const parsedArgs = arg({
|
|
25
|
+
'--authentication': String,
|
|
26
|
+
'-a': '--authentication',
|
|
27
|
+
'--config': String,
|
|
28
|
+
'-c': '--config',
|
|
29
|
+
'--build-dir': String,
|
|
30
|
+
'--build-dest': String,
|
|
31
|
+
'--skip-resource-templates': Boolean,
|
|
32
|
+
'--api-url': String,
|
|
33
|
+
}, { argv: options })
|
|
34
|
+
|
|
35
|
+
const configFlag = parsedArgs['--config']
|
|
36
|
+
const configPath = resolveAppConfigPath(configFlag)
|
|
37
|
+
|
|
38
|
+
if (!configPath || !existsSync(configPath)) {
|
|
39
|
+
logError({
|
|
40
|
+
message: configFlag
|
|
41
|
+
? `[@ossy/cli] app publish: config not found: ${configFlag}`
|
|
42
|
+
: '[@ossy/cli] app publish: no config file. Pass --config (-c) or add src/config.js.',
|
|
43
|
+
})
|
|
44
|
+
process.exit(1)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const config = readAppConfig(configPath)
|
|
48
|
+
const { workspaceId, apiUrl: configApiUrl, domain, resourceTemplates } = config
|
|
49
|
+
|
|
50
|
+
if (!workspaceId) {
|
|
51
|
+
logError({ message: '[@ossy/cli] app publish: no workspaceId in config' })
|
|
52
|
+
process.exit(1)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const apiBaseUrl = resolveApiBaseUrlForUpload({
|
|
56
|
+
flag: parsedArgs['--api-url'],
|
|
57
|
+
envVar: process.env.OSSY_API_URL,
|
|
58
|
+
configApiUrl,
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
let authToken
|
|
62
|
+
try {
|
|
63
|
+
authToken = requireAppAuthentication(
|
|
64
|
+
getAuth({ flag: parsedArgs['--authentication'] }),
|
|
65
|
+
'publish'
|
|
66
|
+
)
|
|
67
|
+
} catch (e) {
|
|
68
|
+
logError({ message: e?.message || '[@ossy/cli] app publish: authentication required.' })
|
|
69
|
+
process.exit(1)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Step 1: upload resource templates
|
|
73
|
+
if (!parsedArgs['--skip-resource-templates']) {
|
|
74
|
+
if (Array.isArray(resourceTemplates) && resourceTemplates.length > 0) {
|
|
75
|
+
logInfo({ message: '[@ossy/cli] app publish: uploading resource templates…' })
|
|
76
|
+
const res = await postResourceTemplates({
|
|
77
|
+
apiBaseUrl,
|
|
78
|
+
token: authToken,
|
|
79
|
+
workspaceId,
|
|
80
|
+
resourceTemplates,
|
|
81
|
+
})
|
|
82
|
+
if (!res.ok) {
|
|
83
|
+
const text = await res.text().catch(() => '')
|
|
84
|
+
logError({
|
|
85
|
+
message: `[@ossy/cli] app publish: resource template upload failed: HTTP ${res.status}${text ? ` — ${text.slice(0, 200)}` : ''}`,
|
|
86
|
+
})
|
|
87
|
+
process.exit(1)
|
|
88
|
+
}
|
|
89
|
+
logInfo({ message: '[@ossy/cli] app publish: resource templates uploaded' })
|
|
90
|
+
} else {
|
|
91
|
+
logInfo({ message: '[@ossy/cli] app publish: no resource templates in config, skipping' })
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Step 2: upload build/ to CMS
|
|
96
|
+
const packageRoot = path.join(path.dirname(configPath), '..')
|
|
97
|
+
const buildDir = parsedArgs['--build-dir']
|
|
98
|
+
? path.resolve(parsedArgs['--build-dir'])
|
|
99
|
+
: path.join(packageRoot, 'build')
|
|
100
|
+
|
|
101
|
+
const remoteDest = parsedArgs['--build-dest']
|
|
102
|
+
? normaliseLoc(parsedArgs['--build-dest'])
|
|
103
|
+
: domain
|
|
104
|
+
? `/sites/${domain}`
|
|
105
|
+
: null
|
|
106
|
+
|
|
107
|
+
if (!remoteDest) {
|
|
108
|
+
logError({
|
|
109
|
+
message: '[@ossy/cli] app publish: no domain in config and no --build-dest specified',
|
|
110
|
+
})
|
|
111
|
+
process.exit(1)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (!existsSync(buildDir)) {
|
|
115
|
+
logError({
|
|
116
|
+
message: `[@ossy/cli] app publish: build directory not found: ${buildDir}. Run \`ossy app build\` first.`,
|
|
117
|
+
})
|
|
118
|
+
process.exit(1)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const sdk = Sdk.SDK.of({
|
|
122
|
+
apiUrl: apiBaseUrl,
|
|
123
|
+
workspaceId,
|
|
124
|
+
authorization: authToken,
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
const { dirs, files } = walk(buildDir)
|
|
128
|
+
logInfo({
|
|
129
|
+
message: `[@ossy/cli] app publish: uploading build (${files.length} file${files.length === 1 ? '' : 's'}) to ${remoteDest}…`,
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
const createDir = Sdk.ResourcesCreateDirectory
|
|
133
|
+
const uploadAction = Sdk.ResourcesUpload
|
|
134
|
+
|
|
135
|
+
let errors = 0
|
|
136
|
+
|
|
137
|
+
for (const d of dirs) {
|
|
138
|
+
const rel = d.slice(buildDir.length)
|
|
139
|
+
const parent = normaliseLoc(remoteDest + path.dirname(rel))
|
|
140
|
+
const name = path.basename(d)
|
|
141
|
+
try {
|
|
142
|
+
await sdk.makeRequest(createDir)({ location: parent, name })
|
|
143
|
+
} catch (err) {
|
|
144
|
+
const status = err && err.status
|
|
145
|
+
if (status !== 409 && status !== 422) {
|
|
146
|
+
logError({
|
|
147
|
+
message: `[@ossy/cli] app publish: mkdir failed for ${parent}/${name}: ${err?.message ?? err}`,
|
|
148
|
+
})
|
|
149
|
+
errors++
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
for (const f of files) {
|
|
155
|
+
const rel = f.slice(buildDir.length)
|
|
156
|
+
const remoteDir = normaliseLoc(remoteDest + path.dirname(rel))
|
|
157
|
+
|
|
158
|
+
let fileMeta
|
|
159
|
+
try {
|
|
160
|
+
fileMeta = prepareFileUpload(f)
|
|
161
|
+
} catch (err) {
|
|
162
|
+
logError({ message: `[@ossy/cli] app publish: cannot read file ${f}: ${err.message}` })
|
|
163
|
+
errors++
|
|
164
|
+
continue
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
let result
|
|
168
|
+
try {
|
|
169
|
+
result = await sdk.makeRequest(uploadAction)({
|
|
170
|
+
location: remoteDir,
|
|
171
|
+
name: fileMeta.name,
|
|
172
|
+
type: fileMeta.type,
|
|
173
|
+
size: fileMeta.size,
|
|
174
|
+
})
|
|
175
|
+
} catch (err) {
|
|
176
|
+
logError({ message: `[@ossy/cli] app publish: upload failed for ${f}: ${err?.message ?? err}` })
|
|
177
|
+
errors++
|
|
178
|
+
continue
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const uploadUrl = result?.content?.uploadUrl
|
|
182
|
+
if (!uploadUrl) {
|
|
183
|
+
logError({ message: `[@ossy/cli] app publish: no uploadUrl in response for ${f}` })
|
|
184
|
+
errors++
|
|
185
|
+
continue
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
try {
|
|
189
|
+
await uploadFileToUrl(fileMeta, uploadUrl)
|
|
190
|
+
} catch (err) {
|
|
191
|
+
logError({ message: `[@ossy/cli] app publish: S3 PUT failed for ${f}: ${err.message}` })
|
|
192
|
+
errors++
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (errors > 0) {
|
|
197
|
+
logError({
|
|
198
|
+
message: `[@ossy/cli] app publish: done with ${errors} error${errors === 1 ? '' : 's'}`,
|
|
199
|
+
})
|
|
200
|
+
process.exit(1)
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
logInfo({
|
|
204
|
+
message: `[@ossy/cli] app publish: done — ${files.length} file${files.length === 1 ? '' : 's'} uploaded to ${remoteDest}`,
|
|
205
|
+
})
|
|
206
|
+
}
|
|
@@ -14,7 +14,7 @@ export function normalizeAuthorizationToken (token) {
|
|
|
14
14
|
* @param {string} label e.g. "Resource template upload"
|
|
15
15
|
* @returns {string} normalized JWT
|
|
16
16
|
*/
|
|
17
|
-
export function
|
|
17
|
+
export function requireAppAuthentication (token, label) {
|
|
18
18
|
const normalized = normalizeAuthorizationToken(token)
|
|
19
19
|
if (!normalized) {
|
|
20
20
|
throw new Error(
|
package/src/auth/cli.js
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import readline from 'node:readline/promises'
|
|
2
|
+
import { stdin as input, stdout as output } from 'node:process'
|
|
3
|
+
import arg from 'arg'
|
|
4
|
+
import { logInfo, logError } from '../log.js'
|
|
5
|
+
import {
|
|
6
|
+
readCredentials,
|
|
7
|
+
writeCredentials,
|
|
8
|
+
deleteCredentials,
|
|
9
|
+
getAuth,
|
|
10
|
+
getApiUrl,
|
|
11
|
+
getWorkspaceId,
|
|
12
|
+
describeStatePaths,
|
|
13
|
+
} from '../state.js'
|
|
14
|
+
|
|
15
|
+
function stripBearer (t) {
|
|
16
|
+
return String(t ?? '').trim().replace(/^Bearer\s+/i, '').trim()
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async function promptHidden (question) {
|
|
20
|
+
// Tokens contain dots; avoid leaking partial input in shell history. Echo is left on
|
|
21
|
+
// because Node's tty masking is fiddly across platforms — paste-and-press-enter is
|
|
22
|
+
// the documented flow. We could revisit with a real prompt lib later.
|
|
23
|
+
const rl = readline.createInterface({ input, output })
|
|
24
|
+
try {
|
|
25
|
+
return (await rl.question(question)).trim()
|
|
26
|
+
} finally {
|
|
27
|
+
rl.close()
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function login (options) {
|
|
32
|
+
const parsed = arg({
|
|
33
|
+
'--token': String,
|
|
34
|
+
'-t': '--token',
|
|
35
|
+
'--api-url': String,
|
|
36
|
+
}, { argv: options })
|
|
37
|
+
|
|
38
|
+
let token = parsed['--token']
|
|
39
|
+
if (!token) {
|
|
40
|
+
if (!input.isTTY) {
|
|
41
|
+
logError({
|
|
42
|
+
message:
|
|
43
|
+
'[@ossy/cli] auth login: pass --token <jwt> or run interactively in a TTY.',
|
|
44
|
+
})
|
|
45
|
+
process.exit(1)
|
|
46
|
+
}
|
|
47
|
+
token = await promptHidden('Paste your Ossy API token: ')
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const normalized = stripBearer(token)
|
|
51
|
+
if (!normalized) {
|
|
52
|
+
logError({ message: '[@ossy/cli] auth login: empty token.' })
|
|
53
|
+
process.exit(1)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const apiUrl = getApiUrl({ flag: parsed['--api-url'] })
|
|
57
|
+
|
|
58
|
+
let me = null
|
|
59
|
+
try {
|
|
60
|
+
const res = await fetch(`${apiUrl}/users/me`, {
|
|
61
|
+
headers: { Authorization: normalized, 'Content-Type': 'application/json' },
|
|
62
|
+
})
|
|
63
|
+
if (!res.ok) {
|
|
64
|
+
logError({
|
|
65
|
+
message: `[@ossy/cli] auth login: token rejected by ${apiUrl}/users/me (HTTP ${res.status})`,
|
|
66
|
+
})
|
|
67
|
+
process.exit(1)
|
|
68
|
+
}
|
|
69
|
+
me = await res.json().catch(() => null)
|
|
70
|
+
} catch (error) {
|
|
71
|
+
logError({ message: `[@ossy/cli] auth login: could not reach ${apiUrl}`, error })
|
|
72
|
+
process.exit(1)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
writeCredentials({ apiUrl, token: normalized })
|
|
76
|
+
logInfo({
|
|
77
|
+
message: `[@ossy/cli] Logged in as ${me?.email || me?.id || 'user'} on ${apiUrl}`,
|
|
78
|
+
})
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function logout () {
|
|
82
|
+
deleteCredentials()
|
|
83
|
+
logInfo({ message: '[@ossy/cli] Logged out' })
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async function status () {
|
|
87
|
+
const creds = readCredentials()
|
|
88
|
+
const paths = describeStatePaths()
|
|
89
|
+
const apiUrl = getApiUrl()
|
|
90
|
+
const workspaceId = getWorkspaceId()
|
|
91
|
+
|
|
92
|
+
if (!creds?.token) {
|
|
93
|
+
console.log(
|
|
94
|
+
`Not logged in. Run \`ossy auth login\` to save credentials at ${paths.credentials}.`
|
|
95
|
+
)
|
|
96
|
+
return
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const auth = getAuth()
|
|
100
|
+
let userInfo = null
|
|
101
|
+
let live = false
|
|
102
|
+
try {
|
|
103
|
+
const res = await fetch(`${apiUrl}/users/me`, {
|
|
104
|
+
headers: { Authorization: auth, 'Content-Type': 'application/json' },
|
|
105
|
+
})
|
|
106
|
+
if (res.ok) {
|
|
107
|
+
userInfo = await res.json().catch(() => null)
|
|
108
|
+
live = true
|
|
109
|
+
}
|
|
110
|
+
} catch {
|
|
111
|
+
// network failure → just report offline status
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
console.log(`API URL: ${apiUrl}`)
|
|
115
|
+
console.log(
|
|
116
|
+
`User: ${live ? (userInfo?.email || userInfo?.id || 'unknown') : '(token rejected — try `ossy auth login`)'}`
|
|
117
|
+
)
|
|
118
|
+
console.log(
|
|
119
|
+
`Workspace: ${workspaceId || '(none — run `ossy workspace use <id>`)'}`
|
|
120
|
+
)
|
|
121
|
+
console.log(`Creds file: ${paths.credentials}`)
|
|
122
|
+
console.log(`Config: ${paths.config}`)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export async function handler (args) {
|
|
126
|
+
const [sub, ...rest] = args
|
|
127
|
+
if (sub === 'login') return await login(rest)
|
|
128
|
+
if (sub === 'logout') return logout()
|
|
129
|
+
if (sub === 'status') return await status()
|
|
130
|
+
logError({
|
|
131
|
+
message:
|
|
132
|
+
'[@ossy/cli] auth: unknown subcommand. Use: ossy auth login | logout | status',
|
|
133
|
+
})
|
|
134
|
+
process.exit(1)
|
|
135
|
+
}
|
package/src/call/cli.js
ADDED
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
import * as Sdk from '@ossy/sdk'
|
|
2
|
+
import { logError } from '../log.js'
|
|
3
|
+
import { getAuth, getApiUrl, getWorkspaceId } from '../state.js'
|
|
4
|
+
import { prepareFileUpload, uploadFileToUrl } from '../file.js'
|
|
5
|
+
|
|
6
|
+
const ACTIONS = Object.values(Sdk).filter(
|
|
7
|
+
(v) =>
|
|
8
|
+
v
|
|
9
|
+
&& typeof v === 'object'
|
|
10
|
+
&& typeof v.id === 'string'
|
|
11
|
+
&& typeof v.endpoint === 'string'
|
|
12
|
+
&& typeof v.method === 'string'
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
const ACTION_BY_ID = new Map(ACTIONS.map((a) => [a.id, a]))
|
|
16
|
+
|
|
17
|
+
function listActionsByDomain () {
|
|
18
|
+
const grouped = new Map()
|
|
19
|
+
for (const action of ACTIONS) {
|
|
20
|
+
const domain = action.id.split('.')[0]
|
|
21
|
+
if (!grouped.has(domain)) grouped.set(domain, [])
|
|
22
|
+
grouped.get(domain).push(action.id)
|
|
23
|
+
}
|
|
24
|
+
for (const [, ids] of grouped) ids.sort()
|
|
25
|
+
return new Map([...grouped.entries()].sort(([a], [b]) => a.localeCompare(b)))
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function buildHelp () {
|
|
29
|
+
const grouped = listActionsByDomain()
|
|
30
|
+
const blocks = []
|
|
31
|
+
for (const [domain, ids] of grouped) {
|
|
32
|
+
blocks.push(` ${domain}\n${ids.map((id) => ` ${id}`).join('\n')}`)
|
|
33
|
+
}
|
|
34
|
+
return `ossy call <action.id> [--key value]... [--json '{...}'] [options]
|
|
35
|
+
|
|
36
|
+
Generic dispatcher for any Ossy API action. Flags become payload fields
|
|
37
|
+
(--first-name -> firstName); --json '{...}' is merged on top.
|
|
38
|
+
|
|
39
|
+
Options:
|
|
40
|
+
--json '{...}' Provide payload as JSON (merged with --key value flags)
|
|
41
|
+
--file <path> Attach a local file: derives name/type/size as defaults
|
|
42
|
+
and PUTs the bytes if the response returns content.uploadUrl
|
|
43
|
+
-w, --workspace-id Override active workspace id (sent as workspaceId header)
|
|
44
|
+
-a, --authentication Override API token (otherwise resolved from state/env)
|
|
45
|
+
--api-url Override API base URL
|
|
46
|
+
--raw Print response body without JSON pretty-printing
|
|
47
|
+
|
|
48
|
+
Per-action help:
|
|
49
|
+
ossy call <action.id> --help
|
|
50
|
+
|
|
51
|
+
Available actions:
|
|
52
|
+
|
|
53
|
+
${blocks.join('\n\n')}
|
|
54
|
+
`
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function actionHelp (action) {
|
|
58
|
+
const def = action.payload ? `Default payload:\n ${JSON.stringify(action.payload)}\n\n` : ''
|
|
59
|
+
return `${action.id} ${action.method} ${action.endpoint}
|
|
60
|
+
|
|
61
|
+
${def}Examples:
|
|
62
|
+
ossy call ${action.id} --key value --other value
|
|
63
|
+
ossy call ${action.id} --json '{"key":"value"}'
|
|
64
|
+
`
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function kebabToCamel (str) {
|
|
68
|
+
return str.replace(/-([a-z])/g, (_, c) => c.toUpperCase())
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const RESERVED_FLAGS_NEEDING_VALUE = new Set([
|
|
72
|
+
'--json',
|
|
73
|
+
'--workspace-id', '-w',
|
|
74
|
+
'--authentication', '-a',
|
|
75
|
+
'--api-url',
|
|
76
|
+
'--file',
|
|
77
|
+
])
|
|
78
|
+
|
|
79
|
+
function parseCallArgs (argv) {
|
|
80
|
+
const out = {
|
|
81
|
+
actionId: null,
|
|
82
|
+
payload: {},
|
|
83
|
+
workspaceId: null,
|
|
84
|
+
authentication: null,
|
|
85
|
+
apiUrl: null,
|
|
86
|
+
raw: false,
|
|
87
|
+
json: null,
|
|
88
|
+
file: null,
|
|
89
|
+
explicitPayloadKeys: new Set(),
|
|
90
|
+
help: false,
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
let i = 0
|
|
94
|
+
while (i < argv.length) {
|
|
95
|
+
const arg = argv[i]
|
|
96
|
+
|
|
97
|
+
if (arg === '--help' || arg === '-h') {
|
|
98
|
+
out.help = true
|
|
99
|
+
i++
|
|
100
|
+
continue
|
|
101
|
+
}
|
|
102
|
+
if (arg === '--raw') {
|
|
103
|
+
out.raw = true
|
|
104
|
+
i++
|
|
105
|
+
continue
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// `--key=value` form, including reserved flags
|
|
109
|
+
if (arg.startsWith('--') && arg.includes('=')) {
|
|
110
|
+
const eq = arg.indexOf('=')
|
|
111
|
+
const key = arg.slice(2, eq)
|
|
112
|
+
const value = arg.slice(eq + 1)
|
|
113
|
+
if (key === 'json') out.json = value
|
|
114
|
+
else if (key === 'workspace-id') out.workspaceId = value
|
|
115
|
+
else if (key === 'authentication') out.authentication = value
|
|
116
|
+
else if (key === 'api-url') out.apiUrl = value
|
|
117
|
+
else if (key === 'file') out.file = value
|
|
118
|
+
else {
|
|
119
|
+
const camel = kebabToCamel(key)
|
|
120
|
+
out.payload[camel] = value
|
|
121
|
+
out.explicitPayloadKeys.add(camel)
|
|
122
|
+
}
|
|
123
|
+
i++
|
|
124
|
+
continue
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Reserved space-separated flags
|
|
128
|
+
if (RESERVED_FLAGS_NEEDING_VALUE.has(arg)) {
|
|
129
|
+
const value = argv[i + 1]
|
|
130
|
+
if (arg === '--json') out.json = value
|
|
131
|
+
else if (arg === '--workspace-id' || arg === '-w') out.workspaceId = value
|
|
132
|
+
else if (arg === '--authentication' || arg === '-a') out.authentication = value
|
|
133
|
+
else if (arg === '--api-url') out.apiUrl = value
|
|
134
|
+
else if (arg === '--file') out.file = value
|
|
135
|
+
i += 2
|
|
136
|
+
continue
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Action.id is the first non-flag positional
|
|
140
|
+
if (!arg.startsWith('-') && out.actionId === null) {
|
|
141
|
+
out.actionId = arg
|
|
142
|
+
i++
|
|
143
|
+
continue
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Generic --key (value | true)
|
|
147
|
+
if (arg.startsWith('--')) {
|
|
148
|
+
const key = arg.slice(2)
|
|
149
|
+
const camel = kebabToCamel(key)
|
|
150
|
+
const next = argv[i + 1]
|
|
151
|
+
if (next === undefined || next.startsWith('-')) {
|
|
152
|
+
out.payload[camel] = true
|
|
153
|
+
out.explicitPayloadKeys.add(camel)
|
|
154
|
+
i++
|
|
155
|
+
} else {
|
|
156
|
+
out.payload[camel] = next
|
|
157
|
+
out.explicitPayloadKeys.add(camel)
|
|
158
|
+
i += 2
|
|
159
|
+
}
|
|
160
|
+
continue
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Stray positional after the action id — ignore
|
|
164
|
+
i++
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (out.json) {
|
|
168
|
+
let parsed
|
|
169
|
+
try {
|
|
170
|
+
parsed = JSON.parse(out.json)
|
|
171
|
+
} catch (e) {
|
|
172
|
+
throw new Error(`--json: invalid JSON (${e.message})`)
|
|
173
|
+
}
|
|
174
|
+
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
175
|
+
throw new Error('--json: must be a JSON object')
|
|
176
|
+
}
|
|
177
|
+
for (const k of Object.keys(parsed)) out.explicitPayloadKeys.add(k)
|
|
178
|
+
Object.assign(out.payload, parsed)
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return out
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function isResponseLike (value) {
|
|
185
|
+
return (
|
|
186
|
+
value
|
|
187
|
+
&& typeof value === 'object'
|
|
188
|
+
&& typeof value.status === 'number'
|
|
189
|
+
&& typeof value.text === 'function'
|
|
190
|
+
&& typeof value.headers === 'object'
|
|
191
|
+
&& typeof value.headers.get === 'function'
|
|
192
|
+
)
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
async function call (args) {
|
|
196
|
+
let parsed
|
|
197
|
+
try {
|
|
198
|
+
parsed = parseCallArgs(args)
|
|
199
|
+
} catch (err) {
|
|
200
|
+
logError({ message: `[@ossy/cli] call: ${err.message}` })
|
|
201
|
+
process.exit(1)
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (parsed.help && !parsed.actionId) {
|
|
205
|
+
console.log(buildHelp())
|
|
206
|
+
return
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (!parsed.actionId) {
|
|
210
|
+
console.log(buildHelp())
|
|
211
|
+
return
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const action = ACTION_BY_ID.get(parsed.actionId)
|
|
215
|
+
if (!action) {
|
|
216
|
+
logError({
|
|
217
|
+
message: `[@ossy/cli] call: unknown action "${parsed.actionId}". Run \`ossy call --help\` for the list.`,
|
|
218
|
+
})
|
|
219
|
+
process.exit(1)
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (parsed.help) {
|
|
223
|
+
console.log(actionHelp(action))
|
|
224
|
+
return
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const auth = getAuth({ flag: parsed.authentication })
|
|
228
|
+
const apiUrl = getApiUrl({ flag: parsed.apiUrl })
|
|
229
|
+
const workspaceId = getWorkspaceId({ flag: parsed.workspaceId })
|
|
230
|
+
|
|
231
|
+
const sdk = Sdk.SDK.of({
|
|
232
|
+
apiUrl,
|
|
233
|
+
workspaceId: workspaceId || undefined,
|
|
234
|
+
authorization: auth || undefined,
|
|
235
|
+
})
|
|
236
|
+
|
|
237
|
+
let fileMeta = null
|
|
238
|
+
if (parsed.file) {
|
|
239
|
+
try {
|
|
240
|
+
fileMeta = prepareFileUpload(parsed.file)
|
|
241
|
+
} catch (err) {
|
|
242
|
+
logError({ message: `[@ossy/cli] call: ${err.message}` })
|
|
243
|
+
process.exit(1)
|
|
244
|
+
}
|
|
245
|
+
if (!parsed.explicitPayloadKeys.has('name')) parsed.payload.name = fileMeta.name
|
|
246
|
+
if (!parsed.explicitPayloadKeys.has('type')) parsed.payload.type = fileMeta.type
|
|
247
|
+
if (!parsed.explicitPayloadKeys.has('size')) parsed.payload.size = fileMeta.size
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
let result
|
|
251
|
+
try {
|
|
252
|
+
result = await sdk.makeRequest(action)(parsed.payload)
|
|
253
|
+
} catch (err) {
|
|
254
|
+
if (isResponseLike(err)) {
|
|
255
|
+
const text = await err.text().catch(() => '')
|
|
256
|
+
process.stderr.write(`HTTP ${err.status}${err.statusText ? ' ' + err.statusText : ''}\n`)
|
|
257
|
+
if (text) process.stderr.write(text + '\n')
|
|
258
|
+
process.exit(1)
|
|
259
|
+
}
|
|
260
|
+
if (typeof err === 'string') {
|
|
261
|
+
process.stderr.write(err + '\n')
|
|
262
|
+
process.exit(1)
|
|
263
|
+
}
|
|
264
|
+
if (err && err.message) {
|
|
265
|
+
process.stderr.write(`${err.message}\n`)
|
|
266
|
+
process.exit(1)
|
|
267
|
+
}
|
|
268
|
+
process.stderr.write(JSON.stringify(err) + '\n')
|
|
269
|
+
process.exit(1)
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (isResponseLike(result)) {
|
|
273
|
+
if (parsed.raw) {
|
|
274
|
+
const text = await result.text().catch(() => '')
|
|
275
|
+
if (text) process.stdout.write(text.endsWith('\n') ? text : text + '\n')
|
|
276
|
+
} else {
|
|
277
|
+
console.log(JSON.stringify({ status: result.status }, null, 2))
|
|
278
|
+
}
|
|
279
|
+
return
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if (fileMeta) {
|
|
283
|
+
const uploadUrl = result && result.content && result.content.uploadUrl
|
|
284
|
+
if (uploadUrl) {
|
|
285
|
+
try {
|
|
286
|
+
await uploadFileToUrl(fileMeta, uploadUrl)
|
|
287
|
+
} catch (err) {
|
|
288
|
+
process.stderr.write(`[@ossy/cli] call: upload PUT failed: ${err.message || err}\n`)
|
|
289
|
+
process.exit(1)
|
|
290
|
+
}
|
|
291
|
+
} else {
|
|
292
|
+
process.stderr.write(
|
|
293
|
+
'[@ossy/cli] call: --file was provided but the response did not include content.uploadUrl; nothing was uploaded.\n'
|
|
294
|
+
)
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
if (parsed.raw) {
|
|
299
|
+
process.stdout.write(
|
|
300
|
+
typeof result === 'string'
|
|
301
|
+
? (result.endsWith('\n') ? result : result + '\n')
|
|
302
|
+
: JSON.stringify(result) + '\n'
|
|
303
|
+
)
|
|
304
|
+
return
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
console.log(JSON.stringify(result, null, 2))
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
export const handler = call
|
|
311
|
+
export { parseCallArgs, ACTIONS, ACTION_BY_ID, listActionsByDomain, buildHelp, actionHelp }
|