pastes 0.1.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/LICENSE +21 -0
- package/README.md +129 -0
- package/dist/api.d.ts +61 -0
- package/dist/api.js +91 -0
- package/dist/args.d.ts +25 -0
- package/dist/args.js +178 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +217 -0
- package/dist/config.d.ts +33 -0
- package/dist/config.js +77 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +5 -0
- package/dist/login.d.ts +17 -0
- package/dist/login.js +59 -0
- package/dist/syntax.d.ts +10 -0
- package/dist/syntax.js +97 -0
- package/dist/terminal.d.ts +15 -0
- package/dist/terminal.js +94 -0
- package/package.json +50 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Pastes.io
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# pastes
|
|
2
|
+
|
|
3
|
+
Share text, code, and logs on [pastes.io](https://pastes.io) from your terminal. Pipe something in, get a link back.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npx pastes --help
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm install -g pastes
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Or run it without installing: `npx pastes`.
|
|
16
|
+
|
|
17
|
+
Requires Node.js 20 or newer. No runtime dependencies.
|
|
18
|
+
|
|
19
|
+
## Sign in
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
pastes login
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
This prints a short code and a link. Open the link on any device — your laptop, your phone — approve the code, and the CLI stores the API key it is issued. Because the code is displayed rather than typed into a browser on the same machine, this works fine over SSH on a box with no GUI.
|
|
26
|
+
|
|
27
|
+
For CI, containers, and production, skip the login and set an environment variable instead:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
export PASTES_API_KEY=your-key
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Create a key at [pastes.io/profile](https://pastes.io/profile). The CLI reads the key from `--api-key`, then `PASTES_API_KEY`, then its config file — so the environment variable always wins over a stale stored key.
|
|
34
|
+
|
|
35
|
+
## Use
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
# Pipe anything
|
|
39
|
+
./deploy.sh 2>&1 | pastes
|
|
40
|
+
kubectl logs pod-1 | pastes -t "pod-1 crash" -s logs
|
|
41
|
+
|
|
42
|
+
# Or point it at a file — title and syntax are inferred from the name
|
|
43
|
+
pastes server.py
|
|
44
|
+
pastes nginx.log --expire 1D
|
|
45
|
+
|
|
46
|
+
# Read, list, delete
|
|
47
|
+
pastes get frosty-mole-8821 > restored.txt
|
|
48
|
+
pastes ls "crash"
|
|
49
|
+
pastes rm frosty-mole-8821
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
The paste URL is the only thing written to stdout, so it composes:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
pastes < error.log | xargs -I{} curl -X POST "$SLACK_WEBHOOK" -d "{\"text\":\"{}\"}"
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Everything else — progress, warnings, the clipboard notice — goes to stderr.
|
|
59
|
+
|
|
60
|
+
## Options
|
|
61
|
+
|
|
62
|
+
| Option | Description |
|
|
63
|
+
| --- | --- |
|
|
64
|
+
| `-t, --title <title>` | Paste title. Defaults to the filename, or the first line of input. |
|
|
65
|
+
| `-s, --syntax <slug>` | Syntax highlighting. Defaults to the one inferred from the filename, else `txt`. |
|
|
66
|
+
| `-e, --expire <code>` | `10M` `1H` `1D` `1W` `2W` `1M` `6M` `1Y` `N` `SD`. Defaults to `1M`. Free accounts must use `1M`. |
|
|
67
|
+
| `-p, --password <pw>` | Protect the paste with a password. |
|
|
68
|
+
| `--no-copy` | Do not copy the URL to the clipboard. |
|
|
69
|
+
| `--api-key <key>` | Use this key for one command. |
|
|
70
|
+
| `--api-url <url>` | Point at a different host (useful for local development). |
|
|
71
|
+
| `--json` | Print the raw JSON response instead of the URL. |
|
|
72
|
+
| `-q, --quiet` | Suppress the non-essential stderr output. |
|
|
73
|
+
|
|
74
|
+
## Environment variables
|
|
75
|
+
|
|
76
|
+
| Variable | Effect |
|
|
77
|
+
| --- | --- |
|
|
78
|
+
| `PASTES_API_KEY` | API key to use. Overrides the stored config. |
|
|
79
|
+
| `PASTES_API_URL` | API base URL. Defaults to `https://pastes.io`. |
|
|
80
|
+
| `PASTES_CONFIG_HOME` | Directory for `config.json`. Defaults to `%APPDATA%\pastes` on Windows, `$XDG_CONFIG_HOME/pastes` or `~/.config/pastes` elsewhere. |
|
|
81
|
+
| `PASTES_NO_BROWSER` | Never launch a browser during `pastes login`; just print the link. |
|
|
82
|
+
|
|
83
|
+
## Exit codes
|
|
84
|
+
|
|
85
|
+
| Code | Meaning |
|
|
86
|
+
| --- | --- |
|
|
87
|
+
| `0` | Success. |
|
|
88
|
+
| `1` | The request failed — network error, rejected key, rate limit. |
|
|
89
|
+
| `2` | Usage error — bad flag, missing input, no API key configured. |
|
|
90
|
+
|
|
91
|
+
## Rate limits
|
|
92
|
+
|
|
93
|
+
Free accounts have per-window and daily paste-creation limits and a daily cap on API reads and searches. Pro raises the creation limits and removes the read/search cap. See [pastes.io/pricing](https://pastes.io/pricing).
|
|
94
|
+
|
|
95
|
+
## Programmatic use
|
|
96
|
+
|
|
97
|
+
```js
|
|
98
|
+
import { PastesClient } from 'pastes';
|
|
99
|
+
|
|
100
|
+
const client = new PastesClient({
|
|
101
|
+
apiUrl: 'https://pastes.io',
|
|
102
|
+
apiKey: process.env.PASTES_API_KEY
|
|
103
|
+
});
|
|
104
|
+
const { success } = await client.createPaste({
|
|
105
|
+
title: 'Build log',
|
|
106
|
+
content: log,
|
|
107
|
+
syntax: 'logs'
|
|
108
|
+
});
|
|
109
|
+
console.log(success.paste_url);
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## Development
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
npm install
|
|
116
|
+
npm test
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
The test suite has no external dependencies — the end-to-end tests run the compiled binary against a local mock of the API, so nothing touches pastes.io and no key is needed.
|
|
120
|
+
|
|
121
|
+
To point the CLI at a local server while developing:
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
npm run dev -- --api-url http://localhost:3012 --help
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
## License
|
|
128
|
+
|
|
129
|
+
MIT
|
package/dist/api.d.ts
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
export declare class ApiError extends Error {
|
|
2
|
+
status: number;
|
|
3
|
+
constructor(status: number, message: string);
|
|
4
|
+
}
|
|
5
|
+
export interface CreatePasteInput {
|
|
6
|
+
title: string;
|
|
7
|
+
content: string;
|
|
8
|
+
syntax: string;
|
|
9
|
+
expire?: string;
|
|
10
|
+
password?: string;
|
|
11
|
+
}
|
|
12
|
+
export interface PastesClientOptions {
|
|
13
|
+
apiUrl: string;
|
|
14
|
+
apiKey?: string;
|
|
15
|
+
}
|
|
16
|
+
export declare class PastesClient {
|
|
17
|
+
private apiUrl;
|
|
18
|
+
private apiKey?;
|
|
19
|
+
constructor({ apiUrl, apiKey }: PastesClientOptions);
|
|
20
|
+
private request;
|
|
21
|
+
createPaste(input: CreatePasteInput): Promise<{
|
|
22
|
+
success: {
|
|
23
|
+
slug: string;
|
|
24
|
+
paste_url: string;
|
|
25
|
+
messages?: string;
|
|
26
|
+
};
|
|
27
|
+
}>;
|
|
28
|
+
getPaste(slug: string, password?: string): Promise<{
|
|
29
|
+
success: Record<string, unknown>;
|
|
30
|
+
}>;
|
|
31
|
+
listPastes({ query, page }?: {
|
|
32
|
+
query?: string;
|
|
33
|
+
page?: number;
|
|
34
|
+
}): Promise<{
|
|
35
|
+
data: unknown;
|
|
36
|
+
page: number;
|
|
37
|
+
}>;
|
|
38
|
+
deletePaste(slug: string): Promise<{
|
|
39
|
+
success: string;
|
|
40
|
+
}>;
|
|
41
|
+
whoami(): Promise<{
|
|
42
|
+
success: {
|
|
43
|
+
email: string;
|
|
44
|
+
plan: string;
|
|
45
|
+
};
|
|
46
|
+
}>;
|
|
47
|
+
startDeviceAuth(): Promise<{
|
|
48
|
+
deviceCode: string;
|
|
49
|
+
userCode: string;
|
|
50
|
+
verificationUri: string;
|
|
51
|
+
verificationUriComplete: string;
|
|
52
|
+
expiresInSec: number;
|
|
53
|
+
intervalSec: number;
|
|
54
|
+
}>;
|
|
55
|
+
pollDeviceAuth(deviceCode: string): Promise<{
|
|
56
|
+
status: "pending" | "approved" | "expired" | "slow_down";
|
|
57
|
+
apiKey?: string;
|
|
58
|
+
email?: string;
|
|
59
|
+
intervalSec?: number;
|
|
60
|
+
}>;
|
|
61
|
+
}
|
package/dist/api.js
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
export class ApiError extends Error {
|
|
2
|
+
status;
|
|
3
|
+
constructor(status, message) {
|
|
4
|
+
super(message);
|
|
5
|
+
this.status = status;
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
/** Pulls the human-readable message out of SvelteKit's error shape. */
|
|
9
|
+
async function readError(response) {
|
|
10
|
+
const text = await response.text().catch(() => '');
|
|
11
|
+
if (text) {
|
|
12
|
+
try {
|
|
13
|
+
const parsed = JSON.parse(text);
|
|
14
|
+
const message = parsed?.message || parsed?.error?.message || parsed?.error;
|
|
15
|
+
if (typeof message === 'string' && message) {
|
|
16
|
+
return message;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
// Not JSON — fall through to the raw body, which is usually an HTML page.
|
|
21
|
+
}
|
|
22
|
+
if (!text.trimStart().startsWith('<') && text.length < 300) {
|
|
23
|
+
return text.trim();
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return `Request failed with status ${response.status}`;
|
|
27
|
+
}
|
|
28
|
+
export class PastesClient {
|
|
29
|
+
apiUrl;
|
|
30
|
+
apiKey;
|
|
31
|
+
constructor({ apiUrl, apiKey }) {
|
|
32
|
+
this.apiUrl = apiUrl;
|
|
33
|
+
this.apiKey = apiKey;
|
|
34
|
+
}
|
|
35
|
+
async request(path, { method = 'GET', body } = {}) {
|
|
36
|
+
const headers = { accept: 'application/json' };
|
|
37
|
+
if (this.apiKey) {
|
|
38
|
+
headers.authorization = `Bearer ${this.apiKey}`;
|
|
39
|
+
}
|
|
40
|
+
if (body !== undefined) {
|
|
41
|
+
headers['content-type'] = 'application/json';
|
|
42
|
+
}
|
|
43
|
+
let response;
|
|
44
|
+
try {
|
|
45
|
+
response = await fetch(`${this.apiUrl}${path}`, {
|
|
46
|
+
method,
|
|
47
|
+
headers,
|
|
48
|
+
body: body === undefined ? undefined : JSON.stringify(body)
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
catch (cause) {
|
|
52
|
+
throw new ApiError(0, `Could not reach ${this.apiUrl} (${cause.message})`);
|
|
53
|
+
}
|
|
54
|
+
if (!response.ok) {
|
|
55
|
+
throw new ApiError(response.status, await readError(response));
|
|
56
|
+
}
|
|
57
|
+
return (await response.json());
|
|
58
|
+
}
|
|
59
|
+
createPaste(input) {
|
|
60
|
+
return this.request('/api/paste', { method: 'POST', body: input });
|
|
61
|
+
}
|
|
62
|
+
getPaste(slug, password) {
|
|
63
|
+
if (password) {
|
|
64
|
+
return this.request(`/api/pastes/${encodeURIComponent(slug)}`, { method: 'POST', body: { password } });
|
|
65
|
+
}
|
|
66
|
+
return this.request(`/api/pastes/${encodeURIComponent(slug)}`);
|
|
67
|
+
}
|
|
68
|
+
listPastes({ query = '', page = 1 } = {}) {
|
|
69
|
+
const params = new URLSearchParams();
|
|
70
|
+
if (query)
|
|
71
|
+
params.set('q', query);
|
|
72
|
+
if (page > 1)
|
|
73
|
+
params.set('page', String(page));
|
|
74
|
+
const suffix = params.toString() ? `?${params}` : '';
|
|
75
|
+
return this.request(`/api/pastes${suffix}`);
|
|
76
|
+
}
|
|
77
|
+
deletePaste(slug) {
|
|
78
|
+
return this.request(`/api/pastes/${encodeURIComponent(slug)}`, {
|
|
79
|
+
method: 'DELETE'
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
whoami() {
|
|
83
|
+
return this.request('/api/cli/whoami');
|
|
84
|
+
}
|
|
85
|
+
startDeviceAuth() {
|
|
86
|
+
return this.request('/api/cli/auth/start', { method: 'POST', body: {} });
|
|
87
|
+
}
|
|
88
|
+
pollDeviceAuth(deviceCode) {
|
|
89
|
+
return this.request('/api/cli/auth/token', { method: 'POST', body: { deviceCode } });
|
|
90
|
+
}
|
|
91
|
+
}
|
package/dist/args.d.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export declare const COMMANDS: readonly ["create", "login", "logout", "whoami", "get", "ls", "rm"];
|
|
2
|
+
export type Command = (typeof COMMANDS)[number];
|
|
3
|
+
export interface ParsedArgs {
|
|
4
|
+
command: Command;
|
|
5
|
+
/** Positional argument: a file path for `create`, a slug for `get`/`rm`, a query for `ls`. */
|
|
6
|
+
target?: string;
|
|
7
|
+
title?: string;
|
|
8
|
+
syntax?: string;
|
|
9
|
+
expire?: string;
|
|
10
|
+
password?: string;
|
|
11
|
+
apiKey?: string;
|
|
12
|
+
apiUrl?: string;
|
|
13
|
+
page?: number;
|
|
14
|
+
copy: boolean;
|
|
15
|
+
quiet: boolean;
|
|
16
|
+
json: boolean;
|
|
17
|
+
help: boolean;
|
|
18
|
+
version: boolean;
|
|
19
|
+
/** `login --with-key` reads a key from stdin instead of running the device flow. */
|
|
20
|
+
withKey: boolean;
|
|
21
|
+
}
|
|
22
|
+
export declare class ArgError extends Error {
|
|
23
|
+
}
|
|
24
|
+
export declare function parseArgs(argv: string[]): ParsedArgs;
|
|
25
|
+
export declare const HELP_TEXT = "pastes \u2014 share text, code, and logs on pastes.io\n\nUsage\n pastes [file] Create a paste from a file, or from stdin\n pastes login Connect this machine to your pastes.io account\n pastes logout Forget the stored API key\n pastes whoami Show the account the stored key belongs to\n pastes get <slug> Print a paste's content to stdout\n pastes ls [query] List your pastes\n pastes rm <slug> Delete a paste you own\n\nCreate options\n -t, --title <title> Paste title (default: filename, or first line)\n -s, --syntax <slug> Syntax highlighting (default: inferred, else txt)\n -e, --expire <code> 10M 1H 1D 1W 2W 1M 6M 1Y N SD (default: 1M)\n -p, --password <password> Protect the paste with a password\n --no-copy Do not copy the URL to the clipboard\n\nGeneral options\n --api-key <key> Use this key instead of the stored one\n --api-url <url> Override the API base URL\n --json Print the raw JSON response\n -q, --quiet Print only essential output\n -h, --help Show this help\n -v, --version Show the version\n\nExamples\n ./deploy.sh 2>&1 | pastes\n pastes server.py --expire 1D\n kubectl logs pod-1 | pastes -t \"pod-1 crash\" -s logs\n pastes get frosty-mole-8821 > restored.txt\n\nThe API key is read from --api-key, then PASTES_API_KEY, then the config file.\nSet PASTES_API_KEY in CI and production; run `pastes login` on your own machine.\n";
|
package/dist/args.js
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
export const COMMANDS = ['create', 'login', 'logout', 'whoami', 'get', 'ls', 'rm'];
|
|
2
|
+
export class ArgError extends Error {
|
|
3
|
+
}
|
|
4
|
+
const FLAG_ALIASES = {
|
|
5
|
+
'-t': '--title',
|
|
6
|
+
'-s': '--syntax',
|
|
7
|
+
'-e': '--expire',
|
|
8
|
+
'-p': '--password',
|
|
9
|
+
'-q': '--quiet',
|
|
10
|
+
'-h': '--help',
|
|
11
|
+
'-v': '--version'
|
|
12
|
+
};
|
|
13
|
+
const VALUE_FLAGS = new Set([
|
|
14
|
+
'--title',
|
|
15
|
+
'--syntax',
|
|
16
|
+
'--expire',
|
|
17
|
+
'--password',
|
|
18
|
+
'--api-key',
|
|
19
|
+
'--api-url',
|
|
20
|
+
'--page'
|
|
21
|
+
]);
|
|
22
|
+
const BOOLEAN_FLAGS = new Set([
|
|
23
|
+
'--quiet',
|
|
24
|
+
'--json',
|
|
25
|
+
'--help',
|
|
26
|
+
'--version',
|
|
27
|
+
'--copy',
|
|
28
|
+
'--no-copy',
|
|
29
|
+
'--with-key'
|
|
30
|
+
]);
|
|
31
|
+
export function parseArgs(argv) {
|
|
32
|
+
const parsed = {
|
|
33
|
+
command: 'create',
|
|
34
|
+
copy: true,
|
|
35
|
+
quiet: false,
|
|
36
|
+
json: false,
|
|
37
|
+
help: false,
|
|
38
|
+
version: false,
|
|
39
|
+
withKey: false
|
|
40
|
+
};
|
|
41
|
+
const positionals = [];
|
|
42
|
+
let commandRead = false;
|
|
43
|
+
let onlyPositionals = false;
|
|
44
|
+
for (let i = 0; i < argv.length; i++) {
|
|
45
|
+
const raw = argv[i];
|
|
46
|
+
if (onlyPositionals) {
|
|
47
|
+
positionals.push(raw);
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
if (raw === '--') {
|
|
51
|
+
onlyPositionals = true;
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
if (raw.startsWith('-') && raw !== '-') {
|
|
55
|
+
// Support --flag=value as well as --flag value.
|
|
56
|
+
const eq = raw.indexOf('=');
|
|
57
|
+
const name = FLAG_ALIASES[eq === -1 ? raw : raw.slice(0, eq)] || (eq === -1 ? raw : raw.slice(0, eq));
|
|
58
|
+
const inlineValue = eq === -1 ? undefined : raw.slice(eq + 1);
|
|
59
|
+
if (VALUE_FLAGS.has(name)) {
|
|
60
|
+
const value = inlineValue ?? argv[++i];
|
|
61
|
+
if (value === undefined) {
|
|
62
|
+
throw new ArgError(`${name} needs a value`);
|
|
63
|
+
}
|
|
64
|
+
applyValueFlag(parsed, name, value);
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
if (BOOLEAN_FLAGS.has(name)) {
|
|
68
|
+
if (inlineValue !== undefined) {
|
|
69
|
+
throw new ArgError(`${name} does not take a value`);
|
|
70
|
+
}
|
|
71
|
+
applyBooleanFlag(parsed, name);
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
throw new ArgError(`Unknown option ${name}`);
|
|
75
|
+
}
|
|
76
|
+
if (!commandRead && COMMANDS.includes(raw)) {
|
|
77
|
+
parsed.command = raw;
|
|
78
|
+
commandRead = true;
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
positionals.push(raw);
|
|
82
|
+
}
|
|
83
|
+
if (positionals.length > 1) {
|
|
84
|
+
throw new ArgError(`Unexpected argument ${positionals[1]}`);
|
|
85
|
+
}
|
|
86
|
+
parsed.target = positionals[0];
|
|
87
|
+
return parsed;
|
|
88
|
+
}
|
|
89
|
+
function applyValueFlag(parsed, name, value) {
|
|
90
|
+
switch (name) {
|
|
91
|
+
case '--title':
|
|
92
|
+
parsed.title = value;
|
|
93
|
+
return;
|
|
94
|
+
case '--syntax':
|
|
95
|
+
parsed.syntax = value;
|
|
96
|
+
return;
|
|
97
|
+
case '--expire':
|
|
98
|
+
parsed.expire = value.toUpperCase();
|
|
99
|
+
return;
|
|
100
|
+
case '--password':
|
|
101
|
+
parsed.password = value;
|
|
102
|
+
return;
|
|
103
|
+
case '--api-key':
|
|
104
|
+
parsed.apiKey = value;
|
|
105
|
+
return;
|
|
106
|
+
case '--api-url':
|
|
107
|
+
parsed.apiUrl = value.replace(/\/+$/, '');
|
|
108
|
+
return;
|
|
109
|
+
case '--page': {
|
|
110
|
+
const page = Number(value);
|
|
111
|
+
if (!Number.isInteger(page) || page < 1) {
|
|
112
|
+
throw new ArgError('--page must be a positive integer');
|
|
113
|
+
}
|
|
114
|
+
parsed.page = page;
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
function applyBooleanFlag(parsed, name) {
|
|
120
|
+
switch (name) {
|
|
121
|
+
case '--quiet':
|
|
122
|
+
parsed.quiet = true;
|
|
123
|
+
return;
|
|
124
|
+
case '--json':
|
|
125
|
+
parsed.json = true;
|
|
126
|
+
return;
|
|
127
|
+
case '--help':
|
|
128
|
+
parsed.help = true;
|
|
129
|
+
return;
|
|
130
|
+
case '--version':
|
|
131
|
+
parsed.version = true;
|
|
132
|
+
return;
|
|
133
|
+
case '--copy':
|
|
134
|
+
parsed.copy = true;
|
|
135
|
+
return;
|
|
136
|
+
case '--no-copy':
|
|
137
|
+
parsed.copy = false;
|
|
138
|
+
return;
|
|
139
|
+
case '--with-key':
|
|
140
|
+
parsed.withKey = true;
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
export const HELP_TEXT = `pastes — share text, code, and logs on pastes.io
|
|
145
|
+
|
|
146
|
+
Usage
|
|
147
|
+
pastes [file] Create a paste from a file, or from stdin
|
|
148
|
+
pastes login Connect this machine to your pastes.io account
|
|
149
|
+
pastes logout Forget the stored API key
|
|
150
|
+
pastes whoami Show the account the stored key belongs to
|
|
151
|
+
pastes get <slug> Print a paste's content to stdout
|
|
152
|
+
pastes ls [query] List your pastes
|
|
153
|
+
pastes rm <slug> Delete a paste you own
|
|
154
|
+
|
|
155
|
+
Create options
|
|
156
|
+
-t, --title <title> Paste title (default: filename, or first line)
|
|
157
|
+
-s, --syntax <slug> Syntax highlighting (default: inferred, else txt)
|
|
158
|
+
-e, --expire <code> 10M 1H 1D 1W 2W 1M 6M 1Y N SD (default: 1M)
|
|
159
|
+
-p, --password <password> Protect the paste with a password
|
|
160
|
+
--no-copy Do not copy the URL to the clipboard
|
|
161
|
+
|
|
162
|
+
General options
|
|
163
|
+
--api-key <key> Use this key instead of the stored one
|
|
164
|
+
--api-url <url> Override the API base URL
|
|
165
|
+
--json Print the raw JSON response
|
|
166
|
+
-q, --quiet Print only essential output
|
|
167
|
+
-h, --help Show this help
|
|
168
|
+
-v, --version Show the version
|
|
169
|
+
|
|
170
|
+
Examples
|
|
171
|
+
./deploy.sh 2>&1 | pastes
|
|
172
|
+
pastes server.py --expire 1D
|
|
173
|
+
kubectl logs pod-1 | pastes -t "pod-1 crash" -s logs
|
|
174
|
+
pastes get frosty-mole-8821 > restored.txt
|
|
175
|
+
|
|
176
|
+
The API key is read from --api-key, then PASTES_API_KEY, then the config file.
|
|
177
|
+
Set PASTES_API_KEY in CI and production; run \`pastes login\` on your own machine.
|
|
178
|
+
`;
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
import { dirname, join } from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { ApiError, PastesClient } from './api.js';
|
|
6
|
+
import { ArgError, HELP_TEXT, parseArgs } from './args.js';
|
|
7
|
+
import { clearConfig, configPath, readConfig, resolveApiKey, resolveApiUrl, writeConfig } from './config.js';
|
|
8
|
+
import { LoginError, deviceLogin } from './login.js';
|
|
9
|
+
import { DEFAULT_SYNTAX, deriveTitle, syntaxForFilename } from './syntax.js';
|
|
10
|
+
import { copyToClipboard, fail, hasPipedStdin, note, readStdin } from './terminal.js';
|
|
11
|
+
// Read from package.json so `pastes --version` can never drift from the
|
|
12
|
+
// published version. npm always ships package.json alongside dist/.
|
|
13
|
+
function readVersion() {
|
|
14
|
+
try {
|
|
15
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
16
|
+
return JSON.parse(readFileSync(join(here, '..', 'package.json'), 'utf8')).version || 'unknown';
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return 'unknown';
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
class UsageError extends Error {
|
|
23
|
+
}
|
|
24
|
+
class AuthError extends Error {
|
|
25
|
+
}
|
|
26
|
+
function buildClient(args, { requireKey = true } = {}) {
|
|
27
|
+
const config = readConfig();
|
|
28
|
+
const apiUrl = resolveApiUrl({ flagUrl: args.apiUrl, config });
|
|
29
|
+
const resolved = resolveApiKey({ flagKey: args.apiKey, config });
|
|
30
|
+
if (requireKey && !resolved) {
|
|
31
|
+
throw new AuthError('No API key found.\n' +
|
|
32
|
+
' Run `pastes login` to connect this machine, or set PASTES_API_KEY\n' +
|
|
33
|
+
' for CI and production environments.');
|
|
34
|
+
}
|
|
35
|
+
return {
|
|
36
|
+
client: new PastesClient({ apiUrl, apiKey: resolved?.apiKey }),
|
|
37
|
+
apiUrl,
|
|
38
|
+
source: resolved?.source
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
async function readContent(args) {
|
|
42
|
+
if (args.target) {
|
|
43
|
+
try {
|
|
44
|
+
return readFileSync(args.target, 'utf8');
|
|
45
|
+
}
|
|
46
|
+
catch (e) {
|
|
47
|
+
throw new UsageError(`Could not read ${args.target}: ${e.message}`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
if (!hasPipedStdin()) {
|
|
51
|
+
throw new UsageError('Nothing to paste. Pass a file, or pipe input:\n cat error.log | pastes');
|
|
52
|
+
}
|
|
53
|
+
return readStdin();
|
|
54
|
+
}
|
|
55
|
+
async function runCreate(args) {
|
|
56
|
+
const content = await readContent(args);
|
|
57
|
+
if (!content.trim()) {
|
|
58
|
+
throw new UsageError('Refusing to create an empty paste.');
|
|
59
|
+
}
|
|
60
|
+
const { client } = buildClient(args);
|
|
61
|
+
const response = await client.createPaste({
|
|
62
|
+
title: args.title || deriveTitle({ filename: args.target, content }),
|
|
63
|
+
content,
|
|
64
|
+
syntax: args.syntax || syntaxForFilename(args.target) || DEFAULT_SYNTAX,
|
|
65
|
+
...(args.expire ? { expire: args.expire } : {}),
|
|
66
|
+
...(args.password ? { password: args.password } : {})
|
|
67
|
+
});
|
|
68
|
+
if (args.json) {
|
|
69
|
+
process.stdout.write(`${JSON.stringify(response, null, 2)}\n`);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
const url = response.success.paste_url;
|
|
73
|
+
process.stdout.write(`${url}\n`);
|
|
74
|
+
// Only touch the clipboard for an interactive run; in a pipeline the user is
|
|
75
|
+
// consuming stdout and a clipboard write would be a surprise.
|
|
76
|
+
if (args.copy && process.stdout.isTTY && copyToClipboard(url)) {
|
|
77
|
+
note(' Copied to clipboard.', { quiet: args.quiet });
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
async function runLogin(args) {
|
|
81
|
+
const { client, apiUrl } = buildClient(args, { requireKey: false });
|
|
82
|
+
if (args.withKey) {
|
|
83
|
+
if (!hasPipedStdin()) {
|
|
84
|
+
throw new UsageError('`--with-key` reads the key from stdin:\n echo $KEY | pastes login --with-key');
|
|
85
|
+
}
|
|
86
|
+
const apiKey = (await readStdin()).trim();
|
|
87
|
+
if (!apiKey) {
|
|
88
|
+
throw new UsageError('No key received on stdin.');
|
|
89
|
+
}
|
|
90
|
+
const verified = await new PastesClient({ apiUrl, apiKey }).whoami();
|
|
91
|
+
const path = writeConfig({ ...readConfig(), apiKey, email: verified.success.email, apiUrl });
|
|
92
|
+
note(`Logged in as ${verified.success.email}. Key saved to ${path}`, { quiet: args.quiet });
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
const result = await deviceLogin({ client, apiUrl, quiet: args.quiet });
|
|
96
|
+
note('', { quiet: args.quiet });
|
|
97
|
+
note(` Logged in as ${result.email}.`, { quiet: args.quiet });
|
|
98
|
+
note(` Key saved to ${result.configPath}`, { quiet: args.quiet });
|
|
99
|
+
}
|
|
100
|
+
async function runLogout(args) {
|
|
101
|
+
const removed = clearConfig();
|
|
102
|
+
note(removed ? `Removed ${configPath()}` : 'No stored credentials to remove.', {
|
|
103
|
+
quiet: args.quiet
|
|
104
|
+
});
|
|
105
|
+
if (process.env.PASTES_API_KEY) {
|
|
106
|
+
note(' PASTES_API_KEY is still set in this environment and will keep being used.', {
|
|
107
|
+
quiet: args.quiet
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
async function runWhoami(args) {
|
|
112
|
+
const { client, source } = buildClient(args);
|
|
113
|
+
const response = await client.whoami();
|
|
114
|
+
if (args.json) {
|
|
115
|
+
process.stdout.write(`${JSON.stringify(response, null, 2)}\n`);
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
process.stdout.write(`${response.success.email}\n`);
|
|
119
|
+
note(` Plan: ${response.success.plan} Key from: ${source}`, { quiet: args.quiet });
|
|
120
|
+
}
|
|
121
|
+
async function runGet(args) {
|
|
122
|
+
if (!args.target) {
|
|
123
|
+
throw new UsageError('Which paste? Try `pastes get <slug>`.');
|
|
124
|
+
}
|
|
125
|
+
const { client } = buildClient(args, { requireKey: false });
|
|
126
|
+
const response = await client.getPaste(args.target, args.password);
|
|
127
|
+
if (args.json) {
|
|
128
|
+
process.stdout.write(`${JSON.stringify(response, null, 2)}\n`);
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
const content = response.success.content;
|
|
132
|
+
process.stdout.write(typeof content === 'string' ? content : '');
|
|
133
|
+
if (typeof content === 'string' && !content.endsWith('\n')) {
|
|
134
|
+
process.stdout.write('\n');
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
async function runList(args) {
|
|
138
|
+
const { client } = buildClient(args);
|
|
139
|
+
const response = await client.listPastes({ query: args.target, page: args.page || 1 });
|
|
140
|
+
if (args.json) {
|
|
141
|
+
process.stdout.write(`${JSON.stringify(response, null, 2)}\n`);
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
const items = Array.isArray(response.data) ? response.data : [];
|
|
145
|
+
if (items.length === 0) {
|
|
146
|
+
note('No pastes found.', { quiet: args.quiet });
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
for (const item of items) {
|
|
150
|
+
// Tab separated so `pastes ls | cut -f1` gives you a list of slugs.
|
|
151
|
+
process.stdout.write(`${item.slug}\t${item.title ?? ''}\n`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
async function runRemove(args) {
|
|
155
|
+
if (!args.target) {
|
|
156
|
+
throw new UsageError('Which paste? Try `pastes rm <slug>`.');
|
|
157
|
+
}
|
|
158
|
+
const { client } = buildClient(args);
|
|
159
|
+
await client.deletePaste(args.target);
|
|
160
|
+
note(`Deleted ${args.target}`, { quiet: args.quiet });
|
|
161
|
+
}
|
|
162
|
+
function explain(e) {
|
|
163
|
+
if (e instanceof ApiError) {
|
|
164
|
+
if (e.status === 401) {
|
|
165
|
+
return (`${e.message}\n` +
|
|
166
|
+
' Run `pastes login`, or check the key in PASTES_API_KEY.');
|
|
167
|
+
}
|
|
168
|
+
if (e.status === 429) {
|
|
169
|
+
return `${e.message}\n See https://pastes.io/pricing for higher limits.`;
|
|
170
|
+
}
|
|
171
|
+
return e.message;
|
|
172
|
+
}
|
|
173
|
+
if (e instanceof Error) {
|
|
174
|
+
return e.message;
|
|
175
|
+
}
|
|
176
|
+
return String(e);
|
|
177
|
+
}
|
|
178
|
+
async function main() {
|
|
179
|
+
let args;
|
|
180
|
+
try {
|
|
181
|
+
args = parseArgs(process.argv.slice(2));
|
|
182
|
+
}
|
|
183
|
+
catch (e) {
|
|
184
|
+
fail(e instanceof ArgError ? e.message : String(e));
|
|
185
|
+
process.stderr.write('\nRun `pastes --help` for usage.\n');
|
|
186
|
+
process.exitCode = 2;
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
if (args.version) {
|
|
190
|
+
process.stdout.write(`${readVersion()}\n`);
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
if (args.help) {
|
|
194
|
+
process.stdout.write(HELP_TEXT);
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
const commands = {
|
|
198
|
+
create: runCreate,
|
|
199
|
+
login: runLogin,
|
|
200
|
+
logout: runLogout,
|
|
201
|
+
whoami: runWhoami,
|
|
202
|
+
get: runGet,
|
|
203
|
+
ls: runList,
|
|
204
|
+
rm: runRemove
|
|
205
|
+
};
|
|
206
|
+
try {
|
|
207
|
+
await commands[args.command](args);
|
|
208
|
+
}
|
|
209
|
+
catch (e) {
|
|
210
|
+
fail(explain(e));
|
|
211
|
+
process.exitCode = e instanceof UsageError || e instanceof AuthError ? 2 : 1;
|
|
212
|
+
if (e instanceof LoginError) {
|
|
213
|
+
process.exitCode = 1;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
void main();
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export declare const DEFAULT_API_URL = "https://pastes.io";
|
|
2
|
+
export interface StoredConfig {
|
|
3
|
+
apiKey?: string;
|
|
4
|
+
email?: string;
|
|
5
|
+
apiUrl?: string;
|
|
6
|
+
}
|
|
7
|
+
export interface ResolvedKey {
|
|
8
|
+
apiKey: string;
|
|
9
|
+
source: 'flag' | 'env' | 'config';
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Windows users expect %APPDATA%; everyone else expects XDG. PASTES_CONFIG_HOME
|
|
13
|
+
* overrides both, which is what containers and CI images tend to want.
|
|
14
|
+
*/
|
|
15
|
+
export declare function configDir(env?: NodeJS.ProcessEnv): string;
|
|
16
|
+
export declare function configPath(env?: NodeJS.ProcessEnv): string;
|
|
17
|
+
export declare function readConfig(env?: NodeJS.ProcessEnv): StoredConfig;
|
|
18
|
+
export declare function writeConfig(config: StoredConfig, env?: NodeJS.ProcessEnv): string;
|
|
19
|
+
export declare function clearConfig(env?: NodeJS.ProcessEnv): boolean;
|
|
20
|
+
/**
|
|
21
|
+
* Precedence is explicit flag, then environment, then the stored config, so a
|
|
22
|
+
* production box can override a stale config file without touching disk.
|
|
23
|
+
*/
|
|
24
|
+
export declare function resolveApiKey({ flagKey, env, config }: {
|
|
25
|
+
flagKey?: string;
|
|
26
|
+
env?: NodeJS.ProcessEnv;
|
|
27
|
+
config?: StoredConfig;
|
|
28
|
+
}): ResolvedKey | undefined;
|
|
29
|
+
export declare function resolveApiUrl({ flagUrl, env, config }: {
|
|
30
|
+
flagUrl?: string;
|
|
31
|
+
env?: NodeJS.ProcessEnv;
|
|
32
|
+
config?: StoredConfig;
|
|
33
|
+
}): string;
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { chmodSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { dirname, join } from 'node:path';
|
|
4
|
+
export const DEFAULT_API_URL = 'https://pastes.io';
|
|
5
|
+
/**
|
|
6
|
+
* Windows users expect %APPDATA%; everyone else expects XDG. PASTES_CONFIG_HOME
|
|
7
|
+
* overrides both, which is what containers and CI images tend to want.
|
|
8
|
+
*/
|
|
9
|
+
export function configDir(env = process.env) {
|
|
10
|
+
if (env.PASTES_CONFIG_HOME) {
|
|
11
|
+
return env.PASTES_CONFIG_HOME;
|
|
12
|
+
}
|
|
13
|
+
if (process.platform === 'win32' && env.APPDATA) {
|
|
14
|
+
return join(env.APPDATA, 'pastes');
|
|
15
|
+
}
|
|
16
|
+
if (env.XDG_CONFIG_HOME) {
|
|
17
|
+
return join(env.XDG_CONFIG_HOME, 'pastes');
|
|
18
|
+
}
|
|
19
|
+
return join(homedir(), '.config', 'pastes');
|
|
20
|
+
}
|
|
21
|
+
export function configPath(env = process.env) {
|
|
22
|
+
return join(configDir(env), 'config.json');
|
|
23
|
+
}
|
|
24
|
+
export function readConfig(env = process.env) {
|
|
25
|
+
try {
|
|
26
|
+
const parsed = JSON.parse(readFileSync(configPath(env), 'utf8'));
|
|
27
|
+
return parsed && typeof parsed === 'object' ? parsed : {};
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
// A missing or unreadable config is not an error — the env var may carry the key.
|
|
31
|
+
return {};
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
export function writeConfig(config, env = process.env) {
|
|
35
|
+
const path = configPath(env);
|
|
36
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
37
|
+
writeFileSync(path, `${JSON.stringify(config, null, 2)}\n`, { mode: 0o600 });
|
|
38
|
+
try {
|
|
39
|
+
// writeFileSync only applies mode when creating, so re-assert it on rewrite.
|
|
40
|
+
chmodSync(path, 0o600);
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
// Permissions are advisory on Windows; the write itself already succeeded.
|
|
44
|
+
}
|
|
45
|
+
return path;
|
|
46
|
+
}
|
|
47
|
+
export function clearConfig(env = process.env) {
|
|
48
|
+
try {
|
|
49
|
+
rmSync(configPath(env));
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Precedence is explicit flag, then environment, then the stored config, so a
|
|
58
|
+
* production box can override a stale config file without touching disk.
|
|
59
|
+
*/
|
|
60
|
+
export function resolveApiKey({ flagKey, env = process.env, config }) {
|
|
61
|
+
if (flagKey) {
|
|
62
|
+
return { apiKey: flagKey, source: 'flag' };
|
|
63
|
+
}
|
|
64
|
+
const fromEnv = env.PASTES_API_KEY?.trim();
|
|
65
|
+
if (fromEnv) {
|
|
66
|
+
return { apiKey: fromEnv, source: 'env' };
|
|
67
|
+
}
|
|
68
|
+
const stored = (config ?? readConfig(env)).apiKey?.trim();
|
|
69
|
+
if (stored) {
|
|
70
|
+
return { apiKey: stored, source: 'config' };
|
|
71
|
+
}
|
|
72
|
+
return undefined;
|
|
73
|
+
}
|
|
74
|
+
export function resolveApiUrl({ flagUrl, env = process.env, config }) {
|
|
75
|
+
const raw = flagUrl || env.PASTES_API_URL || config?.apiUrl || DEFAULT_API_URL;
|
|
76
|
+
return raw.replace(/\/+$/, '');
|
|
77
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { ApiError, PastesClient } from './api.js';
|
|
2
|
+
export type { CreatePasteInput, PastesClientOptions } from './api.js';
|
|
3
|
+
export { DEFAULT_API_URL, resolveApiKey, resolveApiUrl, readConfig } from './config.js';
|
|
4
|
+
export type { StoredConfig } from './config.js';
|
|
5
|
+
export { DEFAULT_SYNTAX, deriveTitle, syntaxForFilename } from './syntax.js';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
// Programmatic entry point, so the client can be reused from a script:
|
|
2
|
+
// import { PastesClient } from 'pastes';
|
|
3
|
+
export { ApiError, PastesClient } from './api.js';
|
|
4
|
+
export { DEFAULT_API_URL, resolveApiKey, resolveApiUrl, readConfig } from './config.js';
|
|
5
|
+
export { DEFAULT_SYNTAX, deriveTitle, syntaxForFilename } from './syntax.js';
|
package/dist/login.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { PastesClient } from './api.js';
|
|
2
|
+
export declare class LoginError extends Error {
|
|
3
|
+
}
|
|
4
|
+
/**
|
|
5
|
+
* RFC 8628-style device flow: we show a short code, the user approves it in any
|
|
6
|
+
* browser (phone, laptop, doesn't matter), and we poll until the key appears.
|
|
7
|
+
* This is what makes `pastes login` work over SSH with no GUI on the host.
|
|
8
|
+
*/
|
|
9
|
+
export declare function deviceLogin({ client, apiUrl, quiet, now }: {
|
|
10
|
+
client: PastesClient;
|
|
11
|
+
apiUrl: string;
|
|
12
|
+
quiet?: boolean;
|
|
13
|
+
now?: () => number;
|
|
14
|
+
}): Promise<{
|
|
15
|
+
email: string | undefined;
|
|
16
|
+
configPath: string;
|
|
17
|
+
}>;
|
package/dist/login.js
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { ApiError } from './api.js';
|
|
2
|
+
import { readConfig, writeConfig } from './config.js';
|
|
3
|
+
import { note, openBrowser } from './terminal.js';
|
|
4
|
+
function sleep(ms) {
|
|
5
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
6
|
+
}
|
|
7
|
+
export class LoginError extends Error {
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* RFC 8628-style device flow: we show a short code, the user approves it in any
|
|
11
|
+
* browser (phone, laptop, doesn't matter), and we poll until the key appears.
|
|
12
|
+
* This is what makes `pastes login` work over SSH with no GUI on the host.
|
|
13
|
+
*/
|
|
14
|
+
export async function deviceLogin({ client, apiUrl, quiet = false, now = () => Date.now() }) {
|
|
15
|
+
const start = await client.startDeviceAuth();
|
|
16
|
+
note('', { quiet });
|
|
17
|
+
note(` Your code: ${start.userCode}`, { quiet });
|
|
18
|
+
note(` Approve at: ${start.verificationUri}`, { quiet });
|
|
19
|
+
note('', { quiet });
|
|
20
|
+
const opened = openBrowser(start.verificationUriComplete);
|
|
21
|
+
note(opened
|
|
22
|
+
? ' Opening your browser... waiting for approval.'
|
|
23
|
+
: ' Open that link on any device and enter the code. Waiting for approval.', { quiet });
|
|
24
|
+
const deadline = now() + start.expiresInSec * 1000;
|
|
25
|
+
let intervalMs = Math.max(1, start.intervalSec) * 1000;
|
|
26
|
+
while (now() < deadline) {
|
|
27
|
+
await sleep(intervalMs);
|
|
28
|
+
let result;
|
|
29
|
+
try {
|
|
30
|
+
result = await client.pollDeviceAuth(start.deviceCode);
|
|
31
|
+
}
|
|
32
|
+
catch (e) {
|
|
33
|
+
// A blip in connectivity should not end a login the user is midway through.
|
|
34
|
+
if (e instanceof ApiError && e.status === 0) {
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
throw e;
|
|
38
|
+
}
|
|
39
|
+
if (result.status === 'approved' && result.apiKey) {
|
|
40
|
+
const config = {
|
|
41
|
+
...readConfig(),
|
|
42
|
+
apiKey: result.apiKey,
|
|
43
|
+
email: result.email
|
|
44
|
+
};
|
|
45
|
+
if (apiUrl) {
|
|
46
|
+
config.apiUrl = apiUrl;
|
|
47
|
+
}
|
|
48
|
+
const path = writeConfig(config);
|
|
49
|
+
return { email: result.email, configPath: path };
|
|
50
|
+
}
|
|
51
|
+
if (result.status === 'expired') {
|
|
52
|
+
throw new LoginError('That code expired before it was approved. Run `pastes login` again.');
|
|
53
|
+
}
|
|
54
|
+
if (result.status === 'slow_down' && result.intervalSec) {
|
|
55
|
+
intervalMs = result.intervalSec * 1000;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
throw new LoginError('Timed out waiting for approval. Run `pastes login` again.');
|
|
59
|
+
}
|
package/dist/syntax.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export declare const DEFAULT_SYNTAX = "txt";
|
|
2
|
+
export declare function syntaxForFilename(filename: string | undefined): string | undefined;
|
|
3
|
+
/**
|
|
4
|
+
* Titles the paste after the file it came from, or the first meaningful line of
|
|
5
|
+
* whatever was piped in. The API requires a title, so this always returns one.
|
|
6
|
+
*/
|
|
7
|
+
export declare function deriveTitle({ filename, content }: {
|
|
8
|
+
filename?: string;
|
|
9
|
+
content: string;
|
|
10
|
+
}): string;
|
package/dist/syntax.js
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
// Maps a filename to a pastes.io syntax slug. The slugs mirror
|
|
2
|
+
// services/pastes-web/src/lib/utils/syntax.json; keep them in sync when a new
|
|
3
|
+
// language is activated there.
|
|
4
|
+
const EXTENSION_TO_SYNTAX = {
|
|
5
|
+
as: 'actionscript',
|
|
6
|
+
bash: 'bash',
|
|
7
|
+
bas: 'basic',
|
|
8
|
+
c: 'c',
|
|
9
|
+
cc: 'cpp',
|
|
10
|
+
cjs: 'javascript',
|
|
11
|
+
cpp: 'cpp',
|
|
12
|
+
cs: 'csharp',
|
|
13
|
+
css: 'css',
|
|
14
|
+
cxx: 'cpp',
|
|
15
|
+
dart: 'dart',
|
|
16
|
+
diff: 'diff',
|
|
17
|
+
dockerfile: 'docker',
|
|
18
|
+
go: 'go',
|
|
19
|
+
h: 'c',
|
|
20
|
+
hh: 'cpp',
|
|
21
|
+
hpp: 'cpp',
|
|
22
|
+
htm: 'html',
|
|
23
|
+
html: 'html',
|
|
24
|
+
java: 'java',
|
|
25
|
+
js: 'javascript',
|
|
26
|
+
json: 'json',
|
|
27
|
+
jsx: 'jsx',
|
|
28
|
+
kt: 'kotlin',
|
|
29
|
+
kts: 'kotlin',
|
|
30
|
+
log: 'logs',
|
|
31
|
+
lua: 'lua',
|
|
32
|
+
markdown: 'markdown',
|
|
33
|
+
matlab: 'matlab',
|
|
34
|
+
md: 'markdown',
|
|
35
|
+
mjs: 'javascript',
|
|
36
|
+
mmd: 'mermaid',
|
|
37
|
+
mermaid: 'mermaid',
|
|
38
|
+
php: 'php',
|
|
39
|
+
ps1: 'powershell',
|
|
40
|
+
psm1: 'powershell',
|
|
41
|
+
patch: 'diff',
|
|
42
|
+
py: 'python',
|
|
43
|
+
pyw: 'python',
|
|
44
|
+
rb: 'ruby',
|
|
45
|
+
rs: 'rust',
|
|
46
|
+
sh: 'bash',
|
|
47
|
+
sql: 'sql',
|
|
48
|
+
swift: 'swift',
|
|
49
|
+
ts: 'typescript',
|
|
50
|
+
tsx: 'typescript',
|
|
51
|
+
txt: 'txt',
|
|
52
|
+
yaml: 'yaml',
|
|
53
|
+
yml: 'yaml',
|
|
54
|
+
zsh: 'bash'
|
|
55
|
+
};
|
|
56
|
+
// Files that carry their language in the name rather than an extension.
|
|
57
|
+
const FILENAME_TO_SYNTAX = {
|
|
58
|
+
dockerfile: 'docker',
|
|
59
|
+
makefile: 'bash',
|
|
60
|
+
'.bashrc': 'bash',
|
|
61
|
+
'.zshrc': 'bash'
|
|
62
|
+
};
|
|
63
|
+
export const DEFAULT_SYNTAX = 'txt';
|
|
64
|
+
export function syntaxForFilename(filename) {
|
|
65
|
+
if (!filename) {
|
|
66
|
+
return undefined;
|
|
67
|
+
}
|
|
68
|
+
const base = filename.replace(/\\/g, '/').split('/').pop() || '';
|
|
69
|
+
const byName = FILENAME_TO_SYNTAX[base.toLowerCase()];
|
|
70
|
+
if (byName) {
|
|
71
|
+
return byName;
|
|
72
|
+
}
|
|
73
|
+
const dot = base.lastIndexOf('.');
|
|
74
|
+
if (dot <= 0 || dot === base.length - 1) {
|
|
75
|
+
return undefined;
|
|
76
|
+
}
|
|
77
|
+
return EXTENSION_TO_SYNTAX[base.slice(dot + 1).toLowerCase()];
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Titles the paste after the file it came from, or the first meaningful line of
|
|
81
|
+
* whatever was piped in. The API requires a title, so this always returns one.
|
|
82
|
+
*/
|
|
83
|
+
export function deriveTitle({ filename, content }) {
|
|
84
|
+
if (filename) {
|
|
85
|
+
const base = filename.replace(/\\/g, '/').split('/').pop();
|
|
86
|
+
if (base) {
|
|
87
|
+
return base;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
for (const line of content.split('\n', 20)) {
|
|
91
|
+
const trimmed = line.trim();
|
|
92
|
+
if (trimmed) {
|
|
93
|
+
return trimmed.length > 60 ? `${trimmed.slice(0, 57)}...` : trimmed;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return 'Untitled paste';
|
|
97
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Everything the user reads goes to stderr so stdout stays a clean pipe:
|
|
3
|
+
* `pastes < log | pbcopy` must receive the URL and nothing else.
|
|
4
|
+
*/
|
|
5
|
+
export declare function note(message: string, { quiet }?: {
|
|
6
|
+
quiet?: boolean | undefined;
|
|
7
|
+
}): void;
|
|
8
|
+
export declare function fail(message: string): void;
|
|
9
|
+
export declare function readStdin(): Promise<string>;
|
|
10
|
+
export declare function hasPipedStdin(): boolean;
|
|
11
|
+
/** True when there is plausibly a desktop session that could show a browser. */
|
|
12
|
+
export declare function hasDisplay(): boolean;
|
|
13
|
+
export declare function openBrowser(url: string): boolean;
|
|
14
|
+
/** Best effort: a machine without a clipboard tool is not an error. */
|
|
15
|
+
export declare function copyToClipboard(text: string): boolean;
|
package/dist/terminal.js
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { spawn, spawnSync } from 'node:child_process';
|
|
2
|
+
/**
|
|
3
|
+
* Everything the user reads goes to stderr so stdout stays a clean pipe:
|
|
4
|
+
* `pastes < log | pbcopy` must receive the URL and nothing else.
|
|
5
|
+
*/
|
|
6
|
+
export function note(message, { quiet = false } = {}) {
|
|
7
|
+
if (!quiet) {
|
|
8
|
+
process.stderr.write(`${message}\n`);
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
export function fail(message) {
|
|
12
|
+
process.stderr.write(`error: ${message}\n`);
|
|
13
|
+
}
|
|
14
|
+
export function readStdin() {
|
|
15
|
+
return new Promise((resolve, reject) => {
|
|
16
|
+
const chunks = [];
|
|
17
|
+
process.stdin.on('data', (chunk) => chunks.push(chunk));
|
|
18
|
+
process.stdin.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')));
|
|
19
|
+
process.stdin.on('error', reject);
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
export function hasPipedStdin() {
|
|
23
|
+
return !process.stdin.isTTY;
|
|
24
|
+
}
|
|
25
|
+
/** True when there is plausibly a desktop session that could show a browser. */
|
|
26
|
+
export function hasDisplay() {
|
|
27
|
+
if (process.platform === 'win32' || process.platform === 'darwin') {
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
if (process.env.SSH_CONNECTION || process.env.SSH_TTY) {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
return Boolean(process.env.DISPLAY || process.env.WAYLAND_DISPLAY);
|
|
34
|
+
}
|
|
35
|
+
function runDetached(command, args) {
|
|
36
|
+
try {
|
|
37
|
+
const child = spawn(command, args, { stdio: 'ignore', detached: true });
|
|
38
|
+
child.on('error', () => {
|
|
39
|
+
// Nothing to do — callers always print the URL as a fallback.
|
|
40
|
+
});
|
|
41
|
+
child.unref();
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
export function openBrowser(url) {
|
|
49
|
+
// Escape hatch for CI, containers, and anyone who does not want their browser
|
|
50
|
+
// taken over. The link is always printed, so nothing is lost.
|
|
51
|
+
if (process.env.PASTES_NO_BROWSER) {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
if (!hasDisplay()) {
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
if (process.platform === 'win32') {
|
|
58
|
+
// `start` is a cmd builtin; the empty string is the (required) window title.
|
|
59
|
+
return runDetached('cmd', ['/c', 'start', '', url]);
|
|
60
|
+
}
|
|
61
|
+
if (process.platform === 'darwin') {
|
|
62
|
+
return runDetached('open', [url]);
|
|
63
|
+
}
|
|
64
|
+
return runDetached('xdg-open', [url]);
|
|
65
|
+
}
|
|
66
|
+
function clipboardCommand() {
|
|
67
|
+
if (process.platform === 'win32') {
|
|
68
|
+
return ['clip', []];
|
|
69
|
+
}
|
|
70
|
+
if (process.platform === 'darwin') {
|
|
71
|
+
return ['pbcopy', []];
|
|
72
|
+
}
|
|
73
|
+
if (process.env.WAYLAND_DISPLAY) {
|
|
74
|
+
return ['wl-copy', []];
|
|
75
|
+
}
|
|
76
|
+
if (process.env.DISPLAY) {
|
|
77
|
+
return ['xclip', ['-selection', 'clipboard']];
|
|
78
|
+
}
|
|
79
|
+
return undefined;
|
|
80
|
+
}
|
|
81
|
+
/** Best effort: a machine without a clipboard tool is not an error. */
|
|
82
|
+
export function copyToClipboard(text) {
|
|
83
|
+
const command = clipboardCommand();
|
|
84
|
+
if (!command) {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
try {
|
|
88
|
+
const result = spawnSync(command[0], command[1], { input: text, stdio: 'pipe' });
|
|
89
|
+
return !result.error && result.status === 0;
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "pastes",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Create and share pastes on pastes.io from your terminal. Pipe logs, upload files, get a link.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"pastes.io",
|
|
7
|
+
"pastebin",
|
|
8
|
+
"paste",
|
|
9
|
+
"cli",
|
|
10
|
+
"share",
|
|
11
|
+
"snippet",
|
|
12
|
+
"logs",
|
|
13
|
+
"gist"
|
|
14
|
+
],
|
|
15
|
+
"homepage": "https://pastes.io/docs",
|
|
16
|
+
"bugs": "https://github.com/pastes-io/pastes-cli/issues",
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "git+https://github.com/pastes-io/pastes-cli.git"
|
|
20
|
+
},
|
|
21
|
+
"license": "MIT",
|
|
22
|
+
"type": "module",
|
|
23
|
+
"bin": {
|
|
24
|
+
"pastes": "dist/cli.js"
|
|
25
|
+
},
|
|
26
|
+
"exports": {
|
|
27
|
+
".": "./dist/index.js"
|
|
28
|
+
},
|
|
29
|
+
"files": [
|
|
30
|
+
"dist",
|
|
31
|
+
"README.md"
|
|
32
|
+
],
|
|
33
|
+
"engines": {
|
|
34
|
+
"node": ">=20"
|
|
35
|
+
},
|
|
36
|
+
"scripts": {
|
|
37
|
+
"build": "tsc -p tsconfig.json",
|
|
38
|
+
"test": "npm run build && node scripts/test.mjs",
|
|
39
|
+
"prepublishOnly": "npm run build",
|
|
40
|
+
"dev": "tsx src/cli.ts"
|
|
41
|
+
},
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"@types/node": "^24.0.0",
|
|
44
|
+
"tsx": "^4.19.2",
|
|
45
|
+
"typescript": "^5.9.2"
|
|
46
|
+
},
|
|
47
|
+
"publishConfig": {
|
|
48
|
+
"access": "public"
|
|
49
|
+
}
|
|
50
|
+
}
|