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 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.exit(1);
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] auto-detect files changed via git")
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
- ` 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.`
89
83
  );
90
- process.exit(1);
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.exit(1);
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
- " On the Free plan, list files one by one:\n" +
131
- " 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"
132
126
  );
133
- process.exit(1);
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
- "No eligible files found (everything was excluded or unsupported).",
147
+ "No eligible files found (everything was excluded or unsupported)."
156
148
  );
157
149
  return;
158
150
  }
159
151
 
160
- console.log(
161
- `📁 Folder(s) scanned: ${directories.map((d) => path.relative(cwd, d) || ".").join(", ")}`,
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
- " Usage: syntaxshot path/to/file.js [another/file.ts ...]\n" +
190
- " 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)"
191
176
  );
192
- process.exit(1);
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.exit(1);
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
- const output = execSync("git diff --name-only HEAD", { encoding: "utf-8" });
289
- return output
290
- .split("\n")
291
- .map((l) => l.trim())
292
- .filter(Boolean)
293
- .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)));
294
282
  } catch {
295
283
  return [];
296
284
  }
297
285
  }
298
286
 
299
- program.parse();
287
+ program.parse();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "syntaxshot-cli",
3
- "version": "1.0.2",
3
+ "version": "1.0.4",
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://api.syntaxshot.dev";
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
- 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
+
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
+ }
@@ -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
- }