Warn if `npm run start` is out of date. (#20)

* Adding some wiring to allow the Ink app to warn if there are local development changes that haven't been captured in the recent build of the Gemini CLI.

* Adding a new useAppEffects.ts file that wores some useEffect handlers in.

* Updating package-lock.json to resolve `npm ci` issues.

* Updating package-lock.json and package.json to resolve `npm ci` issues.
This commit is contained in:
Evan Senter 2025-04-18 21:55:02 +01:00 committed by GitHub
parent 3ed61f1ff2
commit 482aeaff10
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 226 additions and 27 deletions

10
package-lock.json generated
View File

@ -257,6 +257,10 @@
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
}
},
"node_modules/@google/gemini-code": {
"resolved": "packages/cli",
"link": true
},
"node_modules/@google/genai": {
"version": "0.8.0",
"resolved": "https://registry.npmjs.org/@google/genai/-/genai-0.8.0.tgz",
@ -2352,10 +2356,6 @@
"node": ">=14"
}
},
"node_modules/gemini-code-cli": {
"resolved": "packages/cli",
"link": true
},
"node_modules/get-caller-file": {
"version": "2.0.5",
"license": "ISC",
@ -5194,7 +5194,7 @@
"license": "MIT"
},
"packages/cli": {
"name": "gemini-code-cli",
"name": "@google/gemini-code",
"version": "1.0.0",
"dependencies": {
"@google/genai": "^0.8.0",

View File

@ -9,10 +9,9 @@
"scripts": {
"build": "npm run build --workspaces",
"test": "npm run test --workspaces",
"start": "node ./scripts/check-build-status.js && npm run start --workspace=@google/gemini-code -- \"$@\"",
"lint": "eslint . --ext .ts,.tsx",
"typecheck": "tsc --noEmit --jsx react",
"start": "npm run start --workspace=@google/gemini-code -- \"$@\"",
"format": "prettier --write .",
"artifactregistry-login": "npx google-artifactregistry-auth"
},
"devDependencies": {

View File

@ -5,8 +5,7 @@
"type": "module",
"main": "gemini.js",
"scripts": {
"build": "tsc && cp package.json README.md ../../LICENSE dist/",
"clean": "rm -rf dist",
"build": "tsc && cp package.json README.md ../../LICENSE dist/ && touch dist/.last_build",
"start": "node dist/gemini.js",
"debug": "node --inspect-brk dist/gemini.js",
"lint": "eslint . --ext .ts,.tsx",

View File

@ -1,5 +1,8 @@
import React, { useState, useEffect } from 'react';
import { Box, Text } from 'ink';
import fs from 'fs';
import path from 'path';
import os from 'os';
import type { HistoryItem } from './types.js';
import { useGeminiStream } from './hooks/useGeminiStream.js';
import { useLoadingIndicator } from './hooks/useLoadingIndicator.js';
@ -11,6 +14,12 @@ import InputPrompt from './components/InputPrompt.js';
import Footer from './components/Footer.js';
import { StreamingState } from '../core/gemini-stream.js';
import { PartListUnion } from '@google/genai';
import {
useStartupWarnings,
useInitializationErrorEffect,
} from './hooks/useAppEffects.js';
const warningsFilePath = path.join(os.tmpdir(), 'gemini-code-cli-warnings.txt');
interface AppProps {
directory: string;
@ -19,11 +28,15 @@ interface AppProps {
const App = ({ directory }: AppProps) => {
const [query, setQuery] = useState('');
const [history, setHistory] = useState<HistoryItem[]>([]);
const [startupWarnings, setStartupWarnings] = useState<string[]>([]);
const { streamingState, submitQuery, initError } =
useGeminiStream(setHistory);
const { elapsedTime, currentLoadingPhrase } =
useLoadingIndicator(streamingState);
useStartupWarnings(setStartupWarnings);
useInitializationErrorEffect(initError, history, setHistory);
const handleInputSubmit = (value: PartListUnion) => {
submitQuery(value)
.then(() => {
@ -34,24 +47,6 @@ const App = ({ directory }: AppProps) => {
});
};
useEffect(() => {
if (
initError &&
!history.some(
(item) => item.type === 'error' && item.text?.includes(initError),
)
) {
setHistory((prev) => [
...prev,
{
id: Date.now(),
type: 'error',
text: `Initialization Error: ${initError}. Please check API key and configuration.`,
} as HistoryItem,
]);
}
}, [initError, history]);
const isWaitingForToolConfirmation = history.some(
(item) =>
item.type === 'tool_group' &&
@ -63,6 +58,22 @@ const App = ({ directory }: AppProps) => {
<Box flexDirection="column" padding={1} marginBottom={1} width="100%">
<Header cwd={directory} />
{startupWarnings.length > 0 && (
<Box
borderStyle="round"
borderColor="yellow"
paddingX={1}
marginY={1}
flexDirection="column"
>
{startupWarnings.map((warning, index) => (
<Text key={index} color="yellow">
{warning}
</Text>
))}
</Box>
)}
<Tips />
{initError &&

View File

@ -0,0 +1,61 @@
import { useEffect } from 'react';
import fs from 'fs';
import path from 'path';
import os from 'os';
import type { HistoryItem } from '../types.js';
const warningsFilePath = path.join(os.tmpdir(), 'gemini-code-cli-warnings.txt');
// Effect to handle startup warnings
export function useStartupWarnings(
setStartupWarnings: React.Dispatch<React.SetStateAction<string[]>>,
) {
useEffect(() => {
try {
if (fs.existsSync(warningsFilePath)) {
const warningsContent = fs.readFileSync(warningsFilePath, 'utf-8');
setStartupWarnings(
warningsContent.split('\n').filter((line) => line.trim() !== ''),
);
try {
fs.unlinkSync(warningsFilePath);
} catch (unlinkErr: any) {
setStartupWarnings((prev) => [
...prev,
`Warning: Could not delete temporary warnings file.`,
]);
}
}
} catch (err: any) {
setStartupWarnings((prev) => [
...prev,
`Error checking/reading warnings file: ${err.message}`,
]);
}
}, [setStartupWarnings]); // Include setStartupWarnings in dependency array
}
// Effect to handle initialization errors
export function useInitializationErrorEffect(
initError: string | null,
history: HistoryItem[],
setHistory: React.Dispatch<React.SetStateAction<HistoryItem[]>>,
) {
useEffect(() => {
if (
initError &&
!history.some(
(item) => item.type === 'error' && item.text?.includes(initError),
)
) {
setHistory((prev) => [
...prev,
{
id: Date.now(),
type: 'error',
text: `Initialization Error: ${initError}. Please check API key and configuration.`,
} as HistoryItem,
]);
}
}, [initError, history, setHistory]); // Include setHistory in dependency array
}

View File

@ -0,0 +1,129 @@
import fs from 'fs';
import path from 'path';
import os from 'os'; // Import os module
// --- Configuration ---
const cliPackageDir = path.resolve('packages', 'cli'); // Base directory for the CLI package
const buildTimestampPath = path.join(cliPackageDir, 'dist', '.last_build'); // Path to the timestamp file within the CLI package
const sourceDirs = [path.join(cliPackageDir, 'src')]; // Source directory within the CLI package
const filesToWatch = [path.join(cliPackageDir, 'package.json'), path.join(cliPackageDir, 'tsconfig.json')]; // Specific files within the CLI package
const buildDir = path.join(cliPackageDir, 'dist'); // Build output directory within the CLI package
const warningsFilePath = path.join(os.tmpdir(), 'gemini-code-cli-warnings.txt'); // Temp file for warnings
// ---------------------
function getMtime(filePath) {
try {
return fs.statSync(filePath).mtimeMs; // Use mtimeMs for higher precision
} catch (err) {
if (err.code === 'ENOENT') {
return null; // File doesn't exist
}
console.error(`Error getting stats for ${filePath}:`, err);
process.exit(1); // Exit on unexpected errors getting stats
}
}
function findSourceFiles(dir, allFiles = []) {
const entries = fs.readdirSync(dir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(dir, entry.name);
// Simple check to avoid recursing into node_modules or build dir itself
if (entry.isDirectory() && entry.name !== 'node_modules' && fullPath !== buildDir) {
findSourceFiles(fullPath, allFiles);
} else if (entry.isFile()) {
allFiles.push(fullPath);
}
}
return allFiles;
}
console.log('Checking build status...');
// Clean up old warnings file before check
try {
if (fs.existsSync(warningsFilePath)) {
fs.unlinkSync(warningsFilePath);
}
} catch (err) {
console.warn(`[Check Script] Warning: Could not delete previous warnings file: ${err.message}`);
}
const buildMtime = getMtime(buildTimestampPath);
if (!buildMtime) {
// If build is missing, write that as a warning and exit(0) so app can display it
const errorMessage = `ERROR: Build timestamp file (${path.relative(process.cwd(), buildTimestampPath)}) not found. Run \`npm run build\` first.`;
console.error(errorMessage); // Still log error here
try {
fs.writeFileSync(warningsFilePath, errorMessage);
} catch (writeErr) {
console.error(`[Check Script] Error writing missing build warning file: ${writeErr.message}`);
}
process.exit(0); // Allow app to start and show the error
}
let newerSourceFileFound = false;
const warningMessages = []; // Collect warnings here
const allSourceFiles = [];
// Collect files from specified directories
sourceDirs.forEach((dir) => {
const dirPath = path.resolve(dir);
if (fs.existsSync(dirPath)) {
findSourceFiles(dirPath, allSourceFiles);
} else {
console.warn(`Warning: Source directory "${dir}" not found.`);
}
});
// Add specific files
filesToWatch.forEach((file) => {
const filePath = path.resolve(file);
if (fs.existsSync(filePath)) {
allSourceFiles.push(filePath);
} else {
console.warn(`Warning: Watched file "${file}" not found.`);
}
});
// Check modification times
for (const file of allSourceFiles) {
const sourceMtime = getMtime(file);
const relativePath = path.relative(process.cwd(), file);
const isNewer = sourceMtime && sourceMtime > buildMtime;
if (isNewer) {
const warning = `Warning: Source file "${relativePath}" has been modified since the last build.`;
console.warn(warning); // Keep console warning for script debugging
warningMessages.push(warning);
newerSourceFileFound = true;
// break; // Uncomment to stop checking after the first newer file
}
}
if (newerSourceFileFound) {
const finalWarning = '\nRun "npm run build" to incorporate changes before starting.';
warningMessages.push(finalWarning);
console.warn(finalWarning);
// Write warnings to the temp file
try {
fs.writeFileSync(warningsFilePath, warningMessages.join('\n'));
// Removed debug log
} catch (err) {
console.error(`[Check Script] Error writing warnings file: ${err.message}`);
// Proceed without writing, app won't show warnings
}
} else {
console.log('Build is up-to-date.');
// Ensure no stale warning file exists if build is ok
try {
if (fs.existsSync(warningsFilePath)) {
fs.unlinkSync(warningsFilePath);
}
} catch (err) {
console.warn(`[Check Script] Warning: Could not delete previous warnings file: ${err.message}`);
}
}
process.exit(0); // Always exit successfully so the app starts