@vantienkhai/shippage-cli 0.1.1 → 0.2.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.
Files changed (3) hide show
  1. package/README.md +10 -5
  2. package/dist/index.js +114 -19
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -10,20 +10,25 @@ npm i -g @vantienkhai/shippage-cli
10
10
 
11
11
  ## Authenticate
12
12
 
13
- Sign in to Shippage and open the **Dashboard → CLI access** panel to copy your
14
- API URL and create a long-lived API key, then export them:
13
+ Run browser login once:
14
+
15
+ ```bash
16
+ shippage login
17
+ ```
18
+
19
+ The CLI opens Shippage in your browser, asks you to confirm a short code, then
20
+ saves a long-lived API key to `~/.config/shippage/config.json`. Environment
21
+ variables still override the saved config:
15
22
 
16
23
  ```bash
17
24
  export SHIPPAGE_API_URL="https://shippage.vantienkhai-uet.workers.dev"
18
25
  export SHIPPAGE_TOKEN="<your spg_ API key>"
19
26
  ```
20
27
 
21
- > API key secrets are shown only once. Store them securely and revoke unused
22
- > keys from the dashboard.
23
-
24
28
  ## Commands
25
29
 
26
30
  ```bash
31
+ shippage login
27
32
  shippage publish ./out/article.html --title "AI Search Glossary" --slug ai-search-glossary --visibility public
28
33
  shippage bulk "out/**/*.html" --visibility unlisted
29
34
  shippage list
package/dist/index.js CHANGED
@@ -21,17 +21,58 @@ var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b));
21
21
 
22
22
  // src/index.ts
23
23
  import "dotenv/config";
24
- import fs from "fs/promises";
25
- import path from "path";
24
+ import { spawn } from "child_process";
25
+ import fs2 from "fs/promises";
26
+ import path2 from "path";
26
27
  import { Command } from "commander";
27
28
  import chalk from "chalk";
28
29
  import { globby } from "globby";
30
+
31
+ // src/config.ts
32
+ import fs from "fs/promises";
33
+ import os from "os";
34
+ import path from "path";
35
+ function parseCliConfig(value) {
36
+ try {
37
+ const parsed = JSON.parse(value);
38
+ return __spreadValues(__spreadValues({}, typeof parsed.apiUrl === "string" ? { apiUrl: parsed.apiUrl } : {}), typeof parsed.token === "string" ? { token: parsed.token } : {});
39
+ } catch (e) {
40
+ return {};
41
+ }
42
+ }
43
+ function mergeCliConfig(current, update) {
44
+ return __spreadValues(__spreadValues({}, current), update);
45
+ }
46
+ function cliConfigPath() {
47
+ const root = process.env.XDG_CONFIG_HOME || path.join(os.homedir(), ".config");
48
+ return path.join(root, "shippage", "config.json");
49
+ }
50
+ async function readCliConfig() {
51
+ try {
52
+ return parseCliConfig(await fs.readFile(cliConfigPath(), "utf8"));
53
+ } catch (e) {
54
+ return {};
55
+ }
56
+ }
57
+ async function writeCliConfig(update) {
58
+ const configPath = cliConfigPath();
59
+ await fs.mkdir(path.dirname(configPath), { recursive: true, mode: 448 });
60
+ const config = mergeCliConfig(await readCliConfig(), update);
61
+ await fs.writeFile(configPath, `${JSON.stringify(config, null, 2)}
62
+ `, { mode: 384 });
63
+ await fs.chmod(configPath, 384);
64
+ return configPath;
65
+ }
66
+
67
+ // src/index.ts
29
68
  var program = new Command();
69
+ var DEFAULT_API_URL = "https://shippage.vantienkhai-uet.workers.dev";
70
+ var storedConfig = {};
30
71
  function apiUrl() {
31
- return (process.env.SHIPPAGE_API_URL || "http://localhost:3000").replace(/\/$/, "");
72
+ return (process.env.SHIPPAGE_API_URL || storedConfig.apiUrl || DEFAULT_API_URL).replace(/\/$/, "");
32
73
  }
33
74
  function apiToken() {
34
- return process.env.SHIPPAGE_TOKEN;
75
+ return process.env.SHIPPAGE_TOKEN || storedConfig.token;
35
76
  }
36
77
  async function requestJson(endpoint, init = {}) {
37
78
  const headers = new Headers(init.headers);
@@ -47,12 +88,12 @@ async function requestJson(endpoint, init = {}) {
47
88
  return payload;
48
89
  }
49
90
  async function publishFile(filePath, options) {
50
- const absolute = path.resolve(filePath);
51
- const buffer = await fs.readFile(absolute);
91
+ const absolute = path2.resolve(filePath);
92
+ const buffer = await fs2.readFile(absolute);
52
93
  const form = new FormData();
53
- const extension = path.extname(filePath).toLowerCase();
94
+ const extension = path2.extname(filePath).toLowerCase();
54
95
  const type = extension === ".md" || extension === ".mdx" ? "text/markdown" : extension === ".txt" ? "text/plain" : "text/html";
55
- form.set("file", new Blob([buffer], { type }), path.basename(filePath));
96
+ form.set("file", new Blob([buffer], { type }), path2.basename(filePath));
56
97
  if (options.title) form.set("title", options.title);
57
98
  if (options.description) form.set("description", options.description);
58
99
  if (options.slug) form.set("slug", options.slug);
@@ -63,13 +104,46 @@ async function publishFile(filePath, options) {
63
104
  body: form
64
105
  });
65
106
  }
66
- program.name("shippage").description("CLI for Shippage fast HTML publishing").version("0.1.0");
67
- program.command("login").description("Print shell export instructions for API auth").option("--token <token>", "Shippage API key").option("--api-url <url>", "Shippage API URL").action((options) => {
68
- if (options.apiUrl) console.log(`export SHIPPAGE_API_URL=${JSON.stringify(options.apiUrl)}`);
69
- if (options.token) console.log(`export SHIPPAGE_TOKEN=${JSON.stringify(options.token)}`);
70
- if (!options.apiUrl && !options.token) {
71
- console.log("Set SHIPPAGE_API_URL and SHIPPAGE_TOKEN in your shell or .env file.");
107
+ program.name("shippage").description("CLI for Shippage fast HTML publishing").version("0.2.0");
108
+ program.command("login").description("Open a browser and save a long-lived Shippage login").option("--token <token>", "Shippage API key").option("--api-url <url>", "Shippage API URL").option("--no-open", "Do not open the browser automatically").action(async (options) => {
109
+ const loginApiUrl = (options.apiUrl || apiUrl()).replace(/\/$/, "");
110
+ if (options.token) {
111
+ const configPath = await writeCliConfig({ apiUrl: loginApiUrl, token: options.token });
112
+ console.log(chalk.green(`Saved Shippage credentials to ${configPath}`));
113
+ return;
114
+ }
115
+ const response = await fetch(`${loginApiUrl}/api/cli-auth/device`, { method: "POST" });
116
+ const request = await response.json();
117
+ if (!response.ok || !request.deviceCode || !request.userCode || !request.verificationUrl || !request.expiresAt) {
118
+ throw new Error(request.error || "Unable to start browser login.");
119
+ }
120
+ console.log(`Open: ${request.verificationUrl}`);
121
+ console.log(`Confirm code: ${chalk.bold(request.userCode)}`);
122
+ if (options.open) {
123
+ try {
124
+ await openBrowser(request.verificationUrl);
125
+ console.log(chalk.dim("Opened your browser. Waiting for authorization..."));
126
+ } catch (e) {
127
+ console.log(chalk.yellow("Could not open a browser automatically. Open the URL above."));
128
+ }
129
+ } else {
130
+ console.log(chalk.dim("Waiting for authorization..."));
131
+ }
132
+ const interval = Math.max(1, request.interval || 2) * 1e3;
133
+ const expiresAt = new Date(request.expiresAt).getTime();
134
+ while (Date.now() < expiresAt) {
135
+ await sleep(interval);
136
+ const pollResponse = await fetch(`${loginApiUrl}/api/cli-auth/device?device_code=${encodeURIComponent(request.deviceCode)}`);
137
+ const poll = await pollResponse.json();
138
+ if (pollResponse.status === 202 || poll.status === "pending") continue;
139
+ if (!pollResponse.ok || !poll.token) {
140
+ throw new Error(poll.error || "Browser login failed.");
141
+ }
142
+ const configPath = await writeCliConfig({ apiUrl: poll.apiUrl || loginApiUrl, token: poll.token });
143
+ console.log(chalk.green(`Logged in. Credentials saved to ${configPath}`));
144
+ return;
72
145
  }
146
+ throw new Error("Browser login expired. Run shippage login again.");
73
147
  });
74
148
  program.command("publish").argument("<file>", "HTML, Markdown, or text file").option("--title <title>", "Page title").option("--description <description>", "Page meta description").option("--slug <slug>", "Custom path slug").option("--domain <domain>", "Verified custom domain").option("--visibility <visibility>", "public, unlisted, or private", "unlisted").action(async (file, options) => {
75
149
  const result = await publishFile(file, options);
@@ -87,7 +161,7 @@ program.command("bulk").argument("<pattern>", "Glob pattern, for example out/**/
87
161
  }
88
162
  for (const file of files) {
89
163
  const result = await publishFile(file, __spreadProps(__spreadValues({}, options), {
90
- title: path.basename(file, path.extname(file))
164
+ title: path2.basename(file, path2.extname(file))
91
165
  }));
92
166
  console.log(`${chalk.green("Published")} ${file} -> ${result.shareUrl}`);
93
167
  }
@@ -152,7 +226,28 @@ domain.command("remove").argument("<domain>", "Domain name").action(async (domai
152
226
  });
153
227
  console.log(chalk.green(`Removed ${domainName}`));
154
228
  });
155
- program.parseAsync().catch((error) => {
156
- console.error(chalk.red(error instanceof Error ? error.message : "Command failed"));
157
- process.exit(1);
158
- });
229
+ void bootstrap();
230
+ async function bootstrap() {
231
+ try {
232
+ storedConfig = await readCliConfig();
233
+ await program.parseAsync();
234
+ } catch (error) {
235
+ console.error(chalk.red(error instanceof Error ? error.message : "Command failed"));
236
+ process.exit(1);
237
+ }
238
+ }
239
+ function sleep(milliseconds) {
240
+ return new Promise((resolve) => setTimeout(resolve, milliseconds));
241
+ }
242
+ function openBrowser(url) {
243
+ const command = process.platform === "darwin" ? "open" : process.platform === "win32" ? "cmd" : "xdg-open";
244
+ const args = process.platform === "win32" ? ["/c", "start", "", url] : [url];
245
+ return new Promise((resolve, reject) => {
246
+ const child = spawn(command, args, { detached: true, stdio: "ignore" });
247
+ child.once("error", reject);
248
+ child.once("spawn", () => {
249
+ child.unref();
250
+ resolve();
251
+ });
252
+ });
253
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vantienkhai/shippage-cli",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "description": "Publish HTML, Markdown, and text files to Shippage from AI agents or scripts.",
5
5
  "license": "MIT",
6
6
  "type": "module",