Compare commits

..

No commits in common. "main" and "0.5.2" have entirely different histories.
main ... 0.5.2

20 changed files with 351 additions and 2352 deletions

View file

@ -1,15 +1,10 @@
# top-most EditorConfig file
root = true
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
indent_style = tab
indent_size = 4
tab_width = 4
[*.{yml,yaml}]
indent_style = space
indent_size = 2
tab_width = 2
# top-most EditorConfig file
root = true
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
indent_style = tab
indent_size = 4
tab_width = 4

View file

@ -1,43 +0,0 @@
name: Claude Code Review
on:
pull_request:
types: [opened, synchronize, ready_for_review, reopened]
# Optional: Only run on specific file changes
# paths:
# - "src/**/*.ts"
# - "src/**/*.tsx"
# - "src/**/*.js"
# - "src/**/*.jsx"
jobs:
claude-review:
# Optional: Filter by PR author
# if: |
# github.event.pull_request.user.login == 'external-contributor' ||
# github.event.pull_request.user.login == 'new-developer' ||
# github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR'
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: read
issues: read
id-token: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Run Claude Code Review
id: claude-review
uses: anthropics/claude-code-action@v1
with:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
plugin_marketplaces: "https://github.com/anthropics/claude-code.git"
plugins: "code-review@claude-code-plugins"
prompt: "/code-review:code-review ${{ github.repository }}/pull/${{ github.event.pull_request.number }}"
# See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md
# or https://code.claude.com/docs/en/cli-reference for available options

View file

