qtex 1.0.0
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/LICENSE +24 -0
- package/README.md +107 -0
- package/index.js +73 -0
- package/package.json +44 -0
- package/src/compiler.js +117 -0
- package/src/ui.js +52 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
Fair Source License, version 0.9
|
|
2
|
+
|
|
3
|
+
Copyright © 2026 Sergio Lázaro
|
|
4
|
+
|
|
5
|
+
1. Grant. The Licensor grants you a non-exclusive, worldwide, royalty-free,
|
|
6
|
+
non-transferable, non-sublicensable license to use, copy, modify, and
|
|
7
|
+
distribute this Software in source code or binary form for any purpose,
|
|
8
|
+
subject to the following conditions:
|
|
9
|
+
|
|
10
|
+
2. Conditions.
|
|
11
|
+
|
|
12
|
+
a. Redistribution. You must include this license and the copyright notice
|
|
13
|
+
above in all copies of the Software.
|
|
14
|
+
|
|
15
|
+
b. Usage Limit. Use of the Software by a single legal entity (including its
|
|
16
|
+
affiliates) is limited to 3 concurrent users.
|
|
17
|
+
|
|
18
|
+
3. Commercial License. To use the Software beyond the Usage Limit, you must
|
|
19
|
+
obtain a separate commercial license from the Licensor at:
|
|
20
|
+
https://github.com/srsergiolazaro/vortex
|
|
21
|
+
|
|
22
|
+
4. Disclaimer. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
23
|
+
EXPRESS OR IMPLIED. THE LICENSOR SHALL NOT BE LIABLE FOR ANY CLAIM, DAMAGES,
|
|
24
|
+
OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT, OR OTHERWISE.
|
package/README.md
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# 🌀 qtex CLI
|
|
2
|
+
|
|
3
|
+
<p align="center">
|
|
4
|
+
<img src="docs/assets/banner.png" alt="Vortex Banner" width="600px">
|
|
5
|
+
</p>
|
|
6
|
+
|
|
7
|
+
<p align="center">
|
|
8
|
+
<a href="https://www.npmjs.com/package/qtex"><img src="https://img.shields.io/npm/v/qtex?style=for-the-badge&logo=npm" alt="NPM Version"></a>
|
|
9
|
+
<a href="./LICENSE"><img src="https://img.shields.io/badge/License-Fair_Source-blue.svg?style=for-the-badge" alt="License: Fair Source"></a>
|
|
10
|
+
<a href="https://latex.taptapp.xyz"><img src="https://img.shields.io/badge/Engine-Tectonic-blueviolet?style=for-the-badge&logo=rust" alt="Tachyon-Tex Engine"></a>
|
|
11
|
+
<a href="https://latex.taptapp.xyz"><img src="https://img.shields.io/badge/Latency-%3C1s-green?style=for-the-badge" alt="Real-time Latency"></a>
|
|
12
|
+
</p>
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
**qtex CLI** is an ultra-fast, cloud-powered LaTeX compiler designed for developers who value speed and simplicity. Say goodbye to heavy local TeX distributions like TeXLive or MikTeX. Compile your documents in the cloud with sub-second latency and real-time feedback.
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## 🚀 Key Features
|
|
21
|
+
|
|
22
|
+
* **⚡ Lightning Fast**: Powered by the Rust-based Tectonic engine, optimized for "moonshot" speed.
|
|
23
|
+
* **📦 Zero-Config**: No local dependencies required. Just run and compile.
|
|
24
|
+
* **👀 Smart Watch Mode**: Automatically detects changes in `.tex`, `.bib`, `.sty`, and even **images** (`.png`, `.jpg`, `.jpeg`) to recompile in milliseconds.
|
|
25
|
+
* **🔍 Intelligent Validation**: Pre-flight checks on the API to catch syntax errors before the full compilation process.
|
|
26
|
+
* **📂 Recursive Project Support**: Handles complex multi-file projects, including nested asset folders.
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## 🧠 How it Works
|
|
31
|
+
|
|
32
|
+
<p align="center">
|
|
33
|
+
<img src="docs/assets/flow.png" alt="qtex Workflow" width="700px">
|
|
34
|
+
</p>
|
|
35
|
+
|
|
36
|
+
1. **Local Scan**: qtex recursively discovers all required assets (TeX, styles, images) in your project.
|
|
37
|
+
2. **Pre-flight Audit**: Sends a lightweight version to the `/validate` endpoint for immediate syntax feedback.
|
|
38
|
+
3. **Cloud Compilation**: Ships project files via high-speed multipart streams to the **Tachyon-Tex** cloud infrastructure.
|
|
39
|
+
4. **Instant Sync**: Downloads and saves the resulting PDF locally, reflecting changes almost instantly.
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## 📦 Installation
|
|
44
|
+
|
|
45
|
+
Install the CLI globally via NPM:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
npm install -g qtex
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
*(Alternatively, you can clone and link it manually)*
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
git clone https://github.com/srsergiolazaro/qtex.git
|
|
55
|
+
cd qtex
|
|
56
|
+
npm install
|
|
57
|
+
npm link
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## 🛠️ Usage
|
|
63
|
+
|
|
64
|
+
### Quick Start
|
|
65
|
+
Compile the folder containing your root LaTeX file:
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
qtex ./my-project
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### Custom Output
|
|
72
|
+
Define a specific filename for your generated PDF:
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
qtex ./my-project --output thesis_final.pdf
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### Development (Live Recompilation)
|
|
79
|
+
The `--watch` flag monitors your directory and recompiles instantly on any save:
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
qtex ./my-project --watch
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
## 📡 Infrastructure & API
|
|
88
|
+
|
|
89
|
+
qtex serves as the official CLI client for the **Tachyon-Tex** infrastructure:
|
|
90
|
+
|
|
91
|
+
* **Endpoint**: `https://latex.taptapp.xyz`
|
|
92
|
+
* **Engine**: Tectonic (Rust / XeTeX)
|
|
93
|
+
* **Privacy**: Stateless and ephemeral. Project data is processed in-memory and never stored.
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
## ⚖️ License
|
|
98
|
+
|
|
99
|
+
This project is licensed under the **Fair Source License**.
|
|
100
|
+
* **Individual/Small Teams**: Free to use for individuals and organizations with up to 3 concurrent users.
|
|
101
|
+
* **Enterprise/Large Scale**: For use beyond 3 users, please contact us for a commercial license.
|
|
102
|
+
|
|
103
|
+
For more details, see the [LICENSE](./LICENSE) file.
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
Built with ❤️ by the **Tachyon-Tex** team. Optimized for modern LaTeX workflows.
|
package/index.js
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { watch } from 'node:fs';
|
|
4
|
+
import { resolve, basename, extname } from 'node:path';
|
|
5
|
+
import { parseArgs } from 'node:util';
|
|
6
|
+
import { colors, ui } from './src/ui.js';
|
|
7
|
+
import { compile } from './src/compiler.js';
|
|
8
|
+
|
|
9
|
+
// --- Entry Point ---
|
|
10
|
+
const args = process.argv.slice(2);
|
|
11
|
+
|
|
12
|
+
const optionsSchema = {
|
|
13
|
+
watch: { type: 'boolean', short: 'w' },
|
|
14
|
+
output: { type: 'string', short: 'o' },
|
|
15
|
+
help: { type: 'boolean', short: 'h' }
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
async function main() {
|
|
19
|
+
try {
|
|
20
|
+
const { values, positionals } = parseArgs({ args, options: optionsSchema, allowPositionals: true });
|
|
21
|
+
|
|
22
|
+
if (values.help || positionals.length === 0) {
|
|
23
|
+
console.log(`
|
|
24
|
+
${colors.magenta}${colors.bold}🌀 qtex CLI${colors.reset}
|
|
25
|
+
Ultra-fast LaTeX compilation powered by Tachyon-Tex
|
|
26
|
+
|
|
27
|
+
${colors.bold}USAGE:${colors.reset}
|
|
28
|
+
qtex <directory> [options]
|
|
29
|
+
|
|
30
|
+
${colors.bold}OPTIONS:${colors.reset}
|
|
31
|
+
-w, --watch Watch for changes and recompile
|
|
32
|
+
-o, --output <file> Define output filename (default: output.pdf)
|
|
33
|
+
-h, --help Show this help message
|
|
34
|
+
`);
|
|
35
|
+
process.exit(0);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const directory = positionals[0];
|
|
39
|
+
console.log(`${colors.magenta}${colors.bold}\n🌀 qtex CLI v1.0.0 (Vanilla)${colors.reset}\n`);
|
|
40
|
+
|
|
41
|
+
if (values.watch) {
|
|
42
|
+
ui.info(`Watching for changes in: ${colors.bold}${directory}${colors.reset}`);
|
|
43
|
+
await compile(directory, values);
|
|
44
|
+
|
|
45
|
+
let isCompiling = false;
|
|
46
|
+
watch(directory, { recursive: true }, async (event, filename) => {
|
|
47
|
+
if (filename && !filename.startsWith('.') && !isCompiling) {
|
|
48
|
+
const ext = extname(filename).toLowerCase();
|
|
49
|
+
const outputFileName = values.output || 'output.pdf';
|
|
50
|
+
|
|
51
|
+
// Watch for LaTeX files and images, but ignore the output PDF to avoid loops
|
|
52
|
+
const watchExts = ['.tex', '.bib', '.sty', '.cls', '.png', '.jpg', '.jpeg', '.pdf'];
|
|
53
|
+
const isOutputFile = basename(filename) === basename(outputFileName);
|
|
54
|
+
|
|
55
|
+
if (watchExts.includes(ext) && !isOutputFile) {
|
|
56
|
+
isCompiling = true;
|
|
57
|
+
console.log(`\n${colors.blue}🔄 Change detected in ${filename}, recompiling...${colors.reset}`);
|
|
58
|
+
await compile(directory, values);
|
|
59
|
+
isCompiling = false;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
} else {
|
|
64
|
+
await compile(directory, values);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
} catch (e) {
|
|
68
|
+
ui.error(e.message);
|
|
69
|
+
process.exit(1);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
main();
|
package/package.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "qtex",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Vortex CLI: Ultra-fast cloud-based LaTeX compilation. Compile LaTeX documents in milliseconds using a high-performance Rust-powered engine without local TeX installation.",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
8
|
+
},
|
|
9
|
+
"keywords": [
|
|
10
|
+
"latex",
|
|
11
|
+
"tex",
|
|
12
|
+
"compiler",
|
|
13
|
+
"tectonic",
|
|
14
|
+
"rust",
|
|
15
|
+
"cloud-latex",
|
|
16
|
+
"pdf-generator",
|
|
17
|
+
"vortex",
|
|
18
|
+
"cli",
|
|
19
|
+
"typesetting"
|
|
20
|
+
],
|
|
21
|
+
"author": "Sergio Lázaro (srsergio)",
|
|
22
|
+
"repository": {
|
|
23
|
+
"type": "git",
|
|
24
|
+
"url": "git+https://github.com/srsergiolazaro/qtex.git"
|
|
25
|
+
},
|
|
26
|
+
"bugs": {
|
|
27
|
+
"url": "https://github.com/srsergiolazaro/qtex/issues"
|
|
28
|
+
},
|
|
29
|
+
"homepage": "https://github.com/srsergiolazaro/qtex#readme",
|
|
30
|
+
"license": "SEE LICENSE IN LICENSE",
|
|
31
|
+
"type": "module",
|
|
32
|
+
"bin": {
|
|
33
|
+
"qtex": "./index.js"
|
|
34
|
+
},
|
|
35
|
+
"files": [
|
|
36
|
+
"index.js",
|
|
37
|
+
"src/",
|
|
38
|
+
"README.md",
|
|
39
|
+
"LICENSE"
|
|
40
|
+
],
|
|
41
|
+
"directories": {
|
|
42
|
+
"example": "example"
|
|
43
|
+
}
|
|
44
|
+
}
|
package/src/compiler.js
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { readFile, writeFile, readdir, stat } from 'node:fs/promises';
|
|
2
|
+
import { resolve, join, extname } from 'node:path';
|
|
3
|
+
import { colors, ui, Spinner } from './ui.js';
|
|
4
|
+
|
|
5
|
+
const API_BASE = 'https://latex.taptapp.xyz';
|
|
6
|
+
|
|
7
|
+
async function getFiles(dir, baseDir = dir) {
|
|
8
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
9
|
+
const files = [];
|
|
10
|
+
for (const entry of entries) {
|
|
11
|
+
const res = join(dir, entry.name);
|
|
12
|
+
if (entry.isDirectory()) {
|
|
13
|
+
if (entry.name !== 'node_modules' && !entry.name.startsWith('.')) {
|
|
14
|
+
files.push(...(await getFiles(res, baseDir)));
|
|
15
|
+
}
|
|
16
|
+
} else {
|
|
17
|
+
files.push({
|
|
18
|
+
path: res,
|
|
19
|
+
relative: join(dir.replace(baseDir, ''), entry.name).replace(/^[\\\/]/, '')
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return files;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function compile(dir, options) {
|
|
27
|
+
const outputFileName = options.output || 'output.pdf';
|
|
28
|
+
const spinner = new Spinner(`${colors.blue}Preparing compilation...`).start();
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
const absoluteDir = resolve(dir);
|
|
32
|
+
const allFiles = await getFiles(absoluteDir);
|
|
33
|
+
|
|
34
|
+
const form = new FormData();
|
|
35
|
+
const validateForm = new FormData();
|
|
36
|
+
let hasLatex = false;
|
|
37
|
+
|
|
38
|
+
for (const fileObj of allFiles) {
|
|
39
|
+
const ext = extname(fileObj.path).toLowerCase();
|
|
40
|
+
const allowedExts = ['.tex', '.bib', '.sty', '.cls', '.pdf', '.png', '.jpg', '.jpeg'];
|
|
41
|
+
|
|
42
|
+
if (allowedExts.includes(ext)) {
|
|
43
|
+
const content = await readFile(fileObj.path);
|
|
44
|
+
const blob = new Blob([content]);
|
|
45
|
+
form.append('files', blob, fileObj.relative);
|
|
46
|
+
|
|
47
|
+
if (ext === '.tex') {
|
|
48
|
+
validateForm.append('files', blob, fileObj.relative);
|
|
49
|
+
hasLatex = true;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (!hasLatex) {
|
|
55
|
+
spinner.fail(`${colors.yellow}No LaTeX files found in directory.`);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Phase 1: Validation
|
|
60
|
+
spinner.update(`${colors.cyan}Validating LaTeX...`);
|
|
61
|
+
try {
|
|
62
|
+
const validateRes = await fetch(`${API_BASE}/validate`, {
|
|
63
|
+
method: 'POST',
|
|
64
|
+
body: validateForm
|
|
65
|
+
});
|
|
66
|
+
const validation = await validateRes.json();
|
|
67
|
+
|
|
68
|
+
if (validation.valid === false) {
|
|
69
|
+
spinner.stop();
|
|
70
|
+
ui.error('Validation failed!');
|
|
71
|
+
validation.errors.forEach(err => {
|
|
72
|
+
console.log(` ${colors.red}• [Line ${err.line || '?'}] ${err.message}${colors.reset}`);
|
|
73
|
+
});
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (validation.warnings?.length > 0) {
|
|
78
|
+
spinner.stop();
|
|
79
|
+
ui.warn('Validation warnings:');
|
|
80
|
+
validation.warnings.forEach(warn => console.log(` ${colors.yellow}⚡ ${warn}${colors.reset}`));
|
|
81
|
+
spinner.start();
|
|
82
|
+
}
|
|
83
|
+
} catch (e) {
|
|
84
|
+
// Validation service unavailable, proceed anyway
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Phase 2: Compilation
|
|
88
|
+
spinner.update(`${colors.cyan}Compiling via Tachyon-Tex API...`);
|
|
89
|
+
|
|
90
|
+
const response = await fetch(`${API_BASE}/compile`, {
|
|
91
|
+
method: 'POST',
|
|
92
|
+
body: form
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
if (response.ok) {
|
|
96
|
+
const buffer = await response.arrayBuffer();
|
|
97
|
+
const compileTime = response.headers.get('x-compile-time-ms') || 'unknown';
|
|
98
|
+
const filesReceived = response.headers.get('x-files-received') || '0';
|
|
99
|
+
|
|
100
|
+
const outputPath = resolve(process.cwd(), outputFileName);
|
|
101
|
+
await writeFile(outputPath, Buffer.from(buffer));
|
|
102
|
+
|
|
103
|
+
spinner.succeed(`${colors.green}PDF generated in ${colors.bold}${compileTime}ms${colors.reset}`);
|
|
104
|
+
console.log(`${colors.dim} ⚡ Files: ${filesReceived} processed${colors.reset}`);
|
|
105
|
+
console.log(`${colors.dim} 📍 Path: ${outputPath}${colors.reset}`);
|
|
106
|
+
} else {
|
|
107
|
+
const errorMsg = await response.text();
|
|
108
|
+
spinner.fail(`Compilation failed (Status ${response.status})`);
|
|
109
|
+
console.log(`\n${colors.yellow}--- Error Log ---${colors.reset}`);
|
|
110
|
+
console.log(`${colors.red}${errorMsg || 'Error details unavailable.'}${colors.reset}`);
|
|
111
|
+
console.log(`${colors.yellow}-----------------\n${colors.reset}`);
|
|
112
|
+
}
|
|
113
|
+
} catch (error) {
|
|
114
|
+
spinner.fail('An unexpected error occurred.');
|
|
115
|
+
console.error(`${colors.red}${error.message}${colors.reset}`);
|
|
116
|
+
}
|
|
117
|
+
}
|
package/src/ui.js
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
export const ESC = '\x1b[';
|
|
2
|
+
export const colors = {
|
|
3
|
+
reset: `${ESC}0m`,
|
|
4
|
+
bold: `${ESC}1m`,
|
|
5
|
+
dim: `${ESC}2m`,
|
|
6
|
+
magenta: `${ESC}35m`,
|
|
7
|
+
blue: `${ESC}34m`,
|
|
8
|
+
cyan: `${ESC}36m`,
|
|
9
|
+
green: `${ESC}32m`,
|
|
10
|
+
yellow: `${ESC}33m`,
|
|
11
|
+
red: `${ESC}31m`,
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export const ui = {
|
|
15
|
+
log: (msg) => console.log(msg),
|
|
16
|
+
error: (msg) => console.log(`${colors.red}✖ ${msg}${colors.reset}`),
|
|
17
|
+
warn: (msg) => console.log(`${colors.yellow}⚠️ ${msg}${colors.reset}`),
|
|
18
|
+
success: (msg) => console.log(`${colors.green}✔ ${msg}${colors.reset}`),
|
|
19
|
+
info: (msg) => console.log(`${colors.cyan}ℹ ${msg}${colors.reset}`),
|
|
20
|
+
clearLine: () => process.stdout.write('\r\x1b[K'),
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export class Spinner {
|
|
24
|
+
constructor(text) {
|
|
25
|
+
this.text = text;
|
|
26
|
+
this.frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
27
|
+
this.index = 0;
|
|
28
|
+
this.timer = null;
|
|
29
|
+
}
|
|
30
|
+
start() {
|
|
31
|
+
this.timer = setInterval(() => {
|
|
32
|
+
process.stdout.write(`\r${colors.cyan}${this.frames[this.index]}${colors.reset} ${this.text}`);
|
|
33
|
+
this.index = (this.index + 1) % this.frames.length;
|
|
34
|
+
}, 80);
|
|
35
|
+
return this;
|
|
36
|
+
}
|
|
37
|
+
update(text) {
|
|
38
|
+
this.text = text;
|
|
39
|
+
}
|
|
40
|
+
stop(symbol = ' ', color = colors.reset) {
|
|
41
|
+
clearInterval(this.timer);
|
|
42
|
+
ui.clearLine();
|
|
43
|
+
}
|
|
44
|
+
succeed(text) {
|
|
45
|
+
this.stop();
|
|
46
|
+
console.log(`${colors.green}✔${colors.reset} ${text}`);
|
|
47
|
+
}
|
|
48
|
+
fail(text) {
|
|
49
|
+
this.stop();
|
|
50
|
+
console.log(`${colors.red}✖${colors.reset} ${text}`);
|
|
51
|
+
}
|
|
52
|
+
}
|