@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,362 @@
|
|
|
1
|
+
// `wtfm yolo`: write a whole help centre as drafts on SupportPages.io.
|
|
2
|
+
// Every account step reuses the terminal flows that init and publish use; the
|
|
3
|
+
// writing itself is the server's `generate_help_centre` operation, which this
|
|
4
|
+
// command starts, follows and (after a failure) resumes.
|
|
5
|
+
import { randomUUID } from 'node:crypto';
|
|
6
|
+
import { z } from 'zod';
|
|
7
|
+
import { resolveAction, requireExecution } from '../../dist/actions.js';
|
|
8
|
+
import { fail, publicError } from '../../dist/errors.js';
|
|
9
|
+
import { track, secondsSince } from '../../dist/telemetry.js';
|
|
10
|
+
import { Cancelled } from './terminal.mjs';
|
|
11
|
+
import { CLI_NAME } from './brand.mjs';
|
|
12
|
+
|
|
13
|
+
export const YOLO_ACTION = 'generate_help_centre';
|
|
14
|
+
const clean = value => String(value).replace(/[\p{Cc}\p{Cf}]/gu, '');
|
|
15
|
+
const plural = (count, word) => `${count} ${word}${count === 1 ? '' : 's'}`;
|
|
16
|
+
const say = (ui, text) => ui.ok ? ui.ok(text) : ui.line(text);
|
|
17
|
+
const inform = (ui, text) => ui.info ? ui.info(text) : ui.line(text);
|
|
18
|
+
const pending = operation => ['queued', 'running'].includes(operation.status);
|
|
19
|
+
const recordSchema = z.object({ version: z.literal(1), key: z.uuid(), operation_id: z.string().regex(/^[1-9][0-9]*$/).optional(),
|
|
20
|
+
reported: z.string().optional() });
|
|
21
|
+
|
|
22
|
+
/** What a failed run's code means for the user, in their terms. */
|
|
23
|
+
export const failureMeanings = {
|
|
24
|
+
generation_failed: 'SupportPages.io could not finish writing the help centre.',
|
|
25
|
+
invalid_content: 'The generated content could not be saved.',
|
|
26
|
+
no_sections: 'No help-centre sections could be worked out from the repository.',
|
|
27
|
+
invalid_credentials: 'The repository connection stopped working while writing.',
|
|
28
|
+
operation_timeout: 'The run stopped responding and timed out.',
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/** This folder's run, kept with the other operation journals under .rtfm/supportpages. */
|
|
32
|
+
const recordFile = (bridge, projectId) => `${bridge.stateRoot}/operations/${projectId}/help-centre.json`;
|
|
33
|
+
export async function readYoloRun(bridge) {
|
|
34
|
+
const { project_id } = await bridge.binding();
|
|
35
|
+
const file = recordFile(bridge, project_id);
|
|
36
|
+
if (!await bridge.ws.exists(file)) return undefined;
|
|
37
|
+
try { const parsed = recordSchema.safeParse(await bridge.ws.json(file)); return parsed.success ? parsed.data : undefined; }
|
|
38
|
+
catch { return undefined; }
|
|
39
|
+
}
|
|
40
|
+
async function saveYoloRun(bridge, record) {
|
|
41
|
+
const { project_id } = await bridge.binding();
|
|
42
|
+
await bridge.ws.writeJson(recordFile(bridge, project_id), record);
|
|
43
|
+
return record;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** The latest known state of this folder's run, read again from the server; undefined when there is none. */
|
|
47
|
+
export async function yoloOperation(bridge) {
|
|
48
|
+
const record = await readYoloRun(bridge);
|
|
49
|
+
if (!record) return undefined;
|
|
50
|
+
if (record.operation_id) return { record, operation: await bridge.hosted.get(record.operation_id, 0, false) };
|
|
51
|
+
// The response to the submission was lost: the key finds it without starting another run.
|
|
52
|
+
try {
|
|
53
|
+
const { project_id } = await bridge.binding();
|
|
54
|
+
const operation = await bridge.api.request('GET', `/projects/${project_id}/writer_operations?idempotency_key=${encodeURIComponent(record.key)}`);
|
|
55
|
+
if (typeof operation?.id !== 'string' || !operation.id) fail('invalid_response', 'The server returned an invalid operation.');
|
|
56
|
+
const found = await bridge.hosted.get(operation.id, 0, false);
|
|
57
|
+
await saveYoloRun(bridge, { ...record, operation_id: found.id });
|
|
58
|
+
return { record: { ...record, operation_id: found.id }, operation: found };
|
|
59
|
+
} catch (error) {
|
|
60
|
+
if (publicError(error).code === 'not_found') return { record };
|
|
61
|
+
throw error;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function describeProgress(result = {}) {
|
|
66
|
+
const articles = result.articles ?? {};
|
|
67
|
+
if (result.stage === 'sections') return 'Working out the sections of your help centre…';
|
|
68
|
+
if (result.stage === 'recommendations') return `Sections: ${result.sections ?? 0} · choosing which articles to write…`;
|
|
69
|
+
if (result.stage === 'articles' || result.stage === 'done') return `Writing drafts: ${articles.generated ?? 0} of ${articles.total ?? 0} done${articles.failed ? ` · ${articles.failed} failed` : ''}`;
|
|
70
|
+
return 'Queued on SupportPages.io · reading the repository analysis…';
|
|
71
|
+
}
|
|
72
|
+
const stageSummary = (stage, result) => stage === 'analysis' ? 'Repository analysis read'
|
|
73
|
+
: stage === 'sections' ? `Sections: ${result.sections ?? 0} accepted`
|
|
74
|
+
: stage === 'recommendations' ? `Articles to write: ${result.recommendations ?? 0} recommended${result.articles?.total !== undefined ? `, ${result.articles.total} started` : ''}`
|
|
75
|
+
: undefined;
|
|
76
|
+
const order = ['analysis', 'sections', 'recommendations', 'articles', 'done'];
|
|
77
|
+
|
|
78
|
+
/** A render-only progress line; test and plain UIs without one print each change once. */
|
|
79
|
+
function progressFor(ui, text) {
|
|
80
|
+
if (ui.progress) return ui.progress(text);
|
|
81
|
+
let last = text; ui.line(text);
|
|
82
|
+
return { update: next => { if (next !== last) { ui.line(next); last = next; } }, stop: message => ui.line(message) };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Ctrl-C stops this terminal waiting, never the run on SupportPages.io. */
|
|
86
|
+
function interruption(deps) {
|
|
87
|
+
const controller = new AbortController();
|
|
88
|
+
const onInterrupt = () => controller.abort();
|
|
89
|
+
process.once('SIGINT', onInterrupt);
|
|
90
|
+
deps.signal?.addEventListener('abort', onInterrupt, { once: true });
|
|
91
|
+
if (deps.signal?.aborted) controller.abort();
|
|
92
|
+
return { signal: controller.signal, done: () => { process.removeListener('SIGINT', onInterrupt); deps.signal?.removeEventListener('abort', onInterrupt); } };
|
|
93
|
+
}
|
|
94
|
+
const pause = (ms, signal) => new Promise(resolve => {
|
|
95
|
+
if (signal.aborted) return resolve();
|
|
96
|
+
const timer = setTimeout(resolve, ms);
|
|
97
|
+
signal.addEventListener('abort', () => { clearTimeout(timer); resolve(); }, { once: true });
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
/** Counted once per run attempt, whichever command observes the end. */
|
|
101
|
+
async function report(bridge, record, operation) {
|
|
102
|
+
if (pending(operation) || !record) return record;
|
|
103
|
+
const id = `${operation.id}:${operation.attempt}`;
|
|
104
|
+
if (record.reported === id) return record;
|
|
105
|
+
track('yolo_completed', { outcome: operation.status, error_code: operation.error?.code,
|
|
106
|
+
duration_s: secondsSince(operation.created_at, operation.finished_at ? Date.parse(operation.finished_at) : Date.now()) });
|
|
107
|
+
return saveYoloRun(bridge, { ...record, reported: id });
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Poll until the run ends or Ctrl-C detaches; network errors back off rather than end it. */
|
|
111
|
+
export async function followYolo(bridge, initial, { ui, deps = {}, record, command = CLI_NAME }) {
|
|
112
|
+
const pollMs = deps.yoloPollMs ?? 3000;
|
|
113
|
+
const interrupt = interruption(deps);
|
|
114
|
+
let operation = initial, stage, progress, delay = pollMs, failures = 0;
|
|
115
|
+
const render = () => {
|
|
116
|
+
const result = operation.result ?? {};
|
|
117
|
+
const next = order.includes(result.stage) ? result.stage : 'analysis';
|
|
118
|
+
if (next !== stage) {
|
|
119
|
+
// A stage that finished between polls is still reported, in order.
|
|
120
|
+
const from = stage === undefined ? -1 : order.indexOf(stage);
|
|
121
|
+
if (progress) { progress.stop(stageSummary(stage, result) ?? describeProgress(result)); progress = undefined; }
|
|
122
|
+
if (stage !== undefined) for (const skipped of order.slice(from + 1, order.indexOf(next))) { const line = stageSummary(skipped, result); if (line) say(ui, line); }
|
|
123
|
+
stage = next;
|
|
124
|
+
if (pending(operation)) progress = progressFor(ui, describeProgress(result));
|
|
125
|
+
} else progress?.update(describeProgress(result));
|
|
126
|
+
};
|
|
127
|
+
try {
|
|
128
|
+
while (true) {
|
|
129
|
+
render();
|
|
130
|
+
if (!pending(operation)) break;
|
|
131
|
+
await pause(delay, interrupt.signal);
|
|
132
|
+
if (interrupt.signal.aborted) {
|
|
133
|
+
const message = `Stopped watching. SupportPages.io keeps writing your help centre; run ${command} status to check on it again.`;
|
|
134
|
+
if (progress) progress.stop(message, 'cancelled'); else ui.line(message);
|
|
135
|
+
return { detached: true, operation };
|
|
136
|
+
}
|
|
137
|
+
try { operation = await bridge.hosted.get(operation.id, 0, false); delay = pollMs; failures = 0; }
|
|
138
|
+
catch (error) {
|
|
139
|
+
const safe = publicError(error);
|
|
140
|
+
if (!['network_error', 'remote_error', 'rate_limited', 'connection_error'].includes(safe.code) || ++failures > (deps.yoloMaxFailures ?? 20)) {
|
|
141
|
+
progress?.stop(`Lost track of the run: ${safe.message}`, 'error');
|
|
142
|
+
ui.line(`SupportPages.io keeps writing. Run ${command} status to check on it again.`);
|
|
143
|
+
throw error;
|
|
144
|
+
}
|
|
145
|
+
delay = Math.min(pollMs * 2 ** failures, deps.yoloMaxDelayMs ?? 30_000);
|
|
146
|
+
progress?.update(`SupportPages.io could not be reached; trying again in ${Math.max(1, Math.round(delay / 1000))}s…`);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
} finally { interrupt.done(); }
|
|
150
|
+
if (progress) { progress.stop(operation.status === 'succeeded' ? 'Writing finished' : 'The run stopped', operation.status === 'succeeded' ? 'success' : 'error'); }
|
|
151
|
+
record = await report(bridge, record ?? await readYoloRun(bridge), operation);
|
|
152
|
+
return { detached: false, operation };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** The closing summary: counts and the editor link, or what failed and how to resume. */
|
|
156
|
+
export function summarizeYolo(operation, { ui, resume = `${CLI_NAME} yolo` }) {
|
|
157
|
+
const result = operation.result ?? {};
|
|
158
|
+
const articles = result.articles ?? {};
|
|
159
|
+
const link = result.editor_url ?? operation.review_url;
|
|
160
|
+
if (operation.status === 'succeeded') {
|
|
161
|
+
const lines = [`Sections ${result.sections ?? 0}`, `Drafts written ${articles.generated ?? 0}${articles.failed ? ` (${articles.failed} could not be written)` : ''}`,
|
|
162
|
+
...(link ? [`Review ${clean(link)}`] : [])];
|
|
163
|
+
if (ui.note) ui.note(lines.join('\n'), 'Your manual is written'); else ui.line(lines.join('\n'));
|
|
164
|
+
const text = (articles.generated ?? 0) === 0 && !(articles.total)
|
|
165
|
+
? 'No new drafts were needed: every recommended article already exists or your plan has no article allowance left.'
|
|
166
|
+
: `Everything is a draft: nothing goes live until you publish it. Review and publish in SupportPages.io${link ? `: ${clean(link)}` : '.'}`;
|
|
167
|
+
if (ui.outro) ui.outro(text); else ui.line(text);
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
const code = operation.error?.code ?? 'generation_failed';
|
|
171
|
+
const meaning = failureMeanings[code] ?? `The run failed (${clean(code)}).`;
|
|
172
|
+
const kept = articles.generated ? ` The ${plural(articles.generated, 'draft')} already written ${articles.generated === 1 ? 'is' : 'are'} kept.` : '';
|
|
173
|
+
if (kept || link) ui.line(`${kept.trim()}${kept && link ? '\n' : ''}${link ? `Review: ${clean(link)}` : ''}`);
|
|
174
|
+
const hint = code === 'invalid_credentials' ? `Reconnect the repository in SupportPages.io, then run ${resume} to resume.` : `Run ${resume} to resume where it stopped.`;
|
|
175
|
+
fail(code, `${meaning} ${hint}`);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Route each prerequisite the server reports to the flow that resolves it, and
|
|
180
|
+
* return once generate_help_centre is allowed. Browser steps open their page
|
|
181
|
+
* (or print it) and poll until the server reports a different state.
|
|
182
|
+
*/
|
|
183
|
+
async function prerequisites(session, deps, helpers, initialBridge, { resuming = false } = {}) {
|
|
184
|
+
const { ui } = deps;
|
|
185
|
+
const open = deps.open;
|
|
186
|
+
let bridge = initialBridge, waited = new Set();
|
|
187
|
+
for (let rounds = 0; rounds < 20; rounds++) {
|
|
188
|
+
const decision = await resolveAction(bridge, YOLO_ACTION);
|
|
189
|
+
if (decision.allowed) { requireExecution(decision, 'hosted'); return bridge; }
|
|
190
|
+
const next = decision.next_step;
|
|
191
|
+
// Resuming reuses the allowance the run already reserved, so a spent plan does not block it.
|
|
192
|
+
if (resuming && next.code === 'plan_limit') return bridge;
|
|
193
|
+
if (next.code === 'authentication_required') {
|
|
194
|
+
await helpers.ensureLogin(session, deps);
|
|
195
|
+
bridge = await session.bridge();
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
if (next.code === 'permission_required') {
|
|
199
|
+
const scopes = (next.missing_scopes ?? ['generate']).filter(scope => ['publish', 'manage', 'generate'].includes(scope));
|
|
200
|
+
ui.line('Writing on SupportPages.io needs this device to have the “generate” permission. Approve it in your browser; the current sign-in keeps working until you do.');
|
|
201
|
+
await helpers.ensureLogin(session, deps, { scopes });
|
|
202
|
+
bridge = await session.bridge();
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
const browserStep = /^repository_(required|disconnected|suspended)$|^analysis_(required|failed)$|^analysing$|^plan_limit$/.test(next.code);
|
|
206
|
+
if (!browserStep) requireExecution(decision, 'hosted');
|
|
207
|
+
const titles = { repository_required: 'Connect your repository', repository_disconnected: 'Reconnect your repository', repository_suspended: 'Restore repository access',
|
|
208
|
+
analysing: 'Analysing your repository', analysis_required: 'Analyse your repository', analysis_failed: 'Repository analysis failed', plan_limit: 'No article allowance left' };
|
|
209
|
+
const why = /^repository_/.test(next.code) ? 'SupportPages.io reads your code to write the manual, so it needs access to the repository.'
|
|
210
|
+
: next.code === 'plan_limit' ? 'Your plan has no article allowance left for new drafts.'
|
|
211
|
+
: 'SupportPages.io analyses the repository before it can plan your help centre.';
|
|
212
|
+
const message = `${why}\n${clean(next.message)}${next.url ? `\n\n${clean(next.url)}` : ''}`;
|
|
213
|
+
if (ui.note) ui.note(message, titles[next.code]); else ui.line(message);
|
|
214
|
+
// Each page opens once per run; later polls of the same state only wait.
|
|
215
|
+
if (next.url && next.code !== 'analysing' && !waited.has(next.url)) {
|
|
216
|
+
waited.add(next.url);
|
|
217
|
+
if (!await open(next.url)) ui.line('Open the link above in your browser, then return here.');
|
|
218
|
+
}
|
|
219
|
+
if (next.code === 'plan_limit') {
|
|
220
|
+
if (!await ui.confirm('Check your plan again and continue?', true)) throw new Cancelled(`Stopped. Run ${CLI_NAME} yolo again once your plan has article allowance.`);
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
await waitForChange(bridge, next.code, deps);
|
|
224
|
+
}
|
|
225
|
+
fail('setup_incomplete', `Setup did not finish. Run ${CLI_NAME} yolo again to continue.`);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/** Poll the server's decision until it leaves `code`, e.g. once the repository is connected. */
|
|
229
|
+
async function waitForChange(bridge, code, deps) {
|
|
230
|
+
const { ui } = deps;
|
|
231
|
+
const interrupt = interruption(deps);
|
|
232
|
+
const waiting = code === 'analysing' || /^analysis_/.test(code) ? 'Waiting for the repository analysis' : code === 'repository_suspended' ? 'Waiting for repository access' : 'Waiting for the repository connection';
|
|
233
|
+
const progress = progressFor(ui, `${waiting}… Ctrl+C stops waiting.`);
|
|
234
|
+
const deadline = Date.now() + (deps.yoloWaitMs ?? 30 * 60_000);
|
|
235
|
+
try {
|
|
236
|
+
while (Date.now() < deadline) {
|
|
237
|
+
await pause(deps.yoloPollMs ?? 3000, interrupt.signal);
|
|
238
|
+
if (interrupt.signal.aborted) { progress.stop('Stopped waiting.', 'cancelled'); throw new Cancelled(`Stopped waiting. Run ${CLI_NAME} yolo again when that step is done.`); }
|
|
239
|
+
let decision;
|
|
240
|
+
try { decision = await resolveAction(bridge, YOLO_ACTION); }
|
|
241
|
+
catch (error) { if (['network_error', 'remote_error', 'rate_limited'].includes(publicError(error).code)) continue; throw error; }
|
|
242
|
+
if (decision.next_step?.code !== code) { progress.stop(code === 'analysing' || /^analysis_/.test(code) ? 'Repository analysis is ready.' : 'Repository connected.'); return; }
|
|
243
|
+
}
|
|
244
|
+
progress.stop('Still waiting.', 'error');
|
|
245
|
+
fail('setup_incomplete', `That step is not finished yet. Run ${CLI_NAME} yolo again when it is.`);
|
|
246
|
+
} finally { interrupt.done(); }
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/** Remaining allowance for this help centre, or undefined when the server does not say. */
|
|
250
|
+
async function allowance(bridge) {
|
|
251
|
+
try {
|
|
252
|
+
const capacity = (await bridge.context()).article_capacity;
|
|
253
|
+
if (!capacity) return undefined;
|
|
254
|
+
return capacity.limit === null ? { unlimited: true } : { remaining: Math.max(0, capacity.limit - capacity.used), limit: capacity.limit };
|
|
255
|
+
} catch { return undefined; }
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
export async function runYolo({ session, config, options, deps, helpers, root }) {
|
|
259
|
+
const { ui } = deps;
|
|
260
|
+
const command = helpers.authCommand('yolo', config);
|
|
261
|
+
ui.intro?.(`WTFM · Write the whole manual${config.dev ? ' · Development' : ''}`);
|
|
262
|
+
ui.line('SupportPages.io writes your help centre on its servers from your repository. Everything is saved as drafts: nothing goes live until you publish it.');
|
|
263
|
+
|
|
264
|
+
// 1. Account: the same browser sign-in as publish.
|
|
265
|
+
const signedIn = await helpers.account(session);
|
|
266
|
+
if (!signedIn) {
|
|
267
|
+
ui.step?.(1, 'Sign in to SupportPages.io', 3);
|
|
268
|
+
const mode = await ui.choose('This needs a SupportPages.io account. How would you like to sign in?', helpers.getStartedChoices.slice(0, 2));
|
|
269
|
+
await helpers.ensureLogin(session, deps, { mode });
|
|
270
|
+
} else await helpers.ensureLogin(session, deps);
|
|
271
|
+
|
|
272
|
+
// 2. Help centre: keep this folder's, or bind/create one as publish does.
|
|
273
|
+
let bridge = await session.bridge();
|
|
274
|
+
if (await bridge.destination() !== 'hosted') {
|
|
275
|
+
ui.step?.(2, 'Choose your help centre', 3);
|
|
276
|
+
const status = await session.status();
|
|
277
|
+
if (status.status === 'connection_error') fail('connection_error', status.error?.message ?? `SupportPages.io could not be reached. Check your connection and retry ${command}.`);
|
|
278
|
+
const local = await bridge.local();
|
|
279
|
+
await helpers.chooseHelpCentre(session, bridge, deps, { draft: { writingStyle: local?.writing_style } });
|
|
280
|
+
await helpers.saveProfile(root, config);
|
|
281
|
+
bridge = await session.bridge();
|
|
282
|
+
} else {
|
|
283
|
+
const status = await session.status();
|
|
284
|
+
if (status.status !== 'ready') fail(status.status === 'project_unavailable' ? 'project_unavailable' : 'connection_error',
|
|
285
|
+
status.status === 'project_unavailable' ? `This folder's help centre is no longer available to this account. Run ${helpers.authCommand('init', config)} to choose another.` : status.error?.message ?? `SupportPages.io could not be reached. Retry ${command}.`);
|
|
286
|
+
ui.line(`Help centre: ${clean(status.project.name)}${status.project.help_centre_url ? ` · ${clean(status.project.help_centre_url)}` : ''}`);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// 3. A run already known to this folder: attach to it, or resume it after a failure.
|
|
290
|
+
ui.step?.(3, 'Write the manual', 3);
|
|
291
|
+
const existing = await yoloOperation(bridge);
|
|
292
|
+
if (existing?.operation && pending(existing.operation)) {
|
|
293
|
+
inform(ui, 'SupportPages.io is already writing this help centre. Showing its progress.');
|
|
294
|
+
return finish(bridge, existing.operation, existing.record);
|
|
295
|
+
}
|
|
296
|
+
if (existing?.operation?.status === 'failed') {
|
|
297
|
+
const code = existing.operation.error?.code ?? 'generation_failed';
|
|
298
|
+
ui.line(`The last run stopped: ${failureMeanings[code] ?? clean(code)} Resuming keeps everything it already wrote.`);
|
|
299
|
+
if (!options.yes && !await ui.confirm('Resume writing where it stopped?', true)) throw new Cancelled(`Nothing was started. Run ${command} to resume later.`);
|
|
300
|
+
bridge = await prerequisites(session, deps, helpers, bridge, { resuming: true });
|
|
301
|
+
let operation;
|
|
302
|
+
try { operation = await bridge.hosted.retry(existing.operation.id, existing.operation.attempt); }
|
|
303
|
+
catch (error) {
|
|
304
|
+
if (error.code === 'generation_running') return attachElsewhere(ui, command);
|
|
305
|
+
throw error;
|
|
306
|
+
}
|
|
307
|
+
return finish(bridge, operation, existing.record);
|
|
308
|
+
}
|
|
309
|
+
if (existing && !existing.operation) {
|
|
310
|
+
// Submitted, but the server never recorded it: start again with the same key.
|
|
311
|
+
return submit(session, deps, helpers, bridge, options, existing.record, command);
|
|
312
|
+
}
|
|
313
|
+
return submit(session, deps, helpers, bridge, options, undefined, command);
|
|
314
|
+
|
|
315
|
+
async function finish(current, operation, record) {
|
|
316
|
+
const followed = await followYolo(current, operation, { ui, deps, record, command: CLI_NAME });
|
|
317
|
+
if (followed.detached) return { status: 'detached', operation_id: operation.id, review_url: followed.operation.review_url };
|
|
318
|
+
summarizeYolo(followed.operation, { ui, resume: command });
|
|
319
|
+
const result = followed.operation.result ?? {};
|
|
320
|
+
return { status: followed.operation.status, operation_id: followed.operation.id, editor_url: result.editor_url ?? followed.operation.review_url, result };
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
async function submit(currentSession, currentDeps, currentHelpers, current, opts, previous, cmd) {
|
|
324
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
325
|
+
current = await prerequisites(currentSession, currentDeps, currentHelpers, current);
|
|
326
|
+
if (!previous) {
|
|
327
|
+
const capacity = await allowance(current);
|
|
328
|
+
const limit = capacity?.unlimited ? 'Your plan has no article limit, so every recommended article is written.'
|
|
329
|
+
: capacity ? `Your plan allows ${plural(capacity.remaining, 'more article')}${capacity.limit ? ` (of ${capacity.limit})` : ''}, so up to ${plural(capacity.remaining, 'draft')} ${capacity.remaining === 1 ? 'is' : 'are'} written.`
|
|
330
|
+
: 'Up to your plan’s article allowance is written; free plans cover 10 guides.';
|
|
331
|
+
const plan = ['SupportPages.io will, on its servers:', ' 1. Work out the sections of your help centre from the repository analysis',
|
|
332
|
+
' 2. Choose the articles your users need', ' 3. Write them as drafts', '', limit,
|
|
333
|
+
'Nothing goes live until you publish it. You can close this terminal; the run keeps going.'].join('\n');
|
|
334
|
+
if (ui.note) ui.note(plan, 'Ready to write the manual'); else ui.line(plan);
|
|
335
|
+
if (!opts.yes && !await ui.confirm('Start writing?', true)) throw new Cancelled('Nothing was started.');
|
|
336
|
+
}
|
|
337
|
+
const record = previous ?? { version: 1, key: randomUUID() };
|
|
338
|
+
// The key is saved before submission, so a lost response never starts a second run.
|
|
339
|
+
await saveYoloRun(current, { version: 1, key: record.key });
|
|
340
|
+
let operation;
|
|
341
|
+
try {
|
|
342
|
+
operation = await current.hosted.submit(YOLO_ACTION, {}, { request_id: record.key, decision: await resolveAction(current, YOLO_ACTION) });
|
|
343
|
+
} catch (error) {
|
|
344
|
+
if (error.code === 'generation_running') return attachElsewhere(ui, cmd);
|
|
345
|
+
// A prerequisite changed between the check and the submission: route it again.
|
|
346
|
+
if (['permission_denied', 'plan_limit', 'permission_required'].includes(error.code) || /^(repository_|analysis_|analysing$)/.test(error.code ?? '')) { previous = undefined; continue; }
|
|
347
|
+
throw error;
|
|
348
|
+
}
|
|
349
|
+
const saved = await saveYoloRun(current, { version: 1, key: record.key, operation_id: operation.id });
|
|
350
|
+
track('yolo_started');
|
|
351
|
+
say(ui, 'Started. SupportPages.io is writing your manual.');
|
|
352
|
+
return finish(current, operation, saved);
|
|
353
|
+
}
|
|
354
|
+
fail('setup_incomplete', `The help centre could not be started. Run ${cmd} again.`);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/** Another device or session started the run this folder does not know about. */
|
|
359
|
+
function attachElsewhere(ui, command) {
|
|
360
|
+
ui.line('SupportPages.io is already writing this help centre from another session or device. Watch it in SupportPages.io; drafts appear in the editor as they are written.');
|
|
361
|
+
return { status: 'already_running', instructions: `Run ${command} again after it finishes to write more.` };
|
|
362
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Plugin SessionStart hook: make sure Chromium for article screenshots exists.
|
|
3
|
+
//
|
|
4
|
+
// Plugin installs run npm without lifecycle scripts, so puppeteer's Chromium
|
|
5
|
+
// download never happened. This hook returns immediately (it must not delay the
|
|
6
|
+
// session or print into its context) and, when Chromium is missing, starts the
|
|
7
|
+
// download in a detached process guarded by a lock.
|
|
8
|
+
//
|
|
9
|
+
// node plugin-session.mjs check; start a background download if needed
|
|
10
|
+
// node plugin-session.mjs --download download now (used by the background process)
|
|
11
|
+
import { spawn } from 'node:child_process';
|
|
12
|
+
import { mkdir, open, rm, stat } from 'node:fs/promises';
|
|
13
|
+
import os from 'node:os';
|
|
14
|
+
import path from 'node:path';
|
|
15
|
+
import { fileURLToPath } from 'node:url';
|
|
16
|
+
import { command } from './lib/install.mjs';
|
|
17
|
+
import { ensureRenderer, rendererReady } from './lib/renderer.mjs';
|
|
18
|
+
|
|
19
|
+
const root = path.dirname(path.dirname(fileURLToPath(import.meta.url)));
|
|
20
|
+
const engine = path.join(root, 'engine');
|
|
21
|
+
const dataDir = process.env.CLAUDE_PLUGIN_DATA || path.join(os.tmpdir(), 'supportpages-writer');
|
|
22
|
+
const lockFile = path.join(dataDir, 'chromium-download.lock');
|
|
23
|
+
const STALE_MS = 30 * 60 * 1000;
|
|
24
|
+
const quiet = (cmd, args, options = {}) => command(cmd, args, { ...options, capture: true });
|
|
25
|
+
|
|
26
|
+
/** True when a download started less than 30 minutes ago is still recorded. */
|
|
27
|
+
export async function downloadInProgress() {
|
|
28
|
+
try { return Date.now() - (await stat(lockFile)).mtimeMs < STALE_MS; }
|
|
29
|
+
catch (error) { if (error.code === 'ENOENT') return false; throw error; }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Start a detached download unless one is already running. */
|
|
33
|
+
export async function startDownload() {
|
|
34
|
+
if (await downloadInProgress()) return;
|
|
35
|
+
const child = spawn(process.execPath, [fileURLToPath(import.meta.url), '--download'], { detached: true, stdio: 'ignore', env: process.env });
|
|
36
|
+
child.on('error', () => {});
|
|
37
|
+
child.unref();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function download() {
|
|
41
|
+
await mkdir(dataDir, { recursive: true });
|
|
42
|
+
let handle;
|
|
43
|
+
try { handle = await open(lockFile, 'wx'); }
|
|
44
|
+
catch (error) {
|
|
45
|
+
if (error.code !== 'EEXIST' || await downloadInProgress()) return;
|
|
46
|
+
await rm(lockFile, { force: true });
|
|
47
|
+
handle = await open(lockFile, 'wx');
|
|
48
|
+
}
|
|
49
|
+
try {
|
|
50
|
+
await handle.writeFile(String(process.pid));
|
|
51
|
+
await ensureRenderer(engine, quiet);
|
|
52
|
+
} finally {
|
|
53
|
+
await handle.close();
|
|
54
|
+
await rm(lockFile, { force: true });
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
59
|
+
try {
|
|
60
|
+
if (process.argv.includes('--download')) await download();
|
|
61
|
+
else if (!await rendererReady(engine, quiet)) await startDownload();
|
|
62
|
+
} catch { /* A hook must never break the session. */ }
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export { lockFile };
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { prepareUpdate } from './lib/prepare-update.mjs';
|
|
5
|
+
|
|
6
|
+
try {
|
|
7
|
+
if (!process.argv[2]) throw Error('The previous installation is required.');
|
|
8
|
+
await prepareUpdate(process.argv[2], path.dirname(path.dirname(fileURLToPath(import.meta.url))));
|
|
9
|
+
} catch (error) {
|
|
10
|
+
process.stderr.write(error.message + '\n');
|
|
11
|
+
process.exitCode = 1;
|
|
12
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { copyFile, readFile, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { realpathSync } from 'node:fs';
|
|
4
|
+
import { createHash } from 'node:crypto';
|
|
5
|
+
import { execFile } from 'node:child_process';
|
|
6
|
+
import { parseArgs, promisify } from 'node:util';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import { pathToFileURL } from 'node:url';
|
|
9
|
+
import { checkRelease, releaseVersionPattern, repositoryRoot } from './check-release.mjs';
|
|
10
|
+
|
|
11
|
+
// Keep publishing independent of the built CLI and its npm dependencies.
|
|
12
|
+
const execute = promisify(execFile);
|
|
13
|
+
const runAws = async (args, env) => (await execute('aws', args, { env, maxBuffer: 1024 * 1024 })).stdout;
|
|
14
|
+
|
|
15
|
+
function newerThan(version, previous) {
|
|
16
|
+
if (!releaseVersionPattern.test(previous)) throw Error('The remote latest.txt is not a valid release version.');
|
|
17
|
+
const a = version.split('.').map(BigInt), b = previous.split('.').map(BigInt);
|
|
18
|
+
for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return a[i] > b[i];
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export async function publishRelease({ directory = 'release', root = repositoryRoot, dryRun = false, env = process.env, run = runAws } = {}) {
|
|
23
|
+
directory = path.resolve(directory);
|
|
24
|
+
const version = await checkRelease({ root, env });
|
|
25
|
+
const archives = [];
|
|
26
|
+
// Validate every platform before making any remote changes.
|
|
27
|
+
for (const platform of ['darwin', 'linux']) for (const arch of ['arm64', 'x64']) {
|
|
28
|
+
const name = `supportpages-${version}-${platform}-${arch}.tar.gz`;
|
|
29
|
+
const sha256 = createHash('sha256').update(await readFile(path.join(directory, name))).digest('hex');
|
|
30
|
+
if ((await readFile(path.join(directory, name + '.sha256'), 'utf8')).trim() !== `${sha256} ${name}`) {
|
|
31
|
+
throw Error(`Release checksum mismatch: ${name}`);
|
|
32
|
+
}
|
|
33
|
+
archives.push({ platform, arch, name, sha256 });
|
|
34
|
+
}
|
|
35
|
+
const manifest = { version, archives };
|
|
36
|
+
await writeFile(path.join(directory, 'manifest.json'), JSON.stringify(manifest, null, 2) + '\n');
|
|
37
|
+
await writeFile(path.join(directory, 'latest.txt'), version + '\n');
|
|
38
|
+
await copyFile(path.join(root, 'install-cli.sh'), path.join(directory, 'install.sh'));
|
|
39
|
+
if (dryRun) return manifest;
|
|
40
|
+
|
|
41
|
+
const bucket = env.CLI_RELEASE_BUCKET;
|
|
42
|
+
if (!bucket || !/^[a-z0-9][a-z0-9-]{1,61}[a-z0-9]$/.test(bucket)) {
|
|
43
|
+
throw Error('Set CLI_RELEASE_BUCKET to the dedicated R2 download bucket.');
|
|
44
|
+
}
|
|
45
|
+
// Accept Cloudflare's default and jurisdiction-specific S3 endpoints only.
|
|
46
|
+
const endpoint = env.CLI_RELEASE_R2_ENDPOINT;
|
|
47
|
+
if (!/^https:\/\/[a-f0-9]{32}(?:\.(?:eu|fedramp|us))?\.r2\.cloudflarestorage\.com\/?$/.test(endpoint ?? '')) {
|
|
48
|
+
throw Error('Set CLI_RELEASE_R2_ENDPOINT to the HTTPS S3 API endpoint shown in the R2 bucket settings.');
|
|
49
|
+
}
|
|
50
|
+
if (!env.AWS_ACCESS_KEY_ID || !env.AWS_SECRET_ACCESS_KEY) {
|
|
51
|
+
throw Error('Set AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY to the R2 access key pair.');
|
|
52
|
+
}
|
|
53
|
+
const awsEnv = { ...env, AWS_DEFAULT_REGION: 'auto', AWS_REGION: 'auto', AWS_EC2_METADATA_DISABLED: 'true', AWS_PAGER: '',
|
|
54
|
+
AWS_REQUEST_CHECKSUM_CALCULATION: 'when_required', AWS_RESPONSE_CHECKSUM_VALIDATION: 'when_required' };
|
|
55
|
+
delete awsEnv.AWS_SESSION_TOKEN;
|
|
56
|
+
delete awsEnv.AWS_PROFILE;
|
|
57
|
+
const aws = args => run(['--endpoint-url', endpoint, '--region', 'auto', ...args], awsEnv);
|
|
58
|
+
const contents = async (prefix, maxKeys) => {
|
|
59
|
+
const result = JSON.parse(await aws(['s3api', 'list-objects-v2', '--bucket', bucket, '--prefix', prefix, '--max-keys', String(maxKeys), '--no-paginate', '--output', 'json']));
|
|
60
|
+
if (!result || typeof result !== 'object' || (result.Contents !== undefined && !Array.isArray(result.Contents))) {
|
|
61
|
+
throw Error('Could not check the R2 bucket contents.');
|
|
62
|
+
}
|
|
63
|
+
return result.Contents ?? [];
|
|
64
|
+
};
|
|
65
|
+
if ((await contents(`${version}/`, 1)).length) throw Error('Cannot publish over an existing release. Use a new version.');
|
|
66
|
+
if ((await contents('latest.txt', 1)).some(object => object.Key === 'latest.txt')) {
|
|
67
|
+
const previous = (await aws(['s3', 'cp', `s3://${bucket}/latest.txt`, '-', '--only-show-errors'])).trim();
|
|
68
|
+
if (!newerThan(version, previous)) throw Error(`Cannot promote ${version}: latest is already ${previous}.`);
|
|
69
|
+
}
|
|
70
|
+
const upload = async (name, key, cacheControl) => {
|
|
71
|
+
const contentType = name.endsWith('.tar.gz') ? 'application/gzip' : name.endsWith('.json') ? 'application/json' : 'text/plain';
|
|
72
|
+
await aws(['s3', 'cp', path.join(directory, name), `s3://${bucket}/${key}`, '--cache-control', cacheControl, '--content-type', contentType, '--only-show-errors']);
|
|
73
|
+
};
|
|
74
|
+
for (const name of [...archives.flatMap(archive => [archive.name, archive.name + '.sha256']), 'manifest.json', 'install.sh']) {
|
|
75
|
+
await upload(name, `${version}/${name}`, 'public,max-age=31536000,immutable');
|
|
76
|
+
}
|
|
77
|
+
// Mutable entry points are promoted only after every versioned upload succeeds.
|
|
78
|
+
await upload('install.sh', 'install.sh', 'no-cache');
|
|
79
|
+
await upload('latest.txt', 'latest.txt', 'no-cache');
|
|
80
|
+
return manifest;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href) {
|
|
84
|
+
const { values } = parseArgs({ options: { directory: { type: 'string' }, 'dry-run': { type: 'boolean' } } });
|
|
85
|
+
try {
|
|
86
|
+
const manifest = await publishRelease({ directory: values.directory, dryRun: values['dry-run'] });
|
|
87
|
+
process.stdout.write(values['dry-run'] ? JSON.stringify(manifest, null, 2) + '\n' : `Published SupportPages.io ${manifest.version} to R2.\n`);
|
|
88
|
+
} catch (error) {
|
|
89
|
+
process.stderr.write(`Release failed: ${error.message}\nlatest.txt is promoted only after all other uploads succeed.\n`);
|
|
90
|
+
process.exitCode = 1;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: supportpages
|
|
3
|
+
description: Create or retry illustrated help articles in SupportPages.io using its MCP connection and article writer. Use for SupportPages.io article requests, including generating an unpublished draft for review.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
<!-- Managed by SupportPages: article coordination v1 -->
|
|
7
|
+
|
|
8
|
+
# SupportPages.io articles
|
|
9
|
+
|
|
10
|
+
Use the SupportPages.io MCP connection for the current project.
|
|
11
|
+
|
|
12
|
+
For inventory questions such as "what support articles do we have?", call
|
|
13
|
+
`supportpages_list_articles` directly, without status or initialization first.
|
|
14
|
+
When signed out or in local mode it reads the project's current local article
|
|
15
|
+
files. Report those titles and paths without asking the user to sign in. An empty
|
|
16
|
+
local list means no local articles were found, not that the hosted help centre is
|
|
17
|
+
empty. Do not infer the inventory from Git status. Use `source: "hosted"` only
|
|
18
|
+
when the user explicitly asks what is on SupportPages.io.
|
|
19
|
+
|
|
20
|
+
For article creation, check
|
|
21
|
+
`supportpages_status`, finish initialization if required, and call
|
|
22
|
+
`supportpages_prepare_article` before writing. Follow the returned readiness or
|
|
23
|
+
capacity instructions. Show the prepared editor link; a preview is not a finished
|
|
24
|
+
draft.
|
|
25
|
+
|
|
26
|
+
When the prepared run has `prefer_background: true`, **delegate the writing task
|
|
27
|
+
to a background subagent**. This skill explicitly requests delegation for that
|
|
28
|
+
article. Use the SupportPages.io writer and the returned Codex launch instructions,
|
|
29
|
+
including the complete task brief, model settings and self-contained entrypoint.
|
|
30
|
+
Pass the complete task brief directly in the subagent tool's task/message argument
|
|
31
|
+
on launches and retries. Do not copy it into a temporary JSON file, create a
|
|
32
|
+
handoff script, or print it with a shell command. The existing entrypoint already
|
|
33
|
+
loads the saved run; no parent-side file edit is needed to launch the writer.
|
|
34
|
+
If no custom-agent selector exists, use a named SupportPages.io subagent with the
|
|
35
|
+
supplied writer instructions. The writer runs the entrypoint before generating
|
|
36
|
+
anything, uses the command wrapper, and runs the finish command after validation.
|
|
37
|
+
|
|
38
|
+
**Prioritize completion and approvals over quiet operation.** After launch,
|
|
39
|
+
record the actual execution mode and host task identity with
|
|
40
|
+
`supportpages_update_run`. Show the editor link and say "launched" unless actual
|
|
41
|
+
writer startup is confirmed. Keep the main turn active and supervise the writer
|
|
42
|
+
using the host's agent wait/message facilities. Use bounded waits and concise
|
|
43
|
+
progress updates; do not end the turn merely because launch succeeded.
|
|
44
|
+
|
|
45
|
+
The writer may send milestone updates and must surface blockers immediately.
|
|
46
|
+
When an operation needs permission and host policy permits it, the writer should
|
|
47
|
+
request approval directly through the host's native approval mechanism, with the
|
|
48
|
+
exact command and reason. A sandbox restriction is not an explicit user denial.
|
|
49
|
+
Do not suppress approval prompts or route them only to an idle parent. Continue
|
|
50
|
+
after approval is granted. An explicit denial or a policy forbidding approval
|
|
51
|
+
requests stops that action; never bypass it or enable YOLO.
|
|
52
|
+
|
|
53
|
+
If a background writer cannot surface approvals, confirm it has stopped and
|
|
54
|
+
continue the same article in the foreground using MCP recovery instructions and
|
|
55
|
+
the normal approval mechanism. Foreground execution retains the same permissions.
|
|
56
|
+
If the host cannot supervise a delegated task, use foreground execution. Keep
|
|
57
|
+
the MCP session alive; do not launch a separate agent CLI.
|
|
58
|
+
|
|
59
|
+
Inspect the writer's final result and saved run. A host "Completed" notification
|
|
60
|
+
does not establish success. For a stopped writer that failed or is
|
|
61
|
+
`permission_blocked` before generation finished, record `event: "failed"` and
|
|
62
|
+
`stopped: true` with `supportpages_update_run`, then show the specific cause,
|
|
63
|
+
recovery and editor link. Do not mark a writer failed while awaiting approval.
|
|
64
|
+
If generation succeeded but delivery is pending or failed, recover with
|
|
65
|
+
`supportpages_complete_article` without regenerating or marking the writer failed.
|
|
66
|
+
|
|
67
|
+
The main agent owns MCP calls. After successful writing, call
|
|
68
|
+
`supportpages_complete_article` to confirm delivery, report the final editor link
|
|
69
|
+
and follow its completion instructions. Leave the article unpublished until the
|
|
70
|
+
user affirmatively asks to publish that draft.
|
|
71
|
+
|
|
72
|
+
**Local folders.** When `supportpages_status` or `supportpages_init` reports
|
|
73
|
+
`local`, the folder has no help centre and needs no sign-in: articles are saved
|
|
74
|
+
as Markdown with their screenshots. There is no capacity check, preview or editor
|
|
75
|
+
link. Show the saved markdown path returned by `supportpages_complete_article` as
|
|
76
|
+
the deliverable and never ask whether to publish automatically. Mention once
|
|
77
|
+
that running `wtfm publish` in the terminal hosts the saved articles.
|
|
78
|
+
|
|
79
|
+
If the user explicitly asks to host or publish saved local articles, call
|
|
80
|
+
`supportpages_publish`. Follow its browser approval and help-centre selection
|
|
81
|
+
steps, then pass the selected article slugs. It uploads drafts and connects the
|
|
82
|
+
folder for future articles. Do not accept credentials in chat or approve sign-in
|
|
83
|
+
for the user. Making those drafts publicly visible is a separate action using
|
|
84
|
+
`supportpages_publish_article` when authorized.
|
|
85
|
+
|
|
86
|
+
**Hosted analysis and videos.** Use `supportpages_find_article_gaps`,
|
|
87
|
+
`supportpages_suggest_sections`, `supportpages_recommend_articles` or
|
|
88
|
+
`supportpages_create_video_walkthrough` for the requested action. These tools
|
|
89
|
+
check the prerequisites and open required browser setup pages. When signed out,
|
|
90
|
+
first explain that an account/device sign-in is needed; sign in or create an
|
|
91
|
+
account through the opened approval page. Do not begin with a repository invitation
|
|
92
|
+
or send the user to the app homepage. Show the comparison code, and show the link
|
|
93
|
+
as a fallback if `browser_opened` is false. Never approve browser consent.
|
|
94
|
+
|
|
95
|
+
After the user completes a step, call `supportpages_setup_hosted` with the
|
|
96
|
+
returned `next_arguments`. Ask which help centre to use when `project_required`
|
|
97
|
+
is returned (or use `supportpages_create_project` if the user wants a new one),
|
|
98
|
+
then pass its `project_id`. The next required device-consent or repository setup
|
|
99
|
+
page opens automatically. Preserve the original request and `resume_arguments`
|
|
100
|
+
through these steps; do not start local analysis, recording or unrelated jobs.
|
|
101
|
+
Once setup is available, retry `resume_tool` with `resume_arguments`. Walkthroughs
|
|
102
|
+
require a completed hosted article: use `supportpages_list_articles` with
|
|
103
|
+
`source: "hosted"` to select it. Do not restore deleted files or upload a local
|
|
104
|
+
article without the user's explicit instruction. Finished videos remain private
|
|
105
|
+
until explicitly shared.
|
|
106
|
+
|
|
107
|
+
Honor an explicit request to return immediately, explaining that approvals and
|
|
108
|
+
completion may need another user turn on hosts that do not wake the parent.
|
|
109
|
+
Retain the run ID and host task identity; reconcile the run on resumption.
|
|
110
|
+
|
|
111
|
+
Honor an explicit foreground request. If delegation cannot run, distinguish a
|
|
112
|
+
missing host tool, an explicit session restriction, or the actual launch error;
|
|
113
|
+
do not infer that background execution is disabled merely because the user did
|
|
114
|
+
not separately request a subagent. Explain the specific limitation and use the
|
|
115
|
+
same prepared brief in the foreground. Stop any launched writer before switching
|
|
116
|
+
modes. Do not override host restrictions or launch a separate agent CLI.
|
|
117
|
+
|
|
118
|
+
For retries, follow the MCP recovery instructions and use
|
|
119
|
+
`supportpages_retry_article` after stopping the old writer. If generation already
|
|
120
|
+
finished and delivery failed, recover delivery instead of regenerating.
|