@ -1,50 +0,0 @@
name: Claude Code
on:
issue_comment:
types: [created]
pull_request_review_comment:
types: [created]
issues:
types: [opened, assigned]
pull_request_review:
types: [submitted]
jobs:
claude:
if: |
(github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) ||
(github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) ||
(github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) ||
(github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')))
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: read
issues: read
id-token: write
actions: read # Required for Claude to read CI results on PRs
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Run Claude Code
id: claude
uses: anthropics/claude-code-action@v1
with:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
# This is an optional setting that allows Claude to read CI results on PRs
additional_permissions: |
actions: read
# Optional: Give a custom prompt to Claude. If this is not specified, Claude will perform the instructions specified in the comment that tagged it.
# prompt: 'Update the pull request description to include a summary of changes.'
# Optional: Add claude_args to customize behavior and configuration
# See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md
# or https://code.claude.com/docs/en/cli-reference for available options
# claude_args: '--allowed-tools Bash(gh pr:*)'

View file

@ -1,48 +1,35 @@
name: Release Obsidian plugin
on:
push:
tags:
- "*"
workflow_dispatch:
push:
tags:
- "*"
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: write
id-token: write
attestations: write
artifact-metadata: write
steps:
- uses: actions/checkout@v6
build:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v3
- name: Use Node.js
uses: actions/setup-node@v6
with:
node-version: "24"
cache: "npm"
- name: Use Node.js
uses: actions/setup-node@v3
with:
node-version: "18.x"
- name: Build plugin
run: |
npm install
npm run build
- name: Build plugin
run: |
npm install
npm run build
- name: Attest
uses: actions/attest@v4
with:
subject-path: |
main.js
manifest.json
styles.css
- name: Create release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
tag="${GITHUB_REF#refs/tags/}"
- name: Create release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
tag="${GITHUB_REF#refs/tags/}"
gh release create "$tag" \
--title="$tag" \
--draft \
main.js manifest.json styles.css
gh release create "$tag" \
--title="$tag" \
--draft \
main.js manifest.json styles.css

View file

@ -1,57 +0,0 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Commands
```bash
npm run dev # Development build with watch mode (esbuild)
npm run build # Type-check (tsc --noEmit) + production bundle
npm run version # Bump version, updates manifest.json and versions.json
```
No test runner is configured. Linting is via ESLint (`npx eslint .`).
## Architecture
This is an **Obsidian plugin** ("Simple Archiver") that moves notes and folders to a configurable archive folder, with an optional auto-archive feature that runs rules on a schedule.
### Core Modules
**`main.ts`** — Plugin entry point (`SimpleArchiverPlugin extends Plugin`). Owns:
- Settings load/save via Obsidian's `loadData()`/`saveData()`
- Command registration (`move-to-archive`, `move-out-of-archive`)
- Context menu items for files and folders
- Core `archiveFile()` / `unarchiveFile()` logic (conflict resolution, path normalization)
**`autoarchive/`** — Auto-archive subsystem:
- `AutoArchiveService.ts` — Rule evaluation engine and scheduler. Uses `setInterval()` for periodic runs, `setTimeout()` for startup delay. Evaluates conditions (file age, filename regex) with AND/OR logic. Rate-limited to 500 files/cycle. Regex patterns validated with 100ms timeout to prevent ReDoS.
- `AutoArchiveTypes.ts` — Type definitions (`AutoArchiveRule`, `AutoArchiveCondition`, `SimpleArchiverSettings`, `ArchiveResult`)
- `AutoArchiveSettings.ts` — Default settings and backward-compatibility migrations
- `AutoArchiveLifecycle.ts` — Factory functions for creating/controlling the service
- `index.ts` — Barrel export
**`SettingsTab.ts`** — Tabbed settings UI (General tab: archive folder config; Auto-Archive tab: schedule and rule management).
**`modals.ts`** — `SimpleArchiverPromptModal` (yes/no confirmations) and `AutoArchiveRuleModal` (rule editor with validation).
### Data Flow
```
User command/menu → archiveFile() → conflict check → Obsidian vault.rename() → saveSettings()
Scheduler → AutoArchiveService.processAutoArchiveRules()
→ enabled rules → matching folders → evaluate conditions → archiveFile()
```
### Build Output
esbuild bundles `main.ts``main.js`. Obsidian, Electron, and all `@codemirror/*` / `@lezer/*` packages are marked external (provided by Obsidian at runtime).
### Plugin Manifest
`manifest.json` and `versions.json` track plugin versioning for the Obsidian plugin marketplace. The release workflow (`.github/workflows/release.yml`) auto-creates GitHub releases when version tags are pushed.
## Community Plugin Listing
https://community.obsidian.md/plugins/simple-archiver

View file

@ -15,36 +15,6 @@ Unarchiving can be done via:
- `Move out of archive` file menu item
- `Move all out of archive` multi-file menu item
## Auto-Archive
Auto-archive lets you define rules that periodically move matching files into your archive folder. Each rule targets a folder (optionally by regex), can include subfolders, and requires at least one condition. Conditions can be based on file age (days since last modified) and/or a file name regex. When a rule matches, files are archived to the same relative path under your archive folder.
How it works:
- Rules are evaluated on a schedule (default every 60 minutes).
- The last auto-archive run time is saved, so short Obsidian sessions can still trigger catch-up runs on startup.
- On startup, auto-archive waits for a configurable delay (default 30 seconds) before checking whether a run is due.
- Multiple conditions can be combined with AND or OR.
- Files already in the archive folder are skipped.
How to use it:
- Open Settings -> Simple Archiver -> Auto-Archive.
- Set the auto-archive frequency and startup delay, and optionally click "Auto Archive Now" to run immediately.
- Click "Add Rule", choose a folder path (or enable regex), decide whether to apply recursively, and add one or more conditions.
- Optional: Right-click a folder and choose "Auto-archive" -> "Add rule" to prefill the folder path.
- Optional: Right-click a folder and choose "Auto-archive" -> "Edit rule" to edit an existing rule.
Example rule:
```text
Folder: Notes/Daily
Apply recursively: No
Conditions (AND):
- File age >= 30 days
- File name regex: ^\d{4}-\d{2}-\d{2}.*\.md$
```
## Planned Improvements
- Archiving a folder that already exists in the archive merges the contents
@ -62,18 +32,9 @@ AI-assisted contributions will be considered with discretion. Obviously vibe-cod
- [nicholaslck](https://github.com/nicholaslck)
- [ggfevans](https://github.com/ggfevans)
- [albertsj1](https://github.com/albertsj1)
## Release Notes
### v0.6.2
- **Fix**: Address issues identified on Obsidian community plugin
### v0.6.1
- **New**: Add auto-archiving functionality. Thanks to [albertsj1](https://github.com/albertsj1)!
### v0.5.2
- **Fix**: Resolve issue when archiving files from folders with leading 0s. Thanks [ggfevans](https://github.com/ggfevans)!

View file

@ -1,316 +0,0 @@
import {
App,
normalizePath,
Notice,
PluginSettingTab,
Setting,
} from "obsidian";
import { AutoArchiveService } from "./autoarchive/AutoArchiveService";
import type { AutoArchiveRule } from "./autoarchive/AutoArchiveTypes";
import type SimpleArchiver from "./main";
import { AutoArchiveRuleModal } from "./modals";
export class SimpleArchiverSettingsTab extends PluginSettingTab {
plugin: SimpleArchiver;
autoArchiveService: AutoArchiveService;
activeTab: "general" | "autoArchive" = "general";
constructor(
app: App,
plugin: SimpleArchiver,
autoArchiveService: AutoArchiveService,
) {
super(app, plugin);
this.plugin = plugin;
this.autoArchiveService = autoArchiveService;
}
display(): void {
const { containerEl } = this;
containerEl.empty();
const tabContainer = containerEl.createDiv({
cls: "setting-tab-container",
});
const tabButtonContainer = tabContainer.createDiv({
cls: "setting-tab-buttons",
});
const generalTabButton = tabButtonContainer.createEl("button", {
text: "General",
cls:
this.activeTab === "general"
? "setting-tab-button active"
: "setting-tab-button",
});
generalTabButton.addEventListener("click", () => {
this.activeTab = "general";
this.display();
});
const autoArchiveTabButton = tabButtonContainer.createEl("button", {
text: "Auto-Archive",
cls:
this.activeTab === "autoArchive"
? "setting-tab-button active"
: "setting-tab-button",
});
autoArchiveTabButton.addEventListener("click", () => {
this.activeTab = "autoArchive";
this.display();
});
const tabContent = containerEl.createDiv({
cls: "setting-tab-content",
});
if (this.activeTab === "general") {
this.displayGeneralSettings(tabContent);
} else {
this.displayAutoArchiveSettings(tabContent);
}
}
private displayGeneralSettings(containerEl: HTMLElement): void {
new Setting(containerEl)
.setName("Archive folder")
.setDesc(
"The folder to use as the Archive. If the folder doesn't exist, it will be created when archiving a " +
'note. Folder names must not contain "\\", "/" or ":" and must not start with ".".',
)
.addText((text) =>
text
.setPlaceholder("Archive folder")
.setValue(normalizePath(this.plugin.settings.archiveFolder))
.onChange(async (value) => {
if (this.setArchiveFolder(value)) {
await this.plugin.saveSettings();
} else {
text.setValue(this.plugin.settings.archiveFolder);
}
}),
);
}
private displayAutoArchiveSettings(containerEl: HTMLElement): void {
const frequencySetting = new Setting(containerEl)
.setName("Auto-archive frequency")
.setDesc("How often to check and process auto-archive rules");
const lastRunLineEl = frequencySetting.descEl.createDiv({
cls: "setting-item-description",
});
const refreshLastRunLine = () => {
lastRunLineEl.setText(
`Last auto-archive run: ${this.getLastAutoArchiveRunText()}`,
);
};
refreshLastRunLine();
frequencySetting
.addButton((button) =>
button
.setButtonText("Auto Archive Now")
.setTooltip("Process auto-archive rules immediately")
.onClick(async () => {
button.setDisabled(true);
button.setButtonText("Processing...");
await this.autoArchiveService.processAutoArchiveRules();
refreshLastRunLine();
this.autoArchiveService.scheduleAutoArchive();
button.setButtonText("Auto Archive Now");
button.setDisabled(false);
new Notice("Auto-archive rules processed");
}),
)
.addDropdown((dropdown) =>
dropdown
.addOption("5", "Every 5 minutes")
.addOption("15", "Every 15 minutes")
.addOption("30", "Every 30 minutes")
.addOption("60", "Every 60 minutes")
.addOption("360", "Every 6 hours")
.addOption("720", "Every 12 hours")
.addOption("1440", "Every 24 hours")
.addOption("2880", "Every 48 hours")
.setValue(
this.plugin.settings.autoArchiveFrequency.toString(),
)
.onChange(async (value) => {
this.plugin.settings.autoArchiveFrequency = parseInt(
value,
10,
);
await this.plugin.saveSettings();
this.autoArchiveService.scheduleAutoArchive();
}),
);
new Setting(containerEl)
.setName("Auto-archive startup delay")
.setDesc(
"How long to wait after Obsidian starts before checking if auto-archive should run",
)
.addDropdown((dropdown) =>
dropdown
.addOption("5", "5 seconds")
.addOption("10", "10 seconds")
.addOption("30", "30 seconds")
.addOption("60", "60 seconds")
.addOption("120", "2 minutes")
.addOption("300", "5 minutes")
.setValue(
this.plugin.settings.autoArchiveStartupDelaySeconds.toString(),
)
.onChange(async (value) => {
this.plugin.settings.autoArchiveStartupDelaySeconds =
parseInt(value, 10);
await this.plugin.saveSettings();
this.autoArchiveService.scheduleStartupAutoArchiveCheck();
}),
);
new Setting(containerEl)
.setName("Auto-archive rules")
.setDesc(
"Define rules for automatically archiving files in specific folders",
)
.addButton((button) =>
button.setButtonText("Add Rule").onClick(() => {
this.addAutoArchiveRule();
}),
);
const rulesContainer = containerEl.createDiv({
cls: "auto-archive-rules-container",
});
this.displayAutoArchiveRules(rulesContainer);
}
private displayAutoArchiveRules(containerEl: HTMLElement): void {
if (this.plugin.settings.autoArchiveRules.length === 0) {
containerEl.createEl("p", {
text: "No auto-archive rules configured yet.",
cls: "setting-item-description",
});
return;
}
for (const rule of this.plugin.settings.autoArchiveRules) {
this.displayAutoArchiveRule(containerEl, rule);
}
}
private displayAutoArchiveRule(
containerEl: HTMLElement,
rule: AutoArchiveRule,
): void {
const ruleContainer = containerEl.createDiv({
cls: "auto-archive-rule",
});
new Setting(ruleContainer)
.setName(`Folder: ${rule.folderPath || "(not set)"}`)
.setClass("auto-archive-rule-header")
.addToggle((toggle) =>
toggle.setValue(rule.enabled).onChange(async (value) => {
rule.enabled = value;
await this.plugin.saveSettings();
}),
)
.addButton((button) =>
button
.setButtonText("Edit")
.setClass("mod-cta")
.onClick(() => {
this.editAutoArchiveRule(rule);
}),
)
.addButton((button) =>
button
.setButtonText("Delete")
.setWarning()
.onClick(async () => {
this.plugin.settings.autoArchiveRules =
this.plugin.settings.autoArchiveRules.filter(
(r) => r.id !== rule.id,
);
await this.plugin.saveSettings();
this.display();
}),
);
const conditionsEl = ruleContainer.createDiv({
cls: "auto-archive-rule-conditions",
});
if (rule.conditions.length === 0) {
conditionsEl.createSpan({
text: "No conditions set",
cls: "setting-item-description",
});
} else {
if (rule.conditions.length > 1) {
conditionsEl.createDiv({
text: `Logic: ${rule.logicOperator || "AND"}`,
cls: "auto-archive-rule-logic",
});
}
for (const condition of rule.conditions) {
const conditionText =
this.autoArchiveService.getConditionText(condition);
conditionsEl.createDiv({
text: `${conditionText}`,
cls: "auto-archive-rule-condition",
});
}
}
}
private addAutoArchiveRule(): void {
const newRule: AutoArchiveRule = {
id: crypto.randomUUID(),
enabled: true,
folderPath: "",
useFolderRegex: false,
applyRecursively: false,
conditions: [],
logicOperator: "AND",
};
this.plugin.settings.autoArchiveRules.push(newRule);
this.editAutoArchiveRule(newRule);
}
private editAutoArchiveRule(rule: AutoArchiveRule): void {
new AutoArchiveRuleModal(this.app, this.plugin, rule, async () => {
await this.plugin.saveSettings();
this.display();
}).open();
}
private validateArchiveFolderName(value: string): boolean {
return !/^\.|[:/\\]\.|:/.test(value);
}
private setArchiveFolder(value: string): boolean {
if (!this.validateArchiveFolderName(value)) {
return false;
}
this.plugin.settings.archiveFolder = value;
return true;
}
private getLastAutoArchiveRunText(): string {
const lastRunAt =
this.plugin.autoArchiveRuntimeData.lastAutoArchiveRunAt;
if (!Number.isFinite(lastRunAt) || lastRunAt <= 0) {
return "never";
}
return new Date(lastRunAt).toLocaleString();
}
}

View file

@ -1,22 +0,0 @@
import { Workspace } from "obsidian";
import { AutoArchiveService } from "./AutoArchiveService";
import type SimpleArchiver from "../main";
/**
* Bootstraps the auto-archive context menu UI.
*
* Registers folder context menu items that allow users to add, edit,
* and manage auto-archive rules at the folder level.
*
* @param service The AutoArchiveService instance
* @param workspace The Obsidian Workspace instance
* @param plugin The SimpleArchiver plugin instance
*/
export function setupAutoArchiveContextMenu(
service: AutoArchiveService,
workspace: Workspace,
plugin: SimpleArchiver,
): void {
service.setupContextMenu(workspace, plugin);
}

View file

@ -1,61 +0,0 @@
import { App, TAbstractFile } from "obsidian";
import { AutoArchiveService } from "./AutoArchiveService";
import type {
ArchiveResult,
AutoArchiveRuntimeData,
SimpleArchiverSettings,
} from "./AutoArchiveTypes";
/**
* Factory function to create and configure an AutoArchiveService instance.
*
* Encapsulates the complex wiring of dependencies (settings getter, archive callbacks, etc.)
* that the service needs to function.
*
* @param app Obsidian App instance
* @param getSettings Getter function to retrieve current plugin settings
* @param archiveFile Callback to archive a file
* @param isFileArchived Callback to check if a file is already archived
* @param persistLastRunAt Callback to persist the last auto-archive run timestamp
* @returns Configured AutoArchiveService instance
*/
export function createAutoArchiveService(
app: App,
getSettings: () => SimpleArchiverSettings & AutoArchiveRuntimeData,
archiveFile: (file: TAbstractFile) => Promise<ArchiveResult>,
isFileArchived: (file: TAbstractFile) => boolean,
persistLastRunAt: (lastRunAt: number) => Promise<void>,
): AutoArchiveService {
return new AutoArchiveService(
app,
getSettings,
archiveFile,
isFileArchived,
persistLastRunAt,
);
}
/**
* Starts the auto-archive scheduler.
*
* Invokes both the startup-delay check (which may run immediately if conditions are met)
* and the recurring interval schedule.
*
* @param service The AutoArchiveService instance
*/
export function startAutoArchive(service: AutoArchiveService): void {
service.scheduleStartupAutoArchiveCheck();
service.scheduleAutoArchive();
}
/**
* Stops the auto-archive scheduler.
*
* Clears all pending timers and intervals.
*
* @param service The AutoArchiveService instance
*/
export function stopAutoArchiveScheduler(service: AutoArchiveService): void {
service.stopAutoArchive();
}

View file

@ -1,611 +0,0 @@
import {
App,
normalizePath,
PluginSettingTab,
TAbstractFile,
TFile,
TFolder,
Workspace,
} from "obsidian";
import type {
AutoArchiveRuntimeData,
AutoArchiveCondition,
AutoArchiveRule,
SimpleArchiverSettings,
ArchiveResult,
ObsidianInternalApis,
} from "./AutoArchiveTypes";
import type SimpleArchiver from "../main";
import { AutoArchiveRuleModal } from "../modals";
import { SimpleArchiverSettingsTab } from "../SettingsTab";
// Constants extracted from magic numbers
const SETTINGS_TAB_RENDER_DELAY_MS = 200;
const MAX_FILES_PER_CYCLE = 500;
const DEFAULT_REGEX_TIMEOUT_MS = 100;
const MAX_REGEX_PATTERN_LENGTH = 512;
const MAX_REGEX_VALIDATION_INPUT_LENGTH = 1024;
type MenuEntry = {
setTitle(title: string): MenuEntry;
setIcon(icon: string): MenuEntry;
onClick(callback: () => void): MenuEntry;
setSubmenu?: () => MenuGroup;
};
type MenuGroup = {
addItem(callback: (item: MenuEntry) => void): void;
};
export class AutoArchiveService {
private app: App;
private getSettings: () => SimpleArchiverSettings & AutoArchiveRuntimeData;
private archiveFile: (file: TAbstractFile) => Promise<ArchiveResult>;
private isFileArchived: (file: TAbstractFile) => boolean;
private persistLastAutoArchiveRunAt: (lastRunAt: number) => Promise<void>;
private autoArchiveInterval: number | null = null;
private startupAutoArchiveTimeout: number | null = null;
private filesProcessedInCycle = 0;
private isProcessingAutoArchiveRules = false;
constructor(
app: App,
getSettings: () => SimpleArchiverSettings & AutoArchiveRuntimeData,
archiveFile: (file: TAbstractFile) => Promise<ArchiveResult>,
isFileArchived: (file: TAbstractFile) => boolean,
persistLastAutoArchiveRunAt: (lastRunAt: number) => Promise<void>,
) {
this.app = app;
this.getSettings = getSettings;
this.archiveFile = archiveFile;
this.isFileArchived = isFileArchived;
this.persistLastAutoArchiveRunAt = persistLastAutoArchiveRunAt;
}
scheduleStartupAutoArchiveCheck(): void {
if (this.startupAutoArchiveTimeout !== null) {
window.clearTimeout(this.startupAutoArchiveTimeout);
}
const settings = this.getSettings();
const startupDelayMs = settings.autoArchiveStartupDelaySeconds * 1000;
this.startupAutoArchiveTimeout = window.setTimeout(() => {
this.startupAutoArchiveTimeout = null;
void this.runStartupAutoArchiveCheck();
}, startupDelayMs);
}
private async runStartupAutoArchiveCheck(): Promise<void> {
const settings = this.getSettings();
const intervalMs = settings.autoArchiveFrequency * 60 * 1000;
const elapsedSinceLastRun = Date.now() - settings.lastAutoArchiveRunAt;
if (elapsedSinceLastRun >= intervalMs) {
await this.processAutoArchiveRules();
}
}
private async persistLastRunTimestamp(timestamp: number): Promise<void> {
try {
await this.persistLastAutoArchiveRunAt(timestamp);
} catch (error) {
console.error(
"Failed to persist last auto-archive run time:",
error,
);
}
}
/**
* Safely compiles and validates a regex pattern.
* This uses conservative static checks and adversarial test input to reduce
* ReDoS risk. It intentionally rejects patterns that look unsafe.
*/
private validateRegexPattern(pattern: string): RegExp | null {
if (pattern.length > MAX_REGEX_PATTERN_LENGTH) {
console.error(
`Regex pattern is too long (${pattern.length} chars): ${pattern}`,
);
return null;
}
if (this.isLikelyUnsafeRegex(pattern)) {
console.error(`Unsafe regex pattern rejected: ${pattern}`);
return null;
}
try {
const regex = new RegExp(pattern);
for (const testInput of this.getRegexValidationInputs()) {
const elapsedMs = this.measureRegexTestTime(regex, testInput);
if (
elapsedMs === null ||
elapsedMs > DEFAULT_REGEX_TIMEOUT_MS
) {
console.error(
`Regex pattern failed performance validation on adversarial input: ${pattern}`,
);
return null;
}
}
return regex;
} catch (error) {
console.error(`Invalid regex pattern: ${pattern}`, error);
return null;
}
}
/**
* Detects common high-risk regex constructs associated with catastrophic
* backtracking. This is intentionally conservative.
*/
private isLikelyUnsafeRegex(pattern: string): boolean {
const hasBackreferences = /(^|[^\\])\\[1-9]/.test(pattern);
if (hasBackreferences) {
return true;
}
const nestedQuantifierPatterns = [
/\((?:[^()\\]|\\.)*[+*](?:[^()\\]|\\.)*\)\s*[+*{]/,
/\((?:[^()\\]|\\.)*\{\d+,?\d*\}(?:[^()\\]|\\.)*\)\s*[+*{]/,
/\((?:[^()\\]|\\.)*\.[+*](?:[^()\\]|\\.)*\)\s*[+*{]/,
];
return nestedQuantifierPatterns.some((regex) => regex.test(pattern));
}
private getRegexValidationInputs(): string[] {
const repeatedA = "a".repeat(MAX_REGEX_VALIDATION_INPUT_LENGTH);
const repeatedPathSegment = "segment/".repeat(
Math.floor(MAX_REGEX_VALIDATION_INPUT_LENGTH / 8),
);
return [
`${repeatedA}!`,
`/${repeatedPathSegment}end`,
`${repeatedA}.md`,
"test_file_name.md",
];
}
private measureRegexTestTime(regex: RegExp, input: string): number | null {
try {
const start = performance.now();
regex.test(input);
return performance.now() - start;
} catch {
return null;
}
}
scheduleAutoArchive(): void {
if (this.autoArchiveInterval !== null) {
window.clearInterval(this.autoArchiveInterval);
}
const settings = this.getSettings();
const intervalMs = settings.autoArchiveFrequency * 60 * 1000;
this.autoArchiveInterval = window.setInterval(() => {
void this.processAutoArchiveRules().catch((error) => {
console.error("Auto-archive cycle failed:", error);
});
}, intervalMs);
}
stopAutoArchive(): void {
if (this.autoArchiveInterval !== null) {
window.clearInterval(this.autoArchiveInterval);
this.autoArchiveInterval = null;
}
if (this.startupAutoArchiveTimeout !== null) {
window.clearTimeout(this.startupAutoArchiveTimeout);
this.startupAutoArchiveTimeout = null;
}
}
async processAutoArchiveRules(): Promise<void> {
if (this.isProcessingAutoArchiveRules) {
console.warn(
"Auto-archive cycle skipped because a previous cycle is still running.",
);
return;
}
this.isProcessingAutoArchiveRules = true;
try {
const settings = this.getSettings();
const enabledRules = settings.autoArchiveRules.filter(
(rule) => rule.enabled,
);
if (enabledRules.length === 0) {
return;
}
this.filesProcessedInCycle = 0;
for (const rule of enabledRules) {
// Check if we've hit the rate limit
if (this.filesProcessedInCycle >= MAX_FILES_PER_CYCLE) {
console.warn(
`Auto-archive cycle reached maximum file limit (${MAX_FILES_PER_CYCLE}). Remaining rules skipped to prevent performance issues.`,
);
break;
}
let foldersToProcess: TFolder[] = [];
if (rule.useFolderRegex) {
const regex = this.validateRegexPattern(rule.folderPath);
if (!regex) {
console.error(
`Skipping rule with invalid regex pattern: ${rule.folderPath}`,
);
continue;
}
const allFolders = this.app.vault.getAllFolders();
foldersToProcess = allFolders.filter((folder) =>
regex.test(folder.path),
);
} else {
const folder = this.app.vault.getFolderByPath(
normalizePath(rule.folderPath),
);
if (!folder) {
console.warn(
`Auto-archive rule references non-existent folder: ${rule.folderPath}`,
);
continue;
}
foldersToProcess = [folder];
}
const filesToArchive = [];
for (const folder of foldersToProcess) {
if (!folder) continue; // Additional safety check
const files = this.getFilesFromFolder(
folder,
rule.applyRecursively || false,
);
for (const file of files) {
if (this.filesProcessedInCycle >= MAX_FILES_PER_CYCLE) {
break;
}
if (await this.evaluateAutoArchiveRule(file, rule)) {
filesToArchive.push(file);
this.filesProcessedInCycle++;
}
}
}
for (const file of filesToArchive) {
try {
await this.archiveFile(file);
} catch (error) {
console.error(
`Auto-archive failed for file: ${file.path}`,
error,
);
}
}
}
} catch (error) {
console.error("Auto-archive cycle failed:", error);
} finally {
await this.persistLastRunTimestamp(Date.now());
this.isProcessingAutoArchiveRules = false;
}
}
private getFilesFromFolder(folder: TFolder, recursive: boolean): TFile[] {
const files: TFile[] = [];
if (!folder || !folder.children) {
return files;
}
for (const child of folder.children) {
if (child instanceof TFile) {
files.push(child);
} else if (recursive && child instanceof TFolder) {
files.push(...this.getFilesFromFolder(child, recursive));
}
}
return files;
}
private async evaluateAutoArchiveRule(
file: TAbstractFile,
rule: AutoArchiveRule,
): Promise<boolean> {
if (this.isFileArchived(file)) {
return false;
}
if (rule.conditions.length === 0) {
return false;
}
const operator = rule.logicOperator || "AND";
if (operator === "OR") {
for (const condition of rule.conditions) {
if (await this.evaluateCondition(file, condition)) {
return true;
}
}
return false;
} else {
for (const condition of rule.conditions) {
if (!(await this.evaluateCondition(file, condition))) {
return false;
}
}
return true;
}
}
private async evaluateCondition(
file: TAbstractFile,
condition: AutoArchiveCondition,
): Promise<boolean> {
if (condition.type === "fileAge") {
const ageInDays = parseInt(condition.value);
if (isNaN(ageInDays)) {
return false;
}
const stats = await this.app.vault.adapter.stat(file.path);
if (!stats) {
return false;
}
const fileAgeMs = Date.now() - stats.mtime;
const fileAgeDays = fileAgeMs / (1000 * 60 * 60 * 24);
return fileAgeDays >= ageInDays;
} else if (condition.type === "regexPattern") {
const regex = this.validateRegexPattern(condition.value);
if (!regex) {
console.error(
`Invalid regex pattern in auto-archive rule: ${condition.value}`,
);
return false;
}
return regex.test(file.name);
}
return false;
}
getRulesForFolder(folderPath: string): AutoArchiveRule[] {
const settings = this.getSettings();
return settings.autoArchiveRules.filter((rule) => {
if (rule.useFolderRegex) {
const regex = this.validateRegexPattern(rule.folderPath);
if (!regex) {
return false;
}
return regex.test(folderPath);
}
return rule.folderPath === folderPath;
});
}
/**
* Generates human-readable text for a condition (shared utility).
*/
getConditionText(condition: AutoArchiveCondition): string {
if (condition.type === "fileAge") {
return `File age ≥ ${condition.value} days`;
} else if (condition.type === "regexPattern") {
return `File name matches: ${condition.value}`;
}
return "Unknown condition";
}
getRuleDisplayText(rule: AutoArchiveRule): string {
const icon = rule.enabled ? "✓" : "○";
if (rule.conditions.length === 0) {
return `${icon} (no conditions)`;
}
if (rule.conditions.length === 1) {
const conditionText = this.getConditionText(rule.conditions[0]);
return `${icon} ${conditionText}`;
}
// Multiple conditions: join with logic operator
const operator = rule.logicOperator === "OR" ? " OR " : " AND ";
const conditionsText = rule.conditions
.map((c) => this.getConditionText(c))
.join(operator);
// Truncate if too long
const fullText = `${icon} ${conditionsText}`;
const MAX_LENGTH = 60;
if (fullText.length > MAX_LENGTH) {
const firstCondition = this.getConditionText(rule.conditions[0]);
return `${icon} ${firstCondition} ${operator}... (${rule.conditions.length} total)`;
}
return fullText;
}
setupContextMenu(workspace: Workspace, plugin: SimpleArchiver): void {
plugin.registerEvent(
workspace.on("file-menu", (menu, file) => {
// Only show for folders
if (!(file instanceof TFolder)) {
return;
}
menu.addItem((item) => {
item.setTitle("Auto-archive").setIcon("clock");
// Add submenu with proper type safety
const submenu = this.getSubmenu(item);
if (!submenu) {
console.error("Failed to create submenu");
return;
}
// Add "Add rule" item
submenu.addItem((subitem: MenuEntry) => {
subitem
.setTitle("Add rule")
.setIcon("plus")
.onClick(() => {
this.openAutoArchiveRuleForFolder(
plugin,
file.path,
);
});
});
// Check if there are existing rules for this folder
const existingRules = this.getRulesForFolder(file.path);
if (existingRules.length > 0) {
// Add "Edit Rule" submenu item
submenu.addItem((subitem: MenuEntry) => {
subitem.setTitle("Edit Rule").setIcon("pencil");
const editSubmenu = this.getSubmenu(subitem);
if (!editSubmenu) return;
// Add each rule as a submenu item
for (const rule of existingRules) {
editSubmenu.addItem((ruleItem: MenuEntry) => {
const displayText =
this.getRuleDisplayText(rule);
const icon = rule.enabled
? "check"
: "circle";
ruleItem
.setTitle(displayText)
.setIcon(icon)
.onClick(() => {
new AutoArchiveRuleModal(
this.app,
plugin,
rule,
async () => {
await plugin.saveSettings();
},
).open();
});
});
}
});
}
});
}),
);
}
/**
* Safely retrieves submenu from menu item with type safety.
*/
private getSubmenu(item: MenuEntry): MenuGroup | null {
try {
return typeof item.setSubmenu === "function"
? item.setSubmenu()
: null;
} catch (error) {
console.error("Failed to create submenu:", error);
return null;
}
}
private openAutoArchiveRuleForFolder(
plugin: SimpleArchiver,
folderPath: string,
): void {
try {
const obsidianApis = this.app as unknown as ObsidianInternalApis;
if (!obsidianApis.setting) {
console.error("Unable to access settings API");
return;
}
obsidianApis.setting.open();
const pluginId = plugin.manifest.id;
obsidianApis.setting.openTabById(pluginId);
window.setTimeout(() => {
const tabs = obsidianApis.setting
.pluginTabs as PluginSettingTab[];
for (const tab of tabs) {
if (tab instanceof SimpleArchiverSettingsTab) {
tab.activeTab = "autoArchive";
tab.display();
const newRule: AutoArchiveRule = {
id: crypto.randomUUID(),
enabled: true,
folderPath,
useFolderRegex: false,
applyRecursively: false,
conditions: [],
logicOperator: "AND",
};
plugin.settings.autoArchiveRules.push(newRule);
try {
new AutoArchiveRuleModal(
this.app,
plugin,
newRule,
async () => {
await plugin.saveSettings();
tab.display();
},
async () => {
plugin.settings.autoArchiveRules =
plugin.settings.autoArchiveRules.filter(
(r) => r.id !== newRule.id,
);
await plugin.saveSettings();
tab.display();
},
).open();
} catch (error) {
plugin.settings.autoArchiveRules =
plugin.settings.autoArchiveRules.filter(
(r) => r.id !== newRule.id,
);
console.error(
"Failed to open auto-archive rule modal:",
error,
);
}
break;
}
}
}, SETTINGS_TAB_RENDER_DELAY_MS);
} catch (error) {
console.error(
"Failed to open auto-archive rule for folder:",
error,
);
}
}
}

View file

@ -1,96 +0,0 @@
import type {
AutoArchiveRuntimeData,
AutoArchiveRule,
SimpleArchiverSettings,
} from "./AutoArchiveTypes";
/**
* Default settings for auto-archive functionality.
* These are composed into the plugin's overall DEFAULT_SETTINGS.
*/
export const AUTO_ARCHIVE_DEFAULT_SETTINGS: Partial<SimpleArchiverSettings> = {
autoArchiveRules: [],
autoArchiveFrequency: 60,
autoArchiveStartupDelaySeconds: 30,
};
export const AUTO_ARCHIVE_DEFAULT_RUNTIME_DATA: AutoArchiveRuntimeData = {
lastAutoArchiveRunAt: 0,
};
/**
* Migrates auto-archive settings from loaded data, ensuring backward compatibility
* and normalizing all required fields to safe defaults.
*
* @param settings The loaded settings object (may be partial from old configs)
* @returns Object with normalized settings and a flag indicating if changes were made
*/
export function migrateAutoArchiveSettings(settings: SimpleArchiverSettings): {
settings: SimpleArchiverSettings;
changed: boolean;
} {
let changed = false;
// Migrate per-rule defaults
if (settings.autoArchiveRules) {
settings.autoArchiveRules = settings.autoArchiveRules.map((rule) => {
const updates: Partial<AutoArchiveRule> = {};
if (!rule.logicOperator) {
changed = true;
updates.logicOperator = "AND" as "AND" | "OR";
}
if (rule.useFolderRegex === undefined) {
changed = true;
updates.useFolderRegex = false;
}
if (rule.applyRecursively === undefined) {
changed = true;
updates.applyRecursively = false;
}
return { ...rule, ...updates };
});
}
// Normalize startup delay seconds
if (
!Number.isFinite(settings.autoArchiveStartupDelaySeconds) ||
settings.autoArchiveStartupDelaySeconds < 0
) {
changed = true;
settings.autoArchiveStartupDelaySeconds =
AUTO_ARCHIVE_DEFAULT_SETTINGS.autoArchiveStartupDelaySeconds ?? 30;
}
// Normalize auto-archive frequency (minutes)
if (
!Number.isFinite(settings.autoArchiveFrequency) ||
settings.autoArchiveFrequency <= 0
) {
changed = true;
settings.autoArchiveFrequency =
AUTO_ARCHIVE_DEFAULT_SETTINGS.autoArchiveFrequency ?? 60;
}
return { settings, changed };
}
export function normalizeAutoArchiveRuntimeData(
runtimeData: Partial<AutoArchiveRuntimeData> | null | undefined,
): AutoArchiveRuntimeData {
const lastAutoArchiveRunAt = runtimeData?.lastAutoArchiveRunAt;
if (
!Number.isFinite(lastAutoArchiveRunAt) ||
(lastAutoArchiveRunAt ?? 0) < 0
) {
return { ...AUTO_ARCHIVE_DEFAULT_RUNTIME_DATA };
}
return {
lastAutoArchiveRunAt: lastAutoArchiveRunAt ?? 0,
};
}

View file

@ -1,42 +0,0 @@
export interface AutoArchiveCondition {
type: "fileAge" | "regexPattern";
value: string;
}
export interface AutoArchiveRule {
id: string;
enabled: boolean;
folderPath: string;
useFolderRegex: boolean;
applyRecursively: boolean;
conditions: AutoArchiveCondition[];
logicOperator: "AND" | "OR";
}
export interface SimpleArchiverSettings {
archiveFolder: string;
autoArchiveRules: AutoArchiveRule[];
autoArchiveFrequency: number;
autoArchiveStartupDelaySeconds: number;
}
export interface AutoArchiveRuntimeData {
lastAutoArchiveRunAt: number;
}
export interface ArchiveResult {
success: boolean;
message: string;
}
/**
* Interface for Obsidian's internal settings API.
* Used for type-safe access to settings functionality.
*/
export interface ObsidianInternalApis {
setting: {
open(): void;
openTabById(pluginId: string): void;
pluginTabs: unknown[];
};
}

View file

@ -1,31 +0,0 @@
/**
* Auto-Archive Module Barrel Export
*
* This file consolidates all public APIs from the auto-archive subsystem,
* allowing main.ts to import everything via a single import statement.
*/
export {
AUTO_ARCHIVE_DEFAULT_SETTINGS,
AUTO_ARCHIVE_DEFAULT_RUNTIME_DATA,
migrateAutoArchiveSettings,
normalizeAutoArchiveRuntimeData,
} from "./AutoArchiveSettings";
export {
createAutoArchiveService,
startAutoArchive,
stopAutoArchiveScheduler,
} from "./AutoArchiveLifecycle";
export { setupAutoArchiveContextMenu } from "./AutoArchiveContextMenu";
export { AutoArchiveService } from "./AutoArchiveService";
export type {
ArchiveResult,
AutoArchiveRuntimeData,
AutoArchiveCondition,
AutoArchiveRule,
SimpleArchiverSettings,
} from "./AutoArchiveTypes";

View file

@ -1,6 +1,6 @@
import esbuild from "esbuild";
import process from "process";
import { builtinModules } from "module";
import builtins from "builtin-modules";
const banner = `/*
THIS IS A GENERATED/BUNDLED FILE BY ESBUILD
@ -30,7 +30,7 @@ const context = await esbuild.context({
"@lezer/common",
"@lezer/highlight",
"@lezer/lr",
...builtinModules,
...builtins,
],
format: "cjs",
target: "es2018",

377
main.ts
View file

@ -1,40 +1,31 @@
import {
App,
Editor,
MarkdownView,
Modal,
normalizePath,
Notice,
Plugin,
PluginSettingTab,
Setting,
TAbstractFile,
} from "obsidian";
import {
AUTO_ARCHIVE_DEFAULT_SETTINGS,
AUTO_ARCHIVE_DEFAULT_RUNTIME_DATA,
migrateAutoArchiveSettings,
normalizeAutoArchiveRuntimeData,
createAutoArchiveService,
startAutoArchive,
stopAutoArchiveScheduler,
setupAutoArchiveContextMenu,
AutoArchiveService,
type AutoArchiveRuntimeData,
type ArchiveResult,
type SimpleArchiverSettings,
} from "./autoarchive";
import { SimpleArchiverPromptModal } from "./modals";
import { SimpleArchiverSettingsTab } from "./SettingsTab";
interface SimpleArchiverSettings {
archiveFolder: string;
}
const AUTO_ARCHIVE_DATA_FILE = "autoarchive_data.json";
interface ArchiveResult {
success: boolean;
message: string;
}
const DEFAULT_SETTINGS: SimpleArchiverSettings = {
archiveFolder: "Archive",
...AUTO_ARCHIVE_DEFAULT_SETTINGS,
} as SimpleArchiverSettings;
};
export default class SimpleArchiver extends Plugin {
settings: SimpleArchiverSettings;
autoArchiveRuntimeData: AutoArchiveRuntimeData;
private autoArchiveService: AutoArchiveService;
async onload() {
await this.loadSettings();
@ -44,24 +35,17 @@ export default class SimpleArchiver extends Plugin {
name: "Move to archive",
editorCheckCallback: (
checking: boolean,
_editor: Editor,
view: MarkdownView,
editor: Editor,
view: MarkdownView
) => {
const canBeArchived =
view.file && !this.isFileArchived(view.file);
if (canBeArchived && view.file != null) {
if (!checking) {
void this.archiveFile(view.file)
.then((result) => {
new Notice(result.message);
})
.catch((e: unknown) => {
const msg =
e instanceof Error ? e.message : String(e);
new Notice(`Error: ${msg}`);
console.error(msg);
});
this.archiveFile(view.file).then((result) => {
new Notice(result.message);
});
}
return true;
@ -76,24 +60,17 @@ export default class SimpleArchiver extends Plugin {
name: "Move out of archive",
editorCheckCallback: (
checking: boolean,
_editor: Editor,
view: MarkdownView,
editor: Editor,
view: MarkdownView
) => {
const canBeUnarchived =
view.file && this.isFileArchived(view.file);
if (canBeUnarchived && view.file != null) {
if (!checking) {
void this.unarchiveFile(view.file)
.then((result) => {
new Notice(result.message);
})
.catch((e: unknown) => {
const msg =
e instanceof Error ? e.message : String(e);
new Notice(`Error: ${msg}`);
console.error(msg);
});
this.unarchiveFile(view.file).then((result) => {
new Notice(result.message);
});
}
return true;
@ -103,37 +80,7 @@ export default class SimpleArchiver extends Plugin {
},
});
this.autoArchiveService = createAutoArchiveService(
this.app,
() => ({
...this.settings,
lastAutoArchiveRunAt:
this.autoArchiveRuntimeData.lastAutoArchiveRunAt,
}),
(file) => this.archiveFile(file),
(file) => this.isFileArchived(file),
async (lastRunAt) => {
await this.saveAutoArchiveRuntimeData({
lastAutoArchiveRunAt: lastRunAt,
});
},
);
startAutoArchive(this.autoArchiveService);
this.addSettingTab(
new SimpleArchiverSettingsTab(
this.app,
this,
this.autoArchiveService,
),
);
// Setup auto-archive context menu
setupAutoArchiveContextMenu(
this.autoArchiveService,
this.app.workspace,
this,
);
this.addSettingTab(new SimpleArchiverSettingsTab(this.app, this));
// Archive file context menu
this.registerEvent(
@ -151,12 +98,11 @@ export default class SimpleArchiver extends Plugin {
if (result.success) {
new Notice(result.message);
} else {
new Notice(`Error: ${result.message}`);
console.error(result.message);
new Error(result.message);
}
});
});
}),
})
);
this.registerEvent(
@ -172,7 +118,7 @@ export default class SimpleArchiver extends Plugin {
await this.archiveAllFiles(files);
});
});
}),
})
);
// Unarchive file context menu
@ -191,12 +137,11 @@ export default class SimpleArchiver extends Plugin {
if (result.success) {
new Notice(result.message);
} else {
new Notice(`Error: ${result.message}`);
console.error(result.message);
new Error(result.message);
}
});
});
}),
})
);
this.registerEvent(
@ -212,7 +157,7 @@ export default class SimpleArchiver extends Plugin {
await this.unarchiveAllFiles(files);
});
});
}),
})
);
}
@ -220,30 +165,13 @@ export default class SimpleArchiver extends Plugin {
return file.path.startsWith(this.settings.archiveFolder);
}
/**
* Safely resolves the original file path from an archived path.
* Handles edge cases like files at vault root or missing parent folders.
*/
private resolveOriginalPath(archivedPath: string): string | null {
const archiveFolderPrefix = this.settings.archiveFolder + "/";
// Check that the file is actually in the archive folder
if (!archivedPath.startsWith(archiveFolderPrefix)) {
console.error(`File is not in archive folder: ${archivedPath}`);
return null;
}
// Remove the archive folder prefix to get the original path
return archivedPath.substring(archiveFolderPrefix.length);
}
private async archiveFile(file: TAbstractFile): Promise<ArchiveResult> {
if (this.isFileArchived(file)) {
return { success: false, message: "Item is already archived" };
}
const destinationFilePath = normalizePath(
`${this.settings.archiveFolder}/${file.path}`,
`${this.settings.archiveFolder}/${file.path}`
);
const existingItem =
@ -269,7 +197,7 @@ export default class SimpleArchiver extends Plugin {
success: false,
message: "Archive operation cancelled",
});
},
}
);
prompt.open();
});
@ -293,10 +221,10 @@ export default class SimpleArchiver extends Plugin {
}
private async moveFileToArchive(
file: TAbstractFile,
file: TAbstractFile
): Promise<ArchiveResult> {
const destinationPath = normalizePath(
`${this.settings.archiveFolder}/${file.parent?.path}`,
`${this.settings.archiveFolder}/${file.parent?.path}`
);
const destinationFolder =
@ -307,7 +235,7 @@ export default class SimpleArchiver extends Plugin {
}
const destinationFilePath = normalizePath(
`${destinationPath}/${file.name}`,
`${destinationPath}/${file.name}`
);
try {
@ -317,69 +245,43 @@ export default class SimpleArchiver extends Plugin {
message: `${file.name} archived successfully`,
};
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : String(error);
return {
success: false,
message: `Unable to archive ${file.name}: ${errorMessage}`,
message: `Unable to archive ${file.name}: ${error}`,
};
}
}
private async moveFileOutOfArchive(
file: TAbstractFile,
file: TAbstractFile
): Promise<ArchiveResult> {
const originalPath = this.resolveOriginalPath(file.path);
if (!originalPath) {
return {
success: false,
message: `Unable to unarchive: Invalid archive path ${file.path}`,
};
}
// Extract parent folder path by finding the last slash
const lastSlashIndex = originalPath.lastIndexOf("/");
const originalParentPath =
lastSlashIndex > 0
? originalPath.substring(0, lastSlashIndex)
: null;
const originalPath = file.path.substring(
this.settings.archiveFolder.length + 1
);
const originalParentPath = originalPath.substring(
0,
originalPath.lastIndexOf("/")
);
if (originalParentPath) {
const originalFolder =
this.app.vault.getFolderByPath(originalParentPath);
if (!originalFolder) {
try {
await this.app.vault.createFolder(
normalizePath(originalParentPath),
);
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : String(error);
return {
success: false,
message: `Unable to create folder: ${errorMessage}`,
};
}
if (originalFolder == null) {
await this.app.vault.createFolder(normalizePath(originalParentPath));
}
}
try {
await this.app.fileManager.renameFile(
file,
normalizePath(originalPath),
);
await this.app.fileManager.renameFile(file, normalizePath(originalPath));
return {
success: true,
message: `${file.name} unarchived successfully`,
};
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : String(error);
return {
success: false,
message: `Unable to unarchive ${file.name}: ${errorMessage}`,
message: `Unable to unarchive ${file.name}: ${error}`,
};
}
}
@ -389,14 +291,9 @@ export default class SimpleArchiver extends Plugin {
return { success: false, message: "Item is not archived" };
}
const originalPath = this.resolveOriginalPath(file.path);
if (!originalPath) {
return {
success: false,
message: `Unable to unarchive: Invalid archive path`,
};
}
const originalPath = file.path.substring(
this.settings.archiveFolder.length + 1
);
const existingItem = this.app.vault.getAbstractFileByPath(originalPath);
@ -419,7 +316,7 @@ export default class SimpleArchiver extends Plugin {
success: false,
message: "Unarchive operation cancelled",
});
},
}
);
prompt.open();
});
@ -441,104 +338,98 @@ export default class SimpleArchiver extends Plugin {
new Notice(`${unarchived} files unarchived`);
}
onunload() {
stopAutoArchiveScheduler(this.autoArchiveService);
}
private async loadSettings() {
const loadedData = await this.loadData();
const lastAutoArchiveRunAt =
this.getLegacyLastAutoArchiveRunAt(loadedData);
this.settings = Object.assign({}, DEFAULT_SETTINGS, loadedData);
delete (
this.settings as Partial<SimpleArchiverSettings> & {
lastAutoArchiveRunAt?: number;
}
).lastAutoArchiveRunAt;
this.autoArchiveRuntimeData =
await this.loadAutoArchiveRuntimeData(lastAutoArchiveRunAt);
// Migrate auto-archive settings for backward compatibility
const { changed } = migrateAutoArchiveSettings(this.settings);
// Persist the migration if any changes were made
if (changed || lastAutoArchiveRunAt !== null) {
await this.saveSettings();
}
this.settings = Object.assign(
{},
DEFAULT_SETTINGS,
await this.loadData()
);
}
async saveSettings() {
await this.saveData(this.settings);
}
}
async saveAutoArchiveRuntimeData(runtimeData: AutoArchiveRuntimeData) {
this.autoArchiveRuntimeData =
normalizeAutoArchiveRuntimeData(runtimeData);
await this.app.vault.adapter.write(
`${this.manifest.dir}/${AUTO_ARCHIVE_DATA_FILE}`,
JSON.stringify(this.autoArchiveRuntimeData, null, 2),
);
}
class SimpleArchiverPromptModal extends Modal {
constructor(
app: App,
title: string,
message: string,
yesButtonText: string,
noButtonText: string,
callback: () => Promise<void>,
cancelCallback: () => Promise<void>
) {
super(app);
private async loadAutoArchiveRuntimeData(
legacyLastAutoArchiveRunAt: number | null,
): Promise<AutoArchiveRuntimeData> {
const existingRuntimeData = await this.readAutoArchiveRuntimeDataFile();
this.setTitle(title);
if (existingRuntimeData !== null) {
return existingRuntimeData;
}
this.setContent(message);
const migratedRuntimeData = normalizeAutoArchiveRuntimeData({
lastAutoArchiveRunAt:
legacyLastAutoArchiveRunAt ??
AUTO_ARCHIVE_DEFAULT_RUNTIME_DATA.lastAutoArchiveRunAt,
});
if (legacyLastAutoArchiveRunAt !== null) {
await this.saveAutoArchiveRuntimeData(migratedRuntimeData);
}
return migratedRuntimeData;
}
private async readAutoArchiveRuntimeDataFile(): Promise<AutoArchiveRuntimeData | null> {
const runtimeDataPath = `${this.manifest.dir}/${AUTO_ARCHIVE_DATA_FILE}`;
if (!(await this.app.vault.adapter.exists(runtimeDataPath))) {
return null;
}
try {
const content = await this.app.vault.adapter.read(runtimeDataPath);
const parsedData = JSON.parse(
content,
) as Partial<AutoArchiveRuntimeData>;
return normalizeAutoArchiveRuntimeData(parsedData);
} catch (error) {
console.error("Failed to load auto-archive runtime data:", error);
return { ...AUTO_ARCHIVE_DEFAULT_RUNTIME_DATA };
}
}
private getLegacyLastAutoArchiveRunAt(loadedData: unknown): number | null {
if (
loadedData === null ||
typeof loadedData !== "object" ||
!("lastAutoArchiveRunAt" in loadedData)
) {
return null;
}
const { lastAutoArchiveRunAt } = loadedData as {
lastAutoArchiveRunAt?: unknown;
};
return typeof lastAutoArchiveRunAt === "number"
? lastAutoArchiveRunAt
: null;
new Setting(this.contentEl)
.addButton((btn) =>
btn
.setButtonText(yesButtonText)
.setWarning()
.onClick(() => {
callback();
this.close();
})
)
.addButton((btn) =>
btn.setButtonText(noButtonText).onClick(() => {
cancelCallback();
this.close();
})
);
}
}
class SimpleArchiverSettingsTab extends PluginSettingTab {
plugin: SimpleArchiver;
constructor(app: App, plugin: SimpleArchiver) {
super(app, plugin);
this.plugin = plugin;
}
display(): void {
const { containerEl } = this;
containerEl.empty();
new Setting(containerEl)
.setName("Archive folder")
.setDesc(
"The folder to use as the Archive. If the folder doesn't exist, it will be created when archiving a " +
'note. Folder names must not contain "\\", "/" or ":" and must not start with ".".'
)
.addText((text) =>
text
.setPlaceholder("Archive folder")
.setValue(normalizePath(this.plugin.settings.archiveFolder))
.onChange(async (value) => {
if (this.setArchiveFolder(value)) {
await this.plugin.saveSettings();
} else {
text.setValue(this.plugin.settings.archiveFolder);
}
})
);
}
private validateArchiveFolderName(value: string): boolean {
// Validate folder does not start with '.', contain ':' or contain a relative path
return !/^\.|[:/\\]\.|:/.test(value);
}
private setArchiveFolder(value: string): boolean {
if (!this.validateArchiveFolderName(value)) {
return false;
}
this.plugin.settings.archiveFolder = value;
return true;
}
}

View file

@ -1,7 +1,7 @@
{
"id": "simple-archiver",
"name": "Simple Archiver",
"version": "0.6.2",
"version": "0.5.2",
"minAppVersion": "1.8.0",
"description": "Move old, stinky notes and folders to an archive, where they belong.",
"author": "Mike Farr",

401
modals.ts
View file

@ -1,401 +0,0 @@
import { App, Modal, normalizePath, Setting } from "obsidian";
import type {
AutoArchiveCondition,
AutoArchiveRule,
} from "./autoarchive/AutoArchiveTypes";
import type SimpleArchiver from "./main";
export class SimpleArchiverPromptModal extends Modal {
constructor(
app: App,
title: string,
message: string,
yesButtonText: string,
noButtonText: string,
callback: () => Promise<void>,
cancelCallback: () => Promise<void>,
) {
super(app);
this.setTitle(title);
this.setContent(message);
new Setting(this.contentEl)
.addButton((btn) =>
btn
.setButtonText(yesButtonText)
.setWarning()
.onClick(() => {
void callback();
this.close();
}),
)
.addButton((btn) =>
btn.setButtonText(noButtonText).onClick(() => {
void cancelCallback();
this.close();
}),
);
}
}
export class AutoArchiveRuleModal extends Modal {
plugin: SimpleArchiver;
rule: AutoArchiveRule;
draftRule: AutoArchiveRule;
onSave: () => Promise<void>;
onCancel?: () => Promise<void>;
folderPathInput: HTMLInputElement;
validationErrorEl: HTMLDivElement;
closeReason: "none" | "saved" | "cancelled" = "none";
constructor(
app: App,
plugin: SimpleArchiver,
rule: AutoArchiveRule,
onSave: () => Promise<void>,
onCancel?: () => Promise<void>,
) {
super(app);
this.plugin = plugin;
this.rule = rule;
this.draftRule = this.cloneRule(rule);
this.onSave = onSave;
this.onCancel = onCancel;
}
onOpen() {
const { contentEl } = this;
contentEl.empty();
this.setTitle("Edit Auto-Archive Rule");
this.validationErrorEl = contentEl.createDiv({
cls: "auto-archive-rule-validation-error",
});
new Setting(contentEl)
.setName("Folder path")
.setDesc(
"The folder to apply this rule to (e.g., 'Projects' or 'Notes/Daily')",
)
.addText((text) => {
this.folderPathInput = text.inputEl;
text.setPlaceholder("folder/path")
.setValue(this.draftRule.folderPath)
.onChange((value) => {
this.draftRule.folderPath = value;
});
});
new Setting(contentEl)
.setName("Use folder path as regular expression")
.setDesc(
"When enabled, the folder path will be treated as a regular expression pattern to match multiple folders",
)
.addToggle((toggle) =>
toggle
.setValue(this.draftRule.useFolderRegex || false)
.onChange((value) => {
this.draftRule.useFolderRegex = value;
}),
);
new Setting(contentEl)
.setName("Apply recursively to subfolders")
.setDesc(
"When enabled, the rule will be applied to all files in subfolders as well",
)
.addToggle((toggle) =>
toggle
.setValue(this.draftRule.applyRecursively || false)
.onChange((value) => {
this.draftRule.applyRecursively = value;
}),
);
new Setting(contentEl)
.setName("Logic operator")
.setDesc("How to combine multiple conditions")
.addDropdown((dropdown) =>
dropdown
.addOption("AND", "AND (all conditions must match)")
.addOption("OR", "OR (any condition must match)")
.setValue(this.draftRule.logicOperator || "AND")
.onChange((value) => {
this.draftRule.logicOperator = value as "AND" | "OR";
}),
);
contentEl.createEl("h3", { text: "Conditions" });
const conditionsContainer = contentEl.createDiv({
cls: "auto-archive-conditions-container",
});
this.displayConditions(conditionsContainer);
new Setting(contentEl).addButton((button) =>
button.setButtonText("Add Condition").onClick(() => {
this.addCondition();
}),
);
new Setting(contentEl)
.addButton((button) =>
button
.setButtonText("Save")
.setCta()
.onClick(async () => {
this.clearValidationError();
const validationMessage = this.validateRule();
if (validationMessage) {
this.showValidationError(validationMessage);
return;
}
this.applyDraftToRule();
await this.onSave();
this.closeReason = "saved";
this.close();
}),
)
.addButton((button) =>
button.setButtonText("Cancel").onClick(async () => {
await this.handleCancel();
this.closeReason = "cancelled";
this.close();
}),
);
}
private displayConditions(containerEl: HTMLElement): void {
containerEl.empty();
if (this.draftRule.conditions.length === 0) {
containerEl.createEl("p", {
text: "No conditions added yet. Add at least one condition.",
cls: "setting-item-description",
});
return;
}
for (let i = 0; i < this.draftRule.conditions.length; i++) {
const condition = this.draftRule.conditions[i];
this.displayCondition(containerEl, condition, i);
}
}
private displayCondition(
containerEl: HTMLElement,
condition: AutoArchiveCondition,
index: number,
): void {
const conditionEl = containerEl.createDiv({
cls: "auto-archive-condition",
});
new Setting(conditionEl)
.setName(`Condition ${index + 1}`)
.addDropdown((dropdown) =>
dropdown
.addOption("fileAge", "File age (days)")
.addOption("regexPattern", "File name regex")
.setValue(condition.type)
.onChange((value) => {
const conditionType = value as
| "fileAge"
| "regexPattern";
if (
conditionType === "fileAge" ||
conditionType === "regexPattern"
) {
condition.type = conditionType;
condition.value = "";
this.displayConditions(containerEl);
}
}),
)
.addText((text) =>
text
.setPlaceholder(
condition.type === "fileAge"
? "Number of days"
: "Regular expression",
)
.setValue(condition.value)
.onChange((value) => {
condition.value = value;
}),
)
.addButton((button) =>
button
.setButtonText("Remove")
.setWarning()
.onClick(() => {
this.draftRule.conditions.splice(index, 1);
this.displayConditions(containerEl);
}),
);
}
private addCondition(): void {
this.draftRule.conditions.push({
type: "fileAge",
value: "",
});
const conditionsContainer = this.contentEl.querySelector(
".auto-archive-conditions-container",
) as HTMLElement;
if (conditionsContainer) {
this.displayConditions(conditionsContainer);
}
}
private validateRule(): string | null {
const folderPath = (this.draftRule.folderPath ?? "").trim();
if (!folderPath) {
return "Folder path is required.";
}
if (this.draftRule.conditions.length === 0) {
return "Cannot save rule with no conditions.";
}
for (let index = 0; index < this.draftRule.conditions.length; index++) {
const condition = this.draftRule.conditions[index];
const conditionValue = (condition.value ?? "").trim();
if (!conditionValue) {
return `Condition ${index + 1} requires a value.`;
}
if (condition.type === "fileAge") {
const dayCount = Number(conditionValue);
if (!Number.isInteger(dayCount) || dayCount <= 0) {
return `Condition ${index + 1} must be a positive whole number of days.`;
}
}
if (condition.type === "regexPattern") {
try {
new RegExp(conditionValue);
} catch {
return `Condition ${index + 1} has an invalid regular expression.`;
}
}
}
const archiveFolder = this.normalizeRulePath(
this.plugin.settings.archiveFolder,
);
if (!archiveFolder) {
return null;
}
if (this.draftRule.useFolderRegex) {
try {
const folderRegex = new RegExp(this.draftRule.folderPath);
if (folderRegex.test(archiveFolder)) {
return "Rule folder path cannot match the archive folder path.";
}
} catch {
return "Folder path regex is invalid.";
}
return null;
}
const ruleFolderPath = this.normalizeRulePath(
this.draftRule.folderPath,
);
if (this.isArchiveFolderOverlap(ruleFolderPath, archiveFolder)) {
return "Rule folder path cannot match or overlap with the archive folder path.";
}
return null;
}
private showValidationError(message: string): void {
this.validationErrorEl.setText(message);
this.validationErrorEl.addClass("is-visible");
}
private clearValidationError(): void {
this.validationErrorEl.empty();
this.validationErrorEl.removeClass("is-visible");
}
private normalizeRulePath(path: string): string {
const trimmedPath = (path ?? "").trim();
if (!trimmedPath) {
return "";
}
return normalizePath(trimmedPath).replace(/^\/+|\/+$/g, "");
}
private isArchiveFolderOverlap(
ruleFolderPath: string,
archiveFolderPath: string,
): boolean {
if (!ruleFolderPath) {
return true;
}
if (ruleFolderPath === archiveFolderPath) {
return true;
}
if (ruleFolderPath.startsWith(`${archiveFolderPath}/`)) {
return true;
}
return archiveFolderPath.startsWith(`${ruleFolderPath}/`);
}
private cloneRule(rule: AutoArchiveRule): AutoArchiveRule {
return {
...rule,
conditions: rule.conditions.map((condition) => ({
...condition,
})),
};
}
private applyDraftToRule(): void {
this.rule.enabled = this.draftRule.enabled;
this.rule.folderPath = this.draftRule.folderPath;
this.rule.useFolderRegex = this.draftRule.useFolderRegex;
this.rule.applyRecursively = this.draftRule.applyRecursively;
this.rule.logicOperator = this.draftRule.logicOperator;
this.rule.conditions = this.draftRule.conditions.map((condition) => ({
...condition,
}));
}
private async handleCancel(): Promise<void> {
if (this.onCancel) {
await this.onCancel();
return;
}
if (!this.rule.folderPath) {
this.plugin.settings.autoArchiveRules =
this.plugin.settings.autoArchiveRules.filter(
(r) => r.id !== this.rule.id,
);
await this.plugin.saveSettings();
}
}
onClose() {
const { contentEl } = this;
if (this.closeReason === "none") {
void this.handleCancel();
}
contentEl.empty();
}
}

365
package-lock.json generated
View file

@ -1,17 +1,18 @@
{
"name": "simple-archiver",
"version": "0.6.2",
"version": "0.5.2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "simple-archiver",
"version": "0.6.2",
"version": "0.5.2",
"license": "GPL-3.0-only",
"devDependencies": {
"@types/node": "^16.11.6",
"@typescript-eslint/eslint-plugin": "5.29.0",
"@typescript-eslint/parser": "5.29.0",
"builtin-modules": "3.3.0",
"esbuild": "^0.25.0",
"obsidian": "latest",
"tslib": "2.4.0",
@ -19,9 +20,9 @@
}
},
"node_modules/@codemirror/state": {
"version": "6.5.0",
"resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.0.tgz",
"integrity": "sha512-MwBHVK60IiIHDcoMet78lxt6iw5gJOGSbNbOIVBHWVXIH4/Nq1+GQgLLGgI1KlnN86WDXsPudVaqYHKBIx7Eyw==",
"version": "6.5.2",
"resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.2.tgz",
"integrity": "sha512-FVqsPqtPWKVVL3dPSxy8wEF/ymIEuVzF1PK3VbUgrxXpJUSHQWWZz4JMToquRxnkw+36LTamCZG2iua2Ptq0fA==",
"dev": true,
"license": "MIT",
"peer": true,
@ -30,23 +31,22 @@
}
},
"node_modules/@codemirror/view": {
"version": "6.38.6",
"resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.38.6.tgz",
"integrity": "sha512-qiS0z1bKs5WOvHIAC0Cybmv4AJSkAXgX5aD6Mqd2epSLlVJsQl8NG23jCVouIgkh4All/mrbdsf2UOLFnJw0tw==",
"version": "6.36.4",
"resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.36.4.tgz",
"integrity": "sha512-ZQ0V5ovw/miKEXTvjgzRyjnrk9TwriUB1k4R5p7uNnHR9Hus+D1SXHGdJshijEzPFjU25xea/7nhIeSqYFKdbA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@codemirror/state": "^6.5.0",
"crelt": "^1.0.6",
"style-mod": "^4.1.0",
"w3c-keyname": "^2.2.4"
}
},
"node_modules/@esbuild/aix-ppc64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz",
"integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==",
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.0.tgz",
"integrity": "sha512-O7vun9Sf8DFjH2UtqK8Ku3LkquL9SZL8OLY1T5NZkA34+wG3OQF7cl4Ql8vdNzM6fzBbYfLaiRLIOZ+2FOCgBQ==",
"cpu": [
"ppc64"
],
@ -61,9 +61,9 @@
}
},
"node_modules/@esbuild/android-arm": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz",
"integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==",
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.0.tgz",
"integrity": "sha512-PTyWCYYiU0+1eJKmw21lWtC+d08JDZPQ5g+kFyxP0V+es6VPPSUhM6zk8iImp2jbV6GwjX4pap0JFbUQN65X1g==",
"cpu": [
"arm"
],
@ -78,9 +78,9 @@
}
},
"node_modules/@esbuild/android-arm64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz",
"integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==",
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.0.tgz",
"integrity": "sha512-grvv8WncGjDSyUBjN9yHXNt+cq0snxXbDxy5pJtzMKGmmpPxeAmAhWxXI+01lU5rwZomDgD3kJwulEnhTRUd6g==",
"cpu": [
"arm64"
],
@ -95,9 +95,9 @@
}
},
"node_modules/@esbuild/android-x64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz",
"integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==",
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.0.tgz",
"integrity": "sha512-m/ix7SfKG5buCnxasr52+LI78SQ+wgdENi9CqyCXwjVR2X4Jkz+BpC3le3AoBPYTC9NHklwngVXvbJ9/Akhrfg==",
"cpu": [
"x64"
],
@ -112,9 +112,9 @@
}
},
"node_modules/@esbuild/darwin-arm64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz",
"integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==",
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.0.tgz",
"integrity": "sha512-mVwdUb5SRkPayVadIOI78K7aAnPamoeFR2bT5nszFUZ9P8UpK4ratOdYbZZXYSqPKMHfS1wdHCJk1P1EZpRdvw==",
"cpu": [
"arm64"
],
@ -129,9 +129,9 @@
}
},
"node_modules/@esbuild/darwin-x64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz",
"integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==",
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.0.tgz",
"integrity": "sha512-DgDaYsPWFTS4S3nWpFcMn/33ZZwAAeAFKNHNa1QN0rI4pUjgqf0f7ONmXf6d22tqTY+H9FNdgeaAa+YIFUn2Rg==",
"cpu": [
"x64"
],
@ -146,9 +146,9 @@
}
},
"node_modules/@esbuild/freebsd-arm64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz",
"integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==",
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.0.tgz",
"integrity": "sha512-VN4ocxy6dxefN1MepBx/iD1dH5K8qNtNe227I0mnTRjry8tj5MRk4zprLEdG8WPyAPb93/e4pSgi1SoHdgOa4w==",
"cpu": [
"arm64"
],
@ -163,9 +163,9 @@
}
},
"node_modules/@esbuild/freebsd-x64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz",
"integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==",
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.0.tgz",
"integrity": "sha512-mrSgt7lCh07FY+hDD1TxiTyIHyttn6vnjesnPoVDNmDfOmggTLXRv8Id5fNZey1gl/V2dyVK1VXXqVsQIiAk+A==",
"cpu": [
"x64"
],
@ -180,9 +180,9 @@
}
},
"node_modules/@esbuild/linux-arm": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz",
"integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==",
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.0.tgz",
"integrity": "sha512-vkB3IYj2IDo3g9xX7HqhPYxVkNQe8qTK55fraQyTzTX/fxaDtXiEnavv9geOsonh2Fd2RMB+i5cbhu2zMNWJwg==",
"cpu": [
"arm"
],
@ -197,9 +197,9 @@
}
},
"node_modules/@esbuild/linux-arm64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz",
"integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==",
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.0.tgz",
"integrity": "sha512-9QAQjTWNDM/Vk2bgBl17yWuZxZNQIF0OUUuPZRKoDtqF2k4EtYbpyiG5/Dk7nqeK6kIJWPYldkOcBqjXjrUlmg==",
"cpu": [
"arm64"
],
@ -214,9 +214,9 @@
}
},
"node_modules/@esbuild/linux-ia32": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz",
"integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==",
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.0.tgz",
"integrity": "sha512-43ET5bHbphBegyeqLb7I1eYn2P/JYGNmzzdidq/w0T8E2SsYL1U6un2NFROFRg1JZLTzdCoRomg8Rvf9M6W6Gg==",
"cpu": [
"ia32"
],
@ -231,9 +231,9 @@
}
},
"node_modules/@esbuild/linux-loong64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz",
"integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==",
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.0.tgz",
"integrity": "sha512-fC95c/xyNFueMhClxJmeRIj2yrSMdDfmqJnyOY4ZqsALkDrrKJfIg5NTMSzVBr5YW1jf+l7/cndBfP3MSDpoHw==",
"cpu": [
"loong64"
],
@ -248,9 +248,9 @@
}
},
"node_modules/@esbuild/linux-mips64el": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz",
"integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==",
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.0.tgz",
"integrity": "sha512-nkAMFju7KDW73T1DdH7glcyIptm95a7Le8irTQNO/qtkoyypZAnjchQgooFUDQhNAy4iu08N79W4T4pMBwhPwQ==",
"cpu": [
"mips64el"
],
@ -265,9 +265,9 @@
}
},
"node_modules/@esbuild/linux-ppc64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz",
"integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==",
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.0.tgz",
"integrity": "sha512-NhyOejdhRGS8Iwv+KKR2zTq2PpysF9XqY+Zk77vQHqNbo/PwZCzB5/h7VGuREZm1fixhs4Q/qWRSi5zmAiO4Fw==",
"cpu": [
"ppc64"
],
@ -282,9 +282,9 @@
}
},
"node_modules/@esbuild/linux-riscv64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz",
"integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==",
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.0.tgz",
"integrity": "sha512-5S/rbP5OY+GHLC5qXp1y/Mx//e92L1YDqkiBbO9TQOvuFXM+iDqUNG5XopAnXoRH3FjIUDkeGcY1cgNvnXp/kA==",
"cpu": [
"riscv64"
],
@ -299,9 +299,9 @@
}
},
"node_modules/@esbuild/linux-s390x": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz",
"integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==",
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.0.tgz",
"integrity": "sha512-XM2BFsEBz0Fw37V0zU4CXfcfuACMrppsMFKdYY2WuTS3yi8O1nFOhil/xhKTmE1nPmVyvQJjJivgDT+xh8pXJA==",
"cpu": [
"s390x"
],
@ -316,9 +316,9 @@
}
},
"node_modules/@esbuild/linux-x64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz",
"integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==",
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.0.tgz",
"integrity": "sha512-9yl91rHw/cpwMCNytUDxwj2XjFpxML0y9HAOH9pNVQDpQrBxHy01Dx+vaMu0N1CKa/RzBD2hB4u//nfc+Sd3Cw==",
"cpu": [
"x64"
],
@ -333,9 +333,9 @@
}
},
"node_modules/@esbuild/netbsd-arm64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz",
"integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==",
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.0.tgz",
"integrity": "sha512-RuG4PSMPFfrkH6UwCAqBzauBWTygTvb1nxWasEJooGSJ/NwRw7b2HOwyRTQIU97Hq37l3npXoZGYMy3b3xYvPw==",
"cpu": [
"arm64"
],
@ -350,9 +350,9 @@
}
},
"node_modules/@esbuild/netbsd-x64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz",
"integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==",
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.0.tgz",
"integrity": "sha512-jl+qisSB5jk01N5f7sPCsBENCOlPiS/xptD5yxOx2oqQfyourJwIKLRA2yqWdifj3owQZCL2sn6o08dBzZGQzA==",
"cpu": [
"x64"
],
@ -367,9 +367,9 @@
}
},
"node_modules/@esbuild/openbsd-arm64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz",
"integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==",
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.0.tgz",
"integrity": "sha512-21sUNbq2r84YE+SJDfaQRvdgznTD8Xc0oc3p3iW/a1EVWeNj/SdUCbm5U0itZPQYRuRTW20fPMWMpcrciH2EJw==",
"cpu": [
"arm64"
],
@ -384,9 +384,9 @@
}
},
"node_modules/@esbuild/openbsd-x64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz",
"integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==",
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.0.tgz",
"integrity": "sha512-2gwwriSMPcCFRlPlKx3zLQhfN/2WjJ2NSlg5TKLQOJdV0mSxIcYNTMhk3H3ulL/cak+Xj0lY1Ym9ysDV1igceg==",
"cpu": [
"x64"
],
@ -400,27 +400,10 @@
"node": ">=18"
}
},
"node_modules/@esbuild/openharmony-arm64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz",
"integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openharmony"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/sunos-x64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz",
"integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==",
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.0.tgz",
"integrity": "sha512-bxI7ThgLzPrPz484/S9jLlvUAHYMzy6I0XiU1ZMeAEOBcS0VePBFxh1JjTQt3Xiat5b6Oh4x7UC7IwKQKIJRIg==",
"cpu": [
"x64"
],
@ -435,9 +418,9 @@
}
},
"node_modules/@esbuild/win32-arm64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz",
"integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==",
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.0.tgz",
"integrity": "sha512-ZUAc2YK6JW89xTbXvftxdnYy3m4iHIkDtK3CLce8wg8M2L+YZhIvO1DKpxrd0Yr59AeNNkTiic9YLf6FTtXWMw==",
"cpu": [
"arm64"
],
@ -452,9 +435,9 @@
}
},
"node_modules/@esbuild/win32-ia32": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz",
"integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==",
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.0.tgz",
"integrity": "sha512-eSNxISBu8XweVEWG31/JzjkIGbGIJN/TrRoiSVZwZ6pkC6VX4Im/WV2cz559/TXLcYbcrDN8JtKgd9DJVIo8GA==",
"cpu": [
"ia32"
],
@ -469,9 +452,9 @@
}
},
"node_modules/@esbuild/win32-x64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz",
"integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==",
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.0.tgz",
"integrity": "sha512-ZENoHJBxA20C2zFzh6AI4fT6RraMzjYw4xKWemRTRmRVtN9c5DcH9r/f2ihEkMjOW5eGgrwCslG/+Y/3bL+DHQ==",
"cpu": [
"x64"
],
@ -486,9 +469,9 @@
}
},
"node_modules/@eslint-community/eslint-utils": {
"version": "4.9.1",
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz",
"integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==",
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.1.tgz",
"integrity": "sha512-s3O3waFUrMV8P/XaF/+ZTp1X9XBZW1a4B97ZnjQF2KYWaFD2A8KyFBsrsfSjEmjn3RGWAIuvlneuZm3CUK3jbA==",
"dev": true,
"license": "MIT",
"peer": true,
@ -506,9 +489,9 @@
}
},
"node_modules/@eslint-community/regexpp": {
"version": "4.12.2",
"resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz",
"integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==",
"version": "4.12.1",
"resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz",
"integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==",
"dev": true,
"license": "MIT",
"peer": true,
@ -650,9 +633,9 @@
}
},
"node_modules/@types/estree": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz",
"integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==",
"dev": true,
"license": "MIT"
},
@ -881,9 +864,9 @@
"peer": true
},
"node_modules/acorn": {
"version": "8.16.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz",
"integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
"version": "8.14.1",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz",
"integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==",
"dev": true,
"license": "MIT",
"peer": true,
@ -906,9 +889,9 @@
}
},
"node_modules/ajv": {
"version": "6.14.0",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz",
"integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==",
"version": "6.12.6",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
"dev": true,
"license": "MIT",
"peer": true,
@ -978,9 +961,9 @@
"peer": true
},
"node_modules/brace-expansion": {
"version": "1.1.13",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz",
"integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==",
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"dev": true,
"license": "MIT",
"peer": true,
@ -1002,6 +985,19 @@
"node": ">=8"
}
},
"node_modules/builtin-modules": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.3.0.tgz",
"integrity": "sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/callsites": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
@ -1061,14 +1057,6 @@
"license": "MIT",
"peer": true
},
"node_modules/crelt": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz",
"integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==",
"dev": true,
"license": "MIT",
"peer": true
},
"node_modules/cross-spawn": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
@ -1086,9 +1074,9 @@
}
},
"node_modules/debug": {
"version": "4.4.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
"integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
"dev": true,
"license": "MIT",
"dependencies": {
@ -1139,9 +1127,9 @@
}
},
"node_modules/esbuild": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz",
"integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==",
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.0.tgz",
"integrity": "sha512-BXq5mqc8ltbaN34cDqWuYKyNhX8D/Z0J1xdtdQ8UcIIIyJyz+ZMKUt58tF3SrZ85jcfN/PZYhjR5uDQAYNVbuw==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
@ -1152,32 +1140,31 @@
"node": ">=18"
},
"optionalDependencies": {
"@esbuild/aix-ppc64": "0.25.12",
"@esbuild/android-arm": "0.25.12",
"@esbuild/android-arm64": "0.25.12",
"@esbuild/android-x64": "0.25.12",
"@esbuild/darwin-arm64": "0.25.12",
"@esbuild/darwin-x64": "0.25.12",
"@esbuild/freebsd-arm64": "0.25.12",
"@esbuild/freebsd-x64": "0.25.12",
"@esbuild/linux-arm": "0.25.12",
"@esbuild/linux-arm64": "0.25.12",
"@esbuild/linux-ia32": "0.25.12",
"@esbuild/linux-loong64": "0.25.12",
"@esbuild/linux-mips64el": "0.25.12",
"@esbuild/linux-ppc64": "0.25.12",
"@esbuild/linux-riscv64": "0.25.12",
"@esbuild/linux-s390x": "0.25.12",
"@esbuild/linux-x64": "0.25.12",
"@esbuild/netbsd-arm64": "0.25.12",
"@esbuild/netbsd-x64": "0.25.12",
"@esbuild/openbsd-arm64": "0.25.12",
"@esbuild/openbsd-x64": "0.25.12",
"@esbuild/openharmony-arm64": "0.25.12",
"@esbuild/sunos-x64": "0.25.12",
"@esbuild/win32-arm64": "0.25.12",
"@esbuild/win32-ia32": "0.25.12",
"@esbuild/win32-x64": "0.25.12"
"@esbuild/aix-ppc64": "0.25.0",
"@esbuild/android-arm": "0.25.0",
"@esbuild/android-arm64": "0.25.0",
"@esbuild/android-x64": "0.25.0",
"@esbuild/darwin-arm64": "0.25.0",
"@esbuild/darwin-x64": "0.25.0",
"@esbuild/freebsd-arm64": "0.25.0",
"@esbuild/freebsd-x64": "0.25.0",
"@esbuild/linux-arm": "0.25.0",
"@esbuild/linux-arm64": "0.25.0",
"@esbuild/linux-ia32": "0.25.0",
"@esbuild/linux-loong64": "0.25.0",
"@esbuild/linux-mips64el": "0.25.0",
"@esbuild/linux-ppc64": "0.25.0",
"@esbuild/linux-riscv64": "0.25.0",
"@esbuild/linux-s390x": "0.25.0",
"@esbuild/linux-x64": "0.25.0",
"@esbuild/netbsd-arm64": "0.25.0",
"@esbuild/netbsd-x64": "0.25.0",
"@esbuild/openbsd-arm64": "0.25.0",
"@esbuild/openbsd-x64": "0.25.0",
"@esbuild/sunos-x64": "0.25.0",
"@esbuild/win32-arm64": "0.25.0",
"@esbuild/win32-ia32": "0.25.0",
"@esbuild/win32-x64": "0.25.0"
}
},
"node_modules/escape-string-regexp": {
@ -1357,9 +1344,9 @@
}
},
"node_modules/esquery": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz",
"integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==",
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz",
"integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==",
"dev": true,
"license": "BSD-3-Clause",
"peer": true,
@ -1480,9 +1467,9 @@
"peer": true
},
"node_modules/fastq": {
"version": "1.20.1",
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz",
"integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==",
"version": "1.19.1",
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz",
"integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==",
"dev": true,
"license": "ISC",
"dependencies": {
@ -1551,9 +1538,9 @@
}
},
"node_modules/flatted": {
"version": "3.4.2",
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz",
"integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==",
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz",
"integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==",
"dev": true,
"license": "ISC",
"peer": true
@ -1577,7 +1564,7 @@
"version": "7.2.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
"deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me",
"deprecated": "Glob versions prior to v9 are no longer supported",
"dev": true,
"license": "ISC",
"peer": true,
@ -1780,9 +1767,9 @@
"peer": true
},
"node_modules/js-yaml": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
"integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
"dev": true,
"license": "MIT",
"peer": true,
@ -1893,9 +1880,9 @@
}
},
"node_modules/minimatch": {
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
"integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"dev": true,
"license": "ISC",
"peer": true,
@ -1932,9 +1919,9 @@
"peer": true
},
"node_modules/obsidian": {
"version": "1.12.3",
"resolved": "https://registry.npmjs.org/obsidian/-/obsidian-1.12.3.tgz",
"integrity": "sha512-HxWqe763dOqzXjnNiHmAJTRERN8KILBSqxDSEqbeSr7W8R8Jxezzbca+nz1LiiqXnMpM8lV2jzAezw3CZ4xNUw==",
"version": "1.8.7",
"resolved": "https://registry.npmjs.org/obsidian/-/obsidian-1.8.7.tgz",
"integrity": "sha512-h4bWwNFAGRXlMlMAzdEiIM2ppTGlrh7uGOJS6w4gClrsjc+ei/3YAtU2VdFUlCiPuTHpY4aBpFJJW75S1Tl/JA==",
"dev": true,
"license": "MIT",
"dependencies": {
@ -1942,8 +1929,8 @@
"moment": "2.29.4"
},
"peerDependencies": {
"@codemirror/state": "6.5.0",
"@codemirror/view": "6.38.6"
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.0.0"
}
},
"node_modules/once": {
@ -2068,9 +2055,9 @@
}
},
"node_modules/picomatch": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
"integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
"dev": true,
"license": "MIT",
"engines": {
@ -2201,9 +2188,9 @@
}
},
"node_modules/semver": {
"version": "7.7.4",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
"integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
"version": "7.7.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz",
"integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==",
"dev": true,
"license": "ISC",
"bin": {
@ -2277,9 +2264,9 @@
}
},
"node_modules/style-mod": {
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.3.tgz",
"integrity": "sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==",
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.2.tgz",
"integrity": "sha512-wnD1HyVqpJUI2+eKZ+eo1UwghftP6yuFheBqqe+bWCotBjC2K1YnteJILRMs3SM4V/0dLEW1SC27MWP5y+mwmw==",
"dev": true,
"license": "MIT",
"peer": true

View file

@ -1,6 +1,6 @@
{
"name": "simple-archiver",
"version": "0.6.2",
"version": "0.5.2",
"description": "Send old, stinky notes to the archive, where they belong.",
"main": "main.js",
"scripts": {
@ -17,6 +17,7 @@
"@types/node": "^16.11.6",
"@typescript-eslint/eslint-plugin": "5.29.0",
"@typescript-eslint/parser": "5.29.0",
"builtin-modules": "3.3.0",
"esbuild": "^0.25.0",
"obsidian": "latest",
"tslib": "2.4.0",

View file

@ -6,96 +6,3 @@ available in the app when your plugin is enabled.
If your plugin does not need CSS, delete this file.
*/
/* Auto-archive settings styles */
.setting-tab-container {
width: 100%;
}
.setting-tab-content {
padding-top: 12px;
}
.setting-tab-buttons {
display: flex;
gap: 8px;
margin-bottom: 20px;
border-bottom: 1px solid var(--background-modifier-border);
}
.setting-tab-button {
padding: 8px 16px;
background: transparent;
border: none;
border-bottom: 2px solid transparent;
cursor: pointer;
font-size: 14px;
color: var(--text-muted);
}
.setting-tab-button.active {
color: var(--text-normal);
border-bottom-color: var(--interactive-accent);
}
.setting-tab-button:hover {
color: var(--text-normal);
}
.auto-archive-rules-container {
margin-top: 20px;
}
.auto-archive-rule {
padding: 16px;
margin-bottom: 16px;
border: 1px solid var(--background-modifier-border);
border-radius: 4px;
background-color: var(--background-secondary);
}
.auto-archive-rule-header {
border-bottom: 1px solid var(--background-modifier-border);
padding-bottom: 8px;
margin-bottom: 8px;
}
.auto-archive-rule-conditions {
padding-left: 16px;
font-size: 0.9em;
color: var(--text-muted);
}
.auto-archive-rule-logic {
margin-top: 4px;
margin-bottom: 8px;
font-weight: 600;
color: var(--text-accent);
}
.auto-archive-rule-condition {
margin-top: 4px;
}
.auto-archive-conditions-container {
margin: 16px 0;
}
.auto-archive-condition {
margin-bottom: 12px;
padding: 12px;
border: 1px solid var(--background-modifier-border);
border-radius: 4px;
background-color: var(--background-primary);
}
.auto-archive-rule-validation-error {
display: none;
color: var(--text-error);
font-weight: 600;
margin-bottom: 12px;
}
.auto-archive-rule-validation-error.is-visible {
display: block;
}