Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions packages/app/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ treq open --port 8080
| `--host, -H` | Host to bind to (default: 127.0.0.1) |
| `--web` | Open the browser dashboard |
| `--expose` | Allow non-loopback binding (disables cookie auth) |
| `--auto-update` / `--no-auto-update` | Enable or disable startup auto-update checks (default: enabled) |

Security: a random token is generated on every launch. `--web` and `--expose` cannot be combined (SSRF protection).

Expand Down Expand Up @@ -323,6 +324,7 @@ treq tui --server http://localhost:8080 --token my-token
|--------|-------------|
| `--server, -s` | Server URL to connect to (default: http://localhost:4097) |
| `--token, -t` | Bearer token for authentication |
| `--auto-update` / `--no-auto-update` | Enable or disable startup auto-update checks (default: enabled) |

### `treq upgrade` - Upgrade treq

Expand All @@ -336,6 +338,16 @@ treq upgrade
treq upgrade 0.3.0
```

### Auto-update

Interactive commands (`treq open`, `treq tui`, `treq web`) perform startup update checks and attempt auto-upgrade for known install methods.

- Checks are cached for 24 hours in `~/.treq/auto-update.json`
- Failed auto-upgrade attempts are backed off for 24 hours for the same target version
- Successful installs apply on the next run (current process continues)
- Use `--no-auto-update` to disable per command
- Use `TREQ_AUTO_UPDATE=0` to disable via environment variable

### Help

```bash
Expand Down
14 changes: 3 additions & 11 deletions packages/app/src/cli/upgrade.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,11 @@
import { Installation } from '../installation';
import { checkForAvailableUpdate } from '../update';

export interface UpdateResult {
version: string;
method: Installation.Method;
method: import('../installation').Installation.Method;
command: string;
}

export async function checkForUpdate(): Promise<UpdateResult | undefined> {
const method = await Installation.method();
const latest = await Installation.latest(method).catch(() => undefined);
if (!latest) return undefined;
if (Installation.VERSION === latest) return undefined;
return {
version: latest,
method,
command: Installation.updateCommand(method, latest)
};
return checkForAvailableUpdate();
}
14 changes: 13 additions & 1 deletion packages/app/src/cmd/open.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { resolve } from 'node:path';
import { flushPendingCookieSaves } from '@t-req/core/cookies/persistence';
import type { CommandModule } from 'yargs';
import { createApp, type ServerConfig } from '../server/app';
import { resolveAutoUpdateEnabled } from '../update';
import {
DEFAULT_HOST,
DEFAULT_PORT,
Expand All @@ -19,6 +20,7 @@ interface OpenOptions {
host: string;
web?: boolean;
expose?: boolean;
autoUpdate?: boolean;
}

export const openCommand: CommandModule<object, OpenOptions> = {
Expand Down Expand Up @@ -51,6 +53,11 @@ export const openCommand: CommandModule<object, OpenOptions> = {
type: 'boolean',
describe: 'Allow non-loopback binding (disables cookie auth for security)',
default: false
})
.option('auto-update', {
type: 'boolean',
describe: 'Automatically check and apply updates on startup',
default: true
}),
handler: async (argv) => {
await runOpen(argv);
Expand Down Expand Up @@ -150,7 +157,12 @@ async function runOpen(argv: OpenOptions): Promise<void> {
// Import and start TUI
const { startTui } = await import('../tui');
try {
await startTui({ serverUrl, token, onExit: shutdown });
await startTui({
serverUrl,
token,
autoUpdate: resolveAutoUpdateEnabled(argv.autoUpdate),
onExit: shutdown
});
} finally {
// Cleanup on TUI exit
await shutdown();
Expand Down
13 changes: 12 additions & 1 deletion packages/app/src/cmd/tui.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import type { CommandModule } from 'yargs';
import { resolveAutoUpdateEnabled } from '../update';

interface TuiOptions {
server: string;
token?: string;
autoUpdate?: boolean;
}

export const tuiCommand: CommandModule<object, TuiOptions> = {
Expand All @@ -19,10 +21,19 @@ export const tuiCommand: CommandModule<object, TuiOptions> = {
type: 'string',
alias: 't',
describe: 'Bearer token for authentication'
},
'auto-update': {
type: 'boolean',
default: true,
describe: 'Automatically check and apply updates on startup'
}
},
handler: async (argv) => {
const { startTui } = await import('../tui');
await startTui({ serverUrl: argv.server, token: argv.token });
await startTui({
serverUrl: argv.server,
token: argv.token,
autoUpdate: resolveAutoUpdateEnabled(argv.autoUpdate)
});
}
};
35 changes: 35 additions & 0 deletions packages/app/src/cmd/web.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { resolve } from 'node:path';
import { flushPendingCookieSaves } from '@t-req/core/cookies/persistence';
import type { CommandModule } from 'yargs';
import { createApp, type ServerConfig } from '../server/app';
import { resolveAutoUpdateEnabled, runAutoUpdate } from '../update';
import {
DEFAULT_HOST,
DEFAULT_PORT,
Expand All @@ -16,6 +17,7 @@ interface WebOptions {
workspace?: string;
port?: number;
host: string;
autoUpdate?: boolean;
}

export const webCommand: CommandModule<object, WebOptions> = {
Expand All @@ -38,6 +40,11 @@ export const webCommand: CommandModule<object, WebOptions> = {
describe: 'Host to bind to',
alias: 'H',
default: DEFAULT_HOST
})
.option('auto-update', {
type: 'boolean',
describe: 'Automatically check and apply updates on startup',
default: true
}),
handler: async (argv) => {
await runWeb(argv);
Expand Down Expand Up @@ -114,6 +121,34 @@ async function runWeb(argv: WebOptions): Promise<void> {
console.log(`Opening browser: ${webUrl}`);
openBrowser(webUrl);

void runAutoUpdate({
enabled: resolveAutoUpdateEnabled(argv.autoUpdate),
interactive: process.stdout.isTTY === true
}).then((outcome) => {
switch (outcome.status) {
case 'updated':
console.log(`Update installed: v${outcome.latestVersion} (applies next run)`);
break;
case 'available_manual':
console.log(`Update available: v${outcome.latestVersion}. Run: ${outcome.command}`);
break;
case 'backoff_skipped':
console.warn(
`Update available: v${outcome.latestVersion}. Auto-update temporarily paused.`
);
console.warn(`Run manually: ${outcome.command}`);
break;
case 'failed':
if (outcome.phase === 'upgrade' && outcome.command) {
console.warn('Auto-update failed. Continuing startup.');
console.warn(`Run manually: ${outcome.command}`);
}
break;
default:
break;
}
});

process.on('SIGTERM', () => void shutdown());
process.on('SIGINT', () => void shutdown());

Expand Down
102 changes: 78 additions & 24 deletions packages/app/src/tui/context/update.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,9 @@
import { createContext, createSignal, onMount, type ParentProps, useContext } from 'solid-js';
import { Installation } from '../../installation';
import type { UpdateInfo } from '../../update';
import { runAutoUpdate } from '../../update';
import { useToast } from '../components/toast';

export interface UpdateInfo {
version: string;
method: Installation.Method;
command: string;
}
export type { UpdateInfo } from '../../update';

export interface UpdateContextValue {
updateInfo: () => UpdateInfo | null;
Expand All @@ -15,7 +12,11 @@ export interface UpdateContextValue {

const UpdateContext = createContext<UpdateContextValue>();

export function UpdateProvider(props: ParentProps) {
export interface UpdateProviderProps extends ParentProps {
autoUpdateEnabled?: boolean;
}

export function UpdateProvider(props: UpdateProviderProps) {
const toast = useToast();
const [updateInfo, setUpdateInfo] = createSignal<UpdateInfo | null>(null);
const updateAvailable = () => updateInfo() !== null;
Expand All @@ -25,23 +26,76 @@ export function UpdateProvider(props: ParentProps) {
});

async function checkForUpdate() {
try {
const method = await Installation.method();
const latestVersion = await Installation.latest(method);
if (!latestVersion) return;
if (Installation.VERSION === latestVersion) return;

const command = Installation.updateCommand(method, latestVersion);
setUpdateInfo({ version: latestVersion, method, command });

toast.show({
variant: 'info',
title: 'Update Available',
message: `v${Installation.VERSION} -> v${latestVersion}\nRun: ${command}`,
duration: 3000
});
} catch {
// Silently fail - network errors, offline, etc.
const outcome = await runAutoUpdate({
enabled: props.autoUpdateEnabled ?? true,
interactive: process.stdout.isTTY === true
});

switch (outcome.status) {
case 'available_manual': {
setUpdateInfo({
version: outcome.latestVersion,
method: outcome.method,
command: outcome.command
});
toast.show({
variant: 'info',
title: 'Update Available',
message: `v${outcome.currentVersion} -> v${outcome.latestVersion}\nRun: ${outcome.command}`,
duration: 3000
});
return;
}

case 'backoff_skipped': {
setUpdateInfo({
version: outcome.latestVersion,
method: outcome.method,
command: outcome.command
});
toast.show({
variant: 'warning',
title: 'Update Available',
message: `Auto-update paused after a recent failure.\nRun: ${outcome.command}`,
duration: 4000
});
return;
}

case 'updated': {
toast.show({
variant: 'success',
title: 'Updated',
message: `Installed v${outcome.latestVersion}. It will apply on your next run.`,
duration: 3500
});
return;
}

case 'failed': {
if (
outcome.phase === 'upgrade' &&
outcome.latestVersion &&
outcome.method &&
outcome.command
) {
setUpdateInfo({
version: outcome.latestVersion,
method: outcome.method,
command: outcome.command
});
toast.show({
variant: 'warning',
title: 'Auto-update failed',
message: `Run manually: ${outcome.command}`,
duration: 4000
});
}
return;
}

default:
return;
}
}

Expand Down
3 changes: 2 additions & 1 deletion packages/app/src/tui/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { createStore } from './store';
export interface TuiConfig {
serverUrl: string;
token?: string;
autoUpdate?: boolean;
onExit?: (reason?: unknown) => Promise<void> | void;
}

Expand Down Expand Up @@ -78,7 +79,7 @@ export async function startTui(config: TuiConfig): Promise<void> {
<LogProvider>
<ToastProvider>
<DialogProvider>
<UpdateProvider>
<UpdateProvider autoUpdateEnabled={config.autoUpdate}>
<App />
</UpdateProvider>
</DialogProvider>
Expand Down
Loading