@vantienkhai/shippage-cli 0.1.0 → 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 +30 -4
  2. package/dist/index.js +117 -24
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -2,15 +2,41 @@
2
2
 
3
3
  Publish HTML, Markdown, and text files into Shippage from AI agents or scripts.
4
4
 
5
+ ## Install
6
+
7
+ ```bash
8
+ npm i -g @vantienkhai/shippage-cli
9
+ ```
10
+
11
+ ## Authenticate
12
+
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:
22
+
5
23
  ```bash
6
- export SHIPPAGE_API_URL=http://localhost:3000
7
- export SHIPPAGE_TOKEN=<supabase-access-token>
24
+ export SHIPPAGE_API_URL="https://shippage.vantienkhai-uet.workers.dev"
25
+ export SHIPPAGE_TOKEN="<your spg_ API key>"
26
+ ```
8
27
 
9
- shippage publish ./out/article.html --title "AI Search Glossary" --visibility unlisted --noindex
10
- shippage bulk "out/**/*.html" --visibility unlisted --noindex
28
+ ## Commands
29
+
30
+ ```bash
31
+ shippage login
32
+ shippage publish ./out/article.html --title "AI Search Glossary" --slug ai-search-glossary --visibility public
33
+ shippage bulk "out/**/*.html" --visibility unlisted
11
34
  shippage list
12
35
  shippage share abc123 --visibility private --private-token
13
36
  shippage domain add docs.example.com
37
+ shippage domain verify docs.example.com
14
38
  ```
15
39
 
16
40
  For anonymous publishing, omit `SHIPPAGE_TOKEN`. Anonymous pages are ownerless until a later claim flow is added.
41
+
42
+ Visibility controls search behavior automatically: `public` pages are indexable; `unlisted` and `private` pages are `noindex`.
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,32 +88,64 @@ 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);
59
100
  if (options.domain) form.set("domain", options.domain);
60
101
  if (options.visibility) form.set("visibility", options.visibility);
61
- if (options.noindex) form.set("noindex", "true");
62
102
  return requestJson("/api/upload", {
63
103
  method: "POST",
64
104
  body: form
65
105
  });
66
106
  }
67
- program.name("shippage").description("CLI for Shippage fast HTML publishing").version("0.1.0");
68
- program.command("login").description("Print shell export instructions for API auth").option("--token <token>", "Supabase access token").option("--api-url <url>", "Shippage API URL").action((options) => {
69
- if (options.apiUrl) console.log(`export SHIPPAGE_API_URL=${JSON.stringify(options.apiUrl)}`);
70
- if (options.token) console.log(`export SHIPPAGE_TOKEN=${JSON.stringify(options.token)}`);
71
- if (!options.apiUrl && !options.token) {
72
- 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;
73
145
  }
146
+ throw new Error("Browser login expired. Run shippage login again.");
74
147
  });
75
- 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").option("--noindex", "Keep robots noindex").action(async (file, options) => {
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) => {
76
149
  const result = await publishFile(file, options);
77
150
  console.log(chalk.green("Published"));
78
151
  console.log(`${result.title}: ${result.shareUrl}`);
@@ -80,7 +153,7 @@ program.command("publish").argument("<file>", "HTML, Markdown, or text file").op
80
153
  console.log(`Custom URL: ${result.customUrl}`);
81
154
  }
82
155
  });
83
- program.command("bulk").argument("<pattern>", "Glob pattern, for example out/**/*.html").option("--visibility <visibility>", "public, unlisted, or private", "unlisted").option("--noindex", "Keep robots noindex").action(async (pattern, options) => {
156
+ program.command("bulk").argument("<pattern>", "Glob pattern, for example out/**/*.html").option("--visibility <visibility>", "public, unlisted, or private", "unlisted").action(async (pattern, options) => {
84
157
  const files = await globby(pattern, { onlyFiles: true });
85
158
  if (!files.length) {
86
159
  console.log(chalk.yellow("No files matched."));
@@ -88,7 +161,7 @@ program.command("bulk").argument("<pattern>", "Glob pattern, for example out/**/
88
161
  }
89
162
  for (const file of files) {
90
163
  const result = await publishFile(file, __spreadProps(__spreadValues({}, options), {
91
- title: path.basename(file, path.extname(file))
164
+ title: path2.basename(file, path2.extname(file))
92
165
  }));
93
166
  console.log(`${chalk.green("Published")} ${file} -> ${result.shareUrl}`);
94
167
  }
@@ -99,13 +172,12 @@ program.command("list").description("List pages owned by the current token").act
99
172
  console.log(`${page.share_id} ${page.visibility} ${page.noindex ? "noindex" : "index"} ${page.canonical_path} ${page.title}`);
100
173
  }
101
174
  });
102
- program.command("share").argument("<shareId>", "Page share id").option("--visibility <visibility>", "public, unlisted, or private").option("--index", "Allow indexing").option("--noindex", "Keep robots noindex").option("--private-token", "Generate a private token").option("--domain <domain>", "Attach to a verified custom domain").option("--slug <slug>", "Custom path slug on the domain").action(async (shareId, options) => {
175
+ program.command("share").argument("<shareId>", "Page share id").option("--visibility <visibility>", "public, unlisted, or private").option("--private-token", "Generate a private token").option("--domain <domain>", "Attach to a verified custom domain").option("--slug <slug>", "Custom path slug on the domain").action(async (shareId, options) => {
103
176
  const payload = await requestJson(`/api/pages/${shareId}`, {
104
177
  method: "PATCH",
105
178
  headers: { "content-type": "application/json" },
106
179
  body: JSON.stringify({
107
180
  visibility: options.visibility,
108
- noindex: options.index ? false : options.noindex ? true : void 0,
109
181
  privateToken: options.privateToken,
110
182
  domain: options.domain,
111
183
  slug: options.slug
@@ -154,7 +226,28 @@ domain.command("remove").argument("<domain>", "Domain name").action(async (domai
154
226
  });
155
227
  console.log(chalk.green(`Removed ${domainName}`));
156
228
  });
157
- program.parseAsync().catch((error) => {
158
- console.error(chalk.red(error instanceof Error ? error.message : "Command failed"));
159
- process.exit(1);
160
- });
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.0",
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",