@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,888 @@
|
|
|
1
|
+
import { resolveAction, requireExecution } from '../../dist/actions.js';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import { createHash } from 'node:crypto';
|
|
5
|
+
import { readFile, realpath, readdir } from 'node:fs/promises';
|
|
6
|
+
import { z } from 'zod';
|
|
7
|
+
import { Workspace } from '../../dist/workspace.js';
|
|
8
|
+
import { Session, defaultConfigDir } from '../../dist/session.js';
|
|
9
|
+
import { apiOrigin, defaultOrigin, developmentMode, connectionStateRoot } from '../../dist/api.js';
|
|
10
|
+
import { fail, publicError } from '../../dist/errors.js';
|
|
11
|
+
import { devicePreferences, saveDevicePreferences } from '../../dist/preferences.js';
|
|
12
|
+
import { configureTelemetry } from '../../dist/telemetry.js';
|
|
13
|
+
import { command, exists, expand, privateJson, runInstaller, backup, installationSkills } from './install.mjs';
|
|
14
|
+
import { claudePluginInstalled } from './claude-plugin.mjs';
|
|
15
|
+
import { Cancelled } from './terminal.mjs';
|
|
16
|
+
import { openBrowser } from './browser.mjs';
|
|
17
|
+
import { removeIntegration } from './remove.mjs';
|
|
18
|
+
import { uninitWorkspace } from './uninit.mjs';
|
|
19
|
+
import { ensureWriterAvailable } from './writer-recovery.mjs';
|
|
20
|
+
import { retirePublicSkillLinks } from './article-skills.mjs';
|
|
21
|
+
import { installClaudeWriter } from './claude-writer.mjs';
|
|
22
|
+
import { installClaudePermissions } from './claude-permissions.mjs';
|
|
23
|
+
import { installCodexIntegration } from './codex-integration.mjs';
|
|
24
|
+
import { configureAgent, confirmAgentModels, readAgentSettings, modelDescription } from './agent-settings.mjs';
|
|
25
|
+
import { Planning } from './planning.mjs';
|
|
26
|
+
import { selectClaudeConnection } from './claude-connection.mjs';
|
|
27
|
+
import { writingStylePresets, defaultWritingStyle } from '../../dist/writing-style.js';
|
|
28
|
+
import { setupInvitation } from '../../dist/hosting-benefits.js';
|
|
29
|
+
import { defaultPreferences } from '../../dist/settings.js';
|
|
30
|
+
import { CLI_NAME, LANDING_URL } from './brand.mjs';
|
|
31
|
+
import { runYolo, yoloOperation, followYolo, summarizeYolo, describeProgress, failureMeanings } from './yolo.mjs';
|
|
32
|
+
|
|
33
|
+
const remoteId = z.string().regex(/^[1-9][0-9]*$/);
|
|
34
|
+
const settingsSchema = z.object({
|
|
35
|
+
account:z.object({id:remoteId,email:z.string()}),
|
|
36
|
+
preferences:z.object({prefer_background:z.boolean(),open_when_ready:z.boolean()}),
|
|
37
|
+
writing_styles:z.record(z.string(),z.string()).default({}),
|
|
38
|
+
can_create_project:z.boolean().default(false),
|
|
39
|
+
project_creation_upgrade_url:z.url().refine(value=>{
|
|
40
|
+
const url=new URL(value);
|
|
41
|
+
return ['https:','http:'].includes(url.protocol) && !url.username && !url.password;
|
|
42
|
+
}).nullish(),
|
|
43
|
+
});
|
|
44
|
+
const projectsSchema = z.object({projects:z.array(z.object({id:remoteId,name:z.string(),help_centre_url:z.url().optional()}))});
|
|
45
|
+
const createdSchema = z.object({project:z.object({id:remoteId,name:z.string(),subdomain:z.string()})});
|
|
46
|
+
const clean = value => String(value).replace(/[\p{Cc}\p{Cf}]/gu,'');
|
|
47
|
+
const parse = (schema, value) => { const result=schema.safeParse(value); if(!result.success) fail('invalid_response','SupportPages.io returned invalid setup data.'); return result.data; };
|
|
48
|
+
const profileFile = (configDir, root) => path.join(configDir,'workspaces',createHash('sha256').update(root).digest('hex')+'.json');
|
|
49
|
+
|
|
50
|
+
export async function repository(options, {cwd=process.cwd(),run=command,ui}={}) {
|
|
51
|
+
if(options.workspace) return (await Workspace.create(expand(options.workspace))).root;
|
|
52
|
+
const current=await realpath(cwd);
|
|
53
|
+
const git=await run('git',['rev-parse','--show-toplevel'],{cwd:current,capture:true});
|
|
54
|
+
const gitRoot=git.code===0 ? (await Workspace.create(git.stdout.trim())).root : undefined;
|
|
55
|
+
const linked=async dir=>await exists(path.join(dir,'binding.json')) || await exists(path.join(dir,'local.json'));
|
|
56
|
+
for(let cursor=current;;cursor=path.dirname(cursor)) {
|
|
57
|
+
if(await linked(path.join(cursor,'.rtfm/supportpages'))) return cursor;
|
|
58
|
+
const devDir=path.join(cursor,'.rtfm/supportpages/dev');
|
|
59
|
+
if(await exists(devDir)) for(const name of await readdir(devDir)) {
|
|
60
|
+
if(/^[a-f0-9]{16}$/.test(name) && await linked(path.join(devDir,name))) return cursor;
|
|
61
|
+
}
|
|
62
|
+
// A saved link in a container directory must not capture a child checkout
|
|
63
|
+
// (including worktrees, whose .git is a file rather than a directory).
|
|
64
|
+
if(cursor===gitRoot || cursor===path.dirname(cursor)) break;
|
|
65
|
+
}
|
|
66
|
+
return gitRoot ?? current;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export async function environment(root, options, env=process.env) {
|
|
70
|
+
const configDir=expand(options['config-dir'] ?? defaultConfigDir());
|
|
71
|
+
let saved;
|
|
72
|
+
const file=profileFile(configDir,root);
|
|
73
|
+
// Device authentication is independent of the current folder's saved connection.
|
|
74
|
+
if(!['setup','login','logout'].includes(options.command) && await exists(file)) {
|
|
75
|
+
try { saved=JSON.parse(await readFile(file,'utf8')); } catch { fail('invalid_configuration','The saved workspace environment is invalid.'); }
|
|
76
|
+
if(saved?.version!==1 || saved.workspace!==root || typeof saved.dev!=='boolean' || typeof saved.origin!=='string') fail('invalid_configuration','The saved workspace environment is invalid.');
|
|
77
|
+
}
|
|
78
|
+
const dev=options.dev ?? (env.SUPPORTPAGES_DEV===undefined ? (options['api-url'] || env.SUPPORTPAGES_API_URL ? false : saved?.dev ?? false) : developmentMode(env.SUPPORTPAGES_DEV));
|
|
79
|
+
const origin=apiOrigin(options['api-url'] ?? env.SUPPORTPAGES_API_URL ?? (options.dev!==undefined || env.SUPPORTPAGES_DEV!==undefined ? defaultOrigin(dev) : saved?.origin ?? defaultOrigin(dev)),dev);
|
|
80
|
+
return {configDir,origin,dev};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function installation(config, installRoot) {
|
|
84
|
+
const filename=path.join(config.configDir,'installations',`${config.dev ? 'supportpages-dev' : 'supportpages'}.mcp.json`);
|
|
85
|
+
if(!await exists(filename)) return;
|
|
86
|
+
try {
|
|
87
|
+
const value=JSON.parse(await readFile(filename,'utf8'));
|
|
88
|
+
const entry=value.mcpServers[config.dev ? 'supportpages-dev' : 'supportpages'];
|
|
89
|
+
const args=entry.args;
|
|
90
|
+
const skills=args[args.indexOf('--skills-dir')+1];
|
|
91
|
+
if(args.includes(config.origin) && typeof skills==='string') return {skills,filename};
|
|
92
|
+
} catch { /* Doctor/init can repair incomplete registration. */ }
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export async function generationChecks(skillsDir, run=command) {
|
|
96
|
+
const results=await Promise.all(['git'].map(async name=>({name,available:(await run(name,['--version'],{capture:true})).code===0})));
|
|
97
|
+
const articleDir=path.join(skillsDir,'generate-illustrated-article');
|
|
98
|
+
results.push({name:'article skills',available:await exists(path.join(articleDir,'SKILL.md'))});
|
|
99
|
+
const browser=await run(process.execPath,['--input-type=module','-e',"import {createRequire} from 'node:module'; import {accessSync} from 'node:fs'; const require=createRequire(process.argv[1]+'/package.json'); const p=require('puppeteer'); accessSync(await p.executablePath());",articleDir],{capture:true});
|
|
100
|
+
results.push({name:'article renderer and Chromium',available:browser.code===0});
|
|
101
|
+
return results;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async function ensureIntegration(config, options, deps, skillsDir, session, { clients: requestedClients } = {}) {
|
|
105
|
+
const {ui,run,installRoot}=deps;
|
|
106
|
+
// Setup and configure share this with init; retry advice names the command that was run.
|
|
107
|
+
const retry=authCommand(['setup','configure'].includes(options.command) ? options.command : 'init',config);
|
|
108
|
+
ui.info?.('Checking your coding agent and article renderer…');
|
|
109
|
+
const receipt=path.join(config.configDir,'installations',`${config.dev ? 'supportpages-dev' : 'supportpages'}.json`);
|
|
110
|
+
let clients=[];
|
|
111
|
+
if(await exists(receipt)) { try { clients=JSON.parse(await readFile(receipt,'utf8')).clients ?? []; } catch {} }
|
|
112
|
+
const available=[];
|
|
113
|
+
for(const client of ['codex','claude']) if((await run(client,['--version'],{capture:true})).code===0) available.push(client);
|
|
114
|
+
if(!available.length) fail('missing_dependency',`Install Codex or Claude Code and make its command available in this terminal, then rerun ${retry}. See ${LANDING_URL}#install.`);
|
|
115
|
+
if(options.agent && !available.includes(options.agent)) fail('agent_unavailable',`Install ${options.agent==='codex'?'Codex':'Claude Code'} before selecting it for this project.`);
|
|
116
|
+
const saved=await readAgentSettings({ws:await session.workspace(),stateRoot:connectionStateRoot(config.origin,config.dev)});
|
|
117
|
+
const previous=saved.agents ?? (saved.agent ? [saved.agent] : clients);
|
|
118
|
+
const preferred=previous.filter(client=>available.includes(client));
|
|
119
|
+
const choices=available.map(value=>({value,label:value==='codex'?'Codex':'Claude Code'}));
|
|
120
|
+
if(available.length>1) choices.push({value:'both',label:'Both',hint:'Configure a model for each agent'});
|
|
121
|
+
const initial=preferred.length===2 ? 'both' : preferred[0];
|
|
122
|
+
const client=options.agent ?? (requestedClients?.length===2 ? 'both' : requestedClients?.[0]) ?? (available.length===1 ? available[0] : await ui.choose(options.command==='setup' ? 'Which coding agents should use SupportPages Writer?' : 'Which coding agents should use SupportPages Writer in this project?',choices,Math.max(0,choices.findIndex(choice=>choice.value===initial))));
|
|
123
|
+
const selectedClients=client==='both' ? available : [client];
|
|
124
|
+
let installed=Boolean(await installation(config,installRoot));
|
|
125
|
+
for(const selected of selectedClients) {
|
|
126
|
+
const info=await run(selected,['mcp','get',config.dev ? 'supportpages-dev' : 'supportpages'],{capture:true});
|
|
127
|
+
installed &&= clients.includes(selected) && info.code===0;
|
|
128
|
+
}
|
|
129
|
+
const checks=await generationChecks(skillsDir,run);
|
|
130
|
+
if(installed && checks.every(item=>item.available)) {
|
|
131
|
+
if (selectedClients.includes('claude')) {
|
|
132
|
+
const agent = await installClaudeWriter({ home: deps.home ?? os.homedir(), env: deps.env });
|
|
133
|
+
if (agent.changed) ui.line('Installed the SupportPages Writer agent for Claude Code. Restart existing Claude sessions to load it.');
|
|
134
|
+
const permission = await installClaudePermissions({ home: deps.home ?? os.homedir(), env: deps.env,
|
|
135
|
+
name: config.dev ? 'supportpages-dev' : 'supportpages', configDir: config.configDir, backup, writeJson: privateJson });
|
|
136
|
+
if (permission.changed) ui.line("Allowed SupportPages Writer's tools in Claude Code; deleting or unpublishing an article still asks first.");
|
|
137
|
+
}
|
|
138
|
+
const registered = await installation(config, installRoot);
|
|
139
|
+
if (selectedClients.includes('codex')) await (deps.installCodex ?? installCodexIntegration)({ home: deps.home ?? os.homedir(), env: deps.env,
|
|
140
|
+
name: config.dev ? 'supportpages-dev' : 'supportpages', configDir: config.configDir, backup, ui });
|
|
141
|
+
const removed = await retirePublicSkillLinks({ home: deps.home ?? os.homedir(), clients:selectedClients, directory: registered.skills, env: deps.env });
|
|
142
|
+
if (removed.length) ui.line('Removed old SupportPages.io skill shortcuts. Restart existing coding-agent sessions to refresh their skill lists.');
|
|
143
|
+
ui.ok('Your coding-agent integration and renderer are already installed.');
|
|
144
|
+
return {skillsDir,clients:selectedClients,restartRequired:removed.length > 0};
|
|
145
|
+
}
|
|
146
|
+
const missing=checks.filter(item=>!item.available && item.name==='git');
|
|
147
|
+
if(missing.length) fail('missing_dependency',`Install Git with your system package manager, then rerun ${retry}. See ${LANDING_URL}#install.`);
|
|
148
|
+
ui.line(`Coding agent: ${clientList(selectedClients)}`);
|
|
149
|
+
const download=checks.some(item=>!item.available && item.name==='article renderer and Chromium');
|
|
150
|
+
if(download) ui.info?.('Article screenshots are taken in a browser that SupportPages Writer downloads once. The first download can take a few minutes.');
|
|
151
|
+
if(!await ui.confirm(`Add SupportPages Writer to ${clientList(selectedClients)}${download ? ' and download the screenshot browser' : ''}?`,true)) throw new Cancelled();
|
|
152
|
+
try {
|
|
153
|
+
const result=await (deps.install ?? runInstaller)({client,dev:config.dev,'api-url':config.origin,'config-dir':config.configDir,
|
|
154
|
+
'skills-dir':await exists(skillsDir) ? skillsDir : undefined,'skip-dependencies':true,embedded:true},deps);
|
|
155
|
+
return {skillsDir:result.skillsDir,clients:selectedClients,restartRequired:true};
|
|
156
|
+
} catch(error) {
|
|
157
|
+
if(error instanceof Cancelled) throw error;
|
|
158
|
+
if(/^(An existing directory or file would be replaced:|Missing |Could not register |This connection name is already|The skills checkout)/.test(error.message ?? '')) ui.line(clean(error.message));
|
|
159
|
+
fail('installation_incomplete',`Coding-agent installation could not finish. Completed steps are kept. Run ${authCommand('doctor',config)}, then rerun ${retry} to retry.`);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** What each destination gives: the local choice is made by people who do not
|
|
164
|
+
* know yet what an account adds, so the difference is stated where it is chosen. */
|
|
165
|
+
export const destinationNote='A help centre gives articles a public URL, editor review links and publishing from your agent. Saving locally keeps Markdown and screenshots in this project; nothing leaves your computer.';
|
|
166
|
+
export const getStartedChoices = [
|
|
167
|
+
{value:'signin',label:'Sign in to my existing SupportPages.io account'},
|
|
168
|
+
{value:'signup',label:'Create a free SupportPages.io account and help centre'},
|
|
169
|
+
{value:'local',label:'Save articles in my projects without an account'},
|
|
170
|
+
];
|
|
171
|
+
|
|
172
|
+
/** Sign this device in once. A working saved credential is reused without a browser. */
|
|
173
|
+
export async function ensureLogin(session, {ui,open=openBrowser}, {publish=false,mode,scopes}={}) {
|
|
174
|
+
let interrupted=false;
|
|
175
|
+
const onInterrupt=()=>{interrupted=true;ui.cancel?.();session.close();};
|
|
176
|
+
process.once('SIGINT',onInterrupt);
|
|
177
|
+
const keepAlive=setInterval(()=>{},1000);
|
|
178
|
+
try {
|
|
179
|
+
const result=await session.login({publish,scopes,onApproval:async approval=>{
|
|
180
|
+
const url=new URL(approval.url);
|
|
181
|
+
if(mode==='signup') url.searchParams.set('signup','1');
|
|
182
|
+
const action=mode==='signup' ? 'Create a free SupportPages.io account' : mode==='signin' ? 'Sign in to your SupportPages.io account' : 'Sign in or create an account';
|
|
183
|
+
const instructions=`${action}, then approve sign-in for this device.\nCheck the code: ${approval.code}\n\n${url.href}\n\nReturn to this terminal after approving.`;
|
|
184
|
+
if(ui.note) ui.note(instructions,'Continue in your browser');
|
|
185
|
+
else {ui.line(instructions);}
|
|
186
|
+
if(!await open(url.href)) ui.line('Open the link above to approve, then return here.');
|
|
187
|
+
ui.info?.('Waiting for browser approval… Ctrl+C cancels sign-in.');
|
|
188
|
+
}});
|
|
189
|
+
if(interrupted) throw new Cancelled();
|
|
190
|
+
if(!result.already_signed_in) ui.ok(`Signed in as ${clean(result.account?.email ?? 'your account')}.`);
|
|
191
|
+
return result;
|
|
192
|
+
} catch(error) {
|
|
193
|
+
if(interrupted || error.code==='authorization_cancelled') throw new Cancelled();
|
|
194
|
+
throw error;
|
|
195
|
+
} finally {clearInterval(keepAlive);process.removeListener('SIGINT',onInterrupt);}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
async function chooseWritingStyle(bridge, projectId, settings, ui, draft={}) {
|
|
199
|
+
if(!Object.keys(settings.writing_styles).length) return;
|
|
200
|
+
const context=await bridge.api.request('GET',`/projects/${projectId}/context`);
|
|
201
|
+
const current=context?.product_context?.tone_preference;
|
|
202
|
+
const normalized=typeof current==='string' ? current.trim().toLowerCase() : '';
|
|
203
|
+
const descriptions={friendly:'Warm, everyday language',minimal:'Short explanations and easy-to-scan steps',technical:'Detailed explanations with precise terminology',formal:'Professional language with a more reserved tone'};
|
|
204
|
+
const styles=Object.entries(settings.writing_styles).map(([value,label])=>({value,label:clean(label),hint:descriptions[value]}));
|
|
205
|
+
if(normalized && !styles.some(style=>style.value===normalized)) styles.unshift({value:'keep',label:'Keep current custom style'});
|
|
206
|
+
const initial=draft.writingStyle ?? (normalized && !settings.writing_styles[normalized] ? 'keep' : normalized || 'friendly');
|
|
207
|
+
ui.line('Choose how your help articles should sound. This applies to the whole help centre, including articles written in the web app.');
|
|
208
|
+
const style=await ui.choose('Writing style',styles,Math.max(0,styles.findIndex(item=>item.value===initial)));
|
|
209
|
+
draft.writingStyle=style;
|
|
210
|
+
if(style!=='keep') await bridge.api.request('PATCH',`/projects/${projectId}`,{writing_style:style});
|
|
211
|
+
return styles.find(item=>item.value===style)?.label;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
async function configurePreferences(bridge, ui) {
|
|
215
|
+
const settings=parse(settingsSchema,await bridge.api.request('GET','/mcp/settings'));
|
|
216
|
+
ui.line('Your preferences apply across all help centres.');
|
|
217
|
+
const preferences={
|
|
218
|
+
prefer_background:await ui.confirm('Write articles in the background when available?',settings.preferences.prefer_background),
|
|
219
|
+
open_when_ready:await ui.confirm('Open completed drafts in your browser?',settings.preferences.open_when_ready),
|
|
220
|
+
};
|
|
221
|
+
await bridge.api.request('PATCH','/mcp/settings',{preferences});
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/** Writing-style preset for a folder without a help centre; a custom style typed elsewhere is kept. */
|
|
225
|
+
async function chooseLocalWritingStyle(ui, current) {
|
|
226
|
+
const styles=Object.entries(writingStylePresets).map(([value,{label,hint}])=>({value,label,hint}));
|
|
227
|
+
const normalized=typeof current==='string' ? current.trim().toLowerCase() : '';
|
|
228
|
+
if(normalized && !writingStylePresets[normalized]) styles.unshift({value:'keep',label:'Keep current custom style'});
|
|
229
|
+
ui.line('Choose how your help articles should sound.');
|
|
230
|
+
const initial=normalized ? (writingStylePresets[normalized] ? normalized : 'keep') : defaultWritingStyle;
|
|
231
|
+
const style=await ui.choose('Writing style',styles,Math.max(0,styles.findIndex(item=>item.value===initial)));
|
|
232
|
+
return style==='keep' ? current : style;
|
|
233
|
+
}
|
|
234
|
+
export const validExportDir = value => {
|
|
235
|
+
const trimmed=String(value).trim().replace(/\/+$/,'');
|
|
236
|
+
if(!trimmed || trimmed.length>500 || path.isAbsolute(trimmed) || trimmed.startsWith('~') || /[\p{Cc}\p{Cf}]/u.test(trimmed) || trimmed.includes('\\')) return;
|
|
237
|
+
if(trimmed.split('/').some(part=>!part || part==='.' || part==='..')) return;
|
|
238
|
+
return trimmed;
|
|
239
|
+
};
|
|
240
|
+
async function askExportDir(ui, current='output/articles') {
|
|
241
|
+
ui.line('Finished articles are saved as index.md with their screenshots in a folder per article, inside this project.');
|
|
242
|
+
const value=await ui.ask('Folder for finished articles',current,{
|
|
243
|
+
validate:value=>validExportDir(value) ? undefined : 'Enter a folder inside this project, such as docs/help, without leading / or ..',
|
|
244
|
+
});
|
|
245
|
+
return validExportDir(value) ?? current;
|
|
246
|
+
}
|
|
247
|
+
async function configureLocalPreferences(ui, current={}) {
|
|
248
|
+
const preferences={...defaultPreferences,...current};
|
|
249
|
+
return {
|
|
250
|
+
prefer_background:await ui.confirm('Write articles in the background when available?',preferences.prefer_background),
|
|
251
|
+
open_when_ready:await ui.confirm('Open the finished article preview in your browser?',preferences.open_when_ready),
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/** Terminal help-centre selection, opening billing when a plan upgrade is needed. */
|
|
256
|
+
export async function chooseHelpCentre(session, bridge, {ui,open=openBrowser}, {bound,replaceProjectId,draft={}}={}) {
|
|
257
|
+
let settings=parse(settingsSchema,await bridge.api.request('GET','/mcp/settings'));
|
|
258
|
+
const {projects}=parse(projectsSchema,await bridge.listProjects());
|
|
259
|
+
const selected=bound ? projects.find(project=>project.id===bound) : undefined;
|
|
260
|
+
if(bound && !selected) fail('permission_denied','The signed-in account cannot access this repository’s help centre.');
|
|
261
|
+
draft.name ??= clean(path.basename(session.options.workspace)).replace(/[-_]+/g,' ');
|
|
262
|
+
ui.line('Choose where this project’s help articles will be saved.');
|
|
263
|
+
while(true) {
|
|
264
|
+
const choices=projects.map(project=>({value:project.id,label:`Use ${clean(project.name)}`,
|
|
265
|
+
hint:project.help_centre_url ? `Existing help centre · ${clean(project.help_centre_url)}` : 'Existing help centre'}));
|
|
266
|
+
if(!bound && (settings.can_create_project || settings.project_creation_upgrade_url)) choices.push({value:'new',label:'Create a new help centre',
|
|
267
|
+
hint:settings.can_create_project ? 'Set up a home for this product’s help articles' : 'Upgrade your plan to add another help centre'});
|
|
268
|
+
if(!selected && !choices.length) fail('permission_denied','No accessible help centres are available and this connection cannot create one.');
|
|
269
|
+
const choice=selected?.id ?? draft.choice ?? (projects.length===0 && settings.can_create_project ? 'new' : await ui.choose('Choose a help centre',choices));
|
|
270
|
+
draft.choice=choice;
|
|
271
|
+
let project;
|
|
272
|
+
if(choice==='new') {
|
|
273
|
+
if(!settings.can_create_project) {
|
|
274
|
+
const url=settings.project_creation_upgrade_url;
|
|
275
|
+
if(!url) fail('permission_denied',`This connection cannot create help centres. Run ${CLI_NAME} login, then try again.`);
|
|
276
|
+
ui.line('Upgrade your plan to create another help centre.');
|
|
277
|
+
if(!await open(url)) ui.line(`Open this link to upgrade: ${url}`);
|
|
278
|
+
ui.line('After upgrading, return here to continue setup.');
|
|
279
|
+
if(!await ui.confirm('Check your plan and continue?',true)) {
|
|
280
|
+
if(!projects.length) throw new Cancelled();
|
|
281
|
+
delete draft.choice;
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
settings=parse(settingsSchema,await bridge.api.request('GET','/mcp/settings'));
|
|
285
|
+
if(!settings.can_create_project) {
|
|
286
|
+
ui.line('Your account still cannot create another help centre.');
|
|
287
|
+
delete draft.choice;
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
ui.line('Use your product or company name. This is the name shown in your help centre.');
|
|
292
|
+
const name=await ui.ask('Help centre name',draft.name, {
|
|
293
|
+
validate:value=>!value || value.length>100 || /[\p{Cc}\p{Cf}]/u.test(value) ? 'Enter a name between 1 and 100 characters.' : undefined,
|
|
294
|
+
});
|
|
295
|
+
draft.name=name;
|
|
296
|
+
ui.line('Choose the name in your help centre’s web address.\nFor example: acme → acme.supportpages.io\nEnter just the name, not a full URL.');
|
|
297
|
+
draft.subdomain=await ui.ask('Help centre address name',draft.subdomain ?? name.toLowerCase().replace(/[^a-z0-9]+/g,'-').slice(0,63).replace(/^-|-$/g,''), {
|
|
298
|
+
validate:value=>/^[a-z0-9][a-z0-9-]{1,61}[a-z0-9]$/.test(value) ? undefined : 'Use 3–63 lowercase letters, numbers or hyphens. Start and end with a letter or number.',
|
|
299
|
+
});
|
|
300
|
+
if(!name || name.length>100 || /[\p{Cc}\p{Cf}]/u.test(name) || !/^[a-z0-9][a-z0-9-]{1,61}[a-z0-9]$/.test(draft.subdomain)) {
|
|
301
|
+
ui.line('Enter a help centre name and an address name of 3–63 lowercase letters, numbers or hyphens.');
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
304
|
+
const summary=[`Help centre: ${clean(name)}`,`Address name: ${draft.subdomain}`].join('\n');
|
|
305
|
+
if(ui.note) ui.note(summary,'Ready to create'); else ui.line(summary);
|
|
306
|
+
if(!await ui.confirm('Create this help centre?',true)) throw new Cancelled();
|
|
307
|
+
ui.info?.('Creating your help centre…');
|
|
308
|
+
try { project=parse(createdSchema,await bridge.createProject(name,draft.subdomain)).project; }
|
|
309
|
+
catch(error) {
|
|
310
|
+
if(!['invalid_artifact','permission_denied','plan_limit'].includes(error.code)) throw error;
|
|
311
|
+
if(error.code==='plan_limit') {
|
|
312
|
+
settings=parse(settingsSchema,await bridge.api.request('GET','/mcp/settings'));
|
|
313
|
+
if(!settings.can_create_project && settings.project_creation_upgrade_url) continue;
|
|
314
|
+
}
|
|
315
|
+
ui.line(publicError(error).message);
|
|
316
|
+
if(await ui.confirm('Review your choices and try again?',true)) { delete draft.choice; continue; }
|
|
317
|
+
throw error;
|
|
318
|
+
}
|
|
319
|
+
} else {
|
|
320
|
+
project=projects.find(project=>project.id===choice);
|
|
321
|
+
if(!project) fail('permission_denied',`This help centre is no longer accessible. Run ${CLI_NAME} init again.`);
|
|
322
|
+
const summary=[`Help centre: ${clean(project.name)}`, ...(project.help_centre_url ? [`Address: ${clean(project.help_centre_url)}`] : []),
|
|
323
|
+
'Your coding agent can read context and upload article drafts.','Review and publish in SupportPages.io.'].join('\n');
|
|
324
|
+
if(ui.note) ui.note(summary,'Ready to connect'); else ui.line(summary);
|
|
325
|
+
if(!await ui.confirm('Connect this help centre?',true)) throw new Cancelled();
|
|
326
|
+
}
|
|
327
|
+
const writingStyleLabel=await chooseWritingStyle(bridge,project.id,settings,ui,draft);
|
|
328
|
+
ui.info?.('Connecting your help centre…');
|
|
329
|
+
if(replaceProjectId && replaceProjectId!==project.id) {
|
|
330
|
+
const saved=await session.switchProject(await session.workspace(),replaceProjectId,project.id);
|
|
331
|
+
if(saved?.archive) ui.line(`Previous project records saved in ${saved.archive}`);
|
|
332
|
+
} else await bridge.bind(project.id);
|
|
333
|
+
return {status:'ready',workspace:session.options.workspace,project_id:project.id,name:project.name,help_centre_url:project.help_centre_url,writing_style:writingStyleLabel};
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
export function retryCommand(options) {
|
|
338
|
+
const quote=value=>"'"+String(value).replaceAll("'", "'\\''")+"'";
|
|
339
|
+
const args=[CLI_NAME,'init'];
|
|
340
|
+
if(options.dev) args.push('--dev');
|
|
341
|
+
for(const key of ['project','workspace','api-url','config-dir','skills-dir','app-type']) {
|
|
342
|
+
if(options[key]!==undefined) args.push('--'+key,quote(options[key]));
|
|
343
|
+
}
|
|
344
|
+
if(options.refresh) args.push('--refresh');
|
|
345
|
+
return args.join(' ');
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const clientLabel = client => client==='codex' ? 'Codex' : 'Claude Code';
|
|
349
|
+
/** Signed-in account, when the session can report one. */
|
|
350
|
+
const account = async session => session.account ? await session.account().catch(()=>undefined) : undefined;
|
|
351
|
+
const clientList = clients => clients.map(clientLabel).join(' and ');
|
|
352
|
+
|
|
353
|
+
async function registeredClients(config, clients, run) {
|
|
354
|
+
return Promise.all(clients.map(async client=>(await run(client,['mcp','get',config.dev?'supportpages-dev':'supportpages'],{capture:true})).code===0));
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
async function verifyInstallation(config, integration, run) {
|
|
358
|
+
const checks=await generationChecks(integration.skillsDir,run);
|
|
359
|
+
const registered=await registeredClients(config, integration.clients, run);
|
|
360
|
+
if(checks.some(check=>!check.available) || !registered.length || registered.some(value=>!value)) fail('installation_incomplete',`The coding-agent integration is incomplete. Run ${CLI_NAME} setup, fix the reported prerequisites, then rerun ${CLI_NAME} init.`);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/** Coding-agent commands present on this computer, whether or not SupportPages.io is connected to them. */
|
|
364
|
+
async function installedAgentCommands(run) {
|
|
365
|
+
const found=[];
|
|
366
|
+
for(const agent of ['codex','claude']) if((await run(agent,['--version'],{capture:true})).code===0) found.push(agent);
|
|
367
|
+
return found;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/** Clients recorded by a completed computer setup, or an empty list when this computer has none. */
|
|
371
|
+
async function installedClients(config, installRoot, run) {
|
|
372
|
+
if(!await installation(config,installRoot)) return [];
|
|
373
|
+
const receipt=path.join(config.configDir,'installations',`${config.dev ? 'supportpages-dev' : 'supportpages'}.json`);
|
|
374
|
+
let clients=[];
|
|
375
|
+
if(await exists(receipt)) { try { clients=JSON.parse(await readFile(receipt,'utf8')).clients ?? []; } catch { /* Repaired by setup. */ } }
|
|
376
|
+
const usable=[];
|
|
377
|
+
for(const client of clients) {
|
|
378
|
+
if(!['codex','claude'].includes(client)) continue;
|
|
379
|
+
if((await run(client,['--version'],{capture:true})).code!==0) continue;
|
|
380
|
+
if((await run(client,['mcp','get',config.dev?'supportpages-dev':'supportpages'],{capture:true})).code===0) usable.push(client);
|
|
381
|
+
}
|
|
382
|
+
return usable;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Computer-level setup: the coding-agent integration, the article renderer and the
|
|
387
|
+
* account choice. Nothing here is project-specific; init reuses what it installs.
|
|
388
|
+
*/
|
|
389
|
+
async function computerSetup(config, options, deps, skillsDir, session) {
|
|
390
|
+
const {ui,run}=deps;
|
|
391
|
+
ui.step?.(1,'Set up your coding agent',3);
|
|
392
|
+
const integration=await ensureIntegration(config,options,deps,skillsDir,session);
|
|
393
|
+
const checks=await generationChecks(integration.skillsDir,run);
|
|
394
|
+
if(checks.some(check=>!check.available)) fail('missing_dependency',`Article rendering is not ready. Run ${CLI_NAME} doctor, fix the reported prerequisites, then rerun ${CLI_NAME} setup.`);
|
|
395
|
+
ui.step?.(2,'Choose your SupportPages.io account',3);
|
|
396
|
+
let mode;
|
|
397
|
+
// Ask before contacting the service, so choosing local articles works offline.
|
|
398
|
+
if(!await account(session)) {
|
|
399
|
+
ui.line(destinationNote);
|
|
400
|
+
mode=await ui.choose('How would you like to get started?',getStartedChoices);
|
|
401
|
+
if(mode==='local') {
|
|
402
|
+
await saveDevicePreferences(config.configDir,{default_destination:'local'});
|
|
403
|
+
ui.ok('No account needed: articles will be saved in your projects.');
|
|
404
|
+
return {integration,mode,already_signed_in:false,account:undefined};
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
const result=await ensureLogin(session,deps,{mode});
|
|
408
|
+
if(result.already_signed_in) ui.ok(`Already signed in as ${clean(result.account?.email ?? 'your account')}.`);
|
|
409
|
+
// Account-wide documentation preferences live with the account, not the folder.
|
|
410
|
+
try { await configurePreferences(await session.bridge(), ui); ui.ok('Documentation preferences saved for your account.'); }
|
|
411
|
+
catch(error) { ui.line(`Documentation preferences could not be read: ${publicError(error).message}`); }
|
|
412
|
+
return {integration,mode:mode ?? 'signin',already_signed_in:Boolean(result.already_signed_in),account:result.account ?? await account(session)};
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/** The last stage of project setup, shared by hosted and local folders: model defaults, then detection. */
|
|
416
|
+
async function detect(session, config, integration, ui, options, deps, { modelStep = 2, analysisStep = 3 } = {}) {
|
|
417
|
+
if(integration.clients.includes('claude')) await selectClaudeConnection({ workspace: session.options.workspace,
|
|
418
|
+
dev: config.dev, configDir: config.configDir, home: deps.home, env: deps.env });
|
|
419
|
+
ui.step?.(modelStep,'Model for this project');
|
|
420
|
+
await confirmAgentModels(session, deps, { clients: integration.clients, agent: options.agent });
|
|
421
|
+
ui.step?.(analysisStep,'Read this project',4);
|
|
422
|
+
const planning = await new Planning(session, options, deps, integration.clients).initialize();
|
|
423
|
+
const analysed = await planning.reviewAnalysis();
|
|
424
|
+
const status = await session.status();
|
|
425
|
+
// A deferred analysis has already closed the wizard with how to run it later.
|
|
426
|
+
if (!analysed) return status;
|
|
427
|
+
ui.step?.(4,'Ready');
|
|
428
|
+
const summary=[
|
|
429
|
+
`Articles ${destinationLine(status,config)}`,
|
|
430
|
+
`Agent ${clientList(integration.clients)}`,
|
|
431
|
+
...writingLines(status,integration.clients),
|
|
432
|
+
...(status.status==='local' ? [`Hosting ${authCommand('publish',config)} to put them on a help centre`] : []),
|
|
433
|
+
].join('\n');
|
|
434
|
+
if(ui.note) ui.note(summary,'This project is ready'); else ui.line(summary);
|
|
435
|
+
await planning.menu();
|
|
436
|
+
return status;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* Where articles are written and what it costs: on this computer it uses the coding
|
|
441
|
+
* agent's own usage; on SupportPages.io (repository connected) the article allowance.
|
|
442
|
+
*/
|
|
443
|
+
export function writingLines(status, clients) {
|
|
444
|
+
const agents=clients.map(clientLabel).join(' or ');
|
|
445
|
+
if(status.status!=='local' && status.writer_action && status.writer_action.execution!=='local')
|
|
446
|
+
return ['Writing On SupportPages.io, using your article allowance'];
|
|
447
|
+
const lines=[`Writing On this computer, using your own ${agents} usage`];
|
|
448
|
+
const connect=status.status==='local' ? undefined : status.repository_connection;
|
|
449
|
+
if(connect?.state==='not_connected') lines.push(' Connect a repository to write on SupportPages.io instead,',` using your article allowance: ${clean(connect.connect_url)}`);
|
|
450
|
+
return lines;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/** What the project's articles are and where they live, for the closing summary. */
|
|
454
|
+
function destinationLine(status, config) {
|
|
455
|
+
if(status.status==='local') return `${status.export_dir} · Markdown and screenshots`;
|
|
456
|
+
const url=status.project?.help_centre_url;
|
|
457
|
+
return `${clean(status.project?.name ?? 'SupportPages.io')}${url ? ` · ${clean(url)}` : ''}`;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/**
|
|
461
|
+
* The project's coding agent and model. An agent installed on this computer but
|
|
462
|
+
* not connected yet is offered here and connected on choice, so changing agents
|
|
463
|
+
* needs no separate command.
|
|
464
|
+
*/
|
|
465
|
+
async function configureProjectAgent(config, options, deps, installRoot, run, session, ui, skillsDir) {
|
|
466
|
+
const connected=await installedClients(config,installRoot,run);
|
|
467
|
+
const unconnected=(await installedAgentCommands(run)).filter(agent=>!connected.includes(agent));
|
|
468
|
+
if(!unconnected.length) return await configureAgent(session, deps);
|
|
469
|
+
const choice=await ui.choose('Which coding agent should SupportPages Writer use for this project?',[
|
|
470
|
+
...connected.map(agent=>({value:agent,label:clientLabel(agent)})),
|
|
471
|
+
...unconnected.map(agent=>({value:agent,label:`${clientLabel(agent)} — not connected yet`,hint:'SupportPages Writer connects it now'})),
|
|
472
|
+
],0);
|
|
473
|
+
if(!connected.includes(choice)) {
|
|
474
|
+
const integration=await ensureIntegration(config,options,deps,skillsDir,session,{clients:[...connected,choice]});
|
|
475
|
+
session.options.skillsDir=integration.skillsDir;
|
|
476
|
+
}
|
|
477
|
+
return await configureAgent(session, deps, { clients: [choice], agent: choice });
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/** Finish a folder that saves articles locally: its settings are already saved. */
|
|
481
|
+
async function readyLocally(session, config, integration, ui, run, options, deps, settings) {
|
|
482
|
+
await verifyInstallation(config, integration, run);
|
|
483
|
+
ui.ok(`Articles will be saved in ${clean(settings.export_dir)} as Markdown and screenshots.`);
|
|
484
|
+
return detect(session, config, integration, ui, options, deps);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
function authCommand(command, config) {
|
|
488
|
+
const quote=value=>"'"+String(value).replaceAll("'", "'\\''")+"'";
|
|
489
|
+
return `${CLI_NAME} ${command}${config.dev ? ' --dev' : ''}${config.origin!==defaultOrigin(config.dev) ? ' --api-url '+quote(config.origin) : ''}`;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// A folder with no destination yet needs setup, not an account: lead with the
|
|
493
|
+
// choice that costs nothing and offer hosting on its merits.
|
|
494
|
+
const statusHint = (status, config) => status==='setup_required' ? [`Run ${CLI_NAME} init to set up this folder. Articles can be saved right here as Markdown and screenshots, with no account.`, ...setupInvitation()]
|
|
495
|
+
: status==='authentication_required' ? [`This folder is linked to a help centre, so it needs a signed-in device. Run ${authCommand('login',config)} to sign in again.`]
|
|
496
|
+
: status==='project_required' ? [`Run ${CLI_NAME} init to choose where this folder’s articles go: a help centre for a public URL and editor review, or saved in the project with no help centre.`]
|
|
497
|
+
: status==='local' ? [`Open your coding agent in this folder and ask for an article, or run ${authCommand('publish',config)} to host the saved articles on a help centre.`]
|
|
498
|
+
: [`Run ${CLI_NAME} init to finish setting up this repository.`];
|
|
499
|
+
|
|
500
|
+
const connectionLabel = status => status==='ready' ? 'connected to a help centre'
|
|
501
|
+
: status==='local' ? 'saving articles in this project (no account needed)'
|
|
502
|
+
: status==='setup_required' ? `not set up yet — run ${CLI_NAME} init (no account needed)`
|
|
503
|
+
: status==='authentication_required' ? 'linked to a help centre, but this device is signed out'
|
|
504
|
+
: status==='project_required' ? 'signed in; this folder has not chosen where its articles go'
|
|
505
|
+
: status.replaceAll('_',' ');
|
|
506
|
+
|
|
507
|
+
/** The folder's `wtfm yolo` run in status: one line, or live progress while it runs in a terminal. */
|
|
508
|
+
async function yoloStatus(bridge, status, options, deps) {
|
|
509
|
+
let run;
|
|
510
|
+
try { run=await yoloOperation(bridge); } catch(error) { if(!options.json) deps.ui.line(`Help centre writing: could not be checked (${publicError(error).message})`); return; }
|
|
511
|
+
const operation=run?.operation;
|
|
512
|
+
if(!operation) return;
|
|
513
|
+
const result=operation.result ?? {};
|
|
514
|
+
status.help_centre_generation={operation_id:operation.id,status:operation.status,attempt:operation.attempt,result,review_url:operation.review_url,error:operation.error};
|
|
515
|
+
if(options.json) return;
|
|
516
|
+
const {ui}=deps;
|
|
517
|
+
const running=['queued','running'].includes(operation.status);
|
|
518
|
+
ui.line(`Help centre writing (${CLI_NAME} yolo): ${running ? describeProgress(result) : operation.status==='succeeded' ? `finished · ${result.articles?.generated ?? 0} drafts written` : `stopped · ${failureMeanings[operation.error?.code] ?? clean(operation.error?.code ?? 'failed')} Run ${CLI_NAME} yolo to resume.`}`);
|
|
519
|
+
if(!running) { const link=result.editor_url ?? operation.review_url; if(link) ui.line(clean(link)); return; }
|
|
520
|
+
// Reattach: follow the run until it ends or Ctrl+C, as yolo itself does.
|
|
521
|
+
if(!(deps.follow ?? (process.stdout.isTTY && process.stdin.isTTY))) return;
|
|
522
|
+
const followed=await followYolo(bridge,operation,{ui,deps,record:run.record});
|
|
523
|
+
status.help_centre_generation={...status.help_centre_generation,status:followed.operation.status,result:followed.operation.result,error:followed.operation.error};
|
|
524
|
+
if(followed.detached) return;
|
|
525
|
+
try { summarizeYolo(followed.operation,{ui}); } catch(error) { ui.line(error.message); }
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
const cliNotice = notice => notice.replace('Ask me to turn this off', `Run ${CLI_NAME} telemetry off to stop it`);
|
|
529
|
+
/** wtfm telemetry on|off|status: device-wide, so it needs no project folder. */
|
|
530
|
+
export async function telemetryCommand(action, options, env=process.env) {
|
|
531
|
+
const dev=options.dev ?? developmentMode(env.SUPPORTPAGES_DEV);
|
|
532
|
+
const telemetry=configureTelemetry({configDir:expand(options['config-dir'] ?? defaultConfigDir()),origin:apiOrigin(options['api-url'] ?? env.SUPPORTPAGES_API_URL ?? defaultOrigin(dev),dev),env});
|
|
533
|
+
const result=action==='status' ? await telemetry.status() : await telemetry.setEnabled(action==='on');
|
|
534
|
+
return result;
|
|
535
|
+
}
|
|
536
|
+
export async function runCli(options, supplied) {
|
|
537
|
+
if(options.project!==undefined && (options.command!=='init' || !remoteId.safeParse(options.project).success)) fail('invalid_request','Use --project with a numeric help-centre ID and the init command.');
|
|
538
|
+
const env={...process.env,...supplied.env};
|
|
539
|
+
// The bundled runtime must also be available to skills/client subprocesses.
|
|
540
|
+
env.PATH=path.dirname(process.execPath)+path.delimiter+(env.PATH ?? '');
|
|
541
|
+
const deps={run:(cmd,args,settings)=>command(cmd,args,{...settings,env}),open:openBrowser,...supplied,env};
|
|
542
|
+
if(env.SUPPORTPAGES_CLI_HOME) {
|
|
543
|
+
try {if(await realpath(path.join(env.SUPPORTPAGES_CLI_HOME,'mcp'))===await realpath(deps.installRoot)) deps.node=path.join(env.SUPPORTPAGES_CLI_HOME,'runtime/bin/node');} catch { /* Ignore stale launcher environment. */ }
|
|
544
|
+
}
|
|
545
|
+
const {ui,installRoot,run}=deps;
|
|
546
|
+
if (options.command === 'remove') return removeIntegration(options, deps);
|
|
547
|
+
const root=await repository(options,deps);
|
|
548
|
+
if (options.command === 'uninit') {
|
|
549
|
+
if (options.dev !== undefined || options['api-url']) fail('invalid_request', 'uninit forgets this folder’s setup in every environment. Omit --dev and --api-url.');
|
|
550
|
+
return uninitWorkspace(root, expand(options['config-dir'] ?? defaultConfigDir()), { ui, yes: options.yes });
|
|
551
|
+
}
|
|
552
|
+
const config=await environment(root,options,env);
|
|
553
|
+
const telemetry=configureTelemetry({configDir:config.configDir,origin:config.origin,installRoot,env,fetcher:deps.fetcher});
|
|
554
|
+
telemetry.start();
|
|
555
|
+
const notice=await telemetry.notice();
|
|
556
|
+
if(notice) ui.info ? ui.info(cliNotice(notice)) : ui.line(cliNotice(notice));
|
|
557
|
+
const installed=await installation(config,installRoot);
|
|
558
|
+
let skillsDir=await installationSkills(installRoot, expand(options['skills-dir'] ?? installed?.skills ?? env.RTFM_SKILLS_DIR ?? path.join(installRoot,'engine')));
|
|
559
|
+
const session=(deps.sessionFactory ?? (value=>new Session(value)))({workspace:root,cwd:root,...config,skillsDir});
|
|
560
|
+
try {
|
|
561
|
+
if(options.command==='logout') {
|
|
562
|
+
ui.info?.(`Signing out of ${config.origin} on this device…`);
|
|
563
|
+
let result;
|
|
564
|
+
try { result=await session.logout(); }
|
|
565
|
+
catch(error) {
|
|
566
|
+
ui.line(`Logout could not finish. Retry ${authCommand('logout',config)}. If the server is unavailable or needs an update, revoke the token in Settings → Local publishing tokens.`);
|
|
567
|
+
throw error;
|
|
568
|
+
}
|
|
569
|
+
ui.ok(result.already_logged_out ? 'This device is already signed out.' : 'Signed out. The device token was revoked and its local credential removed.');
|
|
570
|
+
ui.line(`Your browser account, folder links, project settings and article files are kept. Run ${authCommand('login',config)} to sign in again.`);
|
|
571
|
+
return result;
|
|
572
|
+
}
|
|
573
|
+
if(options.command==='login') {
|
|
574
|
+
ui.intro?.(`SupportPages Writer · Sign in${config.dev?' · Development':''}`);
|
|
575
|
+
if(config.dev || config.origin!==defaultOrigin(config.dev)) ui.line(`Environment: ${config.origin}`);
|
|
576
|
+
const result=await ensureLogin(session,deps,{scopes:options.scopes?.split(',')});
|
|
577
|
+
if(result.already_signed_in) {
|
|
578
|
+
ui.ok(`Already signed in as ${clean(result.account?.email ?? 'your account')}.`);
|
|
579
|
+
ui.line(`To switch accounts, run ${authCommand('logout',config)}, then ${authCommand('login',config)}.`);
|
|
580
|
+
} else ui.line(`Run ${authCommand('init',config)} inside a project folder to connect it to a help centre.`);
|
|
581
|
+
return result;
|
|
582
|
+
}
|
|
583
|
+
if(options.command==='setup') {
|
|
584
|
+
ui.intro?.(`SupportPages Writer · Set up this computer${config.dev?' · Development':''}`,{banner:true});
|
|
585
|
+
const {integration,mode,already_signed_in,account:signedIn}=await computerSetup(config,options,deps,skillsDir,session);
|
|
586
|
+
ui.step?.(3,'Ready',3);
|
|
587
|
+
// The outro below says what to do next; the note only records what was set up.
|
|
588
|
+
const summary=[
|
|
589
|
+
`Coding agent: ${clientList(integration.clients)}`,
|
|
590
|
+
`Account: ${mode==='local' ? 'none — articles are saved in your projects' : clean(signedIn?.email ?? 'signed in')}`,
|
|
591
|
+
].join('\n');
|
|
592
|
+
if(ui.note) ui.note(summary,'This computer is set up'); else ui.line(summary);
|
|
593
|
+
ui.outro?.(mode==='local'
|
|
594
|
+
? `Run ${authCommand('init',config)} in a project to set up writing.\nOptional: ${authCommand('publish',config)} hosts saved articles on a help centre later.`
|
|
595
|
+
: `Run ${authCommand('init',config)} in a project to choose a help centre and write articles.`);
|
|
596
|
+
return {status:mode==='local'?'local':'signed_in',api_origin:config.origin,already_signed_in,...(signedIn?{account:signedIn}:{})};
|
|
597
|
+
}
|
|
598
|
+
if(options.command==='sync') {
|
|
599
|
+
const result=await (await session.bridge()).sync();
|
|
600
|
+
if(options.json) ui.line(JSON.stringify(result),{literal:true});
|
|
601
|
+
else {
|
|
602
|
+
ui.line(`Help centre: ${clean(result.project.name)}`);
|
|
603
|
+
if(result.project.help_centre_url) ui.line(clean(result.project.help_centre_url));
|
|
604
|
+
if(result.status==='ready') {
|
|
605
|
+
ui.ok(`Synced ${result.counts.remote} remote articles.`);
|
|
606
|
+
for(const item of result.articles) {
|
|
607
|
+
const title=item.remote?.title ?? item.title ?? 'Untitled article';
|
|
608
|
+
ui.line(`${clean(title)} · ${item.sync_status.replaceAll('_',' ')}${item.remote?.status ? ` · ${item.remote.status}` : ''}${item.identity_mismatch ? ' · server remembers a different local copy; this folder’s record is used' : ''}`);
|
|
609
|
+
if(item.remote?.editor_url) ui.line(clean(item.remote.editor_url));
|
|
610
|
+
}
|
|
611
|
+
if(result.walkthroughs?.status==='ready') {
|
|
612
|
+
ui.ok(`Synced ${result.walkthroughs.counts.remote} remote video walkthroughs.`);
|
|
613
|
+
for(const item of result.walkthroughs.items) {
|
|
614
|
+
const video=item.remote;
|
|
615
|
+
ui.line(`${clean(video?.title ?? item.last_known?.title ?? 'Unavailable video')} · ${item.sync_status.replaceAll('_',' ')}${video ? ` · ${video.generation_status} · ${video.public_on_article ? 'on public article' : 'not on public article'} · ${video.public_url ? 'share link live' : 'no live share link'}` : ''}`);
|
|
616
|
+
if(video?.review_url) ui.line(clean(video.review_url));
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
ui.line(result.instructions);
|
|
621
|
+
}
|
|
622
|
+
return result;
|
|
623
|
+
}
|
|
624
|
+
if(options.command==='status') {
|
|
625
|
+
const status=await session.status();
|
|
626
|
+
if(options.json) { if(status.status==='ready') await yoloStatus(await session.bridge(),status,options,deps); ui.line(JSON.stringify(status),{literal:true}); }
|
|
627
|
+
else {
|
|
628
|
+
ui.line(`${clean(root)} — ${status.status==='ready'?'Connected':status.status==='local'?'Local articles':status.status==='setup_required'?'Not set up yet':status.status==='project_required'?'Destination not chosen':clean(status.status.replaceAll('_',' '))}`);
|
|
629
|
+
ui.line(`Environment: ${config.origin}`);
|
|
630
|
+
if(status.account?.email) ui.line(`Signed in as ${clean(status.account.email)}`);
|
|
631
|
+
if(status.project) ui.line(`Help centre: ${clean(status.project.name)}`);
|
|
632
|
+
if(status.status==='local') ui.line(`Articles are saved in ${clean(status.export_dir)} as Markdown and screenshots. No account is connected.`);
|
|
633
|
+
ui.line(`Analysis: ${status.analysis?.status ?? 'required'}${status.generation_ready ? ' · ready to write' : ''}`);
|
|
634
|
+
if(status.setup_task) ui.line(`Last setup progress: ${clean(status.setup_task.skill)} · ${clean(status.setup_task.status)}`);
|
|
635
|
+
if(status.setup_task?.total !== undefined) ui.line(`Articles: ${status.setup_task.completed ?? 0}/${status.setup_task.total} drafts ready · ${status.setup_task.failed ?? 0} failed`);
|
|
636
|
+
if(status.article_run) ui.line(JSON.stringify(status.article_run,null,2),{literal:true});
|
|
637
|
+
if(status.status!=='ready') for(const line of statusHint(status.status,config)) ui.line(line);
|
|
638
|
+
}
|
|
639
|
+
if(!options.json && status.status==='ready') await yoloStatus(await session.bridge(),status,options,deps);
|
|
640
|
+
return status;
|
|
641
|
+
}
|
|
642
|
+
if(options.command==='doctor') {
|
|
643
|
+
const checks=await generationChecks(skillsDir,run);
|
|
644
|
+
const clientChecks=[];
|
|
645
|
+
for(const client of ['codex','claude']) {
|
|
646
|
+
const available=(await run(client,['--version'],{capture:true})).code===0;
|
|
647
|
+
const registered=available && (await run(client,['mcp','get',config.dev ? 'supportpages-dev':'supportpages'],{capture:true})).code===0;
|
|
648
|
+
clientChecks.push({client,available,registered});
|
|
649
|
+
}
|
|
650
|
+
const plugin=await claudePluginInstalled({home:deps.home ?? os.homedir(),env:deps.env});
|
|
651
|
+
const result={workspace:root,api_origin:config.origin,checks,coding_clients:clientChecks,claude_plugin:plugin,connection:await session.status(),telemetry:await telemetry.status()};
|
|
652
|
+
if(options.json) ui.line(JSON.stringify(result),{literal:true});
|
|
653
|
+
else {
|
|
654
|
+
ui.line(`Repository: ${clean(root)}\nEnvironment: ${config.origin}`);
|
|
655
|
+
for(const check of checks) ui.line(`${check.available?'OK':'Missing'}: ${check.name}`);
|
|
656
|
+
for(const client of clientChecks) ui.line(`${client.client}: ${client.registered?'MCP registered':client.available?'MCP not registered':'not installed'}`);
|
|
657
|
+
ui.line(`Connection: ${clean(connectionLabel(result.connection.status))}`);
|
|
658
|
+
ui.line(`Anonymous usage reporting: ${result.telemetry.enabled?'on':'off'} (${result.telemetry.reason})${result.telemetry.enabled?` · ${CLI_NAME} telemetry off to stop it`:''}`);
|
|
659
|
+
for(const client of clientChecks) if(client.available && !client.registered && !(client.client==='claude' && plugin)) ui.line(`${clientLabel(client.client)} is installed but not connected. Run ${CLI_NAME} setup to connect it.`);
|
|
660
|
+
if(plugin) ui.line(clientChecks.find(client=>client.client==='claude')?.registered ? `Claude Code has the SupportPages Writer plugin and a separate registration, so its tools appear twice. Run ${CLI_NAME} remove --agent claude to keep only the plugin.` : 'Claude Code: SupportPages Writer plugin installed.');
|
|
661
|
+
if(checks.some(check=>!check.available) || !clientChecks.some(client=>client.registered)) ui.line(`Run ${CLI_NAME} setup to finish the integration; it downloads Chromium for screenshots if it is missing. Git and your coding agent must be available in your terminal.`);
|
|
662
|
+
}
|
|
663
|
+
return result;
|
|
664
|
+
}
|
|
665
|
+
if(options.command==='yolo') return await runYolo({session,config,options,deps,root,helpers:{ensureLogin,chooseHelpCentre,getStartedChoices,account,authCommand,
|
|
666
|
+
saveProfile:(workspace,current)=>privateJson(profileFile(current.configDir,workspace),{version:1,workspace,origin:current.origin,dev:current.dev})}});
|
|
667
|
+
if(options.command==='publish') {
|
|
668
|
+
ui.intro?.(`SupportPages Writer · Publish${config.dev?' · Development':''}`);
|
|
669
|
+
const initial=await session.bridge();
|
|
670
|
+
const destination=await initial.destination();
|
|
671
|
+
if(destination==='none') fail('project_required',`Run ${CLI_NAME} init to set up this folder first.`);
|
|
672
|
+
const articles=await initial.localArticles();
|
|
673
|
+
if(!articles.length) {
|
|
674
|
+
if(destination==='hosted') { ui.ok('Every saved article in this folder is already on SupportPages.io.'); return session.status(); }
|
|
675
|
+
fail('nothing_to_publish','No saved articles to publish yet. Ask your coding agent to write one first.');
|
|
676
|
+
}
|
|
677
|
+
await ensureWriterAvailable(initial, ui);
|
|
678
|
+
const local=await initial.local();
|
|
679
|
+
ui.line(`${articles.length} saved article${articles.length===1?'':'s'} in this folder:`);
|
|
680
|
+
for(const item of articles) ui.line(` ${clean(item.title)}`);
|
|
681
|
+
const total=destination==='local' ? 3 : 1;
|
|
682
|
+
if(destination==='local') {
|
|
683
|
+
ui.info?.('Publishing needs a SupportPages.io account. Your articles stay in this folder as well.');
|
|
684
|
+
ui.step?.(1,'Sign in to SupportPages.io',total);
|
|
685
|
+
const mode=await account(session) ? undefined : await ui.choose('How would you like to sign in?',getStartedChoices.slice(0,2));
|
|
686
|
+
await ensureLogin(session,deps,{mode});
|
|
687
|
+
const signedIn=await session.bridge();
|
|
688
|
+
const status=await session.status();
|
|
689
|
+
if(status.status==='connection_error') fail('connection_error', status.error?.message ?? `SupportPages.io could not be reached. Check your connection and retry ${CLI_NAME} publish.`);
|
|
690
|
+
ui.step?.(2,'Choose your help centre',total);
|
|
691
|
+
await chooseHelpCentre(session,signedIn,deps,{draft:{writingStyle:local?.writing_style}});
|
|
692
|
+
await privateJson(profileFile(config.configDir,root),{version:1,workspace:root,origin:config.origin,dev:config.dev});
|
|
693
|
+
} else await ensureLogin(session,deps);
|
|
694
|
+
ui.step?.(total,'Upload your articles',total);
|
|
695
|
+
const bridge=await session.bridge();
|
|
696
|
+
const verified=await session.status();
|
|
697
|
+
if(verified.status!=='ready') fail('connection_error',`The help centre connection could not be verified. Run ${CLI_NAME} status, then retry ${CLI_NAME} publish.`);
|
|
698
|
+
await bridge.sync();
|
|
699
|
+
const chosen=await ui.multiselect('Which articles should be uploaded as drafts?',articles.map(item=>({value:item.slug,label:clean(item.title)})),articles.map(item=>item.slug));
|
|
700
|
+
const result=await bridge.uploadLocalArticles(chosen,event=>{
|
|
701
|
+
if(event.error) {
|
|
702
|
+
ui.line(`${clean(event.title)}: ${event.error.message}`);
|
|
703
|
+
if(event.error.code==='plan_limit') ui.line(`The remaining articles were not uploaded. Free up capacity or upgrade, then run ${CLI_NAME} publish again.`);
|
|
704
|
+
} else if(event.editor_url) ui.ok(`${clean(event.title)} · ${clean(event.editor_url)}`);
|
|
705
|
+
else ui.info?.(`Uploading ${clean(event.title)}…`);
|
|
706
|
+
});
|
|
707
|
+
const uploaded=result.uploaded ?? [];
|
|
708
|
+
ui.outro?.(uploaded.length
|
|
709
|
+
? `${uploaded.length} draft${uploaded.length===1?'':'s'} uploaded to ${clean(verified.project.name)}. Review and publish them in SupportPages.io. This folder now publishes to that help centre; ask your coding agent for the next article.`
|
|
710
|
+
: `This folder now publishes to ${clean(verified.project.name)}. No drafts were uploaded.`);
|
|
711
|
+
return {status:'published',project_id:verified.project_id,help_centre_url:verified.project.help_centre_url,uploaded};
|
|
712
|
+
}
|
|
713
|
+
if(['sections','recommend'].includes(options.command)) {
|
|
714
|
+
const bridge = await session.bridge();
|
|
715
|
+
const decision = await resolveAction(bridge, options.command === 'sections' ? 'suggest_sections' : 'recommend_articles');
|
|
716
|
+
requireExecution(decision, 'hosted');
|
|
717
|
+
const result=await bridge.hosted.submit(options.command==='sections'?'suggest_sections':'recommend_articles',{}, {decision});
|
|
718
|
+
ui.line(`Operation ${result.operation_id}: ${result.status}`);
|
|
719
|
+
if(result.review_url)ui.line(result.review_url);
|
|
720
|
+
return result;
|
|
721
|
+
}
|
|
722
|
+
if(['analyse','write'].includes(options.command)) {
|
|
723
|
+
const status = await session.status();
|
|
724
|
+
if(!['ready','local'].includes(status.status)) fail(status.status==='setup_required'?'setup_required':'authentication_required',`Run ${CLI_NAME} init to set up this project first.`);
|
|
725
|
+
ui.intro?.(`SupportPages Writer · ${options.command}${config.dev?' · Development':''}`);
|
|
726
|
+
if(options.command === 'write') {
|
|
727
|
+
const bridge=await session.bridge();
|
|
728
|
+
const decision=await resolveAction(bridge,'create_article');
|
|
729
|
+
if(!decision.allowed) requireExecution(decision,decision.execution);
|
|
730
|
+
if(decision.execution==='hosted') {
|
|
731
|
+
const title=await ui.ask('Which article should SupportPages write?',undefined,{validate:value=>value?.trim() ? undefined : 'Enter an article title.'});
|
|
732
|
+
const result=await bridge.createArticle({title,article_type:'how-to'});
|
|
733
|
+
ui.line(`Operation ${result.operation_id}: ${result.status}`);
|
|
734
|
+
if(result.review_url)ui.line(result.review_url);
|
|
735
|
+
return result;
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
const planning = await new Planning(session,options,deps).initialize();
|
|
739
|
+
if(options.command === 'analyse') { await planning.reviewAnalysis(); return session.status(); }
|
|
740
|
+
if(options.command === 'write') return planning.write();
|
|
741
|
+
return;
|
|
742
|
+
}
|
|
743
|
+
if (options.command === 'configure') {
|
|
744
|
+
ui.intro?.(`SupportPages Writer · This project${config.dev?' · Development':''}`);
|
|
745
|
+
const bridge = await session.bridge();
|
|
746
|
+
const destination = await bridge.destination();
|
|
747
|
+
if (destination === 'none') fail('project_required', `Run ${CLI_NAME} init to set up this project before configuring it.`);
|
|
748
|
+
const local = destination === 'local' ? await bridge.local() : undefined;
|
|
749
|
+
// Project settings only: the computer's agents live in setup, and a help
|
|
750
|
+
// centre's writing style and the account's preferences are asked for by init
|
|
751
|
+
// and setup respectively.
|
|
752
|
+
if (!local) return await configureProjectAgent(config, options, deps, installRoot, run, session, ui, skillsDir);
|
|
753
|
+
const area = await ui.choose('What would you like to configure?', [
|
|
754
|
+
{ value: 'agent', label: 'Coding agent and model' },
|
|
755
|
+
{ value: 'writing_style', label: 'Writing style' },
|
|
756
|
+
{ value: 'preferences', label: 'Documentation preferences' },
|
|
757
|
+
{ value: 'export_dir', label: 'Article folder' },
|
|
758
|
+
{ value: 'hosting_reminders', label: 'Hosting reminders', hint: 'Occasional notes after a saved article about hosting it on SupportPages.io' },
|
|
759
|
+
]);
|
|
760
|
+
if (area === 'agent') return await configureProjectAgent(config, options, deps, installRoot, run, session, ui, skillsDir);
|
|
761
|
+
if (area === 'hosting_reminders') {
|
|
762
|
+
// A device preference for every local folder, not a folder setting.
|
|
763
|
+
const current = await bridge.hostingReminders();
|
|
764
|
+
const enabled = await ui.confirm('Show occasional reminders about hosting saved articles on SupportPages.io?', current.enabled);
|
|
765
|
+
await bridge.hostingReminders(enabled);
|
|
766
|
+
ui.ok(enabled ? 'Hosting reminders are on for local folders on this computer.' : 'Hosting reminders are off for local folders on this computer until you enable them again.');
|
|
767
|
+
return session.status();
|
|
768
|
+
}
|
|
769
|
+
// No account is involved: these settings live in the folder.
|
|
770
|
+
const settings = area === 'writing_style' ? { ...local, writing_style: await chooseLocalWritingStyle(ui, local.writing_style) }
|
|
771
|
+
: area === 'preferences' ? { ...local, preferences: await configureLocalPreferences(ui, local.preferences) }
|
|
772
|
+
: { ...local, export_dir: await askExportDir(ui, local.export_dir) };
|
|
773
|
+
await bridge.saveLocal(settings);
|
|
774
|
+
ui.ok(area === 'writing_style' ? 'Writing style saved for this folder.' : area === 'preferences' ? 'Your preferences have been saved for this folder.' : `Finished articles will be saved in ${clean(settings.export_dir)}.`);
|
|
775
|
+
await privateJson(profileFile(config.configDir,root),{version:1,workspace:root,origin:config.origin,dev:config.dev});
|
|
776
|
+
return session.status();
|
|
777
|
+
}
|
|
778
|
+
const title=`SupportPages Writer · Set up this project${config.dev?' · Development':''}`;
|
|
779
|
+
if(ui.intro) ui.intro(title,{banner:true});
|
|
780
|
+
else ui.line(title);
|
|
781
|
+
const initial=await session.bridge();
|
|
782
|
+
const savedBinding=await initial.ws.exists(`${initial.stateRoot}/binding.json`) ? (await initial.binding()).project_id : undefined;
|
|
783
|
+
const savedLocal=savedBinding ? undefined : await initial.local();
|
|
784
|
+
const details=`${clean(path.basename(root))}${root===path.basename(root) ? '' : ` (${clean(root)})`}`;
|
|
785
|
+
ui.line(details);
|
|
786
|
+
|
|
787
|
+
// The coding-agent integration is computer-level: install it once with setup,
|
|
788
|
+
// and only offer that here rather than repeating it for every project.
|
|
789
|
+
let clients=await installedClients(config,installRoot,run);
|
|
790
|
+
let integration;
|
|
791
|
+
if(!clients.length) {
|
|
792
|
+
ui.line('This computer is not set up for SupportPages Writer yet.');
|
|
793
|
+
if(!await ui.confirm('Set it up now?',true)) throw new Cancelled(`Setup cancelled. Run ${CLI_NAME} setup when you are ready, then rerun ${CLI_NAME} init.`);
|
|
794
|
+
const installed=await computerSetup(config,options,deps,skillsDir,session);
|
|
795
|
+
integration=installed.integration; skillsDir=integration.skillsDir;
|
|
796
|
+
} else {
|
|
797
|
+
integration=await ensureIntegration(config,options,deps,skillsDir,session,{clients});
|
|
798
|
+
skillsDir=integration.skillsDir;
|
|
799
|
+
}
|
|
800
|
+
session.options.skillsDir=skillsDir;
|
|
801
|
+
const checks=await generationChecks(skillsDir,run);
|
|
802
|
+
if(checks.some(check=>!check.available)) fail('missing_dependency',`Article rendering is not ready. Run ${CLI_NAME} doctor, fix the reported prerequisites, then rerun ${CLI_NAME} init.`);
|
|
803
|
+
|
|
804
|
+
ui.step?.(1,'Where articles go',4);
|
|
805
|
+
let hosted=Boolean(savedBinding || options.project);
|
|
806
|
+
let replaceProjectId, bound=options.project ?? savedBinding, result;
|
|
807
|
+
if(savedLocal) {
|
|
808
|
+
const message=`This project saves articles in ${clean(savedLocal.export_dir)} as Markdown and screenshots. No SupportPages.io account is connected.`;
|
|
809
|
+
if(ui.note) ui.note(message,'Local articles'); else ui.line(message);
|
|
810
|
+
hosted=!await ui.confirm('Keep saving articles in this project?',true);
|
|
811
|
+
if(!hosted) {
|
|
812
|
+
// Keeping the setup asks nothing: wtfm configure changes its settings.
|
|
813
|
+
result=await readyLocally(session,config,integration,ui,run,options,deps,savedLocal);
|
|
814
|
+
await privateJson(profileFile(config.configDir,root),{version:1,workspace:root,origin:config.origin,dev:config.dev});
|
|
815
|
+
return result;
|
|
816
|
+
}
|
|
817
|
+
} else if(!hosted) {
|
|
818
|
+
const preferLocal=(await devicePreferences(config.configDir)).default_destination==='local' && !await account(session);
|
|
819
|
+
ui.line(destinationNote);
|
|
820
|
+
const choice=await ui.choose('Where should finished articles go?',[
|
|
821
|
+
{value:'hosted',label:'Publish them to a SupportPages.io help centre'},
|
|
822
|
+
{value:'local',label:'Save them in this project without an account'},
|
|
823
|
+
],preferLocal ? 1 : 0);
|
|
824
|
+
hosted=choice==='hosted';
|
|
825
|
+
if(!hosted) {
|
|
826
|
+
const settings={ version:1, export_dir:'output/articles', writing_style: await chooseLocalWritingStyle(ui, defaultWritingStyle) };
|
|
827
|
+
settings.export_dir=await askExportDir(ui, settings.export_dir);
|
|
828
|
+
await initial.saveLocal(settings);
|
|
829
|
+
result=await readyLocally(session,config,integration,ui,run,options,deps,settings);
|
|
830
|
+
await privateJson(profileFile(config.configDir,root),{version:1,workspace:root,origin:config.origin,dev:config.dev});
|
|
831
|
+
return result;
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
// Hosted: sign in, then reuse or choose the help centre.
|
|
836
|
+
if(!await account(session)) await ensureLogin(session,deps);
|
|
837
|
+
const bridge=await session.bridge();
|
|
838
|
+
const status=await session.status();
|
|
839
|
+
if(status.status==='connection_error') fail('connection_error', status.error?.message ?? `SupportPages.io could not be reached. Check your connection and retry ${CLI_NAME} init.`);
|
|
840
|
+
if(savedBinding && status.status!=='ready') {
|
|
841
|
+
const unavailable=status.status==='project_unavailable';
|
|
842
|
+
const message=unavailable
|
|
843
|
+
? `The previously linked help centre (Project ${savedBinding}) is no longer available to this account. It may have been deleted or your access may have changed.`
|
|
844
|
+
: `The previous link to Project ${savedBinding} could not be verified. Choose a help centre for this repository.`;
|
|
845
|
+
if(ui.note) ui.note(message,unavailable?'Previous help centre unavailable':'Reconnect SupportPages.io');
|
|
846
|
+
else ui.line(message);
|
|
847
|
+
if(unavailable && options.project===savedBinding) fail('project_unavailable','The requested project is no longer accessible. Run init without --project to choose or create a help centre.');
|
|
848
|
+
await ensureWriterAvailable(bridge, ui);
|
|
849
|
+
replaceProjectId=savedBinding;
|
|
850
|
+
bound=options.project;
|
|
851
|
+
}
|
|
852
|
+
if(savedBinding && status.status==='ready') {
|
|
853
|
+
const url=status.project?.help_centre_url;
|
|
854
|
+
const message=`This project publishes to ${clean(status.project.name)}${url ? `\n${clean(url)}` : ''}`;
|
|
855
|
+
if(ui.note) ui.note(message,'Existing connection'); else ui.line(message);
|
|
856
|
+
const explicitSwitch=options.project && options.project!==savedBinding;
|
|
857
|
+
const keep=explicitSwitch
|
|
858
|
+
? !await ui.confirm(`Switch this repository to SupportPages.io project ${options.project}?`,false)
|
|
859
|
+
: await ui.confirm('Keep publishing to this help centre?',true);
|
|
860
|
+
if(explicitSwitch && keep) throw new Cancelled();
|
|
861
|
+
if(keep) {
|
|
862
|
+
if(await ui.confirm('Change the writing style for this help centre?',false)) {
|
|
863
|
+
const settings=parse(settingsSchema,await bridge.api.request('GET','/mcp/settings'));
|
|
864
|
+
await chooseWritingStyle(bridge,savedBinding,settings,ui);
|
|
865
|
+
ui.ok('Writing style saved for this help centre.');
|
|
866
|
+
}
|
|
867
|
+
const synchronization=await bridge.sync();
|
|
868
|
+
if(synchronization.status==='ready') ui.line(`Synced ${synchronization.counts.remote} remote articles.`);
|
|
869
|
+
result=await detect(session,config,integration,ui,options,deps);
|
|
870
|
+
await privateJson(profileFile(config.configDir,root),{version:1,workspace:root,origin:config.origin,dev:config.dev});
|
|
871
|
+
return result;
|
|
872
|
+
}
|
|
873
|
+
await ensureWriterAvailable(bridge, ui);
|
|
874
|
+
replaceProjectId=savedBinding;
|
|
875
|
+
bound=explicitSwitch ? options.project : undefined;
|
|
876
|
+
}
|
|
877
|
+
// Choosing a help centre is a sub-step of "where articles go", so it keeps step 1's number.
|
|
878
|
+
const chosen=await chooseHelpCentre(session,bridge,deps,{bound,replaceProjectId,draft:{writingStyle:savedLocal?.writing_style}});
|
|
879
|
+
await privateJson(profileFile(config.configDir,root),{version:1,workspace:root,origin:config.origin,dev:config.dev});
|
|
880
|
+
result=await detect(session,config,integration,ui,options,deps);
|
|
881
|
+
return result;
|
|
882
|
+
} catch(error) {
|
|
883
|
+
// The caller prints the error first, then this hint: the message must come
|
|
884
|
+
// before the recovery command, not the other way round.
|
|
885
|
+
if(options.command==='init' && !(error instanceof Cancelled) && error && typeof error==='object') error.retryHint=`Retry: ${retryCommand(options)}`;
|
|
886
|
+
throw error;
|
|
887
|
+
} finally {session.close();}
|
|
888
|
+
}
|