staticx 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -13,14 +13,22 @@ 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
25
33
  staticx logs --site-id SITE_ID
26
34
  ```
@@ -41,4 +49,4 @@ Generate those tokens from:
41
49
 
42
50
  - `staticx login` stores the base URL and bearer token locally.
43
51
  - `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.
52
+ - `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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "staticx",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
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": "^12.1.0",
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 assertEntryHtmlExists(sourceDirectory);
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 assertEntryHtmlExists(directoryPath) {
39
- const candidates = ['index.html', 'index.htm'];
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
- throw new Error('Build directory must contain index.html or index.htm at its root.');
68
+ return false;
53
69
  }
54
70
 
55
71
  async function collectFiles(directoryPath) {
package/src/index.js CHANGED
@@ -6,7 +6,17 @@ 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 { formatTokenSummary, printJson, printKeyValue, printTable } from './output.js';
9
+ import {
10
+ cliGuideJson,
11
+ formatTokenSummary,
12
+ printCliGuide,
13
+ printJson,
14
+ printKeyValue,
15
+ printLoginSuccess,
16
+ printTable,
17
+ } from './output.js';
18
+
19
+ const CLI_VERSION = '0.1.2';
10
20
 
11
21
  export async function run(argv) {
12
22
  const program = new Command();
@@ -14,10 +24,32 @@ export async function run(argv) {
14
24
  program
15
25
  .name('staticx')
16
26
  .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();
27
+ .version(CLI_VERSION)
28
+ .showHelpAfterError()
29
+ .addHelpText('after', `
30
+
31
+ Common workflow:
32
+ staticx login --base-url "https://staticx.site/api/v1" --token "STATICX_API_TOKEN"
33
+ staticx guide
34
+ staticx deploy --site-id SITE_ID --dir dist
35
+
36
+ Deploy rule:
37
+ The build directory must contain index.html or index.htm and 404.html at its root.
38
+ `);
39
+
40
+ program
41
+ .command('guide')
42
+ .alias('commands')
43
+ .description('Explain the STATICX CLI commands and the recommended release workflow.')
44
+ .option('--json', 'Print JSON output')
45
+ .action((options) => {
46
+ if (options.json) {
47
+ printJson(cliGuideJson());
48
+ return;
49
+ }
50
+
51
+ printCliGuide();
52
+ });
21
53
 
22
54
  program
23
55
  .command('login')
@@ -42,10 +74,11 @@ export async function run(argv) {
42
74
  return;
43
75
  }
44
76
 
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));
77
+ printLoginSuccess({
78
+ baseUrl,
79
+ user: response?.data ?? null,
80
+ token: response?.data?.current_token || response?.data?.token,
81
+ });
49
82
  });
50
83
 
51
84
  program
@@ -60,6 +93,8 @@ export async function run(argv) {
60
93
  .command('whoami')
61
94
  .description('Verify the stored token and print the current user/token metadata.')
62
95
  .option('--json', 'Print JSON output')
96
+ .option('--base-url <url>', 'Override the stored STATICX API base URL')
97
+ .option('--token <token>', 'Override the stored STATICX bearer token')
63
98
  .action(async (options, command) => {
64
99
  const client = await clientFromCommand(command);
65
100
  const response = await client.get('/user');
@@ -81,6 +116,8 @@ export async function run(argv) {
81
116
  .command('workspaces')
82
117
  .description('List workspaces visible to the current token.')
83
118
  .option('--json', 'Print JSON output')
119
+ .option('--base-url <url>', 'Override the stored STATICX API base URL')
120
+ .option('--token <token>', 'Override the stored STATICX bearer token')
84
121
  .action(async (options, command) => {
85
122
  const client = await clientFromCommand(command);
86
123
  const response = await client.get('/workspaces');
@@ -104,6 +141,8 @@ export async function run(argv) {
104
141
  .description('List sites visible to the current token, optionally filtered by workspace.')
105
142
  .option('--workspace-id <id>', 'Filter by workspace ID')
106
143
  .option('--json', 'Print JSON output')
144
+ .option('--base-url <url>', 'Override the stored STATICX API base URL')
145
+ .option('--token <token>', 'Override the stored STATICX bearer token')
107
146
  .action(async (options, command) => {
108
147
  const client = await clientFromCommand(command);
109
148
  const response = await client.get('/projects', {
@@ -134,6 +173,8 @@ export async function run(argv) {
134
173
  .option('--archive <path>', 'ZIP archive path to seed the site')
135
174
  .option('--source-url <url>', 'Public URL to import into the site')
136
175
  .option('--json', 'Print JSON output')
176
+ .option('--base-url <url>', 'Override the stored STATICX API base URL')
177
+ .option('--token <token>', 'Override the stored STATICX bearer token')
137
178
  .action(async (options, command) => {
138
179
  if (options.archive && options.sourceUrl) {
139
180
  throw new Error('Use either --archive or --source-url, not both.');
@@ -167,10 +208,12 @@ export async function run(argv) {
167
208
 
168
209
  program
169
210
  .command('deploy')
170
- .description('Zip a local build directory, upload it, build-check it, and publish it to one site.')
211
+ .description('Zip a local build directory, upload it, and publish it to one site.')
171
212
  .requiredOption('--site-id <id>', 'Site ID')
172
213
  .requiredOption('--dir <path>', 'Built site directory, for example dist')
173
214
  .option('--json', 'Print JSON output')
215
+ .option('--base-url <url>', 'Override the stored STATICX API base URL')
216
+ .option('--token <token>', 'Override the stored STATICX bearer token')
174
217
  .action(async (options, command) => {
175
218
  const client = await clientFromCommand(command);
176
219
  const zipPath = await createZipFromDirectory(options.dir);
@@ -182,22 +225,12 @@ export async function run(argv) {
182
225
  uploadFormData.set('archive', await fileToBlob(zipPath, 'application/zip'), path.basename(zipPath));
183
226
 
184
227
  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
- });
228
+ const deployment = await client.post(`/projects/${options.siteId}/deployments`);
195
229
  const project = await client.get(`/projects/${options.siteId}`);
196
230
 
197
231
  const result = {
198
232
  project: project?.data ?? null,
199
233
  upload: upload?.data ?? null,
200
- build: build?.data ?? null,
201
234
  deployment: deployment?.data ?? null,
202
235
  };
203
236
 
@@ -208,7 +241,6 @@ export async function run(argv) {
208
241
 
209
242
  console.log('Deployment completed.');
210
243
  printKeyValue('Site', result.project?.name || options.siteId);
211
- printKeyValue('Build', result.build?.id || 'Unknown');
212
244
  printKeyValue('Deployment', result.deployment?.id || 'Unknown');
213
245
  printKeyValue('Public URL', result.project?.public_url || 'Pending');
214
246
  } finally {
@@ -222,6 +254,8 @@ export async function run(argv) {
222
254
  .requiredOption('--site-id <id>', 'Site ID')
223
255
  .option('--limit <number>', 'Maximum number of log rows', '25')
224
256
  .option('--json', 'Print JSON output')
257
+ .option('--base-url <url>', 'Override the stored STATICX API base URL')
258
+ .option('--token <token>', 'Override the stored STATICX bearer token')
225
259
  .action(async (options, command) => {
226
260
  const client = await clientFromCommand(command);
227
261
  const response = await client.get(`/projects/${options.siteId}/logs`, {
@@ -246,7 +280,7 @@ export async function run(argv) {
246
280
  }
247
281
 
248
282
  async function clientFromCommand(command) {
249
- const options = command.optsWithGlobals ? command.optsWithGlobals() : command.opts();
283
+ const options = command.opts();
250
284
  const auth = await resolveRuntimeAuth(options);
251
285
 
252
286
  return new StaticxApiClient(auth);
package/src/output.js CHANGED
@@ -10,6 +10,88 @@ export function printTable(rows) {
10
10
  console.table(rows);
11
11
  }
12
12
 
13
+ export function printLoginSuccess({ baseUrl, user, token }) {
14
+ printBanner();
15
+ console.log('Successfully authenticated.');
16
+ console.log('');
17
+ printAlignedRows([
18
+ ['Base URL', baseUrl],
19
+ ['User', user?.email || user?.name || 'Unknown'],
20
+ ['Token', formatTokenSummary(token)],
21
+ ]);
22
+ console.log('');
23
+ printCommandGroup('Next commands', [
24
+ ['staticx whoami', 'Verify the stored token.'],
25
+ ['staticx guide', 'Show the complete CLI workflow.'],
26
+ ['staticx sites', 'List the sites visible to this token.'],
27
+ ['staticx deploy --site-id SITE_ID --dir dist', 'Upload and publish a build folder.'],
28
+ ]);
29
+ console.log('');
30
+ console.log('Build rule: deploy folders must include index.html or index.htm and 404.html at the root.');
31
+ }
32
+
33
+ export function printCliGuide() {
34
+ printBanner();
35
+ console.log('Command guide');
36
+ console.log('');
37
+ printCommandGroup('Authenticate', [
38
+ ['staticx login --base-url "https://staticx.site/api/v1" --token "STATICX_API_TOKEN"', 'Store and verify a token.'],
39
+ ['staticx whoami', 'Print the current user and token scope.'],
40
+ ['staticx logout', 'Remove local CLI credentials.'],
41
+ ]);
42
+ console.log('');
43
+ printCommandGroup('Browse account state', [
44
+ ['staticx workspaces', 'List visible workspaces.'],
45
+ ['staticx sites', 'List visible sites.'],
46
+ ['staticx sites --workspace-id WORKSPACE_ID', 'List sites inside one workspace.'],
47
+ ]);
48
+ console.log('');
49
+ printCommandGroup('Create and publish', [
50
+ ['staticx create --workspace-id WORKSPACE_ID --name "Marketing Site"', 'Create a site.'],
51
+ ['staticx create --name "Imported Site" --archive site.zip', 'Create a site from a ZIP archive.'],
52
+ ['staticx create --name "Imported Site" --source-url "https://example.com"', 'Import from a public URL.'],
53
+ ['staticx deploy --site-id SITE_ID --dir dist', 'Zip, upload, and publish a build folder.'],
54
+ ]);
55
+ console.log('');
56
+ printCommandGroup('Inspect', [
57
+ ['staticx logs --site-id SITE_ID', 'Read recent site activity.'],
58
+ ['staticx logs --site-id SITE_ID --limit 50', 'Read more rows.'],
59
+ ]);
60
+ console.log('');
61
+ console.log('Token scopes: use Site tokens for one-site deploys, Workspace tokens for client workspaces, and Global tokens only for broad operator workflows.');
62
+ console.log('Build rule: deploy folders must include index.html or index.htm and 404.html at the root.');
63
+ }
64
+
65
+ export function cliGuideJson() {
66
+ return {
67
+ install: 'npm install -g staticx',
68
+ commands: {
69
+ authenticate: [
70
+ 'staticx login --base-url "https://staticx.site/api/v1" --token "STATICX_API_TOKEN"',
71
+ 'staticx whoami',
72
+ 'staticx logout',
73
+ ],
74
+ browse: [
75
+ 'staticx workspaces',
76
+ 'staticx sites',
77
+ 'staticx sites --workspace-id WORKSPACE_ID',
78
+ ],
79
+ publish: [
80
+ 'staticx create --workspace-id WORKSPACE_ID --name "Marketing Site"',
81
+ 'staticx create --name "Imported Site" --archive site.zip',
82
+ 'staticx create --name "Imported Site" --source-url "https://example.com"',
83
+ 'staticx deploy --site-id SITE_ID --dir dist',
84
+ ],
85
+ inspect: [
86
+ 'staticx logs --site-id SITE_ID',
87
+ 'staticx logs --site-id SITE_ID --limit 50',
88
+ ],
89
+ },
90
+ build_rule: 'Deploy folders must include index.html or index.htm and 404.html at the root.',
91
+ token_scopes: 'Use Site tokens for one-site deploys, Workspace tokens for client workspaces, and Global tokens only for broad operator workflows.',
92
+ };
93
+ }
94
+
13
95
  export function formatTokenSummary(token) {
14
96
  if (!token) {
15
97
  return 'No token metadata returned.';
@@ -17,7 +99,48 @@ export function formatTokenSummary(token) {
17
99
 
18
100
  const scope = token.scope_label || token.scope_type || token.kind || 'Unknown scope';
19
101
  const preset = token.preset_label || token.preset || 'Unknown access level';
20
- const expiry = token.expires_at || 'Never expires';
102
+ const expiry = formatTokenExpiry(token.expires_at);
21
103
 
22
104
  return `${scope} · ${preset} · ${expiry}`;
23
105
  }
106
+
107
+ function printBanner() {
108
+ console.log(' ____ _ _ _ __ __');
109
+ console.log(' / ___|| |_ __ _| |_(_) ___ \\ \\/ /');
110
+ console.log(' \\___ \\| __/ _` | __| |/ __| \\ /');
111
+ console.log(' ___) | || (_| | |_| | (__ / \\');
112
+ console.log(' |____/ \\__\\__,_|\\__|_|\\___|/_/\\_\\');
113
+ console.log('');
114
+ }
115
+
116
+ function printAlignedRows(rows) {
117
+ const width = Math.max(...rows.map(([label]) => label.length));
118
+
119
+ for (const [label, value] of rows) {
120
+ console.log(`${label.padEnd(width)} ${value}`);
121
+ }
122
+ }
123
+
124
+ function printCommandGroup(title, rows) {
125
+ console.log(title);
126
+
127
+ const width = Math.max(...rows.map(([command]) => command.length));
128
+
129
+ for (const [command, description] of rows) {
130
+ console.log(` ${command.padEnd(width)} ${description}`);
131
+ }
132
+ }
133
+
134
+ function formatTokenExpiry(expiresAt) {
135
+ if (!expiresAt) {
136
+ return 'Never expires';
137
+ }
138
+
139
+ const date = new Date(expiresAt);
140
+
141
+ if (Number.isNaN(date.getTime())) {
142
+ return String(expiresAt);
143
+ }
144
+
145
+ return `expires ${date.toISOString().slice(0, 16).replace('T', ' ')} UTC`;
146
+ }