@lobehub/lobehub 2.0.0-next.258 → 2.0.0-next.259

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -43,7 +43,7 @@ export const agentCronJobs = pgTable(
43
43
 
44
44
  // Core configuration
45
45
  enabled: boolean('enabled').default(true),
46
- cronPattern: text('cron_pattern').notNull(), // e.g., "0 */30 * * *"
46
+ cronPattern: text('cron_pattern').notNull(), // e.g., "*/30 * * * *" (every 30 minutes)
47
47
  timezone: text('timezone').default('UTC'),
48
48
 
49
49
  // Content fields
@@ -82,27 +82,61 @@ export const cronPatternSchema = z
82
82
  'Invalid cron pattern',
83
83
  );
84
84
 
85
- // Minimum 30 minutes validation
85
+ // Minimum 30 minutes validation (using standard cron format)
86
86
  export const minimumIntervalSchema = z.string().refine((pattern) => {
87
- // For simplicity, we'll validate common patterns
88
- // More complex validation can be added later
89
- const thirtyMinPatterns = [
90
- '0 */30 * * *', // Every 30 minutes
91
- '0 0 * * *', // Every hour
92
- '0 0 */2 * *', // Every 2 hours
93
- '0 0 */6 * *', // Every 6 hours
94
- '0 0 0 * *', // Daily
95
- '0 0 0 * * 1', // Weekly
96
- '0 0 0 1 *', // Monthly
87
+ // Standard cron format: minute hour day month weekday
88
+ const allowedPatterns = [
89
+ '*/30 * * * *', // Every 30 minutes
90
+ '0 * * * *', // Every hour
91
+ '0 */2 * * *', // Every 2 hours
92
+ '0 */3 * * *', // Every 3 hours
93
+ '0 */4 * * *', // Every 4 hours
94
+ '0 */6 * * *', // Every 6 hours
95
+ '0 */8 * * *', // Every 8 hours
96
+ '0 */12 * * *', // Every 12 hours
97
+ '0 0 * * *', // Daily at midnight
98
+ '0 0 * * 0', // Weekly on Sunday
99
+ '0 0 1 * *', // Monthly on 1st
97
100
  ];
98
101
 
99
- // Check if it matches allowed patterns or follows 30+ minute intervals
100
- return (
101
- thirtyMinPatterns.includes(pattern) ||
102
- pattern.includes('*/30') ||
103
- pattern.includes('*/60') ||
104
- /0 \d+ \* \* \*/.test(pattern)
105
- ); // Hours pattern
102
+ // Check if it matches allowed patterns
103
+ if (allowedPatterns.includes(pattern)) {
104
+ return true;
105
+ }
106
+
107
+ // Parse pattern to validate minimum 30-minute interval
108
+ const parts = pattern.split(' ');
109
+ if (parts.length !== 5) {
110
+ return false;
111
+ }
112
+
113
+ const [minute, hour] = parts;
114
+
115
+ // Allow minute intervals >= 30 (e.g., */30, */45, */60)
116
+ if (minute.startsWith('*/')) {
117
+ const interval = parseInt(minute.slice(2));
118
+ if (!isNaN(interval) && interval >= 30) {
119
+ return true;
120
+ }
121
+ }
122
+
123
+ // Allow hourly patterns: 0 */N * * * where N >= 1
124
+ if (minute === '0' && hour.startsWith('*/')) {
125
+ const interval = parseInt(hour.slice(2));
126
+ if (!isNaN(interval) && interval >= 1) {
127
+ return true;
128
+ }
129
+ }
130
+
131
+ // Allow specific hour patterns: 0 N * * * (runs once per day)
132
+ if (minute === '0' && /^\d+$/.test(hour)) {
133
+ const h = parseInt(hour);
134
+ if (!isNaN(h) && h >= 0 && h <= 23) {
135
+ return true;
136
+ }
137
+ }
138
+
139
+ return false;
106
140
  }, 'Minimum execution interval is 30 minutes');
107
141
 
108
142
  export const executionConditionsSchema = z
@@ -25,7 +25,6 @@
25
25
  "test:coverage": "vitest --coverage --silent='passed-only'"
26
26
  },
27
27
  "dependencies": {
28
- "@napi-rs/canvas": "^0.1.70",
29
28
  "@xmldom/xmldom": "^0.9.8",
30
29
  "concat-stream": "^2.0.0",
31
30
  "debug": "^4.4.3",
@@ -37,6 +36,7 @@
37
36
  "yauzl": "^3.2.0"
38
37
  },
39
38
  "devDependencies": {
39
+ "@napi-rs/canvas": "^0.1.70",
40
40
  "@types/concat-stream": "^2.0.3",
41
41
  "@types/yauzl": "^2.10.3",
42
42
  "typescript": "^5.9.3"
@@ -20,6 +20,31 @@ const foldersToSymlink = [
20
20
 
21
21
  const foldersToCopy = ['src', 'scripts'];
22
22
 
23
+ // Assets to remove from desktop build output (not needed for Electron app)
24
+ const assetsToRemove = [
25
+ // Icons & favicons
26
+ 'apple-touch-icon.png',
27
+ 'favicon.ico',
28
+ 'favicon-32x32.ico',
29
+ 'favicon-16x16.png',
30
+ 'favicon-32x32.png',
31
+
32
+ // SEO & sitemap
33
+ 'sitemap.xml',
34
+ 'sitemap-index.xml',
35
+ 'sitemap',
36
+ 'robots.txt',
37
+
38
+ // Incompatible pages
39
+ 'not-compatible.html',
40
+ 'not-compatible',
41
+
42
+ // Large media assets
43
+ 'videos',
44
+ 'screenshots',
45
+ 'og',
46
+ ];
47
+
23
48
  const filesToCopy = [
24
49
  'package.json',
25
50
  'tsconfig.json',
@@ -85,10 +110,13 @@ const build = async () => {
85
110
 
86
111
  console.log('🏗 Running next build in shadow workspace...');
87
112
  try {
88
- execSync('next build --webpack', {
113
+ execSync('next build', {
89
114
  cwd: TEMP_DIR,
90
115
  env: {
91
116
  ...process.env,
117
+ // Pass PROJECT_ROOT to next.config.ts for outputFileTracingRoot
118
+ // This fixes Turbopack symlink resolution when building in shadow workspace
119
+ ELECTRON_BUILD_PROJECT_ROOT: PROJECT_ROOT,
92
120
  NODE_OPTIONS: process.env.NODE_OPTIONS || '--max-old-space-size=8192',
93
121
  },
94
122
  stdio: 'inherit',
@@ -106,6 +134,16 @@ const build = async () => {
106
134
  if (fs.existsSync(sourceOutDir)) {
107
135
  console.log('📦 Moving "out" directory...');
108
136
  await fs.move(sourceOutDir, targetOutDir);
137
+
138
+ // Remove unnecessary assets from desktop build
139
+ console.log('🗑️ Removing unnecessary assets...');
140
+ for (const asset of assetsToRemove) {
141
+ const assetPath = path.join(targetOutDir, asset);
142
+ if (fs.existsSync(assetPath)) {
143
+ await fs.remove(assetPath);
144
+ console.log(` Removed: ${asset}`);
145
+ }
146
+ }
109
147
  } else {
110
148
  console.warn("⚠️ 'out' directory not found. Using '.next' instead (fallback)?");
111
149
  const sourceNextDir = path.join(TEMP_DIR, '.next');
@@ -144,6 +144,32 @@ export const modifyNextConfig = async (TEMP_DIR: string) => {
144
144
  }
145
145
  }
146
146
 
147
+ // 6. Inject outputFileTracingRoot to fix symlink resolution for Turbopack
148
+ // When building in shadow workspace (TEMP_DIR), symlinks (e.g., node_modules) point to PROJECT_ROOT
149
+ // Turbopack's root defaults to TEMP_DIR, causing strip_prefix to fail for paths outside TEMP_DIR
150
+ // Setting outputFileTracingRoot to PROJECT_ROOT allows Turbopack to correctly resolve these symlinks
151
+ // We use ELECTRON_BUILD_PROJECT_ROOT env var which is set by buildNextApp.mts
152
+ const outputFileTracingRootPair = nextConfigDecl.find({
153
+ rule: {
154
+ pattern: 'outputFileTracingRoot: $A',
155
+ },
156
+ });
157
+ if (!outputFileTracingRootPair) {
158
+ const objectNode = nextConfigDecl.find({
159
+ rule: { kind: 'object' },
160
+ });
161
+ if (objectNode) {
162
+ const range = objectNode.range();
163
+ // Insert outputFileTracingRoot that reads from env var at build time
164
+ // Falls back to current directory if not in electron build context
165
+ edits.push({
166
+ end: range.start.index + 1,
167
+ start: range.start.index + 1,
168
+ text: "\n outputFileTracingRoot: process.env.ELECTRON_BUILD_PROJECT_ROOT || process.cwd(),",
169
+ });
170
+ }
171
+ }
172
+
147
173
  // Remove withPWA wrapper
148
174
  const withPWA = root.find({
149
175
  rule: {
@@ -85,13 +85,15 @@ const AutoSaveHintSlot = memo(() => {
85
85
  return <AutoSaveHint lastUpdatedTime={lastUpdatedTime} saveStatus={status} />;
86
86
  });
87
87
 
88
+ // Standard cron format: minute hour day month weekday
88
89
  const CRON_PATTERNS = [
89
- { label: 'agentCronJobs.interval.30min', value: '0 */30 * * *' },
90
- { label: 'agentCronJobs.interval.1hour', value: '0 0 * * *' },
90
+ { label: 'agentCronJobs.interval.30min', value: '*/30 * * * *' },
91
+ { label: 'agentCronJobs.interval.1hour', value: '0 * * * *' },
92
+ { label: 'agentCronJobs.interval.2hours', value: '0 */2 * * *' },
91
93
  { label: 'agentCronJobs.interval.6hours', value: '0 */6 * * *' },
92
94
  { label: 'agentCronJobs.interval.12hours', value: '0 */12 * * *' },
93
- { label: 'agentCronJobs.interval.daily', value: '0 0 0 * *' },
94
- { label: 'agentCronJobs.interval.weekly', value: '0 0 0 * 0' },
95
+ { label: 'agentCronJobs.interval.daily', value: '0 0 * * *' },
96
+ { label: 'agentCronJobs.interval.weekly', value: '0 0 * * 0' },
95
97
  ];
96
98
 
97
99
  const WEEKDAY_OPTIONS = [
@@ -115,13 +117,15 @@ const WEEKDAY_LABELS: Record<number, string> = {
115
117
  };
116
118
 
117
119
  const getIntervalText = (cronPattern: string) => {
120
+ // Standard cron format mapping
118
121
  const intervalMap: Record<string, string> = {
122
+ '*/30 * * * *': 'agentCronJobs.interval.30min',
123
+ '0 * * * *': 'agentCronJobs.interval.1hour',
119
124
  '0 */12 * * *': 'agentCronJobs.interval.12hours',
120
- '0 */30 * * *': 'agentCronJobs.interval.30min',
125
+ '0 */2 * * *': 'agentCronJobs.interval.2hours',
121
126
  '0 */6 * * *': 'agentCronJobs.interval.6hours',
122
- '0 0 * * *': 'agentCronJobs.interval.1hour',
123
- '0 0 0 * *': 'agentCronJobs.interval.daily',
124
- '0 0 0 * 0': 'agentCronJobs.interval.weekly',
127
+ '0 0 * * *': 'agentCronJobs.interval.daily',
128
+ '0 0 * * 0': 'agentCronJobs.interval.weekly',
125
129
  };
126
130
 
127
131
  return intervalMap[cronPattern] || cronPattern;
@@ -26,13 +26,15 @@ interface CronJobFormProps {
26
26
  onSubmit: (data: CronJobFormData) => void;
27
27
  }
28
28
 
29
+ // Standard cron format: minute hour day month weekday
29
30
  const CRON_PATTERNS = [
30
- { label: 'agentCronJobs.interval.30min', value: '0 */30 * * *' },
31
- { label: 'agentCronJobs.interval.1hour', value: '0 0 * * *' },
32
- { label: 'agentCronJobs.interval.6hours', value: '0 */6 * * *' },
33
- { label: 'agentCronJobs.interval.12hours', value: '0 */12 * * *' },
34
- { label: 'agentCronJobs.interval.daily', value: '0 0 0 * *' },
35
- { label: 'agentCronJobs.interval.weekly', value: '0 0 0 * 0' },
31
+ { label: 'agentCronJobs.interval.30min', value: '*/30 * * * *' }, // Every 30 minutes
32
+ { label: 'agentCronJobs.interval.1hour', value: '0 * * * *' }, // Every hour
33
+ { label: 'agentCronJobs.interval.2hours', value: '0 */2 * * *' }, // Every 2 hours
34
+ { label: 'agentCronJobs.interval.6hours', value: '0 */6 * * *' }, // Every 6 hours
35
+ { label: 'agentCronJobs.interval.12hours', value: '0 */12 * * *' }, // Every 12 hours
36
+ { label: 'agentCronJobs.interval.daily', value: '0 0 * * *' }, // Daily at midnight
37
+ { label: 'agentCronJobs.interval.weekly', value: '0 0 * * 0' }, // Weekly on Sunday
36
38
  ];
37
39
 
38
40
  const WEEKDAY_OPTIONS = [
@@ -120,7 +122,7 @@ const CronJobForm = memo<CronJobFormProps>(({ editingJob, formRef, onSubmit }) =
120
122
  <Form
121
123
  form={form}
122
124
  initialValues={{
123
- cronPattern: '0 */30 * * *', // Default to 30 minutes
125
+ cronPattern: '*/30 * * * *', // Default to every 30 minutes
124
126
  weekdays: [],
125
127
  }}
126
128
  layout="vertical"
@@ -1,121 +0,0 @@
1
- /**
2
- * Full Disk Access utilities for macOS
3
- * Based on https://github.com/inket/FullDiskAccess
4
- */
5
- import { shell } from 'electron';
6
- import { macOS } from 'electron-is';
7
- import fs from 'node:fs';
8
- import os from 'node:os';
9
- import path from 'node:path';
10
-
11
- import { createLogger } from './logger';
12
-
13
- const logger = createLogger('utils:fullDiskAccess');
14
-
15
- /**
16
- * Get the macOS major version number
17
- * Returns 0 if not macOS or unable to determine
18
- *
19
- * Darwin version to macOS version mapping:
20
- * - Darwin 23.x = macOS 14 (Sonoma)
21
- * - Darwin 22.x = macOS 13 (Ventura)
22
- * - Darwin 21.x = macOS 12 (Monterey)
23
- * - Darwin 20.x = macOS 11 (Big Sur)
24
- * - Darwin 19.x = macOS 10.15 (Catalina)
25
- * - Darwin 18.x = macOS 10.14 (Mojave)
26
- */
27
- export function getMacOSMajorVersion(): number {
28
- if (!macOS()) return 0;
29
- try {
30
- const release = os.release(); // e.g., "23.0.0" for macOS 14 (Sonoma)
31
- const darwinMajor = Number.parseInt(release.split('.')[0], 10);
32
- if (darwinMajor >= 20) {
33
- return darwinMajor - 9; // Darwin 20 = macOS 11, Darwin 21 = macOS 12, etc.
34
- }
35
- // For older versions, return 10 (covers Mojave and Catalina)
36
- return 10;
37
- } catch {
38
- return 0;
39
- }
40
- }
41
-
42
- /**
43
- * Check if Full Disk Access is granted by attempting to read a protected directory.
44
- *
45
- * On macOS 12+ (Monterey, Ventura, Sonoma, Sequoia): checks ~/Library/Containers/com.apple.stocks
46
- * On macOS 10.14-11 (Mojave, Catalina, Big Sur): checks ~/Library/Safari
47
- *
48
- * Reading these directories will also register the app in TCC database,
49
- * making it appear in System Settings > Privacy & Security > Full Disk Access
50
- */
51
- export function checkFullDiskAccess(): boolean {
52
- if (!macOS()) return true;
53
-
54
- const homeDir = os.homedir();
55
- const macOSVersion = getMacOSMajorVersion();
56
-
57
- // Determine which protected directory to check based on macOS version
58
- let checkPath: string;
59
- if (macOSVersion >= 12) {
60
- // macOS 12+ (Monterey, Ventura, Sonoma, Sequoia)
61
- checkPath = path.join(homeDir, 'Library', 'Containers', 'com.apple.stocks');
62
- } else {
63
- // macOS 10.14-11 (Mojave, Catalina, Big Sur)
64
- checkPath = path.join(homeDir, 'Library', 'Safari');
65
- }
66
-
67
- try {
68
- fs.readdirSync(checkPath);
69
- logger.info(`[FullDiskAccess] Access granted (able to read ${checkPath})`);
70
- return true;
71
- } catch {
72
- logger.info(`[FullDiskAccess] Access not granted (unable to read ${checkPath})`);
73
- return false;
74
- }
75
- }
76
-
77
- /**
78
- * Open Full Disk Access settings page in System Settings
79
- *
80
- * NOTE: Full Disk Access cannot be requested programmatically.
81
- * User must manually add the app in System Settings.
82
- * There is NO entitlement for Full Disk Access - it's purely TCC controlled.
83
- */
84
- export async function openFullDiskAccessSettings(): Promise<void> {
85
- if (!macOS()) {
86
- logger.info('[FullDiskAccess] Not macOS, skipping');
87
- return;
88
- }
89
-
90
- logger.info('[FullDiskAccess] Opening Full Disk Access settings...');
91
-
92
- // On macOS 13+ (Ventura), System Preferences is replaced by System Settings,
93
- // and deep links may differ. We try multiple known schemes for compatibility.
94
- const candidates = [
95
- // macOS 13+ (Ventura and later) - System Settings
96
- 'x-apple.systempreferences:com.apple.settings.PrivacySecurity.extension?Privacy_AllFiles',
97
- // macOS 13+ alternative format
98
- 'x-apple.systempreferences:com.apple.preference.security?Privacy_AllFiles',
99
- ];
100
-
101
- for (const url of candidates) {
102
- try {
103
- logger.info(`[FullDiskAccess] Trying URL: ${url}`);
104
- await shell.openExternal(url);
105
- logger.info(`[FullDiskAccess] Successfully opened via ${url}`);
106
- return;
107
- } catch (error) {
108
- logger.warn(`[FullDiskAccess] Failed with URL ${url}:`, error);
109
- }
110
- }
111
-
112
- // Fallback: open Privacy & Security pane
113
- try {
114
- const fallbackUrl = 'x-apple.systempreferences:com.apple.preference.security?Privacy';
115
- logger.info(`[FullDiskAccess] Trying fallback URL: ${fallbackUrl}`);
116
- await shell.openExternal(fallbackUrl);
117
- logger.info('[FullDiskAccess] Opened Privacy & Security settings as fallback');
118
- } catch (error) {
119
- logger.error('[FullDiskAccess] Failed to open any Privacy settings:', error);
120
- }
121
- }