web-snapshot-cli 0.2.0__tar.gz → 1.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. web_snapshot_cli-1.1.0/.github/workflows/pages.yml +33 -0
  2. {web_snapshot_cli-0.2.0 → web_snapshot_cli-1.1.0}/PKG-INFO +17 -2
  3. {web_snapshot_cli-0.2.0 → web_snapshot_cli-1.1.0}/README.md +15 -0
  4. web_snapshot_cli-1.1.0/docs/CNAME +1 -0
  5. web_snapshot_cli-1.1.0/docs/app.js +221 -0
  6. web_snapshot_cli-1.1.0/docs/index.html +200 -0
  7. web_snapshot_cli-1.1.0/docs/style.css +289 -0
  8. {web_snapshot_cli-0.2.0 → web_snapshot_cli-1.1.0}/pyproject.toml +6 -2
  9. {web_snapshot_cli-0.2.0 → web_snapshot_cli-1.1.0}/src/snapshot/__init__.py +1 -1
  10. {web_snapshot_cli-0.2.0 → web_snapshot_cli-1.1.0}/src/snapshot/cli.py +41 -2
  11. {web_snapshot_cli-0.2.0 → web_snapshot_cli-1.1.0}/src/snapshot/downloader.py +73 -1
  12. {web_snapshot_cli-0.2.0 → web_snapshot_cli-1.1.0}/src/snapshot/utils.py +50 -0
  13. web_snapshot_cli-1.1.0/src/snapshot/wordlist.py +172 -0
  14. web_snapshot_cli-1.1.0/src/snapshot/wordlists/__init__.py +0 -0
  15. web_snapshot_cli-1.1.0/src/snapshot/wordlists/common.txt +132 -0
  16. web_snapshot_cli-1.1.0/src/snapshot/wordlists/large.txt +18637 -0
  17. {web_snapshot_cli-0.2.0 → web_snapshot_cli-1.1.0}/tests/test_crawler.py +27 -0
  18. web_snapshot_cli-1.1.0/tests/test_utils.py +56 -0
  19. web_snapshot_cli-1.1.0/tests/test_wordlist.py +69 -0
  20. web_snapshot_cli-0.2.0/tests/test_utils.py +0 -26
  21. {web_snapshot_cli-0.2.0 → web_snapshot_cli-1.1.0}/.github/workflows/ci.yml +0 -0
  22. {web_snapshot_cli-0.2.0 → web_snapshot_cli-1.1.0}/.github/workflows/publish.yml +0 -0
  23. {web_snapshot_cli-0.2.0 → web_snapshot_cli-1.1.0}/.gitignore +0 -0
  24. {web_snapshot_cli-0.2.0 → web_snapshot_cli-1.1.0}/LICENSE +0 -0
  25. {web_snapshot_cli-0.2.0 → web_snapshot_cli-1.1.0}/src/snapshot/__main__.py +0 -0
  26. {web_snapshot_cli-0.2.0 → web_snapshot_cli-1.1.0}/src/snapshot/crawl_policy.py +0 -0
  27. {web_snapshot_cli-0.2.0 → web_snapshot_cli-1.1.0}/src/snapshot/manifest.py +0 -0
  28. {web_snapshot_cli-0.2.0 → web_snapshot_cli-1.1.0}/src/snapshot/restore.py +0 -0
  29. {web_snapshot_cli-0.2.0 → web_snapshot_cli-1.1.0}/tests/test_crawl_policy.py +0 -0
  30. {web_snapshot_cli-0.2.0 → web_snapshot_cli-1.1.0}/tests/test_features.py +0 -0
