staticx 0.1.1 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +28 -1
- package/package.json +2 -2
- package/src/archive.js +21 -5
- package/src/index.js +87 -20
- package/src/output.js +174 -1
package/README.md
CHANGED
|
@@ -13,18 +13,44 @@ npm install -g staticx
|
|
|
13
13
|
```bash
|
|
14
14
|
staticx login --base-url "https://staticx.site/api/v1" --token "STATICX_API_TOKEN"
|
|
15
15
|
staticx whoami
|
|
16
|
+
staticx guide
|
|
16
17
|
```
|
|
17
18
|
|
|
19
|
+
`staticx login` stores the credentials locally, verifies them with `GET /user`, and prints the active user, token scope, access level, expiry, and next safe commands.
|
|
20
|
+
|
|
21
|
+
Use `staticx guide` or `staticx commands` when you want the full command map from the terminal.
|
|
22
|
+
|
|
18
23
|
## Core commands
|
|
19
24
|
|
|
20
25
|
```bash
|
|
26
|
+
staticx guide
|
|
21
27
|
staticx workspaces
|
|
22
28
|
staticx sites --workspace-id WORKSPACE_ID
|
|
23
29
|
staticx create --workspace-id WORKSPACE_ID --name "Marketing Site"
|
|
30
|
+
staticx create --name "Imported Site" --archive site.zip
|
|
31
|
+
staticx create --name "Imported Site" --source-url "https://example.com"
|
|
24
32
|
staticx deploy --site-id SITE_ID --dir dist
|
|
33
|
+
staticx domain --site-id SITE_ID --domain app.example.com
|
|
34
|
+
staticx domain-status --site-id SITE_ID
|
|
25
35
|
staticx logs --site-id SITE_ID
|
|
26
36
|
```
|
|
27
37
|
|
|
38
|
+
## Custom domains
|
|
39
|
+
|
|
40
|
+
Use the CLI when you want to connect a domain without opening the dashboard.
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
staticx domain --site-id SITE_ID --domain app.example.com
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
The command moves the site to that domain and prints the single DNS record to create. After the DNS record is added, StaticX detects it and activates SSL automatically.
|
|
47
|
+
|
|
48
|
+
Check progress any time:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
staticx domain-status --site-id SITE_ID
|
|
52
|
+
```
|
|
53
|
+
|
|
28
54
|
## Token scopes
|
|
29
55
|
|
|
30
56
|
- Global token: account-wide access for internal operator tools.
|
|
@@ -41,4 +67,5 @@ Generate those tokens from:
|
|
|
41
67
|
|
|
42
68
|
- `staticx login` stores the base URL and bearer token locally.
|
|
43
69
|
- `staticx whoami` verifies the token with `GET /user`.
|
|
44
|
-
- `staticx deploy` zips the contents of the given build directory, uploads them,
|
|
70
|
+
- `staticx deploy` zips the contents of the given build directory, uploads them, and publishes a new release. The directory must contain `index.html` or `index.htm` plus `404.html` at its root.
|
|
71
|
+
- `staticx domain` calls `POST /projects/{project}/domain` and returns the DNS record plus activation status.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "staticx",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "Public CLI for STATICX using token-authenticated /api/v1 routes.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"publishConfig": {
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
"node": ">=18.17"
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"commander": "^
|
|
38
|
+
"commander": "^13.1.0",
|
|
39
39
|
"yazl": "^3.3.1"
|
|
40
40
|
}
|
|
41
41
|
}
|
package/src/archive.js
CHANGED
|
@@ -13,7 +13,7 @@ export async function createZipFromDirectory(directoryPath) {
|
|
|
13
13
|
throw new Error(`Build directory not found: ${sourceDirectory}`);
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
await
|
|
16
|
+
await assertRequiredRootFilesExist(sourceDirectory);
|
|
17
17
|
|
|
18
18
|
const tempPath = path.join(
|
|
19
19
|
await fsp.mkdtemp(path.join(os.tmpdir(), 'staticx-cli-')),
|
|
@@ -35,21 +35,37 @@ export async function createZipFromDirectory(directoryPath) {
|
|
|
35
35
|
return tempPath;
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
async function
|
|
39
|
-
const
|
|
38
|
+
async function assertRequiredRootFilesExist(directoryPath) {
|
|
39
|
+
const hasEntryHtml = await fileExists(directoryPath, ['index.html', 'index.htm']);
|
|
40
|
+
const hasNotFoundHtml = await fileExists(directoryPath, ['404.html']);
|
|
41
|
+
const missing = [];
|
|
40
42
|
|
|
43
|
+
if (!hasEntryHtml) {
|
|
44
|
+
missing.push('index.html or index.htm');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (!hasNotFoundHtml) {
|
|
48
|
+
missing.push('404.html');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (missing.length > 0) {
|
|
52
|
+
throw new Error(`Build directory must contain required root files: ${missing.join(', ')}.`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function fileExists(directoryPath, candidates) {
|
|
41
57
|
for (const candidate of candidates) {
|
|
42
58
|
try {
|
|
43
59
|
const stats = await fsp.stat(path.join(directoryPath, candidate));
|
|
44
60
|
if (stats.isFile()) {
|
|
45
|
-
return;
|
|
61
|
+
return true;
|
|
46
62
|
}
|
|
47
63
|
} catch {
|
|
48
64
|
// Keep checking.
|
|
49
65
|
}
|
|
50
66
|
}
|
|
51
67
|
|
|
52
|
-
|
|
68
|
+
return false;
|
|
53
69
|
}
|
|
54
70
|
|
|
55
71
|
async function collectFiles(directoryPath) {
|
package/src/index.js
CHANGED
|
@@ -6,7 +6,18 @@ import { resolveRuntimeAuth, persistLogin } from './auth.js';
|
|
|
6
6
|
import { clearConfig, normalizeBaseUrl } from './config.js';
|
|
7
7
|
import { fileToBlob } from './files.js';
|
|
8
8
|
import { StaticxApiClient } from './http.js';
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
cliGuideJson,
|
|
11
|
+
formatTokenSummary,
|
|
12
|
+
printCliGuide,
|
|
13
|
+
printDomainSetup,
|
|
14
|
+
printJson,
|
|
15
|
+
printKeyValue,
|
|
16
|
+
printLoginSuccess,
|
|
17
|
+
printTable,
|
|
18
|
+
} from './output.js';
|
|
19
|
+
|
|
20
|
+
const CLI_VERSION = '0.1.3';
|
|
10
21
|
|
|
11
22
|
export async function run(argv) {
|
|
12
23
|
const program = new Command();
|
|
@@ -14,8 +25,33 @@ export async function run(argv) {
|
|
|
14
25
|
program
|
|
15
26
|
.name('staticx')
|
|
16
27
|
.description('Public CLI for STATICX using token-authenticated /api/v1 routes.')
|
|
17
|
-
.version(
|
|
18
|
-
.showHelpAfterError()
|
|
28
|
+
.version(CLI_VERSION)
|
|
29
|
+
.showHelpAfterError()
|
|
30
|
+
.addHelpText('after', `
|
|
31
|
+
|
|
32
|
+
Common workflow:
|
|
33
|
+
staticx login --base-url "https://staticx.site/api/v1" --token "STATICX_API_TOKEN"
|
|
34
|
+
staticx guide
|
|
35
|
+
staticx deploy --site-id SITE_ID --dir dist
|
|
36
|
+
staticx domain --site-id SITE_ID --domain app.example.com
|
|
37
|
+
|
|
38
|
+
Deploy rule:
|
|
39
|
+
The build directory must contain index.html or index.htm and 404.html at its root.
|
|
40
|
+
`);
|
|
41
|
+
|
|
42
|
+
program
|
|
43
|
+
.command('guide')
|
|
44
|
+
.alias('commands')
|
|
45
|
+
.description('Explain the STATICX CLI commands and the recommended release workflow.')
|
|
46
|
+
.option('--json', 'Print JSON output')
|
|
47
|
+
.action((options) => {
|
|
48
|
+
if (options.json) {
|
|
49
|
+
printJson(cliGuideJson());
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
printCliGuide();
|
|
54
|
+
});
|
|
19
55
|
|
|
20
56
|
program
|
|
21
57
|
.command('login')
|
|
@@ -40,10 +76,11 @@ export async function run(argv) {
|
|
|
40
76
|
return;
|
|
41
77
|
}
|
|
42
78
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
79
|
+
printLoginSuccess({
|
|
80
|
+
baseUrl,
|
|
81
|
+
user: response?.data ?? null,
|
|
82
|
+
token: response?.data?.current_token || response?.data?.token,
|
|
83
|
+
});
|
|
47
84
|
});
|
|
48
85
|
|
|
49
86
|
program
|
|
@@ -173,7 +210,7 @@ export async function run(argv) {
|
|
|
173
210
|
|
|
174
211
|
program
|
|
175
212
|
.command('deploy')
|
|
176
|
-
.description('Zip a local build directory, upload it,
|
|
213
|
+
.description('Zip a local build directory, upload it, and publish it to one site.')
|
|
177
214
|
.requiredOption('--site-id <id>', 'Site ID')
|
|
178
215
|
.requiredOption('--dir <path>', 'Built site directory, for example dist')
|
|
179
216
|
.option('--json', 'Print JSON output')
|
|
@@ -190,22 +227,12 @@ export async function run(argv) {
|
|
|
190
227
|
uploadFormData.set('archive', await fileToBlob(zipPath, 'application/zip'), path.basename(zipPath));
|
|
191
228
|
|
|
192
229
|
const upload = await client.post(`/projects/${options.siteId}/files`, { formData: uploadFormData });
|
|
193
|
-
const
|
|
194
|
-
const buildId = build?.data?.id;
|
|
195
|
-
|
|
196
|
-
if (!buildId) {
|
|
197
|
-
throw new Error('Build response did not include a build ID.');
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
const deployment = await client.post(`/projects/${options.siteId}/deployments`, {
|
|
201
|
-
json: { build_id: buildId },
|
|
202
|
-
});
|
|
230
|
+
const deployment = await client.post(`/projects/${options.siteId}/deployments`);
|
|
203
231
|
const project = await client.get(`/projects/${options.siteId}`);
|
|
204
232
|
|
|
205
233
|
const result = {
|
|
206
234
|
project: project?.data ?? null,
|
|
207
235
|
upload: upload?.data ?? null,
|
|
208
|
-
build: build?.data ?? null,
|
|
209
236
|
deployment: deployment?.data ?? null,
|
|
210
237
|
};
|
|
211
238
|
|
|
@@ -216,7 +243,6 @@ export async function run(argv) {
|
|
|
216
243
|
|
|
217
244
|
console.log('Deployment completed.');
|
|
218
245
|
printKeyValue('Site', result.project?.name || options.siteId);
|
|
219
|
-
printKeyValue('Build', result.build?.id || 'Unknown');
|
|
220
246
|
printKeyValue('Deployment', result.deployment?.id || 'Unknown');
|
|
221
247
|
printKeyValue('Public URL', result.project?.public_url || 'Pending');
|
|
222
248
|
} finally {
|
|
@@ -252,6 +278,47 @@ export async function run(argv) {
|
|
|
252
278
|
})));
|
|
253
279
|
});
|
|
254
280
|
|
|
281
|
+
program
|
|
282
|
+
.command('domain')
|
|
283
|
+
.description('Move a site to a custom domain and print the one DNS record to create.')
|
|
284
|
+
.requiredOption('--site-id <id>', 'Site ID')
|
|
285
|
+
.requiredOption('--domain <domain>', 'Custom domain, for example app.example.com')
|
|
286
|
+
.option('--json', 'Print JSON output')
|
|
287
|
+
.option('--base-url <url>', 'Override the stored STATICX API base URL')
|
|
288
|
+
.option('--token <token>', 'Override the stored STATICX bearer token')
|
|
289
|
+
.action(async (options, command) => {
|
|
290
|
+
const client = await clientFromCommand(command);
|
|
291
|
+
const response = await client.post(`/projects/${options.siteId}/domain`, {
|
|
292
|
+
json: { domain: options.domain },
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
if (options.json) {
|
|
296
|
+
printJson(response);
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
printDomainSetup(response);
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
program
|
|
304
|
+
.command('domain-status')
|
|
305
|
+
.description('Check the custom domain status for one site.')
|
|
306
|
+
.requiredOption('--site-id <id>', 'Site ID')
|
|
307
|
+
.option('--json', 'Print JSON output')
|
|
308
|
+
.option('--base-url <url>', 'Override the stored STATICX API base URL')
|
|
309
|
+
.option('--token <token>', 'Override the stored STATICX bearer token')
|
|
310
|
+
.action(async (options, command) => {
|
|
311
|
+
const client = await clientFromCommand(command);
|
|
312
|
+
const response = await client.get(`/projects/${options.siteId}/domain`);
|
|
313
|
+
|
|
314
|
+
if (options.json) {
|
|
315
|
+
printJson(response);
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
printDomainSetup({ message: 'Custom domain status.', data: response?.data ?? response }, { statusOnly: true });
|
|
320
|
+
});
|
|
321
|
+
|
|
255
322
|
await program.parseAsync(argv);
|
|
256
323
|
}
|
|
257
324
|
|
package/src/output.js
CHANGED
|
@@ -10,6 +10,138 @@ export function printTable(rows) {
|
|
|
10
10
|
console.table(rows);
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
+
export function printDomainSetup(response, { statusOnly = false } = {}) {
|
|
14
|
+
const data = response?.data ?? {};
|
|
15
|
+
const project = data.project ?? {};
|
|
16
|
+
const domain = data.custom_domain ?? null;
|
|
17
|
+
const records = data.dns?.records ?? [];
|
|
18
|
+
const steps = Array.isArray(data.steps) ? data.steps : [];
|
|
19
|
+
|
|
20
|
+
console.log(response?.message || (statusOnly ? 'Custom domain status.' : 'Custom domain setup started.'));
|
|
21
|
+
console.log('');
|
|
22
|
+
printAlignedRows([
|
|
23
|
+
['Site', project.name || project.id || 'Unknown'],
|
|
24
|
+
['Current URL', project.public_url || 'Pending'],
|
|
25
|
+
['Custom domain', domain?.host || 'Not connected'],
|
|
26
|
+
['Status', domain?.status_label || data.status?.label || 'Pending'],
|
|
27
|
+
]);
|
|
28
|
+
|
|
29
|
+
if (domain?.status_message || data.status?.message) {
|
|
30
|
+
console.log('');
|
|
31
|
+
console.log(domain?.status_message || data.status?.message);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (records.length > 0) {
|
|
35
|
+
console.log('');
|
|
36
|
+
console.log('Create this DNS record:');
|
|
37
|
+
printTable(records.map((record) => ({
|
|
38
|
+
type: record.type,
|
|
39
|
+
name: record.name,
|
|
40
|
+
value: record.value,
|
|
41
|
+
})));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (steps.length > 0) {
|
|
45
|
+
console.log('');
|
|
46
|
+
console.log('Next steps');
|
|
47
|
+
steps.forEach((step, index) => {
|
|
48
|
+
console.log(` ${index + 1}. ${step}`);
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function printLoginSuccess({ baseUrl, user, token }) {
|
|
54
|
+
printBanner();
|
|
55
|
+
console.log('Successfully authenticated.');
|
|
56
|
+
console.log('');
|
|
57
|
+
printAlignedRows([
|
|
58
|
+
['Base URL', baseUrl],
|
|
59
|
+
['User', user?.email || user?.name || 'Unknown'],
|
|
60
|
+
['Token', formatTokenSummary(token)],
|
|
61
|
+
]);
|
|
62
|
+
console.log('');
|
|
63
|
+
printCommandGroup('Next commands', [
|
|
64
|
+
['staticx whoami', 'Verify the stored token.'],
|
|
65
|
+
['staticx guide', 'Show the complete CLI workflow.'],
|
|
66
|
+
['staticx sites', 'List the sites visible to this token.'],
|
|
67
|
+
['staticx deploy --site-id SITE_ID --dir dist', 'Upload and publish a build folder.'],
|
|
68
|
+
['staticx domain --site-id SITE_ID --domain app.example.com', 'Move a site to a custom domain.'],
|
|
69
|
+
]);
|
|
70
|
+
console.log('');
|
|
71
|
+
console.log('Build rule: deploy folders must include index.html or index.htm and 404.html at the root.');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function printCliGuide() {
|
|
75
|
+
printBanner();
|
|
76
|
+
console.log('Command guide');
|
|
77
|
+
console.log('');
|
|
78
|
+
printCommandGroup('Authenticate', [
|
|
79
|
+
['staticx login --base-url "https://staticx.site/api/v1" --token "STATICX_API_TOKEN"', 'Store and verify a token.'],
|
|
80
|
+
['staticx whoami', 'Print the current user and token scope.'],
|
|
81
|
+
['staticx logout', 'Remove local CLI credentials.'],
|
|
82
|
+
]);
|
|
83
|
+
console.log('');
|
|
84
|
+
printCommandGroup('Browse account state', [
|
|
85
|
+
['staticx workspaces', 'List visible workspaces.'],
|
|
86
|
+
['staticx sites', 'List visible sites.'],
|
|
87
|
+
['staticx sites --workspace-id WORKSPACE_ID', 'List sites inside one workspace.'],
|
|
88
|
+
]);
|
|
89
|
+
console.log('');
|
|
90
|
+
printCommandGroup('Create and publish', [
|
|
91
|
+
['staticx create --workspace-id WORKSPACE_ID --name "Marketing Site"', 'Create a site.'],
|
|
92
|
+
['staticx create --name "Imported Site" --archive site.zip', 'Create a site from a ZIP archive.'],
|
|
93
|
+
['staticx create --name "Imported Site" --source-url "https://example.com"', 'Import from a public URL.'],
|
|
94
|
+
['staticx deploy --site-id SITE_ID --dir dist', 'Zip, upload, and publish a build folder.'],
|
|
95
|
+
]);
|
|
96
|
+
console.log('');
|
|
97
|
+
printCommandGroup('Custom domains', [
|
|
98
|
+
['staticx domain --site-id SITE_ID --domain app.example.com', 'Move a site to a custom domain and print the DNS record.'],
|
|
99
|
+
['staticx domain-status --site-id SITE_ID', 'Check DNS, SSL, and activation status.'],
|
|
100
|
+
]);
|
|
101
|
+
console.log('');
|
|
102
|
+
printCommandGroup('Inspect', [
|
|
103
|
+
['staticx logs --site-id SITE_ID', 'Read recent site activity.'],
|
|
104
|
+
['staticx logs --site-id SITE_ID --limit 50', 'Read more rows.'],
|
|
105
|
+
]);
|
|
106
|
+
console.log('');
|
|
107
|
+
console.log('Token scopes: use Site tokens for one-site deploys, Workspace tokens for client workspaces, and Global tokens only for broad operator workflows.');
|
|
108
|
+
console.log('Build rule: deploy folders must include index.html or index.htm and 404.html at the root.');
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function cliGuideJson() {
|
|
112
|
+
return {
|
|
113
|
+
install: 'npm install -g staticx',
|
|
114
|
+
commands: {
|
|
115
|
+
authenticate: [
|
|
116
|
+
'staticx login --base-url "https://staticx.site/api/v1" --token "STATICX_API_TOKEN"',
|
|
117
|
+
'staticx whoami',
|
|
118
|
+
'staticx logout',
|
|
119
|
+
],
|
|
120
|
+
browse: [
|
|
121
|
+
'staticx workspaces',
|
|
122
|
+
'staticx sites',
|
|
123
|
+
'staticx sites --workspace-id WORKSPACE_ID',
|
|
124
|
+
],
|
|
125
|
+
publish: [
|
|
126
|
+
'staticx create --workspace-id WORKSPACE_ID --name "Marketing Site"',
|
|
127
|
+
'staticx create --name "Imported Site" --archive site.zip',
|
|
128
|
+
'staticx create --name "Imported Site" --source-url "https://example.com"',
|
|
129
|
+
'staticx deploy --site-id SITE_ID --dir dist',
|
|
130
|
+
],
|
|
131
|
+
domains: [
|
|
132
|
+
'staticx domain --site-id SITE_ID --domain app.example.com',
|
|
133
|
+
'staticx domain-status --site-id SITE_ID',
|
|
134
|
+
],
|
|
135
|
+
inspect: [
|
|
136
|
+
'staticx logs --site-id SITE_ID',
|
|
137
|
+
'staticx logs --site-id SITE_ID --limit 50',
|
|
138
|
+
],
|
|
139
|
+
},
|
|
140
|
+
build_rule: 'Deploy folders must include index.html or index.htm and 404.html at the root.',
|
|
141
|
+
token_scopes: 'Use Site tokens for one-site deploys, Workspace tokens for client workspaces, and Global tokens only for broad operator workflows.',
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
13
145
|
export function formatTokenSummary(token) {
|
|
14
146
|
if (!token) {
|
|
15
147
|
return 'No token metadata returned.';
|
|
@@ -17,7 +149,48 @@ export function formatTokenSummary(token) {
|
|
|
17
149
|
|
|
18
150
|
const scope = token.scope_label || token.scope_type || token.kind || 'Unknown scope';
|
|
19
151
|
const preset = token.preset_label || token.preset || 'Unknown access level';
|
|
20
|
-
const expiry = token.expires_at
|
|
152
|
+
const expiry = formatTokenExpiry(token.expires_at);
|
|
21
153
|
|
|
22
154
|
return `${scope} · ${preset} · ${expiry}`;
|
|
23
155
|
}
|
|
156
|
+
|
|
157
|
+
function printBanner() {
|
|
158
|
+
console.log(' ____ _ _ _ __ __');
|
|
159
|
+
console.log(' / ___|| |_ __ _| |_(_) ___ \\ \\/ /');
|
|
160
|
+
console.log(' \\___ \\| __/ _` | __| |/ __| \\ /');
|
|
161
|
+
console.log(' ___) | || (_| | |_| | (__ / \\');
|
|
162
|
+
console.log(' |____/ \\__\\__,_|\\__|_|\\___|/_/\\_\\');
|
|
163
|
+
console.log('');
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function printAlignedRows(rows) {
|
|
167
|
+
const width = Math.max(...rows.map(([label]) => label.length));
|
|
168
|
+
|
|
169
|
+
for (const [label, value] of rows) {
|
|
170
|
+
console.log(`${label.padEnd(width)} ${value}`);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function printCommandGroup(title, rows) {
|
|
175
|
+
console.log(title);
|
|
176
|
+
|
|
177
|
+
const width = Math.max(...rows.map(([command]) => command.length));
|
|
178
|
+
|
|
179
|
+
for (const [command, description] of rows) {
|
|
180
|
+
console.log(` ${command.padEnd(width)} ${description}`);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function formatTokenExpiry(expiresAt) {
|
|
185
|
+
if (!expiresAt) {
|
|
186
|
+
return 'Never expires';
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const date = new Date(expiresAt);
|
|
190
|
+
|
|
191
|
+
if (Number.isNaN(date.getTime())) {
|
|
192
|
+
return String(expiresAt);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return `expires ${date.toISOString().slice(0, 16).replace('T', ' ')} UTC`;
|
|
196
|
+
}
|