Initial commit: move CLI (milestone 2)
- src/move.ts entry point with CLI parsing, --help, --version - src/cli.ts: parseCliArgs, printHelp, ParsedCliArgs, CliError, VERSION - src/config.ts: Config type, DEFAULT_CONFIG, resolveConfig - src/keeper.ts: synthetic-activity sweep + idle-watch loop - package.json bin entry + shebang for 'bun link' global install - install.sh: contributor bootstrap (will be repurposed; see DISTRIBUTION-PLAN.md for the end-user installer design) - DISTRIBUTION-PLAN.md captures the tabled end-user distribution work
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
node_modules/
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
|
||||
# OS / editor cruft
|
||||
.DS_Store
|
||||
.vscode/
|
||||
.idea/
|
||||
@@ -0,0 +1,222 @@
|
||||
# Distribution Plan for `move`
|
||||
|
||||
**Status:** Tabled 2026-06-15. Revisit after the initial Gitea push lands
|
||||
and the repo is publicly reachable at <https://gitea.cahlen.com/nokeo08/Move>.
|
||||
|
||||
This document captures the analysis and decisions for distributing the
|
||||
`move` CLI as an end-user-friendly tool, so we can resume without redoing
|
||||
the discussion.
|
||||
|
||||
---
|
||||
|
||||
## Decisions locked in
|
||||
|
||||
| Decision | Choice |
|
||||
| -------- | ------ |
|
||||
| Primary hosting | Self-hosted Gitea (`https://gitea.cahlen.com/nokeo08/Move`) |
|
||||
| Default branch | `master` |
|
||||
| Audience | Anyone who finds the repo (broader public-ish) |
|
||||
| Effort budget for now | Smallest viable curl-able `install.sh` |
|
||||
| Bun handling | Fail with a clear message when missing (no auto-install) |
|
||||
| Reinstall behavior | Idempotent via a version-marker file; `MOVE_FORCE=1` overrides |
|
||||
| Install location | `~/.local/bin` + `~/.local/share/move` (XDG, no sudo), env-overridable |
|
||||
| `move` shim | Bash wrapper script, not symlink |
|
||||
| PATH rc files | Never modified; print instructions if PATH isn't right |
|
||||
| Uninstaller | In scope; same `curl ... \| sh` pattern |
|
||||
| npm publishing | Out of scope |
|
||||
| Standalone binaries (`bun build --compile`) | Future work; depends on nut.js native-bundling testing |
|
||||
| Homebrew tap | Future work; cheap once releases exist |
|
||||
|
||||
---
|
||||
|
||||
## Distribution-option survey (from the discussion)
|
||||
|
||||
Four options were weighed:
|
||||
|
||||
1. **Compiled standalone binary** (`bun build --compile`). Best UX (single
|
||||
binary, no Bun needed) but needs per-platform builds, a release
|
||||
pipeline, code signing on macOS, and depends on nut.js's prebuilt native
|
||||
`.node` files bundling correctly under `--compile` — **unverified**.
|
||||
2. **Curl-piped installer** — **chosen**. Single command, no clone,
|
||||
source-and-Bun model. Conventions used by bun, deno, rustup, nvm.
|
||||
Slight cold-start cost vs. compiled binary; tradeoff accepted.
|
||||
3. **Package-manager distribution** (`bun add -g`, brew). `move` is taken
|
||||
on npm; brew is macOS-centric; both add publishing overhead. Out of
|
||||
scope for first cut.
|
||||
4. **Status quo** (clone + `./install.sh` + `bun link`). Developer flow
|
||||
only; will be renamed to `dev-setup.sh`.
|
||||
|
||||
---
|
||||
|
||||
## Target end-user UX
|
||||
|
||||
```sh
|
||||
curl -fsSL https://gitea.cahlen.com/nokeo08/Move/raw/branch/master/install.sh | sh
|
||||
move --help
|
||||
```
|
||||
|
||||
One command. No clone. No `bun link`. No `chmod`. After this the user has
|
||||
a global `move` on their PATH.
|
||||
|
||||
---
|
||||
|
||||
## `install.sh` design (end-user, curl-able)
|
||||
|
||||
- **Shebang:** `#!/usr/bin/env sh` (POSIX).
|
||||
- **Strict mode:** `set -eu`. Trap on errors with a clear
|
||||
"install failed at <step>" message.
|
||||
|
||||
### Top-to-bottom behavior
|
||||
|
||||
1. **Platform detection:**
|
||||
- `uname -s` → `Darwin` / `Linux` / else → "unsupported OS" + exit 1.
|
||||
- `uname -m` → `arm64` / `x86_64` (mapped to `x64`) / else →
|
||||
"unsupported arch" + exit 1.
|
||||
- nut.js supports darwin-arm64/x64, linux-x64, win32-x64. WSL users
|
||||
fall under Linux.
|
||||
2. **Bun check:**
|
||||
- `command -v bun` present → continue.
|
||||
- Absent → print
|
||||
`Error: bun is not installed. Install from https://bun.sh (e.g. 'curl -fsSL https://bun.sh/install | bash'), then re-run.`
|
||||
→ exit 1.
|
||||
3. **Resolve paths:**
|
||||
- `INSTALL_DIR=${MOVE_PREFIX:-$HOME/.local/share/move}`
|
||||
- `BIN_DIR=${MOVE_BIN_DIR:-$HOME/.local/bin}`
|
||||
- `mkdir -p` both.
|
||||
4. **Resolve version:**
|
||||
- `VERSION=${MOVE_VERSION:-master}`.
|
||||
5. **Idempotence check:**
|
||||
- If `$INSTALL_DIR/.installed-version` exists AND its content equals
|
||||
`$VERSION` AND `${MOVE_FORCE:-0}` is not `1`:
|
||||
- Print `move <version> already installed at $BIN_DIR/move. Set MOVE_FORCE=1 to reinstall, or pass a different MOVE_VERSION.`
|
||||
- Exit 0.
|
||||
6. **Clean install dir:** `rm -rf "$INSTALL_DIR"/*` (preserving the dir
|
||||
itself so user-set permissions on it are kept).
|
||||
7. **Download source:**
|
||||
- `TARBALL="https://gitea.cahlen.com/nokeo08/Move/archive/${VERSION}.tar.gz"`
|
||||
- `curl -fsSL "$TARBALL" -o "$TMP"` (mktemp) → on failure, fail
|
||||
loudly with the offending URL.
|
||||
- `tar -xzf "$TMP" -C "$INSTALL_DIR" --strip-components=1`
|
||||
- `rm -f "$TMP"`
|
||||
8. **Install deps:** `cd "$INSTALL_DIR" && bun install --production`.
|
||||
`--production` skips devDependencies (TypeScript, @types/bun); saves
|
||||
tens of MB and seconds per install.
|
||||
9. **Drop the wrapper:**
|
||||
```sh
|
||||
cat > "$BIN_DIR/move" <<EOF
|
||||
#!/usr/bin/env sh
|
||||
exec bun "$INSTALL_DIR/src/move.ts" "\$@"
|
||||
EOF
|
||||
chmod +x "$BIN_DIR/move"
|
||||
```
|
||||
Wrapper, not a symlink, so the path is absolute and stable regardless
|
||||
of how the user's shell resolves things.
|
||||
10. **Write version marker:** `printf '%s\n' "$VERSION" > "$INSTALL_DIR/.installed-version"`.
|
||||
11. **PATH sanity check:**
|
||||
- If `$BIN_DIR` is not on `$PATH`, print:
|
||||
`Warning: $BIN_DIR is not on your PATH. Add this to your shell rc: export PATH="$BIN_DIR:$PATH"`
|
||||
- Do **not** mutate the user's rc files — that breaks the "clean
|
||||
uninstall" contract.
|
||||
12. **macOS Accessibility hint** (carried over from current `install.sh`).
|
||||
13. **Final message:** `Installed move $VERSION at $BIN_DIR/move. Run 'move --help' to get started.`
|
||||
|
||||
### Env vars
|
||||
|
||||
| Var | Default | Purpose |
|
||||
| --- | ------- | ------- |
|
||||
| `MOVE_VERSION` | `master` | Branch or tag to fetch from Gitea. |
|
||||
| `MOVE_PREFIX` | `$HOME/.local/share/move` | Source install location. |
|
||||
| `MOVE_BIN_DIR` | `$HOME/.local/bin` | Wrapper install location. |
|
||||
| `MOVE_FORCE` | unset | Force reinstall even if version marker matches. |
|
||||
|
||||
---
|
||||
|
||||
## `uninstall.sh` design
|
||||
|
||||
```sh
|
||||
curl -fsSL https://gitea.cahlen.com/nokeo08/Move/raw/branch/master/uninstall.sh | sh
|
||||
```
|
||||
|
||||
**Behavior:**
|
||||
|
||||
1. `set -eu`.
|
||||
2. Respect `MOVE_PREFIX` and `MOVE_BIN_DIR` env vars (same defaults as
|
||||
install).
|
||||
3. `rm -f "$BIN_DIR/move"`.
|
||||
4. `rm -rf "$INSTALL_DIR"`.
|
||||
5. Print `Uninstalled. (Bun was not touched.)`.
|
||||
|
||||
Does **not** uninstall Bun — that's the user's runtime, not ours.
|
||||
|
||||
---
|
||||
|
||||
## `dev-setup.sh` design (renamed from current `install.sh`)
|
||||
|
||||
The current `install.sh` is appropriate for contributors but inappropriate
|
||||
as the end-user installer. Rename to `dev-setup.sh`. Content stays nearly
|
||||
identical:
|
||||
|
||||
- Verify Bun is installed (hard-error if missing).
|
||||
- `bun install`.
|
||||
- macOS Accessibility hint.
|
||||
- Trailing message updated to:
|
||||
`Done. Dev workflow: 'bun run start' to run, or 'bun link' to install the global 'move' command. End-user installer is install.sh.`
|
||||
|
||||
---
|
||||
|
||||
## README changes (when we resume)
|
||||
|
||||
- `## Install` section becomes the curl one-liner targeting `master`, plus
|
||||
a short note about env-var overrides and a pointer to uninstall.
|
||||
- `## Run` section collapses to `move` / `move --help` for the global
|
||||
install path.
|
||||
- New `## Uninstall` section with the curl-pipe uninstall command.
|
||||
- New `## For contributors` section absorbing the existing
|
||||
`bun run start` / `bun run src/move.ts` / `bun link` examples; points
|
||||
at `dev-setup.sh`.
|
||||
- `## Files` table updates: add `install.sh` (end-user, curl-able),
|
||||
`uninstall.sh` (end-user, curl-able), `dev-setup.sh` (contributor
|
||||
bootstrap).
|
||||
|
||||
---
|
||||
|
||||
## Verification plan (when we resume)
|
||||
|
||||
1. `sh -n install.sh` and `sh -n uninstall.sh` — POSIX syntax check.
|
||||
2. Local end-to-end with overridden paths:
|
||||
```sh
|
||||
MOVE_PREFIX=/tmp/movetest MOVE_BIN_DIR=/tmp/movetest/bin ./install.sh
|
||||
/tmp/movetest/bin/move --help
|
||||
./install.sh # second run: already-installed message
|
||||
MOVE_FORCE=1 ./install.sh # forced reinstall
|
||||
```
|
||||
3. Uninstall against the test prefix; confirm nothing left under it.
|
||||
4. Re-run existing acceptance tests (`tsc`, `--help`, `--version`,
|
||||
`--bogus`) — should be unaffected; no source changes in this milestone.
|
||||
5. After committing/pushing, end-to-end against the actual Gitea URL.
|
||||
|
||||
---
|
||||
|
||||
## Caveat: Gitea archive URL shape
|
||||
|
||||
The plan assumes
|
||||
`https://gitea.cahlen.com/nokeo08/Move/archive/<ref>.tar.gz` returns a
|
||||
tarball whose top-level directory contains the project files (peeled by
|
||||
`--strip-components=1`). This is the standard Gitea archive format, but
|
||||
if your Gitea version produces a different layout the installer will need
|
||||
a small tweak. The installer should error loudly with the offending URL
|
||||
if extraction fails.
|
||||
|
||||
---
|
||||
|
||||
## Open future work (out of scope for the resume session)
|
||||
|
||||
- Switch `MOVE_VERSION` default from `master` to "latest tag" via the
|
||||
Gitea API (`/api/v1/repos/nokeo08/Move/tags`) once tagged releases are
|
||||
routine.
|
||||
- Investigate `bun build --compile` + nut.js native-binary bundling for
|
||||
true zero-Bun installs. If it works, layer prebuilt binaries onto the
|
||||
curl installer so users without Bun also get one-command installs.
|
||||
- Homebrew tap for macOS users (cheap once a release pipeline exists).
|
||||
- Possible: PowerShell installer (`install.ps1`) for native Windows users
|
||||
without WSL.
|
||||
@@ -0,0 +1,674 @@
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The GNU General Public License is a free, copyleft license for
|
||||
software and other kinds of works.
|
||||
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
the GNU General Public License is intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains free
|
||||
software for all its users. We, the Free Software Foundation, use the
|
||||
GNU General Public License for most of our software; it applies also to
|
||||
any other work released this way by its authors. You can apply it to
|
||||
your programs, too.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
them if you wish), that you receive source code or can get it if you
|
||||
want it, that you can change the software or use pieces of it in new
|
||||
free programs, and that you know you can do these things.
|
||||
|
||||
To protect your rights, we need to prevent others from denying you
|
||||
these rights or asking you to surrender the rights. Therefore, you have
|
||||
certain responsibilities if you distribute copies of the software, or if
|
||||
you modify it: responsibilities to respect the freedom of others.
|
||||
|
||||
For example, if you distribute copies of such a program, whether
|
||||
gratis or for a fee, you must pass on to the recipients the same
|
||||
freedoms that you received. You must make sure that they, too, receive
|
||||
or can get the source code. And you must show them these terms so they
|
||||
know their rights.
|
||||
|
||||
Developers that use the GNU GPL protect your rights with two steps:
|
||||
(1) assert copyright on the software, and (2) offer you this License
|
||||
giving you legal permission to copy, distribute and/or modify it.
|
||||
|
||||
For the developers' and authors' protection, the GPL clearly explains
|
||||
that there is no warranty for this free software. For both users' and
|
||||
authors' sake, the GPL requires that modified versions be marked as
|
||||
changed, so that their problems will not be attributed erroneously to
|
||||
authors of previous versions.
|
||||
|
||||
Some devices are designed to deny users access to install or run
|
||||
modified versions of the software inside them, although the manufacturer
|
||||
can do so. This is fundamentally incompatible with the aim of
|
||||
protecting users' freedom to change the software. The systematic
|
||||
pattern of such abuse occurs in the area of products for individuals to
|
||||
use, which is precisely where it is most unacceptable. Therefore, we
|
||||
have designed this version of the GPL to prohibit the practice for those
|
||||
products. If such problems arise substantially in other domains, we
|
||||
stand ready to extend this provision to those domains in future versions
|
||||
of the GPL, as needed to protect the freedom of users.
|
||||
|
||||
Finally, every program is threatened constantly by software patents.
|
||||
States should not allow patents to restrict development and use of
|
||||
software on general-purpose computers, but in those that do, we wish to
|
||||
avoid the special danger that patents applied to a free program could
|
||||
make it effectively proprietary. To prevent this, the GPL assures that
|
||||
patents cannot be used to render the program non-free.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
TERMS AND CONDITIONS
|
||||
|
||||
0. Definitions.
|
||||
|
||||
"This License" refers to version 3 of the GNU General Public License.
|
||||
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.
|
||||
|
||||
"The Program" refers to any copyrightable work licensed under this
|
||||
License. Each licensee is addressed as "you". "Licensees" and
|
||||
"recipients" may be individuals or organizations.
|
||||
|
||||
To "modify" a work means to copy from or adapt all or part of the work
|
||||
in a fashion requiring copyright permission, other than the making of an
|
||||
exact copy. The resulting work is called a "modified version" of the
|
||||
earlier work or a work "based on" the earlier work.
|
||||
|
||||
A "covered work" means either the unmodified Program or a work based
|
||||
on the Program.
|
||||
|
||||
To "propagate" a work means to do anything with it that, without
|
||||
permission, would make you directly or secondarily liable for
|
||||
infringement under applicable copyright law, except executing it on a
|
||||
computer or modifying a private copy. Propagation includes copying,
|
||||
distribution (with or without modification), making available to the
|
||||
public, and in some countries other activities as well.
|
||||
|
||||
To "convey" a work means any kind of propagation that enables other
|
||||
parties to make or receive copies. Mere interaction with a user through
|
||||
a computer network, with no transfer of a copy, is not conveying.
|
||||
|
||||
An interactive user interface displays "Appropriate Legal Notices"
|
||||
to the extent that it includes a convenient and prominently visible
|
||||
feature that (1) displays an appropriate copyright notice, and (2)
|
||||
tells the user that there is no warranty for the work (except to the
|
||||
extent that warranties are provided), that licensees may convey the
|
||||
work under this License, and how to view a copy of this License. If
|
||||
the interface presents a list of user commands or options, such as a
|
||||
menu, a prominent item in the list meets this criterion.
|
||||
|
||||
1. Source Code.
|
||||
|
||||
The "source code" for a work means the preferred form of the work
|
||||
for making modifications to it. "Object code" means any non-source
|
||||
form of a work.
|
||||
|
||||
A "Standard Interface" means an interface that either is an official
|
||||
standard defined by a recognized standards body, or, in the case of
|
||||
interfaces specified for a particular programming language, one that
|
||||
is widely used among developers working in that language.
|
||||
|
||||
The "System Libraries" of an executable work include anything, other
|
||||
than the work as a whole, that (a) is included in the normal form of
|
||||
packaging a Major Component, but which is not part of that Major
|
||||
Component, and (b) serves only to enable use of the work with that
|
||||
Major Component, or to implement a Standard Interface for which an
|
||||
implementation is available to the public in source code form. A
|
||||
"Major Component", in this context, means a major essential component
|
||||
(kernel, window system, and so on) of the specific operating system
|
||||
(if any) on which the executable work runs, or a compiler used to
|
||||
produce the work, or an object code interpreter used to run it.
|
||||
|
||||
The "Corresponding Source" for a work in object code form means all
|
||||
the source code needed to generate, install, and (for an executable
|
||||
work) run the object code and to modify the work, including scripts to
|
||||
control those activities. However, it does not include the work's
|
||||
System Libraries, or general-purpose tools or generally available free
|
||||
programs which are used unmodified in performing those activities but
|
||||
which are not part of the work. For example, Corresponding Source
|
||||
includes interface definition files associated with source files for
|
||||
the work, and the source code for shared libraries and dynamically
|
||||
linked subprograms that the work is specifically designed to require,
|
||||
such as by intimate data communication or control flow between those
|
||||
subprograms and other parts of the work.
|
||||
|
||||
The Corresponding Source need not include anything that users
|
||||
can regenerate automatically from other parts of the Corresponding
|
||||
Source.
|
||||
|
||||
The Corresponding Source for a work in source code form is that
|
||||
same work.
|
||||
|
||||
2. Basic Permissions.
|
||||
|
||||
All rights granted under this License are granted for the term of
|
||||
copyright on the Program, and are irrevocable provided the stated
|
||||
conditions are met. This License explicitly affirms your unlimited
|
||||
permission to run the unmodified Program. The output from running a
|
||||
covered work is covered by this License only if the output, given its
|
||||
content, constitutes a covered work. This License acknowledges your
|
||||
rights of fair use or other equivalent, as provided by copyright law.
|
||||
|
||||
You may make, run and propagate covered works that you do not
|
||||
convey, without conditions so long as your license otherwise remains
|
||||
in force. You may convey covered works to others for the sole purpose
|
||||
of having them make modifications exclusively for you, or provide you
|
||||
with facilities for running those works, provided that you comply with
|
||||
the terms of this License in conveying all material for which you do
|
||||
not control copyright. Those thus making or running the covered works
|
||||
for you must do so exclusively on your behalf, under your direction
|
||||
and control, on terms that prohibit them from making any copies of
|
||||
your copyrighted material outside their relationship with you.
|
||||
|
||||
Conveying under any other circumstances is permitted solely under
|
||||
the conditions stated below. Sublicensing is not allowed; section 10
|
||||
makes it unnecessary.
|
||||
|
||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||
|
||||
No covered work shall be deemed part of an effective technological
|
||||
measure under any applicable law fulfilling obligations under article
|
||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||
similar laws prohibiting or restricting circumvention of such
|
||||
measures.
|
||||
|
||||
When you convey a covered work, you waive any legal power to forbid
|
||||
circumvention of technological measures to the extent such circumvention
|
||||
is effected by exercising rights under this License with respect to
|
||||
the covered work, and you disclaim any intention to limit operation or
|
||||
modification of the work as a means of enforcing, against the work's
|
||||
users, your or third parties' legal rights to forbid circumvention of
|
||||
technological measures.
|
||||
|
||||
4. Conveying Verbatim Copies.
|
||||
|
||||
You may convey verbatim copies of the Program's source code as you
|
||||
receive it, in any medium, provided that you conspicuously and
|
||||
appropriately publish on each copy an appropriate copyright notice;
|
||||
keep intact all notices stating that this License and any
|
||||
non-permissive terms added in accord with section 7 apply to the code;
|
||||
keep intact all notices of the absence of any warranty; and give all
|
||||
recipients a copy of this License along with the Program.
|
||||
|
||||
You may charge any price or no price for each copy that you convey,
|
||||
and you may offer support or warranty protection for a fee.
|
||||
|
||||
5. Conveying Modified Source Versions.
|
||||
|
||||
You may convey a work based on the Program, or the modifications to
|
||||
produce it from the Program, in the form of source code under the
|
||||
terms of section 4, provided that you also meet all of these conditions:
|
||||
|
||||
a) The work must carry prominent notices stating that you modified
|
||||
it, and giving a relevant date.
|
||||
|
||||
b) The work must carry prominent notices stating that it is
|
||||
released under this License and any conditions added under section
|
||||
7. This requirement modifies the requirement in section 4 to
|
||||
"keep intact all notices".
|
||||
|
||||
c) You must license the entire work, as a whole, under this
|
||||
License to anyone who comes into possession of a copy. This
|
||||
License will therefore apply, along with any applicable section 7
|
||||
additional terms, to the whole of the work, and all its parts,
|
||||
regardless of how they are packaged. This License gives no
|
||||
permission to license the work in any other way, but it does not
|
||||
invalidate such permission if you have separately received it.
|
||||
|
||||
d) If the work has interactive user interfaces, each must display
|
||||
Appropriate Legal Notices; however, if the Program has interactive
|
||||
interfaces that do not display Appropriate Legal Notices, your
|
||||
work need not make them do so.
|
||||
|
||||
A compilation of a covered work with other separate and independent
|
||||
works, which are not by their nature extensions of the covered work,
|
||||
and which are not combined with it such as to form a larger program,
|
||||
in or on a volume of a storage or distribution medium, is called an
|
||||
"aggregate" if the compilation and its resulting copyright are not
|
||||
used to limit the access or legal rights of the compilation's users
|
||||
beyond what the individual works permit. Inclusion of a covered work
|
||||
in an aggregate does not cause this License to apply to the other
|
||||
parts of the aggregate.
|
||||
|
||||
6. Conveying Non-Source Forms.
|
||||
|
||||
You may convey a covered work in object code form under the terms
|
||||
of sections 4 and 5, provided that you also convey the
|
||||
machine-readable Corresponding Source under the terms of this License,
|
||||
in one of these ways:
|
||||
|
||||
a) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by the
|
||||
Corresponding Source fixed on a durable physical medium
|
||||
customarily used for software interchange.
|
||||
|
||||
b) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by a
|
||||
written offer, valid for at least three years and valid for as
|
||||
long as you offer spare parts or customer support for that product
|
||||
model, to give anyone who possesses the object code either (1) a
|
||||
copy of the Corresponding Source for all the software in the
|
||||
product that is covered by this License, on a durable physical
|
||||
medium customarily used for software interchange, for a price no
|
||||
more than your reasonable cost of physically performing this
|
||||
conveying of source, or (2) access to copy the
|
||||
Corresponding Source from a network server at no charge.
|
||||
|
||||
c) Convey individual copies of the object code with a copy of the
|
||||
written offer to provide the Corresponding Source. This
|
||||
alternative is allowed only occasionally and noncommercially, and
|
||||
only if you received the object code with such an offer, in accord
|
||||
with subsection 6b.
|
||||
|
||||
d) Convey the object code by offering access from a designated
|
||||
place (gratis or for a charge), and offer equivalent access to the
|
||||
Corresponding Source in the same way through the same place at no
|
||||
further charge. You need not require recipients to copy the
|
||||
Corresponding Source along with the object code. If the place to
|
||||
copy the object code is a network server, the Corresponding Source
|
||||
may be on a different server (operated by you or a third party)
|
||||
that supports equivalent copying facilities, provided you maintain
|
||||
clear directions next to the object code saying where to find the
|
||||
Corresponding Source. Regardless of what server hosts the
|
||||
Corresponding Source, you remain obligated to ensure that it is
|
||||
available for as long as needed to satisfy these requirements.
|
||||
|
||||
e) Convey the object code using peer-to-peer transmission, provided
|
||||
you inform other peers where the object code and Corresponding
|
||||
Source of the work are being offered to the general public at no
|
||||
charge under subsection 6d.
|
||||
|
||||
A separable portion of the object code, whose source code is excluded
|
||||
from the Corresponding Source as a System Library, need not be
|
||||
included in conveying the object code work.
|
||||
|
||||
A "User Product" is either (1) a "consumer product", which means any
|
||||
tangible personal property which is normally used for personal, family,
|
||||
or household purposes, or (2) anything designed or sold for incorporation
|
||||
into a dwelling. In determining whether a product is a consumer product,
|
||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||
product received by a particular user, "normally used" refers to a
|
||||
typical or common use of that class of product, regardless of the status
|
||||
of the particular user or of the way in which the particular user
|
||||
actually uses, or expects or is expected to use, the product. A product
|
||||
is a consumer product regardless of whether the product has substantial
|
||||
commercial, industrial or non-consumer uses, unless such uses represent
|
||||
the only significant mode of use of the product.
|
||||
|
||||
"Installation Information" for a User Product means any methods,
|
||||
procedures, authorization keys, or other information required to install
|
||||
and execute modified versions of a covered work in that User Product from
|
||||
a modified version of its Corresponding Source. The information must
|
||||
suffice to ensure that the continued functioning of the modified object
|
||||
code is in no case prevented or interfered with solely because
|
||||
modification has been made.
|
||||
|
||||
If you convey an object code work under this section in, or with, or
|
||||
specifically for use in, a User Product, and the conveying occurs as
|
||||
part of a transaction in which the right of possession and use of the
|
||||
User Product is transferred to the recipient in perpetuity or for a
|
||||
fixed term (regardless of how the transaction is characterized), the
|
||||
Corresponding Source conveyed under this section must be accompanied
|
||||
by the Installation Information. But this requirement does not apply
|
||||
if neither you nor any third party retains the ability to install
|
||||
modified object code on the User Product (for example, the work has
|
||||
been installed in ROM).
|
||||
|
||||
The requirement to provide Installation Information does not include a
|
||||
requirement to continue to provide support service, warranty, or updates
|
||||
for a work that has been modified or installed by the recipient, or for
|
||||
the User Product in which it has been modified or installed. Access to a
|
||||
network may be denied when the modification itself materially and
|
||||
adversely affects the operation of the network or violates the rules and
|
||||
protocols for communication across the network.
|
||||
|
||||
Corresponding Source conveyed, and Installation Information provided,
|
||||
in accord with this section must be in a format that is publicly
|
||||
documented (and with an implementation available to the public in
|
||||
source code form), and must require no special password or key for
|
||||
unpacking, reading or copying.
|
||||
|
||||
7. Additional Terms.
|
||||
|
||||
"Additional permissions" are terms that supplement the terms of this
|
||||
License by making exceptions from one or more of its conditions.
|
||||
Additional permissions that are applicable to the entire Program shall
|
||||
be treated as though they were included in this License, to the extent
|
||||
that they are valid under applicable law. If additional permissions
|
||||
apply only to part of the Program, that part may be used separately
|
||||
under those permissions, but the entire Program remains governed by
|
||||
this License without regard to the additional permissions.
|
||||
|
||||
When you convey a copy of a covered work, you may at your option
|
||||
remove any additional permissions from that copy, or from any part of
|
||||
it. (Additional permissions may be written to require their own
|
||||
removal in certain cases when you modify the work.) You may place
|
||||
additional permissions on material, added by you to a covered work,
|
||||
for which you have or can give appropriate copyright permission.
|
||||
|
||||
Notwithstanding any other provision of this License, for material you
|
||||
add to a covered work, you may (if authorized by the copyright holders of
|
||||
that material) supplement the terms of this License with terms:
|
||||
|
||||
a) Disclaiming warranty or limiting liability differently from the
|
||||
terms of sections 15 and 16 of this License; or
|
||||
|
||||
b) Requiring preservation of specified reasonable legal notices or
|
||||
author attributions in that material or in the Appropriate Legal
|
||||
Notices displayed by works containing it; or
|
||||
|
||||
c) Prohibiting misrepresentation of the origin of that material, or
|
||||
requiring that modified versions of such material be marked in
|
||||
reasonable ways as different from the original version; or
|
||||
|
||||
d) Limiting the use for publicity purposes of names of licensors or
|
||||
authors of the material; or
|
||||
|
||||
e) Declining to grant rights under trademark law for use of some
|
||||
trade names, trademarks, or service marks; or
|
||||
|
||||
f) Requiring indemnification of licensors and authors of that
|
||||
material by anyone who conveys the material (or modified versions of
|
||||
it) with contractual assumptions of liability to the recipient, for
|
||||
any liability that these contractual assumptions directly impose on
|
||||
those licensors and authors.
|
||||
|
||||
All other non-permissive additional terms are considered "further
|
||||
restrictions" within the meaning of section 10. If the Program as you
|
||||
received it, or any part of it, contains a notice stating that it is
|
||||
governed by this License along with a term that is a further
|
||||
restriction, you may remove that term. If a license document contains
|
||||
a further restriction but permits relicensing or conveying under this
|
||||
License, you may add to a covered work material governed by the terms
|
||||
of that license document, provided that the further restriction does
|
||||
not survive such relicensing or conveying.
|
||||
|
||||
If you add terms to a covered work in accord with this section, you
|
||||
must place, in the relevant source files, a statement of the
|
||||
additional terms that apply to those files, or a notice indicating
|
||||
where to find the applicable terms.
|
||||
|
||||
Additional terms, permissive or non-permissive, may be stated in the
|
||||
form of a separately written license, or stated as exceptions;
|
||||
the above requirements apply either way.
|
||||
|
||||
8. Termination.
|
||||
|
||||
You may not propagate or modify a covered work except as expressly
|
||||
provided under this License. Any attempt otherwise to propagate or
|
||||
modify it is void, and will automatically terminate your rights under
|
||||
this License (including any patent licenses granted under the third
|
||||
paragraph of section 11).
|
||||
|
||||
However, if you cease all violation of this License, then your
|
||||
license from a particular copyright holder is reinstated (a)
|
||||
provisionally, unless and until the copyright holder explicitly and
|
||||
finally terminates your license, and (b) permanently, if the copyright
|
||||
holder fails to notify you of the violation by some reasonable means
|
||||
prior to 60 days after the cessation.
|
||||
|
||||
Moreover, your license from a particular copyright holder is
|
||||
reinstated permanently if the copyright holder notifies you of the
|
||||
violation by some reasonable means, this is the first time you have
|
||||
received notice of violation of this License (for any work) from that
|
||||
copyright holder, and you cure the violation prior to 30 days after
|
||||
your receipt of the notice.
|
||||
|
||||
Termination of your rights under this section does not terminate the
|
||||
licenses of parties who have received copies or rights from you under
|
||||
this License. If your rights have been terminated and not permanently
|
||||
reinstated, you do not qualify to receive new licenses for the same
|
||||
material under section 10.
|
||||
|
||||
9. Acceptance Not Required for Having Copies.
|
||||
|
||||
You are not required to accept this License in order to receive or
|
||||
run a copy of the Program. Ancillary propagation of a covered work
|
||||
occurring solely as a consequence of using peer-to-peer transmission
|
||||
to receive a copy likewise does not require acceptance. However,
|
||||
nothing other than this License grants you permission to propagate or
|
||||
modify any covered work. These actions infringe copyright if you do
|
||||
not accept this License. Therefore, by modifying or propagating a
|
||||
covered work, you indicate your acceptance of this License to do so.
|
||||
|
||||
10. Automatic Licensing of Downstream Recipients.
|
||||
|
||||
Each time you convey a covered work, the recipient automatically
|
||||
receives a license from the original licensors, to run, modify and
|
||||
propagate that work, subject to this License. You are not responsible
|
||||
for enforcing compliance by third parties with this License.
|
||||
|
||||
An "entity transaction" is a transaction transferring control of an
|
||||
organization, or substantially all assets of one, or subdividing an
|
||||
organization, or merging organizations. If propagation of a covered
|
||||
work results from an entity transaction, each party to that
|
||||
transaction who receives a copy of the work also receives whatever
|
||||
licenses to the work the party's predecessor in interest had or could
|
||||
give under the previous paragraph, plus a right to possession of the
|
||||
Corresponding Source of the work from the predecessor in interest, if
|
||||
the predecessor has it or can get it with reasonable efforts.
|
||||
|
||||
You may not impose any further restrictions on the exercise of the
|
||||
rights granted or affirmed under this License. For example, you may
|
||||
not impose a license fee, royalty, or other charge for exercise of
|
||||
rights granted under this License, and you may not initiate litigation
|
||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||
any patent claim is infringed by making, using, selling, offering for
|
||||
sale, or importing the Program or any portion of it.
|
||||
|
||||
11. Patents.
|
||||
|
||||
A "contributor" is a copyright holder who authorizes use under this
|
||||
License of the Program or a work on which the Program is based. The
|
||||
work thus licensed is called the contributor's "contributor version".
|
||||
|
||||
A contributor's "essential patent claims" are all patent claims
|
||||
owned or controlled by the contributor, whether already acquired or
|
||||
hereafter acquired, that would be infringed by some manner, permitted
|
||||
by this License, of making, using, or selling its contributor version,
|
||||
but do not include claims that would be infringed only as a
|
||||
consequence of further modification of the contributor version. For
|
||||
purposes of this definition, "control" includes the right to grant
|
||||
patent sublicenses in a manner consistent with the requirements of
|
||||
this License.
|
||||
|
||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||
patent license under the contributor's essential patent claims, to
|
||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||
propagate the contents of its contributor version.
|
||||
|
||||
In the following three paragraphs, a "patent license" is any express
|
||||
agreement or commitment, however denominated, not to enforce a patent
|
||||
(such as an express permission to practice a patent or covenant not to
|
||||
sue for patent infringement). To "grant" such a patent license to a
|
||||
party means to make such an agreement or commitment not to enforce a
|
||||
patent against the party.
|
||||
|
||||
If you convey a covered work, knowingly relying on a patent license,
|
||||
and the Corresponding Source of the work is not available for anyone
|
||||
to copy, free of charge and under the terms of this License, through a
|
||||
publicly available network server or other readily accessible means,
|
||||
then you must either (1) cause the Corresponding Source to be so
|
||||
available, or (2) arrange to deprive yourself of the benefit of the
|
||||
patent license for this particular work, or (3) arrange, in a manner
|
||||
consistent with the requirements of this License, to extend the patent
|
||||
license to downstream recipients. "Knowingly relying" means you have
|
||||
actual knowledge that, but for the patent license, your conveying the
|
||||
covered work in a country, or your recipient's use of the covered work
|
||||
in a country, would infringe one or more identifiable patents in that
|
||||
country that you have reason to believe are valid.
|
||||
|
||||
If, pursuant to or in connection with a single transaction or
|
||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||
covered work, and grant a patent license to some of the parties
|
||||
receiving the covered work authorizing them to use, propagate, modify
|
||||
or convey a specific copy of the covered work, then the patent license
|
||||
you grant is automatically extended to all recipients of the covered
|
||||
work and works based on it.
|
||||
|
||||
A patent license is "discriminatory" if it does not include within
|
||||
the scope of its coverage, prohibits the exercise of, or is
|
||||
conditioned on the non-exercise of one or more of the rights that are
|
||||
specifically granted under this License. You may not convey a covered
|
||||
work if you are a party to an arrangement with a third party that is
|
||||
in the business of distributing software, under which you make payment
|
||||
to the third party based on the extent of your activity of conveying
|
||||
the work, and under which the third party grants, to any of the
|
||||
parties who would receive the covered work from you, a discriminatory
|
||||
patent license (a) in connection with copies of the covered work
|
||||
conveyed by you (or copies made from those copies), or (b) primarily
|
||||
for and in connection with specific products or compilations that
|
||||
contain the covered work, unless you entered into that arrangement,
|
||||
or that patent license was granted, prior to 28 March 2007.
|
||||
|
||||
Nothing in this License shall be construed as excluding or limiting
|
||||
any implied license or other defenses to infringement that may
|
||||
otherwise be available to you under applicable patent law.
|
||||
|
||||
12. No Surrender of Others' Freedom.
|
||||
|
||||
If conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot convey a
|
||||
covered work so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you may
|
||||
not convey it at all. For example, if you agree to terms that obligate you
|
||||
to collect a royalty for further conveying from those to whom you convey
|
||||
the Program, the only way you could satisfy both those terms and this
|
||||
License would be to refrain entirely from conveying the Program.
|
||||
|
||||
13. Use with the GNU Affero General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU Affero General Public License into a single
|
||||
combined work, and to convey the resulting work. The terms of this
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the special requirements of the GNU Affero General Public License,
|
||||
section 13, concerning interaction through a network will apply to the
|
||||
combination as such.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU General Public License from time to time. Such new versions will
|
||||
be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Program specifies that a certain numbered version of the GNU General
|
||||
Public License "or any later version" applies to it, you have the
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
Foundation. If the Program does not specify a version number of the
|
||||
GNU General Public License, you may choose any version ever published
|
||||
by the Free Software Foundation.
|
||||
|
||||
If the Program specifies that a proxy can decide which future
|
||||
versions of the GNU General Public License can be used, that proxy's
|
||||
public statement of acceptance of a version permanently authorizes you
|
||||
to choose that version for the Program.
|
||||
|
||||
Later license versions may give you additional or different
|
||||
permissions. However, no additional obligations are imposed on any
|
||||
author or copyright holder as a result of your choosing to follow a
|
||||
later version.
|
||||
|
||||
15. Disclaimer of Warranty.
|
||||
|
||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
16. Limitation of Liability.
|
||||
|
||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||
SUCH DAMAGES.
|
||||
|
||||
17. Interpretation of Sections 15 and 16.
|
||||
|
||||
If the disclaimer of warranty and limitation of liability provided
|
||||
above cannot be given local legal effect according to their terms,
|
||||
reviewing courts shall apply local law that most closely approximates
|
||||
an absolute waiver of all civil liability in connection with the
|
||||
Program, unless a warranty or assumption of liability accompanies a
|
||||
copy of the Program in return for a fee.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
state the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If the program does terminal interaction, make it output a short
|
||||
notice like this when it starts in an interactive mode:
|
||||
|
||||
<program> Copyright (C) <year> <name of author>
|
||||
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||
This is free software, and you are welcome to redistribute it
|
||||
under certain conditions; type `show c' for details.
|
||||
|
||||
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||
parts of the General Public License. Of course, your program's commands
|
||||
might be different; for a GUI interface, you would use an "about box".
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||
For more information on this, and how to apply and follow the GNU GPL, see
|
||||
<https://www.gnu.org/licenses/>.
|
||||
|
||||
The GNU General Public License does not permit incorporating your program
|
||||
into proprietary programs. If your program is a subroutine library, you
|
||||
may consider it more useful to permit linking proprietary applications with
|
||||
the library. If this is what you want to do, use the GNU Lesser General
|
||||
Public License instead of this License. But first, please read
|
||||
<https://www.gnu.org/licenses/why-not-lgpl.html>.
|
||||
@@ -0,0 +1,161 @@
|
||||
# Teams Status Keeper
|
||||
|
||||
Keeps Microsoft Teams (or any presence-tracking app) from marking you as "Away"
|
||||
by nudging the mouse cursor when the machine has been idle long enough to
|
||||
trigger an idle timeout.
|
||||
|
||||
Real user movement always wins: the script never fires while the user is
|
||||
actively using the mouse, and any synthetic sweep aborts the moment the
|
||||
cursor leaves the position the script just commanded.
|
||||
|
||||
## Requirements
|
||||
|
||||
- [Bun](https://bun.sh) >= 1.0.0
|
||||
- macOS, Linux, or Windows (relies on
|
||||
[`@nut-tree-fork/nut-js`](https://github.com/nut-tree-fork/nut.js) for
|
||||
cross-platform mouse + screen control)
|
||||
- On macOS: Accessibility permission for the terminal running Bun
|
||||
(System Settings > Privacy & Security > Accessibility)
|
||||
|
||||
## Install
|
||||
|
||||
```sh
|
||||
./install.sh
|
||||
```
|
||||
|
||||
The installer verifies `bun` is on `PATH` and runs `bun install`. It does
|
||||
not auto-install Bun; if Bun is missing it prints a hard error pointing at
|
||||
<https://bun.sh>.
|
||||
|
||||
## Run
|
||||
|
||||
```sh
|
||||
bun run start
|
||||
```
|
||||
|
||||
Or directly:
|
||||
|
||||
```sh
|
||||
bun run src/move.ts
|
||||
```
|
||||
|
||||
Or install it as a global `move` command via Bun's bin-linking:
|
||||
|
||||
```sh
|
||||
bun link
|
||||
move
|
||||
```
|
||||
|
||||
`bun link` registers the `bin.move` entry from `package.json` (`./src/move.ts`,
|
||||
which carries `#!/usr/bin/env bun`) as an executable on your `PATH`.
|
||||
|
||||
Stop with `Ctrl+C`. If `SIGINT` arrives mid-sweep, the cursor stays at
|
||||
whichever step was last commanded — by design.
|
||||
|
||||
## Usage
|
||||
|
||||
```text
|
||||
Usage: move [options]
|
||||
|
||||
Options:
|
||||
-h, --help Show this help and exit.
|
||||
-v, --version Print version and exit.
|
||||
-m, --move-interval <seconds> Idle time before a sweep fires. Default: 240.
|
||||
-c, --check-interval <seconds> Cursor poll cadence. Default: 10.
|
||||
-d, --step-delay <ms> Pause between synthetic steps. Default: 50.
|
||||
-n, --step-count <pixels> Steps per sweep. Default: 250.
|
||||
-V, --verbose Log every sweep, interrupt, and bounds event
|
||||
(default prints only the startup banner).
|
||||
```
|
||||
|
||||
Run `bun run src/move.ts --help` for the full text.
|
||||
|
||||
All flags are wired. Numeric overrides are layered onto the defaults via
|
||||
`resolveConfig` in `src/config.ts`; time-valued inputs (`-m`, `-c`) are
|
||||
expressed in seconds at the CLI boundary and converted to milliseconds
|
||||
internally.
|
||||
|
||||
Logging is **quiet by default**: only the startup banner ("Teams Status
|
||||
Keeper started…") and any error from an unhandled rejection print on a
|
||||
default run. `-V` / `--verbose` opens up per-sweep, user-interrupt, and
|
||||
out-of-bounds events.
|
||||
|
||||
Invalid input (unknown flag, missing value, non-positive number) prints an
|
||||
error to `stderr` and exits with code `2`.
|
||||
|
||||
## How it works
|
||||
|
||||
The source lives under `src/`, split into an entry point plus three logic
|
||||
modules:
|
||||
|
||||
- `src/move.ts` is a thin entry point: parses args, dispatches `--help` /
|
||||
`--version`, resolves the runtime config, and calls
|
||||
`runKeeper(config, verbose)`.
|
||||
- `src/cli.ts` owns argument parsing, validation, and help/version output.
|
||||
- `src/config.ts` exports the `Config` type, `DEFAULT_CONFIG`, and the
|
||||
`resolveConfig` overlay function.
|
||||
- `src/keeper.ts` owns the synthetic-activity sweep and the idle-watch loop.
|
||||
|
||||
Defaults live in `src/config.ts` as `DEFAULT_CONFIG`:
|
||||
|
||||
| Field | Default | CLI flag | Purpose |
|
||||
| --------------- | ------------ | ------------------------- | ---------------------------------------------------------------- |
|
||||
| `moveInterval` | `4 * 60_000` | `-m`, `--move-interval` | Idle time (ms) required before a synthetic sweep fires. |
|
||||
| `checkInterval` | `10_000` | `-c`, `--check-interval` | How often (ms) the main loop polls the cursor for real activity. |
|
||||
| `stepDelay` | `50` | `-d`, `--step-delay` | Pause (ms) between individual synthetic steps in a sweep. |
|
||||
| `stepCount` | `250` | `-n`, `--step-count` | Pixel-steps per sweep. |
|
||||
|
||||
`-m` and `-c` are accepted in seconds at the CLI; `resolveConfig` converts
|
||||
to milliseconds before handing the resolved `Config` to `runKeeper`.
|
||||
|
||||
### Main loop (`runKeeper`)
|
||||
|
||||
1. Print the startup banner (unconditional).
|
||||
2. Snapshot `lastPos` and `lastActivity = now`.
|
||||
3. Every `config.checkInterval`:
|
||||
- If the cursor moved since the last check, the user is active — reset
|
||||
`lastActivity` and `lastPos`, continue.
|
||||
- Otherwise, if `now - lastActivity >= config.moveInterval`, call
|
||||
`simulateActivity` and reset the idleness clock.
|
||||
|
||||
### Synthetic sweep (`simulateActivity`)
|
||||
|
||||
1. Read the starting position and current screen dimensions.
|
||||
2. Pick a horizontal direction (`dx = +1` if there's room to the right,
|
||||
else `-1`) so the sweep stays on-screen. Vertical is `dy = 0` for now.
|
||||
3. For each of `config.stepCount` steps:
|
||||
- Compute and bounds-check the next target.
|
||||
- Move the cursor there, sleep `config.stepDelay`.
|
||||
- Re-read the cursor. If it isn't where we put it, the user moved it —
|
||||
log (when `--verbose`) and return early without snapping back.
|
||||
4. On a clean full sweep, restore the cursor to its starting position so
|
||||
the next idle-check sees "no movement" and doesn't misread the synthetic
|
||||
activity as real user input.
|
||||
|
||||
### Why `mouse.config.autoDelayMs = 0`
|
||||
|
||||
nut.js inserts a 100ms delay after every action by default. With two mouse
|
||||
calls per step that would silently more-than-double the duration of a
|
||||
sweep. The script controls cadence itself via `config.stepDelay`, so the
|
||||
implicit delay is disabled at module load (a side effect of importing
|
||||
`keeper.ts`).
|
||||
|
||||
## Files
|
||||
|
||||
| File | Purpose |
|
||||
| ------------------- | ----------------------------------------------------------------------- |
|
||||
| `src/move.ts` | CLI entry point: parses args, dispatches help/version, starts the loop. |
|
||||
| `src/cli.ts` | Argument parsing, validation, and help/version output. |
|
||||
| `src/config.ts` | `Config` type, `DEFAULT_CONFIG`, and `resolveConfig` overlay. |
|
||||
| `src/keeper.ts` | Synthetic-activity sweep and idle-watch loop. |
|
||||
| `install.sh` | Bootstrap: verify Bun, `bun install`, print macOS permission hint. |
|
||||
| `package.json` | Bun project manifest. Single runtime dep: `@nut-tree-fork/nut-js`. |
|
||||
| `tsconfig.json` | Strict TypeScript config tuned for Bun (ESNext, bundler resolution). |
|
||||
| `bun.lock` | Bun's lockfile. Commit this. |
|
||||
| `LICENSE` | GPLv3 license text. |
|
||||
| `CHANGES.md` | History of the post-port review fixes. |
|
||||
| `MouseMover.java` | Original Java source, kept for reference. |
|
||||
|
||||
## License
|
||||
|
||||
GPL-3.0-only. See `LICENSE`.
|
||||
@@ -0,0 +1,281 @@
|
||||
{
|
||||
"lockfileVersion": 1,
|
||||
"configVersion": 1,
|
||||
"workspaces": {
|
||||
"": {
|
||||
"name": "move",
|
||||
"dependencies": {
|
||||
"@nut-tree-fork/nut-js": "^4.2.2",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "latest",
|
||||
"typescript": "^5",
|
||||
},
|
||||
},
|
||||
},
|
||||
"packages": {
|
||||
"@jimp/bmp": ["@jimp/bmp@0.22.12", "", { "dependencies": { "@jimp/utils": "^0.22.12", "bmp-js": "^0.1.0" }, "peerDependencies": { "@jimp/custom": ">=0.3.5" } }, "sha512-aeI64HD0npropd+AR76MCcvvRaa+Qck6loCOS03CkkxGHN5/r336qTM5HPUdHKMDOGzqknuVPA8+kK1t03z12g=="],
|
||||
|
||||
"@jimp/core": ["@jimp/core@0.22.12", "", { "dependencies": { "@jimp/utils": "^0.22.12", "any-base": "^1.1.0", "buffer": "^5.2.0", "exif-parser": "^0.1.12", "file-type": "^16.5.4", "isomorphic-fetch": "^3.0.0", "pixelmatch": "^4.0.2", "tinycolor2": "^1.6.0" } }, "sha512-l0RR0dOPyzMKfjUW1uebzueFEDtCOj9fN6pyTYWWOM/VS4BciXQ1VVrJs8pO3kycGYZxncRKhCoygbNr8eEZQA=="],
|
||||
|
||||
"@jimp/custom": ["@jimp/custom@0.22.12", "", { "dependencies": { "@jimp/core": "^0.22.12" } }, "sha512-xcmww1O/JFP2MrlGUMd3Q78S3Qu6W3mYTXYuIqFq33EorgYHV/HqymHfXy9GjiCJ7OI+7lWx6nYFOzU7M4rd1Q=="],
|
||||
|
||||
"@jimp/gif": ["@jimp/gif@0.22.12", "", { "dependencies": { "@jimp/utils": "^0.22.12", "gifwrap": "^0.10.1", "omggif": "^1.0.9" }, "peerDependencies": { "@jimp/custom": ">=0.3.5" } }, "sha512-y6BFTJgch9mbor2H234VSjd9iwAhaNf/t3US5qpYIs0TSbAvM02Fbc28IaDETj9+4YB4676sz4RcN/zwhfu1pg=="],
|
||||
|
||||
"@jimp/jpeg": ["@jimp/jpeg@0.22.12", "", { "dependencies": { "@jimp/utils": "^0.22.12", "jpeg-js": "^0.4.4" }, "peerDependencies": { "@jimp/custom": ">=0.3.5" } }, "sha512-Rq26XC/uQWaQKyb/5lksCTCxXhtY01NJeBN+dQv5yNYedN0i7iYu+fXEoRsfaJ8xZzjoANH8sns7rVP4GE7d/Q=="],
|
||||
|
||||
"@jimp/plugin-blit": ["@jimp/plugin-blit@0.22.12", "", { "dependencies": { "@jimp/utils": "^0.22.12" }, "peerDependencies": { "@jimp/custom": ">=0.3.5" } }, "sha512-xslz2ZoFZOPLY8EZ4dC29m168BtDx95D6K80TzgUi8gqT7LY6CsajWO0FAxDwHz6h0eomHMfyGX0stspBrTKnQ=="],
|
||||
|
||||
"@jimp/plugin-blur": ["@jimp/plugin-blur@0.22.12", "", { "dependencies": { "@jimp/utils": "^0.22.12" }, "peerDependencies": { "@jimp/custom": ">=0.3.5" } }, "sha512-S0vJADTuh1Q9F+cXAwFPlrKWzDj2F9t/9JAbUvaaDuivpyWuImEKXVz5PUZw2NbpuSHjwssbTpOZ8F13iJX4uw=="],
|
||||
|
||||
"@jimp/plugin-circle": ["@jimp/plugin-circle@0.22.12", "", { "dependencies": { "@jimp/utils": "^0.22.12" }, "peerDependencies": { "@jimp/custom": ">=0.3.5" } }, "sha512-SWVXx1yiuj5jZtMijqUfvVOJBwOifFn0918ou4ftoHgegc5aHWW5dZbYPjvC9fLpvz7oSlptNl2Sxr1zwofjTg=="],
|
||||
|
||||
"@jimp/plugin-color": ["@jimp/plugin-color@0.22.12", "", { "dependencies": { "@jimp/utils": "^0.22.12", "tinycolor2": "^1.6.0" }, "peerDependencies": { "@jimp/custom": ">=0.3.5" } }, "sha512-xImhTE5BpS8xa+mAN6j4sMRWaUgUDLoaGHhJhpC+r7SKKErYDR0WQV4yCE4gP+N0gozD0F3Ka1LUSaMXrn7ZIA=="],
|
||||
|
||||
"@jimp/plugin-contain": ["@jimp/plugin-contain@0.22.12", "", { "dependencies": { "@jimp/utils": "^0.22.12" }, "peerDependencies": { "@jimp/custom": ">=0.3.5", "@jimp/plugin-blit": ">=0.3.5", "@jimp/plugin-resize": ">=0.3.5", "@jimp/plugin-scale": ">=0.3.5" } }, "sha512-Eo3DmfixJw3N79lWk8q/0SDYbqmKt1xSTJ69yy8XLYQj9svoBbyRpSnHR+n9hOw5pKXytHwUW6nU4u1wegHNoQ=="],
|
||||
|
||||
"@jimp/plugin-cover": ["@jimp/plugin-cover@0.22.12", "", { "dependencies": { "@jimp/utils": "^0.22.12" }, "peerDependencies": { "@jimp/custom": ">=0.3.5", "@jimp/plugin-crop": ">=0.3.5", "@jimp/plugin-resize": ">=0.3.5", "@jimp/plugin-scale": ">=0.3.5" } }, "sha512-z0w/1xH/v/knZkpTNx+E8a7fnasQ2wHG5ze6y5oL2dhH1UufNua8gLQXlv8/W56+4nJ1brhSd233HBJCo01BXA=="],
|
||||
|
||||
"@jimp/plugin-crop": ["@jimp/plugin-crop@0.22.12", "", { "dependencies": { "@jimp/utils": "^0.22.12" }, "peerDependencies": { "@jimp/custom": ">=0.3.5" } }, "sha512-FNuUN0OVzRCozx8XSgP9MyLGMxNHHJMFt+LJuFjn1mu3k0VQxrzqbN06yIl46TVejhyAhcq5gLzqmSCHvlcBVw=="],
|
||||
|
||||
"@jimp/plugin-displace": ["@jimp/plugin-displace@0.22.12", "", { "dependencies": { "@jimp/utils": "^0.22.12" }, "peerDependencies": { "@jimp/custom": ">=0.3.5" } }, "sha512-qpRM8JRicxfK6aPPqKZA6+GzBwUIitiHaZw0QrJ64Ygd3+AsTc7BXr+37k2x7QcyCvmKXY4haUrSIsBug4S3CA=="],
|
||||
|
||||
"@jimp/plugin-dither": ["@jimp/plugin-dither@0.22.12", "", { "dependencies": { "@jimp/utils": "^0.22.12" }, "peerDependencies": { "@jimp/custom": ">=0.3.5" } }, "sha512-jYgGdSdSKl1UUEanX8A85v4+QUm+PE8vHFwlamaKk89s+PXQe7eVE3eNeSZX4inCq63EHL7cX580dMqkoC3ZLw=="],
|
||||
|
||||
"@jimp/plugin-fisheye": ["@jimp/plugin-fisheye@0.22.12", "", { "dependencies": { "@jimp/utils": "^0.22.12" }, "peerDependencies": { "@jimp/custom": ">=0.3.5" } }, "sha512-LGuUTsFg+fOp6KBKrmLkX4LfyCy8IIsROwoUvsUPKzutSqMJnsm3JGDW2eOmWIS/jJpPaeaishjlxvczjgII+Q=="],
|
||||
|
||||
"@jimp/plugin-flip": ["@jimp/plugin-flip@0.22.12", "", { "dependencies": { "@jimp/utils": "^0.22.12" }, "peerDependencies": { "@jimp/custom": ">=0.3.5", "@jimp/plugin-rotate": ">=0.3.5" } }, "sha512-m251Rop7GN8W0Yo/rF9LWk6kNclngyjIJs/VXHToGQ6EGveOSTSQaX2Isi9f9lCDLxt+inBIb7nlaLLxnvHX8Q=="],
|
||||
|
||||
"@jimp/plugin-gaussian": ["@jimp/plugin-gaussian@0.22.12", "", { "dependencies": { "@jimp/utils": "^0.22.12" }, "peerDependencies": { "@jimp/custom": ">=0.3.5" } }, "sha512-sBfbzoOmJ6FczfG2PquiK84NtVGeScw97JsCC3rpQv1PHVWyW+uqWFF53+n3c8Y0P2HWlUjflEla2h/vWShvhg=="],
|
||||
|
||||
"@jimp/plugin-invert": ["@jimp/plugin-invert@0.22.12", "", { "dependencies": { "@jimp/utils": "^0.22.12" }, "peerDependencies": { "@jimp/custom": ">=0.3.5" } }, "sha512-N+6rwxdB+7OCR6PYijaA/iizXXodpxOGvT/smd/lxeXsZ/empHmFFFJ/FaXcYh19Tm04dGDaXcNF/dN5nm6+xQ=="],
|
||||
|
||||
"@jimp/plugin-mask": ["@jimp/plugin-mask@0.22.12", "", { "dependencies": { "@jimp/utils": "^0.22.12" }, "peerDependencies": { "@jimp/custom": ">=0.3.5" } }, "sha512-4AWZg+DomtpUA099jRV8IEZUfn1wLv6+nem4NRJC7L/82vxzLCgXKTxvNvBcNmJjT9yS1LAAmiJGdWKXG63/NA=="],
|
||||
|
||||
"@jimp/plugin-normalize": ["@jimp/plugin-normalize@0.22.12", "", { "dependencies": { "@jimp/utils": "^0.22.12" }, "peerDependencies": { "@jimp/custom": ">=0.3.5" } }, "sha512-0So0rexQivnWgnhacX4cfkM2223YdExnJTTy6d06WbkfZk5alHUx8MM3yEzwoCN0ErO7oyqEWRnEkGC+As1FtA=="],
|
||||
|
||||
"@jimp/plugin-print": ["@jimp/plugin-print@0.22.12", "", { "dependencies": { "@jimp/utils": "^0.22.12", "load-bmfont": "^1.4.1" }, "peerDependencies": { "@jimp/custom": ">=0.3.5", "@jimp/plugin-blit": ">=0.3.5" } }, "sha512-c7TnhHlxm87DJeSnwr/XOLjJU/whoiKYY7r21SbuJ5nuH+7a78EW1teOaj5gEr2wYEd7QtkFqGlmyGXY/YclyQ=="],
|
||||
|
||||
"@jimp/plugin-resize": ["@jimp/plugin-resize@0.22.12", "", { "dependencies": { "@jimp/utils": "^0.22.12" }, "peerDependencies": { "@jimp/custom": ">=0.3.5" } }, "sha512-3NyTPlPbTnGKDIbaBgQ3HbE6wXbAlFfxHVERmrbqAi8R3r6fQPxpCauA8UVDnieg5eo04D0T8nnnNIX//i/sXg=="],
|
||||
|
||||
"@jimp/plugin-rotate": ["@jimp/plugin-rotate@0.22.12", "", { "dependencies": { "@jimp/utils": "^0.22.12" }, "peerDependencies": { "@jimp/custom": ">=0.3.5", "@jimp/plugin-blit": ">=0.3.5", "@jimp/plugin-crop": ">=0.3.5", "@jimp/plugin-resize": ">=0.3.5" } }, "sha512-9YNEt7BPAFfTls2FGfKBVgwwLUuKqy+E8bDGGEsOqHtbuhbshVGxN2WMZaD4gh5IDWvR+emmmPPWGgaYNYt1gA=="],
|
||||
|
||||
"@jimp/plugin-scale": ["@jimp/plugin-scale@0.22.12", "", { "dependencies": { "@jimp/utils": "^0.22.12" }, "peerDependencies": { "@jimp/custom": ">=0.3.5", "@jimp/plugin-resize": ">=0.3.5" } }, "sha512-dghs92qM6MhHj0HrV2qAwKPMklQtjNpoYgAB94ysYpsXslhRTiPisueSIELRwZGEr0J0VUxpUY7HgJwlSIgGZw=="],
|
||||
|
||||
"@jimp/plugin-shadow": ["@jimp/plugin-shadow@0.22.12", "", { "dependencies": { "@jimp/utils": "^0.22.12" }, "peerDependencies": { "@jimp/custom": ">=0.3.5", "@jimp/plugin-blur": ">=0.3.5", "@jimp/plugin-resize": ">=0.3.5" } }, "sha512-FX8mTJuCt7/3zXVoeD/qHlm4YH2bVqBuWQHXSuBK054e7wFRnRnbSLPUqAwSeYP3lWqpuQzJtgiiBxV3+WWwTg=="],
|
||||
|
||||
"@jimp/plugin-threshold": ["@jimp/plugin-threshold@0.22.12", "", { "dependencies": { "@jimp/utils": "^0.22.12" }, "peerDependencies": { "@jimp/custom": ">=0.3.5", "@jimp/plugin-color": ">=0.8.0", "@jimp/plugin-resize": ">=0.8.0" } }, "sha512-4x5GrQr1a/9L0paBC/MZZJjjgjxLYrqSmWd+e+QfAEPvmRxdRoQ5uKEuNgXnm9/weHQBTnQBQsOY2iFja+XGAw=="],
|
||||
|
||||
"@jimp/plugins": ["@jimp/plugins@0.22.12", "", { "dependencies": { "@jimp/plugin-blit": "^0.22.12", "@jimp/plugin-blur": "^0.22.12", "@jimp/plugin-circle": "^0.22.12", "@jimp/plugin-color": "^0.22.12", "@jimp/plugin-contain": "^0.22.12", "@jimp/plugin-cover": "^0.22.12", "@jimp/plugin-crop": "^0.22.12", "@jimp/plugin-displace": "^0.22.12", "@jimp/plugin-dither": "^0.22.12", "@jimp/plugin-fisheye": "^0.22.12", "@jimp/plugin-flip": "^0.22.12", "@jimp/plugin-gaussian": "^0.22.12", "@jimp/plugin-invert": "^0.22.12", "@jimp/plugin-mask": "^0.22.12", "@jimp/plugin-normalize": "^0.22.12", "@jimp/plugin-print": "^0.22.12", "@jimp/plugin-resize": "^0.22.12", "@jimp/plugin-rotate": "^0.22.12", "@jimp/plugin-scale": "^0.22.12", "@jimp/plugin-shadow": "^0.22.12", "@jimp/plugin-threshold": "^0.22.12", "timm": "^1.6.1" }, "peerDependencies": { "@jimp/custom": ">=0.3.5" } }, "sha512-yBJ8vQrDkBbTgQZLty9k4+KtUQdRjsIDJSPjuI21YdVeqZxYywifHl4/XWILoTZsjTUASQcGoH0TuC0N7xm3ww=="],
|
||||
|
||||
"@jimp/png": ["@jimp/png@0.22.12", "", { "dependencies": { "@jimp/utils": "^0.22.12", "pngjs": "^6.0.0" }, "peerDependencies": { "@jimp/custom": ">=0.3.5" } }, "sha512-Mrp6dr3UTn+aLK8ty/dSKELz+Otdz1v4aAXzV5q53UDD2rbB5joKVJ/ChY310B+eRzNxIovbUF1KVrUsYdE8Hg=="],
|
||||
|
||||
"@jimp/tiff": ["@jimp/tiff@0.22.12", "", { "dependencies": { "utif2": "^4.0.1" }, "peerDependencies": { "@jimp/custom": ">=0.3.5" } }, "sha512-E1LtMh4RyJsoCAfAkBRVSYyZDTtLq9p9LUiiYP0vPtXyxX4BiYBUYihTLSBlCQg5nF2e4OpQg7SPrLdJ66u7jg=="],
|
||||
|
||||
"@jimp/types": ["@jimp/types@0.22.12", "", { "dependencies": { "@jimp/bmp": "^0.22.12", "@jimp/gif": "^0.22.12", "@jimp/jpeg": "^0.22.12", "@jimp/png": "^0.22.12", "@jimp/tiff": "^0.22.12", "timm": "^1.6.1" }, "peerDependencies": { "@jimp/custom": ">=0.3.5" } }, "sha512-wwKYzRdElE1MBXFREvCto5s699izFHNVvALUv79GXNbsOVqlwlOxlWJ8DuyOGIXoLP4JW/m30YyuTtfUJgMRMA=="],
|
||||
|
||||
"@jimp/utils": ["@jimp/utils@0.22.12", "", { "dependencies": { "regenerator-runtime": "^0.13.3" } }, "sha512-yJ5cWUknGnilBq97ZXOyOS0HhsHOyAyjHwYfHxGbSyMTohgQI6sVyE8KPgDwH8HHW/nMKXk8TrSwAE71zt716Q=="],
|
||||
|
||||
"@nut-tree-fork/default-clipboard-provider": ["@nut-tree-fork/default-clipboard-provider@4.2.6", "", { "dependencies": { "clipboardy": "2.3.0" } }, "sha512-Hzqj57rheIMGtsS4zK4//kOhaX5FxMluOiz+4TVaHXx+idZS/bPhZwd8e6o1w1GT0PVJOUIP+4CdUe//k5VRig=="],
|
||||
|
||||
"@nut-tree-fork/libnut": ["@nut-tree-fork/libnut@4.2.6", "", { "dependencies": { "@nut-tree-fork/libnut-darwin": "2.7.5", "@nut-tree-fork/libnut-linux": "2.7.5", "@nut-tree-fork/libnut-win32": "2.7.5" } }, "sha512-2FCiTBokMGrMl4eL/trEIO+mtpkXpdPHoVKdTBmW8UBIbhCbrCKmnXb2skWGfVs+U3q7o5EYDjVTNUYaUWbaxQ=="],
|
||||
|
||||
"@nut-tree-fork/libnut-darwin": ["@nut-tree-fork/libnut-darwin@2.7.5", "", { "dependencies": { "bindings": "1.5.0" }, "optionalDependencies": { "@nut-tree-fork/node-mac-permissions": "2.2.1" }, "os": [ "linux", "win32", "darwin", ], "cpu": [ "x64", "arm64", ] }, "sha512-LbqtPtMPTJUcg4XoPP2jsU1wc8flBcGyKTerKsIfK9cD7nBHROnO0QksbrsbSWEpLym8T8fRtuU7XEY83l6Z2Q=="],
|
||||
|
||||
"@nut-tree-fork/libnut-linux": ["@nut-tree-fork/libnut-linux@2.7.5", "", { "dependencies": { "bindings": "1.5.0" }, "optionalDependencies": { "@nut-tree-fork/node-mac-permissions": "2.2.1" }, "os": [ "linux", "win32", "darwin", ], "cpu": [ "x64", "arm64", ] }, "sha512-uxaXEcRKnFObAljsoR6tLOBUU1dJ2sctloG6gFgCBGN7+k6Jdv6jZfOuNjd/fpdq2C5WPMm0rtn9EE7h5J3Jcg=="],
|
||||
|
||||
"@nut-tree-fork/libnut-win32": ["@nut-tree-fork/libnut-win32@2.7.5", "", { "dependencies": { "bindings": "1.5.0" }, "optionalDependencies": { "@nut-tree-fork/node-mac-permissions": "2.2.1" }, "os": [ "linux", "win32", "darwin", ], "cpu": [ "x64", "arm64", ] }, "sha512-yqC87zvmFcDPwFrRU40DYhN0xmEVM3aSkOuyF0IX+y1x+HWSu/i0PNklATpPBhGid3QVb/TOHuVoaraMrUFCNw=="],
|
||||
|
||||
"@nut-tree-fork/node-mac-permissions": ["@nut-tree-fork/node-mac-permissions@2.2.1", "", { "dependencies": { "bindings": "1.5.0", "node-addon-api": "5.0.0" }, "os": "darwin" }, "sha512-iSfOTDiBZ7VDa17PoQje5rUaZSvSAaq+XEyXCmhPuQwV5XuNU02Grv6oFhsdpz89w7+UvB/8KX/cX5IYQ5o2Bw=="],
|
||||
|
||||
"@nut-tree-fork/nut-js": ["@nut-tree-fork/nut-js@4.2.6", "", { "dependencies": { "@nut-tree-fork/default-clipboard-provider": "4.2.6", "@nut-tree-fork/libnut": "4.2.6", "@nut-tree-fork/provider-interfaces": "4.2.6", "@nut-tree-fork/shared": "4.2.6", "jimp": "0.22.10", "node-abort-controller": "3.1.1" }, "os": [ "linux", "win32", "darwin", ], "cpu": [ "x64", "arm64", ] }, "sha512-aI/WCX7gE1HFGPH3EZP/UWqpNMM1NMoM/EkXqp7pKMgXFCi8e5+o5p+jd/QOYpmALv9bQg7+s69nI7FONbMqDg=="],
|
||||
|
||||
"@nut-tree-fork/provider-interfaces": ["@nut-tree-fork/provider-interfaces@4.2.6", "", { "dependencies": { "@nut-tree-fork/shared": "4.2.6" } }, "sha512-brtRegDkLSV0sa5DUAigjWf6hCoamBNPb/hKK9AQlW+j3BxQ/8djaEdEB2cihqUh1ZjEtgPyXRqpCWSdKCX68A=="],
|
||||
|
||||
"@nut-tree-fork/shared": ["@nut-tree-fork/shared@4.2.6", "", { "dependencies": { "jimp": "0.22.10", "node-abort-controller": "3.1.1" } }, "sha512-xZaa0YtJt/DDDq/i1vZkabjq8HOWzfhXieMai61cMbYD11J6VhAfhV23ZtQEM02WG7nc2LKjl4UwRnQCteikwA=="],
|
||||
|
||||
"@tokenizer/token": ["@tokenizer/token@0.3.0", "", {}, "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A=="],
|
||||
|
||||
"@types/bun": ["@types/bun@1.3.14", "", { "dependencies": { "bun-types": "1.3.14" } }, "sha512-h1hFqFVcvAvD9j9K7ZW7vd82aSA+rTdznZa+5bwvCwqSB1jmmfLcbIWhOLx1/+boy/xmjgCs/OMUL8hRJSmnPw=="],
|
||||
|
||||
"@types/node": ["@types/node@25.9.1", "", { "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } }, "sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg=="],
|
||||
|
||||
"abort-controller": ["abort-controller@3.0.0", "", { "dependencies": { "event-target-shim": "^5.0.0" } }, "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg=="],
|
||||
|
||||
"any-base": ["any-base@1.1.0", "", {}, "sha512-uMgjozySS8adZZYePpaWs8cxB9/kdzmpX6SgJZ+wbz1K5eYk5QMYDVJaZKhxyIHUdnnJkfR7SVgStgH7LkGUyg=="],
|
||||
|
||||
"arch": ["arch@2.2.0", "", {}, "sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ=="],
|
||||
|
||||
"base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="],
|
||||
|
||||
"bindings": ["bindings@1.5.0", "", { "dependencies": { "file-uri-to-path": "1.0.0" } }, "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ=="],
|
||||
|
||||
"bmp-js": ["bmp-js@0.1.0", "", {}, "sha512-vHdS19CnY3hwiNdkaqk93DvjVLfbEcI8mys4UjuWrlX1haDmroo8o4xCzh4wD6DGV6HxRCyauwhHRqMTfERtjw=="],
|
||||
|
||||
"buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="],
|
||||
|
||||
"buffer-equal": ["buffer-equal@0.0.1", "", {}, "sha512-RgSV6InVQ9ODPdLWJ5UAqBqJBOg370Nz6ZQtRzpt6nUjc8v0St97uJ4PYC6NztqIScrAXafKM3mZPMygSe1ggA=="],
|
||||
|
||||
"bun-types": ["bun-types@1.3.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ=="],
|
||||
|
||||
"centra": ["centra@2.7.0", "", { "dependencies": { "follow-redirects": "^1.15.6" } }, "sha512-PbFMgMSrmgx6uxCdm57RUos9Tc3fclMvhLSATYN39XsDV29B89zZ3KA89jmY0vwSGazyU+uerqwa6t+KaodPcg=="],
|
||||
|
||||
"clipboardy": ["clipboardy@2.3.0", "", { "dependencies": { "arch": "^2.1.1", "execa": "^1.0.0", "is-wsl": "^2.1.1" } }, "sha512-mKhiIL2DrQIsuXMgBgnfEHOZOryC7kY7YO//TN6c63wlEm3NG5tz+YgY5rVi29KCmq/QQjKYvM7a19+MDOTHOQ=="],
|
||||
|
||||
"cross-spawn": ["cross-spawn@6.0.6", "", { "dependencies": { "nice-try": "^1.0.4", "path-key": "^2.0.1", "semver": "^5.5.0", "shebang-command": "^1.2.0", "which": "^1.2.9" } }, "sha512-VqCUuhcd1iB+dsv8gxPttb5iZh/D0iubSP21g36KXdEuf6I5JiioesUVjpCdHV9MZRUfVFlvwtIUyPfxo5trtw=="],
|
||||
|
||||
"dom-walk": ["dom-walk@0.1.2", "", {}, "sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w=="],
|
||||
|
||||
"end-of-stream": ["end-of-stream@1.4.5", "", { "dependencies": { "once": "^1.4.0" } }, "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg=="],
|
||||
|
||||
"event-target-shim": ["event-target-shim@5.0.1", "", {}, "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ=="],
|
||||
|
||||
"events": ["events@3.3.0", "", {}, "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q=="],
|
||||
|
||||
"execa": ["execa@1.0.0", "", { "dependencies": { "cross-spawn": "^6.0.0", "get-stream": "^4.0.0", "is-stream": "^1.1.0", "npm-run-path": "^2.0.0", "p-finally": "^1.0.0", "signal-exit": "^3.0.0", "strip-eof": "^1.0.0" } }, "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA=="],
|
||||
|
||||
"exif-parser": ["exif-parser@0.1.12", "", {}, "sha512-c2bQfLNbMzLPmzQuOr8fy0csy84WmwnER81W88DzTp9CYNPJ6yzOj2EZAh9pywYpqHnshVLHQJ8WzldAyfY+Iw=="],
|
||||
|
||||
"file-type": ["file-type@16.5.4", "", { "dependencies": { "readable-web-to-node-stream": "^3.0.0", "strtok3": "^6.2.4", "token-types": "^4.1.1" } }, "sha512-/yFHK0aGjFEgDJjEKP0pWCplsPFPhwyfwevf/pVxiN0tmE4L9LmwWxWukdJSHdoCli4VgQLehjJtwQBnqmsKcw=="],
|
||||
|
||||
"file-uri-to-path": ["file-uri-to-path@1.0.0", "", {}, "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw=="],
|
||||
|
||||
"follow-redirects": ["follow-redirects@1.16.0", "", { "peerDependencies": { "debug": "*" }, "optionalPeers": ["debug"] }, "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw=="],
|
||||
|
||||
"get-stream": ["get-stream@4.1.0", "", { "dependencies": { "pump": "^3.0.0" } }, "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w=="],
|
||||
|
||||
"gifwrap": ["gifwrap@0.10.1", "", { "dependencies": { "image-q": "^4.0.0", "omggif": "^1.0.10" } }, "sha512-2760b1vpJHNmLzZ/ubTtNnEx5WApN/PYWJvXvgS+tL1egTTthayFYIQQNi136FLEDcN/IyEY2EcGpIITD6eYUw=="],
|
||||
|
||||
"global": ["global@4.4.0", "", { "dependencies": { "min-document": "^2.19.0", "process": "^0.11.10" } }, "sha512-wv/LAoHdRE3BeTGz53FAamhGlPLhlssK45usmGFThIi4XqnBmjKQ16u+RNbP7WvigRZDxUsM0J3gcQ5yicaL0w=="],
|
||||
|
||||
"ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="],
|
||||
|
||||
"image-q": ["image-q@4.0.0", "", { "dependencies": { "@types/node": "16.9.1" } }, "sha512-PfJGVgIfKQJuq3s0tTDOKtztksibuUEbJQIYT3by6wctQo+Rdlh7ef4evJ5NCdxY4CfMbvFkocEwbl4BF8RlJw=="],
|
||||
|
||||
"is-docker": ["is-docker@2.2.1", "", { "bin": { "is-docker": "cli.js" } }, "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ=="],
|
||||
|
||||
"is-function": ["is-function@1.0.2", "", {}, "sha512-lw7DUp0aWXYg+CBCN+JKkcE0Q2RayZnSvnZBlwgxHBQhqt5pZNVy4Ri7H9GmmXkdu7LUthszM+Tor1u/2iBcpQ=="],
|
||||
|
||||
"is-stream": ["is-stream@1.1.0", "", {}, "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ=="],
|
||||
|
||||
"is-wsl": ["is-wsl@2.2.0", "", { "dependencies": { "is-docker": "^2.0.0" } }, "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww=="],
|
||||
|
||||
"isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
|
||||
|
||||
"isomorphic-fetch": ["isomorphic-fetch@3.0.0", "", { "dependencies": { "node-fetch": "^2.6.1", "whatwg-fetch": "^3.4.1" } }, "sha512-qvUtwJ3j6qwsF3jLxkZ72qCgjMysPzDfeV240JHiGZsANBYd+EEuu35v7dfrJ9Up0Ak07D7GGSkGhCHTqg/5wA=="],
|
||||
|
||||
"jimp": ["jimp@0.22.10", "", { "dependencies": { "@jimp/custom": "^0.22.10", "@jimp/plugins": "^0.22.10", "@jimp/types": "^0.22.10", "regenerator-runtime": "^0.13.3" } }, "sha512-lCaHIJAgTOsplyJzC1w/laxSxrbSsEBw4byKwXgUdMmh+ayPsnidTblenQm+IvhIs44Gcuvlb6pd2LQ0wcKaKg=="],
|
||||
|
||||
"jpeg-js": ["jpeg-js@0.4.4", "", {}, "sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg=="],
|
||||
|
||||
"load-bmfont": ["load-bmfont@1.4.2", "", { "dependencies": { "buffer-equal": "0.0.1", "mime": "^1.3.4", "parse-bmfont-ascii": "^1.0.3", "parse-bmfont-binary": "^1.0.5", "parse-bmfont-xml": "^1.1.4", "phin": "^3.7.1", "xhr": "^2.0.1", "xtend": "^4.0.0" } }, "sha512-qElWkmjW9Oq1F9EI5Gt7aD9zcdHb9spJCW1L/dmPf7KzCCEJxq8nhHz5eCgI9aMf7vrG/wyaCqdsI+Iy9ZTlog=="],
|
||||
|
||||
"mime": ["mime@1.6.0", "", { "bin": { "mime": "cli.js" } }, "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg=="],
|
||||
|
||||
"min-document": ["min-document@2.19.2", "", { "dependencies": { "dom-walk": "^0.1.0" } }, "sha512-8S5I8db/uZN8r9HSLFVWPdJCvYOejMcEC82VIzNUc6Zkklf/d1gg2psfE79/vyhWOj4+J8MtwmoOz3TmvaGu5A=="],
|
||||
|
||||
"nice-try": ["nice-try@1.0.5", "", {}, "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ=="],
|
||||
|
||||
"node-abort-controller": ["node-abort-controller@3.1.1", "", {}, "sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ=="],
|
||||
|
||||
"node-addon-api": ["node-addon-api@5.0.0", "", {}, "sha512-CvkDw2OEnme7ybCykJpVcKH+uAOLV2qLqiyla128dN9TkEWfrYmxG6C2boDe5KcNQqZF3orkqzGgOMvZ/JNekA=="],
|
||||
|
||||
"node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="],
|
||||
|
||||
"npm-run-path": ["npm-run-path@2.0.2", "", { "dependencies": { "path-key": "^2.0.0" } }, "sha512-lJxZYlT4DW/bRUtFh1MQIWqmLwQfAxnqWG4HhEdjMlkrJYnJn0Jrr2u3mgxqaWsdiBc76TYkTG/mhrnYTuzfHw=="],
|
||||
|
||||
"omggif": ["omggif@1.0.10", "", {}, "sha512-LMJTtvgc/nugXj0Vcrrs68Mn2D1r0zf630VNtqtpI1FEO7e+O9FP4gqs9AcnBaSEeoHIPm28u6qgPR0oyEpGSw=="],
|
||||
|
||||
"once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="],
|
||||
|
||||
"p-finally": ["p-finally@1.0.0", "", {}, "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow=="],
|
||||
|
||||
"pako": ["pako@1.0.11", "", {}, "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="],
|
||||
|
||||
"parse-bmfont-ascii": ["parse-bmfont-ascii@1.0.6", "", {}, "sha512-U4RrVsUFCleIOBsIGYOMKjn9PavsGOXxbvYGtMOEfnId0SVNsgehXh1DxUdVPLoxd5mvcEtvmKs2Mmf0Mpa1ZA=="],
|
||||
|
||||
"parse-bmfont-binary": ["parse-bmfont-binary@1.0.6", "", {}, "sha512-GxmsRea0wdGdYthjuUeWTMWPqm2+FAd4GI8vCvhgJsFnoGhTrLhXDDupwTo7rXVAgaLIGoVHDZS9p/5XbSqeWA=="],
|
||||
|
||||
"parse-bmfont-xml": ["parse-bmfont-xml@1.1.6", "", { "dependencies": { "xml-parse-from-string": "^1.0.0", "xml2js": "^0.5.0" } }, "sha512-0cEliVMZEhrFDwMh4SxIyVJpqYoOWDJ9P895tFuS+XuNzI5UBmBk5U5O4KuJdTnZpSBI4LFA2+ZiJaiwfSwlMA=="],
|
||||
|
||||
"parse-headers": ["parse-headers@2.0.6", "", {}, "sha512-Tz11t3uKztEW5FEVZnj1ox8GKblWn+PvHY9TmJV5Mll2uHEwRdR/5Li1OlXoECjLYkApdhWy44ocONwXLiKO5A=="],
|
||||
|
||||
"path-key": ["path-key@2.0.1", "", {}, "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw=="],
|
||||
|
||||
"peek-readable": ["peek-readable@4.1.0", "", {}, "sha512-ZI3LnwUv5nOGbQzD9c2iDG6toheuXSZP5esSHBjopsXH4dg19soufvpUGA3uohi5anFtGb2lhAVdHzH6R/Evvg=="],
|
||||
|
||||
"phin": ["phin@3.7.1", "", { "dependencies": { "centra": "^2.7.0" } }, "sha512-GEazpTWwTZaEQ9RhL7Nyz0WwqilbqgLahDM3D0hxWwmVDI52nXEybHqiN6/elwpkJBhcuj+WbBu+QfT0uhPGfQ=="],
|
||||
|
||||
"pixelmatch": ["pixelmatch@4.0.2", "", { "dependencies": { "pngjs": "^3.0.0" }, "bin": { "pixelmatch": "bin/pixelmatch" } }, "sha512-J8B6xqiO37sU/gkcMglv6h5Jbd9xNER7aHzpfRdNmV4IbQBzBpe4l9XmbG+xPF/znacgu2jfEw+wHffaq/YkXA=="],
|
||||
|
||||
"pngjs": ["pngjs@6.0.0", "", {}, "sha512-TRzzuFRRmEoSW/p1KVAmiOgPco2Irlah+bGFCeNfJXxxYGwSw7YwAOAcd7X28K/m5bjBWKsC29KyoMfHbypayg=="],
|
||||
|
||||
"process": ["process@0.11.10", "", {}, "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A=="],
|
||||
|
||||
"pump": ["pump@3.0.4", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA=="],
|
||||
|
||||
"readable-stream": ["readable-stream@4.7.0", "", { "dependencies": { "abort-controller": "^3.0.0", "buffer": "^6.0.3", "events": "^3.3.0", "process": "^0.11.10", "string_decoder": "^1.3.0" } }, "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg=="],
|
||||
|
||||
"readable-web-to-node-stream": ["readable-web-to-node-stream@3.0.4", "", { "dependencies": { "readable-stream": "^4.7.0" } }, "sha512-9nX56alTf5bwXQ3ZDipHJhusu9NTQJ/CVPtb/XHAJCXihZeitfJvIRS4GqQ/mfIoOE3IelHMrpayVrosdHBuLw=="],
|
||||
|
||||
"regenerator-runtime": ["regenerator-runtime@0.13.11", "", {}, "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg=="],
|
||||
|
||||
"safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
|
||||
|
||||
"sax": ["sax@1.6.0", "", {}, "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA=="],
|
||||
|
||||
"semver": ["semver@5.7.2", "", { "bin": { "semver": "bin/semver" } }, "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g=="],
|
||||
|
||||
"shebang-command": ["shebang-command@1.2.0", "", { "dependencies": { "shebang-regex": "^1.0.0" } }, "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg=="],
|
||||
|
||||
"shebang-regex": ["shebang-regex@1.0.0", "", {}, "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ=="],
|
||||
|
||||
"signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="],
|
||||
|
||||
"string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="],
|
||||
|
||||
"strip-eof": ["strip-eof@1.0.0", "", {}, "sha512-7FCwGGmx8mD5xQd3RPUvnSpUXHM3BWuzjtpD4TXsfcZ9EL4azvVVUscFYwD9nx8Kh+uCBC00XBtAykoMHwTh8Q=="],
|
||||
|
||||
"strtok3": ["strtok3@6.3.0", "", { "dependencies": { "@tokenizer/token": "^0.3.0", "peek-readable": "^4.1.0" } }, "sha512-fZtbhtvI9I48xDSywd/somNqgUHl2L2cstmXCCif0itOf96jeW18MBSyrLuNicYQVkvpOxkZtkzujiTJ9LW5Jw=="],
|
||||
|
||||
"timm": ["timm@1.7.1", "", {}, "sha512-IjZc9KIotudix8bMaBW6QvMuq64BrJWFs1+4V0lXwWGQZwH+LnX87doAYhem4caOEusRP9/g6jVDQmZ8XOk1nw=="],
|
||||
|
||||
"tinycolor2": ["tinycolor2@1.6.0", "", {}, "sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw=="],
|
||||
|
||||
"token-types": ["token-types@4.2.1", "", { "dependencies": { "@tokenizer/token": "^0.3.0", "ieee754": "^1.2.1" } }, "sha512-6udB24Q737UD/SDsKAHI9FCRP7Bqc9D/MQUV02ORQg5iskjtLJlZJNdN4kKtcdtwCeWIwIHDGaUsTsCCAa8sFQ=="],
|
||||
|
||||
"tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="],
|
||||
|
||||
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
||||
|
||||
"undici-types": ["undici-types@7.24.6", "", {}, "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg=="],
|
||||
|
||||
"utif2": ["utif2@4.1.0", "", { "dependencies": { "pako": "^1.0.11" } }, "sha512-+oknB9FHrJ7oW7A2WZYajOcv4FcDR4CfoGB0dPNfxbi4GO05RRnFmt5oa23+9w32EanrYcSJWspUiJkLMs+37w=="],
|
||||
|
||||
"webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="],
|
||||
|
||||
"whatwg-fetch": ["whatwg-fetch@3.6.20", "", {}, "sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg=="],
|
||||
|
||||
"whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="],
|
||||
|
||||
"which": ["which@1.3.1", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "which": "./bin/which" } }, "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ=="],
|
||||
|
||||
"wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="],
|
||||
|
||||
"xhr": ["xhr@2.6.0", "", { "dependencies": { "global": "~4.4.0", "is-function": "^1.0.1", "parse-headers": "^2.0.0", "xtend": "^4.0.0" } }, "sha512-/eCGLb5rxjx5e3mF1A7s+pLlR6CGyqWN91fv1JgER5mVWg1MZmlhBvy9kjcsOdRk8RrIujotWyJamfyrp+WIcA=="],
|
||||
|
||||
"xml-parse-from-string": ["xml-parse-from-string@1.0.1", "", {}, "sha512-ErcKwJTF54uRzzNMXq2X5sMIy88zJvfN2DmdoQvy7PAFJ+tPRU6ydWuOKNMyfmOjdyBQTFREi60s0Y0SyI0G0g=="],
|
||||
|
||||
"xml2js": ["xml2js@0.5.0", "", { "dependencies": { "sax": ">=0.6.0", "xmlbuilder": "~11.0.0" } }, "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA=="],
|
||||
|
||||
"xmlbuilder": ["xmlbuilder@11.0.1", "", {}, "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA=="],
|
||||
|
||||
"xtend": ["xtend@4.0.2", "", {}, "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="],
|
||||
|
||||
"image-q/@types/node": ["@types/node@16.9.1", "", {}, "sha512-QpLcX9ZSsq3YYUUnD3nFDY8H7wctAhQj/TFKL8Ya8v5fMm3CFXxo8zStsLAl780ltoYoo1WvKUVGBQK+1ifr7g=="],
|
||||
|
||||
"pixelmatch/pngjs": ["pngjs@3.4.0", "", {}, "sha512-NCrCHhWmnQklfH4MtJMRjZ2a8c80qXeMlQMv2uVp9ISJMTt562SbGd6n2oq0PaPgKm7Z6pL9E2UlLIhC+SHL3w=="],
|
||||
|
||||
"readable-stream/buffer": ["buffer@6.0.3", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" } }, "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA=="],
|
||||
}
|
||||
}
|
||||
Executable
+41
@@ -0,0 +1,41 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# install.sh - bootstrap the Teams Status Keeper project.
|
||||
#
|
||||
# Verifies Bun is installed, then runs `bun install` to fetch dependencies
|
||||
# (notably `@nut-tree-fork/nut-js`, which ships prebuilt native binaries
|
||||
# for darwin-arm64/x64, linux, and windows).
|
||||
#
|
||||
# This script is idempotent: re-running it just re-resolves the dependency
|
||||
# tree against the existing `bun.lock`.
|
||||
|
||||
# Fail fast on any error, unset variable, or failed pipe stage.
|
||||
set -euo pipefail
|
||||
|
||||
# Always operate relative to the script's own directory so the install works
|
||||
# regardless of the caller's CWD.
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
echo "==> Checking for Bun..."
|
||||
if ! command -v bun >/dev/null 2>&1; then
|
||||
# Hard error rather than auto-install: keep the script's behavior
|
||||
# predictable and let the user pick their own install method.
|
||||
echo "Error: bun is not installed." >&2
|
||||
echo " Install it from https://bun.sh (e.g. 'brew install oven-sh/bun/bun') and re-run." >&2
|
||||
exit 1
|
||||
fi
|
||||
echo " bun $(bun --version)"
|
||||
|
||||
echo "==> Installing dependencies..."
|
||||
bun install
|
||||
|
||||
echo
|
||||
echo "Done. Run with: bun run start"
|
||||
|
||||
# macOS gates synthetic mouse events behind the Accessibility permission.
|
||||
# Without this hint, the first run silently fails to move the cursor and
|
||||
# the user has no obvious next step.
|
||||
if [[ "$(uname)" == "Darwin" ]]; then
|
||||
echo "Note: macOS will prompt for Accessibility permission on first mouse move."
|
||||
echo " Grant it under System Settings > Privacy & Security > Accessibility."
|
||||
fi
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"name": "move",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"license": "GPL-3.0-only",
|
||||
"type": "module",
|
||||
"bin": {
|
||||
"move": "./src/move.ts"
|
||||
},
|
||||
"engines": {
|
||||
"bun": ">=1.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "bun run src/move.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@nut-tree-fork/nut-js": "^4.2.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "latest",
|
||||
"typescript": "^5"
|
||||
}
|
||||
}
|
||||
+153
@@ -0,0 +1,153 @@
|
||||
/**
|
||||
* cli.ts
|
||||
* ------
|
||||
* Command-line argument parsing, validation, and help/version output for
|
||||
* the `move` CLI. Pure module: no side effects on import beyond reading
|
||||
* `package.json` once to populate `VERSION`.
|
||||
*
|
||||
* Flag table:
|
||||
*
|
||||
* -h, --help Prints `printHelp()` to stdout; entry exits 0.
|
||||
* -v, --version Prints `move <VERSION>` to stdout; entry exits 0.
|
||||
* -m, --move-interval Idle time (seconds) before a sweep fires.
|
||||
* -c, --check-interval Cursor poll cadence (seconds).
|
||||
* -d, --step-delay Pause between synthetic steps (ms).
|
||||
* -n, --step-count Steps per sweep (pixels).
|
||||
* -V, --verbose Enable per-sweep / interrupt / bounds logging.
|
||||
* (`-V` capital because `-v` is `--version`.)
|
||||
*
|
||||
* Numeric overrides are layered onto `DEFAULT_CONFIG` by
|
||||
* `resolveConfig` in `config.ts`; this module only parses and validates.
|
||||
*/
|
||||
|
||||
import { readFileSync } from "node:fs";
|
||||
import { dirname, join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { parseArgs } from "node:util";
|
||||
|
||||
import { DEFAULT_CONFIG } from "./config.ts";
|
||||
|
||||
/**
|
||||
* Thrown when CLI input is invalid (unknown option, missing value, bad number).
|
||||
* Distinct from runtime errors so the top-level entry can exit with code 2
|
||||
* (user error) instead of code 1 (runtime failure).
|
||||
*/
|
||||
export class CliError extends Error {}
|
||||
|
||||
/**
|
||||
* Result of `parseCliArgs`. Numeric fields are `undefined` when the user did
|
||||
* not supply the flag; this lets `resolveConfig` cleanly distinguish "use the
|
||||
* default" from "explicit override".
|
||||
*/
|
||||
export interface ParsedCliArgs {
|
||||
help: boolean;
|
||||
version: boolean;
|
||||
moveInterval: number | undefined; // seconds
|
||||
checkInterval: number | undefined; // seconds
|
||||
stepDelay: number | undefined; // milliseconds
|
||||
stepCount: number | undefined; // pixels
|
||||
verbose: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate a CLI-supplied numeric value. Returns `undefined` if the user did
|
||||
* not supply the flag at all; throws `CliError` on anything that isn't a
|
||||
* positive finite number. Zero is rejected: every numeric tunable here is a
|
||||
* duration or count where zero is meaningless or actively broken.
|
||||
*/
|
||||
function parsePositiveNumber(name: string, raw: string | undefined): number | undefined {
|
||||
if (raw === undefined) return undefined;
|
||||
const n: number = Number(raw);
|
||||
if (!Number.isFinite(n) || n <= 0) {
|
||||
throw new CliError(`invalid value for --${name}: '${raw}' (expected a positive number)`);
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse `process.argv` into a typed `ParsedCliArgs`. Uses Node's built-in
|
||||
* `parseArgs` in strict mode so unknown flags and missing values surface as
|
||||
* `CliError`s that the entry point can turn into exit code 2.
|
||||
*
|
||||
* Numeric flags are stored as `string` by `parseArgs` and then validated by
|
||||
* `parsePositiveNumber`.
|
||||
*/
|
||||
export function parseCliArgs(): ParsedCliArgs {
|
||||
let values: Record<string, string | boolean | undefined>;
|
||||
try {
|
||||
const result = parseArgs({
|
||||
args: process.argv.slice(2),
|
||||
options: {
|
||||
help: { type: "boolean", short: "h" },
|
||||
version: { type: "boolean", short: "v" },
|
||||
"move-interval": { type: "string", short: "m" },
|
||||
"check-interval": { type: "string", short: "c" },
|
||||
"step-delay": { type: "string", short: "d" },
|
||||
"step-count": { type: "string", short: "n" },
|
||||
verbose: { type: "boolean", short: "V" },
|
||||
},
|
||||
strict: true,
|
||||
allowPositionals: false,
|
||||
});
|
||||
values = result.values as Record<string, string | boolean | undefined>;
|
||||
} catch (err: unknown) {
|
||||
// node:util throws TypeError for unknown options / missing values;
|
||||
// surface its message verbatim so the user sees exactly what was wrong.
|
||||
const msg: string = err instanceof Error ? err.message : String(err);
|
||||
throw new CliError(msg);
|
||||
}
|
||||
|
||||
return {
|
||||
help: Boolean(values.help),
|
||||
version: Boolean(values.version),
|
||||
moveInterval: parsePositiveNumber("move-interval", values["move-interval"] as string | undefined),
|
||||
checkInterval: parsePositiveNumber("check-interval", values["check-interval"] as string | undefined),
|
||||
stepDelay: parsePositiveNumber("step-delay", values["step-delay"] as string | undefined),
|
||||
stepCount: parsePositiveNumber("step-count", values["step-count"] as string | undefined),
|
||||
verbose: Boolean(values.verbose),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the package version from `package.json` at runtime so help/version
|
||||
* output stays in sync with the manifest without a build step. Resolved
|
||||
* relative to this module's own location so the lookup works regardless of
|
||||
* the caller's CWD.
|
||||
*/
|
||||
export const VERSION: string = (() => {
|
||||
// This module lives in `src/`, so `package.json` is one directory up.
|
||||
const here: string = dirname(fileURLToPath(import.meta.url));
|
||||
const pkg = JSON.parse(readFileSync(join(here, "..", "package.json"), "utf-8")) as { version: string };
|
||||
return pkg.version;
|
||||
})();
|
||||
|
||||
/**
|
||||
* Write the usage block to stdout. Default values are pulled from
|
||||
* `DEFAULT_CONFIG` (converted to the units the CLI exposes) so the help
|
||||
* text never drifts from the actual defaults.
|
||||
*/
|
||||
export function printHelp(): void {
|
||||
const moveDefaultSec: number = DEFAULT_CONFIG.moveInterval / 1000;
|
||||
const checkDefaultSec: number = DEFAULT_CONFIG.checkInterval / 1000;
|
||||
|
||||
process.stdout.write(`Usage: move [options]
|
||||
|
||||
Keeps presence-tracking apps (e.g. Microsoft Teams) from going Away by
|
||||
nudging the mouse cursor after a configurable idle period.
|
||||
|
||||
Options:
|
||||
-h, --help Show this help and exit.
|
||||
-v, --version Print version and exit.
|
||||
-m, --move-interval <seconds> Idle time before a sweep fires. Default: ${moveDefaultSec}.
|
||||
-c, --check-interval <seconds> Cursor poll cadence. Default: ${checkDefaultSec}.
|
||||
-d, --step-delay <ms> Pause between synthetic steps. Default: ${DEFAULT_CONFIG.stepDelay}.
|
||||
-n, --step-count <pixels> Steps per sweep. Default: ${DEFAULT_CONFIG.stepCount}.
|
||||
-V, --verbose Log every sweep, interrupt, and bounds event
|
||||
(default prints only the startup banner).
|
||||
|
||||
Examples:
|
||||
move
|
||||
move --move-interval 180 --check-interval 5
|
||||
move -m 300 -V
|
||||
`);
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
/**
|
||||
* config.ts
|
||||
* ---------
|
||||
* Runtime configuration types, defaults, and the CLI->config resolver.
|
||||
*
|
||||
* The keeper is driven by a single `Config` object that carries the four
|
||||
* tunables it cares about. Defaults live in `DEFAULT_CONFIG`; CLI overrides
|
||||
* are layered on top by `resolveConfig` rather than mutating the defaults,
|
||||
* so the defaults stay genuinely constant and the resolved config stays
|
||||
* structurally typed.
|
||||
*
|
||||
* All fields are in their internal units (ms, pixels). The CLI exposes the
|
||||
* time-valued fields in seconds for ergonomics; `resolveConfig` performs
|
||||
* the seconds->ms conversion at the boundary so downstream code never has
|
||||
* to think about it.
|
||||
*/
|
||||
|
||||
/**
|
||||
* The shape of a resolved runtime configuration. `readonly` to make
|
||||
* accidental mutation a type error.
|
||||
*
|
||||
* - `moveInterval` — how long the mouse must be idle (no real movement)
|
||||
* before a synthetic sweep is triggered. Milliseconds.
|
||||
* - `checkInterval` — cadence of the idleness poll in the main loop.
|
||||
* Milliseconds.
|
||||
* - `stepDelay` — pause between individual synthetic mouse steps inside
|
||||
* a sweep. Also the window in which the user can
|
||||
* "interrupt" by moving the cursor. Milliseconds.
|
||||
* - `stepCount` — number of pixel-steps in a single sweep. Pixels.
|
||||
*/
|
||||
export interface Config {
|
||||
readonly moveInterval: number;
|
||||
readonly checkInterval: number;
|
||||
readonly stepDelay: number;
|
||||
readonly stepCount: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Built-in defaults used when the user did not supply an explicit CLI
|
||||
* override for the corresponding flag.
|
||||
*/
|
||||
export const DEFAULT_CONFIG: Config = {
|
||||
moveInterval: 4 * 60 * 1000, // 4 minutes in milliseconds
|
||||
checkInterval: 10 * 1000, // Check every 10 seconds
|
||||
stepDelay: 50, // ms between mouse steps
|
||||
stepCount: 250, // pixels to move per sweep
|
||||
};
|
||||
|
||||
/**
|
||||
* Subset of `ParsedCliArgs` that `resolveConfig` actually consumes. Declared
|
||||
* locally instead of importing from `cli.ts` to keep the dependency arrow
|
||||
* pointing one way (cli -> config), which lets `config.ts` stay a leaf
|
||||
* module with no internal imports.
|
||||
*/
|
||||
export interface ConfigOverrides {
|
||||
readonly moveInterval: number | undefined; // seconds (CLI units)
|
||||
readonly checkInterval: number | undefined; // seconds (CLI units)
|
||||
readonly stepDelay: number | undefined; // milliseconds
|
||||
readonly stepCount: number | undefined; // pixels
|
||||
}
|
||||
|
||||
/**
|
||||
* Overlay user-supplied CLI values on top of `DEFAULT_CONFIG` and return a
|
||||
* resolved `Config`. Time-valued CLI inputs (move/check interval) are in
|
||||
* seconds; this is where they're converted to milliseconds for internal use.
|
||||
*
|
||||
* Any field left `undefined` in the overrides falls back to the default.
|
||||
*/
|
||||
export function resolveConfig(overrides: ConfigOverrides): Config {
|
||||
return {
|
||||
moveInterval: overrides.moveInterval !== undefined ? overrides.moveInterval * 1000 : DEFAULT_CONFIG.moveInterval,
|
||||
checkInterval: overrides.checkInterval !== undefined ? overrides.checkInterval * 1000 : DEFAULT_CONFIG.checkInterval,
|
||||
stepDelay: overrides.stepDelay ?? DEFAULT_CONFIG.stepDelay,
|
||||
stepCount: overrides.stepCount ?? DEFAULT_CONFIG.stepCount,
|
||||
};
|
||||
}
|
||||
+175
@@ -0,0 +1,175 @@
|
||||
/**
|
||||
* keeper.ts
|
||||
* ---------
|
||||
* The actual "Teams Status Keeper" behavior: synthetic mouse activity with
|
||||
* real-user-wins semantics, plus the idle-watch loop that drives it.
|
||||
*
|
||||
* Runtime: Bun (uses `@nut-tree-fork/nut-js` for cross-platform mouse +
|
||||
* screen). Importing this module sets `mouse.config.autoDelayMs = 0` as a
|
||||
* side effect — see below.
|
||||
*
|
||||
* Logging policy:
|
||||
* - The startup banner in `runKeeper` is unconditional so the user always
|
||||
* sees confirmation that the process is alive.
|
||||
* - Every per-sweep / interrupt / bounds log is gated by `verbose` so the
|
||||
* default is quiet. Errors stay on `console.error` (unconditional, raised
|
||||
* by the entry point on unhandled rejection).
|
||||
*/
|
||||
|
||||
import { mouse, Point, screen } from "@nut-tree-fork/nut-js";
|
||||
|
||||
import type { Config } from "./config.ts";
|
||||
|
||||
/**
|
||||
* nut.js inserts a configurable delay after every action (default 100ms).
|
||||
* That default would silently more-than-double the duration of every
|
||||
* `setPosition` and `getPosition` call. We drive cadence ourselves via
|
||||
* `Config.stepDelay`, so disable nut.js's implicit delay entirely.
|
||||
*/
|
||||
mouse.config.autoDelayMs = 0;
|
||||
|
||||
/**
|
||||
* Promise-based `setTimeout` wrapper. Allows `await sleep(ms)` ergonomics.
|
||||
*
|
||||
* @param ms - Duration to wait, in milliseconds.
|
||||
*/
|
||||
const sleep = (ms: number): Promise<void> =>
|
||||
new Promise<void>((resolve: () => void): void => {
|
||||
setTimeout(resolve, ms);
|
||||
});
|
||||
|
||||
/**
|
||||
* Format the current local time as `HH:MM:SS` (24-hour, zero-padded).
|
||||
* Used for human-readable log lines. Date is intentionally omitted.
|
||||
*/
|
||||
const timestamp = (): string => {
|
||||
const d: Date = new Date();
|
||||
const pad = (n: number): string => String(n).padStart(2, "0");
|
||||
return `${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Build a verbose-gated logger. `info` is unconditional; `event` only fires
|
||||
* when the caller asked for verbose output. Returning a small object keeps
|
||||
* `simulateActivity` free of `if (verbose)` noise at every log site.
|
||||
*/
|
||||
function makeLogger(verbose: boolean): { info(msg: string): void; event(msg: string): void } {
|
||||
return {
|
||||
info: (msg: string): void => {
|
||||
console.log(msg);
|
||||
},
|
||||
event: (msg: string): void => {
|
||||
if (verbose) console.log(msg);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform a single synthetic mouse-activity sweep.
|
||||
*
|
||||
* Behavior:
|
||||
* 1. Snapshot the starting cursor position.
|
||||
* 2. Read current screen dimensions (re-read every call so monitor changes
|
||||
* are handled correctly).
|
||||
* 3. Pick a horizontal direction (`dx`) that keeps the sweep on-screen:
|
||||
* move right if there's room, otherwise move left. Vertical movement is
|
||||
* currently disabled (`dy = 0`) but the framework is in place for
|
||||
* richer patterns later.
|
||||
* 4. For each of `config.stepCount` steps:
|
||||
* - Compute the next target position.
|
||||
* - Defensive bounds check (belt-and-braces given the `dx` choice).
|
||||
* - Command nut.js to move the cursor there.
|
||||
* - Sleep `config.stepDelay` — also the user's interrupt window.
|
||||
* - Re-read the cursor. If it isn't where we put it, the user
|
||||
* touched the mouse: log (verbose) and return early, leaving the
|
||||
* cursor wherever the user moved it.
|
||||
* 5. On a clean full sweep, restore the cursor to the starting position
|
||||
* so the next idle-check sees "no movement" and doesn't misread the
|
||||
* synthetic activity as the user returning.
|
||||
*/
|
||||
async function simulateActivity(config: Config, log: ReturnType<typeof makeLogger>): Promise<void> {
|
||||
const start: Point = await mouse.getPosition();
|
||||
const screenWidth: number = await screen.width();
|
||||
const screenHeight: number = await screen.height();
|
||||
const dx: number = start.x + config.stepCount < screenWidth ? 1 : -1;
|
||||
const dy: number = 0;
|
||||
|
||||
log.event(`Simulating activity at ${timestamp()}...`);
|
||||
|
||||
for (let i: number = 1; i <= config.stepCount; i++) {
|
||||
const expected: Point = new Point(start.x + i * dx, start.y + i * dy);
|
||||
|
||||
if (expected.x < 0 || expected.x >= screenWidth || expected.y < 0 || expected.y >= screenHeight) {
|
||||
// Safety net for future non-linear movement patterns. With the
|
||||
// current straight-line sweep + `dx` selection above, this branch
|
||||
// should never fire.
|
||||
log.event(`Out of bounds at ${timestamp()}; aborting simulation.`);
|
||||
return;
|
||||
}
|
||||
|
||||
await mouse.setPosition(expected);
|
||||
await sleep(config.stepDelay);
|
||||
|
||||
const current: Point = await mouse.getPosition();
|
||||
if (current.x !== expected.x || current.y !== expected.y) {
|
||||
// Cursor isn't where we put it -> real user activity. Abort
|
||||
// without snapping back, so we don't yank the cursor out from
|
||||
// under the user.
|
||||
log.event(`User activity detected at ${timestamp()}; aborting simulation.`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
await mouse.setPosition(start);
|
||||
log.event("Mouse moved.");
|
||||
}
|
||||
|
||||
/**
|
||||
* Main idle-watch loop. Runs forever; exits only on `Ctrl+C` (SIGINT) or
|
||||
* an unhandled rejection caught by the entry point.
|
||||
*
|
||||
* Algorithm:
|
||||
* - Track the last known cursor position (`lastPos`) and the timestamp of
|
||||
* the last observed real user movement (`lastActivity`).
|
||||
* - Every `config.checkInterval`:
|
||||
* * If the cursor moved since the last check, that's real user
|
||||
* activity: reset `lastActivity` and `lastPos`, skip the rest.
|
||||
* * Otherwise, if it's been at least `config.moveInterval` since the
|
||||
* last real activity, fire a synthetic sweep, then reset the
|
||||
* idleness clock so we wait another full `moveInterval` before
|
||||
* firing again.
|
||||
*
|
||||
* `simulateActivity` is designed so that its own synthetic movement never
|
||||
* counts as real activity: on a clean sweep it restores the cursor (so the
|
||||
* next position check matches), and on a user-interrupted sweep the next
|
||||
* iteration sees the user's new position and correctly resets the clock.
|
||||
*/
|
||||
export async function runKeeper(config: Config, verbose: boolean): Promise<void> {
|
||||
const log = makeLogger(verbose);
|
||||
log.info("Teams Status Keeper started. Press Ctrl+C to stop.");
|
||||
|
||||
let lastPos: Point = await mouse.getPosition();
|
||||
let lastActivity: number = Date.now();
|
||||
|
||||
while (true) {
|
||||
await sleep(config.checkInterval);
|
||||
const pos: Point = await mouse.getPosition();
|
||||
const now: number = Date.now();
|
||||
|
||||
if (pos.x !== lastPos.x || pos.y !== lastPos.y) {
|
||||
// Real user activity since the last check; reset the idleness clock.
|
||||
lastActivity = now;
|
||||
lastPos = pos;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (now - lastActivity >= config.moveInterval) {
|
||||
await simulateActivity(config, log);
|
||||
// `simulateActivity` either returns the cursor to its start
|
||||
// (clean sweep) or leaves it where the user moved it (interrupt).
|
||||
// Either way we reset the clock and require another full
|
||||
// moveInterval of inactivity before firing again.
|
||||
lastActivity = Date.now();
|
||||
}
|
||||
}
|
||||
}
|
||||
Executable
+57
@@ -0,0 +1,57 @@
|
||||
#!/usr/bin/env bun
|
||||
/**
|
||||
* move.ts
|
||||
* -------
|
||||
* Entry point for the `move` CLI.
|
||||
*
|
||||
* Thin shim that ties the three logic modules together:
|
||||
* - `cli.ts` parses and validates `process.argv`.
|
||||
* - `config.ts` holds the default tunables and the `resolveConfig` overlay.
|
||||
* - `keeper.ts` owns the synthetic-activity sweep and idle-watch loop.
|
||||
*
|
||||
* Order of operations:
|
||||
* 1. Parse CLI args. Bad input -> stderr message + short usage hint, exit 2.
|
||||
* 2. `--help` / `--version` short-circuit before any mouse work happens.
|
||||
* 3. Resolve CLI overrides on top of `DEFAULT_CONFIG` and hand the result
|
||||
* (plus the verbose flag) to `runKeeper`.
|
||||
* 4. Any unhandled rejection from `runKeeper` is logged and exits with
|
||||
* code 1 so it's catchable by shells / supervisors.
|
||||
*
|
||||
* Runtime: Bun (uses `@nut-tree-fork/nut-js` via `keeper.ts`). The shebang
|
||||
* above lets this file run as a real CLI once linked via `bun link`.
|
||||
*/
|
||||
|
||||
import { parseCliArgs, printHelp, VERSION } from "./cli.ts";
|
||||
import type { ParsedCliArgs } from "./cli.ts";
|
||||
import { resolveConfig } from "./config.ts";
|
||||
import { runKeeper } from "./keeper.ts";
|
||||
|
||||
let cliArgs: ParsedCliArgs;
|
||||
try {
|
||||
cliArgs = parseCliArgs();
|
||||
} catch (err: unknown) {
|
||||
const msg: string = err instanceof Error ? err.message : String(err);
|
||||
process.stderr.write(`move: ${msg}\nTry 'move --help' for more information.\n`);
|
||||
process.exit(2);
|
||||
}
|
||||
|
||||
if (cliArgs.help) {
|
||||
printHelp();
|
||||
process.exit(0);
|
||||
}
|
||||
if (cliArgs.version) {
|
||||
process.stdout.write(`move ${VERSION}\n`);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const config = resolveConfig({
|
||||
moveInterval: cliArgs.moveInterval,
|
||||
checkInterval: cliArgs.checkInterval,
|
||||
stepDelay: cliArgs.stepDelay,
|
||||
stepCount: cliArgs.stepCount,
|
||||
});
|
||||
|
||||
runKeeper(config, cliArgs.verbose).catch((err: unknown): void => {
|
||||
console.error("Error:", err);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"noEmit": true,
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true
|
||||
},
|
||||
"include": ["src/**/*.ts"]
|
||||
}
|
||||
Reference in New Issue
Block a user