@@ -0,0 +1,33 @@
1
+ name: Deploy GitHub Pages
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ paths:
7
+ - "docs/**"
8
+ - ".github/workflows/pages.yml"
9
+ workflow_dispatch:
10
+
11
+ permissions:
12
+ contents: read
13
+ pages: write
14
+ id-token: write
15
+
16
+ concurrency:
17
+ group: pages
18
+ cancel-in-progress: false
19
+
20
+ jobs:
21
+ deploy:
22
+ environment:
23
+ name: github-pages
24
+ url: ${{ steps.deployment.outputs.page_url }}
25
+ runs-on: ubuntu-latest
26
+ steps:
27
+ - uses: actions/checkout@v4
28
+ - uses: actions/configure-pages@v5
29
+ - uses: actions/upload-pages-artifact@v3
30
+ with:
31
+ path: docs
32
+ - uses: actions/deploy-pages@v4
33
+ id: deployment
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: web-snapshot-cli
3
- Version: 0.2.0
3
+ Version: 1.1.0
4
4
  Summary: Take offline snapshots of any website and restore them locally
5
5
  Project-URL: Homepage, https://github.com/codingsushi79/Snapshot
6
6
  Project-URL: Repository, https://github.com/codingsushi79/Snapshot
@@ -8,7 +8,7 @@ Author: snapshot contributors
8
8
  License-Expression: MIT
9
9
  License-File: LICENSE
10
10
  Keywords: cli,crawler,offline,snapshot,web-archive
11
- Classifier: Development Status :: 4 - Beta
11
+ Classifier: Development Status :: 5 - Production/Stable
12
12
  Classifier: Environment :: Console
13
13
  Classifier: Intended Audience :: Developers
14
14
  Classifier: License :: OSI Approved :: MIT License
@@ -45,6 +45,8 @@ Then run:
45
45
  snapshot https://example.com ./mirror
46
46
  ```
47
47
 
48
+ **Command generator:** [snapshot.sushii.dev](https://snapshot.sushii.dev/)
49
+
48
50
  ## Usage
49
51
 
50
52
  ### Snapshot a single page
@@ -85,6 +87,16 @@ snapshot --cookie session=abc123 --header "Authorization: Bearer TOKEN" \
85
87
  snapshot --crawl --sitemap https://example.com ./mirror --max-pages 500
86
88
  ```
87
89
 
90
+ ### Gobuster-style path discovery
91
+
92
+ Brute-force common paths (including `robots.txt`, `sitemap.xml`, admin panels, etc.) using bundled wordlists (`common` + ~18k `large` entries). Add your own SecLists/gobuster wordlists with `-w`:
93
+
94
+ ```bash
95
+ snapshot --crawl --gobuster https://example.com ./mirror
96
+ snapshot --crawl -w common -w large -w /path/to/SecLists/Discovery/Web-Content/common.txt URL ./out
97
+ snapshot --crawl --gobuster --wordlist-ext html,php,asp URL ./out
98
+ ```
99
+
88
100
  ### Resume an interrupted snapshot
89
101
 
90
102
  ```bash
@@ -137,6 +149,9 @@ snapshot -restore ./mirror --port 3000 --no-open
137
149
  | `--robots` / `--no-robots` | Respect robots.txt (default: on) |
138
150
  | `--crawl-delay` | Seconds to wait after each request (default: 0) |
139
151
  | `--sitemap` | Seed crawl from sitemap.xml |
152
+ | `--gobuster`, `-g` | Discover paths with bundled wordlists |
153
+ | `--wordlist`, `-w` | Custom or builtin wordlist (`common`, `large`) |
154
+ | `--wordlist-ext` | Extensions to append per word (`html,php`) |
140
155
  | `--resume` | Skip pages/assets already saved |
141
156
  | `--verbose`, `-v` | Detailed log output |
142
157
  | `--dry-run` | Fetch without writing files |
@@ -16,6 +16,8 @@ Then run:
16
16
  snapshot https://example.com ./mirror
17
17
  ```
18
18
 
