@gxp-dev/tools 2.0.5
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/.github/workflows/npm-publish.yml +48 -0
- package/CLAUDE.md +400 -0
- package/README.md +247 -0
- package/REFACTOR_PLAN.md +194 -0
- package/bin/gx-devtools.js +87 -0
- package/bin/lib/cli.js +251 -0
- package/bin/lib/commands/assets.js +337 -0
- package/bin/lib/commands/build.js +259 -0
- package/bin/lib/commands/datastore.js +433 -0
- package/bin/lib/commands/dev.js +328 -0
- package/bin/lib/commands/extensions.js +298 -0
- package/bin/lib/commands/index.js +35 -0
- package/bin/lib/commands/init.js +307 -0
- package/bin/lib/commands/publish.js +189 -0
- package/bin/lib/commands/socket.js +158 -0
- package/bin/lib/commands/ssl.js +47 -0
- package/bin/lib/constants.js +120 -0
- package/bin/lib/tui/App.tsx +600 -0
- package/bin/lib/tui/components/CommandInput.tsx +278 -0
- package/bin/lib/tui/components/GeminiPanel.tsx +161 -0
- package/bin/lib/tui/components/Header.tsx +27 -0
- package/bin/lib/tui/components/LogPanel.tsx +122 -0
- package/bin/lib/tui/components/TabBar.tsx +56 -0
- package/bin/lib/tui/components/WelcomeScreen.tsx +80 -0
- package/bin/lib/tui/index.tsx +63 -0
- package/bin/lib/tui/services/ExtensionService.ts +122 -0
- package/bin/lib/tui/services/GeminiService.ts +395 -0
- package/bin/lib/tui/services/ServiceManager.ts +336 -0
- package/bin/lib/tui/services/SocketService.ts +204 -0
- package/bin/lib/tui/services/ViteService.ts +107 -0
- package/bin/lib/tui/services/index.ts +13 -0
- package/bin/lib/utils/files.js +180 -0
- package/bin/lib/utils/index.js +17 -0
- package/bin/lib/utils/paths.js +138 -0
- package/bin/lib/utils/prompts.js +71 -0
- package/bin/lib/utils/ssl.js +233 -0
- package/browser-extensions/README.md +1 -0
- package/browser-extensions/chrome/background.js +857 -0
- package/browser-extensions/chrome/content.js +51 -0
- package/browser-extensions/chrome/devtools.html +9 -0
- package/browser-extensions/chrome/devtools.js +23 -0
- package/browser-extensions/chrome/icons/gx_off_128.png +0 -0
- package/browser-extensions/chrome/icons/gx_off_16.png +0 -0
- package/browser-extensions/chrome/icons/gx_off_32.png +0 -0
- package/browser-extensions/chrome/icons/gx_off_64.png +0 -0
- package/browser-extensions/chrome/icons/gx_on_128.png +0 -0
- package/browser-extensions/chrome/icons/gx_on_16.png +0 -0
- package/browser-extensions/chrome/icons/gx_on_32.png +0 -0
- package/browser-extensions/chrome/icons/gx_on_64.png +0 -0
- package/browser-extensions/chrome/inspector.js +1087 -0
- package/browser-extensions/chrome/manifest.json +70 -0
- package/browser-extensions/chrome/panel.html +638 -0
- package/browser-extensions/chrome/panel.js +862 -0
- package/browser-extensions/chrome/popup.html +399 -0
- package/browser-extensions/chrome/popup.js +515 -0
- package/browser-extensions/chrome/rules.json +1 -0
- package/browser-extensions/chrome/test-chrome.html +145 -0
- package/browser-extensions/chrome/test-mixed-content.html +190 -0
- package/browser-extensions/chrome/test-uri-pattern.html +199 -0
- package/browser-extensions/firefox/README.md +134 -0
- package/browser-extensions/firefox/background.js +804 -0
- package/browser-extensions/firefox/content.js +120 -0
- package/browser-extensions/firefox/debug-errors.html +229 -0
- package/browser-extensions/firefox/debug-https.html +113 -0
- package/browser-extensions/firefox/devtools.html +9 -0
- package/browser-extensions/firefox/devtools.js +24 -0
- package/browser-extensions/firefox/icons/gx_off_128.png +0 -0
- package/browser-extensions/firefox/icons/gx_off_16.png +0 -0
- package/browser-extensions/firefox/icons/gx_off_32.png +0 -0
- package/browser-extensions/firefox/icons/gx_off_64.png +0 -0
- package/browser-extensions/firefox/icons/gx_on_128.png +0 -0
- package/browser-extensions/firefox/icons/gx_on_16.png +0 -0
- package/browser-extensions/firefox/icons/gx_on_32.png +0 -0
- package/browser-extensions/firefox/icons/gx_on_64.png +0 -0
- package/browser-extensions/firefox/inspector.js +1087 -0
- package/browser-extensions/firefox/manifest.json +67 -0
- package/browser-extensions/firefox/panel.html +638 -0
- package/browser-extensions/firefox/panel.js +862 -0
- package/browser-extensions/firefox/popup.html +525 -0
- package/browser-extensions/firefox/popup.js +536 -0
- package/browser-extensions/firefox/test-gramercy.html +126 -0
- package/browser-extensions/firefox/test-imports.html +58 -0
- package/browser-extensions/firefox/test-masking.html +147 -0
- package/browser-extensions/firefox/test-uri-pattern.html +199 -0
- package/docs/DOCUSAURUS_IMPORT.md +378 -0
- package/docs/_category_.json +8 -0
- package/docs/app-manifest.md +272 -0
- package/docs/building-for-platform.md +315 -0
- package/docs/dev-tools.md +291 -0
- package/docs/getting-started.md +180 -0
- package/docs/gxp-store.md +305 -0
- package/docs/index.md +44 -0
- package/package.json +77 -0
- package/runtime/PortalContainer.vue +326 -0
- package/runtime/dev-tools/DevToolsModal.vue +217 -0
- package/runtime/dev-tools/LayoutSwitcher.vue +221 -0
- package/runtime/dev-tools/MockDataEditor.vue +621 -0
- package/runtime/dev-tools/SocketSimulator.vue +562 -0
- package/runtime/dev-tools/StoreInspector.vue +644 -0
- package/runtime/dev-tools/index.js +6 -0
- package/runtime/gxpStringsPlugin.js +428 -0
- package/runtime/index.html +22 -0
- package/runtime/main.js +32 -0
- package/runtime/mock-api/auth-middleware.js +97 -0
- package/runtime/mock-api/image-generator.js +221 -0
- package/runtime/mock-api/index.js +197 -0
- package/runtime/mock-api/response-generator.js +394 -0
- package/runtime/mock-api/route-generator.js +323 -0
- package/runtime/mock-api/socket-triggers.js +371 -0
- package/runtime/mock-api/spec-loader.js +300 -0
- package/runtime/server.js +180 -0
- package/runtime/stores/gxpPortalConfigStore.js +554 -0
- package/runtime/stores/index.js +6 -0
- package/runtime/vite-inspector-plugin.js +749 -0
- package/runtime/vite-source-tracker-plugin.js +232 -0
- package/runtime/vite.config.js +402 -0
- package/scripts/launch-chrome.js +90 -0
- package/scripts/pack-chrome.js +91 -0
- package/socket-events/AiSessionMessageCreated.json +18 -0
- package/socket-events/SocialStreamPostCreated.json +24 -0
- package/socket-events/SocialStreamPostVariantCompleted.json +23 -0
- package/template/README.md +332 -0
- package/template/app-manifest.json +32 -0
- package/template/dev-assets/images/avatar-placeholder.png +0 -0
- package/template/dev-assets/images/background-placeholder.jpg +0 -0
- package/template/dev-assets/images/banner-placeholder.jpg +0 -0
- package/template/dev-assets/images/icon-placeholder.png +0 -0
- package/template/dev-assets/images/logo-placeholder.png +0 -0
- package/template/dev-assets/images/product-placeholder.jpg +0 -0
- package/template/dev-assets/images/thumbnail-placeholder.jpg +0 -0
- package/template/env.example +51 -0
- package/template/gitignore +53 -0
- package/template/index.html +22 -0
- package/template/main.js +28 -0
- package/template/src/DemoPage.vue +459 -0
- package/template/src/Plugin.vue +38 -0
- package/template/src/stores/index.js +9 -0
- package/template/src/stores/test-data.json +173 -0
- package/template/theme-layouts/AdditionalStyling.css +0 -0
- package/template/theme-layouts/PrivateLayout.vue +39 -0
- package/template/theme-layouts/PublicLayout.vue +39 -0
- package/template/theme-layouts/SystemLayout.vue +39 -0
- package/template/vite.config.js +333 -0
- package/tsconfig.tui.json +21 -0
- package/vite.config.js +164 -0
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
|
|
4
|
+
const LOGO = `
|
|
5
|
+
██████╗ ██╗ ██╗██████╗
|
|
6
|
+
██╔════╝ ╚██╗██╔╝██╔══██╗
|
|
7
|
+
██║ ███╗ ╚███╔╝ ██████╔╝
|
|
8
|
+
██║ ██║ ██╔██╗ ██╔═══╝
|
|
9
|
+
╚██████╔╝██╔╝ ██╗██║
|
|
10
|
+
╚═════╝ ╚═╝ ╚═╝╚═╝
|
|
11
|
+
`;
|
|
12
|
+
|
|
13
|
+
export default function WelcomeScreen() {
|
|
14
|
+
return (
|
|
15
|
+
<Box
|
|
16
|
+
flexDirection="column"
|
|
17
|
+
alignItems="center"
|
|
18
|
+
justifyContent="center"
|
|
19
|
+
flexGrow={1}
|
|
20
|
+
padding={1}
|
|
21
|
+
>
|
|
22
|
+
<Text color="blue">{LOGO}</Text>
|
|
23
|
+
|
|
24
|
+
<Box marginTop={1}>
|
|
25
|
+
<Text bold color="white">GxP DevStudio</Text>
|
|
26
|
+
</Box>
|
|
27
|
+
|
|
28
|
+
<Box marginTop={1}>
|
|
29
|
+
<Text color="gray">Interactive development environment for GxP plugins</Text>
|
|
30
|
+
</Box>
|
|
31
|
+
|
|
32
|
+
<Box marginTop={2} flexDirection="row" justifyContent="center">
|
|
33
|
+
{/* Quick Start Column */}
|
|
34
|
+
<Box flexDirection="column" marginRight={4}>
|
|
35
|
+
<Text color="cyan" bold>Quick Start</Text>
|
|
36
|
+
<Box marginTop={1} flexDirection="column">
|
|
37
|
+
<Text> <Text color="yellow">/dev</Text> Start Vite dev server</Text>
|
|
38
|
+
<Text> <Text color="yellow">/dev --with-socket</Text> Start Vite + Socket.IO</Text>
|
|
39
|
+
<Text> <Text color="yellow">/dev --no-socket</Text> Start Vite only (no Socket)</Text>
|
|
40
|
+
<Text> <Text color="yellow">/socket</Text> Start Socket.IO server</Text>
|
|
41
|
+
<Text> <Text color="yellow">/ext chrome</Text> Launch Chrome extension</Text>
|
|
42
|
+
<Text> <Text color="yellow">/help</Text> Show all commands</Text>
|
|
43
|
+
</Box>
|
|
44
|
+
</Box>
|
|
45
|
+
|
|
46
|
+
{/* Keyboard Shortcuts Column */}
|
|
47
|
+
<Box flexDirection="column" marginLeft={4}>
|
|
48
|
+
<Text color="cyan" bold>Keyboard Shortcuts</Text>
|
|
49
|
+
<Box marginTop={1} flexDirection="column">
|
|
50
|
+
<Text> <Text color="green">Tab</Text> Cycle through tabs</Text>
|
|
51
|
+
<Text> <Text color="green">Left/Right</Text> Switch tabs</Text>
|
|
52
|
+
<Text> <Text color="green">Ctrl+K</Text> Stop current service</Text>
|
|
53
|
+
<Text> <Text color="green">Ctrl+L</Text> Clear current log</Text>
|
|
54
|
+
<Text> <Text color="green">Ctrl+C</Text> Exit application</Text>
|
|
55
|
+
</Box>
|
|
56
|
+
</Box>
|
|
57
|
+
</Box>
|
|
58
|
+
|
|
59
|
+
<Box marginTop={2} flexDirection="column" alignItems="center">
|
|
60
|
+
<Text color="cyan" bold>Socket Events</Text>
|
|
61
|
+
<Box marginTop={1} flexDirection="column" alignItems="center">
|
|
62
|
+
<Text color="gray">Use <Text color="yellow">/socket list</Text> to see available events</Text>
|
|
63
|
+
<Text color="gray">Use <Text color="yellow">/socket send EventName</Text> to simulate events</Text>
|
|
64
|
+
</Box>
|
|
65
|
+
</Box>
|
|
66
|
+
|
|
67
|
+
<Box marginTop={2} flexDirection="column" alignItems="center">
|
|
68
|
+
<Text color="cyan" bold>Browser Extensions</Text>
|
|
69
|
+
<Box marginTop={1} flexDirection="column" alignItems="center">
|
|
70
|
+
<Text color="gray">Test your plugin on live GxP pages with the browser extension</Text>
|
|
71
|
+
<Text color="gray">Open DevTools and use the "GxP Inspector" panel to select components</Text>
|
|
72
|
+
</Box>
|
|
73
|
+
</Box>
|
|
74
|
+
|
|
75
|
+
<Box marginTop={2}>
|
|
76
|
+
<Text dimColor>Type a command below to get started...</Text>
|
|
77
|
+
</Box>
|
|
78
|
+
</Box>
|
|
79
|
+
);
|
|
80
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { render } from 'ink';
|
|
4
|
+
import dotenv from 'dotenv';
|
|
5
|
+
import path from 'path';
|
|
6
|
+
import App from './App.js';
|
|
7
|
+
import { serviceManager } from './services/index.js';
|
|
8
|
+
|
|
9
|
+
// Load .env from the current working directory (project directory)
|
|
10
|
+
dotenv.config({ path: path.join(process.cwd(), '.env') });
|
|
11
|
+
|
|
12
|
+
interface TUIOptions {
|
|
13
|
+
autoStart?: string[]; // Commands to auto-start (e.g., ['dev', 'socket'])
|
|
14
|
+
args?: Record<string, unknown>; // Command arguments
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function startTUI(options: TUIOptions = {}) {
|
|
18
|
+
// Check if stdin supports raw mode (required for Ink input handling)
|
|
19
|
+
// This can fail when:
|
|
20
|
+
// - Running in CI environments
|
|
21
|
+
// - Piped input (stdin is not a TTY)
|
|
22
|
+
// - Some terminal emulators
|
|
23
|
+
const stdin = process.stdin;
|
|
24
|
+
const stdinIsTTY = stdin.isTTY === true;
|
|
25
|
+
|
|
26
|
+
// If stdin doesn't support raw mode, we need to handle it gracefully
|
|
27
|
+
// Ink 5.x uses stdin by default and requires raw mode for input
|
|
28
|
+
if (!stdinIsTTY) {
|
|
29
|
+
console.error('Warning: Terminal does not support interactive mode.');
|
|
30
|
+
console.error('The TUI requires an interactive terminal (TTY) to function.');
|
|
31
|
+
console.error('');
|
|
32
|
+
console.error('Try running the command directly from your terminal, not from a script or pipe.');
|
|
33
|
+
console.error('');
|
|
34
|
+
console.error('Alternatively, use the non-TUI commands:');
|
|
35
|
+
console.error(' npm run dev # Start Vite dev server');
|
|
36
|
+
console.error(' npm run dev-http # Start HTTP dev server');
|
|
37
|
+
console.error(' gxdev socket list # List socket events');
|
|
38
|
+
process.exit(1);
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const { waitUntilExit } = render(
|
|
43
|
+
<App
|
|
44
|
+
autoStart={options.autoStart}
|
|
45
|
+
args={options.args}
|
|
46
|
+
/>
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
waitUntilExit().then(() => {
|
|
50
|
+
// Ensure all services are stopped before exiting
|
|
51
|
+
serviceManager.forceStopAll();
|
|
52
|
+
process.exit(0);
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Check if run directly (ESM way)
|
|
57
|
+
import { fileURLToPath } from 'url';
|
|
58
|
+
const isMain = process.argv[1] && process.argv[1] === fileURLToPath(import.meta.url);
|
|
59
|
+
if (isMain) {
|
|
60
|
+
startTUI();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export default startTUI;
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { serviceManager, ServiceConfig } from './ServiceManager.js';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
5
|
+
|
|
6
|
+
export type BrowserType = 'chrome' | 'firefox';
|
|
7
|
+
|
|
8
|
+
export interface ExtensionOptions {
|
|
9
|
+
browser: BrowserType;
|
|
10
|
+
cwd?: string;
|
|
11
|
+
useHttps?: boolean;
|
|
12
|
+
port?: number | string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Get the toolkit root directory
|
|
16
|
+
function getToolkitRoot(): string {
|
|
17
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
18
|
+
// Navigate from dist/tui/services to project root (3 levels up)
|
|
19
|
+
// dist/tui/services -> dist/tui -> dist -> gx-devtools
|
|
20
|
+
return path.resolve(__dirname, '..', '..', '..');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Find the extension path (project-local or toolkit built-in)
|
|
24
|
+
function findExtensionPath(browser: BrowserType, cwd: string): string | null {
|
|
25
|
+
// Check local project first
|
|
26
|
+
const localPath = path.join(cwd, 'browser-extensions', browser);
|
|
27
|
+
if (fs.existsSync(localPath)) {
|
|
28
|
+
return localPath;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Fall back to toolkit's built-in extension
|
|
32
|
+
const toolkitPath = path.join(getToolkitRoot(), 'browser-extensions', browser);
|
|
33
|
+
if (fs.existsSync(toolkitPath)) {
|
|
34
|
+
return toolkitPath;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function startExtension(options: ExtensionOptions): void {
|
|
41
|
+
const { browser, useHttps = true, port = 3060 } = options;
|
|
42
|
+
const cwd = options.cwd || process.cwd();
|
|
43
|
+
const serviceId = `ext-${browser}`;
|
|
44
|
+
|
|
45
|
+
// Compute the start URL based on options
|
|
46
|
+
const protocol = useHttps ? 'https' : 'http';
|
|
47
|
+
const startUrl = `${protocol}://localhost:${port}`;
|
|
48
|
+
|
|
49
|
+
// Check if already running
|
|
50
|
+
if (serviceManager.isRunning(serviceId)) {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const extensionPath = findExtensionPath(browser, cwd);
|
|
55
|
+
if (!extensionPath) {
|
|
56
|
+
// Create a dummy service to show the error
|
|
57
|
+
const errorState = serviceManager.start({
|
|
58
|
+
id: serviceId,
|
|
59
|
+
name: `${browser.charAt(0).toUpperCase() + browser.slice(1)} Extension`,
|
|
60
|
+
command: 'echo',
|
|
61
|
+
args: [`Extension not found for ${browser}`],
|
|
62
|
+
cwd,
|
|
63
|
+
});
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (browser === 'firefox') {
|
|
68
|
+
const config: ServiceConfig = {
|
|
69
|
+
id: serviceId,
|
|
70
|
+
name: 'Firefox Extension',
|
|
71
|
+
command: 'npx',
|
|
72
|
+
args: ['web-ext', 'run', '--source-dir', extensionPath, '--start-url', startUrl],
|
|
73
|
+
cwd,
|
|
74
|
+
env: {
|
|
75
|
+
FORCE_COLOR: '1',
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
serviceManager.start(config);
|
|
79
|
+
} else if (browser === 'chrome') {
|
|
80
|
+
// For Chrome, we need to use the launch script
|
|
81
|
+
const toolkitRoot = getToolkitRoot();
|
|
82
|
+
let scriptPath = path.join(cwd, 'scripts', 'launch-chrome.js');
|
|
83
|
+
|
|
84
|
+
if (!fs.existsSync(scriptPath)) {
|
|
85
|
+
scriptPath = path.join(toolkitRoot, 'scripts', 'launch-chrome.js');
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (!fs.existsSync(scriptPath)) {
|
|
89
|
+
const errorState = serviceManager.start({
|
|
90
|
+
id: serviceId,
|
|
91
|
+
name: 'Chrome Extension',
|
|
92
|
+
command: 'echo',
|
|
93
|
+
args: ['Chrome launcher script not found'],
|
|
94
|
+
cwd,
|
|
95
|
+
});
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const config: ServiceConfig = {
|
|
100
|
+
id: serviceId,
|
|
101
|
+
name: 'Chrome Extension',
|
|
102
|
+
command: 'node',
|
|
103
|
+
args: [scriptPath],
|
|
104
|
+
cwd,
|
|
105
|
+
env: {
|
|
106
|
+
FORCE_COLOR: '1',
|
|
107
|
+
CHROME_EXTENSION_PATH: extensionPath,
|
|
108
|
+
USE_HTTPS: String(useHttps),
|
|
109
|
+
NODE_PORT: String(port),
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
serviceManager.start(config);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function stopExtension(browser: BrowserType): boolean {
|
|
117
|
+
return serviceManager.stop(`ext-${browser}`);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function isExtensionRunning(browser: BrowserType): boolean {
|
|
121
|
+
return serviceManager.isRunning(`ext-${browser}`);
|
|
122
|
+
}
|
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import http from 'http';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
5
|
+
import open from 'open';
|
|
6
|
+
import { EventEmitter } from 'events';
|
|
7
|
+
|
|
8
|
+
// Gemini configuration interface
|
|
9
|
+
export interface GeminiConfig {
|
|
10
|
+
systemPrompt?: string;
|
|
11
|
+
includeDocs?: string[];
|
|
12
|
+
projectContext?: boolean;
|
|
13
|
+
maxContextTokens?: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Auth tokens interface
|
|
17
|
+
interface AuthTokens {
|
|
18
|
+
accessToken: string;
|
|
19
|
+
refreshToken: string;
|
|
20
|
+
expiresAt: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Get the gxdev config directory
|
|
24
|
+
function getConfigDir(): string {
|
|
25
|
+
const home = process.env.HOME || process.env.USERPROFILE || '';
|
|
26
|
+
return path.join(home, '.gxdev');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Ensure config directory exists
|
|
30
|
+
function ensureConfigDir(): void {
|
|
31
|
+
const configDir = getConfigDir();
|
|
32
|
+
if (!fs.existsSync(configDir)) {
|
|
33
|
+
fs.mkdirSync(configDir, { recursive: true });
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Get auth file path
|
|
38
|
+
function getAuthFilePath(): string {
|
|
39
|
+
return path.join(getConfigDir(), 'gemini-auth.json');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Get config file path
|
|
43
|
+
function getConfigFilePath(): string {
|
|
44
|
+
return path.join(getConfigDir(), 'gemini-config.json');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Get docs directory path
|
|
48
|
+
function getDocsDir(): string {
|
|
49
|
+
return path.join(getConfigDir(), 'gemini-docs');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Load saved auth tokens
|
|
53
|
+
export function loadAuthTokens(): AuthTokens | null {
|
|
54
|
+
try {
|
|
55
|
+
const authPath = getAuthFilePath();
|
|
56
|
+
if (fs.existsSync(authPath)) {
|
|
57
|
+
const content = fs.readFileSync(authPath, 'utf-8');
|
|
58
|
+
return JSON.parse(content);
|
|
59
|
+
}
|
|
60
|
+
} catch {
|
|
61
|
+
// Invalid or missing auth file
|
|
62
|
+
}
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Save auth tokens
|
|
67
|
+
function saveAuthTokens(tokens: AuthTokens): void {
|
|
68
|
+
ensureConfigDir();
|
|
69
|
+
const authPath = getAuthFilePath();
|
|
70
|
+
fs.writeFileSync(authPath, JSON.stringify(tokens, null, 2));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Clear auth tokens
|
|
74
|
+
export function clearAuthTokens(): void {
|
|
75
|
+
const authPath = getAuthFilePath();
|
|
76
|
+
if (fs.existsSync(authPath)) {
|
|
77
|
+
fs.unlinkSync(authPath);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Load Gemini config
|
|
82
|
+
export function loadGeminiConfig(): GeminiConfig {
|
|
83
|
+
try {
|
|
84
|
+
const configPath = getConfigFilePath();
|
|
85
|
+
if (fs.existsSync(configPath)) {
|
|
86
|
+
const content = fs.readFileSync(configPath, 'utf-8');
|
|
87
|
+
return JSON.parse(content);
|
|
88
|
+
}
|
|
89
|
+
} catch {
|
|
90
|
+
// Invalid or missing config file
|
|
91
|
+
}
|
|
92
|
+
return {
|
|
93
|
+
systemPrompt: 'You are a helpful assistant for GxP plugin development.',
|
|
94
|
+
includeDocs: [],
|
|
95
|
+
projectContext: true,
|
|
96
|
+
maxContextTokens: 4000,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Save Gemini config
|
|
101
|
+
export function saveGeminiConfig(config: GeminiConfig): void {
|
|
102
|
+
ensureConfigDir();
|
|
103
|
+
const configPath = getConfigFilePath();
|
|
104
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Check if authenticated
|
|
108
|
+
export function isAuthenticated(): boolean {
|
|
109
|
+
const tokens = loadAuthTokens();
|
|
110
|
+
if (!tokens) return false;
|
|
111
|
+
// Check if token is expired (with 5 min buffer)
|
|
112
|
+
return tokens.expiresAt > Date.now() + 5 * 60 * 1000;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Gemini Service class
|
|
116
|
+
export class GeminiService extends EventEmitter {
|
|
117
|
+
private conversationHistory: Array<{ role: string; content: string }> = [];
|
|
118
|
+
private projectContext: string = '';
|
|
119
|
+
|
|
120
|
+
constructor() {
|
|
121
|
+
super();
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Start OAuth flow
|
|
125
|
+
async startOAuthFlow(): Promise<{ success: boolean; message: string }> {
|
|
126
|
+
return new Promise((resolve) => {
|
|
127
|
+
// Create local server for OAuth callback
|
|
128
|
+
const PORT = 8234;
|
|
129
|
+
let server: http.Server;
|
|
130
|
+
|
|
131
|
+
const handleCallback = (req: http.IncomingMessage, res: http.ServerResponse) => {
|
|
132
|
+
const url = new URL(req.url || '', `http://localhost:${PORT}`);
|
|
133
|
+
|
|
134
|
+
if (url.pathname === '/callback') {
|
|
135
|
+
const code = url.searchParams.get('code');
|
|
136
|
+
const error = url.searchParams.get('error');
|
|
137
|
+
|
|
138
|
+
if (error) {
|
|
139
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
140
|
+
res.end('<html><body><h1>Authentication Failed</h1><p>You can close this window.</p></body></html>');
|
|
141
|
+
server.close();
|
|
142
|
+
resolve({ success: false, message: `OAuth error: ${error}` });
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (code) {
|
|
147
|
+
// Exchange code for tokens
|
|
148
|
+
this.exchangeCodeForTokens(code)
|
|
149
|
+
.then((tokens) => {
|
|
150
|
+
saveAuthTokens(tokens);
|
|
151
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
152
|
+
res.end('<html><body><h1>Authentication Successful!</h1><p>You can close this window and return to gxdev.</p></body></html>');
|
|
153
|
+
server.close();
|
|
154
|
+
resolve({ success: true, message: 'Successfully authenticated with Google.' });
|
|
155
|
+
})
|
|
156
|
+
.catch((err) => {
|
|
157
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
158
|
+
res.end('<html><body><h1>Authentication Failed</h1><p>Error exchanging code for tokens.</p></body></html>');
|
|
159
|
+
server.close();
|
|
160
|
+
resolve({ success: false, message: `Token exchange error: ${err.message}` });
|
|
161
|
+
});
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
res.writeHead(404);
|
|
167
|
+
res.end('Not found');
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
server = http.createServer(handleCallback);
|
|
171
|
+
|
|
172
|
+
server.listen(PORT, () => {
|
|
173
|
+
// Construct OAuth URL
|
|
174
|
+
// Note: These are placeholder values - you'll need to set up actual Google Cloud credentials
|
|
175
|
+
const clientId = process.env.GOOGLE_CLIENT_ID || 'YOUR_CLIENT_ID';
|
|
176
|
+
const redirectUri = `http://localhost:${PORT}/callback`;
|
|
177
|
+
const scope = 'https://www.googleapis.com/auth/generative-language';
|
|
178
|
+
|
|
179
|
+
const authUrl = `https://accounts.google.com/o/oauth2/v2/auth?` +
|
|
180
|
+
`client_id=${encodeURIComponent(clientId)}` +
|
|
181
|
+
`&redirect_uri=${encodeURIComponent(redirectUri)}` +
|
|
182
|
+
`&response_type=code` +
|
|
183
|
+
`&scope=${encodeURIComponent(scope)}` +
|
|
184
|
+
`&access_type=offline` +
|
|
185
|
+
`&prompt=consent`;
|
|
186
|
+
|
|
187
|
+
this.emit('log', `Opening browser for Google authentication...`);
|
|
188
|
+
this.emit('log', `If browser doesn't open, visit: ${authUrl}`);
|
|
189
|
+
|
|
190
|
+
open(authUrl).catch(() => {
|
|
191
|
+
this.emit('log', `Could not open browser automatically. Please visit the URL above.`);
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
// Timeout after 5 minutes
|
|
196
|
+
setTimeout(() => {
|
|
197
|
+
server.close();
|
|
198
|
+
resolve({ success: false, message: 'Authentication timed out. Please try again.' });
|
|
199
|
+
}, 5 * 60 * 1000);
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Exchange authorization code for tokens
|
|
204
|
+
private async exchangeCodeForTokens(code: string): Promise<AuthTokens> {
|
|
205
|
+
const clientId = process.env.GOOGLE_CLIENT_ID || 'YOUR_CLIENT_ID';
|
|
206
|
+
const clientSecret = process.env.GOOGLE_CLIENT_SECRET || 'YOUR_CLIENT_SECRET';
|
|
207
|
+
const redirectUri = 'http://localhost:8234/callback';
|
|
208
|
+
|
|
209
|
+
const response = await fetch('https://oauth2.googleapis.com/token', {
|
|
210
|
+
method: 'POST',
|
|
211
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
212
|
+
body: new URLSearchParams({
|
|
213
|
+
code,
|
|
214
|
+
client_id: clientId,
|
|
215
|
+
client_secret: clientSecret,
|
|
216
|
+
redirect_uri: redirectUri,
|
|
217
|
+
grant_type: 'authorization_code',
|
|
218
|
+
}),
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
if (!response.ok) {
|
|
222
|
+
throw new Error(`Token exchange failed: ${response.statusText}`);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const data = await response.json() as any;
|
|
226
|
+
return {
|
|
227
|
+
accessToken: data.access_token,
|
|
228
|
+
refreshToken: data.refresh_token,
|
|
229
|
+
expiresAt: Date.now() + (data.expires_in * 1000),
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Refresh access token
|
|
234
|
+
private async refreshAccessToken(): Promise<void> {
|
|
235
|
+
const tokens = loadAuthTokens();
|
|
236
|
+
if (!tokens?.refreshToken) {
|
|
237
|
+
throw new Error('No refresh token available');
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const clientId = process.env.GOOGLE_CLIENT_ID || 'YOUR_CLIENT_ID';
|
|
241
|
+
const clientSecret = process.env.GOOGLE_CLIENT_SECRET || 'YOUR_CLIENT_SECRET';
|
|
242
|
+
|
|
243
|
+
const response = await fetch('https://oauth2.googleapis.com/token', {
|
|
244
|
+
method: 'POST',
|
|
245
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
246
|
+
body: new URLSearchParams({
|
|
247
|
+
refresh_token: tokens.refreshToken,
|
|
248
|
+
client_id: clientId,
|
|
249
|
+
client_secret: clientSecret,
|
|
250
|
+
grant_type: 'refresh_token',
|
|
251
|
+
}),
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
if (!response.ok) {
|
|
255
|
+
throw new Error('Failed to refresh token');
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const data = await response.json() as any;
|
|
259
|
+
saveAuthTokens({
|
|
260
|
+
accessToken: data.access_token,
|
|
261
|
+
refreshToken: tokens.refreshToken,
|
|
262
|
+
expiresAt: Date.now() + (data.expires_in * 1000),
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Get valid access token (refresh if needed)
|
|
267
|
+
private async getAccessToken(): Promise<string> {
|
|
268
|
+
const tokens = loadAuthTokens();
|
|
269
|
+
if (!tokens) {
|
|
270
|
+
throw new Error('Not authenticated. Run /gemini enable first.');
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Check if token needs refresh (with 5 min buffer)
|
|
274
|
+
if (tokens.expiresAt < Date.now() + 5 * 60 * 1000) {
|
|
275
|
+
await this.refreshAccessToken();
|
|
276
|
+
const newTokens = loadAuthTokens();
|
|
277
|
+
return newTokens!.accessToken;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return tokens.accessToken;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Load project context
|
|
284
|
+
loadProjectContext(cwd: string): void {
|
|
285
|
+
const files = ['CLAUDE.md', 'README.md', 'package.json'];
|
|
286
|
+
const contextParts: string[] = [];
|
|
287
|
+
|
|
288
|
+
for (const file of files) {
|
|
289
|
+
const filePath = path.join(cwd, file);
|
|
290
|
+
if (fs.existsSync(filePath)) {
|
|
291
|
+
try {
|
|
292
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
293
|
+
contextParts.push(`=== ${file} ===\n${content.slice(0, 2000)}`);
|
|
294
|
+
} catch {
|
|
295
|
+
// Skip unreadable files
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
this.projectContext = contextParts.join('\n\n');
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Load custom docs
|
|
304
|
+
loadCustomDocs(): string {
|
|
305
|
+
const docsDir = getDocsDir();
|
|
306
|
+
if (!fs.existsSync(docsDir)) {
|
|
307
|
+
return '';
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const docParts: string[] = [];
|
|
311
|
+
try {
|
|
312
|
+
const files = fs.readdirSync(docsDir).filter(f => f.endsWith('.md'));
|
|
313
|
+
for (const file of files.slice(0, 5)) { // Limit to 5 docs
|
|
314
|
+
const content = fs.readFileSync(path.join(docsDir, file), 'utf-8');
|
|
315
|
+
docParts.push(`=== ${file} ===\n${content.slice(0, 2000)}`);
|
|
316
|
+
}
|
|
317
|
+
} catch {
|
|
318
|
+
// Skip on error
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
return docParts.join('\n\n');
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// Send message to Gemini
|
|
325
|
+
async sendMessage(message: string): Promise<string> {
|
|
326
|
+
const config = loadGeminiConfig();
|
|
327
|
+
const accessToken = await this.getAccessToken();
|
|
328
|
+
|
|
329
|
+
// Build context
|
|
330
|
+
let systemContext = config.systemPrompt || '';
|
|
331
|
+
if (config.projectContext && this.projectContext) {
|
|
332
|
+
systemContext += '\n\nProject Context:\n' + this.projectContext;
|
|
333
|
+
}
|
|
334
|
+
const customDocs = this.loadCustomDocs();
|
|
335
|
+
if (customDocs) {
|
|
336
|
+
systemContext += '\n\nDocumentation:\n' + customDocs;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// Add to conversation history
|
|
340
|
+
this.conversationHistory.push({ role: 'user', content: message });
|
|
341
|
+
|
|
342
|
+
// Prepare request body for Gemini API
|
|
343
|
+
const requestBody = {
|
|
344
|
+
contents: [
|
|
345
|
+
{
|
|
346
|
+
role: 'user',
|
|
347
|
+
parts: [{ text: systemContext + '\n\nUser: ' + message }]
|
|
348
|
+
}
|
|
349
|
+
],
|
|
350
|
+
generationConfig: {
|
|
351
|
+
maxOutputTokens: 2048,
|
|
352
|
+
temperature: 0.7,
|
|
353
|
+
}
|
|
354
|
+
};
|
|
355
|
+
|
|
356
|
+
// Call Gemini API
|
|
357
|
+
const response = await fetch(
|
|
358
|
+
'https://generativelanguage.googleapis.com/v1beta/models/gemini-pro:generateContent',
|
|
359
|
+
{
|
|
360
|
+
method: 'POST',
|
|
361
|
+
headers: {
|
|
362
|
+
'Authorization': `Bearer ${accessToken}`,
|
|
363
|
+
'Content-Type': 'application/json',
|
|
364
|
+
},
|
|
365
|
+
body: JSON.stringify(requestBody),
|
|
366
|
+
}
|
|
367
|
+
);
|
|
368
|
+
|
|
369
|
+
if (!response.ok) {
|
|
370
|
+
const errorText = await response.text();
|
|
371
|
+
throw new Error(`Gemini API error: ${response.status} - ${errorText}`);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
const data = await response.json() as any;
|
|
375
|
+
const responseText = data.candidates?.[0]?.content?.parts?.[0]?.text || 'No response generated.';
|
|
376
|
+
|
|
377
|
+
// Add to conversation history
|
|
378
|
+
this.conversationHistory.push({ role: 'assistant', content: responseText });
|
|
379
|
+
|
|
380
|
+
return responseText;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// Clear conversation history
|
|
384
|
+
clearConversation(): void {
|
|
385
|
+
this.conversationHistory = [];
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// Get conversation history
|
|
389
|
+
getConversationHistory(): Array<{ role: string; content: string }> {
|
|
390
|
+
return [...this.conversationHistory];
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// Singleton instance
|
|
395
|
+
export const geminiService = new GeminiService();
|