@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 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. `init`, `build`, `auth`, `workspace`, `cms validate`.
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 | `cms validate` | Validate `src/config.js` (`workspaceId`, `resourceTemplates`) without calling the API |
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 | `cms upload` | Read `resourceTemplates` from `src/config.js` and call `workspaces.import-resource-templates` |
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
- ## Publish (container / website)
111
+ ## App: publish
111
112
 
112
- Publishes a site by sending a deployment request to your platform queue (same as `npx @ossy/deployment-tools deployment deploy`). Run from the **website package** directory (where `src/config.js` lives) so domain/platform can be read automatically.
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
- **This is a multi-step workflow, not a single SDK action.** It shells out to `@ossy/deployment-tools` for the deploy and then performs two CMS uploads (resource templates → `workspaces.import-resource-templates`; site build artifacts → `/site-artifacts/presign-batch` + `/site-artifacts/commit-batch`). The deploy step is **temporary** — see *Future direction* below.
115
-
116
- ### Temporary: no execution of `src/config.js` for CMS steps
117
-
118
- After deploy succeeds, **`publish`** still needs **`workspaceId`**, **`apiUrl`** (optional, for absolute URLs), and **`resourceTemplates`** from `src/config.js`. Those values are **not** loaded with `import()` anymore: running the real config in plain Node would execute imports such as `@ossy/themes`, which are only meant to be resolved during **`ossy build`** (Rollup).
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
- If `resourceTemplates` cannot be extracted, template upload is skipped. This is a **stopgap** until publish is driven by build output or platform events (see **Future direction**).
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
- ### Future direction (planned)
125
+ ### Config extraction
128
126
 
129
- The explicit **`deployment deploy`** step (SQS / deployment queue from **`@ossy/deployment-tools`**) is **intended to go away**: the platform should **react to a website upload / artifact event** (e.g. after the container image or site bundle is published) and roll out without the CLI calling deploy. When that exists, **`publish`** can shrink to CMS-only steps (resource templates + site artifacts), or those can move to separate workflows entirely. Treat the current **deploy + follow-ups** combo as **temporary** coupling.
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
- **`publish`** requires **`--authentication` / `-a`** or **`OSSY_API_KEY`**: the **Ossy API JWT** (workspace API token). That same value is used for post-deploy CMS calls (resource templates, site artifacts).
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> # or pass --authentication <ossy-api-jwt>
142
-
143
- ossy publish \
144
- --platforms-path ../infrastructure/platforms.json \
145
- --deployments-path "../infrastructure/deployments/**/*.json"
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
- **Non-ECR container registries** are not supported; container rows in **`deployments.json`** must use **ECR** and a **`registry`** endpoint as described above.
147
+ ### Options
149
148
 
150
- API and worker packages can use the same pattern: a minimal **`src/config.js`** with string-literal **`domain`** (and optional **`platform`**) matching **`deployments.json`**, then run **`publish`** from **`packages/api`** or **`packages/worker`** with **`--skip-resource-templates`** and **`--skip-site-artifacts`** (no website `workspaceId` / `build/` flow).
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
- - **`--domain` / `--platform`** — Optional if `src/config.js` contains string literals `domain: '…'` and `platform: '…'` (or `targetDeploymentPlatform`).
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
- Requires network access so `npx` can run `@ossy/deployment-tools`.
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
- ## CMS: upload
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 cms upload --authentication <ossy-api-jwt> --config src/config.js
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`), **CMS** (`cms upload` / `cms validate`), and **publish** all use **`--config`** (`-c`) for the app / workspace config file (`src/config.js` by default when present).
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 deploy and template upload run together (see **Publish** above). For template-only updates, `cms upload` still works.
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 cms upload \
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
- ### cms validate
218
+ ### app validate
213
219
 
214
220
  Validate an ossy config file before uploading:
215
221
 
216
222
  ```bash
217
- ossy cms validate --config src/config.js
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.16.11",
3
+ "version": "1.17.4",
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.16.11",
24
- "@ossy/sdk": "^1.16.11",
23
+ "@ossy/app": "^1.17.4",
24
+ "@ossy/sdk": "^1.17.4",
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": "17a1a491a5239502f989b3745dab7a6c8ae17076"
36
+ "gitHead": "45251429b7b1f4cdf7f81a5046419905c3c118c9"
37
37
  }
