packmd 0.0.1 → 1.0.1
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 +150 -0
- package/dist/index.js +435 -1
- package/package.json +38 -5
package/README.md
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+

|
|
2
|
+
|
|
3
|
+
## packmd
|
|
4
|
+
|
|
5
|
+
**PackMD** is a command‑line tool that converts any GitHub repository, local directory, or web page into a clean, token‑efficient Markdown digest – ready to paste into ChatGPT, Claude, or any LLM.
|
|
6
|
+
|
|
7
|
+
### Installation
|
|
8
|
+
|
|
9
|
+
**Global (recommended)**
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm install -g packmd
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Now you can use `packmd` from anywhere:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
packmd --help
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
**Run without installing (using npx)**
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npx packmd <target> [options]
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
**From source (monorepo)**
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
git clone https://github.com/thelastofinusa/packmd.git
|
|
31
|
+
cd packmd
|
|
32
|
+
bun install
|
|
33
|
+
cd packages/cli
|
|
34
|
+
bun run build
|
|
35
|
+
# run locally
|
|
36
|
+
node dist/index.js <target> [options]
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### Quick Start
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
# Generate a digest from a public GitHub repo
|
|
43
|
+
packmd https://github.com/vercel/next.js -o next.md
|
|
44
|
+
|
|
45
|
+
# Scrape a documentation website (requires Jina API key for higher limits)
|
|
46
|
+
packmd https://react.dev --jina-api-key YOUR_KEY --copy
|
|
47
|
+
|
|
48
|
+
# Digest the current directory (respects .gitignore)
|
|
49
|
+
packmd .
|
|
50
|
+
|
|
51
|
+
# Digest a local folder with custom options
|
|
52
|
+
packmd ~/projects/my-app -m 300 -s 50 --exclude "*.test.js"
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### Usage
|
|
56
|
+
|
|
57
|
+
```
|
|
58
|
+
packmd [target] [options]
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
| Argument | Description |
|
|
62
|
+
| -------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
63
|
+
| `target` | GitHub URL (e.g., `https://github.com/owner/repo`), owner/repo slug, local directory path, or any webpage URL. Defaults to current directory (`.`). |
|
|
64
|
+
|
|
65
|
+
**Options**
|
|
66
|
+
|
|
67
|
+
| Flag | Description |
|
|
68
|
+
| ----------------------------- | ------------------------------------------------------------------------------------------ |
|
|
69
|
+
| `-o, --output <path>` | Save the generated Markdown to a specific file (e.g., `digest.md`). |
|
|
70
|
+
| `-c, --copy` | Copy the output directly to your clipboard (no file saved). |
|
|
71
|
+
| `-t, --token <token>` | GitHub Personal Access Token for private repositories or higher rate limits. |
|
|
72
|
+
| `-m, --max-files <number>` | Maximum number of files to include (default: `200`). |
|
|
73
|
+
| `-s, --max-file-size <kb>` | Maximum file size in KB (files larger are skipped) (default: `100`). |
|
|
74
|
+
| `-i, --include <patterns...>` | Glob patterns to explicitly include (e.g., `"*.ts" "*.md"`). |
|
|
75
|
+
| `-e, --exclude <patterns...>` | Glob patterns to explicitly ignore (e.g., `"test/**" "*.log"`). |
|
|
76
|
+
| `--jina-api-key <key>` | Jina Reader API key – raises the free rate limit from 20 to 500 requests per minute. |
|
|
77
|
+
| `--no-gitignore` | Ignore `.gitignore` rules when scanning a local directory (by default they are respected). |
|
|
78
|
+
| `-v, --version` | Show the version number. |
|
|
79
|
+
| `-h, --help` | Show help. |
|
|
80
|
+
|
|
81
|
+
> **Note:** When using `--output`, if the file already exists, you will be prompted to overwrite or enter a new name.
|
|
82
|
+
|
|
83
|
+
### Examples
|
|
84
|
+
|
|
85
|
+
**GitHub repository**
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
# Basic
|
|
89
|
+
packmd facebook/react -o react.md
|
|
90
|
+
|
|
91
|
+
# With token (for private repos)
|
|
92
|
+
packmd my-org/private-repo -t ghp_xxxxx -o private.md
|
|
93
|
+
|
|
94
|
+
# Limit to 300 files, exclude everything in the "examples" folder
|
|
95
|
+
packmd vercel/next.js -m 300 -e "examples/**" -o next-limited.md
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
**Web page (via Jina Reader)**
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
# Simple scrape
|
|
102
|
+
packmd https://docs.nestjs.com --copy
|
|
103
|
+
|
|
104
|
+
# With Jina API key (higher rate limit)
|
|
105
|
+
packmd https://tailwindcss.com/docs --jina-api-key jina_xxxxx -o tailwind.md
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
**Local directory**
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
# Digest the current folder
|
|
112
|
+
packmd .
|
|
113
|
+
|
|
114
|
+
# Digest a specific path
|
|
115
|
+
packmd ~/code/my-project -o project.md
|
|
116
|
+
|
|
117
|
+
# Override .gitignore and include only TypeScript files
|
|
118
|
+
packmd ./src --no-gitignore -i "*.ts" "*.tsx"
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
**Combine options**
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
packmd https://github.com/expressjs/express -o express.md -m 100 -s 50 -e "test/**" "*.spec.js"
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### How It Works
|
|
128
|
+
|
|
129
|
+
The CLI uses the same core engine (`@packmd/core`) as the web app:
|
|
130
|
+
|
|
131
|
+
1. **Input detection** – determines if the target is a GitHub repo, a web page, or a local path.
|
|
132
|
+
2. **Fetching** – for GitHub, uses the GitHub API; for web pages, uses Jina Reader; for local, uses Node.js `fs`.
|
|
133
|
+
3. **Filtering** – applies ignore rules, globs, size limits, and binary detection.
|
|
134
|
+
4. **Processing** – builds a directory tree and downloads file contents (in parallel for GitHub).
|
|
135
|
+
5. **Markdown generation** – creates a header with metadata, an ASCII tree, and fenced code blocks for each file.
|
|
136
|
+
|
|
137
|
+
Progress is shown via a live spinner (`ora`). Output can be saved to a file or copied to your clipboard.
|
|
138
|
+
|
|
139
|
+
### Notes
|
|
140
|
+
|
|
141
|
+
- **GitHub rate limits** – Without a token, you are limited to 60 requests per hour. Use `-t` to raise this to 5,000 per hour.
|
|
142
|
+
- **Jina rate limits** – Without an API key, you get 20 requests per minute. Provide `--jina-api-key` for 500 RPM.
|
|
143
|
+
- The CLI respects `.gitignore` by default. Use `--no-gitignore` to scan everything.
|
|
144
|
+
- **Binary files** (images, fonts, archives) are automatically skipped.
|
|
145
|
+
|
|
146
|
+
---
|
|
147
|
+
|
|
148
|
+
### License
|
|
149
|
+
|
|
150
|
+
MIT © [Holiday](https://github.com/thelastofinusa)
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,438 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
-
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
|
|
6
|
+
// src/actions/run.ts
|
|
7
|
+
import color2 from "picocolors";
|
|
8
|
+
import clipboard from "clipboardy";
|
|
9
|
+
import fs4 from "fs/promises";
|
|
10
|
+
import path5 from "path";
|
|
11
|
+
import inquirer2 from "inquirer";
|
|
12
|
+
import ora from "ora";
|
|
13
|
+
|
|
14
|
+
// package.json
|
|
15
|
+
var name = "packmd";
|
|
16
|
+
var description = "Convert GitHub repositories and webpages into AI-ready Markdown digests";
|
|
17
|
+
var version = "1.0.1";
|
|
18
|
+
|
|
19
|
+
// src/utils/version-check.ts
|
|
20
|
+
import fs from "fs/promises";
|
|
21
|
+
import os from "os";
|
|
22
|
+
import path from "path";
|
|
23
|
+
import color from "picocolors";
|
|
24
|
+
var REGISTRY_URL = `https://registry.npmjs.org/${name}/latest`;
|
|
25
|
+
var CACHE_PATH = path.join(os.homedir(), `.${name}`, "version-check.json");
|
|
26
|
+
var CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
27
|
+
var FETCH_TIMEOUT_MS = 1500;
|
|
28
|
+
function compareVersions(a, b) {
|
|
29
|
+
const pa = a.split(".").map(Number);
|
|
30
|
+
const pb = b.split(".").map(Number);
|
|
31
|
+
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
|
|
32
|
+
const na = pa[i] || 0;
|
|
33
|
+
const nb = pb[i] || 0;
|
|
34
|
+
if (na > nb) return 1;
|
|
35
|
+
if (na < nb) return -1;
|
|
36
|
+
}
|
|
37
|
+
return 0;
|
|
38
|
+
}
|
|
39
|
+
async function readCache() {
|
|
40
|
+
try {
|
|
41
|
+
return JSON.parse(await fs.readFile(CACHE_PATH, "utf-8"));
|
|
42
|
+
} catch {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
async function writeCache(data) {
|
|
47
|
+
try {
|
|
48
|
+
await fs.mkdir(path.dirname(CACHE_PATH), { recursive: true });
|
|
49
|
+
await fs.writeFile(CACHE_PATH, JSON.stringify(data), "utf-8");
|
|
50
|
+
} catch {
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
async function fetchLatestVersion() {
|
|
54
|
+
const controller = new AbortController();
|
|
55
|
+
const timeout = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
|
|
56
|
+
try {
|
|
57
|
+
const res = await fetch(REGISTRY_URL, {
|
|
58
|
+
signal: controller.signal,
|
|
59
|
+
headers: { Accept: "application/vnd.npm.install-v1+json" }
|
|
60
|
+
// lightweight abbreviated response
|
|
61
|
+
});
|
|
62
|
+
if (!res.ok) return null;
|
|
63
|
+
const data = await res.json();
|
|
64
|
+
return typeof data.version === "string" ? data.version : null;
|
|
65
|
+
} catch {
|
|
66
|
+
return null;
|
|
67
|
+
} finally {
|
|
68
|
+
clearTimeout(timeout);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
async function checkForUpdate(currentVersion) {
|
|
72
|
+
const cache = await readCache();
|
|
73
|
+
const isFresh = cache && Date.now() - cache.lastChecked < CACHE_TTL_MS;
|
|
74
|
+
const latestVersion = isFresh ? cache.latestVersion : await fetchLatestVersion();
|
|
75
|
+
if (!latestVersion) return null;
|
|
76
|
+
if (!isFresh) await writeCache({ lastChecked: Date.now(), latestVersion });
|
|
77
|
+
return compareVersions(latestVersion, currentVersion) > 0 ? latestVersion : null;
|
|
78
|
+
}
|
|
79
|
+
function printUpdateNotice(currentVersion, latestVersion) {
|
|
80
|
+
console.log();
|
|
81
|
+
console.log(
|
|
82
|
+
color.yellow(` \u2191 Update available: `) + color.dim(currentVersion) + color.yellow(" \u2192 ") + color.green(latestVersion)
|
|
83
|
+
);
|
|
84
|
+
console.log(
|
|
85
|
+
color.dim(` Run `) + color.cyan(`npm install -g ${name}@latest`) + color.dim(` to update.`)
|
|
86
|
+
);
|
|
87
|
+
console.log();
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// src/handlers/github.ts
|
|
91
|
+
import { fetchGithubRepo } from "@packmd/core";
|
|
92
|
+
async function handleGitHub(target, options, spinner) {
|
|
93
|
+
const result = await fetchGithubRepo(target, {
|
|
94
|
+
token: options.token,
|
|
95
|
+
maxFiles: Number(options.maxFiles),
|
|
96
|
+
maxFileSizeKB: Number(options.maxFileSize),
|
|
97
|
+
includeGlobs: options.include || [],
|
|
98
|
+
excludeGlobs: options.exclude || [],
|
|
99
|
+
onProgress: (msg) => {
|
|
100
|
+
spinner.text = msg;
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
return result.markdown;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// src/handlers/local.ts
|
|
107
|
+
import path4 from "path";
|
|
108
|
+
import { DEFAULT_IGNORES, buildDigestHeader } from "@packmd/core";
|
|
109
|
+
|
|
110
|
+
// src/utils/fs.ts
|
|
111
|
+
import fs2 from "fs/promises";
|
|
112
|
+
import path2 from "path";
|
|
113
|
+
import { minimatch } from "minimatch";
|
|
114
|
+
async function walkLocalDir(dir, base, excludeGlobs, includeGlobs, maxFileSizeKB, maxFiles, gitignore = null, collected = []) {
|
|
115
|
+
if (collected.length >= maxFiles) return collected;
|
|
116
|
+
const entries = await fs2.readdir(dir);
|
|
117
|
+
for (const name2 of entries) {
|
|
118
|
+
if (collected.length >= maxFiles) break;
|
|
119
|
+
const fullPath = path2.join(dir, name2);
|
|
120
|
+
const relativePath = path2.relative(base, fullPath).replace(/\\/g, "/");
|
|
121
|
+
const isExcluded = excludeGlobs.some(
|
|
122
|
+
(g) => minimatch(relativePath, g, { dot: true })
|
|
123
|
+
);
|
|
124
|
+
if (isExcluded) continue;
|
|
125
|
+
const stats = await fs2.stat(fullPath);
|
|
126
|
+
if (gitignore?.ignores(
|
|
127
|
+
stats.isDirectory() ? `${relativePath}/` : relativePath
|
|
128
|
+
)) {
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
if (stats.isDirectory()) {
|
|
132
|
+
await walkLocalDir(
|
|
133
|
+
fullPath,
|
|
134
|
+
base,
|
|
135
|
+
excludeGlobs,
|
|
136
|
+
includeGlobs,
|
|
137
|
+
maxFileSizeKB,
|
|
138
|
+
maxFiles,
|
|
139
|
+
gitignore,
|
|
140
|
+
collected
|
|
141
|
+
);
|
|
142
|
+
} else if (stats.isFile()) {
|
|
143
|
+
if (includeGlobs.length > 0) {
|
|
144
|
+
const isIncluded = includeGlobs.some(
|
|
145
|
+
(g) => minimatch(relativePath, g, { dot: true })
|
|
146
|
+
);
|
|
147
|
+
if (!isIncluded) continue;
|
|
148
|
+
}
|
|
149
|
+
const sizeKB = stats.size / 1024;
|
|
150
|
+
if (sizeKB <= maxFileSizeKB) {
|
|
151
|
+
const content = await fs2.readFile(fullPath, "utf-8");
|
|
152
|
+
collected.push({ path: relativePath, content });
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return collected;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// src/utils/gitignore.ts
|
|
160
|
+
import fs3 from "fs/promises";
|
|
161
|
+
import path3 from "path";
|
|
162
|
+
import ignore from "ignore";
|
|
163
|
+
async function loadGitignore(startDir) {
|
|
164
|
+
const ig = ignore();
|
|
165
|
+
let found = false;
|
|
166
|
+
let dir = startDir;
|
|
167
|
+
while (true) {
|
|
168
|
+
try {
|
|
169
|
+
const content = await fs3.readFile(path3.join(dir, ".gitignore"), "utf-8");
|
|
170
|
+
ig.add(content);
|
|
171
|
+
found = true;
|
|
172
|
+
} catch {
|
|
173
|
+
}
|
|
174
|
+
try {
|
|
175
|
+
await fs3.access(path3.join(dir, ".git"));
|
|
176
|
+
break;
|
|
177
|
+
} catch {
|
|
178
|
+
}
|
|
179
|
+
const parent = path3.dirname(dir);
|
|
180
|
+
if (parent === dir) break;
|
|
181
|
+
dir = parent;
|
|
182
|
+
}
|
|
183
|
+
return found ? ig : null;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// src/handlers/local.ts
|
|
187
|
+
async function handleLocalDir(target, options, spinner) {
|
|
188
|
+
const absolutePath = path4.resolve(process.cwd(), target);
|
|
189
|
+
const excludeGlobs = options.exclude || DEFAULT_IGNORES;
|
|
190
|
+
const includeGlobs = options.include || [];
|
|
191
|
+
const maxFiles = Number(options.maxFiles);
|
|
192
|
+
const maxFileSizeKB = Number(options.maxFileSize);
|
|
193
|
+
const gitignore = options.gitignore === false ? null : await loadGitignore(absolutePath);
|
|
194
|
+
const files = await walkLocalDir(
|
|
195
|
+
absolutePath,
|
|
196
|
+
absolutePath,
|
|
197
|
+
excludeGlobs,
|
|
198
|
+
includeGlobs,
|
|
199
|
+
maxFileSizeKB,
|
|
200
|
+
maxFiles,
|
|
201
|
+
gitignore
|
|
202
|
+
);
|
|
203
|
+
spinner.text = `Processing ${files.length} local files..`;
|
|
204
|
+
const totalChars = files.reduce((sum, f) => sum + f.content.length, 0);
|
|
205
|
+
const estTokens = Math.round(totalChars / 4);
|
|
206
|
+
const header = buildDigestHeader({
|
|
207
|
+
title: `Local Digest \u2014 \`${path4.basename(absolutePath)}\``,
|
|
208
|
+
meta: {
|
|
209
|
+
Path: `\`${absolutePath}\``,
|
|
210
|
+
Date: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10),
|
|
211
|
+
Files: files.length,
|
|
212
|
+
"Est. tokens": `~${estTokens.toLocaleString()}`
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
const markdownParts = [header];
|
|
216
|
+
for (const file of files) {
|
|
217
|
+
markdownParts.push(`## File: ${file.path}
|
|
218
|
+
\`\`\`
|
|
219
|
+
${file.content}
|
|
220
|
+
\`\`\``);
|
|
221
|
+
}
|
|
222
|
+
return markdownParts.join("\n\n");
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// src/handlers/webpage.ts
|
|
226
|
+
import { buildDigestHeader as buildDigestHeader2, scrapeWebPage } from "@packmd/core";
|
|
227
|
+
async function handleWebpage(target, options) {
|
|
228
|
+
const scraped = await scrapeWebPage(target, {
|
|
229
|
+
jinaApiKey: options.jinaApiKey
|
|
230
|
+
});
|
|
231
|
+
const estTokens = Math.round((scraped.content?.length || 0) / 4);
|
|
232
|
+
const header = buildDigestHeader2({
|
|
233
|
+
icon: "\u{1F310}",
|
|
234
|
+
title: scraped.title || target,
|
|
235
|
+
meta: {
|
|
236
|
+
Source: `\`${target}\``,
|
|
237
|
+
Date: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10),
|
|
238
|
+
"Est. tokens": `~${estTokens.toLocaleString()}`
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
return `${header}
|
|
242
|
+
|
|
243
|
+
${scraped.content || ""}`;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// src/prompts/github.ts
|
|
247
|
+
import inquirer from "inquirer";
|
|
248
|
+
async function promptGithubOptions(currentOptions) {
|
|
249
|
+
const { customize } = await inquirer.prompt([
|
|
250
|
+
{
|
|
251
|
+
type: "confirm",
|
|
252
|
+
name: "customize",
|
|
253
|
+
message: "GitHub URL detected. Would you like to configure options?",
|
|
254
|
+
default: false
|
|
255
|
+
}
|
|
256
|
+
]);
|
|
257
|
+
if (!customize) return currentOptions;
|
|
258
|
+
const answers = await inquirer.prompt([
|
|
259
|
+
{
|
|
260
|
+
type: "input",
|
|
261
|
+
name: "maxFiles",
|
|
262
|
+
message: "Maximum number of files to include:",
|
|
263
|
+
default: currentOptions.maxFiles || "200",
|
|
264
|
+
validate: (val) => !isNaN(Number(val)) || "Please enter a valid number"
|
|
265
|
+
},
|
|
266
|
+
{
|
|
267
|
+
type: "input",
|
|
268
|
+
name: "maxFileSize",
|
|
269
|
+
message: "Maximum file size threshold (in KB):",
|
|
270
|
+
default: currentOptions.maxFileSize || "100",
|
|
271
|
+
validate: (val) => !isNaN(Number(val)) || "Please enter a valid number"
|
|
272
|
+
},
|
|
273
|
+
{
|
|
274
|
+
type: "password",
|
|
275
|
+
name: "token",
|
|
276
|
+
message: "GitHub Token (Optional - for private or larger repos):",
|
|
277
|
+
default: currentOptions.token || ""
|
|
278
|
+
}
|
|
279
|
+
]);
|
|
280
|
+
return { ...currentOptions, ...answers };
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// src/actions/run.ts
|
|
284
|
+
async function runAction(target, options) {
|
|
285
|
+
console.log("");
|
|
286
|
+
console.log(color2.cyan(`${name} \u2014 AI Markdown Packager`));
|
|
287
|
+
const updateCheck = checkForUpdate(version).catch(() => null);
|
|
288
|
+
let finalOptions = { ...options };
|
|
289
|
+
try {
|
|
290
|
+
let digest = "";
|
|
291
|
+
const spinner = ora();
|
|
292
|
+
if (target.startsWith("http://") || target.startsWith("https://")) {
|
|
293
|
+
const targetUrl = new URL(target);
|
|
294
|
+
const isGitHub = targetUrl.hostname.includes("github.com");
|
|
295
|
+
if (isGitHub) {
|
|
296
|
+
finalOptions = await promptGithubOptions(finalOptions);
|
|
297
|
+
spinner.start("Fetching repository. Please wait..");
|
|
298
|
+
digest = await handleGitHub(target, finalOptions, spinner);
|
|
299
|
+
} else {
|
|
300
|
+
spinner.start("Scraping webpage. Please wait..");
|
|
301
|
+
digest = await handleWebpage(target, finalOptions);
|
|
302
|
+
}
|
|
303
|
+
} else {
|
|
304
|
+
spinner.start("Scanning local directory. Please wait..");
|
|
305
|
+
digest = await handleLocalDir(target, finalOptions, spinner);
|
|
306
|
+
}
|
|
307
|
+
let outputPath = finalOptions.copy ? null : finalOptions.output || "pack.md";
|
|
308
|
+
if (outputPath) {
|
|
309
|
+
let finalPath = outputPath;
|
|
310
|
+
while (true) {
|
|
311
|
+
let fileExists = false;
|
|
312
|
+
try {
|
|
313
|
+
await fs4.access(finalPath);
|
|
314
|
+
fileExists = true;
|
|
315
|
+
} catch {
|
|
316
|
+
fileExists = false;
|
|
317
|
+
}
|
|
318
|
+
if (fileExists) {
|
|
319
|
+
const { overwrite } = await inquirer2.prompt([
|
|
320
|
+
{
|
|
321
|
+
type: "confirm",
|
|
322
|
+
name: "overwrite",
|
|
323
|
+
message: `File ${color2.cyan(finalPath)} already exists. Overwrite?`,
|
|
324
|
+
default: false
|
|
325
|
+
}
|
|
326
|
+
]);
|
|
327
|
+
if (overwrite) {
|
|
328
|
+
break;
|
|
329
|
+
} else {
|
|
330
|
+
const { newFileName } = await inquirer2.prompt([
|
|
331
|
+
{
|
|
332
|
+
type: "input",
|
|
333
|
+
name: "newFileName",
|
|
334
|
+
message: "Please enter a new file name:",
|
|
335
|
+
default: "pack-new.md",
|
|
336
|
+
validate: (val) => val.trim().length > 0 || "File name cannot be empty."
|
|
337
|
+
}
|
|
338
|
+
]);
|
|
339
|
+
finalPath = newFileName;
|
|
340
|
+
}
|
|
341
|
+
} else {
|
|
342
|
+
break;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
await fs4.writeFile(finalPath, digest, { encoding: "utf-8", flag: "w" });
|
|
346
|
+
const gitignorePath = path5.join(process.cwd(), ".gitignore");
|
|
347
|
+
let gitignoreContent = "";
|
|
348
|
+
try {
|
|
349
|
+
gitignoreContent = await fs4.readFile(gitignorePath, "utf-8");
|
|
350
|
+
} catch {
|
|
351
|
+
}
|
|
352
|
+
const isIgnored = gitignoreContent.split("\n").map((line) => line.trim()).includes(finalPath);
|
|
353
|
+
if (!isIgnored) {
|
|
354
|
+
const prefix = gitignoreContent.endsWith("\n") || gitignoreContent === "" ? "" : "\n";
|
|
355
|
+
await fs4.appendFile(gitignorePath, `${prefix}${finalPath}
|
|
356
|
+
`, "utf-8");
|
|
357
|
+
spinner.succeed(
|
|
358
|
+
`Generated successfully! ${color2.cyan(finalPath)} was added to .gitignore.`
|
|
359
|
+
);
|
|
360
|
+
} else {
|
|
361
|
+
spinner.succeed("Markdown generated successfully!");
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
if (finalOptions.copy) {
|
|
365
|
+
await clipboard.write(digest);
|
|
366
|
+
spinner.succeed("Markdown compiled successfully.");
|
|
367
|
+
}
|
|
368
|
+
const latestVersion = await updateCheck;
|
|
369
|
+
if (latestVersion) printUpdateNotice(version, latestVersion);
|
|
370
|
+
console.log("");
|
|
371
|
+
} catch (error) {
|
|
372
|
+
console.error(
|
|
373
|
+
color2.red(`
|
|
374
|
+
\u2716 ${error.message || "An unexpected error occurred"}`)
|
|
375
|
+
);
|
|
376
|
+
process.exit(1);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// src/utils/program.ts
|
|
381
|
+
var cliOptions = [
|
|
382
|
+
{
|
|
383
|
+
flags: "-o, --output <path>",
|
|
384
|
+
description: "Specify a custom file path to save the generated Markdown digest"
|
|
385
|
+
},
|
|
386
|
+
{
|
|
387
|
+
flags: "-t, --token <token>",
|
|
388
|
+
description: "Provide a GitHub Personal Access Token for private repositories or extended rate limits"
|
|
389
|
+
},
|
|
390
|
+
{
|
|
391
|
+
flags: "-m, --max-files <number>",
|
|
392
|
+
description: "Limit the total number of files to include in the final digest",
|
|
393
|
+
defaultValue: "200"
|
|
394
|
+
},
|
|
395
|
+
{
|
|
396
|
+
flags: "-s, --max-file-size <number>",
|
|
397
|
+
description: "Set the maximum allowed size (in KB) for individual files",
|
|
398
|
+
defaultValue: "100"
|
|
399
|
+
},
|
|
400
|
+
{
|
|
401
|
+
flags: "-i, --include <patterns...>",
|
|
402
|
+
description: "Specify glob patterns to explicitly include certain files or directories"
|
|
403
|
+
},
|
|
404
|
+
{
|
|
405
|
+
flags: "-e, --exclude <patterns...>",
|
|
406
|
+
description: "Specify glob patterns to explicitly ignore certain files or directories"
|
|
407
|
+
},
|
|
408
|
+
{
|
|
409
|
+
flags: "-c, --copy",
|
|
410
|
+
description: "Automatically copy the generated Markdown digest to your clipboard",
|
|
411
|
+
defaultValue: false
|
|
412
|
+
},
|
|
413
|
+
{
|
|
414
|
+
flags: "--jina-api-key <key>",
|
|
415
|
+
description: "Provide a Jina API key for enhanced webpage scraping and parsing"
|
|
416
|
+
},
|
|
417
|
+
{
|
|
418
|
+
flags: "--no-gitignore",
|
|
419
|
+
description: "Don't respect .gitignore rules when scanning a local directory"
|
|
420
|
+
}
|
|
421
|
+
];
|
|
422
|
+
|
|
423
|
+
// src/index.ts
|
|
424
|
+
var program = new Command();
|
|
425
|
+
program.name(name).description(description).version(version, "-v, --version", "Output the version number").argument(
|
|
426
|
+
"[target]",
|
|
427
|
+
"GitHub URL, Webpage URL, or local directory path (defaults to current directory)",
|
|
428
|
+
"."
|
|
429
|
+
);
|
|
430
|
+
cliOptions.forEach(({ flags, description: description2, defaultValue }) => {
|
|
431
|
+
if (defaultValue !== void 0) {
|
|
432
|
+
program.option(flags, description2, defaultValue);
|
|
433
|
+
} else {
|
|
434
|
+
program.option(flags, description2);
|
|
435
|
+
}
|
|
436
|
+
});
|
|
437
|
+
program.action(runAction);
|
|
438
|
+
program.parse();
|
package/package.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "packmd",
|
|
3
|
-
"
|
|
3
|
+
"description": "Convert GitHub repositories and webpages into AI-ready Markdown digests",
|
|
4
|
+
"version": "1.0.1",
|
|
4
5
|
"license": "MIT",
|
|
5
6
|
"type": "module",
|
|
6
7
|
"bin": {
|
|
@@ -13,13 +14,15 @@
|
|
|
13
14
|
],
|
|
14
15
|
"scripts": {
|
|
15
16
|
"build": "tsup",
|
|
16
|
-
"
|
|
17
|
-
"start": "node dist/index.js",
|
|
17
|
+
"start": "tsx src/index.js",
|
|
18
|
+
"start:node": "node dist/index.js",
|
|
19
|
+
"clean": "rm -rf dist node_modules .turbo",
|
|
18
20
|
"prepublishOnly": "bun run build",
|
|
19
21
|
"pushout": "npm publish",
|
|
20
22
|
"pushout-inc": "npm version patch --no-git-tag-version && bun run pushout"
|
|
21
23
|
},
|
|
22
24
|
"devDependencies": {
|
|
25
|
+
"@types/inquirer": "^9.0.10",
|
|
23
26
|
"@types/node": "^26.1.1",
|
|
24
27
|
"tsup": "^8.5.1",
|
|
25
28
|
"tsx": "^4.23.1",
|
|
@@ -27,7 +30,37 @@
|
|
|
27
30
|
},
|
|
28
31
|
"dependencies": {
|
|
29
32
|
"chalk": "^5.6.2",
|
|
33
|
+
"clipboardy": "^5.3.2",
|
|
30
34
|
"commander": "^15.0.0",
|
|
31
|
-
"
|
|
32
|
-
|
|
35
|
+
"ignore": "^7.0.6",
|
|
36
|
+
"inquirer": "^14.0.2",
|
|
37
|
+
"minimatch": "^10.2.5",
|
|
38
|
+
"ora": "^9.4.1",
|
|
39
|
+
"@packmd/core": "^1.0.0",
|
|
40
|
+
"picocolors": "^1.1.1"
|
|
41
|
+
},
|
|
42
|
+
"keywords": [
|
|
43
|
+
"packmd",
|
|
44
|
+
"markdown",
|
|
45
|
+
"github",
|
|
46
|
+
"llm",
|
|
47
|
+
"codebase",
|
|
48
|
+
"digest",
|
|
49
|
+
"cli",
|
|
50
|
+
"generator",
|
|
51
|
+
"scraper",
|
|
52
|
+
"developer-tools",
|
|
53
|
+
"chatgpt",
|
|
54
|
+
"claude"
|
|
55
|
+
],
|
|
56
|
+
"author": "Holiday (https://github.com/thelastofinusa)",
|
|
57
|
+
"repository": {
|
|
58
|
+
"type": "git",
|
|
59
|
+
"url": "git+https://github.com/thelastofinusa/packmd.git",
|
|
60
|
+
"directory": "packages/cli"
|
|
61
|
+
},
|
|
62
|
+
"bugs": {
|
|
63
|
+
"url": "https://github.com/thelastofinusa/packmd/issues"
|
|
64
|
+
},
|
|
65
|
+
"homepage": "https://packmd.vercel.app/docs"
|
|
33
66
|
}
|