sitevision-cli 1.0.0-beta.31 → 1.0.0-beta.32

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 CHANGED
@@ -1,5 +1,25 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.0.0-beta.32
4
+
5
+ - Sitevision CLI can now create new apps. **New app** in the command palette
6
+ runs Sitevision's own `create-sitevision-app` as a managed experience and
7
+ guides you through the setup: pick a name and a folder, then answer the
8
+ tool's questions right in `svc`. What your workspace already knows (domain,
9
+ site name, username) is filled in for you, and passwords never end up in a
10
+ file.
11
+ - When the app is created, `svc` fills in its manifest (the name, plus author
12
+ and help URL from the workspace's `package.json`) and offers to create the
13
+ addon on the site, so the app is ready for its first deploy.
14
+ - If the tool cannot be run this way, `svc` hands it the terminal and picks up
15
+ again when it is done.
16
+ - The Config tab has a MANIFEST section for editing `manifest.json`: id,
17
+ version, name, description, author and help URL, one row per language for
18
+ localized fields. Comments and formatting in the file are kept.
19
+ - `y` (sync `package.json`) now also mirrors the manifest's `version`,
20
+ `description` and `author` into the app's `package.json`. An app whose
21
+ `package.json` lacks them shows as out of sync until `y` is pressed.
22
+
3
23
  ## 1.0.0-beta.31
4
24
 
5
25
  - The help panel (`?`) now starts with a short guide to the pane you are in:
package/dist/cli.js CHANGED
@@ -157,8 +157,26 @@ async function runShell(apps, workspaceRoot, updatedFrom) {
157
157
  await playIntro(art);
158
158
  process.stdout.write('\x1b[2J\x1b[H');
159
159
  }
160
- const app = render(_jsx(Shell, { apps: apps, workspaceRoot: workspaceRoot, version: pkg.version, minimal: cli.flags.minimal, updatedFrom: updatedFrom }));
161
- await app.waitUntilExit();
160
+ // A handover unmounts the shell, runs its job on the normal screen with
161
+ // the terminal to itself, and starts the shell again.
162
+ for (;;) {
163
+ let job;
164
+ const app = render(_jsx(Shell, { apps: apps, workspaceRoot: workspaceRoot, version: pkg.version, minimal: cli.flags.minimal, updatedFrom: updatedFrom, handover: next => {
165
+ job = next;
166
+ app.unmount();
167
+ } }));
168
+ // eslint-disable-next-line no-await-in-loop
169
+ await app.waitUntilExit();
170
+ if (!job)
171
+ break;
172
+ process.stdout.write('\x1b[?1049l');
173
+ // eslint-disable-next-line no-await-in-loop
174
+ await job();
175
+ if (workspaceRoot)
176
+ apps = discoverApps(workspaceRoot);
177
+ updatedFrom = undefined;
178
+ process.stdout.write('\x1b[?1049h\x1b[2J\x1b[H');
179
+ }
162
180
  }
