@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.
- package/README.md +30 -4
- package/dist/index.js +117 -24
- 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=
|
|
7
|
-
export SHIPPAGE_TOKEN
|
|
24
|
+
export SHIPPAGE_API_URL="https://shippage.vantienkhai-uet.workers.dev"
|
|
25
|
+
export SHIPPAGE_TOKEN="<your spg_ API key>"
|
|
26
|
+
```
|
|
8
27
|
|
|
9
|
-
|
|
10
|
-
|
|
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
|
|
25
|
-
import
|
|
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 ||
|
|
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 =
|
|
51
|
-
const buffer = await
|
|
91
|
+
const absolute = path2.resolve(filePath);
|
|
92
|
+
const buffer = await fs2.readFile(absolute);
|
|
52
93
|
const form = new FormData();
|
|
53
|
-
const extension =
|
|
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 }),
|
|
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.
|
|
68
|
-
program.command("login").description("
|
|
69
|
-
|
|
70
|
-
if (options.token)
|
|
71
|
-
|
|
72
|
-
console.log(
|
|
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").
|
|
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").
|
|
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:
|
|
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("--
|
|
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
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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
|
+
}
|