soja-cli 1.4.0 → 1.5.0
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/README.md +1 -1
- package/dist/application/services/index.js +2 -0
- package/dist/application/services/preference-service.js +17 -0
- package/dist/cli/commands/info.js +1 -0
- package/dist/cli/commands/preferences.js +25 -0
- package/dist/cli/commands/tui.js +1 -1
- package/dist/cli/index.js +3 -0
- package/dist/config/config.js +2 -0
- package/dist/data/sync/index.js +2 -0
- package/dist/ui/Shell.js +5 -1
- package/dist/ui/chrome/StatusBar.js +7 -4
- package/dist/ui/hooks/use-commands.js +3 -0
- package/dist/ui/hooks/use-mouse-toggle.js +20 -0
- package/dist/ui/input/KeyProvider.js +12 -4
- package/dist/ui/screens/HelpScreen.js +1 -0
- package/dist/ui/theme/theme.js +1 -0
- package/docs/NPM_PUBLISH.md +14 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { GitConsole } from '../../git/console.js';
|
|
2
2
|
import { FolderService } from './folder-service.js';
|
|
3
|
+
import { PreferenceService } from './preference-service.js';
|
|
3
4
|
import { GitWorkflowService } from './git-workflow-service.js';
|
|
4
5
|
import { ProjectService } from './project-service.js';
|
|
5
6
|
import { SessionService } from './session-service.js';
|
|
@@ -16,6 +17,7 @@ export function createServices(repos, config, options) {
|
|
|
16
17
|
tasks,
|
|
17
18
|
git: new GitWorkflowService(tasks, projects, options.git),
|
|
18
19
|
folders: new FolderService(config),
|
|
20
|
+
preferences: new PreferenceService(config),
|
|
19
21
|
gitConsole: options.gitConsole ?? new GitConsole(),
|
|
20
22
|
environment: { mode: 'local' },
|
|
21
23
|
backups: options.backups,
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/** Per-machine interface preferences, kept in the config file. */
|
|
2
|
+
export class PreferenceService {
|
|
3
|
+
config;
|
|
4
|
+
constructor(config) {
|
|
5
|
+
this.config = config;
|
|
6
|
+
}
|
|
7
|
+
/** Whether the interface reports the mouse. On unless turned off. */
|
|
8
|
+
mouse() {
|
|
9
|
+
return this.config.load()?.mouse ?? true;
|
|
10
|
+
}
|
|
11
|
+
/** Before setup there is no config file yet: the choice then lasts for this session only. */
|
|
12
|
+
setMouse(on) {
|
|
13
|
+
const current = this.config.load();
|
|
14
|
+
if (current)
|
|
15
|
+
this.config.save({ ...current, mouse: on });
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -40,6 +40,7 @@ const COMMANDS = [
|
|
|
40
40
|
['logout', 'Sign out and go back to local mode'],
|
|
41
41
|
['account delete [--yes]', 'Delete the Online account and anonymize shared authorship'],
|
|
42
42
|
['mode [local|remote]', 'Show or switch where SOJA keeps data'],
|
|
43
|
+
['mouse [on|off]', 'Show or switch whether the interface uses the mouse (M in the interface)'],
|
|
43
44
|
['whoami', 'Current user, workspace and mode'],
|
|
44
45
|
['import-local [--from <slug>]', 'Bring local-mode projects, tasks and history to the team server (owners)'],
|
|
45
46
|
['sync [--dismiss]', 'Sync now (remote mode); shows conflicts and rejected changes'],
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { PreferenceService } from '../../application/services/preference-service.js';
|
|
2
|
+
import { FileConfigStore } from '../../config/config.js';
|
|
3
|
+
import { resolvePaths } from '../../config/paths.js';
|
|
4
|
+
import { SojaError, ValidationError } from '../../domain/errors.js';
|
|
5
|
+
import { bold, dim, print, success } from '../output.js';
|
|
6
|
+
import { parseCommand } from './args.js';
|
|
7
|
+
/** `soja mouse [on|off]`: whether the interface uses the mouse on this machine. */
|
|
8
|
+
export async function mouseCommand(args) {
|
|
9
|
+
const { positionals } = parseCommand(args, {});
|
|
10
|
+
const store = new FileConfigStore(resolvePaths().configFile);
|
|
11
|
+
const preferences = new PreferenceService(store);
|
|
12
|
+
const target = positionals[0];
|
|
13
|
+
if (!target) {
|
|
14
|
+
print(`mouse ${bold(preferences.mouse() ? 'on' : 'off')}`);
|
|
15
|
+
if (process.env.SOJA_MOUSE === '0')
|
|
16
|
+
print(dim('SOJA_MOUSE=0 turns it off for this shell anyway.'));
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
if (target !== 'on' && target !== 'off')
|
|
20
|
+
throw new ValidationError('Use `soja mouse on` or `soja mouse off`.');
|
|
21
|
+
if (!store.load())
|
|
22
|
+
throw new SojaError('SOJA is not set up yet.', { hint: 'Run `soja` once; `M` in the interface also switches the mouse.' });
|
|
23
|
+
preferences.setMouse(target === 'on');
|
|
24
|
+
success(`Mouse ${bold(target)}${target === 'off' ? dim(' · the terminal selects and copies text as usual; M in the interface turns it back on') : ''}.`);
|
|
25
|
+
}
|
package/dist/cli/commands/tui.js
CHANGED
|
@@ -31,7 +31,7 @@ export async function runInterface(options = {}) {
|
|
|
31
31
|
const updates = createUpdater();
|
|
32
32
|
// Light or dark palette, from the terminal's own background (SOJA_THEME overrides).
|
|
33
33
|
setThemeMode(await detectThemeMode());
|
|
34
|
-
const instance = render(_jsx(App, { services: runtime.services, welcomeOnLocal: savedConfig?.mode === 'remote' && !hasRemoteToken, updates: () => updates.cachedCheck(), mouse: process.env.SOJA_MOUSE !== '0', ...(options.route ? { initialRoute: options.route } : {}) }), { alternateScreen: true, exitOnCtrlC: true });
|
|
34
|
+
const instance = render(_jsx(App, { services: runtime.services, welcomeOnLocal: savedConfig?.mode === 'remote' && !hasRemoteToken, updates: () => updates.cachedCheck(), mouse: process.env.SOJA_MOUSE !== '0' && runtime.services.preferences.mouse(), ...(options.route ? { initialRoute: options.route } : {}) }), { alternateScreen: true, exitOnCtrlC: true });
|
|
35
35
|
await instance.waitUntilExit();
|
|
36
36
|
reopenInCurrentMode = new FileConfigStore(runtime.paths.configFile).load()?.mode !== runtime.services.environment.mode;
|
|
37
37
|
}
|
package/dist/cli/index.js
CHANGED
|
@@ -11,6 +11,7 @@ import { chatCommand } from './commands/chat.js';
|
|
|
11
11
|
import { importLocalCommand } from './commands/import.js';
|
|
12
12
|
import { branchCommand, commitCommand, mergeCommand, pullRequestCommand, pushCommand } from './commands/git-ops.js';
|
|
13
13
|
import { printHelp, printVersion } from './commands/info.js';
|
|
14
|
+
import { mouseCommand } from './commands/preferences.js';
|
|
14
15
|
import { projectCommand } from './commands/project.js';
|
|
15
16
|
import { taskCommand } from './commands/task.js';
|
|
16
17
|
import { startCommand } from './commands/start.js';
|
|
@@ -74,6 +75,8 @@ async function main(argv) {
|
|
|
74
75
|
return whoamiCommand();
|
|
75
76
|
case 'mode':
|
|
76
77
|
return modeCommand(rest);
|
|
78
|
+
case 'mouse':
|
|
79
|
+
return mouseCommand(rest);
|
|
77
80
|
case 'sync':
|
|
78
81
|
return syncCommand(rest);
|
|
79
82
|
case 'chat':
|
package/dist/config/config.js
CHANGED
|
@@ -28,6 +28,8 @@ export const configSchema = z
|
|
|
28
28
|
* repository picker.
|
|
29
29
|
*/
|
|
30
30
|
parentFolders: z.array(z.string().min(1)).default([]),
|
|
31
|
+
/** Whether the interface reports the mouse (`soja mouse on|off`, `M` in the interface). On when unset. */
|
|
32
|
+
mouse: z.boolean().optional(),
|
|
31
33
|
remote: remoteSchema.optional(),
|
|
32
34
|
})
|
|
33
35
|
.refine((config) => config.mode !== 'remote' || config.remote !== undefined, {
|
package/dist/data/sync/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { basename } from 'node:path';
|
|
2
2
|
import { GitWorkflowService } from '../../application/services/git-workflow-service.js';
|
|
3
3
|
import { FolderService } from '../../application/services/folder-service.js';
|
|
4
|
+
import { PreferenceService } from '../../application/services/preference-service.js';
|
|
4
5
|
import { ProjectService } from '../../application/services/project-service.js';
|
|
5
6
|
import { TaskService } from '../../application/services/task-service.js';
|
|
6
7
|
import { createLocalRepositories } from '../local/index.js';
|
|
@@ -85,6 +86,7 @@ export function createReplicaServices(api, handle, config, git, gitConsole, WebS
|
|
|
85
86
|
...(localDatabaseFile ? { importer: new LocalImporter(localDatabaseFile, config, api, store, engine) } : {}),
|
|
86
87
|
git: new GitWorkflowService(tasks, projects, git),
|
|
87
88
|
folders: new FolderService(config),
|
|
89
|
+
preferences: new PreferenceService(config),
|
|
88
90
|
gitConsole,
|
|
89
91
|
environment: { mode: 'remote', server: api.baseUrl },
|
|
90
92
|
backups,
|
package/dist/ui/Shell.js
CHANGED
|
@@ -30,6 +30,7 @@ import { AccessScreen } from './screens/AccessScreen.js';
|
|
|
30
30
|
import { ApiClient } from '../data/remote/api-client.js';
|
|
31
31
|
import { CredentialStore } from '../config/credentials.js';
|
|
32
32
|
import { resolvePaths } from '../config/paths.js';
|
|
33
|
+
import { useMouseToggle } from './hooks/use-mouse-toggle.js';
|
|
33
34
|
/**
|
|
34
35
|
* The running app, laid out like a desktop application: title bar, sidebar,
|
|
35
36
|
* the screen stack in the main area, status bar, and floating windows and
|
|
@@ -73,6 +74,7 @@ function Frame() {
|
|
|
73
74
|
const timer = setInterval(() => void followPullRequests(), 180_000);
|
|
74
75
|
return () => clearInterval(timer);
|
|
75
76
|
}, [followPullRequests]);
|
|
77
|
+
const mouseToggle = useMouseToggle();
|
|
76
78
|
useKeys(Layer.global, (input, key) => {
|
|
77
79
|
if (input === ':' || (key.ctrl && input === 'k'))
|
|
78
80
|
openPalette();
|
|
@@ -90,6 +92,8 @@ function Frame() {
|
|
|
90
92
|
go({ type: 'push', route: { name: 'workspaces' } });
|
|
91
93
|
else if (input === 'A')
|
|
92
94
|
go({ type: 'push', route: { name: 'access' } });
|
|
95
|
+
else if (input === 'M')
|
|
96
|
+
mouseToggle.toggle();
|
|
93
97
|
else if (input === '#' && chat)
|
|
94
98
|
go({ type: 'push', route: { name: 'chat' } });
|
|
95
99
|
else if (key.escape && stack.length > 1)
|
|
@@ -113,7 +117,7 @@ function Frame() {
|
|
|
113
117
|
// The screen under a window stays visible, but only the window takes keys and clicks.
|
|
114
118
|
const active = top && !overlay;
|
|
115
119
|
return (_jsx(Box, { display: top ? 'flex' : 'none', flexDirection: "column", flexGrow: 1, children: _jsx(ActiveScope, { active: active, children: _jsx(ScreenFor, { route: entry, active: active }) }) }, `${index}:${JSON.stringify(entry)}`));
|
|
116
|
-
}) })] }), _jsx(Box, { height: FOOTER_GAP_ROWS, flexShrink: 0 }), _jsx(StatusBar, { columns: columns, mode: overlay ? modeForOverlay(overlay) : modeFor(route), hints: hints, right: server ? undefined : 'local' }), overlay ? (_jsx(ActiveScope, { active: true, children: _jsx(OverlayFor, { overlay: overlay }) })) : null, flash ? _jsx(Toast, { flash: flash, columns: columns, rows: rows }) : null] }));
|
|
120
|
+
}) })] }), _jsx(Box, { height: FOOTER_GAP_ROWS, flexShrink: 0 }), _jsx(StatusBar, { columns: columns, mode: overlay ? modeForOverlay(overlay) : modeFor(route), hints: hints, right: server ? undefined : 'local', mouse: { on: mouseToggle.enabled, onToggle: mouseToggle.toggle, active: !overlay } }), overlay ? (_jsx(ActiveScope, { active: true, children: _jsx(OverlayFor, { overlay: overlay }) })) : null, flash ? _jsx(Toast, { flash: flash, columns: columns, rows: rows }) : null] }));
|
|
117
121
|
}
|
|
118
122
|
function ScreenFor({ route, active }) {
|
|
119
123
|
switch (route.name) {
|
|
@@ -1,18 +1,21 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Box, Text } from 'ink';
|
|
3
|
-
import {
|
|
3
|
+
import { Clickable } from '../kit/Clickable.js';
|
|
4
|
+
import { palette, symbols } from '../theme/theme.js';
|
|
4
5
|
import { APP_SLOGAN, WORDMARK_WIDTH } from '../branding/brand.js';
|
|
5
6
|
import { Logo } from '../branding/Logo.js';
|
|
6
7
|
import { FOOTER_BRAND_GAP_ROWS, footerRowsFor } from '../hooks/use-layout.js';
|
|
8
|
+
const mouseLabel = (on) => ` ${on ? symbols.active : symbols.inactive} mouse ${on ? 'on' : 'off'} `;
|
|
7
9
|
/**
|
|
8
10
|
* The bottom bar: a pill with where you are, then the shortcuts that work
|
|
9
11
|
* right now as keys. Shortcuts that do not fit are dropped from the end.
|
|
10
12
|
*/
|
|
11
|
-
export function StatusBar({ columns, mode, hints, right }) {
|
|
13
|
+
export function StatusBar({ columns, mode, hints, right, mouse }) {
|
|
12
14
|
const footerRows = footerRowsFor(columns);
|
|
13
15
|
const inlineBrand = columns >= WORDMARK_WIDTH + APP_SLOGAN.length + 4;
|
|
14
16
|
const sloganWidth = inlineBrand ? columns - WORDMARK_WIDTH - 4 : columns - 2;
|
|
15
|
-
const
|
|
17
|
+
const mouseWidth = mouse ? mouseLabel(mouse.on).length + 4 : 0;
|
|
18
|
+
const available = columns - mode.length - 4 - (right ? right.length + 3 : 0) - mouseWidth;
|
|
16
19
|
const shown = [];
|
|
17
20
|
let used = 0;
|
|
18
21
|
for (const hint of hints) {
|
|
@@ -22,5 +25,5 @@ export function StatusBar({ columns, mode, hints, right }) {
|
|
|
22
25
|
shown.push(hint);
|
|
23
26
|
used += size;
|
|
24
27
|
}
|
|
25
|
-
return (_jsxs(Box, { width: columns, height: footerRows, flexDirection: "column", backgroundColor: palette.bar, flexShrink: 0, children: [_jsxs(Box, { width: columns, height: 1, backgroundColor: palette.bar, justifyContent: "space-between", flexShrink: 0, children: [_jsxs(Text, { backgroundColor: palette.bar, wrap: "truncate-end", children: [_jsx(Text, { backgroundColor: palette.accent, color: palette.onAccent, bold: true, children: ` ${mode.toUpperCase()} ` }), shown.map(([keys, action]) => (_jsxs(Text, { children: [_jsx(Text, { backgroundColor: palette.bar, children: " " }), _jsx(Text, { backgroundColor: palette.neutralSoft, color: palette.text, bold: true, children: ` ${keys} ` }), _jsx(Text, { backgroundColor: palette.bar, color: palette.muted, children: ` ${action}` })] }, keys + action)))] }), right ? (_jsx(Text, { backgroundColor: palette.bar, color: palette.faint, children: `${right} ` })) : null] }), _jsx(Box, { height: FOOTER_BRAND_GAP_ROWS, flexShrink: 0 }), _jsxs(Box, { width: columns, height: footerRows - 1 - FOOTER_BRAND_GAP_ROWS, paddingX: 1, backgroundColor: palette.bar, flexShrink: 0, flexDirection: inlineBrand ? 'row' : 'column', alignItems: inlineBrand ? 'center' : 'flex-start', justifyContent: "center", children: [_jsx(Logo, { cursorVisible: true }), _jsx(Box, { width: sloganWidth, marginLeft: inlineBrand ? 2 : 0, children: _jsx(Text, { backgroundColor: palette.bar, color: palette.muted, wrap: "wrap", children: APP_SLOGAN }) })] })] }));
|
|
28
|
+
return (_jsxs(Box, { width: columns, height: footerRows, flexDirection: "column", backgroundColor: palette.bar, flexShrink: 0, children: [_jsxs(Box, { width: columns, height: 1, backgroundColor: palette.bar, justifyContent: "space-between", flexShrink: 0, children: [_jsxs(Text, { backgroundColor: palette.bar, wrap: "truncate-end", children: [_jsx(Text, { backgroundColor: palette.accent, color: palette.onAccent, bold: true, children: ` ${mode.toUpperCase()} ` }), shown.map(([keys, action]) => (_jsxs(Text, { children: [_jsx(Text, { backgroundColor: palette.bar, children: " " }), _jsx(Text, { backgroundColor: palette.neutralSoft, color: palette.text, bold: true, children: ` ${keys} ` }), _jsx(Text, { backgroundColor: palette.bar, color: palette.muted, children: ` ${action}` })] }, keys + action)))] }), _jsxs(Box, { flexShrink: 0, children: [right ? (_jsx(Text, { backgroundColor: palette.bar, color: palette.faint, children: `${right} ` })) : null, mouse ? (_jsx(Clickable, { onClick: mouse.onToggle, active: mouse.active, children: _jsxs(Text, { backgroundColor: palette.bar, children: [_jsx(Text, { backgroundColor: palette.neutralSoft, color: palette.text, bold: true, children: ' M ' }), _jsx(Text, { backgroundColor: palette.bar, color: mouse.on ? palette.muted : palette.warning, children: mouseLabel(mouse.on) })] }) })) : null] })] }), _jsx(Box, { height: FOOTER_BRAND_GAP_ROWS, flexShrink: 0 }), _jsxs(Box, { width: columns, height: footerRows - 1 - FOOTER_BRAND_GAP_ROWS, paddingX: 1, backgroundColor: palette.bar, flexShrink: 0, flexDirection: inlineBrand ? 'row' : 'column', alignItems: inlineBrand ? 'center' : 'flex-start', justifyContent: "center", children: [_jsx(Logo, { cursorVisible: true }), _jsx(Box, { width: sloganWidth, marginLeft: inlineBrand ? 2 : 0, children: _jsx(Text, { backgroundColor: palette.bar, color: palette.muted, wrap: "wrap", children: APP_SLOGAN }) })] })] }));
|
|
26
29
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { FILTER_LABELS, TASK_FILTERS } from '../../application/filters.js';
|
|
2
2
|
import { useAppState } from '../app-state.js';
|
|
3
3
|
import { useFlows } from './use-flows.js';
|
|
4
|
+
import { useMouseToggle } from './use-mouse-toggle.js';
|
|
4
5
|
/** The command palette: every major action, filterable by typing. */
|
|
5
6
|
export function useCommandPalette(newTaskProjectId) {
|
|
6
7
|
const { openOverlay, go, quit, services, session, notify, setHomeFilter } = useAppState();
|
|
@@ -10,6 +11,7 @@ export function useCommandPalette(newTaskProjectId) {
|
|
|
10
11
|
go({ type: 'reset' });
|
|
11
12
|
};
|
|
12
13
|
const flows = useFlows();
|
|
14
|
+
const mouse = useMouseToggle();
|
|
13
15
|
return () => {
|
|
14
16
|
const commands = [
|
|
15
17
|
{ id: 'new-task', label: 'New task', key: 'n', run: () => openOverlay({ kind: 'new-task', projectId: newTaskProjectId }) },
|
|
@@ -40,6 +42,7 @@ export function useCommandPalette(newTaskProjectId) {
|
|
|
40
42
|
},
|
|
41
43
|
}]
|
|
42
44
|
: []),
|
|
45
|
+
{ id: 'mouse', label: mouse.enabled ? 'Mouse off' : 'Mouse on', key: 'M', run: mouse.toggle },
|
|
43
46
|
{ id: 'help', label: 'Help', key: '?', run: () => go({ type: 'push', route: { name: 'help' } }) },
|
|
44
47
|
{ id: 'quit', label: 'Quit', key: 'q', run: quit },
|
|
45
48
|
];
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { useAppState } from '../app-state.js';
|
|
2
|
+
import { useMouseSwitch } from '../input/KeyProvider.js';
|
|
3
|
+
/**
|
|
4
|
+
* Turns the mouse on or off now and remembers it on this machine. With the
|
|
5
|
+
* mouse off the terminal selects and copies text as usual; `M` brings it back.
|
|
6
|
+
*/
|
|
7
|
+
export function useMouseToggle() {
|
|
8
|
+
const { services, notify } = useAppState();
|
|
9
|
+
const mouse = useMouseSwitch();
|
|
10
|
+
const toggle = () => {
|
|
11
|
+
const next = !mouse.enabled;
|
|
12
|
+
mouse.set(next);
|
|
13
|
+
services.preferences.setMouse(next);
|
|
14
|
+
if (next)
|
|
15
|
+
notify('Mouse on', 'success');
|
|
16
|
+
else
|
|
17
|
+
notify('Mouse off', 'success', 'Press M to turn it back on.');
|
|
18
|
+
};
|
|
19
|
+
return { enabled: mouse.enabled, toggle };
|
|
20
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { useApp, useInput, useStdout } from 'ink';
|
|
3
|
-
import { createContext, useCallback, useContext, useEffect, useLayoutEffect, useRef, useState } from 'react';
|
|
3
|
+
import { createContext, useCallback, useContext, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
|
|
4
4
|
import { KeyDispatcher, Layer } from './dispatcher.js';
|
|
5
5
|
import { MOUSE_OFF, MOUSE_ON, MouseDispatcher, parseMouse, rectOf } from './mouse.js';
|
|
6
6
|
const InputContext = createContext(null);
|
|
@@ -9,7 +9,9 @@ const InputContext = createContext(null);
|
|
|
9
9
|
* reports to the clickable regions. A mouse report never reaches a key handler.
|
|
10
10
|
*/
|
|
11
11
|
export function KeyProvider({ children, mouse = false }) {
|
|
12
|
-
const [
|
|
12
|
+
const [dispatchers] = useState(() => ({ keys: new KeyDispatcher(), mouse: new MouseDispatcher() }));
|
|
13
|
+
const [mouseEnabled, setMouseEnabled] = useState(mouse);
|
|
14
|
+
const context = useMemo(() => ({ ...dispatchers, mouseEnabled, setMouseEnabled }), [dispatchers, mouseEnabled]);
|
|
13
15
|
const { stdout } = useStdout();
|
|
14
16
|
useInput((input, key) => {
|
|
15
17
|
const event = parseMouse(input);
|
|
@@ -18,8 +20,9 @@ export function KeyProvider({ children, mouse = false }) {
|
|
|
18
20
|
else
|
|
19
21
|
context.keys.dispatch(input, key);
|
|
20
22
|
});
|
|
23
|
+
// Turning the mouse off hands selection and copy back to the terminal.
|
|
21
24
|
useEffect(() => {
|
|
22
|
-
if (!
|
|
25
|
+
if (!mouseEnabled)
|
|
23
26
|
return;
|
|
24
27
|
stdout.write(MOUSE_ON);
|
|
25
28
|
// Whatever way SOJA ends, the terminal must stop sending mouse reports.
|
|
@@ -29,9 +32,14 @@ export function KeyProvider({ children, mouse = false }) {
|
|
|
29
32
|
off();
|
|
30
33
|
process.off('exit', off);
|
|
31
34
|
};
|
|
32
|
-
}, [
|
|
35
|
+
}, [mouseEnabled, stdout]);
|
|
33
36
|
return _jsx(InputContext.Provider, { value: context, children: children });
|
|
34
37
|
}
|
|
38
|
+
/** The mouse switch: whether it is on, and turning it on or off for this session. */
|
|
39
|
+
export function useMouseSwitch() {
|
|
40
|
+
const { mouseEnabled, setMouseEnabled } = useInputContext();
|
|
41
|
+
return { enabled: mouseEnabled, set: setMouseEnabled };
|
|
42
|
+
}
|
|
35
43
|
function useInputContext() {
|
|
36
44
|
const context = useContext(InputContext);
|
|
37
45
|
if (!context)
|
package/dist/ui/theme/theme.js
CHANGED
package/docs/NPM_PUBLISH.md
CHANGED
|
@@ -20,15 +20,27 @@ Luego, en npmjs.com → `soja-cli` → **Settings → Trusted Publisher → GitH
|
|
|
20
20
|
- Repository: `soja-cli`
|
|
21
21
|
- Workflow filename: `publish.yml`
|
|
22
22
|
- Environment: vacío
|
|
23
|
+
- Permisos: **npm stage publish** (solo dejar versiones en espera, no publicarlas)
|
|
23
24
|
|
|
24
|
-
Y en **Settings → Publishing access**, elige *Require two-factor authentication and disallow
|
|
25
|
+
Y en **Settings → Publishing access**, elige *Require two-factor authentication and disallow bypass 2fa tokens*.
|
|
26
|
+
|
|
27
|
+
Con eso, ni el workflow ni un token robado pueden publicar solos: el workflow deja la versión **en espera** (*staged publishing*) y solo sale a npm cuando la cuenta propietaria la aprueba con 2FA.
|
|
25
28
|
|
|
26
29
|
## Versiones siguientes (automático)
|
|
27
30
|
|
|
28
31
|
Sube la versión en `package.json` y `package-lock.json`, mueve el changelog y haz commit `release: vX.Y.Z` en `main` (por PR). Al subir el tag `vX.Y.Z`:
|
|
29
32
|
|
|
30
33
|
- `release.yml` crea el release de GitHub con el paquete adjunto;
|
|
31
|
-
- `publish.yml` valida (lint, tests, build, que el tag coincida con la versión), empaqueta sin *source maps* en un job sin permisos de publicación y
|
|
34
|
+
- `publish.yml` valida (lint, tests, build, que el tag coincida con la versión), empaqueta sin *source maps* en un job sin permisos de publicación y deja ese mismo archivo en espera en npm desde otro job, con OIDC (sin tokens de larga duración) y *provenance* (npm registra desde qué commit y workflow se construyó). Si la versión ya está en npm, no hace nada.
|
|
35
|
+
|
|
36
|
+
Después apruébala tú (necesita npm ≥ 11.17 y tu 2FA):
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
npm stage list soja-cli # muestra la versión en espera y su id
|
|
40
|
+
npm stage approve <stage-id> # la publica
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
El resumen del run de `publish.yml` en GitHub recuerda estos comandos. Si algo no cuadra, `npm stage reject <stage-id>` la descarta.
|
|
32
44
|
|
|
33
45
|
## Qué implica que sea público
|
|
34
46
|
|