Skip to content

Commit 9b3842c

Browse files
mschmickingclaude
andcommitted
chore: add eslint and prettier, automate versioning from PR titles
ESLint uses the type-aware strict ruleset, because the rules that matter for a tool doing async I/O against a live house — floating promises, misused promises — cannot be detected without type information. Two rules encode existing invariants instead of taste: - no-console is an error everywhere in src/ except cli.ts, which owns stdout/stderr. This immediately caught three console.warn calls in sync/manifest.ts that bypassed ctx.log entirely: they could not be captured by tests and ignored --json. loadManifest now takes a warn callback and every caller threads ctx.log.warn through. - no-floating-promises is an error. node:test's describe/it are declared safe via allowForKnownSafeCalls rather than disabling the rule in tests, which would have hidden real cases. Three strictTypeChecked rules are off with reasoning recorded in the config. The significant one is no-unnecessary-condition: socket.emit<T>() returns T, but that type is a claim about what the server should send rather than a guarantee, so guards like `result?.rows ?? []` look redundant to the rule and are essential at runtime. prefer-nullish-coalescing exempts strings because ioBroker represents "unset" as an empty string, making `||` the correct operator and `??` a silent bug. Fixes found while clearing the remaining reports: dead imports in new.ts, pull.ts, list.ts and cli.ts; non-null assertions replaced with narrowing; and WebSocket RawData stringified properly — the array-of-Buffers form produced "[object Object]" and would have failed to parse for a reason invisible in a stack trace. PR titles are validated as conventional commits, since the title becomes the squash commit and drives the version bump. release-please maintains a release PR rather than publishing automatically; npm publish stays manual. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent 0d2dc13 commit 9b3842c

59 files changed

Lines changed: 2520 additions & 507 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/ci.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,12 @@ jobs:
2727

2828
- run: npm ci
2929

30+
- name: Lint
31+
run: npm run lint
32+
33+
- name: Formatting
34+
run: npm run format:check
35+
3036
- name: Typecheck
3137
run: npx tsc -p tsconfig.json --noEmit
3238

.github/workflows/pr-title.yml

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
name: PR title
2+
3+
# The PR title becomes the squashed commit message, and release-please derives the
4+
# next version from it. A title that does not parse means a release that silently
5+
# does not happen, so it is validated before merge rather than discovered after.
6+
on:
7+
pull_request_target:
8+
types: [opened, edited, synchronize, reopened]
9+
10+
permissions:
11+
pull-requests: read
12+
13+
jobs:
14+
conventional-commit:
15+
runs-on: ubuntu-latest
16+
steps:
17+
- uses: amannn/action-semantic-pull-request@v5
18+
env:
19+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
20+
with:
21+
# feat -> minor, fix/perf -> patch, feat! or a BREAKING CHANGE footer -> major.
22+
# The rest are patch-or-nothing and never drive a release on their own.
23+
types: |
24+
feat
25+
fix
26+
perf
27+
refactor
28+
docs
29+
test
30+
build
31+
ci
32+
chore
33+
revert
34+
# Optional, but keep the vocabulary small so it stays meaningful.
35+
scopes: |
36+
sync
37+
auth
38+
watch
39+
logs
40+
json
41+
cli
42+
deps
43+
docs
44+
requireScope: false
45+
# Subject should read like a sentence fragment, lower-case, no full stop.
46+
subjectPattern: ^(?![A-Z])(?!.*\.$).+$
47+
subjectPatternError: |
48+
The subject "{subject}" is invalid: start lower-case and omit the trailing
49+
full stop, e.g. "feat(logs): stream the server log".
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
name: Release PR
2+
3+
# Maintains an open "chore(main): release x.y.z" pull request that accumulates every
4+
# merged change, derives the next version from the conventional-commit history, and
5+
# rewrites CHANGELOG.md. Nothing is versioned or tagged until that PR is merged.
6+
#
7+
# Chosen over fully automatic publishing on purpose: releasing is irreversible on npm,
8+
# so there is a human gate. Merging the release PR tags the commit; the actual
9+
# `npm publish` stays in release.yml, which is manual.
10+
on:
11+
push:
12+
branches: [main]
13+
14+
permissions:
15+
contents: write
16+
pull-requests: write
17+
18+
jobs:
19+
release-please:
20+
runs-on: ubuntu-latest
21+
steps:
22+
- uses: googleapis/release-please-action@v4
23+
with:
24+
release-type: node
25+
# Keeps the existing hand-written CHANGELOG entries intact by appending
26+
# below the header rather than replacing the file.
27+
changelog-notes-type: default

.prettierignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
dist/
2+
dist-test/
3+
node_modules/
4+
package-lock.json
5+
test/fixtures/*.pem

.prettierrc.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"printWidth": 100,
3+
"singleQuote": true,
4+
"trailingComma": "all",
5+
"semi": true,
6+
"arrowParens": "always"
7+
}

AGENTS.md

Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -30,13 +30,13 @@ instruction from the user:
3030
3. **Conflicts block.** If local and remote both changed, refuse and exit non-zero.
3131
4. **Deletion is explicit.** Only `remove`, `rename` and `move` delete anything, each
3232
requires `--yes`, and each writes the full object JSON to `.iobroker-sync/trash/`
33-
*before* deleting. A failed backup aborts the operation.
33+
_before_ deleting. A failed backup aborts the operation.
3434
5. **Copy-then-delete must verify first.** ioBroker has no native rename/move, so both
3535
are implemented as copy-verify-delete. The verification compares the actual source
3636
text. Checking only that "something exists at the new id" is not verification — a
3737
truncated copy would pass and the original would be destroyed.
3838
6. **`backup` is read-only and all-or-nothing.** It fetches objects and writes files,
39-
never mutating the server. It stores the *whole* object, not just the source, because
39+
never mutating the server. It stores the _whole_ object, not just the source, because
4040
`push` cannot write `common.enabled` or `common.engine` back (invariant 2) — so
4141
`objects/*.json` is the only record of those. A partial snapshot is worse than none,
4242
so any write failure aborts with a `UserError` rather than leaving a plausible-looking
@@ -111,7 +111,7 @@ infinite-loops on connection errors against this server.
111111
never be closed, and nothing would appear until exit.
112112
- User-facing failures are `UserError` (message + optional hint); `cli.ts` prints them
113113
without a stack trace and sets exit code 1.
114-
- Comments explain *why*, not *what*. The non-obvious protocol and safety reasoning is
114+
- Comments explain _why_, not _what_. The non-obvious protocol and safety reasoning is
115115
worth writing down; restating the code is not.
116116
- No new npm dependencies without a good reason. Current set: `ws`, `commander`,
117117
`chokidar`, `diff`.
@@ -138,7 +138,7 @@ and the commands `pull`, `push`, `status`, `diff` (including `--against`), `watc
138138
`sync/manifest` and `sync/scan` are covered indirectly by every pull/push test.
139139

140140
`test/cli.test.ts` spawns the built `dist/cli.js` and asserts on real argv handling.
141-
It exists because a duplicate `--password-stdin` declaration — global *and* on the
141+
It exists because a duplicate `--password-stdin` declaration — global _and_ on the
142142
`login` subcommand — was silently shadowed by commander, and no in-process test could
143143
see it. **Anything about option wiring, exit codes or stdout/stderr separation belongs
144144
there, and it needs `dist/` built first.**
@@ -178,7 +178,7 @@ The password must never reach the project directory, argv, or a log line:
178178
(override with `IOBROKER_SYNC_CREDENTIALS`, which every test does so the suite never
179179
reads the developer's real store), mode `0600` in a `0700` directory, written
180180
temp-then-rename so an interrupted write cannot truncate it.
181-
- `iob-sync login` verifies a password against the live instance *before* saving, so a
181+
- `iob-sync login` verifies a password against the live instance _before_ saving, so a
182182
typo fails now rather than on the next command.
183183
- Prompts require stdin **and** stdout to be TTYs, so a script, CI job or agent gets a
184184
`UserError` instead of a hang.
@@ -200,7 +200,7 @@ that can actually drive the watcher:
200200

201201
1. `watch()` returned before chokidar finished its initial scan, so any edit saved in
202202
that window was silently dropped. It now awaits the `ready` event.
203-
2. `lastPushedHash` was recorded *after* the server write completed, so an adapter
203+
2. `lastPushedHash` was recorded _after_ the server write completed, so an adapter
204204
echo arriving mid-flight escaped the echo guard and produced a spurious pull. The
205205
hash is now recorded before the write.
206206

@@ -237,3 +237,30 @@ The CLI is a tool; the scripts it syncs belong in their own repository. Do not a
237237
`scripts/` directory here — running `iob-sync init` inside this repo is what caused the
238238
`tsconfig.json` breakage above. Test commands against `test/fake-server.ts` and
239239
temporary project directories instead.
240+
241+
## Linting and formatting
242+
243+
```bash
244+
npm run lint # eslint, type-aware
245+
npm run format # prettier --write
246+
npm run verify # lint + format check + typecheck + tests
247+
```
248+
249+
Two rules encode invariants rather than taste, and must not be relaxed:
250+
251+
- **`no-console` is an error everywhere in `src/` except `cli.ts`.** Only the CLI layer
252+
owns stdout/stderr; everything else goes through `ctx.log`. This caught three
253+
`console.warn` calls in `sync/manifest.ts` that bypassed the logger entirely — they
254+
could not be captured by tests and ignored `--json`. `loadManifest` now takes a
255+
`warn` callback.
256+
- **`no-floating-promises` is an error.** An unawaited write to a live instance means the
257+
command reports success and exits with the request still in flight. `node:test`'s
258+
`describe`/`it` are declared safe via `allowForKnownSafeCalls` rather than switching
259+
the rule off in tests.
260+
261+
Three rules from `strictTypeChecked` are deliberately **off**, with the reasoning in
262+
`eslint.config.mjs`. The important one: `no-unnecessary-condition`. `socket.emit<T>()`
263+
returns `T`, but that type is a _claim_ about what the server should send, not a
264+
guarantee — so guards like `result?.rows ?? []` look redundant to the rule and are
265+
essential at runtime. Following it would trade a handled edge case for a crash against
266+
someone's house.

CHANGELOG.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ for the first release.
2121
than the server. `latest` resolves to the newest.
2222
- `login` / `logout` — save or remove the password for an instance. Verified against the
2323
live server before saving.
24-
- `backup` — read-only snapshot of every script's source *and* full object, since `push`
24+
- `backup` — read-only snapshot of every script's source _and_ full object, since `push`
2525
cannot restore `common.enabled` or `common.engine`.
2626
- Interactive `init`, which asks for URL, certificate handling, script root and username
2727
when run without flags, and adds `.iobroker-sync/` to `.gitignore`.
@@ -43,7 +43,7 @@ for the first release.
4343
- `watch` dropped edits saved before chokidar finished its initial scan. It now waits for
4444
the watcher to be ready before reporting that it is watching.
4545
- `watch` could re-pull its own push. The hash used to suppress the javascript adapter's
46-
`compiled`/`sourceHash` echo was recorded *after* the server write, so an echo arriving
46+
`compiled`/`sourceHash` echo was recorded _after_ the server write, so an echo arriving
4747
mid-write escaped the guard.
4848
- `login --password-stdin` always reported "no terminal available". The option was
4949
declared both globally and on the subcommand, and the parent silently shadowed the

README.md

Lines changed: 43 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ search across scripts, no editor of your choice, and no way to touch them from a
88
or a CI job. `iobroker-sync` maps them to a local folder and keeps the two in step.
99

1010
<!-- Screenshot to add before v1.0 — see docs/images/README.md for exactly what to capture. -->
11+
1112
![Two terminal panes side by side. On the left, a script is edited and iob-sync push reports it was uploaded. On the right, iob-sync logs is streaming the ioBroker log, where the script's own output appears a moment later.](docs/images/edit-loop.png)
1213

1314
## Quick start
@@ -41,13 +42,13 @@ That is the whole loop. `git init` in that folder and your home automation has h
4142

4243
The five commands worth knowing on day one:
4344

44-
| | |
45-
| --- | --- |
46-
| `iob-sync pull` | bring the server's scripts down |
47-
| `iob-sync status` | what differs, locally and remotely |
48-
| `iob-sync push` | send your edits up |
49-
| `iob-sync logs` | watch script output — **this is how you find out a push actually worked** |
50-
| `iob-sync backup` | snapshot everything before you touch something important |
45+
| | |
46+
| ----------------- | ------------------------------------------------------------------------- |
47+
| `iob-sync pull` | bring the server's scripts down |
48+
| `iob-sync status` | what differs, locally and remotely |
49+
| `iob-sync push` | send your edits up |
50+
| `iob-sync logs` | watch script output — **this is how you find out a push actually worked** |
51+
| `iob-sync backup` | snapshot everything before you touch something important |
5152

5253
## Contents
5354

@@ -84,24 +85,24 @@ The [ioBroker JavaScript VS Code extension](https://github.com/nokxs/iobroker-ja
8485
by nokxs solves the same core problem and inspired this tool. If you work in VS Code, it
8586
is genuinely good and does things a CLI cannot — go use it.
8687

87-
| | iobroker-sync | VS Code extension |
88-
| --- | --- | --- |
89-
| Editor | any, or none | VS Code |
90-
| State-ID autocompletion, hover values || yes |
91-
| Script tree, buttons, context menus | — (`iob-sync list`) | yes |
92-
| Works over SSH, in CI, headless | yes ||
93-
| Machine-readable output | yes ||
94-
| Scriptable from a shell or an agent | yes ||
88+
| | iobroker-sync | VS Code extension |
89+
| ------------------------------------- | ------------------- | ----------------- |
90+
| Editor | any, or none | VS Code |
91+
| State-ID autocompletion, hover values | | yes |
92+
| Script tree, buttons, context menus | — (`iob-sync list`) | yes |
93+
| Works over SSH, in CI, headless | yes | |
94+
| Machine-readable output | yes | |
95+
| Scriptable from a shell or an agent | yes | |
9596

9697
Rough rule: **if you live in VS Code, use the extension.** If you want your own editor, a
9798
git-first workflow, or anything automated, use this. They are not exclusive — both talk
9899
to the same Admin API, and this tool never writes fields it does not own.
99100

100101
## How scripts map to files
101102

102-
| ioBroker object | local file |
103-
| --- | --- |
104-
| `script.js.common.garage` (`TypeScript/ts`) | `scripts/common/garage.ts` |
103+
| ioBroker object | local file |
104+
| ---------------------------------------------- | ----------------------------- |
105+
| `script.js.common.garage` (`TypeScript/ts`) | `scripts/common/garage.ts` |
105106
| `script.js.Switch-Musiccast` (`Javascript/js`) | `scripts/Switch-Musiccast.js` |
106107

107108
Script folders are ioBroker `channel` objects; nested folders map to nested directories.
@@ -134,26 +135,26 @@ iob-sync logs --level error # only failures
134135

135136
**Sync**
136137

137-
| Command | Description |
138-
| --- | --- |
139-
| `init` | Write `.iobroker-sync.json`, verify the connection, create the script folder. Asks interactively when run without flags. `--types` also sets up TypeScript definitions. |
140-
| `login` / `logout` | Save or remove the password for this instance. Never stored in the project. |
141-
| `pull [pattern]` | Download scripts to disk. Never deletes local files. |
142-
| `push [pattern]` | Upload locally modified scripts. Never deletes remote objects. |
143-
| `status` | Show what changed, locally and remotely. |
144-
| `diff [pattern]` | Unified diff of local vs server. `--against <snapshot>` compares with a backup instead. |
145-
| `watch` | Push on save. `--pull` also applies remote changes. |
146-
| `logs [pattern]` | Stream the server log. `--level`, `--limit`. Read-only. |
147-
| `backup [pattern]` | Snapshot every script — source *and* full object — to `.iobroker-sync/backup/<timestamp>/`. Read-only against the server. |
138+
| Command | Description |
139+
| ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
140+
| `init` | Write `.iobroker-sync.json`, verify the connection, create the script folder. Asks interactively when run without flags. `--types` also sets up TypeScript definitions. |
141+
| `login` / `logout` | Save or remove the password for this instance. Never stored in the project. |
142+
| `pull [pattern]` | Download scripts to disk. Never deletes local files. |
143+
| `push [pattern]` | Upload locally modified scripts. Never deletes remote objects. |
144+
| `status` | Show what changed, locally and remotely. |
145+
| `diff [pattern]` | Unified diff of local vs server. `--against <snapshot>` compares with a backup instead. |
146+
| `watch` | Push on save. `--pull` also applies remote changes. |
147+
| `logs [pattern]` | Stream the server log. `--level`, `--limit`. Read-only. |
148+
| `backup [pattern]` | Snapshot every script — source _and_ full object — to `.iobroker-sync/backup/<timestamp>/`. Read-only against the server. |
148149

149150
**Lifecycle**
150151

151-
| Command | Description |
152-
| --- | --- |
153-
| `list` | All scripts with instance and enabled state. |
154-
| `start` / `stop` / `restart` `<pattern>` | Toggle `common.enabled`. |
155-
| `new <path>` | Create a new script (disabled) plus any missing folders. |
156-
| `rename` / `move` / `remove` | Destructive. Require `--yes` and back up the object first. `remove` keeps the local file unless `--delete-local`. |
152+
| Command | Description |
153+
| ---------------------------------------- | ----------------------------------------------------------------------------------------------------------------- |
154+
| `list` | All scripts with instance and enabled state. |
155+
| `start` / `stop` / `restart` `<pattern>` | Toggle `common.enabled`. |
156+
| `new <path>` | Create a new script (disabled) plus any missing folders. |
157+
| `rename` / `move` / `remove` | Destructive. Require `--yes` and back up the object first. `remove` keeps the local file unless `--delete-local`. |
157158

158159
`--dry-run`, `--verbose`, `--json` and `-C <dir>` are global options and work with every
159160
command. When in doubt, `--dry-run` shows what would happen and changes nothing.
@@ -172,7 +173,7 @@ destroying work:
172173
- **`pull` never deletes local files.** A script removed on the server shows up in `status`
173174
as `remote-missing`; what to do about it is your call.
174175
- **`push` never deletes remote objects**, and writes only `common.source` and
175-
`common.engineType`. It *cannot* disable a running script or move it to a different
176+
`common.engineType`. It _cannot_ disable a running script or move it to a different
176177
javascript instance, because it never sends those fields — a sync bug structurally
177178
cannot stop your heating.
178179
- **Conflicts block.** A manifest at `.iobroker-sync/state.json` records the source hash at
@@ -215,12 +216,12 @@ in a `0700` directory, outside your repo. `iob-sync logout` removes it.
215216
There is deliberately **no `--password` flag**: argv is visible to other local processes
216217
via `ps` and is recorded in shell history. The alternatives, in the order they are tried:
217218

218-
| Source | Use |
219-
| --- | --- |
220-
| `--password-stdin` | scripts and CI: `printf '%s' "$PW" \| iob-sync --password-stdin login` |
221-
| `IOBROKER_PASSWORD` | ad-hoc shells |
222-
| saved credentials | normal interactive use, after `iob-sync login` |
223-
| hidden prompt | when nothing else is available and a terminal is attached |
219+
| Source | Use |
220+
| ------------------- | ---------------------------------------------------------------------- |
221+
| `--password-stdin` | scripts and CI: `printf '%s' "$PW" \| iob-sync --password-stdin login` |
222+
| `IOBROKER_PASSWORD` | ad-hoc shells |
223+
| saved credentials | normal interactive use, after `iob-sync login` |
224+
| hidden prompt | when nothing else is available and a terminal is attached |
224225

225226
OAuth2 (`/oauth/token`) is tried first, falling back to the legacy `/login` endpoint.
226227
`--verbose` reports which path was used.

0 commit comments

Comments
 (0)