Node.js CLI ツール開発:commander.js で本格コマンドを作る

コマンドラインインターフェース(CLI)ツールは、開発者の生産性を劇的に向上させる強力な武器です。特に Node.js エコシステムでは、commander.js
というライブラリが CLI ツール開発の標準となっています。
この記事では、commander.js
を使った本格的な CLI ツールの開発方法を、実際のコード例とエラーハンドリングを含めて詳しく解説します。初心者の方でも、記事を読み終える頃には自分の CLI ツールを作れるようになるでしょう。
commander.js とは
commander.js
は、Node.js で CLI ツールを開発するための最も人気のあるライブラリです。Express.js の作者である TJ Holowaychuk 氏によって開発され、直感的で使いやすい API を提供しています。
主な特徴
- 直感的な API: チェーンメソッドでコマンドを定義
- 豊富な機能: オプション、フラグ、サブコマンドを簡単に実装
- 自動ヘルプ生成: コマンドの説明やオプションを自動でヘルプに反映
- 型安全性: TypeScript との相性が抜群
- アクティブな開発: 定期的なアップデートとコミュニティサポート
他のライブラリとの比較
ライブラリ | 学習コスト | 機能性 | コミュニティ | 推奨度 |
---|---|---|---|---|
commander.js | 低 | 高 | 大 | ⭐⭐⭐⭐⭐ |
yargs | 中 | 高 | 大 | ⭐⭐⭐⭐ |
meow | 低 | 中 | 中 | ⭐⭐⭐ |
oclif | 高 | 高 | 中 | ⭐⭐⭐⭐ |
CLI ツール開発の基本概念
CLI ツールを開発する前に、基本的な概念を理解しておきましょう。
CLI ツールの構造
CLI ツールは通常、以下の要素で構成されます:
- コマンド: 実行したい操作(例:
git commit
) - オプション: コマンドの動作を変更するフラグ(例:
--message
) - 引数: コマンドに渡す値(例:ファイル名)
- サブコマンド: メインコマンドの下位コマンド(例:
git remote add
)
一般的なエラーパターン
CLI ツール開発でよく遭遇するエラーとその対処法を理解しておくことが重要です:
bash# よくあるエラー例
Error: Cannot find module 'commander'
at Function.Module._resolveFilename (node:internal/modules/cjs/loader:933:17)
at Function.Module._load (node:internal/modules/cjs/loader:778:32)
at Module.require (node:internal/modules/cjs/helpers:102:12)
at require (node:internal/modules/cjs/helpers:108:19)
このエラーは、commander.js
がインストールされていない場合に発生します。
commander.js のインストールとセットアップ
まず、新しいプロジェクトを作成し、commander.js
をインストールしましょう。
プロジェクトの初期化
bash# プロジェクトディレクトリを作成
mkdir my-cli-tool
cd my-cli-tool
# package.jsonを初期化
yarn init -y
commander.js のインストール
bash# commander.jsをインストール
yarn add commander
# TypeScriptを使用する場合
yarn add -D typescript @types/node
基本的なセットアップ
最初の CLI ツールを作成してみましょう。index.js
ファイルを作成します:
javascript#!/usr/bin/env node
const { Command } = require('commander');
// プログラムの基本情報を設定
const program = new Command();
program
.name('my-cli')
.description('My awesome CLI tool')
.version('1.0.0');
// プログラムを実行
program.parse();
実行権限の設定
CLI ツールとして実行できるように、ファイルに実行権限を付与します:
bash# 実行権限を付与
chmod +x index.js
# テスト実行
node index.js --help
基本的なコマンドの作成
実際に動作するコマンドを作成してみましょう。
シンプルなコマンド
ファイル操作を行う CLI ツールを作成します:
javascript#!/usr/bin/env node
const { Command } = require('commander');
const fs = require('fs');
const path = require('path');
const program = new Command();
program
.name('file-manager')
.description('Simple file management CLI tool')
.version('1.0.0');
// ファイル一覧を表示するコマンド
program
.command('list')
.description('List files in current directory')
.action(() => {
try {
const files = fs.readdirSync('.');
console.log('Files in current directory:');
files.forEach((file) => {
const stats = fs.statSync(file);
const type = stats.isDirectory() ? '📁' : '📄';
console.log(`${type} ${file}`);
});
} catch (error) {
console.error(
'Error reading directory:',
error.message
);
process.exit(1);
}
});
program.parse();
引数を受け取るコマンド
コマンドに引数を渡せるようにしましょう:
javascript// ファイル内容を表示するコマンド
program
.command('read <filename>')
.description('Read and display file content')
.action((filename) => {
try {
if (!fs.existsSync(filename)) {
console.error(
`Error: File '${filename}' not found`
);
process.exit(1);
}
const content = fs.readFileSync(filename, 'utf8');
console.log(`Content of ${filename}:`);
console.log('─'.repeat(50));
console.log(content);
} catch (error) {
console.error('Error reading file:', error.message);
process.exit(1);
}
});
オプションとフラグの実装
オプションとフラグを使って、コマンドの動作をカスタマイズできるようにしましょう。
基本的なオプション
javascript// オプション付きのファイル一覧コマンド
program
.command('list')
.description('List files in current directory')
.option('-a, --all', 'Show hidden files')
.option('-l, --long', 'Show detailed information')
.action((options) => {
try {
const files = fs.readdirSync('.');
console.log('Files in current directory:');
files.forEach((file) => {
// 隠しファイルの処理
if (!options.all && file.startsWith('.')) {
return;
}
const stats = fs.statSync(file);
const type = stats.isDirectory() ? '📁' : '📄';
if (options.long) {
const size = stats.size;
const modified = stats.mtime.toLocaleDateString();
console.log(
`${type} ${file.padEnd(20)} ${size
.toString()
.padStart(10)} bytes ${modified}`
);
} else {
console.log(`${type} ${file}`);
}
});
} catch (error) {
console.error(
'Error reading directory:',
error.message
);
process.exit(1);
}
});
値を受け取るオプション
javascript// ファイル作成コマンド
program
.command('create <filename>')
.description('Create a new file')
.option('-c, --content <content>', 'Initial file content')
.option(
'-t, --type <type>',
'File type (txt, json, js)',
'txt'
)
.action((filename, options) => {
try {
let content = options.content || '';
// ファイルタイプに応じてデフォルトコンテンツを設定
if (!options.content) {
switch (options.type) {
case 'json':
content =
'{\n "name": "example",\n "version": "1.0.0"\n}';
break;
case 'js':
content = 'console.log("Hello, World!");';
break;
default:
content =
'# New File\n\nThis is a new file created by CLI tool.';
}
}
fs.writeFileSync(filename, content);
console.log(
`✅ File '${filename}' created successfully`
);
} catch (error) {
console.error('Error creating file:', error.message);
process.exit(1);
}
});
サブコマンドの活用
複雑な CLI ツールでは、サブコマンドを使って機能を整理します。
サブコマンドの実装
javascript// ファイル操作のサブコマンド
const fileCommand = program
.command('file')
.description('File operations');
// ファイルのコピー
fileCommand
.command('copy <source> <destination>')
.description('Copy a file')
.option('-f, --force', 'Overwrite existing file')
.action((source, destination, options) => {
try {
if (!fs.existsSync(source)) {
console.error(
`Error: Source file '${source}' not found`
);
process.exit(1);
}
if (fs.existsSync(destination) && !options.force) {
console.error(
`Error: Destination file '${destination}' already exists. Use --force to overwrite`
);
process.exit(1);
}
fs.copyFileSync(source, destination);
console.log(
`✅ File copied from '${source}' to '${destination}'`
);
} catch (error) {
console.error('Error copying file:', error.message);
process.exit(1);
}
});
// ファイルの移動
fileCommand
.command('move <source> <destination>')
.description('Move a file')
.action((source, destination) => {
try {
if (!fs.existsSync(source)) {
console.error(
`Error: Source file '${source}' not found`
);
process.exit(1);
}
fs.renameSync(source, destination);
console.log(
`✅ File moved from '${source}' to '${destination}'`
);
} catch (error) {
console.error('Error moving file:', error.message);
process.exit(1);
}
});
ネストしたサブコマンド
より複雑な構造のサブコマンドも作成できます:
javascript// プロジェクト管理のサブコマンド
const projectCommand = program
.command('project')
.description('Project management operations');
const initCommand = projectCommand
.command('init')
.description('Initialize a new project');
initCommand
.command('node')
.description('Initialize a Node.js project')
.option('-n, --name <name>', 'Project name')
.option('-y, --yes', 'Skip prompts')
.action((options) => {
try {
const projectName = options.name || 'my-project';
if (!options.yes) {
console.log(
`Creating Node.js project: ${projectName}`
);
}
// package.jsonの作成
const packageJson = {
name: projectName,
version: '1.0.0',
description: '',
main: 'index.js',
scripts: {
start: 'node index.js',
test: 'echo "Error: no test specified" && exit 1',
},
keywords: [],
author: '',
license: 'ISC',
};
fs.writeFileSync(
'package.json',
JSON.stringify(packageJson, null, 2)
);
console.log('✅ Node.js project initialized');
} catch (error) {
console.error(
'Error initializing project:',
error.message
);
process.exit(1);
}
});
エラーハンドリングとバリデーション
堅牢な CLI ツールには、適切なエラーハンドリングとバリデーションが不可欠です。
基本的なエラーハンドリング
javascript// グローバルエラーハンドラー
process.on('uncaughtException', (error) => {
console.error('Uncaught Exception:', error.message);
process.exit(1);
});
process.on('unhandledRejection', (reason, promise) => {
console.error(
'Unhandled Rejection at:',
promise,
'reason:',
reason
);
process.exit(1);
});
// カスタムエラークラス
class CLIError extends Error {
constructor(message, code = 1) {
super(message);
this.name = 'CLIError';
this.code = code;
}
}
// バリデーション関数
function validateFileExists(filename) {
if (!fs.existsSync(filename)) {
throw new CLIError(`File '${filename}' not found`, 2);
}
}
function validateDirectory(path) {
if (!fs.existsSync(path)) {
throw new CLIError(`Directory '${path}' not found`, 3);
}
const stats = fs.statSync(path);
if (!stats.isDirectory()) {
throw new CLIError(`'${path}' is not a directory`, 4);
}
}
コマンドでのエラーハンドリング
javascript// エラーハンドリング付きのコマンド
program
.command('validate <path>')
.description('Validate file or directory')
.option(
'-t, --type <type>',
'Type to validate (file|directory)',
'file'
)
.action((path, options) => {
try {
if (options.type === 'file') {
validateFileExists(path);
console.log(
`✅ File '${path}' exists and is valid`
);
} else if (options.type === 'directory') {
validateDirectory(path);
console.log(
`✅ Directory '${path}' exists and is valid`
);
} else {
throw new CLIError(
`Invalid type: ${options.type}`,
5
);
}
} catch (error) {
if (error instanceof CLIError) {
console.error(`Error: ${error.message}`);
process.exit(error.code);
} else {
console.error('Unexpected error:', error.message);
process.exit(1);
}
}
});
入力バリデーション
javascript// 高度なバリデーション
function validateEmail(email) {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(email)) {
throw new CLIError('Invalid email format', 6);
}
}
function validateNumber(value, min = 0, max = Infinity) {
const num = Number(value);
if (isNaN(num)) {
throw new CLIError('Value must be a number', 7);
}
if (num < min || num > max) {
throw new CLIError(
`Value must be between ${min} and ${max}`,
8
);
}
return num;
}
// バリデーション付きのユーザー作成コマンド
program
.command('user')
.description('User management')
.command('create')
.description('Create a new user')
.requiredOption('-n, --name <name>', 'User name')
.requiredOption('-e, --email <email>', 'User email')
.option('-a, --age <age>', 'User age')
.action((options) => {
try {
// バリデーション
if (options.name.length < 2) {
throw new CLIError(
'Name must be at least 2 characters long',
9
);
}
validateEmail(options.email);
let age = null;
if (options.age) {
age = validateNumber(options.age, 0, 150);
}
// ユーザーオブジェクトの作成
const user = {
name: options.name,
email: options.email,
age: age,
createdAt: new Date().toISOString(),
};
console.log('✅ User created successfully:');
console.log(JSON.stringify(user, null, 2));
} catch (error) {
if (error instanceof CLIError) {
console.error(`Error: ${error.message}`);
process.exit(error.code);
} else {
console.error('Unexpected error:', error.message);
process.exit(1);
}
}
});
実際のプロジェクトでの活用例
実際のプロジェクトで使える実用的な CLI ツールを作成してみましょう。
プロジェクトテンプレート生成ツール
javascript#!/usr/bin/env node
const { Command } = require('commander');
const fs = require('fs');
const path = require('path');
const program = new Command();
program
.name('project-generator')
.description('Generate project templates')
.version('1.0.0');
// テンプレートの定義
const templates = {
'node-api': {
description: 'Node.js API project with Express',
files: {
'package.json': JSON.stringify(
{
name: '{{name}}',
version: '1.0.0',
description: '{{description}}',
main: 'src/index.js',
scripts: {
start: 'node src/index.js',
dev: 'nodemon src/index.js',
test: 'jest',
},
dependencies: {
express: '^4.18.2',
cors: '^2.8.5',
},
devDependencies: {
nodemon: '^2.0.22',
jest: '^29.5.0',
},
},
null,
2
),
'src/index.js': `const express = require('express');
const cors = require('cors');
const app = express();
const PORT = process.env.PORT || 3000;
app.use(cors());
app.use(express.json());
app.get('/', (req, res) => {
res.json({ message: 'Hello from {{name}}!' });
});
app.listen(PORT, () => {
console.log(\`Server running on port \${PORT}\`);
});`,
'README.md': `# {{name}}
{{description}}
# Installation
\`\`\`bash
yarn install
\`\`\`
# Usage
\`\`\`bash
yarn start
\`\`\`
# Development
\`\`\`bash
yarn dev
\`\`\`
`,
},
},
'react-app': {
description: 'React application with Vite',
files: {
'package.json': JSON.stringify(
{
name: '{{name}}',
version: '1.0.0',
description: '{{description}}',
scripts: {
dev: 'vite',
build: 'vite build',
preview: 'vite preview',
},
dependencies: {
react: '^18.2.0',
'react-dom': '^18.2.0',
},
devDependencies: {
'@vitejs/plugin-react': '^4.0.0',
vite: '^4.3.9',
},
},
null,
2
),
'vite.config.js': `import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
});`,
'index.html': `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>{{name}}</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>`,
'src/main.jsx': `import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<App />
</React.StrictMode>
);`,
'src/App.jsx': `import { useState } from 'react';
function App() {
const [count, setCount] = useState(0);
return (
<div className="App">
<h1>{{name}}</h1>
<p>{{description}}</p>
<button onClick={() => setCount(count + 1)}>
Count: {count}
</button>
</div>
);
}
export default App;`,
},
},
};
// テンプレート生成コマンド
program
.command('create <template> <name>')
.description('Create a new project from template')
.option(
'-d, --description <description>',
'Project description',
'A new project'
)
.option('-f, --force', 'Overwrite existing directory')
.action((template, name, options) => {
try {
// テンプレートの存在確認
if (!templates[template]) {
console.error(
`Error: Template '${template}' not found`
);
console.log('Available templates:');
Object.keys(templates).forEach((t) => {
console.log(
` - ${t}: ${templates[t].description}`
);
});
process.exit(1);
}
const projectPath = path.join(process.cwd(), name);
// ディレクトリの存在確認
if (fs.existsSync(projectPath) && !options.force) {
console.error(
`Error: Directory '${name}' already exists. Use --force to overwrite`
);
process.exit(1);
}
// ディレクトリの作成
if (!fs.existsSync(projectPath)) {
fs.mkdirSync(projectPath, { recursive: true });
}
const templateData = templates[template];
// ファイルの作成
Object.entries(templateData.files).forEach(
([filename, content]) => {
const filePath = path.join(projectPath, filename);
const dir = path.dirname(filePath);
// ディレクトリの作成
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
// テンプレート変数の置換
let processedContent = content
.replace(/\{\{name\}\}/g, name)
.replace(
/\{\{description\}\}/g,
options.description
);
fs.writeFileSync(filePath, processedContent);
console.log(`✅ Created: ${filename}`);
}
);
console.log(
`\n🎉 Project '${name}' created successfully!`
);
console.log(`\nNext steps:`);
console.log(` cd ${name}`);
console.log(` yarn install`);
if (template === 'node-api') {
console.log(` yarn dev`);
} else if (template === 'react-app') {
console.log(` yarn dev`);
}
} catch (error) {
console.error(
'Error creating project:',
error.message
);
process.exit(1);
}
});
// 利用可能なテンプレート一覧
program
.command('list')
.description('List available templates')
.action(() => {
console.log('Available templates:\n');
Object.entries(templates).forEach(
([name, template]) => {
console.log(
`${name.padEnd(15)} ${template.description}`
);
}
);
});
program.parse();
デバッグとテスト
CLI ツールの品質を保つために、適切なデバッグとテストの仕組みを導入しましょう。
デバッグ機能の実装
javascript// デバッグモードの実装
program
.option('-D, --debug', 'Enable debug mode')
.hook('preAction', (thisCommand) => {
if (thisCommand.opts().debug) {
console.log('🐛 Debug mode enabled');
console.log('Command:', thisCommand.name());
console.log('Options:', thisCommand.opts());
console.log('Arguments:', thisCommand.args);
}
});
// ログ機能
class Logger {
constructor(debug = false) {
this.debug = debug;
}
info(message) {
console.log(`ℹ️ ${message}`);
}
success(message) {
console.log(`✅ ${message}`);
}
warning(message) {
console.log(`⚠️ ${message}`);
}
error(message) {
console.error(`❌ ${message}`);
}
debug(message) {
if (this.debug) {
console.log(`🐛 ${message}`);
}
}
}
// ログ機能付きのコマンド
program
.command('process <input>')
.description('Process input with logging')
.option('-o, --output <output>', 'Output file')
.option('-v, --verbose', 'Verbose output')
.action((input, options) => {
const logger = new Logger(options.verbose);
logger.info(`Processing input: ${input}`);
logger.debug(`Options: ${JSON.stringify(options)}`);
try {
// 処理のシミュレーション
const result = input.toUpperCase();
if (options.output) {
fs.writeFileSync(options.output, result);
logger.success(
`Result written to: ${options.output}`
);
} else {
console.log(result);
}
logger.success('Processing completed');
} catch (error) {
logger.error(`Processing failed: ${error.message}`);
process.exit(1);
}
});
テストの実装
javascript// テスト用のユーティリティ
function runCommand(command, args = []) {
return new Promise((resolve, reject) => {
const { spawn } = require('child_process');
const child = spawn('node', [
'index.js',
command,
...args,
]);
let stdout = '';
let stderr = '';
child.stdout.on('data', (data) => {
stdout += data.toString();
});
child.stderr.on('data', (data) => {
stderr += data.toString();
});
child.on('close', (code) => {
resolve({ code, stdout, stderr });
});
child.on('error', (error) => {
reject(error);
});
});
}
// テストファイル (test.js)
async function runTests() {
console.log('🧪 Running CLI tests...\n');
try {
// ヘルプコマンドのテスト
console.log('Testing help command...');
const helpResult = await runCommand('--help');
if (
helpResult.code === 0 &&
helpResult.stdout.includes('Usage:')
) {
console.log('✅ Help command works correctly');
} else {
console.log('❌ Help command failed');
}
// バージョンコマンドのテスト
console.log('Testing version command...');
const versionResult = await runCommand('--version');
if (
versionResult.code === 0 &&
versionResult.stdout.includes('1.0.0')
) {
console.log('✅ Version command works correctly');
} else {
console.log('❌ Version command failed');
}
// エラーハンドリングのテスト
console.log('Testing error handling...');
const errorResult = await runCommand(
'nonexistent-command'
);
if (errorResult.code !== 0) {
console.log('✅ Error handling works correctly');
} else {
console.log('❌ Error handling failed');
}
console.log('\n🎉 All tests completed!');
} catch (error) {
console.error('Test execution failed:', error.message);
process.exit(1);
}
}
// テストの実行
if (require.main === module) {
runTests();
}
パッケージングと配布
作成した CLI ツールを他の人と共有するために、パッケージングと配布の方法を学びましょう。
package.json の設定
json{
"name": "my-awesome-cli",
"version": "1.0.0",
"description": "A powerful CLI tool built with commander.js",
"main": "index.js",
"bin": {
"my-cli": "./index.js"
},
"scripts": {
"start": "node index.js",
"test": "node test.js",
"build": "echo 'No build step required'",
"prepublishOnly": "npm test"
},
"keywords": ["cli", "commander", "nodejs", "tools"],
"author": "Your Name <your.email@example.com>",
"license": "MIT",
"dependencies": {
"commander": "^11.0.0"
},
"devDependencies": {
"typescript": "^5.0.0",
"@types/node": "^20.0.0"
},
"engines": {
"node": ">=14.0.0"
},
"repository": {
"type": "git",
"url": "https://github.com/yourusername/my-awesome-cli.git"
},
"bugs": {
"url": "https://github.com/yourusername/my-awesome-cli/issues"
},
"homepage": "https://github.com/yourusername/my-awesome-cli#readme"
}
グローバルインストール用の設定
bash# グローバルインストール
yarn global add .
# または、npmを使用する場合
npm install -g .
配布用の README 作成
markdown# My Awesome CLI
A powerful command-line interface tool built with Node.js and commander.js.
# Installation
```bash
# Using yarn
yarn global add my-awesome-cli
# Using npm
npm install -g my-awesome-cli
```
Usage
bash# Show help
my-cli --help
# List files
my-cli list
# Create a new file
my-cli create myfile.txt --content "Hello, World!"
# Process files
my-cli process input.txt --output result.txt
Features
- 📁 File management operations
- 🔧 Project template generation
- ✅ Robust error handling
- 📝 Comprehensive logging
- 🧪 Built-in testing
Contributing
- Fork the repository
- Create your feature branch (
git checkout -b feature/amazing-feature
) - Commit your changes (
git commit -m 'Add some amazing feature'
) - Push to the branch (
git push origin feature/amazing-feature
) - Open a Pull Request
License
This project is licensed under the MIT License - see the LICENSE file for details.
csharp
## パッケージの公開
```bash
# npmにログイン
npm login
# パッケージを公開
npm publish
# または、スコープ付きパッケージの場合
npm publish --access public
まとめ
この記事では、commander.js
を使った本格的な CLI ツール開発について詳しく解説しました。
学んだこと
- commander.js の基本: 直感的で強力な CLI ツール開発ライブラリ
- コマンド設計: 引数、オプション、サブコマンドの効果的な活用
- エラーハンドリング: 堅牢なエラー処理とバリデーション
- 実践的な開発: 実際のプロジェクトで使える CLI ツールの作成
- テストとデバッグ: 品質を保つための仕組み
- 配布と共有: 他の開発者とツールを共有する方法
次のステップ
CLI ツール開発の世界は奥深く、まだまだ学ぶことがたくさんあります。以下のような発展的なトピックに挑戦してみてください:
- インタラクティブ機能:
inquirer.js
を使った対話的な CLI - カラフルな出力:
chalk
を使った美しいターミナル表示 - プログレスバー:
ora
やcli-progress
を使った進捗表示 - 設定管理: 設定ファイルの読み書きと管理
- プラグインシステム: 拡張可能な CLI ツールの設計
CLI ツール開発は、開発者の生産性を劇的に向上させる強力なスキルです。この記事で学んだことを基に、自分だけの便利なツールを作成してみてください。きっと、開発の楽しさを再発見できるはずです。
関連リンク
- review
今の自分に満足していますか?『持たざる者の逆襲 まだ何者でもない君へ』溝口勇児
- review
ついに語られた業界の裏側!『フジテレビの正体』堀江貴文が描くテレビ局の本当の姿
- review
愛する勇気を持てば人生が変わる!『幸せになる勇気』岸見一郎・古賀史健のアドラー実践編で真の幸福を手に入れる
- review
週末を変えれば年収も変わる!『世界の一流は「休日」に何をしているのか』越川慎司の一流週末メソッド
- review
新しい自分に会いに行こう!『自分の変え方』村岡大樹の認知科学コーチングで人生リセット
- review
科学革命から AI 時代へ!『サピエンス全史 下巻』ユヴァル・ノア・ハラリが予見する人類の未来