@ossy/cli 1.16.10 → 1.17.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/file.js ADDED
@@ -0,0 +1,99 @@
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
+ * PUT file bytes to a presigned S3 upload URL.
46
+ * Throws on network error or non-2xx response.
47
+ * @param {{ absPath: string, size: number, type: string }} fileMeta
48
+ * @param {string} uploadUrl
49
+ * @returns {Promise<void>}
50
+ */
51
+ export async function uploadFileToUrl (fileMeta, uploadUrl) {
52
+ const { readFile } = await import('node:fs/promises')
53
+ const body = await readFile(fileMeta.absPath)
54
+ const res = await fetch(uploadUrl, {
55
+ method: 'PUT',
56
+ headers: {
57
+ 'Content-Type': fileMeta.type,
58
+ 'Content-Length': String(fileMeta.size),
59
+ },
60
+ body,
61
+ })
62
+ if (!res.ok) {
63
+ const t = await res.text().catch(() => '')
64
+ throw new Error(
65
+ `S3 PUT failed: HTTP ${res.status}${t ? ' — ' + t.slice(0, 200) : ''}`
66
+ )
67
+ }
68
+ }
69
+
70
+ /**
71
+ * Stat a local file and derive metadata for an upload payload.
72
+ * Throws if the path doesn't exist or isn't a regular file.
73
+ * @param {string} filePath absolute or cwd-relative
74
+ * @returns {{ absPath: string, name: string, size: number, type: string }}
75
+ */
76
+ export function prepareFileUpload (filePath) {
77
+ if (!filePath || typeof filePath !== 'string') {
78
+ throw new Error('--file: missing path')
79
+ }
80
+ const absPath = path.resolve(filePath)
81
+ let stat
82
+ try {
83
+ stat = fs.statSync(absPath)
84
+ } catch (err) {
85
+ if (err && err.code === 'ENOENT') {
86
+ throw new Error(`--file: not found: ${filePath}`)
87
+ }
88
+ throw new Error(`--file: cannot stat ${filePath}: ${err.message || err}`)
89
+ }
90
+ if (!stat.isFile()) {
91
+ throw new Error(`--file: not a regular file: ${filePath}`)
92
+ }
93
+ return {
94
+ absPath,
95
+ name: path.basename(absPath),
96
+ size: stat.size,
97
+ type: guessContentType(absPath),
98
+ }
99
+ }
package/src/index.js CHANGED
@@ -2,11 +2,12 @@
2
2
  import fs from 'node:fs'
3
3
  import path from 'node:path'
4
4
  import { fileURLToPath } from 'node:url'
5
- import { build } from '@ossy/app'
6
- import * as Cms from './cms/cli.js'
7
- import * as Init from './init/cli.js'
8
- import { publish } from './publish/cli.js'
5
+ import * as Auth from './auth/cli.js'
6
+ import * as Call from './call/cli.js'
7
+ import * as App from './app/cli.js'
9
8
  import * as Registry from './registry/cli.js'
9
+ import * as UploadDir from './upload-dir/cli.js'
10
+ import * as Workspace from './workspace/cli.js'
10
11
 
11
12
  const __dirname = path.dirname(fileURLToPath(import.meta.url))
