@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,381 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* profile-types.ts
|
|
3
|
+
*
|
|
4
|
+
* Response shapes for the owner-profile control-plane verbs (`profile.*`), and
|
|
5
|
+
* the runtime checks that turn an untyped daemon response into one of them.
|
|
6
|
+
*
|
|
7
|
+
* ## The shapes come from the contract, not from a copy of it
|
|
8
|
+
*
|
|
9
|
+
* Every type below is derived from `OperatorMethodOutput<'profile.…'>` — the
|
|
10
|
+
* types generated from the SDK's own output schemas
|
|
11
|
+
* (`platform/control-plane/method-catalog-owner-profile.ts`). Nothing here
|
|
12
|
+
* restates a field name, so this file cannot drift from the contract: if a
|
|
13
|
+
* verb's shape changes, the checkers stop compiling against it.
|
|
14
|
+
*
|
|
15
|
+
* ## Why the responses are still checked at runtime
|
|
16
|
+
*
|
|
17
|
+
* A generated type is a compile-time claim about what the wire *should* carry,
|
|
18
|
+
* and nothing enforces it at runtime here. The operator client's
|
|
19
|
+
* `validateResponses` defaults on, but it only validates method ids with a
|
|
20
|
+
* matching exported Zod schema and `buildSchemaRegistry` silently skips the
|
|
21
|
+
* rest. Measured against the installed contracts package: 5 of 452 method ids
|
|
22
|
+
* have one, and no `profile.*` verb is among them. So response validation is
|
|
23
|
+
* not a safety net this command can lean on — for these verbs or for most
|
|
24
|
+
* others.
|
|
25
|
+
*
|
|
26
|
+
* So a daemon on a different version — or a 200 from something that is not this
|
|
27
|
+
* daemon at all — would otherwise reach a renderer and throw on a missing array,
|
|
28
|
+
* which in a slash command means a stack trace instead of an answer. Each `toX`
|
|
29
|
+
* function does the cast to `Record<string, unknown>` in ONE place, checks every
|
|
30
|
+
* property a renderer will read, and returns {@link MALFORMED} when anything is
|
|
31
|
+
* off. The command then prints an honest "this build does not recognise that
|
|
32
|
+
* response" line.
|
|
33
|
+
*
|
|
34
|
+
* A malformed *member* fails the whole response rather than being filtered out.
|
|
35
|
+
* Silently dropping a section or a line the checker did not recognise would be
|
|
36
|
+
* the same failure §4.4 forbids in the parser: content disappearing without
|
|
37
|
+
* anyone being told.
|
|
38
|
+
*/
|
|
39
|
+
import type { OperatorMethodInput, OperatorMethodOutput } from '@pellux/goodvibes-sdk';
|
|
40
|
+
|
|
41
|
+
/** Returned by every checker when the response does not match the contract. */
|
|
42
|
+
export const MALFORMED: unique symbol = Symbol('malformed-profile-response');
|
|
43
|
+
export type Checked<T> = T | typeof MALFORMED;
|
|
44
|
+
|
|
45
|
+
/** The verbs this command calls. Typing the ids is what types the inputs. */
|
|
46
|
+
export type ProfileReadVerb = 'profile.read' | 'profile.provenance' | 'profile.status';
|
|
47
|
+
export type ProfileWriteVerb = 'profile.set' | 'profile.append' | 'profile.forget' | 'profile.undo';
|
|
48
|
+
export type ProfileVerb = ProfileReadVerb | ProfileWriteVerb;
|
|
49
|
+
|
|
50
|
+
/** The contract's own input shape for a verb, so a wrong field name will not compile. */
|
|
51
|
+
export type ProfileInput<TVerb extends ProfileVerb> = OperatorMethodInput<TVerb>;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Every key of every member of a union. `keyof` on a union yields only the keys
|
|
55
|
+
* the members SHARE, so a union target would be checked against that
|
|
56
|
+
* intersection and would wave through a key belonging to a sibling verb.
|
|
57
|
+
*/
|
|
58
|
+
type UnionKeys<T> = T extends unknown ? keyof T : never;
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Reject a key the contract does not declare, including on a body that is not a
|
|
62
|
+
* fresh object literal.
|
|
63
|
+
*
|
|
64
|
+
* DO NOT "simplify" this away to plain parameter typing. It is load-bearing,
|
|
65
|
+
* not a belt-and-braces backstop, and the measurements say which cases it
|
|
66
|
+
* carries alone. Four ways a stale key can arrive, all compiled against this
|
|
67
|
+
* repo's TypeScript with a `profile.forget`-shaped input:
|
|
68
|
+
*
|
|
69
|
+
* 1. written inline in a fresh literal — parameter typing already rejects
|
|
70
|
+
* 2. written inline BESIDE a spread — parameter typing already rejects
|
|
71
|
+
* 3. body built as a variable first — SLIPS PAST parameter typing
|
|
72
|
+
* 4. carried in by the spread source's type — SLIPS PAST parameter typing
|
|
73
|
+
*
|
|
74
|
+
* A spread literal is not uniformly unchecked, which is the easy thing to get
|
|
75
|
+
* wrong: anything written inline in one is still checked normally, and only what
|
|
76
|
+
* arrives THROUGH the spread escapes, because it belongs to the source's type
|
|
77
|
+
* rather than to the literal. Rows 3 and 4 are the two nothing else catches, and
|
|
78
|
+
* this guard rejects both with TS2345 — measured, not reasoned about.
|
|
79
|
+
*
|
|
80
|
+
* That is not hypothetical: `profile.forget` really did retire `lineIndex`, and
|
|
81
|
+
* a body built one line earlier would have carried it silently past a correctly
|
|
82
|
+
* typed parameter. Seeding inline literals proves only the cases someone thought
|
|
83
|
+
* to seed; this closes the shape.
|
|
84
|
+
*
|
|
85
|
+
* There is a second reason it earns its place here. The SDK's `invoke` is
|
|
86
|
+
* OVERLOADED — a typed signature plus a loose
|
|
87
|
+
* `invoke<T = unknown>(methodId: string, input?: Record<string, unknown>)`. At a
|
|
88
|
+
* bare call site a body the typed overload rejects does not raise an
|
|
89
|
+
* excess-property error at all; overload resolution simply falls through to the
|
|
90
|
+
* loose one and the result degrades to `unknown`. Any error is then incidental,
|
|
91
|
+
* downstream, and disappears entirely if the caller ignores or casts the result.
|
|
92
|
+
* `ProfileInvoke` is a single generic signature with no such fallback, so a bad
|
|
93
|
+
* body fails here, at the call, naming the offending key.
|
|
94
|
+
*
|
|
95
|
+
* Intersecting the parameter with this maps every undeclared key to `never`, so
|
|
96
|
+
* no real value satisfies it however the object was built. Distributed over the
|
|
97
|
+
* target union via {@link UnionKeys} so each member is checked rather than only
|
|
98
|
+
* the keys they have in common.
|
|
99
|
+
*/
|
|
100
|
+
export type NoExcessKeys<TBody, TShape> = Record<Exclude<keyof TBody, UnionKeys<TShape>>, never>;
|
|
101
|
+
|
|
102
|
+
/** A body that conforms to a verb's declared input and carries nothing extra. */
|
|
103
|
+
export type ExactProfileInput<TVerb extends ProfileVerb, TBody> =
|
|
104
|
+
TBody & NoExcessKeys<TBody, ProfileInput<TVerb>>;
|
|
105
|
+
|
|
106
|
+
/** What `profile.read` answers: the whole document, by section. */
|
|
107
|
+
export type ProfileDocumentView = OperatorMethodOutput<'profile.read'>;
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Load state — what `profile.status` answers, and the `state` half of a read.
|
|
111
|
+
*
|
|
112
|
+
* `kind` is `loaded` | `unavailable` | `disabled`. The counts belong to
|
|
113
|
+
* `loaded` and `reason` to `unavailable`. There is no value property anywhere
|
|
114
|
+
* in this shape, which is what makes the status output safe to show in a
|
|
115
|
+
* diagnostic context (§11.3).
|
|
116
|
+
*/
|
|
117
|
+
export type ProfileStateView = OperatorMethodOutput<'profile.status'>;
|
|
118
|
+
|
|
119
|
+
/** What `profile.provenance` answers for one field. */
|
|
120
|
+
export type ProfileProvenanceReportView = OperatorMethodOutput<'profile.provenance'>;
|
|
121
|
+
|
|
122
|
+
/** What every write verb answers. `ok: false` always carries a reason. */
|
|
123
|
+
export type ProfileWriteResultView = OperatorMethodOutput<'profile.set'>;
|
|
124
|
+
|
|
125
|
+
/** One `## ` section, with the tier its content belongs to. */
|
|
126
|
+
export type ProfileSectionView = ProfileDocumentView['sections'][number];
|
|
127
|
+
/** One mechanical field. `valid: false` still carries the value — see §4.3. */
|
|
128
|
+
export type ProfileFieldView = ProfileSectionView['fields'][number];
|
|
129
|
+
/** One prose line, preserved as written. */
|
|
130
|
+
export type ProfileLineView = ProfileSectionView['prose'][number];
|
|
131
|
+
/** Where a line came from: which surface, when, and the owner's exact words. */
|
|
132
|
+
export type ProfileProvenanceView = NonNullable<ProfileFieldView['provenance']>;
|
|
133
|
+
/** A mechanical value that did not validate, and why. Never fails the file. */
|
|
134
|
+
export type ProfileInvalidFieldView = NonNullable<ProfileStateView['invalidFields']>[number];
|
|
135
|
+
/** A retained `<!-- was: … -->` predecessor, so a wrong correction is recoverable. */
|
|
136
|
+
export type ProfileSupersededView = ProfileProvenanceReportView['superseded'][number];
|
|
137
|
+
/** One thing a write did. Names the field; never repeats the value. */
|
|
138
|
+
export type ProfileChangeView = ProfileWriteResultView['changes'][number];
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Compile-time proof of two things this file assumes: that all four write verbs
|
|
142
|
+
* really do answer one shape, and that `profile.status` really is the same
|
|
143
|
+
* `state` a read carries.
|
|
144
|
+
*
|
|
145
|
+
* One write checker serves four verbs and `toProfileState` checks both the
|
|
146
|
+
* status response and a read's `state`. If the contract ever splits them,
|
|
147
|
+
* `Exact` collapses to `never` and this assignment stops compiling, rather than
|
|
148
|
+
* the difference going unnoticed until a response renders wrong.
|
|
149
|
+
*/
|
|
150
|
+
type Exact<A, B> = [A] extends [B] ? ([B] extends [A] ? true : never) : never;
|
|
151
|
+
export const PROFILE_CONTRACT_SHAPES_AGREE: Exact<OperatorMethodOutput<'profile.append'>, ProfileWriteResultView>
|
|
152
|
+
& Exact<OperatorMethodOutput<'profile.forget'>, ProfileWriteResultView>
|
|
153
|
+
& Exact<OperatorMethodOutput<'profile.undo'>, ProfileWriteResultView>
|
|
154
|
+
& Exact<ProfileDocumentView['state'], ProfileStateView> = true;
|
|
155
|
+
|
|
156
|
+
// ---------------------------------------------------------------------------
|
|
157
|
+
// Primitive checks
|
|
158
|
+
// ---------------------------------------------------------------------------
|
|
159
|
+
|
|
160
|
+
/** The single place an untyped response is narrowed to an indexable object. */
|
|
161
|
+
function asRecord(value: unknown): Record<string, unknown> | undefined {
|
|
162
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) return undefined;
|
|
163
|
+
return value as unknown as Record<string, unknown>;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function isString(value: unknown): value is string {
|
|
167
|
+
return typeof value === 'string';
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function isBoolean(value: unknown): value is boolean {
|
|
171
|
+
return typeof value === 'boolean';
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function isFiniteNumber(value: unknown): value is number {
|
|
175
|
+
return typeof value === 'number' && Number.isFinite(value);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** Absent and null both mean "not supplied"; anything else must be a string. */
|
|
179
|
+
function optionalString(value: unknown): Checked<string | undefined> {
|
|
180
|
+
if (value === undefined || value === null) return undefined;
|
|
181
|
+
return isString(value) ? value : MALFORMED;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function optionalBoolean(value: unknown): Checked<boolean | undefined> {
|
|
185
|
+
if (value === undefined || value === null) return undefined;
|
|
186
|
+
return isBoolean(value) ? value : MALFORMED;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function optionalNumber(value: unknown): Checked<number | undefined> {
|
|
190
|
+
if (value === undefined || value === null) return undefined;
|
|
191
|
+
return isFiniteNumber(value) ? value : MALFORMED;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** An array whose every member checks out, or MALFORMED. Never a filtered subset. */
|
|
195
|
+
function checkedArray<T>(value: unknown, check: (entry: unknown) => Checked<T>): Checked<T[]> {
|
|
196
|
+
if (!Array.isArray(value)) return MALFORMED;
|
|
197
|
+
const out: T[] = [];
|
|
198
|
+
for (const entry of value) {
|
|
199
|
+
const parsed = check(entry);
|
|
200
|
+
if (parsed === MALFORMED) return MALFORMED;
|
|
201
|
+
out.push(parsed);
|
|
202
|
+
}
|
|
203
|
+
return out;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function checkedStringArray(value: unknown): Checked<string[]> {
|
|
207
|
+
return checkedArray(value, (entry) => (isString(entry) ? entry : MALFORMED));
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// ---------------------------------------------------------------------------
|
|
211
|
+
// Response checks
|
|
212
|
+
// ---------------------------------------------------------------------------
|
|
213
|
+
|
|
214
|
+
function toProvenance(value: unknown): Checked<ProfileProvenanceView | undefined> {
|
|
215
|
+
if (value === undefined || value === null) return undefined;
|
|
216
|
+
const record = asRecord(value);
|
|
217
|
+
if (!record) return MALFORMED;
|
|
218
|
+
const { surface, date, said } = record;
|
|
219
|
+
if (!isString(surface) || !isString(date) || !isString(said)) return MALFORMED;
|
|
220
|
+
return { surface, date, said };
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function toLine(value: unknown): Checked<ProfileLineView> {
|
|
224
|
+
const record = asRecord(value);
|
|
225
|
+
if (!record) return MALFORMED;
|
|
226
|
+
const { lineIndex, section, text } = record;
|
|
227
|
+
if (!isFiniteNumber(lineIndex) || !isString(section) || !isString(text)) return MALFORMED;
|
|
228
|
+
const provenance = toProvenance(record.provenance);
|
|
229
|
+
if (provenance === MALFORMED) return MALFORMED;
|
|
230
|
+
return { lineIndex, section, text, ...(provenance ? { provenance } : {}) };
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function toField(value: unknown): Checked<ProfileFieldView> {
|
|
234
|
+
const record = asRecord(value);
|
|
235
|
+
if (!record) return MALFORMED;
|
|
236
|
+
const { fieldId, label, value: fieldValue, valid } = record;
|
|
237
|
+
if (!isString(fieldId) || !isString(label) || !isString(fieldValue) || !isBoolean(valid)) return MALFORMED;
|
|
238
|
+
const invalidReason = optionalString(record.invalidReason);
|
|
239
|
+
if (invalidReason === MALFORMED) return MALFORMED;
|
|
240
|
+
const provenance = toProvenance(record.provenance);
|
|
241
|
+
if (provenance === MALFORMED) return MALFORMED;
|
|
242
|
+
return {
|
|
243
|
+
fieldId,
|
|
244
|
+
label,
|
|
245
|
+
value: fieldValue,
|
|
246
|
+
valid,
|
|
247
|
+
...(invalidReason === undefined ? {} : { invalidReason }),
|
|
248
|
+
...(provenance ? { provenance } : {}),
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function toSection(value: unknown): Checked<ProfileSectionView> {
|
|
253
|
+
const record = asRecord(value);
|
|
254
|
+
if (!record) return MALFORMED;
|
|
255
|
+
const { heading, tier } = record;
|
|
256
|
+
if (!isString(heading) || !isString(tier)) return MALFORMED;
|
|
257
|
+
const fields = checkedArray(record.fields, toField);
|
|
258
|
+
if (fields === MALFORMED) return MALFORMED;
|
|
259
|
+
const prose = checkedArray(record.prose, toLine);
|
|
260
|
+
if (prose === MALFORMED) return MALFORMED;
|
|
261
|
+
return { heading, tier, fields, prose };
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function toInvalidField(value: unknown): Checked<ProfileInvalidFieldView> {
|
|
265
|
+
const record = asRecord(value);
|
|
266
|
+
if (!record) return MALFORMED;
|
|
267
|
+
const { fieldId, reason } = record;
|
|
268
|
+
if (!isString(fieldId) || !isString(reason)) return MALFORMED;
|
|
269
|
+
return { fieldId, reason };
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/** `profile.status`, and the `state` half of `profile.read`. */
|
|
273
|
+
export function toProfileState(value: unknown): Checked<ProfileStateView> {
|
|
274
|
+
const record = asRecord(value);
|
|
275
|
+
if (!record) return MALFORMED;
|
|
276
|
+
const { kind, path } = record;
|
|
277
|
+
if (!isString(kind) || !isString(path)) return MALFORMED;
|
|
278
|
+
const exists = optionalBoolean(record.exists);
|
|
279
|
+
if (exists === MALFORMED) return MALFORMED;
|
|
280
|
+
const lineCount = optionalNumber(record.lineCount);
|
|
281
|
+
if (lineCount === MALFORMED) return MALFORMED;
|
|
282
|
+
const fieldCount = optionalNumber(record.fieldCount);
|
|
283
|
+
if (fieldCount === MALFORMED) return MALFORMED;
|
|
284
|
+
const proseLineCount = optionalNumber(record.proseLineCount);
|
|
285
|
+
if (proseLineCount === MALFORMED) return MALFORMED;
|
|
286
|
+
const reason = optionalString(record.reason);
|
|
287
|
+
if (reason === MALFORMED) return MALFORMED;
|
|
288
|
+
const sections = record.sections === undefined || record.sections === null
|
|
289
|
+
? undefined
|
|
290
|
+
: checkedStringArray(record.sections);
|
|
291
|
+
if (sections === MALFORMED) return MALFORMED;
|
|
292
|
+
const invalidFields = record.invalidFields === undefined || record.invalidFields === null
|
|
293
|
+
? undefined
|
|
294
|
+
: checkedArray(record.invalidFields, toInvalidField);
|
|
295
|
+
if (invalidFields === MALFORMED) return MALFORMED;
|
|
296
|
+
return {
|
|
297
|
+
kind,
|
|
298
|
+
path,
|
|
299
|
+
...(exists === undefined ? {} : { exists }),
|
|
300
|
+
...(lineCount === undefined ? {} : { lineCount }),
|
|
301
|
+
...(fieldCount === undefined ? {} : { fieldCount }),
|
|
302
|
+
...(proseLineCount === undefined ? {} : { proseLineCount }),
|
|
303
|
+
...(sections === undefined ? {} : { sections }),
|
|
304
|
+
...(invalidFields === undefined ? {} : { invalidFields }),
|
|
305
|
+
...(reason === undefined ? {} : { reason }),
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/** `profile.read`. */
|
|
310
|
+
export function toProfileDocument(value: unknown): Checked<ProfileDocumentView> {
|
|
311
|
+
const record = asRecord(value);
|
|
312
|
+
if (!record) return MALFORMED;
|
|
313
|
+
const state = toProfileState(record.state);
|
|
314
|
+
if (state === MALFORMED) return MALFORMED;
|
|
315
|
+
const sections = checkedArray(record.sections, toSection);
|
|
316
|
+
if (sections === MALFORMED) return MALFORMED;
|
|
317
|
+
return { state, sections };
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function toSuperseded(value: unknown): Checked<ProfileSupersededView> {
|
|
321
|
+
const record = asRecord(value);
|
|
322
|
+
if (!record) return MALFORMED;
|
|
323
|
+
const { lineIndex, fieldId, section, text, value: supersededValue, supersededOn, previousLine } = record;
|
|
324
|
+
if (
|
|
325
|
+
!isFiniteNumber(lineIndex)
|
|
326
|
+
|| !isString(fieldId)
|
|
327
|
+
|| !isString(section)
|
|
328
|
+
|| !isString(text)
|
|
329
|
+
|| !isString(supersededValue)
|
|
330
|
+
|| !isString(supersededOn)
|
|
331
|
+
|| !isString(previousLine)
|
|
332
|
+
) return MALFORMED;
|
|
333
|
+
const provenance = toProvenance(record.provenance);
|
|
334
|
+
if (provenance === MALFORMED) return MALFORMED;
|
|
335
|
+
return {
|
|
336
|
+
lineIndex,
|
|
337
|
+
fieldId,
|
|
338
|
+
section,
|
|
339
|
+
text,
|
|
340
|
+
value: supersededValue,
|
|
341
|
+
supersededOn,
|
|
342
|
+
previousLine,
|
|
343
|
+
...(provenance ? { provenance } : {}),
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/** `profile.provenance`. */
|
|
348
|
+
export function toProfileProvenanceReport(value: unknown): Checked<ProfileProvenanceReportView> {
|
|
349
|
+
const record = asRecord(value);
|
|
350
|
+
if (!record) return MALFORMED;
|
|
351
|
+
const { fieldId, present, handEdited } = record;
|
|
352
|
+
if (!isString(fieldId) || !isBoolean(present) || !isBoolean(handEdited)) return MALFORMED;
|
|
353
|
+
const provenance = toProvenance(record.provenance);
|
|
354
|
+
if (provenance === MALFORMED) return MALFORMED;
|
|
355
|
+
const superseded = checkedArray(record.superseded, toSuperseded);
|
|
356
|
+
if (superseded === MALFORMED) return MALFORMED;
|
|
357
|
+
return { fieldId, present, handEdited, ...(provenance ? { provenance } : {}), superseded };
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function toChange(value: unknown): Checked<ProfileChangeView> {
|
|
361
|
+
const record = asRecord(value);
|
|
362
|
+
if (!record) return MALFORMED;
|
|
363
|
+
const { kind, section, label, superseded } = record;
|
|
364
|
+
if (!isString(kind) || !isString(section) || !isString(label) || !isBoolean(superseded)) return MALFORMED;
|
|
365
|
+
const fieldId = optionalString(record.fieldId);
|
|
366
|
+
if (fieldId === MALFORMED) return MALFORMED;
|
|
367
|
+
return { kind, section, label, superseded, ...(fieldId === undefined ? {} : { fieldId }) };
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/** `profile.set`, `profile.append`, `profile.forget`, `profile.undo`. */
|
|
371
|
+
export function toProfileWriteResult(value: unknown): Checked<ProfileWriteResultView> {
|
|
372
|
+
const record = asRecord(value);
|
|
373
|
+
if (!record) return MALFORMED;
|
|
374
|
+
const { ok, disclosure } = record;
|
|
375
|
+
if (!isBoolean(ok) || !isString(disclosure)) return MALFORMED;
|
|
376
|
+
const reason = optionalString(record.reason);
|
|
377
|
+
if (reason === MALFORMED) return MALFORMED;
|
|
378
|
+
const changes = checkedArray(record.changes, toChange);
|
|
379
|
+
if (changes === MALFORMED) return MALFORMED;
|
|
380
|
+
return { ok, disclosure, changes, ...(reason === undefined ? {} : { reason }) };
|
|
381
|
+
}
|
|
@@ -26,7 +26,19 @@ interface RevertHunkResult {
|
|
|
26
26
|
readonly refusal: { readonly reason: string } | null;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
/**
|
|
30
|
+
* The review UI's invoke context.
|
|
31
|
+
*
|
|
32
|
+
* `explicitUserRequest: true` is honest here and only here-shaped: this path
|
|
33
|
+
* runs because a person is looking at a hunk and pressed a key. Scheduled
|
|
34
|
+
* work, triggers and channel-driven work must never set it — the distinction
|
|
35
|
+
* is exactly "did the owner ask for this right now", and it is what lets a
|
|
36
|
+
* confirmation-gated verb tell an authorized action apart from one initiated
|
|
37
|
+
* by content.
|
|
38
|
+
*/
|
|
39
|
+
const INVOKE_CONTEXT = {
|
|
40
|
+
context: { clientKind: 'tui' as const, metadata: { explicitUserRequest: true } },
|
|
41
|
+
};
|
|
30
42
|
|
|
31
43
|
/** True for the honest 409 the revert verb throws when the hunk drifted since it was captured. */
|
|
32
44
|
function isConflict(err: unknown): boolean {
|
package/src/input/commands.ts
CHANGED
|
@@ -11,6 +11,7 @@ import { registerConfigCommand } from './commands/config.ts';
|
|
|
11
11
|
import { registerDiscoveryRuntimeCommands } from './commands/discovery-runtime.ts';
|
|
12
12
|
import { registerPlanningRuntimeCommands } from './commands/planning-runtime.ts';
|
|
13
13
|
import { registerSecretRuntimeCommands } from './commands/secret-runtime.ts';
|
|
14
|
+
import { registerPaymentCardCommands } from './commands/payment-card-intake.ts';
|
|
14
15
|
import { registerScheduleRuntimeCommands } from './commands/schedule-runtime.ts';
|
|
15
16
|
import { registerBranchRuntimeCommands } from './commands/branch-runtime.ts';
|
|
16
17
|
import { registerOperatorRuntimeCommands } from './commands/operator-runtime.ts';
|
|
@@ -79,7 +80,10 @@ import { registerReviewRuntimeCommands } from './commands/review-runtime.ts';
|
|
|
79
80
|
import { registerPaletteRuntimeCommands } from './commands/palette-runtime.ts';
|
|
80
81
|
import { registerCiRuntimeCommands } from './commands/ci-runtime.ts';
|
|
81
82
|
import { registerCheckinRuntimeCommands } from './commands/checkin-runtime.ts';
|
|
83
|
+
import { registerMailRuntimeCommands } from './commands/mail-runtime.ts';
|
|
84
|
+
import { registerCalendarRuntimeCommands } from './commands/calendar-runtime.ts';
|
|
82
85
|
import { registerPrincipalsRuntimeCommands } from './commands/principals-runtime.ts';
|
|
86
|
+
import { registerProfileRuntimeCommands } from './commands/profile-runtime.ts';
|
|
83
87
|
|
|
84
88
|
/**
|
|
85
89
|
* A named group of built-in commands. The `category` label is the single
|
|
@@ -160,6 +164,7 @@ export const BUILTIN_COMMAND_GROUPS: readonly BuiltinCommandGroup[] = [
|
|
|
160
164
|
{ category: 'Discovery', register: registerDiscoveryRuntimeCommands },
|
|
161
165
|
{ category: 'Planning', register: registerPlanningRuntimeCommands },
|
|
162
166
|
{ category: 'Secrets', register: registerSecretRuntimeCommands },
|
|
167
|
+
{ category: 'Payments', register: registerPaymentCardCommands },
|
|
163
168
|
{ category: 'Scheduling', register: registerScheduleRuntimeCommands },
|
|
164
169
|
{ category: 'Branches', register: registerBranchRuntimeCommands },
|
|
165
170
|
{ category: 'Session Content', register: registerSessionContentCommands },
|
|
@@ -173,7 +178,13 @@ export const BUILTIN_COMMAND_GROUPS: readonly BuiltinCommandGroup[] = [
|
|
|
173
178
|
{ category: 'Diff & Review', register: registerReviewRuntimeCommands },
|
|
174
179
|
{ category: 'CI', register: registerCiRuntimeCommands },
|
|
175
180
|
{ category: 'Check-in', register: registerCheckinRuntimeCommands },
|
|
181
|
+
{ category: 'Mail & Calendar', register: registerMailRuntimeCommands },
|
|
182
|
+
{ category: 'Mail & Calendar', register: registerCalendarRuntimeCommands },
|
|
176
183
|
{ category: 'Principals', register: registerPrincipalsRuntimeCommands },
|
|
184
|
+
// "Owner Profile", not "Profiles": the Profiles category above is
|
|
185
|
+
// /profile-sync's named-config-preset bundles, which have nothing to do with
|
|
186
|
+
// what the platform knows about the person who owns it.
|
|
187
|
+
{ category: 'Owner Profile', register: registerProfileRuntimeCommands },
|
|
177
188
|
{ category: 'Policy', register: (registry) => registry.register(policyCommand) },
|
|
178
189
|
{ category: 'Providers', register: (registry) => registry.register(providerCommand) },
|
|
179
190
|
{ category: 'Eval', register: (registry) => registry.register(evalCommand) },
|
|
@@ -66,6 +66,10 @@ export function openComposerInEditor(deps: ComposerEditorDeps): void {
|
|
|
66
66
|
return;
|
|
67
67
|
}
|
|
68
68
|
const draft = deps.readDraft();
|
|
69
|
+
// Real production scratch (runs on the end user's machine, not test
|
|
70
|
+
// infrastructure), so this stays rooted at the real OS temp dir rather
|
|
71
|
+
// than the test-only makeProjectTempDir helper. Cleaned up in the
|
|
72
|
+
// `finally` below on every exit path.
|
|
69
73
|
const dir = mkdtempSync(join(tmpdir(), 'goodvibes-composer-'));
|
|
70
74
|
const file = join(dir, 'COMPOSER.md');
|
|
71
75
|
const spawn = deps.spawn ?? defaultSpawn;
|
|
@@ -52,6 +52,18 @@ export function buildOnboardingApplyRequest(controller: OnboardingWizardControll
|
|
|
52
52
|
acknowledged: controller.getBooleanFieldValue(fieldId, false),
|
|
53
53
|
});
|
|
54
54
|
};
|
|
55
|
+
// Every credential the first-run wizard captures is one the DAEMON executes
|
|
56
|
+
// with: model provider API keys (the daemon runs the model), the Cloudflare
|
|
57
|
+
// API token, and the masked fields of the external surfaces — all of which
|
|
58
|
+
// live under daemon-owned `surfaces.*` config paths. So they all go to the
|
|
59
|
+
// daemon tier, which is the one tier every surface AND the daemon read.
|
|
60
|
+
//
|
|
61
|
+
// 'project' — what this used to pass — filed them under whatever directory
|
|
62
|
+
// the wizard happened to be run from. Run the wizard in ~/code/foo and the
|
|
63
|
+
// daemon, which has no idea that directory exists, found nothing; run it
|
|
64
|
+
// again elsewhere and the first setup silently stopped applying. First-run
|
|
65
|
+
// setup is exactly the moment where "configure once, works everywhere" has
|
|
66
|
+
// to hold.
|
|
55
67
|
const setSecret = (key: string, value: string): void => {
|
|
56
68
|
if (value.length === 0) return;
|
|
57
69
|
const medium = controller.getSelectedSecretMedium();
|
|
@@ -59,7 +71,7 @@ export function buildOnboardingApplyRequest(controller: OnboardingWizardControll
|
|
|
59
71
|
kind: 'set-secret',
|
|
60
72
|
key,
|
|
61
73
|
value,
|
|
62
|
-
scope: '
|
|
74
|
+
scope: 'daemon',
|
|
63
75
|
medium,
|
|
64
76
|
});
|
|
65
77
|
};
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* payments-config.ts — TUI-local synthetic settings for the payment card
|
|
3
|
+
* capability's card MATERIAL fields.
|
|
4
|
+
*
|
|
5
|
+
* The SDK's CONFIG_SCHEMA now has a full real `payments` section (28 keys:
|
|
6
|
+
* enabled, defaultCardId, currency, cvvHandling, the six budget keys,
|
|
7
|
+
* shipping.preferredTier, the fourteen billing/shipping address sub-fields
|
|
8
|
+
* (name, line1, line2, city, region, postalCode, country — each repeated for
|
|
9
|
+
* billingAddress and shippingAddress), the two window keys, and
|
|
10
|
+
* notifyChannels — see `@pellux/goodvibes-sdk/platform/config`). Every one of
|
|
11
|
+
* those keys reads and writes through the ordinary CONFIG_SCHEMA-driven path
|
|
12
|
+
* in settings-modal-data.ts's buildSettingGroups, exactly like `relay.*` or
|
|
13
|
+
* any other real SDK domain — no synthetic entry or dedicated store is needed
|
|
14
|
+
* for them, and this module builds none. (An earlier round of this module
|
|
15
|
+
* modeled billing/shipping address as two flat TUI-local strings; the SDK's
|
|
16
|
+
* own structured per-field keys superseded that the moment they shipped, so
|
|
17
|
+
* those two synthetic entries are gone from this file entirely.)
|
|
18
|
+
*
|
|
19
|
+
* What this module still builds synthetic entries for is the four card
|
|
20
|
+
* MATERIAL fields: number, expiry, CVV, cardholder name. Per the SDK's own
|
|
21
|
+
* design (see its schema-domain-payments.ts header and `platform/payments`'s
|
|
22
|
+
* method catalog), card material is deliberately never a config path at all:
|
|
23
|
+
* it lives write-only in the daemon secret store, named by keys the daemon's
|
|
24
|
+
* own `payments.cards.create` control-plane method derives internally. The
|
|
25
|
+
* SDK does not yet expose an equivalent config-level path for a single
|
|
26
|
+
* implicit card the way this app's `/payments card` flow models it, so these
|
|
27
|
+
* four fields are synthetic sub-keys under the SDK's real `payments`
|
|
28
|
+
* section — the same established pattern this codebase already uses for
|
|
29
|
+
* `tts.speed`, `behavior.notifyAfterSeconds`, etc.: a key one level under an
|
|
30
|
+
* EXISTING section that CONFIG_SCHEMA hasn't grown a scalar entry for yet.
|
|
31
|
+
*
|
|
32
|
+
* That one-level-under-an-existing-section shape matters mechanically: the
|
|
33
|
+
* real ConfigManager's dotted-path resolver only throws "Invalid config path"
|
|
34
|
+
* when an INTERMEDIATE segment is missing (e.g. `payments.card.number`, where
|
|
35
|
+
* `card` is not itself a section) — it does not throw for a final leaf that
|
|
36
|
+
* the schema hasn't declared (`payments.cardNumber`), the same tolerance
|
|
37
|
+
* `tts.speed` already relies on under `tts`. So these four keys are named
|
|
38
|
+
* FLAT (`payments.cardNumber`, not `payments.card.number`) specifically so
|
|
39
|
+
* `ConfigManager.get/setDynamic` accepts them — verified against the real
|
|
40
|
+
* ConfigManager (not the fake store this module used before this round): a
|
|
41
|
+
* flat sub-key persists to the real daemon-owned settings file precisely
|
|
42
|
+
* because `payments.` is a `DAEMON_OWNED_CONFIG_PREFIXES` entry, which is
|
|
43
|
+
* exactly what makes the daemon (and every other surface) able to see it
|
|
44
|
+
* after this TUI closes — the thing the local JSON store this module used to
|
|
45
|
+
* use never gave them.
|
|
46
|
+
*
|
|
47
|
+
* All four keys are secret-tier (see config/secret-config.ts's
|
|
48
|
+
* SECRET_CONFIG_KEYS). Primary entry for those is the concealed-input flow in
|
|
49
|
+
* commands/payment-card-intake.ts, itself gated on the SDK's
|
|
50
|
+
* `mayOfferCardEntryFlow` (card details may only be typed at a local
|
|
51
|
+
* terminal or the webui, never over a remote messaging surface — see that
|
|
52
|
+
* file's header); they are also reachable as ordinary secret-backed settings
|
|
53
|
+
* rows here, which is what makes them masked mid-edit — see
|
|
54
|
+
* renderer/settings-modal.ts's currentSettingValue().
|
|
55
|
+
*
|
|
56
|
+
* cvvHandling — real now, not built here — selects whether the CVV is stored
|
|
57
|
+
* (the default, per the 2026-07-27 payment-capability ruling) or requested at
|
|
58
|
+
* purchase time; see `CVV_PROMPT_TRADEOFF_WARNING`, imported directly from
|
|
59
|
+
* `@pellux/goodvibes-sdk/platform/payments` by every caller that needs it
|
|
60
|
+
* (renderer/settings-modal.ts, settings-modal.ts) — no local copy lives here
|
|
61
|
+
* or anywhere else in this app.
|
|
62
|
+
*/
|
|
63
|
+
|
|
64
|
+
import type { ConfigManager } from '@pellux/goodvibes-sdk/platform/config';
|
|
65
|
+
import type { ConfigKey } from '../config/index.ts';
|
|
66
|
+
import type { SettingEntry } from './settings-modal-types.ts';
|
|
67
|
+
|
|
68
|
+
/** Real ConfigManager's read surface — these four keys are defensive reads (see header comment). */
|
|
69
|
+
export type PaymentsConfigReader = Pick<ConfigManager, 'get'>;
|
|
70
|
+
|
|
71
|
+
export const PAYMENTS_CARD_NUMBER_CONFIG_KEY = 'payments.cardNumber' as ConfigKey;
|
|
72
|
+
export const PAYMENTS_CARD_EXPIRY_CONFIG_KEY = 'payments.cardExpiry' as ConfigKey;
|
|
73
|
+
export const PAYMENTS_CARD_CVV_CONFIG_KEY = 'payments.cardCvv' as ConfigKey;
|
|
74
|
+
export const PAYMENTS_CARD_CARDHOLDER_NAME_CONFIG_KEY = 'payments.cardholderName' as ConfigKey;
|
|
75
|
+
|
|
76
|
+
/** The real SDK schema key for the CVV-handling selector — no longer synthetic. */
|
|
77
|
+
export const PAYMENTS_CVV_HANDLING_CONFIG_KEY = 'payments.cvvHandling' as ConfigKey;
|
|
78
|
+
|
|
79
|
+
function readStringField(configManager: PaymentsConfigReader, key: ConfigKey): string {
|
|
80
|
+
// Defensive try/catch, same rationale as worktree-setup-config.ts's
|
|
81
|
+
// readWorktreeSetupList and this codebase's other one-level-under-a-real-
|
|
82
|
+
// section synthetic reads (tts.speed, behavior.notifyAfterSeconds): the real
|
|
83
|
+
// ConfigManager never throws for these particular keys (verified — see
|
|
84
|
+
// header comment), but degrading to empty rather than crashing the whole
|
|
85
|
+
// settings modal is the same posture every synthetic setting here takes
|
|
86
|
+
// toward an unexpected value.
|
|
87
|
+
try {
|
|
88
|
+
const raw = configManager.get(key);
|
|
89
|
+
return typeof raw === 'string' ? raw : '';
|
|
90
|
+
} catch {
|
|
91
|
+
return '';
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function buildStringFieldEntry(
|
|
96
|
+
configManager: PaymentsConfigReader,
|
|
97
|
+
key: ConfigKey,
|
|
98
|
+
description: string,
|
|
99
|
+
): SettingEntry {
|
|
100
|
+
const currentValue = readStringField(configManager, key);
|
|
101
|
+
return {
|
|
102
|
+
setting: { key, type: 'string', default: '', description },
|
|
103
|
+
currentValue,
|
|
104
|
+
isDefault: currentValue === '',
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function buildPaymentsCardNumberEntry(configManager: PaymentsConfigReader): SettingEntry {
|
|
109
|
+
return buildStringFieldEntry(
|
|
110
|
+
configManager,
|
|
111
|
+
PAYMENTS_CARD_NUMBER_CONFIG_KEY,
|
|
112
|
+
'Payment card number. Stored through the secret manager at daemon scope; entering it here or via /payments card never shows the typed characters in plaintext.',
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function buildPaymentsCardExpiryEntry(configManager: PaymentsConfigReader): SettingEntry {
|
|
117
|
+
return buildStringFieldEntry(
|
|
118
|
+
configManager,
|
|
119
|
+
PAYMENTS_CARD_EXPIRY_CONFIG_KEY,
|
|
120
|
+
'Payment card expiry (MM/YY). Stored through the secret manager at daemon scope, same handling as the card number.',
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function buildPaymentsCardCvvEntry(configManager: PaymentsConfigReader): SettingEntry {
|
|
125
|
+
return buildStringFieldEntry(
|
|
126
|
+
configManager,
|
|
127
|
+
PAYMENTS_CARD_CVV_CONFIG_KEY,
|
|
128
|
+
'Payment card CVV. Stored through the secret manager at daemon scope: never logged, never rendered, never shown mid-edit, excluded from every export and diagnostic dump.',
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function buildPaymentsCardCardholderNameEntry(configManager: PaymentsConfigReader): SettingEntry {
|
|
133
|
+
return buildStringFieldEntry(
|
|
134
|
+
configManager,
|
|
135
|
+
PAYMENTS_CARD_CARDHOLDER_NAME_CONFIG_KEY,
|
|
136
|
+
'Name on the payment card. Stored through the secret manager at daemon scope, same handling as the card number.',
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** All payments synthetic entries (the four card-material fields), in the order they should render. */
|
|
141
|
+
export function buildPaymentsSyntheticEntries(configManager: PaymentsConfigReader): SettingEntry[] {
|
|
142
|
+
return [
|
|
143
|
+
buildPaymentsCardNumberEntry(configManager),
|
|
144
|
+
buildPaymentsCardExpiryEntry(configManager),
|
|
145
|
+
buildPaymentsCardCvvEntry(configManager),
|
|
146
|
+
buildPaymentsCardCardholderNameEntry(configManager),
|
|
147
|
+
];
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** Every payments synthetic config key this module owns, for refresh-loop membership checks. */
|
|
151
|
+
export const PAYMENTS_SYNTHETIC_CONFIG_KEYS: readonly ConfigKey[] = [
|
|
152
|
+
PAYMENTS_CARD_NUMBER_CONFIG_KEY,
|
|
153
|
+
PAYMENTS_CARD_EXPIRY_CONFIG_KEY,
|
|
154
|
+
PAYMENTS_CARD_CVV_CONFIG_KEY,
|
|
155
|
+
PAYMENTS_CARD_CARDHOLDER_NAME_CONFIG_KEY,
|
|
156
|
+
];
|
|
157
|
+
|
|
158
|
+
export function isPaymentsSyntheticConfigKey(key: string): key is ConfigKey {
|
|
159
|
+
return (PAYMENTS_SYNTHETIC_CONFIG_KEYS as readonly string[]).includes(key);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** Refresh one payments synthetic entry's currentValue/isDefault in place. */
|
|
163
|
+
export function refreshPaymentsSyntheticEntry(entry: SettingEntry, configManager: PaymentsConfigReader): void {
|
|
164
|
+
const currentValue = readStringField(configManager, entry.setting.key as ConfigKey);
|
|
165
|
+
entry.currentValue = currentValue;
|
|
166
|
+
entry.isDefault = currentValue === '';
|
|
167
|
+
}
|