@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,40 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readFile } from 'node:fs/promises';
|
|
3
|
+
import { realpathSync } from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
6
|
+
|
|
7
|
+
export const repositoryRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
8
|
+
export const releaseVersionPattern = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/;
|
|
9
|
+
|
|
10
|
+
export async function checkRelease({ root = repositoryRoot, env = process.env } = {}) {
|
|
11
|
+
const pkg = JSON.parse(await readFile(path.join(root, 'package.json'), 'utf8'));
|
|
12
|
+
const lock = JSON.parse(await readFile(path.join(root, 'package-lock.json'), 'utf8'));
|
|
13
|
+
if (!releaseVersionPattern.test(pkg.version) || lock.version !== pkg.version || lock.packages?.['']?.version !== pkg.version) {
|
|
14
|
+
throw Error('package.json and package-lock.json must have the same X.Y.Z release version.');
|
|
15
|
+
}
|
|
16
|
+
// The engine's renderer runs on the package's puppeteer, so the two pins must agree.
|
|
17
|
+
const engine = JSON.parse(await readFile(path.join(root, 'engine', 'generate-illustrated-article', 'package.json'), 'utf8'));
|
|
18
|
+
if (engine.dependencies?.puppeteer !== pkg.dependencies?.puppeteer) {
|
|
19
|
+
throw Error(`package.json must pin puppeteer ${engine.dependencies?.puppeteer} to match engine/generate-illustrated-article.`);
|
|
20
|
+
}
|
|
21
|
+
// MCP Registry metadata names the same package version.
|
|
22
|
+
let server;
|
|
23
|
+
try { server = JSON.parse(await readFile(path.join(root, 'server.json'), 'utf8')); }
|
|
24
|
+
catch (error) { if (error.code !== 'ENOENT') throw error; }
|
|
25
|
+
if (server && (server.version !== pkg.version || server.packages?.some(entry => entry.version !== pkg.version))) {
|
|
26
|
+
throw Error(`server.json must use version ${pkg.version} for the server and its packages.`);
|
|
27
|
+
}
|
|
28
|
+
const tagged = env.GITHUB_REF_TYPE === 'tag';
|
|
29
|
+
if (tagged && env.GITHUB_REF_NAME !== `v${pkg.version}`) {
|
|
30
|
+
throw Error(`Release tag must be v${pkg.version}, matching package.json.`);
|
|
31
|
+
}
|
|
32
|
+
if (env.CLI_RELEASE_PUBLISH === 'true' && !tagged) {
|
|
33
|
+
throw Error('Publishing requires a vX.Y.Z tag. Run a build-only workflow on branches.');
|
|
34
|
+
}
|
|
35
|
+
return pkg.version;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href) {
|
|
39
|
+
process.stdout.write(`${await checkRelease()}\n`);
|
|
40
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Run by the installer before switching releases: the bundled Node must be able
|
|
3
|
+
// to run the article renderer on this machine.
|
|
4
|
+
try {
|
|
5
|
+
const [major, minor] = process.versions.node.split('.').map(Number);
|
|
6
|
+
if (!(major > 22 || major === 22 && minor >= 12)) throw Error('The local article renderer requires Node.js 22.12 or later.');
|
|
7
|
+
} catch (error) {
|
|
8
|
+
process.stderr.write(error.message + '\n');
|
|
9
|
+
process.exitCode = 1;
|
|
10
|
+
}
|
package/scripts/cli.mjs
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// `wtfm mcp [options]` (or the `supportpages` alias) runs the stdio MCP server, so MCP clients and
|
|
3
|
+
// registries can launch it through the package's single command (npx).
|
|
4
|
+
if (process.argv[2] === 'mcp') {
|
|
5
|
+
const server = new URL('../dist/index.js', import.meta.url);
|
|
6
|
+
process.argv.splice(1, 2, (await import('node:url')).fileURLToPath(server));
|
|
7
|
+
await import(server.href);
|
|
8
|
+
} else await import('./lib/cli-main.mjs');
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { parseArgs } from 'node:util';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { createTerminal, Cancelled } from './lib/terminal.mjs';
|
|
6
|
+
import { runInstaller } from './lib/install.mjs';
|
|
7
|
+
import { CLI_NAME } from './lib/brand.mjs';
|
|
8
|
+
|
|
9
|
+
const help = `SupportPages Writer — guided local setup
|
|
10
|
+
|
|
11
|
+
Usage: ./install.sh [options]
|
|
12
|
+
node scripts/install.mjs [options]
|
|
13
|
+
|
|
14
|
+
The wizard builds the MCP server, prepares the article engine, and connects your coding
|
|
15
|
+
client globally. Optionally connect a repository through browser setup. Run it from a terminal on macOS or Linux.
|
|
16
|
+
|
|
17
|
+
--workspace PATH Connect this repository after installation
|
|
18
|
+
--client NAME codex, claude, both, or manual (auto-detected menu)
|
|
19
|
+
--dev Connect to local RTFM (https://app.lvh.me:3443)
|
|
20
|
+
--api-url URL SupportPages.io app origin (default: https://app.supportpages.io)
|
|
21
|
+
--skills-dir PATH Use another engine directory instead of the bundled one
|
|
22
|
+
--name NAME Custom MCP connection name (default: supportpages or supportpages-dev)
|
|
23
|
+
--skip-skills Skip the trace hook and Chromium download
|
|
24
|
+
--skip-dependencies Use an already-built MCP checkout without npm ci/build
|
|
25
|
+
--config-dir PATH Private settings directory (~/.config/supportpages)
|
|
26
|
+
--data-dir PATH Installation data (~/.local/share/supportpages)
|
|
27
|
+
--yes Noninteractive; requires --client
|
|
28
|
+
-h, --help Show this help without installing anything
|
|
29
|
+
|
|
30
|
+
Installation does not sign you in. Afterwards run ${CLI_NAME} setup once on this
|
|
31
|
+
computer, then ${CLI_NAME} init inside each project folder, or ask your coding
|
|
32
|
+
client to initialize SupportPages.io. Articles can be saved in the project without
|
|
33
|
+
an account; signing in (browser approval, private credentials) is needed only to
|
|
34
|
+
host them on a help centre.
|
|
35
|
+
`;
|
|
36
|
+
let options;
|
|
37
|
+
try {
|
|
38
|
+
const stringOptions = ['workspace', 'client', 'api-url', 'skills-dir', 'name', 'config-dir', 'data-dir'];
|
|
39
|
+
const booleanOptions = ['dev', 'skip-skills', 'skip-dependencies', 'yes'];
|
|
40
|
+
options = parseArgs({ options: {
|
|
41
|
+
...Object.fromEntries(stringOptions.map(name => [name, { type: 'string' }])),
|
|
42
|
+
...Object.fromEntries(booleanOptions.map(name => [name, { type: 'boolean' }])),
|
|
43
|
+
help: { type: 'boolean', short: 'h' },
|
|
44
|
+
}}).values;
|
|
45
|
+
} catch {
|
|
46
|
+
process.stderr.write('Invalid installer options. Run ./install.sh --help. Authentication happens in your browser.\n');
|
|
47
|
+
process.exitCode = 2;
|
|
48
|
+
}
|
|
49
|
+
if (options) {
|
|
50
|
+
if (options.help) process.stdout.write(help);
|
|
51
|
+
else {
|
|
52
|
+
try {
|
|
53
|
+
if (process.platform === 'win32') throw new Error('Run this installer in WSL. The skills currently require a Unix shell.');
|
|
54
|
+
const [nodeMajor, nodeMinor] = process.versions.node.split('.').map(Number);
|
|
55
|
+
if (nodeMajor < 22 || nodeMajor === 22 && nodeMinor < 12) throw new Error('Node.js 22.12 or later is required.');
|
|
56
|
+
process.env.PATH = path.dirname(process.execPath) + path.delimiter + (process.env.PATH ?? '');
|
|
57
|
+
if (!options.yes && (!process.stdin.isTTY || !process.stdout.isTTY)) throw new Error('Run ./install.sh in a terminal, or use --yes with explicit options.');
|
|
58
|
+
await runInstaller(options, { ui: createTerminal(), installRoot: path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..') });
|
|
59
|
+
} catch (error) {
|
|
60
|
+
process.stderr.write(`\n${error instanceof Cancelled ? '' : 'Setup stopped: '}${error.message}\n`);
|
|
61
|
+
process.exitCode = error instanceof Cancelled ? 130 : 1;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { open } from 'node:fs/promises';
|
|
3
|
+
import { constants } from 'node:fs';
|
|
4
|
+
import readline from 'node:readline';
|
|
5
|
+
import { modelSettings } from './agent-settings.mjs';
|
|
6
|
+
import { Cancelled } from './terminal.mjs';
|
|
7
|
+
import { fail } from '../../dist/errors.js';
|
|
8
|
+
import { claudeWriterAgents } from '../../dist/writer-agent.js';
|
|
9
|
+
|
|
10
|
+
// Display observed activity, never raw commands, agent prose or reasoning.
|
|
11
|
+
export function agentActivity(event) {
|
|
12
|
+
const commandActivity = command => {
|
|
13
|
+
if (/classify_app_type\.(sh|js)/.test(command ?? '')) return 'Detecting app type';
|
|
14
|
+
if (/detect_static\.js/.test(command ?? '')) return 'Detecting project branding';
|
|
15
|
+
if (/(compile_css\.sh|run_css_build\.sh|check_css_health\.js)/.test(command ?? '')) return 'Preparing project styles';
|
|
16
|
+
if (/(detect_structure\.js|apply_runtime_profiles\.js)/.test(command ?? '')) return 'Mapping project structure';
|
|
17
|
+
return 'Running project commands';
|
|
18
|
+
};
|
|
19
|
+
if (event.type === 'system' && event.subtype === 'init' || event.type === 'turn.started') return 'Inspecting the project';
|
|
20
|
+
if (event.type === 'system' && event.subtype === 'thinking_tokens') return 'Analysing the project';
|
|
21
|
+
if (event.type === 'assistant') {
|
|
22
|
+
const tool = event.message?.content?.find(item => item.type === 'tool_use');
|
|
23
|
+
if (tool?.name === 'Bash') return commandActivity(tool.input?.command);
|
|
24
|
+
if (['Read', 'Glob', 'Grep'].includes(tool?.name)) return 'Reading project files';
|
|
25
|
+
if (['Write', 'Edit', 'MultiEdit'].includes(tool?.name)) return /(?:summary\.md|overview\.txt)$/.test(tool.input?.file_path ?? '') ? 'Writing the product summary' : 'Saving project files';
|
|
26
|
+
}
|
|
27
|
+
if (['item.started', 'item.updated', 'item.completed'].includes(event.type)) {
|
|
28
|
+
if (event.item?.type === 'command_execution') return event.type === 'item.completed' ? 'Reviewing command results' : commandActivity(event.item.command);
|
|
29
|
+
if (event.item?.type === 'file_change') return 'Saving project files';
|
|
30
|
+
if (event.item?.type === 'agent_message') return 'Preparing the agent response';
|
|
31
|
+
if (event.type === 'item.started') return 'Inspecting the project';
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function agentProgressText(agent, activity, elapsedMs, quietMs = 0) {
|
|
36
|
+
const seconds = Math.floor(elapsedMs / 1000);
|
|
37
|
+
const elapsed = `${Math.floor(seconds / 60)}:${String(seconds % 60).padStart(2, '0')}`;
|
|
38
|
+
const quiet = quietMs >= 20000 ? ` · Last update ${Math.floor(quietMs / 1000)}s ago` : '';
|
|
39
|
+
return `${agent === 'claude' ? 'Claude Code' : 'Codex'} · ${activity} · ${elapsed} elapsed${quiet} · Ctrl+C to cancel`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function agentCommand(agent, { interactive = false, prompt = '', model, effort, disabledConnection } = {}) {
|
|
43
|
+
if (!['claude', 'codex'].includes(agent)) fail('invalid_agent', 'Choose Claude Code or Codex.');
|
|
44
|
+
if (disabledConnection !== undefined && !['supportpages', 'supportpages-dev'].includes(disabledConnection)) fail('invalid_configuration', 'Invalid SupportPages.io connection.');
|
|
45
|
+
const permissionFlag = agent === 'claude' ? '--dangerously-skip-permissions' : '--dangerously-bypass-approvals-and-sandbox';
|
|
46
|
+
({ model, effort } = modelSettings(agent, { model, effort }));
|
|
47
|
+
const flags = [...(interactive ? [] : [permissionFlag]), ...(model ? ['--model', model] : []),
|
|
48
|
+
...(disabledConnection ? agent === 'claude' ? ['--disallowedTools', `mcp__${disabledConnection}`] : ['-c', `mcp_servers.${disabledConnection}.enabled=false`] : []),
|
|
49
|
+
...(effort ? agent === 'claude' ? ['--effort', effort] : ['-c', `model_reasoning_effort=${JSON.stringify(effort)}`] : [])];
|
|
50
|
+
if (interactive) return { command: agent, args: [...flags, ...(agent === 'claude' ? ['--agents', JSON.stringify(claudeWriterAgents)] : []), prompt] };
|
|
51
|
+
return agent === 'claude'
|
|
52
|
+
? { command: 'claude', args: ['-p', '--verbose', '--output-format', 'stream-json', ...flags] }
|
|
53
|
+
: { command: 'codex', args: ['exec', '-', '--json', ...flags] };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Run locally with the user's auth and permission checks bypassed. */
|
|
57
|
+
export async function runAgent({ agent, model, effort, workspace, prompt, env, logPath, ui, timeoutMs = 15 * 60_000, spawnProcess = spawn, signal, manageTerminal = true }) {
|
|
58
|
+
if (signal?.aborted) throw new Cancelled();
|
|
59
|
+
const invocation = agentCommand(agent, { model, effort });
|
|
60
|
+
const log = await open(logPath, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 0o600);
|
|
61
|
+
const childEnv = { ...env };
|
|
62
|
+
delete childEnv.SUPPORTPAGES_API_TOKEN;
|
|
63
|
+
delete childEnv.SUPPORTPAGES_API_TOKEN_FILE;
|
|
64
|
+
let child, interrupted = false, timedOut = false, streamError = false, permissionBlocked = false, failed = false, completed = false, buffer = '', logBytes = 0;
|
|
65
|
+
let forceTimer, timer, heartbeat, progress, agentFinished = false, logWork = Promise.resolve();
|
|
66
|
+
const started = Date.now();
|
|
67
|
+
let activity = 'Starting agent', lastEvent = started, lastRendered = started;
|
|
68
|
+
const progressText = () => agentProgressText(agent, activity, Date.now() - started, Date.now() - lastEvent);
|
|
69
|
+
const render = (isHeartbeat = false) => {
|
|
70
|
+
if (progress) progress.update(progressText(), { heartbeat: isHeartbeat });
|
|
71
|
+
else if (!isHeartbeat || Date.now() - lastRendered >= 15000) { ui.info?.(progressText()); lastRendered = Date.now(); }
|
|
72
|
+
};
|
|
73
|
+
const record = text => {
|
|
74
|
+
if (logBytes > 8 * 1024 * 1024) return;
|
|
75
|
+
logBytes += Buffer.byteLength(text);
|
|
76
|
+
logWork = logWork.then(() => log.write(text)).catch(() => { streamError = true; });
|
|
77
|
+
};
|
|
78
|
+
const signalGroup = signal => {
|
|
79
|
+
if (!child?.pid) return;
|
|
80
|
+
try { process.kill(-child.pid, signal); } catch { try { child.kill(signal); } catch {} }
|
|
81
|
+
};
|
|
82
|
+
const stop = () => { signalGroup('SIGTERM'); forceTimer ??= setTimeout(() => signalGroup('SIGKILL'), 1500); };
|
|
83
|
+
const cancel = () => { interrupted = true; stop(); };
|
|
84
|
+
const keypress = (_text, key) => { if (key?.ctrl && ['c', 'd'].includes(key.name)) cancel(); };
|
|
85
|
+
const input = process.stdin;
|
|
86
|
+
const wasRaw = Boolean(input.isRaw), wasFlowing = input.readableFlowing === true;
|
|
87
|
+
try {
|
|
88
|
+
child = spawnProcess(invocation.command, invocation.args, { cwd: workspace, env: childEnv, detached: true, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
89
|
+
if (manageTerminal) { process.once('SIGINT', cancel); process.once('SIGTERM', cancel); }
|
|
90
|
+
signal?.addEventListener('abort', cancel, { once: true });
|
|
91
|
+
if (signal?.aborted) cancel();
|
|
92
|
+
if (manageTerminal && input.isTTY && input.setRawMode) {
|
|
93
|
+
readline.emitKeypressEvents(input);
|
|
94
|
+
input.setRawMode(true); input.on('keypress', keypress); input.resume();
|
|
95
|
+
}
|
|
96
|
+
timer = setTimeout(() => { timedOut = true; stop(); }, timeoutMs);
|
|
97
|
+
heartbeat = setInterval(() => render(true), 1000);
|
|
98
|
+
const event = line => {
|
|
99
|
+
if (!line.trim()) return;
|
|
100
|
+
let value;
|
|
101
|
+
try { value = JSON.parse(line); } catch { streamError = true; return; }
|
|
102
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) { streamError = true; return; }
|
|
103
|
+
lastEvent = Date.now();
|
|
104
|
+
if (value.type === 'system' && value.subtype === 'permission_denied') {
|
|
105
|
+
// Headless mode cannot ask for approval. Stop promptly and let the user
|
|
106
|
+
// approve in their interactive agent, rather than retrying other tools.
|
|
107
|
+
permissionBlocked = true; activity = 'Permission required'; render(); stop();
|
|
108
|
+
}
|
|
109
|
+
if (value.type === 'result') {
|
|
110
|
+
completed = true;
|
|
111
|
+
// permission_denials is historical: a successful run may have recovered.
|
|
112
|
+
// Success still requires the caller's artifact validation.
|
|
113
|
+
failed ||= value.is_error === true || (value.is_error !== false && value.subtype !== 'success');
|
|
114
|
+
if (failed && value.permission_denials?.length > 0) permissionBlocked = true;
|
|
115
|
+
}
|
|
116
|
+
if (value.type === 'turn.completed') completed = true;
|
|
117
|
+
if (['turn.failed', 'error'].includes(value.type)) failed = true;
|
|
118
|
+
const nextActivity = agentActivity(value);
|
|
119
|
+
if (nextActivity && nextActivity !== activity) { activity = nextActivity; render(); }
|
|
120
|
+
};
|
|
121
|
+
child.stdout.on('data', chunk => {
|
|
122
|
+
const text = chunk.toString(); record(text); buffer += text;
|
|
123
|
+
if (buffer.length > 1024 * 1024) { streamError = true; stop(); return; }
|
|
124
|
+
let newline;
|
|
125
|
+
while ((newline = buffer.indexOf('\n')) >= 0) { event(buffer.slice(0, newline)); buffer = buffer.slice(newline + 1); }
|
|
126
|
+
});
|
|
127
|
+
child.stderr.on('data', chunk => record(chunk.toString()));
|
|
128
|
+
child.stdin.on('error', () => {}); // EPIPE is reported through the child's final status.
|
|
129
|
+
const done = new Promise((resolve, reject) => {
|
|
130
|
+
child.once('error', reject);
|
|
131
|
+
child.once('close', (code, signal) => resolve({ code, signal }));
|
|
132
|
+
});
|
|
133
|
+
progress = ui.progress?.(progressText());
|
|
134
|
+
if (!progress) render();
|
|
135
|
+
child.stdin.end(prompt);
|
|
136
|
+
let result;
|
|
137
|
+
try { result = await done; }
|
|
138
|
+
catch { fail('agent_unavailable', `Could not start ${agent}. Check its installation, sign in, and retry.`); }
|
|
139
|
+
if (buffer.trim()) event(buffer);
|
|
140
|
+
await logWork;
|
|
141
|
+
if (interrupted) throw new Cancelled();
|
|
142
|
+
if (permissionBlocked) fail('agent_permission_required', 'Claude Code needs tool or file-access approval to continue. Open it interactively to review the requested permissions. Your completed setup steps are kept.');
|
|
143
|
+
if (timedOut) fail('agent_timeout', 'The agent timed out. Completed setup steps are saved; retry this stage.');
|
|
144
|
+
if (result.code !== 0 || failed || !completed || streamError) fail('agent_failed', `The agent could not complete this stage. Open ${agent} in this project to check authentication and permissions, then retry. Private diagnostics: ${logPath}`);
|
|
145
|
+
agentFinished = true;
|
|
146
|
+
} finally {
|
|
147
|
+
clearTimeout(timer); clearTimeout(forceTimer); clearInterval(heartbeat);
|
|
148
|
+
// Kill remaining descendants after cancellation, even if the leader exited first.
|
|
149
|
+
if (interrupted || timedOut || streamError || permissionBlocked) signalGroup('SIGKILL');
|
|
150
|
+
if (manageTerminal) {
|
|
151
|
+
process.off('SIGINT', cancel); process.off('SIGTERM', cancel);
|
|
152
|
+
input.off('keypress', keypress);
|
|
153
|
+
if (input.isTTY && input.setRawMode) { input.setRawMode(wasRaw); if (!wasFlowing) input.pause(); }
|
|
154
|
+
}
|
|
155
|
+
signal?.removeEventListener('abort', cancel);
|
|
156
|
+
const elapsed = Math.floor((Date.now() - started) / 1000);
|
|
157
|
+
const message = `${interrupted ? 'Agent stopped' : permissionBlocked ? 'Agent needs permission to continue' : timedOut ? 'Agent timed out' : agentFinished ? 'Agent finished; checking its outputs next' : 'Agent could not finish'} · ${elapsed}s elapsed`;
|
|
158
|
+
if (progress) progress.stop(message, interrupted ? 'cancelled' : agentFinished ? 'success' : 'error');
|
|
159
|
+
else ui.info?.(message);
|
|
160
|
+
await logWork; await log.close();
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export async function openAgent({ agent, model, effort, workspace, prompt, env, ui, disabledConnection, spawnProcess = spawn }) {
|
|
165
|
+
const { command, args } = agentCommand(agent, { interactive: true, prompt, model, effort, disabledConnection });
|
|
166
|
+
try {
|
|
167
|
+
const child = spawnProcess(command, args, { cwd: workspace, env, stdio: 'inherit' });
|
|
168
|
+
const ignoreInterrupt = () => {}; // Foreground agent owns the terminal while it is open.
|
|
169
|
+
process.on('SIGINT', ignoreInterrupt);
|
|
170
|
+
try {
|
|
171
|
+
const code = await new Promise((resolve, reject) => { child.once('error', reject); child.once('exit', resolve); });
|
|
172
|
+
if (code !== 0) throw Error('Agent exited');
|
|
173
|
+
} finally { process.off('SIGINT', ignoreInterrupt); }
|
|
174
|
+
return true;
|
|
175
|
+
} catch {
|
|
176
|
+
const quote = value => "'" + value.replaceAll("'", "'\\''") + "'";
|
|
177
|
+
const manualCommand = [command, ...args.slice(0, -1)].map(quote).join(' ');
|
|
178
|
+
ui.line(`Open your coding agent manually:\ncd ${quote(workspace)}\n${manualCommand}\n\nThen paste:\n${prompt}`);
|
|
179
|
+
return false;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import { fail } from '../../dist/errors.js';
|
|
2
|
+
import { discoverModels } from './harness-models.mjs';
|
|
3
|
+
import { Cancelled } from './terminal.mjs';
|
|
4
|
+
|
|
5
|
+
export const agentLabel = agent => agent === 'claude' ? 'Claude Code' : 'Codex';
|
|
6
|
+
import { efforts, modelValid, settingsFile, modelSettings, readAgentSettings } from '../../dist/agent-settings.js';
|
|
7
|
+
import { CLI_NAME } from './brand.mjs';
|
|
8
|
+
export { modelSettings, readAgentSettings, executionSettings } from '../../dist/agent-settings.js';
|
|
9
|
+
|
|
10
|
+
// Preferred families for articles, best first: Opus then Fable on Claude Code, Sol
|
|
11
|
+
// then Astra on Codex (today Opus 5.5 and GPT-6-Sol). Catalogue order and isDefault
|
|
12
|
+
// are not rankings. Only recommend IDs actually exposed by the coding agent.
|
|
13
|
+
function recommendedModel(agent, models) {
|
|
14
|
+
const families = agent === 'claude' ? ['opus', 'fable'] : ['sol', 'astra'];
|
|
15
|
+
for (const family of families) {
|
|
16
|
+
const pattern = agent === 'claude'
|
|
17
|
+
? new RegExp(`^(?:${family}|claude-${family}-\\d+(?:[.-]\\d+)*)(?:\\[[^\\]]+\\])?$`)
|
|
18
|
+
: new RegExp(`^gpt-\\d+(?:\\.\\d+)?-${family}$`);
|
|
19
|
+
const matches = models.filter(({ value }) => pattern.test(value));
|
|
20
|
+
if (!matches.length) continue;
|
|
21
|
+
// Claude's explicit family alias follows its current release. Otherwise
|
|
22
|
+
// choose the newest version within the family, independently of list order.
|
|
23
|
+
return matches.find(model => model.value === family)
|
|
24
|
+
?? matches.find(model => model.value === `${family}[1m]`)
|
|
25
|
+
?? matches.sort((a, b) => b.value.localeCompare(a.value, 'en', { numeric: true }))[0];
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function modelHint(agent, model, recommended) {
|
|
30
|
+
if (model.value === recommended?.value) return 'Recommended for faithful mockups and detailed articles. Use Medium effort when available.';
|
|
31
|
+
const lighter = agent === 'claude'
|
|
32
|
+
? /^(?:(?:sonnet|haiku)|claude-(?:sonnet|haiku)-\d+(?:[.-]\d+)*)(?:\[[^\]]+\])?$/.test(model.value)
|
|
33
|
+
: /^gpt-\d+(?:\.\d+)?-(?:terra|luna)$/.test(model.value);
|
|
34
|
+
return lighter ? 'Suitable for rough drafts and simpler mockups. Expect more review and visual corrections.' : model.hint;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const effortHints = {
|
|
38
|
+
minimal: 'For quick drafts where approximate mockups are acceptable.',
|
|
39
|
+
low: 'For quick drafts where approximate mockups are acceptable.',
|
|
40
|
+
medium: 'Our recommended starting point for faithful mockups and article quality.',
|
|
41
|
+
high: 'Allow more reasoning for complex interfaces. Generation can take longer.',
|
|
42
|
+
xhigh: 'Allow more reasoning for complex interfaces. Generation can take longer.',
|
|
43
|
+
max: 'Allow more reasoning for complex interfaces. Generation can take longer.',
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export function modelDescription(agent, settings) {
|
|
47
|
+
const { model, effort } = modelSettings(agent, settings);
|
|
48
|
+
return `${agentLabel(agent)} · ${model ?? 'agent-configured model'} · ${effort ? `${effort} effort` : 'agent-configured effort'}`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export async function availableAgents(session, deps, clients = []) {
|
|
52
|
+
const available = [];
|
|
53
|
+
for (const agent of ['claude', 'codex']) {
|
|
54
|
+
if (clients.length && !clients.includes(agent)) continue;
|
|
55
|
+
if ((await deps.run(agent, ['--version'], { capture: true })).code !== 0) continue;
|
|
56
|
+
if (!clients.length && (await deps.run(agent, ['mcp', 'get', session.options.dev ? 'supportpages-dev' : 'supportpages'], { capture: true })).code !== 0) continue;
|
|
57
|
+
available.push(agent);
|
|
58
|
+
}
|
|
59
|
+
if (!available.length) fail('agent_unavailable', `No coding agent is connected yet. Install Claude Code or Codex, then run ${CLI_NAME} setup to connect it.`);
|
|
60
|
+
return available;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* With both agents connected, ask which runs project analysis. Claude Code is listed
|
|
65
|
+
* first and preselected unless this project already chose; writing always uses
|
|
66
|
+
* whichever agent the user is working in.
|
|
67
|
+
*/
|
|
68
|
+
async function chooseAnalysisAgent(ui, clients, preferred) {
|
|
69
|
+
if (clients.length < 2) return clients[0];
|
|
70
|
+
const ordered = [...clients].sort((a, b) => (b === 'claude') - (a === 'claude'));
|
|
71
|
+
const initial = clients.includes(preferred) ? preferred : ordered[0];
|
|
72
|
+
ui.line(`Project analysis runs in one agent and becomes the default for ${CLI_NAME} analyse. When you write, SupportPages uses whichever agent you are working in.`);
|
|
73
|
+
return ui.choose('Which coding agent should run project analysis?', ordered.map(value => ({ value, label: agentLabel(value),
|
|
74
|
+
...(value === 'claude' ? { hint: 'Recommended' } : {}) })), ordered.indexOf(initial));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export async function configureAgent(session, deps, { clients = [], agent: requestedAgent } = {}) {
|
|
78
|
+
const bridge = await session.bridge(), saved = await readAgentSettings(bridge);
|
|
79
|
+
const available = await availableAgents(session, deps, clients);
|
|
80
|
+
const { ui } = deps;
|
|
81
|
+
if (requestedAgent && !available.includes(requestedAgent)) fail('agent_unavailable', `The selected coding agent is not connected. Run ${CLI_NAME} setup to connect it.`);
|
|
82
|
+
const preferred = requestedAgent ?? saved.agent;
|
|
83
|
+
const agent = available.length === 1
|
|
84
|
+
? available[0]
|
|
85
|
+
: await ui.choose('Coding agent for this project', available.map(value => ({ value, label: agentLabel(value) })), Math.max(0, available.indexOf(preferred)));
|
|
86
|
+
const settings = await chooseModelSettings(agent, saved, bridge, deps);
|
|
87
|
+
// Commit only after all prompts succeed; keep settings for the other agent.
|
|
88
|
+
await bridge.ws.writeJson(settingsFile(bridge), { ...saved,
|
|
89
|
+
...(saved.agents ? { agents: [...new Set([...saved.agents, agent])] } : {}),
|
|
90
|
+
agent, models: { ...saved.models, [agent]: settings } });
|
|
91
|
+
ui.ok(`Saved for this project: ${modelDescription(agent, settings)}.`);
|
|
92
|
+
return { agent, ...settings };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Init configures every selected integration before committing project choices. */
|
|
96
|
+
export async function configureAgents(session, deps, { clients, agent: requestedAgent } = {}) {
|
|
97
|
+
const bridge = await session.bridge(), saved = await readAgentSettings(bridge);
|
|
98
|
+
const available = await availableAgents(session, deps, clients);
|
|
99
|
+
if (available.length !== clients.length) fail('agent_unavailable', `A selected coding agent is no longer available. Run ${CLI_NAME} init again.`);
|
|
100
|
+
const models = { ...saved.models };
|
|
101
|
+
for (const agent of clients) {
|
|
102
|
+
deps.ui.line(`Configure ${agentLabel(agent)} for this project.`);
|
|
103
|
+
models[agent] = await chooseModelSettings(agent, saved, bridge, deps);
|
|
104
|
+
}
|
|
105
|
+
const agent = await chooseAnalysisAgent(deps.ui, clients, requestedAgent ?? saved.agent);
|
|
106
|
+
await bridge.ws.writeJson(settingsFile(bridge), { ...saved, agents: clients, agent, models });
|
|
107
|
+
for (const client of clients) deps.ui.ok(`Saved for this project: ${modelDescription(client, models[client])}.`);
|
|
108
|
+
return { agent, models };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** The recommended model with Medium effort when the catalogue supports it, without prompting. */
|
|
112
|
+
export function quietModelSettings(agent, discovered) {
|
|
113
|
+
const recommended = recommendedModel(agent, discovered);
|
|
114
|
+
const model = recommended?.value ?? discovered.find(entry => entry.isDefault)?.value ?? null;
|
|
115
|
+
const metadata = discovered.find(entry => entry.value === model);
|
|
116
|
+
const effort = (metadata?.efforts ?? efforts[agent]).includes('medium') ? 'medium' : null;
|
|
117
|
+
return modelSettings(agent, { model, effort });
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** How init names a model: the agent's own label when it lists the model. */
|
|
121
|
+
function describeModel(agent, settings, discovered) {
|
|
122
|
+
const model = settings.model === null ? `${agentLabel(agent)}’s default model`
|
|
123
|
+
: discovered.find(entry => entry.value === settings.model)?.label ?? settings.model;
|
|
124
|
+
const effort = settings.effort ? `${settings.effort[0].toUpperCase()}${settings.effort.slice(1)} effort` : 'its default effort';
|
|
125
|
+
return `${model} at ${effort}`;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Init: state each agent's model for this project (the saved choice, or the
|
|
130
|
+
* recommended one) and ask whether to change it. Nothing is saved until every
|
|
131
|
+
* question is answered.
|
|
132
|
+
*/
|
|
133
|
+
export async function confirmAgentModels(session, deps, { clients, agent: requestedAgent } = {}) {
|
|
134
|
+
const bridge = await session.bridge(), saved = await readAgentSettings(bridge);
|
|
135
|
+
const available = await availableAgents(session, deps, clients);
|
|
136
|
+
if (available.length !== clients.length) fail('agent_unavailable', `A selected coding agent is no longer available. Run ${CLI_NAME} setup to repair it.`);
|
|
137
|
+
const { ui } = deps;
|
|
138
|
+
const models = { ...saved.models };
|
|
139
|
+
for (const agent of clients) {
|
|
140
|
+
ui.info?.(`Getting available models from ${agentLabel(agent)}…`);
|
|
141
|
+
let discovered = [];
|
|
142
|
+
try { discovered = await (deps.discoverModels ?? discoverModels)(agent, { cwd: bridge.ws.root, env: deps.env }); }
|
|
143
|
+
catch (error) { if (error instanceof Cancelled) throw error; }
|
|
144
|
+
let current;
|
|
145
|
+
try { current = saved.models?.[agent] ? modelSettings(agent, saved.models[agent]) : undefined; }
|
|
146
|
+
catch { current = undefined; }
|
|
147
|
+
current ??= quietModelSettings(agent, discovered);
|
|
148
|
+
if (!discovered.length && !saved.models?.[agent]) ui.line(`Could not get the model list from ${agentLabel(agent)}, so it will use its own default model. Check its sign-in and version to choose one.`);
|
|
149
|
+
ui.line(`${agentLabel(agent)} will write this project’s articles with ${describeModel(agent, current, discovered)}.`);
|
|
150
|
+
models[agent] = await ui.confirm(`Change the ${agentLabel(agent)} model?`, false)
|
|
151
|
+
? await chooseModelSettings(agent, saved, bridge, deps, discovered)
|
|
152
|
+
: current;
|
|
153
|
+
}
|
|
154
|
+
// The analysis step asks which agent runs it; until then keep the project's choice,
|
|
155
|
+
// or default to Claude Code.
|
|
156
|
+
const preferred = requestedAgent ?? saved.agent;
|
|
157
|
+
const agent = clients.includes(preferred) ? preferred : clients.includes('claude') ? 'claude' : clients[0];
|
|
158
|
+
await bridge.ws.writeJson(settingsFile(bridge), { ...saved, agents: clients, agent, models });
|
|
159
|
+
return { agent, models };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async function chooseModelSettings(agent, saved, bridge, deps, listed) {
|
|
163
|
+
const { ui } = deps;
|
|
164
|
+
// Allow configure to repair an obsolete model or effort selection.
|
|
165
|
+
let current;
|
|
166
|
+
try { current = modelSettings(agent, saved.models?.[agent] ?? {}); }
|
|
167
|
+
catch { current = modelSettings(agent); }
|
|
168
|
+
let discovered = listed ?? [];
|
|
169
|
+
// Init passes the catalogue it has just listed; an empty one is retried here.
|
|
170
|
+
if (!discovered.length) try {
|
|
171
|
+
ui.info?.(`Getting available models from ${agentLabel(agent)}…`);
|
|
172
|
+
discovered = await (deps.discoverModels ?? discoverModels)(agent, { cwd: bridge.ws.root, env: deps.env });
|
|
173
|
+
} catch (error) {
|
|
174
|
+
if (error instanceof Cancelled) throw error;
|
|
175
|
+
ui.line(`Could not get models from ${agentLabel(agent)}. You can keep your saved model, use the agent-configured model, or enter a model name. Check your coding agent's sign-in and version, then retry with ${CLI_NAME} configure.`);
|
|
176
|
+
}
|
|
177
|
+
const recommended = recommendedModel(agent, discovered);
|
|
178
|
+
const models = [
|
|
179
|
+
...discovered.map(model => ({ ...model,
|
|
180
|
+
label: model.value === recommended?.value ? `${model.label} — Recommended for SupportPages.io` : model.label,
|
|
181
|
+
hint: modelHint(agent, model, recommended) })),
|
|
182
|
+
...(saved.models?.[agent]?.model && current.model && !discovered.some(m => m.value === current.model)
|
|
183
|
+
? [{ value: current.model, label: `${current.model} (current setting)`, hint: 'Not listed by the coding agent' }] : []),
|
|
184
|
+
{ value: 'inherit', label: 'Use my coding agent’s default model', hint: 'Mockup fidelity will depend on your coding agent’s model choice.' },
|
|
185
|
+
{ value: 'custom', label: 'Enter a model name', hint: 'For a model you already know you want to use' },
|
|
186
|
+
];
|
|
187
|
+
const selected = saved.models?.[agent]?.model !== undefined
|
|
188
|
+
? current.model === null ? 'inherit' : models.some(m => m.value === current.model) ? current.model : 'custom'
|
|
189
|
+
: recommended?.value ?? discovered.find(m => m.isDefault)?.value ?? 'inherit';
|
|
190
|
+
ui.line('Your model analyses the application and creates the article’s illustrated mockups. Lighter models may simplify layouts or miss interface details.');
|
|
191
|
+
if (recommended) ui.line(`For faithful mockups, SupportPages.io recommends ${recommended.label} with Medium effort when supported.`);
|
|
192
|
+
else if (discovered.length) ui.line('We could not identify a recommended model in this catalogue. Choose an available model or use your agent’s default.');
|
|
193
|
+
const choice = await ui.choose(`${agentLabel(agent)} model`, models, models.findIndex(m => m.value === selected));
|
|
194
|
+
const model = choice === 'inherit' ? null : choice === 'custom'
|
|
195
|
+
? (await ui.ask('Model name', current.model ?? '', { validate: value => modelValid(value.trim()) ? undefined : 'Enter a model name, without spaces or flags.' })).trim()
|
|
196
|
+
: choice;
|
|
197
|
+
const metadata = discovered.find(item => item.value === model);
|
|
198
|
+
const effortOptions = [
|
|
199
|
+
...(metadata?.efforts ?? efforts[agent]).map(value => ({ value,
|
|
200
|
+
label: `${value[0].toUpperCase()}${value.slice(1)}${value === 'medium' ? ' — Recommended for SupportPages.io' : ''}`,
|
|
201
|
+
hint: effortHints[value] })),
|
|
202
|
+
{ value: 'inherit', label: 'Use my coding agent’s default effort' },
|
|
203
|
+
];
|
|
204
|
+
const preferredEffort = saved.models?.[agent]?.effort === null ? 'inherit'
|
|
205
|
+
: saved.models?.[agent]?.effort === undefined && model === recommended?.value
|
|
206
|
+
? effortOptions.some(e => e.value === 'medium') ? 'medium' : metadata?.defaultEffort ?? 'inherit'
|
|
207
|
+
: effortOptions.some(e => e.value === current.effort) ? current.effort
|
|
208
|
+
: metadata?.defaultEffort ?? 'inherit';
|
|
209
|
+
ui.line(effortOptions.some(e => e.value === 'medium')
|
|
210
|
+
? 'Reasoning effort controls how much time the model spends thinking. We recommend Medium for mockups; higher settings can take longer.'
|
|
211
|
+
: 'This model does not list Medium effort. Choose a supported effort or use your coding agent’s default.');
|
|
212
|
+
const effortChoice = await ui.choose('Reasoning effort', effortOptions, Math.max(0, effortOptions.findIndex(e => e.value === preferredEffort)));
|
|
213
|
+
const settings = modelSettings(agent, { model, effort: effortChoice === 'inherit' ? null : effortChoice });
|
|
214
|
+
return settings;
|
|
215
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { readFile, readlink, realpath, rm } from 'node:fs/promises';
|
|
3
|
+
|
|
4
|
+
export const ARTICLE_SKILLS = ['detect-project', 'generate-illustrated-article'];
|
|
5
|
+
const retired = ['suggest-sections', 'recommend-articles', 'recommend-walkthroughs', 'generate-walkthrough', 'generate-card-backgrounds'];
|
|
6
|
+
|
|
7
|
+
export async function retirePublicSkillLinks({ home, clients, directory, previousDirectory, env = {}, dryRun = false }) {
|
|
8
|
+
const canonical = async value => {
|
|
9
|
+
try { return await realpath(value); } catch (error) { if (error.code === 'ENOENT') return path.resolve(value); throw error; }
|
|
10
|
+
};
|
|
11
|
+
const roots = await Promise.all([directory, previousDirectory].filter(Boolean).map(canonical));
|
|
12
|
+
const anchor = path.join(home, '.rtfm-skills');
|
|
13
|
+
// Older receipts omitted skills_dir, and MCP may now point at a worktree.
|
|
14
|
+
// The managed engine shortcut still identifies those earlier public links.
|
|
15
|
+
try { roots.push(await canonical(path.resolve(home, await readlink(anchor))), anchor); }
|
|
16
|
+
catch (error) { if (!['ENOENT', 'EINVAL'].includes(error.code)) throw error; }
|
|
17
|
+
const removed = [];
|
|
18
|
+
const discoveries = new Set(clients.flatMap(selected => selected === 'claude'
|
|
19
|
+
? [path.join(home, '.claude/skills'), path.join(env.CLAUDE_CONFIG_DIR || path.join(home, '.claude'), 'skills')]
|
|
20
|
+
: [path.join(home, '.agents/skills'), path.join(home, '.codex/skills'), path.join(env.CODEX_HOME || path.join(home, '.codex'), 'skills')]));
|
|
21
|
+
for (const discovery of discoveries) {
|
|
22
|
+
for (const name of [...ARTICLE_SKILLS, ...retired]) {
|
|
23
|
+
const filename = path.join(discovery, name);
|
|
24
|
+
try {
|
|
25
|
+
const linked = path.resolve(discovery, await readlink(filename));
|
|
26
|
+
const targets = [await canonical(linked), path.join(await canonical(path.dirname(linked)), path.basename(linked))];
|
|
27
|
+
if (roots.some(root => targets.includes(path.join(root, name)))) removed.push(filename);
|
|
28
|
+
} catch (error) { if (!['ENOENT', 'EINVAL'].includes(error.code)) throw error; }
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
// Discover all ownership first: some clients link through another discovery directory.
|
|
32
|
+
if (!dryRun) for (const filename of removed) await rm(filename);
|
|
33
|
+
return removed;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Earlier releases linked ~/.rtfm-skills at the managed skills copy. Remove that
|
|
37
|
+
* link when it points into SupportPages Writer's own data directory; a link to
|
|
38
|
+
* a separate skills checkout belongs to that checkout and is left alone. */
|
|
39
|
+
export async function retireManagedAnchor({ home, dataDir }) {
|
|
40
|
+
const anchor = path.join(home, '.rtfm-skills');
|
|
41
|
+
let target;
|
|
42
|
+
try { target = path.resolve(home, await readlink(anchor)); }
|
|
43
|
+
catch (error) { if (['ENOENT', 'EINVAL'].includes(error.code)) return false; throw error; }
|
|
44
|
+
const within = (root, file) => { const relative = path.relative(root, file); return relative && !relative.startsWith('..') && !path.isAbsolute(relative); };
|
|
45
|
+
const roots = [path.resolve(dataDir)];
|
|
46
|
+
try { roots.push(await realpath(dataDir)); } catch (error) { if (error.code !== 'ENOENT') throw error; }
|
|
47
|
+
if (!roots.some(root => within(root, target))) return false;
|
|
48
|
+
await rm(anchor);
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const shellQuote = value => "'" + value.replaceAll("'", "'\\''") + "'";
|
|
53
|
+
|
|
54
|
+
/** Register the engine's trace hook with Claude Code, replacing earlier trace hooks. */
|
|
55
|
+
export async function installTraceHook({ home, env, writeJson, hookCommand }) {
|
|
56
|
+
const filename = path.join(env.CLAUDE_CONFIG_DIR || path.join(home, '.claude'), 'settings.json');
|
|
57
|
+
let settings;
|
|
58
|
+
try { settings = JSON.parse(await readFile(filename, 'utf8')); }
|
|
59
|
+
catch (error) { if (error.code !== 'ENOENT') throw error; settings = {}; }
|
|
60
|
+
const groups = settings.hooks?.PostToolUse ?? [];
|
|
61
|
+
if (!Array.isArray(groups) || groups.some(group => !Array.isArray(group.hooks))) throw Error('Claude Code PostToolUse settings are invalid. Repair them before installing the article trace hook.');
|
|
62
|
+
// The engine hook is Node; running it with the installation's own node keeps
|
|
63
|
+
// it working where Node is not on the user's PATH.
|
|
64
|
+
const command = hookCommand.map(shellQuote).join(' ');
|
|
65
|
+
settings.hooks = { ...settings.hooks, PostToolUse: [
|
|
66
|
+
...groups.map(group => ({ ...group, hooks: group.hooks.filter(hook => !/trace_hook\.(sh|js)/.test(String(hook.command ?? ''))) })).filter(group => group.hooks.length),
|
|
67
|
+
{ matcher: '*', hooks: [{ type: 'command', command }] },
|
|
68
|
+
] };
|
|
69
|
+
await writeJson(filename, settings);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Connect the bundled article engine to the chosen clients. The engine's
|
|
73
|
+
* dependencies live in the package's own node_modules, so nothing is installed
|
|
74
|
+
* into the skill folders and no skills are exposed to public discovery. */
|
|
75
|
+
export async function installArticleSkills({ home, client, directory, previousDirectory, writeJson, env = {}, dataDir, hookCommand }) {
|
|
76
|
+
const clients = client === 'both' ? ['claude', 'codex'] : [client === 'manual' ? 'codex' : client];
|
|
77
|
+
// Retire links before the anchor, so previous installations remain identifiable.
|
|
78
|
+
const removed = await retirePublicSkillLinks({ home, clients, directory, previousDirectory, env });
|
|
79
|
+
if (dataDir) await retireManagedAnchor({ home, dataDir });
|
|
80
|
+
if (clients.includes('claude') && hookCommand) await installTraceHook({ home, env, writeJson, hookCommand });
|
|
81
|
+
return removed;
|
|
82
|
+
}
|