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 +21 -0
- package/README.md +117 -36
- package/dist/api.d.ts +59 -1
- package/dist/api.js +43 -2
- package/dist/editor.d.ts +23 -0
- package/dist/editor.js +46 -0
- package/dist/entry-commands.d.ts +111 -0
- package/dist/entry-commands.js +157 -0
- package/dist/index.js +486 -110
- package/dist/init.d.ts +38 -0
- package/dist/init.js +86 -0
- package/dist/open.d.ts +33 -0
- package/dist/open.js +51 -0
- package/dist/project-config.d.ts +15 -5
- package/dist/project-config.js +43 -7
- package/dist/push.d.ts +71 -0
- package/dist/push.js +149 -0
- package/dist/resolve.d.ts +18 -0
- package/dist/resolve.js +16 -0
- package/package.json +4 -3
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { listEntries, getEntry, updateEntry, deleteEntry, setEntryPublished, ApiError, } from './api.js';
|
|
3
|
+
import { readProjectConfig } from './project-config.js';
|
|
4
|
+
import { resolveEntryRef } from './resolve.js';
|
|
5
|
+
import { editInEditor, saveRecoveryFile } from './editor.js';
|
|
6
|
+
import { defaultPushDeps } from './push.js';
|
|
7
|
+
export async function runSetPublished(opts, deps = defaultEntryCommandDeps) {
|
|
8
|
+
const projectConfig = deps.readProjectConfig();
|
|
9
|
+
const slug = opts.project ?? projectConfig?.project;
|
|
10
|
+
if (!slug) {
|
|
11
|
+
return {
|
|
12
|
+
kind: 'missing-fields',
|
|
13
|
+
message: 'No project specified. Use --project <slug> or create a .deploylog.yml with:\n project: my-app',
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
const resolved = await resolveEntryRef(opts.ref, slug, deps.api.listEntries);
|
|
17
|
+
if (resolved.kind === 'not-found') {
|
|
18
|
+
return { kind: 'not-found', message: resolved.message };
|
|
19
|
+
}
|
|
20
|
+
const entry = await deps.api.setEntryPublished(resolved.id, opts.publish);
|
|
21
|
+
return { kind: 'updated', entry, publish: opts.publish };
|
|
22
|
+
}
|
|
23
|
+
export async function runView(opts, deps = defaultEntryCommandDeps) {
|
|
24
|
+
const projectConfig = deps.readProjectConfig();
|
|
25
|
+
const slug = opts.project ?? projectConfig?.project;
|
|
26
|
+
if (!slug) {
|
|
27
|
+
return {
|
|
28
|
+
kind: 'missing-fields',
|
|
29
|
+
message: 'No project specified. Use --project <slug> or create a .deploylog.yml with:\n project: my-app',
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
const resolved = await resolveEntryRef(opts.ref, slug, deps.api.listEntries);
|
|
33
|
+
if (resolved.kind === 'not-found') {
|
|
34
|
+
return { kind: 'not-found', message: resolved.message };
|
|
35
|
+
}
|
|
36
|
+
const entry = await deps.api.getEntry(resolved.id);
|
|
37
|
+
return { kind: 'found', entry };
|
|
38
|
+
}
|
|
39
|
+
export async function runEdit(opts, deps = defaultEntryCommandDeps) {
|
|
40
|
+
const projectConfig = deps.readProjectConfig();
|
|
41
|
+
const slug = opts.project ?? projectConfig?.project;
|
|
42
|
+
if (!slug) {
|
|
43
|
+
return {
|
|
44
|
+
kind: 'missing-fields',
|
|
45
|
+
message: 'No project specified. Use --project <slug> or create a .deploylog.yml with:\n project: my-app',
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
const resolved = await resolveEntryRef(opts.ref, slug, deps.api.listEntries);
|
|
49
|
+
if (resolved.kind === 'not-found') {
|
|
50
|
+
return { kind: 'not-found', message: resolved.message };
|
|
51
|
+
}
|
|
52
|
+
const current = await deps.api.getEntry(resolved.id);
|
|
53
|
+
const hasFieldFlags = opts.title !== undefined ||
|
|
54
|
+
opts.type !== undefined ||
|
|
55
|
+
opts.version !== undefined ||
|
|
56
|
+
opts.body !== undefined ||
|
|
57
|
+
opts.bodyFile !== undefined;
|
|
58
|
+
let body = opts.body;
|
|
59
|
+
let bodyFromEditor = false;
|
|
60
|
+
if (opts.bodyFile !== undefined && body === undefined) {
|
|
61
|
+
try {
|
|
62
|
+
body = deps.readFile(opts.bodyFile);
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
return { kind: 'missing-fields', message: `Could not read body file '${opts.bodyFile}'.` };
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
if (!hasFieldFlags) {
|
|
69
|
+
if (!deps.isTTY) {
|
|
70
|
+
return {
|
|
71
|
+
kind: 'missing-fields',
|
|
72
|
+
message: 'Nothing to edit. Pass --title / --type / --version / --body / --body-file (the $EDITOR flow needs an interactive terminal).',
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
const edited = deps.editor(current.body_markdown);
|
|
76
|
+
switch (edited.kind) {
|
|
77
|
+
case 'unchanged':
|
|
78
|
+
return { kind: 'unchanged', message: 'No changes made; entry left as is.' };
|
|
79
|
+
case 'aborted':
|
|
80
|
+
return { kind: 'cancelled', message: edited.message };
|
|
81
|
+
case 'no-editor':
|
|
82
|
+
return { kind: 'missing-fields', message: edited.message };
|
|
83
|
+
case 'edited':
|
|
84
|
+
body = edited.body;
|
|
85
|
+
bodyFromEditor = true;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
const input = {};
|
|
89
|
+
if (opts.title !== undefined)
|
|
90
|
+
input.title = opts.title;
|
|
91
|
+
if (opts.type !== undefined)
|
|
92
|
+
input.entry_type = opts.type;
|
|
93
|
+
if (opts.version !== undefined)
|
|
94
|
+
input.version = opts.version;
|
|
95
|
+
if (body !== undefined)
|
|
96
|
+
input.body_markdown = body;
|
|
97
|
+
try {
|
|
98
|
+
const entry = await deps.api.updateEntry(resolved.id, input);
|
|
99
|
+
return { kind: 'updated', entry, previousSlug: current.slug };
|
|
100
|
+
}
|
|
101
|
+
catch (err) {
|
|
102
|
+
// A rejected editor-authored body must never be discarded — park it on
|
|
103
|
+
// disk and point at it.
|
|
104
|
+
if (bodyFromEditor && err instanceof ApiError && body !== undefined) {
|
|
105
|
+
const recoveryPath = deps.saveRecovery(body);
|
|
106
|
+
return {
|
|
107
|
+
kind: 'body-rejected',
|
|
108
|
+
message: `${err.message}\nYour edited body was saved to ${recoveryPath}`,
|
|
109
|
+
recoveryPath,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
throw err;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
export async function runDelete(opts, deps = defaultEntryCommandDeps) {
|
|
116
|
+
const projectConfig = deps.readProjectConfig();
|
|
117
|
+
const slug = opts.project ?? projectConfig?.project;
|
|
118
|
+
if (!slug) {
|
|
119
|
+
return {
|
|
120
|
+
kind: 'missing-fields',
|
|
121
|
+
message: 'No project specified. Use --project <slug> or create a .deploylog.yml with:\n project: my-app',
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
const resolved = await resolveEntryRef(opts.ref, slug, deps.api.listEntries);
|
|
125
|
+
if (resolved.kind === 'not-found') {
|
|
126
|
+
return { kind: 'not-found', message: resolved.message };
|
|
127
|
+
}
|
|
128
|
+
// Fetch first so the confirm prompt names what's about to be destroyed
|
|
129
|
+
// (and so a wrong-org/missing id 404s before any prompt).
|
|
130
|
+
const entry = await deps.api.getEntry(resolved.id);
|
|
131
|
+
if (!opts.yes) {
|
|
132
|
+
// Deleting is permanent — stricter than push: a non-interactive shell
|
|
133
|
+
// must opt in explicitly instead of auto-proceeding.
|
|
134
|
+
if (!deps.isTTY) {
|
|
135
|
+
return {
|
|
136
|
+
kind: 'confirm-required',
|
|
137
|
+
message: 'Refusing to delete without confirmation. Pass --yes in non-interactive mode.',
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
const ok = await deps.confirm(`Delete '${entry.title}' (${entry.slug})? This is permanent.`);
|
|
141
|
+
if (!ok) {
|
|
142
|
+
return { kind: 'cancelled', message: 'Cancelled. Entry not deleted.' };
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
await deps.api.deleteEntry(resolved.id);
|
|
146
|
+
return { kind: 'deleted', id: resolved.id, title: entry.title };
|
|
147
|
+
}
|
|
148
|
+
export const defaultEntryCommandDeps = {
|
|
149
|
+
api: { listEntries, getEntry, updateEntry, deleteEntry, setEntryPublished },
|
|
150
|
+
readProjectConfig: () => readProjectConfig(),
|
|
151
|
+
editor: editInEditor,
|
|
152
|
+
saveRecovery: saveRecoveryFile,
|
|
153
|
+
// '-' reads stdin so agents/CI can pipe a body without a temp file.
|
|
154
|
+
readFile: (path) => readFileSync(path === '-' ? 0 : path, 'utf8'),
|
|
155
|
+
confirm: (question) => defaultPushDeps.confirm(question),
|
|
156
|
+
isTTY: Boolean(process.stdin.isTTY),
|
|
157
|
+
};
|