@f12r/f12r 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.
@@ -0,0 +1,26 @@
1
+ const { getSystemStatus } = require('../utils/checker');
2
+ const { heading, info, success, error, warning, blank } = require('../utils/logger');
3
+
4
+ async function runDoctor() {
5
+ heading('f12r doctor');
6
+ info('๐Ÿ” Checking system...');
7
+
8
+ const results = await getSystemStatus();
9
+ blank();
10
+
11
+ results.forEach((tool) => {
12
+ if (tool.installed) {
13
+ success(`โœ… ${tool.label}: Installed${tool.version ? ` (${tool.version})` : ''}`);
14
+ return;
15
+ }
16
+
17
+ if (tool.optional) {
18
+ warning(`โŒ ${tool.label}: Not Installed (optional)`);
19
+ return;
20
+ }
21
+
22
+ error(`โŒ ${tool.label}: Not Installed`);
23
+ });
24
+ }
25
+
26
+ module.exports = runDoctor;
@@ -0,0 +1,48 @@
1
+ const { TOOLS, getSystemStatus } = require('../utils/checker');
2
+ const { ensureHomebrewInstalled, installTool, installVSCodeExtensions } = require('../utils/installer');
3
+ const { confirmInstallTool, confirmInstallExtensions } = require('../utils/prompts');
4
+ const { heading, info, success, warning, blank } = require('../utils/logger');
5
+
6
+ async function runSetup(options = {}) {
7
+ heading('f12r setup');
8
+ info('๐Ÿ” Checking system...');
9
+
10
+ await ensureHomebrewInstalled();
11
+
12
+ const results = await getSystemStatus();
13
+ const missingTools = results.filter((tool) => !tool.installed && tool.key !== 'brew');
14
+
15
+ if (!missingTools.length) {
16
+ success('โœ… All core tools are already installed.');
17
+ } else {
18
+ for (const tool of missingTools) {
19
+ const shouldInstall = options.all ? true : await confirmInstallTool(tool.label);
20
+
21
+ if (!shouldInstall) {
22
+ warning(`Skipped ${tool.label}.`);
23
+ continue;
24
+ }
25
+
26
+ await installTool(tool.key);
27
+ }
28
+ }
29
+
30
+ const codeStatus = await TOOLS.code.check();
31
+ if (!codeStatus.installed) {
32
+ warning('VS Code extensions were skipped because the `code` command is not available.');
33
+ return;
34
+ }
35
+
36
+ const shouldInstallExtensions = options.all ? true : await confirmInstallExtensions();
37
+
38
+ if (!shouldInstallExtensions) {
39
+ info('VS Code extensions skipped.');
40
+ return;
41
+ }
42
+
43
+ await installVSCodeExtensions();
44
+ blank();
45
+ success('โœ… Setup complete.');
46
+ }
47
+
48
+ module.exports = runSetup;
@@ -0,0 +1,31 @@
1
+ const { getManagedTools } = require('../utils/checker');
2
+ const { uninstallTool } = require('../utils/installer');
3
+ const { chooseToolToUninstall, confirmUninstallTool } = require('../utils/prompts');
4
+ const { heading, info, warning, success } = require('../utils/logger');
5
+
6
+ async function runUninstall() {
7
+ heading('f12r uninstall');
8
+ info('๐Ÿ” Checking installed removable tools...');
9
+
10
+ const removableTools = await getManagedTools();
11
+ const installedTools = removableTools.filter((tool) => tool.installed);
12
+
13
+ if (!installedTools.length) {
14
+ warning('No supported tools are currently installed via detectable commands.');
15
+ return;
16
+ }
17
+
18
+ const selectedToolKey = await chooseToolToUninstall(installedTools);
19
+ const selectedTool = installedTools.find((tool) => tool.key === selectedToolKey);
20
+ const shouldUninstall = await confirmUninstallTool(selectedTool.label);
21
+
22
+ if (!shouldUninstall) {
23
+ warning(`Cancelled uninstall for ${selectedTool.label}.`);
24
+ return;
25
+ }
26
+
27
+ await uninstallTool(selectedTool.key);
28
+ success(`โœ… ${selectedTool.label} uninstall flow finished.`);
29
+ }
30
+
31
+ module.exports = runUninstall;
package/index.js ADDED
@@ -0,0 +1,56 @@
1
+ #!/usr/bin/env node
2
+
3
+ const { Command } = require('commander');
4
+ const runDoctor = require('./commands/doctor');
5
+ const runSetup = require('./commands/setup');
6
+ const runUninstall = require('./commands/uninstall');
7
+ const { error } = require('./utils/logger');
8
+ const packageJson = require('./package.json');
9
+
10
+ const program = new Command();
11
+
12
+ program
13
+ .name('f12r')
14
+ .description('Set up and manage a macOS development environment.')
15
+ .version(packageJson.version, '-v, --version', 'Show the current f12r version');
16
+
17
+ program
18
+ .command('doctor')
19
+ .description('Check installed development tools and their versions')
20
+ .action(async () => {
21
+ try {
22
+ await runDoctor();
23
+ } catch (err) {
24
+ error(err.message);
25
+ process.exitCode = 1;
26
+ }
27
+ });
28
+
29
+ program
30
+ .command('setup')
31
+ .description('Interactively install missing development tools')
32
+ .option('-a, --all', 'Install all missing tools without prompting')
33
+ .action(async (options) => {
34
+ try {
35
+ await runSetup(options);
36
+ } catch (err) {
37
+ error(err.message);
38
+ process.exitCode = 1;
39
+ }
40
+ });
41
+
42
+ program
43
+ .command('uninstall')
44
+ .description('Remove an installed development tool')
45
+ .action(async () => {
46
+ try {
47
+ await runUninstall();
48
+ } catch (err) {
49
+ error(err.message);
50
+ process.exitCode = 1;
51
+ }
52
+ });
53
+
54
+ program.showHelpAfterError('(run with --help for usage information)');
55
+
56
+ program.parseAsync(process.argv);
package/package.json ADDED
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "@f12r/f12r",
3
+ "version": "1.0.0",
4
+ "description": "A macOS developer environment setup and management CLI.",
5
+ "main": "index.js",
6
+ "bin": {
7
+ "f12r": "index.js"
8
+ },
9
+ "scripts": {
10
+ "doctor": "node index.js doctor",
11
+ "setup": "node index.js setup",
12
+ "uninstall": "node index.js uninstall"
13
+ },
14
+ "keywords": [
15
+ "cli",
16
+ "macos",
17
+ "developer-tools",
18
+ "homebrew",
19
+ "setup"
20
+ ],
21
+ "author": "",
22
+ "license": "MIT",
23
+ "dependencies": {
24
+ "chalk": "^4.1.2",
25
+ "commander": "^11.1.0",
26
+ "execa": "^5.1.1",
27
+ "inquirer": "^8.2.6",
28
+ "ora": "^5.4.1"
29
+ }
30
+ }
@@ -0,0 +1,123 @@
1
+ const execa = require('execa');
2
+
3
+ async function commandExists(command) {
4
+ try {
5
+ await execa('which', [command]);
6
+ return true;
7
+ } catch (err) {
8
+ return false;
9
+ }
10
+ }
11
+
12
+ async function getVersion(command, args = ['--version']) {
13
+ try {
14
+ const { stdout } = await execa(command, args);
15
+ const firstLine = stdout.split('\n').find(Boolean);
16
+ return firstLine ? firstLine.trim() : null;
17
+ } catch (err) {
18
+ return null;
19
+ }
20
+ }
21
+
22
+ async function checkToolWithVersion({ key, label, command, versionArgs, optional }) {
23
+ const installed = await commandExists(command);
24
+
25
+ return {
26
+ key,
27
+ label,
28
+ command,
29
+ optional: Boolean(optional),
30
+ installed,
31
+ version: installed ? await getVersion(command, versionArgs) : null
32
+ };
33
+ }
34
+
35
+ const TOOL_DEFINITIONS = [
36
+ {
37
+ key: 'brew',
38
+ label: 'Homebrew',
39
+ command: 'brew',
40
+ versionArgs: ['--version']
41
+ },
42
+ {
43
+ key: 'git',
44
+ label: 'Git',
45
+ command: 'git',
46
+ versionArgs: ['--version']
47
+ },
48
+ {
49
+ key: 'node',
50
+ label: 'Node.js',
51
+ command: 'node',
52
+ versionArgs: ['--version']
53
+ },
54
+ {
55
+ key: 'npm',
56
+ label: 'npm',
57
+ command: 'npm',
58
+ versionArgs: ['--version']
59
+ },
60
+ {
61
+ key: 'docker',
62
+ label: 'Docker',
63
+ command: 'docker',
64
+ versionArgs: ['--version'],
65
+ optional: true
66
+ },
67
+ {
68
+ key: 'code',
69
+ label: 'VS Code',
70
+ command: 'code',
71
+ versionArgs: ['--version']
72
+ }
73
+ ];
74
+
75
+ const TOOLS = {
76
+ brew: {
77
+ brewName: 'brew',
78
+ installType: 'formula',
79
+ check: () => checkToolWithVersion(TOOL_DEFINITIONS[0])
80
+ },
81
+ git: {
82
+ brewName: 'git',
83
+ installType: 'formula',
84
+ check: () => checkToolWithVersion(TOOL_DEFINITIONS[1])
85
+ },
86
+ node: {
87
+ brewName: 'node',
88
+ installType: 'formula',
89
+ check: () => checkToolWithVersion(TOOL_DEFINITIONS[2])
90
+ },
91
+ npm: {
92
+ brewName: null,
93
+ installType: null,
94
+ check: () => checkToolWithVersion(TOOL_DEFINITIONS[3])
95
+ },
96
+ docker: {
97
+ brewName: 'docker',
98
+ installType: 'cask',
99
+ check: () => checkToolWithVersion(TOOL_DEFINITIONS[4])
100
+ },
101
+ code: {
102
+ brewName: 'visual-studio-code',
103
+ installType: 'cask',
104
+ check: () => checkToolWithVersion(TOOL_DEFINITIONS[5])
105
+ }
106
+ };
107
+
108
+ async function getSystemStatus() {
109
+ return Promise.all(TOOL_DEFINITIONS.map((tool) => checkToolWithVersion(tool)));
110
+ }
111
+
112
+ async function getManagedTools() {
113
+ const keys = ['git', 'node', 'docker', 'code'];
114
+ const tools = await Promise.all(keys.map((key) => TOOLS[key].check()));
115
+ return tools;
116
+ }
117
+
118
+ module.exports = {
119
+ TOOLS,
120
+ commandExists,
121
+ getSystemStatus,
122
+ getManagedTools
123
+ };
@@ -0,0 +1,125 @@
1
+ const execa = require('execa');
2
+ const ora = require('ora');
3
+ const { TOOLS } = require('./checker');
4
+ const { warning, info } = require('./logger');
5
+
6
+ const VS_CODE_EXTENSIONS = [
7
+ 'esbenp.prettier-vscode',
8
+ 'dbaeumer.vscode-eslint',
9
+ 'GitHub.copilot'
10
+ ];
11
+
12
+ async function runBrew(args) {
13
+ return execa('brew', args, { stdio: 'pipe' });
14
+ }
15
+
16
+ async function ensureHomebrewInstalled() {
17
+ const brewStatus = await TOOLS.brew.check();
18
+
19
+ if (brewStatus.installed) {
20
+ return;
21
+ }
22
+
23
+ const spinner = ora('๐Ÿš€ Installing Homebrew...').start();
24
+
25
+ try {
26
+ await execa(
27
+ '/bin/bash',
28
+ [
29
+ '-c',
30
+ '$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)'
31
+ ],
32
+ {
33
+ env: {
34
+ ...process.env,
35
+ NONINTERACTIVE: '1'
36
+ },
37
+ shell: false,
38
+ stdio: 'pipe'
39
+ }
40
+ );
41
+ spinner.succeed('โœ… Homebrew installed successfully');
42
+ } catch (err) {
43
+ spinner.fail('โŒ Failed to install Homebrew');
44
+ throw new Error(formatCommandError(err));
45
+ }
46
+ }
47
+
48
+ async function installTool(toolKey) {
49
+ const tool = TOOLS[toolKey];
50
+
51
+ if (!tool || !tool.brewName) {
52
+ throw new Error(`Unsupported tool: ${toolKey}`);
53
+ }
54
+
55
+ const spinner = ora(`๐Ÿš€ Installing ${toolKey === 'code' ? 'VS Code' : capitalize(toolKey)}...`).start();
56
+
57
+ try {
58
+ if (tool.installType === 'cask') {
59
+ await runBrew(['install', '--cask', tool.brewName]);
60
+ } else {
61
+ await runBrew(['install', tool.brewName]);
62
+ }
63
+
64
+ spinner.succeed(`โœ… ${toolKey === 'code' ? 'VS Code' : capitalize(toolKey)} installed successfully`);
65
+ } catch (err) {
66
+ spinner.fail(`โŒ Failed to install ${toolKey === 'code' ? 'VS Code' : capitalize(toolKey)}`);
67
+ throw new Error(formatCommandError(err));
68
+ }
69
+ }
70
+
71
+ async function uninstallTool(toolKey) {
72
+ const tool = TOOLS[toolKey];
73
+
74
+ if (!tool || !tool.brewName) {
75
+ throw new Error(`Unsupported tool: ${toolKey}`);
76
+ }
77
+
78
+ const spinner = ora(`๐Ÿงน Uninstalling ${toolKey === 'code' ? 'VS Code' : capitalize(toolKey)}...`).start();
79
+
80
+ try {
81
+ if (tool.installType === 'cask') {
82
+ await runBrew(['uninstall', '--cask', tool.brewName]);
83
+ } else {
84
+ await runBrew(['uninstall', tool.brewName]);
85
+ }
86
+
87
+ spinner.succeed(`โœ… ${toolKey === 'code' ? 'VS Code' : capitalize(toolKey)} removed successfully`);
88
+ } catch (err) {
89
+ spinner.fail(`โŒ Failed to uninstall ${toolKey === 'code' ? 'VS Code' : capitalize(toolKey)}`);
90
+ throw new Error(formatCommandError(err));
91
+ }
92
+ }
93
+
94
+ async function installVSCodeExtensions() {
95
+ info('๐Ÿš€ Installing VS Code extensions...');
96
+
97
+ for (const extension of VS_CODE_EXTENSIONS) {
98
+ const spinner = ora(`Installing ${extension}...`).start();
99
+
100
+ try {
101
+ await execa('code', ['--install-extension', extension], { stdio: 'pipe' });
102
+ spinner.succeed(`โœ… Installed ${extension}`);
103
+ } catch (err) {
104
+ spinner.fail(`โŒ Failed to install ${extension}`);
105
+ warning(formatCommandError(err));
106
+ }
107
+ }
108
+ }
109
+
110
+ function formatCommandError(err) {
111
+ const output = err.stderr || err.stdout || err.shortMessage || err.message;
112
+ return output.trim();
113
+ }
114
+
115
+ function capitalize(value) {
116
+ return value.charAt(0).toUpperCase() + value.slice(1);
117
+ }
118
+
119
+ module.exports = {
120
+ VS_CODE_EXTENSIONS,
121
+ ensureHomebrewInstalled,
122
+ installTool,
123
+ uninstallTool,
124
+ installVSCodeExtensions
125
+ };
@@ -0,0 +1,34 @@
1
+ const chalk = require('chalk');
2
+
3
+ function heading(message) {
4
+ console.log(chalk.cyan.bold(message));
5
+ }
6
+
7
+ function info(message) {
8
+ console.log(chalk.blue(message));
9
+ }
10
+
11
+ function success(message) {
12
+ console.log(chalk.green(message));
13
+ }
14
+
15
+ function warning(message) {
16
+ console.log(chalk.yellow(message));
17
+ }
18
+
19
+ function error(message) {
20
+ console.error(chalk.red(message));
21
+ }
22
+
23
+ function blank() {
24
+ console.log('');
25
+ }
26
+
27
+ module.exports = {
28
+ heading,
29
+ info,
30
+ success,
31
+ warning,
32
+ error,
33
+ blank
34
+ };
@@ -0,0 +1,63 @@
1
+ const inquirer = require('inquirer');
2
+
3
+ async function confirmInstallTool(toolLabel) {
4
+ const { shouldInstall } = await inquirer.prompt([
5
+ {
6
+ type: 'confirm',
7
+ name: 'shouldInstall',
8
+ message: `Install ${toolLabel}?`,
9
+ default: true
10
+ }
11
+ ]);
12
+
13
+ return shouldInstall;
14
+ }
15
+
16
+ async function confirmInstallExtensions() {
17
+ const { shouldInstallExtensions } = await inquirer.prompt([
18
+ {
19
+ type: 'confirm',
20
+ name: 'shouldInstallExtensions',
21
+ message: 'Install VS Code extensions?',
22
+ default: true
23
+ }
24
+ ]);
25
+
26
+ return shouldInstallExtensions;
27
+ }
28
+
29
+ async function chooseToolToUninstall(installedTools) {
30
+ const { tool } = await inquirer.prompt([
31
+ {
32
+ type: 'list',
33
+ name: 'tool',
34
+ message: 'Which tool would you like to remove?',
35
+ choices: installedTools.map((installedTool) => ({
36
+ name: installedTool.label,
37
+ value: installedTool.key
38
+ }))
39
+ }
40
+ ]);
41
+
42
+ return tool;
43
+ }
44
+
45
+ async function confirmUninstallTool(toolLabel) {
46
+ const { shouldUninstall } = await inquirer.prompt([
47
+ {
48
+ type: 'confirm',
49
+ name: 'shouldUninstall',
50
+ message: `Uninstall ${toolLabel}?`,
51
+ default: false
52
+ }
53
+ ]);
54
+
55
+ return shouldUninstall;
56
+ }
57
+
58
+ module.exports = {
59
+ confirmInstallTool,
60
+ confirmInstallExtensions,
61
+ chooseToolToUninstall,
62
+ confirmUninstallTool
63
+ };