catherd-cli 0.2.0 → 1.0.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.
Files changed (187) hide show
  1. package/README.md +114 -30
  2. package/THIRD_PARTY_NOTICES.md +37 -0
  3. package/catalog/jev.json +89 -0
  4. package/catalog/models.json +236 -0
  5. package/catalog/scores.json +366 -0
  6. package/package.json +24 -8
  7. package/plugin/.claude-plugin/plugin.json +2 -2
  8. package/plugin/.mcp.json +1 -1
  9. package/plugin/commands/catherd.md +1 -1
  10. package/plugin/skills/catherd/SKILL.md +97 -51
  11. package/plugin/skills/catherd-setup/SKILL.md +34 -24
  12. package/src/adapters/all.ts +8 -0
  13. package/src/adapters/backend.ts +144 -0
  14. package/src/adapters/claude-code/events.ts +86 -0
  15. package/src/adapters/claude-code/index.ts +223 -0
  16. package/src/adapters/claude-code/models-api.ts +65 -0
  17. package/src/adapters/claude-code/models.ts +26 -0
  18. package/src/adapters/cli.ts +74 -0
  19. package/src/adapters/codex/events.ts +65 -0
  20. package/src/adapters/codex/home.ts +47 -0
  21. package/src/adapters/codex/index.ts +227 -0
  22. package/src/adapters/discovery.ts +87 -0
  23. package/src/adapters/opencode/agents.ts +84 -0
  24. package/src/adapters/opencode/events.ts +109 -0
  25. package/src/adapters/opencode/index.ts +316 -0
  26. package/src/adapters/registry.ts +14 -0
  27. package/src/cli.ts +159 -19
  28. package/src/domain/agents.ts +60 -0
  29. package/src/domain/budget.ts +45 -0
  30. package/src/domain/catalog.ts +262 -0
  31. package/src/domain/changes.ts +52 -0
  32. package/src/domain/cost.ts +89 -0
  33. package/src/domain/errors.ts +58 -0
  34. package/src/domain/hints.ts +22 -0
  35. package/src/domain/ids.ts +97 -0
  36. package/src/domain/jev.ts +283 -0
  37. package/src/domain/lane.ts +69 -0
  38. package/src/domain/profile-rules.ts +205 -0
  39. package/src/domain/profile.ts +383 -0
  40. package/src/domain/record.ts +84 -0
  41. package/src/{profile → domain}/role-prompts.ts +42 -25
  42. package/src/domain/roles.ts +25 -0
  43. package/src/domain/route.ts +126 -0
  44. package/src/domain/runtime.ts +21 -0
  45. package/src/domain/sanitize.ts +29 -0
  46. package/src/domain/secrets.ts +57 -0
  47. package/src/domain/select.ts +136 -0
  48. package/src/domain/state.ts +33 -0
  49. package/src/domain/util.ts +18 -0
  50. package/src/entry/capture-fixtures-command.ts +57 -0
  51. package/src/entry/catalog-command.ts +106 -0
  52. package/src/entry/cli-kit.ts +36 -0
  53. package/src/entry/deps.ts +16 -0
  54. package/src/entry/doctor-command.ts +45 -0
  55. package/src/entry/glyphs.ts +33 -0
  56. package/src/entry/init-command.ts +142 -0
  57. package/src/entry/lock-command.ts +123 -0
  58. package/src/entry/mcp/dispatch-tools.ts +82 -0
  59. package/src/entry/mcp/handshake.ts +48 -0
  60. package/src/entry/mcp/lane-tools.ts +83 -0
  61. package/src/entry/mcp/result.ts +51 -0
  62. package/src/entry/mcp/run-tools.ts +144 -0
  63. package/src/entry/mcp/server.ts +72 -0
  64. package/src/entry/mcp/setup-tools.ts +81 -0
  65. package/src/entry/profile-command.ts +320 -0
  66. package/src/entry/prompt.ts +103 -0
  67. package/src/entry/runs-command.ts +217 -0
  68. package/src/entry/supervise-bin.ts +18 -0
  69. package/src/entry/supervise-command.ts +37 -0
  70. package/src/entry/tui/commands.ts +545 -0
  71. package/src/entry/tui/effects.ts +270 -0
  72. package/src/entry/tui/fixtures.ts +318 -0
  73. package/src/entry/tui/profile-edits.ts +138 -0
  74. package/src/entry/tui/profile-tree.ts +419 -0
  75. package/src/entry/tui/providers/app.tsx +162 -0
  76. package/src/entry/tui/providers/boundary.tsx +57 -0
  77. package/src/entry/tui/providers/data.tsx +133 -0
  78. package/src/entry/tui/providers/keymap.tsx +166 -0
  79. package/src/entry/tui/providers/root.tsx +45 -0
  80. package/src/entry/tui/providers/theme.tsx +17 -0
  81. package/src/entry/tui/providers/toast.tsx +77 -0
  82. package/src/entry/tui/run.tsx +98 -0
  83. package/src/entry/tui/state.ts +336 -0
  84. package/src/entry/tui/stories.ts +75 -0
  85. package/src/entry/tui/text.ts +118 -0
  86. package/src/entry/tui/theme.ts +139 -0
  87. package/src/entry/tui/views/app.tsx +208 -0
  88. package/src/entry/tui/views/dialogs.tsx +41 -0
  89. package/src/entry/tui/views/profile-actions.ts +321 -0
  90. package/src/entry/tui/views/profiles.tsx +440 -0
  91. package/src/entry/tui/views/runs.tsx +281 -0
  92. package/src/entry/tui/views/save-dialog.tsx +332 -0
  93. package/src/entry/tui/views/status.tsx +206 -0
  94. package/src/entry/tui/widgets/chrome.tsx +114 -0
  95. package/src/entry/tui/widgets/dialog-confirm.tsx +58 -0
  96. package/src/entry/tui/widgets/dialog-prompt.tsx +52 -0
  97. package/src/entry/tui/widgets/dialog-select.tsx +212 -0
  98. package/src/entry/tui/widgets/dialog.tsx +68 -0
  99. package/src/entry/tui/widgets/line.tsx +92 -0
  100. package/src/entry/tui/widgets/list.tsx +196 -0
  101. package/src/entry/tui/widgets/spinner.tsx +49 -0
  102. package/src/entry/tui/widgets/toast.tsx +62 -0
  103. package/src/infra/assets.ts +4 -0
  104. package/src/infra/dispatch-dir.ts +278 -0
  105. package/src/infra/env.ts +43 -0
  106. package/src/infra/filelock.ts +159 -0
  107. package/src/infra/git.ts +91 -0
  108. package/src/infra/heavy-lock.ts +36 -0
  109. package/src/infra/jev-client.ts +162 -0
  110. package/src/infra/launch.ts +25 -0
  111. package/src/infra/log.ts +84 -0
  112. package/src/infra/paths.ts +38 -0
  113. package/src/infra/proc.ts +110 -0
  114. package/src/infra/store.ts +229 -0
  115. package/src/infra/supervisor.ts +257 -0
  116. package/src/infra/version.ts +6 -0
  117. package/src/services/admission.ts +277 -0
  118. package/src/services/agent-links.ts +139 -0
  119. package/src/services/backends.ts +44 -0
  120. package/src/services/budget.ts +47 -0
  121. package/src/services/capture.ts +233 -0
  122. package/src/services/catalog-service.ts +382 -0
  123. package/src/services/dispatch-service.ts +479 -0
  124. package/src/services/dispatches.ts +145 -0
  125. package/src/services/doctor-backends.ts +185 -0
  126. package/src/services/doctor-checks.ts +103 -0
  127. package/src/services/doctor.ts +358 -0
  128. package/src/services/finalize.ts +359 -0
  129. package/src/services/jev-service.ts +183 -0
  130. package/src/services/lane-service.ts +248 -0
  131. package/src/services/ports.ts +132 -0
  132. package/src/services/preflight.ts +207 -0
  133. package/src/services/profile-service.ts +292 -0
  134. package/src/services/profile-store.ts +141 -0
  135. package/src/services/reconcile.ts +101 -0
  136. package/src/services/routing-service.ts +190 -0
  137. package/src/services/run-debug.ts +79 -0
  138. package/src/services/run-service.ts +161 -0
  139. package/src/services/run-store.ts +316 -0
  140. package/src/services/setup.ts +87 -0
  141. package/src/services/state.ts +104 -0
  142. package/src/services/summary.ts +227 -0
  143. package/catalog/catalog.json +0 -112
  144. package/catalog/models-dev.json +0 -56453
  145. package/src/core/codex-events.ts +0 -49
  146. package/src/core/codex.ts +0 -166
  147. package/src/core/harness.ts +0 -89
  148. package/src/core/lanes.ts +0 -67
  149. package/src/core/lock.ts +0 -111
  150. package/src/core/opencode.ts +0 -207
  151. package/src/core/reconcile.ts +0 -26
  152. package/src/core/reply.ts +0 -54
  153. package/src/core/runstore.ts +0 -163
  154. package/src/core/spawn.ts +0 -89
  155. package/src/core/status.ts +0 -193
  156. package/src/files.ts +0 -19
  157. package/src/mcp/dispatch.ts +0 -154
  158. package/src/mcp/lane-tools.ts +0 -200
  159. package/src/mcp/out.ts +0 -6
  160. package/src/mcp/preflight.ts +0 -69
  161. package/src/mcp/run-tools.ts +0 -121
  162. package/src/mcp/runs.ts +0 -118
  163. package/src/mcp/server.ts +0 -26
  164. package/src/mcp/setup-tools.ts +0 -175
  165. package/src/paths.ts +0 -20
  166. package/src/profile/agents.ts +0 -130
  167. package/src/profile/profile.ts +0 -228
  168. package/src/routing/catalog.ts +0 -227
  169. package/src/routing/commands.ts +0 -22
  170. package/src/routing/jev.ts +0 -185
  171. package/src/routing/route.ts +0 -155
  172. package/src/routing/select.ts +0 -90
  173. package/src/tui/backends.ts +0 -57
  174. package/src/tui/commands.ts +0 -53
  175. package/src/tui/dashboard.tsx +0 -245
  176. package/src/tui/editor.tsx +0 -214
  177. package/src/tui/init.tsx +0 -208
  178. package/src/tui/matrix-model.ts +0 -279
  179. package/src/tui/matrix.tsx +0 -512
  180. package/src/tui/profiles.ts +0 -38
  181. package/src/tui/theme.ts +0 -177
  182. package/src/tui/ui.tsx +0 -267
  183. package/src/tui/watch-model.ts +0 -83
  184. package/src/tui/watch.tsx +0 -236
  185. package/src/types.ts +0 -184
  186. package/src/version.ts +0 -7
  187. /package/src/{mcp/commands.ts → entry/mcp/command.ts} +0 -0
