mirror of
https://github.com/wiseguru/ReWrite-Voice-Notes.git
synced 2026-07-22 07:49:19 +00:00
Compare commits
4 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
76243ecdf4 | ||
|
|
42f0a43c62 | ||
|
|
01e3fb8d75 | ||
|
|
437aa5e050 |
18 changed files with 346 additions and 68 deletions
|
|
@ -1,6 +1,6 @@
|
||||||
# ReWrite (Voice Notes) — release feature checklist
|
# ReWrite (Voice Notes) — release feature checklist
|
||||||
|
|
||||||
Manual pass to run in a **scratch** Obsidian vault against a fresh build (see `npm run release:prep`). This is the source of truth for pre-release verification; the old checklist in `obsidian-voice-notes-spec.md` is retired.
|
Manual pass to run in a **scratch** Obsidian vault against the artifact under test — for a release gate, the published `-alpha`/`-beta` prerelease assets installed into the vault (see `docs/RELEASING.md`, "The alpha/beta channel"); for mid-development iteration, a fresh local build via `npm run release:prep`. This is the source of truth for release verification; the old checklist in `obsidian-voice-notes-spec.md` is retired.
|
||||||
|
|
||||||
Runnable standalone (no Claude Code needed). For each item, do the action and record the outcome on its `Result:` line as PASS / FAIL / SKIP plus any notes. Scope the pass to what changed, but always run **Core** and the clean-load check.
|
Runnable standalone (no Claude Code needed). For each item, do the action and record the outcome on its `Result:` line as PASS / FAIL / SKIP plus any notes. Scope the pass to what changed, but always run **Core** and the clean-load check.
|
||||||
|
|
||||||
|
|
@ -119,7 +119,7 @@ Keep this file current: when a feature is added or changed, add or update its it
|
||||||
- [ ] Settings "Auto-ingest folders": Add opens the rule popup; the template dropdown lists only newFile templates; rules list with enable toggle / Edit / Delete. Result: ____
|
- [ ] Settings "Auto-ingest folders": Add opens the rule popup; the template dropdown lists only newFile templates; rules list with enable toggle / Edit / Delete. Result: ____
|
||||||
- [ ] `Process auto-ingest folders` processes each audio file in an enabled folder with that rule's template, one at a time, behind a sticky Notice with Cancel. Result: ____
|
- [ ] `Process auto-ingest folders` processes each audio file in an enabled folder with that rule's template, one at a time, behind a sticky Notice with Cancel. Result: ____
|
||||||
- [ ] On success each recording is moved to the attachments location and its note's `![[embed]]` still resolves; a re-run does not reprocess it. Result: ____
|
- [ ] On success each recording is moved to the attachments location and its note's `![[embed]]` still resolves; a re-run does not reprocess it. Result: ____
|
||||||
- [ ] A file that fails stays in the ingest folder and is retried next run; the summary Notice reports processed/failed counts **and** a second sticky Notice lists the concrete failure reason per file (first 5, then "…and N more"). Result: ____
|
- [ ] A file that fails stays in the ingest folder and is retried next run; the summary Notice reports processed/failed counts **and** a second sticky Notice lists the concrete failure reason per file (first 5, then "…and N more"). Result: ____
|
||||||
- [ ] **Ingest folder == recordings/attachments folder** is caught UP FRONT: setting a rule's folder to the same folder recordings are stored in (either the ReWrite Attachments folder, or Obsidian's own attachment folder when ReWrite's is blank) skips the rule with a clear "this folder is also where recordings are stored; point the rule at a different folder" Notice BEFORE any note is created. Result: ____
|
- [ ] **Ingest folder == recordings/attachments folder** is caught UP FRONT: setting a rule's folder to the same folder recordings are stored in (either the ReWrite Attachments folder, or Obsidian's own attachment folder when ReWrite's is blank) skips the rule with a clear "this folder is also where recordings are stored; point the rule at a different folder" Notice BEFORE any note is created. Result: ____
|
||||||
- [ ] **Move-after-success failure**: if the note is created but the recording can't be moved out, the error says the note was created and to move/delete the source manually (so it isn't reprocessed). Result: ____
|
- [ ] **Move-after-success failure**: if the note is created but the recording can't be moved out, the error says the note was created and to move/delete the source manually (so it isn't reprocessed). Result: ____
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
---
|
---
|
||||||
name: release-checklist
|
name: release-checklist
|
||||||
description: Run the ReWrite (Voice Notes) plugin's pre-release verification. Use before tagging a release, or when the user asks to smoke-test / verify the plugin in a real vault. Runs the automated pre-checks (build, lint, test, and the advisory local reviews), installs a fresh build into a test vault, then walks the feature-by-feature CHECKLIST.md so the human effort goes into clicking through features rather than copying files.
|
description: Run the ReWrite (Voice Notes) plugin's release verification. Use before cutting a stable release, or when the user asks to smoke-test / verify the plugin in a real vault. Runs the automated pre-checks (build, lint, test, and the advisory local reviews), installs the published -alpha/-beta prerelease artifact (or a fresh local build when no prerelease exists yet) into a test vault, then walks the feature-by-feature CHECKLIST.md so the human effort goes into clicking through features rather than copying files.
|
||||||
---
|
---
|
||||||
|
|
||||||
# Release checklist
|
# Release checklist
|
||||||
|
|
||||||
This plugin has no headless UI test harness, so event-wiring and UI-lifecycle bugs are invisible to `npm run build` / `npm run lint` / `npm test`. This skill closes that gap: it runs every automated check available, installs a fresh build into a test Obsidian vault, then drives a human through a feature-by-feature manual pass.
|
This plugin has no headless UI test harness, so event-wiring and UI-lifecycle bugs are invisible to `npm run build` / `npm run lint` / `npm test`. This skill closes that gap: it runs every automated check available, installs the artifact under test into a test Obsidian vault (the published `-alpha`/`-beta` prerelease for a release gate; a fresh local build for mid-development iteration), then drives a human through a feature-by-feature manual pass.
|
||||||
|
|
||||||
It is the successor to the stale "Testing Checklist" in `obsidian-voice-notes-spec.md` and the token "manual smoke test" step in `docs/RELEASING.md`. The mechanics of each tool are in `docs/DEV_TOOLING.md`; the tag/push release steps are in `docs/RELEASING.md`. This skill is the front door that sequences them.
|
It is the successor to the stale "Testing Checklist" in `obsidian-voice-notes-spec.md` and the token "manual smoke test" step in `docs/RELEASING.md`. The mechanics of each tool are in `docs/DEV_TOOLING.md`; the tag/push release steps are in `docs/RELEASING.md`. This skill is the front door that sequences them.
|
||||||
|
|
||||||
|
|
@ -19,22 +19,28 @@ npm run review # advisory local code review of the working-tree diff
|
||||||
npm run review:docs # advisory local doc-consistency review
|
npm run review:docs # advisory local doc-consistency review
|
||||||
```
|
```
|
||||||
|
|
||||||
- `build`/`lint`/`test` are release blockers if they fail.
|
- `build`/`lint`/`test` are release blockers if they fail. `npm run lint` is configured to mirror the Obsidian community-review bot for the classes that previously slipped through to submission: the type-checked `@typescript-eslint` rules (`no-deprecated` / `no-unsafe-*` / `no-unnecessary-type-assertion`), `no-unsupported-api` (an Obsidian API newer than `manifest.json`'s `minAppVersion`), and `noInlineConfig` (an `eslint-disable` can no longer silence a rule). If lint fails on one of these, it is doing its job; do not work around it with a disable comment. See RELEASING.md's guideline-conflict checklist.
|
||||||
- The two `review` runs are **advisory** (always exit 0). Read the reports in `docs/claude-scratch/local-review-<mode>-report.md` and surface anything real. They will not block, and they are imperfect (local, quantized model), so treat findings as a first-pass filter, not a gate.
|
- The two `review` runs are **advisory** (always exit 0). Read the reports in `docs/claude-scratch/local-review-<mode>-report.md` and surface anything real. They will not block, and they are imperfect (local, quantized model), so treat findings as a first-pass filter, not a gate.
|
||||||
- **If a review report says "(no findings returned)" or looks empty/garbled**, it is almost always a model/tooling issue, not a clean bill of health — see `docs/DEV_TOOLING.md` Gotchas (reasoning models need thinking disabled; the context must fit the prompt; a huge diff truncates). Fix the tooling or note it; do not read empty output as "no problems."
|
- **If a review report says "(no findings returned)" or looks empty/garbled**, it is almost always a model/tooling issue, not a clean bill of health — see `docs/DEV_TOOLING.md` Gotchas (reasoning models need thinking disabled; the context must fit the prompt; a huge diff truncates). Fix the tooling or note it; do not read empty output as "no problems."
|
||||||
|
|
||||||
## Phase 2 — Setup check
|
## Phase 2 — Setup check
|
||||||
|
|
||||||
1. Confirm `dev-tools.config.json` exists at the repo root with a non-empty `releaseVault.vaultPath`. If not, tell the user to copy `dev-tools.config.example.json` to `dev-tools.config.json` and fill it in. A dedicated **scratch** vault is ideal; a real vault is acceptable because `release:prep` only overwrites the three build artifacts (see Phase 3), but never point it anywhere you'd mind a plugin reload.
|
1. Confirm `dev-tools.config.json` exists at the repo root with a non-empty `releaseVault.vaultPath`. If not, tell the user to copy `dev-tools.config.example.json` to `dev-tools.config.json` and fill it in. A dedicated **scratch** vault is ideal; a real vault is acceptable because the install only overwrites the three build artifacts (see Phase 3), but never point it anywhere you'd mind a plugin reload.
|
||||||
2. Confirm the working tree holds exactly what the user intends to ship. For a real release you also want the version already bumped (`npm version <patch|minor|major> --no-git-tag-version`) so the manual pass exercises the actual versioned artifact **before** you commit and tag — do not tag first.
|
2. Confirm the working tree holds exactly what the user intends to ship, and that master carries **no version bump**: per `docs/RELEASING.md`, all testing happens on published `-alpha`/`-beta` prerelease artifacts while `manifest.json` stays at the last stable version. The bump + bare tag come only after this skill's go decision.
|
||||||
|
3. Identify the prerelease to test: the newest `-alpha`/`-beta` tag whose commit matches what the user intends to ship (`gh release list`). If the tree has moved past the latest prerelease, cut a new suffixed tag first (see RELEASING.md) — do not sign off on an artifact that doesn't match master.
|
||||||
|
|
||||||
## Phase 3 — Build and install
|
## Phase 3 — Install the artifact under test
|
||||||
|
|
||||||
|
**Release gate (a prerelease exists):** install the published, attested prerelease assets into the vault — that exact build is what the stable release will re-version:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm run release:prep
|
gh release download <tag> --repo WiseGuru/ReWrite-Voice-Notes --clobber \
|
||||||
|
--dir "<vaultPath>/.obsidian/plugins/rewrite-voice-notes"
|
||||||
```
|
```
|
||||||
|
|
||||||
This runs `npm run build`, then copies `main.js` / `manifest.json` / `styles.css` into `<vaultPath>/.obsidian/plugins/rewrite-voice-notes/`. It fails loudly (non-zero exit) on a build error or a bad vault path; surface any failure and stop. It **only overwrites those three files** — `data.json` and `secrets.json.nosync` in the target folder are left untouched, so a re-install keeps the user's settings and keys (this is why running against a vault with real data is safe).
|
**Local iteration (no prerelease yet, or mid-development):** `npm run release:prep` builds the working tree and copies the three files into the same folder. Fine for finding bugs fast; the final pass before a stable tag should still run against the prerelease artifact.
|
||||||
|
|
||||||
|
Both paths **only overwrite `main.js` / `manifest.json` / `styles.css`** — `data.json` and `secrets.json.nosync` in the target folder are left untouched, so a re-install keeps the user's settings and keys (this is why running against a vault with real data is safe). `release:prep` fails loudly (non-zero exit) on a build error or a bad vault path; surface any failure and stop.
|
||||||
|
|
||||||
Then tell the user to open the vault and reload the plugin (Settings -> Community plugins -> toggle it off and on, or reload Obsidian), and confirm it loads with no errors in the developer console. **A clean load is itself the first checklist item** — it is the exact step that would have caught the regression that motivated this tooling.
|
Then tell the user to open the vault and reload the plugin (Settings -> Community plugins -> toggle it off and on, or reload Obsidian), and confirm it loads with no errors in the developer console. **A clean load is itself the first checklist item** — it is the exact step that would have caught the regression that motivated this tooling.
|
||||||
|
|
||||||
|
|
@ -49,6 +55,6 @@ Open `CHECKLIST.md` (in this skill folder) and walk it area by area. For each it
|
||||||
|
|
||||||
## Phase 5 — Summary and hand-off to release
|
## Phase 5 — Summary and hand-off to release
|
||||||
|
|
||||||
Save the filled-out run to `docs/claude-scratch/release-checklist-<version>.md` (read `<version>` from `manifest.json`; that folder is gitignored). Then give a clear **go / no-go**: list every FAIL with its symptom, note any SKIP and why, and state plainly whether the build is releasable. Do not soften a FAIL into a pass.
|
Save the filled-out run to `docs/claude-scratch/release-checklist-<version>.md` (name it after the tag under test, e.g. `1.3.0-alpha`; when testing a local `release:prep` build instead, use `manifest.json`'s version. The folder is gitignored). Then give a clear **go / no-go**: list every FAIL with its symptom, note any SKIP and why, and state plainly whether the build is releasable. Do not soften a FAIL into a pass.
|
||||||
|
|
||||||
On a **go**, the actual release (commit the bump + rolled `docs/ROADMAP.md`, tag the bare version, push, let CI build the attested assets) follows `docs/RELEASING.md` — this skill stops at the go/no-go decision; it does not tag or push.
|
On a **go**, the actual release (bump `npm version <patch|minor|major> --no-git-tag-version`, commit the bump + rolled `docs/ROADMAP.md`, tag the bare version, push, let CI build the attested assets) follows `docs/RELEASING.md`. The bump commit (plus doc-only commits) is the only permitted delta between the tested prerelease and the stable tag; any artifact-affecting change means a new prerelease round. This skill stops at the go/no-go decision; it does not tag or push.
|
||||||
|
|
|
||||||
32
.github/workflows/release.yml
vendored
32
.github/workflows/release.yml
vendored
|
|
@ -2,6 +2,10 @@ name: Release
|
||||||
|
|
||||||
# Build, attest provenance, and publish release assets when a version tag is pushed.
|
# Build, attest provenance, and publish release assets when a version tag is pushed.
|
||||||
# Obsidian tags are bare versions with no leading "v" (e.g. 1.0.0).
|
# Obsidian tags are bare versions with no leading "v" (e.g. 1.0.0).
|
||||||
|
# A tag carrying a pre-release suffix (a "-", e.g. 1.3.0-alpha / 1.3.0-beta.1) is
|
||||||
|
# published as a GitHub *prerelease*: GitHub's "latest release" (what Obsidian's
|
||||||
|
# updater reads) excludes prereleases, so the official channel never sees it, while
|
||||||
|
# BRAT / manual install still can. See docs/RELEASING.md.
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
tags:
|
tags:
|
||||||
|
|
@ -28,6 +32,33 @@ jobs:
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: npm ci
|
run: npm ci
|
||||||
|
|
||||||
|
# A tag with a "-" (e.g. 1.3.0-alpha) is a pre-release channel.
|
||||||
|
- name: Determine release channel
|
||||||
|
id: channel
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
if [[ "${{ github.ref_name }}" == *-* ]]; then
|
||||||
|
echo "prerelease=true" >> "$GITHUB_OUTPUT"
|
||||||
|
else
|
||||||
|
echo "prerelease=false" >> "$GITHUB_OUTPUT"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Master's manifest.json stays at the last stable version during alpha/beta
|
||||||
|
# testing, so stamp the tag's version into the *published* manifest only.
|
||||||
|
# (Stable tags publish manifest.json as committed; this step is skipped.)
|
||||||
|
- name: Stamp pre-release version into manifest
|
||||||
|
if: steps.channel.outputs.prerelease == 'true'
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
TAG: ${{ github.ref_name }}
|
||||||
|
run: |
|
||||||
|
node -e '
|
||||||
|
const fs = require("fs");
|
||||||
|
const m = JSON.parse(fs.readFileSync("manifest.json", "utf8"));
|
||||||
|
m.version = process.env.TAG;
|
||||||
|
fs.writeFileSync("manifest.json", JSON.stringify(m, null, "\t"));
|
||||||
|
'
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
run: npm run build
|
run: npm run build
|
||||||
|
|
||||||
|
|
@ -44,6 +75,7 @@ jobs:
|
||||||
with:
|
with:
|
||||||
tag_name: ${{ github.ref_name }}
|
tag_name: ${{ github.ref_name }}
|
||||||
name: ${{ github.ref_name }}
|
name: ${{ github.ref_name }}
|
||||||
|
prerelease: ${{ steps.channel.outputs.prerelease }}
|
||||||
files: |
|
files: |
|
||||||
main.js
|
main.js
|
||||||
manifest.json
|
manifest.json
|
||||||
|
|
|
||||||
19
CLAUDE.md
19
CLAUDE.md
|
|
@ -59,7 +59,7 @@ npm run release:prep # build + copy main.js/manifest.json/styles.css into a scr
|
||||||
|
|
||||||
Verification is `npm run build && npm run lint && npm test` (CI parity) plus the manual feature pass driven by the **`release-checklist` skill** ([.claude/skills/release-checklist/](.claude/skills/release-checklist/); see [docs/DEV_TOOLING.md](docs/DEV_TOOLING.md)), which replaced the stale checklist in [obsidian-voice-notes-spec.md](obsidian-voice-notes-spec.md).
|
Verification is `npm run build && npm run lint && npm test` (CI parity) plus the manual feature pass driven by the **`release-checklist` skill** ([.claude/skills/release-checklist/](.claude/skills/release-checklist/); see [docs/DEV_TOOLING.md](docs/DEV_TOOLING.md)), which replaced the stale checklist in [obsidian-voice-notes-spec.md](obsidian-voice-notes-spec.md).
|
||||||
|
|
||||||
To publish a new version, follow [docs/RELEASING.md](docs/RELEASING.md) (version bump + bare-version tag + CI-built, attested assets; do not hand-upload).
|
To publish a new version, follow [docs/RELEASING.md](docs/RELEASING.md). The flow is alpha/beta-first: tag `<next-version>-alpha` (no version bump on master), test the published prerelease artifact in a real vault via the `release-checklist` skill, iterate with further suffixed tags as needed, and only on a green pass bump the version + push the bare-version tag (CI-built, attested assets; do not hand-upload). The bump commit (plus doc-only commits) is the only permitted delta between the tested prerelease and the stable tag; artifact-affecting changes mean a new prerelease round.
|
||||||
|
|
||||||
CI ([.github/workflows/lint.yml](.github/workflows/lint.yml)) runs `npm ci`, `npm run build`, `npm run lint`, and `npm test` on Node 20.x and 22.x for every push/PR.
|
CI ([.github/workflows/lint.yml](.github/workflows/lint.yml)) runs `npm ci`, `npm run build`, `npm run lint`, and `npm test` on Node 20.x and 22.x for every push/PR.
|
||||||
|
|
||||||
|
|
@ -78,8 +78,8 @@ Two things make `src/` runnable under Vitest even though it targets the Obsidian
|
||||||
- Entry: [src/main.ts](src/main.ts) → bundled to `./main.js` at repo root (the file Obsidian loads). Kept minimal: settings load, ribbon icon, two commands, settings tab, plus a single `activeQuickRecord` ref so the Quick Record command can toggle.
|
- Entry: [src/main.ts](src/main.ts) → bundled to `./main.js` at repo root (the file Obsidian loads). Kept minimal: settings load, ribbon icon, two commands, settings tab, plus a single `activeQuickRecord` ref so the Quick Record command can toggle.
|
||||||
- Bundler: [esbuild.config.mjs](esbuild.config.mjs). `obsidian`, `electron`, all `@codemirror/*`, all `@lezer/*`, and Node built-ins are marked `external`. Never import other runtime deps without bundling them in.
|
- Bundler: [esbuild.config.mjs](esbuild.config.mjs). `obsidian`, `electron`, all `@codemirror/*`, all `@lezer/*`, and Node built-ins are marked `external`. Never import other runtime deps without bundling them in.
|
||||||
- Release artifacts are `main.js`, `manifest.json`, and `styles.css` at the repo root. Do not commit the generated `main.js`.
|
- Release artifacts are `main.js`, `manifest.json`, and `styles.css` at the repo root. Do not commit the generated `main.js`.
|
||||||
- TypeScript config ([tsconfig.json](tsconfig.json)) is strict: `noImplicitAny`, `strictNullChecks`, `noImplicitReturns`, `noUncheckedIndexedAccess`, `useUnknownInCatchVariables`, `baseUrl: "src"`. Target ES6, module ESNext, lib DOM + ES5/6/7 only. No Node lib, so don't reach for Node APIs in plugin code.
|
- TypeScript config ([tsconfig.json](tsconfig.json)) is strict: `noImplicitAny`, `strictNullChecks`, `noImplicitReturns`, `noUncheckedIndexedAccess`, `useUnknownInCatchVariables`, `baseUrl: "src"`. Target ES6, module ESNext, lib DOM + ES2019, `types: []`, `skipLibCheck`. `lib` must stay matched to the runtime APIs the code actually uses (ES2019 covers `Object.entries`/`values`/`fromEntries`, `padStart`, `Promise.finally`; raise it deliberately if you adopt a newer API — Obsidian's WebViews support far newer). `types: []` is load-bearing: without it tsc auto-includes the test suite's `@types/node`, which silently supplied the ES2017+ types the old ES7 lib lacked, and the mismatch surfaced only as `no-unsafe-*` warnings from the Obsidian review bot (see the "Review-bot type environment" gotcha). No Node lib or types, so don't reach for Node APIs in plugin code.
|
||||||
- ESLint ([eslint.config.mts](eslint.config.mts)) layers `eslint-plugin-obsidianmd`'s recommended rules on top of `typescript-eslint`. These rules encode Obsidian-specific correctness checks; respect them rather than disabling.
|
- ESLint ([eslint.config.mts](eslint.config.mts)) layers `eslint-plugin-obsidianmd`'s recommended rules on top of `typescript-eslint`. These rules encode Obsidian-specific correctness checks; respect them rather than disabling. The config is deliberately tuned to **mirror the Obsidian community-review bot** for the classes that once slipped through to submission (see [docs/RELEASING.md](docs/RELEASING.md)): it enables the type-checked `@typescript-eslint` rules (`no-deprecated`, `no-unsafe-*`, `no-unnecessary-type-assertion`) scoped to `src/`; cherry-picks `no-unsupported-api` from a `0.4.1` alias of the plugin (`obsidianmd-latest`, since the pinned `0.1.9` base predates that rule) so an Obsidian API newer than `manifest.json`'s `minAppVersion` fails lint locally; and sets `noInlineConfig` so an `eslint-disable` comment can never silence a rule (the bot forbids disables). The base plugin stays pinned at `0.1.9` because `0.4.x`'s recommended `ui/sentence-case` is over-aggressive and diverges from what the bot enforces; only the single `no-unsupported-api` rule is taken from the alias. To reach an API outside the typed/deprecated surface, use a local structural type-alias (e.g. `ScriptProcessorNodeLike` in [src/realtime/pcm.ts](src/realtime/pcm.ts)), never a disable comment.
|
||||||
|
|
||||||
## Source layout
|
## Source layout
|
||||||
|
|
||||||
|
|
@ -97,6 +97,7 @@ src/
|
||||||
├── audio-transcode.ts # WebAudio decode + resample to 16 kHz mono PCM WAV (shared by whisper-local and mistral-voxtral)
|
├── audio-transcode.ts # WebAudio decode + resample to 16 kHz mono PCM WAV (shared by whisper-local and mistral-voxtral)
|
||||||
├── pipeline.ts # transcribe → cleanup → insert orchestrator
|
├── pipeline.ts # transcribe → cleanup → insert orchestrator
|
||||||
├── insert.ts # cursor/newFile/append + {{date}}/{{time}} expansion
|
├── insert.ts # cursor/newFile/append + {{date}}/{{time}} expansion
|
||||||
|
├── time.ts # formatMoment: Obsidian's bundled moment behind a narrow structural alias (moment's types don't resolve in the review bot's environment; never call moment directly)
|
||||||
├── ingest.ts # Auto-ingest batch runner: scan rule folders, pipeline each audio file, move on success
|
├── ingest.ts # Auto-ingest batch runner: scan rule folders, pipeline each audio file, move on success
|
||||||
├── whisper-host.ts # Spawns/stops a user-supplied whisper-server child process (desktop only; Phase B auto-start + idle-stop)
|
├── whisper-host.ts # Spawns/stops a user-supplied whisper-server child process (desktop only; Phase B auto-start + idle-stop)
|
||||||
├── templates-folder.ts # Load templates from a vault folder + populate it with the 10 defaults
|
├── templates-folder.ts # Load templates from a vault folder + populate it with the 10 defaults
|
||||||
|
|
@ -148,7 +149,7 @@ src/
|
||||||
1. **Persist audio** (`audio` source only): writes the raw `Blob` to the vault via [src/audio-persist.ts](src/audio-persist.ts) before transcription, so the user keeps the recording even if later stages fail. Path resolution: when `settings.attachmentsFolderPath` is set, the file goes under that folder with manual de-collision (`-1`, `-2`, ...); when empty, the path comes from `app.fileManager.getAvailablePathForAttachment(filename)`, which respects Obsidian's own attachments setting. Filename is `ReWrite-YYYY-MM-DD-HHmmss.<ext>` with the extension derived from the blob's mime type (`webm` / `m4a` / `ogg` / `wav` / `mp3`, default `webm`). Failure is non-fatal: a Notice fires and transcription proceeds. The resolved path is later prepended to the cleaned output as `![[<path>]]\n\n` before insertion.
|
1. **Persist audio** (`audio` source only): writes the raw `Blob` to the vault via [src/audio-persist.ts](src/audio-persist.ts) before transcription, so the user keeps the recording even if later stages fail. Path resolution: when `settings.attachmentsFolderPath` is set, the file goes under that folder with manual de-collision (`-1`, `-2`, ...); when empty, the path comes from `app.fileManager.getAvailablePathForAttachment(filename)`, which respects Obsidian's own attachments setting. Filename is `ReWrite-YYYY-MM-DD-HHmmss.<ext>` with the extension derived from the blob's mime type (`webm` / `m4a` / `ogg` / `wav` / `mp3`, default `webm`). Failure is non-fatal: a Notice fires and transcription proceeds. The resolved path is later prepended to the cleaned output as `![[<path>]]\n\n` before insertion.
|
||||||
2. **Transcribe**: `audio` → `createTranscriptionProvider(profile.transcriptionProvider).transcribe(blob, config)`. Skipped when the source is `paste` or `text` (input passes through). Just before dispatching, `validateRecording(blobSize, durationMs, providerId)` from [src/transcription/limits.ts](src/transcription/limits.ts) throws a friendly per-provider error if the recording exceeds the provider's documented byte or duration cap. Because validation runs after `persist-audio`, the user keeps the saved file and can switch providers + reprocess from the vault.
|
2. **Transcribe**: `audio` → `createTranscriptionProvider(profile.transcriptionProvider).transcribe(blob, config)`. Skipped when the source is `paste` or `text` (input passes through). Just before dispatching, `validateRecording(blobSize, durationMs, providerId)` from [src/transcription/limits.ts](src/transcription/limits.ts) throws a friendly per-provider error if the recording exceeds the provider's documented byte or duration cap. Because validation runs after `persist-audio`, the user keeps the saved file and can switch providers + reprocess from the vault.
|
||||||
3. **Cleanup**: `createLLMProvider(profile.llmProvider).complete(systemPrompt, transcript, config)`. The system prompt is the template prompt, optionally augmented with an `## Ad-hoc instructions` block when the wake-name scan ([src/wake-name.ts](src/wake-name.ts)) extracts directives from the transcript, a `## Context` block when `params.contextHint` is non-empty (see Context hint below), a `## Known nouns` block when `plugin.knownNouns` is non-empty (see Assistant prompt and Known nouns sections below), and a `## Note properties` block when the template declares `noteProperties` (see Note properties below). `cleanupTranscript` returns `{ body, properties, title? }`: when the template declares properties OR sets `titleFromContent`, the LLM is asked to emit a leading ```` ```yaml ```` block which `extractFromBlock` parses off the front (the rest is the body); otherwise `properties` is `{}`, `title` is undefined, and `body` is the full output. On error, the provider error propagates unchanged; nothing is written to the clipboard (the earlier clipboard fallback was removed as a sensitive-content exposure, and for `audio` sources the persisted recording is the recovery path).
|
3. **Cleanup**: `createLLMProvider(profile.llmProvider).complete(systemPrompt, transcript, config)`. The system prompt is the template prompt, optionally augmented with an `## Ad-hoc instructions` block when the wake-name scan ([src/wake-name.ts](src/wake-name.ts)) extracts directives from the transcript, a `## Context` block when `params.contextHint` is non-empty (see Context hint below), a `## Known nouns` block when `plugin.knownNouns` is non-empty (see Assistant prompt and Known nouns sections below), and a `## Note properties` block when the template declares `noteProperties` (see Note properties below). `cleanupTranscript` returns `{ body, properties, title? }`: when the template declares properties OR sets `titleFromContent`, the LLM is asked to emit a leading ```` ```yaml ```` block which `extractFromBlock` parses off the front (the rest is the body); otherwise `properties` is `{}`, `title` is undefined, and `body` is the full output. On error, the provider error propagates unchanged; nothing is written to the clipboard (the earlier clipboard fallback was removed as a sensitive-content exposure, and for `audio` sources the persisted recording is the recovery path).
|
||||||
4. **Insert**: `src/insert.ts` routes to `cursor` / `newFile` / `append` per the template. `cursor` falls back to `append` when no editor is active; `append` falls back to `newFile` when no markdown file exists. `{{date}}` / `{{time}}` in filename templates expand via Obsidian's `moment`, and `{{title}}` expands to the LLM-generated title (empty when no title). The modal's per-invocation Destination control overrides `insertMode` / `newFileFolder` / `newFileNameTemplate` via `PipelineParams.destinationOverride`; the override is shallow-merged onto a copy of the template before the insert call, so the template file on disk is never mutated. Extracted `properties` are threaded via `InsertParams.properties` and written into the note's frontmatter via `app.fileManager.processFrontMatter` in `newFile` mode only (see Note properties below). An extracted `title` (from `titleFromContent`) is threaded via `InsertParams.title` and shapes the `newFile` filename via the `{{title}}` token / whole-name replacement (see Note title below).
|
4. **Insert**: `src/insert.ts` routes to `cursor` / `newFile` / `append` per the template. `cursor` falls back to `append` when no editor is active; `append` falls back to `newFile` when no markdown file exists. `{{date}}` / `{{time}}` in filename templates expand via Obsidian's `moment` (reached through `formatMoment` in [src/time.ts](src/time.ts)), and `{{title}}` expands to the LLM-generated title (empty when no title). The modal's per-invocation Destination control overrides `insertMode` / `newFileFolder` / `newFileNameTemplate` via `PipelineParams.destinationOverride`; the override is shallow-merged onto a copy of the template before the insert call, so the template file on disk is never mutated. Extracted `properties` are threaded via `InsertParams.properties` and written into the note's frontmatter via `app.fileManager.processFrontMatter` in `newFile` mode only (see Note properties below). An extracted `title` (from `titleFromContent`) is threaded via `InsertParams.title` and shapes the `newFile` filename via the `{{title}}` token / whole-name replacement (see Note title below).
|
||||||
|
|
||||||
The pipeline accepts an `AbortSignal` (forwarded to providers) and is consumed by [src/ui/modal.ts](src/ui/modal.ts), [src/ui/quick-record.ts](src/ui/quick-record.ts), and [src/ui/text-source.ts](src/ui/text-source.ts) (the `runTextPipeline` helper for command + editor-menu entry points). Every caller passes the plugin itself as `host: PipelineHost`; `PipelineHost` is a narrow interface ({ `assistantPrompt`, `knownNouns` }) so `cleanupTranscript` can read the loaded vault content without importing `ReWritePlugin` (which would form a circular dep through the UI layer).
|
The pipeline accepts an `AbortSignal` (forwarded to providers) and is consumed by [src/ui/modal.ts](src/ui/modal.ts), [src/ui/quick-record.ts](src/ui/quick-record.ts), and [src/ui/text-source.ts](src/ui/text-source.ts) (the `runTextPipeline` helper for command + editor-menu entry points). Every caller passes the plugin itself as `host: PipelineHost`; `PipelineHost` is a narrow interface ({ `assistantPrompt`, `knownNouns` }) so `cleanupTranscript` can read the loaded vault content without importing `ReWritePlugin` (which would form a circular dep through the UI layer).
|
||||||
|
|
||||||
|
|
@ -348,10 +349,18 @@ Per [.editorconfig](.editorconfig): tabs (width 4), LF, UTF-8, final newline. Ma
|
||||||
- **Keep [src/main.ts](src/main.ts) minimal**: only plugin lifecycle, command registration, settings tab registration. Feature logic belongs in dedicated modules.
|
- **Keep [src/main.ts](src/main.ts) minimal**: only plugin lifecycle, command registration, settings tab registration. Feature logic belongs in dedicated modules.
|
||||||
- **Defer heavy work**: no long tasks in `onload`. Providers/recorders lazy-init when first used.
|
- **Defer heavy work**: no long tasks in `onload`. Providers/recorders lazy-init when first used.
|
||||||
- **Network policy**: provider calls go to user-configured endpoints with user-provided keys. No telemetry, no auto-update of plugin code, no `fetch`+`eval`.
|
- **Network policy**: provider calls go to user-configured endpoints with user-provided keys. No telemetry, no auto-update of plugin code, no `fetch`+`eval`.
|
||||||
- **Releases**: GitHub release tag must exactly match `manifest.json`'s `version` (no leading `v`). Attach `main.js`, `manifest.json`, `styles.css` as individual binary assets (not zipped). This is automated by [.github/workflows/release.yml](.github/workflows/release.yml): pushing a version tag builds the bundle, runs `actions/attest-build-provenance` (GitHub artifact attestations for provenance), and publishes the assets via `softprops/action-gh-release`. To cut a release, push a tag named exactly the version; do not hand-upload assets (that loses attestation). **Before publishing any release, follow [docs/RELEASING.md](docs/RELEASING.md)** for the full step-by-step (version bump, tag, push, verify) and the guideline-conflict checklist that keeps the Obsidian community review green.
|
- **Releases**: GitHub release tag must exactly match `manifest.json`'s `version` (no leading `v`). Attach `main.js`, `manifest.json`, `styles.css` as individual binary assets (not zipped). This is automated by [.github/workflows/release.yml](.github/workflows/release.yml): pushing a version tag builds the bundle, runs `actions/attest-build-provenance` (GitHub artifact attestations for provenance), and publishes the assets via `softprops/action-gh-release`. To cut a release, push a tag named exactly the version; do not hand-upload assets (that loses attestation). A tag carrying a pre-release suffix (any `-`, e.g. `1.3.0-alpha`/`1.3.0-beta`) is instead published as a GitHub **prerelease**: the workflow stamps the tag's version into the *published* `manifest.json` only (master's copy is untouched, so the official updater and community review never see it). This channel is where ALL release testing happens — every stable release is a tested prerelease plus only the version-bump commit (and doc-only changes). See "The alpha/beta channel" section of RELEASING.md. **Before publishing any release, follow [docs/RELEASING.md](docs/RELEASING.md)** for the full step-by-step (prerelease tag, feature pass, bump, bare tag, verify) and the guideline-conflict checklist that keeps the Obsidian community review green.
|
||||||
|
|
||||||
## Gotchas
|
## Gotchas
|
||||||
|
|
||||||
|
### Review-bot type environment (lint parity)
|
||||||
|
|
||||||
|
The Obsidian community-review bot runs the same typed ESLint rules our config mirrors, but in a **different type environment**: it uses this repo's `tsconfig.json` while lacking parts of our `node_modules` (`@types/node`, `moment`'s typings) and possibly running an older TypeScript. Anything error-typed there trips `no-unsafe-*` at every use, even though local lint is clean — all ~30 of the 1.2.1 review warnings were this (see [docs/DEVCONFLICTS.md](docs/DEVCONFLICTS.md) finding 10 and the parity section of [docs/RELEASING.md](docs/RELEASING.md)). Three standing invariants:
|
||||||
|
|
||||||
|
- **Keep `tsconfig.json`'s `lib` matched to the APIs the code uses; `types: []` enforces it.** With `types: []`, an API newer than the declared lib fails `npm run build` locally instead of being silently typed by the test suite's auto-included `@types/node` (which is exactly how ES2017+ usage slipped past an ES2016 lib before 1.2.1).
|
||||||
|
- **Never call `moment` (re-exported by `obsidian`) directly; go through `formatMoment` in [src/time.ts](src/time.ts).** `obsidian` itself types fine in the bot's environment, but its `moment` export's type comes from the `moment` package, which does not resolve there, so direct calls are error-typed. The wrapper reaches it through a narrow structural alias (same pattern as `ScriptProcessorNodeLike`).
|
||||||
|
- **Avoid TS-version-dependent type assertions.** On TS 5.7+ a value declared `Uint8Array` is `ArrayBufferLike`-backed and needs `as BufferSource` for WebCrypto params; on older TS that same assertion trips `no-unnecessary-type-assertion`. Write code that needs no assertion on any version: let byte-helper return types be inferred (a `new Uint8Array(...)` infers `ArrayBuffer`-backed on 5.7+), type params that only feed WebCrypto as `BufferSource`, and copy a foreign library's bytes (`new Uint8Array(raw)`) rather than asserting ([src/secrets.ts](src/secrets.ts) does all three). Never write the generic `Uint8Array<ArrayBuffer>` syntax — it is a hard error before TS 5.7. Prefer a type-predicate narrow (`isRecord`) over an `as Record<string, unknown>` assertion where either would do.
|
||||||
|
|
||||||
### HTTP and provider requests
|
### HTTP and provider requests
|
||||||
|
|
||||||
- **`requestUrl` multipart bodies are hand-built.** `requestUrl` does not accept `FormData`. [src/http.ts](src/http.ts) exports `buildMultipart(parts)` which produces a `Uint8Array` with a random boundary; transcription adapters (Whisper, Rev.ai) call into it. If you add a multipart-LLM provider, reuse this rather than reaching for `FormData`.
|
- **`requestUrl` multipart bodies are hand-built.** `requestUrl` does not accept `FormData`. [src/http.ts](src/http.ts) exports `buildMultipart(parts)` which produces a `Uint8Array` with a random boundary; transcription adapters (Whisper, Rev.ai) call into it. If you add a multipart-LLM provider, reuse this rather than reaching for `FormData`.
|
||||||
|
|
|
||||||
|
|
@ -89,6 +89,15 @@ Guideline: "Use `editorCallback` or `editorCheckCallback` for commands requiring
|
||||||
### 9. `app.vault.getFiles()` full-vault scan
|
### 9. `app.vault.getFiles()` full-vault scan
|
||||||
[src/ui/audio-source.ts:18](../src/ui/audio-source.ts#L18) iterates all vault files to filter audio by extension. The guideline's "don't iterate all files" advice is specifically about *finding a file by path* (use `getFileByPath`), which this is not — there is no extension index in the API, so a scan is reasonable. Listed only for completeness; not a real conflict.
|
[src/ui/audio-source.ts:18](../src/ui/audio-source.ts#L18) iterates all vault files to filter audio by extension. The guideline's "don't iterate all files" advice is specifically about *finding a file by path* (use `getFileByPath`), which this is not — there is no extension index in the API, so a scan is reasonable. Listed only for completeness; not a real conflict.
|
||||||
|
|
||||||
|
### 10. 1.2.1 automated-review `no-unsafe-*` warnings (type-environment mismatch)
|
||||||
|
The 1.2.1 submission drew ~30 warnings (`no-unsafe-assignment/call/member-access/argument/return`, `no-unnecessary-type-assertion`) that local `npm run lint` did not reproduce even after the rule mirror ([eslint.config.mts](../eslint.config.mts)) was in place. Investigation showed none of the flagged code was genuinely unsafe; every warning came from the bot's **type environment** differing from local. The bot's typed lint uses the repo's own `tsconfig.json` but not the repo's full `node_modules`, so a value that types cleanly locally can be error-typed there (and an error-typed value trips `no-unsafe-*` at every use). Three sub-causes:
|
||||||
|
|
||||||
|
1. **Declared `lib` was lower than the APIs the code uses.** `tsconfig.json` declared `lib` ES2016 while the source uses `Object.entries`/`values`/`fromEntries`, `String.padStart`, and `Promise.finally` (ES2017–ES2019). Locally, tsc auto-included the test suite's `@types/node`, which silently supplied those types; in the bot's environment they were error-typed (~20 of the warnings).
|
||||||
|
2. **`moment`'s types don't resolve in the bot's environment.** `obsidian` itself types fine there, but its `moment` re-export's type comes from the `moment` package (a transitive dependency), which the bot doesn't have — every direct `moment(...)` call was error-typed (5 warnings across [src/audio-persist.ts](../src/audio-persist.ts), [src/insert.ts](../src/insert.ts), [src/template-guide.ts](../src/template-guide.ts)).
|
||||||
|
3. **TS-version-dependent assertions.** On TS 5.7+ (local, 5.8) a value declared `Uint8Array` is `ArrayBufferLike`-backed and needs `as BufferSource` to satisfy WebCrypto; on the bot's older TS the same assertion is "unnecessary" ([src/secrets.ts](../src/secrets.ts)).
|
||||||
|
|
||||||
|
> **Resolution: Fixed (all three).** `tsconfig.json` now declares `lib: ["DOM", "ES2019"]` with `types: []` (so the `@types/node` masking can never recur — a lib/API mismatch now fails `npm run build` locally) and `skipLibCheck`. All `moment` calls go through `formatMoment` in [src/time.ts](../src/time.ts), a narrow structural alias over Obsidian's bundled moment (same pattern as `ScriptProcessorNodeLike`). The secrets byte helpers were restructured to need no `BufferSource` assertion under any TS version (inferred `ArrayBuffer`-backed return types, a `BufferSource` param, one 32-byte defensive copy of the hash-wasm output), and the two `as Record<string, unknown>` narrows the bot flagged were replaced with an `isRecord` type predicate.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Checked and clean (no conflict found)
|
## Checked and clean (no conflict found)
|
||||||
|
|
|
||||||
|
|
@ -2,45 +2,51 @@
|
||||||
|
|
||||||
How to cut a new release the Obsidian way, without tripping the community-plugin review. Read this before every release.
|
How to cut a new release the Obsidian way, without tripping the community-plugin review. Read this before every release.
|
||||||
|
|
||||||
Releases are automated by [.github/workflows/release.yml](../.github/workflows/release.yml): pushing a version tag builds the bundle, attaches build-provenance attestations, and publishes the GitHub release. Your job is the version bump, the pre-flight checks, and pushing a correctly named tag.
|
Releases are automated by [.github/workflows/release.yml](../.github/workflows/release.yml): pushing a version tag builds the bundle, attaches build-provenance attestations, and publishes the GitHub release. Your job is the pre-flight checks and pushing correctly named tags.
|
||||||
|
|
||||||
|
**Every release rides the alpha/beta channel first.** All testing (the manual feature pass included) happens against published `-alpha`/`-beta` prerelease artifacts; cutting the stable release is then *only* the version bump + bare tag on the same source. Between the last prerelease you tested and the stable tag, nothing that affects the built artifact (`src/`, `styles.css`, `manifest.json` beyond the version, build config, dependencies) may change — doc-only commits may ride along, but any artifact-affecting change means a new prerelease round, not a direct-to-stable commit.
|
||||||
|
|
||||||
## TL;DR
|
## TL;DR
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 0. On master, clean working tree, everything you want shipped is committed.
|
# 1. On master, clean tree, everything you want to ship committed. Do NOT bump yet —
|
||||||
npm run build && npm run lint # must both pass
|
# manifest.json stays at the last stable version throughout testing.
|
||||||
# 0a. Roll docs/ROADMAP.md: move the Unreleased items into a new
|
npm run build && npm run lint && npm test # must all pass
|
||||||
# "### <version> — <YYYY-MM-DD>" block under Released, leave Unreleased empty.
|
# 2. Cut a prerelease of the version you are ABOUT to ship
|
||||||
# 1. Bump version files (no auto commit/tag so we control the message)
|
git tag -a 1.3.0-alpha -m "Pre-release 1.3.0-alpha"
|
||||||
|
git push origin 1.3.0-alpha
|
||||||
|
# 3. Test the PUBLISHED prerelease artifact in a real vault (the release-checklist
|
||||||
|
# skill / CHECKLIST.md — see "The alpha/beta channel" below for install options).
|
||||||
|
# Fixes needed? Commit them, tag 1.3.0-alpha.2 / 1.3.0-beta, re-test. Repeat until green.
|
||||||
|
# 4. Go: roll docs/ROADMAP.md (Unreleased -> "### <version> — <YYYY-MM-DD>" under
|
||||||
|
# Released), then bump and tag the bare version.
|
||||||
npm version patch --no-git-tag-version # or minor / major
|
npm version patch --no-git-tag-version # or minor / major
|
||||||
# 2. Commit the bump (include the rolled roadmap)
|
|
||||||
git add manifest.json package.json package-lock.json versions.json docs/ROADMAP.md
|
git add manifest.json package.json package-lock.json versions.json docs/ROADMAP.md
|
||||||
git commit -m "1.0.1" # use the new version as the subject
|
git commit -m "1.3.0" # use the new version as the subject
|
||||||
# 3. Tag with the BARE version (no leading v) and push
|
git tag -a 1.3.0 -m "Release 1.3.0"
|
||||||
git tag -a 1.0.1 -m "Release 1.0.1"
|
|
||||||
git push origin master
|
git push origin master
|
||||||
git push origin 1.0.1
|
git push origin 1.3.0
|
||||||
# 4. Watch CI, then verify provenance (see Verify below)
|
# 5. Watch CI, then verify provenance (see Verify below)
|
||||||
```
|
```
|
||||||
|
|
||||||
The tag name must equal `manifest.json`'s `version` exactly. `.npmrc` already pins `tag-version-prefix=""`, so `npm version` produces a bare tag too if you ever let it tag directly.
|
The stable tag name must equal `manifest.json`'s `version` exactly (bare, no leading `v`). `.npmrc` already pins `tag-version-prefix=""`, so `npm version` produces a bare tag too if you ever let it tag directly.
|
||||||
|
|
||||||
## Hard rules (Obsidian requirements)
|
## Hard rules (Obsidian requirements)
|
||||||
|
|
||||||
- **No `v` in the tag.** The release tag must match `manifest.json` `version` character-for-character: `1.0.1`, never `v1.0.1`.
|
- **No `v` in the tag.** The release tag must match `manifest.json` `version` character-for-character: `1.0.1`, never `v1.0.1`.
|
||||||
- **Three loose asset files.** `main.js`, `manifest.json`, `styles.css` attached as individual binary assets, never zipped. The workflow does this; do not hand-upload.
|
- **Three loose asset files.** `main.js`, `manifest.json`, `styles.css` attached as individual binary assets, never zipped. The workflow does this; do not hand-upload.
|
||||||
- **A new release needs a new version number.** Obsidian's automated review only registers a change when the version increments. Re-pushing the same version does not count as a new submission. Bump the patch/minor/major rather than overwriting a published version. (To address review feedback, update the repo and publish a new GitHub release with an incremented version.)
|
- **A new release needs a new version number.** Obsidian's automated review only registers a change when the version increments. Re-pushing the same version does not count as a new submission. Bump the patch/minor/major rather than overwriting a published version. (To address review feedback, update the repo and publish a new GitHub release with an incremented version.)
|
||||||
- **Version format is `x.y.z` only.** Semantic Versioning, no pre-release suffixes (no `1.0.0-beta`, no build metadata). The initial release is `1.0.0`.
|
- **Stable version format is `x.y.z` only.** Semantic Versioning, no build metadata. The initial release is `1.0.0`. Pre-release suffixes (`1.3.0-alpha`, `1.3.0-beta.1`) are allowed **only** for the alpha/beta channel below, which is deliberately invisible to the official review/updater; a stable tag never carries a suffix.
|
||||||
- **The directory reads `manifest.json` at the HEAD of your default branch** (`master`), not just the release asset. Keep master's `manifest.json` correct and in sync with the released version.
|
- **The directory reads `manifest.json` at the HEAD of your default branch** (`master`), not just the release asset. Keep master's `manifest.json` correct and in sync with the released version.
|
||||||
- **`minAppVersion` must be >= the highest `@since` of every Obsidian API you call directly** (anything not behind a runtime feature-detect). Check `node_modules/obsidian/obsidian.d.ts` for the `@since` of new APIs. Example from this project: `FileManager.trashFile` is `@since 1.6.6`, which is why `minAppVersion` is `1.6.6` (raised from 1.4.4, which `FileManager.processFrontMatter` `@since 1.4.4` had driven). The `obsidianmd/no-unsupported-api` lint rule flags a direct call newer than the declared floor. Feature-detected APIs (like `app.secretStorage`) do not raise the floor.
|
- **`minAppVersion` must be >= the highest `@since` of every Obsidian API you call directly** (anything not behind a runtime feature-detect). Check `node_modules/obsidian/obsidian.d.ts` for the `@since` of new APIs. Example from this project: `FileManager.trashFile` is `@since 1.6.6`, which is why `minAppVersion` is `1.6.6` (raised from 1.4.4, which `FileManager.processFrontMatter` `@since 1.4.4` had driven). The `obsidianmd/no-unsupported-api` lint rule flags a direct call newer than the declared floor. Feature-detected APIs (like `app.secretStorage`) do not raise the floor.
|
||||||
- **`versions.json` maps plugin version -> minAppVersion.** Our [version-bump.mjs](../version-bump.mjs) only adds a new line when the `minAppVersion` value is not already present (i.e. when the floor actually changes). That is valid: Obsidian reads the latest version straight from the release `manifest.json`, and consults `versions.json` only to find the newest plugin version compatible with an older app. If you raise `minAppVersion`, confirm a new `versions.json` entry was written; if you keep it, no new line is expected.
|
- **`versions.json` maps plugin version -> minAppVersion.** Our [version-bump.mjs](../version-bump.mjs) records an entry for **every** bumped version (it skips only when the version key already exists). It previously added a line only when the `minAppVersion` *value* changed, which silently dropped every release after the first from the compatibility map; that was fixed, so expect one new line per release. Obsidian reads the latest version straight from the release `manifest.json`, and consults `versions.json` only to find the newest plugin version compatible with an older app.
|
||||||
- **Public repo + LICENSE.** The repo must be public to be listed, with a real LICENSE whose copyright holder is correct (this plugin is 0BSD). The README must disclose network use, and `manifest.json` carries `author`, `authorUrl`, and (if you take donations) `fundingUrl`.
|
- **Public repo + LICENSE.** The repo must be public to be listed, with a real LICENSE whose copyright holder is correct (this plugin is 0BSD). The README must disclose network use, and `manifest.json` carries `author`, `authorUrl`, and (if you take donations) `fundingUrl`.
|
||||||
|
|
||||||
## Pre-flight checklist
|
## Pre-flight checklist
|
||||||
|
|
||||||
1. `npm run build` passes (this is `tsc -noEmit` then esbuild production; a type error here is a release blocker).
|
1. `npm run build` passes (this is `tsc -noEmit` then esbuild production; a type error here is a release blocker).
|
||||||
2. `npm run lint` passes with zero warnings, and `npm test` passes. The local `eslint-plugin-obsidianmd` is looser than the official review bot, so also eyeball the conflict checklist below.
|
2. `npm run lint` passes with zero warnings, and `npm test` passes. The local `eslint-plugin-obsidianmd` is looser than the official review bot, so also eyeball the conflict checklist below.
|
||||||
3. Manual feature pass via the **`release-checklist` skill** (`.claude/skills/release-checklist/`), which sequences the whole verification. Its Phase 1 runs the automated pre-checks (`build` / `lint` / `test` plus the advisory `npm run review` and `npm run review:docs` local reviews); then `npm run release:prep` builds and installs `main.js` / `manifest.json` / `styles.css` into your test vault's `.obsidian/plugins/rewrite-voice-notes/` (configure `releaseVault.vaultPath` in `dev-tools.config.json` first — `release:prep` only overwrites those three files, so a vault with real data is safe); then the skill walks `CHECKLIST.md` feature by feature. `CHECKLIST.md` is also runnable standalone by a human without Claude Code. See [DEV_TOOLING.md](DEV_TOOLING.md). Test the actual **versioned** artifact: bump first (`npm version <patch|minor|major> --no-git-tag-version`), then run the pass, then commit + tag.
|
3. Manual feature pass via the **`release-checklist` skill** (`.claude/skills/release-checklist/`), which sequences the whole verification. Its Phase 1 runs the automated pre-checks (`build` / `lint` / `test` plus the advisory `npm run review` and `npm run review:docs` local reviews); then the **published prerelease artifact** is installed into your test vault's `.obsidian/plugins/rewrite-voice-notes/` (download the three assets from the prerelease page, or use BRAT — the install only replaces `main.js` / `manifest.json` / `styles.css`, so a vault with real data is safe); then the skill walks `CHECKLIST.md` feature by feature. `CHECKLIST.md` is also runnable standalone by a human without Claude Code. See [DEV_TOOLING.md](DEV_TOOLING.md). Sign off on the prerelease build, not a local one: `npm run release:prep` (which builds and copies from the working tree) remains the fast path for iterating *before* a prerelease exists, but the pass that gates the release runs against the attested `-alpha`/`-beta` assets, and the stable bump happens only after the go decision.
|
||||||
4. Update docs for any behavioral change ([CLAUDE.md](../CLAUDE.md), the user-facing [`wiki/`](../wiki/) pages, and the [README](../README.md)), per the doc-maintenance rules in CLAUDE.md.
|
4. Update docs for any behavioral change ([CLAUDE.md](../CLAUDE.md), the user-facing [`wiki/`](../wiki/) pages, and the [README](../README.md)), per the doc-maintenance rules in CLAUDE.md.
|
||||||
5. Roll [ROADMAP.md](ROADMAP.md): every item shipping in this release should already have an **Unreleased** entry. Move them into a new `### <version> — <YYYY-MM-DD>` heading at the top of the **Released** archive, and leave `## Unreleased` empty for the next cycle. The version + date must match the tag.
|
5. Roll [ROADMAP.md](ROADMAP.md): every item shipping in this release should already have an **Unreleased** entry. Move them into a new `### <version> — <YYYY-MM-DD>` heading at the top of the **Released** archive, and leave `## Unreleased` empty for the next cycle. The version + date must match the tag.
|
||||||
|
|
||||||
|
|
@ -48,9 +54,13 @@ The tag name must equal `manifest.json`'s `version` exactly. `.npmrc` already pi
|
||||||
|
|
||||||
These are the recurring findings; clear them before tagging. Most are also why the items above exist.
|
These are the recurring findings; clear them before tagging. Most are also why the items above exist.
|
||||||
|
|
||||||
|
**Local lint now mirrors the bot for the classes that previously slipped through** (the 1.2.0 submission failed on three the local lint did not catch). [eslint.config.mts](../eslint.config.mts) enables: the type-checked `@typescript-eslint` rules (`no-deprecated`, the `no-unsafe-*` family, `no-unnecessary-type-assertion`); `no-unsupported-api` (cherry-picked from a `0.4.1` alias of `eslint-plugin-obsidianmd` since our pinned `0.1.9` base lacks it) so a direct Obsidian API newer than `minAppVersion` is caught locally; and `noInlineConfig` so an `eslint-disable` can never silence a rule. So `npm run lint` failing on these now is the point. When the review bot adds a new rule class we do not catch, add it here the same way (prefer cherry-picking one rule from the alias over adopting `0.4.x`'s recommended config wholesale, whose `ui/sentence-case` is over-aggressive and diverges from the bot).
|
||||||
|
|
||||||
|
**The bot's TYPE environment also differs from local, and mirroring the rules alone does not cover that** (the 1.2.1 submission drew ~30 `no-unsafe-*` warnings local lint could not reproduce; see [DEVCONFLICTS.md](DEVCONFLICTS.md) finding 10). The bot lints with the repo's own `tsconfig.json` but not the repo's full `node_modules`: `@types/node` and `moment`'s typings are absent there, and its TypeScript version may lag ours. Anything that is error-typed in that environment trips `no-unsafe-*` at every use. The standing guards: `tsconfig.json` declares `lib` matched to the APIs the code actually uses plus `types: []`, so an API newer than the declared lib fails `npm run build` locally instead of being silently typed by the test suite's `@types/node`; `moment` is only reached through `formatMoment` in [src/time.ts](../src/time.ts) (never called directly from `obsidian`'s re-export); and type assertions whose necessity depends on the TS version (`as BufferSource`) are avoided in favor of code that needs no assertion on any version. If the bot ever flags a `no-unsafe-*` or `no-unnecessary-type-assertion` warning that local lint does not show, suspect a type-resolution difference first: check what supplies the type locally (`npx tsc -noEmit --explainFiles`), rather than assuming the warning is spurious.
|
||||||
|
|
||||||
- **Plugin `id`**: lowercase letters and hyphens only, must not end in `plugin`, must not contain `obsidian`. Locked once published; do not change it. (Ours is `rewrite-voice-notes`.)
|
- **Plugin `id`**: lowercase letters and hyphens only, must not end in `plugin`, must not contain `obsidian`. Locked once published; do not change it. (Ours is `rewrite-voice-notes`.)
|
||||||
- **No newer-than-minAppVersion APIs**: see the `minAppVersion` rule above.
|
- **No newer-than-minAppVersion APIs**: see the `minAppVersion` rule above. Enforced locally by `obsidianmd-latest/no-unsupported-api`.
|
||||||
- **No `eslint-disable` directives.** The bot rejects disabling its rules. If a string trips `ui/sentence-case` (e.g. a random example), pass it through a variable instead of a string literal; the rule only inspects literals.
|
- **No `eslint-disable` directives.** The bot rejects disabling its rules, and `noInlineConfig` makes them inert locally (an attempt to suppress an error just leaves the error). Reach APIs outside the typed/deprecated surface through local type-aliases instead (see [src/realtime/pcm.ts](../src/realtime/pcm.ts)'s `ScriptProcessorNodeLike`). If a string trips `ui/sentence-case` (e.g. a random example), pass it through a variable instead of a string literal; the rule only inspects literals.
|
||||||
- **Popout-window safety**: use `activeDocument` / `activeWindow` instead of `document` / `window`-as-globalThis where a popout could differ; use `window.setTimeout` / `window.clearTimeout` (not bare `setTimeout`); avoid `globalThis` (use `window`). For paired `addEventListener` / `removeEventListener`, capture one document reference so removal targets the same object.
|
- **Popout-window safety**: use `activeDocument` / `activeWindow` instead of `document` / `window`-as-globalThis where a popout could differ; use `window.setTimeout` / `window.clearTimeout` (not bare `setTimeout`); avoid `globalThis` (use `window`). For paired `addEventListener` / `removeEventListener`, capture one document reference so removal targets the same object.
|
||||||
- **No `!important` in [styles.css](../styles.css).** Raise specificity, use CSS variables, or toggle via Obsidian's `el.toggle()` / `hide()` / `show()` (which set inline display) instead.
|
- **No `!important` in [styles.css](../styles.css).** Raise specificity, use CSS variables, or toggle via Obsidian's `el.toggle()` / `hide()` / `show()` (which set inline display) instead.
|
||||||
- **Manifest `description`**: action-focused, <= 250 chars, ends with a period, no emoji.
|
- **Manifest `description`**: action-focused, <= 250 chars, ends with a period, no emoji.
|
||||||
|
|
@ -89,6 +99,29 @@ gh attestation verify /tmp/rel/main.js --repo WiseGuru/ReWrite-Voice-Notes # mus
|
||||||
|
|
||||||
Also confirm the release page shows the bare tag (`1.0.1`, no `v`) and all three assets.
|
Also confirm the release page shows the bare tag (`1.0.1`, no `v`) and all three assets.
|
||||||
|
|
||||||
|
## The alpha/beta channel (where all testing happens)
|
||||||
|
|
||||||
|
Every release candidate is published as a prerelease and tested in that form. A prerelease builds and publishes the same three attested assets as a stable release, but as a GitHub **prerelease**, so it is invisible to the official channel: GitHub's "latest release" (what Obsidian's in-app updater reads) excludes prereleases, and the community submission review reads `manifest.json` at **master's HEAD**, which this flow never touches.
|
||||||
|
|
||||||
|
Convention: pre-release the version you are *about to* ship. If the next release is `1.3.0`, tag `1.3.0-alpha` (then `-alpha.2`, `-beta`, `-rc`, etc. as rounds accumulate) while master's `manifest.json` still reads the last stable version (`1.2.1`). Because `1.3.0-alpha` < `1.3.0` in SemVer, a BRAT beta tester is auto-upgraded when the real `1.3.0` lands.
|
||||||
|
|
||||||
|
Any tag containing a `-` is treated as a pre-release by [release.yml](../.github/workflows/release.yml): it stamps the tag's version into the **published** `manifest.json` at build time (master's copy is left alone) and sets `prerelease: true` on the GitHub release.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# On master, clean tree, everything you want to test committed.
|
||||||
|
# Do NOT bump manifest.json / package.json / versions.json and do NOT commit a bump.
|
||||||
|
npm run build && npm run lint # must both pass
|
||||||
|
git tag -a 1.3.0-alpha -m "Pre-release 1.3.0-alpha"
|
||||||
|
git push origin 1.3.0-alpha
|
||||||
|
# CI publishes a prerelease with a manifest.json version of 1.3.0-alpha.
|
||||||
|
```
|
||||||
|
|
||||||
|
To install for testing, either download the three assets from the prerelease page and copy them into `<Vault>/.obsidian/plugins/rewrite-voice-notes/`, or add the repo in **BRAT** with "beta versions" enabled (BRAT reads the newest prerelease). Run the feature pass (`release-checklist` skill / `CHECKLIST.md`) against that install. Fixes go to master and get a further suffixed tag (`1.3.0-alpha.2`, `1.3.0-beta`, ...); re-test at least the affected areas plus a clean load.
|
||||||
|
|
||||||
|
When the pass is green, cut the stable `1.3.0` per the TL;DR: version bump + bare tag on the **same source** you just tested — the bump commit (plus doc-only commits) is the only permitted delta between the last prerelease and the stable release; anything touching the artifact means a new prerelease round. Pre-release tags and their GitHub releases can be left in place (they stay marked prerelease) or deleted; they never affect the stable channel.
|
||||||
|
|
||||||
|
Note: `versions.json` is not updated for pre-releases (no bump runs), which is correct. Never point a stable tag at a suffixed name, and never commit a `-alpha`/`-beta` version into master's `manifest.json` (it would corrupt what the directory reads at HEAD).
|
||||||
|
|
||||||
## Re-releasing the same version (rare)
|
## Re-releasing the same version (rare)
|
||||||
|
|
||||||
Only for fixing a botched release that nobody has consumed, and never once the version is accepted/depended on. Move the tag to the new commit and force-push to re-trigger CI:
|
Only for fixing a botched release that nobody has consumed, and never once the version is accepted/depended on. Move the tag to the new commit and force-push to re-trigger CI:
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,10 @@ The auto-start and idle-stop lifecycle toggles shipped (see Unreleased). The rem
|
||||||
|
|
||||||
Merged to `master`, not yet tagged. These become the next version's release notes.
|
Merged to `master`, not yet tagged. These become the next version's release notes.
|
||||||
|
|
||||||
|
### Fix: resolve the 1.2.1 automated-review `no-unsafe-*` warnings at the root (type-environment parity)
|
||||||
|
|
||||||
|
All ~30 warnings the Obsidian review bot raised on 1.2.1 traced to its lint environment resolving types differently from local, not to genuinely unsafe code (full analysis: [DEVCONFLICTS.md](DEVCONFLICTS.md) finding 10). Fixed at the root so the class cannot recur: `tsconfig.json` now declares `lib: ["DOM", "ES2019"]` (matching the ES2017–ES2019 APIs the code actually uses) with `types: []` so the test suite's `@types/node` can no longer mask a lib/API mismatch locally; all `moment` calls go through the new `formatMoment` structural wrapper in `src/time.ts` (moment's typings don't resolve in the bot's environment); `src/secrets.ts` was restructured to need no `as BufferSource` assertions under any TypeScript version; and two `as Record<string, unknown>` narrows became an `isRecord` type predicate. Runtime behavior is unchanged. Guardrails documented in CLAUDE.md's "Review-bot type environment" gotcha and RELEASING.md's parity section.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Released
|
## Released
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,12 @@
|
||||||
import tseslint from 'typescript-eslint';
|
import tseslint from 'typescript-eslint';
|
||||||
import obsidianmd from "eslint-plugin-obsidianmd";
|
import obsidianmd from "eslint-plugin-obsidianmd";
|
||||||
|
// The Obsidian community-review bot runs a newer eslint-plugin-obsidianmd than our
|
||||||
|
// pinned 0.1.9 base, whose `no-unsupported-api` rule (flags a direct Obsidian API call
|
||||||
|
// newer than manifest `minAppVersion`) is what caught `FileManager.trashFile` at
|
||||||
|
// submission. We keep 0.1.9 as the base config (its `ui/sentence-case` is the one the bot
|
||||||
|
// actually enforces; 0.4.x's is over-aggressive and produces wrong fixes), and cherry-pick
|
||||||
|
// ONLY that one rule from a 0.4.1 alias so the minAppVersion check runs locally too.
|
||||||
|
import obsidianmdLatest from "obsidianmd-latest";
|
||||||
import globals from "globals";
|
import globals from "globals";
|
||||||
import { globalIgnores } from "eslint/config";
|
import { globalIgnores } from "eslint/config";
|
||||||
|
|
||||||
|
|
@ -55,6 +62,15 @@ const sentenceCaseOptions = {
|
||||||
|
|
||||||
export default tseslint.config(
|
export default tseslint.config(
|
||||||
{
|
{
|
||||||
|
// The community-review bot rejects `eslint-disable` directives (it flags both the
|
||||||
|
// undescribed comment and disabling a rule at all). `noInlineConfig` makes every inline
|
||||||
|
// eslint directive inert, so a rule (ours or the bot's) cannot be silenced from a
|
||||||
|
// comment: an attempt to suppress a real error just leaves the error in place and fails
|
||||||
|
// `npm run lint`. Reach APIs outside the typed/deprecated surface through local
|
||||||
|
// type-aliases instead (see src/realtime/pcm.ts), never a disable comment.
|
||||||
|
linterOptions: {
|
||||||
|
noInlineConfig: true,
|
||||||
|
},
|
||||||
languageOptions: {
|
languageOptions: {
|
||||||
globals: {
|
globals: {
|
||||||
...globals.browser,
|
...globals.browser,
|
||||||
|
|
@ -101,6 +117,18 @@ export default tseslint.config(
|
||||||
'@typescript-eslint/no-unnecessary-type-assertion': 'error',
|
'@typescript-eslint/no-unnecessary-type-assertion': 'error',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
// Cherry-picked from the 0.4.1 alias (see the import comment): the single rule that
|
||||||
|
// flags a direct Obsidian API newer than manifest `minAppVersion`. Registering the
|
||||||
|
// plugin only makes its rules available; nothing else from 0.4.1 runs unless enabled
|
||||||
|
// here, so its divergent sentence-case rule stays off. This is the check that would
|
||||||
|
// have caught `trashFile` locally before the 1.2.0 submission.
|
||||||
|
files: ['src/**/*.ts'],
|
||||||
|
plugins: { 'obsidianmd-latest': obsidianmdLatest },
|
||||||
|
rules: {
|
||||||
|
'obsidianmd-latest/no-unsupported-api': 'error',
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
// test/ runs under Node via Vitest, not inside Obsidian, so the Node-builtin ban that
|
// test/ runs under Node via Vitest, not inside Obsidian, so the Node-builtin ban that
|
||||||
// keeps src/ portable to the Electron/mobile plugin runtime doesn't apply here.
|
// keeps src/ portable to the Electron/mobile plugin runtime doesn't apply here.
|
||||||
|
|
|
||||||
136
package-lock.json
generated
136
package-lock.json
generated
|
|
@ -22,6 +22,7 @@
|
||||||
"eslint-plugin-obsidianmd": "0.1.9",
|
"eslint-plugin-obsidianmd": "0.1.9",
|
||||||
"globals": "14.0.0",
|
"globals": "14.0.0",
|
||||||
"jiti": "2.6.1",
|
"jiti": "2.6.1",
|
||||||
|
"obsidianmd-latest": "npm:eslint-plugin-obsidianmd@0.4.1",
|
||||||
"tslib": "2.4.0",
|
"tslib": "2.4.0",
|
||||||
"typescript": "^5.8.3",
|
"typescript": "^5.8.3",
|
||||||
"typescript-eslint": "8.35.1",
|
"typescript-eslint": "8.35.1",
|
||||||
|
|
@ -478,6 +479,34 @@
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@eslint-community/eslint-plugin-eslint-comments": {
|
||||||
|
"version": "4.7.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-plugin-eslint-comments/-/eslint-plugin-eslint-comments-4.7.2.tgz",
|
||||||
|
"integrity": "sha512-LF03qURSwEWm2dz5wtdDCzNk+7Opl0X7q6I3undsaIuNsEiNvRV3BCtqu14Q/6Pzg1tBj44LcxpW2EpSLZStZw==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"escape-string-regexp": "^4.0.0",
|
||||||
|
"ignore": "^7.0.5"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/eslint"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"eslint": "^6.0.0 || ^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@eslint-community/eslint-plugin-eslint-comments/node_modules/ignore": {
|
||||||
|
"version": "7.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz",
|
||||||
|
"integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==",
|
||||||
|
"dev": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@eslint-community/eslint-utils": {
|
"node_modules/@eslint-community/eslint-utils": {
|
||||||
"version": "4.9.0",
|
"version": "4.9.0",
|
||||||
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz",
|
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz",
|
||||||
|
|
@ -626,7 +655,6 @@
|
||||||
"integrity": "sha512-rvR/EZtvUG3p9uqrSmcDJPYSH7atmWr0RnFWN6m917MAPx82+zQgPUmDu0whPFG6XTyM0vB/hR6c1Q63OaYtCQ==",
|
"integrity": "sha512-rvR/EZtvUG3p9uqrSmcDJPYSH7atmWr0RnFWN6m917MAPx82+zQgPUmDu0whPFG6XTyM0vB/hR6c1Q63OaYtCQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@eslint/core": "^0.17.0",
|
"@eslint/core": "^0.17.0",
|
||||||
"@eslint/plugin-kit": "^0.4.1",
|
"@eslint/plugin-kit": "^0.4.1",
|
||||||
|
|
@ -705,7 +733,6 @@
|
||||||
"integrity": "sha512-KWiFQpSAqEIyrTXko3hFNLeQvSK8zXlJQzhhxsyVn58WFRYXST99b3Nqnu+ttOtjds2Pl2grUHGpe2NzhPynuQ==",
|
"integrity": "sha512-KWiFQpSAqEIyrTXko3hFNLeQvSK8zXlJQzhhxsyVn58WFRYXST99b3Nqnu+ttOtjds2Pl2grUHGpe2NzhPynuQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
}
|
}
|
||||||
|
|
@ -1188,7 +1215,6 @@
|
||||||
"resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.2.tgz",
|
"resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.2.tgz",
|
||||||
"integrity": "sha512-uQDwm1wFHmbBbCZCqAlq6Do9LYwByNZHWzXppSnay9SuwJ+VRbjkbLABer54kcPnMSlG6Fdiy2yaFXm/z9Z5gw==",
|
"integrity": "sha512-uQDwm1wFHmbBbCZCqAlq6Do9LYwByNZHWzXppSnay9SuwJ+VRbjkbLABer54kcPnMSlG6Fdiy2yaFXm/z9Z5gw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/estree": "*",
|
"@types/estree": "*",
|
||||||
"@types/json-schema": "*"
|
"@types/json-schema": "*"
|
||||||
|
|
@ -2882,12 +2908,20 @@
|
||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/eslint-plugin-no-unsanitized": {
|
||||||
|
"version": "4.1.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/eslint-plugin-no-unsanitized/-/eslint-plugin-no-unsanitized-4.1.5.tgz",
|
||||||
|
"integrity": "sha512-MSB4hXPVFQrI8weqzs6gzl7reP2k/qSjtCoL2vUMSDejIIq9YL1ZKvq5/ORBXab/PvfBBrWO2jWviYpL+4Ghfg==",
|
||||||
|
"dev": true,
|
||||||
|
"peerDependencies": {
|
||||||
|
"eslint": "^9 || ^10"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/eslint-plugin-obsidianmd": {
|
"node_modules/eslint-plugin-obsidianmd": {
|
||||||
"version": "0.1.9",
|
"version": "0.1.9",
|
||||||
"resolved": "https://registry.npmjs.org/eslint-plugin-obsidianmd/-/eslint-plugin-obsidianmd-0.1.9.tgz",
|
"resolved": "https://registry.npmjs.org/eslint-plugin-obsidianmd/-/eslint-plugin-obsidianmd-0.1.9.tgz",
|
||||||
"integrity": "sha512-/gyo5vky3Y7re4BtT/8MQbHU5Wes4o6VRqas3YmXE7aTCnMsdV0kfzV1GDXJN9Hrsc9UQPoeKUMiapKL0aGE4g==",
|
"integrity": "sha512-/gyo5vky3Y7re4BtT/8MQbHU5Wes4o6VRqas3YmXE7aTCnMsdV0kfzV1GDXJN9Hrsc9UQPoeKUMiapKL0aGE4g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@microsoft/eslint-plugin-sdl": "^1.1.0",
|
"@microsoft/eslint-plugin-sdl": "^1.1.0",
|
||||||
"@types/eslint": "8.56.2",
|
"@types/eslint": "8.56.2",
|
||||||
|
|
@ -2930,7 +2964,6 @@
|
||||||
"resolved": "https://registry.npmjs.org/obsidian/-/obsidian-1.8.7.tgz",
|
"resolved": "https://registry.npmjs.org/obsidian/-/obsidian-1.8.7.tgz",
|
||||||
"integrity": "sha512-h4bWwNFAGRXlMlMAzdEiIM2ppTGlrh7uGOJS6w4gClrsjc+ei/3YAtU2VdFUlCiPuTHpY4aBpFJJW75S1Tl/JA==",
|
"integrity": "sha512-h4bWwNFAGRXlMlMAzdEiIM2ppTGlrh7uGOJS6w4gClrsjc+ei/3YAtU2VdFUlCiPuTHpY4aBpFJJW75S1Tl/JA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/codemirror": "5.60.8",
|
"@types/codemirror": "5.60.8",
|
||||||
"moment": "2.29.4"
|
"moment": "2.29.4"
|
||||||
|
|
@ -4560,10 +4593,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/obsidian": {
|
"node_modules/obsidian": {
|
||||||
"version": "1.10.3",
|
"version": "1.12.3",
|
||||||
"resolved": "https://registry.npmjs.org/obsidian/-/obsidian-1.10.3.tgz",
|
"resolved": "https://registry.npmjs.org/obsidian/-/obsidian-1.12.3.tgz",
|
||||||
"integrity": "sha512-VP+ZSxNMG7y6Z+sU9WqLvJAskCfkFrTz2kFHWmmzis+C+4+ELjk/sazwcTHrHXNZlgCeo8YOlM6SOrAFCynNew==",
|
"integrity": "sha512-HxWqe763dOqzXjnNiHmAJTRERN8KILBSqxDSEqbeSr7W8R8Jxezzbca+nz1LiiqXnMpM8lV2jzAezw3CZ4xNUw==",
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/codemirror": "5.60.8",
|
"@types/codemirror": "5.60.8",
|
||||||
"moment": "2.29.4"
|
"moment": "2.29.4"
|
||||||
|
|
@ -4573,6 +4605,92 @@
|
||||||
"@codemirror/view": "6.38.6"
|
"@codemirror/view": "6.38.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/obsidianmd-latest": {
|
||||||
|
"name": "eslint-plugin-obsidianmd",
|
||||||
|
"version": "0.4.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/eslint-plugin-obsidianmd/-/eslint-plugin-obsidianmd-0.4.1.tgz",
|
||||||
|
"integrity": "sha512-Nv3593kVsFOS8kir/HWaJ5CR3xcyiPWEPyXst5Pib8mxRYYuLYPpJS2ALMoZXHulHyiGwoYW8AaxvLRc4rhrRg==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@eslint-community/eslint-plugin-eslint-comments": "^4.7.2",
|
||||||
|
"@eslint/config-helpers": "^0.4.2",
|
||||||
|
"@eslint/js": "^9.30.1",
|
||||||
|
"@eslint/json": "0.14.0",
|
||||||
|
"@microsoft/eslint-plugin-sdl": "^1.1.0",
|
||||||
|
"@types/eslint": "9.6.1",
|
||||||
|
"@types/node": "20.12.12",
|
||||||
|
"@typescript-eslint/types": "^8.33.1",
|
||||||
|
"@typescript-eslint/utils": "^8.33.1",
|
||||||
|
"eslint": ">=9.19.0",
|
||||||
|
"eslint-plugin-depend": "1.3.1",
|
||||||
|
"eslint-plugin-import": "^2.31.0",
|
||||||
|
"eslint-plugin-json-schema-validator": "5.1.0",
|
||||||
|
"eslint-plugin-no-unsanitized": "^4.1.5",
|
||||||
|
"eslint-plugin-security": "2.1.1",
|
||||||
|
"globals": "14.0.0",
|
||||||
|
"obsidian": "1.12.3",
|
||||||
|
"semver": "^7.7.4",
|
||||||
|
"typescript": "5.4.5",
|
||||||
|
"typescript-eslint": "^8.35.1"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"eslint-plugin-obsidian": "dist/lib/index.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 18"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@eslint/js": "^9.30.1",
|
||||||
|
"@eslint/json": "0.14.0",
|
||||||
|
"eslint": ">=9.19.0",
|
||||||
|
"obsidian": "1.8.7",
|
||||||
|
"typescript-eslint": "^8.35.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/obsidianmd-latest/node_modules/@types/eslint": {
|
||||||
|
"version": "9.6.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz",
|
||||||
|
"integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@types/estree": "*",
|
||||||
|
"@types/json-schema": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/obsidianmd-latest/node_modules/@types/node": {
|
||||||
|
"version": "20.12.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.12.tgz",
|
||||||
|
"integrity": "sha512-eWLDGF/FOSPtAvEqeRAQ4C8LSA7M1I7i0ky1I8U7kD1J5ITyW3AsRhQrKVoWf5pFKZ2kILsEGJhsI9r93PYnOw==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"undici-types": "~5.26.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/obsidianmd-latest/node_modules/semver": {
|
||||||
|
"version": "7.8.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz",
|
||||||
|
"integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==",
|
||||||
|
"dev": true,
|
||||||
|
"bin": {
|
||||||
|
"semver": "bin/semver.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/obsidianmd-latest/node_modules/typescript": {
|
||||||
|
"version": "5.4.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz",
|
||||||
|
"integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==",
|
||||||
|
"dev": true,
|
||||||
|
"bin": {
|
||||||
|
"tsc": "bin/tsc",
|
||||||
|
"tsserver": "bin/tsserver"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.17"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/optionator": {
|
"node_modules/optionator": {
|
||||||
"version": "0.9.4",
|
"version": "0.9.4",
|
||||||
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
|
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@
|
||||||
"eslint-plugin-obsidianmd": "0.1.9",
|
"eslint-plugin-obsidianmd": "0.1.9",
|
||||||
"globals": "14.0.0",
|
"globals": "14.0.0",
|
||||||
"jiti": "2.6.1",
|
"jiti": "2.6.1",
|
||||||
|
"obsidianmd-latest": "npm:eslint-plugin-obsidianmd@0.4.1",
|
||||||
"tslib": "2.4.0",
|
"tslib": "2.4.0",
|
||||||
"typescript": "^5.8.3",
|
"typescript": "^5.8.3",
|
||||||
"typescript-eslint": "8.35.1",
|
"typescript-eslint": "8.35.1",
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { App, moment, normalizePath } from 'obsidian';
|
import { App, normalizePath } from 'obsidian';
|
||||||
|
import { formatMoment } from 'time';
|
||||||
import { GlobalSettings } from './types';
|
import { GlobalSettings } from './types';
|
||||||
|
|
||||||
interface AttachmentFileManager {
|
interface AttachmentFileManager {
|
||||||
|
|
@ -50,7 +51,7 @@ export function mimeToExtension(mime: string): string {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function buildAudioFilename(now: Date, ext: string): string {
|
export function buildAudioFilename(now: Date, ext: string): string {
|
||||||
const stamp = moment(now).format('YYYY-MM-DD-HHmmss');
|
const stamp = formatMoment('YYYY-MM-DD-HHmmss', now);
|
||||||
return `ReWrite-${stamp}.${ext}`;
|
return `ReWrite-${stamp}.${ext}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { App, MarkdownView, Modal, moment, normalizePath, Notice, Setting, TFile } from 'obsidian';
|
import { App, MarkdownView, Modal, normalizePath, Notice, Setting, TFile } from 'obsidian';
|
||||||
|
import { formatMoment } from 'time';
|
||||||
import { NewFileCollisionMode, NoteTemplate } from './types';
|
import { NewFileCollisionMode, NoteTemplate } from './types';
|
||||||
import { guardReservedName, sanitizeFilename } from './templates-folder';
|
import { guardReservedName, sanitizeFilename } from './templates-folder';
|
||||||
|
|
||||||
|
|
@ -207,11 +208,11 @@ class RenamePromptModal extends Modal {
|
||||||
}
|
}
|
||||||
|
|
||||||
function expandFilenameTemplate(template: string, title?: string): string {
|
function expandFilenameTemplate(template: string, title?: string): string {
|
||||||
const now = moment();
|
const now = new Date();
|
||||||
const safeTitle = title ? titleToFilename(title) : '';
|
const safeTitle = title ? titleToFilename(title) : '';
|
||||||
return template
|
return template
|
||||||
.replace(/\{\{date\}\}/g, now.format('YYYY-MM-DD'))
|
.replace(/\{\{date\}\}/g, formatMoment('YYYY-MM-DD', now))
|
||||||
.replace(/\{\{time\}\}/g, now.format('HHmmss'))
|
.replace(/\{\{time\}\}/g, formatMoment('HHmmss', now))
|
||||||
.replace(/\{\{title\}\}/g, safeTitle);
|
.replace(/\{\{title\}\}/g, safeTitle);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -234,9 +234,9 @@ export function extractFromBlock(
|
||||||
let usedStrict = false;
|
let usedStrict = false;
|
||||||
try {
|
try {
|
||||||
const parsed: unknown = parseYaml(block);
|
const parsed: unknown = parseYaml(block);
|
||||||
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
if (isRecord(parsed)) {
|
||||||
usedStrict = true;
|
usedStrict = true;
|
||||||
for (const [key, value] of Object.entries(parsed as Record<string, unknown>)) {
|
for (const [key, value] of Object.entries(parsed)) {
|
||||||
// Values are scalars. Take strings as-is, coerce numbers/booleans,
|
// Values are scalars. Take strings as-is, coerce numbers/booleans,
|
||||||
// and ignore null/undefined or nested objects/arrays.
|
// and ignore null/undefined or nested objects/arrays.
|
||||||
let coerced: string | null = null;
|
let coerced: string | null = null;
|
||||||
|
|
@ -280,6 +280,10 @@ export function extractFromBlock(
|
||||||
return { body, properties, title: title || undefined };
|
return { body, properties, title: title || undefined };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isRecord(v: unknown): v is Record<string, unknown> {
|
||||||
|
return typeof v === 'object' && v !== null && !Array.isArray(v);
|
||||||
|
}
|
||||||
|
|
||||||
function stripQuotes(value: string): string {
|
function stripQuotes(value: string): string {
|
||||||
let v = value.trim();
|
let v = value.trim();
|
||||||
// Peel matched leading/trailing quote pairs.
|
// Peel matched leading/trailing quote pairs.
|
||||||
|
|
|
||||||
|
|
@ -404,14 +404,19 @@ function bytesToBase64(bytes: Uint8Array): string {
|
||||||
return btoa(s);
|
return btoa(s);
|
||||||
}
|
}
|
||||||
|
|
||||||
function base64ToBytes(b64: string): Uint8Array {
|
// Return types are deliberately inferred (not annotated `Uint8Array`): on TS 5.7+
|
||||||
|
// the inferred type is the ArrayBuffer-backed Uint8Array that WebCrypto's
|
||||||
|
// BufferSource params accept directly, while an explicit `Uint8Array` annotation
|
||||||
|
// widens to an ArrayBufferLike backing and forces `as BufferSource` assertions
|
||||||
|
// that older TS (the community-review bot's) then flags as unnecessary.
|
||||||
|
function base64ToBytes(b64: string) {
|
||||||
const bin = atob(b64);
|
const bin = atob(b64);
|
||||||
const bytes = new Uint8Array(bin.length);
|
const bytes = new Uint8Array(bin.length);
|
||||||
for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
|
for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
|
||||||
return bytes;
|
return bytes;
|
||||||
}
|
}
|
||||||
|
|
||||||
function randomBytes(n: number): Uint8Array {
|
function randomBytes(n: number) {
|
||||||
const out = new Uint8Array(n);
|
const out = new Uint8Array(n);
|
||||||
crypto.getRandomValues(out);
|
crypto.getRandomValues(out);
|
||||||
return out;
|
return out;
|
||||||
|
|
@ -428,7 +433,7 @@ function isAllocationFailure(e: unknown): boolean {
|
||||||
|
|
||||||
// ---------- key derivation ----------
|
// ---------- key derivation ----------
|
||||||
|
|
||||||
async function deriveKeyFromPassphrase(passphrase: string, salt: Uint8Array, iterations: number): Promise<CryptoKey> {
|
async function deriveKeyFromPassphrase(passphrase: string, salt: BufferSource, iterations: number): Promise<CryptoKey> {
|
||||||
const passBytes = new TextEncoder().encode(passphrase);
|
const passBytes = new TextEncoder().encode(passphrase);
|
||||||
const baseKey = await crypto.subtle.importKey(
|
const baseKey = await crypto.subtle.importKey(
|
||||||
'raw',
|
'raw',
|
||||||
|
|
@ -438,7 +443,7 @@ async function deriveKeyFromPassphrase(passphrase: string, salt: Uint8Array, ite
|
||||||
['deriveKey'],
|
['deriveKey'],
|
||||||
);
|
);
|
||||||
return crypto.subtle.deriveKey(
|
return crypto.subtle.deriveKey(
|
||||||
{ name: 'PBKDF2', hash: 'SHA-256', salt: salt as BufferSource, iterations },
|
{ name: 'PBKDF2', hash: 'SHA-256', salt, iterations },
|
||||||
baseKey,
|
baseKey,
|
||||||
{ name: 'AES-GCM', length: 256 },
|
{ name: 'AES-GCM', length: 256 },
|
||||||
false,
|
false,
|
||||||
|
|
@ -464,7 +469,10 @@ async function deriveArgon2idKey(
|
||||||
});
|
});
|
||||||
return crypto.subtle.importKey(
|
return crypto.subtle.importKey(
|
||||||
'raw',
|
'raw',
|
||||||
raw as BufferSource,
|
// hash-wasm types the binary output as a bare Uint8Array (ArrayBufferLike
|
||||||
|
// backing on TS 5.7+); copying into a fresh array satisfies BufferSource on
|
||||||
|
// every TS version without an assertion. 32 bytes, once per derivation.
|
||||||
|
new Uint8Array(raw),
|
||||||
{ name: 'AES-GCM' },
|
{ name: 'AES-GCM' },
|
||||||
false,
|
false,
|
||||||
['encrypt', 'decrypt'],
|
['encrypt', 'decrypt'],
|
||||||
|
|
@ -516,7 +524,7 @@ async function buildPassphraseKdfAndKey(passphrase: string): Promise<{ kdf: Pass
|
||||||
async function aesGcmEncrypt(key: CryptoKey, plaintext: string): Promise<string> {
|
async function aesGcmEncrypt(key: CryptoKey, plaintext: string): Promise<string> {
|
||||||
const iv = randomBytes(AES_IV_BYTES);
|
const iv = randomBytes(AES_IV_BYTES);
|
||||||
const ct = await crypto.subtle.encrypt(
|
const ct = await crypto.subtle.encrypt(
|
||||||
{ name: 'AES-GCM', iv: iv as BufferSource },
|
{ name: 'AES-GCM', iv },
|
||||||
key,
|
key,
|
||||||
new TextEncoder().encode(plaintext),
|
new TextEncoder().encode(plaintext),
|
||||||
);
|
);
|
||||||
|
|
@ -529,9 +537,9 @@ async function aesGcmDecrypt(key: CryptoKey, payload: string): Promise<string> {
|
||||||
const iv = base64ToBytes(payload.slice(0, sepIdx));
|
const iv = base64ToBytes(payload.slice(0, sepIdx));
|
||||||
const ct = base64ToBytes(payload.slice(sepIdx + 1));
|
const ct = base64ToBytes(payload.slice(sepIdx + 1));
|
||||||
const pt = await crypto.subtle.decrypt(
|
const pt = await crypto.subtle.decrypt(
|
||||||
{ name: 'AES-GCM', iv: iv as BufferSource },
|
{ name: 'AES-GCM', iv },
|
||||||
key,
|
key,
|
||||||
ct as BufferSource,
|
ct,
|
||||||
);
|
);
|
||||||
return new TextDecoder().decode(pt);
|
return new TextDecoder().decode(pt);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { App, moment, normalizePath, TFile } from 'obsidian';
|
import { App, normalizePath, TFile } from 'obsidian';
|
||||||
|
import { formatMoment } from 'time';
|
||||||
import type { TemplateUpdateConflict, TemplateUpdateEntry, UpdateResult } from './templates-folder';
|
import type { TemplateUpdateConflict, TemplateUpdateEntry, UpdateResult } from './templates-folder';
|
||||||
|
|
||||||
// The Update button's worklist. Lives next to the templates folder (in its
|
// The Update button's worklist. Lives next to the templates folder (in its
|
||||||
|
|
@ -120,7 +121,7 @@ function renderUpdateReport(result: UpdateResult): string {
|
||||||
lines.push('# ReWrite: template update report');
|
lines.push('# ReWrite: template update report');
|
||||||
lines.push('');
|
lines.push('');
|
||||||
const failNote = result.parseFailed ? `, ${result.parseFailed} could not be parsed` : '';
|
const failNote = result.parseFailed ? `, ${result.parseFailed} could not be parsed` : '';
|
||||||
lines.push(`Generated ${moment().format('YYYY-MM-DD HH:mm')}. ${result.updated} updated, ${result.conflicts} need manual review, ${result.created} created, ${result.unchanged} unchanged${failNote}.`);
|
lines.push(`Generated ${formatMoment('YYYY-MM-DD HH:mm')}. ${result.updated} updated, ${result.conflicts} need manual review, ${result.created} created, ${result.unchanged} unchanged${failNote}.`);
|
||||||
lines.push('');
|
lines.push('');
|
||||||
|
|
||||||
const actionable = result.entries.filter((e) => e.status !== 'unchanged');
|
const actionable = result.entries.filter((e) => e.status !== 'unchanged');
|
||||||
|
|
|
||||||
|
|
@ -421,6 +421,10 @@ async function parseTemplateFile(app: App, file: TFile): Promise<NoteTemplate |
|
||||||
return parseTemplateContent(file, content);
|
return parseTemplateContent(file, content);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isRecord(v: unknown): v is Record<string, unknown> {
|
||||||
|
return typeof v === 'object' && v !== null && !Array.isArray(v);
|
||||||
|
}
|
||||||
|
|
||||||
// Split out from parseTemplateFile so callers that already hold the file content (the update
|
// Split out from parseTemplateFile so callers that already hold the file content (the update
|
||||||
// walk previously read each candidate file up to three times: once for the id, once for the
|
// walk previously read each candidate file up to three times: once for the id, once for the
|
||||||
// full parse, once more to diff against the rendered output) can parse without a redundant read.
|
// full parse, once more to diff against the rendered output) can parse without a redundant read.
|
||||||
|
|
@ -482,8 +486,8 @@ export function parseTemplateContent(file: TFile, content: string): NoteTemplate
|
||||||
// blank keys are skipped; a missing/non-string instruction becomes "".
|
// blank keys are skipped; a missing/non-string instruction becomes "".
|
||||||
const noteProperties: NotePropertySpec[] = [];
|
const noteProperties: NotePropertySpec[] = [];
|
||||||
const rawProps = obj.noteProperties;
|
const rawProps = obj.noteProperties;
|
||||||
if (rawProps && typeof rawProps === 'object' && !Array.isArray(rawProps)) {
|
if (isRecord(rawProps)) {
|
||||||
for (const [name, instruction] of Object.entries(rawProps as Record<string, unknown>)) {
|
for (const [name, instruction] of Object.entries(rawProps)) {
|
||||||
const key = name.trim();
|
const key = name.trim();
|
||||||
if (!key) continue;
|
if (!key) continue;
|
||||||
noteProperties.push({
|
noteProperties.push({
|
||||||
|
|
|
||||||
19
src/time.ts
Normal file
19
src/time.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
import { moment } from 'obsidian';
|
||||||
|
|
||||||
|
// Obsidian re-exports its bundled moment, but the export's TYPE comes from the
|
||||||
|
// `moment` package (a transitive dependency of `obsidian`), which the community
|
||||||
|
// review bot's lint environment does not resolve: `obsidian` itself types fine
|
||||||
|
// there, `moment` does not, so every direct `moment(...)` call is error-typed and
|
||||||
|
// trips the bot's no-unsafe-* rules. All timestamp formatting therefore goes
|
||||||
|
// through this narrow structural alias (the same pattern as ScriptProcessorNodeLike
|
||||||
|
// and WakeLockLike), which needs nothing from moment's own typings. Runtime
|
||||||
|
// behavior is unchanged: it is still Obsidian's bundled moment doing the work.
|
||||||
|
interface MomentLike {
|
||||||
|
format(fmt: string): string;
|
||||||
|
}
|
||||||
|
|
||||||
|
type MomentFactory = (input?: Date) => MomentLike;
|
||||||
|
|
||||||
|
export function formatMoment(fmt: string, input?: Date): string {
|
||||||
|
return (moment as unknown as MomentFactory)(input).format(fmt);
|
||||||
|
}
|
||||||
|
|
@ -17,11 +17,11 @@
|
||||||
"strictBindCallApply": true,
|
"strictBindCallApply": true,
|
||||||
"allowSyntheticDefaultImports": true,
|
"allowSyntheticDefaultImports": true,
|
||||||
"useUnknownInCatchVariables": true,
|
"useUnknownInCatchVariables": true,
|
||||||
|
"types": [],
|
||||||
|
"skipLibCheck": true,
|
||||||
"lib": [
|
"lib": [
|
||||||
"DOM",
|
"DOM",
|
||||||
"ES5",
|
"ES2019"
|
||||||
"ES6",
|
|
||||||
"ES7"
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue