spinuptui 1.3.0-alpha.1 → 1.3.0-alpha.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -11,6 +11,17 @@ version; such changes are called out here.
11
11
 
12
12
  ## [Unreleased]
13
13
 
14
+ ## [1.3.0-alpha.2] - 2026-10-26
15
+
16
+ A pre-release, published on npm's `next` tag (`bun add -g spinuptui@next --no-cache`). `@latest` and Homebrew stay on 1.2.0.
17
+
18
+ ### Added
19
+ - **Scheduler: follow-ups that run themselves.** Queue a job for later (`spinuptui scheduler add verify-410s --kind spinuptui --when "2026-10-21 08:00" --expires 2026-10-28 --created-by "…" -- incidents example.com`) and this machine runs it at that time, writes a report, and sends you the result through Beacon. Jobs can be a SpinupTUI command, a shell script, or a prompt for an agent CLI (Claude Code or Codex, read-only by default). They can be one-offs or recurring (`every 30m`, `weekdays 08:00`, `mon,thu 08:00`), and every job carries an expiry date so a forgotten one stops on its own. Jobs and reports live in `~/SpinupTUI/scheduler/`, a visible folder of your own. Make `~/SpinupTUI/` a git repository and Scheduler pulls it before each run and commits and pushes the reports, so you can queue a job from any machine with a commit. Only one machine runs a repository's jobs: the first station to tick it claims it, so a second machine with a dispatcher can't run them twice. `scheduler source claim` and `release` move them between machines. A job can say what happens when the station reaches it late (`--late run`, `skip`, or a grace window like `2h`). A skipped job is paged, not dropped silently. `scheduler list`, `show`, `run`, `cancel` and `validate` round it out. See [docs/scheduler.md](docs/scheduler.md).
20
+ - **`spinuptui scheduler install --plan|--apply`** sets up the one LaunchAgent everything runs on: a tick at login and every five minutes, with the PATH a bare launchd environment lacks written into it. `--apply` doesn't report success until a tick has actually run under launchd. `uninstall` removes it and keeps your jobs and reports. Scheduler shows up live on the Modules tab (`6`), where `⏎` shows the same plan and installs it on confirm, and in `spinuptui modules`, which reports how long ago the last tick ran. Modules will declare their own recurring work for Scheduler to run, so a station needs only this one unit. The design is in [docs/2026-10-20_scheduler-spec.md](docs/2026-10-20_scheduler-spec.md).
21
+
22
+ ### Changed
23
+ - **Headless commands start about three times faster.** `beacon send`, `incidents`, `ssh-exec`, `scheduler` and the rest no longer load the dashboard's renderer before looking at their arguments, which matters for something started every five minutes.
24
+
14
25
  ## [1.3.0-alpha.1] - 2026-10-21
15
26
 
16
27
  A pre-release, published on npm's `next` tag (`bun add -g spinuptui@next --no-cache`). `@latest` and Homebrew stay on 1.2.0. It continues `1.2.0-alpha.1` under a new number, because stable 1.2.0 shipped without the modules: it has Beacon and the Modules tab (see [1.2.0-alpha.1]) plus everything in [1.2.0]. The stable 1.3.0 will roll these notes up with the next modules.
@@ -1506,7 +1517,8 @@ Initial tagged release.
1506
1517
  ### Notes
1507
1518
  - Read-only release: works with a SpinupWP **Read Only** API token.
1508
1519
 
1509
- [Unreleased]: https://github.com/mwender/spinupwp-tui/compare/v1.3.0-alpha.1...HEAD
1520
+ [Unreleased]: https://github.com/mwender/spinupwp-tui/compare/v1.3.0-alpha.2...HEAD
1521
+ [1.3.0-alpha.2]: https://github.com/mwender/spinupwp-tui/compare/v1.3.0-alpha.1...v1.3.0-alpha.2
1510
1522
  [1.3.0-alpha.1]: https://github.com/mwender/spinupwp-tui/compare/v1.2.0...v1.3.0-alpha.1
1511
1523
  [1.2.0]: https://github.com/mwender/spinupwp-tui/compare/v1.1.2...v1.2.0
1512
1524
  [1.2.0-alpha.1]: https://github.com/mwender/spinupwp-tui/compare/v1.1.2...v1.2.0-alpha.1
package/README.md CHANGED
@@ -82,6 +82,11 @@
82
82
  pages you on Telegram from any cron job, watcher or agent, with retries, long
83
83
  messages split instead of dropped, and a send log. A guided setup creates the
84
84
  bot connection for you. → [docs/beacon.md](docs/beacon.md)
85
+ Then **Scheduler**: follow-ups that run themselves. Queue "check this
86
+ tomorrow at 8" from any session, and the result arrives through Beacon. Jobs
87
+ and reports live in `~/SpinupTUI/scheduler/`; make that folder a git
88
+ repository to queue from any machine. One LaunchAgent ticks every
89
+ 5 minutes and is the clock the other modules run on. → [docs/scheduler.md](docs/scheduler.md)
85
90
  - **Non-interactive CLI subcommands for external tooling** — `spinuptui ssh <domain>`
86
91
  resolves a domain to a live SSH target with a live connectivity probe;
87
92
  `spinuptui ssh-exec <domain> -- <command>` runs a command over SSH, but only
@@ -375,7 +380,7 @@ These can be set in `config.json` or via an environment variable:
375
380
  | `w` | Open the selected server/site in the SpinupWP web app |
376
381
  | `/` | Jump to global search |
377
382
  | `A` | Accounts — switch, add, rename, or remove SpinupWP accounts (from anywhere) |
378
- | `⏎` / `s` | Set up the selected module — Beacon opens its guided Telegram setup (Modules tab) |
383
+ | `⏎` / `s` | Set up the selected module — Beacon opens its guided Telegram setup, Scheduler its install plan (Modules tab) |
379
384
  | `t` | Send a test page through Beacon (Modules tab, once Beacon is set up) |
380
385
  | `r` | Refresh data from the API |
381
386
  | `i` | Explain the current screen (what each pane and key does) |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spinuptui",
3
- "version": "1.3.0-alpha.1",
3
+ "version": "1.3.0-alpha.2",
4
4
  "description": "SpinupTUI — a terminal dashboard for browsing, monitoring, and managing your SpinupWP servers and sites.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/config.ts CHANGED
@@ -109,6 +109,12 @@ export interface AppConfig {
109
109
  // it relies on the file's 600 mode. The env channel (SPINUPTUI_BEACON_*) is
110
110
  // resolved in lib/beacon.ts, not here, so this is only what's stored.
111
111
  beacon: BeaconStored | null
112
+ // Scheduler's sources and installed dispatcher (lib/scheduler*.ts,
113
+ // docs/2026-10-20_scheduler-spec.md). Machine-level: a station runs one clock.
114
+ scheduler: SchedulerStored | null
115
+ // Where modules put the user's work (Scheduler's jobs and reports…); null =
116
+ // the default, ~/SpinupTUI. SPINUPTUI_HOME overrides. See modules/contract.ts.
117
+ homeDir: string | null
112
118
  }
113
119
 
