ghimg 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/README.md +60 -0
- package/bin/ghimg.js +56 -0
- package/commands/check.js +13 -0
- package/commands/init.js +33 -0
- package/commands/upload.js +31 -0
- package/lib/args.js +22 -0
- package/lib/config.js +55 -0
- package/lib/github.js +164 -0
- package/package.json +49 -0
package/README.md
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# ghimg
|
|
2
|
+
|
|
3
|
+
Upload images to GitHub's `user-attachments` CDN from the command line and get a
|
|
4
|
+
markdown reference back. Replicates GitHub's web UI upload flow, so the resulting
|
|
5
|
+
URLs are scoped to the repository they're uploaded to (private-repo images stay
|
|
6
|
+
private).
|
|
7
|
+
|
|
8
|
+
## Setup
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
npx ghimg init
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
The wizard asks for your github.com `user_session` cookie value — copy it from
|
|
15
|
+
devtools (`Application -> Cookies -> https://github.com -> user_session`). It's
|
|
16
|
+
saved to `.env` as `GH_SESSION_TOKEN` and validated against GitHub.
|
|
17
|
+
|
|
18
|
+
## Usage
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
# repo inferred from the current git remote
|
|
22
|
+
npx ghimg upload screenshot.png
|
|
23
|
+
|
|
24
|
+
# target a repo explicitly, multiple files at once
|
|
25
|
+
npx ghimg upload --repo owner/repo a.png b.png
|
|
26
|
+
|
|
27
|
+
# verify the saved token
|
|
28
|
+
npx ghimg check
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Each upload prints ``.
|
|
32
|
+
|
|
33
|
+
## Commands
|
|
34
|
+
|
|
35
|
+
| Command | Description |
|
|
36
|
+
| -------- | ---------------------------------------------------------------- |
|
|
37
|
+
| `init` | Setup wizard for your GitHub session token |
|
|
38
|
+
| `upload` | Upload one or more images and print markdown references |
|
|
39
|
+
| `check` | Verify the saved token and print the authenticated username |
|
|
40
|
+
|
|
41
|
+
All commands accept `--config <path>` to point at a specific `.env` file
|
|
42
|
+
(defaults to `.env` in the current directory). `upload` also takes
|
|
43
|
+
`--repo owner/repo`.
|
|
44
|
+
|
|
45
|
+
## Requirements
|
|
46
|
+
|
|
47
|
+
- **Node 22+** — uses global `fetch`, `FormData`, and `Blob`.
|
|
48
|
+
- **[`gh` CLI](https://cli.github.com)**, authenticated — used to resolve the numeric
|
|
49
|
+
repository ID (works for private repos).
|
|
50
|
+
|
|
51
|
+
## Security
|
|
52
|
+
|
|
53
|
+
This tool uses your `user_session` cookie, which is your full authenticated GitHub
|
|
54
|
+
web session — more powerful than a scoped token. It is only ever sent to
|
|
55
|
+
`github.com` and to the presigned S3 URL GitHub returns. The token is stored in
|
|
56
|
+
`.env` (gitignored); don't commit it.
|
|
57
|
+
|
|
58
|
+
## License
|
|
59
|
+
|
|
60
|
+
MIT
|
package/bin/ghimg.js
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { init } from '../commands/init.js'
|
|
4
|
+
import { upload } from '../commands/upload.js'
|
|
5
|
+
import { check } from '../commands/check.js'
|
|
6
|
+
|
|
7
|
+
const command = process.argv[2]
|
|
8
|
+
const args = process.argv.slice(3)
|
|
9
|
+
|
|
10
|
+
const commands = {
|
|
11
|
+
init,
|
|
12
|
+
upload: () => upload(args),
|
|
13
|
+
check: () => check(args)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// A leading argument that is neither a flag nor a known command is treated as a
|
|
17
|
+
// file path, so `ghimg screenshot.png` is shorthand for `ghimg upload screenshot.png`.
|
|
18
|
+
function looksLikeFile(arg) {
|
|
19
|
+
return Boolean(arg) && !arg.startsWith('-')
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async function main() {
|
|
23
|
+
try {
|
|
24
|
+
if (commands[command]) {
|
|
25
|
+
await commands[command]()
|
|
26
|
+
} else if (looksLikeFile(command)) {
|
|
27
|
+
await upload(process.argv.slice(2))
|
|
28
|
+
} else {
|
|
29
|
+
console.log(`
|
|
30
|
+
Usage: ghimg [command] <image-path>...
|
|
31
|
+
|
|
32
|
+
Commands:
|
|
33
|
+
init Setup wizard for your GitHub session token
|
|
34
|
+
upload [--repo owner/repo] Upload one or more images, print markdown
|
|
35
|
+
<image-path>... (repo defaults to the current git remote)
|
|
36
|
+
check Verify the saved token and print the username
|
|
37
|
+
|
|
38
|
+
The "upload" command is the default: a first argument that looks like a file is
|
|
39
|
+
uploaded directly. All commands accept --config <path> to point at a specific
|
|
40
|
+
.env file (defaults to .env in the current directory).
|
|
41
|
+
|
|
42
|
+
Examples:
|
|
43
|
+
npx ghimg init
|
|
44
|
+
npx ghimg screenshot.png
|
|
45
|
+
npx ghimg upload --repo owner/repo a.png b.png
|
|
46
|
+
npx ghimg check
|
|
47
|
+
`)
|
|
48
|
+
process.exit(command ? 1 : 0)
|
|
49
|
+
}
|
|
50
|
+
} catch (error) {
|
|
51
|
+
console.error('Error:', error.message)
|
|
52
|
+
process.exit(1)
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
main()
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { loadValidatedConfig } from '../lib/config.js'
|
|
2
|
+
import { parseArgs } from '../lib/args.js'
|
|
3
|
+
import { checkValidity } from '../lib/github.js'
|
|
4
|
+
|
|
5
|
+
export async function check(argv) {
|
|
6
|
+
const { options } = parseArgs(argv, { flags: ['--config'] })
|
|
7
|
+
|
|
8
|
+
const { sessionToken } = loadValidatedConfig(options.config)
|
|
9
|
+
const username = await checkValidity(sessionToken)
|
|
10
|
+
|
|
11
|
+
console.error('Token is valid')
|
|
12
|
+
if (username) console.log(username)
|
|
13
|
+
}
|
package/commands/init.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { intro, outro, password, isCancel, cancel, log, spinner } from '@clack/prompts'
|
|
2
|
+
import { saveConfig } from '../lib/config.js'
|
|
3
|
+
import { checkValidity } from '../lib/github.js'
|
|
4
|
+
|
|
5
|
+
function required(value) {
|
|
6
|
+
return value ? undefined : 'Required'
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export async function init() {
|
|
10
|
+
intro('ghimg setup')
|
|
11
|
+
|
|
12
|
+
log.info('Copy the "user_session" cookie from github.com:\n devtools -> Application -> Cookies -> https://github.com -> user_session')
|
|
13
|
+
|
|
14
|
+
const token = await password({
|
|
15
|
+
message: 'GitHub user_session cookie value',
|
|
16
|
+
validate: required
|
|
17
|
+
})
|
|
18
|
+
if (isCancel(token)) return cancel('Setup cancelled')
|
|
19
|
+
|
|
20
|
+
saveConfig({ sessionToken: token.trim() })
|
|
21
|
+
|
|
22
|
+
const s = spinner()
|
|
23
|
+
s.start('Validating token')
|
|
24
|
+
try {
|
|
25
|
+
const username = await checkValidity(token.trim())
|
|
26
|
+
s.stop(username ? `Authenticated as ${username}` : 'Token saved (could not read username)')
|
|
27
|
+
} catch (error) {
|
|
28
|
+
s.stop(`Saved, but validation failed: ${error.message}`)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
log.success('Saved to .env')
|
|
32
|
+
outro('Done. Try: npx ghimg upload screenshot.png')
|
|
33
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { loadValidatedConfig } from '../lib/config.js'
|
|
2
|
+
import { parseArgs } from '../lib/args.js'
|
|
3
|
+
import { resolveRepo, getUploadToken, uploadAsset } from '../lib/github.js'
|
|
4
|
+
|
|
5
|
+
export async function upload(argv) {
|
|
6
|
+
const { options, positionals } = parseArgs(argv, {
|
|
7
|
+
flags: ['--repo', '--config']
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
if (positionals.length === 0) {
|
|
11
|
+
console.error('Usage: ghimg upload [--repo owner/repo] <image-path>...')
|
|
12
|
+
process.exit(1)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const { sessionToken } = loadValidatedConfig(options.config)
|
|
16
|
+
const info = await resolveRepo(options.repo)
|
|
17
|
+
const uploadToken = await getUploadToken(sessionToken, info.owner, info.name)
|
|
18
|
+
|
|
19
|
+
let failed = false
|
|
20
|
+
for (const path of positionals) {
|
|
21
|
+
try {
|
|
22
|
+
const { markdown } = await uploadAsset(sessionToken, info, uploadToken, path)
|
|
23
|
+
console.log(markdown)
|
|
24
|
+
} catch (error) {
|
|
25
|
+
console.error(`Error uploading ${path}: ${error.message}`)
|
|
26
|
+
failed = true
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (failed) process.exit(1)
|
|
31
|
+
}
|
package/lib/args.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// Split argv into named options and bare positionals, so flags can appear in
|
|
2
|
+
// any order. `flags` take a following value; `booleans` are standalone.
|
|
3
|
+
// Option keys drop the leading '--' (e.g. '--repo' -> 'repo').
|
|
4
|
+
function parseArgs(argv, { flags = [], booleans = [] } = {}) {
|
|
5
|
+
const options = {}
|
|
6
|
+
const positionals = []
|
|
7
|
+
|
|
8
|
+
for (let i = 0; i < argv.length; i++) {
|
|
9
|
+
const arg = argv[i]
|
|
10
|
+
if (booleans.includes(arg)) {
|
|
11
|
+
options[arg.replace(/^--/, '')] = true
|
|
12
|
+
} else if (flags.includes(arg)) {
|
|
13
|
+
options[arg.replace(/^--/, '')] = argv[++i]
|
|
14
|
+
} else {
|
|
15
|
+
positionals.push(arg)
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return { options, positionals }
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export { parseArgs }
|
package/lib/config.js
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
import { existsSync, writeFileSync } from 'fs'
|
|
3
|
+
|
|
4
|
+
const ENV_FILE = '.env'
|
|
5
|
+
|
|
6
|
+
const configSchema = z.object({
|
|
7
|
+
sessionToken: z.string().min(1)
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
// Load the given .env file (if present), then fall back to whatever is already
|
|
11
|
+
// in the environment.
|
|
12
|
+
function readEnv(envFile) {
|
|
13
|
+
if (existsSync(envFile)) process.loadEnvFile(envFile)
|
|
14
|
+
return process.env
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function loadConfig(envFile = ENV_FILE) {
|
|
18
|
+
const env = readEnv(envFile)
|
|
19
|
+
|
|
20
|
+
if (!env.GH_SESSION_TOKEN) return null
|
|
21
|
+
|
|
22
|
+
const result = configSchema.safeParse({
|
|
23
|
+
sessionToken: env.GH_SESSION_TOKEN.trim()
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
if (!result.success) {
|
|
27
|
+
console.error('Configuration validation failed:')
|
|
28
|
+
console.error(z.prettifyError(result.error))
|
|
29
|
+
process.exit(1)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return result.data
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function loadValidatedConfig(envFile = ENV_FILE) {
|
|
36
|
+
const config = loadConfig(envFile)
|
|
37
|
+
|
|
38
|
+
if (!config) {
|
|
39
|
+
console.error('No session token found. Run "npx ghimg init" first, or set GH_SESSION_TOKEN.')
|
|
40
|
+
process.exit(1)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return config
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function saveConfig(config) {
|
|
47
|
+
try {
|
|
48
|
+
writeFileSync(ENV_FILE, `GH_SESSION_TOKEN="${config.sessionToken}"\n`)
|
|
49
|
+
} catch (error) {
|
|
50
|
+
console.error('Error writing .env file: ' + error.message)
|
|
51
|
+
process.exit(1)
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export { loadConfig, loadValidatedConfig, saveConfig }
|
package/lib/github.js
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { execa } from 'execa'
|
|
2
|
+
import { readFileSync } from 'fs'
|
|
3
|
+
import { basename, extname } from 'path'
|
|
4
|
+
|
|
5
|
+
const GITHUB = 'https://github.com'
|
|
6
|
+
const USER_AGENT =
|
|
7
|
+
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 ' +
|
|
8
|
+
'(KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36'
|
|
9
|
+
|
|
10
|
+
const MIME = {
|
|
11
|
+
'.png': 'image/png',
|
|
12
|
+
'.jpg': 'image/jpeg',
|
|
13
|
+
'.jpeg': 'image/jpeg',
|
|
14
|
+
'.gif': 'image/gif',
|
|
15
|
+
'.webp': 'image/webp',
|
|
16
|
+
'.svg': 'image/svg+xml'
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// GitHub requires both cookies for CSRF validation on the upload endpoint.
|
|
20
|
+
function cookieHeader(token) {
|
|
21
|
+
return `user_session=${token}; __Host-user_session_same_site=${token}`
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function ghHeaders(token, owner, repo, extra = {}) {
|
|
25
|
+
return {
|
|
26
|
+
Cookie: cookieHeader(token),
|
|
27
|
+
'User-Agent': USER_AGENT,
|
|
28
|
+
Origin: GITHUB,
|
|
29
|
+
Referer: `${GITHUB}/${owner}/${repo}`,
|
|
30
|
+
'X-Requested-With': 'XMLHttpRequest',
|
|
31
|
+
...extra
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Resolve owner/name (from --repo or the git remote) plus the numeric repo ID.
|
|
36
|
+
// The ID is looked up via the gh CLI, so private repos resolve too.
|
|
37
|
+
async function resolveRepo(repoFlag) {
|
|
38
|
+
let slug = repoFlag
|
|
39
|
+
|
|
40
|
+
if (!slug) {
|
|
41
|
+
let url
|
|
42
|
+
try {
|
|
43
|
+
const { stdout } = await execa('git', ['remote', 'get-url', 'origin'])
|
|
44
|
+
url = stdout.trim()
|
|
45
|
+
} catch {
|
|
46
|
+
throw new Error('not a git repository — pass --repo owner/repo')
|
|
47
|
+
}
|
|
48
|
+
const match = url.match(/github\.com[:/](.+?)\/(.+?)(?:\.git)?$/)
|
|
49
|
+
slug = match ? `${match[1]}/${match[2]}` : ''
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const [owner, name] = slug.split('/')
|
|
53
|
+
if (!owner || !name) throw new Error(`could not parse owner/repo from: ${slug || '(empty)'}`)
|
|
54
|
+
|
|
55
|
+
let id
|
|
56
|
+
try {
|
|
57
|
+
const { stdout } = await execa('gh', ['api', `repos/${owner}/${name}`, '--jq', '.id'])
|
|
58
|
+
id = Number(stdout.trim())
|
|
59
|
+
} catch {
|
|
60
|
+
throw new Error(`failed to look up repo ID for ${owner}/${name} (is the gh CLI installed and authenticated?)`)
|
|
61
|
+
}
|
|
62
|
+
if (!id) throw new Error(`unexpected repo ID for ${owner}/${name}`)
|
|
63
|
+
|
|
64
|
+
return { owner, name, id }
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Verify a session token by loading the profile page; returns the username.
|
|
68
|
+
async function checkValidity(token) {
|
|
69
|
+
const response = await fetch(`${GITHUB}/settings/profile`, {
|
|
70
|
+
headers: { Cookie: cookieHeader(token), 'User-Agent': USER_AGENT },
|
|
71
|
+
redirect: 'manual'
|
|
72
|
+
})
|
|
73
|
+
if (response.status !== 200) {
|
|
74
|
+
throw new Error(`token is invalid or expired (status ${response.status})`)
|
|
75
|
+
}
|
|
76
|
+
const match = (await response.text()).match(/<meta name="user-login" content="([^"]+)"/)
|
|
77
|
+
return match ? match[1] : ''
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Step 0: scrape the per-repo uploadToken from the repo page.
|
|
81
|
+
async function getUploadToken(token, owner, repo) {
|
|
82
|
+
const response = await fetch(`${GITHUB}/${owner}/${repo}`, {
|
|
83
|
+
headers: { Cookie: cookieHeader(token), 'User-Agent': USER_AGENT }
|
|
84
|
+
})
|
|
85
|
+
if (!response.ok) {
|
|
86
|
+
throw new Error(`repo page ${response.status} — do you have access to ${owner}/${repo}?`)
|
|
87
|
+
}
|
|
88
|
+
const match = (await response.text()).match(/"uploadToken":"([^"]+)"/)
|
|
89
|
+
if (!match) {
|
|
90
|
+
throw new Error(
|
|
91
|
+
`uploadToken not found — need write access to ${owner}/${repo} ` +
|
|
92
|
+
`(or, if it enforces SAML SSO, authorize at ${GITHUB}/orgs/${owner}/sso)`
|
|
93
|
+
)
|
|
94
|
+
}
|
|
95
|
+
return match[1]
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Step 1: request the S3 upload policy.
|
|
99
|
+
async function requestPolicy(token, info, uploadToken, file, size, contentType) {
|
|
100
|
+
const form = new FormData()
|
|
101
|
+
form.set('name', file)
|
|
102
|
+
form.set('size', String(size))
|
|
103
|
+
form.set('content_type', contentType)
|
|
104
|
+
form.set('authenticity_token', uploadToken)
|
|
105
|
+
form.set('repository_id', String(info.id))
|
|
106
|
+
|
|
107
|
+
const response = await fetch(`${GITHUB}/upload/policies/assets`, {
|
|
108
|
+
method: 'POST',
|
|
109
|
+
body: form,
|
|
110
|
+
headers: ghHeaders(token, info.owner, info.name, { Accept: 'application/json' })
|
|
111
|
+
})
|
|
112
|
+
if (response.status !== 201) {
|
|
113
|
+
throw new Error(`policy request ${response.status}: ${(await response.text()).slice(0, 200)}`)
|
|
114
|
+
}
|
|
115
|
+
return response.json()
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Step 2: upload the bytes to the presigned S3 URL (no GitHub cookies here).
|
|
119
|
+
async function uploadToS3(policy, buffer, file, contentType) {
|
|
120
|
+
const form = new FormData()
|
|
121
|
+
// Server-provided field order; the file part must come last.
|
|
122
|
+
for (const [key, value] of Object.entries(policy.form)) form.set(key, value)
|
|
123
|
+
form.set('file', new Blob([buffer], { type: contentType }), file)
|
|
124
|
+
|
|
125
|
+
const response = await fetch(policy.upload_url, {
|
|
126
|
+
method: 'POST',
|
|
127
|
+
body: form,
|
|
128
|
+
headers: { Origin: GITHUB, 'User-Agent': USER_AGENT }
|
|
129
|
+
})
|
|
130
|
+
if (![200, 201, 204].includes(response.status)) {
|
|
131
|
+
throw new Error(`S3 upload ${response.status}: ${(await response.text()).slice(0, 300)}`)
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Step 3: finalize the asset and return its public reference.
|
|
136
|
+
async function finalizeUpload(token, info, policy) {
|
|
137
|
+
const form = new FormData()
|
|
138
|
+
form.set('authenticity_token', policy.asset_upload_authenticity_token)
|
|
139
|
+
|
|
140
|
+
const response = await fetch(`${GITHUB}/upload/assets/${policy.asset.id}`, {
|
|
141
|
+
method: 'PUT',
|
|
142
|
+
body: form,
|
|
143
|
+
headers: ghHeaders(token, info.owner, info.name, { Accept: 'application/json' })
|
|
144
|
+
})
|
|
145
|
+
if (response.status !== 200) {
|
|
146
|
+
throw new Error(`finalize ${response.status}: ${(await response.text()).slice(0, 200)}`)
|
|
147
|
+
}
|
|
148
|
+
return response.json()
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Upload one image through the full flow and return its markdown reference.
|
|
152
|
+
async function uploadAsset(token, info, uploadToken, path) {
|
|
153
|
+
const buffer = readFileSync(path)
|
|
154
|
+
const file = basename(path)
|
|
155
|
+
const contentType = MIME[extname(path).toLowerCase()] ?? 'application/octet-stream'
|
|
156
|
+
|
|
157
|
+
const policy = await requestPolicy(token, info, uploadToken, file, buffer.length, contentType)
|
|
158
|
+
await uploadToS3(policy, buffer, file, contentType)
|
|
159
|
+
const { href, name } = await finalizeUpload(token, info, policy)
|
|
160
|
+
|
|
161
|
+
return { markdown: ``, url: href, name }
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export { resolveRepo, checkValidity, getUploadToken, uploadAsset }
|
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "ghimg",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Upload images to GitHub from the command line and print markdown references.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"ghimg": "bin/ghimg.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin",
|
|
11
|
+
"commands",
|
|
12
|
+
"lib"
|
|
13
|
+
],
|
|
14
|
+
"scripts": {
|
|
15
|
+
"lint": "eslint .",
|
|
16
|
+
"lint:fix": "eslint . --fix"
|
|
17
|
+
},
|
|
18
|
+
"keywords": [
|
|
19
|
+
"github",
|
|
20
|
+
"image",
|
|
21
|
+
"upload",
|
|
22
|
+
"markdown",
|
|
23
|
+
"cli",
|
|
24
|
+
"user-attachments"
|
|
25
|
+
],
|
|
26
|
+
"author": "Nico Devs <hi@nicodevs.com>",
|
|
27
|
+
"license": "MIT",
|
|
28
|
+
"repository": {
|
|
29
|
+
"type": "git",
|
|
30
|
+
"url": "git+https://github.com/nicodevs/ghimg.git"
|
|
31
|
+
},
|
|
32
|
+
"bugs": {
|
|
33
|
+
"url": "https://github.com/nicodevs/ghimg/issues"
|
|
34
|
+
},
|
|
35
|
+
"homepage": "https://github.com/nicodevs/ghimg#readme",
|
|
36
|
+
"engines": {
|
|
37
|
+
"node": ">=22"
|
|
38
|
+
},
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"@clack/prompts": "^0.11.0",
|
|
41
|
+
"execa": "^9.0.0",
|
|
42
|
+
"zod": "^4.3.4"
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"@eslint/js": "^9.39.2",
|
|
46
|
+
"eslint": "^9.39.2",
|
|
47
|
+
"globals": "^17.3.0"
|
|
48
|
+
}
|
|
49
|
+
}
|