@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,270 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import * as Sdk from '@ossy/sdk'
|
|
4
|
+
import { logInfo, logError } from '../log.js'
|
|
5
|
+
import { getAuth, getApiUrl, getWorkspaceId } from '../state.js'
|
|
6
|
+
import { prepareFileUpload, uploadFileToUrl } from '../file.js'
|
|
7
|
+
|
|
8
|
+
const HELP = `ossy upload-dir <localDir> <remoteLocation> [options]
|
|
9
|
+
|
|
10
|
+
Recursively mirror a local directory into the Ossy CMS.
|
|
11
|
+
Creates folders first (parents before children), then uploads every file.
|
|
12
|
+
Per-item errors are logged and skipped; exit code is non-zero if any failed.
|
|
13
|
+
|
|
14
|
+
Arguments:
|
|
15
|
+
<localDir> Path to the local directory to upload
|
|
16
|
+
<remoteLocation> Remote location to mirror into (e.g. /my-folder)
|
|
17
|
+
|
|
18
|
+
Options:
|
|
19
|
+
--dry-run Print what would happen without making any API calls
|
|
20
|
+
-a, --authentication Ossy API JWT (or set OSSY_API_KEY, or run \`ossy auth login\`)
|
|
21
|
+
-w, --workspace-id Workspace id (or set OSSY_WORKSPACE_ID, or run \`ossy workspace use\`)
|
|
22
|
+
--api-url Override API base URL
|
|
23
|
+
-h, --help Show this help
|
|
24
|
+
|
|
25
|
+
Example:
|
|
26
|
+
ossy upload-dir ./public /my-folder
|
|
27
|
+
ossy upload-dir ./public /my-folder --dry-run
|
|
28
|
+
`
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Walk a local directory tree.
|
|
32
|
+
* Returns dirs breadth-first (parents before children) and files in discovery order.
|
|
33
|
+
* @param {string} absRoot
|
|
34
|
+
* @returns {{ dirs: string[], files: string[] }}
|
|
35
|
+
*/
|
|
36
|
+
export function walk (absRoot) {
|
|
37
|
+
const dirs = []
|
|
38
|
+
const files = []
|
|
39
|
+
|
|
40
|
+
function recurse (dir) {
|
|
41
|
+
let entries
|
|
42
|
+
try {
|
|
43
|
+
entries = fs.readdirSync(dir, { withFileTypes: true })
|
|
44
|
+
} catch (err) {
|
|
45
|
+
throw new Error(`Cannot read directory ${dir}: ${err.message}`)
|
|
46
|
+
}
|
|
47
|
+
const sorted = [...entries].sort((a, b) => a.name.localeCompare(b.name))
|
|
48
|
+
const subdirs = []
|
|
49
|
+
for (const entry of sorted) {
|
|
50
|
+
const full = path.join(dir, entry.name)
|
|
51
|
+
if (entry.isDirectory()) {
|
|
52
|
+
dirs.push(full)
|
|
53
|
+
subdirs.push(full)
|
|
54
|
+
} else if (entry.isFile()) {
|
|
55
|
+
files.push(full)
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
for (const sub of subdirs) recurse(sub)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
recurse(absRoot)
|
|
62
|
+
return { dirs, files }
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Parse argv for the upload-dir command.
|
|
67
|
+
* @param {string[]} argv
|
|
68
|
+
* @returns {{ localDir: string|null, remoteLocation: string|null, dryRun: boolean, authentication: string|null, workspaceId: string|null, apiUrl: string|null, help: boolean }}
|
|
69
|
+
*/
|
|
70
|
+
export function parseArgs (argv) {
|
|
71
|
+
const out = {
|
|
72
|
+
localDir: null,
|
|
73
|
+
remoteLocation: null,
|
|
74
|
+
dryRun: false,
|
|
75
|
+
authentication: null,
|
|
76
|
+
workspaceId: null,
|
|
77
|
+
apiUrl: null,
|
|
78
|
+
help: false,
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const NEEDS_VALUE = new Set(['--authentication', '-a', '--workspace-id', '-w', '--api-url'])
|
|
82
|
+
|
|
83
|
+
let i = 0
|
|
84
|
+
while (i < argv.length) {
|
|
85
|
+
const arg = argv[i]
|
|
86
|
+
|
|
87
|
+
if (arg === '--help' || arg === '-h') { out.help = true; i++; continue }
|
|
88
|
+
if (arg === '--dry-run') { out.dryRun = true; i++; continue }
|
|
89
|
+
|
|
90
|
+
if (arg.startsWith('--') && arg.includes('=')) {
|
|
91
|
+
const eq = arg.indexOf('=')
|
|
92
|
+
const key = arg.slice(2, eq)
|
|
93
|
+
const val = arg.slice(eq + 1)
|
|
94
|
+
if (key === 'authentication') out.authentication = val
|
|
95
|
+
else if (key === 'workspace-id') out.workspaceId = val
|
|
96
|
+
else if (key === 'api-url') out.apiUrl = val
|
|
97
|
+
i++; continue
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (NEEDS_VALUE.has(arg)) {
|
|
101
|
+
const val = argv[i + 1]
|
|
102
|
+
if (arg === '--authentication' || arg === '-a') out.authentication = val
|
|
103
|
+
else if (arg === '--workspace-id' || arg === '-w') out.workspaceId = val
|
|
104
|
+
else if (arg === '--api-url') out.apiUrl = val
|
|
105
|
+
i += 2; continue
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (!arg.startsWith('-')) {
|
|
109
|
+
if (out.localDir === null) { out.localDir = arg; i++; continue }
|
|
110
|
+
if (out.remoteLocation === null) { out.remoteLocation = arg; i++; continue }
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
i++
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return out
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Normalise a remote location path: leading slash, no trailing slash.
|
|
121
|
+
* @param {string} loc
|
|
122
|
+
* @returns {string}
|
|
123
|
+
*/
|
|
124
|
+
function normaliseLoc (loc) {
|
|
125
|
+
let s = loc.replace(/\/+$/, '')
|
|
126
|
+
if (!s.startsWith('/')) s = '/' + s
|
|
127
|
+
return s
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export async function handler (args) {
|
|
131
|
+
const parsed = parseArgs(args)
|
|
132
|
+
|
|
133
|
+
if (parsed.help) {
|
|
134
|
+
console.log(HELP)
|
|
135
|
+
return
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (!parsed.localDir || !parsed.remoteLocation) {
|
|
139
|
+
logError({ message: '[@ossy/cli] upload-dir: provide <localDir> and <remoteLocation>. Run `ossy upload-dir --help`.' })
|
|
140
|
+
process.exit(1)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const absRoot = path.resolve(parsed.localDir)
|
|
144
|
+
if (!fs.existsSync(absRoot) || !fs.statSync(absRoot).isDirectory()) {
|
|
145
|
+
logError({ message: `[@ossy/cli] upload-dir: not a directory: ${parsed.localDir}` })
|
|
146
|
+
process.exit(1)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const remoteBase = normaliseLoc(parsed.remoteLocation)
|
|
150
|
+
|
|
151
|
+
let dirs, files
|
|
152
|
+
try {
|
|
153
|
+
;({ dirs, files } = walk(absRoot))
|
|
154
|
+
} catch (err) {
|
|
155
|
+
logError({ message: `[@ossy/cli] upload-dir: ${err.message}` })
|
|
156
|
+
process.exit(1)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const dryRun = parsed.dryRun
|
|
160
|
+
if (dryRun) {
|
|
161
|
+
console.log(`[dry-run] upload-dir ${absRoot} → ${remoteBase}`)
|
|
162
|
+
console.log(`[dry-run] ${dirs.length} director${dirs.length === 1 ? 'y' : 'ies'}, ${files.length} file${files.length === 1 ? '' : 's'}`)
|
|
163
|
+
for (const d of dirs) {
|
|
164
|
+
const rel = d.slice(absRoot.length)
|
|
165
|
+
const parent = normaliseLoc(remoteBase + path.dirname(rel))
|
|
166
|
+
const name = path.basename(d)
|
|
167
|
+
console.log(`[dry-run] mkdir ${parent}/${name}`)
|
|
168
|
+
}
|
|
169
|
+
for (const f of files) {
|
|
170
|
+
const rel = f.slice(absRoot.length)
|
|
171
|
+
const remoteDir = normaliseLoc(remoteBase + path.dirname(rel))
|
|
172
|
+
console.log(`[dry-run] upload ${f} → ${remoteDir}`)
|
|
173
|
+
}
|
|
174
|
+
return
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const auth = getAuth({ flag: parsed.authentication })
|
|
178
|
+
const apiUrl = getApiUrl({ flag: parsed.apiUrl })
|
|
179
|
+
const workspaceId = getWorkspaceId({ flag: parsed.workspaceId })
|
|
180
|
+
|
|
181
|
+
const sdk = Sdk.SDK.of({
|
|
182
|
+
apiUrl,
|
|
183
|
+
workspaceId: workspaceId || undefined,
|
|
184
|
+
authorization: auth || undefined,
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
const createDir = Sdk.ResourcesCreateDirectory
|
|
188
|
+
const uploadAction = Sdk.ResourcesUpload
|
|
189
|
+
|
|
190
|
+
let dirsMade = 0
|
|
191
|
+
let filesUploaded = 0
|
|
192
|
+
let errors = 0
|
|
193
|
+
|
|
194
|
+
// Create directories (breadth-first so parents always exist first)
|
|
195
|
+
for (const d of dirs) {
|
|
196
|
+
const rel = d.slice(absRoot.length)
|
|
197
|
+
const parent = normaliseLoc(remoteBase + path.dirname(rel))
|
|
198
|
+
const name = path.basename(d)
|
|
199
|
+
logInfo({ message: `[@ossy/cli] upload-dir: mkdir ${parent}/${name}` })
|
|
200
|
+
try {
|
|
201
|
+
await sdk.makeRequest(createDir)({ location: parent, name })
|
|
202
|
+
dirsMade++
|
|
203
|
+
} catch (err) {
|
|
204
|
+
// Treat "already exists" (HTTP 409 / 422 / resource conflict) as success
|
|
205
|
+
const status = err && err.status
|
|
206
|
+
if (status === 409 || status === 422) {
|
|
207
|
+
logInfo({ message: `[@ossy/cli] upload-dir: dir already exists, skipping: ${parent}/${name}` })
|
|
208
|
+
dirsMade++
|
|
209
|
+
} else {
|
|
210
|
+
const msg = err && err.message ? err.message : String(err)
|
|
211
|
+
logError({ message: `[@ossy/cli] upload-dir: mkdir failed for ${parent}/${name}: ${msg}` })
|
|
212
|
+
errors++
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Upload files
|
|
218
|
+
for (const f of files) {
|
|
219
|
+
const rel = f.slice(absRoot.length)
|
|
220
|
+
const remoteDir = normaliseLoc(remoteBase + path.dirname(rel))
|
|
221
|
+
logInfo({ message: `[@ossy/cli] upload-dir: upload ${f} → ${remoteDir}` })
|
|
222
|
+
|
|
223
|
+
let fileMeta
|
|
224
|
+
try {
|
|
225
|
+
fileMeta = prepareFileUpload(f)
|
|
226
|
+
} catch (err) {
|
|
227
|
+
logError({ message: `[@ossy/cli] upload-dir: cannot read file ${f}: ${err.message}` })
|
|
228
|
+
errors++
|
|
229
|
+
continue
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
let result
|
|
233
|
+
try {
|
|
234
|
+
result = await sdk.makeRequest(uploadAction)({
|
|
235
|
+
location: remoteDir,
|
|
236
|
+
name: fileMeta.name,
|
|
237
|
+
type: fileMeta.type,
|
|
238
|
+
size: fileMeta.size,
|
|
239
|
+
})
|
|
240
|
+
} catch (err) {
|
|
241
|
+
const msg = err && err.message ? err.message : String(err)
|
|
242
|
+
logError({ message: `[@ossy/cli] upload-dir: upload failed for ${f}: ${msg}` })
|
|
243
|
+
errors++
|
|
244
|
+
continue
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const uploadUrl = result && result.content && result.content.uploadUrl
|
|
248
|
+
if (!uploadUrl) {
|
|
249
|
+
logError({ message: `[@ossy/cli] upload-dir: no uploadUrl in response for ${f}` })
|
|
250
|
+
errors++
|
|
251
|
+
continue
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
try {
|
|
255
|
+
await uploadFileToUrl(fileMeta, uploadUrl)
|
|
256
|
+
filesUploaded++
|
|
257
|
+
} catch (err) {
|
|
258
|
+
logError({ message: `[@ossy/cli] upload-dir: S3 PUT failed for ${f}: ${err.message}` })
|
|
259
|
+
errors++
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const summary = `${dirsMade} director${dirsMade === 1 ? 'y' : 'ies'} created, ${filesUploaded} file${filesUploaded === 1 ? '' : 's'} uploaded`
|
|
264
|
+
if (errors > 0) {
|
|
265
|
+
logError({ message: `[@ossy/cli] upload-dir: done with ${errors} error${errors === 1 ? '' : 's'} — ${summary}` })
|
|
266
|
+
process.exit(1)
|
|
267
|
+
} else {
|
|
268
|
+
logInfo({ message: `[@ossy/cli] upload-dir: done — ${summary}` })
|
|
269
|
+
}
|
|
270
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { logInfo, logError } from '../log.js'
|
|
2
|
+
import {
|
|
3
|
+
readConfig,
|
|
4
|
+
writeConfig,
|
|
5
|
+
getAuth,
|
|
6
|
+
getApiUrl,
|
|
7
|
+
getWorkspaceId,
|
|
8
|
+
} from '../state.js'
|
|
9
|
+
|
|
10
|
+
function ensureAuth () {
|
|
11
|
+
const auth = getAuth()
|
|
12
|
+
if (!auth) {
|
|
13
|
+
logError({
|
|
14
|
+
message: '[@ossy/cli] Not logged in. Run `ossy auth login` first.',
|
|
15
|
+
})
|
|
16
|
+
process.exit(1)
|
|
17
|
+
}
|
|
18
|
+
return auth
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function use (options) {
|
|
22
|
+
const id = options[0]
|
|
23
|
+
if (!id) {
|
|
24
|
+
logError({ message: '[@ossy/cli] workspace use: pass a workspace id.' })
|
|
25
|
+
process.exit(1)
|
|
26
|
+
}
|
|
27
|
+
writeConfig({ ...(readConfig() || {}), workspaceId: id })
|
|
28
|
+
logInfo({ message: `[@ossy/cli] Active workspace: ${id}` })
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function list () {
|
|
32
|
+
const auth = ensureAuth()
|
|
33
|
+
const apiUrl = getApiUrl()
|
|
34
|
+
|
|
35
|
+
let res
|
|
36
|
+
try {
|
|
37
|
+
res = await fetch(`${apiUrl}/workspaces`, {
|
|
38
|
+
headers: { Authorization: auth, 'Content-Type': 'application/json' },
|
|
39
|
+
})
|
|
40
|
+
} catch (error) {
|
|
41
|
+
logError({ message: `[@ossy/cli] workspace list: could not reach ${apiUrl}`, error })
|
|
42
|
+
process.exit(1)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (!res.ok) {
|
|
46
|
+
logError({
|
|
47
|
+
message: `[@ossy/cli] workspace list: HTTP ${res.status} from ${apiUrl}/workspaces`,
|
|
48
|
+
})
|
|
49
|
+
process.exit(1)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const workspaces = await res.json().catch(() => null)
|
|
53
|
+
if (!Array.isArray(workspaces) || workspaces.length === 0) {
|
|
54
|
+
console.log('No workspaces found.')
|
|
55
|
+
return
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const active = getWorkspaceId()
|
|
59
|
+
for (const w of workspaces) {
|
|
60
|
+
const marker = w.id === active ? '* ' : ' '
|
|
61
|
+
console.log(`${marker}${w.id}\t${w.name || ''}`)
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function current () {
|
|
66
|
+
const id = getWorkspaceId()
|
|
67
|
+
if (!id) {
|
|
68
|
+
console.log('(no active workspace)')
|
|
69
|
+
return
|
|
70
|
+
}
|
|
71
|
+
console.log(id)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export async function handler (args) {
|
|
75
|
+
const [sub, ...rest] = args
|
|
76
|
+
if (sub === 'use') return use(rest)
|
|
77
|
+
if (sub === 'list') return await list()
|
|
78
|
+
if (sub === 'current') return current()
|
|
79
|
+
logError({
|
|
80
|
+
message:
|
|
81
|
+
'[@ossy/cli] workspace: unknown subcommand. Use: ossy workspace use <id> | list | current',
|
|
82
|
+
})
|
|
83
|
+
process.exit(1)
|
|
84
|
+
}
|
package/src/publish/cli.js
DELETED
|
@@ -1,180 +0,0 @@
|
|
|
1
|
-
import { existsSync } from 'fs'
|
|
2
|
-
import { spawn } from 'child_process'
|
|
3
|
-
import arg from 'arg'
|
|
4
|
-
import { logInfo, logError } from '../log.js'
|
|
5
|
-
import { resolveAppConfigPath } from '../resolve-app-config-path.js'
|
|
6
|
-
import {
|
|
7
|
-
readWebsiteConfigDeployFields,
|
|
8
|
-
resolvePlatformFromDeployments
|
|
9
|
-
} from './resolve-config.js'
|
|
10
|
-
import { maybeUploadResourceTemplatesAfterPublish } from './resource-templates-after-publish.js'
|
|
11
|
-
import { maybeUploadSiteArtifactsAfterPublish } from './site-artifacts-after-publish.js'
|
|
12
|
-
import { requireCmsAuthentication } from '../cms/upload-resource-templates.js'
|
|
13
|
-
|
|
14
|
-
const DEPLOYMENT_TOOLS = '@ossy/deployment-tools'
|
|
15
|
-
|
|
16
|
-
function runNpxDeploymentTools (deploymentArgs) {
|
|
17
|
-
const args = ['--yes', DEPLOYMENT_TOOLS, 'deployment', ...deploymentArgs]
|
|
18
|
-
logInfo({ message: `[@ossy/cli] publish: npx ${DEPLOYMENT_TOOLS} deployment ${deploymentArgs[0]} …` })
|
|
19
|
-
return new Promise((resolvePromise, reject) => {
|
|
20
|
-
const npx = process.platform === 'win32' ? 'npx.cmd' : 'npx'
|
|
21
|
-
const child = spawn(npx, args, { stdio: 'inherit' })
|
|
22
|
-
child.on('error', reject)
|
|
23
|
-
child.on('close', (code) => {
|
|
24
|
-
if (code === 0) resolvePromise()
|
|
25
|
-
else reject(new Error(`npx exited with code ${code}`))
|
|
26
|
-
})
|
|
27
|
-
})
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Publish container deployments: default = one site (`deployment deploy`);
|
|
32
|
-
* `--all` = `deployment deploy-all` for the platform.
|
|
33
|
-
*
|
|
34
|
-
* Requires **`--authentication`** (Ossy API JWT) or **`OSSY_API_KEY`**. Container
|
|
35
|
-
* deployments target **ECR** (`registry` in deployments JSON); the worker pulls
|
|
36
|
-
* with **IAM** (`aws ecr get-login-password`). No registry password is sent on the queue.
|
|
37
|
-
*/
|
|
38
|
-
export async function publish (options) {
|
|
39
|
-
const parsedArgs = arg({
|
|
40
|
-
'--authentication': String,
|
|
41
|
-
'-a': '--authentication',
|
|
42
|
-
'--domain': String,
|
|
43
|
-
'-d': '--domain',
|
|
44
|
-
'--platform': String,
|
|
45
|
-
'-p': '--platform',
|
|
46
|
-
'--config': String,
|
|
47
|
-
'-c': '--config',
|
|
48
|
-
'--platforms-path': String,
|
|
49
|
-
'-pp': '--platforms-path',
|
|
50
|
-
'--deployments-path': String,
|
|
51
|
-
'-dp': '--deployments-path',
|
|
52
|
-
'--all': Boolean,
|
|
53
|
-
'--skip-resource-templates': Boolean,
|
|
54
|
-
'--skip-site-artifacts': Boolean,
|
|
55
|
-
'--site-artifacts-build-dir': String,
|
|
56
|
-
'--api-url': String,
|
|
57
|
-
}, { argv: options })
|
|
58
|
-
|
|
59
|
-
const platformsPath = parsedArgs['--platforms-path']
|
|
60
|
-
const deploymentsPath = parsedArgs['--deployments-path']
|
|
61
|
-
|
|
62
|
-
const apiUrlForTemplates = parsedArgs['--api-url']
|
|
63
|
-
|
|
64
|
-
let apiToken
|
|
65
|
-
try {
|
|
66
|
-
apiToken = requireCmsAuthentication(
|
|
67
|
-
parsedArgs['--authentication'] || process.env.OSSY_API_KEY,
|
|
68
|
-
'publish'
|
|
69
|
-
)
|
|
70
|
-
} catch (e) {
|
|
71
|
-
logError({
|
|
72
|
-
message:
|
|
73
|
-
e?.message
|
|
74
|
-
|| '[@ossy/cli] publish: pass --authentication (-a) or set OSSY_API_KEY (Ossy API JWT).',
|
|
75
|
-
})
|
|
76
|
-
process.exit(1)
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
if (!platformsPath || !deploymentsPath) {
|
|
80
|
-
logError({
|
|
81
|
-
message: '[@ossy/cli] publish: --platforms-path (-pp) and --deployments-path (-dp) are required.'
|
|
82
|
-
})
|
|
83
|
-
process.exit(1)
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
const configFlag = parsedArgs['--config']
|
|
87
|
-
let configPath = resolveAppConfigPath(configFlag)
|
|
88
|
-
if (configFlag) {
|
|
89
|
-
if (!configPath || !existsSync(configPath)) {
|
|
90
|
-
logError({ message: `[@ossy/cli] publish: --config file not found: ${configFlag}` })
|
|
91
|
-
process.exit(1)
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
const skipResourceTemplates = parsedArgs['--skip-resource-templates']
|
|
96
|
-
const skipSiteArtifacts = parsedArgs['--skip-site-artifacts']
|
|
97
|
-
const siteArtifactsBuildDir = parsedArgs['--site-artifacts-build-dir']
|
|
98
|
-
|
|
99
|
-
const fromConfig = configPath ? readWebsiteConfigDeployFields(configPath) : {}
|
|
100
|
-
|
|
101
|
-
if (parsedArgs['--all']) {
|
|
102
|
-
let targetPlatform = parsedArgs['--platform'] || fromConfig.platform
|
|
103
|
-
if (!targetPlatform) {
|
|
104
|
-
logError({
|
|
105
|
-
message: '[@ossy/cli] publish --all: pass --platform (-p) or set platform in src/config.js (or --config).'
|
|
106
|
-
})
|
|
107
|
-
process.exit(1)
|
|
108
|
-
}
|
|
109
|
-
logInfo({ message: `[@ossy/cli] publish --all: platform=${targetPlatform}` })
|
|
110
|
-
await runNpxDeploymentTools([
|
|
111
|
-
'deploy-all',
|
|
112
|
-
'-p', targetPlatform,
|
|
113
|
-
'--platforms-path', platformsPath,
|
|
114
|
-
'--deployments-path', deploymentsPath,
|
|
115
|
-
])
|
|
116
|
-
if (!skipResourceTemplates && configPath) {
|
|
117
|
-
await maybeUploadResourceTemplatesAfterPublish({
|
|
118
|
-
configPath,
|
|
119
|
-
cmsToken: apiToken,
|
|
120
|
-
apiUrlFlag: apiUrlForTemplates,
|
|
121
|
-
})
|
|
122
|
-
}
|
|
123
|
-
if (!skipSiteArtifacts && configPath) {
|
|
124
|
-
await maybeUploadSiteArtifactsAfterPublish({
|
|
125
|
-
configPath,
|
|
126
|
-
cmsToken: apiToken,
|
|
127
|
-
apiUrlFlag: apiUrlForTemplates,
|
|
128
|
-
buildDir: siteArtifactsBuildDir,
|
|
129
|
-
})
|
|
130
|
-
}
|
|
131
|
-
return
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
let targetDomain = parsedArgs['--domain'] || fromConfig.domain
|
|
135
|
-
let targetPlatform = parsedArgs['--platform'] || fromConfig.platform
|
|
136
|
-
|
|
137
|
-
if (!targetPlatform && targetDomain && deploymentsPath) {
|
|
138
|
-
const resolved = resolvePlatformFromDeployments(deploymentsPath, targetDomain)
|
|
139
|
-
if (resolved && typeof resolved === 'object' && resolved.ambiguous) {
|
|
140
|
-
logError({
|
|
141
|
-
message: `[@ossy/cli] publish: domain "${targetDomain}" matches multiple platforms (${resolved.platforms.join(', ')}). Set platform in src/config.js or pass --platform (-p).`
|
|
142
|
-
})
|
|
143
|
-
process.exit(1)
|
|
144
|
-
}
|
|
145
|
-
targetPlatform = resolved
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
if (!targetDomain || !targetPlatform) {
|
|
149
|
-
logError({
|
|
150
|
-
message: '[@ossy/cli] publish: need --domain (-d) and --platform (-p), or src/config.js / --config with domain (and optional platform); platform can be inferred from deployments when domain is unique.'
|
|
151
|
-
})
|
|
152
|
-
process.exit(1)
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
logInfo({ message: `[@ossy/cli] publish: domain=${targetDomain} platform=${targetPlatform}` })
|
|
156
|
-
|
|
157
|
-
await runNpxDeploymentTools([
|
|
158
|
-
'deploy',
|
|
159
|
-
'-d', targetDomain,
|
|
160
|
-
'-p', targetPlatform,
|
|
161
|
-
'--platforms-path', platformsPath,
|
|
162
|
-
'--deployments-path', deploymentsPath,
|
|
163
|
-
])
|
|
164
|
-
|
|
165
|
-
if (!skipResourceTemplates && configPath) {
|
|
166
|
-
await maybeUploadResourceTemplatesAfterPublish({
|
|
167
|
-
configPath,
|
|
168
|
-
cmsToken: apiToken,
|
|
169
|
-
apiUrlFlag: apiUrlForTemplates,
|
|
170
|
-
})
|
|
171
|
-
}
|
|
172
|
-
if (!skipSiteArtifacts && configPath) {
|
|
173
|
-
await maybeUploadSiteArtifactsAfterPublish({
|
|
174
|
-
configPath,
|
|
175
|
-
cmsToken: apiToken,
|
|
176
|
-
apiUrlFlag: apiUrlForTemplates,
|
|
177
|
-
buildDir: siteArtifactsBuildDir,
|
|
178
|
-
})
|
|
179
|
-
}
|
|
180
|
-
}
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
import { readFileSync } from 'fs'
|
|
2
|
-
import { resolve } from 'path'
|
|
3
|
-
import { globSync } from 'glob'
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Reads string `domain` / `platform` from a site `src/config.js` without executing it
|
|
7
|
-
* (config is often ESM with package-only imports).
|
|
8
|
-
*
|
|
9
|
-
* @param {string} configPath
|
|
10
|
-
* @returns {{ domain?: string, platform?: string }}
|
|
11
|
-
*/
|
|
12
|
-
export function readWebsiteConfigDeployFields (configPath) {
|
|
13
|
-
const abs = resolve(configPath)
|
|
14
|
-
const source = readFileSync(abs, 'utf8')
|
|
15
|
-
|
|
16
|
-
const pick = (key) => {
|
|
17
|
-
const m = source.match(new RegExp(`${key}\\s*:\\s*['"]([^'"]+)['"]`))
|
|
18
|
-
return m ? m[1] : undefined
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
const domain = pick('domain')
|
|
22
|
-
const platform = pick('platform') || pick('targetDeploymentPlatform')
|
|
23
|
-
|
|
24
|
-
return { domain, platform }
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* @param {string} deploymentsGlob
|
|
29
|
-
* @param {string} domain
|
|
30
|
-
* @returns {string | undefined | { ambiguous: true, platforms: string[] }}
|
|
31
|
-
*/
|
|
32
|
-
export function resolvePlatformFromDeployments (deploymentsGlob, domain) {
|
|
33
|
-
const filePaths = globSync(deploymentsGlob, { ignore: 'node_modules/**' })
|
|
34
|
-
const deployments = filePaths.flatMap((p) => JSON.parse(readFileSync(p, 'utf8')))
|
|
35
|
-
const matches = deployments.filter((d) => d.domain === domain)
|
|
36
|
-
if (matches.length === 0) {
|
|
37
|
-
return undefined
|
|
38
|
-
}
|
|
39
|
-
const platforms = [...new Set(matches.map((d) => d.targetDeploymentPlatform))]
|
|
40
|
-
if (platforms.length > 1) {
|
|
41
|
-
return { ambiguous: true, platforms }
|
|
42
|
-
}
|
|
43
|
-
return platforms[0]
|
|
44
|
-
}
|
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
import { logInfo } from '../log.js'
|
|
2
|
-
import { readPublishFieldsFromWebsiteConfig } from './load-website-config.js'
|
|
3
|
-
import {
|
|
4
|
-
postResourceTemplates,
|
|
5
|
-
requireCmsAuthentication,
|
|
6
|
-
resolveApiBaseUrlForUpload,
|
|
7
|
-
} from '../cms/upload-resource-templates.js'
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* After a successful deployment, sync `resourceTemplates` from app config to the workspace API.
|
|
11
|
-
*/
|
|
12
|
-
export async function maybeUploadResourceTemplatesAfterPublish ({
|
|
13
|
-
configPath,
|
|
14
|
-
cmsToken,
|
|
15
|
-
apiUrlFlag,
|
|
16
|
-
}) {
|
|
17
|
-
const config = readPublishFieldsFromWebsiteConfig(configPath)
|
|
18
|
-
const workspaceId = config.workspaceId
|
|
19
|
-
const resourceTemplates = config.resourceTemplates
|
|
20
|
-
|
|
21
|
-
if (!workspaceId) {
|
|
22
|
-
logInfo({
|
|
23
|
-
message:
|
|
24
|
-
'[@ossy/cli] publish: skipping resource template upload (no workspaceId in config)',
|
|
25
|
-
})
|
|
26
|
-
return
|
|
27
|
-
}
|
|
28
|
-
if (!Array.isArray(resourceTemplates) || resourceTemplates.length === 0) {
|
|
29
|
-
logInfo({
|
|
30
|
-
message:
|
|
31
|
-
'[@ossy/cli] publish: skipping resource template upload (resourceTemplates missing or empty)',
|
|
32
|
-
})
|
|
33
|
-
return
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
const apiBaseUrl = resolveApiBaseUrlForUpload({
|
|
37
|
-
flag: apiUrlFlag,
|
|
38
|
-
envVar: process.env.OSSY_API_URL,
|
|
39
|
-
configApiUrl: config?.apiUrl,
|
|
40
|
-
})
|
|
41
|
-
const uploadUrl = `${apiBaseUrl.replace(/\/$/, '')}/resource-templates`
|
|
42
|
-
const authToken = requireCmsAuthentication(
|
|
43
|
-
cmsToken,
|
|
44
|
-
'Resource template upload'
|
|
45
|
-
)
|
|
46
|
-
|
|
47
|
-
logInfo({
|
|
48
|
-
message: `[@ossy/cli] publish: uploading resource templates → POST ${uploadUrl}`,
|
|
49
|
-
})
|
|
50
|
-
|
|
51
|
-
const response = await postResourceTemplates({
|
|
52
|
-
apiBaseUrl,
|
|
53
|
-
token: authToken,
|
|
54
|
-
workspaceId,
|
|
55
|
-
resourceTemplates,
|
|
56
|
-
})
|
|
57
|
-
|
|
58
|
-
if (!response.ok) {
|
|
59
|
-
const text = await response.text().catch(() => '')
|
|
60
|
-
const hint404 =
|
|
61
|
-
response.status === 404
|
|
62
|
-
? ` Wrong host or path: requested ${uploadUrl}. Confirm OSSY_API_URL / --api-url points at the Ossy API base including /api/v0.`
|
|
63
|
-
: ''
|
|
64
|
-
const hint401 =
|
|
65
|
-
response.status === 401
|
|
66
|
-
? ' Use an Ossy API JWT with --authentication (or OSSY_API_KEY in CI), not a non-API deploy token.'
|
|
67
|
-
: ''
|
|
68
|
-
throw new Error(
|
|
69
|
-
`Resource template upload failed: HTTP ${response.status}${hint404}${hint401}${
|
|
70
|
-
text ? ` — ${text.slice(0, 200)}` : ''
|
|
71
|
-
}`
|
|
72
|
-
)
|
|
73
|
-
}
|
|
74
|
-
logInfo({ message: '[@ossy/cli] publish: resource templates uploaded' })
|
|
75
|
-
}
|