spinuptui 1.3.0-alpha.1 → 1.3.0-alpha.3
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 +32 -1
- package/README.md +13 -3
- package/package.json +1 -1
- package/src/config.ts +25 -0
- package/src/index.tsx +26 -38
- package/src/lib/cliHome.ts +107 -0
- package/src/lib/cliScheduler.ts +630 -0
- package/src/lib/flock.ts +51 -0
- package/src/lib/heartbeat.test.ts +86 -0
- package/src/lib/heartbeat.ts +245 -0
- package/src/lib/home.test.ts +102 -0
- package/src/lib/home.ts +442 -0
- package/src/lib/schedule.ts +156 -0
- package/src/lib/scheduler.test.ts +353 -0
- package/src/lib/schedulerGit.ts +119 -0
- package/src/lib/schedulerInstall.ts +325 -0
- package/src/lib/schedulerJobs.ts +290 -0
- package/src/lib/schedulerRun.ts +247 -0
- package/src/lib/schedulerTick.ts +322 -0
- package/src/lib/spinupweb.test.ts +22 -0
- package/src/lib/spinupweb.ts +11 -0
- package/src/lib/stackCache.test.ts +43 -0
- package/src/lib/stackCache.ts +35 -1
- package/src/modules/beacon.ts +2 -1
- package/src/modules/contract.ts +22 -1
- package/src/modules/index.ts +2 -7
- package/src/modules/scheduler.ts +159 -0
- package/src/ui/App.tsx +19 -1
- package/src/ui/Details.tsx +7 -1
- package/src/ui/Onboarding.tsx +71 -3
- package/src/ui/main.tsx +42 -0
- package/src/ui/store.tsx +87 -3
- package/src/ui/views/AccountSlugPrompt.tsx +102 -0
- package/src/ui/views/Browser.tsx +5 -5
- package/src/ui/views/HeartbeatSetup.tsx +284 -0
- package/src/ui/views/HomeSync.tsx +299 -0
- package/src/ui/views/Modules.tsx +69 -5
- package/src/ui/views/SchedulerInstall.tsx +172 -0
- package/src/ui/views/Search.tsx +5 -6
- package/src/ui/views/ServerActions.tsx +6 -4
- package/src/ui/views/Stacks.tsx +22 -9
package/CHANGELOG.md
CHANGED
|
@@ -11,6 +11,34 @@ version; such changes are called out here.
|
|
|
11
11
|
|
|
12
12
|
## [Unreleased]
|
|
13
13
|
|
|
14
|
+
## [1.3.0-alpha.3] - 2026-10-27
|
|
15
|
+
|
|
16
|
+
A pre-release, published on npm's `next` tag (`bun add -g spinuptui@next --no-cache`). `@latest` and Homebrew are on 1.2.1.
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
- **Station heartbeat: find out when the station itself stops.** If the machine running Scheduler is off, the installed `spinuptui` is broken, or its LaunchAgent is gone, nothing on that machine can page you. Now every dispatcher tick pings an Uptime Kuma push monitor that you run somewhere else (a small VPS is easiest), and when the pings stop, Kuma alerts you through its own notifications. If SpinupTUI has your Kuma login (from site monitoring), `spinuptui scheduler heartbeat setup --plan|--apply` creates the monitor for you; otherwise make a push monitor by hand and run `spinuptui scheduler heartbeat set '<push URL>'`. Both ping once before saving. In the app, `h` on the Modules tab's Scheduler card walks through the same setup and later shows the last ping, sends a test ping, or removes it. The ping reports **down** when the station is running but can't page you, because Beacon isn't set up or its last page failed, so a broken pager reaches you too. `scheduler status` and the Modules tab show the last ping. See [docs/scheduler.md](docs/scheduler.md#know-when-the-station-itself-stops).
|
|
20
|
+
- **Your SpinupTUI folder explains itself, and can be shared across machines in one step.** `~/SpinupTUI/`, where Scheduler keeps your jobs and reports, now gets a `README.md` when it's created, covering what each part is, which parts are yours to edit, and how to move or share it. `spinuptui home` shows where it is and whether it's shared. `spinuptui home sync --plan|--apply` shares it through a private git repository: it creates one on GitHub (`--github <name>`, using the GitHub CLI) or pushes to an empty one you made (`--remote <url>`). Then you can queue jobs from any machine and read the reports anywhere. On a second machine, `--clone <url>` sets up the same folder. It won't share a folder inside iCloud, Dropbox or another sync service, push to a repository that already has commits, or use a public GitHub repository. The Modules tab shows where jobs and reports land, and `f` on Scheduler walks through the same steps. Queuing a job now prints the full path of the job file and where its reports will go.
|
|
21
|
+
|
|
22
|
+
### Fixed
|
|
23
|
+
- **Scheduler no longer reports a problem on every tick before you've queued anything locally.** `~/SpinupTUI/scheduler/` is created on first use, so its absence is normal; `scheduler status` used to list "source local … does not exist" as a problem from the last tick.
|
|
24
|
+
|
|
25
|
+
## [1.2.1] - 2026-10-27
|
|
26
|
+
|
|
27
|
+
### Fixed
|
|
28
|
+
- **WordPress versions no longer go stale.** The version shown for a site was read once, when the site was first identified, and never again, so sites updated outside SpinupTUI (auto-updates, wp-admin, deploys) kept showing the old version, and `r` didn't help. Versions older than a day are now re-checked in the background when the site list loads, one site at a time per server, stopping at a server's first failure so unattended SSH can't trip fail2ban. A reading that couldn't be refreshed shows its age in the Stacks list, and the Details pane always says when it was taken.
|
|
29
|
+
- **`w` opens the site or server you picked, not the SpinupWP dashboard.** Deep links need the account slug, which SpinupWP's API doesn't provide and first-run setup never asked for, so unless you had added `accountSlug` to `config.json` by hand, `w` quietly fell back to the dashboard. Now `w` asks for the slug the first time it needs it (pasting any dashboard URL works), saves it to the current account and opens the right page. First-run setup also asks for it, as an optional step after the token.
|
|
30
|
+
|
|
31
|
+
## [1.3.0-alpha.2] - 2026-10-26
|
|
32
|
+
|
|
33
|
+
A pre-release, published on npm's `next` tag (`bun add -g spinuptui@next --no-cache`). `@latest` and Homebrew stay on 1.2.0.
|
|
34
|
+
|
|
35
|
+
### Added
|
|
36
|
+
- **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).
|
|
37
|
+
- **`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).
|
|
38
|
+
|
|
39
|
+
### Changed
|
|
40
|
+
- **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.
|
|
41
|
+
|
|
14
42
|
## [1.3.0-alpha.1] - 2026-10-21
|
|
15
43
|
|
|
16
44
|
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 +1534,10 @@ Initial tagged release.
|
|
|
1506
1534
|
### Notes
|
|
1507
1535
|
- Read-only release: works with a SpinupWP **Read Only** API token.
|
|
1508
1536
|
|
|
1509
|
-
[Unreleased]: https://github.com/mwender/spinupwp-tui/compare/v1.3.0-alpha.
|
|
1537
|
+
[Unreleased]: https://github.com/mwender/spinupwp-tui/compare/v1.3.0-alpha.3...HEAD
|
|
1538
|
+
[1.3.0-alpha.3]: https://github.com/mwender/spinupwp-tui/compare/v1.3.0-alpha.2...v1.3.0-alpha.3
|
|
1539
|
+
[1.2.1]: https://github.com/mwender/spinupwp-tui/compare/v1.2.0...v1.2.1
|
|
1540
|
+
[1.3.0-alpha.2]: https://github.com/mwender/spinupwp-tui/compare/v1.3.0-alpha.1...v1.3.0-alpha.2
|
|
1510
1541
|
[1.3.0-alpha.1]: https://github.com/mwender/spinupwp-tui/compare/v1.2.0...v1.3.0-alpha.1
|
|
1511
1542
|
[1.2.0]: https://github.com/mwender/spinupwp-tui/compare/v1.1.2...v1.2.0
|
|
1512
1543
|
[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,13 @@
|
|
|
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/`; `spinuptui home sync` (or `f`)
|
|
88
|
+
shares that folder through a private git repository so you can queue from
|
|
89
|
+
any machine. One LaunchAgent ticks every 5 minutes and is the clock the other
|
|
90
|
+
modules run on, and a station heartbeat (`h`) pings your Uptime Kuma each
|
|
91
|
+
tick, so a station that stops is noticed from elsewhere. → [docs/scheduler.md](docs/scheduler.md)
|
|
85
92
|
- **Non-interactive CLI subcommands for external tooling** — `spinuptui ssh <domain>`
|
|
86
93
|
resolves a domain to a live SSH target with a live connectivity probe;
|
|
87
94
|
`spinuptui ssh-exec <domain> -- <command>` runs a command over SSH, but only
|
|
@@ -310,8 +317,9 @@ These can be set in `config.json` or via an environment variable:
|
|
|
310
317
|
- **`accountSlug`** / `SPINUPWP_ACCOUNT_SLUG` — your SpinupWP account/team slug
|
|
311
318
|
(the first path segment in a SpinupWP URL, e.g. `acme-agency` in
|
|
312
319
|
`https://spinupwp.app/acme-agency/servers/12345`). The API
|
|
313
|
-
doesn't expose it, so
|
|
314
|
-
|
|
320
|
+
doesn't expose it, so it's needed for the `w` deep links into the web app.
|
|
321
|
+
You don't have to set it by hand: first-run setup asks for it (optional), and
|
|
322
|
+
`w` asks the first time it's needed, then saves it to the current account.
|
|
315
323
|
- **`sshUser`** / `SPINUPWP_SSH_USER` — override the SSH user for the health view
|
|
316
324
|
and stack probes (see "Server health" below).
|
|
317
325
|
- **`localSync`** / `SPINUPWP_LOCAL_SYNC` — opt-in for the **Pull production →
|
|
@@ -375,8 +383,10 @@ These can be set in `config.json` or via an environment variable:
|
|
|
375
383
|
| `w` | Open the selected server/site in the SpinupWP web app |
|
|
376
384
|
| `/` | Jump to global search |
|
|
377
385
|
| `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) |
|
|
386
|
+
| `⏎` / `s` | Set up the selected module — Beacon opens its guided Telegram setup, Scheduler its install plan (Modules tab) |
|
|
379
387
|
| `t` | Send a test page through Beacon (Modules tab, once Beacon is set up) |
|
|
388
|
+
| `f` | Your SpinupTUI folder: where jobs and reports live, and sharing it across machines (Modules tab, Scheduler) |
|
|
389
|
+
| `h` | Station heartbeat: set up the Uptime Kuma push monitor, see the last ping, test or remove it (Modules tab, Scheduler) |
|
|
380
390
|
| `r` | Refresh data from the API |
|
|
381
391
|
| `i` | Explain the current screen (what each pane and key does) |
|
|
382
392
|
| `?` | Toggle the help overlay |
|
package/package.json
CHANGED
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,21 @@ 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
|
+
// The station heartbeat: an Uptime Kuma push monitor, pinged at the end of
|
|
146
|
+
// every dispatcher tick (lib/heartbeat.ts). Set by `scheduler heartbeat set`.
|
|
147
|
+
heartbeat?: { url: string; setAt: string }
|
|
148
|
+
}
|
|
149
|
+
|
|
129
150
|
export interface ServerProviderRef {
|
|
130
151
|
id: number
|
|
131
152
|
databaseProviderId?: number
|
|
@@ -224,6 +245,8 @@ export interface StoredConfig {
|
|
|
224
245
|
kumaMonitors?: Record<string, KumaMonitorRef>
|
|
225
246
|
lastSeenVersion?: string
|
|
226
247
|
beacon?: BeaconStored
|
|
248
|
+
scheduler?: SchedulerStored
|
|
249
|
+
homeDir?: string
|
|
227
250
|
}
|
|
228
251
|
|
|
229
252
|
// ---- Accounts (profiles) -------------------------------------------------------
|
|
@@ -510,6 +533,8 @@ export function loadConfig(): AppConfig {
|
|
|
510
533
|
kumaMonitors: stored.kumaMonitors ?? {},
|
|
511
534
|
lastSeenVersion: stored.lastSeenVersion?.trim() || null,
|
|
512
535
|
beacon: stored.beacon ?? null,
|
|
536
|
+
scheduler: stored.scheduler ?? null,
|
|
537
|
+
homeDir: stored.homeDir?.trim() || null,
|
|
513
538
|
}
|
|
514
539
|
}
|
|
515
540
|
|
package/src/index.tsx
CHANGED
|
@@ -1,22 +1,15 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
|
-
// Entry point. Handles
|
|
3
|
-
//
|
|
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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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 {
|
|
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,15 @@ 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
|
|
115
|
+
spinuptui home Where the SpinupTUI folder (your jobs and reports) is
|
|
116
|
+
spinuptui home sync --plan|--apply --github <name> | --remote <url> | --clone <url>
|
|
117
|
+
Share that folder across machines through a private git
|
|
118
|
+
repository, or set it up from one on a second machine
|
|
116
119
|
spinuptui modules List SpinupTUI's modules and their status (--json)
|
|
117
120
|
spinuptui --version Print the version
|
|
118
121
|
spinuptui --help Show this help
|
|
@@ -313,31 +316,16 @@ if (command === "modules") {
|
|
|
313
316
|
process.exit(await runModules(args.slice(1)))
|
|
314
317
|
}
|
|
315
318
|
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
+
if (command === "home") {
|
|
320
|
+
const { runHome } = await import("./lib/cliHome.ts")
|
|
321
|
+
process.exit(await runHome(args.slice(args.indexOf("home") + 1)))
|
|
322
|
+
}
|
|
319
323
|
|
|
320
|
-
|
|
321
|
-
|
|
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
|
-
)
|
|
324
|
+
if (command === "scheduler") {
|
|
325
|
+
process.exit(await runScheduler(args.slice(args.indexOf("scheduler") + 1)))
|
|
340
326
|
}
|
|
341
327
|
|
|
342
|
-
|
|
343
|
-
|
|
328
|
+
// `spinuptui login` forces the onboarding wizard even when a token already exists,
|
|
329
|
+
// so the token can be (re)saved to the config file for global use.
|
|
330
|
+
const { startDashboard } = await import("./ui/main.tsx")
|
|
331
|
+
await startDashboard({ forceLogin: command === "login" })
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
// `spinuptui home`: where the SpinupTUI folder is and whether it's shared, and
|
|
2
|
+
// `home sync`, which shares it across machines (lib/home.ts). Headless like the
|
|
3
|
+
// module commands: --json prints one object on stdout.
|
|
4
|
+
|
|
5
|
+
import { applySync, describeShare, ensureHome, homeStatus, planSync, tilde, type SyncOptions } from "./home.ts"
|
|
6
|
+
|
|
7
|
+
const USAGE = `Usage:
|
|
8
|
+
spinuptui home [--json] where the SpinupTUI folder is, and whether it's shared
|
|
9
|
+
spinuptui home sync --plan | --apply [--json]
|
|
10
|
+
--github <name> create a private GitHub repository for it (needs gh)
|
|
11
|
+
--remote <url> or push it to an empty private repository you made
|
|
12
|
+
--clone <url> on another machine: set the same folder up here
|
|
13
|
+
|
|
14
|
+
The SpinupTUI folder (~/SpinupTUI) holds your Scheduler jobs and reports and,
|
|
15
|
+
later, other modules' work. Shared through a private git repository, you can
|
|
16
|
+
queue jobs from any machine and read the reports anywhere.`
|
|
17
|
+
|
|
18
|
+
const VALUE_FLAGS = new Set(["--github", "--remote", "--clone"])
|
|
19
|
+
|
|
20
|
+
function fail(json: boolean, reason: string, message: string, remedy?: string): number {
|
|
21
|
+
if (json) console.log(JSON.stringify({ ok: false, reason, message, ...(remedy ? { remedy } : {}) }))
|
|
22
|
+
else {
|
|
23
|
+
console.error(message)
|
|
24
|
+
if (remedy) console.error(remedy)
|
|
25
|
+
}
|
|
26
|
+
return 1
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export async function runHome(argv: string[]): Promise<number> {
|
|
30
|
+
const flags = new Map<string, string>()
|
|
31
|
+
const pos: string[] = []
|
|
32
|
+
for (let i = 0; i < argv.length; i++) {
|
|
33
|
+
const a = argv[i]!
|
|
34
|
+
if (VALUE_FLAGS.has(a)) flags.set(a, argv[++i] ?? "")
|
|
35
|
+
else if (a.startsWith("-")) flags.set(a, "")
|
|
36
|
+
else pos.push(a)
|
|
37
|
+
}
|
|
38
|
+
const json = flags.has("--json")
|
|
39
|
+
if (flags.has("--help") || flags.has("-h")) {
|
|
40
|
+
console.log(USAGE)
|
|
41
|
+
return 0
|
|
42
|
+
}
|
|
43
|
+
if (pos.length === 0) return status(json)
|
|
44
|
+
if (pos[0] === "sync") return sync(flags, json)
|
|
45
|
+
return fail(json, "usage", USAGE)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function status(json: boolean): Promise<number> {
|
|
49
|
+
const s = await homeStatus()
|
|
50
|
+
if (json) {
|
|
51
|
+
console.log(JSON.stringify({ ok: true, ...s }))
|
|
52
|
+
return 0
|
|
53
|
+
}
|
|
54
|
+
console.log(`SpinupTUI folder: ${tilde(s.path)}${s.exists ? "" : " (created the first time a module writes to it)"}`)
|
|
55
|
+
console.log(` ${s.git && s.remote ? "✓" : "·"} ${describeShare(s)}`)
|
|
56
|
+
if (s.station) console.log(` · its jobs run on ${s.station === s.thisMachine ? "this machine" : s.station}`)
|
|
57
|
+
if (s.syncedBy) console.log(` ✗ inside ${s.syncedBy}: ${s.git ? "two programs syncing the same .git can corrupt it. Move it (homeDir in config.json)." : "fine on its own, but move it before sharing it with git."}`)
|
|
58
|
+
if (!s.git && !s.insideRepo) console.log(`\nTo queue jobs from other machines: spinuptui home sync --plan`)
|
|
59
|
+
return 0
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function sync(flags: Map<string, string>, json: boolean): Promise<number> {
|
|
63
|
+
const apply = flags.has("--apply")
|
|
64
|
+
if (apply === flags.has("--plan")) return fail(json, "usage", "Pass --plan to see what would happen, or --apply to do it.\n\n" + USAGE)
|
|
65
|
+
const opts: SyncOptions = {}
|
|
66
|
+
for (const [flag, key] of [
|
|
67
|
+
["--github", "github"],
|
|
68
|
+
["--remote", "remote"],
|
|
69
|
+
["--clone", "clone"],
|
|
70
|
+
] as const) {
|
|
71
|
+
if (!flags.has(flag)) continue
|
|
72
|
+
if (!flags.get(flag)) return fail(json, "usage", `${flag} needs a value.`)
|
|
73
|
+
opts[key] = flags.get(flag)
|
|
74
|
+
}
|
|
75
|
+
const plan = await planSync(opts)
|
|
76
|
+
if (plan.blockers.length) {
|
|
77
|
+
const b = plan.blockers[0]!
|
|
78
|
+
if (json) console.log(JSON.stringify({ ok: false, reason: b.reason, message: b.message, ...(b.remedy ? { remedy: b.remedy } : {}), blockers: plan.blockers }))
|
|
79
|
+
else for (const x of plan.blockers) fail(false, x.reason, x.message, x.remedy)
|
|
80
|
+
return 1
|
|
81
|
+
}
|
|
82
|
+
const steps = plan.steps.map((s) => s.label)
|
|
83
|
+
if (!apply) {
|
|
84
|
+
if (json) console.log(JSON.stringify({ ok: true, applied: false, mode: plan.mode, home: plan.home, steps, notes: plan.notes }))
|
|
85
|
+
else if (!steps.length) console.log(`${tilde(plan.home)} is already shared and up to date. Nothing to do.`)
|
|
86
|
+
else {
|
|
87
|
+
console.log(`Plan for ${tilde(plan.home)}:`)
|
|
88
|
+
steps.forEach((s, i) => console.log(` ${i + 1}. ${s}`))
|
|
89
|
+
for (const n of plan.notes) console.log(`\n${n}`)
|
|
90
|
+
console.log(`\nRun with --apply to do it.`)
|
|
91
|
+
}
|
|
92
|
+
return 0
|
|
93
|
+
}
|
|
94
|
+
const r = await applySync(plan)
|
|
95
|
+
if (json) console.log(JSON.stringify({ ok: r.ok, applied: true, mode: plan.mode, home: plan.home, done: r.done, ...(r.failed ? { failed: r.failed } : {}) }))
|
|
96
|
+
else {
|
|
97
|
+
for (const d of r.done) console.log(`✓ ${d}`)
|
|
98
|
+
if (r.failed) console.error(`✗ ${r.failed.step}: ${r.failed.message}\nSteps above it are done; fix this and run it again, and it picks up where it stopped.`)
|
|
99
|
+
else if (steps.length) {
|
|
100
|
+
// A clone brings the other machine's README; make sure there is one.
|
|
101
|
+
ensureHome()
|
|
102
|
+
const s = await homeStatus()
|
|
103
|
+
console.log(`\n${tilde(s.path)} is ${describeShare(s)}.`)
|
|
104
|
+
} else console.log(`${tilde(plan.home)} is already shared and up to date. Nothing to do.`)
|
|
105
|
+
}
|
|
106
|
+
return r.ok ? 0 : 1
|
|
107
|
+
}
|