@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.
Files changed (275) hide show
  1. package/LICENSE +201 -0
  2. package/NOTICE +23 -0
  3. package/README.md +392 -2
  4. package/dist/actions.d.ts +98 -0
  5. package/dist/actions.js +86 -0
  6. package/dist/actions.js.map +1 -0
  7. package/dist/agent-settings.d.ts +22 -0
  8. package/dist/agent-settings.js +35 -0
  9. package/dist/agent-settings.js.map +1 -0
  10. package/dist/api.d.ts +14 -0
  11. package/dist/api.js +159 -0
  12. package/dist/api.js.map +1 -0
  13. package/dist/article-link.d.ts +16 -0
  14. package/dist/article-link.js +25 -0
  15. package/dist/article-link.js.map +1 -0
  16. package/dist/artifacts.d.ts +45 -0
  17. package/dist/artifacts.js +144 -0
  18. package/dist/artifacts.js.map +1 -0
  19. package/dist/brand.d.ts +4 -0
  20. package/dist/brand.js +9 -0
  21. package/dist/brand.js.map +1 -0
  22. package/dist/bridge.d.ts +2964 -0
  23. package/dist/bridge.js +1036 -0
  24. package/dist/bridge.js.map +1 -0
  25. package/dist/capacity.d.ts +5 -0
  26. package/dist/capacity.js +11 -0
  27. package/dist/capacity.js.map +1 -0
  28. package/dist/credentials.d.ts +13 -0
  29. package/dist/credentials.js +88 -0
  30. package/dist/credentials.js.map +1 -0
  31. package/dist/development-tls.d.ts +8 -0
  32. package/dist/development-tls.js +38 -0
  33. package/dist/development-tls.js.map +1 -0
  34. package/dist/errors.d.ts +15 -0
  35. package/dist/errors.js +18 -0
  36. package/dist/errors.js.map +1 -0
  37. package/dist/export.d.ts +11 -0
  38. package/dist/export.js +91 -0
  39. package/dist/export.js.map +1 -0
  40. package/dist/hosted-operations.d.ts +108 -0
  41. package/dist/hosted-operations.js +120 -0
  42. package/dist/hosted-operations.js.map +1 -0
  43. package/dist/hosting-benefits.d.ts +66 -0
  44. package/dist/hosting-benefits.js +67 -0
  45. package/dist/hosting-benefits.js.map +1 -0
  46. package/dist/index.d.ts +2 -0
  47. package/dist/index.js +49 -0
  48. package/dist/index.js.map +1 -0
  49. package/dist/local-inventory.d.ts +22 -0
  50. package/dist/local-inventory.js +59 -0
  51. package/dist/local-inventory.js.map +1 -0
  52. package/dist/local-setup.d.ts +208 -0
  53. package/dist/local-setup.js +140 -0
  54. package/dist/local-setup.js.map +1 -0
  55. package/dist/pairing.d.ts +71 -0
  56. package/dist/pairing.js +235 -0
  57. package/dist/pairing.js.map +1 -0
  58. package/dist/preferences.d.ts +18 -0
  59. package/dist/preferences.js +44 -0
  60. package/dist/preferences.js.map +1 -0
  61. package/dist/progress.d.ts +205 -0
  62. package/dist/progress.js +224 -0
  63. package/dist/progress.js.map +1 -0
  64. package/dist/reminders.d.ts +31 -0
  65. package/dist/reminders.js +73 -0
  66. package/dist/reminders.js.map +1 -0
  67. package/dist/replace-connection.d.ts +6 -0
  68. package/dist/replace-connection.js +77 -0
  69. package/dist/replace-connection.js.map +1 -0
  70. package/dist/repository-actions.d.ts +18 -0
  71. package/dist/repository-actions.js +9 -0
  72. package/dist/repository-actions.js.map +1 -0
  73. package/dist/repository-benefits.d.ts +96 -0
  74. package/dist/repository-benefits.js +64 -0
  75. package/dist/repository-benefits.js.map +1 -0
  76. package/dist/run-update.d.ts +17 -0
  77. package/dist/run-update.js +27 -0
  78. package/dist/run-update.js.map +1 -0
  79. package/dist/runs.d.ts +541 -0
  80. package/dist/runs.js +146 -0
  81. package/dist/runs.js.map +1 -0
  82. package/dist/runtime.d.ts +12 -0
  83. package/dist/runtime.js +58 -0
  84. package/dist/runtime.js.map +1 -0
  85. package/dist/schema.d.ts +277 -0
  86. package/dist/schema.js +66 -0
  87. package/dist/schema.js.map +1 -0
  88. package/dist/server.d.ts +4 -0
  89. package/dist/server.js +298 -0
  90. package/dist/server.js.map +1 -0
  91. package/dist/session.d.ts +1714 -0
  92. package/dist/session.js +619 -0
  93. package/dist/session.js.map +1 -0
  94. package/dist/settings.d.ts +9 -0
  95. package/dist/settings.js +21 -0
  96. package/dist/settings.js.map +1 -0
  97. package/dist/sync.d.ts +495 -0
  98. package/dist/sync.js +191 -0
  99. package/dist/sync.js.map +1 -0
  100. package/dist/telemetry-scrub.d.ts +13 -0
  101. package/dist/telemetry-scrub.js +51 -0
  102. package/dist/telemetry-scrub.js.map +1 -0
  103. package/dist/telemetry.d.ts +73 -0
  104. package/dist/telemetry.js +173 -0
  105. package/dist/telemetry.js.map +1 -0
  106. package/dist/walkthroughs.d.ts +224 -0
  107. package/dist/walkthroughs.js +109 -0
  108. package/dist/walkthroughs.js.map +1 -0
  109. package/dist/workspace.d.ts +14 -0
  110. package/dist/workspace.js +127 -0
  111. package/dist/workspace.js.map +1 -0
  112. package/dist/writer-agent.d.ts +21 -0
  113. package/dist/writer-agent.js +27 -0
  114. package/dist/writer-agent.js.map +1 -0
  115. package/dist/writer-entry.d.ts +171 -0
  116. package/dist/writer-entry.js +233 -0
  117. package/dist/writer-entry.js.map +1 -0
  118. package/dist/writing-style.d.ts +7 -0
  119. package/dist/writing-style.js +52 -0
  120. package/dist/writing-style.js.map +1 -0
  121. package/engine/SYNC.json +4 -0
  122. package/engine/VERSION +1 -0
  123. package/engine/detect-project/README.md +141 -0
  124. package/engine/detect-project/SKILL.md +1421 -0
  125. package/engine/detect-project/assets/desktop/desktop-frame.css +428 -0
  126. package/engine/detect-project/assets/game/game-frame.css +132 -0
  127. package/engine/detect-project/assets/macosui/LICENSE-puppertino.txt +21 -0
  128. package/engine/detect-project/assets/macosui/VERSIONS.txt +1 -0
  129. package/engine/detect-project/assets/macosui/fonts.css +15 -0
  130. package/engine/detect-project/assets/macosui/macos-frame.css +481 -0
  131. package/engine/detect-project/assets/macosui/puppertino.css +2153 -0
  132. package/engine/detect-project/assets/mobileui/LICENSE-fonts.txt +13 -0
  133. package/engine/detect-project/assets/mobileui/LICENSE-framework7.txt +52 -0
  134. package/engine/detect-project/assets/mobileui/VERSIONS.txt +6 -0
  135. package/engine/detect-project/assets/mobileui/device-frame.css +316 -0
  136. package/engine/detect-project/assets/mobileui/f7-color-theme.mjs +1345 -0
  137. package/engine/detect-project/assets/mobileui/f7-icons-names.json +1254 -0
  138. package/engine/detect-project/assets/mobileui/fonts.css +16 -0
  139. package/engine/detect-project/assets/mobileui/framework7-components.css +39 -0
  140. package/engine/detect-project/assets/mobileui/framework7-core.css +5245 -0
  141. package/engine/detect-project/assets/mobileui/icons.css +31 -0
  142. package/engine/detect-project/assets/mobileui/md3-defaults.css +89 -0
  143. package/engine/detect-project/assets/mobileui/platforms.json +46 -0
  144. package/engine/detect-project/assets/tailwind-fallback.css +1729 -0
  145. package/engine/detect-project/assets/webtui/LICENSE-webtui.txt +28 -0
  146. package/engine/detect-project/assets/webtui/VERSIONS.txt +7 -0
  147. package/engine/detect-project/assets/webtui/terminal-frame.css +195 -0
  148. package/engine/detect-project/assets/webtui/theme-catppuccin.css +1 -0
  149. package/engine/detect-project/assets/webtui/theme-everforest.css +1 -0
  150. package/engine/detect-project/assets/webtui/theme-gruvbox.css +1 -0
  151. package/engine/detect-project/assets/webtui/theme-nord.css +1 -0
  152. package/engine/detect-project/assets/webtui/theme-vitesse.css +1 -0
  153. package/engine/detect-project/assets/webtui/themes.json +37 -0
  154. package/engine/detect-project/assets/webtui/webtui-core.css +1 -0
  155. package/engine/detect-project/assets/win32ui/7css.css +2 -0
  156. package/engine/detect-project/assets/win32ui/LICENSE-7css.txt +21 -0
  157. package/engine/detect-project/assets/win32ui/VERSIONS.txt +1 -0
  158. package/engine/detect-project/assets/win32ui/win32-frame.css +278 -0
  159. package/engine/detect-project/package-lock.json +12 -0
  160. package/engine/detect-project/package.json +10 -0
  161. package/engine/detect-project/scripts/apply_runtime_profiles.js +313 -0
  162. package/engine/detect-project/scripts/check_css_health.js +412 -0
  163. package/engine/detect-project/scripts/check_project_map.js +150 -0
  164. package/engine/detect-project/scripts/check_runtime_coverage.js +311 -0
  165. package/engine/detect-project/scripts/check_runtime_recipe_quality.js +184 -0
  166. package/engine/detect-project/scripts/classify_app_type.sh +246 -0
  167. package/engine/detect-project/scripts/classify_surface.sh +95 -0
  168. package/engine/detect-project/scripts/classify_workspace.js +39 -0
  169. package/engine/detect-project/scripts/compile_css.sh +447 -0
  170. package/engine/detect-project/scripts/detect_static.js +645 -0
  171. package/engine/detect-project/scripts/detect_structure.js +187 -0
  172. package/engine/detect-project/scripts/include_census.js +451 -0
  173. package/engine/detect-project/scripts/json_get.js +142 -0
  174. package/engine/detect-project/scripts/merge_json.js +52 -0
  175. package/engine/detect-project/scripts/recommend_model_tier.js +252 -0
  176. package/engine/detect-project/scripts/resolve_route_chains.js +135 -0
  177. package/engine/detect-project/scripts/run_css_build.sh +40 -0
  178. package/engine/detect-project/scripts/sanitize_css.js +83 -0
  179. package/engine/detect-project/scripts/test_classify_surface.js +108 -0
  180. package/engine/detect-project/scripts/test_node_helpers.js +142 -0
  181. package/engine/detect-project/scripts/test_recommend_model_tier.js +119 -0
  182. package/engine/detect-project/scripts/test_resolve_route_chains.js +182 -0
  183. package/engine/detect-project/scripts/test_runtime_coverage.js +323 -0
  184. package/engine/detect-project/scripts/test_runtime_recipe_quality.js +187 -0
  185. package/engine/detect-project/scripts/theme_overrides.js +169 -0
  186. package/engine/detect-project/scripts/write_branding.js +129 -0
  187. package/engine/generate-illustrated-article/SKILL.md +461 -0
  188. package/engine/generate-illustrated-article/contracts/desktop.md +90 -0
  189. package/engine/generate-illustrated-article/contracts/game.md +38 -0
  190. package/engine/generate-illustrated-article/contracts/label-evidence.md +38 -0
  191. package/engine/generate-illustrated-article/contracts/macos.md +79 -0
  192. package/engine/generate-illustrated-article/contracts/mobile.md +40 -0
  193. package/engine/generate-illustrated-article/contracts/terminal.md +29 -0
  194. package/engine/generate-illustrated-article/contracts/win32.md +43 -0
  195. package/engine/generate-illustrated-article/package-lock.json +366 -0
  196. package/engine/generate-illustrated-article/package.json +15 -0
  197. package/engine/generate-illustrated-article/scripts/article_blocks.js +34 -0
  198. package/engine/generate-illustrated-article/scripts/check_article_json.js +105 -0
  199. package/engine/generate-illustrated-article/scripts/emit_walkthrough_signals.js +100 -0
  200. package/engine/generate-illustrated-article/scripts/extract_images.js +187 -0
  201. package/engine/generate-illustrated-article/scripts/generate_content_images.js +376 -0
  202. package/engine/generate-illustrated-article/scripts/include_census.js +451 -0
  203. package/engine/generate-illustrated-article/scripts/inject_assets.js +1691 -0
  204. package/engine/generate-illustrated-article/scripts/jit_mockup_css.js +191 -0
  205. package/engine/generate-illustrated-article/scripts/label_evidence.js +87 -0
  206. package/engine/generate-illustrated-article/scripts/lint_article_copy.js +252 -0
  207. package/engine/generate-illustrated-article/scripts/lint_mockup_fidelity.js +2403 -0
  208. package/engine/generate-illustrated-article/scripts/polish_tickets.js +1148 -0
  209. package/engine/generate-illustrated-article/scripts/related_repos.sh +52 -0
  210. package/engine/generate-illustrated-article/scripts/render_all.js +177 -0
  211. package/engine/generate-illustrated-article/scripts/render_mockup.js +665 -0
  212. package/engine/generate-illustrated-article/scripts/render_ready.js +164 -0
  213. package/engine/generate-illustrated-article/scripts/resolve_workspace.sh +86 -0
  214. package/engine/generate-illustrated-article/scripts/runtime_region_geometry.js +55 -0
  215. package/engine/generate-illustrated-article/scripts/source_paths.js +49 -0
  216. package/engine/generate-illustrated-article/scripts/test_control_visibility.js +30 -0
  217. package/engine/generate-illustrated-article/scripts/test_emit_walkthrough_signals.js +157 -0
  218. package/engine/generate-illustrated-article/scripts/test_generate_content_images.js +168 -0
  219. package/engine/generate-illustrated-article/scripts/test_include_census.js +126 -0
  220. package/engine/generate-illustrated-article/scripts/test_label_evidence.js +49 -0
  221. package/engine/generate-illustrated-article/scripts/test_lint_article_copy.js +138 -0
  222. package/engine/generate-illustrated-article/scripts/test_lint_mockup_fidelity.js +653 -0
  223. package/engine/generate-illustrated-article/scripts/test_node_ports.js +152 -0
  224. package/engine/generate-illustrated-article/scripts/test_polish_tickets.js +481 -0
  225. package/engine/generate-illustrated-article/scripts/test_related_repos.js +64 -0
  226. package/engine/generate-illustrated-article/scripts/test_render_ready.js +85 -0
  227. package/engine/generate-illustrated-article/scripts/test_source_paths.js +68 -0
  228. package/engine/generate-illustrated-article/scripts/trace_hook.js +79 -0
  229. package/engine/generate-illustrated-article/scripts/trace_hook.sh +4 -0
  230. package/engine/generate-illustrated-article/scripts/validate_html.js +114 -0
  231. package/engine/generate-illustrated-article/scripts/watermark.js +69 -0
  232. package/install.sh +14 -0
  233. package/package.json +57 -4
  234. package/scripts/auth.mjs +33 -0
  235. package/scripts/auto-update.mjs +7 -0
  236. package/scripts/build-plugin.mjs +58 -0
  237. package/scripts/build-release.mjs +94 -0
  238. package/scripts/check-release.mjs +40 -0
  239. package/scripts/check-runtime.mjs +10 -0
  240. package/scripts/cli.mjs +8 -0
  241. package/scripts/install.mjs +64 -0
  242. package/scripts/lib/agent-runner.mjs +181 -0
  243. package/scripts/lib/agent-settings.mjs +215 -0
  244. package/scripts/lib/article-skills.mjs +82 -0
  245. package/scripts/lib/auto-update.mjs +50 -0
  246. package/scripts/lib/brand.mjs +9 -0
  247. package/scripts/lib/browser.mjs +12 -0
  248. package/scripts/lib/claude-connection.mjs +33 -0
  249. package/scripts/lib/claude-permissions.mjs +47 -0
  250. package/scripts/lib/claude-plugin.mjs +15 -0
  251. package/scripts/lib/claude-writer.mjs +10 -0
  252. package/scripts/lib/cli-main.mjs +85 -0
  253. package/scripts/lib/cli.mjs +888 -0
  254. package/scripts/lib/codex-config.mjs +51 -0
  255. package/scripts/lib/codex-integration.mjs +61 -0
  256. package/scripts/lib/codex-skill.mjs +34 -0
  257. package/scripts/lib/harness-models.mjs +109 -0
  258. package/scripts/lib/install.mjs +269 -0
  259. package/scripts/lib/managed-writer.mjs +38 -0
  260. package/scripts/lib/planning.mjs +263 -0
  261. package/scripts/lib/prepare-update.mjs +85 -0
  262. package/scripts/lib/refresh-writers.mjs +12 -0
  263. package/scripts/lib/remove.mjs +167 -0
  264. package/scripts/lib/renderer.mjs +34 -0
  265. package/scripts/lib/terminal.mjs +252 -0
  266. package/scripts/lib/uninit.mjs +72 -0
  267. package/scripts/lib/update.mjs +55 -0
  268. package/scripts/lib/writer-recovery.mjs +35 -0
  269. package/scripts/lib/yolo.mjs +362 -0
  270. package/scripts/plugin-session.mjs +65 -0
  271. package/scripts/prepare-update.mjs +12 -0
  272. package/scripts/publish-release.mjs +92 -0
  273. package/scripts/skills/supportpages/SKILL.md +120 -0
  274. package/scripts/smoke-release.mjs +101 -0
  275. package/server.json +27 -0
