@ossy/cli 0.16.1 → 0.16.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 +18 -9
- package/package.json +3 -3
- package/src/cms/cli.js +47 -28
- package/src/cms/upload-resource-templates.js +41 -0
- package/src/index.js +3 -1
- package/src/publish/cli.js +53 -10
- package/src/publish/load-website-config.js +12 -0
- package/src/publish/resource-templates-after-publish.js +59 -0
- package/src/publish/site-artifacts-after-publish.js +201 -0
- package/src/resolve-app-config-path.js +15 -0
- package/LICENSE +0 -21
package/README.md
CHANGED
|
@@ -9,8 +9,8 @@ Unified CLI for the Ossy platform: app dev/build and CMS workflows.
|
|
|
9
9
|
| `init [dir]` | Scaffold a new Ossy app (default: current directory) |
|
|
10
10
|
| `dev` | Start dev server with watch (uses `src/*.page.jsx` or `src/pages.jsx`, `src/config.js`) |
|
|
11
11
|
| `build` | Production build |
|
|
12
|
-
| `publish` | Queue a container deployment via `@ossy/deployment-tools` (see below) |
|
|
13
|
-
| `cms upload` | Upload resource templates
|
|
12
|
+
| `publish` | Queue a container deployment via `@ossy/deployment-tools`, then upload `resourceTemplates` and **site build artifacts** (S3 presign + CMS resource) when `workspaceId` is set (see below) |
|
|
13
|
+
| `cms upload` | Upload resource templates only (same API as publish’s upload step) |
|
|
14
14
|
| `cms validate` | Validate ossy config and resource templates |
|
|
15
15
|
|
|
16
16
|
## App: dev & build
|
|
@@ -39,6 +39,10 @@ npx @ossy/cli publish \
|
|
|
39
39
|
- **`--config`** — Path to another `config.js` if not `./src/config.js`.
|
|
40
40
|
- If `platform` is omitted but `domain` is set (from flags or config), it is inferred from `deployments.json` when that domain appears under exactly one `targetDeploymentPlatform`.
|
|
41
41
|
- **`--all`** — Runs `deployment deploy-all` for the platform; requires `--platform` or `platform` in config.
|
|
42
|
+
- **Resource templates** — After a successful deploy, the CLI loads `./src/config.js` (or `--config`). If it exports **`workspaceId`** and a non-empty **`resourceTemplates`** array, they are **POST**ed to **`{api}/resource-templates`** with the **`workspaceId`** header (same as the CMS). Skipped when **`--skip-resource-templates`** is set, or when `workspaceId` / `resourceTemplates` are missing.
|
|
43
|
+
- **Site artifacts** — If **`workspaceId`** is set and **`build/`** exists next to `src/` (i.e. run **`npm run build`** first), the CLI calls **`/site-artifacts/presign-batch`**, **PUT**s each file to S3, then **`/site-artifacts/commit-batch`** so a **`@ossy/platform/site-artifact-batch`** resource is created in the CMS. Skipped with **`--skip-site-artifacts`**, when there is no **`build/`**, or when **`workspaceId`** is missing. Override the build output directory with **`--site-artifacts-build-dir`** (absolute or cwd-relative).
|
|
44
|
+
- **`--cms-authentication`** — Optional token used only for CMS/API steps (templates + site artifacts); defaults to **`--authentication`** (deployment token).
|
|
45
|
+
- **`--api-url`** — Optional API base for CMS calls (e.g. `https://api.ossy.se/api/v0`). Otherwise **`OSSY_API_URL`**, else an **absolute** `apiUrl` from config, else `https://api.ossy.se/api/v0`. Relative app `apiUrl` values (e.g. `/@ossy`) are ignored unless you pass **`--api-url`** or set **`OSSY_API_URL`**.
|
|
42
46
|
|
|
43
47
|
Requires network access so `npx` can run `@ossy/deployment-tools`.
|
|
44
48
|
|
|
@@ -47,16 +51,20 @@ Requires network access so `npx` can run `@ossy/deployment-tools`.
|
|
|
47
51
|
Upload resource templates to your workspace so they can be used in the UI.
|
|
48
52
|
|
|
49
53
|
```bash
|
|
50
|
-
npx @ossy/cli cms upload --authentication <cms-api-token> --
|
|
54
|
+
npx @ossy/cli cms upload --authentication <cms-api-token> --config src/config.js
|
|
55
|
+
# optional: --api-url https://api.ossy.se/api/v0 (or set OSSY_API_URL)
|
|
51
56
|
```
|
|
52
57
|
|
|
58
|
+
When **`--config`** is omitted, **`./src/config.js`** is used if it exists (same as **`publish`**).
|
|
59
|
+
|
|
53
60
|
### Config consistency
|
|
54
61
|
|
|
55
|
-
- **App** (`dev`, `build`)
|
|
56
|
-
- **CMS** (`cms upload`): `--ossy-file` → workspace config with `workspaceId` and `resourceTemplates`
|
|
62
|
+
- **App** (`dev`, `build`), **CMS** (`cms upload` / `cms validate`), and **publish** all use **`--config`** (`-c`) for the app / workspace config file (`src/config.js` by default when present).
|
|
57
63
|
|
|
58
64
|
### Workflow example
|
|
59
65
|
|
|
66
|
+
Prefer **`publish`** so deploy and template upload run together (see **Publish** above). For template-only updates, `cms upload` still works.
|
|
67
|
+
|
|
60
68
|
```yaml
|
|
61
69
|
name: "[CMS] Upload resource templates"
|
|
62
70
|
|
|
@@ -76,7 +84,7 @@ jobs:
|
|
|
76
84
|
run: |
|
|
77
85
|
npx --yes @ossy/cli cms upload \
|
|
78
86
|
--authentication ${{ secrets.CMS_API_TOKEN }} \
|
|
79
|
-
--
|
|
87
|
+
--config src/config.js
|
|
80
88
|
```
|
|
81
89
|
|
|
82
90
|
### cms validate
|
|
@@ -84,17 +92,18 @@ jobs:
|
|
|
84
92
|
Validate an ossy config file before uploading:
|
|
85
93
|
|
|
86
94
|
```bash
|
|
87
|
-
npx @ossy/cli cms validate --
|
|
95
|
+
npx @ossy/cli cms validate --config src/config.js
|
|
88
96
|
```
|
|
89
97
|
|
|
90
|
-
|
|
98
|
+
When **`--config`** is omitted, **`./src/config.js`** is used if it exists.
|
|
91
99
|
|
|
92
100
|
### Arguments
|
|
93
101
|
|
|
94
102
|
| Argument | Description | Required |
|
|
95
103
|
|----------|-------------|----------|
|
|
96
104
|
| --authentication, -a | Your CMS API token | Yes (upload only) |
|
|
97
|
-
| --
|
|
105
|
+
| --config, -c | App config (`workspaceId`, `resourceTemplates`, …) | Optional if `./src/config.js` exists |
|
|
106
|
+
| --api-url | API base URL for upload (`…/api/v0`) | No |
|
|
98
107
|
|
|
99
108
|
## init
|
|
100
109
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ossy/cli",
|
|
3
|
-
"version": "0.16.
|
|
3
|
+
"version": "0.16.4",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "git+https://github.com/ossy-se/packages.git"
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"license": "MIT",
|
|
18
18
|
"bin": "./src/index.js",
|
|
19
19
|
"dependencies": {
|
|
20
|
-
"@ossy/app": "^0.15.
|
|
20
|
+
"@ossy/app": "^0.15.4",
|
|
21
21
|
"arg": "^5.0.2",
|
|
22
22
|
"glob": "^10.3.10"
|
|
23
23
|
},
|
|
@@ -29,5 +29,5 @@
|
|
|
29
29
|
"/src",
|
|
30
30
|
"README.md"
|
|
31
31
|
],
|
|
32
|
-
"gitHead": "
|
|
32
|
+
"gitHead": "f9c6ce7e08475bc0c96cae92e1eff3404964e315"
|
|
33
33
|
}
|
package/src/cms/cli.js
CHANGED
|
@@ -1,66 +1,79 @@
|
|
|
1
|
-
import { resolve } from 'path'
|
|
2
1
|
import { readFileSync, existsSync } from 'fs'
|
|
2
|
+
import { pathToFileURL } from 'url'
|
|
3
3
|
import arg from 'arg'
|
|
4
4
|
import { logInfo, logError } from '../log.js'
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
method: 'POST',
|
|
11
|
-
headers: { 'Authorization': token, 'Content-Type': 'application/json' },
|
|
12
|
-
body: JSON.stringify(resourceTemplates)
|
|
13
|
-
}
|
|
14
|
-
return fetch(endpoint, fetchOptions)
|
|
15
|
-
}
|
|
16
|
-
}
|
|
5
|
+
import { resolveAppConfigPath } from '../resolve-app-config-path.js'
|
|
6
|
+
import {
|
|
7
|
+
postResourceTemplates,
|
|
8
|
+
resolveApiBaseUrlForUpload,
|
|
9
|
+
} from './upload-resource-templates.js'
|
|
17
10
|
|
|
18
11
|
const resolveConfigImport = (filePath) =>
|
|
19
12
|
filePath.endsWith('json')
|
|
20
13
|
? Promise.resolve(JSON.parse(readFileSync(filePath, 'utf8')))
|
|
21
|
-
: import(filePath)
|
|
14
|
+
: import(pathToFileURL(filePath).href)
|
|
22
15
|
|
|
23
16
|
const upload = (options) => {
|
|
24
17
|
const parsedArgs = arg({
|
|
25
18
|
'--authentication': String,
|
|
26
19
|
'--a': '--authentication',
|
|
27
|
-
'--
|
|
20
|
+
'--config': String,
|
|
21
|
+
'-c': '--config',
|
|
22
|
+
'--api-url': String,
|
|
28
23
|
}, { argv: options })
|
|
29
24
|
|
|
30
25
|
const token = parsedArgs['--authentication']
|
|
31
|
-
const
|
|
26
|
+
const apiUrlFlag = parsedArgs['--api-url']
|
|
32
27
|
|
|
33
28
|
if (!token) {
|
|
34
29
|
logError({ message: '[@ossy/cli] No token provided. Use --authentication or -a' })
|
|
35
30
|
process.exit(1)
|
|
36
31
|
}
|
|
37
|
-
|
|
38
|
-
|
|
32
|
+
|
|
33
|
+
const filePath = resolveAppConfigPath(parsedArgs['--config'])
|
|
34
|
+
if (!filePath) {
|
|
35
|
+
logError({
|
|
36
|
+
message:
|
|
37
|
+
'[@ossy/cli] No config file. Pass --config (-c) or add src/config.js in the current directory.',
|
|
38
|
+
})
|
|
39
|
+
process.exit(1)
|
|
40
|
+
}
|
|
41
|
+
if (!existsSync(filePath)) {
|
|
42
|
+
logError({ message: `[@ossy/cli] Config not found: ${filePath}` })
|
|
39
43
|
process.exit(1)
|
|
40
44
|
}
|
|
41
45
|
|
|
42
46
|
logInfo({ message: '[@ossy/cli] Reading config...' })
|
|
43
|
-
const filePath = resolve(ossyFilePath)
|
|
44
47
|
|
|
45
48
|
return resolveConfigImport(filePath)
|
|
46
49
|
.then((module) => {
|
|
47
50
|
const config = module?.default ?? module
|
|
48
|
-
const apiUrl = config?.apiUrl || 'https://api.ossy.se/api/v0'
|
|
49
51
|
const workspaceId = config?.workspaceId
|
|
50
52
|
const resourceTemplates = config?.resourceTemplates
|
|
51
53
|
|
|
52
54
|
if (!workspaceId) {
|
|
53
|
-
logError({ message: '[@ossy/cli] No workspaceId in
|
|
55
|
+
logError({ message: '[@ossy/cli] No workspaceId in config' })
|
|
54
56
|
process.exit(1)
|
|
55
57
|
}
|
|
56
58
|
if (!resourceTemplates) {
|
|
57
|
-
logError({ message: '[@ossy/cli] No resourceTemplates in
|
|
59
|
+
logError({ message: '[@ossy/cli] No resourceTemplates in config' })
|
|
58
60
|
process.exit(1)
|
|
59
61
|
}
|
|
60
62
|
|
|
63
|
+
const apiBaseUrl = resolveApiBaseUrlForUpload({
|
|
64
|
+
flag: apiUrlFlag,
|
|
65
|
+
envVar: process.env.OSSY_API_URL,
|
|
66
|
+
configApiUrl: config?.apiUrl,
|
|
67
|
+
})
|
|
68
|
+
|
|
61
69
|
logInfo({ message: '[@ossy/cli] Uploading resource templates...' })
|
|
62
70
|
|
|
63
|
-
return
|
|
71
|
+
return postResourceTemplates({
|
|
72
|
+
apiBaseUrl,
|
|
73
|
+
token,
|
|
74
|
+
workspaceId,
|
|
75
|
+
resourceTemplates,
|
|
76
|
+
})
|
|
64
77
|
.then((response) => {
|
|
65
78
|
if (!response.ok) throw new Error(`Upload failed: ${response.status}`)
|
|
66
79
|
logInfo({ message: '[@ossy/cli] Done' })
|
|
@@ -78,14 +91,20 @@ const upload = (options) => {
|
|
|
78
91
|
|
|
79
92
|
const validate = (options) => {
|
|
80
93
|
const parsedArgs = arg({
|
|
81
|
-
'--
|
|
94
|
+
'--config': String,
|
|
95
|
+
'-c': '--config',
|
|
82
96
|
}, { argv: options })
|
|
83
97
|
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
|
|
98
|
+
const filePath = resolveAppConfigPath(parsedArgs['--config'])
|
|
99
|
+
if (!filePath) {
|
|
100
|
+
logError({
|
|
101
|
+
message:
|
|
102
|
+
'[@ossy/cli] Config not found. Pass --config (-c) or add src/config.js in the current directory.',
|
|
103
|
+
})
|
|
104
|
+
process.exit(1)
|
|
105
|
+
}
|
|
87
106
|
if (!existsSync(filePath)) {
|
|
88
|
-
logError({ message: `[@ossy/cli]
|
|
107
|
+
logError({ message: `[@ossy/cli] Config not found: ${filePath}` })
|
|
89
108
|
process.exit(1)
|
|
90
109
|
}
|
|
91
110
|
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* POST workspace-imported resource templates to the Ossy API.
|
|
3
|
+
* @param {{ apiBaseUrl: string, token: string, workspaceId: string, resourceTemplates: unknown[] }} opts
|
|
4
|
+
* @returns {Promise<Response>}
|
|
5
|
+
*/
|
|
6
|
+
export function postResourceTemplates ({
|
|
7
|
+
apiBaseUrl,
|
|
8
|
+
token,
|
|
9
|
+
workspaceId,
|
|
10
|
+
resourceTemplates,
|
|
11
|
+
}) {
|
|
12
|
+
const base = apiBaseUrl.replace(/\/$/, '')
|
|
13
|
+
const url = `${base}/resource-templates`
|
|
14
|
+
return fetch(url, {
|
|
15
|
+
method: 'POST',
|
|
16
|
+
headers: {
|
|
17
|
+
Authorization: token,
|
|
18
|
+
'Content-Type': 'application/json',
|
|
19
|
+
workspaceId,
|
|
20
|
+
},
|
|
21
|
+
body: JSON.stringify(resourceTemplates),
|
|
22
|
+
})
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Resolves API base URL for uploads (no trailing slash).
|
|
27
|
+
* Relative `apiUrl` in app config (e.g. `/@ossy`) is ignored; use flag or OSSY_API_URL.
|
|
28
|
+
*/
|
|
29
|
+
export function resolveApiBaseUrlForUpload ({ flag, envVar, configApiUrl }) {
|
|
30
|
+
if (flag && String(flag).trim()) {
|
|
31
|
+
return String(flag).replace(/\/$/, '')
|
|
32
|
+
}
|
|
33
|
+
if (envVar && String(envVar).trim()) {
|
|
34
|
+
return String(envVar).replace(/\/$/, '')
|
|
35
|
+
}
|
|
36
|
+
const raw = configApiUrl
|
|
37
|
+
if (typeof raw === 'string' && /^https?:\/\//i.test(raw.trim())) {
|
|
38
|
+
return raw.trim().replace(/\/$/, '')
|
|
39
|
+
}
|
|
40
|
+
return 'https://api.ossy.se/api/v0'
|
|
41
|
+
}
|
package/src/index.js
CHANGED
|
@@ -7,7 +7,9 @@ import { publish } from './publish/cli.js'
|
|
|
7
7
|
const [,, command, ...restArgs] = process.argv
|
|
8
8
|
|
|
9
9
|
if (!command) {
|
|
10
|
-
console.error(
|
|
10
|
+
console.error(
|
|
11
|
+
'[@ossy/cli] No command provided. Usage: ossy dev | build [--worker] | publish | init | cms <subcommand>'
|
|
12
|
+
)
|
|
11
13
|
process.exit(1)
|
|
12
14
|
}
|
|
13
15
|
|
package/src/publish/cli.js
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { existsSync } from 'fs'
|
|
2
|
-
import { resolve as pathResolve } from 'path'
|
|
3
2
|
import { spawn } from 'child_process'
|
|
4
3
|
import arg from 'arg'
|
|
5
4
|
import { logInfo, logError } from '../log.js'
|
|
5
|
+
import { resolveAppConfigPath } from '../resolve-app-config-path.js'
|
|
6
6
|
import {
|
|
7
7
|
readWebsiteConfigDeployFields,
|
|
8
8
|
resolvePlatformFromDeployments
|
|
9
9
|
} from './resolve-config.js'
|
|
10
|
+
import { maybeUploadResourceTemplatesAfterPublish } from './resource-templates-after-publish.js'
|
|
11
|
+
import { maybeUploadSiteArtifactsAfterPublish } from './site-artifacts-after-publish.js'
|
|
10
12
|
|
|
11
13
|
const DEPLOYMENT_TOOLS = '@ossy/deployment-tools'
|
|
12
14
|
|
|
@@ -43,7 +45,12 @@ export async function publish (options) {
|
|
|
43
45
|
'-pp': '--platforms-path',
|
|
44
46
|
'--deployments-path': String,
|
|
45
47
|
'-dp': '--deployments-path',
|
|
46
|
-
'--all': Boolean
|
|
48
|
+
'--all': Boolean,
|
|
49
|
+
'--skip-resource-templates': Boolean,
|
|
50
|
+
'--skip-site-artifacts': Boolean,
|
|
51
|
+
'--site-artifacts-build-dir': String,
|
|
52
|
+
'--api-url': String,
|
|
53
|
+
'--cms-authentication': String
|
|
47
54
|
}, { argv: options })
|
|
48
55
|
|
|
49
56
|
const username = parsedArgs['--username']
|
|
@@ -64,17 +71,22 @@ export async function publish (options) {
|
|
|
64
71
|
process.exit(1)
|
|
65
72
|
}
|
|
66
73
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
if (existsSync(
|
|
71
|
-
|
|
74
|
+
const configFlag = parsedArgs['--config']
|
|
75
|
+
let configPath = resolveAppConfigPath(configFlag)
|
|
76
|
+
if (configFlag) {
|
|
77
|
+
if (!configPath || !existsSync(configPath)) {
|
|
78
|
+
logError({ message: `[@ossy/cli] publish: --config file not found: ${configFlag}` })
|
|
79
|
+
process.exit(1)
|
|
72
80
|
}
|
|
73
|
-
} else if (!existsSync(pathResolve(configPath))) {
|
|
74
|
-
logError({ message: `[@ossy/cli] publish: --config file not found: ${configPath}` })
|
|
75
|
-
process.exit(1)
|
|
76
81
|
}
|
|
77
82
|
|
|
83
|
+
const skipResourceTemplates = parsedArgs['--skip-resource-templates']
|
|
84
|
+
const skipSiteArtifacts = parsedArgs['--skip-site-artifacts']
|
|
85
|
+
const siteArtifactsBuildDir = parsedArgs['--site-artifacts-build-dir']
|
|
86
|
+
const apiUrlForTemplates = parsedArgs['--api-url']
|
|
87
|
+
const cmsAuthentication =
|
|
88
|
+
parsedArgs['--cms-authentication'] || authentication
|
|
89
|
+
|
|
78
90
|
const fromConfig = configPath ? readWebsiteConfigDeployFields(configPath) : {}
|
|
79
91
|
|
|
80
92
|
if (parsedArgs['--all']) {
|
|
@@ -94,6 +106,21 @@ export async function publish (options) {
|
|
|
94
106
|
'-pp', platformsPath,
|
|
95
107
|
'-dp', deploymentsPath
|
|
96
108
|
])
|
|
109
|
+
if (!skipResourceTemplates && configPath) {
|
|
110
|
+
await maybeUploadResourceTemplatesAfterPublish({
|
|
111
|
+
configPath,
|
|
112
|
+
cmsToken: cmsAuthentication,
|
|
113
|
+
apiUrlFlag: apiUrlForTemplates,
|
|
114
|
+
})
|
|
115
|
+
}
|
|
116
|
+
if (!skipSiteArtifacts && configPath) {
|
|
117
|
+
await maybeUploadSiteArtifactsAfterPublish({
|
|
118
|
+
configPath,
|
|
119
|
+
cmsToken: cmsAuthentication,
|
|
120
|
+
apiUrlFlag: apiUrlForTemplates,
|
|
121
|
+
buildDir: siteArtifactsBuildDir,
|
|
122
|
+
})
|
|
123
|
+
}
|
|
97
124
|
return
|
|
98
125
|
}
|
|
99
126
|
|
|
@@ -129,4 +156,20 @@ export async function publish (options) {
|
|
|
129
156
|
'-pp', platformsPath,
|
|
130
157
|
'-dp', deploymentsPath
|
|
131
158
|
])
|
|
159
|
+
|
|
160
|
+
if (!skipResourceTemplates && configPath) {
|
|
161
|
+
await maybeUploadResourceTemplatesAfterPublish({
|
|
162
|
+
configPath,
|
|
163
|
+
cmsToken: cmsAuthentication,
|
|
164
|
+
apiUrlFlag: apiUrlForTemplates,
|
|
165
|
+
})
|
|
166
|
+
}
|
|
167
|
+
if (!skipSiteArtifacts && configPath) {
|
|
168
|
+
await maybeUploadSiteArtifactsAfterPublish({
|
|
169
|
+
configPath,
|
|
170
|
+
cmsToken: cmsAuthentication,
|
|
171
|
+
apiUrlFlag: apiUrlForTemplates,
|
|
172
|
+
buildDir: siteArtifactsBuildDir,
|
|
173
|
+
})
|
|
174
|
+
}
|
|
132
175
|
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { resolve } from 'path'
|
|
2
|
+
import { pathToFileURL } from 'url'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Loads app `config.js` (ESM) for publish-side steps (e.g. resource template upload).
|
|
6
|
+
* @param {string} configPath Absolute or cwd-relative path to config file
|
|
7
|
+
*/
|
|
8
|
+
export async function loadWebsiteConfig (configPath) {
|
|
9
|
+
const abs = resolve(configPath)
|
|
10
|
+
const mod = await import(pathToFileURL(abs).href)
|
|
11
|
+
return mod.default ?? mod
|
|
12
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { logInfo } from '../log.js'
|
|
2
|
+
import { loadWebsiteConfig } from './load-website-config.js'
|
|
3
|
+
import {
|
|
4
|
+
postResourceTemplates,
|
|
5
|
+
resolveApiBaseUrlForUpload,
|
|
6
|
+
} from '../cms/upload-resource-templates.js'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* After a successful deployment, sync `resourceTemplates` from app config to the workspace API.
|
|
10
|
+
*/
|
|
11
|
+
export async function maybeUploadResourceTemplatesAfterPublish ({
|
|
12
|
+
configPath,
|
|
13
|
+
cmsToken,
|
|
14
|
+
apiUrlFlag,
|
|
15
|
+
}) {
|
|
16
|
+
const config = await loadWebsiteConfig(configPath)
|
|
17
|
+
const workspaceId = config?.workspaceId
|
|
18
|
+
const resourceTemplates = config?.resourceTemplates
|
|
19
|
+
|
|
20
|
+
if (!workspaceId) {
|
|
21
|
+
logInfo({
|
|
22
|
+
message:
|
|
23
|
+
'[@ossy/cli] publish: skipping resource template upload (no workspaceId in config)',
|
|
24
|
+
})
|
|
25
|
+
return
|
|
26
|
+
}
|
|
27
|
+
if (!Array.isArray(resourceTemplates) || resourceTemplates.length === 0) {
|
|
28
|
+
logInfo({
|
|
29
|
+
message:
|
|
30
|
+
'[@ossy/cli] publish: skipping resource template upload (resourceTemplates missing or empty)',
|
|
31
|
+
})
|
|
32
|
+
return
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const apiBaseUrl = resolveApiBaseUrlForUpload({
|
|
36
|
+
flag: apiUrlFlag,
|
|
37
|
+
envVar: process.env.OSSY_API_URL,
|
|
38
|
+
configApiUrl: config?.apiUrl,
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
logInfo({ message: '[@ossy/cli] publish: uploading resource templates…' })
|
|
42
|
+
|
|
43
|
+
const response = await postResourceTemplates({
|
|
44
|
+
apiBaseUrl,
|
|
45
|
+
token: cmsToken,
|
|
46
|
+
workspaceId,
|
|
47
|
+
resourceTemplates,
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
if (!response.ok) {
|
|
51
|
+
const text = await response.text().catch(() => '')
|
|
52
|
+
throw new Error(
|
|
53
|
+
`Resource template upload failed: HTTP ${response.status}${
|
|
54
|
+
text ? ` — ${text.slice(0, 200)}` : ''
|
|
55
|
+
}`
|
|
56
|
+
)
|
|
57
|
+
}
|
|
58
|
+
logInfo({ message: '[@ossy/cli] publish: resource templates uploaded' })
|
|
59
|
+
}
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import { readdir, stat, readFile } from 'fs/promises'
|
|
2
|
+
import { existsSync } from 'fs'
|
|
3
|
+
import path from 'path'
|
|
4
|
+
import { logInfo } from '../log.js'
|
|
5
|
+
import { loadWebsiteConfig } from './load-website-config.js'
|
|
6
|
+
import { resolveApiBaseUrlForUpload } from '../cms/upload-resource-templates.js'
|
|
7
|
+
|
|
8
|
+
const MAX_FILES = 200
|
|
9
|
+
|
|
10
|
+
/** @type {Record<string, string>} */
|
|
11
|
+
const MIME_BY_EXT = {
|
|
12
|
+
'.js': 'application/javascript',
|
|
13
|
+
'.mjs': 'application/javascript',
|
|
14
|
+
'.cjs': 'application/javascript',
|
|
15
|
+
'.css': 'text/css',
|
|
16
|
+
'.html': 'text/html',
|
|
17
|
+
'.htm': 'text/html',
|
|
18
|
+
'.json': 'application/json',
|
|
19
|
+
'.map': 'application/json',
|
|
20
|
+
'.svg': 'image/svg+xml',
|
|
21
|
+
'.png': 'image/png',
|
|
22
|
+
'.jpg': 'image/jpeg',
|
|
23
|
+
'.jpeg': 'image/jpeg',
|
|
24
|
+
'.gif': 'image/gif',
|
|
25
|
+
'.webp': 'image/webp',
|
|
26
|
+
'.ico': 'image/x-icon',
|
|
27
|
+
'.woff': 'font/woff',
|
|
28
|
+
'.woff2': 'font/woff2',
|
|
29
|
+
'.ttf': 'font/ttf',
|
|
30
|
+
'.txt': 'text/plain',
|
|
31
|
+
'.xml': 'application/xml',
|
|
32
|
+
'.webmanifest': 'application/manifest+json',
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function guessContentType (filePath) {
|
|
36
|
+
const ext = path.extname(filePath).toLowerCase()
|
|
37
|
+
return MIME_BY_EXT[ext] || 'application/octet-stream'
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* @param {string} buildDir absolute path to `build/` output
|
|
42
|
+
* @returns {Promise<{ absPath: string, relativePath: string, size: number }[]>}
|
|
43
|
+
*/
|
|
44
|
+
export async function collectBuildFiles (buildDir) {
|
|
45
|
+
const out = []
|
|
46
|
+
|
|
47
|
+
async function walk (dir, rel = '') {
|
|
48
|
+
const entries = await readdir(dir, { withFileTypes: true })
|
|
49
|
+
for (const e of entries) {
|
|
50
|
+
const abs = path.join(dir, e.name)
|
|
51
|
+
const relPath = rel ? `${rel}/${e.name}` : e.name
|
|
52
|
+
if (e.isDirectory()) {
|
|
53
|
+
await walk(abs, relPath)
|
|
54
|
+
} else {
|
|
55
|
+
const st = await stat(abs)
|
|
56
|
+
out.push({
|
|
57
|
+
absPath: abs,
|
|
58
|
+
relativePath: relPath.split(path.sep).join('/'),
|
|
59
|
+
size: st.size,
|
|
60
|
+
})
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
await walk(buildDir)
|
|
66
|
+
return out
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function workspaceHeaders (token, workspaceId) {
|
|
70
|
+
return {
|
|
71
|
+
Authorization: token,
|
|
72
|
+
'Content-Type': 'application/json',
|
|
73
|
+
workspaceId,
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* After deploy: upload `build/` to S3 via presigned URLs and commit a CMS resource (`@ossy/platform/site-artifact-batch`).
|
|
79
|
+
*/
|
|
80
|
+
export async function maybeUploadSiteArtifactsAfterPublish ({
|
|
81
|
+
configPath,
|
|
82
|
+
cmsToken,
|
|
83
|
+
apiUrlFlag,
|
|
84
|
+
buildDir: buildDirOpt,
|
|
85
|
+
}) {
|
|
86
|
+
const config = await loadWebsiteConfig(configPath)
|
|
87
|
+
const workspaceId = config?.workspaceId
|
|
88
|
+
|
|
89
|
+
if (!workspaceId) {
|
|
90
|
+
logInfo({
|
|
91
|
+
message:
|
|
92
|
+
'[@ossy/cli] publish: skipping site artifacts (no workspaceId in config)',
|
|
93
|
+
})
|
|
94
|
+
return
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const packageRoot = path.join(path.dirname(configPath), '..')
|
|
98
|
+
const buildDir = buildDirOpt
|
|
99
|
+
? path.resolve(buildDirOpt)
|
|
100
|
+
: path.join(packageRoot, 'build')
|
|
101
|
+
|
|
102
|
+
if (!existsSync(buildDir)) {
|
|
103
|
+
logInfo({
|
|
104
|
+
message: `[@ossy/cli] publish: skipping site artifacts (no build at ${buildDir}; run npm run build first)`,
|
|
105
|
+
})
|
|
106
|
+
return
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const apiBaseUrl = resolveApiBaseUrlForUpload({
|
|
110
|
+
flag: apiUrlFlag,
|
|
111
|
+
envVar: process.env.OSSY_API_URL,
|
|
112
|
+
configApiUrl: config?.apiUrl,
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
const files = await collectBuildFiles(buildDir)
|
|
116
|
+
if (files.length === 0) {
|
|
117
|
+
logInfo({ message: '[@ossy/cli] publish: skipping site artifacts (build directory is empty)' })
|
|
118
|
+
return
|
|
119
|
+
}
|
|
120
|
+
if (files.length > MAX_FILES) {
|
|
121
|
+
throw new Error(
|
|
122
|
+
`[@ossy/cli] publish: site artifact upload supports at most ${MAX_FILES} files; build has ${files.length}`
|
|
123
|
+
)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
logInfo({ message: `[@ossy/cli] publish: uploading ${files.length} site artifact file(s) to CMS…` })
|
|
127
|
+
|
|
128
|
+
const presignRes = await fetch(`${apiBaseUrl}/site-artifacts/presign-batch`, {
|
|
129
|
+
method: 'POST',
|
|
130
|
+
headers: workspaceHeaders(cmsToken, workspaceId),
|
|
131
|
+
body: JSON.stringify({
|
|
132
|
+
files: files.map((f) => ({
|
|
133
|
+
path: f.relativePath,
|
|
134
|
+
contentType: guessContentType(f.relativePath),
|
|
135
|
+
contentLength: f.size,
|
|
136
|
+
})),
|
|
137
|
+
}),
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
if (!presignRes.ok) {
|
|
141
|
+
const text = await presignRes.text().catch(() => '')
|
|
142
|
+
throw new Error(
|
|
143
|
+
`Site artifact presign failed: HTTP ${presignRes.status}${text ? ` — ${text.slice(0, 300)}` : ''}`
|
|
144
|
+
)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** @type {{ batchId: string, files: { path: string, key: string, uploadUrl: string, contentType: string, contentLength: number }[] }} */
|
|
148
|
+
const presignJson = await presignRes.json()
|
|
149
|
+
const { batchId, files: uploadSlots } = presignJson
|
|
150
|
+
|
|
151
|
+
if (!batchId || !Array.isArray(uploadSlots) || uploadSlots.length !== files.length) {
|
|
152
|
+
throw new Error('[@ossy/cli] publish: invalid presign-batch response')
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const byPath = new Map(uploadSlots.map((s) => [s.path, s]))
|
|
156
|
+
|
|
157
|
+
for (const local of files) {
|
|
158
|
+
const slot = byPath.get(local.relativePath)
|
|
159
|
+
if (!slot?.uploadUrl) {
|
|
160
|
+
throw new Error(`[@ossy/cli] publish: presign response missing entry for ${local.relativePath}`)
|
|
161
|
+
}
|
|
162
|
+
const body = await readFile(local.absPath)
|
|
163
|
+
if (body.length !== local.size) {
|
|
164
|
+
throw new Error(`[@ossy/cli] publish: file size mismatch for ${local.relativePath}`)
|
|
165
|
+
}
|
|
166
|
+
const putRes = await fetch(slot.uploadUrl, {
|
|
167
|
+
method: 'PUT',
|
|
168
|
+
headers: {
|
|
169
|
+
'Content-Type': slot.contentType,
|
|
170
|
+
'Content-Length': String(slot.contentLength),
|
|
171
|
+
},
|
|
172
|
+
body,
|
|
173
|
+
})
|
|
174
|
+
if (!putRes.ok) {
|
|
175
|
+
const t = await putRes.text().catch(() => '')
|
|
176
|
+
throw new Error(
|
|
177
|
+
`S3 PUT failed for ${local.relativePath}: HTTP ${putRes.status}${t ? ` — ${t.slice(0, 200)}` : ''}`
|
|
178
|
+
)
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
const commitRes = await fetch(`${apiBaseUrl}/site-artifacts/commit-batch`, {
|
|
184
|
+
method: 'POST',
|
|
185
|
+
headers: workspaceHeaders(cmsToken, workspaceId),
|
|
186
|
+
body: JSON.stringify({
|
|
187
|
+
batchId,
|
|
188
|
+
paths: files.map((f) => f.relativePath),
|
|
189
|
+
name: batchId,
|
|
190
|
+
}),
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
if (!commitRes.ok) {
|
|
194
|
+
const text = await commitRes.text().catch(() => '')
|
|
195
|
+
throw new Error(
|
|
196
|
+
`Site artifact commit failed: HTTP ${commitRes.status}${text ? ` — ${text.slice(0, 300)}` : ''}`
|
|
197
|
+
)
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
logInfo({ message: '[@ossy/cli] publish: site artifacts uploaded and CMS resource created' })
|
|
201
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { resolve } from 'path'
|
|
2
|
+
import { existsSync } from 'fs'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Resolves app config path: explicit `--config` or `./src/config.js` when it exists (cwd-relative).
|
|
6
|
+
* @param {string | undefined} configFlag
|
|
7
|
+
* @returns {string | null} Absolute path, or null when no default exists and flag omitted.
|
|
8
|
+
*/
|
|
9
|
+
export function resolveAppConfigPath (configFlag) {
|
|
10
|
+
if (configFlag) {
|
|
11
|
+
return resolve(configFlag)
|
|
12
|
+
}
|
|
13
|
+
const fallback = resolve(process.cwd(), 'src/config.js')
|
|
14
|
+
return existsSync(fallback) ? fallback : null
|
|
15
|
+
}
|
package/LICENSE
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2025 Ossy
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|