@pellux/goodvibes-tui 1.24.2 → 1.26.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/CHANGELOG.md +32 -0
- package/README.md +1 -1
- package/bin/goodvibes-daemon +24 -0
- package/docs/foundation-artifacts/operator-contract.json +18234 -12253
- package/package.json +8 -7
- package/src/cli/help.ts +10 -0
- package/src/cli/redaction.ts +68 -3
- package/src/config/goodvibes-home.ts +149 -0
- package/src/config/secret-config.ts +64 -8
- package/src/core/turn-event-wiring.ts +7 -16
- package/src/core/workstream-notification.ts +69 -0
- package/src/daemon/cli.ts +62 -7
- package/src/daemon/handlers/credentials.ts +15 -5
- package/src/daemon/handlers/index.ts +7 -6
- package/src/daemon/send/channels.ts +200 -0
- package/src/daemon/send/command.ts +332 -0
- package/src/daemon/send/composition.ts +99 -0
- package/src/daemon/send/failure-text.ts +93 -0
- package/src/daemon/send/inert-text.ts +225 -0
- package/src/daemon/send/stdin.ts +24 -0
- package/src/input/commands/calendar-runtime.ts +245 -0
- package/src/input/commands/config.ts +68 -2
- package/src/input/commands/connection-status.ts +258 -0
- package/src/input/commands/local-provider-runtime.ts +9 -1
- package/src/input/commands/mail-runtime.ts +267 -0
- package/src/input/commands/operator-runtime.ts +6 -1
- package/src/input/commands/payment-card-intake.ts +200 -0
- package/src/input/commands/profile-render.ts +250 -0
- package/src/input/commands/profile-runtime.ts +362 -0
- package/src/input/commands/profile-types.ts +381 -0
- package/src/input/commands/review-runtime.ts +13 -1
- package/src/input/commands.ts +11 -0
- package/src/input/composer-editor.ts +4 -0
- package/src/input/onboarding/onboarding-wizard-apply.ts +13 -1
- package/src/input/payments-config.ts +167 -0
- package/src/input/provider-key-intake.ts +10 -2
- package/src/input/settings-modal-commit.ts +146 -0
- package/src/input/settings-modal-connections.ts +102 -0
- package/src/input/settings-modal-data.ts +30 -0
- package/src/input/settings-modal-reset.ts +6 -2
- package/src/input/settings-modal-secrets.ts +9 -2
- package/src/input/settings-modal-types.ts +19 -3
- package/src/input/settings-modal.ts +54 -107
- package/src/main.ts +7 -2
- package/src/panels/modals/marketplace-modal.ts +5 -0
- package/src/panels/modals/skills-modal.ts +5 -0
- package/src/renderer/settings-modal-connections.ts +63 -0
- package/src/renderer/settings-modal-helpers.ts +6 -0
- package/src/renderer/settings-modal.ts +21 -1
- package/src/runtime/bootstrap-core.ts +1 -1
- package/src/runtime/bootstrap.ts +2 -2
- package/src/runtime/context.ts +9 -0
- package/src/runtime/credential-composition.ts +54 -0
- package/src/runtime/daemon-handler-composition.ts +0 -4
- package/src/runtime/install-self-check-startup.ts +23 -0
- package/src/runtime/mail-composition.ts +65 -0
- package/src/runtime/onboarding/types.ts +6 -1
- package/src/runtime/path-shadow-startup.ts +243 -0
- package/src/runtime/path-shadow.ts +418 -0
- package/src/runtime/reachability-notice.ts +100 -0
- package/src/runtime/runtime-services-types.ts +243 -0
- package/src/runtime/services.ts +30 -224
- package/src/shell/ui-openers.ts +7 -0
- package/src/verification/verification-ledger.ts +158 -0
- package/src/version.ts +1 -1
- package/src/daemon/handlers/calendar/caldav-client.ts +0 -662
- package/src/daemon/handlers/calendar/ics.ts +0 -556
- package/src/daemon/handlers/calendar/index.ts +0 -316
- package/src/daemon/handlers/email/config.ts +0 -164
- package/src/daemon/handlers/email/imap-connector.ts +0 -441
- package/src/daemon/handlers/email/imap-parsing.ts +0 -499
- package/src/daemon/handlers/email/index.ts +0 -43
- package/src/daemon/handlers/email/read-handlers.ts +0 -80
- package/src/daemon/handlers/email/runtime.ts +0 -140
- package/src/daemon/handlers/email/smtp-connector.ts +0 -557
- package/src/daemon/handlers/email/validation.ts +0 -147
- package/src/daemon/handlers/email/write-handlers.ts +0 -133
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* mail-composition.ts
|
|
3
|
+
*
|
|
4
|
+
* The deps that let the platform register this daemon's `email.*` verbs.
|
|
5
|
+
*
|
|
6
|
+
* `calendar.*` and `email.*` are served by the platform now, not by handlers in
|
|
7
|
+
* this repository — those were deleted when the platform gained an
|
|
8
|
+
* implementation the daemon could call. What is left for a product to supply is
|
|
9
|
+
* the wiring, and it is not optional: without `homeDirectory` the calendar
|
|
10
|
+
* composition returns null, without these deps the mail one does, and either way
|
|
11
|
+
* the verbs stay cataloged-but-unhandled while every surface reports the daemon
|
|
12
|
+
* unreachable on a daemon that is working perfectly.
|
|
13
|
+
*
|
|
14
|
+
* Extracted from services.ts rather than living there, because that file sits at
|
|
15
|
+
* the 800-line architecture cap and a composition step is exactly the kind of
|
|
16
|
+
* thing the cap exists to push into a module of its own.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import {
|
|
20
|
+
withSurfaceEmailConfig,
|
|
21
|
+
describeSurfaceEmailConfigProblem,
|
|
22
|
+
describeSenderClaimNeutrally,
|
|
23
|
+
type EmailServiceDeps,
|
|
24
|
+
type SurfaceEmailConfigProblem,
|
|
25
|
+
} from '@pellux/goodvibes-sdk/platform/email';
|
|
26
|
+
import { nodeEmailTransport } from '@pellux/goodvibes-sdk/platform/email/node';
|
|
27
|
+
|
|
28
|
+
/** The narrow slices this composition needs; the real managers satisfy them. */
|
|
29
|
+
interface MailCompositionInput {
|
|
30
|
+
readonly configManager: { get(key: never): unknown };
|
|
31
|
+
readonly secretsManager: { get(key: string): Promise<string | null> };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Build the mail deps and the not-configured describer that go to
|
|
36
|
+
* `registerGatewayVerbGroups`.
|
|
37
|
+
*
|
|
38
|
+
* The settings come from the daemon's own `surfaces.email.*` keys through
|
|
39
|
+
* `withSurfaceEmailConfig`, so the keys an operator has already set — and that
|
|
40
|
+
* the settings modal now shows — keep working unchanged, and a not-configured
|
|
41
|
+
* answer names the keys THIS operator actually has rather than the ones the
|
|
42
|
+
* service validates internally.
|
|
43
|
+
*/
|
|
44
|
+
export function composeMailDeps(input: MailCompositionInput): {
|
|
45
|
+
readonly emailServiceDeps: EmailServiceDeps;
|
|
46
|
+
readonly describeEmailConfigProblem: () => Promise<SurfaceEmailConfigProblem | null>;
|
|
47
|
+
} {
|
|
48
|
+
const getConfig = (key: string): unknown => input.configManager.get(key as never);
|
|
49
|
+
const emailServiceDeps = withSurfaceEmailConfig({
|
|
50
|
+
getConfig,
|
|
51
|
+
secretsManager: input.secretsManager,
|
|
52
|
+
transport: nodeEmailTransport,
|
|
53
|
+
// The daemon has no wording of its own for a sender line, so it takes the
|
|
54
|
+
// platform's rather than growing a second implementation of a rule that is
|
|
55
|
+
// security-relevant: a From: header is a claim, sender authentication
|
|
56
|
+
// raises display confidence only, and commandAuthority is the literal
|
|
57
|
+
// 'none'.
|
|
58
|
+
describeSenderClaim: describeSenderClaimNeutrally,
|
|
59
|
+
} as never);
|
|
60
|
+
return {
|
|
61
|
+
emailServiceDeps,
|
|
62
|
+
describeEmailConfigProblem: () =>
|
|
63
|
+
describeSurfaceEmailConfigProblem(getConfig, input.secretsManager),
|
|
64
|
+
};
|
|
65
|
+
}
|
|
@@ -281,7 +281,12 @@ export type OnboardingApplyOperation =
|
|
|
281
281
|
readonly kind: 'set-secret';
|
|
282
282
|
readonly key: string;
|
|
283
283
|
readonly value: string;
|
|
284
|
-
|
|
284
|
+
/**
|
|
285
|
+
* Which secret tier the value is filed in. 'daemon' is the tier the
|
|
286
|
+
* daemon reads with every surface closed — see
|
|
287
|
+
* config/secret-config.ts#defaultSecretBackedScope.
|
|
288
|
+
*/
|
|
289
|
+
readonly scope?: 'project' | 'user' | 'daemon';
|
|
285
290
|
readonly medium: 'secure' | 'plaintext';
|
|
286
291
|
}
|
|
287
292
|
| {
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* path-shadow-startup.ts — the boot-time surfacing of "is this the build you
|
|
3
|
+
* are actually reaching, and is it the current one".
|
|
4
|
+
*
|
|
5
|
+
* It wires the real host inputs (this process's executable, the real PATH,
|
|
6
|
+
* existsSync/realpathSync, and a bounded `<path> --version` spawn) into the
|
|
7
|
+
* pure scan in path-shadow.ts and the pure wording in reachability-notice.ts,
|
|
8
|
+
* then prints the result through the same SystemMessageRouter the rest of
|
|
9
|
+
* startup uses, at high priority, before the session gets going.
|
|
10
|
+
*
|
|
11
|
+
* Cost discipline, because this runs on every start:
|
|
12
|
+
* - the first scan is existence-only: no process is spawned while there is
|
|
13
|
+
* nothing to report, which is the overwhelmingly common case;
|
|
14
|
+
* - versions are probed only after a shadow has already been found, and
|
|
15
|
+
* only with `--version`, bounded by a short timeout;
|
|
16
|
+
* - the latest-release lookup only happens when it can actually change what
|
|
17
|
+
* the user should do: a package-managed or source install (which will
|
|
18
|
+
* never swap itself), or an install that has just been found unreachable.
|
|
19
|
+
* A healthy binary install has already been brought to the latest release
|
|
20
|
+
* by the launch auto-updater, so asking again would be a network round
|
|
21
|
+
* trip that can only confirm what just happened.
|
|
22
|
+
*
|
|
23
|
+
* Every failure is swallowed. A reachability check must never block or crash
|
|
24
|
+
* boot.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import { spawnSync } from 'node:child_process';
|
|
28
|
+
import { realpathSync, statSync } from 'node:fs';
|
|
29
|
+
import { basename, dirname, join } from 'node:path';
|
|
30
|
+
import { detectInstallKind, fallbackUpdateCommand, type UpdateFetchLike } from './update-check.ts';
|
|
31
|
+
import { checkForUpdate } from '../input/commands/update-runtime.ts';
|
|
32
|
+
import {
|
|
33
|
+
scanCommandShadows,
|
|
34
|
+
splitPathEntries,
|
|
35
|
+
type ShadowScanResult,
|
|
36
|
+
} from './path-shadow.ts';
|
|
37
|
+
import { buildReachabilityNotices, type ReachabilityNotice } from './reachability-notice.ts';
|
|
38
|
+
import { VERSION } from '../version.ts';
|
|
39
|
+
import type { SystemMessageRouter } from '../core/system-message-router.ts';
|
|
40
|
+
|
|
41
|
+
/** The commands a goodvibes install places side by side in one directory. */
|
|
42
|
+
export const INSTALLED_COMMANDS = ['goodvibes', 'goodvibes-daemon', 'goodvibes-agent'] as const;
|
|
43
|
+
|
|
44
|
+
/** How long a `<path> --version` probe may take before it is abandoned. */
|
|
45
|
+
const VERSION_PROBE_TIMEOUT_MS = 3000;
|
|
46
|
+
/** How long the latest-release lookup may take before startup moves on without it. */
|
|
47
|
+
const LATEST_LOOKUP_TIMEOUT_MS = 2500;
|
|
48
|
+
|
|
49
|
+
function safeRealPath(path: string): string {
|
|
50
|
+
try {
|
|
51
|
+
return realpathSync(path);
|
|
52
|
+
} catch {
|
|
53
|
+
return path;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function isExecutableFile(path: string): boolean {
|
|
58
|
+
try {
|
|
59
|
+
const stats = statSync(path);
|
|
60
|
+
if (!stats.isFile()) return false;
|
|
61
|
+
// Any execute bit is enough: the shell searches for an executable file,
|
|
62
|
+
// and which bit applies depends on ownership we are not going to compute.
|
|
63
|
+
return (stats.mode & 0o111) !== 0;
|
|
64
|
+
} catch {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Runs `<path> --version` and returns its first line. Never inherits stdio, so
|
|
71
|
+
* a binary that tries to draw a terminal cannot disturb this one.
|
|
72
|
+
*/
|
|
73
|
+
export function probeVersionLine(path: string): string | undefined {
|
|
74
|
+
try {
|
|
75
|
+
const result = spawnSync(path, ['--version'], {
|
|
76
|
+
timeout: VERSION_PROBE_TIMEOUT_MS,
|
|
77
|
+
encoding: 'utf8',
|
|
78
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
79
|
+
windowsHide: true,
|
|
80
|
+
});
|
|
81
|
+
if (result.error || typeof result.stdout !== 'string') return undefined;
|
|
82
|
+
return result.stdout.split('\n')[0];
|
|
83
|
+
} catch {
|
|
84
|
+
return undefined;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* The PATH directory that provides THIS running executable, which is the
|
|
90
|
+
* position everything else is measured against.
|
|
91
|
+
*
|
|
92
|
+
* Preferred answer: the first PATH entry whose `<dir>/<command>` resolves,
|
|
93
|
+
* through symlinks, to this same file. That is exactly "where the shell would
|
|
94
|
+
* find me", and it is right for a linked package install as well as a
|
|
95
|
+
* standalone binary.
|
|
96
|
+
*
|
|
97
|
+
* Fallback, for a standalone binary only: the directory the executable sits
|
|
98
|
+
* in. That directory being absent from PATH is itself worth reporting — an
|
|
99
|
+
* installed binary nobody can reach by name. For a package-managed install
|
|
100
|
+
* the executable lives inside node_modules, which is never on PATH and never
|
|
101
|
+
* meant to be, so there is nothing honest to say and the check stays silent.
|
|
102
|
+
*/
|
|
103
|
+
export function resolveSelfDirectory(input: {
|
|
104
|
+
readonly execPath: string;
|
|
105
|
+
readonly command: string;
|
|
106
|
+
readonly pathEntries: readonly string[];
|
|
107
|
+
readonly realPath: (path: string) => string;
|
|
108
|
+
readonly isExecutableFile: (path: string) => boolean;
|
|
109
|
+
}): string | undefined {
|
|
110
|
+
const self = input.realPath(input.execPath);
|
|
111
|
+
for (const directory of input.pathEntries) {
|
|
112
|
+
const candidate = join(directory, input.command);
|
|
113
|
+
if (!input.isExecutableFile(candidate)) continue;
|
|
114
|
+
if (input.realPath(candidate) === self) return directory;
|
|
115
|
+
}
|
|
116
|
+
if (detectInstallKind(input.execPath) === 'binary') return dirname(self);
|
|
117
|
+
return undefined;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Which of the installed commands actually sit in this directory. */
|
|
121
|
+
function commandsPresentIn(directory: string, exists: (path: string) => boolean): string[] {
|
|
122
|
+
return INSTALLED_COMMANDS.filter((command) => exists(join(directory, command)));
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export interface ReachabilityCheckResult {
|
|
126
|
+
readonly notices: readonly ReachabilityNotice[];
|
|
127
|
+
readonly scan?: ShadowScanResult | undefined;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* The whole check, with the network lookup injected so tests never reach it.
|
|
132
|
+
* Returns the notices to print; an empty list is the healthy case.
|
|
133
|
+
*/
|
|
134
|
+
export async function runReachabilityCheck(input: {
|
|
135
|
+
readonly execPath: string;
|
|
136
|
+
readonly pathValue: string | undefined;
|
|
137
|
+
readonly homeDir: string;
|
|
138
|
+
readonly runningVersion: string;
|
|
139
|
+
readonly commandName?: string | undefined;
|
|
140
|
+
/** Resolves the newest released version, or undefined when it cannot be determined. */
|
|
141
|
+
readonly resolveLatest: () => Promise<string | undefined>;
|
|
142
|
+
// The host touches, injectable so a test drives a whole scenario with fake
|
|
143
|
+
// paths and never spawns anything. Production passes none of these.
|
|
144
|
+
readonly isExecutableFile?: ((path: string) => boolean) | undefined;
|
|
145
|
+
readonly realPath?: ((path: string) => string) | undefined;
|
|
146
|
+
readonly probeVersion?: ((path: string) => string | undefined) | undefined;
|
|
147
|
+
}): Promise<ReachabilityCheckResult> {
|
|
148
|
+
const command = input.commandName ?? basename(input.execPath);
|
|
149
|
+
const pathEntries = splitPathEntries(input.pathValue);
|
|
150
|
+
const installKind = detectInstallKind(input.execPath);
|
|
151
|
+
const fileIsExecutable = input.isExecutableFile ?? isExecutableFile;
|
|
152
|
+
const resolvePath = input.realPath ?? safeRealPath;
|
|
153
|
+
const versionProbe = input.probeVersion ?? probeVersionLine;
|
|
154
|
+
|
|
155
|
+
// A source checkout is not an install: there is no maintained copy to be
|
|
156
|
+
// shadowed and no release to be behind.
|
|
157
|
+
if (installKind === 'source') return { notices: [] };
|
|
158
|
+
|
|
159
|
+
const selfDirectory = resolveSelfDirectory({
|
|
160
|
+
execPath: input.execPath,
|
|
161
|
+
command,
|
|
162
|
+
pathEntries,
|
|
163
|
+
realPath: resolvePath,
|
|
164
|
+
isExecutableFile: fileIsExecutable,
|
|
165
|
+
});
|
|
166
|
+
if (!selfDirectory) return { notices: [] };
|
|
167
|
+
|
|
168
|
+
const commands = commandsPresentIn(selfDirectory, fileIsExecutable);
|
|
169
|
+
const base = {
|
|
170
|
+
commands: commands.length > 0 ? commands : [command],
|
|
171
|
+
installDir: selfDirectory,
|
|
172
|
+
pathEntries,
|
|
173
|
+
homeDir: input.homeDir,
|
|
174
|
+
isExecutableFile: fileIsExecutable,
|
|
175
|
+
realPath: resolvePath,
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
// Existence-only first: nothing is spawned while there is nothing to report.
|
|
179
|
+
const cheapScan = scanCommandShadows(base);
|
|
180
|
+
const scan = cheapScan.hasProblem
|
|
181
|
+
? scanCommandShadows({ ...base, probeVersion: versionProbe })
|
|
182
|
+
: cheapScan;
|
|
183
|
+
|
|
184
|
+
const latestVersion = scan.hasProblem || installKind !== 'binary'
|
|
185
|
+
? await input.resolveLatest()
|
|
186
|
+
: undefined;
|
|
187
|
+
|
|
188
|
+
return {
|
|
189
|
+
scan,
|
|
190
|
+
notices: buildReachabilityNotices({
|
|
191
|
+
scan,
|
|
192
|
+
runningVersion: input.runningVersion,
|
|
193
|
+
latestVersion,
|
|
194
|
+
updateCommand: installKind === 'binary'
|
|
195
|
+
? 'curl -fsSL https://goodvibes.sh/install.sh | sh'
|
|
196
|
+
: fallbackUpdateCommand(installKind),
|
|
197
|
+
}),
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** The real latest-release lookup, bounded so a slow network cannot hold up boot. */
|
|
202
|
+
async function resolveLatestReleaseBounded(): Promise<string | undefined> {
|
|
203
|
+
try {
|
|
204
|
+
// The same release lookup /update uses — there is deliberately no second
|
|
205
|
+
// source of truth for what the latest version is.
|
|
206
|
+
const lookup = checkForUpdate(fetch as UpdateFetchLike, VERSION).then((result) => result.latestTag);
|
|
207
|
+
const timed = await Promise.race([
|
|
208
|
+
lookup,
|
|
209
|
+
new Promise<undefined>((resolve) => {
|
|
210
|
+
const timer = setTimeout(() => resolve(undefined), LATEST_LOOKUP_TIMEOUT_MS);
|
|
211
|
+
timer.unref?.();
|
|
212
|
+
}),
|
|
213
|
+
]);
|
|
214
|
+
return timed ?? undefined;
|
|
215
|
+
} catch {
|
|
216
|
+
return undefined;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Boot-time wiring. Prints one high-priority system message per line so the
|
|
222
|
+
* report reads as prose rather than a single wrapped paragraph, and swallows
|
|
223
|
+
* everything.
|
|
224
|
+
*/
|
|
225
|
+
export async function announceReachability(router: SystemMessageRouter): Promise<void> {
|
|
226
|
+
try {
|
|
227
|
+
const result = await runReachabilityCheck({
|
|
228
|
+
execPath: process.execPath,
|
|
229
|
+
pathValue: process.env['PATH'],
|
|
230
|
+
homeDir: process.env['HOME'] ?? '',
|
|
231
|
+
runningVersion: VERSION,
|
|
232
|
+
commandName: 'goodvibes',
|
|
233
|
+
resolveLatest: resolveLatestReleaseBounded,
|
|
234
|
+
});
|
|
235
|
+
for (const notice of result.notices) {
|
|
236
|
+
for (const line of notice.lines) {
|
|
237
|
+
router.high(`[Install] ${line}`);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
} catch {
|
|
241
|
+
// Best-effort — a reachability check must never block or crash boot.
|
|
242
|
+
}
|
|
243
|
+
}
|