【怠惰でもできる】ズボラ専用TIL環境構築
【怠惰でもできる】ズボラ専用TIL環境構築
毎日TIL(Today I Learned)を書くのは良い習慣ですが、ファイル名を考えたり、日付を入力したり、フォルダに整理したりするのは面倒ですよね。この記事では、最小限のコマンドでTILを管理できる環境の構築方法を紹介します。
この記事で作るもの
- コマンド1つでTILファイルを自動作成
- 月次で自動アーカイブ(フォルダ整理)
- タグの自動正規化と整形
- 複数のテンプレートから選択可能
面倒な作業は一切なし。コマンドを実行するだけです。
前提条件
- Node.js と npm(または pnpm)がインストールされていること
- Git がインストールされていること
- 基本的なターミナル操作ができること
セットアップ手順
1. リポジトリの作成
まず、TILを管理するためのリポジトリを作成します。
mkdir til_diary
cd til_diary
git init
2. package.json の作成
プロジェクトのルートに package.json を作成します。
Pathで表すならここです。TIL_diary/package.json
{
"name": "til_diary",
"private": true,
"type": "module",
"version": "0.1.0",
"description": "TIL workspace scripts",
"scripts": {
"til": "node scripts/til.mjs",
"til:new": "node scripts/til.mjs new",
"til:archive": "node scripts/til.mjs archive",
"til:arc": "node scripts/til.mjs arc"
}
}
3. フォルダの作成
以下のようにフォルダを作成します。
TIL_diary/templates/-
TIL_diary/scripts/
を作ってください。テンプレートを保存するところ,コマンド類のスクリプトを保存するところです。
mkdir -p scripts templates
4. タグ定義ファイルの作成
tags.yaml を作成して、使用するタグを定義します。エイリアス(別名)も設定できます。
こちらは頻繁に編集することを想定しているので,ルートに配置します。
Pathで表すならここです。TIL_diary/tags.yaml
タグ名:
desc: 説明
aliases: [別名1,別名2]
tags:
til:
desc: Today I Learned
aliases: []
vscode:
desc: Visual Studio Code
aliases: [vs-code]
js:
desc: JavaScript
aliases: [javascript]
rust:
desc: Rust
aliases: []
shell:
desc: Shell scripting
aliases: [bash, zsh]
5. テンプレートファイルの作成
templates ディレクトリ(TIL_diary/templates/) に、以下のテンプレートファイルを作成します。
templates/note.md(基本ノート)
---
id: {{ID}}
title: {{TITLE}}
date: {{DATE}}
type: {{TYPE}}
tags: [{{TAGS}}]
links: []
---
## 今日のTIL
-
## メモ
-
## 次にやること
-
templates/snippet.md(コードスニペット)
---
id: {{ID}}
title: {{TITLE}}
date: {{DATE}}
type: {{TYPE}}
tags: [{{TAGS}}]
links: []
---
## スニペット
```言語名
// コードをここに
```
## 説明
-
templates/bug.md(バグ記録)
---
id: {{ID}}
title: {{TITLE}}
date: {{DATE}}
type: {{TYPE}}
tags: [{{TAGS}}]
links: []
---
## 問題
-
## 原因
-
## 解決方法
-
templates/reading.md(読書記録)
---
id: {{ID}}
title: {{TITLE}}
date: {{DATE}}
type: {{TYPE}}
tags: [{{TAGS}}]
links: []
---
## 読んだもの
-
## 学んだこと
-
## 感想
-
templates/retro.md(振り返り)
---
id: {{ID}}
title: {{TITLE}}
date: {{DATE}}
type: {{TYPE}}
tags: [{{TAGS}}]
links: []
---
## 今週/今月の振り返り
-
## 良かったこと
-
## 改善したいこと
-
6. CLIスクリプトの作成
scripts/til.mjs を作成します。このスクリプトがTIL管理の核となります。
#!/usr/bin/env node
import fs from 'fs';
import path from 'path';
import { spawnSync } from 'child_process';
function exitWith(message, code = 1) {
console.error(message);
process.exit(code);
}
function parseArgs(argv) {
const args = { _: [] };
for (let i = 0; i < argv.length; i++) {
const a = argv[i];
if (a === '--') break;
if (a.startsWith('--')) {
const [k, v] = a.split('=');
const key = k.replace(/^--/, '');
if (v !== undefined) args[key] = v;
else if (argv[i + 1] && !argv[i + 1].startsWith('-')) args[key] = argv[++i];
else args[key] = true;
} else if (a.startsWith('-')) {
const key = a.replace(/^-/, '');
if (argv[i + 1] && !argv[i + 1].startsWith('-')) args[key] = argv[++i];
else args[key] = true;
} else {
args._.push(a);
}
}
return args;
}
function ensureDir(dir) {
fs.mkdirSync(dir, { recursive: true });
}
function todayYMD() {
const d = new Date();
const yyyy = d.getFullYear();
const mm = String(d.getMonth() + 1).padStart(2, '0');
const dd = String(d.getDate()).padStart(2, '0');
return `${yyyy}-${mm}-${dd}`;
}
function makeId(dateISO) {
const now = new Date();
const ymd = dateISO.replace(/-/g, '');
const hh = String(now.getHours()).padStart(2, '0');
const mi = String(now.getMinutes()).padStart(2, '0');
const ss = String(now.getSeconds()).padStart(2, '0');
return `${ymd}T${hh}${mi}${ss}Z`;
}
function toYY(yyyy) { return String(yyyy).slice(2); }
function toYYYY(yy) { return 2000 + Number(yy); }
function parseDateFromFilename(name) {
const m = name.match(/^(\d{2})-(\d{2})-(\d{2})\.md$/);
if (!m) return null;
const [_, yy, mm, dd] = m;
const yyyy = toYYYY(yy);
return { yyyy, mm, dd };
}
function readTagsMap() {
const file = path.join(process.cwd(), 'tags.yaml');
if (!fs.existsSync(file)) return { aliasToPrimary: {}, primaries: new Set() };
const txt = fs.readFileSync(file, 'utf8');
const aliasToPrimary = {};
const primaries = new Set();
let current = null;
for (const raw of txt.split(/\r?\n/)) {
const line = raw.trimEnd();
if (/^tags:\s*$/.test(line)) continue;
const mKey = line.match(/^([A-Za-z0-9_\-]+):\s*$/);
if (mKey) { current = mKey[1]; primaries.add(current); continue; }
const mAliases = line.match(/^aliases:\s*\[(.*)\]\s*$/);
if (mAliases && current) {
const list = mAliases[1]
.split(',')
.map(s => s.trim())
.filter(Boolean);
for (const a of list) aliasToPrimary[a] = current;
}
}
return { aliasToPrimary, primaries };
}
function normalizeTags(input) {
if (!input) return [];
const { aliasToPrimary, primaries } = readTagsMap();
const raw = input.split(',').map(s => s.trim()).filter(Boolean);
const out = [];
for (const t of raw) {
const norm = aliasToPrimary[t] || t;
if (!primaries.size || primaries.has(norm)) out.push(norm);
else console.warn(`警告: タグ "${t}" は tags.yaml に登録されていません`);
}
return [...new Set(out)].sort();
}
function cmdNew(args) {
const dateStr = args.d || args.date || todayYMD();
const [yyyy, mm, dd] = dateStr.split('-');
const yy = toYY(yyyy);
const type = args.y || args.type || 'note';
const tags = normalizeTags(args.g || args.tags || 'til');
const title = args.t || args.title || dateStr;
const shouldOpen = args.o || args.open;
const templatePath = path.join(process.cwd(), 'templates', `${type}.md`);
if (!fs.existsSync(templatePath)) {
exitWith(`エラー: テンプレート "${type}.md" が見つかりません`);
}
const template = fs.readFileSync(templatePath, 'utf8');
const id = makeId(dateStr);
const tagsStr = tags.length > 0 ? tags.join(', ') : '';
const content = template
.replace(/\{\{ID\}\}/g, id)
.replace(/\{\{TITLE\}\}/g, title)
.replace(/\{\{DATE\}\}/g, dateStr)
.replace(/\{\{TYPE\}\}/g, type)
.replace(/\{\{TAGS\}\}/g, tagsStr);
const isPastMonth = new Date(dateStr) < new Date(todayYMD().substring(0, 7) + '-01');
const targetDir = isPastMonth ? path.join(yyyy, mm) : process.cwd();
ensureDir(targetDir);
const filename = `${yy}-${mm}-${dd}.md`;
const filepath = path.join(targetDir, filename);
if (fs.existsSync(filepath)) {
exitWith(`エラー: ファイル "${filepath}" は既に存在します`);
}
fs.writeFileSync(filepath, content, 'utf8');
console.log(`作成しました: ${filepath}`);
if (shouldOpen) {
spawnSync('code', [filepath], { stdio: 'inherit' });
}
}
function cmdArchive(args) {
const root = process.cwd();
const files = fs.readdirSync(root)
.filter(f => /^\d{2}-\d{2}-\d{2}\.md$/.test(f))
.map(f => ({ name: f, parsed: parseDateFromFilename(f) }))
.filter(f => f.parsed);
if (files.length === 0) {
console.log('アーカイブ対象のファイルがありません');
return;
}
const today = new Date();
const currentMonth = `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, '0')}`;
const until = args.until ? new Date(args.until) : null;
const toArchive = files.filter(f => {
const fileMonth = `${f.parsed.yyyy}-${f.parsed.mm}`;
if (fileMonth === currentMonth) return false;
if (until) {
const fileDate = new Date(`${f.parsed.yyyy}-${f.parsed.mm}-${f.parsed.dd}`);
return fileDate <= until;
}
return true;
});
if (toArchive.length === 0) {
console.log('アーカイブ対象のファイルがありません');
return;
}
if (args['dry-run']) {
console.log('以下のファイルをアーカイブします:');
toArchive.forEach(f => {
const dest = path.join(f.parsed.yyyy, f.parsed.mm, f.name);
console.log(` ${f.name} -> ${dest}`);
});
return;
}
for (const f of toArchive) {
const destDir = path.join(root, f.parsed.yyyy, f.parsed.mm);
ensureDir(destDir);
const dest = path.join(destDir, f.name);
if (fs.existsSync(dest)) {
if (args.force) {
let counter = 1;
let newDest = dest;
while (fs.existsSync(newDest)) {
const ext = path.extname(f.name);
const base = path.basename(f.name, ext);
newDest = path.join(destDir, `${base}-${counter}${ext}`);
counter++;
}
fs.renameSync(path.join(root, f.name), newDest);
console.log(`移動しました: ${f.name} -> ${newDest}`);
} else {
console.log(`スキップ: ${dest} は既に存在します`);
}
} else {
const result = spawnSync('git', ['mv', f.name, dest], { cwd: root });
if (result.status === 0) {
console.log(`移動しました: ${f.name} -> ${dest}`);
} else {
fs.renameSync(path.join(root, f.name), dest);
console.log(`移動しました: ${f.name} -> ${dest} (git mv 失敗、通常の移動を使用)`);
}
}
}
}
const cmd = process.argv[2];
const args = parseArgs(process.argv.slice(3));
if (cmd === 'new') {
cmdNew(args);
} else if (cmd === 'archive' || cmd === 'arc') {
cmdArchive(args);
} else {
exitWith('使用法: til new|archive|arc [オプション]');
}
使い方
基本的な使い方
今日のTILを作成する
pnpm til new
# または
npm run til:new
これだけで、今日の日付で 25-11-06.md のようなファイルが作成されます。
タグを指定して作成する
pnpm til new -g "rust,shell"
タグは自動的に正規化され、アルファベット順に整形されます。tags.yaml にエイリアスが定義されていれば、それも自動変換されます(例: javascript → js)。
テンプレートを指定する
pnpm til new -y bug -g "shell,ux"
利用可能なテンプレート:
-
note: 基本ノート(デフォルト) -
snippet: コードスニペット -
bug: バグ記録 -
reading: 読書記録 -
retro: 振り返り
作成後にVS Codeで開く
pnpm til new -o
過去の日付で作成する
pnpm til new -d 2025-10-31 -y bug -g "shell,ux"
過去の月の日付を指定した場合、最初から 2025/10/25-10-31.md のように適切なディレクトリに作成されます。
オプション一覧
- オプション:
-
-d, --date YYYY-MM-DD: 作成日(既定: 今日)。 -
-t, --title: 本文タイトル(既定: 日付文字列)。ファイル名には使用しません。 -
-y, --type: テンプレ種類note|snippet|bug|reading|retro(既定:note)。 -
-g, --tags: カンマ区切りタグ。tags.yamlと照合し、エイリアス正規化・アルファベット順整形(未登録は警告)。 -
-o, --open: 生成後に VS Code で開きます。
-
アーカイブ機能
今月以外のファイルをアーカイブする
pnpm til arc
# または
pnpm til archive
リポジトリ直下の YY-MM-DD.md 形式のファイルのうち、今月以外のものを YYYY/MM/ ディレクトリに移動します。
アーカイブ前に確認する
pnpm til arc --dry-run
実際に移動せず、どのファイルが移動されるか確認できます。
特定の日付までをアーカイブする
pnpm til arc --until 2025-10-31
指定した日付以前(<=)のファイルのみをアーカイブします。
競合時に自動リネームする
pnpm til arc --force
移動先に同名ファイルが存在する場合、-1, -2 などを付与して移動します。
オプション一覧
- オプション:
-
--until YYYY-MM-DD: 指定日「以前(<=)」のファイルをアーカイブ対象に限定。 -
--dry-run: 実行せず予定のみ表示。 -
--force: 競合時に-1,-2などを付与して移動(既定はスキップ)。
-
実際の使用例
日々の記録
# 毎朝、今日のTILを作成してVS Codeで開く
pnpm til new -y note -g "til" -o
バグを記録する
# バグを見つけたらすぐに記録
pnpm til new -y bug -g "rust,debugging" -t "配列のインデックスエラー"
月初の整理
# まず確認
pnpm til arc --dry-run
# 問題なければ実行
pnpm til arc
ファイル構造の例
til_diary/
├── 25-11-06.md # 今月のファイル(ルート直下)
├── 25-11-07.md
├── 2025/
│ └── 10/
│ ├── 25-10-31.md # 過去月のファイル(アーカイブ済み)
│ └── 25-10-30.md
├── scripts/
│ └── til.mjs
├── templates/
│ ├── note.md
│ ├── snippet.md
│ ├── bug.md
│ ├── reading.md
│ └── retro.md
├── tags.yaml
└── package.json
メリット
- 最小限のコマンド: ファイル名や日付を入力する必要がない
- 自動整理: 月次で自動的にフォルダに整理される→基本的な執筆はルートで行える。
- タグ管理: エイリアス対応で、タグの表記ゆれを防止
- テンプレート: 用途に応じたテンプレートで、やるべき作業は内容を書くだけ!
-
Git履歴保持:
git mvを使用するため、ファイル移動の履歴が保持される
カスタマイズ
テンプレートの編集
templates/ ディレクトリ内のファイルを編集することで、テンプレートをカスタマイズできます。
タグの追加
tags.yaml に新しいタグを追加することで、タグシステムを拡張できます。
tags:
python:
desc: Python
aliases: [py]
docker:
desc: Docker
aliases: []
発展
VSCodeのショートカット登録
ルートの.vscodeにtasks.jsonを追加して以下のように設定すればpnpm new -oやpnpm arcなどのよく使う操作をタスクに設定できます。
{
"version": "2.0.0",
"tasks": [
{
"label": "TIL: new -o",
"type": "shell",
"command": "pnpm",
"args": ["til", "new", "-o"],
"options": {
"cwd": "${workspaceFolder}"
},
"problemMatcher": [],
"group": {
"kind": "build",
"isDefault": true
}
},
{
"label": "TIL: arc",
"type": "shell",
"command": "pnpm",
"args": ["til", "arc"],
"options": {
"cwd": "${workspaceFolder}"
},
"problemMatcher": []
},
{
"label": "TIL: push (today)",
"type": "shell",
"command": "pnpm",
"args": ["til", "push"],
"options": {
"cwd": "${workspaceFolder}"
},
"problemMatcher": []
}
]
}
さらにこれをVSCodeのショートカットに入れればコマンドすら打ち込まずに執筆できます。究極のズボラですね。
一応,.vscode/settings.jsonに以下のように書かないとグローバルにショートカットが設定されるのでご注意を。
{
"files.defaultLanguage": "markdown",
"editor.wordWrap": "on",
"files.trimTrailingWhitespace": true,
"frontMatter.taxonomy.tags": [
"til",
"vscode",
"js"
],
"tildiary.enableKeybindings": true
}
↑↑↑これを.vscode/settings.jsonに。↑↑↑
↓↓↓これをVScodeのショートカット設定に追加…↓↓↓
{
"key": "cmd+n",
"command": "workbench.action.tasks.runTask",
"args": "TIL: new -o",
"when": "config.tildiary.enableKeybindings"
},
{
"key": "ctrl+a",
"command": "workbench.action.tasks.runTask",
"args": "TIL: arc",
"when": "config.tildiary.enableKeybindings"
},
{
"key": "ctrl+p",
"command": "workbench.action.tasks.runTask",
"args": "TIL: push (today)",
"when": "config.tildiary.enableKeybindings"
}
さいごに
この環境を構築することで、TILを書くことだけに集中できます。ファイル名を考える必要も、フォルダに整理する必要もありません。コマンド1つで、大体の操作をやらずにすみます。
怠惰でも、ズボラでも、TILを続けられる環境が整いました。ぜひ試してみてください!
(という私は今TIL2日目なので3日坊主にならなければいいなーと思ってます。)
割とAIに聞きながら適当にスクリプトは作っているので,改良点・問題点あれば言ってください。
ここにgithubのリンクを貼っておきます。こちらをcloneしていただければすぐに使えるようになっています。
Discussion