19
+ **Command generator:** [snapshot.sushii.dev](https://snapshot.sushii.dev/)
20
+
19
21
  ## Usage
20
22
 
21
23
  ### Snapshot a single page
@@ -56,6 +58,16 @@ snapshot --cookie session=abc123 --header "Authorization: Bearer TOKEN" \
56
58
  snapshot --crawl --sitemap https://example.com ./mirror --max-pages 500
57
59
  ```
58
60
 
61
+ ### Gobuster-style path discovery
62
+
63
+ Brute-force common paths (including `robots.txt`, `sitemap.xml`, admin panels, etc.) using bundled wordlists (`common` + ~18k `large` entries). Add your own SecLists/gobuster wordlists with `-w`:
64
+
65
+ ```bash
66
+ snapshot --crawl --gobuster https://example.com ./mirror
67
+ snapshot --crawl -w common -w large -w /path/to/SecLists/Discovery/Web-Content/common.txt URL ./out
68
+ snapshot --crawl --gobuster --wordlist-ext html,php,asp URL ./out
69
+ ```
70
+
59
71
  ### Resume an interrupted snapshot
60
72
 
61
73
  ```bash
@@ -108,6 +120,9 @@ snapshot -restore ./mirror --port 3000 --no-open
108
120
  | `--robots` / `--no-robots` | Respect robots.txt (default: on) |
109
121
  | `--crawl-delay` | Seconds to wait after each request (default: 0) |
110
122
  | `--sitemap` | Seed crawl from sitemap.xml |
123
+ | `--gobuster`, `-g` | Discover paths with bundled wordlists |
124
+ | `--wordlist`, `-w` | Custom or builtin wordlist (`common`, `large`) |
125
+ | `--wordlist-ext` | Extensions to append per word (`html,php`) |
111
126
  | `--resume` | Skip pages/assets already saved |
112
127
  | `--verbose`, `-v` | Detailed log output |
113
128
  | `--dry-run` | Fetch without writing files |
@@ -0,0 +1 @@
1
+ snapshot.sushii.dev
@@ -0,0 +1,221 @@
1
+ const $ = (sel) => document.querySelector(sel);
2
+ const $$ = (sel) => document.querySelectorAll(sel);
3
+
4
+ const PRESETS = {
5
+ quick: {
6
+ crawl: false,
7
+ gobuster: false,
8
+ sitemap: false,
9
+ maxPages: 50,
10
+ depth: 3,
11
+ crawlDelay: 0,
12
+ },
13
+ crawl: {
14
+ crawl: true,
15
+ gobuster: false,
16
+ sitemap: true,
17
+ maxPages: 200,
18
+ depth: 5,
19
+ crawlDelay: 0,
20
+ resume: true,
21
+ verbose: true,
22
+ },
23
+ full: {
24
+ crawl: true,
25
+ gobuster: true,
26
+ sitemap: true,
27
+ maxPages: 5000,
28
+ depth: 10,
29
+ crawlDelay: 0.5,
30
+ wordlistExt: "html,php,asp,aspx",
31
+ resume: true,
32
+ verbose: true,
33
+ },
34
+ };
35
+
36
+ function shellQuote(value) {
37
+ if (!value) return '""';
38
+ if (/^[A-Za-z0-9_./~-]+$/.test(value)) return value;
39
+ return `"${value.replace(/"/g, '\\"')}"`;
40
+ }
41
+
42
+ function linesFromTextarea(id) {
43
+ const el = $(id);
44
+ if (!el || !el.value.trim()) return [];
45
+ return el.value
46
+ .split("\n")
47
+ .map((l) => l.trim())
48
+ .filter(Boolean);
49
+ }
50
+
51
+ function isRestoreMode() {
52
+ return document.querySelector(".mode-tab.active")?.dataset.mode === "restore";
53
+ }
54
+
55
+ function buildCommand() {
56
+ if (isRestoreMode()) {
57
+ return buildRestoreCommand();
58
+ }
59
+ return buildSnapshotCommand();
60
+ }
61
+
62
+ function buildRestoreCommand() {
63
+ const parts = ["snapshot", "-restore"];
64
+ const dir = $("#outputDir").value.trim() || "./mirror";
65
+ const port = $("#port").value;
66
+ const host = $("#host").value.trim();
67
+ const noOpen = $("#noOpen").checked;
68
+
69
+ parts.push(shellQuote(dir));
70
+ if (port && port !== "8080") parts.push("--port", port);
71
+ if (host && host !== "127.0.0.1") parts.push("--host", shellQuote(host));
72
+ if (noOpen) parts.push("--no-open");
73
+
74
+ return parts.join(" ");
75
+ }
76
+
77
+ function buildSnapshotCommand() {
78
+ const parts = ["snapshot"];
79
+ const flags = [];
80
+
81
+ if ($("#crawl").checked) flags.push("--crawl");
82
+ if ($("#gobuster").checked) flags.push("--gobuster");
83
+ if ($("#sitemap").checked) flags.push("--sitemap");
84
+ if ($("#resume").checked) flags.push("--resume");
85
+ if ($("#verbose").checked) flags.push("--verbose");
86
+ if ($("#dryRun").checked) flags.push("--dry-run");
87
+ if ($("#noAssets").checked) flags.push("--no-assets");
88
+ if (!$("#robots").checked) flags.push("--no-robots");
89
+ if (!$("#sameOrigin").checked) flags.push("--no-same-origin");
90
+
91
+ const lang = $("#lang").value;
92
+ if (lang && lang !== "html") flags.push("--lang", lang);
93
+
94
+ const maxPages = $("#maxPages").value;
95
+ if (maxPages && maxPages !== "50") flags.push("--max-pages", maxPages);
96
+
97
+ const depth = $("#depth").value;
98
+ if (depth && depth !== "3") flags.push("--depth", depth);
99
+
100
+ const timeout = $("#timeout").value;
101
+ if (timeout && timeout !== "15") flags.push("--timeout", timeout);
102
+
103
+ const concurrency = $("#concurrency").value;
104
+ if (concurrency && concurrency !== "16") flags.push("--concurrency", concurrency);
105
+
106
+ const crawlDelay = $("#crawlDelay").value;
107
+ if (crawlDelay && crawlDelay !== "0" && crawlDelay !== "0.0") {
108
+ flags.push("--crawl-delay", crawlDelay);
109
+ }
110
+
111
+ const userAgent = $("#userAgent").value.trim();
112
+ if (userAgent) flags.push("--user-agent", shellQuote(userAgent));
113
+
114
+ const wordlistExt = $("#wordlistExt").value.trim();
115
+ if (wordlistExt) flags.push("--wordlist-ext", shellQuote(wordlistExt));
116
+
117
+ for (const cookie of linesFromTextarea("#cookies")) {
118
+ flags.push("--cookie", shellQuote(cookie));
119
+ }
120
+ for (const header of linesFromTextarea("#headers")) {
121
+ flags.push("--header", shellQuote(header));
122
+ }
123
+ for (const inc of linesFromTextarea("#includes")) {
124
+ flags.push("--include", shellQuote(inc));
125
+ }
126
+ for (const exc of linesFromTextarea("#excludes")) {
127
+ flags.push("--exclude", shellQuote(exc));
128
+ }
129
+ for (const wl of linesFromTextarea("#wordlists")) {
130
+ flags.push("--wordlist", shellQuote(wl));
131
+ }
132
+
133
+ parts.push(...flags);
134
+
135
+ let url = $("#url").value.trim();
136
+ if (url && !/^https?:\/\//i.test(url)) url = `https://${url}`;
137
+ const out = $("#outputDir").value.trim() || "./mirror";
138
+
139
+ parts.push(shellQuote(url || "https://example.com"));
140
+ parts.push(shellQuote(out));
141
+
142
+ return parts.join(" ");
143
+ }
144
+
145
+ function updateCommand() {
146
+ const cmd = buildCommand();
147
+ $("#command").textContent = cmd;
148
+
149
+ const restoreHint = $("#restoreHint");
150
+ if (isRestoreMode()) {
151
+ restoreHint.classList.add("hidden");
152
+ } else {
153
+ restoreHint.classList.remove("hidden");
154
+ const dir = $("#outputDir").value.trim() || "./mirror";
155
+ $("#restoreCommand").textContent = `snapshot -restore ${shellQuote(dir)}`;
156
+ }
157
+ }
158
+
159
+ function applyPreset(name) {
160
+ const p = PRESETS[name];
161
+ if (!p) return;
162
+
163
+ $("#crawl").checked = !!p.crawl;
164
+ $("#gobuster").checked = !!p.gobuster;
165
+ $("#sitemap").checked = !!p.sitemap;
166
+ $("#resume").checked = !!p.resume;
167
+ $("#verbose").checked = !!p.verbose;
168
+ if (p.maxPages != null) $("#maxPages").value = p.maxPages;
169
+ if (p.depth != null) $("#depth").value = p.depth;
170
+ if (p.crawlDelay != null) $("#crawlDelay").value = p.crawlDelay;
171
+ if (p.wordlistExt != null) $("#wordlistExt").value = p.wordlistExt;
172
+
173
+ setMode("snapshot");
174
+ updateCommand();
175
+ }
176
+
177
+ function setMode(mode) {
178
+ $$(".mode-tab").forEach((tab) => {
179
+ tab.classList.toggle("active", tab.dataset.mode === mode);
180
+ });
181
+
182
+ const snapshotOnly = $$(".snapshot-only");
183
+ const restoreOnly = $$(".restore-only");
184
+
185
+ snapshotOnly.forEach((el) => el.classList.toggle("hidden", mode === "restore"));
186
+ restoreOnly.forEach((el) => el.classList.toggle("hidden", mode !== "restore"));
187
+
188
+ updateCommand();
189
+ }
190
+
191
+ async function copyCommand() {
192
+ const text = $("#command").textContent;
193
+ await navigator.clipboard.writeText(text);
194
+ const btn = $("#copyBtn");
195
+ btn.textContent = "Copied!";
196
+ btn.classList.add("copied");
197
+ setTimeout(() => {
198
+ btn.textContent = "Copy";
199
+ btn.classList.remove("copied");
200
+ }, 1500);
201
+ }
202
+
203
+ function init() {
204
+ $$(".mode-tab").forEach((tab) => {
205
+ tab.addEventListener("click", () => setMode(tab.dataset.mode));
206
+ });
207
+
208
+ $$(".preset-btn").forEach((btn) => {
209
+ btn.addEventListener("click", () => applyPreset(btn.dataset.preset));
210
+ });
211
+
212
+ $("#copyBtn").addEventListener("click", copyCommand);
213
+
214
+ document.addEventListener("input", updateCommand);
215
+ document.addEventListener("change", updateCommand);
216
+
217
+ setMode("snapshot");
218
+ updateCommand();
219
+ }
220
+
221
+ document.addEventListener("DOMContentLoaded", init);
@@ -0,0 +1,200 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>snapshot — command generator</title>
7
+ <meta name="description" content="Build snapshot CLI commands for offline website mirrors." />
8
+ <link rel="stylesheet" href="style.css" />
9
+ </head>
10
+ <body>
11
+ <header>
12
+ <h1>snapshot command generator</h1>
13
+ <p>
14
+ Build install and run commands for
15
+ <a href="https://github.com/codingsushi79/Snapshot">web-snapshot-cli</a>
16
+ </p>
17
+ </header>
18
+
19
+ <div class="layout">
20
+ <div class="panel">
21
+ <div class="mode-tabs">
22
+ <button type="button" class="mode-tab active" data-mode="snapshot">Snapshot</button>
23
+ <button type="button" class="mode-tab" data-mode="restore">Restore</button>
24
+ </div>
25
+
26
+ <div class="presets snapshot-only">
27
+ <button type="button" class="preset-btn" data-preset="quick">Single page</button>
28
+ <button type="button" class="preset-btn" data-preset="crawl">Crawl</button>
29
+ <button type="button" class="preset-btn" data-preset="full">Full + gobuster</button>
30
+ </div>
31
+
32
+ <h2>Target</h2>
33
+ <div class="field snapshot-only">
34
+ <label for="url">Website URL</label>
35
+ <input type="url" id="url" placeholder="https://example.com" value="https://example.com" />
36
+ </div>
37
+ <div class="field">
38
+ <label for="outputDir">Output directory</label>
39
+ <input type="text" id="outputDir" placeholder="./mirror or ~/sites/example" value="./mirror" />
40
+ <div class="hint">Where files are saved (snapshot) or served from (restore)</div>
41
+ </div>
42
+
43
+ <hr class="section-divider snapshot-only" />
44
+
45
+ <h2 class="snapshot-only">Discovery</h2>
46
+ <div class="check-grid snapshot-only">
47
+ <label class="check-item">
48
+ <input type="checkbox" id="crawl" />
49
+ <div>--crawl<span>Follow same-origin links</span></div>
50
+ </label>
51
+ <label class="check-item">
52
+ <input type="checkbox" id="gobuster" />
53
+ <div>--gobuster<span>Bundled wordlists; soft 404 pages are filtered out</span></div>
54
+ </label>
55
+ <label class="check-item">
56
+ <input type="checkbox" id="sitemap" />
57
+ <div>--sitemap<span>Seed from sitemap.xml / robots.txt</span></div>
58
+ </label>
59
+ <label class="check-item">
60
+ <input type="checkbox" id="resume" />
61
+ <div>--resume<span>Skip already-downloaded files</span></div>
62
+ </label>
63
+ </div>
64
+
65
+ <div class="field snapshot-only">
66
+ <label for="wordlists">Extra wordlists (one per line)</label>
67
+ <textarea id="wordlists" placeholder="common&#10;large&#10;/path/to/SecLists/Discovery/Web-Content/common.txt"></textarea>
68
+ <div class="hint">Builtin names: <code>common</code>, <code>large</code>. --gobuster uses both by default. Pages that return 200 but show 404/not-found content are skipped.</div>
69
+ </div>
70
+ <div class="field snapshot-only">
71
+ <label for="wordlistExt">Wordlist extensions</label>
72
+ <input type="text" id="wordlistExt" placeholder="html,php,asp,aspx" />
73
+ </div>
74
+
75
+ <hr class="section-divider snapshot-only" />
76
+
77
+ <h2 class="snapshot-only">Crawl limits</h2>
78
+ <div class="row snapshot-only">
79
+ <div class="field">
80
+ <label for="maxPages">Max pages</label>
81
+ <input type="number" id="maxPages" value="50" min="1" />
82
+ </div>
83
+ <div class="field">
84
+ <label for="depth">Depth</label>
85
+ <input type="number" id="depth" value="3" min="1" />
86
+ </div>
87
+ <div class="field">
88
+ <label for="concurrency">Concurrency</label>
89
+ <input type="number" id="concurrency" value="16" min="1" />
90
+ </div>
91
+ <div class="field">
92
+ <label for="crawlDelay">Crawl delay (sec)</label>
93
+ <input type="number" id="crawlDelay" value="0" min="0" step="0.1" />
94
+ </div>
95
+ <div class="field">
96
+ <label for="timeout">Timeout (sec)</label>
97
+ <input type="number" id="timeout" value="15" min="1" step="0.5" />
98
+ </div>
99
+ <div class="field">
100
+ <label for="lang">Output format</label>
101
+ <select id="lang">
102
+ <option value="html">html</option>
103
+ <option value="md">md</option>
104
+ </select>
105
+ </div>
106
+ </div>
107
+
108
+ <hr class="section-divider snapshot-only" />
109
+
110
+ <h2 class="snapshot-only">Filters &amp; auth</h2>
111
+ <div class="field snapshot-only">
112
+ <label for="includes">Include globs (one per line)</label>
113
+ <textarea id="includes" placeholder="/docs/*&#10;/blog/*"></textarea>
114
+ </div>
115
+ <div class="field snapshot-only">
116
+ <label for="excludes">Exclude globs (one per line)</label>
117
+ <textarea id="excludes" placeholder="/admin/*&#10;/drafts/*"></textarea>
118
+ </div>
119
+ <div class="field snapshot-only">
120
+ <label for="cookies">Cookies (name=value, one per line)</label>
121
+ <textarea id="cookies" placeholder="session=abc123"></textarea>
122
+ </div>
123
+ <div class="field snapshot-only">
124
+ <label for="headers">Headers (Name: Value, one per line)</label>
125
+ <textarea id="headers" placeholder="Authorization: Bearer TOKEN"></textarea>
126
+ </div>
127
+ <div class="field snapshot-only">
128
+ <label for="userAgent">Custom User-Agent</label>
129
+ <input type="text" id="userAgent" placeholder="Leave blank for default" />
130
+ </div>
131
+
132
+ <hr class="section-divider" />
133
+
134
+ <h2>Options</h2>
135
+ <div class="check-grid snapshot-only">
136
+ <label class="check-item">
137
+ <input type="checkbox" id="robots" checked />
138
+ <div>--robots<span>Respect robots.txt</span></div>
139
+ </label>
140
+ <label class="check-item">
141
+ <input type="checkbox" id="sameOrigin" checked />
142
+ <div>--same-origin<span>Same origin only</span></div>
143
+ </label>
144
+ <label class="check-item">
145
+ <input type="checkbox" id="noAssets" />
146
+ <div>--no-assets<span>Skip CSS/JS/images</span></div>
147
+ </label>
148
+ <label class="check-item">
149
+ <input type="checkbox" id="verbose" />
150
+ <div>--verbose<span>Detailed logs</span></div>
151
+ </label>
152
+ <label class="check-item">
153
+ <input type="checkbox" id="dryRun" />
154
+ <div>--dry-run<span>Don't write files</span></div>
155
+ </label>
156
+ </div>
157
+ <div class="row restore-only hidden">
158
+ <div class="field">
159
+ <label for="port">Port</label>
160
+ <input type="number" id="port" value="8080" min="1" />
161
+ </div>
162
+ <div class="field">
163
+ <label for="host">Host</label>
164
+ <input type="text" id="host" value="127.0.0.1" />
165
+ </div>
166
+ </div>
167
+ <label class="check-item restore-only hidden">
168
+ <input type="checkbox" id="noOpen" />
169
+ <div>--no-open<span>Don't open browser</span></div>
170
+ </label>
171
+ </div>
172
+
173
+ <div class="panel output-panel">
174
+ <h2>Generated command</h2>
175
+ <div class="command-box">
176
+ <code id="command">snapshot https://example.com ./mirror</code>
177
+ <button type="button" class="copy-btn" id="copyBtn">Copy</button>
178
+ </div>
179
+
180
+ <div class="install-line">
181
+ Install first:
182
+ <code>pip install web-snapshot-cli</code>
183
+ </div>
184
+
185
+ <div class="install-line" id="restoreHint">
186
+ After snapshot, restore locally:
187
+ <code id="restoreCommand">snapshot -restore ./mirror</code>
188
+ </div>
189
+ </div>
190
+ </div>
191
+
192
+ <footer>
193
+ <a href="https://github.com/codingsushi79/Snapshot">GitHub</a>
194
+ ·
195
+ <a href="https://pypi.org/project/web-snapshot-cli/">PyPI</a>
196
+ </footer>
197
+
198
+ <script src="app.js"></script>
199
+ </body>
200
+ </html>