@ossy/cli 1.16.11 → 1.17.4
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 +97 -53
- package/package.json +4 -4
- package/src/{cms → app}/cli.js +10 -4
- package/src/{publish/load-website-config.js → app/load-config.js} +6 -20
- package/src/app/publish.js +245 -0
- package/src/{cms → app}/upload-resource-templates.js +1 -1
- package/src/call/cli.js +2 -18
- package/src/file.js +26 -0
- package/src/index.js +35 -56
- package/src/registry/ecr-push-credentials.js +2 -2
- package/src/upload-dir/cli.js +270 -0
- package/src/publish/cli.js +0 -181
- 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 -183
package/src/file.js
CHANGED
|
@@ -41,6 +41,32 @@ export function guessContentType (filePath) {
|
|
|
41
41
|
return MIME_BY_EXT[ext] || 'application/octet-stream'
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
/**
|
|
45
|
+
* PUT file bytes to a presigned S3 upload URL.
|
|
46
|
+
* Throws on network error or non-2xx response.
|
|
47
|
+
* @param {{ absPath: string, size: number, type: string }} fileMeta
|
|
48
|
+
* @param {string} uploadUrl
|
|
49
|
+
* @returns {Promise<void>}
|
|
50
|
+
*/
|
|
51
|
+
export async function uploadFileToUrl (fileMeta, uploadUrl) {
|
|
52
|
+
const { readFile } = await import('node:fs/promises')
|
|
53
|
+
const body = await readFile(fileMeta.absPath)
|
|
54
|
+
const res = await fetch(uploadUrl, {
|
|
55
|
+
method: 'PUT',
|
|
56
|
+
headers: {
|
|
57
|
+
'Content-Type': fileMeta.type,
|
|
58
|
+
'Content-Length': String(fileMeta.size),
|
|
59
|
+
},
|
|
60
|
+
body,
|
|
61
|
+
})
|
|
62
|
+
if (!res.ok) {
|
|
63
|
+
const t = await res.text().catch(() => '')
|
|
64
|
+
throw new Error(
|
|
65
|
+
`S3 PUT failed: HTTP ${res.status}${t ? ' — ' + t.slice(0, 200) : ''}`
|
|
66
|
+
)
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
44
70
|
/**
|
|
45
71
|
* Stat a local file and derive metadata for an upload payload.
|
|
46
72
|
* Throws if the path doesn't exist or isn't a regular file.
|
package/src/index.js
CHANGED
|
@@ -2,13 +2,11 @@
|
|
|
2
2
|
import fs from 'node:fs'
|
|
3
3
|
import path from 'node:path'
|
|
4
4
|
import { fileURLToPath } from 'node:url'
|
|
5
|
-
import { build } from '@ossy/app'
|
|
6
5
|
import * as Auth from './auth/cli.js'
|
|
7
6
|
import * as Call from './call/cli.js'
|
|
8
|
-
import * as
|
|
9
|
-
import * as Init from './init/cli.js'
|
|
10
|
-
import { publish } from './publish/cli.js'
|
|
7
|
+
import * as App from './app/cli.js'
|
|
11
8
|
import * as Registry from './registry/cli.js'
|
|
9
|
+
import * as UploadDir from './upload-dir/cli.js'
|
|
12
10
|
import * as Workspace from './workspace/cli.js'
|
|
13
11
|
|
|
14
12
|
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
@@ -25,20 +23,15 @@ Usage:
|
|
|
25
23
|
Local utilities:
|
|
26
24
|
auth <subcommand> Sign in / out and check status: login | logout | status
|
|
27
25
|
workspace <subcommand> Manage active workspace: use | list | current
|
|
28
|
-
init
|
|
29
|
-
build Production build of the app in the current directory
|
|
30
|
-
cms validate Validate src/config.js (workspaceId + resourceTemplates)
|
|
26
|
+
app <subcommand> App commands: init | build | upload | validate | publish
|
|
31
27
|
|
|
32
28
|
Action dispatcher (any Ossy SDK action, no per-action wiring needed):
|
|
33
29
|
call <action.id> Run \`ossy call --help\` for the full catalog
|
|
34
30
|
|
|
35
31
|
Workflows (wrap one or more actions with extra ergonomics):
|
|
36
|
-
|
|
37
|
-
(wraps workspaces.import-resource-templates)
|
|
32
|
+
upload-dir <dir> <loc> Recursively upload a local directory to the CMS
|
|
38
33
|
registry <subcommand> ecr-push-credentials (wraps registry.ecr-push-credentials,
|
|
39
34
|
adds --format github-actions and password masking)
|
|
40
|
-
publish Deploy via @ossy/deployment-tools, then upload
|
|
41
|
-
resource templates and site build artifacts
|
|
42
35
|
|
|
43
36
|
Options:
|
|
44
37
|
-h, --help Show this help, or for a command: ossy <command> --help
|
|
@@ -62,48 +55,42 @@ Options (login):
|
|
|
62
55
|
list List workspaces the current user can access
|
|
63
56
|
current Print the active workspace id
|
|
64
57
|
`,
|
|
65
|
-
|
|
66
|
-
Scaffold a new Ossy app in [dir] (defaults to current directory).
|
|
67
|
-
Creates src/home.page.jsx, src/config.js, and package.json (if missing).
|
|
68
|
-
`,
|
|
69
|
-
build: `ossy build
|
|
70
|
-
Run the production build for the app in the current directory.
|
|
71
|
-
See the @ossy/app README for build options.
|
|
72
|
-
`,
|
|
73
|
-
publish: `ossy publish [options]
|
|
74
|
-
Multi-step workflow: deploy via @ossy/deployment-tools (temporary), then
|
|
75
|
-
optionally upload resource templates and site build artifacts. The deploy
|
|
76
|
-
step is intended to go away once the platform reacts to artifact events;
|
|
77
|
-
the CMS upload steps are equivalent to:
|
|
78
|
-
workspaces.import-resource-templates (resource templates)
|
|
79
|
-
/site-artifacts/presign-batch + /commit-batch (site artifacts)
|
|
80
|
-
|
|
81
|
-
Options:
|
|
82
|
-
-a, --authentication Ossy API JWT (or set OSSY_API_KEY, or run \`ossy auth login\`)
|
|
83
|
-
-d, --domain Site domain
|
|
84
|
-
-p, --platform Target deployment platform
|
|
85
|
-
-c, --config Path to src/config.js
|
|
86
|
-
--platforms-path Path to platforms.json
|
|
87
|
-
--deployments-path Glob for deployments JSON files
|
|
88
|
-
--all Deploy all sites for the platform
|
|
89
|
-
--skip-resource-templates Skip post-deploy resource-template upload
|
|
90
|
-
--skip-site-artifacts Skip post-deploy site-artifact upload
|
|
91
|
-
--site-artifacts-build-dir Override the site build directory
|
|
92
|
-
--api-url API base URL for CMS calls
|
|
93
|
-
`,
|
|
94
|
-
cms: `ossy cms <subcommand>
|
|
58
|
+
app: `ossy app <subcommand>
|
|
95
59
|
Subcommands:
|
|
60
|
+
init Scaffold a new Ossy app (default dir: current directory)
|
|
61
|
+
build Production build of the app in the current directory
|
|
96
62
|
upload Upload resource templates from src/config.js to the workspace
|
|
97
63
|
(wraps the SDK action workspaces.import-resource-templates)
|
|
98
64
|
validate Validate ossy app config and resource templates locally
|
|
65
|
+
publish Upload resource templates and build/ to the CMS
|
|
66
|
+
(resource templates + build files to /sites/{domain})
|
|
99
67
|
|
|
100
68
|
Options (upload):
|
|
101
69
|
-a, --authentication Ossy API JWT (or set OSSY_API_KEY, or run \`ossy auth login\`)
|
|
102
70
|
-c, --config Path to src/config.js
|
|
103
|
-
--api-url API base URL for
|
|
71
|
+
--api-url API base URL for API calls
|
|
104
72
|
|
|
105
73
|
Options (validate):
|
|
106
74
|
-c, --config Path to src/config.js
|
|
75
|
+
|
|
76
|
+
Options (publish):
|
|
77
|
+
-a, --authentication Ossy API JWT (or set OSSY_API_KEY, or run \`ossy auth login\`)
|
|
78
|
+
-c, --config Path to src/config.js
|
|
79
|
+
--build-dir Override build directory (default: <package>/build)
|
|
80
|
+
--build-dest Override remote CMS location (default: /sites/{domain})
|
|
81
|
+
--skip-resource-templates Skip resource template upload
|
|
82
|
+
--api-url API base URL for API calls
|
|
83
|
+
`,
|
|
84
|
+
'upload-dir': `ossy upload-dir <localDir> <remoteLocation> [options]
|
|
85
|
+
Recursively mirror a local directory into the Ossy CMS.
|
|
86
|
+
Creates parent directories first, then uploads every file.
|
|
87
|
+
Per-item errors are logged and skipped; exit code is non-zero if any failed.
|
|
88
|
+
|
|
89
|
+
Options:
|
|
90
|
+
--dry-run Print what would happen without making any API calls
|
|
91
|
+
-a, --authentication Ossy API JWT (or set OSSY_API_KEY, or run \`ossy auth login\`)
|
|
92
|
+
-w, --workspace-id Workspace id (or set OSSY_WORKSPACE_ID, or run \`ossy workspace use\`)
|
|
93
|
+
--api-url Override API base URL
|
|
107
94
|
`,
|
|
108
95
|
registry: `ossy registry <subcommand>
|
|
109
96
|
Subcommands:
|
|
@@ -151,24 +138,16 @@ const run = async () => {
|
|
|
151
138
|
await Call.handler(restArgs)
|
|
152
139
|
return
|
|
153
140
|
}
|
|
154
|
-
if (command === '
|
|
155
|
-
await
|
|
156
|
-
return
|
|
157
|
-
}
|
|
158
|
-
if (command === 'cms') {
|
|
159
|
-
Cms.handler(restArgs)
|
|
141
|
+
if (command === 'upload-dir') {
|
|
142
|
+
await UploadDir.handler(restArgs)
|
|
160
143
|
return
|
|
161
144
|
}
|
|
162
|
-
if (command === '
|
|
163
|
-
|
|
164
|
-
return
|
|
165
|
-
}
|
|
166
|
-
if (command === 'publish') {
|
|
167
|
-
await publish(restArgs)
|
|
145
|
+
if (command === 'registry') {
|
|
146
|
+
await Registry.handler(restArgs)
|
|
168
147
|
return
|
|
169
148
|
}
|
|
170
|
-
if (command === '
|
|
171
|
-
await
|
|
149
|
+
if (command === 'app') {
|
|
150
|
+
await App.handler(restArgs)
|
|
172
151
|
return
|
|
173
152
|
}
|
|
174
153
|
console.error(`[@ossy/cli] Unknown command: ${command}`)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import arg from 'arg'
|
|
2
2
|
import { appendFileSync } from 'fs'
|
|
3
|
-
import {
|
|
3
|
+
import { requireAppAuthentication } from '../app/upload-resource-templates.js'
|
|
4
4
|
import { getAuth, getWorkspaceId } from '../state.js'
|
|
5
5
|
|
|
6
6
|
const DEFAULT_API_URL = 'https://api.ossy.se/api/v0'
|
|
@@ -25,7 +25,7 @@ export async function ecrPushCredentials (options) {
|
|
|
25
25
|
|
|
26
26
|
let token
|
|
27
27
|
try {
|
|
28
|
-
token =
|
|
28
|
+
token = requireAppAuthentication(
|
|
29
29
|
getAuth({ flag: parsed['--authentication'] }),
|
|
30
30
|
'registry ecr-push-credentials'
|
|
31
31
|
)
|
|
@@ -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
|
+
}
|
package/src/publish/cli.js
DELETED
|
@@ -1,181 +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
|
-
import { getAuth } from '../state.js'
|
|
14
|
-
|
|
15
|
-
const DEPLOYMENT_TOOLS = '@ossy/deployment-tools'
|
|
16
|
-
|
|
17
|
-
function runNpxDeploymentTools (deploymentArgs) {
|
|
18
|
-
const args = ['--yes', DEPLOYMENT_TOOLS, 'deployment', ...deploymentArgs]
|
|
19
|
-
logInfo({ message: `[@ossy/cli] publish: npx ${DEPLOYMENT_TOOLS} deployment ${deploymentArgs[0]} …` })
|
|
20
|
-
return new Promise((resolvePromise, reject) => {
|
|
21
|
-
const npx = process.platform === 'win32' ? 'npx.cmd' : 'npx'
|
|
22
|
-
const child = spawn(npx, args, { stdio: 'inherit' })
|
|
23
|
-
child.on('error', reject)
|
|
24
|
-
child.on('close', (code) => {
|
|
25
|
-
if (code === 0) resolvePromise()
|
|
26
|
-
else reject(new Error(`npx exited with code ${code}`))
|
|
27
|
-
})
|
|
28
|
-
})
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* Publish container deployments: default = one site (`deployment deploy`);
|
|
33
|
-
* `--all` = `deployment deploy-all` for the platform.
|
|
34
|
-
*
|
|
35
|
-
* Requires **`--authentication`** (Ossy API JWT) or **`OSSY_API_KEY`**. Container
|
|
36
|
-
* deployments target **ECR** (`registry` in deployments JSON); the worker pulls
|
|
37
|
-
* with **IAM** (`aws ecr get-login-password`). No registry password is sent on the queue.
|
|
38
|
-
*/
|
|
39
|
-
export async function publish (options) {
|
|
40
|
-
const parsedArgs = arg({
|
|
41
|
-
'--authentication': String,
|
|
42
|
-
'-a': '--authentication',
|
|
43
|
-
'--domain': String,
|
|
44
|
-
'-d': '--domain',
|
|
45
|
-
'--platform': String,
|
|
46
|
-
'-p': '--platform',
|
|
47
|
-
'--config': String,
|
|
48
|
-
'-c': '--config',
|
|
49
|
-
'--platforms-path': String,
|
|
50
|
-
'-pp': '--platforms-path',
|
|
51
|
-
'--deployments-path': String,
|
|
52
|
-
'-dp': '--deployments-path',
|
|
53
|
-
'--all': Boolean,
|
|
54
|
-
'--skip-resource-templates': Boolean,
|
|
55
|
-
'--skip-site-artifacts': Boolean,
|
|
56
|
-
'--site-artifacts-build-dir': String,
|
|
57
|
-
'--api-url': String,
|
|
58
|
-
}, { argv: options })
|
|
59
|
-
|
|
60
|
-
const platformsPath = parsedArgs['--platforms-path']
|
|
61
|
-
const deploymentsPath = parsedArgs['--deployments-path']
|
|
62
|
-
|
|
63
|
-
const apiUrlForTemplates = parsedArgs['--api-url']
|
|
64
|
-
|
|
65
|
-
let apiToken
|
|
66
|
-
try {
|
|
67
|
-
apiToken = requireCmsAuthentication(
|
|
68
|
-
getAuth({ flag: parsedArgs['--authentication'] }),
|
|
69
|
-
'publish'
|
|
70
|
-
)
|
|
71
|
-
} catch (e) {
|
|
72
|
-
logError({
|
|
73
|
-
message:
|
|
74
|
-
e?.message
|
|
75
|
-
|| '[@ossy/cli] publish: pass --authentication (-a), set OSSY_API_KEY, or run `ossy auth login`.',
|
|
76
|
-
})
|
|
77
|
-
process.exit(1)
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
if (!platformsPath || !deploymentsPath) {
|
|
81
|
-
logError({
|
|
82
|
-
message: '[@ossy/cli] publish: --platforms-path (-pp) and --deployments-path (-dp) are required.'
|
|
83
|
-
})
|
|
84
|
-
process.exit(1)
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
const configFlag = parsedArgs['--config']
|
|
88
|
-
let configPath = resolveAppConfigPath(configFlag)
|
|
89
|
-
if (configFlag) {
|
|
90
|
-
if (!configPath || !existsSync(configPath)) {
|
|
91
|
-
logError({ message: `[@ossy/cli] publish: --config file not found: ${configFlag}` })
|
|
92
|
-
process.exit(1)
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
const skipResourceTemplates = parsedArgs['--skip-resource-templates']
|
|
97
|
-
const skipSiteArtifacts = parsedArgs['--skip-site-artifacts']
|
|
98
|
-
const siteArtifactsBuildDir = parsedArgs['--site-artifacts-build-dir']
|
|
99
|
-
|
|
100
|
-
const fromConfig = configPath ? readWebsiteConfigDeployFields(configPath) : {}
|
|
101
|
-
|
|
102
|
-
if (parsedArgs['--all']) {
|
|
103
|
-
let targetPlatform = parsedArgs['--platform'] || fromConfig.platform
|
|
104
|
-
if (!targetPlatform) {
|
|
105
|
-
logError({
|
|
106
|
-
message: '[@ossy/cli] publish --all: pass --platform (-p) or set platform in src/config.js (or --config).'
|
|
107
|
-
})
|
|
108
|
-
process.exit(1)
|
|
109
|
-
}
|
|
110
|
-
logInfo({ message: `[@ossy/cli] publish --all: platform=${targetPlatform}` })
|
|
111
|
-
await runNpxDeploymentTools([
|
|
112
|
-
'deploy-all',
|
|
113
|
-
'-p', targetPlatform,
|
|
114
|
-
'--platforms-path', platformsPath,
|
|
115
|
-
'--deployments-path', deploymentsPath,
|
|
116
|
-
])
|
|
117
|
-
if (!skipResourceTemplates && configPath) {
|
|
118
|
-
await maybeUploadResourceTemplatesAfterPublish({
|
|
119
|
-
configPath,
|
|
120
|
-
cmsToken: apiToken,
|
|
121
|
-
apiUrlFlag: apiUrlForTemplates,
|
|
122
|
-
})
|
|
123
|
-
}
|
|
124
|
-
if (!skipSiteArtifacts && configPath) {
|
|
125
|
-
await maybeUploadSiteArtifactsAfterPublish({
|
|
126
|
-
configPath,
|
|
127
|
-
cmsToken: apiToken,
|
|
128
|
-
apiUrlFlag: apiUrlForTemplates,
|
|
129
|
-
buildDir: siteArtifactsBuildDir,
|
|
130
|
-
})
|
|
131
|
-
}
|
|
132
|
-
return
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
let targetDomain = parsedArgs['--domain'] || fromConfig.domain
|
|
136
|
-
let targetPlatform = parsedArgs['--platform'] || fromConfig.platform
|
|
137
|
-
|
|
138
|
-
if (!targetPlatform && targetDomain && deploymentsPath) {
|
|
139
|
-
const resolved = resolvePlatformFromDeployments(deploymentsPath, targetDomain)
|
|
140
|
-
if (resolved && typeof resolved === 'object' && resolved.ambiguous) {
|
|
141
|
-
logError({
|
|
142
|
-
message: `[@ossy/cli] publish: domain "${targetDomain}" matches multiple platforms (${resolved.platforms.join(', ')}). Set platform in src/config.js or pass --platform (-p).`
|
|
143
|
-
})
|
|
144
|
-
process.exit(1)
|
|
145
|
-
}
|
|
146
|
-
targetPlatform = resolved
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
if (!targetDomain || !targetPlatform) {
|
|
150
|
-
logError({
|
|
151
|
-
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.'
|
|
152
|
-
})
|
|
153
|
-
process.exit(1)
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
logInfo({ message: `[@ossy/cli] publish: domain=${targetDomain} platform=${targetPlatform}` })
|
|
157
|
-
|
|
158
|
-
await runNpxDeploymentTools([
|
|
159
|
-
'deploy',
|
|
160
|
-
'-d', targetDomain,
|
|
161
|
-
'-p', targetPlatform,
|
|
162
|
-
'--platforms-path', platformsPath,
|
|
163
|
-
'--deployments-path', deploymentsPath,
|
|
164
|
-
])
|
|
165
|
-
|
|
166
|
-
if (!skipResourceTemplates && configPath) {
|
|
167
|
-
await maybeUploadResourceTemplatesAfterPublish({
|
|
168
|
-
configPath,
|
|
169
|
-
cmsToken: apiToken,
|
|
170
|
-
apiUrlFlag: apiUrlForTemplates,
|
|
171
|
-
})
|
|
172
|
-
}
|
|
173
|
-
if (!skipSiteArtifacts && configPath) {
|
|
174
|
-
await maybeUploadSiteArtifactsAfterPublish({
|
|
175
|
-
configPath,
|
|
176
|
-
cmsToken: apiToken,
|
|
177
|
-
apiUrlFlag: apiUrlForTemplates,
|
|
178
|
-
buildDir: siteArtifactsBuildDir,
|
|
179
|
-
})
|
|
180
|
-
}
|
|
181
|
-
}
|
|
@@ -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
|
-
}
|