deploylog 0.1.0 → 0.2.1

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 ADDED
@@ -0,0 +1,138 @@
1
+ <p align="center">
2
+ <img src="https://raw.githubusercontent.com/Idzuo32/deploylog-cli/main/.github/assets/logo.png" width="120" alt="DeployLog" />
3
+ </p>
4
+
5
+ <h1 align="center">deploylog</h1>
6
+
7
+ <p align="center">
8
+ Push changelog entries from the terminal. Generate release notes from git with one flag. Rewrite them with Claude Haiku with another.
9
+ </p>
10
+
11
+ <p align="center">
12
+ <a href="https://www.npmjs.com/package/deploylog"><img src="https://img.shields.io/npm/v/deploylog.svg" alt="npm"></a>
13
+ <a href="https://www.npmjs.com/package/deploylog"><img src="https://img.shields.io/npm/dm/deploylog.svg" alt="downloads"></a>
14
+ <img src="https://img.shields.io/node/v/deploylog.svg" alt="node">
15
+ <img src="https://img.shields.io/npm/l/deploylog.svg" alt="license">
16
+ </p>
17
+
18
+ ---
19
+
20
+ ## Install
21
+
22
+ ```bash
23
+ npm i -g deploylog
24
+ ```
25
+
26
+ Node 18+ required.
27
+
28
+ ## Authenticate
29
+
30
+ Create an API key in your dashboard at [deploylog.dev/dashboard/api-keys](https://deploylog.dev/dashboard/api-keys), then:
31
+
32
+ ```bash
33
+ deploylog login --key dk_xxx
34
+ ```
35
+
36
+ Credentials are stored with [`conf`](https://github.com/sindresorhus/conf) in your OS's standard config directory.
37
+
38
+ ## Quick start
39
+
40
+ ```bash
41
+ # List your projects
42
+ deploylog projects
43
+
44
+ # Publish an entry
45
+ deploylog push \
46
+ --project my-app \
47
+ --title "Dark mode" \
48
+ --body "Auto-detects system preference." \
49
+ --type feature \
50
+ --version 1.4.0 \
51
+ --publish
52
+ ```
53
+
54
+ ## Project config
55
+
56
+ Create a `.deploylog.yml` at your repo root so you don't have to pass `--project` every time:
57
+
58
+ ```yaml
59
+ project: my-app
60
+ ```
61
+
62
+ ## Commands
63
+
64
+ ### `deploylog login`
65
+
66
+ Authenticate with an API key.
67
+
68
+ ```
69
+ --key <key> API key (starts with dk_)
70
+ --api-url <url> API base URL (default: https://deploylog.dev)
71
+ ```
72
+
73
+ ### `deploylog logout`
74
+
75
+ Remove stored credentials.
76
+
77
+ ### `deploylog projects`
78
+
79
+ List projects in your organization.
80
+
81
+ ### `deploylog list`
82
+
83
+ List recent entries for a project.
84
+
85
+ ```
86
+ -p, --project <slug> Project slug (or set in .deploylog.yml)
87
+ ```
88
+
89
+ ### `deploylog push`
90
+
91
+ Create a new changelog entry.
92
+
93
+ ```
94
+ -t, --title <title> Entry title
95
+ -b, --body <markdown> Entry body (Markdown)
96
+ -p, --project <slug> Project slug (or set in .deploylog.yml)
97
+ --type <type> feature | fix | improvement | breaking | announcement
98
+ --version <version> Semver (e.g. 1.2.3)
99
+ --publish Publish immediately
100
+ --draft Save as draft (default)
101
+ --from-git Derive title/body from commits since the last tag
102
+ --ai-summarize Rewrite the entry with Claude Haiku
103
+ -y, --yes Skip interactive confirmation for AI-generated content
104
+ ```
105
+
106
+ ## Recipes
107
+
108
+ **Draft from recent commits:**
109
+
110
+ ```bash
111
+ deploylog push --from-git
112
+ ```
113
+
114
+ Collects commits since the last git tag, formats them as a Markdown list, and opens the entry as a draft.
115
+
116
+ **AI-polished release notes:**
117
+
118
+ ```bash
119
+ deploylog push --from-git --ai-summarize --version 1.4.0 --publish
120
+ ```
121
+
122
+ Uses Claude Haiku to rewrite your raw commits into user-friendly release notes. Free plan includes 5 AI summaries per month; paid plans are unlimited.
123
+
124
+ **CI / GitHub Actions:**
125
+
126
+ For CI workflows, prefer the official Action:
127
+
128
+ - [`deploylogdev/action`](https://github.com/marketplace/actions/deploylog) on the GitHub Marketplace
129
+
130
+ ## Related
131
+
132
+ - **Dashboard** — [deploylog.dev](https://deploylog.dev)
133
+ - **Widget** — embeddable changelog widget at `cdn.deploylog.dev`
134
+ - **GitHub Action** — [`deploylogdev/action@v1`](https://github.com/marketplace/actions/deploylog)
135
+
136
+ ## License
137
+
138
+ MIT © DeployLog
package/dist/api.d.ts CHANGED
@@ -30,3 +30,25 @@ export interface CreateEntryInput {
30
30
  publish?: boolean;
31
31
  }
32
32
  export declare function createEntry(projectSlug: string, input: CreateEntryInput): Promise<Entry>;
33
+ export interface SummarizeInput {
34
+ project_slug?: string;
35
+ commits?: string[];
36
+ release_notes?: string;
37
+ version?: string;
38
+ }
39
+ export type EntryType = 'feature' | 'fix' | 'improvement' | 'breaking' | 'announcement';
40
+ export interface AiSummary {
41
+ title: string;
42
+ entry_type: EntryType;
43
+ body_markdown: string;
44
+ }
45
+ export interface SummarizeResponse {
46
+ summary: AiSummary;
47
+ model: string;
48
+ usage: {
49
+ used: number;
50
+ limit: number | null;
51
+ month_key: string;
52
+ };
53
+ }
54
+ export declare function summarize(input: SummarizeInput): Promise<SummarizeResponse>;
package/dist/api.js CHANGED
@@ -41,3 +41,9 @@ export async function createEntry(projectSlug, input) {
41
41
  body: JSON.stringify(input),
42
42
  });
43
43
  }
44
+ export async function summarize(input) {
45
+ return request('/ai-summarize', {
46
+ method: 'POST',
47
+ body: JSON.stringify(input),
48
+ });
49
+ }
package/dist/git.d.ts ADDED
@@ -0,0 +1,38 @@
1
+ export declare class GitError extends Error {
2
+ constructor(message: string);
3
+ }
4
+ /**
5
+ * Thin shell-out wrapper. Exported for test injection.
6
+ * Returns stdout with trailing newline trimmed, or null on non-zero exit.
7
+ */
8
+ export declare function runGit(args: string[], cwd?: string): string | null;
9
+ export interface GitRunner {
10
+ (args: string[]): string | null;
11
+ }
12
+ export declare function isGitRepo(run?: GitRunner): boolean;
13
+ /**
14
+ * Most recent annotated or lightweight tag reachable from HEAD, or null.
15
+ */
16
+ export declare function getLastTag(run?: GitRunner): string | null;
17
+ /**
18
+ * If HEAD is on a tag that looks like semver (v1.2.3 or 1.2.3), return the
19
+ * bare semver string (without the 'v'). Otherwise null.
20
+ */
21
+ export declare function getHeadVersion(run?: GitRunner): string | null;
22
+ export interface CommitSummary {
23
+ hash: string;
24
+ subject: string;
25
+ }
26
+ /**
27
+ * Commit subjects (and short hashes) between `ref` and HEAD, oldest-first.
28
+ * If `ref` is null, returns the most recent `limit` commits on HEAD.
29
+ */
30
+ export declare function getCommitsSince(ref: string | null, limit?: number, run?: GitRunner): CommitSummary[];
31
+ /**
32
+ * Derive a sensible default title from the latest tag (or fallback).
33
+ */
34
+ export declare function defaultTitleFromGit(version: string | null, lastTag: string | null): string;
35
+ /**
36
+ * Format a commit list as a markdown bullet body.
37
+ */
38
+ export declare function formatCommitsAsMarkdown(commits: CommitSummary[]): string;
package/dist/git.js ADDED
@@ -0,0 +1,92 @@
1
+ import { execFileSync } from 'node:child_process';
2
+ export class GitError extends Error {
3
+ constructor(message) {
4
+ super(message);
5
+ this.name = 'GitError';
6
+ }
7
+ }
8
+ /**
9
+ * Thin shell-out wrapper. Exported for test injection.
10
+ * Returns stdout with trailing newline trimmed, or null on non-zero exit.
11
+ */
12
+ export function runGit(args, cwd = process.cwd()) {
13
+ try {
14
+ return execFileSync('git', args, {
15
+ cwd,
16
+ encoding: 'utf8',
17
+ stdio: ['ignore', 'pipe', 'pipe'],
18
+ }).replace(/\n$/, '');
19
+ }
20
+ catch {
21
+ return null;
22
+ }
23
+ }
24
+ export function isGitRepo(run = runGit) {
25
+ return run(['rev-parse', '--is-inside-work-tree']) === 'true';
26
+ }
27
+ /**
28
+ * Most recent annotated or lightweight tag reachable from HEAD, or null.
29
+ */
30
+ export function getLastTag(run = runGit) {
31
+ const out = run(['describe', '--tags', '--abbrev=0']);
32
+ return out && out.length > 0 ? out : null;
33
+ }
34
+ /**
35
+ * If HEAD is on a tag that looks like semver (v1.2.3 or 1.2.3), return the
36
+ * bare semver string (without the 'v'). Otherwise null.
37
+ */
38
+ export function getHeadVersion(run = runGit) {
39
+ const exact = run(['tag', '--points-at', 'HEAD']);
40
+ if (!exact)
41
+ return null;
42
+ for (const line of exact.split('\n')) {
43
+ const m = line.match(/^v?(\d+\.\d+\.\d+)$/);
44
+ if (m?.[1])
45
+ return m[1];
46
+ }
47
+ return null;
48
+ }
49
+ /**
50
+ * Commit subjects (and short hashes) between `ref` and HEAD, oldest-first.
51
+ * If `ref` is null, returns the most recent `limit` commits on HEAD.
52
+ */
53
+ export function getCommitsSince(ref, limit = 200, run = runGit) {
54
+ const range = ref ? `${ref}..HEAD` : 'HEAD';
55
+ const args = ['log', range, `--pretty=format:%h\t%s`, '--no-merges', `-${limit}`, '--reverse'];
56
+ const out = run(args);
57
+ if (out === null) {
58
+ // `ref..HEAD` with an unknown ref (or an empty repo) returns null via our wrapper.
59
+ return [];
60
+ }
61
+ if (out.length === 0)
62
+ return [];
63
+ const commits = [];
64
+ for (const line of out.split('\n')) {
65
+ const tab = line.indexOf('\t');
66
+ if (tab < 0)
67
+ continue;
68
+ const hash = line.slice(0, tab);
69
+ const subject = line.slice(tab + 1).trim();
70
+ if (subject.length > 0)
71
+ commits.push({ hash, subject });
72
+ }
73
+ return commits;
74
+ }
75
+ /**
76
+ * Derive a sensible default title from the latest tag (or fallback).
77
+ */
78
+ export function defaultTitleFromGit(version, lastTag) {
79
+ if (version)
80
+ return `Release v${version}`;
81
+ if (lastTag)
82
+ return `Changes since ${lastTag}`;
83
+ return 'Recent changes';
84
+ }
85
+ /**
86
+ * Format a commit list as a markdown bullet body.
87
+ */
88
+ export function formatCommitsAsMarkdown(commits) {
89
+ if (commits.length === 0)
90
+ return '_No new commits since last tag._';
91
+ return commits.map((c) => `- ${c.subject}`).join('\n');
92
+ }
package/dist/index.js CHANGED
@@ -2,13 +2,14 @@
2
2
  import { Command } from 'commander';
3
3
  import chalk from 'chalk';
4
4
  import { setApiKey, setApiUrl, getConfigPath, clearConfig } from './config.js';
5
- import { listProjects, listEntries, createEntry, ApiError } from './api.js';
5
+ import { listProjects, listEntries, createEntry, summarize, ApiError, } from './api.js';
6
6
  import { readProjectConfig } from './project-config.js';
7
+ import { isGitRepo, getLastTag, getHeadVersion, getCommitsSince, defaultTitleFromGit, formatCommitsAsMarkdown, } from './git.js';
7
8
  const program = new Command();
8
9
  program
9
10
  .name('deploylog')
10
11
  .description('Push changelog entries from the terminal')
11
- .version('0.1.0');
12
+ .version('0.2.0');
12
13
  // ─── login ──────────────────────────────────────────────────────────────────
13
14
  program
14
15
  .command('login')
@@ -108,22 +109,82 @@ program
108
109
  program
109
110
  .command('push')
110
111
  .description('Create a new changelog entry')
111
- .requiredOption('-t, --title <title>', 'Entry title')
112
- .requiredOption('-b, --body <markdown>', 'Entry body (Markdown)')
112
+ .option('-t, --title <title>', 'Entry title (required unless --from-git or --ai-summarize)')
113
+ .option('-b, --body <markdown>', 'Entry body (Markdown)')
113
114
  .option('-p, --project <slug>', 'Project slug (or set in .deploylog.yml)')
114
115
  .option('--type <type>', 'Entry type: feature, fix, improvement, breaking, announcement')
115
116
  .option('--version <version>', 'Semver version (e.g. 1.2.3)')
116
117
  .option('--publish', 'Publish immediately (default: draft)')
117
118
  .option('--draft', 'Save as draft (default)')
119
+ .option('--from-git', 'Derive title/body from commits since the last tag')
120
+ .option('--ai-summarize', 'Rewrite the entry with Claude Haiku (user-friendly release notes)')
121
+ .option('-y, --yes', 'Skip interactive confirmation for AI-generated content')
118
122
  .action(async (opts) => {
119
123
  try {
120
124
  const slug = resolveProject(opts.project);
121
125
  const projectConfig = readProjectConfig();
126
+ // Gather source material (commits + version) if --from-git.
127
+ let commits = [];
128
+ let gitVersion = null;
129
+ let gitTitle = null;
130
+ let gitBody = null;
131
+ if (opts.fromGit) {
132
+ if (!isGitRepo()) {
133
+ console.error(chalk.red('Not in a git repository. Remove --from-git or cd to a repo.'));
134
+ process.exit(1);
135
+ }
136
+ const lastTag = getLastTag();
137
+ commits = getCommitsSince(lastTag);
138
+ gitVersion = getHeadVersion();
139
+ gitTitle = defaultTitleFromGit(gitVersion, lastTag);
140
+ gitBody = formatCommitsAsMarkdown(commits);
141
+ if (commits.length === 0 && !opts.body && !opts.aiSummarize) {
142
+ console.error(chalk.yellow(lastTag
143
+ ? `No commits since tag ${lastTag}. Nothing to summarize.`
144
+ : 'No commits found on HEAD.'));
145
+ process.exit(1);
146
+ }
147
+ }
148
+ // Build the entry (with optional AI rewrite).
149
+ let title = opts.title ?? gitTitle ?? '';
150
+ let body = opts.body ?? gitBody ?? '';
151
+ let entryType = opts.type ?? projectConfig?.default_type ?? null;
152
+ const version = opts.version ?? gitVersion ?? undefined;
153
+ if (opts.aiSummarize) {
154
+ const hasSource = commits.length > 0 || (opts.body && opts.body.trim().length > 0);
155
+ if (!hasSource) {
156
+ console.error(chalk.red('--ai-summarize needs source material. Pass --from-git or provide --body as raw notes.'));
157
+ process.exit(1);
158
+ }
159
+ process.stdout.write(chalk.dim('Summarizing with Claude Haiku... '));
160
+ const res = await summarize({
161
+ project_slug: slug,
162
+ commits: commits.map((c) => c.subject),
163
+ release_notes: opts.body,
164
+ version,
165
+ });
166
+ process.stdout.write(chalk.green('done\n'));
167
+ title = opts.title ?? res.summary.title;
168
+ body = res.summary.body_markdown;
169
+ entryType = opts.type ?? res.summary.entry_type;
170
+ printAiPreview(res.summary, res.usage);
171
+ if (!opts.yes && process.stdin.isTTY) {
172
+ const ok = await confirm('Publish this entry?');
173
+ if (!ok) {
174
+ console.log(chalk.yellow('Cancelled. No entry was created.'));
175
+ return;
176
+ }
177
+ }
178
+ }
179
+ if (!title || !body) {
180
+ console.error(chalk.red('Entry requires --title and --body (or --from-git / --ai-summarize to derive them).'));
181
+ process.exit(1);
182
+ }
122
183
  const entry = await createEntry(slug, {
123
- title: opts.title,
124
- body_markdown: opts.body,
125
- entry_type: opts.type ?? projectConfig?.default_type ?? null,
126
- version: opts.version,
184
+ title,
185
+ body_markdown: body,
186
+ entry_type: entryType,
187
+ version,
127
188
  publish: opts.publish && !opts.draft,
128
189
  });
129
190
  const status = entry.published
@@ -140,6 +201,28 @@ program
140
201
  }
141
202
  });
142
203
  // ─── helpers ────────────────────────────────────────────────────────────────
204
+ function printAiPreview(summary, usage) {
205
+ console.log();
206
+ console.log(chalk.bold('AI-generated entry:'));
207
+ console.log(` ${chalk.dim('Title:')} ${summary.title}`);
208
+ console.log(` ${chalk.dim('Type:')} ${summary.entry_type}`);
209
+ console.log(chalk.dim(' Body:'));
210
+ for (const line of summary.body_markdown.split('\n')) {
211
+ console.log(` ${line}`);
212
+ }
213
+ const limitLabel = usage.limit === null ? '∞' : String(usage.limit);
214
+ console.log(chalk.dim(` Usage: ${usage.used}/${limitLabel} this month (${usage.month_key})`));
215
+ console.log();
216
+ }
217
+ async function confirm(question) {
218
+ const { createInterface } = await import('node:readline');
219
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
220
+ const answer = await new Promise((resolve) => {
221
+ rl.question(`${question} [y/N] `, resolve);
222
+ });
223
+ rl.close();
224
+ return /^y(es)?$/i.test(answer.trim());
225
+ }
143
226
  function resolveProject(cliArg) {
144
227
  if (cliArg)
145
228
  return cliArg;
package/package.json CHANGED
@@ -1,19 +1,29 @@
1
1
  {
2
2
  "name": "deploylog",
3
- "version": "0.1.0",
3
+ "version": "0.2.1",
4
4
  "description": "Push changelog entries from the terminal",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "deploylog": "./dist/index.js"
8
8
  },
9
- "files": ["dist"],
9
+ "files": [
10
+ "dist"
11
+ ],
10
12
  "scripts": {
11
13
  "build": "tsc",
12
14
  "dev": "tsc --watch",
13
15
  "prepublishOnly": "tsc",
14
- "start": "node dist/index.js"
16
+ "start": "node dist/index.js",
17
+ "test": "vitest run",
18
+ "test:watch": "vitest"
15
19
  },
16
- "keywords": ["changelog", "deploylog", "cli", "release-notes", "devtools"],
20
+ "keywords": [
21
+ "changelog",
22
+ "deploylog",
23
+ "cli",
24
+ "release-notes",
25
+ "devtools"
26
+ ],
17
27
  "license": "MIT",
18
28
  "repository": {
19
29
  "type": "git",
@@ -28,6 +38,7 @@
28
38
  },
29
39
  "devDependencies": {
30
40
  "@types/node": "^22.15.3",
31
- "typescript": "^5.8.3"
41
+ "typescript": "^5.8.3",
42
+ "vitest": "^4.1.4"
32
43
  }
33
44
  }