sitevision-cli 1.0.0-beta.30 → 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,33 @@
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
+
23
+ ## 1.0.0-beta.31
24
+
25
+ - The help panel (`?`) now starts with a short guide to the pane you are in:
26
+ what it shows, how the values relate, and which keys apply. The Config guide
27
+ explains how local, shared and workspace settings layer on top of each
28
+ other. The panel scrolls when it does not fit.
29
+ - Updated dependencies.
30
+
3
31
  ## 1.0.0-beta.30
4
32
 
5
33
  - Dev and watch no longer rebuild on file events that change nothing, such as
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
  }
@@ -1,6 +1,9 @@
1
1
  import { type Hint } from './Frame.js';
2
- export declare function HelpPanel({ here, height, onClose, }: {
2
+ import type { Tab } from './actions.js';
3
+ export type Where = Tab | 'nav' | 'workspace';
4
+ export declare function HelpPanel({ here, where, height, onClose, }: {
3
5
  here: Hint[];
6
+ where: Where;
4
7
  height: number;
5
8
  onClose: () => void;
6
9
  }): import("react").JSX.Element;
@@ -1,9 +1,56 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { Box, Text, useInput } from 'ink';
2
+ import { useEffect, useRef, useState } from 'react';
3
+ import { Box, Text, measureElement, useInput } from 'ink';
3
4
  import { t } from '../utils/i18n.js';
4
5
  import { ACCENT } from './Frame.js';
5
6
  import { actions } from './actions.js';
6
7
  import { GROUPS } from './CommandPalette.js';
8
+ // What the pane you are in does, beyond the key list. One entry per paragraph.
9
+ const GUIDE = {
10
+ nav: {
11
+ title: 'Navigator',
12
+ text: [
13
+ 'Typing filters the app list. ↑↓ move, Enter selects the app and moves focus to the content pane. Action keys such as d or p only work there; in the navigator they search.',
14
+ 'The last row is Workspace settings: the shared root config that every app inherits.',
15
+ ],
16
+ },
17
+ workspace: {
18
+ title: 'Workspace settings',
19
+ text: [
20
+ "The root .dev_properties.json shared by every app, with the root package.json defaults underneath. Fill in site, auth and environments once; an app's own config overrides only what differs.",
21
+ 'Same keys as the Config tab: Enter edits and saves, Esc cancels, ←→ or space cycles choices.',
22
+ ],
23
+ },
24
+ overview: {
25
+ title: 'Overview',
26
+ text: [
27
+ 'Each status line names the key that fixes it: i installs dependencies, e opens Config, y syncs package.json, / sets up signing.',
28
+ 'The badge in the top bar is the active environment: green for the base, yellow for others, red for production. v cycles it.',
29
+ ],
30
+ },
31
+ config: {
32
+ title: 'Config',
33
+ text: [
34
+ 'Values come in layers and the source is shown next to each: your local .dev_properties.json wins, shared package.json values sit underneath, and in a workspace the root config underneath that. Environments add on top of the base and override only what differs.',
35
+ 'Enter edits and saves a field, Esc cancels, ←→ or space cycles choices, Ctrl+O lists the addons on the site. y copies shared values into package.json for the team.',
36
+ 'Passwords and secrets go to the OS keychain, never to a file. Leave them empty to be asked on each run.',
37
+ ],
38
+ },
39
+ versions: {
40
+ title: 'Versions',
41
+ text: [
42
+ "The versions uploaded to the addon on the active environment's site, the domain in the top bar. v switches environment.",
43
+ '↑↓ select, a activates the selected version, r refreshes. Production deploys activate on their own.',
44
+ ],
45
+ },
46
+ log: {
47
+ title: 'Log',
48
+ text: [
49
+ 'Output from build, sign, deploy, dev and watch for the selected app. Dev and watch keep running while you switch apps; K stops them.',
50
+ '↑↓ scroll, PgUp/PgDn page, f jumps to the end, x toggles line wrap.',
51
+ ],
52
+ },
53
+ };
7
54
  const MOVE = [
8
55
  ['Tab', 'switch pane'],
9
56
  ['1–4', 'tabs'],
@@ -15,17 +62,34 @@ const MOVE = [
15
62
  function Row({ keys, label }) {
16
63
  return (_jsxs(Text, { wrap: "truncate", children: [_jsx(Text, { bold: true, color: ACCENT, children: keys.padEnd(6) }), label] }));
17
64
  }
18
- export function HelpPanel({ here, height, onClose, }) {
65
+ export function HelpPanel({ here, where, height, onClose, }) {
66
+ const inner = useRef(null);
67
+ const [top, setTop] = useState(0);
68
+ const [total, setTotal] = useState(0);
69
+ useEffect(() => {
70
+ if (inner.current)
71
+ setTotal(measureElement(inner.current).height);
72
+ });
73
+ const visible = Math.max(1, height - 1);
74
+ const max = Math.max(0, total - visible);
19
75
  useInput((input, key) => {
20
76
  if (key.escape || key.return || input === '?' || input === 'q')
21
77
  onClose();
78
+ else if (key.upArrow)
79
+ setTop(n => Math.max(0, n - 1));
80
+ else if (key.downArrow)
81
+ setTop(n => Math.min(max, n + 1));
82
+ else if (key.pageUp)
83
+ setTop(n => Math.max(0, n - visible));
84
+ else if (key.pageDown)
85
+ setTop(n => Math.min(max, n + visible));
22
86
  });
23
87
  // Only keys not already listed elsewhere, e.g. f and x on the Log tab.
24
88
  const local = here.filter(hint => actions.every(action => action.key !== hint.key) &&
25
89
  MOVE.every(([keys]) => keys !== hint.key));
26
- return (_jsxs(Box, { flexDirection: "column", paddingX: 1, height: height, overflow: "hidden", children: [_jsxs(Text, { children: [_jsx(Text, { bold: true, children: t('Keys') }), _jsxs(Text, { dimColor: true, children: [" \u00B7 ", t('Esc close')] })] }), _jsxs(Box, { flexWrap: "wrap", columnGap: 4, children: [_jsx(Box, { flexDirection: "column", marginTop: 1, children: GROUPS.map(group => (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, dimColor: true, children: t(group.label) }), actions.flatMap(action => action.group === group.id && action.key
27
- ? [
28
- _jsx(Row, { keys: action.key, label: t(action.label) }, action.id),
29
- ]
30
- : [])] }, group.id))) }), _jsxs(Box, { flexDirection: "column", marginTop: 1, children: [local.length > 0 && (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsx(Text, { bold: true, dimColor: true, children: t('Here') }), local.map(hint => (_jsx(Row, { keys: hint.key, label: hint.label }, hint.key + hint.label)))] })), _jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, dimColor: true, children: t('Move around') }), MOVE.map(([keys, label]) => (_jsx(Row, { keys: keys, label: t(label) }, keys + label)))] })] })] })] }));
90
+ return (_jsxs(Box, { flexDirection: "column", paddingX: 1, height: height, overflow: "hidden", children: [_jsxs(Text, { children: [_jsx(Text, { bold: true, children: t(GUIDE[where].title) }), _jsxs(Text, { dimColor: true, children: [' · ', max > 0 ? t('↑↓ scroll · Esc close') : t('Esc close')] })] }), _jsx(Box, { flexDirection: "column", overflow: "hidden", height: visible, children: _jsxs(Box, { ref: inner, flexDirection: "column", flexShrink: 0, marginTop: -top, children: [GUIDE[where].text.map(line => (_jsx(Text, { wrap: "wrap", children: t(line) }, line))), _jsx(Box, { marginTop: 1, children: _jsx(Text, { bold: true, children: t('Keys') }) }), _jsxs(Box, { flexWrap: "wrap", columnGap: 4, children: [_jsx(Box, { flexDirection: "column", children: GROUPS.map(group => (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, dimColor: true, children: t(group.label) }), actions.flatMap(action => action.group === group.id && action.key
91
+ ? [
92
+ _jsx(Row, { keys: action.key, label: t(action.label) }, action.id),
93
+ ]
94
+ : [])] }, group.id))) }), _jsxs(Box, { flexDirection: "column", children: [local.length > 0 && (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsx(Text, { bold: true, dimColor: true, children: t('Here') }), local.map(hint => (_jsx(Row, { keys: hint.key, label: hint.label }, hint.key + hint.label)))] })), _jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, dimColor: true, children: t('Move around') }), MOVE.map(([keys, label]) => (_jsx(Row, { keys: keys, label: t(label) }, keys + label)))] })] })] })] }) })] }));
31
95
  }
@@ -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 {};