shennian 0.2.89 → 0.2.90
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/assets/wechat-channel/macos/manifest.json +13 -4
- package/dist/assets/wechat-channel/macos/shennian-wechat-channel-helper +0 -0
- package/dist/bin/shennian.js +1 -1
- package/dist/publish-build-manifest.json +548 -0
- package/dist/scripts/wechat-rpa-confirmation.mjs +5 -97
- package/dist/src/agent-env.js +4 -105
- package/dist/src/agents/adapter.js +1 -19
- package/dist/src/agents/claude.js +8 -305
- package/dist/src/agents/codex-control.js +2 -188
- package/dist/src/agents/codex-utils.js +7 -200
- package/dist/src/agents/codex.js +15 -916
- package/dist/src/agents/command-spec.js +2 -413
- package/dist/src/agents/config-status.js +1 -226
- package/dist/src/agents/cursor.js +1 -249
- package/dist/src/agents/custom.js +4 -271
- package/dist/src/agents/detect.js +1 -56
- package/dist/src/agents/external-channel-instructions.js +10 -94
- package/dist/src/agents/gemini.js +1 -173
- package/dist/src/agents/manager.js +13 -157
- package/dist/src/agents/model-registry/cache.js +1 -37
- package/dist/src/agents/model-registry/discovery.js +2 -187
- package/dist/src/agents/model-registry/parsers.js +4 -447
- package/dist/src/agents/model-registry/runner.js +1 -30
- package/dist/src/agents/model-registry/service.js +1 -78
- package/dist/src/agents/model-registry/types.js +1 -8
- package/dist/src/agents/model-registry.js +1 -18
- package/dist/src/agents/openclaw.js +2 -275
- package/dist/src/agents/opencode.js +1 -231
- package/dist/src/agents/pi-context.js +12 -217
- package/dist/src/agents/pi.js +14 -723
- package/dist/src/agents/platform-instructions.js +9 -54
- package/dist/src/channels/base.js +1 -3
- package/dist/src/channels/registry.js +1 -30
- package/dist/src/channels/reply-split.js +10 -89
- package/dist/src/channels/runtime.js +5 -564
- package/dist/src/channels/secret-registry.js +1 -46
- package/dist/src/channels/websocket.js +8 -378
- package/dist/src/channels/wechat-channel/anchor.js +1 -65
- package/dist/src/channels/wechat-channel/client.js +1 -96
- package/dist/src/channels/wechat-channel/cooldown.js +1 -38
- package/dist/src/channels/wechat-channel/fingerprint.js +1 -71
- package/dist/src/channels/wechat-channel/helper-assets.d.ts +10 -1
- package/dist/src/channels/wechat-channel/helper-assets.js +1 -68
- package/dist/src/channels/wechat-channel/helper-client.js +3 -149
- package/dist/src/channels/wechat-channel/helper-protocol.d.ts +1 -1
- package/dist/src/channels/wechat-channel/helper-protocol.js +1 -115
- package/dist/src/channels/wechat-channel/index.d.ts +1 -0
- package/dist/src/channels/wechat-channel/index.js +1 -19
- package/dist/src/channels/wechat-channel/ledger.js +1 -54
- package/dist/src/channels/wechat-channel/media-resolver.js +1 -181
- package/dist/src/channels/wechat-channel/message-key.js +1 -105
- package/dist/src/channels/wechat-channel/observer.js +1 -118
- package/dist/src/channels/wechat-channel/outbound-ledger.d.ts +3 -0
- package/dist/src/channels/wechat-channel/outbound-ledger.js +2 -112
- package/dist/src/channels/wechat-channel/outbound-sender.d.ts +26 -0
- package/dist/src/channels/wechat-channel/outbound-sender.js +1 -0
- package/dist/src/channels/wechat-channel/preflight.js +1 -48
- package/dist/src/channels/wechat-channel/runner.js +1 -84
- package/dist/src/channels/wechat-channel/runtime.js +1 -66
- package/dist/src/channels/wechat-channel/scheduler.d.ts +5 -0
- package/dist/src/channels/wechat-channel/scheduler.js +1 -152
- package/dist/src/channels/wechat-rpa/macos-flow.js +1 -96
- package/dist/src/channels/wechat-rpa/macos.js +6 -48
- package/dist/src/channels/wechat-rpa/normalizer.js +7 -127
- package/dist/src/channels/wechat-rpa.js +6 -1028
- package/dist/src/channels/wecom.js +4 -357
- package/dist/src/commands/agent.js +6 -131
- package/dist/src/commands/daemon-windows.js +8 -48
- package/dist/src/commands/daemon.js +19 -1013
- package/dist/src/commands/external-attachments.js +1 -51
- package/dist/src/commands/external.js +1 -137
- package/dist/src/commands/manager.js +2 -391
- package/dist/src/commands/pair-qr.js +1 -6
- package/dist/src/commands/pair.js +9 -287
- package/dist/src/commands/tools.js +1 -34
- package/dist/src/commands/upgrade.js +1 -198
- package/dist/src/config/index.js +1 -35
- package/dist/src/daemon-log.js +6 -58
- package/dist/src/env-path.js +1 -64
- package/dist/src/fs/boundary.js +1 -126
- package/dist/src/fs/handler.js +1 -130
- package/dist/src/fs/security.js +1 -32
- package/dist/src/fs/text-decoder.js +1 -110
- package/dist/src/index.js +2 -404
- package/dist/src/log-reporter.js +1 -16
- package/dist/src/manager/prompt.js +29 -34
- package/dist/src/manager/registry.js +2 -269
- package/dist/src/manager/runtime.js +19 -1007
- package/dist/src/native-fusion/config.js +1 -5
- package/dist/src/native-fusion/opencode-parser.js +3 -123
- package/dist/src/native-fusion/parser-common.js +8 -264
- package/dist/src/native-fusion/parsers.js +8 -729
- package/dist/src/native-fusion/service.js +2 -225
- package/dist/src/native-fusion/state.js +1 -22
- package/dist/src/native-fusion/types.js +1 -1
- package/dist/src/region.js +1 -88
- package/dist/src/relay/client.js +1 -343
- package/dist/src/session/archive-zip.js +1 -220
- package/dist/src/session/handlers/agent-config.js +1 -150
- package/dist/src/session/handlers/agents.js +1 -55
- package/dist/src/session/handlers/chat.js +2 -751
- package/dist/src/session/handlers/control.js +1 -55
- package/dist/src/session/handlers/fs.js +1 -783
- package/dist/src/session/handlers/session-refresh.js +1 -47
- package/dist/src/session/handlers/skills.js +1 -121
- package/dist/src/session/handlers/title.js +1 -60
- package/dist/src/session/handlers/tool-detail.js +1 -218
- package/dist/src/session/manager.js +1 -319
- package/dist/src/session/projection.js +1 -54
- package/dist/src/session/queue.js +4 -317
- package/dist/src/session/remote-attachments.js +1 -72
- package/dist/src/session/store.js +3 -109
- package/dist/src/session/types.js +1 -4
- package/dist/src/skills/registry.js +15 -148
- package/dist/src/skills/setup.js +1 -101
- package/dist/src/tools/markdown-to-pdf.js +10 -346
- package/dist/src/upgrade/engine.js +3 -347
- package/package.json +3 -2
|
@@ -1,148 +1,15 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
if (!normalized || normalized.startsWith('/') || normalized.includes('\0')) {
|
|
17
|
-
throw new Error(`Unsafe bundle path: ${relativePath}`);
|
|
18
|
-
}
|
|
19
|
-
const parts = normalized.split('/');
|
|
20
|
-
if (parts.some((part) => !part || part === '.' || part === '..')) {
|
|
21
|
-
throw new Error(`Unsafe bundle path: ${relativePath}`);
|
|
22
|
-
}
|
|
23
|
-
return normalized;
|
|
24
|
-
}
|
|
25
|
-
function readJsonFile(filePath) {
|
|
26
|
-
try {
|
|
27
|
-
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
28
|
-
}
|
|
29
|
-
catch {
|
|
30
|
-
return null;
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
export function getSkillsDir() {
|
|
34
|
-
const skillsDir = resolveShennianPath('skills');
|
|
35
|
-
fs.mkdirSync(skillsDir, { recursive: true });
|
|
36
|
-
return skillsDir;
|
|
37
|
-
}
|
|
38
|
-
export function getSkillDir(skillId) {
|
|
39
|
-
return path.join(getSkillsDir(), safeSkillId(skillId));
|
|
40
|
-
}
|
|
41
|
-
export function listInstalledSkills() {
|
|
42
|
-
const dir = getSkillsDir();
|
|
43
|
-
return fs
|
|
44
|
-
.readdirSync(dir, { withFileTypes: true })
|
|
45
|
-
.filter((entry) => entry.isDirectory())
|
|
46
|
-
.map((entry) => {
|
|
47
|
-
const skillPath = path.join(dir, entry.name);
|
|
48
|
-
const manifest = readJsonFile(path.join(skillPath, 'skill.json'));
|
|
49
|
-
const install = readJsonFile(path.join(skillPath, '.shennian-install.json'));
|
|
50
|
-
if (!manifest?.id || !manifest.name)
|
|
51
|
-
return null;
|
|
52
|
-
const item = {
|
|
53
|
-
...manifest,
|
|
54
|
-
installedAt: install?.installedAt ?? new Date(0).toISOString(),
|
|
55
|
-
path: skillPath,
|
|
56
|
-
};
|
|
57
|
-
if (install?.setupStatus)
|
|
58
|
-
item.setupStatus = install.setupStatus;
|
|
59
|
-
if (install?.doctorResults)
|
|
60
|
-
item.doctorResults = install.doctorResults;
|
|
61
|
-
return item;
|
|
62
|
-
})
|
|
63
|
-
.filter((item) => item != null)
|
|
64
|
-
.sort((left, right) => left.name.localeCompare(right.name));
|
|
65
|
-
}
|
|
66
|
-
export function getInstalledSkill(skillId) {
|
|
67
|
-
return listInstalledSkills().find((skill) => skill.id === skillId) ?? null;
|
|
68
|
-
}
|
|
69
|
-
export function updateInstalledSkillSetup(skillId, update) {
|
|
70
|
-
const skillDir = getSkillDir(skillId);
|
|
71
|
-
const installPath = path.join(skillDir, '.shennian-install.json');
|
|
72
|
-
const current = readJsonFile(installPath) ?? {};
|
|
73
|
-
fs.writeFileSync(installPath, JSON.stringify({ ...current, ...update, updatedAt: new Date().toISOString() }, null, 2) + '\n', 'utf8');
|
|
74
|
-
}
|
|
75
|
-
async function fetchSkillBundle(installUrl) {
|
|
76
|
-
if (!installUrl.startsWith('http://') && !installUrl.startsWith('https://')) {
|
|
77
|
-
return (readJsonFile(path.resolve(installUrl)) ??
|
|
78
|
-
(() => {
|
|
79
|
-
throw new Error('Invalid skill bundle');
|
|
80
|
-
})());
|
|
81
|
-
}
|
|
82
|
-
const res = await fetch(installUrl);
|
|
83
|
-
if (!res.ok)
|
|
84
|
-
throw new Error(`Skill download failed: ${res.status} ${res.statusText}`);
|
|
85
|
-
return (await res.json());
|
|
86
|
-
}
|
|
87
|
-
function writeBundleFile(skillDir, file) {
|
|
88
|
-
const relativePath = assertSafeBundlePath(file.path);
|
|
89
|
-
const target = path.join(skillDir, relativePath);
|
|
90
|
-
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
91
|
-
fs.writeFileSync(target, file.content, 'utf8');
|
|
92
|
-
if (file.executable && process.platform !== 'win32') {
|
|
93
|
-
fs.chmodSync(target, 0o755);
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
export async function installSkillFromUrl(installUrl) {
|
|
97
|
-
const bundle = await fetchSkillBundle(installUrl);
|
|
98
|
-
if (!bundle?.manifest?.id || !Array.isArray(bundle.files)) {
|
|
99
|
-
throw new Error('Invalid skill bundle');
|
|
100
|
-
}
|
|
101
|
-
const skillDir = getSkillDir(bundle.manifest.id);
|
|
102
|
-
fs.rmSync(skillDir, { recursive: true, force: true });
|
|
103
|
-
fs.mkdirSync(skillDir, { recursive: true });
|
|
104
|
-
fs.writeFileSync(path.join(skillDir, 'skill.json'), JSON.stringify(bundle.manifest, null, 2) + '\n', 'utf8');
|
|
105
|
-
for (const file of bundle.files) {
|
|
106
|
-
writeBundleFile(skillDir, file);
|
|
107
|
-
}
|
|
108
|
-
const doctorResults = bundle.manifest.setup?.doctors?.length
|
|
109
|
-
? (await Promise.all(bundle.manifest.setup.doctors.map((doctorId) => runSkillDoctor(bundle.manifest.id, doctorId)))).flat()
|
|
110
|
-
: [];
|
|
111
|
-
const setupStatus = summarizeSetupStatus(doctorResults);
|
|
112
|
-
fs.writeFileSync(path.join(skillDir, '.shennian-install.json'), JSON.stringify({ installedAt: new Date().toISOString(), installUrl, setupStatus, doctorResults }, null, 2) + '\n', 'utf8');
|
|
113
|
-
const installed = getInstalledSkill(bundle.manifest.id);
|
|
114
|
-
if (!installed)
|
|
115
|
-
throw new Error('Skill installation did not complete');
|
|
116
|
-
return installed;
|
|
117
|
-
}
|
|
118
|
-
export function buildSkillUsePrompt(skillId, workDir, attachments) {
|
|
119
|
-
const installed = getInstalledSkill(skillId);
|
|
120
|
-
if (!installed)
|
|
121
|
-
throw new Error(`Skill not installed: ${skillId}`);
|
|
122
|
-
const attachmentLines = (attachments ?? [])
|
|
123
|
-
.map((attachment) => `- ${attachment.name} (${attachment.mimeType}): ${attachment.path}`)
|
|
124
|
-
.join('\n');
|
|
125
|
-
return [
|
|
126
|
-
`Use the Shennian skill "${installed.name}".`,
|
|
127
|
-
'',
|
|
128
|
-
`Skill folder: ${installed.path}`,
|
|
129
|
-
`Before using it, read: ${path.join(installed.path, 'SKILL.md')}`,
|
|
130
|
-
`Working directory: ${workDir}`,
|
|
131
|
-
attachmentLines ? `\nCurrent attachments:\n${attachmentLines}` : '',
|
|
132
|
-
'',
|
|
133
|
-
'Follow the skill output contract. Prefer local deterministic extraction first, then use agent document or vision capability only when needed.',
|
|
134
|
-
]
|
|
135
|
-
.filter(Boolean)
|
|
136
|
-
.join('\n');
|
|
137
|
-
}
|
|
138
|
-
export function buildInstalledSkillInstructions() {
|
|
139
|
-
const skills = listInstalledSkills();
|
|
140
|
-
if (skills.length === 0)
|
|
141
|
-
return '';
|
|
142
|
-
const lines = skills.map((skill) => [
|
|
143
|
-
`- ${skill.id}: ${skill.description}`,
|
|
144
|
-
` Skill folder: ${skill.path}`,
|
|
145
|
-
` Read ${path.join(skill.path, 'SKILL.md')} before using this skill.`,
|
|
146
|
-
].join('\n'));
|
|
147
|
-
return `## Shennian Skills\n\nInstalled skills are file-based capabilities managed by Shennian under ${getSkillsDir()}.\nWhen a user asks to use one, read its SKILL.md and follow its output contract.\n\n${lines.join('\n')}`;
|
|
148
|
-
}
|
|
1
|
+
import o from"node:fs";import s from"node:path";import{resolveShennianPath as p}from"../config/index.js";import{runSkillDoctor as S,summarizeSetupStatus as h}from"./setup.js";function k(n){const t=n.trim();if(!/^[a-z0-9][a-z0-9._-]{1,80}$/i.test(t))throw new Error("Invalid skill id");return t}function w(n){const t=n.replace(/\\/g,"/");if(!t||t.startsWith("/")||t.includes("\0"))throw new Error(`Unsafe bundle path: ${n}`);if(t.split("/").some(i=>!i||i==="."||i===".."))throw new Error(`Unsafe bundle path: ${n}`);return t}function a(n){try{return JSON.parse(o.readFileSync(n,"utf8"))}catch{return null}}function c(){const n=p("skills");return o.mkdirSync(n,{recursive:!0}),n}function d(n){return s.join(c(),k(n))}function f(){const n=c();return o.readdirSync(n,{withFileTypes:!0}).filter(t=>t.isDirectory()).map(t=>{const e=s.join(n,t.name),i=a(s.join(e,"skill.json")),r=a(s.join(e,".shennian-install.json"));if(!i?.id||!i.name)return null;const l={...i,installedAt:r?.installedAt??new Date(0).toISOString(),path:e};return r?.setupStatus&&(l.setupStatus=r.setupStatus),r?.doctorResults&&(l.doctorResults=r.doctorResults),l}).filter(t=>t!=null).sort((t,e)=>t.name.localeCompare(e.name))}function m(n){return f().find(t=>t.id===n)??null}function x(n,t){const e=d(n),i=s.join(e,".shennian-install.json"),r=a(i)??{};o.writeFileSync(i,JSON.stringify({...r,...t,updatedAt:new Date().toISOString()},null,2)+`
|
|
2
|
+
`,"utf8")}async function y(n){if(!n.startsWith("http://")&&!n.startsWith("https://"))return a(s.resolve(n))??(()=>{throw new Error("Invalid skill bundle")})();const t=await fetch(n);if(!t.ok)throw new Error(`Skill download failed: ${t.status} ${t.statusText}`);return await t.json()}function j(n,t){const e=w(t.path),i=s.join(n,e);o.mkdirSync(s.dirname(i),{recursive:!0}),o.writeFileSync(i,t.content,"utf8"),t.executable&&process.platform!=="win32"&&o.chmodSync(i,493)}async function D(n){const t=await y(n);if(!t?.manifest?.id||!Array.isArray(t.files))throw new Error("Invalid skill bundle");const e=d(t.manifest.id);o.rmSync(e,{recursive:!0,force:!0}),o.mkdirSync(e,{recursive:!0}),o.writeFileSync(s.join(e,"skill.json"),JSON.stringify(t.manifest,null,2)+`
|
|
3
|
+
`,"utf8");for(const u of t.files)j(e,u);const i=t.manifest.setup?.doctors?.length?(await Promise.all(t.manifest.setup.doctors.map(u=>S(t.manifest.id,u)))).flat():[],r=h(i);o.writeFileSync(s.join(e,".shennian-install.json"),JSON.stringify({installedAt:new Date().toISOString(),installUrl:n,setupStatus:r,doctorResults:i},null,2)+`
|
|
4
|
+
`,"utf8");const l=m(t.manifest.id);if(!l)throw new Error("Skill installation did not complete");return l}function F(n,t,e){const i=m(n);if(!i)throw new Error(`Skill not installed: ${n}`);const r=(e??[]).map(l=>`- ${l.name} (${l.mimeType}): ${l.path}`).join(`
|
|
5
|
+
`);return[`Use the Shennian skill "${i.name}".`,"",`Skill folder: ${i.path}`,`Before using it, read: ${s.join(i.path,"SKILL.md")}`,`Working directory: ${t}`,r?`
|
|
6
|
+
Current attachments:
|
|
7
|
+
${r}`:"","","Follow the skill output contract. Prefer local deterministic extraction first, then use agent document or vision capability only when needed."].filter(Boolean).join(`
|
|
8
|
+
`)}function v(){const n=f();if(n.length===0)return"";const t=n.map(e=>[`- ${e.id}: ${e.description}`,` Skill folder: ${e.path}`,` Read ${s.join(e.path,"SKILL.md")} before using this skill.`].join(`
|
|
9
|
+
`));return`## Shennian Skills
|
|
10
|
+
|
|
11
|
+
Installed skills are file-based capabilities managed by Shennian under ${c()}.
|
|
12
|
+
When a user asks to use one, read its SKILL.md and follow its output contract.
|
|
13
|
+
|
|
14
|
+
${t.join(`
|
|
15
|
+
`)}`}export{v as buildInstalledSkillInstructions,F as buildSkillUsePrompt,m as getInstalledSkill,d as getSkillDir,c as getSkillsDir,D as installSkillFromUrl,f as listInstalledSkills,x as updateInstalledSkillSetup};
|
package/dist/src/skills/setup.js
CHANGED
|
@@ -1,101 +1 @@
|
|
|
1
|
-
|
|
2
|
-
// @test src/__tests__/skill-registry.test.ts
|
|
3
|
-
import { findChromiumExecutable, installManagedChromium, MARKDOWN_PDF_BROWSER_DOCTOR_ID, MARKDOWN_PDF_MANAGED_CHROMIUM_ACTION_ID, MARKDOWN_PDF_SKILL_ID, managedChromiumBrowsersPath, } from '../tools/markdown-to-pdf.js';
|
|
4
|
-
export const MARKDOWN_PDF_REPAIR_ACTION = {
|
|
5
|
-
id: MARKDOWN_PDF_MANAGED_CHROMIUM_ACTION_ID,
|
|
6
|
-
label: 'Install PDF export component',
|
|
7
|
-
description: 'Download a Shennian-managed Chromium browser into ~/.shennian/browsers without using sudo.',
|
|
8
|
-
requiresNetwork: true,
|
|
9
|
-
requiresSudo: false,
|
|
10
|
-
downloadSizeMb: 180,
|
|
11
|
-
};
|
|
12
|
-
export function markdownPdfRepairActions() {
|
|
13
|
-
return [MARKDOWN_PDF_REPAIR_ACTION];
|
|
14
|
-
}
|
|
15
|
-
function result(input) {
|
|
16
|
-
return {
|
|
17
|
-
skillId: MARKDOWN_PDF_SKILL_ID,
|
|
18
|
-
doctorId: MARKDOWN_PDF_BROWSER_DOCTOR_ID,
|
|
19
|
-
status: input.status,
|
|
20
|
-
issues: input.issues,
|
|
21
|
-
checkedAt: input.checkedAt ?? new Date().toISOString(),
|
|
22
|
-
recommendedRepairActionId: MARKDOWN_PDF_MANAGED_CHROMIUM_ACTION_ID,
|
|
23
|
-
repairActions: markdownPdfRepairActions(),
|
|
24
|
-
};
|
|
25
|
-
}
|
|
26
|
-
export async function runMarkdownPdfBrowserDoctor() {
|
|
27
|
-
try {
|
|
28
|
-
const browserPath = await findChromiumExecutable();
|
|
29
|
-
return result({
|
|
30
|
-
status: 'ready',
|
|
31
|
-
issues: [
|
|
32
|
-
{
|
|
33
|
-
code: 'MARKDOWN_PDF_BROWSER_READY',
|
|
34
|
-
severity: 'info',
|
|
35
|
-
message: `PDF export browser is ready: ${browserPath}`,
|
|
36
|
-
},
|
|
37
|
-
],
|
|
38
|
-
});
|
|
39
|
-
}
|
|
40
|
-
catch {
|
|
41
|
-
return result({
|
|
42
|
-
status: 'needs_setup',
|
|
43
|
-
issues: [
|
|
44
|
-
{
|
|
45
|
-
code: 'MARKDOWN_PDF_BROWSER_MISSING',
|
|
46
|
-
severity: 'error',
|
|
47
|
-
message: 'This machine needs a headless browser before Markdown files can be exported to PDF.',
|
|
48
|
-
repairActionId: MARKDOWN_PDF_MANAGED_CHROMIUM_ACTION_ID,
|
|
49
|
-
},
|
|
50
|
-
],
|
|
51
|
-
});
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
export async function runSkillDoctor(skillId, doctorId) {
|
|
55
|
-
if (skillId === MARKDOWN_PDF_SKILL_ID &&
|
|
56
|
-
(!doctorId || doctorId === MARKDOWN_PDF_BROWSER_DOCTOR_ID)) {
|
|
57
|
-
return [await runMarkdownPdfBrowserDoctor()];
|
|
58
|
-
}
|
|
59
|
-
return [
|
|
60
|
-
{
|
|
61
|
-
skillId,
|
|
62
|
-
doctorId: doctorId || 'unknown',
|
|
63
|
-
status: 'ready',
|
|
64
|
-
issues: [
|
|
65
|
-
{
|
|
66
|
-
code: 'SKILL_DOCTOR_NOT_REQUIRED',
|
|
67
|
-
severity: 'info',
|
|
68
|
-
message: 'This skill does not declare a local environment doctor.',
|
|
69
|
-
},
|
|
70
|
-
],
|
|
71
|
-
checkedAt: new Date().toISOString(),
|
|
72
|
-
},
|
|
73
|
-
];
|
|
74
|
-
}
|
|
75
|
-
export async function runSkillSetup(skillId, repairActionId) {
|
|
76
|
-
if (skillId !== MARKDOWN_PDF_SKILL_ID ||
|
|
77
|
-
repairActionId !== MARKDOWN_PDF_MANAGED_CHROMIUM_ACTION_ID) {
|
|
78
|
-
throw new Error('Unsupported skill repair action');
|
|
79
|
-
}
|
|
80
|
-
await installManagedChromium();
|
|
81
|
-
const doctorResults = await runSkillDoctor(skillId, MARKDOWN_PDF_BROWSER_DOCTOR_ID);
|
|
82
|
-
const status = doctorResults.every((item) => item.status === 'ready') ? 'ready' : 'setup_failed';
|
|
83
|
-
return {
|
|
84
|
-
status,
|
|
85
|
-
doctorResults,
|
|
86
|
-
message: status === 'ready'
|
|
87
|
-
? `PDF export component installed in ${managedChromiumBrowsersPath()}`
|
|
88
|
-
: 'PDF export component was installed, but the browser is still not ready. The system may need additional Linux libraries.',
|
|
89
|
-
};
|
|
90
|
-
}
|
|
91
|
-
export function summarizeSetupStatus(doctorResults) {
|
|
92
|
-
if (doctorResults.length === 0)
|
|
93
|
-
return undefined;
|
|
94
|
-
if (doctorResults.every((item) => item.status === 'ready'))
|
|
95
|
-
return 'ready';
|
|
96
|
-
if (doctorResults.some((item) => item.status === 'blocked'))
|
|
97
|
-
return 'blocked';
|
|
98
|
-
if (doctorResults.some((item) => item.status === 'setup_failed'))
|
|
99
|
-
return 'setup_failed';
|
|
100
|
-
return 'needs_setup';
|
|
101
|
-
}
|
|
1
|
+
import{findChromiumExecutable as d,installManagedChromium as c,MARKDOWN_PDF_BROWSER_DOCTOR_ID as s,MARKDOWN_PDF_MANAGED_CHROMIUM_ACTION_ID as t,MARKDOWN_PDF_SKILL_ID as n,managedChromiumBrowsersPath as l}from"../tools/markdown-to-pdf.js";const m={id:t,label:"Install PDF export component",description:"Download a Shennian-managed Chromium browser into ~/.shennian/browsers without using sudo.",requiresNetwork:!0,requiresSudo:!1,downloadSizeMb:180};function D(){return[m]}function i(e){return{skillId:n,doctorId:s,status:e.status,issues:e.issues,checkedAt:e.checkedAt??new Date().toISOString(),recommendedRepairActionId:t,repairActions:D()}}async function _(){try{const e=await d();return i({status:"ready",issues:[{code:"MARKDOWN_PDF_BROWSER_READY",severity:"info",message:`PDF export browser is ready: ${e}`}]})}catch{return i({status:"needs_setup",issues:[{code:"MARKDOWN_PDF_BROWSER_MISSING",severity:"error",message:"This machine needs a headless browser before Markdown files can be exported to PDF.",repairActionId:t}]})}}async function p(e,r){return e===n&&(!r||r===s)?[await _()]:[{skillId:e,doctorId:r||"unknown",status:"ready",issues:[{code:"SKILL_DOCTOR_NOT_REQUIRED",severity:"info",message:"This skill does not declare a local environment doctor."}],checkedAt:new Date().toISOString()}]}async function w(e,r){if(e!==n||r!==t)throw new Error("Unsupported skill repair action");await c();const o=await p(e,s),a=o.every(u=>u.status==="ready")?"ready":"setup_failed";return{status:a,doctorResults:o,message:a==="ready"?`PDF export component installed in ${l()}`:"PDF export component was installed, but the browser is still not ready. The system may need additional Linux libraries."}}function h(e){if(e.length!==0)return e.every(r=>r.status==="ready")?"ready":e.some(r=>r.status==="blocked")?"blocked":e.some(r=>r.status==="setup_failed")?"setup_failed":"needs_setup"}export{m as MARKDOWN_PDF_REPAIR_ACTION,D as markdownPdfRepairActions,_ as runMarkdownPdfBrowserDoctor,p as runSkillDoctor,w as runSkillSetup,h as summarizeSetupStatus};
|
|
@@ -1,222 +1,13 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
import { pathToFileURL } from 'node:url';
|
|
8
|
-
import { promisify } from 'node:util';
|
|
9
|
-
import { resolveShennianPath } from '../config/index.js';
|
|
10
|
-
const execFileAsync = promisify(execFile);
|
|
11
|
-
export const MARKDOWN_PDF_SKILL_ID = 'markdown-to-pdf';
|
|
12
|
-
export const MARKDOWN_PDF_BROWSER_DOCTOR_ID = 'markdownPdf.browser';
|
|
13
|
-
export const MARKDOWN_PDF_MANAGED_CHROMIUM_ACTION_ID = 'markdownPdf.installManagedChromium';
|
|
14
|
-
export const MARKDOWN_PDF_SETUP_REQUIRED_CODE = 'SKILL_SETUP_REQUIRED';
|
|
15
|
-
export const PLAYWRIGHT_VERSION_FOR_MANAGED_CHROMIUM = '1.59.1';
|
|
16
|
-
export function markdownPdfSetupRequiredPayload() {
|
|
17
|
-
return {
|
|
18
|
-
code: MARKDOWN_PDF_SETUP_REQUIRED_CODE,
|
|
19
|
-
skillId: MARKDOWN_PDF_SKILL_ID,
|
|
20
|
-
doctorId: MARKDOWN_PDF_BROWSER_DOCTOR_ID,
|
|
21
|
-
recommendedRepairActionId: MARKDOWN_PDF_MANAGED_CHROMIUM_ACTION_ID,
|
|
22
|
-
};
|
|
23
|
-
}
|
|
24
|
-
export class MarkdownPdfBrowserMissingError extends Error {
|
|
25
|
-
code = MARKDOWN_PDF_SETUP_REQUIRED_CODE;
|
|
26
|
-
setup = markdownPdfSetupRequiredPayload();
|
|
27
|
-
constructor(message = 'PDF export needs a local headless browser. Install the PDF export component, then retry.') {
|
|
28
|
-
super(message);
|
|
29
|
-
this.name = 'MarkdownPdfBrowserMissingError';
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
const FENCED_CODE_RE = /^```(\S*)\s*$/;
|
|
33
|
-
function isWindowsAbsolutePath(value) {
|
|
34
|
-
return /^[A-Za-z]:([\\/]|$)/.test(value) || /^\\\\[^\\]+\\[^\\]+/.test(value);
|
|
35
|
-
}
|
|
36
|
-
function pathApiForPath(value) {
|
|
37
|
-
return isWindowsAbsolutePath(value) ? path.win32 : path;
|
|
38
|
-
}
|
|
39
|
-
export function defaultPdfOutputPath(inputPath) {
|
|
40
|
-
const api = pathApiForPath(inputPath);
|
|
41
|
-
const parsed = api.parse(inputPath);
|
|
42
|
-
return api.join(parsed.dir, `${parsed.name}.pdf`);
|
|
43
|
-
}
|
|
44
|
-
function escapeHtml(value) {
|
|
45
|
-
return value
|
|
46
|
-
.replace(/&/g, '&')
|
|
47
|
-
.replace(/</g, '<')
|
|
48
|
-
.replace(/>/g, '>')
|
|
49
|
-
.replace(/"/g, '"')
|
|
50
|
-
.replace(/'/g, ''');
|
|
51
|
-
}
|
|
52
|
-
function slugify(value) {
|
|
53
|
-
return value
|
|
54
|
-
.toLowerCase()
|
|
55
|
-
.replace(/<[^>]+>/g, '')
|
|
56
|
-
.replace(/[^\p{L}\p{N}]+/gu, '-')
|
|
57
|
-
.replace(/^-+|-+$/g, '');
|
|
58
|
-
}
|
|
59
|
-
function renderInline(markdown, sourceDir) {
|
|
60
|
-
let text = escapeHtml(markdown);
|
|
61
|
-
text = text.replace(/`([^`]+)`/g, '<code>$1</code>');
|
|
62
|
-
text = text.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>');
|
|
63
|
-
text = text.replace(/__([^_]+)__/g, '<strong>$1</strong>');
|
|
64
|
-
text = text.replace(/\*([^*]+)\*/g, '<em>$1</em>');
|
|
65
|
-
text = text.replace(/_([^_]+)_/g, '<em>$1</em>');
|
|
66
|
-
text = text.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, (_match, alt, rawUrl) => {
|
|
67
|
-
const url = rawUrl.trim().replace(/^<|>$/g, '');
|
|
68
|
-
const src = toSafeAssetUrl(url, sourceDir);
|
|
69
|
-
return `<img src="${escapeHtml(src)}" alt="${alt}" />`;
|
|
70
|
-
});
|
|
71
|
-
text = text.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_match, label, rawUrl) => {
|
|
72
|
-
const url = rawUrl.trim().replace(/^<|>$/g, '');
|
|
73
|
-
return `<a href="${escapeHtml(url)}">${label}</a>`;
|
|
74
|
-
});
|
|
75
|
-
return text;
|
|
76
|
-
}
|
|
77
|
-
function toSafeAssetUrl(rawUrl, sourceDir) {
|
|
78
|
-
if (/^(https?:|data:|file:)/i.test(rawUrl))
|
|
79
|
-
return rawUrl;
|
|
80
|
-
const withoutAnchor = rawUrl.split('#')[0].split('?')[0];
|
|
81
|
-
const resolved = path.resolve(sourceDir, decodeURIComponent(withoutAnchor));
|
|
82
|
-
const relative = path.relative(sourceDir, resolved);
|
|
83
|
-
if (relative.startsWith('..') || path.isAbsolute(relative))
|
|
84
|
-
return '';
|
|
85
|
-
return pathToFileURL(resolved).href;
|
|
86
|
-
}
|
|
87
|
-
function renderTable(lines, sourceDir) {
|
|
88
|
-
if (lines.length < 2)
|
|
89
|
-
return null;
|
|
90
|
-
const separator = lines[1].trim();
|
|
91
|
-
if (!/^\|?\s*:?-{3,}:?\s*(\|\s*:?-{3,}:?\s*)+\|?$/.test(separator))
|
|
92
|
-
return null;
|
|
93
|
-
const parseRow = (line) => line
|
|
94
|
-
.trim()
|
|
95
|
-
.replace(/^\||\|$/g, '')
|
|
96
|
-
.split('|')
|
|
97
|
-
.map((cell) => cell.trim());
|
|
98
|
-
const header = parseRow(lines[0]);
|
|
99
|
-
const rows = lines.slice(2).map(parseRow);
|
|
100
|
-
return [
|
|
101
|
-
'<table>',
|
|
102
|
-
'<thead><tr>',
|
|
103
|
-
...header.map((cell) => `<th>${renderInline(cell, sourceDir)}</th>`),
|
|
104
|
-
'</tr></thead>',
|
|
105
|
-
'<tbody>',
|
|
106
|
-
...rows.map((row) => `<tr>${row.map((cell) => `<td>${renderInline(cell, sourceDir)}</td>`).join('')}</tr>`),
|
|
107
|
-
'</tbody></table>',
|
|
108
|
-
].join('');
|
|
109
|
-
}
|
|
110
|
-
export function renderMarkdownBody(markdown, sourceDir = process.cwd()) {
|
|
111
|
-
const lines = markdown.replace(/\r\n?/g, '\n').split('\n');
|
|
112
|
-
const out = [];
|
|
113
|
-
let paragraph = [];
|
|
114
|
-
let list = null;
|
|
115
|
-
let code = null;
|
|
116
|
-
const flushParagraph = () => {
|
|
117
|
-
if (paragraph.length === 0)
|
|
118
|
-
return;
|
|
119
|
-
out.push(`<p>${renderInline(paragraph.join(' '), sourceDir)}</p>`);
|
|
120
|
-
paragraph = [];
|
|
121
|
-
};
|
|
122
|
-
const flushList = () => {
|
|
123
|
-
if (!list)
|
|
124
|
-
return;
|
|
125
|
-
const tag = list.ordered ? 'ol' : 'ul';
|
|
126
|
-
out.push(`<${tag}>${list.items.map((item) => `<li>${renderInline(item, sourceDir)}</li>`).join('')}</${tag}>`);
|
|
127
|
-
list = null;
|
|
128
|
-
};
|
|
129
|
-
for (let i = 0; i < lines.length; i += 1) {
|
|
130
|
-
const line = lines[i];
|
|
131
|
-
const fence = line.match(FENCED_CODE_RE);
|
|
132
|
-
if (code) {
|
|
133
|
-
if (fence) {
|
|
134
|
-
out.push(`<pre><code${code.lang ? ` class="language-${escapeHtml(code.lang)}"` : ''}>${escapeHtml(code.lines.join('\n'))}</code></pre>`);
|
|
135
|
-
code = null;
|
|
136
|
-
}
|
|
137
|
-
else {
|
|
138
|
-
code.lines.push(line);
|
|
139
|
-
}
|
|
140
|
-
continue;
|
|
141
|
-
}
|
|
142
|
-
if (fence) {
|
|
143
|
-
flushParagraph();
|
|
144
|
-
flushList();
|
|
145
|
-
code = { lang: fence[1] || '', lines: [] };
|
|
146
|
-
continue;
|
|
147
|
-
}
|
|
148
|
-
if (!line.trim()) {
|
|
149
|
-
flushParagraph();
|
|
150
|
-
flushList();
|
|
151
|
-
continue;
|
|
152
|
-
}
|
|
153
|
-
const tableCandidate = lines.slice(i, i + 2);
|
|
154
|
-
const maybeTable = renderTable(tableCandidate, sourceDir);
|
|
155
|
-
if (maybeTable) {
|
|
156
|
-
const tableLines = [line, lines[i + 1]];
|
|
157
|
-
i += 2;
|
|
158
|
-
while (i < lines.length && lines[i].includes('|') && lines[i].trim()) {
|
|
159
|
-
tableLines.push(lines[i]);
|
|
160
|
-
i += 1;
|
|
161
|
-
}
|
|
162
|
-
i -= 1;
|
|
163
|
-
flushParagraph();
|
|
164
|
-
flushList();
|
|
165
|
-
out.push(renderTable(tableLines, sourceDir));
|
|
166
|
-
continue;
|
|
167
|
-
}
|
|
168
|
-
const heading = line.match(/^(#{1,6})\s+(.+)$/);
|
|
169
|
-
if (heading) {
|
|
170
|
-
flushParagraph();
|
|
171
|
-
flushList();
|
|
172
|
-
const level = heading[1].length;
|
|
173
|
-
const content = renderInline(heading[2].trim(), sourceDir);
|
|
174
|
-
const id = slugify(content);
|
|
175
|
-
out.push(`<h${level}${id ? ` id="${id}"` : ''}>${content}</h${level}>`);
|
|
176
|
-
continue;
|
|
177
|
-
}
|
|
178
|
-
const unordered = line.match(/^\s*[-*+]\s+(.+)$/);
|
|
179
|
-
const ordered = line.match(/^\s*\d+[.)]\s+(.+)$/);
|
|
180
|
-
if (unordered || ordered) {
|
|
181
|
-
flushParagraph();
|
|
182
|
-
const isOrdered = Boolean(ordered);
|
|
183
|
-
if (!list || list.ordered !== isOrdered)
|
|
184
|
-
flushList();
|
|
185
|
-
if (!list)
|
|
186
|
-
list = { ordered: isOrdered, items: [] };
|
|
187
|
-
list.items.push((unordered?.[1] ?? ordered?.[1] ?? '').trim());
|
|
188
|
-
continue;
|
|
189
|
-
}
|
|
190
|
-
const quote = line.match(/^>\s?(.+)$/);
|
|
191
|
-
if (quote) {
|
|
192
|
-
flushParagraph();
|
|
193
|
-
flushList();
|
|
194
|
-
out.push(`<blockquote>${renderInline(quote[1], sourceDir)}</blockquote>`);
|
|
195
|
-
continue;
|
|
196
|
-
}
|
|
197
|
-
if (/^---+$/.test(line.trim())) {
|
|
198
|
-
flushParagraph();
|
|
199
|
-
flushList();
|
|
200
|
-
out.push('<hr />');
|
|
201
|
-
continue;
|
|
202
|
-
}
|
|
203
|
-
paragraph.push(line.trim());
|
|
204
|
-
}
|
|
205
|
-
if (code) {
|
|
206
|
-
out.push(`<pre><code${code.lang ? ` class="language-${escapeHtml(code.lang)}"` : ''}>${escapeHtml(code.lines.join('\n'))}</code></pre>`);
|
|
207
|
-
}
|
|
208
|
-
flushParagraph();
|
|
209
|
-
flushList();
|
|
210
|
-
return out.join('\n');
|
|
211
|
-
}
|
|
212
|
-
export function renderMarkdownHtml(markdown, options = {}) {
|
|
213
|
-
const title = options.title || 'Markdown Export';
|
|
214
|
-
const body = renderMarkdownBody(markdown, options.sourceDir);
|
|
215
|
-
return `<!doctype html>
|
|
1
|
+
import{execFile as E}from"node:child_process";import m from"node:fs";import R from"node:os";import i from"node:path";import{pathToFileURL as P}from"node:url";import{promisify as v}from"node:util";import{resolveShennianPath as O}from"../config/index.js";const w=v(E),I="markdown-to-pdf",F="markdownPdf.browser",D="markdownPdf.installManagedChromium",A="SKILL_SETUP_REQUIRED",j="1.59.1";function B(){return{code:A,skillId:I,doctorId:F,recommendedRepairActionId:D}}class L extends Error{code=A;setup=B();constructor(o="PDF export needs a local headless browser. Install the PDF export component, then retry."){super(o),this.name="MarkdownPdfBrowserMissingError"}}const H=/^```(\S*)\s*$/;function N(t){return/^[A-Za-z]:([\\/]|$)/.test(t)||/^\\\\[^\\]+\\[^\\]+/.test(t)}function T(t){return N(t)?i.win32:i}function W(t){const o=T(t),e=o.parse(t);return o.join(e.dir,`${e.name}.pdf`)}function f(t){return t.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""").replace(/'/g,"'")}function z(t){return t.toLowerCase().replace(/<[^>]+>/g,"").replace(/[^\p{L}\p{N}]+/gu,"-").replace(/^-+|-+$/g,"")}function h(t,o){let e=f(t);return e=e.replace(/`([^`]+)`/g,"<code>$1</code>"),e=e.replace(/\*\*([^*]+)\*\*/g,"<strong>$1</strong>"),e=e.replace(/__([^_]+)__/g,"<strong>$1</strong>"),e=e.replace(/\*([^*]+)\*/g,"<em>$1</em>"),e=e.replace(/_([^_]+)_/g,"<em>$1</em>"),e=e.replace(/!\[([^\]]*)\]\(([^)]+)\)/g,(r,s,c)=>{const n=c.trim().replace(/^<|>$/g,""),l=G(n,o);return`<img src="${f(l)}" alt="${s}" />`}),e=e.replace(/\[([^\]]+)\]\(([^)]+)\)/g,(r,s,c)=>{const n=c.trim().replace(/^<|>$/g,"");return`<a href="${f(n)}">${s}</a>`}),e}function G(t,o){if(/^(https?:|data:|file:)/i.test(t))return t;const e=t.split("#")[0].split("?")[0],r=i.resolve(o,decodeURIComponent(e)),s=i.relative(o,r);return s.startsWith("..")||i.isAbsolute(s)?"":P(r).href}function M(t,o){if(t.length<2)return null;const e=t[1].trim();if(!/^\|?\s*:?-{3,}:?\s*(\|\s*:?-{3,}:?\s*)+\|?$/.test(e))return null;const r=n=>n.trim().replace(/^\||\|$/g,"").split("|").map(l=>l.trim()),s=r(t[0]),c=t.slice(2).map(r);return["<table>","<thead><tr>",...s.map(n=>`<th>${h(n,o)}</th>`),"</tr></thead>","<tbody>",...c.map(n=>`<tr>${n.map(l=>`<td>${h(l,o)}</td>`).join("")}</tr>`),"</tbody></table>"].join("")}function U(t,o=process.cwd()){const e=t.replace(/\r\n?/g,`
|
|
2
|
+
`).split(`
|
|
3
|
+
`),r=[];let s=[],c=null,n=null;const l=()=>{s.length!==0&&(r.push(`<p>${h(s.join(" "),o)}</p>`),s=[])},d=()=>{if(!c)return;const a=c.ordered?"ol":"ul";r.push(`<${a}>${c.items.map(p=>`<li>${h(p,o)}</li>`).join("")}</${a}>`),c=null};for(let a=0;a<e.length;a+=1){const p=e[a],g=p.match(H);if(n){g?(r.push(`<pre><code${n.lang?` class="language-${f(n.lang)}"`:""}>${f(n.lines.join(`
|
|
4
|
+
`))}</code></pre>`),n=null):n.lines.push(p);continue}if(g){l(),d(),n={lang:g[1]||"",lines:[]};continue}if(!p.trim()){l(),d();continue}const k=e.slice(a,a+2);if(M(k,o)){const u=[p,e[a+1]];for(a+=2;a<e.length&&e[a].includes("|")&&e[a].trim();)u.push(e[a]),a+=1;a-=1,l(),d(),r.push(M(u,o));continue}const b=p.match(/^(#{1,6})\s+(.+)$/);if(b){l(),d();const u=b[1].length,_=h(b[2].trim(),o),S=z(_);r.push(`<h${u}${S?` id="${S}"`:""}>${_}</h${u}>`);continue}const $=p.match(/^\s*[-*+]\s+(.+)$/),x=p.match(/^\s*\d+[.)]\s+(.+)$/);if($||x){l();const u=!!x;(!c||c.ordered!==u)&&d(),c||(c={ordered:u,items:[]}),c.items.push(($?.[1]??x?.[1]??"").trim());continue}const y=p.match(/^>\s?(.+)$/);if(y){l(),d(),r.push(`<blockquote>${h(y[1],o)}</blockquote>`);continue}if(/^---+$/.test(p.trim())){l(),d(),r.push("<hr />");continue}s.push(p.trim())}return n&&r.push(`<pre><code${n.lang?` class="language-${f(n.lang)}"`:""}>${f(n.lines.join(`
|
|
5
|
+
`))}</code></pre>`),l(),d(),r.join(`
|
|
6
|
+
`)}function K(t,o={}){const e=o.title||"Markdown Export",r=U(t,o.sourceDir);return`<!doctype html>
|
|
216
7
|
<html>
|
|
217
8
|
<head>
|
|
218
9
|
<meta charset="utf-8" />
|
|
219
|
-
<title>${
|
|
10
|
+
<title>${f(e)}</title>
|
|
220
11
|
<style>
|
|
221
12
|
@page { size: A4; margin: 18mm 16mm; }
|
|
222
13
|
* { box-sizing: border-box; }
|
|
@@ -245,134 +36,7 @@ hr { border: none; border-top: 1px solid #e5e7eb; margin: 1.6em 0; }
|
|
|
245
36
|
</style>
|
|
246
37
|
</head>
|
|
247
38
|
<body>
|
|
248
|
-
${
|
|
39
|
+
${r}
|
|
249
40
|
</body>
|
|
250
|
-
</html
|
|
251
|
-
}
|
|
252
|
-
function executableCandidates() {
|
|
253
|
-
const envPath = process.env.SHENNIAN_CHROME_PATH;
|
|
254
|
-
const candidates = envPath ? [envPath] : [];
|
|
255
|
-
candidates.push(...managedChromiumExecutableCandidates());
|
|
256
|
-
if (process.platform === 'darwin') {
|
|
257
|
-
candidates.push('/Applications/Google Chrome.app/Contents/MacOS/Google Chrome', '/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge', '/Applications/Chromium.app/Contents/MacOS/Chromium', '/Applications/Brave Browser.app/Contents/MacOS/Brave Browser');
|
|
258
|
-
}
|
|
259
|
-
else if (process.platform === 'win32') {
|
|
260
|
-
const roots = [
|
|
261
|
-
process.env.PROGRAMFILES,
|
|
262
|
-
process.env['PROGRAMFILES(X86)'],
|
|
263
|
-
process.env.LOCALAPPDATA,
|
|
264
|
-
].filter((value) => Boolean(value));
|
|
265
|
-
for (const root of roots) {
|
|
266
|
-
candidates.push(path.join(root, 'Google', 'Chrome', 'Application', 'chrome.exe'), path.join(root, 'Microsoft', 'Edge', 'Application', 'msedge.exe'));
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
else {
|
|
270
|
-
candidates.push('google-chrome-stable', 'google-chrome', 'chromium-browser', 'chromium', 'microsoft-edge', 'brave-browser');
|
|
271
|
-
}
|
|
272
|
-
return candidates;
|
|
273
|
-
}
|
|
274
|
-
export function managedChromiumBrowsersPath() {
|
|
275
|
-
return resolveShennianPath('browsers', 'ms-playwright');
|
|
276
|
-
}
|
|
277
|
-
function managedChromiumExecutableCandidates() {
|
|
278
|
-
const root = managedChromiumBrowsersPath();
|
|
279
|
-
if (!fs.existsSync(root))
|
|
280
|
-
return [];
|
|
281
|
-
const candidates = [];
|
|
282
|
-
for (const entry of fs.readdirSync(root, { withFileTypes: true })) {
|
|
283
|
-
if (!entry.isDirectory() || !entry.name.startsWith('chromium-'))
|
|
284
|
-
continue;
|
|
285
|
-
const dir = path.join(root, entry.name);
|
|
286
|
-
if (process.platform === 'darwin') {
|
|
287
|
-
candidates.push(path.join(dir, 'chrome-mac', 'Chromium.app', 'Contents', 'MacOS', 'Chromium'));
|
|
288
|
-
}
|
|
289
|
-
else if (process.platform === 'win32') {
|
|
290
|
-
candidates.push(path.join(dir, 'chrome-win', 'chrome.exe'));
|
|
291
|
-
}
|
|
292
|
-
else {
|
|
293
|
-
candidates.push(path.join(dir, 'chrome-linux', 'chrome'));
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
return candidates;
|
|
297
|
-
}
|
|
298
|
-
export async function findChromiumExecutable(explicitPath) {
|
|
299
|
-
const candidates = explicitPath ? [explicitPath] : executableCandidates();
|
|
300
|
-
for (const candidate of candidates) {
|
|
301
|
-
if (candidate.includes(path.sep) || path.isAbsolute(candidate)) {
|
|
302
|
-
if (fs.existsSync(candidate))
|
|
303
|
-
return candidate;
|
|
304
|
-
continue;
|
|
305
|
-
}
|
|
306
|
-
try {
|
|
307
|
-
await execFileAsync(candidate, ['--version'], { timeout: 5000 });
|
|
308
|
-
return candidate;
|
|
309
|
-
}
|
|
310
|
-
catch {
|
|
311
|
-
// continue
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
throw new MarkdownPdfBrowserMissingError();
|
|
315
|
-
}
|
|
316
|
-
export async function installManagedChromium() {
|
|
317
|
-
const browsersPath = managedChromiumBrowsersPath();
|
|
318
|
-
fs.mkdirSync(browsersPath, { recursive: true });
|
|
319
|
-
const npx = process.platform === 'win32' ? 'npx.cmd' : 'npx';
|
|
320
|
-
const { stdout, stderr } = await execFileAsync(npx, ['-y', `playwright@${PLAYWRIGHT_VERSION_FOR_MANAGED_CHROMIUM}`, 'install', 'chromium'], {
|
|
321
|
-
timeout: 10 * 60_000,
|
|
322
|
-
maxBuffer: 4 * 1024 * 1024,
|
|
323
|
-
env: {
|
|
324
|
-
...process.env,
|
|
325
|
-
PLAYWRIGHT_BROWSERS_PATH: browsersPath,
|
|
326
|
-
},
|
|
327
|
-
});
|
|
328
|
-
return {
|
|
329
|
-
browsersPath,
|
|
330
|
-
output: [stdout, stderr].filter(Boolean).join('\n').slice(0, 20_000),
|
|
331
|
-
};
|
|
332
|
-
}
|
|
333
|
-
export async function convertMarkdownToPdf(inputPath, options = {}) {
|
|
334
|
-
const resolvedInput = path.resolve(inputPath);
|
|
335
|
-
const stat = fs.statSync(resolvedInput);
|
|
336
|
-
if (!stat.isFile())
|
|
337
|
-
throw new Error(`Not a file: ${inputPath}`);
|
|
338
|
-
if (!/\.mdx?$/i.test(resolvedInput))
|
|
339
|
-
throw new Error('Input must be a Markdown file (.md or .mdx)');
|
|
340
|
-
const outputPath = path.resolve(options.outputPath || defaultPdfOutputPath(resolvedInput));
|
|
341
|
-
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
|
|
342
|
-
const markdown = fs.readFileSync(resolvedInput, 'utf8');
|
|
343
|
-
const html = renderMarkdownHtml(markdown, {
|
|
344
|
-
title: options.title || path.basename(resolvedInput),
|
|
345
|
-
sourceDir: path.dirname(resolvedInput),
|
|
346
|
-
});
|
|
347
|
-
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'shennian-mdpdf-'));
|
|
348
|
-
const htmlPath = options.keepHtml
|
|
349
|
-
? outputPath.replace(/\.pdf$/i, '.html')
|
|
350
|
-
: path.join(tempDir, 'document.html');
|
|
351
|
-
fs.writeFileSync(htmlPath, html, 'utf8');
|
|
352
|
-
const browserPath = await findChromiumExecutable(options.chromePath);
|
|
353
|
-
const args = [
|
|
354
|
-
'--headless=new',
|
|
355
|
-
'--disable-gpu',
|
|
356
|
-
'--no-sandbox',
|
|
357
|
-
'--disable-dev-shm-usage',
|
|
358
|
-
'--allow-file-access-from-files',
|
|
359
|
-
`--print-to-pdf=${outputPath}`,
|
|
360
|
-
pathToFileURL(htmlPath).href,
|
|
361
|
-
];
|
|
362
|
-
try {
|
|
363
|
-
await execFileAsync(browserPath, args, { timeout: 120_000, maxBuffer: 1024 * 1024 });
|
|
364
|
-
}
|
|
365
|
-
finally {
|
|
366
|
-
if (!options.keepHtml)
|
|
367
|
-
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
368
|
-
}
|
|
369
|
-
if (!fs.existsSync(outputPath) || fs.statSync(outputPath).size === 0) {
|
|
370
|
-
throw new Error('PDF export failed: output file was not created');
|
|
371
|
-
}
|
|
372
|
-
return {
|
|
373
|
-
inputPath: resolvedInput,
|
|
374
|
-
outputPath,
|
|
375
|
-
htmlPath: options.keepHtml ? htmlPath : null,
|
|
376
|
-
browserPath,
|
|
377
|
-
};
|
|
378
|
-
}
|
|
41
|
+
</html>`}function q(){const t=process.env.SHENNIAN_CHROME_PATH,o=t?[t]:[];if(o.push(...Y()),process.platform==="darwin")o.push("/Applications/Google Chrome.app/Contents/MacOS/Google Chrome","/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge","/Applications/Chromium.app/Contents/MacOS/Chromium","/Applications/Brave Browser.app/Contents/MacOS/Brave Browser");else if(process.platform==="win32"){const e=[process.env.PROGRAMFILES,process.env["PROGRAMFILES(X86)"],process.env.LOCALAPPDATA].filter(r=>!!r);for(const r of e)o.push(i.join(r,"Google","Chrome","Application","chrome.exe"),i.join(r,"Microsoft","Edge","Application","msedge.exe"))}else o.push("google-chrome-stable","google-chrome","chromium-browser","chromium","microsoft-edge","brave-browser");return o}function C(){return O("browsers","ms-playwright")}function Y(){const t=C();if(!m.existsSync(t))return[];const o=[];for(const e of m.readdirSync(t,{withFileTypes:!0})){if(!e.isDirectory()||!e.name.startsWith("chromium-"))continue;const r=i.join(t,e.name);process.platform==="darwin"?o.push(i.join(r,"chrome-mac","Chromium.app","Contents","MacOS","Chromium")):process.platform==="win32"?o.push(i.join(r,"chrome-win","chrome.exe")):o.push(i.join(r,"chrome-linux","chrome"))}return o}async function Q(t){const o=t?[t]:q();for(const e of o){if(e.includes(i.sep)||i.isAbsolute(e)){if(m.existsSync(e))return e;continue}try{return await w(e,["--version"],{timeout:5e3}),e}catch{}}throw new L}async function ne(){const t=C();m.mkdirSync(t,{recursive:!0});const o=process.platform==="win32"?"npx.cmd":"npx",{stdout:e,stderr:r}=await w(o,["-y",`playwright@${j}`,"install","chromium"],{timeout:10*6e4,maxBuffer:4*1024*1024,env:{...process.env,PLAYWRIGHT_BROWSERS_PATH:t}});return{browsersPath:t,output:[e,r].filter(Boolean).join(`
|
|
42
|
+
`).slice(0,2e4)}}async function se(t,o={}){const e=i.resolve(t);if(!m.statSync(e).isFile())throw new Error(`Not a file: ${t}`);if(!/\.mdx?$/i.test(e))throw new Error("Input must be a Markdown file (.md or .mdx)");const s=i.resolve(o.outputPath||W(e));m.mkdirSync(i.dirname(s),{recursive:!0});const c=m.readFileSync(e,"utf8"),n=K(c,{title:o.title||i.basename(e),sourceDir:i.dirname(e)}),l=m.mkdtempSync(i.join(R.tmpdir(),"shennian-mdpdf-")),d=o.keepHtml?s.replace(/\.pdf$/i,".html"):i.join(l,"document.html");m.writeFileSync(d,n,"utf8");const a=await Q(o.chromePath),p=["--headless=new","--disable-gpu","--no-sandbox","--disable-dev-shm-usage","--allow-file-access-from-files",`--print-to-pdf=${s}`,P(d).href];try{await w(a,p,{timeout:12e4,maxBuffer:1024*1024})}finally{o.keepHtml||m.rmSync(l,{recursive:!0,force:!0})}if(!m.existsSync(s)||m.statSync(s).size===0)throw new Error("PDF export failed: output file was not created");return{inputPath:e,outputPath:s,htmlPath:o.keepHtml?d:null,browserPath:a}}export{F as MARKDOWN_PDF_BROWSER_DOCTOR_ID,D as MARKDOWN_PDF_MANAGED_CHROMIUM_ACTION_ID,A as MARKDOWN_PDF_SETUP_REQUIRED_CODE,I as MARKDOWN_PDF_SKILL_ID,L as MarkdownPdfBrowserMissingError,j as PLAYWRIGHT_VERSION_FOR_MANAGED_CHROMIUM,se as convertMarkdownToPdf,W as defaultPdfOutputPath,Q as findChromiumExecutable,ne as installManagedChromium,C as managedChromiumBrowsersPath,B as markdownPdfSetupRequiredPayload,U as renderMarkdownBody,K as renderMarkdownHtml};
|