sitevision-cli 1.0.0-beta.16 → 1.0.0-beta.18

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.
@@ -15,6 +15,7 @@ export function AuthLoginScreen({ method, devProperties, onComplete, onError, on
15
15
  const [phase, setPhase] = React.useState('starting');
16
16
  const [authUrl, setAuthUrl] = React.useState('');
17
17
  const [note, setNote] = React.useState('');
18
+ const [loginUrl, setLoginUrl] = React.useState('');
18
19
  const cookieRef = React.useRef(null);
19
20
  const cancelOAuthRef = React.useRef(null);
20
21
  React.useEffect(() => {
@@ -45,6 +46,7 @@ export function AuthLoginScreen({ method, devProperties, onComplete, onError, on
45
46
  return;
46
47
  }
47
48
  cookieRef.current = session;
49
+ setLoginUrl(session.loginUrl);
48
50
  setPhase('awaiting');
49
51
  }
50
52
  })();
@@ -84,5 +86,5 @@ export function AuthLoginScreen({ method, devProperties, onComplete, onError, on
84
86
  })();
85
87
  }
86
88
  });
87
- return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, color: "cyan", children: method === 'oauth2' ? t('OAuth2 login') : t('Session cookie login') }) }), method === 'oauth2' ? (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { color: "green", children: _jsx(Spinner, { type: "dots" }) }), _jsxs(Text, { children: [" ", t('Waiting for you to finish login in the browser…')] })] }), authUrl && (_jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { dimColor: true, children: t("If the browser didn't open, visit:") }), _jsx(Text, { children: authUrl })] }))] })) : (_jsxs(Box, { flexDirection: "column", children: [phase === 'starting' && (_jsxs(Box, { children: [_jsx(Text, { color: "green", children: _jsx(Spinner, { type: "dots" }) }), _jsxs(Text, { children: [" ", t('Opening browser…')] })] })), phase === 'awaiting' && (_jsx(Text, { children: t('Log in in the opened browser, then press Enter to capture the session.') })), phase === 'capturing' && (_jsxs(Box, { children: [_jsx(Text, { color: "green", children: _jsx(Spinner, { type: "dots" }) }), _jsxs(Text, { children: [" ", t('Capturing session…')] })] })), note && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "yellow", children: note }) }))] })), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: t('Press Esc to cancel.') }) })] }));
89
+ return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, color: "cyan", children: method === 'oauth2' ? t('OAuth2 login') : t('Session cookie login') }) }), method === 'oauth2' ? (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { color: "green", children: _jsx(Spinner, { type: "dots" }) }), _jsxs(Text, { children: [" ", t('Waiting for you to finish login in the browser…')] })] }), authUrl && (_jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { dimColor: true, children: t("If the browser didn't open, visit:") }), _jsx(Text, { children: authUrl })] }))] })) : (_jsxs(Box, { flexDirection: "column", children: [phase === 'starting' && (_jsxs(Box, { children: [_jsx(Text, { color: "green", children: _jsx(Spinner, { type: "dots" }) }), _jsxs(Text, { children: [" ", t('Opening browser…')] })] })), phase === 'awaiting' && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: t('A Chrome window is open at:') }), _jsx(Text, { color: "cyan", children: loginUrl }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { children: t('1. Log in to the site there, single sign-on included.') }), _jsx(Text, { children: t('2. Wait until the site itself has finished loading.') }), _jsx(Text, { children: t('3. Come back here and press Enter.') })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: t('Leave the browser window open; it closes once the session is captured.') }) })] })), phase === 'capturing' && (_jsxs(Box, { children: [_jsx(Text, { color: "green", children: _jsx(Spinner, { type: "dots" }) }), _jsxs(Text, { children: [" ", t('Capturing session…')] })] })), note && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "yellow", children: note }) }))] })), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: t('Press Esc to cancel.') }) })] }));
88
90
  }
