cpoach.sh 0.2.4 → 0.3.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/index.js CHANGED
@@ -1,7 +1,170 @@
1
+ #!/usr/bin/env node
1
2
  const fs = require('fs');
3
+ const os = require('os');
2
4
  const path = require('path');
5
+ const cp = require('child_process');
6
+ const readline = require('readline');
3
7
 
8
+ const PRE = '# 🍳 ';
4
9
 
10
+
11
+
12
+
13
+ //#region UTILS
14
+ // Escape special characters in a string for use in a regular expression.
15
+ function escapeRegExp(pat) {
16
+ return pat.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
17
+ }
18
+
19
+
20
+ // Read a text file synchronously and normalize line endings to LF.
21
+ function readTextFileSync(pth) {
22
+ const data = fs.readFileSync(pth, 'utf8');
23
+ return data.replace(/\r?\n|\r/g, '\n');
24
+ }
25
+
26
+
27
+ // Write a text file synchronously and normalize line endings to LF.
28
+ function writeTextFileSync(pth, text) {
29
+ const data = text.replace(/\r?\n|\r/g, os.EOL);
30
+ fs.writeFileSync(pth, data, 'utf8');
31
+ }
32
+
33
+
34
+ // Read a JSON file synchronously and parse it.
35
+ function readJSONFileSync(pth) {
36
+ const text = readTextFileSync(pth);
37
+ return JSON.parse(text);
38
+ }
39
+
40
+
41
+ // Write a JSON file synchronously and stringify it with indentation.
42
+ function writeJSONFileSync(pth, obj) {
43
+ const text = JSON.stringify(obj, null, 2) + '\n';
44
+ writeTextFileSync(pth, text);
45
+ }
46
+
47
+
48
+ // Prompt the user for input.
49
+ function prompt(query, def='') {
50
+ const rl = readline.createInterface({
51
+ input: process.stdin,
52
+ output: process.stdout
53
+ });
54
+ return new Promise(resolve => {
55
+ let q = def? `${query} [${def}]: ` : `${query}: `;
56
+ rl.question(q, answer => {
57
+ rl.close();
58
+ resolve(answer.trim()? answer : def);
59
+ });
60
+ });
61
+ }
62
+ //#endregion
63
+
64
+
65
+
66
+
67
+ //#region SECTION COMMANDS
68
+ // Remove excess section gaps (> 2).
69
+ function sectionTrim(data, prefix) {
70
+ const pre = escapeRegExp(prefix);
71
+ const re = new RegExp(`^${pre}endregion\n{4,}${pre}region.*?`, 'gm');
72
+ return data.replace(re, `${prefix}endregion\n\n\n${prefix}region`).trim() + '\n';
73
+ }
74
+
75
+
76
+ // Extract a section from a text file based on region markers.
77
+ function sectionExtract(data, prefix, name) {
78
+ const pre = escapeRegExp(prefix);
79
+ const re = new RegExp(`^${pre}region ${name}$([\\s\\S]*?)^${pre}endregion$`, 'g');
80
+ const m = re.exec(data);
81
+ return m? m[1].trim() : null;
82
+ }
83
+
84
+
85
+ // Remove a section from a text file based on region markers.
86
+ function sectionRemove(data, prefix, name) {
87
+ const pre = escapeRegExp(prefix);
88
+ const re = new RegExp(`^${pre}region ${name}$[\\s\\S]*?^${pre}endregion$`, 'g');
89
+ return data.replace(re, '');
90
+ }
91
+
92
+
93
+ // Add a section to a text file based on region markers (if it doesn't already exist).
94
+ function sectionAdd(data, prefix, name, content) {
95
+ if (sectionExists(data, prefix, name)) return data;
96
+ return data.trim() +
97
+ `\n\n\n` +
98
+ `${prefix}region ${name}\n` +
99
+ `${content.trim()}\n` +
100
+ `${prefix}endregion\n`;
101
+ }
102
+
103
+
104
+ // Replace a section in a text file based on region markers.
105
+ function sectionReplace(data, prefix, name, content) {
106
+ const pre = escapeRegExp(prefix);
107
+ const re = new RegExp(`^${pre}region ${name}$[\\s\\S]*?^${pre}endregion$`, 'g');
108
+ return data.replace(re,
109
+ `${prefix}region ${name}\n` +
110
+ `${content.trim()}\n` +
111
+ `${prefix}endregion\n`
112
+ );
113
+ }
114
+ //#endregion
115
+
116
+
117
+
118
+
119
+ //#region VERSION COMMAND
120
+ // Fetch the current version of the tool from package.json.
121
+ function fetchVersion() {
122
+ const p = readJSONFileSync(path.join(__dirname, 'package.json'));
123
+ return p.version || '0.0.0';
124
+ }
125
+
126
+
127
+ // Run the `version` command, which displays the current version of the tool.
128
+ function runVersion() {
129
+ console.error(`cpoach version ${fetchVersion()}\n`);
130
+ }
131
+ //#endregion
132
+
133
+
134
+
135
+
136
+ //#region HELP COMMAND
137
+ // Run the help command, which displays usage information.
138
+ function runHelp() {
139
+ console.error(
140
+ `cpoach - A C/C++ package manager using the npm registry (v${fetchVersion()})\n` +
141
+ `\n` +
142
+ `Usage: cpoach [command] [options]\n` +
143
+ `\n` +
144
+ `Commands:\n` +
145
+ ` init Initialize a new C/C++ project.\n` +
146
+ ` install Install dependencies.\n` +
147
+ ` build Build the project.\n` +
148
+ ` run Run the executable.\n` +
149
+ ` config Configure dependencies.\n` +
150
+ ` format Format code.\n` +
151
+ ` sanitize Run sanitizers.\n` +
152
+ ` lint Lint code.\n` +
153
+ ` i | includes Generate compiler flags for include paths.\n` +
154
+ `\n` +
155
+ `Options:\n` +
156
+ ` --compiler [name] Specify the compiler (msvc, gcc, clang). Default is gcc.\n` +
157
+ ` --msvc Shortcut for --compiler msvc.\n` +
158
+ ` --gcc Shortcut for --compiler gcc.\n` +
159
+ ` --clang Shortcut for --compiler clang.\n`
160
+ );
161
+ }
162
+ //#endregion
163
+
164
+
165
+
166
+
167
+ //#region INCLUDES COMMAND
5
168
  // Run the `includes` command, which generates compiler flags for include paths.
