syntaxshot-cli 1.0.2 → 1.0.4
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 +52 -64
- package/package.json +1 -1
- package/src/license.js +12 -3
- 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
|
|
@@ -30,7 +28,8 @@ program
|
|
|
30
28
|
const result = await activateLicense(licenseKey.trim());
|
|
31
29
|
if (!result.ok) {
|
|
32
30
|
console.error(`❌ ${result.error}`);
|
|
33
|
-
process.
|
|
31
|
+
process.exitCode = 1;
|
|
32
|
+
return;
|
|
34
33
|
}
|
|
35
34
|
console.log(`✅ License activated. Plan: ${result.plan}.`);
|
|
36
35
|
});
|
|
@@ -45,9 +44,7 @@ program
|
|
|
45
44
|
|
|
46
45
|
program
|
|
47
46
|
.command("init")
|
|
48
|
-
.description(
|
|
49
|
-
"Create a .syntaxshotrc.json file with the default configuration",
|
|
50
|
-
)
|
|
47
|
+
.description("Create a .syntaxshotrc.json file with the default configuration")
|
|
51
48
|
.action(() => writeDefaultConfig());
|
|
52
49
|
|
|
53
50
|
program
|
|
@@ -55,22 +52,20 @@ program
|
|
|
55
52
|
.description("List the available themes")
|
|
56
53
|
.action(() => {
|
|
57
54
|
console.log("Available themes:");
|
|
58
|
-
Object.entries(THEMES).forEach(([key, t]) =>
|
|
59
|
-
console.log(` - ${key} (${t.label})`),
|
|
60
|
-
);
|
|
55
|
+
Object.entries(THEMES).forEach(([key, t]) => console.log(` - ${key} (${t.label})`));
|
|
61
56
|
});
|
|
62
57
|
|
|
63
58
|
program
|
|
64
59
|
.argument(
|
|
65
60
|
"[paths...]",
|
|
66
|
-
"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)"
|
|
67
62
|
)
|
|
68
63
|
.option("-t, --theme <name>", "Visual theme (overrides config)")
|
|
69
64
|
.option("-f, --format <ext>", "png | jpg (overrides config)")
|
|
70
65
|
.option("-o, --output <dir>", "Output folder (overrides config)")
|
|
71
66
|
.option("--no-line-numbers", "Hide line numbers")
|
|
72
67
|
.option("-y, --yes", "Skip the confirmation prompt when scanning a folder")
|
|
73
|
-
.option("--auto", "[Pro plan]
|
|
68
|
+
.option("--auto", "[Pro plan] screenshot only the files changed since the last commit — perfect for reviews")
|
|
74
69
|
.action(async (inputPaths, cmdOpts) => {
|
|
75
70
|
const cwd = process.cwd();
|
|
76
71
|
const config = loadConfig(cwd);
|
|
@@ -80,14 +75,14 @@ program
|
|
|
80
75
|
const lineNumbers = cmdOpts.lineNumbers ?? config.lineNumbers;
|
|
81
76
|
const plan = await resolvePlan(config.plan);
|
|
82
77
|
|
|
83
|
-
const allowedFormats =
|
|
84
|
-
plan === "pro" ? ["png", "jpg", "jpeg", "svg"] : ["png", "jpg", "jpeg"];
|
|
78
|
+
const allowedFormats = plan === "pro" ? ["png", "jpg", "jpeg", "svg"] : ["png", "jpg", "jpeg"];
|
|
85
79
|
if (!allowedFormats.includes(format)) {
|
|
86
80
|
console.error(
|
|
87
81
|
`❌ The "${format}" format is a Pro plan feature. The Free plan supports png or jpg.\n` +
|
|
88
|
-
|
|
82
|
+
` Use --format png or --format jpg, or upgrade to Pro for svg.`
|
|
89
83
|
);
|
|
90
|
-
process.
|
|
84
|
+
process.exitCode = 1;
|
|
85
|
+
return;
|
|
91
86
|
}
|
|
92
87
|
|
|
93
88
|
let targets = [];
|
|
@@ -95,18 +90,17 @@ program
|
|
|
95
90
|
if (cmdOpts.auto) {
|
|
96
91
|
if (plan !== "pro") {
|
|
97
92
|
console.error(
|
|
98
|
-
"❌ --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"
|
|
99
94
|
);
|
|
100
|
-
process.
|
|
95
|
+
process.exitCode = 1;
|
|
96
|
+
return;
|
|
101
97
|
}
|
|
102
98
|
const changed = getChangedFilesFromGit();
|
|
103
99
|
if (changed.length === 0) {
|
|
104
100
|
console.log("No modified files detected via git. Nothing to capture.");
|
|
105
101
|
return;
|
|
106
102
|
}
|
|
107
|
-
console.log(
|
|
108
|
-
`🔎 Auto-detected ${changed.length} file(s) changed via git.`,
|
|
109
|
-
);
|
|
103
|
+
console.log(`🔎 Auto-detected ${changed.length} file(s) changed via git.`);
|
|
110
104
|
targets = changed.map((f) => ({ file: path.resolve(f), scanRoot: null }));
|
|
111
105
|
} else {
|
|
112
106
|
const explicitFiles = [];
|
|
@@ -127,16 +121,14 @@ program
|
|
|
127
121
|
if (directories.length > 0 && plan !== "pro") {
|
|
128
122
|
console.error(
|
|
129
123
|
"❌ Scanning a whole folder is a Pro plan feature.\n" +
|
|
130
|
-
|
|
131
|
-
|
|
124
|
+
" On the Free plan, list files one by one:\n" +
|
|
125
|
+
" syntaxshot file1.js file2.css file3.ts"
|
|
132
126
|
);
|
|
133
|
-
process.
|
|
127
|
+
process.exitCode = 1;
|
|
128
|
+
return;
|
|
134
129
|
}
|
|
135
130
|
|
|
136
|
-
targets = explicitFiles.map((f) => ({
|
|
137
|
-
file: path.resolve(f),
|
|
138
|
-
scanRoot: null,
|
|
139
|
-
}));
|
|
131
|
+
targets = explicitFiles.map((f) => ({ file: path.resolve(f), scanRoot: null }));
|
|
140
132
|
|
|
141
133
|
for (const dir of directories) {
|
|
142
134
|
const found = findEligibleFiles(dir, {
|
|
@@ -152,29 +144,22 @@ program
|
|
|
152
144
|
if (scannedCount === 0) {
|
|
153
145
|
console.log(
|
|
154
146
|
`📁 Scanned: ${directories.map((d) => path.relative(cwd, d) || ".").join(", ")}\n` +
|
|
155
|
-
|
|
147
|
+
"No eligible files found (everything was excluded or unsupported)."
|
|
156
148
|
);
|
|
157
149
|
return;
|
|
158
150
|
}
|
|
159
151
|
|
|
160
|
-
console.log(
|
|
161
|
-
|
|
162
|
-
);
|
|
163
|
-
console.log(
|
|
164
|
-
`📄 Found ${scannedCount} eligible file(s). This will generate ${scannedCount} image(s):`,
|
|
165
|
-
);
|
|
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):`);
|
|
166
154
|
targets
|
|
167
155
|
.filter((t) => t.scanRoot)
|
|
168
156
|
.slice(0, 25)
|
|
169
157
|
.forEach((t) => console.log(` - ${path.relative(cwd, t.file)}`));
|
|
170
|
-
if (scannedCount > 25)
|
|
171
|
-
console.log(` ...and ${scannedCount - 25} more`);
|
|
158
|
+
if (scannedCount > 25) console.log(` ...and ${scannedCount - 25} more`);
|
|
172
159
|
console.log("");
|
|
173
160
|
|
|
174
161
|
if (!cmdOpts.yes) {
|
|
175
|
-
const proceed = await askConfirmation(
|
|
176
|
-
`Continue and generate ${scannedCount} image(s)? (y/N) `,
|
|
177
|
-
);
|
|
162
|
+
const proceed = await askConfirmation(`Continue and generate ${scannedCount} image(s)? (y/N) `);
|
|
178
163
|
if (!proceed) {
|
|
179
164
|
console.log("Cancelled. No images were generated.");
|
|
180
165
|
return;
|
|
@@ -186,10 +171,11 @@ program
|
|
|
186
171
|
if (targets.length === 0) {
|
|
187
172
|
console.error(
|
|
188
173
|
"❌ You must provide at least one file or one folder.\n" +
|
|
189
|
-
|
|
190
|
-
|
|
174
|
+
" Usage: syntaxshot path/to/file.js [another/file.ts ...]\n" +
|
|
175
|
+
" Pro plan: syntaxshot path/to/folder (scans and generates everything automatically)"
|
|
191
176
|
);
|
|
192
|
-
process.
|
|
177
|
+
process.exitCode = 1;
|
|
178
|
+
return;
|
|
193
179
|
}
|
|
194
180
|
|
|
195
181
|
const check = checkAndConsume(plan, targets.length, cwd);
|
|
@@ -197,10 +183,11 @@ program
|
|
|
197
183
|
console.error(
|
|
198
184
|
`❌ Free plan limit reached (${FREE_LIMIT} images). You have ${Math.max(
|
|
199
185
|
check.remaining,
|
|
200
|
-
0
|
|
201
|
-
)} left. Upgrade to Pro for unlimited generation
|
|
186
|
+
0
|
|
187
|
+
)} left. Upgrade to Pro for unlimited generation.`
|
|
202
188
|
);
|
|
203
|
-
process.
|
|
189
|
+
process.exitCode = 1;
|
|
190
|
+
return;
|
|
204
191
|
}
|
|
205
192
|
|
|
206
193
|
const resolvedOutputDir = path.resolve(cwd, outputDir);
|
|
@@ -219,17 +206,12 @@ program
|
|
|
219
206
|
}
|
|
220
207
|
|
|
221
208
|
if (plan === "free") {
|
|
222
|
-
console.log(
|
|
223
|
-
`ℹ️ Free plan: ${check.remaining} capture(s) left this month.`,
|
|
224
|
-
);
|
|
209
|
+
console.log(`ℹ️ Free plan: ${check.remaining} capture(s) left this month.`);
|
|
225
210
|
}
|
|
226
211
|
});
|
|
227
212
|
|
|
228
213
|
async function askConfirmation(question) {
|
|
229
|
-
const rl = readline.createInterface({
|
|
230
|
-
input: process.stdin,
|
|
231
|
-
output: process.stdout,
|
|
232
|
-
});
|
|
214
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
233
215
|
const answer = await rl.question(question);
|
|
234
216
|
rl.close();
|
|
235
217
|
return /^y(es)?$/i.test(answer.trim());
|
|
@@ -278,22 +260,28 @@ async function processFile(absPath, opts) {
|
|
|
278
260
|
}
|
|
279
261
|
|
|
280
262
|
fs.writeFileSync(outPath, buffer);
|
|
281
|
-
console.log(
|
|
282
|
-
`✅ ${path.relative(process.cwd(), absPath)} → ${path.relative(process.cwd(), outPath)}`,
|
|
283
|
-
);
|
|
263
|
+
console.log(`✅ ${path.relative(process.cwd(), absPath)} → ${path.relative(process.cwd(), outPath)}`);
|
|
284
264
|
}
|
|
285
265
|
|
|
286
266
|
function getChangedFilesFromGit() {
|
|
287
267
|
try {
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
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)));
|
|
294
282
|
} catch {
|
|
295
283
|
return [];
|
|
296
284
|
}
|
|
297
285
|
}
|
|
298
286
|
|
|
299
|
-
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://api.
|
|
14
|
+
const API_URL = process.env.SYNTAXSHOT_API_URL || "https://syntaxshot-api.vercel.app";
|
|
15
15
|
|
|
16
16
|
function readLicenseFile() {
|
|
17
17
|
if (!fs.existsSync(LICENSE_FILE)) return null;
|
|
@@ -39,7 +39,16 @@ async function validateWithApi(licenseKey) {
|
|
|
39
39
|
|
|
40
40
|
/** Called by `syntaxshot login <key>`. Validates once and stores it. */
|
|
41
41
|
export async function activateLicense(licenseKey) {
|
|
42
|
-
|
|
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
|
+
|
|
43
52
|
if (!result.valid) {
|
|
44
53
|
return { ok: false, error: result.error || "This license key is not active." };
|
|
45
54
|
}
|
|
@@ -92,4 +101,4 @@ export async function resolvePlan(localConfigPlan) {
|
|
|
92
101
|
if (cached.lastKnownValid && age < OFFLINE_GRACE_MS) return "pro";
|
|
93
102
|
return "free";
|
|
94
103
|
}
|
|
95
|
-
}
|
|
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