syntaxshot-cli 1.0.3 → 1.0.5
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 +1 -0
- package/bin/syntaxshot.js +50 -63
- package/package.json +1 -1
- package/src/license.js +19 -17
- package/src/scanner.js +7 -1
- package/.syntaxshot-usage.json +0 -3
- package/.syntaxshotrc.json +0 -11
package/README.md
CHANGED
|
@@ -94,3 +94,4 @@ Quick list: `syntaxshot themes`
|
|
|
94
94
|
- `bin/syntaxshot.js` — CLI, plan limits, `--auto`, confirmation prompt, `login`/`logout`
|
|
95
95
|
|
|
96
96
|
The license backend itself (Stripe webhook, `/api/v1/validate`, license key generation) lives in the separate `syntaxshot-api` project.
|
|
97
|
+
# syntaxshot-cli
|
package/bin/syntaxshot.js
CHANGED
|
@@ -11,16 +11,14 @@ import { detectLang, tokenizeCode } from "../src/highlighter.js";
|
|
|
11
11
|
import { renderSnippet } from "../src/renderer.js";
|
|
12
12
|
import { renderSnippetSVG } from "../src/svgRenderer.js";
|
|
13
13
|
import { checkAndConsume, FREE_LIMIT } from "../src/usage.js";
|
|
14
|
-
import { findEligibleFiles } from "../src/scanner.js";
|
|
14
|
+
import { findEligibleFiles, isEligibleFile } from "../src/scanner.js";
|
|
15
15
|
import { activateLicense, logoutLicense, resolvePlan } from "../src/license.js";
|
|
16
16
|
|
|
17
17
|
const program = new Command();
|
|
18
18
|
|
|
19
19
|
program
|
|
20
20
|
.name("syntaxshot")
|
|
21
|
-
.description(
|
|
22
|
-
"Generate syntax-highlighted code screenshots from your terminal.",
|
|
23
|
-
)
|
|
21
|
+
.description("Generate syntax-highlighted code screenshots from your terminal.")
|
|
24
22
|
.version("1.0.0");
|
|
25
23
|
|
|
26
24
|
program
|
|
@@ -46,9 +44,7 @@ program
|
|
|
46
44
|
|
|
47
45
|
program
|
|
48
46
|
.command("init")
|
|
49
|
-
.description(
|
|
50
|
-
"Create a .syntaxshotrc.json file with the default configuration",
|
|
51
|
-
)
|
|
47
|
+
.description("Create a .syntaxshotrc.json file with the default configuration")
|
|
52
48
|
.action(() => writeDefaultConfig());
|
|
53
49
|
|
|
54
50
|
program
|
|
@@ -56,22 +52,20 @@ program
|
|
|
56
52
|
.description("List the available themes")
|
|
57
53
|
.action(() => {
|
|
58
54
|
console.log("Available themes:");
|
|
59
|
-
Object.entries(THEMES).forEach(([key, t]) =>
|
|
60
|
-
console.log(` - ${key} (${t.label})`),
|
|
61
|
-
);
|
|
55
|
+
Object.entries(THEMES).forEach(([key, t]) => console.log(` - ${key} (${t.label})`));
|
|
62
56
|
});
|
|
63
57
|
|
|
64
58
|
program
|
|
65
59
|
.argument(
|
|
66
60
|
"[paths...]",
|
|
67
|
-
"File(s) to capture (Free and Pro), or a folder to auto-scan (Pro only)"
|
|
61
|
+
"File(s) to capture (Free and Pro), or a folder to auto-scan (Pro only)"
|
|
68
62
|
)
|
|
69
63
|
.option("-t, --theme <name>", "Visual theme (overrides config)")
|
|
70
64
|
.option("-f, --format <ext>", "png | jpg (overrides config)")
|
|
71
65
|
.option("-o, --output <dir>", "Output folder (overrides config)")
|
|
72
66
|
.option("--no-line-numbers", "Hide line numbers")
|
|
73
67
|
.option("-y, --yes", "Skip the confirmation prompt when scanning a folder")
|
|
74
|
-
.option("--auto", "[Pro plan]
|
|
68
|
+
.option("--auto", "[Pro plan] screenshot only the files changed since the last commit — perfect for reviews")
|
|
75
69
|
.action(async (inputPaths, cmdOpts) => {
|
|
76
70
|
const cwd = process.cwd();
|
|
77
71
|
const config = loadConfig(cwd);
|
|
@@ -81,14 +75,14 @@ program
|
|
|
81
75
|
const lineNumbers = cmdOpts.lineNumbers ?? config.lineNumbers;
|
|
82
76
|
const plan = await resolvePlan(config.plan);
|
|
83
77
|
|
|
84
|
-
const allowedFormats =
|
|
85
|
-
plan === "pro" ? ["png", "jpg", "jpeg", "svg"] : ["png", "jpg", "jpeg"];
|
|
78
|
+
const allowedFormats = plan === "pro" ? ["png", "jpg", "jpeg", "svg"] : ["png", "jpg", "jpeg"];
|
|
86
79
|
if (!allowedFormats.includes(format)) {
|
|
87
80
|
console.error(
|
|
88
81
|
`❌ The "${format}" format is a Pro plan feature. The Free plan supports png or jpg.\n` +
|
|
89
|
-
|
|
82
|
+
` Use --format png or --format jpg, or upgrade to Pro for svg.`
|
|
90
83
|
);
|
|
91
|
-
process.
|
|
84
|
+
process.exitCode = 1;
|
|
85
|
+
return;
|
|
92
86
|
}
|
|
93
87
|
|
|
94
88
|
let targets = [];
|
|
@@ -96,18 +90,17 @@ program
|
|
|
96
90
|
if (cmdOpts.auto) {
|
|
97
91
|
if (plan !== "pro") {
|
|
98
92
|
console.error(
|
|
99
|
-
"❌ --auto is a Pro plan feature. On the Free plan, list file(s) manually:\n syntaxshot path/to/file.js"
|
|
93
|
+
"❌ --auto is a Pro plan feature. On the Free plan, list file(s) manually:\n syntaxshot path/to/file.js"
|
|
100
94
|
);
|
|
101
|
-
process.
|
|
95
|
+
process.exitCode = 1;
|
|
96
|
+
return;
|
|
102
97
|
}
|
|
103
98
|
const changed = getChangedFilesFromGit();
|
|
104
99
|
if (changed.length === 0) {
|
|
105
100
|
console.log("No modified files detected via git. Nothing to capture.");
|
|
106
101
|
return;
|
|
107
102
|
}
|
|
108
|
-
console.log(
|
|
109
|
-
`🔎 Auto-detected ${changed.length} file(s) changed via git.`,
|
|
110
|
-
);
|
|
103
|
+
console.log(`🔎 Auto-detected ${changed.length} file(s) changed via git.`);
|
|
111
104
|
targets = changed.map((f) => ({ file: path.resolve(f), scanRoot: null }));
|
|
112
105
|
} else {
|
|
113
106
|
const explicitFiles = [];
|
|
@@ -128,16 +121,14 @@ program
|
|
|
128
121
|
if (directories.length > 0 && plan !== "pro") {
|
|
129
122
|
console.error(
|
|
130
123
|
"❌ Scanning a whole folder is a Pro plan feature.\n" +
|
|
131
|
-
|
|
132
|
-
|
|
124
|
+
" On the Free plan, list files one by one:\n" +
|
|
125
|
+
" syntaxshot file1.js file2.css file3.ts"
|
|
133
126
|
);
|
|
134
|
-
process.
|
|
127
|
+
process.exitCode = 1;
|
|
128
|
+
return;
|
|
135
129
|
}
|
|
136
130
|
|
|
137
|
-
targets = explicitFiles.map((f) => ({
|
|
138
|
-
file: path.resolve(f),
|
|
139
|
-
scanRoot: null,
|
|
140
|
-
}));
|
|
131
|
+
targets = explicitFiles.map((f) => ({ file: path.resolve(f), scanRoot: null }));
|
|
141
132
|
|
|
142
133
|
for (const dir of directories) {
|
|
143
134
|
const found = findEligibleFiles(dir, {
|
|
@@ -153,29 +144,22 @@ program
|
|
|
153
144
|
if (scannedCount === 0) {
|
|
154
145
|
console.log(
|
|
155
146
|
`📁 Scanned: ${directories.map((d) => path.relative(cwd, d) || ".").join(", ")}\n` +
|
|
156
|
-
|
|
147
|
+
"No eligible files found (everything was excluded or unsupported)."
|
|
157
148
|
);
|
|
158
149
|
return;
|
|
159
150
|
}
|
|
160
151
|
|
|
161
|
-
console.log(
|
|
162
|
-
|
|
163
|
-
);
|
|
164
|
-
console.log(
|
|
165
|
-
`📄 Found ${scannedCount} eligible file(s). This will generate ${scannedCount} image(s):`,
|
|
166
|
-
);
|
|
152
|
+
console.log(`📁 Folder(s) scanned: ${directories.map((d) => path.relative(cwd, d) || ".").join(", ")}`);
|
|
153
|
+
console.log(`📄 Found ${scannedCount} eligible file(s). This will generate ${scannedCount} image(s):`);
|
|
167
154
|
targets
|
|
168
155
|
.filter((t) => t.scanRoot)
|
|
169
156
|
.slice(0, 25)
|
|
170
157
|
.forEach((t) => console.log(` - ${path.relative(cwd, t.file)}`));
|
|
171
|
-
if (scannedCount > 25)
|
|
172
|
-
console.log(` ...and ${scannedCount - 25} more`);
|
|
158
|
+
if (scannedCount > 25) console.log(` ...and ${scannedCount - 25} more`);
|
|
173
159
|
console.log("");
|
|
174
160
|
|
|
175
161
|
if (!cmdOpts.yes) {
|
|
176
|
-
const proceed = await askConfirmation(
|
|
177
|
-
`Continue and generate ${scannedCount} image(s)? (y/N) `,
|
|
178
|
-
);
|
|
162
|
+
const proceed = await askConfirmation(`Continue and generate ${scannedCount} image(s)? (y/N) `);
|
|
179
163
|
if (!proceed) {
|
|
180
164
|
console.log("Cancelled. No images were generated.");
|
|
181
165
|
return;
|
|
@@ -187,10 +171,11 @@ program
|
|
|
187
171
|
if (targets.length === 0) {
|
|
188
172
|
console.error(
|
|
189
173
|
"❌ You must provide at least one file or one folder.\n" +
|
|
190
|
-
|
|
191
|
-
|
|
174
|
+
" Usage: syntaxshot path/to/file.js [another/file.ts ...]\n" +
|
|
175
|
+
" Pro plan: syntaxshot path/to/folder (scans and generates everything automatically)"
|
|
192
176
|
);
|
|
193
|
-
process.
|
|
177
|
+
process.exitCode = 1;
|
|
178
|
+
return;
|
|
194
179
|
}
|
|
195
180
|
|
|
196
181
|
const check = checkAndConsume(plan, targets.length, cwd);
|
|
@@ -198,10 +183,11 @@ program
|
|
|
198
183
|
console.error(
|
|
199
184
|
`❌ Free plan limit reached (${FREE_LIMIT} images). You have ${Math.max(
|
|
200
185
|
check.remaining,
|
|
201
|
-
0
|
|
202
|
-
)} left. Upgrade to Pro for unlimited generation
|
|
186
|
+
0
|
|
187
|
+
)} left. Upgrade to Pro for unlimited generation.`
|
|
203
188
|
);
|
|
204
|
-
process.
|
|
189
|
+
process.exitCode = 1;
|
|
190
|
+
return;
|
|
205
191
|
}
|
|
206
192
|
|
|
207
193
|
const resolvedOutputDir = path.resolve(cwd, outputDir);
|
|
@@ -220,17 +206,12 @@ program
|
|
|
220
206
|
}
|
|
221
207
|
|
|
222
208
|
if (plan === "free") {
|
|
223
|
-
console.log(
|
|
224
|
-
`ℹ️ Free plan: ${check.remaining} capture(s) left this month.`,
|
|
225
|
-
);
|
|
209
|
+
console.log(`ℹ️ Free plan: ${check.remaining} capture(s) left this month.`);
|
|
226
210
|
}
|
|
227
211
|
});
|
|
228
212
|
|
|
229
213
|
async function askConfirmation(question) {
|
|
230
|
-
const rl = readline.createInterface({
|
|
231
|
-
input: process.stdin,
|
|
232
|
-
output: process.stdout,
|
|
233
|
-
});
|
|
214
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
234
215
|
const answer = await rl.question(question);
|
|
235
216
|
rl.close();
|
|
236
217
|
return /^y(es)?$/i.test(answer.trim());
|
|
@@ -279,22 +260,28 @@ async function processFile(absPath, opts) {
|
|
|
279
260
|
}
|
|
280
261
|
|
|
281
262
|
fs.writeFileSync(outPath, buffer);
|
|
282
|
-
console.log(
|
|
283
|
-
`✅ ${path.relative(process.cwd(), absPath)} → ${path.relative(process.cwd(), outPath)}`,
|
|
284
|
-
);
|
|
263
|
+
console.log(`✅ ${path.relative(process.cwd(), absPath)} → ${path.relative(process.cwd(), outPath)}`);
|
|
285
264
|
}
|
|
286
265
|
|
|
287
266
|
function getChangedFilesFromGit() {
|
|
288
267
|
try {
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
268
|
+
// Tracked files modified since the last commit (staged or not).
|
|
269
|
+
const modified = execSync("git diff --name-only HEAD", { encoding: "utf-8" });
|
|
270
|
+
// New files not yet tracked by git (e.g. a component you just created).
|
|
271
|
+
const untracked = execSync("git ls-files --others --exclude-standard", { encoding: "utf-8" });
|
|
272
|
+
|
|
273
|
+
const combined = new Set(
|
|
274
|
+
[...modified.split("\n"), ...untracked.split("\n")]
|
|
275
|
+
.map((l) => l.trim())
|
|
276
|
+
.filter(Boolean)
|
|
277
|
+
);
|
|
278
|
+
|
|
279
|
+
return [...combined]
|
|
280
|
+
.filter((f) => fs.existsSync(f) && fs.statSync(f).isFile())
|
|
281
|
+
.filter((f) => isEligibleFile(path.basename(f)));
|
|
295
282
|
} catch {
|
|
296
283
|
return [];
|
|
297
284
|
}
|
|
298
285
|
}
|
|
299
286
|
|
|
300
|
-
program.parse();
|
|
287
|
+
program.parse();
|
package/package.json
CHANGED
package/src/license.js
CHANGED
|
@@ -11,7 +11,7 @@ const REVALIDATE_AFTER_MS = 24 * 60 * 60 * 1000; // 24h
|
|
|
11
11
|
// license for this long before falling back to Free.
|
|
12
12
|
const OFFLINE_GRACE_MS = 7 * 24 * 60 * 60 * 1000; // 7 days
|
|
13
13
|
|
|
14
|
-
const API_URL = process.env.SYNTAXSHOT_API_URL || "https://
|
|
14
|
+
const API_URL = process.env.SYNTAXSHOT_API_URL || "https://api.syntaxshot.dev";
|
|
15
15
|
|
|
16
16
|
function readLicenseFile() {
|
|
17
17
|
if (!fs.existsSync(LICENSE_FILE)) return null;
|
|
@@ -28,25 +28,27 @@ function writeLicenseFile(data) {
|
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
async function validateWithApi(licenseKey) {
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
body: JSON.stringify({ licenseKey }),
|
|
39
|
-
signal: controller.signal,
|
|
40
|
-
});
|
|
41
|
-
return res.json();
|
|
42
|
-
} finally {
|
|
43
|
-
clearTimeout(timeoutId);
|
|
44
|
-
}
|
|
31
|
+
const res = await fetch(`${API_URL}/api/v1/validate`, {
|
|
32
|
+
method: "POST",
|
|
33
|
+
headers: { "Content-Type": "application/json" },
|
|
34
|
+
body: JSON.stringify({ licenseKey }),
|
|
35
|
+
signal: AbortSignal.timeout(8000),
|
|
36
|
+
});
|
|
37
|
+
return res.json();
|
|
45
38
|
}
|
|
46
39
|
|
|
47
40
|
/** Called by `syntaxshot login <key>`. Validates once and stores it. */
|
|
48
41
|
export async function activateLicense(licenseKey) {
|
|
49
|
-
|
|
42
|
+
let result;
|
|
43
|
+
try {
|
|
44
|
+
result = await validateWithApi(licenseKey);
|
|
45
|
+
} catch {
|
|
46
|
+
return {
|
|
47
|
+
ok: false,
|
|
48
|
+
error: "Couldn't reach the license server. Check your internet connection and try again.",
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
50
52
|
if (!result.valid) {
|
|
51
53
|
return { ok: false, error: result.error || "This license key is not active." };
|
|
52
54
|
}
|
|
@@ -99,4 +101,4 @@ export async function resolvePlan(localConfigPlan) {
|
|
|
99
101
|
if (cached.lastKnownValid && age < OFFLINE_GRACE_MS) return "pro";
|
|
100
102
|
return "free";
|
|
101
103
|
}
|
|
102
|
-
}
|
|
104
|
+
}
|
package/src/scanner.js
CHANGED
|
@@ -27,6 +27,12 @@ function isExcludedFile(fileName, excludedFilenames) {
|
|
|
27
27
|
return false;
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
/** Same exclusion rule used by the folder scan, exposed for --auto (git mode). */
|
|
31
|
+
export function isEligibleFile(fileName, opts = {}) {
|
|
32
|
+
const excludedFilenames = new Set([...DEFAULT_EXCLUDED_FILENAMES, ...(opts.excludeFiles || [])]);
|
|
33
|
+
return !isExcludedFile(fileName, excludedFilenames) && !!EXT_TO_LANG[path.extname(fileName).toLowerCase()];
|
|
34
|
+
}
|
|
35
|
+
|
|
30
36
|
/**
|
|
31
37
|
* Recursively walks `rootDir` and returns the absolute paths of
|
|
32
38
|
* "capturable" files: they have a recognized language extension and are not
|
|
@@ -60,4 +66,4 @@ export function findEligibleFiles(rootDir, opts = {}) {
|
|
|
60
66
|
|
|
61
67
|
walk(rootDir);
|
|
62
68
|
return results;
|
|
63
|
-
}
|
|
69
|
+
}
|
package/.syntaxshot-usage.json
DELETED