6
169
  function runIncludes(opt) {
7
170
  const includes = [];
@@ -14,40 +177,205 @@ function runIncludes(opt) {
14
177
  const FLAG = opt.compiler==='msvc'? '/I' : '-I';
15
178
  console.log(includes.map(pth => `${FLAG}"${pth}"`).join(' ').trim());
16
179
  }
180
+ //#endregion
17
181
 
18
182
 
19
- // Run the help command, which displays usage information.
20
- function runHelp() {
21
- console.error(
22
- `Usage: cpoach [command] [options]\n` +
23
- `\n` +
24
- `Commands:\n` +
25
- ` i | includes Generate compiler flags for include paths.\n` +
183
+
184
+ //#region INIT COMMAND
185
+ // Get the Git repository URL from the current directory, if it exists.
186
+ function gitRepoUrl() {
187
+ try { return cp.execSync('git config --get remote.origin.url', {encoding: 'utf8'}).trim(); }
188
+ catch { return ''; }
189
+ }
190
+
191
+
192
+ // Initialize .gitignore to ignore dependencies and build artifacts.
193
+ function initGitignore(cwd) {
194
+ const pth = path.join(cwd, '.gitignore');
195
+ let data = fs.existsSync(pth)? readTextFileSync(pth) : '';
196
+ if (!/node_modules\//.test(data)) data = sectionAdd(data, PRE, 'Dependencies', `node_modules/`);
197
+ if (!/build\//.test(data)) data = sectionAdd(data, PRE, 'Build', `build/`);
198
+ writeTextFileSync(pth, sectionTrim(data));
199
+ }
200
+
201
+
202
+ // Initialize .npmignore to ignore dependencies and build artifacts.
203
+ function initNpmignore(cwd) {
204
+ const pth = path.join(cwd, '.npmignore');
205
+ let data = fs.existsSync(pth)? readTextFileSync(pth) : '';
206
+ if (!/build\//.test(data)) data = sectionAdd(data, PRE, 'Build', `build/`);
207
+ writeTextFileSync(pth, sectionTrim(data));
208
+ }
209
+
210
+
211
+ // Initialize source files based on package.json.
212
+ function initSourceFiles(cwd, pkg) {
213
+ const p = pkg;
214
+ const files = p.sourceFiles || ['main.cxx'];
215
+ let main = null;
216
+ for (const file of files) {
217
+ const pth = path.join(cwd, file);
218
+ const dir = path.dirname(pth);
219
+ if (!/\?|\*/.test(dir)) fs.mkdirSync(dir, {recursive: true});
220
+ if ( /\?|\*/.test(file)) continue;
221
+ if (fs.existsSync(pth)) continue;
222
+ if (/main\.(c|cxx|cpp|cc)$/.test(file)) main = main || pth;
223
+ fs.writeFileSync(pth, '');
224
+ }
225
+ if (!main) return;
226
+ const data = readTextFileSync(main);
227
+ if (data.trim()) return;
228
+ writeTextFileSync(main,
229
+ `#include <iostream>\n` +
26
230
  `\n` +
27
- `Options:\n` +
28
- ` --compiler [name] Specify the compiler (msvc, gcc, clang). Default is gcc.\n` +
29
- ` --msvc Shortcut for --compiler msvc.\n` +
30
- ` --gcc Shortcut for --compiler gcc.\n` +
31
- ` --clang Shortcut for --compiler clang.\n`
231
+ `int main() {\n` +
232
+ ` std::cout << "Hello, world!\\n";\n` +
233
+ ` return 0;\n` +
234
+ `}\n`
32
235
  );
33
236
  }
34
237
 
35
238
 
36
- // Parse the command name.
37
- function parseCommand(opt, cmd) {
38
- if (/i|includes/.test(cmd)) opt.command = 'includes';
39
- else opt.error = `Unknown option: ${cmd}`;
239
+ // Initialize CMakeLists.txt based on package.json.
240
+ function initCMakeLists(cwd, pkg) {
241
+ const p = pkg;
242
+ const projectName = p.name;
243
+ const isExecutable = p.type === 'executable';
244
+ const cxxStandard = p.cmake.options.CMAKE_CXX_STANDARD || '17';
245
+ const generator = p.cmake.generator || null;
246
+ const pth = path.join(cwd, 'CMakeLists.txt');
247
+ let data = fs.existsSync(pth)? readTextFileSync(pth) : '';
248
+ if (!/cmake_minimum_required/.test(data)) {
249
+ data = sectionReplace(data, PRE, 'Project details',
250
+ `cmake_minimum_required(VERSION ${p.cmake.minVersion || '3.15'})\n` +
251
+ `project(${projectName} VERSION ${p.version} LANGUAGES CXX)\n`
252
+ );
253
+ }
254
+ data = sectionReplace(data, PRE, 'Include dependencies',
255
+ `include(dependencies.cmake OPTIONAL)`
256
+ );
257
+ if (!/set\(CMAKE_CXX_STANDARD\s/.test(data)) {
258
+ data = sectionReplace(data, PRE, 'Set C++ standard',
259
+ `set(CMAKE_CXX_STANDARD ${cxxStandard})\n` +
260
+ `set(CMAKE_CXX_STANDARD_REQUIRED ON)`
261
+ );
262
+ }
263
+ if (isExecutable) {
264
+ data = sectionReplace(data, PRE, 'Define executable target',
265
+ `add_executable(${projectName} main.cxx)\n` +
266
+ `target_include_directories(${projectName} PRIVATE include)`
267
+ );
268
+ }
269
+ else {
270
+ data = sectionReplace(data, PRE, 'Define library target',
271
+ `add_library(${projectName} main.cxx)\n` +
272
+ `target_include_directories(${projectName} PUBLIC include)`
273
+ );
274
+ }
275
+ data = sectionReplace(data, PRE, 'Link dependencies',
276
+ `# target_link_libraries(${projectName} PRIVATE dependencies)`
277
+ );
278
+ data = sectionReplace(data, PRE, 'Link system dependencies',
279
+ `# find_package(OpenSSL REQUIRED)\n` +
280
+ `# target_link_libraries(${projectName} PRIVATE OpenSSL::SSL)`
281
+ );
282
+ writeTextFileSync(pth, sectionTrim(data));
283
+ }
284
+
285
+
286
+ async function runInit() {
287
+ const cwd = process.cwd();
288
+ const pth = path.join(cwd, 'package.json');
289
+ if (fs.existsSync(pth)) {
290
+ const overwrite = await prompt('package.json already exists. Overwrite? (y/N) ');
291
+ if (overwrite.toLowerCase() !== 'y') { console.error('Aborted.'); return; }
292
+ }
293
+ // Gather project info
294
+ const name = await prompt('Project name', path.basename(cwd));
295
+ const version = await prompt('Version', '1.0.0');
296
+ const description = await prompt('Description', '');
297
+ const type = await prompt('Project type (executable/library)', 'executable');
298
+ const sourceFiles = await prompt('Source files (A; B)', 'main.cxx');
299
+ const cxxStandard = await prompt('C++ standard (11/14/17/20)', '17');
300
+ // const generator = await prompt('CMake generator (Ninja/Makefiles/VS)', 'Ninja');
301
+ const testCommand = await prompt('Test command', 'echo "No tests defined"');
302
+ const gitRepo = await prompt('Git repository URL', gitRepoUrl());
303
+ const keywords = await prompt('Keywords (A; B)', '');
304
+ const author = await prompt('Author', '');
305
+ const license = await prompt('License', 'MIT');
306
+ // Create package.json
307
+ const pkg = {}, p = pkg;
308
+ p.name = name;
309
+ p.version = version;
310
+ p.description = description;
311
+ p.type = type === 'executable'? 'executable' : 'library';
312
+ p.sourceFiles = sourceFiles.split(';').map(s => s.trim()).filter(s => s);
313
+ if (testCommand) p.scripts = {test: testCommand};
314
+ if (gitRepo) {
315
+ p.homepage = `${gitRepo}#readme`;
316
+ p.bugs = {url: `${gitRepo}/issues`};
317
+ p.repository = {
318
+ type: 'git',
319
+ url: gitRepo
320
+ };
321
+ }
322
+ p.keywords = keywords.split(';').map(s => s.trim()).filter(s => s);
323
+ if (author) p.author = author;
324
+ if (license) p.license = license;
325
+ p.dependencies = {};
326
+ p.devDependencies = {};
327
+ p.systemDependencies = {
328
+ linux: [],
329
+ darwin: [],
330
+ win32: []
331
+ };
332
+ p.cmake = {};
333
+ p.cmake.minVersion = '3.15';
334
+ if (generator) p.cmake.generator = generator;
335
+ p.cmake.options = {
336
+ CMAKE_CXX_STANDARD: cxxStandard,
337
+ BUILD_SHARED_LIBS: 'OFF'
338
+ }
339
+ // Default target same as name.
340
+ p.targets = [name];
341
+ writeJSONFileSync(pth, pkg);
342
+ initCMakeLists(cwd, pkg);
343
+ initGitignore(cwd);
344
+ initNpmignore(cwd);
345
+ initSourceFiles(cwd, pkg);
346
+ console.log('✓ Project initialized!\n');
40
347
  }
348
+ //#endregion
349
+
350
+
351
+
41
352
 
353
+ //#region PARSE ARG
42
354
  // Parse a single command-line argument.
43
355
  function parseArg(opt, argv, k, i) {
44
356
  if (k==='--help') opt.help = true;
45
- else if (k==='--compiler') opt.compiler = argv[++i];
46
- else if (k==='--msvc') opt.compiler = 'msvc';
47
- else if (k==='--gcc') opt.compiler = 'gcc';
48
- else if (k==='--clang') opt.compiler = 'clang';
49
- else if (!opt.command) parseCommand(opt, k);
50
- else opt.error = `Unknown option: ${k}`;
357
+ else if (k==='--version') opt.version = true;
358
+ // Parse command name.
359
+ else if (!opt.command) switch (k.toLowerCase()) {
360
+ case 'i': opt.command = 'includes'; break;
361
+ case 'includes': opt.command = 'includes'; break;
362
+ case 'init': opt.command = 'init'; break;
363
+ default: opt.error = `Unknown command: ${k}`; break;
364
+ }
365
+ // Parse command options.
366
+ else switch (opt.command) {
367
+ // Parse options for `includes` command.
368
+ case 'includes':
369
+ if (k==='--compiler') opt.compiler = argv[++i];
370
+ else if (k==='--msvc') opt.compiler = 'msvc';
371
+ else if (k==='--gcc') opt.compiler = 'gcc';
372
+ else if (k==='--clang') opt.compiler = 'clang';
373
+ else opt.error = `Unknown option: ${k}`;
374
+ break;
375
+ // Parse options for `init` command.
376
+ case 'init':
377
+ break;
378
+ }
51
379
  return i;
52
380
  }
53
381
 
@@ -56,15 +384,20 @@ function parseArgs(argv) {
56
384
  const opt = {
57
385
  error: null,
58
386
  help: false,
59
- command: 'includes',
387
+ version: false,
388
+ command: '',
60
389
  compiler: 'gcc'
61
390
  };
62
391
  for (let i=2; i<argv.length; i++)
63
392
  i = parseArg(opt, argv, argv[i], i);
64
393
  return opt;
65
394
  }
395
+ //#endregion
396
+
66
397
 
67
398
 
399
+
400
+ //#region MAIN
68
401
  // Main entry point.
69
402
  function main(argv) {
70
403
  const opt = parseArgs(argv);
@@ -77,13 +410,16 @@ function main(argv) {
77
410
  runHelp();
78
411
  process.exit(0);
79
412
  }
413
+ if (opt.version) {
414
+ runVersion();
415
+ process.exit(0);
416
+ }
80
417
  switch (opt.command) {
81
418
  case 'includes': runIncludes(opt); break;
82
- default:
83
- console.error(`Unknown command: ${opt.command}`);
84
- process.exit(1);
419
+ case 'init': runInit(opt); break;
85
420
  }
86
421
  }
87
422
 
88
423
  // Run the main entry point.
89
424
  main(process.argv);
425
+ //#endregion
package/package.json CHANGED
@@ -1,11 +1,13 @@
1
1
  {
2
2
  "name": "cpoach.sh",
3
- "version": "0.2.4",
4
- "description": "Support tool for for easy-to-use, C/C++ libraries that can be effortlessly installed via NPM.",
3
+ "version": "0.3.0",
4
+ "description": "A C/C++ package manager using the npm registry.",
5
5
  "keywords": [
6
6
  "c",
7
7
  "c++",
8
8
  "cpoach",
9
+ "package",
10
+ "manager",
9
11
  "includes",
10
12
  "support",
11
13
  "tool",
@@ -1,35 +0,0 @@
1
- name: On Push
2
- on:
3
- push:
4
- branches:
5
- - main
6
- - master
7
- tags:
8
- - '!*' # Do not execute on tags
9
- env:
10
- NAME: ${{vars.NAME}}
11
- EMAIL: ${{vars.EMAIL}}
12
- NPM_TOKEN: ${{secrets.NPM_TOKEN}}
13
- GITHUB_TOKEN: ${{secrets.GH_TOKEN}}
14
- FORCE_COLOR: 1
15
-
16
-
17
- jobs:
18
- publish:
19
- name: Publish packages
20
- runs-on: ubuntu-latest
21
- steps:
22
- - uses: actions/checkout@v6
23
- - uses: actions/setup-node@v6
24
- with:
25
- node-version: 24.x
26
- - uses: nodef/npm-config.action@v1.2.0
27
- with:
28
- credentials: auto
29
- entries: access=public
30
- env:
31
- GITHUB_TOKEN: ${{env.GITHUB_TOKEN}}
32
- NPM_TOKEN: ${{env.NPM_TOKEN}}
33
- - run: npm publish
34
- env:
35
- NODE_AUTH_TOKEN: ${{env.NPM_TOKEN}}