deploylog 0.2.2 → 0.4.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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Monolithiq LLC
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 CHANGED
@@ -1,5 +1,5 @@
1
1
  <p align="center">
2
- <img src="https://raw.githubusercontent.com/Idzuo32/deploylog-cli/main/.github/assets/logo.png" width="120" alt="DeployLog" />
2
+ <img src="https://raw.githubusercontent.com/marko-builds/deploylog-cli/main/.github/assets/logo.png" width="120" alt="DeployLog" />
3
3
  </p>
4
4
 
5
5
  <h1 align="center">deploylog</h1>
@@ -23,7 +23,7 @@
23
23
  npm i -g deploylog
24
24
  ```
25
25
 
26
- Node 18+ required.
26
+ Node 18+ required. Installs two equivalent commands: `deploylog` and the short alias `dpl`.
27
27
 
28
28
  ## Authenticate
29
29
 
@@ -38,54 +38,94 @@ Credentials are stored with [`conf`](https://github.com/sindresorhus/conf) in yo
38
38
  ## Quick start
39
39
 
40
40
  ```bash
41
- # List your projects
42
- deploylog projects
41
+ # Wire this repo to a project (writes .deploylog.yml)
42
+ deploylog init
43
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
44
+ # Draft an entry from your commits, rewritten by AI
45
+ deploylog push --from-git --ai-summarize
46
+
47
+ # Review it, then ship it
48
+ deploylog list --drafts
49
+ deploylog view dark-mode
50
+ deploylog publish dark-mode
52
51
  ```
53
52
 
53
+ The full lifecycle lives in the terminal: create, list, view, edit, publish, unpublish, delete. No dashboard detour.
54
+
54
55
  ## Project config
55
56
 
56
- Create a `.deploylog.yml` at your repo root so you don't have to pass `--project` every time:
57
+ `deploylog init` writes a `.deploylog.yml` at your repo root so you don't have to pass `--project` every time:
57
58
 
58
59
  ```yaml
59
60
  project: my-app
61
+ default_type: feature # optional
62
+ ```
63
+
64
+ ## Referencing entries
65
+
66
+ Entry commands accept a **slug or an id** (`deploylog publish dark-mode`, `deploylog publish 3f2b8a1c-...`). Slugs are matched against the 50 most recent entries; older entries need the id (`deploylog list` shows both). Note that editing a draft's title can change its slug, so scripts should prefer ids.
67
+
68
+ ## Machine-readable output
69
+
70
+ Every data command takes `--json`: raw JSON on stdout, errors as `{"error":{"code","message"}}` on stderr, and no interactive prompts, ever. Built for CI and AI agents.
71
+
72
+ ```bash
73
+ deploylog list --drafts --json | jq -r '.[0].id'
60
74
  ```
61
75
 
62
76
  ## Commands
63
77
 
64
- ### `deploylog login`
78
+ ### `deploylog login` / `deploylog logout`
65
79
 
66
- Authenticate with an API key.
80
+ Authenticate with an API key (create one at [deploylog.dev/dashboard/api-keys](https://deploylog.dev/dashboard/api-keys)).
67
81
 
68
82
  ```
69
83
  --key <key> API key (starts with dk_)