163
181
  finally {
164
182
  process.stdout.write('\x1b[?1049l');
@@ -0,0 +1,6 @@
1
+ type Answers = Record<string, unknown>;
2
+ declare function prompt(questions: unknown, initial?: Answers): Promise<Answers>;
3
+ declare const inquirer: {
4
+ prompt: typeof prompt;
5
+ };
6
+ export default inquirer;
@@ -0,0 +1,119 @@
1
+ /**
2
+ * Stands in for inquirer inside the scaffolder: every question goes to svc over
3
+ * the socket in SVC_PROMPT_SOCKET and the reply comes back as the answer. `when`,
4
+ * `filter`, `validate` and function-valued fields run here, where they live.
5
+ * Self-contained on purpose: it is loaded by the tool's node, not by svc.
6
+ */
7
+ import net from 'node:net';
8
+ import process from 'node:process';
9
+ import readline from 'node:readline';
10
+ const CHOICE_TYPES = new Set(['list', 'rawlist', 'expand', 'select', 'search']);
11
+ const evaluate = async (field, ...args) => typeof field === 'function' ? (await field(...args)) : field;
12
+ const text = (value) => typeof value === 'string' ? value : JSON.stringify(value);
13
+ function normalizeChoices(raw) {
14
+ if (!Array.isArray(raw))
15
+ return [];
16
+ const choices = [];
17
+ for (const entry of raw) {
18
+ if (entry === null || typeof entry !== 'object') {
19
+ choices.push({ name: text(entry), value: entry, checked: false });
20
+ continue;
21
+ }
22
+ const choice = entry;
23
+ if (choice['type'] === 'separator' || choice['disabled'])
24
+ continue;
25
+ const value = 'value' in choice ? choice['value'] : choice['name'];
26
+ choices.push({
27
+ name: text(choice['name'] ?? value),
28
+ value,
29
+ checked: Boolean(choice['checked']),
30
+ });
31
+ }
32
+ return choices;
33
+ }
34
+ /** Ask svc until the tool's own validation accepts; undefined when skipped. */
35
+ async function askOne(question, answers, exchange) {
36
+ const type = text(question['type'] ?? 'input');
37
+ const choices = normalizeChoices(await evaluate(question['choices'], answers));
38
+ const fallback = await evaluate(question['default'], answers);
39
+ const values = choices.map(choice => choice.value);
40
+ const message = text(await evaluate(question['message'], answers));
41
+ let initial = fallback;
42
+ if (type === 'checkbox') {
43
+ const preset = Array.isArray(fallback) ? fallback : [];
44
+ initial = choices.flatMap((choice, i) => choice.checked || preset.includes(choice.value) ? [i] : []);
45
+ }
46
+ else if (CHOICE_TYPES.has(type)) {
47
+ // Inquirer takes a default as either the value or its index.
48
+ const at = values.indexOf(fallback);
49
+ initial = at === -1 && typeof fallback === 'number' ? fallback : at;
50
+ initial = Math.max(0, initial);
51
+ }
52
+ let error;
53
+ for (;;) {
54
+ // eslint-disable-next-line no-await-in-loop
55
+ const reply = await exchange({
56
+ name: question.name,
57
+ type,
58
+ message,
59
+ choices: choices.map(choice => choice.name),
60
+ default: initial,
61
+ error,
62
+ });
63
+ if (reply.skip)
64
+ return undefined;
65
+ let { value } = reply;
66
+ if (type === 'checkbox')
67
+ value = value.map(i => values[i]);
68
+ else if (CHOICE_TYPES.has(type))
69
+ value = values[value];
70
+ else if (type === 'number')
71
+ value = Number(value);
72
+ // eslint-disable-next-line no-await-in-loop
73
+ value = await evaluate(question['filter'] ?? value, value, answers);
74
+ // eslint-disable-next-line no-await-in-loop
75
+ const valid = await evaluate(question['validate'] ?? true, value, answers);
76
+ if (valid === true)
77
+ return value;
78
+ error = typeof valid === 'string' ? valid : 'Invalid value';
79
+ }
80
+ }
81
+ async function prompt(questions, initial = {}) {
82
+ const list = Array.isArray(questions)
83
+ ? questions
84
+ : typeof questions.name === 'string'
85
+ ? [questions]
86
+ : Object.entries(questions).map(([name, question]) => ({ ...question, name }));
87
+ const socket = net.connect(process.env['SVC_PROMPT_SOCKET']);
88
+ const lines = readline.createInterface({ input: socket });
89
+ const replies = lines[Symbol.asyncIterator]();
90
+ const exchange = async (message) => {
91
+ socket.write(JSON.stringify(message) + '\n');
92
+ const next = await replies.next();
93
+ if (next.done)
94
+ throw new Error('svc closed the prompt');
95
+ return JSON.parse(next.value);
96
+ };
97
+ // ponytail: dotted names ("a.b") stay flat keys; nest them if a question ever uses one.
98
+ const answers = { ...initial };
99
+ try {
100
+ for (const question of list) {
101
+ const wanted = question['when'] === undefined ||
102
+ // eslint-disable-next-line no-await-in-loop
103
+ Boolean(await evaluate(question['when'], answers));
104
+ const answered = answers[question.name] !== undefined && !question['askAnswered'];
105
+ if (!wanted || answered)
106
+ continue;
107
+ // eslint-disable-next-line no-await-in-loop
108
+ const value = await askOne(question, answers, exchange);
109
+ if (value !== undefined)
110
+ answers[question.name] = value;
111
+ }
112
+ }
113
+ finally {
114
+ socket.end();
115
+ }
116
+ return answers;
117
+ }
118
+ const inquirer = { prompt };
119
+ export default inquirer;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,15 @@
1
+ // Loaded through NODE_OPTIONS into every process the scaffolder starts: hands
2
+ // sitevision-scripts svc's prompt bridge in place of inquirer.
3
+ import module from 'node:module';
4
+ import path from 'node:path';
5
+ const stub = new URL(`inquirer-stub${path.extname(import.meta.url)}`, import.meta.url).href;
6
+ // Missing before Node 22.15; svc then finds the run unmanaged and falls back.
7
+ module.registerHooks?.({
8
+ resolve(specifier, context, nextResolve) {
9
+ if (specifier === 'inquirer' &&
10
+ context.parentURL?.includes('sitevision-scripts')) {
11
+ return { url: stub, shortCircuit: true };
12
+ }
13
+ return nextResolve(specifier, context);
14
+ },
15
+ });
@@ -1,4 +1,4 @@
1
- import type { DevProperties } from '../types/index.js';
1
+ import type { DevProperties, SitevisionManifest } from '../types/index.js';
2
2
  type Method = 'basic' | 'oauth2' | 'cookie';
3
3
  interface Field {
4
4
  key: string;
@@ -10,7 +10,11 @@ interface Field {
10
10
  hint?: string;
11
11
  perApp?: boolean;
12
12
  help: string;
13
+ manifestKey?: string;
14
+ lang?: string;
13
15
  }
16
+ /** Rows for manifest.json: one per text field, one per language of a localized one. */
17
+ export declare function manifestFields(manifest?: SitevisionManifest): Field[];
14
18
  type Values = Record<string, string>;
15
19
  /** What the form edits: an app, or the workspace root (no addon, no package.json). */
16
20
  export interface ConfigTarget {
@@ -20,6 +24,8 @@ export interface ConfigTarget {
20
24
  environment?: string;
21
25
  workspace?: boolean;
22
26
  workspaceRoot?: string;
27
+ manifest?: SitevisionManifest;
28
+ manifestPath?: string;
23
29
  }
24
30
  /** Apply the form to disk and the keychain. Exported for the test. */
25
31
  export declare function saveConfig(project: ConfigTarget, values: Values, edited: Set<string>): void;
@@ -1,7 +1,7 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useEffect, useState } from 'react';
3
3
  import { Box, Text, useInput } from 'ink';
4
- import { findDevPropertiesPath, getPackageJsonSyncChanges, hasPackageJson, IMPLICIT_VALUES, normalizeDomain, readAncestorDevProperties, readInheritedDevProperties, readWorkspaceDevProperties, updatePackageJson, writeDevProperties, } from '../utils/project-detection.js';
4
+ import { findDevPropertiesPath, getPackageJsonSyncChanges, hasPackageJson, IMPLICIT_VALUES, normalizeDomain, readAncestorDevProperties, readInheritedDevProperties, readWorkspaceDevProperties, updatePackageJson, writeDevProperties, writeManifestField, } from '../utils/project-detection.js';
5
5
  import { setDeployPassword, deleteDeployPassword, getOAuth2ClientSecret, setOAuth2ClientSecret, deleteOAuth2ClientSecret, getSigningPassword, setSigningPassword, deleteSigningPassword, } from '../utils/keychain.js';
6
6
  import { DEFAULT_SCOPES, discoverOAuth2Config } from '../utils/oauth2-auth.js';
7
7
  import { ACCENT } from './Frame.js';
@@ -132,6 +132,61 @@ const FIELDS = [
132
132
  hint: 'blank = prompt each run',
133
133
  },
134
134
  ];
135
+ const MANIFEST_FIELDS = [
136
+ {
137
+ key: 'id',
138
+ label: 'App id',
139
+ required: true,
140
+ help: 'Identifier of the app in Sitevision; it also names the zip. Changing it makes the next deploy a new app instead of an update.',
141
+ },
142
+ {
143
+ key: 'version',
144
+ label: 'Version',
145
+ required: true,
146
+ help: 'Version of the app. Id and version together identify an upload, so bump it to deploy a new version instead of overwriting the current one.',
147
+ },
148
+ {
149
+ key: 'name',
150
+ label: 'Name',
151
+ required: true,
152
+ help: 'Name shown when importing and administering the app in the Sitevision editor. A multilingual manifest needs at least English.',
153
+ },
154
+ {
155
+ key: 'description',
156
+ label: 'Description',
157
+ help: 'Short description shown next to the name in the Sitevision editor.',
158
+ },
159
+ {
160
+ key: 'author',
161
+ label: 'Author',
162
+ help: 'Who made the app, shown in the Sitevision editor.',
163
+ },
164
+ {
165
+ key: 'helpUrl',
166
+ label: 'Help URL',
167
+ help: "Link to the app's documentation, shown in the Sitevision editor.",
168
+ },
169
+ ];
170
+ /** Rows for manifest.json: one per text field, one per language of a localized one. */
171
+ export function manifestFields(manifest) {
172
+ if (!manifest)
173
+ return [];
174
+ const raw = manifest;
175
+ return MANIFEST_FIELDS.flatMap(f => {
176
+ const value = raw[f.key];
177
+ const base = { ...f, section: 'MANIFEST', manifestKey: f.key };
178
+ if (value && typeof value === 'object') {
179
+ return Object.keys(value).map(lang => ({
180
+ ...base,
181
+ key: `manifest.${f.key}.${lang}`,
182
+ lang,
183
+ required: f.required && lang === 'en',
184
+ }));
185
+ }
186
+ return [{ ...base, key: `manifest.${f.key}` }];
187
+ });
188
+ }
189
+ const labelOf = (f) => t(f.label) + (f.lang ? ` (${f.lang})` : '');
135
190
  const ENV_KEYS = new Set([
136
191
  'domain',
137
192
  'siteName',
@@ -170,6 +225,13 @@ function fromProject(project) {
170
225
  signingUsername: dev.signingUsername ?? '',
171
226
  certificateName: dev.certificateName ?? '',
172
227
  signingPassword: '',
228
+ ...Object.fromEntries(manifestFields(project.manifest).map(f => {
229
+ const value = project.manifest[f.manifestKey];
230
+ const text = f.lang
231
+ ? value[f.lang]
232
+ : value;
233
+ return [f.key, typeof text === 'string' ? text : ''];
234
+ })),
173
235
  };
174
236
  }
175
237
  function storedSecret(project, key) {
@@ -325,28 +387,41 @@ export function ConfigForm({ project, active, width, height, pickAddon, onSaved,
325
387
  const envMode = Boolean(project.environment &&
326
388
  project.environment !==
327
389
  baseEnvironment(project.base ?? project.devProperties));
328
- const fields = visibleFields(method, project.workspace, envMode);
390
+ const fields = [
391
+ ...visibleFields(method, project.workspace, envMode),
392
+ ...manifestFields(project.manifest),
393
+ ];
329
394
  const current = fields[Math.min(cursor, fields.length - 1)];
330
395
  const inherited = readInheritedDevProperties(project.root);
331
396
  const ancestors = readAncestorDevProperties(project.root);
332
397
  const changes = getPackageJsonSyncChanges(project.root);
333
398
  const packageJsonExists = hasPackageJson(project.root);
334
399
  // Write one field to disk (and the keychain for secrets) right away.
335
- const commit = (key, value, label = current.label) => {
400
+ const commit = (key, value, label = labelOf(current)) => {
336
401
  const clean = key === 'domain' ? normalizeDomain(value) : value;
402
+ const field = fields.find(f => f.key === key);
403
+ if (field?.manifestKey && field.required && !clean) {
404
+ setNote(t('Not saved: {label} is required.', { label }));
405
+ return;
406
+ }
337
407
  const next = { ...values, [key]: clean };
338
408
  setValues(next);
339
409
  try {
340
- saveConfig(project, next, new Set([key]));
410
+ if (field?.manifestKey) {
411
+ writeManifestField(project.manifestPath, field.manifestKey, clean, field.lang);
412
+ }
413
+ else {
414
+ saveConfig(project, next, new Set([key]));
415
+ }
341
416
  }
342
417
  catch (error) {
343
418
  setNote(t('Not saved: {error}', { error: errorText(error) }));
344
419
  return;
345
420
  }
346
421
  setNote(clean === value
347
- ? t('Saved {label}.', { label: t(label) })
422
+ ? t('Saved {label}.', { label })
348
423
  : t('Saved {label} as {value} — a domain is a host only.', {
349
- label: t(label),
424
+ label,
350
425
  value: clean,
351
426
  }));
352
427
  onSaved();
@@ -394,7 +469,7 @@ export function ConfigForm({ project, active, width, height, pickAddon, onSaved,
394
469
  void pickAddon().then(name => {
395
470
  if (name) {
396
471
  setEditing(false);
397
- commit('addonName', name, 'Addon name');
472
+ commit('addonName', name, t('Addon name'));
398
473
  }
399
474
  });
400
475
  };
@@ -468,6 +543,8 @@ export function ConfigForm({ project, active, width, height, pickAddon, onSaved,
468
543
  if (f.kind === 'secret') {
469
544
  return { text: storedSecret(project, f.key) ? t('keychain') : '—' };
470
545
  }
546
+ if (f.manifestKey)
547
+ return { text: (values[f.key] ?? '') ? 'manifest.json' : '' };
471
548
  const value = f.kind === 'bool' ? values[f.key] === 'yes' : values[f.key];
472
549
  if (envMode && ENV_KEYS.has(f.key)) {
473
550
  const override = project.base?.environments?.[project.environment];
@@ -507,12 +584,15 @@ export function ConfigForm({ project, active, width, height, pickAddon, onSaved,
507
584
  // label column (24) + source column + paddings; never below 20.
508
585
  const valueWidth = Math.max(20, width - 24 - SOURCE_WIDTH - 2);
509
586
  const rows = [];
587
+ let focusRow = 0;
510
588
  let lastSection;
511
589
  for (const f of fields) {
512
590
  if (f.section && f.section !== lastSection) {
513
- rows.push(_jsx(Box, { marginTop: 1, flexShrink: 0, children: _jsx(Text, { bold: true, dimColor: true, children: t(f.section) }) }, `s-${f.section}`));
591
+ rows.push(_jsx(Box, { height: 1, flexShrink: 0 }, `g-${f.section}`), _jsx(Box, { height: 1, flexShrink: 0, children: _jsx(Text, { bold: true, dimColor: true, children: t(f.section) }) }, `s-${f.section}`));
514
592
  lastSection = f.section;
515
593
  }
594
+ if (f === current)
595
+ focusRow = rows.length;
516
596
  const focused = active && f === current;
517
597
  const typing = focused && editing;
518
598
  // A value being typed into, scrolled so the caret stays in view and drawn
@@ -544,9 +624,13 @@ export function ConfigForm({ project, active, width, height, pickAddon, onSaved,
544
624
  display = typing ? (withCaret(draft)) : value ? (_jsx(Text, { children: value })) : (_jsx(Text, { dimColor: true, children: f.hint ? t(f.hint) : '—' }));
545
625
  }
546
626
  const src = source(f);
547
- rows.push(_jsxs(Box, { height: 1, flexShrink: 0, children: [_jsx(Text, { color: focused ? ACCENT : undefined, bold: focused, dimColor: !focused, children: (focused ? '▸ ' : ' ') + t(f.label).padEnd(22) }), _jsx(Box, { width: valueWidth, flexShrink: 0, children: _jsx(Text, { wrap: "truncate", children: display }) }), _jsx(Box, { width: SOURCE_WIDTH, flexShrink: 0, children: _jsx(Text, { dimColor: !src.color, color: src.color, wrap: "truncate", children: focused && f.hint && f.key === 'addonName' ? t(f.hint) : src.text }) })] }, f.key));
627
+ rows.push(_jsxs(Box, { height: 1, flexShrink: 0, children: [_jsx(Text, { color: focused ? ACCENT : undefined, bold: focused, dimColor: !focused, children: (focused ? '▸ ' : ' ') + labelOf(f).padEnd(22) }), _jsx(Box, { width: valueWidth, flexShrink: 0, children: _jsx(Text, { wrap: "truncate", children: display }) }), _jsx(Box, { width: SOURCE_WIDTH, flexShrink: 0, children: _jsx(Text, { dimColor: !src.color, color: src.color, wrap: "truncate", children: focused && f.hint && f.key === 'addonName' ? t(f.hint) : src.text }) })] }, f.key));
548
628
  }
549
- return (_jsxs(Box, { flexDirection: "column", paddingX: 1, overflow: "hidden", height: height, children: [_jsx(Box, { height: 1, flexShrink: 0, children: _jsxs(Text, { dimColor: true, wrap: "truncate", children: [(' ' + t('FIELD')).padEnd(24), t('VALUE').padEnd(valueWidth), t('SOURCE')] }) }), rows, project.workspace && (_jsx(Box, { marginTop: 1, flexShrink: 0, children: _jsx(Text, { dimColor: true, children: t("Shared by every app below {root}. An app's own value wins.", {
629
+ // ponytail: the help panel is assumed to wrap to three lines; measure it if
630
+ // a narrow pane ever clips the note.
631
+ const room = Math.max(3, height - 9 - changes.length - (project.workspace ? 2 : 0));
632
+ const firstRow = Math.max(0, Math.min(focusRow - Math.floor(room / 2), rows.length - room));
633
+ return (_jsxs(Box, { flexDirection: "column", paddingX: 1, overflow: "hidden", height: height, children: [_jsx(Box, { height: 1, flexShrink: 0, children: _jsxs(Text, { dimColor: true, wrap: "truncate", children: [(' ' + t('FIELD')).padEnd(24), t('VALUE').padEnd(valueWidth), t('SOURCE')] }) }), rows.slice(firstRow, firstRow + room), project.workspace && (_jsx(Box, { marginTop: 1, flexShrink: 0, children: _jsx(Text, { dimColor: true, children: t("Shared by every app below {root}. An app's own value wins.", {
550
634
  root: project.root,
551
635
  }) }) })), _jsxs(Box, { marginTop: 1, flexDirection: "column", flexShrink: 0, children: [_jsxs(Text, { bold: true, dimColor: true, children: [t('PACKAGE.JSON SYNC'), ' ', _jsx(Text, { color: changes.length > 0 || !packageJsonExists ? 'yellow' : 'green', children: !packageJsonExists
552
636
  ? t('no workspace package.json · y to set up')
@@ -554,5 +638,5 @@ export function ConfigForm({ project, active, width, height, pickAddon, onSaved,
554
638
  ? t('in sync')
555
639
  : changes.length === 1
556
640
  ? t('1 diff · y to apply')
557
- : t('{n} diffs · y to apply', { n: changes.length }) })] }), changes.map(c => (_jsxs(Text, { wrap: "truncate", children: [_jsx(Text, { color: c.from === undefined ? 'green' : 'yellow', children: c.from === undefined ? '+ ' : '~ ' }), c.key, ": ", c.from !== undefined && _jsxs(Text, { dimColor: true, children: [c.from, " \u2192 "] }), c.to] }, c.key)))] }), _jsx(Box, { flexGrow: 1 }), _jsx(Box, { flexDirection: "column", borderStyle: "single", borderDimColor: true, borderLeft: false, borderRight: false, borderBottom: false, children: _jsxs(Text, { wrap: "wrap", children: [_jsx(Text, { bold: true, color: ACCENT, children: t(current.label) }), _jsxs(Text, { dimColor: true, children: [" \u00B7 ", t(current.help)] })] }) }), _jsx(Text, { color: "yellow", children: note })] }));
641
+ : t('{n} diffs · y to apply', { n: changes.length }) })] }), changes.map(c => (_jsxs(Text, { wrap: "truncate", children: [_jsx(Text, { color: c.from === undefined ? 'green' : 'yellow', children: c.from === undefined ? '+ ' : '~ ' }), c.key, ": ", c.from !== undefined && _jsxs(Text, { dimColor: true, children: [c.from, " \u2192 "] }), c.to] }, c.key)))] }), _jsx(Box, { flexGrow: 1 }), _jsx(Box, { flexDirection: "column", borderStyle: "single", borderDimColor: true, borderLeft: false, borderRight: false, borderBottom: false, children: _jsxs(Text, { wrap: "wrap", children: [_jsx(Text, { bold: true, color: ACCENT, children: labelOf(current) }), _jsxs(Text, { dimColor: true, children: [" \u00B7 ", t(current.help)] })] }) }), _jsx(Text, { color: "yellow", children: note })] }));
558
642
  }
@@ -5,6 +5,7 @@ interface Props {
5
5
  version: string;
6
6
  minimal?: boolean;
7
7
  updatedFrom?: string;
8
+ handover?: (job: () => Promise<void>) => void;
8
9
  }
9
- export declare function Shell({ apps: initialApps, workspaceRoot, version, minimal, updatedFrom, }: Props): import("react").JSX.Element;
10
+ export declare function Shell({ apps: initialApps, workspaceRoot, version, minimal, updatedFrom, handover, }: Props): import("react").JSX.Element;
10
11
  export {};