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 +20 -0
- package/dist/cli.js +20 -2
- package/dist/scaffold/inquirer-stub.d.ts +6 -0
- package/dist/scaffold/inquirer-stub.js +119 -0
- package/dist/scaffold/register.d.ts +1 -0
- package/dist/scaffold/register.js +15 -0
- package/dist/shell/ConfigForm.d.ts +7 -1
- package/dist/shell/ConfigForm.js +95 -11
- package/dist/shell/Shell.d.ts +2 -1
- package/dist/shell/Shell.js +245 -10
- package/dist/shell/actions.d.ts +1 -0
- package/dist/shell/actions.js +0 -0
- package/dist/utils/create-app.d.ts +53 -0
- package/dist/utils/create-app.js +219 -0
- package/dist/utils/i18n.js +32 -0
- package/dist/utils/process-runner.d.ts +2 -1
- package/dist/utils/process-runner.js +4 -1
- package/dist/utils/project-detection.d.ts +10 -2
- package/dist/utils/project-detection.js +88 -11
- package/dist/utils/tasks.d.ts +12 -1
- package/dist/utils/tasks.js +2 -2
- package/package.json +1 -1
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
|
-
|
|
161
|
-
|
|
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,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;
|
package/dist/shell/ConfigForm.js
CHANGED
|
@@ -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 =
|
|
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
|
|
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
|
-
|
|
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
|
|
422
|
+
? t('Saved {label}.', { label })
|
|
348
423
|
: t('Saved {label} as {value} — a domain is a host only.', {
|
|
349
|
-
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, {
|
|
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 ? '▸ ' : ' ') +
|
|
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
|
-
|
|
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:
|
|
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
|
}
|
package/dist/shell/Shell.d.ts
CHANGED
|
@@ -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 {};
|