jettypod 4.4.115 → 4.4.118
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.
- package/.env +7 -0
- package/apps/dashboard/app/api/claude/[workItemId]/message/route.ts +25 -9
- package/apps/dashboard/app/api/claude/sessions/[sessionId]/message/route.ts +7 -3
- package/apps/dashboard/app/api/tests/run/stream/route.ts +13 -1
- package/apps/dashboard/app/api/usage/route.ts +17 -0
- package/apps/dashboard/app/connect-claude/page.tsx +24 -0
- package/apps/dashboard/app/install-claude/page.tsx +8 -6
- package/apps/dashboard/app/login/page.tsx +229 -0
- package/apps/dashboard/app/page.tsx +5 -3
- package/apps/dashboard/app/settings/page.tsx +2 -0
- package/apps/dashboard/app/subscribe/page.tsx +11 -0
- package/apps/dashboard/app/welcome/page.tsx +23 -0
- package/apps/dashboard/components/AppShell.tsx +51 -9
- package/apps/dashboard/components/CardMenu.tsx +14 -5
- package/apps/dashboard/components/ClaudePanel.tsx +65 -9
- package/apps/dashboard/components/ConnectClaudeScreen.tsx +223 -0
- package/apps/dashboard/components/DragContext.tsx +73 -64
- package/apps/dashboard/components/DraggableCard.tsx +6 -46
- package/apps/dashboard/components/GateCard.tsx +21 -0
- package/apps/dashboard/components/InstallClaudeScreen.tsx +132 -30
- package/apps/dashboard/components/KanbanBoard.tsx +173 -56
- package/apps/dashboard/components/PlaceholderCard.tsx +9 -19
- package/apps/dashboard/components/ProjectSwitcher.tsx +28 -0
- package/apps/dashboard/components/RealTimeKanbanWrapper.tsx +34 -3
- package/apps/dashboard/components/RealTimeTestsWrapper.tsx +30 -2
- package/apps/dashboard/components/SubscribeContent.tsx +191 -0
- package/apps/dashboard/components/TipCard.tsx +176 -0
- package/apps/dashboard/components/UpgradeBanner.tsx +29 -0
- package/apps/dashboard/components/WelcomeScreen.tsx +14 -4
- package/apps/dashboard/components/settings/AccountSection.tsx +163 -0
- package/apps/dashboard/contexts/ClaudeSessionContext.tsx +292 -29
- package/apps/dashboard/contexts/UsageContext.tsx +131 -0
- package/apps/dashboard/contexts/usageHelpers.js +9 -0
- package/apps/dashboard/electron/ipc-handlers.js +220 -114
- package/apps/dashboard/electron/main.js +415 -37
- package/apps/dashboard/electron/preload.js +23 -4
- package/apps/dashboard/electron/session-manager.js +141 -0
- package/apps/dashboard/electron-builder.config.js +3 -5
- package/apps/dashboard/lib/claude-process-manager.ts +6 -4
- package/apps/dashboard/lib/db-bridge.ts +32 -0
- package/apps/dashboard/lib/db.ts +159 -13
- package/apps/dashboard/lib/session-state-machine.ts +3 -0
- package/apps/dashboard/lib/session-stream-manager.ts +76 -13
- package/apps/dashboard/lib/tests.ts +3 -1
- package/apps/dashboard/next.config.js +19 -14
- package/apps/dashboard/package.json +3 -1
- package/apps/dashboard/scripts/upload-to-r2.js +89 -0
- package/apps/dashboard/tsconfig.tsbuildinfo +1 -0
- package/apps/update-server/package.json +16 -0
- package/apps/update-server/schema.sql +31 -0
- package/apps/update-server/src/index.ts +1074 -0
- package/apps/update-server/tsconfig.json +16 -0
- package/apps/update-server/wrangler.toml +35 -0
- package/docs/bdd-guidance.md +390 -0
- package/jettypod.js +5 -4
- package/lib/migrations/027-plan-at-creation-column.js +31 -0
- package/lib/migrations/028-ready-for-review-column.js +27 -0
- package/lib/schema.js +3 -1
- package/lib/seed-onboarding.js +100 -68
- package/lib/work-commands/index.js +43 -13
- package/lib/work-tracking/index.js +46 -27
- package/package.json +1 -1
- package/skills-templates/bug-mode/SKILL.md +5 -11
- package/skills-templates/request-routing/SKILL.md +24 -11
- package/skills-templates/simple-improvement/SKILL.md +35 -19
- package/skills-templates/stable-mode/SKILL.md +5 -6
- package/templates/bdd-guidance.md +139 -0
- package/templates/bdd-scaffolding/wait.js +18 -0
- package/templates/bdd-scaffolding/world.js +19 -0
- package/.jettypod-backup/work.db +0 -0
- package/apps/dashboard/app/access-code/page.tsx +0 -110
- package/lib/discovery-checkpoint.js +0 -123
- package/skills-templates/project-discovery/SKILL.md +0 -372
|
@@ -153,77 +153,6 @@ function initializeJettypod(projectPath) {
|
|
|
153
153
|
|
|
154
154
|
}
|
|
155
155
|
|
|
156
|
-
/**
|
|
157
|
-
* Check if a project folder has no implementation code.
|
|
158
|
-
*/
|
|
159
|
-
function isBlankProject(projectPath) {
|
|
160
|
-
const IMPL_PATTERNS = [
|
|
161
|
-
/\.js$/, /\.ts$/, /\.jsx$/, /\.tsx$/, /\.mjs$/, /\.cjs$/,
|
|
162
|
-
/\.py$/, /\.rb$/, /\.go$/, /\.rs$/, /\.java$/, /\.kt$/,
|
|
163
|
-
/\.c$/, /\.cpp$/, /\.h$/, /\.swift$/, /\.php$/,
|
|
164
|
-
/^src\//, /^lib\//, /^app\//,
|
|
165
|
-
/^package\.json$/, /^Cargo\.toml$/, /^requirements\.txt$/,
|
|
166
|
-
/^Gemfile$/, /^go\.mod$/, /^pom\.xml$/, /^build\.gradle$/,
|
|
167
|
-
];
|
|
168
|
-
const IGNORE = [/^\.git\//, /^\.jettypod\//, /^\.claude\//, /^node_modules\//];
|
|
169
|
-
|
|
170
|
-
function scan(dir, baseDir) {
|
|
171
|
-
if (!fs.existsSync(dir)) return false;
|
|
172
|
-
let entries;
|
|
173
|
-
try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return false; }
|
|
174
|
-
for (const entry of entries) {
|
|
175
|
-
const rel = path.relative(baseDir, path.join(dir, entry.name));
|
|
176
|
-
if (IGNORE.some(p => p.test(rel) || p.test(entry.name))) continue;
|
|
177
|
-
if (entry.isDirectory()) {
|
|
178
|
-
if (IMPL_PATTERNS.some(p => p.test(rel + '/'))) return true;
|
|
179
|
-
if (scan(path.join(dir, entry.name), baseDir)) return true;
|
|
180
|
-
} else {
|
|
181
|
-
if (IMPL_PATTERNS.some(p => p.test(rel))) return true;
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
return false;
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
return !scan(projectPath, projectPath);
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
/**
|
|
191
|
-
* Seed onboarding epic and chores into a blank project's database.
|
|
192
|
-
*/
|
|
193
|
-
function seedOnboardingEpic(projectPath) {
|
|
194
|
-
const dbPath = path.join(projectPath, '.jettypod', 'work.db');
|
|
195
|
-
const db = new Database(dbPath);
|
|
196
|
-
db.pragma('journal_mode = WAL');
|
|
197
|
-
db.pragma('foreign_keys = ON');
|
|
198
|
-
runMigrations(db);
|
|
199
|
-
|
|
200
|
-
// Check if already seeded
|
|
201
|
-
const existing = db.prepare("SELECT id FROM work_items WHERE type = 'epic' AND title = 'Project Onboarding'").get();
|
|
202
|
-
if (existing) { db.close(); return; }
|
|
203
|
-
|
|
204
|
-
const insert = db.prepare(
|
|
205
|
-
'INSERT INTO work_items (type, title, description, parent_id, status, created_at) VALUES (?, ?, ?, ?, ?, ?)'
|
|
206
|
-
);
|
|
207
|
-
const now = new Date().toISOString();
|
|
208
|
-
|
|
209
|
-
const epicResult = insert.run('epic', 'Project Onboarding',
|
|
210
|
-
'Get your project set up and planned. Work through these chores one at a time — each one is a short conversation.', null, 'backlog', now);
|
|
211
|
-
const epicId = epicResult.lastInsertRowid;
|
|
212
|
-
|
|
213
|
-
const chores = [
|
|
214
|
-
{ title: 'Align on the user journey', desc: 'Help the user define what their product does.\n\nCLAUDE SESSION GUIDANCE:\nOpen with: "What do users DO in this product?"\n\nOUTCOME:\n- A clear description of the core user journey' },
|
|
215
|
-
{ title: 'Explore UX approaches', desc: 'Help the user decide how the product should feel.\n\nCONTEXT FROM PREVIOUS CHORE:\nRead previous decisions first.\n\nCLAUDE SESSION GUIDANCE:\nPresent 3 UX approaches.\n\nOUTCOME:\n- 3 UX options compared\n- A winner chosen' },
|
|
216
|
-
{ title: 'Choose a tech stack', desc: 'Help the user pick the right tech stack.\n\nCONTEXT FROM PREVIOUS CHORES:\nRead previous decisions first.\n\nCLAUDE SESSION GUIDANCE:\nPresent 3 tech stack options.\n\nOUTCOME:\n- A tech stack chosen with rationale' },
|
|
217
|
-
{ title: 'Break the project into epics', desc: 'Break the project into buildable phases.\n\nCONTEXT FROM PREVIOUS CHORES:\nRead all previous decisions.\n\nCLAUDE SESSION GUIDANCE:\nPropose 3-5 epics.\n\nOUTCOME:\n- 3-5 epics created in the backlog' },
|
|
218
|
-
];
|
|
219
|
-
|
|
220
|
-
for (const chore of chores) {
|
|
221
|
-
insert.run('chore', chore.title, chore.desc, epicId, 'backlog', now);
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
db.close();
|
|
225
|
-
}
|
|
226
|
-
|
|
227
156
|
// Register all IPC handlers
|
|
228
157
|
function registerIpcHandlers() {
|
|
229
158
|
// ==================== Kanban ====================
|
|
@@ -731,7 +660,56 @@ function registerIpcHandlers() {
|
|
|
731
660
|
return listDevServers();
|
|
732
661
|
});
|
|
733
662
|
|
|
734
|
-
// ==================== Project Dialog ====================
|
|
663
|
+
// ==================== New Project Dialog ====================
|
|
664
|
+
ipcMain.handle('dialog:newProject', async () => {
|
|
665
|
+
const mainWindow = BrowserWindow.getFocusedWindow();
|
|
666
|
+
|
|
667
|
+
const result = await dialog.showSaveDialog(mainWindow, {
|
|
668
|
+
title: 'Create New Project',
|
|
669
|
+
buttonLabel: 'Create',
|
|
670
|
+
nameFieldLabel: 'Project Name:',
|
|
671
|
+
showsTagField: false,
|
|
672
|
+
});
|
|
673
|
+
|
|
674
|
+
if (result.canceled || !result.filePath) {
|
|
675
|
+
return { success: false, canceled: true };
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
const projectPath = result.filePath;
|
|
679
|
+
|
|
680
|
+
// Create the directory
|
|
681
|
+
try {
|
|
682
|
+
fs.mkdirSync(projectPath, { recursive: true });
|
|
683
|
+
} catch (mkdirError) {
|
|
684
|
+
return {
|
|
685
|
+
success: false,
|
|
686
|
+
error: `Failed to create directory: ${mkdirError.message}`
|
|
687
|
+
};
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
// Initialize JettyPod in the new directory
|
|
691
|
+
try {
|
|
692
|
+
initializeJettypod(projectPath);
|
|
693
|
+
} catch (initError) {
|
|
694
|
+
return {
|
|
695
|
+
success: false,
|
|
696
|
+
error: `Failed to initialize JettyPod: ${initError.message}`
|
|
697
|
+
};
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
const { setProjectRoot } = require('./main');
|
|
701
|
+
await setProjectRoot(projectPath);
|
|
702
|
+
|
|
703
|
+
closeDb();
|
|
704
|
+
cachedDb = null;
|
|
705
|
+
projectRoot = projectPath;
|
|
706
|
+
|
|
707
|
+
addRecentProject(projectPath);
|
|
708
|
+
|
|
709
|
+
return { success: true, path: projectPath };
|
|
710
|
+
});
|
|
711
|
+
|
|
712
|
+
// ==================== Open Project Dialog ====================
|
|
735
713
|
ipcMain.handle('dialog:openProject', async () => {
|
|
736
714
|
const mainWindow = BrowserWindow.getFocusedWindow();
|
|
737
715
|
|
|
@@ -770,15 +748,6 @@ function registerIpcHandlers() {
|
|
|
770
748
|
cachedDb = null;
|
|
771
749
|
projectRoot = selectedPath;
|
|
772
750
|
|
|
773
|
-
// Seed onboarding epic for blank projects (safe to call every open - has dupe check)
|
|
774
|
-
if (isBlankProject(selectedPath)) {
|
|
775
|
-
try {
|
|
776
|
-
seedOnboardingEpic(selectedPath);
|
|
777
|
-
} catch (e) {
|
|
778
|
-
console.error('Could not seed onboarding:', e.message);
|
|
779
|
-
}
|
|
780
|
-
}
|
|
781
|
-
|
|
782
751
|
// Add to recent projects list
|
|
783
752
|
addRecentProject(selectedPath);
|
|
784
753
|
|
|
@@ -827,15 +796,6 @@ function registerIpcHandlers() {
|
|
|
827
796
|
cachedDb = null;
|
|
828
797
|
projectRoot = projectPath;
|
|
829
798
|
|
|
830
|
-
// Seed onboarding epic for blank projects (safe to call every open - has dupe check)
|
|
831
|
-
if (isBlankProject(projectPath)) {
|
|
832
|
-
try {
|
|
833
|
-
seedOnboardingEpic(projectPath);
|
|
834
|
-
} catch (e) {
|
|
835
|
-
console.error('Could not seed onboarding:', e.message);
|
|
836
|
-
}
|
|
837
|
-
}
|
|
838
|
-
|
|
839
799
|
// Update recent projects (move to top)
|
|
840
800
|
addRecentProject(projectPath);
|
|
841
801
|
|
|
@@ -858,49 +818,195 @@ function registerIpcHandlers() {
|
|
|
858
818
|
return await updateClaudeCode();
|
|
859
819
|
});
|
|
860
820
|
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
821
|
+
ipcMain.handle('claudeCode:isAuthenticated', () => {
|
|
822
|
+
const { checkClaudeCodeAuthenticated } = require('./main');
|
|
823
|
+
return checkClaudeCodeAuthenticated();
|
|
824
|
+
});
|
|
825
|
+
|
|
826
|
+
ipcMain.handle('claudeCode:login', async () => {
|
|
827
|
+
const { loginClaudeCode } = require('./main');
|
|
828
|
+
return await loginClaudeCode();
|
|
829
|
+
});
|
|
830
|
+
|
|
831
|
+
// ==================== Subscription Gating ====================
|
|
832
|
+
|
|
833
|
+
const UPDATE_SERVER_URL = 'https://jettypod-update-server.spangbaryn2.workers.dev';
|
|
834
|
+
|
|
835
|
+
ipcMain.handle('subscription:createCheckout', async (event, plan) => {
|
|
836
|
+
try {
|
|
837
|
+
// Include JWT auth header so checkout session gets user_id in metadata
|
|
838
|
+
const headers = { 'Content-Type': 'application/json' };
|
|
839
|
+
const authPath = path.join(app.getPath('userData'), 'auth.json');
|
|
840
|
+
if (fs.existsSync(authPath)) {
|
|
841
|
+
try {
|
|
842
|
+
const authData = JSON.parse(fs.readFileSync(authPath, 'utf-8'));
|
|
843
|
+
if (authData.token) {
|
|
844
|
+
headers['Authorization'] = `Bearer ${authData.token}`;
|
|
845
|
+
}
|
|
846
|
+
} catch {
|
|
847
|
+
// Continue without auth if read fails
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
const response = await fetch(`${UPDATE_SERVER_URL}/checkout/create-session`, {
|
|
852
|
+
method: 'POST',
|
|
853
|
+
headers,
|
|
854
|
+
body: JSON.stringify({ plan: plan || 'monthly' }),
|
|
855
|
+
});
|
|
856
|
+
|
|
857
|
+
if (!response.ok) {
|
|
858
|
+
return { success: false, error: 'Failed to create checkout session' };
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
const data = await response.json();
|
|
862
|
+
if (data.url) {
|
|
863
|
+
const { shell } = require('electron');
|
|
864
|
+
shell.openExternal(data.url);
|
|
865
|
+
return { success: true };
|
|
866
|
+
}
|
|
867
|
+
return { success: false, error: 'No checkout URL returned' };
|
|
868
|
+
} catch (error) {
|
|
869
|
+
return { success: false, error: `Checkout failed: ${error.message}` };
|
|
870
|
+
}
|
|
871
|
+
});
|
|
872
|
+
|
|
873
|
+
ipcMain.handle('billing:openCustomerPortal', async () => {
|
|
874
|
+
try {
|
|
875
|
+
const headers = { 'Content-Type': 'application/json' };
|
|
876
|
+
const authPath = path.join(app.getPath('userData'), 'auth.json');
|
|
877
|
+
if (fs.existsSync(authPath)) {
|
|
878
|
+
try {
|
|
879
|
+
const authData = JSON.parse(fs.readFileSync(authPath, 'utf-8'));
|
|
880
|
+
if (authData.token) {
|
|
881
|
+
headers['Authorization'] = `Bearer ${authData.token}`;
|
|
882
|
+
}
|
|
883
|
+
} catch {
|
|
884
|
+
// Continue without auth if read fails
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
const response = await fetch(`${UPDATE_SERVER_URL}/billing/customer-portal`, {
|
|
889
|
+
method: 'POST',
|
|
890
|
+
headers,
|
|
891
|
+
});
|
|
892
|
+
|
|
893
|
+
if (!response.ok) {
|
|
894
|
+
return { success: false, error: 'Failed to open billing portal' };
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
const data = await response.json();
|
|
898
|
+
if (data.url) {
|
|
899
|
+
shell.openExternal(data.url);
|
|
900
|
+
return { success: true };
|
|
901
|
+
}
|
|
902
|
+
return { success: false, error: 'No portal URL returned' };
|
|
903
|
+
} catch (error) {
|
|
904
|
+
return { success: false, error: `Billing portal failed: ${error.message}` };
|
|
867
905
|
}
|
|
906
|
+
});
|
|
868
907
|
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
activatedAt: new Date().toISOString(),
|
|
874
|
-
code: code.trim()
|
|
875
|
-
};
|
|
908
|
+
ipcMain.handle('subscription:activate', async (event, customerId) => {
|
|
909
|
+
if (!customerId || !customerId.trim().startsWith('cus_')) {
|
|
910
|
+
return { success: false, error: 'Invalid customer ID. It should start with cus_' };
|
|
911
|
+
}
|
|
876
912
|
|
|
877
913
|
try {
|
|
878
|
-
//
|
|
879
|
-
const
|
|
914
|
+
// Validate against the update server
|
|
915
|
+
const response = await fetch(`${UPDATE_SERVER_URL}/subscription/validate`, {
|
|
916
|
+
method: 'POST',
|
|
917
|
+
headers: { 'Content-Type': 'application/json' },
|
|
918
|
+
body: JSON.stringify({ customerId: customerId.trim() }),
|
|
919
|
+
});
|
|
920
|
+
|
|
921
|
+
const result = await response.json();
|
|
922
|
+
if (!result.valid) {
|
|
923
|
+
return { success: false, error: result.error || 'No active subscription found' };
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
// Store the subscription
|
|
927
|
+
const subPath = path.join(app.getPath('userData'), 'subscription.json');
|
|
928
|
+
const subData = {
|
|
929
|
+
customerId: customerId.trim(),
|
|
930
|
+
activatedAt: new Date().toISOString(),
|
|
931
|
+
};
|
|
932
|
+
|
|
933
|
+
const dir = path.dirname(subPath);
|
|
880
934
|
if (!fs.existsSync(dir)) {
|
|
881
935
|
fs.mkdirSync(dir, { recursive: true });
|
|
882
936
|
}
|
|
883
|
-
fs.writeFileSync(
|
|
937
|
+
fs.writeFileSync(subPath, JSON.stringify(subData, null, 2));
|
|
884
938
|
return { success: true };
|
|
885
939
|
} catch (error) {
|
|
886
|
-
return { success: false, error: `
|
|
940
|
+
return { success: false, error: `Activation failed: ${error.message}` };
|
|
941
|
+
}
|
|
942
|
+
});
|
|
943
|
+
|
|
944
|
+
ipcMain.handle('subscription:getStatus', () => {
|
|
945
|
+
const subPath = path.join(app.getPath('userData'), 'subscription.json');
|
|
946
|
+
if (!fs.existsSync(subPath)) {
|
|
947
|
+
return { active: false };
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
try {
|
|
951
|
+
const data = JSON.parse(fs.readFileSync(subPath, 'utf-8'));
|
|
952
|
+
return { active: !!data.customerId, customerId: data.customerId, activatedAt: data.activatedAt };
|
|
953
|
+
} catch {
|
|
954
|
+
return { active: false };
|
|
955
|
+
}
|
|
956
|
+
});
|
|
957
|
+
|
|
958
|
+
// ==================== Auth (JWT-based) ====================
|
|
959
|
+
|
|
960
|
+
ipcMain.handle('auth:loginWithGoogle', () => {
|
|
961
|
+
const { shell } = require('electron');
|
|
962
|
+
shell.openExternal(`${UPDATE_SERVER_URL}/auth/google`);
|
|
963
|
+
return { success: true };
|
|
964
|
+
});
|
|
965
|
+
|
|
966
|
+
ipcMain.handle('auth:saveToken', (event, token, user) => {
|
|
967
|
+
const authPath = path.join(app.getPath('userData'), 'auth.json');
|
|
968
|
+
const dir = path.dirname(authPath);
|
|
969
|
+
if (!fs.existsSync(dir)) {
|
|
970
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
887
971
|
}
|
|
972
|
+
fs.writeFileSync(authPath, JSON.stringify({ token, user, savedAt: new Date().toISOString() }, null, 2));
|
|
973
|
+
return { success: true };
|
|
888
974
|
});
|
|
889
975
|
|
|
890
|
-
ipcMain.handle('
|
|
891
|
-
const
|
|
892
|
-
if (!fs.existsSync(
|
|
893
|
-
return {
|
|
976
|
+
ipcMain.handle('auth:getStatus', () => {
|
|
977
|
+
const authPath = path.join(app.getPath('userData'), 'auth.json');
|
|
978
|
+
if (!fs.existsSync(authPath)) {
|
|
979
|
+
return { authenticated: false };
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
try {
|
|
983
|
+
const data = JSON.parse(fs.readFileSync(authPath, 'utf-8'));
|
|
984
|
+
return { authenticated: !!data.token, token: data.token, user: data.user };
|
|
985
|
+
} catch {
|
|
986
|
+
return { authenticated: false };
|
|
894
987
|
}
|
|
988
|
+
});
|
|
989
|
+
|
|
990
|
+
ipcMain.handle('auth:getToken', () => {
|
|
991
|
+
const authPath = path.join(app.getPath('userData'), 'auth.json');
|
|
992
|
+
if (!fs.existsSync(authPath)) return null;
|
|
895
993
|
|
|
896
994
|
try {
|
|
897
|
-
const data = JSON.parse(fs.readFileSync(
|
|
898
|
-
return
|
|
995
|
+
const data = JSON.parse(fs.readFileSync(authPath, 'utf-8'));
|
|
996
|
+
return data.token || null;
|
|
899
997
|
} catch {
|
|
900
|
-
return
|
|
998
|
+
return null;
|
|
901
999
|
}
|
|
902
1000
|
});
|
|
903
1001
|
|
|
1002
|
+
ipcMain.handle('auth:logout', () => {
|
|
1003
|
+
const authPath = path.join(app.getPath('userData'), 'auth.json');
|
|
1004
|
+
if (fs.existsSync(authPath)) {
|
|
1005
|
+
fs.unlinkSync(authPath);
|
|
1006
|
+
}
|
|
1007
|
+
return { success: true };
|
|
1008
|
+
});
|
|
1009
|
+
|
|
904
1010
|
// ==================== Shell Operations ====================
|
|
905
1011
|
ipcMain.handle('shell:openPath', async (event, filePath) => {
|
|
906
1012
|
// Use openExternal with file:// URL to open in default browser
|