@msout/microsoft-onenote-exporter 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +51 -0
- package/LICENSE +21 -0
- package/NOTICE.md +38 -0
- package/README.md +199 -0
- package/package.json +72 -0
- package/src/config.js +80 -0
- package/src/index.js +183 -0
- package/src/logger.js +96 -0
- package/src/steps/auth.js +124 -0
- package/src/steps/export.js +67 -0
- package/src/steps/list.js +55 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented here.
|
|
4
|
+
The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
5
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
|
+
|
|
7
|
+
## [Unreleased]
|
|
8
|
+
|
|
9
|
+
## [0.1.0] - 2026-10-01
|
|
10
|
+
|
|
11
|
+
The first release. It adds a CLI over three existing packages and changes none of
|
|
12
|
+
their behaviour.
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
|
|
16
|
+
- **`microsoft-onenote-exporter <login|check|logout|list|export>`.** One binary over
|
|
17
|
+
`@msout/microsoft-webauth`, `@msout/microsoft-onenote-list-notebooks` and
|
|
18
|
+
`@msout/microsoft-onenote-export-notebook`, all three of which remain
|
|
19
|
+
separately installable and separately tested. A typical first run is `login`,
|
|
20
|
+
then `list`, then `export --notebook "Work"`.
|
|
21
|
+
- **A single Playwright, and therefore a single Chromium.** The three step
|
|
22
|
+
packages each declared `playwright: ^1.58.1` behind their own lockfile, so npm
|
|
23
|
+
resolved them independently — 1.61.0 in one, 1.61.1 in another — and each
|
|
24
|
+
wanted a different Chromium revision. A machine with all three checkouts out
|
|
25
|
+
held four revisions and 3.2 GB of browser. They are now pinned to exact versions
|
|
26
|
+
with an `overrides` block forcing one `playwright` and `playwright-core` across
|
|
27
|
+
the tree, and `test/wiring.test.js` fails if that ever stops being true.
|
|
28
|
+
- **One log per run.** Each package resolved its own log directory at load time,
|
|
29
|
+
so a pipeline produced three `app.log` files in three directories. The
|
|
30
|
+
environment variable those packages read is set before any of them is loaded,
|
|
31
|
+
so all four loggers — this CLI's and the three steps' — write one file.
|
|
32
|
+
- **One Docker image** for all five commands, with one Chromium. It replaces
|
|
33
|
+
three images that each carried their own browser; the list image also installed
|
|
34
|
+
a distribution `chromium` from apt that nothing used. `start-container.sh`
|
|
35
|
+
replaces the positional `<session-guid> <notebook-name>` interface with
|
|
36
|
+
subcommands, waits for the run, and explains exit code 3 as a partial export
|
|
37
|
+
rather than a failure.
|
|
38
|
+
- **Exit codes that mean something.** 0 success, 1 failure, 2 bad arguments, and
|
|
39
|
+
3 for an export that finished while missing pages, sections or groups. The
|
|
40
|
+
third is passed through from the export package's own `exitCodeForStats`, which
|
|
41
|
+
is asked rather than reimplemented, so the two CLIs cannot disagree.
|
|
42
|
+
|
|
43
|
+
### Notes
|
|
44
|
+
|
|
45
|
+
- `login` cannot run headless without `--email` and `--password`, and the
|
|
46
|
+
container refuses `login` outright rather than opening a browser nobody can see.
|
|
47
|
+
Interactive sign-in happens on the host.
|
|
48
|
+
- `all`, which chains login → list → export in one invocation, is deliberately
|
|
49
|
+
not in this release. None of the three packages accepts an injected browser, so
|
|
50
|
+
it would still be three Chromium launches behind one command — and the change
|
|
51
|
+
needed to make it one launch reaches into the most delicate file in the set.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ms-OneNote-Exporter
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/NOTICE.md
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# Notices
|
|
2
|
+
|
|
3
|
+
This project is distributed under the MIT licence. It is an umbrella: it contains
|
|
4
|
+
no copy of the work below, and depends on it at runtime.
|
|
5
|
+
|
|
6
|
+
## Bundled work
|
|
7
|
+
|
|
8
|
+
None. The three packages in the table below are installed from npm as ordinary
|
|
9
|
+
dependencies. None of their source is vendored into this repository or into the
|
|
10
|
+
Docker image built from it.
|
|
11
|
+
|
|
12
|
+
## Dependencies
|
|
13
|
+
|
|
14
|
+
| Package | Version | Licence | Role |
|
|
15
|
+
|---|---|---|---|
|
|
16
|
+
| `@msout/microsoft-webauth` | 0.1.8 | MIT | `login`, `check`, `logout` |
|
|
17
|
+
| `@msout/microsoft-onenote-list-notebooks` | 0.0.6 | MIT | `list` |
|
|
18
|
+
| `@msout/microsoft-onenote-export-notebook` | 0.3.7 | MIT | `export` |
|
|
19
|
+
|
|
20
|
+
All three are by the same authors and under the same MIT licence as this project.
|
|
21
|
+
They are developed in sibling repositories:
|
|
22
|
+
|
|
23
|
+
- <https://github.com/Ms-OneNote-Exporter/microsoft-webauth>
|
|
24
|
+
- <https://github.com/Ms-OneNote-Exporter/microsoft-onenote-list-notebooks>
|
|
25
|
+
- <https://github.com/Ms-OneNote-Exporter/microsoft-onenote-export-notebook>
|
|
26
|
+
|
|
27
|
+
One file *is* duplicated on purpose: `logPaths.js` exists in all three packages
|
|
28
|
+
with only the package name changed. They are separately published and cannot share
|
|
29
|
+
source, and keeping the copies byte-identical is what stops the three from
|
|
30
|
+
disagreeing about where an installed copy writes its logs. It says so at the top
|
|
31
|
+
of each file, and it should collapse to one module if the packages are ever
|
|
32
|
+
merged.
|
|
33
|
+
|
|
34
|
+
## Trademarks
|
|
35
|
+
|
|
36
|
+
Microsoft, OneNote, Outlook and Microsoft 365 are trademarks of Microsoft
|
|
37
|
+
Corporation. This project is not affiliated with or endorsed by Microsoft. It
|
|
38
|
+
automates a browser session against services the user already has an account for.
|
package/README.md
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
# microsoft-onenote-exporter
|
|
2
|
+
|
|
3
|
+
One command for the whole Microsoft OneNote pipeline: sign in, list the notebooks
|
|
4
|
+
on the account, export one to Obsidian-flavoured Markdown.
|
|
5
|
+
|
|
6
|
+
This is an umbrella over three packages that remain separately installable and
|
|
7
|
+
separately tested:
|
|
8
|
+
|
|
9
|
+
| Step | Package |
|
|
10
|
+
|---|---|
|
|
11
|
+
| `login`, `check`, `logout` | [`@msout/microsoft-webauth`](https://github.com/Ms-OneNote-Exporter/microsoft-webauth) |
|
|
12
|
+
| `list` | [`@msout/microsoft-onenote-list-notebooks`](https://github.com/Ms-OneNote-Exporter/microsoft-onenote-list-notebooks) |
|
|
13
|
+
| `export` | [`@msout/microsoft-onenote-export-notebook`](https://github.com/Ms-OneNote-Exporter/microsoft-onenote-export-notebook) |
|
|
14
|
+
|
|
15
|
+
Each one works on its own, and you can still install just the one you need. What
|
|
16
|
+
this adds is a single binary that can do all three, one Playwright install, one
|
|
17
|
+
Docker image, and one log per run.
|
|
18
|
+
|
|
19
|
+
## Why it exists
|
|
20
|
+
|
|
21
|
+
The three packages each declared `playwright: ^1.58.1` behind their own
|
|
22
|
+
lockfile, so npm resolved them independently — 1.61.0 in one, 1.61.1 in another.
|
|
23
|
+
Each resolved version wants a different Chromium revision, so a machine with all
|
|
24
|
+
three checkouts out downloads several browser builds into the shared
|
|
25
|
+
`~/Library/Caches/ms-playwright` cache. On the machine this was built on, that
|
|
26
|
+
cache held four Chromium revisions and weighed 3.2 GB.
|
|
27
|
+
|
|
28
|
+
It also let a single run produce three `app.log` files in three directories,
|
|
29
|
+
because each package resolved its own log path at load time.
|
|
30
|
+
|
|
31
|
+
Both problems are fixed at the root rather than papered over:
|
|
32
|
+
|
|
33
|
+
- the three dependencies are pinned to **exact** versions, and `overrides` forces
|
|
34
|
+
a single `playwright` / `playwright-core` across the whole tree. A caret range
|
|
35
|
+
always takes the newest match, so it is the wrong tool here;
|
|
36
|
+
- `ONENOTE_EXPORT_LOG_DIR` is set before any step package is loaded, so all four
|
|
37
|
+
loggers write one `app.log`.
|
|
38
|
+
|
|
39
|
+
There is a test for each: `test/wiring.test.js` fails if more than one Playwright
|
|
40
|
+
is installed, or if the pins drift back to a range.
|
|
41
|
+
|
|
42
|
+
## Install
|
|
43
|
+
|
|
44
|
+
```sh
|
|
45
|
+
npm install -g @msout/microsoft-onenote-exporter
|
|
46
|
+
npx playwright install chromium
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
The Chromium download is separate from `npm install` and is required — without it
|
|
50
|
+
every command that opens a browser fails.
|
|
51
|
+
|
|
52
|
+
Two command names are installed, pointing at the same binary:
|
|
53
|
+
|
|
54
|
+
```sh
|
|
55
|
+
microsoft-onenote-exporter login # matches the package and repository name
|
|
56
|
+
ms-onenote-exporter login # shorter, for typing
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Everything below uses the long one.
|
|
60
|
+
|
|
61
|
+
## Use
|
|
62
|
+
|
|
63
|
+
```sh
|
|
64
|
+
microsoft-onenote-exporter login # opens a browser; sign in there
|
|
65
|
+
microsoft-onenote-exporter list # what is on the account
|
|
66
|
+
microsoft-onenote-exporter export --notebook "Work" # export one
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
That is the whole pipeline. The session written by `login` is picked up by
|
|
70
|
+
`list` and `export` automatically.
|
|
71
|
+
|
|
72
|
+
### Commands
|
|
73
|
+
|
|
74
|
+
| Command | What it does |
|
|
75
|
+
|---|---|
|
|
76
|
+
| `login` | Signs in and saves the session. Without `--email`/`--password` the browser opens and you sign in yourself — an interactive login cannot run headless. |
|
|
77
|
+
| `check` | Reports whether the saved session is still valid. A dead session is deleted, so the next command needs a fresh `login`. |
|
|
78
|
+
| `logout` | Deletes the saved session and its metadata. |
|
|
79
|
+
| `list` | Lists the notebooks on the account. An empty result is a success, not an error. |
|
|
80
|
+
| `export` | Exports one notebook to Markdown. |
|
|
81
|
+
|
|
82
|
+
### Options
|
|
83
|
+
|
|
84
|
+
Every command accepts `--auth-file`, `--notheadless`, `--dodump`, `--verbose` and
|
|
85
|
+
`--quiet`.
|
|
86
|
+
|
|
87
|
+
| Command | Additional options |
|
|
88
|
+
|---|---|
|
|
89
|
+
| `login` | `--email`, `--password`, `--against onenote\|outlook`, `--screenshot` |
|
|
90
|
+
| `check` | `--against onenote\|outlook` |
|
|
91
|
+
| `export` | `--notebook <name>`, `--notebook-link <url>`, `--output-dir <path>`, `--nopassasked`, `--non-interactive` |
|
|
92
|
+
|
|
93
|
+
`--screenshot` only means something together with `--dodump`, so asking for one
|
|
94
|
+
turns the other on and says so.
|
|
95
|
+
|
|
96
|
+
### Unattended use
|
|
97
|
+
|
|
98
|
+
`--non-interactive` is for containers and CI. It requires `--notebook` or
|
|
99
|
+
`--notebook-link`, and implies `--nopassasked`, so the run cannot stop at a prompt
|
|
100
|
+
nobody is there to answer:
|
|
101
|
+
|
|
102
|
+
```sh
|
|
103
|
+
microsoft-onenote-exporter export --notebook "Work" --non-interactive --output-dir ./out
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### Exit codes
|
|
107
|
+
|
|
108
|
+
| Code | Meaning |
|
|
109
|
+
|---|---|
|
|
110
|
+
| 0 | Success. |
|
|
111
|
+
| 1 | The command failed — unusable auth file, dead run. |
|
|
112
|
+
| 2 | The arguments were wrong. |
|
|
113
|
+
| 3 | The export finished, but pages, sections or groups are missing. |
|
|
114
|
+
|
|
115
|
+
Three is deliberately not an error: the notes that were written are complete and
|
|
116
|
+
name any asset they could not download, so the output is worth keeping and
|
|
117
|
+
re-running is worth doing. It comes from
|
|
118
|
+
`@msout/microsoft-onenote-export-notebook`'s own rule, which this CLI asks for
|
|
119
|
+
rather than reimplementing.
|
|
120
|
+
|
|
121
|
+
## Logs
|
|
122
|
+
|
|
123
|
+
Every step of a run — this CLI's messages and all three packages' — goes to one
|
|
124
|
+
`logs/app.log` under the working directory. Override it with
|
|
125
|
+
`ONENOTE_EXPORT_LOG_DIR`, or use `--dodump` to also write the HTML of each page.
|
|
126
|
+
|
|
127
|
+
The directory and the log file are created owner-only. Dumps hold the
|
|
128
|
+
authenticated DOM of a real account: cookies, tenant hostnames, note titles.
|
|
129
|
+
|
|
130
|
+
## Docker
|
|
131
|
+
|
|
132
|
+
```sh
|
|
133
|
+
docker build -t microsoft-onenote-exporter .
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
One image, one Chromium, serving all five commands. The auth file is read from
|
|
137
|
+
`/data/output/auth.json` unless you pass `--auth-file`.
|
|
138
|
+
|
|
139
|
+
```sh
|
|
140
|
+
docker run --rm --init --shm-size=1g \
|
|
141
|
+
-v "$PWD/out:/data/output" \
|
|
142
|
+
microsoft-onenote-exporter export --notebook "Work" --non-interactive
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Or use the wrapper, which waits for the run and translates the exit code:
|
|
146
|
+
|
|
147
|
+
```sh
|
|
148
|
+
./start-container.sh --notebook "Work" --output-dir ./out
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
`--shm-size=1g` is required: Chromium crashes on memory-heavy pages with Docker's
|
|
152
|
+
default 64 MB of shared memory. `--init` reaps Chromium's child processes.
|
|
153
|
+
|
|
154
|
+
`login` is refused inside the container — without credentials the browser has to
|
|
155
|
+
be shown, and there is nobody there to show it to. Log in on the host first and
|
|
156
|
+
mount the resulting file.
|
|
157
|
+
|
|
158
|
+
## Development
|
|
159
|
+
|
|
160
|
+
The three step packages are pinned to versions that must exist on npm. To work
|
|
161
|
+
against local checkouts before they are published:
|
|
162
|
+
|
|
163
|
+
```sh
|
|
164
|
+
npm run use:local # pack the sibling repos and install those
|
|
165
|
+
npm test
|
|
166
|
+
npm run use:published # go back to the registry copies
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
`use:local` expects the three repositories one directory up, and leaves
|
|
170
|
+
`package.json` pinning published versions — only `node_modules` differs, so a
|
|
171
|
+
`file:` spec can never be committed.
|
|
172
|
+
|
|
173
|
+
```sh
|
|
174
|
+
npm test # no browser and no account needed
|
|
175
|
+
npm run lint
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
### Release order
|
|
179
|
+
|
|
180
|
+
There is an ordering constraint, and it is the one thing that cannot be worked
|
|
181
|
+
around: **this package cannot have a committed `package-lock.json` until the
|
|
182
|
+
three step packages are on npm.** Their pinned versions do not resolve before
|
|
183
|
+
then, and `use:local` produces a lockfile full of `file:.local-tarballs/…`
|
|
184
|
+
entries that works only on the machine that made it — `.local-tarballs/` is
|
|
185
|
+
gitignored, so CI and the Docker image would both fail to install from it.
|
|
186
|
+
|
|
187
|
+
So the first release goes:
|
|
188
|
+
|
|
189
|
+
1. Publish the three step packages from their own repositories.
|
|
190
|
+
2. Here: `rm -rf node_modules .local-tarballs && npm install`.
|
|
191
|
+
3. Commit the generated `package-lock.json`. After this, `npm ci` works and CI
|
|
192
|
+
and the Dockerfile function.
|
|
193
|
+
|
|
194
|
+
`test/lockfile.test.js` fails with these instructions until step 3 happens, so the
|
|
195
|
+
gap is visible rather than discovered during a build.
|
|
196
|
+
|
|
197
|
+
## Licence
|
|
198
|
+
|
|
199
|
+
MIT. See [LICENSE](LICENSE) and [NOTICE.md](NOTICE.md).
|
package/package.json
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@msout/microsoft-onenote-exporter",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Log in, list and export Microsoft OneNote notebooks as Obsidian Markdown - one CLI over @msout/microsoft-webauth, @msout/microsoft-onenote-list-notebooks and @msout/microsoft-onenote-export-notebook.",
|
|
5
|
+
"main": "src/index.js",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": "./src/index.js",
|
|
8
|
+
"./cli": "./src/index.js",
|
|
9
|
+
"./package.json": "./package.json"
|
|
10
|
+
},
|
|
11
|
+
"bin": {
|
|
12
|
+
"ms-onenote-exporter": "src/index.js",
|
|
13
|
+
"microsoft-onenote-exporter": "src/index.js"
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"src/",
|
|
17
|
+
"README.md",
|
|
18
|
+
"LICENSE",
|
|
19
|
+
"NOTICE.md",
|
|
20
|
+
"CHANGELOG.md"
|
|
21
|
+
],
|
|
22
|
+
"engines": {
|
|
23
|
+
"node": ">=24.0.0"
|
|
24
|
+
},
|
|
25
|
+
"scripts": {
|
|
26
|
+
"start": "node src/index.js",
|
|
27
|
+
"test": "jest",
|
|
28
|
+
"test:watch": "jest --watch",
|
|
29
|
+
"lint": "eslint .",
|
|
30
|
+
"use:local": "sh scripts/use-local-packages.sh",
|
|
31
|
+
"use:published": "sh scripts/use-published-packages.sh"
|
|
32
|
+
},
|
|
33
|
+
"keywords": [
|
|
34
|
+
"microsoft",
|
|
35
|
+
"onenote",
|
|
36
|
+
"playwright",
|
|
37
|
+
"export",
|
|
38
|
+
"obsidian",
|
|
39
|
+
"markdown",
|
|
40
|
+
"cli"
|
|
41
|
+
],
|
|
42
|
+
"author": "msout@tuta.io",
|
|
43
|
+
"license": "MIT",
|
|
44
|
+
"repository": {
|
|
45
|
+
"type": "git",
|
|
46
|
+
"url": "git+https://github.com/Ms-OneNote-Exporter/microsoft-onenote-exporter.git"
|
|
47
|
+
},
|
|
48
|
+
"bugs": {
|
|
49
|
+
"url": "https://github.com/Ms-OneNote-Exporter/microsoft-onenote-exporter/issues"
|
|
50
|
+
},
|
|
51
|
+
"homepage": "https://github.com/Ms-OneNote-Exporter/microsoft-onenote-exporter#readme",
|
|
52
|
+
"dependencies": {
|
|
53
|
+
"@msout/microsoft-onenote-export-notebook": "0.3.7",
|
|
54
|
+
"@msout/microsoft-onenote-list-notebooks": "0.0.6",
|
|
55
|
+
"@msout/microsoft-webauth": "0.1.8",
|
|
56
|
+
"chalk": "^4.1.2",
|
|
57
|
+
"commander": "^14.0.3",
|
|
58
|
+
"fs-extra": "^11.3.3",
|
|
59
|
+
"playwright": "1.63.0"
|
|
60
|
+
},
|
|
61
|
+
"overrides": {
|
|
62
|
+
"playwright": "1.63.0",
|
|
63
|
+
"playwright-core": "1.63.0"
|
|
64
|
+
},
|
|
65
|
+
"devDependencies": {
|
|
66
|
+
"eslint": "^9.39.5",
|
|
67
|
+
"jest": "^29.7.0"
|
|
68
|
+
},
|
|
69
|
+
"publishConfig": {
|
|
70
|
+
"access": "public"
|
|
71
|
+
}
|
|
72
|
+
}
|
package/src/config.js
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Configuration for the umbrella CLI.
|
|
3
|
+
* @copyright 2026 msout
|
|
4
|
+
*/
|
|
5
|
+
const os = require('os');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Where the three step packages should write their logs.
|
|
10
|
+
*
|
|
11
|
+
* Each of them is a separately installable package with its own logger, and each
|
|
12
|
+
* one is a singleton constructed the moment it is required. This function has to
|
|
13
|
+
* run before any of them is loaded, which is why src/index.js calls it at the
|
|
14
|
+
* very top rather than passing a path around: by the time a step is invoked the
|
|
15
|
+
* log directories have already been decided.
|
|
16
|
+
*
|
|
17
|
+
* The default is <cwd>/logs rather than a per-package directory because all three
|
|
18
|
+
* steps write `app.log` into the same directory, so a pipeline run produces one
|
|
19
|
+
* log with the steps in order rather than three logs in three places.
|
|
20
|
+
*/
|
|
21
|
+
function resolveLogDir() {
|
|
22
|
+
const override = process.env.ONENOTE_EXPORT_LOG_DIR;
|
|
23
|
+
if (override && override.trim()) {
|
|
24
|
+
return path.resolve(override.trim());
|
|
25
|
+
}
|
|
26
|
+
return path.resolve(process.cwd(), 'logs');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Points the three step packages at this process's log directory.
|
|
31
|
+
*
|
|
32
|
+
* Setting the environment variable rather than passing an option is not a
|
|
33
|
+
* shortcut, it is the only channel those packages have: each resolves its log
|
|
34
|
+
* directory at require time from ONENOTE_EXPORT_LOG_DIR, and none of them accept
|
|
35
|
+
* a path as an argument. Idempotent, and it never overrides a value the caller
|
|
36
|
+
* already set - a container or a CI step that mounts its own log volume keeps it.
|
|
37
|
+
*
|
|
38
|
+
* @param {string} [logDir] - Directory to use; resolved from the environment when omitted
|
|
39
|
+
* @returns {string} The absolute directory all four loggers will write to
|
|
40
|
+
*/
|
|
41
|
+
function shareLogDir(logDir = resolveLogDir()) {
|
|
42
|
+
process.env.ONENOTE_EXPORT_LOG_DIR = logDir;
|
|
43
|
+
return logDir;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** The saved session, shared by every step. */
|
|
47
|
+
function defaultAuthFile() {
|
|
48
|
+
const home = os.homedir();
|
|
49
|
+
return path.join(home, '.microsoft-webauth', 'auth-file.json');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Where to authenticate against.
|
|
54
|
+
*
|
|
55
|
+
* Both URLs are the ones the step packages pin. ONENOTE_URL is the pre-rebrand
|
|
56
|
+
* /notebooks path on purpose: Microsoft 365 Copilot moved the app to
|
|
57
|
+
* /copilotnotebooks but /notebooks still redirects there, so a login entered at
|
|
58
|
+
* the old path is detected as authenticated a few seconds later. Re-pinning it
|
|
59
|
+
* here would trade a working alias for a path that can move again - see
|
|
60
|
+
* ONENOTE_URL in @msout/microsoft-webauth.
|
|
61
|
+
*/
|
|
62
|
+
const TARGETS = {
|
|
63
|
+
onenote: 'https://onenote.cloud.microsoft/notebooks',
|
|
64
|
+
outlook: 'https://outlook.cloud.microsoft/mail/',
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
/** Exit codes, so a caller can tell the outcomes apart without parsing output. */
|
|
68
|
+
const EXIT = {
|
|
69
|
+
ok: 0,
|
|
70
|
+
/** The command itself failed: bad arguments, unusable auth file, a dead run. */
|
|
71
|
+
failed: 1,
|
|
72
|
+
/** Unattended use asked for without a way to choose a notebook. */
|
|
73
|
+
usage: 2,
|
|
74
|
+
/** Finished, but pages, sections or groups are missing. Passed through from
|
|
75
|
+
* @msout/microsoft-onenote-export-notebook, which distinguishes this from a
|
|
76
|
+
* total failure because partial output is still worth keeping. */
|
|
77
|
+
partial: 3,
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
module.exports = { resolveLogDir, shareLogDir, defaultAuthFile, TARGETS, EXIT };
|
package/src/index.js
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* @fileoverview CLI over the three OneNote exporter steps.
|
|
4
|
+
* @copyright 2026 msout
|
|
5
|
+
*
|
|
6
|
+
* One binary, three steps. The packages it drives remain separately installable
|
|
7
|
+
* and separately tested; what this adds is a single command that can do all
|
|
8
|
+
* three, one Playwright install, one Docker image, and one log per run.
|
|
9
|
+
*/
|
|
10
|
+
const { shareLogDir } = require('./config');
|
|
11
|
+
|
|
12
|
+
// Before anything else, and before any step package is loaded: each of those
|
|
13
|
+
// decides its log directory at require time, from the environment. Doing this
|
|
14
|
+
// lower in the file would leave three loggers pointing somewhere this run never
|
|
15
|
+
// writes. See shareLogDir for why an environment variable is the only channel
|
|
16
|
+
// available.
|
|
17
|
+
const LOG_DIR = shareLogDir();
|
|
18
|
+
|
|
19
|
+
const { program } = require('commander');
|
|
20
|
+
const logger = require('./logger');
|
|
21
|
+
const { EXIT, TARGETS } = require('./config');
|
|
22
|
+
const { version: PKG_VERSION } = require('../package.json');
|
|
23
|
+
|
|
24
|
+
// Read from ./config rather than from the step package, so `--help` does not
|
|
25
|
+
// pull in Playwright and three loggers just to print a default path. steps/auth
|
|
26
|
+
// re-exports the same value for anyone who wants the package's own answer.
|
|
27
|
+
const { defaultAuthFile } = require('./steps/auth');
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Options every step understands.
|
|
31
|
+
*
|
|
32
|
+
* Declared once and reused, because a flag that works on `login` but not on
|
|
33
|
+
* `export` is indistinguishable from a bug in the tool.
|
|
34
|
+
*/
|
|
35
|
+
function sharedOptions(command) {
|
|
36
|
+
return command
|
|
37
|
+
// No "(default: ...)" in the description: commander appends the real
|
|
38
|
+
// value itself, and spelling it out here printed it twice.
|
|
39
|
+
.option('--auth-file <path>', 'Path to the saved session', defaultAuthFile())
|
|
40
|
+
.option('--notheadless', 'Show the browser. Required for an interactive login, which cannot run headless')
|
|
41
|
+
.option('--dodump', 'Write the HTML of each page to the log directory, for debugging')
|
|
42
|
+
.option('-v, --verbose', 'Include debug output')
|
|
43
|
+
.option('-q, --quiet', 'Warnings and errors only');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Applies the verbosity flags.
|
|
48
|
+
*
|
|
49
|
+
* Sets this process's logger and the environment variable the export step reads,
|
|
50
|
+
* so one flag configures all four loggers instead of two of them.
|
|
51
|
+
*/
|
|
52
|
+
function applyVerbosity(options) {
|
|
53
|
+
if (options.verbose) {
|
|
54
|
+
logger.setLevel('debug');
|
|
55
|
+
process.env.ONENOTE_EXPORT_LOG_LEVEL = 'debug';
|
|
56
|
+
} else if (options.quiet) {
|
|
57
|
+
logger.setLevel('warn');
|
|
58
|
+
process.env.ONENOTE_EXPORT_LOG_LEVEL = 'warn';
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Runs one step and turns its result into this process's exit code.
|
|
64
|
+
*
|
|
65
|
+
* Every failure path ends here, which is why there is exactly one place that
|
|
66
|
+
* decides the status: a command that reported an error and exited 0 is how a
|
|
67
|
+
* pipeline ends up treating a failed listing as a pass.
|
|
68
|
+
*/
|
|
69
|
+
async function run(step) {
|
|
70
|
+
try {
|
|
71
|
+
const { exitCode } = await step();
|
|
72
|
+
process.exitCode = exitCode;
|
|
73
|
+
} catch (e) {
|
|
74
|
+
logger.error(`${program.name()} failed:`, e);
|
|
75
|
+
process.exitCode = EXIT.failed;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
program
|
|
80
|
+
.name('microsoft-onenote-exporter')
|
|
81
|
+
.description('Sign in to Microsoft OneNote, list the notebooks on the account, and export one to Markdown.')
|
|
82
|
+
.version(PKG_VERSION)
|
|
83
|
+
.addHelpText('after', `
|
|
84
|
+
Also installed as: ms-onenote-exporter
|
|
85
|
+
|
|
86
|
+
Logs for every step of a run are written to one app.log in:
|
|
87
|
+
${LOG_DIR}
|
|
88
|
+
|
|
89
|
+
A typical first run:
|
|
90
|
+
microsoft-onenote-exporter login
|
|
91
|
+
microsoft-onenote-exporter list
|
|
92
|
+
microsoft-onenote-exporter export --notebook "Work"
|
|
93
|
+
`);
|
|
94
|
+
|
|
95
|
+
sharedOptions(
|
|
96
|
+
program
|
|
97
|
+
.command('login')
|
|
98
|
+
.description('Sign in to Microsoft and save the session for the other steps')
|
|
99
|
+
)
|
|
100
|
+
.option('--email <email>', 'Account email. Without it, the browser opens and you sign in yourself')
|
|
101
|
+
.option('--password <password>', 'Account password. Supplying it makes the login headless')
|
|
102
|
+
.option('--against <target>', `Service to authenticate against: ${Object.keys(TARGETS).join(' | ')}`, 'onenote')
|
|
103
|
+
.option('--screenshot', 'With --dodump, also save a PNG of each dumped page')
|
|
104
|
+
.action(async (options) => {
|
|
105
|
+
applyVerbosity(options);
|
|
106
|
+
await run(() => require('./steps/auth').login(options));
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
sharedOptions(
|
|
110
|
+
program
|
|
111
|
+
.command('check')
|
|
112
|
+
.description('Report whether the saved session is still valid')
|
|
113
|
+
)
|
|
114
|
+
.option('--against <target>', `Service to check: ${Object.keys(TARGETS).join(' | ')}`, 'onenote')
|
|
115
|
+
.action(async (options) => {
|
|
116
|
+
applyVerbosity(options);
|
|
117
|
+
await run(() => require('./steps/auth').check(options));
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
sharedOptions(
|
|
121
|
+
program
|
|
122
|
+
.command('logout')
|
|
123
|
+
.description('Delete the saved session and its metadata')
|
|
124
|
+
)
|
|
125
|
+
.action(async (options) => {
|
|
126
|
+
applyVerbosity(options);
|
|
127
|
+
await run(() => require('./steps/auth').logout(options));
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
sharedOptions(
|
|
131
|
+
program
|
|
132
|
+
.command('list')
|
|
133
|
+
.description('List the notebooks on the signed-in account')
|
|
134
|
+
)
|
|
135
|
+
.action(async (options) => {
|
|
136
|
+
applyVerbosity(options);
|
|
137
|
+
await run(() => require('./steps/list').list(options));
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
sharedOptions(
|
|
141
|
+
program
|
|
142
|
+
.command('export')
|
|
143
|
+
.description('Export one notebook to Obsidian-flavoured Markdown')
|
|
144
|
+
)
|
|
145
|
+
.option('--notebook <name>', 'Notebook to export, by name (skips the interactive picker)')
|
|
146
|
+
.option('--notebook-link <url>', 'Notebook to export, by URL (skips listing and picking)')
|
|
147
|
+
.option('--output-dir <path>', 'Where to write the Markdown (default: ./output)')
|
|
148
|
+
.option('--nopassasked', 'Skip password-protected sections instead of asking for the password')
|
|
149
|
+
.option('--non-interactive', 'Run unattended: requires --notebook or --notebook-link, and implies --nopassasked')
|
|
150
|
+
.action(async (options) => {
|
|
151
|
+
applyVerbosity(options);
|
|
152
|
+
|
|
153
|
+
// Fail before a browser is launched. Left to the export step, an
|
|
154
|
+
// unattended run with no way to choose a notebook reaches the
|
|
155
|
+
// interactive picker and waits forever - which in a container looks
|
|
156
|
+
// exactly like a hung export.
|
|
157
|
+
if (options.nonInteractive && !(options.notebook || options.notebookLink)) {
|
|
158
|
+
logger.error('--non-interactive requires --notebook <name> or --notebook-link <url>.');
|
|
159
|
+
logger.error('Without one of them the export would stop at the interactive notebook picker.');
|
|
160
|
+
process.exitCode = EXIT.usage;
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
await run(() => require('./steps/export').exportNotebook(options));
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
// A Playwright target that dies mid-run - the tab closed, the renderer crashed,
|
|
168
|
+
// the browser was killed - also rejects one of Playwright's own internal
|
|
169
|
+
// promises, which nothing awaits. Node treats that as fatal and kills the
|
|
170
|
+
// process with a bare stack trace, so a dead OneNote tab would end a run with no
|
|
171
|
+
// message, no summary and no status. Report it like any other failure and let
|
|
172
|
+
// the run finish unwinding.
|
|
173
|
+
process.on('unhandledRejection', (reason) => {
|
|
174
|
+
logger.error('Unexpected internal failure (this is a bug):', reason);
|
|
175
|
+
process.exitCode = EXIT.failed;
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
program.parseAsync().catch((e) => {
|
|
179
|
+
// The handlers above report their own failures; this is the net for anything
|
|
180
|
+
// else, including a throw while arguments were parsed.
|
|
181
|
+
logger.error('Failed:', e);
|
|
182
|
+
process.exitCode = EXIT.failed;
|
|
183
|
+
});
|
package/src/logger.js
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Logger for the umbrella CLI.
|
|
3
|
+
* @copyright 2026 msout
|
|
4
|
+
*
|
|
5
|
+
* A fourth logger, which is not the point of this exercise but is unavoidable
|
|
6
|
+
* while the three steps are separate packages: none of them exports its logger,
|
|
7
|
+
* and each constructs its own singleton at require time. This one handles the
|
|
8
|
+
* umbrella's own messages, and points the other three at the same directory (see
|
|
9
|
+
* src/config.js shareLogDir) so a run produces one app.log.
|
|
10
|
+
*/
|
|
11
|
+
const chalk = require('chalk');
|
|
12
|
+
const fs = require('fs-extra');
|
|
13
|
+
const path = require('path');
|
|
14
|
+
const { resolveLogDir } = require('./config');
|
|
15
|
+
|
|
16
|
+
/** Severity order, lowest first. A message is emitted if its level >= the threshold. */
|
|
17
|
+
const LEVELS = { debug: 10, info: 20, step: 20, success: 20, warn: 30, error: 40 };
|
|
18
|
+
|
|
19
|
+
class Logger {
|
|
20
|
+
constructor() {
|
|
21
|
+
this.months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
|
22
|
+
this.logDir = resolveLogDir();
|
|
23
|
+
this.logFilePath = path.join(this.logDir, 'app.log');
|
|
24
|
+
this.level = Logger._initialLevel();
|
|
25
|
+
fs.ensureDirSync(this.logDir);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Reads the initial threshold from the environment.
|
|
30
|
+
*
|
|
31
|
+
* ONENOTE_EXPORT_LOG_LEVEL is the name the export step already uses, so
|
|
32
|
+
* `--verbose` and a container's environment variable set the same thing for
|
|
33
|
+
* all four loggers rather than two of them.
|
|
34
|
+
*/
|
|
35
|
+
static _initialLevel() {
|
|
36
|
+
const raw = (process.env.ONENOTE_EXPORT_LOG_LEVEL || '').toLowerCase().trim();
|
|
37
|
+
if (raw === 'debug' || raw === 'verbose') return LEVELS.debug;
|
|
38
|
+
if (raw === 'warn' || raw === 'quiet') return LEVELS.warn;
|
|
39
|
+
if (raw === 'error') return LEVELS.error;
|
|
40
|
+
return LEVELS.info;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Raises or lowers the threshold at runtime.
|
|
45
|
+
* @param {string} name - One of debug|info|warn|error
|
|
46
|
+
*/
|
|
47
|
+
setLevel(name) {
|
|
48
|
+
const level = LEVELS[(name || '').toLowerCase()];
|
|
49
|
+
if (level !== undefined) {
|
|
50
|
+
this.level = level;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** True when a message at `level` should be emitted. */
|
|
55
|
+
_enabled(level) {
|
|
56
|
+
return (LEVELS[level] ?? LEVELS.info) >= this.level;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
_timestamp() {
|
|
60
|
+
const now = new Date();
|
|
61
|
+
return `[${this.months[now.getMonth()]} ${String(now.getDate()).padStart(2, '0')} ${now.toTimeString().split(' ')[0]}]`;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
_stripColors(str) {
|
|
65
|
+
// eslint-disable-next-line no-control-regex
|
|
66
|
+
return str.replace(/\u001b\[[0-9;]*m/g, '');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
_write(level, message, color) {
|
|
70
|
+
if (!this._enabled(level)) return;
|
|
71
|
+
|
|
72
|
+
const stamp = this._timestamp();
|
|
73
|
+
const body = message instanceof Error
|
|
74
|
+
? (message.stack || message.message)
|
|
75
|
+
: (typeof message === 'string' ? message : JSON.stringify(message, null, 2));
|
|
76
|
+
|
|
77
|
+
const plain = body.split('\n').map((line) => `[${level}] ${line}`).join('\n');
|
|
78
|
+
const colored = body.split('\n').map((line) => `${chalk.gray(stamp)} ${color(`[${level}]`)} ${line}`).join('\n');
|
|
79
|
+
|
|
80
|
+
// mode only applies at creation; an app.log from another logger in this
|
|
81
|
+
// same directory may predate it and be 0644.
|
|
82
|
+
fs.appendFileSync(this.logFilePath, `${plain}\n`, { mode: 0o600 });
|
|
83
|
+
|
|
84
|
+
const stream = level === 'error' ? process.stderr : process.stdout;
|
|
85
|
+
stream.write(`${colored}\n`);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
debug(message) { this._write('debug', message, chalk.gray); }
|
|
89
|
+
info(message) { this._write('info', message, chalk.blue); }
|
|
90
|
+
step(message) { this._write('step', message, chalk.magenta); }
|
|
91
|
+
success(message) { this._write('success', message, chalk.green); }
|
|
92
|
+
warn(message) { this._write('warn', message, chalk.yellow); }
|
|
93
|
+
error(message) { this._write('error', message, chalk.red); }
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
module.exports = new Logger();
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Adapters over @msout/microsoft-webauth.
|
|
3
|
+
* @copyright 2026 msout
|
|
4
|
+
*
|
|
5
|
+
* This is a translation layer and nothing more: it renames the CLI's options into
|
|
6
|
+
* the names that package expects, calls one function, and returns. All the
|
|
7
|
+
* behaviour - the login flow, the blocking-screen handling, the credential
|
|
8
|
+
* redaction in dumps - stays in that package, which is separately installable and
|
|
9
|
+
* separately tested. If a rule ever needs to exist in two places, it belongs
|
|
10
|
+
* here as a comment and not as code.
|
|
11
|
+
*/
|
|
12
|
+
const logger = require('../logger');
|
|
13
|
+
const { TARGETS, EXIT } = require('../config');
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Loads the package.
|
|
17
|
+
*
|
|
18
|
+
* Required lazily, inside the command, rather than at the top of the module: the
|
|
19
|
+
* package's logger is a singleton built at require time and decides its log
|
|
20
|
+
* directory from the environment, so it must not be loaded before src/index.js
|
|
21
|
+
* has set ONENOTE_EXPORT_LOG_DIR. Loading it at import time would put its app.log
|
|
22
|
+
* somewhere this run never looks.
|
|
23
|
+
*/
|
|
24
|
+
function load() {
|
|
25
|
+
return require('@msout/microsoft-webauth');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** The auth file default, read from the package that owns the convention. */
|
|
29
|
+
function defaultAuthFile() {
|
|
30
|
+
return require('@msout/microsoft-webauth/config').DEFAULT_AUTH_FILE;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Resolves the --against value to a URL the login flow will accept. */
|
|
34
|
+
function targetUrl(against) {
|
|
35
|
+
return against === 'outlook' ? TARGETS.outlook : TARGETS.onenote;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Signs in and writes the session to the auth file.
|
|
40
|
+
*
|
|
41
|
+
* Note what decides headless: that package runs headless only when it is given
|
|
42
|
+
* both an email and a password. Without them it shows the browser, because an
|
|
43
|
+
* interactive login needs somewhere to type the password and click the MFA
|
|
44
|
+
* prompt. `--notheadless` is therefore only meaningful alongside credentials, and
|
|
45
|
+
* saying so is better than letting a flag appear to do nothing.
|
|
46
|
+
*
|
|
47
|
+
* @param {object} options - CLI options
|
|
48
|
+
* @returns {Promise<object>} The auth metadata the package recorded
|
|
49
|
+
*/
|
|
50
|
+
async function login(options) {
|
|
51
|
+
const { login: doLogin } = load();
|
|
52
|
+
|
|
53
|
+
if (!options.notheadless && !(options.email && options.password)) {
|
|
54
|
+
logger.debug('No --email/--password given, so the browser will be shown for an interactive login.');
|
|
55
|
+
}
|
|
56
|
+
if (options.screenshot && !options.dodump) {
|
|
57
|
+
logger.warn('--screenshot only applies to the pages written by --dodump; enabling --dodump as well.');
|
|
58
|
+
options.dodump = true;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
await doLogin({
|
|
62
|
+
email: options.email,
|
|
63
|
+
password: options.password,
|
|
64
|
+
targetUrl: targetUrl(options.against),
|
|
65
|
+
authFile: options.authFile,
|
|
66
|
+
notheadless: options.notheadless,
|
|
67
|
+
dodump: options.dodump,
|
|
68
|
+
screenshot: options.screenshot,
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
return { exitCode: EXIT.ok };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Reports whether the saved session still works.
|
|
76
|
+
*
|
|
77
|
+
* Worth being precise about what this proves: the underlying check launches a
|
|
78
|
+
* browser, loads the auth file and follows the redirect. If it redirects to a
|
|
79
|
+
* Microsoft login page the session is dead, and the package deletes the stale
|
|
80
|
+
* file. So a `false` here does not just mean "not authenticated", it means the
|
|
81
|
+
* file has been cleaned up and the next command needs a fresh `login`.
|
|
82
|
+
*
|
|
83
|
+
* @param {object} options - CLI options
|
|
84
|
+
* @returns {Promise<object>} { authenticated, exitCode }
|
|
85
|
+
*/
|
|
86
|
+
async function check(options) {
|
|
87
|
+
const { checkAuth, getAuthMeta } = load();
|
|
88
|
+
|
|
89
|
+
const authenticated = await checkAuth(targetUrl(options.against), options.authFile);
|
|
90
|
+
|
|
91
|
+
if (!authenticated) {
|
|
92
|
+
logger.error('Not authenticated, or the saved session has expired.');
|
|
93
|
+
logger.error('Run "microsoft-onenote-exporter login" to create a new one.');
|
|
94
|
+
return { authenticated: false, exitCode: EXIT.failed };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
logger.success(`Authenticated${options.against === 'outlook' ? ' against Outlook' : ''}.`);
|
|
98
|
+
|
|
99
|
+
const meta = await getAuthMeta(options.authFile);
|
|
100
|
+
if (meta && meta.email) {
|
|
101
|
+
logger.info(`Signed in as: ${meta.email}`);
|
|
102
|
+
}
|
|
103
|
+
if (meta && meta.loginTime) {
|
|
104
|
+
logger.info(`Session started: ${new Date(meta.loginTime).toLocaleString()}`);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return { authenticated: true, exitCode: EXIT.ok };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Deletes the saved session and its metadata.
|
|
112
|
+
*
|
|
113
|
+
* @param {object} options - CLI options
|
|
114
|
+
* @returns {Promise<object>} { exitCode }
|
|
115
|
+
*/
|
|
116
|
+
async function logout(options) {
|
|
117
|
+
const { logout: doLogout } = load();
|
|
118
|
+
|
|
119
|
+
await doLogout(options.authFile);
|
|
120
|
+
logger.success('Signed out. The saved session has been deleted.');
|
|
121
|
+
return { exitCode: EXIT.ok };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
module.exports = { login, check, logout, defaultAuthFile, targetUrl };
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Adapter over @msout/microsoft-onenote-export-notebook.
|
|
3
|
+
* @copyright 2026 msout
|
|
4
|
+
*/
|
|
5
|
+
const logger = require('../logger');
|
|
6
|
+
const { EXIT } = require('../config');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Loads the package.
|
|
10
|
+
*
|
|
11
|
+
* Lazily, for the same reason as in steps/auth.js: its logger is a singleton
|
|
12
|
+
* constructed at require time and reads the log directory from the environment,
|
|
13
|
+
* so it must not be loaded before src/index.js has set ONENOTE_EXPORT_LOG_DIR.
|
|
14
|
+
*/
|
|
15
|
+
function load() {
|
|
16
|
+
return require('@msout/microsoft-onenote-export-notebook');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Exports one notebook to Obsidian-flavoured Markdown.
|
|
21
|
+
*
|
|
22
|
+
* On exit codes. That package uses three - 1 for a run that produced nothing
|
|
23
|
+
* usable, 2 for bad arguments, 3 for a run that finished while missing pages,
|
|
24
|
+
* sections or groups - and it reports 3 by setting `process.exitCode` as a side
|
|
25
|
+
* effect from inside the export, then returns its stats object.
|
|
26
|
+
*
|
|
27
|
+
* The return value is the better channel and this uses it: exitCodeForStats is
|
|
28
|
+
* exported precisely so a caller can ask the same question the package asked,
|
|
29
|
+
* and reading a global that a library mutated is how a composed pipeline ends up
|
|
30
|
+
* with the wrong status. src/index.js sets the real exit code afterwards, which
|
|
31
|
+
* also means the side effect is overwritten rather than inherited.
|
|
32
|
+
*
|
|
33
|
+
* Three is deliberately not collapsed into one here. Partial output is worth
|
|
34
|
+
* keeping and re-running, and a caller that cannot tell "partial" from "nothing"
|
|
35
|
+
* has to discard both.
|
|
36
|
+
*
|
|
37
|
+
* @param {object} options - CLI options
|
|
38
|
+
* @returns {Promise<object>} { stats, exitCode }
|
|
39
|
+
*/
|
|
40
|
+
async function exportNotebook(options) {
|
|
41
|
+
const { runExport, exitCodeForStats } = load();
|
|
42
|
+
|
|
43
|
+
const stats = await runExport({
|
|
44
|
+
authFile: options.authFile,
|
|
45
|
+
notebook: options.notebook,
|
|
46
|
+
notebookLink: options.notebookLink,
|
|
47
|
+
exportDir: options.outputDir,
|
|
48
|
+
notheadless: options.notheadless,
|
|
49
|
+
dodump: options.dodump,
|
|
50
|
+
nopassasked: options.nopassasked,
|
|
51
|
+
nonInteractive: options.nonInteractive,
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// The package's own rule, not a copy of it: if the two ever disagree, this
|
|
55
|
+
// one follows the package.
|
|
56
|
+
const exitCode = exitCodeForStats(stats);
|
|
57
|
+
|
|
58
|
+
if (exitCode === EXIT.partial) {
|
|
59
|
+
logger.warn('The export finished, but some pages, sections or groups are missing.');
|
|
60
|
+
logger.warn('The notes that were written are complete, and name any asset they could not download.');
|
|
61
|
+
logger.warn('Re-run the export to try again.');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return { stats, exitCode };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
module.exports = { exportNotebook };
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Adapter over @msout/microsoft-onenote-list-notebooks.
|
|
3
|
+
* @copyright 2026 msout
|
|
4
|
+
*/
|
|
5
|
+
const logger = require('../logger');
|
|
6
|
+
const { EXIT } = require('../config');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Loads the package.
|
|
10
|
+
*
|
|
11
|
+
* Lazily, for the same reason as in steps/auth.js: its logger is a singleton
|
|
12
|
+
* constructed at require time and reads the log directory from the environment,
|
|
13
|
+
* so it must not be loaded before src/index.js has set ONENOTE_EXPORT_LOG_DIR.
|
|
14
|
+
*/
|
|
15
|
+
function load() {
|
|
16
|
+
return require('@msout/microsoft-onenote-list-notebooks');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Lists the notebooks on the signed-in account.
|
|
21
|
+
*
|
|
22
|
+
* An empty result is not an error: it means the account is authenticated and has
|
|
23
|
+
* no notebooks, which is a different situation from a failure and is reported as
|
|
24
|
+
* success. The distinction matters because the export step can still work from a
|
|
25
|
+
* `--notebook-link` when the listing comes back empty - Microsoft for the web
|
|
26
|
+
* does not always list a notebook the account can otherwise open directly.
|
|
27
|
+
*
|
|
28
|
+
* @param {object} options - CLI options
|
|
29
|
+
* @returns {Promise<object>} { notebooks, exitCode }
|
|
30
|
+
*/
|
|
31
|
+
async function list(options) {
|
|
32
|
+
const { listNotebooks } = load();
|
|
33
|
+
|
|
34
|
+
const notebooks = await listNotebooks({
|
|
35
|
+
authFile: options.authFile,
|
|
36
|
+
notheadless: options.notheadless,
|
|
37
|
+
dodump: options.dodump,
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
if (notebooks.length === 0) {
|
|
41
|
+
logger.warn('No notebooks were found on this account.');
|
|
42
|
+
logger.warn('If you know the notebook exists, export it by URL:');
|
|
43
|
+
logger.warn(' microsoft-onenote-exporter export --notebook-link <url>');
|
|
44
|
+
} else {
|
|
45
|
+
logger.step('\nAvailable notebooks:');
|
|
46
|
+
notebooks.forEach((nb, index) => {
|
|
47
|
+
logger.info(`${index + 1}. ${nb.name}`);
|
|
48
|
+
logger.debug(` ${nb.url}`);
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return { notebooks, exitCode: EXIT.ok };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
module.exports = { list };
|