chore: implement requested changes

This commit is contained in:
Ralph Khreish
2025-09-03 13:54:18 +02:00
parent 8b164e5436
commit 04cefd84b4
21 changed files with 446 additions and 190 deletions

View File

@@ -29,6 +29,7 @@
"cli-table3": "^0.6.5",
"commander": "^12.1.0",
"inquirer": "^9.2.10",
"open": "^10.2.0",
"ora": "^8.1.0"
},
"devDependencies": {

View File

@@ -7,11 +7,12 @@ import { Command } from 'commander';
import chalk from 'chalk';
import inquirer from 'inquirer';
import ora, { type Ora } from 'ora';
import open from 'open';
import {
AuthManager,
AuthenticationError,
type AuthCredentials
} from '@tm/core';
} from '@tm/core/auth';
import * as ui from '../utils/ui.js';
/**
@@ -125,7 +126,7 @@ export class AuthCommand extends Command {
*/
private async executeLogout(): Promise<void> {
try {
const result = this.performLogout();
const result = await this.performLogout();
this.setLastResult(result);
if (!result.success) {
@@ -232,9 +233,9 @@ export class AuthCommand extends Command {
/**
* Perform logout
*/
private performLogout(): AuthResult {
private async performLogout(): Promise<AuthResult> {
try {
this.authManager.logout();
await this.authManager.logout();
ui.displaySuccess('Successfully logged out');
return {
@@ -366,7 +367,10 @@ export class AuthCommand extends Command {
try {
// Use AuthManager's new unified OAuth flow method with callbacks
const credentials = await this.authManager.authenticateWithOAuth({
openBrowser: true,
// Callback to handle browser opening
openBrowser: async (authUrl) => {
await open(authUrl);
},
timeout: 5 * 60 * 1000, // 5 minutes
// Callback when auth URL is ready

2
package-lock.json generated
View File

@@ -101,6 +101,7 @@
"cli-table3": "^0.6.5",
"commander": "^12.1.0",
"inquirer": "^9.2.10",
"open": "^10.2.0",
"ora": "^8.1.0"
},
"devDependencies": {
@@ -26298,7 +26299,6 @@
"dependencies": {
"@supabase/supabase-js": "^2.57.0",
"chalk": "^5.3.0",
"open": "^10.2.0",
"zod": "^3.22.4"
},
"devDependencies": {

View File

@@ -10,7 +10,58 @@
"types": "./src/index.ts",
"import": "./dist/index.js",
"require": "./dist/index.js"
}
},
"./auth": {
"types": "./src/auth/index.ts",
"import": "./dist/auth/index.js",
"require": "./dist/auth/index.js"
},
"./storage": {
"types": "./src/storage/index.ts",
"import": "./dist/storage/index.js",
"require": "./dist/storage/index.js"
},
"./config": {
"types": "./src/config/index.ts",
"import": "./dist/config/index.js",
"require": "./dist/config/index.js"
},
"./providers": {
"types": "./src/providers/index.ts",
"import": "./dist/providers/index.js",
"require": "./dist/providers/index.js"
},
"./services": {
"types": "./src/services/index.ts",
"import": "./dist/services/index.js",
"require": "./dist/services/index.js"
},
"./errors": {
"types": "./src/errors/index.ts",
"import": "./dist/errors/index.js",
"require": "./dist/errors/index.js"
},
"./logger": {
"types": "./src/logger/index.ts",
"import": "./dist/logger/index.js",
"require": "./dist/logger/index.js"
},
"./types": {
"types": "./src/types/index.ts",
"import": "./dist/types/index.js",
"require": "./dist/types/index.js"
},
"./interfaces": {
"types": "./src/interfaces/index.ts",
"import": "./dist/interfaces/index.js",
"require": "./dist/interfaces/index.js"
},
"./utils": {
"types": "./src/utils/index.ts",
"import": "./dist/utils/index.js",
"require": "./dist/utils/index.js"
},
"./package.json": "./package.json"
},
"scripts": {
"build": "tsup",
@@ -28,7 +79,6 @@
"dependencies": {
"@supabase/supabase-js": "^2.57.0",
"chalk": "^5.3.0",
"open": "^10.2.0",
"zod": "^3.22.4"
},
"devDependencies": {

View File

@@ -65,10 +65,13 @@ export class AuthManager {
* Note: This would require a custom implementation or Supabase RLS policies
*/
async authenticateWithApiKey(apiKey: string): Promise<AuthCredentials> {
// TODO: Implement API key validation if needed
// For now, we can store the API key directly as a token
const token = apiKey.trim();
if (!token || token.length < 10) {
throw new AuthenticationError('Invalid API key', 'INVALID_API_KEY');
}
const authData: AuthCredentials = {
token: apiKey,
token,
tokenType: 'api_key',
userId: 'api-user',
email: undefined,
@@ -84,7 +87,9 @@ export class AuthManager {
* Refresh authentication token
*/
async refreshToken(): Promise<AuthCredentials> {
const authData = this.getCredentials();
const authData = this.credentialStore.getCredentials({
allowExpired: true
});
if (!authData || !authData.refreshToken) {
throw new AuthenticationError(
@@ -118,7 +123,16 @@ export class AuthManager {
/**
* Logout and clear credentials
*/
logout(): void {
async logout(): Promise<void> {
try {
// First try to sign out from Supabase to revoke tokens
await this.supabaseClient.signOut();
} catch (error) {
// Log but don't throw - we still want to clear local credentials
console.warn('Failed to sign out from Supabase:', error);
}
// Always clear local credentials (removes auth.json file)
this.credentialStore.clearCredentials();
}

View File

@@ -18,7 +18,7 @@ export class CredentialStore {
/**
* Get stored authentication credentials
*/
getCredentials(): AuthCredentials | null {
getCredentials(options?: { allowExpired?: boolean }): AuthCredentials | null {
try {
if (!fs.existsSync(this.config.configFile)) {
return null;
@@ -29,7 +29,11 @@ export class CredentialStore {
) as AuthCredentials;
// Check if token is expired
if (authData.expiresAt && new Date(authData.expiresAt) < new Date()) {
if (
authData.expiresAt &&
new Date(authData.expiresAt) < new Date() &&
!options?.allowExpired
) {
this.logger.warn('Authentication token has expired');
return null;
}
@@ -50,24 +54,23 @@ export class CredentialStore {
try {
// Ensure directory exists
if (!fs.existsSync(this.config.configDir)) {
fs.mkdirSync(this.config.configDir, { recursive: true });
fs.mkdirSync(this.config.configDir, { recursive: true, mode: 0o700 });
}
// Add timestamp
authData.savedAt = new Date().toISOString();
// Save credentials
fs.writeFileSync(
this.config.configFile,
JSON.stringify(authData, null, 2)
);
// Set file permissions to read/write for owner only
fs.chmodSync(this.config.configFile, 0o600);
// Save credentials atomically with secure permissions
const tempFile = `${this.config.configFile}.tmp`;
fs.writeFileSync(tempFile, JSON.stringify(authData, null, 2), {
mode: 0o600
});
fs.renameSync(tempFile, this.config.configFile);
} catch (error) {
throw new AuthenticationError(
`Failed to save auth credentials: ${(error as Error).message}`,
'SAVE_FAILED'
'SAVE_FAILED',
error
);
}
}
@@ -83,7 +86,8 @@ export class CredentialStore {
} catch (error) {
throw new AuthenticationError(
`Failed to clear credentials: ${(error as Error).message}`,
'CLEAR_FAILED'
'CLEAR_FAILED',
error
);
}
}
@@ -92,7 +96,7 @@ export class CredentialStore {
* Check if credentials exist and are valid
*/
hasValidCredentials(): boolean {
const credentials = this.getCredentials();
const credentials = this.getCredentials({ allowExpired: false });
return credentials !== null;
}

View File

@@ -8,8 +8,6 @@ export { OAuthService } from './oauth-service';
export type {
AuthCredentials,
AuthOptions,
AuthResponse,
OAuthFlowOptions,
AuthConfig,
CliData

View File

@@ -6,7 +6,6 @@ import http from 'http';
import { URL } from 'url';
import crypto from 'crypto';
import os from 'os';
import open from 'open';
import {
AuthCredentials,
AuthenticationError,
@@ -27,6 +26,8 @@ export class OAuthService {
private baseUrl: string;
private authorizationUrl: string | null = null;
private originalState: string | null = null;
private authorizationReady: Promise<void> | null = null;
private resolveAuthorizationReady: (() => void) | null = null;
constructor(
credentialStore: CredentialStore,
@@ -43,7 +44,7 @@ export class OAuthService {
*/
async authenticate(options: OAuthFlowOptions = {}): Promise<AuthCredentials> {
const {
openBrowser = true,
openBrowser,
timeout = 300000, // 5 minutes default
onAuthUrl,
onWaitingForAuth,
@@ -55,8 +56,10 @@ export class OAuthService {
// Start the OAuth flow (starts local server)
const authPromise = this.startFlow(timeout);
// Wait for server to start
await new Promise((resolve) => setTimeout(resolve, 100));
// Wait for server to be ready and URL to be generated
if (this.authorizationReady) {
await this.authorizationReady;
}
// Get the authorization URL
const authUrl = this.getAuthorizationUrl();
@@ -73,9 +76,15 @@ export class OAuthService {
onAuthUrl(authUrl);
}
// Open browser if requested
// Open browser if callback provided
if (openBrowser) {
await this.openBrowser(authUrl);
try {
await openBrowser(authUrl);
this.logger.debug('Browser opened successfully with URL:', authUrl);
} catch (error) {
// Log the error but don't throw - user can still manually open the URL
this.logger.warn('Failed to open browser automatically:', error);
}
}
// Notify that we're waiting for authentication
@@ -98,7 +107,8 @@ export class OAuthService {
? error
: new AuthenticationError(
`OAuth authentication failed: ${(error as Error).message}`,
'OAUTH_FAILED'
'OAUTH_FAILED',
error
);
// Notify error
@@ -115,50 +125,62 @@ export class OAuthService {
*/
private async startFlow(timeout: number = 300000): Promise<AuthCredentials> {
const state = this.generateState();
const port = await this.getRandomPort();
const callbackUrl = `http://localhost:${port}/callback`;
// Store the original state for verification
this.originalState = state;
// Prepare CLI data object (server handles OAuth/PKCE)
const cliData: CliData = {
callback: callbackUrl,
state: state,
name: 'Task Master CLI',
version: this.getCliVersion(),
device: os.hostname(),
user: os.userInfo().username,
platform: os.platform(),
timestamp: Date.now()
};
// Create a promise that will resolve when the server is ready
this.authorizationReady = new Promise<void>((resolve) => {
this.resolveAuthorizationReady = resolve;
});
return new Promise((resolve, reject) => {
let serverClosed = false;
let timeoutId: NodeJS.Timeout;
// Create local HTTP server for OAuth callback
const server = http.createServer(async (req, res) => {
const url = new URL(req.url!, `http://127.0.0.1:${port}`);
const server = http.createServer();
if (url.pathname === '/callback') {
await this.handleCallback(
url,
res,
server,
serverClosed,
resolve,
reject
);
serverClosed = true;
} else {
// Handle other paths (favicon, etc.)
res.writeHead(404);
res.end();
// Start server on localhost only, bind to port 0 for automatic port assignment
server.listen(0, '127.0.0.1', () => {
const address = server.address();
if (!address || typeof address === 'string') {
reject(new Error('Failed to get server address'));
return;
}
});
const port = address.port;
const callbackUrl = `http://localhost:${port}/callback`;
// Set up request handler after we know the port
server.on('request', async (req, res) => {
const url = new URL(req.url!, `http://127.0.0.1:${port}`);
if (url.pathname === '/callback') {
await this.handleCallback(
url,
res,
server,
resolve,
reject,
timeoutId
);
} else {
// Handle other paths (favicon, etc.)
res.writeHead(404);
res.end();
}
});
// Prepare CLI data object (server handles OAuth/PKCE)
const cliData: CliData = {
callback: callbackUrl,
state: state,
name: 'Task Master CLI',
version: this.getCliVersion(),
device: os.hostname(),
user: os.userInfo().username,
platform: os.platform(),
timestamp: Date.now()
};
// Start server on localhost only
server.listen(port, '127.0.0.1', () => {
// Build authorization URL for web app sign-in page
const authUrl = new URL(`${this.baseUrl}/auth/sign-in`);
@@ -177,13 +199,23 @@ export class OAuthService {
`OAuth session started - ${cliData.name} v${cliData.version} on port ${port}`
);
this.logger.debug('CLI data:', cliData);
// Signal that the server is ready and URL is available
if (this.resolveAuthorizationReady) {
this.resolveAuthorizationReady();
this.resolveAuthorizationReady = null;
}
});
// Set timeout for authentication
setTimeout(() => {
if (!serverClosed) {
serverClosed = true;
timeoutId = setTimeout(() => {
if (server.listening) {
server.close();
// Clean up the readiness promise if still pending
if (this.resolveAuthorizationReady) {
this.resolveAuthorizationReady();
this.resolveAuthorizationReady = null;
}
reject(
new AuthenticationError('Authentication timeout', 'AUTH_TIMEOUT')
);
@@ -199,9 +231,9 @@ export class OAuthService {
url: URL,
res: http.ServerResponse,
server: http.Server,
serverClosed: boolean,
resolve: (value: AuthCredentials) => void,
reject: (error: any) => void
reject: (error: any) => void,
timeoutId?: NodeJS.Timeout
): Promise<void> {
// Server now returns tokens directly instead of code
const type = url.searchParams.get('type');
@@ -217,26 +249,26 @@ export class OAuthService {
res.end();
if (error) {
if (!serverClosed) {
if (server.listening) {
server.close();
reject(
new AuthenticationError(
errorDescription || error || 'Authentication failed',
'OAUTH_ERROR'
)
);
}
reject(
new AuthenticationError(
errorDescription || error || 'Authentication failed',
'OAUTH_ERROR'
)
);
return;
}
// Verify state parameter for CSRF protection
if (returnedState !== this.originalState) {
if (!serverClosed) {
if (server.listening) {
server.close();
reject(
new AuthenticationError('Invalid state parameter', 'INVALID_STATE')
);
}
reject(
new AuthenticationError('Invalid state parameter', 'INVALID_STATE')
);
return;
}
@@ -269,34 +301,25 @@ export class OAuthService {
this.credentialStore.saveCredentials(authData);
if (!serverClosed) {
if (server.listening) {
server.close();
resolve(authData);
}
// Clear timeout since authentication succeeded
if (timeoutId) {
clearTimeout(timeoutId);
}
resolve(authData);
} catch (error) {
if (!serverClosed) {
if (server.listening) {
server.close();
reject(error);
}
reject(error);
}
} else {
if (!serverClosed) {
if (server.listening) {
server.close();
reject(new AuthenticationError('No access token received', 'NO_TOKEN'));
}
}
}
/**
* Open browser with the given URL
*/
private async openBrowser(url: string): Promise<void> {
try {
await open(url);
this.logger.debug('Browser opened successfully with URL:', url);
} catch (error) {
// Log the error but don't throw - user can still manually open the URL
this.logger.warn('Failed to open browser automatically:', error);
reject(new AuthenticationError('No access token received', 'NO_TOKEN'));
}
}
@@ -307,25 +330,6 @@ export class OAuthService {
return crypto.randomBytes(32).toString('base64url');
}
/**
* Get a random available port
*/
private async getRandomPort(): Promise<number> {
return new Promise((resolve, reject) => {
const server = http.createServer();
server.listen(0, '127.0.0.1', () => {
const address = server.address();
if (!address || typeof address === 'string') {
reject(new Error('Failed to get port'));
return;
}
const port = address.port;
server.close(() => resolve(port));
});
server.on('error', reject);
});
}
/**
* Get CLI version from package.json if available
*/

View File

@@ -12,23 +12,9 @@ export interface AuthCredentials {
savedAt: string;
}
export interface AuthOptions {
email?: string;
password?: string;
apiKey?: string;
}
export interface AuthResponse {
token: string;
refreshToken?: string;
userId: string;
email?: string;
expiresAt?: string;
}
export interface OAuthFlowOptions {
/** Whether to automatically open the browser. Default: true */
openBrowser?: boolean;
/** Callback to open the browser with the auth URL. If not provided, browser won't be opened */
openBrowser?: (url: string) => Promise<void>;
/** Timeout for the OAuth flow in milliseconds. Default: 300000 (5 minutes) */
timeout?: number;
/** Callback to be invoked with the authorization URL */
@@ -58,15 +44,42 @@ export interface CliData {
timestamp?: number;
}
/**
* Authentication error codes
*/
export type AuthErrorCode =
| 'AUTH_TIMEOUT'
| 'AUTH_EXPIRED'
| 'OAUTH_FAILED'
| 'OAUTH_ERROR'
| 'OAUTH_CANCELED'
| 'URL_GENERATION_FAILED'
| 'INVALID_STATE'
| 'NO_TOKEN'
| 'TOKEN_EXCHANGE_FAILED'
| 'INVALID_API_KEY'
| 'INVALID_CREDENTIALS'
| 'NO_REFRESH_TOKEN'
| 'NOT_AUTHENTICATED'
| 'NETWORK_ERROR'
| 'CONFIG_MISSING'
| 'SAVE_FAILED'
| 'CLEAR_FAILED'
| 'STORAGE_ERROR';
/**
* Authentication error class
*/
export class AuthenticationError extends Error {
constructor(
message: string,
public code: string
public code: AuthErrorCode,
public cause?: unknown
) {
super(message);
this.name = 'AuthenticationError';
if (cause && cause instanceof Error) {
this.stack = `${this.stack}\nCaused by: ${cause.stack}`;
}
}
}

View File

@@ -15,15 +15,13 @@ export class SupabaseAuthClient {
*/
private getClient(): SupabaseClient {
if (!this.client) {
// Get Supabase configuration from environment
const supabaseUrl = process.env.SUPABASE_URL || 'http://localhost:8080';
const supabaseAnonKey =
process.env.SUPABASE_ANON_KEY ||
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0';
// Get Supabase configuration from environment - using TM_PUBLIC prefix
const supabaseUrl = process.env.TM_PUBLIC_SUPABASE_URL;
const supabaseAnonKey = process.env.TM_PUBLIC_SUPABASE_ANON_KEY;
if (!supabaseUrl || !supabaseAnonKey) {
throw new AuthenticationError(
'Supabase configuration missing. Please set SUPABASE_URL and SUPABASE_ANON_KEY environment variables.',
'Supabase configuration missing. Please set TM_PUBLIC_SUPABASE_URL and TM_PUBLIC_SUPABASE_ANON_KEY environment variables.',
'CONFIG_MISSING'
);
}
@@ -136,13 +134,15 @@ export class SupabaseAuthClient {
/**
* Sign out (revoke tokens)
* Note: This requires the user to be authenticated with the current session.
* For remote token revocation, a server-side admin API with service_role key would be needed.
*/
async signOut(token: string): Promise<void> {
async signOut(): Promise<void> {
try {
const client = this.getClient();
// Sign out using the token
const { error } = await client.auth.admin.signOut(token);
// Sign out the current session with global scope to revoke all refresh tokens
const { error } = await client.auth.signOut({ scope: 'global' });
if (error) {
this.logger.warn('Failed to sign out:', error);

View File

@@ -177,7 +177,7 @@ describe('ConfigManager', () => {
it('should return storage configuration', () => {
const storage = manager.getStorageConfig();
expect(storage).toEqual({ type: 'file' });
expect(storage).toEqual({ type: 'auto', apiConfigured: false });
});
it('should return API storage configuration when configured', async () => {
@@ -206,7 +206,65 @@ describe('ConfigManager', () => {
expect(storage).toEqual({
type: 'api',
apiEndpoint: 'https://api.example.com',
apiAccessToken: 'token123'
apiAccessToken: 'token123',
apiConfigured: true
});
});
it('should return auto storage configuration with apiConfigured flag', async () => {
// Create a new instance with auto storage config and partial API settings
vi.mocked(ConfigMerger).mockImplementationOnce(
() =>
({
addSource: vi.fn(),
clearSources: vi.fn(),
merge: vi.fn().mockReturnValue({
storage: {
type: 'auto',
apiEndpoint: 'https://api.example.com'
// No apiAccessToken - partial config
}
}),
getSources: vi.fn().mockReturnValue([])
}) as any
);
const autoManager = await ConfigManager.create(testProjectRoot);
const storage = autoManager.getStorageConfig();
expect(storage).toEqual({
type: 'auto',
apiEndpoint: 'https://api.example.com',
apiAccessToken: undefined,
apiConfigured: true // true because apiEndpoint is provided
});
});
it('should return auto storage with apiConfigured false when no API settings', async () => {
// Create a new instance with auto storage but no API settings
vi.mocked(ConfigMerger).mockImplementationOnce(
() =>
({
addSource: vi.fn(),
clearSources: vi.fn(),
merge: vi.fn().mockReturnValue({
storage: {
type: 'auto'
// No API settings at all
}
}),
getSources: vi.fn().mockReturnValue([])
}) as any
);
const autoManager = await ConfigManager.create(testProjectRoot);
const storage = autoManager.getStorageConfig();
expect(storage).toEqual({
type: 'auto',
apiEndpoint: undefined,
apiAccessToken: undefined,
apiConfigured: false // false because no API settings
});
});

View File

@@ -138,6 +138,7 @@ export class ConfigManager {
type: 'file' | 'api' | 'auto';
apiEndpoint?: string;
apiAccessToken?: string;
apiConfigured: boolean;
} {
const storage = this.config.storage;
@@ -148,11 +149,12 @@ export class ConfigManager {
return {
type: storageType,
apiEndpoint: storage?.apiEndpoint,
apiAccessToken: storage?.apiAccessToken
apiAccessToken: storage?.apiAccessToken,
apiConfigured: Boolean(storage?.apiEndpoint || storage?.apiAccessToken)
};
}
return { type: storageType };
return { type: storageType, apiConfigured: false };
}
/**

View File

@@ -85,6 +85,11 @@ describe('EnvironmentConfigProvider', () => {
provider = new EnvironmentConfigProvider(); // Reset provider
config = provider.loadConfig();
expect(config.storage?.type).toBe('api');
process.env.TASKMASTER_STORAGE_TYPE = 'auto';
provider = new EnvironmentConfigProvider(); // Reset provider
config = provider.loadConfig();
expect(config.storage?.type).toBe('auto');
});
it('should handle nested configuration paths', () => {

View File

@@ -31,7 +31,7 @@ export class EnvironmentConfigProvider {
{
env: 'TASKMASTER_STORAGE_TYPE',
path: ['storage', 'type'],
validate: (v: string) => ['file', 'api'].includes(v)
validate: (v: string) => ['file', 'api', 'auto'].includes(v)
},
{ env: 'TASKMASTER_API_ENDPOINT', path: ['storage', 'apiEndpoint'] },
{ env: 'TASKMASTER_API_TOKEN', path: ['storage', 'apiAccessToken'] },

View File

@@ -49,8 +49,6 @@ export {
AuthManager,
AuthenticationError,
type AuthCredentials,
type AuthOptions,
type AuthResponse,
type OAuthFlowOptions,
type AuthConfig
} from './auth';

View File

@@ -85,6 +85,8 @@ export interface StorageSettings {
apiEndpoint?: string;
/** Access token for API authentication */
apiAccessToken?: string;
/** Indicates whether API is configured (has endpoint or token) */
apiConfigured?: boolean;
/** Enable automatic backups */
enableBackup: boolean;
/** Maximum number of backups to retain */
@@ -388,7 +390,7 @@ export const DEFAULT_CONFIG_VALUES = {
NAMING_CONVENTION: 'kebab-case' as const
},
STORAGE: {
TYPE: 'file' as const,
TYPE: 'auto' as const,
ENCODING: 'utf8' as BufferEncoding,
MAX_BACKUPS: 5
},

View File

@@ -0,0 +1,6 @@
/**
* Services module exports
* Provides business logic and service layer functionality
*/
export { TaskService } from './task-service';

View File

@@ -0,0 +1,99 @@
/**
* Test file documenting subpath export usage
* This demonstrates how consumers can use granular imports for better tree-shaking
*/
import { describe, it, expect } from 'vitest';
describe('Subpath Exports', () => {
it('should allow importing from auth subpath', async () => {
// Instead of: import { AuthManager } from '@tm/core';
// Use: import { AuthManager } from '@tm/core/auth';
const authModule = await import('./auth');
expect(authModule.AuthManager).toBeDefined();
expect(authModule.AuthenticationError).toBeDefined();
});
it('should allow importing from storage subpath', async () => {
// Instead of: import { FileStorage } from '@tm/core';
// Use: import { FileStorage } from '@tm/core/storage';
const storageModule = await import('./storage');
expect(storageModule.FileStorage).toBeDefined();
expect(storageModule.ApiStorage).toBeDefined();
expect(storageModule.StorageFactory).toBeDefined();
});
it('should allow importing from config subpath', async () => {
// Instead of: import { ConfigManager } from '@tm/core';
// Use: import { ConfigManager } from '@tm/core/config';
const configModule = await import('./config');
expect(configModule.ConfigManager).toBeDefined();
});
it('should allow importing from errors subpath', async () => {
// Instead of: import { TaskMasterError } from '@tm/core';
// Use: import { TaskMasterError } from '@tm/core/errors';
const errorsModule = await import('./errors');
expect(errorsModule.TaskMasterError).toBeDefined();
expect(errorsModule.ERROR_CODES).toBeDefined();
});
it('should allow importing from logger subpath', async () => {
// Instead of: import { getLogger } from '@tm/core';
// Use: import { getLogger } from '@tm/core/logger';
const loggerModule = await import('./logger');
expect(loggerModule.getLogger).toBeDefined();
expect(loggerModule.createLogger).toBeDefined();
});
it('should allow importing from providers subpath', async () => {
// Instead of: import { BaseProvider } from '@tm/core';
// Use: import { BaseProvider } from '@tm/core/providers';
const providersModule = await import('./providers');
expect(providersModule.BaseProvider).toBeDefined();
});
it('should allow importing from services subpath', async () => {
// Instead of: import { TaskService } from '@tm/core';
// Use: import { TaskService } from '@tm/core/services';
const servicesModule = await import('./services');
expect(servicesModule.TaskService).toBeDefined();
});
it('should allow importing from utils subpath', async () => {
// Instead of: import { generateId } from '@tm/core';
// Use: import { generateId } from '@tm/core/utils';
const utilsModule = await import('./utils');
expect(utilsModule.generateId).toBeDefined();
});
});
/**
* Usage Examples for Consumers:
*
* 1. Import only authentication (smaller bundle):
* ```typescript
* import { AuthManager, AuthenticationError } from '@tm/core/auth';
* ```
*
* 2. Import only storage (no auth code bundled):
* ```typescript
* import { FileStorage, StorageFactory } from '@tm/core/storage';
* ```
*
* 3. Import only errors (minimal bundle):
* ```typescript
* import { TaskMasterError, ERROR_CODES } from '@tm/core/errors';
* ```
*
* 4. Still support convenience imports (larger bundle but better DX):
* ```typescript
* import { AuthManager, FileStorage, TaskMasterError } from '@tm/core';
* ```
*
* Benefits:
* - Better tree-shaking: unused modules are not bundled
* - Clearer dependencies: explicit about what parts of the library you use
* - Faster builds: bundlers can optimize better with granular imports
* - Smaller bundles: especially important for browser/edge deployments
*/

View File

@@ -3,29 +3,17 @@
* This file exports all utility functions and helper classes
*/
// Utility implementations will be defined here
// export * from './validation.js';
// export * from './formatting.js';
// export * from './file-utils.js';
// export * from './async-utils.js';
// Export ID generation utilities
export {
generateTaskId as generateId, // Alias for backward compatibility
generateTaskId,
generateSubtaskId,
isValidTaskId,
isValidSubtaskId,
getParentTaskId
} from './id-generator';
// Placeholder exports - these will be implemented in later tasks
/**
* Generates a unique ID for tasks
* @deprecated This is a placeholder function that will be properly implemented in later tasks
*/
export function generateTaskId(): string {
return `task-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
}
/**
* Validates a task ID format
* @deprecated This is a placeholder function that will be properly implemented in later tasks
*/
export function isValidTaskId(id: string): boolean {
return typeof id === 'string' && id.length > 0;
}
// Additional utility exports
/**
* Formats a date for task timestamps

View File

@@ -28,12 +28,17 @@
"isolatedModules": true,
"paths": {
"@/*": ["./src/*"],
"@/types": ["./src/types"],
"@/providers": ["./src/providers"],
"@/storage": ["./src/storage"],
"@/auth": ["./src/auth"],
"@/config": ["./src/config"],
"@/errors": ["./src/errors"],
"@/interfaces": ["./src/interfaces"],
"@/logger": ["./src/logger"],
"@/parser": ["./src/parser"],
"@/utils": ["./src/utils"],
"@/errors": ["./src/errors"]
"@/providers": ["./src/providers"],
"@/services": ["./src/services"],
"@/storage": ["./src/storage"],
"@/types": ["./src/types"],
"@/utils": ["./src/utils"]
}
},
"include": ["src/**/*"],

View File

@@ -17,12 +17,17 @@ const getBuildTimeEnvs = () => {
export default defineConfig({
entry: {
index: 'src/index.ts',
'types/index': 'src/types/index.ts',
'providers/index': 'src/providers/index.ts',
'storage/index': 'src/storage/index.ts',
'auth/index': 'src/auth/index.ts',
'config/index': 'src/config/index.ts',
'errors/index': 'src/errors/index.ts',
'interfaces/index': 'src/interfaces/index.ts',
'logger/index': 'src/logger/index.ts',
'parser/index': 'src/parser/index.ts',
'utils/index': 'src/utils/index.ts',
'errors/index': 'src/errors/index.ts'
'providers/index': 'src/providers/index.ts',
'services/index': 'src/services/index.ts',
'storage/index': 'src/storage/index.ts',
'types/index': 'src/types/index.ts',
'utils/index': 'src/utils/index.ts'
},
format: ['cjs', 'esm'],
dts: true,