shennian 0.2.88 → 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.
Files changed (143) hide show
  1. package/dist/assets/wechat-channel/macos/manifest.json +22 -0
  2. package/dist/assets/wechat-channel/macos/shennian-wechat-channel-helper +0 -0
  3. package/dist/bin/shennian.js +1 -1
  4. package/dist/publish-build-manifest.json +548 -0
  5. package/dist/scripts/wechat-rpa-confirmation.mjs +5 -97
  6. package/dist/src/agent-env.js +4 -105
  7. package/dist/src/agents/adapter.d.ts +6 -0
  8. package/dist/src/agents/adapter.js +1 -19
  9. package/dist/src/agents/claude.js +8 -305
  10. package/dist/src/agents/codex-control.d.ts +35 -0
  11. package/dist/src/agents/codex-control.js +2 -0
  12. package/dist/src/agents/codex-utils.js +7 -200
  13. package/dist/src/agents/codex.d.ts +8 -0
  14. package/dist/src/agents/codex.js +15 -863
  15. package/dist/src/agents/command-spec.js +2 -413
  16. package/dist/src/agents/config-status.js +1 -226
  17. package/dist/src/agents/cursor.js +1 -249
  18. package/dist/src/agents/custom.js +4 -271
  19. package/dist/src/agents/detect.js +1 -56
  20. package/dist/src/agents/external-channel-instructions.js +10 -94
  21. package/dist/src/agents/gemini.js +1 -173
  22. package/dist/src/agents/manager.js +13 -157
  23. package/dist/src/agents/model-registry/cache.js +1 -37
  24. package/dist/src/agents/model-registry/discovery.js +2 -187
  25. package/dist/src/agents/model-registry/parsers.js +4 -447
  26. package/dist/src/agents/model-registry/runner.js +1 -30
  27. package/dist/src/agents/model-registry/service.js +1 -78
  28. package/dist/src/agents/model-registry/types.js +1 -8
  29. package/dist/src/agents/model-registry.js +1 -18
  30. package/dist/src/agents/openclaw.js +2 -275
  31. package/dist/src/agents/opencode.js +1 -231
  32. package/dist/src/agents/pi-context.js +12 -217
  33. package/dist/src/agents/pi.js +14 -723
  34. package/dist/src/agents/platform-instructions.js +9 -54
  35. package/dist/src/channels/base.d.ts +4 -1
  36. package/dist/src/channels/base.js +1 -3
  37. package/dist/src/channels/registry.js +1 -30
  38. package/dist/src/channels/reply-split.js +10 -89
  39. package/dist/src/channels/runtime.d.ts +1 -0
  40. package/dist/src/channels/runtime.js +5 -533
  41. package/dist/src/channels/secret-registry.d.ts +1 -0
  42. package/dist/src/channels/secret-registry.js +1 -46
  43. package/dist/src/channels/websocket.js +8 -378
  44. package/dist/src/channels/wechat-channel/anchor.d.ts +10 -0
  45. package/dist/src/channels/wechat-channel/anchor.js +1 -0
  46. package/dist/src/channels/wechat-channel/client.d.ts +74 -0
  47. package/dist/src/channels/wechat-channel/client.js +1 -0
  48. package/dist/src/channels/wechat-channel/cooldown.d.ts +15 -0
  49. package/dist/src/channels/wechat-channel/cooldown.js +1 -0
  50. package/dist/src/channels/wechat-channel/fingerprint.d.ts +28 -0
  51. package/dist/src/channels/wechat-channel/fingerprint.js +1 -0
  52. package/dist/src/channels/wechat-channel/helper-assets.d.ts +37 -0
  53. package/dist/src/channels/wechat-channel/helper-assets.js +1 -0
  54. package/dist/src/channels/wechat-channel/helper-client.d.ts +25 -0
  55. package/dist/src/channels/wechat-channel/helper-client.js +3 -0
  56. package/dist/src/channels/wechat-channel/helper-protocol.d.ts +84 -0
  57. package/dist/src/channels/wechat-channel/helper-protocol.js +1 -0
  58. package/dist/src/channels/wechat-channel/index.d.ts +17 -0
  59. package/dist/src/channels/wechat-channel/index.js +1 -0
  60. package/dist/src/channels/wechat-channel/ledger.d.ts +33 -0
  61. package/dist/src/channels/wechat-channel/ledger.js +1 -0
  62. package/dist/src/channels/wechat-channel/media-resolver.d.ts +32 -0
  63. package/dist/src/channels/wechat-channel/media-resolver.js +1 -0
  64. package/dist/src/channels/wechat-channel/message-key.d.ts +19 -0
  65. package/dist/src/channels/wechat-channel/message-key.js +1 -0
  66. package/dist/src/channels/wechat-channel/observer.d.ts +64 -0
  67. package/dist/src/channels/wechat-channel/observer.js +1 -0
  68. package/dist/src/channels/wechat-channel/outbound-ledger.d.ts +69 -0
  69. package/dist/src/channels/wechat-channel/outbound-ledger.js +2 -0
  70. package/dist/src/channels/wechat-channel/outbound-sender.d.ts +26 -0
  71. package/dist/src/channels/wechat-channel/outbound-sender.js +1 -0
  72. package/dist/src/channels/wechat-channel/preflight.d.ts +37 -0
  73. package/dist/src/channels/wechat-channel/preflight.js +1 -0
  74. package/dist/src/channels/wechat-channel/runner.d.ts +34 -0
  75. package/dist/src/channels/wechat-channel/runner.js +1 -0
  76. package/dist/src/channels/wechat-channel/runtime.d.ts +45 -0
  77. package/dist/src/channels/wechat-channel/runtime.js +1 -0
  78. package/dist/src/channels/wechat-channel/scheduler.d.ts +35 -0
  79. package/dist/src/channels/wechat-channel/scheduler.js +1 -0
  80. package/dist/src/channels/wechat-rpa/macos-flow.js +1 -96
  81. package/dist/src/channels/wechat-rpa/macos.js +6 -48
  82. package/dist/src/channels/wechat-rpa/normalizer.js +7 -127
  83. package/dist/src/channels/wechat-rpa.d.ts +21 -0
  84. package/dist/src/channels/wechat-rpa.js +6 -1022
  85. package/dist/src/channels/wecom.js +4 -357
  86. package/dist/src/commands/agent.js +6 -131
  87. package/dist/src/commands/daemon-windows.js +8 -48
  88. package/dist/src/commands/daemon.js +19 -1013
  89. package/dist/src/commands/external-attachments.js +1 -51
  90. package/dist/src/commands/external.js +1 -137
  91. package/dist/src/commands/manager.js +2 -389
  92. package/dist/src/commands/pair-qr.js +1 -6
  93. package/dist/src/commands/pair.js +9 -287
  94. package/dist/src/commands/tools.js +1 -34
  95. package/dist/src/commands/upgrade.js +1 -198
  96. package/dist/src/config/index.js +1 -35
  97. package/dist/src/daemon-log.js +6 -58
  98. package/dist/src/env-path.js +1 -64
  99. package/dist/src/fs/boundary.js +1 -126
  100. package/dist/src/fs/handler.js +1 -130
  101. package/dist/src/fs/security.js +1 -32
  102. package/dist/src/fs/text-decoder.d.ts +10 -0
  103. package/dist/src/fs/text-decoder.js +1 -0
  104. package/dist/src/index.js +2 -404
  105. package/dist/src/log-reporter.js +1 -16
  106. package/dist/src/manager/prompt.js +29 -34
  107. package/dist/src/manager/registry.js +2 -269
  108. package/dist/src/manager/runtime.js +19 -1003
  109. package/dist/src/native-fusion/config.js +1 -5
  110. package/dist/src/native-fusion/opencode-parser.js +3 -123
  111. package/dist/src/native-fusion/parser-common.js +8 -264
  112. package/dist/src/native-fusion/parsers.js +8 -729
  113. package/dist/src/native-fusion/service.d.ts +10 -0
  114. package/dist/src/native-fusion/service.js +2 -198
  115. package/dist/src/native-fusion/state.js +1 -22
  116. package/dist/src/native-fusion/types.js +1 -1
  117. package/dist/src/region.js +1 -88
  118. package/dist/src/relay/client.js +1 -343
  119. package/dist/src/session/archive-zip.js +1 -220
  120. package/dist/src/session/handlers/agent-config.js +1 -150
  121. package/dist/src/session/handlers/agents.js +1 -55
  122. package/dist/src/session/handlers/chat.js +2 -733
  123. package/dist/src/session/handlers/control.js +1 -55
  124. package/dist/src/session/handlers/fs.js +1 -747
  125. package/dist/src/session/handlers/session-refresh.js +1 -35
  126. package/dist/src/session/handlers/skills.js +1 -121
  127. package/dist/src/session/handlers/title.js +1 -60
  128. package/dist/src/session/handlers/tool-detail.d.ts +3 -0
  129. package/dist/src/session/handlers/tool-detail.js +1 -0
  130. package/dist/src/session/manager.d.ts +3 -0
  131. package/dist/src/session/manager.js +1 -261
  132. package/dist/src/session/projection.js +1 -54
  133. package/dist/src/session/queue.js +4 -317
  134. package/dist/src/session/remote-attachments.js +1 -72
  135. package/dist/src/session/store.js +3 -109
  136. package/dist/src/session/types.d.ts +4 -0
  137. package/dist/src/session/types.js +1 -4
  138. package/dist/src/skills/registry.js +15 -148
  139. package/dist/src/skills/setup.js +1 -101
  140. package/dist/src/tools/markdown-to-pdf.js +10 -346
  141. package/dist/src/upgrade/engine.js +3 -347
  142. package/package.json +3 -2
  143. package/dist/scripts/wechat-rpa-download-candidates.mjs +0 -105
