@gobing-ai/knowledge-kit 0.0.11 → 0.0.12
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/index.js +363 -138
- package/package.json +1 -1
- package/plugins/generations/content-gen/src/storm.ts +99 -50
- package/plugins/generations/core-facts-gen/package.json +17 -0
- package/plugins/generations/core-facts-gen/plugin.json +7 -0
- package/plugins/generations/core-facts-gen/src/index.ts +116 -0
- package/plugins/generations/core-facts-gen/tsconfig.json +4 -0
- package/plugins/generations/daily-article-gen/package.json +17 -0
- package/plugins/generations/daily-article-gen/plugin.json +7 -0
- package/plugins/generations/daily-article-gen/src/index.ts +91 -0
- package/plugins/generations/daily-article-gen/tsconfig.json +4 -0
- package/plugins/generations/dailynews-gen/src/index.ts +11 -0
- package/plugins/generations/dailynews-gen/src/script-builder.ts +1 -1
- package/plugins/generations/episode-plan-gen/package.json +17 -0
- package/plugins/generations/episode-plan-gen/plugin.json +7 -0
- package/plugins/generations/episode-plan-gen/src/index.ts +726 -0
- package/plugins/generations/episode-plan-gen/tsconfig.json +4 -0
- package/plugins/generations/voice-gen/src/index.ts +102 -11
- package/plugins/generations/voice-gen/src/qc.ts +154 -9
- package/plugins/ingestions/aihot-ingest/plugin.json +1 -1
- package/plugins/ingestions/aihot-ingest/src/index.ts +72 -13
- package/plugins/ingestions/aihot-ingest/src/mapper.ts +1 -0
- package/plugins/ingestions/aihot-ingest/src/rss.ts +151 -0
- package/plugins/ingestions/horizon-ingest/package.json +17 -0
- package/plugins/ingestions/horizon-ingest/plugin.json +7 -0
- package/plugins/ingestions/horizon-ingest/src/index.ts +205 -0
- package/plugins/ingestions/horizon-ingest/tsconfig.json +4 -0
- package/plugins/ingestions/last30days-ingest/package.json +17 -0
- package/plugins/ingestions/last30days-ingest/plugin.json +7 -0
- package/plugins/ingestions/last30days-ingest/src/index.ts +148 -0
- package/plugins/ingestions/last30days-ingest/tsconfig.json +4 -0
- package/plugins/kk/skills/taste-unslop/SKILL.md +12 -6
- package/plugins/kk/skills/taste-unslop/references/pattern-guide.md +128 -48
- package/plugins/kk/workflows/kk-daily-ai-voice.yaml +432 -19
- package/plugins/publishings/podcast-pub/package.json +17 -0
- package/plugins/publishings/podcast-pub/plugin.json +7 -0
- package/plugins/publishings/podcast-pub/src/index.ts +538 -0
- package/plugins/publishings/podcast-pub/src/map.ts +165 -0
- package/plugins/publishings/podcast-pub/src/microfeed-client.ts +196 -0
- package/plugins/publishings/podcast-pub/src/show-notes.ts +132 -0
- package/plugins/publishings/podcast-pub/tsconfig.json +4 -0
- package/plugins/publishings/surfdash-pub/src/index.ts +328 -62
|
@@ -1,16 +1,42 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { copyFile, cp } from 'node:fs/promises';
|
|
3
|
+
import { dirname, isAbsolute, join, resolve, sep } from 'node:path';
|
|
2
4
|
import { parseArgs } from 'node:util';
|
|
3
5
|
import { type Content, ContentSchema, type Result, ResultSchema } from '@gobing-ai/kk-core';
|
|
4
6
|
import type { PublishTransport, PublishTransportPayload } from '@gobing-ai/publish-harness';
|
|
5
7
|
import { createNodeFileSystem } from '@gobing-ai/ts-runtime';
|
|
6
8
|
import { echoError } from '@gobing-ai/ts-utils';
|
|
7
9
|
|
|
10
|
+
/** Result target name for this plugin. */
|
|
11
|
+
const TARGET = 'surfdash';
|
|
12
|
+
|
|
13
|
+
/** Management operations routed via `options.operation` (L1 R7; pattern from podcast-pub). */
|
|
14
|
+
const OPERATIONS = ['publish', 'scaffold-locale', 'sync'] as const;
|
|
15
|
+
type Operation = (typeof OPERATIONS)[number];
|
|
16
|
+
|
|
17
|
+
/** Locales `scaffold-locale` can scaffold; LLM translation stays in the workflow's agent step. */
|
|
18
|
+
const SCAFFOLD_LOCALES = ['en', 'ja'] as const;
|
|
19
|
+
type ScaffoldLocale = (typeof SCAFFOLD_LOCALES)[number];
|
|
20
|
+
|
|
21
|
+
/** Target-specific options carried on the invoke payload's `options` channel. */
|
|
22
|
+
interface SurfdashPubOptions {
|
|
23
|
+
operation?: unknown;
|
|
24
|
+
/** zh `index.md` absolute path inside the surfdash tree; required for `scaffold-locale` and `sync`. */
|
|
25
|
+
postPath?: unknown;
|
|
26
|
+
/** Draft locale to scaffold; required for `scaffold-locale`. */
|
|
27
|
+
target?: unknown;
|
|
28
|
+
}
|
|
29
|
+
|
|
8
30
|
/**
|
|
9
31
|
* Options for mapping Content into Surfing Markdown with frontmatter.
|
|
10
32
|
*/
|
|
11
33
|
export interface MapContentOptions {
|
|
12
34
|
/** Target publishing site name or base URL. Defaults to 'surfdash'. */
|
|
13
35
|
targetName?: string;
|
|
36
|
+
/** Frontmatter image value (e.g. `./assets/cover.png`); written to `image` and `og_image`. */
|
|
37
|
+
image?: string;
|
|
38
|
+
/** ISO date for publishDate; defaults to today. */
|
|
39
|
+
publishDate?: string;
|
|
14
40
|
}
|
|
15
41
|
|
|
16
42
|
/**
|
|
@@ -27,7 +53,7 @@ export function slugify(str: string): string {
|
|
|
27
53
|
/**
|
|
28
54
|
* Maps Content object into Surfing frontmatter and body per the publish spec.
|
|
29
55
|
*/
|
|
30
|
-
export function mapContentToSurfingMarkdown(content: Content): string {
|
|
56
|
+
export function mapContentToSurfingMarkdown(content: Content, options: MapContentOptions = {}): string {
|
|
31
57
|
let title = content.title;
|
|
32
58
|
if (!title) {
|
|
33
59
|
const match = content.body.match(/^#\s+(.+)$/m);
|
|
@@ -56,86 +82,316 @@ export function mapContentToSurfingMarkdown(content: Content): string {
|
|
|
56
82
|
.filter((t): t is string => typeof t === 'string' && t.trim().length > 0)
|
|
57
83
|
.map((t: string) => slugify(t));
|
|
58
84
|
|
|
59
|
-
const
|
|
85
|
+
const isoDate = options.publishDate ?? new Date().toISOString().slice(0, 10);
|
|
60
86
|
const firstRefUrl = content.references?.[0]?.url;
|
|
61
|
-
const
|
|
87
|
+
const canonical =
|
|
62
88
|
typeof firstRefUrl === 'string'
|
|
63
89
|
? firstRefUrl
|
|
64
90
|
: typeof content.metadata?.canonicalUrl === 'string'
|
|
65
91
|
? content.metadata.canonicalUrl
|
|
66
92
|
: undefined;
|
|
67
|
-
const
|
|
93
|
+
const draft = typeof content.metadata?.published === 'boolean' ? !content.metadata.published : false;
|
|
68
94
|
|
|
95
|
+
// Strict surfdash PostSchema (surfdash/src/content/schema.ts): publishDate (ISO date) is
|
|
96
|
+
// required; unknown keys (`date`, `canonical_url`, `published`) are rejected.
|
|
69
97
|
const frontmatterObj: Record<string, unknown> = {
|
|
70
98
|
title,
|
|
71
|
-
|
|
99
|
+
description: description ?? '',
|
|
72
100
|
tags,
|
|
73
|
-
|
|
74
|
-
...(
|
|
75
|
-
|
|
101
|
+
publishDate: isoDate,
|
|
102
|
+
...(canonical ? { canonical } : {}),
|
|
103
|
+
draft,
|
|
104
|
+
...(options.image ? { image: options.image, og_image: options.image } : {}),
|
|
76
105
|
};
|
|
77
106
|
|
|
78
107
|
const yamlLines = ['---', ...Object.entries(frontmatterObj).map(([k, v]) => `${k}: ${JSON.stringify(v)}`), '---'];
|
|
79
108
|
return `${yamlLines.join('\n')}\n\n${content.body.trim()}\n`;
|
|
80
109
|
}
|
|
81
110
|
|
|
111
|
+
/** `Result` failure helper: ok false + error (podcast-pub pattern). */
|
|
112
|
+
function fail(error: string): Result {
|
|
113
|
+
return ResultSchema.parse({ ok: false, target: TARGET, error });
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Parses and routes `options.operation`; unknown or misplaced values fail loud. */
|
|
117
|
+
function resolveOperation(options: SurfdashPubOptions): { operation: Operation } | { error: string } {
|
|
118
|
+
const raw = options.operation ?? 'publish';
|
|
119
|
+
if (typeof raw !== 'string' || !OPERATIONS.includes(raw as Operation)) {
|
|
120
|
+
return { error: `invalid options.operation "${String(raw)}": expected one of ${OPERATIONS.join(', ')}` };
|
|
121
|
+
}
|
|
122
|
+
return { operation: raw as Operation };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** `options.postPath` (zh index.md abs path inside the surfdash checkout) is required for the path-scoped operations. */
|
|
126
|
+
function resolvePostPath(options: SurfdashPubOptions): { postPath: string } | { error: string } {
|
|
127
|
+
const raw = options.postPath;
|
|
128
|
+
if (typeof raw !== 'string' || raw.trim().length === 0) {
|
|
129
|
+
return { error: `options.postPath is required for the "${String(options.operation)}" operation` };
|
|
130
|
+
}
|
|
131
|
+
const postPath = raw.trim();
|
|
132
|
+
if (!isAbsolute(postPath)) {
|
|
133
|
+
return { error: `options.postPath must be an absolute path inside the surfdash checkout: ${postPath}` };
|
|
134
|
+
}
|
|
135
|
+
return { postPath };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Containment guard (review fix #1): `resolve(postPath)` must land inside the resolved surfdash
|
|
140
|
+
* checkout — both CLI-backed operations spawn with cwd=surfdashRoot, so a relative or
|
|
141
|
+
* `..`-escaping path would otherwise be existence-checked against one base and executed against
|
|
142
|
+
* another (or act outside the checkout entirely). Array argv + no shell already rule out argv
|
|
143
|
+
* injection; this closes path traversal. Returns the fail-loud error text, or undefined when inside.
|
|
144
|
+
*/
|
|
145
|
+
function postPathOutsideRoot(postPath: string, surfdashRoot: string): string | undefined {
|
|
146
|
+
const root = resolve(surfdashRoot);
|
|
147
|
+
if (!resolve(postPath).startsWith(root + sep)) {
|
|
148
|
+
return `options.postPath must resolve inside the surfdash checkout (${root}): ${postPath}`;
|
|
149
|
+
}
|
|
150
|
+
return undefined;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** `options.target` must be one of the scaffoldable locales — no default, fail loud. */
|
|
154
|
+
function resolveScaffoldTarget(options: SurfdashPubOptions): { locale: ScaffoldLocale } | { error: string } {
|
|
155
|
+
const raw = options.target;
|
|
156
|
+
if (typeof raw !== 'string' || !SCAFFOLD_LOCALES.includes(raw as ScaffoldLocale)) {
|
|
157
|
+
return {
|
|
158
|
+
error: `options.target is required for the "scaffold-locale" operation: expected one of ${SCAFFOLD_LOCALES.join(', ')}`,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
return { locale: raw as ScaffoldLocale };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Derives the locale draft path from the zh post path: the last `/zh/` segment becomes
|
|
166
|
+
* `/<locale>/` (`content/posts/articles/zh/<slug>/index.md` → `.../<locale>/<slug>/index.md`).
|
|
167
|
+
* Returns undefined when the path carries no `/zh/` segment (caller fails loud).
|
|
168
|
+
*/
|
|
169
|
+
function deriveLocaleDraftPath(postPath: string, locale: ScaffoldLocale): string | undefined {
|
|
170
|
+
const marker = `${sep}zh${sep}`;
|
|
171
|
+
const idx = postPath.lastIndexOf(marker);
|
|
172
|
+
if (idx < 0) {
|
|
173
|
+
return undefined;
|
|
174
|
+
}
|
|
175
|
+
return `${postPath.slice(0, idx)}${sep}${locale}${sep}${postPath.slice(idx + marker.length)}`;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Resolves the surfdash checkout root: `SURFDASH_ROOT` explicitly, or derived from a
|
|
180
|
+
* `POSTSURFING_BIN` path inside the checkout (`<root>/scripts/<cli>`). Returns undefined
|
|
181
|
+
* when neither channel yields a root — every operation fails loud on undefined.
|
|
182
|
+
*/
|
|
183
|
+
function resolveSurfdashRoot(): string | undefined {
|
|
184
|
+
const postsurfingBin = process.env.POSTSURFING_BIN ?? 'postsurfing';
|
|
185
|
+
return (
|
|
186
|
+
process.env.SURFDASH_ROOT ??
|
|
187
|
+
(postsurfingBin.includes('/') ? resolve(dirname(dirname(postsurfingBin))) : undefined)
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** Spawn result shared by the CLI-backed operations. */
|
|
192
|
+
interface CliSpawnResult {
|
|
193
|
+
exitCode: number;
|
|
194
|
+
stdout: string;
|
|
195
|
+
stderr: string;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** One spawn helper for every CLI-backed operation (postsurfing publish / sd translate / orchestrator sync). */
|
|
199
|
+
async function spawnCli(cmd: string[], cwd: string): Promise<CliSpawnResult> {
|
|
200
|
+
const proc = Bun.spawn(cmd, {
|
|
201
|
+
cwd,
|
|
202
|
+
stdout: 'pipe',
|
|
203
|
+
stderr: 'pipe',
|
|
204
|
+
});
|
|
205
|
+
const [exitCode, stdout, stderr] = await Promise.all([
|
|
206
|
+
proc.exited,
|
|
207
|
+
new Response(proc.stdout).text(),
|
|
208
|
+
new Response(proc.stderr).text(),
|
|
209
|
+
]);
|
|
210
|
+
return { exitCode, stdout, stderr };
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/** Stderr-first error text for a failed CLI spawn (stderr may be empty → fall back to stdout). */
|
|
214
|
+
function cliErrorText(spawned: CliSpawnResult): string {
|
|
215
|
+
return spawned.stderr.trim() || spawned.stdout.trim() || 'Unknown error';
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Scaffolds a locale draft for an existing zh post (L1 R7 / 0096): runs the surfdash
|
|
220
|
+
* `sd translate` scaffold CLI inside the surfdash checkout, copies the post's `assets/`
|
|
221
|
+
* dir into the draft dir when present, and returns the draft path in `metadata.draftPath`.
|
|
222
|
+
* No LLM translation happens here — the workflow's agent step edits the returned draft.
|
|
223
|
+
*/
|
|
224
|
+
async function scaffoldLocale(options: SurfdashPubOptions): Promise<Result> {
|
|
225
|
+
const postPath = resolvePostPath(options);
|
|
226
|
+
if ('error' in postPath) return fail(postPath.error);
|
|
227
|
+
const target = resolveScaffoldTarget(options);
|
|
228
|
+
if ('error' in target) return fail(target.error);
|
|
229
|
+
|
|
230
|
+
const surfdashRoot = resolveSurfdashRoot();
|
|
231
|
+
if (!surfdashRoot) {
|
|
232
|
+
return fail('SURFDASH_ROOT is required when POSTSURFING_BIN is not a path inside the surfdash checkout');
|
|
233
|
+
}
|
|
234
|
+
const outsideRoot = postPathOutsideRoot(postPath.postPath, surfdashRoot);
|
|
235
|
+
if (outsideRoot) return fail(outsideRoot);
|
|
236
|
+
if (!existsSync(postPath.postPath)) {
|
|
237
|
+
return fail(`options.postPath does not exist: ${postPath.postPath}`);
|
|
238
|
+
}
|
|
239
|
+
const draftPath = deriveLocaleDraftPath(postPath.postPath, target.locale);
|
|
240
|
+
if (!draftPath) {
|
|
241
|
+
return fail(
|
|
242
|
+
`options.postPath must be a zh post path containing a "${sep}zh${sep}" segment: ${postPath.postPath}`,
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
try {
|
|
247
|
+
const spawned = await spawnCli(
|
|
248
|
+
['bun', 'run', 'sd', 'translate', postPath.postPath, '--source', 'zh', '--target', target.locale],
|
|
249
|
+
surfdashRoot,
|
|
250
|
+
);
|
|
251
|
+
if (spawned.exitCode !== 0) {
|
|
252
|
+
return fail(`sd translate failed (exit ${spawned.exitCode}): ${cliErrorText(spawned)}`);
|
|
253
|
+
}
|
|
254
|
+
if (!existsSync(draftPath)) {
|
|
255
|
+
return fail(`sd translate reported success but the draft is missing: ${draftPath}`);
|
|
256
|
+
}
|
|
257
|
+
const assetsDir = join(dirname(postPath.postPath), 'assets');
|
|
258
|
+
if (existsSync(assetsDir)) {
|
|
259
|
+
await cp(assetsDir, join(dirname(draftPath), 'assets'), { recursive: true });
|
|
260
|
+
}
|
|
261
|
+
return ResultSchema.parse({
|
|
262
|
+
ok: true,
|
|
263
|
+
target: TARGET,
|
|
264
|
+
message: `Scaffolded ${target.locale} draft at ${draftPath}`,
|
|
265
|
+
metadata: { draftPath, postPath: postPath.postPath, target: target.locale },
|
|
266
|
+
});
|
|
267
|
+
} catch (error: unknown) {
|
|
268
|
+
return fail(`scaffold-locale transport error: ${error instanceof Error ? error.message : String(error)}`);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
82
272
|
/**
|
|
83
|
-
*
|
|
273
|
+
* Runs the surfdash publish-orchestrator sync for a post (L1 R7 / 0096): spawns
|
|
274
|
+
* `bun scripts/publish-orchestrator.ts sync <postPath>` inside the surfdash checkout
|
|
275
|
+
* and returns ok/fail with stderr on failure.
|
|
276
|
+
*/
|
|
277
|
+
async function syncPost(options: SurfdashPubOptions): Promise<Result> {
|
|
278
|
+
const postPath = resolvePostPath(options);
|
|
279
|
+
if ('error' in postPath) return fail(postPath.error);
|
|
280
|
+
|
|
281
|
+
const surfdashRoot = resolveSurfdashRoot();
|
|
282
|
+
if (!surfdashRoot) {
|
|
283
|
+
return fail('SURFDASH_ROOT is required when POSTSURFING_BIN is not a path inside the surfdash checkout');
|
|
284
|
+
}
|
|
285
|
+
const outsideRoot = postPathOutsideRoot(postPath.postPath, surfdashRoot);
|
|
286
|
+
if (outsideRoot) return fail(outsideRoot);
|
|
287
|
+
|
|
288
|
+
try {
|
|
289
|
+
const spawned = await spawnCli(
|
|
290
|
+
['bun', 'scripts/publish-orchestrator.ts', 'sync', postPath.postPath],
|
|
291
|
+
surfdashRoot,
|
|
292
|
+
);
|
|
293
|
+
if (spawned.exitCode !== 0) {
|
|
294
|
+
return fail(`publish-orchestrator sync failed (exit ${spawned.exitCode}): ${cliErrorText(spawned)}`);
|
|
295
|
+
}
|
|
296
|
+
return ResultSchema.parse({
|
|
297
|
+
ok: true,
|
|
298
|
+
target: TARGET,
|
|
299
|
+
message: 'publish-orchestrator sync completed',
|
|
300
|
+
metadata: { postPath: postPath.postPath },
|
|
301
|
+
});
|
|
302
|
+
} catch (error: unknown) {
|
|
303
|
+
return fail(`sync transport error: ${error instanceof Error ? error.message : String(error)}`);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Publish path (behavior unchanged since E3): maps Content into surfdash markdown under
|
|
309
|
+
* `content/posts/<collection>/<locale>/<slug>/index.md` and invokes the postsurfing CLI.
|
|
310
|
+
*/
|
|
311
|
+
async function publishPost(payload: PublishTransportPayload): Promise<Result> {
|
|
312
|
+
const postsurfingBin = process.env.POSTSURFING_BIN ?? 'postsurfing';
|
|
313
|
+
|
|
314
|
+
// The publishing FSM requires the markdown at
|
|
315
|
+
// content/posts/<collection>/<locale>/<slug>/index.md inside the surfdash site tree
|
|
316
|
+
// (its refine step rejects other layouts); the root resolution lives in resolveSurfdashRoot().
|
|
317
|
+
const surfdashRoot = resolveSurfdashRoot();
|
|
318
|
+
if (!surfdashRoot) {
|
|
319
|
+
return fail('SURFDASH_ROOT is required when POSTSURFING_BIN is not a path inside the surfdash checkout');
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const locale = process.env.SURFDASH_LOCALE ?? 'zh';
|
|
323
|
+
// Date-stamped slug (metadata.runDate, e.g. 20260904) keeps same-title daily posts
|
|
324
|
+
// from colliding; Chinese titles slugify to '' and fall back to the bare date.
|
|
325
|
+
const runDate = typeof payload.content.metadata?.runDate === 'string' ? payload.content.metadata.runDate : '';
|
|
326
|
+
const today = new Date().toISOString().slice(0, 10).replaceAll('-', '');
|
|
327
|
+
const dateStamp = runDate || today;
|
|
328
|
+
const titleSlug = slugify(payload.content.title ?? '');
|
|
329
|
+
const slug = titleSlug ? `${dateStamp}-${titleSlug}` : `${dateStamp}-post`;
|
|
330
|
+
const postDir = join(surfdashRoot, 'content', 'posts', 'articles', locale, slug);
|
|
331
|
+
await createNodeFileSystem().ensureDir(postDir);
|
|
332
|
+
|
|
333
|
+
// Cover image: metadata.coverImage (absolute local path) is copied into the post's
|
|
334
|
+
// assets dir and referenced relatively — the surfdash PostSchema accepts ./assets/<file>.
|
|
335
|
+
let imageFrontmatter: string | undefined;
|
|
336
|
+
const coverPath = payload.content.metadata?.coverImage;
|
|
337
|
+
if (typeof coverPath === 'string' && existsSync(coverPath)) {
|
|
338
|
+
const assetsDir = join(postDir, 'assets');
|
|
339
|
+
await createNodeFileSystem().ensureDir(assetsDir);
|
|
340
|
+
const coverName = `cover${coverPath.slice(coverPath.lastIndexOf('.'))}`;
|
|
341
|
+
await copyFile(coverPath, join(assetsDir, coverName));
|
|
342
|
+
imageFrontmatter = `./assets/${coverName}`;
|
|
343
|
+
}
|
|
344
|
+
const markdown = mapContentToSurfingMarkdown(payload.content, {
|
|
345
|
+
image: imageFrontmatter,
|
|
346
|
+
publishDate: runDate ? `${runDate.slice(0, 4)}-${runDate.slice(4, 6)}-${runDate.slice(6, 8)}` : undefined,
|
|
347
|
+
});
|
|
348
|
+
const filePath = join(postDir, 'index.md');
|
|
349
|
+
|
|
350
|
+
try {
|
|
351
|
+
await createNodeFileSystem().writeFile(filePath, markdown);
|
|
352
|
+
const spawned = await spawnCli([postsurfingBin, 'publish', filePath], surfdashRoot);
|
|
353
|
+
|
|
354
|
+
if (spawned.exitCode !== 0) {
|
|
355
|
+
return fail(`postsurfing CLI failed (exit ${spawned.exitCode}): ${cliErrorText(spawned)}`);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
return ResultSchema.parse({
|
|
359
|
+
ok: true,
|
|
360
|
+
target: TARGET,
|
|
361
|
+
url: `https://surfdash.local/${locale}/posts/${slug}`,
|
|
362
|
+
message: 'Published successfully to Surfdash via postsurfing CLI',
|
|
363
|
+
// Absolute post path lets downstream steps (locale translation + CMS sync)
|
|
364
|
+
// derive the surfdash checkout root without extra configuration.
|
|
365
|
+
metadata: { postPath: filePath },
|
|
366
|
+
});
|
|
367
|
+
} catch (err: unknown) {
|
|
368
|
+
return fail(`Postsurfing transport error: ${err instanceof Error ? err.message : String(err)}`);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* FileCli transport invoking postsurfing CLI for `publish` (default) and the
|
|
374
|
+
* `scaffold-locale` / `sync` surfdash mechanics via `options.operation` (L1 R7).
|
|
84
375
|
*/
|
|
85
376
|
export class PostsurfingFileCliTransport implements PublishTransport {
|
|
86
377
|
public readonly kind = 'file-cli';
|
|
87
378
|
|
|
88
379
|
public async publish(payload: PublishTransportPayload): Promise<Result> {
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
const [exitCode, stdout, stderr] = await Promise.all([
|
|
107
|
-
proc.exited,
|
|
108
|
-
new Response(proc.stdout).text(),
|
|
109
|
-
new Response(proc.stderr).text(),
|
|
110
|
-
]);
|
|
111
|
-
|
|
112
|
-
if (exitCode !== 0) {
|
|
113
|
-
return ResultSchema.parse({
|
|
114
|
-
ok: false,
|
|
115
|
-
target: 'surfdash',
|
|
116
|
-
error: `postsurfing CLI failed (exit ${exitCode}): ${stderr.trim() || stdout.trim() || 'Unknown error'}`,
|
|
117
|
-
});
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
return ResultSchema.parse({
|
|
121
|
-
ok: true,
|
|
122
|
-
target: 'surfdash',
|
|
123
|
-
url: `https://surfdash.local/posts/${fileName}`,
|
|
124
|
-
message: 'Published successfully to Surfdash via postsurfing CLI',
|
|
125
|
-
});
|
|
126
|
-
} catch (err: unknown) {
|
|
127
|
-
return ResultSchema.parse({
|
|
128
|
-
ok: false,
|
|
129
|
-
target: 'surfdash',
|
|
130
|
-
error: `Postsurfing transport error: ${err instanceof Error ? err.message : String(err)}`,
|
|
131
|
-
});
|
|
132
|
-
} finally {
|
|
133
|
-
// Best-effort temp cleanup: a failure here must not mask the publish result.
|
|
134
|
-
try {
|
|
135
|
-
await createNodeFileSystem().deleteFile(filePath);
|
|
136
|
-
} catch {
|
|
137
|
-
// ignore
|
|
138
|
-
}
|
|
380
|
+
const options: SurfdashPubOptions =
|
|
381
|
+
payload.options !== undefined && typeof payload.options === 'object' && payload.options !== null
|
|
382
|
+
? (payload.options as SurfdashPubOptions)
|
|
383
|
+
: {};
|
|
384
|
+
|
|
385
|
+
const routed = resolveOperation(options);
|
|
386
|
+
if ('error' in routed) return fail(routed.error);
|
|
387
|
+
|
|
388
|
+
switch (routed.operation) {
|
|
389
|
+
case 'scaffold-locale':
|
|
390
|
+
return scaffoldLocale(options);
|
|
391
|
+
case 'sync':
|
|
392
|
+
return syncPost(options);
|
|
393
|
+
case 'publish':
|
|
394
|
+
return publishPost(payload);
|
|
139
395
|
}
|
|
140
396
|
}
|
|
141
397
|
}
|
|
@@ -153,6 +409,11 @@ export function getPublishTransport(): PublishTransport {
|
|
|
153
409
|
|
|
154
410
|
/**
|
|
155
411
|
* Processes input Content and writes output Result via transport.
|
|
412
|
+
*
|
|
413
|
+
* The input JSON may carry a top-level `options` object (`operation`, `postPath`,
|
|
414
|
+
* `target`) alongside the Content fields; it is forwarded to the transport so
|
|
415
|
+
* `kk executor run` callers can select scaffold-locale/sync without extra flags
|
|
416
|
+
* (podcast-pub pattern, 0096).
|
|
156
417
|
*/
|
|
157
418
|
export async function processPublishIO(
|
|
158
419
|
inputPath: string,
|
|
@@ -161,9 +422,14 @@ export async function processPublishIO(
|
|
|
161
422
|
): Promise<void> {
|
|
162
423
|
await createNodeFileSystem().deleteFile(outputPath);
|
|
163
424
|
const rawInput = await createNodeFileSystem().readFile(inputPath);
|
|
164
|
-
const
|
|
425
|
+
const raw = JSON.parse(rawInput) as Record<string, unknown>;
|
|
426
|
+
const content = ContentSchema.parse(raw);
|
|
427
|
+
const options =
|
|
428
|
+
raw.options !== undefined && typeof raw.options === 'object' && raw.options !== null
|
|
429
|
+
? (raw.options as Record<string, unknown>)
|
|
430
|
+
: undefined;
|
|
165
431
|
|
|
166
|
-
const result = await transport.publish({ content });
|
|
432
|
+
const result = await transport.publish({ content, options });
|
|
167
433
|
const validatedResult = ResultSchema.parse(result);
|
|
168
434
|
|
|
169
435
|
const outDir = dirname(outputPath);
|