staticx 0.1.0
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 +44 -0
- package/bin/staticx.js +9 -0
- package/package.json +41 -0
- package/src/archive.js +73 -0
- package/src/auth.js +28 -0
- package/src/config.js +77 -0
- package/src/files.js +10 -0
- package/src/http.js +101 -0
- package/src/index.js +283 -0
- package/src/output.js +23 -0
package/README.md
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# STATICX CLI
|
|
2
|
+
|
|
3
|
+
`staticx` is the public CLI for STATICX. It uses the same token-authenticated `/api/v1` contract as the dashboard, the agent instructions, and the MCP server.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g staticx
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Login
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
staticx login --base-url "https://staticx.site/api/v1" --token "STATICX_API_TOKEN"
|
|
15
|
+
staticx whoami
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Core commands
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
staticx workspaces
|
|
22
|
+
staticx sites --workspace-id WORKSPACE_ID
|
|
23
|
+
staticx create --workspace-id WORKSPACE_ID --name "Marketing Site"
|
|
24
|
+
staticx deploy --site-id SITE_ID --dir dist
|
|
25
|
+
staticx logs --site-id SITE_ID
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Token scopes
|
|
29
|
+
|
|
30
|
+
- Global token: account-wide access for internal operator tools.
|
|
31
|
+
- Site token: one-site deploys, logs, and release verification.
|
|
32
|
+
- Workspace token: several sites inside one workspace.
|
|
33
|
+
|
|
34
|
+
Generate those tokens from:
|
|
35
|
+
|
|
36
|
+
- `Settings → API tokens` for Global
|
|
37
|
+
- `Project Settings → Agent deploy` for Site
|
|
38
|
+
- `Workspace → Agent deploy` for Workspace
|
|
39
|
+
|
|
40
|
+
## Notes
|
|
41
|
+
|
|
42
|
+
- `staticx login` stores the base URL and bearer token locally.
|
|
43
|
+
- `staticx whoami` verifies the token with `GET /user`.
|
|
44
|
+
- `staticx deploy` zips the contents of the given build directory, uploads them, runs a build check, and publishes the successful build.
|
package/bin/staticx.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "staticx",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Public CLI for STATICX using token-authenticated /api/v1 routes.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"publishConfig": {
|
|
7
|
+
"access": "public"
|
|
8
|
+
},
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "https://github.com/madprodworks-coder/static_site.git",
|
|
12
|
+
"directory": "packages/staticx-cli"
|
|
13
|
+
},
|
|
14
|
+
"homepage": "https://staticx.site/documentation/cli",
|
|
15
|
+
"bin": {
|
|
16
|
+
"staticx": "./bin/staticx.js"
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"bin",
|
|
20
|
+
"src",
|
|
21
|
+
"README.md"
|
|
22
|
+
],
|
|
23
|
+
"scripts": {
|
|
24
|
+
"test": "node --test tests/*.test.js"
|
|
25
|
+
},
|
|
26
|
+
"keywords": [
|
|
27
|
+
"staticx",
|
|
28
|
+
"cli",
|
|
29
|
+
"static-sites",
|
|
30
|
+
"deployment",
|
|
31
|
+
"laravel"
|
|
32
|
+
],
|
|
33
|
+
"license": "UNLICENSED",
|
|
34
|
+
"engines": {
|
|
35
|
+
"node": ">=18.17"
|
|
36
|
+
},
|
|
37
|
+
"dependencies": {
|
|
38
|
+
"commander": "^12.1.0",
|
|
39
|
+
"yazl": "^3.3.1"
|
|
40
|
+
}
|
|
41
|
+
}
|
package/src/archive.js
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import fsp from 'node:fs/promises';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { once } from 'node:events';
|
|
6
|
+
import yazl from 'yazl';
|
|
7
|
+
|
|
8
|
+
export async function createZipFromDirectory(directoryPath) {
|
|
9
|
+
const sourceDirectory = path.resolve(directoryPath);
|
|
10
|
+
const stats = await fsp.stat(sourceDirectory).catch(() => null);
|
|
11
|
+
|
|
12
|
+
if (!stats || !stats.isDirectory()) {
|
|
13
|
+
throw new Error(`Build directory not found: ${sourceDirectory}`);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
await assertEntryHtmlExists(sourceDirectory);
|
|
17
|
+
|
|
18
|
+
const tempPath = path.join(
|
|
19
|
+
await fsp.mkdtemp(path.join(os.tmpdir(), 'staticx-cli-')),
|
|
20
|
+
`site-${Date.now()}.zip`,
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
const zipFile = new yazl.ZipFile();
|
|
24
|
+
const output = fs.createWriteStream(tempPath);
|
|
25
|
+
zipFile.outputStream.pipe(output);
|
|
26
|
+
|
|
27
|
+
for (const filePath of await collectFiles(sourceDirectory)) {
|
|
28
|
+
const relativePath = path.relative(sourceDirectory, filePath).replaceAll(path.sep, '/');
|
|
29
|
+
zipFile.addFile(filePath, relativePath);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
zipFile.end();
|
|
33
|
+
await once(output, 'close');
|
|
34
|
+
|
|
35
|
+
return tempPath;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function assertEntryHtmlExists(directoryPath) {
|
|
39
|
+
const candidates = ['index.html', 'index.htm'];
|
|
40
|
+
|
|
41
|
+
for (const candidate of candidates) {
|
|
42
|
+
try {
|
|
43
|
+
const stats = await fsp.stat(path.join(directoryPath, candidate));
|
|
44
|
+
if (stats.isFile()) {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
} catch {
|
|
48
|
+
// Keep checking.
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
throw new Error('Build directory must contain index.html or index.htm at its root.');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function collectFiles(directoryPath) {
|
|
56
|
+
const entries = await fsp.readdir(directoryPath, { withFileTypes: true });
|
|
57
|
+
const files = [];
|
|
58
|
+
|
|
59
|
+
for (const entry of entries) {
|
|
60
|
+
const entryPath = path.join(directoryPath, entry.name);
|
|
61
|
+
|
|
62
|
+
if (entry.isDirectory()) {
|
|
63
|
+
files.push(...await collectFiles(entryPath));
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (entry.isFile()) {
|
|
68
|
+
files.push(entryPath);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return files;
|
|
73
|
+
}
|
package/src/auth.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { loadConfig, normalizeBaseUrl, saveConfig } from './config.js';
|
|
2
|
+
|
|
3
|
+
export async function resolveRuntimeAuth(options = {}) {
|
|
4
|
+
const config = await loadConfig();
|
|
5
|
+
const baseUrl = options.baseUrl || process.env.STATICX_API_BASE_URL || config.baseUrl;
|
|
6
|
+
const token = options.token || process.env.STATICX_API_TOKEN || config.token;
|
|
7
|
+
|
|
8
|
+
if (!baseUrl) {
|
|
9
|
+
throw new Error('No STATICX base URL found. Pass --base-url or run `staticx login` first.');
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
if (!token) {
|
|
13
|
+
throw new Error('No STATICX token found. Pass --token or run `staticx login` first.');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return {
|
|
17
|
+
baseUrl: normalizeBaseUrl(baseUrl),
|
|
18
|
+
token: String(token).trim(),
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export async function persistLogin(baseUrl, token) {
|
|
23
|
+
await saveConfig({
|
|
24
|
+
baseUrl: normalizeBaseUrl(baseUrl),
|
|
25
|
+
token: String(token).trim(),
|
|
26
|
+
updatedAt: new Date().toISOString(),
|
|
27
|
+
});
|
|
28
|
+
}
|
package/src/config.js
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
|
|
5
|
+
const APP_DIR = 'staticx';
|
|
6
|
+
const CONFIG_FILE = 'config.json';
|
|
7
|
+
|
|
8
|
+
export function resolveConfigDir() {
|
|
9
|
+
if (process.env.STATICX_CONFIG_DIR) {
|
|
10
|
+
return process.env.STATICX_CONFIG_DIR;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
if (process.platform === 'win32') {
|
|
14
|
+
return path.join(process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming'), APP_DIR);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if (process.env.XDG_CONFIG_HOME) {
|
|
18
|
+
return path.join(process.env.XDG_CONFIG_HOME, APP_DIR);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return path.join(os.homedir(), '.config', APP_DIR);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function resolveConfigPath() {
|
|
25
|
+
return path.join(resolveConfigDir(), CONFIG_FILE);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function loadConfig() {
|
|
29
|
+
try {
|
|
30
|
+
const content = await fs.readFile(resolveConfigPath(), 'utf8');
|
|
31
|
+
return JSON.parse(content);
|
|
32
|
+
} catch (error) {
|
|
33
|
+
if (error && typeof error === 'object' && 'code' in error && error.code === 'ENOENT') {
|
|
34
|
+
return {};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
throw new Error(`Could not read STATICX CLI config: ${error instanceof Error ? error.message : String(error)}`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export async function saveConfig(config) {
|
|
42
|
+
const configDir = resolveConfigDir();
|
|
43
|
+
await fs.mkdir(configDir, { recursive: true });
|
|
44
|
+
await fs.writeFile(resolveConfigPath(), JSON.stringify(config, null, 2) + '\n', 'utf8');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export async function clearConfig() {
|
|
48
|
+
try {
|
|
49
|
+
await fs.unlink(resolveConfigPath());
|
|
50
|
+
} catch (error) {
|
|
51
|
+
if (error && typeof error === 'object' && 'code' in error && error.code === 'ENOENT') {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
throw error;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function normalizeBaseUrl(baseUrl) {
|
|
60
|
+
const value = String(baseUrl || '').trim().replace(/\/+$/, '');
|
|
61
|
+
|
|
62
|
+
if (value === '') {
|
|
63
|
+
throw new Error('STATICX base URL is required. Pass --base-url or run `staticx login` first.');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
const url = new URL(value);
|
|
68
|
+
|
|
69
|
+
if (!['http:', 'https:'].includes(url.protocol)) {
|
|
70
|
+
throw new Error('STATICX base URL must use http or https.');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return url.toString().replace(/\/+$/, '');
|
|
74
|
+
} catch (error) {
|
|
75
|
+
throw new Error(`Invalid STATICX base URL: ${value}`);
|
|
76
|
+
}
|
|
77
|
+
}
|
package/src/files.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
|
|
3
|
+
export async function fileToBlob(filePath, type = 'application/octet-stream') {
|
|
4
|
+
if (typeof fs.openAsBlob === 'function') {
|
|
5
|
+
return fs.openAsBlob(filePath, { type });
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const buffer = await fs.readFile(filePath);
|
|
9
|
+
return new Blob([buffer], { type });
|
|
10
|
+
}
|
package/src/http.js
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
export class StaticxApiError extends Error {
|
|
2
|
+
constructor(message, { status, payload } = {}) {
|
|
3
|
+
super(message);
|
|
4
|
+
this.name = 'StaticxApiError';
|
|
5
|
+
this.status = status;
|
|
6
|
+
this.payload = payload;
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export class StaticxApiClient {
|
|
11
|
+
constructor({ baseUrl, token, fetchImpl = fetch }) {
|
|
12
|
+
this.baseUrl = baseUrl.replace(/\/+$/, '');
|
|
13
|
+
this.token = token;
|
|
14
|
+
this.fetchImpl = fetchImpl;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async get(path, options = {}) {
|
|
18
|
+
return this.request(path, { method: 'GET', ...options });
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async post(path, options = {}) {
|
|
22
|
+
return this.request(path, { method: 'POST', ...options });
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async delete(path, options = {}) {
|
|
26
|
+
return this.request(path, { method: 'DELETE', ...options });
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async request(path, { method = 'GET', query, json, formData } = {}) {
|
|
30
|
+
const url = new URL(path.replace(/^\//, ''), `${this.baseUrl}/`);
|
|
31
|
+
|
|
32
|
+
if (query) {
|
|
33
|
+
for (const [key, value] of Object.entries(query)) {
|
|
34
|
+
if (value === undefined || value === null || value === '') {
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
url.searchParams.set(key, String(value));
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const headers = {
|
|
43
|
+
Accept: 'application/json',
|
|
44
|
+
Authorization: `Bearer ${this.token}`,
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
let body;
|
|
48
|
+
|
|
49
|
+
if (formData) {
|
|
50
|
+
body = formData;
|
|
51
|
+
} else if (json !== undefined) {
|
|
52
|
+
body = JSON.stringify(json);
|
|
53
|
+
headers['Content-Type'] = 'application/json';
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const response = await this.fetchImpl(url, {
|
|
57
|
+
method,
|
|
58
|
+
headers,
|
|
59
|
+
body,
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
const payload = await parsePayload(response);
|
|
63
|
+
|
|
64
|
+
if (!response.ok) {
|
|
65
|
+
throw new StaticxApiError(apiErrorMessage(payload, response), {
|
|
66
|
+
status: response.status,
|
|
67
|
+
payload,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return payload;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function parsePayload(response) {
|
|
76
|
+
const contentType = response.headers.get('content-type') || '';
|
|
77
|
+
|
|
78
|
+
if (contentType.includes('application/json')) {
|
|
79
|
+
return response.json();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const text = await response.text();
|
|
83
|
+
|
|
84
|
+
if (text === '') {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
try {
|
|
89
|
+
return JSON.parse(text);
|
|
90
|
+
} catch {
|
|
91
|
+
return { message: text };
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function apiErrorMessage(payload, response) {
|
|
96
|
+
if (payload && typeof payload === 'object' && 'message' in payload && payload.message) {
|
|
97
|
+
return String(payload.message);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return `${response.status} ${response.statusText}`.trim();
|
|
101
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { Command } from 'commander';
|
|
4
|
+
import { createZipFromDirectory } from './archive.js';
|
|
5
|
+
import { resolveRuntimeAuth, persistLogin } from './auth.js';
|
|
6
|
+
import { clearConfig, normalizeBaseUrl } from './config.js';
|
|
7
|
+
import { fileToBlob } from './files.js';
|
|
8
|
+
import { StaticxApiClient } from './http.js';
|
|
9
|
+
import { formatTokenSummary, printJson, printKeyValue, printTable } from './output.js';
|
|
10
|
+
|
|
11
|
+
export async function run(argv) {
|
|
12
|
+
const program = new Command();
|
|
13
|
+
|
|
14
|
+
program
|
|
15
|
+
.name('staticx')
|
|
16
|
+
.description('Public CLI for STATICX using token-authenticated /api/v1 routes.')
|
|
17
|
+
.version('0.1.0')
|
|
18
|
+
.option('--base-url <url>', 'STATICX API base URL, for example https://staticx.site/api/v1')
|
|
19
|
+
.option('--token <token>', 'STATICX bearer token')
|
|
20
|
+
.showHelpAfterError();
|
|
21
|
+
|
|
22
|
+
program
|
|
23
|
+
.command('login')
|
|
24
|
+
.description('Store the base URL and token locally, then verify them with GET /user.')
|
|
25
|
+
.requiredOption('--base-url <url>', 'STATICX API base URL')
|
|
26
|
+
.requiredOption('--token <token>', 'STATICX bearer token')
|
|
27
|
+
.option('--json', 'Print JSON output')
|
|
28
|
+
.action(async (options) => {
|
|
29
|
+
const baseUrl = normalizeBaseUrl(options.baseUrl);
|
|
30
|
+
const token = String(options.token).trim();
|
|
31
|
+
const client = new StaticxApiClient({ baseUrl, token });
|
|
32
|
+
const response = await client.get('/user');
|
|
33
|
+
|
|
34
|
+
await persistLogin(baseUrl, token);
|
|
35
|
+
|
|
36
|
+
if (options.json) {
|
|
37
|
+
printJson({
|
|
38
|
+
message: 'Login successful.',
|
|
39
|
+
base_url: baseUrl,
|
|
40
|
+
user: response?.data ?? null,
|
|
41
|
+
});
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
console.log('Login successful.');
|
|
46
|
+
printKeyValue('Base URL', baseUrl);
|
|
47
|
+
printKeyValue('User', response?.data?.email || response?.data?.name || 'Unknown');
|
|
48
|
+
printKeyValue('Token', formatTokenSummary(response?.data?.current_token || response?.data?.token));
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
program
|
|
52
|
+
.command('logout')
|
|
53
|
+
.description('Remove the locally stored STATICX CLI credentials.')
|
|
54
|
+
.action(async () => {
|
|
55
|
+
await clearConfig();
|
|
56
|
+
console.log('Local STATICX CLI credentials removed.');
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
program
|
|
60
|
+
.command('whoami')
|
|
61
|
+
.description('Verify the stored token and print the current user/token metadata.')
|
|
62
|
+
.option('--json', 'Print JSON output')
|
|
63
|
+
.action(async (options, command) => {
|
|
64
|
+
const client = await clientFromCommand(command);
|
|
65
|
+
const response = await client.get('/user');
|
|
66
|
+
|
|
67
|
+
if (options.json) {
|
|
68
|
+
printJson(response?.data ?? null);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const user = response?.data ?? {};
|
|
73
|
+
console.log(user.name || user.email || 'Unknown user');
|
|
74
|
+
if (user.email) {
|
|
75
|
+
printKeyValue('Email', user.email);
|
|
76
|
+
}
|
|
77
|
+
printKeyValue('Token', formatTokenSummary(user.current_token || user.token));
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
program
|
|
81
|
+
.command('workspaces')
|
|
82
|
+
.description('List workspaces visible to the current token.')
|
|
83
|
+
.option('--json', 'Print JSON output')
|
|
84
|
+
.action(async (options, command) => {
|
|
85
|
+
const client = await clientFromCommand(command);
|
|
86
|
+
const response = await client.get('/workspaces');
|
|
87
|
+
const workspaces = response?.data ?? [];
|
|
88
|
+
|
|
89
|
+
if (options.json) {
|
|
90
|
+
printJson(workspaces);
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
printTable(workspaces.map((workspace) => ({
|
|
95
|
+
id: workspace.id,
|
|
96
|
+
name: workspace.name,
|
|
97
|
+
role: workspace.role,
|
|
98
|
+
sites: workspace.projects_count,
|
|
99
|
+
})));
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
program
|
|
103
|
+
.command('sites')
|
|
104
|
+
.description('List sites visible to the current token, optionally filtered by workspace.')
|
|
105
|
+
.option('--workspace-id <id>', 'Filter by workspace ID')
|
|
106
|
+
.option('--json', 'Print JSON output')
|
|
107
|
+
.action(async (options, command) => {
|
|
108
|
+
const client = await clientFromCommand(command);
|
|
109
|
+
const response = await client.get('/projects', {
|
|
110
|
+
query: options.workspaceId ? { workspace_id: options.workspaceId } : undefined,
|
|
111
|
+
});
|
|
112
|
+
const sites = response?.data ?? [];
|
|
113
|
+
|
|
114
|
+
if (options.json) {
|
|
115
|
+
printJson(sites);
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
printTable(sites.map((site) => ({
|
|
120
|
+
id: site.id,
|
|
121
|
+
name: site.name,
|
|
122
|
+
host: site.host,
|
|
123
|
+
workspace: site.workspace?.name || '',
|
|
124
|
+
status: site.status,
|
|
125
|
+
})));
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
program
|
|
129
|
+
.command('create')
|
|
130
|
+
.description('Create a new site, optionally inside a workspace or from a ZIP/source URL.')
|
|
131
|
+
.requiredOption('--name <name>', 'Site name')
|
|
132
|
+
.option('--workspace-id <id>', 'Workspace ID')
|
|
133
|
+
.option('--description <description>', 'Site description')
|
|
134
|
+
.option('--archive <path>', 'ZIP archive path to seed the site')
|
|
135
|
+
.option('--source-url <url>', 'Public URL to import into the site')
|
|
136
|
+
.option('--json', 'Print JSON output')
|
|
137
|
+
.action(async (options, command) => {
|
|
138
|
+
if (options.archive && options.sourceUrl) {
|
|
139
|
+
throw new Error('Use either --archive or --source-url, not both.');
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const client = await clientFromCommand(command);
|
|
143
|
+
const response = options.archive || options.sourceUrl
|
|
144
|
+
? await client.post('/projects', {
|
|
145
|
+
formData: await createProjectFormData(options),
|
|
146
|
+
})
|
|
147
|
+
: await client.post('/projects', {
|
|
148
|
+
json: compactObject({
|
|
149
|
+
workspace_id: options.workspaceId,
|
|
150
|
+
name: options.name,
|
|
151
|
+
description: options.description,
|
|
152
|
+
}),
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
if (options.json) {
|
|
156
|
+
printJson(response);
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const project = response?.data ?? {};
|
|
161
|
+
console.log('Site created.');
|
|
162
|
+
printKeyValue('ID', project.id ?? 'Unknown');
|
|
163
|
+
printKeyValue('Name', project.name ?? options.name);
|
|
164
|
+
printKeyValue('Host', project.host || 'Pending');
|
|
165
|
+
printKeyValue('Public URL', project.public_url || 'Pending');
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
program
|
|
169
|
+
.command('deploy')
|
|
170
|
+
.description('Zip a local build directory, upload it, build-check it, and publish it to one site.')
|
|
171
|
+
.requiredOption('--site-id <id>', 'Site ID')
|
|
172
|
+
.requiredOption('--dir <path>', 'Built site directory, for example dist')
|
|
173
|
+
.option('--json', 'Print JSON output')
|
|
174
|
+
.action(async (options, command) => {
|
|
175
|
+
const client = await clientFromCommand(command);
|
|
176
|
+
const zipPath = await createZipFromDirectory(options.dir);
|
|
177
|
+
|
|
178
|
+
try {
|
|
179
|
+
const uploadFormData = new FormData();
|
|
180
|
+
uploadFormData.set('mode', 'zip');
|
|
181
|
+
uploadFormData.set('overwrite_confirmed', '1');
|
|
182
|
+
uploadFormData.set('archive', await fileToBlob(zipPath, 'application/zip'), path.basename(zipPath));
|
|
183
|
+
|
|
184
|
+
const upload = await client.post(`/projects/${options.siteId}/files`, { formData: uploadFormData });
|
|
185
|
+
const build = await client.post(`/projects/${options.siteId}/builds`);
|
|
186
|
+
const buildId = build?.data?.id;
|
|
187
|
+
|
|
188
|
+
if (!buildId) {
|
|
189
|
+
throw new Error('Build response did not include a build ID.');
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const deployment = await client.post(`/projects/${options.siteId}/deployments`, {
|
|
193
|
+
json: { build_id: buildId },
|
|
194
|
+
});
|
|
195
|
+
const project = await client.get(`/projects/${options.siteId}`);
|
|
196
|
+
|
|
197
|
+
const result = {
|
|
198
|
+
project: project?.data ?? null,
|
|
199
|
+
upload: upload?.data ?? null,
|
|
200
|
+
build: build?.data ?? null,
|
|
201
|
+
deployment: deployment?.data ?? null,
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
if (options.json) {
|
|
205
|
+
printJson(result);
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
console.log('Deployment completed.');
|
|
210
|
+
printKeyValue('Site', result.project?.name || options.siteId);
|
|
211
|
+
printKeyValue('Build', result.build?.id || 'Unknown');
|
|
212
|
+
printKeyValue('Deployment', result.deployment?.id || 'Unknown');
|
|
213
|
+
printKeyValue('Public URL', result.project?.public_url || 'Pending');
|
|
214
|
+
} finally {
|
|
215
|
+
await fs.rm(path.dirname(zipPath), { recursive: true, force: true });
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
program
|
|
220
|
+
.command('logs')
|
|
221
|
+
.description('Read recent activity logs for one site.')
|
|
222
|
+
.requiredOption('--site-id <id>', 'Site ID')
|
|
223
|
+
.option('--limit <number>', 'Maximum number of log rows', '25')
|
|
224
|
+
.option('--json', 'Print JSON output')
|
|
225
|
+
.action(async (options, command) => {
|
|
226
|
+
const client = await clientFromCommand(command);
|
|
227
|
+
const response = await client.get(`/projects/${options.siteId}/logs`, {
|
|
228
|
+
query: { limit: options.limit },
|
|
229
|
+
});
|
|
230
|
+
const logs = response?.data ?? [];
|
|
231
|
+
|
|
232
|
+
if (options.json) {
|
|
233
|
+
printJson(logs);
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
printTable(logs.map((entry) => ({
|
|
238
|
+
time: entry.created_at,
|
|
239
|
+
level: entry.level,
|
|
240
|
+
event: entry.event,
|
|
241
|
+
message: entry.message,
|
|
242
|
+
})));
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
await program.parseAsync(argv);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
async function clientFromCommand(command) {
|
|
249
|
+
const options = command.optsWithGlobals ? command.optsWithGlobals() : command.opts();
|
|
250
|
+
const auth = await resolveRuntimeAuth(options);
|
|
251
|
+
|
|
252
|
+
return new StaticxApiClient(auth);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
async function createProjectFormData(options) {
|
|
256
|
+
const formData = new FormData();
|
|
257
|
+
formData.set('name', options.name);
|
|
258
|
+
|
|
259
|
+
if (options.workspaceId) {
|
|
260
|
+
formData.set('workspace_id', String(options.workspaceId));
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (options.description) {
|
|
264
|
+
formData.set('description', options.description);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (options.archive) {
|
|
268
|
+
const archivePath = path.resolve(options.archive);
|
|
269
|
+
formData.set('archive', await fileToBlob(archivePath, 'application/zip'), path.basename(archivePath));
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (options.sourceUrl) {
|
|
273
|
+
formData.set('source_url', options.sourceUrl);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return formData;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function compactObject(values) {
|
|
280
|
+
return Object.fromEntries(
|
|
281
|
+
Object.entries(values).filter(([, value]) => value !== undefined && value !== null && value !== ''),
|
|
282
|
+
);
|
|
283
|
+
}
|
package/src/output.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export function printJson(value) {
|
|
2
|
+
console.log(JSON.stringify(value, null, 2));
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export function printKeyValue(label, value) {
|
|
6
|
+
console.log(`${label}: ${value}`);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function printTable(rows) {
|
|
10
|
+
console.table(rows);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function formatTokenSummary(token) {
|
|
14
|
+
if (!token) {
|
|
15
|
+
return 'No token metadata returned.';
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const scope = token.scope_label || token.scope_type || token.kind || 'Unknown scope';
|
|
19
|
+
const preset = token.preset_label || token.preset || 'Unknown access level';
|
|
20
|
+
const expiry = token.expires_at || 'Never expires';
|
|
21
|
+
|
|
22
|
+
return `${scope} · ${preset} · ${expiry}`;
|
|
23
|
+
}
|