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

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 (107) hide show
  1. package/CHANGELOG.md +70 -0
  2. package/LICENSE +21 -0
  3. package/dist/cli.js +73 -34
  4. package/dist/commands/TaskScreen.js +32 -0
  5. package/dist/commands/build.js +6 -185
  6. package/dist/commands/deploy.js +85 -238
  7. package/dist/commands/dev.js +34 -37
  8. package/dist/commands/info.js +1 -1
  9. package/dist/commands/setup-signing.js +43 -28
  10. package/dist/commands/sign.js +9 -86
  11. package/dist/commands/use-finish.js +17 -0
  12. package/dist/commands/watch.js +0 -8
  13. package/dist/components/AuthLoginScreen.js +27 -12
  14. package/dist/scaffold/inquirer-stub.js +119 -0
  15. package/dist/scaffold/register.js +15 -0
  16. package/dist/shell/ConfigForm.js +174 -29
  17. package/dist/shell/Frame.js +4 -1
  18. package/dist/shell/Help.js +2 -1
  19. package/dist/shell/Settings.js +59 -6
  20. package/dist/shell/Shell.js +361 -43
  21. package/dist/shell/Tabs.js +8 -2
  22. package/dist/shell/actions.js +0 -0
  23. package/dist/utils/config.js +46 -3
  24. package/dist/utils/create-app.js +226 -0
  25. package/dist/utils/i18n.js +68 -13
  26. package/dist/utils/jsonc.js +2 -1
  27. package/dist/utils/keychain.js +44 -4
  28. package/dist/utils/oauth2-auth.js +35 -7
  29. package/dist/utils/password-prompt.js +23 -3
  30. package/dist/utils/process-runner.js +8 -3
  31. package/dist/utils/project-detection.js +172 -44
  32. package/dist/utils/session-cookie-auth.js +5 -4
  33. package/dist/utils/sitevision-api.js +21 -14
  34. package/dist/utils/sitevision-scripts-runner.js +32 -15
  35. package/dist/utils/tasks.js +132 -66
  36. package/dist/utils/version-check.js +3 -3
  37. package/dist/utils/webpack-runner.js +6 -5
  38. package/dist/utils/workspace.js +41 -3
  39. package/dist/utils/zip.js +18 -19
  40. package/package.json +24 -3
  41. package/readme.md +11 -4
  42. package/dist/app.d.ts +0 -6
  43. package/dist/app.js +0 -321
  44. package/dist/cli.d.ts +0 -2
  45. package/dist/commands/build.d.ts +0 -12
  46. package/dist/commands/deploy.d.ts +0 -17
  47. package/dist/commands/dev.d.ts +0 -14
  48. package/dist/commands/index.d.ts +0 -4
  49. package/dist/commands/info.d.ts +0 -2
  50. package/dist/commands/setup-signing.d.ts +0 -2
  51. package/dist/commands/sign.d.ts +0 -14
  52. package/dist/commands/types.d.ts +0 -18
  53. package/dist/commands/watch.d.ts +0 -2
  54. package/dist/components/AnimatedLogo.d.ts +0 -10
  55. package/dist/components/AuthLoginScreen.d.ts +0 -21
  56. package/dist/components/DevPropertiesForm.d.ts +0 -11
  57. package/dist/components/DevPropertiesForm.js +0 -246
  58. package/dist/components/InfoScreen.d.ts +0 -7
  59. package/dist/components/InfoScreen.js +0 -14
  60. package/dist/components/KeychainPasswordChoice.d.ts +0 -8
  61. package/dist/components/KeychainPasswordChoice.js +0 -30
  62. package/dist/components/MainMenu.d.ts +0 -7
  63. package/dist/components/MainMenu.js +0 -88
  64. package/dist/components/PasswordInput.d.ts +0 -10
  65. package/dist/components/ProcessOutput.d.ts +0 -6
  66. package/dist/components/ProcessOutput.js +0 -30
  67. package/dist/components/SetupFlow.d.ts +0 -8
  68. package/dist/components/SetupFlow.js +0 -233
  69. package/dist/components/SigningPropertiesForm.d.ts +0 -7
  70. package/dist/components/SigningPropertiesForm.js +0 -38
  71. package/dist/components/StatusIndicator.d.ts +0 -8
  72. package/dist/components/TextInput.d.ts +0 -10
  73. package/dist/components/TextInput.js +0 -29
  74. package/dist/components/WelcomeScreen.d.ts +0 -14
  75. package/dist/shell/AddonPicker.d.ts +0 -14
  76. package/dist/shell/Changelog.d.ts +0 -10
  77. package/dist/shell/CommandPalette.d.ts +0 -12
  78. package/dist/shell/ConfigForm.d.ts +0 -36
  79. package/dist/shell/Frame.d.ts +0 -59
  80. package/dist/shell/Help.d.ts +0 -9
  81. package/dist/shell/Popover.d.ts +0 -12
  82. package/dist/shell/Settings.d.ts +0 -6
  83. package/dist/shell/Shell.d.ts +0 -10
  84. package/dist/shell/Tabs.d.ts +0 -36
  85. package/dist/shell/actions.d.ts +0 -48
  86. package/dist/types/index.d.ts +0 -295
  87. package/dist/utils/branding.d.ts +0 -28
  88. package/dist/utils/config.d.ts +0 -27
  89. package/dist/utils/environments.d.ts +0 -20
  90. package/dist/utils/i18n.d.ts +0 -12
  91. package/dist/utils/jsonc.d.ts +0 -19
  92. package/dist/utils/keychain.d.ts +0 -15
  93. package/dist/utils/oauth2-auth.d.ts +0 -64
  94. package/dist/utils/password-prompt.d.ts +0 -14
  95. package/dist/utils/process-runner.d.ts +0 -31
  96. package/dist/utils/project-detection.d.ts +0 -202
  97. package/dist/utils/session-cookie-auth.d.ts +0 -35
  98. package/dist/utils/signing-password.d.ts +0 -12
  99. package/dist/utils/signing-step.d.ts +0 -15
  100. package/dist/utils/signing-step.js +0 -16
  101. package/dist/utils/sitevision-api.d.ts +0 -155
  102. package/dist/utils/sitevision-scripts-runner.d.ts +0 -84
  103. package/dist/utils/tasks.d.ts +0 -50
  104. package/dist/utils/version-check.d.ts +0 -9
  105. package/dist/utils/webpack-runner.d.ts +0 -76
  106. package/dist/utils/workspace.d.ts +0 -17
  107. package/dist/utils/zip.d.ts +0 -71