@@ -0,0 +1,321 @@
1
+ import { isDeepStrictEqual } from "node:util";
2
+ import { errorMessage, isCatherdError } from "../../../domain/errors.ts";
3
+ import { PROFILE_NAME, patchBetween, type ProfileDoc } from "../../../domain/profile.ts";
4
+ import { CHANGED_ON_DISK } from "../effects.ts";
5
+ import { type AppApi, useApp, useDialogHandler } from "../providers/app.tsx";
6
+ import { type Data, useData } from "../providers/data.tsx";
7
+ import { errorToast } from "../providers/toast.tsx";
8
+ import { currentDraft, dirtyCount, isSaving } from "../state.ts";
9
+ import { plural } from "../text.ts";
10
+
11
+ const fail = (app: AppApi, e: unknown) => app.toast(errorToast(e));
12
+
13
+ /**
14
+ * Shows a profile in the Profiles tab, starting its draft from disk unless one is staged. A read that
15
+ * fails is a toast, or with `quiet` only returned (the caller shows it); null when it was shown.
16
+ */
17
+ export function showProfile(app: AppApi, name: string, o: { quiet?: boolean } = {}): unknown {
18
+ try {
19
+ app.dispatch({ type: "show", name, doc: app.effects.readProfile(name) });
20
+ return null;
21
+ } catch (e) {
22
+ if (!o.quiet) fail(app, e);
23
+ return e;
24
+ }
25
+ }
26
+
27
+ /**
28
+ * The profiles as they are now: the poll's value while its last read was good, else a read of them now
29
+ * (which throws as the poll's did, until the file is fixed); a good one also brings the poll up to date.
30
+ */
31
+ function fresh(app: AppApi, data: Data): NonNullable<Data["profiles"]["value"]> {
32
+ if (data.profiles.value && data.profiles.error === null) return data.profiles.value;
33
+ const p = app.effects.profiles();
34
+ if (data.profiles.error !== null) data.profiles.refresh();
35
+ return p;
36
+ }
37
+
38
+ /** The profile list (`<leader>l`): enter shows one, ctrl+d twice deletes one. */
39
+ export function openProfileList(app: AppApi, data: Data): void {
40
+ let p: NonNullable<Data["profiles"]["value"]>;
41
+ try {
42
+ // the poll has not read them yet, or its last read failed (a kept value would be stale): read them now;
43
+ // a key handler's throw would end the TUI
44
+ p = fresh(app, data);
45
+ } catch (e) {
46
+ return fail(app, e);
47
+ }
48
+ app.dispatch({
49
+ type: "open",
50
+ dialog: {
51
+ kind: "select",
52
+ purpose: { type: "profiles" },
53
+ title: "Profiles",
54
+ deletable: true,
55
+ empty: "no profiles",
56
+ options: p.names.map((n) => {
57
+ const d = app.getState().drafts[n];
58
+ const unsaved = d ? dirtyCount(d) : 0;
59
+ return {
60
+ value: n,
61
+ title: n,
62
+ current: n === p.here,
63
+ detail: [
64
+ n === p.active ? "active" : "",
65
+ p.repo && n === p.here ? "this repo" : "",
66
+ unsaved ? `${unsaved} unsaved` : "",
67
+ ]
68
+ .filter(Boolean)
69
+ .join(" · "),
70
+ };
71
+ }),
72
+ },
73
+ });
74
+ }
75
+
76
+ export const openNewProfile = (app: AppApi, from: string | null = null): void =>
77
+ app.dispatch({
78
+ type: "open",
79
+ dialog: {
80
+ kind: "prompt",
81
+ purpose: from ? { type: "copy", from } : { type: "new" },
82
+ title: from ? `Copy profile ${from}` : "New profile",
83
+ label: "name: lowercase letters, digits and -, up to 32",
84
+ value: "",
85
+ error: null,
86
+ },
87
+ });
88
+
89
+ /** Opens the save dialog, or says there is nothing to save. */
90
+ export function openSave(app: AppApi): void {
91
+ const d = currentDraft(app.getState());
92
+ if (!d || dirtyCount(d) === 0) return app.toast({ variant: "info", message: "No unsaved changes" });
93
+ app.dispatch({
94
+ type: "open",
95
+ dialog: { kind: "save", purpose: { type: "save", name: d.name }, error: null },
96
+ });
97
+ }
98
+
99
+ export function openActivate(app: AppApi, data: Data): void {
100
+ const d = currentDraft(app.getState());
101
+ if (!d) return;
102
+ let p: Data["profiles"]["value"];
103
+ try {
104
+ // a failed poll is read again here, so the prompt never names a stale active profile or binding
105
+ p = data.profiles.error === null ? data.profiles.value : fresh(app, data);
106
+ } catch (e) {
107
+ return fail(app, e);
108
+ }
109
+ askActivate(app, d.name, p);
110
+ }
111
+
112
+ /**
113
+ * The activate confirmation for `name`, worded for the scope `p` shows (its bound repo, or the global
114
+ * profile), which the answer then applies; `notice` says why it asks again.
115
+ */
116
+ function askActivate(app: AppApi, name: string, p: Data["profiles"]["value"], notice?: string): void {
117
+ if (p?.here === name)
118
+ return app.toast({
119
+ variant: "info",
120
+ message: p.repo ? `${name} is already this repo's profile` : `${name} is already active`,
121
+ });
122
+ const d = app.getState().drafts[name];
123
+ const unsaved = d ? dirtyCount(d) : 0;
124
+ app.dispatch({
125
+ type: "open",
126
+ dialog: {
127
+ kind: "confirm",
128
+ purpose: { type: "activate", name, repo: p?.repo ?? null },
129
+ title: p?.repo ? `Use ${name} in this repo?` : `Make ${name} active?`,
130
+ message: [
131
+ ...(notice ? [notice] : []),
132
+ p?.repo
133
+ ? `Binds ${p.repo} to it; the active profile (${p.active}) and other repos keep theirs.`
134
+ : "New Claude Code sessions and catherd's next dispatch use it; repos bound to another profile keep theirs.",
135
+ ...(unsaved
136
+ ? [`Its ${unsaved} unsaved change${unsaved === 1 ? " is" : "s are"} not part of it until you save.`]
137
+ : []),
138
+ ],
139
+ yes: p?.repo ? "Bind to this repo" : "Make active",
140
+ no: "Cancel",
141
+ destructive: false,
142
+ },
143
+ });
144
+ }
145
+
146
+ export function openRevert(app: AppApi): void {
147
+ const d = currentDraft(app.getState());
148
+ if (!d || dirtyCount(d) === 0) return app.toast({ variant: "info", message: "No unsaved changes" });
149
+ app.dispatch({
150
+ type: "open",
151
+ dialog: {
152
+ kind: "confirm",
153
+ purpose: { type: "revert", name: d.name },
154
+ title: "Discard unsaved changes?",
155
+ message: [
156
+ `${plural(dirtyCount(d), "change")} to ${d.name} go back to what is saved. ctrl+x u undoes this.`,
157
+ ],
158
+ yes: "Discard",
159
+ no: "Keep them",
160
+ destructive: true,
161
+ },
162
+ });
163
+ }
164
+
165
+ /** Keeps a line for after exit, and says it now. */
166
+ function sessionsNeeded(app: AppApi, agents: string[]): void {
167
+ if (agents.length === 0) return;
168
+ app.keep([`catherd: start a new Claude Code session to use: ${agents.join(", ")}`]);
169
+ app.toast({
170
+ variant: "info",
171
+ message: `New Claude Code session needed for ${plural(agents.length, "agent")}`,
172
+ });
173
+ }
174
+
175
+ /**
176
+ * Applies a confirmed activation to the scope the user was shown (`shown`: the bound repo, or null for the
177
+ * global profile). The binding is read again first: when another process bound or unbound this repo while
178
+ * the question was open, nothing is applied and the question comes back in the words for the scope now.
179
+ */
180
+ function activateNow(app: AppApi, data: Data, name: string, shown: string | null): void {
181
+ try {
182
+ const now = app.effects.profiles();
183
+ if (now.repo !== shown) {
184
+ data.profiles.refresh();
185
+ return askActivate(
186
+ app,
187
+ name,
188
+ now,
189
+ `Changed since this was first asked: ${now.repo ? `${now.repo} is now bound to ${now.here}` : `${shown} is no longer bound`}.`,
190
+ );
191
+ }
192
+ const r = app.effects.activate(name, shown);
193
+ data.profiles.refresh();
194
+ app.toast({ variant: "success", message: shown ? `${name} is bound to ${shown}` : `${name} is active` });
195
+ sessionsNeeded(app, r.newSessionNeededFor);
196
+ } catch (e) {
197
+ fail(app, e);
198
+ }
199
+ }
200
+
201
+ /** The handlers of the profile dialogs that any tab can open; the App registers them once. */
202
+ export function useProfileDialogs(): void {
203
+ const app = useApp();
204
+ const data = useData();
205
+ const nameError = (name: string): string | null => {
206
+ if (!PROFILE_NAME.test(name))
207
+ return "use lowercase letters, digits and -, up to 32, starting with a letter or digit";
208
+ if ((data.profiles.value?.names ?? []).includes(name)) return `"${name}" already exists`;
209
+ return null;
210
+ };
211
+ const create = (value: string, from?: string) => {
212
+ const name = value.trim();
213
+ const err = nameError(name);
214
+ if (err) return app.dispatch({ type: "invalid", error: err });
215
+ try {
216
+ const r = app.effects.create(name, from);
217
+ if (!r.saved)
218
+ return app.dispatch({
219
+ type: "invalid",
220
+ error: r.errors.map((e) => `${e.path}: ${e.message}`).join("; "),
221
+ });
222
+ data.profiles.refresh();
223
+ app.dispatch({ type: "close" });
224
+ showProfile(app, name);
225
+ app.toast({ variant: "success", message: `Created profile ${name}` });
226
+ sessionsNeeded(app, r.newSessionNeededFor);
227
+ } catch (e) {
228
+ app.dispatch({
229
+ type: "invalid",
230
+ error: isCatherdError(e) && e.fix ? `${e.message}. ${e.fix}` : errorMessage(e),
231
+ });
232
+ }
233
+ };
234
+ useDialogHandler("new", (_p, value) => create(value));
235
+ useDialogHandler("copy", (p, value) => create(value, p.type === "copy" ? p.from : undefined));
236
+ useDialogHandler("profiles", (_p, value) => {
237
+ if (!value.startsWith("delete:")) {
238
+ app.dispatch({ type: "close" });
239
+ showProfile(app, value);
240
+ return;
241
+ }
242
+ const name = value.slice("delete:".length);
243
+ try {
244
+ app.effects.remove(name);
245
+ app.dispatch({ type: "forget", name });
246
+ data.profiles.refresh();
247
+ app.dispatch({ type: "close" });
248
+ app.toast({ variant: "success", message: `Deleted profile ${name}` });
249
+ } catch (e) {
250
+ fail(app, e);
251
+ }
252
+ });
253
+ useDialogHandler("activate", (p) => {
254
+ app.dispatch({ type: "close" });
255
+ if (p.type === "activate") activateNow(app, data, p.name, p.repo);
256
+ });
257
+ useDialogHandler("revert", (p) => {
258
+ app.dispatch({ type: "close" });
259
+ if (p.type !== "revert") return;
260
+ const d = app.getState().drafts[p.name];
261
+ let doc: ProfileDoc;
262
+ try {
263
+ // "what is saved" is the file now: another process may have saved it since the draft opened
264
+ doc = app.effects.readProfile(p.name);
265
+ } catch (e) {
266
+ return fail(app, e);
267
+ }
268
+ app.dispatch({ type: "revert", name: p.name, doc });
269
+ if (d && !isDeepStrictEqual(d.base, doc))
270
+ app.toast({
271
+ variant: "info",
272
+ message: `${p.name} changed on disk: showing what is saved now`,
273
+ });
274
+ });
275
+ useDialogHandler("save", async (p, value, extra) => {
276
+ if (p.type !== "save") return;
277
+ // the scope "Save & make active" was worded for as it was answered
278
+ const shown = data.profiles.value?.repo ?? null;
279
+ const d = app.getState().drafts[p.name];
280
+ if (!d) return app.dispatch({ type: "close" });
281
+ // a second enter while the first save is writing is not a second save
282
+ if (isSaving(app.getState())) return;
283
+ // while it writes the dialog stays open and takes no answer, and the TUI does not quit
284
+ app.dispatch({ type: "saving", name: p.name, on: true });
285
+ let r: Awaited<ReturnType<AppApi["effects"]["save"]>>;
286
+ try {
287
+ r = await app.effects.save(p.name, patchBetween(d.base, d.doc), d.treatLikes, extra.shown);
288
+ } catch (e) {
289
+ app.dispatch({ type: "saving", name: p.name, on: false });
290
+ return app.dispatch({
291
+ type: "invalid",
292
+ error: isCatherdError(e) && e.fix ? `${e.message}. ${e.fix}` : errorMessage(e),
293
+ });
294
+ }
295
+ app.dispatch({ type: "saving", name: p.name, on: false });
296
+ // another process changed the profile while this one saved: nothing is written, and the dialog shows
297
+ // what saving would do now and asks again
298
+ if (!r.saved && r.errors.some((e) => e.path === CHANGED_ON_DISK) && extra.changed) {
299
+ app.dispatch({ type: "invalid", error: null });
300
+ return extra.changed();
301
+ }
302
+ if (!r.saved)
303
+ return app.dispatch({
304
+ type: "invalid",
305
+ error: r.errors.map((e) => `${e.path}: ${e.message}`).join("; "),
306
+ });
307
+ // saved: this dialog closes (only if it is still the top one), so what fails from here on is a toast
308
+ const top = app.getState().dialogs.at(-1);
309
+ if (top?.kind === "save" && top.purpose.name === p.name) app.dispatch({ type: "close" });
310
+ try {
311
+ // the draft as the save began: an edit made since stays staged over what was saved
312
+ const from = { doc: d.doc, treatLikes: d.treatLikes };
313
+ app.dispatch({ type: "saved", name: p.name, doc: app.effects.readProfile(p.name), from });
314
+ } catch (e) {
315
+ fail(app, e);
316
+ }
317
+ app.toast({ variant: "success", message: `Saved profile ${p.name}` });
318
+ sessionsNeeded(app, r.newSessionNeededFor);
319
+ if (value === "activate") activateNow(app, data, p.name, shown);
320
+ });
321
+ }