@kentwynn/kgraph 0.2.32 → 0.2.33

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,4 @@
1
- import { loadConfig, writeDefaultConfig } from '../../config/config.js';
1
+ import { loadConfig, saveConfig, writeDefaultConfig, } from '../../config/config.js';
2
2
  import { normalizeIntegrationNames } from '../../integrations/integration-registry.js';
3
3
  import { addIntegrations } from '../../integrations/integration-store.js';
4
4
  import { ensureKnowledgeStore } from '../../knowledge/atom-store.js';
@@ -6,9 +6,10 @@ import { scanRepository } from '../../scanner/repo-scanner.js';
6
6
  import { ensureWorkspace } from '../../storage/kgraph-paths.js';
7
7
  import { readMaps, writeMaps } from '../../storage/map-store.js';
8
8
  import { KGraphError, runCommand } from '../errors.js';
9
- import { promptForInitIntegrations, shouldPromptForInitIntegrations, } from '../init-prompt.js';
9
+ import { promptForInitIntegrations, promptScopeConfirmation, promptWorkspaceSetup, shouldPromptForInitIntegrations, } from '../init-prompt.js';
10
10
  import { detectMachineIntegrationRecommendations, recommendedIntegrationsForInit, } from '../init-recommendations.js';
11
11
  import { renderInitSummary } from '../init-summary.js';
