@dotdotdash/afterhours 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/README.md +189 -0
- package/dist/chunk-UC2WX3XO.js +56 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +2920 -0
- package/dist/report-2P72ZPRH.js +10 -0
- package/package.json +62 -0
- package/templates/config.yml +90 -0
- package/templates/docker-cron/Dockerfile +23 -0
- package/templates/docker-cron/README.md +31 -0
- package/templates/docker-cron/crontab +3 -0
- package/templates/github-actions.yml +50 -0
package/README.md
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
# afterhours
|
|
2
|
+
|
|
3
|
+
**afterhours** is an autonomous, schedulable system that keeps your Node.js / web projects up-to-date while you sleep. It audits dependencies, applies agent-driven code optimizations, verifies everything still works, opens a pull request, and (optionally) merges and deploys — all hands-free.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- **Dependency audit** — finds vulnerable and outdated packages, upgrades safe deps automatically, uses an LLM agent to fix any breakages
|
|
10
|
+
- **Code optimization** — runs an LLM agent to find and apply code quality improvements, verifies each change with your build + test suite
|
|
11
|
+
- **Deployment** — pushes Docker images, publishes npm packages, creates GitHub Releases, deploys to Vercel or Render
|
|
12
|
+
- **Platform integrations** — GitHub, GitLab, Azure Repos, AWS CodeCommit, Google Cloud Source (PR/MR creation, auto-merge, commenting)
|
|
13
|
+
- **Notifications** — PR comment, email, or webhook on every run
|
|
14
|
+
- **Scheduler adapters** — GitHub Actions, Docker + cron, or bring your own
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## Install
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npm install -g @dotdotdash/afterhours
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Or use `npx @dotdotdash/afterhours` without installing globally. The CLI command is `afterhours`.
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## Quick start
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
# 1. Scaffold config
|
|
32
|
+
cd /your/project
|
|
33
|
+
afterhours init --scheduler github
|
|
34
|
+
|
|
35
|
+
# 2. Add secrets to .env (never commit it)
|
|
36
|
+
cp .env.example .env
|
|
37
|
+
# Fill in GITHUB_TOKEN, ANTHROPIC_API_KEY, etc.
|
|
38
|
+
|
|
39
|
+
# 3. Verify the setup
|
|
40
|
+
afterhours doctor
|
|
41
|
+
|
|
42
|
+
# 4. Do a dry run
|
|
43
|
+
afterhours run --dry-run
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## Configuration
|
|
49
|
+
|
|
50
|
+
`afterhours init` creates `.afterhours/config.yml`. The full annotated reference is in [`templates/config.yml`](templates/config.yml).
|
|
51
|
+
|
|
52
|
+
### Minimal config
|
|
53
|
+
|
|
54
|
+
```yaml
|
|
55
|
+
version: 1
|
|
56
|
+
|
|
57
|
+
project:
|
|
58
|
+
type: auto # auto-detected
|
|
59
|
+
packageManager: auto
|
|
60
|
+
|
|
61
|
+
platform:
|
|
62
|
+
provider: github
|
|
63
|
+
baseBranch: main
|
|
64
|
+
autoMerge: false
|
|
65
|
+
|
|
66
|
+
llm:
|
|
67
|
+
provider: anthropic
|
|
68
|
+
model: claude-sonnet-4-5
|
|
69
|
+
apiKeyEnv: ANTHROPIC_API_KEY
|
|
70
|
+
|
|
71
|
+
tasks:
|
|
72
|
+
dependencyAudit:
|
|
73
|
+
enabled: true
|
|
74
|
+
codeOptimization:
|
|
75
|
+
enabled: true
|
|
76
|
+
deployment:
|
|
77
|
+
enabled: false
|
|
78
|
+
targets: []
|
|
79
|
+
issueTriage:
|
|
80
|
+
enabled: false # resolve GitHub issues automatically (branch, PR, close on success)
|
|
81
|
+
allowedAuthors:
|
|
82
|
+
- "*" # or specific usernames, e.g. ["octocat", "hubot"]
|
|
83
|
+
|
|
84
|
+
notifications:
|
|
85
|
+
- type: github
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Issue triage (`tasks.issueTriage`) requires a platform provider that supports issues (currently GitHub). When enabled, afterhours lists open issues, filters them to `allowedAuthors`, and for each match creates a branch, runs the agent to attempt a fix, and:
|
|
89
|
+
|
|
90
|
+
- On success: verifies the change, commits, opens a PR (auto-merging if `platform.autoMerge` is set), and closes the issue with a comment.
|
|
91
|
+
- On failure (no changes made, or verify gate fails): leaves the issue open and adds a comment explaining what happened.
|
|
92
|
+
|
|
93
|
+
### Using a GitHub Copilot seat for the agent
|
|
94
|
+
|
|
95
|
+
Set `llm.provider: copilot` to drive the agent with a GitHub Copilot subscription instead of a direct LLM API key:
|
|
96
|
+
|
|
97
|
+
```yaml
|
|
98
|
+
llm:
|
|
99
|
+
provider: copilot
|
|
100
|
+
model: gpt-4o # any Copilot-supported chat model
|
|
101
|
+
apiKeyEnv: COPILOT_TOKEN # GitHub token for the Copilot-licensed account
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
The Copilot account is fully independent from `platform` (repo access) — `apiKeyEnv` here can point to a token for a different GitHub account than `GITHUB_TOKEN`. This lets an org member's Copilot seat drive automated work against a repo (e.g. a client-owned repo) where only a separate PAT has push/PR access. Note that this relies on GitHub's internal Copilot chat-completions API (the same mechanism used by third-party editor integrations); confirm it's compatible with your Copilot license terms before enabling it for unattended/agentic use.
|
|
105
|
+
|
|
106
|
+
---
|
|
107
|
+
|
|
108
|
+
## Environment variables
|
|
109
|
+
|
|
110
|
+
All secrets are read from environment variables — never from config files. The canonical list is in [`.env.example`](.env.example).
|
|
111
|
+
|
|
112
|
+
| Variable | Used for |
|
|
113
|
+
|---|---|
|
|
114
|
+
| `GITHUB_TOKEN` | GitHub API + auto-merge + notifications |
|
|
115
|
+
| `ANTHROPIC_API_KEY` | LLM agent (Anthropic) |
|
|
116
|
+
| `OPENAI_API_KEY` | LLM agent (OpenAI) |
|
|
117
|
+
| `GEMINI_API_KEY` | LLM agent (Google Gemini) |
|
|
118
|
+
| `COPILOT_TOKEN` | LLM agent (GitHub Copilot); can belong to a different GitHub account than `GITHUB_TOKEN` |
|
|
119
|
+
| `NPM_TOKEN` | npm publish |
|
|
120
|
+
| `REGISTRY_USER` / `REGISTRY_PASS` | Docker registry login |
|
|
121
|
+
| `VERCEL_TOKEN` / `VERCEL_PROJECT_ID` / `VERCEL_ORG_ID` | Vercel deploy |
|
|
122
|
+
| `RENDER_API_KEY` / `RENDER_SERVICE_ID` | Render deploy |
|
|
123
|
+
|
|
124
|
+
---
|
|
125
|
+
|
|
126
|
+
## Security
|
|
127
|
+
|
|
128
|
+
- Secrets are **never** written to disk or logged. All log output is redacted against known env values.
|
|
129
|
+
- Agent file-system tools enforce path-traversal checks (`assertSafePath`).
|
|
130
|
+
- Agent shell-tool has a denylist for dangerous patterns (`rm -rf /`, `dd if=`, etc.).
|
|
131
|
+
- LLM prompts include "minimal changes" and no-secrets guardrails.
|
|
132
|
+
- Dependency upgrades are split into **safe** (patch/minor, non-deprecated) and **risky** (major, deprecated) and the risky set is skipped unless `allowMajor: true`.
|
|
133
|
+
- Every change must pass the full verify gate (build + tests) before it is committed.
|
|
134
|
+
- The PR is opened without auto-merge by default.
|
|
135
|
+
|
|
136
|
+
### OWASP Top 10 mitigations
|
|
137
|
+
|
|
138
|
+
| Risk | Mitigation |
|
|
139
|
+
|---|---|
|
|
140
|
+
| Injection | Command denylist; no shell=true; args passed as arrays |
|
|
141
|
+
| Broken access control | Path traversal guard on all agent FS tools |
|
|
142
|
+
| Sensitive data exposure | Secrets redacted from all log/output; env-only secret resolution |
|
|
143
|
+
| Insecure deps | Core task: `npm audit`; automated upgrades with gate |
|
|
144
|
+
|
|
145
|
+
---
|
|
146
|
+
|
|
147
|
+
## Scheduler adapters
|
|
148
|
+
|
|
149
|
+
### GitHub Actions (recommended)
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
afterhours init --scheduler github
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
Creates `.github/workflows/afterhours.yml`. Add `ANTHROPIC_API_KEY` (or equivalent) as a repository secret.
|
|
156
|
+
|
|
157
|
+
### Docker + cron
|
|
158
|
+
|
|
159
|
+
```bash
|
|
160
|
+
afterhours init --scheduler docker-cron
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
Creates `.afterhours/docker-cron/` — a `Dockerfile` + `crontab`. Build and run:
|
|
164
|
+
|
|
165
|
+
```bash
|
|
166
|
+
cd .afterhours/docker-cron
|
|
167
|
+
docker build -t afterhours-cron .
|
|
168
|
+
docker run -d --env-file ../../.env -v $(pwd)/../../:/workspace afterhours-cron
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
---
|
|
172
|
+
|
|
173
|
+
## CLI reference
|
|
174
|
+
|
|
175
|
+
```
|
|
176
|
+
afterhours init [--scheduler github|docker-cron|none] [--force]
|
|
177
|
+
afterhours run [--dry-run] [--task <id>] [--no-deploy]
|
|
178
|
+
afterhours doctor [--platform]
|
|
179
|
+
afterhours report [runId]
|
|
180
|
+
afterhours audit # (planned)
|
|
181
|
+
afterhours optimize # (planned)
|
|
182
|
+
afterhours deploy # (planned)
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
---
|
|
186
|
+
|
|
187
|
+
## License
|
|
188
|
+
|
|
189
|
+
MIT
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// src/report/index.ts
|
|
2
|
+
import { mkdirSync, writeFileSync } from "fs";
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
function renderMarkdown(report) {
|
|
5
|
+
const lines = [
|
|
6
|
+
`# afterhours Run Report (${report.id})`,
|
|
7
|
+
"",
|
|
8
|
+
`**Started:** ${report.startedAt} `,
|
|
9
|
+
report.completedAt ? `**Completed:** ${report.completedAt}` : "",
|
|
10
|
+
"",
|
|
11
|
+
`**Project:** ${report.projectType} / ${report.projectPackageManager} `,
|
|
12
|
+
`**Branch:** ${report.branch} `,
|
|
13
|
+
`**Base:** ${report.baseBranch}`,
|
|
14
|
+
"",
|
|
15
|
+
"## Tasks",
|
|
16
|
+
""
|
|
17
|
+
];
|
|
18
|
+
for (const task of report.tasks) {
|
|
19
|
+
const icon = task.status === "changed" ? "\u2705" : task.status === "noop" ? "\u23ED" : task.status === "failed" ? "\u274C" : "\u26A0\uFE0F";
|
|
20
|
+
lines.push(`### ${icon} ${task.title} (${task.status})`);
|
|
21
|
+
if (task.summary) lines.push(`> ${task.summary}`);
|
|
22
|
+
if (task.commit) lines.push(`**Commit:** ${task.commit}`);
|
|
23
|
+
if (task.outstandingReason) lines.push(`**Outstanding:** ${task.outstandingReason}`);
|
|
24
|
+
lines.push("");
|
|
25
|
+
}
|
|
26
|
+
if (report.outstandingWork.length > 0) {
|
|
27
|
+
lines.push("## Outstanding Work");
|
|
28
|
+
for (const item of report.outstandingWork) {
|
|
29
|
+
lines.push(`- ${item}`);
|
|
30
|
+
}
|
|
31
|
+
lines.push("");
|
|
32
|
+
}
|
|
33
|
+
if (report.pr) {
|
|
34
|
+
lines.push(`## Pull Request`);
|
|
35
|
+
lines.push(`[${report.pr.id}](${report.pr.url}) \u2014 ${report.pr.merged ? "merged" : "open"}`);
|
|
36
|
+
lines.push("");
|
|
37
|
+
}
|
|
38
|
+
return lines.filter((l) => l !== void 0).join("\n");
|
|
39
|
+
}
|
|
40
|
+
function renderJson(report) {
|
|
41
|
+
return JSON.stringify(report, null, 2);
|
|
42
|
+
}
|
|
43
|
+
function writeReports(report, reportsDir) {
|
|
44
|
+
mkdirSync(reportsDir, { recursive: true });
|
|
45
|
+
const jsonPath = join(reportsDir, `${report.id}.json`);
|
|
46
|
+
const mdPath = join(reportsDir, `${report.id}.md`);
|
|
47
|
+
writeFileSync(jsonPath, renderJson(report), "utf8");
|
|
48
|
+
writeFileSync(mdPath, renderMarkdown(report), "utf8");
|
|
49
|
+
return { jsonPath, mdPath };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export {
|
|
53
|
+
renderMarkdown,
|
|
54
|
+
renderJson,
|
|
55
|
+
writeReports
|
|
56
|
+
};
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
#!/usr/bin/env node
|