12
13
  const pkg = JSON.parse(
@@ -19,12 +20,18 @@ Command line tool for the Ossy platform.
19
20
  Usage:
20
21
  ossy <command> [options]
21
22
 
22
- Commands:
23
- init [dir] Scaffold a new Ossy app
24
- build Production build of the app in the current directory
25
- publish Deploy a site and upload templates / site artifacts
26
- cms <subcommand> Resource template workflows: upload | validate
27
- registry <subcommand> Container registry helpers: ecr-push-credentials
23
+ Local utilities:
24
+ auth <subcommand> Sign in / out and check status: login | logout | status
25
+ workspace <subcommand> Manage active workspace: use | list | current
26
+ app <subcommand> App commands: init | build | upload | validate | publish
27
+
28
+ Action dispatcher (any Ossy SDK action, no per-action wiring needed):
29
+ call <action.id> Run \`ossy call --help\` for the full catalog
30
+
31
+ Workflows (wrap one or more actions with extra ergonomics):
32
+ upload-dir <dir> <loc> Recursively upload a local directory to the CMS
33
+ registry <subcommand> ecr-push-credentials (wraps registry.ecr-push-credentials,
34
+ adds --format github-actions and password masking)
28
35
 
29
36
  Options:
30
37
  -h, --help Show this help, or for a command: ossy <command> --help
@@ -32,51 +39,68 @@ Options:
32
39
  `
33
40
 
34
41
  const SUBCOMMAND_HELP = {
35
- init: `ossy init [dir]
36
- Scaffold a new Ossy app in [dir] (defaults to current directory).
37
- Creates src/home.page.jsx, src/config.js, and package.json (if missing).
38
- `,
39
- build: `ossy build
40
- Run the production build for the app in the current directory.
41
- See the @ossy/app README for build options.
42
- `,
43
- publish: `ossy publish [options]
44
- Deploy via @ossy/deployment-tools, then optionally upload resource templates
45
- and site artifacts.
42
+ auth: `ossy auth <subcommand>
43
+ Subcommands:
44
+ login Save an Ossy API token under ~/.config/ossy/credentials.json
45
+ logout Delete the saved token
46
+ status Print logged-in user, active workspace, and config paths
46
47
 
47
- Options:
48
- -a, --authentication Ossy API JWT (or set OSSY_API_KEY)
49
- -d, --domain Site domain
50
- -p, --platform Target deployment platform
51
- -c, --config Path to src/config.js
52
- --platforms-path Path to platforms.json
53
- --deployments-path Glob for deployments JSON files
54
- --all Deploy all sites for the platform
55
- --skip-resource-templates Skip post-deploy resource-template upload
56
- --skip-site-artifacts Skip post-deploy site-artifact upload
57
- --site-artifacts-build-dir Override the site build directory
58
- --api-url API base URL for CMS calls
48
+ Options (login):
49
+ -t, --token Ossy API JWT (otherwise prompts in a TTY)
50
+ --api-url API base URL (default: https://api.ossy.se/api/v0)
51
+ `,
52
+ workspace: `ossy workspace <subcommand>
53
+ Subcommands:
54
+ use <id> Set the active workspace (saved to ~/.config/ossy/config.json)
55
+ list List workspaces the current user can access
56
+ current Print the active workspace id
59
57
  `,
60
- cms: `ossy cms <subcommand>
58
+ app: `ossy app <subcommand>
61
59
  Subcommands:
60
+ init Scaffold a new Ossy app (default dir: current directory)
61
+ build Production build of the app in the current directory
62
62
  upload Upload resource templates from src/config.js to the workspace
63
+ (wraps the SDK action workspaces.import-resource-templates)
63
64
  validate Validate ossy app config and resource templates locally
65
+ publish Upload resource templates and build/ to the CMS
66
+ (resource templates + build files to /sites/{domain})
64
67
 
65
68
  Options (upload):
66
- -a, --authentication Ossy API JWT (or set OSSY_API_KEY)
69
+ -a, --authentication Ossy API JWT (or set OSSY_API_KEY, or run \`ossy auth login\`)
67
70
  -c, --config Path to src/config.js
68
- --api-url API base URL for CMS calls
71
+ --api-url API base URL for API calls
69
72
 
70
73
  Options (validate):
71
74
  -c, --config Path to src/config.js
75
+
76
+ Options (publish):
77
+ -a, --authentication Ossy API JWT (or set OSSY_API_KEY, or run \`ossy auth login\`)
78
+ -c, --config Path to src/config.js
79
+ --build-dir Override build directory (default: <package>/build)
80
+ --build-dest Override remote CMS location (default: /sites/{domain})
81
+ --skip-resource-templates Skip resource template upload
82
+ --api-url API base URL for API calls
83
+ `,
84
+ 'upload-dir': `ossy upload-dir <localDir> <remoteLocation> [options]
85
+ Recursively mirror a local directory into the Ossy CMS.
86
+ Creates parent directories first, then uploads every file.
87
+ Per-item errors are logged and skipped; exit code is non-zero if any failed.
88
+
89
+ Options:
90
+ --dry-run Print what would happen without making any API calls
91
+ -a, --authentication Ossy API JWT (or set OSSY_API_KEY, or run \`ossy auth login\`)
92
+ -w, --workspace-id Workspace id (or set OSSY_WORKSPACE_ID, or run \`ossy workspace use\`)
93
+ --api-url Override API base URL
72
94
  `,
73
95
  registry: `ossy registry <subcommand>
74
96
  Subcommands:
75
97
  ecr-push-credentials Fetch a short-lived ECR password for docker login
98
+ (wraps the SDK action registry.ecr-push-credentials;
99
+ adds --format github-actions and password masking)
76
100
 
77
101
  Options:
78
- -a, --authentication Ossy API JWT (or set OSSY_API_KEY)
79
- --workspace-id Workspace id
102
+ -a, --authentication Ossy API JWT (or set OSSY_API_KEY, or run \`ossy auth login\`)
103
+ -w, --workspace-id Workspace id (or set OSSY_WORKSPACE_ID, or run \`ossy workspace use\`)
80
104
  --format json | github-actions
81
105
  `,
82
106
  }
@@ -102,24 +126,28 @@ if (restArgs.some(isHelpFlag) && SUBCOMMAND_HELP[command]) {
102
126
  }
103
127
 
104
128
  const run = async () => {
105
- if (command === 'registry') {
106
- await Registry.handler(restArgs)
129
+ if (command === 'auth') {
130
+ await Auth.handler(restArgs)
107
131
  return
108
132
  }
109
- if (command === 'cms') {
110
- Cms.handler(restArgs)
133
+ if (command === 'workspace') {
134
+ await Workspace.handler(restArgs)
111
135
  return
112
136
  }
113
- if (command === 'init') {
114
- Init.init(restArgs)
137
+ if (command === 'call') {
138
+ await Call.handler(restArgs)
115
139
  return
116
140
  }
117
- if (command === 'publish') {
118
- await publish(restArgs)
141
+ if (command === 'upload-dir') {
142
+ await UploadDir.handler(restArgs)
143
+ return
144
+ }
145
+ if (command === 'registry') {
146
+ await Registry.handler(restArgs)
119
147
  return
120
148
  }
121
- if (command === 'build') {
122
- await build(restArgs)
149
+ if (command === 'app') {
150
+ await App.handler(restArgs)
123
151
  return
124
152
  }
125
153
  console.error(`[@ossy/cli] Unknown command: ${command}`)
@@ -1,6 +1,7 @@
1
1
  import arg from 'arg'
2
2
  import { appendFileSync } from 'fs'
3
- import { requireCmsAuthentication } from '../cms/upload-resource-templates.js'
3
+ import { requireAppAuthentication } from '../app/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
 
@@ -24,18 +25,18 @@ export async function ecrPushCredentials (options) {
24
25
 
25
26
  let token
26
27
  try {
27
- token = requireCmsAuthentication(
28
- parsed['--authentication'] || process.env.OSSY_API_KEY,
28
+ token = requireAppAuthentication(
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 OSSY_API_KEY')
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'] || process.env.OSSY_WORKSPACE_ID || ''
37
- if (!workspaceId.trim()) {
38
- console.error('[@ossy/cli] registry ecr-push-credentials: pass --workspace-id (-w) or set OSSY_WORKSPACE_ID')
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: workspaceId.trim(),
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
+ }