12
+ import { countScopeFiles, detectWorkspaces } from '../workspace-detection.js';
12
13
  export function registerInitCommand(program) {
13
14
  program
14
15
  .command('init')
@@ -33,6 +34,26 @@ export function registerInitCommand(program) {
33
34
  console.log(`Configured integrations: ${changed.map((item) => `${item.name}:${item.mode}`).join(', ')}`);
34
35
  }
35
36
  let config = await loadConfig(workspace);
37
+ // Workspace detection — only for fresh init, non-destructive
38
+ const workspaceInfo = await detectWorkspaces(workspace.rootPath);
39
+ if (workspaceInfo && Object.keys(config.domainHints).length === 0) {
40
+ const result = await promptWorkspaceSetup(workspaceInfo);
41
+ if (result.applyDomains && result.domainHints) {
42
+ config = { ...config, domainHints: result.domainHints };
43
+ await saveConfig(workspace, config);
44
+ }
45
+ }
46
+ // Pre-scan scope check — fast file count before heavy scan
47
+ const fileCount = await countScopeFiles(workspace.rootPath, config);
48
+ const scopeResult = await promptScopeConfirmation(fileCount);
49
+ if (!scopeResult.proceed) {
50
+ console.log('Init cancelled. Edit .kgraph/config.yaml to adjust scope, then run `kgraph init` again.');
51
+ return;
52
+ }
53
+ if (scopeResult.narrowedInclude) {
54
+ config = { ...config, include: scopeResult.narrowedInclude };
55
+ await saveConfig(workspace, config);
56
+ }
36
57
  const previousMaps = await readMaps(workspace);
37
58
  const result = await scanRepository(workspace.rootPath, config, {
38
59
  files: previousMaps.fileMap.files,
@@ -1,8 +1,27 @@
1
- import type { IntegrationConfig, IntegrationName } from '../types/config.js';
1
+ import type { DomainHint, IntegrationConfig, IntegrationName } from '../types/config.js';
2
2
  import type { InitIntegrationRecommendation } from './init-recommendations.js';
3
+ import type { WorkspaceInfo } from './workspace-detection.js';
3
4
  export declare function shouldPromptForInitIntegrations(options: {
4
5
  explicitIntegrationsRequested: boolean;
5
6
  configuredIntegrations: Pick<IntegrationConfig, 'name'>[];
6
7
  interactive?: boolean;
7
8
  }): boolean;
8
9
  export declare function promptForInitIntegrations(recommendations: InitIntegrationRecommendation[]): Promise<IntegrationName[]>;
10
+ export interface ScopeConfirmResult {
11
+ proceed: boolean;
12
+ narrowedInclude?: string[];
13
+ }
14
+ /**
15
+ * If file count exceeds threshold, ask user whether to proceed or narrow scope.
16
+ * For small repos, returns { proceed: true } without prompting.
17
+ */
18
+ export declare function promptScopeConfirmation(fileCount: number, threshold?: number): Promise<ScopeConfirmResult>;
19
+ export interface WorkspacePromptResult {
20
+ applyDomains: boolean;
21
+ domainHints?: Record<string, DomainHint>;
22
+ }
23
+ /**
24
+ * If a monorepo workspace is detected, offer to configure domain hints.
25
+ * For simple projects (no workspace detected), skips silently.
26
+ */
27
+ export declare function promptWorkspaceSetup(info: WorkspaceInfo): Promise<WorkspacePromptResult>;
@@ -59,3 +59,60 @@ export async function promptForInitIntegrations(recommendations) {
59
59
  function isInteractiveTerminal() {
60
60
  return Boolean(process.stdin.isTTY) && Boolean(process.stdout.isTTY);
61
61
  }
62
+ /**
63
+ * If file count exceeds threshold, ask user whether to proceed or narrow scope.
64
+ * For small repos, returns { proceed: true } without prompting.
65
+ */
66
+ export async function promptScopeConfirmation(fileCount, threshold = 500) {
67
+ if (fileCount <= threshold || !isInteractiveTerminal()) {
68
+ return { proceed: true };
69
+ }
70
+ const action = await clack.select({
71
+ message: `Found ${fileCount.toLocaleString()} files in scope`,
72
+ options: [
73
+ { value: 'proceed', label: 'Continue with all files' },
74
+ {
75
+ value: 'narrow',
76
+ label: 'Narrow to src/ only',
77
+ hint: 'include: ["src/**"]',
78
+ },
79
+ { value: 'cancel', label: "Cancel — I'll edit config.yaml manually" },
80
+ ],
81
+ });
82
+ if (clack.isCancel(action) || action === 'cancel') {
83
+ return { proceed: false };
84
+ }
85
+ if (action === 'narrow') {
86
+ return { proceed: true, narrowedInclude: ['src/**'] };
87
+ }
88
+ return { proceed: true };
89
+ }
90
+ /**
91
+ * If a monorepo workspace is detected, offer to configure domain hints.
92
+ * For simple projects (no workspace detected), skips silently.
93
+ */
94
+ export async function promptWorkspaceSetup(info) {
95
+ if (!isInteractiveTerminal()) {
96
+ return { applyDomains: false };
97
+ }
98
+ const packageNames = info.packages.map((p) => p.name).join(', ');
99
+ const action = await clack.select({
100
+ message: `Detected ${info.tool} workspace (${info.packages.length} packages: ${packageNames})`,
101
+ options: [
102
+ {
103
+ value: 'apply',
104
+ label: 'Configure domain hints from packages',
105
+ hint: 'context packs will prefer the active package',
106
+ },
107
+ { value: 'skip', label: 'Skip — scan everything flat' },
108
+ ],
109
+ });
110
+ if (clack.isCancel(action) || action === 'skip') {
111
+ return { applyDomains: false };
112
+ }
113
+ const hints = {};
114
+ for (const pkg of info.packages) {
115
+ hints[pkg.name] = { paths: [pkg.path + '/**'] };
116
+ }
117
+ return { applyDomains: true, domainHints: hints };
118
+ }
@@ -0,0 +1,23 @@
1
+ import type { DomainHint, KGraphConfig } from '../types/config.js';
2
+ export interface WorkspaceInfo {
3
+ tool: string;
4
+ packages: WorkspacePackage[];
5
+ }
6
+ export interface WorkspacePackage {
7
+ name: string;
8
+ path: string;
9
+ }
10
+ /**
11
+ * Detect monorepo workspace tools and their packages.
12
+ * Returns null for simple single-package projects.
13
+ */
14
+ export declare function detectWorkspaces(rootPath: string): Promise<WorkspaceInfo | null>;
15
+ /**
16
+ * Convert detected workspace packages into domainHints.
17
+ */
18
+ export declare function workspacesToDomainHints(info: WorkspaceInfo): Record<string, DomainHint>;
19
+ /**
20
+ * Quick file count using fast-glob (no content read).
21
+ * Used to warn about large scopes before a full scan.
22
+ */
23
+ export declare function countScopeFiles(rootPath: string, config: KGraphConfig): Promise<number>;
@@ -0,0 +1,169 @@
1
+ import fg from 'fast-glob';
2
+ import { readFile } from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ import { buildFastGlobIgnore, readGitignorePatterns, } from '../scanner/file-classifier.js';
5
+ import { pathExists } from '../storage/kgraph-paths.js';
6
+ /**
7
+ * Detect monorepo workspace tools and their packages.
8
+ * Returns null for simple single-package projects.
9
+ */
10
+ export async function detectWorkspaces(rootPath) {
11
+ // pnpm
12
+ const pnpmPath = path.join(rootPath, 'pnpm-workspace.yaml');
13
+ if (await pathExists(pnpmPath)) {
14
+ const packages = await resolvePnpmPackages(rootPath, pnpmPath);
15
+ if (packages.length > 1)
16
+ return { tool: 'pnpm', packages };
17
+ }
18
+ // nx
19
+ const nxPath = path.join(rootPath, 'nx.json');
20
+ if (await pathExists(nxPath)) {
21
+ const packages = await resolveNxPackages(rootPath);
22
+ if (packages.length > 1)
23
+ return { tool: 'nx', packages };
24
+ }
25
+ // lerna
26
+ const lernaPath = path.join(rootPath, 'lerna.json');
27
+ if (await pathExists(lernaPath)) {
28
+ const packages = await resolveLernaPackages(rootPath, lernaPath);
29
+ if (packages.length > 1)
30
+ return { tool: 'lerna', packages };
31
+ }
32
+ // rush
33
+ const rushPath = path.join(rootPath, 'rush.json');
34
+ if (await pathExists(rushPath)) {
35
+ const packages = await resolveRushPackages(rootPath, rushPath);
36
+ if (packages.length > 1)
37
+ return { tool: 'rush', packages };
38
+ }
39
+ // npm/yarn workspaces (package.json)
40
+ const pkgPath = path.join(rootPath, 'package.json');
41
+ if (await pathExists(pkgPath)) {
42
+ const packages = await resolveNpmWorkspaces(rootPath, pkgPath);
43
+ if (packages.length > 1)
44
+ return { tool: 'npm', packages };
45
+ }
46
+ return null;
47
+ }
48
+ /**
49
+ * Convert detected workspace packages into domainHints.
50
+ */
51
+ export function workspacesToDomainHints(info) {
52
+ const hints = {};
53
+ for (const pkg of info.packages) {
54
+ hints[pkg.name] = { paths: [pkg.path + '/**'] };
55
+ }
56
+ return hints;
57
+ }
58
+ /**
59
+ * Quick file count using fast-glob (no content read).
60
+ * Used to warn about large scopes before a full scan.
61
+ */
62
+ export async function countScopeFiles(rootPath, config) {
63
+ const gitignorePatterns = await readGitignorePatterns(rootPath);
64
+ const allExcludes = [...config.exclude, ...gitignorePatterns];
65
+ const entries = await fg(config.include, {
66
+ cwd: rootPath,
67
+ dot: true,
68
+ onlyFiles: true,
69
+ unique: true,
70
+ ignore: buildFastGlobIgnore(allExcludes),
71
+ stats: false,
72
+ });
73
+ return entries.length;
74
+ }
75
+ // --- Resolvers ---
76
+ async function resolvePnpmPackages(rootPath, filePath) {
77
+ try {
78
+ const content = await readFile(filePath, 'utf8');
79
+ // Simple YAML parsing for packages: array
80
+ const lines = content.split(/\r?\n/);
81
+ const globs = [];
82
+ let inPackages = false;
83
+ for (const line of lines) {
84
+ if (/^packages:/i.test(line.trim())) {
85
+ inPackages = true;
86
+ continue;
87
+ }
88
+ if (inPackages) {
89
+ const match = line.match(/^\s+-\s+['"]?([^'"]+)['"]?\s*$/);
90
+ if (match) {
91
+ globs.push(match[1]);
92
+ }
93
+ else if (/^\S/.test(line) && line.trim().length > 0) {
94
+ break;
95
+ }
96
+ }
97
+ }
98
+ if (globs.length === 0)
99
+ globs.push('packages/*');
100
+ return await resolvePackageGlobs(rootPath, globs);
101
+ }
102
+ catch {
103
+ return [];
104
+ }
105
+ }
106
+ async function resolveNxPackages(rootPath) {
107
+ // Nx projects live in common dirs
108
+ const candidates = ['apps/*', 'libs/*', 'packages/*'];
109
+ return await resolvePackageGlobs(rootPath, candidates);
110
+ }
111
+ async function resolveLernaPackages(rootPath, filePath) {
112
+ try {
113
+ const content = await readFile(filePath, 'utf8');
114
+ const parsed = JSON.parse(content);
115
+ const globs = Array.isArray(parsed.packages)
116
+ ? parsed.packages
117
+ : ['packages/*'];
118
+ return await resolvePackageGlobs(rootPath, globs);
119
+ }
120
+ catch {
121
+ return [];
122
+ }
123
+ }
124
+ async function resolveRushPackages(rootPath, filePath) {
125
+ try {
126
+ const content = await readFile(filePath, 'utf8');
127
+ const parsed = JSON.parse(content);
128
+ if (!Array.isArray(parsed.projects))
129
+ return [];
130
+ return parsed.projects
131
+ .filter((p) => p.projectFolder)
132
+ .map((p) => ({
133
+ name: p.packageName || path.basename(p.projectFolder),
134
+ path: p.projectFolder,
135
+ }));
136
+ }
137
+ catch {
138
+ return [];
139
+ }
140
+ }
141
+ async function resolveNpmWorkspaces(rootPath, filePath) {
142
+ try {
143
+ const content = await readFile(filePath, 'utf8');
144
+ const parsed = JSON.parse(content);
145
+ const workspaces = parsed.workspaces;
146
+ if (!workspaces)
147
+ return [];
148
+ const globs = Array.isArray(workspaces)
149
+ ? workspaces
150
+ : (workspaces.packages ?? []);
151
+ if (globs.length === 0)
152
+ return [];
153
+ return await resolvePackageGlobs(rootPath, globs);
154
+ }
155
+ catch {
156
+ return [];
157
+ }
158
+ }
159
+ async function resolvePackageGlobs(rootPath, globs) {
160
+ const dirs = await fg(globs, {
161
+ cwd: rootPath,
162
+ onlyDirectories: true,
163
+ deep: 1,
164
+ });
165
+ return dirs.sort().map((dir) => ({
166
+ name: path.basename(dir),
167
+ path: dir.split(path.sep).join('/'),
168
+ }));
169
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kentwynn/kgraph",
3
- "version": "0.2.32",
3
+ "version": "0.2.33",
4
4
  "description": "Persistent repo intelligence for AI coding assistants.",
5
5
  "type": "module",
6
6
  "bin": {