@@ -0,0 +1,50 @@
1
+ import path from 'node:path';
2
+ import { mkdir, readFile, rm, stat } from 'node:fs/promises';
3
+ import { autoUpdateEnabled, installationRoot } from '../../dist/runtime.js';
4
+ import { releaseInstallation, updateCli } from './update.mjs';
5
+ import { privateJson } from './install.mjs';
6
+
7
+ export const CHECK_INTERVAL = 24 * 60 * 60 * 1000;
8
+
9
+ /** An installation-wide lock plus a persisted attempt time prevents every agent
10
+ * session checking separately. Failed checks are throttled too; manual updates
11
+ * don't consult this state. Only the installer promotes a verified release.
12
+ */
13
+ export async function automaticUpdate({ installRoot, env = process.env, now = Date.now, update = updateCli }) {
14
+ if (!autoUpdateEnabled(env)) return;
15
+ const root = installationRoot(installRoot);
16
+ if (!root) return;
17
+ const updateEnv = { ...env, SUPPORTPAGES_CLI_HOME: path.join(root, 'current') };
18
+ const lock = path.join(root, '.auto-update.lock');
19
+ const recovery = path.join(root, '.auto-update-recovery.lock');
20
+ const state = path.join(root, 'auto-update.json');
21
+ let locked = false;
22
+ try {
23
+ // An old server still running after an update cannot update on its behalf.
24
+ await releaseInstallation(installRoot, updateEnv);
25
+ try { await mkdir(lock, { mode: 0o700 }); }
26
+ catch (error) {
27
+ if (error.code !== 'EEXIST') return;
28
+ // Recover an abandoned worker lock on a later day. Installer downloads
29
+ // are time-bounded, so an attempt can never normally hold it this long.
30
+ if (now() - (await stat(lock)).mtimeMs < CHECK_INTERVAL) return;
31
+ // Serialize recovery and recheck: another worker may already have
32
+ // replaced the stale lock with a live one while we were waiting.
33
+ try { await mkdir(recovery, { mode: 0o700 }); } catch { return; }
34
+ try {
35
+ if (now() - (await stat(lock)).mtimeMs < CHECK_INTERVAL) return;
36
+ await rm(lock, { recursive: true });
37
+ await mkdir(lock, { mode: 0o700 });
38
+ } finally { await rm(recovery, { recursive: true, force: true }); }
39
+ }
40
+ locked = true;
41
+ let previous;
42
+ try { previous = JSON.parse(await readFile(state, 'utf8')).checked_at; }
43
+ catch (error) { if (error.code !== 'ENOENT' && !(error instanceof SyntaxError)) return; }
44
+ const checkedAt = now();
45
+ if (Number.isFinite(previous) && checkedAt >= previous && checkedAt - previous < CHECK_INTERVAL) return;
46
+ await privateJson(state, { version: 1, checked_at: checkedAt });
47
+ await update({ installRoot, env: updateEnv, checkTimeout: 5, ui: { line() {} } });
48
+ } catch { /* Offline, read-only, unavailable and invalid releases are silent. */ }
49
+ finally { if (locked) await rm(lock, { recursive: true, force: true }).catch(() => {}); }
50
+ }
@@ -0,0 +1,9 @@
1
+ // The command users type and read. `supportpages` stays installed as an alias,
2
+ // and every persisted integration name (MCP server, tools, config and data
3
+ // directories, SUPPORTPAGES_* variables) keeps its supportpages spelling.
4
+ // Kept dependency-free: the source bootstrap loads it before dist/ exists.
5
+ // Mirrored in src/brand.ts for the MCP server; a test keeps them equal.
6
+ export const CLI_NAME = 'wtfm';
7
+ export const LEGACY_CLI_NAME = 'supportpages';
8
+ export const INSTALL_COMMAND = 'curl -fsSL https://wtfm.sh/install | bash';
9
+ export const LANDING_URL = 'https://supportpages.io/wtfm';
@@ -0,0 +1,12 @@
1
+ import { execFile } from 'node:child_process';
2
+
3
+ export async function openBrowser(url, { platform = process.platform, execute = execFile, env = process.env } = {}) {
4
+ if (!['darwin', 'linux'].includes(platform)) return false;
5
+ const safeEnv = { ...env };
6
+ delete safeEnv.SUPPORTPAGES_API_TOKEN;
7
+ delete safeEnv.SUPPORTPAGES_API_TOKEN_FILE;
8
+ return new Promise(resolve => {
9
+ execute(platform === 'darwin' ? 'open' : 'xdg-open', [url],
10
+ { env: safeEnv, timeout: 5000 }, error => resolve(!error));
11
+ });
12
+ }
@@ -0,0 +1,33 @@
1
+ import path from 'node:path';
2
+ import os from 'node:os';
3
+ import { lstat, readFile } from 'node:fs/promises';
4
+ import { backup, privateJson } from './install.mjs';
5
+ import { CLI_NAME } from './brand.mjs';
6
+
7
+ /** Match Claude's /mcp project toggle without removing global registrations. */
8
+ export async function selectClaudeConnection({ workspace, dev, configDir, home = os.homedir(), env = process.env }) {
9
+ const filename = env.CLAUDE_CONFIG_DIR ? path.join(env.CLAUDE_CONFIG_DIR, '.claude.json') : path.join(home, '.claude.json');
10
+ for (const entry of [path.dirname(filename), filename]) {
11
+ try { if ((await lstat(entry)).isSymbolicLink()) throw Error('Cannot update a linked Claude configuration.'); }
12
+ catch (error) { if (error.code !== 'ENOENT') throw error; }
13
+ }
14
+ let contents;
15
+ try { contents = await readFile(filename, 'utf8'); }
16
+ catch (error) { if (error.code !== 'ENOENT') throw error; }
17
+ const config = contents === undefined ? {} : JSON.parse(contents);
18
+ const object = value => value !== null && typeof value === 'object' && !Array.isArray(value);
19
+ if (!object(config) || (config.projects !== undefined && !object(config.projects))) throw Error('Claude project settings are invalid.');
20
+ const root = path.resolve(workspace);
21
+ const project = config.projects?.[root] ?? {};
22
+ if (!object(project) || (project.disabledMcpServers !== undefined && (!Array.isArray(project.disabledMcpServers) || project.disabledMcpServers.some(name => typeof name !== 'string')))) throw Error('Claude MCP choices are invalid.');
23
+ const selected = dev ? 'supportpages-dev' : 'supportpages';
24
+ const other = dev ? 'supportpages' : 'supportpages-dev';
25
+ const disabled = [...new Set([...(project.disabledMcpServers ?? []).filter(name => name !== selected), other])];
26
+ if (JSON.stringify(disabled) === JSON.stringify(project.disabledMcpServers)) return false;
27
+ await backup(filename, path.join(configDir, 'backups'));
28
+ let current;
29
+ try { current = await readFile(filename, 'utf8'); } catch (error) { if (error.code !== 'ENOENT') throw error; }
30
+ if (current !== contents) throw Error(`Claude configuration changed. Rerun ${CLI_NAME} init to retry.`);
31
+ await privateJson(filename, { ...config, projects: { ...config.projects, [root]: { ...project, disabledMcpServers: disabled } } });
32
+ return true;
33
+ }
@@ -0,0 +1,47 @@
1
+ import path from 'node:path';
2
+ import { lstat, readFile } from 'node:fs/promises';
3
+
4
+ /** Tools that remove content from a help centre: always confirmed, even when the rest are allowed. */
5
+ export const askFirstTools = ['supportpages_delete_article', 'supportpages_unpublish_article'];
6
+
7
+ /** The rules setup adds for a registered MCP connection: every tool on it allowed, removals asked. */
8
+ export const claudePermissionRules = name => ({
9
+ allow: [`mcp__${name}`],
10
+ ask: askFirstTools.map(tool => `mcp__${name}__${tool}`),
11
+ });
12
+
13
+ /**
14
+ * Allow the SupportPages tools on the successfully registered MCP connection, so
15
+ * uploads and edits the user asked for are not blocked in auto mode. Deleting and
16
+ * unpublishing stay on ask. Existing rules are kept: Claude Code applies deny, then
17
+ * ask, then allow, so any restriction the user added still wins.
18
+ */
19
+ export async function installClaudePermissions({ home, env = process.env, name, configDir, backup, writeJson }) {
20
+ if (typeof name !== 'string' || !/^[a-z0-9][a-z0-9_-]{0,79}$/.test(name)) throw Error('Invalid SupportPages.io connection name.');
21
+ const directory = path.resolve(env.CLAUDE_CONFIG_DIR || path.join(home, '.claude'));
22
+ const filename = path.join(directory, 'settings.json');
23
+ for (const entry of [directory, filename]) {
24
+ try {
25
+ const info = await lstat(entry);
26
+ if (info.isSymbolicLink() || (entry === filename ? !info.isFile() : !info.isDirectory())) throw Error(`Cannot update Claude permissions at an unsafe path: ${entry}`);
27
+ } catch (error) { if (error.code !== 'ENOENT') throw error; }
28
+ }
29
+ let settings;
30
+ try { settings = JSON.parse(await readFile(filename, 'utf8')); }
31
+ catch (error) { if (error.code !== 'ENOENT') throw error; settings = {}; }
32
+ const object = value => value !== null && typeof value === 'object' && !Array.isArray(value);
33
+ if (!object(settings) || (settings.permissions !== undefined && !object(settings.permissions))) throw Error('Claude Code permission settings are invalid. Repair settings.json before installing.');
34
+ const permissions = settings.permissions ?? {};
35
+ for (const key of ['allow', 'ask', 'deny']) {
36
+ if (permissions[key] !== undefined && (!Array.isArray(permissions[key]) || permissions[key].some(rule => typeof rule !== 'string'))) throw Error('Claude Code permission rules are invalid. Repair settings.json before installing.');
37
+ }
38
+ const rules = claudePermissionRules(name);
39
+ const missing = key => rules[key].filter(rule => !permissions[key]?.includes(rule));
40
+ const allow = missing('allow'), ask = missing('ask');
41
+ if (!allow.length && !ask.length) return { filename, rules, changed: false };
42
+ await backup(filename, path.join(configDir, 'backups'));
43
+ await writeJson(filename, { ...settings, permissions: { ...permissions,
44
+ ...(allow.length ? { allow: [...(permissions.allow ?? []), ...allow] } : {}),
45
+ ...(ask.length ? { ask: [...(permissions.ask ?? []), ...ask] } : {}) } });
46
+ return { filename, rules, changed: true };
47
+ }
@@ -0,0 +1,15 @@
1
+ import path from 'node:path';
2
+ import { readFile } from 'node:fs/promises';
3
+
4
+ export const PLUGIN_NAME = 'supportpages-writer';
5
+
6
+ /** True when Claude Code has the SupportPages Writer plugin installed (any
7
+ * marketplace, any scope). The plugin brings its own MCP server and writer
8
+ * agent, so a separate registration would show every tool twice. */
9
+ export async function claudePluginInstalled({ home, env = {} }) {
10
+ const file = path.join(env.CLAUDE_CONFIG_DIR || path.join(home, '.claude'), 'plugins', 'installed_plugins.json');
11
+ try {
12
+ const plugins = JSON.parse(await readFile(file, 'utf8')).plugins ?? {};
13
+ return Object.entries(plugins).some(([id, installs]) => id.split('@')[0] === PLUGIN_NAME && Array.isArray(installs) && installs.length > 0);
14
+ } catch { return false; }
15
+ }
@@ -0,0 +1,10 @@
1
+ import path from 'node:path';
2
+ import os from 'node:os';
3
+ import { installManagedWriter } from './managed-writer.mjs';
4
+
5
+ /** Install the personal agent so Claude launched outside our CLI can discover it. */
6
+ export async function installClaudeWriter({ home = os.homedir(), env = process.env } = {}) {
7
+ const { writerAgentFile, writerAgentType } = await import('../../dist/writer-agent.js');
8
+ return installManagedWriter({ config: path.resolve(env.CLAUDE_CONFIG_DIR || path.join(home, '.claude')),
9
+ name: writerAgentType, extension: 'md', contents: writerAgentFile, marker: '<!-- Managed by SupportPages: article writer v1 -->' });
10
+ }
@@ -0,0 +1,85 @@
1
+ import { parseArgs } from 'node:util';
2
+ import path from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { readFile, realpath } from 'node:fs/promises';
5
+ import { createTerminal, Cancelled, emphasizeCommands } from './terminal.mjs';
6
+ import { publicError } from '../../dist/errors.js';
7
+ import { runCli, telemetryCommand } from './cli.mjs';
8
+ import { reportError } from '../../dist/telemetry.js';
9
+ import { updateCli } from './update.mjs';
10
+ import { CLI_NAME, LEGACY_CLI_NAME } from './brand.mjs';
11
+
12
+ let installRoot=path.resolve(path.dirname(fileURLToPath(import.meta.url)),'../..');
13
+ if(process.env.SUPPORTPAGES_CLI_HOME) {
14
+ const stableRoot=path.join(process.env.SUPPORTPAGES_CLI_HOME,'mcp');
15
+ try {if(await realpath(stableRoot)===await realpath(installRoot)) installRoot=stableRoot;} catch { /* Ignore stale launcher environment. */ }
16
+ }
17
+ const help=`WTFM (Write the F***ing Manual) — local article generation
18
+
19
+ Usage: ${CLI_NAME} <command> [options]
20
+
21
+ setup One-time setup for this computer: coding agent, renderer and account
22
+ init Set up this project: where its articles go, model and project detection
23
+ uninit Forget this folder's setup and environment; archive recovery files
24
+ publish Host this project's locally saved articles on a SupportPages.io help centre
25
+ yolo Write the whole manual on SupportPages.io (account required; drafts only)
26
+ analyse Complete required project analysis (--refresh to rerun)
27
+ status Show the connection and last-reported article progress
28
+ configure Choose coding agent, model, effort, writing style and preferences
29
+ login Sign in to SupportPages.io on this computer (one browser approval)
30
+ logout Sign out of SupportPages.io on this computer
31
+ sync Refresh remote project settings and article history
32
+ doctor Check your installation and generation prerequisites
33
+ telemetry Show or change anonymous usage reporting: telemetry on|off|status
34
+ update Install the latest SupportPages Writer release; keep your settings and articles
35
+ remove Remove local coding-agent integrations; keep projects and articles
36
+ mcp Run the MCP server on stdio (for MCP client configuration)
37
+
38
+ Run ${CLI_NAME} setup once on this computer and ${CLI_NAME} init inside each
39
+ project, then open Claude Code or Codex there and ask for an article. Articles can be saved in the project as Markdown and screenshots with no
40
+ account, or published to a SupportPages.io help centre. Your repository stays
41
+ with your coding agent.
42
+
43
+ Or run ${CLI_NAME} yolo to have SupportPages.io write the whole manual as drafts
44
+ on its servers; it needs an account and access to your repository.
45
+
46
+ Permission consent: ${CLI_NAME} login --scopes manage,generate,publish
47
+
48
+ Options: --workspace PATH, --dev, --api-url ORIGIN, --config-dir PATH,
49
+ --skills-dir PATH, --project ID (init), --agent claude|codex,
50
+ --refresh (init, analyse), --app-type TYPE (init, analyse),
51
+ --skills-only (remove), --yes (remove/uninit/yolo),
52
+ --json (status/doctor/sync), --help, --version
53
+
54
+ ${LEGACY_CLI_NAME} is an alias for ${CLI_NAME}; both run the same command.
55
+ `;
56
+ try {
57
+ let parsed;
58
+ try {parsed=parseArgs({allowPositionals:true,options:{
59
+ ...Object.fromEntries(['workspace','api-url','config-dir','skills-dir','project','agent','app-type','scopes'].map(name=>[name,{type:'string'}])),
60
+ 'skills-only':{type:'boolean'},yes:{type:'boolean'},refresh:{type:'boolean'},dev:{type:'boolean'},json:{type:'boolean'},help:{type:'boolean',short:'h'},version:{type:'boolean',short:'v'},
61
+ }});} catch {throw Object.assign(new Error(),{usage:true});}
62
+ const {values,positionals}=parsed;
63
+ if(values.version) process.stdout.write(JSON.parse(await readFile(path.join(installRoot,'package.json'),'utf8')).version+'\n');
64
+ else if(values.help || positionals.length===0) process.stdout.write(emphasizeCommands(help));
65
+ else {
66
+ const command=positionals[0];
67
+ if(command==='telemetry') {
68
+ const action=positionals[1] ?? 'status';
69
+ if(positionals.length>2 || !['on','off','status'].includes(action) || Object.keys(values).some(key=>!['dev','api-url','config-dir','json'].includes(key))) throw Object.assign(new Error(),{usage:true});
70
+ const result=await telemetryCommand(action,values);
71
+ if(values.json) process.stdout.write(JSON.stringify(result)+'\n');
72
+ else process.stdout.write(`${result.message ?? `Anonymous usage reporting is ${result.enabled?'on':'off'} (${result.reason}).`}\n${result.sends}\n${result.disable}\n`);
73
+ } else if(command==='update') {
74
+ if(positionals.length!==1 || Object.keys(values).length) throw Object.assign(new Error(),{usage:true});
75
+ await updateCli({installRoot,ui:createTerminal()});
76
+ } else {
77
+ if((values.scopes !== undefined && (command !== 'login' || !/^(publish|manage|generate)(,(publish|manage|generate))*$/.test(values.scopes))) || (command === 'remove' && values.workspace !== undefined) || (values.yes && !['remove','uninit','yolo'].includes(command)) || (values['skills-only'] && command!=='remove') || (values.project !== undefined && (command!=='init' || !/^[1-9][0-9]*$/.test(values.project))) || positionals.length!==1 || !['setup','init','uninit','login','logout','status','configure','doctor','analyse','sections','recommend','write','publish','yolo','remove','sync'].includes(command) || (values.json && !['status','doctor','sync'].includes(command)) || (values.agent && !['claude','codex'].includes(values.agent)) || (values.refresh && !['init','analyse','sections','recommend'].includes(command)) || (values['app-type'] && (!['init','analyse'].includes(command) || !['web','terminal','mobile','desktop','win32','macos','game'].includes(values['app-type'])))) throw Object.assign(new Error(),{usage:true});
78
+ if(!['status','doctor','sync','logout'].includes(command) && !(['remove','uninit'].includes(command) && values.yes) && (!process.stdin.isTTY || !process.stdout.isTTY)) {process.stderr.write(`Run ${CLI_NAME} `+command+' in an interactive terminal. Use status --json or doctor --json for automation.\n');process.exitCode=2;}
79
+ else await runCli({...values,command},{installRoot,ui:createTerminal()});
80
+ }
81
+ }
82
+ } catch(error) {
83
+ if(error.usage) {process.stderr.write(emphasizeCommands(`Invalid command or options. Run ${CLI_NAME} --help.\n`,process.stderr));process.exitCode=2;}
84
+ else {if(!(error instanceof Cancelled)) reportError(error);process.stderr.write(emphasizeCommands((error instanceof Cancelled ? error.message : publicError(error).message)+'\n',process.stderr));process.exitCode=error instanceof Cancelled || error.code==='authorization_cancelled' ? 130:1;}
85
+ }