@@ -1,148 +1,15 @@
1
- // @arch docs/features/skill-marketplace.md
2
- // @test src/__tests__/skill-registry.test.ts
3
- import fs from 'node:fs';
4
- import path from 'node:path';
5
- import { resolveShennianPath } from '../config/index.js';
6
- import { runSkillDoctor, summarizeSetupStatus } from './setup.js';
7
- function safeSkillId(skillId) {
8
- const normalized = skillId.trim();
9
- if (!/^[a-z0-9][a-z0-9._-]{1,80}$/i.test(normalized)) {
10
- throw new Error('Invalid skill id');
11
- }
12
- return normalized;
13
- }
14
- function assertSafeBundlePath(relativePath) {
15
- const normalized = relativePath.replace(/\\/g, '/');
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};
@@ -1,101 +1 @@
1
- // @arch docs/features/skill-marketplace.md
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
- // @arch docs/features/markdown-pdf-export.md
2
- // @test src/__tests__/markdown-to-pdf.test.ts
3
- import { execFile } from 'node:child_process';
4
- import fs from 'node:fs';
5
- import os from 'node:os';
6
- import path from 'node:path';
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, '&lt;')
48
- .replace(/>/g, '&gt;')
49
- .replace(/"/g, '&quot;')
50
- .replace(/'/g, '&#39;');
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(/^&lt;|&gt;$/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(/^&lt;|&gt;$/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,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;").replace(/'/g,"&#39;")}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(/^&lt;|&gt;$/g,""),l=G(n,o);return`<img src="${f(l)}" alt="${s}" />`}),e=e.replace(/\[([^\]]+)\]\(([^)]+)\)/g,(r,s,c)=>{const n=c.trim().replace(/^&lt;|&gt;$/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>${escapeHtml(title)}</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
- ${body}
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};