@ossy/cli 0.16.1 → 0.16.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 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 to your workspace |
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> --ossy-file ossy.json
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`): `--config` app config (`src/config.js` by default)
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
- --ossy-file ossy.json
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 --ossy-file ossy.json
95
+ npx @ossy/cli cms validate --config src/config.js
88
96
  ```
89
97
 
90
- Defaults to `ossy.json` in the current directory when `--ossy-file` is omitted.
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
- | --ossy-file | Path to file with `workspaceId` and `resourceTemplates` | Yes (upload), optional (validate) |
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.1",
3
+ "version": "0.16.3",
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.1",
20
+ "@ossy/app": "^0.15.3",
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": "19917b325524994f304813ba9d81e52a79da544e"
32
+ "gitHead": "2f021c5267eda824587a8670dc51507ce48720e0"
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
- const Api = {
7
- uploadResourceTemplates: (apiUrl, token, workspaceId, resourceTemplates) => {
8
- const endpoint = `${apiUrl}/workspaces/${workspaceId}/resource-templates`
9
- const fetchOptions = {
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
- '--ossy-file': String,
20
+ '--config': String,
21
+ '-c': '--config',
22
+ '--api-url': String,
28
23
  }, { argv: options })
29
24
 
30
25
  const token = parsedArgs['--authentication']
31
- const ossyFilePath = parsedArgs['--ossy-file']
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
- if (!ossyFilePath) {
38
- logError({ message: '[@ossy/cli] No config file provided. Use --ossy-file' })
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 ossy file' })
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 ossy file' })
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 Api.uploadResourceTemplates(apiUrl, token, workspaceId, resourceTemplates)
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
- '--ossy-file': String,
94
+ '--config': String,
95
+ '-c': '--config',
82
96
  }, { argv: options })
83
97
 
84
- const ossyFilePath = parsedArgs['--ossy-file']
85
- const filePath = ossyFilePath ? resolve(ossyFilePath) : resolve('ossy.json')
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] File not found: ${filePath}. Use --ossy-file` })
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('[@ossy/cli] No command provided. Usage: ossy dev | build | publish | init | cms <subcommand>')
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
 
@@ -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
- let configPath = parsedArgs['--config']
68
- if (!configPath) {
69
- const cwdConfig = pathResolve(process.cwd(), 'src/config.js')
70
- if (existsSync(cwdConfig)) {
71
- configPath = cwdConfig
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.