114
120
  export interface BeaconTelegramChannel {
@@ -126,6 +132,18 @@ export interface BeaconStored {
126
132
  channels?: Record<string, BeaconChannel>
127
133
  }
128
134
 
135
+ export interface SchedulerStored {
136
+ // Named job sources beyond the built-in `local` one: directories holding
137
+ // jobs/ and runs/, pulled and pushed when they are git repositories.
138
+ sources?: Record<string, { path: string }>
139
+ defaultSource?: string
140
+ // Git sources this machine only queues to: it never claims or runs them
141
+ // (set by `scheduler source release`, cleared by `claim`).
142
+ queueOnly?: string[]
143
+ // Written by `scheduler install --apply`, cleared by uninstall.
144
+ dispatcher?: { label: string; unitPath: string; bin: string; installedAt: string }
145
+ }
146
+
129
147
  export interface ServerProviderRef {
130
148
  id: number
131
149
  databaseProviderId?: number
@@ -224,6 +242,8 @@ export interface StoredConfig {
224
242
  kumaMonitors?: Record<string, KumaMonitorRef>
225
243
  lastSeenVersion?: string
226
244
  beacon?: BeaconStored
245
+ scheduler?: SchedulerStored
246
+ homeDir?: string
227
247
  }
228
248
 
229
249
  // ---- Accounts (profiles) -------------------------------------------------------
@@ -510,6 +530,8 @@ export function loadConfig(): AppConfig {
510
530
  kumaMonitors: stored.kumaMonitors ?? {},
511
531
  lastSeenVersion: stored.lastSeenVersion?.trim() || null,
512
532
  beacon: stored.beacon ?? null,
533
+ scheduler: stored.scheduler ?? null,
534
+ homeDir: stored.homeDir?.trim() || null,
513
535
  }
514
536
  }
515
537
 
package/src/index.tsx CHANGED
@@ -1,22 +1,15 @@
1
1
  #!/usr/bin/env bun
2
- // Entry point. Handles a few non-interactive CLI subcommands, then boots the
3
- // OpenTUI renderer and mounts either the first-run onboarding wizard (no token
4
- // yet) or the main app wrapped in the data store.
2
+ // Entry point. Handles the non-interactive CLI subcommands, then hands off to
3
+ // the dashboard (ui/main.tsx).
5
4
 
6
5
  // First, before any config read or spawn: keep a site checkout's .env from
7
6
  // leaking into child processes (see the module).
8
7
  import "./lib/cwdDotenv.ts"
9
- import { useCallback, useMemo, useState } from "react"
10
- import { createCliRenderer } from "@opentui/core"
11
- import { createRoot } from "@opentui/react"
8
+ // Nothing UI-related is imported statically: headless commands (the Scheduler
9
+ // tick every 5 minutes, beacon send, incidents…) never load the renderer or
10
+ // React. The dashboard is a dynamic import at the bottom.
12
11
  import pkg from "../package.json" with { type: "json" }
13
- import { hasToken, configPath, loadConfig, pinActiveProfile, setActiveProfile, listProfiles } from "./config.ts"
14
- import { StoreProvider } from "./ui/store.tsx"
15
- import { App } from "./ui/App.tsx"
16
- import { AccountsContext, type AccountsApi } from "./ui/accounts.tsx"
17
- import { resetWpCoreJobs } from "./ui/wpCoreJobs.tsx"
18
- import { Onboarding } from "./ui/Onboarding.tsx"
19
- import { isDevMode } from "./dev/devMode.ts"
12
+ import { configPath, loadConfig, listProfiles } from "./config.ts"
20
13
  import { SpinupWPClient } from "./api/client.ts"
21
14
  import { resolveSshAccess } from "./lib/cliSsh.ts"
22
15
  import { resolveIncidents } from "./lib/cliIncidents.ts"
@@ -25,6 +18,7 @@ import { execSshCommand } from "./lib/sshExec.ts"
25
18
  import { runPullFiles, runPullDb } from "./lib/cliPull.ts"
26
19
  import { runBeacon, runModules } from "./lib/cliBeacon.ts"
27
20
  import { runOffline, runOnline } from "./lib/cliOffline.ts"
21
+ import { runScheduler } from "./lib/cliScheduler.ts"
28
22
 
29
23
  const args = process.argv.slice(2)
30
24
  const positionals = args.filter((a) => !a.startsWith("-"))
@@ -113,6 +107,11 @@ Usage:
113
107
  Add a channel from a file holding TELEGRAM_BOT_TOKEN and
114
108
  TELEGRAM_CHAT_ID; sends a test message before saving.
115
109
  spinuptui beacon test | status | remove <name>
110
+ spinuptui scheduler add <id> --kind spinuptui|script|agent --when <time> | --every <schedule>
111
+ --expires <date> --created-by <who> -- <body>
112
+ Queue a follow-up for this station to run later and report
113
+ on through Beacon (\`spinuptui scheduler\` for the rest)
114
+ spinuptui scheduler list | status | install --plan|--apply
116
115
  spinuptui modules List SpinupTUI's modules and their status (--json)
117
116
  spinuptui --version Print the version
118
117
  spinuptui --help Show this help
@@ -313,31 +312,11 @@ if (command === "modules") {
313
312
  process.exit(await runModules(args.slice(1)))
314
313
  }
315
314
 
316
- // `spinuptui login` forces the onboarding wizard even when a token already exists,
317
- // so the token can be (re)saved to the config file for global use.
318
- const forceLogin = command === "login"
319
-
320
- function Root() {
321
- const [configured, setConfigured] = useState(isDevMode() ? true : forceLogin ? false : hasToken())
322
- // The store is keyed by account: switching remounts it (see ui/accounts.tsx).
323
- const [account, setAccount] = useState(() => pinActiveProfile())
324
- const switchAccount = useCallback(async (id: string) => {
325
- await setActiveProfile(id)
326
- resetWpCoreJobs()
327
- setAccount(id)
328
- }, [])
329
- const accounts = useMemo<AccountsApi>(() => ({ switchAccount }), [switchAccount])
330
- if (!configured) {
331
- return <Onboarding onComplete={() => setConfigured(true)} />
332
- }
333
- return (
334
- <AccountsContext.Provider value={accounts}>
335
- <StoreProvider key={account}>
336
- <App />
337
- </StoreProvider>
338
- </AccountsContext.Provider>
339
- )
315
+ if (command === "scheduler") {
316
+ process.exit(await runScheduler(args.slice(args.indexOf("scheduler") + 1)))
340
317
  }
341
318
 
342
- const renderer = await createCliRenderer()
343
- createRoot(renderer).render(<Root />)
319
+ // `spinuptui login` forces the onboarding wizard even when a token already exists,
320
+ // so the token can be (re)saved to the config file for global use.
321
+ const { startDashboard } = await import("./ui/main.tsx")
322
+ await startDashboard({ forceLogin: command === "login" })