@natjswenson/devlog 0.1.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/LICENSE +21 -0
- package/README.md +162 -0
- package/SKILL.md +174 -0
- package/bin/devlog.js +316 -0
- package/config.example.json +17 -0
- package/examples/react/DevLogPage.css +372 -0
- package/examples/react/DevLogPage.jsx +170 -0
- package/examples/react/README.md +74 -0
- package/examples/react/devlog-config.js +24 -0
- package/examples/react/useDevLogEntries.js +95 -0
- package/package.json +49 -0
- package/preview/App.jsx +84 -0
- package/preview/README.md +25 -0
- package/preview/demo.js +181 -0
- package/preview/index.html +18 -0
- package/preview/main.jsx +15 -0
- package/preview/preview.css +132 -0
- package/preview/vite.config.js +10 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Nate Swenson
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
# devlog
|
|
2
|
+
|
|
3
|
+
A Claude Code skill that turns your daily git commits into a published dev log — and a React example for displaying it on your site.
|
|
4
|
+
|
|
5
|
+
> **Build in public, automatically.** Make commits like you always do. Run `/devlog`. Today's work shows up on your site as a narrative entry, not raw commit messages.
|
|
6
|
+
|
|
7
|
+
## Live example
|
|
8
|
+
|
|
9
|
+
The skill is in production at [natejswenson.io/devlog](https://natejswenson.io/devlog), publishing entries to [github.com/natejswenson/daily-dev-log](https://github.com/natejswenson/daily-dev-log).
|
|
10
|
+
|
|
11
|
+
## How it works
|
|
12
|
+
|
|
13
|
+
1. **You commit code** in your projects, like you already do.
|
|
14
|
+
2. **Run `/devlog` in Claude Code.** The skill reads today's commits, writes a narrative markdown entry, and pushes it to your dev-log GitHub repo.
|
|
15
|
+
3. **Your site fetches it.** Static `manifest.json` + per-day markdown files served from `raw.githubusercontent.com` — no backend needed.
|
|
16
|
+
|
|
17
|
+
## Quick start
|
|
18
|
+
|
|
19
|
+
```sh
|
|
20
|
+
npx @natjswenson/devlog init
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
That command:
|
|
24
|
+
- Creates `<your-username>/daily-dev-log` on GitHub
|
|
25
|
+
- Installs the skill at `~/.claude/skills/devlog/`
|
|
26
|
+
- Writes `~/.claude/skills/devlog/config.json` with your answers
|
|
27
|
+
|
|
28
|
+
Then:
|
|
29
|
+
```sh
|
|
30
|
+
npx @natjswenson/devlog preview
|
|
31
|
+
```
|
|
32
|
+
to see your dev log rendered locally.
|
|
33
|
+
|
|
34
|
+
## Prerequisites
|
|
35
|
+
|
|
36
|
+
- **Node 18+** — for the CLI and preview app
|
|
37
|
+
- **GitHub CLI** (`gh`), authenticated — used to create your dev-log repo and push entries
|
|
38
|
+
- **Claude Code** — to run the `/devlog` skill
|
|
39
|
+
|
|
40
|
+
## What you end up with
|
|
41
|
+
|
|
42
|
+
```
|
|
43
|
+
~/.claude/skills/devlog/
|
|
44
|
+
├── SKILL.md # The slash command
|
|
45
|
+
└── config.json # Your settings (target repo, projects, etc)
|
|
46
|
+
|
|
47
|
+
github.com/<you>/daily-dev-log/ # Created by `init`, populated by /devlog
|
|
48
|
+
├── myproject/
|
|
49
|
+
│ ├── manifest.json
|
|
50
|
+
│ ├── 2026-05-01.md
|
|
51
|
+
│ └── ...
|
|
52
|
+
└── ...
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Manual setup (if you'd rather not use the CLI)
|
|
56
|
+
|
|
57
|
+
1. **Create your dev-log repo:**
|
|
58
|
+
```sh
|
|
59
|
+
gh repo create <you>/daily-dev-log --public --add-readme
|
|
60
|
+
```
|
|
61
|
+
2. **Install the skill:**
|
|
62
|
+
```sh
|
|
63
|
+
mkdir -p ~/.claude/skills/devlog
|
|
64
|
+
curl -o ~/.claude/skills/devlog/SKILL.md \
|
|
65
|
+
https://raw.githubusercontent.com/natejswenson/devlog/main/SKILL.md
|
|
66
|
+
```
|
|
67
|
+
3. **Write your config** at `~/.claude/skills/devlog/config.json` — copy [`config.example.json`](./config.example.json) and fill in.
|
|
68
|
+
|
|
69
|
+
## Add to your site
|
|
70
|
+
|
|
71
|
+
### React (drop-in)
|
|
72
|
+
|
|
73
|
+
```sh
|
|
74
|
+
cp -r examples/react/ your-site/src/devlog/
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Edit `your-site/src/devlog/devlog-config.js` to point at your repo, then mount:
|
|
78
|
+
|
|
79
|
+
```jsx
|
|
80
|
+
import DevLogPage from './devlog/DevLogPage.jsx';
|
|
81
|
+
|
|
82
|
+
<DevLogPage project="myproject" />
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Full instructions: [`examples/react/README.md`](./examples/react/README.md).
|
|
86
|
+
|
|
87
|
+
### No site yet?
|
|
88
|
+
|
|
89
|
+
The `preview/` directory is a complete deployable Vite app. Set `VITE_DEVLOG_OWNER` / `VITE_DEVLOG_REPO` / `VITE_DEVLOG_PROJECTS` env vars on Vercel, Netlify, or Cloudflare Pages, build with `vite build`, deploy `dist/`. Done — you have a public dev log at your own URL. See [`preview/README.md`](./preview/README.md).
|
|
90
|
+
|
|
91
|
+
### Other stacks (Next, Astro, plain HTML, anything)
|
|
92
|
+
|
|
93
|
+
It's static JSON and Markdown on GitHub. Build whatever UI you want — see the **Data contract** below.
|
|
94
|
+
|
|
95
|
+
## Data contract
|
|
96
|
+
|
|
97
|
+
The dev-log repo has this layout, all served as raw files from `https://raw.githubusercontent.com/<owner>/<repo>/main/`:
|
|
98
|
+
|
|
99
|
+
```
|
|
100
|
+
<repo>/
|
|
101
|
+
└── <project-key>/
|
|
102
|
+
├── manifest.json # Index of all entries (newest first)
|
|
103
|
+
├── 2026-05-01.md # One entry per day
|
|
104
|
+
├── 2026-04-30.md
|
|
105
|
+
└── ...
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
**`manifest.json`:**
|
|
109
|
+
```json
|
|
110
|
+
{
|
|
111
|
+
"entries": [
|
|
112
|
+
{ "date": "2026-05-01", "file": "2026-05-01.md", "title": "...", "summary": "..." }
|
|
113
|
+
]
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
**Entry markdown:**
|
|
118
|
+
```markdown
|
|
119
|
+
---
|
|
120
|
+
title: "Concise day summary"
|
|
121
|
+
date: 2026-05-01
|
|
122
|
+
project: myproject
|
|
123
|
+
summary: "1-2 sentence summary"
|
|
124
|
+
---
|
|
125
|
+
|
|
126
|
+
## What I Built
|
|
127
|
+
Narrative paragraphs.
|
|
128
|
+
|
|
129
|
+
## What's Next
|
|
130
|
+
Forward-looking note.
|
|
131
|
+
|
|
132
|
+
## Public Commits
|
|
133
|
+
- [myproject] commit message ([abc1234](https://github.com/.../commit/abc1234567...))
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
That's the entire contract.
|
|
137
|
+
|
|
138
|
+
## Configuration reference
|
|
139
|
+
|
|
140
|
+
`~/.claude/skills/devlog/config.json`:
|
|
141
|
+
|
|
142
|
+
| Field | Type | Description |
|
|
143
|
+
|---|---|---|
|
|
144
|
+
| `targetRepo` | `"<owner>/<repo>"` | Repo where dev log entries are published. **You create this — `init` does it for you, or `gh repo create` manually.** |
|
|
145
|
+
| `gitAuthor` | string | Used as `git log --author=...` to find your commits. |
|
|
146
|
+
| `githubUser` | string | Your GitHub username (for constructing commit links). |
|
|
147
|
+
| `projects` | array | One entry per project you want dev logs for. |
|
|
148
|
+
| `projects[].key` | string | Subdirectory name in the dev-log repo + tab label on the UI. |
|
|
149
|
+
| `projects[].path` | string | Local filesystem path to the project. |
|
|
150
|
+
| `projects[].remote` | `"<owner>/<repo>"` | The project's GitHub remote, used to mark public commits and link them. |
|
|
151
|
+
|
|
152
|
+
See [`config.example.json`](./config.example.json) for a complete template.
|
|
153
|
+
|
|
154
|
+
## Customization
|
|
155
|
+
|
|
156
|
+
- **Tweak the entry template:** edit `~/.claude/skills/devlog/SKILL.md` (Step 4 — generate the entry).
|
|
157
|
+
- **Tweak the UI:** override the `--devlog-*` CSS variables in `examples/react/DevLogPage.css` to match your theme. Or build your own UI against the data contract above.
|
|
158
|
+
- **Add more projects:** edit `~/.claude/skills/devlog/config.json` directly.
|
|
159
|
+
|
|
160
|
+
## License
|
|
161
|
+
|
|
162
|
+
MIT
|
package/SKILL.md
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: devlog
|
|
3
|
+
description: Generate a daily dev log entry from today's git commits and publish to GitHub
|
|
4
|
+
user_invocable: true
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# /devlog — Daily Dev Log Generator
|
|
8
|
+
|
|
9
|
+
You are generating a daily dev log entry from the user's git commits and publishing it to a GitHub repo configured in `~/.claude/skills/devlog/config.json`.
|
|
10
|
+
|
|
11
|
+
Usage: `/devlog` (all configured projects) or `/devlog <project-key>` (single project)
|
|
12
|
+
|
|
13
|
+
## Configuration
|
|
14
|
+
|
|
15
|
+
This skill is configuration-driven. All user-specific values (target repo, git author, project list) live in `~/.claude/skills/devlog/config.json`.
|
|
16
|
+
|
|
17
|
+
Schema:
|
|
18
|
+
|
|
19
|
+
```json
|
|
20
|
+
{
|
|
21
|
+
"targetRepo": "<owner>/<repo>",
|
|
22
|
+
"gitAuthor": "Your Name",
|
|
23
|
+
"githubUser": "<your-github-username>",
|
|
24
|
+
"projects": [
|
|
25
|
+
{
|
|
26
|
+
"key": "project-key",
|
|
27
|
+
"path": "/absolute/path/to/project",
|
|
28
|
+
"remote": "<owner>/<repo>"
|
|
29
|
+
}
|
|
30
|
+
]
|
|
31
|
+
}
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Step 0: Load and validate config
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
cat ~/.claude/skills/devlog/config.json
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
If the file does not exist or cannot be parsed, stop and tell the user:
|
|
41
|
+
|
|
42
|
+
> No devlog config found at `~/.claude/skills/devlog/config.json`. Run `npx @natjswenson/devlog init` to set up, or copy `config.example.json` from the devlog repo and fill it in manually.
|
|
43
|
+
|
|
44
|
+
Validate that `targetRepo`, `gitAuthor`, `githubUser`, and `projects` (non-empty array) are all present. Each project must have `key`, `path`, and `remote`.
|
|
45
|
+
|
|
46
|
+
## Step 1: Determine scope
|
|
47
|
+
|
|
48
|
+
- If the user passed a project argument (e.g. `/devlog myproject`), filter `projects` to that one. If the key is not in the registry, list available keys and stop.
|
|
49
|
+
- If no argument, run for **all projects** in `config.projects`. Generate a separate entry per project (only for projects that have commits today). Use a single clone of the target repo and a single commit/push for all entries.
|
|
50
|
+
|
|
51
|
+
## Step 2: Gather today's commits
|
|
52
|
+
|
|
53
|
+
For each project in scope, run:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
cd <project.path> && git log --author="<config.gitAuthor>" --since="midnight" --format="%H|%s|%D" --all
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
If no commits are found for a project, skip it. If no commits are found across all projects, inform the user and stop.
|
|
60
|
+
|
|
61
|
+
## Step 3: Check for public commits
|
|
62
|
+
|
|
63
|
+
For each commit, check if it's on the `main` branch and if the remote is public:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
cd <project.path> && git remote get-url origin
|
|
67
|
+
git branch --contains <hash> -r 2>/dev/null | grep -q 'origin/main'
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
- If the remote URL matches `<project.remote>` (i.e. `github.com/<project.remote>` or the SSH equivalent) and the commit is on `origin/main`, it's a public commit — include a link using `https://github.com/<project.remote>/commit/<hash>`.
|
|
71
|
+
- Otherwise, describe the feature without linking.
|
|
72
|
+
|
|
73
|
+
## Step 4: Generate the entry
|
|
74
|
+
|
|
75
|
+
Based on the commit messages, generate a markdown entry with this structure:
|
|
76
|
+
|
|
77
|
+
```markdown
|
|
78
|
+
---
|
|
79
|
+
title: "<concise title summarizing the day's work>"
|
|
80
|
+
date: YYYY-MM-DD
|
|
81
|
+
project: <project.key>
|
|
82
|
+
summary: "<1-2 sentence summary>"
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
## What I Built
|
|
86
|
+
|
|
87
|
+
<Narrative paragraphs about features implemented. Focus on WHAT was built and WHY, not raw commit messages. Group related commits into coherent feature descriptions. Write in first person, casual but professional tone.>
|
|
88
|
+
|
|
89
|
+
## What's Next
|
|
90
|
+
|
|
91
|
+
<Brief 1-2 sentence forward-looking note based on the trajectory of current work.>
|
|
92
|
+
|
|
93
|
+
## Public Commits
|
|
94
|
+
|
|
95
|
+
- [<project.key>] commit message ([short-hash](https://github.com/<project.remote>/commit/full-hash))
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
**Important rules for content generation:**
|
|
99
|
+
- The "What I Built" section is a NARRATIVE, not a commit list. Describe features, not individual commits.
|
|
100
|
+
- Only include "Public Commits" section if there are commits on `main` of a public repo.
|
|
101
|
+
- "What's Next" should be a reasonable inference from the work done today.
|
|
102
|
+
- Tone: first person, casual but professional, like a senior engineer's standup notes for a public audience.
|
|
103
|
+
|
|
104
|
+
## Step 5: Check for existing entry (append mode)
|
|
105
|
+
|
|
106
|
+
For each project with commits, check if an entry for today already exists:
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
gh api repos/<config.targetRepo>/contents/<project.key>/YYYY-MM-DD.md --jq '.content' 2>/dev/null | base64 -d
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
**If the entry exists:**
|
|
113
|
+
1. Fetch and read the existing content
|
|
114
|
+
2. Keep the original frontmatter (title, date, project, summary) unchanged
|
|
115
|
+
3. Append new content under an `## Update — HH:MM AM/PM` heading
|
|
116
|
+
4. Merge any new public commits into the existing "Public Commits" section
|
|
117
|
+
5. Update "What's Next" with the latest context
|
|
118
|
+
|
|
119
|
+
**If the entry does NOT exist:**
|
|
120
|
+
1. Create a new file with the full structure above
|
|
121
|
+
|
|
122
|
+
## Step 6: Push to GitHub
|
|
123
|
+
|
|
124
|
+
Clone the repo once, write all project entries, then push:
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
# Clone to temp directory
|
|
128
|
+
TMPDIR=$(mktemp -d)
|
|
129
|
+
cd "$TMPDIR"
|
|
130
|
+
git clone https://github.com/<config.targetRepo>.git
|
|
131
|
+
cd $(basename <config.targetRepo>)
|
|
132
|
+
|
|
133
|
+
# For each project with commits:
|
|
134
|
+
# - Create directory if needed: mkdir -p <project.key>/
|
|
135
|
+
# - Write the entry file to <project.key>/YYYY-MM-DD.md
|
|
136
|
+
# - Update <project.key>/manifest.json
|
|
137
|
+
# - Read manifest, add/update entry in entries array (newest first)
|
|
138
|
+
# - Entry object: { "date": "YYYY-MM-DD", "file": "YYYY-MM-DD.md", "title": "...", "summary": "..." }
|
|
139
|
+
# - If appending to existing entry, update title/summary only if changed
|
|
140
|
+
# - If manifest doesn't exist, create it as { "entries": [...] }
|
|
141
|
+
|
|
142
|
+
# Stage all changed project directories
|
|
143
|
+
git add .
|
|
144
|
+
|
|
145
|
+
# Single commit covering all projects
|
|
146
|
+
git commit -m "devlog: add entries for YYYY-MM-DD"
|
|
147
|
+
git push origin main
|
|
148
|
+
|
|
149
|
+
# Cleanup
|
|
150
|
+
rm -rf "$TMPDIR"
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
## Step 7: Confirm
|
|
154
|
+
|
|
155
|
+
After pushing, output a summary for each project:
|
|
156
|
+
|
|
157
|
+
```
|
|
158
|
+
Dev log entries published for <Month Day, Year>
|
|
159
|
+
|
|
160
|
+
Project: <project.key>
|
|
161
|
+
Commits summarized: <count>
|
|
162
|
+
Public commits linked: <count>
|
|
163
|
+
URL: https://github.com/<config.targetRepo>/blob/main/<project.key>/YYYY-MM-DD.md
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
## Edge Cases
|
|
167
|
+
|
|
168
|
+
- **No commits today:** Stop with a message. Do not create an empty entry.
|
|
169
|
+
- **Project path doesn't exist:** Error with "Repository not found at <project.path>" and skip that project.
|
|
170
|
+
- **Push fails:** Inform the user of the error. Do not retry automatically.
|
|
171
|
+
- **All WIP/fixup commits:** Still generate a narrative about the intent of the work.
|
|
172
|
+
- **Manifest doesn't exist:** Create it with the standard structure.
|
|
173
|
+
- **Unknown project argument:** List available project keys from `config.projects`.
|
|
174
|
+
- **Config missing or invalid:** Stop at Step 0 with the setup instructions above.
|
package/bin/devlog.js
ADDED
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { spawn, execSync } from 'node:child_process';
|
|
3
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, copyFileSync } from 'node:fs';
|
|
4
|
+
import { homedir } from 'node:os';
|
|
5
|
+
import { dirname, join, resolve, basename } from 'node:path';
|
|
6
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
7
|
+
import { createRequire } from 'node:module';
|
|
8
|
+
import prompts from 'prompts';
|
|
9
|
+
import kleur from 'kleur';
|
|
10
|
+
|
|
11
|
+
const require = createRequire(import.meta.url);
|
|
12
|
+
const PACKAGE_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
|
13
|
+
const SKILL_SRC = join(PACKAGE_ROOT, 'SKILL.md');
|
|
14
|
+
const CONFIG_DIR = join(homedir(), '.claude', 'skills', 'devlog');
|
|
15
|
+
const CONFIG_PATH = join(CONFIG_DIR, 'config.json');
|
|
16
|
+
const SKILL_DEST = join(CONFIG_DIR, 'SKILL.md');
|
|
17
|
+
const PREVIEW_DIR = join(PACKAGE_ROOT, 'preview');
|
|
18
|
+
|
|
19
|
+
const log = {
|
|
20
|
+
info: (msg) => console.log(msg),
|
|
21
|
+
ok: (msg) => console.log(kleur.green('✓ ') + msg),
|
|
22
|
+
warn: (msg) => console.log(kleur.yellow('! ') + msg),
|
|
23
|
+
err: (msg) => console.error(kleur.red('✗ ') + msg),
|
|
24
|
+
step: (msg) => console.log(kleur.cyan('→ ') + msg),
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
function readPackageVersion() {
|
|
28
|
+
const pkg = JSON.parse(readFileSync(join(PACKAGE_ROOT, 'package.json'), 'utf8'));
|
|
29
|
+
return pkg.version;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function tryExec(cmd) {
|
|
33
|
+
try {
|
|
34
|
+
return execSync(cmd, { stdio: ['ignore', 'pipe', 'pipe'], encoding: 'utf8' }).trim();
|
|
35
|
+
} catch {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function expandHome(p) {
|
|
41
|
+
if (!p) return p;
|
|
42
|
+
if (p === '~') return homedir();
|
|
43
|
+
if (p.startsWith('~/')) return join(homedir(), p.slice(2));
|
|
44
|
+
return p;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function preflight() {
|
|
48
|
+
const nodeMajor = parseInt(process.versions.node.split('.')[0], 10);
|
|
49
|
+
if (nodeMajor < 18) {
|
|
50
|
+
log.err(`Node 18+ required (you have ${process.versions.node}).`);
|
|
51
|
+
process.exit(1);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const ghVersion = tryExec('gh --version');
|
|
55
|
+
if (!ghVersion) {
|
|
56
|
+
log.err('GitHub CLI (`gh`) is not installed.');
|
|
57
|
+
log.info('Install: https://cli.github.com/');
|
|
58
|
+
process.exit(1);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const ghAuth = tryExec('gh auth status');
|
|
62
|
+
if (!ghAuth) {
|
|
63
|
+
log.err('GitHub CLI is not authenticated.');
|
|
64
|
+
log.info('Run: gh auth login');
|
|
65
|
+
process.exit(1);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function detectGhUser() {
|
|
70
|
+
const out = tryExec('gh api user --jq .login');
|
|
71
|
+
return out || null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function detectGitName() {
|
|
75
|
+
return tryExec('git config --global user.name');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function detectProjectRemote(path) {
|
|
79
|
+
const url = tryExec(`git -C "${path}" remote get-url origin`);
|
|
80
|
+
if (!url) return null;
|
|
81
|
+
const m = url.match(/[:/]([^/:]+\/[^/]+?)(?:\.git)?$/);
|
|
82
|
+
return m ? m[1] : null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function confirmOverwrite(label, path) {
|
|
86
|
+
if (!existsSync(path)) return true;
|
|
87
|
+
const { ok } = await prompts({
|
|
88
|
+
type: 'confirm',
|
|
89
|
+
name: 'ok',
|
|
90
|
+
message: `${label} already exists at ${path}. Overwrite?`,
|
|
91
|
+
initial: false,
|
|
92
|
+
});
|
|
93
|
+
return ok === true;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async function cmdInit() {
|
|
97
|
+
log.info(kleur.bold('\ndevlog setup\n'));
|
|
98
|
+
await preflight();
|
|
99
|
+
|
|
100
|
+
const defaults = {
|
|
101
|
+
gitAuthor: detectGitName() || '',
|
|
102
|
+
githubUser: detectGhUser() || '',
|
|
103
|
+
targetRepoName: 'daily-dev-log',
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const answers = await prompts([
|
|
107
|
+
{
|
|
108
|
+
type: 'text',
|
|
109
|
+
name: 'gitAuthor',
|
|
110
|
+
message: 'Your name (used to filter `git log --author`):',
|
|
111
|
+
initial: defaults.gitAuthor,
|
|
112
|
+
validate: (v) => v.trim().length > 0 || 'Required',
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
type: 'text',
|
|
116
|
+
name: 'githubUser',
|
|
117
|
+
message: 'Your GitHub username:',
|
|
118
|
+
initial: defaults.githubUser,
|
|
119
|
+
validate: (v) => /^[a-z0-9-]+$/i.test(v.trim()) || 'Invalid username',
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
type: 'text',
|
|
123
|
+
name: 'targetRepoName',
|
|
124
|
+
message: 'Name of the repo where dev logs will be published:',
|
|
125
|
+
initial: defaults.targetRepoName,
|
|
126
|
+
validate: (v) => /^[a-z0-9._-]+$/i.test(v.trim()) || 'Invalid repo name',
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
type: 'confirm',
|
|
130
|
+
name: 'registerProject',
|
|
131
|
+
message: 'Register a project now? (you can add more later by editing config.json)',
|
|
132
|
+
initial: true,
|
|
133
|
+
},
|
|
134
|
+
], { onCancel: () => process.exit(1) });
|
|
135
|
+
|
|
136
|
+
let projectAnswers = null;
|
|
137
|
+
if (answers.registerProject) {
|
|
138
|
+
const cwd = process.cwd();
|
|
139
|
+
const cwdRemote = detectProjectRemote(cwd);
|
|
140
|
+
projectAnswers = await prompts([
|
|
141
|
+
{
|
|
142
|
+
type: 'text',
|
|
143
|
+
name: 'path',
|
|
144
|
+
message: 'Project absolute path:',
|
|
145
|
+
initial: cwd,
|
|
146
|
+
validate: (v) => existsSync(expandHome(v)) || 'Path does not exist',
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
type: 'text',
|
|
150
|
+
name: 'key',
|
|
151
|
+
message: 'Project key (used as dev-log subdir name):',
|
|
152
|
+
initial: (prev) => basename(expandHome(prev || cwd)),
|
|
153
|
+
validate: (v) => /^[a-z0-9._-]+$/i.test(v.trim()) || 'Invalid key',
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
type: 'text',
|
|
157
|
+
name: 'remote',
|
|
158
|
+
message: 'Project GitHub remote (<owner>/<repo>):',
|
|
159
|
+
initial: (_prev, values) => detectProjectRemote(expandHome(values.path)) || cwdRemote || `${answers.githubUser}/${basename(expandHome(values.path))}`,
|
|
160
|
+
validate: (v) => /^[\w.-]+\/[\w.-]+$/.test(v.trim()) || 'Expected <owner>/<repo>',
|
|
161
|
+
},
|
|
162
|
+
], { onCancel: () => process.exit(1) });
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const targetRepo = `${answers.githubUser}/${answers.targetRepoName}`;
|
|
166
|
+
const config = {
|
|
167
|
+
targetRepo,
|
|
168
|
+
gitAuthor: answers.gitAuthor,
|
|
169
|
+
githubUser: answers.githubUser,
|
|
170
|
+
projects: projectAnswers ? [{
|
|
171
|
+
key: projectAnswers.key,
|
|
172
|
+
path: expandHome(projectAnswers.path),
|
|
173
|
+
remote: projectAnswers.remote,
|
|
174
|
+
}] : [],
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
log.info('\n' + kleur.bold('Summary:'));
|
|
178
|
+
log.info(` Target repo: ${kleur.cyan(`github.com/${targetRepo}`)}`);
|
|
179
|
+
log.info(` Git author: ${config.gitAuthor}`);
|
|
180
|
+
log.info(` GitHub user: ${config.githubUser}`);
|
|
181
|
+
log.info(` Projects: ${config.projects.length === 0 ? '(none — add later)' : config.projects.map(p => p.key).join(', ')}`);
|
|
182
|
+
log.info(` Skill location: ${CONFIG_DIR}`);
|
|
183
|
+
|
|
184
|
+
const { proceed } = await prompts({
|
|
185
|
+
type: 'confirm',
|
|
186
|
+
name: 'proceed',
|
|
187
|
+
message: 'Continue?',
|
|
188
|
+
initial: true,
|
|
189
|
+
}, { onCancel: () => process.exit(1) });
|
|
190
|
+
if (!proceed) process.exit(0);
|
|
191
|
+
|
|
192
|
+
log.info('');
|
|
193
|
+
|
|
194
|
+
const repoExists = tryExec(`gh repo view ${targetRepo} --json name`) !== null;
|
|
195
|
+
if (repoExists) {
|
|
196
|
+
log.warn(`Repo github.com/${targetRepo} already exists. Will use it as-is.`);
|
|
197
|
+
} else {
|
|
198
|
+
log.step(`Creating github.com/${targetRepo}...`);
|
|
199
|
+
try {
|
|
200
|
+
execSync(`gh repo create ${targetRepo} --public --description "Daily dev log" --add-readme`, {
|
|
201
|
+
stdio: 'inherit',
|
|
202
|
+
});
|
|
203
|
+
log.ok('Repo created');
|
|
204
|
+
} catch {
|
|
205
|
+
log.err('Failed to create repo. Check `gh` permissions.');
|
|
206
|
+
process.exit(1);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (!existsSync(CONFIG_DIR)) {
|
|
211
|
+
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
212
|
+
log.ok(`Created ${CONFIG_DIR}`);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (await confirmOverwrite('SKILL.md', SKILL_DEST)) {
|
|
216
|
+
copyFileSync(SKILL_SRC, SKILL_DEST);
|
|
217
|
+
log.ok(`Installed SKILL.md → ${SKILL_DEST}`);
|
|
218
|
+
} else {
|
|
219
|
+
log.warn('Skipped SKILL.md');
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (await confirmOverwrite('config.json', CONFIG_PATH)) {
|
|
223
|
+
writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2) + '\n');
|
|
224
|
+
log.ok(`Wrote config → ${CONFIG_PATH}`);
|
|
225
|
+
} else {
|
|
226
|
+
log.warn('Skipped config.json');
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
log.info('\n' + kleur.bold().green('Done.') + '\n');
|
|
230
|
+
log.info('Next steps:');
|
|
231
|
+
log.info(` 1. ${config.projects.length === 0 ? 'Edit config.json to register projects' : '(Optional) edit config.json to register more projects'}`);
|
|
232
|
+
log.info(' 2. Make some commits in a registered project');
|
|
233
|
+
log.info(' 3. In Claude Code, run: /devlog');
|
|
234
|
+
log.info(' 4. Preview locally: npx @natjswenson/devlog preview');
|
|
235
|
+
log.info('');
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
async function cmdPreview() {
|
|
239
|
+
if (!existsSync(CONFIG_PATH)) {
|
|
240
|
+
log.err(`No config found at ${CONFIG_PATH}`);
|
|
241
|
+
log.info('Run `npx @natjswenson/devlog init` first.');
|
|
242
|
+
process.exit(1);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
let config;
|
|
246
|
+
try {
|
|
247
|
+
config = JSON.parse(readFileSync(CONFIG_PATH, 'utf8'));
|
|
248
|
+
} catch (e) {
|
|
249
|
+
log.err(`Failed to parse config: ${e.message}`);
|
|
250
|
+
process.exit(1);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const [owner, repo] = (config.targetRepo || '').split('/');
|
|
254
|
+
if (!owner || !repo) {
|
|
255
|
+
log.err('config.targetRepo is not in <owner>/<repo> format');
|
|
256
|
+
process.exit(1);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const projects = (config.projects || []).map((p) => ({ key: p.key, label: p.key }));
|
|
260
|
+
|
|
261
|
+
log.step(`Launching preview against github.com/${config.targetRepo}...`);
|
|
262
|
+
|
|
263
|
+
const vitePkgPath = require.resolve('vite/package.json');
|
|
264
|
+
const vitePkg = JSON.parse(readFileSync(vitePkgPath, 'utf8'));
|
|
265
|
+
const viteBin = resolve(dirname(vitePkgPath), vitePkg.bin?.vite || 'bin/vite.js');
|
|
266
|
+
|
|
267
|
+
const proc = spawn(process.execPath, [viteBin], {
|
|
268
|
+
cwd: PREVIEW_DIR,
|
|
269
|
+
stdio: 'inherit',
|
|
270
|
+
env: {
|
|
271
|
+
...process.env,
|
|
272
|
+
VITE_DEVLOG_OWNER: owner,
|
|
273
|
+
VITE_DEVLOG_REPO: repo,
|
|
274
|
+
VITE_DEVLOG_BRANCH: 'main',
|
|
275
|
+
VITE_DEVLOG_PROJECTS: JSON.stringify(projects),
|
|
276
|
+
},
|
|
277
|
+
});
|
|
278
|
+
proc.on('exit', (code) => process.exit(code ?? 0));
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function printHelp() {
|
|
282
|
+
console.log(`
|
|
283
|
+
${kleur.bold('@natjswenson/devlog')} — daily dev log generator
|
|
284
|
+
|
|
285
|
+
Usage:
|
|
286
|
+
npx @natjswenson/devlog init Set up the skill, create your dev-log repo, write config
|
|
287
|
+
npx @natjswenson/devlog preview Run a local preview of your published dev log
|
|
288
|
+
npx @natjswenson/devlog --help
|
|
289
|
+
npx @natjswenson/devlog --version
|
|
290
|
+
|
|
291
|
+
Docs: https://github.com/natejswenson/devlog
|
|
292
|
+
`);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const arg = process.argv[2];
|
|
296
|
+
switch (arg) {
|
|
297
|
+
case 'init':
|
|
298
|
+
cmdInit();
|
|
299
|
+
break;
|
|
300
|
+
case 'preview':
|
|
301
|
+
cmdPreview();
|
|
302
|
+
break;
|
|
303
|
+
case '-v':
|
|
304
|
+
case '--version':
|
|
305
|
+
console.log(readPackageVersion());
|
|
306
|
+
break;
|
|
307
|
+
case undefined:
|
|
308
|
+
case '-h':
|
|
309
|
+
case '--help':
|
|
310
|
+
printHelp();
|
|
311
|
+
break;
|
|
312
|
+
default:
|
|
313
|
+
log.err(`Unknown command: ${arg}`);
|
|
314
|
+
printHelp();
|
|
315
|
+
process.exit(1);
|
|
316
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"targetRepo": "yourusername/daily-dev-log",
|
|
3
|
+
"gitAuthor": "Your Name",
|
|
4
|
+
"githubUser": "yourusername",
|
|
5
|
+
"projects": [
|
|
6
|
+
{
|
|
7
|
+
"key": "midnight-side-quest",
|
|
8
|
+
"path": "/Users/yourusername/code/midnight-side-quest",
|
|
9
|
+
"remote": "yourusername/midnight-side-quest"
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
"key": "todays-existential-crisis",
|
|
13
|
+
"path": "/Users/yourusername/code/todays-existential-crisis",
|
|
14
|
+
"remote": "yourusername/todays-existential-crisis"
|
|
15
|
+
}
|
|
16
|
+
]
|
|
17
|
+
}
|