cluaupp 0.1.2 → 0.1.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/CHANGELOG.md +29 -1
- package/README.md +10 -13
- package/docs/README.md +13 -10
- package/docs/architecture.md +30 -73
- package/docs/cli.md +5 -6
- package/docs/comparison.md +3 -3
- package/docs/config.md +6 -6
- package/docs/cpp-advanced.md +10 -6
- package/docs/cpp-organization.md +4 -2
- package/docs/cpp-safety.md +11 -7
- package/docs/cpp-types.md +1 -1
- package/docs/examples/_category_.json +5 -0
- package/docs/examples/combat.md +239 -0
- package/docs/examples/data-boot.md +98 -0
- package/docs/examples/hud.md +96 -0
- package/docs/examples/index.md +43 -0
- package/docs/examples/leaderstats.md +140 -0
- package/docs/examples/shop.md +122 -0
- package/docs/examples/sword.md +108 -0
- package/docs/getting-started.md +14 -7
- package/docs/intellisense.md +31 -0
- package/docs/intro.md +3 -3
- package/docs/libraries/_category_.json +5 -0
- package/docs/libraries/dataservice.md +104 -0
- package/docs/libraries/index.md +22 -0
- package/docs/libraries/janitor.md +65 -0
- package/docs/libraries/more.md +79 -0
- package/docs/libraries/net.md +35 -0
- package/docs/libraries/promise.md +27 -0
- package/docs/oop/_category_.json +5 -0
- package/docs/oop/file-tags.md +48 -0
- package/docs/oop/index.md +22 -0
- package/docs/oop/modules.md +88 -0
- package/docs/oop/services.md +76 -0
- package/docs/print-cout.md +91 -0
- package/docs/syntax.md +63 -8
- package/editors/vscode/extension.js +146 -0
- package/editors/vscode/package.json +25 -0
- package/include/cluaupp/libs/janitor.hpp +7 -3
- package/include/cluaupp/roblox.hpp +19 -0
- package/package.json +66 -65
- package/runtime/Janitor/init.luau +4 -34
- package/src/architecture.js +174 -66
- package/src/cli.js +88 -21
- package/src/client/init.client.cpp +5 -0
- package/src/compile.js +77 -7
- package/src/editor-install.js +218 -0
- package/src/emit.js +321 -9
- package/src/headers.js +234 -0
- package/src/intellisense.js +1368 -0
- package/src/layout.js +129 -0
- package/src/lex.js +17 -9
- package/src/libs.js +165 -18
- package/src/lsp.js +227 -0
- package/src/parse.js +187 -6
- package/src/preprocess.js +118 -3
- package/src/server/leaderstats.server.cpp +28 -0
- package/src/shared/config.cpp +5 -0
- package/src/shared/config.h +3 -0
- package/src/understand.js +66 -6
- package/templates/game/.clangd +11 -0
- package/templates/game/.vscode/c_cpp_properties.json +6 -2
- package/templates/game/.vscode/extensions.json +6 -0
- package/templates/game/.vscode/settings.json +16 -2
- package/templates/game/cluaupp.config.json +2 -2
- package/templates/game/compile_flags.txt +2 -0
- package/templates/game/src/client/init.client.cpp +1 -0
- package/templates/game/src/server/leaderstats.server.cpp +1 -0
- package/docs/libraries.md +0 -80
package/src/cli.js
CHANGED
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
const fs = require("fs");
|
|
4
4
|
const path = require("path");
|
|
5
5
|
const { compileService } = require("./compile");
|
|
6
|
-
const { isSourceFile,
|
|
6
|
+
const { isSourceFile, toLuauPath } = require("./preprocess");
|
|
7
|
+
const { syncEditorSupport, installEditorSupport } = require("./intellisense");
|
|
7
8
|
const pkg = require("../package.json");
|
|
8
9
|
|
|
9
10
|
function printHelp() {
|
|
@@ -11,8 +12,10 @@ function printHelp() {
|
|
|
11
12
|
|
|
12
13
|
Usage:
|
|
13
14
|
cluaupp init [folder] create a game (src/server, src/client, src/shared)
|
|
14
|
-
cluaupp build [folder] transpile src → out (
|
|
15
|
+
cluaupp build [folder] transpile src → out (one file per .cpp)
|
|
15
16
|
cluaupp watch [folder] rebuild on save
|
|
17
|
+
cluaupp lsp [folder] language server (stdio JSON-RPC)
|
|
18
|
+
cluaupp intellisense install Cursor/VS Code IntelliSense
|
|
16
19
|
cluaupp --version print version
|
|
17
20
|
cluaupp --help this help
|
|
18
21
|
|
|
@@ -107,10 +110,10 @@ function loadConfig(root) {
|
|
|
107
110
|
for (const name of configNames) {
|
|
108
111
|
const file = path.join(root, name);
|
|
109
112
|
if (fs.existsSync(file)) {
|
|
110
|
-
return { rootDir: "src", outDir: "out", strict:
|
|
113
|
+
return { rootDir: "src", outDir: "out", strict: false, architecture: false, ...JSON.parse(fs.readFileSync(file, "utf8")) };
|
|
111
114
|
}
|
|
112
115
|
}
|
|
113
|
-
return { rootDir: "src", outDir: "out", strict:
|
|
116
|
+
return { rootDir: "src", outDir: "out", strict: false, architecture: false };
|
|
114
117
|
}
|
|
115
118
|
|
|
116
119
|
function collectCpp(dir, files = []) {
|
|
@@ -143,10 +146,6 @@ function collectFiles(dir, files = []) {
|
|
|
143
146
|
return files;
|
|
144
147
|
}
|
|
145
148
|
|
|
146
|
-
function siblingCpp(file) {
|
|
147
|
-
return file.replace(/\.(h|hpp|hh)$/i, ".cpp");
|
|
148
|
-
}
|
|
149
|
-
|
|
150
149
|
function resolveKey(file) {
|
|
151
150
|
const resolved = path.resolve(file);
|
|
152
151
|
return process.platform === "win32" ? resolved.toLowerCase() : resolved;
|
|
@@ -220,14 +219,10 @@ function pruneOut(root, config, written, failedPrefixes) {
|
|
|
220
219
|
function compileProject(root, config) {
|
|
221
220
|
const srcDir = path.join(root, config.rootDir);
|
|
222
221
|
const files = collectCpp(srcDir);
|
|
223
|
-
const fileSet = new Set(files.map((file) => path.resolve(file)));
|
|
224
222
|
const jobs = [];
|
|
225
223
|
const errors = [];
|
|
226
224
|
|
|
227
225
|
for (const file of files) {
|
|
228
|
-
if (isHeaderFile(file) && fileSet.has(path.resolve(siblingCpp(file)))) {
|
|
229
|
-
continue;
|
|
230
|
-
}
|
|
231
226
|
const source = fs.readFileSync(file, "utf8");
|
|
232
227
|
const rel = posixRel(srcDir, file);
|
|
233
228
|
try {
|
|
@@ -236,8 +231,9 @@ function compileProject(root, config) {
|
|
|
236
231
|
filePath: file,
|
|
237
232
|
relativeName: rel,
|
|
238
233
|
outName: toLuauPath(rel),
|
|
239
|
-
architecture: config.architecture
|
|
234
|
+
architecture: config.architecture === true,
|
|
240
235
|
includeDirs: [path.dirname(file), srcDir, path.join(root, "include")],
|
|
236
|
+
srcDir,
|
|
241
237
|
});
|
|
242
238
|
jobs.push({ rel, files: result.files, stale: result.stale || [] });
|
|
243
239
|
} catch (err) {
|
|
@@ -293,12 +289,8 @@ function ensureVendor(root) {
|
|
|
293
289
|
function build(root, options = {}) {
|
|
294
290
|
const exitOnError = options.exitOnError !== false;
|
|
295
291
|
const holdOnError = options.holdOnError === true;
|
|
296
|
-
const syncVendor = options.syncVendor === true;
|
|
297
292
|
const config = loadConfig(root);
|
|
298
|
-
if (syncVendor) {
|
|
299
|
-
copyRuntime(root);
|
|
300
|
-
copyHeaders(root);
|
|
301
|
-
} else {
|
|
293
|
+
if (options.syncVendor !== false) {
|
|
302
294
|
ensureVendor(root);
|
|
303
295
|
}
|
|
304
296
|
const srcDir = path.join(root, config.rootDir);
|
|
@@ -320,6 +312,11 @@ function build(root, options = {}) {
|
|
|
320
312
|
}
|
|
321
313
|
if (holdOnError) {
|
|
322
314
|
console.error(`cluaupp: out not updated (${compiled.errors.length} compile error${compiled.errors.length === 1 ? "" : "s"})`);
|
|
315
|
+
try {
|
|
316
|
+
syncEditorSupport(root, config);
|
|
317
|
+
} catch (err) {
|
|
318
|
+
console.error("cluaupp: intellisense sync failed", err.message);
|
|
319
|
+
}
|
|
323
320
|
return { failed: compiled.errors.length, written: new Set() };
|
|
324
321
|
}
|
|
325
322
|
}
|
|
@@ -327,24 +324,74 @@ function build(root, options = {}) {
|
|
|
327
324
|
const written = writeJobs(root, config, compiled.jobs);
|
|
328
325
|
const failedPrefixes = compiled.errors.flatMap((err) => err.prefixes);
|
|
329
326
|
pruneOut(root, config, written, failedPrefixes);
|
|
327
|
+
try {
|
|
328
|
+
syncEditorSupport(root, config);
|
|
329
|
+
} catch (err) {
|
|
330
|
+
console.error("cluaupp: intellisense sync failed", err.message);
|
|
331
|
+
}
|
|
330
332
|
if (compiled.errors.length > 0 && exitOnError) {
|
|
331
333
|
process.exit(1);
|
|
332
334
|
}
|
|
333
335
|
return { failed: compiled.errors.length, written };
|
|
334
336
|
}
|
|
335
337
|
|
|
336
|
-
function init(dest) {
|
|
338
|
+
async function init(dest) {
|
|
337
339
|
const template = path.join(__dirname, "..", "templates", "game");
|
|
338
340
|
const include = path.join(__dirname, "..", "include");
|
|
339
341
|
copyDir(template, dest);
|
|
340
342
|
copyDir(include, path.join(dest, "include"));
|
|
341
343
|
copyRuntime(dest);
|
|
344
|
+
syncEditorSupport(dest);
|
|
345
|
+
const installed = await installEditorSupport();
|
|
342
346
|
console.log("Cluaupp ready in", dest);
|
|
343
347
|
console.log(" cluaupp build");
|
|
344
348
|
console.log(" rojo serve");
|
|
349
|
+
if (installed.local.length || (installed.cpp && installed.cpp.status === "installed")) {
|
|
350
|
+
console.log(" reload Cursor (Ctrl+Shift+P → Developer: Reload Window)");
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function pidAlive(pid) {
|
|
355
|
+
try {
|
|
356
|
+
process.kill(pid, 0);
|
|
357
|
+
return true;
|
|
358
|
+
} catch {
|
|
359
|
+
return false;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function acquireWatchLock(root) {
|
|
364
|
+
const file = path.join(root, ".cluaupp-watch.lock");
|
|
365
|
+
if (fs.existsSync(file)) {
|
|
366
|
+
const pid = Number(fs.readFileSync(file, "utf8").trim());
|
|
367
|
+
if (Number.isFinite(pid) && pid !== process.pid && pidAlive(pid)) {
|
|
368
|
+
console.error(`cluaupp: watch already running (pid ${pid}). Stop it first — two watchers make Rojo crash on libs/.`);
|
|
369
|
+
process.exit(1);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
fs.writeFileSync(file, String(process.pid), "utf8");
|
|
373
|
+
const release = () => {
|
|
374
|
+
try {
|
|
375
|
+
if (fs.existsSync(file) && fs.readFileSync(file, "utf8").trim() === String(process.pid)) {
|
|
376
|
+
fs.unlinkSync(file);
|
|
377
|
+
}
|
|
378
|
+
} catch {
|
|
379
|
+
// ignore
|
|
380
|
+
}
|
|
381
|
+
};
|
|
382
|
+
process.on("exit", release);
|
|
383
|
+
process.on("SIGINT", () => {
|
|
384
|
+
release();
|
|
385
|
+
process.exit(0);
|
|
386
|
+
});
|
|
387
|
+
process.on("SIGTERM", () => {
|
|
388
|
+
release();
|
|
389
|
+
process.exit(0);
|
|
390
|
+
});
|
|
345
391
|
}
|
|
346
392
|
|
|
347
393
|
function watch(root) {
|
|
394
|
+
acquireWatchLock(root);
|
|
348
395
|
build(root, { exitOnError: false, syncVendor: false, holdOnError: true });
|
|
349
396
|
const config = loadConfig(root);
|
|
350
397
|
const dir = path.join(root, config.rootDir);
|
|
@@ -371,7 +418,7 @@ function watch(root) {
|
|
|
371
418
|
}
|
|
372
419
|
};
|
|
373
420
|
|
|
374
|
-
console.log("watching", dir);
|
|
421
|
+
console.log("cluaupp", pkg.version, "watching", dir, "(libs untouched)");
|
|
375
422
|
if (!fs.existsSync(dir)) {
|
|
376
423
|
fs.mkdirSync(dir, { recursive: true });
|
|
377
424
|
}
|
|
@@ -385,11 +432,31 @@ function dispatch(args) {
|
|
|
385
432
|
const cmd = args[0] || "help";
|
|
386
433
|
const cwd = process.cwd();
|
|
387
434
|
if (cmd === "init") {
|
|
388
|
-
init(path.resolve(cwd, args[1] || "."))
|
|
435
|
+
return init(path.resolve(cwd, args[1] || ".")).catch((err) => {
|
|
436
|
+
console.error(err.message || err);
|
|
437
|
+
process.exit(1);
|
|
438
|
+
});
|
|
389
439
|
} else if (cmd === "build") {
|
|
390
440
|
build(path.resolve(cwd, args[1] || "."));
|
|
391
441
|
} else if (cmd === "watch") {
|
|
392
442
|
watch(path.resolve(cwd, args[1] || "."));
|
|
443
|
+
} else if (cmd === "lsp") {
|
|
444
|
+
const { start } = require("./lsp");
|
|
445
|
+
start({ projectRoot: path.resolve(cwd, args[1] || ".") });
|
|
446
|
+
} else if (cmd === "intellisense" || cmd === "intelisense") {
|
|
447
|
+
const root = path.resolve(cwd, args[1] || ".");
|
|
448
|
+
syncEditorSupport(root, loadConfig(root));
|
|
449
|
+
return installEditorSupport().then((installed) => {
|
|
450
|
+
console.log("cluaupp: compile_commands.json, .clangd, and .vscode updated in", root);
|
|
451
|
+
if (installed.local.length) {
|
|
452
|
+
for (const dest of installed.local) {
|
|
453
|
+
console.log("cluaupp: Cluaupp IntelliSense", dest);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
}).catch((err) => {
|
|
457
|
+
console.error(err.message || err);
|
|
458
|
+
process.exit(1);
|
|
459
|
+
});
|
|
393
460
|
} else if (cmd === "--version" || cmd === "-v" || cmd === "version") {
|
|
394
461
|
console.log(pkg.version);
|
|
395
462
|
} else {
|
package/src/compile.js
CHANGED
|
@@ -1,30 +1,100 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
|
+
const path = require("path");
|
|
3
4
|
const { parse } = require("./parse");
|
|
4
5
|
const { emit } = require("./emit");
|
|
5
|
-
const { preprocess } = require("./preprocess");
|
|
6
|
-
const { collectLibraries,
|
|
6
|
+
const { preprocess, isHeaderFile, toLuauPath, siblingImplementation, siblingHeader, implOutName } = require("./preprocess");
|
|
7
|
+
const { collectLibraries, insertPreamble } = require("./libs");
|
|
7
8
|
const { planOutput } = require("./architecture");
|
|
9
|
+
const { emitHeaderModule, primaryHeaderName } = require("./headers");
|
|
10
|
+
|
|
11
|
+
function shouldAttachRequires(name) {
|
|
12
|
+
return !/\.(json)$/i.test(String(name || ""));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function attachRequires(contents, source, ast, options, outName) {
|
|
16
|
+
if (!shouldAttachRequires(outName)) {
|
|
17
|
+
return contents;
|
|
18
|
+
}
|
|
19
|
+
const libraries = String(contents).includes("CluauppLibs") ? [] : collectLibraries(source, ast);
|
|
20
|
+
return insertPreamble(contents, options.moduleIncludes || [], libraries, outName);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function posixRel(from, file) {
|
|
24
|
+
return path.relative(from, file).replace(/\\/g, "/");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function wireHeaderImpl(ast, fileName, options) {
|
|
28
|
+
const headerPath = options.filePath || null;
|
|
29
|
+
const impl = headerPath ? siblingImplementation(headerPath) : null;
|
|
30
|
+
if (!impl) {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
const typeName = primaryHeaderName(ast, options.relativeName || fileName);
|
|
34
|
+
const implRel = options.srcDir ? posixRel(options.srcDir, impl) : path.basename(impl).replace(/\\/g, "/");
|
|
35
|
+
const implOut = implOutName(implRel);
|
|
36
|
+
options.headerImplName = `${typeName}Module`;
|
|
37
|
+
options.moduleIncludes = options.moduleIncludes || [];
|
|
38
|
+
if (!options.moduleIncludes.some((spec) => spec.name === options.headerImplName)) {
|
|
39
|
+
options.moduleIncludes.push({
|
|
40
|
+
name: options.headerImplName,
|
|
41
|
+
outRel: implOut,
|
|
42
|
+
exports: { consts: [], structs: [] },
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function compileHeader(source, fileName, options, prepared, ast) {
|
|
48
|
+
wireHeaderImpl(ast, fileName, options);
|
|
49
|
+
const rel = (options.relativeName || fileName || "header.h").replace(/\\/g, "/");
|
|
50
|
+
const outName = toLuauPath(rel);
|
|
51
|
+
const luau = attachRequires(emitHeaderModule(ast, options), source, ast, options, outName);
|
|
52
|
+
return {
|
|
53
|
+
kind: "module",
|
|
54
|
+
plan: { kind: "module", strict: options.strict === true },
|
|
55
|
+
files: [{ name: outName, contents: luau }],
|
|
56
|
+
stale: [],
|
|
57
|
+
};
|
|
58
|
+
}
|
|
8
59
|
|
|
9
60
|
function compileSource(source, fileName, options = {}) {
|
|
10
|
-
|
|
61
|
+
options.moduleIncludes = options.moduleIncludes || [];
|
|
62
|
+
const prepared = preprocess(source, options.filePath || null, options);
|
|
11
63
|
const ast = parse(prepared, fileName || "input.cpp");
|
|
64
|
+
if (isHeaderFile(fileName) || isHeaderFile(options.filePath || "")) {
|
|
65
|
+
return compileHeader(source, fileName, options, prepared, ast).files[0].contents;
|
|
66
|
+
}
|
|
12
67
|
const luau = emit(ast, options);
|
|
13
|
-
return
|
|
68
|
+
return attachRequires(luau, source, ast, options, options.outName || fileName);
|
|
14
69
|
}
|
|
15
70
|
|
|
16
71
|
function compileService(source, fileName, options = {}) {
|
|
17
|
-
|
|
72
|
+
options.moduleIncludes = options.moduleIncludes || [];
|
|
73
|
+
const prepared = preprocess(source, options.filePath || null, options);
|
|
18
74
|
const ast = parse(prepared, fileName || "input.cpp");
|
|
75
|
+
if (isHeaderFile(fileName) || isHeaderFile(options.filePath || "") || isHeaderFile(options.relativeName || "")) {
|
|
76
|
+
return compileHeader(source, fileName, options, prepared, ast);
|
|
77
|
+
}
|
|
78
|
+
const implHeader = options.filePath ? siblingHeader(options.filePath) : null;
|
|
79
|
+
if (implHeader) {
|
|
80
|
+
options.skipInit = true;
|
|
81
|
+
}
|
|
19
82
|
const planned = planOutput(ast, fileName || "input.cpp", {
|
|
20
83
|
...options,
|
|
21
84
|
relativeName: options.relativeName || fileName,
|
|
22
85
|
source: prepared,
|
|
86
|
+
siblingHeader: implHeader,
|
|
23
87
|
});
|
|
24
88
|
if (planned.files) {
|
|
25
|
-
return
|
|
89
|
+
return {
|
|
90
|
+
...planned,
|
|
91
|
+
files: planned.files.map((file) => ({
|
|
92
|
+
...file,
|
|
93
|
+
contents: attachRequires(file.contents, source, ast, options, file.name),
|
|
94
|
+
})),
|
|
95
|
+
};
|
|
26
96
|
}
|
|
27
|
-
const luau =
|
|
97
|
+
const luau = attachRequires(emit(ast, options), source, ast, options, planned.outName || options.outName || fileName);
|
|
28
98
|
const name =
|
|
29
99
|
planned.outName ||
|
|
30
100
|
options.outName ||
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const fs = require("fs");
|
|
4
|
+
const os = require("os");
|
|
5
|
+
const path = require("path");
|
|
6
|
+
const https = require("https");
|
|
7
|
+
const { spawnSync } = require("child_process");
|
|
8
|
+
|
|
9
|
+
const CPPTOOLS_ID = "ms-vscode.cpptools";
|
|
10
|
+
const GITHUB_LATEST = "https://api.github.com/repos/microsoft/vscode-cpptools/releases/latest";
|
|
11
|
+
|
|
12
|
+
function which(command) {
|
|
13
|
+
const finder = process.platform === "win32" ? "where" : "which";
|
|
14
|
+
const result = spawnSync(finder, [command], { encoding: "utf8", windowsHide: true });
|
|
15
|
+
if (result.status !== 0) {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
return String(result.stdout || "")
|
|
19
|
+
.split(/\r?\n/)
|
|
20
|
+
.map((line) => line.trim())
|
|
21
|
+
.find((line) => line && !line.toLowerCase().includes("info:"));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function editorBin() {
|
|
25
|
+
const found = which("cursor") || which("code");
|
|
26
|
+
if (found) {
|
|
27
|
+
return found;
|
|
28
|
+
}
|
|
29
|
+
const local = process.env.LOCALAPPDATA || "";
|
|
30
|
+
const fallbacks = [
|
|
31
|
+
path.join(local, "Programs", "cursor", "resources", "app", "bin", "cursor.cmd"),
|
|
32
|
+
path.join(local, "Programs", "Microsoft VS Code", "bin", "code.cmd"),
|
|
33
|
+
"/usr/bin/cursor",
|
|
34
|
+
"/usr/local/bin/cursor",
|
|
35
|
+
"/usr/bin/code",
|
|
36
|
+
"/usr/local/bin/code",
|
|
37
|
+
];
|
|
38
|
+
return fallbacks.find((file) => fs.existsSync(file)) || null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function extensionHomes() {
|
|
42
|
+
return [
|
|
43
|
+
path.join(os.homedir(), ".cursor", "extensions"),
|
|
44
|
+
path.join(os.homedir(), ".vscode", "extensions"),
|
|
45
|
+
];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function isCppToolsInstalled() {
|
|
49
|
+
for (const home of extensionHomes()) {
|
|
50
|
+
if (!fs.existsSync(home)) {
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
try {
|
|
54
|
+
if (fs.readdirSync(home).some((name) => name.startsWith(CPPTOOLS_ID + "-"))) {
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
} catch {
|
|
58
|
+
// ignore
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
const bin = editorBin();
|
|
62
|
+
if (!bin) {
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
const listed = spawnSync(bin, ["--list-extensions"], { encoding: "utf8", windowsHide: true, timeout: 20000 });
|
|
66
|
+
return String(listed.stdout || "").split(/\r?\n/).includes(CPPTOOLS_ID);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function vsixAssetName(platform = process.platform, arch = process.arch) {
|
|
70
|
+
const cpu = arch === "arm64" ? "arm64" : "x64";
|
|
71
|
+
if (platform === "win32") {
|
|
72
|
+
return `cpptools-windows-${cpu}.vsix`;
|
|
73
|
+
}
|
|
74
|
+
if (platform === "darwin") {
|
|
75
|
+
return `cpptools-macOS-${cpu}.vsix`;
|
|
76
|
+
}
|
|
77
|
+
return `cpptools-linux-${cpu}.vsix`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function httpsJson(url) {
|
|
81
|
+
return new Promise((resolve, reject) => {
|
|
82
|
+
const request = https.get(
|
|
83
|
+
url,
|
|
84
|
+
{ headers: { "User-Agent": "cluaupp", Accept: "application/vnd.github+json" } },
|
|
85
|
+
(response) => {
|
|
86
|
+
if (response.statusCode >= 300 && response.statusCode < 400 && response.headers.location) {
|
|
87
|
+
response.resume();
|
|
88
|
+
httpsJson(response.headers.location).then(resolve, reject);
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
const chunks = [];
|
|
92
|
+
response.on("data", (chunk) => chunks.push(chunk));
|
|
93
|
+
response.on("end", () => {
|
|
94
|
+
if (response.statusCode !== 200) {
|
|
95
|
+
reject(new Error("GitHub " + response.statusCode));
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
try {
|
|
99
|
+
resolve(JSON.parse(Buffer.concat(chunks).toString("utf8")));
|
|
100
|
+
} catch (err) {
|
|
101
|
+
reject(err);
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
},
|
|
105
|
+
);
|
|
106
|
+
request.on("error", reject);
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function downloadFile(url, dest) {
|
|
111
|
+
return new Promise((resolve, reject) => {
|
|
112
|
+
const follow = (current) => {
|
|
113
|
+
https
|
|
114
|
+
.get(current, { headers: { "User-Agent": "cluaupp" } }, (response) => {
|
|
115
|
+
if (response.statusCode >= 300 && response.statusCode < 400 && response.headers.location) {
|
|
116
|
+
response.resume();
|
|
117
|
+
follow(response.headers.location);
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
if (response.statusCode !== 200) {
|
|
121
|
+
reject(new Error("download failed " + response.statusCode));
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
const out = fs.createWriteStream(dest);
|
|
125
|
+
response.pipe(out);
|
|
126
|
+
out.on("finish", () => out.close(() => resolve(dest)));
|
|
127
|
+
out.on("error", reject);
|
|
128
|
+
})
|
|
129
|
+
.on("error", reject);
|
|
130
|
+
};
|
|
131
|
+
follow(url);
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async function downloadCppToolsVsix() {
|
|
136
|
+
const name = vsixAssetName();
|
|
137
|
+
const dest = path.join(os.tmpdir(), name);
|
|
138
|
+
if (fs.existsSync(dest) && fs.statSync(dest).size > 1_000_000) {
|
|
139
|
+
return dest;
|
|
140
|
+
}
|
|
141
|
+
const release = await httpsJson(GITHUB_LATEST);
|
|
142
|
+
const asset = (release.assets || []).find((item) => item.name === name);
|
|
143
|
+
if (!asset || !asset.browser_download_url) {
|
|
144
|
+
throw new Error("no " + name + " in vscode-cpptools " + (release.tag_name || "latest"));
|
|
145
|
+
}
|
|
146
|
+
console.log("cluaupp: downloading", name, release.tag_name || "");
|
|
147
|
+
return downloadFile(asset.browser_download_url, dest);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function spawnInstall(bin, target) {
|
|
151
|
+
const result = spawnSync(bin, ["--install-extension", target, "--force"], {
|
|
152
|
+
encoding: "utf8",
|
|
153
|
+
windowsHide: true,
|
|
154
|
+
timeout: 180000,
|
|
155
|
+
});
|
|
156
|
+
return {
|
|
157
|
+
ok: result.status === 0 && !/not found/i.test(String(result.stderr || "") + String(result.stdout || "")),
|
|
158
|
+
stdout: String(result.stdout || ""),
|
|
159
|
+
stderr: String(result.stderr || ""),
|
|
160
|
+
status: result.status,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async function installCppTools() {
|
|
165
|
+
if (isCppToolsInstalled()) {
|
|
166
|
+
return { status: "already", id: CPPTOOLS_ID };
|
|
167
|
+
}
|
|
168
|
+
const bin = editorBin();
|
|
169
|
+
if (!bin) {
|
|
170
|
+
return { status: "missing-editor", id: CPPTOOLS_ID };
|
|
171
|
+
}
|
|
172
|
+
console.log("cluaupp: installing", CPPTOOLS_ID);
|
|
173
|
+
const marketplace = spawnInstall(bin, CPPTOOLS_ID);
|
|
174
|
+
if (marketplace.ok) {
|
|
175
|
+
return { status: "installed", id: CPPTOOLS_ID, how: "marketplace" };
|
|
176
|
+
}
|
|
177
|
+
try {
|
|
178
|
+
const vsix = await downloadCppToolsVsix();
|
|
179
|
+
const fromFile = spawnInstall(bin, vsix);
|
|
180
|
+
if (fromFile.ok) {
|
|
181
|
+
return { status: "installed", id: CPPTOOLS_ID, how: "vsix", file: vsix };
|
|
182
|
+
}
|
|
183
|
+
return { status: "failed", id: CPPTOOLS_ID, detail: fromFile.stderr || fromFile.stdout || marketplace.stderr };
|
|
184
|
+
} catch (err) {
|
|
185
|
+
return { status: "failed", id: CPPTOOLS_ID, detail: err.message };
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function reportCppTools(result) {
|
|
190
|
+
if (!result) {
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
if (result.status === "already") {
|
|
194
|
+
console.log("cluaupp: Microsoft C/C++ already installed");
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
if (result.status === "installed") {
|
|
198
|
+
console.log("cluaupp: installed", CPPTOOLS_ID, result.how === "vsix" ? "(VSIX — Cursor marketplace does not list it)" : "");
|
|
199
|
+
console.log("cluaupp: reload the editor (Ctrl+Shift+P → Developer: Reload Window)");
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
if (result.status === "missing-editor") {
|
|
203
|
+
console.error("cluaupp: Cursor/VS Code CLI not found. Install ms-vscode.cpptools from the VSIX at");
|
|
204
|
+
console.error(" https://github.com/microsoft/vscode-cpptools/releases");
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
console.error("cluaupp: could not install", CPPTOOLS_ID + ":", result.detail || "unknown error");
|
|
208
|
+
console.error(" download https://github.com/microsoft/vscode-cpptools/releases and: cursor --install-extension cpptools-windows-x64.vsix");
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
module.exports = {
|
|
212
|
+
CPPTOOLS_ID,
|
|
213
|
+
editorBin,
|
|
214
|
+
isCppToolsInstalled,
|
|
215
|
+
vsixAssetName,
|
|
216
|
+
installCppTools,
|
|
217
|
+
reportCppTools,
|
|
218
|
+
};
|