@@ -1,12 +1,13 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useEffect, useState } from 'react';
2
+ import { useEffect, useRef, 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 { addonNameDrift, 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';
8
8
  import { baseEnvironment, withEnvironmentOverride, } from '../utils/environments.js';
9
9
  import { t } from '../utils/i18n.js';
10
+ import { getGlobalSigning, getSettings } from '../utils/config.js';
10
11
  const METHODS = ['basic', 'oauth2', 'cookie'];
11
12
  const FIELDS = [
12
13
  {
@@ -23,7 +24,7 @@ const FIELDS = [
23
24
  },
24
25
  {
25
26
  key: 'addonName',
26
- help: "Name of the addon (custom module) in the site's Addon Repository that this app is uploaded into. Ctrl+O lists the existing ones.",
27
+ help: "Name of the addon (custom module) in the site's Addon Repository that this app is uploaded into. Ctrl+O lists the existing ones. It is separate from the manifest name: to make them match, rename the addon in Sitevision (Addons, General, Settings) and pick it again with Ctrl+O. For a RESTApp the addon name is part of its endpoint URL.",
27
28
  label: 'Addon name',
28
29
  required: true,
29
30
  hint: '^O pick from repo',
@@ -132,6 +133,67 @@ const FIELDS = [
132
133
  hint: 'blank = prompt each run',
133
134
  },
134
135
  ];
136
+ const MANIFEST_FIELDS = [
137
+ {
138
+ key: 'id',
139
+ label: 'App id',
140
+ required: true,
141
+ 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.',
142
+ },
143
+ {
144
+ key: 'version',
145
+ label: 'Version',
146
+ required: true,
147
+ 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.',
148
+ },
149
+ {
150
+ key: 'name',
151
+ label: 'Name',
152
+ required: true,
153
+ help: 'Name shown when importing and administering the app in the Sitevision editor. A multilingual manifest needs at least English.',
154
+ },
155
+ {
156
+ key: 'description',
157
+ label: 'Description',
158
+ help: 'Short description shown next to the name in the Sitevision editor.',
159
+ },
160
+ {
161
+ key: 'author',
162
+ label: 'Author',
163
+ help: 'Who made the app, shown in the Sitevision editor.',
164
+ },
165
+ {
166
+ key: 'helpUrl',
167
+ label: 'Help URL',
168
+ help: "Link to the app's documentation, shown in the Sitevision editor.",
169
+ },
170
+ ];
171
+ /** Rows for manifest.json: one per text field, one per language of a localized one. */
172
+ const LOCALIZABLE = new Set(['name', 'description']);
173
+ export function manifestFields(manifest) {
174
+ if (!manifest)
175
+ return [];
176
+ const raw = manifest;
177
+ return MANIFEST_FIELDS.flatMap(f => {
178
+ const value = raw[f.key];
179
+ const base = { ...f, section: 'MANIFEST', manifestKey: f.key };
180
+ const localized = (lang) => ({
181
+ ...base,
182
+ key: `manifest.${f.key}.${lang}`,
183
+ lang,
184
+ required: f.required && lang === 'en',
185
+ });
186
+ if (value && typeof value === 'object') {
187
+ const langs = Object.keys(value);
188
+ if (!langs.includes('sv'))
189
+ langs.push('sv');
190
+ return langs.map(lang => localized(lang));
191
+ }
192
+ const plain = { ...base, key: `manifest.${f.key}` };
193
+ return LOCALIZABLE.has(f.key) ? [plain, localized('sv')] : [plain];
194
+ });
195
+ }
196
+ const labelOf = (f) => t(f.label) + (f.lang ? ` (${f.lang})` : '');
135
197
  const ENV_KEYS = new Set([
136
198
  'domain',
137
199
  'siteName',
@@ -170,6 +232,15 @@ function fromProject(project) {
170
232
  signingUsername: dev.signingUsername ?? '',
171
233
  certificateName: dev.certificateName ?? '',
172
234
  signingPassword: '',
235
+ ...Object.fromEntries(manifestFields(project.manifest).map(f => {
236
+ const value = project.manifest[f.manifestKey];
237
+ const text = f.lang && value && typeof value === 'object'
238
+ ? value[f.lang]
239
+ : f.lang
240
+ ? undefined
241
+ : value;
242
+ return [f.key, typeof text === 'string' ? text : ''];
243
+ })),
173
244
  };
174
245
  }
175
246
  function storedSecret(project, key) {
@@ -190,8 +261,9 @@ const sameValue = (key, a, b) => JSON.stringify(a === '' || a === undefined ? IM
190
261
  JSON.stringify(b === '' || b === undefined ? IMPLICIT_VALUES[key] : b);
191
262
  /**
192
263
  * App mode writes the app's complete file. Workspace mode never creates an
193
- * app's .dev_properties.json: changes go to the root file, and the addon name
194
- * to the app's package.json. An app that already has its own file keeps it.
264
+ * app's .dev_properties.json: changes go to the root file, and addon names (the
265
+ * base one and each environment's) to the app's package.json, since the root
266
+ * file is shared by every app. An app that already has its own file keeps it.
195
267
  */
196
268
  function writeConfigFile(project, file) {
197
269
  const { workspaceRoot } = project;
@@ -214,6 +286,40 @@ function writeConfigFile(project, file) {
214
286
  packageJson['addonName'] = after[key] || undefined;
215
287
  });
216
288
  }
289
+ else if (key === 'environments') {
290
+ // `after` is this app's merged view. Only the environment being edited
291
+ // goes to the shared file, or the app's own overrides would reach
292
+ // every other app.
293
+ const environments = Object.entries((after[key] ?? {})).filter(([name]) => !project.environment || name === project.environment);
294
+ root[key] = {
295
+ ...root[key],
296
+ ...Object.fromEntries(environments.map(([name, { addonName: _addonName, ...shared }]) => [
297
+ name,
298
+ shared,
299
+ ])),
300
+ };
301
+ updatePackageJson(project.root, packageJson => {
302
+ const svc = { ...packageJson['svc'] };
303
+ const own = new Map(Object.entries((svc['environments'] ?? {})));
304
+ for (const [name, { addonName }] of environments) {
305
+ const override = {
306
+ ...own.get(name),
307
+ addonName: addonName || undefined,
308
+ };
309
+ if (Object.values(override).every(value => value === undefined)) {
310
+ own.delete(name);
311
+ }
312
+ else {
313
+ own.set(name, override);
314
+ }
315
+ }
316
+ svc['environments'] =
317
+ own.size > 0 ? Object.fromEntries(own) : undefined;
318
+ packageJson['svc'] = Object.values(svc).some(value => value !== undefined)
319
+ ? svc
320
+ : undefined;
321
+ });
322
+ }
217
323
  else {
218
324
  root[key] = after[key];
219
325
  }
@@ -223,22 +329,21 @@ function writeConfigFile(project, file) {
223
329
  /** Apply the form to disk and the keychain. Exported for the test. */
224
330
  export function saveConfig(project, values, edited) {
225
331
  const method = values['authMethod'];
332
+ // Start from what is there: keys the form does not show, and the settings of
333
+ // the auth methods not in use, survive a save.
226
334
  const next = {
335
+ ...project.devProperties,
227
336
  domain: values['domain'],
228
337
  siteName: values['siteName'],
229
338
  addonName: values['addonName'],
230
339
  username: values['username'],
231
340
  authMethod: method,
232
341
  useHTTPForDevDeploy: values['useHTTPForDevDeploy'] === 'yes',
342
+ baseEnvironment: values['baseEnvironment']?.trim().toLowerCase() || undefined,
343
+ production: values['production'] === 'yes' ? true : undefined,
344
+ signingUsername: values['signingUsername'] || undefined,
345
+ certificateName: values['certificateName'] || undefined,
233
346
  };
234
- if (values['baseEnvironment'])
235
- next.baseEnvironment = values['baseEnvironment'].trim().toLowerCase();
236
- if (values['production'] === 'yes')
237
- next.production = true;
238
- if (values['signingUsername'])
239
- next.signingUsername = values['signingUsername'];
240
- if (values['certificateName'])
241
- next.certificateName = values['certificateName'];
242
347
  if (method === 'oauth2') {
243
348
  const scopes = values['scopes'].split(/[\s,]+/).filter(Boolean);
244
349
  next.oauth2 = {
@@ -251,8 +356,8 @@ export function saveConfig(project, values, edited) {
251
356
  }),
252
357
  };
253
358
  }
254
- else if (method === 'cookie' && values['sessionLoginUrl']) {
255
- next.sessionLoginUrl = values['sessionLoginUrl'];
359
+ else if (method === 'cookie') {
360
+ next.sessionLoginUrl = values['sessionLoginUrl'] || undefined;
256
361
  }
257
362
  const env = project.environment;
258
363
  if (env && env !== baseEnvironment(project.base) && project.base) {
@@ -306,6 +411,11 @@ export function visibleFields(method, workspace = false, envMode = false) {
306
411
  }
307
412
  export function ConfigForm({ project, active, width, height, pickAddon, onSaved, onEditingChange, }) {
308
413
  const [values, setValues] = useState(() => fromProject(project));
414
+ // What the form holds when an async lookup lands, not when it started.
415
+ const latest = useRef({ values, project });
416
+ useEffect(() => {
417
+ latest.current = { values, project };
418
+ });
309
419
  const [cursor, setCursor] = useState(0);
310
420
  const [editing, setEditing] = useState(false);
311
421
  const [draft, setDraft] = useState('');
@@ -325,28 +435,42 @@ export function ConfigForm({ project, active, width, height, pickAddon, onSaved,
325
435
  const envMode = Boolean(project.environment &&
326
436
  project.environment !==
327
437
  baseEnvironment(project.base ?? project.devProperties));
328
- const fields = visibleFields(method, project.workspace, envMode);
438
+ const fields = [
439
+ ...visibleFields(method, project.workspace, envMode),
440
+ ...manifestFields(project.manifest),
441
+ ];
329
442
  const current = fields[Math.min(cursor, fields.length - 1)];
330
443
  const inherited = readInheritedDevProperties(project.root);
331
444
  const ancestors = readAncestorDevProperties(project.root);
445
+ const globalSigning = getGlobalSigning();
332
446
  const changes = getPackageJsonSyncChanges(project.root);
333
447
  const packageJsonExists = hasPackageJson(project.root);
334
448
  // Write one field to disk (and the keychain for secrets) right away.
335
- const commit = (key, value, label = current.label) => {
449
+ const commit = (key, value, label = labelOf(current)) => {
336
450
  const clean = key === 'domain' ? normalizeDomain(value) : value;
451
+ const field = fields.find(f => f.key === key);
452
+ if (field?.manifestKey && field.required && !clean) {
453
+ setNote(t('Not saved: {label} is required.', { label }));
454
+ return;
455
+ }
337
456
  const next = { ...values, [key]: clean };
338
457
  setValues(next);
339
458
  try {
340
- saveConfig(project, next, new Set([key]));
459
+ if (field?.manifestKey) {
460
+ writeManifestField(project.manifestPath, field.manifestKey, clean, field.lang);
461
+ }
462
+ else {
463
+ saveConfig(project, next, new Set([key]));
464
+ }
341
465
  }
342
466
  catch (error) {
343
467
  setNote(t('Not saved: {error}', { error: errorText(error) }));
344
468
  return;
345
469
  }
346
470
  setNote(clean === value
347
- ? t('Saved {label}.', { label: t(label) })
471
+ ? t('Saved {label}.', { label })
348
472
  : t('Saved {label} as {value} — a domain is a host only.', {
349
- label: t(label),
473
+ label,
350
474
  value: clean,
351
475
  }));
352
476
  onSaved();
@@ -363,14 +487,15 @@ export function ConfigForm({ project, active, width, height, pickAddon, onSaved,
363
487
  if (cancelled)
364
488
  return;
365
489
  if (found) {
490
+ const now = latest.current.values;
366
491
  const next = {
367
- ...values,
368
- authorizationEndpoint: values['authorizationEndpoint'] || found.authorizationEndpoint,
369
- tokenEndpoint: values['tokenEndpoint'] || found.tokenEndpoint,
492
+ ...now,
493
+ authorizationEndpoint: now['authorizationEndpoint'] || found.authorizationEndpoint,
494
+ tokenEndpoint: now['tokenEndpoint'] || found.tokenEndpoint,
370
495
  };
371
496
  setValues(next);
372
497
  try {
373
- saveConfig(project, next, new Set());
498
+ saveConfig(latest.current.project, next, new Set());
374
499
  }
375
500
  catch (error) {
376
501
  setNote(t('Not saved: {error}', { error: errorText(error) }));
@@ -394,7 +519,7 @@ export function ConfigForm({ project, active, width, height, pickAddon, onSaved,
394
519
  void pickAddon().then(name => {
395
520
  if (name) {
396
521
  setEditing(false);
397
- commit('addonName', name, 'Addon name');
522
+ commit('addonName', name, t('Addon name'));
398
523
  }
399
524
  });
400
525
  };
@@ -461,13 +586,21 @@ export function ConfigForm({ project, active, width, height, pickAddon, onSaved,
461
586
  setEditing(true);
462
587
  }
463
588
  }, { isActive: active });
589
+ // An environment's addon name differs from the app's name on purpose.
590
+ const drift = envMode || !getSettings().addonNameDriftWarning
591
+ ? undefined
592
+ : addonNameDrift(values['addonName'], project.manifest);
464
593
  const source = (f) => {
465
594
  const required = f.required === true || f.required === method;
466
595
  if (required && !(values[f.key] ?? ''))
467
596
  return { text: t('✗ required'), color: 'red' };
597
+ if (f.key === 'addonName' && drift)
598
+ return { text: t('≠ manifest'), color: 'yellow' };
468
599
  if (f.kind === 'secret') {
469
600
  return { text: storedSecret(project, f.key) ? t('keychain') : '—' };
470
601
  }
602
+ if (f.manifestKey)
603
+ return { text: (values[f.key] ?? '') ? 'manifest.json' : '' };
471
604
  const value = f.kind === 'bool' ? values[f.key] === 'yes' : values[f.key];
472
605
  if (envMode && ENV_KEYS.has(f.key)) {
473
606
  const override = project.base?.environments?.[project.environment];
@@ -502,17 +635,22 @@ export function ConfigForm({ project, active, width, height, pickAddon, onSaved,
502
635
  : 'package.json',
503
636
  };
504
637
  }
638
+ if (value && globalSigning[f.key] === value)
639
+ return { text: t('global') };
505
640
  return { text: (values[f.key] ?? '') ? t('local') : '' };
506
641
  };
507
642
  // label column (24) + source column + paddings; never below 20.
508
643
  const valueWidth = Math.max(20, width - 24 - SOURCE_WIDTH - 2);
509
644
  const rows = [];
645
+ let focusRow = 0;
510
646
  let lastSection;
511
647
  for (const f of fields) {
512
648
  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}`));
649
+ 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
650
  lastSection = f.section;
515
651
  }
652
+ if (f === current)
653
+ focusRow = rows.length;
516
654
  const focused = active && f === current;
517
655
  const typing = focused && editing;
518
656
  // A value being typed into, scrolled so the caret stays in view and drawn
@@ -544,9 +682,13 @@ export function ConfigForm({ project, active, width, height, pickAddon, onSaved,
544
682
  display = typing ? (withCaret(draft)) : value ? (_jsx(Text, { children: value })) : (_jsx(Text, { dimColor: true, children: f.hint ? t(f.hint) : '—' }));
545
683
  }
546
684
  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));
685
+ 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
686
  }
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.", {
687
+ // ponytail: the help panel is assumed to wrap to three lines; measure it if
688
+ // a narrow pane ever clips the note.
689
+ const room = Math.max(3, height - 9 - changes.length - (project.workspace ? 2 : 0));
690
+ const firstRow = Math.max(0, Math.min(focusRow - Math.floor(room / 2), rows.length - room));
691
+ 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
692
  root: project.root,
551
693
  }) }) })), _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
694
  ? t('no workspace package.json · y to set up')
@@ -554,5 +696,8 @@ export function ConfigForm({ project, active, width, height, pickAddon, onSaved,
554
696
  ? t('in sync')
555
697
  : changes.length === 1
556
698
  ? 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 })] }));
699
+ : t('{n} diffs · y to apply', { n: changes.length }) })] }), drift && (_jsxs(Text, { wrap: "truncate", children: [_jsx(Text, { color: "yellow", children: "\u2260 " }), t('addon name {addon} · manifest name {names}', {
700
+ addon: values['addonName'] ?? '',
701
+ names: drift.join(' / '),
702
+ })] })), 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
703
  }
@@ -11,7 +11,10 @@ export function navWidth(columns) {
11
11
  return Math.min(48, Math.max(32, Math.floor(columns / 4)));
12
12
  }
13
13
  export function TopBar({ context, domain, auth, version, environment, }) {
14
- return (_jsxs(Box, { paddingX: 1, justifyContent: "space-between", height: 1, children: [_jsx(Box, { flexShrink: 1, marginRight: 2, children: _jsxs(Text, { wrap: "truncate", children: [_jsx(Text, { bold: true, color: ACCENT, children: "svc" }), environment && (_jsxs(Text, { children: [' ', _jsx(Text, { backgroundColor: environment.color, color: "black", bold: true, children: ` ${environment.name.toUpperCase()} ` })] })), _jsxs(Text, { dimColor: true, children: [" ", context] })] }) }), _jsx(Box, { flexShrink: 0, children: _jsxs(Text, { wrap: "truncate", children: [domain && _jsxs(Text, { dimColor: true, children: [domain, " "] }), _jsx(Text, { color: auth.ready ? 'green' : 'yellow', children: auth.ready ? '●' : '○' }), _jsxs(Text, { children: [" ", auth.label, " "] }), _jsxs(Text, { dimColor: true, children: ["v", version] })] }) })] }));
14
+ // Narrow terminals squeeze the domain first, then the context, then the
15
+ // login. `svc` and the environment badge never give way: the badge is the
16
+ // one thing that says a deploy goes to production.
17
+ return (_jsxs(Box, { paddingX: 1, height: 1, overflow: "hidden", children: [_jsxs(Box, { flexShrink: 0, children: [_jsx(Text, { bold: true, color: ACCENT, children: "svc" }), environment && (_jsxs(Text, { children: [' ', _jsx(Text, { backgroundColor: environment.color, color: "black", bold: true, children: ` ${environment.name.toUpperCase()} ` })] }))] }), _jsx(Box, { flexShrink: 2, flexGrow: 1, marginRight: 2, minWidth: 0, children: _jsxs(Text, { dimColor: true, wrap: "truncate", children: [' ', context] }) }), domain && (_jsx(Box, { flexShrink: 100, minWidth: 0, marginRight: 1, children: _jsx(Text, { dimColor: true, wrap: "truncate", children: domain }) })), _jsx(Box, { flexShrink: 1, minWidth: 0, children: _jsxs(Text, { wrap: "truncate", children: [_jsx(Text, { color: auth.ready ? 'green' : 'yellow', children: auth.ready ? '●' : '○' }), _jsxs(Text, { children: [" ", auth.label] })] }) }), _jsx(Box, { flexShrink: 0, children: _jsxs(Text, { dimColor: true, children: [" v", version] }) })] }));
15
18
  }
16
19
  const TYPE_GLYPH = {
17
20
  web: 'web',
@@ -66,10 +66,11 @@ export function HelpPanel({ here, where, height, onClose, }) {
66
66
  const inner = useRef(null);
67
67
  const [top, setTop] = useState(0);
68
68
  const [total, setTotal] = useState(0);
69
+ // The text depends on the pane and the language, both fixed while this is open.
69
70
  useEffect(() => {
70
71
  if (inner.current)
71
72
  setTotal(measureElement(inner.current).height);
72
- });
73
+ }, [here, where, height]);
73
74
  const visible = Math.max(1, height - 1);
74
75
  const max = Math.max(0, total - visible);
75
76
  useInput((input, key) => {
@@ -1,36 +1,73 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useState } from 'react';
3
3
  import { Box, Text, useInput } from 'ink';
4
- import { getSettings, setSettings, settingsFile } from '../utils/config.js';
4
+ import { configProblem, getGlobalSigning, getSettings, setGlobalSigning, setSettings, settingsFile, } from '../utils/config.js';
5
5
  import { LANGUAGES, LANGUAGE_NAMES, setLanguage, t, } from '../utils/i18n.js';
6
6
  import { ACCENT } from './Frame.js';
7
7
  const ROWS = [
8
8
  {
9
9
  key: 'language',
10
10
  label: 'Language',
11
+ help: 'The language svc itself speaks.',
11
12
  options: LANGUAGES,
12
13
  display: value => LANGUAGE_NAMES[value],
13
14
  },
14
15
  {
15
16
  key: 'introAnimation',
16
17
  label: 'Intro animation',
18
+ help: 'Play the logo animation when the shell starts.',
17
19
  options: ['on', 'off'],
18
20
  display: value => t(value),
19
21
  },
22
+ {
23
+ key: 'updateCheck',
24
+ label: 'Update check',
25
+ help: 'Look for a newer svc on npm at start.',
26
+ options: ['on', 'off'],
27
+ display: value => t(value),
28
+ },
29
+ {
30
+ key: 'addonNameDriftWarning',
31
+ label: 'Addon name warning',
32
+ help: 'Mark the addon name with ≠ manifest when it is none of the names in manifest.json.',
33
+ options: ['on', 'off'],
34
+ display: value => t(value),
35
+ },
36
+ {
37
+ key: 'signingUsername',
38
+ label: 'Default signing user',
39
+ help: 'The developer.sitevision.se account that signs in every project without a signing user of its own.',
40
+ options: [],
41
+ display: value => value,
42
+ },
43
+ {
44
+ key: 'certificateName',
45
+ label: 'Default certificate',
46
+ help: 'Which certificate signs when the account has several. Empty = the account default.',
47
+ options: [],
48
+ display: value => value,
49
+ },
20
50
  ];
21
51
  /** Global preferences, saved per field like the Config tab. */
22
52
  export function SettingsScreen({ onChanged, onClose, onOpenWorkspace, }) {
23
53
  const [values, setValues] = useState(() => {
24
54
  const s = getSettings();
55
+ const signing = getGlobalSigning();
25
56
  return {
26
57
  language: s.language,
27
58
  introAnimation: s.introAnimation ? 'on' : 'off',
59
+ updateCheck: s.updateCheck ? 'on' : 'off',
60
+ addonNameDriftWarning: s.addonNameDriftWarning ? 'on' : 'off',
61
+ signingUsername: signing.signingUsername ?? '',
62
+ certificateName: signing.certificateName ?? '',
28
63
  };
29
64
  });
30
65
  const [cursor, setCursor] = useState(0);
31
66
  const [editing, setEditing] = useState(false);
32
67
  const [draft, setDraft] = useState('');
33
- const [note, setNote] = useState('');
68
+ const [note, setNote] = useState(() => configProblem()
69
+ ? t('The settings file does not parse and is ignored until it is fixed.')
70
+ : '');
34
71
  const rowCount = ROWS.length + (onOpenWorkspace ? 1 : 0);
35
72
  const onWorkspaceRow = cursor === ROWS.length;
36
73
  const current = ROWS[Math.min(cursor, ROWS.length - 1)];
@@ -42,8 +79,16 @@ export function SettingsScreen({ onChanged, onClose, onOpenWorkspace, }) {
42
79
  setSettings({
43
80
  language: next.language,
44
81
  introAnimation: next.introAnimation === 'on',
82
+ updateCheck: next.updateCheck === 'on',
83
+ addonNameDriftWarning: next.addonNameDriftWarning === 'on',
84
+ });
85
+ setGlobalSigning({
86
+ signingUsername: next.signingUsername.trim() || undefined,
87
+ certificateName: next.certificateName.trim() || undefined,
45
88
  });
46
- setNote(t('Saved.'));
89
+ setNote(configProblem()
90
+ ? t('Not saved: the settings file does not parse.')
91
+ : t('Saved.'));
47
92
  onChanged();
48
93
  };
49
94
  useInput((input, key) => {
@@ -56,6 +101,12 @@ export function SettingsScreen({ onChanged, onClose, onOpenWorkspace, }) {
56
101
  if (draft !== values[current.key])
57
102
  commit(draft);
58
103
  }
104
+ else if (current.options.length === 0) {
105
+ if (key.backspace || key.delete)
106
+ setDraft(d => d.slice(0, -1));
107
+ else if (input && !key.ctrl && !key.meta)
108
+ setDraft(d => d + input);
109
+ }
59
110
  else {
60
111
  const step = key.leftArrow || key.upArrow
61
112
  ? -1
@@ -89,8 +140,10 @@ export function SettingsScreen({ onChanged, onClose, onOpenWorkspace, }) {
89
140
  const focused = i === cursor && !onWorkspaceRow;
90
141
  const typing = focused && editing;
91
142
  const chosen = typing ? draft : values[row.key];
92
- return (_jsxs(Box, { height: 1, children: [_jsx(Text, { color: focused ? ACCENT : undefined, bold: focused, dimColor: !focused, children: (focused ? '▸ ' : ' ') + t(row.label).padEnd(18) }), _jsx(Text, { children: row.options.map((option, j) => (_jsxs(Text, { children: [_jsx(Text, { bold: option === chosen, color: option === chosen ? ACCENT : undefined, inverse: typing && option === chosen, dimColor: option !== chosen, children: row.display(option) }), j < row.options.length - 1 && _jsx(Text, { dimColor: true, children: " \u00B7 " })] }, option))) })] }, row.key));
93
- }), onOpenWorkspace && (_jsxs(Box, { height: 1, children: [_jsx(Text, { color: onWorkspaceRow ? ACCENT : undefined, bold: onWorkspaceRow, dimColor: !onWorkspaceRow, children: (onWorkspaceRow ? '▸ ' : ' ') + t('Workspace config').padEnd(18) }), _jsx(Text, { dimColor: true, children: t('shared auth and site config for every app') })] })), _jsx(Text, { color: "yellow", children: note }), _jsx(Text, { dimColor: true, children: editing
94
- ? t('←→ choose · Enter confirm · Esc cancel')
143
+ return (_jsxs(Box, { height: 1, children: [_jsx(Text, { color: focused ? ACCENT : undefined, bold: focused, dimColor: !focused, children: (focused ? '▸ ' : ' ') + t(row.label).padEnd(24) }), row.options.length === 0 && (_jsxs(Text, { dimColor: !typing && !chosen, children: [chosen || (typing ? '' : t('not set')), typing && _jsx(Text, { inverse: true, children: " " })] })), _jsx(Text, { children: row.options.map((option, j) => (_jsxs(Text, { children: [_jsx(Text, { bold: option === chosen, color: option === chosen ? ACCENT : undefined, inverse: typing && option === chosen, dimColor: option !== chosen, children: row.display(option) }), j < row.options.length - 1 && _jsx(Text, { dimColor: true, children: " \u00B7 " })] }, option))) })] }, row.key));
144
+ }), onOpenWorkspace && (_jsxs(Box, { height: 1, children: [_jsx(Text, { color: onWorkspaceRow ? ACCENT : undefined, bold: onWorkspaceRow, dimColor: !onWorkspaceRow, children: (onWorkspaceRow ? '▸ ' : ' ') + t('Workspace config').padEnd(24) }), _jsx(Text, { dimColor: true, children: t('shared auth and site config for every app') })] })), _jsx(Box, { marginTop: 1, paddingLeft: 2, children: _jsx(Text, { dimColor: true, italic: true, children: onWorkspaceRow ? ' ' : t(current.help) }) }), _jsx(Text, { color: "yellow", children: note }), _jsx(Text, { dimColor: true, children: editing
145
+ ? current.options.length === 0
146
+ ? t('Enter confirm · Esc cancel')
147
+ : t('←→ choose · Enter confirm · Esc cancel')
95
148
  : t('↑↓ setting · Enter edit · Esc close') })] }));
96
149
  }