failproofai 1.0.10 → 1.0.11-beta.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/.next/standalone/.next/BUILD_ID +1 -1
- package/.next/standalone/.next/build-manifest.json +6 -6
- package/.next/standalone/.next/prerender-manifest.json +4 -4
- package/.next/standalone/.next/required-server-files.json +1 -1
- package/.next/standalone/.next/server/app/_global-error/page/build-manifest.json +3 -3
- package/.next/standalone/.next/server/app/_global-error/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/_global-error/page.js +4 -4
- package/.next/standalone/.next/server/app/_global-error/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/_global-error.html +1 -1
- package/.next/standalone/.next/server/app/_global-error.meta +8 -0
- package/.next/standalone/.next/server/app/_global-error.rsc +7 -7
- package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +6 -6
- package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +7 -7
- package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found/page/build-manifest.json +3 -3
- package/.next/standalone/.next/server/app/_not-found/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/_not-found/page.js +4 -4
- package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/_not-found.html +1 -1
- package/.next/standalone/.next/server/app/_not-found.meta +8 -0
- package/.next/standalone/.next/server/app/_not-found.rsc +14 -14
- package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +14 -14
- package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +13 -13
- package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/api/audit/invite/route.js +4 -4
- package/.next/standalone/.next/server/app/api/audit/invite/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/audit/run/route.js +4 -4
- package/.next/standalone/.next/server/app/api/audit/run/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/audit/status/route.js +1 -1
- package/.next/standalone/.next/server/app/api/audit/status/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/login-request/route.js +2 -2
- package/.next/standalone/.next/server/app/api/auth/login-request/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/login-verify/route.js +4 -4
- package/.next/standalone/.next/server/app/api/auth/login-verify/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/logout/route.js +3 -3
- package/.next/standalone/.next/server/app/api/auth/logout/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/status/route.js +3 -3
- package/.next/standalone/.next/server/app/api/auth/status/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js +1 -1
- package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/audit/page/build-manifest.json +3 -3
- package/.next/standalone/.next/server/app/audit/page/server-reference-manifest.json +2 -2
- package/.next/standalone/.next/server/app/audit/page.js +4 -4
- package/.next/standalone/.next/server/app/audit/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/audit/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/index.html +1 -1
- package/.next/standalone/.next/server/app/index.meta +8 -0
- package/.next/standalone/.next/server/app/index.rsc +14 -14
- package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +13 -13
- package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +14 -14
- package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/page/build-manifest.json +3 -3
- package/.next/standalone/.next/server/app/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/page.js +4 -4
- package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/policies/page/build-manifest.json +3 -3
- package/.next/standalone/.next/server/app/policies/page/server-reference-manifest.json +14 -14
- package/.next/standalone/.next/server/app/policies/page.js +8 -8
- package/.next/standalone/.next/server/app/policies/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/policies/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/project/[name]/page/build-manifest.json +3 -3
- package/.next/standalone/.next/server/app/project/[name]/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/project/[name]/page.js +5 -5
- package/.next/standalone/.next/server/app/project/[name]/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/project/[name]/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/build-manifest.json +3 -3
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/react-loadable-manifest.json +2 -2
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/server-reference-manifest.json +2 -2
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js +5 -5
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/projects/page/build-manifest.json +3 -3
- package/.next/standalone/.next/server/app/projects/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/projects/page.js +5 -5
- package/.next/standalone/.next/server/app/projects/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/projects/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/settings/page/build-manifest.json +3 -3
- package/.next/standalone/.next/server/app/settings/page/server-reference-manifest.json +8 -8
- package/.next/standalone/.next/server/app/settings/page.js +9 -9
- package/.next/standalone/.next/server/app/settings/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/settings/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/chunks/{[externals]__0qxhecv._.js → [externals]__0d7i9ea._.js} +2 -2
- package/.next/standalone/.next/server/chunks/{[externals]__0sg9-3p._.js → [externals]__0hjaw70._.js} +1 -1
- package/.next/standalone/.next/server/chunks/[externals]__1_9ze9b._.js +3 -0
- package/.next/standalone/.next/server/chunks/[externals]__1oenfmn._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__08dchfu._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0ga9hs7._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0l3yhx4._.js +2 -2
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0moxohc._.js +22 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0o07qi9._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0z2ll-a._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1adacul._.js +11 -11
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1autwno._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1v25n-6._.js +3 -0
- package/.next/standalone/.next/server/chunks/_09dz7xv._.js +1 -1
- package/.next/standalone/.next/server/chunks/_0tovk6q._.js +1 -1
- package/.next/standalone/.next/server/chunks/_0trp3yc._.js +1 -1
- package/.next/standalone/.next/server/chunks/_1ek68ln._.js +2 -2
- package/.next/standalone/.next/server/chunks/_1fz8m31._.js +1 -1
- package/.next/standalone/.next/server/chunks/_1jrv3hg._.js +3 -0
- package/.next/standalone/.next/server/chunks/_1nx8fim._.js +3 -0
- package/.next/standalone/.next/server/chunks/_1ogwigf._.js +1 -1
- package/.next/standalone/.next/server/chunks/{_1q5i8mb._.js → _1q27azx._.js} +2 -2
- package/.next/standalone/.next/server/chunks/lib_telemetry_ts_0dctyyw._.js +1 -1
- package/.next/standalone/.next/server/chunks/{node_modules_posthog-node_dist_entrypoints_index_node_mjs_0el6rg8._.js → node_modules_posthog-node_dist_entrypoints_index_node_mjs_0ol5pa3._.js} +2 -2
- package/.next/standalone/.next/server/chunks/{node_modules_posthog-node_dist_entrypoints_index_node_mjs_04op_ou._.js → node_modules_posthog-node_dist_entrypoints_index_node_mjs_1rqmk27._.js} +2 -2
- package/.next/standalone/.next/server/chunks/{node_modules_posthog-node_dist_entrypoints_index_node_mjs_0dr20uz._.js → node_modules_posthog-node_dist_entrypoints_index_node_mjs_1w7tiq4._.js} +2 -2
- package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_1nxcc4v._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__04usis8._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1-bm5zf._.js → [root-of-the-server]__0502lat._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0_gn5iz._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0f765xr._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__01bmjsj._.js → [root-of-the-server]__0h38gzz._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0hoaz1g._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1ouobn4._.js → [root-of-the-server]__0mxmwtx._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__19evfi8._.js → [root-of-the-server]__0uescqv._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1484zv2._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1mf3zp6._.js → [root-of-the-server]__19dutr7._.js} +4 -4
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0oqrl71._.js → [root-of-the-server]__1_qv8oh._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1u6sn69._.js → [root-of-the-server]__1ab4ifb._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1fbrk56._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1pprgri._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1um4yxn._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1wb4yt2._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1y3n3cn._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/_08x1r5t._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_0bqoto4._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{_16pa9ht._.js → _0lnpwk8._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{_0yq5_3a._.js → _15y8myg._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_1gb0ifp._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{_042cgl1._.js → _1o95rf0._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{_1zopuov._.js → _1thr5rf._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/_next-internal_server_app_policies_page_actions_1sp2-yo.js +12 -14
- package/.next/standalone/.next/server/chunks/ssr/app_actions_get-scheduled-audit_ts_0ei9sni._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_audit__components_audit-dashboard_tsx_0p9ud47._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_global-error_tsx_1kp6l3x._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_policies_hooks-client_tsx_19dqvpc._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_settings_02tf1h4._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/node_modules_1hl81y4._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_0nkkbfv._.js +6 -0
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_fp-config_ts_04t589g._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_fp-home_ts_0je3xkv._.js +1 -1
- package/.next/standalone/.next/server/middleware-build-manifest.js +6 -6
- package/.next/standalone/.next/server/middleware.js +1 -1
- package/.next/standalone/.next/server/pages/404.html +1 -1
- package/.next/standalone/.next/server/pages/500.html +1 -1
- package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/server-reference-manifest.json +23 -23
- package/.next/standalone/.next/static/chunks/{03h3b-uomi7_d.js → 0pra9i676pym1.js} +1 -1
- package/.next/standalone/.next/static/chunks/{2kj826h1w9r47.js → 1-lpi0m-hc1kb.js} +1 -1
- package/.next/standalone/.next/static/chunks/{3ob2161ap37ul.js → 17yvyyblr_eb5.js} +1 -1
- package/.next/standalone/.next/static/chunks/{3nkc2jlgyygda.js → 1mylcr99gccyz.js} +1 -1
- package/.next/standalone/.next/static/chunks/{2j92xul6y24_v.js → 1wozchtn2etvh.js} +1 -1
- package/.next/standalone/.next/static/chunks/{3mjfvng11xnoo.js → 23thtelz1a-ur.js} +2 -2
- package/.next/standalone/.next/static/chunks/{1fqukzzowjflt.js → 2bc17tohgs4bs.js} +1 -1
- package/.next/standalone/.next/static/chunks/{3u1s8dsqiean5.js → 2xq-ejo8kti18.js} +1 -1
- package/.next/standalone/.next/static/chunks/3-_pth_c9n938.js +1 -0
- package/.next/standalone/.next/static/chunks/{3i8rg2fv9nd6e.js → 3n46e45atic-m.js} +1 -1
- package/.next/standalone/.next/static/chunks/{078dofaxvjgpd.js → 3q7sndqftqhw9.js} +1 -1
- package/.next/standalone/.next/static/chunks/{0542cuk_va8g1.js → 3sg07wnn969xq.js} +1 -1
- package/.next/standalone/.next/static/chunks/{turbopack-11dn2pegmg9_9.js → turbopack-1opxka4dtjbc5.js} +1 -1
- package/.next/standalone/app/actions/install-hooks-web.ts +10 -2
- package/.next/standalone/hermes-plugin/plugin.yaml +1 -1
- package/.next/standalone/node_modules/@next/env/package.json +1 -1
- package/.next/standalone/node_modules/next/dist/build/swc/index.js +1 -1
- package/.next/standalone/node_modules/next/dist/compiled/hash.js/sha256/256.js +1 -0
- package/.next/standalone/node_modules/next/dist/compiled/hash.js/sha256/package.json +1 -0
- package/.next/standalone/node_modules/next/dist/compiled/next-server/app-page-turbo-experimental.runtime.prod.js +17 -17
- package/.next/standalone/node_modules/next/dist/compiled/next-server/app-page-turbo.runtime.prod.js +18 -18
- package/.next/standalone/node_modules/next/dist/compiled/next-server/app-route-turbo.runtime.prod.js +3 -3
- package/.next/standalone/node_modules/next/dist/compiled/next-server/pages-turbo.runtime.prod.js +13 -13
- package/.next/standalone/node_modules/next/dist/lib/patch-incorrect-lockfile.js +3 -3
- package/.next/standalone/node_modules/next/dist/server/base-server.js +1 -2
- package/.next/standalone/node_modules/next/dist/server/config.js +1 -1
- package/.next/standalone/node_modules/next/dist/server/dev/hot-reloader-turbopack.js +2 -2
- package/.next/standalone/node_modules/next/dist/server/dev/hot-reloader-webpack.js +1 -1
- package/.next/standalone/node_modules/next/dist/server/image-optimizer.js +265 -53
- package/.next/standalone/node_modules/next/dist/server/lib/app-info-log.js +1 -1
- package/.next/standalone/node_modules/next/dist/server/lib/incremental-cache/file-system-cache.js +177 -19
- package/.next/standalone/node_modules/next/dist/server/lib/incremental-cache/index.js +36 -17
- package/.next/standalone/node_modules/next/dist/server/lib/incremental-cache/shared-cache-controls.external.js +14 -6
- package/.next/standalone/node_modules/next/dist/server/lib/match-next-data-pathname.js +3 -1
- package/.next/standalone/node_modules/next/dist/server/lib/node-fs-methods.js +14 -0
- package/.next/standalone/node_modules/next/dist/server/lib/route-cache-key.js +77 -0
- package/.next/standalone/node_modules/next/dist/server/lib/router-utils/block-cross-site-dev.js +9 -11
- package/.next/standalone/node_modules/next/dist/server/lib/start-server.js +1 -1
- package/.next/standalone/node_modules/next/dist/server/mcp/get-mcp-middleware.js +4 -2
- package/.next/standalone/node_modules/next/dist/server/next-server.js +7 -3
- package/.next/standalone/node_modules/next/dist/server/response-cache/index.js +32 -16
- package/.next/standalone/node_modules/next/dist/server/route-matcher-providers/app-page-route-matcher-provider.js +2 -2
- package/.next/standalone/node_modules/next/dist/server/route-modules/pages/pages-handler.js +6 -1
- package/.next/standalone/node_modules/next/dist/server/route-modules/route-module.js +9 -2
- package/.next/standalone/node_modules/next/dist/server/use-cache/use-cache-wrapper.js +30 -29
- package/.next/standalone/node_modules/next/dist/shared/lib/router/utils/app-paths.js +18 -1
- package/.next/standalone/node_modules/next/dist/telemetry/anonymous-meta.js +1 -1
- package/.next/standalone/node_modules/next/dist/telemetry/events/swc-load-failure.js +1 -1
- package/.next/standalone/node_modules/next/dist/telemetry/events/version.js +2 -2
- package/.next/standalone/node_modules/next/package.json +10 -10
- package/.next/standalone/package.json +14 -11
- package/.next/standalone/sdk/python/README.md +41 -0
- package/.next/standalone/sdk/python/docs/README.md +1 -0
- package/.next/standalone/sdk/python/docs/otel/README.md +36 -0
- package/.next/standalone/sdk/python/docs/otel/examples/agent_trace.py +30 -0
- package/.next/standalone/sdk/python/docs/otel/examples/quickstart.py +22 -0
- package/.next/standalone/sdk/python/failproofai_sdk/__init__.py +8 -0
- package/.next/standalone/sdk/python/failproofai_sdk/integrations/__init__.py +3 -0
- package/.next/standalone/sdk/python/failproofai_sdk/integrations/otel.py +189 -0
- package/.next/standalone/sdk/python/pyproject.toml +2 -1
- package/.next/standalone/sdk/python/tests/test_docs.py +16 -3
- package/.next/standalone/sdk/python/tests/test_integrations.py +1 -1
- package/.next/standalone/sdk/python/tests/test_otel.py +147 -0
- package/.next/standalone/sdk/python/tests/test_zero_dependencies.py +4 -1
- package/.next/standalone/sdk/python/uv.lock +258 -147
- package/.next/standalone/sdk/typescript/README.md +43 -0
- package/.next/standalone/sdk/typescript/integration/ai.test.ts +9 -2
- package/.next/standalone/sdk/typescript/package-lock.json +57 -5
- package/.next/standalone/sdk/typescript/package.json +2 -0
- package/.next/standalone/sdk/typescript/src/edge/index.ts +9 -0
- package/.next/standalone/sdk/typescript/src/index.ts +2 -0
- package/.next/standalone/sdk/typescript/src/integrations/index.ts +4 -1
- package/.next/standalone/sdk/typescript/src/integrations/otel.ts +204 -0
- package/.next/standalone/sdk/typescript/test/integrations.test.ts +1 -1
- package/.next/standalone/sdk/typescript/test/otel.test.ts +125 -0
- package/.next/standalone/server.js +1 -1
- package/bin/failproofai.mjs +653 -1077
- package/dist/cli.mjs +8781 -4050
- package/dist/worker.mjs +170 -62
- package/hermes-plugin/plugin.yaml +1 -1
- package/package.json +14 -11
- package/scripts/launch-facts.ts +157 -0
- package/scripts/launch-ready.ts +228 -0
- package/scripts/launch-screen.ts +202 -0
- package/scripts/launch.ts +106 -49
- package/scripts/skew-log-filter.ts +38 -0
- package/src/audit/cli-login.ts +39 -59
- package/src/audit/cli.ts +308 -228
- package/src/audit/index.ts +45 -2
- package/src/audit/schedule-cli.ts +83 -120
- package/src/audit/types.ts +31 -0
- package/src/hooks/agent-selection.ts +155 -0
- package/src/hooks/backfill-cli.ts +151 -137
- package/src/hooks/backfill-request.ts +149 -0
- package/src/hooks/cloud-connection.ts +1 -1
- package/src/hooks/cloud-enrollment-cli.ts +78 -28
- package/src/hooks/cloud-introspect.ts +3 -0
- package/src/hooks/collector-config.ts +10 -1
- package/src/hooks/config-status.ts +717 -0
- package/src/hooks/configure-wizard.ts +967 -988
- package/src/hooks/daemon-service.ts +16 -6
- package/src/hooks/did-you-mean.ts +185 -0
- package/src/hooks/flush-cli.ts +33 -7
- package/src/hooks/fp-config.ts +96 -0
- package/src/hooks/fp-home.ts +11 -0
- package/src/hooks/fp-reset.ts +102 -95
- package/src/hooks/handler.ts +4 -0
- package/src/hooks/harness-cli.ts +87 -138
- package/src/hooks/install-prompt.ts +38 -477
- package/src/hooks/integrations.ts +1 -1
- package/src/hooks/jev-cli.ts +183 -196
- package/src/hooks/manager.ts +554 -358
- package/src/hooks/onboarding-attempt.ts +11 -9
- package/src/hooks/otel-cli.ts +350 -0
- package/src/hooks/otel-output.ts +24 -0
- package/src/hooks/otel-writers.ts +215 -0
- package/src/hooks/pack-cli.ts +219 -204
- package/src/hooks/report-body.ts +50 -0
- package/src/hooks/safe-config-write.ts +4 -2
- package/src/hooks/session-pause-cli.ts +93 -73
- package/src/hooks/tui.ts +849 -389
- package/src/hooks/uninstall-cli.ts +136 -149
- package/src/hooks/untraced-note.ts +52 -0
- package/.next/standalone/.next/server/chunks/[externals]__19_pzeq._.js +0 -3
- package/.next/standalone/.next/server/chunks/[externals]__20kzpkf._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__01eqny5._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__05ja_05._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0igxt32._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0zyc_nm._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1ttrwnd._.js +0 -22
- package/.next/standalone/.next/server/chunks/_0bs-v8p._.js +0 -3
- package/.next/standalone/.next/server/chunks/_185gyk9._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__056wjo4._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0n0xg95._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0rwtwpm._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0yrsbd_._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__11mayhe._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__15578wp._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1m_svbe._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1q4p5b8._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_1n3w9lb._.js +0 -6
- package/.next/standalone/.next/static/chunks/1lyg_puttohbc.js +0 -1
- /package/.next/standalone/.next/static/{ECs8__hcNabYV58WfDh-3 → xtcpqBNBdjQVerPMkI2HI}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{ECs8__hcNabYV58WfDh-3 → xtcpqBNBdjQVerPMkI2HI}/_clientMiddlewareManifest.js +0 -0
- /package/.next/standalone/.next/static/{ECs8__hcNabYV58WfDh-3 → xtcpqBNBdjQVerPMkI2HI}/_ssgManifest.js +0 -0
package/src/hooks/tui.ts
CHANGED
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
* 24-bit, the xterm-256 cube, or the 16 basic ANSI hues.
|
|
19
19
|
*/
|
|
20
20
|
import * as readline from "node:readline";
|
|
21
|
+
import { version as cliVersion } from "../../package.json";
|
|
21
22
|
|
|
22
23
|
export type TTYIn = NodeJS.ReadableStream & {
|
|
23
24
|
isTTY?: boolean;
|
|
@@ -40,6 +41,12 @@ export interface SelectOneOptions<T> {
|
|
|
40
41
|
choices: SelectChoice<T>[];
|
|
41
42
|
/** Static info lines rendered under the question (e.g. a review summary). */
|
|
42
43
|
body?: string[];
|
|
44
|
+
/** Grey text after the heading — what the list is, e.g. "9 found on this machine". */
|
|
45
|
+
meta?: string;
|
|
46
|
+
/** The lines the prompt collapses to once answered, instead of `✓ <label>`. */
|
|
47
|
+
collapsed?: (value: T) => string[];
|
|
48
|
+
/** Let space pick the row as well as enter, for a menu that always took both. */
|
|
49
|
+
spaceSelects?: boolean;
|
|
43
50
|
stdin?: TTYIn;
|
|
44
51
|
stdout?: TTYOut;
|
|
45
52
|
}
|
|
@@ -87,7 +94,23 @@ export interface MultiSelectOptions<T> {
|
|
|
87
94
|
message: string;
|
|
88
95
|
choices: MultiChoice<T>[];
|
|
89
96
|
minSelected?: number;
|
|
97
|
+
/** What `✕` says when enter is pressed with fewer than `minSelected` ticked. */
|
|
98
|
+
minMessage?: string;
|
|
99
|
+
/** Replaces the key hints at the foot. Rarely right: the default IS the design. */
|
|
90
100
|
hint?: string;
|
|
101
|
+
/**
|
|
102
|
+
* Grey text after the heading, redrawn on every toggle. Defaults to
|
|
103
|
+
* "n of m selected"; a function gets what is ticked now; `null` shows none.
|
|
104
|
+
*/
|
|
105
|
+
meta?: string | null | ((checked: T[]) => string);
|
|
106
|
+
/**
|
|
107
|
+
* A tag after a row's name, already coloured — "will be added", "new on this
|
|
108
|
+
* machine" — recomputed on every toggle, so it can describe the change a
|
|
109
|
+
* toggle would make. Shown instead of the row's hint when present.
|
|
110
|
+
*/
|
|
111
|
+
tag?: (value: T, checked: boolean) => string | undefined;
|
|
112
|
+
/** The lines the prompt collapses to once answered, instead of `✓ <summary>`. */
|
|
113
|
+
collapsed?: (values: T[]) => string[];
|
|
91
114
|
/** Noun used when collapsing many selections to a count (e.g. "assistants"). */
|
|
92
115
|
summaryNoun?: string;
|
|
93
116
|
stdin?: TTYIn;
|
|
@@ -100,21 +123,15 @@ const ESC = "\x1B";
|
|
|
100
123
|
export const TOKEN_ON_ARGV =
|
|
101
124
|
"--token was on the command line: your shell history has it, and while this command ran any process of yours could read it from the process list.";
|
|
102
125
|
|
|
103
|
-
// ── glyphs ────────────────────────────────────────────────────────────────
|
|
104
|
-
// Exported so the other branded prompts (install-prompt.ts) share the exact
|
|
105
|
-
// same set instead of hand-syncing copies.
|
|
106
|
-
export const BAR = "│";
|
|
107
|
-
const BAR_END = "└";
|
|
108
|
-
export const STEP_ACTIVE = "◆";
|
|
109
|
-
export const STEP_DONE = "◇";
|
|
110
126
|
const RADIO_ON = "●";
|
|
111
127
|
const RADIO_OFF = "○";
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
128
|
+
// The redesign's boxes: ■ ticked, □ not. Two new names rather than new values
|
|
129
|
+
// for the two above, which install-prompt.ts still draws its own menus with.
|
|
130
|
+
const CHECK_SELECTED = "■";
|
|
131
|
+
const CHECK_EMPTY = "□";
|
|
115
132
|
/** The return key, as the multi-select hints already spell it. */
|
|
116
133
|
export const CARET_RETURN = "↵";
|
|
117
|
-
|
|
134
|
+
// ▮▮ — the brand mark, per the design system
|
|
118
135
|
|
|
119
136
|
// ── color ─────────────────────────────────────────────────────────────────
|
|
120
137
|
// The single source of truth for the brand palette — exported (via `paint`)
|
|
@@ -126,11 +143,11 @@ interface Hue {
|
|
|
126
143
|
* xterm-256 index for the middle tier, verified as the nearest cube entry by
|
|
127
144
|
* Euclidean distance rather than eyeballed.
|
|
128
145
|
*
|
|
129
|
-
* Optional because `dim`
|
|
130
|
-
* ATTRIBUTE that steps down whatever foreground the
|
|
131
|
-
* using, and the cube's nearest grey (243, #767676)
|
|
132
|
-
* fights every theme that is not ours. So
|
|
133
|
-
*
|
|
146
|
+
* Optional because the greys (`dim`, `ink3`, `track`) have no honest answer:
|
|
147
|
+
* their `basic` is SGR 2, an ATTRIBUTE that steps down whatever foreground the
|
|
148
|
+
* user's theme is already using, and the cube's nearest grey (243, #767676)
|
|
149
|
+
* is a fixed colour that fights every theme that is not ours. So a grey keeps
|
|
150
|
+
* the attribute below 24-bit and this stays undefined.
|
|
134
151
|
*/
|
|
135
152
|
c256?: number;
|
|
136
153
|
basic: string;
|
|
@@ -149,29 +166,30 @@ const HUES = {
|
|
|
149
166
|
pink: { rgb: [228, 88, 125], c256: 168, basic: "95" }, // #e4587d — selection, enabled, the mark, the brand
|
|
150
167
|
warn: { rgb: [227, 179, 65], c256: 179, basic: "33" },
|
|
151
168
|
dim: { rgb: [107, 118, 132], basic: "2" },
|
|
169
|
+
// The 2026-10 screen language's two greys. Both are the designed hex at 24-bit
|
|
170
|
+
// and the SGR 2 ATTRIBUTE below it, for the reason `dim` gives above: a fixed
|
|
171
|
+
// cube grey fights every theme that is not ours, and the basic tier's "bright
|
|
172
|
+
// black" (90) is the BACKGROUND colour in Solarized Dark, so a label drawn in
|
|
173
|
+
// it would vanish.
|
|
174
|
+
//
|
|
175
|
+
// There is no `ink2`: descriptions use the terminal's own foreground at every
|
|
176
|
+
// tier. The designed #a7adb6 is tuned for a dark ground, nothing here can tell
|
|
177
|
+
// which ground it is on, and on a light one it measures about 2.2:1 — half of
|
|
178
|
+
// what today's dim descriptions get. `paint().ink2` is therefore the identity.
|
|
179
|
+
ink3: { rgb: [118, 127, 139], basic: "2" }, // #767f8b — kv labels, heading meta, key hints
|
|
180
|
+
track: { rgb: [62, 67, 76], basic: "2" }, // #3e434c — the empty part of a progress bar, never text
|
|
181
|
+
// A state, never branding: the ✕ on a failed line. 203 rather than 204, which
|
|
182
|
+
// is the nearest cube entry by distance but differs from the brand pink's 168
|
|
183
|
+
// only in its red channel and reads as pink at that tier — the one deliberate,
|
|
184
|
+
// eyeballed exception to the nearest-entry rule above.
|
|
185
|
+
err: { rgb: [240, 113, 120], c256: 203, basic: "31" }, // #f07178
|
|
152
186
|
} satisfies Record<string, Hue>;
|
|
153
187
|
|
|
154
188
|
export function colorsEnabled(out: TTYOut): boolean {
|
|
155
189
|
return !!out.isTTY && !process.env.NO_COLOR;
|
|
156
190
|
}
|
|
157
191
|
|
|
158
|
-
/**
|
|
159
|
-
* The raw ANSI opening sequence for a brand role, for callers that assemble
|
|
160
|
-
* their own strings instead of using `paint()`'s wrappers (`src/audit/cli.ts`).
|
|
161
|
-
* That file previously hardcoded its own 256-colour palette — a green and a
|
|
162
|
-
* blue that appear nowhere in the brand — so `audit` looked like a different
|
|
163
|
-
* product from `config`. Routing it here
|
|
164
|
-
* keeps HUES the single source of truth: change a hue once and every surface
|
|
165
|
-
* follows. Paints at the deepest tier the terminal admits to; the caller still
|
|
166
|
-
* decides *whether* to colour at all.
|
|
167
|
-
*/
|
|
168
|
-
export function brandAnsi(role: keyof typeof HUES): string {
|
|
169
|
-
return `${ESC}[${fg(HUES[role], colorTier())}m`;
|
|
170
|
-
}
|
|
171
|
-
|
|
172
192
|
export const ANSI_RESET = `${ESC}[0m`;
|
|
173
|
-
export const ANSI_BOLD = `${ESC}[1m`;
|
|
174
|
-
export const ANSI_DIM = `${ESC}[2m`;
|
|
175
193
|
|
|
176
194
|
/** How much colour this terminal will actually render. */
|
|
177
195
|
type ColorTier = "truecolor" | "ansi256" | "basic";
|
|
@@ -237,6 +255,13 @@ export function paint(on: boolean) {
|
|
|
237
255
|
// to `pink` there, at which point it is deleted.
|
|
238
256
|
softPink: mk(HUES.pink),
|
|
239
257
|
warn: mk(HUES.warn),
|
|
258
|
+
// The 2026-10 roles. `ink2` is the terminal's own foreground on purpose —
|
|
259
|
+
// see the note on HUES — so it is the identity at every tier, and exists
|
|
260
|
+
// so a call site says which role it means rather than leaving text bare.
|
|
261
|
+
ink2: (s: string): string => s,
|
|
262
|
+
ink3: mk(HUES.ink3),
|
|
263
|
+
track: mk(HUES.track),
|
|
264
|
+
err: mk(HUES.err),
|
|
240
265
|
};
|
|
241
266
|
}
|
|
242
267
|
|
|
@@ -247,7 +272,6 @@ export function paint(on: boolean) {
|
|
|
247
272
|
// (▀ top, ▄ bottom); `t` = teal, `p` = pink, `.` = transparent. Shown when
|
|
248
273
|
// there's room, else a compact one-liner.
|
|
249
274
|
const LOGO_MIN_COLS = 22;
|
|
250
|
-
const TAGLINE = "end-to-end failure layer for ai agents";
|
|
251
275
|
|
|
252
276
|
// Three editing rules, all learned the hard way:
|
|
253
277
|
// 1. The two uprights must be the SAME width. In the artwork both are 93px of
|
|
@@ -372,130 +396,13 @@ export function summarize(labels: string[], noun = "selected"): string {
|
|
|
372
396
|
return `${labels.length} ${noun} · ${head} +${labels.length - 3}`;
|
|
373
397
|
}
|
|
374
398
|
|
|
375
|
-
function writeLines(out: TTYOut, lines: string[]): void {
|
|
376
|
-
const cols = out.columns || 80;
|
|
377
|
-
out.write(lines.map((l) => (l === "" ? l : truncate(l, cols))).join("\n") + "\n");
|
|
378
|
-
}
|
|
379
|
-
|
|
380
399
|
// ── framing ─────────────────────────────────────────────────────────────────
|
|
381
400
|
|
|
382
|
-
/** The logomark + wordmark + tagline block (with a 2-space margin), color-aware.
|
|
383
|
-
* Shared by the wizard intro and the dashboard launch banner. On a too-narrow
|
|
384
|
-
* target it collapses to a single compact line. */
|
|
385
|
-
export function renderBrandLogo(stdout: TTYOut = process.stdout): string[] {
|
|
386
|
-
const c = paint(colorsEnabled(stdout));
|
|
387
|
-
const cols = stdout.columns || 80;
|
|
388
|
-
if (cols < LOGO_MIN_COLS) {
|
|
389
|
-
return [`${c.pink(MARK)} fa${c.pink("il")}proof ai ${c.dim("· " + TAGLINE)}`];
|
|
390
|
-
}
|
|
391
|
-
// NO_COLOR / non-TTY forces `basic`, which renderLogo draws monochrome — so
|
|
392
|
-
// the mark never emits an escape byte on a stream that asked for none.
|
|
393
|
-
const tier: ColorTier = colorsEnabled(stdout) ? colorTier() : "basic";
|
|
394
|
-
const lines = renderLogo(tier).map((l) => ` ${l}`);
|
|
395
|
-
lines.push("");
|
|
396
|
-
lines.push(` fa${c.pink("il")}proof ai`);
|
|
397
|
-
lines.push(` ${c.dim(TAGLINE)}`);
|
|
398
|
-
return lines;
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
/** Print the flow header — the brand logo, tagline, and the opening step. */
|
|
402
|
-
export function intro(message: string, stdout: TTYOut = process.stdout): void {
|
|
403
|
-
if (!stdout.isTTY) return;
|
|
404
|
-
const c = paint(colorsEnabled(stdout));
|
|
405
|
-
const lines: string[] = ["", ...renderBrandLogo(stdout)];
|
|
406
|
-
lines.push(c.dim(BAR));
|
|
407
|
-
lines.push(`${c.guide(STEP_ACTIVE)} ${c.bold(message)}`);
|
|
408
|
-
writeLines(stdout, lines);
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
/** The branded splash the dashboard prints on launch — logomark, wordmark, and a
|
|
412
|
-
* tidy version/links column. Returned as ready-to-print lines (caller writes
|
|
413
|
-
* them). Degrades to plain text off a TTY so it stays clean in piped logs. */
|
|
414
|
-
export function renderLaunchBanner(version: string, stdout: TTYOut = process.stdout): string[] {
|
|
415
|
-
// paint() is identity when colors are off, so the link rows are built once
|
|
416
|
-
// and only the header block differs between TTY and piped output.
|
|
417
|
-
const c = paint(colorsEnabled(stdout));
|
|
418
|
-
const row = (label: string, value: string) => ` ${c.guide(label.padEnd(9))}${value}`;
|
|
419
|
-
const header = stdout.isTTY
|
|
420
|
-
? renderBrandLogo(stdout)
|
|
421
|
-
: [" failproof ai", ` ${TAGLINE}`];
|
|
422
|
-
return [
|
|
423
|
-
"",
|
|
424
|
-
...header,
|
|
425
|
-
"",
|
|
426
|
-
row("version", c.pink(version)),
|
|
427
|
-
row("star", c.dim("https://github.com/failproofai/failproofai")),
|
|
428
|
-
row("docs", c.dim("https://docs.befailproof.ai/introduction")),
|
|
429
|
-
row("discord", c.dim("https://discord.befailproof.ai/")),
|
|
430
|
-
row("reddit", c.dim("https://www.reddit.com/r/failproofai/")),
|
|
431
|
-
"",
|
|
432
|
-
];
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
/**
|
|
436
|
-
* Print a step that is already SETTLED — the `◇ / message / summary` block
|
|
437
|
-
* `selectOne` leaves behind when it resolves.
|
|
438
|
-
*
|
|
439
|
-
* Extracted because a flow assembled out of `promptText` had no way to show its
|
|
440
|
-
* own history: `intro` opens the spine and `outro` closes it, and everything in
|
|
441
|
-
* between was bare lines that made the frame look like it belonged to a
|
|
442
|
-
* different command. `summary` is the answer, dimmed under the question, which
|
|
443
|
-
* is what makes a completed step readable at a glance rather than a heading
|
|
444
|
-
* with nothing under it.
|
|
445
|
-
*/
|
|
446
|
-
export function step(
|
|
447
|
-
message: string,
|
|
448
|
-
summary?: string | string[],
|
|
449
|
-
stdout: TTYOut = process.stdout,
|
|
450
|
-
): void {
|
|
451
|
-
const c = paint(colorsEnabled(stdout));
|
|
452
|
-
const rows = summary === undefined ? [] : Array.isArray(summary) ? summary : [summary];
|
|
453
|
-
if (!stdout.isTTY) {
|
|
454
|
-
if (rows.length) stdout.write(`${message}: ${rows.join(" ")}\n`);
|
|
455
|
-
return;
|
|
456
|
-
}
|
|
457
|
-
// Truncated to the terminal, because a summary that wraps loses the spine on
|
|
458
|
-
// its second row and the block stops reading as one step.
|
|
459
|
-
const cols = stdout.columns || 80;
|
|
460
|
-
const lines = [c.dim(BAR), truncate(`${c.dim(STEP_DONE)} ${message}`, cols - 1)];
|
|
461
|
-
for (const r of rows) lines.push(truncate(`${c.dim(BAR)} ${c.dim(r)}`, cols - 1));
|
|
462
|
-
writeLines(stdout, lines);
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
/**
|
|
466
|
-
* Open a step and leave the cursor on it, for a prompt that draws its own line.
|
|
467
|
-
*
|
|
468
|
-
* The counterpart to {@link step}: `◆` in teal with the question in bold, then
|
|
469
|
-
* a spine row the prompt is expected to hang off via `PromptTextOptions.prefix`.
|
|
470
|
-
*/
|
|
471
|
-
export function stepOpen(message: string, stdout: TTYOut = process.stdout): void {
|
|
472
|
-
const c = paint(colorsEnabled(stdout));
|
|
473
|
-
if (!stdout.isTTY) return;
|
|
474
|
-
writeLines(stdout, [c.dim(BAR), `${c.guide(STEP_ACTIVE)} ${c.bold(message)}`]);
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
/** Close the flow with a terminating └ line — pink on success, dim on cancel. */
|
|
478
|
-
export function outro(
|
|
479
|
-
message: string,
|
|
480
|
-
opts: { ok?: boolean } = {},
|
|
481
|
-
stdout: TTYOut = process.stdout,
|
|
482
|
-
): void {
|
|
483
|
-
const c = paint(colorsEnabled(stdout));
|
|
484
|
-
const ok = opts.ok !== false;
|
|
485
|
-
if (!stdout.isTTY) {
|
|
486
|
-
stdout.write(message + "\n");
|
|
487
|
-
return;
|
|
488
|
-
}
|
|
489
|
-
const end = ok ? c.pink(BAR_END) : c.dim(BAR_END);
|
|
490
|
-
const text = ok ? c.pink(message) : c.dim(message);
|
|
491
|
-
writeLines(stdout, [c.dim(BAR), `${end} ${text}`]);
|
|
492
|
-
}
|
|
493
|
-
|
|
494
401
|
// ── shared render engine ─────────────────────────────────────────────────────
|
|
495
402
|
|
|
496
403
|
type Region = { lastCount: number };
|
|
497
404
|
|
|
498
|
-
const WINDOW =
|
|
405
|
+
const WINDOW = 12; // visible rows before a list scrolls; 12 reproduces every frame in the redesign
|
|
499
406
|
|
|
500
407
|
/**
|
|
501
408
|
* Redraw the region as ONE atomic frame.
|
|
@@ -510,12 +417,24 @@ const WINDOW = 8; // visible rows before the checklist scrolls
|
|
|
510
417
|
* the terminal to hold what it has until the reset rather than painting each
|
|
511
418
|
* chunk as it lands. Terminals that do not implement it ignore an unknown
|
|
512
419
|
* private mode, so it costs nothing where it does not help.
|
|
420
|
+
*
|
|
421
|
+
* `frame` is for a caller that has to change a terminal mode alongside a frame
|
|
422
|
+
* — hide the cursor on the first, show it on the last — inside the SAME write,
|
|
423
|
+
* and for a last frame that must not be cut: lines are truncated so each one is
|
|
424
|
+
* exactly one row and the next cursor-up lands where it should, but a frame
|
|
425
|
+
* nothing will repaint stays in scrollback, where a cut is permanent.
|
|
513
426
|
*/
|
|
514
|
-
function repaint(
|
|
427
|
+
function repaint(
|
|
428
|
+
out: TTYOut,
|
|
429
|
+
region: Region,
|
|
430
|
+
lines: string[],
|
|
431
|
+
frame: { lead?: string; tail?: string; cut?: boolean } = {},
|
|
432
|
+
): void {
|
|
515
433
|
const cols = out.columns || 80;
|
|
516
|
-
const
|
|
434
|
+
const cut = frame.cut ?? true;
|
|
435
|
+
const body = lines.map((l) => (l === "" || !cut ? l : truncate(l, cols))).join("\n") + "\n";
|
|
517
436
|
const clear = region.lastCount > 0 ? `${ESC}[${region.lastCount}A${ESC}[J` : "";
|
|
518
|
-
out.write(`${ESC}[?2026h${clear}${body}${ESC}[?2026l`);
|
|
437
|
+
out.write(`${ESC}[?2026h${frame.lead ?? ""}${clear}${body}${frame.tail ?? ""}${ESC}[?2026l`);
|
|
519
438
|
region.lastCount = lines.length;
|
|
520
439
|
}
|
|
521
440
|
|
|
@@ -586,8 +505,10 @@ interface PromptSpec<R> {
|
|
|
586
505
|
/** Static info lines rendered under the question. */
|
|
587
506
|
body?: string[];
|
|
588
507
|
choices: Array<{ label: string; section?: string }>;
|
|
589
|
-
/**
|
|
508
|
+
/** A whole row, cursor gutter included: " › ■ name hint". */
|
|
590
509
|
renderRow: (index: number, active: boolean, budget: number) => string;
|
|
510
|
+
/** Grey text after the heading, re-read on every frame. */
|
|
511
|
+
meta?: () => string | null | undefined;
|
|
591
512
|
/** Extra line(s) above the footer (e.g. a min-selected warning). */
|
|
592
513
|
warnLine?: () => string | null;
|
|
593
514
|
/** When set, ← resolves `BACK` so the caller can step backwards. */
|
|
@@ -597,8 +518,19 @@ interface PromptSpec<R> {
|
|
|
597
518
|
footer: string;
|
|
598
519
|
/** Handle non-navigation keys. `{done}` finishes, `"redraw"` repaints. */
|
|
599
520
|
onKey: (key: readline.Key, cursor: number) => { done: R } | "redraw" | undefined;
|
|
600
|
-
/**
|
|
521
|
+
/** The answer, said once the prompt collapses: `✓ <this>`. */
|
|
601
522
|
summaryFor: (result: R | null) => string;
|
|
523
|
+
/** The collapsed lines, when a caller says the answer its own way. */
|
|
524
|
+
collapsed?: (result: R) => string[];
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
/**
|
|
528
|
+
* A prompt's heading: the question in bold UPPERCASE, with grey meta after it —
|
|
529
|
+
* the same shape as `screenKit().head`, so a picker and the screen around it
|
|
530
|
+
* read as one page.
|
|
531
|
+
*/
|
|
532
|
+
function promptHeading(c: ReturnType<typeof paint>, message: string, meta?: string | null): string {
|
|
533
|
+
return c.bold(message.toUpperCase()) + (meta ? ` ${c.ink3(meta)}` : "");
|
|
602
534
|
}
|
|
603
535
|
|
|
604
536
|
function runPrompt<R>(p: PromptSpec<R>): Promise<R | null> {
|
|
@@ -609,8 +541,8 @@ function runPrompt<R>(p: PromptSpec<R>): Promise<R | null> {
|
|
|
609
541
|
|
|
610
542
|
const build = (): string[] => {
|
|
611
543
|
const cols = stdout.columns || 80;
|
|
612
|
-
const lines: string[] = [c.
|
|
613
|
-
for (const b of p.body ?? []) lines.push(`${
|
|
544
|
+
const lines: string[] = [promptHeading(c, p.message, p.meta?.())];
|
|
545
|
+
for (const b of p.body ?? []) lines.push(`${INDENT}${c.ink2(b)}`);
|
|
614
546
|
|
|
615
547
|
const rows = displayRows(choices);
|
|
616
548
|
let cursorRow = 0;
|
|
@@ -620,22 +552,25 @@ function runPrompt<R>(p: PromptSpec<R>): Promise<R | null> {
|
|
|
620
552
|
const { start, end } = viewport(rows, cursorRow, WINDOW);
|
|
621
553
|
const above = rows.slice(0, start).filter((r) => r.kind === "item").length;
|
|
622
554
|
const below = rows.slice(end).filter((r) => r.kind === "item").length;
|
|
623
|
-
if (above > 0) lines.push(
|
|
555
|
+
if (above > 0) lines.push(` ${c.ink2(`↑ ${above} more`)}`);
|
|
624
556
|
|
|
625
|
-
|
|
557
|
+
// The room a row's hint has: the terminal, less the 3-column cursor gutter,
|
|
558
|
+
// the name column and the gap after it. A checklist's box takes two more,
|
|
559
|
+
// which its renderRow subtracts itself.
|
|
560
|
+
const budget = Math.max(6, cols - nameCol - 6);
|
|
626
561
|
for (let ri = start; ri < end; ri++) {
|
|
627
562
|
const row = rows[ri];
|
|
628
563
|
if (row.kind === "header") {
|
|
629
|
-
lines.push(`${
|
|
564
|
+
lines.push(`${INDENT}${c.bold(row.text)}`);
|
|
630
565
|
} else {
|
|
631
|
-
lines.push(
|
|
566
|
+
lines.push(p.renderRow(row.index, row.index === cursor, budget));
|
|
632
567
|
}
|
|
633
568
|
}
|
|
634
|
-
if (below > 0) lines.push(
|
|
569
|
+
if (below > 0) lines.push(` ${c.ink2(`↓ ${below} more`)}`);
|
|
635
570
|
|
|
636
571
|
const warn = p.warnLine?.();
|
|
637
|
-
if (warn) lines.push(
|
|
638
|
-
lines.push(
|
|
572
|
+
if (warn) lines.push(warn);
|
|
573
|
+
lines.push("", c.ink3(p.footer));
|
|
639
574
|
return lines;
|
|
640
575
|
};
|
|
641
576
|
|
|
@@ -650,12 +585,18 @@ function runPrompt<R>(p: PromptSpec<R>): Promise<R | null> {
|
|
|
650
585
|
// `resolve(result)`, so pressing ← never settled the promise and the wizard
|
|
651
586
|
// stopped responding entirely. `selectOne` fell through to `String(value)`
|
|
652
587
|
// and rendered the literal text `Symbol(failproofai.back)`.
|
|
653
|
-
const
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
588
|
+
const heading = promptHeading(c, p.message);
|
|
589
|
+
const lines =
|
|
590
|
+
(result as unknown) === BACK
|
|
591
|
+
? [heading, c.ink3("back")]
|
|
592
|
+
: result === null
|
|
593
|
+
? [heading, c.ink3(p.summaryFor(null))]
|
|
594
|
+
: p.collapsed
|
|
595
|
+
? p.collapsed(result)
|
|
596
|
+
: [heading, `${c.guide("✓")} ${p.summaryFor(result)}`];
|
|
597
|
+
// Not cut: nothing repaints this frame, so it stays in scrollback, where a
|
|
598
|
+
// cut is permanent — and a summary names agents, hosts and orgs.
|
|
599
|
+
repaint(stdout, region, lines, { cut: false });
|
|
659
600
|
};
|
|
660
601
|
|
|
661
602
|
return new Promise<R | null>((resolve) => {
|
|
@@ -750,22 +691,23 @@ export function selectOne<T>(opts: SelectOneOptions<T>): Promise<T | Back | null
|
|
|
750
691
|
c,
|
|
751
692
|
body: opts.body,
|
|
752
693
|
choices,
|
|
694
|
+
meta: () => opts.meta,
|
|
753
695
|
renderRow: (index, active, budget) => {
|
|
754
696
|
const choice = choices[index];
|
|
755
|
-
const
|
|
756
|
-
const
|
|
757
|
-
const label = active ? c.pinkBold(rawLabel) : rawLabel;
|
|
697
|
+
const rawLabel = choice.hint ? choice.label.padEnd(nameCol) : choice.label;
|
|
698
|
+
const label = active ? c.bold(rawLabel) : rawLabel;
|
|
758
699
|
const hint = choice.hint
|
|
759
|
-
? ` ${c.
|
|
700
|
+
? ` ${c.ink2(ellipsize(choice.hint, hintBudget(choice.label, nameCol, budget)))}`
|
|
760
701
|
: "";
|
|
761
|
-
return `${
|
|
702
|
+
return `${active ? c.pink(" › ") : " "}${label}${hint}`;
|
|
762
703
|
},
|
|
763
704
|
allowBack: opts.allowBack,
|
|
764
|
-
footer: opts.allowBack
|
|
765
|
-
? "↑/↓ navigate · enter to select · ← back · esc to cancel"
|
|
766
|
-
: "↑/↓ navigate · enter to select · esc to cancel",
|
|
705
|
+
footer: ["↑↓ move", "enter select", ...(opts.allowBack ? ["← back"] : []), "esc cancel"].join(" · "),
|
|
767
706
|
onKey: (key, cursor) =>
|
|
768
|
-
key.name === "return"
|
|
707
|
+
key.name === "return" || (opts.spaceSelects && key.name === "space")
|
|
708
|
+
? { done: choices[cursor].value }
|
|
709
|
+
: undefined,
|
|
710
|
+
collapsed: opts.collapsed,
|
|
769
711
|
summaryFor: (value) =>
|
|
770
712
|
value === null
|
|
771
713
|
? "cancelled"
|
|
@@ -800,27 +742,40 @@ export function multiSelect<T>(opts: MultiSelectOptions<T>): Promise<T[] | null
|
|
|
800
742
|
message: opts.message,
|
|
801
743
|
c,
|
|
802
744
|
choices,
|
|
745
|
+
meta: () => {
|
|
746
|
+
if (opts.meta === null) return null;
|
|
747
|
+
if (typeof opts.meta === "string") return opts.meta;
|
|
748
|
+
if (typeof opts.meta === "function") {
|
|
749
|
+
return opts.meta(choices.filter((_, i) => checked[i]).map((ch) => ch.value));
|
|
750
|
+
}
|
|
751
|
+
// Selector rows ("Everything available") are not one of the things counted.
|
|
752
|
+
const counted = choices.map((ch, i) => (ch.summaryExclude ? null : checked[i])).filter((v) => v !== null);
|
|
753
|
+
return `${counted.filter(Boolean).length} of ${counted.length} selected`;
|
|
754
|
+
},
|
|
803
755
|
renderRow: (index, active, budget) => {
|
|
804
756
|
const choice = choices[index];
|
|
805
|
-
|
|
806
|
-
// Locked rows use the teal guide hue rather than selection pink, so they
|
|
757
|
+
// Locked rows use the mint guide hue rather than selection pink, so they
|
|
807
758
|
// read as "already true" instead of "you picked this"; an unchecked
|
|
808
|
-
// locked row is
|
|
759
|
+
// locked row is an empty grey box — nothing there yet.
|
|
809
760
|
const box = choice.locked
|
|
810
761
|
? checked[index]
|
|
811
|
-
? c.guide(
|
|
812
|
-
: c.
|
|
762
|
+
? c.guide(CHECK_SELECTED)
|
|
763
|
+
: c.ink3(CHECK_EMPTY)
|
|
813
764
|
: checked[index]
|
|
814
|
-
? c.pink(
|
|
815
|
-
: c.
|
|
816
|
-
const
|
|
817
|
-
const
|
|
818
|
-
const
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
765
|
+
? c.pink(CHECK_SELECTED)
|
|
766
|
+
: c.ink3(CHECK_EMPTY);
|
|
767
|
+
const tag = opts.tag?.(choice.value, checked[index]);
|
|
768
|
+
const rawLabel = tag || choice.hint ? choice.label.padEnd(nameCol) : choice.label;
|
|
769
|
+
const label = active ? c.bold(rawLabel) : checked[index] ? rawLabel : c.ink2(rawLabel);
|
|
770
|
+
const trail = tag
|
|
771
|
+
? ` ${tag}`
|
|
772
|
+
: choice.hint
|
|
773
|
+
? ` ${c.ink2(ellipsize(choice.hint, hintBudget(choice.label, nameCol, budget - 2)))}`
|
|
774
|
+
: "";
|
|
775
|
+
return `${active ? c.pink(" › ") : " "}${box} ${label}${trail}`;
|
|
822
776
|
},
|
|
823
|
-
warnLine: () =>
|
|
777
|
+
warnLine: () =>
|
|
778
|
+
warn ? `${c.err("✕")} ${opts.minMessage ?? `Select at least ${minSelected}.`}` : null,
|
|
824
779
|
allowBack: opts.allowBack,
|
|
825
780
|
// Reads the SAME `checked` array the prompt is driving, so what the caller
|
|
826
781
|
// learns is exactly what was on screen when ← was pressed.
|
|
@@ -829,9 +784,10 @@ export function multiSelect<T>(opts: MultiSelectOptions<T>): Promise<T[] | null
|
|
|
829
784
|
: undefined,
|
|
830
785
|
footer:
|
|
831
786
|
opts.hint ??
|
|
832
|
-
(opts.allowBack
|
|
833
|
-
|
|
834
|
-
|
|
787
|
+
["↑↓ move", "space toggle", "a all", ...(opts.allowBack ? ["← back"] : []), "enter confirm", "esc cancel"].join(
|
|
788
|
+
" · ",
|
|
789
|
+
),
|
|
790
|
+
collapsed: opts.collapsed,
|
|
835
791
|
onKey: (key, cursor) => {
|
|
836
792
|
if (key.name === "space") {
|
|
837
793
|
if (choices[cursor]?.locked) return "redraw"; // always on — not a choice
|
|
@@ -839,7 +795,9 @@ export function multiSelect<T>(opts: MultiSelectOptions<T>): Promise<T[] | null
|
|
|
839
795
|
warn = false;
|
|
840
796
|
return "redraw";
|
|
841
797
|
}
|
|
842
|
-
|
|
798
|
+
// Plain `a`, as the redesign's hints spell it; ctrl+a stays as an alias,
|
|
799
|
+
// since GNU screen swallows it as its command prefix anyway.
|
|
800
|
+
if (key.name === "a") {
|
|
843
801
|
const allOn = choices.every((ch, i) => checked[i] || ch.locked);
|
|
844
802
|
for (let i = 0; i < checked.length; i++) {
|
|
845
803
|
checked[i] = choices[i]?.locked ? true : !allOn;
|
|
@@ -895,6 +853,15 @@ export interface PromptTextOptions {
|
|
|
895
853
|
* the scrollback of every recording of that session.
|
|
896
854
|
*/
|
|
897
855
|
mask?: boolean;
|
|
856
|
+
/**
|
|
857
|
+
* Draw the 2026-10 field instead of the spine prompt: ` <message> › <value>`
|
|
858
|
+
* — the label in the terminal's own ink at a ten-column label, a pink `›`, the
|
|
859
|
+
* hint in the label grey, and a refusal as a `✕` line under the field that is
|
|
860
|
+
* cleared again as soon as the value changes. `prefix` is ignored. For the
|
|
861
|
+
* screens that have moved to the new language; the spine form stays for the
|
|
862
|
+
* rest until they move.
|
|
863
|
+
*/
|
|
864
|
+
field?: boolean;
|
|
898
865
|
/** Return an error string to reject and re-ask, or null to accept. */
|
|
899
866
|
validate?: (value: string) => string | null;
|
|
900
867
|
stdin?: TTYIn;
|
|
@@ -959,9 +926,38 @@ export function promptText(opts: PromptTextOptions): Promise<string | null> {
|
|
|
959
926
|
|
|
960
927
|
return new Promise((resolve) => {
|
|
961
928
|
let value = "";
|
|
929
|
+
// Whether a field's `✕` line is on the row under it right now.
|
|
930
|
+
let refused = false;
|
|
931
|
+
/**
|
|
932
|
+
* The field form, in ONE write per keystroke. The refusal goes on the row
|
|
933
|
+
* under the field and the field is drawn LAST, so the cursor ends where the
|
|
934
|
+
* typing is — the spine form moves back up after its error line and leaves
|
|
935
|
+
* the cursor at the end of the error instead. A refusal still on screen is
|
|
936
|
+
* cleared the moment the value changes, so a corrected answer never sits
|
|
937
|
+
* above the complaint about the old one.
|
|
938
|
+
*/
|
|
939
|
+
const drawField = (cols: number, shown: string, error?: string) => {
|
|
940
|
+
const hint = hintText && value.length === 0 ? ` ${c.ink3(hintText)}` : "";
|
|
941
|
+
const line = truncate(`${INDENT}${opts.message.padEnd(10)}${c.pink("›")} ${shown}${hint}`, cols - 1);
|
|
942
|
+
if (error) {
|
|
943
|
+
const refusal = truncate(`${c.err("✕")} ${error}`, cols - 1);
|
|
944
|
+
stdout.write(`\n\x1b[2K${refusal}\x1b[1A\r\x1b[2K${line}`);
|
|
945
|
+
refused = true;
|
|
946
|
+
return;
|
|
947
|
+
}
|
|
948
|
+
// Down, clear, back up: the row under the field exists, because the
|
|
949
|
+
// refusal being cleared was written there.
|
|
950
|
+
const clearRefusal = refused ? "\x1b[1B\x1b[2K\x1b[1A" : "";
|
|
951
|
+
stdout.write(`${clearRefusal}\r\x1b[2K${line}`);
|
|
952
|
+
refused = false;
|
|
953
|
+
};
|
|
962
954
|
const draw = (error?: string) => {
|
|
963
955
|
const cols = stdout.columns || 80;
|
|
964
956
|
const shown = opts.mask ? "•".repeat(value.length) : value;
|
|
957
|
+
if (opts.field) {
|
|
958
|
+
drawField(cols, shown, error);
|
|
959
|
+
return;
|
|
960
|
+
}
|
|
965
961
|
// The hint is a PLACEHOLDER — an example of what belongs here — so it
|
|
966
962
|
// steps aside as soon as there is a real answer to look at. Keeping both
|
|
967
963
|
// on one line put the example and the input side by side, which is the
|
|
@@ -992,7 +988,10 @@ export function promptText(opts: PromptTextOptions): Promise<string | null> {
|
|
|
992
988
|
stdin.removeListener("keypress", onKey);
|
|
993
989
|
stdin.setRawMode?.(wasRaw ?? false);
|
|
994
990
|
stdin.pause();
|
|
995
|
-
|
|
991
|
+
// Past a field's refusal, when one is still showing (a cancel right after
|
|
992
|
+
// it), so whatever prints next starts on a clean row instead of on top of
|
|
993
|
+
// the complaint.
|
|
994
|
+
stdout.write(refused ? "\n\n" : "\n");
|
|
996
995
|
};
|
|
997
996
|
|
|
998
997
|
function onKey(str: string | undefined, key: readline.Key): void {
|
|
@@ -1029,6 +1028,326 @@ export function promptText(opts: PromptTextOptions): Promise<string | null> {
|
|
|
1029
1028
|
});
|
|
1030
1029
|
}
|
|
1031
1030
|
|
|
1031
|
+
/**
|
|
1032
|
+
* Run something that writes straight to the terminal — `sudo -v` and its
|
|
1033
|
+
* password prompt — then erase whatever it wrote, so the step around it can
|
|
1034
|
+
* collapse to one line.
|
|
1035
|
+
*
|
|
1036
|
+
* The rows are reserved FIRST: `reserve` newlines then back up, so that what
|
|
1037
|
+
* the child prints never scrolls the screen. A scroll would move the saved
|
|
1038
|
+
* cursor position off the line it was saved on, and the erase would then land
|
|
1039
|
+
* below the prompt instead of on it. Three failed sudo attempts print about
|
|
1040
|
+
* six lines, so ten is enough room. Without a terminal it just runs.
|
|
1041
|
+
*/
|
|
1042
|
+
export function collapseAfter<T>(stdout: TTYOut, reserve: number, run: () => T): T {
|
|
1043
|
+
if (!stdout.isTTY) return run();
|
|
1044
|
+
stdout.write(`${"\n".repeat(reserve)}${ESC}[${reserve}A${ESC}7`);
|
|
1045
|
+
try {
|
|
1046
|
+
return run();
|
|
1047
|
+
} finally {
|
|
1048
|
+
stdout.write(`${ESC}8${ESC}[J`);
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
// ── the cloud-key prompt ─────────────────────────────────────────────────────
|
|
1053
|
+
|
|
1054
|
+
/** What `promptCloudKey` resolves when Tab is pressed: no new connection. */
|
|
1055
|
+
export const OPEN_SOURCE: unique symbol = Symbol("failproofai.open-source");
|
|
1056
|
+
export type OpenSource = typeof OPEN_SOURCE;
|
|
1057
|
+
|
|
1058
|
+
/**
|
|
1059
|
+
* What a key check found, in words the prompt prints as they are. `ok` with a
|
|
1060
|
+
* `line` is a key that works for part of what it was asked: the caller says
|
|
1061
|
+
* which part, as a `▲` line after the prompt collapses. `usable` marks a SAVED
|
|
1062
|
+
* key that could not be checked (offline, a timeout) rather than one that was
|
|
1063
|
+
* refused, so Enter can still keep it.
|
|
1064
|
+
*/
|
|
1065
|
+
export type KeyVerdict = { ok: true; line?: string } | { ok: false; line: string; usable?: boolean };
|
|
1066
|
+
|
|
1067
|
+
export type CloudKeyAnswer =
|
|
1068
|
+
| { kind: "typed"; key: string; note?: string }
|
|
1069
|
+
| { kind: "saved"; verified: boolean };
|
|
1070
|
+
|
|
1071
|
+
export interface CloudKeyPromptOptions {
|
|
1072
|
+
/** The heading, e.g. "Connect to cloud". */
|
|
1073
|
+
message: string;
|
|
1074
|
+
/** Grey text after the heading: where the key will be sent. */
|
|
1075
|
+
meta?: string;
|
|
1076
|
+
/** What Tab does, for the key hints: "use open source instead", or "skip". */
|
|
1077
|
+
tabHint: string;
|
|
1078
|
+
/** The `✕` line for Enter on an empty field with no saved key to use. */
|
|
1079
|
+
emptyError: string;
|
|
1080
|
+
/** A key already on this machine: drawn at once, checked in the background. */
|
|
1081
|
+
saved?: { masked: string; check: () => Promise<KeyVerdict> };
|
|
1082
|
+
/** Checks a typed key. Never called for one shorter than `minLength`. */
|
|
1083
|
+
check: (key: string) => Promise<KeyVerdict>;
|
|
1084
|
+
minLength?: number;
|
|
1085
|
+
/** The lines the prompt collapses to once answered. */
|
|
1086
|
+
collapsed?: (answer: CloudKeyAnswer | OpenSource) => string[];
|
|
1087
|
+
stdin?: TTYIn;
|
|
1088
|
+
stdout?: TTYOut;
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
/**
|
|
1092
|
+
* One masked `API key ›` field, with Tab as the way out to open source.
|
|
1093
|
+
*
|
|
1094
|
+
* Its own prompt rather than options on `promptText`, which keeps a one-row
|
|
1095
|
+
* rule its other callers depend on. This one needs rows above and below the
|
|
1096
|
+
* field: a saved key's state, a spinner while a key is checked, and a `✕` line
|
|
1097
|
+
* that keeps the typed key in place so Enter can retry it.
|
|
1098
|
+
*
|
|
1099
|
+
* Three rules hold for every state:
|
|
1100
|
+
* - Tab inside a paste is part of the paste. A pasted key arrives as keys,
|
|
1101
|
+
* so a tab in it would otherwise switch to open source halfway through.
|
|
1102
|
+
* Bracketed paste is switched on for exactly this, and Enter inside a paste
|
|
1103
|
+
* (a copied trailing newline) never submits either.
|
|
1104
|
+
* - Keys are ignored while a typed key is being checked, Esc excepted: a
|
|
1105
|
+
* check can take ten seconds and leaving must stay possible.
|
|
1106
|
+
* - The terminal is put back on every way out: cursor, raw mode and paste
|
|
1107
|
+
* mode, including a process exit while the prompt is open.
|
|
1108
|
+
*/
|
|
1109
|
+
export function promptCloudKey(opts: CloudKeyPromptOptions): Promise<CloudKeyAnswer | OpenSource | null> {
|
|
1110
|
+
const stdin: TTYIn = opts.stdin ?? process.stdin;
|
|
1111
|
+
const stdout: TTYOut = opts.stdout ?? process.stdout;
|
|
1112
|
+
// Nobody is there to type a key. The wizard never asks without a terminal;
|
|
1113
|
+
// this answers the same way a cancel would rather than hang on a pipe.
|
|
1114
|
+
if (!stdin.isTTY || !stdout.isTTY) return Promise.resolve(null);
|
|
1115
|
+
|
|
1116
|
+
const c = paint(colorsEnabled(stdout));
|
|
1117
|
+
const kit = screenKit({ cols: stdout.columns || 80, color: colorsEnabled(stdout) });
|
|
1118
|
+
const minLength = opts.minLength ?? 8;
|
|
1119
|
+
const spinnerFrames = "⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏";
|
|
1120
|
+
const region: Region = { lastCount: 0 };
|
|
1121
|
+
|
|
1122
|
+
let typed = "";
|
|
1123
|
+
let inPaste = false;
|
|
1124
|
+
// The saved key's state: being checked, works, could not be checked, or
|
|
1125
|
+
// refused — and a refused key is treated as no saved key at all.
|
|
1126
|
+
let savedState: "checking" | "works" | "unchecked" | "refused" | "none" = opts.saved ? "checking" : "none";
|
|
1127
|
+
let savedLine = "";
|
|
1128
|
+
let enterWaiting = false;
|
|
1129
|
+
let checkingTyped = false;
|
|
1130
|
+
let fieldError = "";
|
|
1131
|
+
let refusedOnce = false;
|
|
1132
|
+
let frame = 0;
|
|
1133
|
+
let spinning = false;
|
|
1134
|
+
let settled = false;
|
|
1135
|
+
let spinDelay: ReturnType<typeof setTimeout> | null = null;
|
|
1136
|
+
let spinTimer: ReturnType<typeof setInterval> | null = null;
|
|
1137
|
+
|
|
1138
|
+
const savedUsable = (): boolean => savedState === "checking" || savedState === "works" || savedState === "unchecked";
|
|
1139
|
+
|
|
1140
|
+
const spinGlyph = (): string => (spinning ? c.ink3(spinnerFrames[frame % spinnerFrames.length]) : " ");
|
|
1141
|
+
|
|
1142
|
+
const build = (): string[] => {
|
|
1143
|
+
const lines: string[] = [promptHeading(c, opts.message, opts.meta)];
|
|
1144
|
+
if (opts.saved) {
|
|
1145
|
+
if (savedState === "checking") lines.push(` ${spinGlyph()} Checking your saved key ${opts.saved.masked}…`);
|
|
1146
|
+
else if (savedState === "works") lines.push(kit.ok(savedLine));
|
|
1147
|
+
else if (savedState === "unchecked" || savedState === "refused") lines.push(kit.caution(savedLine));
|
|
1148
|
+
}
|
|
1149
|
+
lines.push(`${INDENT}${"API key".padEnd(10)}${c.pink("› ")}${"•".repeat(typed.length)}`);
|
|
1150
|
+
if (checkingTyped) {
|
|
1151
|
+
lines.push(` ${spinGlyph()} Checking the key…`);
|
|
1152
|
+
return lines;
|
|
1153
|
+
}
|
|
1154
|
+
if (fieldError) lines.push(kit.fail(fieldError));
|
|
1155
|
+
// "type to replace it", not the reference's "type a new key to replace it":
|
|
1156
|
+
// the reference is drawn at 104 columns, and at 80 the longer line is cut
|
|
1157
|
+
// through the Tab hint, which is the only place Tab is mentioned.
|
|
1158
|
+
const hints =
|
|
1159
|
+
fieldError && refusedOnce
|
|
1160
|
+
? ["enter try again", `tab ${opts.tabHint}`, "esc cancel"]
|
|
1161
|
+
: savedUsable() && typed.length === 0
|
|
1162
|
+
? ["enter use saved key", "type to replace it", `tab ${opts.tabHint}`]
|
|
1163
|
+
: ["enter connect", `tab ${opts.tabHint}`, "esc cancel"];
|
|
1164
|
+
lines.push("", kit.keys(hints));
|
|
1165
|
+
return lines;
|
|
1166
|
+
};
|
|
1167
|
+
|
|
1168
|
+
const draw = (): void => {
|
|
1169
|
+
if (!settled) repaint(stdout, region, build());
|
|
1170
|
+
};
|
|
1171
|
+
|
|
1172
|
+
const stopSpinner = (): void => {
|
|
1173
|
+
if (spinDelay) clearTimeout(spinDelay);
|
|
1174
|
+
if (spinTimer) clearInterval(spinTimer);
|
|
1175
|
+
spinDelay = null;
|
|
1176
|
+
spinTimer = null;
|
|
1177
|
+
spinning = false;
|
|
1178
|
+
};
|
|
1179
|
+
// 150 ms before the first frame, so a check that answers at once never
|
|
1180
|
+
// flashes a spinner; 80 ms a frame after that.
|
|
1181
|
+
const startSpinner = (): void => {
|
|
1182
|
+
stopSpinner();
|
|
1183
|
+
spinDelay = setTimeout(() => {
|
|
1184
|
+
spinning = true;
|
|
1185
|
+
draw();
|
|
1186
|
+
spinTimer = setInterval(() => {
|
|
1187
|
+
frame++;
|
|
1188
|
+
draw();
|
|
1189
|
+
}, 80);
|
|
1190
|
+
spinTimer.unref?.();
|
|
1191
|
+
}, 150);
|
|
1192
|
+
spinDelay.unref?.();
|
|
1193
|
+
};
|
|
1194
|
+
|
|
1195
|
+
const restoreTerminal = (): void => {
|
|
1196
|
+
stdout.write(`${ESC}[?2004l`);
|
|
1197
|
+
showCursor(stdout);
|
|
1198
|
+
};
|
|
1199
|
+
|
|
1200
|
+
return new Promise((resolve) => {
|
|
1201
|
+
const wasRaw = stdin.isRaw;
|
|
1202
|
+
const finish = (answer: CloudKeyAnswer | OpenSource | null): void => {
|
|
1203
|
+
if (settled) return;
|
|
1204
|
+
stopSpinner();
|
|
1205
|
+
stdin.removeListener("keypress", onKey);
|
|
1206
|
+
stdin.setRawMode?.(wasRaw ?? false);
|
|
1207
|
+
stdin.pause();
|
|
1208
|
+
process.removeListener("exit", restoreTerminal);
|
|
1209
|
+
const lines =
|
|
1210
|
+
answer === null
|
|
1211
|
+
? [promptHeading(c, opts.message), c.ink3("cancelled")]
|
|
1212
|
+
: (opts.collapsed?.(answer) ?? [promptHeading(c, opts.message)]);
|
|
1213
|
+
// The last frame, left in scrollback: never cut (see `repaint`). It names
|
|
1214
|
+
// the org and the host the key went to.
|
|
1215
|
+
repaint(stdout, region, lines, { cut: false });
|
|
1216
|
+
settled = true;
|
|
1217
|
+
restoreTerminal();
|
|
1218
|
+
resolve(answer);
|
|
1219
|
+
};
|
|
1220
|
+
|
|
1221
|
+
const keepSaved = (): void => {
|
|
1222
|
+
if (savedState === "checking") {
|
|
1223
|
+
// Enter waits for a check still in flight, and says so by spinning.
|
|
1224
|
+
enterWaiting = true;
|
|
1225
|
+
return;
|
|
1226
|
+
}
|
|
1227
|
+
finish({ kind: "saved", verified: savedState === "works" });
|
|
1228
|
+
};
|
|
1229
|
+
|
|
1230
|
+
const submit = (): void => {
|
|
1231
|
+
if (typed.length === 0) {
|
|
1232
|
+
if (savedUsable()) return keepSaved();
|
|
1233
|
+
fieldError = opts.emptyError;
|
|
1234
|
+
refusedOnce = false;
|
|
1235
|
+
return draw();
|
|
1236
|
+
}
|
|
1237
|
+
if (typed.length < minLength) {
|
|
1238
|
+
fieldError = "That looks too short to be a key.";
|
|
1239
|
+
refusedOnce = false;
|
|
1240
|
+
return draw();
|
|
1241
|
+
}
|
|
1242
|
+
checkingTyped = true;
|
|
1243
|
+
fieldError = "";
|
|
1244
|
+
startSpinner();
|
|
1245
|
+
draw();
|
|
1246
|
+
const key = typed;
|
|
1247
|
+
opts.check(key).then(
|
|
1248
|
+
(verdict) => {
|
|
1249
|
+
if (settled) return;
|
|
1250
|
+
checkingTyped = false;
|
|
1251
|
+
stopSpinner();
|
|
1252
|
+
if (verdict.ok) return finish({ kind: "typed", key, note: verdict.line });
|
|
1253
|
+
fieldError = verdict.line;
|
|
1254
|
+
refusedOnce = true;
|
|
1255
|
+
draw();
|
|
1256
|
+
},
|
|
1257
|
+
(err: unknown) => {
|
|
1258
|
+
if (settled) return;
|
|
1259
|
+
checkingTyped = false;
|
|
1260
|
+
stopSpinner();
|
|
1261
|
+
fieldError = `Couldn't check the key: ${err instanceof Error ? err.message : String(err)}`;
|
|
1262
|
+
refusedOnce = true;
|
|
1263
|
+
draw();
|
|
1264
|
+
},
|
|
1265
|
+
);
|
|
1266
|
+
};
|
|
1267
|
+
|
|
1268
|
+
function onKey(str: string | undefined, key: readline.Key): void {
|
|
1269
|
+
if (!key || settled) return;
|
|
1270
|
+
if ((key.ctrl && (key.name === "c" || key.name === "d")) || key.name === "escape") return finish(null);
|
|
1271
|
+
if (key.name === "paste-start") {
|
|
1272
|
+
inPaste = true;
|
|
1273
|
+
return;
|
|
1274
|
+
}
|
|
1275
|
+
if (key.name === "paste-end") {
|
|
1276
|
+
inPaste = false;
|
|
1277
|
+
return draw();
|
|
1278
|
+
}
|
|
1279
|
+
if (checkingTyped || enterWaiting) return;
|
|
1280
|
+
if (key.name === "tab") {
|
|
1281
|
+
if (!inPaste) finish(OPEN_SOURCE);
|
|
1282
|
+
return;
|
|
1283
|
+
}
|
|
1284
|
+
if (key.name === "return" || key.name === "enter") {
|
|
1285
|
+
if (!inPaste) submit();
|
|
1286
|
+
return;
|
|
1287
|
+
}
|
|
1288
|
+
if (key.name === "backspace") {
|
|
1289
|
+
typed = typed.slice(0, -1);
|
|
1290
|
+
if (fieldError) fieldError = "";
|
|
1291
|
+
return draw();
|
|
1292
|
+
}
|
|
1293
|
+
// A key is never whitespace, so a copied newline or space is dropped
|
|
1294
|
+
// rather than becoming part of what gets sent.
|
|
1295
|
+
if (str && !key.ctrl && !key.meta) {
|
|
1296
|
+
const clean = str.replace(/\s+/g, "");
|
|
1297
|
+
if (!clean) return;
|
|
1298
|
+
typed += clean;
|
|
1299
|
+
if (fieldError) fieldError = "";
|
|
1300
|
+
if (!inPaste) draw();
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
process.once("exit", restoreTerminal);
|
|
1305
|
+
readline.emitKeypressEvents(stdin);
|
|
1306
|
+
stdin.setRawMode?.(true);
|
|
1307
|
+
stdin.resume();
|
|
1308
|
+
hideCursor(stdout);
|
|
1309
|
+
stdout.write(`${ESC}[?2004h`);
|
|
1310
|
+
stdin.on("keypress", onKey);
|
|
1311
|
+
if (opts.saved) startSpinner();
|
|
1312
|
+
draw();
|
|
1313
|
+
|
|
1314
|
+
opts.saved?.check().then(
|
|
1315
|
+
(verdict) => {
|
|
1316
|
+
if (settled) return;
|
|
1317
|
+
if (verdict.ok) {
|
|
1318
|
+
savedState = "works";
|
|
1319
|
+
savedLine = verdict.line ?? `Your saved key works: ${opts.saved?.masked}`;
|
|
1320
|
+
} else {
|
|
1321
|
+
savedState = verdict.usable ? "unchecked" : "refused";
|
|
1322
|
+
savedLine = verdict.line;
|
|
1323
|
+
}
|
|
1324
|
+
if (!checkingTyped) stopSpinner();
|
|
1325
|
+
if (enterWaiting) {
|
|
1326
|
+
enterWaiting = false;
|
|
1327
|
+
if (savedUsable()) return keepSaved();
|
|
1328
|
+
// Enter was pressed on an empty field, and the saved key it waited
|
|
1329
|
+
// for turned out refused: answer it as `submit` answers an empty
|
|
1330
|
+
// field, rather than redrawing as if nothing had been pressed.
|
|
1331
|
+
fieldError = opts.emptyError;
|
|
1332
|
+
refusedOnce = false;
|
|
1333
|
+
}
|
|
1334
|
+
draw();
|
|
1335
|
+
},
|
|
1336
|
+
(err: unknown) => {
|
|
1337
|
+
if (settled) return;
|
|
1338
|
+
savedState = "unchecked";
|
|
1339
|
+
savedLine = `Couldn't check your saved key: ${err instanceof Error ? err.message : String(err)}`;
|
|
1340
|
+
if (!checkingTyped) stopSpinner();
|
|
1341
|
+
if (enterWaiting) {
|
|
1342
|
+
enterWaiting = false;
|
|
1343
|
+
return keepSaved();
|
|
1344
|
+
}
|
|
1345
|
+
draw();
|
|
1346
|
+
},
|
|
1347
|
+
);
|
|
1348
|
+
});
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1032
1351
|
// ── the kit ──────────────────────────────────────────────────────────────────
|
|
1033
1352
|
/**
|
|
1034
1353
|
* The block builders every PRINTED surface is assembled from.
|
|
@@ -1043,9 +1362,7 @@ export function promptText(opts: PromptTextOptions): Promise<string | null> {
|
|
|
1043
1362
|
* answers to "how does this product state a fact".
|
|
1044
1363
|
*
|
|
1045
1364
|
* These are the one answer. Every builder is pure — `(spec, opts) => string[]` —
|
|
1046
|
-
* so a surface can be asserted at any width, with color on or off, without a pty
|
|
1047
|
-
* that is the same shape `renderBrandLogo`, `reviewLines` and `buildSummary`
|
|
1048
|
-
* already have, and the reason they are the only rendering we can currently test.
|
|
1365
|
+
* so a surface can be asserted at any width, with color on or off, without a pty.
|
|
1049
1366
|
*
|
|
1050
1367
|
* Callers pass `optsFor(stdout)` and print with `printBlock`, which owns the
|
|
1051
1368
|
* outer margins so no surface has to remember them.
|
|
@@ -1202,7 +1519,11 @@ function closeSgr(text: string): string {
|
|
|
1202
1519
|
* single-ellipsis cut when it is plain. */
|
|
1203
1520
|
function fit(cell: string, width: number): string {
|
|
1204
1521
|
if (visibleWidth(cell) <= width) return cell;
|
|
1205
|
-
|
|
1522
|
+
// A coloured cell is cut on its visible width and still ends on `…`: cut
|
|
1523
|
+
// bare, a shortened `description · observe` lost its tag with nothing to
|
|
1524
|
+
// say any of it was missing.
|
|
1525
|
+
if (cell.includes(ESC)) return width <= 1 ? "…" : `${closeSgr(truncate(cell, width - 1))}…`;
|
|
1526
|
+
return ellipsize(cell, width);
|
|
1206
1527
|
}
|
|
1207
1528
|
|
|
1208
1529
|
/** Pad a possibly-coloured cell to `width` visible columns. */
|
|
@@ -1246,10 +1567,17 @@ export function stack(...groups: Array<string[] | null | undefined>): string[] {
|
|
|
1246
1567
|
* a URL, a session id. Cutting those loses characters silently and with no
|
|
1247
1568
|
* ellipsis to admit it; letting the terminal wrap keeps them copyable, which is
|
|
1248
1569
|
* the entire reason they are printed.
|
|
1570
|
+
*
|
|
1571
|
+
* Colour is decided again from the stream written to. A command renders its
|
|
1572
|
+
* lines before it knows whether they succeed, so they are drawn for stdout,
|
|
1573
|
+
* and a failure then goes to stderr: with stdout a terminal and stderr
|
|
1574
|
+
* redirected, escape codes reached the log. A stream that cannot show colour
|
|
1575
|
+
* gets none, whatever the lines were drawn for.
|
|
1249
1576
|
*/
|
|
1250
1577
|
export function printBlock(stdout: TTYOut, lines: string[]): void {
|
|
1251
1578
|
if (lines.length === 0) return;
|
|
1252
|
-
stdout
|
|
1579
|
+
const shown = colorsEnabled(stdout) ? lines.map(closeSgr) : lines.map((l) => l.replace(/\x1b\[[0-9;?]*[A-Za-z]/g, ""));
|
|
1580
|
+
stdout.write(["", ...shown, ""].join("\n") + "\n");
|
|
1253
1581
|
}
|
|
1254
1582
|
|
|
1255
1583
|
/**
|
|
@@ -1482,21 +1810,6 @@ export function chip(state: ChipState, opts?: RenderOpts): string {
|
|
|
1482
1810
|
return pad(painted, CHIP_WIDTH);
|
|
1483
1811
|
}
|
|
1484
1812
|
|
|
1485
|
-
/** A bulleted list, wrapped, with continuation lines aligned under the text —
|
|
1486
|
-
* `uninstall` prints 200-column bullets today that wrap into column 0. */
|
|
1487
|
-
export function bullets(items: string[], opts?: RenderOpts): string[] {
|
|
1488
|
-
const { cols, c } = ctx(opts);
|
|
1489
|
-
const budget = Math.max(8, cols - INDENT.length - 4);
|
|
1490
|
-
const out: string[] = [];
|
|
1491
|
-
for (const item of items) {
|
|
1492
|
-
const [first, ...rest] = wrap(item, budget);
|
|
1493
|
-
if (first === undefined) continue;
|
|
1494
|
-
out.push(`${INDENT}${c.pink("•")} ${first}`);
|
|
1495
|
-
for (const line of rest) out.push(`${INDENT} ${line}`);
|
|
1496
|
-
}
|
|
1497
|
-
return out;
|
|
1498
|
-
}
|
|
1499
|
-
|
|
1500
1813
|
/** A dim aside under a block. */
|
|
1501
1814
|
export function note(text: string, opts?: RenderOpts): string[] {
|
|
1502
1815
|
const { cols, c } = ctx(opts);
|
|
@@ -1534,12 +1847,6 @@ export function warning(lines: string[], opts?: RenderOpts): string[] {
|
|
|
1534
1847
|
return gutterBlock(c.warn("\u25B2"), lines, opts);
|
|
1535
1848
|
}
|
|
1536
1849
|
|
|
1537
|
-
/** The same, for the ones that destroy something. */
|
|
1538
|
-
export function danger(lines: string[], opts?: RenderOpts): string[] {
|
|
1539
|
-
const { c } = ctx(opts);
|
|
1540
|
-
return gutterBlock(c.pink("!"), lines, opts);
|
|
1541
|
-
}
|
|
1542
|
-
|
|
1543
1850
|
/** Nothing to show, said the same way everywhere: what is empty, then the one
|
|
1544
1851
|
* command that changes that. */
|
|
1545
1852
|
export function emptyState(
|
|
@@ -1549,132 +1856,6 @@ export function emptyState(
|
|
|
1549
1856
|
return stack(note(spec.what, opts), spec.cmd ? nextStep(spec.cmd, spec.hint, opts) : null);
|
|
1550
1857
|
}
|
|
1551
1858
|
|
|
1552
|
-
export interface HelpSpec {
|
|
1553
|
-
usage: Array<[string, string?]>;
|
|
1554
|
-
options?: Array<[string, string]>;
|
|
1555
|
-
examples?: string[];
|
|
1556
|
-
/** Free lines above the first section. */
|
|
1557
|
-
lead?: string[];
|
|
1558
|
-
}
|
|
1559
|
-
|
|
1560
|
-
/**
|
|
1561
|
-
* The description column for one block of entries, derived from its widest
|
|
1562
|
-
* described name and capped.
|
|
1563
|
-
*
|
|
1564
|
-
* Its own step because the callers differ on the SCOPE they compute it over —
|
|
1565
|
-
* {@link helpBlock} across usage and options together, {@link helpScreen} per
|
|
1566
|
-
* section — and neither should be restating the cap or the floor.
|
|
1567
|
-
*/
|
|
1568
|
-
export function helpColumn(entries: Array<[string, string?]>): number {
|
|
1569
|
-
// Only DESCRIBED entries set the column. A bare usage line — `failproofai
|
|
1570
|
-
// flush [--wait] [--timeout <secs>]`, which describes itself — is 44
|
|
1571
|
-
// characters and has nothing in the second column, so letting it vote pushed
|
|
1572
|
-
// every real description on the screen out to column 38.
|
|
1573
|
-
const named = entries.filter(([, d]) => d).map(([n]) => visibleWidth(n));
|
|
1574
|
-
return Math.min(34, Math.max(12, ...named));
|
|
1575
|
-
}
|
|
1576
|
-
|
|
1577
|
-
/** `<name> <description>` at a column the caller fixed. */
|
|
1578
|
-
export function helpEntries(
|
|
1579
|
-
items: Array<[string, string?]>,
|
|
1580
|
-
nameWidth: number,
|
|
1581
|
-
opts?: RenderOpts,
|
|
1582
|
-
): string[] {
|
|
1583
|
-
const { cols, c } = ctx(opts);
|
|
1584
|
-
const out: string[] = [];
|
|
1585
|
-
for (const [raw, description] of items) {
|
|
1586
|
-
// Pink is what you TYPE, dim is what it means, bold is the section it sits
|
|
1587
|
-
// in. Three tones, the same three on every screen, so a flag looks like a
|
|
1588
|
-
// flag whether you found it on `publish --help` or on the index. Decoration
|
|
1589
|
-
// only: the column already separates the two halves, so the screen reads
|
|
1590
|
-
// the same piped to a file or under NO_COLOR.
|
|
1591
|
-
const name = c.pink(raw);
|
|
1592
|
-
if (!description) {
|
|
1593
|
-
out.push(`${INDENT}${name}`);
|
|
1594
|
-
continue;
|
|
1595
|
-
}
|
|
1596
|
-
const budget = Math.max(12, cols - INDENT.length * 2 - nameWidth - 2);
|
|
1597
|
-
const [first, ...rest] = wrap(description, budget);
|
|
1598
|
-
// A name wider than the column takes its own line rather than shoving the
|
|
1599
|
-
// description out — the top-level help has several of these today.
|
|
1600
|
-
if (visibleWidth(raw) > nameWidth) {
|
|
1601
|
-
out.push(`${INDENT}${name}`);
|
|
1602
|
-
for (const line of [first, ...rest]) {
|
|
1603
|
-
if (line !== undefined) out.push(`${INDENT}${" ".repeat(nameWidth)} ${c.dim(line)}`);
|
|
1604
|
-
}
|
|
1605
|
-
continue;
|
|
1606
|
-
}
|
|
1607
|
-
// Padded on the VISIBLE width, so a painted name still lands the column
|
|
1608
|
-
// where an unpainted one does.
|
|
1609
|
-
const gap = nameWidth - visibleWidth(raw);
|
|
1610
|
-
out.push(`${INDENT}${name}${" ".repeat(gap)} ${c.dim(first ?? "")}`);
|
|
1611
|
-
for (const line of rest) out.push(`${INDENT}${" ".repeat(nameWidth)} ${c.dim(line)}`);
|
|
1612
|
-
}
|
|
1613
|
-
return out;
|
|
1614
|
-
}
|
|
1615
|
-
|
|
1616
|
-
/**
|
|
1617
|
-
* A usage/options/examples block, on one column across all three.
|
|
1618
|
-
*
|
|
1619
|
-
* The plain shape, for a surface with nothing else to say. A screen with prose
|
|
1620
|
-
* sections, a heading, or a footer wants {@link helpScreen}, which is what the
|
|
1621
|
-
* twelve `--help` screens use.
|
|
1622
|
-
*/
|
|
1623
|
-
export function helpBlock(spec: HelpSpec, opts?: RenderOpts): string[] {
|
|
1624
|
-
const nameWidth = helpColumn([...spec.usage, ...(spec.options ?? [])]);
|
|
1625
|
-
const section = (label: string, body: string[]): string[] =>
|
|
1626
|
-
body.length === 0 ? [] : [...rule(label, opts), ...body];
|
|
1627
|
-
return stack(
|
|
1628
|
-
spec.lead ? spec.lead.map((l) => `${INDENT}${l}`) : null,
|
|
1629
|
-
section("usage", helpEntries(spec.usage, nameWidth, opts)),
|
|
1630
|
-
section("options", helpEntries(spec.options ?? [], nameWidth, opts)),
|
|
1631
|
-
section("examples", (spec.examples ?? []).map((e) => `${INDENT}${e}`)),
|
|
1632
|
-
);
|
|
1633
|
-
}
|
|
1634
|
-
|
|
1635
|
-
/**
|
|
1636
|
-
* Help is the one family of screens read at every width that belongs to no
|
|
1637
|
-
* terminal in particular. Capped at 80 so `publish --help` is the same shape in
|
|
1638
|
-
* a maximised window as in a tmux pane — and never wider than the terminal it
|
|
1639
|
-
* is actually in, so the cap can only ever narrow.
|
|
1640
|
-
*/
|
|
1641
|
-
export const HELP_COLS = 80;
|
|
1642
|
-
|
|
1643
|
-
/** {@link optsFor}, capped to {@link HELP_COLS}. Every help screen uses this. */
|
|
1644
|
-
export function helpOptsFor(stdout: TTYOut = process.stdout): Required<RenderOpts> {
|
|
1645
|
-
const base = optsFor(stdout);
|
|
1646
|
-
return { cols: Math.min(base.cols, HELP_COLS), color: base.color };
|
|
1647
|
-
}
|
|
1648
|
-
|
|
1649
|
-
export interface HelpHeading {
|
|
1650
|
-
/** The command being documented. Omitted for the top-level index. */
|
|
1651
|
-
command?: string;
|
|
1652
|
-
version: string;
|
|
1653
|
-
/** One line: what this command is for. */
|
|
1654
|
-
tagline: string;
|
|
1655
|
-
}
|
|
1656
|
-
|
|
1657
|
-
/**
|
|
1658
|
-
* The two lines every help screen opens with.
|
|
1659
|
-
*
|
|
1660
|
-
* The wordmark carries the same pink on `il` that {@link renderBrandLogo} tints,
|
|
1661
|
-
* so a help screen and the wizard are visibly one product rather than two that
|
|
1662
|
-
* happen to ship together. The version sits right-aligned and dim because it is
|
|
1663
|
-
* the fact you want when a help screen and its binary disagree, and the fact you
|
|
1664
|
-
* never want in the way otherwise.
|
|
1665
|
-
*/
|
|
1666
|
-
export function helpHeading(spec: HelpHeading, opts?: RenderOpts): string[] {
|
|
1667
|
-
const { cols, c } = ctx(opts);
|
|
1668
|
-
const mark = `${c.bold("fa")}${c.pink("il")}${c.bold("proofai")}`;
|
|
1669
|
-
const left = `${INDENT}${mark}${spec.command ? ` ${c.bold(spec.command)}` : ""}`;
|
|
1670
|
-
const right = c.dim(`v${spec.version}`);
|
|
1671
|
-
const gap = cols - visibleWidth(left) - visibleWidth(right) - INDENT.length;
|
|
1672
|
-
// Too narrow for both: the version goes under rather than wrapping into the
|
|
1673
|
-
// middle of the name, where it reads as part of the command.
|
|
1674
|
-
const head = gap < 2 ? [left, `${INDENT}${right}`] : [left + " ".repeat(gap) + right];
|
|
1675
|
-
return [...head, ...note(spec.tagline, opts)];
|
|
1676
|
-
}
|
|
1677
|
-
|
|
1678
1859
|
/**
|
|
1679
1860
|
* A section of a help screen: a table of names, a paragraph, or lines that are
|
|
1680
1861
|
* already laid out. Every one of them gets the same `rule` heading, which is
|
|
@@ -1688,46 +1869,325 @@ export type HelpSection =
|
|
|
1688
1869
|
| { label: string; prose: string }
|
|
1689
1870
|
| { label: string; lines: string[] };
|
|
1690
1871
|
|
|
1691
|
-
|
|
1692
|
-
sections: HelpSection[];
|
|
1693
|
-
/** Dim closing lines — where to read more. */
|
|
1694
|
-
footer?: string[];
|
|
1695
|
-
}
|
|
1696
|
-
|
|
1872
|
+
// ── the 2026-10 screen language ─────────────────────────────────────────────
|
|
1697
1873
|
/**
|
|
1698
|
-
*
|
|
1874
|
+
* The redesign's building blocks: open text on the terminal — the wordmark
|
|
1875
|
+
* header, UPPERCASE headings at column 0, rows indented two, colour by role, and
|
|
1876
|
+
* no boxes or rules. The byte-level target is `reference/screens.js` →
|
|
1877
|
+
* `FP.primitives` in the design handoff; the bytes differ only where the tiers
|
|
1878
|
+
* above say how a colour is spelled.
|
|
1699
1879
|
*
|
|
1700
|
-
*
|
|
1701
|
-
*
|
|
1702
|
-
*
|
|
1703
|
-
*
|
|
1704
|
-
*
|
|
1880
|
+
* Commands, URLs, paths and ids are never cut: they are the part a person
|
|
1881
|
+
* copies, and half of one runs the wrong thing. Prose may be shortened with `…`
|
|
1882
|
+
* when `fit` is on, which a caller sets only for a terminal — piped output is
|
|
1883
|
+
* never cut.
|
|
1884
|
+
*
|
|
1885
|
+
* ONE exported factory rather than a dozen functions, on purpose. The CLI ships
|
|
1886
|
+
* as a single scope-hoisted bundle, and a new top-level name that matches a
|
|
1887
|
+
* local inside a builtin policy or an audit detector (`cmd`, `row`, `head`, …)
|
|
1888
|
+
* makes the bundler rename that local. That changes the function source the
|
|
1889
|
+
* audit cache keys on (`engineVersion` / `detectorVersion`), and every user's
|
|
1890
|
+
* history is rescanned from zero at upgrade. Everything here lives inside the
|
|
1891
|
+
* factory, so the bundle gains exactly one name.
|
|
1705
1892
|
*/
|
|
1706
|
-
export
|
|
1893
|
+
export interface ScreenKitOpts extends RenderOpts {
|
|
1894
|
+
/** The version the header shows. Defaults to this build's. */
|
|
1895
|
+
version?: string;
|
|
1896
|
+
/** Shorten prose that does not fit `cols`, ending on `…`. Terminal only. */
|
|
1897
|
+
fit?: boolean;
|
|
1898
|
+
}
|
|
1899
|
+
|
|
1900
|
+
export function screenKit(opts: ScreenKitOpts = {}) {
|
|
1707
1901
|
const { cols, c } = ctx(opts);
|
|
1708
|
-
const
|
|
1709
|
-
const
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1902
|
+
const colour = opts.color ?? false;
|
|
1903
|
+
const version = opts.version ?? cliVersion;
|
|
1904
|
+
const sep = c.ink3(" · ");
|
|
1905
|
+
// Never paint nothing: an empty span is two escapes with no text between.
|
|
1906
|
+
const tint = (paintFn: (s: string) => string, s: string): string => (s ? paintFn(s) : "");
|
|
1907
|
+
|
|
1908
|
+
/** Typeable: every command on a screen is pink. */
|
|
1909
|
+
const cmd = (s: string): string => c.pink(s);
|
|
1910
|
+
const on = c.guide(RADIO_ON);
|
|
1911
|
+
const off = c.ink3(RADIO_OFF);
|
|
1912
|
+
/** A row whose thing could not run — a file that is missing or will not load. */
|
|
1913
|
+
const failed = c.err("✕");
|
|
1914
|
+
/** Picked and not picked: a pack's defaults on `policies show`, a checklist row. */
|
|
1915
|
+
const selected = c.pink("■");
|
|
1916
|
+
const unselected = c.ink3("□");
|
|
1917
|
+
/** Grey text that qualifies a value rather than being one: `observe` beside a pack. */
|
|
1918
|
+
const meta = (s: string): string => tint(c.ink3, s);
|
|
1919
|
+
|
|
1920
|
+
/** Prose shortened to `room` columns when fitting is on; untouched otherwise. */
|
|
1921
|
+
const prose = (s: string, room: number): string =>
|
|
1922
|
+
opts.fit && visibleWidth(s) > room ? fit(s, Math.max(1, room)) : s;
|
|
1923
|
+
|
|
1924
|
+
/** `failproof ai v1.0.11 · Context` — the line every screen opens with. */
|
|
1925
|
+
const header = (context?: string): string =>
|
|
1926
|
+
`${c.bold("fa")}${c.pinkBold("il")}${c.bold("proof ai")} ${c.ink3(`v${version}`)}` +
|
|
1927
|
+
(context ? `${sep}${context}` : "");
|
|
1928
|
+
|
|
1929
|
+
/** An UPPERCASE heading, with optional grey meta after it. */
|
|
1930
|
+
const head = (text: string, meta?: string): string =>
|
|
1931
|
+
c.bold(text.toUpperCase()) + (meta ? ` ${c.ink3(meta)}` : "");
|
|
1932
|
+
|
|
1933
|
+
/**
|
|
1934
|
+
* `name description` rows on one column for the block: at least `minCol`
|
|
1935
|
+
* wide, and always two spaces past the widest name. The design fixes its
|
|
1936
|
+
* columns by hand, and the longest real policy name is exactly the 32 it
|
|
1937
|
+
* fixes, so a fixed column runs that name into its description. Names are
|
|
1938
|
+
* never cut; descriptions are prose and give up the room.
|
|
1939
|
+
*/
|
|
1940
|
+
const rows = (items: Array<[string, string?]>, minCol = 0): string[] => {
|
|
1941
|
+
const width = Math.max(minCol, ...items.map(([name, desc]) => (desc ? visibleWidth(name) + 2 : 0)));
|
|
1942
|
+
return items.map(([name, desc]) => {
|
|
1943
|
+
if (!desc) return `${INDENT}${name}`;
|
|
1944
|
+
const lead = `${INDENT}${pad(name, width)}`;
|
|
1945
|
+
return lead + prose(c.ink2(desc), cols - visibleWidth(lead));
|
|
1946
|
+
});
|
|
1947
|
+
};
|
|
1948
|
+
|
|
1949
|
+
/**
|
|
1950
|
+
* Grey labels and their values, on one column at least nine wide — where the
|
|
1951
|
+
* reference starts its values. Values are never cut: they are URLs, ids and
|
|
1952
|
+
* counts, and a line that outgrows the terminal is left for it to wrap.
|
|
1953
|
+
*
|
|
1954
|
+
* An empty label continues the row above it, under the same value column —
|
|
1955
|
+
* `config --status` lists each paused session that way.
|
|
1956
|
+
*/
|
|
1957
|
+
const kv = (items: Array<[string, string]>, minLabel = 9): string[] => {
|
|
1958
|
+
const width = Math.max(minLabel, ...items.map(([label]) => visibleWidth(label))) + 2;
|
|
1959
|
+
return items.map(([label, value]) =>
|
|
1960
|
+
value ? `${INDENT}${pad(tint(c.ink3, label), width)}${value}` : `${INDENT}${tint(c.ink3, label)}`,
|
|
1961
|
+
);
|
|
1962
|
+
};
|
|
1963
|
+
|
|
1964
|
+
/** `✓ Done, said in the past tense.` with optional grey detail. */
|
|
1965
|
+
const ok = (text: string, detail?: string): string =>
|
|
1966
|
+
`${c.guide("✓")} ${text}${detail ? ` ${c.ink3(detail)}` : ""}`;
|
|
1967
|
+
|
|
1968
|
+
/** `▲ Needs attention. · the fix` */
|
|
1969
|
+
const caution = (text: string, fix?: string): string =>
|
|
1970
|
+
`${c.warn("▲")} ${text}${fix ? `${sep}${cmd(fix)}` : ""}`;
|
|
1971
|
+
|
|
1972
|
+
/** `✕ Failed, said plainly. · the fix` */
|
|
1973
|
+
const fail = (text: string, fix?: string): string =>
|
|
1974
|
+
`${c.err("✕")} ${text}${fix ? `${sep}${cmd(fix)}` : ""}`;
|
|
1975
|
+
|
|
1976
|
+
/**
|
|
1977
|
+
* A message as every notice prints it: the first line after one state glyph,
|
|
1978
|
+
* each later line a fix line indented two (keeping any indentation of its
|
|
1979
|
+
* own), and each `backticked` span painted as a command with the backticks
|
|
1980
|
+
* dropped. A message author writes `Run \`failproofai help\` for usage.`,
|
|
1981
|
+
* and it reads `Run failproofai help for usage.` in a pipe, with the
|
|
1982
|
+
* command in pink on a terminal. Unbalanced backticks are left as written
|
|
1983
|
+
* rather than guessed at.
|
|
1984
|
+
*
|
|
1985
|
+
* Never cut: a notice holds the reason something failed and the command that
|
|
1986
|
+
* fixes it, and half of either is worse than a line the terminal wraps.
|
|
1987
|
+
*/
|
|
1988
|
+
const notice = (state: "ok" | "caution" | "fail", message: string): string[] => {
|
|
1989
|
+
const typeable = (line: string): string =>
|
|
1990
|
+
(line.split("`").length - 1) % 2 === 0 ? line.replace(/`([^`]+)`/g, (_whole, typed: string) => cmd(typed)) : line;
|
|
1991
|
+
const glyph = state === "ok" ? c.guide("✓") : state === "caution" ? c.warn("▲") : c.err("✕");
|
|
1992
|
+
const [first = "", ...rest] = message.split("\n");
|
|
1993
|
+
return [
|
|
1994
|
+
`${glyph} ${typeable(first)}`,
|
|
1995
|
+
...rest.map((line) => (line.trim() === "" ? "" : `${INDENT}${typeable(line)}`)),
|
|
1996
|
+
];
|
|
1997
|
+
};
|
|
1998
|
+
|
|
1999
|
+
/**
|
|
2000
|
+
* `Continue? y/N` — the one question a destructive command asks, after the
|
|
2001
|
+
* caller has printed what it would do. Only `y` or `yes`, in any case, then
|
|
2002
|
+
* Enter, is a yes. Enter alone, anything else, Esc, Ctrl+C, Ctrl+D or the
|
|
2003
|
+
* input closing is a no: the default answer to "remove these?" has to be the
|
|
2004
|
+
* one that removes nothing, and one stray key must not be consent. Keys are
|
|
2005
|
+
* read the way the pickers read them (raw, echoed here), so it behaves the
|
|
2006
|
+
* same on every terminal. Callers ask only when there is one to answer on.
|
|
2007
|
+
*/
|
|
2008
|
+
const confirm = (io: { stdin: TTYIn; stdout: TTYOut }, question = "Continue?"): Promise<boolean> =>
|
|
2009
|
+
new Promise((resolve) => {
|
|
2010
|
+
const { stdin, stdout } = io;
|
|
2011
|
+
let typed = "";
|
|
2012
|
+
let settled = false;
|
|
2013
|
+
const wasRaw = stdin.isRaw;
|
|
2014
|
+
const settle = (answer: boolean): void => {
|
|
2015
|
+
if (settled) return;
|
|
2016
|
+
settled = true;
|
|
2017
|
+
stdin.removeListener("keypress", onKey);
|
|
2018
|
+
stdin.removeListener("end", onEnd);
|
|
2019
|
+
stdin.setRawMode?.(wasRaw ?? false);
|
|
2020
|
+
stdin.pause();
|
|
2021
|
+
stdout.write("\n");
|
|
2022
|
+
resolve(answer);
|
|
2023
|
+
};
|
|
2024
|
+
function onEnd(): void {
|
|
2025
|
+
settle(false);
|
|
2026
|
+
}
|
|
2027
|
+
function onKey(sequence: string | undefined, key: readline.Key | undefined): void {
|
|
2028
|
+
const name = key?.name;
|
|
2029
|
+
if ((key?.ctrl && (name === "c" || name === "d")) || name === "escape") settle(false);
|
|
2030
|
+
else if (name === "return" || name === "enter") settle(/^y(es)?$/i.test(typed.trim()));
|
|
2031
|
+
else if (name === "backspace") {
|
|
2032
|
+
if (typed) {
|
|
2033
|
+
typed = typed.slice(0, -1);
|
|
2034
|
+
stdout.write("\b \b");
|
|
2035
|
+
}
|
|
2036
|
+
} else if (sequence && sequence.length === 1 && sequence >= " " && !key?.ctrl && !key?.meta) {
|
|
2037
|
+
typed += sequence;
|
|
2038
|
+
stdout.write(sequence);
|
|
2039
|
+
}
|
|
2040
|
+
}
|
|
2041
|
+
stdout.write(`${question} ${c.ink3("y/N")} `);
|
|
2042
|
+
readline.emitKeypressEvents(stdin);
|
|
2043
|
+
stdin.setRawMode?.(true);
|
|
2044
|
+
stdin.on("keypress", onKey);
|
|
2045
|
+
stdin.on("end", onEnd);
|
|
2046
|
+
stdin.resume();
|
|
2047
|
+
});
|
|
2048
|
+
|
|
2049
|
+
/** Lowercase key hints at the foot of an interactive screen. */
|
|
2050
|
+
const keys = (list: string[]): string => c.ink3(list.join(" · "));
|
|
2051
|
+
|
|
2052
|
+
/**
|
|
2053
|
+
* A progress bar `width` cells wide and `fraction` full: the filled part pink,
|
|
2054
|
+
* the rest the track grey. With colour off the rest is blank, because one
|
|
2055
|
+
* unbroken run of `━` would show no progress at all.
|
|
2056
|
+
*/
|
|
2057
|
+
const bar = (width: number, fraction: number): string => {
|
|
2058
|
+
const filled = Math.round(Math.max(0, Math.min(1, fraction)) * width);
|
|
2059
|
+
const rest = Math.max(0, width - filled);
|
|
2060
|
+
return tint(c.pink, "━".repeat(filled)) + (colour ? tint(c.track, "━".repeat(rest)) : " ".repeat(rest));
|
|
2061
|
+
};
|
|
2062
|
+
|
|
2063
|
+
/**
|
|
2064
|
+
* The logomark with a two-space margin, or nothing on a terminal too narrow
|
|
2065
|
+
* to draw it — the old `▮▮` one-liner is retired with the rest of that mark.
|
|
2066
|
+
* Callers show it only on a TTY, and only on bare `failproofai` and at the
|
|
2067
|
+
* start of a `config` run.
|
|
2068
|
+
*/
|
|
2069
|
+
const logo = (): string[] => {
|
|
2070
|
+
if (cols < LOGO_MIN_COLS) return [];
|
|
2071
|
+
const tier: ColorTier = colour ? colorTier() : "basic";
|
|
2072
|
+
return renderLogo(tier).map((line) => `${INDENT}${line}`);
|
|
2073
|
+
};
|
|
2074
|
+
|
|
2075
|
+
/** A command's `--help`: usage, options and examples. Nothing else. */
|
|
2076
|
+
const helpPage = (spec: {
|
|
2077
|
+
name: string;
|
|
2078
|
+
usage: Array<[string, string?]>;
|
|
2079
|
+
options?: Array<[string, string]>;
|
|
2080
|
+
examples?: string[];
|
|
2081
|
+
/** The options column, when the design fixes a wider one than the data needs. */
|
|
2082
|
+
optionsCol?: number;
|
|
2083
|
+
}): string[] => {
|
|
2084
|
+
// Option descriptions name flags and agent ids (`-u -c`, `goose`), which are
|
|
2085
|
+
// never cut: one too wide for the terminal wraps under its own column
|
|
2086
|
+
// rather than ending on `…`. Usage descriptions are prose, and shorten.
|
|
2087
|
+
const options = (items: Array<[string, string]>, minCol: number): string[] => {
|
|
2088
|
+
const width = Math.max(minCol, ...items.map(([name, desc]) => (desc ? visibleWidth(name) + 2 : 0)));
|
|
2089
|
+
return items.flatMap(([name, desc]) => {
|
|
2090
|
+
if (!desc) return [`${INDENT}${name}`];
|
|
2091
|
+
const lead = `${INDENT}${pad(name, width)}`;
|
|
2092
|
+
const room = cols - visibleWidth(lead);
|
|
2093
|
+
if (!opts.fit || visibleWidth(desc) <= room) return [lead + c.ink2(desc)];
|
|
2094
|
+
const under = " ".repeat(visibleWidth(lead));
|
|
2095
|
+
return wrapAnsi(desc, Math.max(1, room)).map((part, i) => (i === 0 ? lead : under) + c.ink2(part));
|
|
2096
|
+
});
|
|
2097
|
+
};
|
|
2098
|
+
const out = [header(spec.name), "", head("Usage"), ...rows(spec.usage)];
|
|
2099
|
+
if (spec.options?.length) out.push("", head("Options"), ...options(spec.options, spec.optionsCol ?? 0));
|
|
2100
|
+
if (spec.examples?.length) out.push("", head("Examples"), ...spec.examples.map((e) => `${INDENT}${cmd(e)}`));
|
|
2101
|
+
return out;
|
|
2102
|
+
};
|
|
2103
|
+
|
|
2104
|
+
/**
|
|
2105
|
+
* A block that redraws in place: a progress screen.
|
|
2106
|
+
*
|
|
2107
|
+
* Every frame is ONE write, through the same atomic `repaint` the prompts use,
|
|
2108
|
+
* and frames are held to one per 100 ms — the design's progress tempo — with
|
|
2109
|
+
* the newest always landing. `build` runs only for a frame that is actually
|
|
2110
|
+
* drawn, so a caller can ask after every event it hears about.
|
|
2111
|
+
*
|
|
2112
|
+
* The cursor is hidden while the block is live and shown again when it ends:
|
|
2113
|
+
* on `done`, on exit, and on SIGINT or SIGTERM. A signal is re-raised once the
|
|
2114
|
+
* cursor is back, so Ctrl+C still stops the command exactly as it did before
|
|
2115
|
+
* anything was listening for it. TTY only: everywhere else the caller prints
|
|
2116
|
+
* its last screen once, with no frames.
|
|
2117
|
+
*/
|
|
2118
|
+
const live = (out: TTYOut) => {
|
|
2119
|
+
const region: Region = { lastCount: 0 };
|
|
2120
|
+
const every = 100;
|
|
2121
|
+
let lastAt = 0;
|
|
2122
|
+
let pending: (() => string[]) | null = null;
|
|
2123
|
+
let timer: ReturnType<typeof setTimeout> | null = null;
|
|
2124
|
+
let hidden = false;
|
|
2125
|
+
let over = false;
|
|
2126
|
+
|
|
2127
|
+
const showAgain = (): void => {
|
|
2128
|
+
out.write(`${ESC}[?25h`);
|
|
2129
|
+
};
|
|
2130
|
+
const release = (): void => {
|
|
2131
|
+
process.removeListener("exit", showAgain);
|
|
2132
|
+
process.removeListener("SIGINT", onSignal);
|
|
2133
|
+
process.removeListener("SIGTERM", onSignal);
|
|
2134
|
+
};
|
|
2135
|
+
const onSignal = (signal: NodeJS.Signals): void => {
|
|
2136
|
+
over = true;
|
|
2137
|
+
if (timer) clearTimeout(timer);
|
|
2138
|
+
release();
|
|
2139
|
+
showAgain();
|
|
2140
|
+
process.kill(process.pid, signal);
|
|
2141
|
+
};
|
|
2142
|
+
const flush = (): void => {
|
|
2143
|
+
timer = null;
|
|
2144
|
+
const build = pending;
|
|
2145
|
+
pending = null;
|
|
2146
|
+
if (!build || over) return;
|
|
2147
|
+
lastAt = Date.now();
|
|
2148
|
+
let lead = "";
|
|
2149
|
+
if (!hidden) {
|
|
2150
|
+
hidden = true;
|
|
2151
|
+
lead = `${ESC}[?25l`;
|
|
2152
|
+
process.once("exit", showAgain);
|
|
2153
|
+
process.on("SIGINT", onSignal);
|
|
2154
|
+
process.on("SIGTERM", onSignal);
|
|
2155
|
+
}
|
|
2156
|
+
repaint(out, region, build(), { lead });
|
|
2157
|
+
};
|
|
2158
|
+
|
|
2159
|
+
return {
|
|
2160
|
+
/** Ask for a frame; it is drawn now, or as soon as the tempo allows. */
|
|
2161
|
+
draw(build: () => string[]): void {
|
|
2162
|
+
if (over) return;
|
|
2163
|
+
pending = build;
|
|
2164
|
+
if (timer) return;
|
|
2165
|
+
const wait = lastAt + every - Date.now();
|
|
2166
|
+
if (wait <= 0) {
|
|
2167
|
+
flush();
|
|
2168
|
+
return;
|
|
2169
|
+
}
|
|
2170
|
+
timer = setTimeout(flush, wait);
|
|
2171
|
+
timer.unref?.();
|
|
2172
|
+
},
|
|
2173
|
+
/**
|
|
2174
|
+
* Draw the last frame at once and give the cursor back. Never cut: nothing
|
|
2175
|
+
* repaints it, so it stays in scrollback exactly as written.
|
|
2176
|
+
*/
|
|
2177
|
+
done(lines: string[]): void {
|
|
2178
|
+
if (over) return;
|
|
2179
|
+
over = true;
|
|
2180
|
+
if (timer) clearTimeout(timer);
|
|
2181
|
+
timer = null;
|
|
2182
|
+
pending = null;
|
|
2183
|
+
release();
|
|
2184
|
+
repaint(out, region, lines, { cut: false, tail: hidden ? `${ESC}[?25h` : "" });
|
|
2185
|
+
},
|
|
2186
|
+
};
|
|
2187
|
+
};
|
|
2188
|
+
|
|
2189
|
+
return {
|
|
2190
|
+
cols, sep, cmd, on, off, failed, selected, unselected, meta,
|
|
2191
|
+
header, head, rows, kv, ok, caution, fail, notice, confirm, keys, bar, logo, helpPage, live,
|
|
2192
|
+
};
|
|
1733
2193
|
}
|