70
84
  --api-url <url> API base URL (default: https://deploylog.dev)
71
85
  ```
72
86
 
73
- ### `deploylog logout`
87
+ ### `deploylog init`
74
88
 
75
- Remove stored credentials.
89
+ Scaffold `.deploylog.yml` in the current directory. Picks the project interactively, or takes `--project`.
76
90
 
77
- ### `deploylog projects`
91
+ ```
92
+ -p, --project <slug> Project slug
93
+ -T, --type <type> Default entry type for pushes from this repo
94
+ --force Overwrite an existing .deploylog.yml
95
+ ```
96
+
97
+ ### `deploylog projects` (alias: `proj`)
78
98
 
79
99
  List projects in your organization.
80
100
 
81
- ### `deploylog list`
101
+ ### `deploylog projects create <name>`
102
+
103
+ Create a project. The slug is generated from the name.
104
+
105
+ ```
106
+ --url <url> Project website URL
107
+ ```
108
+
109
+ ### `deploylog whoami`
110
+
111
+ Show the authenticated org, plan, API key (name, prefix, permissions), and AI usage this month.
112
+
113
+ ### `deploylog list` (alias: `ls`)
82
114
 
83
- List recent entries for a project.
115
+ List recent entries for a project. Prints each entry's slug and id.
84
116
 
85
117
  ```
86
118
  -p, --project <slug> Project slug (or set in .deploylog.yml)
119
+ --drafts Only drafts
120
+ --published Only published entries
121
+ -T, --type <type> Filter by entry type
122
+ -n, --limit <n> Max entries (1-50)
87
123
  ```
88
124
 
125
+ ### `deploylog view <entry>` (alias: `show`)
126
+
127
+ Show a full entry, including its Markdown body.
128
+
89
129
  ### `deploylog push`
90
130
 
91
131
  Create a new changelog entry.
@@ -94,44 +134,85 @@ Create a new changelog entry.
94
134
  -t, --title <title> Entry title
95
135
  -b, --body <markdown> Entry body (Markdown)
96
136
  -p, --project <slug> Project slug (or set in .deploylog.yml)
97
- --type <type> feature | fix | improvement | breaking | announcement
137
+ -T, --type <type> feature | fix | improvement | breaking | announcement
98
138
  --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
139
+ -P, --publish Publish immediately
140
+ -D, --draft Save as draft (default)
141
+ -g, --from-git Derive title/body from commits since the last tag (alias: --git)
142
+ -a, --ai-summarize Rewrite the entry with Claude Haiku (alias: --ai)
103
143
  -y, --yes Skip interactive confirmation for AI-generated content
104
144
  ```
105
145
 
146
+ ### `deploylog edit <entry>`
147
+
148
+ Update an entry. With no field flags on an interactive terminal, your `$EDITOR` opens prefilled with the current body. If the server rejects an edited body, it is saved to a recovery file, never lost.
149
+
150
+ ```
151
+ -t, --title <title> New title (may change a draft's slug; the CLI tells you)
152
+ -T, --type <type> Entry type
153
+ --version <version> Semver version (pass "" to clear)
154
+ -b, --body <markdown> New body
155
+ --body-file <path> Read the new body from a file (- for stdin)
156
+ ```
157
+
158
+ ### `deploylog publish <entry>` (alias: `pub`) / `deploylog unpublish <entry>` (alias: `unpub`)
159
+
160
+ Publish a draft, or revert a published entry to draft. Publishing is idempotent: re-running is a no-op, and the email digest (Pro) is sent at most once per entry, ever. Unpublishing resets the publish date; republishing gets a new one.
161
+
162
+ ### `deploylog delete <entry>` (alias: `rm`)
163
+
164
+ Delete an entry permanently. Prompts for confirmation on a terminal; requires `--yes` in CI or `--json` mode.
165
+
166
+ ### `deploylog import github <repo>`
167
+
168
+ Backfill your existing GitHub releases as draft entries. Takes `owner/repo` or a github.com URL. Skips versions you already imported.
169
+
170
+ ```
171
+ -p, --project <slug> Project slug (or set in .deploylog.yml)
172
+ --token <token> GitHub PAT for private repos / rate limits (or DEPLOYLOG_GITHUB_TOKEN); never stored
173
+ ```
174
+
175
+ ### `deploylog open [entry]` (alias: `o`)
176
+
177
+ Open the project's public changelog (or one entry's page) in your browser. Prints the URL when headless.
178
+
106
179
  ## Recipes
107
180
 
108
- **Draft from recent commits:**
181
+ **Onboard a repo in one minute:**
109
182
 
110
183
  ```bash
111
- deploylog push --from-git
184
+ deploylog login --key dk_xxx
185
+ deploylog projects create "My App" --url https://myapp.dev
186
+ deploylog init
187
+ deploylog import github me/my-app # backfill old releases as drafts
188
+ deploylog list --drafts # review
189
+ deploylog publish v1-4-0
112
190
  ```
113
191
 
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:**
192
+ **Draft from recent commits, ship after review:**
117
193
 
118
194
  ```bash
119
- deploylog push --from-git --ai-summarize --version 1.4.0 --publish
195
+ deploylog push --from-git --ai-summarize
196
+ deploylog view <slug> # read what the AI wrote
197
+ deploylog edit <slug> # tweak in $EDITOR
198
+ deploylog publish <slug>
120
199
  ```
121
200
 
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.
201
+ **CI: publish on release, no prompts:**
123
202
 
124
- **CI / GitHub Actions:**
203
+ ```bash
204
+ deploylog push --from-git --ai-summarize --yes --publish --json
205
+ ```
125
206
 
