mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-08-01 12:41:00 -07:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ce18470921 | |||
| f8dbc8084a | |||
| 115f8bce8b |
@@ -1,22 +1,97 @@
|
||||
---
|
||||
name: docs-writer
|
||||
description: Expert technical documentation suite for Gemini CLI. Use when asked to write, review, or edit .md files. Supports a "Deep content update workflow" for comprehensive audits and a "Report-only mode" for documentation analysis without making changes.
|
||||
description:
|
||||
Always use this skill when the task involves writing, reviewing, or editing
|
||||
files in the `/docs` directory or any `.md` files in the repository.
|
||||
---
|
||||
|
||||
# `docs-writer` skill instructions
|
||||
|
||||
As an expert technical writer and editor for the Gemini CLI project, you produce
|
||||
accurate, clear, and consistent documentation. You must adhere to the
|
||||
contribution process in `CONTRIBUTING.md` and the
|
||||
[style-guide.md](references/style-guide.md).
|
||||
accurate, clear, and consistent documentation. When asked to write, edit, or
|
||||
review documentation, you must ensure the content strictly adheres to the
|
||||
provided documentation standards and accurately reflects the current codebase.
|
||||
Adhere to the contribution process in `CONTRIBUTING.md` and the following
|
||||
project standards.
|
||||
|
||||
---
|
||||
## Phase 1: Documentation standards
|
||||
|
||||
## Standard workflow: writing and reviewing
|
||||
Use this workflow for standard requests to write new content or review/edit
|
||||
existing documentation.
|
||||
Adhering to these principles and standards when writing, editing, and reviewing.
|
||||
|
||||
### Phase 1: preparation
|
||||
### Voice and tone
|
||||
Adopt a tone that balances professionalism with a helpful, conversational
|
||||
approach.
|
||||
|
||||
- **Perspective and tense:** Address the reader as "you." Use active voice and
|
||||
present tense (e.g., "The API returns...").
|
||||
- **Tone:** Professional, friendly, and direct.
|
||||
- **Clarity:** Use simple vocabulary. Avoid jargon, slang, and marketing hype.
|
||||
- **Global Audience:** Write in standard US English. Avoid idioms and cultural
|
||||
references.
|
||||
- **Requirements:** Be clear about requirements ("must") vs. recommendations
|
||||
("we recommend"). Avoid "should."
|
||||
- **Word Choice:** Avoid "please" and anthropomorphism (e.g., "the server
|
||||
thinks"). Use contractions (don't, it's).
|
||||
|
||||
### Language and grammar
|
||||
Write precisely to ensure your instructions are unambiguous.
|
||||
|
||||
- **Abbreviations:** Avoid Latin abbreviations; use "for example" (not "e.g.")
|
||||
and "that is" (not "i.e.").
|
||||
- **Punctuation:** Use the serial comma. Place periods and commas inside
|
||||
quotation marks.
|
||||
- **Dates:** Use unambiguous formats (e.g., "January 22, 2026").
|
||||
- **Conciseness:** Use "lets you" instead of "allows you to." Use precise,
|
||||
specific verbs.
|
||||
- **Examples:** Use meaningful names in examples; avoid placeholders like
|
||||
"foo" or "bar."
|
||||
- **Quota and limit terminology:** For any content involving resource capacity
|
||||
or using the word "quota" or "limit", strictly adhere to the guidelines in
|
||||
the `quota-limit-style-guide.md` resource file. Generally, Use "quota" for the
|
||||
administrative bucket and "limit" for the numerical ceiling.
|
||||
|
||||
### Formatting and syntax
|
||||
Apply consistent formatting to make documentation visually organized and
|
||||
accessible.
|
||||
|
||||
- **Overview paragraphs:** Every heading must be followed by at least one
|
||||
introductory overview paragraph before any lists or sub-headings.
|
||||
- **Text wrap:** Wrap text at 80 characters (except long links or tables).
|
||||
- **Casing:** Use sentence case for headings, titles, and bolded text.
|
||||
- **Naming:** Always refer to the project as `Gemini CLI` (never
|
||||
`the Gemini CLI`).
|
||||
- **Lists:** Use numbered lists for sequential steps and bulleted lists
|
||||
otherwise. Keep list items parallel in structure.
|
||||
- **UI and code:** Use **bold** for UI elements and `code font` for filenames,
|
||||
snippets, commands, and API elements. Focus on the task when discussing
|
||||
interaction.
|
||||
- **Links:** Use descriptive anchor text; avoid "click here." Ensure the link
|
||||
makes sense out of context.
|
||||
- **Accessibility:** Use semantic HTML elements correctly (headings, lists,
|
||||
tables).
|
||||
- **Media:** Use lowercase hyphenated filenames. Provide descriptive alt text
|
||||
for all images.
|
||||
|
||||
### Structure
|
||||
- **BLUF:** Start with an introduction explaining what to expect.
|
||||
- **Experimental features:** If a feature is clearly noted as experimental,
|
||||
add the following note immediately after the introductory paragraph:
|
||||
`> **Note:** This is a preview feature currently under active development.`
|
||||
- **Headings:** Use hierarchical headings to support the user journey.
|
||||
- **Procedures:**
|
||||
- Introduce lists of steps with a complete sentence.
|
||||
- Start each step with an imperative verb.
|
||||
- Number sequential steps; use bullets for non-sequential lists.
|
||||
- Put conditions before instructions (e.g., "On the Settings page, click...").
|
||||
- Provide clear context for where the action takes place.
|
||||
- Indicate optional steps clearly (e.g., "Optional: ...").
|
||||
- **Elements:** Use bullet lists, tables, notes (`> **Note:**`), and warnings
|
||||
(`> **Warning:**`).
|
||||
- **Avoid using a table of contents:** If a table of contents is present, remove
|
||||
it.
|
||||
- **Next steps:** Conclude with a "Next steps" section if applicable.
|
||||
|
||||
## Phase 2: Preparation
|
||||
Before modifying any documentation, thoroughly investigate the request and the
|
||||
surrounding context.
|
||||
|
||||
@@ -30,17 +105,20 @@ surrounding context.
|
||||
`docs/sidebar.json` needs updates.
|
||||
5. **Plan:** Create a step-by-step plan before making changes.
|
||||
|
||||
### Phase 2: execution
|
||||
## Phase 3: Execution
|
||||
Implement your plan by either updating existing files or creating new ones
|
||||
using the appropriate file system tools. Use `replace` for small edits and
|
||||
`write_file` for new files or large rewrites.
|
||||
|
||||
#### Editing existing documentation
|
||||
### Editing existing documentation
|
||||
Follow these additional steps when asked to review or update existing
|
||||
documentation.
|
||||
|
||||
- **Gaps:** Identify areas where the documentation is incomplete or no longer
|
||||
reflects existing code.
|
||||
- **Structure:** Apply style guide rules (BLUF, headings, etc.) when
|
||||
- **Structure:** Apply "Structure (New Docs)" rules (BLUF, headings, etc.) when
|
||||
adding new sections to existing pages.
|
||||
- **Headers:** If you change a header, you must check for links that lead to
|
||||
- **Headers**: If you change a header, you must check for links that lead to
|
||||
that header and update them.
|
||||
- **Tone:** Ensure the tone is active and engaging. Use "you" and contractions.
|
||||
- **Clarity:** Correct awkward wording, spelling, and grammar. Rephrase
|
||||
@@ -48,22 +126,17 @@ using the appropriate file system tools. Use `replace` for small edits and
|
||||
- **Consistency:** Check for consistent terminology and style across all edited
|
||||
documents.
|
||||
|
||||
### Phase 3: verification and finalization
|
||||
|
||||
## Phase 4: Verification and finalization
|
||||
Perform a final quality check to ensure that all changes are correctly formatted
|
||||
and that all links are functional.
|
||||
|
||||
1. **Accuracy:** Ensure content accurately reflects the implementation and
|
||||
technical behavior.
|
||||
2. **Self-review:** Re-read changes for formatting, correctness, and flow.
|
||||
3. **Link check:** Verify all new and existing links leading to or from modified
|
||||
pages.
|
||||
4. **Format:** Once all changes are complete, ask to execute `npm run format`.
|
||||
|
||||
---
|
||||
|
||||
## Feature: deep content update workflow
|
||||
When specifically asked for a **"deep audit," "comprehensive update,"**
|
||||
**"interactively audit,"** or a **"report-only audit"** of the docset, you MUST
|
||||
follow the multi-role procedural guidance in
|
||||
[docs-auditing.md](references/docs-auditing.md).
|
||||
|
||||
- **Standard audit:** Iterate through strategist, engineer, writer, and editor.
|
||||
- **Report-only mode:** Iterate through strategist and engineer only, and
|
||||
output a final `audit-report-YYYY-MM-DD.md`.
|
||||
pages. If you changed a header, ensure that any links that lead to it are
|
||||
updated.
|
||||
4. **Format:** Once all changes are complete, ask to execute `npm run format`
|
||||
to ensure consistent formatting across the project. If the user confirms,
|
||||
execute the command.
|
||||
|
||||
@@ -1,109 +0,0 @@
|
||||
# Deep content update workflow: docs auditing
|
||||
|
||||
Perform this workflow when asked for a "deep audit," "comprehensive update," or
|
||||
to "interactively audit" the docset. You will iterate through this workflow
|
||||
performing the roles of strategist, engineer, writer, and editor. You will
|
||||
view the documentation set found at `/docs/`.
|
||||
|
||||
**Report-only mode:** If asked for a "report-only audit" or "documentation
|
||||
report," follow only the strategist and engineer roles. Finalize the
|
||||
`audit-report-YYYY-MM-DD.md` (using today's date) with findings and
|
||||
recommendations, then stop without making changes to documentation files.
|
||||
|
||||
**Interactive mode:** If asked to do this “interactively,” you must ask questions
|
||||
when uncertain. You MUST present the `audit-plan-YYYY-MM-DD.md` to the user for
|
||||
approval after the engineer phase and before the writer phase.
|
||||
|
||||
---
|
||||
|
||||
## Role 1: strategist
|
||||
You are an expert content strategist. Your goal is to ensure 100% coverage and
|
||||
adherence to the information architecture and style standards.
|
||||
|
||||
### Rules
|
||||
- **Source of truth:** `sidebar.json` is the sole source of truth for the site's
|
||||
Information Architecture (IA). Do NOT propose moving files; the directory
|
||||
structure is irrelevant to the IA.
|
||||
- **Deep audit requirement:** You MUST audit EVERY single page listed in
|
||||
`sidebar.json`. For every page, you must verify:
|
||||
1. **BLUF:** Does it start with a clear introductory paragraph?
|
||||
2. **No TOC:** Is the manual "Table of Contents" section removed?
|
||||
3. **Sentence case:** Are ALL headings and bold labels in sentence case?
|
||||
4. **Experimental notes:** Are 🔬 features correctly marked with the mandatory
|
||||
warning note? Note as follows:
|
||||
|
||||
> **Note:** This is a preview feature currently under active development.
|
||||
> Preview features may be available on the **Preview** channel or may need to be
|
||||
> enabled under `/settings`.
|
||||
- **Proactive codebase audit:** You MUST audit high-signal areas
|
||||
(`packages/cli/src/commands/`, `packages/core/src/tools/`,
|
||||
`packages/cli/src/config/settings.ts`) to find undocumented features.
|
||||
|
||||
### Tasks
|
||||
1. **Reconciliation:** Create a master list of all pages from `sidebar.json`.
|
||||
2. **Comprehensive review:** Read every page. Do not skip any.
|
||||
3. **Codebase scan:** Check for new commands, tools, or settings in the code.
|
||||
4. **Outdated content:** Identify features in docs that no longer exist in code.
|
||||
|
||||
### Deliverable
|
||||
Create a `audit-plan-YYYY-MM-DD.md` (using today's date) containing:
|
||||
- **Audit matrix:** A table listing every page, its Pass/Fail status, and
|
||||
specific violations (Missing BLUF, TOC Present, Case Error, Wrap, etc.).
|
||||
- **Net-new content:** List of undocumented features from the codebase scan.
|
||||
- **Deprecations:** List of pages or sections to be removed.
|
||||
- **Handover:** A comment stating the strategist phase is complete.
|
||||
|
||||
---
|
||||
|
||||
## Role 2: engineer
|
||||
You are an expert Gemini CLI engineer. Your role is to ensure technical
|
||||
accuracy and provide specific examples.
|
||||
|
||||
### Rules
|
||||
- **Verification:** Use `grep_search` and `read_file` to verify the accuracy of
|
||||
every code snippet, command, and version number.
|
||||
- **Samples:** Provide specific, functional code samples for multiple shells
|
||||
(macOS/Bash and Windows/PowerShell) where applicable.
|
||||
|
||||
### Tasks
|
||||
1. **Technical deep dive:** For every page flagged for update or addition,
|
||||
verify the implementation details in the `packages/` directory.
|
||||
2. **Sample generation:** Write the code samples for the new documentation.
|
||||
3. **Clarification:** Correct any technical misunderstandings in the plan.
|
||||
|
||||
### Deliverable
|
||||
Update the audit plan with technical blocks and handover comment.
|
||||
|
||||
---
|
||||
|
||||
## Role 3: writer
|
||||
You are an expert technical writer. You implement the plan using surgical edits.
|
||||
|
||||
### Rules
|
||||
- **Style adherence:** Strictly follow [style-guide.md](style-guide.md).
|
||||
- **Surgical edits:** Use `replace` whenever possible to minimize noise.
|
||||
- **No relocation:** Do not move files between directories.
|
||||
- **No mass rewriting:** Make edits as recommended but do not rewrite pages.
|
||||
|
||||
### Tasks
|
||||
1. **Implementation:** Iterate through the approved plan.
|
||||
2. **Creation:** Create net-new files if required for missing features.
|
||||
3. **Status tracking:** Mark each item as COMPLETED in the audit plan.
|
||||
|
||||
### Deliverable
|
||||
Updated audit plan with completion reports and handover comment.
|
||||
|
||||
---
|
||||
|
||||
## Role 4: editor
|
||||
You are an expert editor. You provide the final quality gate.
|
||||
|
||||
### Tasks
|
||||
1. **Final pass:** Re-read every updated page to avoid introduced errors. Do
|
||||
not rewrite pages; concentrate on changes that have been made.
|
||||
2. **Link check:** Run `node scripts/find_broken_links.cjs docs/`.
|
||||
3. **TOC removal:** Ensure no manual "Table of Contents" sections remain.
|
||||
4. **Format:** Run `npm run format`.
|
||||
|
||||
### Deliverable
|
||||
Finalized audit plan with a summary of changes and verification results.
|
||||
@@ -1,99 +0,0 @@
|
||||
# Gemini CLI documentation style guide
|
||||
|
||||
As an expert technical writer and editor for the Gemini CLI project, you must
|
||||
ensure the content strictly adheres to these standards. This guide provides
|
||||
detailed instructions on voice, language, formatting, and structure.
|
||||
|
||||
## Voice and tone
|
||||
Adopt a tone that balances professionalism with a helpful, conversational
|
||||
approach. This ensures the documentation is accessible and engaging for all
|
||||
users.
|
||||
|
||||
- **Perspective and tense:** Address the reader as "you." Use active voice and
|
||||
present tense (e.g., "The API returns..."). Avoid "will" (second-person
|
||||
future).
|
||||
- **Tone:** Maintain a professional, friendly, and direct tone.
|
||||
- **Clarity:** Use simple vocabulary. Avoid jargon, slang, and marketing hype.
|
||||
- **Global audience:** Write in standard US English. Avoid idioms, cultural
|
||||
references, and region-specific analogies.
|
||||
- **Requirements:** Be clear about requirements ("must") vs. recommendations
|
||||
("we recommend"). Avoid the word "should."
|
||||
- **Word choice:** Avoid "please" and anthropomorphism (e.g., "the server
|
||||
thinks"). Use common contractions (don't, it's).
|
||||
|
||||
## Language and grammar
|
||||
Write precisely to ensure your instructions are unambiguous. Consistency in
|
||||
grammar and punctuation helps users follow technical procedures correctly.
|
||||
|
||||
- **Abbreviations:** Avoid Latin abbreviations. Use "for example" (not "e.g.")
|
||||
and "that is" (not "i.e.").
|
||||
- **Punctuation:** Use the serial comma. Place periods and commas inside
|
||||
quotation marks.
|
||||
- **Dates:** Use unambiguous formats (e.g., "January 22, 2026").
|
||||
- **Conciseness:** Use "lets you" instead of "allows you to." Use precise,
|
||||
specific verbs.
|
||||
- **Examples and placeholders:**
|
||||
- Use meaningful names in examples; avoid "foo" or "bar."
|
||||
- Format placeholders in `ITALIC_ALL_CAPS` (e.g., `YOUR_API_KEY`).
|
||||
- **Quota and limit terminology:** For any content involving resource capacity,
|
||||
strictly adhere to the guidelines in
|
||||
[quota-limit-style-guide.md](quota-limit-style-guide.md).
|
||||
|
||||
## Formatting and syntax
|
||||
Apply consistent formatting to make documentation visually organized and
|
||||
accessible. These rules help users distinguish between UI elements, code, and
|
||||
narrative text.
|
||||
|
||||
- **Overview paragraphs:** Follow every heading with at least one
|
||||
introductory overview paragraph before any lists or sub-headings.
|
||||
- **Text wrap:** Wrap text at 80 characters (except long links or tables).
|
||||
- **Casing:** Use sentence case for headings, titles, and bolded text.
|
||||
- **Naming:** Always refer to the project as `Gemini CLI` (never
|
||||
`the Gemini CLI`).
|
||||
- **Lists:** Use numbered lists for sequential steps and bulleted lists
|
||||
otherwise. Keep list items parallel in structure and start with imperative
|
||||
verbs.
|
||||
- **UI and code:** Use **bold** for UI elements and `code font` for filenames,
|
||||
snippets, commands, and API elements.
|
||||
- **Links:** Use descriptive anchor text that makes sense out of context.
|
||||
Avoid "click here" or "here."
|
||||
- **Accessibility:** Use semantic HTML elements correctly (headings, lists,
|
||||
tables). Provide descriptive alt text for all images.
|
||||
- **Media:** Use lowercase hyphenated filenames for all assets.
|
||||
|
||||
## Experimental features
|
||||
Ensure that users are correctly informed about experimental or preview
|
||||
capabilities. These often require a specific setting to be enabled.
|
||||
|
||||
- **Enablement:** Any feature that must be enabled via a setting (e.g.,
|
||||
`general.devtools`) is considered experimental.
|
||||
- **Warning note:** If a feature is experimental, add this note
|
||||
immediately after the introductory paragraph:
|
||||
|
||||
> **Note:** This is a preview feature currently under active development.
|
||||
> Preview features may be available on the **Preview** channel or may need to be
|
||||
> enabled under `/settings`.
|
||||
|
||||
- **Visual indicators:** Use the microscope emoji (🔬) in the heading or
|
||||
sidebar label for experimental features.
|
||||
|
||||
## Structure
|
||||
Follow a consistent structure to support the user journey from introduction to
|
||||
next steps. This hierarchy helps users find information quickly.
|
||||
|
||||
- **BLUF:** Start every page with an introduction (Bottom Line Up Front)
|
||||
explaining what the reader can expect to learn.
|
||||
- **Headings:** Use hierarchical headings (H1, H2, H3) to organize content.
|
||||
- **Procedures:**
|
||||
- Introduce lists of steps with a complete sentence.
|
||||
- Start each step with an imperative verb.
|
||||
- Number sequential steps; use bullets for non-sequential lists.
|
||||
- Put conditions before instructions (e.g., "On the **Settings** page,
|
||||
click...").
|
||||
- Provide clear context for where the action takes place.
|
||||
- Indicate optional steps clearly (e.g., "Optional: ...").
|
||||
- **Visual elements:** Use bullet lists, tables, notes (`> **Note:**`), and
|
||||
warnings (`> **Warning:**`) to break up dense text.
|
||||
- **Table of contents:** Do not include a manual table of contents.
|
||||
- **Next steps:** Conclude with a "Next steps" section if applicable to guide
|
||||
the user's next action.
|
||||
@@ -1,63 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Simple script to find broken internal Markdown links in a directory.
|
||||
* Usage: node find_broken_links.cjs <directory>
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const targetDir = process.argv[2];
|
||||
if (!targetDir) {
|
||||
console.error('Error: Please provide a directory path.');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
function findFiles(dir, ext, fileList = []) {
|
||||
const files = fs.readdirSync(dir);
|
||||
files.forEach(file => {
|
||||
const filePath = path.join(dir, file);
|
||||
if (fs.statSync(filePath).isDirectory()) {
|
||||
findFiles(filePath, ext, fileList);
|
||||
} else if (filePath.endsWith(ext)) {
|
||||
fileList.push(filePath);
|
||||
}
|
||||
});
|
||||
return fileList;
|
||||
}
|
||||
|
||||
const mdFiles = findFiles(targetDir, '.md');
|
||||
const linkRegex = /\[([^\]]+)\]\(([^)]+)\)/g;
|
||||
let brokenLinksFound = 0;
|
||||
|
||||
mdFiles.forEach(file => {
|
||||
const content = fs.readFileSync(file, 'utf8');
|
||||
const dir = path.dirname(file);
|
||||
let match;
|
||||
|
||||
while ((match = linkRegex.exec(content)) !== null) {
|
||||
const linkPath = match[2];
|
||||
|
||||
// Only check relative, internal links (not http, mailto, etc.)
|
||||
if (linkPath.startsWith('http') || linkPath.startsWith('mailto:') || linkPath.startsWith('#')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Strip anchor from link path
|
||||
const cleanLinkPath = linkPath.split('#')[0];
|
||||
if (!cleanLinkPath) continue;
|
||||
|
||||
const absoluteLinkPath = path.resolve(dir, cleanLinkPath);
|
||||
|
||||
if (!fs.existsSync(absoluteLinkPath)) {
|
||||
console.log(`Broken link in ${path.relative(targetDir, file)}: [${match[1]}](${linkPath}) -> Not found at: ${path.relative(targetDir, absoluteLinkPath)}`);
|
||||
brokenLinksFound++;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (brokenLinksFound === 0) {
|
||||
console.log('Success: No broken internal links found.');
|
||||
} else {
|
||||
console.log(`\nFinished: Found ${brokenLinksFound} broken link(s).`);
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
name: 'Weekly Docs Audit'
|
||||
|
||||
on:
|
||||
schedule:
|
||||
# Runs every Monday at 00:00 UTC
|
||||
- cron: '0 0 * * MON'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
audit-docs:
|
||||
runs-on: 'ubuntu-latest'
|
||||
permissions:
|
||||
contents: 'write'
|
||||
pull-requests: 'write'
|
||||
|
||||
steps:
|
||||
- name: 'Checkout repository'
|
||||
uses: 'actions/checkout@v4'
|
||||
with:
|
||||
fetch-depth: 0
|
||||
ref: 'main'
|
||||
|
||||
- name: 'Set up Node.js'
|
||||
uses: 'actions/setup-node@v4'
|
||||
with:
|
||||
node-version: '20'
|
||||
|
||||
- name: 'Run Docs Audit with Gemini'
|
||||
uses: 'google-github-actions/run-gemini-cli@v0' # Use a specific tag or SHA for stability
|
||||
with:
|
||||
gemini_api_key: '${{ secrets.GEMINI_API_KEY }}'
|
||||
prompt: |
|
||||
Activate the 'docs-writer' skill.
|
||||
|
||||
**Task:** Audit the entire documentation set for correctness and adherence to style guidelines, as defined in your 'docs-auditing.md' reference.
|
||||
|
||||
When you are done, please output your thought process and the steps you took for future debugging purposes, and save the audit results to 'audit-results-${{ github.run_id }}.md'.
|
||||
|
||||
- name: 'Create Pull Request with Audit Results'
|
||||
uses: 'peter-evans/create-pull-request@v6'
|
||||
with:
|
||||
token: '${{ secrets.GEMINI_CLI_ROBOT_GITHUB_PAT }}'
|
||||
commit-message: 'docs: weekly audit results for ${{ github.run_id }}'
|
||||
title: 'Docs Audit for Week of ${{ github.event.schedule }}'
|
||||
body: |
|
||||
This PR contains the auto-generated documentation audit for the week. It includes a new `audit-results-*.md` file with findings and any direct fixes applied by the agent.
|
||||
|
||||
Please review the suggestions and merge.
|
||||
branch: 'docs-audit-${{ github.run_id }}'
|
||||
base: 'main'
|
||||
team-reviewers: 'gemini-cli-docs, gemini-cli-maintainers'
|
||||
delete-branch: true
|
||||
@@ -1,88 +0,0 @@
|
||||
# Documentation Audit Plan - 2026-03-18
|
||||
|
||||
This audit plan identifies areas for improvement in the Gemini CLI documentation
|
||||
set to ensure 100% coverage, technical accuracy, and adherence to the
|
||||
information architecture and style standards.
|
||||
|
||||
## Audit Matrix
|
||||
|
||||
| Page | Status | Violations |
|
||||
| :--------------------------------------------- | :----- | :--------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `docs/index.md` | PASS | |
|
||||
| `docs/get-started/index.md` | PASS | |
|
||||
| `docs/get-started/installation.md` | PASS | |
|
||||
| `docs/get-started/authentication.md` | PASS | |
|
||||
| `docs/get-started/examples.md` | PASS | |
|
||||
| `docs/cli/cli-reference.md` | FAIL | Case Error: "CLI Options" |
|
||||
| `docs/get-started/gemini-3.md` | PASS | |
|
||||
| `docs/cli/tutorials/file-management.md` | PASS | |
|
||||
| `docs/cli/tutorials/skills-getting-started.md` | PASS | |
|
||||
| `docs/cli/tutorials/memory-management.md` | PASS | |
|
||||
| `docs/cli/tutorials/shell-commands.md` | PASS | |
|
||||
| `docs/cli/tutorials/session-management.md` | PASS | |
|
||||
| `docs/cli/tutorials/task-planning.md` | PASS | |
|
||||
| `docs/cli/tutorials/web-tools.md` | PASS | |
|
||||
| `docs/cli/tutorials/mcp-setup.md` | PASS | |
|
||||
| `docs/cli/tutorials/automation.md` | PASS | |
|
||||
| `docs/extensions/index.md` | PASS | |
|
||||
| `docs/cli/skills.md` | FAIL | Case Error: "Key Benefits", "Skill Discovery Tiers", "Managing Skills", "In an Interactive Session", "From the Terminal", "How it Works" |
|
||||
| `docs/cli/checkpointing.md` | PASS | |
|
||||
| `docs/cli/headless.md` | PASS | |
|
||||
| `docs/hooks/index.md` | PASS | |
|
||||
| `docs/hooks/reference.md` | PASS | |
|
||||
| `docs/ide-integration/index.md` | PASS | |
|
||||
| `docs/tools/mcp-server.md` | PASS | |
|
||||
| `docs/cli/model-routing.md` | PASS | |
|
||||
| `docs/cli/model.md` | PASS | |
|
||||
| `docs/cli/model-steering.md` | PASS | |
|
||||
| `docs/cli/notifications.md` | PASS | |
|
||||
| `docs/cli/plan-mode.md` | FAIL | Missing mandatory experimental note; Case Error: "Tool Restrictions", "Automatic Model Routing", "Research Subagents" |
|
||||
| `docs/core/subagents.md` | FAIL | Case Error in multiple headings; Missing/Inconsistent experimental note |
|
||||
| `docs/core/remote-agents.md` | FAIL | Case Error: "Remote Subagents", "Managing Subagents" |
|
||||
| `docs/cli/rewind.md` | PASS | |
|
||||
| `docs/cli/sandbox.md` | PASS | |
|
||||
| `docs/cli/settings.md` | FAIL | Case Error: "HooksConfig" -> "Hooks config" |
|
||||
| `docs/cli/telemetry.md` | PASS | |
|
||||
| `docs/cli/token-caching.md` | PASS | |
|
||||
| `docs/cli/custom-commands.md` | PASS | |
|
||||
| `docs/cli/enterprise.md` | PASS | |
|
||||
| `docs/cli/gemini-ignore.md` | PASS | |
|
||||
| `docs/cli/generation-settings.md` | PASS | |
|
||||
| `docs/cli/gemini-md.md` | PASS | |
|
||||
| `docs/cli/system-prompt.md` | FAIL | Case Error: "Variable Substitution" |
|
||||
| `docs/cli/themes.md` | PASS | |
|
||||
| `docs/cli/trusted-folders.md` | PASS | |
|
||||
| `docs/contributing.md` | PASS | |
|
||||
| `docs/integration-tests.md` | PASS | |
|
||||
| `docs/issue-and-pr-automation.md` | PASS | |
|
||||
| `docs/local-development.md` | PASS | |
|
||||
| `docs/npm.md` | PASS | |
|
||||
| `docs/reference/keyboard-shortcuts.md` | FAIL | Case Error in almost all subheadings (Title Case instead of Sentence case) |
|
||||
| `docs/reference/memport.md` | FAIL | Case Error: "Best Practices" |
|
||||
| `docs/reference/policy-engine.md` | FAIL | Case Error: "Standard Locations", "Supplemental Admin Policies", "Security Requirements" |
|
||||
| `docs/reference/tools.md` | FAIL | Missing tracker tools; Possible outdated parameters |
|
||||
|
||||
## Net-new content
|
||||
|
||||
- **Task Tracker:** Document `experimental.taskTracker` and associated tools
|
||||
(`tracker_create_task`, `tracker_update_task`, `tracker_get_task`,
|
||||
`tracker_list_tasks`, `tracker_add_dependency`, `tracker_visualize`).
|
||||
- **Gemma Model Router:** Document `experimental.gemmaModelRouter`.
|
||||
- **Tool Output Masking:** Document `experimental.toolOutputMasking`.
|
||||
- **New Commands:** Document `/corgi`, `/agents`.
|
||||
|
||||
## Strategy for updates
|
||||
|
||||
1. **Surgical Fixes:** Apply sentence case and missing experimental notes to
|
||||
all FAILED pages.
|
||||
2. **Feature Documentation:** Create a new page `docs/cli/task-tracker.md` and
|
||||
link it from `tools.md` and `index.md`.
|
||||
3. **Tool Reference Update:** Add the missing tools to
|
||||
`docs/reference/tools.md`.
|
||||
4. **Settings Sync:** Ensure `docs/cli/settings.md` includes the new
|
||||
experimental settings found in `settingsSchema.ts`.
|
||||
|
||||
## Handover
|
||||
|
||||
Strategist phase complete. Transitioning to Engineer/Writer role to apply
|
||||
changes.
|
||||
@@ -1,101 +0,0 @@
|
||||
# Documentation audit report: 2026-03-17
|
||||
|
||||
This report contains the findings of a deep content audit performed on March
|
||||
17, 2026. It identifies areas for improvement in technical accuracy, structure,
|
||||
and adherence to the style guide.
|
||||
|
||||
---
|
||||
|
||||
## Role 1: strategist
|
||||
|
||||
**Handover:** Strategist phase is complete.
|
||||
|
||||
### Existing content to be updated
|
||||
|
||||
- **`docs/index.md`**:
|
||||
- **Issue:** Section headings use title case (e.g., "Use Gemini CLI",
|
||||
"Features").
|
||||
- **Recommendation:** Update to sentence case (e.g., "Use Gemini CLI",
|
||||
"Features" -> "Use Gemini CLI", "Features"). Wait, "Gemini CLI" is a proper
|
||||
noun, so "Use Gemini CLI" is correct. However, "Get started" is correct.
|
||||
"Configuration" is correct.
|
||||
- **Issue:** Links to development files are at the root of `docs/` but should
|
||||
be moved to `docs/development/`.
|
||||
- **Recommendation:** Update links after moving files.
|
||||
|
||||
- **`docs/sidebar.json`**:
|
||||
- **Issue:** Slugs for development files point to root `docs/` instead of
|
||||
`docs/development/`.
|
||||
- **Issue:** `docs/contributing` slug is inconsistent with the physical
|
||||
`CONTRIBUTING.md` at the repository root.
|
||||
- **Recommendation:** Move development files to a subfolder and update slugs.
|
||||
|
||||
- **`docs/cli/settings.md`**: (Needs verification)
|
||||
- **Issue:** Likely missing recent settings like `general.devtools`.
|
||||
|
||||
### Existing content to be deprecated
|
||||
|
||||
- **None identified.** All current files seem relevant but some need relocation.
|
||||
|
||||
### Net-new content to be added
|
||||
|
||||
- **`docs/cli/devtools.md`**:
|
||||
- **Purpose:** Document the integrated DevTools feature.
|
||||
- **Placement:** Features section.
|
||||
- **Content:** Network inspector, console inspector, session management, and
|
||||
`general.devtools` setting.
|
||||
|
||||
---
|
||||
|
||||
## Role 2: engineer
|
||||
|
||||
**Handover:** Engineer phase is complete. Report-only mode finalized.
|
||||
|
||||
### Technical clarifications and code samples
|
||||
|
||||
#### For `docs/cli/devtools.md` (Net-new):
|
||||
|
||||
The DevTools server probes port `25417`. It can be enabled via settings:
|
||||
|
||||
```bash
|
||||
gemini config set general.devtools true
|
||||
```
|
||||
|
||||
**Architecture details:**
|
||||
|
||||
- Probes port `25417`.
|
||||
- Uses WebSocket for log ingestion.
|
||||
- Uses SSE for pushing events to the client.
|
||||
|
||||
#### For `docs/index.md` (Update):
|
||||
|
||||
Verify the install command:
|
||||
|
||||
```bash
|
||||
npm install -g @google/gemini-cli
|
||||
```
|
||||
|
||||
_Note: Check package.json in packages/cli to confirm the public package name._
|
||||
|
||||
#### Verification results:
|
||||
|
||||
- **`packages/devtools`**: Confirmed port `25417` in
|
||||
`packages/devtools/GEMINI.md`.
|
||||
- **`packages/cli`**: Confirmed `general.devtools` exists in the configuration
|
||||
logic.
|
||||
- **`packages/cli/package.json`**: (Need to verify public name).
|
||||
|
||||
---
|
||||
|
||||
## Summary of findings
|
||||
|
||||
1. **Structural issues:** Development documentation is currently cluttered at
|
||||
the root of the `docs/` directory. It should be consolidated into a
|
||||
`development/` subfolder.
|
||||
2. **Missing features:** The DevTools package, a significant feature for
|
||||
debugging network and console activity, lacks a dedicated documentation page.
|
||||
3. **Style consistency:** Several headings in the landing page require
|
||||
adjustment to strictly follow sentence case (e.g., "Use Gemini CLI" is fine,
|
||||
but "Get Started" should be "Get started").
|
||||
4. **Link integrity:** Inconsistencies between `sidebar.json` and file locations
|
||||
(specifically `CONTRIBUTING.md`) need resolution.
|
||||
Binary file not shown.
@@ -28,6 +28,7 @@ import {
|
||||
type MergedSettings,
|
||||
createTestMergedSettings,
|
||||
} from './settings.js';
|
||||
import * as SettingsModule from './settings.js';
|
||||
import * as ServerConfig from '@google/gemini-cli-core';
|
||||
|
||||
import { isWorkspaceTrusted } from './trustedFolders.js';
|
||||
@@ -813,7 +814,7 @@ describe('Hierarchical Memory Loading (config.ts) - Placeholder Suite', () => {
|
||||
});
|
||||
|
||||
it('should pass extension context file paths to loadServerHierarchicalMemory', async () => {
|
||||
process.argv = ['node', 'script.js'];
|
||||
process.argv = ['node', 'script.js', '--prompt', 'test'];
|
||||
const settings = createTestMergedSettings();
|
||||
vi.spyOn(ExtensionManager.prototype, 'getExtensions').mockReturnValue([
|
||||
{
|
||||
@@ -862,7 +863,7 @@ describe('Hierarchical Memory Loading (config.ts) - Placeholder Suite', () => {
|
||||
});
|
||||
|
||||
it('should pass includeDirectories to loadServerHierarchicalMemory when loadMemoryFromIncludeDirectories is true', async () => {
|
||||
process.argv = ['node', 'script.js'];
|
||||
process.argv = ['node', 'script.js', '--prompt', 'test'];
|
||||
const includeDir = path.resolve(path.sep, 'path', 'to', 'include');
|
||||
const settings = createTestMergedSettings({
|
||||
context: {
|
||||
@@ -890,7 +891,7 @@ describe('Hierarchical Memory Loading (config.ts) - Placeholder Suite', () => {
|
||||
});
|
||||
|
||||
it('should NOT pass includeDirectories to loadServerHierarchicalMemory when loadMemoryFromIncludeDirectories is false', async () => {
|
||||
process.argv = ['node', 'script.js'];
|
||||
process.argv = ['node', 'script.js', '--prompt', 'test'];
|
||||
const settings = createTestMergedSettings({
|
||||
context: {
|
||||
includeDirectories: ['/path/to/include'],
|
||||
@@ -915,6 +916,39 @@ describe('Hierarchical Memory Loading (config.ts) - Placeholder Suite', () => {
|
||||
200,
|
||||
);
|
||||
});
|
||||
|
||||
it('should skip extension, memory, and PTY discovery in bootstrap mode', async () => {
|
||||
process.argv = ['node', 'script.js'];
|
||||
const settings = createTestMergedSettings();
|
||||
const argv = await parseArguments(settings);
|
||||
const loadSettingsSpy = vi.spyOn(SettingsModule, 'loadSettings');
|
||||
const getPtySpy = vi.spyOn(ServerConfig, 'getPty');
|
||||
|
||||
await loadCliConfig(settings, 'session-id', argv, {
|
||||
mode: 'bootstrap',
|
||||
loadedSettings: { merged: settings } as SettingsModule.LoadedSettings,
|
||||
});
|
||||
|
||||
expect(loadSettingsSpy).not.toHaveBeenCalled();
|
||||
expect(ExtensionManager.prototype.loadExtensions).not.toHaveBeenCalled();
|
||||
expect(ServerConfig.loadServerHierarchicalMemory).not.toHaveBeenCalled();
|
||||
expect(getPtySpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should defer extension and memory discovery during interactive startup', async () => {
|
||||
process.argv = ['node', 'script.js'];
|
||||
const settings = createTestMergedSettings();
|
||||
const argv = await parseArguments(settings);
|
||||
const getPtySpy = vi.spyOn(ServerConfig, 'getPty');
|
||||
|
||||
await loadCliConfig(settings, 'session-id', argv, {
|
||||
loadedSettings: { merged: settings } as SettingsModule.LoadedSettings,
|
||||
});
|
||||
|
||||
expect(ExtensionManager.prototype.loadExtensions).not.toHaveBeenCalled();
|
||||
expect(ServerConfig.loadServerHierarchicalMemory).not.toHaveBeenCalled();
|
||||
expect(getPtySpy).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('mergeMcpServers', () => {
|
||||
|
||||
@@ -39,6 +39,7 @@ import {
|
||||
resolveToRealPath,
|
||||
applyAdminAllowlist,
|
||||
getAdminBlockedMcpServersMessage,
|
||||
startupProfiler,
|
||||
type HookDefinition,
|
||||
type HookEventName,
|
||||
type OutputFormat,
|
||||
@@ -47,6 +48,7 @@ import {
|
||||
import {
|
||||
type Settings,
|
||||
type MergedSettings,
|
||||
type LoadedSettings,
|
||||
saveModelChange,
|
||||
loadSettings,
|
||||
} from './settings.js';
|
||||
@@ -417,6 +419,8 @@ export function isDebugMode(argv: CliArgs): boolean {
|
||||
|
||||
export interface LoadCliConfigOptions {
|
||||
cwd?: string;
|
||||
mode?: 'bootstrap' | 'full';
|
||||
loadedSettings?: LoadedSettings;
|
||||
projectHooks?: { [K in HookEventName]?: HookDefinition[] } & {
|
||||
disabled?: string[];
|
||||
};
|
||||
@@ -428,10 +432,15 @@ export async function loadCliConfig(
|
||||
argv: CliArgs,
|
||||
options: LoadCliConfigOptions = {},
|
||||
): Promise<Config> {
|
||||
const { cwd = process.cwd(), projectHooks } = options;
|
||||
const {
|
||||
cwd = process.cwd(),
|
||||
mode = 'full',
|
||||
loadedSettings,
|
||||
projectHooks,
|
||||
} = options;
|
||||
const debugMode = isDebugMode(argv);
|
||||
|
||||
const loadedSettings = loadSettings(cwd);
|
||||
const resolvedLoadedSettings = loadedSettings ?? loadSettings(cwd);
|
||||
const isBootstrap = mode === 'bootstrap';
|
||||
|
||||
if (argv.sandbox) {
|
||||
process.env['GEMINI_SANDBOX'] = 'true';
|
||||
@@ -439,6 +448,18 @@ export async function loadCliConfig(
|
||||
|
||||
const memoryImportFormat = settings.context?.importFormat || 'tree';
|
||||
const includeDirectoryTree = settings.context?.includeDirectoryTree ?? true;
|
||||
const interactive =
|
||||
!!argv.promptInteractive ||
|
||||
!!argv.acp ||
|
||||
!!argv.experimentalAcp ||
|
||||
(!isHeadlessMode({ prompt: argv.prompt, query: argv.query }) &&
|
||||
!argv.isCommand);
|
||||
const shouldDeferInteractiveStartupWork =
|
||||
!isBootstrap &&
|
||||
interactive &&
|
||||
!argv.listExtensions &&
|
||||
!argv.listSessions &&
|
||||
!argv.deleteSession;
|
||||
|
||||
const ideMode = settings.ide?.enabled ?? false;
|
||||
|
||||
@@ -480,6 +501,7 @@ export async function loadCliConfig(
|
||||
.map(resolvePath)
|
||||
.concat((argv.includeDirectories || []).map(resolvePath));
|
||||
|
||||
const clientVersion = await getVersion();
|
||||
const extensionManager = new ExtensionManager({
|
||||
settings,
|
||||
requestConsent: requestConsentNonInteractive,
|
||||
@@ -488,9 +510,15 @@ export async function loadCliConfig(
|
||||
enabledExtensionOverrides: argv.extensions,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||
eventEmitter: coreEvents as EventEmitter<ExtensionEvents>,
|
||||
clientVersion: await getVersion(),
|
||||
clientVersion,
|
||||
});
|
||||
await extensionManager.loadExtensions();
|
||||
if (!isBootstrap && !shouldDeferInteractiveStartupWork) {
|
||||
const loadExtensionsHandle = startupProfiler.start(
|
||||
'load_extensions_during_config',
|
||||
);
|
||||
await extensionManager.loadExtensions();
|
||||
loadExtensionsHandle?.end();
|
||||
}
|
||||
|
||||
const extensionPlanSettings = extensionManager
|
||||
.getExtensions()
|
||||
@@ -511,7 +539,12 @@ export async function loadCliConfig(
|
||||
let fileCount = 0;
|
||||
let filePaths: string[] = [];
|
||||
|
||||
if (!experimentalJitContext) {
|
||||
if (
|
||||
!experimentalJitContext &&
|
||||
!isBootstrap &&
|
||||
!shouldDeferInteractiveStartupWork
|
||||
) {
|
||||
const loadMemoryHandle = startupProfiler.start('load_memory');
|
||||
// Call the (now wrapper) loadHierarchicalGeminiMemory which calls the server's version
|
||||
const result = await loadServerHierarchicalMemory(
|
||||
cwd,
|
||||
@@ -528,6 +561,7 @@ export async function loadCliConfig(
|
||||
memoryContent = result.memoryContent;
|
||||
fileCount = result.fileCount;
|
||||
filePaths = result.filePaths;
|
||||
loadMemoryHandle?.end();
|
||||
}
|
||||
|
||||
const question = argv.promptInteractive || argv.prompt || '';
|
||||
@@ -617,15 +651,6 @@ export async function loadCliConfig(
|
||||
throw err;
|
||||
}
|
||||
|
||||
// -p/--prompt forces non-interactive (headless) mode
|
||||
// -i/--prompt-interactive forces interactive mode with an initial prompt
|
||||
const interactive =
|
||||
!!argv.promptInteractive ||
|
||||
!!argv.acp ||
|
||||
!!argv.experimentalAcp ||
|
||||
(!isHeadlessMode({ prompt: argv.prompt, query: argv.query }) &&
|
||||
!argv.isCommand);
|
||||
|
||||
const allowedTools = argv.allowedTools || settings.tools?.allowed || [];
|
||||
|
||||
// In non-interactive mode, exclude tools that require a prompt.
|
||||
@@ -695,7 +720,7 @@ export async function loadCliConfig(
|
||||
? argv.screenReader
|
||||
: (settings.ui?.accessibility?.screenReader ?? false);
|
||||
|
||||
const ptyInfo = await getPty();
|
||||
const ptyInfo = isBootstrap ? null : await getPty();
|
||||
|
||||
const mcpEnabled = settings.admin?.mcp?.enabled ?? true;
|
||||
const extensionsEnabled = settings.admin?.extensions?.enabled ?? true;
|
||||
@@ -741,7 +766,7 @@ export async function loadCliConfig(
|
||||
acpMode: isAcpMode,
|
||||
clientName,
|
||||
sessionId,
|
||||
clientVersion: await getVersion(),
|
||||
clientVersion,
|
||||
embeddingModel: DEFAULT_GEMINI_EMBEDDING_MODEL,
|
||||
sandbox: sandboxConfig,
|
||||
targetDir: cwd,
|
||||
@@ -822,6 +847,8 @@ export async function loadCliConfig(
|
||||
skillsSupport: settings.skills?.enabled ?? true,
|
||||
disabledSkills: settings.skills?.disabled,
|
||||
experimentalJitContext: settings.experimental?.jitContext,
|
||||
deferInitialMemoryLoad:
|
||||
shouldDeferInteractiveStartupWork && !experimentalJitContext,
|
||||
modelSteering: settings.experimental?.modelSteering,
|
||||
toolOutputMasking: settings.experimental?.toolOutputMasking,
|
||||
noBrowser: !!process.env['NO_BROWSER'],
|
||||
@@ -864,7 +891,8 @@ export async function loadCliConfig(
|
||||
hooks: settings.hooks || {},
|
||||
disabledHooks: settings.hooksConfig?.disabled || [],
|
||||
projectHooks: projectHooks || {},
|
||||
onModelChange: (model: string) => saveModelChange(loadedSettings, model),
|
||||
onModelChange: (model: string) =>
|
||||
saveModelChange(resolvedLoadedSettings, model),
|
||||
onReload: async () => {
|
||||
const refreshedSettings = loadSettings(cwd);
|
||||
return {
|
||||
|
||||
@@ -94,7 +94,7 @@ interface ExtensionManagerParams {
|
||||
/**
|
||||
* Actual implementation of an ExtensionLoader.
|
||||
*
|
||||
* You must call `loadExtensions` prior to calling other methods on this class.
|
||||
* Extension metadata is loaded lazily via `loadExtensions`/`ensureLoaded`.
|
||||
*/
|
||||
export class ExtensionManager extends ExtensionLoader {
|
||||
private extensionEnablementManager: ExtensionEnablementManager;
|
||||
@@ -146,12 +146,24 @@ export class ExtensionManager extends ExtensionLoader {
|
||||
}
|
||||
|
||||
getExtensions(): GeminiCLIExtension[] {
|
||||
if (!this.loadedExtensions) {
|
||||
throw new Error(
|
||||
'Extensions not yet loaded, must call `loadExtensions` first',
|
||||
);
|
||||
return this.loadedExtensions ?? [];
|
||||
}
|
||||
|
||||
override isLoaded(): boolean {
|
||||
return this.loadedExtensions !== undefined;
|
||||
}
|
||||
|
||||
override async ensureLoaded(): Promise<void> {
|
||||
if (this.loadedExtensions) {
|
||||
return;
|
||||
}
|
||||
return this.loadedExtensions;
|
||||
|
||||
if (this.loadingPromise) {
|
||||
await this.loadingPromise;
|
||||
return;
|
||||
}
|
||||
|
||||
await this.loadExtensions();
|
||||
}
|
||||
|
||||
async installOrUpdateExtension(
|
||||
|
||||
@@ -289,6 +289,11 @@ export function createTestMergedSettings(
|
||||
) as MergedSettings;
|
||||
}
|
||||
|
||||
export function createDefaultMergedSettings(): MergedSettings {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||
return getDefaultsFromSchema() as MergedSettings;
|
||||
}
|
||||
|
||||
/**
|
||||
* An immutable snapshot of settings state.
|
||||
* Used with useSyncExternalStore for reactive updates.
|
||||
|
||||
@@ -269,6 +269,7 @@ vi.mock('./validateNonInterActiveAuth.js', () => ({
|
||||
|
||||
describe('gemini.tsx main function', () => {
|
||||
let originalIsTTY: boolean | undefined;
|
||||
const originalArgv = [...process.argv];
|
||||
let initialUnhandledRejectionListeners: NodeJS.UnhandledRejectionListener[] =
|
||||
[];
|
||||
|
||||
@@ -284,6 +285,7 @@ describe('gemini.tsx main function', () => {
|
||||
originalIsTTY = process.stdin.isTTY;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(process.stdin as any).isTTY = true;
|
||||
process.argv = ['node', 'script.js'];
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -296,11 +298,70 @@ describe('gemini.tsx main function', () => {
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(process.stdin as any).isTTY = originalIsTTY;
|
||||
process.argv = [...originalArgv];
|
||||
|
||||
vi.unstubAllEnvs();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('should fast-path --version without loading settings', async () => {
|
||||
process.argv = ['node', 'script.js', '--version'];
|
||||
|
||||
await main();
|
||||
|
||||
expect(loadSettings).not.toHaveBeenCalled();
|
||||
expect(parseArguments).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should fast-path --help without loading settings', async () => {
|
||||
process.argv = ['node', 'script.js', '--help'];
|
||||
|
||||
await main();
|
||||
|
||||
expect(loadSettings).not.toHaveBeenCalled();
|
||||
expect(parseArguments).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should not relaunch a child process when sandboxing and memory relaunch are unnecessary', async () => {
|
||||
vi.mocked(loadSandboxConfig).mockResolvedValue(undefined);
|
||||
vi.mocked(loadSettings).mockReturnValue(
|
||||
createMockSettings({
|
||||
merged: { advanced: {}, security: { auth: {} }, ui: {} },
|
||||
workspace: { settings: {} },
|
||||
setValue: vi.fn(),
|
||||
forScope: () => ({ settings: {}, originalSettings: {}, path: '' }),
|
||||
}),
|
||||
);
|
||||
vi.mocked(parseArguments).mockResolvedValue({
|
||||
prompt: 'test',
|
||||
} as unknown as CliArgs);
|
||||
vi.mocked(loadCliConfig).mockResolvedValue(
|
||||
createMockConfig({
|
||||
isInteractive: () => false,
|
||||
getQuestion: () => 'test',
|
||||
getSandbox: () => undefined,
|
||||
}),
|
||||
);
|
||||
const processExitSpy = vi
|
||||
.spyOn(process, 'exit')
|
||||
.mockImplementation((code) => {
|
||||
throw new MockProcessExitError(code);
|
||||
});
|
||||
|
||||
try {
|
||||
await main();
|
||||
expect.fail('Should have thrown MockProcessExitError');
|
||||
} catch (e) {
|
||||
expect(e).toBeInstanceOf(MockProcessExitError);
|
||||
expect((e as MockProcessExitError).code).toBe(0);
|
||||
} finally {
|
||||
processExitSpy.mockRestore();
|
||||
}
|
||||
|
||||
const { relaunchAppInChildProcess } = await import('./utils/relaunch.js');
|
||||
expect(relaunchAppInChildProcess).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should log unhandled promise rejections and open debug console on first error', async () => {
|
||||
const processExitSpy = vi
|
||||
.spyOn(process, 'exit')
|
||||
@@ -665,6 +726,42 @@ describe('gemini.tsx main function kitty protocol', () => {
|
||||
processExitSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('should use bootstrap config for the pre-relaunch startup pass', async () => {
|
||||
const mockSettings = createMockSettings({
|
||||
merged: {
|
||||
advanced: {},
|
||||
security: { auth: {} },
|
||||
ui: {},
|
||||
},
|
||||
workspace: { settings: {} },
|
||||
setValue: vi.fn(),
|
||||
forScope: () => ({ settings: {}, originalSettings: {}, path: '' }),
|
||||
});
|
||||
vi.mocked(loadSettings).mockReturnValue(mockSettings);
|
||||
vi.mocked(parseArguments).mockResolvedValue({
|
||||
promptInteractive: false,
|
||||
} as unknown as CliArgs);
|
||||
|
||||
vi.mocked(loadCliConfig).mockResolvedValue(
|
||||
createMockConfig({
|
||||
isInteractive: () => true,
|
||||
getQuestion: () => '',
|
||||
getSandbox: () => undefined,
|
||||
}),
|
||||
);
|
||||
|
||||
await main();
|
||||
|
||||
expect(loadCliConfig).toHaveBeenCalledTimes(2);
|
||||
expect(vi.mocked(loadCliConfig).mock.calls[0][3]).toMatchObject({
|
||||
mode: 'bootstrap',
|
||||
loadedSettings: mockSettings,
|
||||
});
|
||||
expect(vi.mocked(loadCliConfig).mock.calls[1][3]).toMatchObject({
|
||||
loadedSettings: mockSettings,
|
||||
});
|
||||
});
|
||||
|
||||
it('should log warning when theme is not found', async () => {
|
||||
const { themeManager } = await import('./ui/themes/theme-manager.js');
|
||||
const debugLoggerWarnSpy = vi
|
||||
|
||||
+66
-14
@@ -17,6 +17,7 @@ import os from 'node:os';
|
||||
import dns from 'node:dns';
|
||||
import { start_sandbox } from './utils/sandbox.js';
|
||||
import {
|
||||
createDefaultMergedSettings,
|
||||
loadSettings,
|
||||
SettingScope,
|
||||
type DnsResolutionOrder,
|
||||
@@ -119,6 +120,24 @@ import { cleanupBackgroundLogs } from './utils/logCleanup.js';
|
||||
import { SlashCommandConflictHandler } from './services/SlashCommandConflictHandler.js';
|
||||
|
||||
const SLOW_RENDER_MS = 200;
|
||||
const HELP_FLAGS = new Set(['--help', '-h']);
|
||||
const VERSION_FLAGS = new Set(['--version', '-v']);
|
||||
|
||||
function getStartupFastPath(args: string[]): 'help' | 'version' | null {
|
||||
if (args.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (args.every((arg) => HELP_FLAGS.has(arg))) {
|
||||
return 'help';
|
||||
}
|
||||
|
||||
if (args.every((arg) => VERSION_FLAGS.has(arg))) {
|
||||
return 'version';
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export function validateDnsResolutionOrder(
|
||||
order: string | undefined,
|
||||
@@ -341,7 +360,37 @@ export async function startInteractiveUI(
|
||||
registerCleanup(setupTtyCheck());
|
||||
}
|
||||
|
||||
async function runStartupCleanup(
|
||||
config: Config,
|
||||
settings: LoadedSettings,
|
||||
): Promise<void> {
|
||||
const projectTempDir = config.storage.getProjectTempDir();
|
||||
try {
|
||||
await Promise.all([
|
||||
cleanupCheckpoints(projectTempDir),
|
||||
cleanupToolOutputFiles(
|
||||
settings.merged,
|
||||
config.getDebugMode(),
|
||||
projectTempDir,
|
||||
),
|
||||
cleanupBackgroundLogs(),
|
||||
]);
|
||||
} catch (error) {
|
||||
debugLogger.warn('Deferred startup cleanup failed:', error);
|
||||
}
|
||||
}
|
||||
|
||||
export async function main() {
|
||||
const fastPath = getStartupFastPath(process.argv.slice(2));
|
||||
if (fastPath === 'version') {
|
||||
writeToStdout(`${await getVersion()}\n`);
|
||||
return;
|
||||
}
|
||||
if (fastPath === 'help') {
|
||||
await parseArguments(createDefaultMergedSettings());
|
||||
return;
|
||||
}
|
||||
|
||||
const cliStartupHandle = startupProfiler.start('cli_startup');
|
||||
|
||||
// Listen for admin controls from parent process (IPC) in non-sandbox mode. In
|
||||
@@ -375,6 +424,10 @@ export async function main() {
|
||||
coreEvents.emitFeedback('warning', error.message);
|
||||
});
|
||||
|
||||
const parseArgsHandle = startupProfiler.start('parse_arguments');
|
||||
const argv = await parseArguments(settings.merged);
|
||||
parseArgsHandle?.end();
|
||||
|
||||
const trustedFolders = loadTrustedFolders();
|
||||
trustedFolders.errors.forEach((error: TrustedFoldersError) => {
|
||||
coreEvents.emitFeedback(
|
||||
@@ -382,17 +435,6 @@ export async function main() {
|
||||
`Error in ${error.path}: ${error.message}`,
|
||||
);
|
||||
});
|
||||
|
||||
await Promise.all([
|
||||
cleanupCheckpoints(),
|
||||
cleanupToolOutputFiles(settings.merged),
|
||||
cleanupBackgroundLogs(),
|
||||
]);
|
||||
|
||||
const parseArgsHandle = startupProfiler.start('parse_arguments');
|
||||
const argv = await parseArguments(settings.merged);
|
||||
parseArgsHandle?.end();
|
||||
|
||||
if (
|
||||
(argv.allowedTools && argv.allowedTools.length > 0) ||
|
||||
(settings.merged.tools?.allowed && settings.merged.tools.allowed.length > 0)
|
||||
@@ -460,9 +502,13 @@ export async function main() {
|
||||
}
|
||||
}
|
||||
|
||||
const bootstrapConfigHandle = startupProfiler.start('load_bootstrap_config');
|
||||
const partialConfig = await loadCliConfig(settings.merged, sessionId, argv, {
|
||||
mode: 'bootstrap',
|
||||
loadedSettings: settings,
|
||||
projectHooks: settings.workspace.settings.hooks,
|
||||
});
|
||||
bootstrapConfigHandle?.end();
|
||||
adminControlsListner.setConfig(partialConfig);
|
||||
|
||||
// Refresh auth to fetch remote admin settings from CCPA and before entering
|
||||
@@ -572,9 +618,7 @@ export async function main() {
|
||||
);
|
||||
await runExitCleanup();
|
||||
process.exit(ExitCodes.SUCCESS);
|
||||
} else {
|
||||
// Relaunch app so we always have a child process that can be internally
|
||||
// restarted if needed.
|
||||
} else if (memoryArgs.length > 0) {
|
||||
await relaunchAppInChildProcess(memoryArgs, [], remoteAdminSettings);
|
||||
}
|
||||
}
|
||||
@@ -585,6 +629,7 @@ export async function main() {
|
||||
{
|
||||
const loadConfigHandle = startupProfiler.start('load_cli_config');
|
||||
const config = await loadCliConfig(settings.merged, sessionId, argv, {
|
||||
loadedSettings: settings,
|
||||
projectHooks: settings.workspace.settings.hooks,
|
||||
});
|
||||
loadConfigHandle?.end();
|
||||
@@ -662,6 +707,13 @@ export async function main() {
|
||||
process.exit(ExitCodes.SUCCESS);
|
||||
}
|
||||
|
||||
const startupCleanup = runStartupCleanup(config, settings);
|
||||
if (config.isInteractive()) {
|
||||
void startupCleanup;
|
||||
} else {
|
||||
await startupCleanup;
|
||||
}
|
||||
|
||||
const wasRaw = process.stdin.isRaw;
|
||||
if (config.isInteractive() && !wasRaw && process.stdin.isTTY) {
|
||||
// Set this as early as possible to avoid spurious characters from
|
||||
|
||||
@@ -796,7 +796,7 @@ export const AppContainer = (props: AppContainerProps) => {
|
||||
Logging in with Google... Restarting Gemini CLI to continue.
|
||||
----------------------------------------------------------------
|
||||
`);
|
||||
await relaunchApp();
|
||||
await relaunchApp(config.getRemoteAdminSettings());
|
||||
}
|
||||
}
|
||||
setAuthState(AuthState.Authenticated);
|
||||
@@ -2478,16 +2478,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
onHintClear: () => {},
|
||||
onHintSubmit: () => {},
|
||||
handleRestart: async () => {
|
||||
if (process.send) {
|
||||
const remoteSettings = config.getRemoteAdminSettings();
|
||||
if (remoteSettings) {
|
||||
process.send({
|
||||
type: 'admin-settings-update',
|
||||
settings: remoteSettings,
|
||||
});
|
||||
}
|
||||
}
|
||||
await relaunchApp();
|
||||
await relaunchApp(config.getRemoteAdminSettings());
|
||||
},
|
||||
handleNewAgentsSelect: async (choice: NewAgentsChoice) => {
|
||||
if (newAgents && choice === NewAgentsChoice.ACKNOWLEDGE) {
|
||||
|
||||
@@ -22,9 +22,8 @@ import { AuthState } from '../types.js';
|
||||
import { RadioButtonSelect } from '../components/shared/RadioButtonSelect.js';
|
||||
import { useKeypress } from '../hooks/useKeypress.js';
|
||||
import { validateAuthMethodWithSettings } from './useAuth.js';
|
||||
import { runExitCleanup } from '../../utils/cleanup.js';
|
||||
import { Text } from 'ink';
|
||||
import { RELAUNCH_EXIT_CODE } from '../../utils/processUtils.js';
|
||||
import { relaunchApp } from '../../utils/processUtils.js';
|
||||
|
||||
// Mocks
|
||||
vi.mock('@google/gemini-cli-core', async (importOriginal) => {
|
||||
@@ -36,14 +35,14 @@ vi.mock('@google/gemini-cli-core', async (importOriginal) => {
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock('../../utils/cleanup.js', () => ({
|
||||
runExitCleanup: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('./useAuth.js', () => ({
|
||||
validateAuthMethodWithSettings: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../../utils/processUtils.js', () => ({
|
||||
relaunchApp: vi.fn().mockResolvedValue(undefined),
|
||||
}));
|
||||
|
||||
vi.mock('../hooks/useKeypress.js', () => ({
|
||||
useKeypress: vi.fn(),
|
||||
}));
|
||||
@@ -64,7 +63,7 @@ vi.mock('../components/shared/RadioButtonSelect.js', () => ({
|
||||
const mockedUseKeypress = useKeypress as Mock;
|
||||
const mockedRadioButtonSelect = RadioButtonSelect as Mock;
|
||||
const mockedValidateAuthMethod = validateAuthMethodWithSettings as Mock;
|
||||
const mockedRunExitCleanup = runExitCleanup as Mock;
|
||||
const mockedRelaunchApp = relaunchApp as Mock;
|
||||
|
||||
describe('AuthDialog', () => {
|
||||
let props: {
|
||||
@@ -85,6 +84,7 @@ describe('AuthDialog', () => {
|
||||
props = {
|
||||
config: {
|
||||
isBrowserLaunchSuppressed: vi.fn().mockReturnValue(false),
|
||||
getRemoteAdminSettings: vi.fn().mockReturnValue(undefined),
|
||||
} as unknown as Config,
|
||||
settings: {
|
||||
merged: {
|
||||
@@ -351,11 +351,8 @@ describe('AuthDialog', () => {
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('exits process for Sign in with Google when browser is suppressed', async () => {
|
||||
it('restarts for Sign in with Google when browser is suppressed', async () => {
|
||||
vi.useFakeTimers();
|
||||
const exitSpy = vi
|
||||
.spyOn(process, 'exit')
|
||||
.mockImplementation(() => undefined as never);
|
||||
const logSpy = vi.spyOn(debugLogger, 'log').mockImplementation(() => {});
|
||||
vi.mocked(props.config.isBrowserLaunchSuppressed).mockReturnValue(true);
|
||||
mockedValidateAuthMethod.mockReturnValue(null);
|
||||
@@ -371,10 +368,8 @@ describe('AuthDialog', () => {
|
||||
await vi.runAllTimersAsync();
|
||||
});
|
||||
|
||||
expect(mockedRunExitCleanup).toHaveBeenCalled();
|
||||
expect(exitSpy).toHaveBeenCalledWith(RELAUNCH_EXIT_CODE);
|
||||
expect(mockedRelaunchApp).toHaveBeenCalledWith(undefined);
|
||||
|
||||
exitSpy.mockRestore();
|
||||
logSpy.mockRestore();
|
||||
vi.useRealTimers();
|
||||
unmount();
|
||||
|
||||
@@ -132,7 +132,9 @@ export function AuthDialog({
|
||||
config.isBrowserLaunchSuppressed()
|
||||
) {
|
||||
setExiting(true);
|
||||
setTimeout(relaunchApp, 100);
|
||||
setTimeout(async () => {
|
||||
await relaunchApp(config.getRemoteAdminSettings());
|
||||
}, 100);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -8,30 +8,23 @@ import { render } from '../../test-utils/render.js';
|
||||
import { describe, it, expect, vi, beforeEach, type Mock } from 'vitest';
|
||||
import { LoginWithGoogleRestartDialog } from './LoginWithGoogleRestartDialog.js';
|
||||
import { useKeypress } from '../hooks/useKeypress.js';
|
||||
import { runExitCleanup } from '../../utils/cleanup.js';
|
||||
import {
|
||||
RELAUNCH_EXIT_CODE,
|
||||
_resetRelaunchStateForTesting,
|
||||
} from '../../utils/processUtils.js';
|
||||
import { type Config } from '@google/gemini-cli-core';
|
||||
import { relaunchApp } from '../../utils/processUtils.js';
|
||||
|
||||
// Mocks
|
||||
vi.mock('../hooks/useKeypress.js', () => ({
|
||||
useKeypress: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../../utils/cleanup.js', () => ({
|
||||
runExitCleanup: vi.fn(),
|
||||
vi.mock('../../utils/processUtils.js', () => ({
|
||||
relaunchApp: vi.fn().mockResolvedValue(undefined),
|
||||
}));
|
||||
|
||||
const mockedUseKeypress = useKeypress as Mock;
|
||||
const mockedRunExitCleanup = runExitCleanup as Mock;
|
||||
const mockedRelaunchApp = relaunchApp as Mock;
|
||||
|
||||
describe('LoginWithGoogleRestartDialog', () => {
|
||||
const onDismiss = vi.fn();
|
||||
const exitSpy = vi
|
||||
.spyOn(process, 'exit')
|
||||
.mockImplementation(() => undefined as never);
|
||||
|
||||
const mockConfig = {
|
||||
getRemoteAdminSettings: vi.fn(),
|
||||
@@ -39,9 +32,7 @@ describe('LoginWithGoogleRestartDialog', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
exitSpy.mockClear();
|
||||
vi.useRealTimers();
|
||||
_resetRelaunchStateForTesting();
|
||||
});
|
||||
|
||||
it('renders correctly', async () => {
|
||||
@@ -78,36 +69,33 @@ describe('LoginWithGoogleRestartDialog', () => {
|
||||
unmount();
|
||||
});
|
||||
|
||||
it.each(['r', 'R'])(
|
||||
'calls runExitCleanup and process.exit when %s is pressed',
|
||||
async (keyName) => {
|
||||
vi.useFakeTimers();
|
||||
it.each(['r', 'R'])('restarts when %s is pressed', async (keyName) => {
|
||||
vi.useFakeTimers();
|
||||
|
||||
const { waitUntilReady, unmount } = render(
|
||||
<LoginWithGoogleRestartDialog
|
||||
onDismiss={onDismiss}
|
||||
config={mockConfig}
|
||||
/>,
|
||||
);
|
||||
await waitUntilReady();
|
||||
const keypressHandler = mockedUseKeypress.mock.calls[0][0];
|
||||
const { waitUntilReady, unmount } = render(
|
||||
<LoginWithGoogleRestartDialog
|
||||
onDismiss={onDismiss}
|
||||
config={mockConfig}
|
||||
/>,
|
||||
);
|
||||
await waitUntilReady();
|
||||
const keypressHandler = mockedUseKeypress.mock.calls[0][0];
|
||||
|
||||
keypressHandler({
|
||||
name: keyName,
|
||||
shift: false,
|
||||
ctrl: false,
|
||||
cmd: false,
|
||||
sequence: keyName,
|
||||
});
|
||||
keypressHandler({
|
||||
name: keyName,
|
||||
shift: false,
|
||||
ctrl: false,
|
||||
cmd: false,
|
||||
sequence: keyName,
|
||||
});
|
||||
|
||||
// Advance timers to trigger the setTimeout callback
|
||||
await vi.runAllTimersAsync();
|
||||
// Advance timers to trigger the setTimeout callback
|
||||
await vi.runAllTimersAsync();
|
||||
|
||||
expect(mockedRunExitCleanup).toHaveBeenCalledTimes(1);
|
||||
expect(exitSpy).toHaveBeenCalledWith(RELAUNCH_EXIT_CODE);
|
||||
expect(mockedRelaunchApp).toHaveBeenCalledTimes(1);
|
||||
expect(mockedRelaunchApp).toHaveBeenCalledWith(undefined);
|
||||
|
||||
vi.useRealTimers();
|
||||
unmount();
|
||||
},
|
||||
);
|
||||
vi.useRealTimers();
|
||||
unmount();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -26,16 +26,7 @@ export const LoginWithGoogleRestartDialog = ({
|
||||
return true;
|
||||
} else if (key.name === 'r' || key.name === 'R') {
|
||||
setTimeout(async () => {
|
||||
if (process.send) {
|
||||
const remoteSettings = config.getRemoteAdminSettings();
|
||||
if (remoteSettings) {
|
||||
process.send({
|
||||
type: 'admin-settings-update',
|
||||
settings: remoteSettings,
|
||||
});
|
||||
}
|
||||
}
|
||||
await relaunchApp();
|
||||
await relaunchApp(config.getRemoteAdminSettings());
|
||||
}, 100);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -231,7 +231,9 @@ export const DialogManager = ({
|
||||
<Box flexDirection="column">
|
||||
<SettingsDialog
|
||||
onSelect={() => uiActions.closeSettingsDialog()}
|
||||
onRestartRequest={relaunchApp}
|
||||
onRestartRequest={async () => {
|
||||
await relaunchApp(config.getRemoteAdminSettings());
|
||||
}}
|
||||
availableTerminalHeight={terminalHeight - staticExtraHeight}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
@@ -103,6 +103,21 @@ async function drainStdin() {
|
||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||
}
|
||||
|
||||
export async function cleanupCheckpoints(projectTempDir?: string) {
|
||||
let tempDir = projectTempDir;
|
||||
if (!tempDir) {
|
||||
const storage = new Storage(process.cwd());
|
||||
await storage.initialize();
|
||||
tempDir = storage.getProjectTempDir();
|
||||
}
|
||||
const checkpointsDir = join(tempDir, 'checkpoints');
|
||||
try {
|
||||
await fs.rm(checkpointsDir, { recursive: true, force: true });
|
||||
} catch {
|
||||
// Ignore errors if the directory doesn't exist or fails to delete.
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gracefully shuts down the process, ensuring cleanup runs exactly once.
|
||||
* Guards against concurrent shutdown from signals (SIGHUP, SIGTERM, SIGINT)
|
||||
@@ -161,15 +176,3 @@ export function setupTtyCheck(): () => void {
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export async function cleanupCheckpoints() {
|
||||
const storage = new Storage(process.cwd());
|
||||
await storage.initialize();
|
||||
const tempDir = storage.getProjectTempDir();
|
||||
const checkpointsDir = join(tempDir, 'checkpoints');
|
||||
try {
|
||||
await fs.rm(checkpointsDir, { recursive: true, force: true });
|
||||
} catch {
|
||||
// Ignore errors if the directory doesn't exist or fails to delete.
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,28 +11,56 @@ import {
|
||||
_resetRelaunchStateForTesting,
|
||||
} from './processUtils.js';
|
||||
import * as cleanup from './cleanup.js';
|
||||
import * as relaunch from './relaunch.js';
|
||||
import * as handleAutoUpdate from './handleAutoUpdate.js';
|
||||
|
||||
vi.mock('./handleAutoUpdate.js', () => ({
|
||||
waitForUpdateCompletion: vi.fn().mockResolvedValue(undefined),
|
||||
}));
|
||||
|
||||
vi.mock('./relaunch.js', () => ({
|
||||
relaunchAppInChildProcess: vi.fn().mockResolvedValue(undefined),
|
||||
}));
|
||||
|
||||
describe('processUtils', () => {
|
||||
const processExit = vi
|
||||
.spyOn(process, 'exit')
|
||||
.mockReturnValue(undefined as never);
|
||||
const runExitCleanup = vi.spyOn(cleanup, 'runExitCleanup');
|
||||
const relaunchAppInChildProcess = vi.spyOn(
|
||||
relaunch,
|
||||
'relaunchAppInChildProcess',
|
||||
);
|
||||
|
||||
beforeEach(() => {
|
||||
_resetRelaunchStateForTesting();
|
||||
});
|
||||
|
||||
afterEach(() => vi.clearAllMocks());
|
||||
afterEach(() => {
|
||||
delete process.env['SANDBOX'];
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
delete (process as any).send;
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should wait for updates, run cleanup, and exit with the relaunch code', async () => {
|
||||
await relaunchApp();
|
||||
it('should wait for updates, run cleanup, and exit with the relaunch code when a parent wrapper exists', async () => {
|
||||
process.env['SANDBOX'] = 'sandbox-exec';
|
||||
processExit.mockImplementationOnce(() => {
|
||||
throw new Error('PROCESS_EXIT_CALLED');
|
||||
});
|
||||
|
||||
await expect(relaunchApp()).rejects.toThrow('PROCESS_EXIT_CALLED');
|
||||
expect(handleAutoUpdate.waitForUpdateCompletion).toHaveBeenCalledTimes(1);
|
||||
expect(runExitCleanup).toHaveBeenCalledTimes(1);
|
||||
expect(processExit).toHaveBeenCalledWith(RELAUNCH_EXIT_CODE);
|
||||
expect(relaunchAppInChildProcess).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should spawn a child wrapper on demand when no parent wrapper exists', async () => {
|
||||
await relaunchApp();
|
||||
expect(handleAutoUpdate.waitForUpdateCompletion).toHaveBeenCalledTimes(1);
|
||||
expect(runExitCleanup).toHaveBeenCalledTimes(1);
|
||||
expect(relaunchAppInChildProcess).toHaveBeenCalledWith([], [], undefined);
|
||||
expect(processExit).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type { AdminControlsSettings } from '@google/gemini-cli-core';
|
||||
import { runExitCleanup } from './cleanup.js';
|
||||
import { waitForUpdateCompletion } from './handleAutoUpdate.js';
|
||||
|
||||
@@ -13,7 +14,8 @@ import { waitForUpdateCompletion } from './handleAutoUpdate.js';
|
||||
export const RELAUNCH_EXIT_CODE = 199;
|
||||
|
||||
/**
|
||||
* Exits the process with a special code to signal that the parent process should relaunch it.
|
||||
* Restarts the CLI, either by signaling an existing parent wrapper or by
|
||||
* spawning one on demand.
|
||||
*/
|
||||
let isRelaunching = false;
|
||||
|
||||
@@ -22,10 +24,24 @@ export function _resetRelaunchStateForTesting(): void {
|
||||
isRelaunching = false;
|
||||
}
|
||||
|
||||
export async function relaunchApp(): Promise<void> {
|
||||
export async function relaunchApp(
|
||||
remoteAdminSettings?: AdminControlsSettings,
|
||||
): Promise<void> {
|
||||
if (isRelaunching) return;
|
||||
isRelaunching = true;
|
||||
await waitForUpdateCompletion();
|
||||
await runExitCleanup();
|
||||
process.exit(RELAUNCH_EXIT_CODE);
|
||||
|
||||
if (process.send || process.env['SANDBOX']) {
|
||||
if (process.send && remoteAdminSettings) {
|
||||
process.send({
|
||||
type: 'admin-settings-update',
|
||||
settings: remoteAdminSettings,
|
||||
});
|
||||
}
|
||||
process.exit(RELAUNCH_EXIT_CODE);
|
||||
}
|
||||
|
||||
const { relaunchAppInChildProcess } = await import('./relaunch.js');
|
||||
await relaunchAppInChildProcess([], [], remoteAdminSettings);
|
||||
}
|
||||
|
||||
@@ -614,6 +614,7 @@ export interface ConfigParameters {
|
||||
disabledSkills?: string[];
|
||||
adminSkillsEnabled?: boolean;
|
||||
experimentalJitContext?: boolean;
|
||||
deferInitialMemoryLoad?: boolean;
|
||||
toolOutputMasking?: Partial<ToolOutputMaskingConfig>;
|
||||
disableLLMCorrection?: boolean;
|
||||
plan?: boolean;
|
||||
@@ -832,6 +833,7 @@ export class Config implements McpContext, AgentLoopContext {
|
||||
private readonly adminSkillsEnabled: boolean;
|
||||
|
||||
private readonly experimentalJitContext: boolean;
|
||||
private readonly deferInitialMemoryLoad: boolean;
|
||||
private readonly disableLLMCorrection: boolean;
|
||||
private readonly planEnabled: boolean;
|
||||
private readonly trackerEnabled: boolean;
|
||||
@@ -934,6 +936,7 @@ export class Config implements McpContext, AgentLoopContext {
|
||||
this.adminSkillsEnabled = params.adminSkillsEnabled ?? true;
|
||||
this.modelAvailabilityService = new ModelAvailabilityService();
|
||||
this.experimentalJitContext = params.experimentalJitContext ?? false;
|
||||
this.deferInitialMemoryLoad = params.deferInitialMemoryLoad ?? false;
|
||||
this.modelSteering = params.modelSteering ?? false;
|
||||
this.userHintService = new UserHintService(() =>
|
||||
this.isModelSteeringEnabled(),
|
||||
@@ -1174,6 +1177,23 @@ export class Config implements McpContext, AgentLoopContext {
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.getExtensionLoader().isLoaded()) {
|
||||
const extensionLoadHandle = startupProfiler.start('load_extensions');
|
||||
await this.getExtensionLoader().ensureLoaded();
|
||||
extensionLoadHandle?.end();
|
||||
} else {
|
||||
await this.getExtensionLoader().ensureLoaded();
|
||||
}
|
||||
|
||||
if (this.deferInitialMemoryLoad) {
|
||||
const memoryLoadHandle = startupProfiler.start('load_memory');
|
||||
const { refreshServerHierarchicalMemory } = await import(
|
||||
'../utils/memoryDiscovery.js'
|
||||
);
|
||||
await refreshServerHierarchicalMemory(this);
|
||||
memoryLoadHandle?.end();
|
||||
}
|
||||
|
||||
// Initialize centralized FileDiscoveryService
|
||||
const discoverToolsHandle = startupProfiler.start('discover_tools');
|
||||
this.getFileService();
|
||||
|
||||
@@ -24,6 +24,19 @@ export abstract class ExtensionLoader {
|
||||
|
||||
constructor(private readonly eventEmitter?: EventEmitter<ExtensionEvents>) {}
|
||||
|
||||
isLoaded(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures extension metadata is available before initialization continues.
|
||||
*
|
||||
* Most implementations have nothing to do here, but loaders backed by disk
|
||||
* discovery can override this to defer filesystem work until after first
|
||||
* render.
|
||||
*/
|
||||
async ensureLoaded(): Promise<void> {}
|
||||
|
||||
/**
|
||||
* All currently known extensions, both active and inactive.
|
||||
*/
|
||||
|
||||
@@ -17,25 +17,38 @@ export interface PtyProcess {
|
||||
kill(signal?: string): void;
|
||||
}
|
||||
|
||||
let ptyImplementationPromise: Promise<PtyImplementation> | undefined;
|
||||
|
||||
export const getPty = async (): Promise<PtyImplementation> => {
|
||||
if (process.env['GEMINI_PTY_INFO'] === 'child_process') {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
const lydell = '@lydell/node-pty';
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
const module = await import(lydell);
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
return { module, name: 'lydell-node-pty' };
|
||||
} catch (_e) {
|
||||
try {
|
||||
const nodePty = 'node-pty';
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
const module = await import(nodePty);
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
return { module, name: 'node-pty' };
|
||||
} catch (_e2) {
|
||||
return null;
|
||||
}
|
||||
if (!ptyImplementationPromise) {
|
||||
ptyImplementationPromise = (async () => {
|
||||
try {
|
||||
const lydell = '@lydell/node-pty';
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
const module = await import(lydell);
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
return { module, name: 'lydell-node-pty' };
|
||||
} catch (_e) {
|
||||
try {
|
||||
const nodePty = 'node-pty';
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
const module = await import(nodePty);
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
return { module, name: 'node-pty' };
|
||||
} catch (_e2) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
})();
|
||||
}
|
||||
|
||||
return ptyImplementationPromise;
|
||||
};
|
||||
|
||||
/** For testing purposes only. */
|
||||
export function resetPtyCache(): void {
|
||||
ptyImplementationPromise = undefined;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user