@ossy/cli 1.16.9 → 1.16.11
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 +129 -15
- package/package.json +7 -4
- package/src/auth/cli.js +135 -0
- package/src/call/cli.js +327 -0
- package/src/cms/cli.js +3 -3
- package/src/file.js +73 -0
- package/src/index.js +143 -6
- package/src/publish/cli.js +3 -2
- package/src/publish/site-artifacts-after-publish.js +2 -29
- package/src/registry/ecr-push-credentials.js +7 -6
- package/src/state.js +115 -0
- package/src/workspace/cli.js +84 -0
package/README.md
CHANGED
|
@@ -1,21 +1,108 @@
|
|
|
1
1
|
# @ossy/cli
|
|
2
2
|
|
|
3
|
-
Unified CLI for the Ossy platform
|
|
3
|
+
Unified CLI for the Ossy platform.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## Install
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g @ossy/cli
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
After install, the `ossy` command is on your PATH:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
ossy --version
|
|
15
|
+
ossy --help
|
|
16
|
+
ossy <command> --help
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
For one-off use without installing, you can still run `npx @ossy/cli <command>`.
|
|
20
|
+
|
|
21
|
+
## How the CLI is organized
|
|
22
|
+
|
|
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
|
+
|
|
25
|
+
1. **Local utilities** — no API call, or per-machine state. `init`, `build`, `auth`, `workspace`, `cms validate`.
|
|
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
|
+
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
|
+
|
|
29
|
+
| Layer | Command | Description |
|
|
30
|
+
|-------|---------|-------------|
|
|
31
|
+
| Local | `auth login \| logout \| status` | Save / delete / inspect the Ossy API token under `~/.config/ossy/credentials.json` |
|
|
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 |
|
|
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` |
|
|
38
|
+
| 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
|
+
If you need an endpoint that no verb wraps, reach for `ossy call` — it's the universal channel.
|
|
42
|
+
|
|
43
|
+
## Auth and active workspace
|
|
44
|
+
|
|
45
|
+
Once installed, the recommended flow is to log in once and pick an active workspace; subsequent commands inherit those:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
ossy auth login # paste your Ossy API token
|
|
49
|
+
ossy workspace list # see workspaces you can access
|
|
50
|
+
ossy workspace use <workspace-id> # set the active one
|
|
51
|
+
ossy auth status # confirm
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Credentials live in `~/.config/ossy/credentials.json` (`chmod 600`); the active workspace lives in `~/.config/ossy/config.json`. Both honor `XDG_CONFIG_HOME`.
|
|
55
|
+
|
|
56
|
+
Resolution order for every command that needs auth:
|
|
57
|
+
1. `--authentication` / `-a` flag
|
|
58
|
+
2. `OSSY_API_KEY` env var
|
|
59
|
+
3. Saved credentials
|
|
60
|
+
|
|
61
|
+
Resolution order for the workspace id (when a command isn't reading it from `src/config.js`):
|
|
62
|
+
1. `--workspace-id` / `-w` flag
|
|
63
|
+
2. `OSSY_WORKSPACE_ID` env var
|
|
64
|
+
3. Saved active workspace
|
|
65
|
+
|
|
66
|
+
So existing CI scripts that set `OSSY_API_KEY` keep working unchanged; you only need `ossy auth login` for interactive use.
|
|
67
|
+
|
|
68
|
+
## Generic dispatcher: `ossy call`
|
|
69
|
+
|
|
70
|
+
`ossy call` invokes any Ossy API action by id. The action catalog comes from `@ossy/sdk`, so any new endpoint added to the SDK is callable from the terminal with no extra wiring.
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
# Discover what's available
|
|
74
|
+
ossy call --help # full catalog, grouped by domain
|
|
75
|
+
ossy call resources.create --help # one action's method, endpoint, default payload
|
|
76
|
+
|
|
77
|
+
# Read calls
|
|
78
|
+
ossy call workspaces.get-current
|
|
79
|
+
ossy call resources.list --search 'location=/docs'
|
|
80
|
+
|
|
81
|
+
# Write calls — flag names map to payload keys (--first-name -> firstName)
|
|
82
|
+
ossy call resources.create-directory --location /docs --name images
|
|
83
|
+
ossy call workspaces.invite-user --email teammate@example.com
|
|
84
|
+
|
|
85
|
+
# Use --json for nested or non-string payloads
|
|
86
|
+
ossy call resources.update-content --id res_abc --json '{"content":{"title":"Hi"}}'
|
|
87
|
+
|
|
88
|
+
# Upload a file: --file fills name/type/size as defaults, then PUTs the bytes
|
|
89
|
+
# when the response carries content.uploadUrl (works for resources.upload,
|
|
90
|
+
# resources.upload-named-version, etc.)
|
|
91
|
+
ossy call resources.upload --location /docs --file ./hero.jpg
|
|
92
|
+
ossy call resources.upload-named-version --id res_abc --name medium --file ./hero.jpg
|
|
93
|
+
|
|
94
|
+
# Per-call overrides (otherwise resolved from `ossy auth login` / `ossy workspace use`)
|
|
95
|
+
ossy call workspaces.get-all -a <jwt> --api-url http://localhost:3001/api/v0
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Output is JSON pretty-printed to stdout on success. Errors go to stderr with a non-zero exit. Add `--raw` to get the response body verbatim (useful for piping non-JSON responses).
|
|
99
|
+
|
|
100
|
+
`--file <path>` is a shortcut for upload-style actions: the CLI stats the file, fills `name`/`type`/`size` *as defaults* (any explicit `--name` / `--type` / `--size` flag wins), and after the dispatch it PUTs the bytes to `result.content.uploadUrl` with the right `Content-Type`/`Content-Length`. If the response doesn't include an upload URL, the bytes are skipped and a warning is printed.
|
|
14
101
|
|
|
15
102
|
## App: build
|
|
16
103
|
|
|
17
104
|
```bash
|
|
18
|
-
|
|
105
|
+
ossy build
|
|
19
106
|
```
|
|
20
107
|
|
|
21
108
|
Options: e.g. `--config` for `src/config.js`. See `packages/app/README.md` for app build behavior.
|
|
@@ -24,6 +111,8 @@ Options: e.g. `--config` for `src/config.js`. See `packages/app/README.md` for a
|
|
|
24
111
|
|
|
25
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.
|
|
26
113
|
|
|
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
|
+
|
|
27
116
|
### Temporary: no execution of `src/config.js` for CMS steps
|
|
28
117
|
|
|
29
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).
|
|
@@ -51,7 +140,7 @@ Container deploys assume **Amazon ECR** in **`deployments.json`** (`registry` li
|
|
|
51
140
|
cd packages/my-website
|
|
52
141
|
export OSSY_API_KEY=<ossy-api-jwt> # or pass --authentication <ossy-api-jwt>
|
|
53
142
|
|
|
54
|
-
|
|
143
|
+
ossy publish \
|
|
55
144
|
--platforms-path ../infrastructure/platforms.json \
|
|
56
145
|
--deployments-path "../infrastructure/deployments/**/*.json"
|
|
57
146
|
```
|
|
@@ -74,10 +163,16 @@ Requires network access so `npx` can run `@ossy/deployment-tools`.
|
|
|
74
163
|
|
|
75
164
|
Upload resource templates to your workspace so they can be used in the UI.
|
|
76
165
|
|
|
166
|
+
Wraps the SDK action **`workspaces.import-resource-templates`** and adds: reading `resourceTemplates` and `workspaceId` from `src/config.js` (so you don't have to inline the templates JSON yourself). If you already have the templates as a JSON object, the equivalent dispatcher call is:
|
|
167
|
+
|
|
168
|
+
```bash
|
|
169
|
+
ossy call workspaces.import-resource-templates --json '{"templates":[…]}'
|
|
170
|
+
```
|
|
171
|
+
|
|
77
172
|
Prefer **`--authentication` / `-a`** for the **Ossy API JWT** (same as **`publish`**); it matches **`OSSY_API_KEY`** in CI.
|
|
78
173
|
|
|
79
174
|
```bash
|
|
80
|
-
|
|
175
|
+
ossy cms upload --authentication <ossy-api-jwt> --config src/config.js
|
|
81
176
|
# optional: --api-url https://api.ossy.se/api/v0 (or set OSSY_API_URL)
|
|
82
177
|
# In CI you may omit --authentication when OSSY_API_KEY is set
|
|
83
178
|
```
|
|
@@ -119,7 +214,7 @@ jobs:
|
|
|
119
214
|
Validate an ossy config file before uploading:
|
|
120
215
|
|
|
121
216
|
```bash
|
|
122
|
-
|
|
217
|
+
ossy cms validate --config src/config.js
|
|
123
218
|
```
|
|
124
219
|
|
|
125
220
|
When **`--config`** is omitted, **`./src/config.js`** is used if it exists.
|
|
@@ -132,13 +227,32 @@ When **`--config`** is omitted, **`./src/config.js`** is used if it exists.
|
|
|
132
227
|
| --config, -c | App config (`workspaceId`, `resourceTemplates`, …) | Optional if `./src/config.js` exists |
|
|
133
228
|
| --api-url | API base URL for upload (`…/api/v0`) | No |
|
|
134
229
|
|
|
230
|
+
## Registry: ecr-push-credentials
|
|
231
|
+
|
|
232
|
+
Fetch a short-lived ECR password so CI can `docker login` and `docker push` to the workspace's ECR registry.
|
|
233
|
+
|
|
234
|
+
Wraps the SDK action **`registry.ecr-push-credentials`** and adds: response validation, JSON pretty-printing, and a `--format github-actions` mode that writes `registry`/`username`/`password`/`expires_at` to `$GITHUB_OUTPUT` and emits `::add-mask::<password>` so the secret never appears in logs.
|
|
235
|
+
|
|
236
|
+
```bash
|
|
237
|
+
ossy registry ecr-push-credentials # JSON to stdout
|
|
238
|
+
ossy registry ecr-push-credentials --format github-actions # writes to $GITHUB_OUTPUT
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
Auth and workspace are resolved from the usual chain (`-a` flag → `OSSY_API_KEY` → saved credentials; `-w` flag → `OSSY_WORKSPACE_ID` → saved active workspace). The plain dispatcher equivalent is:
|
|
242
|
+
|
|
243
|
+
```bash
|
|
244
|
+
ossy call registry.ecr-push-credentials
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
…but you lose the GitHub Actions output formatting and the `::add-mask::` line, so prefer the verb in CI.
|
|
248
|
+
|
|
135
249
|
## init
|
|
136
250
|
|
|
137
251
|
Scaffold a new Ossy app:
|
|
138
252
|
|
|
139
253
|
```bash
|
|
140
|
-
|
|
141
|
-
|
|
254
|
+
ossy init
|
|
255
|
+
ossy init my-app
|
|
142
256
|
```
|
|
143
257
|
|
|
144
258
|
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.
|
|
3
|
+
"version": "1.16.11",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "git+https://github.com/ossy-se/packages.git"
|
|
@@ -15,10 +15,13 @@
|
|
|
15
15
|
},
|
|
16
16
|
"author": "Ossy <yourfriends@ossy.se> (https://ossy.se)",
|
|
17
17
|
"license": "MIT",
|
|
18
|
-
"bin":
|
|
18
|
+
"bin": {
|
|
19
|
+
"ossy": "./src/index.js"
|
|
20
|
+
},
|
|
19
21
|
"dependencies": {
|
|
20
22
|
"@babel/parser": "^7.28.6",
|
|
21
|
-
"@ossy/app": "^1.16.
|
|
23
|
+
"@ossy/app": "^1.16.11",
|
|
24
|
+
"@ossy/sdk": "^1.16.11",
|
|
22
25
|
"arg": "^5.0.2",
|
|
23
26
|
"glob": "^10.3.10"
|
|
24
27
|
},
|
|
@@ -30,5 +33,5 @@
|
|
|
30
33
|
"/src",
|
|
31
34
|
"README.md"
|
|
32
35
|
],
|
|
33
|
-
"gitHead": "
|
|
36
|
+
"gitHead": "17a1a491a5239502f989b3745dab7a6c8ae17076"
|
|
34
37
|
}
|
package/src/auth/cli.js
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import readline from 'node:readline/promises'
|
|
2
|
+
import { stdin as input, stdout as output } from 'node:process'
|
|
3
|
+
import arg from 'arg'
|
|
4
|
+
import { logInfo, logError } from '../log.js'
|
|
5
|
+
import {
|
|
6
|
+
readCredentials,
|
|
7
|
+
writeCredentials,
|
|
8
|
+
deleteCredentials,
|
|
9
|
+
getAuth,
|
|
10
|
+
getApiUrl,
|
|
11
|
+
getWorkspaceId,
|
|
12
|
+
describeStatePaths,
|
|
13
|
+
} from '../state.js'
|
|
14
|
+
|
|
15
|
+
function stripBearer (t) {
|
|
16
|
+
return String(t ?? '').trim().replace(/^Bearer\s+/i, '').trim()
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async function promptHidden (question) {
|
|
20
|
+
// Tokens contain dots; avoid leaking partial input in shell history. Echo is left on
|
|
21
|
+
// because Node's tty masking is fiddly across platforms — paste-and-press-enter is
|
|
22
|
+
// the documented flow. We could revisit with a real prompt lib later.
|
|
23
|
+
const rl = readline.createInterface({ input, output })
|
|
24
|
+
try {
|
|
25
|
+
return (await rl.question(question)).trim()
|
|
26
|
+
} finally {
|
|
27
|
+
rl.close()
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function login (options) {
|
|
32
|
+
const parsed = arg({
|
|
33
|
+
'--token': String,
|
|
34
|
+
'-t': '--token',
|
|
35
|
+
'--api-url': String,
|
|
36
|
+
}, { argv: options })
|
|
37
|
+
|
|
38
|
+
let token = parsed['--token']
|
|
39
|
+
if (!token) {
|
|
40
|
+
if (!input.isTTY) {
|
|
41
|
+
logError({
|
|
42
|
+
message:
|
|
43
|
+
'[@ossy/cli] auth login: pass --token <jwt> or run interactively in a TTY.',
|
|
44
|
+
})
|
|
45
|
+
process.exit(1)
|
|
46
|
+
}
|
|
47
|
+
token = await promptHidden('Paste your Ossy API token: ')
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const normalized = stripBearer(token)
|
|
51
|
+
if (!normalized) {
|
|
52
|
+
logError({ message: '[@ossy/cli] auth login: empty token.' })
|
|
53
|
+
process.exit(1)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const apiUrl = getApiUrl({ flag: parsed['--api-url'] })
|
|
57
|
+
|
|
58
|
+
let me = null
|
|
59
|
+
try {
|
|
60
|
+
const res = await fetch(`${apiUrl}/users/me`, {
|
|
61
|
+
headers: { Authorization: normalized, 'Content-Type': 'application/json' },
|
|
62
|
+
})
|
|
63
|
+
if (!res.ok) {
|
|
64
|
+
logError({
|
|
65
|
+
message: `[@ossy/cli] auth login: token rejected by ${apiUrl}/users/me (HTTP ${res.status})`,
|
|
66
|
+
})
|
|
67
|
+
process.exit(1)
|
|
68
|
+
}
|
|
69
|
+
me = await res.json().catch(() => null)
|
|
70
|
+
} catch (error) {
|
|
71
|
+
logError({ message: `[@ossy/cli] auth login: could not reach ${apiUrl}`, error })
|
|
72
|
+
process.exit(1)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
writeCredentials({ apiUrl, token: normalized })
|
|
76
|
+
logInfo({
|
|
77
|
+
message: `[@ossy/cli] Logged in as ${me?.email || me?.id || 'user'} on ${apiUrl}`,
|
|
78
|
+
})
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function logout () {
|
|
82
|
+
deleteCredentials()
|
|
83
|
+
logInfo({ message: '[@ossy/cli] Logged out' })
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async function status () {
|
|
87
|
+
const creds = readCredentials()
|
|
88
|
+
const paths = describeStatePaths()
|
|
89
|
+
const apiUrl = getApiUrl()
|
|
90
|
+
const workspaceId = getWorkspaceId()
|
|
91
|
+
|
|
92
|
+
if (!creds?.token) {
|
|
93
|
+
console.log(
|
|
94
|
+
`Not logged in. Run \`ossy auth login\` to save credentials at ${paths.credentials}.`
|
|
95
|
+
)
|
|
96
|
+
return
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const auth = getAuth()
|
|
100
|
+
let userInfo = null
|
|
101
|
+
let live = false
|
|
102
|
+
try {
|
|
103
|
+
const res = await fetch(`${apiUrl}/users/me`, {
|
|
104
|
+
headers: { Authorization: auth, 'Content-Type': 'application/json' },
|
|
105
|
+
})
|
|
106
|
+
if (res.ok) {
|
|
107
|
+
userInfo = await res.json().catch(() => null)
|
|
108
|
+
live = true
|
|
109
|
+
}
|
|
110
|
+
} catch {
|
|
111
|
+
// network failure → just report offline status
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
console.log(`API URL: ${apiUrl}`)
|
|
115
|
+
console.log(
|
|
116
|
+
`User: ${live ? (userInfo?.email || userInfo?.id || 'unknown') : '(token rejected — try `ossy auth login`)'}`
|
|
117
|
+
)
|
|
118
|
+
console.log(
|
|
119
|
+
`Workspace: ${workspaceId || '(none — run `ossy workspace use <id>`)'}`
|
|
120
|
+
)
|
|
121
|
+
console.log(`Creds file: ${paths.credentials}`)
|
|
122
|
+
console.log(`Config: ${paths.config}`)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export async function handler (args) {
|
|
126
|
+
const [sub, ...rest] = args
|
|
127
|
+
if (sub === 'login') return await login(rest)
|
|
128
|
+
if (sub === 'logout') return logout()
|
|
129
|
+
if (sub === 'status') return await status()
|
|
130
|
+
logError({
|
|
131
|
+
message:
|
|
132
|
+
'[@ossy/cli] auth: unknown subcommand. Use: ossy auth login | logout | status',
|
|
133
|
+
})
|
|
134
|
+
process.exit(1)
|
|
135
|
+
}
|
package/src/call/cli.js
ADDED
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises'
|
|
2
|
+
import * as Sdk from '@ossy/sdk'
|
|
3
|
+
import { logError } from '../log.js'
|
|
4
|
+
import { getAuth, getApiUrl, getWorkspaceId } from '../state.js'
|
|
5
|
+
import { prepareFileUpload } from '../file.js'
|
|
6
|
+
|
|
7
|
+
const ACTIONS = Object.values(Sdk).filter(
|
|
8
|
+
(v) =>
|
|
9
|
+
v
|
|
10
|
+
&& typeof v === 'object'
|
|
11
|
+
&& typeof v.id === 'string'
|
|
12
|
+
&& typeof v.endpoint === 'string'
|
|
13
|
+
&& typeof v.method === 'string'
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
const ACTION_BY_ID = new Map(ACTIONS.map((a) => [a.id, a]))
|
|
17
|
+
|
|
18
|
+
function listActionsByDomain () {
|
|
19
|
+
const grouped = new Map()
|
|
20
|
+
for (const action of ACTIONS) {
|
|
21
|
+
const domain = action.id.split('.')[0]
|
|
22
|
+
if (!grouped.has(domain)) grouped.set(domain, [])
|
|
23
|
+
grouped.get(domain).push(action.id)
|
|
24
|
+
}
|
|
25
|
+
for (const [, ids] of grouped) ids.sort()
|
|
26
|
+
return new Map([...grouped.entries()].sort(([a], [b]) => a.localeCompare(b)))
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function buildHelp () {
|
|
30
|
+
const grouped = listActionsByDomain()
|
|
31
|
+
const blocks = []
|
|
32
|
+
for (const [domain, ids] of grouped) {
|
|
33
|
+
blocks.push(` ${domain}\n${ids.map((id) => ` ${id}`).join('\n')}`)
|
|
34
|
+
}
|
|
35
|
+
return `ossy call <action.id> [--key value]... [--json '{...}'] [options]
|
|
36
|
+
|
|
37
|
+
Generic dispatcher for any Ossy API action. Flags become payload fields
|
|
38
|
+
(--first-name -> firstName); --json '{...}' is merged on top.
|
|
39
|
+
|
|
40
|
+
Options:
|
|
41
|
+
--json '{...}' Provide payload as JSON (merged with --key value flags)
|
|
42
|
+
--file <path> Attach a local file: derives name/type/size as defaults
|
|
43
|
+
and PUTs the bytes if the response returns content.uploadUrl
|
|
44
|
+
-w, --workspace-id Override active workspace id (sent as workspaceId header)
|
|
45
|
+
-a, --authentication Override API token (otherwise resolved from state/env)
|
|
46
|
+
--api-url Override API base URL
|
|
47
|
+
--raw Print response body without JSON pretty-printing
|
|
48
|
+
|
|
49
|
+
Per-action help:
|
|
50
|
+
ossy call <action.id> --help
|
|
51
|
+
|
|
52
|
+
Available actions:
|
|
53
|
+
|
|
54
|
+
${blocks.join('\n\n')}
|
|
55
|
+
`
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function actionHelp (action) {
|
|
59
|
+
const def = action.payload ? `Default payload:\n ${JSON.stringify(action.payload)}\n\n` : ''
|
|
60
|
+
return `${action.id} ${action.method} ${action.endpoint}
|
|
61
|
+
|
|
62
|
+
${def}Examples:
|
|
63
|
+
ossy call ${action.id} --key value --other value
|
|
64
|
+
ossy call ${action.id} --json '{"key":"value"}'
|
|
65
|
+
`
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function kebabToCamel (str) {
|
|
69
|
+
return str.replace(/-([a-z])/g, (_, c) => c.toUpperCase())
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const RESERVED_FLAGS_NEEDING_VALUE = new Set([
|
|
73
|
+
'--json',
|
|
74
|
+
'--workspace-id', '-w',
|
|
75
|
+
'--authentication', '-a',
|
|
76
|
+
'--api-url',
|
|
77
|
+
'--file',
|
|
78
|
+
])
|
|
79
|
+
|
|
80
|
+
function parseCallArgs (argv) {
|
|
81
|
+
const out = {
|
|
82
|
+
actionId: null,
|
|
83
|
+
payload: {},
|
|
84
|
+
workspaceId: null,
|
|
85
|
+
authentication: null,
|
|
86
|
+
apiUrl: null,
|
|
87
|
+
raw: false,
|
|
88
|
+
json: null,
|
|
89
|
+
file: null,
|
|
90
|
+
explicitPayloadKeys: new Set(),
|
|
91
|
+
help: false,
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
let i = 0
|
|
95
|
+
while (i < argv.length) {
|
|
96
|
+
const arg = argv[i]
|
|
97
|
+
|
|
98
|
+
if (arg === '--help' || arg === '-h') {
|
|
99
|
+
out.help = true
|
|
100
|
+
i++
|
|
101
|
+
continue
|
|
102
|
+
}
|
|
103
|
+
if (arg === '--raw') {
|
|
104
|
+
out.raw = true
|
|
105
|
+
i++
|
|
106
|
+
continue
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// `--key=value` form, including reserved flags
|
|
110
|
+
if (arg.startsWith('--') && arg.includes('=')) {
|
|
111
|
+
const eq = arg.indexOf('=')
|
|
112
|
+
const key = arg.slice(2, eq)
|
|
113
|
+
const value = arg.slice(eq + 1)
|
|
114
|
+
if (key === 'json') out.json = value
|
|
115
|
+
else if (key === 'workspace-id') out.workspaceId = value
|
|
116
|
+
else if (key === 'authentication') out.authentication = value
|
|
117
|
+
else if (key === 'api-url') out.apiUrl = value
|
|
118
|
+
else if (key === 'file') out.file = value
|
|
119
|
+
else {
|
|
120
|
+
const camel = kebabToCamel(key)
|
|
121
|
+
out.payload[camel] = value
|
|
122
|
+
out.explicitPayloadKeys.add(camel)
|
|
123
|
+
}
|
|
124
|
+
i++
|
|
125
|
+
continue
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Reserved space-separated flags
|
|
129
|
+
if (RESERVED_FLAGS_NEEDING_VALUE.has(arg)) {
|
|
130
|
+
const value = argv[i + 1]
|
|
131
|
+
if (arg === '--json') out.json = value
|
|
132
|
+
else if (arg === '--workspace-id' || arg === '-w') out.workspaceId = value
|
|
133
|
+
else if (arg === '--authentication' || arg === '-a') out.authentication = value
|
|
134
|
+
else if (arg === '--api-url') out.apiUrl = value
|
|
135
|
+
else if (arg === '--file') out.file = value
|
|
136
|
+
i += 2
|
|
137
|
+
continue
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Action.id is the first non-flag positional
|
|
141
|
+
if (!arg.startsWith('-') && out.actionId === null) {
|
|
142
|
+
out.actionId = arg
|
|
143
|
+
i++
|
|
144
|
+
continue
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Generic --key (value | true)
|
|
148
|
+
if (arg.startsWith('--')) {
|
|
149
|
+
const key = arg.slice(2)
|
|
150
|
+
const camel = kebabToCamel(key)
|
|
151
|
+
const next = argv[i + 1]
|
|
152
|
+
if (next === undefined || next.startsWith('-')) {
|
|
153
|
+
out.payload[camel] = true
|
|
154
|
+
out.explicitPayloadKeys.add(camel)
|
|
155
|
+
i++
|
|
156
|
+
} else {
|
|
157
|
+
out.payload[camel] = next
|
|
158
|
+
out.explicitPayloadKeys.add(camel)
|
|
159
|
+
i += 2
|
|
160
|
+
}
|
|
161
|
+
continue
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Stray positional after the action id — ignore
|
|
165
|
+
i++
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (out.json) {
|
|
169
|
+
let parsed
|
|
170
|
+
try {
|
|
171
|
+
parsed = JSON.parse(out.json)
|
|
172
|
+
} catch (e) {
|
|
173
|
+
throw new Error(`--json: invalid JSON (${e.message})`)
|
|
174
|
+
}
|
|
175
|
+
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
176
|
+
throw new Error('--json: must be a JSON object')
|
|
177
|
+
}
|
|
178
|
+
for (const k of Object.keys(parsed)) out.explicitPayloadKeys.add(k)
|
|
179
|
+
Object.assign(out.payload, parsed)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return out
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function isResponseLike (value) {
|
|
186
|
+
return (
|
|
187
|
+
value
|
|
188
|
+
&& typeof value === 'object'
|
|
189
|
+
&& typeof value.status === 'number'
|
|
190
|
+
&& typeof value.text === 'function'
|
|
191
|
+
&& typeof value.headers === 'object'
|
|
192
|
+
&& typeof value.headers.get === 'function'
|
|
193
|
+
)
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
async function call (args) {
|
|
197
|
+
let parsed
|
|
198
|
+
try {
|
|
199
|
+
parsed = parseCallArgs(args)
|
|
200
|
+
} catch (err) {
|
|
201
|
+
logError({ message: `[@ossy/cli] call: ${err.message}` })
|
|
202
|
+
process.exit(1)
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (parsed.help && !parsed.actionId) {
|
|
206
|
+
console.log(buildHelp())
|
|
207
|
+
return
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (!parsed.actionId) {
|
|
211
|
+
console.log(buildHelp())
|
|
212
|
+
return
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const action = ACTION_BY_ID.get(parsed.actionId)
|
|
216
|
+
if (!action) {
|
|
217
|
+
logError({
|
|
218
|
+
message: `[@ossy/cli] call: unknown action "${parsed.actionId}". Run \`ossy call --help\` for the list.`,
|
|
219
|
+
})
|
|
220
|
+
process.exit(1)
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (parsed.help) {
|
|
224
|
+
console.log(actionHelp(action))
|
|
225
|
+
return
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const auth = getAuth({ flag: parsed.authentication })
|
|
229
|
+
const apiUrl = getApiUrl({ flag: parsed.apiUrl })
|
|
230
|
+
const workspaceId = getWorkspaceId({ flag: parsed.workspaceId })
|
|
231
|
+
|
|
232
|
+
const sdk = Sdk.SDK.of({
|
|
233
|
+
apiUrl,
|
|
234
|
+
workspaceId: workspaceId || undefined,
|
|
235
|
+
authorization: auth || undefined,
|
|
236
|
+
})
|
|
237
|
+
|
|
238
|
+
let fileMeta = null
|
|
239
|
+
if (parsed.file) {
|
|
240
|
+
try {
|
|
241
|
+
fileMeta = prepareFileUpload(parsed.file)
|
|
242
|
+
} catch (err) {
|
|
243
|
+
logError({ message: `[@ossy/cli] call: ${err.message}` })
|
|
244
|
+
process.exit(1)
|
|
245
|
+
}
|
|
246
|
+
if (!parsed.explicitPayloadKeys.has('name')) parsed.payload.name = fileMeta.name
|
|
247
|
+
if (!parsed.explicitPayloadKeys.has('type')) parsed.payload.type = fileMeta.type
|
|
248
|
+
if (!parsed.explicitPayloadKeys.has('size')) parsed.payload.size = fileMeta.size
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
let result
|
|
252
|
+
try {
|
|
253
|
+
result = await sdk.makeRequest(action)(parsed.payload)
|
|
254
|
+
} catch (err) {
|
|
255
|
+
if (isResponseLike(err)) {
|
|
256
|
+
const text = await err.text().catch(() => '')
|
|
257
|
+
process.stderr.write(`HTTP ${err.status}${err.statusText ? ' ' + err.statusText : ''}\n`)
|
|
258
|
+
if (text) process.stderr.write(text + '\n')
|
|
259
|
+
process.exit(1)
|
|
260
|
+
}
|
|
261
|
+
if (typeof err === 'string') {
|
|
262
|
+
process.stderr.write(err + '\n')
|
|
263
|
+
process.exit(1)
|
|
264
|
+
}
|
|
265
|
+
if (err && err.message) {
|
|
266
|
+
process.stderr.write(`${err.message}\n`)
|
|
267
|
+
process.exit(1)
|
|
268
|
+
}
|
|
269
|
+
process.stderr.write(JSON.stringify(err) + '\n')
|
|
270
|
+
process.exit(1)
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (isResponseLike(result)) {
|
|
274
|
+
if (parsed.raw) {
|
|
275
|
+
const text = await result.text().catch(() => '')
|
|
276
|
+
if (text) process.stdout.write(text.endsWith('\n') ? text : text + '\n')
|
|
277
|
+
} else {
|
|
278
|
+
console.log(JSON.stringify({ status: result.status }, null, 2))
|
|
279
|
+
}
|
|
280
|
+
return
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
if (fileMeta) {
|
|
284
|
+
const uploadUrl = result && result.content && result.content.uploadUrl
|
|
285
|
+
if (uploadUrl) {
|
|
286
|
+
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
|
+
}
|
|
303
|
+
} catch (err) {
|
|
304
|
+
process.stderr.write(`[@ossy/cli] call: upload PUT failed: ${err.message || err}\n`)
|
|
305
|
+
process.exit(1)
|
|
306
|
+
}
|
|
307
|
+
} else {
|
|
308
|
+
process.stderr.write(
|
|
309
|
+
'[@ossy/cli] call: --file was provided but the response did not include content.uploadUrl; nothing was uploaded.\n'
|
|
310
|
+
)
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
if (parsed.raw) {
|
|
315
|
+
process.stdout.write(
|
|
316
|
+
typeof result === 'string'
|
|
317
|
+
? (result.endsWith('\n') ? result : result + '\n')
|
|
318
|
+
: JSON.stringify(result) + '\n'
|
|
319
|
+
)
|
|
320
|
+
return
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
console.log(JSON.stringify(result, null, 2))
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
export const handler = call
|
|
327
|
+
export { parseCallArgs, ACTIONS, ACTION_BY_ID, listActionsByDomain, buildHelp, actionHelp }
|
package/src/cms/cli.js
CHANGED
|
@@ -3,6 +3,7 @@ import { pathToFileURL } from 'url'
|
|
|
3
3
|
import arg from 'arg'
|
|
4
4
|
import { logInfo, logError } from '../log.js'
|
|
5
5
|
import { resolveAppConfigPath } from '../resolve-app-config-path.js'
|
|
6
|
+
import { getAuth } from '../state.js'
|
|
6
7
|
import {
|
|
7
8
|
postResourceTemplates,
|
|
8
9
|
resolveApiBaseUrlForUpload,
|
|
@@ -22,14 +23,13 @@ const upload = (options) => {
|
|
|
22
23
|
'--api-url': String,
|
|
23
24
|
}, { argv: options })
|
|
24
25
|
|
|
25
|
-
const token =
|
|
26
|
-
parsedArgs['--authentication'] || process.env.OSSY_API_KEY
|
|
26
|
+
const token = getAuth({ flag: parsedArgs['--authentication'] })
|
|
27
27
|
const apiUrlFlag = parsedArgs['--api-url']
|
|
28
28
|
|
|
29
29
|
if (!token) {
|
|
30
30
|
logError({
|
|
31
31
|
message:
|
|
32
|
-
'[@ossy/cli] No token provided. Use --authentication / -a or
|
|
32
|
+
'[@ossy/cli] No token provided. Use --authentication / -a, set OSSY_API_KEY, or run `ossy auth login`.',
|
|
33
33
|
})
|
|
34
34
|
process.exit(1)
|
|
35
35
|
}
|
package/src/file.js
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
|
|
4
|
+
/** @type {Record<string, string>} */
|
|
5
|
+
const MIME_BY_EXT = {
|
|
6
|
+
'.js': 'application/javascript',
|
|
7
|
+
'.mjs': 'application/javascript',
|
|
8
|
+
'.cjs': 'application/javascript',
|
|
9
|
+
'.css': 'text/css',
|
|
10
|
+
'.html': 'text/html',
|
|
11
|
+
'.htm': 'text/html',
|
|
12
|
+
'.json': 'application/json',
|
|
13
|
+
'.map': 'application/json',
|
|
14
|
+
'.svg': 'image/svg+xml',
|
|
15
|
+
'.png': 'image/png',
|
|
16
|
+
'.jpg': 'image/jpeg',
|
|
17
|
+
'.jpeg': 'image/jpeg',
|
|
18
|
+
'.gif': 'image/gif',
|
|
19
|
+
'.webp': 'image/webp',
|
|
20
|
+
'.ico': 'image/x-icon',
|
|
21
|
+
'.woff': 'font/woff',
|
|
22
|
+
'.woff2': 'font/woff2',
|
|
23
|
+
'.ttf': 'font/ttf',
|
|
24
|
+
'.txt': 'text/plain',
|
|
25
|
+
'.xml': 'application/xml',
|
|
26
|
+
'.webmanifest': 'application/manifest+json',
|
|
27
|
+
'.pdf': 'application/pdf',
|
|
28
|
+
'.mp4': 'video/mp4',
|
|
29
|
+
'.webm': 'video/webm',
|
|
30
|
+
'.mp3': 'audio/mpeg',
|
|
31
|
+
'.wav': 'audio/wav',
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Best-effort MIME type from extension. Falls back to `application/octet-stream`.
|
|
36
|
+
* @param {string} filePath
|
|
37
|
+
* @returns {string}
|
|
38
|
+
*/
|
|
39
|
+
export function guessContentType (filePath) {
|
|
40
|
+
const ext = path.extname(filePath).toLowerCase()
|
|
41
|
+
return MIME_BY_EXT[ext] || 'application/octet-stream'
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Stat a local file and derive metadata for an upload payload.
|
|
46
|
+
* Throws if the path doesn't exist or isn't a regular file.
|
|
47
|
+
* @param {string} filePath absolute or cwd-relative
|
|
48
|
+
* @returns {{ absPath: string, name: string, size: number, type: string }}
|
|
49
|
+
*/
|
|
50
|
+
export function prepareFileUpload (filePath) {
|
|
51
|
+
if (!filePath || typeof filePath !== 'string') {
|
|
52
|
+
throw new Error('--file: missing path')
|
|
53
|
+
}
|
|
54
|
+
const absPath = path.resolve(filePath)
|
|
55
|
+
let stat
|
|
56
|
+
try {
|
|
57
|
+
stat = fs.statSync(absPath)
|
|
58
|
+
} catch (err) {
|
|
59
|
+
if (err && err.code === 'ENOENT') {
|
|
60
|
+
throw new Error(`--file: not found: ${filePath}`)
|
|
61
|
+
}
|
|
62
|
+
throw new Error(`--file: cannot stat ${filePath}: ${err.message || err}`)
|
|
63
|
+
}
|
|
64
|
+
if (!stat.isFile()) {
|
|
65
|
+
throw new Error(`--file: not a regular file: ${filePath}`)
|
|
66
|
+
}
|
|
67
|
+
return {
|
|
68
|
+
absPath,
|
|
69
|
+
name: path.basename(absPath),
|
|
70
|
+
size: stat.size,
|
|
71
|
+
type: guessContentType(absPath),
|
|
72
|
+
}
|
|
73
|
+
}
|
package/src/index.js
CHANGED
|
@@ -1,20 +1,156 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import fs from 'node:fs'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import { fileURLToPath } from 'node:url'
|
|
2
5
|
import { build } from '@ossy/app'
|
|
6
|
+
import * as Auth from './auth/cli.js'
|
|
7
|
+
import * as Call from './call/cli.js'
|
|
3
8
|
import * as Cms from './cms/cli.js'
|
|
4
9
|
import * as Init from './init/cli.js'
|
|
5
10
|
import { publish } from './publish/cli.js'
|
|
6
11
|
import * as Registry from './registry/cli.js'
|
|
12
|
+
import * as Workspace from './workspace/cli.js'
|
|
7
13
|
|
|
8
|
-
const
|
|
14
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
15
|
+
const pkg = JSON.parse(
|
|
16
|
+
fs.readFileSync(path.join(__dirname, '..', 'package.json'), 'utf8')
|
|
17
|
+
)
|
|
9
18
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
19
|
+
const HELP = `ossy ${pkg.version}
|
|
20
|
+
Command line tool for the Ossy platform.
|
|
21
|
+
|
|
22
|
+
Usage:
|
|
23
|
+
ossy <command> [options]
|
|
24
|
+
|
|
25
|
+
Local utilities:
|
|
26
|
+
auth <subcommand> Sign in / out and check status: login | logout | status
|
|
27
|
+
workspace <subcommand> Manage active workspace: use | list | current
|
|
28
|
+
init [dir] Scaffold a new Ossy app
|
|
29
|
+
build Production build of the app in the current directory
|
|
30
|
+
cms validate Validate src/config.js (workspaceId + resourceTemplates)
|
|
31
|
+
|
|
32
|
+
Action dispatcher (any Ossy SDK action, no per-action wiring needed):
|
|
33
|
+
call <action.id> Run \`ossy call --help\` for the full catalog
|
|
34
|
+
|
|
35
|
+
Workflows (wrap one or more actions with extra ergonomics):
|
|
36
|
+
cms upload Upload resource templates from src/config.js
|
|
37
|
+
(wraps workspaces.import-resource-templates)
|
|
38
|
+
registry <subcommand> ecr-push-credentials (wraps registry.ecr-push-credentials,
|
|
39
|
+
adds --format github-actions and password masking)
|
|
40
|
+
publish Deploy via @ossy/deployment-tools, then upload
|
|
41
|
+
resource templates and site build artifacts
|
|
42
|
+
|
|
43
|
+
Options:
|
|
44
|
+
-h, --help Show this help, or for a command: ossy <command> --help
|
|
45
|
+
-v, --version Print the installed version
|
|
46
|
+
`
|
|
47
|
+
|
|
48
|
+
const SUBCOMMAND_HELP = {
|
|
49
|
+
auth: `ossy auth <subcommand>
|
|
50
|
+
Subcommands:
|
|
51
|
+
login Save an Ossy API token under ~/.config/ossy/credentials.json
|
|
52
|
+
logout Delete the saved token
|
|
53
|
+
status Print logged-in user, active workspace, and config paths
|
|
54
|
+
|
|
55
|
+
Options (login):
|
|
56
|
+
-t, --token Ossy API JWT (otherwise prompts in a TTY)
|
|
57
|
+
--api-url API base URL (default: https://api.ossy.se/api/v0)
|
|
58
|
+
`,
|
|
59
|
+
workspace: `ossy workspace <subcommand>
|
|
60
|
+
Subcommands:
|
|
61
|
+
use <id> Set the active workspace (saved to ~/.config/ossy/config.json)
|
|
62
|
+
list List workspaces the current user can access
|
|
63
|
+
current Print the active workspace id
|
|
64
|
+
`,
|
|
65
|
+
init: `ossy init [dir]
|
|
66
|
+
Scaffold a new Ossy app in [dir] (defaults to current directory).
|
|
67
|
+
Creates src/home.page.jsx, src/config.js, and package.json (if missing).
|
|
68
|
+
`,
|
|
69
|
+
build: `ossy build
|
|
70
|
+
Run the production build for the app in the current directory.
|
|
71
|
+
See the @ossy/app README for build options.
|
|
72
|
+
`,
|
|
73
|
+
publish: `ossy publish [options]
|
|
74
|
+
Multi-step workflow: deploy via @ossy/deployment-tools (temporary), then
|
|
75
|
+
optionally upload resource templates and site build artifacts. The deploy
|
|
76
|
+
step is intended to go away once the platform reacts to artifact events;
|
|
77
|
+
the CMS upload steps are equivalent to:
|
|
78
|
+
workspaces.import-resource-templates (resource templates)
|
|
79
|
+
/site-artifacts/presign-batch + /commit-batch (site artifacts)
|
|
80
|
+
|
|
81
|
+
Options:
|
|
82
|
+
-a, --authentication Ossy API JWT (or set OSSY_API_KEY, or run \`ossy auth login\`)
|
|
83
|
+
-d, --domain Site domain
|
|
84
|
+
-p, --platform Target deployment platform
|
|
85
|
+
-c, --config Path to src/config.js
|
|
86
|
+
--platforms-path Path to platforms.json
|
|
87
|
+
--deployments-path Glob for deployments JSON files
|
|
88
|
+
--all Deploy all sites for the platform
|
|
89
|
+
--skip-resource-templates Skip post-deploy resource-template upload
|
|
90
|
+
--skip-site-artifacts Skip post-deploy site-artifact upload
|
|
91
|
+
--site-artifacts-build-dir Override the site build directory
|
|
92
|
+
--api-url API base URL for CMS calls
|
|
93
|
+
`,
|
|
94
|
+
cms: `ossy cms <subcommand>
|
|
95
|
+
Subcommands:
|
|
96
|
+
upload Upload resource templates from src/config.js to the workspace
|
|
97
|
+
(wraps the SDK action workspaces.import-resource-templates)
|
|
98
|
+
validate Validate ossy app config and resource templates locally
|
|
99
|
+
|
|
100
|
+
Options (upload):
|
|
101
|
+
-a, --authentication Ossy API JWT (or set OSSY_API_KEY, or run \`ossy auth login\`)
|
|
102
|
+
-c, --config Path to src/config.js
|
|
103
|
+
--api-url API base URL for CMS calls
|
|
104
|
+
|
|
105
|
+
Options (validate):
|
|
106
|
+
-c, --config Path to src/config.js
|
|
107
|
+
`,
|
|
108
|
+
registry: `ossy registry <subcommand>
|
|
109
|
+
Subcommands:
|
|
110
|
+
ecr-push-credentials Fetch a short-lived ECR password for docker login
|
|
111
|
+
(wraps the SDK action registry.ecr-push-credentials;
|
|
112
|
+
adds --format github-actions and password masking)
|
|
113
|
+
|
|
114
|
+
Options:
|
|
115
|
+
-a, --authentication Ossy API JWT (or set OSSY_API_KEY, or run \`ossy auth login\`)
|
|
116
|
+
-w, --workspace-id Workspace id (or set OSSY_WORKSPACE_ID, or run \`ossy workspace use\`)
|
|
117
|
+
--format json | github-actions
|
|
118
|
+
`,
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const isHelpFlag = (a) => a === '--help' || a === '-h'
|
|
122
|
+
const isVersionFlag = (a) => a === '--version' || a === '-v'
|
|
123
|
+
|
|
124
|
+
const [, , command, ...restArgs] = process.argv
|
|
125
|
+
|
|
126
|
+
if (command === undefined || isHelpFlag(command)) {
|
|
127
|
+
console.log(HELP)
|
|
128
|
+
process.exit(0)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (isVersionFlag(command)) {
|
|
132
|
+
console.log(pkg.version)
|
|
133
|
+
process.exit(0)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (restArgs.some(isHelpFlag) && SUBCOMMAND_HELP[command]) {
|
|
137
|
+
console.log(SUBCOMMAND_HELP[command])
|
|
138
|
+
process.exit(0)
|
|
15
139
|
}
|
|
16
140
|
|
|
17
141
|
const run = async () => {
|
|
142
|
+
if (command === 'auth') {
|
|
143
|
+
await Auth.handler(restArgs)
|
|
144
|
+
return
|
|
145
|
+
}
|
|
146
|
+
if (command === 'workspace') {
|
|
147
|
+
await Workspace.handler(restArgs)
|
|
148
|
+
return
|
|
149
|
+
}
|
|
150
|
+
if (command === 'call') {
|
|
151
|
+
await Call.handler(restArgs)
|
|
152
|
+
return
|
|
153
|
+
}
|
|
18
154
|
if (command === 'registry') {
|
|
19
155
|
await Registry.handler(restArgs)
|
|
20
156
|
return
|
|
@@ -36,6 +172,7 @@ const run = async () => {
|
|
|
36
172
|
return
|
|
37
173
|
}
|
|
38
174
|
console.error(`[@ossy/cli] Unknown command: ${command}`)
|
|
175
|
+
console.error('Run `ossy --help` for available commands.')
|
|
39
176
|
process.exit(1)
|
|
40
177
|
}
|
|
41
178
|
|
package/src/publish/cli.js
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
import { maybeUploadResourceTemplatesAfterPublish } from './resource-templates-after-publish.js'
|
|
11
11
|
import { maybeUploadSiteArtifactsAfterPublish } from './site-artifacts-after-publish.js'
|
|
12
12
|
import { requireCmsAuthentication } from '../cms/upload-resource-templates.js'
|
|
13
|
+
import { getAuth } from '../state.js'
|
|
13
14
|
|
|
14
15
|
const DEPLOYMENT_TOOLS = '@ossy/deployment-tools'
|
|
15
16
|
|
|
@@ -64,14 +65,14 @@ export async function publish (options) {
|
|
|
64
65
|
let apiToken
|
|
65
66
|
try {
|
|
66
67
|
apiToken = requireCmsAuthentication(
|
|
67
|
-
parsedArgs['--authentication']
|
|
68
|
+
getAuth({ flag: parsedArgs['--authentication'] }),
|
|
68
69
|
'publish'
|
|
69
70
|
)
|
|
70
71
|
} catch (e) {
|
|
71
72
|
logError({
|
|
72
73
|
message:
|
|
73
74
|
e?.message
|
|
74
|
-
|| '[@ossy/cli] publish: pass --authentication (-a)
|
|
75
|
+
|| '[@ossy/cli] publish: pass --authentication (-a), set OSSY_API_KEY, or run `ossy auth login`.',
|
|
75
76
|
})
|
|
76
77
|
process.exit(1)
|
|
77
78
|
}
|
|
@@ -8,38 +8,11 @@ import {
|
|
|
8
8
|
requireCmsAuthentication,
|
|
9
9
|
resolveApiBaseUrlForUpload,
|
|
10
10
|
} from '../cms/upload-resource-templates.js'
|
|
11
|
+
import { guessContentType } from '../file.js'
|
|
11
12
|
|
|
12
13
|
const MAX_FILES = 200
|
|
13
14
|
|
|
14
|
-
|
|
15
|
-
const MIME_BY_EXT = {
|
|
16
|
-
'.js': 'application/javascript',
|
|
17
|
-
'.mjs': 'application/javascript',
|
|
18
|
-
'.cjs': 'application/javascript',
|
|
19
|
-
'.css': 'text/css',
|
|
20
|
-
'.html': 'text/html',
|
|
21
|
-
'.htm': 'text/html',
|
|
22
|
-
'.json': 'application/json',
|
|
23
|
-
'.map': 'application/json',
|
|
24
|
-
'.svg': 'image/svg+xml',
|
|
25
|
-
'.png': 'image/png',
|
|
26
|
-
'.jpg': 'image/jpeg',
|
|
27
|
-
'.jpeg': 'image/jpeg',
|
|
28
|
-
'.gif': 'image/gif',
|
|
29
|
-
'.webp': 'image/webp',
|
|
30
|
-
'.ico': 'image/x-icon',
|
|
31
|
-
'.woff': 'font/woff',
|
|
32
|
-
'.woff2': 'font/woff2',
|
|
33
|
-
'.ttf': 'font/ttf',
|
|
34
|
-
'.txt': 'text/plain',
|
|
35
|
-
'.xml': 'application/xml',
|
|
36
|
-
'.webmanifest': 'application/manifest+json',
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export function guessContentType (filePath) {
|
|
40
|
-
const ext = path.extname(filePath).toLowerCase()
|
|
41
|
-
return MIME_BY_EXT[ext] || 'application/octet-stream'
|
|
42
|
-
}
|
|
15
|
+
export { guessContentType }
|
|
43
16
|
|
|
44
17
|
/**
|
|
45
18
|
* @param {string} buildDir absolute path to `build/` output
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import arg from 'arg'
|
|
2
2
|
import { appendFileSync } from 'fs'
|
|
3
3
|
import { requireCmsAuthentication } from '../cms/upload-resource-templates.js'
|
|
4
|
+
import { getAuth, getWorkspaceId } from '../state.js'
|
|
4
5
|
|
|
5
6
|
const DEFAULT_API_URL = 'https://api.ossy.se/api/v0'
|
|
6
7
|
|
|
@@ -25,17 +26,17 @@ export async function ecrPushCredentials (options) {
|
|
|
25
26
|
let token
|
|
26
27
|
try {
|
|
27
28
|
token = requireCmsAuthentication(
|
|
28
|
-
parsed['--authentication']
|
|
29
|
+
getAuth({ flag: parsed['--authentication'] }),
|
|
29
30
|
'registry ecr-push-credentials'
|
|
30
31
|
)
|
|
31
32
|
} catch (e) {
|
|
32
|
-
console.error(e?.message || '[@ossy/cli] registry ecr-push-credentials: need --authentication (-a) or
|
|
33
|
+
console.error(e?.message || '[@ossy/cli] registry ecr-push-credentials: need --authentication (-a), OSSY_API_KEY, or `ossy auth login`')
|
|
33
34
|
process.exit(1)
|
|
34
35
|
}
|
|
35
36
|
|
|
36
|
-
const workspaceId = parsed['--workspace-id']
|
|
37
|
-
if (!workspaceId
|
|
38
|
-
console.error('[@ossy/cli] registry ecr-push-credentials: pass --workspace-id (-w) or
|
|
37
|
+
const workspaceId = getWorkspaceId({ flag: parsed['--workspace-id'] })
|
|
38
|
+
if (!workspaceId) {
|
|
39
|
+
console.error('[@ossy/cli] registry ecr-push-credentials: pass --workspace-id (-w), set OSSY_WORKSPACE_ID, or run `ossy workspace use <id>`')
|
|
39
40
|
process.exit(1)
|
|
40
41
|
}
|
|
41
42
|
|
|
@@ -46,7 +47,7 @@ export async function ecrPushCredentials (options) {
|
|
|
46
47
|
method: 'POST',
|
|
47
48
|
headers: {
|
|
48
49
|
authorization: token,
|
|
49
|
-
workspaceId
|
|
50
|
+
workspaceId,
|
|
50
51
|
'content-type': 'application/json',
|
|
51
52
|
},
|
|
52
53
|
body: '{}',
|
package/src/state.js
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import os from 'node:os'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
|
|
5
|
+
const DEFAULT_API_URL = 'https://api.ossy.se/api/v0'
|
|
6
|
+
|
|
7
|
+
function configDir () {
|
|
8
|
+
const xdg = process.env.XDG_CONFIG_HOME
|
|
9
|
+
if (xdg && xdg.trim()) return path.join(xdg.trim(), 'ossy')
|
|
10
|
+
return path.join(os.homedir(), '.config', 'ossy')
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function ensureDir () {
|
|
14
|
+
const dir = configDir()
|
|
15
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 })
|
|
16
|
+
return dir
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function credentialsPath () { return path.join(configDir(), 'credentials.json') }
|
|
20
|
+
function configPath () { return path.join(configDir(), 'config.json') }
|
|
21
|
+
|
|
22
|
+
/** @returns {{ apiUrl?: string, token?: string } | null} */
|
|
23
|
+
export function readCredentials () {
|
|
24
|
+
try {
|
|
25
|
+
return JSON.parse(fs.readFileSync(credentialsPath(), 'utf8'))
|
|
26
|
+
} catch {
|
|
27
|
+
return null
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** @param {{ apiUrl?: string, token: string }} creds */
|
|
32
|
+
export function writeCredentials (creds) {
|
|
33
|
+
ensureDir()
|
|
34
|
+
fs.writeFileSync(credentialsPath(), JSON.stringify(creds, null, 2), { mode: 0o600 })
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function deleteCredentials () {
|
|
38
|
+
try { fs.unlinkSync(credentialsPath()) } catch { /* not logged in */ }
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** @returns {{ workspaceId?: string } | null} */
|
|
42
|
+
export function readConfig () {
|
|
43
|
+
try {
|
|
44
|
+
return JSON.parse(fs.readFileSync(configPath(), 'utf8'))
|
|
45
|
+
} catch {
|
|
46
|
+
return null
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** @param {{ workspaceId?: string }} cfg */
|
|
51
|
+
export function writeConfig (cfg) {
|
|
52
|
+
ensureDir()
|
|
53
|
+
fs.writeFileSync(configPath(), JSON.stringify(cfg, null, 2))
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function stripBearer (token) {
|
|
57
|
+
return String(token ?? '').trim().replace(/^Bearer\s+/i, '').trim()
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Resolve the Ossy API JWT used for authenticated requests.
|
|
62
|
+
* Order: explicit flag > OSSY_API_KEY env > saved credentials.
|
|
63
|
+
* @param {{ flag?: string }} [opts]
|
|
64
|
+
* @returns {string | null}
|
|
65
|
+
*/
|
|
66
|
+
export function getAuth ({ flag } = {}) {
|
|
67
|
+
const fromFlag = stripBearer(flag)
|
|
68
|
+
if (fromFlag) return fromFlag
|
|
69
|
+
const fromEnv = stripBearer(process.env.OSSY_API_KEY)
|
|
70
|
+
if (fromEnv) return fromEnv
|
|
71
|
+
const fromFile = stripBearer(readCredentials()?.token)
|
|
72
|
+
if (fromFile) return fromFile
|
|
73
|
+
return null
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Order: explicit absolute flag > OSSY_API_URL env > saved credentials > default.
|
|
78
|
+
* Relative paths in the flag are ignored (matches existing publish behavior).
|
|
79
|
+
* @param {{ flag?: string }} [opts]
|
|
80
|
+
* @returns {string}
|
|
81
|
+
*/
|
|
82
|
+
export function getApiUrl ({ flag } = {}) {
|
|
83
|
+
if (flag && /^https?:\/\//i.test(String(flag).trim())) {
|
|
84
|
+
return String(flag).trim().replace(/\/$/, '')
|
|
85
|
+
}
|
|
86
|
+
const fromEnv = String(process.env.OSSY_API_URL ?? '').trim()
|
|
87
|
+
if (fromEnv) return fromEnv.replace(/\/$/, '')
|
|
88
|
+
const fromFile = String(readCredentials()?.apiUrl ?? '').trim()
|
|
89
|
+
if (/^https?:\/\//i.test(fromFile)) return fromFile.replace(/\/$/, '')
|
|
90
|
+
return DEFAULT_API_URL
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Order: explicit flag > OSSY_WORKSPACE_ID env > saved active workspace.
|
|
95
|
+
* Project-scoped commands (publish/cms) keep reading from src/config.js separately.
|
|
96
|
+
* @param {{ flag?: string }} [opts]
|
|
97
|
+
* @returns {string | null}
|
|
98
|
+
*/
|
|
99
|
+
export function getWorkspaceId ({ flag } = {}) {
|
|
100
|
+
const fromFlag = String(flag ?? '').trim()
|
|
101
|
+
if (fromFlag) return fromFlag
|
|
102
|
+
const fromEnv = String(process.env.OSSY_WORKSPACE_ID ?? '').trim()
|
|
103
|
+
if (fromEnv) return fromEnv
|
|
104
|
+
const fromFile = String(readConfig()?.workspaceId ?? '').trim()
|
|
105
|
+
if (fromFile) return fromFile
|
|
106
|
+
return null
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function describeStatePaths () {
|
|
110
|
+
return {
|
|
111
|
+
dir: configDir(),
|
|
112
|
+
credentials: credentialsPath(),
|
|
113
|
+
config: configPath(),
|
|
114
|
+
}
|
|
115
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { logInfo, logError } from '../log.js'
|
|
2
|
+
import {
|
|
3
|
+
readConfig,
|
|
4
|
+
writeConfig,
|
|
5
|
+
getAuth,
|
|
6
|
+
getApiUrl,
|
|
7
|
+
getWorkspaceId,
|
|
8
|
+
} from '../state.js'
|
|
9
|
+
|
|
10
|
+
function ensureAuth () {
|
|
11
|
+
const auth = getAuth()
|
|
12
|
+
if (!auth) {
|
|
13
|
+
logError({
|
|
14
|
+
message: '[@ossy/cli] Not logged in. Run `ossy auth login` first.',
|
|
15
|
+
})
|
|
16
|
+
process.exit(1)
|
|
17
|
+
}
|
|
18
|
+
return auth
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function use (options) {
|
|
22
|
+
const id = options[0]
|
|
23
|
+
if (!id) {
|
|
24
|
+
logError({ message: '[@ossy/cli] workspace use: pass a workspace id.' })
|
|
25
|
+
process.exit(1)
|
|
26
|
+
}
|
|
27
|
+
writeConfig({ ...(readConfig() || {}), workspaceId: id })
|
|
28
|
+
logInfo({ message: `[@ossy/cli] Active workspace: ${id}` })
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function list () {
|
|
32
|
+
const auth = ensureAuth()
|
|
33
|
+
const apiUrl = getApiUrl()
|
|
34
|
+
|
|
35
|
+
let res
|
|
36
|
+
try {
|
|
37
|
+
res = await fetch(`${apiUrl}/workspaces`, {
|
|
38
|
+
headers: { Authorization: auth, 'Content-Type': 'application/json' },
|
|
39
|
+
})
|
|
40
|
+
} catch (error) {
|
|
41
|
+
logError({ message: `[@ossy/cli] workspace list: could not reach ${apiUrl}`, error })
|
|
42
|
+
process.exit(1)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (!res.ok) {
|
|
46
|
+
logError({
|
|
47
|
+
message: `[@ossy/cli] workspace list: HTTP ${res.status} from ${apiUrl}/workspaces`,
|
|
48
|
+
})
|
|
49
|
+
process.exit(1)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const workspaces = await res.json().catch(() => null)
|
|
53
|
+
if (!Array.isArray(workspaces) || workspaces.length === 0) {
|
|
54
|
+
console.log('No workspaces found.')
|
|
55
|
+
return
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const active = getWorkspaceId()
|
|
59
|
+
for (const w of workspaces) {
|
|
60
|
+
const marker = w.id === active ? '* ' : ' '
|
|
61
|
+
console.log(`${marker}${w.id}\t${w.name || ''}`)
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function current () {
|
|
66
|
+
const id = getWorkspaceId()
|
|
67
|
+
if (!id) {
|
|
68
|
+
console.log('(no active workspace)')
|
|
69
|
+
return
|
|
70
|
+
}
|
|
71
|
+
console.log(id)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export async function handler (args) {
|
|
75
|
+
const [sub, ...rest] = args
|
|
76
|
+
if (sub === 'use') return use(rest)
|
|
77
|
+
if (sub === 'list') return await list()
|
|
78
|
+
if (sub === 'current') return current()
|
|
79
|
+
logError({
|
|
80
|
+
message:
|
|
81
|
+
'[@ossy/cli] workspace: unknown subcommand. Use: ossy workspace use <id> | list | current',
|
|
82
|
+
})
|
|
83
|
+
process.exit(1)
|
|
84
|
+
}
|