@ossy/cli 1.16.11 → 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 +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 +206 -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/README.md
CHANGED
|
@@ -22,7 +22,7 @@ For one-off use without installing, you can still run `npx @ossy/cli <command>`.
|
|
|
22
22
|
|
|
23
23
|
The contract for everything the platform can do is the **SDK action POJO** in [`@ossy/sdk`](../sdk) (`{ id, endpoint, method, payload }`). The CLI is one channel onto that catalog, organized in three layers:
|
|
24
24
|
|
|
25
|
-
1. **Local utilities** — no API call, or per-machine state. `
|
|
25
|
+
1. **Local utilities** — no API call, or per-machine state. `auth`, `workspace`, `app init`, `app build`, `app validate`.
|
|
26
26
|
2. **Action dispatcher** — `call <action.id>` invokes any action in the SDK catalog. New endpoints added to the SDK are callable from the terminal with no extra CLI wiring.
|
|
27
27
|
3. **High-level workflows** — verbs that wrap one or more actions and add value the dispatcher doesn't (reading `src/config.js`, formatting output for CI, multi-step pipelines). Each workflow's docs name the underlying action(s) so you can drop down to `ossy call` if you outgrow the verb.
|
|
28
28
|
|
|
@@ -30,13 +30,14 @@ The contract for everything the platform can do is the **SDK action POJO** in [`
|
|
|
30
30
|
|-------|---------|-------------|
|
|
31
31
|
| Local | `auth login \| logout \| status` | Save / delete / inspect the Ossy API token under `~/.config/ossy/credentials.json` |
|
|
32
32
|
| Local | `workspace use <id> \| list \| current` | Manage the active workspace (saved to `~/.config/ossy/config.json`) |
|
|
33
|
-
| Local | `init [dir]` | Scaffold a new Ossy app (default: current directory) |
|
|
34
|
-
| Local | `build` | Production build of the app in the current directory |
|
|
35
|
-
| Local | `
|
|
33
|
+
| Local | `app init [dir]` | Scaffold a new Ossy app (default: current directory) |
|
|
34
|
+
| Local | `app build` | Production build of the app in the current directory |
|
|
35
|
+
| Local | `app validate` | Validate `src/config.js` (`workspaceId`, `resourceTemplates`) without calling the API |
|
|
36
36
|
| Dispatcher | `call <action.id>` | Invoke any Ossy SDK action by id (`ossy call --help` for the full catalog) |
|
|
37
|
-
| Workflow | `
|
|
37
|
+
| Workflow | `app upload` | Read `resourceTemplates` from `src/config.js` and call `workspaces.import-resource-templates` |
|
|
38
|
+
| Workflow | `app publish` | Upload resource templates and `build/` to the CMS (combines `app upload` + `upload-dir`) |
|
|
39
|
+
| Workflow | `upload-dir <dir> <location>` | Recursively mirror a local directory into the CMS (mkdir + upload per file) |
|
|
38
40
|
| Workflow | `registry ecr-push-credentials` | Call `registry.ecr-push-credentials`, format for `docker login` or GitHub Actions |
|
|
39
|
-
| Workflow | `publish` | Queue a deployment via `@ossy/deployment-tools` (**temporary**; see below), then run the resource-template and site-artifact uploads |
|
|
40
41
|
|
|
41
42
|
If you need an endpoint that no verb wraps, reach for `ossy call` — it's the universal channel.
|
|
42
43
|
|
|
@@ -102,64 +103,69 @@ Output is JSON pretty-printed to stdout on success. Errors go to stderr with a n
|
|
|
102
103
|
## App: build
|
|
103
104
|
|
|
104
105
|
```bash
|
|
105
|
-
ossy build
|
|
106
|
+
ossy app build
|
|
106
107
|
```
|
|
107
108
|
|
|
108
109
|
Options: e.g. `--config` for `src/config.js`. See `packages/app/README.md` for app build behavior.
|
|
109
110
|
|
|
110
|
-
##
|
|
111
|
+
## App: publish
|
|
111
112
|
|
|
112
|
-
Publishes a
|
|
113
|
+
Publishes a website: uploads resource templates to the CMS and mirrors the `build/` folder to a CMS location. Run from the **website package** directory (where `src/config.js` lives) so config values are read automatically.
|
|
113
114
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
Instead, the CLI uses a **temporary** pipeline:
|
|
121
|
-
|
|
122
|
-
- **`workspaceId`** and **`apiUrl`** — read with **string-literal regexes** (same idea as deploy hints).
|
|
123
|
-
- **`resourceTemplates`** — parsed with **`@babel/parser`** from `export default { … }` when that array is **JSON-like literals only** (no function calls, variables, spreads, or template literals in that subtree).
|
|
115
|
+
```bash
|
|
116
|
+
cd packages/my-website
|
|
117
|
+
npm run build # produce build/ first
|
|
118
|
+
ossy app publish # reads src/config.js, uploads templates + build/
|
|
119
|
+
```
|
|
124
120
|
|
|
125
|
-
|
|
121
|
+
**This is a two-step workflow:**
|
|
122
|
+
1. Upload resource templates → `POST /resource-templates` (same as `app upload`).
|
|
123
|
+
2. Upload `build/` to the CMS → `resources.create-directory` + `resources.upload` per file (same as `upload-dir`), mirrored to `/sites/{domain}` by default.
|
|
126
124
|
|
|
127
|
-
###
|
|
125
|
+
### Config extraction
|
|
128
126
|
|
|
129
|
-
|
|
127
|
+
`publish` reads `workspaceId`, `apiUrl`, `domain`, and `resourceTemplates` from `src/config.js` **without executing** it (static regex + Babel AST parse), so imports like `@ossy/themes` that only resolve under Rollup are never run.
|
|
130
128
|
|
|
131
|
-
|
|
129
|
+
If `resourceTemplates` cannot be extracted, the template upload step is skipped silently.
|
|
132
130
|
|
|
133
131
|
### Authentication
|
|
134
132
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
Container deploys assume **Amazon ECR** in **`deployments.json`** (`registry` like `123456789012.dkr.ecr.eu-north-1.amazonaws.com`, **`image`** unchanged). The **worker** always pulls with **IAM** (`aws ecr get-login-password`); nothing in the queue carries a registry password. For **`docker push` from CI**, call **`POST /api/v0/registry/ecr/push-credentials`** with the same JWT and **`workspaceId`**, then **`docker login`** / **`docker push`**.
|
|
133
|
+
Requires `--authentication` / `-a` or `OSSY_API_KEY`: an Ossy API JWT (workspace API token).
|
|
138
134
|
|
|
139
135
|
```bash
|
|
140
136
|
cd packages/my-website
|
|
141
|
-
export OSSY_API_KEY=<ossy-api-jwt>
|
|
142
|
-
|
|
143
|
-
ossy publish
|
|
144
|
-
|
|
145
|
-
|
|
137
|
+
export OSSY_API_KEY=<ossy-api-jwt>
|
|
138
|
+
npm run build
|
|
139
|
+
ossy app publish
|
|
140
|
+
# or with explicit options:
|
|
141
|
+
ossy app publish \
|
|
142
|
+
--config src/config.js \
|
|
143
|
+
--build-dir ./build \
|
|
144
|
+
--build-dest /sites/my-site.se
|
|
146
145
|
```
|
|
147
146
|
|
|
148
|
-
|
|
147
|
+
### Options
|
|
149
148
|
|
|
150
|
-
|
|
149
|
+
| Flag | Description |
|
|
150
|
+
|------|-------------|
|
|
151
|
+
| `-a, --authentication` | Ossy API JWT (or `OSSY_API_KEY`, or `ossy auth login`) |
|
|
152
|
+
| `-c, --config` | Path to `src/config.js` (default: `./src/config.js`) |
|
|
153
|
+
| `--build-dir` | Override build directory (default: `<package>/build`) |
|
|
154
|
+
| `--build-dest` | Override remote CMS location (default: `/sites/{domain}`) |
|
|
155
|
+
| `--skip-resource-templates` | Skip the resource template upload step |
|
|
156
|
+
| `--api-url` | API base URL (or `OSSY_API_URL`; relative `apiUrl` in config is ignored) |
|
|
151
157
|
|
|
152
|
-
|
|
153
|
-
- **`--config`** — Path to another `config.js` if not `./src/config.js`.
|
|
154
|
-
- 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`.
|
|
155
|
-
- **`--all`** — Runs `deployment deploy-all` for the platform; requires `--platform` or `platform` in config.
|
|
156
|
-
- **Resource templates** — After a successful deploy, the CLI reads **`workspaceId`** / **`resourceTemplates`** from `./src/config.js` (or `--config`) using the **static extraction** described above (not `import()`). If **`workspaceId`** is set and **`resourceTemplates`** is a non-empty 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 or not extractable.
|
|
157
|
-
- **Site artifacts** — Uses the same **static `workspaceId` / `apiUrl` extraction** as resource templates. 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).
|
|
158
|
-
- **`--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`**.
|
|
158
|
+
### CI example
|
|
159
159
|
|
|
160
|
-
|
|
160
|
+
```yaml
|
|
161
|
+
- name: Publish
|
|
162
|
+
run: |
|
|
163
|
+
npm run build
|
|
164
|
+
npx --yes @ossy/cli app publish \
|
|
165
|
+
--authentication ${{ secrets.OSSY_API_KEY }}
|
|
166
|
+
```
|
|
161
167
|
|
|
162
|
-
##
|
|
168
|
+
## App: upload
|
|
163
169
|
|
|
164
170
|
Upload resource templates to your workspace so they can be used in the UI.
|
|
165
171
|
|
|
@@ -169,23 +175,23 @@ Wraps the SDK action **`workspaces.import-resource-templates`** and adds: readin
|
|
|
169
175
|
ossy call workspaces.import-resource-templates --json '{"templates":[…]}'
|
|
170
176
|
```
|
|
171
177
|
|
|
172
|
-
Prefer **`--authentication` / `-a`** for the **Ossy API JWT** (same as **`publish`**); it matches **`OSSY_API_KEY`** in CI.
|
|
178
|
+
Prefer **`--authentication` / `-a`** for the **Ossy API JWT** (same as **`app publish`**); it matches **`OSSY_API_KEY`** in CI.
|
|
173
179
|
|
|
174
180
|
```bash
|
|
175
|
-
ossy
|
|
181
|
+
ossy app upload --authentication <ossy-api-jwt> --config src/config.js
|
|
176
182
|
# optional: --api-url https://api.ossy.se/api/v0 (or set OSSY_API_URL)
|
|
177
183
|
# In CI you may omit --authentication when OSSY_API_KEY is set
|
|
178
184
|
```
|
|
179
185
|
|
|
180
|
-
When **`--config`** is omitted, **`./src/config.js`** is used if it exists (same as **`publish`**).
|
|
186
|
+
When **`--config`** is omitted, **`./src/config.js`** is used if it exists (same as **`app publish`**).
|
|
181
187
|
|
|
182
188
|
### Config consistency
|
|
183
189
|
|
|
184
|
-
- **App** (`build`), **
|
|
190
|
+
- **App** (`build`), **app** (`app upload` / `app validate` / `app publish`), and **upload-dir** all use **`--config`** (`-c`) for the app / workspace config file (`src/config.js` by default when present).
|
|
185
191
|
|
|
186
192
|
### Workflow example
|
|
187
193
|
|
|
188
|
-
Prefer **`publish`** so
|
|
194
|
+
Prefer **`app publish`** so templates and build files are uploaded together. For template-only updates, `app upload` still works.
|
|
189
195
|
|
|
190
196
|
```yaml
|
|
191
197
|
name: "[CMS] Upload resource templates"
|
|
@@ -204,17 +210,17 @@ jobs:
|
|
|
204
210
|
node-version: "16"
|
|
205
211
|
- name: Upload
|
|
206
212
|
run: |
|
|
207
|
-
npx --yes @ossy/cli
|
|
213
|
+
npx --yes @ossy/cli app upload \
|
|
208
214
|
--authentication ${{ secrets.OSSY_API_KEY }} \
|
|
209
215
|
--config src/config.js
|
|
210
216
|
```
|
|
211
217
|
|
|
212
|
-
###
|
|
218
|
+
### app validate
|
|
213
219
|
|
|
214
220
|
Validate an ossy config file before uploading:
|
|
215
221
|
|
|
216
222
|
```bash
|
|
217
|
-
ossy
|
|
223
|
+
ossy app validate --config src/config.js
|
|
218
224
|
```
|
|
219
225
|
|
|
220
226
|
When **`--config`** is omitted, **`./src/config.js`** is used if it exists.
|
|
@@ -227,6 +233,44 @@ When **`--config`** is omitted, **`./src/config.js`** is used if it exists.
|
|
|
227
233
|
| --config, -c | App config (`workspaceId`, `resourceTemplates`, …) | Optional if `./src/config.js` exists |
|
|
228
234
|
| --api-url | API base URL for upload (`…/api/v0`) | No |
|
|
229
235
|
|
|
236
|
+
## Upload a directory
|
|
237
|
+
|
|
238
|
+
Recursively mirrors a local directory tree into the Ossy CMS, preserving the folder structure.
|
|
239
|
+
|
|
240
|
+
```bash
|
|
241
|
+
ossy upload-dir ./public /my-folder
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
This:
|
|
245
|
+
1. Walks the local tree and collects all subdirectories (breadth-first) and files.
|
|
246
|
+
2. Creates each remote directory with `resources.create-directory` (parents before children; already-existing directories are skipped silently).
|
|
247
|
+
3. Uploads each file with `resources.upload` + S3 presigned PUT, in the same order they were discovered.
|
|
248
|
+
4. Logs per-item progress and prints a summary at the end.
|
|
249
|
+
|
|
250
|
+
Per-item errors are logged and skipped — the command continues to the next item and exits non-zero when any item failed.
|
|
251
|
+
|
|
252
|
+
**Options:**
|
|
253
|
+
|
|
254
|
+
| Flag | Description |
|
|
255
|
+
|------|-------------|
|
|
256
|
+
| `--dry-run` | Print what would happen without making any API calls |
|
|
257
|
+
| `-a, --authentication` | Ossy API JWT (or `OSSY_API_KEY`, or `ossy auth login`) |
|
|
258
|
+
| `-w, --workspace-id` | Workspace id (or `OSSY_WORKSPACE_ID`, or `ossy workspace use`) |
|
|
259
|
+
| `--api-url` | Override the API base URL |
|
|
260
|
+
|
|
261
|
+
**Examples:**
|
|
262
|
+
|
|
263
|
+
```bash
|
|
264
|
+
# Preview before committing
|
|
265
|
+
ossy upload-dir ./public /my-folder --dry-run
|
|
266
|
+
|
|
267
|
+
# Upload into a nested location
|
|
268
|
+
ossy upload-dir ./assets /projects/2026/assets
|
|
269
|
+
|
|
270
|
+
# Point at a local API
|
|
271
|
+
ossy upload-dir ./public /my-folder --api-url http://localhost:3001/api/v0
|
|
272
|
+
```
|
|
273
|
+
|
|
230
274
|
## Registry: ecr-push-credentials
|
|
231
275
|
|
|
232
276
|
Fetch a short-lived ECR password so CI can `docker login` and `docker push` to the workspace's ECR registry.
|
|
@@ -246,13 +290,13 @@ ossy call registry.ecr-push-credentials
|
|
|
246
290
|
|
|
247
291
|
…but you lose the GitHub Actions output formatting and the `::add-mask::` line, so prefer the verb in CI.
|
|
248
292
|
|
|
249
|
-
## init
|
|
293
|
+
## App: init
|
|
250
294
|
|
|
251
295
|
Scaffold a new Ossy app:
|
|
252
296
|
|
|
253
297
|
```bash
|
|
254
|
-
ossy init
|
|
255
|
-
ossy init my-app
|
|
298
|
+
ossy app init
|
|
299
|
+
ossy app init my-app
|
|
256
300
|
```
|
|
257
301
|
|
|
258
302
|
Creates `src/home.page.jsx`, `src/config.js`, and `package.json` (if missing).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ossy/cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.17.3",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "git+https://github.com/ossy-se/packages.git"
|
|
@@ -20,8 +20,8 @@
|
|
|
20
20
|
},
|
|
21
21
|
"dependencies": {
|
|
22
22
|
"@babel/parser": "^7.28.6",
|
|
23
|
-
"@ossy/app": "^1.
|
|
24
|
-
"@ossy/sdk": "^1.
|
|
23
|
+
"@ossy/app": "^1.17.3",
|
|
24
|
+
"@ossy/sdk": "^1.17.3",
|
|
25
25
|
"arg": "^5.0.2",
|
|
26
26
|
"glob": "^10.3.10"
|
|
27
27
|
},
|
|
@@ -33,5 +33,5 @@
|
|
|
33
33
|
"/src",
|
|
34
34
|
"README.md"
|
|
35
35
|
],
|
|
36
|
-
"gitHead": "
|
|
36
|
+
"gitHead": "f96e3c98e87f9a19b92f7eab0d54e4de12509007"
|
|
37
37
|
}
|
package/src/{cms → app}/cli.js
RENAMED
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
import { readFileSync, existsSync } from 'fs'
|
|
2
2
|
import { pathToFileURL } from 'url'
|
|
3
3
|
import arg from 'arg'
|
|
4
|
+
import { build } from '@ossy/app'
|
|
4
5
|
import { logInfo, logError } from '../log.js'
|
|
5
6
|
import { resolveAppConfigPath } from '../resolve-app-config-path.js'
|
|
6
7
|
import { getAuth } from '../state.js'
|
|
8
|
+
import { init } from '../init/cli.js'
|
|
7
9
|
import {
|
|
8
10
|
postResourceTemplates,
|
|
9
11
|
resolveApiBaseUrlForUpload,
|
|
10
12
|
} from './upload-resource-templates.js'
|
|
13
|
+
import { publish } from './publish.js'
|
|
11
14
|
|
|
12
15
|
const resolveConfigImport = (filePath) =>
|
|
13
16
|
filePath.endsWith('json')
|
|
@@ -150,19 +153,22 @@ const validate = (options) => {
|
|
|
150
153
|
}
|
|
151
154
|
|
|
152
155
|
const subcommands = {
|
|
156
|
+
init: (options) => init(options),
|
|
157
|
+
build: async (options) => { await build(options) },
|
|
153
158
|
upload,
|
|
154
159
|
validate,
|
|
160
|
+
publish: (options) => publish(options),
|
|
155
161
|
}
|
|
156
162
|
|
|
157
|
-
export const handler = ([subcommand, ...options]) => {
|
|
163
|
+
export const handler = async ([subcommand, ...options]) => {
|
|
158
164
|
if (!subcommand) {
|
|
159
|
-
logError({ message: '[@ossy/cli]
|
|
165
|
+
logError({ message: '[@ossy/cli] app: no subcommand. Use: ossy app init | build | upload | validate | publish' })
|
|
160
166
|
process.exit(1)
|
|
161
167
|
}
|
|
162
168
|
const fn = subcommands[subcommand]
|
|
163
169
|
if (!fn) {
|
|
164
|
-
logError({ message: `[@ossy/cli]
|
|
170
|
+
logError({ message: `[@ossy/cli] app: unknown subcommand "${subcommand}". Use: ossy app init | build | upload | validate | publish` })
|
|
165
171
|
process.exit(1)
|
|
166
172
|
}
|
|
167
|
-
fn(options)
|
|
173
|
+
await fn(options)
|
|
168
174
|
}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { readFileSync } from 'fs'
|
|
2
2
|
import { resolve } from 'path'
|
|
3
3
|
import { parse } from '@babel/parser'
|
|
4
|
-
import { pathToFileURL } from 'url'
|
|
5
4
|
|
|
6
5
|
/** @returns {unknown | undefined} `undefined` if the AST cannot be reduced to JSON-like data */
|
|
7
6
|
function astNodeToLiteralValue (node) {
|
|
@@ -58,7 +57,7 @@ function astNodeToLiteralValue (node) {
|
|
|
58
57
|
|
|
59
58
|
/**
|
|
60
59
|
* Reads `resourceTemplates` from `export default { ... }` when it is a literal array
|
|
61
|
-
* (no imports, calls, or templates). Matches typical
|
|
60
|
+
* (no imports, calls, or templates). Matches typical Ossy website configs.
|
|
62
61
|
*
|
|
63
62
|
* @param {string} source
|
|
64
63
|
* @returns {unknown[] | undefined}
|
|
@@ -98,16 +97,13 @@ function extractResourceTemplatesFromSource (source) {
|
|
|
98
97
|
}
|
|
99
98
|
|
|
100
99
|
/**
|
|
101
|
-
* Reads fields
|
|
102
|
-
* Avoids resolving imports such as `@ossy/themes` in CI
|
|
103
|
-
*
|
|
104
|
-
* **Temporary** — prefer a build-time `publish-meta.json` or platform events once deploy is decoupled
|
|
105
|
-
* from the CLI; see `packages/cli/README.md` → *Publish* → *Temporary: no execution of src/config.js*.
|
|
100
|
+
* Reads fields from `src/config.js` **without executing** it.
|
|
101
|
+
* Avoids resolving imports such as `@ossy/themes` in CI.
|
|
106
102
|
*
|
|
107
103
|
* @param {string} configPath Absolute or cwd-relative path to `src/config.js`
|
|
108
|
-
* @returns {{ workspaceId?: string, apiUrl?: string, resourceTemplates?: unknown[] }}
|
|
104
|
+
* @returns {{ workspaceId?: string, apiUrl?: string, domain?: string, resourceTemplates?: unknown[] }}
|
|
109
105
|
*/
|
|
110
|
-
export function
|
|
106
|
+
export function readAppConfig (configPath) {
|
|
111
107
|
const abs = resolve(configPath)
|
|
112
108
|
const source = readFileSync(abs, 'utf8')
|
|
113
109
|
|
|
@@ -126,17 +122,7 @@ export function readPublishFieldsFromWebsiteConfig (configPath) {
|
|
|
126
122
|
return {
|
|
127
123
|
workspaceId: pickString('workspaceId'),
|
|
128
124
|
apiUrl: pickString('apiUrl'),
|
|
125
|
+
domain: pickString('domain'),
|
|
129
126
|
resourceTemplates,
|
|
130
127
|
}
|
|
131
128
|
}
|
|
132
|
-
|
|
133
|
-
/**
|
|
134
|
-
* @deprecated Prefer {@link readPublishFieldsFromWebsiteConfig} for publish — dynamic import
|
|
135
|
-
* runs all side effects and `import`s in config (e.g. `@ossy/themes`), which breaks in plain Node.
|
|
136
|
-
* @param {string} configPath Absolute or cwd-relative path to config file
|
|
137
|
-
*/
|
|
138
|
-
export async function loadWebsiteConfig (configPath) {
|
|
139
|
-
const abs = resolve(configPath)
|
|
140
|
-
const mod = await import(pathToFileURL(abs).href)
|
|
141
|
-
return mod.default ?? mod
|
|
142
|
-
}
|
|
@@ -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/call/cli.js
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
import { readFile } from 'node:fs/promises'
|
|
2
1
|
import * as Sdk from '@ossy/sdk'
|
|
3
2
|
import { logError } from '../log.js'
|
|
4
3
|
import { getAuth, getApiUrl, getWorkspaceId } from '../state.js'
|
|
5
|
-
import { prepareFileUpload } from '../file.js'
|
|
4
|
+
import { prepareFileUpload, uploadFileToUrl } from '../file.js'
|
|
6
5
|
|
|
7
6
|
const ACTIONS = Object.values(Sdk).filter(
|
|
8
7
|
(v) =>
|
|
@@ -284,22 +283,7 @@ async function call (args) {
|
|
|
284
283
|
const uploadUrl = result && result.content && result.content.uploadUrl
|
|
285
284
|
if (uploadUrl) {
|
|
286
285
|
try {
|
|
287
|
-
|
|
288
|
-
const putRes = await fetch(uploadUrl, {
|
|
289
|
-
method: 'PUT',
|
|
290
|
-
headers: {
|
|
291
|
-
'Content-Type': fileMeta.type,
|
|
292
|
-
'Content-Length': String(fileMeta.size),
|
|
293
|
-
},
|
|
294
|
-
body,
|
|
295
|
-
})
|
|
296
|
-
if (!putRes.ok) {
|
|
297
|
-
const t = await putRes.text().catch(() => '')
|
|
298
|
-
process.stderr.write(
|
|
299
|
-
`[@ossy/cli] call: upload PUT failed: HTTP ${putRes.status}${t ? ' — ' + t.slice(0, 200) : ''}\n`
|
|
300
|
-
)
|
|
301
|
-
process.exit(1)
|
|
302
|
-
}
|
|
286
|
+
await uploadFileToUrl(fileMeta, uploadUrl)
|
|
303
287
|
} catch (err) {
|
|
304
288
|
process.stderr.write(`[@ossy/cli] call: upload PUT failed: ${err.message || err}\n`)
|
|
305
289
|
process.exit(1)
|
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.
|