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 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] auto-detect files changed via git")
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
- ` Use --format png or --format jpg, or upgrade to Pro for svg.`,
82
+ ` Use --format png or --format jpg, or upgrade to Pro for svg.`
90
83
  );
91
- process.exit(1);
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.exit(1);
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
- " On the Free plan, list files one by one:\n" +
132
- " syntaxshot file1.js file2.css file3.ts",
124
+ " On the Free plan, list files one by one:\n" +
125
+ " syntaxshot file1.js file2.css file3.ts"
133
126
  );
134
- process.exit(1);
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
- "No eligible files found (everything was excluded or unsupported).",
147
+ "No eligible files found (everything was excluded or unsupported)."
157
148
  );
158
149
  return;
159
150
  }
160
151
 
161
- console.log(
162
- `📁 Folder(s) scanned: ${directories.map((d) => path.relative(cwd, d) || ".").join(", ")}`,
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
- " Usage: syntaxshot path/to/file.js [another/file.ts ...]\n" +
191
- " Pro plan: syntaxshot path/to/folder (scans and generates everything automatically)",
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.exit(1);
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.exit(1);
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
- const output = execSync("git diff --name-only HEAD", { encoding: "utf-8" });
290
- return output
291
- .split("\n")
292
- .map((l) => l.trim())
293
- .filter(Boolean)
294
- .filter((f) => fs.existsSync(f));
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "syntaxshot-cli",
3
- "version": "1.0.3",
3
+ "version": "1.0.5",
4
4
  "description": "Genera capturas de codigo con resaltado de sintaxis desde la terminal",
5
5
  "type": "module",
6
6
  "bin": {
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://syntaxshot-api.vercel.app";
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 controller = new AbortController();
32
- const timeoutId = setTimeout(() => controller.abort(), 8000);
33
-
34
- try {
35
- const res = await fetch(`${API_URL}/api/v1/validate`, {
36
- method: "POST",
37
- headers: { "Content-Type": "application/json" },
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
- const result = await validateWithApi(licenseKey);
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
+ }
@@ -1,3 +0,0 @@
1
- {
2
- "count": 1
3
- }
@@ -1,11 +0,0 @@
1
- {
2
- "theme": "solar",
3
- "format": "png",
4
- "output": "syntaxshots",
5
- "fontSize": 16,
6
- "lineNumbers": true,
7
- "quality": 0.95,
8
- "excludeDirs": [],
9
- "excludeFiles": [],
10
- "plan": "pro"
11
- }