@supportpages.io/wtfm 0.0.0-stage → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/NOTICE +23 -0
- package/README.md +392 -2
- package/dist/actions.d.ts +98 -0
- package/dist/actions.js +86 -0
- package/dist/actions.js.map +1 -0
- package/dist/agent-settings.d.ts +22 -0
- package/dist/agent-settings.js +35 -0
- package/dist/agent-settings.js.map +1 -0
- package/dist/api.d.ts +14 -0
- package/dist/api.js +159 -0
- package/dist/api.js.map +1 -0
- package/dist/article-link.d.ts +16 -0
- package/dist/article-link.js +25 -0
- package/dist/article-link.js.map +1 -0
- package/dist/artifacts.d.ts +45 -0
- package/dist/artifacts.js +144 -0
- package/dist/artifacts.js.map +1 -0
- package/dist/brand.d.ts +4 -0
- package/dist/brand.js +9 -0
- package/dist/brand.js.map +1 -0
- package/dist/bridge.d.ts +2964 -0
- package/dist/bridge.js +1036 -0
- package/dist/bridge.js.map +1 -0
- package/dist/capacity.d.ts +5 -0
- package/dist/capacity.js +11 -0
- package/dist/capacity.js.map +1 -0
- package/dist/credentials.d.ts +13 -0
- package/dist/credentials.js +88 -0
- package/dist/credentials.js.map +1 -0
- package/dist/development-tls.d.ts +8 -0
- package/dist/development-tls.js +38 -0
- package/dist/development-tls.js.map +1 -0
- package/dist/errors.d.ts +15 -0
- package/dist/errors.js +18 -0
- package/dist/errors.js.map +1 -0
- package/dist/export.d.ts +11 -0
- package/dist/export.js +91 -0
- package/dist/export.js.map +1 -0
- package/dist/hosted-operations.d.ts +108 -0
- package/dist/hosted-operations.js +120 -0
- package/dist/hosted-operations.js.map +1 -0
- package/dist/hosting-benefits.d.ts +66 -0
- package/dist/hosting-benefits.js +67 -0
- package/dist/hosting-benefits.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +49 -0
- package/dist/index.js.map +1 -0
- package/dist/local-inventory.d.ts +22 -0
- package/dist/local-inventory.js +59 -0
- package/dist/local-inventory.js.map +1 -0
- package/dist/local-setup.d.ts +208 -0
- package/dist/local-setup.js +140 -0
- package/dist/local-setup.js.map +1 -0
- package/dist/pairing.d.ts +71 -0
- package/dist/pairing.js +235 -0
- package/dist/pairing.js.map +1 -0
- package/dist/preferences.d.ts +18 -0
- package/dist/preferences.js +44 -0
- package/dist/preferences.js.map +1 -0
- package/dist/progress.d.ts +205 -0
- package/dist/progress.js +224 -0
- package/dist/progress.js.map +1 -0
- package/dist/reminders.d.ts +31 -0
- package/dist/reminders.js +73 -0
- package/dist/reminders.js.map +1 -0
- package/dist/replace-connection.d.ts +6 -0
- package/dist/replace-connection.js +77 -0
- package/dist/replace-connection.js.map +1 -0
- package/dist/repository-actions.d.ts +18 -0
- package/dist/repository-actions.js +9 -0
- package/dist/repository-actions.js.map +1 -0
- package/dist/repository-benefits.d.ts +96 -0
- package/dist/repository-benefits.js +64 -0
- package/dist/repository-benefits.js.map +1 -0
- package/dist/run-update.d.ts +17 -0
- package/dist/run-update.js +27 -0
- package/dist/run-update.js.map +1 -0
- package/dist/runs.d.ts +541 -0
- package/dist/runs.js +146 -0
- package/dist/runs.js.map +1 -0
- package/dist/runtime.d.ts +12 -0
- package/dist/runtime.js +58 -0
- package/dist/runtime.js.map +1 -0
- package/dist/schema.d.ts +277 -0
- package/dist/schema.js +66 -0
- package/dist/schema.js.map +1 -0
- package/dist/server.d.ts +4 -0
- package/dist/server.js +298 -0
- package/dist/server.js.map +1 -0
- package/dist/session.d.ts +1714 -0
- package/dist/session.js +619 -0
- package/dist/session.js.map +1 -0
- package/dist/settings.d.ts +9 -0
- package/dist/settings.js +21 -0
- package/dist/settings.js.map +1 -0
- package/dist/sync.d.ts +495 -0
- package/dist/sync.js +191 -0
- package/dist/sync.js.map +1 -0
- package/dist/telemetry-scrub.d.ts +13 -0
- package/dist/telemetry-scrub.js +51 -0
- package/dist/telemetry-scrub.js.map +1 -0
- package/dist/telemetry.d.ts +73 -0
- package/dist/telemetry.js +173 -0
- package/dist/telemetry.js.map +1 -0
- package/dist/walkthroughs.d.ts +224 -0
- package/dist/walkthroughs.js +109 -0
- package/dist/walkthroughs.js.map +1 -0
- package/dist/workspace.d.ts +14 -0
- package/dist/workspace.js +127 -0
- package/dist/workspace.js.map +1 -0
- package/dist/writer-agent.d.ts +21 -0
- package/dist/writer-agent.js +27 -0
- package/dist/writer-agent.js.map +1 -0
- package/dist/writer-entry.d.ts +171 -0
- package/dist/writer-entry.js +233 -0
- package/dist/writer-entry.js.map +1 -0
- package/dist/writing-style.d.ts +7 -0
- package/dist/writing-style.js +52 -0
- package/dist/writing-style.js.map +1 -0
- package/engine/SYNC.json +4 -0
- package/engine/VERSION +1 -0
- package/engine/detect-project/README.md +141 -0
- package/engine/detect-project/SKILL.md +1421 -0
- package/engine/detect-project/assets/desktop/desktop-frame.css +428 -0
- package/engine/detect-project/assets/game/game-frame.css +132 -0
- package/engine/detect-project/assets/macosui/LICENSE-puppertino.txt +21 -0
- package/engine/detect-project/assets/macosui/VERSIONS.txt +1 -0
- package/engine/detect-project/assets/macosui/fonts.css +15 -0
- package/engine/detect-project/assets/macosui/macos-frame.css +481 -0
- package/engine/detect-project/assets/macosui/puppertino.css +2153 -0
- package/engine/detect-project/assets/mobileui/LICENSE-fonts.txt +13 -0
- package/engine/detect-project/assets/mobileui/LICENSE-framework7.txt +52 -0
- package/engine/detect-project/assets/mobileui/VERSIONS.txt +6 -0
- package/engine/detect-project/assets/mobileui/device-frame.css +316 -0
- package/engine/detect-project/assets/mobileui/f7-color-theme.mjs +1345 -0
- package/engine/detect-project/assets/mobileui/f7-icons-names.json +1254 -0
- package/engine/detect-project/assets/mobileui/fonts.css +16 -0
- package/engine/detect-project/assets/mobileui/framework7-components.css +39 -0
- package/engine/detect-project/assets/mobileui/framework7-core.css +5245 -0
- package/engine/detect-project/assets/mobileui/icons.css +31 -0
- package/engine/detect-project/assets/mobileui/md3-defaults.css +89 -0
- package/engine/detect-project/assets/mobileui/platforms.json +46 -0
- package/engine/detect-project/assets/tailwind-fallback.css +1729 -0
- package/engine/detect-project/assets/webtui/LICENSE-webtui.txt +28 -0
- package/engine/detect-project/assets/webtui/VERSIONS.txt +7 -0
- package/engine/detect-project/assets/webtui/terminal-frame.css +195 -0
- package/engine/detect-project/assets/webtui/theme-catppuccin.css +1 -0
- package/engine/detect-project/assets/webtui/theme-everforest.css +1 -0
- package/engine/detect-project/assets/webtui/theme-gruvbox.css +1 -0
- package/engine/detect-project/assets/webtui/theme-nord.css +1 -0
- package/engine/detect-project/assets/webtui/theme-vitesse.css +1 -0
- package/engine/detect-project/assets/webtui/themes.json +37 -0
- package/engine/detect-project/assets/webtui/webtui-core.css +1 -0
- package/engine/detect-project/assets/win32ui/7css.css +2 -0
- package/engine/detect-project/assets/win32ui/LICENSE-7css.txt +21 -0
- package/engine/detect-project/assets/win32ui/VERSIONS.txt +1 -0
- package/engine/detect-project/assets/win32ui/win32-frame.css +278 -0
- package/engine/detect-project/package-lock.json +12 -0
- package/engine/detect-project/package.json +10 -0
- package/engine/detect-project/scripts/apply_runtime_profiles.js +313 -0
- package/engine/detect-project/scripts/check_css_health.js +412 -0
- package/engine/detect-project/scripts/check_project_map.js +150 -0
- package/engine/detect-project/scripts/check_runtime_coverage.js +311 -0
- package/engine/detect-project/scripts/check_runtime_recipe_quality.js +184 -0
- package/engine/detect-project/scripts/classify_app_type.sh +246 -0
- package/engine/detect-project/scripts/classify_surface.sh +95 -0
- package/engine/detect-project/scripts/classify_workspace.js +39 -0
- package/engine/detect-project/scripts/compile_css.sh +447 -0
- package/engine/detect-project/scripts/detect_static.js +645 -0
- package/engine/detect-project/scripts/detect_structure.js +187 -0
- package/engine/detect-project/scripts/include_census.js +451 -0
- package/engine/detect-project/scripts/json_get.js +142 -0
- package/engine/detect-project/scripts/merge_json.js +52 -0
- package/engine/detect-project/scripts/recommend_model_tier.js +252 -0
- package/engine/detect-project/scripts/resolve_route_chains.js +135 -0
- package/engine/detect-project/scripts/run_css_build.sh +40 -0
- package/engine/detect-project/scripts/sanitize_css.js +83 -0
- package/engine/detect-project/scripts/test_classify_surface.js +108 -0
- package/engine/detect-project/scripts/test_node_helpers.js +142 -0
- package/engine/detect-project/scripts/test_recommend_model_tier.js +119 -0
- package/engine/detect-project/scripts/test_resolve_route_chains.js +182 -0
- package/engine/detect-project/scripts/test_runtime_coverage.js +323 -0
- package/engine/detect-project/scripts/test_runtime_recipe_quality.js +187 -0
- package/engine/detect-project/scripts/theme_overrides.js +169 -0
- package/engine/detect-project/scripts/write_branding.js +129 -0
- package/engine/generate-illustrated-article/SKILL.md +461 -0
- package/engine/generate-illustrated-article/contracts/desktop.md +90 -0
- package/engine/generate-illustrated-article/contracts/game.md +38 -0
- package/engine/generate-illustrated-article/contracts/label-evidence.md +38 -0
- package/engine/generate-illustrated-article/contracts/macos.md +79 -0
- package/engine/generate-illustrated-article/contracts/mobile.md +40 -0
- package/engine/generate-illustrated-article/contracts/terminal.md +29 -0
- package/engine/generate-illustrated-article/contracts/win32.md +43 -0
- package/engine/generate-illustrated-article/package-lock.json +366 -0
- package/engine/generate-illustrated-article/package.json +15 -0
- package/engine/generate-illustrated-article/scripts/article_blocks.js +34 -0
- package/engine/generate-illustrated-article/scripts/check_article_json.js +105 -0
- package/engine/generate-illustrated-article/scripts/emit_walkthrough_signals.js +100 -0
- package/engine/generate-illustrated-article/scripts/extract_images.js +187 -0
- package/engine/generate-illustrated-article/scripts/generate_content_images.js +376 -0
- package/engine/generate-illustrated-article/scripts/include_census.js +451 -0
- package/engine/generate-illustrated-article/scripts/inject_assets.js +1691 -0
- package/engine/generate-illustrated-article/scripts/jit_mockup_css.js +191 -0
- package/engine/generate-illustrated-article/scripts/label_evidence.js +87 -0
- package/engine/generate-illustrated-article/scripts/lint_article_copy.js +252 -0
- package/engine/generate-illustrated-article/scripts/lint_mockup_fidelity.js +2403 -0
- package/engine/generate-illustrated-article/scripts/polish_tickets.js +1148 -0
- package/engine/generate-illustrated-article/scripts/related_repos.sh +52 -0
- package/engine/generate-illustrated-article/scripts/render_all.js +177 -0
- package/engine/generate-illustrated-article/scripts/render_mockup.js +665 -0
- package/engine/generate-illustrated-article/scripts/render_ready.js +164 -0
- package/engine/generate-illustrated-article/scripts/resolve_workspace.sh +86 -0
- package/engine/generate-illustrated-article/scripts/runtime_region_geometry.js +55 -0
- package/engine/generate-illustrated-article/scripts/source_paths.js +49 -0
- package/engine/generate-illustrated-article/scripts/test_control_visibility.js +30 -0
- package/engine/generate-illustrated-article/scripts/test_emit_walkthrough_signals.js +157 -0
- package/engine/generate-illustrated-article/scripts/test_generate_content_images.js +168 -0
- package/engine/generate-illustrated-article/scripts/test_include_census.js +126 -0
- package/engine/generate-illustrated-article/scripts/test_label_evidence.js +49 -0
- package/engine/generate-illustrated-article/scripts/test_lint_article_copy.js +138 -0
- package/engine/generate-illustrated-article/scripts/test_lint_mockup_fidelity.js +653 -0
- package/engine/generate-illustrated-article/scripts/test_node_ports.js +152 -0
- package/engine/generate-illustrated-article/scripts/test_polish_tickets.js +481 -0
- package/engine/generate-illustrated-article/scripts/test_related_repos.js +64 -0
- package/engine/generate-illustrated-article/scripts/test_render_ready.js +85 -0
- package/engine/generate-illustrated-article/scripts/test_source_paths.js +68 -0
- package/engine/generate-illustrated-article/scripts/trace_hook.js +79 -0
- package/engine/generate-illustrated-article/scripts/trace_hook.sh +4 -0
- package/engine/generate-illustrated-article/scripts/validate_html.js +114 -0
- package/engine/generate-illustrated-article/scripts/watermark.js +69 -0
- package/install.sh +14 -0
- package/package.json +57 -4
- package/scripts/auth.mjs +33 -0
- package/scripts/auto-update.mjs +7 -0
- package/scripts/build-plugin.mjs +58 -0
- package/scripts/build-release.mjs +94 -0
- package/scripts/check-release.mjs +40 -0
- package/scripts/check-runtime.mjs +10 -0
- package/scripts/cli.mjs +8 -0
- package/scripts/install.mjs +64 -0
- package/scripts/lib/agent-runner.mjs +181 -0
- package/scripts/lib/agent-settings.mjs +215 -0
- package/scripts/lib/article-skills.mjs +82 -0
- package/scripts/lib/auto-update.mjs +50 -0
- package/scripts/lib/brand.mjs +9 -0
- package/scripts/lib/browser.mjs +12 -0
- package/scripts/lib/claude-connection.mjs +33 -0
- package/scripts/lib/claude-permissions.mjs +47 -0
- package/scripts/lib/claude-plugin.mjs +15 -0
- package/scripts/lib/claude-writer.mjs +10 -0
- package/scripts/lib/cli-main.mjs +85 -0
- package/scripts/lib/cli.mjs +888 -0
- package/scripts/lib/codex-config.mjs +51 -0
- package/scripts/lib/codex-integration.mjs +61 -0
- package/scripts/lib/codex-skill.mjs +34 -0
- package/scripts/lib/harness-models.mjs +109 -0
- package/scripts/lib/install.mjs +269 -0
- package/scripts/lib/managed-writer.mjs +38 -0
- package/scripts/lib/planning.mjs +263 -0
- package/scripts/lib/prepare-update.mjs +85 -0
- package/scripts/lib/refresh-writers.mjs +12 -0
- package/scripts/lib/remove.mjs +167 -0
- package/scripts/lib/renderer.mjs +34 -0
- package/scripts/lib/terminal.mjs +252 -0
- package/scripts/lib/uninit.mjs +72 -0
- package/scripts/lib/update.mjs +55 -0
- package/scripts/lib/writer-recovery.mjs +35 -0
- package/scripts/lib/yolo.mjs +362 -0
- package/scripts/plugin-session.mjs +65 -0
- package/scripts/prepare-update.mjs +12 -0
- package/scripts/publish-release.mjs +92 -0
- package/scripts/skills/supportpages/SKILL.md +120 -0
- package/scripts/smoke-release.mjs +101 -0
- package/server.json +27 -0
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* source_paths.js — cited source paths: project-relative always; `../<repo>/…`
|
|
4
|
+
* only with RTFM_REPOS_ROOT set, only into a git checkout beside the project,
|
|
5
|
+
* never escaping that checkout (nested `..`, symlinks, the root itself, a
|
|
6
|
+
* non-repo dir, the project itself). Plus label evidence found in a sibling.
|
|
7
|
+
*/
|
|
8
|
+
'use strict';
|
|
9
|
+
const assert = require('assert');
|
|
10
|
+
const fs = require('fs');
|
|
11
|
+
const os = require('os');
|
|
12
|
+
const path = require('path');
|
|
13
|
+
|
|
14
|
+
function write(root, rel, text) { const p = path.join(root, rel); fs.mkdirSync(path.dirname(p), { recursive: true }); fs.writeFileSync(p, text); }
|
|
15
|
+
|
|
16
|
+
const root = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'source-paths-')));
|
|
17
|
+
const web = path.join(root, 'acme-web');
|
|
18
|
+
fs.mkdirSync(path.join(web, '.git'), { recursive: true });
|
|
19
|
+
fs.mkdirSync(path.join(root, 'acme-api', '.git'), { recursive: true });
|
|
20
|
+
write(web, 'app/views/plans/index.html.erb', '<h1>Plans</h1>');
|
|
21
|
+
write(root, 'acme-api/config/locales/en.yml', 'en:\n limit_reached: "You have reached your plan limit"\n');
|
|
22
|
+
write(root, 'notes/readme.md', 'not a repo');
|
|
23
|
+
write(root, 'secret.txt', 'outside every checkout');
|
|
24
|
+
fs.symlinkSync(path.join(root, 'secret.txt'), path.join(root, 'acme-api', 'leak.txt'));
|
|
25
|
+
|
|
26
|
+
delete process.env.RTFM_REPOS_ROOT;
|
|
27
|
+
const load = () => { for (const m of ['./source_paths.js', './label_evidence.js']) delete require.cache[require.resolve(m)]; return require('./source_paths.js'); };
|
|
28
|
+
|
|
29
|
+
// Without RTFM_REPOS_ROOT: the strict single-repo rule.
|
|
30
|
+
let sp = load();
|
|
31
|
+
assert.ok(sp.isSafeSourcePath('app/views/plans/index.html.erb'));
|
|
32
|
+
assert.ok(!sp.isSafeSourcePath('../acme-api/config/locales/en.yml'));
|
|
33
|
+
assert.ok(!sp.isSafeSourcePath('/etc/passwd'));
|
|
34
|
+
assert.ok(!sp.isSafeSourcePath(''));
|
|
35
|
+
assert.strictEqual(sp.resolveSourcePath(web, 'app/views/plans/index.html.erb'), path.join(web, 'app/views/plans/index.html.erb'));
|
|
36
|
+
assert.throws(() => sp.resolveSourcePath(web, '../acme-api/config/locales/en.yml'), /project-relative/);
|
|
37
|
+
|
|
38
|
+
// With RTFM_REPOS_ROOT: one leading `..` into a sibling checkout.
|
|
39
|
+
process.env.RTFM_REPOS_ROOT = root;
|
|
40
|
+
sp = load();
|
|
41
|
+
assert.ok(sp.isSafeSourcePath('../acme-api/config/locales/en.yml'));
|
|
42
|
+
assert.ok(!sp.isSafeSourcePath('../acme-api/../secret.txt'));
|
|
43
|
+
assert.ok(!sp.isSafeSourcePath('app/../../secret.txt'));
|
|
44
|
+
assert.ok(!sp.isSafeSourcePath('../secret.txt'));
|
|
45
|
+
assert.strictEqual(sp.resolveSourcePath(web, '../acme-api/config/locales/en.yml'), path.join(root, 'acme-api/config/locales/en.yml'));
|
|
46
|
+
assert.ok(sp.sourceExists(web, '../acme-api/config/locales/en.yml'));
|
|
47
|
+
assert.ok(!sp.sourceExists(web, '../acme-api/missing.yml'));
|
|
48
|
+
assert.throws(() => sp.resolveSourcePath(web, '../notes/readme.md'), /not a related repository/);
|
|
49
|
+
assert.throws(() => sp.resolveSourcePath(web, '../acme-web/app/views/plans/index.html.erb'), /not a related repository/);
|
|
50
|
+
assert.throws(() => sp.resolveSourcePath(web, '../acme-api/leak.txt'), /outside the project/);
|
|
51
|
+
|
|
52
|
+
// A project dir that isn't a direct child of the root can't reach siblings.
|
|
53
|
+
const nested = path.join(root, 'acme-api', 'nested');
|
|
54
|
+
fs.mkdirSync(nested);
|
|
55
|
+
assert.throws(() => sp.resolveSourcePath(nested, '../acme-web/app/views/plans/index.html.erb'), /inside RTFM_REPOS_ROOT/);
|
|
56
|
+
|
|
57
|
+
// Label evidence found in a related repository verifies against that file.
|
|
58
|
+
const { verifyLabelEvidence } = require('./label_evidence.js');
|
|
59
|
+
assert.deepStrictEqual(
|
|
60
|
+
verifyLabelEvidence({ string: 'You have reached your plan limit', found_in: '../acme-api/config/locales/en.yml' }, 'app/views/plans/index.html.erb', web),
|
|
61
|
+
{ ok: true, kind: 'literal' }
|
|
62
|
+
);
|
|
63
|
+
assert.strictEqual(verifyLabelEvidence({ string: 'Nope', found_in: '../acme-api/config/locales/en.yml' }, null, web).ok, false);
|
|
64
|
+
assert.strictEqual(verifyLabelEvidence({ string: 'outside', found_in: '../acme-api/leak.txt' }, null, web).ok, false);
|
|
65
|
+
|
|
66
|
+
delete process.env.RTFM_REPOS_ROOT;
|
|
67
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
68
|
+
console.log('test_source_paths: ok');
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/*
|
|
5
|
+
* trace_hook.js — PostToolUse hook for the rtfm-skills bundle.
|
|
6
|
+
*
|
|
7
|
+
* Logs every tool call the agent makes WHILE a skill run is active into a
|
|
8
|
+
* project-local NDJSON trace, so runs can be diffed for repeated navigation.
|
|
9
|
+
* The same hook serves generate-illustrated-article and generate-walkthrough.
|
|
10
|
+
*
|
|
11
|
+
* Scoping: a run is "active" only while ./.rtfm-trace/CURRENT exists. STEP 0 of
|
|
12
|
+
* each SKILL.md writes it (the run's <slug>); the final STEP removes it. Outside
|
|
13
|
+
* a run this hook is a quick no-op, so it's safe to leave registered.
|
|
14
|
+
*
|
|
15
|
+
* Contract (PostToolUse): receives the event JSON on stdin (.cwd, .session_id,
|
|
16
|
+
* .tool_name, .tool_input). It is non-blocking — it never affects the tool
|
|
17
|
+
* result. It writes ONLY to the trace file, emits nothing on stdout, and always
|
|
18
|
+
* exits 0. Works in interactive and headless runs alike.
|
|
19
|
+
*
|
|
20
|
+
* No lock: each entry is a single small line (long fields truncated to 500
|
|
21
|
+
* characters), so the append stays under PIPE_BUF and is atomic on local
|
|
22
|
+
* filesystems even when the agent fires tool calls in parallel.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
const fs = require('fs');
|
|
26
|
+
const path = require('path');
|
|
27
|
+
|
|
28
|
+
function truncate(value) {
|
|
29
|
+
if (value === undefined || value === null) return null;
|
|
30
|
+
const text = typeof value === 'string' ? value : JSON.stringify(value);
|
|
31
|
+
return [...text].slice(0, 500).join('');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function orNull(value) {
|
|
35
|
+
return value === undefined || value === null || value === false ? null : value;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function record(payload, now = new Date()) {
|
|
39
|
+
const input = payload.tool_input && typeof payload.tool_input === 'object' ? payload.tool_input : {};
|
|
40
|
+
return {
|
|
41
|
+
ts: now.toISOString().replace(/\.\d{3}Z$/, 'Z'),
|
|
42
|
+
session: payload.session_id ?? null,
|
|
43
|
+
tool: payload.tool_name ?? null,
|
|
44
|
+
file: orNull(input.file_path),
|
|
45
|
+
pattern: orNull(input.pattern),
|
|
46
|
+
path: orNull(input.path),
|
|
47
|
+
glob: orNull(input.glob),
|
|
48
|
+
command: truncate(input.command),
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function main(stdin) {
|
|
53
|
+
let payload;
|
|
54
|
+
try { payload = JSON.parse(stdin); } catch { return; }
|
|
55
|
+
if (!payload || typeof payload !== 'object') return;
|
|
56
|
+
const cwd = orNull(payload.cwd);
|
|
57
|
+
if (!cwd) return;
|
|
58
|
+
|
|
59
|
+
const marker = path.join(String(cwd), '.rtfm-trace', 'CURRENT');
|
|
60
|
+
let slug;
|
|
61
|
+
try { slug = fs.readFileSync(marker, 'utf8').split('\n')[0].split('\t')[0]; }
|
|
62
|
+
catch { return; } // not inside a skill run -> no-op
|
|
63
|
+
const out = path.join(String(cwd), '.rtfm-trace', `${slug || 'run'}.ndjson`);
|
|
64
|
+
|
|
65
|
+
try { fs.appendFileSync(out, JSON.stringify(record(payload)) + '\n'); }
|
|
66
|
+
catch { /* never affect the tool call */ }
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (require.main === module) {
|
|
70
|
+
const chunks = [];
|
|
71
|
+
process.stdin.on('data', chunk => chunks.push(chunk));
|
|
72
|
+
process.stdin.on('end', () => {
|
|
73
|
+
try { main(Buffer.concat(chunks).toString('utf8')); } catch { /* always exit 0 */ }
|
|
74
|
+
process.exitCode = 0;
|
|
75
|
+
});
|
|
76
|
+
process.stdin.on('error', () => { process.exitCode = 0; });
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
module.exports = { record };
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* HTML Validation Script
|
|
4
|
+
* Validates HTML mockups before rendering to catch common issues.
|
|
5
|
+
* Returns JSON with validation results.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
|
|
10
|
+
function validateHtml(htmlFile) {
|
|
11
|
+
const html = fs.readFileSync(htmlFile, 'utf8').trim();
|
|
12
|
+
const errors = [];
|
|
13
|
+
const warnings = [];
|
|
14
|
+
|
|
15
|
+
if (!html) {
|
|
16
|
+
errors.push('HTML file is empty');
|
|
17
|
+
return { valid: false, errors, warnings, html_length: 0 };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (!html.match(/<!DOCTYPE\s+html>/i)) {
|
|
21
|
+
warnings.push('Missing DOCTYPE declaration');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (!/<html/i.test(html)) {
|
|
25
|
+
errors.push('Missing <html> tag');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (!/<body/i.test(html)) {
|
|
29
|
+
errors.push('Missing <body> tag');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const styleOpenCount = (html.match(/<style/gi) || []).length;
|
|
33
|
+
const styleCloseCount = (html.match(/<\/style>/gi) || []).length;
|
|
34
|
+
if (styleOpenCount !== styleCloseCount) {
|
|
35
|
+
errors.push(`Mismatched style tags: ${styleOpenCount} opening, ${styleCloseCount} closing`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const scriptOpenCount = (html.match(/<script/gi) || []).length;
|
|
39
|
+
const scriptCloseCount = (html.match(/<\/script>/gi) || []).length;
|
|
40
|
+
if (scriptOpenCount !== scriptCloseCount) {
|
|
41
|
+
errors.push(`Mismatched script tags: ${scriptOpenCount} opening, ${scriptCloseCount} closing`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const fileUrlMatches = html.match(/(?:src|href)=["']file:\/\/[^"']+["']/gi);
|
|
45
|
+
if (fileUrlMatches) {
|
|
46
|
+
fileUrlMatches.forEach((match) => {
|
|
47
|
+
errors.push(`file:// URL found (won't render): ${match.substring(0, 50)}...`);
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const imgWithoutSrc = html.match(/<img(?![^>]*\bsrc\s*=)[^>]*>/gi);
|
|
52
|
+
if (imgWithoutSrc) {
|
|
53
|
+
warnings.push(`${imgWithoutSrc.length} image(s) without src attribute`);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const bodyMatch = html.match(/<body[^>]*>([\s\S]*?)<\/body>/i);
|
|
57
|
+
if (bodyMatch) {
|
|
58
|
+
const bodyContent = bodyMatch[1].replace(/<[^>]*>/g, '').trim();
|
|
59
|
+
if (bodyContent.length === 0) {
|
|
60
|
+
const hasVisibleElements = /<(div|p|h[1-6]|span|button|input|form|table|ul|ol|li|img|a)[^>]*>/i.test(bodyMatch[1]);
|
|
61
|
+
if (!hasVisibleElements) {
|
|
62
|
+
warnings.push('Body appears to have no visible content');
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (html.includes('class="undefined"')) {
|
|
68
|
+
warnings.push('Found "undefined" in class attribute - possible template error');
|
|
69
|
+
}
|
|
70
|
+
if (html.includes('class="null"')) {
|
|
71
|
+
warnings.push('Found "null" in class attribute - possible template error');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const hasHead = /<head/i.test(html);
|
|
75
|
+
if (!hasHead) {
|
|
76
|
+
warnings.push('Missing <head> tag - styles may not load correctly');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const hasStyle = /<style/i.test(html);
|
|
80
|
+
const hasStylesheet = /<link[^>]*stylesheet/i.test(html);
|
|
81
|
+
const hasInlineStyle = /style\s*=/i.test(html);
|
|
82
|
+
if (!hasStyle && !hasStylesheet && !hasInlineStyle) {
|
|
83
|
+
warnings.push('No CSS found - mockup may appear unstyled');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
valid: errors.length === 0,
|
|
88
|
+
errors,
|
|
89
|
+
warnings,
|
|
90
|
+
html_length: html.length,
|
|
91
|
+
has_styles: hasStyle || hasStylesheet || hasInlineStyle,
|
|
92
|
+
has_doctype: /<!DOCTYPE\s+html>/i.test(html),
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const htmlFile = process.argv[2];
|
|
97
|
+
if (!htmlFile) {
|
|
98
|
+
console.error('Usage: validate_html.js <html_file>');
|
|
99
|
+
process.exit(1);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (!fs.existsSync(htmlFile)) {
|
|
103
|
+
console.error(JSON.stringify({ valid: false, errors: [`File not found: ${htmlFile}`], warnings: [] }));
|
|
104
|
+
process.exit(1);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
try {
|
|
108
|
+
const result = validateHtml(htmlFile);
|
|
109
|
+
console.log(JSON.stringify(result));
|
|
110
|
+
process.exit(result.valid ? 0 : 1);
|
|
111
|
+
} catch (e) {
|
|
112
|
+
console.error(JSON.stringify({ valid: false, errors: [`Validation error: ${e.message}`], warnings: [] }));
|
|
113
|
+
process.exit(1);
|
|
114
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Watermark strip — "Screenshot/Video created automatically by supportpages.io".
|
|
3
|
+
*
|
|
4
|
+
* Injected at CAPTURE time (render_mockup.js for article PNGs,
|
|
5
|
+
* record_walkthrough.js for walkthrough MP4s), never into the step_N.html
|
|
6
|
+
* artifacts — the fidelity lint runs before capture in both skills, so the
|
|
7
|
+
* banner needs no lint exemptions and the authored HTML stays clean.
|
|
8
|
+
*
|
|
9
|
+
* Disable via SUPPORTPAGES_WATERMARK=off (also 0/false/no); default is on.
|
|
10
|
+
* RTFM_WATERMARK is the older name and is read when the new one is unset. The
|
|
11
|
+
* `watermark` skill argument prefixes the env var onto the render/record
|
|
12
|
+
* invocation (see both SKILL.md post-process blocks).
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const WATERMARK_TEXT_SCREENSHOT = 'Screenshot created automatically by supportpages.io';
|
|
16
|
+
const WATERMARK_TEXT_VIDEO = 'Video created automatically by supportpages.io';
|
|
17
|
+
const WATERMARK_FONT = "-apple-system,BlinkMacSystemFont,'Segoe UI',system-ui,sans-serif";
|
|
18
|
+
|
|
19
|
+
// One sizing rule for both outputs, anchored to PHYSICAL pixels of the
|
|
20
|
+
// captured frame — physical size is what survives when a page embed scales
|
|
21
|
+
// the output down, and captures differ in deviceScaleFactor (article PNGs
|
|
22
|
+
// and touch recordings render at dSF 2, web/desktop recordings at dSF 1),
|
|
23
|
+
// so a plain logical-px rule made a desktop video's strip half the physical
|
|
24
|
+
// size of a mobile one. The rule: ~3.4% of frame height, clamped to a
|
|
25
|
+
// 48..70 PHYSICAL px window (expressed here in CSS px by dividing the
|
|
26
|
+
// bounds by dSF, keeping every result an integer). The 48px physical floor
|
|
27
|
+
// is what lifts the dSF-1 frames — web 900 and desktop 1100 — to the
|
|
28
|
+
// same physical text size as a mobile recording.
|
|
29
|
+
function watermarkMetrics(viewportHeight, deviceScaleFactor) {
|
|
30
|
+
const stripHeight = Math.min(
|
|
31
|
+
70 / deviceScaleFactor,
|
|
32
|
+
Math.max(48 / deviceScaleFactor, Math.round(viewportHeight * 0.034))
|
|
33
|
+
);
|
|
34
|
+
const fontSize = Math.round(stripHeight * 0.46);
|
|
35
|
+
return { stripHeight, fontSize };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Video overlay strip: fixed, full-width, appended as a body-level child
|
|
39
|
+
// OUTSIDE [data-walkthrough-stage] so the recorder's zoom transform never
|
|
40
|
+
// moves it (the ghost-cursor pattern). z-index 9999: above the step-transition
|
|
41
|
+
// fade overlay (9997) and the outro fade-to-black (9998) so the strip reads as
|
|
42
|
+
// constant broadcast chrome from first frame to last; below the cursor helper
|
|
43
|
+
// (10000) so pointers stay visually on top.
|
|
44
|
+
function buildWatermarkVideoCss(viewportHeight, deviceScaleFactor) {
|
|
45
|
+
const { stripHeight, fontSize } = watermarkMetrics(viewportHeight, deviceScaleFactor);
|
|
46
|
+
return `
|
|
47
|
+
[data-rtfm-watermark]{
|
|
48
|
+
position:fixed;left:0;right:0;bottom:0;height:${stripHeight}px;
|
|
49
|
+
display:flex;align-items:center;justify-content:center;
|
|
50
|
+
box-sizing:border-box;margin:0;padding:0;
|
|
51
|
+
background:rgba(15,23,42,0.82);color:rgba(255,255,255,0.92);
|
|
52
|
+
font:500 ${fontSize}px/1 ${WATERMARK_FONT};letter-spacing:0.02em;
|
|
53
|
+
z-index:9999;pointer-events:none;
|
|
54
|
+
}`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function isWatermarkEnabled(env = process.env) {
|
|
58
|
+
const v = String(env.SUPPORTPAGES_WATERMARK ?? env.RTFM_WATERMARK ?? 'on').trim().toLowerCase();
|
|
59
|
+
return !(v === 'off' || v === '0' || v === 'false' || v === 'no');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
module.exports = {
|
|
63
|
+
WATERMARK_TEXT_SCREENSHOT,
|
|
64
|
+
WATERMARK_TEXT_VIDEO,
|
|
65
|
+
WATERMARK_FONT,
|
|
66
|
+
watermarkMetrics,
|
|
67
|
+
buildWatermarkVideoCss,
|
|
68
|
+
isWatermarkEnabled,
|
|
69
|
+
};
|
package/install.sh
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
# A small, dependency-free bootstrap. The Node wizard handles the actual setup.
|
|
5
|
+
if ! command -v node >/dev/null 2>&1; then
|
|
6
|
+
echo 'SupportPages Writer needs Node.js 22.12 or later. Install it from https://nodejs.org, then rerun ./install.sh.' >&2
|
|
7
|
+
exit 1
|
|
8
|
+
fi
|
|
9
|
+
if ! node -e 'const [major, minor] = process.versions.node.split(".").map(Number); process.exit(major > 22 || major === 22 && minor >= 12 ? 0 : 1)'; then
|
|
10
|
+
echo 'Please upgrade to Node.js 22.12 or later, then rerun ./install.sh.' >&2
|
|
11
|
+
exit 1
|
|
12
|
+
fi
|
|
13
|
+
installer_root="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
|
14
|
+
exec node "$installer_root/scripts/install.mjs" "$@"
|
package/package.json
CHANGED
|
@@ -1,6 +1,59 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@supportpages.io/wtfm",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "SupportPages Writer — illustrated article authoring from your codebase",
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/SupportPages-io/wtfm.git"
|
|
9
|
+
},
|
|
10
|
+
"homepage": "https://github.com/SupportPages-io/wtfm#readme",
|
|
11
|
+
"bugs": {
|
|
12
|
+
"url": "https://github.com/SupportPages-io/wtfm/issues"
|
|
13
|
+
},
|
|
14
|
+
"type": "module",
|
|
15
|
+
"bin": {
|
|
16
|
+
"supportpages-writer": "scripts/cli.mjs",
|
|
17
|
+
"supportpages-mcp": "dist/index.js",
|
|
18
|
+
"supportpages-setup": "scripts/install.mjs",
|
|
19
|
+
"supportpages": "scripts/cli.mjs",
|
|
20
|
+
"wtfm": "scripts/cli.mjs"
|
|
21
|
+
},
|
|
22
|
+
"files": [
|
|
23
|
+
"dist",
|
|
24
|
+
"engine",
|
|
25
|
+
"scripts",
|
|
26
|
+
"install.sh",
|
|
27
|
+
"README.md",
|
|
28
|
+
"LICENSE",
|
|
29
|
+
"NOTICE",
|
|
30
|
+
"server.json"
|
|
31
|
+
],
|
|
32
|
+
"publishConfig": {
|
|
33
|
+
"access": "public"
|
|
34
|
+
},
|
|
35
|
+
"engines": {
|
|
36
|
+
"node": ">=22.12.0"
|
|
37
|
+
},
|
|
38
|
+
"scripts": {
|
|
39
|
+
"build": "tsc -p tsconfig.json",
|
|
40
|
+
"test": "npm run build && SUPPORTPAGES_TELEMETRY=0 node --test test/*.test.mjs",
|
|
41
|
+
"start": "node dist/index.js",
|
|
42
|
+
"prepack": "npm run build",
|
|
43
|
+
"setup": "node scripts/install.mjs",
|
|
44
|
+
"test:scenarios": "npm run build && node --test test/access-scenarios.test.mjs test/cli-scenarios.test.mjs"
|
|
45
|
+
},
|
|
46
|
+
"dependencies": {
|
|
47
|
+
"@clack/prompts": "1.6.0",
|
|
48
|
+
"@modelcontextprotocol/sdk": "1.30.0",
|
|
49
|
+
"marked": "17.0.6",
|
|
50
|
+
"puppeteer": "25.11.0",
|
|
51
|
+
"yauzl": "3.4.0",
|
|
52
|
+
"zod": "4.4.3"
|
|
53
|
+
},
|
|
54
|
+
"devDependencies": {
|
|
55
|
+
"@types/node": "22.20.1",
|
|
56
|
+
"typescript": "5.9.3"
|
|
57
|
+
},
|
|
58
|
+
"mcpName": "io.github.supportpages-io/supportpages-writer"
|
|
59
|
+
}
|
package/scripts/auth.mjs
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { parseArgs } from 'node:util';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { pathToFileURL } from 'node:url';
|
|
5
|
+
import { createTerminal } from './lib/terminal.mjs';
|
|
6
|
+
import { apiOrigin, ApiClient, defaultOrigin } from '../dist/api.js';
|
|
7
|
+
import { credentialLocation, defaultConfigDir } from '../dist/session.js';
|
|
8
|
+
import { saveTokenFile } from '../dist/credentials.js';
|
|
9
|
+
import { CLI_NAME } from './lib/brand.mjs';
|
|
10
|
+
|
|
11
|
+
/** Manual fallback: paste a token created in the web app and save it as this device's credential. */
|
|
12
|
+
export async function authenticate(options, { ui, fetcher = fetch } = {}) {
|
|
13
|
+
const dev = options.dev === true;
|
|
14
|
+
const origin = apiOrigin(options['api-url'] ?? defaultOrigin(dev), dev);
|
|
15
|
+
ui.line(`Sign this device in to ${origin} with a token\nCreate a token for all help centres at ${origin}/settings/local-api-tokens\nUse read and import permissions, plus create projects and publish if required.`);
|
|
16
|
+
const token = (await ui.secret('Paste API token (hidden)')).trim();
|
|
17
|
+
if (!/^sp_local_[a-f0-9]{64}$/.test(token)) throw new Error('Invalid token format. No credential was saved.');
|
|
18
|
+
const settings = await new ApiClient(origin, token, fetcher, dev).request('GET', '/mcp/settings');
|
|
19
|
+
const account = settings?.account;
|
|
20
|
+
if (!account || typeof account.id !== 'string' || !/^[1-9][0-9]*$/.test(account.id) || typeof account.email !== 'string') throw new Error('The API returned an invalid account. No credential was saved.');
|
|
21
|
+
const { filename } = credentialLocation(origin, path.resolve(options['config-dir'] ?? defaultConfigDir()));
|
|
22
|
+
await saveTokenFile(filename, origin, token, { id: account.id, email: account.email });
|
|
23
|
+
ui.line(`Token verified and saved privately for ${account.email}. Run ${CLI_NAME} init inside a project folder to connect it to a help centre.`);
|
|
24
|
+
}
|
|
25
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href) {
|
|
26
|
+
try {
|
|
27
|
+
let options;
|
|
28
|
+
try { options = parseArgs({ options: { workspace: { type: 'string' }, 'api-url': { type: 'string' }, 'config-dir': { type: 'string' }, dev: { type: 'boolean' } } }).values; }
|
|
29
|
+
catch { throw new Error('Invalid authentication options. Tokens must only be entered at the hidden prompt.'); }
|
|
30
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) throw new Error('Run this command in your own interactive terminal.');
|
|
31
|
+
await authenticate(options, { ui: createTerminal() });
|
|
32
|
+
} catch (error) { process.stderr.write(`${error.message}\n`); process.exitCode = 1; }
|
|
33
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { automaticUpdate } from './lib/auto-update.mjs';
|
|
5
|
+
|
|
6
|
+
// This worker is launched with all streams disconnected from the MCP transport.
|
|
7
|
+
await automaticUpdate({ installRoot: path.dirname(path.dirname(fileURLToPath(import.meta.url))) });
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Assemble the installable plugin tree that the `plugin` branch publishes.
|
|
3
|
+
//
|
|
4
|
+
// Claude Code and Codex install plugins by copying a git checkout. Neither
|
|
5
|
+
// builds TypeScript, and Codex installs no npm dependencies, so the tree ships
|
|
6
|
+
// compiled dist/ and production node_modules. Chromium is not included: the
|
|
7
|
+
// plugin's SessionStart hook downloads it on first use.
|
|
8
|
+
//
|
|
9
|
+
// Usage: node scripts/build-plugin.mjs --out <directory> [--skip-install]
|
|
10
|
+
// --skip-install leave out node_modules (tests)
|
|
11
|
+
import { cp, mkdir, readFile, readdir, rm, writeFile } from 'node:fs/promises';
|
|
12
|
+
import path from 'node:path';
|
|
13
|
+
import { parseArgs } from 'node:util';
|
|
14
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
15
|
+
import { command, exists } from './lib/install.mjs';
|
|
16
|
+
|
|
17
|
+
export const repositoryRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
18
|
+
const PACKAGE_FILES = ['dist', 'engine', 'scripts', 'package.json', 'package-lock.json', 'LICENSE', 'NOTICE', 'README.md'];
|
|
19
|
+
|
|
20
|
+
export async function buildPlugin({ out, root = repositoryRoot, install = true, run = command } = {}) {
|
|
21
|
+
if (!await exists(path.join(root, 'dist/writer-agent.js'))) throw Error('Build the server first: npm run build.');
|
|
22
|
+
const pkg = JSON.parse(await readFile(path.join(root, 'package.json'), 'utf8'));
|
|
23
|
+
out = path.resolve(out);
|
|
24
|
+
await mkdir(out, { recursive: true });
|
|
25
|
+
// Replace everything except the branch's own git metadata.
|
|
26
|
+
for (const name of await readdir(out)) if (name !== '.git') await rm(path.join(out, name), { recursive: true, force: true });
|
|
27
|
+
|
|
28
|
+
await cp(path.join(root, 'plugin'), out, { recursive: true });
|
|
29
|
+
const manifest = path.join(out, '.claude-plugin/plugin.json');
|
|
30
|
+
const plugin = JSON.parse(await readFile(manifest, 'utf8'));
|
|
31
|
+
await writeFile(manifest, JSON.stringify({ ...plugin, version: pkg.version }, null, 2) + '\n');
|
|
32
|
+
|
|
33
|
+
// The writer subagent, from the same definition the CLI installs.
|
|
34
|
+
const { writerAgentFile, writerAgentType } = await import(pathToFileURL(path.join(root, 'dist/writer-agent.js')).href);
|
|
35
|
+
await mkdir(path.join(out, 'agents'), { recursive: true });
|
|
36
|
+
await writeFile(path.join(out, 'agents', `${writerAgentType}.md`), writerAgentFile);
|
|
37
|
+
|
|
38
|
+
for (const name of PACKAGE_FILES) await cp(path.join(root, name), path.join(out, name), { recursive: true });
|
|
39
|
+
|
|
40
|
+
if (install) {
|
|
41
|
+
// --ignore-scripts skips puppeteer's Chromium download, as plugin installs do.
|
|
42
|
+
const result = await run('npm', ['ci', '--omit=dev', '--ignore-scripts', '--no-audit', '--no-fund'], { cwd: out });
|
|
43
|
+
if (result.code) throw Error('npm ci failed while building the plugin.');
|
|
44
|
+
}
|
|
45
|
+
return { out, version: pkg.version };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href) {
|
|
49
|
+
try {
|
|
50
|
+
const { values } = parseArgs({ options: { out: { type: 'string' }, 'skip-install': { type: 'boolean' } } });
|
|
51
|
+
if (!values.out) throw Error('Usage: build-plugin.mjs --out <directory> [--skip-install]');
|
|
52
|
+
const result = await buildPlugin({ out: values.out, install: !values['skip-install'] });
|
|
53
|
+
process.stdout.write(`${result.out} (${result.version})\n`);
|
|
54
|
+
} catch (error) {
|
|
55
|
+
process.stderr.write(error.message + '\n');
|
|
56
|
+
process.exitCode = 1;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { cp, mkdir, mkdtemp, readFile, readdir, rm, writeFile, realpath, chmod } from 'node:fs/promises';
|
|
3
|
+
import { createHash } from 'node:crypto';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import os from 'node:os';
|
|
6
|
+
import { parseArgs } from 'node:util';
|
|
7
|
+
import { fileURLToPath } from 'node:url';
|
|
8
|
+
import { command, checkSkills, exists } from './lib/install.mjs';
|
|
9
|
+
|
|
10
|
+
const {values}=parseArgs({options:{local:{type:'boolean'},'runtime-dir':{type:'string'},out:{type:'string'}}});
|
|
11
|
+
if(values.local && values['runtime-dir']) throw Error('--local uses this machine’s runtime. Use --runtime-dir for a public release.');
|
|
12
|
+
if(!values.local && !values['runtime-dir']) throw Error('Use --local, or --runtime-dir with a built checkout.');
|
|
13
|
+
const root=path.resolve(path.dirname(fileURLToPath(import.meta.url)),'..');
|
|
14
|
+
// The article engine ships in this repository (exported from the skills repository).
|
|
15
|
+
const engine=path.join(root,'engine');
|
|
16
|
+
await checkSkills(engine);
|
|
17
|
+
const version=JSON.parse(await readFile(path.join(root,'package.json'),'utf8')).version;
|
|
18
|
+
const lock=JSON.parse(await readFile(path.join(root,'package-lock.json'),'utf8'));
|
|
19
|
+
if(lock.version!==version || lock.packages?.['']?.version!==version) throw Error('Keep package.json and package-lock.json release versions aligned.');
|
|
20
|
+
if(!/^\d+\.\d+\.\d+$/.test(version)) throw Error('Release versions must be X.Y.Z.');
|
|
21
|
+
if(!['darwin','linux'].includes(process.platform) || !['x64','arm64'].includes(process.arch)) throw Error('Unsupported release platform.');
|
|
22
|
+
let npmRoot;
|
|
23
|
+
if(values.local) {
|
|
24
|
+
const [nodeMajor, nodeMinor] = process.versions.node.split('.').map(Number);
|
|
25
|
+
if(nodeMajor < 22 || nodeMajor === 22 && nodeMinor < 12) throw Error('Local builds require Node.js 22.12 or later.');
|
|
26
|
+
// Find npm next to Node first (nvm/official distributions), then on PATH
|
|
27
|
+
// (Homebrew). Copy its package, not the entire system runtime prefix.
|
|
28
|
+
for(const directory of [path.dirname(process.execPath),...(process.env.PATH ?? '').split(path.delimiter).filter(Boolean)]) {
|
|
29
|
+
const candidate=path.join(directory,'npm');
|
|
30
|
+
if(!await exists(candidate)) continue;
|
|
31
|
+
const executable=await realpath(candidate);
|
|
32
|
+
const candidateRoot=path.dirname(path.dirname(executable));
|
|
33
|
+
if(await exists(path.join(candidateRoot,'bin/npm-cli.js'))) {npmRoot=candidateRoot;break;}
|
|
34
|
+
}
|
|
35
|
+
if(!npmRoot) throw Error('Could not find npm alongside Node or on PATH. Install npm and rerun the installer.');
|
|
36
|
+
const npmArgs=[path.join(npmRoot,'bin/npm-cli.js')];
|
|
37
|
+
const env={...process.env,PATH:path.dirname(process.execPath)+path.delimiter+(process.env.PATH ?? '')};
|
|
38
|
+
const checked=async args=>{
|
|
39
|
+
const result=await command(process.execPath,[...npmArgs,...args],{cwd:root,capture:true,env});
|
|
40
|
+
if(result.stdout) process.stderr.write(result.stdout);
|
|
41
|
+
if(result.stderr) process.stderr.write(result.stderr);
|
|
42
|
+
if(result.code) throw Error(`npm ${args[0]} failed. Fix the reported issue and rerun the installer.`);
|
|
43
|
+
};
|
|
44
|
+
const dependencies=await command(process.execPath,[...npmArgs,'ls','--include=dev','--all'],{cwd:root,capture:true,env});
|
|
45
|
+
if(dependencies.code) {
|
|
46
|
+
process.stderr.write('Installing build dependencies...\n');
|
|
47
|
+
await checked(['ci','--include=dev','--no-audit','--no-fund','--prefer-offline']);
|
|
48
|
+
}
|
|
49
|
+
process.stderr.write('Building SupportPages Writer...\n');
|
|
50
|
+
await checked(['run','build']);
|
|
51
|
+
} else {
|
|
52
|
+
npmRoot=path.dirname(path.dirname(await realpath(path.join(values['runtime-dir'],'bin/npm'))));
|
|
53
|
+
}
|
|
54
|
+
const stage=await mkdtemp(path.join(os.tmpdir(),'supportpages-release-'));
|
|
55
|
+
const out=path.resolve(values.out ?? path.join(root,'release'));
|
|
56
|
+
try {
|
|
57
|
+
const bundle=path.join(stage,'supportpages');await mkdir(path.join(bundle,'mcp'),{recursive:true});
|
|
58
|
+
if(values.local) {
|
|
59
|
+
await mkdir(path.join(bundle,'runtime/bin'),{recursive:true});
|
|
60
|
+
await cp(process.execPath,path.join(bundle,'runtime/bin/node'),{dereference:true});
|
|
61
|
+
// Homebrew's Node executable loads libnode relative to its runtime prefix.
|
|
62
|
+
// Copy that library too, without pulling in unrelated system packages.
|
|
63
|
+
const lib=path.resolve(path.dirname(process.execPath),'../lib');
|
|
64
|
+
if(await exists(lib)) for(const name of await readdir(lib)) {
|
|
65
|
+
if(!/^libnode[.-]/.test(name)) continue;
|
|
66
|
+
await mkdir(path.join(bundle,'runtime/lib'),{recursive:true});
|
|
67
|
+
await cp(path.join(lib,name),path.join(bundle,'runtime/lib',name),{dereference:true});
|
|
68
|
+
}
|
|
69
|
+
} else await cp(values['runtime-dir'],path.join(bundle,'runtime'),{recursive:true,dereference:true});
|
|
70
|
+
await cp(npmRoot,path.join(bundle,'runtime/lib/node_modules/npm'),{recursive:true,dereference:true});
|
|
71
|
+
for(const name of ['npm','npx']) {
|
|
72
|
+
const script=path.join(bundle,'runtime/bin',name);
|
|
73
|
+
await writeFile(script,`#!/bin/sh\nsp_runtime_bin="$(cd -- "$(dirname -- "$0")" && pwd)"\nexec "$sp_runtime_bin/node" "$sp_runtime_bin/../lib/node_modules/npm/bin/${name}-cli.js" "$@"\n`);
|
|
74
|
+
await chmod(script,0o755);
|
|
75
|
+
}
|
|
76
|
+
const npmProbe=await command(path.join(bundle,'runtime/bin/npm'),['--version'],{capture:true});
|
|
77
|
+
if(npmProbe.code) {process.stderr.write(npmProbe.stderr);throw Error('Bundled npm smoke test failed.');}
|
|
78
|
+
for(const name of ['dist','scripts','engine','install.sh','install-cli.sh','package.json','package-lock.json','README.md','LICENSE','NOTICE','node_modules']) await cp(path.join(root,name),path.join(bundle,'mcp',name),{recursive:true,dereference:true});
|
|
79
|
+
await rm(path.join(bundle,'mcp/node_modules/typescript'),{recursive:true,force:true});
|
|
80
|
+
await rm(path.join(bundle,'mcp/node_modules/@types'),{recursive:true,force:true});
|
|
81
|
+
const probe=await command(path.join(bundle,'runtime/bin/node'),[path.join(bundle,'mcp/scripts/cli.mjs'),'--version'],{capture:true});
|
|
82
|
+
if(probe.code || probe.stdout.trim()!==version) {process.stderr.write(probe.stderr);throw Error('Bundled runtime smoke test failed.');}
|
|
83
|
+
await writeFile(path.join(bundle,'release.json'),JSON.stringify({version,platform:process.platform,arch:process.arch,node:process.version,engine:(await readFile(path.join(engine,'VERSION'),'utf8')).trim(),...(values.local ? {local:true} : {})})+'\n');
|
|
84
|
+
await mkdir(out,{recursive:true});
|
|
85
|
+
const archive=path.join(out,`supportpages-${version}-${process.platform}-${process.arch}.tar.gz`);
|
|
86
|
+
// -h stores symlink targets as ordinary files, so extraction needs no link
|
|
87
|
+
// support or trust. The stage was copied with dereference, so it holds no
|
|
88
|
+
// hardlinks either. COPYFILE_DISABLE keeps macOS metadata out of the archive.
|
|
89
|
+
const result=await command('tar',['-czhf',archive,'-C',stage,'supportpages'],{capture:true,env:{...process.env,COPYFILE_DISABLE:'1'}});
|
|
90
|
+
if(result.code) throw Error('Could not create the release archive.');
|
|
91
|
+
const digest=createHash('sha256').update(await readFile(archive)).digest('hex');
|
|
92
|
+
await writeFile(archive+'.sha256',`${digest} ${path.basename(archive)}\n`);
|
|
93
|
+
process.stdout.write(archive+'\n');
|
|
94
|
+
} finally {await rm(stage,{recursive:true,force:true});}
|