diff --git a/__tests__/classifier/ClassificationService.test.ts b/__tests__/classifier/ClassificationService.test.ts index 971980a..ba23e4f 100644 --- a/__tests__/classifier/ClassificationService.test.ts +++ b/__tests__/classifier/ClassificationService.test.ts @@ -1,27 +1,27 @@ -import { ClassificationService, ClassificationContext } from '../../src/classifier/ClassificationService'; +import { ClassificationService, ClassificationContext } from '../../src/ui/ClassificationService'; import type { App, TFile, MetadataCache, Vault } from 'obsidian'; import type { FrontmatterField, ProviderConfig, OAuthTokens } from '../../src/types'; -import { processAPIRequest } from '../../src/provider'; -import { insertToFrontMatter, getFieldValues } from '../../src/lib/frontmatter'; -import { getPromptTemplate } from '../../src/provider/prompt'; +import { processAPIRequest } from '../../src/ui/provider-api'; +import { insertToFrontMatter, getFieldValues } from '../../src/ui/frontmatter'; +import { getPromptTemplate } from '../../src/domain/prompt'; // Mock dependencies -vi.mock('../../src/provider', () => ({ +vi.mock('../../src/ui/provider-api', () => ({ processAPIRequest: vi.fn(), })); -vi.mock('../../src/provider/prompt', () => ({ +vi.mock('../../src/domain/prompt', () => ({ DEFAULT_SYSTEM_ROLE: 'Test system role', getPromptTemplate: vi.fn().mockReturnValue('Test prompt'), })); -vi.mock('../../src/lib/frontmatter', () => ({ +vi.mock('../../src/ui/frontmatter', () => ({ getContentWithoutFrontmatter: vi.fn().mockReturnValue('Test content'), getFieldValues: vi.fn().mockReturnValue(['tag1', 'tag2']), insertToFrontMatter: vi.fn().mockResolvedValue(undefined), })); -vi.mock('../../src/settings/components/Notice', () => ({ +vi.mock('../../src/ui/settings/components/Notice', () => ({ Notice: { error: vi.fn(), success: vi.fn(), diff --git a/__tests__/main.test.ts b/__tests__/main.test.ts index 5843873..e34c8db 100644 --- a/__tests__/main.test.ts +++ b/__tests__/main.test.ts @@ -1,7 +1,7 @@ import AutoClassifierPlugin from 'main'; import { App, TFile, createMockTFile } from 'obsidian'; -import { DEFAULT_SETTINGS, DEFAULT_FRONTMATTER_SETTING } from '../src/constants'; -import type { ProviderConfig, FrontmatterField, OAuthTokens } from 'types'; +import { DEFAULT_SETTINGS, DEFAULT_FRONTMATTER_SETTING } from '../src/domain/constants'; +import type { ProviderConfig, FrontmatterField, OAuthTokens } from '../src/types/index'; import { Notice as SettingsNotice } from 'settings/components/Notice'; import { processAPIRequest } from 'provider'; import { insertToFrontMatter, getFieldValues } from 'lib/frontmatter'; diff --git a/__tests__/provider/auth/oauth.test.ts b/__tests__/provider/auth/oauth.test.ts index ece29a8..9317351 100644 --- a/__tests__/provider/auth/oauth.test.ts +++ b/__tests__/provider/auth/oauth.test.ts @@ -1,9 +1,9 @@ import { vi, describe, it, expect, beforeEach } from 'vitest'; import { Platform, requestUrl } from 'obsidian'; -import { CodexOAuth } from '../../../src/provider/auth/oauth'; -import type { OAuthTokens } from '../../../src/provider/auth/types'; -import { OAuthCallbackServer } from '../../../src/provider/auth/oauth-server'; -import { createTokensFromResponse, isTokenExpired } from '../../../src/provider/auth/token-manager'; +import { CodexOAuth } from '../../../src/ui/auth/oauth'; +import type { OAuthTokens } from '../../../src/types/auth'; +import { OAuthCallbackServer } from '../../../src/domain/auth/oauth-server'; +import { createTokensFromResponse, isTokenExpired } from '../../../src/domain/auth/token-manager'; import type { Mock } from 'vitest'; // Mock obsidian @@ -15,7 +15,7 @@ vi.mock('obsidian', () => ({ })); // Mock the oauth-server - use a class-like constructor mock -vi.mock('../../../src/provider/auth/oauth-server', () => { +vi.mock('../../../src/domain/auth/oauth-server', () => { const MockServer = vi.fn(function (this: any) { this.waitForCallback = vi.fn(); this.stop = vi.fn(); @@ -24,7 +24,7 @@ vi.mock('../../../src/provider/auth/oauth-server', () => { }); // Mock pkce -vi.mock('../../../src/provider/auth/pkce', () => ({ +vi.mock('../../../src/domain/auth/pkce', () => ({ generatePKCEChallenge: vi.fn().mockResolvedValue({ codeVerifier: 'test-verifier', codeChallenge: 'test-challenge', @@ -33,7 +33,7 @@ vi.mock('../../../src/provider/auth/pkce', () => ({ })); // Mock token-manager -vi.mock('../../../src/provider/auth/token-manager', () => ({ +vi.mock('../../../src/domain/auth/token-manager', () => ({ createTokensFromResponse: vi.fn().mockReturnValue({ accessToken: 'new-access-token', refreshToken: 'new-refresh-token', diff --git a/__tests__/provider/auth/pkce.test.ts b/__tests__/provider/auth/pkce.test.ts index 960e2b8..f5abb17 100644 --- a/__tests__/provider/auth/pkce.test.ts +++ b/__tests__/provider/auth/pkce.test.ts @@ -3,7 +3,7 @@ import { generateCodeChallenge, generateState, generatePKCEChallenge, -} from '../../../src/provider/auth/pkce'; +} from '../../../src/domain/auth/pkce'; describe('pkce', () => { describe('generateCodeVerifier', () => { diff --git a/__tests__/provider/auth/token-manager.test.ts b/__tests__/provider/auth/token-manager.test.ts index 4e3fb33..f261661 100644 --- a/__tests__/provider/auth/token-manager.test.ts +++ b/__tests__/provider/auth/token-manager.test.ts @@ -5,8 +5,8 @@ import { createTokensFromResponse, getTokenRemainingTime, formatTokenExpiry, -} from '../../../src/provider/auth/token-manager'; -import type { OAuthTokens, TokenResponse } from '../../../src/provider/auth/types'; +} from '../../../src/domain/auth/token-manager'; +import type { OAuthTokens, TokenResponse } from '../../../src/types/auth'; describe('token-manager', () => { // Mock Date.now for consistent testing diff --git a/__tests__/settings/ProviderSection.test.ts b/__tests__/settings/ProviderSection.test.ts index 0213715..f0716f5 100644 --- a/__tests__/settings/ProviderSection.test.ts +++ b/__tests__/settings/ProviderSection.test.ts @@ -1,13 +1,13 @@ import type { Mock } from 'vitest'; -import { ProviderSection } from '../../src/settings/ProviderSection'; +import { ProviderSection } from '../../src/ui/settings/ProviderSection'; import type { App } from 'obsidian'; import type AutoClassifierPlugin from '../../src/main'; import type { ProviderConfig, OAuthTokens } from '../../src/types'; -import { formatTokenExpiry, isTokenExpired } from '../../src/provider/auth'; -import { Setting } from '../../src/settings/components/Setting'; +import { formatTokenExpiry, isTokenExpired } from '../../src/ui/auth'; +import { Setting } from '../../src/ui/settings/components/Setting'; // Mock the provider/auth module -vi.mock('../../src/provider/auth', () => ({ +vi.mock('../../src/ui/auth', () => ({ formatTokenExpiry: vi.fn((tokens) => { const remaining = tokens.expiresAt - Math.floor(Date.now() / 1000); if (remaining <= 0) return 'Expired'; @@ -22,14 +22,14 @@ vi.mock('../../src/provider/auth', () => ({ })); // Mock the ProviderModal -vi.mock('../../src/settings/modals/ProviderModal', () => ({ +vi.mock('../../src/ui/settings/modals/ProviderModal', () => ({ ProviderModal: vi.fn().mockImplementation(() => ({ open: vi.fn(), })), })); // Mock Setting component -vi.mock('../../src/settings/components/Setting', () => ({ +vi.mock('../../src/ui/settings/components/Setting', () => ({ Setting: { create: vi.fn(), }, diff --git a/__tests__/settings/TagSection.test.ts b/__tests__/settings/TagSection.test.ts index 2267b56..71b102d 100644 --- a/__tests__/settings/TagSection.test.ts +++ b/__tests__/settings/TagSection.test.ts @@ -5,7 +5,7 @@ vi.mock('settings/modals/FrontmatterEditorModal', () => ({ import type { Mock } from 'vitest'; import type { FrontmatterField } from 'types'; import { Tag } from 'settings/TagSection'; -import { DEFAULT_TAG_SETTING } from '../../src/constants'; +import { DEFAULT_TAG_SETTING } from '../../src/domain/constants'; interface MockPlugin { app: { vault: { getMarkdownFiles: Mock } }; diff --git a/eslint.base.js b/eslint.base.js index ae1291f..168952a 100644 --- a/eslint.base.js +++ b/eslint.base.js @@ -24,6 +24,18 @@ export const baseConfig = tseslint.config( 'no-console': 'error', }, }, + { + files: ['src/domain/**/*.ts', 'src/types/**/*.ts', 'src/utils/**/*.ts'], + ignores: ['**/*.d.ts'], + rules: { + 'no-restricted-imports': ['error', { + patterns: [{ + group: ['obsidian', 'obsidian/*'], + message: 'domain/, types/, and utils/ layers must not import from obsidian. Move this code to ui/ or inject the dependency.', + }], + }], + }, + }, { files: ['scripts/**/*.{js,mjs}', 'tooling/**/*.{js,mjs}', 'esbuild.config.mjs', 'version-bump.mjs'], languageOptions: { diff --git a/eslint.config.mjs b/eslint.config.mjs index 2cb6b4d..ee8ffb9 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -20,51 +20,56 @@ export default [ unicorn, }, settings: { - // Module boundary definitions - // Structure: provider/ | classifier/ | settings/ | lib/ + // Module boundary definitions — 4-layer architecture + // main → ui → domain → utils/types 'boundaries/elements': [ { - type: 'provider', - pattern: 'src/provider/**', + type: 'main', + pattern: 'src/main.ts', + mode: 'file', + }, + { + type: 'ui', + pattern: 'src/ui/**', mode: 'folder', }, { - type: 'classifier', - pattern: 'src/classifier/**', + type: 'domain', + pattern: 'src/domain/**', mode: 'folder', }, { - type: 'settings', - pattern: 'src/settings/**', + type: 'utils', + pattern: 'src/utils/**', mode: 'folder', }, { - type: 'lib', - pattern: 'src/lib/**', + type: 'types', + pattern: 'src/types/**', mode: 'folder', }, { - type: 'main', - pattern: 'src/main.ts', - mode: 'file', + type: 'shared', + pattern: 'src/shared/**', + mode: 'folder', }, ], - // Dependency rules between modules + // Dependency rules between layers 'boundaries/rules': [ { - from: 'settings', - disallow: ['provider'], - message: 'settings cannot import provider directly. Use classifier instead.', + from: 'domain', + disallow: ['ui'], + message: 'domain layer must not import from ui layer.', }, { - from: 'provider', - disallow: ['settings', 'classifier'], - message: 'provider is pure API layer. No UI/business logic dependency.', + from: 'utils', + disallow: ['ui', 'domain'], + message: 'utils layer must not import from ui or domain.', }, { - from: 'lib', - disallow: ['provider', 'classifier', 'settings'], - message: 'lib is pure utility. No domain module dependency.', + from: 'types', + disallow: ['ui', 'domain', 'utils'], + message: 'types layer must not import from other layers.', }, ], }, diff --git a/src/domain/.gitkeep b/src/domain/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/src/domain/auth/index.ts b/src/domain/auth/index.ts new file mode 100644 index 0000000..0798422 --- /dev/null +++ b/src/domain/auth/index.ts @@ -0,0 +1,16 @@ +// Domain auth module exports — pure, no obsidian imports +export { CODEX_OAUTH } from './oauth-constants'; +export { + isTokenExpired, + formatTokenExpiry, + getTokenRemainingTime, + parseJwtClaims, + extractAccountId, + createTokensFromResponse, +} from './token-manager'; +export type { + OAuthTokens, + PKCEChallenge, + TokenResponse, + OAuthCallbackResponse, +} from '../../types/auth'; diff --git a/src/provider/auth/oauth-constants.ts b/src/domain/auth/oauth-constants.ts similarity index 100% rename from src/provider/auth/oauth-constants.ts rename to src/domain/auth/oauth-constants.ts diff --git a/src/provider/auth/oauth-server.ts b/src/domain/auth/oauth-server.ts similarity index 98% rename from src/provider/auth/oauth-server.ts rename to src/domain/auth/oauth-server.ts index d2fc996..d9251f4 100644 --- a/src/provider/auth/oauth-server.ts +++ b/src/domain/auth/oauth-server.ts @@ -1,5 +1,5 @@ import { CODEX_OAUTH } from './oauth-constants'; -import type { OAuthCallbackResponse } from './types'; +import type { OAuthCallbackResponse } from '../../types/auth'; import { macLogger } from '../../shared/mac-logger'; // Types for Node.js http module (imported dynamically) diff --git a/src/provider/auth/pkce.ts b/src/domain/auth/pkce.ts similarity index 96% rename from src/provider/auth/pkce.ts rename to src/domain/auth/pkce.ts index 26daac0..efe48a7 100644 --- a/src/provider/auth/pkce.ts +++ b/src/domain/auth/pkce.ts @@ -1,4 +1,4 @@ -import type { PKCEChallenge } from './types'; +import type { PKCEChallenge } from '../../types/auth'; /** * Generate a cryptographically secure random string for PKCE code verifier diff --git a/src/provider/auth/token-manager.ts b/src/domain/auth/token-manager.ts similarity index 97% rename from src/provider/auth/token-manager.ts rename to src/domain/auth/token-manager.ts index 07bfa7d..5fec344 100644 --- a/src/provider/auth/token-manager.ts +++ b/src/domain/auth/token-manager.ts @@ -1,5 +1,5 @@ import { CODEX_OAUTH } from './oauth-constants'; -import type { OAuthTokens, TokenResponse } from './types'; +import type { OAuthTokens, TokenResponse } from '../../types/auth'; /** * Parse JWT claims without verification (for extracting account_id) diff --git a/src/constants.ts b/src/domain/constants.ts similarity index 96% rename from src/constants.ts rename to src/domain/constants.ts index 0b8f7b0..63f8e3a 100644 --- a/src/constants.ts +++ b/src/domain/constants.ts @@ -2,11 +2,11 @@ // Consolidated Constants - All constants in one place // ========================================== -import type { FrontmatterField, LinkType, ProviderConfig, AutoClassifierSettings } from './types'; -import type { NoticeCatalog } from './shared/plugin-notices'; +import type { FrontmatterField, LinkType, ProviderConfig, AutoClassifierSettings } from '../types'; +import type { NoticeCatalog } from '../shared/plugin-notices'; // ========================================== -// Common Constants (from api/constants.ts) +// Common Constants // ========================================== export const COMMON_CONSTANTS = { @@ -152,10 +152,10 @@ export const OLLAMA_STRUCTURE_OUTPUT = { }; // ========================================== -// Default Settings (from utils/constants.ts) +// Default Settings // ========================================== -// Prompt template - originally in api/prompt.ts but used by constants +// Prompt template export const DEFAULT_TASK_TEMPLATE = ` Classify the following content using the provided reference categories. diff --git a/src/provider/presets.json b/src/domain/presets.json similarity index 100% rename from src/provider/presets.json rename to src/domain/presets.json diff --git a/src/provider/prompt.ts b/src/domain/prompt.ts similarity index 95% rename from src/provider/prompt.ts rename to src/domain/prompt.ts index ab88e3c..b959ae1 100644 --- a/src/provider/prompt.ts +++ b/src/domain/prompt.ts @@ -1,5 +1,5 @@ -import { DEFAULT_TASK_TEMPLATE } from '../constants'; -import { sanitizePromptInput, sanitizeReferenceValues } from '../lib/sanitizer'; +import { DEFAULT_TASK_TEMPLATE } from './constants'; +import { sanitizePromptInput, sanitizeReferenceValues } from '../utils/sanitizer'; // Constants for system behaviour export const DEFAULT_SYSTEM_ROLE = `You are a JSON classification assistant. Respond only with a valid JSON object adhering to the specified schema.`; diff --git a/src/main.ts b/src/main.ts index fb5b6f1..4a67f6c 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,11 +1,11 @@ import type { TFile } from 'obsidian'; import { Plugin } from 'obsidian'; -import { CodexOAuth, isTokenExpired } from './provider/auth'; -import { ClassificationService, CommandService } from './classifier'; -import { DEFAULT_FRONTMATTER_SETTING, DEFAULT_SETTINGS, NOTICE_CATALOG } from './constants'; -import type { AutoClassifierSettings } from './settings'; -import { AutoClassifierSettingTab } from './settings'; -import type { FrontmatterField, ProviderConfig } from './types'; +import { CodexOAuth, isTokenExpired } from './ui/auth'; +import { ClassificationService } from './ui/ClassificationService'; +import { CommandService } from './ui/CommandService'; +import { DEFAULT_FRONTMATTER_SETTING, DEFAULT_SETTINGS, NOTICE_CATALOG } from './domain/constants'; +import type { AutoClassifierSettings, FrontmatterField, ProviderConfig } from './types'; +import { AutoClassifierSettingTab } from './ui/settings'; import { PluginLogger } from './shared/plugin-logger'; import { PluginNotices } from './shared/plugin-notices'; import { migrateSettings } from './shared/settings-migration'; diff --git a/src/provider/auth/index.ts b/src/provider/auth/index.ts deleted file mode 100644 index 141652b..0000000 --- a/src/provider/auth/index.ts +++ /dev/null @@ -1,11 +0,0 @@ -// Provider Auth module exports -export { CodexOAuth } from './oauth'; -export { CODEX_OAUTH } from './oauth-constants'; -export { - isTokenExpired, - formatTokenExpiry, - getTokenRemainingTime, - parseJwtClaims, - extractAccountId, -} from './token-manager'; -export type { OAuthTokens, PKCEChallenge, TokenResponse } from './types'; diff --git a/src/settings/components/BaseSettings.ts b/src/settings/components/BaseSettings.ts index b22acd5..cf7e9eb 100644 --- a/src/settings/components/BaseSettings.ts +++ b/src/settings/components/BaseSettings.ts @@ -1,64 +1,2 @@ -import type { FrontmatterField } from '../../types'; -import type AutoClassifierPlugin from '../../main'; -import { ConfigurableSettingModal } from '../modals/FrontmatterEditorModal'; -import type { - FrontmatterActions, - FrontmatterEditorModalProps, - SettingsComponent, - SettingsComponentOptions, -} from '../../types'; -import { Setting } from './Setting'; - -export abstract class BaseSettingsComponent implements SettingsComponent { - protected abstract readonly options: SettingsComponentOptions; - - constructor( - protected readonly plugin: AutoClassifierPlugin, - protected readonly containerEl: HTMLElement - ) {} - - abstract display(frontmatterId?: number): void; - - protected createFrontmatterSetting( - containerEl: HTMLElement, - frontmatterSetting: FrontmatterField, - actions: FrontmatterActions, - showDeleteButton: boolean = false - ): void { - const button = [ - { - icon: 'pencil', - tooltip: 'Edit Frontmatter', - onClick: () => actions.onEdit(frontmatterSetting), - }, - ]; - if (showDeleteButton) { - button.push({ - icon: 'trash', - tooltip: 'Delete Frontmatter', - onClick: () => actions.onDelete(frontmatterSetting), - }); - } - - Setting.create(containerEl, { - name: frontmatterSetting.name || 'Please enter name', - desc: `Type: ${frontmatterSetting.linkType}, Max: ${frontmatterSetting.count?.max ?? 5}, Overwrite: ${frontmatterSetting.overwrite}`, - buttons: button, - }); - } - - // Common modal creation and opening logic - protected openEditModal( - frontmatterSetting: FrontmatterField, - onSave: (updatedFrontmatter: FrontmatterField) => Promise - ): void { - const modalProps: FrontmatterEditorModalProps = { - frontmatterSetting: frontmatterSetting, - options: this.options, - onSave: onSave, - }; - - const modal = new ConfigurableSettingModal(this.plugin.app, modalProps); - modal.open(); - } -} +// Compatibility shim — new code should import from '../../ui/settings/components/BaseSettings' directly +export * from '../../ui/settings/components/BaseSettings'; diff --git a/src/types.ts b/src/types.ts index 5494021..b470257 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,161 +1,3 @@ -// ========================================== -// Consolidated Types - Centralized type definitions -// ========================================== - -import type { TFile } from 'obsidian'; -import type { OAuthTokens } from './provider/auth/types'; - -// ========================================== -// Frontmatter Types (from frontmatter/types.ts) -// ========================================== - -export type LinkType = 'WikiLink' | 'Normal'; - -export interface Range { - min: number; - max: number; -} - -export interface FrontmatterField { - id: number; - name: string; - count: Range; - refs: string[]; - overwrite: boolean; - linkType: LinkType; - customQuery: string; -} - -export interface FrontMatter { - [key: string]: string[]; -} - -export type ProcessFrontMatterFn = ( - file: TFile, - fn: (frontmatter: FrontMatter) => void -) => Promise; - -export interface InsertFrontMatterParams extends Pick< - FrontmatterField, - 'name' | 'overwrite' | 'linkType' -> { - file: TFile; - value: string[]; -} - -// ========================================== -// Provider Types (from api/types.ts) -// ========================================== - -export interface Model { - id: string; - name: string; -} - -export type AuthType = 'apiKey' | 'oauth'; - -/** - * Unified authentication type - supports both API Key and OAuth - */ -export type ProviderAuth = - | { type: 'apiKey'; apiKey: string } - | { type: 'oauth'; oauth: OAuthTokens }; - -/** - * Provider configuration interface - * NOTE: During migration, both old format (apiKey, oauth fields) and new format (auth field) are supported - */ -export interface ProviderBase { - name: string; - baseUrl: string; - temperature?: number; - models: Model[]; - // New unified auth field - auth?: ProviderAuth; - // Legacy fields (kept for backward compatibility during migration) - apiKey?: string; - authType?: AuthType; - oauth?: OAuthTokens; -} - -export type ProviderConfig = ProviderBase; - -export interface StructuredOutput { - output: string[]; - reliability: number; -} - -export interface ProviderPreset extends Omit { - presetId?: string; - apiKeyUrl: string; - apiKeyRequired: boolean; - modelsList: string; - popularModels: Model[]; -} - -/** - * Callback for when OAuth tokens are refreshed during API calls - */ -export type OnTokenRefreshCallback = (tokens: OAuthTokens) => Promise; - -export interface APIProvider { - callAPI( - systemRole: string, - user_prompt: string, - provider: ProviderConfig, - selectedModel: string, - temperature?: number, - onTokenRefresh?: OnTokenRefreshCallback - ): Promise; - - buildHeaders(apiKey: string): Record; - processApiResponse(responseData: any): StructuredOutput; -} - -// ========================================== -// Settings UI Types (from ui/types.ts) -// ========================================== - -export interface SettingsComponentOptions { - showLinkType?: boolean; - showOptions?: boolean; - showTextArea?: boolean; -} - -export interface SettingsComponent { - display(frontmatterId?: number): void; -} - -export interface FrontmatterActions { - onEdit: (frontmatterSetting: FrontmatterField) => void; - onDelete: (frontmatterSetting: FrontmatterField) => void; -} - -export interface FrontmatterEditorModalProps { - frontmatterSetting: FrontmatterField; - options: SettingsComponentOptions; - onSave: (frontmatter: FrontmatterField) => Promise; -} - -/** - * Frontmatter reference type - minimal identification info for selection/reference - * Used in modals, dropdowns, lists etc. to identify frontmatter - */ -export type FrontmatterRef = Pick; - -// ========================================== -// Plugin Settings Type -// ========================================== - -export interface AutoClassifierSettings { - providers: ProviderConfig[]; - selectedProvider: string; - selectedModel: string; - frontmatter: FrontmatterField[]; - classificationRule: string; - codexConnection?: OAuthTokens; - plugin_notices?: { muted: Record }; -} - -// Re-export auth types for convenience -export type { OAuthTokens } from './provider/auth/types'; +// Re-export all types from the new layered architecture types module +// This file is kept as a compatibility shim — new code should import from './types/index' directly +export * from './types/index'; diff --git a/src/types/.gitkeep b/src/types/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/src/provider/auth/types.ts b/src/types/auth.ts similarity index 100% rename from src/provider/auth/types.ts rename to src/types/auth.ts diff --git a/src/types/index.ts b/src/types/index.ts new file mode 100644 index 0000000..72b30c0 --- /dev/null +++ b/src/types/index.ts @@ -0,0 +1,163 @@ +// ========================================== +// Consolidated Types - Centralized type definitions +// ========================================== + +import type { OAuthTokens } from './auth'; + +// ========================================== +// Obsidian type shims — used to avoid importing from obsidian in domain/utils layers +// Keep in sync with the Obsidian API signatures that we depend on. +// ========================================== + +/** + * Minimal shim for Obsidian's TFile used in pure-type positions. + * Do NOT add methods — only structural properties needed by domain code. + */ +export interface TFileShim { + path: string; + name: string; + basename: string; + extension: string; + parent: { path: string } | null; +} + +// ========================================== +// Frontmatter Types +// ========================================== + +export type LinkType = 'WikiLink' | 'Normal'; + +export interface Range { + min: number; + max: number; +} + +export interface FrontmatterField { + id: number; + name: string; + count: Range; + refs: string[]; + overwrite: boolean; + linkType: LinkType; + customQuery: string; +} + +export interface FrontMatter { + [key: string]: string[]; +} + +// ========================================== +// Provider Types +// ========================================== + +export interface Model { + id: string; + name: string; +} + +export type AuthType = 'apiKey' | 'oauth'; + +/** + * Unified authentication type - supports both API Key and OAuth + */ +export type ProviderAuth = + | { type: 'apiKey'; apiKey: string } + | { type: 'oauth'; oauth: OAuthTokens }; + +/** + * Provider configuration interface + * NOTE: During migration, both old format (apiKey, oauth fields) and new format (auth field) are supported + */ +export interface ProviderBase { + name: string; + baseUrl: string; + temperature?: number; + models: Model[]; + // New unified auth field + auth?: ProviderAuth; + // Legacy fields (kept for backward compatibility during migration) + apiKey?: string; + authType?: AuthType; + oauth?: OAuthTokens; +} + +export type ProviderConfig = ProviderBase; + +export interface StructuredOutput { + output: string[]; + reliability: number; +} + +export interface ProviderPreset extends Omit { + presetId?: string; + apiKeyUrl: string; + apiKeyRequired: boolean; + modelsList: string; + popularModels: Model[]; +} + +/** + * Callback for when OAuth tokens are refreshed during API calls + */ +export type OnTokenRefreshCallback = (tokens: OAuthTokens) => Promise; + +export interface APIProvider { + callAPI( + systemRole: string, + user_prompt: string, + provider: ProviderConfig, + selectedModel: string, + temperature?: number, + onTokenRefresh?: OnTokenRefreshCallback + ): Promise; + + buildHeaders(apiKey: string): Record; + processApiResponse(responseData: any): StructuredOutput; +} + +// ========================================== +// Settings UI Types +// ========================================== + +export interface SettingsComponentOptions { + showLinkType?: boolean; + showOptions?: boolean; + showTextArea?: boolean; +} + +export interface SettingsComponent { + display(frontmatterId?: number): void; +} + +export interface FrontmatterActions { + onEdit: (frontmatterSetting: FrontmatterField) => void; + onDelete: (frontmatterSetting: FrontmatterField) => void; +} + +export interface FrontmatterEditorModalProps { + frontmatterSetting: FrontmatterField; + options: SettingsComponentOptions; + onSave: (frontmatter: FrontmatterField) => Promise; +} + +/** + * Frontmatter reference type - minimal identification info for selection/reference + */ +export type FrontmatterRef = Pick; + +// ========================================== +// Plugin Settings Type +// ========================================== + +export interface AutoClassifierSettings { + providers: ProviderConfig[]; + selectedProvider: string; + selectedModel: string; + frontmatter: FrontmatterField[]; + classificationRule: string; + codexConnection?: OAuthTokens; + plugin_notices?: { muted: Record }; +} + +// Re-export auth types for convenience +export type { OAuthTokens } from './auth'; diff --git a/src/ui/.gitkeep b/src/ui/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/src/classifier/ClassificationService.ts b/src/ui/ClassificationService.ts similarity index 94% rename from src/classifier/ClassificationService.ts rename to src/ui/ClassificationService.ts index a8be727..ab3ede8 100644 --- a/src/classifier/ClassificationService.ts +++ b/src/ui/ClassificationService.ts @@ -1,14 +1,10 @@ import type { TFile, App } from 'obsidian'; -import { COMMON_CONSTANTS } from '../constants'; -import { - getContentWithoutFrontmatter, - getFieldValues, - insertToFrontMatter, -} from '../lib/frontmatter'; -import { processAPIRequest } from '../provider'; -import { DEFAULT_SYSTEM_ROLE, getPromptTemplate } from '../provider/prompt'; -import { Notice } from '../settings/components/Notice'; +import { COMMON_CONSTANTS } from '../domain/constants'; +import { getContentWithoutFrontmatter, getFieldValues, insertToFrontMatter } from './frontmatter'; +import { processAPIRequest } from './provider-api'; +import { DEFAULT_SYSTEM_ROLE, getPromptTemplate } from '../domain/prompt'; +import { Notice } from './settings/components/Notice'; import type { PluginNotices } from '../shared/plugin-notices'; import type { FrontmatterField, FrontMatter, ProviderConfig, StructuredOutput } from '../types'; diff --git a/src/classifier/CommandService.ts b/src/ui/CommandService.ts similarity index 100% rename from src/classifier/CommandService.ts rename to src/ui/CommandService.ts diff --git a/src/provider/UnifiedProvider.ts b/src/ui/UnifiedProvider.ts similarity index 98% rename from src/provider/UnifiedProvider.ts rename to src/ui/UnifiedProvider.ts index 76cd374..a34400e 100644 --- a/src/provider/UnifiedProvider.ts +++ b/src/ui/UnifiedProvider.ts @@ -4,8 +4,8 @@ import { GEMINI_STRUCTURE_OUTPUT, OLLAMA_STRUCTURE_OUTPUT, OPENAI_STRUCTURE_OUTPUT, -} from '../constants'; -import { PROVIDER_NAMES } from '../lib'; +} from '../domain/constants'; +import { PROVIDER_NAMES } from '../utils/lib-utils'; import type { APIProvider, ProviderConfig, @@ -13,7 +13,7 @@ import type { OnTokenRefreshCallback, } from '../types'; import { sendRequest, sendStreamingRequest, HttpError, type SSEEvent } from './request'; -import { CODEX_OAUTH } from './auth/oauth-constants'; +import { CODEX_OAUTH } from '../domain/auth/oauth-constants'; import { CodexOAuth } from './auth/oauth'; import { macLogger } from '../shared/mac-logger'; diff --git a/src/ui/auth/index.ts b/src/ui/auth/index.ts new file mode 100644 index 0000000..8d15780 --- /dev/null +++ b/src/ui/auth/index.ts @@ -0,0 +1,12 @@ +// UI auth module exports — obsidian-dependent auth implementations +export { CodexOAuth } from './oauth'; +// Re-export pure domain auth utilities for convenience +export { CODEX_OAUTH } from '../../domain/auth/oauth-constants'; +export { + isTokenExpired, + formatTokenExpiry, + getTokenRemainingTime, + parseJwtClaims, + extractAccountId, +} from '../../domain/auth/token-manager'; +export type { OAuthTokens, PKCEChallenge, TokenResponse } from '../../types/auth'; diff --git a/src/provider/auth/oauth.ts b/src/ui/auth/oauth.ts similarity index 92% rename from src/provider/auth/oauth.ts rename to src/ui/auth/oauth.ts index 30f7e38..909bde8 100644 --- a/src/provider/auth/oauth.ts +++ b/src/ui/auth/oauth.ts @@ -1,9 +1,9 @@ import { Platform, requestUrl } from 'obsidian'; -import { CODEX_OAUTH } from './oauth-constants'; -import { OAuthCallbackServer } from './oauth-server'; -import { generatePKCEChallenge, generateState } from './pkce'; -import { createTokensFromResponse, isTokenExpired } from './token-manager'; -import type { OAuthTokens, TokenResponse } from './types'; +import { CODEX_OAUTH } from '../../domain/auth/oauth-constants'; +import { OAuthCallbackServer } from '../../domain/auth/oauth-server'; +import { generatePKCEChallenge, generateState } from '../../domain/auth/pkce'; +import { createTokensFromResponse, isTokenExpired } from '../../domain/auth/token-manager'; +import type { OAuthTokens, TokenResponse } from '../../types/auth'; /** * OAuth handler for OpenAI Codex/ChatGPT Pro authentication diff --git a/src/lib/frontmatter.ts b/src/ui/frontmatter.ts similarity index 89% rename from src/lib/frontmatter.ts rename to src/ui/frontmatter.ts index 513bca4..452bb16 100644 --- a/src/lib/frontmatter.ts +++ b/src/ui/frontmatter.ts @@ -1,12 +1,17 @@ import type { MetadataCache, TFile } from 'obsidian'; import { getAllTags, getFrontMatterInfo, parseFrontMatterStringArray } from 'obsidian'; -import type { - FrontMatter, +import type { FrontMatter, FrontmatterField } from '../types'; + +type ProcessFrontMatterFn = (file: TFile, fn: (frontmatter: FrontMatter) => void) => Promise; + +interface InsertFrontMatterParams extends Pick< FrontmatterField, - InsertFrontMatterParams, - ProcessFrontMatterFn, -} from '../types'; + 'name' | 'overwrite' | 'linkType' +> { + file: TFile; + value: string[]; +} /** * Extracts the content of a markdown file excluding frontmatter @@ -53,10 +58,7 @@ export function getFieldValues( /** * Gets a frontmatter setting by ID from the settings array. */ -export function getFrontmatterSetting( - id: number, - settings: FrontmatterField[] -): FrontmatterField { +export function getFrontmatterSetting(id: number, settings: FrontmatterField[]): FrontmatterField { const setting = settings?.find((f) => f.id === id); if (!setting) { throw new Error('Setting not found'); diff --git a/src/classifier/index.ts b/src/ui/index.ts similarity index 100% rename from src/classifier/index.ts rename to src/ui/index.ts diff --git a/src/provider/index.ts b/src/ui/provider-api.ts similarity index 95% rename from src/provider/index.ts rename to src/ui/provider-api.ts index cf46811..36d8d26 100644 --- a/src/provider/index.ts +++ b/src/ui/provider-api.ts @@ -1,4 +1,4 @@ -import { COMMON_CONSTANTS } from '../constants'; +import { COMMON_CONSTANTS } from '../domain/constants'; import type { APIProvider, ProviderConfig, StructuredOutput } from '../types'; import { UnifiedProvider } from './UnifiedProvider'; diff --git a/src/provider/request.ts b/src/ui/request.ts similarity index 99% rename from src/provider/request.ts rename to src/ui/request.ts index 2d97a1c..69c6fcf 100644 --- a/src/provider/request.ts +++ b/src/ui/request.ts @@ -130,8 +130,6 @@ async function sendStreamingRequestViaNode( body: Record, timeoutMs: number = STREAM_TIMEOUT_MS ): Promise<{ status: number; text: string }> { - // https is imported at module level for proper testability - const parsedUrl = new URL(url); const payload = JSON.stringify(body); const payloadLength = Buffer.byteLength(payload); diff --git a/src/settings/ApiSection.ts b/src/ui/settings/ApiSection.ts similarity index 96% rename from src/settings/ApiSection.ts rename to src/ui/settings/ApiSection.ts index 16da814..860653b 100644 --- a/src/settings/ApiSection.ts +++ b/src/ui/settings/ApiSection.ts @@ -1,6 +1,6 @@ import { App } from 'obsidian'; -import type AutoClassifierPlugin from '../main'; +import type AutoClassifierPlugin from '../../main'; import { ClassificationRuleSection } from './ClassificationRuleSection'; import { ModelSection } from './ModelSection'; import { ProviderSection } from './ProviderSection'; diff --git a/src/settings/ClassificationRuleSection.ts b/src/ui/settings/ClassificationRuleSection.ts similarity index 92% rename from src/settings/ClassificationRuleSection.ts rename to src/ui/settings/ClassificationRuleSection.ts index fe4ec57..5fb957c 100644 --- a/src/settings/ClassificationRuleSection.ts +++ b/src/ui/settings/ClassificationRuleSection.ts @@ -1,7 +1,7 @@ import { TextAreaComponent } from 'obsidian'; -import { DEFAULT_TASK_TEMPLATE } from '../constants'; -import type AutoClassifierPlugin from '../main'; +import { DEFAULT_TASK_TEMPLATE } from '../../domain/constants'; +import type AutoClassifierPlugin from '../../main'; import { Setting } from './components/Setting'; export class ClassificationRuleSection { diff --git a/src/settings/FrontmatterSection.ts b/src/ui/settings/FrontmatterSection.ts similarity index 58% rename from src/settings/FrontmatterSection.ts rename to src/ui/settings/FrontmatterSection.ts index 432ba18..bb9e461 100644 --- a/src/settings/FrontmatterSection.ts +++ b/src/ui/settings/FrontmatterSection.ts @@ -1,4 +1,4 @@ -import type { FrontmatterActions, FrontmatterField, SettingsComponentOptions } from '../types'; +import type { FrontmatterActions, FrontmatterField, SettingsComponentOptions } from '../../types'; import { BaseSettingsComponent } from './components/BaseSettings'; export class Frontmatter extends BaseSettingsComponent { @@ -13,17 +13,17 @@ export class Frontmatter extends BaseSettingsComponent { const filteredFrontmatter = frontmatter.filter((frontmatter) => frontmatter.id !== 0); this.containerEl.empty(); - filteredFrontmatter.forEach((frontmatter: FrontmatterField) => { + filteredFrontmatter.forEach((frontmatter: FrontmatterField) => { const actions: FrontmatterActions = { - onEdit: (setting: FrontmatterField) => this.handleEdit(setting), - onDelete: (setting: FrontmatterField) => this.handleDelete(setting), + onEdit: (setting: FrontmatterField) => this.handleEdit(setting), + onDelete: (setting: FrontmatterField) => this.handleDelete(setting), }; this.createFrontmatterSetting(this.containerEl, frontmatter, actions, true); }); } - private handleEdit(frontmatterSetting: FrontmatterField): void { + private handleEdit(frontmatterSetting: FrontmatterField): void { this.openEditModal(frontmatterSetting, async (updatedFrontmatter) => { // Register command for the updated frontmatter this.plugin.registerCommand( @@ -36,15 +36,17 @@ export class Frontmatter extends BaseSettingsComponent { }); } - private async handleDelete(frontmatterSetting: FrontmatterField): Promise { - const confirmed = confirm(`Are you sure you want to delete "${frontmatterSetting.name}" frontmatter?`); + private async handleDelete(frontmatterSetting: FrontmatterField): Promise { + const confirmed = confirm( + `Are you sure you want to delete "${frontmatterSetting.name}" frontmatter?` + ); if (!confirmed) { return; } - this.plugin.settings.frontmatter = this.plugin.settings.frontmatter.filter( - (f: FrontmatterField) => f.id !== frontmatterSetting.id - ); + this.plugin.settings.frontmatter = this.plugin.settings.frontmatter.filter( + (f: FrontmatterField) => f.id !== frontmatterSetting.id + ); await this.plugin.saveSettings(); this.display(); diff --git a/src/settings/ModelSection.ts b/src/ui/settings/ModelSection.ts similarity index 96% rename from src/settings/ModelSection.ts rename to src/ui/settings/ModelSection.ts index 4c82b9b..12a08a7 100644 --- a/src/settings/ModelSection.ts +++ b/src/ui/settings/ModelSection.ts @@ -1,8 +1,8 @@ import type { App } from 'obsidian'; -import type AutoClassifierPlugin from '../main'; -import { testModel } from '../provider'; -import type { Model, ProviderConfig } from '../types'; +import type AutoClassifierPlugin from '../../main'; +import { testModel } from '../provider-api'; +import type { Model, ProviderConfig } from '../../types'; import { Setting } from './components/Setting'; import { ModelModal, type ModelModalProps } from './modals/ModelModal'; diff --git a/src/settings/ProviderSection.ts b/src/ui/settings/ProviderSection.ts similarity index 94% rename from src/settings/ProviderSection.ts rename to src/ui/settings/ProviderSection.ts index 3b9f4ec..c860d77 100644 --- a/src/settings/ProviderSection.ts +++ b/src/ui/settings/ProviderSection.ts @@ -1,8 +1,8 @@ import type { App } from 'obsidian'; -import type AutoClassifierPlugin from '../main'; -import { formatTokenExpiry, isTokenExpired } from '../provider/auth'; -import type { ProviderConfig } from '../types'; +import type AutoClassifierPlugin from '../../main'; +import { formatTokenExpiry, isTokenExpired } from '../auth'; +import type { ProviderConfig } from '../../types'; import { Setting } from './components/Setting'; import { ProviderModal } from './modals/ProviderModal'; diff --git a/src/settings/TagSection.ts b/src/ui/settings/TagSection.ts similarity index 79% rename from src/settings/TagSection.ts rename to src/ui/settings/TagSection.ts index ae489c3..f7af6d5 100644 --- a/src/settings/TagSection.ts +++ b/src/ui/settings/TagSection.ts @@ -1,4 +1,4 @@ -import type { FrontmatterActions, FrontmatterField, SettingsComponentOptions } from '../types'; +import type { FrontmatterActions, FrontmatterField, SettingsComponentOptions } from '../../types'; import { BaseSettingsComponent } from './components/BaseSettings'; export class Tag extends BaseSettingsComponent { @@ -16,15 +16,15 @@ export class Tag extends BaseSettingsComponent { return; } - const actions: FrontmatterActions = { - onEdit: (setting: FrontmatterField) => this.handleEdit(setting), + const actions: FrontmatterActions = { + onEdit: (setting: FrontmatterField) => this.handleEdit(setting), onDelete: () => {}, }; this.createFrontmatterSetting(this.containerEl, tagSetting, actions, false); } - private handleEdit(frontmatterSetting: FrontmatterField): void { + private handleEdit(frontmatterSetting: FrontmatterField): void { this.openEditModal(frontmatterSetting, async (updatedFrontmatter) => { const tagSettingIndex = this.plugin.settings.frontmatter.findIndex((f) => f.id === 0); if (tagSettingIndex !== -1) { diff --git a/src/ui/settings/components/BaseSettings.ts b/src/ui/settings/components/BaseSettings.ts new file mode 100644 index 0000000..ed6d23b --- /dev/null +++ b/src/ui/settings/components/BaseSettings.ts @@ -0,0 +1,64 @@ +import type { FrontmatterField } from '../../../types'; +import type AutoClassifierPlugin from '../../../main'; +import { ConfigurableSettingModal } from '../modals/FrontmatterEditorModal'; +import type { + FrontmatterActions, + FrontmatterEditorModalProps, + SettingsComponent, + SettingsComponentOptions, +} from '../../../types'; +import { Setting } from './Setting'; + +export abstract class BaseSettingsComponent implements SettingsComponent { + protected abstract readonly options: SettingsComponentOptions; + + constructor( + protected readonly plugin: AutoClassifierPlugin, + protected readonly containerEl: HTMLElement + ) {} + + abstract display(frontmatterId?: number): void; + + protected createFrontmatterSetting( + containerEl: HTMLElement, + frontmatterSetting: FrontmatterField, + actions: FrontmatterActions, + showDeleteButton: boolean = false + ): void { + const button = [ + { + icon: 'pencil', + tooltip: 'Edit Frontmatter', + onClick: () => actions.onEdit(frontmatterSetting), + }, + ]; + if (showDeleteButton) { + button.push({ + icon: 'trash', + tooltip: 'Delete Frontmatter', + onClick: () => actions.onDelete(frontmatterSetting), + }); + } + + Setting.create(containerEl, { + name: frontmatterSetting.name || 'Please enter name', + desc: `Type: ${frontmatterSetting.linkType}, Max: ${frontmatterSetting.count?.max ?? 5}, Overwrite: ${frontmatterSetting.overwrite}`, + buttons: button, + }); + } + + // Common modal creation and opening logic + protected openEditModal( + frontmatterSetting: FrontmatterField, + onSave: (updatedFrontmatter: FrontmatterField) => Promise + ): void { + const modalProps: FrontmatterEditorModalProps = { + frontmatterSetting: frontmatterSetting, + options: this.options, + onSave: onSave, + }; + + const modal = new ConfigurableSettingModal(this.plugin.app, modalProps); + modal.open(); + } +} diff --git a/src/settings/components/ModalAccessibilityHelper.ts b/src/ui/settings/components/ModalAccessibilityHelper.ts similarity index 94% rename from src/settings/components/ModalAccessibilityHelper.ts rename to src/ui/settings/components/ModalAccessibilityHelper.ts index 7d1a144..d26712f 100644 --- a/src/settings/components/ModalAccessibilityHelper.ts +++ b/src/ui/settings/components/ModalAccessibilityHelper.ts @@ -38,9 +38,7 @@ export class ModalAccessibilityHelper { */ focusFirstInput(contentEl: HTMLElement): void { setTimeout(() => { - const firstInput = contentEl.querySelector( - 'input, select, button, textarea' - ) as HTMLElement; + const firstInput = contentEl.querySelector('input, select, button, textarea') as HTMLElement; if (firstInput) { firstInput.focus(); } diff --git a/src/settings/components/Notice.ts b/src/ui/settings/components/Notice.ts similarity index 100% rename from src/settings/components/Notice.ts rename to src/ui/settings/components/Notice.ts diff --git a/src/settings/components/Setting.ts b/src/ui/settings/components/Setting.ts similarity index 98% rename from src/settings/components/Setting.ts rename to src/ui/settings/components/Setting.ts index ad6ebb7..657bfe1 100644 --- a/src/settings/components/Setting.ts +++ b/src/ui/settings/components/Setting.ts @@ -1,4 +1,9 @@ -import { ExtraButtonComponent, Setting as ObsidianSetting, TextAreaComponent, TextComponent } from 'obsidian'; +import { + ExtraButtonComponent, + Setting as ObsidianSetting, + TextAreaComponent, + TextComponent, +} from 'obsidian'; export interface ButtonProps { text?: string; diff --git a/src/settings/components/WikiLinkSelector.ts b/src/ui/settings/components/WikiLinkSelector.ts similarity index 100% rename from src/settings/components/WikiLinkSelector.ts rename to src/ui/settings/components/WikiLinkSelector.ts diff --git a/src/settings/index.ts b/src/ui/settings/index.ts similarity index 90% rename from src/settings/index.ts rename to src/ui/settings/index.ts index e09f96b..7412786 100644 --- a/src/settings/index.ts +++ b/src/ui/settings/index.ts @@ -1,9 +1,9 @@ -import type AutoClassifierPlugin from 'main'; +import type AutoClassifierPlugin from '../../main'; import { PluginSettingTab } from 'obsidian'; -import type { FrontmatterField } from '../types'; -import { generateId } from '../lib'; -import { DEFAULT_FRONTMATTER_SETTING } from '../constants'; +import type { FrontmatterField } from '../../types'; +import { generateId } from '../../utils/lib-utils'; +import { DEFAULT_FRONTMATTER_SETTING } from '../../domain/constants'; import { Setting } from './components/Setting'; import { Api } from './ApiSection'; import { Frontmatter } from './FrontmatterSection'; @@ -84,7 +84,7 @@ export class AutoClassifierSettingTab extends PluginSettingTab { } } -export type { AutoClassifierSettings } from '../types'; +export type { AutoClassifierSettings } from '../../types'; export * from './components/WikiLinkSelector'; export * from './modals/FrontmatterEditorModal'; export * from './modals/FrontmatterSelectModal'; diff --git a/src/settings/modals/FrontmatterEditorModal.ts b/src/ui/settings/modals/FrontmatterEditorModal.ts similarity index 96% rename from src/settings/modals/FrontmatterEditorModal.ts rename to src/ui/settings/modals/FrontmatterEditorModal.ts index 78696dc..bfe1512 100644 --- a/src/settings/modals/FrontmatterEditorModal.ts +++ b/src/ui/settings/modals/FrontmatterEditorModal.ts @@ -1,8 +1,8 @@ import type { App, TextAreaComponent } from 'obsidian'; import { Modal, Setting, TextAreaComponent as ObsidianTextArea } from 'obsidian'; -import { deepCloneFrontmatterField } from '../../lib/frontmatter'; -import type { FrontmatterEditorModalProps, FrontmatterField, LinkType } from '../../types'; +import { deepCloneFrontmatterField } from '../../frontmatter'; +import type { FrontmatterEditorModalProps, FrontmatterField, LinkType } from '../../../types'; import { Notice } from '../components/Notice'; import { Setting as CommonSetting } from '../components/Setting'; import { WikiLinkSelector } from '../components/WikiLinkSelector'; @@ -119,9 +119,7 @@ export class ConfigurableSettingModal extends Modal { const wikiLinkSelector = new WikiLinkSelector(this.app); wikiLinkSelector.openFileSelector((selectedLink) => { const formattedLink = - this.localState.linkType === 'WikiLink' - ? `[[${selectedLink}]]` - : selectedLink; + this.localState.linkType === 'WikiLink' ? `[[${selectedLink}]]` : selectedLink; const currentOptions = this.localState.refs || []; this.localState.refs = [...currentOptions, formattedLink]; this.updateOptionsTextarea(); diff --git a/src/settings/modals/FrontmatterSelectModal.ts b/src/ui/settings/modals/FrontmatterSelectModal.ts similarity index 92% rename from src/settings/modals/FrontmatterSelectModal.ts rename to src/ui/settings/modals/FrontmatterSelectModal.ts index 8239a09..e5b15a4 100644 --- a/src/settings/modals/FrontmatterSelectModal.ts +++ b/src/ui/settings/modals/FrontmatterSelectModal.ts @@ -1,7 +1,7 @@ import type { App } from 'obsidian'; import { FuzzySuggestModal } from 'obsidian'; -import type { FrontmatterRef } from '../../types'; +import type { FrontmatterRef } from '../../../types'; export class FrontmatterSelectModal extends FuzzySuggestModal { constructor( diff --git a/src/settings/modals/ModelModal.ts b/src/ui/settings/modals/ModelModal.ts similarity index 98% rename from src/settings/modals/ModelModal.ts rename to src/ui/settings/modals/ModelModal.ts index fdd0b66..b28860e 100644 --- a/src/settings/modals/ModelModal.ts +++ b/src/ui/settings/modals/ModelModal.ts @@ -1,11 +1,11 @@ import type { App } from 'obsidian'; import { ButtonComponent, Modal } from 'obsidian'; -import { getProviderPresets } from '../../lib'; -import type { Model, ProviderConfig, ProviderPreset } from '../../types'; +import { getProviderPresets } from '../../../utils/lib-utils'; +import type { Model, ProviderConfig, ProviderPreset } from '../../../types'; import { ModalAccessibilityHelper } from '../components/ModalAccessibilityHelper'; import { Notice } from '../components/Notice'; -import type { PluginNotices } from '../../shared/plugin-notices'; +import type { PluginNotices } from '../../../shared/plugin-notices'; import type { DropdownOption } from '../components/Setting'; import { Setting as CommonSetting } from '../components/Setting'; diff --git a/src/settings/modals/ProviderModal.ts b/src/ui/settings/modals/ProviderModal.ts similarity index 96% rename from src/settings/modals/ProviderModal.ts rename to src/ui/settings/modals/ProviderModal.ts index d452c7d..e7953d1 100644 --- a/src/settings/modals/ProviderModal.ts +++ b/src/ui/settings/modals/ProviderModal.ts @@ -1,12 +1,12 @@ import type { App } from 'obsidian'; import { ButtonComponent, Modal, Platform } from 'obsidian'; -import { getProviderPreset, getProviderPresets } from '../../lib'; -import { CodexOAuth, formatTokenExpiry, isTokenExpired, CODEX_OAUTH } from '../../provider/auth'; -import type { OAuthTokens, ProviderConfig } from '../../types'; +import { getProviderPreset, getProviderPresets } from '../../../utils/lib-utils'; +import { CodexOAuth, formatTokenExpiry, isTokenExpired, CODEX_OAUTH } from '../../auth'; +import type { OAuthTokens, ProviderConfig } from '../../../types'; import { ModalAccessibilityHelper } from '../components/ModalAccessibilityHelper'; import { Notice } from '../components/Notice'; -import type { PluginNotices } from '../../shared/plugin-notices'; +import type { PluginNotices } from '../../../shared/plugin-notices'; import { Setting as CommonSetting, type DropdownOption } from '../components/Setting'; export class ProviderModal extends Modal { @@ -207,8 +207,7 @@ export class ProviderModal extends Modal { infoEl.style.marginBottom = '16px'; infoEl.style.paddingLeft = '16px'; infoEl.style.color = 'var(--text-muted)'; - infoEl.style.fontSize = '0.85em'; - infoEl.innerHTML = 'Requires ChatGPT Pro subscription'; + infoEl.textContent = 'Requires ChatGPT Pro subscription'; } } diff --git a/src/settings/modals/WikiLinkSuggestModal.ts b/src/ui/settings/modals/WikiLinkSuggestModal.ts similarity index 100% rename from src/settings/modals/WikiLinkSuggestModal.ts rename to src/ui/settings/modals/WikiLinkSuggestModal.ts diff --git a/src/utils/.gitkeep b/src/utils/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/src/lib/ErrorHandler.ts b/src/utils/ErrorHandler.ts similarity index 100% rename from src/lib/ErrorHandler.ts rename to src/utils/ErrorHandler.ts diff --git a/src/lib/index.ts b/src/utils/lib-utils.ts similarity index 96% rename from src/lib/index.ts rename to src/utils/lib-utils.ts index 6753cb1..ea251e1 100644 --- a/src/lib/index.ts +++ b/src/utils/lib-utils.ts @@ -1,5 +1,5 @@ import type { ProviderPreset } from '../types'; -import providerPresetsData from '../provider/presets.json'; +import providerPresetsData from '../domain/presets.json'; export const PROVIDER_NAMES = { OPENAI: providerPresetsData.openai.name, diff --git a/src/lib/sanitizer.ts b/src/utils/sanitizer.ts similarity index 100% rename from src/lib/sanitizer.ts rename to src/utils/sanitizer.ts diff --git a/tsconfig.json b/tsconfig.json index dfc51f9..c469b0d 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -4,14 +4,15 @@ "outDir": "dist", "paths": { "main": ["src/main"], - "provider": ["src/provider/index"], - "provider/*": ["src/provider/*"], - "lib": ["src/lib/index"], - "lib/*": ["src/lib/*"], - "settings": ["src/settings/index"], - "settings/*": ["src/settings/*"], - "classifier": ["src/classifier/index"], - "classifier/*": ["src/classifier/*"] + "domain": ["src/domain"], + "domain/*": ["src/domain/*"], + "ui": ["src/ui"], + "ui/*": ["src/ui/*"], + "types": ["src/types"], + "types/*": ["src/types/*"], + "utils": ["src/utils"], + "utils/*": ["src/utils/*"], + "shared/*": ["src/shared/*"] }, "inlineSourceMap": true, "inlineSources": true, diff --git a/vitest.config.ts b/vitest.config.ts index d2d2cef..53018ce 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -17,13 +17,40 @@ export default defineConfig({ }, }, resolve: { - alias: { - obsidian: path.resolve(__dirname, '__mocks__/obsidian.ts'), - main: path.resolve(__dirname, 'src/main'), - provider: path.resolve(__dirname, 'src/provider'), - lib: path.resolve(__dirname, 'src/lib'), - settings: path.resolve(__dirname, 'src/settings'), - classifier: path.resolve(__dirname, 'src/classifier'), - }, + alias: [ + // Obsidian mock + { find: 'obsidian', replacement: path.resolve(__dirname, '__mocks__/obsidian.ts') }, + // Main entry + { find: 'main', replacement: path.resolve(__dirname, 'src/main') }, + // New layered architecture aliases + { find: 'domain', replacement: path.resolve(__dirname, 'src/domain') }, + { find: 'ui', replacement: path.resolve(__dirname, 'src/ui') }, + { find: 'types', replacement: path.resolve(__dirname, 'src/types') }, + { find: 'utils', replacement: path.resolve(__dirname, 'src/utils') }, + // Legacy aliases — map old paths to new locations for test backward compat + // More specific paths must come before less specific ones + { find: 'provider/UnifiedProvider', replacement: path.resolve(__dirname, 'src/ui/UnifiedProvider') }, + { find: 'provider/request', replacement: path.resolve(__dirname, 'src/ui/request') }, + { find: 'provider/prompt', replacement: path.resolve(__dirname, 'src/domain/prompt') }, + { find: 'provider/auth/oauth', replacement: path.resolve(__dirname, 'src/ui/auth/oauth') }, + { find: 'provider/auth', replacement: path.resolve(__dirname, 'src/ui/auth/index') }, + { find: 'provider', replacement: path.resolve(__dirname, 'src/ui/provider-api') }, + { find: 'lib/frontmatter', replacement: path.resolve(__dirname, 'src/ui/frontmatter') }, + { find: 'lib/sanitizer', replacement: path.resolve(__dirname, 'src/utils/sanitizer') }, + { find: 'lib/ErrorHandler', replacement: path.resolve(__dirname, 'src/utils/ErrorHandler') }, + { find: 'lib', replacement: path.resolve(__dirname, 'src/utils/lib-utils') }, + { find: 'settings/components/Notice', replacement: path.resolve(__dirname, 'src/ui/settings/components/Notice') }, + { find: 'settings/modals/ProviderModal', replacement: path.resolve(__dirname, 'src/ui/settings/modals/ProviderModal') }, + { find: 'settings/modals/ModelModal', replacement: path.resolve(__dirname, 'src/ui/settings/modals/ModelModal') }, + { find: 'settings/ApiSection', replacement: path.resolve(__dirname, 'src/ui/settings/ApiSection') }, + { find: 'settings/TagSection', replacement: path.resolve(__dirname, 'src/ui/settings/TagSection') }, + { find: 'settings/FrontmatterSection', replacement: path.resolve(__dirname, 'src/ui/settings/FrontmatterSection') }, + { find: 'settings/ProviderSection', replacement: path.resolve(__dirname, 'src/ui/settings/ProviderSection') }, + { find: 'settings/ModelSection', replacement: path.resolve(__dirname, 'src/ui/settings/ModelSection') }, + { find: 'settings', replacement: path.resolve(__dirname, 'src/ui/settings/index') }, + { find: 'classifier/ClassificationService', replacement: path.resolve(__dirname, 'src/ui/ClassificationService') }, + { find: 'classifier/CommandService', replacement: path.resolve(__dirname, 'src/ui/CommandService') }, + { find: 'classifier', replacement: path.resolve(__dirname, 'src/ui/ClassificationService') }, + ], }, })