@@ -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 { getPackageJsonSyncChanges, readInheritedDevProperties, writeDevProperties, } from '../utils/project-detection.js';
4
+ import { getPackageJsonSyncChanges, normalizeDomain, readInheritedDevProperties, writeDevProperties, } 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';
@@ -298,10 +298,16 @@ export function ConfigForm({ project, active, width, height, pickAddon, onSaved,
298
298
  : [];
299
299
  // Write one field to disk (and the keychain for secrets) right away.
300
300
  const commit = (key, value, label = current.label) => {
301
- const next = { ...values, [key]: value };
301
+ const clean = key === 'domain' ? normalizeDomain(value) : value;
302
+ const next = { ...values, [key]: clean };
302
303
  setValues(next);
303
304
  saveConfig(project, next, new Set([key]));
304
- setNote(t('Saved {label}.', { label: t(label) }));
305
+ setNote(clean === value
306
+ ? t('Saved {label}.', { label: t(label) })
307
+ : t('Saved {label} as {value} — a domain is a host only.', {
308
+ label: t(label),
309
+ value: clean,
310
+ }));
305
311
  onSaved();
306
312
  };
307
313
  // Auto-fill OAuth2 endpoints from the site's OpenID configuration.
@@ -28,6 +28,12 @@ export declare function appStatus(project: ProjectInfo): {
28
28
  signing: boolean;
29
29
  scriptsWarning: string | undefined;
30
30
  };
31
+ /** The name shown for an app in the navigator, and what the filter matches. */
32
+ export declare function appLabel(app: ProjectInfo): string;
33
+ /** Indices into `apps` whose label fuzzy-matches the filter. */
34
+ export declare function navMatches(apps: ProjectInfo[], filter: string): number[];
35
+ /** Next selectable index when moving by `delta`, wrapping at both ends. */
36
+ export declare function navMove(ring: number[], selected: number, delta: number): number;
31
37
  export interface NavigatorProps {
32
38
  apps: ProjectInfo[];
33
39
  groupOf: (app: ProjectInfo) => string;
@@ -38,8 +44,9 @@ export interface NavigatorProps {
38
44
  single: boolean;
39
45
  settingsSelected?: boolean;
40
46
  width: number;
47
+ filter?: string;
41
48
  }
42
- export declare function Navigator({ apps, groupOf, selected, focused, tasks, height, single, settingsSelected, width, }: NavigatorProps): import("react").JSX.Element;
49
+ export declare function Navigator({ apps, groupOf, selected, focused, tasks, height, single, settingsSelected, width, filter, }: NavigatorProps): import("react").JSX.Element;
43
50
  export declare function NavigatorStrip({ apps, selected, focused, }: Pick<NavigatorProps, 'apps' | 'selected' | 'focused'>): import("react").JSX.Element;
44
51
  export declare function elapsed(task: Task): string;
45
52
  export interface Hint {
@@ -2,6 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Box, Text } from 'ink';
3
3
  import Spinner from 'ink-spinner';
4
4
  import { appTypeOf, getPackageJsonSyncChanges, localizedText, } from '../utils/project-detection.js';
5
+ import { fuzzyMatch } from './actions.js';
5
6
  import { t } from '../utils/i18n.js';
6
7
  export const ACCENT = 'cyan';
7
8
  export const NARROW_BELOW = 100;
@@ -40,7 +41,24 @@ function Dots({ project }) {
40
41
  const dot = (ok, warn = false) => (_jsx(Text, { color: ok ? 'green' : warn ? 'yellow' : 'red', children: "\u25CF" }));
41
42
  return (_jsxs(Text, { children: [dot(s.deps), dot(s.config), dot(s.sync === 0, s.sync > 0), dot(s.signing, !s.signing)] }));
42
43
  }
43
- export function Navigator({ apps, groupOf, selected, focused, tasks, height, single, settingsSelected = false, width, }) {
44
+ /** The name shown for an app in the navigator, and what the filter matches. */
45
+ export function appLabel(app) {
46
+ return localizedText(app.manifest.name) || app.manifest.id;
47
+ }
48
+ /** Indices into `apps` whose label fuzzy-matches the filter. */
49
+ export function navMatches(apps, filter) {
50
+ return apps
51
+ .map((_, index) => index)
52
+ .filter(index => fuzzyMatch(filter, appLabel(apps[index])));
53
+ }
54
+ /** Next selectable index when moving by `delta`, wrapping at both ends. */
55
+ export function navMove(ring, selected, delta) {
56
+ if (ring.length === 0)
57
+ return selected;
58
+ const at = ring.indexOf(selected);
59
+ return ring[at === -1 ? 0 : (at + delta + ring.length) % ring.length];
60
+ }
61
+ export function Navigator({ apps, groupOf, selected, focused, tasks, height, single, settingsSelected = false, width, filter = '', }) {
44
62
  // Row: marker(1) glyph(3) sp name sp version(6) sp dots(4) inside the padding.
45
63
  const nameWidth = width - 2 - 17;
46
64
  const running = tasks.filter(task => task.status === 'running');
@@ -51,16 +69,20 @@ export function Navigator({ apps, groupOf, selected, focused, tasks, height, sin
51
69
  for (const [index, app] of apps.entries()) {
52
70
  const group = groupOf(app);
53
71
  if (!single && group !== lastGroup) {
54
- rows.push(_jsx(Box, { height: 1, flexShrink: 0, children: _jsxs(Text, { dimColor: true, wrap: "truncate", children: [' ', group] }) }, `g-${group}`));
72
+ rows.push(_jsx(Box, { height: 1, flexShrink: 0, children: _jsxs(Text, { dimColor: true, wrap: "truncate", children: [' ', group] }) }, `g-${index}-${group}`));
55
73
  rowApp.push(-1);
56
74
  lastGroup = group;
57
75
  }
58
76
  rowApp.push(index);
59
77
  const active = index === selected;
60
78
  const busy = running.some(task => task.appRoot === app.root);
61
- const name = localizedText(app.manifest.name) || app.manifest.id;
79
+ const name = appLabel(app);
62
80
  rows.push(_jsxs(Box, { width: width - 2, height: 1, flexShrink: 0, children: [_jsxs(Text, { backgroundColor: active && focused ? ACCENT : undefined, color: active && focused ? 'black' : undefined, bold: active, wrap: "truncate", children: [active ? '▎' : ' ', _jsx(Text, { dimColor: !active, children: typeGlyph(app.manifest) }), ' ', name.padEnd(nameWidth).slice(0, nameWidth), ' ', _jsx(Text, { dimColor: true, children: app.manifest.version.padStart(6).slice(0, 6) }), ' '] }), busy ? (_jsx(Text, { color: ACCENT, children: _jsx(Spinner, { type: "dots" }) })) : (_jsx(Dots, { project: app }))] }, app.root));
63
81
  }
82
+ if (rows.length === 0) {
83
+ rows.push(_jsx(Box, { height: 1, flexShrink: 0, children: _jsx(Text, { dimColor: true, children: ' ' + t('no matches') }) }, "none"));
84
+ rowApp.push(-1);
85
+ }
64
86
  // Window the list so the selected app stays visible; the lines outside
65
87
  // are summarised as "… n more".
66
88
  const fixed = 1 + 1 + (single ? 0 : 2) + (running.length > 0 ? running.length + 2 : 0);
@@ -79,14 +101,16 @@ export function Navigator({ apps, groupOf, selected, focused, tasks, height, sin
79
101
  if (end < rows.length)
80
102
  shown[shown.length - 1] = more(rows.length - end, '↓');
81
103
  }
82
- return (_jsxs(Box, { flexDirection: "column", width: width, height: height, borderStyle: "single", borderDimColor: true, borderTop: false, borderBottom: false, borderLeft: false, paddingX: 1, overflow: "hidden", children: [_jsx(Text, { bold: true, dimColor: true, children: single
104
+ return (_jsxs(Box, { flexDirection: "column", width: width, height: height, borderStyle: "single", borderDimColor: true, borderTop: false, borderBottom: false, borderLeft: false, paddingX: 1, overflow: "hidden", children: [filter ? (_jsxs(Text, { wrap: "truncate", children: [_jsx(Text, { color: ACCENT, children: "\u276F " }), filter, _jsx(Text, { inverse: true, children: " " }), _jsxs(Text, { dimColor: true, children: [' ', apps.length === 1
105
+ ? t('1 match')
106
+ : t('{n} matches', { n: apps.length })] })] })) : (_jsx(Text, { bold: true, dimColor: true, children: single
83
107
  ? t('APP')
84
108
  : apps.length === 1
85
109
  ? t('WORKSPACE 1 app')
86
- : t('WORKSPACE {n} apps', { n: apps.length }) }), shown, _jsx(Text, { dimColor: true, children: ' ' + t('deps·config·sync·signing') }), !single && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { backgroundColor: settingsSelected && focused ? ACCENT : undefined, color: settingsSelected && focused ? 'black' : undefined, bold: settingsSelected, children: [settingsSelected ? '▎' : ' ', "\u2699 ", t('Workspace settings')] }) })), running.length > 0 && (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { bold: true, dimColor: true, children: t('TASKS') }), running.map(task => (_jsxs(Text, { wrap: "truncate", children: [_jsx(Text, { color: ACCENT, children: _jsx(Spinner, { type: "dots" }) }), ' ', task.label, " ", task.appName, " ", _jsx(Text, { dimColor: true, children: elapsed(task) })] }, task.id)))] }))] }));
110
+ : t('WORKSPACE {n} apps', { n: apps.length }) })), shown, _jsx(Text, { dimColor: true, children: ' ' + t('deps·config·sync·signing') }), !single && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { backgroundColor: settingsSelected && focused ? ACCENT : undefined, color: settingsSelected && focused ? 'black' : undefined, bold: settingsSelected, children: [settingsSelected ? '▎' : ' ', "\u2699 ", t('Workspace settings')] }) })), running.length > 0 && (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { bold: true, dimColor: true, children: t('TASKS') }), running.map(task => (_jsxs(Text, { wrap: "truncate", children: [_jsx(Text, { color: ACCENT, children: _jsx(Spinner, { type: "dots" }) }), ' ', task.label, " ", task.appName, " ", _jsx(Text, { dimColor: true, children: elapsed(task) })] }, task.id)))] }))] }));
87
111
  }
