failproofai 1.0.0-beta.11 → 1.0.0-beta.13
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 +3 -3
- package/.next/standalone/.next/prerender-manifest.json +3 -3
- package/.next/standalone/.next/required-server-files.json +1 -1
- 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 +2 -3
- 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.rsc +7 -7
- package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +7 -7
- package/.next/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +3 -3
- package/.next/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +3 -3
- package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- 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 +2 -3
- 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.rsc +15 -15
- package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +15 -15
- package/.next/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +4 -4
- package/.next/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +10 -10
- package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +3 -3
- package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/api/audit/invite/route.js +2 -2
- 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 +2 -2
- 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.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/login-request/route.js +3 -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 +2 -2
- 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 +2 -2
- package/.next/standalone/.next/server/app/api/auth/logout/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/reminder/route.js +2 -2
- package/.next/standalone/.next/server/app/api/auth/reminder/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/status/route.js +2 -2
- 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.nft.json +1 -1
- package/.next/standalone/.next/server/app/audit/page/server-reference-manifest.json +2 -2
- package/.next/standalone/.next/server/app/audit/page.js +2 -3
- 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.rsc +15 -15
- package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +15 -15
- package/.next/standalone/.next/server/app/index.segments/_head.segment.rsc +4 -4
- package/.next/standalone/.next/server/app/index.segments/_index.segment.rsc +10 -10
- package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/page.js +2 -3
- 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/server-reference-manifest.json +10 -10
- package/.next/standalone/.next/server/app/policies/page.js +2 -3
- 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/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/project/[name]/page.js +3 -4
- 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/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 +3 -4
- 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/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/projects/page.js +3 -4
- 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/server-reference-manifest.json +4 -4
- package/.next/standalone/.next/server/app/settings/page.js +2 -3
- 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/{[root-of-the-server]__1a7i2q4._.js → [root-of-the-server]__00ni9za._.js} +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__08y94xv._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__094p1f1._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0_0xu5z._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0ed01_d._.js +1 -1
- package/.next/standalone/.next/server/chunks/{[root-of-the-server]__1_ps0jw._.js → [root-of-the-server]__0effm9m._.js} +1 -1
- package/.next/standalone/.next/server/chunks/{[root-of-the-server]__0lk_935._.js → [root-of-the-server]__0ix8gm7._.js} +1 -1
- package/.next/standalone/.next/server/chunks/{[root-of-the-server]__0ky_n0r._.js → [root-of-the-server]__0ji11a-._.js} +1 -1
- package/.next/standalone/.next/server/chunks/{[root-of-the-server]__0v2fpxe._.js → [root-of-the-server]__0ojwjxl._.js} +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0vb5fl2._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1ibjy5x._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1tvn4wf._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1uyc-z4._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__20d06rh._.js +3 -0
- package/.next/standalone/.next/server/chunks/_0lxbzdq._.js +1 -3
- package/.next/standalone/.next/server/chunks/_1zuiiy3._.js +1 -3
- 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]__1rdruw4._.js → [root-of-the-server]__0_1tiya._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__20ie7me._.js → [root-of-the-server]__0eeipso._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0spkm68._.js +3 -3
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1tyf0tc._.js → [root-of-the-server]__17itmx-._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1gwr8l-._.js → [root-of-the-server]__1dx-a-c._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1mmefsr._.js +5 -0
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1yw368c._.js → [root-of-the-server]__1n2vcv4._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1rv-sc9._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1a2hcb_._.js → [root-of-the-server]__1tlvv9a._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_0rjgfln._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{_1fahg9e._.js → _0vo2qyt._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{_0uz7o9y._.js → _0xej-0e._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{_04mekdy._.js → _10399ge._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{_0se484m._.js → _149k9oy._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{_0n4bc3i._.js → _166jvu8._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{_0r-dsre._.js → _1_hwp06._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{_1w8sola._.js → _1mnhi_r._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{_1w_6p9g._.js → _1ror7k9._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{_1gmzkwj._.js → _1sdgprl._.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/middleware-build-manifest.js +3 -3
- 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 +15 -15
- package/.next/standalone/.next/static/chunks/{0-m1vctj2jny6.js → 01s3vpzky_qew.js} +1 -1
- package/.next/standalone/.next/static/chunks/0km-jn9o5xyqs.css +2 -0
- package/.next/standalone/.next/static/chunks/{0on899o88y54u.js → 0zbcd_n8ku2ov.js} +1 -1
- package/.next/standalone/.next/static/chunks/{04y6igmx5olt6.js → 1erji0ratofla.js} +1 -1
- package/.next/standalone/.next/static/chunks/{06abmgw7cvecc.js → 23hnlzqx44vbe.js} +1 -1
- package/.next/standalone/.next/static/chunks/{1a9qlmo5s678i.js → 2cwsiyhc7d2cw.js} +1 -1
- package/.next/standalone/.next/static/chunks/{37nu_nos28w1x.js → 2y42ebzynyxdv.js} +1 -1
- package/.next/standalone/.next/static/chunks/{0ug1y_6-uwu5t.js → 315lncj7dsp9c.js} +1 -1
- package/.next/standalone/.next/static/chunks/31_px0dfgdg9j.js +6 -0
- package/.next/standalone/.next/static/chunks/{43ylh_2r15k-w.js → 3l4-fu3xt75j8.js} +1 -1
- package/.next/standalone/.next/static/chunks/3poscoxn69x9g.js +1 -0
- package/.next/standalone/app/policies/hooks-client.tsx +4 -4
- package/.next/standalone/ino.ujh3/.failproofai/VERSION +4 -0
- package/.next/standalone/ino.ujh3/.failproofai/state/last-version +1 -0
- package/.next/standalone/ino.ujh3/.failproofai/state/telemetry-id +1 -0
- package/.next/standalone/package.json +10 -10
- package/.next/standalone/server.js +1 -1
- package/bin/failproofai-worker.mjs +1 -1
- package/bin/failproofai.mjs +186 -5
- package/dist/cli.mjs +1130 -1352
- package/dist/worker.mjs +171 -1010
- package/package.json +10 -10
- package/src/hooks/backfill-cli.ts +1 -1
- package/src/hooks/cloud-connection.ts +4 -4
- package/src/hooks/cloud-enrollment-cli.ts +1 -1
- package/src/hooks/cloud-enrollment.ts +3 -3
- package/src/hooks/cloud-managed-policies.ts +60 -22
- package/src/hooks/configure-wizard.ts +124 -53
- package/src/hooks/custom-hooks-loader.ts +57 -8
- package/src/hooks/daemon-service.ts +66 -11
- package/src/hooks/flush-cli.ts +180 -0
- package/src/hooks/fp-config.ts +242 -118
- package/src/hooks/fp-home.ts +138 -22
- package/src/hooks/fp-reset.ts +357 -43
- package/src/hooks/handler.ts +11 -11
- package/src/hooks/harness-cli.ts +309 -0
- package/src/hooks/hook-activity-store.ts +5 -5
- package/src/hooks/manager.ts +35 -0
- package/src/hooks/onboarding-attempt.ts +22 -6
- package/src/hooks/tui.ts +64 -7
- package/src/hooks/uninstall-cli.ts +1 -1
- package/src/hooks/worker-server.ts +69 -12
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__06hexd0._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1iny24d._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1tyedjt._.js +0 -3
- package/.next/standalone/.next/server/chunks/lib_telemetry_ts_0dctyyw._.js +0 -5
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1u-wx42._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_fp-config_ts_04t589g._.js +0 -5
- package/.next/standalone/.next/static/chunks/0p_y4ndalc68e.js +0 -6
- package/.next/standalone/.next/static/chunks/13vrxdsdihqd_.css +0 -2
- package/.next/standalone/.next/static/chunks/19lrnqxn7ni7i.js +0 -1
- /package/.next/standalone/.next/static/{BZDo0aAHzDwh5ntxgIgpc → ziAsXfNE0cH_S7LHK-Py3}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{BZDo0aAHzDwh5ntxgIgpc → ziAsXfNE0cH_S7LHK-Py3}/_clientMiddlewareManifest.js +0 -0
- /package/.next/standalone/.next/static/{BZDo0aAHzDwh5ntxgIgpc → ziAsXfNE0cH_S7LHK-Py3}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `failproofai harness` — manage extra capture paths per harness.
|
|
3
|
+
*
|
|
4
|
+
* Each supported agent CLI has one location its own installer put it in, and
|
|
5
|
+
* the collector watches that. This command adds others: a second profile, a
|
|
6
|
+
* bind-mounted team share, a container's home mapped in beside the host's, an
|
|
7
|
+
* agent an operator relocated. Those hold real sessions and nothing collected
|
|
8
|
+
* them.
|
|
9
|
+
*
|
|
10
|
+
* # This file edits config; the daemon decides what the config means
|
|
11
|
+
*
|
|
12
|
+
* Entries are stored as written. The grammar (`label=path`, folder-name
|
|
13
|
+
* fallback), `~` expansion, overlap rejection and the `<label>-<agentId>`
|
|
14
|
+
* namespacing all live in `crates/fpai-collect/src/extra_paths.rs`, and the
|
|
15
|
+
* daemon reports what it rejected at startup. Re-implementing any of that here
|
|
16
|
+
* would give one product two parsers, and the way that fails is the CLI
|
|
17
|
+
* accepting a path the daemon then silently drops — which is indistinguishable
|
|
18
|
+
* from the feature working.
|
|
19
|
+
*
|
|
20
|
+
* The one thing validated here is the HARNESS NAME, because that failure has no
|
|
21
|
+
* other detector: `[collector.sources.claud]` is valid TOML, parses cleanly,
|
|
22
|
+
* captures nothing, and is mentioned nowhere else a user would look.
|
|
23
|
+
*
|
|
24
|
+
* # No restart, and no sudo
|
|
25
|
+
*
|
|
26
|
+
* This writes one file in the user's own home and nothing else. The daemon's
|
|
27
|
+
* collector manager re-reads `config.toml` on an interval and cycles the
|
|
28
|
+
* collector whenever the resolved `CollectorConfig` changes — the same path
|
|
29
|
+
* that already picks up `--connect`, a stream being switched off, and a
|
|
30
|
+
* verbosity change. `extra_paths` rides it for free by living inside
|
|
31
|
+
* `Settings`, which is part of the value being compared. That is load-bearing
|
|
32
|
+
* rather than incidental, and `collector_config_change_cycles_the_collector`
|
|
33
|
+
* in `crates/fpai-collect/src/config.rs` pins it: move `sources` out of
|
|
34
|
+
* `CollectorConfig` and the CLI would keep reporting success for paths the
|
|
35
|
+
* running daemon never picks up.
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
import { readConfig, updateConfig } from "./fp-config";
|
|
39
|
+
import { configFile } from "./fp-home";
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Harness keys `[collector.sources.<key>]` accepts.
|
|
43
|
+
*
|
|
44
|
+
* MUST stay in step with `HARNESS_KEYS` in `crates/failproofaid/src/main.rs`,
|
|
45
|
+
* which is the side that actually registers the tasks. A name here that the
|
|
46
|
+
* daemon does not know configures a table nothing reads; a name the daemon
|
|
47
|
+
* knows and this list omits is a path the user cannot add. `__tests__` asserts
|
|
48
|
+
* the two lists are identical by reading the Rust source, because nothing else
|
|
49
|
+
* connects them.
|
|
50
|
+
*
|
|
51
|
+
* Twelve keys, thirteen sources: `claude` covers both the main and the subagent
|
|
52
|
+
* transcript formats, which share a root — an extra path holding Claude
|
|
53
|
+
* transcripts holds their subagents too.
|
|
54
|
+
*/
|
|
55
|
+
export const HARNESS_KEYS = [
|
|
56
|
+
"claude",
|
|
57
|
+
"codex",
|
|
58
|
+
"copilot",
|
|
59
|
+
"openclaw",
|
|
60
|
+
"pi",
|
|
61
|
+
"factory",
|
|
62
|
+
"antigravity",
|
|
63
|
+
"cursor",
|
|
64
|
+
"goose",
|
|
65
|
+
"opencode",
|
|
66
|
+
"devin",
|
|
67
|
+
"hermes",
|
|
68
|
+
] as const;
|
|
69
|
+
|
|
70
|
+
export type HarnessKey = (typeof HARNESS_KEYS)[number];
|
|
71
|
+
|
|
72
|
+
export interface HarnessResult {
|
|
73
|
+
lines: string[];
|
|
74
|
+
exitCode: number;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const ok = (lines: string[]): HarnessResult => ({ lines, exitCode: 0 });
|
|
78
|
+
const fail = (lines: string[]): HarnessResult => ({ lines, exitCode: 1 });
|
|
79
|
+
|
|
80
|
+
function unknownHarness(name: string): HarnessResult {
|
|
81
|
+
return fail([
|
|
82
|
+
`Unknown harness: ${name}`,
|
|
83
|
+
"",
|
|
84
|
+
`Known harnesses: ${HARNESS_KEYS.join(", ")}`,
|
|
85
|
+
]);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** The path half of an entry, for comparing against what a user typed. */
|
|
89
|
+
function pathOf(entry: string): string {
|
|
90
|
+
const eq = entry.indexOf("=");
|
|
91
|
+
if (eq <= 0) return entry.trim();
|
|
92
|
+
const label = entry.slice(0, eq).trim();
|
|
93
|
+
return label === "" ? entry.trim() : entry.slice(eq + 1).trim();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** The label half, or `null` when the entry did not name one. */
|
|
97
|
+
function labelOf(entry: string): string | null {
|
|
98
|
+
const eq = entry.indexOf("=");
|
|
99
|
+
if (eq <= 0) return null;
|
|
100
|
+
const label = entry.slice(0, eq).trim();
|
|
101
|
+
return label === "" ? null : label;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function currentPaths(harness: string): string[] {
|
|
105
|
+
return readConfig().collector.sources?.[harness]?.extraPaths ?? [];
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function writePaths(harness: string, paths: string[]): void {
|
|
109
|
+
const cfg = readConfig();
|
|
110
|
+
const sources = { ...(cfg.collector.sources ?? {}) };
|
|
111
|
+
if (paths.length > 0) sources[harness] = { extraPaths: paths };
|
|
112
|
+
else delete sources[harness];
|
|
113
|
+
updateConfig({
|
|
114
|
+
collector: {
|
|
115
|
+
...cfg.collector,
|
|
116
|
+
// `undefined` rather than `{}` when the last entry goes, so removing what
|
|
117
|
+
// you added leaves the file as it was rather than carrying an empty table
|
|
118
|
+
// forever.
|
|
119
|
+
sources: Object.keys(sources).length > 0 ? sources : undefined,
|
|
120
|
+
},
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* No restart, and deliberately no instruction to perform one.
|
|
126
|
+
*
|
|
127
|
+
* The daemon's collector manager re-reads `config.toml` on an interval and
|
|
128
|
+
* cycles the collector whenever the resolved `CollectorConfig` changes — the
|
|
129
|
+
* same path that already picks up `--connect`, a stream being switched off and
|
|
130
|
+
* a verbosity change. `extra_paths` rides that for free because it lives inside
|
|
131
|
+
* `Settings`, which lives inside the value being compared.
|
|
132
|
+
*
|
|
133
|
+
* Verified live: adding a path to a running daemon logged "collector
|
|
134
|
+
* configuration changed; cycling the collector", took the task count from 21 to
|
|
135
|
+
* 23, and the first transcript written under the new path reached the server.
|
|
136
|
+
*
|
|
137
|
+
* This said "run `sudo systemctl restart failproofaid@$USER`" first. That was
|
|
138
|
+
* wrong twice over: the restart is unnecessary, and it asks for root from a
|
|
139
|
+
* command that writes one file in the user's own home and needs none.
|
|
140
|
+
*/
|
|
141
|
+
const TAKES_EFFECT_HINT = [
|
|
142
|
+
"",
|
|
143
|
+
"The daemon picks this up on its own within a few seconds — no restart, no sudo.",
|
|
144
|
+
];
|
|
145
|
+
|
|
146
|
+
export function addPath(harness: string, entry: string): HarnessResult {
|
|
147
|
+
if (!(HARNESS_KEYS as readonly string[]).includes(harness)) return unknownHarness(harness);
|
|
148
|
+
const trimmed = entry.trim();
|
|
149
|
+
if (trimmed === "") return fail(["A path is required."]);
|
|
150
|
+
if (pathOf(trimmed) === "") {
|
|
151
|
+
return fail([`Could not read a path out of ${JSON.stringify(entry)}.`, "", "Use `<path>` or `<label>=<path>`."]);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const existing = currentPaths(harness);
|
|
155
|
+
|
|
156
|
+
// Exact duplicate: succeed rather than error. `add-path` run twice from a
|
|
157
|
+
// provisioning script must not fail the second time.
|
|
158
|
+
if (existing.includes(trimmed)) {
|
|
159
|
+
return ok([`${harness}: ${trimmed} is already configured.`]);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Same path under a different label, or the same label on a different path.
|
|
163
|
+
// Both are rejected HERE rather than left to the daemon, because the daemon
|
|
164
|
+
// resolves them at startup and drops one — so the CLI would report success
|
|
165
|
+
// for a path that is never captured.
|
|
166
|
+
const samePath = existing.find((e) => pathOf(e) === pathOf(trimmed));
|
|
167
|
+
if (samePath) {
|
|
168
|
+
return fail([
|
|
169
|
+
`${harness} already captures ${pathOf(trimmed)} as ${JSON.stringify(samePath)}.`,
|
|
170
|
+
"",
|
|
171
|
+
"Remove it first if you meant to relabel it:",
|
|
172
|
+
` failproofai harness remove-path ${harness} ${pathOf(trimmed)}`,
|
|
173
|
+
]);
|
|
174
|
+
}
|
|
175
|
+
const wanted = labelOf(trimmed);
|
|
176
|
+
if (wanted) {
|
|
177
|
+
const sameLabel = existing.find((e) => labelOf(e) === wanted);
|
|
178
|
+
if (sameLabel) {
|
|
179
|
+
return fail([
|
|
180
|
+
`${harness} already uses the label ${JSON.stringify(wanted)} for ${pathOf(sameLabel)}.`,
|
|
181
|
+
"",
|
|
182
|
+
"Labels namespace agent ids, so two paths cannot share one.",
|
|
183
|
+
]);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
writePaths(harness, [...existing, trimmed]);
|
|
188
|
+
return ok([
|
|
189
|
+
`${harness}: now also capturing ${pathOf(trimmed)}`,
|
|
190
|
+
wanted
|
|
191
|
+
? ` agent ids will be namespaced ${wanted}-*`
|
|
192
|
+
: " a label will be derived from the folder name; `failproofai harness list` shows it",
|
|
193
|
+
` written to ${configFile()}`,
|
|
194
|
+
...TAKES_EFFECT_HINT,
|
|
195
|
+
]);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export function removePath(harness: string, target: string): HarnessResult {
|
|
199
|
+
if (!(HARNESS_KEYS as readonly string[]).includes(harness)) return unknownHarness(harness);
|
|
200
|
+
const existing = currentPaths(harness);
|
|
201
|
+
const t = target.trim();
|
|
202
|
+
// Match on the whole entry, on its path, or on its label — a user removing
|
|
203
|
+
// something is working from what `list` printed, which shows all three.
|
|
204
|
+
const kept = existing.filter((e) => e !== t && pathOf(e) !== t && labelOf(e) !== t);
|
|
205
|
+
if (kept.length === existing.length) {
|
|
206
|
+
return fail([
|
|
207
|
+
`${harness} has no extra path matching ${JSON.stringify(t)}.`,
|
|
208
|
+
"",
|
|
209
|
+
existing.length > 0
|
|
210
|
+
? `Configured: ${existing.join(", ")}`
|
|
211
|
+
: `${harness} has no extra paths configured.`,
|
|
212
|
+
]);
|
|
213
|
+
}
|
|
214
|
+
writePaths(harness, kept);
|
|
215
|
+
return ok([
|
|
216
|
+
`${harness}: no longer capturing ${t}`,
|
|
217
|
+
"",
|
|
218
|
+
"Already-collected sessions from that path are NOT removed — they are on the",
|
|
219
|
+
"server. This only stops new ones.",
|
|
220
|
+
...TAKES_EFFECT_HINT,
|
|
221
|
+
]);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export function listPaths(harness?: string): HarnessResult {
|
|
225
|
+
if (harness && !(HARNESS_KEYS as readonly string[]).includes(harness)) return unknownHarness(harness);
|
|
226
|
+
const sources = readConfig().collector.sources ?? {};
|
|
227
|
+
const names: string[] = harness
|
|
228
|
+
? [harness]
|
|
229
|
+
: HARNESS_KEYS.filter((k) => sources[k]?.extraPaths?.length);
|
|
230
|
+
|
|
231
|
+
// Computed BEFORE the empty-state return, not after. A config whose only
|
|
232
|
+
// tables are typos has no known harness configured, so the early return fired
|
|
233
|
+
// and the warning below was unreachable in precisely the case it exists for —
|
|
234
|
+
// the user sees "No extra capture paths configured" while their file plainly
|
|
235
|
+
// contains one, which is the most confusing possible answer.
|
|
236
|
+
const unknown = Object.keys(sources).filter(
|
|
237
|
+
(k) => !(HARNESS_KEYS as readonly string[]).includes(k),
|
|
238
|
+
);
|
|
239
|
+
|
|
240
|
+
if (names.length === 0 && unknown.length === 0) {
|
|
241
|
+
return ok([
|
|
242
|
+
"No extra capture paths configured.",
|
|
243
|
+
"",
|
|
244
|
+
"Every harness is watching only its default location. Add one with:",
|
|
245
|
+
" failproofai harness add-path <harness> [<label>=]<path>",
|
|
246
|
+
"",
|
|
247
|
+
`Harnesses: ${HARNESS_KEYS.join(", ")}`,
|
|
248
|
+
]);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const lines: string[] = [];
|
|
252
|
+
for (const name of names) {
|
|
253
|
+
const entries = sources[name]?.extraPaths ?? [];
|
|
254
|
+
if (entries.length === 0) {
|
|
255
|
+
if (harness) lines.push(`${name}: no extra paths configured (default location only)`);
|
|
256
|
+
continue;
|
|
257
|
+
}
|
|
258
|
+
lines.push(`${name}:`);
|
|
259
|
+
for (const e of entries) {
|
|
260
|
+
const label = labelOf(e);
|
|
261
|
+
lines.push(
|
|
262
|
+
label
|
|
263
|
+
? ` ${pathOf(e)} → agent ids ${label}-*`
|
|
264
|
+
: ` ${pathOf(e)} → label derived from the folder name`,
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
// Unknown tables are surfaced here as well as by the daemon: a user who
|
|
269
|
+
// hand-edited config.toml runs `list` to check it, and that is the moment the
|
|
270
|
+
// typo is cheapest to find.
|
|
271
|
+
if (unknown.length > 0) {
|
|
272
|
+
if (lines.length === 0) lines.push("No extra capture paths configured for any known harness.");
|
|
273
|
+
lines.push(
|
|
274
|
+
"",
|
|
275
|
+
`⚠ config.json configures ${unknown.length} unknown harness(es): ${unknown.join(", ")}`,
|
|
276
|
+
" Nothing is captured from them.",
|
|
277
|
+
` Known: ${HARNESS_KEYS.join(", ")}`,
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
return ok(lines);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/** Dispatch for `failproofai harness <sub> ...`. */
|
|
284
|
+
export function runHarnessCommand(argv: string[]): HarnessResult {
|
|
285
|
+
const [sub, ...rest] = argv;
|
|
286
|
+
switch (sub) {
|
|
287
|
+
case "add-path":
|
|
288
|
+
if (rest.length < 2) {
|
|
289
|
+
return fail(["Usage: failproofai harness add-path <harness> [<label>=]<path>"]);
|
|
290
|
+
}
|
|
291
|
+
return addPath(rest[0], rest.slice(1).join(" "));
|
|
292
|
+
case "remove-path":
|
|
293
|
+
if (rest.length < 2) {
|
|
294
|
+
return fail(["Usage: failproofai harness remove-path <harness> <path|label>"]);
|
|
295
|
+
}
|
|
296
|
+
return removePath(rest[0], rest.slice(1).join(" "));
|
|
297
|
+
case "list":
|
|
298
|
+
return listPaths(rest[0]);
|
|
299
|
+
default:
|
|
300
|
+
return fail([
|
|
301
|
+
sub ? `Unknown subcommand: ${sub}` : "A subcommand is required.",
|
|
302
|
+
"",
|
|
303
|
+
"Usage:",
|
|
304
|
+
" failproofai harness list [<harness>]",
|
|
305
|
+
" failproofai harness add-path <harness> [<label>=]<path>",
|
|
306
|
+
" failproofai harness remove-path <harness> <path|label>",
|
|
307
|
+
]);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
@@ -99,16 +99,16 @@ export interface HookActivityEntry {
|
|
|
99
99
|
policySource?: "builtin" | "custom" | "convention" | "cloud";
|
|
100
100
|
/** Cloud policy id of the decider. Present only when `policySource` is "cloud". */
|
|
101
101
|
cloudPolicyId?: string;
|
|
102
|
-
/** Immutable
|
|
103
|
-
|
|
102
|
+
/** Immutable version of that policy — the half of attribution that identifies WHICH version ran. */
|
|
103
|
+
cloudVersion?: number;
|
|
104
104
|
/**
|
|
105
|
-
* The cloud
|
|
105
|
+
* The cloud deployment active when this event was evaluated, recorded on
|
|
106
106
|
* every row of a managed machine regardless of what decided. "What was
|
|
107
107
|
* deployed here" is a different question from "what decided", and only this
|
|
108
108
|
* distinguishes a rollout that changed no outcomes from one that never
|
|
109
109
|
* arrived.
|
|
110
110
|
*/
|
|
111
|
-
|
|
111
|
+
cloudDeployment?: number;
|
|
112
112
|
/**
|
|
113
113
|
* What observe-mode policies WOULD have done, had they been enforcing. This
|
|
114
114
|
* record is the entire point of observe mode: without it the row is
|
|
@@ -117,7 +117,7 @@ export interface HookActivityEntry {
|
|
|
117
117
|
*/
|
|
118
118
|
observed?: Array<{
|
|
119
119
|
policyId: string;
|
|
120
|
-
|
|
120
|
+
version: number;
|
|
121
121
|
decision: "deny" | "instruct";
|
|
122
122
|
reason: string | null;
|
|
123
123
|
}>;
|
package/src/hooks/manager.ts
CHANGED
|
@@ -25,6 +25,7 @@ import { getInstanceId, hashToId } from "../../lib/telemetry-id";
|
|
|
25
25
|
import { CliError } from "../cli-error";
|
|
26
26
|
import { hookLogWarn } from "./hook-logger";
|
|
27
27
|
import { customPoliciesDir, globalPolicyConfigFile } from "./fp-home";
|
|
28
|
+
import { readActiveCloudManagedPolicies } from "./cloud-managed-policies";
|
|
28
29
|
|
|
29
30
|
const VALID_POLICY_NAMES = new Set(BUILTIN_POLICIES.map((p) => p.name));
|
|
30
31
|
|
|
@@ -839,6 +840,40 @@ export async function listHooks(cwd?: string): Promise<void> {
|
|
|
839
840
|
console.log();
|
|
840
841
|
}
|
|
841
842
|
|
|
843
|
+
// Cloud-managed policies. These enforce on this machine exactly like the two
|
|
844
|
+
// sections above, but nothing here listed them — so `failproofai policies`
|
|
845
|
+
// answered "what is enforcing?" with a subset, and the policies an operator
|
|
846
|
+
// pushed to a fleet were the ones invisible to the person running the
|
|
847
|
+
// command on it.
|
|
848
|
+
//
|
|
849
|
+
// Read-only on purpose: these are owned by the deployment, not by local
|
|
850
|
+
// config. `--uninstall <name>` cannot switch one off, and printing them
|
|
851
|
+
// beside toggleable rows without saying so would imply it can.
|
|
852
|
+
try {
|
|
853
|
+
const cloud = readActiveCloudManagedPolicies();
|
|
854
|
+
if (cloud.length > 0) {
|
|
855
|
+
const gen = cloud[0].deployment;
|
|
856
|
+
console.log(
|
|
857
|
+
`\n \u2500\u2500 Cloud-managed \u2014 deployment ${gen} \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500`,
|
|
858
|
+
);
|
|
859
|
+
const colWidth = Math.max(nameColWidth, ...cloud.map((c) => c.id.length + 2));
|
|
860
|
+
for (const artifact of cloud) {
|
|
861
|
+
// `observe` is evaluated and then has its verdict discarded, so a row
|
|
862
|
+
// that read "ON" would claim enforcement this policy deliberately is
|
|
863
|
+
// not doing.
|
|
864
|
+
const status =
|
|
865
|
+
artifact.effect === "observe" ? "\x1B[33m\u25D0 OBS\x1B[0m" : "\x1B[32m\u2713 ON\x1B[0m";
|
|
866
|
+
console.log(` ${status} ${artifact.id.padEnd(colWidth)}v${artifact.version}`);
|
|
867
|
+
}
|
|
868
|
+
console.log("\n Managed from the dashboard \u2014 not switchable with `failproofai policies`.");
|
|
869
|
+
console.log();
|
|
870
|
+
}
|
|
871
|
+
} catch {
|
|
872
|
+
// A machine with no deployment, or an unreadable manifest, simply has no
|
|
873
|
+
// section. The hook path reports its own failures; a listing must not be
|
|
874
|
+
// the thing that turns a bad manifest into a broken command.
|
|
875
|
+
}
|
|
876
|
+
|
|
842
877
|
// Mirror what was just listed into the USER config. Safe here because
|
|
843
878
|
// `failproofai policies` is a one-shot command — never do this on the hook
|
|
844
879
|
// path (see the HooksConfig.conventionPolicies doc comment).
|
|
@@ -29,11 +29,13 @@
|
|
|
29
29
|
* elevate. So each reason carries a cheap local check for whether its blocker
|
|
30
30
|
* is gone, and the wizard offers itself again the moment one is:
|
|
31
31
|
*
|
|
32
|
-
* needs_root
|
|
33
|
-
* daemon_failed
|
|
34
|
-
* cancelled
|
|
35
|
-
*
|
|
36
|
-
*
|
|
32
|
+
* needs_root -> elevation is now possible without a prompt
|
|
33
|
+
* daemon_failed -> the service manager now reports something different
|
|
34
|
+
* cancelled -> the CLI version changed; an upgrade is a fair moment to
|
|
35
|
+
* ask again, and nothing else about a deliberate cancel
|
|
36
|
+
* should re-nag
|
|
37
|
+
* unsupported_platform -> the CLI version changed (a future release might
|
|
38
|
+
* support this platform)
|
|
37
39
|
*
|
|
38
40
|
* Every probe is local and takes milliseconds. None of them runs on the hook
|
|
39
41
|
* path — `--hook` never reaches the first-run gate at all.
|
|
@@ -150,6 +152,11 @@ export function blockerCleared(attempt: OnboardingAttempt, probe: RetryProbe): b
|
|
|
150
152
|
// this module removes — but an upgrade is a new thing to say, so it is
|
|
151
153
|
// allowed to ask once more.
|
|
152
154
|
return probe.cliVersion !== attempt.cliVersion;
|
|
155
|
+
case "unsupported_platform":
|
|
156
|
+
// A permanent property of the machine, not the invocation — nagging every
|
|
157
|
+
// command would just repeat the hard-fail. Only a new release is a reason
|
|
158
|
+
// to ask again (it might add support for this platform).
|
|
159
|
+
return probe.cliVersion !== attempt.cliVersion;
|
|
153
160
|
case "not_a_tty":
|
|
154
161
|
case "running_as_sudo":
|
|
155
162
|
// Both are properties of the invocation, not of the machine, so the next
|
|
@@ -166,14 +173,23 @@ export function attemptHintLines(attempt: OnboardingAttempt): string[] {
|
|
|
166
173
|
needs_root: "it needs root to install the failproofaid service",
|
|
167
174
|
daemon_failed: "the failproofaid service could not be started",
|
|
168
175
|
cancelled: "it was cancelled",
|
|
176
|
+
unsupported_platform: "failproofaid does not run on this platform",
|
|
169
177
|
not_a_tty: "there was no terminal to ask in",
|
|
170
178
|
running_as_sudo: "it was run under sudo",
|
|
171
179
|
};
|
|
172
180
|
const detail = why[attempt.reason] ?? "it did not finish";
|
|
181
|
+
// Every other reason is fixable by the user right now, so the generic
|
|
182
|
+
// retry is correct. unsupported_platform is not — blockerCleared only
|
|
183
|
+
// re-offers it on a CLI version change, and `failproofai config` would
|
|
184
|
+
// hit the exact same hard-fail in the meantime.
|
|
185
|
+
const action =
|
|
186
|
+
attempt.reason === "unsupported_platform"
|
|
187
|
+
? "Check for a failproofai update — this platform may be supported by a newer release."
|
|
188
|
+
: "Run `failproofai config` when you are ready.";
|
|
173
189
|
return [
|
|
174
190
|
``,
|
|
175
191
|
`[failproofai] Setup is not finished — ${detail}.`,
|
|
176
|
-
`
|
|
192
|
+
` ${action}`,
|
|
177
193
|
``,
|
|
178
194
|
];
|
|
179
195
|
}
|
package/src/hooks/tui.ts
CHANGED
|
@@ -34,6 +34,8 @@ export interface SelectChoice<T> {
|
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
export interface SelectOneOptions<T> {
|
|
37
|
+
/** Offer ← to go back a step. The caller must handle `BACK`. */
|
|
38
|
+
allowBack?: boolean;
|
|
37
39
|
message: string;
|
|
38
40
|
choices: SelectChoice<T>[];
|
|
39
41
|
/** Static info lines rendered under the question (e.g. a review summary). */
|
|
@@ -64,6 +66,8 @@ export interface MultiChoice<T> {
|
|
|
64
66
|
}
|
|
65
67
|
|
|
66
68
|
export interface MultiSelectOptions<T> {
|
|
69
|
+
/** Offer ← to go back a step. The caller must handle `BACK`. */
|
|
70
|
+
allowBack?: boolean;
|
|
67
71
|
message: string;
|
|
68
72
|
choices: MultiChoice<T>[];
|
|
69
73
|
minSelected?: number;
|
|
@@ -423,6 +427,8 @@ interface PromptSpec<R> {
|
|
|
423
427
|
renderRow: (index: number, active: boolean, budget: number) => string;
|
|
424
428
|
/** Extra line(s) above the footer (e.g. a min-selected warning). */
|
|
425
429
|
warnLine?: () => string | null;
|
|
430
|
+
/** When set, ← resolves `BACK` so the caller can step backwards. */
|
|
431
|
+
allowBack?: boolean;
|
|
426
432
|
footer: string;
|
|
427
433
|
/** Handle non-navigation keys. `{done}` finishes, `"redraw"` repaints. */
|
|
428
434
|
onKey: (key: readline.Key, cursor: number) => { done: R } | "redraw" | undefined;
|
|
@@ -469,10 +475,21 @@ function runPrompt<R>(p: PromptSpec<R>): Promise<R | null> {
|
|
|
469
475
|
};
|
|
470
476
|
|
|
471
477
|
const collapse = (result: R | null): void => {
|
|
478
|
+
// BACK is handled HERE, not in each `summaryFor`, because it is not a value
|
|
479
|
+
// of `R` at all — it is a sentinel the shared key handler injects, so every
|
|
480
|
+
// prompt would otherwise have to know about a symbol it never declared.
|
|
481
|
+
//
|
|
482
|
+
// Both existing callers got it wrong in different ways, and one of them
|
|
483
|
+
// hung: `multiSelect`'s summary calls `values.includes(...)`, which throws
|
|
484
|
+
// `TypeError` on a symbol — and it throws INSIDE `finish`, before
|
|
485
|
+
// `resolve(result)`, so pressing ← never settled the promise and the wizard
|
|
486
|
+
// stopped responding entirely. `selectOne` fell through to `String(value)`
|
|
487
|
+
// and rendered the literal text `Symbol(failproofai.back)`.
|
|
488
|
+
const summary = (result as unknown) === BACK ? "back" : p.summaryFor(result);
|
|
472
489
|
repaint(stdout, region, [
|
|
473
490
|
c.dim(BAR),
|
|
474
491
|
`${c.dim(STEP_DONE)} ${p.message}`,
|
|
475
|
-
`${c.dim(BAR)} ${c.dim(
|
|
492
|
+
`${c.dim(BAR)} ${c.dim(summary)}`,
|
|
476
493
|
]);
|
|
477
494
|
};
|
|
478
495
|
|
|
@@ -500,6 +517,10 @@ function runPrompt<R>(p: PromptSpec<R>): Promise<R | null> {
|
|
|
500
517
|
if (!key) return;
|
|
501
518
|
if ((key.ctrl && (key.name === "c" || key.name === "d")) || key.name === "escape") {
|
|
502
519
|
finish(null);
|
|
520
|
+
} else if (key.name === "left" && p.allowBack) {
|
|
521
|
+
// Only when the caller opted in. A prompt with nowhere to go back TO
|
|
522
|
+
// must not appear to offer it.
|
|
523
|
+
finish(BACK as unknown as never);
|
|
503
524
|
} else if (key.name === "up") {
|
|
504
525
|
cursor = cursor > 0 ? cursor - 1 : choices.length - 1;
|
|
505
526
|
repaint(stdout, region, build());
|
|
@@ -519,7 +540,24 @@ function runPrompt<R>(p: PromptSpec<R>): Promise<R | null> {
|
|
|
519
540
|
|
|
520
541
|
// ── selectOne (radio) ─────────────────────────────────────────────────────────
|
|
521
542
|
|
|
522
|
-
|
|
543
|
+
/**
|
|
544
|
+
* Returned by a prompt when the user asked to go BACK a step, as distinct from
|
|
545
|
+
* cancelling. Both used to be `null`, which made "I picked the wrong scope" and
|
|
546
|
+
* "I want out" the same keystroke — so the only way to change an earlier answer
|
|
547
|
+
* was to abandon setup and start over.
|
|
548
|
+
*
|
|
549
|
+
* A symbol rather than a sentinel string because a caller's value type is its
|
|
550
|
+
* own: `selectOne<string>` could legitimately have "back" as a real choice.
|
|
551
|
+
*/
|
|
552
|
+
export const BACK: unique symbol = Symbol("failproofai.back");
|
|
553
|
+
export type Back = typeof BACK;
|
|
554
|
+
|
|
555
|
+
// Overloaded so `BACK` appears in the return type ONLY where it was asked for.
|
|
556
|
+
// Widening every caller to `T | Back | null` would make dozens of call sites
|
|
557
|
+
// handle a value they can never receive.
|
|
558
|
+
export function selectOne<T>(opts: SelectOneOptions<T> & { allowBack: true }): Promise<T | Back | null>;
|
|
559
|
+
export function selectOne<T>(opts: SelectOneOptions<T>): Promise<T | null>;
|
|
560
|
+
export function selectOne<T>(opts: SelectOneOptions<T>): Promise<T | Back | null> {
|
|
523
561
|
const stdin: TTYIn = opts.stdin ?? process.stdin;
|
|
524
562
|
const stdout: TTYOut = opts.stdout ?? process.stdout;
|
|
525
563
|
const choices = opts.choices;
|
|
@@ -550,7 +588,10 @@ export function selectOne<T>(opts: SelectOneOptions<T>): Promise<T | null> {
|
|
|
550
588
|
const hint = choice.hint ? ` ${c.dim(ellipsize(choice.hint, budget))}` : "";
|
|
551
589
|
return `${dot} ${label}${hint}`;
|
|
552
590
|
},
|
|
553
|
-
|
|
591
|
+
allowBack: opts.allowBack,
|
|
592
|
+
footer: opts.allowBack
|
|
593
|
+
? "↑/↓ navigate · enter to select · ← back · esc to cancel"
|
|
594
|
+
: "↑/↓ navigate · enter to select · esc to cancel",
|
|
554
595
|
onKey: (key, cursor) =>
|
|
555
596
|
key.name === "return" ? { done: choices[cursor].value } : undefined,
|
|
556
597
|
summaryFor: (value) =>
|
|
@@ -562,7 +603,9 @@ export function selectOne<T>(opts: SelectOneOptions<T>): Promise<T | null> {
|
|
|
562
603
|
|
|
563
604
|
// ── multiSelect (checklist) ────────────────────────────────────────────────────
|
|
564
605
|
|
|
565
|
-
export function multiSelect<T>(opts: MultiSelectOptions<T>): Promise<T[] | null
|
|
606
|
+
export function multiSelect<T>(opts: MultiSelectOptions<T> & { allowBack: true }): Promise<T[] | null | Back>;
|
|
607
|
+
export function multiSelect<T>(opts: MultiSelectOptions<T>): Promise<T[] | null>;
|
|
608
|
+
export function multiSelect<T>(opts: MultiSelectOptions<T>): Promise<T[] | null | Back> {
|
|
566
609
|
const stdin: TTYIn = opts.stdin ?? process.stdin;
|
|
567
610
|
const stdout: TTYOut = opts.stdout ?? process.stdout;
|
|
568
611
|
const choices = opts.choices;
|
|
@@ -604,7 +647,12 @@ export function multiSelect<T>(opts: MultiSelectOptions<T>): Promise<T[] | null>
|
|
|
604
647
|
return `${caret} ${box} ${label}${hint}`;
|
|
605
648
|
},
|
|
606
649
|
warnLine: () => (warn ? c.warn(`Select at least ${minSelected}.`) : null),
|
|
607
|
-
|
|
650
|
+
allowBack: opts.allowBack,
|
|
651
|
+
footer:
|
|
652
|
+
opts.hint ??
|
|
653
|
+
(opts.allowBack
|
|
654
|
+
? "↑/↓ move · space select · ctrl+a all · ← back · enter confirm"
|
|
655
|
+
: "↑/↓ move · space select · ctrl+a all · enter confirm"),
|
|
608
656
|
onKey: (key, cursor) => {
|
|
609
657
|
if (key.name === "space") {
|
|
610
658
|
if (choices[cursor]?.locked) return "redraw"; // always on — not a choice
|
|
@@ -705,10 +753,19 @@ export function promptText(opts: PromptTextOptions): Promise<string | null> {
|
|
|
705
753
|
return new Promise((resolve) => {
|
|
706
754
|
let value = "";
|
|
707
755
|
const draw = (error?: string) => {
|
|
756
|
+
const cols = stdout.columns || 80;
|
|
708
757
|
const shown = opts.mask ? "•".repeat(value.length) : value;
|
|
709
758
|
const hint = opts.hint ? ` ${c.dim(opts.hint)}` : "";
|
|
710
|
-
|
|
711
|
-
|
|
759
|
+
// Truncate to ONE physical row. `\r\x1b[2K` erases the row the cursor is
|
|
760
|
+
// on and nothing above it — so a line wider than the terminal wraps, the
|
|
761
|
+
// erase reaches only its last row, and every keystroke leaves the earlier
|
|
762
|
+
// rows behind. That is why pasting a 40-character API key printed 40
|
|
763
|
+
// stacked copies of the prompt: `API key for <host>` plus the masked
|
|
764
|
+
// value plus the `needs events:add · policies:pull …` hint is past 80
|
|
765
|
+
// columns before the key is even half typed.
|
|
766
|
+
const line = truncate(`${c.bold(opts.message)} ${shown}${hint}`, cols - 1);
|
|
767
|
+
const err = error ? `\n ${truncate(c.warn(error), cols - 3)}` : "";
|
|
768
|
+
stdout.write(`\r\x1b[2K${line}${err}`);
|
|
712
769
|
if (err) stdout.write("\x1b[1A");
|
|
713
770
|
};
|
|
714
771
|
draw();
|
|
@@ -229,7 +229,7 @@ export async function runUninstallCommand(opts: UninstallOptions = {}): Promise<
|
|
|
229
229
|
purged: false,
|
|
230
230
|
lines: [
|
|
231
231
|
...lines,
|
|
232
|
-
`✗ could not update ${failproofaiHome()}/config.
|
|
232
|
+
`✗ could not update ${failproofaiHome()}/config.json: ${err instanceof Error ? err.message : String(err)}`,
|
|
233
233
|
``,
|
|
234
234
|
`Stopped before touching the service. Removing it while this machine still`,
|
|
235
235
|
`requires it would deny every tool call. Fix the file's permissions and re-run.`,
|