@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,252 @@
|
|
|
1
|
+
import readline from 'node:readline';
|
|
2
|
+
import { Writable } from 'node:stream';
|
|
3
|
+
import { stripVTControlCharacters } from 'node:util';
|
|
4
|
+
import { CLI_NAME, LEGACY_CLI_NAME } from './brand.mjs';
|
|
5
|
+
// The standalone CLI bundles Clack. The source install.sh bootstrap can run
|
|
6
|
+
// before npm dependencies exist, so keep its plain prompts and --help usable.
|
|
7
|
+
let prompts;
|
|
8
|
+
try {
|
|
9
|
+
prompts = await import('@clack/prompts');
|
|
10
|
+
}
|
|
11
|
+
catch (error) {
|
|
12
|
+
if (error.code !== 'ERR_MODULE_NOT_FOUND' || !error.message.includes("Cannot find package '@clack/prompts'")) throw error;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const plain = value => stripVTControlCharacters(String(value)).replace(/[\r\n\t]/g, ' ').replace(/[\p{Cc}\p{Cf}]/gu, '');
|
|
16
|
+
|
|
17
|
+
// Style human-facing command references at the display boundary, keeping saved
|
|
18
|
+
// messages, prompt values and machine-readable output free of ANSI escapes.
|
|
19
|
+
export function emphasizeCommands(text, output = process.stdout, env = process.env) {
|
|
20
|
+
if (!output.isTTY || env.TERM === 'dumb' || env.NO_COLOR !== undefined) return String(text);
|
|
21
|
+
const argument = String.raw`(?:'[^'\n]*'|"[^"\n]*"|[^\s,;!?]+)`;
|
|
22
|
+
const flag = String.raw`(?:--(?:dev|refresh|yes|json|skills-only)\b|--(?:workspace|api-url|config-dir|skills-dir|project|agent|app-type)(?:[ \t]+|=)${argument})`;
|
|
23
|
+
const command = new RegExp(String.raw`(?<![\w/.-])(?:${CLI_NAME}|${LEGACY_CLI_NAME})[ \t]+(?:setup|init|uninit|login|logout|status|sync|configure|doctor|analyse|sections|recommend|write|publish|yolo|update|remove|--help|--version)\b(?:[ \t]+${flag})*`, 'g');
|
|
24
|
+
return String(text).replace(command, value => {
|
|
25
|
+
const suffix = value.match(/[.]+$/)?.[0] ?? '';
|
|
26
|
+
return `\x1b[1;36m${suffix ? value.slice(0, -suffix.length) : value}\x1b[39;22m${suffix}`;
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const wrapWords = (text, width) => {
|
|
31
|
+
const lines = [];
|
|
32
|
+
let current = '';
|
|
33
|
+
for (const word of text.trim().split(/\s+/u)) {
|
|
34
|
+
if (current && Array.from(`${current} ${word}`).length <= width) { current += ` ${word}`; continue; }
|
|
35
|
+
if (current) lines.push(current);
|
|
36
|
+
const chars = Array.from(word);
|
|
37
|
+
while (chars.length > width) lines.push(chars.splice(0, width).join(''));
|
|
38
|
+
current = chars.join('');
|
|
39
|
+
}
|
|
40
|
+
if (current) lines.push(current);
|
|
41
|
+
return lines;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
// "Write the F***ing Manual" in figlet's "Pagga" font, over two lines so it
|
|
45
|
+
// fits an 80-column terminal. Generated once with figlet (see
|
|
46
|
+
// dev/figlet-fonts.mjs); the CLI does not depend on it.
|
|
47
|
+
const logoRows = [
|
|
48
|
+
'░█░█░█▀▄░▀█▀░▀█▀░█▀▀░░░▀█▀░█░█░█▀▀░░░░░░░░░░░░░░░░░░░░',
|
|
49
|
+
'░█▄█░█▀▄░░█░░░█░░█▀▀░░░░█░░█▀█░█▀▀░░░░░░░░░░░░░░░░░░░░',
|
|
50
|
+
'░▀░▀░▀░▀░▀▀▀░░▀░░▀▀▀░░░░▀░░▀░▀░▀▀▀░░░░░░░░░░░░░░░░░░░░',
|
|
51
|
+
'░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░',
|
|
52
|
+
'░█▀▀░▄░▄░▄░▄░▄░▄░▀█▀░█▀█░█▀▀░░░█▄█░█▀█░█▀█░█░█░█▀█░█░░',
|
|
53
|
+
'░█▀▀░▄█▄░▄█▄░▄█▄░░█░░█░█░█░█░░░█░█░█▀█░█░█░█░█░█▀█░█░░',
|
|
54
|
+
'░▀░░░▄▀▄░▄▀▄░▄▀▄░▀▀▀░▀░▀░▀▀▀░░░▀░▀░▀░▀░▀░▀░▀▀▀░▀░▀░▀▀▀',
|
|
55
|
+
];
|
|
56
|
+
const logoWidth = 54;
|
|
57
|
+
// "F***ing" (rows 4-6, before this column) is green, as on the landing page;
|
|
58
|
+
// the ░ texture is dimmed.
|
|
59
|
+
const accentRow = 4, accentColumn = 28;
|
|
60
|
+
|
|
61
|
+
/** The logo when it fits unwrapped, otherwise undefined. */
|
|
62
|
+
export function logo({ columns = 80, color = false } = {}) {
|
|
63
|
+
if (columns <= logoWidth) return undefined;
|
|
64
|
+
if (!color) return logoRows.join('\n');
|
|
65
|
+
const paint = (char, row, column) =>
|
|
66
|
+
char === '░' ? `\x1b[90m${char}\x1b[39m` : row >= accentRow && column < accentColumn ? `\x1b[32m${char}\x1b[39m` : char;
|
|
67
|
+
return logoRows.map((row, index) => Array.from(row, (char, column) => paint(char, index, column)).join('')
|
|
68
|
+
.replace(/\x1b\[39m\x1b\[(90|32)m/g, '\x1b[$1m')).join('\n');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export class Cancelled extends Error {
|
|
72
|
+
constructor(message = 'Setup cancelled. Completed installation steps are kept; rerun to continue.') { super(message); }
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function createTerminal(input = process.stdin, output = process.stdout, env = process.env) {
|
|
76
|
+
let active;
|
|
77
|
+
const color = output.isTTY && env.TERM !== 'dumb' && env.NO_COLOR === undefined;
|
|
78
|
+
const menus = prompts && input.isTTY && output.isTTY && typeof input.setRawMode === 'function' && env.TERM !== 'dumb';
|
|
79
|
+
const accent = text => color ? `\x1b[36m${text}\x1b[0m` : text;
|
|
80
|
+
const format = text => emphasizeCommands(text, output, env);
|
|
81
|
+
const raw = text => output.write(text + '\n');
|
|
82
|
+
// Clack does not wrap log lines; wrap paragraphs to the frame width ourselves.
|
|
83
|
+
const wrap = text => String(text).split('\n').flatMap(paragraph => paragraph.trim() ? wrapWords(paragraph, Math.max(20, (output.columns || 80) - 5)) : ['']).join('\n');
|
|
84
|
+
// Interactive sessions keep every message inside Clack's guide line; literal
|
|
85
|
+
// output (JSON, commands to copy) is never reflowed or prefixed.
|
|
86
|
+
const line = (text, { literal = false } = {}) => literal || !menus ? raw(literal ? text : format(text)) : prompts.log.message(format(wrap(text)), display);
|
|
87
|
+
// Clack uses process-wide colour detection. Respect this terminal's NO_COLOR
|
|
88
|
+
// setting too, while retaining cursor movement for keyboard navigation.
|
|
89
|
+
const styledOutput = new Proxy(output, {
|
|
90
|
+
get(target, key) {
|
|
91
|
+
if (key === 'columns') return target.columns > 0 ? target.columns : 80;
|
|
92
|
+
if (key === 'rows') return target.rows > 0 ? target.rows : 24;
|
|
93
|
+
if (key === 'write' && !color) return (chunk, ...args) => target.write(String(chunk).replace(/\x1b\[[0-9;]*m/g, ''), ...args);
|
|
94
|
+
const value = Reflect.get(target, key, target);
|
|
95
|
+
return typeof value === 'function' ? value.bind(target) : value;
|
|
96
|
+
},
|
|
97
|
+
});
|
|
98
|
+
const display = { output: styledOutput };
|
|
99
|
+
const prompt = async (render, options) => {
|
|
100
|
+
const controller = new AbortController();
|
|
101
|
+
const cancel = () => controller.abort();
|
|
102
|
+
const onKey = (_text, key) => { if (key?.ctrl && key.name === 'd') cancel(); };
|
|
103
|
+
const wasRaw = Boolean(input.isRaw), wasFlowing = input.readableFlowing === true;
|
|
104
|
+
active = cancel;
|
|
105
|
+
input.once('end', cancel);
|
|
106
|
+
input.once('close', cancel);
|
|
107
|
+
input.on('keypress', onKey);
|
|
108
|
+
try {
|
|
109
|
+
const value = await render({ ...options, input, ...display, signal: controller.signal });
|
|
110
|
+
if (prompts.isCancel(value)) throw new Cancelled();
|
|
111
|
+
return value;
|
|
112
|
+
} finally {
|
|
113
|
+
input.off('end', cancel);
|
|
114
|
+
input.off('close', cancel);
|
|
115
|
+
input.off('keypress', onKey);
|
|
116
|
+
input.setRawMode(wasRaw);
|
|
117
|
+
if (!wasFlowing) input.pause();
|
|
118
|
+
if (active === cancel) active = undefined;
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
const ask = (label, fallback = '', hidden = false) => new Promise((resolve, reject) => {
|
|
122
|
+
if (!input.isTTY || !output.isTTY) return reject(new Error('Interactive setup requires a terminal. Use --yes with explicit options for unattended setup.'));
|
|
123
|
+
// A muted output stream keeps readline editing/history off screen for secrets.
|
|
124
|
+
// There is no fallback to echoed input on terminals that cannot hide it.
|
|
125
|
+
const sink = hidden ? new Writable({ write(_chunk, _encoding, done) { done(); } }) : output;
|
|
126
|
+
const rl = readline.createInterface({ input, output: sink, terminal: true, historySize: 0 });
|
|
127
|
+
let settled = false;
|
|
128
|
+
const finish = (err, value) => {
|
|
129
|
+
if (settled) return;
|
|
130
|
+
settled = true;
|
|
131
|
+
active = undefined;
|
|
132
|
+
rl.close();
|
|
133
|
+
if (hidden) output.write('\n');
|
|
134
|
+
err ? reject(err) : resolve(value?.trim() || fallback);
|
|
135
|
+
};
|
|
136
|
+
active = () => finish(new Cancelled());
|
|
137
|
+
rl.once('SIGINT', () => finish(new Cancelled()));
|
|
138
|
+
rl.once('close', () => { if (!settled) finish(new Cancelled()); });
|
|
139
|
+
output.write(`${label}${fallback ? ` [${fallback}]` : ''}: `);
|
|
140
|
+
rl.question('', value => finish(null, value));
|
|
141
|
+
});
|
|
142
|
+
const choices = options => {
|
|
143
|
+
if (!options.length) throw new Error('This question has no options to select.');
|
|
144
|
+
return options.map(option => ({ ...option, label: plain(option.label), ...(option.hint ? { hint: plain(option.hint) } : {}) }));
|
|
145
|
+
};
|
|
146
|
+
return {
|
|
147
|
+
cancel: () => active?.(),
|
|
148
|
+
line,
|
|
149
|
+
// A fresh screen keeps the wizard's frame and choices in view; scrollback is kept.
|
|
150
|
+
// The wordmark is decoration: only on the wizard screen, and only when it fits unwrapped.
|
|
151
|
+
intro: (title, { banner = false } = {}) => {
|
|
152
|
+
if (!menus) return line(`\n${title}`);
|
|
153
|
+
output.write('\x1b[H\x1b[2J');
|
|
154
|
+
const art = banner && logo({ columns: output.columns || 80, color });
|
|
155
|
+
if (art) output.write(`\n${art}\n\n`);
|
|
156
|
+
prompts.intro(title, display);
|
|
157
|
+
},
|
|
158
|
+
// Clack starts the outro after its └ corner; align continuation lines with the first.
|
|
159
|
+
outro: text => menus ? prompts.outro(format(wrap(text)).replace(/\n/g, '\n '), display) : line(`\n${text}`),
|
|
160
|
+
note: (text, title) => menus ? prompts.note(format(text), title, display) : line(`\n${title}\n${text}`),
|
|
161
|
+
info: text => menus ? prompts.log.info(format(wrap(text)), display) : line(text),
|
|
162
|
+
progress: initial => {
|
|
163
|
+
// Clack's spinner captures stdin and exits on Ctrl+C. Keep this display
|
|
164
|
+
// render-only so the runner can stop the child and restore the terminal.
|
|
165
|
+
let stopped = false, lastLine = Date.now(), lastMessage = plain(initial);
|
|
166
|
+
let frame = 0, animation;
|
|
167
|
+
const render = () => {
|
|
168
|
+
const width = Math.max(8, (output.columns || 80) - 6);
|
|
169
|
+
const chars = Array.from(lastMessage);
|
|
170
|
+
const text = chars.length > width ? chars.slice(0, width - 1).join('') + '…' : lastMessage;
|
|
171
|
+
output.write(`\r\x1b[2K│ ${accent(['|', '/', '-', '\\'][frame++ % 4])} ${format(text)}`);
|
|
172
|
+
};
|
|
173
|
+
if (menus) {
|
|
174
|
+
output.write('\x1b[?25l'); render();
|
|
175
|
+
animation = setInterval(render, 100);
|
|
176
|
+
} else line(lastMessage);
|
|
177
|
+
return {
|
|
178
|
+
update(message, { heartbeat = false } = {}) {
|
|
179
|
+
if (stopped) return;
|
|
180
|
+
const text = plain(message);
|
|
181
|
+
if (menus) { lastMessage = text; render(); return; }
|
|
182
|
+
if (text === lastMessage || (heartbeat && Date.now() - lastLine < 15000)) return;
|
|
183
|
+
line(text); lastMessage = text; lastLine = Date.now();
|
|
184
|
+
},
|
|
185
|
+
stop(message, status = 'success') {
|
|
186
|
+
if (stopped) return;
|
|
187
|
+
stopped = true;
|
|
188
|
+
clearInterval(animation);
|
|
189
|
+
if (!menus) return line(plain(message));
|
|
190
|
+
output.write('\r\x1b[2K\x1b[?25h');
|
|
191
|
+
(status === 'success' ? prompts.log.success : status === 'cancelled' ? prompts.log.warn : prompts.log.error)(format(wrap(plain(message))), display);
|
|
192
|
+
},
|
|
193
|
+
};
|
|
194
|
+
},
|
|
195
|
+
step: (number, title, total) => {
|
|
196
|
+
const text = `${number}${total ? ` of ${total}` : ''}. ${title}`;
|
|
197
|
+
menus ? prompts.log.step(text, display) : line(`\n${accent(text)}`);
|
|
198
|
+
},
|
|
199
|
+
ok: text => menus ? prompts.log.success(format(wrap(text)), display) : line(` OK ${text}`),
|
|
200
|
+
async ask(label, fallback = '', { validate } = {}) {
|
|
201
|
+
if (menus) return (await prompt(prompts.text, { message: plain(label), placeholder: plain(fallback), defaultValue: fallback,
|
|
202
|
+
validate: value => validate?.(value?.trim() || fallback) })).trim() || fallback;
|
|
203
|
+
while (true) {
|
|
204
|
+
const value = await ask(label, fallback);
|
|
205
|
+
const error = validate?.(value);
|
|
206
|
+
if (!error) return value;
|
|
207
|
+
line(error);
|
|
208
|
+
}
|
|
209
|
+
},
|
|
210
|
+
secret: label => menus ? prompt(prompts.password, { message: plain(label), mask: '*' }) : ask(label, '', true),
|
|
211
|
+
async confirm(label, fallback = true) {
|
|
212
|
+
if (menus) return prompt(prompts.confirm, { message: plain(label), initialValue: fallback });
|
|
213
|
+
while (true) {
|
|
214
|
+
const answer = (await ask(`${label} (${fallback ? 'Y/n' : 'y/N'})`, fallback ? 'y' : 'n')).toLowerCase();
|
|
215
|
+
if (['y', 'yes'].includes(answer)) return true;
|
|
216
|
+
if (['n', 'no'].includes(answer)) return false;
|
|
217
|
+
line('Please enter y or n.');
|
|
218
|
+
}
|
|
219
|
+
},
|
|
220
|
+
async choose(label, options, defaultIndex = 0) {
|
|
221
|
+
options = choices(options);
|
|
222
|
+
// Short menus are plain lists; searching only helps once a list would scroll.
|
|
223
|
+
if (menus && options.length <= 8) return prompt(prompts.select, { message: plain(label), options, initialValue: options[defaultIndex]?.value });
|
|
224
|
+
if (menus) return prompt(prompts.autocomplete, { message: plain(label), options, initialValue: options[defaultIndex]?.value,
|
|
225
|
+
placeholder: 'Type to filter', maxItems: 8,
|
|
226
|
+
validate: value => options.some(option => option.value === value) ? undefined : 'Choose a matching option, or clear your search.' });
|
|
227
|
+
line(label);
|
|
228
|
+
options.forEach((option, i) => line(` ${i + 1}) ${option.label}`));
|
|
229
|
+
while (true) {
|
|
230
|
+
const value = await ask('Choose a number', String(defaultIndex + 1));
|
|
231
|
+
const index = Number(value) - 1;
|
|
232
|
+
if (Number.isInteger(index) && index >= 0 && index < options.length) return options[index].value;
|
|
233
|
+
line(`Please enter a number from 1 to ${options.length}.`);
|
|
234
|
+
}
|
|
235
|
+
},
|
|
236
|
+
async multiselect(label, options, defaults = []) {
|
|
237
|
+
options = choices(options);
|
|
238
|
+
if (menus) return prompt(prompts.autocompleteMultiselect, { message: plain(label), options, initialValues: defaults,
|
|
239
|
+
required: false, placeholder: 'Type to filter', maxItems: 8 });
|
|
240
|
+
line(label);
|
|
241
|
+
options.forEach((option, i) => line(` ${i + 1}) ${option.label}`));
|
|
242
|
+
const fallback = options.flatMap((option, index) => defaults.includes(option.value) ? [index + 1] : []).join(',');
|
|
243
|
+
while (true) {
|
|
244
|
+
const raw = await ask(defaults.length ? 'Choose numbers separated by commas, none to clear, or Enter to keep selection' : 'Choose numbers separated by commas, or Enter to skip', fallback);
|
|
245
|
+
if (!raw || raw.toLowerCase() === 'none') return [];
|
|
246
|
+
const indices = raw.split(',').map(value => Number(value.trim()) - 1);
|
|
247
|
+
if (indices.every(index => Number.isInteger(index) && index >= 0 && index < options.length)) return [...new Set(indices)].map(index => options[index].value);
|
|
248
|
+
line(`Please enter numbers from 1 to ${options.length}.`);
|
|
249
|
+
}
|
|
250
|
+
},
|
|
251
|
+
};
|
|
252
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
2
|
+
import { readFile, lstat, rename, unlink } from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { Workspace } from '../../dist/workspace.js';
|
|
5
|
+
import { Runs } from '../../dist/runs.js';
|
|
6
|
+
import { fail } from '../../dist/errors.js';
|
|
7
|
+
import { Cancelled } from './terminal.mjs';
|
|
8
|
+
import { CLI_NAME } from './brand.mjs';
|
|
9
|
+
|
|
10
|
+
const stateRoot = '.rtfm/supportpages';
|
|
11
|
+
const retained = new Set(['operation.lock', 'archives']);
|
|
12
|
+
async function readProfile(filename) {
|
|
13
|
+
try {
|
|
14
|
+
if (!(await lstat(filename)).isFile()) fail('invalid_configuration', 'The workspace profile must be a regular file.');
|
|
15
|
+
return await readFile(filename);
|
|
16
|
+
} catch (error) { if (error.code === 'ENOENT') return null; throw error; }
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Forget this folder's setup in every environment, without contacting an API. */
|
|
20
|
+
export async function uninitWorkspace(root, configDir, { ui, yes = false } = {}) {
|
|
21
|
+
const ws = await Workspace.create(root);
|
|
22
|
+
const profile = path.join(configDir, 'workspaces', createHash('sha256').update(ws.root).digest('hex') + '.json');
|
|
23
|
+
const before = await readProfile(profile);
|
|
24
|
+
const entries = async () => (await ws.list(stateRoot)).filter(entry => !retained.has(entry.name)).map(entry => entry.name).sort();
|
|
25
|
+
const originalEntries = await entries();
|
|
26
|
+
if (!before && !originalEntries.length) {
|
|
27
|
+
ui.ok(`This folder is already uninitialized. Run ${CLI_NAME} init to set it up.`);
|
|
28
|
+
return { status: 'uninitialized', already_uninitialized: true, workspace: ws.root };
|
|
29
|
+
}
|
|
30
|
+
ui.intro?.('SupportPages Writer · Forget project setup');
|
|
31
|
+
ui.line(`Folder: ${ws.root.replace(/[\p{Cc}\p{Cf}]/gu, '')}`);
|
|
32
|
+
ui.line('Forget this folder’s local, development and production setup, including its remembered API environment.');
|
|
33
|
+
ui.line('Setup and recovery files will be archived. Exported articles, remote content, device sign-in and agent integrations are kept. Hosted jobs already submitted keep running.');
|
|
34
|
+
if (!yes && !await ui.confirm('Forget setup for this folder?', false)) throw new Cancelled('Project setup was kept.');
|
|
35
|
+
const result = await ws.lock(async () => {
|
|
36
|
+
const current = await readProfile(profile);
|
|
37
|
+
if (!((before === null && current === null) || before?.equals(current ?? Buffer.alloc(0))) ||
|
|
38
|
+
JSON.stringify(await entries()) !== JSON.stringify(originalEntries)) {
|
|
39
|
+
fail('configuration_changed', `Project setup changed during review. Rerun ${CLI_NAME} uninit.`);
|
|
40
|
+
}
|
|
41
|
+
await new Runs(ws, stateRoot).assertAvailable();
|
|
42
|
+
const roots = [stateRoot, ...(await ws.list(`${stateRoot}/dev`)).filter(entry => entry.isDirectory()).map(entry => `${stateRoot}/dev/${entry.name}`)];
|
|
43
|
+
for (const directory of roots) {
|
|
44
|
+
const task = `${directory}/setup/task.json`;
|
|
45
|
+
if (await ws.exists(task) && (await ws.json(task)).status === 'running') {
|
|
46
|
+
fail('workspace_busy', `Finish or stop project analysis before running ${CLI_NAME} uninit.`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
const archive = `${stateRoot}/archives/uninit-${randomUUID()}`;
|
|
50
|
+
await ws.writeJson(`${archive}/recovery.json`, { version: 1, workspace: ws.root, profile, entries: originalEntries, created_at: new Date().toISOString() });
|
|
51
|
+
if (before) await ws.write(`${archive}/workspace-profile.json`, before);
|
|
52
|
+
const moved = [];
|
|
53
|
+
try {
|
|
54
|
+
for (const entry of originalEntries) {
|
|
55
|
+
await rename(await ws.resolve(`${stateRoot}/${entry}`), await ws.resolve(`${archive}/${entry}`));
|
|
56
|
+
moved.push(entry);
|
|
57
|
+
}
|
|
58
|
+
if (before) {
|
|
59
|
+
if (!before.equals(await readProfile(profile) ?? Buffer.alloc(0))) fail('configuration_changed', `The workspace environment changed. Rerun ${CLI_NAME} uninit.`);
|
|
60
|
+
await unlink(profile);
|
|
61
|
+
}
|
|
62
|
+
} catch (error) {
|
|
63
|
+
for (const entry of moved.reverse()) await rename(await ws.resolve(`${archive}/${entry}`), await ws.resolve(`${stateRoot}/${entry}`));
|
|
64
|
+
throw error;
|
|
65
|
+
}
|
|
66
|
+
return { status: 'uninitialized', workspace: ws.root, archive: await ws.resolve(archive) };
|
|
67
|
+
});
|
|
68
|
+
ui.ok(`Project setup removed. Run ${CLI_NAME} init to choose where articles go.`);
|
|
69
|
+
ui.line(`Recovery files: ${result.archive}`);
|
|
70
|
+
ui.line('Use --dev only when you want the development service. Restart existing agent sessions to refresh their connection state.');
|
|
71
|
+
return result;
|
|
72
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { lstat, readFile, realpath } from 'node:fs/promises';
|
|
3
|
+
import { fail } from '../../dist/errors.js';
|
|
4
|
+
import { command } from './install.mjs';
|
|
5
|
+
import { CLI_NAME } from './brand.mjs';
|
|
6
|
+
|
|
7
|
+
const versionPattern = /^\d+\.\d+\.\d+$/;
|
|
8
|
+
export function compareVersions(left, right) {
|
|
9
|
+
if (!versionPattern.test(left) || !versionPattern.test(right)) fail('invalid_release', 'The release version must be X.Y.Z. Retry after the release has been corrected.');
|
|
10
|
+
const a = left.split('.').map(BigInt), b = right.split('.').map(BigInt);
|
|
11
|
+
for (let i = 0; i < 3; i++) if (a[i] !== b[i]) return a[i] > b[i] ? 1 : -1;
|
|
12
|
+
return 0;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Update the managed executable without opening a workspace or authenticating. */
|
|
16
|
+
export async function releaseInstallation(installRoot, env = process.env) {
|
|
17
|
+
const reinstall = 'Rerun the SupportPages Writer installer with your original --data-dir and --bin-dir to enable updates. Your configuration and article files are kept.';
|
|
18
|
+
let receipt;
|
|
19
|
+
const home = env.SUPPORTPAGES_CLI_HOME;
|
|
20
|
+
if (!home) fail('unmanaged_installation', 'This command is running from a source or unmanaged installation. Rerun ./install-cli.sh from your source checkout, or use the public installer.');
|
|
21
|
+
const root = path.dirname(path.resolve(home));
|
|
22
|
+
try {
|
|
23
|
+
if (path.basename(home) !== 'current' || await realpath(path.join(home, 'mcp')) !== await realpath(installRoot)) throw Error();
|
|
24
|
+
const file = path.join(root, 'install.json');
|
|
25
|
+
if (!(await lstat(file)).isFile()) throw Error();
|
|
26
|
+
receipt = JSON.parse(await readFile(file, 'utf8'));
|
|
27
|
+
if (receipt.version !== 1 || !['release', 'local'].includes(receipt.type) ||
|
|
28
|
+
typeof receipt.data_dir !== 'string' || !path.isAbsolute(receipt.data_dir) ||
|
|
29
|
+
await realpath(receipt.data_dir) !== await realpath(root) ||
|
|
30
|
+
typeof receipt.bin_dir !== 'string' || !path.isAbsolute(receipt.bin_dir)) throw Error();
|
|
31
|
+
const url = new URL(receipt.release_url);
|
|
32
|
+
if (url.protocol !== 'https:' || url.username || url.password || url.search || url.hash) throw Error();
|
|
33
|
+
} catch { fail('invalid_installation', `The installation receipt is missing or invalid. ${reinstall}`); }
|
|
34
|
+
if (receipt.type === 'local') fail('local_installation', 'This is a local development build. Rerun ./install-cli.sh from your source checkout to update it.');
|
|
35
|
+
return receipt;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export async function updateCli({ installRoot, env = process.env, run = command, ui, checkTimeout = 60 }) {
|
|
39
|
+
const receipt = await releaseInstallation(installRoot, env);
|
|
40
|
+
const current = JSON.parse(await readFile(path.join(installRoot, 'package.json'), 'utf8')).version;
|
|
41
|
+
const releaseUrl = receipt.release_url.replace(/\/$/, '');
|
|
42
|
+
ui.line('Checking for a SupportPages Writer update…');
|
|
43
|
+
const latest = await run('curl', ['--proto', '=https', '--proto-redir', '=https', '-fsSL', '--max-time', String(checkTimeout), `${releaseUrl}/latest.txt`], { capture: true, env });
|
|
44
|
+
if (latest.code !== 0) fail('update_unavailable', `Could not check for updates. Ensure curl is installed and your network is available, then rerun ${CLI_NAME} update.`);
|
|
45
|
+
const next = latest.stdout.trim();
|
|
46
|
+
if (compareVersions(current, next) >= 0) {
|
|
47
|
+
ui.line(`SupportPages Writer ${current} is up to date. No update needed.`);
|
|
48
|
+
return { status: 'current', version: current };
|
|
49
|
+
}
|
|
50
|
+
ui.line(`Updating SupportPages Writer ${current} to ${next}…`);
|
|
51
|
+
const result = await run('bash', [path.join(installRoot, 'install-cli.sh'), '--yes', '--update', '--version', next,
|
|
52
|
+
'--data-dir', receipt.data_dir, '--bin-dir', receipt.bin_dir], { env: { ...env, SUPPORTPAGES_CLI_RELEASE_URL: releaseUrl } });
|
|
53
|
+
if (result.code !== 0) fail('update_failed', `Update could not finish. Your previous version remains available. Resolve the installer error above and rerun ${CLI_NAME} update.`);
|
|
54
|
+
return { status: 'updated', version: next };
|
|
55
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { fail } from '../../dist/errors.js';
|
|
2
|
+
import { Cancelled } from './terminal.mjs';
|
|
3
|
+
import { CLI_NAME } from './brand.mjs';
|
|
4
|
+
|
|
5
|
+
const clean = value => String(value).replace(/[\p{Cc}\p{Cf}]/gu, '');
|
|
6
|
+
|
|
7
|
+
/** Host task liveness is unknown to the CLI. Clear only after the user confirms it stopped. */
|
|
8
|
+
export async function ensureWriterAvailable(bridge, ui) {
|
|
9
|
+
for (;;) {
|
|
10
|
+
const active = await bridge.lock(() => bridge.runs.blocking());
|
|
11
|
+
if (!active) return;
|
|
12
|
+
const { run, state_directory: stateRoot } = active;
|
|
13
|
+
if (stateRoot !== bridge.stateRoot) {
|
|
14
|
+
fail('generation_active', `An unfinished article run belongs to another SupportPages.io connection (${clean(run.api_origin ?? stateRoot)}). Run ${CLI_NAME} init for that connection${stateRoot.includes('/dev/') ? ' with --dev' : ' without --dev'} to clear it after its writer has stopped.`, { run_id: run.id, state_directory: stateRoot });
|
|
15
|
+
}
|
|
16
|
+
const message = [
|
|
17
|
+
clean(run.title ?? 'Untitled article'),
|
|
18
|
+
`Last reported: ${clean(run.phase ?? run.status)} · ${clean(run.updated_at ?? run.started_at)}`,
|
|
19
|
+
`Article files: ${clean(run.artifact_dir)}`,
|
|
20
|
+
'This saved record does not mean a writer is still running.',
|
|
21
|
+
].join('\n');
|
|
22
|
+
if (ui.note) ui.note(message, 'Unfinished article run');
|
|
23
|
+
else ui.line(message);
|
|
24
|
+
const action = await ui.choose('How would you like to continue?', [
|
|
25
|
+
{ value: 'clear', label: 'Writer has stopped — clear run and continue', hint: 'Keeps all existing article files.' },
|
|
26
|
+
{ value: 'keep', label: 'Writer is running or unsure — keep run and exit' },
|
|
27
|
+
]);
|
|
28
|
+
if (action !== 'clear') {
|
|
29
|
+
ui.line('The article run was kept. Rerun when its writer has stopped.');
|
|
30
|
+
throw new Cancelled();
|
|
31
|
+
}
|
|
32
|
+
await bridge.cancel(run.id, true, run);
|
|
33
|
+
ui.ok('Cleared the unfinished run. Existing article files were kept.');
|
|
34
|
+
}
|
|
35
|
+
}
|