@@ -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] cms: no subcommand. Use: ossy cms upload | validate' })
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] cms: unknown subcommand "${subcommand}". Use: ossy cms upload | validate` })
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 OSSY website configs.
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 needed for `ossy publish` follow-up steps **without executing** `config.js`.
102
- * Avoids resolving imports such as `@ossy/themes` in CI (publish runs after deploy, outside Rollup).
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 readPublishFieldsFromWebsiteConfig (configPath) {
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,245 @@
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
+ ? `/@ossy/apps/${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
+ // Ensure the full remoteDest path exists (e.g. /sites then /sites/ossy.se)
138
+ const destSegments = remoteDest.replace(/^\//, '').split('/')
139
+ for (let i = 0; i < destSegments.length; i++) {
140
+ const parent = i === 0 ? '/' : '/' + destSegments.slice(0, i).join('/')
141
+ const name = destSegments[i]
142
+ try {
143
+ await sdk.makeRequest(createDir)({ location: parent, name })
144
+ } catch (err) {
145
+ const status = err && err.status
146
+ if (status !== 409 && status !== 422) {
147
+ logError({
148
+ message: `[@ossy/cli] app publish: mkdir failed for ${parent}/${name}: ${err?.message ?? err}`,
149
+ })
150
+ errors++
151
+ }
152
+ }
153
+ }
154
+
155
+ // Create all directories first (sequential; parents must exist before children)
156
+ for (const d of dirs) {
157
+ const rel = d.slice(buildDir.length)
158
+ const parent = normaliseLoc(remoteDest + path.dirname(rel))
159
+ const name = path.basename(d)
160
+ try {
161
+ await sdk.makeRequest(createDir)({ location: parent, name })
162
+ } catch (err) {
163
+ const status = err && err.status
164
+ if (status !== 409 && status !== 422) {
165
+ logError({
166
+ message: `[@ossy/cli] app publish: mkdir failed for ${parent}/${name}: ${err?.message ?? err}`,
167
+ })
168
+ errors++
169
+ }
170
+ }
171
+ }
172
+
173
+ // Upload files in parallel with a concurrency limit
174
+ const CONCURRENCY = 10
175
+ let completed = 0
176
+
177
+ async function uploadOne (f) {
178
+ const rel = f.slice(buildDir.length)
179
+ const remoteDir = normaliseLoc(remoteDest + path.dirname(rel))
180
+
181
+ let fileMeta
182
+ try {
183
+ fileMeta = prepareFileUpload(f)
184
+ } catch (err) {
185
+ logError({ message: `[@ossy/cli] app publish: cannot read file ${f}: ${err.message}` })
186
+ errors++
187
+ return
188
+ }
189
+
190
+ let result
191
+ try {
192
+ result = await sdk.makeRequest(uploadAction)({
193
+ location: remoteDir,
194
+ name: fileMeta.name,
195
+ type: fileMeta.type,
196
+ size: fileMeta.size,
197
+ })
198
+ } catch (err) {
199
+ logError({ message: `[@ossy/cli] app publish: upload failed for ${f}: ${err?.message ?? err}` })
200
+ errors++
201
+ return
202
+ }
203
+
204
+ const uploadUrl = result?.content?.uploadUrl
205
+ if (!uploadUrl) {
206
+ logError({ message: `[@ossy/cli] app publish: no uploadUrl in response for ${f}` })
207
+ errors++
208
+ return
209
+ }
210
+
211
+ try {
212
+ await uploadFileToUrl(fileMeta, uploadUrl)
213
+ } catch (err) {
214
+ logError({ message: `[@ossy/cli] app publish: S3 PUT failed for ${f}: ${err.message}` })
215
+ errors++
216
+ return
217
+ }
218
+
219
+ completed++
220
+ if (completed % 50 === 0 || completed === files.length) {
221
+ logInfo({ message: `[@ossy/cli] app publish: ${completed}/${files.length} files uploaded…` })
222
+ }
223
+ }
224
+
225
+ // Run uploadOne over all files with max CONCURRENCY in-flight at once
226
+ const queue = [...files]
227
+ const workers = Array.from({ length: Math.min(CONCURRENCY, files.length) }, async () => {
228
+ while (queue.length > 0) {
229
+ const f = queue.shift()
230
+ if (f) await uploadOne(f)
231
+ }
232
+ })
233
+ await Promise.all(workers)
234
+
235
+ if (errors > 0) {
236
+ logError({
237
+ message: `[@ossy/cli] app publish: done with ${errors} error${errors === 1 ? '' : 's'}`,
238
+ })
239
+ process.exit(1)
240
+ }
241
+
242
+ logInfo({
243
+ message: `[@ossy/cli] app publish: done — ${files.length} file${files.length === 1 ? '' : 's'} uploaded to ${remoteDest}`,
244
+ })
245
+ }
@@ -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 requireCmsAuthentication (token, label) {
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
- const body = await readFile(fileMeta.absPath)
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)