88
112
  export function NavigatorStrip({ apps, selected, focused, }) {
89
- return (_jsx(Box, { paddingX: 1, children: _jsx(Text, { wrap: "truncate", children: apps.map((app, index) => (_jsxs(Text, { backgroundColor: index === selected && focused ? ACCENT : undefined, color: index === selected && focused ? 'black' : undefined, bold: index === selected, children: [' ', typeGlyph(app.manifest), ' ', localizedText(app.manifest.name) || app.manifest.id, ' '] }, app.root))) }) }));
113
+ return (_jsx(Box, { paddingX: 1, children: _jsx(Text, { wrap: "truncate", children: apps.map((app, index) => (_jsxs(Text, { backgroundColor: index === selected && focused ? ACCENT : undefined, color: index === selected && focused ? 'black' : undefined, bold: index === selected, children: [' ', typeGlyph(app.manifest), " ", appLabel(app), ' '] }, app.root))) }) }));
90
114
  }
91
115
  export function elapsed(task) {
92
116
  const ms = (task.endedAt ?? Date.now()) - task.startedAt;
@@ -9,7 +9,7 @@ import { listAddons, listExecutables, } from '../utils/sitevision-api.js';
9
9
  import { useTasks, runningTasks, startActivate, getTasks, } from '../utils/tasks.js';
10
10
  import { PasswordInput } from '../components/PasswordInput.js';
11
11
  import { AuthLoginScreen } from '../components/AuthLoginScreen.js';
12
- import { TopBar, Navigator, NavigatorStrip, BottomBar, navWidth, NARROW_BELOW, ACCENT, } from './Frame.js';
12
+ import { TopBar, Navigator, NavigatorStrip, BottomBar, navMatches, navMove, appLabel, navWidth, NARROW_BELOW, ACCENT, } from './Frame.js';
13
13
  import { TabBar, TABS, Overview, Versions, Log, } from './Tabs.js';
14
14
  import { CommandPalette } from './CommandPalette.js';
15
15
  import { ConfigForm } from './ConfigForm.js';
@@ -43,6 +43,7 @@ export function Shell({ apps: initialApps, workspaceRoot, version }) {
43
43
  const [tab, setTab] = useState('overview');
44
44
  const [focus, setFocus] = useState(workspaceRoot && !onboard ? 'nav' : 'content');
45
45
  const [overlay, setOverlay] = useState(null);
46
+ const [filter, setFilter] = useState('');
46
47
  const [versions, setVersions] = useState({});
47
48
  const [versionRow, setVersionRow] = useState(0);
48
49
  const [logScroll, setLogScroll] = useState(0);
@@ -78,6 +79,10 @@ export function Shell({ apps: initialApps, workspaceRoot, version }) {
78
79
  [workspaceRoot, apps, env]);
79
80
  const narrow = columns < NARROW_BELOW;
80
81
  const sidebar = navWidth(columns);
82
+ // The navigator shows the fuzzy matches; `selected` stays an index into
83
+ // `apps` (with `apps.length` meaning the workspace settings row).
84
+ const matches = useMemo(() => navMatches(apps, filter), [apps, filter]);
85
+ const ring = single ? matches : [...matches, apps.length];
81
86
  const running = runningTasks();
82
87
  // Re-render once a second while something runs so elapsed times move.
83
88
  useEffect(() => {
@@ -254,6 +259,10 @@ export function Shell({ apps: initialApps, workspaceRoot, version }) {
254
259
  ? raw.toUpperCase()
255
260
  : raw;
256
261
  if (key.escape) {
262
+ if (focus === 'nav' && filter) {
263
+ setFilter('');
264
+ return;
265
+ }
257
266
  // Esc backs out of the workspace settings pane, not just its focus.
258
267
  if (settings)
259
268
  setSelected(0);
@@ -265,9 +274,40 @@ export function Shell({ apps: initialApps, workspaceRoot, version }) {
265
274
  return;
266
275
  }
267
276
  if (key.tab) {
277
+ setFilter('');
268
278
  setFocus(f => f === 'nav' && !single ? 'content' : single ? 'content' : 'nav');
269
279
  return;
270
280
  }
281
+ // Navigator: typing searches, so no action key fires until Enter has
282
+ // moved the focus into the content pane.
283
+ if (focus === 'nav') {
284
+ const move = (delta) => {
285
+ setSelected(navMove(ring, selected, delta));
286
+ };
287
+ const search = (next) => {
288
+ setFilter(next);
289
+ const found = navMatches(apps, next);
290
+ if (found.length > 0 && !found.includes(selected))
291
+ setSelected(found[0]);
292
+ };
293
+ if (key.upArrow)
294
+ move(-1);
295
+ else if (key.downArrow)
296
+ move(1);
297
+ else if (key.return) {
298
+ if (matches.length > 0 || settings) {
299
+ setFilter('');
300
+ setFocus('content');
301
+ }
302
+ }
303
+ else if (key.backspace || key.delete)
304
+ search(filter.slice(0, -1));
305
+ else if (input === 'q' && !filter)
306
+ quit();
307
+ else if (input?.length === 1 && input >= ' ' && !key.ctrl && !key.meta)
308
+ search(filter + input);
309
+ return;
310
+ }
271
311
  if (input === 'a' && tab !== 'versions') {
272
312
  setTab('versions');
273
313
  setFocus('content');
@@ -292,16 +332,7 @@ export function Shell({ apps: initialApps, workspaceRoot, version }) {
292
332
  quit();
293
333
  return;
294
334
  }
295
- if (focus === 'nav') {
296
- const last = single ? apps.length - 1 : apps.length;
297
- if (key.upArrow)
298
- setSelected(s => (s > 0 ? s - 1 : last));
299
- if (key.downArrow)
300
- setSelected(s => (s < last ? s + 1 : 0));
301
- if (key.return)
302
- setFocus('content');
303
- }
304
- else if (tab === 'versions') {
335
+ if (tab === 'versions') {
305
336
  const count = versions[versionsKey]?.executables?.length ?? 0;
306
337
  if (key.upArrow)
307
338
  setVersionRow(r => Math.max(0, r - 1));
@@ -336,7 +367,7 @@ export function Shell({ apps: initialApps, workspaceRoot, version }) {
336
367
  const mainHeight = frameRows - 2;
337
368
  const contentHeight = mainHeight - 1 - (narrow ? 1 : 0);
338
369
  const groupOf = (app) => workspaceRoot ? appGroup(workspaceRoot, app.root) : '.';
339
- const appName = localizedText(project.manifest.name) || project.manifest.id;
370
+ const appName = appLabel(project);
340
371
  const tabName = t(TABS.find(entry => entry.id === tab).label).toLowerCase();
341
372
  const contextLabel = settings
342
373
  ? `${t('workspace')} ▸ ${t('settings')}`
@@ -361,61 +392,77 @@ export function Shell({ apps: initialApps, workspaceRoot, version }) {
361
392
  ['↑↓', 'apps'],
362
393
  ['q', 'quit'],
363
394
  ]);
395
+ const navHints = filter
396
+ ? h([
397
+ ['↑↓', 'move'],
398
+ ['Enter', 'select'],
399
+ ['Esc', 'clear'],
400
+ ['/', 'commands'],
401
+ ])
402
+ : h([
403
+ ['a–z', 'search'],
404
+ ['↑↓', 'move'],
405
+ ['Enter', 'select'],
406
+ ['/', 'commands'],
407
+ ['q', 'quit'],
408
+ ]);
364
409
  const hints = overlay
365
410
  ? h([['Esc', 'cancel']])
366
- : settings
367
- ? settingsHints
368
- : tab === 'versions'
369
- ? h([
370
- ['a', 'activate'],
371
- ['R', 'refresh'],
372
- ['p', 'deploy'],
373
- ['/', 'commands'],
374
- ['q', 'quit'],
375
- ])
376
- : tab === 'log'
411
+ : focus === 'nav' && !settings
412
+ ? navHints
413
+ : settings
414
+ ? settingsHints
415
+ : tab === 'versions'
377
416
  ? h([
378
- ['f', 'follow'],
379
- ['x', 'wrap'],
380
- ['K', 'stop'],
417
+ ['a', 'activate'],
418
+ ['R', 'refresh'],
381
419
  ['p', 'deploy'],
382
420
  ['/', 'commands'],
383
421
  ['q', 'quit'],
384
422
  ])
385
- : tab === 'config'
386
- ? editing
387
- ? h([
388
- ['Enter', 'save'],
389
- ['Esc', 'cancel'],
390
- ])
391
- : formActive
392
- ? h([
393
- ['↑↓', 'field'],
394
- ['Enter', 'edit'],
395
- ['^O', 'pick addon'],
396
- ['y', 'sync'],
397
- ['/', 'commands'],
398
- ['q', 'quit'],
399
- ])
400
- : h([
401
- ['Tab', 'edit'],
402
- ['y', 'sync'],
403
- ['l', 'login'],
404
- ['/', 'commands'],
405
- ['q', 'quit'],
406
- ])
407
- : h([
408
- ['d', 'dev'],
409
- ['w', 'watch'],
410
- ['b', 'build'],
411
- ['s', 'sign'],
423
+ : tab === 'log'
424
+ ? h([
425
+ ['f', 'follow'],
426
+ ['x', 'wrap'],
427
+ ['K', 'stop'],
412
428
  ['p', 'deploy'],
413
- ['a', 'activate'],
414
- ['E', 'env'],
415
- ['i', 'install'],
416
429
  ['/', 'commands'],
417
430
  ['q', 'quit'],
418
- ]);
431
+ ])
432
+ : tab === 'config'
433
+ ? editing
434
+ ? h([
435
+ ['Enter', 'save'],
436
+ ['Esc', 'cancel'],
437
+ ])
438
+ : formActive
439
+ ? h([
440
+ ['↑↓', 'field'],
441
+ ['Enter', 'edit'],
442
+ ['^O', 'pick addon'],
443
+ ['y', 'sync'],
444
+ ['/', 'commands'],
445
+ ['q', 'quit'],
446
+ ])
447
+ : h([
448
+ ['Tab', 'edit'],
449
+ ['y', 'sync'],
450
+ ['l', 'login'],
451
+ ['/', 'commands'],
452
+ ['q', 'quit'],
453
+ ])
454
+ : h([
455
+ ['d', 'dev'],
456
+ ['w', 'watch'],
457
+ ['b', 'build'],
458
+ ['s', 'sign'],
459
+ ['p', 'deploy'],
460
+ ['a', 'activate'],
461
+ ['E', 'env'],
462
+ ['i', 'install'],
463
+ ['/', 'commands'],
464
+ ['q', 'quit'],
465
+ ]);
419
466
  const right = running.length > 0 ? (_jsxs(Text, { children: [_jsx(Text, { color: ACCENT, children: _jsx(Spinner, { type: "dots" }) }), ' ', running[0].label, " ", running[0].appName, running.length > 1 && (_jsxs(Text, { dimColor: true, children: [" \u00B7 ", t('{n} tasks', { n: running.length })] }))] })) : notice ? (_jsx(Text, { color: { info: undefined, ok: 'green', warn: 'yellow', error: 'red' }[notice.level], wrap: "truncate", children: notice.text })) : (_jsx(Text, { dimColor: true, children: t('idle') }));
420
467
  const closeOverlay = () => setOverlay(null);
421
468
  const content = overlay ? (renderOverlay(overlay, {
@@ -448,7 +495,7 @@ export function Shell({ apps: initialApps, workspaceRoot, version }) {
448
495
  return (_jsxs(Box, { flexDirection: "column", width: columns, height: frameRows, children: [_jsx(TopBar, { context: contextLabel, domain: project.devProperties?.domain, auth: authState(project), environment: {
449
496
  name: env,
450
497
  color: environmentColor(env, rawProject.devProperties),
451
- }, version: version }), _jsxs(Box, { flexGrow: 1, height: mainHeight, borderStyle: "single", borderDimColor: true, borderLeft: false, borderRight: false, borderBottom: false, children: [!narrow && (_jsx(Navigator, { apps: apps, groupOf: groupOf, selected: selected, focused: focus === 'nav', tasks: tasks, height: mainHeight - 1, single: single, settingsSelected: settings, width: sidebar })), _jsxs(Box, { flexDirection: "column", width: narrow ? columns : columns - sidebar, overflow: "hidden", children: [narrow && !single && (_jsx(NavigatorStrip, { apps: apps, selected: selected, focused: focus === 'nav' })), settings ? (_jsxs(Box, { paddingX: 1, children: [_jsx(Text, { bold: true, color: ACCENT, children: t('Workspace settings') }), _jsx(Text, { dimColor: true, children: onboard && configIncomplete(workspaceTarget?.base)
498
+ }, version: version }), _jsxs(Box, { flexGrow: 1, height: mainHeight, borderStyle: "single", borderDimColor: true, borderLeft: false, borderRight: false, borderBottom: false, children: [!narrow && (_jsx(Navigator, { apps: matches.map(i => apps[i]), groupOf: groupOf, selected: matches.indexOf(selected), focused: focus === 'nav', tasks: tasks, height: mainHeight - 1, single: single, settingsSelected: settings, width: sidebar, filter: filter })), _jsxs(Box, { flexDirection: "column", width: narrow ? columns : columns - sidebar, overflow: "hidden", children: [narrow && !single && (_jsx(NavigatorStrip, { apps: matches.map(i => apps[i]), selected: matches.indexOf(selected), focused: focus === 'nav' })), settings ? (_jsxs(Box, { paddingX: 1, children: [_jsx(Text, { bold: true, color: ACCENT, children: t('Workspace settings') }), _jsx(Text, { dimColor: true, children: onboard && configIncomplete(workspaceTarget?.base)
452
499
  ? t(' · new workspace: fill in once, every app inherits · Esc skips')
453
500
  : t(' · shared .dev_properties.json at the root') })] })) : (_jsx(TabBar, { tab: tab, narrow: narrow, focused: focus === 'content' })), _jsx(Box, { height: contentHeight, overflow: "hidden", alignItems: "flex-start", children: content })] })] }), _jsx(BottomBar, { hints: hints, right: right })] }));
454
501
  }
@@ -203,6 +203,13 @@ const sv = {
203
203
  edit: 'redigera',
204
204
  back: 'tillbaka',
205
205
  quit: 'avsluta',
206
+ search: 'sök',
207
+ move: 'flytta',
208
+ select: 'välj',
209
+ clear: 'rensa',
210
+ 'no matches': 'inga träffar',
211
+ '{n} matches': '{n} träffar',
212
+ '1 match': '1 träff',
206
213
  'edit settings': 'redigera inställningar',
207
214
  apps: 'appar',
208
215
  activate: 'aktivera',
@@ -223,6 +230,7 @@ const sv = {
223
230
  idle: 'inaktiv',
224
231
  'workspace config saved': 'arbetsytans konfig sparad',
225
232
  'config saved': 'konfig sparad',
233
+ 'Saved {label} as {value} — a domain is a host only.': 'Sparade {label} som {value} — en domän är enbart ett värdnamn.',
226
234
  ' · shared .dev_properties.json at the root': ' · delad .dev_properties.json i roten',
227
235
  ' · new workspace: fill in once, every app inherits · Esc skips': ' · ny arbetsyta: fyll i en gång, alla appar ärver · Esc hoppar över',
228
236
  'y confirm · n cancel': 'y bekräfta · n avbryt',
@@ -260,5 +268,10 @@ const sv = {
260
268
  'Opening browser…': 'Öppnar webbläsare…',
261
269
  'Log in in the opened browser, then press Enter to capture the session.': 'Logga in i webbläsaren och tryck sedan Enter för att fånga sessionen.',
262
270
  'Capturing session…': 'Fångar session…',
271
+ 'A Chrome window is open at:': 'Ett Chrome-fönster är öppet på:',
272
+ '1. Log in to the site there, single sign-on included.': '1. Logga in på webbplatsen där, även med enkel inloggning.',
273
+ '2. Wait until the site itself has finished loading.': '2. Vänta tills själva webbplatsen har laddat klart.',
274
+ '3. Come back here and press Enter.': '3. Kom tillbaka hit och tryck Enter.',
275
+ 'Leave the browser window open; it closes once the session is captured.': 'Låt webbläsarfönstret vara öppet; det stängs när sessionen har hämtats.',
263
276
  'Press Esc to cancel.': 'Tryck Esc för att avbryta.',
264
277
  };
@@ -22,6 +22,8 @@ export declare function findDevPropertiesPath(root: string): string | null;
22
22
  * Get the default dev properties path (for creating new files)
23
23
  */
24
24
  export declare function getDefaultDevPropertiesPath(root: string): string;
25
+ /** A bare host: no scheme, no path, no stray spaces. */
26
+ export declare function normalizeDomain(value: string): string;
25
27
  /**
26
28
  * The dev properties a workspace root defines (its own file merged over any
27
29
  * ancestors'), with the deploy password resolved from the keychain like an
@@ -81,12 +81,25 @@ function ancestorDirs(root) {
81
81
  }
82
82
  return dirs;
83
83
  }
84
+ /** A bare host: no scheme, no path, no stray spaces. */
85
+ export function normalizeDomain(value) {
86
+ const host = value
87
+ .trim()
88
+ .replace(/^[a-z]+:\/\//i, '')
89
+ .replaceAll(/\s/g, '');
90
+ const slash = host.indexOf('/');
91
+ return slash === -1 ? host : host.slice(0, slash);
92
+ }
84
93
  function readDevPropertiesFile(dir) {
85
94
  const file = findDevPropertiesPath(dir);
86
95
  if (!file)
87
96
  return null;
88
97
  try {
89
- return JSON.parse(fs.readFileSync(file, 'utf-8'));
98
+ const dev = JSON.parse(fs.readFileSync(file, 'utf-8'));
99
+ // ponytail: top level only; a hand-edited environment override keeps its
100
+ // scheme until it is saved from the config form.
101
+ dev.domain &&= normalizeDomain(dev.domain);
102
+ return dev;
90
103
  }
91
104
  catch {
92
105
  return null;
@@ -12,6 +12,7 @@ export interface CaptureResult {
12
12
  export interface CookieLoginSession {
13
13
  capture: () => Promise<CaptureResult>;
14
14
  close: () => Promise<void>;
15
+ loginUrl: string;
15
16
  }
16
17
  /**
17
18
  * Pick the session from a cookie jar: find JSESSIONID (preferring the deploy
@@ -86,7 +86,7 @@ export async function beginCookieLogin(dev) {
86
86
  // Best-effort close.
87
87
  });
88
88
  };
89
- return { capture, close };
89
+ return { capture, close, loginUrl };
90
90
  }
91
91
  catch {
92
92
  if (browser) {
@@ -39,7 +39,10 @@ export function discoverApps(root) {
39
39
  }
40
40
  };
41
41
  walk(root, 1);
42
- return found.toSorted((a, b) => a.root.localeCompare(b.root));
42
+ // Group first, then path: a deeper folder (webapps/nested) must not split
43
+ // its parent's run of apps, or the same group heading renders twice.
44
+ return found.toSorted((a, b) => path.dirname(a.root).localeCompare(path.dirname(b.root)) ||
45
+ a.root.localeCompare(b.root));
43
46
  }
44
47
  /** Group label for an app: its parent folder relative to the workspace root. */
45
48
  export function appGroup(root, appRoot) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sitevision-cli",
3
- "version": "1.0.0-beta.16",
3
+ "version": "1.0.0-beta.18",
4
4
  "license": "MIT",
5
5
  "bin": {
6
6
  "svc": "dist/cli.js"