openwriter 0.40.0 → 0.40.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/dist/client/assets/{index-vmEPerKn.js → index-Dxbv2n2m.js} +1 -1
- package/dist/client/index.html +1 -1
- package/package.json +1 -1
- package/dist/plugins/authors-voice/dist/index.d.ts +0 -48
- package/dist/plugins/authors-voice/dist/index.js +0 -235
- package/dist/plugins/authors-voice/package.json +0 -24
- package/dist/plugins/authors-voice/skill/LICENSE +0 -21
- package/dist/plugins/authors-voice/skill/README.md +0 -126
- package/dist/plugins/authors-voice/skill/SKILL.md +0 -151
- package/dist/plugins/authors-voice/skill/catalog/ai-tells.md +0 -144
- package/dist/plugins/authors-voice/skill/catalog/anchor-prompt.md +0 -189
- package/dist/plugins/authors-voice/skill/catalog/author-hints.md +0 -119
- package/dist/plugins/authors-voice/skill/catalog/fingerprints.md +0 -175
- package/dist/plugins/authors-voice/skill/catalog/hurdle.md +0 -76
- package/dist/plugins/authors-voice/skill/catalog/post-write-audit.md +0 -105
- package/dist/plugins/authors-voice/skill/docs/analysis.md +0 -31
- package/dist/plugins/authors-voice/skill/docs/anchor-iteration.md +0 -176
- package/dist/plugins/authors-voice/skill/docs/api/import.md +0 -78
- package/dist/plugins/authors-voice/skill/docs/api/protocol.md +0 -140
- package/dist/plugins/authors-voice/skill/docs/api/setup.md +0 -37
- package/dist/plugins/authors-voice/skill/docs/api/tools.md +0 -102
- package/dist/plugins/authors-voice/skill/docs/api/troubleshooting.md +0 -7
- package/dist/plugins/authors-voice/skill/docs/apply-protocol-deep.md +0 -191
- package/dist/plugins/authors-voice/skill/docs/context-hygiene.md +0 -33
- package/dist/plugins/authors-voice/skill/docs/setup.md +0 -74
- package/dist/plugins/authors-voice/skill/docs/tiers.md +0 -13
- package/dist/plugins/authors-voice/skill/package.json +0 -35
- package/dist/plugins/authors-voice/skill/prompts/skeleton.md +0 -29
- package/dist/plugins/authors-voice/skill/voice/README.md +0 -51
- package/dist/plugins/authors-voice/skill/voice/corpus/.gitkeep +0 -0
- package/dist/plugins/github/dist/blog-tools.d.ts +0 -65
- package/dist/plugins/github/dist/blog-tools.js +0 -1189
- package/dist/plugins/github/dist/git-sync.d.ts +0 -46
- package/dist/plugins/github/dist/git-sync.js +0 -335
- package/dist/plugins/github/dist/helpers.d.ts +0 -127
- package/dist/plugins/github/dist/helpers.js +0 -67
- package/dist/plugins/github/dist/index.d.ts +0 -12
- package/dist/plugins/github/dist/index.js +0 -112
- package/dist/plugins/github/package.json +0 -24
- package/dist/plugins/image-gen/dist/index.d.ts +0 -35
- package/dist/plugins/image-gen/dist/index.js +0 -149
- package/dist/plugins/image-gen/package.json +0 -26
- package/dist/plugins/publish/dist/helpers.d.ts +0 -66
- package/dist/plugins/publish/dist/helpers.js +0 -199
- package/dist/plugins/publish/dist/index.d.ts +0 -3
- package/dist/plugins/publish/dist/index.js +0 -1156
- package/dist/plugins/publish/dist/newsletter-tools.d.ts +0 -2
- package/dist/plugins/publish/dist/newsletter-tools.js +0 -394
- package/dist/plugins/publish/package.json +0 -31
- package/dist/plugins/x-api/dist/index.d.ts +0 -27
- package/dist/plugins/x-api/dist/index.js +0 -368
- package/dist/plugins/x-api/dist/server-bridge.d.ts +0 -22
- package/dist/plugins/x-api/dist/server-bridge.js +0 -43
- package/dist/plugins/x-api/package.json +0 -27
- package/dist/server/blog-routes.js +0 -160
- package/dist/server/git-sync.js +0 -273
- package/dist/server/marks.js +0 -182
- package/dist/server/sync-routes.js +0 -75
- package/skill/docs/enrichment.md +0 -180
- package/skill/docs/footnotes.md +0 -178
- package/skill/docs/setup.md +0 -62
- package/skill/docs/welcome.md +0 -21
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Git sync module: all git/gh CLI interactions for GitHub docs backup.
|
|
3
|
-
* Lifted from packages/openwriter/server/git-sync.ts into the github plugin.
|
|
4
|
-
*
|
|
5
|
-
* Uses child_process.execFile with an argv array and NO shell (MCP-1): values
|
|
6
|
-
* are passed to git/gh as literal arguments, never interpreted by a shell.
|
|
7
|
-
* The GitHub PAT is supplied to authenticated pushes out-of-band via an
|
|
8
|
-
* inline credential helper reading from an env var (MCP-3) — it is never
|
|
9
|
-
* embedded in the remote URL, written to .git/config, or placed in argv.
|
|
10
|
-
* Server-internal modules (state, helpers, ws) accessed via getServerModules().
|
|
11
|
-
*/
|
|
12
|
-
export type SyncState = 'unconfigured' | 'synced' | 'pending' | 'syncing' | 'error';
|
|
13
|
-
export interface SyncStatus {
|
|
14
|
-
state: SyncState;
|
|
15
|
-
lastSyncTime?: string;
|
|
16
|
-
pendingFiles?: number;
|
|
17
|
-
error?: string;
|
|
18
|
-
}
|
|
19
|
-
export interface SyncCapabilities {
|
|
20
|
-
gitInstalled: boolean;
|
|
21
|
-
ghInstalled: boolean;
|
|
22
|
-
ghAuthenticated: boolean;
|
|
23
|
-
existingRepo: boolean;
|
|
24
|
-
remoteUrl?: string;
|
|
25
|
-
}
|
|
26
|
-
/**
|
|
27
|
-
* Strip any embedded credentials (PAT / user:pass) from a git remote URL so a
|
|
28
|
-
* credential-bearing URL is never returned to a client or logged. SSH scp-style
|
|
29
|
-
* remotes (git@github.com:owner/repo.git) carry no secret and are left as-is.
|
|
30
|
-
*/
|
|
31
|
-
export declare function sanitizeRemoteUrl(url: string): string;
|
|
32
|
-
export declare function isGitInstalled(): Promise<boolean>;
|
|
33
|
-
export declare function isGhInstalled(): Promise<boolean>;
|
|
34
|
-
export declare function isGhAuthenticated(): Promise<boolean>;
|
|
35
|
-
export declare function isGitRepo(): Promise<boolean>;
|
|
36
|
-
export interface PendingFile {
|
|
37
|
-
status: 'added' | 'modified' | 'deleted' | 'renamed';
|
|
38
|
-
file: string;
|
|
39
|
-
}
|
|
40
|
-
export declare function getPendingFiles(): Promise<PendingFile[]>;
|
|
41
|
-
export declare function getSyncStatus(): Promise<SyncStatus>;
|
|
42
|
-
export declare function getCapabilities(): Promise<SyncCapabilities>;
|
|
43
|
-
export declare function setupWithGh(repoName: string, isPrivate: boolean): Promise<void>;
|
|
44
|
-
export declare function setupWithPat(pat: string, repoName: string, isPrivate: boolean): Promise<void>;
|
|
45
|
-
export declare function connectExisting(remoteUrl: string, pat?: string): Promise<void>;
|
|
46
|
-
export declare function pushSync(onStatus: (status: SyncStatus) => void): Promise<SyncStatus>;
|
|
@@ -1,335 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Git sync module: all git/gh CLI interactions for GitHub docs backup.
|
|
3
|
-
* Lifted from packages/openwriter/server/git-sync.ts into the github plugin.
|
|
4
|
-
*
|
|
5
|
-
* Uses child_process.execFile with an argv array and NO shell (MCP-1): values
|
|
6
|
-
* are passed to git/gh as literal arguments, never interpreted by a shell.
|
|
7
|
-
* The GitHub PAT is supplied to authenticated pushes out-of-band via an
|
|
8
|
-
* inline credential helper reading from an env var (MCP-3) — it is never
|
|
9
|
-
* embedded in the remote URL, written to .git/config, or placed in argv.
|
|
10
|
-
* Server-internal modules (state, helpers, ws) accessed via getServerModules().
|
|
11
|
-
*/
|
|
12
|
-
import { execFile } from 'child_process';
|
|
13
|
-
import { existsSync, writeFileSync } from 'fs';
|
|
14
|
-
import { join } from 'path';
|
|
15
|
-
import { getServerModules } from './helpers.js';
|
|
16
|
-
const GITIGNORE_CONTENT = `config.json\n.versions/\n`;
|
|
17
|
-
const NETWORK_TIMEOUT = 30000;
|
|
18
|
-
let currentSyncState = 'unconfigured';
|
|
19
|
-
let lastError;
|
|
20
|
-
// SECURITY (MCP-1): no shell. Arguments are passed to git/gh as an argv array,
|
|
21
|
-
// so each element is a single literal argument with no shell interpretation.
|
|
22
|
-
// The optional `env` is merged over the parent process env for that one call.
|
|
23
|
-
function exec(cmd, args, cwd, timeout = 10000, env) {
|
|
24
|
-
return new Promise((resolve, reject) => {
|
|
25
|
-
execFile(cmd, args, { cwd, timeout, env: env ? { ...process.env, ...env } : process.env }, (err, stdout, stderr) => {
|
|
26
|
-
if (err)
|
|
27
|
-
reject(new Error(stderr?.trim() || err.message));
|
|
28
|
-
else
|
|
29
|
-
resolve(stdout.trim());
|
|
30
|
-
});
|
|
31
|
-
});
|
|
32
|
-
}
|
|
33
|
-
// ── Credential handling (MCP-3) ─────────────────────────────────────────────
|
|
34
|
-
// The PAT is supplied to authenticated git pushes WITHOUT ever touching the
|
|
35
|
-
// remote URL, .git/config, or argv. An inline credential helper (run by git
|
|
36
|
-
// through its own sh) reads the token from the OW_GIT_PAT env var at call time;
|
|
37
|
-
// only the variable *name* appears in arguments, never the secret itself.
|
|
38
|
-
const PAT_CRED_HELPER = '!f() { test "$1" = get && printf "username=x-access-token\\npassword=%s\\n" "$OW_GIT_PAT"; }; f';
|
|
39
|
-
/**
|
|
40
|
-
* Run a git command that needs the PAT for network auth (push/fetch against a
|
|
41
|
-
* private remote). The `-c credential.helper=` first resets any inherited
|
|
42
|
-
* helper so only ours answers; the token is passed via env, not argv. The
|
|
43
|
-
* remote URL stays credential-free.
|
|
44
|
-
*/
|
|
45
|
-
function execGitWithPat(args, cwd, pat, timeout = NETWORK_TIMEOUT) {
|
|
46
|
-
const authArgs = ['-c', 'credential.helper=', '-c', `credential.helper=${PAT_CRED_HELPER}`];
|
|
47
|
-
return exec('git', [...authArgs, ...args], cwd, timeout, { OW_GIT_PAT: pat });
|
|
48
|
-
}
|
|
49
|
-
/**
|
|
50
|
-
* Strip any embedded credentials (PAT / user:pass) from a git remote URL so a
|
|
51
|
-
* credential-bearing URL is never returned to a client or logged. SSH scp-style
|
|
52
|
-
* remotes (git@github.com:owner/repo.git) carry no secret and are left as-is.
|
|
53
|
-
*/
|
|
54
|
-
export function sanitizeRemoteUrl(url) {
|
|
55
|
-
try {
|
|
56
|
-
const u = new URL(url);
|
|
57
|
-
if (u.username || u.password) {
|
|
58
|
-
u.username = '';
|
|
59
|
-
u.password = '';
|
|
60
|
-
}
|
|
61
|
-
return u.toString();
|
|
62
|
-
}
|
|
63
|
-
catch {
|
|
64
|
-
// Not a parseable URL (e.g. ssh scp-like form) — defensively drop any
|
|
65
|
-
// userinfo that precedes an @ in a scheme://… authority.
|
|
66
|
-
return url.replace(/^([a-z][a-z0-9+.-]*:\/\/)[^/@]*@/i, '$1');
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
async function dataDir() {
|
|
70
|
-
return (await getServerModules()).getDataDir();
|
|
71
|
-
}
|
|
72
|
-
export async function isGitInstalled() {
|
|
73
|
-
try {
|
|
74
|
-
await exec('git', ['--version'], await dataDir());
|
|
75
|
-
return true;
|
|
76
|
-
}
|
|
77
|
-
catch {
|
|
78
|
-
return false;
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
export async function isGhInstalled() {
|
|
82
|
-
try {
|
|
83
|
-
await exec('gh', ['--version'], await dataDir());
|
|
84
|
-
return true;
|
|
85
|
-
}
|
|
86
|
-
catch {
|
|
87
|
-
return false;
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
export async function isGhAuthenticated() {
|
|
91
|
-
try {
|
|
92
|
-
await exec('gh', ['auth', 'status'], await dataDir());
|
|
93
|
-
return true;
|
|
94
|
-
}
|
|
95
|
-
catch {
|
|
96
|
-
return false;
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
export async function isGitRepo() {
|
|
100
|
-
return existsSync(join(await dataDir(), '.git'));
|
|
101
|
-
}
|
|
102
|
-
async function ensureGitignore() {
|
|
103
|
-
const gitignorePath = join(await dataDir(), '.gitignore');
|
|
104
|
-
if (!existsSync(gitignorePath)) {
|
|
105
|
-
writeFileSync(gitignorePath, GITIGNORE_CONTENT, 'utf-8');
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
async function countPendingFiles() {
|
|
109
|
-
if (!(await isGitRepo()))
|
|
110
|
-
return 0;
|
|
111
|
-
try {
|
|
112
|
-
const status = await exec('git', ['status', '--porcelain'], await dataDir());
|
|
113
|
-
if (!status)
|
|
114
|
-
return 0;
|
|
115
|
-
return status.split('\n').filter(Boolean).length;
|
|
116
|
-
}
|
|
117
|
-
catch {
|
|
118
|
-
return 0;
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
export async function getPendingFiles() {
|
|
122
|
-
if (!(await isGitRepo()))
|
|
123
|
-
return [];
|
|
124
|
-
try {
|
|
125
|
-
const output = await exec('git', ['status', '--porcelain'], await dataDir());
|
|
126
|
-
if (!output)
|
|
127
|
-
return [];
|
|
128
|
-
return output.split('\n').filter(Boolean).map(line => {
|
|
129
|
-
const code = line.substring(0, 2);
|
|
130
|
-
const file = line.substring(3);
|
|
131
|
-
let status = 'modified';
|
|
132
|
-
if (code.includes('?') || code.includes('A'))
|
|
133
|
-
status = 'added';
|
|
134
|
-
else if (code.includes('D'))
|
|
135
|
-
status = 'deleted';
|
|
136
|
-
else if (code.includes('R'))
|
|
137
|
-
status = 'renamed';
|
|
138
|
-
return { status, file };
|
|
139
|
-
});
|
|
140
|
-
}
|
|
141
|
-
catch {
|
|
142
|
-
return [];
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
export async function getSyncStatus() {
|
|
146
|
-
const srv = await getServerModules();
|
|
147
|
-
const config = srv.readConfig();
|
|
148
|
-
if (!config.gitConfigured || !(await isGitRepo())) {
|
|
149
|
-
return { state: 'unconfigured' };
|
|
150
|
-
}
|
|
151
|
-
if (currentSyncState === 'syncing') {
|
|
152
|
-
return { state: 'syncing' };
|
|
153
|
-
}
|
|
154
|
-
if (currentSyncState === 'error' && lastError) {
|
|
155
|
-
return { state: 'error', error: lastError, lastSyncTime: config.lastSyncTime };
|
|
156
|
-
}
|
|
157
|
-
const pending = await countPendingFiles();
|
|
158
|
-
return {
|
|
159
|
-
state: pending > 0 ? 'pending' : 'synced',
|
|
160
|
-
pendingFiles: pending,
|
|
161
|
-
lastSyncTime: config.lastSyncTime,
|
|
162
|
-
};
|
|
163
|
-
}
|
|
164
|
-
export async function getCapabilities() {
|
|
165
|
-
const [git, gh] = await Promise.all([isGitInstalled(), isGhInstalled()]);
|
|
166
|
-
let ghAuth = false;
|
|
167
|
-
if (gh)
|
|
168
|
-
ghAuth = await isGhAuthenticated();
|
|
169
|
-
let remoteUrl;
|
|
170
|
-
if (await isGitRepo()) {
|
|
171
|
-
try {
|
|
172
|
-
// MCP-3: never expose embedded credentials. Even though the remote is
|
|
173
|
-
// now stored credential-free, strip defensively so a legacy URL written
|
|
174
|
-
// by an older build (PAT-in-URL) can't leak through this route.
|
|
175
|
-
remoteUrl = sanitizeRemoteUrl(await exec('git', ['remote', 'get-url', 'origin'], await dataDir()));
|
|
176
|
-
}
|
|
177
|
-
catch { /* no remote */ }
|
|
178
|
-
}
|
|
179
|
-
return {
|
|
180
|
-
gitInstalled: git,
|
|
181
|
-
ghInstalled: gh,
|
|
182
|
-
ghAuthenticated: ghAuth,
|
|
183
|
-
existingRepo: await isGitRepo(),
|
|
184
|
-
remoteUrl,
|
|
185
|
-
};
|
|
186
|
-
}
|
|
187
|
-
async function initRepo() {
|
|
188
|
-
const dir = await dataDir();
|
|
189
|
-
if (!(await isGitRepo())) {
|
|
190
|
-
await exec('git', ['init'], dir);
|
|
191
|
-
}
|
|
192
|
-
await ensureGitignore();
|
|
193
|
-
try {
|
|
194
|
-
await exec('git', ['config', 'user.name'], dir);
|
|
195
|
-
}
|
|
196
|
-
catch {
|
|
197
|
-
await exec('git', ['config', 'user.name', 'OpenWriter'], dir);
|
|
198
|
-
}
|
|
199
|
-
try {
|
|
200
|
-
await exec('git', ['config', 'user.email'], dir);
|
|
201
|
-
}
|
|
202
|
-
catch {
|
|
203
|
-
await exec('git', ['config', 'user.email', 'openwriter@local'], dir);
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
async function initialCommit() {
|
|
207
|
-
const dir = await dataDir();
|
|
208
|
-
await exec('git', ['add', '-A'], dir);
|
|
209
|
-
const status = await exec('git', ['status', '--porcelain'], dir);
|
|
210
|
-
if (!status)
|
|
211
|
-
return;
|
|
212
|
-
await exec('git', ['commit', '-m', 'Initial sync from OpenWriter'], dir);
|
|
213
|
-
await exec('git', ['branch', '-M', 'main'], dir);
|
|
214
|
-
}
|
|
215
|
-
export async function setupWithGh(repoName, isPrivate) {
|
|
216
|
-
const srv = await getServerModules();
|
|
217
|
-
const dir = await dataDir();
|
|
218
|
-
await initRepo();
|
|
219
|
-
await initialCommit();
|
|
220
|
-
const visibility = isPrivate ? '--private' : '--public';
|
|
221
|
-
await exec('gh', ['repo', 'create', repoName, visibility, '--source=.', '--remote=origin'], dir, NETWORK_TIMEOUT);
|
|
222
|
-
await exec('git', ['push', '-u', 'origin', 'main'], dir, NETWORK_TIMEOUT);
|
|
223
|
-
srv.saveConfig({
|
|
224
|
-
gitConfigured: true,
|
|
225
|
-
repoName,
|
|
226
|
-
lastSyncTime: new Date().toISOString(),
|
|
227
|
-
});
|
|
228
|
-
currentSyncState = 'synced';
|
|
229
|
-
}
|
|
230
|
-
export async function setupWithPat(pat, repoName, isPrivate) {
|
|
231
|
-
const srv = await getServerModules();
|
|
232
|
-
const dir = await dataDir();
|
|
233
|
-
const res = await fetch('https://api.github.com/user/repos', {
|
|
234
|
-
method: 'POST',
|
|
235
|
-
headers: {
|
|
236
|
-
Authorization: `Bearer ${pat}`,
|
|
237
|
-
'Content-Type': 'application/json',
|
|
238
|
-
Accept: 'application/vnd.github+json',
|
|
239
|
-
},
|
|
240
|
-
body: JSON.stringify({ name: repoName, private: isPrivate, auto_init: false }),
|
|
241
|
-
});
|
|
242
|
-
if (!res.ok) {
|
|
243
|
-
const body = await res.json().catch(() => ({}));
|
|
244
|
-
throw new Error(body.message || `GitHub API error: ${res.status}`);
|
|
245
|
-
}
|
|
246
|
-
const repo = await res.json();
|
|
247
|
-
// MCP-3: store a credential-free remote. The PAT is supplied per-push via
|
|
248
|
-
// the credential helper (execGitWithPat), never embedded in the URL.
|
|
249
|
-
const remoteUrl = `https://github.com/${repo.full_name}.git`;
|
|
250
|
-
await initRepo();
|
|
251
|
-
await initialCommit();
|
|
252
|
-
try {
|
|
253
|
-
await exec('git', ['remote', 'remove', 'origin'], dir);
|
|
254
|
-
}
|
|
255
|
-
catch { /* no remote */ }
|
|
256
|
-
await exec('git', ['remote', 'add', 'origin', remoteUrl], dir);
|
|
257
|
-
await execGitWithPat(['push', '-u', 'origin', 'main'], dir, pat, NETWORK_TIMEOUT);
|
|
258
|
-
srv.saveConfig({
|
|
259
|
-
gitConfigured: true,
|
|
260
|
-
gitPat: pat,
|
|
261
|
-
repoName,
|
|
262
|
-
gitRemote: repo.html_url,
|
|
263
|
-
lastSyncTime: new Date().toISOString(),
|
|
264
|
-
});
|
|
265
|
-
currentSyncState = 'synced';
|
|
266
|
-
}
|
|
267
|
-
export async function connectExisting(remoteUrl, pat) {
|
|
268
|
-
const srv = await getServerModules();
|
|
269
|
-
const dir = await dataDir();
|
|
270
|
-
await initRepo();
|
|
271
|
-
await initialCommit();
|
|
272
|
-
// MCP-3: keep the remote credential-free; never splice the PAT into the URL.
|
|
273
|
-
// Strip any credentials the caller may have included before storing/using it.
|
|
274
|
-
const finalUrl = sanitizeRemoteUrl(remoteUrl);
|
|
275
|
-
try {
|
|
276
|
-
await exec('git', ['remote', 'remove', 'origin'], dir);
|
|
277
|
-
}
|
|
278
|
-
catch { /* no remote */ }
|
|
279
|
-
await exec('git', ['remote', 'add', 'origin', finalUrl], dir);
|
|
280
|
-
if (pat) {
|
|
281
|
-
await execGitWithPat(['push', '-u', 'origin', 'main'], dir, pat, NETWORK_TIMEOUT);
|
|
282
|
-
}
|
|
283
|
-
else {
|
|
284
|
-
await exec('git', ['push', '-u', 'origin', 'main'], dir, NETWORK_TIMEOUT);
|
|
285
|
-
}
|
|
286
|
-
srv.saveConfig({
|
|
287
|
-
gitConfigured: true,
|
|
288
|
-
gitPat: pat,
|
|
289
|
-
gitRemote: finalUrl,
|
|
290
|
-
lastSyncTime: new Date().toISOString(),
|
|
291
|
-
});
|
|
292
|
-
currentSyncState = 'synced';
|
|
293
|
-
}
|
|
294
|
-
export async function pushSync(onStatus) {
|
|
295
|
-
const srv = await getServerModules();
|
|
296
|
-
const dir = await dataDir();
|
|
297
|
-
currentSyncState = 'syncing';
|
|
298
|
-
lastError = undefined;
|
|
299
|
-
onStatus({ state: 'syncing' });
|
|
300
|
-
try {
|
|
301
|
-
srv.cancelDebouncedSave();
|
|
302
|
-
srv.save();
|
|
303
|
-
await ensureGitignore();
|
|
304
|
-
await exec('git', ['add', '-A'], dir);
|
|
305
|
-
const status = await exec('git', ['status', '--porcelain'], dir);
|
|
306
|
-
if (status) {
|
|
307
|
-
const timestamp = new Date().toLocaleString('en-US', {
|
|
308
|
-
month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit',
|
|
309
|
-
});
|
|
310
|
-
await exec('git', ['commit', '-m', `Sync: ${timestamp}`], dir);
|
|
311
|
-
}
|
|
312
|
-
// MCP-3: the remote is credential-free. When configured via PAT, supply
|
|
313
|
-
// the token out-of-band per-push; gh-based / SSH remotes auth on their own.
|
|
314
|
-
const pat = srv.readConfig()?.gitPat;
|
|
315
|
-
if (pat) {
|
|
316
|
-
await execGitWithPat(['push'], dir, pat, NETWORK_TIMEOUT);
|
|
317
|
-
}
|
|
318
|
-
else {
|
|
319
|
-
await exec('git', ['push'], dir, NETWORK_TIMEOUT);
|
|
320
|
-
}
|
|
321
|
-
const now = new Date().toISOString();
|
|
322
|
-
srv.saveConfig({ lastSyncTime: now });
|
|
323
|
-
currentSyncState = 'synced';
|
|
324
|
-
const result = { state: 'synced', lastSyncTime: now, pendingFiles: 0 };
|
|
325
|
-
onStatus(result);
|
|
326
|
-
return result;
|
|
327
|
-
}
|
|
328
|
-
catch (err) {
|
|
329
|
-
currentSyncState = 'error';
|
|
330
|
-
lastError = err.message;
|
|
331
|
-
const result = { state: 'error', error: err.message };
|
|
332
|
-
onStatus(result);
|
|
333
|
-
return result;
|
|
334
|
-
}
|
|
335
|
-
}
|
|
@@ -1,127 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Local helpers for the github plugin — server-module bridge.
|
|
3
|
-
* Mirrors the lazy-import pattern from plugins/publish/src/helpers.ts.
|
|
4
|
-
*/
|
|
5
|
-
export interface ServerModules {
|
|
6
|
-
getDataDir: () => string;
|
|
7
|
-
readConfig: () => any;
|
|
8
|
-
saveConfig: (patch: Record<string, any>) => void;
|
|
9
|
-
save: () => void;
|
|
10
|
-
cancelDebouncedSave: () => void;
|
|
11
|
-
getDocument: () => any;
|
|
12
|
-
getTitle: () => string;
|
|
13
|
-
getMetadata: () => Record<string, any>;
|
|
14
|
-
getDocId: () => string;
|
|
15
|
-
setMetadata: (updates: Record<string, any>) => void;
|
|
16
|
-
bumpDocVersion: () => number;
|
|
17
|
-
broadcastSyncStatus: (status: any) => void;
|
|
18
|
-
broadcastMetadataChanged: (metadata: Record<string, any>) => void;
|
|
19
|
-
broadcastDocumentsChanged: () => void;
|
|
20
|
-
broadcastToast: (message: string, kind?: 'info' | 'error', durationMs?: number) => void;
|
|
21
|
-
tiptapToMarkdown: (doc: any, title: string, metadata?: Record<string, any>) => string;
|
|
22
|
-
validateBlogFrontmatter: (opts: {
|
|
23
|
-
repoRoot: string;
|
|
24
|
-
contentDir: string;
|
|
25
|
-
frontmatter: string;
|
|
26
|
-
}) => Promise<BlogFrontmatterValidation>;
|
|
27
|
-
}
|
|
28
|
-
/** Result of the pre-commit schema gate. Mirrors server/blog-schema-gate.ts. */
|
|
29
|
-
export interface BlogFrontmatterValidation {
|
|
30
|
-
ok: boolean;
|
|
31
|
-
skipped?: boolean;
|
|
32
|
-
reason?: string;
|
|
33
|
-
configPath?: string;
|
|
34
|
-
collection?: string;
|
|
35
|
-
issues?: Array<{
|
|
36
|
-
field: string;
|
|
37
|
-
code: string;
|
|
38
|
-
message: string;
|
|
39
|
-
}>;
|
|
40
|
-
summary?: string;
|
|
41
|
-
}
|
|
42
|
-
export declare function getServerModules(): Promise<ServerModules>;
|
|
43
|
-
export interface PluginConfigField {
|
|
44
|
-
type: 'string' | 'number' | 'boolean';
|
|
45
|
-
required?: boolean;
|
|
46
|
-
env?: string;
|
|
47
|
-
description?: string;
|
|
48
|
-
}
|
|
49
|
-
export interface PluginRouteContext {
|
|
50
|
-
app: import('express').Router;
|
|
51
|
-
config: Record<string, string>;
|
|
52
|
-
dataDir: string;
|
|
53
|
-
}
|
|
54
|
-
export interface PluginMcpTool {
|
|
55
|
-
name: string;
|
|
56
|
-
description: string;
|
|
57
|
-
inputSchema: Record<string, unknown>;
|
|
58
|
-
handler: (params: Record<string, unknown>) => Promise<unknown>;
|
|
59
|
-
}
|
|
60
|
-
export interface OpenWriterPlugin {
|
|
61
|
-
name: string;
|
|
62
|
-
version: string;
|
|
63
|
-
description?: string;
|
|
64
|
-
category?: 'writing' | 'social-media' | 'image-generation' | 'publishing' | 'productivity' | 'analytics';
|
|
65
|
-
configSchema?: Record<string, PluginConfigField>;
|
|
66
|
-
registerRoutes?(ctx: PluginRouteContext): void | Promise<void>;
|
|
67
|
-
mcpTools?(config: Record<string, string>): PluginMcpTool[];
|
|
68
|
-
}
|
|
69
|
-
export interface BlogSite {
|
|
70
|
-
id: string;
|
|
71
|
-
label: string;
|
|
72
|
-
owner: string;
|
|
73
|
-
repo: string;
|
|
74
|
-
branch: string;
|
|
75
|
-
content_dir: string;
|
|
76
|
-
image_dir: string;
|
|
77
|
-
image_public_prefix: string;
|
|
78
|
-
framework: 'astro' | 'next' | 'jekyll' | 'hugo' | 'unknown';
|
|
79
|
-
/** Constant frontmatter fields applied to every post (e.g. layout, author, prerender). */
|
|
80
|
-
frontmatter_defaults?: Record<string, any>;
|
|
81
|
-
/**
|
|
82
|
-
* Map openwriter blogContext key → site frontmatter key.
|
|
83
|
-
* e.g. { date: "publishedDate" } for sites that use a non-standard date field.
|
|
84
|
-
*/
|
|
85
|
-
frontmatter_field_map?: Record<string, string>;
|
|
86
|
-
/**
|
|
87
|
-
* Schema of frontmatter fields the site's posts use (from inspection).
|
|
88
|
-
* Surfaced in the panel so users can see what the site expects.
|
|
89
|
-
* Not used as a filter — only the defaults + blogContext determine output.
|
|
90
|
-
*/
|
|
91
|
-
frontmatter_schema?: string[];
|
|
92
|
-
/**
|
|
93
|
-
* Public base URL of the site (e.g. "https://example.com"). Used by
|
|
94
|
-
* post_to_blog to construct the live URL stamped on `blogContext.lastPublish.publishedUrl`
|
|
95
|
-
* so the file-tree right-click menu can surface "View Post" after a publish.
|
|
96
|
-
* inspect_blog_repo proposes this from `CNAME` / `public/CNAME` when present.
|
|
97
|
-
*/
|
|
98
|
-
site_url?: string;
|
|
99
|
-
/**
|
|
100
|
-
* URL path pattern for a blog post, with `{slug}` as the placeholder.
|
|
101
|
-
* Default: `/blog/{slug}/`. Used together with `site_url` to build the live URL.
|
|
102
|
-
*/
|
|
103
|
-
blog_url_pattern?: string;
|
|
104
|
-
/**
|
|
105
|
-
* How image paths are written into frontmatter + body:
|
|
106
|
-
* "relative" — no leading slash (`images/og/x.png`); the site's template
|
|
107
|
-
* prepends the slash (e.g. Astro `<img src={`/${image}`}>`).
|
|
108
|
-
* "absolute" — leading slash (`/images/og/x.png`); value used verbatim.
|
|
109
|
-
* Inferred by inspect_blog_repo from existing posts' image values. Absent ⇒
|
|
110
|
-
* "absolute" — the legacy behavior the plugin always had before this field.
|
|
111
|
-
* adr: adr/blog-image-contract.md
|
|
112
|
-
*/
|
|
113
|
-
image_path_style?: 'relative' | 'absolute';
|
|
114
|
-
/**
|
|
115
|
-
* Filename template the published COVER image is copied under. Placeholders:
|
|
116
|
-
* `{slug}` — the post slug
|
|
117
|
-
* `{ext}` — the source file's extension, no dot (preserved from the
|
|
118
|
-
* `/_images/` original)
|
|
119
|
-
* Default `og-{slug}.{ext}`. Deterministic: same doc + same slug ⇒ same
|
|
120
|
-
* cover filename on every republish (idempotent overwrite, no orphaned
|
|
121
|
-
* cover files). Inferred by inspect_blog_repo from existing posts.
|
|
122
|
-
* adr: adr/blog-image-contract.md
|
|
123
|
-
*/
|
|
124
|
-
image_naming?: string;
|
|
125
|
-
}
|
|
126
|
-
export declare function listBlogSites(): Promise<BlogSite[]>;
|
|
127
|
-
export declare function writeBlogSites(sites: BlogSite[]): Promise<void>;
|
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Local helpers for the github plugin — server-module bridge.
|
|
3
|
-
* Mirrors the lazy-import pattern from plugins/publish/src/helpers.ts.
|
|
4
|
-
*/
|
|
5
|
-
// npm package: dist/plugins/github/dist/helpers.js → ../../../server/
|
|
6
|
-
// Monorepo dev: plugins/github/dist/helpers.js → ../../../packages/openwriter/dist/server/
|
|
7
|
-
const npmBase = new URL('../../../server/', import.meta.url).href;
|
|
8
|
-
const monoBase = new URL('../../../packages/openwriter/dist/server/', import.meta.url).href;
|
|
9
|
-
let _cached = null;
|
|
10
|
-
async function tryImport(base) {
|
|
11
|
-
const [helpers, state, ws, markdown, schemaGate] = await Promise.all([
|
|
12
|
-
import(base + 'helpers.js'),
|
|
13
|
-
import(base + 'state.js'),
|
|
14
|
-
import(base + 'ws.js'),
|
|
15
|
-
import(base + 'markdown.js'),
|
|
16
|
-
import(base + 'blog-schema-gate.js'),
|
|
17
|
-
]);
|
|
18
|
-
return { helpers, state, ws, markdown, schemaGate };
|
|
19
|
-
}
|
|
20
|
-
export async function getServerModules() {
|
|
21
|
-
if (_cached)
|
|
22
|
-
return _cached;
|
|
23
|
-
let helpers, state, ws, markdown, schemaGate;
|
|
24
|
-
try {
|
|
25
|
-
({ helpers, state, ws, markdown, schemaGate } = await tryImport(npmBase));
|
|
26
|
-
}
|
|
27
|
-
catch {
|
|
28
|
-
({ helpers, state, ws, markdown, schemaGate } = await tryImport(monoBase));
|
|
29
|
-
}
|
|
30
|
-
_cached = {
|
|
31
|
-
getDataDir: helpers.getDataDir,
|
|
32
|
-
readConfig: helpers.readConfig,
|
|
33
|
-
saveConfig: helpers.saveConfig,
|
|
34
|
-
save: state.save,
|
|
35
|
-
cancelDebouncedSave: state.cancelDebouncedSave,
|
|
36
|
-
getDocument: state.getDocument,
|
|
37
|
-
getTitle: state.getTitle,
|
|
38
|
-
getMetadata: state.getMetadata,
|
|
39
|
-
getDocId: state.getDocId,
|
|
40
|
-
setMetadata: state.setMetadata,
|
|
41
|
-
bumpDocVersion: state.bumpDocVersion,
|
|
42
|
-
broadcastSyncStatus: ws.broadcastSyncStatus,
|
|
43
|
-
broadcastMetadataChanged: ws.broadcastMetadataChanged,
|
|
44
|
-
broadcastDocumentsChanged: ws.broadcastDocumentsChanged,
|
|
45
|
-
broadcastToast: ws.broadcastToast,
|
|
46
|
-
tiptapToMarkdown: markdown.tiptapToMarkdown,
|
|
47
|
-
validateBlogFrontmatter: schemaGate.validateBlogFrontmatter,
|
|
48
|
-
};
|
|
49
|
-
return _cached;
|
|
50
|
-
}
|
|
51
|
-
const PLUGIN_NAME = '@openwriter/plugin-github';
|
|
52
|
-
export async function listBlogSites() {
|
|
53
|
-
const srv = await getServerModules();
|
|
54
|
-
const cfg = srv.readConfig() || {};
|
|
55
|
-
const slot = cfg.plugins?.[PLUGIN_NAME];
|
|
56
|
-
return slot?.blogSites || [];
|
|
57
|
-
}
|
|
58
|
-
// adr: adr/plugin-slot-nested-data.md
|
|
59
|
-
export async function writeBlogSites(sites) {
|
|
60
|
-
const srv = await getServerModules();
|
|
61
|
-
const cfg = srv.readConfig() || {};
|
|
62
|
-
const plugins = { ...(cfg.plugins || {}) };
|
|
63
|
-
const slot = { ...(plugins[PLUGIN_NAME] || { enabled: true, config: {} }) };
|
|
64
|
-
slot.blogSites = sites;
|
|
65
|
-
plugins[PLUGIN_NAME] = slot;
|
|
66
|
-
srv.saveConfig({ plugins });
|
|
67
|
-
}
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* GitHub Plugin for OpenWriter.
|
|
3
|
-
* Provides two capabilities:
|
|
4
|
-
* 1. Docs backup sync — lifted from core. Auth: existing `gh auth`.
|
|
5
|
-
* 2. Blog sites (Phase 3+) — list of blog repos, post via local git ops.
|
|
6
|
-
*
|
|
7
|
-
* This phase ships capability 1 only. Routes match the previous core mount
|
|
8
|
-
* points exactly so SyncButton + SyncSetupModal continue to work unchanged.
|
|
9
|
-
*/
|
|
10
|
-
import type { OpenWriterPlugin } from './helpers.js';
|
|
11
|
-
declare const plugin: OpenWriterPlugin;
|
|
12
|
-
export default plugin;
|