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.
Files changed (69) hide show
  1. package/CHANGELOG.md +29 -1
  2. package/README.md +10 -13
  3. package/docs/README.md +13 -10
  4. package/docs/architecture.md +30 -73
  5. package/docs/cli.md +5 -6
  6. package/docs/comparison.md +3 -3
  7. package/docs/config.md +6 -6
  8. package/docs/cpp-advanced.md +10 -6
  9. package/docs/cpp-organization.md +4 -2
  10. package/docs/cpp-safety.md +11 -7
  11. package/docs/cpp-types.md +1 -1
  12. package/docs/examples/_category_.json +5 -0
  13. package/docs/examples/combat.md +239 -0
  14. package/docs/examples/data-boot.md +98 -0
  15. package/docs/examples/hud.md +96 -0
  16. package/docs/examples/index.md +43 -0
  17. package/docs/examples/leaderstats.md +140 -0
  18. package/docs/examples/shop.md +122 -0
  19. package/docs/examples/sword.md +108 -0
  20. package/docs/getting-started.md +14 -7
  21. package/docs/intellisense.md +31 -0
  22. package/docs/intro.md +3 -3
  23. package/docs/libraries/_category_.json +5 -0
  24. package/docs/libraries/dataservice.md +104 -0
  25. package/docs/libraries/index.md +22 -0
  26. package/docs/libraries/janitor.md +65 -0
  27. package/docs/libraries/more.md +79 -0
  28. package/docs/libraries/net.md +35 -0
  29. package/docs/libraries/promise.md +27 -0
  30. package/docs/oop/_category_.json +5 -0
  31. package/docs/oop/file-tags.md +48 -0
  32. package/docs/oop/index.md +22 -0
  33. package/docs/oop/modules.md +88 -0
  34. package/docs/oop/services.md +76 -0
  35. package/docs/print-cout.md +91 -0
  36. package/docs/syntax.md +63 -8
  37. package/editors/vscode/extension.js +146 -0
  38. package/editors/vscode/package.json +25 -0
  39. package/include/cluaupp/libs/janitor.hpp +7 -3
  40. package/include/cluaupp/roblox.hpp +19 -0
  41. package/package.json +66 -65
  42. package/runtime/Janitor/init.luau +4 -34
  43. package/src/architecture.js +174 -66
  44. package/src/cli.js +88 -21
  45. package/src/client/init.client.cpp +5 -0
  46. package/src/compile.js +77 -7
  47. package/src/editor-install.js +218 -0
  48. package/src/emit.js +321 -9
  49. package/src/headers.js +234 -0
  50. package/src/intellisense.js +1368 -0
  51. package/src/layout.js +129 -0
  52. package/src/lex.js +17 -9
  53. package/src/libs.js +165 -18
  54. package/src/lsp.js +227 -0
  55. package/src/parse.js +187 -6
  56. package/src/preprocess.js +118 -3
  57. package/src/server/leaderstats.server.cpp +28 -0
  58. package/src/shared/config.cpp +5 -0
  59. package/src/shared/config.h +3 -0
  60. package/src/understand.js +66 -6
  61. package/templates/game/.clangd +11 -0
  62. package/templates/game/.vscode/c_cpp_properties.json +6 -2
  63. package/templates/game/.vscode/extensions.json +6 -0
  64. package/templates/game/.vscode/settings.json +16 -2
  65. package/templates/game/cluaupp.config.json +2 -2
  66. package/templates/game/compile_flags.txt +2 -0
  67. package/templates/game/src/client/init.client.cpp +1 -0
  68. package/templates/game/src/server/leaderstats.server.cpp +1 -0
  69. 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, isHeaderFile, toLuauPath } = require("./preprocess");
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 (PascalCase services)
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: true, ...JSON.parse(fs.readFileSync(file, "utf8")) };
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: true };
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 !== false,
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 {
@@ -0,0 +1,5 @@
1
+ #include <cluaupp/roblox.hpp>
2
+
3
+ void init() {
4
+ print("Cluaupp client ok");
5
+ }
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, insertRequires } = require("./libs");
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
- const prepared = options.filePath ? preprocess(source, options.filePath, options) : source;
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 insertRequires(luau, collectLibraries(source, ast));
68
+ return attachRequires(luau, source, ast, options, options.outName || fileName);
14
69
  }
15
70
 
16
71
  function compileService(source, fileName, options = {}) {
17
- const prepared = options.filePath ? preprocess(source, options.filePath, options) : source;
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 planned;
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 = insertRequires(emit(ast, options), collectLibraries(source, ast));
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
+ };