126
- For CI workflows, prefer the official Action:
207
+ For GitHub Actions specifically, prefer the official Action: [`deploylogdev/action`](https://github.com/marketplace/actions/publish-to-deploylog).
127
208
 
128
- - [`deploylogdev/action`](https://github.com/marketplace/actions/deploylog) on the GitHub Marketplace
209
+ **Agent-driven usage:** every command's `--json` output is stable and prompt-free; destructive operations refuse without an explicit `--yes`.
129
210
 
130
211
  ## Related
131
212
 
132
213
  - **Dashboard** — [deploylog.dev](https://deploylog.dev)
133
214
  - **Widget** — embeddable changelog widget at `cdn.deploylog.dev`
134
- - **GitHub Action** — [`deploylogdev/action@v1`](https://github.com/marketplace/actions/deploylog)
215
+ - **GitHub Action** — [`deploylogdev/action@v1`](https://github.com/marketplace/actions/publish-to-deploylog)
135
216
 
136
217
  ## License
137
218
 
package/dist/api.d.ts CHANGED
@@ -21,7 +21,55 @@ export interface Entry {
21
21
  created_at: string;
22
22
  }
23
23
  export declare function listProjects(): Promise<Project[]>;
24
- export declare function listEntries(projectSlug: string): Promise<Entry[]>;
24
+ export declare function createProject(name: string, websiteUrl?: string): Promise<Project>;
25
+ export interface GithubImportResult {
26
+ imported: number;
27
+ skipped: number;
28
+ }
29
+ export declare function importGithub(projectSlug: string, repo: string, token?: string): Promise<GithubImportResult>;
30
+ export interface WhoAmI {
31
+ organization: {
32
+ name: string;
33
+ slug: string;
34
+ plan: string | null;
35
+ };
36
+ api_key: {
37
+ name: string;
38
+ prefix: string;
39
+ permissions: string[];
40
+ last_used_at: string | null;
41
+ created_at: string;
42
+ };
43
+ ai_usage: {
44
+ used: number;
45
+ limit: number | null;
46
+ month_key: string;
47
+ };
48
+ }
49
+ export declare function whoami(): Promise<WhoAmI>;
50
+ export interface ListEntriesFilters {
51
+ status?: 'draft' | 'published';
52
+ type?: string;
53
+ limit?: number;
54
+ }
55
+ export declare function listEntries(projectSlug: string, filters?: ListEntriesFilters): Promise<Entry[]>;
56
+ /** Full entry including the markdown body — the single-entry GET. */
57
+ export interface EntryDetail extends Entry {
58
+ body_markdown: string;
59
+ updated_at: string | null;
60
+ }
61
+ export declare function getEntry(entryId: string): Promise<EntryDetail>;
62
+ export interface UpdateEntryInput {
63
+ title?: string;
64
+ body_markdown?: string;
65
+ entry_type?: string | null;
66
+ version?: string;
67
+ slug?: string;
68
+ }
69
+ export declare function updateEntry(entryId: string, input: UpdateEntryInput): Promise<EntryDetail>;
70
+ export declare function deleteEntry(entryId: string): Promise<{
71
+ id: string;
72
+ }>;
25
73
  export interface CreateEntryInput {
26
74
  title: string;
27
75
  body_markdown: string;
@@ -30,6 +78,16 @@ export interface CreateEntryInput {
30
78
  publish?: boolean;
31
79
  }
32
80
  export declare function createEntry(projectSlug: string, input: CreateEntryInput): Promise<Entry>;
81
+ /** Entry row returned by the publish route, plus whether the state actually changed. */
82
+ export interface PublishStateEntry {
83
+ id: string;
84
+ title: string;
85
+ slug: string;
86
+ published: boolean;
87
+ published_at: string | null;
88
+ changed: boolean;
89
+ }
90
+ export declare function setEntryPublished(entryId: string, published: boolean): Promise<PublishStateEntry>;
33
91
  export interface SummarizeInput {
34
92
  project_slug?: string;
35
93
  commits?: string[];
package/dist/api.js CHANGED
@@ -46,8 +46,43 @@ async function request(path, options = {}) {
46
46
  export async function listProjects() {
47
47
  return request('/projects');
48
48
  }
49
- export async function listEntries(projectSlug) {
50
- return request(`/projects/${projectSlug}/entries`);
49
+ export async function createProject(name, websiteUrl) {
50
+ return request('/projects', {
51
+ method: 'POST',
52
+ body: JSON.stringify({ name, website_url: websiteUrl ?? '' }),
53
+ });
54
+ }
55
+ export async function importGithub(projectSlug, repo, token) {
56
+ return request(`/projects/${projectSlug}/import/github`, {
57
+ method: 'POST',
58
+ body: JSON.stringify(token ? { repo, token } : { repo }),
59
+ });
60
+ }
61
+ export async function whoami() {
62
+ return request('/whoami');
63
+ }
64
+ export async function listEntries(projectSlug, filters = {}) {
65
+ const params = new URLSearchParams();
66
+ if (filters.status)
67
+ params.set('status', filters.status);
68
+ if (filters.type)
69
+ params.set('type', filters.type);
70
+ if (filters.limit !== undefined)
71
+ params.set('limit', String(filters.limit));
72
+ const qs = params.toString();
73
+ return request(`/projects/${projectSlug}/entries${qs ? `?${qs}` : ''}`);
74
+ }
75
+ export async function getEntry(entryId) {
76
+ return request(`/entries/${entryId}`);
77
+ }
78
+ export async function updateEntry(entryId, input) {
79
+ return request(`/entries/${entryId}`, {
80
+ method: 'PATCH',
81
+ body: JSON.stringify(input),
82
+ });
83
+ }
84
+ export async function deleteEntry(entryId) {
85
+ return request(`/entries/${entryId}`, { method: 'DELETE' });
51
86
  }
52
87
  export async function createEntry(projectSlug, input) {
53
88
  return request(`/projects/${projectSlug}/entries`, {
@@ -55,6 +90,12 @@ export async function createEntry(projectSlug, input) {
55
90
  body: JSON.stringify(input),
56
91
  });
57
92
  }
93
+ export async function setEntryPublished(entryId, published) {
94
+ return request(`/entries/${entryId}/publish`, {
95
+ method: 'POST',
96
+ body: JSON.stringify({ published }),
97
+ });
98
+ }
58
99
  export async function summarize(input) {
59
100
  return request('/ai-summarize', {
60
101
  method: 'POST',
@@ -0,0 +1,23 @@
1
+ export type EditorResult = {
2
+ kind: 'edited';
3
+ body: string;
4
+ } | {
5
+ kind: 'unchanged';
6
+ } | {
7
+ kind: 'aborted';
8
+ message: string;
9
+ } | {
10
+ kind: 'no-editor';
11
+ message: string;
12
+ };
13
+ /**
14
+ * Round-trip a markdown body through $EDITOR (fallback: $VISUAL, then vi).
15
+ * Trailing newlines are normalized before the no-op check — editors append
16
+ * one on save, and that alone must not count as a change.
17
+ */
18
+ export declare function editInEditor(initial: string): EditorResult;
19
+ /**
20
+ * Persist a body the server rejected so the user's writing is never lost —
21
+ * printed in the error message as a recoverable path.
22
+ */
23
+ export declare function saveRecoveryFile(body: string): string;
package/dist/editor.js ADDED
@@ -0,0 +1,46 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import { mkdtempSync, readFileSync, writeFileSync, rmSync, mkdirSync } from 'node:fs';
3
+ import { tmpdir } from 'node:os';
4
+ import { join } from 'node:path';
5
+ /**
6
+ * Round-trip a markdown body through $EDITOR (fallback: $VISUAL, then vi).
7
+ * Trailing newlines are normalized before the no-op check — editors append
8
+ * one on save, and that alone must not count as a change.
9
+ */
10
+ export function editInEditor(initial) {
11
+ const editor = process.env.EDITOR ?? process.env.VISUAL ?? 'vi';
12
+ const dir = mkdtempSync(join(tmpdir(), 'deploylog-'));
13
+ const file = join(dir, 'entry.md');
14
+ writeFileSync(file, initial, 'utf8');
15
+ try {
16
+ const res = spawnSync(editor, [file], { stdio: 'inherit' });
17
+ if (res.error) {
18
+ return {
19
+ kind: 'no-editor',
20
+ message: `Could not launch editor '${editor}'. Set $EDITOR, or pass --body / --body-file instead.`,
21
+ };
22
+ }
23
+ if (res.status !== 0) {
24
+ return { kind: 'aborted', message: `Editor exited with status ${res.status}; entry not changed.` };
25
+ }
26
+ const edited = readFileSync(file, 'utf8');
27
+ if (edited.replace(/\n+$/, '') === initial.replace(/\n+$/, '')) {
28
+ return { kind: 'unchanged' };
29
+ }
30
+ return { kind: 'edited', body: edited };
31
+ }
32
+ finally {
33
+ rmSync(dir, { recursive: true, force: true });
34
+ }
35
+ }
36
+ /**
37
+ * Persist a body the server rejected so the user's writing is never lost —
38
+ * printed in the error message as a recoverable path.
39
+ */
40
+ export function saveRecoveryFile(body) {
41
+ const dir = join(tmpdir(), 'deploylog-recovery');
42
+ mkdirSync(dir, { recursive: true });
43
+ const file = join(dir, `entry-${process.pid}-${Date.now()}.md`);
44
+ writeFileSync(file, body, 'utf8');
45
+ return file;
46
+ }
@@ -0,0 +1,111 @@
1
+ import { type Entry, type EntryDetail, type PublishStateEntry, type UpdateEntryInput } from './api.js';
2
+ import { type ProjectConfig } from './project-config.js';
3
+ import { type EditorResult } from './editor.js';
4
+ /**
5
+ * Shared deps for the entry-lifecycle commands (publish/unpublish, and the
6
+ * view/edit/delete siblings that follow) — same DI shape as PushDeps.
7
+ */
8
+ export interface EntryCommandDeps {
9
+ api: {
10
+ listEntries(slug: string): Promise<Entry[]>;
11
+ getEntry(id: string): Promise<EntryDetail>;
12
+ updateEntry(id: string, input: UpdateEntryInput): Promise<EntryDetail>;
13
+ deleteEntry(id: string): Promise<{
14
+ id: string;
15
+ }>;
16
+ setEntryPublished(id: string, published: boolean): Promise<PublishStateEntry>;
17
+ };
18
+ readProjectConfig(): ProjectConfig | null;
19
+ editor(initial: string): EditorResult;
20
+ saveRecovery(body: string): string;
21
+ readFile(path: string): string;
22
+ confirm(question: string): Promise<boolean>;
23
+ isTTY: boolean;
24
+ }
25
+ export interface SetPublishedOptions {
26
+ ref: string;
27
+ project?: string;
28
+ publish: boolean;
29
+ }
30
+ export type SetPublishedResult = {
31
+ kind: 'updated';
32
+ entry: PublishStateEntry;
33
+ publish: boolean;
34
+ } | {
35
+ kind: 'not-found';
36
+ message: string;
37
+ } | {
38
+ kind: 'missing-fields';
39
+ message: string;
40
+ };
41
+ export declare function runSetPublished(opts: SetPublishedOptions, deps?: EntryCommandDeps): Promise<SetPublishedResult>;
42
+ export interface ViewOptions {
43
+ ref: string;
44
+ project?: string;
45
+ }
46
+ export type ViewResult = {
47
+ kind: 'found';
48
+ entry: EntryDetail;
49
+ } | {
50
+ kind: 'not-found';
51
+ message: string;
52
+ } | {
53
+ kind: 'missing-fields';
54
+ message: string;
55
+ };
56
+ export declare function runView(opts: ViewOptions, deps?: EntryCommandDeps): Promise<ViewResult>;
57
+ export interface EditOptions {
58
+ ref: string;
59
+ project?: string;
60
+ title?: string;
61
+ type?: string;
62
+ version?: string;
63
+ body?: string;
64
+ bodyFile?: string;
65
+ }
66
+ export type EditResult = {
67
+ kind: 'updated';
68
+ entry: EntryDetail;
69
+ previousSlug: string;
70
+ } | {
71
+ kind: 'not-found';
72
+ message: string;
73
+ } | {
74
+ kind: 'missing-fields';
75
+ message: string;
76
+ } | {
77
+ kind: 'cancelled';
78
+ message: string;
79
+ } | {
80
+ kind: 'unchanged';
81
+ message: string;
82
+ } | {
83
+ kind: 'body-rejected';
84
+ message: string;
85
+ recoveryPath: string;
86
+ };
87
+ export declare function runEdit(opts: EditOptions, deps?: EntryCommandDeps): Promise<EditResult>;
88
+ export interface DeleteOptions {
89
+ ref: string;
90
+ project?: string;
91
+ yes?: boolean;
92
+ }
93
+ export type DeleteResult = {
94
+ kind: 'deleted';
95
+ id: string;
96
+ title: string;
97
+ } | {
98
+ kind: 'cancelled';
99
+ message: string;
100
+ } | {
101
+ kind: 'confirm-required';
102
+ message: string;
103
+ } | {
104
+ kind: 'not-found';
105
+ message: string;
106
+ } | {
107
+ kind: 'missing-fields';
108
+ message: string;
109
+ };
110
+ export declare function runDelete(opts: DeleteOptions, deps?: EntryCommandDeps): Promise<DeleteResult>;
111
+ export declare const defaultEntryCommandDeps: EntryCommandDeps;