Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

- A new `deprioritize` setting in `codegraph.json` keeps the paths you name from outranking your product code in search and `codegraph_explore` answers, without removing anything from the index. It takes gitignore-style patterns just like `exclude`, but is ranking-only: helper-script trees, generated output, or optional add-on directories whose generic symbol names (`usage`, `run`, `status`) would otherwise crowd out the code that actually answers a query stay fully indexed and findable — and a query that genuinely targets such a tree still returns it. Thanks @maxmilian. (#982)

- `codegraph install --init` wires up your agents and builds the current project's index in one command, and `codegraph init --yes` runs without any prompts — so a fresh container or CI job can bootstrap CodeGraph with a single non-interactive line (`codegraph install --yes --init`). The installer still never indexes anything unless you ask for it with the flag, and the usual safety refusal for a home directory or filesystem root applies. (#1578)

### Fixes

- Indexing no longer hangs on a Swift Vapor project containing a call with a long argument list. A single `.get(...)`-style call with many labeled arguments and no `use:` handler — the shape generated request builders produce — could stall `codegraph index`, `codegraph sync`, and the MCP server indefinitely. Route detection now handles such files in milliseconds, and every previously-recognized route shape still parses exactly as before. Thanks @maxmilian. (#1544) (Swift)
Expand Down
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -388,6 +388,7 @@ The installer **wires up your agents only — it does not index your code.** Aft

```bash
codegraph install --yes # auto-detect agents, install global
codegraph install --yes --init # same, then build the current project's index (one-shot bootstrap)
codegraph install --target=cursor,claude --yes # explicit target list
codegraph install --target=auto --location=local # detected agents, project-local
codegraph install --target=copilot-vscode,copilot-cli,copilot-jetbrains --yes # GitHub Copilot everywhere
Expand All @@ -400,6 +401,7 @@ codegraph install --print-config copilot-vscode # same, for Copilot in VS C
| `--target` | `auto`, `all`, `none`, or csv (`claude,cursor,...`) | prompt |
| `--location` | `global`, `local` | prompt |
| `--yes` | (boolean) | prompt every step |
| `--init` | (boolean) run `codegraph init` in the current directory after wiring agents | — |
| `--no-permissions` | (boolean) skip Claude auto-allow list | permissions on |
| `--print-config <id>` | dump snippet for one agent and exit | — |

Expand All @@ -414,7 +416,7 @@ cd your-project
codegraph init
```

Builds the per-project knowledge graph index, which then auto-syncs on every file change. A single global `codegraph install` works in every project you open — no need to re-run the installer per project.
Builds the per-project knowledge graph index, which then auto-syncs on every file change. A single global `codegraph install` works in every project you open — no need to re-run the installer per project. Add `--yes` to skip every prompt (scripts / CI / container bootstraps).

That's it — your agent will use CodeGraph tools automatically when a `.codegraph/` directory exists.

Expand Down
108 changes: 108 additions & 0 deletions __tests__/cli-install-init.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
/**
* `codegraph install --init` and `codegraph init --yes` (#1578): the one-shot,
* non-interactive "wire agents + build this project's index" bootstrap a fresh
* container / CI job needs.
*
* Exercised end-to-end against the built binary so the CLI wiring (the shared
* `runInit` flow, the flag plumbing, exit codes) is what's covered. Every run
* uses `--target none`, so the installer touches no agent config on the
* machine running the suite; the only side effect is the temp project's
* `.codegraph/`.
*/

import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { execFileSync } from 'child_process';
import * as fs from 'fs';
import * as path from 'path';
import * as os from 'os';

const BIN = path.resolve(__dirname, '../dist/bin/codegraph.js');

interface RunResult {
status: number;
stdout: string;
stderr: string;
}

/** Run the CLI with stdin closed — a prompt that blocks would hang / fail here. */
function runCodegraph(args: string[], cwd: string): RunResult {
try {
const stdout = execFileSync(process.execPath, [BIN, ...args], {
cwd,
encoding: 'utf-8',
env: {
...process.env,
CODEGRAPH_NO_DAEMON: '1',
CODEGRAPH_TELEMETRY: '0',
DO_NOT_TRACK: '1',
NO_COLOR: '1',
},
stdio: ['ignore', 'pipe', 'pipe'],
timeout: 120_000,
});
return { status: 0, stdout, stderr: '' };
} catch (err) {
const e = err as { status?: number | null; stdout?: string | Buffer; stderr?: string | Buffer };
return {
status: e.status ?? -1,
stdout: String(e.stdout ?? ''),
stderr: String(e.stderr ?? ''),
};
}
}

describe('codegraph install --init / init --yes (#1578)', () => {
let tempDir: string;

beforeEach(() => {
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-install-init-'));
fs.writeFileSync(
path.join(tempDir, 'a.ts'),
`export function greet(name: string) { return hello(name); }\n` +
`export function hello(n: string) { return 'hi ' + n; }\n`,
);
});

afterEach(() => {
fs.rmSync(tempDir, { recursive: true, force: true });
});

it('install --yes --target none --init builds the current project\'s index in one command', () => {
const r = runCodegraph(['install', '--yes', '--target', 'none', '--init'], tempDir);
expect(r.status, r.stdout + r.stderr).toBe(0);
// The installer ran (and had nothing to wire) …
expect(r.stdout).toContain('No agent targets selected');
// … and the init ran afterwards, in cwd.
expect(r.stdout).toContain(`Initialized in ${fs.realpathSync(tempDir)}`);
expect(fs.existsSync(path.join(tempDir, '.codegraph', 'codegraph.db'))).toBe(true);
});

it('install --init on an already-initialized project reports that and still exits 0', () => {
expect(runCodegraph(['init', '--yes'], tempDir).status).toBe(0);
const r = runCodegraph(['install', '--yes', '--target', 'none', '--init'], tempDir);
expect(r.status, r.stdout + r.stderr).toBe(0);
expect(r.stdout).toContain('Already initialized');
});

it('install --init refuses an unsafe root (filesystem root) with exit code 1, like init does', () => {
// `/` (or the drive root on Windows) is the canonical unsafe root: the
// refusal fires before anything is created, so nothing is written there.
const root = path.parse(process.cwd()).root;
const r = runCodegraph(['install', '--yes', '--target', 'none', '--init'], root);
expect(r.status).toBe(1);
expect(r.stdout).toContain('Refusing to initialize');
expect(fs.existsSync(path.join(root, '.codegraph'))).toBe(false);
});

it('init --yes runs non-interactively with stdin closed and builds the index', () => {
const r = runCodegraph(['init', '--yes'], tempDir);
expect(r.status, r.stdout + r.stderr).toBe(0);
expect(r.stdout).toContain('Initialized in');
expect(fs.existsSync(path.join(tempDir, '.codegraph', 'codegraph.db'))).toBe(true);
});

it('documents the new flags in --help', () => {
expect(runCodegraph(['init', '--help'], tempDir).stdout).toMatch(/-y, --yes\b/);
expect(runCodegraph(['install', '--help'], tempDir).stdout).toMatch(/-i, --init\b/);
});
});
189 changes: 110 additions & 79 deletions src/bin/codegraph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -607,94 +607,111 @@ async function recordIndexTelemetry(
// =============================================================================

/**
* codegraph init [path]
* The `init` flow — shared by `codegraph init` and `codegraph install --init`
* (#1578): refuse an unsafe root, create `.codegraph/`, build the initial
* index under supervision, then the post-index offers. `yes` makes every
* offer non-interactive (defaults only), so a container / CI bootstrap never
* blocks on a prompt. An unsafe root sets `process.exitCode = 1` and returns
* (no `--force` is implied by any caller); an index failure exits 1.
*/
program
.command('init [path]')
.description('Initialize CodeGraph in a project directory and build the initial index')
.option('-i, --index', 'Deprecated: indexing now runs by default; flag accepted for backward compatibility')
.option('-f, --force', 'Initialize even if the path looks like your home directory or a filesystem root')
.option('-v, --verbose', 'Show detailed worker lifecycle and memory info')
.action(async (pathArg: string | undefined, options: { index?: boolean; force?: boolean; verbose?: boolean }) => {
const projectPath = path.resolve(pathArg || process.cwd());
const clack = await importESM('@clack/prompts');

clack.intro('Initializing CodeGraph');

try {
// Refuse to index your home directory / a filesystem root — it pulls in
// caches, other projects, and your whole tree (a multi-GB index + watcher
// churn, and on pre-1.0 macOS a machine-crashing fd blowup, #845).
const unsafe = unsafeIndexRootReason(projectPath);
if (unsafe && !options.force) {
clack.log.error(`Refusing to initialize in ${projectPath} — it looks like ${unsafe}.`);
clack.log.info('Run this inside a specific project directory, or pass --force if you really mean to index everything under it.');
clack.outro('');
process.exitCode = 1;
return;
}

if (isInitialized(projectPath)) {
clack.log.warn(`Already initialized in ${projectPath}`);
clack.log.info('Use "codegraph index" to re-index or "codegraph sync" to update');
try {
const { offerWatchFallback } = await import('../installer');
await offerWatchFallback(clack, projectPath);
} catch { /* non-fatal */ }
clack.outro('');
return;
}
async function runInit(
projectPath: string,
options: { index?: boolean; force?: boolean; verbose?: boolean; yes?: boolean },
): Promise<void> {
const clack = await importESM('@clack/prompts');

const { default: CodeGraph, getDatabasePath } = await loadCodeGraph();
const cg = await CodeGraph.init(projectPath, { index: false });
clack.log.success(`Initialized in ${projectPath}`);
clack.intro('Initializing CodeGraph');

// Indexing runs by default now. The legacy -i/--index flag is still
// accepted (so existing muscle memory and scripts don't break) but is a
// no-op — initializing always builds the initial index.
// Supervise the index: self-terminate if orphaned or wedged (#999).
// The DB + WAL paths let the liveness watchdog tell a slow store on
// degraded storage from a true wedge (#1231).
// A closure so we can re-run the exact same supervised, progress-rendered
// index if the user opts gitignored child repos in below (#1156).
const dbPath = getDatabasePath(projectPath);
const runIndex = async (): Promise<IndexResult> => {
const supervision = installCommandSupervision('init', { progressPaths: [dbPath, `${dbPath}-wal`] });
try {
if (options.verbose) {
return await cg.indexAll({ onProgress: createVerboseProgress(), verbose: true });
}
process.stdout.write(`${colors.dim}${getGlyphs().rail}${colors.reset}\n`);
const progress = createShimmerProgress();
const r = await cg.indexAll({ onProgress: progress.onProgress });
await progress.stop();
return r;
} finally {
supervision.stop();
}
};
const result = await runIndex();
printIndexResult(clack, result, projectPath);
await recordIndexTelemetry(cg, result);

// An empty graph at a git super-repo usually means `.gitignore` excludes
// the child repos that hold the code — surface them and offer to opt in
// rather than leaving the user with a silent 0-node "Done". (#1156)
if (result.nodesCreated === 0) {
await offerIndexIgnoredRepos(clack, projectPath, runIndex, { interactive: true });
}
try {
// Refuse to index your home directory / a filesystem root — it pulls in
// caches, other projects, and your whole tree (a multi-GB index + watcher
// churn, and on pre-1.0 macOS a machine-crashing fd blowup, #845).
const unsafe = unsafeIndexRootReason(projectPath);
if (unsafe && !options.force) {
clack.log.error(`Refusing to initialize in ${projectPath} — it looks like ${unsafe}.`);
clack.log.info('Run this inside a specific project directory, or pass --force if you really mean to index everything under it.');
clack.outro('');
process.exitCode = 1;
return;
}

if (isInitialized(projectPath)) {
clack.log.warn(`Already initialized in ${projectPath}`);
clack.log.info('Use "codegraph index" to re-index or "codegraph sync" to update');
try {
const { offerWatchFallback } = await import('../installer');
await offerWatchFallback(clack, projectPath);
await offerWatchFallback(clack, projectPath, { yes: options.yes });
} catch { /* non-fatal */ }
clack.outro('');
return;
}

clack.outro('Done');
cg.destroy();
} catch (err) {
clack.log.error(`Failed: ${err instanceof Error ? err.message : String(err)}`);
process.exit(1);
const { default: CodeGraph, getDatabasePath } = await loadCodeGraph();
const cg = await CodeGraph.init(projectPath, { index: false });
clack.log.success(`Initialized in ${projectPath}`);

// Indexing runs by default now. The legacy -i/--index flag is still
// accepted (so existing muscle memory and scripts don't break) but is a
// no-op — initializing always builds the initial index.
// Supervise the index: self-terminate if orphaned or wedged (#999).
// The DB + WAL paths let the liveness watchdog tell a slow store on
// degraded storage from a true wedge (#1231).
// A closure so we can re-run the exact same supervised, progress-rendered
// index if the user opts gitignored child repos in below (#1156).
const dbPath = getDatabasePath(projectPath);
const runIndex = async (): Promise<IndexResult> => {
const supervision = installCommandSupervision('init', { progressPaths: [dbPath, `${dbPath}-wal`] });
try {
if (options.verbose) {
return await cg.indexAll({ onProgress: createVerboseProgress(), verbose: true });
}
process.stdout.write(`${colors.dim}${getGlyphs().rail}${colors.reset}\n`);
const progress = createShimmerProgress();
const r = await cg.indexAll({ onProgress: progress.onProgress });
await progress.stop();
return r;
} finally {
supervision.stop();
}
};
const result = await runIndex();
printIndexResult(clack, result, projectPath);
await recordIndexTelemetry(cg, result);

// An empty graph at a git super-repo usually means `.gitignore` excludes
// the child repos that hold the code — surface them and offer to opt in
// rather than leaving the user with a silent 0-node "Done". (#1156)
// Under --yes the offer prints its one-line opt-in snippet instead of
// prompting (same as a non-TTY run).
if (result.nodesCreated === 0) {
await offerIndexIgnoredRepos(clack, projectPath, runIndex, { interactive: !options.yes });
}

try {
const { offerWatchFallback } = await import('../installer');
await offerWatchFallback(clack, projectPath, { yes: options.yes });
} catch { /* non-fatal */ }

clack.outro('Done');
cg.destroy();
} catch (err) {
clack.log.error(`Failed: ${err instanceof Error ? err.message : String(err)}`);
process.exit(1);
}
}

/**
* codegraph init [path]
*/
program
.command('init [path]')
.description('Initialize CodeGraph in a project directory and build the initial index')
.option('-i, --index', 'Deprecated: indexing now runs by default; flag accepted for backward compatibility')
.option('-f, --force', 'Initialize even if the path looks like your home directory or a filesystem root')
.option('-v, --verbose', 'Show detailed worker lifecycle and memory info')
.option('-y, --yes', 'Non-interactive: skip every prompt and take the defaults (for scripts / CI / container bootstraps)')
.action(async (pathArg: string | undefined, options: { index?: boolean; force?: boolean; verbose?: boolean; yes?: boolean }) => {
await runInit(path.resolve(pathArg || process.cwd()), options);
});

/**
Expand Down Expand Up @@ -2268,13 +2285,15 @@ program
.option('-t, --target <ids>', 'Target agent(s): comma-separated ids, or "auto"|"all"|"none". Default: prompt')
.option('-l, --location <where>', 'Install location: "global" or "local". Default: prompt')
.option('-y, --yes', 'Non-interactive: defaults to --location=global --target=auto, auto-allow on')
.option('-i, --init', 'After wiring agents, also run `codegraph init` in the current directory — builds this project’s index, so install + index is one command (combine with --yes for an unattended bootstrap)')
.option('--no-permissions', 'Skip writing the auto-allow permissions list (Claude Code only)')
.option('--print-config <id>', 'Print MCP config snippet for the named agent and exit (no file writes)')
.option('--refresh', 'Rewrite what previous installs configured, for already-configured agents only (never adds new ones). Run automatically by `codegraph upgrade`')
.action(async (opts: {
target?: string;
location?: string;
yes?: boolean;
init?: boolean;
permissions?: boolean;
printConfig?: string;
refresh?: boolean;
Expand Down Expand Up @@ -2352,6 +2371,18 @@ program
error(err instanceof Error ? err.message : String(err));
process.exit(1);
}

// --init: the one-shot "wire agents AND build this project's index"
// bootstrap (#1578). The installer itself never indexes implicitly (a
// surprise index of $HOME is the thing we refuse) — an explicit flag is
// the user choosing. Runs after a successful install, including the
// `--target none` / nothing-detected case (the installer returns normally
// there), and shares every guard with `codegraph init`: an unsafe root
// is refused (exit 1, no implied --force), an already-initialized
// project just says so. `--yes` flows through so no offer prompts.
if (opts.init) {
await runInit(process.cwd(), { yes: opts.yes });
}
});

/**
Expand Down
5 changes: 3 additions & 2 deletions src/installer/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -285,9 +285,10 @@ export async function runInstallerWithOptions(opts: RunInstallerOptions): Promis
// index a surprise directory (e.g. a shell sitting in $HOME). Same next step
// regardless of global/local scope.
clack.note(
location === 'local'
(location === 'local'
? 'codegraph init # build this project’s graph (one time; auto-syncs after)'
: 'cd <your-project>\ncodegraph init # build a project’s graph (one time; auto-syncs after)',
: 'cd <your-project>\ncodegraph init # build a project’s graph (one time; auto-syncs after)') +
'\n# (codegraph install --init does both steps in one command)',
'Next: index a project',
);

Expand Down